<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://blog.magical.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://blog.magical.dev/" rel="alternate" type="text/html" /><updated>2026-09-19T10:35:16+00:00</updated><id>https://blog.magical.dev/feed.xml</id><title type="html">Magical Blog</title><subtitle>Somethin&apos; Magical
</subtitle><author><name>Las aka Wonho</name></author><entry><title type="html">어떻게 살아갈 것인가</title><link href="https://blog.magical.dev/iron-life" rel="alternate" type="text/html" title="어떻게 살아갈 것인가" /><published>2026-02-16T00:00:00+00:00</published><updated>2026-02-16T00:00:00+00:00</updated><id>https://blog.magical.dev/iron-life</id><content type="html" xml:base="https://blog.magical.dev/iron-life"><![CDATA[<h2 id="어떻게-살아갈-것인가">어떻게 살아갈 것인가.</h2>

<blockquote>
  <p>인정한다. 나는 쓸모없다. 당신, 녹슬면 끝이라 했지만. 천번을 두드리는 삶도 세상에는 있는 것이었다.</p>
</blockquote>

<p>정우의 노래를 들으며 저 문장에 나에게 다가왔다. 다가와서는 몇일째 나에게서 빠져나가지 않고있다.</p>

<p>어째서일까?</p>

<h2 id="나의-장점이란">나의 장점이란</h2>

<p>나는 행동을 변화하는게 제일 큰 장점이었다. 뭔가 계가기 있다면 행동을 쉽게 바꿀 수 있고, 조금은 극단적으로 바꿀때도 있지만 그게 결국 더 나은 답을 빠르게 찾는 방법이라고 생각한다.</p>]]></content><author><name>Las aka Wonho</name></author><category term="essay" /><summary type="html"><![CDATA[어떻게 살아갈 것인가.]]></summary></entry><entry><title type="html">팀이 만들어졌기에 새로운 서비스가 필요하다는 주장의 반박</title><link href="https://blog.magical.dev/new-service-for-conway-laws" rel="alternate" type="text/html" title="팀이 만들어졌기에 새로운 서비스가 필요하다는 주장의 반박" /><published>2025-12-28T00:00:00+00:00</published><updated>2025-12-28T00:00:00+00:00</updated><id>https://blog.magical.dev/new-service-for-conway-laws</id><content type="html" xml:base="https://blog.magical.dev/new-service-for-conway-laws"><![CDATA[<h2 id="tldr">TL;DR</h2>

<p>서비스 분리가 복잡도를 낮춘다는 믿음은 잘못되었고, 오히려 시스템 전체 복잡도를 높일 수 있습니다.
따라서 서비스를 분리하는 것이 전체적인 속도를 늦추게 됩니다.
속도를 높이면서 적절히 서비스를 나누는 방법에 대한 아이디어와 복잡도 계산 방법을 제안합니다.</p>

<h2 id="urge-to-write-this-article">urge to write this article</h2>

<p>종종 일을 하다보면 “이 서비스는 왜 이렇게 복잡하게 나뉘어있어?”와 같은 생각을 하게 됩니다. 
MSA 붐 이후에 관련된 서비스들이 여러개의 협력으로 만들어지는 제품은 매우 흔한 패턴이 되었습니다.
흔히 콘웨이 법칙에 따라서 서비스를 설계해야한다는 주장과 함께, 하나의 서비스를 여러 팀에서 관리하는 경우 속도가 느려진다는 불평이 많습니다.
팀이 나눠지면 새로운 서비스를 만들어서 배포를 자유롭게 수행하고, 그린 필드 프로젝트를 시작하고 싶다는 충동이 오죠.
하지만 정말 그게 좋은 방향일까요?</p>

<h2 id="복잡성이-속도를-좌우한다">복잡성이 속도를 좌우한다</h2>

<table>
  <tbody>
    <tr>
      <td>속도가 전부다</td>
    </tr>
  </tbody>
</table>

<p>우리는 언제나 속도를 외칩니다. 불확실한 세상에서 제품을 만들기 위하여 여러 기능을 만들고, 방향을 수정하고, 결국 목표를 향해 나아가는 것.
다들 빠른 속도를 원하고, 엔지니어링은 속도(방향과 속력)을 만족하기 위한 구체적인 방법들을 제시합니다.
더욱 안정적인 제품을 만들고, 빠르게 반응하도록 만들고, 멋지게 만들고, 기능을 추가하더라도 속도가 점점 느려지지 않게 만들죠.</p>

<p>속도는 복잡도와 큰 연관관계가 있습니다. 복잡한 프로그램을 구현하는 것은 간단한 프로그램을 구현하는것과 비교하여 몇배의 시간이 들거에요.
동일한 기능을 하는 프로그램이더라도 더 많은 복잡성은 시간을 더 많이 소요하게 만들어서 우리의 속력을 늦추죠.
그래서 우리는 복잡도를 아주 싫어합니다. 프로젝트를 가능한 최소한의 복잡도만을 가지도록 노력하죠. 끊임없는 리팩터링과 추상화, 관심사의 분리를 통해 얻는 것은 단순함이죠.</p>

<h2 id="팀이-견딜-수-있는-복잡도">팀이 견딜 수 있는 복잡도</h2>

<p>팀이 견딜 수 있는 복잡도를 넘어갈 때 마다 속도는 복잡도에 반비례함.
팀이 견딜 수 있는 복잡도는 아래와 같이 아주 나이브하게 모델링할 수 있음.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>C_capacity = BaseCapacity(TeamSize) × SkillMultiplier × ToolMultiplier

where:
  BaseCapacity(n) = n × (1 - communication_overhead(n))
  SkillMultiplier = weighted_avg(experience × domain_familiarity)
  ToolMultiplier = (1 + tooling_level + abstraction_quality)
</code></pre></div></div>

<p>이 경우 우리 팀의 복잡도 수용량을 계산해보면 좋음. 우리팀은 150임.</p>

<h2 id="서비스와-시스템에-대한-복잡도-모델">서비스와 시스템에 대한 복잡도 모델</h2>

<p>시스템의 복잡도가 중요하다는 것은 모두가 공감할만한 가치이다. 
게다가 대부분의 서비스를 나누려는 사람들도 더 단순하게 시스템을 만들고싶다는 명분이 있다.
그러나 서비스를 나누는 것이 항상 복잡도를 낮추는 것일까?
이러한 질문에 답하기 위하여 서비스의 복잡도와 시스템의 복잡도를 모델링하는 것이 필요하다.</p>

<p>서비스의 복잡도라는 개념은 아주 미묘하고 애매해서 엄밀하게 정의하기 어렵다. 
그러나 정의없이 느낌만으로 서비스의 복잡도를 측정하는 것이 더 좋지 않다. 직관은 강력하지만, 완벽하지 않다.</p>

<p>나는 서비스를 하나의 노드로 보겠다. 그리고 서비스의 fanin, fan-out을 기준으로 복잡도를 생각해보겠다.
아주 나이브한 정의이고, 당연히 다들 생각이 다르다보니 달라지겠다. 나는 Henry-Kafura Flow Metric과 비슷하게 모델링 하였다.</p>

<p>소프트웨어 공학의 고전인 Henry-Kafura 메트릭은 모듈 간의 결합도를 $(FanIn \times FanOut)^2$로 정의한다.
그러나 나는 조금 더 단순화하여서 계산하였다.</p>

<table>
  <tbody>
    <tr>
      <td>$Complexity(S) = N + \lambda \times (FanIn + FanOut)$</td>
    </tr>
  </tbody>
</table>

<p>여기에서 N은 서비스를 유지하는 최소 복잡도이다. 나는 15정도로 생각하는데 이는 공용 라이브러리, 배포 파이프라인, cpu와 같은 리소스 관리, 트래픽 모니터링, 알림, 분산 서버 디버깅등을 포함한 휴리스틱 값이다. 아주 단순한 서비스의 경우 이를 5까지 낮추어도 된다. 서비스의 level에 따라서, SLO에 따라서 다르게 가져가도 좋을 것이다.</p>

<p><em>예시 서비스 S1</em></p>

<p>S1 서비스의 복잡도는 몇일까? <code class="language-plaintext highlighter-rouge">15 + (2 * 5) + (2 * 3)</code>으로 31로 정의할 수 있다. 단일 복잡도 지표로 이정도가 좋은지, 안좋은지 알 수는 없다.</p>

<p><strong>시스템에 대한 복잡도 계산</strong></p>

<p>그러면 N개의 서비스의 복잡도는 어떻게 될까? 아래의 시스템의 전체 복잡도를 어떻게 정의할까?</p>

<table>
  <tbody>
    <tr>
      <td>Complexcity(TargetServices) = Sum(Complexcity(S)) \in TargetServices</td>
    </tr>
  </tbody>
</table>

<p>특정 서비스들의 집합의 복잡도는 각 서비스의 복잡도의 합으로 볼 수 있다. 그럼 그래프의 마지막에 있는 서비스는 어떻게 될까? db나 cache에 접근하는 것도 외부로 본다면 어느정도 비슷하게 모델링할 수 있다.</p>

<p><strong>Fallacies of Distributed Computing</strong></p>

<p>분산 시스템에서 api 호출은 되게 많은 문제들을 야기하는데, 이는 내부 호출보다 더 케이스가 많아진다.</p>

<p>함수 호출은 메모리 내에서 나노초(ns) 단위지만, 네트워크 호출은 밀리초(ms) 단위다. 실패 확률도 0%에서 0.1% 이상으로 뛴다. 내부 호출을 외부 호출로 바꾸는 것은, 상수 시간($O(1)$) 작업을 확률적 실패($P(fail)$) 작업으로 바꾸는 행위다</p>

<p>모든 엣지(Edge)가 평등하지 않다. 조회(Read)는 캐싱하면 그만이지만, <strong>상태 변경(Write)</strong>이 포함된 연결은 분산 트랜잭션을 요구하며 복잡도를 제곱으로 증가시킨다. 따라서 위 수식에서 Write API가 포함된 경우 가중치를 훨씬 높게 잡아야 한다.</p>

<h2 id="좋은-추상화는-서비스의-복잡도를-줄입니다">좋은 추상화는 서비스의 복잡도를 줄입니다</h2>

<p>우리는 위에서 서비스, 그리고 시스템의 복잡도 모델을 정의하고 계산해보았다.
위 모델이 우리의 직관을 어느정도 만족하는지 알아보기 위해 추상화가 잘 되어서 서비스가 잘 나누어지는 경우를 살펴보자.</p>

<h2 id="서비스가-잘-나누어지지-않고-마구-만들어지는-경우">서비스가 잘 나누어지지 않고, 마구 만들어지는 경우</h2>

<p>너무 마구 만들어지면 기본 오버헤드가 지배함.</p>

<p>Universal Scalability Law</p>

<h2 id="일단-나누고-합치면-된다는-주장의-반박">일단 나누고 합치면 된다는 주장의 반박</h2>

<p>merge함수에 대한 정의</p>

<p>merge가 너무 커지는 경우에 대한 실제 예시</p>

<p>merge함수에 대한 특성</p>

<h2 id="콘웨이-법칙의-반란-다시-보는-맨먼스-미신">콘웨이 법칙의 반란, 다시 보는 맨먼스 미신</h2>

<p>콘웨이 법칙에 따라서 시스템을 설계해야한다는 주장에 대한 반론</p>

<p>맨먼스 미신을 인용하면서 커뮤니케이션이 지수적으로 커지는 문제점 지적</p>

<p>콘웨이 법칙은 관찰 결과일 뿐이지, 꼭 그래야한다는 것은 아님</p>

<p>인지 부하가 줄어드는 관점이라고 하지만 실제로는 복잡도가 폭발함</p>

<p>조직 구조는 그렇게 쉽게 바뀌지 않고, 정말 독립적이지도 않음</p>

<h2 id="msa의-이점이-있는-경우">MSA의 이점이 있는 경우</h2>

<p>배포가 병목이 아닌 경우가 많음(리틀의 법칙)</p>

<p>정말 피처때문에 같이 영향을 받는다면 그걸 다른 서비스로 쪼갠다고 온전히 없어지지 않을 수 있음</p>

<p>적절한 전략으로 회피 가능</p>

<p>같은 코드 베이스에서 협업을 잘 하기 위한 인터페이스 정의 방법, arch unit, code owner
데이터베이스 스키마 분리도 가능</p>

<p>자원 격리가 있어야한다면 가능</p>

<h2 id="그러면-어디까지-나누어야할까">그러면 어디까지 나누어야할까?</h2>

<p>복잡도가 줄어드는 방향으로 가야함</p>

<p>U자형 곡선에 대한 최적화 이론</p>

<p>배포가 병목이면 그럴 수 있음</p>

<h2 id="왜-이러한-실수를-반복할까">왜 이러한 실수를 반복할까?</h2>

<ol>
  <li>업적의 가시성</li>
  <li>책임 회피</li>
  <li>팀 자율성에 대한 욕구</li>
</ol>]]></content><author><name>Las aka Wonho</name></author><category term="essay" /><summary type="html"><![CDATA[TL;DR]]></summary></entry><entry><title type="html">That’s not strong consistency cache architecture</title><link href="https://blog.magical.dev/tossbank-strong-cache" rel="alternate" type="text/html" title="That’s not strong consistency cache architecture" /><published>2025-11-22T00:00:00+00:00</published><updated>2025-11-22T00:00:00+00:00</updated><id>https://blog.magical.dev/tossbank-strong-cache</id><content type="html" xml:base="https://blog.magical.dev/tossbank-strong-cache"><![CDATA[<h2 id="urge-to-write-this-article">urge to write this article</h2>

<p>회사에서 이야기를 나누다가 어떤 아티클을 공유받았습니다. 토스뱅크에서 작성한 캐시에 대한 글이었고,
꽤나 어려워보이는 개념들을 이야기하면서 멋진 시스템 개선 방식을 소개하고있어서 즐겁게 읽었습니다. 
한편으로는 분산 시스템에서의 strong consistency를 적용하는 방식이 꽤나 특이하다는 느낌도 받았습니다. 
읽다보니 점점 엄밀한 잣대로 글을 읽게 되더군요. 제 안의 분산시스템 악마가 깨어나고 있었습니다.
이렇게 간단히 성능 저하 없이 문제를 잘 해결하는게 불가능하다는 생각이 들면서 몇가지 반례를 찾아내었습니다.
블로그 길이의 제약으로 인해 모든걸 설명하지 못한것을 비판하는건 단순히 트집잡기에 불과하여 글을 약 반년간 방치했는데요.
이번에 저도 분산 시스템의 안정성을 검증해야하는 일이 생겨서 좋은 훈련으로 사용해보았습니다.</p>

<blockquote>
  <p>멋진 캐시 적용 아티클을 비난하고싶은 마음은 없습니다. 토스 뱅크는 매우 거대한 시스템을 안정적으로 운영하는 최고의 기술력을 가진 은행입니다. 
토스 뱅크의 글을 읽고 추측으로 작성된 글이며, 실제로 내부에서 Lock이나 다른 매커니즘을 추가적으로 사용할 가능성이 높습니다.</p>
</blockquote>

<h2 id="아티클을-요약하자면">아티클을 요약하자면</h2>

<p><strong><a href="https://toss.tech/article/34481">이 아티클</a>을 먼저 읽고, 저의 블로그를 읽으시길 강력히 권장합니다.</strong></p>

<p>약관 데이터 서빙을 leader DB(아마 RDBMS겠죠?)에서만 수행하고 있었습니다. 이는 ‘강한 일관성;’이라고 하는 속성을 지키기 위함이라고 합니다.
요청이 점점 늘어나감에 따라서 단일 DB 인스턴스로는 성능 한계에 도달하게 됩니다.
따라서 빠르게 읽기를 할 수 있는 캐시 레이어를 도입합니다. 이때 redis를 캐시 저장소로 선택하고,
write시에 cache evict, read시에 cache를 채우는 방식으로 구현을 합니다.
일관성이 매우 중요하기 때문에 Kafka 이벤트 발행 순서도 조절하고, redis에 대하여 서킷도 달았습니다.</p>

<p>write시에 cache evict하는 타이밍은 db에 commit이 된 다음 application server에서 redis unlink를 수행하고, 성공적으로 redis unlink가 수행되면 API의 응답을 주게 됩니다.</p>

<p><img src="https://static.toss.im/ipd-tcs/toss_core/live/cb4390cc-0d43-4d98-998f-ff21a1d7102d/image.png" alt="토스뱅크 캐시 이미지" /></p>

<h2 id="일관성-모델">일관성 모델</h2>

<p>원 블로그에서는 처음에 강한 일관성(Strong Consistency, 혹은 Linearizability)을 의도하면서 구현을 이어나가다가 후반부에는 비교적 약한 일관성을 보장하기로 결정합니다.</p>

<table>
  <tbody>
    <tr>
      <td>“약관 동의 또는 철회 요청 API 처리가 완료된 순간, 바로 다음 요청에 DB에 저장된 값이 응답되어야 한다”</td>
    </tr>
  </tbody>
</table>

<p>이게 우리의 요구사항입니다. API의 처리가 완료되었다면 그 다음 요청에서 DB의 값이 응답된다. 정확히 어떠한 속성인지 해석의 여지가 있으니 조금 더 너그러운 일관성 모델로 생각해보겠습니다. 바로 Read Your Writes입니다.</p>

<p><strong>Read-Your-Writes</strong>: 사용자가 성공적으로 쓰기를 완료한 후, 그 사용자의 다음 읽기 요청은 반드시 그 쓰기 내용을 포함한 최신 값을 반환해야 합니다.</p>

<p>RYW(Read Your Writes)의 경우에 세션의 일관성 모델인데, 세션의 인과관계가 역전되는 일을 방지합니다. 물론 동일 세션이 아닌 다른 세션을 기준으로도 역전이 일어나게 됩니다.</p>

<p>Monotonic Read(No-Flickering)과 같은 속성은 지금은 무시하도록합니다.</p>

<h2 id="반례를-발견하는-것의-어려움">반례를 발견하는 것의 어려움</h2>

<p>분산 시스템의 안전성, 혹은 특정 속성을 보장하지 않는 다는 것은 비교적 쉽게 찾을 수 있습니다. 단 한가지 반례를 찾으면 우리의 모델이 틀렸다는 것을 검증할 수 있습니다. 하지만 반례가 없다는 것을 증명하는 것은 조금 더 까다롭습니다. 시나리오는 조건의 갯수에 비례하여 지수적으로 증가합니다. 동시에 실행되는 프로세스가 2개로 늘어나면 상황은 4가지가 됩니다. 프로세스가 3개가 되면 더욱 많이 늘어나겠지요. 네트워크 순서는 또 어떻게 될까요? 끝없이 실행되는 프로그램이라면 어떨까요? 아주 우연히 특정 순서대로 프로그램이 수행된다면, 우리는 그 상황을 검토할 수 있을까요? 아마 불가능할겁니다. 개발자의 두뇌는 그렇게 동작하지 않습니다.</p>

<p>그렇기에 반례를 찾는 상황은 온전히 개발자의 상상력과, 경험에 의존합니다. 이건 문제가 있을 것 같다는 직관, 비슷한 문제를 풀어보았던 경험이 많이 작용하죠.
호기심이 많은 개발자가 이 상황, 저 상황 탐색하면서 문제를 찾을 수도 있습니다. 또, 분산 시스템에 익숙한 사람이라면 특정 정합성을 만족하기 위한 최소 조건을 증명하고, 이 시스템이 충분조건을 만족하고 있다는 것을 증명할 수도 있습니다.</p>

<p>저는 이런 시스템에 관심이 많기에 글을 읽자마다 반례가 바로 떠올랐습니다. 또, 분산 시스템에 익숙한 사람이라면 특정 정합성을 만족하기 위한 필요조건 or 최소 조건이 무엇인지 감으로 알고 있기에, 블로그에 기술된 내용만으로는 시스템이 조건을 충족하지 못한다는 것을 빠르게 파악할 수 있습니다. 물론 아주 익숙한 사람이더라도 시간을 들여 천천히 생각하지 않으면 문제를 놓칠 수 있습니다.</p>

<h2 id="tla-spec으로-모델-정의하여-검증하기">TLA+ Spec으로 모델 정의하여 검증하기</h2>

<p>저는 이번 검증에서 TLA+를 사용하겠습니다. 언급했다시피 단순한 반례 몇가지를 찾는 것은 매우 쉽지만, 개개인의 상상력에 의존합니다.
개발자 한명이 생각할 수 있는 시나리오는 고작해야 한번에 20가지가 전부이고, 수천만건이 있는 상황을 빠짐없이 시뮬레이션하는 것은 불가능하죠. 이는 컴퓨터가 잘 하는 것입니다. 
어떤 컴포넌트의 문제가 있다는 것을 잘 드러낼 수 있는 방법인 Formal Method를 사용하여 모델링을 시작해보겠습니다.</p>

<p>아주 단순한 user -&gt; server -&gt; db &amp; redis 모델입니다. 각 컴포넌트 간의 통신은 network로 통신하며 순서가 바뀔 수 있습니다. 물론 tcp 기반으로 생각하고있기에 커넥션 단위의 메시지 순서가 바뀌지 않습니다. 네트워크는 유실도 일어나지 않습니다. 현실세계에 비하면 매우 너그러운 조건이죠.</p>

<p>단순한 쓰기 &amp; 읽기 시나리오입니다.</p>

<p><img src="https://github.com/MagicalLas/MagicalLas.github.io/blob/master/_screenshots/read-seq.svg?raw=true" alt="읽기 시나리오" />
<img src="https://github.com/MagicalLas/MagicalLas.github.io/blob/master/_screenshots/write-seq.svg?raw=true" alt="쓰기 시나리오" /></p>

<p>의사 코드는 아래와 같습니다.</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">read</span><span class="p">(</span><span class="n">userID</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">string</span> <span class="p">{</span>
    <span class="n">result</span> <span class="o">:=</span> <span class="n">redis</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">userID</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">result</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">result</span>
    <span class="p">}</span>

    <span class="n">dbResult</span> <span class="o">:=</span> <span class="n">db</span><span class="o">.</span><span class="n">findByID</span><span class="p">(</span><span class="n">userID</span><span class="p">)</span>
    <span class="n">redis</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="n">userID</span><span class="p">,</span> <span class="n">dbResult</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">dbResult</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">write</span><span class="p">(</span><span class="n">userID</span><span class="p">,</span> <span class="n">newValue</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">string</span> <span class="p">{</span>
    <span class="n">db</span><span class="o">.</span><span class="n">openTransaction</span><span class="p">()</span>
    <span class="n">db</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="n">userID</span><span class="p">,</span> <span class="n">newValue</span><span class="p">)</span>
    <span class="n">db</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>

    <span class="n">redis</span><span class="o">.</span><span class="n">unlink</span><span class="p">(</span><span class="n">userID</span><span class="p">)</span> <span class="c">// wait for unlink done</span>
    <span class="k">return</span> <span class="n">newValue</span>
<span class="p">}</span>
</code></pre></div></div>

<p>꽤나 간단하죠? Golang으로 작성되었지만 kotlin, spring도 비슷하게 동작할겁니다.</p>

<p>대부분의 어플리케이션에서는 유저가 한번에 단 하나의 호출만 하는 경우는 드뭅니다. 속도와 성능을 위하여 여러 호출이 동시에 진행되죠.
또한 서버가 여러개 존재하는 MSA로 구성되어있다면 요청 하나가 동일한 TermsServer를 여러번 호출하는 경우도 빈번할 것입니다.
내부적으로 요청이 퍼지면서 거의 동일한 시점에 여러 요청이 동시에 처리될 수 있죠.</p>

<p><img src="https://github.com/MagicalLas/MagicalLas.github.io/blob/master/_screenshots/req-spread.svg?raw=true" alt="요청 스프레드" /></p>

<p>강한 일관성이라면 이러한 문제에서 자유로워야합니다. 하지만 우리는 그정도로 원하지 않으니 단 하나의 속성만 체크하겠습니다.
동일한 유저의 요청 스레드라면 자신이 write에 성공한 이후에 새로 시작하는 read에서는 write가 반영되어야한다는 것입니다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ReadYourWriteConsistency ==
    \A u \in Users:
        clientView[u] # NoneVal =&gt;
            clientView[u].version &gt;= lastWriteVersion[u]
</code></pre></div></div>

<p>TLA+로 위처럼 모델링할 수 있습니다. client는 마지막에 write가 성공한 값이 그 다음 read의 결과로 나와야 한다는 것입니다. 참고로 client는 요청 하나씩만 처리할 수 있는 유저의 커넥션을 의미합니다. 이전 요청이 끝나지 않으면 다음 요청이 시작될 수 없습니다.</p>

<p>여기서 Users는 사실 “세션(session)”을 의미합니다. 하나의 실제 유저는 여러 세션(웹/앱, 여러 탭 등)을 가질 수 있지만, 이 글에서 정의하는 Read-Your-Writes는 “각 세션 기준 RYW”입니다.</p>

<h2 id="반례">반례</h2>

<p><img src="https://github.com/MagicalLas/MagicalLas.github.io/blob/master/_screenshots/1-violence.svg?raw=true" alt="반례 시나리오 1" /></p>

<p>이렇게 단순한 모델, 그리고 네트워크 지연만 있는 경우에도 문제는 발생합니다. 문제가 되는 시퀀스를 하나 보여드리겠습니다. Set이 지연해서 도착하면서 unlink가 되었지만 기존값으로 캐시가 채워진 모습입니다. 단일 요청 흐름에서 보더라도 동시에 2가지 요청이 수행되는 경우 실제로 write가 성공했지만 이전값이 나온다는 점입니다. 자신의 쓰기가 무시된 상태죠.</p>

<p>이 시나리오에서 토스뱅크가 주장하는 “약관 동의 여부는 약관 동의 또는 철회 요청 API 처리가 완료된 순간, 바로 다음 요청에 DB에 저장된 값이 응답되어야 한다.”에 정면으로 위배되는 케이스이죠.  Write 요청이 다시 발생하거나, TTL이 지나지 않는다면 계속해서 이전값을 보게 됩니다. 심지어 Write 요청이 DB에 commit되었고, 성공으로 응답이 내려온 상태죠. DB에 저장된 값이 아닌, 자신이 쓴 값도 내려오지 않는겁니다.</p>

<p>네트워크 지연을 가정하는게 합리적이지 않을 수도 있습니다. 아주 적을 확률일 수 있습니다. 하지만 실제로 발생할 수 있는 경우입니다. network rtt, gc 지연등을 짧게 잡더라도 분산시스템간의 atomic하지 않은 순간에 대하여 끼어들 수 있는 상황은 매일 생깁니다. 아주 조금의 순간이더라도 요청마다 계속해서 수행되다보면 언젠가 발생하는 경우도 있을겁니다.</p>

<p>발생하지 않게 만드려면 지금의 구조로는 부족합니다. 추가적인 매커니즘이 필요해요.</p>

<h3 id="서킷브레이커는-도움이-되지-않는다">서킷브레이커는 도움이 되지 않는다</h3>

<p>그리고 그 메커니즘은 분명히 서킷브레이커가 아닙니다. 저는 서킷브레이커를 아예 모델에 넣지도 않았는데요, 이는 redis 호출이 항상 성공하기 때문입니다. 위의 반례의 경우 unlink가 항상 성공하고있습니다.
서킷 브레이커가 열리지 않고도 정합성 문제가 몇분이상 지속될 수 있다는걸 볼 수 있죠. 애초에 서킷브레이커는 문제 발생시의 차단 역할입니다. 근본적인 시스템의 일관성 위반을 막아주지 않습니다.</p>

<p>물론 서킷이 있다는 것은 실용적인 접근이고, 저는 서킷을 좋아합니다. 장애 차단 관점에서 얼마나 중요한지 잘 알고있습니다 ;)</p>

<h3 id="unlink를-db-transaction안에서-수행하는건-도움이-되지-않는다">Unlink를 DB Transaction안에서 수행하는건 도움이 되지 않는다</h3>

<p>몇가지 변형을 가하면, 단순한 수정으로 이런 일관성 속성을 개선할 수 있을까요? 지금은 AFTER_COMMIT으로 DB에 커밋한 다음 redis에서 unlink를 수행합니다. 만약 Transaction commit 전에 unlink를 한다면 어떻게 될까요?</p>

<p>여전히 동일한 문제는 남습니다. 아래 시나리오로 확인할 수 있습니다.</p>

<p><img src="https://github.com/MagicalLas/MagicalLas.github.io/blob/master/_screenshots/2-violence.svg?raw=true" alt="반례 시나리오 2" /></p>

<p><strong>(1)DB Commit -&gt; Unlink와 (2)Read Path에서의 DB Read-&gt;Set간의 순서 불일치</strong>가 현상의 원인입니다. 1번과 2번이 각각 Atomic하지 않기에 DB에서의 순서는 존재하지만 Redis에서의 순서는 DB의 순서와 달라지기 때문입니다.</p>

<p>혹시 너무나 마법적인 일이 일어나서 DB에 commit하는 순간 redis도 unlink가 일어난다면 어떻게 될까요? 동일하게 TLA+로 모델링해보겠습니다.</p>

<p><img src="https://github.com/MagicalLas/MagicalLas.github.io/blob/master/_screenshots/3-violence.svg?raw=true" alt="반례 시나리오 3" /></p>

<p>그래도 여전히 동일한 문제가 남습니다. 이는 순서가 바뀌었다는걸 모르기 때문입니다. 1번이 Atomic하더라도 2번이 Atomic하지 않으면, 혹은 2번의 순서가 바뀌어서 수행될 수 있다면 문제가 존재합니다.</p>

<h3 id="redis-set-nx를-사용해도-도움이-되지-않는다">redis SET NX를 사용해도 도움이 되지 않는다</h3>

<p>혹시 Read시에 보통 SET NX를 사용하면 값이 이미 있는 경우 무시하는 속성을 사용해서 해결할 수 있을까요? 종종 SET NX를 사용하는게 좋다는 이야기를 어디선가 들었던 기억도 납니다. 하지만 실제로는 도움이 되지 않습니다. 이전 시나리오만 보더라도 SET NX로 바뀐들 해결되지 않습니다. 위에서 나온 반례 시나리오 1, 2, 3을 참고하세요. <strong>결국 DB의 순서와 redis의 순서가 맞춰져야합니다.</strong></p>

<h3 id="write에서도-cache를-채우도록-해도-도움이-되지-않는다">Write에서도 Cache를 채우도록 해도 도움이 되지 않는다</h3>

<p>Read할 때 캐시를 채우는 경우 여러가지 방법을 넣어도 크게 도움이 되지 않네요. 계속해서 읽을 때 캐시를 이전값으로 채우는게 문제라면, 쓰기시점에 제일 최신값으로 업데이트할 수 있을겁니다. Read가 아니라 Write에서 SET을 수행하는겁니다. 이제 조금 더 근본적인 시스템의 재설계가 수행됩니다.</p>

<p>Write시점에 Redis에 값을 넣는 타이밍을 정해야합니다. 이건 단 한가지 방법밖에 없습니다. DB에 commit이 되지 않은 데이터를 set을 할 수는 없으니 commit된 이후에 SET을 수행하도록합시다. commit되지 않은 값을 set해서 최신값이 미리 보이는 것이 RYW 일관성에서는 문제는 아닐 수 있습니다. 하지만 모종의 이유로 Transaction Commit이 실패한다면 DB와 redis의 상태가 깨지게 됩니다. redis는 잘못된 값이 저장되고, 순간의 상황에서 그 잘못된 값이 서빙되겠죠.</p>

<p>Read시점에 Redis에 값을 채우는 매커니즘도 단 한가지만 존재합니다. 바로 SET NX이죠. Write시점에 채운 캐시를 Read시점에 더 이전값으로 채울 수 있기 때문이죠. 따라서 이번 케이스에서는 Read에서 SET NX를 사용하면서 Write Path에서 SET NX를 사용하지 않는 시나리오를 검증하겠습니다.</p>

<p>의사 코드는 아래와 같습니다.</p>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">read</span><span class="p">(</span><span class="n">userID</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">string</span> <span class="p">{</span>
    <span class="n">result</span> <span class="o">:=</span> <span class="n">redis</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">userID</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">result</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">result</span>
    <span class="p">}</span>

    <span class="n">dbResult</span> <span class="o">:=</span> <span class="n">db</span><span class="o">.</span><span class="n">findByID</span><span class="p">(</span><span class="n">userID</span><span class="p">)</span>
    <span class="n">redis</span><span class="o">.</span><span class="n">setNx</span><span class="p">(</span><span class="n">userID</span><span class="p">,</span> <span class="n">dbResult</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">dbResult</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">write</span><span class="p">(</span><span class="n">userID</span><span class="p">,</span> <span class="n">newValue</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">string</span> <span class="p">{</span>
    <span class="n">db</span><span class="o">.</span><span class="n">openTransaction</span><span class="p">()</span>
    <span class="n">db</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="n">userID</span><span class="p">,</span> <span class="n">newValue</span><span class="p">)</span>
    <span class="n">db</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>

    <span class="n">redis</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="n">userID</span><span class="p">,</span> <span class="n">newValue</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">newValue</span>
<span class="p">}</span>
</code></pre></div></div>

<table>
  <tbody>
    <tr>
      <td>혹시 글을 읽은 과정에서 순간적으로 “이것도 안되겠네. 이런 경우도 있잖아”하면서 구체적인 시나리오가 생각나셨나요?</td>
    </tr>
  </tbody>
</table>

<p>그러나 여전히 동일한 문제가 존재합니다. 아래 시나리오를 한번 보시죠.</p>

<p><img src="https://github.com/MagicalLas/MagicalLas.github.io/blob/master/_screenshots/4-violence.svg?raw=true" alt="반례 시나리오 4" /></p>

<p>직전 시나리오를 보니까 Write가 동시에 처리되는 경우 덮어씌우는게 문제로 보입니다. SET NX로 위 케이스는 해소할 수 있을 것 같습니다. Write시에도 SET NX를 사용하면 어떻게 될까요?</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">write</span><span class="p">(</span><span class="n">userID</span><span class="p">,</span> <span class="n">newValue</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">string</span> <span class="p">{</span>
    <span class="n">db</span><span class="o">.</span><span class="n">openTransaction</span><span class="p">()</span>
    <span class="n">db</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="n">userID</span><span class="p">,</span> <span class="n">newValue</span><span class="p">)</span>
    <span class="n">db</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>

    <span class="n">redis</span><span class="o">.</span><span class="n">setNx</span><span class="p">(</span><span class="n">userID</span><span class="p">,</span> <span class="n">newValue</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">newValue</span>
<span class="p">}</span>
</code></pre></div></div>

<p><img src="https://github.com/MagicalLas/MagicalLas.github.io/blob/master/_screenshots/5-violence.svg?raw=true" alt="반례 시나리오 5" /></p>

<p>결국 Write요청이 동시에 오게된다면 DB가 Serializable하더라도 Redis에 저장되는 값은 DB와 다른 순서를 가지게 됩니다.</p>

<h3 id="write에서만-cache를-채우도록-해도-도움이-되지-않는다">Write에서만 Cache를 채우도록 해도 도움이 되지 않는다</h3>

<p>Read에서 채우지않고, Write시에만 채우게하면 문제가 없을까요?</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">read</span><span class="p">(</span><span class="n">userID</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">string</span> <span class="p">{</span>
    <span class="n">result</span> <span class="o">:=</span> <span class="n">redis</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">userID</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">result</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">result</span>
    <span class="p">}</span>

    <span class="n">dbResult</span> <span class="o">:=</span> <span class="n">db</span><span class="o">.</span><span class="n">findByID</span><span class="p">(</span><span class="n">userID</span><span class="p">)</span>
    <span class="c">// redis.setNx(userID, dbResult) cache를 채우지 않는다</span>

    <span class="k">return</span> <span class="n">dbResult</span>
<span class="p">}</span>
</code></pre></div></div>

<p>아마 여러분들이라면 1초만에 답하실 수 있을겁니다. 직전 케이스들 모두 Write 충돌로 인한 문제였습니다. Read는 관여하지 않았죠. <strong>핵심은 여전히 DB의 순서와 Redis의 순서의 불일치입니다.</strong></p>

<p>지금까지 몇가지 생각나는, 흔히 시도하는 방법들에 대하여 시도해보았지만 문제가 사라지지 않았습니다. 이제는 그만 탐색하고 해결책을 이야기해보겠습니다.</p>

<h2 id="ryw-일관성을-만족시키는-방법">RYW 일관성을 만족시키는 방법</h2>

<p>문제는 DB Commit과 Redis SET이 원자적이지 않다는 것입니다. 이를 원자적으로 만들면 문제가 해결됩니다.</p>

<h3 id="locks-will-save-us">Locks will save us</h3>

<p>Lock은 분산 시스템에서 아주 강력한 도구입니다. Lock이 우리를 구해주는 것은 자명합니다. 강력한만큼 부작용도 있습니다. 꽤나 오버헤드가 크다는 문제가 존재하죠.</p>

<p>최소한으로 Lock을 잡기 위하여 쓰기시점에만 Lock을 잡아서 일관성을 맞출 수 있습니다. 약관의 경우 Read Heavy한 패턴이기에 합리적인 방법입니다. 이 방법의 경우 Read에서 SET NX를 쓰고, Write시에 Lock으로 DB와 Redis의 순서를 엄밀히 보장하면 “Read Your Writes”는 보장합니다.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">read</span><span class="p">(</span><span class="n">userID</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">string</span> <span class="p">{</span>
    <span class="n">result</span> <span class="o">:=</span> <span class="n">redis</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">userID</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">result</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">result</span>
    <span class="p">}</span>

    <span class="n">dbResult</span> <span class="o">:=</span> <span class="n">db</span><span class="o">.</span><span class="n">findByID</span><span class="p">(</span><span class="n">userID</span><span class="p">)</span>
    <span class="n">redis</span><span class="o">.</span><span class="n">setNx</span><span class="p">(</span><span class="n">userID</span><span class="p">,</span> <span class="n">dbResult</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">dbResult</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">write</span><span class="p">(</span><span class="n">userID</span><span class="p">,</span> <span class="n">newValue</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">string</span> <span class="p">{</span>
    <span class="n">l</span> <span class="o">:=</span> <span class="n">distLock</span><span class="o">.</span><span class="n">lock</span><span class="p">(</span><span class="n">userID</span><span class="p">)</span>
    <span class="k">defer</span> <span class="n">distLock</span><span class="o">.</span><span class="n">unlock</span><span class="p">(</span><span class="n">l</span><span class="p">)</span> <span class="c">// 이 함수의 결과가 반환될때 release</span>

    <span class="n">db</span><span class="o">.</span><span class="n">openTransaction</span><span class="p">()</span>
    <span class="n">db</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="n">userID</span><span class="p">,</span> <span class="n">newValue</span><span class="p">)</span>
    <span class="n">db</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>

    <span class="n">redis</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="n">userID</span><span class="p">,</span> <span class="n">newValue</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">newValue</span>
<span class="p">}</span>
</code></pre></div></div>

<p>TLA+로 검증한 결과, 160만가지의 상황을 시뮬레이션하였고 위반하는 경우가 없다는 것을 확인하였습니다. read시점에 이전값이 저장될 수 있지만, write시점에 항상 최신값으로 순서대로 반영되기에 정합성 문제에서 승리하게 되었습니다.</p>

<p>Write에만 Lock을 잡는 것은 Monotonic Read를 만족할 수는 없지만, 중요한 속성은 아니기에 후술하겠습니다.</p>

<h3 id="or-just-use-versioned-conditional-set-mechanism">Or just use Versioned Conditional Set Mechanism</h3>

<p>Lock만 우리를 구원해줄까요? 다른 방법은 없을까요? 우리에게는 다른 방법도 존재합니다. 바로 Versioned Conditional Set(혹은 Last-Write-Wins with Version, LWW-V)라고 불리는 방법이죠. 조금 더 약한 보장만 필요한 경우에 자주 사용되는 방법이죠. DB &lt;-&gt; Redis의 순서를 완전히 동일하게 맞추는 것이 아닌 <strong>각 컴포넌트의 시간의 흐름이 거꾸로 가는 것을 막는</strong> 방법입니다. 각 값에 version을 부여하고, Redis는 오직 더 높은 version의 값만 받아들입니다. 이는 MVCC(Multi-Version Concurrency Control)의 단순화된 형태로도 볼 수 있습니다.</p>

<p>보통의 경우 redis에서 아래와 같은 lua/functions를 이용하여 값의 version을 확인해서 이전 버전에 대한 SET (NX)를 무시하는 것입니다.</p>

<div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- KEYS[1] = dataKey</span>
<span class="c1">-- KEYS[2] = versionKey</span>
<span class="c1">-- ARGV[1] = newVersion (number)</span>
<span class="c1">-- ARGV[2] = newData (string)</span>

<span class="kd">local</span> <span class="n">dataKey</span> <span class="o">=</span> <span class="n">KEYS</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
<span class="kd">local</span> <span class="n">verKey</span> <span class="o">=</span> <span class="n">KEYS</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span>

<span class="kd">local</span> <span class="n">newVersion</span> <span class="o">=</span> <span class="nb">tonumber</span><span class="p">(</span><span class="n">ARGV</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span>
<span class="kd">local</span> <span class="n">newData</span> <span class="o">=</span> <span class="n">ARGV</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span>

<span class="c1">-- 현재 버전 가져오기 (없으면 0 버전으로 처리)</span>
<span class="kd">local</span> <span class="n">curVersionStr</span> <span class="o">=</span> <span class="n">redis</span><span class="p">.</span><span class="n">call</span><span class="p">(</span><span class="s2">"GET"</span><span class="p">,</span> <span class="n">verKey</span><span class="p">)</span>
<span class="kd">local</span> <span class="n">curVersion</span> <span class="o">=</span> <span class="mi">0</span>
<span class="k">if</span> <span class="n">curVersionStr</span> <span class="k">then</span>
    <span class="n">curVersion</span> <span class="o">=</span> <span class="nb">tonumber</span><span class="p">(</span><span class="n">curVersionStr</span><span class="p">)</span>
<span class="k">end</span>

<span class="c1">-- version 비교</span>
<span class="k">if</span> <span class="n">newVersion</span> <span class="o">&gt;</span> <span class="n">curVersion</span> <span class="k">then</span>
    <span class="c1">-- set version</span>
    <span class="n">redis</span><span class="p">.</span><span class="n">call</span><span class="p">(</span><span class="s2">"SET"</span><span class="p">,</span> <span class="n">verKey</span><span class="p">,</span> <span class="n">newVersion</span><span class="p">)</span>
    <span class="c1">-- set data</span>
    <span class="n">redis</span><span class="p">.</span><span class="n">call</span><span class="p">(</span><span class="s2">"SET"</span><span class="p">,</span> <span class="n">dataKey</span><span class="p">,</span> <span class="n">newData</span><span class="p">)</span>
    <span class="k">return</span> <span class="mi">1</span>
<span class="k">else</span>
    <span class="k">return</span> <span class="mi">0</span>
<span class="k">end</span>
</code></pre></div></div>

<p>우리의 첫 모델에서 Read시에 캐시를 채우는 방식만 바꿔도보록 합시다. 의사코드는 아래와 같습니다.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">read</span><span class="p">(</span><span class="n">userID</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">string</span> <span class="p">{</span>
    <span class="n">result</span> <span class="o">:=</span> <span class="n">redis</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">userID</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">result</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">result</span>
    <span class="p">}</span>

    <span class="n">dbResult</span> <span class="o">:=</span> <span class="n">db</span><span class="o">.</span><span class="n">findByID</span><span class="p">(</span><span class="n">userID</span><span class="p">)</span>
    <span class="n">redis</span><span class="o">.</span><span class="n">VCS</span><span class="p">(</span><span class="n">userID</span><span class="p">,</span> <span class="n">dbResult</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">dbResult</span>
<span class="p">}</span>
</code></pre></div></div>

<p>이 경우에도 예외 시나리오가 발생합니다. 읽기와 쓰기가 동시에 처리되는 경우 redis에 데이터가 없어서 채우는 순서가 read를 나중에 처리한다면 unlink가 무시되는 일이 발생합니다.</p>

<p><img src="https://github.com/MagicalLas/MagicalLas.github.io/blob/master/_screenshots/10-violence.svg?raw=true" alt="반례 시나리오 10" /></p>

<h3 id="write시에-cache를-채우면서-vsc를-써야한다">Write시에 Cache를 채우면서 VSC를 써야한다</h3>

<p>그럼 Write시에 VCS를 사용해서 캐시를 채우면 어떨까요?</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">write</span><span class="p">(</span><span class="n">userID</span><span class="p">,</span> <span class="n">newValue</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">string</span> <span class="p">{</span>
    <span class="n">db</span><span class="o">.</span><span class="n">openTransaction</span><span class="p">()</span>
    <span class="n">db</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="n">userID</span><span class="p">,</span> <span class="n">newValue</span><span class="p">)</span>
    <span class="n">db</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>

    <span class="n">redis</span><span class="o">.</span><span class="n">VCS</span><span class="p">(</span><span class="n">userID</span><span class="p">,</span> <span class="n">newValue</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">newValue</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Write시점에 VCS를 사용하면 일관성이 깨지지 않고 항상 보장하게 됩니다..</strong></p>

<p>VCS가 RYW를 만족시키는 이유는 Redis의 버전이 단조 증가하기 때문입니다. 
Write 시점에 Redis의 logical clock을 해당 버전까지 끌어올리고, VCS는 이 clock이 후퇴하지 않음을 보장합니다.
따라서 Write 완료 후의 모든 Read는 최소한 그 버전 이상을 보게 됩니다.</p>

<h3 id="but-redis-have-a-ttl-and-its-more-complicated">But redis have a TTL, and it’s more complicated</h3>

<p>보통의 경우 redis는 모든 데이터를 memory에 올려서 동작합니다. 메모리는 비교적 희소한 자원이기에 DB의 모든 데이터를 항상 메모리에 올려두고있는 것은 바람직하지 않습니다.
또한 redis의 역할을 캐시 레이어이기 때문에 자주 조회되는 데이터에 대하여 저장하면 되죠. 따라서 TTL을 걸어두고 사용하게 됩니다.
특정 시간이 지나면 그 데이터는 메모리에서 내려가고 전체 데이터중에 일부만 저장하고 잇게 됩니다. 캐시는 HitRate이 매우 중요한데 적절하게 TTL이나 캐시 만료 정책을 세워서 운영해야합니다. 오늘은 캐시 Expire 전략을 이야기하는 시간은 아니기에 이정도로 줄입니다.</p>

<p>중요한 것은 redis에 저장된 데이터는 항상 남아있는게 아니라 종종 사라진다는 것입니다. 실제로 발생하는 케이스인데도 우리의 모델에는 그게 없죠.
TTL대신에 redis에 있는 데이터 일부가 random하게 사라지는 경우를 모델링하겠습니다. 매우 공격적인 상황이라고 볼 수 있습니다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>RedisTTLExpired ==
    /\ redisValue' = NoneVal
    /\ UNCHANGED &lt;&lt; dbValue, serverState, clientView, clientState, clientRequestType, lastWriteVersion, msgs &gt;&gt;

Next ==
    ...
    \/ RedisTTLExpired
</code></pre></div></div>

<p>이 경우에 우리의 마지막 모델인 Write VCS, Read SetNX는 RYW 일관성을 위반하게 됩니다.</p>

<p><img src="https://github.com/MagicalLas/MagicalLas.github.io/blob/master/_screenshots/6-violence.svg?raw=true" alt="반례 시나리오 6" /></p>

<p>redis ttl이 있다고 하더라도 이렇게 순간적으로 없어지는 경우는 드물거에요. 그러나 redis의 메모리가 올라가면서 TTL이 지나지 않은 KEY들을 evict해버릴 수도 있습니다. 혹은 관리자가 캐시를 지우는 상황을 테스트하기 위하여 자신의 redis key를 unlink할 수도 있겠죠. 무슨일이든 redis cache가 만료될 가능성이 있다면 발생할 수 있는 케이스입니다.</p>

<h3 id="혹시-read시에도-vcs를-쓰면-괜찮을까요">혹시 Read시에도 VCS를 쓰면 괜찮을까요?</h3>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">read</span><span class="p">(</span><span class="n">userID</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">string</span> <span class="p">{</span>
    <span class="n">result</span> <span class="o">:=</span> <span class="n">redis</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">userID</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">result</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">result</span>
    <span class="p">}</span>

    <span class="n">dbResult</span> <span class="o">:=</span> <span class="n">db</span><span class="o">.</span><span class="n">findByID</span><span class="p">(</span><span class="n">userID</span><span class="p">)</span>
    <span class="n">redis</span><span class="o">.</span><span class="n">VCS</span><span class="p">(</span><span class="n">userID</span><span class="p">,</span> <span class="n">dbResult</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">dbResult</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">write</span><span class="p">(</span><span class="n">userID</span><span class="p">,</span> <span class="n">newValue</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">string</span> <span class="p">{</span>
    <span class="n">db</span><span class="o">.</span><span class="n">openTransaction</span><span class="p">()</span>
    <span class="n">db</span><span class="o">.</span><span class="n">update</span><span class="p">(</span><span class="n">userID</span><span class="p">,</span> <span class="n">newValue</span><span class="p">)</span>
    <span class="n">db</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>

    <span class="n">redis</span><span class="o">.</span><span class="n">VCS</span><span class="p">(</span><span class="n">userID</span><span class="p">,</span> <span class="n">newValue</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">newValue</span>
<span class="p">}</span>
</code></pre></div></div>

<p><img src="https://github.com/MagicalLas/MagicalLas.github.io/blob/master/_screenshots/7-violence.svg?raw=true" alt="반례 시나리오 7" /></p>

<h3 id="혹시-write시점에-lock을-잡은-모델이면-괜찮을까요">혹시 Write시점에 Lock을 잡은 모델이면 괜찮을까요?</h3>

<p>아뇨, 괜찮지 않습니다. 반례 시나리오는 다음과 같습니다.</p>

<p><img src="https://github.com/MagicalLas/MagicalLas.github.io/blob/master/_screenshots/8-violence.svg?raw=true" alt="반례 시나리오 8" /></p>

<h3 id="readwrite에-하나의-lock을-잡으면-해결될까요">Read&amp;Write에 하나의 Lock을 잡으면 해결될까요?</h3>

<p>유저별로 강한 일관성을 가져갈 수도 있는 방법이죠. 한번에 Read 혹은 Write 요청 하나만 수행되는겁니다. 아주 강력한 방법이기에 이걸 시도해보죠.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">read</span><span class="p">(</span><span class="n">userID</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">string</span> <span class="p">{</span>
    <span class="n">l</span> <span class="o">:=</span> <span class="n">distLock</span><span class="o">.</span><span class="n">lock</span><span class="p">(</span><span class="n">userID</span><span class="p">)</span>
    <span class="k">defer</span> <span class="n">distLock</span><span class="o">.</span><span class="n">unlock</span><span class="p">(</span><span class="n">l</span><span class="p">)</span> <span class="c">// 이 함수의 결과가 반환될때 release</span>

    <span class="n">result</span> <span class="o">:=</span> <span class="n">redis</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">userID</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">result</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">result</span>
    <span class="p">}</span>

    <span class="n">dbResult</span> <span class="o">:=</span> <span class="n">db</span><span class="o">.</span><span class="n">findByID</span><span class="p">(</span><span class="n">userID</span><span class="p">)</span>
    <span class="n">redis</span><span class="o">.</span><span class="n">VCS</span><span class="p">(</span><span class="n">userID</span><span class="p">,</span> <span class="n">dbResult</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">dbdbResultResule</span>
<span class="p">}</span>
</code></pre></div></div>

<p>이 상황에서 동시성이라는건 없습니다. TLA+로 300만개의 유니크한 경우의 수를 전부 시뮬레이션해보아도 일관성이 깨지는 현상을 발견하지 못합니다.</p>

<p>하지만 모든 Read &amp; Write 요청에서 Lock을 잡는건 비효율적입니다. Read Heavy한 트래픽이기도 하지만, Redis 정합성을 위하여 Redis Cache Hit의 경우에도 Lock을 잡기 때문이죠. 그러면 이렇게 Redis에 값을 넣는 시점에만 Lock을 잡으면 어떨까요? 아래 의사코드를 작성했습니다.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">read</span><span class="p">(</span><span class="n">userID</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">string</span> <span class="p">{</span>
    <span class="n">result</span> <span class="o">:=</span> <span class="n">redis</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">userID</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">result</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">result</span>
    <span class="p">}</span>

    <span class="n">dbResult</span> <span class="o">:=</span> <span class="n">db</span><span class="o">.</span><span class="n">findByID</span><span class="p">(</span><span class="n">userID</span><span class="p">)</span>

    <span class="n">l</span> <span class="o">:=</span> <span class="n">distLock</span><span class="o">.</span><span class="n">lock</span><span class="p">(</span><span class="n">userID</span><span class="p">)</span> <span class="c">// cache miss시에 redis에 넣기전 lock</span>
    <span class="n">redis</span><span class="o">.</span><span class="n">VCS</span><span class="p">(</span><span class="n">userID</span><span class="p">,</span> <span class="n">dbResult</span><span class="p">)</span>
    <span class="n">distLock</span><span class="o">.</span><span class="n">unlock</span><span class="p">(</span><span class="n">l</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">dbResult</span>
<span class="p">}</span>
</code></pre></div></div>

<p>이 경우는 아쉽게도 커버하지 못하는 경우가 생겼습니다. 하지만 좋은 접근이에요.</p>

<p><img src="https://github.com/MagicalLas/MagicalLas.github.io/blob/master/_screenshots/9-violence.svg?raw=true" alt="반례 시나리오 9" /></p>

<p>그러면 Cache miss된 시점, DB Read 이전에 Lock을 잡으면 어떨까요?</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">read</span><span class="p">(</span><span class="n">userID</span> <span class="kt">string</span><span class="p">)</span> <span class="kt">string</span> <span class="p">{</span>
    <span class="n">result</span> <span class="o">:=</span> <span class="n">redis</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">userID</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">result</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">result</span>
    <span class="p">}</span>

    <span class="n">l</span> <span class="o">:=</span> <span class="n">distLock</span><span class="o">.</span><span class="n">lock</span><span class="p">(</span><span class="n">userID</span><span class="p">)</span>

    <span class="n">dbResult</span> <span class="o">:=</span> <span class="n">db</span><span class="o">.</span><span class="n">findByID</span><span class="p">(</span><span class="n">userID</span><span class="p">)</span>
    <span class="n">redis</span><span class="o">.</span><span class="n">VCS</span><span class="p">(</span><span class="n">userID</span><span class="p">,</span> <span class="n">dbResult</span><span class="p">)</span>

    <span class="n">distLock</span><span class="o">.</span><span class="n">unlock</span><span class="p">(</span><span class="n">l</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">dbResult</span>
<span class="p">}</span>
</code></pre></div></div>

<p>이렇게 작성한다면 RYW 일관성을 지킬 수 있습니다. 매우 많은 경우와 구현이 있고 복잡하지 않나요? Redis Cache를 사용할 때 위에 나와있는 구현 방법과 반례 시나리오를 전부 고려할 수 있을까요? 하루 이틀로는 불가능한 양으로 보여집니다. Jeff Dean이나 Lamport라면 다르겠지만요.</p>

<table>
  <tbody>
    <tr>
      <td>자신이 쓴걸 그 다음 요청에서 읽을 수 있게 만드는 것은 매우 세심하게 설계되어야합니다.</td>
    </tr>
  </tbody>
</table>

<h3 id="정말-이런-상황이-발생할-수-있는가">정말 이런 상황이 발생할 수 있는가?</h3>

<p>분산 시스템의 edge case를 이야기하면 항상 따라오는 질문입니다.</p>

<ul>
  <li>“그게 정말 일어나?”</li>
  <li>“확률이 얼마나 되는데?”</li>
  <li>“우리는 안 일어날 것 같은데?”</li>
  <li>등등 많은 질문들…</li>
</ul>

<p>아마도 거의 일어나지 않을거에요. Network 지연이 그렇게 크지 않을거고, STW 시간이 몇분씩 걸리지도 않겠죠.
갑자기 ACK, RST도 주지 않고 네트워크가 죽어버리는 경우는 어떤 제품은 한번도 겪지 않을 수도 있습니다.
그러나 발생하지 않는 것은 아닙니다. 모든 것은 확률과 리스크에 달렸죠.</p>

<p>간단한 산술로 대략적인 규모를 살펴볼까요? 잘못되었을 가능성이 매우 높은 가정들이기에 실제와 다릅니다.</p>

<p>가정:</p>
<ul>
  <li>읽기 요청: 10,000건/초</li>
  <li>쓰기 요청: 10건/초 (피크)</li>
  <li>동시 접속 유저: 5,000명</li>
  <li>쓰기 duration: 200ms (GC는 10ms + 네트워크 RTT 2ms로 가정시)</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>동시 진행 중인 쓰기 = 10 * 0.2 = 2건
동일 유저에게 요청이 겹칠 확률 = 2 / 5,000 = 0.04%

하루에 쓰기 요청이 겹쳐진 상태로 처리된 횟수(확률적) = 10 * 0.0004 * 86,400 = 346건
</code></pre></div></div>

<p>0.04%는 무시해도 될 것 같은 수치입니다. 쓰기-쓰기 충돌은 매우 적게 발생합니다. 아마 실제로는 더 적을 수도 있어요. 혹은 더 많을 수도 있습니다. 위에서 이야기한 요청 하나에서 여러 요청이 파생되는 경우는 거의 동시에 요청이 들어올 수도 있죠. 발생하지 않을 수도 있어요. 하지만 시간이 지나면서 계속 수행하다보면 언젠가 발생할 수 있습니다.</p>

<h3 id="우리가-가정하는-것들">우리가 가정하는 것들</h3>

<p>저는 이렇게 이야기하고싶습니다. 우리는 어떠한 가정에 기대고 있는지를 아는게 매우 중요합니다. 네트워크는 이렇고, STW는 저렇고, TTL은 어떻고, DB는 어떻게 동작하고 등등… 그리고 그 가정을 어느정도는 알고있지 못하면 가정이 깨졌을 때 문제를 식별하기 힘들어집니다. 혹은 그 가정을 스스로 깨트릴 수도 있습니다.</p>

<p>실제 우리 시스템이나 구현을 전부 모델링해서 검증하는게 아니라면 완벽히 안전하다는걸 검증할 수 없습니다. 하지만 우리의 시스템의 논리적인 버그를 잡는데 도와줍니다.</p>

<p>결국 어디까지 리스크를 견딜것인지가 중요합니다. 그 관점에서 트레이드오프를 하는것이죠. 질문은 이겁니다. <strong>“이 가정이 깨졌을 때 우리 시스템은 어떻게 동작하는가?”</strong> 대답이 “모르겠다”라면, 그건 리스크를 관리하는 게 아니라 운에 맡기는 겁니다.</p>

<h2 id="일관성-모델을-정의하고-지켜나가는-방법">일관성 모델을 정의하고 지켜나가는 방법</h2>

<p>일관성을 보장하는 건 공짜가 아닙니다. 분산 락은 latency를 추가하고, VCS는 구현 복잡도를 높이며, TTL을 제거하면 메모리 비용이 증가합니다.
어떤 선택이 맞는지는 비즈니스 요구사항에 달려있습니다.</p>

<p>중요한 건 어떤 일관성 모델을 선택했는지 명확히 알고, 그에 맞는 정확한 구현을 하는 것입니다. 일관성 모델을 잘 지키기는게 중요하다면 그걸 중요하게 모델링하세요.</p>

<table>
  <tbody>
    <tr>
      <td>Hope is not a strategy.</td>
    </tr>
  </tbody>
</table>

<p>제가 좋아하는 말을 하나 인용해볼게요. 희망은 전략이 아닙니다. 어떤 일관성 모델을 만족할거라고 희망, 기대하지 말고 전략을 세워서 접근해야합니다. 머릿속으로 상상해본 한두가지 케이스에 대하여 잘 동작하는걸 보는건 적절한 전략이 아닐 수도 있습니다.</p>

<h2 id="마무리">마무리</h2>

<p>분산시스템은 직관으로 이해하기 어렵습니다. 개발자의 상상력과 사고력은 한계가 있고, 유명한 오픈소스들도 오류를 범하기도합니다.
자신있게 안전하다고 생각한 속성도 실제로 지켜지지 못할 수도 있습니다.</p>

<p>추상적인 모델을 검증할 수 있는 Formal Method를 사용해서 캐시 시스템의 일관성을 모델링하고 검증해보았습니다. 
작은 노력으로 사람이 상상할 수 없는 조합을 검증하면서 우리의 시스템을 더 잘 이해할 수 있습니다.
단순 Look-aside 방식에서 SET, SET NX, Lock등의 방법들을 쉽게 전환하면서 검증하며 시스템을 견고하게 디자인해나갈 수 있습니다.</p>

<p>앞으로도 안전한 시스템 디자인하시길 바랍니다. 혹시 안전한, 정확한 시스템을 구현하기 위한 이야기를 나누고싶으신 분이 있다면 편히 메일주세요!</p>]]></content><author><name>Las aka Wonho</name></author><category term="essay" /><summary type="html"><![CDATA[urge to write this article]]></summary></entry><entry><title type="html">regex에 대하여</title><link href="https://blog.magical.dev/regex0" rel="alternate" type="text/html" title="regex에 대하여" /><published>2025-11-08T00:00:00+00:00</published><updated>2025-11-08T00:00:00+00:00</updated><id>https://blog.magical.dev/regex0</id><content type="html" xml:base="https://blog.magical.dev/regex0"><![CDATA[<h2 id="regex는-아주-자주-사용된다">regex는 아주 자주 사용된다</h2>

<p>regex, 정규 표현식은 개발을 하다보면 흔히 사용되는 것들이다. 복잡한 파싱 로직을 작성하지 않고도 쉽게 텍스트 관련 로직을 처리할 수 있다.
물론 우리는 다들 복잡한 정규식을 이해하거나, 작성할 일은 자주 없겠지만 그래도 단순한 형식의 정규식은 매우 유용하다. 내가 본 몇가지 케이스는,, 특정 문자열을 포함하고 있는지 확인하는 contains도 있었고, 특정 prefix, suffix가 있는 문자열을 찾는 것도 있었다. 혹은 전화번호, 카드번호로 의심되는 문자열을 찾는 것도 있었는데, 정규식이 엄청나게 느리다는걸 알게된 계기였다.</p>

<p>정규색 객체의 생성 비용은 비교적 비싼 편이다. 나는 처음에 단순한 문자열을 담고있는 객체라고 생각했었지만, 실제로는 그렇지 않다는걸 프로파일러를 통해 알 수 있었다. 정규식을 함수가 호출할때 매번 생성하는 것은 매우 비효율적이다. 종종 이러한 서비스의 코드를 보고 수정하거나 알려드리곤 했었다. 하지만 실제로 왜 이렇게 복잡하고, 느리고, 객체 생성 자체가 비싼지 생각해보지는 않았었다. 그리고 그게 나에게 엄청 중요한 문제는 아니었다.</p>

<p>정규식의 속도는 문제가 된다. 적어도 나에게는 문제가 되었다. 개인정보로 의심되는 문자열이 있는지 확인해야하는 프로그램을 작성해야하는 일이 주어졌는데, 병목지점의 대부분이 regex 매칭을 처리하는 부분이었다. 수~수십 GB정도 되는 크기의 가변길이로 인코딩된 lz4 압축 파일이 6개정도 있었는데 이 파일을 전체 처리하는데 엄청난 시간이 걸렸다. 파일 갯수만큼 스레드를 만들어서 처리하더라도 regex는 CPU를 아주 많이 사용해서 스레드들이 전부 CPU 100%로 열심히 일하고 있었다.</p>

<p>6core를 전부 사용하면서 수십기가 정도 파일을 몇십분동안 처리하는게 나로서는 이해되지 않았다. 프로파일링을 해보아도, russ cox가 작성한 re2로 바꾸어도 그렇게까지 성능 개선이 없었다. 실험적, 경험적인 최적화는 이제 거의 한계였다. 스레드를 더 늘리면 해소야 되겠지만, 결국 성능을 결정짓는 함수의 계수와 기울기는 바뀌지 않는다. 이제 이론적으로 이 문제를 개선할 시간이다.</p>

<p>(잡설을 조금 적자면, 나는 scale 문제에서 기울기가 매우 중요하다고 생각한다. keyvalue와 bitmap은 둘다 1차 함수로 linear하게 증가하지만 계수가 다르다.)</p>

<h2 id="regex와-finite-automata">regex와 Finite Automata</h2>

<p>re2의 방식이 왜 실패했는지, 기존 regex는 어떻게 동작하고 re2는 어떻게 다를지를 보아야했다. 그래서 처음 시작은 (내가 아주 좋아하는 개발자인) russ cox의 블로그 글을 읽는 것으로 시작했다.</p>

<p>regex가 무엇인지 아주 간단하게 설명하고 시작하는데, 동일하게 인용하면서 시작해보자.</p>

<table>
  <tbody>
    <tr>
      <td>Regular expressions are a notation for describing sets of character strings. When a particular string is in the set described by a regular expression, we often say that the regular expression matches the string.</td>
    </tr>
    <tr>
      <td>정규 표현식은 문자열 집합을 설명하는 표기법입니다. 특정 문자열이 정규 표현식이 나타내는 문자열 집합에 속하는 경우 우리는 그 문자열이 matching된다고 합니다.</td>
    </tr>
  </tbody>
</table>

<p>문자열 집합을 설명하는 다른 방법은 FSM(Finite Automata, State Machine)이 있다. 다들 알고있을 이 방식으로 문자열 집합을 설명할 수 있다. 간단한 예시는 다음과 같다.</p>

<p>예시의 정규식을 간단한 FSM으로 만들어보겠다.
“(l?s)|(won?o)”</p>]]></content><author><name>Las aka Wonho</name></author><category term="essay" /><summary type="html"><![CDATA[regex는 아주 자주 사용된다]]></summary></entry><entry><title type="html">가치는 원칙보다 우선된다</title><link href="https://blog.magical.dev/values-practice" rel="alternate" type="text/html" title="가치는 원칙보다 우선된다" /><published>2023-09-08T00:00:00+00:00</published><updated>2023-09-08T00:00:00+00:00</updated><id>https://blog.magical.dev/values-practice</id><content type="html" xml:base="https://blog.magical.dev/values-practice"><![CDATA[<p>가치는 원칙보다 우선되고, 원칙은 실천방식보다 우선된다고 생각한다.</p>

<p>나는 매우 가치 지향적인 사람이고 그에 공감하려고 노력한다. 그러다보니 ‘일단 한번 해보자’와 좀 잘 맞지 않는 것 같다. 돌이켜보면 가치를 공감하지 못한채로 특정 프랙티스를 한다면 잘 안지켜지는 경우가 많았던 것 같고, 이는 프랙티스를 먼저 도입하려고하기 때문으로 느낀다.</p>

<p>마치 그런거다. (어느 구체적인 팀을 의미하는 것은 아님) 새롭게 팀이 만들어지고 다들 일단 목표가 있으니 프로세스를 잡자고 이야기한다. 플래닝이 생기거나 데일리 스탠드업 미팅이 생긴다. 처음에는 왜 해야하는지 잘 이야기를 하는 경우도 있지만 많은 경우 그냥 “스크럼”을 많이 하니까 / 다른 팀에서 그렇게 하더라 / 이전 팀에서 이렇게 했다 와 같이 정해진다.</p>

<p>다른 사람들도 ‘아 그냥 프로세스는 이런거구나’하면서 따라간다. 그러다보면 처음에는 뭔가 잘 굴러가는 것 처럼 보이다가 나중에는 왜 하는지도 모르고 이상하게 바껴있더라. 나는 항상 이런 것들이 의문이었다. 왜 이렇게되지? 왜 다들 습관적으로 하게될까? 다들 이걸 왜 하는거지?</p>

<p>내가 생각한 결론은 팀이 가치에 공감하지 못한다는 것이다. 이 팀은 가치가 좋아서 프로세스를 도입한게 아니라 프로세스가 좋아서 도입한 것이다. 만약 칸반을 도입한다면 (아마 많은 회사들이 이런 방식을 쓰고있을텐데) 정말로 낭비를 줄이는데 신경을 많이 쓸까? 이전 팀에서도 칸반을 했지만 다들 낭비를 줄이거나 배송주기를 짧게 만드는데 관심이 크지 않았다.</p>

<p>많은 팀에서 ‘애자일’을 한다고 했는데 정말 애자일 선언문을 읽고 가슴이 뛰며 그렇게 일하는게 진정으로 좋은 방향이라고 생각할까? 정말 동작하는 소프트웨어를 중요하게 생각하고 프로세스보다는 개인과 상호작용을 중요하게 여길까? 내가 보아왔던 사람들중 많은 이들은 그렇지 않았다.</p>

<p>나는 그들이 정말로 가치에 공감을 하고 그렇게 일하고싶었다면 그들이 방식을 만들었을거라고 생각한다. 애초에 우리가 ‘스크럼/칸반’을 몰랐다면… 애자일 선언문만 알았다면 더 나은 문화가 되었을것같다. 그렇게 만들어진 프로세스는 모두의 공감을 통해 만들어지고… 나는 공감하지 않은 것은 진정으로 이루어지지 않는다고 생각한다.</p>

<p>그래서 나에게는 언제나 가치가 원칙보다 중요하고, 원칙이 실천방식보다 중요하다. 내가 실천방식을 잘 지키려면 1) 가치에 공감하고 2) 원칙을 이해하며 3) 실천방식이 효과적임을 느껴야한다.</p>]]></content><author><name>Las aka Wonho</name></author><category term="essay" /><summary type="html"><![CDATA[가치는 원칙보다 우선되고, 원칙은 실천방식보다 우선된다고 생각한다.]]></summary></entry><entry><title type="html">Java의 pointer와 memory allocation</title><link href="https://blog.magical.dev/jvm-pointer" rel="alternate" type="text/html" title="Java의 pointer와 memory allocation" /><published>2023-08-25T00:00:00+00:00</published><updated>2023-08-25T00:00:00+00:00</updated><id>https://blog.magical.dev/jvm-pointer</id><content type="html" xml:base="https://blog.magical.dev/jvm-pointer"><![CDATA[<div class="language-md highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gt">&gt; 메모리 해제 누락과 같은 실수를 줄이는 가장 좋은 방법은 뭘까? 오답노트 만들기? 시행착오 거치기? 각자 생각이 다르겠지만, 내 생각은 처음부터 실수할 일이 없는 도구나 환경을 만드는 것이다. 개인의 의지나 역량에 의존하는 것만으로는 실수를 줄이기 어렵다.</span>
<span class="gt">&gt;</span>
<span class="gt">&gt; 그런 도구 중 가장 잘 알려진 것은 Java의 가비지 콜렉션이 아닐까? Java는 포인터를 없애 버리고, 개발자가 메모리 해제를 신경쓰지 않아도 되게 했다. 그 결과 모든 개체를 힙에 생성/해제하는 비용은 있을지언정, 메모리 누수 같은 짜치는(..) 실수로 다른 기능들을 개발하고 개선할 시간이 낭비될 가능성을 줄였다.</span>

ref: https://je-continue.tistory.com/51
</code></pre></div></div>

<p>꽁띠뉘에님의 글을 읽다가 “Java는 포인터를 없애 버리고”라는 부분이 눈에 띄였습니다. 제가 갖고있던 Java의 모습과는 다소 다른 표현이어서 조금 더 깊게 알아보고 싶었습니다. 따라서 이 부분에 대한 저의 생각과 이해를 공유해보려고 합니다. 위 글의 내용이 틀렸다고 주장하기보다는 조금 부가적인 설명을 하고자하는 목적입니다.</p>

<blockquote>
  <p>ps. 개인적으로 꽁띠뉘에님의 글이 매우 잘 작성되었으며, 깊이있고 정확한 내용을 담고있음을 명확히 하고싶습니다. 한두문장으로 트집을 잡는 것 같아 조금 마음이 불편하네요.</p>
</blockquote>

<p>(아래 내용은 조금 학술적인 톤을 유지하려고 노력했습니다.)</p>

<h2 id="포인터란">포인터란?</h2>

<p>아마 이 글을 읽는 사람이라면 포인터에 대하여 어느정도 지식이 있다고 생각한다. 이 글에서 pointer는 C의 pointer를 기준으로 이야기를 해보고자한다. C의 pointer는 특정한 메모리 주소를 가르키는 값의 타입으로 보통 32비트 혹은 64비트의 unsigned integer의 값을 가진다. 이는 메모리 공간의 주소를 가르키기때문인데, 정확히는 실행되는 프로그램의 메모리 공간이라면 어디든 의미할 수 있다.</p>

<p>ok, 여기까지는 간단하고 명료하다. 하지만 어려워지는 것은 여기부터이다. C나 다른 언어의 경우 대부분 자신이 가르키고있는 특정한 메모리 위치에 어떠한 구조로 위치해있는지 명시하고있다. 예를들어, int* a의 경우에는 a가 가르키는 주소에 int의 메모리 구조로 할당되어있을 것을 나타낸다. 만약 SomeStrcut* b의 경우에는 b가 가르키는 주소에 SomeStruct의 메모리 구조로 할당되어있다고 예측할 수 있다. 하지만 이는 매번 지켜지는 것은 아니고, 컴파일 시점에 체크하거나 체크하지 않으면 프로그래머를 믿고 동작하게 된다. 특히 void*를 사용하거나 이러한 포인터를 형변환하는 경우에 이러한 점들이 잘 나타난다.</p>

<p>대부분의 자료형들은 기본값이 존재한다. int는 0, bool은 false, (C언어는 아니지만) string은 “”, 어레이는 []가 대표적이다. 이러한 기본값은 특정 변수를 할당하는 과정에서 바로 할당을 하지 않는다면 설정하는 값으로, 여러가지 실용적인 이유와 이론적인 이유로 동작한다. 포인터도 마찬가지로 특정한 메모리 주소를 가르키는 자료형이기에 기본값이 필요했다. 우리는 이 값을 너무나 잘 알고있는데 바로 <strong>null</strong>이다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ps. C의 경우에 변수를 선언한 경우 자동으로 값이 초기화되지 않는다. 초기화되지 않은 변수에 대한 접근은 Undefined Behavior이다.
</code></pre></div></div>

<p>null pointer exception으로 아주 잘 알려진 이 포인터의 초기값은 포인터가 아무것도 가르키고있지 않다는 것을 의미한다. 아무것도 가르키고있지 않은데, 그곳에 접근하려고하니 에러가 발생하는 것이다.</p>

<h2 id="java의-포인터">Java의 포인터</h2>

<p>위에서 이미 힌트가 나왔는데, Java에는 포인터라는 개념이 존재한다. Java의 에러 타입에 이미 <code class="language-plaintext highlighter-rouge">NullPointerExceptions</code>가 존재하기 때문에 포인터라는 개념이 존재하는 것은 부정할 수 없다. 하지만 좀 더 자세히 살펴보자.</p>

<p>C는 값(value) 기반으로 동작하는 언어로, 포인터도 primitive type 중 하나로 취급된다. Java는 조금 다르게 동작하는데, primitive type(예: int, boolean)들과 객체 참조가 존재한다. 지금은 객체 참조를 C의 포인터와 비슷한 무언가로 이해해도 된다. Java의 경우 대부분의 객체의 변수는 객체의 참조를 나타내는데, 이는 특정 메모리 주소를 가르키는 값이다. 그리고 C와 유사하게도 가르키고있는 메모리 주소가 어떠한 구조로 할당되어있는지에 대한 정보를 담고있다.</p>

<p>Java의 객체 참조와 C의 포인터의 주요 차이점은 Java에서는 저수준(low-level) 연산들을 지원하지 않는다는 것이다. 특정 객체 참조에 대하여 +와 같은 연산을 시도하면 컴파일 과정에서 문제가 발생한다. 따라서 이러한 부분은 실제 포인터를 사용해서 구현되어있더라도 연산을 제한하는 문법을 퉁하여 여러가지 문제점들을 보완하고있다.</p>

<p>하지만 Java도 모든 연산을 엄밀하게 검증하는 것은 아니다. 예를들어 Object와 같은 타입은 모든 객체의 sub type인데, A type &lt;-&gt; Object &lt;-&gt; B type과 같은 변환을 컴파일하는 것이 가능하다. Java의 타입 변환은 업캐스팅(자동 변환)과 다운캐스팅(명시적 변환)으로 나뉘는데, 모든 객체는 Object 타입으로 업캐스팅될 수 있지만, 실제 타입을 모르는 상태에서의 다운캐스팅은 오류를 발생시킬 수 있다.</p>

<p>이를 포인터 관점에서 보면, 다운캐스팅의 잘못된 사용은 잘못된 메모리 영역에 접근하려는 것과 유사하다고 볼 수 있다. C나 C++의 포인터 오류와 유사하게, Java에서는 잘못된 다운캐스팅 시 ClassCastException이 발생하게된다. 이런 관점에서 Java의 타입 변환은 “포인터”의 안전성과 직접적인 연관이 있다고 볼 수 있다. 포인터의 잘못된 사용을 방지하고 메모리 관리를 안전하게 유지하기 위해 타입 시스템을 사용하여 안전한 메모리 관리를 보장한다.</p>

<p>이러한 관점에서 보면 Pointer를 직접 노출하지는 않지만 내부적으로는 사용하고있으며 프로그래머입장에서 내부적인 개념을 드러내는 경우에는 Pointer를 보게된다.</p>

<h2 id="포인터와-gc의-관계">포인터와 GC의 관계</h2>

<p>우리는 위에서 Java는 포인터가 존재하지만, 내부적으로만 사용하며 직접 low한 operation을 수행할 수 없다는 것을 알게되었다. 그렇다면 이러한 포인터를 직접 사용하지 않는 것이 GC에 필수적일까?</p>

<p>GC의 종류에 대해 살펴보면, 보통 tracing gc와 reference count gc로 나눌 수 있다. tracing GC를 사용하는 경우, mutator root로부터 어떤 객체가 참조 가능한지를 추적하는 방식으로 동작한다. 일단 Java의 경우 tracing gc를 많이 사용하기에 tracing gc를 기준으로 이야기해보겠다.</p>

<p>이 과정에서 가장 중요한 것은 pointer에 대한 +-연산으로 참조하는 메모리 공간을 추적하는 방법이다. 이러한 포인터를 파생 포인터(derived pointer)라고 한다. C/C++과 같은 파생 포인터의 경우 GC가 이를 감지하기가 어려운데, 특정 메모리 영역을 참조하여 도달가능한지가 런타임에 정해지기 때문이다. GC는 최대한 보수적으로 동작해야하며, 사용중인 메모리를 할당하지 않는 safty를 보장해야하기 때문에 이런 파생 포인터는 GC의 구현에 큰 어려움을 준다.</p>

<p>Java의 경우도 이는 마찬가지인데, <a href="https://dl.acm.org/doi/pdf/10.1145/277650.277738">Garbage Collection and Local Variable Type-Precision and Liveness in JavaTM Virtual Machines</a>에서도 다음과 같은 언급이 등장한다.</p>

<div class="language-md highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gt">&gt; The relatively simple and highly constrained model of references presented by the JVM avoids the optimization-induced problems these other works address, such as interior and derived pointers.</span>
<span class="gt">&gt;</span>
<span class="gt">&gt; JVM이 제공하는 비교적 단순하고 제약이 많은 참조 모델은 내부 및 파생 포인터와 같은 다른 연구들에서 다루는 최적화 관련 문제를 피할 수 있습니다.</span>
</code></pre></div></div>

<p>이러한 점들을 비교해보았을 때 GC 구현을 쉽게하는데 있어서 자바와 같이 pointer 연산에 제약을 주는 것은 매우 효과적이라고 할 수 있다.</p>

<p>GC와 포인터가 함께 존재하는 언어인 Go를 살펴보는 것도 좋다. Go는 포인터가 명시적으로 존재하며 *int와 같은 타입을 실제로 많이 사용하는 언어이다. Java와 같이 pointer에 대하여 연산을 거의 지원하지 않는다. 따라서 대부분의 경우에는 Java와 비슷하게 tracing하는게 크게 문제가 없겠지만, 특수한 예외로 uintptr이라는 타입을 제공한다. 이는 C의 포인터와 유사한데, uintptr은 단순한 integer로 인식되며 GC는 uintptr가 가르키는 포인터를 할당해제할 수도 있다.</p>

<div class="language-md highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gt">&gt; A uintptr is an integer, not a reference. Converting a Pointer to a uintptr creates an integer value with no pointer semantics. Even if a uintptr holds the address of some object, the garbage collector will not update that uintptr's value if the object moves, nor will that uintptr keep the object from being reclaimed.</span>
<span class="gt">&gt;</span>
<span class="gt">&gt; uintptr은 참조가 아닌 정수입니다. 포인터를 uintptr로 변환하면 포인터 시맨틱이 없는 정수 값이 생성됩니다. 가비지 컬렉터가 어떤 객체의 주소를 보유하고 있더라도 객체가 이동하면 가비지 컬렉터는 해당 객체의 값을 업데이트하지 않으며, 해당 객체가 회수되지 않도록 유지하지도 않습니다.</span>
<span class="gt">&gt;</span>
<span class="gt">&gt; ref: https://pkg.go.dev/unsafe#Pointer</span>
</code></pre></div></div>

<p>여기서 핵심적인 포인트는 포인터를 직접적으로 노출하는 것이 아니라, 파생 포인터의 존재 없이 객체의 참조 가능성을 명확하게 만드는 것이 GC 구현에 중요하다는 것이다.</p>

<h2 id="conclusion">Conclusion</h2>

<ol>
  <li>Java에는 객체 참조를 구현하기 위하여 내부적으로는 Pointer가 존재하지만, C와 같은 저수준 연산을 지원하지 않는다.</li>
  <li>이러한 객체 참조의 특성 상 Java에서는 파생 포인터와 같은 복잡한 연산이 필요하지 않게 되었다.</li>
  <li>파생 포인터는 GC를 구현하는데 매우 힘들게 만드는 매커니즘이다.</li>
  <li>포인터라는 용어나 개념을 드러내는 것보다는 low-level의 연산을 막는 것이 GC 구현에 더 중요하다</li>
</ol>

<h2 id="ps">ps.</h2>

<p>저는 CS 전공을 하지 않았으며 이 글의 내용은 동료로부터 엄격한 리뷰를 거치지 않아 잘못된 내용이 존재할 수도 있습니다. 관련해서 정확하지 않은 내용이나 개선할 점이 있다면 언제든 메일(las@magical.dev)로 연락 부탁드립니다. 감사합니다.</p>]]></content><author><name>Las aka Wonho</name></author><category term="jvm" /><summary type="html"><![CDATA[```md 메모리 해제 누락과 같은 실수를 줄이는 가장 좋은 방법은 뭘까? 오답노트 만들기? 시행착오 거치기? 각자 생각이 다르겠지만, 내 생각은 처음부터 실수할 일이 없는 도구나 환경을 만드는 것이다. 개인의 의지나 역량에 의존하는 것만으로는 실수를 줄이기 어렵다. 그런 도구 중 가장 잘 알려진 것은 Java의 가비지 콜렉션이 아닐까? Java는 포인터를 없애 버리고, 개발자가 메모리 해제를 신경쓰지 않아도 되게 했다. 그 결과 모든 개체를 힙에 생성/해제하는 비용은 있을지언정, 메모리 누수 같은 짜치는(..) 실수로 다른 기능들을 개발하고 개선할 시간이 낭비될 가능성을 줄였다.]]></summary></entry><entry><title type="html">Golang GC</title><link href="https://blog.magical.dev/golang-gc" rel="alternate" type="text/html" title="Golang GC" /><published>2023-05-03T00:00:00+00:00</published><updated>2023-05-03T00:00:00+00:00</updated><id>https://blog.magical.dev/golang-gc</id><content type="html" xml:base="https://blog.magical.dev/golang-gc"><![CDATA[<p>Golang은 GC(garbage collector)를 갖고있는 언어입니다. 이 글에서는 Golang의 GC를 자세하게 살펴보겠습니다.</p>

<hr />

<p>Golang의 GC는 concurrent mark and sweep GC이며, 비세대화(non-generational) &amp; 비압축(non-compacting) GC입니다. 하나하나 풀어나가보겠습니다.</p>

<h2 id="mark-and-sweep">Mark and Sweep</h2>

<p>concurrent mark and sweep은 GC 알고리즘으로, mark and sweep 작업을 유저의 코드와 동시에 수행합니다. mark and sweep은 매우 전통적인 GC 알고리즘인데, 우리는 일단 concurrent하지 않은 일반 버전의 mark and sweep을 먼저 알아보겠습니다.</p>

<p>mark and sweep은 mark와 sweep의 2가지 단계로 이루어져있습니다. 단순하게 말하자면 mark하고 sweep하죠. mark는 우리가 청소해야할 영역이 어디인지 확인하고 sweep은 실제로 청소합니다.</p>

<p>mark 과정을 먼저 살펴보겠습니다. mark 단계에서는 사용 중인 모든 메모리 영역에 표시(mark)를 합니다. 메모리(주로 힙)에 존재하는 객체들은 그래프 구조를 형성합니다. 다음은 예시 golang 코드와 메모리 구조입니다.</p>

<p><img src="https://github.com/MagicalLas/MagicalLas.github.io/blob/master/_screenshots/golang-gc-01.png?raw=true" alt="mem structure sample " /></p>

<p>일반적으로, mutator root라는 특정 위치로부터 접근 가능한 모든 자식 객체들을 탐색하면서 mark하게 됩니다. 이 과정이 전부 끝나면 mark되지 않은 객체들은 root로부터 접근가능하지 않다고 할 수 있습니다. 따라서 mark되지 않은 모든 곳은 새로운 객체를 할당할 수 있는 공간입니다.</p>

<p>sweep 단계에서는 모든 힙을 검사하고, mark되지 않은 메모리를 해제합니다. 이렇게 보면 간단하고 쉬운 알고리즘같습니다. 조금 더 디테일을 살펴보기 위하여 tri-color abstraction을 알아봅시다.</p>

<p>tri-color abstraction은 mark and sweep의 작업 상태를 추상화하는 방법입니다. tri-color라는 말처럼 3가지 색상으로 객체를 마크합니다.</p>

<ol>
  <li>black</li>
  <li>grey</li>
  <li>white</li>
</ol>

<p><strong>black</strong>은 접근 가능하며 제거되어서는 안 되는 라이브 객체를 표현합니다. 간단하게 이미 mark가 끝난 객체라고 볼 수도 있습니다. <strong>grey</strong>는 black과 유사하지만 다른데, root로부터 접근은 가능하지만 아직 모든 자식 객체들이 mark되지 않아서 mark단계가 끝나지 않았음을 암시합니다. <strong>white</strong>는 mark되지 않은 객체를 의미합니다. sweep 단계에서 white객체들을 해제하게 됩니다.</p>

<p>처음에 모든 노드(객체)들은 white 상태로 시작합니다. marking단계에 접어들었을 때 mutator root를 시작으로 marking이 진행됩니다. 노드를 처음 만난다면 grey로 색칠하고, 그 노드의 자식을 모두 식별했다면 black으로 채색합니다.</p>

<p>이런 추상화 방법을 사용하면, mark 단계가 진행 중일 때는 black, grey, white 객체가 모두 존재하게 됩니다. grey 객체가 있다는 것은 아직 grey객체의 자식중에 mark(grey or black)가 되지 않은 것이 존재함을 의미합니다. 그러므로 안전하게 메모리 해제를 진행하려면, sweep 단계로 진입하기 전에는 grey 객체가 없어야 합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tri-color abstraction은 다음의 불변식(invariant)을 만족합니다

1. mark가 끝나면, black 객체에서 white 객체로의 접근이 존재하지 않습니다.
2. mark가 끝나면, grey객체가 없습니다.
</code></pre></div></div>

<p>tri-color abstraction에 대해 더 자세히 알고 싶다면, 다음 논문을 참조하세요. <a href="https://lamport.azurewebsites.net/pubs/garbage.pdf">On-the-Fly Garbage Collection: An Exercise in Cooperation</a></p>

<hr />

<h3 id="concurrently-running-mark-and-sweep">Concurrently Running Mark and Sweep</h3>

<p>전통적인 mark and sweep은 잘못된 메모리 할당을 방지하기 위해 mark와 sweep 과정 동안 일시정지(STW)가 필요합니다. 하지만 STW는 여러가지 문제점을 안고있는데, 예를들어 어떤 요청을 받은 순간 GC가 시작되고 STW가 실행되어 요청을 처리하지 못할 수 있습니다. 요청에 대한 응답시간이 증가하는 것은 물론이며 부작용이 발생할 수 있습니다. 따라서 GC를 concurrent하게 실행하여 GC latency를 줄이는 것이 중요합니다.</p>

<blockquote>
  <p>보통 GC는 latency와 throuput의 트레이드 오프가 있습니다. latency는 얼마나 빠르게 GC 사이클이 실행되어 STW를 줄이는지이며, throuput은 단위 시간당 메모리를 정리할 수 있는 크기를 말합니다.</p>
</blockquote>

<p>Golang의 GC는 최소 지연시간을 달성하기 위해 다양한 노력을 기울였으며, 그 결과가 바로 CMS(Concurrent Mark Sweep)입니다. mark and sweep을 concurrent하게 실행하는 CSM는 상대적으로 단순한 개념으로, 이는 mark와 sweep 과정을 수행하는 쓰레드 또는 고루틴을 생성함으로써 실현됩니다. 하지만 동시성이 복잡한 만큼, 예를 들어 잘못된 객체가 해제되는 등의 문제가 발생할 수 있습니다. 다음의 케이스를 살펴보겠습니다.</p>

<p>black객체는 이미 체크를 전부 진행했기에 다시 확인하지 않습니다. tri-color abstraction의 불변식중의 하나인 “mark가 끝났으면 black객체로 부터 white객체들은 전부 접근이 불가능한 객체입니다.”를 보장하기 때문에 이게 가능하죠. 그러나, 아래 예시에서는 black 객체에 white 객체의 참조가 추가된 것을 확인할 수 있습니다.</p>

<p>// TODO: 이미지 추가</p>

<p>GC에 대한 지식이 없는 mutator 때문에 이러한 문제가 발생하는 것입니다. 불변식이 깨지게 되었기에 sweep단계에서 실제로 사용하고 있는 white 객체를 해제할 수 있습니다. 잘못 해제된 객체는 매우 큰 문제를 유발합니다. 따라서 우리는 몇가지 방법을 적용하고 있습니다.</p>

<p>write barrier라는 방법을 통하여 객체를 생성하는 순간 바로 채색할 수 있습니다. 예를들어, mark phase를 시작하면서 “지금부터 생성되는 객체는 모두 grey이야”라고 설정할 수도 있습니다. 모든 할당은 저 값이 설정되어있다면 black으로 채색된 객체를 반환할 수 있습니다. memory allocator의 도움을 받고있지만, memory allocator와 GC는 많은 연관이 되어있기에 엄청 이상한 의존성은 아닙니다.</p>

<h4 id="to-simplicity">to Simplicity</h4>

<p>Golang은 초기에 Dijkstra style write barrier만을 사용하고 있었습니다. 이 write barrier는 생성되는 객체들을 grey로 만들어서 tri-color abstraction의 불변성을 보장합니다. 하지만 stack의 경우 write barrier를 적용하면 너무 많은 오버헤드가 생겨서 write barrier를 적용하기 꺼려집니다. Go는 stack을 grey로 유지하는 것으로 이 문제를 해결했었는데, 여전히 문제는 존재했습니다.</p>

<p>mark를 진행하면서 stack이 grey를 계속 생성하기에 mark 단계를 나누어 해결하고 있었습니다. mark phase 1에서 객체들의 scan을 진행한 다음, mark phase 2에서는 STW를 설정하고 grey 객체들을 rescan하게 됩니다. 이는 mark단계가 복잡해질 뿐만 아니라 GC latency에도 좋지 않은 영향을 주고 있습니다.</p>

<p>이 문제를 해결하기 위해, Golang의 compiler/runtime 팀은 하이브리드 방식의 barrier를 사용합니다. Yuasa style deletion barrier를 사용하여, A객체에서 B객체로의 참조를 삭제하는 경우에 B객체를 grey로 만들어서 GC가 놓치지 않도록 합니다. (증명은 생략합니다) 이 방식으로 stack을 black으로 만들어서 STW가 필수적이었던 stack rescan의 필요성을 제거할 수 있습니다.</p>

<p>더욱이, mark phase 1과 mark phase 2를 별도로 유지할 필요성이 없어진 덕분에, 우리는 이를 단일 mark phase로 통합함으로써 mark 단계를 단순화할 수 있었습니다. 이 작업들로 인하여 STW 시간을 줄이면서도 mark 단계를 간결하게 유지할 수 있습니다.</p>

<hr />

<h2 id="memory-allocations">Memory Allocations</h2>

<p>GC의 목적은 동적인 메모리를 잘 관리하는 것에 있습니다. 생성되는 객체를 개발자가 직접 관리하지 않아도 되도록 하여 더욱 편한 개발이 가능해집니다. 이렇듯 GC는 메모리 할당/해제와 매우 밀접하게 연관되어있습니다. 어떻게 연관되어있는지 한번 살펴보겠습니다.</p>

<blockquote>
  <p>주의: 이 글은 GC에 관련된 글로서 메모리에 대한 내용은 개념 설명을 위하여 조금 생략되거나 강조된 내용이 있습니다.
메모리와 관련된 정확한 내용은 추후 별도의 글로 소개하겠습니다.</p>
</blockquote>

<h3 id="tcmalloc-like-allocator">TCMalloc like allocator</h3>

<p>Go의 메모리 할당은 TCMalloc과 비슷한 방식으로 구현되어 있습니다. TCMalloc은 구글에서 개발한 메모리 할당자로, T(Thread)별로 local C(Cache)를 갖고있는 메모리 할당자입니다. TCMalloc은 스레드의 캐시를 사용하여 메모리 지역성을 높이고 성능 증가를 가져옵니다. Go도 이와 거의 유사한 방식을 사용하고 있습니다.</p>

<p><img src="https://goog-perftools.sourceforge.net/doc/overview.gif" alt="TCMALLOC-MEM-OVERVIEW" /></p>

<p>TCMalloc은 작은 객체들을 cache에 할당하고 큰 객체들은 central에 할당하는 방식으로 더욱 효과적인 메모리 할당을 달성합니다. 이 글은 GC와 관련된 글이기에 추가적인 세부사항은 스킵합니다. 관심있으시다면 <a href="https://goog-perftools.sourceforge.net/doc/tcmalloc.html">tcmalloc</a>을 참고해주세요.</p>

<h3 id="ms-ps-gs-mcache--mcentral">Ms Ps Gs, mcache &amp; mcentral</h3>

<p>Go의 핵심 개념으로는 M, P, G가 있습니다. 여기서 M은 실제 OS thread, P는 goroutine을 실행하기 위한 자원들을 관리하며, G는 goroutine을 의미합니다.</p>

<p>기본적으로 goroutine은 개별 stack을 갖고있으며 최소 사이즈는 2KB입니다. 이러한 goroutine의 stack은 heap에 저장되며 여러가지 정보들이 goroutine의 stack에 저장됩니다. goroutine의 stack은 최소 사이즈에서 시작해서 더 많은 메모리가 필요하다면 사이즈를 키우게(Grow) 됩니다. 너무 많은 메모리를 점유하는 것을 방지하기 위하여 grown stack의 사용량이 너무 적다면 스택을 줄이게(Shrinking) 됩니다. 이러한 동작을 통하여 stack의 사이즈를 조정하고 stack overflow등의 문제를 보완합니다.</p>

<blockquote>
  <p>goroutine의 stack이 heap에 저장되면 성능 저하가 있을 수도 있지만 몇가지 장점들을 활용해서 이 부분을 최소화 시킵니다. stack이 heap에 비하여 빠른 이유는 지역성(locality)과 pc를 이용하여 값을 바로 가져올 수 있다는 점이 있고, goroutine의 stack도 위와 비슷하게 지역성을 어느정도 보장합니다.</p>
</blockquote>

<p>goroutine의 stack의 경우 로컬 변수, 함수 스택 프레임(parameter, return value, caller’s pc)등이 저장됩니다. 그에비하여 new, make 키워드로 할당되는 객체들이나 escape되는 객체들은 다른 곳에 할당됩니다.</p>

<p>go의 mem allocator는 TCMalloc과 비슷한 형태로, mcache(thread local cache)와 mcentral로 이루어져 있습니다. mcache는 P가 소유하고 있으며, 작은 객체들이 이곳에 할당됩니다. 다음 이미지는 대략적인 메모리 구조를 나타냅니다.</p>

<p><img src="https://github.com/MagicalLas/MagicalLas.github.io/blob/master/_screenshots/golang-gc-02.png?raw=true" alt="golang-gc-02.png" /></p>

<h3 id="non-copying-non-moving">non-copying, non-moving</h3>

<p>Go의 GC는 객체를 복사하지 않으며, 객체를 이동시키지도 않습니다. 이는 객체의 포인터가 항상 고정되는 것을 의미하는데, 여러가지 장단점이 존재합니다. 단순하다는 장점이 있지만, 일반적으로는 다음과 같은 문제를 야기합니다. 메모리를 할당하고 해제하는 과정에서 메모리 단편화가 발생할 수 있습니다. 메모리 단편화는 메모리를 할당하는 과정에서 발생하는 비효율로 인하여 실제보다 더 많은 메모리를 사용하거나, 메모리를 할당하지 못하는 문제를 이야기합니다.</p>

<p>이러한 메모리 단편화는 위에서 언급한 문제 이외에도 메모리 접근에 대한 공간적, 시간적 지역성을 떨어뜨리고 이는 메모리 cache miss 혹은 여러가지 성능 제약을 가져옵니다. 또한 잘 정렬되지 않은 메모리에서는 큰 객체를 할당하는 경우 단편화 때문에 할당할 공간이 부족해지는 문제가 발생할 수도 있습니다. 이러한 문제는 특정 애플리케이션에 따라서는 매우 심각할 수 있으며, 시스템의 중요한 부분입니다. 메모리를 과도하게 쓰거나, 메모리 때문에 성능 저하가 있다면 좋지않을 것입니다. Go는 여러가지 방법을 사용하여 이를 보완해왔습니다.</p>

<p>Go는 TCMalloc과 유사하게, 객체의 크기에 따라 메모리를 다르게 할당하는 방식을 적용하여 이를 개선합니다. 객체의 할당은 크게 3가지로 분류할 수 있습니다.</p>

<ol>
  <li>tiny</li>
  <li>small</li>
  <li>large</li>
</ol>

<p>tiny 객체들은 매우 작은 크기의 할당으로, mcache에서 tiny 객체들끼리 묶어서 관리합니다. 같이 관리되는 만큼 메모리를 절약할 수 있습니다. small 객체들은 mcache에서 적절한 span(메모리 덩어리)을 찾아서 할당합니다. 이 과정에서 span별로 bitmap을 사용해서 객체 할당 여부, 채색 여부를 관리하게 됩니다.</p>

<p>큰 객체들은 mhaep에서 직접 관리하게 합니다. 이렇듯 비슷한 객체끼리 모여있기에 객체 크기차이가 심한 메모리 단편화는 비교적 조금 발생합니다. 큰 객체들은 mhaep에서 직접 관리하기에 이러한 문제를 직접 다룰 수 있습니다.</p>

<h3 id="write-barrier">Write Barrier</h3>

<p>CMS 챕터에서 언급했다시피, mark 과정에서 메모리를 할당하게되면 잘못된 메모리를 해제하는 등의 문제가 생길 수 있기에 write barrier를 사용합니다. write barrier는 memory allocator가 GC에 대한 내용을 알고있기에 달성할 수 있습니다. write barrier는 메모리를 할당하는 과정에서 write barrier가 켜져있다면 객체를 바로 채색합니다.</p>

<p>gcalloc이라는 함수를 통하여 객체를 할당하게 됩니다. 객체 자체에도 객체의 header(첫 몇 bit)에 색을 나타내는 mark bit를 사용하고 있습니다. 이 과정에서 _GCOff상태가 아니라면 black 객체를 생성하게 됩니다. 이는 STW latency를 최소화 하기 위함입니다.</p>

<h3 id="marking-mechanism">Marking Mechanism</h3>

<p>객체 마킹 메커니즘은 특정 객체가 마크되었음을 표시하고, 이 정보를 사용하여 가비지 컬렉션을 수행하는 데 중요한 역할을 합니다. 우리는 아래의 문서를 통하여 어떻게 mark하는지 살펴볼 수 있지만, 간단하게 설명하자면 객체의 시작 몇 비트를 mark bit로 설정하여 객체의 색을 나타냅니다.</p>

<p>ref: https://go.googlesource.com/proposal/+/master/design/12800-sweep-free-alloc.md</p>

<hr />

<h2 id="triggering-gc">Triggering GC</h2>

<p>가비지 수집을 아무리 동시에 실행하고 STW가 적다고 하지만, 적절한 시기에 GC를 실행해야하는 것은 중요합니다. 너무 자주 실행하게 되면 latency가 증가하고 메모리 정리를 거의 하지 못할 수도 있습니다. 그렇다고 너무 가끔 실행하게 된다면 메모리 사용량이 너무 많아지고 mark phase가 길어질 수 있습니다. (mark는 lived object에 비례해서 시간이 걸립니다)</p>

<p>기본적으로 특정 시간이 지나면 GC를 트리거합니다. 이 시간은 하드코딩되어있는데, 특정 시간동안 GC가 실행되지 않고 가만히 있는 것을 방지합니다.</p>

<p>메모리 할당 과정에서 fast track을 실행할 수 없는 경우에도 GC의 도움을 받아 메모리를 정리합니다. 메모리를 할당하기 위하여 새로운 메모리를 얻어야 한다면, 일단 메모리는 얻으면서도 GC를 트리거하여 메모리 사용량을 줄입니다. 이렇게 할당 해제된 메모리는 반환하지 않고 free list로 들고있으면서 mcache에서 재사용합니다.</p>

<p>위의 방식은 상호보완적인 면이 있지만, 그게 전부는 아닙니다. Go는 pacer를 구현하여 더욱 적절한 타이밍에 GC를 트리거합니다.</p>

<p>Go는 GC를 조정할 수 있는 파라미터 2개를 노출하여 어느정도</p>

<hr />

<h2 id="assist-goroutine-and-cpu-limiter">Assist goroutine and CPU Limiter</h2>

<p>위를 실행하기 위하여 user goroutine의 assist를 받음</p>

<p>mark goroutine worker들을 사용함</p>

<p>CPU를 너무 많이 사용하는 것을 방지하지 위하여 CPU limiter를 구현해서 쓰고잇음. 대충 이런 비율…</p>

<hr />

<h2 id="preemption">Preemption</h2>

<p>Preemption은 특정 실행의 제어권을 넘기는 것을 의미합니다. Go로 작성된 프로그램은 여러 goroutine들이 동시에 실행되며 스케줄링되고, 어느 goroutine이 실행되다가 다른 goroutine이 자원을 선점하기도 합니다. goroutine의 제어권을 넘기는 가장 대표적인 예시는 channel을 통하는 것입니다. 다음과 같이 명시적으로 goroutine의 제어권을 넘겨 다른 goroutine이 실행되게 만들 수 있습니다.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">handoverToOtherGoroutine</span><span class="p">(</span><span class="n">c</span> <span class="o">&lt;-</span><span class="k">chan</span> <span class="k">struct</span><span class="p">{})</span> <span class="p">{</span>
	<span class="c">// some code here</span>
	<span class="o">&lt;-</span><span class="n">c</span> <span class="c">// handover</span>
	<span class="c">// some code here</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">c</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="k">chan</span> <span class="k">struct</span><span class="p">{},</span> <span class="m">0</span><span class="p">)</span>
	<span class="k">go</span> <span class="n">handoverToOtherGoroutine</span><span class="p">(</span><span class="n">c</span><span class="p">)</span> <span class="c">// goroutine A</span>

	<span class="n">c</span> <span class="o">&lt;-</span> <span class="k">struct</span><span class="p">{}</span> <span class="c">// re-excute goroutine A</span>
<span class="p">}</span>
</code></pre></div></div>

<p>이 다음에서 preemption이 왜 중요하고 제어권을 넘겨주는 다른 방법에는 어떤 것들이 있는지 더 살펴보겠습니다.</p>

<h3 id="stw-implementation-using-preemption">STW Implementation using Preemption</h3>

<p>Preemption은 STW의 구현에서 필수적입니다. STW를 실행하기 위하여는 GC를 제외한 모든 goroutine(정확히는 mutator)들의 실행을 멈추어야합니다. 이러한 goroutine의 실행을 멈추기 위하여 preemption을 사용하여 스케줄러는 goroutine을 멈추게 됩니다. 가장 간단한 방법으로는 goroutine이 제어권을 넘겨주기까지 기다리는 방식이 있습니다.</p>

<p>Preemption은 GC의 STW 외에도 goroutine 스케줄링에서 중요합니다. 만약 다음 고루틴이 실행되고 있다면 제어권을 넘겨주는 부분이 없기에 goroutine의 실행이 끝나기 전에는 다른 고루틴들이 스케줄링되지 못합니다.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">neverHandoverToOtherGoroutine</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">strings</span><span class="o">.</span><span class="n">ToUpper</span><span class="p">(</span><span class="n">bigData</span><span class="p">)</span> <span class="c">// 100GB data</span>
<span class="p">}</span>
</code></pre></div></div>

<p>많은 프로세스를 가진경우 유저가 생성한 goroutine은 작은 문제로 보일 수 있지만, GC에는 큰 문제가 됩니다. STW의 경우, 모든 mutator goroutine이 멈춰야 합니다. 이로 인해 GC의 STW latency가 증가하게 되는데, 이는 Golang의 low latency GC의 목적에 부합하지 않습니다.</p>

<h3 id="function-call-preemption">Function Call Preemption</h3>

<p>많은 goroutine의 실행은 스케줄러와 GC 모두에게 불편함을 줍니다. 따라서 Go 1.2에서 function call을 할 때 preemption check하는 기능이 추가되었습니다. goroutine이 함수를 호출하는 시점에 추가적인 check를 통하여 제어권을 넘겨주어야하는지 확인합니다.</p>

<p>이정도만 해도 어느정도 큰 문제는 발생하지 않았습니다. 대부분의 goroutine은 함수를 자주 호출하기 때문이죠. 하지만 함수를 호출하지 않는 경우, 특히 tight loop라고 불리는 경우에는 여전히 제어권을 넘겨주지 못하는 문제가 있습니다.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">tightLoopCase</span><span class="p">(</span><span class="n">image</span> <span class="p">[][]</span><span class="kt">int</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">for</span> <span class="n">y</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">image</span> <span class="p">{</span>
		<span class="k">for</span> <span class="n">x</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">image</span><span class="p">[</span><span class="n">y</span><span class="p">]</span> <span class="p">{</span>
			<span class="n">image</span><span class="p">[</span><span class="n">y</span><span class="p">][</span><span class="n">x</span><span class="p">]</span> <span class="o">+=</span> <span class="m">1</span>
		<span class="p">}</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>위 케이스처럼 함수 호출이 없이 빠르게 loop를 실행하는 경우에는 여전히 제어권을 넘겨주지 못합니다.  문제는 GC에서 특히 명확하게 나타나며, STW 동안 대부분의 goroutine들이 멈춘 반면, tight loop goroutine은 계속 실행됩니다. 이 경우 다른 goroutine들도 tight loop goroutine이 제어권을 넘겨주는 것을 기다려야합니다. GC STW latency도 증가하고, tight loop를 기다리는 동안 다른 고루틴들이 실행되지 못하니 프로그램의 throuput도 떨어지게 됩니다.</p>

<p>특정 코드를 삽입해 이 문제를 해결하는 것도 가능하지만, 이는 사용자 경험을 손상시킵니다. 또한 자기가 소유하지 않은 코드라면 코드를 삽입할 수도 없습니다.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">func</span> <span class="n">tightLoopCaseWorkAround</span><span class="p">(</span><span class="n">image</span> <span class="p">[][]</span><span class="kt">uint8</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">for</span> <span class="n">y</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">image</span> <span class="p">{</span>
		<span class="k">for</span> <span class="n">x</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">image</span><span class="p">[</span><span class="n">y</span><span class="p">]</span> <span class="p">{</span>
			<span class="n">image</span><span class="p">[</span><span class="n">y</span><span class="p">][</span><span class="n">x</span><span class="p">]</span> <span class="o">+=</span> <span class="m">1</span>
		<span class="p">}</span>
		<span class="c">// 다른 goroutine들이 스케줄링될 수 있도록 양보</span>
		<span class="n">runtime</span><span class="o">.</span><span class="n">Gosched</span><span class="p">()</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="async-preemption-aka-non-cooperative-preemption">Async Preemption (aka non-cooperative preemption)</h3>

<p>이러한 문제는 goroutine이 직접 preempt를 결정하기 때문입니다. 이와 같은 방식을 cooperative preemption이라고 합니다.</p>

<p>Go는 non-cooperative preemption라고 불리는 Async Preemption를 구현하여 scehduler가 goroutine들을 직접 멈추게 됩니다.</p>

<p>scheduler가 직접 preempt하는 경우도 있지만, 시간에 따라서 preempt하는 경우도 있습니다. Go의 경우 10ms가 지나면 다른 goroutine들이 선점될 수 있도록 하고있습니다.</p>

<h3 id="async-preemption-implementation-using-signals">Async Preemption Implementation Using Signals</h3>

<p>async preemption의 경우 스케줄러가 preempt를 실행하기 때문에 goroutine들에게 이를 알려주어야합니다. 구현은 OS마다 조금의 차이는 있지만 unix의 경우 signal을 사용해서 구현하고 있습니다. signal handler를 구현하고 특정한 signal을 주고받는 것으로 동작합니다.</p>

<p>preempt를 위한 signal은 sigurg(urgent, Socket is urgent)를 사용합니다. 잘 사용되지 않은 signal code라서 어느정도 괜찮다고 합니다.</p>

<p>하지만 signal을 받는다고해서 모든 곳에서 제어권을 넘겨줄 수 있지는 않습니다. 우리는 이러한 제어권을 넘겨줄 수 있는 부분을 safepoint라고 합니다. async preempt를 구현하는 과정에서 더 많은 safepoint를 마련해야했습니다. 따라서 몇가지 개선사항을 적용하여 safepoint를 늘리며 이런 문제를 해결하였습니다.</p>

<h3 id="to-simplicity-1">to Simplicity</h3>

<p>이 문제를 해결하기 위하여 몇가지 대안들이 고려되었습니다. 가장 간단하고 많은 언어에서 채택된 방법은 모든 loop에 preemption코드를 추가하는 것이었습니다. 이러한 방법은 바이너리 사이즈를 키우거나, 성능을 희생해야했습니다. 성능 희생을 줄이기 위한 매우 복잡한 구현이 필요한 방법도 있었지만 단순함이 철학인 언어에는 맞지 않았습니다.</p>

<p>따라서 GC 문제를 단순하게 해결하면서 사용자 경험을 해치지 않는 방식으로 문제를 해결하였습니다. 이와 관련된 내용은 다음 자료를 살펴보시면 더 자세히 알 수 있습니다.</p>

<hr />

<h2 id="golangs-implementation">Golang’s Implementation</h2>

<p>Golang의 GC 구현을 살펴보는 것으로 가장 좋은 시작지점은 src/runtime/mgc.go입니다. 이 코드를 읽어내려가다보면 ‘gcStart(trigger gcTrigger)’를 확인할 수 있습니다. 이 부분이 GC를 시작하는 부분이며, 내부 구현을 좀 더 자세히 살펴볼 수 있습니다.</p>

<p>함수의 대략적인 구현(동작)은 다음과 같습니다.</p>

<h4 id="1-가비지-컬렉션-시작-조건-확인">1. 가비지 컬렉션 시작 조건 확인</h4>

<p>이 단계는 함수 시작 부분에서 acquirem() 함수를 호출한 후에 발생합니다.</p>

<h4 id="2-가비지-컬렉션-모드-설정">2. 가비지 컬렉션 모드 설정</h4>

<p>이 단계는 debug.gcstoptheworld 값을 확인하여 가비지 컬렉션의 모드를 결정합니다. Go의 GC는 기본적으로 gcBackgroundMode로 background에서 실행되지만, debug.gcstoptheworld 값에 따라서 STW로도 실행이 가능합니다.</p>

<h4 id="3-세계를-정지시키고-스윕-단계-종료">3. 세계를 정지시키고 스윕 단계 종료</h4>

<p>“세계를 정지시키는” 작업은 semacquire(&amp;gcsema)와 semacquire(&amp;worldsema)에서 이루어집니다. semapore를 얻은 다음 stopTheWorldWithSema(reason stwReason)을 실행하여 실제로 stop the world를 실행합니다.</p>

<p>STW상태에서 finishsweep_m 함수를 호출하여 sweep 단계를 종료합니다.</p>

<h4 id="4-가비지-컬렉션-모드에-따라서-스케줄러-조정">4. 가비지 컬렉션 모드에 따라서 스케줄러 조정</h4>

<p>뒤에 나올 mark단계는 background에서 동시에 실행될 수 있습니다. 2단계에서 설정한 가비지 컬렉션 모드에 따라서 만약 background mode가 아니라면 유저의 고루틴들이 스케줄링 되는 것을 막습니다.</p>

<h4 id="5-마킹-단계-시작">5. 마킹 단계 시작</h4>

<p>마킹 단계는 GC의 phase를 _GCmark로 설정하는 것으로 시작합니다. 몇몇 mark단계 실행을 위한 준비들을 진행합니다.</p>

<p>뒤에서 소개하겠지만, 이 단계 이후부터 생성되는 객체들은 모두 black으로 칠해집니다.</p>

<h4 id="6-stw를-해제하고-mark를-진행합니다">6. STW를 해제하고, mark를 진행합니다.</h4>

<p>background worker들이 mutator root로부터 시작하여 greyobject들을 처리하기 시작합니다.</p>

<h4 id="7-가비지-컬렉션-시작의-완료-후-정리">7. 가비지 컬렉션 시작의 완료 후 정리</h4>

<p>가비지 컬렉션의 시작 완료와 이후 정리 작업이 이루어집니다. 여기서 유저의 고루틴이 다시 시작되고 필요한 세마포어(semaphore)가 해제됩니다.</p>

<p>이 시점에도 background worker들은 marking을 진행하고 있습니다.</p>

<h4 id="8-worker들이-전부-mark를-실행했고-mark단계를-끝낸다">8. worker들이 전부 mark를 실행했고, mark단계를 끝낸다</h4>

<p>marking을 진행할 것이 남았다면 다시 check하고 marking을 진행합니다.</p>

<p>전부 marking이 진행되었다면 stopTheWorldWithSema(stwGCMarkTerm)를 통하여 STW를 실행하고, GC의 phase를 _GCmarktermination로 설정합니다. mark가 전부 되었는지 확인하고나서 GC phase를 _GCoff로 변경합니다.</p>

<h4 id="9-sweep을-진행합니다">9. Sweep을 진행합니다</h4>

<p>sweep goroutine을 ready 상태로 만들고, 직접 sweep을 진행하지는 않습니다. sweep은 매우 빠르게 실행되며, heap에 할당하는 과정에서 application이 sweep을 실행하기에 낮은 우선순위로 sweep goroutine을 설정하고 끝내게 됩니다.</p>

<p>그 다음 GC를 시작하기 전에 sweep이 되었는지 체크하기 때문에, heap이 너무 커지는 문제를 막을 수 있습니다.</p>

<hr />

<h3 id="가비지-컬렉터에서-자주-쓰이는-primitive--용어들">가비지 컬렉터에서 자주 쓰이는 primitive / 용어들</h3>

<h4 id="heap">Heap</h4>

<p>힙은 컴퓨터 메모리에서 프로그램이 필요에 따라 데이터를 저장하고 제거하는 영역입니다. 이 공간은 자유롭게 사용할 수 있으며, 여러분이 변수나 객체를 생성하면 그 정보는 힙에 저장됩니다. 힙에 저장된 데이터는 프로그램이 더 이상 사용하지 않을 때, 가비지 컬렉터에 의해 제거됩니다. Heap(힙)은 연속적인 메모리 워드의 배열, 연속적인 워드의 불연속적인 블록의 집합으로 이루어집니다.</p>

<h4 id="객체">객체</h4>

<p>객체는 메모리에서 프로그램이 사용하는 데이터의 집합을 나타냅니다. 객체는 특정 유형의 데이터를 나타내며, 이 데이터는 메모리의 연속된 영역, 즉 힙(heap)에 저장됩니다. 예를 들어, ‘학생’이라는 객체는 이름, 학번, 성적 등의 정보를 포함할 수 있습니다. 이 객체가 생성되면, 해당 정보는 힙 영역에 저장되며, 프로그램은 이 영역에 있는 데이터에 액세스하고 조작할 수 있습니다.</p>

<h4 id="뮤테이터">뮤테이터</h4>

<p>쉽게 말해 뮤테이터는 프로그램의 일부분으로, 객체를 생성하고 변경하고 삭제하는 역할을 합니다. 여러분이 코딩해서 만드는 대부분의 프로그램 코드는 뮤테이터로 볼 수 있습니다.</p>

<h4 id="뮤테이터-루트">뮤테이터 루트</h4>

<p>뮤테이터 루트는 프로그램 코드(뮤테이터)가 현재 사용하고 있는, 즉 접근 가능한 모든 객체들의 ‘시작점’을 말합니다. 이는 변수나 함수 등에서 참조되는 모든 객체를 포함합니다. 가비지 컬렉터는 뮤테이터 루트를 통해 어떤 객체가 아직 필요하고 어떤 객체를 제거해도 되는지를 판단합니다.</p>

<h4 id="stop-the-world">Stop The World</h4>

<p>STW로 자주 줄여서 부르는 STW는 모든 프로그램의 실행이 중지되는 것을 의미합니다.</p>

<p>Golang에서는 Stop된 프로그램을 실행시키는 것을 Start The World라고 부르기도 합니다.</p>

<h3 id="ps">ps.</h3>

<p>긴 글을 읽어주셔서 감사합니다. 이 글은 Gophercon Korea 2023 발표에서도 사용되었기에 발표도 많은 관심 부탁드립니다.</p>

<p>개인적으로 현재 구직중입니다. 이 글을 재미있게 읽으셨고 구인중인 회사라면 <a href="https://blog.magical.dev/about">about</a>을 참고해서 메일 주시면 감사드리겠습니다.</p>]]></content><author><name>Las aka Wonho</name></author><category term="mem" /><summary type="html"><![CDATA[Golang은 GC(garbage collector)를 갖고있는 언어입니다. 이 글에서는 Golang의 GC를 자세하게 살펴보겠습니다.]]></summary></entry><entry><title type="html">Post Example With Headings And Toc</title><link href="https://blog.magical.dev/post-example-with-headings-and-toc" rel="alternate" type="text/html" title="Post Example With Headings And Toc" /><published>2020-07-09T00:00:00+00:00</published><updated>2020-07-09T00:00:00+00:00</updated><id>https://blog.magical.dev/post-example-with-headings-and-toc</id><content type="html" xml:base="https://blog.magical.dev/post-example-with-headings-and-toc"><![CDATA[<p>Mauris viverra dictum ultricies. Vestibulum quis ipsum euismod, facilisis metus sed, varius ipsum. Donec scelerisque lacus libero, eu dignissim sem venenatis at. Nunc a egestas tortor, sed feugiat leo.</p>

<h2 id="table-of-contents">Table of contents</h2>
<ul>
  <li><a href="#table-of-contents">Table of contents</a></li>
  <li><a href="#the-start">The start</a></li>
  <li><a href="#the-middle">The middle</a></li>
  <li><a href="#the-end">The end</a></li>
</ul>

<p>Mauris viverra dictum ultricies. Vestibulum quis ipsum euismod, facilisis metus sed, varius ipsum. Donec scelerisque lacus libero, eu dignissim sem venenatis at. Nunc a egestas tortor, sed feugiat leo. Vestibulum porta tincidunt tellus, vitae ornare tortor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Sed nunc neque, tempor in iaculis non, faucibus et metus. Etiam id nisl ut lorem gravida euismod.</p>

<h2 id="the-start"><a href="#the-start">The start</a></h2>

<p>Fusce non velit cursus ligula mattis convallis vel at metus. Sed pharetra tellus massa, non elementum eros vulputate non. Suspendisse potenti. Quisque arcu felis, laoreet vel accumsan sit amet, fermentum at nunc. Sed massa quam, auctor in eros quis, porttitor tincidunt orci. Nulla convallis id sapien ornare viverra. Cras nec est lacinia ligula porta tincidunt. Nam a est eget ligula pellentesque posuere. Maecenas quis enim ac risus accumsan scelerisque. Aliquam vitae libero sapien. Etiam convallis, metus nec suscipit condimentum, quam massa congue velit, sit amet sollicitudin nisi tortor a lectus. Cras a arcu enim. Suspendisse hendrerit euismod est ac gravida. Donec vitae elit tristique, suscipit eros at, aliquam augue. In ac faucibus dui. Sed tempor lacus tristique elit sagittis, vitae tempor massa convallis.</p>

<h2 id="the-middle"><a href="#the-middle">The middle</a></h2>

<p>Proin quis velit et eros auctor laoreet. Aenean eget nibh odio. Suspendisse mollis enim pretium, fermentum urna vitae, egestas purus. Donec convallis tincidunt purus, scelerisque fermentum eros sagittis vel. Aliquam ac aliquet risus, tempus iaculis est. Fusce molestie mauris non interdum hendrerit. Curabitur ullamcorper, eros vitae interdum volutpat, lacus magna lacinia turpis, at accumsan dui tortor vel lectus. Aenean risus massa, semper non lectus rutrum, facilisis imperdiet mi. Praesent sed quam quis purus auctor ornare et sed augue. Vestibulum non quam quis ligula luctus placerat sed sit amet erat. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Fusce auctor, sem eu volutpat dignissim, turpis nibh malesuada arcu, in consequat elit mauris quis sem. Nam tristique sit amet enim vel accumsan. Sed id nibh commodo, dictum sem id, semper quam.</p>

<h2 id="the-end">The end</h2>

<p>Donec ex lectus, tempus non lacinia quis, pretium non ipsum. Praesent est nunc, rutrum vel tellus eu, tristique laoreet purus. In rutrum orci sit amet ex ornare, sit amet finibus lacus laoreet. Etiam ac facilisis purus, eget porttitor odio. Suspendisse tempus dolor nec risus sodales posuere. Proin dui dui, mollis a consectetur molestie, lobortis vitae tellus. Vivamus at purus sed urna sollicitudin mattis. Mauris lacinia libero in lobortis pulvinar. Nullam sit amet condimentum justo. Donec orci justo, pharetra ut dolor non, interdum finibus orci. Proin vitae ante a dui sodales commodo ac id elit. Nunc vel accumsan nunc, sit amet congue nunc. Aliquam in lacinia velit. Integer lobortis luctus eros, in fermentum metus aliquet a. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.</p>]]></content><author><name>Las aka Wonho</name></author><category term="example2" /><summary type="html"><![CDATA[Mauris viverra dictum ultricies. Vestibulum quis ipsum euismod, facilisis metus sed, varius ipsum. Donec scelerisque lacus libero, eu dignissim sem venenatis at. Nunc a egestas tortor, sed feugiat leo.]]></summary></entry><entry><title type="html">Post Example With Hr</title><link href="https://blog.magical.dev/hr-example" rel="alternate" type="text/html" title="Post Example With Hr" /><published>2020-07-09T00:00:00+00:00</published><updated>2020-07-09T00:00:00+00:00</updated><id>https://blog.magical.dev/hr%20example</id><content type="html" xml:base="https://blog.magical.dev/hr-example"><![CDATA[<p>Lorem ipsum<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> dolor sit amet, consectetur adipiscing elit. Pellentesque vel lacinia neque. Praesent nulla quam, ullamcorper in sollicitudin ac, molestie sed justo. Cras aliquam, sapien id consectetur accumsan, augue magna faucibus ex, ut ultricies turpis tortor vel ante. In at rutrum tellus. Nullam vestibulum metus eu purus malesuada, volutpat mattis leo facilisis. Sed consectetur, nisl et semper laoreet, velit augue congue nunc, eget eleifend odio erat eu sapien. Phasellus dictum efficitur dapibus. Morbi porta lacinia tincidunt. Nam aliquet est mi, nec lacinia ipsum elementum sed. Nam feugiat ipsum tortor, et pretium purus sollicitudin et.</p>

<hr />

<p>Mauris viverra dictum ultricies<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>. Vestibulum<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup> quis ipsum euismod, facilisis metus sed, varius ipsum. Donec scelerisque lacus libero, eu dignissim sem venenatis at. Nunc a egestas tortor, sed feugiat leo. Vestibulum porta tincidunt tellus, vitae ornare tortor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Sed nunc neque, tempor in iaculis non, faucibus et metus. Etiam id nisl ut lorem gravida euismod.</p>

<p>Fusce non velit cursus ligula mattis convallis vel at metus. Sed pharetra tellus massa, non elementum eros vulputate non. Suspendisse potenti. Quisque arcu felis, laoreet vel accumsan sit amet, fermentum at nunc. Sed massa quam, auctor in eros quis, porttitor tincidunt orci. Nulla convallis id sapien ornare viverra. Cras nec est lacinia ligula porta tincidunt. Nam a est eget ligula pellentesque posuere. Maecenas quis enim ac risus accumsan scelerisque. Aliquam vitae libero sapien. Etiam convallis, metus nec suscipit condimentum, quam massa congue velit, sit amet sollicitudin nisi tortor a lectus. Cras a arcu enim. Suspendisse hendrerit euismod est ac gravida. Donec vitae elit tristique, suscipit eros at, aliquam augue. In ac faucibus dui. Sed tempor lacus tristique elit sagittis, vitae tempor massa convallis.</p>

<hr data-content="discussions" />

<p>This article has been discussed here:</p>
<ul>
  <li><a href="#">lobste.rs</a></li>
  <li><a href="#">/r/webdev</a></li>
</ul>

<p>Feel free to reach out at my email to leave feedback and talk about the article.</p>

<hr data-content="footnotes" />

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>Okay here I should put something about “ipsum”. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p>same goes for this. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p>I studied latin in high school but im not able to translate <em>anything</em>! By the way this is a longer footnote and i think it is still pretty cool, even prettier than shortier ones even though it does not say anything useful but whatever. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Las aka Wonho</name></author><summary type="html"><![CDATA[Lorem ipsum1 dolor sit amet, consectetur adipiscing elit. Pellentesque vel lacinia neque. Praesent nulla quam, ullamcorper in sollicitudin ac, molestie sed justo. Cras aliquam, sapien id consectetur accumsan, augue magna faucibus ex, ut ultricies turpis tortor vel ante. In at rutrum tellus. Nullam vestibulum metus eu purus malesuada, volutpat mattis leo facilisis. Sed consectetur, nisl et semper laoreet, velit augue congue nunc, eget eleifend odio erat eu sapien. Phasellus dictum efficitur dapibus. Morbi porta lacinia tincidunt. Nam aliquet est mi, nec lacinia ipsum elementum sed. Nam feugiat ipsum tortor, et pretium purus sollicitudin et. Okay here I should put something about “ipsum”. &#8617;]]></summary></entry><entry><title type="html">Overview Post</title><link href="https://blog.magical.dev/overview-post" rel="alternate" type="text/html" title="Overview Post" /><published>2020-07-07T00:00:00+00:00</published><updated>2020-07-07T00:00:00+00:00</updated><id>https://blog.magical.dev/overview-post</id><content type="html" xml:base="https://blog.magical.dev/overview-post"><![CDATA[<p>Lorem ipsum<sup id="fnref:1" role="doc-noteref"><a href="#fn:1" class="footnote" rel="footnote">1</a></sup> dolor sit amet, consectetur adipiscing elit. Pellentesque vel lacinia neque. Praesent nulla quam, ullamcorper in sollicitudin ac, molestie sed justo. Cras aliquam, sapien id consectetur accumsan, augue magna faucibus ex, ut ultricies turpis tortor vel ante. In at rutrum tellus.</p>

<h1 id="sample-heading-1">Sample heading 1</h1>
<h2 id="sample-heading-2">Sample heading 2</h2>
<h3 id="sample-heading-3">Sample heading 3</h3>
<h4 id="sample-heading-4">Sample heading 4</h4>
<h5 id="sample-heading-5">Sample heading 5</h5>
<h6 id="sample-heading-6">Sample heading 6</h6>

<p>Mauris viverra dictum ultricies. Vestibulum quis ipsum euismod, facilisis metus sed, varius ipsum. Donec scelerisque lacus libero, eu dignissim sem venenatis at. Etiam id nisl ut lorem gravida euismod.</p>

<h2 id="lists">Lists</h2>

<p>Unordered:</p>

<ul>
  <li>Fusce non velit cursus ligula mattis convallis vel at metus<sup id="fnref:2" role="doc-noteref"><a href="#fn:2" class="footnote" rel="footnote">2</a></sup>.</li>
  <li>Sed pharetra tellus massa, non elementum eros vulputate non.</li>
  <li>Suspendisse potenti.</li>
</ul>

<p>Ordered:</p>

<ol>
  <li>Quisque arcu felis, laoreet vel accumsan sit amet, fermentum at nunc.</li>
  <li>Sed massa quam, auctor in eros quis, porttitor tincidunt orci.</li>
  <li>Nulla convallis id sapien ornare viverra.</li>
  <li>Nam a est eget ligula pellentesque posuere.</li>
</ol>

<h2 id="blockquote">Blockquote</h2>

<p>The following is a blockquote:</p>

<blockquote>
  <p>Suspendisse tempus dolor nec risus sodales posuere. Proin dui dui, mollis a consectetur molestie, lobortis vitae tellus.</p>
</blockquote>

<h2 id="thematic-breaks-hr">Thematic breaks (&lt;hr&gt;)</h2>

<p>Mauris viverra dictum ultricies<sup id="fnref:3" role="doc-noteref"><a href="#fn:3" class="footnote" rel="footnote">3</a></sup>. Vestibulum quis ipsum euismod, facilisis metus sed, varius ipsum. Donec scelerisque lacus libero, eu dignissim sem venenatis at. Etiam id nisl ut lorem gravida euismod. <strong>You can put some text inside the horizontal rule like so.</strong></p>

<hr data-content="hr with text" />

<p>Mauris viverra dictum ultricies. Vestibulum quis ipsum euismod, facilisis metus sed, varius ipsum. Donec scelerisque lacus libero, eu dignissim sem venenatis at. Etiam id nisl ut lorem gravida euismod. <strong>Or you can just have an clean horizontal rule.</strong></p>

<hr />

<p>Mauris viverra dictum ultricies. Vestibulum quis ipsum euismod, facilisis metus sed, varius ipsum. Donec scelerisque lacus libero, eu dignissim sem venenatis at. Etiam id nisl ut lorem gravida euismod. Or you can just have an clean horizontal rule.</p>

<h2 id="code">Code</h2>

<p>Now some code:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>const ultimateTruth = 'this theme is the best!';
console.log(ultimateTruth);
</code></pre></div></div>

<p>And here is some <code class="language-plaintext highlighter-rouge">inline code</code>!</p>

<h2 id="tables">Tables</h2>

<p>Now a table:</p>

<table>
  <thead>
    <tr>
      <th>Tables</th>
      <th style="text-align: center">Are</th>
      <th style="text-align: right">Cool</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>col 3 is</td>
      <td style="text-align: center">right-aligned</td>
      <td style="text-align: right">$1600</td>
    </tr>
    <tr>
      <td>col 2 is</td>
      <td style="text-align: center">centered</td>
      <td style="text-align: right">$12</td>
    </tr>
    <tr>
      <td>zebra stripes</td>
      <td style="text-align: center">are neat</td>
      <td style="text-align: right">$1</td>
    </tr>
  </tbody>
</table>

<h2 id="images">Images</h2>

<p><img src="https://raw.githubusercontent.com/riggraz/no-style-please/master/logo.png" alt="theme logo" class="ioda" /></p>

<p>Logo of <em>no style, please!</em> theme<sup id="fnref:4" role="doc-noteref"><a href="#fn:4" class="footnote" rel="footnote">4</a></sup></p>

<hr data-content="footnotes" />

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1" role="doc-endnote">
      <p>this is a footnote. It should highlight if you click on the corresponding superscript number. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:2" role="doc-endnote">
      <p>hey there, i’m using no style please! <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:3" role="doc-endnote">
      <p>this is another footnote. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:4" role="doc-endnote">
      <p>this is a very very long footnote to test if a very very long footnote brings some problems or not. I strongly hope that there are no problems but you know sometimes problems arise from nowhere. <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Las aka Wonho</name></author><category term="example" /><summary type="html"><![CDATA[Lorem ipsum1 dolor sit amet, consectetur adipiscing elit. Pellentesque vel lacinia neque. Praesent nulla quam, ullamcorper in sollicitudin ac, molestie sed justo. Cras aliquam, sapien id consectetur accumsan, augue magna faucibus ex, ut ultricies turpis tortor vel ante. In at rutrum tellus. this is a footnote. It should highlight if you click on the corresponding superscript number. &#8617;]]></summary></entry></feed>