<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Icarus</title>
    <link>https://icarus8050.tistory.com/</link>
    <description>e-mail :icarus8050@naver.com

Github :https://github.com/icarus8050

Blog (이전 블로그) :https://blog.naver.com/icarus8050</description>
    <language>ko</language>
    <pubDate>Thu, 9 Apr 2026 07:55:50 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Icarus8050</managingEditor>
    <image>
      <title>Icarus</title>
      <url>https://tistory1.daumcdn.net/tistory/3179802/attach/de7a768f4f3442b2a9aacb9dcafc3fb6</url>
      <link>https://icarus8050.tistory.com</link>
    </image>
    <item>
      <title>Paxos 합의 알고리즘</title>
      <link>https://icarus8050.tistory.com/167</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Paxos는 Leslie Lamport가 1989년에 제안한 합의 알고리즘으로, 이런 환경에서도 &lt;b&gt;단 하나의 값만 안전하게 결정&lt;/b&gt;되도록 보장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Raft, Kafka(KRaft), etcd 등 현대 분산 시스템의 합의 메커니즘을 이해하려면, 그 원조인 Paxos를 먼저 이해하는 것이 중요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결하려는 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;분산 환경에서 노드들이 &quot;이 값을 X로 하자&quot;라고 제안할 때, 일부 노드가 다운되거나 메시지가 지연되더라도 &lt;b&gt;단 하나의 값만 최종 결정&lt;/b&gt;되어야 한다. 두 노드가 동시에 다른 값을 제안하더라도, 시스템 전체는 반드시 하나만 선택해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;세 가지 역할&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Paxos에는 세 가지 역할이 있으며, 하나의 노드가 여러 역할을 동시에 수행할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;Client &amp;rarr; Proposer &amp;rarr; Acceptor(A, B, C) &amp;rarr; Learner
         (값 제안)     (투표/수락)         (결과 학습)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Proposer&lt;/b&gt;는 클라이언트의 요청을 받아 &quot;이 값을 합의하자&quot;고 제안을 시작하는 역할이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Acceptor&lt;/b&gt;는 제안을 수락하거나 거부하는 투표자 역할이다. 과반수(majority)의 Acceptor가 수락해야 값이 결정된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Learner&lt;/b&gt;는 최종 결정된 값을 전달받아 학습하는 역할이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;두 단계 프로토콜&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Paxos는 &lt;b&gt;Prepare-Promise&lt;/b&gt;와 &lt;b&gt;Accept-Accepted&lt;/b&gt; 두 단계로 동작한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1단계: Prepare &amp;rarr; Promise&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Proposer가 고유한 제안 번호(proposal number) N을 선택하고, 모든 Acceptor에게 Prepare(N) 메시지를 보낸다. 이것은 &quot;나 N번 제안을 하려고 하는데, 받아줄 수 있어?&quot;라는 요청이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Acceptor는 N이 자신이 지금까지 본 제안 번호 중 가장 크면, 두 가지를 약속한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;N보다 작은 번호의 제안은 앞으로 수락하지 않겠다.&lt;/li&gt;
&lt;li&gt;이전에 수락했던 제안이 있으면 그 정보(제안 번호와 값)를 Proposer에게 돌려준다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 응답이 Promise이다. 만약 N이 이미 본 번호보다 작으면, Acceptor는 무시하거나 거부한다.&lt;/p&gt;
&lt;pre class=&quot;gherkin&quot;&gt;&lt;code&gt;Proposer                    Acceptor A, B, C
   |                              |
   |--- Prepare(N=1) -----------&amp;gt;|
   |                              | N=1 &amp;gt; 0 이므로 OK
   |&amp;lt;-- Promise(N=1) ------------|
   |   (이전 수락 값 없음)          |
   |   과반수 Promise 수신!        |
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2단계: Accept &amp;rarr; Accepted&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Proposer가 과반수의 Acceptor로부터 Promise를 받으면, 다음 단계로 진행한다. 이때 &lt;b&gt;제안할 값을 결정하는 규칙&lt;/b&gt;이 핵심이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Promise 응답 중에 이전에 수락된 값이 있으면, &lt;b&gt;가장 높은 제안 번호에 연결된 값&lt;/b&gt;을 자신의 제안 값으로 사용해야 한다.&lt;/li&gt;
&lt;li&gt;이전에 수락된 값이 하나도 없으면, 자신이 원래 제안하려던 값을 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Proposer가 Accept(N, value) 메시지를 Acceptor들에게 보내고, Acceptor는 그 사이에 N보다 큰 번호의 Prepare를 받지 않았다면 이 제안을 수락한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과반수의 Acceptor가 수락하면, 해당 값이 **최종 결정(chosen)**된다.&lt;/p&gt;
&lt;pre class=&quot;gherkin&quot;&gt;&lt;code&gt;Proposer                    Acceptor A, B, C
   |                              |
   |--- Accept(N=1, V=&quot;X&quot;) -----&amp;gt;|
   |                              | N=1이 아직 유효하므로 수락
   |&amp;lt;-- Accepted(N=1) -----------|
   |   과반수 Accepted!           |
   |   &quot;X&quot;가 최종 결정됨            |
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;충돌 시나리오: 두 Proposer가 동시에 제안할 때&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적인 경우는 단순하지만, 핵심은 &lt;b&gt;충돌이 발생했을 때 어떻게 안전성을 보장하는가&lt;/b&gt;이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시나리오: P1이 &quot;X&quot;를, P2가 &quot;Y&quot;를 제안&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Step 1.&lt;/b&gt; P1이 Prepare(N=1)을 A, B에게 보내고 과반수 Promise를 확보한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Step 2.&lt;/b&gt; P1이 Accept(1, &quot;X&quot;)를 보내기 전에, P2가 Prepare(N=2)를 B, C에게 보내서 과반수 Promise를 확보한다. B는 이제 N=2에 Promise했으므로 N=1 이하의 제안을 수락하지 않겠다고 약속한 상태다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Step 3.&lt;/b&gt; P1이 Accept(1, &quot;X&quot;)를 보내면, A는 수락하지만 &lt;b&gt;B는 거부&lt;/b&gt;한다 (이미 N=2에 Promise했으므로). P1은 과반수 수락에 실패한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Step 4.&lt;/b&gt; P2가 Accept(2, &quot;Y&quot;)를 B, C에게 보내면, 둘 다 수락한다. 과반수 달성으로 &lt;b&gt;&quot;Y&quot;가 최종 결정&lt;/b&gt;된다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;시간 &amp;rarr;

P1: Prepare(1) &amp;rarr; A,B Promise   Accept(1,&quot;X&quot;) &amp;rarr; A 수락, B 거부! &amp;rarr; 실패
P2:              Prepare(2) &amp;rarr; B,C Promise     Accept(2,&quot;Y&quot;) &amp;rarr; B,C 수락 &amp;rarr; &quot;Y&quot; 결정!

핵심: 제안 번호가 더 큰 쪽이 이긴다.
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;안전성의 핵심: 이전에 수락된 값 이어받기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 값이 결정된 후 새로운 Proposer가 나타나면 어떻게 될까?&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;시나리오: &quot;Y&quot;가 결정된 후 P3가 &quot;Z&quot;를 제안하려는 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B와 C가 Accept(2, &quot;Y&quot;)를 수락하여 &quot;Y&quot;가 결정된 상태에서, P3가 Prepare(N=3)을 모든 Acceptor에게 보낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 Acceptor는 Promise와 함께 &lt;b&gt;이전에 수락했던 값&lt;/b&gt;을 돌려준다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;A: Promise(3) + 이전 수락 정보 (1, &quot;X&quot;)&lt;/li&gt;
&lt;li&gt;B: Promise(3) + 이전 수락 정보 (2, &quot;Y&quot;)&lt;/li&gt;
&lt;li&gt;C: Promise(3) + 이전 수락 정보 (2, &quot;Y&quot;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;P3는 이 중에서 &lt;b&gt;가장 높은 제안 번호(2)에 연결된 값 &quot;Y&quot;&lt;/b&gt;를 자신의 제안 값으로 사용해야 한다. 원래 &quot;Z&quot;를 제안하고 싶었지만, Accept(3, &quot;Y&quot;)를 보내야 한다.&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;P3가 받은 Promise 응답:
  A &amp;rarr; (1, &quot;X&quot;)
  B &amp;rarr; (2, &quot;Y&quot;)
  C &amp;rarr; (2, &quot;Y&quot;)

규칙: max(proposal number)에 연결된 값 선택
  max = 2 &amp;rarr; 값 = &quot;Y&quot;

&amp;there4; P3는 Accept(3, &quot;Y&quot;)를 전송 &amp;rarr; &quot;Y&quot;가 다시 확인됨
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 규칙이 Paxos 안전성의 핵심이다. &lt;b&gt;과반수끼리는 반드시 최소 하나의 Acceptor가 겹치기 때문에&lt;/b&gt;, 새로운 Proposer는 항상 이전에 결정된 값을 발견하게 된다. 한번 결정된 값은 절대 바뀌지 않는다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Paxos 알고리즘의 한계&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Livelock - Safety는 보장하지만, Liveness는 보장하지 못한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Basic Paxos를 그대로 사용하면 트래픽이 큰 시스템에서 심각한 성능 문제가 발생한다. Proposer 간 충돌이 반복되면서 라이브락(livelock)이 발생할 수 있기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 P1이 Prepare(1)로 과반수 Promise를 받고 Accept를 보내려는 순간, P2가 Prepare(2)를 보내서 Acceptor들의 약속을 덮어쓴다. P1이 실패하고 다시 Prepare(3)을 보내면, 이번엔 P2의 Accept가 거부된다. 이렇게 서로 끊임없이 상대방을 무효화하면서 &lt;b&gt;아무도 값을 결정하지 못하는 상태&lt;/b&gt;가 반복될 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;Proposer A: prepare(n=1) &amp;rarr; 과반 Promise 획득
Proposer B: prepare(n=2) &amp;rarr; 과반 Promise 획득 (A의 n=1 무효화)
Proposer A: accept(n=1)  &amp;rarr; 거절됨 &amp;rarr; prepare(n=3) 재시도
Proposer B: accept(n=2)  &amp;rarr; 거절됨 &amp;rarr; prepare(n=4) 재시도
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 성능 한계&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2라운드 메시지 교환&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Basic Paxos에서 하나의 값에 합의하려면 최소 &lt;b&gt;2라운드의 메시지 교환&lt;/b&gt;이 필요하다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;less&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;Round 1: Proposer &amp;rarr; Acceptor  (Prepare)
         Acceptor &amp;rarr; Proposer  (Promise)

Round 2: Proposer &amp;rarr; Acceptor  (Accept)
         Acceptor &amp;rarr; Proposer  (Accepted)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;각 라운드는 네트워크 왕복(RTT) 1회와 Acceptor 측의 디스크 fsync를 수반한다. 합의 한 건당 &lt;b&gt;2 RTT + 2 fsync&lt;/b&gt;가 최소 비용이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Multi-Paxos에서 안정적인 Leader가 확보되면 Prepare 단계를 생략하여 1라운드로 줄일 수 있다. 하지만 Leader가 교체되면 다시 2라운드로 돌아가며, Leader 교체가 잦은 불안정한 환경에서는 이 최적화의 효과가 반감된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Proposer 충돌에 의한 Throughput 저하&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Paxos의 기본 구조에서는 &lt;b&gt;모든 노드가 Proposer가 될 수 있다.&lt;/b&gt; 이론적으로는 유연하지만, 실제로 여러 Proposer가 동시에 활동하면 충돌이 잦아지고 재시도가 늘어나면서 throughput이 급격히 떨어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;지리적 분산 환경에서의 Latency&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Quorum 기반 알고리즘의 특성상, 과반수 응답을 기다려야 한다. 노드가 서울, 도쿄, 버지니아에 분산되어 있다면, 가장 느린 quorum 멤버의 RTT가 전체 합의 latency를 결정한다. 이 문제는 Paxos에만 국한되지 않지만, 2라운드 구조가 latency를 더 증폭시킨다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결책: Multi-Paxos과 Raft&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 문제를 해결하기 위한 아이디어는 &lt;b&gt;리더를 하나 선출하고, 리더만 Proposer 역할을 하도록 제한&lt;/b&gt;하는 것이다. 이 아이디어를 Paxos에 적용한 것이 Multi-Paxos이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;리더가 안정적으로 유지되는 동안에는 Prepare 단계를 매번 반복할 필요가 없다. 리더가 한 번 Prepare로 과반수의 Promise를 확보하면, 이후의 요청들은 Accept 단계만으로 처리할 수 있다. &lt;b&gt;2단계 프로토콜이 사실상 1단계로 줄어드는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;[Basic Paxos]
  매 요청마다: Prepare &amp;rarr; Promise &amp;rarr; Accept &amp;rarr; Accepted  (2 RTT)

[Multi-Paxos (리더 안정 시)]
  최초 1회: Prepare &amp;rarr; Promise
  이후:     Accept &amp;rarr; Accepted  (1 RTT)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;이 구조에서는 충돌이 원천적으로 발생하지 않는다. 클라이언트 요청이 모두 리더를 통해 직렬화되기 때문이다. 리더가 다운되었을 때만 새 리더를 선출하고, 그 과정에서 잠깐의 충돌이 있을 수 있지만 정상 운영 중에는 충돌 없이 높은 처리량을 유지한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Raft는 Multi-Paxos의 리더 중심 구조를 프로토콜 자체에 내장하면서, 리더 선출, 로그 복제, 안전성을 하나의 통합된 프로토콜로 명확하게 정의했다. &quot;이해하기 쉬운 합의 알고리즘&quot;을 목표로 설계되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;현재 대부분의 분산 시스템은 Raft를 선택한다. etcd, Consul, CockroachDB, TiKV, Kafka(KRaft) 등이 모두 Raft 기반이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 정리&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Paxos는 두 단계 프로토콜이다.&lt;/b&gt; Prepare-Promise로 제안 권한을 확보하고, Accept-Accepted로 값을 결정한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;제안 번호가 안전성의 기반이다.&lt;/b&gt; 더 큰 번호의 Prepare가 오면 이전 약속이 무효화되어, 항상 최신 제안이 우선한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이전에 수락된 값을 이어받는 규칙이 핵심이다.&lt;/b&gt; Promise 응답에서 가장 높은 제안 번호의 값을 사용해야 하므로, 한번 결정된 값은 절대 바뀌지 않는다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;과반수의 교집합이 안전성을 보장한다.&lt;/b&gt; 어떤 두 과반수든 최소 하나의 Acceptor가 겹치므로, 결정된 값은 반드시 새로운 Proposer에게 전달된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Basic Paxos는 실무에서 그대로 쓰기 어렵다.&lt;/b&gt; 라이브락 문제 때문에 Multi-Paxos(리더 선출)로 확장하여 사용하며, Raft는 이 구조를 처음부터 프로토콜에 내장한 것이다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Development/Architecture</category>
      <author>Icarus8050</author>
      <guid isPermaLink="true">https://icarus8050.tistory.com/167</guid>
      <comments>https://icarus8050.tistory.com/167#entry167comment</comments>
      <pubDate>Sat, 4 Apr 2026 13:49:00 +0900</pubDate>
    </item>
    <item>
      <title>Raft Figure 8 문제 파헤치기 - &amp;quot;과반수 복제 = 커밋&amp;quot;이 위험한 이유</title>
      <link>https://icarus8050.tistory.com/166</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;분산 시스템에서 합의(Consensus) 알고리즘은 여러 노드가 하나의 일관된 상태를 유지하도록 보장하는 핵심 메커니즘이다. Raft는 이해하기 쉬운 합의 알고리즘을 목표로 설계되었지만, 그 안에는 깊이 생각하지 않으면 놓칠 수 있는 미묘한 안전성 문제가 숨어 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그중 대표적인 것이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Figure 8 문제&lt;/b&gt;다. 이 글에서는 Raft 논문의 Figure 8이 보여주는 시나리오를 단계별로 분석하고, 해결 규칙이 왜 필요한지, 그리고 이것이 Kafka(KRaft)와 어떻게 다른지까지 살펴본다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Raft의 기본 커밋 규칙&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Raft에서 로그 엔트리가 커밋되려면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;과반수(majority) 노드에 복제&lt;/b&gt;되어야 한다. 리더가 엔트리를 기록하고, 팔로워들에게 AppendEntries RPC로 복제한 뒤, 과반수 이상이 응답하면 커밋으로 간주한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 규칙은 직관적이고 단순하다. 하지만&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&quot;이전 텀의 엔트리를 새 리더가 복제하는 경우&quot;&lt;/b&gt;에는 이 규칙만으로 안전성을 보장할 수 없다. Figure 8이 바로 이 상황을 보여준다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Figure 8 시나리오 분석&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;5개의 노드(S1~S5)가 있는 클러스터를 가정한다. 각 단계에서 어떤 일이 벌어지는지 살펴보자.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;(a) 텀 2 &amp;mdash; S1이 리더&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;S1이 리더로 선출되어 인덱스 2에 텀 2 엔트리를 기록한다. S2에게만 복제한 뒤 S1이 다운된다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;         idx 1    idx 2
S1 ★   [  1  ]  [  2  ]    &amp;larr; 리더, 다운됨
S2     [  1  ]  [  2  ]    &amp;larr; 복제 받음
S3     [  1  ]
S4     [  1  ]
S5     [  1  ]&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 시점에서 인덱스 2(텀 2)는 S1, S2 두 곳에만 있다. 과반수(3)에 도달하지 못했으므로 미커밋 상태다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;(b) 텀 3 &amp;mdash; S5가 리더&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;S5가 새 리더로 선출되어 인덱스 2에 텀 3 엔트리를 기록한다. 아직 어디에도 복제하지 않은 상태다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;         idx 1    idx 2
S1     [  1  ]  [  2  ]    &amp;larr; 다운 상태
S2     [  1  ]  [  2  ]
S3     [  1  ]
S4     [  1  ]
S5 ★   [  1  ]  [  3  ]    &amp;larr; 새 리더&lt;/code&gt;&lt;/pre&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;(c) 텀 4 &amp;mdash; S1이 다시 리더&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;S1이 복귀하여 텀 4의 리더가 된다. S1은 인덱스 3에 텀 4 엔트리를 기록한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때 S3의 로그가 뒤처져 있으므로 AppendEntries의 일관성 검사(consistency check) 과정에서 nextIndex가 줄어들면서, 인덱스 2의 텀 2 엔트리가 S3에 복제된다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;         idx 1    idx 2    idx 3
S1 ★   [  1  ]  [  2  ]  [  4  ]    &amp;larr; 리더
S2     [  1  ]  [  2  ]
S3     [  1  ]  [  2  ]              &amp;larr; 복제 받음!
S4     [  1  ]
S5     [  1  ]  [  3  ]&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;인덱스 2(텀 2)가 S1, S2, S3 세 곳에 존재한다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;과반수를 달성했다!&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;만약 여기서 &quot;과반수에 복제되었으니 커밋&quot;이라고 판단하면 어떻게 될까?&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;(d) 문제 발생 &amp;mdash; S5가 텀 5 리더&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;S1이 다시 다운되고, S5가 텀 5의 리더로 선출된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;S5가 리더로 선출될 수 있는 이유가 중요하다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Raft의 투표 규칙은 마지막 로그의 텀을 먼저 비교&lt;/b&gt;한다. S5의 마지막 로그 텀은 3이고, S2와 S3의 마지막 로그 텀은 2다. 텀 3 &amp;gt; 텀 2이므로 S5가 &quot;더 최신&quot;으로 판단되어 투표를 받을 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;S5가 리더가 되면 자신의 로그가 진실(truth)이 된다. S5의 인덱스 2에는 텀 3 엔트리가 있으므로, S1, S2, S3의 텀 2 엔트리는 텀 3 엔트리로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;덮어씌워진다&lt;/b&gt;.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;         idx 1    idx 2    idx 3
S1     [  1  ]  [  2  ]  [  4  ]    &amp;larr; 다운, 복귀 시 덮어씌워짐
S2     [  1  ]  [  3  ]             &amp;larr; 덮어씌워짐!
S3     [  1  ]  [  3  ]             &amp;larr; 덮어씌워짐!
S4     [  1  ]  [  3  ]             &amp;larr; 덮어씌워짐!
S5 ★   [  1  ]  [  3  ]             &amp;larr; 리더&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(c)에서 커밋되었다고 판단한 인덱스 2(텀 2) 엔트리가 사라졌다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이것은 Raft의 핵심 안전성 속성인&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&quot;커밋된 로그는 절대 유실되지 않는다&quot;를 정면으로 위반&lt;/b&gt;한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;해결 규칙: 이전 텀의 엔트리를 직접 커밋하지 않는다&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 문제를 방지하기 위해 Raft는 다음 규칙을 도입한다.&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #000000; color: #333333; text-align: center;&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p style=&quot;color: #666666;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Raft는 이전 텀의 로그 엔트리를 복제본 수를 세는 방식으로 커밋하지 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;새 리더는 반드시&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;현재 텀의 엔트리&lt;/b&gt;를 로그에 추가하고, 이것을 과반수에 복제하여 커밋해야 한다. Raft의 로그는 순차적이므로, 현재 텀의 엔트리가 커밋되면 그 앞의 이전 텀 엔트리들도 간접적으로 커밋된다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;no-op: 현재 텀의 빈 엔트리&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;새 리더가 취임 직후 사용하는 &quot;현재 텀의 엔트리&quot;를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;no-op(no operation)&lt;/b&gt;이라 한다. 말 그대로 아무 작업도 하지 않는 빈 엔트리지만, 현재 텀 번호가 찍혀 있다는 것이 핵심이다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;(e) 규칙 적용 후 &amp;mdash; 안전한 시나리오&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;(c)에서 S1은 이전 텀(텀 2)의 인덱스 2를 직접 커밋하지 않는다. 대신 인덱스 3에 텀 4 엔트리를 과반수(S1, S2, S3)에 복제한다.&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;         idx 1    idx 2    idx 3
S1 ★   [  1  ]  [  2  ]  [  4  ]    &amp;larr; 리더
S2     [  1  ]  [  2  ]  [  4  ]    &amp;larr; 텀 4 복제 완료
S3     [  1  ]  [  2  ]  [  4  ]    &amp;larr; 텀 4 복제 완료
S4     [  1  ]
S5     [  1  ]  [  3  ]&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;텀 4 엔트리가 과반수에 복제되어 커밋되면, 로그 순서에 의해 인덱스 2(텀 2)도 간접 커밋된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 상태에서 S5가 리더에 도전하면 어떻게 될까? S2, S3의 마지막 로그 텀은 4이고, S5의 마지막 로그 텀은 3이다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;텀 4 &amp;gt; 텀 3이므로 S2, S3은 S5에게 투표하지 않는다.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;S5는 과반수 투표를 받을 수 없어 리더 선출 자체가 불가능하다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;따라서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;(d) 상황이 원천 차단&lt;/b&gt;된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;커밋되지 않은 엔트리의 운명&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;만약 S1이 텀 4 엔트리를 과반수에 복제하기 전에 다운되면 어떻게 될까?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;인덱스 2는 미커밋 상태이므로,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;누가 리더가 되느냐에 따라 텀 2 엔트리 또는 텀 3 엔트리 중 하나가 채택&lt;/b&gt;된다. Raft의 투표 규칙이 자연스럽게 더 최신 로그를 가진 쪽이 리더가 되도록 유도하고, 리더의 로그가 곧 진실이 된다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;어느 쪽이 선택되든 미커밋 엔트리이므로 안전성을 위반하지 않는다. Raft가 보장하는 것은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&quot;커밋된 로그는 절대 유실되지 않는다&quot;&lt;/b&gt;이지, &quot;복제된 로그가 유실되지 않는다&quot;가 아니기 때문이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Kafka(KRaft)는 다르게 동작한다&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;같은 합의 문제를 Kafka는 다른 방식으로 해결한다. Kafka에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;하이워터마크(High Watermark)&lt;/b&gt;를 기준으로 커밋 여부를 판단하는데, 하이워터마크가 팔로워의 fetch 요청을 통해 비동기적으로 전파된다는 점이 Raft와 다르다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;하이워터마크의 비동기 전파&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Raft에서는 리더가 AppendEntries 응답으로 팔로워의 복제 상태를 실시간으로 파악하고 commitIndex를 직접 계산한다. 반면 Kafka에서는 팔로워가 리더에게 fetch 요청을 보내면서 자신의 현재 오프셋을 알려주고, 리더가 이를 보고 하이워터마크를 갱신한 뒤 다음 fetch 응답에 실어 보내는 구조다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 때문에 팔로워가 알고 있는 하이워터마크는 항상 리더보다&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;최소 한 라운드 이상 뒤처져&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;있다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Log Truncation&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;새 리더로 승격된 팔로워는 자신의 하이워터마크까지만 커밋이 확정된 것으로 판단하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;그 이후의 로그는 잘라낸다(truncate)&lt;/b&gt;. Raft처럼 no-op을 써서 이전 엔트리를 살리는 방식을 택하지 않았다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;메시지 유실과 프로듀서의 선택&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Log truncation으로 인해 잘려나간 메시지는 유실될 수 있다. 하지만 이것이 실제 &quot;데이터 손실&quot;로 이어지는지는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;프로듀서의&lt;span&gt;&amp;nbsp;&lt;/span&gt;acks&lt;span&gt;&amp;nbsp;&lt;/span&gt;설정&lt;/b&gt;에 따라 결정된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;acks 설정동작유실 가능성&lt;/p&gt;
&lt;table style=&quot;color: #333333; text-align: start; border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;acks=0&lt;/td&gt;
&lt;td&gt;프로듀서가 응답을 기다리지 않음&lt;/td&gt;
&lt;td&gt;유실 인지 불가, 재전송 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;acks=1&lt;/td&gt;
&lt;td&gt;리더 기록 시점에 성공 응답&lt;/td&gt;
&lt;td&gt;유실 사실 인지 못함 &amp;rarr; 실질적 데이터 유실&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;acks=all&lt;/td&gt;
&lt;td&gt;ISR 전체 복제 후 성공 응답&lt;/td&gt;
&lt;td&gt;리더 다운 시 타임아웃 &amp;rarr; 프로듀서가 재전송 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Kafka의 설계 철학은&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;유실 방지의 책임을 프로듀서에게 선택권을 주는 것&lt;/b&gt;이다. 처리량이 중요하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;acks=1, 안정성이 중요하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;acks=all을 선택하는 트레이드오프를 제공한다.&lt;/p&gt;</description>
      <category>Development/Architecture</category>
      <author>Icarus8050</author>
      <guid isPermaLink="true">https://icarus8050.tistory.com/166</guid>
      <comments>https://icarus8050.tistory.com/166#entry166comment</comments>
      <pubDate>Sat, 4 Apr 2026 03:40:50 +0900</pubDate>
    </item>
    <item>
      <title>Go + Fiber로 서버 초기 세팅하며 배운 것들 정리</title>
      <link>https://icarus8050.tistory.com/165</link>
      <description>&lt;h1&gt;Go + Fiber로 서버 만들기: Java 개발자가 Go를 시작하며 배운 것들&lt;/h1&gt;
&lt;p&gt;Go와 Fiber 프레임워크를 사용하여 고가용성 분산 시스템 서버의 초기 구조를 설계하면서 학습한 내용을 정리한다.&lt;br&gt;&lt;a href=&quot;https://github.com/icarus8050/peacock&quot;&gt;https://github.com/icarus8050/peacock&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;프로젝트 구조&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;peacock/
├── main.go             # 진입점 (config → server → handler 연결)
├── config/
│   └── config.go       # 환경변수 기반 설정 관리
├── server/
│   └── server.go       # Fiber 앱 생성 + Graceful Shutdown
└── handler/
    ├── handler.go      # 라우트 등록 중앙 관리
    └── health.go       # 헬스체크 엔드포인트 (/health, /ready)&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func main() {
    cfg := config.Load()
    srv := server.New(cfg)
    handler.Register(srv.App)
    if err := srv.Start(); err != nil {
        log.Fatal(err)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;go mod tidy&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;go mod tidy&lt;/code&gt;는 Go 모듈 의존성을 정리하는 명령어다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;코드에서 &lt;code&gt;import&lt;/code&gt;한 외부 패키지를 &lt;code&gt;go.mod&lt;/code&gt;에 추가하고 다운로드한다.&lt;/li&gt;
&lt;li&gt;더 이상 사용하지 않는 패키지는 &lt;code&gt;go.mod&lt;/code&gt;에서 제거한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;go.sum&lt;/code&gt; 파일을 생성하여 의존성의 체크섬(해시값)을 기록한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;go.sum&lt;/code&gt;은 다른 환경에서 동일한 의존성이 다운로드되었는지 검증하는 역할을 하므로, &lt;code&gt;go.mod&lt;/code&gt;과 함께 버전 관리에 포함하는 것이 좋다.&lt;/p&gt;
&lt;h2&gt;구조체 (struct)&lt;/h2&gt;
&lt;p&gt;Go에는 &lt;code&gt;class&lt;/code&gt; 키워드가 없다. 대신 &lt;strong&gt;구조체(struct) + 메서드&lt;/strong&gt;로 동일한 역할을 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type Config struct {
    Port            string
    ReadTimeout     time.Duration
    WriteTimeout    time.Duration
    ShutdownTimeout time.Duration
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Java와 비교하면 다음과 같다:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Config {
    public String port;
    public Duration readTimeout;
    public Duration writeTimeout;
    public Duration shutdownTimeout;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;*&lt;/code&gt;는 &lt;strong&gt;포인터&lt;/strong&gt;를 의미하며, 값 자체를 복사하는 게 아니라 원본 객체의 메모리 주소를 참조한다. Java에서 객체 변수가 참조(reference)를 갖는 것과 같은 개념이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type Server struct {
    App *fiber.App      // Fiber 앱에 대한 포인터
    Cfg *config.Config  // Config에 대한 포인터
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;생성자&lt;/h3&gt;
&lt;p&gt;Go에는 생성자 문법이 없다. 관례적으로 &lt;code&gt;New()&lt;/code&gt; 함수가 생성자 역할을 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func New(cfg *config.Config) *Server {
    app := fiber.New(fiber.Config{
        ReadTimeout:  cfg.ReadTimeout,
        WriteTimeout: cfg.WriteTimeout,
    })
    return &amp;amp;Server{App: app, Cfg: cfg}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;&amp;amp;Server{...}&lt;/code&gt;는 Server 구조체를 만들고 그 포인터를 반환한다는 뜻이다.&lt;/p&gt;
&lt;h3&gt;gofmt&lt;/h3&gt;
&lt;p&gt;Go에는 &lt;code&gt;gofmt&lt;/code&gt;라는 공식 포매터가 있어서, 코드 저장 시 자동으로 포맷을 정리해 준다. 구조체 필드의 타입 정렬도 &lt;code&gt;gofmt&lt;/code&gt;가 자동으로 처리하므로 작성 시 포맷을 신경 쓸 필요가 없다.&lt;/p&gt;
&lt;h2&gt;패키지와 파일명&lt;/h2&gt;
&lt;p&gt;Go에서 &lt;strong&gt;파일명과 패키지명은 관계가 없다.&lt;/strong&gt; 규칙은 하나뿐이다: &lt;strong&gt;같은 디렉토리의 모든 &lt;code&gt;.go&lt;/code&gt; 파일은 같은 패키지명을 사용해야 한다.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;handler/
├── handler.go    → package handler
└── health.go     → package handler&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;파일명이 무엇이든, 같은 디렉토리이므로 둘 다 &lt;code&gt;package handler&lt;/code&gt;다.&lt;/p&gt;
&lt;h2&gt;리시버 (Receiver)&lt;/h2&gt;
&lt;p&gt;메서드가 어떤 타입에 속하는지를 지정하는 문법이다. Java의 &lt;code&gt;this&lt;/code&gt;와 비슷한 역할을 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func (s *Server) Start() error {
    s.Cfg.Port       // Java의 this.cfg.port 와 동일
    s.App.Listen(...)
}&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Go&lt;/th&gt;
&lt;th&gt;Java&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;s *Server&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;this&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;s.Cfg&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;this.cfg&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;&lt;code&gt;*Server&lt;/code&gt;에서 &lt;code&gt;*&lt;/code&gt;는 포인터 리시버로, 원본 인스턴스를 참조하여 내부 필드를 수정할 수 있다. &lt;code&gt;*&lt;/code&gt; 없이 &lt;code&gt;Server&lt;/code&gt;로 쓰면 값이 복사되어 원본에 영향을 줄 수 없다.&lt;/p&gt;
&lt;h3&gt;리시버의 제약&lt;/h3&gt;
&lt;p&gt;리시버는 &lt;strong&gt;같은 패키지 안에서 정의된 구조체만&lt;/strong&gt; 사용할 수 있다. 다른 패키지의 타입에 메서드를 추가하려면 &lt;strong&gt;래핑(wrapping)&lt;/strong&gt;이나 &lt;strong&gt;임베딩&lt;/strong&gt;으로 해결한다.&lt;/p&gt;
&lt;h2&gt;임베딩 (Embedding)&lt;/h2&gt;
&lt;p&gt;구조체 안에 다른 구조체를 &lt;strong&gt;필드 이름 없이&lt;/strong&gt; 넣는 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;type ServerConfig struct {
    *config.Config  // 임베딩
}

sc := &amp;amp;ServerConfig{Config: config.Load()}
sc.Port  // Config의 필드에 바로 접근 가능&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Java의 상속과 비슷하지만, Go에서는 &lt;strong&gt;조합(composition)&lt;/strong&gt;이라는 다른 개념이다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Java 상속&lt;/th&gt;
&lt;th&gt;Go 임베딩&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;관계&lt;/td&gt;
&lt;td&gt;&amp;quot;is-a&amp;quot; (ServerConfig &lt;strong&gt;는&lt;/strong&gt; Config이다)&lt;/td&gt;
&lt;td&gt;&amp;quot;has-a&amp;quot; (ServerConfig &lt;strong&gt;가&lt;/strong&gt; Config을 &lt;strong&gt;갖고 있다&lt;/strong&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;다형성&lt;/td&gt;
&lt;td&gt;부모 타입으로 사용 가능&lt;/td&gt;
&lt;td&gt;불가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;다중&lt;/td&gt;
&lt;td&gt;단일 상속만&lt;/td&gt;
&lt;td&gt;여러 구조체 임베딩 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2&gt;에러 처리: if 초기화문&lt;/h2&gt;
&lt;p&gt;Go에는 &lt;code&gt;try/catch&lt;/code&gt;가 없다. 함수가 에러를 &lt;strong&gt;반환값&lt;/strong&gt;으로 돌려주고, &lt;code&gt;if err != nil&lt;/code&gt; 패턴으로 처리한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;if err := srv.Start(); err != nil {
    log.Fatal(err)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;;&lt;/code&gt; 앞이 &lt;strong&gt;초기화문&lt;/strong&gt;(함수 실행 및 결과 저장), 뒤가 &lt;strong&gt;조건문&lt;/strong&gt;(에러 여부 확인)이다. 위 코드는 아래와 동일하다:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;err := srv.Start()
if err != nil {
    log.Fatal(err)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;초기화문을 사용하면 &lt;code&gt;err&lt;/code&gt; 변수의 스코프가 if 블록 안으로 제한되는 장점이 있다.&lt;/p&gt;
&lt;h2&gt;고루틴 (Goroutine)과 채널 (Channel)&lt;/h2&gt;
&lt;h3&gt;go func()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;go&lt;/code&gt; 키워드로 &lt;strong&gt;고루틴&lt;/strong&gt;을 생성하여 함수를 비동기로 실행한다. Java의 스레드와 비슷하지만 훨씬 가볍다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;go func() {
    s.App.Listen(addr)
}()&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;func() { ... }()&lt;/code&gt;는 익명 함수를 즉시 실행하는 문법이며, 마지막 &lt;code&gt;()&lt;/code&gt;가 호출 부분이다.&lt;/p&gt;
&lt;h3&gt;chan과 &amp;lt;- 연산자&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;chan&lt;/code&gt;은 고루틴 간에 데이터를 주고받는 &lt;strong&gt;채널&lt;/strong&gt;이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;ch := make(chan string)    // string 채널 생성
ch := make(chan string, 5) // 버퍼 크기 5인 채널

ch &amp;lt;- &amp;quot;hello&amp;quot;   // 채널에 값 보내기 (채널이 왼쪽)
msg := &amp;lt;-ch     // 채널에서 값 받기 (채널이 오른쪽, 값 올 때까지 대기)&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;select&lt;/h3&gt;
&lt;p&gt;여러 채널을 동시에 기다리는 문법이다. &lt;strong&gt;먼저 값이 도착한 채널&lt;/strong&gt;의 case가 실행된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;select {
case err := &amp;lt;-errCh:    // 서버에 에러가 발생하거나
    return err
case sig := &amp;lt;-quit:     // 종료 시그널(Ctrl+C)이 오거나
    log.Printf(&amp;quot;received signal %s, shutting down...&amp;quot;, sig)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;select&lt;/code&gt;는 채널에 값이 올 때까지 &lt;strong&gt;블로킹&lt;/strong&gt;된다. &lt;code&gt;server.go&lt;/code&gt;에서 고루틴이 서버를 별도로 실행하고, &lt;code&gt;Start()&lt;/code&gt; 함수는 &lt;code&gt;select&lt;/code&gt;에서 대기 상태에 진입한다. 이 대기가 없으면 &lt;code&gt;main&lt;/code&gt; 함수가 바로 끝나면서 프로그램이 종료된다.&lt;/p&gt;
&lt;h2&gt;defer&lt;/h2&gt;
&lt;p&gt;함수가 &lt;strong&gt;끝날 때 실행할 코드를 예약&lt;/strong&gt;하는 키워드다. Java의 &lt;code&gt;finally&lt;/code&gt;와 비슷하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;ctx, cancel := context.WithTimeout(context.Background(), s.Cfg.ShutdownTimeout)
defer cancel()  // 함수 종료 시 cancel() 자동 호출 → 타이머 리소스 해제&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;cancel()&lt;/code&gt;은 &lt;code&gt;context.WithTimeout&lt;/code&gt;이 반환하는 컨텍스트 취소 함수다. &lt;code&gt;defer&lt;/code&gt;로 호출하는 이유는 shutdown이 타임아웃 전에 완료되더라도 컨텍스트 내부의 타이머 리소스를 해제해야 하기 때문이다. 호출하지 않으면 메모리 누수가 발생할 수 있다.&lt;/p&gt;
&lt;h3&gt;여러 번의 defer&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;defer&lt;/code&gt;는 한 함수 내에서 여러 번 호출 가능하며, &lt;strong&gt;역순(LIFO)&lt;/strong&gt;으로 실행된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func process() error {
    file, _ := os.Open(&amp;quot;data.txt&amp;quot;)
    defer file.Close()           // 2번째로 실행

    conn, _ := db.Connect()
    defer conn.Close()           // 1번째로 실행

    // 작업 수행...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;나중에 열린 리소스가 먼저 닫히므로, 의존 관계가 있을 때 안전한 순서가 보장된다.&lt;/p&gt;
&lt;h2&gt;fmt.Sprintf&lt;/h2&gt;
&lt;p&gt;문자열을 포맷팅하여 반환하는 함수다. Java의 &lt;code&gt;String.format()&lt;/code&gt;과 동일하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;addr := fmt.Sprintf(&amp;quot;:%s&amp;quot;, s.Cfg.Port)  // &amp;quot;:3000&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;자주 쓰는 포맷 지정자:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;지정자&lt;/th&gt;
&lt;th&gt;의미&lt;/th&gt;
&lt;th&gt;예시 결과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;문자열&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fmt.Sprintf(&amp;quot;hello %s&amp;quot;, &amp;quot;world&amp;quot;)&lt;/code&gt; → &lt;code&gt;&amp;quot;hello world&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;정수&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fmt.Sprintf(&amp;quot;port: %d&amp;quot;, 3000)&lt;/code&gt; → &lt;code&gt;&amp;quot;port: 3000&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%v&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;값의 기본 형식&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fmt.Sprintf(&amp;quot;%v&amp;quot;, true)&lt;/code&gt; → &lt;code&gt;&amp;quot;true&amp;quot;&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%w&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;에러 래핑 (&lt;code&gt;Errorf&lt;/code&gt;에서만)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;fmt.Errorf(&amp;quot;failed: %w&amp;quot;, err)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;</description>
      <category>Development/golang</category>
      <author>Icarus8050</author>
      <guid isPermaLink="true">https://icarus8050.tistory.com/165</guid>
      <comments>https://icarus8050.tistory.com/165#entry165comment</comments>
      <pubDate>Sun, 22 Mar 2026 18:50:07 +0900</pubDate>
    </item>
    <item>
      <title>Endofunctor (엔도펑터), 모나드 (Monad)</title>
      <link>https://icarus8050.tistory.com/164</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;엔도펑터 (Endofunctor)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;출발하는 카테고리와 도착하는 카테고리가 같은 펑터로, 그리스어 접두사 'Endo-'(내부의)와 의미와 Functor가 결합한 것이다. 프로그래밍에서 다루는 대부분의 펑터가 엔도펑터다. 가장 흔한 예시로 Maybe(or 자바의 Optional)가 있다. (F : C -&amp;gt; C)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;엔도펑터 역시 펑터와 마찬가지로 대상 객체를 매핑하는 것 뿐만 아니라 사상 또한 매핑해야 한다. 이를 리프팅(lifting) 한다고 하며, 들어올린다고도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ex) a -&amp;gt; b를 Maybe a -&amp;gt; Maybe b 로 리프팅&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;모나드 (Monad)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;모나드는 엔도펑터 카테고리에서의 모노이드다. 즉, 결합 법칙과 항등 법칙을 만족해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;펑터는 컨텍스트 안의 값을 바꾸는 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;함수를 적용한다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;map : F&amp;lt;A&amp;gt; -&amp;gt; (A -&amp;gt; B) -&amp;gt; F&amp;lt;B&amp;gt;&lt;/li&gt;
&lt;li&gt;함수가 컨텍스트를 생성하지는 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;모나드는 컨텍스트 안의 값에 컨텍스트를 생성하는 함수를 적용한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;flatMap : M&amp;lt;A&amp;gt; -&amp;gt; (A -&amp;gt; M&amp;lt;B&amp;gt;) -&amp;gt; M&amp;lt;B&amp;gt;&lt;/li&gt;
&lt;li&gt;함수 자체가 새로운 컨텍스트를 반환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1772443708664&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sealed class Maybe&amp;lt;out A&amp;gt; {
    data class Just&amp;lt;A&amp;gt;(val value: A) : Maybe&amp;lt;A&amp;gt;()
    data object Nothing : Maybe&amp;lt;kotlin.Nothing&amp;gt;()

    fun &amp;lt;B&amp;gt; map(f: (A) -&amp;gt; B): Maybe&amp;lt;B&amp;gt; = when (this) {
        is Just -&amp;gt; Just(f(value))
        is Nothing -&amp;gt; Nothing
    }

    fun &amp;lt;B&amp;gt; flatMap(f: (A) -&amp;gt; Maybe&amp;lt;B&amp;gt;): Maybe&amp;lt;B&amp;gt; = when (this) {
        is Just -&amp;gt; f(value)
        is Nothing -&amp;gt; Nothing
    }
}

// ── 순수한 변환: 펑터로 충분
val number: Maybe&amp;lt;Int&amp;gt; = Maybe.Just(42)
val doubled: Maybe&amp;lt;Int&amp;gt; = number.map { it * 2 }         // Just(84)
val asString: Maybe&amp;lt;String&amp;gt; = number.map { it.toString() } // Just(&quot;42&quot;)

// ── 실패 가능한 연산: 펑터만으로는 한계
fun safeDivide(a: Int, b: Int): Maybe&amp;lt;Int&amp;gt; =
    if (b != 0) Maybe.Just(a / b) else Maybe.Nothing

val result: Maybe&amp;lt;Maybe&amp;lt;Int&amp;gt;&amp;gt; = number.map { safeDivide(it, 0) }
// 결과: Just(Nothing) &amp;mdash; Maybe가 이중으로 중첩됨.
// 타입이 Maybe&amp;lt;Maybe&amp;lt;Int&amp;gt;&amp;gt;가 되어버림.

// ── 모나드의 flatMap이 해결
val clean: Maybe&amp;lt;Int&amp;gt; = number.flatMap { safeDivide(it, 0) }
// 결과: Nothing &amp;mdash; 평탄화됨.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Development/Architecture</category>
      <author>Icarus8050</author>
      <guid isPermaLink="true">https://icarus8050.tistory.com/164</guid>
      <comments>https://icarus8050.tistory.com/164#entry164comment</comments>
      <pubDate>Mon, 2 Mar 2026 18:36:30 +0900</pubDate>
    </item>
    <item>
      <title>함수형 프로그래밍, 커링, 모노이드, 펑터..</title>
      <link>https://icarus8050.tistory.com/163</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;함수형 프로그래밍&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;순수 함수(Pure function)와 불변 데이터(Immutable data)를 중심으로 프로그래밍을 구성하는 패러다임.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;순수 함수 : 같은 입력에 대해서 항상 같은 출력을 반환하고, 외부 상태를 변경하지 않는 함수다.&lt;/li&gt;
&lt;li&gt;불변성 : 데이터를 생성한 후에는 변경이 되지 않고, 변경이 필요하면 새로운 데이터를 만든다.&lt;/li&gt;
&lt;li&gt;일급 함수(First-Class Function) : 함수를 일반 변수처럼 취급하여 변수 할당, 인자 전달 및 반환값으로 사용 가능하도록 하는 특성이다.&lt;/li&gt;
&lt;li&gt;고차 함수(Higher-Order Function) : 함수를 인자로 받거나 함수를 반환하는 함수다.&lt;/li&gt;
&lt;li&gt;함수 합성(Function Composition) : 함수들을 조합하여 다양한 함수를 만들 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771386059669&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 작은 단위 함수들
val removeSpaces: (String) -&amp;gt; String = { it.replace(&quot; &quot;, &quot;&quot;) }
val toUpperCase: (String) -&amp;gt; String = { it.uppercase() }
val addPrefix: (String) -&amp;gt; String = { &quot;PREFIX_$it&quot; }

// 합성 유틸리티
infix fun &amp;lt;A, B, C&amp;gt; ((A) -&amp;gt; B).then(next: (B) -&amp;gt; C): (A) -&amp;gt; C =
    { a -&amp;gt; next(this(a)) }

// 함수 합성
val processKey = removeSpaces then toUpperCase then addPrefix

println(processKey(&quot;hello world&quot;))  // PREFIX_HELLOWORLD&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커링(Currying) : 다중 인자를 갖는 함수를 단일 인자를 갖는 함수 체인으로 변환하는 방법이다.&lt;/p&gt;
&lt;pre id=&quot;code_1771386524998&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 커링: 여러 인자를 받는 함수를 한 인자씩 받는 함수 체인으로 변환
fun &amp;lt;A, B, C&amp;gt; ((A, B) -&amp;gt; C).curried(): (A) -&amp;gt; (B) -&amp;gt; C =
    { a -&amp;gt; { b -&amp;gt; this(a, b) } }

val multiply = { a: Int, b: Int -&amp;gt; a * b }
val curriedMultiply = multiply.curried()

val double = curriedMultiply(2)   // b만 받으면 되는 함수
val triple = curriedMultiply(3)

println(double(5))   // 10
println(triple(5))   // 15&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;모노이드(Monoid)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래 세 가지 조건을 만족하면 모노이드다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;결합 법칙(associativity) : (a * b) * c == a * (b * c)&lt;/li&gt;
&lt;li&gt;항등원(identity element) : a * e == a, e * a == a&lt;/li&gt;
&lt;li&gt;닫힌 연산(closure) : 같은 타입끼리 연산하면 같은 타입이 나옴.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;결합 법칙이 성립하기 때문에 데이터를 여러 덩어리로 나누어 각각 계산한 뒤에 마지막에 합쳐도 결과가 동일하다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;안전한 병렬 처리의 추상화가 가능하여 대규모 병렬 처리(Map Reduce), 분산 집계 등의 핵심 원리가 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;background-color: #f0f2f5; color: #0a0a0a; text-align: start;&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;/ 모노이드 인터페이스
interface Monoid&amp;lt;T&amp;gt; {
    val empty: T                       // 항등원
    fun combine(a: T, b: T): T        // 결합 연산
}

// 정수 덧셈 모노이드: 항등원 = 0, 연산 = +
object IntAddMonoid : Monoid&amp;lt;Int&amp;gt; {
    override val empty: Int = 0
    override fun combine(a: Int, b: Int): Int = a + b
}

// 문자열 모노이드: 항등원 = &quot;&quot;, 연산 = 연결
object StringMonoid : Monoid&amp;lt;String&amp;gt; {
    override val empty: String = &quot;&quot;
    override fun combine(a: String, b: String): String = a + b
}

// 리스트 모노이드: 항등원 = emptyList(), 연산 = +
class ListMonoid&amp;lt;T&amp;gt; : Monoid&amp;lt;List&amp;lt;T&amp;gt;&amp;gt; {
    override val empty: List&amp;lt;T&amp;gt; = emptyList()
    override fun combine(a: List&amp;lt;T&amp;gt;, b: List&amp;lt;T&amp;gt;): List&amp;lt;T&amp;gt; = a + b
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;교환 법칙은 성립하지 않는다. 위 문자열 모노이드가 그 예시다. (&quot;Hello&quot; 와 &quot;World&quot;는 연산의 순사가 달라지면 결과가 달라진다.)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;범주론에서 살펴 보자면, 추상화의 대상이 순서가 중요한 결합까지 모두 포함하기 때문이다.&lt;/li&gt;
&lt;li&gt;원소 그 자체를 바라보는 것이 아니라, 객체와 객체를 변환하는 사상(morphism)으로 이해해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style7&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;펑터(Functor)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;펑터는 두 카테고리 사이의 구조를 보존하는 매핑을 의미한다.&lt;/li&gt;
&lt;li&gt;두 카테고리 C와 D가 있을 때, 펑터 F : C -&amp;gt; D는 두 가지 매핑으로 구성된다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대상 매핑 (Object Mapping) : 카테고리 C의 객체 A를 카테고리 D의 객체 F(A)로 보낸다.&lt;/li&gt;
&lt;li&gt;사상 매핑 (Morphism Mapping) : 카테고리 C의 사상 f : A -&amp;gt; B 를 카테고리 D의 사상 F(f) : F(A) -&amp;gt; F(B)로 보낸다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;펑터는 다음의 성질을 만족해야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;항등 사상 보존 : F(id_A) = id_F(A)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;원래의 카테고리에서 아무것도 안 하는 화살표는 옮겨진 카테고리에서도 아무것도 하지 않아야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;사상 합성 보존 : F(g * f) = F(g) * F(f)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;f를 가고나서 g를 가는 경로를 한꺼번에 옮긴 것은, 각각을 옮겨서 연결한 것과 같아야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1771404946090&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 펑터 인터페이스
interface Functor&amp;lt;out A&amp;gt; {
    fun &amp;lt;B&amp;gt; map(f: (A) -&amp;gt; B): Functor&amp;lt;B&amp;gt;
}

// Maybe &amp;mdash; null-safe한 값을 담는 컨텍스트
sealed class Maybe&amp;lt;out A&amp;gt; : Functor&amp;lt;A&amp;gt; {
    data class Just&amp;lt;A&amp;gt;(val value: A) : Maybe&amp;lt;A&amp;gt;()
    data object None : Maybe&amp;lt;Nothing&amp;gt;()

    override fun &amp;lt;B&amp;gt; map(f: (A) -&amp;gt; B): Maybe&amp;lt;B&amp;gt; = when (this) {
        is Just -&amp;gt; Just(f(value))    // 값이 있으면 변환
        is None -&amp;gt; None              // 없으면 그대로 None
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Development/Architecture</category>
      <category>Functional Programming</category>
      <category>모나드</category>
      <category>모노이드</category>
      <category>커링</category>
      <category>펑터</category>
      <category>함수형 프로그래밍</category>
      <author>Icarus8050</author>
      <guid isPermaLink="true">https://icarus8050.tistory.com/163</guid>
      <comments>https://icarus8050.tistory.com/163#entry163comment</comments>
      <pubDate>Wed, 18 Feb 2026 18:56:51 +0900</pubDate>
    </item>
    <item>
      <title>아키텍처 퀀텀 (Architecture Quantum)</title>
      <link>https://icarus8050.tistory.com/162</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;아키텍처 퀀텀 (Architecture Quantum)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아키텍처 퀀텀은 높은 기능 응집도, 높은 정적 커플링, 동기적 동적 커플링의 특성을 띤, 독립적으로 배포 가능한 아티팩트다.&lt;/li&gt;
&lt;li&gt;모놀리식 아키텍처는 정의에 따라 단일 아키텍처 퀀텀이다.&lt;/li&gt;
&lt;li&gt;마이크로 서비스와 같은 분산 아키텍처는 서비스를 독립적으로 배포가 가능하기 때문에 각각이 아키텍처 퀀텀이 될 수 있다. (서비스가 서로 격리되어 있다고 해서 퀀텀이 생성되는 것은 아니다.)&lt;/li&gt;
&lt;li&gt;독립적으로 배포가 가능해도 공유 데이터베이스와 같은 공통 결합점이 생긴다면 아키텍처 퀀텀에 포함된다. 따라서 단순히 배포의 경계만 봐서는 아키텍처 퀀텀을 가늠하기 어렵다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;정적 커플링&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터베이스는 정적 커플링을 판단하는 기준으로, 단일 데이터베이스에 의존하는 시스템은 1 이상의 퀀텀을 가질 수 없다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;책에서는 데이터베이스가 정적 커플링을 판단하는 기준으로 나와있지만 무조건적이지는 않을 것 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;아키텍처 퀀텀을 정적 분석할 때는 아키텍처에 종속된 이것이 서비스 작동에 필요한지를 확인해야 한다.&lt;/li&gt;
&lt;li&gt;마이크로 서비스들이 유저 인터페이스와 단단하게 결합돼 있으면 아키텍처 퀀텀이 합쳐질 수 있다. 이는 서비스마다 운영을 위한 특성을 세밀하게 적용하기가 어려워지게 만든다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;동적 커플링&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;런타임에 퀀텀들이 서로 어떻게 통신하는지를 나타낸다. 이러한 특성들은 피트니스 함수를 지속적으로 실행시켜 모니터링해야 한다.&lt;/li&gt;
&lt;li&gt;서비스가 서로를 호출할 때는 아래 3가지를 고려해야 한다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;통신(Communication) : 동기 통신인지, 비동기 통신인지?&lt;/li&gt;
&lt;li&gt;일관성(Consistency) : 워크플로 통신에 원자성이 필수로 적용되어야 하는지, 최종적 일관성만 맞춰도 충분한지?&lt;/li&gt;
&lt;li&gt;조정(Coordination) : 워크플로가 오케스트레이터를 활용하는지, 코레오그래피 방식으로 서비스가 서로 통신하는지?&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;동적 커플링은 서비스들 간의 특성으로 인해서 서로 영향을 주면 아키텍처 퀀텀이 일시적으로 뒤얽힐 수도 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ex) 100,000TPS를 처리하는 서비스가 50,000TPS를 처리하는 서비스를 동기적으로 호출하는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고 서적&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[&lt;span style=&quot;color: #000000; text-align: center;&quot;&gt;소프트웨어 아키텍처 The Hard Parts]&lt;/span&gt;&lt;/p&gt;</description>
      <category>Development/Architecture</category>
      <author>Icarus8050</author>
      <guid isPermaLink="true">https://icarus8050.tistory.com/162</guid>
      <comments>https://icarus8050.tistory.com/162#entry162comment</comments>
      <pubDate>Tue, 17 Feb 2026 18:24:19 +0900</pubDate>
    </item>
    <item>
      <title>[Coroutine] 코루틴 학습 - 17 (Flow processing)</title>
      <link>https://icarus8050.tistory.com/160</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;map, filter&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;suspend fun main() {
    flowOf(1, 2, 3, 4)
        .map { it * it }
        .filter { it % 2 == 0 }
        .collect { println(it) }
}
// 4
// 16&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;컬렉션에서 흔하게 사용하는 map과 filter 기능과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;take and drop&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;suspend fun main() {
    ('A'..'Z').asFlow()
        .take(5)
        .collect { println(it) }

    println()

    ('A'..'Z').asFlow()
        .drop(20)
        .collect { println(it) }

    println()
}
// A
// B
// C
// D
// E
//
// U
// V
// W
// X
// Y
// Z&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;take는 element의 앞부분부터 정해진 수만큼 사용하고, drop은 정해진 수만큼 앞의 element를 제외하고 그 뒤의 나머지 element를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;merge&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;suspend fun main() {
    val ints: Flow&amp;lt;Int&amp;gt; = flowOf(1, 2, 3)
        .onEach { delay(1000) }
    val doubles: Flow&amp;lt;Double&amp;gt; = flowOf(0.1, 0.2, 0.3)

    val together: Flow&amp;lt;Number&amp;gt; = merge(ints, doubles)
    println(together.collect { println(it) })
}
// 0.1
// 0.2
// 0.3
// (1 sec)
// 1
// (1 sec)
// 2
// (1 sec)
// 3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;merge는 여러 flow를 하나로 합치는데, 순서에 상관없이 한 flow가 지연되더라도 다른 flow는 기다리지 않고 처리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;zip&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;suspend fun main() {
    val flow1 = flowOf(&quot;A&quot;, &quot;B&quot;, &quot;C&quot;)
        .onEach { delay(400) }
    val flow2 = flowOf(1, 2, 3 ,4)
        .onEach { delay(1000) }
    flow1.zip(flow2) { f1, f2 -&amp;gt; &quot;${f1}_${f2}&quot;}
        .collect { println(it) }
}
// (1 sec)
// A_1
// (1 sec)
// B_2
// (1 sec)
// C_3&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;zip은 두 flow의 element를 한 쌍으로 만든다. 각 element는 오직 한 쌍의 부분 요소로만 사용할 수 있고, 한 쌍이 되지 않았다면 대기하게 된다. 한 쪽 flow가 완료되었을 때는 다른 flow에 element가 남아있더라도 종료된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;combine&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;suspend fun main() {
    val flow1 = flowOf(&quot;A&quot;, &quot;B&quot;, &quot;C&quot;)
        .onEach { delay(400) }
    val flow2 = flowOf(1, 2, 3, 4)
        .onEach { delay(1000) }
    flow1.combine(flow2) { f1, f2 -&amp;gt; &quot;${f1}_${f2}&quot; }
        .collect { println(it) }
}
// (1 sec)
// B_2
// (0.2 sec)
// C_1
// (0.8 sec)
// C_2
// (1 sec)
// C_3
// (1 sec)
// C_4&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;combine은 zip과 같이 두 flow를 합쳐서 한 쌍의 element로 만들어 emit 된다는 점은 같지만 두 flow 모두 한 쪽이 느린 flow를 기다려서 한 쌍의 element를 만든다는 점이 다르다. 새로운 element는 이전의 element를 대체하여 새로운 한 쌍을 만든다. 위의 예제 코드를 통해 이를 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;fold and scan&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;suspend fun main() {
    val list = flowOf(1, 2, 3, 4)
        .onEach { delay(1000) }
    val res = list.fold(0) { acc, i -&amp;gt; acc + i }
    println(res)
}
// (4 sec)
// 10&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;fold는 종단 연산이고, collect() 처럼 flow가 완료될 때까지 일시중단된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;suspend fun main() {
    flowOf(1, 2, 3, 4)
        .onEach { delay(1000) }
        .scan(0) { acc, v -&amp;gt; acc + v }
        .collect { println(it) }
}
// 0
// (1 sec)
// 1
// (1 sec)
// 3
// (1 sec)
// 6
// (1 sec)
// 10&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;flow에서의 scan은 중간 연산이며, 이전 단계에서의 값을 수신한 후에 새로운 값을 만들어낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;flatMapConcat, flatMapMerge, flatMapLatest&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;private fun flowFrom(element: String) = flowOf(1, 2, 3)
    .onEach { delay(1000) }
    .map { &quot;${it}_${element}&quot; }

suspend fun main() {
    flowOf(&quot;A&quot;, &quot;B&quot;, &quot;C&quot;)
        .flatMapConcat { flowFrom(it) }
        .collect { println(it) }
}
// (1 sec)
// 1_A
// (1 sec)
// 2_A
// (1 sec)
// 3_A
// (1 sec)
// 1_B
// (1 sec)
// 2_B
// (1 sec)
// 3_B
// (1 sec)
// 1_C
// (1 sec)
// 2_C
// (1 sec)
// 3_C&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;flatMapConcat은 두 플로우를 함께 처리하는데 flatMap과 같이 평탄화하여 작업을 수행한다. flatMapConcat은 생성된 flow를 차례로 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;private fun flowFrom(element: String) = flowOf(1, 2, 3)
    .onEach { delay(1000) }
    .map { &quot;${it}_${element}&quot; }

suspend fun main() {
    flowOf(&quot;A&quot;, &quot;B&quot;, &quot;C&quot;)
        .flatMapMerge { flowFrom(it) }
        .collect { println(it) }
}
// (1 sec)
// 1_A
// 1_B
// 1_C
// (1 sec)
// 2_A
// 2_B
// 2_C
// (1 sec)
// 3_A
// 3_B
// 3_C&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;flatMapMerge는 첫 번째 flow를 동시에 처리한다. flatMapMerge 함수에는 concurrency 파라미터를 통해서 동시에 처리할 element 수를 지정할 수 있다. 기본 값은 16이며, JVM에서 사용하는 DEFAULT_CONCURRENCY_PROPERTY_NAME 프로퍼티에 따라서 달라질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;private fun flowFrom(element: String) = flowOf(1, 2, 3)
    .onEach { delay(1000) }
    .map { &quot;${it}_${element}&quot; }

suspend fun main() {
    flowOf(&quot;A&quot;, &quot;B&quot;, &quot;C&quot;)
        .flatMapLatest { flowFrom(it) }
        .collect { println(it) }
}
// (1 sec)
// 1_C
// (1 sec)
// 2_C
// (1 sec)
// 3_C&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;flatMapLatest는 flow에서 새로운 element가 emit되면 이전의 element는 덮어씌워진다.&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;private fun flowFrom(element: String) = flowOf(1, 2, 3)
    .onEach { delay(1000) }
    .map { &quot;${it}_${element}&quot; }

suspend fun main() {
    flowOf(&quot;A&quot;, &quot;B&quot;, &quot;C&quot;)
        .onEach { delay(1200) }
        .flatMapLatest { flowFrom(it) }
        .collect { println(it) }
}
// (2.2 sec)
// 1_A
// (1.2 sec)
// 1_B
// (1 sec)
// 1_C
// (1 sec)
// 2_C
// (1 sec)
// 3_C&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Terminal 연산&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;종단 연산에는 collect() 뿐만 아니라 다양한 연산을 지원한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;first(), firstOrNull()
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;처음으로 emit된 element를 찾는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;fold(), reduce()
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;emit된 값들을 람다 표현식에 정의된 연산을 통해 하나의 결과 값으로 만들어낸다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;count()
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;emit된 element의 수를 반환한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;종단 연산은 일시중단되며, flow가 완료되면 값을 리턴한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고자료&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.amazon.com/Kotlin-Coroutines-Deep-Marcin-Moskala/dp/8396395837&quot;&gt;https://www.amazon.com/Kotlin-Coroutines-Deep-Marcin-Moskala/dp/8396395837&lt;/a&gt;&lt;/p&gt;</description>
      <category>Development/Kotlin</category>
      <author>Icarus8050</author>
      <guid isPermaLink="true">https://icarus8050.tistory.com/160</guid>
      <comments>https://icarus8050.tistory.com/160#entry160comment</comments>
      <pubDate>Sat, 28 May 2022 17:27:11 +0900</pubDate>
    </item>
    <item>
      <title>[Coroutine] 코루틴 학습 - 16 (Flow lifecycle functions)</title>
      <link>https://icarus8050.tistory.com/159</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Flow는 한 쪽에서 다른 쪽으로 흐르는 파이프와 유사하다. Flow가 예외가 발생하거나 요청이 완료되었을 때, 이 정보는 전파되어 중단 단계에서 Flow를 close시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Flow 연산의 결과로 값, 예외, 특정 이벤트들을 수신할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;onEach&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;suspend fun main() {
    flowOf(1, 2, 3, 4)
        .onEach { println(it) }
        .collect()
}
// 1
// 2
// 3
// 4&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;flow의 각 값에 대한 연산을 수행할 경우엔 onEach를 사용할 수 있다. onEach의 람다 표현식은 일시중단 되는동안, 각 종단 요소들이 순차적으로 처리된다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;suspend fun main() {

    flowOf(1, 2)
        .onEach { delay(1000) }
        .collect { println(it) }
}
// (1 sec)
// 1
// (1 sec)
// 2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;따라서 위와 같이 onEach에서 delay가 추가되면 flow에서 각 요소들은 처리가 지연된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;onStart&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;flow가 시작되자마자 바로 호출 될 수 있도록 리스너를 설정하는 함수이다. &amp;nbsp;onStart 내에서 emit을 호출할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;suspend fun main() {
    flowOf(1, 2)
        .onEach { delay(1000) }
        .onStart {
            println(&quot;On Start!&quot;)
            emit(0)
        }
        .collect { println(it) }
}
// On start!
// 0
// (1 sec)
// 1
// (1 sec)
// 2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;onCompletion&lt;/h2&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;suspend fun main() {
    flowOf(1, 2)
        .onEach { delay(1000) }
        .onCompletion {
            emit(-1)
            println(&quot;Completed&quot;)
        }
        .collect { println(it) }
}
// (1 sec)
// 1
// (1 sec)
// 2
// -1
// Completed&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;onCompletion은 완료되거나 예외, 취소가 발생하는 경우에 flow의 완료 처리를 위한 리스너로 사용될 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;suspend fun main() = coroutineScope {
    val job = launch {
        flowOf(1, 2)
            .onEach { delay(1000) }
            .onCompletion { println(&quot;Completed&quot;) }
            .collect { println(it) }
    }
    delay(1100)
    job.cancel()
}
// (1 sec)
// 1
// (0.1 sec)
// Completed&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;onEmpty&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;flow는 값을 내보내지 않고 완료될 수도 있다. 이런 케이스는 예상치 못한 상황의 케이스의 표시일 수 있는데 이를 위해 onEmpty를 지원한다. flow가 완료되었을 때 emit된 값이 없으면 호출되고, 람다 표현식 내부에서 디폴트 값을 생성하여 내보낼 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;suspend fun main() = coroutineScope {
    flow&amp;lt;List&amp;lt;Int&amp;gt;&amp;gt; { delay(1000) }
        .onEmpty { emit(emptyList()) }
        .collect { println(it) }
}
// (1 sec)
// []&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Catch&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;flow 처리 도중에 예외가 발생하는 경우, catch 함수를 설정하여 필요한 처리를 수행할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;private class MyError : Throwable(&quot;My error&quot;)

val flow = flow {
    emit(1)
    emit(2)
    throw MyError()
}

suspend fun main(): Unit {
    flow.onCompletion { println(&quot;Completed!&quot;) }
        .onEach { println(&quot;Got $it&quot;) }
        .catch { 
            println(&quot;Caught $it&quot;)
            emit(-1)
        }
        .collect { println(&quot;Collected $it&quot;) }
}
// Got 1
// Collected 1
// Got 2
// Collected 2
// Caught MyError: My error
// Collected -1
// Completed!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;catch 리스너는 예외를 인수로 수신하고 복구 작업을 수행할 수 있도록 한다. catch 리스너 내부에서는 다시 새로운 값을 emit하여 값을 생성할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;만약 에러가 catch되지 않았다면 flow는 즉시 cancel되고, collect()에서 예외를 던지게 된다. 해당 예외는 바깥쪽에서 try-catch 블록으로 잡아내는 것이 가능하다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;private class MyError : Throwable(&quot;My error&quot;)

private val flow = flow {
    emit(&quot;My message&quot;)
    throw MyError()
}

suspend fun main(): Unit {
    try {
        flow.collect { println(&quot;Collected $it&quot;)}
    } catch (e: MyError) {
        println(&quot;Caught&quot;)
    }
}
// Collected My message
// Caught&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;종단 연산에서의 예외는 catch 리스너가 동작하지 않으므로 주의해야한다. catch는 마지막 연산에 위치할 수 없기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;flowOn&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;람다 표현식은 onEach, onStart, onCompletion 등 flow 연산의 인자로 사용되고, flow 빌더는 본질적으로 모두 일시중단된다. 일시중단 함수는 context가 필요로한데, structured concurrency를 위한 부모 컨텍스트와의 관계되어야 한다. 이러한 함수가 컨텍스트를 가져오는 위치는 collect()가 호출될 때이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;private fun usersFlow(): Flow&amp;lt;String&amp;gt; = flow {
    repeat(2) {
        val ctx = currentCoroutineContext()
        val name = ctx[CoroutineName]?.name
        emit(&quot;User$it in $name&quot;)
    }
}

suspend fun main() {
    val users = usersFlow()
    withContext(CoroutineName(&quot;Name1&quot;)) {
        users.collect { println(it) }
    }
    withContext(CoroutineName(&quot;Name2&quot;)) {
        users.collect { println(it) }
    }
}
// User0 in Name1
// User1 in Name1
// User0 in Name2
// User1 in Name2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;종단 연산을 호출하면 업스트림으로부터 element를 요청하는데, 이때 coroutine context가 전파된다. 그리고 전파되는 컨텍스트는 flowOn()에서 수정이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;suspend fun present(place: String, message: String) {
    val ctx = coroutineContext
    val name = ctx[CoroutineName]?.name
    println(&quot;[$name] $message on $place&quot;)
}

fun messagesFlow(): Flow&amp;lt;String&amp;gt; = flow {
    present(&quot;flow builder&quot;, &quot;Message&quot;)
    emit(&quot;Message&quot;)
}

suspend fun main() {
    val messages = messagesFlow()
    withContext(CoroutineName(&quot;Name1&quot;)) {
        messages.flowOn(CoroutineName(&quot;Name3&quot;))
            .onEach { present(&quot;onEach&quot;, it) }
            .flowOn(CoroutineName(&quot;Name2&quot;))
            .collect { present(&quot;collect&quot;, it) }
    }
}
// [Name3] Message on flow builder
// [Name2] Message on onEach
// [Name1] Message on collect&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;flowOn()은 오직 업스트림 flow의 함수에 대해서만 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;launchIn&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;collect는 flow가 완료될 때까지 일시 중단하는 작업이다. 그리고 이를 다른 코루틴에서 flow 연산을 시작하기 위해서는 일반적으로 launch builder로 래핑한다. 이러한 케이스를 위해서 launchIn 함수를 이용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;launchIn은 인자로 넘겨진 스코프에서 시작되는 새로운 코루틴에서 collect()를 처리한다. launchIn은 별도의 코루틴에서 flow를 처리할 때 자주 사용된다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;public fun &amp;lt;T&amp;gt; Flow&amp;lt;T&amp;gt;.launchIn(scope: CoroutineScope): Job = scope.launch {
    collect()
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;suspend fun main(): Unit = coroutineScope {
    flowOf(&quot;User1&quot;, &quot;User2&quot;)
        .onStart { println(&quot;Users:&quot;) }
        .onEach { println(it) }
        .launchIn(this)
}
// Users:
// User1
// User2&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고자료&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.amazon.com/Kotlin-Coroutines-Deep-Marcin-Moskala/dp/8396395837&quot;&gt;https://www.amazon.com/Kotlin-Coroutines-Deep-Marcin-Moskala/dp/8396395837&lt;/a&gt;&lt;/p&gt;</description>
      <category>Development/Kotlin</category>
      <author>Icarus8050</author>
      <guid isPermaLink="true">https://icarus8050.tistory.com/159</guid>
      <comments>https://icarus8050.tistory.com/159#entry159comment</comments>
      <pubDate>Wed, 25 May 2022 07:59:06 +0900</pubDate>
    </item>
    <item>
      <title>[Coroutine] 코루틴 학습 - 15 (Flow Building)</title>
      <link>https://icarus8050.tistory.com/158</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Flow는 빌더를 통해 비교적 간단하게 생성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Flow raw values&lt;/h2&gt;
&lt;pre id=&quot;code_1653083912214&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;suspend fun main() {
    flowOf(1, 2, 3, 4, 5)
        .collect { println(it) }
        
     emptyFlow&amp;lt;Int&amp;gt;()
        .collect { println(it) }
}
// 1
// 2
// 3
// 4
// 5&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;listOf와 같이 유사하게 flowOf()를 통해 간단하게 Flow를 생성할 수 있다. emptyFlow는 emptyList와 비슷하며, collect를 호출해도 아무일도 일어나지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Converters&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Iterable 인터페이스의 확장함수로, asFlow()를 호출하면 컬렉션을 손쉽게 Flow로 변환할 수 있다. (Sequence 또한 asFlow()를 지원한다.)&lt;/p&gt;
&lt;pre id=&quot;code_1653094632773&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;suspend fun main() {
    listOf(1, 2, 3, 4, 5)
        .asFlow()
        .collect { println(it) }

    val function = suspend {
        delay(1000)
        &quot;UserName&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1653094810299&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;suspend fun main() {
    val function = suspend {
        delay(1000)
        &quot;UserName&quot;
    }

    function.asFlow()
        .collect { println(it) }

    ::getUserName
        .asFlow()
        .collect { println(it) }
}

private suspend fun getUserName(): String {
    delay(1000)
    return &quot;UserName&quot;
}

// (1 sec)
// UserName
// (1 sec)
// UserName&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;지연된 단일 값을 반환하는 일시중단 함수 또한 Flow로 변환이 가능하다. 위의 예시는 일시중단 람다 표현식으로 선언된 함수를 asFlow()를 통해 Flow로 변환하는 코드이다. asFlow 확장 함수는 함수 타입에 대해서도 지원한다. (suspend() -&amp;gt; T 또는 () -&amp;gt; T)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Flow와 리액티브 스트림&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Flow는 Reactive Stream(Reactor, RxJava 2.x, RxJava 3.x)에 대해서도 kotlinx-coroutines-reactive 라이브러리를 이용하면 손쉽게 변환이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;kotlinx-coroutines-reactor 라이브러리를 활용하면 Flow를 Flux로 변환할 수도 있다. kotlinx-coroutines-rx3(또는 kotlinx-coroutines-rx2) 라이브러리를 이용하면 Flow를 Flowable 또는 Observable로 변환할 수도 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1653096826746&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;suspend fun main() = coroutineScope {
    Flux.range(1, 5).asFlow()
        .collect { println(it) }

    println()
    
    flowOf(1, 2, 3, 4, 5).asFlux()
        .doOnNext { println(it) }
        .subscribe()
}

// 1
// 2
// 3
// 4
// 5

// 1
// 2
// 3
// 4
// 5&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Flow Builders&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Flow를 생성하는 방법 중 가장 흔한 방법은 flow 빌더를 사용하는 것이다. flow 빌더는 sequence 빌더와 produce 빌더와 비슷하게 람다 표현식 정의를 통해 Flow를 생성하는 함수이다. emit()를 통해서 다음 값을 보내거나, emitAll()를 통해서 모든 값을 Channel 또는 Flow로 보낼 수 있다. emitAll(flow)은 내부적으로 flow.collect { emit(it) } 로 정의되어 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;private fun makeFlow(): Flow&amp;lt;Int&amp;gt; = flow {
    repeat(3) { num -&amp;gt;
        delay(1000)
        emit(num)
    }
}

suspend fun main() = coroutineScope {
    makeFlow()
        .collect { println(it) }
}
// (1 sec)
// 0
// (1 sec)
// 1
// (1 sec)
// 2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;channelFlow&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;element를 처리하는 중에 페이지를 미리 가져와야 하는 경우가 있다. 페이지를 미리 가져오면 네트워크 호출은 더 많이 이루어질 수 있지만 더 빠른 결과를 얻을 수 있다. 이를 위해서는 독립적인 프로듀싱과 컨슈밍이 필요한데, 이러한 독립성은 Channel과 같은 Hot data stream의 형태에서 자주 볼 수 있다. 따라서 위와 같은 기능을 위해서는 Channel과 Flow의 특징을 모두 가진 하이브리드 형태가 channelFlow 함수이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;public fun &amp;lt;T&amp;gt; channelFlow(@BuilderInference block: suspend ProducerScope&amp;lt;T&amp;gt;.() -&amp;gt; Unit): Flow&amp;lt;T&amp;gt; =
    ChannelFlowBuilder(block)

public interface FusibleFlow&amp;lt;T&amp;gt; : Flow&amp;lt;T&amp;gt;

public abstract class ChannelFlow&amp;lt;T&amp;gt;(
    // upstream context
    public val context: CoroutineContext,
    // buffer capacity between upstream and downstream context
    public val capacity: Int,
    // buffer overflow strategy
    public val onBufferOverflow: BufferOverflow
) : FusibleFlow&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;channelFlow 빌더는 Flow 인터페이스를 구현하고 있다. 그리고 collect와 같은 종단연산에 의해 시작된다. 동시에 Channel과 같이 일단 시작되면, 분리된 코루틴 속에서 리시버를 기다리지 않고 값을 프로듀싱한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;덕분에 아래의 예시처럼 다음 페이지를 가져오는 것과 사용자를 확인하는 작업이 동시에 수행될 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;private data class User(val name: String)

private interface UserApi {
    suspend fun takePage(pageNumber: Int): List&amp;lt;User&amp;gt;
}

private class FakeUserApi : UserApi {
    private val users = List(20) { User(&quot;User $it&quot;) }
    private val pageSize: Int = 3

    override suspend fun takePage(pageNumber: Int): List&amp;lt;User&amp;gt; {
        delay(1000)
        return users
            .drop(pageSize * pageNumber)
            .take(pageSize)
    }
}

private fun allUsersFlow(api: UserApi): Flow&amp;lt;User&amp;gt; = channelFlow {
    var page = 0
    do {
        println(&quot;Fetching page $page&quot;)
        val users = api.takePage(page++)
        users.forEach { send(it) }
    } while (!users.isNullOrEmpty())
}

suspend fun main() {
    val api = FakeUserApi()
    val users = allUsersFlow(api)
    val user = users
        .first {
            println(&quot;Checking $it&quot;)
            delay(1000)
            it.name == &quot;User 3&quot;
        }
    println(user)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;channelFlow 내부에는 ProducerScope&amp;lt;T&amp;gt;에서 연산이 수행되는데, produce 빌더에서 사용되는 타입과 같다. ProducerScope는 CoroutineScope를 구현하고 있다. 따라서 channelFlow 빌더를 통해서 새로운 코루틴을 시작할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;element를 프로듀싱하기 위해서 emit 대신에 send()를 사용하고, SendChannel를 통해서 channel에 직접 접근하거나 제어할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;public interface ProducerScope&amp;lt;in E&amp;gt; : CoroutineScope, SendChannel&amp;lt;E&amp;gt; {
    public val channel: SendChannel&amp;lt;E&amp;gt;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;channelFlow는 독립적으로 값을 연산해야할 필요가 있을 때 주로 사용한다. 이를 위해 channelFlow는 코루틴 스코프를 생성한다. 따라서 launch와 같이 코루틴 빌더를 바로 실행할 수 있다.&amp;nbsp; 다른 코루틴들과 마찬가지로 channelFlow도 모든 자식 코루틴이 종료 상태가 될 때까지 기다린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;callbackFlow&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;클라이언트의 이벤트를 리스닝할 때는 처리하는 프로세스와 독립적이어야 하므로 channelFlow가 적합하지만 더 나은 대안으로 callbackFlow가 있다. channelFlow는 ProducerScope&amp;lt;T&amp;gt;위에서 수행되지만, Callback을 래핑함으로써 channelFlow와 차이점을 보인다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;awaitClose { /* ... */ }
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;channel이 닫힐 때까지 일시중단 시키는 함수이다. 채널이 닫히고나면, 인자로 넘어온 본문을 호출한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;trySendBlocking(value)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;send와 유사하지만 일시중단 대신에 블로킹한다. 일시중단 함수가 아닌 함수에 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;close()
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;채널을 종료시킨다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;cancel(throwable)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;채널을 종료하고, flow로 예외를 보낸다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고자료&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.amazon.com/Kotlin-Coroutines-Deep-Marcin-Moskala/dp/8396395837&quot;&gt;https://www.amazon.com/Kotlin-Coroutines-Deep-Marcin-Moskala/dp/8396395837&lt;/a&gt;&lt;/p&gt;</description>
      <category>Development/Kotlin</category>
      <author>Icarus8050</author>
      <guid isPermaLink="true">https://icarus8050.tistory.com/158</guid>
      <comments>https://icarus8050.tistory.com/158#entry158comment</comments>
      <pubDate>Sat, 21 May 2022 19:36:26 +0900</pubDate>
    </item>
    <item>
      <title>[Coroutine] 코루틴 학습 - 14 (Hot and Cold data sources)</title>
      <link>https://icarus8050.tistory.com/157</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Hot data source&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컬렉션(List, Set 등..)과 같이 컨슈밍과 상관없이 데이터가 미리 생성되어 프로듀싱 되는 데이터를 hot이라고 한다.&lt;/li&gt;
&lt;li&gt;Channel 또한 hot에 속한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Cold data source&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Sequence, Java Stream과 같이 실제로 데이터를 필요로 할 때 프로듀싱하는 것을 cold라고 한다.&lt;/li&gt;
&lt;li&gt;Flow, RxJava streams(Observable, Single 등..)을 cold에 속한다.&lt;/li&gt;
&lt;li&gt;무한하게 실행될 수 있다.&lt;/li&gt;
&lt;li&gt;최소한의 연산만 수행할 수 있다.&lt;/li&gt;
&lt;li&gt;메모리를 즉시 할당할 필요가 없기 때문에 필요한 만큼 최소한의 메모리만 사용이 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1652837954773&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;fun main() {
    val l = buildList {
        repeat(3) {
            add(&quot;User$it&quot;)
            println(&quot;L: Added User&quot;)
        }
    }

    val l2 = l.map {
        println(&quot;L: Processing&quot;)
    }

    val s = sequence {
        repeat(3) {
            yield(&quot;User$it&quot;)
            println(&quot;S: Added User&quot;)
        }
    }

    val s2 = s.map {
        println(&quot;S: Processing&quot;)
        &quot;Processed $it&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Sequence는 element 연산을 뒤로 미루기 때문에 필요한 만큼의 최소 연산만 수행할 수 있다. Sequence는 각 중간 연산(map 또는 filter와 같은)이 이전의 시퀀스를 데코레이팅하여 동작한다. 그리고 최종 연산에서 이 모든 연산들이 수행된다. 이러한 특징은 모든 element를 메모리 위에 올려놓고 수행해야하는 컬렉션과 큰 차이점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;컬렉션은 중간 연산마다 모든 element를 처리하여 모든 컬렉션 데이터들을 처리하는 방식으로 동작한다. 이는 Sequence보다 많은 메모리를 필요로 하는 이유이다.&lt;/p&gt;
&lt;pre id=&quot;code_1652839023087&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private fun m(i: Int): Int {
    print(&quot;m $i &quot;)
    return i * i
}

private fun f(i: Int): Boolean {
    print(&quot;f $i &quot;)
    return i &amp;gt;= 10
}

fun main() {
    listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
        .map { m(it) }
        .find{ f(it) }
        .let { print(&quot;$it &quot;) }

    println()

    sequenceOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
        .map { m(it) }
        .find { f(it) }
        .let { print(&quot;$it &quot;) }
        
    println()
}
// m1 m2 m3 m4 m5 m6 m7 m8 m9 m10 f1 f4 f9 f16 16
// m 1 f 1 m 2 f 4 m 3 f 9 m 4 f 16 16&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;위 예시를 보면 List 컬렉션은 map 연산이 모든 element에 대해서 수행되지만, Sequence는 lazy하게 동작하여 map 연산이 최소한으로만 동작하는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;그렇다면 무조건 Cold data stream이 좋은 것인가?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;그렇지는 않다. hot 데이터는 미리 연산이 되어 있기 때문에 재연산할 필요가 없다는 장점이 있다. 상황에 따라서 필요에 맞게 적절하게 사용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Hot channels, Cold flow&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;channel를 생성할 때 produce 빌더를 사용할 때와 마찬가지로 flow 또한 빌더를 통해 생성할 수 있다. 해당 빌더는 flow()이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;Channel과 Flow는 유사해보이지만, Channel은 Hot data stream, Flow는 Cold data stream라는 차이가 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1652847725473&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private fun CoroutineScope.makeChannel() = produce {
    println(&quot;Channel started&quot;)
    for (i in 1..3) {
        delay(1000)
        send(i)
    }
}

suspend fun main() = coroutineScope {
    val channel = makeChannel()

    delay(1000)
    println(&quot;Calling channel...&quot;)
    channel.consumeEach { value -&amp;gt; println(value) }
    println(&quot;Consuming again...&quot;)
    channel.consumeEach { value -&amp;gt; println(value) }
}
// Channel started
// Calling channel...
// 1
// 2
// 3
// Consuming again...&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Channel은 hot data stream으로, 호출되면 즉시 값을 연산한다. 이때 연산은 각 코루틴에서 시작된다. 이는 produce가 CoroutineScope의 확장 함수로 코루틴 빌더를 정의해야 해야하는 이유다.&lt;/li&gt;
&lt;li&gt;위 예시는 디폴트 버퍼 사이즈가 0(rendezvous)이므로 리시버가 컨슘하기 전까지 일시중단된다.&lt;/li&gt;
&lt;li&gt;Channel은 각 element를 컨슘과는 독립적으로 생성하고 유지한다.&lt;/li&gt;
&lt;li&gt;얼마나 많은 리시버가 있는지는 고려하지 않는다.&lt;/li&gt;
&lt;li&gt;각 element는 한 번만 수신될 수 있으므로 첫 번째 리시버가 모든 element를 컨슘해버리면 두 번째로 컨슘하는 코루틴은 channel이 이미 비어있고 close되어 있는 상황이 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1652848981698&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private fun makeFlow() = flow {
    println(&quot;Flow started&quot;)
    for (i in 1..3) {
        delay(1000)
        emit(i)
    }
}

suspend fun main() = coroutineScope {
    val flow = makeFlow()

    delay(1000)
    println(&quot;Calling flow...&quot;)
    flow.collect { value -&amp;gt; println(value) }
    println(&quot;Consuming again...&quot;)
    flow.collect { value -&amp;gt; println(value) }
}
// Calling flow...
// Flow started
// 1
// 2
// 3
// Consuming again...
// Flow started
// 1
// 2
// 3&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Flow는 Cold data stream으로, 실제로 연산이 필요로 할 때 element가 생성된다.&lt;/li&gt;
&lt;li&gt;flow는 element가 어떻게 생성되어야 하는지만 정의한다. 정의된 플로우는 종단 연산이 호출 되었을 때 사용된다.&lt;/li&gt;
&lt;li&gt;이는 flow 빌더가 CoroutineScope를 필요로하지 않는 이유이다.&lt;/li&gt;
&lt;li&gt;flow는 종단 함수가 실행됐을 때에만 코루틴 스코프 내에서 수행된다.&lt;/li&gt;
&lt;li&gt;Channel과 다르게 각 종단 연산은 element 재소비가 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고자료&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.amazon.com/Kotlin-Coroutines-Deep-Marcin-Moskala/dp/8396395837&quot;&gt;https://www.amazon.com/Kotlin-Coroutines-Deep-Marcin-Moskala/dp/8396395837&lt;/a&gt;&lt;/p&gt;</description>
      <category>Development/Kotlin</category>
      <author>Icarus8050</author>
      <guid isPermaLink="true">https://icarus8050.tistory.com/157</guid>
      <comments>https://icarus8050.tistory.com/157#entry157comment</comments>
      <pubDate>Wed, 18 May 2022 13:54:24 +0900</pubDate>
    </item>
  </channel>
</rss>