<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>IT_STUDY</title>
    <link>https://it-study.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Thu, 25 Jun 2026 00:46:16 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>level_?</managingEditor>
    <item>
      <title>9장. 일관성과 합의</title>
      <link>https://it-study.tistory.com/130</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;일관성 보장&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;복제 데이터베이스는 대부분 최소한 최종적 일관성을 제공한다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;데이터베이스에 쓰기를 멈추고 불특정 시간 동안 기다리면 결국 모든 읽기 요청이 같은 값을 반환한다는 의미이다.&lt;/li&gt;
&lt;li&gt;바꿔 말하면 불일치는 일시적이며 결국 스스로 해소한다(네트워크 결함도 결국에는 복구된다고 가정)&lt;/li&gt;
&lt;li&gt;하지만 이것은 매우 약한 보장이다. 언제 복제본이 수렴될지에 대해서는 아무것도 얘기하지 않는다. 수렴될 때까지 읽기는 뭔가를 반환할 수도, 아무것도 반환하지 않을 수도 있다.&lt;/li&gt;
&lt;li&gt;약한 보장만 제공하는 데이터베이스를 다룰 때는 그 제한을 계속 알아야 하고 뜻하지 않게 너무 많은 것을 가정하면 안 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;분산 일관성은 대개 지연과 결함이 있더라도 복제본의 상태를 코디네이션하는 것과 관련되어 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;선형성&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;선형성을 뒷받침하는 아이디어
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;원자적 일관성(atomic consistency)&lt;/li&gt;
&lt;li&gt;강한 일관성(strong consistency)&lt;/li&gt;
&lt;li&gt;즉각 일관성(immediate consistency)&lt;/li&gt;
&lt;li&gt;외부 일관성(external consistency)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;선형성의 기본 아이디어는 시스템에 데이터 복사본이 하나만 있고 그 데이터를 대상으로 수행하는 모든 연산은 원자적인 것처럼 보이게 만드는 것이다.&lt;/li&gt;
&lt;li&gt;선형성 시스템에서는 클라이언트가 쓰기를 성공적으로 완료하자마자 그 데이터베이스를 읽는 모든 클라이언트는 방금 쓰여진 값을 볼 수 있어야 한다.&lt;/li&gt;
&lt;li&gt;선형성은 최신성 보장(recency guarantee)으로, 읽힌 값이 최근에 갱신된 값이며 뒤처진 캐시나 복제본에서 나온 값이 아니라고 보장해준다는 의미이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;726&quot; data-origin-height=&quot;362&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nJdBJ/btsQS44S52K/EP73wdOKeiQdMBtfX5Q7R0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nJdBJ/btsQS44S52K/EP73wdOKeiQdMBtfX5Q7R0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nJdBJ/btsQS44S52K/EP73wdOKeiQdMBtfX5Q7R0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnJdBJ%2FbtsQS44S52K%2FEP73wdOKeiQdMBtfX5Q7R0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;726&quot; height=&quot;362&quot; data-origin-width=&quot;726&quot; data-origin-height=&quot;362&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;잠금과 리더 선출&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단일 리더 복제를 사용하는 시스템은 리더가 여러 개(스플릿 브레인)가 아니라 진짜로 하나만 존재하도록 보장해야 한다.&lt;/li&gt;
&lt;li&gt;리더를 선출하는 한 가지 방법은 잠금을 사용하는 것으로, 모든 노드가 시작할 때 잠금 획득을 시도하고 성공한 노드가 리더가 된다.&lt;/li&gt;
&lt;li&gt;분산 잠금과 리더 선출을 구현하기 위해 아파치 주키퍼나 etcd 같은 코디네이션 서비스가 사용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;제약 조건과 유일성 보장&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;유일성 제약 조건은 데이터베이스에서 흔하다.(ex 사용자명, 이메일 주소)&lt;/li&gt;
&lt;li&gt;데이터가 기록될 때 이 제약 조건을 강제하고 싶다면 선형성이 필요하다.&lt;/li&gt;
&lt;li&gt;실제 애플리케이션에서는 이런 제약 조건을 느슨하게 다뤄도 되지만, 관계형 데이터베이스에서 전형적으로 볼 수 있는 엄격한 유일성 제약 조건은 선형성이 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;채널 간 타이밍 의존성&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;724&quot; data-origin-height=&quot;251&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b2Ezg9/btsQSQFxkL3/aU4GpuG0OMFGuS5dKDI0xK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b2Ezg9/btsQSQFxkL3/aU4GpuG0OMFGuS5dKDI0xK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b2Ezg9/btsQSQFxkL3/aU4GpuG0OMFGuS5dKDI0xK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb2Ezg9%2FbtsQSQFxkL3%2FaU4GpuG0OMFGuS5dKDI0xK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;724&quot; height=&quot;251&quot; data-origin-width=&quot;724&quot; data-origin-height=&quot;251&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;선형성의 최신성 보장이 없으면 채널 사이의 경쟁 조건이 발생할 수 있다.&lt;/li&gt;
&lt;li&gt;선형성이 경쟁 조건을 회피하는 유일한 방법은 아니지만 이해하기에 가장 단순하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;선형성 시스템 구현하기&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 간단한 방법은 데이터 복사본을 하나만 사용하는 것이지만, 이 방법으로는 결함을 견뎌낼 수 없다.&lt;/li&gt;
&lt;li&gt;시스템이 내결함성을 지니도록 만드는 가장 흔한 방법은 복제를 사용하는 것이다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;단일 리더 복제(선형적이 될 가능성이 있음) : 리더는 쓰기에 사용되는 데이터의 주 복사본을 갖고 있고 팔로워는 다른 노드에 데이터의 백업 복사본을 보관한다. 리더나 동기식으로 갱신된 팔로워에서 실행한 읽기는 선형적이 될 가능성이 있다.&lt;/li&gt;
&lt;li&gt;합의 알고리즘(선형적) : 합의 프로토콜에서는 스플릿 브레인과 복제본이 뒤처지는 문제를 막을 수단이 포함되어있어 선형성 저장소를 안전하게 구현할 수 있다.&lt;/li&gt;
&lt;li&gt;다중 리더 복제(비선형적) : 여러 노드에서 동시에 쓰기를 처리하고 비동기로 다른 노드에 복제하기 때문에 일반적으로 선형적이지 않다.&lt;/li&gt;
&lt;li&gt;리더 없는 복제(아마도 비선형적) : 정족수의 정확한 설정과 엄격한 일관성을 어떻게 정의하냐에 따라 비선형적으로 동작할 가능성이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;선형성과 정족수&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;723&quot; data-origin-height=&quot;413&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cGugqc/btsQUf5FmcM/NyOnkAPrkGmkRXOpBUlOE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cGugqc/btsQUf5FmcM/NyOnkAPrkGmkRXOpBUlOE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cGugqc/btsQUf5FmcM/NyOnkAPrkGmkRXOpBUlOE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcGugqc%2FbtsQUf5FmcM%2FNyOnkAPrkGmkRXOpBUlOE1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;723&quot; height=&quot;413&quot; data-origin-width=&quot;723&quot; data-origin-height=&quot;413&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;엄격한 정족수를 사용한 읽기쓰기는 선형적인 것처럼 보이지만 네트워크 지연의 변동이 심하면 경쟁 조건이 생길 수 있다.&lt;/li&gt;
&lt;li&gt;정족수 조건이 만족(w + r &amp;gt; n)됨에도 실행은 선형적이지 않을 수 있다.&lt;/li&gt;
&lt;li&gt;성능 저하 비용을 지불하면 다이나모 스타일 정족수를 선형적으로 만드는게 가능하다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;읽기 클라이언트는 결과를 애플리케이션에 반환하기 전에 읽기 복구를 동기식으로 수행해야 한다.&lt;/li&gt;
&lt;li&gt;쓰기 클라이언트는 쓰기 요청을 보내기 전에 노드들의 정족수로부터 최신 상태를 읽어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;다이나모 스타일 복제를 하는 리더 없는 시스템은 선형성을 제동하지 않는다고 보는게 안전하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;선형성의 비용&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다중 리더 복제
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;다중 데이터 센터는 데이터 센터 간 네트워크 연결이 끊겨도, 각 데이터 센터는 정상 동작하고 복제는 큐에 쌓였다가 네트워크 연결이 복귀되면 다른 데이터 센터로 전달된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단일 리더 복제
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;리더가 데이터센터 중 하나에만 있어야 하고 모든 쓰기와 선형성 읽기는 리더로 보내져야 한다.&lt;/li&gt;
&lt;li&gt;데이터센터 사이의 네트워크가 끊기면 팔로워 데이터센터로 접속한 클라이언트들은 리더로 연결할 수 없으므로 쓰기와 읽기가 불가능하다. 팔로워로부터 읽을 수는 있지만 데이터가 뒤처졌을 수 있다.(비선형적)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;CAP 정리&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일관성(Consistency), 가용성(Availability), 분단 내성(Partition tolerance)&lt;/li&gt;
&lt;li&gt;트레이드 오프
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;애플리케이션에서 선형성을 요구하고 네트워크 문제 때문에 일부 복제 서버가 다른 복제 서버와 연결이 끊기면 일부 복제 서버는 연결이 끊긴 동안은 요청을 처리할 수 없다. 네트워크 문제가 고쳐질 때까지 기다리거나 오류를 반환해야 한다(가용성이 없다)&lt;/li&gt;
&lt;li&gt;애플리케이션에서 선형성을 요구하지 않는다면 각 복제 서버가 다른 복제 서버와 연결이 끊기더라도 독립적으로 요청을 처리하는 방식으로 쓰기를 처리할 수 있다. 네트워크 문제가 발생해도 가용한 상태를 유지하지만 선형적이지 않다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;하나의 일관성 모델(선형성)과 한 종류의 결함(네트워크 분단 혹은 노드가 살아 있지만 서로 연결이 끊긴 상황)만 고려한다.&lt;/li&gt;
&lt;li&gt;네트워크 분단이 생겼을 때 일관성과 가용성 중 하나를 선택하라는 의미이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;선형성과 네트워크 지연&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;선형성 보장을 제공하지 않는 이유는 내결함성이 아니라 성능 때문이다.(선형성은 느림)&lt;/li&gt;
&lt;li&gt;선형성을 제공하는 더울 빠른 알고리즘은 존재하지 않지만 완화된 일관성 모델은 훨씬 더 빠를 수 있다. 따라서 지연 시간에 민감한 시스템에서는 이 트레이드오프가 중요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;순서화 보장&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;순서화, 선형서으 합의 사이에는 깊은 연결 관계가 존재한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;순서와 인과성&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;순서화는 인과성을 보존하는 데 도움을 준다.&lt;/li&gt;
&lt;li&gt;인과성은 이벤트에 순서를 부과한다. 결과가 나타나기 전에 원인이 발생한다.&lt;/li&gt;
&lt;li&gt;인과적으로 의존하는 연산의 연쇄는 시스템에서 인과적 순서, 즉 무엇이 무엇보다 먼저 일어났는가를 정의한다.&lt;/li&gt;
&lt;li&gt;시스템이 인과성에 의해 부과된 순서를 지키면 그 시스템은 인과적으로 일관적(causally consistent)이라고 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;인과적 순서가 전체 순서는 아니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전체 순서는 어떤 두 요소를 비교할 수 있게 하므로 두 요소가 있으면 항상 어떤 것이 더 크고 어떤 것이 더 작은지 말할 수 있다.&lt;/li&gt;
&lt;li&gt;부분 순서는 비교가 불가능하고 부분적으로 순서가 정해진다.&lt;/li&gt;
&lt;li&gt;데이터베이스 일관성 모델
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;선형성 : 선형성 시스템에서는 연산의 전체 순서를 정할 수 있다.&lt;/li&gt;
&lt;li&gt;인과성 : 두 이벤트가 인과적인 관계가 있으면 이들은 순서가 있지만, 동시에 실행되면 비교할 수 없다. 인과성은 전체 순서가 아닌 부분 순서를 정의한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;선형성 데이터스토어에는 동시적 연산이 없다. 하나의 타임라인이 있고 모든 연산은 그 타임라인을 따라서 전체 순서가 정해져야 한다.&lt;/li&gt;
&lt;li&gt;동시성은 타임라인이 갈라졌다 다시 합쳐지는 것을 의미하며, 다른 가지에 있는 연산은 비교 불가(동시적)하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;선형성은 인과적 일관성보다 강하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;선형성은 인과성을 내포한다.&lt;/li&gt;
&lt;li&gt;어떤 시스템이든지 선형적이라면 인과성도 올바르게 유지한다.&lt;/li&gt;
&lt;li&gt;하지만 네트워크 지연이 크면 선형성은 성능과 가용성에 해가 될 수 있다.&lt;/li&gt;
&lt;li&gt;선형성은 인과성을 보존하는 유일한 방법이 아니다. 시스템은 선형적으로 만드는 성능 손해를 유발하지 않고도 인과적 일관성을 만족시킬 수 있다.&lt;/li&gt;
&lt;li&gt;많은 경우에 선형성이 필요한 것처럼 보이는 시스템에 진짜로 필요한 것은 인과적 일관성이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;인과적 의존성 담기&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비선형 시스템이 인과적 일관성을 유지하는 방법
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;인과성 유지를 위해서는 어떤 연산이 어떤 다른 연산보다 먼저 실행됐는지 알아야 한다.(부분 순서)&lt;/li&gt;
&lt;li&gt;동시에 실행되는 연산은 어떤 순서로든 처리될 수 있지만 한 연산이 다른 연산보다 먼저 실행됐다면 모든 복제 서버는 그 순서로 처리되어야 한다.&lt;/li&gt;
&lt;li&gt;복제 서버가 연산을 처리할 때 인과적으로 앞서는 모든 연산이 이미 처리됐다고 보장할 수 있어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;인과적 의존성을 결정하려면 시스템에 있는 노드에 관한 지식을 기술할 방법이 필요하다.(어떤 연산이 다른 연산보다 먼저 실행됐는지 결정하는 기법)&lt;/li&gt;
&lt;li&gt;인과적 순서를 결정하기 위해 데이터베이스는 애플리케이션이 데이터의 어떤 버전을 읽었는지 알아야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;일련번호 순서화&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인과성은 중요한 이론적 개념이지만 모든 인과적 의존성을 실제로 추적하는 것은 실용성이 떨어진다.&lt;/li&gt;
&lt;li&gt;읽은 데이터를 모두 명시적으로 추적하는 것은 오버헤드가 크다.&lt;/li&gt;
&lt;li&gt;일련번호나 타임스탬프를 써서 이벤트의 순서를 정하는 방법이 더 좋다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일련번호나 타임스탬프는 크기가 작고 전체 순서를 제공한다.&lt;/li&gt;
&lt;li&gt;즉 모든 연산은 고유 일련번호를 갖고 항상 두 개의 일련번호를 비교해서 어떤 것이 큰지 결정할 수 있다.&lt;/li&gt;
&lt;li&gt;인과성에 일관적인 전체 순서대로 일련번호를 생성할 수 있다.&lt;/li&gt;
&lt;li&gt;ex) 단일 리더 복제 : 리더는 연산마다 카운터를 증가시키고 복제 로그의 각 연산에 단조 증가하는 일련번호를 할당 -&amp;gt; 팔로워가 복제 로그에 나오는 순서대로 쓰기 적용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;비인과적 일련번호 생성기&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단일 리더가 없는 경우(다중 리더, 리더 없는 데이터베이스, 데이터베이스 파티셔닝)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;각 노드가 자신만의 독립적인 일련번호 집합을 생성&lt;/li&gt;
&lt;li&gt;각 연산에 일 기준 시계(물리적 시계)에서 얻은 타임스탬프를 붙임&lt;/li&gt;
&lt;li&gt;일련번호 블록을 미리 할당(A - 1~1000, B - 1001 ~ 2000)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;카운터를 증가시키는 단일 리더에 모든 연산을 밀어넣는 것보다 확장성이 좋다.&lt;/li&gt;
&lt;li&gt;하지만 생성한 일련번호가 인과성에 일관적이지 않다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;각 노드는 초당 연산수가 다를 수 있어서 홀수 연산과 짝수 연산이 있을 때 어떤 것이 인과적으로 먼저 실행됐는지 알 수 없다.&lt;/li&gt;
&lt;li&gt;시계 스큐에 종속적이어서 인과성에 일관적이지 않게 될 수 있다.(나중에 실행된 연상이 더 낮은 타임스탬프 배정)&lt;/li&gt;
&lt;li&gt;블록 할당자의 경우 나중에 실행되는 연산이 1~1000 사이의 구간에서 일련번호를 받을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;램포트 타임스탬프&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;723&quot; data-origin-height=&quot;316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvr0o8/btsQR3k6HiM/cRiIguFkSSqlwm9fE4bNok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvr0o8/btsQR3k6HiM/cRiIguFkSSqlwm9fE4bNok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvr0o8/btsQR3k6HiM/cRiIguFkSSqlwm9fE4bNok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcvr0o8%2FbtsQR3k6HiM%2FcRiIguFkSSqlwm9fE4bNok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;723&quot; height=&quot;316&quot; data-origin-width=&quot;723&quot; data-origin-height=&quot;316&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인과성에 일관적인 일련번호를 생성하는 간단한 방법&lt;/li&gt;
&lt;li&gt;각 노드는 고유 식별자를 갖고 각 노드는 처리한 연산 개수를 카운터로 유지한다.&lt;/li&gt;
&lt;li&gt;(카운터, 노드ID)의 쌍이다.&lt;/li&gt;
&lt;li&gt;물리적 일 기준 시계와 아무 관련이 없지만 전체 순서화를 제공한다.&lt;/li&gt;
&lt;li&gt;두 타임스탬프가 있으면 카운터가 큰 것이 타임스탬프가 크다. 카운터 값이 같으면 노드 ID가 큰 것이 타임스탬프가 크다.&lt;/li&gt;
&lt;li&gt;모든 노드와 모든 클라이언트가 지금까지 본 카운터 값 중 최댓값을 추적하고 모든 요청에 그 최댓값을 포함시킨다.&lt;/li&gt;
&lt;li&gt;노드가 자신의 카운터 값보다 큰 최대 카운터를 가진 요청이나 응답을 받으면 바로 자신의 카운터를 그 최댓값으로 증가시킨다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;타임스탬프 순서화로는 충분하지 않다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;램포트 타임스탬프가 인과성에 일관적인 연산의 전체 순서를 정의하지만 분산 시스템의 여러 공통 문제를 해결하는 데 충분하지는 않다.&lt;/li&gt;
&lt;li&gt;예를들어 사용자명에 대하 유일성 제약 조건 같은 것을 구현하려면 연산의 전체 순서가 있는 것으로는 충분하지 않다. 언제 그 순서가 확정되는지도 알아야 한다.&lt;/li&gt;
&lt;li&gt;연산의 전체 순서는 모든 연산을 모은 후에야 드러난다. 다른 노드가 어떤 연산을 생성했지만 그것이 무엇인지 아직 알 수 없다면 연산의 최종 순서를 만들어낼 수 없다.&lt;/li&gt;
&lt;li&gt;언제 전체 순서가 확정되는지 알아야 한다. =&amp;gt; 전체 순서 브로드캐스트&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;전체 순서 브로드캐스트(total order broadcast)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;노드 사이에 메시지를 교환하는 프로토콜로 기술된다.&lt;/li&gt;
&lt;li&gt;두 가지 안전성 속성을 항상 만족해야 한다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;신뢰성 있는 전달(reliable delivery) : 어떤 메시지도 손실되지 않는다. 메시지가 한 노드에 전달되면 모든 노드에도 전달된다.&lt;/li&gt;
&lt;li&gt;전체 순서가 정해진 전달(totally ordered delivery) : 메시지는 모든 노드에 같은 순서로 전달된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;전체 순서 브로드캐스트를 구현하는 알고리즘은 노드나 네트워크에 결함이 있더라도 신뢰성과 순서화 속성이 항상 만족되도록 보장해야 한다.&lt;/li&gt;
&lt;li&gt;네트워크가 끊긴 동안 메시지가 전달될 수 없지만 알고리즘이 재시도를 계속해서 네트워크가 복구되면 메시지가 전달되게 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;전체 순서 브로드캐스트 사용하기&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주키퍼나 etcd 같은 합의 서비스는 전체 순서 브로드캐스트를 실제로 구현한다.&lt;/li&gt;
&lt;li&gt;데이터베이스 복제에 딱 필요한 것이다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;상태 기계 복제(state machine replication)&lt;/li&gt;
&lt;li&gt;모든 메시지가 데이터베이스에 쓰기를 나타내고 모든 복제 서버가 같은 쓰기 연산을 순서로 처리하면 복제 서버들은 서로 일관성 있는 상태를 유지한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;직렬성 트랜잭션을 구현하는 데도 사용된다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;모든 메시지가 스토어드 프로시저로 실행되는 결정적 트랜잭션을 나타낸다면, 그리고 모든 노드가 그 메시지들을 같은 순서로 처리한다면 데이터베이스의 파티션과 복제본은 서로 일관적인 상태를 유지한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;메시지가 전달되는 시점에 그 순서가 고정된다.(타임스탬프 순서화보다 강하다)&lt;/li&gt;
&lt;li&gt;로그를 만드는 방법 중 하나이다.(복제 로그, 트랜잭션 로그, 쓰기 전 로그)&lt;/li&gt;
&lt;li&gt;펜싱 토큰을 제공하는 잠금 서비스를 구현하는 데도 유용하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;전체 순서 브로드캐스트를 사용해 선형성 저장소 구현하기&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;선형성 시스템과 전체 순서 브로드캐스트 사이에는 밀접한 관계가 있다.&lt;/li&gt;
&lt;li&gt;전체 순서 브로드캐스트
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;비동기식&lt;/li&gt;
&lt;li&gt;메시지는 고정된 순서로 신뢰성 있게 전달되도록 보장되지만 언제 메시지가 전달될지는 보장하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;선형성 시스템
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;최신성 보장&lt;/li&gt;
&lt;li&gt;읽기가 최근에 쓰여진 값을 보는게 보장된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;전체 순서 브로드캐스트 구현이 있다면 이를 기반으로 한 선형성 저장소를 만들 수 있다.&lt;/li&gt;
&lt;li&gt;사용자 계정의 사용자명으로 식별 -&amp;gt; 사용 가능한 모든 사용자명마다 원자적 compare-and-set 연산이 구현된 선형성 저장소&lt;br /&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;메시지를 로그에 추가해서 점유하기 원하는 사용자명을 시험적으로 가리킨다.&lt;/li&gt;
&lt;li&gt;로그를 읽고, 추가한 메시지가 되돌아오기를 기다린다.&lt;/li&gt;
&lt;li&gt;원하는 사용자명을 점유하려고 하는 메시지가 있는지 확인한다. 자신의 메시지가 첫번째면 성공, 아니면 어보트한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;로그 항목은 모든 노드에 같은 순서로 전달되므로 여러 개의 쓰기가 동시에 실행되면 모든 노드가 어떤 쓰기가 먼저 실행될 것인지 동의한다.&lt;/li&gt;
&lt;li&gt;충돌하는 쓰기 중 첫 번째 것을 승자로 택하고 나머지를 어보트시키면 모든 노드는 쓰기가 커밋되거나 어보트되는지에 동의하게 된다.&lt;/li&gt;
&lt;li&gt;이 절차는 선형성 쓰기를 보장하지만 선형성 읽기는 보장하지 않는다. 로그로부터 비동기로 갱신되는 저장소를 읽으면 오래된 값이 읽힐 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;선형성 저장소를 사용해 전체 순서 브로드캐스트 구현하기&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;정수를 저장하고 원자적 increment-and-set 연산이 지원되는 선형성 레지스터가 있다고 가정&lt;/li&gt;
&lt;li&gt;전체 순서 브로그캐스트를 통해 보내고 싶은 모든 메시지에 대해 선형성 정수로 increment-and-get 연산을 수행하고 레지스터에서 얻은 값을 일련번호로 메시지에 붙힌다.&lt;/li&gt;
&lt;li&gt;그 후 메시지를 모든 노드에 보낼 수 있고 수신자들은 일련번호 순서대로 메시지를 전달한다.&lt;/li&gt;
&lt;li&gt;전체 순서 브로드캐스트와 타임스탬프 순서화의 핵심 차이
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;선형성 레지스터를 증가시켜서 얻은 숫자들은 틈이 없는 순열을 형성&lt;/li&gt;
&lt;li&gt;따라서 어떤 노드가 메시지 4를 전달하고 일련번호가 6인 메시지를 받았다면 메시지 6을 전달하기 전에 메시지 5를 기다려야 한다는 것을 알 수 있다.&lt;/li&gt;
&lt;li&gt;램포트 타임스탬프는 그렇지 않다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;선형성 compare-and-set(또는 increment-and-get) 레지스터와 전체 순서 브로드캐스트는 둘 다 합의와 동등하다고 증명할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;분산 트랜잭션과 합의&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;합의의 목적은 단지 여러 노드들이 뭔가에 동의하게 만드는 것이다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;리더 선출 : 모든 노드는 어떤 노드가 리더인지 동의해야 한다.&lt;/li&gt;
&lt;li&gt;원자적 커밋 : 여러 노드나 파티션에 걸친 트랜잭션을 지원하는 데이터베이스에서는 모든 노드가 트랜잭션의 결과에 동의하게 만들어야 한다.(모두 어보트/롤백되거나 모두 커밋)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;원자적 커밋과 2단계 커밋(2PC)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;단일 노드에서 분산 원자적 커밋으로&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단일 노드
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;트랜잭션 커밋은 데이터가 디스크에 지속성 있게 쓰여지는 순서에 결정적으로 의존한다.&lt;/li&gt;
&lt;li&gt;데이터가 먼저고 커밋 레코드는 그 다음이다.&lt;/li&gt;
&lt;li&gt;트랜잭션이 커밋되거나 어보트되는지를 결정하는 핵심적인 시점은 디스크가 커밋 레스트 쓰기를 마치는 시점이다.&lt;/li&gt;
&lt;li&gt;커밋을 원자적으로 만들어주는 것은 단일 장치(특정한 하나의 노드에 부탁된 하나의 특정 디스크 드라이브의 컨트롤러)다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;다중 노드
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;어떤 노드에서는 커밋이 성공하고 다른 노드에서는 실패해서 원자성 보장을 위반하기 쉽다.&lt;/li&gt;
&lt;li&gt;어떤 노드는 트랜잭션을 커밋하지만 다른 노드는 어보트한다면 노드들이 서로 일관성이 없어진다.&lt;/li&gt;
&lt;li&gt;노드는 트랜잭션에 참여하는 다른 모든 노드도 커밋될 것이라고 확신할 때만 커밋 돼야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2단계 커밋 소개&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;763&quot; data-origin-height=&quot;265&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nHfXM/btsQRxNSM0S/luqyPOPuva8aHZvvPhubKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nHfXM/btsQRxNSM0S/luqyPOPuva8aHZvvPhubKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nHfXM/btsQRxNSM0S/luqyPOPuva8aHZvvPhubKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnHfXM%2FbtsQRxNSM0S%2FluqyPOPuva8aHZvvPhubKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;763&quot; height=&quot;265&quot; data-origin-width=&quot;763&quot; data-origin-height=&quot;265&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 노드에 걸친 원자적 트랜잭션 커밋을 달성하는, 즉 모든 노드가 커밋되거나 모든 노드가 어보트되도록 보장하는 알고리즘이다.&lt;/li&gt;
&lt;li&gt;단일 노드 트랜잭션에서는 보통 존재하지 않는 새로운 컴포넌트인 코디네이터(coordinator, 트랜잭션 관리자)를 사용한다.&lt;/li&gt;
&lt;li&gt;코디네이터는 종종 트랜잭션을 요청하는 애플리케이션 프로세스 내에서 라이브러리 형태(자바 EE 컨테이너에 내장)로 구현되지만 분리된 프로세스나 서비스가 될 수 있다.(Narayana, JOTM, BTM, MSDTC)&lt;/li&gt;
&lt;li&gt;과정
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;애플리케이션이 여러 데이터베이스 노드(트랜잭션 참여자, participant)에서 데이터를 읽고 쓰기 시작&lt;/li&gt;
&lt;li&gt;애플리케이션이 커밋할 준비가 되면 코디네이터가 1단계를 시작한다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;각 노드에 준비 요청을 보내서 커밋할 수 있는지 물어본다.&lt;/li&gt;
&lt;li&gt;그 후 코디네이터는 참여자들의 응답을 추적한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;모든 참여자가 커밋할 준비가 됐다고 응답하면 코디네이터는 2단계에서 커밋 요청을 보내고 커밋이 실제로 일어난다.&lt;/li&gt;
&lt;li&gt;만약 참여자 중 누구라도 아니오로 응답하면 코디네이터는 2단계에서 모든 노드에 어보트 요청을 보낸다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;약속에 관한 시스템&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션은 분산 트랜잭션을 시작하기 원할 때 코디네이터에게 트랜잭션 ID를 요청하고, 이는 전역적으로 유일하다.&lt;/li&gt;
&lt;li&gt;애플리케이션은 각 참여자에게 단일 노드 트랜잭션을 시작하고, 단일 노드 트랜잭션에 전역으로 유일한 트랜잭션 ID를 붙인다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;모든 읽기와 쓰기는 단일 노드 트랜잭션 중 하나로 실행된다.&lt;/li&gt;
&lt;li&gt;이 단계에서 잘못되면 코디네이터나 참여자 중 누군가 어보트할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;애플리케이션이 커밋할 준비가 되면 코디네이터는 모든 참여자에게 전역 트랜잭션 ID로 태깅된 준비 요청을 보낸다. 요청 중 실패하거나 타임아웃이 될 경우 코디네이터는 모든 참여자에게 어보트 요청을 보낸다.&lt;/li&gt;
&lt;li&gt;참여자가 준비 요청을 받으면 모든 상황에서 트랜잭션을 커밋할 수 있는지 확인한다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;트랜잭션 데이터를 쓰는 것과 충돌, 제약조건 위반 등을 확인&lt;/li&gt;
&lt;li&gt;코디네이터에게 '네'라고 응답함으로써 노드에 요청이 있으면 트랜잭션을 오류없이 커밋할 것이라고 약속&lt;/li&gt;
&lt;li&gt;트랜잭션을 어보트할 권리를 포기하지만 실제로 커밋하지는 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;코디네이터가 모든 준비 요청에 응답을 받았을 때 최종적으로 커밋/어보트 결정한다. 코디네이터가 추후 죽는 경우에 어떻게 결정했는지를 알 수 있도록 그 결정을 디스트에 있는 트랜잭션 로그에 기록해야 한다.(=커밋 포인트)&lt;/li&gt;
&lt;li&gt;코디네이터 결정이 디스크에 쓰여지면 모든 참여자에게 커밋/어보트 요청이 전송되고, 요청이 실패하거나 타임아웃이 되면 코디네이터는 성공할 때까지 영원히 재시도 한다.&lt;/li&gt;
&lt;li&gt;코디네이터가 한 번 결정하면 그 결정은 변경할 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;코디네이터 장애&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코디네이터가 준비 요청을 보내기 전에 장애가 나면 참여자가 안전하게 트랜잭션을 어보트할 수 있다.&lt;/li&gt;
&lt;li&gt;참여자가 준비 요청을 받고 &quot;네&quot; 응답을 보낸 이후에는 더 이상 일방적으로 어보트할 수 없다.&lt;/li&gt;
&lt;li&gt;코디네이터로부터 트랜잭션이 커밋됐는지 어보트됐는지 회신 받을 때까지 기다려야 하는데, 이 때 코디네이터가 죽거나 네트워크 장애가 발생하면 참여자는 기다릴 수밖에 없다.&lt;/li&gt;
&lt;li&gt;이 상태에 있는 참여자의 트랜잭션을 의심스럽다(in doubt) 또는 불확실하다(uncertain)고 한다.&lt;/li&gt;
&lt;li&gt;2PC를 완료할 수 있는 유일한 방법은 코디네이터의 복구이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;3단계 커밋&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2단계 커밋은 2PC가 코디네이터가 복구하기를 기다리느라 멈출 수 있다는 사실 때문에 블로킹 원자적 커밋 프로토콜이라고 불린다.&lt;/li&gt;
&lt;li&gt;3단계 커밋 알고리즘
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;2PC의 대안&lt;/li&gt;
&lt;li&gt;&amp;nbsp;지연에 제한이 있는 네트워크와 응답 시간에 제한이 있는 노드를 가정&lt;/li&gt;
&lt;li&gt;기약 없는 네트워크 지연과 프로세스 중단이 있는 대부분의 실용적 시스템에서 3PC는 원자성을 보장하지 못한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;논블로킹 원자적 커밋
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;완벽한 장애 감지기, 즉 노드가 죽었는지 아닌지 구별할 수 있는 신뢰성 있는 메커니즘이 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;현실의 분산 트랜잭션&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2단계 커밋으로 구현된 분산 트랜잭션은 안전성 보장을 제공하지만, 운영상의 문제를 일으키고 성능을 떨어뜨린다는 평가를 받는다.&lt;/li&gt;
&lt;li&gt;어떤 분산 트랜잭션 구현은 무거운 성능 손해를 수반한다.(ex MySQL의 분산 트랜잭션)&lt;/li&gt;
&lt;li&gt;분산 트랜잭션 종류
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;데이터베이스 내부 분산 트랜잭션: 데이터베이스 노드 사이에 내부 트랜잭션을 지원한다. 트랜잭션에 참여하는 모든 모드는 동일한 데이터에비스 소프트웨어를 실행한다.&lt;/li&gt;
&lt;li&gt;이종 분산 트랜잭션: 서로 다른 벤더의 데이터베이스이거나 메시지 브로커처럼 비데이터베이스 시스템 간의 트랜잭션으로 시스템의 내부가 완전히 다르더라도 원자적 커밋을 보장해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;정확히 한 번 메시지 처리&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이종 분산 트랜잭션은 다양한 시스템들이 강력한 방법으로 통합될 수 있게 한다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;메시지 큐에서 나온 메시지는 그 메시지를 처리하는 데이터베이스 트랜잭션이 커밋에 성공했을 때만 처리된 것으로 확인받을 수 있다.&lt;/li&gt;
&lt;li&gt;메시지 확인과 데이터베이스 쓰기를 단일 트랜잭션에서 원자적으로 커밋함으로써 구현할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이런 분산 트랜잭션은 트랜잭션의 영향을 받는 모든 시스템이 동일한 원자적 커밋 프로토콜을 사용할 수 있을 때만 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;XA 트랜잭션&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;X/Open XA(eXtended Architecture)는 이종 기술에 걸친 2단계 커밋을 구현하는 표준이다.&lt;/li&gt;
&lt;li&gt;XA는 네트워크 프로토콜이 아니라 트랜잭션 코디네이터와 연결되는 인터페이스를 제공하는 C API일 뿐이다.&lt;/li&gt;
&lt;li&gt;XA는 애플리케이션이 네트워크 드라이브나 클라이언트 라이브러리를 사용해 참여자 데이터베이스나 메시징 서비스와 통신한다고 가정한다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;드라이버가 XA를 지원한다는 것은 연산이 분산 트랜잭션의 일부가 돼야 하는지 알아내기 위해 XA API를 호출한다는 의미이다.&lt;/li&gt;
&lt;li&gt;드라이버는 데이터베이스 서버로 필요한 정보를 보낸다.&lt;/li&gt;
&lt;li&gt;드라이버는 코디네이터가 참여자에게 준비, 커밋, 어보트를 요청할 수 있는 콜백을 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;트랜잭션 코디네이터는 XA API를 구현한다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;현실에서는 트랜잭션을 시작하는 애플리케이션과 같은 프로세스에 로딩되는 단순란 라이브러리다.&lt;/li&gt;
&lt;li&gt;트랜잭션의 참여자를 추적하고 참여자들에게 준비 요청을 보낸 후 그들의 응답을 수집하고 각 트랜잭션에 대한 커밋/어보트 결정을 추적하기 위해 로컬 디스크에 있는 로그를 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;애플리케이션 프로세스가 죽거나 애플리케이션이 실행 중인 장비가 죽으면 코디네이터도 함께 사라진다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;준비됐지만 커밋되지 않은 트랜잭션들을 가진 참여자들은 의심스러운 상태에 빠진다.&lt;/li&gt;
&lt;li&gt;서버가 재시작되면 코디네이터 라이브러리가 로그를 읽어서 각 트랜잭션의 커밋/어보트 결과를 복구해야 한다.&lt;/li&gt;
&lt;li&gt;데이터베이스의 드라이버의 XA 콜백을 사용해 요청할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;의심스로운 상태에 있는 동안 잠금을 유지하는 문제&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터베이스 트랜잭션은 보통 더티 쓰기를 막기 위해 그들이 변경한 로우에 로우 수준의 독자적인 잠금을 획득하고, 추가로 직렬성 격리를 원한다면 2단계 잠금을 사용하는 데이터베이스는 트랜잭션에서 읽은 로우에 공유 잠금도 획인해야 한다.&lt;/li&gt;
&lt;li&gt;데이터베이스는 트랜잭션이 커밋하거나 어보트할 때까지 이런 잠금을 해제할 수 없다.&lt;/li&gt;
&lt;li&gt;2단계 커밋을 사용할 때 트랜잭션은 의심스러운 상태에 잇는 동안 내내 잠금을 잡고 있어야 한다.&lt;/li&gt;
&lt;li&gt;이런 잠금이 유지되는 동안 다른 어떤 트랜잭션도 그 로우를 변경할 수 없어 일을 처리할 수 없다.&lt;/li&gt;
&lt;li&gt;의심스러운 트랜잭션이 해소될 때까지 성능 이슈가 발생하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;코디네이터 장애에서 복구하기&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이론상 코디네이터가 죽은 후 재시작하면 로그로부터 그 상태를 깨끗하게 복구하고 의심스러운 트랜잭션을 해소해야 한다.&lt;/li&gt;
&lt;li&gt;현실에서는 코디네이터가 어떤 이유 때문이지 그 결과를 결정할 수 없는 고아가 된 의심스러운 트랜잭션이 생길 수 있다.&lt;/li&gt;
&lt;li&gt;이런 트랜잭션은 자동으로 해소될 수 없어서 잠금을 유지하고 다른 트랜잭션을 차단하면서 데이터베이스에 영원히 남는다.&lt;/li&gt;
&lt;li&gt;관리자가 수동으로 커밋하거나 롤백할지 결정해야하는 것으로 해결할 수 있다.&lt;/li&gt;
&lt;li&gt;여러 XA 구현에는 참여자가 코디네이터로부터 확정적 겨정을 얻지 않고 의심스러운 트랜잭션을 어보트하거나 커밋할지를 일방적으로 결정할 수 있도록 하는 경험적 결정(heuristic decision)을 제공한다.&lt;/li&gt;
&lt;li&gt;경험적 결정은 2단계 커밋의 약속 체계를 위반하기 땜누에 원자성을 깰 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;분산 트랜잭션의 제약&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코디네이터가 복제되지 않고 단일 장비에서만 실행되면 전체 시스템의 단일 장애점이 된다.&lt;/li&gt;
&lt;li&gt;여러 서버 사이드 애플리케이션은 모든 영속적인 상태를 데이터베이스에 저장하고 상태 비저장 모드로 개발되지만, 코디네이터가 애플리케이션 서비의 일부가 되면 코디네이터 로그가 시스템 상태의 중대한 부분이 되므로 더 이상 상태 비저장이 아니게 된다.&lt;/li&gt;
&lt;li&gt;XA는 광범위한 시스템과 호환돼야 하므로 최소 공통 분모가 될 필요가 있다. 여러 시스템에 걸친 교착 상태를 감지할 수 없고 SSI와 함께 동작하지 않는다.&lt;/li&gt;
&lt;li&gt;2PC가 성공적으로 트랜잭션을 커밋하려면 모든 참여자가 응답해야 하는 문제가 있는데, 이는 시스템의 어떤 부분이라도 고장나면 트랜잭션도 실패하게 된다. 따라서 분산 트랜잭션은 장애를 증폭시키는 경향이 있으며 이는 내결함성을 지닌 시스템을 구축하려는 목적에 어긋한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;내결함성을 지닌 합의&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;합의는 여러 노드가 어떤 것에 동의해야 한다는 의미이다.&lt;/li&gt;
&lt;li&gt;하나 또는 그 이상의 노드들이 값을 제안할 수 있고 합의 알고리즘이 그 값 중 하나를 결정한다.&lt;/li&gt;
&lt;li&gt;합의 알고리즘 속성
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;균일한 동의 : 어떤 두 노드도 다르게 결정하지 않는다.&lt;/li&gt;
&lt;li&gt;무결성 : 어떤 노드도 두 번 결정하지 않는다.&lt;/li&gt;
&lt;li&gt;유효성 : 한 노드가 값 v를 결정한다면 v는 어떤 노드에서 제안된 것이다.&lt;/li&gt;
&lt;li&gt;종료 : 죽지 않은 모든 노드는 결국 어떤 값을 결정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;합의 알고리즘과 전체 순서 브로드캐스트&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;합의 알고리즘
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;뷰스탬프 복제(viewstamped Replication)&lt;/li&gt;
&lt;li&gt;팍소스(Paxos)&lt;/li&gt;
&lt;li&gt;라프트(Raft)&lt;/li&gt;
&lt;li&gt;잽(Zab)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;전체 순서 브로드캐스트를 하려면 모든 노드에게 메시지가 정확히 한 번, 같은 순서로 전달돼야 한다.&lt;/li&gt;
&lt;li&gt;합의를 몇 회 하는 것과 동일하다. 각 회마다 노드들은 다음에 보내기 원하는 메시지를 제안하고 전체 순서 상에서 전달될 다음 메시지를 결정한다.&lt;/li&gt;
&lt;li&gt;전체 순서 브로드캐스트는 합의를 여러 번 반복하는 것과 동일하다.(각 합의 결정이 하나의 메시지 전달에 해당)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;합의의 동의 속성 때문에 모든 노드는 같은 메시지를 같은 순서로 전달하도록 결정한다.&lt;/li&gt;
&lt;li&gt;무결성 속성 때문에 메시지는 중복되지 않는다.&lt;/li&gt;
&lt;li&gt;유효성 속성 때문에 메시지는 오염되지 않고 난데없이 조작되지 않는다.&lt;/li&gt;
&lt;li&gt;종료 속성 때문에 메시지는 손실되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;단일 리더 복제와 합의&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;수동으로 리더를 선택
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;독재자 방식의 합의 알고리즘 사용&lt;/li&gt;
&lt;li&gt;한 노드만 쓰기를 받아들이는게 허용되고, 그 노드가 죽으면 시스템은 운영자가 수동으로 다른 노드를 리더로 설정할 때까지 쓰기 불가능&lt;/li&gt;
&lt;li&gt;사람의 개입이 필요하므로 합의의 종료 속성을 만족하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;자동 리더 선출과 장애 복구
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;내결함성을 지닌 전체 순서 브로드캐스트에 가까워지고 합의를 해결하는 데도 가까워짐&lt;/li&gt;
&lt;li&gt;스플릿 브레인 문제가 존재&lt;/li&gt;
&lt;li&gt;리더를 선출하려면 합의가 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;에포크 번호 붙이기와 정족수&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;합의 프로토콜은 모두 내부적으로 어떤 형태로든 리더를 사용하지만 리더가 유일하다고 보장하지 않는다. 대신 더 약한 보장을 할 수 있다.&lt;/li&gt;
&lt;li&gt;프로토콜들은 에포크 번호(epoch number)를 정의하고 각 에포크 내에서는 리더가 유일하다고 보장한다.&lt;/li&gt;
&lt;li&gt;리더 선출 투표
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;선출은 에포크 번호를 증가시킨다(에포크 번호는 전체 순서가 있고 단조 증가한다.)&lt;/li&gt;
&lt;li&gt;두가지 다른 에포크에 있는 두 가지 다른 리더 사이에 충돌이 있으면 에포크 번호가 높은 리더가 이긴다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;노드의 정족수 투표
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;리더가 무너가를 결정하도록 허용하기 전에 충돌되는 결정을 할지도 모르는 에포크 번호가 더 높은 다른 리더가 없는지 먼저 확인해야 한다.&lt;/li&gt;
&lt;li&gt;리더는 내리려고 하는 모든 결정에 대해 제안된 값을 다른 노드에게 보내서 노드의 정족수가 그 제안을 찬성한다고 응답하기를 기다려야 한다.&lt;/li&gt;
&lt;li&gt;노드는 에포크 번호가 더 높은 다른 리더를 알지 못할 때만 제안에 찬성하는 투표를 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;두 번의 투표가 존재(리더 선출, 리더의 제안)하는데, 두 번의 투표를 하는 정족수가 겹쳐야 한다.&lt;/li&gt;
&lt;li&gt;제안에 대한 투표가 성공하면 그것에 투표한 노드 중 최소 하나는 가장 최근의 리더 선출에도 참여했어야 한다.&lt;/li&gt;
&lt;li&gt;제안에 대한 투표를 할 때 에포크 번호가 더 큰 것이 있다고 밝혀 지지 않았다면, 현재 리더는 에포크 번호가 더 높은 리더 선출이 발생하지 않았다고 결론내려 리더십 유지가 가능하다.&lt;/li&gt;
&lt;li&gt;2PC와 차이점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;2PC의 코디네이터는 선출되지 않고 2PC는 모든 참여자로부터 &quot;네&quot; 응답을 받아야 한다.&lt;/li&gt;
&lt;li&gt;합의 알고리즘은 노드의 과반수로부터만 투표를 받으면 된다.&lt;/li&gt;
&lt;li&gt;합의 알고리즘은 새로운 리더가 선출된 후 노드를 일관적인 상태로 만들어주는 복구 과정을 정의해서 안전성 속성이 항상 만족되도록 보장한다. =&amp;gt; 정확성과 내결함성의 핵심&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;합의의 제약&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;제안이 결정되기전에 노드가 제안에 투표하는 과정은 일종의 동기식 복제이다. 비동기식 복제 설정 시 커밋된 데이터는 장애 복구 시 잠재적으로 손실될 수 있다.&lt;/li&gt;
&lt;li&gt;항상 엄격한 과반수가 동작하기를 요구한다. 노드 1대가 장애를 견디려면 최소 3대의 노드가 필요하고 두 대의 장애를 견디려면 최소 5대의 노드가 필요하다.&lt;/li&gt;
&lt;li&gt;투표에 참여하는 노드 집합이 고정돼 있다고 가정하며 이는 클러스터에 노르를 그냥 추가하거나 제거할 수 없다는 의미이다.&lt;/li&gt;
&lt;li&gt;장애 노드를 감지하기 위해 일반적으로 타임아웃에 의존한다. 네트워크 지연의 변동이 심한 환경에서 일시적인 네트워크 문제 때문에 노드가 리더에 장애가 발생했다고 잘못 생각하는 일이 발생할 수 있다.&lt;/li&gt;
&lt;li&gt;때때로 네트워크 문제에 민감하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;멤버십과 코디네이션 서비스&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주키퍼와 etcd
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;분산 키-값 저장소나 코디네이션과 설정 서비스라고 설명된다.&lt;/li&gt;
&lt;li&gt;완전히 메모리 안에 들어올 수 있는 작은 양의 데이터를 보관하도록 설계됐다.&lt;/li&gt;
&lt;li&gt;이 소량의 데이터는 내결함성을 지닌 전체 순서 브로드캐스트 알고리즘을 사용해 모든 노드에 걸쳐 복제된다.&lt;/li&gt;
&lt;li&gt;개별 메시지가 데이터베이스에 쓰기를 나타낸다면 같은 쓰기를 같은 순서로 적용함으로써 복제본들이 서로 일관성을 유지할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;주키퍼는 구글의 처비(Chubby) 잠금 서비스를 모델로 삼아 전체 순서 브로드캐스트뿐만 아니라 분산 시스템을 구축할 때 유용한 기능 집합도 구현한다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;선형성 원자적 연산: 원자적 compare-and-set 연산을 사용해 잠금 구현&lt;/li&gt;
&lt;li&gt;연산의 전체 순서화: 펜싱 토큰&lt;/li&gt;
&lt;li&gt;장애 감지&lt;/li&gt;
&lt;li&gt;변경 알림&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;작업을 노드에 할당하기&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주키퍼/처비 모델이 잘 동작하는 예시
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 개의 프로세스나 서비스가 있고 그 중 하나가 리더나 주 구성요소로 선택돼야 할 때&lt;/li&gt;
&lt;li&gt;파티셔닝된 자원이 있고 어떤 파티션을 어느 노드에 할당해야 할지 결정해야 하는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;매우 많은 노드에서 과반수 투표를 수행하려고 하는 것은 비효율적이다.&lt;/li&gt;
&lt;li&gt;주키퍼는 고정된 수의 노드에서 실행되고, 이 노드들 사이에서 과반수 투표를 수행하면서 많아질 수 있는 클라이언트를 지원한다.&lt;/li&gt;
&lt;li&gt;노드들을 코디네이트하는 작업(합의, 연산 순서화, 장애 감시)의 일부를 외부 서비스에 위탁하는 방법을 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;서비스 찾기&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주키퍼, etcd, 콘술(Consul)은 서비스 찾기, 즉 특정 서비스에 연결하려면 어떤 IP 주소로 접속해야 하는지 알아내는 용도로도 자주 사용된다.&lt;/li&gt;
&lt;li&gt;서비스 찾기가 합의가 필요한지는 분명하지 않다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;멤버십 서비스&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;멤버십 서비스는 클러스터에서 어떤 노드가 현재 활성화된 살아 있는 멤버인지 결정한다.&lt;/li&gt;
&lt;li&gt;장애 감지를 합의와 연결하면 노드들은 어떤 노드가 살아있는 것으로 여겨져야 하는지 혹은 죽은 겻으로 여겨져야 하는지 동의할 수 있다.&lt;/li&gt;
&lt;li&gt;합의는 시스템에서 어떤 노드가 현재 멤버십을 구성하는지 동의하는 데 매우 유용하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>STUDY/데이터 중심 애플리케이션 설계</category>
      <author>level_?</author>
      <guid isPermaLink="true">https://it-study.tistory.com/130</guid>
      <comments>https://it-study.tistory.com/130#entry130comment</comments>
      <pubDate>Tue, 23 Sep 2025 20:59:49 +0900</pubDate>
    </item>
    <item>
      <title>8장. 분산 시스템의 골칫거리</title>
      <link>https://it-study.tistory.com/129</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;결함과 부분 장애&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 컴퓨터&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하드웨어가 올바르게 동작하면 같은 연산은 항상 같은 결과를 낸다(결정적).&lt;/li&gt;
&lt;li&gt;하드웨어 문제(메모리 오염, 헐거운 커넥터)가 있으면 보통 시스템이 완전히 실패하는 결과를 낳는다.&lt;/li&gt;
&lt;li&gt;좋은 소프트웨어가 설치된 각각의 컴퓨터는 보통 완전하게 동작하거나 전체 장애가 발생하지 그 중간 상태가 되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 시스템&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;부분 장애(partial failure) : 시스템의 어떤 부분은 잘 동작하지만 다른 부분은 예측할 수 없는 방식으로 고장날 수 있다.&lt;/li&gt;
&lt;li&gt;부분 장애는 비결정적이라서 어렵다.&lt;/li&gt;
&lt;li&gt;여러 노드와 네트워크와 관련된 뭔가를 시도하면 어떨 때는 동작하지만 어떨 때는 예측할 수 없는 방식으로 실패한다.&lt;/li&gt;
&lt;li&gt;메시지가 네트워크를 거쳐 전송되는 시간도 비결정적이기 때문에, 뭔가 성공했는지 아닌지 알지 못할 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;클라우드 컴퓨팅과 슈퍼컴퓨팅&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대규모 컴퓨팅의 한쪽 끝에는 고성능 컴퓨팅이 있고, 다른 극단에는 클라우드 컴퓨팅이 있다.&lt;/li&gt;
&lt;li&gt;슈퍼 컴퓨터는 분산 시스템보다는 단일 노드 컴퓨터에 가까우며, 부분 장애를 전체 장애로 확대하는 방법으로 처리한다.&lt;/li&gt;
&lt;li&gt;분산 시스템이 동작하게 만들려면 부분 장애 가능성을 받아들이고 소프트웨어에 내결함성 메커니즘을 넣어야 한다. 바꿔 말하면&amp;nbsp;신뢰성 없는 구성 요소를 사용해 신뢰성 있는 시스템을 구축해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;신뢰성 없는 네트워크&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;분산 시스템은 비공유 시스템, 즉 네트워크로 연결된 다수의 장비를 의미한다.&lt;/li&gt;
&lt;li&gt;네트워크는 이 장비들이 통신하는 유일한 수단으로, 각 장비는 자신만의 메모리와 디스크를 갖고 있으며 다른 장비의 메모리나 디스트에 접근할 수 없다고 가정한다.&lt;/li&gt;
&lt;li&gt;비공유 시스템을 구축했을 때 장점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;특별한 하드웨어가 필요하지 않아서 상대적으로 저렴하다.&lt;/li&gt;
&lt;li&gt;상품화된 클라우드 서비스를 활용할 수 있다.&lt;/li&gt;
&lt;li&gt;지리적으로 분산된 여러 데이터센터에 중복 배치함으로써 높은 신뢰성을 확보할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;비동기 패킷 네트워크(asynchronous packet network)&amp;nbsp;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;인터넷과 데이터센터 내부 네트워크&lt;/li&gt;
&lt;li&gt;노드는 다른 노드로 메시지(패킷)를 보낼 수 있지만 네트워크는 메시지가 언제 도착할지 혹은 메시지가 도착하기는 할 것인지 보장하지 않는다.&lt;/li&gt;
&lt;li&gt;요청을 보내고 응답을 기다릴 때 여러가지가 잘못될 수 있다.(ex. 요청 손실/지연 전송, 노드 장애, 응답 손실/지연 전송)&lt;/li&gt;
&lt;li&gt;이런 문제를 다루는 흔한 방법으로 타임아웃이 있다. 얼마 간의 시간이 지나면 응답 대기를 멈추고 응답이 도착하지 않는다고 가정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;현실의 네트워크 결함&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;네트워크 상으로 통신할 때마다 실패할 가능성이 있다.&lt;/li&gt;
&lt;li&gt;네트워크 결함의 오류 처리가 정의되고 테스트되지 않는다면 나쁜 일이 생길 수 있다.&lt;/li&gt;
&lt;li&gt;반드시 네트워크 결함을 견뎌내도록 처리할 필요는 없지만, 소프트웨어가 네트워크 문제에 어떻게 반응하는지 알고 시스템이 그로부터 복구할 수 있도록 보장해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;결함 감지&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;많은 시스템은 결함 있는 노드를 자동으로 감지할 수 있어야 한다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;로드 밸런서는 죽은 노드로 요청을 그만 보내야 한다.&lt;/li&gt;
&lt;li&gt;단일 리더 복제를 사용하는 분산 데이터베이스에서 리더에 장애가 나면 팔로워 중 하나가 리더로 승격돼야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;네트워크에 관한 불확실성 때문에 노드가 동작 중인지 아닌지 구별하기 어렵다.&lt;/li&gt;
&lt;li&gt;특정한 환경에서는 뭔가 동작하지 않는다고 명시적으로 알려주는 피드백을 받을 수도 있다.&lt;/li&gt;
&lt;li&gt;뭔가 잘못되면 스택의 어떤 수준에서 오류 응답을 받을지도 모르지만 일반적으로 아무 응답도 받지 못할 것이라고 가정하고, 몇 번 재시도한 후 타임아웃이 만료되기를 기다렸다가 타임아웃 내에 응답을 받지 못하면 마침내 노드가 죽었다고 선언할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;타임아웃과 기약 없는 지연&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;타임아웃 기준은?
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;타임아웃이 길면 노드가 죽었다고 선언될 때까지 기다리는 시간이 길어진다.&lt;/li&gt;
&lt;li&gt;타임아웃이 짧으면 결함을 빨리 발견하지만 노드가 일시적으로 느려졌을 뿐인데도 죽었다고 잘못 선언할 위험이 높아진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;비동기 네트워크는 기약 없는 지연(unbounded delay)이 있고(패킷을 가능한 한 빨리 보내려고 하지만 패킷이 도착하는 데 걸리는 시간에 상한치는 없다), 서버 구현은 대부분 어떤 최대 시간 내에 요청을 처리한다고 보장할 수 없다.&lt;/li&gt;
&lt;li&gt;시스템이 대부분의 시간에 빠르다는 것은 장애 감지에 충분하지 않고, 타임아웃이 낮으면 왕복 시간이 순간적으로 급증해 시스템의 균형을 깨뜨릴 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;네트워크 혼잡과 큐 대기&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴퓨터 네트워크에서 패킷 지연의 변동성은 큐 대기 때문인 경우가 많다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;여러 다른 노드가 동시에 같은 목적지로 패킷을 보내려고 하면 네트워크 스위치는 패킷을 큐에 넣고 한 번에 하나씩 목적지 네트워크 링크로 넘겨야 한다. 네트워크 링크가 붐비면 패킷은 슬롯을 얻을 수 있을 때까지 잠시 기다려야 할 수 있다.(네트워크 혼잡)&lt;/li&gt;
&lt;li&gt;패킷이 목적지 장비에 도착했을 때 모든 CPU 코어가 바쁜 상태라면 네트워크에서 들어온 요청은 애플리케이션에서 처리할 준비가 될 때까지 운영체제가 큐에 넣어 둔다.&lt;/li&gt;
&lt;li&gt;가상 환경에서 실행되는 운영체제는 다른 가상 장비가 CPU 코어를 사용하는 동안 수십 밀리초 동안 멈출 때가 흔하다. 이 시간 동안 가상 장비는 네트워크에서 어떤 데이터도 받아들일 수 없으므로 가상 장비 모니터가 들어오는 데이터를 큐에 넣어서 네트워크 지연의 변동성을 증기시킨다.&lt;/li&gt;
&lt;li&gt;TCP는 흐름 제어를 수행한다. 노드가 네트워크 링크나 수신 노드에 과부하를 가하지 않도록 자신의 송신율을 제한하는 것으로, 데이터가 네트워크로 들어가기 전에도 부가적인 큐 대기를 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;네트워크 지연의 변동이 큰 환경에서는 실험적으로 타임아웃을 선택해야 한다.&lt;/li&gt;
&lt;li&gt;지연의 변동성이 얼마나 되는지 알아내려면 긴 기간동안 여러 장비에 걸쳐서 네트워크 왕복 시간의 분포를 측정해야 한다.&lt;/li&gt;
&lt;li&gt;그 후 애플리케이션의 특성을 고려해서 장애 감지 지연과 너무 이른 타임아웃의 위험성 사이에서 적절한 트레이드오프를 결정할 수 있다.&lt;/li&gt;
&lt;li&gt;더 좋은 방법으로는 고정된 타임아웃을 설정하는 대신 시스템이 지속적으로 응답 시간과 그들의 변동성을 측정하고 관찰된 응답 시간 분포에 따라 타임아웃을 자동으로 조절하게 하는 것이다.(파이 증가 장애 감지기 사용)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;동기 네트워크 VS 비동기 네트워크&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동기식 네트워크는 데이터가 여러 라우터를 거치더라도 큐 대기 문제를 겪지 않는다.(ex. 전화 네트워크)&lt;/li&gt;
&lt;li&gt;큐 대기가 없으므로 네트워크 종단 지연 시간의 최대치가 고정돼 있고, 이를 제한 있는 지연(bounded delay)이라고 한다.&lt;/li&gt;
&lt;li&gt;전화 네트워크 회선은 TCP 연결과 매우 다르다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;회선은 만들어져 있는 동안 다른 누구도 사용할 수 없는 고정된 양의 에약된 대역폭이다.&lt;/li&gt;
&lt;li&gt;TCP 연결의 패킷은 가용한 네트워크 대역폭을 기회주의적으로 사용한다. TCP 연결이 유휴 상태에 있는 동안은 어떤 대역폭도 사용하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;신뢰성 없는 시계&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;분산 시스템에서는 통신이 즉각적이지 않으므로 시간은 다루기 까다롭다.&lt;/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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;단조 시계 VS 일 기준 시계&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컴퓨터는 최소 두 가지 종류의 시계를 갖고 있다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;일 기준 시계(time-of-day clock)&lt;/li&gt;
&lt;li&gt;단조 시계(monotonic clock)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;둘 다 시간을 측정하지만 다른 목적으로 사용되므로 구별이 중요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;일 기준 시계&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 달력에 따라 현재 날짜와 시간을 반환한다.(벽시계 시간이라고도 함, wall-clock time)&lt;/li&gt;
&lt;li&gt;보통 NTP로 동기화된다. 한 장비의 타임스탬프는 다른 장비의 타임스탬프와 동일한 의미를 지닌다는 의미이다.&lt;/li&gt;
&lt;li&gt;시간이 거꾸로 뛸 수도 있다(ex. 로컬 시계가 NTP서버보다 너무 앞서는 경우 강제로 리셋되어 과거 시점으로 거꾸로 뛰는 것처럼 보일 수 있다).&lt;/li&gt;
&lt;li&gt;역사적으로 매우 거친(coarse-grained) 해상도를 가진다(ex. 오래된 윈도우 시스템에서는 10밀리초 단위로 흐른다).&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;단조 시계&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;타임아웃이나 서비스 응답 시간과 같은 지속 시간(시간 구간)을 재는 데 적합하다.&lt;/li&gt;
&lt;li&gt;항상 시간이 앞으로 흐른다.&lt;/li&gt;
&lt;li&gt;한 시점에서 단조 시계의 값을 확인하고 어떤 일을 한 후 나중에 다시 시계를 확인하면, 두 값 사이의 차이로 시간이 얼마나 흘렀는지 알 수 있다.&lt;/li&gt;
&lt;li&gt;시계의 절대적인 값은 의미가 없다.&lt;/li&gt;
&lt;li&gt;NTP는 컴퓨터의 로컬 시계가 NTP 서버보다 빠르거나 느리다는 것을 발견하면 단조 시계가 진행하는 진도수를 조정할 수도 있다.(시계를 돌린다(slewing)고 한다). NTP는 시계 속도를 0.05%까지 올리거나 내리는 것을 허용하지만 단조 시계가 앞이나 뒤로 뛰게 할 수는 없다.&lt;/li&gt;
&lt;li&gt;단조 시계의 해상도는 보통 상당히 좋다.(대부분의 시스템에서 시간 구간을 마이크로초나 그 이하 단위로 측정할 수 있다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;시계 동기화와 정확도&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단조 시계는 동기화가 필요없지만 일 기준 시계는 NTP 서버나 다른 외부 시간 출처에 맞춰 설정돼야 유용하다.&lt;/li&gt;
&lt;li&gt;시계가 정확한 시간을 알려주게 하는 방법은 기대만큼 신뢰성 있거나 정확하지 않다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;컴퓨터의 수정 시계는 아주 정확하지 않고, 더 빠르거나 느리게 실행되는 드리프트 현상이 생긴다.&lt;/li&gt;
&lt;li&gt;컴퓨터 시계가 NTP 서버와 너무 많은 차이가 나는 경우 동기화가 거부되거나 강제로 리셋될 수 있다.&lt;/li&gt;
&lt;li&gt;패킷 지연의 변화가 큰 혼잡한 네트워크에서는 정확도에 한계가 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;상당한 자원을 투입해 시계 정확도를 높일 수 있다.(ex. GPS 수신기, 정밀 시간 프로토콜(Precision Time Protocol, PTP)과 세심한 배포 및 모니터링을 사용)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;동기화된 시계에 의존하기&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;장비의 수정 시계에 결함이 있거나 NTP 클라이언트가 잘못 설정됐다면 시계는 드리프트가 생겨서 점점 실제 시간으로부터 멀어져가지만 대부분이 잘 동작하는 것처럼 보이기 때문에, 잘못됐다는 것을 눈치채기 어렵다.&lt;/li&gt;
&lt;li&gt;소프트웨어의 어떤 부분이 정확히 동기화된 시계에 의존한다면 그 결과는 극적인 고장보다는 조용하고 미묘한 데이터 손실이 발생할 가능성이 높다.&lt;/li&gt;
&lt;li&gt;동기화된 시계가 필요한 소프트웨어의 경우 필수적으로 모든 장비 사이의 시계 차이를 모니터링하거나, 다른 노드와 너무 차이나는 노드는 죽은 것으로 선언되고 클러스터에서 제거돼야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;이벤트 순서화용 타임스탬프&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;최종 쓰기 승리(last write wins, LWW) : 다중 리더 복제와, 카산드라와 리악 같은 리더 없는 데이터베이스에서 널리 사용되는 충돌 해소 전략&lt;/li&gt;
&lt;li&gt;문제점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;데이터베이스 쓰기가 불가사의하게 사라질 수 있다.&lt;/li&gt;
&lt;li&gt;순차적인 쓰기가 빠른 시간 내에 연속으로 실행되는 것과 진짜 동시에 쓰기가 실행되는 것을 구별할 수 없다.&lt;/li&gt;
&lt;li&gt;두 노드가 독립적으로 동일한 타임스탬프를 가진 쓰기 작업을 만들 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;최근 값을 유지하고 다른 것들을 버림으로써 충돌을 해소할 때, 최근의 정의가 로컬 일 기준 시계에 의존하며 그 시계가 틀릴 수도 있다는 것을 알아야 한다.&lt;/li&gt;
&lt;li&gt;논리적 시계(logical clock)는 진동하는 수정 대신 증가하는 카운터를 기반으로 하며 이벤트 순서화의 안전한 대안으로, 일 기준 시간이나 경과한 초 수를 측정하지 않고 이벤트의 상대적인 순서만 측정한다.&lt;/li&gt;
&lt;li&gt;물리적 시계(physical clock)는 일 기준 시계와 단조 시계로 실제 경과 시간을 측정한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;시계 읽기는 신뢰 구간이 있다.&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시계 읽기는 어떤 신뢰 구간에 속하는 시간의 범위로 읽는 게 좋다.&lt;/li&gt;
&lt;li&gt;예를 들어 어떤 시스템은 현재 시간이 해당 분의 10.3초와 10.5초 사이에 있다고 95% 확신할 수 있으나 그보다 더 정확히는 알지 못한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;전역 스냅숏용 동기화된 시계&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 흔한 스냅숏 격리 구현
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;단조 증가하는 트랙잰션 ID가 필요하다.&lt;/li&gt;
&lt;li&gt;스냅숏보다 나중에 쓰기가 실행됐다면(스냅숏보다 큰 트랜잭션 ID로 쓰기를 실행한 경우) 그 내용은 스냅숏 트랜잭션에게 보이지 않는다.&lt;/li&gt;
&lt;li&gt;단일 노드 데이터베이스에서는 단순한 카운터가 트랜잭션 ID를 생성하는 데 충분하다.&lt;/li&gt;
&lt;li&gt;데이터베이스가 여러 데이터센터에 있는 여러 장비에 분산돼 있는 경우에는 사용하기 어렵다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;스패너의 스냅숏 구현
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;트루타입 API가 보고한 시계 신뢰 구간을 사용한다.&lt;/li&gt;
&lt;li&gt;각각 가장 이른 타임스탬프와 가장 늦은 타임스탬프를 포함하는 두 개의 신뢰 구간(A = [A_earliest, A_latest], B = [B_earliest, B_latest]이 있고, 두 구간이 겹치지 않는다면(A_earliest &amp;lt; A_latest &amp;lt; B_earliest &amp;lt; B_latest) B는 A보다 나중에 실행된 것을 확신할 수 있다. 구간이 겹칠 때만 어떤 순서로 실행됐는지 확신할 수 없다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;트랜잭션 타임스탬프가 인과성을 반영하는 것을 보장하기 위해 읽기 쓰기 트랜잭션을 커밋하기 전에 의도적으로 신뢰 구간의 길이만큼 기다린다. 이러면 해당 데이터를 읽을지도 모르는 트랜잭션은 충분히 나중에 실행되는 게 보장되므로 신뢰 구간이 겹치지 않는다.&lt;/li&gt;
&lt;li&gt;대기 시간을 가능하면 짧게 유지하기 위해 시계 불확실성을 가능하면 작게 유지해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;프로세스 중단&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;분산 시스템의 노드는 어느 시점에 실행이 상당한 시간 동안 멈출 수 있다.&lt;/li&gt;
&lt;li&gt;멈춰 있는 동안 외부 세계는 계속 움지이며 멈춘 노드가 응답하지 않으서 죽었다고 선언할 수도 있다.&lt;/li&gt;
&lt;li&gt;멈춘 노드는 다시 실행되겠지만 얼마 후 시계를 확인할 때까지 중단됐다는 것을 알아채지 못한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;응답 시간 보장&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;명시된 시간 안에 응답하는 데 실패하면 심각한 손상을 유발할 수 있는 환경에서 실행되는 소프트웨어의 경우 응답해야 하는 데드라인이 명시된다.&lt;/li&gt;
&lt;li&gt;데드라인을 만족시키지 못하면 전체 시스템의 장애를 유발할 수 있는데, 이를 엄격한 실시간 시스템(hard real-time)이라고 한다.&lt;/li&gt;
&lt;li&gt;시스템에서 실시간 보장을 제공하려면 소프트웨어 스택의 모든 수준에서 지원이 필요하다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;프로세스가 명시된 간격의 CPU 시간을 할당받을 수 있게 보장되도록 스케줄링해주는 실시간 운영체제 필요&lt;/li&gt;
&lt;li&gt;라이브러리 함수는 최악의 실행 시간을 문서화 해야함.&lt;/li&gt;
&lt;li&gt;동적 메모리 할당은 제한되거나 완전히 금지될 수 있다.&lt;/li&gt;
&lt;li&gt;막대한 양의 테스트와 측정 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;안전이 필수인 임베디드 장치에서 흔히 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;지식, 진실 그리고 거짓말&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;분산 시스템에서는 공유 메모리가 없고 지연 변동이 큰 신뢰할 수 없는 네트워크를 통해 메시지를 보낼 수 있을 뿐이며 부분 장애, 신뢰성 없는 시계, 프로세스 중단에 시달릴 수 있다.&lt;/li&gt;
&lt;li&gt;분산 시스템은 한 노드에만 의존할 수 없다. 노드에 언제든 장애가 나서 잠재적으로 시스템이 멈추고 복구할 수 없게 될 수도 있기 때문이다.&lt;/li&gt;
&lt;li&gt;여러 분산 알고리즘은 정족수(quoarum), 즉 노드들 사이의 투표에 의존한다. 특정한 노드 하나에 대한 의존을 줄이기 위해 결정을 하려면 여러 노드로부터 어떤 최소 개수의 투표를 받아야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;리더와 잠금&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 노드가 스스로를 선택된 자(파티션의 리더, 잠금을 획득한 자, 사용자명을 차지하는 데 성공한 사용자의 요청 처리기)라고 믿을지라도 노드의 정족수도 반드시 동의하는 것은 아니다.&lt;/li&gt;
&lt;li&gt;노드의 과반수가 어떤 노드가 죽었다고 선언했음에도 그 노드가 선택된 것처럼 계속 행동한다면 신중하게 설계되지 않은 시스템에서는 문제를 유발할 수 있다.&lt;/li&gt;
&lt;li&gt;분산 잠금의 잘못된 구현
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;임차권을 가진 클라이언트가 오랫동안 멈춰있어 임차권이 만료됨.&lt;/li&gt;
&lt;li&gt;다른 클라이언트가 같은 파일에 대한 임차권을 획득해서 쓰기 시작&lt;/li&gt;
&lt;li&gt;멈췄던 클라이언트가 되돌아왔을 때 임차권이 여전히 유효하다고 생각해서 저장소에 있는 파일에 쓰기&lt;/li&gt;
&lt;li&gt;클라이언트들의 쓰기가 충돌되고 파일이 오염됨&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;펜싱 토큰(fencing token)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;잠금이 승인될 때마다 증가하는 숫자&lt;/li&gt;
&lt;li&gt;잠금 서버가 잠금이나 임차권을 승인할 때마다 펜싱 토큰도 반환한다고 가정하면, 클라이언트가 쓰기 요청을 저장소 서비스로 보낼 때마다 자신의 현재 펜싱 토큰을 포함하도록 요구할 수 있다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클라이언트1이 33번 토큰으로 임차권 획득 후 중단돼서 임차권 만료&lt;/li&gt;
&lt;li&gt;클라이언트2가 34번 토큰으로 임차권을 얻은 후 저장소 서비스로 34번 토큰을 포함한 쓰기 요청을 보냄&lt;/li&gt;
&lt;li&gt;클라이언트1이 되살아 난 후 저장 서비스로 33번 토큰을 포함한 쓰기 요청을 보냄&lt;/li&gt;
&lt;li&gt;저장소 서버는 더 큰 토큰 번호(34번)를 가진 쓰기를 이미 처리했으므로 요청을 거부&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;잠금 서비스로 주키퍼를 사용하면 트랜잭션 ID나 노드 버전을 펜싱 토큰으로 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;비잔틴 결함&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 노드가 실제로는 받지 않은 특정 메시지를 받았다고 주장하는 것처럼 노드가 거짓말을 하는 경우를 비잔틴 결함(Byzantine fault)라고 하며, 신뢰할 수 없는 환경에서 합의에 도달하는 문제를 비잔틴 장군 문제(Byzantine Generals Problem)라고 한다.&lt;/li&gt;
&lt;li&gt;일부 노드가 오동작하고 프로토콜을 준수하지 않거나 악의적인 공격자가 네트워크를 방해하더라도 시스템이 계속 올바르게 동작한다면 이 시스템은 비잔틴 내결함성을 지닌다.&lt;/li&gt;
&lt;li&gt;시스템이 비잔틴 내결함성을 지니도록 만드는 프로토콜은 매우 복잡하고 내결함성을 지닌 임베디드 시스템은 하드웨어 수준의 지원에 의존한다. 즉, 비용이 커서 실용적이지 않다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;약한 형태의 거짓말&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하드웨어 문제, 소프트웨어 버그, 잘못된 설정 때문에 유효하지 않는 메시지로부터 보호해주는 메커니즘을 소프트웨어에 추가하는 것이 좋다.&lt;/li&gt;
&lt;li&gt;완전한 비잔틴 내결함성을 지니지는 않지만, 나은 신뢰성으로 향할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;시스템 모델과 현실&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타이밍 가정에서 사용되는 시스템 모델&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동기식 모델
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;네트워크 지연, 프로세스 중단, 시계 오차에 모두 제한이 있다고 가정한다.&lt;/li&gt;
&lt;li&gt;시계가 정확하게 동기화된다거나 네트워크 지연이 없다고 암시하는 것은 아니고, 어떤 고정된 상한치를 초과하지 않을 것임을 안다.&lt;/li&gt;
&lt;li&gt;기약 없는 지연과 중단이 발생하기 때문에 현실적인 모델은 아니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;부분 동기식 모델
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;시스템이 대부분의 시간에는 동기식 시스템처럼 동작하지만 때때로 네트워크 지연, 프로세스 중단, 시계 드리프트의 한계치를 초과하는 것을 의미한다.&lt;/li&gt;
&lt;li&gt;많은 시스템에서 현실적인 모델&lt;/li&gt;
&lt;li&gt;가끔 어떤 타이밍 가정이 산산조각 날지도 모른다는 사실을 고려해야 하고, 이 때 네트워크 지연, 중단, 시계 오차가 커질 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;비동기식 모델
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;타이밍에 대한 어떤 가정도 할 수 없다.&lt;/li&gt;
&lt;li&gt;시계가 없을 수도(타임아웃을 쓸 수 없을 수도) 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드용 시스템 모델&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;죽으면 중단하는(crash-stop) 결함
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;노드에 장애가 발생하는 것은 죽는 것 뿐이라고 가정한다.&lt;/li&gt;
&lt;li&gt;노드가 어느 순간에 갑자기 응답하기를 멈추면 이후로 그 노드는 영원히 사용할 수 없고 결코 되돌아오지 않는 것을 의미한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;죽으면 복구하는(crash-recovery) 결함
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;노드가 어느 순간에 죽을 수 있지만 알려지지 않은 시간이 흐른 후에는 아마도 다시 응답하기 시작할 것이라고 가정한다.&lt;/li&gt;
&lt;li&gt;노드는 메모리에 있는 상태는 손실되지만 죽어도 데이터가 남아있는 저장소가 있다고 가정한다.&lt;/li&gt;
&lt;li&gt;유용한 모델&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;비잔틴(임의적인) 결함
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;노드는 다른 노드를 속이거나 기만하는 것을 포함해 무슨 일이든 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;알고리즘의 정확성&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;알고리즘이 정확하다는 게 어떤 의미인지 정의하기 위해 알고리즘의 속성을 기술할 수 있다(ex. 정렬 알고리즘은 왼쪽 요소가 오른쪽 요소보다 작다는 속성이 있다).&lt;/li&gt;
&lt;li&gt;알고리즘은 시스템 모델에서 발생하리라고 가정한 모든 상황에서 그 속성들을 항상 만족시키면 해당 시스템 모델에서 정확하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;안전성과 활동성&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;안전성은 흔히 나쁜 일은 일어나지 않는다라고 하고, 활동성은 좋은 일은 결국 일어난다라고 정의된다.&lt;/li&gt;
&lt;li&gt;안전성 속성이 위반되면 그 속성이 깨진 특정 시점을 가리킬 수 있다. 안전성 속성이 위반된 후에는 그 위반을 취소할 수 없다. 이미 손상된 상태이다.&lt;/li&gt;
&lt;li&gt;활동성 속성은 반대로 어떤 시점을 정하지 못할 수 있지만 항상 미래에 그 속성을 만족시킬 수 있다는 희망이 있다.&lt;/li&gt;
&lt;li&gt;분산 알고리즘은 시스템 모델의 모든 상황에서 안전성 속성이 항상 만족되기를 요구하는 게 일반적이다. 즉, 모든 노드가 죽거나 네트워크 전체에 장애가 생기더라도 알고리즘은 잘못된 결과를 반환하지 않는다고 보장해야 한다.&lt;/li&gt;
&lt;li&gt;활동성에 대해서는 경고를 하는 게 허용된다. 예를 들어 노드의 다수가 죽지 않고 네트워크가 중단으로부터 결국 복구됐을 때만 요청이 응답을 받아야 한다고 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>STUDY/데이터 중심 애플리케이션 설계</category>
      <author>level_?</author>
      <guid isPermaLink="true">https://it-study.tistory.com/129</guid>
      <comments>https://it-study.tistory.com/129#entry129comment</comments>
      <pubDate>Sun, 31 Aug 2025 21:40:11 +0900</pubDate>
    </item>
    <item>
      <title>7장. 트랜잭션</title>
      <link>https://it-study.tistory.com/128</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 시스템의 여러 가지 문제&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터베이스 소프트웨어나 하드웨어는 (쓰기 연산이 실행 중일 때를 포함해서) 언제라도 실패할 수 있다.&lt;/li&gt;
&lt;li&gt;애플리케이션은 (연속된 연산이 실행되는 도중도 포함해서) 언제라도 죽을 수 있다.&lt;/li&gt;
&lt;li&gt;네트워크가 끊기면 애플리케이션과 데이터베이스의 연결이 갑자기 끊기거나 데이터베이스 노드 사이의 통신이 안 될 수 있다.&lt;/li&gt;
&lt;li&gt;여러 클라이언트가 동시에 데이터베이스에 쓰기를 실행해서 다른 클라이언트가 쓴 내용을 덮어쓸 수 있다.&lt;/li&gt;
&lt;li&gt;클라이언트가 부분적으로만 갱신돼서 비정상적인 데이터를 읽을 수 있다.&lt;/li&gt;
&lt;li&gt;클라이언트 사이의 경쟁 조건은 예측하지 못한 버그를 유발할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션에서 몇 개의 읽기와 쓰기를 하나의 논리적 단위로 묶는 방법으로, 위에서 발생하는 문제를 단순화하는 메커니즘으로 채택돼 왔다.&lt;/li&gt;
&lt;li&gt;한 트랜잭션 내의 모든 읽기와 쓰기는 한 연산으로 실행된다.&lt;/li&gt;
&lt;li&gt;전체가 성공(커밋)하거나 실패(어보트/abort, 롤백)한다.&lt;/li&gt;
&lt;li&gt;부분적인 실패를 걱정할 필요가 없다.&lt;/li&gt;
&lt;li&gt;현대의 거의 모든 관계형 데이터베이스와 일부 비관계형 데이터베이스는 트랜잭션을 지원한다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ACID&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션이 제공하는 안전성 보장은 원자성(Atomicity), 일관성(Consistency), 격리성(Isolation), 지속성(Durability)을 의미하는 ACID로 잘 알려져 있다.&lt;/li&gt;
&lt;li&gt;데이터베이스에서 내결함성 메커니즘을 나타내는 용어로 만들어졌지만, 현실에서는 데이터베이스마다 ACID 구현이 제각각이다.&lt;/li&gt;
&lt;li&gt;ACID 표준을 따르지 않는 시스템은 때로 BASE라고 불리는데, 기본적으로 가용성을 제공하고(Basically Available), 유연한 상태를 가지며(Soft state), 최종적 일관성(Eventual consistency)을 지닌다는 뜻이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;원자성(Atomicity)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;여러 쓰기 작업이 하나의 원자적인 트랜잭션으로 묶여 있어 결함 때문에 완료(커밋)될 수 없다면 어보트(abort)되고 데이터베이스는 이 트랜잭션에서 지금까지 실행한 쓰기를 무시하거나 취소해야 한다.&lt;/li&gt;
&lt;li&gt;트랜잭션 내의 모든 연산들은 전체가 정상적으로 수행이 완료되거나 아니면 어떠한 연산도 수행되지 않아야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;일관성(Consistency)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;트랜잭션 전후의 데이터베이스는 항상 일관된 상태여야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;격리성(Isolation)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;동시에 실행되는 트랜잭션은 서로 격리되어야 한다. 즉, 동시에 실행되는 여러 트랜잭션은 서로 영향을 주지 않고 독립적으로 실행되는 것처럼 보여야 한다.&lt;/li&gt;
&lt;li&gt;데이터베이스는 여러 트랜잭션이 동시에 실행됐더라도 트랜잭션이 커밋됐을 때의 결과가 트랜잭션이 순차적으로 실행됐을 때의 결과와 동일하도록 보장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;지속성(Durability)&lt;/b&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;트랜잭션이 성공적으로 커밋됐다면 하드웨어 결함이 발생하거나 데이터베이스가 죽더라도 트랜잭션에서 기록한 모든 데이터는 손실되는 않는 것을 보장한다.&lt;/li&gt;
&lt;li&gt;단일 노드 데이터베이스에서 지속성은 일반적으로 데이터가 하드디스크나 SSD 같은 비휘발성 메모리에 기록됐다는 의미이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 객체 연산과 다중 객체 연산&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ACID에서 원자성과 격리성은 클라이언트가 한 트랜잭션 내에서 여러 번의 쓰기를 하면 데이터베이스가 어떻게 해야하는지 알려준다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;원자성 : 쓰기를 이어서 실행하는 도중 오류가 발생하면 트랜잭션은 어보트돼야 하고 그때까지 쓰여진 내용은 폐기돼야 한다. 즉, 데이터베이스는 전부 반영되거나 아무것도 반영되지 않는 것을 보장함으로써 부분 실패를 걱정할 필요가 없게 한다.&lt;/li&gt;
&lt;li&gt;격리성 : 동시에 실행되는 트랜잭션들은 서로를 방해하지 말아야 한다. 예를 들어, 한 트랜잭션이 여러 번 쓴다면 다른 트랜잭션은 그 내용을 전부 볼 수 있든지 아무것도 볼 수 없든지 둘 중 하나여야 하고 일부만 볼 수 있어서는 안 된다.&lt;/li&gt;
&lt;li&gt;격리성 위반 : 트랜잭션이 다른 트랜잭션에서 썼지만 커밋되지 않은 데이터를 읽음(dirty read)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;다중 객체 트랜잭션
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;어떤 읽기 연산과 쓰기 연산이 동일한 트랜잭션에 속하는지 알아낼 수단이 필요한데, 관계형 데이터베이스에서는 클라이언트와 데이터베이스 서버 사이의 TCP 연결을 기반으로 한다.&lt;/li&gt;
&lt;li&gt;어떤 특정 연결 내에서 BEGIN TRANSACION 문과 COMMIT 문 사이의 모든 것은 같은 트랜잭션에 속하는 것으로 여겨진다.&lt;/li&gt;
&lt;li&gt;비관계형 데이터베이스는 이런 식으로 연산을 묶는 방법이 없는 경우가 많아, 어떤 연산은 성공하고 나머지 연산은 실패해도 데이터베이스가 부분적으로 갱신된 상태가 될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단일 객체 쓰기
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;저장소 엔진들은 거의 보편적으로 한 노드에 존재하는 (키-값 쌍 같은) 단일 객체 수준에서 원자성과 격리성을 제공하는 것을 목표로 한다.&lt;/li&gt;
&lt;li&gt;원자성은 장애 복구용 로그를 써서 구현할 수 있고, 격리성은 각 객체에 잠금을 사용해(동시에 한 스레드만 객체에 접근하도록) 구현할 수 있다.&lt;/li&gt;
&lt;li&gt;여러 클라이언트에서 동시에 같은 객체에 쓰려고 할 때 갱신 손실을 방지하므로 유용하지만, 일반적으로 쓰이는 의미의 트랜잭션은 아니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;오류와 어보트 처리
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;트랜잭션의 핵심 기능은 오류가 생기면 어보트되고 안전하게 재시도할 수 있다는 것이다.&lt;/li&gt;
&lt;li&gt;데이터베이스가 원자성, 격리성, 또는 지속성 보장을 위반할 위험이 있으면 트랜잭션이 절반 정도 완료된 상태에 머물게 하는 대신 트랜잭션을 완전히 폐기한다.&lt;/li&gt;
&lt;li&gt;어보트된 트랜잭션을 재시도하는 것은 간단하고 효과적인 오류 처리 매커니즘이지만 완벽하지는 않다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완화된 격리 수준&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동시성 문제(경쟁 조건)는 트랜잭션이 다른 트랜잭션에서 동시에 변경한 데이터를 읽거나 두 트랜잭션이 동시에 같은 데이터를 변경하려고 할 때 나타난다.&lt;/li&gt;
&lt;li&gt;데이터베이스는 오랫동안 트랜잭션 격리를 제공함으로써 애플리케이션 개발자들에게 동시성 문제를 감추려고 했다.&lt;/li&gt;
&lt;li&gt;직렬성 격리는 데이터베이스가 여러 트랜잭션들이 직렬적으로 실행되는 것(즉 동시성 없이 한 번에 트랜잭션 하나만 실행)과 동일한 결과가 나오도록 보장한다는 것을 의미한다.&lt;/li&gt;
&lt;li&gt;하지만 직렬성 격리는 성능 비용이 있기 때문에, 완화된 격리 수준을 사용하는(어떤 동시성 이슈로부터는 보호하지만, 모든 이슈로부터는 보호하지 않는) 시스템들이 흔하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커밋 후 읽기&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 기본적인 수준의 트랜잭션 격리는 커밋 후 읽기(read committed)이다.&lt;/li&gt;
&lt;li&gt;두 가지를 보장
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;데이터베이스에서 읽을 때 커밋된 데이터만 보게 된다(더티 읽기가 없음).&lt;/li&gt;
&lt;li&gt;데이터베이스에 쓸 때 커밋된 데이터만 덮어쓰게 된다(더티 쓰기가 없음).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;더티 읽기 방지
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;더티 읽기는 다른 트랜잭션에서 커밋되지 않는 데이터를 보는 경우를 의미한다.&lt;/li&gt;
&lt;li&gt;커밋 후 읽기 격리 수준에서 실행되는 트랜잭션은 더티 읽기를 막아야 한다.&lt;/li&gt;
&lt;li&gt;트랜잭션이 쓴 내용은 커밋된 후에야 다른 트랜잭션에게 보여야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;더티 쓰기 방지
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;더티 쓰기는 나중에 실행된 쓰기 작업이 커밋되지 않은 값(먼저 실행된 쓰기 작업)을 덮어써버리는 경우를 의미한다.&lt;/li&gt;
&lt;li&gt;커밋 후 읽기 격리 수준에서 실행되는 트랜잭션은 더티 쓰기를 막아야 한다.&lt;/li&gt;
&lt;li&gt;보통 먼저 쓴 트랜잭션이 커밋되거나 어보트될 때까지 두 번째 쓰기를 지연시키는 방법을 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;구현 방법
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;로우 수준 잠금을 사용해 더티 쓰기를 방지한다.&lt;/li&gt;
&lt;li&gt;트랜잭션에서 특정 객체(로우나 문서)를 변경하고 싶다면 먼저 해당 객체에 대한 잠금을 획득해야 한다. 그리고 트랜잭션이 커밋되거나 어보트될 때까지 잠금을 보유하고 있어야 한다. 오직 한 트랜잭션만 어떤 주어진 객체에 대한 잠금을 보유할 수 있다.&lt;/li&gt;
&lt;li&gt;더티 읽기를 방지하는 방법으로는 쓰여진 모든 객체에 대해 데이터베이스가 과거에 커밋된 값과 현재 쓰기 잠금을 갖고 있는 트랜잭션에서 쓴 새로운 값을 모두 기억하고, 해당 트랜잭션이 실행 중인 동안 그 객체를 읽는 다른 트랜잭션들은 과거의 값을 읽게 하는 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스냅숏 격리와 반복 읽기&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;커밋 후 읽기 격리 수준을 사용하더라도 동시성 버그가 생길 수 있는 경우가 많다.&lt;/li&gt;
&lt;li&gt;ex) 비반복 읽기(monrepeatable read)나 읽기 스큐(read skew), 일관성이 깨진 상태인 데이터베이스를 보게되는 경우&lt;/li&gt;
&lt;li&gt;일시적인 비일관성이 문제가 되는 경우
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;백업 : 백업 프로세스가 실행되는 동안에도 계속 데이터베이스에 쓰기가 실행되기 때문에, 백업의 일부는 데이터의 과거 버전을 다른 부분은 새 버전을 갖게 된다. 이러한 백업을 사용해서 복원하면 비일관성이 영속적이게 된다.&lt;/li&gt;
&lt;li&gt;분석 질의와 무결성 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;스냅숏 격리
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;위 문제의 가장 흔한 해결책&lt;/li&gt;
&lt;li&gt;각 트랜잭션은 데이터베이스의 일관된 스냅숏으로부터 읽는다.&lt;/li&gt;
&lt;li&gt;즉 트랜잭션은 시작할 때 데이터베이스에 커밋된 상태였던 모든 데이터를 본다.&lt;/li&gt;
&lt;li&gt;데이터가 나중에 다른 트랜잭션에 의해 바뀌더라도 각 트랜잭션은 특정한 시점의 과거 데이터를 보게 된다.&lt;/li&gt;
&lt;li&gt;백업이나 분석처럼 실행하는 데 오래 걸리며 읽기만 실행하는 질의에 유용하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;스냅숏 격리 구현
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;커밋 후 읽기 격리처럼 전형적으로 더티 쓰기를 방지하기 위해 쓰기 잠금을 사용하고, 읽을 때는 잠금이 필요하지 않다.&lt;/li&gt;
&lt;li&gt;핵심 원리는 읽는 쪽에서 쓰는 쪽을 결코 차단하지 않고 쓰는 쪽에서 읽는 쪽을 결코 차단하지 않는다는 것이다. 데이터베이스는 잠금 경쟁 없이 쓰기 작업이 일상적으로 처리되는 것과 동시에 일관성 있는 스냅숏에 대해 오래 실행되는 읽기 작업을 처리할 수 있다.&lt;/li&gt;
&lt;li&gt;데이터베이스는 객체마다 커밋된 버전 여러 개를 유지할 수 있어야 한다. =&amp;gt; 다중 버전 동시성 제어(multi-version concurrency control, MVCC)&lt;/li&gt;
&lt;li&gt;커밋 후 읽기는 질의마다 독립된 스냅숏을 사용하고, 스냅숏 격리는 전체 트랜잭션에 대해 동일한 스냅숏을 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;일관된 스냅숏을 보는 가시성 규칙
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;트랜잭션은 데이터베이스에서 객체를 읽을 때 트랜잭션 ID를 사용해 어떤 것을 볼 수 있고 어떤 것을 볼 수 없는지 결정한다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;데이터베이스는 각 트랜잭션을 시작할 때 그 시점에 진행 중인 모든 트랜잭션의 목록을 만든다. 이 트랜잭션들이 쓴 데이터는 모두 무시된다. 설령 데이터를 쓴 트랜잭션이 나중에 커밋되더라도 마찬가지다.&lt;/li&gt;
&lt;li&gt;어보트된 트랜잭션이 쓴 데이터는 모두 무시된다.&lt;/li&gt;
&lt;li&gt;트랜잭션 ID가 더 큰(즉 현재 트랜잭션이 시작한 후에 시작한) 트랜잭션이 쓴 데이터는 그 트랜잭션의 커밋 여부에 관계없이 모두 무시된다.&lt;/li&gt;
&lt;li&gt;그 밖의 모든 데이터는 애플리케이션의 질의로 볼 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;이 규칙은 객체 생성과 삭제 모두에 적용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;색인과 스냅숏 격리
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;다중 버전 데이터베이스에서 색인 동작 방식&lt;/li&gt;
&lt;li&gt;색인이 객체의 모든 버전을 가리키게 하고 색인 질의가 현재 트랙잭션에서 볼 수 없는 버전을 걸러내게 한다. 가비지 컬렉션이 어떤 트랜잭션에게도 더 이상 보이지 않는 오래된 객체 버전을 삭제할 때 대응되는 색인 항목도 삭제된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;스냅숏 격리는 유용한 격리 수준이며 특리 읽기 전용 트랜잭션에 유용하며, 오라클에서는 직렬성, 포스트그레스큐엘과 MySQL 에서는 반복 읽기라고 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;갱신 손실 방지&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동시에 실행되는 쓰기 트랜잭션 사이에 발생할 수 있는 충돌로, 애플리케이션이 데이터베이스에서 값을 읽고 변경한 후 변경된 값을 다시 쓸 때(read-modify-write 주기) 발생할 수 있다.&lt;/li&gt;
&lt;li&gt;만약 두 트랜잭션이 이 작업을 동시에 하면 두 번째 쓰기 작업이 첫 번째 변경을 포함하지 않으므로 변경 중 하나는 손실될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원자적 쓰기 연산&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;여러 데이터베이스에서 원자적 갱신 연산을 제공한다.&lt;/li&gt;
&lt;li&gt;UPDATE counters SET value = value + 1 WHERE key = 'foo';&lt;/li&gt;
&lt;li&gt;대부분의 관계형 데이터베이스에서 동시성 안전(concnrrency-safe)하다.&lt;/li&gt;
&lt;li&gt;원자적 연산은 객체를 읽을 때 그 객체에 독점적인 잠금을 획득해서 구현한다. 갱신이 적용될 때까지 다른 트랜잭션에서 그 객체를 읽지 못하게 한다. =&amp;gt; 커서 안정성(cursor stability)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명시적 잠금&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터베이스에 내장된 원자적 연산이 필요한 기능을 제공하지 않는 경우 갱신 손실을 막기 위해 애플리케이션에서 갱신할 객체를 명시적으로 잠글 수 있다.&lt;/li&gt;
&lt;li&gt;애플리케이션이 read-modify-write 주기를 수행하고 있을 때 다른 트랜잭션이 동시에 같은 객체를 읽으려고 할 때 첫번째 주기가 완료될 때까지 기다리도록 강제된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;갱신 손실 자동 감지&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;병렬 실행을 허용하고 트랜잭션 관리자가 갱신 손실을 발견하면 트랜잭션을 어보트시키고 read-modify-write 주가를 재시도하도록 강제하는 방법이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compare-and-set&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;값을 마지막으로 읽은 후로 변경되지 않았을 때만 갱신을 혀용함으로써 갱신 손실을 회피한다.&lt;/li&gt;
&lt;li&gt;현재 값이 이전에 읽은 값과 일치하지 않으면 갱신은 반영되지 않고 read-modify-write 주기를 재시도해야 한다.&lt;/li&gt;
&lt;li&gt;UPDATE&lt;span style=&quot;color: #666666; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;wiki_pages&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;SET&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;content =&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;'new content' &lt;/span&gt;&lt;/span&gt;WHERE&lt;span style=&quot;color: #666666; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;id = 1234&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;AND&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;content =&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;'old content';&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&amp;nbsp;충돌 해소와 복제&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;복제가 적용된 데이터베이스의 경우 여러 노드에 데이터의 복사본이 있어서 데이터가 다른 노드들에서 동시에 변경될 수 있으므로 갱신 손실을 방지하려면 추가 단계가 필요하다.&lt;/li&gt;
&lt;li&gt;쓰기가 동시에 실행될 때 한 값에 대해 여러 개의 충돌된 버전(형제,sibling)을 생성하는 것을 허용하고 사후에 애플리케이션 코드나 특별한 데이터 구조를 사용해 충돌을 해소하고 이 버전들을 병합한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓰기 스큐와 팬덤&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쓰기 스큐(write skew)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;트랙잭션이 동시에 실행됐을 때 발생하는 이상 현상으로, 두 트랜잭션이 두 개의 다른 객체를 갱신하므로 더티 쓰기도 갱신 손실도 아니다.&lt;/li&gt;
&lt;li&gt;두 트랜잭션이 같은 객체들을 읽어서 그 중 일부를 갱신할 때 나타날 수 있다.(다른 트랜잭션은 다른 객체를 갱신한다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;예시
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;병원의 호출 대기 관리 애플리케이션&lt;/li&gt;
&lt;li&gt;회의실 예약 시스템&lt;/li&gt;
&lt;li&gt;다중플레이어 게임&lt;/li&gt;
&lt;li&gt;사용자명 획득&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;쓰기 스큐 패턴
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;SELECT 질의가 어떤 검색 조건에 부합하는 로우를 검색함으로써 어떤 요구사항을 만족하는지 확인한다.&lt;/li&gt;
&lt;li&gt;첫 번째 질의의 결과에 따라 애플리케이션 코드는 어떻게 진행할지(해당 연산을 계속 처리할지 사용자에게 오류를 보고하고 중단할지) 결정한다.&lt;/li&gt;
&lt;li&gt;애플리케이션이 계속 처리하기로 결정했다면 데이터베이스에 쓰고 트랜잭션을 커밋한다. =&amp;gt; 해당 커밋 후 1단계 SELECT 질의를 재실행하면 다른 결과를 얻게 된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;팬텀(phantom)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;어떤 트랜잭션에서 실행한 쓰기가 다른 트랜잭션의 검색 질의 결과를 바꾸는 효과&lt;/li&gt;
&lt;li&gt;스냅숏 격리는 읽기 전용 질의에서는 팬텀을 회피하지만 읽기 쓰기 트랜잭션에서는 팬텀이 쓰기 스큐의 까다로운 경우를 유발할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;충돌 구체화(materializing conflict)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;팬텀을 데이터베이스에 존재하는 구체적인 로우 집합에 대한 잠금 충돌로 변환하는 것(ex. 회의실 예약)&lt;/li&gt;
&lt;li&gt;방법을 알아내기 어렵고 오류가 발생하기 쉽다.&lt;/li&gt;
&lt;li&gt;동시성 제어 메커니즘이 애플리케이션 데이터 모델로 새러 나오는 것이 보기 좋지 않다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직렬성&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;격리 수준의 문제점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;격리 수준은 이해하기 어렵고 데이터베이스가 그 구현에 일관성이 없다.&lt;/li&gt;
&lt;li&gt;애플리케이션 코드를 보고 특정한 격리 수준에서 해당 코드를 실행하는게 안전한지 알기 어렵다.&lt;/li&gt;
&lt;li&gt;경쟁 조건을 감지하는데 도움이 되는 좋은 도구가 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;문제를 해결하기 위해 직렬성 격리를 사용&lt;/li&gt;
&lt;li&gt;직렬성 격리
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;보통 가장 강력한 격리 수준이다.&lt;/li&gt;
&lt;li&gt;여러 트랜잭션이 병렬로 실행되더라도 최종 결과는 동시성 없이 한 번에 하나씩 직렬로 실행될 때와 같도록 보장한다.&lt;/li&gt;
&lt;li&gt;데이터베이스가 발생할 수 있는 모든 경쟁 조건을 막아준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;직렬성 격리 기법
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;트랜잭션을 순차적으로 실행하기&lt;/li&gt;
&lt;li&gt;2단계 잠금&lt;/li&gt;
&lt;li&gt;직렬성 스냅숏 격리 같은 낙관적 동시성 제어 기법&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제적인 직렬 실행&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동시성 문제를 피하는 가장 간단한 방법은 동시성을 완전히 제거하는 것&lt;/li&gt;
&lt;li&gt;한 번에 트랜잭션 하나씩만 직렬로 단일 스레드에서 실행하면 된다. 그러면 트랜잭션 사이의 충돌을 감지하고 방지하는 문제를 완전히 회피할 수 있어 격리 수준은 직렬성 격리가 된다.&lt;/li&gt;
&lt;li&gt;단일 스레드 실행이 가능한 이유
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;램 가격이 저렴해져서 많은 사용 사례에서 활성화된 데이터셋 전체를 메모리에 유지할 수 있다. 트랜잭션이 접근해야 하는 모든 데이터가 메모리에 있는 경우 데이터를 디스크에서 읽어 오기를 기다려야 할 때보다 트랜잭션이 훨씬 빨리 실행될 수 있다.&lt;/li&gt;
&lt;li&gt;OLTP 트랜잭션은 보통 짧고 실행하는 읽기와 쓰기의 개수가 적다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션을 스토어드 프로시저 안에 캡슐화하기&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OLTP 애플리케이션은 트랜잭션 내에서 대화식으로 사용자 응답을 대기하는 것을 회피함으로써 트랜잭션을 짧게 유지한다.&lt;/li&gt;
&lt;li&gt;트랜잭션은 계속 상호작용하는 클라이언트/서버 스타일로 실행돼 왔고, 한 번에 구문 하나씩 실행하는 방식이다.&lt;/li&gt;
&lt;li&gt;상호작용식 트랜잭션 : 애플리케이션 코드와 데이터베이스 서버 사이에서 질의와 결과를 주고받는다. 이는 네트워크 통신에 많은 시간을 소비하게 된다.&lt;/li&gt;
&lt;li&gt;애플리케이션은 트랜잭션 코드 전체를 스토어드 프로시저 형태로 데이터베이스에 미리 제출한다. 트랜잭션에 필요한 데이터는 모두 메모리에 있고 스토어드 프로시저는 네트워크나 디스크 I/O 대기 없이 매우 빨리 실행된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스토어드 프로시저의 장단점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;데이터베이스 벤거마다 제각각 스토어드 프로시저용 언어가 있다.(라이브러리 생태계가 약하다.)&lt;/li&gt;
&lt;li&gt;데이터베이스에서 실행되는 코드는 관리가 어렵다.&lt;/li&gt;
&lt;li&gt;데이터베이스는 애플리케이션 서버보다 훨씬 더 성능에 민감할 때가 많다. 여러 애플리케이션 서버에서 데이터베이스 인스턴스 하나를 공유하기 때문이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;스토어드 프로시저가 있고 데이터가 메모리에 저장된다면 모든 트랜잭션을 단일 스레드에서 실행하는 게 현실성 있다.&lt;/li&gt;
&lt;li&gt;&amp;nbsp;장점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;I/O 대기가 필요없다.&lt;/li&gt;
&lt;li&gt;다른 동시성 제어 메커니즘의 오버헤드를 회피하므로 단일 스레드로 상당히 좋은 처리량을 얻을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티셔닝&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;읽기 전용 트랜잭션은 스냅숏 격리를 사용해 다른 곳에서 실행될 수 있지만 쓰기 처리량이 높은 애플리케이션에게는 단일 스레드 트랜잭션 처리자가 심각한 병목이 될 수 있다.&lt;/li&gt;
&lt;li&gt;단일 파티션
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;각 트랜잭션이 단일 파티션 내에서만 데이터를 읽고 쓰도록 데이터셋을 파티셔닝하는 경우 각 파티션은 다른 파티션과 독립적으로 실행되는 자신만의 트랜잭션 처리 스레드를 가질 수 있다.&lt;/li&gt;
&lt;li&gt;각 CPU 코에어 각자의 파티션을 할당해서 트랜잭션 처리량을 CPU 코어 개수레 맞춰 선형적으로 확장할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;여러 파티션
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;데이터베이스가 해당 트랜잭션이 접근하는 모든 파티션에 걸쳐서 코디네이션을 해야 한다.&lt;/li&gt;
&lt;li&gt;스토어드 프로시저는 전체 시스템에 걸쳐 직렬성을 보장하기 위해 모든 파티션에 걸쳐 잠금을 획득한 단계에서 실행돼야 한다.&lt;/li&gt;
&lt;li&gt;추가적인 코디네이션 오버헤드로 인해 단일 파티션 트랜잭션보다 엄청나게 느리고 처리량이 매우 낮다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직렬 실행 요약&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 트랜잭션은 작고 빨라야 한다.&lt;/li&gt;
&lt;li&gt;활성화된 데이터셋이 메모리에 적재될 수 있는 경우로 사용이 제한된다.&lt;/li&gt;
&lt;li&gt;쓰기 처리량이 단일 CPU 코어에서 처리할 수 있을 정도로 충분히 낮아야 한다.&lt;/li&gt;
&lt;li&gt;여러 파티션에 걸친 트랜잭션도 쓸 수 있지만 이것을 사용할 수 있는 정도에는 엄격한 제한이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2단계 잠금(two-phase locking, 2PL)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;직렬성을 구현하는 데 널리 쓰이는 유일한 알고리즘&lt;/li&gt;
&lt;li&gt;쓰기를 실행하는 트랜잭션이 없는 객체는 여러 트랜잭션에서 동시에 읽을 수 있지만, 누군가 어떤 객체에 쓰러고하면 독점적인 접근이 필요하다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;트랜잭션 A가 객체 하나를 읽고 트랜잭션 B가 그 객체에 쓰기를 원한다면 B는 진행하기 전에 A가 커밋되거나 어보트될 때까지 기다려야 한다.&lt;/li&gt;
&lt;li&gt;트랜잭션 A가 객체에 썼고 트랜잭션 B가 그 객체를 읽기 원한다면 B는 진행하기 전에 A가 커밋되거나 어보트될 때까지 기다려야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;쓰기 트랜잭션은 다른 쓰기 트랜잭션뿐만 아니라 읽기 트랜잭션도 진행하지 못하게 막고 그 역도 성립한다.&lt;/li&gt;
&lt;li&gt;스냅숏 격리의 '읽는 쪽은 결코 쓰는 쪽을 막지 않으며 쓰는 쪽도 결코 읽는 쪽을 막지 않는다'는 원칙과 차이가 존재한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2단계 잠금 구현&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;잠금은 공유 모드(shared mode)나 독점 모드(exclusive mode)로 사용될 수 있다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;트랜잭션이 객체를 읽기 원한다면 먼저 공유 모드로 잠금을 획득해야 한다. 동시에 여러 트랜잭션이 공유 모드로 잠금을 획득하는 것은 허용되지만 만약 그 객체에 이미 독점 모드로 잠금을 획득한 트랜잭션이 있으면 해당 트랜잭션이 완료될 때까지 기다려야 한다.&lt;/li&gt;
&lt;li&gt;트랜잭션이 객체에 쓰기를 원한다면 먼저 독점 모드로 잠금을 획득해야 한다.(동시 획득 불가능)&lt;/li&gt;
&lt;li&gt;트랜잭션이 객체를 읽다가 쓰기를 실행할 때는 공유 잠금을 독점 잠금으로 업그레이드해야 한다.&lt;/li&gt;
&lt;li&gt;트랜잭션이 잠금을 획득한 후에는 트랜잭션이 종료될 때까지 잠금을 갖고 있어야 한다.(1단계는 잠금을 획득, 2단계는 모든 잠금을 해제)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;교착 상태가 발생할 수 있다. 데이터베이스는 교착 상태를 자동으로 감지하고 트랜잭션 중 하나를 어보트시켜서 다른 트랜잭션들이 진행할 수 있게 하고 어보트된 트랜잭션은 애플리케이션에서 재시도해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2단계 잠금 성능&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;완화된 격리 수준보다 트랜잭션 처리량과 질의 응답 시간이 크게 나빠진다.&lt;/li&gt;
&lt;li&gt;원인
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;잠금을 획득하고 해제하는 오버헤드&lt;/li&gt;
&lt;li&gt;동시성 감소&lt;/li&gt;
&lt;li&gt;관계형 데이터베이스에서 트랜잭션의 실행 시간을 제한하지 않기 때문에 잠금을 획득한 트랜잭션이 끝날 때까지 무한정 대기하게 되고 대기열이 생겨 여러 다른 트랜잭션들이 완료되기를 기다려야 한다.&lt;/li&gt;
&lt;li&gt;교착 상태가 자주 발생하고, 교착 상태로 인해 어보트된 트랜잭션을 재시도하면 작업을 전부 다시 해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서술 잠금(predicate lock)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;팬텀을 방지하기 위해 사용되며, 어떤 검색 조건에 부합하는 모든 객체를 잠금&lt;/li&gt;
&lt;li&gt;구현
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;트랜잭션 A가 어떤 조건에 부합하는 객체를 읽기 원한다면 질의의 조건에 대한 공유 모드 서술 잠금을 획득해야 한다. 다른 트랜잭션이 그 조건에 부합하는 어떤 객체에 독점 잠금을 갖고 있으면 질의를 실행하도록 허용되기 전에 잠금이 해제하기를 기다려야 한다.&lt;/li&gt;
&lt;li&gt;어떤 객체를 삽입, 갱신, 삭제하기를 원한다면 먼저 기존 값이나 새로운 값 중에 기존의 서술 잠금에 부합하는게 있는지 확인하고, 부합하는게 있다면 종료될 때까지 대기해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;서술 잠금은 데이터베이스에 아직 존재하지 않지만 미래에 추가될 수 있는 객체에도 적용할 수 있다.&lt;/li&gt;
&lt;li&gt;2단계 잠금이 서술 잠금을 포함하면 데이터베이스에서 모든 형태의 쓰기 스큐와 다른 경쟁 조건을 막을 수 있어서 격리 수준이 직렬성 격리가 된다.&lt;/li&gt;
&lt;li&gt;하지만 진행 중인 트랜잭션들이 획득한 잠금이 많으면 조건에 부합하는 잠금을 확인하는 데 시간이 오래 걸린다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;색인 범위 잠금(index-range locking, 다음 키 잠금(next-key locking))&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;명확히 조건에 부합하는 객체만 잠그는 것이 아니라 근사 조건에 부합하면 잠금&lt;/li&gt;
&lt;li&gt;보통 검색 조건이 색인에 붙는 경우가 많아 색인에 잠금을 건다.&lt;/li&gt;
&lt;li&gt;다른 트랜잭션에서 해당 로우를 갱신하고 싶다면 색인의 같은 부분을 갱신해야 한다. 공유 잠금을 발견하는 경우 해제될 때까지 대기해야 한다.&lt;/li&gt;
&lt;li&gt;팬텀과 쓰기 스큐로부터 보호해주는 효과가 있다.&lt;/li&gt;
&lt;li&gt;서술 잠금보다 정밀하지는 않지만 오버헤드가 훨씬 낮다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직렬성 스냅숏 격리(serializable snapshot isolation, SSI)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2단계 잠금 : 성능이 좋지 않음&lt;/li&gt;
&lt;li&gt;직렬성 구현 : 확장이 잘 되지 않음&lt;/li&gt;
&lt;li&gt;완화된 격리 수준 : 성능은 좋지만 다양한 경쟁 조건(갱신 손실, 쓰기 스큐, 팬텀 등)에 취약&lt;/li&gt;
&lt;li&gt;직렬설 스냅숏 격리는 완전한 직렬성을 제공하지만 스냅숏 격리에 비래 약간의 성능 손해만 존재한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비관적 동시성 제어 vs 낙관적 동시성 제어&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비관적 동시성 제어
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;2단계 잠금 : 뭔가 잘못될 가능성이 있으면 뭔가를 하기 전에 상황이 다시 안전해질 때까지 기다리는게 낫다는 원칙을 기반&amp;nbsp;&lt;/li&gt;
&lt;li&gt;직렬 실행 : 각 트랜잭션이 실행되는 동안 전체 데이터베이스에 독점 잠금을 획득하는 것과 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;낙관적 동시성 제어
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;직렬성 스냅숏 격리 : 뭔가 위험한 상황이 발생할 가능성이 있을 때 트랜잭션을 막는 대신 모든 것이 괜찮아질 거라는 희망을 갖고 계속 진행한다.&lt;/li&gt;
&lt;li&gt;트랜잭션이 커밋되기를 원할 때 데이터베이스는 나쁜 상황이 발생했는지 확인하고, 그렇다면 어보트되고 재시도한다.&lt;/li&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;경쟁이 심하면 어보트시켜야 할 트랜잭션의 비율이 높아지므로 성능이 떨어진다.&lt;/li&gt;
&lt;li&gt;시스템이 최대 처리량에 근접한 경우 재시도되는 트랜잭션으로 인해 성능이 저하될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;장점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;예비 용량이 충분하고 트랜잭션 사이의 경쟁이 심하지 않는 경우 성능이 더 좋다. 경쟁은 가환 원자적 연산을 통해 줄일 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뒤쳐진 전제에 기반한 결정&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션은 어떤 전제를 기반으로 동작하는데, 해당 트랜잭션이 커밋하려고 할 때 원래 데이터가 바뀌어서 그 전제가 더 이상 참이 아닐 수 있다.&lt;/li&gt;
&lt;li&gt;직렬성 격리를 제공하려면 데이터베이스는 트랜잭션이 뒤처진 전체를 기반으로 동작하는 상황을 감지하고 그런 상황에서는 트랜잭션을 어보트시켜야 한다.&lt;/li&gt;
&lt;li&gt;질의 결과를 감지하는 방법
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;오래된 MVCC 객체 버전을 읽었는지 감지하기(읽기 전에 커밋되지 않은 쓰기가 발생했음)&lt;/li&gt;
&lt;li&gt;과거의 읽기에 영향을 미치는 쓰기 감지하기(읽은 후에 쓰기가 실행됨)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직렬성 스냅숏 격리의 성능&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션이 다른 트랜잭션들이 잡고 있는 잠금을 기다리느라 차단될 필요가 없다.&lt;/li&gt;
&lt;li&gt;읽기 전용 질의는 어떤 잠금도 없이 일관된 스냅숏 위에서 실행될 수 있다.&lt;/li&gt;
&lt;li&gt;단일 CPU 코어의 처리량에 제한되지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>STUDY/데이터 중심 애플리케이션 설계</category>
      <author>level_?</author>
      <guid isPermaLink="true">https://it-study.tistory.com/128</guid>
      <comments>https://it-study.tistory.com/128#entry128comment</comments>
      <pubDate>Tue, 19 Aug 2025 22:22:50 +0900</pubDate>
    </item>
    <item>
      <title>6장. 파티셔닝</title>
      <link>https://it-study.tistory.com/127</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;파티셔닝&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터셋이 매우 크거나 질의 처리량이 매우 높다면 복제만으로는 부족하고 데이터를 &lt;b&gt;파티션&lt;/b&gt;으로 쪼갤 필요가 있다. &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이 작업을 &lt;/span&gt;&lt;b&gt;샤딩&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이라고도 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;파티션을 나눌 때는 보통 각 데이터 단위(레코드, 로우, 문서)가 하나의 파티션에 속하게 한다. 데이터베이스가 여러 파티션을 동시에 건드리는 연산을 지원할 수도 있지만 결과적으로 각 파티션은 그 자체로 작은 데이터베이스가 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;파티셔닝을 하는 주된 이유는 &lt;/span&gt;&lt;b&gt;확장성&lt;/b&gt;이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비공유 클러스터에서 다른 파티션은 다른 노드에 저장될 수 있다. 따라서 대용량 데이터셋이 여러 디스크에 분산될 수 있고 질의 부하는 여러 프로세서에 분산될 수 있다.&lt;/li&gt;
&lt;li&gt;단일 파티션에 실행되는 질의는 각 노드에서 자신의 파티션에 해당하는 질의를 독립적으로 실행할 수 있으므로 노드를 추가함으로써 질의 처리량을 늘릴 수 있다. 크고 복잡한 질의의 경우 여러 노드에서 병렬 실행이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;파티셔닝과 복제&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;보통 복제와 파티셔닝을 함께 적용해 각 파티션의 복사본을 여러 노드에 저장한다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;각 레코드는 정확히 한 파티션에 속하더라도 이를 여러 다른 노드에 저장해서 내결함성을 보장할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;한 노드에 여러 파티션을 저장할 수도 있다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;ex. 리더 팔로워 복제 모델&lt;/li&gt;
&lt;li&gt;각 파티션의 리더는 하나의 노드에 할당되고 팔로워들은 다른 노드에 할당된다.&lt;/li&gt;
&lt;li&gt;각 노드는 어떤 파티션에게는 리더로 어떤 파티션에게는 팔로워로 동작한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;키-값 데이터 파티셔닝&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파티셔닝의 목적은 데이터와 질의 부하를 노드 사이에 고르게 분산시키는 것이다.&lt;/li&gt;
&lt;li&gt;파티셔닝이 고르게 이뤄지지 않아 다른 파티션보다 데이터가 많거나 질의를 많이 받는 파티션이 있다면 쏠렸다(skewed)고 말하고, 파티셔닝의 효과가 매우 떨어지게 된다.&lt;/li&gt;
&lt;li&gt;이 때, 불균형하게 부하가 높은 파티션을 핫스팟이라고 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;키 범위 기준 파티셔닝&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 파티션에 연속된 범위(어떤 최솟값에서 최댓값까지)의 키를 할당하는 것&lt;/li&gt;
&lt;li&gt;장점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;각 범위들 사이의 경계를 알면 어떤 키가 어느 파티션에 속하는지 쉽게 찾을 수 있다.&lt;/li&gt;
&lt;li&gt;어떤 파티션이 어느 노드에 할당됐는지 알면 적절한 노드로 요청을 직접 보낼 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;키 범위 크기가 반드시 동일할 필요는 없다. 데이터를 고르게 분산시키려면 파티션 경계를 데이터에 맞춰 조정해야 한다.&lt;/li&gt;
&lt;li&gt;파티션 경계는 관리자가 수동으로 선택하거나 데이터베이스에서 자동으로 선택되게 할 수 있다.&lt;/li&gt;
&lt;li&gt;각 파티션 내에서는 키를 정렬된 순서로 저장할 수 있다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;범위 스캔이 쉬워진다.&lt;/li&gt;
&lt;li&gt;키를 연쇄된 색인으로 간주해서 질의 하나로 관련 레코드 여러 개를 읽어오는 데 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;문제점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;특정한 접근 패턴이 핫스팟을 유발할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;키의 해시값 기준 파티셔닝&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쏠림과 핫스팟의 위험 때문에 많은 분산 데이터스토어는 키의 파티션을 정하는 데 해시 함수를 사용한다.&lt;/li&gt;
&lt;li&gt;좋은 해시 함수는 쏠린 데이터를 입력으로 받아 균일하게 분산되게 한다.&lt;/li&gt;
&lt;li&gt;파티셔닝용 해시 함수는 암호적으로 강력할 필요는 없다.&lt;/li&gt;
&lt;li&gt;카에 적합한 해시 함수를 구했다면 각 파티션에 해시값 범위를 할당하고 해시값이 파티션의 범위에 속하는 모든 키를 그 파티션에 할당하면 된다.&lt;/li&gt;
&lt;li&gt;일관성 해시 : 파티션 경계는 크기가 동일하도록 나눌 수 있고 무작위에 가깝게 선택할 수도 있다.&lt;/li&gt;
&lt;li&gt;문제점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;범위 질의를 효율적으로 실행할 수 있는 키 범위 파티셔닝의 좋은 속성을 잃어 버린다.&lt;/li&gt;
&lt;li&gt;전에는 인접했던 키들이 이제는 모든 파티션에 흩어져서 정렬 순서가 유지되지 않는다.&lt;/li&gt;
&lt;li&gt;몽고 DB에서는 해시 기반 샤딩 모드를 활성화하면 범위 질의가 모든 파티션에 전송돼야 한다. 리악, 카우치베이스, 볼드모트에서는 기본키에 대한 범위 질의가 지원되지 않는다.&lt;/li&gt;
&lt;li&gt;카산드라는 테이블을 선언할 때 여러 컬럼을 포함하는 복합 기본키를 지정할 수 있다. 키의 첫 부분에만 해싱을 적용해 파티션 경정에 사용하고 남은 컬럼은 카산드라의 SS테이블에서 데이터를 정렬하는 연쇄된 색인으로 사용한다. =&amp;gt; 첫 번째 컬럼에 고정된 값을 지정하면 키의 다른 컬럼에 대해서는 범위 스캔을 실행할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;쏠린 작업부하와 핫스팟 완화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키를 해싱해서 파티션을 정하면 핫스팟을 줄이는 데 도움이 되지만, 핫스팟을 완벽히 제거할 수는 없다. 항상 동일한 키를 읽고 쓰는 극단적인 상황에서는 모든 요청이 동일한 파티션으로 쏠리게 된다. 현대 데이터 시스템은 대부분 크게 쏠린 작업부하를 자동으로 보정하지 못하므로 애플리케이션에서 쏠림을 완화해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;파티셔닝과 보조 색인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;레코드를 기본키를 통해서만 접근한다면 키로부터 파티션을 결정하고 이를 사용해 해당 키를 담당하는 파티션으로 읽기 쓰기 요청을 전달할 수 있다. 그러나 특정한 값이 발생한 항목을 검색하는 수단인 보조 색인이 연관되는 경우 상황이 복잡해는데, 보조 색인은 파티션에 깔끔하게 대응되지 않는 문제점이 있다. 보조 색인이 있는 데이터베이스를 파티셔닝하는 방법을 알아보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;문서 기준 보조 색인 파티셔닝&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;보조 색인을 기본키와 값이 저장된 파티션에 저장한다.&lt;/li&gt;
&lt;li&gt;각 파티션은 자신의 보조 색인을 유지하며 그 파티션에 속하는 문서만 담당한다. 다른 파티션에 어떤 데이터가 저장되는지는 신경쓰지 않는다.&lt;/li&gt;
&lt;li&gt;데이터베이스에 문서 추가, 삭제, 갱신 등의 쓰기 작업을 실행할 때는 쓰려고 하는 문서 ID를 포함하는 파티션만 다루면 된다. 이러한 이유로 지역 색인(local index)이라고도 한다.&lt;/li&gt;
&lt;li&gt;주의할 점&lt;br /&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;문서ID에 뭔가 특별한 작업을 하지 않는다면 특정한 색상이거나 특정한 제조사가 만든 자동차가 동일한 파티션에 저장되리라는 보장이 없다.&lt;/li&gt;
&lt;li&gt;따라서 모든 파티션으로 질의를 보내서 얻은 결과를 모두 모아햐 한다.&lt;/li&gt;
&lt;li&gt;파티셔닝된 데이터베이스에 이런 식으로 질의를 보내는 방법을 스캐터/개더(scatter/gather)라고도 하는데 보조 색인을 써서 읽는 질의는 큰 비용이 들 수 있다.&lt;/li&gt;
&lt;li&gt;여러 파티션에서 질의를 병렬 실행하더라도 스캐터/개더는 꼬리 지연 시간 증폭이 발생하기 쉽다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;용어 기준 보조 색인 파티셔닝&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 파티션의 데이터를 담당하는 전역 색인이다.&lt;/li&gt;
&lt;li&gt;한 노드에만 색인을 저장할 경우 해당 노드가 병목이 되어 파티셔닝의 목적을 해칠 수 있기 때문에 여러 노드에 나눠 저장해야 한다. 즉, 색인된 값을 사용해서 보조 색인을 별도로 파티셔닝한다.&lt;/li&gt;
&lt;li&gt;찾고자 하는 용어에 따라 색인의 파티션이 결정되므로 이런 식의 색인을 용어 기준으로 파티셔닝됐다고 한다.&lt;/li&gt;
&lt;li&gt;색인을 파티셔닝할 때 용어 자체를 쓸 수도 있고 용어의 해시값을 사용할 수도 있다. 용어 자체로 파티셔닝하면 범위 스캔에 유용한 반면 용어의 해시값을 사용하면 부하가 좀 더 고르게 분산된다.&lt;/li&gt;
&lt;li&gt;장점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;읽기가 효율적이다.&lt;/li&gt;
&lt;li&gt;클라이언트는 모든 파티션에 스캐터/개더를 실행할 필요 없이 원하는 용어를 포함하는 파티션으로만 요청을 보내면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;쓰기가 느리고 복잡하다.&lt;/li&gt;
&lt;li&gt;단일 문서를 쓸 때 해당 색인의 여러 파티션에 영향을 줄 수 있다.(ex. 문서에 있는 모든 용어가 다른 노드에 있는 다른 파티션에 속하는 경우)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;파티션 재균형화&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시간이 지나면 데이터베이스에 변화가 생긴다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;질의 처리량이 증가해서 늘어난 부하를 처리하기 위해 CPU를 더 추가해야 하는 경우&lt;/li&gt;
&lt;li&gt;데이터셋 크기가 증가해서 데이터셋 저장에 사용할 디스크와 램을 추가해야 하는 경우&lt;/li&gt;
&lt;li&gt;장비에 장애가 발생해서 그 장비가 담당하던 역할을 다른 장비가 넘겨받아야 하는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;재균형화(rebalancing)&lt;/b&gt; : 클러스터에서 한 노드가 담당하던 부하를 다른 노드로 옮기는 과정&lt;/li&gt;
&lt;li&gt;요구사항
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;재균형화 후, 부하(데이터 저장소, 읽기 쓰기 요청)가 클러스터 내에 있는 노드들 사이에 균등하게 분배돼야 한다.&lt;/li&gt;
&lt;li&gt;재균형화 도중에도 데이터베이스는 읽기 쓰기 요청을 받아들여야 한다.&lt;/li&gt;
&lt;li&gt;재균형화가 빨리 실행되고 네트워크와 디스크 I/O 부하를 최소화할 수 있도록 노드들 사이에 데이터가 필요 이상으로 옮겨져서는 안 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;재균형화 전략&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해시값에 모드 N 연산을 실행
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;사용하면 안되는 방법&lt;/li&gt;
&lt;li&gt;노드 개수 N이 바뀌면 대부분의 키가 노드 사이에 옮겨져야 하는데, 키가 자주 이동하면 재균형화 비용이 지나치게 커진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;파티션 개수 고정
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;파티션을 노드 대수보다 많이 만들고 각 노드에 여러 파티션을 할당(10대의 노드에 1000개의 파티션을 쪼개서 100개씩 할당)&lt;/li&gt;
&lt;li&gt;클러스터에 노드가 추가되면 새 노드는 파티션이 다시 균일하게 분배될 때까지 기존 노드에서 파티션을 몇 개 뺏어올 수 있다.&lt;/li&gt;
&lt;li&gt;파티션은 노드 사이에서 통째로 이동하기만 한다. 파티션의 개수나 할당된 키는 변경되지 않는다.&lt;/li&gt;
&lt;li&gt;보통 데이터베이스가 처음 구축될 때 파티션 개수가 고정되고 이후에 변하지 않는다.&lt;/li&gt;
&lt;li&gt;전체 데이터셋의 크기 변동이 심한 경우 적절한 파티션 개수를 정하기 어렵고, 파티션이 너무 커지면 재균형화 및 노드 장애 복구 비용이 커진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;동적 파티셔닝
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;파티션의 크기가 설정된 값을 넘어서면 파티션을 두 개로 쪼개 각각에 원래 파티션의 절반 정도의 데이터가 포함되게 한다.&lt;/li&gt;
&lt;li&gt;데이터가 많이 삭제되어 파티션 크기가 임계값 아래로 떨어지면 인접한 파티션과 합쳐질 수 있다.&lt;/li&gt;
&lt;li&gt;큰 파티션이 쪼개진 후 부하의 균형을 맞추기 위해 분할된 파티션 중 하나가 다른 노드로 이동될 수 있다.&lt;/li&gt;
&lt;li&gt;장점 : 파티션 개수가 전체 데이터 용량에 맞춰 조정된다.&lt;/li&gt;
&lt;li&gt;단점 : 데이터셋이 작을 때는 모든 쓰기 요청이 하나의 노드에서 실행되고 다른 노드들은 유휴 상태에 머물게 될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;노드 비례 파티셔닝
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파티션 개수가 노드 대수에 비례하게 하는 것으로, 노드당 할당되는 파티션 개수를 고정한다.&lt;/li&gt;
&lt;li&gt;노드 대수가 변함 없는 동안은 개별 파티션 크기가 데이터셋 크기에 비례해서 증가하지만 노드 대수를 늘리면 파티션 크기를 다시 작아진다.&lt;/li&gt;
&lt;li&gt;데이터 용량이 클수록 데이터를 저장할 노드도 많이 필요하므로 이 방법을 통해 개별 파티션 크기도 안정적으로 유지할 수 있다.&lt;/li&gt;
&lt;li&gt;새 노드가 클러스터에 추가되면 고정된 개수의 파티션을 무작위로 선택해 분할하고 각 분할된 파티션의 절반은 그대로 두고 다른 절반은 새 노드에 할당한다.&lt;/li&gt;
&lt;li&gt;여러 파티션에 대해 평균적으로 보면 새 노드는 기존 노드들이 담당하던 부하에서 균등한 몫을 할당받게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;운영: 자동 재균형화와 수동 재균형화&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;완전 자동 재균형화: 관리자의 개입이 전혀 없이 시스템이 자동으로 언제 파티션을 노드 사이에 이동할지 결정&lt;/li&gt;
&lt;li&gt;완전 수동 재균형화: 관리자가 명시적으로 파티션을 노드에 할당하도록 설정하고 관리자가 재설정할 때만 파티션 할당이 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;요청 라우팅&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클라이언트가 아무 노드에나 접속하게 한다. 만약 해당 노드에 마침 요청을 적용할 파티션이 있다면 거기서 요청을 직접 처리할 수 있다. 그렇지 않으면 요청을 올바른 노드로 전달해서 응답을 받고 클라이언트에게 응답을 전달한다.&lt;/li&gt;
&lt;li&gt;클라이언트의 모든 요청을 라우팅 계층으로 먼저 보낸다. 라우팅 계층에서는 각 요청을 처리할 노드를 알아내고 그에 따라 해당 노드로 요청을 전달한다. 라우팅 계층 자체에서는 아무 요청도 처리하지 않는다. 파티션 인지 로드 밸런서로 동작할 뿐이다.&lt;/li&gt;
&lt;li&gt;클라이언트가 파티셔닝 방법과 파티션이 어떤 노드에 할당됐는지를 알고 있게 한다. 이 경우 클라이언트는 중개자 없이 올바른 노드로 직접 접속할 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>STUDY/데이터 중심 애플리케이션 설계</category>
      <author>level_?</author>
      <guid isPermaLink="true">https://it-study.tistory.com/127</guid>
      <comments>https://it-study.tistory.com/127#entry127comment</comments>
      <pubDate>Wed, 30 Jul 2025 22:29:19 +0900</pubDate>
    </item>
    <item>
      <title>4장. 부호화와 발전</title>
      <link>https://it-study.tistory.com/126</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;주요 내용&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;데이터 부호화를 위한 다양한 형식&lt;/li&gt;
&lt;li&gt;스키마 변경 방법과 예전 버전과 새로운 버전의 데이터와 코드가 공존하는 시스템 지원 방식&lt;/li&gt;
&lt;li&gt;REST, RPC, 액터(actor)와 메시지 전달 시스템(메시지 큐)에서 다양한 데이터 부화화 형식이 데이터 저장과 통신에 어떻게 사용되는지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;양방향 호환성&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하위 호환성 : 새로운 코드는 에전 코드가 기록한 데이터를 읽을 수 있어야 한다.&lt;/li&gt;
&lt;li&gt;상위 호환성 : 예전 코드는 새로운 코드가 기록한 데이터를 읽을 수 있어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;데이터 부호화 형식&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로그램은 보통 (최소한) 두 가지 형태로 표현된 데이터를 사용해 동작한다.&lt;/li&gt;
&lt;li&gt;메모리에 객체, 구초제, 목록, 배열, 해시 테이블, 트리 등으로 데이터가 유지되며, 이런 데이터 구조는 CPU에서 효율적으로 접근하고 조작할 수 있게 최적화된다.&lt;/li&gt;
&lt;li&gt;데이터를 파일에 쓰거나 네트워크를 통해 전송하려면 스스로를 포함한 일련의 바이트열의 형태로 부호화해야 한다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;부호화 : 인메모리 표현 &amp;rarr; 바이트열 전환, &lt;b&gt;직렬화나 마샬링&lt;/b&gt;이라고도 함.&lt;/li&gt;
&lt;li&gt;복호화 : 바이트열 &amp;rarr; 읽어올 수 있도록 변환, &lt;b&gt;파싱, 역직렬화, 언마샬링&lt;/b&gt;이라고도 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언어별 형식&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;많은 프로그래밍 언어는 인메모리 객체를 바이트열로 부호화하는 기능을 내장한다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;자바 : java.io.Serializable&lt;/li&gt;
&lt;li&gt;루비 : Marshal&lt;/li&gt;
&lt;li&gt;파이썬 pickle&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;최소한의 추가 코드로 인메모리 객체를 저장하고 복원할 수 있어 편리하지만 심각한 문제점이 있다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;부호화는 보통 특정 프로그래밍 언어와 묶여 있어 다른 언어에서 데이터를 읽기 매우 어렵다.&lt;/li&gt;
&lt;li&gt;동일한 객체 유형의 데이터를 복원하려면 복호화 과정이 임의의 클래스를 인스턴스화할 수 있어야 하는데, 보안 문제의 원인이 될 수 있다.&lt;/li&gt;
&lt;li&gt;데이터를 빠르고 쉽게 부호화하기 위해 &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;상위, 하위 호환성의 문제가 &lt;/span&gt;발생한다.&lt;/li&gt;
&lt;li&gt;효율성 문제(ex. 자바의 내장 직렬화는 성능이 좋지 않고 비대해지는 부호화로 유명)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON과 XML, 이진 변형&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JSON, XML, CSV는 텍스트 형식이라 사람이 읽을 수 있고, 특히 데이터 교환 형식(한 조직에서 다른 조직으로 데이터를 전송)으로 사용하기 좋다.&lt;/li&gt;
&lt;li&gt;단점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;수(number)의 부호화 : XML과 CSV는 수와 숫자로 구성된 문자열을 구분할 수 없다. JSON은 구분은 가능하지만 정수와 부동소수점 수를 구별하지 않고 정밀도를 지정하지 않는다. 이런 점은 큰 수를 다룰 때 문제가 된다.&lt;/li&gt;
&lt;li&gt;JSON과 XML은 유니코드 문자열(사람이 읽을 수 있는 텍스트)은 지원하지만, 이진 문자열(문자 부호화가 없는 바이트열)을 지원하지 않는다.&lt;/li&gt;
&lt;li&gt;JSON과 XML은 모두 스키마를 지원하지만, 익히고 구현하기 상당히 난해하다.&lt;/li&gt;
&lt;li&gt;CSV는 스키마가 없어 각 로우와 컬럼의 의미를 정의해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이진 부호화&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JSON과 XML은 이진 형식과 비교했을 때 많은 공간을 사용&amp;nbsp;&amp;rarr; JSON과 XML용으로 사용 가능한 다양한 이진 부호화 개발
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;데이터타입 셋을 확장하지만(ex. 정수와 부동소수점 수의 구분이나 이진 문자열 지원 추가) JSON/XML 데이터 모델은 변경하지 않고 유지&lt;/li&gt;
&lt;li&gt;특히 스키마를 지정하지 않아 부호화된 데이터 안에 모든 객체의 필드 이름을 포함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아파치 스리프트(Apache Thrift)와 프로토콜 버퍼(Protocol Buffers)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이진 부호화 라이브러리&lt;/li&gt;
&lt;li&gt;부호화할 데이터를 위한 스키마 필요&lt;/li&gt;
&lt;li&gt;스리프트
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;바이너리프로토콜 방식 : 필드 이름이 없는 대신 &lt;b&gt;필드 태그&lt;/b&gt; 존재&lt;/li&gt;
&lt;li&gt;컴팩트프로토콜 방식 : 동일한 정보를 34바이트로 줄여 부호화, 필드 타입과 태그 숫자를 단일 바이트로 줄이고 가변 길이 정수를 사용해서 부호화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;프로토콜 버퍼
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;스리프트의 컴팩트프로토콜과 매우 비슷한데, 33바이트로 부호화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필드 태그와 스키마 발전&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스키마 발전(schema evolution) : 스키마가 시간이 지남에 따라 변하는 것&lt;/li&gt;
&lt;li&gt;필드 태그
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;부호화된 데이터를 해석하기 위해 매우 중요&lt;/li&gt;
&lt;li&gt;기존의 모든 부호화된 데이터를 인식 불가능하게 만들 수 있어 변경이 불가능&lt;/li&gt;
&lt;li&gt;상위 호환성 : 새로운 태그 번호를 추가&amp;nbsp;&amp;rarr; 새로운 필드 추가 가능, 예전 코드는 해당 필드 무시&lt;/li&gt;
&lt;li&gt;하위 호환성 : 예전 코드는 새로운 필드를 기록하지 않기 때문에 추가되는 필드의 경우 optional로 하거나 기본값 설정&lt;/li&gt;
&lt;li&gt;필드 삭제의 경우 optional 필드만 가능하고, 같은 태그 번호는 다시 사용할 수 없음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터타입과 스키마 발전&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로토콜 버퍼 : repeated 표시자,(단일) optional 필드를 (다중) repeated 필드로 변경 가능&lt;/li&gt;
&lt;li&gt;스리프트 : 전용 목록 데이터타입, 목록 엘리먼트의 데이터타입을 매개변수로 받아서 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아파치 아브로(avro)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이진 부호화 형식&lt;/li&gt;
&lt;li&gt;부호화할 데이터 구조를 지정하기 위해 스키마 사용, 두 개의 스키마 언어 존재(아브로 IDL, JSON 기반 언어)&lt;/li&gt;
&lt;li&gt;스키마에 태그 번호가 없음&lt;/li&gt;
&lt;li&gt;이진 부호화 길이는 32바이트로 모든 부호화 중 길이가 가장 짧음&lt;/li&gt;
&lt;li&gt;필드나 데이터타입을 식별하기 위한 정보가 없고, 단순히 연결된 값으로 구성&lt;/li&gt;
&lt;li&gt;이진 데이터를 파싱하기 위해 스키마의 필드 순서와 데이터 타입을 미리 파악해야 함&lt;/li&gt;
&lt;li&gt;읽기와 쓰기 스키마가 일치하지 않는 경우 복호화가 정확하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓰기 스키마와 읽기 스키마&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쓰기 스키마 : 파일이나 데이터베이스 쓰기 또는 네트워크 전송 등의 목적으로 어떤 데이터를 아브로로 부호화할 때 사용하는 스키마&lt;/li&gt;
&lt;li&gt;읽기 스키마 : 파일이나 데이터베이스 또는 네트워크로 수신 등으로 읽은 어떤 데이터를 복호화할 때 사용하는 스키마&lt;/li&gt;
&lt;li&gt;아브로의 핵심 아이디어는 쓰기 스키마와 읽기 스키마가 동일하지 않아도 되며 단지 호환 가능하면 되는 것이다.&lt;/li&gt;
&lt;li&gt;데이터를 복호화할 때 쓰기 스키마와 읽기 스키마를 함께 살펴본 다음 쓰기 스키마에서 읽기 스키마로 데이터를 변환해 차이를 해소
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;필드 순서 상관 없음(이름으로 매핑)&lt;/li&gt;
&lt;li&gt;읽기 스키마에 없고 쓰기 스키마에 있는 경우 &amp;rarr; 필드 무시&lt;/li&gt;
&lt;li&gt;쓰기 스키마에 없고 읽기 스키마에 있는 경우&amp;nbsp;&amp;rarr; 읽기 스키마의 기본값 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스키마 발전 규칙&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상위 호환성 : 새로운 버전의 쓰기 스키마와 예전 버전의 읽기 스키마를 가질 수 있음&lt;/li&gt;
&lt;li&gt;하위 호환성 : 새로운 버전의 읽기 스키마와 예전 버전의 쓰기 스키마를 가질 수 있음&lt;/li&gt;
&lt;li&gt;호화성 유지를 위해 기본값이 있는 필드만 추가하거나 삭제 가능
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본값이 없는 필드 추가 &amp;rarr; 하위 호환성 문제, 새로운 읽기는 예전 버전 데이터 읽기 불가&lt;/li&gt;
&lt;li&gt;기본값이 없는 필드 삭제&amp;nbsp;&amp;rarr; 상위 호환성 문제, 예전 읽기는 새로운 데이터 읽기 불가&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;아브로는 널을 허용하기 위해 유니온 타입 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러면 쓰기 스키마는 무엇인가?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;많은 레코드가 있는 대용량 파일(ex 하둡) : 파일의 시작 부분에 한 번만 쓰기 스키마를 포함&lt;/li&gt;
&lt;li&gt;개별적으로 기록된 레코드를 가진 데이터베이스 : 모든 부호화된 레코드의 시작 부분에 버전 번호를 포함하고 데이터베이스에는 스키마 버전 목록을 유지.&lt;/li&gt;
&lt;li&gt;네트워크 연결을 통해 레코드 보내기 : 연결 설정에서 스키마 버전 합의&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동적 생성 스키마&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로토콜 버퍼, 스리프트와 달리 스키마에 태그 번호를 포함하지 않음&amp;nbsp;&amp;rarr; 동적 생성 스키마에 친숙&lt;/li&gt;
&lt;li&gt;데이터베이스 스키마가 변경되는 경우 새로운 아브로 스키마를 생성해서 데이터를 내보냄.&lt;/li&gt;
&lt;li&gt;필드가 이름으로 식별되기 때문에 갱신된 쓰기 스키마는 읽기 스키마와 매치 가능&lt;/li&gt;
&lt;li&gt;스리프트나 프로토콜 버퍼의 경우 컬럼 이름과 필드 태그의 매핑을 수동으로 갱신해야 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 생성과 동적 타입 언어&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스리프트와 프로토콜 버터는 코드 생성에 의존, 스키마를 정의한 후 선택한 프로그래밍 언어로 스키마를 구현한 코드를 생성&lt;/li&gt;
&lt;li&gt;이브로는 코드 생성 없이 사용 가능(코드 생성을 선택적으로 제공)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스키마의 장점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;XML 스키마, JSON 스키마보다 간단하며, 더 자세한 유효성 검사 규칙을 지원&lt;/li&gt;
&lt;li&gt;구현과 사용이 더 간단해 광범위한 프로그래밍 언어 지원&lt;/li&gt;
&lt;li&gt;즉, 스키마 발전은 schemaless 또는 읽기 스키마 JSON 데이터베이스가 제동하는 것과 동일한 종유의 유연성을 제공하며 데이터나 도구 지원도 잘 보장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;데이터플로 모드&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로세스 간 데이터를 전달하는 방법
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;데이터베이스를 통해&lt;/li&gt;
&lt;li&gt;서비스 호출을 통해&lt;/li&gt;
&lt;li&gt;비동기 메시지 전달을 통해&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스를 통한 데이터플로&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터베이스에 기록하는 프로세스는 데이터를 부호화하고 데이터베이스에서 읽는 프로세스는 데이터를 복호화한다.&lt;/li&gt;
&lt;li&gt;상위 호환성과 하위 호환성 모두 필요&lt;/li&gt;
&lt;li&gt;새로운 필드 추가됐는데, 예전 버전에 의해 갱신+재부화되는 경우 필드가 유실될 수 있음&lt;/li&gt;
&lt;li&gt;데이터베이스 데이터를 새로운 스키마로 다시 기록하는 직업은 값비싸기 때문에, 보통 기존 데이터를 다시 기록하지 않고 널을 기본값으로 갖는 새로운 칼럼을 추가하는 간단한 스키마 변경을 허용&lt;/li&gt;
&lt;li&gt;예전 로우를 읽는 경우 디스크 상의 부호화된 데이터에서 누락된 임의 컬럼은 널로 채움&lt;/li&gt;
&lt;li&gt;데이터 덤프는 보통 최신 스키마를 사용해 부호화. 한 번에 기록하고 이후에는 변하지 않으므로 아브로 객체 컨테이너 파일과 같은 형식이 적합&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스를 통한 데이터 플로: REST와 RPC&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버와 클라이언트가 사용하는 데이터 부호화는 서비스 API의 버전 간 호환이 가능해야 한다.&lt;/li&gt;
&lt;li&gt;REST와 RPC는 하나의 프로세스가 네트워크를 통해 다른 프로세스로 요청을 전송하고 가능한 빠른 응답을 기대하는 방식이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 서비스&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스와 통신하기 위한 기본 프로토콜로 HTTP를 사용할 때&lt;/li&gt;
&lt;li&gt;대중적인 두 가지 방식 REST와 SOAP가 존재
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;REST : 프로토콜이 아닌 HTTP의 원칙을 토대로 한 설계 철학&lt;/li&gt;
&lt;li&gt;SOAP : 네트워크 API 요청을 위한 XML 기반 프로토콜로 HTTP상에서 가장 일반적으로 사용되지만 HTTP와 독립적이며 대부분의 HTTP 기능을 사용하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RPC&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RPC 모델은 원격 네트워크 서비스 요청을 같은 프로세스 안에서 특정 프로그래밍 언어의 함수나 메서드를 호출하는 것과 동일하게 사용 가능하게 해줌&lt;/li&gt;
&lt;li&gt;하지만 예측이 어렵고, 네트워크 문제로 요청과 응답이 유실되거나 원격 장비가 느려지거나 요청에 응답하지 않는 문제가 발생할 수 있음
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;타임아웃으로 결과 없이 반환&lt;/li&gt;
&lt;li&gt;실패한 네티워크 요청을 다시 시도할 때 요청이 실제로는 처리되고 응답만 유실되어 재시도 작업이 여러 번 수행될 수 있음&lt;/li&gt;
&lt;li&gt;네트워크가 혼잡하거나 원격 서비스에 과부하가 걸리는 경우 처리 시간이 오래 걸림&lt;/li&gt;
&lt;li&gt;모든 매개변수를 네트워크 전송이 가능하도록 바이트열로 부호화 해야함.&lt;/li&gt;
&lt;li&gt;하나의 언어에서 다른 언어로 데이터타입을 변환해야 함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;RPC의 현재 방향은 같은 데이터센터 내의 같은 조직이 소유한 서비스 간 요청에 초점을 두는 것이다.&lt;/li&gt;
&lt;li&gt;발전성을 위해서는 RPC 클라이언트와 서버를 독립적으로 변경하고 배포할 수 있어야 함.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 전달 데이터플로&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비동기 메시지 전달 시스템
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;클라이언트 요청(메시지)를 낮은 지연 시간으로 다른 프로세스에 전달&lt;/li&gt;
&lt;li&gt;메시지를 직접 네트워크 연결로 전송하지 않고 임시로 메시지를 저장하는 메시지 브로커(message broker) 또는 메시지 큐(message queue)나 메시지 지향 미들웨어(message-oriented middleware)라는 중간 단계를 거쳐 전송&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;장점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;수신자가 사용 불가능하거나 과부하 상태여도 메시지 브로커가 버퍼처럼 동작할 수 있기 때문에 시스템 안정성이 향상됨&lt;/li&gt;
&lt;li&gt;죽었던 프로세스에 메시지를 다시 전달할 수 있어, 메시지 유실 방지 가능&lt;/li&gt;
&lt;li&gt;송신자가 수신자의 IP 주소나 포트를 몰라도 됨.&lt;/li&gt;
&lt;li&gt;하나의 메시지를 여러 수신자로 전송 가능&lt;/li&gt;
&lt;li&gt;논리적으로 송신자는 수신자와 분리됨.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;메시지 전달 통신은 단방향&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 액터 프레임워크&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;액터 모델
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;단일 프로세스 안에서 동시성을 위한 프로그래밍 모델이다.&lt;/li&gt;
&lt;li&gt;스레드(경쟁 조건, 잠금, 교착 상태와 관련된 문제들)를 직접 처리하는 대신 로직이 액터에 캡슐화&lt;/li&gt;
&lt;li&gt;보통 각 액터는 하나의 클라이언트나 엔티티를 나타냄&lt;/li&gt;
&lt;li&gt;다른 액터와 공유하지 않는 로컬 상태를 가질 수 있고 비동기 메시지의 송수신으로 다른 액터와 통신&lt;/li&gt;
&lt;li&gt;메시지 전달을 보장하지 않아 에러 상황에서 유실 가능성 존재&lt;/li&gt;
&lt;li&gt;각 액터 프로세스는 한 번에 하나의 메시지만 처리하기 때문에 스레드에 대해 걱정할 필요가 없고, 프레임워크와 독립적으로 실행 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;분산 액터 프레임워크에서 액터 모델은 여러 노드 간의 애플리케이션 확장에 사용&lt;/li&gt;
&lt;li&gt;송신자와 수신자의 노드 위치에 관계없이 동일한 메시지 전달 구조를 사용&lt;/li&gt;
&lt;li&gt;서로 다른 노드에 있는 경우 바이트열로 부호화되고 네트워크를 통해 전송됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>STUDY/데이터 중심 애플리케이션 설계</category>
      <author>level_?</author>
      <guid isPermaLink="true">https://it-study.tistory.com/126</guid>
      <comments>https://it-study.tistory.com/126#entry126comment</comments>
      <pubDate>Wed, 25 Jun 2025 22:35:23 +0900</pubDate>
    </item>
    <item>
      <title>3장. 저장소와 검색</title>
      <link>https://it-study.tistory.com/125</link>
      <description>&lt;p data-ke-size=&quot;size18&quot;&gt;데이터베이스의 기본적인 기능&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 데이터를 받으면 데이터를 저장&lt;/li&gt;
&lt;li&gt;데이터를 요청하면 데이터를 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 개발자는 사용 가능한 여러 저장소 엔진 중에 애플리케이션에 적합한 엔진을 선택하는 작업이 필요하다.&amp;nbsp;특정 작업부하(workload) 유형에서 좋은 성능을 내게끔 저장소 엔진을 조정하려면 저장소 엔진의 내부 수행 작업에 대해 이해할 필요가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;관계형 데이터베이스와 NoSQL 에서 사용되는 저장소 엔진&lt;/li&gt;
&lt;li&gt;로그 구조(log-structured) 계열 저장소 엔진&lt;/li&gt;
&lt;li&gt;페이지 지향(page-oriented) 계열 저장소 엔진&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;데이터베이스를 강력하세 만드는 데이터 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;가장 간단한 데이터베이스&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;키-값 저장소를 함수로 구현&lt;/li&gt;
&lt;li&gt;매 라인마다 쉼표로 구분된 키-값 쌍을 포함한 텍스트 파일 형식&lt;/li&gt;
&lt;li&gt;db_set을 호출할 때마다 파일의 끝에 추가하므로 키를 여러 번 갱신해도 값의 예전 버전을 덮어 쓰지 않는다.&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1750247159961&quot; class=&quot;shell&quot; data-ke-language=&quot;shell&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#!/bin/bash

db_set() {
	echo &quot;$1,$2&quot; &amp;gt;&amp;gt; database
}

db_get() {
	grep &quot;^$1,&quot; database | sed -e &quot;s/^$1,//&quot; | tail -n 1
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일 추가 작업은 매우 효율적이기 때문에 db_set 함수는 매우 간단한 작업의 경우에는 좋은 성능을 보여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;db_set과 마찬가지로 많은 데이터베이스는 내부적으로 추가 전용(append-only) 데이터 파일인 &lt;b&gt;로그(log)&lt;/b&gt;를 사용한다. 여기서 로그는 연속된 추가 전용 레코드를 의미하며, 사람이 읽을 수 있는 형식일 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 d&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;b_get 함수는 데이터베이스에 많은 레코드가 있으면 성능이 매우 좋지 않다.(검색 비용&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: left;&quot;&gt;O(n))&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스에서는 특정 키의 값을 효율적으로 찾기 위해서는 다른 데이터 구조가 필요한데, 이를 &lt;b&gt;색인(index)&lt;/b&gt;이라 한다. 색인의 일반적인 개념은 어떤 부가적인 메타데이터를 유지하는 것이다. 이 메타데이터는 이정표 역할을 해서 원하는 데이터의 위치를 찾는데 도움을 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;색인은 기본 데이터(primary data)에서 파생된 추가적인 구조이다. 많은 데이터베이스는 색인의 추가와 삭제를 허용하는데, 이는 질의 성능에만 영향을 준다. 추가적인 구조의 유지보수는 특히 쓰기 과정에서 오버헤드가 발생한다. 데이터를 쓸 때마다 매번 색인을 갱신해야 하기 때문에, 어떤 종류의 색인이라도 쓰기 속도를 느리게 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;해시 색인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키-값 저장소는 대부분의 프로그래밍 언어에서 볼 수 있는 사전 타입(dictionary type)과 매우 유사하며, 보통 해시 맵(hash map, 해시 테이블(hash table))으로 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 파일에 추가하는 방식으로 데이터 저장소를 구현하는 경우, 가장 간단하게 가능한 색인 전략은 키를 데이터 파일의 바이트 오프셋에 매핑해 인메모리 해시 맵을 유지하는 것이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1119&quot; data-origin-height=&quot;557&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9SPY8/btsOHZKP4Im/FbdOg5EtVcP7AA52FQuAk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9SPY8/btsOHZKP4Im/FbdOg5EtVcP7AA52FQuAk0/img.png&quot; data-alt=&quot;In-memory hash map&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9SPY8/btsOHZKP4Im/FbdOg5EtVcP7AA52FQuAk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9SPY8%2FbtsOHZKP4Im%2FFbdOg5EtVcP7AA52FQuAk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;299&quot; data-origin-width=&quot;1119&quot; data-origin-height=&quot;557&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;In-memory hash map&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바이트 오프셋은 값을 바로 찾을 수 있는 위치이며, 파일에 새로운 키-값 쌍을 추가할 때마다 데이터의 오프셋을 반영하기 위해 해시 맵도 갱신해야 한다. 값을 조회하려면 해시 맵을 사용해 데이터 파일에서 오프셋을 찾아 해당 위치를 구하고 값을 읽으면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 많이 사용하는 접근 방식으로, 비트캐스크(Bitcask)라는 저장소 엔진이 근본적으로 사용하는 방식이다. 비트캐스크는 해시 맵을 전부 메모리에 유지하기 때문에 사용 가능한 RAM에 모든 키가 저장된다는 조건을 전제로 고성능으로 읽기, 쓰기를 보장한다. 값은 한 번의 디스크 탐색으로 디스크에서 적재할 수 있기 때문에 사용 가능한 메모리보다 더 많은 공간을 사용할 수 있다. 이러한 저장소 엔진은 각 키의 값이 자주 갱신되는 상황에 매우 적합하다.(키=동영상URL, 값=재생횟수)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파일에 계속 추가만 한다면 결국 디스크 공간이 부족해진다. 이 상황은 특정 크기의 세그먼트(segment)로 로그를 나누는 방식으로 해결할 수 있다. 특정 크기에 도달하면 세그먼트 파일을 닫고 새로운 세그먼트 파일에 이후 쓰기를 수행한다. 이러면 세그먼트 파일들에서 중복된 키를 버리고 각 키의 최신 갱신 값만 유지하는 컴팩션(compaction)을 수행할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1079&quot; data-origin-height=&quot;387&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cuCa1y/btsOHEGXXZk/AvhuoJpbtLbkiZpuMNebG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cuCa1y/btsOHEGXXZk/AvhuoJpbtLbkiZpuMNebG1/img.png&quot; data-alt=&quot;컴팩션(compaction)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cuCa1y/btsOHEGXXZk/AvhuoJpbtLbkiZpuMNebG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcuCa1y%2FbtsOHEGXXZk%2FAvhuoJpbtLbkiZpuMNebG1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;215&quot; data-origin-width=&quot;1079&quot; data-origin-height=&quot;387&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;컴팩션(compaction)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴팩션은 보통 세그먼트를 더 작게 만들기 때문에, 컴팩션을 수행할 때 동시에 여러 세그먼트들을 병합할 수 있다. 고정된 세그먼트의 병합과 컴팩션은 백그라운드 스레드에서 수행되고, 그동안 이전 세그먼트 파일을 사용해 읽기와 쓰기 요청을 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1105&quot; data-origin-height=&quot;569&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/M1z0A/btsOF4NHrkG/3gVjy6cGmDCFxqmyNGL2zk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/M1z0A/btsOF4NHrkG/3gVjy6cGmDCFxqmyNGL2zk/img.png&quot; data-alt=&quot;컴팩션과 세그먼트 병합을 동시에 수행&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/M1z0A/btsOF4NHrkG/3gVjy6cGmDCFxqmyNGL2zk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FM1z0A%2FbtsOF4NHrkG%2F3gVjy6cGmDCFxqmyNGL2zk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;309&quot; data-origin-width=&quot;1105&quot; data-origin-height=&quot;569&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;컴팩션과 세그먼트 병합을 동시에 수행&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러면 각 세그먼트는 키를 파일 오프셋에 매핑한 자체 인메모리 해시 테이블을 갖게 된다. 키의 값을 찾으려면 최신 세그먼트 해시 맵을 먼저 확인하면 되고, 키가 없는 경우 두 번째 세그먼트 등을 확인하면 된다. 병합 과정을 통해 세그먼트 수를 적게 유지하기 때문에 조회할 때 많은 해시 맵을 확인할 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현할 때 고려해야 할 사항&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일 형식 : 바이트 단위 문자열 길잉를 부호화한 다음 바이너리 형식을 사용하는 편이 더 빠르고 간단하다.&lt;/li&gt;
&lt;li&gt;레코드 삭제 : 키와 관련된 값을 삭제하려면 데이터 파일에 특수한 삭제 레코드를 추가해야 한다.&lt;/li&gt;
&lt;li&gt;고장(Crash) 복구 : 데이터베이스가 재시작되면 전체 세그먼트 파일을 읽어 인메모리 해시 맵을 복원하는데, 파일이 크면 시간이 오래걸릴 수 있다.&lt;/li&gt;
&lt;li&gt;부분적으로 레코드 쓰기 : 데이터베이스는 로그에 레코드를 추가하는 도중에 죽을 수 있다.&lt;/li&gt;
&lt;li&gt;동시성 제어 : 로그 쓰기는 하나의 쓰기 스레드만 사용해 동시성 제어한다. 세그먼트는 추가 전용이거나 불변이므로 다중 스레드로 동시에 읽을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가 전용 설계의 좋은 점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추가와 세그먼트 병합은 순차적인 쓰기 작업이기 때문에 보통 무작위 쓰기보다 훨씬 빠르다.&lt;/li&gt;
&lt;li&gt;세그먼트 파일이 추가 전용이나 불변이면 동시성과 고장 복구는 훨씬 간단하다.&lt;/li&gt;
&lt;li&gt;오래된 세그먼트 병합은 시간이 지남에 따라 조각화되는 데이터 파일 문제를 피할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해시 테이블 색인의 제한 사항&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해시 테이블은 메모리에 저장해야 하므로 키가 너무 많으면 문제가 된다. 디스크에 해시 맵을 유지할 수는 있지만, 무작위 접근 I/O가 많이 필요하고 디스크가 가득 찼을 때 확장하는 비용이 비싸며 해시 충돌 해소를 위한 로직이 필요한다.&lt;/li&gt;
&lt;li&gt;해시 테이블은 해시 맵에서 모든 개별 키를 조회해야 하기 때문에 범위 질의(range query)에 효율적이지 않다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;SS테이블과 LSM 트리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세그먼트 파일 형식에서 일련의 키-값 쌍을 키로 정렬하면, &lt;b&gt;정렬된 문자열 테이블(Sorted String Table)&lt;/b&gt; 또는 &lt;b&gt;SS테이블&lt;/b&gt;이라 부르는 형식이 된다. 각 키는 병합된 세그먼트 파일 내에 한 번만 나타나야 한다.(컴팩션이 이를 보장함.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해시 색인을 가진 로그 세그먼트와 비교했을 때 장점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;세그먼트 병합은 파일이 사용 가능한 메모리보다 크더라도 간단하고 효율적이다. 이 접근법은 병합정렬(mergesort) 알고리즘에서 사용하는 방식과 유사하다.&lt;/li&gt;
&lt;li&gt;파일에서 특정 키를 찾기 위해 더는 메모리에 모든 키의 색인을 유지할 필요가 없다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;읽기 요청은 요청 범위 내에서 여러 키-값 쌍을 스캔해야 하기 때문에 해당 레코드들을 블록으로 그룹화하고 디스크에 쓰기 전에 압축한다. 그러면 희소 인메모리 색인의 각 항목은 압축된 블록의 시작을 가리키게 된다. 디스크 공간을 절약하고, I/O 대역폭 사용도 줄일 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;SS테이블 생성과 유지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디스크 상에 정렬된 구조를 유지하는 일은 가능하지만 메모리에 유지하는 편이 훨씬 쉽다. red-black tree나 AVL 트리와 같은 데이터 구조를 이용하면 임의 순서로 키를 삽입하고 정렬된 순서로 해당 키를 다시 읽을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쓰기가 들어오면 인메모리 균형 트리(balanced tree) 데이터 구조에 추가한다. 이 인메모리 트리를 &lt;b&gt;멤테이블(memtable)&lt;/b&gt;이라고도 한다.&lt;/li&gt;
&lt;li&gt;멤테이블이 보통 수 메가바이트 정도의 임계값보다 커지면 SS테이블 파일로 디스크에 기록한다. 새로운 SS테이블 파일은 데이터베이스의 가장 최신 세그먼트가 된다. SS테이블을 디스크에 기록하는 동안 쓰기는 새로운 멤테이블 인스턴스에 기록한다.&lt;/li&gt;
&lt;li&gt;읽기 요청을 제공하려면 멤테이블에서 키를 찾고, 그 다음 디스크 상의 최신 세그먼트 순서로 찾는다.&lt;/li&gt;
&lt;li&gt;세그먼트 파일의 병합과 컴팩션 과정을 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 데이터베이스가 고장나면 아직 디스크로 기록되지 않은 최신 쓰기가 손실되므로, 이런 문제를 피하기 위해서는 매번 쓰기를 즉시 추가할 수 있게 분리된 로그를 디스크 상에 유지해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;SS테이블에서 LSM 트리 만들기&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;위에 기술된 알고리즘은 LevelDB, RocksDB, Bigtable, Cassandra, HBase의 키-값 저장소 엔진 라이브러리에서 사용한다.&lt;/span&gt;&lt;/p&gt;
&lt;div id=&quot;block-da0f2938381d42e4accbcb1bb5f90176&quot; style=&quot;color: #333333; text-align: start;&quot;&gt;
&lt;p style=&quot;color: #555555;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;위의 색인 구조는&amp;nbsp;&lt;b&gt;Log-Structured Merge-Tree, LSM(로그 구조화 병합 트리)&lt;/b&gt;란 이름으로 발표되었다. 정렬된 파일 병합과 컴팩션 원리를 기반으로 하는 저장소 엔진을 LSM 저장소 엔진이라 부른다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;성능 최적화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 세부 사항이 저장소 엔진을 잘 동작하게 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LSM 트리 알고리즘의 경우 데이터베이스에 존재하지 않는 키를 찾는 경우 느릴 수 있다. 없는 키를 찾기 위해 멤테이블부터 가장 오래된 세그먼트 끝까지 찾기 때문이다. 이런 종류의 접근을 최적화하기 위해 저장소 엔진은 보통 &lt;b&gt;Bloom Filter(블룸 필터)&lt;/b&gt;를 제공한다. 블룸 필터는 키가 데이터베이스에 존재하지 않음을 알려주므로, 불필요한 디스크 읽기를 절약할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SS테이블을 압축하고 병합하는 순서와 시기를 결정하는 다양한 전력이 있다. 일반적으로 크기 계층 컴팩션(size-tiered compaction)과 레벨 컴팩션(leveled compaction)이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;크기 계층 컴팩션은 상대적으로 좀 더 새롭고 작은 SS테이블을 상대적으로 오래됐고 큰 SS테이블에 병합하고, 레벨 컴팩션은 키 범위를 더 작은 SS테이블로 나누고 오래된 데이터는 개별 &quot;레벨&quot;로 이동시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LSM 트리의 기본 개념은 백그라운드에서 연쇄적으로 SS테이블을 지속적으로 병합하는 것이다. 이 개념은 데이터셋이 메모리보다 훨씬 크더라도 효과적이고, 데이터가 정렬된 순서로 저장돼 있다면 범위 질의를 효율적으로 실행할 수 있다. 그리고 디스크 쓰기가 순차적이기 때문에 매우 높은 쓰기 처리량을 보장할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;B트리(B-tree)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1970년대에 등장한 B-tree는 전통적으로 4KB 크기(때로는 더 큰)의 고정 크기 &lt;b&gt;블록&lt;/b&gt;이나 &lt;b&gt;페이지&lt;/b&gt;로 나누고 한 번에 하나의 페이지에 읽기 또는 쓰기를 한다. 각 페이지는 주소나 위피를 이용해 식별할 수 있고, 하나의 페이지가 다른 페이지를 참조할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;색인에서 키를 찾으려면 루트(root)에서 시작한다. 페이지는 여러 키와 하위 페이지의 참조를 포함하며, 각 하위 페이지는 키가 계속 이어지는 범위를 담당하고 참조 사이의 키는 해당 범위 경계가 어디인지 나타낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로는 개별 키(리트 페이지, leaf page)를 포함하는 페이지에 도달하게 되고, 이 페이지는 각 키의 값을 포함하거나 값을 찾을 수 있는 페이지의 참조를 포함한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B 트리의 한 페이지에서 하위 페이지를 참조하는 수를 &lt;b&gt;분기 계수(branching factor)&lt;/b&gt;라고 부른다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B 트리에 존재하는 키의 값을 갱신할 때는 키를 포함하고 있는 리프 페이지를 검색하고 페이지의 값을 바꾼 다음 페이지를 디스크에 다시 기록한다. 새로운 키를 추가하는 경우, 키를 포함하는 범위의 페이지를 찾아 해당 페이지에 키와 값을 추가하고, 만약 여유 공간이 없다면 페이지를 둘로 나누고 상위 페이지가 새로운 키 범위의 하위 부분들을 알 수 있게 갱신한다. 이러한 알고리즘은&amp;nbsp;트리가 계속 균현을 유지하는 것을 보장한다. n개의 키를 가진 B트리는 깊이가 항상 O(log n)이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;신뢰할 수 있는 B트리 만들기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B 트리의 기존적인 쓰기 동작은 새로운 데이터를 디스크 상의 페이지에 덮어쓰는 것이다. (페이지를 덮어쓰더라도 페이지를 가리키는 모든 참조는 온전하게 남는다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;삽입 때문에 페이지가 너무 많아져 페이지를 나눠야 하는 경우 분할된 두 페이지를 기록하고 두 하위 페이지의 참조를 갱신하게끔 상위 페이지를 덮어써야 한다. 이 때, 일부 페이지만 기록하고 데이터베이스가 고장 난다면 색인이 훼손되기 때문에 &lt;b&gt;고아 페이지(orphan page, 어떤 페이지와도 부모 관계가 없는 페이지)&lt;/b&gt;가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;고장 상황에서 스스로 복구할 수 있게 하기 위해 디스크 상에 &lt;b&gt;쓰기 전 로그(write-ahead log, WAL, 재실행 로그(redo log))&lt;/b&gt;라고 하는 데이터 구조를 추가해 B 트리를 구현한다. 이는 트리 페이지에 변경된 내용을 적용하기 전에 모든 B 트리의 변경 사항을 기록하는 추가 전용 파일로, 일관성 있는 상태로 B 트리를 복원하는데 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다중 스레드가 동시에 B 트리에 접근하는 경우 동시성 제어를 해야하는데, 보통 &lt;b&gt;래치(latch, 가벼운 잠금)&lt;/b&gt;로 트리의 데이터 구조를 보호한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;B 트리 최적화&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일부 데이터베이스는 쓰기 시 복사 방식(copy-on-write scheme)을 사용한다.(동시성 제어에도 유용)&lt;/li&gt;
&lt;li&gt;페이지 전체 키를 저장하는 게 아니라 키를 축약해 쓰면 공간을 절약할 수 있다.&lt;/li&gt;
&lt;li&gt;B 트리 구현에서 리프 페이지를 디스크 상에 연속된 순서로 나타나게끔 트리르 배치하려 시도한다.&lt;/li&gt;
&lt;li&gt;트리에 포인터를 추가한다.&lt;/li&gt;
&lt;li&gt;프랙탈 트리(fractal tree, B 트리 변형)은 디스크 찾기를 줄이기 위해 로그 구조화 개념을 일부 빌렸다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;B트리와 LSM 트리 비교&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.3488%;&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.0698%;&quot;&gt;B-Tree&lt;/td&gt;
&lt;td style=&quot;width: 45.4651%;&quot;&gt;LSM-Tree&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.3488%;&quot;&gt;&lt;b&gt;쓰기 처리 성능&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.0698%;&quot;&gt;중간 수준 (디스크 페이지 읽기 및 수정 후 다시 쓰기 필요)&lt;/td&gt;
&lt;td style=&quot;width: 45.4651%;&quot;&gt;매우 뛰어남 (메모리에 기록 후 배치 단위로 디스크에 순차적 쓰기)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.3488%;&quot;&gt;&lt;b&gt;읽기 성능 (포인트 조회)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.0698%;&quot;&gt;빠름 (트리 탐색으로 한 번에 위치 조회 가능)&lt;/td&gt;
&lt;td style=&quot;width: 45.4651%;&quot;&gt;보통 (여러 SSTable에서 검색 필요하므로 Bloom 필터 등 최적화 필요)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.3488%;&quot;&gt;&lt;b&gt;읽기 성능 (범위 조회)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.0698%;&quot;&gt;뛰어남 (정렬된 트리 구조를 따라 범위 순회)&lt;/td&gt;
&lt;td style=&quot;width: 45.4651%;&quot;&gt;뛰어남 (SSTable이 정렬되어 있으므로 범위 병합 가능)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.3488%;&quot;&gt;&lt;b&gt;디스크 접근 패턴&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.0698%;&quot;&gt;**임의 접근(Random Access)**이 많음&lt;/td&gt;
&lt;td style=&quot;width: 45.4651%;&quot;&gt;&lt;b&gt;순차 접근(Sequential Write)&lt;/b&gt; 위주, 디스크 효율성 높음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.3488%;&quot;&gt;&lt;b&gt;디스크 공간 사용&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.0698%;&quot;&gt;다소 비효율적 (중복된 값 존재, 페이지 분할 등으로 단편화 가능)&lt;/td&gt;
&lt;td style=&quot;width: 45.4651%;&quot;&gt;효율적 (컴팩션 시 중복 제거 및 정렬 저장으로 공간 활용 우수)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.3488%;&quot;&gt;&lt;b&gt;컴팩션/정리 작업&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.0698%;&quot;&gt;필요 없음&lt;/td&gt;
&lt;td style=&quot;width: 45.4651%;&quot;&gt;필요 (주기적인 SSTable 병합 및 정리 작업으로 I/O 소모 발생)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.3488%;&quot;&gt;&lt;b&gt;동시성 및 트랜잭션 처리&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.0698%;&quot;&gt;정교한 메커니즘 필요 (WAL, 페이지 잠금 등)&lt;/td&gt;
&lt;td style=&quot;width: 45.4651%;&quot;&gt;비교적 단순한 구조이지만, WAL 병행 필요 시 설계 복잡해짐&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.3488%;&quot;&gt;&lt;b&gt;구현 복잡도&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.0698%;&quot;&gt;오래된 구조로 널리 구현되어 있음, 구현은 복잡하나 안정적&lt;/td&gt;
&lt;td style=&quot;width: 45.4651%;&quot;&gt;구조 자체는 단순하나 컴팩션 전략, Bloom 필터, 멀티 레벨 병합 등은 복잡성 존재&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.3488%;&quot;&gt;&lt;b&gt;적합한 경우&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.0698%;&quot;&gt;읽기와 쓰기 균형이 필요하고, 트랜잭션 강력 지원이 중요한 OLTP 시스템&lt;/td&gt;
&lt;td style=&quot;width: 45.4651%;&quot;&gt;쓰기 처리량이 중요하고, 대용량 로그성 데이터에 적합 (예: 타임시리즈, 로그 저장 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;기타 색인 구조&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;보조 색인(secondary index) : 효율적으로 조인을 수행하는 데 결정적인 역할을 한다. 키-값 색인에서 쉽게 생성할 수 있고, 키가 고유하지 않아 같은 키를 가진 많은 로우가 있을 수 있다.&lt;/li&gt;
&lt;li&gt;색인 안에 값 저장하기
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;비클러스터 색인: 색인 안에 데이터의 참조만 저장&lt;/li&gt;
&lt;li&gt;클러스터 색인(clustered index): 색인 안에 모든 로우 데이터를 저장&lt;/li&gt;
&lt;li&gt;커버링 색인(covering index) / 포괄열이 있는 색인(index with included column) : 색인 안에 테이블의 칼럼 일부를 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;다중 칼럼 색인
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;결합 색인(concatenated index) : 하나의 칼럼에 다른 칼럼을 추가하는 방식으로 하나의 키에 여러 필드를 단순히 결합한다.&lt;/li&gt;
&lt;li&gt;다차원 색인 : 한 번에 여러 칼럼에 질의하는 조금 더 일반적인 방법&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;전문 검색과 퍼지 색인
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전문 검색 엔진 : 특정 단어를 검색할 때 해당 단어의 동의어로 질의를 확장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;모든 것을 메모리에 보관
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인메모리 데이터베이스 : 특수 하드웨어를 사용하거나 디스크에 변경 사항의 로그를 기록하거나 디스크에 주기적인 스냅숏을 기록하거나 다른 장비에 인메모리 상태를 복제하는 방법&lt;/li&gt;
&lt;li&gt;안티 캐싱(anti-caching) : 메모리가 충분하지 않을 때 가장 최근에 사용하지 않은 데이터를 메모리에서 디스크로 내보내고 나중에 다시 접근할 때 메모리에 적재하는 방식&lt;/li&gt;
&lt;li&gt;비휘발성 메모리(non-volatile memory. NVM) 기술&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;트랜잭션 처리나 분석?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;온라인 트랜잭션 처리(online transaction processing, OLTP)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;색인을 사용해 일부 키에 대한 적은 수의 레코드를 찾고, 레코드는 사용자 입력을 기반으로 삽입되거나 갱신된다.&lt;/li&gt;
&lt;li&gt;대화식으로 진행된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;온라인 분석 처리(online analytic processing, OLAP)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터베이스를 데이터 분석에도 점점 더 많이 사용하면서 등장&lt;/li&gt;
&lt;li&gt;많은 수의 레코드를 스캔해 레코드당 일부 칼람만 읽어 집계 통산을 계산하는 방식&lt;/li&gt;
&lt;li&gt;더 나은 의사결정을 하게끔 돕는 보고서를 제공(비즈니스 인텔리전스, business intelligence)&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 117px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;OLTP&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;OLAP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;주요 읽기 패턴&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;질의당 적은 수의 레코드, 키 기준으로 가져옴&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;많은 레코드에 대한 집계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;주요 쓰기 패턴&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;임의 접근, 사용자 입력을 낮은 지연 시간으로 기록&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;대규모 불러오기 또는 이벤트 스트림&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;주요 사용처&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;웹 애플리케이션을 통한 최종 사용자/소비자&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;의사결정 지원을 위한 내부 분석가&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;데이터 표현&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;데이터의 최신 상태(현재 시점)&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;시간이 지나며 일어난 이벤트 이력&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;&lt;b&gt;데이터셋 크기&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;기가바이트에서 테라바이트&lt;/td&gt;
&lt;td style=&quot;height: 20px;&quot;&gt;테라바이트에서 페타바이트&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;데이터 웨어하우스(data warehouse)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분석가들이 OLTP 작업에 영향을 주지 않고 마음껏 질의할 수 있는 개별 데이터베이스로, 회사 내의 모든 다양한 OLTP 시스템에 있는 데이터의 읽기 전용 복사본이다. 데이터는 OLTP 데이터베이스에서 추출하고 분석 친화적인 스키마로 변환하고 깨끗하게 정리한 다음 데이터 웨어하우스에 적재한다. 데이터 웨어하우스로 데이터를 가져오는 과정을 &lt;b&gt;ETL(Extract-Transform-Load)&lt;/b&gt;이라 한다. 데이터 웨어하우스를 사용하는 큰 장점은 분석 접근 패턴에 맞게 최적화할 수 있다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1074&quot; data-origin-height=&quot;760&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bzp1jK/btsOIY7mKWw/Hiou9ulvQcwLuz4R7Cv141/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bzp1jK/btsOIY7mKWw/Hiou9ulvQcwLuz4R7Cv141/img.png&quot; data-alt=&quot;Extract Transform Load&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bzp1jK/btsOIY7mKWw/Hiou9ulvQcwLuz4R7Cv141/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbzp1jK%2FbtsOIY7mKWw%2FHiou9ulvQcwLuz4R7Cv141%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;425&quot; data-origin-width=&quot;1074&quot; data-origin-height=&quot;760&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Extract Transform Load&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;OLTP 데이터베이스와 데이터 웨어하우스의 차이점&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;표면적으로 데이터 웨어하우스와 관계형 OLTP 데이터베이스는 둘 다 SQL 질의 인터페이스를 지원하기 때문에 비슷해 보이지만, 각각 매우 다른 질의 패턴에 맞게 최적화됐기 때문에 시스템의 내부는 완전히 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;분석용 스키마: 별 모양 스키마와 눈꽃송이 모양 스키마&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별 모양 스키마(star schema)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;차원 모델링(dimensional modeling) 이라고도 한다.&lt;/li&gt;
&lt;li&gt;사실 테이블(fact table)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;각 로우는 특정 시각에 발생한 이벤트에 해당한다.&lt;/li&gt;
&lt;li&gt;사실 테이블의 일부 칼럼은 속성이고, 다른 칼럼은 차원 테이블을 가리키는 외래 키 참조다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;차원 테이블(dimension table)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;각 로우는 이벤트의 속성인 누가(who), 언제(when), 어디서(where), 무엇을(what), 어떻게(how), 왜(why) 를 나타낸다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;테이블 관계가 시각화될 때 사실 테이블이 가운데 있고 차원 테이블로 둘러싸고 있다는 사실에서 이름이 비롯됐다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;눈꽃송이 모양 스키마(snowflake schema)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;별 모양 스키마 템플릿의 변형으로, &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;치원이 하위차원으로 더 세분화된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;별 모양 스키마보다 더 정규화됐다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;칼럼 지향 저장소&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대부분의 OLTP 데이터베이스에서 저장소는 로우 지향 방식으로 데이터를 배치한다.(테이블에서 한 로우의 모든 값은 서로 인접하게 저장된다.)&lt;/li&gt;
&lt;li&gt;칼럼 지향 저장소의 기본 개념은 모든 값을 하나의 로우에 함께 저장하지 않는 대신 각 칼럼별로 모든 값을 함께 저장하는 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1103&quot; data-origin-height=&quot;813&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cnzsZ4/btsOH8WHelb/F9RdtxkRBkE7kuKnjtUV1k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cnzsZ4/btsOH8WHelb/F9RdtxkRBkE7kuKnjtUV1k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cnzsZ4/btsOH8WHelb/F9RdtxkRBkE7kuKnjtUV1k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcnzsZ4%2FbtsOH8WHelb%2FF9RdtxkRBkE7kuKnjtUV1k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;442&quot; data-origin-width=&quot;1103&quot; data-origin-height=&quot;813&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;칼럼 지향 저장소의 배치는 각 칼럼 파일에 포함된 로우가 모두 같은 순서인 점에 의존한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;칼럼 압축&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 압축하면 디스크 처리량을 더 줄일 수 있고, 칼럼 지향 저장소는 대개 압축에 적합하다. 칼럼의 데이터에 따라 다양한 압축 기법을 사용할 수 있는데, 그 중 한 가지 기법은 데이터 웨어하우스에서 특히 효과적인 &lt;b&gt;비트맵 부호화(bitmap encoding)&lt;/b&gt;이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1103&quot; data-origin-height=&quot;811&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgDtym/btsOJpwDGjg/YfUXED0h2zKBlKNh65GgyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgDtym/btsOJpwDGjg/YfUXED0h2zKBlKNh65GgyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgDtym/btsOJpwDGjg/YfUXED0h2zKBlKNh65GgyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgDtym%2FbtsOJpwDGjg%2FYfUXED0h2zKBlKNh65GgyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;441&quot; data-origin-width=&quot;1103&quot; data-origin-height=&quot;811&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;메모리 대역폭과 벡터화 처리&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벡터화 처리(vectorized processing) : 비트 AND와 OR 같은 연사자를 이용하여 압축된 컬럼 덩어리를 바로 연산할 수 있도록 처리하는 방법&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;칼럼 저장소의 순서 정렬&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;칼럼 저장소에서 로우가 저장되는 순서가 반드시 중요하지는 않다.&lt;/li&gt;
&lt;li&gt;각 컬럼을 독립적으로 정렬할 수는 없다.&lt;/li&gt;
&lt;li&gt;칼럼별로 저장됐을지라도 데이터는 한 번에 전체 로우를 정렬해야 한다.&lt;/li&gt;
&lt;li&gt;장점
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;그룹화하거나 필터링하는 질의에 도움이 된다.&lt;/li&gt;
&lt;li&gt;칼럼 압축에 도움이 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;칼럼 지향 저장소에 쓰기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;칼람 지향 저장소, 압축, 정렬은 모두 읽기 질의를 더 빠르게 하지만 쓰기는 어렵게 하는 단점이 존재한다. 이러한 단점은 LSM 트리를 활용하여 해결할 수 있다. 모든 쓰기는 먼저 인메모리 저장소로 이동해 정렬된 구조에 추가하고 디스크에 쓸 준비를 한다. 인메모리 저장소가 로우 지향인지 칼럼 지향인지는 중요하지 않다. 충분한 쓰기를 모으면 디스크의 칼럼 파일에 병합하고 대량의 새로운 파일에 기록한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;집계: 데이터 큐브와 구체와 뷰&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구체화 집계(materialized aggragate)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;질의가 자주 사용하는 일부 카운트나 합을 캐시하는 방법&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;구체화 뷰(materialized view)
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;캐시를 만드는 방법으로, 관계형 데이터 모델에서는 이런 캐시를 대개 표준 (가상) 뷰로 정의한다.&lt;/li&gt;
&lt;li&gt;원본 데이터의 비정규화된 복사본이기 때문에 원본 데이터를 변경하면 구체화 뷰를 갱신해야 한다.&lt;/li&gt;
&lt;li&gt;갱신으로 인한 쓰기 비용이 비싸서 OLTP 데이터베이스에서는 구체화 뷰를 자주 사용하지 않지만, 데이터 웨어하우스는 읽기 비중이 크기 때문에 구체화 뷰를 사용하는 전략이 합리적이다.&lt;/li&gt;
&lt;li&gt;데이터 큐브(data cube) 또는 OLAP 큐브라고 알려진 구체화 뷰는 특정 질의를 효과적으로 미리 계산했기 때문에 해당 질의를 수행할 때 매우 빠지만, 원시 데이터에 질의하는 것과 동일한 유연성은 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>STUDY/데이터 중심 애플리케이션 설계</category>
      <author>level_?</author>
      <guid isPermaLink="true">https://it-study.tistory.com/125</guid>
      <comments>https://it-study.tistory.com/125#entry125comment</comments>
      <pubDate>Wed, 18 Jun 2025 22:15:00 +0900</pubDate>
    </item>
    <item>
      <title>2장. 데이터 모델과 질의 언어</title>
      <link>https://it-study.tistory.com/124</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;주요 내용&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;데이터 저장과 질의를 위한 다양한 범용 데이터 모델
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;관계형 모델(relational model)&lt;/li&gt;
&lt;li&gt;문서 모델(document model)&lt;/li&gt;
&lt;li&gt;그래프 기반 데이터 모델(graph-based data model)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;다양한 질의 언어와 사용 사례&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;데이터 모델&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;소프트웨어가 어떻게 작성됐는지 뿐만 아니라 해결하려는 &lt;b&gt;문제를 어떻게 생각해야 하는지&lt;/b&gt;에 대해서도 영향을 미친다.&lt;/li&gt;
&lt;li&gt;소프트웨어가 할 수 있는 일과 할 수 없는 일에 지대한 영향을 주므로 적합한 데이터 모델을 선택하는게 중요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;관계형 모델과 문서 모델&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계형 모델&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터는 &lt;b&gt;관계(relation)&lt;/b&gt;로 구성되고, 각 관계는 순서없는 &lt;b&gt;튜플(tuple)&lt;/b&gt; 모음이다.&lt;/li&gt;
&lt;li&gt;SQL은 1970년 Edgar Codd가 제안한 관계형 모델을 기반으로 한 데이터 모델이다.&lt;/li&gt;
&lt;li&gt;정규화된 구조로 데이터를 저장하고 질의할 필요가 있는 경우&amp;nbsp; &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;관계형 데이터베이스 관리 시스템(RDBMS)과 SQL이 &lt;/span&gt;사용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NoSQL(Not Only SQL)의 탄생&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대규모 데이터셋이나 메우 높은 쓰기 처리량 달성을 관계형 데이터베이스보다 쉽게 할 수 있는 뛰어난 확장성 필요&lt;/li&gt;
&lt;li&gt;상용 데이터베이스 제품보다 무료 오픈소스 소프트웨어에 대한 선호도 확산&lt;/li&gt;
&lt;li&gt;관계형 모델에서 지원하지 않는 특수 질의 동작&lt;/li&gt;
&lt;li&gt;관계형 스키마의 제한에 대한 불만과 더욱 동적이고 표현력이 풍부한 데이터 모델에 대한 바람&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션마다 요구사항이 다르기 때문에, 가까운 미래에는 관계형 데이터베이스가 폭넓은 다양함을 가진 비관계형 데이터스토어와 함께 사용될 것이다. 이런 개념을 &lt;b&gt;다중 저장소 지속성(polyglot persistence)&lt;/b&gt;이라 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 관계형 불일치&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;임피던스 불일치(impedance mismatch)&lt;/b&gt; : 데이터를 관계형 테이블에 저장하는 경우 애플리케이션 코드와 데이터베이스 모델 객체 사이에 거추장스러운 전환 계층이 필요&lt;/li&gt;
&lt;li&gt;ActiveRecord나 Hibernate 같은 객체 관계형 매핑(ORM) 프레임워크는 전환 계층에 필요한 상용구 코드(boilerplate code)의 양을 줄이지만 두 모델 간의 차이를 완벽히 숨기지 못한다.&lt;/li&gt;
&lt;li&gt;일부 개발자는 JSON 모델이 애플리케이션 코드와 저장 계층 간 임피던스 불일치를 줄인다고 생각한다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;ex) one-to-many(일대다) 관계를 갖는 이력서를 표현하는 경우 관계형 모델보다 JSON 데이터 모델이 더 적합&lt;/li&gt;
&lt;li&gt;JSON 표현은 다중 테이블 스키마보다 더 나은 지역성을 갖기 때문에, 질의 하나로 프로필 정보를 가져올 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다대일 관계&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터의 중복을 제거하는 일은 데이터베이스 정규화의 핵심 개념이다.&lt;/li&gt;
&lt;li&gt;중복된 데이터를 정규화하려면 다대일(many-to-one) 관계가 필요하다.&lt;/li&gt;
&lt;li&gt;관계형 데이터베이스에서는 조인이 쉽기 때문에 ID로 다른 테이블의 로우를 참조하는 방식이 일반적이다.&lt;/li&gt;
&lt;li&gt;문서 데이터베이스에서는 일대다 트리 구조를 위해 조인이 필요하지 않다.(조인에 대한 지원이 보통 약하다.)&lt;/li&gt;
&lt;li&gt;데이터베이스 자체가 조인을 지원하지 않으면 데이터베이스에 대한 다중 질의를 만들어서 애플리케이션 코드에서 조인을 흉내내야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다대다 관계&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;관계형 데이터베이스는 일상적으로 다대다 관계와 조인을 사용한다.&lt;/li&gt;
&lt;li&gt;문서 데이터베이스와 NoSQL 이전에도 데이터베이스에서 다대다 관계를 표현하는 제일 좋은 방법에 대한 논쟁이 있었다.&lt;/li&gt;
&lt;li&gt;1970년대 계층 모델은 문서 데이터베이스처럼 일대다 관계에서는 잘 동작하지만, 다대다 관계 표현이 어렵고 조인을 지원하지 않는다. 개발자는 (비정규화된) 데이터를 중복할지, 한 레코드와 다른 레코드의 참조를 수동으로 해결할지 결정해야 했다.&lt;/li&gt;
&lt;li&gt;이때 제시된 해결책 두 가지가 관계형 모델과 네트워크 모델이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 모델(코다실 모델)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;네트워크 모델은 계층 모델을 일반화하고, 네트워크 모델에서 레코드는 다중 부모가 있을 수 있다.&lt;/li&gt;
&lt;li&gt;다대일과 다대다 관계를 모델링할 수 있다.&lt;/li&gt;
&lt;li&gt;레코드 간 연결은 외래 키보다는 프로그래밍 언어의 포인터와 비슷하고, 레코드에 접근하는 유일한 방법은 최상위 레코드(root record)에서부터 연속된 연결 경로를 따르는 것이다. 이를 &lt;b&gt;접근 경로&lt;/b&gt;라 한다.&lt;/li&gt;
&lt;li&gt;다대다 관계에서는 다양한 다른 경로가 같은 레코드로 이어질 수 있어서 다양한 접근 경로를 계속 추적해야 한다.&lt;/li&gt;
&lt;li&gt;계층 모델과 네트워크 모델 모두, 원하는 데이터에 대한 경로가 없다면 어려운 상황에 놓이게 된다. 접근 경로를 변경할 수 있지만 아주 많은 수작업 데이터베이스 질의 코드를 살펴봐야 하고 새로운 접근 경로를 다루기 위해 재작성해야 하는 문제가 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계형 모델&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;nbsp;관계(테이블)는 단순히 튜플(로우)의 컬렉션이 전부이다.&lt;/li&gt;
&lt;li&gt;중첩 구조와 데이터를 보고 싶을 때 따라가야 할 복잡한 접근 경로가 없다.&lt;/li&gt;
&lt;li&gt;임의 조건과 일치하는 테이블의 일부 또는 모든 로우를 선택해서 읽을 수 있고, 일부 컬럼을 키로 지정해 컬럼과 일치하는 특정 로우를 읽을 수 있다.&lt;/li&gt;
&lt;li&gt;다른 테이블과의 외래 키 관계에 대해 신경쓰지 않고 임의 테이블에 새 로우를 삽입할 수 있다.&lt;/li&gt;
&lt;li&gt;관계형 데이터베이스에서 질의 최적화기(query opimizer)는 질의의 어느 부분을 어떤 순서로 실행할지를 결정하고 사용할 색인을 자동으로 결정한다.(접근 경로)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서 데이터베이스와의 비교&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;별도 테이블이 아닌 상위 레코드 내에 중첩된 레코드를 저장한다.&lt;/li&gt;
&lt;li&gt;다대일과 다대다 관계를 표현할 때 관련 항목은 문서 참조라고 부르는 고유 식별자로 참조한다. 이 식별자는 조인이나 후속 질의를 사용해 읽기 시점에 확인한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관계형 데이터베이스와 오늘날의 문서 데이터베이스 선호 이유&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문서 테이더 모델 : 스키마 유연성, 지역성에 기인한 더 나은 성능&lt;/li&gt;
&lt;li&gt;관계형 모델 : 조인, 다대일, 다대다 관계 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서 모델&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;애플리케이션에서 데이터가 문서와 비슷한 구조라면 문서 모델을 사용하는 것이 좋다.&lt;/li&gt;
&lt;li&gt;제한 존재
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;중첩 항목을 바로 참조할 수 없다. '사용자251의 직위 목록의 두 번째 항목'과 같이 표현해야 한다.&lt;/li&gt;
&lt;li&gt;조인 지원이 미흡하다.&lt;/li&gt;
&lt;li&gt;다대다 관계에서 훨씬 더 복잡한 애플리케이션 코드와 나쁜 성능으로 이어질 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;스키마 유연성
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;종종 스키마리스(schemaless)로 불리지만 오해의 소지가 있다.&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;/li&gt;
&lt;li&gt;질의를 위한 데이터 지역성
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;관련 데이터를 함께 그룹화하는 개념&lt;/li&gt;
&lt;li&gt;애플리케이션이 자주 전체 문서에 접근해야 할 때 저장소 지역성(storage locality)을 활용하면 성능 이점이 있다.&lt;/li&gt;
&lt;li&gt;지역성의 이점은 한 번에 해당 문서의 많은 부분을 필요로 하는 경우에만 적용된다. 데이터베이스는 대개 문서의 작은 부분에만 접근해도 전체 문서를 적재해야 하기에 큰 문서에서는 낭비일 수 있다.&lt;/li&gt;
&lt;li&gt;문서를 갱신할 때도 보통 전체 문서를 재작성해야 한다.&lt;/li&gt;
&lt;li&gt;부호화된 문서의 크기를 바꾸지 않는 수정은 쉽게 수행할 수 있다.&lt;/li&gt;
&lt;li&gt;일반적으로 문서를 아주 작게 유지하면서 문서의 크기가 증가하는 쓰기는 피하는걸 권장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;데이터를 위한 질의 언어&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;명령형 언어
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;특정 순서로 특정 연산을 수행하게끔 컴퓨터에게 지시한다.&lt;/li&gt;
&lt;li&gt;명령어를 특정 순서로 수행하게끔 지정하기 때문에 다중 코어나 다중 장비에서 병렬 처리가 매우 어렵다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;선언형 질의 언어
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;SQL이나 관계 대수&lt;/li&gt;
&lt;li&gt;결과가 충족해야 하는 조건과 데이터를 어떻게 변환할지를 결정하기만 하면 된다. 어떤 색인과 어떤 조인 함수를 사용할지, 질의의 다양한 부분을 어떤 순서로 실행할지는 질의 최적화기가 결정한다.&lt;/li&gt;
&lt;li&gt;명령형 API보다 더 간결하고 쉽게 작업할 수 있다.&lt;/li&gt;
&lt;li&gt;데이터베이스 엔진의 상세 구현이 숨겨져 있어 질의를 변경하지 않고도 데이터베이스 시스템의 성능을 향상시킬 수 있다.&lt;/li&gt;
&lt;li&gt;병렬 실행에 적합하다. 결과의 패턴만 지정하기 때문에 병렬 실행으로 더 빨라질 가능성이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;맵리듀스 질의
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;많은 컴퓨터에서 대량의 데이터를 처리하기 위한 프로그래밍 모델&lt;/li&gt;
&lt;li&gt;몽고DB와 카우치DB를 포함한 일부 NoSQL 데이터 저장소는 제한된 형태의 맵리듀스를 지원한다.&lt;/li&gt;
&lt;li&gt;선언형 질의 언어도 완전한 명령형 질의 API도 아닌 그 중간 정도에 있고, 질의 로직은 처리 프레임워크가 반복적으로 호출하는 조각 코드로 표현한다.&lt;/li&gt;
&lt;li&gt;상당히 저수준 프로그래밍 모델이다.&lt;/li&gt;
&lt;li&gt;연계된 자바스크립트 함수 두 개를 신중하게 작성해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;그래프형 데이터 모델&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다대다 관계가 일반적일 때 사용한다.&lt;/li&gt;
&lt;li&gt;그래프는 정점(vertex, 노드나 엔티티라고도 한다.)과 간선(edge, 관계나 호(arc)라고도 한다.)으로 이뤄진다.&lt;/li&gt;
&lt;li&gt;예시
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;소셜 그래프 : 정점은 사람이고 간선은 사람들이 서로 알고 있음을 나타낸다.&lt;/li&gt;
&lt;li&gt;웹 그래프 : 정점은 웹 페이지고 간선은 다른 페이지에 대한 HTML 링크를 나타낸다.&lt;/li&gt;
&lt;li&gt;도로나 철도 네트워크 : 정점은 교차로이고 간선은 교차로 간 도로나 철로 선을 나타낸다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;그래프는 동종 데이터에 국한되지 않는다.&lt;/li&gt;
&lt;li&gt;그래프를 동종 데이터와 마찬가지 방식으로 사용하면 단일 데이터 저장소에 완전히 다른 유형의 객체를 일관성 있게 저장할 수 있는 강력한 방법을 제공한다.
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;ex) 페이스북은 다른 유형의 정점과 간선을 단일 그래프로 유지한다. 정점은 사람, 장소, 이벤트 등을 나타내고, 간선은 어떤 사람이 서로 친구인지 누가 이벤트에 참여했는지 등을 나타낸다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;종류
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;모델 : 속성 그래프 모델(Neo4j, Titan, InfiniteGraph 등으로 구현), 트리플 저장소 모델(Datomic, Allegrograph 등으로 구현)&lt;/li&gt;
&lt;li&gt;그래프형 선언형 질의 언어 : 사이퍼(Cypher), 스파클(SPARQL), 데이터로그(Datalog)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;속성 그래프 모델&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정점 구성 요소&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;고유한 식별자&lt;/li&gt;
&lt;li&gt;유출(outgoing) 간선 집합&lt;/li&gt;
&lt;li&gt;유입(incoming) 간선 집합&lt;/li&gt;
&lt;li&gt;속성 컬렉션(키-값 쌍)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간선의 구성 요소&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;고유한 식별자&lt;/li&gt;
&lt;li&gt;간선이 시작하는 정점(꼬리 정점)&lt;/li&gt;
&lt;li&gt;간선이 끝나는 정점(머리 정점)&lt;/li&gt;
&lt;li&gt;두 정잠 간 관계 유형을 설명하는 레이블&lt;/li&gt;
&lt;li&gt;속성 컬렉션(키-값 쌍)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 특징&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;정점은 다른 정점과 간선으로 연결된다. 특정 유형과 관련 여부를 제한하는 스키마는 없다.&lt;/li&gt;
&lt;li&gt;정점이 주어지면 정점의 유입과 유출 간선을 효율적으로 찾을 수 있고 그래프를 순회할 수 있다. 즉 일련의 정점을 따라 앞뒤 방향으로 순회한다.&lt;/li&gt;
&lt;li&gt;다른 유형의 관계에 서로 다른 레이블을 사용하면 단일 그래프에 다른 유형의 정보를 저장하면서도 데이터 모델을 깔끔하게 유지할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래프는 발전성이 좋아서 애플리케이션에 기능을 추가하는 경우 애플리케이션의 데이터 구조 변경을 수용하게끔 그래프를 쉽게 확장할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사이퍼 질의 언어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사이퍼(Cypher)는 속성 그래프를 위한 선언형 질의 언어로, 네오포제이(Neo4j) 그래프 데이터베이스용으로 만들어졌다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL의 그래프 질의&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;재귀 공통 테이블식(recursive common table expression)(WITH RECURSIVE문)을 사용해서 표현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트리플 저장소와 스파클&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트리플 저장소에서는 모든 정보를 &lt;b&gt;주어(subject), 서술어(predicate), 목적어(object)&lt;/b&gt;처럼 매우 간단한 세 부분 구문(three-part statements) 형식으로 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주어&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;그래프의 정점&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;목적어&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;문자열이나 숫자 같은 원시 데이터타입의 값. 이 경우 트리플의 서술어와 목적어는 주어 정점에서 속성의 키, 값과 동등하다.&lt;/li&gt;
&lt;li&gt;그래프의 다른 정점. 이 경우 서술어는 그래프의 간선이고 주어는 꼬리 정점이며 목적어는 머리 정점이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시멘틱 웹&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDF 데이터 모델&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스파클 질의 언어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스파클(SPARQL)은 RDF 데이터 모델을 사용한 트리플 저장소 질의 언어다. 사이퍼보다 먼저 만들었고 사이퍼의 패턴 매칭을 스파클에서 차용했기 때문에 둘은 매우 유사해 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초석:데이터로그&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스파클이나 사이퍼보다 훨씬 오래된 언어로, &lt;b&gt;서술어(주어, 목적어)&lt;/b&gt;로 작성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사이퍼와 스파클과 비교했을 때, 두 가지 방식은 SELECT로 바로 질의하는 반면 데이터로그는 단계를 나눠 한 번에 조금씩 질의로 나아간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;정리&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;관계형 모델&lt;/td&gt;
&lt;td&gt;문서 모델&lt;/td&gt;
&lt;td&gt;그래프 모델&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;데이터 구조&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;테이블(행과 열)&lt;/td&gt;
&lt;td&gt;계층적 문서 (JSON, XML 등)&lt;/td&gt;
&lt;td&gt;노드와 엣지 (vertex-edge)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;스키마&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;엄격한 스키마 (정의 필요)&lt;/td&gt;
&lt;td&gt;유연한 스키마 (동적 구조 가능)&lt;/td&gt;
&lt;td&gt;유연한 구조 (관계 중심)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;질의 언어&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;SQL (표준화됨)&lt;/td&gt;
&lt;td&gt;자체 API 또는 확장된 SQL&lt;/td&gt;
&lt;td&gt;그래프 질의 언어 (Cypher, Gremlin 등)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;관계 표현&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;외래 키 (JOIN 사용)&lt;/td&gt;
&lt;td&gt;중첩 문서 또는 참조&lt;/td&gt;
&lt;td&gt;직접적인 엣지로 관계 표현&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;중첩 구조 지원&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;지원 안 됨 (정규화 필요)&lt;/td&gt;
&lt;td&gt;자연스럽게 지원&lt;/td&gt;
&lt;td&gt;가능 (노드/엣지 구조에서 표현)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;확장성 (수평 확장)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;어렵지만 가능 (Sharding 복잡)&lt;/td&gt;
&lt;td&gt;비교적 쉬움&lt;/td&gt;
&lt;td&gt;어렵거나 제한적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;일관성 모델&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;강한 일관성 (트랜잭션 지원)&lt;/td&gt;
&lt;td&gt;보통 약한 일관성 (시스템에 따라 다름)&lt;/td&gt;
&lt;td&gt;다양함 (ACID 또는 eventual)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;적합한 사용 사례&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;전통적인 비즈니스 앱, 금융, ERP&lt;/td&gt;
&lt;td&gt;콘텐츠 관리, 제품 카탈로그&lt;/td&gt;
&lt;td&gt;소셜 네트워크, 추천 시스템, 사기 탐지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;성숙한 기술, 표준화된 도구 및 언어&lt;/td&gt;
&lt;td&gt;유연성, 개발 편의성&lt;/td&gt;
&lt;td&gt;복잡한 관계 표현에 적합&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;단점&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;스키마 변경 어려움, 복잡한 JOIN&lt;/td&gt;
&lt;td&gt;중복 데이터, JOIN 비효율&lt;/td&gt;
&lt;td&gt;대규모 확장이 어려움, 복잡도 증가&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</description>
      <category>STUDY/데이터 중심 애플리케이션 설계</category>
      <author>level_?</author>
      <guid isPermaLink="true">https://it-study.tistory.com/124</guid>
      <comments>https://it-study.tistory.com/124#entry124comment</comments>
      <pubDate>Wed, 11 Jun 2025 22:31:18 +0900</pubDate>
    </item>
    <item>
      <title>Visitor Pattern(방문자 패턴)</title>
      <link>https://it-study.tistory.com/123</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;방문자 패턴이란?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 구조를 변경하지 않고, 그 객체들에 대해 새로운 기능을 추가할 수 있게 해주는 행동 디자인 패턴이다. 객체의 구조가 자주 바뀌지 않지만, 객체에 대한 새로운 작업을 자주 추가해야 하는 경우 유용하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;문제&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;260&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b3Draf/btsMBS83fJv/LAGbONSoEmnVRbSV494qu0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b3Draf/btsMBS83fJv/LAGbONSoEmnVRbSV494qu0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b3Draf/btsMBS83fJv/LAGbONSoEmnVRbSV494qu0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb3Draf%2FbtsMBS83fJv%2FLAGbONSoEmnVRbSV494qu0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;260&quot; data-origin-width=&quot;500&quot; data-origin-height=&quot;260&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;XML 내보내기라는 새로운 기능을 추가할 때, 기존 클래스에 기능을 추가하게 되면 기존 코드에 영향을 많이 주고 복잡성을 증가시킬 수 있다. 또한, 다른 기능이 추가되는 경우 기존 클래스에 변경이 자주 발생하게 되어, 다른 부분에 영향을 줄 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;해결&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;방문자 패턴을 사용해 각 객체를 처리하는 별도의 Visitor 클래스를 만들고, 객체들에 대한 작업을 외부에서 수행한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 객체들은 accept() 메서드를 통해 Visitor 클래스를 받아들이고, 이를 통해 외부에서 작업을 수행하도록 한다.&lt;/li&gt;
&lt;li&gt;Visitor 클래스는 각 객체들에 대한 작업을 정의한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;구조&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;520&quot; data-origin-height=&quot;540&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AD1q6/btsMzhIghK6/V9jEtMc6vFNwMXLEKZkF3K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AD1q6/btsMzhIghK6/V9jEtMc6vFNwMXLEKZkF3K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AD1q6/btsMzhIghK6/V9jEtMc6vFNwMXLEKZkF3K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAD1q6%2FbtsMzhIghK6%2FV9jEtMc6vFNwMXLEKZkF3K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;520&quot; height=&quot;540&quot; data-origin-width=&quot;520&quot; data-origin-height=&quot;540&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Visitor&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체 구조의 구상 요소들을 인수들로 사용할 수 있는 비지터 메서드들의 집합을 선언하는 인터페이스로, 이러한 메서드들은 같은 이름을 가질 수 있지만 매개변수 유형은 달라야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ConcreteVisitors&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구상 비지터 클래스는 다양한 구상 요소 클래스들에 맞춤으로 작성된 같은 행동들의 여러 버전을 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Element&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비지터를 '수락'하는 메서드를 선언하는 인터페이스로, 이 메서드에는 비지터 인터페이스 유형으로 선언된 하나의 매개변수가 있어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ConcreteElement&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 구상 요소 클래스는 반드시 수락 메서드를 구현해야 한다. 이 메서드의 목적은 호출을 현재 요소 클래스에 해당하는 적절한 비지터 메서드로 리다이렉트하는 것이다. 기초 요소 클래스가 이 메서드를 구현하더라도 모든 자식 클래스들은 여전히 자신들의 클래스들 내에서 이 메서드를 오버라이드해야 하며 비지터 객체에 적절한 메서드를 호출해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Client&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 컬렉션 또는 기타 복잡한 객체를 나타낸다. 일반적으로 클라이언트들은 해당 컬렌셔의 객체들과 어떠한 추상 인터페이스를 통해 작업하기 때문에 모든 구상 요소 클래스들을 인식하지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예시&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;560&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7zj8f/btsMAGUDJg9/xqya59G69l7lnGrGmrVYR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7zj8f/btsMAGUDJg9/xqya59G69l7lnGrGmrVYR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7zj8f/btsMAGUDJg9/xqya59G69l7lnGrGmrVYR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7zj8f%2FbtsMAGUDJg9%2Fxqya59G69l7lnGrGmrVYR0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;560&quot; height=&quot;490&quot; data-origin-width=&quot;560&quot; data-origin-height=&quot;490&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Element&lt;/p&gt;
&lt;pre id=&quot;code_1740997766129&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Shape {
    void move(int x, int y);
    void draw();
    String accept(Visitor visitor);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ConcreteElement&lt;/p&gt;
&lt;pre id=&quot;code_1740997784514&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Dot implements Shape {
    private int id;
    private int x;
    private int y;

    public Dot() {
    }

    public Dot(int id, int x, int y) {
        this.id = id;
        this.x = x;
        this.y = y;
    }

    @Override
    public void move(int x, int y) {
        // move shape
    }

    @Override
    public void draw() {
        // draw shape
    }

    @Override
    public String accept(Visitor visitor) {
        return visitor.visitDot(this);
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public int getId() {
        return id;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1740997806189&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Circle extends Dot {
    private int radius;

    public Circle(int id, int x, int y, int radius) {
        super(id, x, y);
        this.radius = radius;
    }

    @Override
    public String accept(Visitor visitor) {
        return visitor.visitCircle(this);
    }

    public int getRadius() {
        return radius;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1740997810134&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Rectangle implements Shape {
    private int id;
    private int x;
    private int y;
    private int width;
    private int height;

    public Rectangle(int id, int x, int y, int width, int height) {
        this.id = id;
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
    }

    @Override
    public String accept(Visitor visitor) {
        return visitor.visitRectangle(this);
    }

    @Override
    public void move(int x, int y) {
        // move shape
    }

    @Override
    public void draw() {
        // draw shape
    }

    public int getId() {
        return id;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public int getWidth() {
        return width;
    }

    public int getHeight() {
        return height;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1740997820672&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class CompoundShape implements Shape {
    public int id;
    public List&amp;lt;Shape&amp;gt; children = new ArrayList&amp;lt;&amp;gt;();

    public CompoundShape(int id) {
        this.id = id;
    }

    @Override
    public void move(int x, int y) {
        // move shape
    }

    @Override
    public void draw() {
        // draw shape
    }

    public int getId() {
        return id;
    }

    @Override
    public String accept(Visitor visitor) {
        return visitor.visitCompoundGraphic(this);
    }

    public void add(Shape shape) {
        children.add(shape);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;Visitor&lt;/p&gt;
&lt;pre id=&quot;code_1740997836977&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Visitor {
    String visitDot(Dot dot);

    String visitCircle(Circle circle);

    String visitRectangle(Rectangle rectangle);

    String visitCompoundGraphic(CompoundShape cg);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ConcreteVisitor&lt;/p&gt;
&lt;pre id=&quot;code_1740997850163&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class XMLExportVisitor implements Visitor {

    public String export(Shape... args) {
        StringBuilder sb = new StringBuilder();
        sb.append(&quot;&amp;lt;?xml version=\&quot;1.0\&quot; encoding=\&quot;utf-8\&quot;?&amp;gt;&quot; + &quot;\n&quot;);
        for (Shape shape : args) {
            sb.append(shape.accept(this)).append(&quot;\n&quot;);
        }
        return sb.toString();
    }

    public String visitDot(Dot d) {
        return &quot;&amp;lt;dot&amp;gt;&quot; + &quot;\n&quot; +
                &quot;    &amp;lt;id&amp;gt;&quot; + d.getId() + &quot;&amp;lt;/id&amp;gt;&quot; + &quot;\n&quot; +
                &quot;    &amp;lt;x&amp;gt;&quot; + d.getX() + &quot;&amp;lt;/x&amp;gt;&quot; + &quot;\n&quot; +
                &quot;    &amp;lt;y&amp;gt;&quot; + d.getY() + &quot;&amp;lt;/y&amp;gt;&quot; + &quot;\n&quot; +
                &quot;&amp;lt;/dot&amp;gt;&quot;;
    }

    public String visitCircle(Circle c) {
        return &quot;&amp;lt;circle&amp;gt;&quot; + &quot;\n&quot; +
                &quot;    &amp;lt;id&amp;gt;&quot; + c.getId() + &quot;&amp;lt;/id&amp;gt;&quot; + &quot;\n&quot; +
                &quot;    &amp;lt;x&amp;gt;&quot; + c.getX() + &quot;&amp;lt;/x&amp;gt;&quot; + &quot;\n&quot; +
                &quot;    &amp;lt;y&amp;gt;&quot; + c.getY() + &quot;&amp;lt;/y&amp;gt;&quot; + &quot;\n&quot; +
                &quot;    &amp;lt;radius&amp;gt;&quot; + c.getRadius() + &quot;&amp;lt;/radius&amp;gt;&quot; + &quot;\n&quot; +
                &quot;&amp;lt;/circle&amp;gt;&quot;;
    }

    public String visitRectangle(Rectangle r) {
        return &quot;&amp;lt;rectangle&amp;gt;&quot; + &quot;\n&quot; +
                &quot;    &amp;lt;id&amp;gt;&quot; + r.getId() + &quot;&amp;lt;/id&amp;gt;&quot; + &quot;\n&quot; +
                &quot;    &amp;lt;x&amp;gt;&quot; + r.getX() + &quot;&amp;lt;/x&amp;gt;&quot; + &quot;\n&quot; +
                &quot;    &amp;lt;y&amp;gt;&quot; + r.getY() + &quot;&amp;lt;/y&amp;gt;&quot; + &quot;\n&quot; +
                &quot;    &amp;lt;width&amp;gt;&quot; + r.getWidth() + &quot;&amp;lt;/width&amp;gt;&quot; + &quot;\n&quot; +
                &quot;    &amp;lt;height&amp;gt;&quot; + r.getHeight() + &quot;&amp;lt;/height&amp;gt;&quot; + &quot;\n&quot; +
                &quot;&amp;lt;/rectangle&amp;gt;&quot;;
    }

    public String visitCompoundGraphic(CompoundShape cg) {
        return &quot;&amp;lt;compound_graphic&amp;gt;&quot; + &quot;\n&quot; +
                &quot;   &amp;lt;id&amp;gt;&quot; + cg.getId() + &quot;&amp;lt;/id&amp;gt;&quot; + &quot;\n&quot; +
                _visitCompoundGraphic(cg) +
                &quot;&amp;lt;/compound_graphic&amp;gt;&quot;;
    }

    private String _visitCompoundGraphic(CompoundShape cg) {
        StringBuilder sb = new StringBuilder();
        for (Shape shape : cg.children) {
            String obj = shape.accept(this);
            // Proper indentation for sub-objects.
            obj = &quot;    &quot; + obj.replace(&quot;\n&quot;, &quot;\n    &quot;) + &quot;\n&quot;;
            sb.append(obj);
        }
        return sb.toString();
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Client&lt;/p&gt;
&lt;pre id=&quot;code_1740997871973&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Demo {
    public static void main(String[] args) {
        Dot dot = new Dot(1, 10, 55);
        Circle circle = new Circle(2, 23, 15, 10);
        Rectangle rectangle = new Rectangle(3, 10, 17, 20, 30);

        CompoundShape compoundShape = new CompoundShape(4);
        compoundShape.add(dot);
        compoundShape.add(circle);
        compoundShape.add(rectangle);

        CompoundShape c = new CompoundShape(5);
        c.add(dot);
        compoundShape.add(c);

        export(circle, compoundShape);
    }

    private static void export(Shape... shapes) {
        XMLExportVisitor exportVisitor = new XMLExportVisitor();
        System.out.println(exportVisitor.export(shapes));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;적용&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;복잡한 객체 구조(ex, 객체 트리)의 모든 요소에 대해 작업을 수행해야 하는 경우&lt;/li&gt;
&lt;li&gt;행동이 클래스 계층구소의 일부 클래스들에서만 의미가 있고 다른 클래스들에서는 의미가 없는 경우&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;장단점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;개방/폐쇄 원칙. 다른 클래스를 변경하지 않으면서 해당 클래스의 객체와 작동할 수 있는 새로운 행동을 도입할 수 있다.&lt;/li&gt;
&lt;li&gt;단일 책임 원칙. 같은 행동의 여러 버전을 같은 클래스로 이동할 수 있다.&lt;/li&gt;
&lt;li&gt;다양한 객체들과 작업하면서 유용한 정보를 축적할 수 있다.&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;클래스가 요소 계층구조에 추가되거나 제거될 때마다 모든 비지터를 업데이트해야 한다.&lt;/li&gt;
&lt;li&gt;비지터들은 함께 작업해야 하는 요소들의 비공개 필드 및 메서드에 접근하기 위해 필요한 권한이 부족할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>STUDY/디자인패턴</category>
      <author>level_?</author>
      <guid isPermaLink="true">https://it-study.tistory.com/123</guid>
      <comments>https://it-study.tistory.com/123#entry123comment</comments>
      <pubDate>Mon, 3 Mar 2025 19:31:16 +0900</pubDate>
    </item>
    <item>
      <title>Template Method Pattern(템플릿 메서드 패턴)</title>
      <link>https://it-study.tistory.com/122</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;템플릿 메서드 패턴이란?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부모 클래스에서 알고리즘의 골격을 정의하지만, 해당 알고리즘의 구조를 변경하지 않고 자식 클래스들이 알고리즘의 특정 단계들을 재정의할 수 있도록 하는 행동 디자인 패턴&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;템플릿 메서드 패턴은 알고리즘을 일련의 단계들로 나누고, 이러한 단계들을 메서드들로 변환한 뒤, 단일 템플릿 메서드 내부에서 이러한 메서드들을 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 단계들은 abstract(추상)이거나 일부 디폴트 구현을 가지게 된다. &lt;span style=&quot;background-color: #ffffff; color: #444444; text-align: justify;&quot;&gt;알고리즘을 사용하기 위해 클라이언트는 자신의 자식 클래스를 제공해야 하고, 모든 추상 단계를 구현해야 하며, 필요하다면 (템플릿 메서드를 제외한) 선택적 단계 중 일부를 오버라이드​(재정의)​해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #444444; text-align: justify;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 자식 클래스는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;추&lt;/span&gt;&lt;span&gt;상&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;단&lt;/span&gt;&lt;span&gt;계&lt;/span&gt;&lt;span&gt;들&lt;/span&gt;을 구현해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;span&gt;선&lt;/span&gt;&lt;span&gt;택&lt;/span&gt;&lt;span&gt;적&lt;/span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span&gt;단&lt;/span&gt;&lt;span&gt;계&lt;/span&gt;&lt;span&gt;들&lt;/span&gt;에는 이미 어떤 디폴트​(기본값) 구현이 있지만, 필요한 경우 이를 무시하고 오버라이드​(재정의) 할 수 있다.&lt;/li&gt;
&lt;li&gt;'훅'은 몸체가 비어 있는 선택적 단계로, 해당 단계는 오버라이드 되지 않아도 작동한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;구조&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;340&quot; data-origin-height=&quot;380&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dSwYD8/btsMA7LdhmM/PY6ajzyJhnChSi4IO1zct0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dSwYD8/btsMA7LdhmM/PY6ajzyJhnChSi4IO1zct0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dSwYD8/btsMA7LdhmM/PY6ajzyJhnChSi4IO1zct0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdSwYD8%2FbtsMA7LdhmM%2FPY6ajzyJhnChSi4IO1zct0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;340&quot; height=&quot;380&quot; data-origin-width=&quot;340&quot; data-origin-height=&quot;380&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AbstractClass&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #444444; text-align: justify;&quot;&gt;추상 클래스는 알고리즘의 단계들의 역할을 하는 메서드들을 선언하며, 이러한 메서드를 특정 순서로 호출하는 실제 템플릿 메서드도 선언한다. 단계들은 abstract로 선언되거나 일부 디폴트 구현을 갖는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ConcreteClass&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #444444; text-align: justify;&quot;&gt;구상 클래스들은 모든 단계들을 오버라이드할 수 있지만 템플릿 메서드 자체는 오버라이드 할 수 없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #444444; text-align: justify;&quot;&gt;예시&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #444444;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;AbstractClass&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1740996369593&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public abstract class Network {
    String userName;
    String password;

    Network() {}

    public boolean post(String message) {
        if (logIn(this.userName, this.password)) {
            boolean result =  sendData(message.getBytes());
            logOut();
            return result;
        }
        return false;
    }

    abstract boolean logIn(String userName, String password);
    abstract boolean sendData(byte[] data);
    abstract void logOut();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #444444; text-align: justify;&quot;&gt;ConcreteClass1&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1740996388689&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Facebook extends Network {
    public Facebook(String userName, String password) {
        this.userName = userName;
        this.password = password;
    }

    public boolean logIn(String userName, String password) {
        System.out.println(&quot;\nChecking user's parameters&quot;);
        System.out.println(&quot;Name: &quot; + this.userName);
        System.out.print(&quot;Password: &quot;);
        for (int i = 0; i &amp;lt; this.password.length(); i++) {
            System.out.print(&quot;*&quot;);
        }
        simulateNetworkLatency();
        System.out.println(&quot;\n\nLogIn success on Facebook&quot;);
        return true;
    }

    public boolean sendData(byte[] data) {
        boolean messagePosted = true;
        if (messagePosted) {
            System.out.println(&quot;Message: '&quot; + new String(data) + &quot;' was posted on Facebook&quot;);
            return true;
        } else {
            return false;
        }
    }

    public void logOut() {
        System.out.println(&quot;User: '&quot; + userName + &quot;' was logged out from Facebook&quot;);
    }

    private void simulateNetworkLatency() {
        try {
            int i = 0;
            System.out.println();
            while (i &amp;lt; 10) {
                System.out.print(&quot;.&quot;);
                Thread.sleep(500);
                i++;
            }
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #444444; text-align: justify;&quot;&gt;ConcreteClass2&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1740996407742&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Twitter extends Network {

    public Twitter(String userName, String password) {
        this.userName = userName;
        this.password = password;
    }

    public boolean logIn(String userName, String password) {
        System.out.println(&quot;\nChecking user's parameters&quot;);
        System.out.println(&quot;Name: &quot; + this.userName);
        System.out.print(&quot;Password: &quot;);
        for (int i = 0; i &amp;lt; this.password.length(); i++) {
            System.out.print(&quot;*&quot;);
        }
        simulateNetworkLatency();
        System.out.println(&quot;\n\nLogIn success on Twitter&quot;);
        return true;
    }

    public boolean sendData(byte[] data) {
        boolean messagePosted = true;
        if (messagePosted) {
            System.out.println(&quot;Message: '&quot; + new String(data) + &quot;' was posted on Twitter&quot;);
            return true;
        } else {
            return false;
        }
    }

    public void logOut() {
        System.out.println(&quot;User: '&quot; + userName + &quot;' was logged out from Twitter&quot;);
    }

    private void simulateNetworkLatency() {
        try {
            int i = 0;
            System.out.println();
            while (i &amp;lt; 10) {
                System.out.print(&quot;.&quot;);
                Thread.sleep(500);
                i++;
            }
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #444444; text-align: justify;&quot;&gt;적용&lt;/span&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클라이언트들이 알고리즘의 특정 단계들만 확장할 수 있도록 하고, &lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;전체 알고리즘이나 알고리즘 구조는 확장하지 못하도록 하는 경우&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;약간의 차이는 있지만 거의 같은 알고리즘들을 포함하는 여러 클래스가 있는 경우&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;장단점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트들이 대규모 알고리즘의 특정 부분만 오버라이드하도록 하여 그들이 알고리즘의 다른 부분에 발생하는 변경에 영향을 덜 받도록 할 수 있다.&lt;/li&gt;
&lt;li&gt;중복 코드를 부모 클래스로 가져올 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;일부 클라이언트들은 알고리즘의 제공된 골격에 의해 제한될 수 있다.&lt;/li&gt;
&lt;li&gt;자식 클래스를 통해 디폴트 단계 구현을 억제하여 리스코프 치환 원칙을 위반할 수 있다.&lt;/li&gt;
&lt;li&gt;템플릿 메서드들은 단계들이 더 많을수록 유지가 더 어려운 경향이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>STUDY/디자인패턴</category>
      <author>level_?</author>
      <guid isPermaLink="true">https://it-study.tistory.com/122</guid>
      <comments>https://it-study.tistory.com/122#entry122comment</comments>
      <pubDate>Mon, 3 Mar 2025 19:06:46 +0900</pubDate>
    </item>
    <item>
      <title>Strategy Pattern(전략 패턴)</title>
      <link>https://it-study.tistory.com/121</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;전략 패턴이란?&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;실행(런타임) 중에 알고리즘 전략을 선택하여 객체 동작을 실시간으로 바뀌도록 할 수 있게 하는 행동 디자인 패턴으로, 특정 작업을 다양한 방식으로 수행하는 클래스를 선택한 후 모든 알고리즘을 전략들(strategies)이라는 별도의 클래스들로 추출한다.&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;구조&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;370&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZUuwO/btsMA6FwTlf/qfiFTtO7mhJDjt3qLH5QMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZUuwO/btsMA6FwTlf/qfiFTtO7mhJDjt3qLH5QMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZUuwO/btsMA6FwTlf/qfiFTtO7mhJDjt3qLH5QMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZUuwO%2FbtsMA6FwTlf%2FqfiFTtO7mhJDjt3qLH5QMk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;440&quot; height=&quot;370&quot; data-origin-width=&quot;440&quot; data-origin-height=&quot;370&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Context&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구상 전략 중 하나에 대한 참조를 유지하고 전략 인터페이스를 통해서만 이 객체와 통신한다. 알고리즘을 실행해야 할 때마다 연결된 전략 객체의 실행 메서드를 호출한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Strategy&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘텍스트가 전략을 실행하는 데 사용하는 메서드를 선언한 전략 인터페이스&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ConcreteStrategies&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구상 전략은 콘텐스트가 사용하는 알고리즘의 다양한 변형들을 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Client&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 전략 객체를 만들어 콘텍스트에 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;예시&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Strategy&lt;/p&gt;
&lt;pre id=&quot;code_1740994851888&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Strategy {
    int execute(int a, int b);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ConcreteStrategies&lt;/p&gt;
&lt;pre id=&quot;code_1740994980046&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ConcreteStrategyAdd implements Strategy {
   @Override
    public int execute(int a, int b) {
        return a + b
    }
}

class ConcreteStrategySubtract implements Strategy {
    @Override
    public int execute(int a, int b) {
        return a - b
    }
}

class ConcreteStrategyMultiply implements Strategy {
    @Override
    public int execute(int a, int b) {
        return a * b
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Context&lt;/p&gt;
&lt;pre id=&quot;code_1740995106312&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Context {
	private Strategy strategy;
    
    public void setStrategy(Strategy strategy) {
    	this.strategy = strategy;
    }
    
    public void executeStrategy(int a, int b) {
    	return strategy.execute(a, b);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Client&lt;/p&gt;
&lt;pre id=&quot;code_1740995299930&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ExampleApplication {
    public static void main(String[] args) {
        Context context = new Context();
        
        int a = 10;
        int b = 5;
        String action = &quot;addition&quot;;

        if (action == addition) {
        	context.setStrategy(new ConcreteStrategyAdd())
        }
        
        if (action == subtraction) {
        	context.setStrategy(new ConcreteStrategySubtract())
        }
        
        if (action == multiplication) {
        	context.setStrategy(new ConcreteStrategyMultiply())
        }

        System.out.println(context.executeStrategy(a, b));
    }
        
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;적용&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;객체 내에서 한 알고리즘의 다양한 변형들을 사용하고 싶은 경우&lt;/li&gt;
&lt;li&gt;런타임 중에 한 알고리즘에서 다른 알고리즘으로 전환하고 싶은 경우&lt;/li&gt;
&lt;li&gt;일부 행동을 실행하는 방식에서만 차이가 있는 유사한 클래스들이 많은 경우&lt;/li&gt;
&lt;li&gt;알고리즘의 다른 변형들 사이를 전환하는 거대한 조건문이 존재하는 경우&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;장단점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;런타임에 한 객체 내부에서 사용되는 알고리즘들을 교환할 수 있다.&lt;/li&gt;
&lt;li&gt;알고리즘을 사용하는 코드에서 알고리즘의 세부 구현 정보들을 분리할 수 있다.&lt;/li&gt;
&lt;li&gt;상속을 합성으로 대체할 수 있다.&lt;/li&gt;
&lt;li&gt;개방/폐쇄 원칙. 콘텍스트를 변경하지 않고도 새로운 전략들을 도입할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;알고리즘이 몇 개밖에 되지 않고 거의 변하지 않는 경우 새로운 클래스들과 인터페이스들로 프로그램이 지나지게 복잡해질 수 있다.&lt;/li&gt;
&lt;li&gt;클라이언트들은 적절한 전략을 선택할 수 있도록 전략 간의 차이점들을 알고 있어야 한다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffffff; color: #444444; text-align: justify;&quot;&gt;많은 프로그래밍 언어에서 익명 함수들의 집합 내에서 알고리즘의 다양한 버전들을 구현할 수 있는 함수형 지원이 있으며, 클래스들과 인터페이스들을 추가하여 코드의 부피를 늘리지 않으면서도 전략 객체를 사용했을 때와 똑같이 이러한 함수들을 사용할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>STUDY/디자인패턴</category>
      <author>level_?</author>
      <guid isPermaLink="true">https://it-study.tistory.com/121</guid>
      <comments>https://it-study.tistory.com/121#entry121comment</comments>
      <pubDate>Mon, 3 Mar 2025 18:48:42 +0900</pubDate>
    </item>
  </channel>
</rss>