-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathrss2.xml
510 lines (250 loc) · 443 KB
/
rss2.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
xmlns:atom="http://www.w3.org/2005/Atom"
xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>CH DEVLOG</title>
<link>https://changhoi.kim/</link>
<atom:link href="https://changhoi.kim/rss2.xml" rel="self" type="application/rss+xml"/>
<description>개발 과정 기록</description>
<pubDate>Tue, 31 Dec 2024 15:00:00 GMT</pubDate>
<generator>http://hexo.io/</generator>
<item>
<title>2024년, 창업 1년 차 회고</title>
<link>https://changhoi.kim/posts/logs/20250101/</link>
<guid>https://changhoi.kim/posts/logs/20250101/</guid>
<pubDate>Tue, 31 Dec 2024 15:00:00 GMT</pubDate>
<description><p>2019년 1월 1일 개발 공부를 시작하며 이 블로그를 쓰기 시작했고, 벌써 6년째이지만 이제는 창업가와 개발자 사이에 어중간한 위치에 있는 사람으로서 ‘창업 1년 차’라는 이름으로 회고를 처음 써본다. 제목을 이렇게 정하니까 굉장히 색다른 느낌이다. <a href="/posts/logs/20240101">작년의 나는</a> 꿈을 확정하고 꿈을 이루기까지 나에게 방해가 되는 요소를 제거하는 한 해를 보냈고 이번 해는 실제로 창업에 뛰어들었던 첫 번째 해였다. 창업하면 일반적으로 정말 대부분의 영역에서 불편함을 느끼는 상태가 되어 엄청난 성장을 만들어낼 수 있는 것 같다. 그래서 이번 해는 배운 게 너무 많아서 추리는 과정이 더 오래 걸렸다. 추려낸 것들을 기록하지 못해 아쉬울 정도로 재밌는 한 해를 보냈다.</p></description>
<content:encoded><![CDATA[<p>2019년 1월 1일 개발 공부를 시작하며 이 블로그를 쓰기 시작했고, 벌써 6년째이지만 이제는 창업가와 개발자 사이에 어중간한 위치에 있는 사람으로서 ‘창업 1년 차’라는 이름으로 회고를 처음 써본다. 제목을 이렇게 정하니까 굉장히 색다른 느낌이다. <a href="/posts/logs/20240101">작년의 나는</a> 꿈을 확정하고 꿈을 이루기까지 나에게 방해가 되는 요소를 제거하는 한 해를 보냈고 이번 해는 실제로 창업에 뛰어들었던 첫 번째 해였다. 창업하면 일반적으로 정말 대부분의 영역에서 불편함을 느끼는 상태가 되어 엄청난 성장을 만들어낼 수 있는 것 같다. 그래서 이번 해는 배운 게 너무 많아서 추리는 과정이 더 오래 걸렸다. 추려낸 것들을 기록하지 못해 아쉬울 정도로 재밌는 한 해를 보냈다.</p><span id="more"></span><blockquote><p>창업을 시작하고 블로그에 한 차례도 글을 남기지 않았다는 걸 후회하고 있다. 사실 중간중간 쓸 일이 있었지만 23년 회고 글 다음이 24년 회고 글인 모습도 궁금했다.</p></blockquote><h1 id="2024년의-목표"><a href="#2024년의-목표" class="headerlink" title="2024년의 목표"></a>2024년의 목표</h1><p>24년 1월 1일 역시 새로운 1년 목표를 만들었는데, 다음과 같다.</p><ul><li>서울에 집을 구한다. (출가!)</li><li>창업을 유지할 수 있는 돈이 들어온다.</li><li>창업 팀을 꾸린다.</li><li>PMF를 찾는다.</li><li>건강해진다.</li><li>책을 15권 이상 읽는다 (AI, 비즈니스, 인간에 대한 책 각각 5권씩!)</li></ul><p>지금 상황을 요약하자면 서울에서 지내고 있고, 창업을 유지할 수 있는 돈이 들어오는 상태다. 책도 15권 이상 읽었다. 하지만 건강해지는 건 실패한 것 같다. 오히려 퇴보한 것 같다. 그리고 퇴보의 대부분은 4Q에 발생했다.</p><p>이번 회고는 아직 말하지 않은 두 가지에 초점이 있다. 창업 팀 찾기와 PMF를 찾기 위한 여정이 이번 연도에 주된 목표였다. 책도 너무 재밌게 읽어서 기록하고 싶은 게 너무 많지만, 이 부분은 별도로 기록 해보려고 한다.</p><h1 id="2024년의-목차"><a href="#2024년의-목차" class="headerlink" title="2024년의 목차"></a>2024년의 목차</h1><p>퇴사를 하며, 그리고 창업을 위한 여러 준비를 하던 초반 시기를 지나 프로덕트를 만들고 공동 창업자를 찾기 위해 엄청나게 많은 사람들을 만나던 중반 시기, 그리고 앤틀러(Antler)라는 글로벌 VC가 운영하는 스타트업 제네레이션 프로그램(이하 앤틀러)에 참여하는 후반기로 나눠진다. 아이템 찾기와 사람 찾기 모두 이 구분으로 크게 나눠진다.</p><h1 id="준비-창업-완료"><a href="#준비-창업-완료" class="headerlink" title="준비 창업 완료"></a>준비 창업 완료</h1><img alt="준비 완료" src="/images/2025-01-01-20250101/ready-set.png?style=centerme" /><p>올해 1Q는 창업 준비를 하며 보냈다. 1월 2일에 팀 리더님에게 퇴사 의사를 전달했고 오늘의집 사람들과 퇴사 계획을 만들기 시작했다. 나는 언제 퇴사할 것이며 그 기간 월루하지 않기 위해 어떤 일을 할 것인지, 인수인계는 어떻게 할 것인지 등을 얘기했다. 퇴사 과정 자체가 깔끔했다고 말하긴 어렵지만 지저분하지도 않았다. 앞으로 현금이 중요해지는 나의 입장에서 회사의 마지막 정을 모두 챙기지 않으면 나중에 후회할 것으로 생각했다. 하지만 연봉 협상이라든지 스톡옵션을 채울 정도로 오래 기다릴 수는 없었다. 그래서 2월 말 퇴사가 결정되었다. 그 사이 기간 나는 (회사 일 제외하고) AI를 공부하고 사람들을 슬슬 만나기 시작했다.</p><h2 id="AI-기술을-알아보자"><a href="#AI-기술을-알아보자" class="headerlink" title="AI 기술을 알아보자"></a>AI 기술을 알아보자</h2><p>올해 초에 <a href="https://blog.naver.com/tmdejr1267/221872996382">지금 시점, 다시 읽는 W</a>라는 아티클을 읽었다. 23년에 SORA 데모를 보면서 다음 W가 AI임을 확신한 내가 떠올랐다. 그러면서 AI에 관한 공부를 시작했다. AI를 위한 기본적인 수학과 기술적 기반이 되는 개념을 공부했고 모델을 사용하는 입장으로서 어떻게 모델을 쓰는지에 관한 공부도 많이 했다. 기본과 기술 자체에 관한 공부를 할 때 흥미로운 점도 있었지만 이걸 내가 직접 만들 필요는 없겠다는 생각을 많이 했다. 마치 내가 컴퓨터의 컴포넌트와 프로토콜들이 어떻게 네트워크를 만들어내는지는 이해하고 있지만 직접 만들 필요는 없는 느낌으로. 나는 사용자의 관점에서 어떻게 하면 모델을 더 잘 쓸 수 있는지에 집중해 공부하고 실습했다. 자연스럽게 Prompt Engineering, LLMOps 등을 많이 공부하게 됐다.</p><p>다행히 이때 내린 나의 판단은 꽤 훌륭했던 것 같다. 지금까지도 적절하게 공부한 기본 개념 덕분에 모델을 사용하는 방법에 대해서도 더 잘 이해할 수 있는 것 같고 무엇보다 그때 많이 공부했던 응용 레벨의 지식을 지금까지도 잘 사용하고 있다. 특히 중간에 만들었던 프로덕트가 <a href="https://lovable.dev/">Lovable</a>, <a href="https://www.marblism.com/">Marblism</a> 같은 Full-stack AI를 만드는 것이었는데, 이때 AI 사용을 굉장히 빡세게 하면서 단순한 것보다는 조금 더 많은 걸 공부하고 사용해 봤던 것 같다.</p><h2 id="AI-비즈니스를-알아보자"><a href="#AI-비즈니스를-알아보자" class="headerlink" title="AI 비즈니스를 알아보자"></a>AI 비즈니스를 알아보자</h2><p>기술 공부를 하면서 동시에 AI를 사용한 비즈니스를 같이 공부했다. 케이스 스터디에 가까웠는데 AI 리서치 클럽이라는 활동을 했다. VC 호스트가 AI와 관련된 비즈니스에 대한 인사이트를 나눠주고 읽기 좋은 아티클을 전달해 주면 사람들이 이를 읽고 공부하는 형식의 스터디였다.</p><p>‘지금 시점, 다시 읽는 W’라는 아티클도 여기서 소개를 받았고 이것뿐만 아니라 재밌는 아티클을 많이 소개받아서 소장하고 있다. 사실 이걸 처음 시작할 때는 AI의 흐름에 타고 싶은 창업가를 만나보고 싶은 생각이었는데 아쉽게도 여기서 더 연락을 이어간 사람은 없긴 하다. 하지만 활동 자체가 꽤 마음에 들었다.</p><blockquote><p><strong>지금 시점, 다시 읽는 W</strong><br>한 외과 의사는 지인에게 특별한 강연에 초대를 받았다. 바쁜 시간을 쪼개서 갔더니 강연자 W는 <code>WWW</code>를 칠판에 적고 ‘인터넷이 세상을 지배한다’는 주제로 강연했고 외과 의사는 미친 소리라며 강연에 초대한 지인을 구박했다. 반면 이를 같이 들은 친구는 W를 찾아가 더 알려달라며 따라다니고 인생을 건 배팅(사업)을 시작했다. 그 사업은 2조가 넘는 사업으로 성장했으며, 강연자 W는 이재웅 대표님이라고 한다. 외과 의사는 ‘같은 강연을 들었는데 왜 본인은 미친 소리로 듣고, 다른 친구는 인생을 건 배팅을 할 수 있었을까?’에 대해 고민했고 제레미 러프킨으로부터 답을 얻었다고 한다.</p><p>지금의 문명에 ‘인류가 기여했다’라고 말하곤 하지만 사실은 인간의 역사는 1%가 만들어낸 역사라고 말한다. 1%는 새로운 문명을 개척하고 99%의 인류는 “세상 참 좋아졌다.”, “기술 발전이 참 빠르다.”라고 말하며 세상을 따라가는 ‘잉여 인간’이라고 말한다.<br>1% 중에서도 크게 둘로 나눠지는데, 0.1%는 새로운 영역을 만들어내는 사람이고 0.9%는 통찰력을 가지고 그 세상으로 뛰어든다. 이야기에서 이재웅 대표님은 0.1%, 친구는 0.9%, 외과 의사는 99%이고 포드가 0.1%, 록펠러는 0.9%, 그 외는 99%인 것이다.<br>인터넷은 이미 성공으로 판명 난 인류 문명 혁신이다. 따라서 우리가 이 글을 읽더라도 적어도 0.9%가 되는 것이 그렇게 어렵지 않은 것처럼 느껴진다. 하지만 우리는 아직 불확실한 현재 상황에서 돈키호테와 진짜를 구분할 수 있어야 한다.</p></blockquote><hr><p>회사를 퇴사한 시점부터 크게 2개의 Phase로 나눠진다. 앤틀러 참여 전(Before Antler)과 앤틀러 참여 후(After Antler)이다. 공동 창업자를 찾는 과정에 대한 회고와 PMF를 찾는 과정에 대한 회고 모두 적용되는 구분법이다.</p><h1 id="공동-창업자를-찾기-위한-여정"><a href="#공동-창업자를-찾기-위한-여정" class="headerlink" title="공동 창업자를 찾기 위한 여정"></a>공동 창업자를 찾기 위한 여정</h1><p>창업을 하면서 1월 1일부터 사람을 만나기 시작했으나 본격적으로 공동 창업자를 찾을 목적으로 사람들을 탐색하는 건 3월 초부터인 것 같다. 내가 사람을 만났던 채널은 앤틀러를 제외하면 지인의 소개, LinkedIn에서 직접 연락드리기, Y Combinator의 Co-Founder 매칭 플랫폼이었다. 앤틀러 외에 거의 50분 가까이 뵌 것 같고 앤틀러에서는 80명 정도 되는 인원을 만났다. 당연히 너무 인상 깊었던 사람도 있고 와중에 많이 친해졌던 사람도 있다.</p><p>공동 창업자를 찾다 보니 자신의 목표를 이루기 위해 컴포트존에서 벗어나 자신의 목표를 이루기 위해 노력하는 사람들을 많이 만나게 됐다. 많은 분으로부터 자신들의 여정에 대해서도 얘기해주고 비슷한 상황을 바라보는 신선한 생각을 얻고 재밌게 얘기해 볼 수 있었다. 지금 회고 쓰는 시점에서 생각해 보니 앞으로 이런 사람들과 더 많이 만나게 될 거고 그런 사람들하고 일하게 될 것으로 생각하면 희열감을 느낀다.</p><h2 id="어떤-사람과-같이-오랜-시간을-보낼-수-있나요"><a href="#어떤-사람과-같이-오랜-시간을-보낼-수-있나요" class="headerlink" title="어떤 사람과 같이 오랜 시간을 보낼 수 있나요?"></a>어떤 사람과 같이 오랜 시간을 보낼 수 있나요?</h2><p>그러나 기쁜 건 기쁜 거고 공동 창업자를 찾는 여정 자체는 너무나 험난했다. 아마 작년에 퇴사 준비를 하면서 ‘어떤 사람과 함께 일해야 할까?’에 대한 생각을 조금씩 했던 것 같다. 그리고 1월 1일부터 사람들을 조금씩 만나보면서 이 생각을 구체적으로 하기 시작했다.</p><p>과거 나는 공동 창업자의 자리를 내려놓고 퇴사를 결정한 적이 있다. 나의 이러한 선택은 어떻게 만들어진 걸까? 협상 자체는 사소한 문구들까지 검토하면서 얘기를 나눴지만, 사실은 큰 원인은 하나 있었던 것 같다. 나는 창업 말고도 하고 싶은 게 너무 많았다. 엄청나게 잘하는 엔지니어가 되고 싶다는 생각도 아주 컸고, 막연하게 미국에서 일하고 싶다는 생각도 했다. 당시 나에게 공동 창업자 자리는 이 둘을 만족시키는 가장 빠르고 확실한 길이 아니었다. 그래서 계약서상 오랜 기간을 헌신해야 하는 역할에 큰 고민을 했던 것 같다. 다른 말로 하자면 나는 공동 창업자로서 적합한 인물은 아니었다. 투자 직전에 이런 결정을 내리고 나오게 된 것은 어찌 보면 팀에게 다행일 수도 있지만 창업을 하는 이 시점에서 다시 생각해 보면 대표님이 짧게나마 고통을 느끼셨으리라는 짐작도 한다. 어찌 됐든 나는 ‘내가 지금보다 더 성장했을 때, 또 이러한 좋은 기회가 올 것이다’라고 생각하고 나의 발목을 잡던 다른 목표를 해치우러 떠났다.</p><img src="/images/2025-01-01-20250101/sorry-boss.png?style=centerme" alt="솔직히 팀이 너무 좋아서 고민 많이 했다. 그때 배운 것, 생각, 지금까지 나에게 미치는 영향력 모두 감사하다." /><blockquote><p>지금은 그런 것들을 모조리 해치웠고 남은 인생의 목표가 창업을 통해 달성할 것들만 남았다.</p></blockquote><hr><p>나는 훌륭한 사람과 최대한 오래 붙어 회사를 만들어내고 싶다. 그러려면 적어도 나는 ‘19시즌 김창회’ 같은 사람은 걸러낼 수 있어야 한다. 그래서 가장 중요하게 생각했던 건 창업의 동기였다. 왜 창업을 하고 싶어 하는가? 꽤 오랜 시간을 이 여정에 걸어도 괜찮을 만큼, 창업 말고는 이룰 수 없는 어떤 인생의 최종 목표가 있는 건가를 열심히 들어보려고 했다.</p><p>그리고 그 당시 ‘어떻게 하다가 팀이 깨질 수 있을까?’를 고민하면서 몇 가지를 만들어봤다.</p><ol><li><strong>나만큼 꿈이 큰 사람</strong>: 아마도 오랜 기간 같이 할 사람이라는 기준이 있기 때문에 필요하다고 생각했던 것 같다. 꿈의 크기는 목표하는 비즈니스의 크기라고 생각해 봐도 좋을 것 같다. 우여곡절 끝에 PMF를 찾더라도 정말 끝까지 갔을 때 캡이 제한적이라고 확실하게 판단되면 과감하게 다른 걸 해보자고 말해줬으면 좋겠다.</li><li><strong>도메인에 특정되지 않는 사람</strong>: 어떤 특정 도메인에 엮여있다면 그 영역에서 문제를 찾기에 실패했을 때 팀이 와해 되지 않을까 하는 걱정이 있다.</li><li><strong>일이 즐거운 사람</strong>: 일을 많이 하는 것 자체에 스트레스를 느끼지 않고 즐겁게 일했으면 좋겠다.</li><li><strong>배우는 것에 자신 있는 사람</strong>: 생소한 영역에서도 ‘배우면 되지’라는 마인드가 필요하고 순수 지능과도 관련이 있는 것 같다. 여러 도메인을 시도할 수 있다는 것도 장점이 된다.</li></ol><p>앤틀러 전까지 대충 4~50명을 만나면서 얘기하면서 느낀 점은 위 네 가지는 몇 번 얘기해 본다고 짐작하기 어렵다는 것이다. 일 즐겁게 하기 능력 평가, 배우기 자신감 자격시험 같은 걸 만들 수 없는 아주 개인적인 기준표를 가지고 타인과 얘기하므로 서로 같은 단어를 쓰더라도 서로 다른 모습을 그리는 경우가 많다. 그래서 창업 동기까지만 열심히 물어보고 나머지는 얘기는 해보지만, 함께 뭔가 해보면서 알아가야겠다고 생각했다.</p><h2 id="Before-Antler-BA"><a href="#Before-Antler-BA" class="headerlink" title="Before Antler(BA)"></a>Before Antler(BA)</h2><p>위에서 말했던 것처럼 앤틀러 시작 전까지 LinkedIn, YC Co-Founder Matching을 애용해다. 그때 만난 분들과 같이 뭔가 해보지는 못했다. 왜 그랬을까? 창업을 하는 사람 중에서 공동 창업자를 찾는 것에만 몰두하며 다른 것을 손 놓고 있는 사람은 거의 없다. 다들 들고 있는 아이템 한두 개는 있고 거기에 관심 있는 사람을 찾게 된다. 근데 서로 창업을 하려고 하는 둘이 방금 만나서 지금 내가 하고 있는 아이템을 같이 해보지 않겠냐고 했을 때 상대가 그에 동의하며 시작되는 케이스가 얼마나 있을까 싶다. 그래서 사실 내가 지금 하고 있던 아이템을 멈추고라도 같이 일을 해보는 것에 초점을 맞춰봤더라면 어땠을까?</p><p>반면 저 기준들이 생각보다 까다로운 것일 수도 있겠다는 생각도 든다. 생각보다 까다로워서 지금 하는 걸 멈추고 ‘이 사람과 뭔가 더 해봐야겠어’라는 생각이 잘 들지 않았던 것 같다. 특히 창업 동기가 창업이 아니라면 이룰 수 없는 무언가가 있으면서도 어떤 특정 도메인에 묶이지 않은 사람은 양립이 어려운 조건일 수도 있을 것 같다. 나 역시 에너지라는 도메인에 최종적으로 접근하고 싶다는 목표가 있다. 다만 이 목표는 인생의 오랜 기간을 거쳐 점진적으로 도달할 목표라고 생각하다 보니 지금 특별히 이 도메인에 묶이지 않게 된 것이다. 이런 경우가 아니라면 어떤 특별한 도메인의 문제를 해결하고 싶은 것도 아니면서 엄청 오랜 시간을 여기에 쏟아야겠다고 생각하는 사람을 보기 쉽지 않다.</p><img src="/images/2025-01-01-20250101/left-and-right.png?style=centerme" alt="왼쪽을 보면서 오른쪽을 보란 말이야"/><p>앤틀러를 했다는 것은 결국 이 기간에 공동 창업자를 찾지 못했다는 것을 의미하지만 정말 감사한 인연들을 많이 만났다. 당장 창업에 뜻이 있는 것은 아니지만 프로덕트를 만들 때 직접적으로 도움을 주셨던 분들도 있고, 심지어 여름 동안 집의 방 한 칸을 내어주신 분도 만났다. 정말 뭐라도 행동해야 이런 인연도 생기고 조금씩이라도 앞으로 간다는 걸 많이 느꼈다.</p><blockquote><p>에어컨이 없어 너무 더운 여름에 출가를 도와주신 고마운 분을 YC를 통해 알게 됐다. 서로의 타이밍이 안 맞아서 함께 팀을 이루지 못했지만, 여름 동안 서울에서 지내면서 많은 사람을 더 쉽게 만날 수 있었고 지금 지낼 집을 찾는 것도 수월했고 무엇보다 탈 없이 지낼 수 있었다. 창업에 대한 이해도 높은 분이라 가끔 나의 상황에 대한 의견을 여쭤보기도 했다. 앤틀러 직전 기수를 하셔서 앤틀러를 추천해 주셨고 최종적으로 앤틀러를 하기로 결정하게 되기도 했다. 내가 드린 도움은 한 개도 없지만 언젠간 도움을 갚을 날이 생기길 바라면서 연락은 가끔 하고 있다.</p><p>내가 하고 있던 프로덕트에 관심이 있으셔서 같이 프로젝트를 시작했던 사람도 있다. 중간에 아이템이 바뀌면서 사실 관심사에서 멀어졌을 수도 있는데 그다음 아이템까지도 도움을 주셨다. 이 사람도 정말 똑똑하고 배울 점 많은 친구로 잘 지내고 있다. 이분에게도 언젠간 도움을 드릴 수 있길 바란다.</p></blockquote><img src="/images/2025-01-01-20250101/sorry-boss.png?style=centerme" alt="생판 남인 나를 도와주신 여러 귀인분에게 무한한 감사를"><h2 id="After-Antler-AA"><a href="#After-Antler-AA" class="headerlink" title="After Antler(AA)"></a>After Antler(AA)</h2><p>앤틀러는 내가 ‘창업가를 찾고, 연락드리고, 얘기를 나눠보고’ 하는 사이클의 대부분을 해결해 주는 고마운 프로그램이었다. 나는 애초에 목표를 많이 두고 다음 액션을 결정하는 사람은 아닌지라 오직 ‘공동 창업자를 찾자’라는 목표만 생각하고 앤틀러에 참여했다. 그러나 80명 정도 되는 창업가들을 만나면서 공동 창업 여부와 상관없이 귀중한 인연도 생기고 내가 가지고 있던 생각들도 몇몇 바뀌는 경험도 했다.</p><h3 id="공식적인-활동-기간-동안"><a href="#공식적인-활동-기간-동안" class="headerlink" title="공식적인 활동 기간 동안"></a>공식적인 활동 기간 동안</h3><p>그러나! 여전히! 공동 창업자를 찾는 과정이 쉽지는 않았다. 창업하겠다는 인재를 80명이나 모아놔도 각자의 기준에 따라 같이 창업을 할 사람과 아닌 사람을 확실히 구분할 수 있게 된다. 그래서 ‘80명이나 되는데 언제 다 맞춰보지~’ 이런 고민은 별로 의미가 없는 고민이 된다. 공식적으로 한 달 반 조금 넘는 기간 동안 10번 정도 팀을 바꿔가면서 아이템을 만들어보게 되는데 이때 정말 많은 사람들과 같이 해본 것 같다. 특히 나는 거의 겹치지 않고 사람들하고 팀을 맞춰봐서 더 많이 해볼 수 있었던 것 같다. 그중 두 번은 <a href="https://proof-assets.s3.amazonaws.com/firstround/50%20Questions%20for%20Co-Founders.pdf">공동 창업자끼리 해야 하는 50개의 질문</a>(앤틀러에서는 한글로 간단히 번역된 걸 제공해 주는 것 같다!)도 해보고 지분에 대한 얘기도 깊게 하는 등 창업 팀으로 발전해 가는 경험도 있었다.</p><p>첫 번째 팀은 개인적으로 기대가 되는 팀이었지만 많은 걸 해보지는 못한 상태로 끝났다. 공동 창업자분이 앤틀러 활동을 하다 보니 생각보다 자신이 하고 싶은 도메인이 정해져 있는 것 같다고 하셨다. 그리고 그 부분이 내 생각에는 좁아서 위에 얘기했던 기준에 부합하지 않았다.</p><blockquote><p>생각보다 앤틀러 참여자 중 이런 경우가 종종 있었다. 도메인에 특별히 묶이지 않았다고 생각했지만, 하다 보니 관심 있는 도메인 외에는 하지 않고 싶어 하는 경우이다. 나도 그런 게 아예 없는 건 아니었지만, ‘꼭 하고 싶은 도메인’이 생기기보단 ‘하기 싫은 도메인’이 생겼다. 보통 최종 모습을 상상했을 때 그 규모가 상당하면 재미를 느꼈지만 에듀 테크에는 흥미를 못 느꼈다.</p></blockquote><p>그다음 팀도 이상적인 팀을 만났다고 생각했지만, 생각보다 일하는 방식이 잘 안 맞아서 팀이 와해됐다. 이때 만나 뵌 형님들은 그냥 하는 소리가 아니라 배울 점이 너무 많고 친절하시고 오래오래 친하게 지내고 싶은 사람들이었지만 일하면서 커뮤니케이션 문제가 몇 번 발생했다. 지금도 돌이켜 생각해 보면 서로의 긴밀한 피드백으로 해결할 수 있지 않았을까? 하는 생각은 있다. 워낙 이상적이라고 느꼈던 탓인가 내가 계속 지랄 지랄했던 것 같기도 하다. 정말 비범한 무언가를 만들어보자며 만난 사람들에게 누구에게든(나는 당연히 포함해서) 충족하기 어려운 강도 높은 기준을 요구하는 게 이상한 일은 아니라고 생각했다.</p><img src="/images/2025-01-01-20250101/zrzr.png?style=centerme" alt="지랄 지랄은 조금 과한 표현일지도? 잔소리?"/><p>당시 팀의 리더분은 그 시점의 불협화음이 아마도 조율하기 어려운 성질의 차이라고 보신 것 같다. 아마 이런 마찰 자체는 내가 이번에 지랄 지랄하지 않았어도 언젠간 나왔을 것 같기 때문에 잘못된 선택을 하신 것 같지 않다. 이때 나 스스로에게 실망스러운 일도 있었고 유지 가능한, 그러면서도 정말 Outstanding 한 팀을 만드는 것에 대한 고민도 많이 하게 됐다.</p><blockquote><p>당시 생각과 행동이 지금 아주 많이 달라진 건 아니다. 여전히 공동 창업자에게, 특히 대표에게 이런 어려운 것들을 해내길 바란다. 그래서 내가 대표를 해야겠다는 생각이 들었다.</p></blockquote><h3 id="외부인"><a href="#외부인" class="headerlink" title="외부인"></a>외부인</h3><p>앤틀러에서 마지막 팀이 와해되고 우연히 LinkedIn을 통해 알게 된 앤틀러 외부 창업가와도 힘을 맞춰봤다. 이때는 앤틀러에서의 활동과 다르게 프로덕트를 하나 잡고 만들면서 시작했다. 간만에 이렇게 외부에서 ‘일단 해보자’ 같은 느낌으로 시작해 보니 기분이 묘했다. 매우 날카롭게 문제 정의하는 데 시간을 엄청나게 쓰는 앤틀러에 익숙해져서 불편함이 있긴 했다.</p><p>당시 내가 만들던 아이템은 내가 공감하기는 어려운 제품이었지만, 팀에서 이걸 메인으로 하기보단 팀끼리 AI 프로덕트로 돈을 버는 경험을 만들기 위한 제품이었다. 이 제품을 만드는 데 대충 3주를 꽉 채웠다. 그다음 4주 차가 되는 날에 공동 창업자로 괜찮은 사람이었을지 여쭤봤다.</p><p>3주까지는 이미 계획된 프로덕트를 만들었으니. 프로덕트를 주도적으로 만드는 모습을 더 보면 좋겠다고 하셨다(토시가 모두 다른 것 같지만 뭐… 이런 뉘앙스였던 듯). 그리고 지금의 모습으로는 판단하기 어려울 것 같다고 하셨다. 이걸 여쭤보면서도 그런 답을 주실 것을 예상은 했지만, 어차피 같이하게 되면 내가 당분간 메인으로 사용할 능력치를 보여드린 것이기도 하고, 말씀하신 그런 주도적인 모습이 4주 차까지 안 보였다면 아마 이후에도 만족하실 만큼 보여드릴 순 있을까? 하는 생각이 들었다. 그리고 무엇보다 공식적인 엔틀러 일정이 곧 끝나가서 공동 창업자를 찾을 기회가 줄어들 것 같아 여기까지 맞춰보는 걸로 하고 팀에서 나오게 됐다.</p><p>결과적으로 나의 상대적 부족함으로 함께 창업하지 않게 됐다. 말씀하신 역량이 정말로 부족했던 것이라고 생각이 들진 않았다. 하지만 결과적으로 내가 보여주지 못한 거라서 어떻게 일해야 이런 부분이 드러날까, 왜 잘 드러나지 않았을까 하는 고민을 해보게 됐다.</p><p>대표님이 그간 일한 걸 월급으로 주려고 하셨는데 나도 같이 일해보기로 한 주체인데 돈 얼마 달라고 하는 것도 좀 이상한 그림이고… 뭐 결과 낸 것도 없기도 하고… “다음번에 제가 도움받을 일이 있겠죠.”하고 마무리 지으려고 했지만, 대표님이 정말 비싼 밥을 사주시는 걸로 마무리됐다. 같이 일할 때 대표님이 ‘자신이 사회적 자본을 잘 쌓아 도와주시는 분들이 많다’고 하셨는데 이렇게 여러 해를 일하셨다면 사회적 자본이 잘 쌓였을 것 같다는 생각이 들었다. 비록 같이 창업하진 않지만, 종종 인사드리면서 친하게 지내면 좋겠다는 생각했다.</p><blockquote><p>물론 밥 잘 사줘서 사회적 자본이 쌓였다고 말씀하신 건 아니다. 이루어온 업적을 통해 주로 획득하셨겠지만 사회적 자본이란 종합적인 거니까. 종종 인사드리고 싶은 것은 맛있는 저녁을 사주신 것과 관계가 희미하다.</p></blockquote><blockquote><p>3주 동안 만든 프로덕트는 자꾸 iOS의 거센 반대에 좌절됐다. 다 만들었으니 올리는 것까지 도와드리겠다고 했는데 맛있는 것만 얻어먹고 지금까지도 못 하고 있다는 사실에 우울감이 든다.</p></blockquote><img src="/images/2025-01-01-20250101/no-no-no.png?style=centerme" alt="앱스토어 거세게 반대라스"><hr><h3 id="돌아온-탕아"><a href="#돌아온-탕아" class="headerlink" title="돌아온 탕아"></a>돌아온 탕아</h3><p>외부에서 열심히 힘을 맞춰보고 다시 앤틀러 내부로 돌아왔다. 당시 계획은 두 개 있었다. 앤틀러 안에서 다시 한번 같이 창업할 사람을 찾거나, 혼자서 아이디어를 깎고 프로덕트를 만드는 것이다.</p><p>일단 내부에 있던 사람 중 그래도 합이 괜찮을 것 같다고 생각하는 분들과 얘기를 간단히 했고 그중에서 첫 번째로 팀을 이뤘던 분이 도메인이 좁아졌던 것이 해소가 되었고 팀도 따로 없는 상태가 되셨다고 해서 다시 팀을 꾸려서 무언가 해보고 있다. 이전에도 그렇고 지금도 그렇고 특별히 잘 안 맞는 부분이 없던 팀이라 아직도 걱정되는 부분 없이 프로덕트를 만들어가고 있다. 그래서 공동 창업자를 찾겠다는 24년도 목표가 하나 해소된 상태이다.</p><hr><p>한때 YC 매칭 플랫폼에 안 들어가던 시기가 있었다. 그러자 YC가 뭔가 메일을 보내줬는데, 거기 내용에는 공동 창업자를 찾는 사람의 중간값은 100일, 20%는 8개월이 걸린다는 내용이었다. 그러니 쉽게 포기하지 말라는 얘기였다.</p><img src="/images/2025-01-01-20250101/yc-letter.png?style=centerme" alt="Great founders are persistent" /><p>나는 3월부터 찾아 나섰으니까 거의 9개월 조금 넘게 걸렸다. 거의 8등급에 해당하는 속도. 기준을 낮추지 않고 신중하게 사람을 만난 것으로 생각하고 싶다. 만약 공동 창업자를 찾고 나서 시작한 사람이라면 남들보다 이 기간을 아낀 것이라고 볼 수 있다. 그렇지만 퇴사하지 않고 공동 창업자를 찾는 것은 좋은 팀을 만들 가능성을 낮추는 것 같다. 어떤 분들은 한 다리 걸쳐놓고 있는 상태는 창업에 대한 의지가 없는 상태로 판단하시고 함께 해볼 생각도 안 하시기도 하기 때문이다.</p><p>그래서 나는 지금 생각을 가지고 다시 창업 처음으로 돌아가도 공동 창업자를 찾고 나가야겠다고 생각하진 않을 것이다. 공동 창업자를 찾는 과정 동안 배운 점도 많고 회사에 다녔다면 얻지 못했을 경험도 있다. 그래서 창업가 X가 공동 창업자가 없어서 퇴사를 고민한다면 정말 고민의 원인이 공동 창업자인지 잘 생각해 보라고 하고 싶고, 만약 그 외 이유가 없는 것 같다면 퇴사하고 더 적극적으로 찾아보면 어떻겠냐고 말해주고 싶다.</p><blockquote><p>나는 내용을 ‘평균적으로 8개월 걸립니다’로 기억하고 있었는데 지금 다시 찾아보니 20%라고 한다. 나는 평균보다 한참 늦었다고 생각이 바뀌었지만, 아무런 심리적 타격도 없긴 했다. 찾았음 됐지~</p></blockquote><h1 id="PMF를-찾기-위한-여정"><a href="#PMF를-찾기-위한-여정" class="headerlink" title="PMF를 찾기 위한 여정"></a>PMF를 찾기 위한 여정</h1><p>앤틀러 전에는 큼직한 프로젝트 몇 개를 시도했지만 앤틀러에서는 PMF를 찾기 전에 논리적으로 사업의 성패를 추론해 보는 훈련을 했다. 그래서 앤틀러 기간에 실제로 프로덕트를 만들어내는 경험을 많이 하지는 않았다. 하지만 이 과정에서 배운 것도 꽤 크다.</p><h2 id="Fullstack-AI-BA-6-month-BA-3-month"><a href="#Fullstack-AI-BA-6-month-BA-3-month" class="headerlink" title="Fullstack AI: BA 6 month ~ BA 3 month"></a>Fullstack AI: BA 6 month ~ BA 3 month</h2><p>3월부터 시도했던 건 Fullstack AI를 만드는 것이었다. 그 당시에는 예비 창업가분들을 많이 만났는데, 겹치는 고통 중 하나가 ‘개발자가 없으니 초기 프로덕트를 만들기 위해 채용을 하거나 외주를 맡겨야 하는데 둘 다 시간 소비가 심하고 비용도 엄청 많이 든다’는 것이었다. 말씀하시는 스펙은 사실 만드는데 엄청 어려운 것은 아니지만 외주 개발을 맡기면 2~3천만 원 사이가 되는 스펙이라고 한다. 당시 나는 지금의 AI 수준에서는 그 정도 스펙의 코드 베이스는 관리할 수 있을 것 같다고 생각했다.</p><img src="/images/2025-01-01-20250101/theory-is-theory.png?style=centerme" alt="이론은 이론이다" /><p>언제나 그렇듯 실전은 이론과 다르다. 정확히 말하자면 이론과 다르다기보단 현실엔 더 다양하게 고려해야 하는 요소가 많다. 모든 코드 베이스를 올리는 것은 Context 사이즈 문제도 있고, 백만 토큰을 쓸 수 있는 모델을 사용하더라도 비용 이슈도 있다. 그리고 무엇보다 정밀해야 하는 프로그래밍 언어가 정밀하지 않은 결과를 자꾸 받아서 테스트해야 하는 것도 문제였다.</p><p>하지만 여전히 불가능하지 않다. 개발자가 API 코드를 작성할 때 사고하는 흐름을 AI에 몇 단계로 나눠서 파이프라이닝 해두면 유의미한 결과를 만든다. 디버깅 과정도 코드를 컴파일하고 컴파일 에러가 생기면 LLM에 에러를 넘겨주고 코드를 수정하는 사이클을 만들 수 있다. 이런 걸 시도하는 팀도 많고 관련된 논문도 많아서 이때 논문을 엄청 많이 읽으면서 간단한 API 서버를 만들 수 있는 서비스까지는 도달했다. 단순하게 서버와 데이터베이스만 사용해야 하는 경우는 사용할 수 있는 정도? 하지만 OAuth라든지 클라우드 컴포넌트를 사용하는 것 등을 수행하지 못했다.</p><p>만들면서 든 생각은, ‘이거 파이프라인을 한 천 개 정도 만들면 정말 꽤 정밀하게 돌아갈 수 있을 것 같아’였다. 그런데 이 성공의 성패가 나에게 있지도 않은데 어느 정도 AI의 비용 문제와 성능 문제가 조금씩 더 나아질 것을 기대하면서 비즈니스를 해야 하나 싶은 생각도 들었고, 같은 도전을 하는 다른 팀과 비교했을 때 자본도 인력도 없는 내가 이길 수 있는 요소가 뭘까 싶은 생각도 들었다. 나는 1,000개의 고도화된 AI 파이프라인을 만들 때까지 얼마나 걸릴까? 만약 불완전한 제품이더라도 고객에게 가치가 있는 걸까? 그런 대답들에 쉽게 끄덕이기 어려웠다. 이런저런 고민을 하던 와중에 다른 아이템을 해도 괜찮겠다는 시그널을 보게 되어서 이 프로젝트를 멈추고 다음으로 넘어갔다.</p><h2 id="LLMOps-Flow-BA-3-month-BA-1-month"><a href="#LLMOps-Flow-BA-3-month-BA-1-month" class="headerlink" title="LLMOps - Flow: BA 3 month ~ BA 1 month"></a>LLMOps - Flow: BA 3 month ~ BA 1 month</h2><p>이전 아이템을 만들면서 내부 툴을 만들려고 한 적이 있었다. 이 내부 툴은 여러 Depth가 있는 LLM 호출을 쉽게 만들어주는 도구였다. 열심히 연구하는 동안 LLM 파이프라인을 자주 바꿔가며 테스트해야 했는데, 이 도구가 없으면 코드에서 자료 구조와 프롬프트를 관리하고 실행 순서도 코드에서 짜야 하는데, 이걸 반복하기에 시간 소비가 너무 심해 힘들었다. <a href="https://dify.ai/">Dify</a> 같은 서비스이고 내부적으로 Langgraph를 활용하고 있는 도구였다. 당시에 프로젝트를 도와주시던 분이 이를 같이 만들어주셨는데, 이런 내부 도구가 자신의 회사에서도 꼭 필요할 것 같다고 말씀을 해주셨다. 그래서 이 파이프라인 버전 관리 도구를 <code>Flow</code>라고 이름 붙이고 고도화하기와 고객을 찾아보려는 시도를 한 달 반 정도 했다.</p><p>고객을 10팀 정도 만났던 것 같은데 대부분이 LLM 파이프라이닝을 통해 고도화할 필요성을 느끼고는 있지만 그것이 지금 당장인가? 하는 생각을 하고 계셨던 것 같다. 대부분의 서비스가 LLM 호출을 굉장히 단순하게 한 번 호출하는 정도로 사용하고 있었다. 큰 기업의 일부 Feature에 AI를 적용하는 것부터 시작해서 AI가 메인 Feature를 담당하는 작은 스타트업들 모두 그랬다. 생각보다 AI를 복잡하게 사용해야만 하는 비즈니스를 하는 사람을 찾기 어렵다는 생각이 들었다.</p><p>하지만 고객 인터뷰는 나름 좋은 인사이트를 줬고, <code>Flow</code>가 해결해 주는 문제보다 더 큰 문제를 푸는 프로덕트를 만들 수 있을 것 같았다. 고객들은 다 동일하게 이런 얘기를 했다.</p><ol><li>Prompt 관리를 잘하고 싶어 한다.</li><li>빠르게 좋은 퀄리티 프롬프트에 도달하고 싶다.</li></ol><h2 id="LLMOps-Platea-AI-BA-1-month-AA-0-month"><a href="#LLMOps-Platea-AI-BA-1-month-AA-0-month" class="headerlink" title="LLMOps - Platea AI: BA 1 month ~ AA 0 month"></a>LLMOps - Platea AI: BA 1 month ~ AA 0 month</h2><img src="/images/2025-01-01-20250101/aha.png?style=centerme" alt="이걸 만들까?" /><p>인터뷰하던 팀들의 대부분은 프롬프트 평가와 관리를 Notion이나 스프레드시트로 했다. Prompt, Input, Output, Evaluation 값을 관리하는 데이터베이스를 만들어서 팀원끼리 공유하는 방식이었다. 이는 사실 협업을 위한 결과지에 가까웠다. Prompt를 작성하고 테스트해야 하는 도메인 전문가와 개발자 사이에 어떤 협업 레이어가 부족함을 느꼈다. 그리고 기본적으로 메이저 모델 여러 개를 테스트하는 환경을 마련하기 쉽지 않았다. Claude, Gemini, OpenAI 모델의 결과를 빠르게 비교하고 싶은데 각각 한 번씩 돌려보고 결과를 모아서 평가하는 방식이었다.</p><p>인터뷰를 도와주신 분 중에 우리나라에서 정말 AI 프로덕트를 잘 만들고 있다고 생각하는 팀의 대표님인 O 님이 자기 팀에서 만들어서 쓰고 있는 협업 도구를 보여주셨다. 여러 프롬프트 N개와 데이터 셋 M개를 NxM 형태의 테이블로 표현해 병렬 실행하는 아주 단순한 도구이다. 여러 모델과 설정값을 갖는 여러 프롬프트 결과를 한 번에 볼 수 있다. 하지만 로컬에서만 동작하는 서비스였기 때문에 이걸 협업할 수 있게만 만들어줘도 일단 우리 팀은 쓸 것 같다는 말씀을 주셨다.</p><p>꽤 현실적인 문제이면서 금방 만들어서 가져다드릴 수도 있고 많은 팀의 문제를 해소해 줄 수 있을 것 같다고 생각했고 2주 동안 만들어서 냅다 가져다드렸다. 그리고 인터뷰했던 여러 팀에게도 소개해 드렸다. 하지만 생각보다 전환이 발생하지는 않았다.</p><img src="/images/2025-01-01-20250101/platea-ai.png?style=centerme" alt="앤틀러 시작 전 마지막 프로덕트" /><p>앤틀러에 들어가기 전에 더 많은 고객에게 전달해 보자는 생각도 했고 결제가 발생하지 않는다면 정말 중요한 문제는 아닌 것으로 생각해서 Paypal을 붙인 뒤 Product Hunt에 게시했다. 특별히 타겟팅된 건 아니었지만 결제는 발생하지 않아서 그 이상 고도화하지 않았다. 올렸던 시기가 앤틀러에 들어온 이후였다.</p><img src="/images/2025-01-01-20250101/ph-platea-ai.png?style=centerme" alt="나의 두 번째 프로덕트 헌트 제품" /><p>최근엔 Platea AI와 비슷한, 그리고 조금 더 발전된 모습을 보이는 프로덕트들이 좀 보이고 있는데, 이 문제가 워낙 명확하다 보니 경쟁 제품이 많이 생기는 것 같다. 이걸 조금 더 붙잡고 싶었지만, 일단 앤틀러에 참여하면서 공동 창업자를 찾는 데 좀 더 초점을 옮기기로 했다.</p><h2 id="4Q에-뒤적인-아이템"><a href="#4Q에-뒤적인-아이템" class="headerlink" title="4Q에 뒤적인 아이템"></a>4Q에 뒤적인 아이템</h2><p>4Q에 뒤적였던 아이템을 다 나열하려면 진짜 1,000,000자 뚫을 것 같아서 다 적지는 못할 것 같다. 앤틀러에서는 창업자들에게 생각의 흐름이라고 해야 할지, 프레임워크라고 해야 할지, 아무튼 아이템을 찾는 방법을 훈련한다. 이 방법이 굉장히 독특하다기보다는 다들 생각하는 진짜 문제가 뭘지 알아내는 합리적인 방법이다. 개인적으로 ‘Five Whys’를 아주 디테일하게 훈련하는 것과 비슷한 것 같다.</p><p>누구의 어떤 문제인지 정의하고 대안으로 해결되지 않는 걸 찾고 왜 그 대안이 여전히 해결되지 않았는지 알아봐야 한다. 대기업은 왜 안 하고 있을까? 기존에 대안들은 왜 해결되지 않는 이 부분을 남겨둔 걸까? 이런 질문을 하면 이 문제를 해결할 때 진짜 어려운 점을 알게 된다.</p><blockquote><p>앤틀러 이전에 얘기가 나오면 ‘대기업은 이런 거 안 하니까’라든지 ‘기존 상황에 익숙해져 있으니까’ 이런 얘기를 가장 많이 듣는 것 같다. 사실 이런 경우가 실제로 많을 수도 있지만, 이 아이디어가 내가 최초라고 생각하는 것도 오만하고, 뭐가 진짜 어려워서 이게 아직 해결되지 않은 것일지 미리 생각해 두는 것은 솔루션을 생각할 때 도움이 된다. 당연히 이런 것에 답하기 어렵다고 시작하지 못하는 건 아니다.</p></blockquote><p>이렇게 찾다 보면 정말 아이템 찾는 게 어려워진다. 물론 이 프레임워크가 항상 정답은 아니다. 모든 질문에 답하지 못한다고 비즈니스를 시작하지 못할 건 아니다. 하지만 우리가 이런 질문에 답변을 완벽히 해도 비즈니스는 어렵다. 그래서 이렇게 날카롭게 깎아서 시작하는 것이 생존율을 올리는 데 도움을 주는 것 같다.</p><img src="/images/2025-01-01-20250101/no-problem.jpg?style=centerme" alt="이 세상은 너무나 잘 굴러가고 있을지도... 선배 기수의 익명 창업가분이 배운 점은 어느 순간부터 매우 공감이 됐다" /><h2 id="지금-해보고-있는-것"><a href="#지금-해보고-있는-것" class="headerlink" title="지금 해보고 있는 것"></a>지금 해보고 있는 것</h2><p>지금 해보고 있는 건 정보를 관리하고 소비하고 재생산하는 이 사이클에 들어가 보고 있다. B2C 제품이고 이 영역에서 아주 큰 문제를 겪는 사람이 어떤 사람들일지 아직은 정확하지 않은 상태다. 그렇지만 우리가 이 사이클에 관심이 많은 사람이기도 하고 콘텐츠를 수집부터 재생산 사이클로 돈을 버는 사람들에게 실질적인 도움을 주는 프로덕트를 만들 수 있을 것 같다. 또 글로벌 프로덕트로 확장이 비교적 쉬운 영역이라고 생각이 들었다.</p><blockquote><p>이 프로덕트의 마켓핏을 찾는 것에 실패하더라도 괜찮다… 또 찾아 나서면 되니까…</p></blockquote><h1 id="2025년에는"><a href="#2025년에는" class="headerlink" title="2025년에는"></a>2025년에는</h1><p>이번 해는 지난해에 못 이룬 PMF 찾기 목표를 위해 열심히 뛰어야 한다. MAU 10만 이상의 프로덕트를 만들고 월 반복 매출 $50,000을 달성하고 싶다. 그리고 지난 해에 이어서 책 읽기 목표를 만들었다. 지난해보다 5권 더 많이 읽기를 목표로 할 예정이다. 그리고 잃었던 건강도 또다시 되찾기 위해 노력하리라…</p><p>올해는 정말 너무 많은 이벤트가 있어서 정말 기록하고 싶던 핵심적인 것만 담았음에도 엄청나게 긴 글을 만들어냈다. 중간중간 글을 좀 써야 할 것 같다.</p>]]></content:encoded>
<category domain="https://changhoi.kim/categories/logs/">logs</category>
<category domain="https://changhoi.kim/tags/retrospect/">retrospect</category>
<category domain="https://changhoi.kim/tags/log/">log</category>
<comments>https://changhoi.kim/posts/logs/20250101/#disqus_thread</comments>
</item>
<item>
<title>2023년, 개발 5년 차 회고</title>
<link>https://changhoi.kim/posts/logs/20240101/</link>
<guid>https://changhoi.kim/posts/logs/20240101/</guid>
<pubDate>Fri, 29 Dec 2023 15:00:00 GMT</pubDate>
<description><p>2019년 1월 1일에 개발 공부를 시작하면서 블로그를 시작했는데, 벌써 이 블로그도 5년째 이어졌다. 이번 해는 나에게 꽤 특별한 해였다. 작년 12월부터 시작된 실존적 고민으로부터 시작되어 앞으로 나는 어떻게 살아갈 것인지, 무엇을 하는 것이 나의 꿈을 위해 가장 좋은 방법일지 고민도 많이 하고 결론도 나왔다. 먼 미래에 뒤돌아봤을 때 2023년에 내렸던 결정과 내 사고 방식의 변화로 인해 2023년은 내 인생의 챕터를 가르는 해라고 판단할 것이 분명하다. 이 글을 읽는 당신은 한 사람이 인생의 한 챕터를 여는 시작점을 보고 있다고 말할 수 있다.</p></description>
<content:encoded><![CDATA[<p>2019년 1월 1일에 개발 공부를 시작하면서 블로그를 시작했는데, 벌써 이 블로그도 5년째 이어졌다. 이번 해는 나에게 꽤 특별한 해였다. 작년 12월부터 시작된 실존적 고민으로부터 시작되어 앞으로 나는 어떻게 살아갈 것인지, 무엇을 하는 것이 나의 꿈을 위해 가장 좋은 방법일지 고민도 많이 하고 결론도 나왔다. 먼 미래에 뒤돌아봤을 때 2023년에 내렸던 결정과 내 사고 방식의 변화로 인해 2023년은 내 인생의 챕터를 가르는 해라고 판단할 것이 분명하다. 이 글을 읽는 당신은 한 사람이 인생의 한 챕터를 여는 시작점을 보고 있다고 말할 수 있다.</p><span id="more"></span><h1 id="내-꿈을-찾아서"><a href="#내-꿈을-찾아서" class="headerlink" title="내 꿈을 찾아서"></a>내 꿈을 찾아서</h1><p>올해 초는 꿈을 구체화하는 시간이었다. 나는 ‘좋아하는 것을 열심히 한다’는 나름의 좌우명을 잘 지켜가며 살고 있는 것 같다. 그리고 나에게 정말 재밌는 것은 ‘많은 사람에게 내가 만든 가치가 전달되는 것’이다. 이 정도의 간단한 미래 꿈과 방향은 오래전부터 설정된 상태였다.</p><p>그리고 이 꿈을 구체화하는 과정에서 일론 머스크가 큰 영향을 줬다. 일론 머스크는 전 세계 사람들, 인류에게 중요한 기여를 하는 것이 정말 재밌을 것 같다고 생각하게 했다. 일론 머스크가 이끄는 기업은 모두 인류에게 어떤 가치를 전달해 주는 것을 목표로 하고 있다.</p><blockquote><p><strong>Tesla</strong>: 세계의 지속 가능한 에너지로 전환을 가속화<br><strong>SpaceX</strong>: 인류 진화의 다음 단계로의 도약<br><strong>Neuralink</strong>: 미래의 인간 잠재력 실현</p><p>위는 미션, 비전 등에서 가져온 문구들이다.</p></blockquote><p><img src="/images/2024-01-01-20240101/musk.png?style=centerme" alt="머스크 짱!"></p><p>일론 머스크는 이런 미친 꿈을 얘기해도 실현 가능성이 있어 보인다. 어떻게 그렇게 될 수 있었을까? 내가 생각한 이유는 이 사람이 그동안 보여준 문제 해결 능력으로 인해 생긴 영향력 때문이 아닐까 싶다. 자본주의 사회의 영향력은 자본이고 일론 머스크는 자산가다. 만약 내가 미래에 이런 인류의 문제를 해결하자는 꿈을 천재들에게 팔고 있는데 어떤 천재가 “정말 좋은 생각이고 당신의 의견에 동감합니다. 그런데 당신은 누구세요?”라고 했다고 가정해 보자. 그때 “저는 개발자입니다.”라고 하는 것은 설득력이 떨어진다.</p><p>쉽게 말해서 영향력이 큰 사람이 되면 내 꿈에 다가갈 수 있을 것 같다. 영향력을 키우는 방법은 부자가 되는 것 말고도 많다. 해결하고자 하는 영역의 아주 존경받는 지식인이 되어 직접 핵심적인 역할을 맡을 수도 있고, 한 국가의 수장이 되어 관련된 사람들을 모아 시작해 볼 수도 있을 것 같다. 하지만 내가 가장 즐거워하면서, 가장 가능성 높게 필요한 영향력을 만들어가는 방법은 일론 머스크처럼 비즈니스를 하는 것이 아닐지 하는 생각이 든다.</p><p>그래서 나는 일단 세상의 문제를 해결하는 걸로 부자가 되는 걸 먼저 마일스톤으로 잡아야 할 것 같다.</p><blockquote><p>꿈은 인류의 문제를 해소하는 것으로 잡았다. 구체적으로는 정해가고 있는데, 문명의 발달 단계를 에너지 생산량에 따라 구분한 기준을 보고, 가장 영향을 크게 끼칠 수 있는 문제 해결은 인류의 에너지 생산 문제를 해소하는 것일 것 같다고 생각하는 중이다. 하지만, 여전히 추상적이긴 하다.</p></blockquote><h1 id="사람에게-얻은-인사이트"><a href="#사람에게-얻은-인사이트" class="headerlink" title="사람에게 얻은 인사이트"></a>사람에게 얻은 인사이트</h1><p>위와 같은 생각을 하며 미래에 대한 숙고의 시간을 2023년 초반에 보낸 것 같다. 무엇을 하고 살아야 할까에 대한 고민, 언제 어떻게 해야 할지 고민 하면서 여러 사람을 만나봤다. 창업가, 개발자, 창업 팀과 얘기를 나눴다. SaaS를 개발하려고 하는 팀, 유니콘 또는 대기업에 다니는 개발자, 현재 시리즈 A를 완료한 팀과 그 창업자, 이제 팀 막 팀을 만들고 있는 창업자 등을 만났다. 몇 사람에게 큰 영향을 받은 이야기를 정리했다.</p><h2 id="이게-정답이라고-생각하기-때문에"><a href="#이게-정답이라고-생각하기-때문에" class="headerlink" title="이게 정답이라고 생각하기 때문에"></a>이게 정답이라고 생각하기 때문에</h2><p>어떤 결정에 대해 왜 그런지 물어봤을 때 정말 약 오르는 답변이 아닐까? “이게 정답이라고 생각했습니다.”라는 말은 너무 당연해서 다음 질문도 떠오르지 않는다. 국내 B2B SaaS를 만드는 팀 대표님과 얘기할 기회가 있었는데 얘기 하던 도중 대표님의 백그라운드가 엔지니어이고, 그 이후 VC, 그 이후 창업을 하게 됐다는 얘기를 들었다. 너무 흥미로운 커리어였다. 개발자를 하시다가 VC를 하게 되셨다는데 왜 그런 결정을 하신 걸까? 그리고 왜 창업을 하셨을까?</p><p>대표님은 자신이 무슨 일을 하든 네트워크가 정말 중요하다고 생각한다고 하셨다. 예를 들어 자신이 싱가폴 같은 곳에서 사업을 하려고 할 때 대통령하고 연결될 수 있는 사람과 네트워크가 있다면 무슨 일을 하든 영향력을 더 크게 만들 수 있다고 하셨다. <strong>네트워크를 만드는 걸 제일 잘 할 수 있는 방법</strong>이 무엇인가 하고 생각했을 때 VC를 하는 것이라고 생각하셔서 그 길을 선택하셨다고 하셨다. 창업도 마찬가지로 비슷한 맥락에서 선택한 건데, VC를 하다 보니 많은 사업 대표를 만났던 것은 맞지만 부족함을 느꼈다고 하셨다. 그리고 은근히 노가다 작업도 많고 시스템화할 수 있는 부분들이 보이니 이 영역에서 창업하면 VC를 하던 때보다 더 많은 사람을 만날 수 있을 것으로 생각하고 창업을 시작했다고 말씀하시고 실제로 수십 배 더 큰 네트워크를 만들 수 있었다고 했다.</p><h2 id="안되는-이유라도"><a href="#안되는-이유라도" class="headerlink" title="안되는 이유라도?"></a>안되는 이유라도?</h2><p>창업을 막 결정한 대표님은 세계에서 손에 꼽는 컨설턴트 회사를 퇴사하시고 창업을 시작하고 계셨다. 팀을 구하고 계셨는데 지인에 의해 나도 팀원 물색 대상에 올라 짧게 얘기를 나눈 적이 있다. 창업가의 동기는 항상 궁금한 주제라 왜 이렇게 혹한기라고 불리는 시기에 창업을 결정하셨는지 여쭤봤다. 대표님은 나에게 지금 하지 말아야 할 이유가 딱히 없기 때문이었다고 말씀하셨다. 반박할 말이 떠오를 수 있지만, 하지 말아야 하는 이유를 자신에게서 찾을 때로 범위를 좁혀 생각해 보면 어느 정도 끄덕여진다.</p><p>내 입장에서 어떤 것을 미루는 건 준비 작업 때문이라고 할 수 있다. 예를 들어 “제가 창업을 하기 위해 초기 창업 팀과 함께 그 과정을 경험하려고 합니다.” 라든지… “초기 서비스 개발과 인프라를 배우고 시작하려고 합니다.” 라든지? 위 예시는 실제 내가 생각하던 준비 작업은 아니지만, 보통 이런 느낌이다. 여기까지 준비해보고! 여기까지 경험해보고!</p><p>하지만 준비는 천 년 만 년 할 수 있다. 그동안 창업을 몇 번 해보면서, 그리고 프로젝트를 리드 해보면서 느낀 점이고 이는 말씀 나눈 대표님도 하신 말이었다. 이 말씀에 정말 크게 공감했다.</p><hr><p>이 두 사람의 얘기를 듣고 내가 하고자 하는 일을 위한 직접적인 방법이 아니라 간접적인 방법을 선택하고 있는 것은 아닐지 한 번 더 고민하게 됐다. “나는 A라는 걸 할 거야.”라고 생각하면 A를 달성할 수 있는 방법을 찾아가야 한다. 하지만 “나는 A를 위해서는 B를 먼저 해야겠다.”라고 하며 먼 길을 돌아가려고 하는 건 아닐까? 당시 조금 작은 팀으로 이직하려고 했던 나는 최종 합격을 포기했다.</p><blockquote><p>처음 독특한 이력을 가진 대표님의 이야기는 사실 최종 면접에서 들은 얘기다. 아이러니하게도 대표님과 얘기한 후 그 회사를 안 가게 됐다.</p></blockquote><blockquote><p>친구와 얘기한 적이 있는 분야긴 한데, 꿈을 이루어 가는 과정에 있는 것인지, 미래에 꿈을 이룰 나를 위해 대비하고 있는 것인지 잘 구분할 필요가 있을 것 같다. 꿈을 이뤄가는 과정이라고 판단되는 어떤 것이든 A이며 해도 좋지만 꿈을 위해 지금은 꾹 참고 무엇을 한다든지, 뭘 배우고 온다든지 이런 것들은 B에 해당한다.</p></blockquote><h1 id="목표-세우고-달성하기"><a href="#목표-세우고-달성하기" class="headerlink" title="목표 세우고 달성하기"></a>목표 세우고 달성하기</h1><p>하반기에는 꿈을 위한 첫 번째 목표를 세운 시기이다. 나는 앞으로 좋은 사람들과 좋은 기회를 얻으려면 지금의 기회비용을 모두 없애야 한다고 생각했다. 창업가를 만나서 얘기할 때 같이 하자고 말씀하시는 걸 들으며 지금 버리게 되는 회사의 연봉이나 커리어를 고민하는 내 모습을 보게 됐다. 나는 지금 제안받은 기회 앞에서 온전히 그 가치에 집중한 평가를 할 수 없다는 걸 느꼈다. 나는 적어도 생활이 가능한 수입이 일을 하지 않더라도 있어야 할 것 같다고 생각했다. 그래서 2023년의 목표를 하반기에 설정했는데 목표들은 다음과 같았다.</p><p><img src="/images/2024-01-01-20240101/2023-objects.png?style=centerme" alt="2023년의 목표들"></p><p>다른 것들도 중요한 목표였지만, 나에게는 추가 수입이 남은 약 5개월 동안의 가장 중요한 목표가 됐다. 3천만 원 정도의 수입이니까 대략 월 250만 원 정도의 수입을 목표로 했다.</p><blockquote><p>최소한의 수입이라는 목표는 중요하지 않은 B가 아닌가? 고민한 적이 있다. 목표를 이뤄가는 과정에 아니라는 것을 느꼈다. 이 과정은 나에게 문제 해결 능력을 주고 꿈을 이루는 과정과 일직선 상에 있다고 느꼈다. </p></blockquote><h2 id="1-AI로-블로그를-흥행시킬-수-없을까"><a href="#1-AI로-블로그를-흥행시킬-수-없을까" class="headerlink" title="1. AI로 블로그를 흥행시킬 수 없을까?"></a>1. AI로 블로그를 흥행시킬 수 없을까?</h2><p>가장 먼저 떠올린 건 지금 블로그처럼 광고를 붙인 조금 더 일반적인 주제를 다루는 블로그이다. 지금 블로그는 한 달에 약 1,500명 정도가 방문하고 한 달에 약 $4가 Adsense 금액으로 들어온다. 선형적으로 증가할지 알 수는 없지만 선형적으로 증가한다면 한 달에 약 1,500,000명이 방문해야 하고 하루에 5만 명 정도가 방문하는 블로그를 만들면 Adsense로 충분한 수입을 만들 수 있을 것 같았다. 그 당시 내 생각에는 “내가 일반적인 주제에 대해 지식이 별로 없으니 AI가 영어로 쓰도록 하면 어떨까?”라는 생각을 했다. 그래서 운동, 여행, 코딩 관련된 내용으로 AI가 글을 쓰도록 했다. 하지만 글의 퀄리티를 높이기 쉽지 않았다. 이런 글을 많이 올리면 과연 노출이 잘 될까? 실제로는 그렇지 않았다. 지금은 모두 내렸지만, 그 당시에 블로그 글을 약 20개 정도 올렸는데 효율이 높지 않았다.</p><blockquote><p>프롬포트를 잘 못 만든 것일 수도 있다. 하지만 글 쓰기 자동화도 쉽지 않았고 글의 퀄리티도 쉽게 나아지지 않았다. 올해 목표 달성을 위해서는 조금 더 빠르게 결과를 얻을 수 있었어야 했다.</p></blockquote><h2 id="2-적은-돈이라도-벌-수-있는-서비스를-해보자"><a href="#2-적은-돈이라도-벌-수-있는-서비스를-해보자" class="headerlink" title="2. 적은 돈이라도 벌 수 있는 서비스를 해보자."></a>2. 적은 돈이라도 벌 수 있는 서비스를 해보자.</h2><p>사소하게 돈을 벌 수 있는 아이템은 뭐가 있을까? 나는 대학생 미팅 앱을 가장 먼저 시도했다. 대학생 미팅 시장에는 시장을 대표하는 큰 기업이 없고, 시장의 수요도 어느 정도 검증된 영역이다. 지금 여기에 큰 플레이어가 없는 이유는 기업으로 발전할 정도의 돈이 모이는 영역이 아니기 때문이라고 생각했다. 만약 전국에서 서비스할 수 있고, 이 서비스가 1년에 사람당 만 원의 수입을 올릴 수 있다면 내가 생각한 작은 용돈벌이가 가능할 것으로 생각했다. 실제로 나와 팀원은 매칭 알고리즘을 준비하고 개강 시점과 축제 시즌에 맞춰 홍보했다. 에브리타임에는 우리와 동일한 생각을 한 아무 특색 없는 미팅 서비스가 비슷한 시기에 우르르 올라왔다. 물론 우리의 서비스 역시 아무 특색이 없는 매칭 서비스였다. 우리 서비스를 통해 매칭된 사람들은 있었지만, 사람들은 우리 서비스였는지 기억도 못했다. 서비스 운영 중에 매칭된 사람과 얘기를 한 적이 있는데 “여러 서비스에 지원했는데, 그게 어떤 건지는 잘 모르겠네요.”라는 말을 들었다. 이 경험을 하면서 ‘최소한 다른 서비스와 차별화될 수 있는 하나의 특징을 만들 필요가 있겠구나’라는 생각과 ‘문제를 해결하는 것 중 간단한 게 없는데, 작은 시장의 문제를 해결하기 위해 에너지를 쏟는 것보단 큰 시장의 문제를 해결하는 것이 좋겠다’라는 생각을 하게 됐다.</p><blockquote><p>구체적인 피드백은 많이 있다. 예를 들어 왜 대학생 미팅 서비스가 돈을 못 벌까, 우리 서비스는 왜 안 됐을까 등 스스로 생각한 결론들은 많이 있다. 중요한 얘기는 아니라서 생략했다.</p></blockquote><blockquote><p>추가 에피소드를 적자면 최근 어떤 사람이 “안 팔린 이유가 특색이 없어서가 맞는가? 휴지도 특색은 없어도 팔린다”라는 말을 했다. 처음엔 차별화가 핵심 이유가 아닐 수도 있겠다고 생각했다가, 휴지랑 비교할 건 아닌 것 같다고 생각했다. 휴지는 생필품이고 사긴 해야 한다. 수요의 규모부터 차이가 나고, 실제로 싸거나 사용했던 경험이 좋았거나 등 작은 이유로(차별화로) 구매가 나눠지기도 한다.</p></blockquote><hr><p>두 번째 프로젝트는 해외 직구 관련된 프로젝트이다. 앱을 만드는 작업이라 대학생 미팅보다는 조금 더 할 게 많지만, 어느 정도 유명한 비즈니스 모델이 있고 운영해 가면서 발전시킬 영역이 많이 있다고 판단했다. 가장 초기 모델은 <a href="https://camelcamelcamel.com/">CamelCamelCamel</a>처럼 가격 변경을 알려주고 할인이 시작되면 유저에게 알려주는 걸 생각 중이다. 하지만 유저 인터뷰를 하면서 해외 직구를 많이 하는 사람들을 만나보니 생각보다 가격에 민감하진 않았다. 오히려 가격보단 한국에 없는 상품을 찾기 위해 시간을 많이 쓰고, 있으면 일단 사는 경우가 많았다. 나처럼 가끔 해외 직구를 하는 사람들은 공감하지 않을 수 있지만 매일 사용하는 사람들이 말하는 말은 꽤 반복되고 설득력 있었다. 현재 이 문제를 해결하는 방법이 꽤 낡은 커뮤니티를 이용하는 것임을 알게 됐다. 앞으로 서비스를 발전시켜 갈 분야는 해외 직구 과정 중 탐색을 위해 사용하는 비용을 줄여주는 프로덕트이다. 한참 진행 중이고 어떻게 만들어갈지 상상하는 과정이 굉장히 즐겁다.</p><h2 id="3-내가-가진-지식을-팔아보자"><a href="#3-내가-가진-지식을-팔아보자" class="headerlink" title="3. 내가 가진 지식을 팔아보자."></a>3. 내가 가진 지식을 팔아보자.</h2><p>지금까지 목표 달성에 가장 효율이 좋았던 방법은 지식을 파는 방법이다. 나는 올해 인프런에 <a href="https://inf.run/t6Wt">내가 가진 지식을 정리해 올렸다</a>. 내가 인프런에 올려야겠다고 생각한 주제는 다음과 같은 기준이 있었다.</p><ol><li>만드는 데 시간이 너무 오래 걸려선 안 됨: 나는 강의 만들기에 익숙한 사람도 아니고 한 번의 사이클을 경험해 봐야 하는데 이 사이클이 너무 길어지지 않게 해야 했다. </li><li>너무 많은 경쟁자가 없어야 함: 처음 하는 강의다 보니 강의의 퀄리티를 아주 잘 뽑기 어려웠다. 니치한 영역이더라도 경쟁자가 적고 적당한 수요가 꾸준히 있는 영역을 찾았다.</li></ol><p>그렇게 출시한 강의는 나름 좋은 성적을 보인다. 이 강의는 대략 달마다 50만 원 정도의 수입을 만들어준다. 목표한 금액은 아니었지만 그래도 상관없다. 다음 강의는 더 잘 만들 수 있고 더 빨리 만들 수 있다. 이런 강의를 두세 개 더 만들면 한 달 목표에 도달할 수 있을 것 같다.</p><blockquote><p>물론 강의가 초반에 잘 팔리고 갈수록 판매량이 저조해지는 형태를 보일 수도 있을 것 같다. 최대한 그런 영향이 없는 내용을 고르겠지만 이 방법으로 얻은 돈은 다음 시도를 위한 체력 보충제 정도로 사용할 수 있을 것 같다. 예를 들어 연 3, 4천이 여기로 들어올 수 있게 하면 2, 3년 정도 다른 일을 할 수 있는 체력을 얻은 것이다.</p></blockquote><hr><p>약 5개월 조금 넘는 기간 동안 이런 일들을 했다. 회사 다니면서 이런 일들을 해내는 것이 쉽지 않았다. 회사 일 끝나고, 주말을 모두 갖다 박으면서 했다. 지난 5개월 동안 내 삶은 아주 단순했다. 크게 회사 일, 내 일, 잠 이렇게 세 개의 사이클을 돌았다.</p><h1 id="열심히-살기-위한-필요-조건"><a href="#열심히-살기-위한-필요-조건" class="headerlink" title="열심히 살기 위한 필요 조건"></a>열심히 살기 위한 필요 조건</h1><p>열심히 살고자 하는 모든 사람들에게 가장 필수적인 걸 깨닫게 됐다. 나는 기본적으로 체력이 좋은 사람이라 건강에 대해 큰 신경을 쓰는 스타일은 아니었다. 상반기 시작부터 대략 3개월 동안 잠도 잘 못자고, 맨날 회사 식대로 배달 음식 먹고 커피 하루 두 잔 마시고 아이스크림을 개많이 먹었다. 건강 검진한 이후 일이 터졌고 몸 상태가 심각한 수준으로 안 좋아졌다. 혈관 문제도 생기고 몸 컨디션도 너무 안좋아졌다. 그 이후는 가능한 매일 운동하고 체중 관리도 시작했다. 생활 습관 중 식습관을 제일 못 챙겼다고 생각해서 열심히 사는 것과 관련이 없을 수도 있다. 하지만 매일 앉아서 일하고 운동을 안 하는 것이 문제가 아니었다고 볼 수도 없다. 열심히 살려고 하는 세상 모든 사람들아, 할 일 하느라 운동할 시간이 없다는 소리는 하지마라. 운동할 시간은 무조건 만들어야한다. 열심히 살고 싶다면 꼭 운동을 하자.</p><h1 id="여러-방법으로-얻은-인사이트들"><a href="#여러-방법으로-얻은-인사이트들" class="headerlink" title="여러 방법으로 얻은 인사이트들"></a>여러 방법으로 얻은 인사이트들</h1><p>나는 원래 잘 바뀌지 않는 대전제를 기반으로 연결 고리가 충분히 합리적인 경우 나의 논리로 활용한다. 그렇게 해서 생긴 나만의 개념도 많고 이런 개념이 모여 나의 행동이 일관되도록 도와준다. 이번 해는 정말 배운 점이 많았다.</p><h2 id="회고는-나에게-축복"><a href="#회고는-나에게-축복" class="headerlink" title="회고는 나에게 축복"></a>회고는 나에게 축복</h2><p>나의 회고 역사는 꽤 길지만, 이렇게 회고가 체계적이게 된 건 2023년이 최초이다. 위에서 말한 것처럼 하반기부터 남은 기간 동안 달성할 목표를 만들고 그걸 달성하기 위한 분기 목표, 월 목표, 주 목표를 만들고 주 목표를 이루기 위해 매일 <a href="https://brain.changhoi.kim/note/ivy-lee-method">Ivy Lee Method</a>를 실천했다. 그리고 매주 일요일에 한 주를 회고하며 목표를 이루기 위한 더 좋은 방법들을 찾아 나섰다. 이 사이클은 나를 엔지니어링 하는 것과 같은 느낌이었다. 내가 조금 더 좋아지는 방향으로 미세한 튜닝을 해가며 아주 만족스러운 생활을 하도록 도와줬다. 이 과정에서 메모어라는 회고 모임과 지인들끼리 하는 회고 모임을 같이 했는데, 다른 사람들의 인사이트 및 생각들을 보며 새로운 시각을 흡수할 수 있었다.</p><h2 id="동기-부여-영상-중독"><a href="#동기-부여-영상-중독" class="headerlink" title="동기 부여 영상 중독?"></a>동기 부여 영상 중독?</h2><p>동기 부여 영상이 유튜브에 유행한 덕분에 많은 부자들의 인사이트를 쉽게 접할 수 있었다. 그리고 그들의 각자 다른 표현에서 공통점을 뽑아낼 수 있었고 나의 개념으로 바꾸는 과정도 경험했다. 일론 머스크, 로버트 기요사키, 그랜트 카돈, 패트릭 벳 데이비드 같은 사람들의 얘기를 들으면서 이 사람들의 묘한 공통된 얘기, 방법론들을 하나씩 내 삶에 적용했다. 그중에는 만족도가 굉장히 높은 경우도 있어서 그런 경우는 별도로 회고에 정리했다. 기회가 된다면 나만의 방법으로 바뀐 여러 방법론을 글로 정리해 보고 싶다.</p><p><img src="/images/2024-01-01-20240101/insight.png?style=centerme" alt="많은 걸 배운 한 분기"></p><h2 id="책이-중요하다는-것을-알아라"><a href="#책이-중요하다는-것을-알아라" class="headerlink" title="책이 중요하다는 것을 알아라."></a>책이 중요하다는 것을 알아라.</h2><p>또한 책을 다른 해보다 다채롭게 읽었다. 나는 원래 엔지니어링 관련된 도서만 좋아하고 읽어왔는데, 올해는 여러 책을 읽었다. 새로운 것들을 시도하면서 배우는 것이 많아지니까 새로운 지식이 새로운 기회를 만들어준다는 것을 경험하게 됐다. 예를 들어 Adsense를 공부하며 광고 관련된 비즈니스를 찾아보니 Affiliate라는 것을 알게 됐고 그걸 활용한 다음 프로젝트를 기획할 수 있었다. 책은 이러한 과정을 간접적인 경험으로 만들어준다. 나는 엔지니어 역할을 하며 살아왔는데 엔지니어링 말고 다른 거 잘 아는 게 있는지 물어보면 하나라도 자신 있게 말할 수 있는 게 없다. 즉, 다른 영역에서 새로운 기회를 알아차릴 수 있는 가능성이 꽤 낮다. 책은 간접적인 경험을 제공하며 이 문제를 조금이나마 해결해 준다. 또한 내가 생각할 때 쓸 수 있는 도구를 제공해 준다. 개발자가 아닌 영역에서 살아남는 방법 중 쉽고 좋은 방법이 책이라는 것을 느끼고 있고 여러 책을 읽어보고 있다.</p><blockquote><p>이동진 작가님이 어떤 것이든 재미를 느끼기까진 어느 정도의 노력이 필요하다고 하셨다. 예를 들어 게임을 잘 못하면 게임에 재미를 느끼기 어렵다. 게임을 잘해지기까지 어느 정도의 노력이 필요한 것이다. 책도 비슷한 느낌이었다. 처음엔 잘 안 읽어본 영역의 책이 그렇게 재밌진 않았는데, 최근에는 점점 재밌다. 그리고 오가는 길에 읽을 수 있는 밀리의서재가 도움을 많이 준다. 읽었던 책들도 하나씩 정리해 두고 싶다.</p></blockquote><h1 id="2024년의-목표는-무엇인가"><a href="#2024년의-목표는-무엇인가" class="headerlink" title="2024년의 목표는 무엇인가?"></a>2024년의 목표는 무엇인가?</h1><p>아직 구체적인 목표는 작성하지 않았다. 일단 대략적인 내용이라도 적자면 서울로 집을 옮길 예정이다. 사람을 만나고 무언가를 하기에 일산이 좋은 곳은 아니다. 서울에서 지낼 수 있는 돈을 마련하려면 최소 월 100은 있어야 할 것 같다. 그를 위해서 연초에 강의를 하나 더 만들고 싶다. 그리고 꿈이 큰 사람, 몰입을 아주 잘하는 똑똑한 사람을 찾고 싶다. ‘적당한 돈이야 벌려면 얼마든 벌 수 있다. 나는 훨씬 더 큰 꿈이 있다.’라고 생각하며 작은 문제가 아니라 큰 문제 해결에 집중하고 싶어 하는 사람을 찾고 싶다. 그리고 가능성 있는 100개의 아이템을 돌면서 PMF를 찾아가는 과정을 함께할 수 있으면 좋을 것 같다.</p><p><a href="/posts/logs/20230105/">지난해</a>도 마찬가지로 좋은 사람을 항상 만나고 싶어 했던 것 같다. 하지만 약간 추상적이게도 다른 많은 사람을 만나는 정도? 사람을 적게 만난 건 아니었지만 훌륭한 팀원을 만들고 싶어 했던 측면에서 내 삶에 추가된 사람은 없었다. 시간이 지나서 점점 더 만나고 싶은 사람의 종류가 구체적으로 되어가는 것 같아서 좋다. 이번엔 꼭 내 삶에 추가될 수 있을 정도로 가까워지는 사람이 생기면 좋겠다.</p>]]></content:encoded>
<category domain="https://changhoi.kim/categories/logs/">logs</category>
<category domain="https://changhoi.kim/tags/retrospect/">retrospect</category>
<category domain="https://changhoi.kim/tags/log/">log</category>
<comments>https://changhoi.kim/posts/logs/20240101/#disqus_thread</comments>
</item>
<item>
<title>대규모 시스템 디자인 Part 1 강의</title>
<link>https://changhoi.kim/posts/etc/system-design-part-1/</link>
<guid>https://changhoi.kim/posts/etc/system-design-part-1/</guid>
<pubDate>Wed, 04 Oct 2023 15:00:00 GMT</pubDate>
<description><p>이번에 제가 시스템 설계와 관련된 <a href="https://inf.run/t6Wt">강의</a>를 만들었습니다. 대규모 시스템 디자인은 대규모 시스템을 만들며 공통적으로 만나게 되는 문제들을 해결하는 방법을 설명하고, 유명한 시스템을 공부하</description>
<content:encoded><![CDATA[<p>이번에 제가 시스템 설계와 관련된 <a href="https://inf.run/t6Wt">강의</a>를 만들었습니다. 대규모 시스템 디자인은 대규모 시스템을 만들며 공통적으로 만나게 되는 문제들을 해결하는 방법을 설명하고, 유명한 시스템을 공부하면서 실제로 어떻게 적용되고 있는지 배웁니다. <a href="https://inf.run/t6Wt">강의 바로 가기</a></p>]]></content:encoded>
<category domain="https://changhoi.kim/categories/etc/">etc</category>
<category domain="https://changhoi.kim/tags/cs/">cs</category>
<category domain="https://changhoi.kim/tags/programming/">programming</category>
<comments>https://changhoi.kim/posts/etc/system-design-part-1/#disqus_thread</comments>
</item>
<item>
<title>Go Package Architecture - 이론편</title>
<link>https://changhoi.kim/posts/go/go-pkg-architecture-theory/</link>
<guid>https://changhoi.kim/posts/go/go-pkg-architecture-theory/</guid>
<pubDate>Wed, 12 Jul 2023 15:00:00 GMT</pubDate>
<description><p>Go에서 다른 언어와 다르게 Directory(디렉토리)는 굉장히 중요하다. 많은 다른 언어들은 실제 디렉토리의 역할 이상을 하지는 않지만, Go는 Package(패키지)와 밀접한 관계가 있으며 프로그램이 어떻게 작성될지 결정하는 한 부분이다. 그 때문에 Go에서 디렉토리 구조를 어떻게 할지는 패키지를 어떻게 구성할 것인지와 꽤 유관하다. 이번 글에서는 Go를 사용하면서 어떤 형태의 디렉토리와 패키지 구조를 구성하는 것이 좋을지 혼자만의 고민을 정리했다.</p></description>
<content:encoded><![CDATA[<p>Go에서 다른 언어와 다르게 Directory(디렉토리)는 굉장히 중요하다. 많은 다른 언어들은 실제 디렉토리의 역할 이상을 하지는 않지만, Go는 Package(패키지)와 밀접한 관계가 있으며 프로그램이 어떻게 작성될지 결정하는 한 부분이다. 그 때문에 Go에서 디렉토리 구조를 어떻게 할지는 패키지를 어떻게 구성할 것인지와 꽤 유관하다. 이번 글에서는 Go를 사용하면서 어떤 형태의 디렉토리와 패키지 구조를 구성하는 것이 좋을지 혼자만의 고민을 정리했다.</p><span id="more"></span><p>일단 패키지 & 디렉토리 아키텍처 얘기를 꺼내기 전에 어떤 점들을 고려하면서 이러한 구조를 생각해 냈는지 정리했다</p><h2 id="고민해-볼-Go-특징"><a href="#고민해-볼-Go-특징" class="headerlink" title="고민해 볼 Go 특징"></a>고민해 볼 Go 특징</h2><h3 id="Directory와-Package의-관계"><a href="#Directory와-Package의-관계" class="headerlink" title="Directory와 Package의 관계"></a>Directory와 Package의 관계</h3><p>Go에서 Package(패키지)는 <code>go.mod</code> 파일이 있는 모듈의 루트를 기준으로 한 디렉토리에 하나의 패키지만 존재할 수 있다. 또한 패키지의 이름은 기본적으로 디렉토리의 이름을 따르게 되어있다. 만약 패키지의 이름이 디렉토리의 이름과 다른 경우 패키지를 호출할 때 <code>Alias</code>를 붙이는 편이다.</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main </span><br><span class="line"> </span><br><span class="line"><span class="keyword">import</span> ( </span><br><span class="line"> playground <span class="string">"changhoi.kim/playground/pkg"</span> </span><br><span class="line"> <span class="string">"fmt"</span> </span><br><span class="line">) </span><br><span class="line"> </span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> { </span><br><span class="line"> fmt.Println(playground.Hello()) </span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>필자는 <code>Goland</code>를 주로 쓰고 있는데, 이 IDE는 일반적인 기준에 따라 별칭을 붙이는 것 같다. 예를 들어 일반적인 <code>v2</code> 패키지 형태의 경우는 별칭을 붙여주지 않는 게 일반적이다.</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> (</span><br><span class="line"> <span class="comment">// fiber는 일반적인 v2 패턴으로 패키지를 제공해 별칭을 붙이지 않는 모습</span></span><br><span class="line"> <span class="string">"github.com/gofiber/fiber/v2"</span></span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">)</span><br><span class="line"><span class="comment">// ...</span></span><br></pre></td></tr></table></figure><p>별칭이 없어도 사실 잘 동작하지만, Go 생태계에서는 패키지 이름이 디렉토리 이름과 다른 경우 별칭을 사용하는 편이다. 개인적으로 이 생태계의 룰을 지키면서 별칭을 굳이 붙이고 싶지는 않다. 즉, 패키지 이름을 디렉토리 이름과 동일하게 맞추고 싶은 욕망이 기본적으로 있다. <a href="https://go.dev/blog/package-names">Go 블로그 글</a>에서도 <strong>Conventionally</strong>, 패키지의 경로를 패키지의 이름으로 둔다는 얘기가 나온다.</p><p>별칭 얘기를 빼더라도 디렉토리가 항상 단일한 패키지 역할을 하기 때문에 디렉토리 구조가 프로그램의 동작과 유관하다. 그리고 가장 큰 문제로 다른 언어에 비해 꽤 쉽게 순환 참조를 만드는 편이다. 패키지의 어떤 함수만 불러오는 경우가 없고 그냥 통으로 패키지 임포트를 하므로 그렇다.</p><blockquote><p>아마 Go를 사용하면서 모킹을 한 패키지로 모으려고 해본 경험이 있다면 쉽게 순환 참조 문제를 만났을 것 같다. 팀에서도 이러한 문제를 자주 만났었다.</p></blockquote><h3 id="Go-Interface"><a href="#Go-Interface" class="headerlink" title="Go Interface"></a>Go Interface</h3><p>Go의 인터페이스는 Duck Typing(덕 타이핑)으로, 명시적인 구현을 선언할 필요 없이 인터페이스를 구현만 한다면 다형성 조건을 만족하게 된다. 즉, <code>implements</code> 같은 구문이 필요 없다. 이러한 특징으로 인해 굉장히 느슨한 연결이 가능하다. 구현체는 인터페이스를 알 필요도 없기 때문이다.</p><p>그래서 개인적으로 패키지 간 연결이 굉장히 매끄럽고 진짜 정확한 의미의 인터페이스를 사용한다고 느껴진다. 예를 들어 A 패키지는 <code>DoSomething</code>이라는 인터페이스를 갖춘 어떤 타입이든 주입하면 사용할 수 있는 어떤 함수를 만들었다고 쳐보자. B 패키지는 이를 구현한 상태라고 했을 때 <code>A</code> 패키지에는 <code>B</code>에 대한 정보가 일절 필요 없다.</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// package domain</span></span><br><span class="line"><span class="keyword">type</span> Helloer <span class="keyword">interface</span> {</span><br><span class="line"> Hello() <span class="type">string</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">HelloPrinter</span><span class="params">(helloer Helloer)</span></span> {</span><br><span class="line"> fmt.Println(helloer.Hello())</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// package korean</span></span><br><span class="line"><span class="keyword">type</span> Korean <span class="keyword">struct</span> {</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(k Korean)</span></span> Hello() <span class="type">string</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="string">"안녕하세요."</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// package main</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> {</span><br><span class="line"> k := korean.Korean{}</span><br><span class="line"> domain.HelloPrinter(k) <span class="comment">// OK</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="고민해-볼-Go-Convention"><a href="#고민해-볼-Go-Convention" class="headerlink" title="고민해 볼 Go Convention"></a>고민해 볼 Go Convention</h2><h3 id="Project-Layout-Convention"><a href="#Project-Layout-Convention" class="headerlink" title="Project Layout Convention"></a>Project Layout Convention</h3><p>공식적인 건 아니지만 이미 꽤 유명한 가장 기본적인 디렉토리 구조가 있다. Go 팀으로부터 오피셜이 아니라고 표기해달라는 요청까지 받은 <a href="https://github.com/golang-standards/project-layout">이 Github Repository</a>가 사실상 표준(de facto)이다. 가장 대표적인 디렉토리는 <code>cmd</code>, <code>internal</code>, <code>pkg</code> 디렉토리인 것 같다.</p><h4 id="cmd"><a href="#cmd" class="headerlink" title="cmd"></a><code>cmd</code></h4><p>프로젝트의 메인 애플리케이션을 다믄 공간이다. 실행할 수 있는, 즉 메인 패키지와 메인 함수가 들어있으며 여러 Entry Point로 나눠질 수도 있다. 즉, <code>cmd/web</code>, <code>cmd/cli</code> 등 여러 <code>main</code> 함수가 디렉토리로 나눠질 수 있다. 보통 여기 작성되는 메인 패키지 코드는 다른 디렉토리의 코드를 가져와서 실행시키는 역할만 하는 작은 코드를 담는다고 한다. </p><p>하지만 “작다”라는 말에 너무 신경 쓰지 않는 것이 좋다. 프로그램을 실행시키기 위해 필요한 동작을 하는 곳이기도 하다. 예를 들어 필요한 의존성을 만들어 주입한 다음 사용하는 것도 일반적으로는 여기서 할 일이다. 보통 “재활용이 가능한 영역인가”를 기준으로 이곳에 둘 코드를 정하면 좋을 것 같다. 예를 들어 동일한 의존성 주입 코드가 여러 <code>cmd</code> 아래의 디렉토리에서 사용된다면 이는 <code>cmd</code>에 있을 필요는 없다.</p><h4 id="internal"><a href="#internal" class="headerlink" title="internal"></a><code>internal</code></h4><p>Private 코드를 담는 공간이다. <code>internal</code> 패키지 아래에 담긴 코드는 상위 디렉토리 혹은 동일 Depth의 다른 디렉토리에 있는 코드에서 사용할 수 없도록 Go 언어 수준에서 강제하고 있다. 예를 들어 SDK를 제공하는 입장에서 안전하지 않은 내부 동작을 숨기고 싶은 경우 이 디렉토리 아래에 패키지를 구성할 수 있다. 가장 최상단의 <code>internal</code>이 아니더라도 어디서든 마찬가지이다. <code>internal/a/internal</code> 패키지는 <code>internal/b</code> 패키지에서 사용할 수 없다.</p><blockquote><p>굳이 내부적으로 <code>internal</code> 패키지를 만드는 경우는 못 봤다. 정말 거대한 팀에서 거대한 프로젝트를 모노리스로 만들고 있다면 필요할 수도 있을 것 같다.</p></blockquote><p>보통 서비스 코드를 작성해야 하는 경우는 이 패키지 아래 많은 코드를 담는 경우가 많다. SDK처럼 굳이 내보내야 할 코드가 없기 때문이다. 약간 다른 언어의 <code>src</code>와 유사하게 사용되는 경향이 있는 것 같다.</p><h4 id="pkg"><a href="#pkg" class="headerlink" title="pkg"></a><code>pkg</code></h4><p><code>internal</code>과 반대로 노출하고자 하는 패키지를 이곳에 담는다. 이 패키지를 사용하려는 외부 개발자들은 <code>pkg</code> 디렉토리에 담긴 함수, 타입, 값 등을 안정감 있게 사용하도록 한다. <code>pkg</code> 디렉토리는 과거에 Go Module이 없던 시절에 익숙할 <code>GOPATH</code>의 <code>pkg</code> 디렉토리의 영향을 받은 건가 싶다. 개인적으로 보통 SDK를 제공할 때 굳이 임포트 경로에 <code>pkg</code>를 포함하고 싶지 않은 마음이라, SDK를 개발할 땐 그냥 루트 디렉토리를 사용한다.</p><h3 id="Package-Convention"><a href="#Package-Convention" class="headerlink" title="Package Convention"></a>Package Convention</h3><p><a href="#Reference">Reference</a>에 적어둔 것처럼 여러 패키지 컨벤션을 확인했다. 하지만 컨벤션은 보통 디렉토리 구조를 어떻게 해야 한다든지, 아키텍처가 어떻게 되어야 한다든지 이런 얘기를 하지는 않는다. 하지만 아예 없는 건 아니고 디렉토리 패스라든지, 어떤 형태로 만들라는 얘기가 조금 나온다. 공통으로 다음과 같은 것들이 있다.</p><h4 id="단-하나의-패키지로-모든-API를-다루려고-하지-마라"><a href="#단-하나의-패키지로-모든-API를-다루려고-하지-마라" class="headerlink" title="단 하나의 패키지로 모든 API를 다루려고 하지 마라"></a>단 하나의 패키지로 모든 API를 다루려고 하지 마라</h4><p>패키지 이름을 <code>interface</code>, <code>model</code>과 같이 만들고 모든 인터페이스나 모델들을 하나의 패키지에서 관리하는 것을 지양하라는 소리다. 대신 책임에 따라 패키지를 구성하라고 말한다. 예를 들어 유저를 관리하는 책임을 지는 패키지 이름을 <code>user</code>라고 만들고 그 안에 <code>UserService</code> 같은 인터페이스를 넣을 수 있다. 이를 다른 표현으로 “<strong>Organize by responsibility</strong>“라고 하기도 한다.</p><h4 id="패키지-경로를-표현으로써-사용하라"><a href="#패키지-경로를-표현으로써-사용하라" class="headerlink" title="패키지 경로를 표현으로써 사용하라"></a>패키지 경로를 표현으로써 사용하라</h4><p>정확히 패키지 경로를 표현으로 쓰라는 문구를 본 것은 아니지만, <a href="https://go.dev/blog/package-names">Go 블로그 글</a>을 보면 공식 패키지 경로가 다른데 같은 패키지 이름을 갖는 것은 전혀 이상한 게 아니며 오히려 명확한 표현이라는 내용이 나온다. 예를 들어 <code>crypto</code>, <code>image</code>, <code>container</code>, <code>encoding</code> 같은 패키지는 하위 경로에 같은 범위(ex. 이미지를 다루는, 알고리즘을 다루는 범위 등)에서 동작하는 패키지를 다루고 있다. 패키지 자체는 독립적으로 취급되지만, 상위 패키지 경로는 패키지를 불러오는 <code>import</code> 구문에서 패키지 표현의 일부로 활용된다. 이러한 관점에서 <code>runtime/pprof</code>와 <code>net/http/pprof</code> 패키지는 같은 이름을 갖지만, 명확히 다른 동작을 할 것을 예상할 수 있다.</p><blockquote><p>이러한 구체적인 <code>Style Decision</code>은 조금 더 원론적인 내용에 해당하는 <a href="https://google.github.io/styleguide/go/guide"><code>Style Guide</code></a> 측면에서 봤을 때 “<strong>반복을 피하라</strong>“와 같은 내용과도 연관이 된다. 예를 들어 <code>httppprof</code>라고 패키지 이름을 짓지 않는 이유는 패키지 이름이 쓰기 싫게 생긴 것도 있지만 이미 경로에서 표현하고 있는 정보를 반복 표현하는 것을 피한 것이다.</p></blockquote><h3 id="Code-Convention"><a href="#Code-Convention" class="headerlink" title="Code Convention"></a>Code Convention</h3><p>코드는 이번 글 주제에서 더 자세한 범위에 속하지만, 어떻게 코드를 작성할지 머릿속에 그릴 수 있어야 패키지를 나눌 수 있다. 공식 라이브러리의 코드들이 어떤 느낌으로 작성되고 있는지 확인해 보려 한다.</p><h4 id="인터페이스는-사용하는-쪽에서-작성한다"><a href="#인터페이스는-사용하는-쪽에서-작성한다" class="headerlink" title="인터페이스는 사용하는 쪽에서 작성한다"></a>인터페이스는 사용하는 쪽에서 작성한다</h4><p>필자는 Go뿐만 아니라 모든 언어에서 “인터페이스”라는 용어를 동일한 맥락에서 사용하는 거라면 인터페이스를 사용하는 쪽에서 해당 인터페이스를 정의하는 방법이 맞다고 생각한다. “이런 동작을 하는 녀석을 데려오면 내가 사용해서 내 목적을 달성해 주지” 형태로 사용하는 방향이 다형성 관점에서 더 적합한 방향이다.</p><p><img src="/images/2023-07-13-go-pkg-architecture/animal-interface.png?style=centerme" alt="Animal 인터페이스"></p><p>밥을 먹이는 <code>Feeding</code> 동작을 수행하려면 <code>Animal</code>이라는 인터페이스를 구현하고 있어야 한다. 이때 <code>Animal</code>이라는 인터페이스는 <code>Feeding</code> 패키지에 위치해야 자연스럽다는 의미다.</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> tycoon</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Animal <span class="keyword">interface</span> {</span><br><span class="line"> Eat(<span class="type">string</span>) <span class="type">error</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Feed</span><span class="params">(animal Animal)</span></span> <span class="type">error</span> {</span><br><span class="line"> <span class="keyword">return</span> animal.Eat(<span class="string">"john mat taeng food"</span>)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// main.go</span></span><br><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> {</span><br><span class="line"> duck := <span class="built_in">new</span>(Duck)</span><br><span class="line"> <span class="keyword">if</span> err := tycoon.Feed(duck); err != <span class="literal">nil</span> {</span><br><span class="line"> <span class="built_in">panic</span>(err)</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="인터페이스는-최대한-작게-작성한다"><a href="#인터페이스는-최대한-작게-작성한다" class="headerlink" title="인터페이스는 최대한 작게 작성한다"></a>인터페이스는 최대한 작게 작성한다</h4><p>하지만 이런 경우를 생각해 보자. 우리는 <code>Animal</code>이라고 불리는 묵직한 인터페이스를 가지고 있다. 이 인터페이스는 다음과 같이 여러 기능을 수행해야 하며, 여러 패키지에서 이를 사용하고 싶어 한다.</p><p><img src="/images/2023-07-13-go-pkg-architecture/big-interface.png?style=centerme" alt="큰 인터페이스"></p><p>이런 경우는 생각보다 많다. 개발자들은 반복을 싫어하는데, 이런 경우 <code>Animal</code>은 어떻게 되는 걸까? <code>Feeding</code>, <code>Riding</code>, <code>Hunting</code>에 각각 <code>Animal</code> 인터페이스를 만들고 싶지는 않다. 위의 경우는 <code>Animal</code>이라는 인터페이스는 사실 너무 거대한 존재이다. 다음과 같이 쪼개어 인터페이스를 정의하는 것이 더 올바르다.</p><p><img src="/images/2023-07-13-go-pkg-architecture/small-interfaces.png?style=centerme" alt="작은 인터페이스"></p><p>그래서 Go의 인터페이스 이름 짓는 컨벤션 중에 <code>-er</code>를 붙이고 단일한 수준의 동작만 정의한 아주 작은 인터페이스를 만드는 것이 있다. <code>fmt.Stringer</code>, <code>io.Writer</code> 등을 예로 들 수 있다.</p><p>바로 머릿속에 “근데 우리가 SDK 개발만 하는 것도 아니고… 우리는 <code>UserService</code>와 같은 거대한 인터페이스를 어쩔 수 없이 만든다고요…”라는 생각이 들 수도 있다. 이를 위해서 우리는 경로를 패키지 일부로써 활용해야 한다. 의존성이 아예 없는 루트 패키지부터 의존성의 말단에 위치한 패키지까지 계층적으로 패키지를 구성해야 한다.</p><p>계층적으로 구성하면 10 Depth가 넘는 패키지 패스가 만들어질 것처럼 느껴지지만 실제로 해보면 그렇지도 않다. 너무 과도하게 깊어지는 것 같다면 적절한 수준에서 패키지를 위로 끌어 올려도 상관없다. 패키지는 어떤 Depth에 있든 독립적인 패키지로서 동작하기 때문이다. 자세한 방법은 예시 패키지를 구성하면서 설명하려고 한다.</p><h2 id="고민해-볼-Hexagonal-Architecture"><a href="#고민해-볼-Hexagonal-Architecture" class="headerlink" title="고민해 볼 Hexagonal Architecture"></a>고민해 볼 Hexagonal Architecture</h2><p><img src="/images/2023-07-13-go-pkg-architecture/hexagonal-architecture.png" alt="Hexagonal Architecture"><br><small>출처: <a src="https://mesh.dev/20210910-dev-notes-007-hexagonal-architecture/">헥사고날(Hexagonal) 아키텍처 in 메쉬코리아</a></small></p><p>필자가 가장 약한 부분이 이런 코드 레벨의 아키텍처 설계라고 생각한다. 여전히 <strong>Hexagonal Architecture</strong>(<strong>육각형 아키텍처</strong>)라고 불리는 방법론에서 사용하고 있는 이름들이 헷갈린다. 육각형 아키텍처의 핵심적인 레이어(의 표면)는 <strong>Adapter</strong>(<strong>어댑터</strong>)와 <strong>Port</strong>(<strong>포트</strong>)라고 생각된다.</p><p>어댑터의 바깥쪽은 통신 프로토콜, Socket 등 프로세스의 아예 바깥을 얘기한다. 따라서 어댑터는 프레임워크에서 HTTP 요청을 받아주는 역할이거나 CLI Flag를 받아오거나, 이벤트 메시지를 생산하거나 소비하는 클라이언트 등이 있을 수 있다. 어댑터를 기준으로 육각형 안쪽부터는 프로세스, 즉 우리가 핸들링하는 코드에 해당한다. 어댑터들의 도움으로 받아온 데이터를 애플리케이션의 순수한 비즈니스 코드를 수행할 수 있도록 포트를 통해 밀어 넣어야 한다.</p><p>이 아키텍처를 보면 “우리가 어떤 부분을 인터페이스화 해야 하는구나”를 느낄 수 있다. <code>Service</code>, <code>Repository</code>와 같이 도메인 코드를 동작시키는 인터페이스를 만들면 된다.</p><h2 id="마무리"><a href="#마무리" class="headerlink" title="마무리"></a>마무리</h2><p>패키지를 구성하기 위한 배경지식을 모두 정리했다. 말은 쉽지, 이제 코드를 보여주자.</p><h2 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h2><ul><li><a href="https://go.dev/blog/package-names">The Go Blog: Package names</a></li><li><a href="https://www.popit.kr/golang%EC%9C%BC%EB%A1%9C-%EB%A7%8C%EB%82%98%EB%B3%B4%EB%8A%94-duck-typing/">golang으로 만나보는 Duck Typing</a></li><li><a href="https://medium.com/@benbjohnson/standard-package-layout-7cdbc8391fc1">BenJohnson - Standard Package Layout</a></li><li>Style Guide<ul><li><a href="https://github.com/golang/go/wiki/CodeReviewComments#imports">Go Code Review Comments</a></li><li><a href="https://go.dev/doc/effective_go#package-names">Effective Go</a></li><li><a href="https://rakyll.org/style-packages/">Style guideline for Go packages</a></li><li><a href="https://google.github.io/styleguide/go/decisions#package-names">Google Go Style Decisions</a></li></ul></li><li><a href="https://mesh.dev/20210910-dev-notes-007-hexagonal-architecture/">헥사고날(Hexagonal) 아키텍처 in 메쉬코리아</a></li></ul>]]></content:encoded>
<category domain="https://changhoi.kim/categories/go/">go</category>
<category domain="https://changhoi.kim/tags/architecture/">architecture</category>
<category domain="https://changhoi.kim/tags/package-architecture/">package_architecture</category>
<category domain="https://changhoi.kim/tags/package/">package</category>
<comments>https://changhoi.kim/posts/go/go-pkg-architecture-theory/#disqus_thread</comments>
</item>
<item>
<title>Scaling Memcache At Facebook</title>
<link>https://changhoi.kim/posts/database/scaling-memcache-at-facebook/</link>
<guid>https://changhoi.kim/posts/database/scaling-memcache-at-facebook/</guid>
<pubDate>Thu, 22 Jun 2023 15:00:00 GMT</pubDate>
<description><p>이 논문은 Planet Scale 서비스 중 하나인 Facebook(이하 Meta, 메타, 페이스북)이 어떻게 Memcache를 사용했는지에 대한 논문인데, 이 글은 이 논문 내용 중 “확장되는 스케일에서 어떻게 Data Consistency를 유지 했는가?”에 집중해 정리했다.</p>
<blockquote>
<p>논문에서는 <code>Memcache</code>와 <code>Memcached</code> 용어를 철저히 분리한다. 전자는 분산 시스템을 구성하는 시스템 자체를 의미하고 후자는 실행되는 서버, 바이너리 자체를 의미한다.</p>
</blockquote></description>
<content:encoded><![CDATA[<p>이 논문은 Planet Scale 서비스 중 하나인 Facebook(이하 Meta, 메타, 페이스북)이 어떻게 Memcache를 사용했는지에 대한 논문인데, 이 글은 이 논문 내용 중 “확장되는 스케일에서 어떻게 Data Consistency를 유지 했는가?”에 집중해 정리했다.</p><blockquote><p>논문에서는 <code>Memcache</code>와 <code>Memcached</code> 용어를 철저히 분리한다. 전자는 분산 시스템을 구성하는 시스템 자체를 의미하고 후자는 실행되는 서버, 바이너리 자체를 의미한다.</p></blockquote><span id="more"></span><h1 id="Overview"><a href="#Overview" class="headerlink" title="Overview"></a>Overview</h1><p>메타에서는 캐시를 정말 대규모로 사용한다. 생각해 보면 페이스북은 캐시를 쓰기에 가장 적합한 유즈 케이스를 가지고 있다. 일단 논문에서 말하는 용례는 다음과 같다.</p><ul><li><strong>Query Cache</strong>: 데이터베이스 읽기 부하를 줄이기 위해 사용한다. 특히 <code>demand-filled look-aside</code> 캐시로 사용한다. </li><li><strong>Generic Cache</strong>: 굉장히 일반적인 용례를 의미한다. 거의 나머지라고 보면 될 수준. 연산이 오래 걸리는 결과(ex. 머신 러닝 결과 등)라든지 어찌 됐든 무거운 무언가를 해야 하는 걸 담아두고 여러 서비스에서 꺼내서 쓰는 용도이다.</li></ul><blockquote><p><code>demand-filled look-aside</code> 캐시는 흔히 우리가 알고 있는 캐싱 방법이다. 읽어올 때 캐시를 확인 먼저하고 없으면 Origin 데이터 소스로부터 값을 가져오는 방식을 의미한다. 논문과 영상에서 짧게 나오는 내용 중에 <code>look-aside</code> 캐시를 만들기 위해서 Origin과 동기화를 위해 데이터 소스의 변경이 발생하면 캐시의 데이터를 수정하지 않고 삭제하는 방법을 선택했다고 한다. 보통 Cache Invalidation이 이렇게 동작하기 때문에 일반적인 것 같지만, 아무튼 삭제를 선택한 이유는 수정보다 멱등적이기 때문이라고 설명한다.</p></blockquote><p>이 논문에서는 위 용례에 대해 (보통 Query Cache에 대한 내용인 듯) 배포 스케일에 따라 마주한 공학적 어려움을 설명해주고 있다. 이를 크게 세 단계로 나눠서 설명한다.</p><ol><li>단일한 클러스터</li><li>여러 개의 Front-End(이하 FE, 프론트엔드) 클러스터 </li><li>세계 단위로 여러 클러스터를 두는 상황</li></ol><h1 id="Single-Cluster"><a href="#Single-Cluster" class="headerlink" title="Single Cluster"></a>Single Cluster</h1><p><img src="/images/2023-06-23-scaling-memcache-at-facebook/single-cluster.png?style=centerme" alt="단일 클러스터"><br>이 수준에서는 지연을 줄이거나 Cache Miss로 인해 발생하는 부하를 줄이기 위해 노력하는 스케일이다. 이 장에서는 지연을 줄이기 위한 방법, 부하를 줄이기 위한 방법, 실패 처리에 대해 자세히 설명한다.</p><blockquote><p>이 글에서 일관성 얘기를 위해 적합한 스테이지가 아니다. 만약 일관성 문제만 궁금하다면 멀티 클러스터 단위로 넘어가도 좋다.</p></blockquote><h2 id="지연-줄이기"><a href="#지연-줄이기" class="headerlink" title="지연 줄이기"></a>지연 줄이기</h2><p>우선 클러스터로 운영하고 있는 <code>Memcache</code>의 상황을 설명하자면, 수 백대의 <code>Memcached</code> 서버에 데이터가 Consistent Hashing으로 분산되어 있다. 또한 웹서버가 하나의 페이지를 만들기 위해 수많은 <code>Memcached</code>로부터 동시에 값을 읽어오게 된다. 예를 들어 인기 있는 페이지의 결과를 위해 평균적으로 521개의 독립적인 아이템을 <code>Memcached</code>에서 가져온다고 한다. 이러한 이유로 클라이언트는 짧은 시간에 엄청난 양의 데이터 응답을 받을 수 있는 상황이다. 이렇게 되면 다음과 같은 문제가 발생할 수 있다.</p><ul><li>하나의 <code>Memcached</code> 병목이 웹서버의 병목 지점이 될 수 있다.</li><li>웹서버에 <code>Incast Congestion</code>이 발생할 수 있다.</li><li>데이터 복제를 통해 단일 서버 병목을 완화할 수 있지만 데이터 비효율을 감수해야 한다.</li></ul><blockquote><p><code>Incast Congestion</code>은 TCP 응답이 과도하게 몰려 TCP 윈도우를 압도하는 상황을 의미한다. 이 상황이 되면 패킷이 드랍되는 등 느려지는 원인이 될 수 있다.</p></blockquote><p>이러한 문제를 메타에서는 <code>Memcache</code> 클라이언트를 개선해 해결했다. </p><h3 id="Parallel-Requests-And-Batching"><a href="#Parallel-Requests-And-Batching" class="headerlink" title="Parallel Requests And Batching"></a>Parallel Requests And Batching</h3><p>Directed Acyclic Graph(DAG)를 그려 데이터 사이의 의존성을 확인한 후 웹서버가 동시에 Fetching할 수 있는 데이터를 최대로 뽑아낼 수 있게 만들었다. 이 구조로 동작하는 배치는 요청당 평균 24개의 키를 동시에 쿼리하게 되었다. 결과적으로 웹서버의 라운드 트립을 최소화하게 되었다.</p><h3 id="Client-Server-Communication"><a href="#Client-Server-Communication" class="headerlink" title="Client-Server Communication"></a>Client-Server Communication</h3><p><code>Memcached</code> 서버는 각자와 커뮤니케이 하지 않는데, 시스템의 복잡성을 클라이언트에 주입했다. 이로써 <code>Memcached</code> 서버는 굉장히 단순하게 유지된다.</p><p>클라이언트는 <code>GET</code> 요청을 보낼 때 지연과 오버헤드를 줄이기 위해 UDP를 사용한다. 클라이언트는 시퀀스 넘버를 통해 UDP 요청에 문제가 있는지 확인할 수는 있지만 Recover 하지는 않는다. 이러한 경우는 그냥 Cache Miss와 동일하게 처리된다. 이러한 방법은 경험적으로는 굉장히 실용적이었다고 한다.</p><blockquote><p>UDP 요청은 실제로 20%의 지연을 줄여주었다고 한다.</p></blockquote><p>신뢰성을 위해 <code>PUT</code> & <code>DELETE</code> 요청은 여전히 TCP를 사용한다. 이를 처리하는 컴포넌트로 <code>mcrouter</code>가 있는데, 이는 Proxy로 동작하거나 라이브러리로 클라이언트에 삽입되기도 한다. 이 프록시가 클라이언트와 <code>Memcached</code>의 TCP 컨넥션을 줄여주는 역할을 함으로써 CPU, Memory, Network를 아꼈다.</p><h3 id="Incast-Congestion"><a href="#Incast-Congestion" class="headerlink" title="Incast Congestion"></a>Incast Congestion</h3><p>클라이언트는 TCP Incast Congestion 문제를 보완하기 위해 자체적인 혼잡 제어 메커니즘을 가지고 있었다. 클라이언트는 Sliding Window(슬라이딩 윈도우) 방법을 사용해 요청 숫자를 제어했다. 슬라이딩 윈도우 방법은 말 그대로 TCP 혼잡 제어 방법처럼 천천히 증가하다가 문제가 생기면 줄어드는 구조이다. 윈도우 사이즈를 최적화하는 것도 중요한 문제였는데, 윈도우가 커지면 Incast Congestion을 막을 수 없어 성능 저하가 발생하고, 윈도우가 작아지면 요청들의 대기 시간이 길어졌다. 이것 역시 메타에서는 경험으로 적절한 수치를 찾은 것으로 보인다.</p><h2 id="부하-줄이기"><a href="#부하-줄이기" class="headerlink" title="부하 줄이기"></a>부하 줄이기</h2><p>부하를 줄이기 위한 노력으로 세 가지를 소개하고 있다.</p><ul><li><strong>Lease</strong></li><li><strong>Memcache Pools</strong></li><li><strong>Replication Within Pools</strong></li></ul><h3 id="Lease"><a href="#Lease" class="headerlink" title="Lease"></a>Lease</h3><p>캐시를 사용하면서 생길 수 있는 문제로 오래된 데이터를 캐시에서 보관하는 Stale Set 문제와 특정 키가 굉장히 활발히 수정되고 읽히는 Thundering Herds 문제가 있는데, 메타는 위 문제들을 해결하기 위해 Lease 기술을 사용했다. <code>Memcached</code>는 클라이언트가 Cache Missing을 경험했을 때 데이터를 다시 채우기 위해 Lease 토큰을 발급해준다. 이 토큰은 64비트의 키 마다 유일한 값이다. 클라이언트는 캐시에 값을 저장할 때 Lease 토큰을 제공해야 한다. <code>Memcached</code>는 이를 확인하고 데이터가 저장되어야 하는지를 결정한다. 이때 “확인”(Verification) 과정은 말 그대로 “이 토큰이 특정 키에 대해 유효한가”를 보는 것이다. 예를 들어 <code>Memcached</code>가 요청을 받기 전에 해당 아이템을 삭제하라는 요청을 처리한 경우 토큰이 유효하지 않게 된 것이다.</p><p>이 동작 방식이 <strong>Load-Link/Store-Conditional</strong>이라고 하는 방식과 유사하게 동작하여 동시 쓰기로 인한 과거 데이터가 쓰이는 것을 막아준다. Load-Link/Store-Conditional 방법은 쉽게 말해 특정 값에 대한 쓰기 A가 수행되기 전에 다른 요청 B에 의해 처리되어버리면 A가 실패하도록 하는 로직이다.</p><p>Lease를 통해 Thundering Herds를 완화할 수도 있다. 각 <code>Memcached</code> 서버는 토큰 반환 속도를 조절할 수 있다. 기본으로 페이스북은 <code>Memcached</code>가 토큰을 10초마다 한 번 반환하도록 조정했다. 토큰 발급 후 10초 이내 값을 요구하는 경우 클라이언트에게 잠시 기다리라는 알람을 보낸다. 일반적으로 쓰기는 수 미리 초 안에 수행되기 때문에 10초 뒤의 클라이언트 요청은 데이터가 캐시에 존재하는 상황일 가능성이 높다. 하지만 이 동작은 선택적이고 만약 오래된 데이터를 어느 정도 감안하는 서비스라면 오래된 데이터일 수도 있지만 값을 리턴해준다.</p><h3 id="Memcache-Pools"><a href="#Memcache-Pools" class="headerlink" title="Memcache Pools"></a>Memcache Pools</h3><p>위에서 언급한 Generic Cache로 사용할 때 여러 애플리케이션에 의해 사용되면 각 서비스가 다른 목적으로 접근하고 각 서비스에서 원하는 퀄리티 수준도 모두 다르다. 이는 사용 방법에서도 차이가 크게 생기기 때문에 Cache Hit을 줄이는 결과로 이어질 수가 있다. 이 차이를 해결하기 위해 <code>Memcached</code> 서버들을 다른 성격의 풀로 나눴다. 기본적으로 디폴트에 해당하는 풀이 있는데 이 풀을 <strong>WildCard</strong>라고 한다. 그리고 이 기본 풀에 있을 때 문제가 되는 키를 복수의 다른 풀에 분배하는 구조이다.</p><p>예를 들어서 자주 접근하지만 캐시 미스가 나도 큰 문제가 없는 키를 작은 풀에 할당하고, 자주 접근하고 캐시 미스가 나면 비싼 연산을 수행해야 하는 키를 조금 더 큰 풀에 담을 수 있다. 이로써 보다 적합한 키를 Eviction 처리할 수 있게 된다.</p><h3 id="Replication-Within-Pools"><a href="#Replication-Within-Pools" class="headerlink" title="Replication Within Pools"></a>Replication Within Pools</h3><p>어떤 풀들은 데이터 복제를 사용하고 있다. 다음과 같은 키는 복제를 사용한다.</p><ul><li>앱에서 주기적으로 많은 키를 동시에 가져감</li><li>전체 데이터 셋 사이즈가 하나 혹은 두 개의 <code>Memcached</code> 서버에 딱 맞음</li><li>요청 속도가 한 대의 서버에서 감당하기 어려운 수준</li></ul><p>메타에서는 이런 경우 키를 나눠서 처리하는 방법 보다 복제해버리는 것을 선호한다. 예를 들어서 100개의 아이템이 있는데 다음과 같이 상황이 다른 것을 가정해 보자.</p><ol><li>키를 공평하게 둘로 나눠서 가지고 있기</li><li>두 개의 서버에 100개를 모두 복제</li></ol><p>요청이 1M/s 속도로 들어오고 있고 모든 키를 가져가야 하는 경우를 생각해 보자. 공평하게 둘로 나눈 상태라면 클라이언트는 아이템을 모두 얻기 위해 두 서버 모두에게 요청을 보내게 된다. 즉 두 서버가 모두 1M/s 부하를 받아야 하는 상황이다. 하지만 키가 두 <code>Memcached</code> 서버에 모두 동일하게 전체 셋이 복제되어 들어가 있다면 부하를 두 서버로 분산할 수 있게 된다. 단점이라고 하면 Invalidation을 두 번 해야 하는 것인데, 페이스북의 경우 요청을 분산하는 것이 Invalidation을 여러 번 처리하는 것보다 나은 선택이었다.</p><h2 id="장애-복구"><a href="#장애-복구" class="headerlink" title="장애 복구"></a>장애 복구</h2><p>페이스북은 <code>Memcache</code> 장애를 두 가지 스케일의 장애로 나눠서 처리했다.</p><ul><li>작은 장애: 몇 개의 서버가 영향을 받는 장애</li><li>큰 장애: 클러스터 내의 꽤 큰 퍼센트의 서버가 영향을 받는 장애</li></ul><p>큰 장애는 그냥 다른 클러스터로 요청을 옮기는 형태로 장애를 복구한다. 작은 장애는 보통 자동 복구에 의존하는데 이런 시스템에 의한 복구는 시간이 걸린다. 그런데 이러면 장애가 전파될 수 있는 상황이 발생할 수 있으므로 이를 막기 위한 메커니즘으로 <code>Gutter</code>를 도입했다. <code>Gutter</code>는 장애를 복구하기 위해 사용하는 전용 머신이다. 이 전용 머신은 클러스터 내에 약 1%를 차지한다. 클라이언트가 서버로부터 응답이 없으면 서버가 죽었다고 판단하고 <code>Gutter</code>에게 요청을 보낸다. <code>Gutter</code>에서 캐시 미스가 발생하면 DB에서 값을 쿼리하고 데이터를 <code>Gutter</code>에 집어넣는다.</p><p>이는 살아남은 다른 캐시 서버에 Rehashing을 해서 값을 채우는 방식과는 차이가 있다. 살아남은 캐시 서버에 값을 넣는 것은 다른 서버로 장애가 전파될 위험이 있다. 죽은 서버는 내부에 있던 부하가 높은 키를 가지고 있을 수 있는데, 이 키가 다른 서버로 전달되어 다른 서버의 과부하로 이어질 수 있다. 그래서 아예 유휴 서버를 두고 위험을 제한하는 방법을 사용하는 것이다.</p><h1 id="Multi-Clusters"><a href="#Multi-Clusters" class="headerlink" title="Multi-Clusters"></a>Multi-Clusters</h1><p><img src="/images/2023-06-23-scaling-memcache-at-facebook/region.png?style=centerme" alt="Region"><br>한 클러스터 안에서 <code>Memcached</code> 서버 수를 늘리는 것은 단순해 보이지만 온전한 해결책이 아니다. 장애 전파나 Incast Congestion을 피할 수 없게 될 수 있다. 따라서 <code>Memcached</code>를 복수의 클러스터로 만드는 방법을 선택했다. 이렇게 복수의 FE 클러스터와 하나의 스토리지 클러스터가 합쳐져서 Region을 구성한다.</p><blockquote><p>논문에서 Web Server와 <code>Memcached</code> 클러스터가 있는 것을 FE 클러스터라고 부른다. </p></blockquote><h2 id="Regional-Invalidations"><a href="#Regional-Invalidations" class="headerlink" title="Regional Invalidations"></a>Regional Invalidations</h2><p>스토리지 클러스터가 FE의 <code>Memcache</code>와 데이터 정합성을 맞추기 위한 Invalidation 책임을 가지고 있다. 이를 위해 메타에서는 <code>mcsqueal</code>이라고 하는 Invalidation Daemon을 사용한다. 이 프로세스는 CDC 형태로 DB의 Delete 요청을 분석해 FE 클러스터에게 알려준다.<br><img src="/images/2023-06-23-scaling-memcache-at-facebook/mcsqueal.png?style=centerme" alt="mcsqueal"><br>최적화를 위해 수정 요청을 보낸 웹서버도 자신의 클러스터 안에 있는 <code>Memcache</code>로 Invalidation 요청을 보낸다. 이로써 한 유저가 쓰기 후 읽기 작업을 할 때 보다 유의미한 결과를 전달해 줄 수 있게 된다.</p><h2 id="Regional-Pools"><a href="#Regional-Pools" class="headerlink" title="Regional Pools"></a>Regional Pools</h2><p>여러 클러스터 유저의 요청이 라우팅이 섞이면서 중복된 데이터들이 자동으로 여러 클러스터 안에 속하게 된다. 이는 클러스터 운영을 위한 캐시 중단을 만들었을 때도 다른 클러스터에 의해 Cache Hit가 줄어들지 않게 되는 등, 복제에 의한 장점이 생긴다. 하지만 문제는 메모리 비효율이 크다는 점이다.</p><p>이러한 메모리 비효율 문제를 해결하기 위해 <strong>Regional Pool</strong>를 적용했다. Regional Pool은 같은 <code>Memcached</code> 서버를 갖는 FE 클러스터를 의미한다.<br><img src="/images/2023-06-23-scaling-memcache-at-facebook/regional-pool.png?style=centerme" alt="Regional Pool"><br>복제는 위에서 언급했던 것처럼 Failure Tolerance, 클러스터 안의 낮은 지연 등의 효과를 가지고 있지만 어떤 경우는 이렇게 하나의 캐시 데이터를 사용하는 경우가 나은 경우가 있다. 어떤 데이터와 웹서버를 Regional Pool에 옮겨야 하는지는 경험적인 수작업에 의해 진행된다. 요구되는 데이터 접근 속도, 데이터 사이즈, 특정 아이템에 접근하는 유니크한 유저의 숫자 등 여러 지표를 룰 베이스로 판단해 옮겨 넣는다.</p><blockquote><p>마찬가지로 Regional Pool의 <code>Memcache</code>는 위에서 언급한 <strong>Gutter</strong>, <code>mcqueal</code>, <code>mcrouter</code> 등의 시스템을 모두 사용한다.</p></blockquote><h2 id="Cold-Cluster-Warm-up"><a href="#Cold-Cluster-Warm-up" class="headerlink" title="Cold Cluster Warm up"></a>Cold Cluster Warm up</h2><p>Cold 클러스터를 Warm up 할 때는 Cache Miss 발생 시 스토리지 대신 Warm 클러스터에서 가져온다. 이런 방법으로 앞서 말한 FE 클러스터 간 데이터 복제 효과도 만들 수 있으며 스토리지를 사용한 것보다 빠르게 가져올 수 있다.</p><p>하지만 이 방법으로 인한 Race Condition이 발생할 수 있는데, 예를 들어 Cold 클러스터에서 삭제한 다음 곧바로 Cold 클러스터에서 해당 값을 읽는 상황을 생각해 보자. 방금 삭제되었기 때문에 없어야 맞는 값인데 이 Warm 클러스터와 데이터가 동기화되지 않은 상태로 Warm 클러스터에서 값을 가져온다면 Cold 클러스터의 이 값은 언제 끝날지 모르는 불일치가 발생한 상황이 된다.</p><p>이를 해결하기 위해 <code>Memcached</code>에서 키 삭제 요청을 처리한 다음 해당 키에 값을 추가하는 작업을 거부하는 기능을 사용했다. 이를 <strong>Hold-Off</strong>라고 하는데 Cold 클러스터는 2초의 Hold-Off 시간을 가지고 있다. 따라서 만약 어떤 키를 Warm 클러스터로부터 가져오려고 할 때 <code>PUT</code> 요청이 실패한다면 DB에 변경이 발생했다는 것을 알 수 있고, 이 경우는 DB에서 값을 가져오도록 되어있다.</p><p>이런 일관성 문제가 발생할 수 있지만 어찌 됐든 Warm up 방식이 그것보다 훨씬 큰 장점을 가져다준다. Cold 클러스터의 Cache Hit이 안정되면 Warm up을 종료하고 다른 클러스터처럼 동작하게 된다.</p><h1 id="Multi-Regions"><a href="#Multi-Regions" class="headerlink" title="Multi-Regions"></a>Multi-Regions</h1><p>이전 챕터의 Region은 하나의 데이터 센터이다. 보통 페이스북 사이즈가 되면 데이터 센터를 대륙 혹은 지역 단위로 확장한다. 이를 통해 다음과 같은 장점을 얻을 수 있다.</p><ul><li>클라이언트와 데이터 센터를 가까이 두어 지연을 줄인다.</li><li>특정 지역의 자연재해, 대규모 정전 등에 영향을 완화한다.</li><li>새로운 장소가 더 저렴한 전력, 경제적 장점 등을 줄 수 있다.</li></ul><p><img src="/images/2023-06-23-scaling-memcache-at-facebook/multi-regions.png?style=centerme" alt="Multi-Regions"><br>각 Region은 스토리지 클러스터와 몇 개의 FE 클러스터로 구성된다. 한 Region을 마스터 데이터베이스를 가진 Region으로 지정하고 다른 Region을 Read-Only Replica(이하 Replica)로 구성한다. 이 구성에서는 <code>Memcache</code> 혹은 스토리지 클러스터에 접근하는 경우 지연이 짧다.</p><p>여러 Region을 운영하게 되면 일단 스토리지와 <code>Memcache</code>의 데이터 일관성을 유지하기 어려워진다. 어려운 원인은 마스터 데이터베이스에서 데이터를 가져올 때 발생하는 지연(Lag) 현상이다. 보통 이런 시스템은 일관성과 성능을 어떻게 Trade-Off 할 것인지 광범위한 스펙트럼이 있고 메타 역시 이 스펙트럼의 어떤 한 지점을 고른 것이다. 이는 서비스의 특징 및 규모에 따라 경험적으로 선택되고 논문에서는 꽤 받아들일 수 있는 수준의 Trade-Off를 찾았다고 설명한다.</p><h2 id="Master-Region에서-쓰는-경우"><a href="#Master-Region에서-쓰는-경우" class="headerlink" title="Master Region에서 쓰는 경우"></a>Master Region에서 쓰는 경우</h2><p>Master Region은 이전에 설명한 한 Region에서 쓰기가 발생했을 때 <code>mcsqueal</code>이 동작하는 방법대로 동작한다. 하지만 이 Daemon 프로세스의 동작은 클러스터 안에서 한정된다. 다른 Region의 <code>Memcache</code>에 Invalidation을 전파하는 것은 동시성 이슈를 만들 수 있다. 예를 들어서 데이터가 수정되어 DB Replication이 발생해야 하는데, 이 데이터보다 Cache Invalidation이 먼저 도착하게 되고, 곧바로 클라이언트가 해당 키를 읽었다고 가정해 보자. 그러면 클라이언트는 해당 키에서 값을 못 찾고 Region 안에 있는 스토리지 클러스터에서 데이터를 찾게 된다. 그러면 오래된 데이터가 다시 캐시되고 유저는 오래된 데이터를 보게 된다.</p><h2 id="Non-Master-Region에서-쓰는-경우"><a href="#Non-Master-Region에서-쓰는-경우" class="headerlink" title="Non-Master Region에서 쓰는 경우"></a>Non-Master Region에서 쓰는 경우</h2><p>복제 지연이 발생하고 있는 상황에서 Non-Master Region에서 데이터를 업데이트 한다고 가정해 보자. Region의 <code>Memcache</code>에 Invalidation을 했든 안 했든, Master 데이터베이스가 변경된 값을 Replica로 전달하지 않은 상태라면 업데이트를 요청한 유저가 오래된 데이터를 읽어오게 되는 상황이 생길 수 있다.</p><blockquote><p><code>Name = "changhoi"</code>라고 수정하고 새로고침 된 페이지에서 여전히 <code>"CHANGHOI"</code>라고 보이는 상황을 의미한다.</p></blockquote><p>따라서 Replica에서 데이터를 캐시에 채울 수 있는 순간은 복제 스트림을 따라잡고 난 다음이어야 한다. 만약 복제 스트림을 따라잡지 못한 상태라면 웹서버는 데이터를 Master Region 스토리지 클러스터에서 가져온다.</p><p>이 동작을 위해 <strong>Remote Marker</strong>를 도입했다. 한 서버가 <code>K</code>라는 키에 영향을 주는 업데이트를 한다면 다음과 같은 순서를 따른다.</p><ol><li>Region 안에 Remote Marker를 <code>R(K)</code>에 둔다.</li><li><code>K</code>와 <code>R(K)</code>를 SQL 구문 안에서 Invalidation 될 수 있도록 포함시켜 마스터에 쓰기를 수행한다.</li><li>Region의 <code>Memcache</code>에서 <code>K</code>를 삭제한다.</li></ol><blockquote><p>2번 단계가 구체적으로 이해가 잘 안되는데, SQL 구문이 Replica에 전파될 때 <code>R(K)</code>를 같이 없앨 수 있게 SQL 구문에 내장한다는 느낌이었다. </p></blockquote><p>이렇게 동작하면 Cache Miss가 발생했을 때 <code>K</code>에 대한 마커가 남아있는 경우 Region의 Replica에서 아직 오래된 데이터를 가지고 있다는 뜻이 되므로 Master Region의 스토리지에서 데이터를 가져온다. 만약 마커가 없다면 Region 안에 있는 Replica에서 값을 가져온다.</p><h1 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h1><ul><li><a href="https://www.usenix.org/system/files/conference/nsdi13/nsdi13-final170_update.pdf">Scaling Memcache at Facebook</a></li><li><a href="https://www.youtube.com/watch?v=m4_7W4XzRgk">Scaling Memcache at Facebook Youtube</a></li><li><a href="https://nymets.medium.com/%EB%B2%88%EC%97%AD-scaling-memcache-at-facebook-9c67f9e61282">Scaling Memcache at Facebook 요약 & 해석</a></li></ul>]]></content:encoded>
<category domain="https://changhoi.kim/categories/database/">database</category>
<category domain="https://changhoi.kim/tags/system-design/">system_design</category>
<category domain="https://changhoi.kim/tags/memcache/">memcache</category>
<comments>https://changhoi.kim/posts/database/scaling-memcache-at-facebook/#disqus_thread</comments>
</item>
<item>
<title>HTTP/3에 대하여</title>
<link>https://changhoi.kim/posts/network/about-http3/</link>
<guid>https://changhoi.kim/posts/network/about-http3/</guid>
<pubDate>Fri, 03 Feb 2023 15:00:00 GMT</pubDate>
<description><p>HTTP&#x2F;2가 나온 지 얼마나 됐다고 벌써 HTTP&#x2F;3 도입 사례가 들리기 시작할까? <a href="https://platum.kr/archives/196664">네이버는 검색 서비스</a>에, <a href="https://blog.toss.im/article/tosspayments-upgrades-web-protocol">토스는 페이먼츠</a>에 HTTP&#x2F;3을 도입했다고 한다. HTTP&#x2F;2는 2015년 5월에 릴리즈됐다고 한다. 약 7년 전이고 생각보다는 오래됐지만 HTTP&#x2F;1.1이 버텨온 기간보다는 짧다. HTTP&#x2F;3은 무엇을 해결하려고 했고, 어떻게 해결했을지 정리했다.</p></description>
<content:encoded><![CDATA[<p>HTTP/2가 나온 지 얼마나 됐다고 벌써 HTTP/3 도입 사례가 들리기 시작할까? <a href="https://platum.kr/archives/196664">네이버는 검색 서비스</a>에, <a href="https://blog.toss.im/article/tosspayments-upgrades-web-protocol">토스는 페이먼츠</a>에 HTTP/3을 도입했다고 한다. HTTP/2는 2015년 5월에 릴리즈됐다고 한다. 약 7년 전이고 생각보다는 오래됐지만 HTTP/1.1이 버텨온 기간보다는 짧다. HTTP/3은 무엇을 해결하려고 했고, 어떻게 해결했을지 정리했다.</p><span id="more"></span><h1 id="HTTP-2의-문제"><a href="#HTTP-2의-문제" class="headerlink" title="HTTP/2의 문제"></a>HTTP/2의 문제</h1><p>이전에 <a href="/posts/backend/grpc-internals">gRPC</a>를 설명하면서 간단하게 HTTP/2에 관해 설명한 적이 있다. HTTP/2는 과거보다 나아진 현대 네트워크 환경에 맞는 프로토콜이라고 볼 수 있다. 하나의 TCP 물리적인 컨넥션을 사용해 논리적인 스트림을 사용하는 구조이다. 이를 통해 시스템의 물리적인 메모리 사용이라든지 리소스를 훨씬 덜 사용하면서도 HTTP/1의 결점을 수정할 수 있었다. 오늘날 <a href="https://w3techs.com/technologies/details/ce-http2">HTTP/2는 인터넷 통신의 약 40%</a>를 차지한다.<br><img src="/images/2023-02-04-about-http3/http2-usage.png?style=centerme" alt="조금 줄어드는 건 왜일까"><br>TCP는 신뢰성 있는 데이터 전송을 위해 Handshake 과정이 포함되어있다. 일반적으로 TLS/TCP 스택을 생각해보면 애플리케이션 레이어가 데이터를 주고받기까지 최소 두 번(TCP 연결 & TLS 연결)의 라운드 트립 딜레이가 발생한다. CPU, 네트워크는 점점 빨라지지만 빛의 속도는 변하지 않기 때문에 절대적으로 걸리는 시간을 줄이는 데 한계가 있다.</p><p>이러한 TCP의 구조적 문제 말고, TCP 위에서 스트림을 이용해 멀티플렉싱하고 있는 HTTP/2의 문제가 있는데, TCP의 선형적인 데이터 전송 특성으로 인해, 임의의 스트림이 느려지는 것이 다른 스트림에게 영향을 주게 된다는 점이다. 즉, HOL(Head Of Line) Blocking 문제가 발생한다. 만약 패킷 손실률이 2%라고 했을 때 HTTP/2를 사용하는 것 보다 여러 TCP 컨넥션을 사용하는 HTTP/1.1이 더 나은 성능을 보여준다고 한다.</p><blockquote><p>패킷을 100개 보냈을 때 2개가 손실되는 것은 정말 열악한 환경이다. 서버 사이의 통신에서는 이런 상황이 자주 나오지 않을 수 있다. 하지만 클라이언트와 통신하는 환경은 워낙 다양하고 네트워크 품질이 안 좋은 상황이 자주 있다.</p></blockquote><h1 id="HTTP-3"><a href="#HTTP-3" class="headerlink" title="HTTP/3"></a>HTTP/3</h1><p>HTTP/3을 단순하게 말하자면 HTTP Over QUIC라고 볼 수 있다. QUIC는 UDP 위에서 구현된 전송 레이어이다. 그래서 이제부터는 HTTP/3의 본체인 QUIC에 대해 얘기할 예정이다.<br><img src="/images/2023-02-04-about-http3/http3-stack.png" alt="QUIC & TLS/TCP"></p><blockquote><p>위는 논문의 공식적인 그림이다. QUIC는 일종의 전송 프로토콜이다. 첫 번째로 HTTP 메시지를 전달하는 역할을 하는 것이다. 그래서 초기 HTTP/3은 HTTP Over QUIC라고 불렸다. 다른 프로토콜을 QUIC 위에 전송하도록 하는 작업은 공식 버전 1.0 출시 이후 연기되었다고 하는데, 그로부터 3년이 지났으므로 어떤 발전이 있을 수도 있다.</p></blockquote><h2 id="왜-QUIC이어야-했을까"><a href="#왜-QUIC이어야-했을까" class="headerlink" title="왜 QUIC이어야 했을까?"></a>왜 QUIC이어야 했을까?</h2><p>왜 QUIC라고 하는 전송 프로토콜을 UDP 위에 만들었어야 했을까?</p><h3 id="왜-새로운-전송-레이어를-만들지-않았나"><a href="#왜-새로운-전송-레이어를-만들지-않았나" class="headerlink" title="왜 새로운 전송 레이어를 만들지 않았나"></a>왜 새로운 전송 레이어를 만들지 않았나</h3><p>왜 TCP, UDP, QUIC같이 전송 계층에 새로운 레이어를 만들지 않았을까? 이는 사실 간단한데, 배포가 굉장히 어렵다. 전송 레이어의 프로토콜이 추가되는 것은 커널의 업데이트가 요구되기도 하고, 모든 중간 미들박스에서 이를 통과시켜줘야 한다. 그냥 단순히 통과시킬 수도 있지만, 보안 측면으로 익숙하지 않은 패킷의 형태가 통과되지 않을 가능성도 있다.</p><h3 id="왜-TCP-업데이트-하지-않았나"><a href="#왜-TCP-업데이트-하지-않았나" class="headerlink" title="왜 TCP 업데이트 하지 않았나"></a>왜 TCP 업데이트 하지 않았나</h3><p>TCP 동작 방식을 변경하는 것은 사실 위와 유사한 이유로 인해 문제가 발생한다. TCP는 커널에 의해 구현되어 있으므로 OS 업데이트가 요구된다. 따라서 QUIC가 배포되기 위해 네트워크 경로의 미들박스들이 모두 커널을 업데이트해야 하는 문제가 있다. 또는 미들 박스들이 기존 TCP 스택과 강하게 바인딩 된 어떤 소프트웨어로 동작하고 있는 경우 문제를 발생시킬 여지가 있다.</p><h3 id="왜-UDP인가"><a href="#왜-UDP인가" class="headerlink" title="왜 UDP인가"></a>왜 UDP인가</h3><p>위에서 언급한 것처럼 TCP를 사용하면서, TCP의 문제점을 수정한 업데이트 버전을 사용하지 않는다면 비효율적인 Handshake라든지, HOL Blocking 등 근본적인 문제와 또다시 직면하게 된다. UDP는 네트워크 프로토콜이라고 불릴 만큼 네트워크 레이어로부터 받은 데이터에 단순히 포트와 IP 정보만 추가된 정말 얇은 전송 계층의 프로토콜이다. 이 위에 신뢰성을 보장해줄 수 있는 QUIC를 만들었다.<br><img src="/images/2023-02-04-about-http3/logo.png?style=centerme" alt="QUIC 로고"><br>이러한 이유로 QUIC를 바닥부터 설계하게 되었는데, QUIC의 설계는 다음과 같은 목표를 달성하기 위해 디자인되었다.</p><ul><li><strong>Deployability</strong>: 기존 시스템이 온전히 인식할 수 있어야 한다.</li><li><strong>Security</strong>: 기존 TLS 암호화 과정처럼 데이터를 안전히 전달해야 한다.</li><li><strong>Reduction in Handshake</strong>: 기존 TLS/TCP 스택이 갖고 있던 Handshake 비효율 문제를 해결해야 한다.</li><li><strong>HOL Blocking</strong>: 멀티플렉싱 되는 데이터가 HOL Blocking 문제를 맞이하면 안 된다.</li></ul><p>위 목적을 달성하면서도 TCP처럼 신뢰성 있는 전달, 흐름 제어 및 혼잡 제어 등을 처리해야 하는 목적도 있다. 이제 QUIC가 어떻게 동작하는지 하나씩 살펴보자.</p><h2 id="Connection"><a href="#Connection" class="headerlink" title="Connection"></a>Connection</h2><p>QUIC 역시 신뢰성 있는 통신을 위한 Handshake 과정이 있지만, TCP와 다르게 암호화와 통신을 위한 Handshake 과정이 통합되어있다. 먼저 논문에 소개된 그림은 다음과 같다. 하나씩 천천히 확인해보자.<br><img src="/images/2023-02-04-about-http3/connection-establishment.jpeg" alt="QUIC 컨넥션을 간단하게 그린 것"></p><h3 id="Initial-Handshake"><a href="#Initial-Handshake" class="headerlink" title="Initial Handshake"></a>Initial Handshake</h3><p>맨 처음 클라이언트는 서버에 대한 어떤 정보도 없기 때문에 서버의 정보를 얻어 오기 위한 가장 처음 요청이 필요하다. 이 Client Hello 단계를 <code>Inchoate CHLO</code>라고 한다. 논문을 봤을 때는 아마 이 메시지에는 QUIC 버전 협상과 관련된 정보가 있는 것으로 보인다. 버전 협상에 대한 내용은 이후 잠깐 후술한다.</p><p><code>REJ</code> 메시지는 “reject”라는 뜻으로, 클라이언트의 메시지가 응답을 보내기에 부적합한 경우 서버가 보내는 메시지이다. 이 메시지에는 다음과 같은 내용이 포함되어 있다.</p><ul><li>서버의 설정 (서버의 장기(Long-term) Diffie-Hellman(이하 DH) 공개값 등)</li><li>서버 인증서와 시그니처</li><li>클라이언트의 공개 IP 주소와 서버의 타임스탬프가 포함된 인증 암호 블록</li></ul><p>인증서와 시그니처는 HTTPS 연결 과정 처음처럼 해당 서버가 인증된 서버인지 확인할 수 있게 해주고, 인증 암호 블록은 이후 클라이언트가 IP 주소의 오너십을 증명하기 위해 다시 서버에게 보낸다.</p><blockquote><p>DH는 두 개의 키를 가지고 하나의 시크릿 값을 추출할 수 있는 알고리즘이다.<br><img src="/images/2023-02-04-about-http3/dh.png?style=centerme" alt="Diffie-Hellman"><br><code>Inchoate CHLO</code> 과정 이후 <code>REJ</code> 메시지를 받은 클라이언트는 서버의 장기 DH 키값을 가지고 있게 되고 자신의 임시(Ephemeral) DH 키를 사용해 시크릿 값을 서버에게 전달할 수 있게 된다. 알고리즘에 대한 자세한 내용은 다음 <a href="https://www.crocus.co.kr/1233">링크</a>에서 확인해보자.</p></blockquote><h3 id="Final-Handshake"><a href="#Final-Handshake" class="headerlink" title="Final Handshake"></a>Final Handshake</h3><p>위에서 언급한 것처럼 <code>REJ</code> 메시지를 받은 클라이언트는 <strong>서버의 장기 키와 클라이언트의 임시 키</strong>를 사용해 DH 알고리즘으로 비밀 값을 만들어낼 수 있다. 이렇게 만들어진 비밀 값을 <code>initial key</code>라고 한다. 클라이언트는 서버에게 메시지를 <code>initial key</code>로 암호화하고, 사용된 클라이언트의 임시 DH 값을 같이 보낸다. 즉 두 번째 Handshake부터 서버에게 암호화된 메시지를 보내기 시작하므로, 이를 <code>1-RTT</code>(1 Round Trip Time) Handshake라고 한다.</p><p>이때 보내지는 클라이언트의 메시지는 <code>Complete CHLO</code>라고 불린다. 이 메시지를 받은 서버가 내용을 올바르게 복호화하고 Handshake가 성공적으로 되면 <code>SHLO</code>(Server Hello)라는 메시지를 보낸다. <code>SHLO</code>에서는 서버의 장기 DH 키가 아니라, 임시 키로 만든 비밀 값을 사용해 응답을 암호화 한다. 즉, <strong>클라이언트의 임시 키와 서버의 임시 키</strong>로 새로운 비밀 값을 만들어내는 것이다. 이때 만들어지는 비밀 값을 <code>forward-secure key</code>라고 한다. 마찬가지로 <code>SHLO</code>에 서버의 임시 키를 포함함으로써 클라이언트 역시 <code>forward-secure key</code>를 만들 수 있게 한다. 이후 메시지는 양측 모두 <code>forward-secure key</code>를 통해 암호화하고 복호화하게 된다.<br><img src="/images/2023-02-04-about-http3/http3-secure-handshake.png" alt="Handshake 전체 그림. 열심히 그렸어요. 제발 확대해서 봐주세요."></p><p>클라이언트는 Handshake에 성공하게 되면 서버 설정 및 소스 주소 토큰을 캐시하고 있다가 같은 곳으로 반복되는 컨넥션이 발생할 때 <code>Inchoate CHLO</code>를 건너뛰고 바로 <code>Complete CHLO</code>를 보낼 수 있게 된다. 만약 이 Handshake가 성공하게 되면 바로 응답을 받게 되므로 <code>0-RTT</code> 컨넥션이 성공하는 것이다.</p><blockquote><p>항상 <code>Complete CHLO</code>가 성공적인 것은 아니다. 위 논문의 이미지에서처럼 만약 클라이언트가 캐시를 사용해 <code>Complete CHLO</code>를 서버에 보냈을 때, 모종의 이유로 서버의 설정이 변경되거나 장기 DH 값이 바뀌는 경우가 있다. 이 경우도 서버는 <code>REJ</code>를 보내게 되고 클라이언트는 다시 <code>Complete CHLO</code>를 보내야한다.</p></blockquote><h3 id="Version-Negotiation"><a href="#Version-Negotiation" class="headerlink" title="Version Negotiation"></a>Version Negotiation</h3><p>QUIC 클라이언트와 서버는 컨넥션이 일어나는 동안 버전 협상을 한다. QUIC 클라이언트는 첫 번째 패킷에 사용할 버전을 명시해서 보내는데 만약 서버가 사용할 수 없는 버전을 가지고 있다면 서버는 서버가 사용할 수 있는 모든 버전을 담아 협상 패킷을 보내야한다. 이 과정은 RTT 딜레이를 만드는 원인이 된다.</p><blockquote><p>컨넥션을 만들 때 복잡한 Handshake 과정을 생략하고 바로 <code>0-RTT</code>로 컨넥션이 성공하거나 <code>1-RTT</code>만으로 암호화된 데이터를 전달할 수 있기 때문에 지표상 컨넥션 퍼포먼스가 TCP보다 항상 높은 편이다.</p></blockquote><h2 id="Stream-Multiplexing"><a href="#Stream-Multiplexing" class="headerlink" title="Stream Multiplexing"></a>Stream Multiplexing</h2><p>QUIC는 HTTP/2가 프레임 같은 데이터 유닛을 TCP의 추상화된 바이트 스트림을 통해 멀티플렉싱하는 것처럼 UDP에서 이렇게 동작하도록 해두었다. QUIC는 TCP의 순차 전달로 인해 생기는 HOL Blocking을 막기 위해 UDP 위에서 동작하고 특정 논리적인 스트림에서 데이터 손실이 발생해도 다른 스트림의 흐름을 막지는 않는다.</p><p>QUIC의 스트림은 신뢰성 있는 양방향으로 바이트 스트림을 전달하는 가벼운 논리적인 스트림이다. 스트림은 최대 2^64 바이트의 임의 크기 메시지를 애플리케이션에 전달할 수 있으며 아주 가벼워 여러 스트림이 동시에 동작할 수 있다.</p><p>QUIC의 패킷은 다음 그림처럼 하나 이상의 프레임이 뒤따르는 공통 헤더로 구성되어 있다.<br><img src="/images/2023-02-04-about-http3/quic-packet.png" alt="QUIC Packet"><br>스트림은 스트림 ID를 통해 구분되는데, 이 값은 서버에서 시작되는 경우 짝수 아이디를 갖게 되고 클라이언트로부터 시작되는 경우 홀수의 아이디를 갖게 되어 충돌을 막는다.</p><p>스트림은 첫 번째 바이트가 보내질 때 생성되고 양측이 마지막 스트림 프레임에 <code>FIN</code> 플래그 비트를 찍음으로써 스트림을 닫게 된다. 만약 클라이언트나 서버 중 한쪽이라도 스트림 위의 데이터가 필요 없다고 판단되는 경우 다른 스트림이나 QUIC 컨넥션 자체를 파괴하지 않으면서 스트림을 취소할 수 있다.</p><p>QUIC 패킷 전송 속도는 흐름 제어 및 혼잡 제어 등으로 인해 제한된다. 따라서 여러 스트림이 사용할 수 있는 대역폭을 나누는 방법을 결정해야 한다. QUIC의 구현에서는 특별한 방법은 없고 HTTP/2의 스트림 우선순위 기능에 의존한다.</p><h2 id="Loss-Recovery"><a href="#Loss-Recovery" class="headerlink" title="Loss Recovery"></a>Loss Recovery</h2><p>TCP에서는 패킷에 시퀀스 번호를 부여하고 순서대로 패킷이 전송될 수 있도록 신뢰성 있는 통신을 한다. 손실 복구 작업 역시 이 패킷의 시퀀스 번호를 사용하는데, 서버가 ACK 응답을 보낼 때 받았던 패킷의 번호를 그대로 사용하므로 클라이언트는 재전송 패킷에 대한 ACK 응답인지 오리지날 패킷에 대한 ACK 응답인지 알지 못한다. 이를 “<strong>재전송 모호 문제</strong>“(<strong>Retransmission Ambiguity Problem</strong>)이라고 한다. 또한 일반적으로 재전송 세그먼트의 손실 같은 경우 타임아웃에 의해 탐지되는데 이는 아주 비싼 동작 방식이다.</p><h2 id="Flow-Control-Congestion-Control"><a href="#Flow-Control-Congestion-Control" class="headerlink" title="Flow Control & Congestion Control"></a>Flow Control & Congestion Control</h2><p>QUIC는 두 가지의 흐름 제어 전략을 쓰고 있다. 하나는 컨넥션 자체의 모든 스트림들이 가지고 있는 버퍼의 총량에 대한 흐름 제어, 다른 하나는 스트림마다 소비하는 버퍼의 흐름제어이다. 만약 애플리케이션에서 특정 스트림을 소비하는 속도가 느리다면 해당 스트림이 전체에 자치하는 버퍼를 제한한다. 그렇지 않으면 스트림 버퍼에 데이터가 늘어나면서 다른 스트림이 사용할 수 있는 버퍼 공간을 더 소비하게 된다. 이는 전체적인 관점에서 HOL Blocking과 같은 효과를 가져올 수 있다. 기본적으로 윈도우 사이즈는 패킷이 주고받을 때마다 커지며, 컨넥션 레벨의 흐름제어와 스트림 레벨의 흐름 제어가 모두 동일하게 동작한다. 다만 컨넥션 레벨의 크기가 훨씬 커 여러 스트림들이 내부적으로 동시 동작해도 문제가 없도록 설계되어 있다.</p><p>QUIC에서 혼잡 제어 알고리즘은 특정하고 있는 바가 없다. 인터페이스를 제공하고 해당 인터페이스를 구현한 혼잡 제어 알고리즘을 사용할 수 있다. 논문에서 말하길 구글에 배포된 상태에서는 TCP와 QUIC 모두 <a href="http://intronetworks.cs.luc.edu/1/html/newtcps.html#tcp-cubic">Cubic</a>을 혼잡 제어 컨트롤러로 사용하고 있다고 한다.</p><h2 id="Connection-Migration"><a href="#Connection-Migration" class="headerlink" title="Connection Migration"></a>Connection Migration</h2><p>QUIC 연결은 64비트 Connection ID를 통해 식별된다. Connection ID는 클라이언트 IP 또는 포트가 달라지더라도 컨넥션을 유지할 수 있게 해준다. 예를 들어 NAT 타임아웃이 발생하면서 Rebinding이 발생한다든지, 클라이언트가 네트워크 연결을 변경한다든지 하는 상황이 있을 수 있다.</p><h2 id="Discovery-for-HTTPS"><a href="#Discovery-for-HTTPS" class="headerlink" title="Discovery for HTTPS"></a>Discovery for HTTPS</h2><p>맨 처음 요청을 보낼 때 클라이언트는 사실 서버가 QUIC를 제공하고 있는 서버인지 알 수는 없다. 처음에는 일반적인 TLS/TCP 요청을 보내는데, 서버는 응답 헤더에 <code>Alt-Svc</code> 헤더를 포함하여 QUIC를 지원하고 있다는 것을 알린다. 이후 클라이언트는 서버에게 후속 요청을 QUIC와 함께 보내게 된다.</p><p>후속 요청의 경우 기존 연결이 시작된 TLS/TCP 스택과 QUIC 스택이 동시에 전달되며 경합 과정을 거친다. 하지만 300ms 정도의 차이가 있어도 (즉, TCP 위의 요청이 300ms 안쪽으로 빨랐다면) QUIC를 선호하여 선택하게 된다.</p><p>후속 요청에서 MTU 패킷 사이즈보다 QUIC Handshake 사이즈가 더 크거나, 중간에 UDP 차단으로 인해 Handshake 과정이 실패한다면 TLS/TCP 연결을 사용하게 된다.</p><hr><h1 id="HTTP-3-성능과-문제"><a href="#HTTP-3-성능과-문제" class="headerlink" title="HTTP/3 성능과 문제"></a>HTTP/3 성능과 문제</h1><p>HTTP/3은 컨넥션 과정을 간결하고 빠르게 설정되도록 줄였고 TCP에서 발생하는 HOL Blocking 문제를 완화했다. 아주 실험적인 것처럼 보일 수 있지만 구글에서는 과거부터 유튜브 같은 곳에서 HTTP/3을 도입했다고 한다. 그래서 생각보다는 <a href="https://w3techs.com/technologies/details/ce-http3">HTTP/3가 인터넷 트래픽에서 차지하는 비율</a>이 꽤 높다.</p><p><img src="/images/2023-02-04-about-http3/http3-usage.png?style=centerme" alt="25% 정도나 된다고 한다."></p><p>QUIC의 Handshake는 그냥 보기에도 의미가 있어 보인다. 아래는 TCP와 QUIC의 Handshake를 비교한 모습이다.<br><img src="/images/2023-02-04-about-http3/quic-handshake-perf.jpeg?style=centerme" alt="Handshake Performance"></p><p>하지만 HOL Blocking은 레이턴시가 적은, 네트워크 환경이 좋은 곳이라면 TCP와 큰 차이가 안 난다. 아래 표에서는 레이턴시가 적은 곳에서는 오히려 성능상 이점을 보이지 못했다.<br><img src="/images/2023-02-04-about-http3/quic-perf.jpeg" alt="QUIC Performance"></p><p>그리고 계속 개선될 것으로 보이지만, UDP 스택이 TCP에 비해서는 그렇게 많이 최적화되지 않았다. 그래서 CPU 같은 컴퓨팅 자원을 더 소비하게 된다고 한다.</p><h1 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h1><ul><li><a href="https://platum.kr/archives/196664">https://platum.kr/archives/196664</a></li><li><a href="https://blog.toss.im/article/tosspayments-upgrades-web-protocol">https://blog.toss.im/article/tosspayments-upgrades-web-protocol</a></li><li><a href="https://blog.cloudflare.com/ko-kr/http3-the-past-present-and-future-ko-kr/">https://blog.cloudflare.com/ko-kr/http3-the-past-present-and-future-ko-kr/</a></li><li><a href="https://http3-explained.haxx.se/ko">https://http3-explained.haxx.se/ko</a></li><li><a href="https://medium.com/codavel-blog/quic-vs-tcp-tls-and-why-quic-is-not-the-next-big-thing-d4ef59143efd">https://medium.com/codavel-blog/quic-vs-tcp-tls-and-why-quic-is-not-the-next-big-thing-d4ef59143efd</a></li><li><a href="https://blog.acolyer.org/2017/10/26/the-quic-transport-protocol-design-and-internet-scale-deployment/">https://blog.acolyer.org/2017/10/26/the-quic-transport-protocol-design-and-internet-scale-deployment/</a></li><li><a href="https://w3techs.com/technologies/details/ce-http2">https://w3techs.com/technologies/details/ce-http2</a></li><li><a href="https://w3techs.com/technologies/details/ce-http3">https://w3techs.com/technologies/details/ce-http3</a></li><li><a href="https://evan-moon.github.io/2019/10/08/what-is-http3">https://evan-moon.github.io/2019/10/08/what-is-http3</a></li><li><a href="https://www.cse.wustl.edu/~jain/cse570-21/ftp/quic/index.html">https://www.cse.wustl.edu/~jain/cse570-21/ftp/quic/index.html</a></li><li><a href="https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/46403.pdf">https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/46403.pdf</a></li></ul>]]></content:encoded>
<category domain="https://changhoi.kim/categories/network/">network</category>
<category domain="https://changhoi.kim/tags/cs/">cs</category>
<category domain="https://changhoi.kim/tags/http3/">http3</category>
<comments>https://changhoi.kim/posts/network/about-http3/#disqus_thread</comments>
</item>
<item>
<title>유난한 도전과 훌륭한 조직</title>
<link>https://changhoi.kim/posts/essay/outstanding-team/</link>
<guid>https://changhoi.kim/posts/essay/outstanding-team/</guid>
<pubDate>Tue, 10 Jan 2023 15:00:00 GMT</pubDate>
<description><p>그리스에서 “<strong>유난한 도전</strong>“이라는 책을 읽었다. 유난한 도전은 토스팀이 달려왔던 길을 얘기하는 책이다. 토스팀은 외부인으로 봤을 때 정말 멋지다고 생각하던 팀 중 하나이다. 이번 책이 나왔을 때, 내부인이 전달하는 현란한 물장구 이야기를 기대하며 책을 구매했다. 책은 생각보다 만족스러웠으며, 이 글은 그 책을 읽고 난 뒤 좋은 조직에 대한 생각을 정리하기 위한 글이다.</p></description>
<content:encoded><![CDATA[<p>그리스에서 “<strong>유난한 도전</strong>“이라는 책을 읽었다. 유난한 도전은 토스팀이 달려왔던 길을 얘기하는 책이다. 토스팀은 외부인으로 봤을 때 정말 멋지다고 생각하던 팀 중 하나이다. 이번 책이 나왔을 때, 내부인이 전달하는 현란한 물장구 이야기를 기대하며 책을 구매했다. 책은 생각보다 만족스러웠으며, 이 글은 그 책을 읽고 난 뒤 좋은 조직에 대한 생각을 정리하기 위한 글이다.</p><span id="more"></span><p><img src="/images/2023-01-11-outstanding-team/toss-book.jpeg?style=centerme" alt="유난한 도전"></p><blockquote><p>본인은 토스 내부와 완전히 단절된 사람으로, 완전히 외부인으로서 책을 통해 느낀 점을 말한다. 토스에 있는 친구들과 토스 내부에 대해 얘기해 본 적도 없으며 내가 느낀점이 실체와 다를 수도 있고 브랜딩이 된 외면을 보는 것일 수도 있다. 그런 건 상관 없고 내가 생각하는 이데아에 가까운 토스팀이 다시 한 번 재생산 되기 위해 무엇이 핵심일까를 고민하면서 읽었다.</p></blockquote><h1 id="토스의-문화"><a href="#토스의-문화" class="headerlink" title="토스의 문화"></a>토스의 문화</h1><p>내가 외부인으로써 공개된 자료를 통해 느낀 토스의 문화는 정말 “가슴뛰게 벅찬” 같은 느낌이다. 문제를 정의하고 해결하는 과정에 걸림돌 없이 목표를 달성하고 싶어하는 마음을 가진 동료들이 뭉쳐서 엄청난 속도로 밀고 나간다. 또한 절대적인 상급자가 존재하지 않고 자신이 책임지는 프로젝트를 끝까지 밀어붙일 수 있는 자율성을 보장한다. 마지막으로 이러한 과정에 발생하는 실패를 관용적으로 받아들이며 이전 실패와 상관 없이 다음 타석에 풀 스윙을 할 수 있는 타선으로 인재를 배치한다. 이 세 가지 정도가 외부인이 느낀 토스팀의 성공 조건이다.</p><blockquote><p>많은 스타트업이 자신들의 회사가 그렇다고 말 하지만 실감은 안나는 것 같다. 누구나 이 방법이 스타트업의 정수라고 생각하지만 그런 능력이나 그런 가치관을 가진 사람들로 구성된 조직을 구성하거나 유지하는 것이 쉽지 않기에 적절한 규모를 가진 사례를 찾기 어렵다. 정말 극초기 스타트업은 모든 일이 이렇게 돌아가지만, 규모가 조금만 커져도 이런 모습이 발견되기란 쉽지 않은 것 같다.</p></blockquote><h1 id="속도를-유지하는-것"><a href="#속도를-유지하는-것" class="headerlink" title="속도를 유지하는 것"></a>속도를 유지하는 것</h1><p>책 기준 2019년 코로나 지원금 신청 및 조회 서비스를 만들 때 토스팀스러운 속도가 나왔다고 말한다. 정부 지원금을 신청하는 프로세스를 제공해야 한다는 목표를 팀원들끼리 만들고 이 목표 달성이 줄 수 있는 임팩트를 상상하면서 많은 사람들이 이 프로젝트에 자발적으로 참여했다. 토스는 2019년도에도 규모있는 스타트업이었지만 메인 페이지에 걸릴 서비스를 개발하는 TF 조직을 자유롭게 구성하고 엄청난 속도로 완성했으며 심지어 대표조차 퇴근할 무렵에나 이 스레드를 확인하며 “무슨 일이 벌어지고 있느냐”고 물었다고 한다. 이런 과정으로 서비스 개발이 가능했던 이유는 다음 세 가지 정도인 것 같다.</p><ul><li>상급자와 무관한 의사 결정</li><li>빠른 공유와 정보 확산</li><li>빠르게 테스트 할 수 있는 인프라</li></ul><p><strong>상급자와 무관한 의사 결정을 하는 것</strong>은 작든 크든 지켜지기 꽤 어려운 것 같다. 작으면 작기 때문에 크면 크기 때문에 상급자의 개입이 들어간다. “우리는 상급자가 없는데요?” 라고 말하는 팀이 있을 수도 있다. 하지만 웬 뜬금없이 한 개발자가 “이거 만들어야겠는데요” 라고 말하면서 스레드를 열고, 여기에 사람이 모여서 프로젝트가 완성되는 과정은 정말 드물게 발생한다. 물론 토스 입장에서도 오랜 시간 동안 회자될 토스 다운 업무 진행이었다고 말할 정도로 자주 있는 일은 아니었겠지만 개인적으로 “그 당시 토스 규모 정도에서도 이런 동작이 가능하구나”라고 생각했다.</p><p><strong>빠르게 정보를 공유하는 것</strong>은 이상적이지만 본래 목적을 잃고 노이즈가 되어버리는 경우도 자주 있다. 구성원들이 소통하는 채널이 노이즈로 인지되지 않도록 유지해야 한다. 중요한 정보가 확실히 전달되어야 하는데, 이때 “중요한 정보”라는 기준이 너무 모호하다. 모두가 중요한 정보라고 판단하는 기준을 비슷하게 가지고 있어야 하는데 이는 전사적인 비전, 미션이라고 불리는 것들이 유의미하게 구성원들과 합의되어야 할 것 같다. 바라보는 방향이 같으면 어떤 정보를 접했을 때 이 정보가 우리 팀에게 어떤 의미이고, 무엇이 필요한지 말하지 않아도 모두 알 수 있다. 물론 세부적이고 현실적인 알람 기준, 얼마나 구체적인 정보인지 등을 파악하고 유의미한 범위에게 우선 전달 되는 것도 중요하지만, 어떤 정보를 보고 각 조직이 어떤 도움을 제공할 수 있는지 자발적으로 참여하며 소통하는 조직이 되는 것이 중요하다.</p><p>빠르게 시도한다는 것은 조금 관용적으로 표현해서 빠르게 실패한다와 같다. 그러나 실패만으로 끝나면 안되고, 회수되어 팀에게 겸험치로서 흡수되어야 한다. 말은 쉽지만 회수되는 과정도 일이다. 만들었던 코드를 수정하고 데이터나 자료들을 정리하는 과정이 필요하다. <strong>반복되는 시도와 실패를 회수하는 과정을 선형적인 속도로 유지하기</strong> 위해서는 인프라에 대한 투자가 필요하다.</p><p>토스팀은 실패를 기본으로 깔고 가던 시기가 있는데, “어짜피 실패할 건데 무슨 리팩토링이야” 같은 느낌으로 개발을 시작하는 것이다. 전재가 그렇다면 논리적으로 미래의 선형적인 개발 속도는 그렇게 중요한 의사 결정 요인이 아니다. 하지만 이는 언젠가 팀의 발목을 잡는다.</p><p>많은 스타트업들처럼 토스팀 역시 미래의 선형적인 속도 유지, 안정적인 개발은 당장 중요한 게 아니였으므로 이 작업은 계속 미뤄졌다. 많은 극복 스토리가 그렇듯 토스팀도 대장애 시대를 맞이하고 나서야 속도만큼이나 중요한 포인트가 있음을 인정하고 인프라에 힘을 쏟고 디자인 시스템을 개발했다.</p><h1 id="인재-밀도"><a href="#인재-밀도" class="headerlink" title="인재 밀도"></a>인재 밀도</h1><p>토스의 문화로 잘 알려진 것들 중 여러 스타트업이 차용해 사용하고 있는 문화들이 있다. 상하 관계가 없는 조직, 정보의 투명성, 상향식이 아닌 자유로운 업무 프로세스 같은 것들인데, 토스가 시작했다고는 볼 수 없고 실리콘벨리의 문화가 적용된 것으로 알고 있다. 현재는 대부분의 IT 스타트업에서 이를 당연시하고 있고 이와 반대되는 조직은 스타트업이라 할 수 없다는 수준의 인식이 있다.</p><blockquote><p>“이런 게 스타트업 아니겠어요?”라는 맥락과 대중들이 가지고 있는 이미지가 있는데 이는 지금까지는 그렇게 부정적으로 사용되고 있지는 않다. 자유로움, 성공에 대한 욕망 등… 창의적이고 부지런한 이미지로 많이 사용되고 있다. 하지만 만약 내가 스타트업이라는 주제로 시트콤을 만들면 이 주제로 전체의 1/3 분량을 만들 수 있을 것 같다.</p></blockquote><p><img src="/images/2023-01-11-outstanding-team/dalle-siliconvalley.png?style=centerme" alt="DALL-E가 그린 실리콘밸리 문화"></p><p>어찌됐든 토스팀은 이런 전형적인 스타트업의 문화를 가지고 성공했다. 토스팀을 보면서 이런 문화가 올바르게 동작하기 위해서는 먼저 인재 밀도가 일정 수준 이상 유지되어야 한다고 생각했다. 그래서 이 모든 걸 아우르는 주제는 인재 밀도이다. 스타트업에게 “인재”는 상황마다 다르다. 현재 어떤 스테이지에 있는가, 구성원과 팀원이 몇 명인가, 서비스는 어디에서 하고 있는가 등 상황에 따라 회사가 필요한 인재는 많이 바뀌는 것 같다.</p><p>하지만 개인적으로 아주 초기부터 유니콘에 도달한 이후에도 공통적으로 요구되는 인재상이 있는 것 같다. 크게 주도력, 실행력, 논리력 정도로 설명할 수 있을 것 같다. 책에서는 토스에서 자랑하는 프로덕트 오너(PO)가 정확히 이러한 능력들을 요구하는 것 같다. 논리적으로 설득적인 가설과 주장을 주도적이고 강하게 몰아붙여 검증하는 사람들이 필요하다. 이후 나오는 “인재”는 이러한 능력이 남들보다 월등한 사람들이라고 가정하고 작성했다.</p><blockquote><p>“블리츠 스케일링”이라는 책에서는 회사의 규모에 따라 단계를 나눠 블리츠 스케일링 전략을 설명하는 부분이 있다. 블리츠 스케일링에 대한 내용을 언급하려는 건 아니고, 약 마을(100명 초과 ~ 999명 이하) 단계까지는 적용될 수 있는 인재의 공통 분모가 아닐까 싶다. 그 이상의 기업에는 있어본 적이 없고 프로세스를 경험해보지 않아서 모르겠다.</p></blockquote><hr><p>스타트업에서 상하 관계라는 것은 적대시해야 할 것으로 치부되고 있다. 이런 경향은 오래 지속되었고 이제 어떤 스타트업의 소개 페이지에 “수평적인 조직문화”를 써놓는 게 오히려 더 이상하게 느껴진다. 수평적이라는 뜻은 이상적인 의미로 누군가의 의견에 실질적인 무게가 더 실리는 것이 없다는 뜻이다. 하지만 이게 현실적으로 효과적일까? 만약 구성원 중에 비즈니스를 잘 모르는 사람과 잘 아는 사람 사이의 의견이 동일한 경중으로 다뤄지고 절충안을 선택했다면 최선의 선택이었음을 확신할 수 있을까? 또는 설득을 통해 서로의 의견을 좁히는 과정이 더 합리적인 결론에 다다를 수 있다고 쳤을 때 더 경험 많은 사람의 결정을 보고 배우는 것보다 빠르고 효과적인가?</p><p>그니까 만약 수평적인 조직 문화를 만들려 “저 동료가 나보다 더 나은 결정을 할 수 있다.” 라는 가정이 필요하다. 가능성을 언급하는 전재이므로 항상 참이지만, 동료로부터 더 나은 결정이 자주 나와야 이 가정을 유지함으로써 얻는 효과가 크다. 따라서 수평적인 조직 문화를 만들고 싶으면 인재 밀도가 높아야 한다.</p><blockquote><p>수평적이라는 것이 회사 의사결정에 자신의 의견이 반영됨으로써 회사에 기여하고 있음을 직접적으로 느끼는 데 도움이 되므로, 구성원들의 만족을 높여준다는 얘기는 부차적인 얘기다. 우리가 수평적일 수 있기 때문에 구성원들은 만족을 느끼는 방향이어야만 한다.</p></blockquote><p>회사 정보의 투명성도 최근 많은 회사에서 가져다 쓰는 문구이다. 회사가 구성원들에게 최소한의 정보(연봉이나 구성원의 프라이버시, 보안 관련 정보 등)를 제외하고 모두 열람이 가능하게 하는 것을 말한다. 투명하게 정보를 공개해서 얻을 수 있는 장점은 무엇일까? 모든 구성원들이 회사와 관련된 많은 정보를 얻음으로써 각자의 문제에 가장 적합한 방법으로 문제를 해결하는 데 도움을 준다. 하지만 상하 관계와 비슷한 맥락으로 인재 밀도가 높아야 정보 공개도 유의미하다.</p><p>하달 방식이 아니라 자유롭게 만들어지는 업무 프로세스는 어떨까? 회사가 유기체처럼 성장한다는 느낌을 받을 수 있을 것 같다. 물론 엄청 뜬금 없는 걸 개발해서 넣겠다고 하는 것은 리더격인 사람이 보기에 당황스러울 수는 있는데 만약 이 사람이 진행하고자 하는 일이 회사에 좋은 결과를 가져올 수 있을 것이라고 믿을만 하다면 이 프로세스는 굉장히 효율적이고 빠르게 서비스를 성장시킬 수 있다.</p><h1 id="인재는-왜-모이는가"><a href="#인재는-왜-모이는가" class="headerlink" title="인재는 왜 모이는가"></a>인재는 왜 모이는가</h1><p>책을 보면 토스에는 적기에 최적의 인재가 필요한 역할을 맡아줬다. “마침 ~ 관련된 경험이 있는 OOO가 합류해 이 역할을 맡았다.”는 뉘앙스가 자주 나온다. 물론 현재 문제를 가장 잘 해결할 수 있어 보이는 사람들을 찾았겠지만 이 사람들이 돈을 많이 받았기 때문에 와서 이 역할을 수행하지는 않았을 것 같다.</p><blockquote><p>물론 토스는 돈을 많이 주는 조직으로 유명하다.</p></blockquote><p>이런 분들이 모이는 이유가 연봉만 있는 것이 아니라고 가정하고, 책을 읽으면서 그 외 몇 가지가 있을 수 있겠다고 생각한 포인트가 있다. 일단 인재가 가서 해결해야 하는 문제의 규모와 영향력이 얼마나 될지를 볼 것 같다. 책에서 초반 토스를 견인하던 제네럴리스트들이 더 이상 자신들의 역량을 발휘하지 못할 것 같다며 다음 스탭을 찾아 떠나는 내용이 있다. 본인이 해결하고 영향을 주는 범위가 큰 시점에 합류해 충분히 자신의 영향력을 행사한 후, 그 시점이 지나가자 퇴사를 하는 모습으로 보였다.</p><blockquote><p>인재 모으는 일반화된 방법이 있을 것이라 생각하지는 않는다. 인복은 그야말로 복이 아닐까.</p></blockquote><h1 id="정리"><a href="#정리" class="headerlink" title="정리"></a>정리</h1><p>토스의 이야기를 다큐멘터리를 보듯 재밌게 읽었다. 마침 뛰어난 조직에 대해 고민하던 시기와도 겹쳐서 이런 저런 생각을 하면서 토스팀이 발전해온 길을 뜯어본 것 같다. 또 이러한 조직이 나타나길 바라고 거기에 내가 포함되어 있으면 좋겠다.</p>]]></content:encoded>
<category domain="https://changhoi.kim/categories/essay/">essay</category>
<comments>https://changhoi.kim/posts/essay/outstanding-team/#disqus_thread</comments>
</item>
<item>
<title>2022년, 개발 4년 차 회고</title>
<link>https://changhoi.kim/posts/logs/20230105/</link>
<guid>https://changhoi.kim/posts/logs/20230105/</guid>
<pubDate>Wed, 04 Jan 2023 15:00:00 GMT</pubDate>
<description><p>개발을 시작한 지 4년, 블로그를 시작하고 4번째 회고가 되었다. 올해는 학생이 아닌 상태로 상태 전환이 발생하기도 했고, 어느 순간부터 마음속에 있던 삶의 마일스톤 중 하나를 해결한 것처럼 느껴지기도 한다. 지난 21년 회고를 거의 1분기가 끝나고 썼던 터라 이번 회고가 좀 짧게 느껴지긴 하지만 아무튼 그 이후(혹은 일부 포함해서) 어떤 일들이 있었나 정리해보려고 한다.</p></description>
<content:encoded><![CDATA[<p>개발을 시작한 지 4년, 블로그를 시작하고 4번째 회고가 되었다. 올해는 학생이 아닌 상태로 상태 전환이 발생하기도 했고, 어느 순간부터 마음속에 있던 삶의 마일스톤 중 하나를 해결한 것처럼 느껴지기도 한다. 지난 21년 회고를 거의 1분기가 끝나고 썼던 터라 이번 회고가 좀 짧게 느껴지긴 하지만 아무튼 그 이후(혹은 일부 포함해서) 어떤 일들이 있었나 정리해보려고 한다.</p><span id="more"></span><h1 id="회사에서-하는-일"><a href="#회사에서-하는-일" class="headerlink" title="회사에서 하는 일"></a>회사에서 하는 일</h1><p><a href="/posts/logs/20220417">지난 회고</a>를 쓴 4월부터 회사에 다니고 있고 플랫폼 엔지니어링 팀에 있다. 이전 인턴을 했던 팀도 그 당시에는 플랫폼 조직이었는데, 그때 영향을 받아 플랫폼 조직에서 일하고 싶어 했다. 시간은 좀 흘렀지만, 올해 취업을 준비했던 사람으로서 플랫폼 조직이 유난히 경력을 보는 경향이 있다고 느꼈다. 회사마다 이유는 다르겠지만 요구사항들이 일반적으로 쉽지도 않고 만들어지는 프로젝트들이 영향을 주는 범위가 넓다 보니 안전하게 개발할 수 있는 능력을 갖추고 있어야 하기 때문이라고 생각한다. 관련된 경력이 그렇게 많은 편은 아니었지만, 주니어로서 입사할 수 있었다.</p><p>플랫폼이라는 말은 조금 모호하게 들릴 수 있다. 실제로 업무 범위도 모호하다. 보통 사내의 회색 영역이라고 불리는 어떠한 팀에도 속하기 애매한 영역의 개발을 맡는다고 생각하는데, 이런 정의조차 모호하다. 그래서 실제로 JD를 살펴보면 회사마다 이를 정의하는 방법이 다양하다.</p><p>내가 취업을 준비하던 시기에 생각하던 플랫폼을 조금 더 구체적으로 서술하자면 서비스들이 공통으로 사용하는 도구들, 예를 들어서 Push 알림(메시징), 지리 정보, 인증, 미디어 서비스를 만드는 것으로 생각했다. 현재 회사의 플랫폼 조직은 그런 역할은 아니고 인프라 레벨의 공통적인 문제를 해결하는 조직이다. 예를 들어 멀티 클러스터 안에서 서비스 디스커버리 서비스, 로깅 파이프라인, 배포 도구 같은 걸 만든다. 이 팀에 처음 왔을 때는 뭔가 SRE 조직 같다는 생각도 들었다.</p><p><img src="/images/2023-01-05-20230105/current-platform.png?style=centerme" alt="현재 회사의 플랫폼 팀"></p><blockquote><p>CTO님이 플랫폼 조직에서 서비스 영역의 개발을 하는 것이 이상하다고 느끼셨다고 들었다. 해외에서 개발하다가 오신 분들이 공통으로 이런 얘기를 하시는 걸 보면, “플랫폼”이라는 팀이 해외에서는 뭔가 인프라 엔지니어링을 하는 그런 느낌이 있나 보다. 이전 인턴을 하던 회사에서도 비슷한 이유로 “플랫폼”이라는 이름을 버리고 “서비스 코어”? 이러한 이름으로 변경된 것으로 알고 있다. 우리팀 역시 서비스 조직에서 직접 가져다 쓰는 컴포넌트를 개발하는 조직으로 “서비스 컴포넌트” 조직을 새로 만들었다.</p></blockquote><p>생각하던 업무 범위는 아니었지만, 오히려 재밌었다. 지금 회사는 규모상 확장을 준비하고 있는 단계라고 느껴진다. 글로벌 서비스도 준비하고 리텐션을 유지하기 위한 여러 전략도 준비 중인 것 같다. 취업을 준비하던 시기에는 이 회사가 구체적으로 어떻게 비즈니스 발전이 있을 예정인지까지는 몰랐지만, 마이크로서비스가 도입되고 있다는 것까지는 알 수 있었다. 이 단계에서 플랫폼 팀의 업무인 쿠버네티스 컴포넌트를 개발한다든지, 인프라에 도입되는 것들을 개발하는 것 등은 경험하기 쉽지 않으리라 생각해서 지금 회사에 오게 되었다. 지금 업무에 대한 만족도는 꽤 높다.</p><h1 id="사이드-프로젝트"><a href="#사이드-프로젝트" class="headerlink" title="사이드 프로젝트"></a>사이드 프로젝트</h1><p><img src="/images/2023-01-05-20230105/inchtown-icon.png?style=centerme" alt="사이드 프로젝트 아이콘"></p><p>언제나 그렇듯 사이드 프로젝트를 시작했다. 이번 사이드 프로젝트에서는 쿠버네티스에 어느 정도 익숙해지기 쿠버네티스를 사용해 서비스를 배포할 목적을 가지고 있었다. 그렇게 크지 않은 서비스지만 억지로 프로젝트를 나눠서 개발했다. 서비스는 거리 기반 공동 구매 플랫폼이다. 실제 시작할 때는 굉장히 유망할 것 같은 생각이 많았는데 바로 배민, 쿠팡, 당근마켓 등이 비슷한 컨셉의 서비스를 공개하고 있다. 공부 목적으로 개발하는 거지만 뭔가 아쉬운 느낌이 들었다.</p><p>이번 사이드 프로젝트는 꽤 많은 사람하고 시작했는데, 약 6명이 같이 시작했다. 클라이언트 개발을 도와주는 팀원 두 명과 백엔드 두 명, 디자인해주는 친구 한 명과 나까지 총 6명이었는데, 지금은 백엔드 해주시는 분이 한 분 나가시고 5명 체제로 여전히 진행 중이다. 사이드 프로젝트를 팀원들과 하는 이유는 혼자 하는 사이드 프로젝트를 지속하지 못할 것 같았기 때문인데, 지금까지 내가 하는 패턴을 보니 지속할 수 있었을 것 같다. 프로젝트 주제가 재밌어서 그런지 뭔가 계속하게 된다. 지난해 사이드 프로젝트를 할 때 잘 모르는 분들과 시작하지는 말아야겠다고 생각해서 모두 지인분들과 같이 했다. 확실히 프로젝트 진행이 원활하기도 하고 프로젝트 회의나 프로젝트를 같이 진행하는 날이 되게 재밌다.</p><p>혼자 진행하는 것과 팀으로 진행하는 것은 뭔가 다른 방면으로 장단점이 있긴 한 것 같다. 혼자서 진행했다면 이미 프로젝트를 완성했을 것 같다. 하지만 사이드 프로젝트를 하는 목적과 다른 여러 가지를 같이 해줘야 하기도 하고 비교적 지루하다. 팀으로 하는 것은 재밌으나 느리다. 우리 모두 이 작업에 몰두하고 있다면 얘기가 다르지만 서로 투자할 수 있는 시간도 다르고 서로에 의해 블로킹 되는 경향도 있기 때문에 점점 느려지는 것 같다.</p><h1 id="스터디"><a href="#스터디" class="headerlink" title="스터디"></a>스터디</h1><p>이번에도 스터디를 많이 시작했다. 기술 세션을 일정 기간 진행했고 데이터 중심 애플리케이션 설계, Rust In Actions, Database Internals 책을 읽었다.</p><p>4월 5월쯤부터 데이터 중심 애플리케이션 설계 스터디를 시작했다. 다른 한 분과 같이 했는데, 각자 챕터를 나눠서 내용을 정리하고 발표했다. 해당 챕터는 각자 읽어오기는 하지만 발표를 맡은 챕터는 심도있게 이해하고 정리해가는 스터디였다. 책 내용이 워낙 어려워서 각자 맡은 챕터에 집중해서 공부하는 것과 다른 사람이 꼼꼼히 이해한 다음 이해하기 쉽게 설명해주는 게 생각보다 도움이 많이 됐다. 책 내용도 데이터베이스의 분산 환경의 사용과 관련된 내용이 많이 있었는데, 재밌어하는 주제라 스터디가 더 재밌었던 것 같다. </p><p>기술 세션은 각자 2, 3주 정도 텀을 가지고 기술 학습을 해서 세션을 준비해오는 구조였는데, 이런 비슷한 스터디를 몇 번 했던 적이 있어서 블로그로 쓰려고 했던 내용이나 이전 세션에 했던 내용 등을 다듬어서 발표했다. 세션을 많은 사람과 진행한 것이 아니라서 타인으로부터 매우 많은 정보를 얻을 수는 없었지만, 이 세션을 준비하면서 특정 주제에 대해 또 깊게 공부해볼 기회가 되어서 개인적으로 만족스러웠다. 기술 세션을 진행하다가 Database Internals 책을 읽는 스터디로 바뀌었다. 이 책도 데이터 중심 애플리케이션 설계 책하고 아주 비슷한 내용이 많이 섞여 있어서 뭔가 복습이 되는 것도 있고 조금 더 깊게 공부하는 것도 있다. 특정 DB 애플리케이션에 종속적이지 않은 DB 내부적인 이야기를 학습하는 것이라 재밌게 읽고 있다. 현재도 진행 중이지만, 이후 나올 원격 근무로 인해 잠시 정지 상태이다.</p><p><img src="/images/2023-01-05-20230105/rust.jpeg?style=centerme" alt="러스트"></p><p>Rust 스터디는 Rust In Action을 공부하는 것을 목표로 했지만, Rust를 처음 만져보는 입장에서는 Rust가 공식적으로 제공해주는 <a href="https://doc.rust-lang.org/book/">The Rust Programming Language</a> 책을 읽는 게 좋다고 판단해서 이 책을 읽고 있다. 스터디에서는 일단 문법을 공부하는 시간을 한 달 반 정도 진행했는데, 지금까지 배웠던 언어 중에서 Rust가 제일 공부할 게 많은 것 같다고 느낀다. 이전에 Kotlin을 공부할 때 이렇게 느꼈는데 코틀린은 뭔가 Helper 같은 도구가 많은 것처럼 느껴졌는데, Rust는 그냥 언어 자체가 조금 복잡한 듯한 인상을 받았다. Rust는 내가 좋아하는 특징과 싫어하는 특징을 모두 갖추고 있다. Go를 좋아하는 나는 언어가 복잡도가 높다는 점이 굉장히 불호인데 강한 타입과 안전한 프로그램을 위한 특징들은 굉장히 마음에 든다. 복잡하다는 건 어느 정도 학습으로 커버가 가능할 것이라 믿어서 지금까지 스터디는 이어지고 있다. 대충 문법들은 마쳤는데, Rust In Action 책에서는 시스템 프로그래밍을 하는 내용이 반 이상이라 그 부분을 시작했다.</p><hr><p><a href="/posts/logs/20220417">4월에 썼던 회고</a>에서도 어떻게 기술을 바라봐야 하는가, 어떻게 학습해야 하는가에 대한 기준이 생겼다고 했는데 이 기준은 여전히 유지되고 있다. 깊게 공부하는 습관을 들여서 여러 스터디와 기술 학습에 적용하면서 이전까지 가지고 있던 마일스톤을 달성한 것 같다. 마일스톤은 “기술적 목마름”이라고 내가 이전부터 표현하던 것인데, 도달하고 싶은 기술적 능력치가 어느 정도 있고 이를 달성하는 것을 의미했다. 이 마일스톤을 달성했다는 것은 그럼 목표로 하던 “어느 정도의 기술적 능력”을 갖춘 것일까? 그렇지는 않다. 여전히 부족함을 많이 느끼기도 하고 공부할 것 자체가 너무 많다. 하지만 이렇게 공부하면 “기술적 목마름”을 해결하는 방향으로 나아간다는 것을 느꼈다. 그전에는 “기술적 목마름”이 경험에 의해 채워질 수 있다고 생각했는데, 내가 원하던 것은 이런 학술적인 접근을 통해 이룰 수 있는 경지였던 것 같다.</p><blockquote><p>주변 시니어분과 경력이 기술적 측면의 목표를 달성하기에 중요한 것일까에 대해 얘기를 나눈 적이 있는데, 시니어분은 플랫폼 개발 말고 트래픽도 관리해보고 장애도 맞아보는 것이 많이 가르침을 주기 때문에 경력이 중요한 것 같다고 하셨다. 정확히 맞는 말이라고 생각하지만, 다시 생각해도 내가 원하는 기술적 목표 달성은 학술적인 접근으로 달성하는 것 같다. 경험으로 얻을 수 있는 능력치와 깊은 학습으로 얻을 수 있는 능력치가 겹치는 부분도 분명히 있으나 각자가 강화 해주는 영역이 다른 것 같이 느껴진다.</p></blockquote><p>이게 항상 머릿속으로 생각하던 거라 글로 썼을 때 온전히 전달이 안 될 수도 있다. 무슨 얘기를 하는 걸까 싶을 수도 있지만… 결론은 올해 나는 아직 내가 정해둔 기술적 경지에 도달한 것은 아니지만 도달하는 방법을 알았다고 느꼈다.</p><blockquote><p>그럼, 마일스톤을 달성한 것이 맞느냐? 아직 기술적 목표에 도달한 것은 아니지 않는가? 하지만 이 과정은 내가 개발하는 동안 무한히 지속되어야 한다. 이를 달성하겠다는 말은 애초에 불가능하다. 방향을 정확히 맞춰놓고 나아간다는 것으로 충분히 마일스톤 달성에 체크를 해도 좋을 것 같다.</p></blockquote><h1 id="네이버-부스트캠프-코드-리뷰어"><a href="#네이버-부스트캠프-코드-리뷰어" class="headerlink" title="네이버 부스트캠프 코드 리뷰어"></a>네이버 부스트캠프 코드 리뷰어</h1><p>이번 해 하반기 조금 넘어서 한 달 조금 넘는 기간 동안 네이버 부스트 캠프 코드 리뷰어로써 활동했다. 경력을 기록하는 단계가 있어서 지금까지 돈을 받으면서 일한 기간이 얼마나 되는지 정리하는 시간을 가졌다. 그 당시 기준으로 한 2년 몇 개월이 나왔던 것 같은데, 지금은 거의 3년이 되어갈 것 같다. 연차가 중요한 건 아니지만 평소에 스스로의 처우에 대해 생각할 때 항상 1년 중간쯤 했나? 라는 기준으로 생각했는데, 앞으로는 정확히 기간에 대해 인지해야할 것 같다.</p><p>아무튼 다행스럽게 네이버 부스트 캠프에서 코드 리뷰어로 선정되어 학생들의 코드를 리뷰했다. 리뷰 내용은 사실 그렇게 대단하지 않기도 하고 도움이 얼마나 됐는지 잘 모르겠지만 리뷰를 받는 학생분들과 따로 티타임 비슷한 시간을 격주에 한 번 정도 다양한 사람들과 진행했는데, 이때 얘기했던 것들이 내가 학생이었다면 더 들어보고 싶었던 내용들이 아니었을까 싶었다. 개인적으로 보람차기도 했고 재밌던 시간을 보낸 것 같아 다음에도 이러한 기회가 생기면 다시 해보고 싶다.</p><h1 id="Working-From-Anywhere"><a href="#Working-From-Anywhere" class="headerlink" title="Working From Anywhere"></a>Working From Anywhere</h1><p>지금 일하고 있는 회사는 6개월에 한 달은 출근을 하지 않고 어디서든 일을 할 수 있는 제도가 있다. 이를 Working From Anywhere(WFA)라고 부른다. 원래 여행을 그렇게 자주 하거나 즐기는 편은 아닌데, WFA를 하는 김에 오래전부터 해보고 싶던 해외에서 디지털 노마드를 준비했다. 그리고 이왕 비행깃값 쓰는 김에 12월과 1월 WFA를 붙여서 갔다 오기로 했다. 해외에 가서 WFA를 한다고 일을 제대로 못 하는 것으로 평가가 된다면 앞으로 해외로 WFA를 하기 눈치가 보인다든지, 조금 과장되어 아예 제도가 폐지되어버릴 수도 있으므로 국내에서 일하는 것보다 더 잘해야겠다고 생각했다. 그래서 지낼 국가를 선택할 때 기준은 다음과 같았다.</p><ul><li>업무를 할 수 있는 공간 (무선 인터넷, 책상, 혼자 쓰는 공간 등)</li><li>업무를 수행한 다음에는 어느 정도 여행을 할 수 있는 시간 (적절한 시차)</li><li>한 달 살이를 하기 적절한 생활 물가, 좋은 날씨</li></ul><p>이렇게 고르고 나니까 그리스와 포르투갈 정도로 선택이 되었다.</p><blockquote><p>이 계획을 짤 때까지만 해도 서머타임이 적용되고 있어서 포르투갈도 새벽에 일어나서 일할 수 있다고 생각하는 범주에 들어왔다. 하지만 지금은 잘 모르겠다. 이제 곧 포르투로 이동하는데 해봐야 할 것 같다.</p></blockquote><p><img src="/images/2023-01-05-20230105/parthenon.jpeg?style=centerme" alt="파르테논 신전은 정말 멋있었다"></p><p>그래서 현재는 그리스에 있다. 1월 초까지 그리스에 있고 그 이후부터 2월 초까지는 포르투갈에 있는다. 아테네에서 한 달을 지내고 있고 주말에는 여기저기 근교나 섬에 가보고 있다. 아테네에 도착한 초반에는 너무 바쁘게 지내서(엄청난 스터디로 인해) 관광 시간을 낼 수가 없었다. 그래서 지금은 Rust만 진행하고 있고, Database Internals 스터디와 알고리즘은 멈췄다. 개인적으로 WFA는 굉장히 만족스럽다. 바쁘게 지내던 시점에서 조금 벗어나서 혼자 여행하니까 현재 상황에 대해 생각해볼 시간이 조금 늘어난 것 같다. 그리고 여유롭게 여행하기도 하고 쉬기도 하고 하면서 컨디션을 적절히 조절하면서 여행이 가능하다. 한국 시간대로 일을 하다 보니 일찍 잠을 자야만 하는 단점도 있는데, 그리스 시차 정도는 여행과 일 모두 가능한 정도로 느껴진다.</p><blockquote><p>여유롭게 한국에서 여유롭게 살았어도 얻는 장점 아닌가? 라고 생각이 들 수도 있으나… 한국에 있으면 절대 여유롭게 살 생각이 안 들었을 것 같다.</p></blockquote><p>하지만 비용은 만만치가 않다. 새벽에 일어나고 업무 공간이 있어야 한다는 것 때문에 에어비엔비에서 집을 30일씩 빌려서 쓰는데, 이 값이 가장 큰 것 같고 외식을 주로 하다 보니 유럽 외식 비용을 감당해야 한다.</p><h1 id="이번-해의-목표"><a href="#이번-해의-목표" class="headerlink" title="이번 해의 목표"></a>이번 해의 목표</h1><p>아테네에 있으면서 실존적인 고민을 많이 하게 되었는데, 마일스톤을 이룬 것과도 연관이 된다. 현재 고민이 많던 기술적 목마름을 어느 정도 해소했는데, 이걸 계속 지속하는 것이 10년 뒤의 내 모습이라면 만족할까? 혹은 현재 수행하고 있는 일을 10년 정도 한다고 하면 내가 원하는 모습이 되어있을까? 하는 질문을 많이 하게 되었다. 일단 질문을 많이 했다는 것 자체가 그렇지 않을 것 같다는 생각 때문인데, 마땅히 무엇을 해야겠다는 결론은 당연히 안 나온다.</p><p>고민 끝에 원초적으로 다음으로 이루고 싶은 목표가 “훌륭한 팀을 만들어보고 싶다”라는 결론에 이르렀다. 논리적인 이유는 딱히 없고 내가 “왜 예전부터 어떤 조직을 만들어서 도전해보길 원할까?”라는 생각을 했는데 그 과정 중 어떤 포인트에 가장 매력을 느끼는 건지 고민해보다가 나온 결론이다. 관련된 얘기를 지인과 했는데, 똑똑한 사람들과 모여서 문제를 해결하는 과정을 겪는 건 원초적으로 재미를 느끼는 요소일 수밖에 없다는 얘기를 했다. 생각해보면 원시 부족을 이루던 시기부터 훌륭한 팀원을 모아서 부족을 유지하는 그 욕구가 DNA로 박혀있을 것 같다. 내가 생각한 나의 원동력이 굉장히 원초적인 욕구로부터 왔다고 생각하니 실존적 고민의 결론이 잘 도출된 것 같은 생각이 든다.</p><p>그래서 목표는 무엇인가? 첫 번째로는 회사든 어디서든 새로운 사람들을 많이 만나볼까 한다. 보통 이런 성격이 아니라서 사람들을 사귀는 것을 목표로 삼은 적은 없는데 이번 해에는 한 번 노력해보려고 한다. 두 번째는 개발 말고 다른 것을 개인적으로 해볼까 한다. 뭘 해볼지는 안 정했는데, 다른 영역에 대한 경험이 좀 필요한 것 같다는 생각을 여행하러 와서 많이 하고 있다.</p>]]></content:encoded>
<category domain="https://changhoi.kim/categories/logs/">logs</category>
<category domain="https://changhoi.kim/tags/retrospect/">retrospect</category>
<category domain="https://changhoi.kim/tags/log/">log</category>
<comments>https://changhoi.kim/posts/logs/20230105/#disqus_thread</comments>
</item>
<item>
<title>etcd deep dive - Client Model</title>
<link>https://changhoi.kim/posts/database/etcd-client-model/</link>
<guid>https://changhoi.kim/posts/database/etcd-client-model/</guid>
<pubDate>Sat, 10 Dec 2022 15:00:00 GMT</pubDate>
<description><p>etcd를 사용하는 개발자들은 etcd가 공식적으로 제공해주는 클라이언트를 사용해 서버에 접근하는 것이 일반적이다. Go로 만들어진 클라이언트를 잘 관리해주고 있어서 보통은 이 클라이언트를 쓰는 것 같다. 지금 진행하는 프로젝트 역시 Go 클라이언트를 통해 etcd에 접근한다. 이번 글은 클라이언트가 어떻게 발전해 왔는지를 알려주는 <a href="https://etcd.io/docs/v3.5/learning/design-client/">Learning의 글</a>을 번역하고 공부했던 내용을 간단히 정리했다.</p></description>
<content:encoded><![CDATA[<p>etcd를 사용하는 개발자들은 etcd가 공식적으로 제공해주는 클라이언트를 사용해 서버에 접근하는 것이 일반적이다. Go로 만들어진 클라이언트를 잘 관리해주고 있어서 보통은 이 클라이언트를 쓰는 것 같다. 지금 진행하는 프로젝트 역시 Go 클라이언트를 통해 etcd에 접근한다. 이번 글은 클라이언트가 어떻게 발전해 왔는지를 알려주는 <a href="https://etcd.io/docs/v3.5/learning/design-client/">Learning의 글</a>을 번역하고 공부했던 내용을 간단히 정리했다.</p><span id="more"></span><hr><h1 id="Requirements"><a href="#Requirements" class="headerlink" title="Requirements"></a>Requirements</h1><p>클라이언트의 구현체는 다음과 같은 요구사항을 가지고 있다 </p><ul><li>Correctness: 서버 실패와 상관없이 일관성 보장을 훼손하면 안 된다. 예를 들어 글로벌 순서, 손상된 데이터를 쓰지 않는 것, 최대 한 번(At Most Once) 동작하는 Mutable Operation, 부분적인 데이터를 Watch 하지 않는 등의 동작을 의미한다.</li><li>Liveness: 서버는 간단히 실패하거나 연결이 끊어질 수 있는데, 클라이언트는 특별한 설정으로 통제하고 있는 게 없다면 문제 상황에서 서버가 다시 회복될 때를 기다리며 DeadLock이 발생하지 않도록 해야 한다.</li><li>Effectiveness: 최소의 리소스를 사용해야 한다. 예를 들어 TCP 컨넥션은 엔드포인트가 교체되면 안전하게 정리되어야 한다.</li><li>Portability: 공식적인 클라이언트의 형태가 문서화 되어있고, 여러 다른 언어에서 이를 구현할 수 있어야 한다. 또한 다른 언어들 사이의 에러 핸들링 방식이 일관되어야 한다.</li></ul><blockquote><p>요구사항의 많은 부분이 gRPC 클라이언트의 동작으로 인해 해결될 수도 있겠다고 생각했다.</p></blockquote><h1 id="Client-Overview"><a href="#Client-Overview" class="headerlink" title="Client Overview"></a>Client Overview</h1><p>클라이언트는 다음 세 가지 컴포넌트로 구성되어 있다.</p><ul><li><strong>Balancer</strong>: etcd 클러스터와 gRPC 컨넥션을 만드는 컴포넌트</li><li><strong>API Client</strong>: RPC를 etcd 서버로 보내는 컴포넌트</li><li><strong>Error Handler</strong>: gRPC 에러를 처리하며 엔드포인트를 변경할 것인지, 재시도할 것인지를 결정하는 컴포넌트</li></ul><p>API Client와 Error Handler의 경우는 v3로 오면서 큰 변화가 없었는지, 이하 내용에서는 밸런서의 변화에 관해서만 이야기한다. 밸런서의 변화는 클러스터 구성과 연결을 어떻게 할 것 인지를 결정할 때 도움을 줄 것 같다.</p><h2 id="clientv3-grpc1-0"><a href="#clientv3-grpc1-0" class="headerlink" title="clientv3-grpc1.0"></a><code>clientv3-grpc1.0</code></h2><h3 id="Overview"><a href="#Overview" class="headerlink" title="Overview"></a>Overview</h3><p>여러 엔드포인트 컨넥션을 유지하지만 하나의 엔드 포인트와 <code>Pinned Connection</code>을 가정하고 통신한다. 즉, 모든 클라이언트의 요청을 하나의 클라이언트에 먼저 보내는 구조다. 이 상황에서 장애가 발생하면 밸런서는 다른 엔드포인트를 선택해서 연결하거나 재시도하게 된다.</p><p><img src="/images/2022-12-11-etcd-client-model/clientv3grpc1.0.png" alt="clientv3-grpc1.0 Overview"></p><h3 id="Limitation"><a href="#Limitation" class="headerlink" title="Limitation"></a>Limitation</h3><p>이렇게 구성하게 되면 여러 엔드포인트에 대해 연결을 유지하므로 더 빠른 FailOver(페일오버)가 가능하지만, 더 많은 리소스를 사용하고 있게 된다. 또한 밸런서가 노드의 상태나 클러스터 멤버십 상태에 대해 이해할 수 없기 때문에 Network Partition이 발생한 노드에 갇힐 수 있다.</p><h2 id="clientv3-grpc1-7"><a href="#clientv3-grpc1-7" class="headerlink" title="clientv3-grpc1.7"></a><code>clientv3-grpc1.7</code></h2><h3 id="Overview-1"><a href="#Overview-1" class="headerlink" title="Overview"></a>Overview</h3><p>여러 엔드포인트 중 하나의 엔드포인트에만 TCP 컨넥션을 구성한다. 처음 연결할 때 클라이언트는 주어진 모든 엔드포인트에 컨넥션을 시도하는데, 가장 빠르게 연결된 컨넥션이 나오면 해당 주소를 <code>Pinned Connection</code>으로써 사용하고, 나머지 연결을 종료한다. <code>Pinned</code> 주소는 연결이 에러에 의해 닫힐 때까지 유지된다. 에러가 발생한다면 클라이언트의 에러 핸들러가 이를 받아 재시도할 수 있는 경우인지 아닌지 판단해서 다음 동작을 적절히 수행한다.</p><p>Stream RPC인 <code>Watch</code>, <code>KeepAlive</code>는 타임아웃 설정 없이 보내지는 경우가 많은데, 클라이언트는 HTTP/2 <code>PING</code>을 통해 서버의 상태를 체크한다. 서버의 응답이 없으면 새로운 서버에 연결한다.</p><p>장애로 인해 <code>Pinned Connection</code> 상태에서 내려갔든, 모종의 이유로 새로운 연결을 시도하든 건강하지 않다고 판단된 엔드포인트는 <code>Unhealthy List</code>에 등록된다. 만약 해당 리스트에 올라가게 되면 기본값으로 5초 동안은 해당 엔드포인트를 사용하지 않게 된다.</p><p><img src="/images/2022-12-11-etcd-client-model/unhealthy-list.png" alt="Unhealthy List 관리"></p><blockquote><p>위 동작은 <code>grpc-1.0</code> 버전 당시에도 마찬가지로 있었다고 한다.</p></blockquote><h3 id="Limitation-1"><a href="#Limitation-1" class="headerlink" title="Limitation"></a>Limitation</h3><p><code>grpc-1.0</code> 버전처럼 여전히 멤버십 정보는 알지 못하므로 동일하게 네트워크 파티션을 판단 못 하는 문제가 발생할 수 있다. 그리고 밸런서가 잘못된 <code>Unhealthy List</code>를 관리하는 문제가 있을 수 있다. 예를 들어 Unhealthy 마킹을 하고 나서 바로 서버가 정상화되었다면 5초 동안 건강한 서버를 사용하지 못하는 문제가 생긴다. 또한 Unhealthy로 관리되는 Recovery 과정이 gRPC의 <code>Dial</code>을 사용하고 하드 코딩된 부분이 있어 굉장히 복잡한 구현체였다고 한다.</p><p>위 문제는 사실 이번 버전의 문제는 아니고 이전 버전과 동일한 구현으로 인해 생기는 문제이다. 이번 버전은 컨넥션을 위한 리소스 소비를 줄였지만, 페일오버의 속도를 느리게 만든다는 단점이 생긴다.</p><blockquote><p>하위 버전과 비교했을 때 페일오버 문제는 트레이드 오프라고 생각된다.</p></blockquote><h2 id="clientv3-grpc1-23"><a href="#clientv3-grpc1-23" class="headerlink" title="clientv3-grpc1.23"></a><code>clientv3-grpc1.23</code></h2><h3 id="Overview-2"><a href="#Overview-2" class="headerlink" title="Overview"></a>Overview</h3><p><code>grpc1.7</code> 버전은 gRPC 인터페이스와 강하게 결합하여 gRPC 버전을 올릴 때마다 클라이언트의 동작이 망가지기 일쑤였고, 개발 과정의 많은 부분이 이를 호환되게 하는 수정이었다고 한다. 결과적으로 구현체는 복잡해지는 문제가 지속되었다.</p><blockquote><p>개발 과정에서도 gRPC 메인테이너가 과거 버전의 인터페이스를 유지하지 말 것을 권장했다고 한다.</p></blockquote><p><code>grpc1.23</code>으로 오면서 가장 주안점으로 둔 점은 밸런서의 페일오버 로직을 단순화하는 것이었다. <code>Unhealthy List</code>를 관리하지 않고 현재 사용 중인 엔드포인트에 문제가 생기면 다른 엔드포인트로 라운드로빈 하도록 바꿨다. 이에 따라 복잡한 상태 체크가 필요 없어졌다.</p><p>다른 포인트는 gRPC의 인터페이스와 강결합하지 않도록 바꾸는 것이었다. 관련된 내용은 자세히 다루지 않는데, 내부적인 변화가 많이 있었다고 한다. 이로써 Backward Compatibility(하위 호환성)를 유지하면서 gRPC 업그레이드에 쉽게 깨지지 않도록 구성했다.</p><p>컨넥션 방법에도 변경이 생겼는데, 여러 엔드포인트가 주어졌을 때 클라이언트는 다시 여러 <code>sub-connection</code>을 만드는 방법으로 바뀌었다. 한 서브 컨넥션은 각 엔드포인트를 의미하는 gRPC의 인터페이스(<a href="https://github.com/grpc/grpc-go/blob/master/balancer/balancer.go#L99">gRPC SubConn</a>)이다. 5개의 노드가 있다면 5개의 TCP 컨넥션 풀을 만든다. 초기 버전에서 설명한 것처럼 TCP 컨넥션은 자원을 더 소모하지만, 더 유연한 페일오버가 가능해진다.</p><p>대신 <code>grpc1.0</code>과는 다르게 <code>Pinned Connection</code>을 사용하지 않고 모든 연결 포인트에 요청을 분산했다. 이로써 더 공평하게 부하를 분산할 수 있게 되었다. 기본적으로는 라운드 로빈을 사용하고 있는데 gRPC처럼 갈아 끼울 수 있다.</p><p><img src="/images/2022-12-11-etcd-client-model/clientv3grpc1.23.png" alt="clientv3-grpc1.23 Overview"></p><p><code>grpc1.7</code>과 High Level은 유사하지만, 내부적으로 gRPC의 기능을 많이 활용하고 있다. 밸런서는 gRPC의 <code>resolver group</code>을 사용하고 <a href="https://github.com/grpc/grpc-go/blob/v1.50.1/balancer/balancer.go#L291">Balancer Picker Policy</a>를 구현해서 복잡한 동작을 gRPC가 수행하도록 위임했다. Retry의 경우도 gRPC의 인터셉터를 통해 처리함으로써 체인 안에서 자동으로, 그리고 보다 정교한 방법으로 처리되도록 했다.</p><blockquote><p>과거 버전에서는 Retry 로직이 직접 구현되어 복잡한 부분이 있었다고 한다.</p></blockquote><h3 id="Limitation-2"><a href="#Limitation-2" class="headerlink" title="Limitation"></a>Limitation</h3><p>이 버전은 현재 구현 상태이기 때문에, 앞으로 어떻게 발전시킬 것인지를 중점적으로 설명했다. 우선 현재 각 엔드포인트 상태를 캐싱함으로써 성능적 향상이 가능하다고 한다. 예를 들어 TCP 연결을 유지하기 때문에 <code>PING</code>을 보내고 Health가 보장되는 앤드포인트를 우선하도록 만들 수 있다. 하지만 <code>Unhealthy List</code>를 관리하는 것처럼 복잡도 증가 우려가 있기 때문에 논의가 더 필요한 주제라고 한다.</p><p>그리고 클라이언트 사이드의 <code>KeepAlive PING</code>을 여전히 사용 중인데, 네트워크 파티션 등 클러스터 멤버십을 고려한 Health Check가 필요하다. (<a href="https://github.com/etcd-io/etcd/issues/8673">관련 논의</a>)</p><p>마지막으로 Retry를 인터셉터에 의해 처리되도록 하고 있는데, 이후 공식적인 gRPC 스펙으로서 처리할 수도 있다.</p><blockquote><p>아직 관련된 gRPC의 Retry는 <a href="https://github.com/grpc/proposal/blob/master/A6-client-retries.md">Proposal</a> 상태인 것 같다.</p></blockquote><h1 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h1><ul><li><a href="https://etcd.io/docs/v3.5/learning/design-client/">https://etcd.io/docs/v3.5/learning/design-client/</a></li></ul>]]></content:encoded>
<category domain="https://changhoi.kim/categories/database/">database</category>
<category domain="https://changhoi.kim/tags/distributed-system/">distributed_system</category>
<category domain="https://changhoi.kim/tags/etcd/">etcd</category>
<comments>https://changhoi.kim/posts/database/etcd-client-model/#disqus_thread</comments>
</item>
<item>
<title>Rust Ownership</title>
<link>https://changhoi.kim/posts/rust/rust-ownership/</link>
<guid>https://changhoi.kim/posts/rust/rust-ownership/</guid>
<pubDate>Sun, 20 Nov 2022 15:00:00 GMT</pubDate>
<description><p>Rust는 GC가 없는 언어이다. 보통은 언어가 힙 메모리를 관리하기 위해 GC를 사용하거나 개발자가 직접 관리하는 두 가지 노선을 선택해 왔지만, Rust는 조금 독자적인 방법을 선택했다. 각 변수가 사용하는 메모리에 대한 소유권을 하나만 유지하면 GC가 필요 없다는 점을 이용한다. 만약 하나의 변수에 힙 영역 데이터가 묶여있다면 해당 변수가 더 이상 접근 불가능한 상태가 되었을 때 메모리를 곧바로 해제해버리면 된다. 실제로 러스트를 사용하다 보면 힙 메모리를 <code>free</code> 하지 않아서 간단한 프로그램을 쓸 때 꼭 GC가 있는 언어처럼 느껴진다. 이번 글에서는 소유권 및 그와 연관된 여러 러스트의 컨셉을 정리했다.</p></description>
<content:encoded><![CDATA[<p>Rust는 GC가 없는 언어이다. 보통은 언어가 힙 메모리를 관리하기 위해 GC를 사용하거나 개발자가 직접 관리하는 두 가지 노선을 선택해 왔지만, Rust는 조금 독자적인 방법을 선택했다. 각 변수가 사용하는 메모리에 대한 소유권을 하나만 유지하면 GC가 필요 없다는 점을 이용한다. 만약 하나의 변수에 힙 영역 데이터가 묶여있다면 해당 변수가 더 이상 접근 불가능한 상태가 되었을 때 메모리를 곧바로 해제해버리면 된다. 실제로 러스트를 사용하다 보면 힙 메모리를 <code>free</code> 하지 않아서 간단한 프로그램을 쓸 때 꼭 GC가 있는 언어처럼 느껴진다. 이번 글에서는 소유권 및 그와 연관된 여러 러스트의 컨셉을 정리했다.</p><span id="more"></span><h1 id="Ownership-소유권"><a href="#Ownership-소유권" class="headerlink" title="Ownership (소유권)"></a>Ownership (소유권)</h1><p>GC가 있는 언어 같다고 했지만, 개인적으로 사실 조금 이질적인 느낌이 있다. Ownership은 러스트를 잘 쓰기 위해 숙련도가 요구되는 주범이다.</p><p><a href="https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html#what-is-ownership">The Rust Programming Language</a> 책에서는 소유권을 “규칙”이라고 설명한다. 소유권과 관련된 규칙들을 컴파일 타임에 모두 확인하고 하나라도 지켜지지 않는다면 컴파일되지 않는다. 컴파일 타임에 확인되므로 런타임에서는 퍼포먼스에 영향을 주지 않는다.</p><p>규칙은 다음과 같다.</p><ul><li>각 값은 모두 주인(Owner)을 가지고 있다.</li><li>한 번에 하나의 Owner를 갖는다.</li><li>Owner가 스코프를 벗어나면 값은 정리된다.</li></ul><p>값은 하나의 식별자를 주인으로 갖게 된다고 이해하면 좋을 것 같다. 식별자는 러스트의 변수 스코프에 의해 접근 불가능해지는 순간이 오는데, 이때 값들을 모두 정리한다.</p><h1 id="Variable"><a href="#Variable" class="headerlink" title="Variable"></a>Variable</h1><p>변수는 자신이 담고 있는 값이 어느 정도의 메모리를 사용해 할당되는지 알고 있어야 한다. 예를 들어서 다음과 같이 <code>u8</code> 타입이 있다면 컴파일러 입장에서 이 타입은 1바이트를 사용한다는 것을 알 수 있다.</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> <span class="variable">v</span> = <span class="number">1_u8</span>;</span><br></pre></td></tr></table></figure><p>하지만 그렇지 않은 경우도 있다. 예를 들어 가변 길이의 벡터라든지, 문자열을 필드로 가지고 있는 구조체라든지 사용자에게 입력받는 경우 런타임에 데이터가 결정되므로 메모리 크기를 컴파일 타임에 알 수 없다.</p><p>일반적으로 언어에서 이러한 경우는 힙에 데이터를 넣는 방식으로 해결한다. 보통 동적 할당한다고 표현하는데, 런타임에 메모리를 필요한 만큼 힙 메모리에 할당해 사용한다. 메모리를 관리해야 하는 상황은 이렇게 힙에 할당하는 상황이다. GC 역시 힙 영역의 메모리 관리에 대해 얘기를 한다.</p><blockquote><p><a href="/posts/go/go-gc">GC에 대해서 작성한 글</a></p></blockquote><p>Rust의 <code>String</code> 타입이 힙을 사용하면서, 아주 친근한 데이터 타입으로, Ownership을 설명하기 적합하다.</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">s</span> = <span class="type">String</span>::<span class="title function_ invoke__">from</span>(<span class="string">"hello"</span>);</span><br><span class="line">} <span class="comment">// scope 종료 이후 s와 해당 메모리는 정리된다.</span></span><br></pre></td></tr></table></figure><p>Rust는 이렇게 스코프가 종료되는 시점에 <code>drop</code> 함수를 호출하는데, 이 함수는 작성자가 메모리를 <code>free</code> 하기 위한 코드가 담겨있다.</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">MyType</span> {</span><br><span class="line"> v: <span class="type">u8</span>,</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">impl</span> <span class="title class_">Drop</span> <span class="keyword">for</span> <span class="title class_">MyType</span> {</span><br><span class="line"> <span class="keyword">fn</span> <span class="title function_">drop</span>(&<span class="keyword">mut</span> <span class="keyword">self</span>) {</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"drop my type"</span>)</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">s</span> = MyType { v: <span class="number">1</span> };</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"{}"</span>, s.v);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 1</span></span><br><span class="line"><span class="comment">// drop my type</span></span><br></pre></td></tr></table></figure><blockquote><p>이 <code>drop</code> 함수는 예시에서 보이는 것처럼 <code>Drop</code> 트레잇을 구현한 것이다. 러스트를 모르지만, 컨셉이 궁금해서 온 사람들은 <code>Drop</code>이 인터페이스라고 생각하면 될 것 같다.</p></blockquote><p>이렇게 소유권은 하나만 갖게 하면서 소유권을 가진 식별자가 스코프를 벗어날 때 Drop 하므로 GC가 필요 없다. 마치 Reference Count를 하나로 유지하는 것과 비슷하다. C 언어를 잘 알지는 못하지만, 패키지를 사용할 때 잘 만들어진 패키지의 경우 사용된 데이터 타입을 어디서 <code>free</code> 하는 책임을 갖는지 주석으로 명시한다고 들었는데, 러스트는 마치 이 방향을 컴파일 타임에 규칙으로서 명시해 문제를 해결하는 느낌이다.</p><blockquote><p>러스트처럼 변수 스코프가 종료되는 시점에 패턴을 C++에서는 <code>RAII</code>(<code>Resource Acquisition Is Initialization</code>)이라는 이름으로 부른다.</p></blockquote><p>이제 <code>String</code> 타입을 통해 소유권에 대해 조금 더 자세히 설명한다.</p><h1 id="소유권-이전"><a href="#소유권-이전" class="headerlink" title="소유권 이전"></a>소유권 이전</h1><p>본격적인 소유권 이전에 대한 설명 전에 스택 위에서 할당되는 값으로 보자. 다음과 같이 <code>y = x</code>처럼 다른 식별자에 값을 복사해 넣는 것을 비교해볼 예정이다.</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">x</span> = <span class="number">1</span>;</span><br><span class="line"><span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">y</span> = x;</span><br><span class="line">x += <span class="number">1</span>;</span><br><span class="line">y -= <span class="number">1</span>;</span><br><span class="line"><span class="built_in">println!</span>(<span class="string">"x: {} / y: {}"</span>, x, y); <span class="comment">// 2, 0</span></span><br></pre></td></tr></table></figure><p><code>x</code>는 1을 할당 받고 <code>y</code>는 <code>x</code>를 할당 받았지만, 둘 다 값이 복사되어 서로 다른 메모리에 있는 값을 보게 된다. 이는 정수형처럼 정해진 사이즈를 가진 값인 경우 할당 연산을 수행할 때 새로운 값이 스택에 복사되어 메모리를 따로 받게 되기 때문이다. 구구절절 설명했지만, 일반적으로 프로그래밍 언어에서 우리는 이러한 상황을 아주 자연스럽게 받아들일 수 있다.</p><p>이제 힙을 사용하는 경우를 확인해 보자. 힙을 사용하는 데이터 중에 일반적이라고 생각되는 자바스크립트의 코드를 살펴보면 다음과 같이 동작한다.</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> obj = { <span class="attr">data</span>: <span class="number">10</span> };</span><br><span class="line"><span class="keyword">let</span> copeid = obj;</span><br><span class="line">copied.<span class="property">data</span> = <span class="number">100</span>;</span><br><span class="line"><span class="variable language_">console</span>.<span class="title function_">log</span>(obj); <span class="comment">// { data: 100 }</span></span><br></pre></td></tr></table></figure><p>우리는 이전에 사용했던 <code>obj</code> 객체에 접근할 수도 있고, 같은 힙 메모리를 공유하며 각자의 수정 사항을 모두 동일하게 확인할 수 있다.</p><p>러스트는 아주 독특하게 동작하는데, 결과만 말하자면 힙에 있는 데이터를 가리키고 있는 식별자는 다른 식별자에 힙 포인터를 복사해 넣는 순간 해당 데이터에 대한 소유권을 이전한다. 그리고 소유권을 잃은 식별자는 더 이상 접근할 수 없다.</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> <span class="variable">s1</span> = <span class="type">String</span>::<span class="title function_ invoke__">from</span>(<span class="string">"hello"</span>);</span><br><span class="line"><span class="keyword">let</span> <span class="variable">s2</span> = s1;</span><br><span class="line"><span class="built_in">println!</span>(<span class="string">"s1: {} / s2: {}"</span>, s1, s2);</span><br><span class="line"></span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">let s1 = String::from("hello");</span></span><br><span class="line"><span class="comment"> -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait</span></span><br><span class="line"><span class="comment">let s2 = s1;</span></span><br><span class="line"><span class="comment"> -- value moved here</span></span><br><span class="line"><span class="comment">println!("s1: {} / s2: {}", s1, s2);</span></span><br><span class="line"><span class="comment"> ^^ value borrowed here after move</span></span><br><span class="line"><span class="comment">*/</span></span><br></pre></td></tr></table></figure><p>이해가 어려운 말들이 나오는데, 동작을 설명하기에 앞서 먼저 <code>String</code> 타입을 간단히 설명하자면 아래와 같이 문자열 맨 앞을 가리키는 포인터, <code>len</code>, 그리고 <code>capacity</code>를 스택에 담는 구조이다.</p><p><img src="/images/2022-11-21-rust-ownership/string-memory.svg?style=centerme" alt="문자열과 메모리"></p><blockquote><p><code>len</code>은 문자열의 길이를 뜻하고 <code>capacity</code>는 바이트로 표시한 컨텐츠의 메모리 크기를 의미한다.</p></blockquote><p>따라서 <code>s2</code> 식별자에 <code>s1</code>을 할당하는 동작은 앞서 정수로 설명한 경우와 동일하게 위 세 개 정보를 스택에 복사하는 것과 같다. 힙에 있는 데이터는 복제되지 않는다.</p><p><img src="/images/2022-11-21-rust-ownership/copied-string.svg?style=centerme" alt="스택 데이터만 복사"></p><p>우리는 앞서 Rust의 동작 중에 식별자가 스코프에 벗어나면 사용되던 값들을 모두 정리한다고 배웠는데, 위와 같은 구조에서는 문자열이 두 번(<code>s1</code>의 <code>Drop</code> && <code>s2</code>의 <code>Drop</code>) 정리되는 상황이 생긴다. 즉, 두 번 <code>free</code>를 하는 것과 같고 이는 메모리 충돌을 발생시킨다.</p><p>메모리 안전성을 위해서 러스트는 이런 상황에서 <code>s1</code>이 더 이상 유효하지 않다고 판단해버린다. 즉 말해서 러스트는<code>s1</code>이 스코프를 벗어나든 아니든 <code>Drop</code>과 관련된 로직을 수행할 필요가 없다.</p><p>다른 언어에서는 이렇게 값을 복사하는 과정에 포인터 내부의 값을 복사하지 않는 것을 <strong>얕은 복사</strong>라고 표현한다. Rust는 이 동작이 모든 데이터에 적용된다. 자동으로 깊은 복사를 수행하는 경우가 없다. 책에서 설명하는 방식으로는 Rust에서 이런 동작을 <code>move</code>라고 한다는데, 한국어로는 이전이라고 표현하면 될 것 같다.</p><blockquote><p>조금 명시적으로 말하자면 힙 포인터를 얕은 복사하는 경우 발생하는 동작이 “이전”이라고 표현할 수 있을 것 같다.</p></blockquote><h1 id="Copy-Clone"><a href="#Copy-Clone" class="headerlink" title="Copy & Clone"></a>Copy & Clone</h1><p>힙에 있는 데이터까지 복제하는 작업을 할 때는 일반적으로 <code>Clone</code>이라는 Trait을 구현한다. <code>String</code> 타입도 마찬가지로 <code>Clone</code> 타입을 구현하고 있다.</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#[cfg(not(no_global_oom_handling))]</span></span><br><span class="line"><span class="meta">#[stable(feature = <span class="string">"rust1"</span>, since = <span class="string">"1.0.0"</span>)]</span></span><br><span class="line"><span class="keyword">impl</span> <span class="title class_">Clone</span> <span class="keyword">for</span> <span class="title class_">String</span> {</span><br><span class="line"> <span class="keyword">fn</span> <span class="title function_">clone</span>(&<span class="keyword">self</span>) <span class="punctuation">-></span> <span class="keyword">Self</span> {</span><br><span class="line"> <span class="type">String</span> { vec: <span class="keyword">self</span>.vec.<span class="title function_ invoke__">clone</span>() }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">fn</span> <span class="title function_">clone_from</span>(&<span class="keyword">mut</span> <span class="keyword">self</span>, source: &<span class="keyword">Self</span>) {</span><br><span class="line"> <span class="keyword">self</span>.vec.<span class="title function_ invoke__">clone_from</span>(&source.vec);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">s1</span> = <span class="type">String</span>::<span class="title function_ invoke__">from</span>(<span class="string">"hello"</span>);</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">s2</span> = s1.<span class="title function_ invoke__">clone</span>();</span><br><span class="line"></span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"s1 = {}, s2 = {}"</span>, s1, s2);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>위 동작은 다음 그림처럼 힙 데이터 역시 복사해서 새로운 식별자인 <code>s2</code>에 담는다. <code>s1</code>의 소유권은 그대로 유지된다.</p><p><img src="/images/2022-11-21-rust-ownership/string-clone.svg?style=centerme" alt="문자열 Clone"></p><hr><p>다시 스택에 할당되는 값만 가진 이 코드로 돌아와 보자.</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">x</span> = <span class="number">1</span>;</span><br><span class="line"><span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">y</span> = x;</span><br><span class="line">x += <span class="number">1</span>;</span><br><span class="line">y -= <span class="number">1</span>;</span><br><span class="line"><span class="built_in">println!</span>(<span class="string">"x: {} / y: {}"</span>, x, y); <span class="comment">// 2, 0</span></span><br></pre></td></tr></table></figure><p>스택에 할당되는 값의 경우 할당 연산을 수행할 때 소유권 이전이 발생하지 않고 값을 복사해버린다. 여기서는 얕은 복사니 깊은 복사니 하는 것이 의미가 없다. 이렇게 소유권 이전 없이 값을 간단히 스택에서 복사해버릴 수 있는 타입들은 모두 <code>Copy</code> Trait을 구현하고 있다. <code>Copy</code> 타입은 러스트 시스템의 특별한 어노테이션으로서, 만약 이 Trait을 구현하고 있는 타입이라면 할당 연산을 할 때 소유권을 이전하지 않는다.</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">let s1 = String::from("hello");</span><br><span class="line"> -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait</span><br></pre></td></tr></table></figure><p>아까 에러 메시지를 다시 한번 보면, <code>String</code>은 <code>Copy</code> Trait을 구현하고 있지 않기 때문에 <code>move</code>가 발생한다고 설명한다.</p><p><code>Copy</code> 타입을 직접 구현하도록 할 수 있는데, 등호 연산을 오버로딩하는 느낌이 아니라 그냥 스택에서 값을 복사할 수 있는 타입의 경우 그 자격을 명시하는 정도이다. <code>Copy</code>를 구현하려면 대상 타입이 <code>Clone</code>을 구현하고, 그 타입 자체 혹은 타입을 구성하는 다른 필드들 모두가 <code>Drop</code> Trait을 구현하고 있지 않아야 한다. 즉, 타입을 구성하는 모든 필드 및 값이 스택에 할당될 수 있어야 한다는 것을 의미한다.</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">impl</span> <span class="title class_">Copy</span> <span class="keyword">for</span> <span class="title class_">MyType</span> {}</span><br><span class="line"><span class="keyword">impl</span> <span class="title class_">Clone</span> <span class="keyword">for</span> <span class="title class_">MyType</span> {</span><br><span class="line"> <span class="keyword">fn</span> <span class="title function_">clone</span>(&<span class="keyword">self</span>) <span class="punctuation">-></span> <span class="keyword">Self</span> {</span><br><span class="line"> *<span class="keyword">self</span></span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// or</span></span><br><span class="line"><span class="meta">#[derive(Copy, Clone)]</span></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">MyStruct</span>;</span><br></pre></td></tr></table></figure><blockquote><p><code>Copy</code> Trait은 위 코드처럼, 구현해야 하는 메소드가 없다. 이는 러스트가 의도적으로 오버로딩을 구현하지 못하도록 막은 것이고, 이를 통해 임의 코드가 런타임에서 실행되지 않도록 막는다.</p></blockquote><blockquote><p><code>Copy</code>가 구현될 수 있는 규칙을 설명할 때 책에서는 위와 같이 <strong>Clone + Not Drop</strong>으로 설명하지만, 코드 주석에서는 필드가 모두 Copy를 구현해야 한다고 설명한다. 즉, 기본 타입들은 모두 <strong>Not Drop</strong>이라면 <strong>Copy</strong>를 구현하고 있는 것 같다.</p></blockquote><blockquote><p>위 코드에서 <code>Copy</code>를 구현하는 두 가지 방법은 미묘한 차이가 있는데, 이번 글의 범위를 벗어난다. 궁금하다면 <a href="https://doc.rust-lang.org/book/appendix-03-derivable-traits.html#appendix-c-derivable-traits">Derivable Traits 문서</a>와 <a href="https://github.com/rust-lang/rust/blob/e702534763599db252f2ca308739ec340d0933de/library/core/src/marker.rs#L216-L393">Rust Copy 코드의 주석</a>을 보자.</p></blockquote><p><code>Copy</code> Trait을 직접 구현해야 하는 일은 거의 드물다. <code>Copy</code>가 구현되어 있다면 최적화가 되어있고 <code>clone</code> 메소드가 아니라 할당 연산자를 사용할 수 있음을 의미하므로 코드가 더 간결해질 수는 있다. <code>Copy</code>를 구현하고 있는 기본 타입들은 다음과 같다.</p><ul><li>모든 정수형 타입들 (<code>u32</code>, <code>u16</code>, …)</li><li>Boolean 타입</li><li>부동소수 타입 (<code>f64</code>, …)</li><li>모든 캐릭터 타입 (<code>char</code>)</li><li><code>Copy</code> 구현체들을 담고 있는 튜플 (<code>(i32, i32)</code>)</li></ul><h1 id="함수와-Ownership"><a href="#함수와-Ownership" class="headerlink" title="함수와 Ownership"></a>함수와 Ownership</h1><p>함수 파라미터로 값을 넘기는 것이나 리턴 값으로 넘기는 것 모두 할당 연산과 비슷한 동작을 한다. 할당과 마찬가지로 값을 복사하기 때문에 파라미터로 <code>Copy</code>를 구현하지 않은 값을 넣거나, 리턴하는 경우 소유권 이전이 발생한다.</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">struct</span> <span class="title class_">MyType</span> {</span><br><span class="line"> v: <span class="type">u8</span>,</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">impl</span> <span class="title class_">MyType</span> {</span><br><span class="line"> <span class="keyword">fn</span> <span class="title function_">from</span>(v: <span class="type">u8</span>) <span class="punctuation">-></span> <span class="keyword">Self</span> {</span><br><span class="line"> MyType { v }</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">impl</span> <span class="title class_">Drop</span> <span class="keyword">for</span> <span class="title class_">MyType</span> {</span><br><span class="line"> <span class="keyword">fn</span> <span class="title function_">drop</span>(&<span class="keyword">mut</span> <span class="keyword">self</span>) {</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"drop my type"</span>)</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">s</span> = MyType::<span class="title function_ invoke__">from</span>(<span class="number">1</span>);</span><br><span class="line"> <span class="title function_ invoke__">take_ownership</span>(s);</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"finish"</span>);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">take_ownership</span>(param: MyType) {</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"{}"</span>, param.v)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// 1</span></span><br><span class="line"><span class="comment">// drop my type</span></span><br><span class="line"><span class="comment">// finish</span></span><br></pre></td></tr></table></figure><p><code>s</code>는 <code>take_ownership</code> 함수에 넘겨질 때 소유권 이전이 발생한다. 따라서 <code>take_ownership</code> 이후로는 접근이 불가능하다. <code>take_ownership</code> 함수가 끝날 때 해당 변수의 스코프가 종료되므로 <code>drop</code> 함수를 수행한다. 리턴하는 값이 <code>Drop</code>을 구현하고 있는 경우도 마찬가지로 리턴 값에 대한 소유권이 할당받는 식별자에게 넘어가게 된다. 따라서 같은 스코프에 변경된 값을 유지하고 싶으면 다시 함수에서 바깥으로 소유권을 전달해야 한다.</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">s</span> = <span class="type">String</span>::<span class="title function_ invoke__">from</span>(<span class="string">"hello"</span>);</span><br><span class="line"> <span class="keyword">let</span> (s, slen) = <span class="title function_ invoke__">calculate_length</span>(s);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">calculate_length</span>(s: <span class="type">String</span>) <span class="punctuation">-></span> (<span class="type">String</span>, <span class="type">usize</span>) {</span><br><span class="line"> (s, s.<span class="title function_ invoke__">len</span>())</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>이러한 동작이 사실 매우 귀찮기 때문에 러스트에서는 값은 참조하지만, 소유권은 넘겨주지 않는 방법으로 함수를 사용할 수 있도록 했다.</p><h1 id="참조와-소유권-대여"><a href="#참조와-소유권-대여" class="headerlink" title="참조와 소유권 대여"></a>참조와 소유권 대여</h1><p>위와 같은 상황에서 소유권을 넘기지 않으려면 <strong>참조</strong>(<strong>Reference</strong>)를 넘긴다. 원본 데이터에 접근하지 않고 스택의 데이터를 참조하는 자료형으로 파라미터에 전달된다.</p><p><img src="/images/2022-11-21-rust-ownership/reference.svg?style=centerme" alt="참조형"><br>참조형으로 전달된 값은 기본적으로 불변 자료형이고, 참조하는 식별자이기 때문에 식별자 스코프가 종료되어도 <code>Drop</code>을 수행하지 않는다.</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">s</span> = <span class="type">String</span>::<span class="title function_ invoke__">from</span>(<span class="string">"hello"</span>);</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">len</span> = <span class="title function_ invoke__">calculate_length</span>(&s);</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"{} length: {}"</span>, s, len); <span class="comment">// hello length: 5</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">calculate_length</span>(s: &<span class="type">String</span>) <span class="punctuation">-></span> <span class="type">usize</span> {</span><br><span class="line"> s.<span class="title function_ invoke__">len</span>()</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>위 코드처럼, <code>s</code> 식별자를 다른 함수에 참조형으로 전달하게 되면 해당 함수 이후에도 <code>s</code>를 사용할 수 있고, 해당 값을 유지하기 위한 리턴도 필요 없게 된다. 다만 다음과 같이 값을 변경하려고 하는 경우는 컴파일 에러가 발생한다.</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">change</span>(s: &<span class="type">String</span>) {</span><br><span class="line"> s.<span class="title function_ invoke__">push_str</span>(<span class="string">", world"</span>);</span><br><span class="line"> <span class="comment">// compile error</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>위에서 짧게 언급한 것처럼 기본적으로 참조형이 <strong>불변형</strong>(<strong>Immutable Reference</strong>)이기 때문이다.</p><blockquote><p>원래 러스트는 기본적으로 값을 불변형으로 선언한다. <code>let</code>으로 선언된 모든 식별자는 불변 식별자이다. 만약 값을 바꾸려면 <code>mut</code> 키워드를 식별자 앞에 선언해서 해당 식별자가 가변적임을 컴파일러에 알려야 한다.</p></blockquote><p>참조한 변수를 <strong>가변적</strong>(<strong>Mutable Reference</strong>)으로 사용하려면 다른 변수들처럼 <code>mut</code> 키워드를 사용한다.</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="comment">// 여기도 mutable 한 변수가 되도록 `mut` 키워드를 사용한다.</span></span><br><span class="line"> <span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">s</span> = <span class="type">String</span>::<span class="title function_ invoke__">from</span>(<span class="string">"hello"</span>);</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">len</span> = <span class="title function_ invoke__">calculate_length</span>(&s);</span><br><span class="line"> <span class="title function_ invoke__">change</span>(&<span class="keyword">mut</span> s);</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"{} length: {}"</span>, s, len); <span class="comment">// hello, world length: 5</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">change</span>(s: &<span class="keyword">mut</span> <span class="type">String</span>) {</span><br><span class="line"> s.<span class="title function_ invoke__">push_str</span>(<span class="string">", world"</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><hr><p>러스트는 참조형을 사용할 때 두 가지 안전 장치가 있다.</p><ul><li>Data Race를 방지하기 위한 특징</li><li>쓰레깃값을 만들지 않기 위한 특징</li></ul><h2 id="Mutable-Reference-Data-Race-방지"><a href="#Mutable-Reference-Data-Race-방지" class="headerlink" title="Mutable Reference Data Race 방지"></a>Mutable Reference Data Race 방지</h2><p>일단 Data Race는 다음과 같은 상황을 얘기한다.</p><ul><li>두 개 이상의 포인터가 같은 데이터에 접근 가능</li><li>최소 하나의 포인터가 데이터 쓰기가 가능</li><li>데이터 접근을 동기화하는 메커니즘 부재</li></ul><p>위 세 개의 상황이 동시에 발생하고 있을 때 Data Race 상태라고 볼 수 있다. 동시성 문제의 Race Condition과 유사한데, 데이터 변경과 읽기 순서에 따라 결과가 달라질 수 있는 상황이다.</p><p><img src="/images/2022-11-21-rust-ownership/datarace.png?style=centerme" alt="Data Race"></p><p>이 문제는 미묘한 상황과 복잡한 코드에 의해 런타임에 찾기가 어려운 경우가 많지만, 러스트는 뚱뚱한 컴파일러를 지향하는 언어답게 사전에 컴파일 단계에서 이를 방지해준다.</p><p>방지하는 방법은 Reference를 전달할 때 Shared Lock, Exclusive Lock을 거는 것처럼 동작을 제한한다. Mutable Reference의 스코프를 벗어나기 전까지 Exclusive Lock처럼 다른 Reference가 걸리는 것을 막는다. 읽기 전용 Reference는 Shared Lock처럼 다른 읽기 전용 Reference가 걸리는 것은 막지 않는다. 하지만 Mutable Reference는 동시에 사용될 수 없다.</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">s</span> = <span class="type">String</span>::<span class="title function_ invoke__">from</span>(<span class="string">"hello"</span>);</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">ref1</span> = &<span class="keyword">mut</span> s;</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">ref2</span> = &<span class="keyword">mut</span> s; <span class="comment">// compile error</span></span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"{} {}"</span>, ref1, ref2)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>다음과 같이 Mutable Reference를 동시에 Borrow 해줄 수 없다는 에러가 나온다. Immutable Reference가 먼저 있어도 비슷한 컴파일 에러가 발생한다.</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">error[E0499]: cannot borrow `s` as mutable more than once at a time</span><br><span class="line"> --> src/main.rs:27:20</span><br><span class="line"> |</span><br><span class="line">26 | let ref1 = &mut s;</span><br><span class="line"> | ------ first mutable borrow occurs here</span><br><span class="line">27 | let ref2 = &mut s;</span><br><span class="line"> | ^^^^^^ second mutable borrow occurs here</span><br><span class="line">28 | println!("{} {}", ref1, ref2)</span><br><span class="line"> | ---- first borrow later used here</span><br></pre></td></tr></table></figure><p>참조형의 스코프는 마지막으로 사용된 시점까지 유지되므로 다음과 같은 경우는 문제없이 코드가 동작한다.</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="keyword">mut </span><span class="variable">s</span> = <span class="type">String</span>::<span class="title function_ invoke__">from</span>(<span class="string">"hello"</span>);</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">ref1</span> = &s;</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"{}"</span>, ref1); <span class="comment">// hello // 컴파일러는 여기서 `ref1` 스코프가 종료된다고 판단한다.</span></span><br><span class="line"> <span class="keyword">let</span> <span class="variable">ref2</span> = &<span class="keyword">mut</span> s;</span><br><span class="line"> <span class="title function_ invoke__">change</span>(ref2);</span><br><span class="line"> <span class="built_in">println!</span>(<span class="string">"{}"</span>, ref2); <span class="comment">// hello, world</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="Dangling-Reference"><a href="#Dangling-Reference" class="headerlink" title="Dangling Reference"></a>Dangling Reference</h2><p>Dangling Reference는 <code>free</code> 된 레퍼런스를 식별자로 가지고 있는 경우를 말한다. 러스트에서는 이를 컴파일러가 절대 Dangling Reference를 가지고 있지 않도록 보장해준다. 만약 어떤 데이터가 참조되고 있다면 해당 참조 식별자의 스코프가 종료되기 전까지 데이터의 스코프가 끝나지 않도록 해야 한다.</p><figure class="highlight rust"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">fn</span> <span class="title function_">main</span>() {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">reference_to_nothing</span> = <span class="title function_ invoke__">dangle</span>();</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">fn</span> <span class="title function_">dangle</span>() <span class="punctuation">-></span> &<span class="type">String</span> {</span><br><span class="line"> <span class="keyword">let</span> <span class="variable">s</span> = <span class="type">String</span>::<span class="title function_ invoke__">from</span>(<span class="string">"hello"</span>);</span><br><span class="line"> &s</span><br><span class="line">}</span><br></pre></td></tr></table></figure><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">$ cargo run</span><br><span class="line"> Compiling ownership v0.1.0 (file:///projects/ownership)</span><br><span class="line">error[E0106]: missing lifetime specifier</span><br><span class="line"> --> src/main.rs:5:16</span><br><span class="line"> |</span><br><span class="line">5 | fn dangle() -> &String {</span><br><span class="line"> | ^ expected named lifetime parameter</span><br><span class="line"> |</span><br><span class="line"> = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from</span><br><span class="line">help: consider using the `'static` lifetime</span><br><span class="line"> |</span><br><span class="line">5 | fn dangle() -> &'static String {</span><br><span class="line"> | ~~~~~~~~</span><br><span class="line"></span><br><span class="line">For more information about this error, try `rustc --explain E0106`.</span><br><span class="line">error: could not compile `ownership` due to previous error</span><br></pre></td></tr></table></figure><p>라이프타임 어노테이션에 대해서는 이 글에서 다루지 않는다. 자세한 내용은 <a href="https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#the-borrow-checker">이 링크</a>에서 확인하면 좋을 것 같다.</p><h1 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h1><ul><li><a href="https://doc.rust-lang.org/book/appendix-03-derivable-traits.html">https://doc.rust-lang.org/book/appendix-03-derivable-traits.html</a></li><li><a href="https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html">https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html</a></li></ul>]]></content:encoded>
<category domain="https://changhoi.kim/categories/rust/">rust</category>
<category domain="https://changhoi.kim/tags/programming/">programming</category>
<comments>https://changhoi.kim/posts/rust/rust-ownership/#disqus_thread</comments>
</item>
<item>
<title>etcd deep dive - Data Model</title>
<link>https://changhoi.kim/posts/database/etcd-data-model/</link>
<guid>https://changhoi.kim/posts/database/etcd-data-model/</guid>
<pubDate>Wed, 02 Nov 2022 15:00:00 GMT</pubDate>
<description><p>etcd 공식 페이지에 가보면 “A distributed, reliable key-value store for the most critical data of a distributed system”라고 설명하고 있다. ZooKeeper와 유사하지만 gRPC를 베이스로 하는 현대적인 코디네이터 역할을 한다. 메타 데이터를 담기 위한 Key-Value 저장소로 사용이 되는 편이고 가장 유명한 활용처는 쿠버네티스가 아닐까 싶다. 최근 사용할 일이 생기고 있어서 깊게 공부해보려고 하나씩 파헤치고 있다. 첫 글은 etcd의 데이터 모델이다.</p></description>
<content:encoded><![CDATA[<p>etcd 공식 페이지에 가보면 “A distributed, reliable key-value store for the most critical data of a distributed system”라고 설명하고 있다. ZooKeeper와 유사하지만 gRPC를 베이스로 하는 현대적인 코디네이터 역할을 한다. 메타 데이터를 담기 위한 Key-Value 저장소로 사용이 되는 편이고 가장 유명한 활용처는 쿠버네티스가 아닐까 싶다. 최근 사용할 일이 생기고 있어서 깊게 공부해보려고 하나씩 파헤치고 있다. 첫 글은 etcd의 데이터 모델이다.</p><span id="more"></span><p>etcd는 고맙게도 공식 페이지나 CNCF 발표 영상 등에서 구체적인 디자인들에 대해 여러 방법으로 알려주고 있다. 이번 글은 etcd의 공식 문서의 <a href="https://etcd.io/docs/v3.5/learning/">Learning</a> 섹션에서 제공해주는 etcd data model을 정리한 글이다.</p><hr><h1 id="Overview"><a href="#Overview" class="headerlink" title="Overview"></a>Overview</h1><p>etcd는 멀티 버전 Key-Value 스토리지이다. 즉, 이전 버전의 키값 쌍을 새 값으로 대체하기 전까지 보존한다. etcd는 여러 버전의 데이터를 효율적이고 불변 데이터 형태로 관리한다. 불변 데이터라고 하면 In-Place 형태로 디스크의 데이터를 업데이트하지 않고 아니라 항상 새로운 버전을 만드는 것을 말한다. In-Place 방식보다 공간 측면에서는 덜 효율적일 것 같긴 한데 “효율적”이라는 키워드를 사용한 이유는 이후 후술한다. 아무튼 이렇게 과거 버전도 모두 가지고 있게 되므로 특정 키에 대한 과거 버전 데이터들은 나중에도 접근이 가능하고 Watchable 하다. 한편으로는 과거 버전이 무한히 늘어나는 것을 막기 위해 Compaction을 진행하기도 한다.</p><h1 id="Logical-View"><a href="#Logical-View" class="headerlink" title="Logical View"></a>Logical View</h1><p>etcd를 논리적인 측면에서 봤을 때 간단히 말하자면 <strong>바이너리 키 공간</strong>이다. 키는 복수의 <strong>Revision</strong>(<strong>리비전</strong>)을 가지고 있을 수 있다. 리비전은 스토리지의 트랜잭션 번호라고 생각해도 된다. 정수 형태이며 스토어가 세팅되면 초기 리비전 값은 1부터 시작하게 된다. Atomic 한 요청 단위(트랜잭션)이 수행될 때마다 새로운 리비전으로 값을 쓰게 된다. 즉, 트랜잭션마다 리비전이 단조 증가하게 된다. 과거의 리비전 역시 스토리지 내에서 일정 기간 보유하고 있어서 실제 쿼리를 통해 접근이 가능하며, 리비전 정보 역시 인덱싱되어 있어서 특정 리비전을 기준으로 쿼리하거나 Watch 하는 것도 빠르게 처리할 수 있다. 만약 저장소가 압축을 수행하면 압축 이전의 리비전들은 모두 삭제되며 쿼리할 수 없게 된다.</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">$ etcdctl compact 5</span><br><span class="line">compacted revision 5</span><br><span class="line"></span><br><span class="line"><span class="comment"># 수동으로 리비전을 압축하면 그 이후 리비전을 기준으로 쿼리할 때 찾을 수 없다는 rpc 에러를 만난다.</span></span><br><span class="line">$ etcdctl get --rev=4 foo</span><br><span class="line">Error: rpc error: code = 11 desc = etcdserver: mvcc: required revision has been compacted</span><br></pre></td></tr></table></figure><blockquote><p>그러면 리비전 Overflow가 생길 수 있는 건가? 라고 생각했는데, 역시나 이런 생각을 한 사람이 있었고 <a href="https://github.com/etcd-io/etcd/issues/11187">이슈</a>에서 찾아볼 수 있었다. 그러나 그러한 걱정은 하덜 말라는 답변. 초당 2만 번씩 53억 년 동안 Mutation이 발생해야 오버플로우가 발생한다고 한다.</p></blockquote><p>etcd는 단순히 <code>put</code>과 <code>delete</code>로 스토리지 안의 키를 변경한다. 키에 대한 값은 리비전에 대한 정보뿐 아니라 키의 변경 버전을 기록한다. Protocol Buffer 메시지 타입은 다음과 같이 생겼다.</p><figure class="highlight protobuf"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">message </span><span class="title class_">KeyValue</span> {</span><br><span class="line"> <span class="type">bytes</span> key = <span class="number">1</span>;</span><br><span class="line"> <span class="type">int64</span> create_revision = <span class="number">2</span>;</span><br><span class="line"> <span class="type">int64</span> mod_revision = <span class="number">3</span>;</span><br><span class="line"> <span class="type">int64</span> version = <span class="number">4</span>;</span><br><span class="line"> <span class="type">bytes</span> value = <span class="number">5</span>;</span><br><span class="line"> <span class="type">int64</span> lease = <span class="number">6</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>키를 생성하면 키의 버전이 증가하는 것과 동일하다. 만약 기존에 해당 키가 존재하지 않았다면 1부터 시작하게 하는 것이고 기존에 해당 키로 이미 값이 존재한다면 그 키에 대해 버전을 올리는 것과 같다. 키를 삭제하는 것은 해당 키에 대한 <strong>Tombstone</strong>을 만들고 해당 키의 버전을 0으로 만드는 것과 같다. 압축이 발생하면 압축 이전의 모든 세대가 제거되고 가장 최근 버전만 남게 된다.</p><p><img src="/images/2022-11-03-etcd-data-model/revision-and-version.png?style=centerme" alt="리비전과 버전"></p><blockquote><p>글에서는 생명 주기로 표현하는데, 굳이 생명 주기라고 할만한 건 없는 것 같다. 그냥 키가 리비전을 통해 여러 버전을 가지고 있을 수 있고 해당 변경 사항들이 불변 데이터처럼 쌓인다는 게 본질적인 내용이다.</p></blockquote><blockquote><p><strong>Tombstone</strong>은 Append Only Log 같은 데이터에서 삭제 표기를 하기 위해 사용하는 데이터이다.</p></blockquote><h1 id="Physical-View"><a href="#Physical-View" class="headerlink" title="Physical View"></a>Physical View</h1><p>실제 구현 레벨에서 etcd는 크게 두 가지 레이어를 통해 스토리지를 구성한다.</p><ul><li>Persistent B+tree</li><li>In-memory B Tree</li></ul><h2 id="Persistent-B-Tree"><a href="#Persistent-B-Tree" class="headerlink" title="Persistent B+Tree"></a>Persistent B+Tree</h2><p>영구 저장소로 B+Tree를 사용하고 있다. B+Tree에 키를 저장할 때 다음과 같이 3개의 튜플로 키를 만들어준다.</p><ul><li><strong>Major</strong>: 해당 키를 들고 있는 Revision을 의미한다.</li><li><strong>Sub</strong>: 같은 리비전 내의 다른 키들과 구분을 위해 사용된다. (아마 개발자가 설정하는 값이 아닐까)</li><li><strong>Type</strong>: 특수한 값을 위한 Optional 한 값이다. (ex. tombstone)</li></ul><p>키에 대한 값은 효율성을 위해 이전 리비전과의 차이(<code>delta</code>)만 담고 있다. B+Tree는 키를 단순 Byte Order 로 정렬한다. 따라서 특정 리비전으로부터 다른 리비전의 수정 사항을 빠르게 찾을 수 있다. 컴팩션이 발생하면 Outdated Key-Value 쌍은 삭제되는 구조이다.</p><blockquote><p>Compaction은 위에서처럼 수동으로 수행할 수도 있지만 특정 시점마다 컴팩션을 수행하도록 할 수도 있다. Compaction이 키가 사용하는 스토리지 공간을 줄여주는 것은 맞지만, 데이터를 쓰고 지우면서 생기는 단편화 문제를 해결해주지는 않는다. 단편화를 해결하려면 defragmentation(<code>defrag</code> 명령어)을 해줘야 한다. 이 글에서 해당 내용을 깊게 다루지는 않으므로 <a href="https://etcd.io/docs/v3.5/op-guide/maintenance/">문서</a>를 참고하면 좋을 것 같다.</p></blockquote><h2 id="In-memory-B-Tree"><a href="#In-memory-B-Tree" class="headerlink" title="In-memory B Tree"></a>In-memory B Tree</h2><p>etcd는 일반적인 데이터베이스들처럼 캐시 레이어를 가지고 있는데, 이 부분이 In-memory B Tree이다. B Tree 인덱스의 키들이 실제 유저들(개발자)에게 노출되는 키이다. 해당 B Tree의 값으로는 키에 대한 Persistent B+Tree의 수정 내역을 가리키는 포인터를 들고 있다. Compaction이 발생하면 삭제되었던 키들(dead pointers)을 삭제하게 된다.</p><h1 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h1><ul><li><a href="https://etcd.io/docs/v3.5/learning/data_model/">https://etcd.io/docs/v3.5/learning/data_model/</a></li><li><a href="https://etcd.io/docs/v3.5/op-guide/maintenance/">https://etcd.io/docs/v3.5/op-guide/maintenance/</a></li></ul>]]></content:encoded>
<category domain="https://changhoi.kim/categories/database/">database</category>
<category domain="https://changhoi.kim/tags/distributed-system/">distributed_system</category>
<category domain="https://changhoi.kim/tags/etcd/">etcd</category>
<comments>https://changhoi.kim/posts/database/etcd-data-model/#disqus_thread</comments>
</item>
<item>
<title>코드로 인프라 관리하기</title>
<link>https://changhoi.kim/posts/books/infrastructure-as-code/</link>
<guid>https://changhoi.kim/posts/books/infrastructure-as-code/</guid>
<pubDate>Thu, 29 Sep 2022 15:00:00 GMT</pubDate>
<description><p>이번년도에도 한빛 미디어의 <strong>나는 리뷰어다</strong>에 선정되어 매달 책 한 권씩을 읽을 수 있게 됐다. 9월에는 Infrastructure as Code 책을 받아서 보게 되었다. 이 글은 해당 책에 대한 간단한 리뷰이다.</p></description>
<content:encoded><![CDATA[<p>이번년도에도 한빛 미디어의 <strong>나는 리뷰어다</strong>에 선정되어 매달 책 한 권씩을 읽을 수 있게 됐다. 9월에는 Infrastructure as Code 책을 받아서 보게 되었다. 이 글은 해당 책에 대한 간단한 리뷰이다.</p><span id="more"></span><p>이 책은 IaC의 아주 폭넓은 관점으로의 학습을 도와주는 책이다. 특정 프레임워크를 가르쳐 주는 느낌은 아니다. 마치 데이터베이스 인터널이라는 책처럼 특정 데이터베이스에 대해 한정된 얘기가 아니라 IaC 라는 것 자체에 대해 가르쳐준다. 장점은 개념 적립과 코어한 컨셉에 대해 학습하기 좋다는 건데, 단점은 이것만으로 IaC를 경험하기는 어려울 것 같다. 이전에 Terraform 이라든지 CloudFormation 등을 사용한 경험이 있는 사람들이 읽어보면 기존 지식과 맞물려 시너지가 있을 것 같다. 책 내용 중에 제일 좋았던 건 인프라 스택을 설계하는 방법에 대해서도 학습할 수 있었다는 점이다. 보통 IaC 관련된 책을 봤을 때는 어떻게 기술을 써야 하는지를 학습하는데 이 책에서는 어떻게 구조를 나눠야 할지 어떤 패턴으로 작성할 수 있는지를 설명해준다. IaC에 대해 어느 정도 지식이 있고 IaC를 더 나은 방식으로 사용하길 원하는 개발자들에게 추천해주고 싶은 책이다.</p>]]></content:encoded>
<category domain="https://changhoi.kim/categories/books/">books</category>
<category domain="https://changhoi.kim/tags/review/">review</category>
<comments>https://changhoi.kim/posts/books/infrastructure-as-code/#disqus_thread</comments>
</item>
<item>
<title>클라우드 네이티브 애플리케이션 디자인 패턴</title>
<link>https://changhoi.kim/posts/books/design-pattern-for-cloud-native-application-review/</link>
<guid>https://changhoi.kim/posts/books/design-pattern-for-cloud-native-application-review/</guid>
<pubDate>Sat, 23 Jul 2022 15:00:00 GMT</pubDate>
<description><p>이번년도에도 한빛 미디어의 <strong>나는 리뷰어다</strong>에 선정되어 매달 책 한 권씩을 읽을 수 있게 됐다. 7월 미션으로 나온 책 중에 하나인 <strong>클라우드 네이티브 애플리케이션 디자인 패턴</strong>을 받게 됐고, 이번 달에 읽어보게 됐다. 이 글은 이 책에 대한 간단한 리뷰이다.</p></description>
<content:encoded><![CDATA[<p>이번년도에도 한빛 미디어의 <strong>나는 리뷰어다</strong>에 선정되어 매달 책 한 권씩을 읽을 수 있게 됐다. 7월 미션으로 나온 책 중에 하나인 <strong>클라우드 네이티브 애플리케이션 디자인 패턴</strong>을 받게 됐고, 이번 달에 읽어보게 됐다. 이 글은 이 책에 대한 간단한 리뷰이다.</p><span id="more"></span><p>내가 개발을 시작할 시점부터 이미 클라우드가 활발히 도입되어 있었고, 실제로 내가 개발을 하는 공간 역시 모두 AWS 위에서만 개발했지만, 무엇이 클라우드 네이티브적 특성을 갖는지 알 수 없었다. 이 책을 읽으면서도 뭔가 이게 클라우드와 연관된 것인지 그냥 MSA를 구성하기 위한 여러 방법들을 나열한 것인가 싶었다. 간간히 클라우드 컴포넌트에 대한 명시적인 언급이 있긴 했지만, 보다 어울리는 제목은 MSA 디자인 패턴이 아니었을까 싶다.</p><p>그런 관점에서 이 책은 지식을 넓히기에 적합했다. 굉장히 얇은 느낌으로 이해하고 있던 부분들이 이번 기회에 아주 명확한 표현으로 이해할 수 있었던 것 같다. 현재 실제 업무로 사용하고 있는 사이드카 패턴, BFF, Event Driven 등 개념을 적립하게 도움이 많이 된다. 이 책의 유일한 단점은 책 이름이 책의 일부분에 치중한 느낌이라는 점이고 (이것은 내가 잘 모르기 때문에 이렇게 느끼는 것일 수도 있다고 생각한다.) 특정 규모 이상의 서비스를 만드는 팀에 합류했는데 그곳에서 사용되는 인프라 아키텍처에 대해 생소한 감이 드는 개발자들에게 추천해주고 싶은 책이다.</p>]]></content:encoded>
<category domain="https://changhoi.kim/categories/books/">books</category>
<category domain="https://changhoi.kim/tags/review/">review</category>
<comments>https://changhoi.kim/posts/books/design-pattern-for-cloud-native-application-review/#disqus_thread</comments>
</item>
<item>
<title>Go Scheduler</title>
<link>https://changhoi.kim/posts/go/go-scheduler/</link>
<guid>https://changhoi.kim/posts/go/go-scheduler/</guid>
<pubDate>Tue, 24 May 2022 15:00:00 GMT</pubDate>
<description><p>Go는 많은 것들을 “알아서” 해준다. 그래서 Go는 빌드 또는 실행 옵션이 다른 언어에 비해서 적은 편이다. Go의 가장 핵심적인 부분이라고 할 수 있는 고루틴 역시 Go의 런타임에서 알아서 관리해주고 있다. Go를 사용하면서 Go의 스케줄러를 알고 있어야만 하는 경우는 많지 않지만, 더 잘 쓰기 위해 조금 디테일한 런타임 동작에 대해 알아보자.</p></description>
<content:encoded><![CDATA[<p>Go는 많은 것들을 “알아서” 해준다. 그래서 Go는 빌드 또는 실행 옵션이 다른 언어에 비해서 적은 편이다. Go의 가장 핵심적인 부분이라고 할 수 있는 고루틴 역시 Go의 런타임에서 알아서 관리해주고 있다. Go를 사용하면서 Go의 스케줄러를 알고 있어야만 하는 경우는 많지 않지만, 더 잘 쓰기 위해 조금 디테일한 런타임 동작에 대해 알아보자.</p><span id="more"></span><h1 id="Go-Runtime-Scheduler"><a href="#Go-Runtime-Scheduler" class="headerlink" title="Go Runtime Scheduler"></a>Go Runtime Scheduler</h1><p>고루틴은 런타임 스케줄러에 의해 관리된다. 아래 원칙을 기준으로 고루틴을 적절히 스케줄링 한다.</p><ul><li>OS Thread는 비싸기 때문에 되도록 적은 수를 유지한다.</li><li>많은 수의 고루틴을 실행해 높은 동시성을 유지한다.</li><li>N 코어의 머신에서 N개의 고루틴이 병렬적으로 동작할 수 있게 한다.</li></ul><p>스케줄러가 동작하는 4가지 이벤트가 있다. 이 이벤트를 마주하면 스케줄러가 동작할 기회를 얻게 된다.</p><ul><li><code>go</code> 키워드를 사용해 새로운 고루틴을 만들고자 할 때</li><li>GC가 동작할 때</li><li>시스템 콜을 사용할 때</li><li>동기화 코드(mutex, atomic, channel)가 동작할 때</li></ul><blockquote><p>GC는 일정 시간마다 트리깅 되도록 되어있기도 하고, 힙 영역을 할당할 때 특정 값을 넘어섰는지 확인하면서 필요한 경우 GC를 트리거 할 수 있다. <a href="https://changhoi.github.io/posts/go/go-gc">링크</a></p></blockquote><h1 id="Goroutine이-관리되는-방식"><a href="#Goroutine이-관리되는-방식" class="headerlink" title="Goroutine이 관리되는 방식"></a>Goroutine이 관리되는 방식</h1><p>Go는 <code>G</code>, <code>M</code>, <code>P</code> 구조체를 가지고 M:N 스레딩 모델을 구현하고 있다. 각각은 다음 의미를 갖고 있다.</p><ul><li>G: Goroutine</li><li>M: Machine (OS Thread)</li><li>P: Processor (고루틴을 동작시키는 가상 프로세서)</li></ul><p>P가 G, M 사이에서 스케줄링 역할을 담당하고 OS Thread가 코드를 동작할 수 있도록 한다. 보통 아래와 같은 이미지로 표현된다.</p><p><img src="/images/2022-05-25-go-scheduler/GMP.png?style=centerme" alt="G, M, P 구조체"></p><h2 id="Goroutine의-상태"><a href="#Goroutine의-상태" class="headerlink" title="Goroutine의 상태"></a>Goroutine의 상태</h2><p>고루틴의 상태는 크게 세 가지로 나눠진다.</p><ol><li><strong>Waiting</strong>: 이벤트 대기 상태. 시스템 콜, 동기화 콜(atomic, mutext, channel)에 의한 정지 상태.</li><li><strong>Runnable</strong>: 실행할 수 있는 상태. M 위에서 돌아가길 원하는 상태이다.</li><li><strong>Executing</strong>: 실행 중 상태. G가 P와 M과 붙어있는 상태를 의미한다.</li></ol><p>위 그림을 확인해보면 Local RunQueue 안에 들어가 있는 고루틴이 <code>Runnable</code> 상태, M과 연결된 고루틴이 <code>Executing</code> 상태라고 볼 수 있다.</p><h2 id="OS-스레드는-필요할-때-만들고-재사용을-위해-남겨둔다"><a href="#OS-스레드는-필요할-때-만들고-재사용을-위해-남겨둔다" class="headerlink" title="OS 스레드는 필요할 때 만들고, 재사용을 위해 남겨둔다."></a>OS 스레드는 필요할 때 만들고, 재사용을 위해 남겨둔다.</h2><p>스케줄러의 목적에 맞게 OS Thread는 최소로 유지된다. 다만 N개의 코어에서 최대 병렬 실행을 위한 수만큼은 생성된다. 그리고 만든 스레드는 스레드 종료 시스템 콜(<code>pthread_exit</code>)을 수행하지 않기 위해 유휴 상태로 남겨둔다. 이를 <code>thread parking</code>이라고 한다. 이렇게 유지되는 스레드를 활용해 빠르게 고루틴을 스레드에 스케줄링할 수 있다.</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//1. main-goroutine 실행</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> {</span><br><span class="line"> ...</span><br><span class="line"> <span class="keyword">go</span> g1() <span class="comment">//2. g1-goroutine 실행</span></span><br><span class="line"> ...</span><br><span class="line"></span><br><span class="line"> <span class="keyword">go</span> g2() <span class="comment">//3. g1이 완료되고 나서 g2-goroutine 실행되었다고 가정</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>위 코드는 아래와 같이 동작하게 된다.</p><ol><li>메인 고루틴을 제외하고는 다른 고루틴이 없는 상태이므로, 현재 OS 스레드 상태는 <code>m-main</code> 한 개</li><li><code>g1</code> 고루틴을 생성 후 RunQueue에 담는다.<ol><li>런타임은 <code>g1</code>을 실행할 OS Thread인 <code>m1</code> 스레드를 만든다.</li><li>P는 RunQueue에 있는 <code>g1</code>을 <code>m1</code>과 붙여준다.</li><li><code>m1</code>은 <code>g1</code> 프로세스가 종료되더라도 사라지지 않고 Parking(idle) 상태가 된다.</li></ol></li><li>새로 <code>g2</code> 고루틴이 RunQueue에 올라간다. (이 시점에서 <code>g1</code>은 종료되었다고 가정한다. 만약 종료되지 않았다면 <code>m2</code>를 생성하고 붙여주는 위와 동일한 작업을 수행함.)<ol><li>런타임은 Parking 상태인 <code>m1</code>을 Unparking 후 <code>g2</code>를 붙여준다.</li></ol></li></ol><blockquote><p>이때 2-2의 P는 처음 고루틴을 만들고 RunQueue에 담아준 P일 수도 있고, M이 만들어진 다음 새롭게 붙은 P일 수도 있다. 일단 여러 P 구조체가 접근할 수 있는 Global Level의 RunQueue처럼 이해하고, 이후 P의 Work Stealing을 이해한 다음 다시 생각해보자.</p></blockquote><p>그런데 위에 잠깐 언급된 것처럼, 동시 실행되는 고루틴이 아주 많이 생기면 계속해서 OS Thread를 만드는 상황이 생길 수 있다. 이 문제를 해결하기 위해서는 RunQueue가 접근하는 스레드 수를 제한할 필요가 있다.</p><h2 id="스레드-수를-제한한다"><a href="#스레드-수를-제한한다" class="headerlink" title="스레드 수를 제한한다."></a>스레드 수를 제한한다.</h2><p>스레드 수를 제한하지 않으면 스레드를 계속 생성하는 문제가 발생할 수 있다. 그래서 만약 스레드 수 제한에 도달하면 더 이상 스레드를 생성하지 않고 고루틴을 런큐에서 대기하도록 한다. Go에서는 이 제한값을 설정할 수 있는데 <code>GOMAXPROCS</code>라는 환경 변숫값을 사용한다. 최근 버전에서는 이 값이 머신의 CPU 코어 수로 설정되어 있다. 임의로 수정할 수도 있고 런타임에서는 <code>runtime.GOMAXPROCS</code> 함수를 사용해 설정할 수 있다.</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ GOMAXPROCS=10 go run main.go</span><br></pre></td></tr></table></figure><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> {</span><br><span class="line"> runtime.GOMAXPROCS(<span class="number">10</span>)</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="예를-들어-GOMAXPROCS-값이-2인-상황에서-g1이-m1-위에서-돌고-있고-g2가-생성되어-RunQueue에-들어가-있는-상황을-생각해보자-현재-GOMAXPROCS-만큼-M이-있기-때문에-g2는-대기하게-된다-만약-g0-고루틴에서-동기화-블락이-발생하면-ex-동기-채널로-g2가-보낸-데이터를-기다린다든지…-g0은-메인-스레드에서-빠져나오게-되고-g2가-메인-스레드로-가서-실행되게-된다"><a href="#예를-들어-GOMAXPROCS-값이-2인-상황에서-g1이-m1-위에서-돌고-있고-g2가-생성되어-RunQueue에-들어가-있는-상황을-생각해보자-현재-GOMAXPROCS-만큼-M이-있기-때문에-g2는-대기하게-된다-만약-g0-고루틴에서-동기화-블락이-발생하면-ex-동기-채널로-g2가-보낸-데이터를-기다린다든지…-g0은-메인-스레드에서-빠져나오게-되고-g2가-메인-스레드로-가서-실행되게-된다" class="headerlink" title="예를 들어 GOMAXPROCS 값이 2인 상황에서 g1이 m1 위에서 돌고 있고, g2가 생성되어 RunQueue에 들어가 있는 상황을 생각해보자.현재 GOMAXPROCS 만큼 M이 있기 때문에 g2는 대기하게 된다. 만약 g0 고루틴에서 동기화 블락이 발생하면 (ex. 동기 채널로 g2가 보낸 데이터를 기다린다든지…) g0은 메인 스레드에서 빠져나오게 되고 g2가 메인 스레드로 가서 실행되게 된다."></a>예를 들어 <code>GOMAXPROCS</code> 값이 2인 상황에서 <code>g1</code>이 <code>m1</code> 위에서 돌고 있고, <code>g2</code>가 생성되어 RunQueue에 들어가 있는 상황을 생각해보자.<br><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">m0 - g0 (메인 고루틴 동작 중) | RQ : [g2]</span><br><span class="line">m1 - g1 (g1 고루틴 동작 중)</span><br></pre></td></tr></table></figure><br>현재 <code>GOMAXPROCS</code> 만큼 M이 있기 때문에 <code>g2</code>는 대기하게 된다. 만약 <code>g0</code> 고루틴에서 동기화 블락이 발생하면 (ex. 동기 채널로 <code>g2</code>가 보낸 데이터를 기다린다든지…) <code>g0</code>은 메인 스레드에서 빠져나오게 되고 <code>g2</code>가 메인 스레드로 가서 실행되게 된다.<br><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">m0 - g2 (g2 동작 중) | channel wait queue: [g0]</span><br><span class="line">m1 - g1 (g1 동작 중)</span><br></pre></td></tr></table></figure></h2><p>왜 스레드를 조절하는 이름을 꼭 프로세스 조절 이름처럼 만들었을까? 이유는 이 값의 목적은 위에서 말한 것처럼 “OS Thread 수 조절”이 맞지만 실제 동작은 “<strong>가상 프로세서 P 숫자를 제어</strong>“하기 때문이다. 말 그대로 “최대 프로세서 수”라는 뜻이다. 무슨 차이가 있는 걸까?</p><p>위에서 고루틴이 실행 상태이기 위해서는 P와 M이 붙은 상황이어야 한다고 했다. 스케줄러 역할을 해줄 P와 실제 코드를 실행해줄 M이 필요하다는 뜻이다. 즉, 실행 상태인 고루틴은 P의 숫자에 종속적이다. 따라서 스레드 수는 늘어나도 그 스레드 M이 P와 함께 있는 상황이 아니면 코드를 실행할 수 없다는 뜻이다. M과 P가 붙어있을 수 없는 상황은 바로 시스템 콜을 수행 중인 M인 경우이다. 고루틴에서 시스템 콜을 호출해 OS 스레드가 블락되게 되면 해당하는 M과 G는 P 구조체와 분리되고 P는 새로운 M과 연결되면서 RunQueue에 있는 다른 고루틴을 스케줄링한다. 블락된 고루틴은 시스템 콜 작업이 끝나면 RunQueue로 돌아오게 된다. 이렇게 스레드가 블락 되었을 때 P를 M과 G에서 떼어내는 작업을 <code>handsoff</code>라고 한다. 이 특징 덕분에 P가 멈추지 않고 다른 고루틴을 새로운 M에 붙여줄 수 있게 되므로 고루틴이 기아 상태에 빠지지 않도록 해준다.</p><p>고 런타임에서는 블락된 고루틴을 확인하기 위해 백그라운드 모니터 스레드를 별도로 사용하고 있다. 이 스레드는 고루틴들이 블락되는 것을 감지했을 때 유휴 상태 스레드가 없다면 새로운 M을 만들어 P에 붙여주고 만약 유휴 상태의 스레드가 있으면 해당 M과 P를 활성화한다</p><p>이러한 구조때문에 Go는 M:P:N 멀티 스레딩 모델이라고도 불린다.</p><p><img src="/images/2022-05-25-go-scheduler/MPN.png?style=centerme" alt="M:P:N Threading Model"></p><blockquote><p>위 내용은 <code>src/runtime/proc.go</code> 파일의 <a href="https://github.com/golang/go/blob/0a1a092c4b56a1d4033372fbd07924dad8cbb50b/src/runtime/proc.go#L2345"><code>handsoffp</code> 함수</a> 주석에서 자세히 확인할 수 있다.</p></blockquote><p>이런 특징이 코드를 짤 때 어떤 문제를 발생시킬 수 있을까? 우리는 <code>GOMAXPROCS</code>를 가지고 OS Thread 수를 컨트롤할 수 있다고 생각할 수 있지만, 실제로는 그렇지 않다는 점이다. 예를 들어서 파일 100개를 고루틴으로 동시에 열어서 작업을 수행하는 것을 가정해보자. 이 경우 블락된 OS 스레드에서 P를 분리하고 새로운 OS 스레드 M을 만드는 작업을 하므로 이론상 100개가 넘는 스레드가 만들어질 수 있다.</p><p>다음 예시 코드는 100개의 고루틴을 돌려서 파일을 만들고 쓰는 작업을 한다.</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main </span><br><span class="line"> </span><br><span class="line"><span class="keyword">import</span> ( </span><br><span class="line"> <span class="string">"fmt"</span> </span><br><span class="line"> <span class="string">"os"</span> </span><br><span class="line"> <span class="string">"runtime/pprof"</span> </span><br><span class="line"> <span class="string">"sync"</span></span><br><span class="line">) </span><br><span class="line"> </span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> { </span><br><span class="line"> threadProfile := pprof.Lookup(<span class="string">"threadcreate"</span>) </span><br><span class="line"> fmt.Printf(<span class="string">"thread count before start: %d\n"</span>, threadProfile.Count()) </span><br><span class="line"> <span class="keyword">var</span> wg sync.WaitGroup </span><br><span class="line"> wg.Add(<span class="number">100</span>) </span><br><span class="line"> <span class="keyword">for</span> i := <span class="number">0</span>; i < <span class="number">100</span>; i++ { </span><br><span class="line"> <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">(n <span class="type">int</span>)</span></span> { </span><br><span class="line"> <span class="keyword">defer</span> wg.Done() </span><br><span class="line"> filename := fmt.Sprintf(<span class="string">"files/%d-file"</span>, n) </span><br><span class="line"> f, err := os.Create(filename) </span><br><span class="line"> <span class="keyword">if</span> err != <span class="literal">nil</span> { </span><br><span class="line"> <span class="built_in">panic</span>(err) </span><br><span class="line"> } </span><br><span class="line"> </span><br><span class="line"> <span class="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> { </span><br><span class="line"> <span class="keyword">if</span> err := f.Close(); err != <span class="literal">nil</span> { </span><br><span class="line"> fmt.Println(err) </span><br><span class="line"> } </span><br><span class="line"> </span><br><span class="line"> err := os.Remove(filename) </span><br><span class="line"> <span class="keyword">if</span> err != <span class="literal">nil</span> { </span><br><span class="line"> fmt.Println(err) </span><br><span class="line"> } </span><br><span class="line"> }() </span><br><span class="line"> </span><br><span class="line"> <span class="keyword">var</span> str []<span class="type">byte</span> </span><br><span class="line"> <span class="keyword">for</span> j := <span class="number">0</span>; j < <span class="number">1000</span>; j++ { </span><br><span class="line"> str = <span class="built_in">append</span>(str, <span class="type">byte</span>(j)) </span><br><span class="line"> } </span><br><span class="line"> _, err = f.Write(str) </span><br><span class="line"> <span class="keyword">if</span> err != <span class="literal">nil</span> { </span><br><span class="line"> <span class="built_in">panic</span>(err) </span><br><span class="line"> } </span><br><span class="line"> </span><br><span class="line"> }(i) </span><br><span class="line"> } </span><br><span class="line"> </span><br><span class="line"> wg.Wait() </span><br><span class="line"> </span><br><span class="line"> fmt.Printf(<span class="string">"threads count aftre program: %d\n"</span>, threadProfile.Count()) </span><br><span class="line">}</span><br></pre></td></tr></table></figure><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ go run main.go</span><br><span class="line">thread count before start: 5</span><br><span class="line">threads count aftre program: 77</span><br></pre></td></tr></table></figure><blockquote><p>시스템 콜 중 Non-Blocking I/O를 사용하는 경우가 있다. 가장 대표적으로 네트워크 I/O의 경우에는 epoll을 사용해 Non-Block으로 응답을 대기한다. 이 경우에는 M이 다른 고루틴을 수행할 수 있다. 네트워크 I/O로 블락이 발생한 고루틴은 Net Poller라고 하는 컴포넌트에서 대기하게 된다. Net Poller는 OS의 알림을 받고 고루틴을 다시 RunQueue로(특히, Local RunQueue로) 보낸다.</p></blockquote><hr><h2 id="분산-RunQueue로-Lock-제거"><a href="#분산-RunQueue로-Lock-제거" class="headerlink" title="분산 RunQueue로 Lock 제거"></a>분산 RunQueue로 Lock 제거</h2><p>RunQueue가 Global RunQueue 형태였다면 여러 P에서 고루틴을 가져오기 위해 Lock을 사용해야 한다. Go는 Global RunQueue(GRQ) 역시 사용하기는 하는데 일단 기본적으로 지금까지 설명한 내용은 Local RunQueue(LRQ)를 사용한다. 각 P 구조체마다 RunQueue를 가지고 P와 연결된 스레드의 스택을 최대한 사용한다.</p><p>또한 P가 가지고 있는 G 안에서 새로운 고루틴을 만들게 되면 이 고루틴 역시 해당 P의 LRQ에 들어가게 된다. GRQ가 사용되는 시점은 몇 가지 있지만 대표적으로 LRQ가 가득 찬 상태에서 또 새로운 고루틴을 생성하려고 할 대 GRQ로 들어가게 된다.</p><p>P가 만약 G를 M에 붙이지 않은 상태라면 M은 현재 놀고 있는 스레드라는 뜻이다. 이 상태에 있는 P와 M은 “<strong>Spinning Thread</strong>“라고 한다. 이 상태에서 P는 M에 붙여줄 고루틴을 찾아야 한다. LRQ를 가장 먼저 확인하는데 만약 P가 LRQ에 고루틴을 가지고 있지 않은 상태가 되면 임의의 P의 LRQ에 있는 작업 절반을 훔친다. 이 과정을 Work Stealing이라고 한다. 이 과정을 통해 전체 작업을 고르게 분산할 수 있게 된다. 만약 Work Steal할 대상도 없는 경우에는 GRQ를 바라본다. 그래도 가져올 게 없으면 M과 P는 Parking 된다.</p><p>Work-Stealing은 작업을 고르게 처리하도록 도와주지만, Locality를 떨어뜨린다. 고루틴은 생성 시 사용된 스레드에서 실행되어야 캐시도 활용하고 같은 메모리 스택을 사용하게 되는데 훔쳐지면 이 이점을 살릴 수 없다. 따라서 LRQ의 구조는 단순히 FIFO 구조가 아니라 맨 앞에는 LIFO 형태로 동작하는 버퍼를 사용한다.</p><p><img src="/images/2022-05-25-go-scheduler/LRQ.png?style=centerme" alt="LRQ 버퍼"></p><p>위 이미지처럼 LIFO 버퍼가 비어있는 경우 그 버퍼에 들어가고 만약 새로운 고루틴이 바로 더 들어오면 버퍼에서 밀려 FIFO 큐로 기존 고루틴이 들어가게 되고 새롭게 Enqueue되는 고루틴이 해당 버퍼 자리를 가져간다.</p><p>이 우선순위가 있는 버퍼와 함께 새로운 고루틴이 3ms 가량 Work-Steeling 되지 않는다는 규칙이 있어서 어느 정도 Work-Stealing으로 인한 지역성 저하를 보완한다.</p><h2 id="Fairness"><a href="#Fairness" class="headerlink" title="Fairness"></a>Fairness</h2><p>스케줄링의 굉장히 중요한 요소 중 하나인 공평성이 보장되기 위해 여러 기법을 적용하고 있다. 이러한 특징을 Fairness라고 부른다.</p><ol><li>스레드를 사용하는 고루틴이 10ms 이상 실행되지 않도록 한다. 이 타임 스판을 넘어가면 선점되어 GRQ로 들어가게 된다.</li><li>LRQ의 구조를 보면 2개의 고루틴이 계속 반복적으로 스레드를 독차지할 수도 있는 구조라는 것을 알 수 있다. 이를 방지하기 위해 버퍼에 들어간 고루틴은 스레드를 반납하더라도 타임 스판이 초기화되지 않는다. 따라서 한 고루틴이 이 버퍼를 차지할 수 있는 시간은 10ms이다.</li><li>P 구조체가 고루틴을 찾는 과정이 LRQ, Work-Stealing, GRQ 순서이기 때문에 GRQ의 고루틴이 기아 상태에 빠질 수 있다. 이를 방지하기 위해서 스케줄러는 61번마다 한 번씩 LRQ보다 GRQ를 우선해서 확인한다. 61이라는 숫자는 소수 중 경험적 테스트를 통해 나온 값이라고 한다.</li><li>Net Poller 같은 경우엔 응답을 확인하는 별도의 스레드를 사용한다 이 스레드는 G M P 구조와 별도로 동작하므로 고 런타임에 의한 기아 상태에 빠지지 않는다.</li></ol><blockquote><p>Go 스케줄러는 기본적으로 비선점적 방식이기 때문에 10ms, 3ms 등의 이벤트는 Best-Effort에 해당한다. 완전히 정확한 타이밍으로 동작하는 것은 아니다. 다만 1.12 버전 이후로 무거운 Loop가 돌면서 선점되지 않는 고루틴이 발생하는 것을 막기 위해 선점적 스케줄링 방식이 일부 도입되었다.</p></blockquote><h2 id="고루틴-재활용"><a href="#고루틴-재활용" class="headerlink" title="고루틴 재활용"></a>고루틴 재활용</h2><p>고루틴이 담고 있던 코드 흐름이 모두 완료되고 나면 고루틴을 보관한다.</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> p <span class="keyword">struct</span> {</span><br><span class="line"> ...</span><br><span class="line"> <span class="comment">// Available G's (status == Gdead)</span></span><br><span class="line"> gFree <span class="keyword">struct</span> {</span><br><span class="line"> gList</span><br><span class="line"> n <span class="type">int32</span></span><br><span class="line"> }</span><br><span class="line"> ...</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>위 구조체는 P 구조체인데, <code>gFree</code>에 유휴 상태의 고루틴을 모아둔다. 이 리스트를 유지함으로써 유휴 상태의 고루틴을 저장하거나 뺄 때 Lock같은 동작이 필요 없게 된다.</p><p>더 나은 고루틴 관리와 분배를 위해 스케줄러 자체적으로 글로벌하게 관리하는 리스트 두 개가 있는데, 하나는 재활용이 가능한 스택이 할당된 고루틴을 보관하는 리스트와 스택 재활용이 불가능해 스택을 해제한 고루틴을 보관하는 리스트이다. P가 관리하는 유휴 상태의 고루틴이 64개가 넘어가면 고루틴의 절반이 중앙 리스트로 이동하게 된다. 이때 고루틴이 추가적인 메모리를 할당 받아 2KB보다 큰 메모리 사이즈를 가지고 있는 경우가 재활용 불가능한 고루틴으로 판단되어 메모리를 할당 해제 후 보관하고, 그렇지 않은 경우 스택 메모리도 재활용해 사용한다.</p><hr><p>이렇게 재활용하는 특성은 OS 스레드를 계속 만드는 것처럼 비슷한 문제를 야기할 수 있다. 즉, 고루틴을 계속 만들어내는 문제가 생길 수 있다.</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">read</span><span class="params">(wg *sync.WaitGroup, gid <span class="type">int</span>)</span></span> {</span><br><span class="line"> sem <- <span class="keyword">struct</span>{}{} <span class="comment">// semaphore P() (Wait())</span></span><br><span class="line"> processing() <span class="comment">// long process</span></span><br><span class="line"> fmt.Println(<span class="string">"Done"</span>, gid)</span><br><span class="line"> wg.Done()</span><br><span class="line"> <-sem <span class="comment">// semaphore V() (Signal())</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line"> wg.Add(<span class="number">100</span>)</span><br><span class="line"> <span class="keyword">for</span> i := <span class="number">0</span>; i < <span class="number">100</span>; i++ {</span><br><span class="line"> <span class="keyword">go</span> read(&wg, i)</span><br><span class="line"> }</span><br><span class="line"> wg.Wait()</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>위 코드는 일단 고루틴이 생성된 다음 실행 흐름을 판단하기 때문에, 고루틴은 무조건 계속 만들어진다. </p><p><img src="/images/2022-05-25-go-scheduler/many-goroutine.png?style=centerme" alt="많은 고루틴이 만들어진 모습"></p><p>따라서 고루틴이 만들어지는 시점과 흐름을 제어해야 하는 시점을 잘 판단해서 코드를 짜야 한다.</p><h1 id="Overall"><a href="#Overall" class="headerlink" title="Overall"></a>Overall</h1><p>지금까지의 이야기로 다음 이미지를 이해할 수 있게 되었다. 이 이미지를 이해하기 글에서 각 컴포넌트들을 다시 살펴보자</p><p><img src="/images/2022-05-25-go-scheduler/overall.png?style=centerme" alt="전체적인 모습"></p><h1 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h1><ul><li><a href="https://morsmachine.dk/netpoller">https://morsmachine.dk/netpoller</a></li><li><a href="https://ssup2.github.io/theory_analysis/Golang_Goroutine_Scheduling/">https://ssup2.github.io/theory_analysis/Golang_Goroutine_Scheduling/</a></li><li><a href="https://www.youtube.com/watch?v=KBZlN0izeiY&ab_channel=GopherAcademy">https://www.youtube.com/watch?v=KBZlN0izeiY&ab_channel=GopherAcademy</a></li><li><a href="https://www.ardanlabs.com/blog/2018/08/scheduling-in-go-part2.html">https://www.ardanlabs.com/blog/2018/08/scheduling-in-go-part2.html</a></li><li><a href="https://rakyll.org/scheduler/">https://rakyll.org/scheduler/</a></li></ul>]]></content:encoded>
<category domain="https://changhoi.kim/categories/go/">go</category>
<category domain="https://changhoi.kim/tags/programming/">programming</category>
<comments>https://changhoi.kim/posts/go/go-scheduler/#disqus_thread</comments>
</item>
<item>
<title>초보를 위한 테라폼</title>
<link>https://changhoi.kim/posts/etc/terraform-for-newbie/</link>
<guid>https://changhoi.kim/posts/etc/terraform-for-newbie/</guid>
<pubDate>Sat, 07 May 2022 15:00:00 GMT</pubDate>
<description><p>서비스의 복잡도가 증가하면서 인프라를 어떻게 더 쉽게 관리할 수 있을까에 대한 고민은 지속해서 증가했다. 정적인 상태와 안전한 코드로서 인프라를 관리하고 인프라에서 해야 하는 작업을 확장성있는 방식으로 구성하기 위해 개발자들은 인프라를 코드로써 표현하려고 했다. 이를 IaC (Infra as Code)라고 한다. 현재는 Terraform이 주류를 잡고 있는 듯 하다. 이 글은 IaC에 대한 이야기부터 테라폼의 얕은 이야기를 다룰 예정이다.</p></description>
<content:encoded><![CDATA[<p>서비스의 복잡도가 증가하면서 인프라를 어떻게 더 쉽게 관리할 수 있을까에 대한 고민은 지속해서 증가했다. 정적인 상태와 안전한 코드로서 인프라를 관리하고 인프라에서 해야 하는 작업을 확장성있는 방식으로 구성하기 위해 개발자들은 인프라를 코드로써 표현하려고 했다. 이를 IaC (Infra as Code)라고 한다. 현재는 Terraform이 주류를 잡고 있는 듯 하다. 이 글은 IaC에 대한 이야기부터 테라폼의 얕은 이야기를 다룰 예정이다.</p><span id="more"></span><blockquote><p>이번 글은 테라폼이 무엇일지 대략적인 감을 잡기 위한 글이다. <code>tfstate</code>, 협업하는 방법, Best Practice같은 얘기는 이번 글에서 다루지 않는다.</p></blockquote><h1 id="IaC-Infrastructure-as-Code"><a href="#IaC-Infrastructure-as-Code" class="headerlink" title="IaC (Infrastructure as Code)"></a>IaC (Infrastructure as Code)</h1><p>말 그대로 인프라를 코드로 관리하는 방법이다. 이런 구성의 목적은 무엇일까?</p><ul><li><p><strong>재사용성</strong>: 손으로 하던 작업을 코드로 변경하게 되면 대표적으로 바뀌는 점은 반복 가능하다는 점이다. 그리고 코드를 어떻게 짜는지에 따라 다르겠지만, 작은 단위마다 재사용이 가능하다. 예를 들어서 웹 서버를 클러스터 형태로 배포하고 앞에 로드 밸런서를 달아둔다고 해보자. 이 구성은 여러 서비스를 새로 배포할 때마다 몇 가지 설정을 제외하고 모두 같은 과정을 거치게 된다. 만약 코드가 있다면 이 과정을 반복하는 지루한 일을 할 필요 없다.</p></li><li><p><strong>안정성</strong>: 코드를 재사용할 수 있다는 것은 안전한 흐름을 재사용하는 것과 같다. 안전하다는 것은 테스트를 통해 확인할 수 있고, 테라폼 역시 테스트 코드를 만들 수 있다. 어찌 됐든 안전한 작은 흐름을 모아 크고 안전한 흐름을 구성할 수 있게 해준다.</p></li><li><p><strong>가시성</strong>: 가시성이라고 표현할만한지 사실 잘 모르겠지만 코드로 인프라를 구성하는 것은 인프라의 구성을 파악하기 훨씬 쉬워진다고 생각한다. 또한 Orphan이 된 인프라를 손쉽게 제거할 수도 있고, 사람이 보지 못해서 생기는 문제들을 줄여준다.</p></li></ul><p>이외에도 여러 장점이 있겠지만 제일 중요한 포인트는 재사용성인 것 같다. 재사용이 가능하다는 것은 여러 스테이지로 현재 서비스 구성을 복사할 수도 있다는 것을 의미하고, 완전히 동일한 인프라 구성에서 테스트 스테이지를 구성할 수도 있다는 것을 뜻한다. IaC의 장점은 이렇게 쉽게 환경을 만들고 부술 수 있는 능력에 있다고 생각한다.</p><h1 id="IaC-구분"><a href="#IaC-구분" class="headerlink" title="IaC 구분"></a>IaC 구분</h1><p>IaC는 크게 다섯 가지로 구분한다. 애드훅 스크립트, 구성 관리 도구, 서버 템플릿 도구, 오케스트레이션 도구, 프로비전 도구로 나눌 수 있다.</p><h2 id="애드훅-스크립트"><a href="#애드훅-스크립트" class="headerlink" title="애드훅 스크립트"></a>애드훅 스크립트</h2><p>인프라 자동화의 가장 기본적인 방법이고 가장 쉽다. 서버에서 수행할 작업을 스크립트로 구성하고 직접 실행시키는 방법이다. Shell Script 또는 익숙한 코드로 필요한 내용을 작성하고 이를 동작시키는 방식이다. 소규모 인프라에서 일회성 작업이 필요한 경우 아주 적합한 방법이다. 하지만 복잡한 작업을 해야 한다면 결국 새로운 코드 베이스를 만들어 관리하는 형태가 되기 때문에 바람직하지 않다.</p><h2 id="구성-관리-도구"><a href="#구성-관리-도구" class="headerlink" title="구성 관리 도구"></a>구성 관리 도구</h2><p>타겟 서버가 가지고 있어야 하는 소프트웨어를 관리하도록 설계되어있다. 애드훅 스크립트와 비슷한데 인프라 관련된 규칙을 가진 도구이기 때문에 무슨 작업을 하는지 비교적 쉽게 코드로 파악할 수 있다. 애드훅 스크립트와 비교했을 때 가장 큰 차이점은 이 종류에 해당하는 도구들의 대부분이 멱등성을 가지고 있다는 점이다. 쉘 스크립트로 인프라에 대한 멱등성 있는 코드를 작성하는 것은 귀찮은 일이고, 코드도 복잡해진다. 이러한 도구의 가장 대표적인 도구는 <a href="https://www.ansible.com/">앤서블</a>이다.</p><h2 id="서버-템플릿-도구"><a href="#서버-템플릿-도구" class="headerlink" title="서버 템플릿 도구"></a>서버 템플릿 도구</h2><p>말 그대로 서버 구성을 스냅샷화 해서 사용하는 도구이다. 운영체제, 소프트웨어, 파일 및 기타 필요한 모든 상황에 대해 스냅샷을 만들 수 있는 도구이다. 이런 스냅샷은 보통 “이미지”라고 불린다. 서버 템플릿 도구로 만들어진 이미지를 구성 관리 도구를 사용해 서버에 다운받는 방식으로 사용할 수도 있다. 이러한 도구의 가장 대표적인 도구는 <a href="https://www.docker.com/">도커</a>와 <a href="https://www.packer.io/">패커</a>가 있다.</p><blockquote><p>서버 템플릿은 <code>Immutable Infrastructure</code>를 구성하기 위한 핵심적인 요소이다. 한 번 만들어진 스냅샷은 변하지 않고, 새로운 버전을 만들고 싶으면 아예 새로운 스냅샷을 빌드해 배포해야 한다. Snow Flake를 확실히 막는 방법이지만, 아주 작은 변경 사항마다 새 버전을 빌드하고 기존 버전의 서버를 내리는 방식은 조금 무거운 작업일 수 있다.</p></blockquote><h2 id="오케스트레이션-도구"><a href="#오케스트레이션-도구" class="headerlink" title="오케스트레이션 도구"></a>오케스트레이션 도구</h2><p>오케스트레이션은 VM및 컨테이너를 어떻게 관리할지에 관해 관심을 두는 도구이다. 몇 개의 컨테이너가 유지되어야 하는지, 어떤 컨테이너에서 장애가 발생했는지, 어디로 요청을 포워딩해야 하는지 등 여러 서비스 사이의 컨트롤 타워 역할을 한다. 가장 대표적인 도구로는 쿠버네티스가 있다.</p><blockquote><p>쿠버네티스도 IaC라고 할 수 있는 건가? 쿠버네티스는 원하는 상태를 오브젝트로 만들어 피드백 루프를 돌면서 지속해서 바람직한 상태에 있는지 확인하면서 오케스트레이션을 수행한다. 개발자는 이러한 “상태”를 yaml 구성 파일을 가지고 정의한다. 따라서 일종의 IaC라고 볼 수 있다.</p></blockquote><h2 id="프로비전-도구"><a href="#프로비전-도구" class="headerlink" title="프로비전 도구"></a>프로비전 도구</h2><p>구성 관리 도구, 서버 템플릿 도구, 오케스트레이션 도구는 크게 서비스에 대해 주된 관심을 두는 도구들이다. 서버 내부가 어떻게 되어야 하는지 또는 서비스가 어떻게 실행되고 있어야 하는지를 관리하는 도구들이다. 반면 프로비전 도구는 이름처럼 서버 자체를 생성하고 관리한다. 인스턴스의 개수, 인프라 사이의 연결 등 인프라에 대한 모든 것을 프로비저닝 하는 데 사용된다. 테라폼은 대표적인 프로비전 도구다.</p><h1 id="테라폼"><a href="#테라폼" class="headerlink" title="테라폼"></a>테라폼</h1><p>테라폼은 IaC를 위한 선언형 언어이다. 선언형 언어라는 것은 인프라의 상태를 정의하는 코드라는 뜻이다. “프로그래밍 언어”로서 기능을 한다고 생각하는데, 이유는 반복문과 조건절, 함수와 비슷한 모듈의 개념이 있기 때문이다. 테라폼 맛을 한번 보자.</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">resource "aws_instance" "example" {</span><br><span class="line"> ami = "ami-something"</span><br><span class="line"> instance_type = "t3.micro"</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">resource "google_dns_record_set" "a" {</span><br><span class="line"> name = "demo.google-example.com"</span><br><span class="line"> managed_zone = "example_zone"</span><br><span class="line"> type = "A"</span><br><span class="line"> ttl = 300</span><br><span class="line"> rrdatas = [aws_instance.example.public_ip]</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>AWS에서 인스턴스를 만들고 GCP에서 AWS 서버에 접속할 IP를 DNS 레코드에 넣는 작업이다. 모르는 사람이 봐도 대충 이렇겠구나 싶은 느낌이 오는 정도의 가독성을 가지고 있다.</p><h2 id="핵심적인-테라폼-흐름"><a href="#핵심적인-테라폼-흐름" class="headerlink" title="핵심적인 테라폼 흐름"></a>핵심적인 테라폼 흐름</h2><p>테라폼의 핵심적인 흐름은 다음 구성을 가지고 있다.</p><ul><li><strong>Write</strong>: 테라폼 코드를 적는 단계. 어떤 리소스들이 어떤 방식으로 구성되어있는지 선언하는 단계이다.</li><li><strong>Plan</strong>: 실제 인프라에 적용하기 전에 어떻게 변화가 생기는지 확인하는 단계이다.</li><li><strong>Apply</strong>: 인프라에 실제로 의도한 대로 변화를 가하는 단계이다.</li></ul><p>Write 단계에서는 테라폼의 문법대로 <code>tf</code> 확장자의 파일을 구성하는 방식이다. Plan은 이렇게 구성한 테라폼 파일들의 실행 계획을 확인하는 단계이다. <code>terraform plan</code>이라는 명령어를 사용한다. Apply는 위 계획대로 인프라에 적용하는 단계이다. <code>terraform apply</code> 명령어로 수행한다.</p><blockquote><p>Plan 단계에서 문제없이 실행될 것처럼 보여도 실제 Apply 단계에서 문제가 발생할 수도 있다. 예를 들어 변경하려고 하는 S3 버킷의 이름이 이미 있는 이름이라든지, 여러 이유가 있을 수 있다.</p></blockquote><p><img src="https://mktg-content-api-hashicorp.vercel.app/api/assets?product=terraform&version=refs/heads/stable-website&asset=website/img/docs/intro-terraform-workflow.png" alt="테라폼 단계"></p><blockquote><p>테라폼 코드를 작성하거나 팀이 이미 작성한 테라폼 코드를 가져온 다음 테라폼 코드가 실행될 수 있도록 초기화해주는 단계가 있다. <code>terraform init</code> 명령어인데, 이 과정에서 테라폼을 실행하기 위해 필요한 플러그인을 설치하기도 하지만 신경 써야 하는 점은 테라폼 백앤드를 설정하는 과정이다. 기본은 로컬에서 동작하지만, 이는 팀과 함께 작업하기에는 부적합하다. <a href="https://www.terraform.io/cli/commands/init"><code>init</code> 명령어가 하는 역할</a>과 협업 과정에서 백앤드 초기화하기 위한 좋은 방법을 <a href="https://www.terraform.io/intro/core-workflow#working-as-a-team">이 링크</a>에서 확인해보자.</p></blockquote><h2 id="Syntax-구성-요소"><a href="#Syntax-구성-요소" class="headerlink" title="Syntax 구성 요소"></a>Syntax 구성 요소</h2><p>테라폼의 문법적 요소를 구성하는 키워드와 규칙이 크게 두 가지 있다. 하나는 <code>Argument</code>, 다른 하나는 <code>Block</code>이다.</p><h3 id="Argument"><a href="#Argument" class="headerlink" title="Argument"></a>Argument</h3><p>특정한 식별자에 할당된 값을 의미한다.</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">image_id = "1234"</span><br></pre></td></tr></table></figure><p>등호를 기준으로 좌측이 식별자, 우측이 표현 식이다. 프로그래밍 언어처럼 그냥 리터럴한 값이 올 수도 있지만, 프로그램에 의해 결정되는 표현 식이 오기도 한다. Argument는 특정 컨텍스트 안에서 유효성이 결정된다. 즉, 어떤 컨텍스트 안에 속해있는지에 따라 타입, 식별자 이름 등이 유효한지 아닌지 판단된다.</p><h3 id="Block"><a href="#Block" class="headerlink" title="Block"></a>Block</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">resource "aws_instance" "example" {</span><br><span class="line"> ami = "ami-id"</span><br><span class="line"> ...</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>위 <code>{ ... }</code> 부분이 블록이다. 블록은 타입을 가지고 있는데 위 예시의 타입은 <code>resource</code>라는 타입이다. <code>resource</code> 타입에서는 두 가지의 라벨을 기대하고 있다. 하나는 <code>aws_instance</code>와 같이 정해진 리소스 이름이고 다른 하나는 <code>example</code>과 같이 임의로 정하는 식별자 이름이다. 지금 당장 <code>resource</code> 타입에 대해 설명하고자 하는 것은 아니니 블록에는 라벨이 붙을 수도 있고 아닐 수도 있다는 점만 알고 있자. 블록 내부와 라벨은 어떤 타입의 블록 컨텍스트인지에 따라 달라진다.</p><h2 id="Module"><a href="#Module" class="headerlink" title="Module"></a>Module</h2><p>테라폼은 <code>tf</code> 확장자를 가진 파일들로 만들어진다. 이 파일들이 모여 모듈이 구성되는데 모듈의 기준은 디렉토리이다. 같은 디렉토리 안에 모여있는 <code>tf</code> 파일들이 모두 모듈로 활용된다. 어떤 디렉토리의[ 하위에 위치하더라도 그 둘은 다른 모듈이다.</p><p>테라폼이 실행되는 모듈은 하나뿐이다. 이 모듈을 “<strong>루트 모듈</strong>“(<strong>Root Module</strong>)이라고 한다. 그리고 이 모듈에서 사용하는 다른 디렉토리에 위치한 모듈들을 “<strong>자식 모듈</strong>“(<strong>Child Module</strong>)이라고 부른다.</p><p>모듈은 테라폼의 함수와 같은 역할을 해준다. 작은 단위로 나누고 테스트하고 확장성 있게 만들어서 여러 스테이지에서 반복되는 코드를 쓸 필요 없도록 해준다. 구체적으로 자식 모듈을 가져와 사용하는 예시는 이후 후술한다.</p><h2 id="Provider"><a href="#Provider" class="headerlink" title="Provider"></a>Provider</h2><p>Provider는 인프라를 제공하는 클라우드 혹은 오픈 스택과 같은 인프라 차원의 API 종류를 정하는데 사용하는 블록 타입이다. 제공하는 벤더에 따라 테라폼이 실행할 때 다운로드 해 오는 코드가 달라진다. Provider는 실행하고자 하는 루트 모듈에만 있으면 된다. 자식 모듈은 실행하는 루트 모듈의 프로바이더 정보를 사용한다.</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">provider "aws" {</span><br><span class="line"> profile = "terraform"</span><br><span class="line"> region = "ap-northeast-2"</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>위 코드는 AWS를 사용하겠다는 것을 의미한다. <code>provider</code> 블록은 정해진 벤더사의 이름을 넣어야 하는 라벨을 요구한다. 그리고 블록 안에서는 필수적인 Argument들이 있을 수 있다.</p><p>테라폼은 정말 많은 프로바이더를 가지고 있다. <a href="https://registry.terraform.io/browse/providers">이 링크</a>에서 확인해볼 수 있다.</p><h2 id="Resource"><a href="#Resource" class="headerlink" title="Resource"></a>Resource</h2><p>테라폼은 인프라를 다루는 언어인 만큼 인프라 리소스를 정의할 수 있는 블록이 필요하다. 이 역할을 <code>resource</code> 타입이 해준다.</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">resource "aws_instance" "web" {</span><br><span class="line"> ami = "ami-id"</span><br><span class="line"> instance_type = "t3.small"a</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>위에서 예시로도 사용됐는데, 두 가지의 라벨을 요구하는 블록 타입이다. 하나는 리소스 종류를 정해야 하고, 그다음 나오는 라벨은 로컬 네임이다. 로컬 네임은 테라폼 모듈 안에서 해당하는 리소스를 참조하기 위해 사용된다. 다른 모듈에서 같은 이름이 나오는 것은 상관없다. 리소스가 무엇이냐에 따라 안에 Argument들도 달라진다. AWS를 생각해보면 정말 수많은 리소스 종류가 있기 때문에 어떻게 작성해야 하는지를 일일히 외울 수는 없다. 공식문서에서 프로바이더마다 제공하는 리소스와 어떻게 테라폼 리소스 코드를 써야 하는지 알려주는 문서를 확인하면서 코드를 써야 한다. AWS의 경우 <a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs">이 링크</a>를 통해 알 수 있다.</p><h2 id="Data"><a href="#Data" class="headerlink" title="Data"></a>Data</h2><p>테라폼 외부의 데이터를 사용하기 위한 타입이다. 여기서 외부라는 말은 분리된 다른 테라폼의 설정이라든지, 인프라 내에서 변경되는 값들을 의미한다.</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">data "aws_ami" "example" {</span><br><span class="line"> most_recent = true</span><br><span class="line"> owner = ["self"]</span><br><span class="line"> tags = {</span><br><span class="line"> Name = "app-server"</span><br><span class="line"> Tested = "true"</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>위 데이터 블록은 테라폼에게 <code>aws_ami</code>를 읽어와서 <code>example</code>이라는 이름으로 필요한 정보를 읽을 수 있게 해준다. 예를 들어 테라폼에 DB를 생성한 다음 만들어지는 주소를 웹서버에게 넘겨주기 위해 사용할 수 있다. 데이터 블록 안의 Argument들은 데이터 블록의 쿼리를 만들어주는 역할을 한다.</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">data "aws_vpc" "default" {</span><br><span class="line"> default = true</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">data "aws_subnet_ids" "default" {</span><br><span class="line"> vpc_id = data.aws_vpc.default.id</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>위 예시 코드는 기본 VPC를 가져와서 <code>data.aws_vpc.default</code>에서 참조할 수 있게 하는 데이터 블록을 만들고 그 블록을 사용해 해당 VPC 안에 있는 서브넷들을 가져오는 코드이다. 서브넷들은 마찬가지로 <code>data.aws_subnet_ids.default</code>로 참조할 수 있다.</p><h2 id="Variable"><a href="#Variable" class="headerlink" title="Variable"></a>Variable</h2><p>확장적인 인프라 모듈을 위해서는 적절한 변수들이 활용되어야 한다. 100개의 인스턴스 이름을 바꾸는 작업에 단순 작업이 필요하다면 확장이 어려운 것으로 볼 수 있다. 임의의 변수를 설정하는 것은 <code>variable</code>이라는 변수 타입으로 만들 수 있다.</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">variable "server_port" {</span><br><span class="line"> description = "The port for HTTP requests.</span><br><span class="line"> type = number</span><br><span class="line"> default = 8080</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">variable "alb_name" {</span><br><span class="line"> description = "The name of the ALB"</span><br><span class="line"> type = string</span><br><span class="line"> default = "terraform-asg"</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>이 변수는 모듈에 대한 <code>input</code>이다. 루트 모듈이 자식 모듈을 불러올 때 변수를 지정할 수도 있고, 루트 모듈을 실행할 때 환경 변수 및 실행할 인자 값 등으로 넣어줄 수 있다. 만약 위 예시처럼 기본값이 정해져 있다면 따로 넣어줄 필요 없다.</p><p>위 예시에서는 <code>server_port</code>, <code>alb_name</code>이라는 이름을 모듈 안에서 <code>var.<name></code> 형태로 사용할 수 있다.</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">resource "alb_something" "example" {</span><br><span class="line"> port = var.server_port</span><br><span class="line"> name = "${var.alb_name}-cluster-lb"</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="Output"><a href="#Output" class="headerlink" title="Output"></a>Output</h2><p>함수처럼 사용할 수 있으려면 반환 값도 필요하다. 모듈의 결과를 내보낼 수 있는 블록이 있는데, 이 블록이 <code>output</code>이다.</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">output "asg_name" {</span><br><span class="line"> value = aws_autoscaling_group.example.name</span><br><span class="line"> description = "The name of the ASG"</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>aws_autoscaling_group.example.name</code>은 모듈 안에 있는 ASG 리소스 중 <code>example</code>이라는 식별자가 붙은 것의 이름을 의미한다. 다른 모듈에서 이 변수를 사용한다면 다음과 같이 사용하게 된다.</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">module.[module_name].[output_name]</span><br><span class="line">module.webclusters.asg_name</span><br></pre></td></tr></table></figure><h2 id="반복문"><a href="#반복문" class="headerlink" title="반복문"></a>반복문</h2><p>선언적 언어는 절차적인 유형의 작업을 처리하기 어렵다. 반복, 조건문 등을 지원하지 않는 경우가 많이 있다. 테라폼에서는 루프를 지원해서 이런 상황에서 사용이 가능하다. 테라폼은 다음과 같은 루프가 있다. </p><ul><li><code>for_each</code>: 리소스 내에서 리소스 또는 인라인 블록을 반복한다.</li><li><code>for</code>: 리소스와 맵 타입을 반복한다.</li><li><code>count</code>: 리소스를 지정한 수만큼 반복한다.</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">variable "aws_iam_user" {</span><br><span class="line"> description = "IAM users with these names"</span><br><span class="line"> type = list(string)</span><br><span class="line"> default = ["neo", "trinity", "morpheus"]</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">resource "aws_iam_user" "example" {</span><br><span class="line"> count = length(var.aws_iam_user)</span><br><span class="line"> name = var.aws_iam_user[count.index]</span><br><span class="line">}</span><br></pre></td></tr></table></figure><blockquote><p><code>length</code>는 내장함수이다. 여러 내장 함수가 있으므로 <a href="https://www.terraform.io/language/functions">이 링크</a>에서 학습하는 것을 추천한다.</p></blockquote><p><code>count</code>는 <code>index</code>를 통해 순회한다. 위 예시는 <code>aws_iam_user</code> 리소스를 콜렉션의 길이만큼 반복한다.</p><p><code>count</code>를 사용할 때는 다음과 같은 제약이 생긴다.</p><ul><li>리소스 안에서 특정 인라인 블록을 반복할 수는 없다. 인라인 블록은 태그나, 로컬 변수 등 블록 안에서 만들어지는 블록을 의미한다.</li><li>배열의 인덱스대로 처리한다. 만약 위 예시에서 <code>trinity</code>를 지운다면, 기존 IAM 중에서 <code>morpheus</code>를 지우고 <code>trinity</code>의 이름을 <code>morpheus</code>로 변경한다.</li></ul><blockquote><p><code>for</code>, <code>for_each</code>에 대한 설명은 이 글에서 하지 않았다. 구체적인 타입에 대한 설명과 몇 가지 내장 함수들을 설명해야 하는데, 글의 취지와 적합하지 않은 듯 하여 뺐다. 자세한 설명은 <a href="https://www.terraform.io/language/expressions/conditionals">이 링크</a>를 읽자.</p></blockquote><h2 id="조건문"><a href="#조건문" class="headerlink" title="조건문"></a>조건문</h2><p>조건문은 아쉽지만 삼항 연산자로만 표현해야 한다.</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">condition ? true : false</span><br></pre></td></tr></table></figure><p>위와 같이 조건이 맞으면 앞의 값을 사용하고 틀리면 뒤의 값을 사용한다. 리소스를 생성할지 말지를 결정하는 것은 <code>count</code>를 함께 사용해 구성할 수 있다.</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">resource "something" "something" {</span><br><span class="line"> count = var.some_boolean ? 1 : 0</span><br><span class="line"> ...</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>만약 <code>var.some_boolean</code> 값이 <code>true</code>라면 이 리소스는 생성될 것이고 그렇지 않으면 생성되지 않는다.</p><h1 id="Usecase"><a href="#Usecase" class="headerlink" title="Usecase"></a>Usecase</h1><p>지금까지의 내용을 통해 테라폼이 어떤 느낌의 언어인지 파악할 수 있다. 선언형 언어라는 점, 프로그래밍 언어적 특성들을 어떤 방식으로 제공하고 있는지 등. 테라폼의 모든 내용을 이 글에서 배우기는 어렵고, 더 자세한 내용은 테라폼의 공식 문서가 굉장히 잘 정리해주고 있으니 참고해서 학습할 수 있다. 테라폼을 사용한 조금 구체적인 활용 예시는 다음과 같다.</p><ul><li>테라폼을 사용하면 인프라에 대한 테스트 코드를 작성할 수 있게 된다. 그렇게 되면 안정적인 인프라 구성을 할 수 있게 된다.</li><li>Stage를 분리하려고 할 때 동일한 상태를 간단하게 복사할 수 있다. 테스트 환경을 제거하는 방법도 <code>destory</code> 명령으로 간단히 해결할 수 있다.</li><li>테라폼으로 인프라를 관리함으로써 제거해야 하는 리소스를 빼먹고 남겨두는 상황을 최소화할 수 있다. 적어도 테라폼으로 삭제하지 못하게 되면 어떤 리소스가 왜 삭제되지 않았는지 파악할 수도 있다.</li></ul><hr><p>이 글로도 사실 테라폼이 어떤 건지 쉽게 감을 잡기 어려울 수 있다. 개인적인 생각으로는 직접 Example을 따라 해본다면 쉽게 이해할 수 있고, 프로그래밍 관점에서 테라폼을 바라볼 때 이 글이 도움이 될 수 있을 것 같다.</p><h1 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h1><ul><li><a href="https://www.terraform.io/cli/commands/init">https://www.terraform.io/cli/commands/init</a></li><li><a href="https://www.terraform.io/intro/core-workflow#working-as-a-team">https://www.terraform.io/intro/core-workflow#working-as-a-team</a></li><li><a href="https://learning.oreilly.com/library/view/terraform-up/9781492046899/">https://learning.oreilly.com/library/view/terraform-up/9781492046899/</a></li></ul>]]></content:encoded>
<category domain="https://changhoi.kim/categories/etc/">etc</category>
<category domain="https://changhoi.kim/tags/terraform/">terraform</category>
<category domain="https://changhoi.kim/tags/iac/">iac</category>
<category domain="https://changhoi.kim/tags/devops/">devops</category>
<comments>https://changhoi.kim/posts/etc/terraform-for-newbie/#disqus_thread</comments>
</item>
<item>
<title>DynamoDB Internals (2) - DynamoDB</title>
<link>https://changhoi.kim/posts/database/dynamodb-internals-2/</link>
<guid>https://changhoi.kim/posts/database/dynamodb-internals-2/</guid>
<pubDate>Mon, 18 Apr 2022 15:00:00 GMT</pubDate>
<description><p><a href="/posts/database/dynamodb-internals-1/">지난 글</a>에서 DynamoDB를 지탱하는 큰 축인 Dynamo 시스템에 대해서 알아봤다. Dynamo 시스템은 DynamoDB가 등장하기 한참 전에 설계되었지만 이름에서 쉽게 알 수 있듯 굉장히 깊은 부분을 공유하고 있다. 그러나 DynamoDB는 관리형 인프라 서비스로 제공되는 만큼 사람들이 더 쉽고 범용적으로 사용할 수 있게 설계되었다. 구체적으로 어떻게 어떤 점이 다른지 알아보자.</p></description>
<content:encoded><![CDATA[<p><a href="/posts/database/dynamodb-internals-1/">지난 글</a>에서 DynamoDB를 지탱하는 큰 축인 Dynamo 시스템에 대해서 알아봤다. Dynamo 시스템은 DynamoDB가 등장하기 한참 전에 설계되었지만 이름에서 쉽게 알 수 있듯 굉장히 깊은 부분을 공유하고 있다. 그러나 DynamoDB는 관리형 인프라 서비스로 제공되는 만큼 사람들이 더 쉽고 범용적으로 사용할 수 있게 설계되었다. 구체적으로 어떻게 어떤 점이 다른지 알아보자.</p><span id="more"></span><blockquote><p>특정 <a href="https://www.youtube.com/watch?v=yvBR71D0nAQ">영상</a>에서 “Dynamo와 다르게 ~ “라고 하면서 특정 컴포넌트가 어떻게 다른지 설명하는 부분은 있지만, 공식적인 텍스트로 차이점에 대해 명시한 문서는 찾지 못했다. 다만 공부하면서 ‘이 부분은 이런 방법으로 변경했구나’ 같은 느낌을 많이 받을 수 있었는데, 이런 포인트에 집중해서 글을 쓰려 노력했다. 본인의 의견에 해당하는 경우 의견임을 명시했고 그 외는 Reference의 도큐먼트 또는 AWS re:Invent 영상을 참조했다.</p></blockquote><blockquote><p>글에서 DynamoDB와 Dynamo를 명확히 구분한다. Dynamo는 DynamoDB의 기반이 되는 분산 스토리지 시스템을 의미한다. 이전 글을 참고하자.</p></blockquote><h1 id="기본적인-이야기"><a href="#기본적인-이야기" class="headerlink" title="기본적인 이야기"></a>기본적인 이야기</h1><p>이 글을 읽고 있다면 DynamoDB의 기본적인 이야기에 대해서는 이미 알고 있을 확률이 높으므로, <a href="#Key-amp-Partition">다음 섹션</a>부터 읽어도 좋다.</p><h2 id="Item-Attribute-Primary-Key"><a href="#Item-Attribute-Primary-Key" class="headerlink" title="Item, Attribute, Primary Key"></a>Item, Attribute, Primary Key</h2><p>DynamoDB는 Key Value Storage NoSQL이다. Redis처럼 키값에 따라 데이터가 매칭이 되고, 해당 데이터의 스키마가 정해지지 않고 자유롭다. 하나의 데이터, 즉, RDB에서 하나의 로우라고 부를 수 있는 것을 DynamoDB에서는 아이템(Item)이라고 부른다. 이 아이템은 속성(Attribute)값으로 이루어져 있다.</p><p>Attribute가 가질 수 있는 값은 아래와 같다.</p><ul><li>Number (N)</li><li>String (S)</li><li>Boolean (BOOL)</li><li>Binary (B)</li><li>List (L)</li><li>Map (M)</li><li>String Set (SS)</li><li>Number Set (NS)</li><li>Binary Set (BS)</li></ul><p>Number, String, Boolean, Binary 같은 단순한 타입도 있지만, List, Set, Map과 같은 복합 타입도 존재한다. 이 값들을 읽고 쓰는 동작들은 모두 Atomic 하게 전달된 순서대로 동작한다.</p><blockquote><p>위 리스트의 괄호로 쳐진 값은 Attribute를 지정할 때 사용하는 코드이다. 직접 사용하다 보면 이해하기 쉽다.</p></blockquote><p>아이템의 스키마가 정해지지 않았다는 것은 아이템마다 자유롭게 속성값을 바꿀 수 있다는 것을 의미하는데, 예외가 존재한다. DynamoDB에서 테이블을 구성할 때 설정하는 Primary Key가 바로 그 예외이다. 이 값은 아이템을 식별하는 키 역할을 한다. 따라서 모든 아이템에서 Non-Nullable한 값이다.</p><p>이 키를 구성하는 방법은 기본적으로 2가지가 있다. 하나는 간단하게 <strong>Partition Key</strong>(파티션 키, PK, Hash Key)만 사용하는 것과 다른 하나는 PK와 함께 <strong>Sort Key</strong>(소트 키, SK, Range Key)를 함께 사용하는 것이다.</p><blockquote><p>이 글에서 PK는 모두 Partition Key를 의미하고, Primary Key는 줄여 쓰지 않았다.</p></blockquote><p>위 두 키를 제외하고는 쿼리를 할 수 있는 방법이 기본적으로는 없다. 만약 일반 Attribute를 가지고 필터링하려면 Scan을 사용해야 한다. 따라서 굉장히 디테일하게 애플리케이션에서 사용하는 쿼리 패턴에 대해 이해하고 키값을 설계해야 한다. DynamoDB를 설계하는 방법으로 가장 유명한 방법은 Single Table Design이 있다. 이는 과거에 한 번 <a href="/posts/database/dynamodb-single-table-design/">소개한 바</a> 있다.</p><blockquote><p>RDB에서도 키를 사용하지 않으면 Scan을 하는 것은 마찬가지이다. 하지만 RDB는 필요에 따라 인덱스를 쉽게 추가할 수 있지만 DynamoDB의 경우엔 제약 조건도 있고, 고민해야 할 포인트가 몇 가지 있다.</p></blockquote><h2 id="Secondary-Index"><a href="#Secondary-Index" class="headerlink" title="Secondary Index"></a>Secondary Index</h2><p>기본 키만으로 필요한 쿼리를 모두 할 수 없는 경우가 많기 때문에, 추가로 두 종류의 보조 인덱스를 지원한다.</p><ul><li>Local Secondary Index (LSI)</li><li>Global Secondary Index (GSI)</li></ul><p>LSI의 경우 테이블을 생성할 때 만드는 기본 인덱스를 가진 테이블(이하 베이스)과 동일한 PK를 사용해야 한다는 제약 조건이 있다. SK만을 설정할 수 있으며, 베이스가 만들어질 때만 설정할 수 있다. 테이블이 이미 운영 중인 경우엔 LSI를 추가할 수 없다. 장점으로는 GSI와 다르게 강력한 읽기 일관성을 제공한다는 점이다. 읽기 일관성에 대해 자세히 모른다면, 이하 글 내용에서 대략 알 수 있다.</p><p>LSI를 굳이 사용해야 하는 케이스는 별로 없다. 강력한 읽기 일관성이 제공되고, GSI보다 비교적 싸게 운영할 수 있다는 장점이 있지만, 파티셔닝에 제약 사항(PK값이 같은 아이템들의 크기의 합이 파티션의 최대 사이즈보다 커질 수 없다는 점)이 생기기도 하고, 생성 시점이 정해져 있다는 점 역시 큰 장애물이다. 공식 문서에서도 GSI 유즈 케이스가 더 많다고 설명하고 있다.</p><blockquote><p>강력한 읽기 일관성이 필요한 경우엔 DynamoDB 자체가 그다지 좋은 선택지가 아닐 수 있다. 일관성이 중요한 애플리케이션은 RDB를 사용하는 것이 일반적으로 더 좋은 선택일 것 같다.</p></blockquote><p>GSI의 경우 LSI와 다르게 파티셔닝의 제약도 붙지 않고, PK를 아예 별도로 설정할 수 있다. 또한 테이블이 생성된 이후에도 자유롭게 GSI를 설정할 수 있다. 다만 강력한 읽기 일관성을 제공하지 않고, 쓰기 읽기 용량을 새로 생성해야 하므로 추가적인 비용이 든다.</p><blockquote><p>LSI는 공짜라는 건 아니다. 베이스의 읽기 쓰기 용량을 공유하게 된다.</p></blockquote><h1 id="Key-Partition"><a href="#Key-Partition" class="headerlink" title="Key & Partition"></a>Key & Partition</h1><p>파티션 키는 테이블 안에서 아이템이 어떤 파티션에 속하는지를 결정하는 키다. 해시 함수를 가지고 키를 해시한 다음 해시값을 이용해 각 파티션으로 분할한다. SK는 파티션 안에서 아이템을 정렬하는 용도로 사용된다. 각 파티션의 최대 사이즈는 10GB로, 만약 파티션 키에 속하는 아이템이 10GB가 넘는다면 SK를 기준으로 파티션을 분할한다. 아이템은 400KB가 최대 사이즈이기 때문에 각 파티션마다 최소 25,000개 정도의 아이템을 담을 수 있다.</p><p><img src="/images/2022-04-19-dynamodb-internals-2/simple-get-item.png?style=centerme" alt="해시 함수를 통해 파티션을 선택"><br><small><a href="https://www.alexdebrie.com/posts/dynamodb-partitions/">이미지 출처</a></small></p><blockquote><p>SK가 없는 경우 PK가 같은 아이템이 테이블마다 하나씩 있을 수 있고, 아이템은 400KB 사이즈 제한이 있기 때문에 파티션 안에서 아이템 컬렉션(PK가 같은 아이템의 모음)이 10GB가 넘을 수 없다.</p></blockquote><blockquote><p>LSI가 설정된 테이블은 위에서 말한 것처럼 10GB 이상의 아이템 컬렉션을 유지할 수 없다. 이 값은 꽤 큰 값이고 PK를 애플리케이션에서 잘 나누면 해결할 수 있는 문제이지만, 아무튼 이런 찜찜한 제약이 생긴다.</p></blockquote><p>각 파티션은 읽기 용량과 쓰기 용량을 설정할 수 있다. On Demand 방식으로 설정한다면 들어오는 처리량을 적절히 받아주겠지만 가격이 비싸진다. 예측 불가능할 정도로 성장하는 서비스가 아니라면 보통 예측치를 기반으로 Read Capacity Unit(RCU), Write Capacity Unit(WCU)를 설정한다. 보내는 요청이 어떤 것인지(일관적인 읽기, 트랜잭션 쓰기 등)에 따라 다르지만, RCU 한 개는 기본 읽기의 경우 4KB 아이템 사이즈 기준 초당 2개의 아이템을 읽을 수 있고 WCU 한 개는 1KB 아이템을 초당 한 개 쓸 수 있다.</p><blockquote><p>On Demand 방식과 Auto Scaling에 대해서는 이 글에서 다루고 있지 않다. 간단히 설명하자면 Auto Scaling은 배달앱처럼 “점심시간에 높은 트래픽을 찍고 평시에는 낮다” 처럼 범위에 대한 이해가 있는 애플리케이션에 대해 해당 범위에 맞춰서 처리 용량을 늘이고 줄이는 설정이다. On Demand는 서비스가 예측 불가능한 속도로 성장하는데, 이를 문제 없이 받아주기 위해 넣는 설정이다. 둘의 차이가 명확히 있다. </p></blockquote><p>테이블마다 최대 RCU, WCU가 정해져 있어서 프로비저닝 당시 설정한 값에 따라 초기 파티셔닝이 결정된다. 읽기 용량은 파티션마다 최대 3,000 그리고 쓰기 용량은 파티션마다 1,000이 최대이다. 따라서 다음 식으로 초기 파티셔닝이 결정된다.</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">initial partition = (RCU / 3000) + (WCU / 1000)</span><br></pre></td></tr></table></figure><p>합친 값을 정수로 올림 한 값을 기본 파티션 수로 설정하고, 2개 이상의 파티션이 생성되면 Capacity Unit은 공평하게 분배된다. 예를 들어 RCU 3000, WCU 1000으로 설정하면 초기 파티션은 2개로 나눠지고, 각각 RCU 1500, WCU 500을 나눠 갖게 된다.</p><h1 id="Request-Router"><a href="#Request-Router" class="headerlink" title="Request Router"></a>Request Router</h1><p>Dynamo 논문에서는 클라이언트에서 직접 파티션을 선택해 요청을 보낼 수 있는 방법과 그 앞단에 로드밸런서를 통해 파티션을 찾아가는 방법에 관해 얘기한다. 특별히 어떻게 사용 중이라는 말은 없었지만, Client 쪽에서 어떤 스토리지 노드로 요청을 보내야 하는지 알고 있는 “Partition-Aware”를 사용하는 것으로 보였다. 하지만 DynamoDB에서는 모든 요청을 Request Router라고 불리는 중간자에 보낸다.</p><p><img src="/images/2022-04-19-dynamodb-internals-2/request-router.png?style=centerme" alt="Request Router"></p><p>Request Router는 두 가지 컴포넌트와 상호작용 후 실제 데이터가 있는 스토리지 노드에 접근하게 된다. </p><ul><li><strong>Authentication System</strong>: AWS 플랫폼에서 공통으로 사용되는 권한 확인 컴포넌트</li><li><strong>Partition Metadata System</strong>: 파티션의 리더 스토리지 노드를 관리해, Request Router가 요청을 보낼 노드를 선정</li></ul><p>Partition Metadata System 내부적으로 <strong>Auto Admin</strong>이라는 시스템이 동작하고 있다. 이는 관리형 서비스를 만들어주는 핵심적인 컴포넌트로, AWS에서는 이를 DynamoDB의 DBA라고 부른다고 한다. 구체적인 관리에 대해서는 조금 있다가 후술한다.</p><h1 id="Partition-Replication"><a href="#Partition-Replication" class="headerlink" title="Partition Replication"></a>Partition Replication</h1><p>해시 함수에 의해 정의되는 파티션은 각각 레플리카 셋(Replica Set)을 갖고 있다. 리더 노드와 두 개의 추가적인 복제 노드를 갖게 되는데, 이는 AWS의 가용 영역에 골고루 나눠서 운용된다. Dynamo 시스템에서는 Sloppy Quorum을 사용하고 어떤 스토리지 노드든 요청을 처음 받게 되는 coordinator로서 동작할 수 있다. DynamoDB에서는 이와 다르게 리더 노드를 선출한다. 이 과정은 <a href="https://ko.wikipedia.org/wiki/%ED%8C%A9%EC%86%8C%EC%8A%A4_(%EC%BB%B4%ED%93%A8%ED%84%B0_%EA%B3%BC%ED%95%99)">Paxos</a> 알고리즘으로 구성되어있다고 하는데, Paxos를 이번 글에서 다루고 있지는 않다.</p><blockquote><p>Paxos는 분산 시스템에서 특정 값이 합의되도록 하는 합의 알고리즘이다. DynamoDB에서는 파티션을 구성하는 세 개의 스토리지 노드들 사이에 “리더”가 어떤 노드인지를 합의하는 과정에 Paxos를 사용한다. Paxos는 스탠포드 대학생을 가르쳐도 이해하기 위해 1년이 걸렸다는 “이해 불가능한” 알고리즘이라는 슬픈 이야기가 있다. 이에 따라 “이해 가능한 합의 알고리즘”이라는 느낌으로 Raft가 나왔다고 한다.</p></blockquote><p>대략 설명하자면, 집단의 리더는 현재 정상 상태임을 다른 스토리지 노드에 하트 비트를 통해 알린다. 영상에서는 1.5초? 2초쯤 한 번씩 하트 비트를 보내고 있다고 하는데, 만약 다른 스토리지 노드가 이를 받지 못하면 새로운 리더를 선출하기 위해 다른 노드에 본인을 리더로 주장하는 정보를 보낸다. 이 요청을 다른 스토리지 노드가 동의하면 새로운 리더가 선출된다.</p><hr><p>어찌 됐든 리더가 있다는 사실이 중요한데, Dynamo와 다르게 쓰기 요청이 리더에게만 보내지기 때문이다. 리더는 항상 최신 데이터를 갖게 된다. 쓰기 요청을 받은 리더는 로컬 데이터를 수정하고 다른 두 레플리카에게 이 요청을 전파한다. 그리고 둘 중 하나의 성공 응답을 받으면 이 쓰기 요청이 성공했다고 응답한다.</p><p><img src="/images/2022-04-19-dynamodb-internals-2/dynamodbmultiaz.png?style=centerme" alt="여러 AZ에 나눠서 레플리카를 운영"></p><p>반면 읽기의 경우 세 개의 레플리카 중 하나에게 요청을 보낸다. 따라서 연속된 요청을 통해 값을 읽는다면 1/3 확률로 최신 데이터가 아닐 수도 있다. Dynamo와 마찬가지로 Eventual Consistency가 발생한다.</p><blockquote><p>Dynamo의 Quorum이 아예 적용 안 된 건 아니라는 것을 알 수 있다. 쓰기 쿼럼이 3개 중 2개 성공으로 설정된 것이나 다름없다. 위 설명은 모두 기본 읽기와 쓰기에 대한 설명이고, 트랜잭션 또는 강력한 읽기 일관성에 대한 옵션은 다르게 동작한다.</p></blockquote><h1 id="Storage-Node"><a href="#Storage-Node" class="headerlink" title="Storage Node"></a>Storage Node</h1><p>위에서 언급했지만, 스토리지 노드는 파티션을 구성하는 레플리카 셋이다. 내부적으로는 크게 두 가지 컴포넌트로 구성되어있다.</p><ul><li><strong>B Tree</strong>: 쿼리와 뮤테이션이 발생할 때 사용되는 자료구조</li><li><strong>Replication Log</strong>: 파티션에서 발생하는 모든 Mutation Log를 기록하는 시스템</li></ul><p>B 트리의 경우 우리가 흔히 아는 그 자료구조가 맞다. RDB에서 인덱스로 사용되는 트리 자료구조가 똑같이 사용된다. Replication Log도 다른 DB에서 레플리카 셋을 유지할 때 복구를 위해 사용되는 컴포넌트와 똑같다. 위에서 잠깐 말했던 Auto Admin이 이 복구 과정에 개입한다. Auto Admin은 레플리카 셋의 리더와 그 위치를 관리하고 스토리지 노드를 모니터링하는데 스토리지 노드에 장애가 발생해서 다운되면 새로운 스토리지 노드를 생성하고 다른 스토리지 노드의 Replication Log를 가지고 자료구조를 복사해간다. 새로운 스토리지 노드의 Replication Log가 성공적으로 B 트리에 적용되면 파티션에 합류할 자격이 생긴 것으로 보고 레플리카 셋에 합류시킨다.</p><h1 id="Secondary-Index-1"><a href="#Secondary-Index-1" class="headerlink" title="Secondary Index"></a>Secondary Index</h1><p>위 기본 내용에 보조 인덱스에 대한 이야기를 짧게 했는데 이 구조가 어떻게 되어있는지 확인해보자. 프로세스는 일반적인 테이블과 비슷하다. PK를 해시 해서 각 파티션으로 나눠 보낸다. 다른 점은 보조 인덱스는 베이스 테이블과 독립적으로 파티션을 구성한다는 점이다. 그리고 테이블 내에서 보조 인덱스에 해당하는 Attribute이 수정되면 이 작업은 <strong>Log Propagator</strong>라고 하는 컴포넌트에 의해 보조 인덱스 파티션의 리더 스토리지 노드에 전파된다.</p><p><img src="/images/2022-04-19-dynamodb-internals-2/secondary-index.png?style=centerme" alt="보조 인덱스와 Log Propagator"></p><p>Log Propagator는 스토리지 노드의 Replication Log를 바라보고 있다가 보조 인덱스 수정이 발생하면 Request Router가 베이스 테이블에 요청하듯 보조 인덱스 파티션에게 변경을 요청하게 된다. 이렇게 비동기적으로 전파되는 구조이므로 보조 인덱스의 Eventual Consistency는 필수적이다.</p><hr><p>해시 기반으로 샤딩 된 데이터를 수정할 때 원래 위치한 스토리지 노드에서 데이터를 삭제하고 해시 된 위치에 맞는 스토리지 노드에 새로 쓰는 작업을 해야 하기 때문에 쓰기 작업이 생각보다는 무겁다.</p><p><img src="/images/2022-04-19-dynamodb-internals-2/secondary-index.png?style=centerme" alt="실제 업데이트는 두 개의 파티션을 수정한다"></p><p>파티션에 뮤테이션을 만드는 작업은 레플리카 셋 3개에 동일한 작업을 하는 것과 같기 때문에 베이스 파티션 레플리카 셋, 보조 인덱스에서 삭제될 파티션 레플리카 셋, 보조 인덱스에 추가될 파티션 레플리카 셋에 뮤테이션이 발생한다. 즉 하나의 보조 인덱스를 수정하는 작업은 9개의 스토리지 노드의 수정을 가져온다.</p><blockquote><p>따라서 Secondary Index의 수정은 자주 발생하지 않는 것이 좋다. 이는 샤딩을 할 때 기준이 되는 필드가 자주 수정이 발생하면 안 된다는 얘기와 같다.</p></blockquote><blockquote><p>영상에서 별다른 설명은 없었지만, 위 설명은 GSI에 해당하는 설명일 것이라 생각한다. LSI는 테이블이 생성될 때 같이 생성되어야 한다는 점, PK는 베이스 테이블과 같아야 한다는 점, 강력한 읽기 일관성이 제공된다는 점, 베이스 테이블의 RCU와 WCU를 공유한다는 점, 그리고 이름에서 알 수 있듯, 베이스 테이블과 같은 파티션을 공유하는 것 같다.</p></blockquote><h1 id="Provisioning-Adaptive-Capacity"><a href="#Provisioning-Adaptive-Capacity" class="headerlink" title="Provisioning & Adaptive Capacity"></a>Provisioning & Adaptive Capacity</h1><p>처리 용량 프로비저닝에 대해서는 위에 파티셔닝에 대해 설명하면서 함께 얘기했다. 이 섹션에서는 조금 구체적인 동작 방식에 대해서 짧게 설명한다.</p><p>RCU, WCU는 쿼터를 정할 때 흔히 사용되는 Token Bucket 알고리즘으로 구성되어있다. 매초 RCU 수만큼 토큰이 버킷에 쌓이는데 버킷의 총용량은 설정한 RCU의 300배 정도이다. 따라서 아무것도 안 하면 5분 정도는 토큰이 쌓인다. 다만 버킷의 용량을 초과하면 토큰은 버려진다. 이런 구조로 트래픽이 치솟는 상황에서도 일시적으로나마 스로틀링이 생기는 것을 막아준다.</p><p>위에서 잠깐 언급한 적 있는데 RCU, WCU는 파티션에 골고루 분산된다. 만약 파티션이 3개이고 RCU가 300이라면 각 파티션은 100씩 RCU를 나눠 갖는다. 이렇게 나눠진 파티션에서 실제 운영할 때 아래 이미지처럼 25 RCU, 150 RCU, 50 RCU를 사용한다고 가정해보자.</p><p><img src="/images/2022-04-19-dynamodb-internals-2/unbalanced-load-1.png?style=centerme" alt="Hot Partition이 발생한 상황"></p><p>위와 같은 상황에서 두 번째 파티션의 RCU가 50만큼 부족하고 나머지는 남는 상황이다. 총합은 75 RCU 만큼 남기 때문에 위 요청량이 잘 처리되어야 하지만 데이터가 고르게 분산되지 않아 Hot Partition이 생길 수 있다.</p><p>AWS에서는 이 문제를 해결하고자 Adaptive Capacity를 도입했다. Adaptive Capacity는 Adaptive Multiplier라는 실숫값을 RCU, WCU에 곱해 일시적으로 처리 용량을 수정해주는 방식이다. Adaptive Multiplier는 피드백 루프를 돌며 지속해서 조절되는데, 이런 조절 루프를 돌리는 컴포넌트를 <a href="https://ko.wikipedia.org/wiki/PID_%EC%A0%9C%EC%96%B4%EA%B8%B0">PID Controller</a>라고 한다. DynamoDB에서 PID Controller는 인풋으로 소비된 용량, 프로비전된 용량, 스로틀링 속도, 현재 Multiplier 값을 받아서 결과로 새로운 Multiplier 값을 넘겨준다.</p><p><img src="/images/2022-04-19-dynamodb-internals-2/unbalanced-load-2.png?style=centerme" alt="Adaptive Capacity 적용 후"></p><p>위 예시에서는 1.5 정도 값이 Multiplier로 설정되면 스로틀링이 해결된다. 하지만 처리 용량은 테이블에 적용되는 개념이기 때문에 파티션마다 부족한 값에 대해 Multiplier를 곱하는 구조가 아니고, 테이블의 전체 처리 용량에 곱해진다. 따라서 위 예시에서는 총 150만큼 처리 용량 초과가 발생한다. 그러나 이 상태는 잠깐 지속되고 PID Controller에 의해 정상화된다. 일시적으로 스로틀링을 해결해주는 장치라고 볼 수 있다.</p><blockquote><p>즉, Adaptive Capacity는 Hot Partition을 완전히 해결해주는 장치는 아니다. 애초에 Hot Partition이 나오지 않도록 데이터 분산을 잘 만들어야 하고 특별히 많은 요청을 처리해야 하는 파티션이 있다면 아예 별도로 테이블을 관리하는 것도 좋은 방법이다.</p></blockquote><hr><p>이번 글은 DynamoDB를 가장 기본적으로 사용하는 상황에서 거치게 되는 컴포넌트 구성에 대해 작성했다. 특히 파티셔닝 얘기와 프로비저닝 얘기는 우리가 <a href="/posts/database/dynamodb-single-table-design/">어떻게 키를 설계해야 할지</a> 대략적인 감을 잡을 수 있게 해준다. 이 외에도 설명하지 않은 Auto Scaling, Stream, 강력한 읽기 일관성, 트랜잭션 등 여러 기능이 DynamoDB에 있다. 특수한 유즈 케이스가 있다면 따로 AWS 문서를 확인해보자.</p><h1 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h1><ul><li><a href="https://en.wikipedia.org/wiki/Amazon_DynamoDB">https://en.wikipedia.org/wiki/Amazon_DynamoDB</a></li><li><a href="https://www.alexdebrie.com/posts/dynamodb-partitions/">https://www.alexdebrie.com/posts/dynamodb-partitions/</a></li><li><a href="https://dzone.com/articles/partitioning-behavior-of-dynamodb">https://dzone.com/articles/partitioning-behavior-of-dynamodb</a></li><li><a href="https://www.allthingsdistributed.com/2012/01/amazon-dynamodb.html">https://www.allthingsdistributed.com/2012/01/amazon-dynamodb.html</a></li><li><a href="https://docs.aws.amazon.com/ko_kr/amazondynamodb/latest/developerguide/ServiceQuotas.html#default-limits-throughput-capacity-modes">https://docs.aws.amazon.com/ko_kr/amazondynamodb/latest/developerguide/ServiceQuotas.html#default-limits-throughput-capacity-modes</a></li><li><a href="https://www.dynamodbguide.com/">https://www.dynamodbguide.com</a></li><li><a href="https://www.youtube.com/watch?v=yvBR71D0nAQ">AWS re:Invent 2018: Amazon DynamoDB Under the Hood: How We Built a Hyper-Scale Database (DAT321)</a></li><li><a href="https://www.youtube.com/watch?v=HaEPXoXVf2k">AWS re:Invent 2018: Amazon DynamoDB Deep Dive: Advanced Design Patterns for DynamoDB (DAT401)</a></li></ul>]]></content:encoded>
<category domain="https://changhoi.kim/categories/database/">database</category>
<category domain="https://changhoi.kim/tags/serverless/">serverless</category>
<category domain="https://changhoi.kim/tags/dynamodb/">dynamodb</category>
<comments>https://changhoi.kim/posts/database/dynamodb-internals-2/#disqus_thread</comments>
</item>
<item>
<title>2021년, 개발 3년 차 회고</title>
<link>https://changhoi.kim/posts/logs/20220417/</link>
<guid>https://changhoi.kim/posts/logs/20220417/</guid>
<pubDate>Sat, 16 Apr 2022 15:00:00 GMT</pubDate>
<description><p>개발을 시작했다고 볼만한 시점부터 2022년 01월 01일 기준으로 3년이 채워졌다. 시작도 2019년 01월 01일부터 했기 때문에 3년을 딱 맞게 채웠다. 조금 늦은 감이 있지만 생일 기념으로, 그리고 매년 하는 일인 만큼 지난해 무슨 일들이 가장 유의미했는지 정리해봤다. </p></description>
<content:encoded><![CDATA[<p>개발을 시작했다고 볼만한 시점부터 2022년 01월 01일 기준으로 3년이 채워졌다. 시작도 2019년 01월 01일부터 했기 때문에 3년을 딱 맞게 채웠다. 조금 늦은 감이 있지만 생일 기념으로, 그리고 매년 하는 일인 만큼 지난해 무슨 일들이 가장 유의미했는지 정리해봤다. </p><span id="more"></span><blockquote><p>다른 개발자의 회고 내용에 기대하듯, 이 주니어 개발자는 무엇을 하며 1년을 보냈을까? 를 기대하며 이 글을 읽는다면 조금 실망하실 수 있습니다. 개발자보다는 개인적인 회고에 가깝습니다. 그러나 저는 개발자입니다.</p></blockquote><h1 id="졸업-전-창업-해보기"><a href="#졸업-전-창업-해보기" class="headerlink" title="졸업 전 창업 해보기"></a>졸업 전 창업 해보기</h1><p><img src="/images/2022-04-17-20220417/spacesuite.svg?style=centerme" alt="우주 갈꺼니까"></p><p>예전부터 만들어보고 싶었던 서비스가 있었다. 프로젝트 관리 SaaS를 만들어보고 싶었다. 간단히 설명하자면 지금 Jira가 충분히 만족스럽지 않고, 태스크 사이의 관계들을 더 잘 파악하도록 바뀌었으면 좋겠다는 생각을 자주 해왔다. 나는 그 방법이 트리 구조에 있으리라 생각했고, 뭐 비슷한 방법으로 문제를 풀어보려고 노력했다.</p><p>결과적으로는 실패를 경험했고, 그 과정은 이전에 “<a href="https://changhoi.github.io/posts/logs/20210501/">4개월간 서비스 개발 후기 (사업화 실패하는 데 성공)</a>“ 이라는 글에 배운 점과 느낀 점을 나름 디테일하게 서술했다.</p><p>과정에서 많은 것들을 배우고 얻었지만, 사람들을 많이 얻은 것 같아서 좋다. 팀원들을 포함해서 내가 만들고자 하는 것을 응원해주고 같이 고민해준 사람들도 있고… 일단 교수님하고 친해져서 좋았다. 교수님은 서비스 자체에 대해서는 잘 모르셨지만, 공간을 빌려주시고 최대한 도움을 주시려고 많이 노력해주셨다. 수업 중에 나서지도 않는 학생이어서 이름도 잘 기억 못하실 법한데 이렇게 도움을 주셔서 정말 감사했다.</p><p>구체적인 내용들은 이미 다른 회고에서 작성했으니 이 정도로 마무리…</p><h1 id="취준"><a href="#취준" class="headerlink" title="취준"></a>취준</h1><p><img src="/images/2022-04-17-20220417/justdoit.JPG?style=centerme" alt="그냥 하는 거지"></p><p>사실 취준 과정이 엄청 고통스럽거나 힘들지는 않았다. 취업할 자신도 있었고 취업 준비 과정이 딱히 그전 생활과 다르지도 않았다. 물론 CS에 집중해서 공부했다든지, 포트폴리오 정리하고 자기소개서를 쓴다든지 하는 것들은 있었지만, 그전에도 개발 공부하고 자유시간 갖고 반복하던 삶을 살았기 때문에 특별히 다르다는 느낌은 없었다.</p><h2 id="CS-스터디-멘토링"><a href="#CS-스터디-멘토링" class="headerlink" title="CS 스터디, 멘토링"></a>CS 스터디, 멘토링</h2><p>스터디를 2개 열어서 진행했는데, 하나는 CS 전반적인 내용에 대한 스터디였고 하나는 시스템 프로그래밍을 주제로 한 멘토링이었다.</p><h3 id="CS-스터디"><a href="#CS-스터디" class="headerlink" title="CS 스터디"></a>CS 스터디</h3><p>과거에 취업 준비를 한다고 생각하면 Github에 올라가 있는 CS 토막 상식 같은 링크를 보곤 했다. 물론 훌륭한 레포지토리들이라 보면서 끄덕끄덕 기억을 살려내곤 했는데, 뭔가 CS에 대해 잘 알게 되었다는 느낌을 받지는 못했다. 그래서 학교에서 사용했던 교과서를 다시 보면서 CS를 정리하기로 했다.</p><p>교과서 스터디는 워낙 양이 많아서 취준을 하던 다른 개발자 친구, 이미 개발하는 친구와 함께 스터디를 진행했다. <a href="https://www.notion.so/changhoi/Deep-CS-a36ac1380d4843c69867b184b36a50c3">Notion에 정리</a>하기로 하고, 학부에서 배운 수준의 교과서 레벨을 커버한다는 느낌으로 OS와 네트워크까지 잘 정리했다. DB, 자료구조까지 정리했으면 좋았을 것 같은데, 친구들이 바빠지고 취업도 되고 하면서 흐지부지 정리됐다. DB는 따로 학습하긴 했으나 정리되지는 않았고 조금 깊숙한 얘기들에 관해 공부해볼 계획이다.</p><p>일단 그래도 OS와 네트워크에 대해서는 배운 내용들이 잘 정리된 느낌이었고, 실제로 면접에 큰 도움이 되었다. 구멍들이 느껴지긴 하지만, 차차 해결해가면 되겠지.</p><h3 id="멘토링"><a href="#멘토링" class="headerlink" title="멘토링"></a>멘토링</h3><p>학교에 연이 깊은 듯 얕은 듯 한 개발 동아리가 있다. 이 개발 동아리에서는 매 학기 멘토를 모집한다. 멘토는 주제를 선정하고 멘티들이 이 주제에 투표하는 구조이다. 평소에 리눅스 시스템을 조금 잘 써보고 싶다는 생각을 항상 하고 있어서 파일 시스템을 다루거나 소켓을 다루는 등의 프로그래밍에 대해 알아본 적이 있었다. 이런 걸 학습하는 주제가 Unix 시스템 프로그래밍이라는 것을 알게 되었고, 그 당시 Go에 흥미가 많아서 Go를 가지고 시스템 프로그래밍하는 내용으로 수업을 준비했다.</p><p>해당 동아리는 비전공자 또는 초보인 분들이 많은 특성이 있어서 관심을 끌기에는 조금 어려운 주제가 아닐까 하는 생각이 들었다. 다행히도 조금 고인물 분들이 나타나 스터디를 재밌게 끌어주셨다.</p><p>일단 교재는 <a href="https://www.oreilly.com/library/view/go-systems-programming/9781787125643/">Go System Programming</a>이라는 책을 사용했다. 시스템 프로그래밍의 이론적인 이야기라든지, 유닉스 시스템이 파일시스템을 구성하고 있는지 등 자세한 설명이 부족한 책이었다. 다만 Go를 가지고 시스템 프로그래밍을 어떻게 할 수 있는지 등 조금 실전적인 이야기가 많이 담긴 책이었다.</p><p>조금 이론적인 이야기도 궁금해서 <a href="https://www.oreilly.com/library/view/advanced-programming-in/9780321638014/">Advanced Programming in the Unix Environment</a> 책을 같이 참조하면서 공부했다. 꼼꼼히 읽어보면 좋을 것 같은데, 스터디 진행이 꽤 빨라서 꼼꼼하게 읽어보지는 못했다.</p><p>스터디 진행은 멘토가 Go언어 자체에 대한, 또는 Go를 통해 어떻게 시스템 프로그래밍을 할 수 있는지 설명해주는 시간과 멘토가 정해준 주제에 대해 멘티들이 특정 주제에 대한 발표를 준비해오고 이를 세션 형식으로 발표하는 형태로 진행되었다. 준비 과정이 진짜 어려웠는데, 사람들이 잘 따라와 주고 발표 준비도 잘 해주셔서 재밌게 마무리 지을 수 있었다.</p><p>취준을 위해 했다고 할 수는 없어서 분류가 조금 애매하긴 한데, 아무튼 시스템 프로그래밍도 CS의 한 부분이기 때문에 취업에 도움이 안 되었다고 볼 수도 없을 것 같다.</p><h2 id="알고리즘"><a href="#알고리즘" class="headerlink" title="알고리즘"></a>알고리즘</h2><p>개발 공부 중에 추가된 것이라고 하면 알고리즘이 있을 것 같다. 알고리즘을 잘하는 편도 아니고, 경험이 많이 있는 편도 아니었다. 알고리즘 자체에 대한 경험을 쌓으려고 하루에 2, 3문제씩 풀었던 것 같다.</p><p><img src="/images/2022-04-17-20220417/boj.png?style=centerme" alt="백준... 한 문제만 더 풀어야겠다"><br><img src="/images/2022-04-17-20220417/programmers.png?style=centerme" alt="프로그래머스"></p><p>대충 200문제 넘어가고 나니까 알고리즘은 어떻게 푸는 거구나 느낌이 생겼던 것 같다. 물론 지금도 골드 1, 2만 만나면 좌절을 맛보고 있다. 그래도 어느 정도 코딩 테스트라고 하는 부분에서 막히던 걸 많이 해결해줬다.</p><p>알고리즘을 처음 공부할 때는 카테고리별로 공부했던 것 같다. 예를 들어서 Brute Force 문제, Greedy 문제, 이분 탐색, DP 이런 식으로 정해진 카테고리 문제들을 풀면서 이런 경우는 이 알고리즘이 도입되는 거구나? 이런 느낌으로 알고리즘을 풀었던 것 같다. 이 부분이 어느 정도 진행되고 나서부터는 그냥 프로그래머스 연습문제 2, 3단계에 있는 걸 쭉 풀었다.</p><p>알고리즘을 공부하면서 느낀 점은 알고리즘이 코딩하는 것과 크게 다른가? 라는 점이다. 이 부분은 아주 갑론을박 말이 많다. “알고리즘을 잘 못 한다고 개발을 못 하는 건 아니다.” 라든지 “사실 개발하면서 알고리즘을 제대로 써본 적이 없다.”든지…</p><p>본인은 알고리즘이 개발과 아주 유관하다고 생각하는 편이다. 그전에는 알고리즘은 아주 다른 영역이라고 생각했지만, 사실 기본적인 문제들을 해결하는 능력은 논리적 사고 능력일 뿐이다. 물론 굉장히 스킬을 가미하며 알고리즘을 해결해야 하는 경우는 조금 현실성 없다고 느낄 수도 있지만, 논리적 사고 연습이라는 측면에서 알고리즘을 연습하는 것이 개발을 더 잘하게 만들어준다는 느낌을 받는다. 따라서 취업하고도 알고리즘을 푸는 걸 취미 삼아 하나씩 하는 것도 좋을 것 같다는 생각이 들었다.</p><h2 id="프로젝트"><a href="#프로젝트" class="headerlink" title="프로젝트"></a>프로젝트</h2><p>프로젝트는 취준 목적으로 했다기보다는 개발을 안 하는 삶이 조금 무료한 감이 있어서 시작했다. 프로젝트를 하는 사회인 동아리? 라고 해야 할지… 아무튼 그런 곳에서 프로젝트를 진행했다. 초반에 프로젝트 진행이 많이 루즈해졌다. 이유는 진짜 창업 아이템을 고르듯 아이템에 대해 조사도 하고 기능 명세도 문서화하고 기본적인 컴포넌트들에 대한 합의 과정이 진짜 길어서 그랬다. 사실 이런 단계를 모두 거치는 사이드 프로젝트는 해본 적이 없지만, PO 역할을 해주시는 분이 요런 저런 걸 해보고 싶어 하시는 것 같기도 하고 나름 재밌어서 쭉 같이 진행했던 것 같다. 루즈한 출발 자체는 아쉽지만, 그 사이 과정에서 배운 점들이 많이 있어서 그래도 이 부분은 긍정적이었다.</p><p>개발에서는 아쉬운 점이 있었다. 개발 스택을 정하는 과정에서 같이 개발하는 팀원에게 많은 부분을 양보해드렸다. 팀원분도 취준 중이신데 목표하시던 회사 스택을 사용해보고 싶다고 하셔서 최근까지 더 이상 잡고 있지 않던 타입스크립트와 NestJS를 사용했고 MySQL을 사용했다. 사실 개발 스택은 별로 신경 쓰지 않기 때문에 상관은 없었지만 배울 수 있는 포인트가 줄었다는 점이 사이드 프로젝트의 매력을 조금 반감되게 했다.</p><blockquote><p>심지어 서버 코드에 거의 기여한 바가 없으시다. 스켈레톤을 작성하는 것 외 서비스 로직이 머지된 적이 없었다. 이 부분은 좀 많이 아쉬웠다.</p></blockquote><p>그러나 진짜 문제는… 사이드 프로젝트가 더 이상 진행되지 않는다는 것이었다. 물론 사이드 프로젝트는 그야말로 사이드니까 원래 하던 일이 있다면 우선순위가 뒤로 밀리는 경우가 허다하다. 그래도 뭔가 우리 팀이 흐지부지 프로젝트를 정지한 이유를 생각해봤는데 다음과 같은 이유가 있었던 것 같다.</p><ul><li><p><strong>주기적인 회의를 안 함</strong>: 주기적인 회의를 해도 했던 작업이 많이 없으니 회의 시간이 짧고 그걸 위해 약속을 취소해야 하는 등 부담이 좀 있다는 이유로 주기적 회의를 없앴다. 사실 이게 가장 큰 실패 원인이 아닐까 싶은데, 회의 주기가 뭔가 개발 기능을 마무리 짓는 주기로 동작했기 때문에 이 부분이 없어지면서 더 안 하게 된 것 같다. 그리고 애초에 이 정도의 강제성조차 안 가지고 사이드 프로젝트를 진행할 수가 없다.</p></li><li><p><strong>작업에 정해진 시간이 없음</strong>: 언제까지는 해요! 라는 작업 시간이 없어서 무기한으로 미뤄진다. 정말 미친 듯이 바쁜 사람은 애초에 사이드를 할 생각을 안 한다는 가정하에, 일상 업무에 일반적인 시간 소비를 하는 사람들 기준으로 일주일 내내 일정 시간을 할애하도록 스케줄링하는 것이 불가능하지 않다. 강제적으로 3시간! 이런 식으로 정할 수 있는데, 각자 정한 이 시간을 기준으로 어떤 작업은 언제까지 마무리되어야 한다는 약속이 필요했던 것 같다.</p></li></ul><p>원래 사이드 프로젝트 완성하기란 정말 어렵다는 것을 알고 있다. 그렇지만 이번 프로젝트는 진짜 사이드 팀 프로젝트에 대해 굉장한 회의를 느끼게 했다. 그냥 혼자 하는 것보다 못한 경험을 했다고 느꼈다. 앞으로 사이드 프로젝트는 진짜 친해서 강제성을 좀 더 부여할 수 있거나, 사이드 프로젝트에 진~심인 디자이너 + 프론트 개발자와 하거나 혼자 하거나 해야겠다고 생각했다.</p><h1 id="인턴"><a href="#인턴" class="headerlink" title="인턴"></a>인턴</h1><p><img src="/images/2022-04-17-20220417/feature.svg?style=centerme" alt="그건 버그가 아니랍니다"></p><p>길다면 길고 짧다면 짧은 듯한 취준 시간을 보내고 평소에 정말 가서 일해보고 싶던 기업의 플랫폼 개발 인턴으로 들어가게 됐다. 회사에서 크게 두 가지를 했던 것 같은데, 하나는 인턴 과제와 같은 서비스를 개발하고 이를 배포하는 과정이었고, 다른 하나는 팀에서 사용하고 있는 기술을 학습하는 과정이었다. 두 가지 모두 본인에게 큰 의미가 있었고 개발자로서는 굉장한 퀀텀 점프를 했던 경험이었다.</p><h2 id="기술-학습에-대해"><a href="#기술-학습에-대해" class="headerlink" title="기술 학습에 대해"></a>기술 학습에 대해</h2><p>회사에서 사용하고 있던 기술들을 내가 잘 알고 있지는 않았다. gRPC, Go를 공통으로 사용하고 있던 조직이었기 때문에 위 기술들을 학습하고, 전반적으로 팀이 사용하고 있던 NoSQL, RDB에 대해 학습했다. 이 학습의 가장 큰 포인트는 “<strong>깊숙한 이해</strong>“ 였다. 단순히 특징을 검색해서 나오는 얘기들 말고, 예를 들어서 “Go는 동시성 프로그래밍에 특화된 언어 구조를 가지고 있습니다.”라는 문구는 쉽게 찾아볼 수 있다. 그렇다면 “왜 특화되었다고 표현되어있지?”라는 질문이 나온다. 어느 겉핥는 학습을 해보면 그 질문에 대한 답변은 “<code>go</code>, <code>channel</code> 키워드와 여러 <code>sync</code> 패키지 등으로 개발자들이 동시성을 구성하면서 고민해야 하는 여러 부분을 해결해주고 있기 때문이다”라는 것을 알 수 있다. 그렇다면 그 고민이 구체적으로 무엇이고 어떻게 해결해주고 있는 것일지 구현체 레벨까지의 궁금증(코드 레벨까지는 아니고, 보다 추상적인 레벨에서)을 갖게 된다. 완전히 어떠한 특징에 대해 깊게 이해한 다음, 그렇다면 우리는 이 기술을 어떻게 활용할 수 있을까? 어떤 옵션들이 우리 상황에 더 잘 맞는지, 그리고 그 이유는 무엇일지? 를 고민하는 순서로 여러 기술들을 학습했던 것 같다.</p><p><img src="/images/2022-04-17-20220417/step.png?style=centerme" alt="기술 학습 단계"></p><p>사실 과거에 상당히 많은 부분이 경험을 통해 얻을 수 있는 영역이라고 생각하던 부분이 있었다. 어느 정도 학습하고 나면, “여기부터는 경험을 통해 알 수 있는 영역”이라고 생각하는 부분들을 마주한다. 물론 그 부분이 없다는 것은 아닌데, 이 과정을 거치면서 상당 부분은 이런 깊숙한 이해를 통해 많이 해결할 수 있다고 생각하게 되었다.</p><hr><p>위에서는 조금 비중 있게 쓰진 않았지만, 사실 “깊숙한 이해”의 근본적인 목표는 “우리는 어떻게 사용할지”이다. 그래서 우리는 “어떻게 쓸 수 있을까?”를 조금 더 중요하게 본다. 식사를 기다리면서 시니어분에게 “대규모 서비스를 구성해본 경험이 없어서 ‘어떻게 쓸 수 있을까?’에 대한 정보는 떠올리기 어려운 것 같다”라고 말씀드린 적 있는데, 시니어분이 “기술적으로 까다로운 특정 상황에서 경험으로 어떤 기술을 사용하려는 사람은 사실 오히려 더 소수이다. 어떻게 쓰지?를 알기 위해서 깊숙한 이해가 요구되는 것”이라고 말씀하셨다. 깊숙한 이해가 기술을 대하는 핵심이지만, 그것이 핵심인 이유는 근본적으로 어떻게 쓸지를 보다 잘 알기 위해서이다.</p><p>잘 정리해서 쓴지 모르겠지만, 아무튼 이 공부 과정을 통해서 기술을 바라보고 대하는 태도, 학습 방법, 문제를 해결하기 위해 지나가야 하는 기술적 탐구 과정에 대한 에티튜드가 바람직한 방향으로 박힌 것 같아, 인턴이 끝나고 나서도 참 기분이 좋았다.</p><blockquote><p>회사에서 학습했던 기술들은 조금 더 포괄적이거나 깊게 다시 학습해 정리하고 있다.</p></blockquote><h2 id="프로젝트에-대해"><a href="#프로젝트에-대해" class="headerlink" title="프로젝트에 대해"></a>프로젝트에 대해</h2><p>회사에서 했던 프로젝트는 <a href="https://medium.com/daangn/%EC%98%88%EC%B8%A1-%EA%B0%80%EB%8A%A5%ED%95%9C-%EB%8C%80%EA%B7%9C%EB%AA%A8-%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B0%9C%EB%B0%9C%ED%95%98%EA%B8%B0-a33e2f3cef88">이 링크</a>에서 구체적으로 확인할 수 있다. 과정은 위 링크에서 잘 정리되어있기 때문에 위 과정으로 뭘 배웠나에 대해서 간단하게 정리해보려고 한다.</p><p>일단 위 프로젝트에서 경험한 핵심은 “예측 가능한 애플리케이션”이다. 예측 가능하다는 것은 내가 만든 서비스가 얼마만큼의 성능을 낼 수 있는 앱인지를 말한다. 서비스에 어떤 부분에 노출이 되는지 또는 어떤 서비스 뒤에서 돌아가는 플랫폼 서비스인지, 그래서 얼마만큼의 TPS가 나올지를 알 수 있다면 약간의 버퍼를 두고 그만큼을 해결할 수 있는 TPS가 뽑히도록 앱을 설계하거나, 그것이 현실적으로 불가능하다면 지금 만든 앱이 몇 개의 서버로 부하를 분산해야 하는지를 아는 것이 예측 가능한 애플리케이션이라고 생각한다.</p><p>일단 위에서 말한 것처럼 어느 정도 규모가 있는 애플리케이션에서는 이런 작업이 필수적이기 때문에 서비스를 개발하는 일련의 과정을 학습했다는 것 자체를 배웠다. 그리고 최대한의 성능을 위해서 애플리케이션의 성능 테스트를 진행하고 병목 지점을 찾아서 고쳐서 다시 배포하는 과정에서 숫자적인 감각이 조금 생겼던 것 같다. 예를 들어서 어떤 서비스인지, 하드웨어 성능이 어떤지에 차이가 있지만 I/O 작업이 추가될 때와 아닐 때 어느 정도의 성능이 나오는 게 일반적인지라든지, 어떤 수치를 보고 어디서 생기는 병목인지 예측하는 감이 생겼던 것 같다. 일시적이지 않으려면 뭐 추가적인 경험을 해봐야 할 것 같은데, 일단은 이러한 경험을 한차례 했다.</p><h1 id="졸업"><a href="#졸업" class="headerlink" title="졸업"></a>졸업</h1><p>학교를 참 열심히 다녔는데, 학교 수업을 열심히 들었다기 보다는 해보고 싶었던 건 거의다 한 것 같다.</p><p><img src="/images/2022-04-17-20220417/achieve.png?style=centerme" alt="교환 학생, 4.5 아쉽다"></p><p>사실 학교 그 자체에 대해서는 좀 회의적이기도 하고 쓸 말도 없다. 졸업 당일에는 아쉬운 감정이 생길 줄 알았는데 그런 거 없고 그냥 행복하게 졸업했다. 지금 약 2, 3개월 지나고 나니까 실감이 나는듯하다. 그러나 여전히 아쉽지는 않다.</p><p>졸업식에는 다행히 친구 몇 명과 같이 사진 찍고 즐겁게 졸업식을 보낼 수 있었다. 굿.</p><h1 id="회고-모임"><a href="#회고-모임" class="headerlink" title="회고 모임"></a>회고 모임</h1><p>2021년 초 “왕각코”라는 왕십리에서 각자 코딩 모임을 했다. 친한 동생 한 명하고 같이 모여서 각자 코딩하거나, 책을 읽거나 그냥 만나서 할 거 하는 모임이었다. 둘이서 하니까 모임이라고 부르기 뭐하긴 했는데, 꾸준히 이 모임에 누군가를 초대해왔었다. 공통으로 알고 있는 여러 명을 초대했는데, 그중 한 분이 정기적으로 이 모임에 나오게 되었고 이 모임의 아이덴티티를 재설정했다.</p><p>재설정된 아이덴티티는 회고 모임이었고, 이름도 “우린 남이니까”라는 이름으로 바꾸었다. 우린 남이니까라는 말은 파카라는 방송인이 과거에 자주 하던 소리였는데, 모두 파카 방송을 재밌게 보는 사람들이기도 하고, 회고 후 피드백이나 조언을 가감 없이 전달할 수 있는 사람들이라는 의미를 붙여서 이름을 정했다.</p><p>이 회고 모임은 지금은 4명이 되었고 꽤 유쾌한 회고 모임이 되었다. 회고 모임에는 아주 각자 영역에서 열심히 살고 계시는 분들이 함께하고 있는데, 조금씩 좋아하는 것이나 생각하는 방향이 차이가 있으면서도 남들의 의견을 폭넓게 수용하고 자신들의 것으로 만드시는 모습을 보면 본인도 아주 삶의 동기부여가 된다. 또 일주일마다 무엇을 했는지 어떤 것이 아쉬웠고 어떤 것이 좋았는지, 다음 주는 어떤 것을 할지를 계획하는 것들이 인생을 막사는 것으로부터 어느 정도 방지턱 역할을 해준다.</p><blockquote><p>막 사는 것을 막을 수는 없다. 그건 행복하다. 그리고 지금만 누릴 수 있는 것 같다. 그런데 그렇게 살고 난 주에 회고를 읽으면 자괴감도 들고 다른 분들이 훌륭하게 일주일을 마무리 지은 것을 보면서 반성하게 되는 효과가 있다.</p></blockquote><p>말이 잘 통하고, 자신의 가치관에 대해 굳이 남을 동의하게 만들려는 분들이 아닌 사람들과 이런 회고 활동을 하는 것은 정말 추천할만하다. 최근 좀 아쉬운 점이 생겼는데, 세 분이 모두 같은 회사에 다니게 되었다는 점이다. 아직 그에 대한 피부에 와닿는 단점은 없지만, 그 전보다 다양한 얘기들을 듣기 힘들고 회사 욕을 하기도 어렵다는 점(<del>예상</del>)이 아쉽다. 그래도 원래 하던 기능은 온전히 잘하고 있어서 피부로 와닿지는 않는듯하다.</p><hr><p>회고 전에는 뭔가 반성할 점이 많은 일 년이구나 싶었는데, 꽤 알찼던 것 같기도 하고? 이번 해에 개발자로서 한 단계 점프한 것 같다는 생각도 들었다. 벌써 상반기가 거의 다 지나가고 졸업 이후 첫 회사도 결정하는 단계가 되었고 고민 중이다. 다음 페이즈가 열리고 있다는 생각도 들고 커리어 골과 마일스톤 사이에 간극에 대한 고민도 많아진다. 이 글은 뇌에서 거의 바로 꺼내 쓴 글이라 두서가 없을 것 같은데 퇴고 과정 없이 올렸다. 회고는 참 어려운 것</p>]]></content:encoded>
<category domain="https://changhoi.kim/categories/logs/">logs</category>
<category domain="https://changhoi.kim/tags/retrospect/">retrospect</category>
<category domain="https://changhoi.kim/tags/log/">log</category>
<comments>https://changhoi.kim/posts/logs/20220417/#disqus_thread</comments>
</item>
<item>
<title>DynamoDB Internals (1) - Dynamo</title>
<link>https://changhoi.kim/posts/database/dynamodb-internals-1/</link>
<guid>https://changhoi.kim/posts/database/dynamodb-internals-1/</guid>
<pubDate>Fri, 15 Apr 2022 15:00:00 GMT</pubDate>
<description><p>아마존은 지구 규모 스케일 서비스를 운영하면서 자신들의 요구와 가장 잘 들어맞는 범용적인 분산 스토리지 시스템을 만들어냈는데 이 시스템이 바로 Dynamo이다. 시스템을 만들고 운영한 경험을 논문으로 발표했고, 이 논문은 분산 스토리지 시스템 생태계에 큰 영향을 주었다. 이 논문에 영향을 받아 오픈소스에서는 Cassandra가 개발되었고 AWS 서비스의 SimpleDB, DynamoDB를 만드는 기초가 되었다. DynamoDB의 구조가 완전히 Dynamo와 같지는 않지만, 뿌리가 되는 Dynamo 시스템에 대해 먼저 알아보자.</p></description>
<content:encoded><![CDATA[<p>아마존은 지구 규모 스케일 서비스를 운영하면서 자신들의 요구와 가장 잘 들어맞는 범용적인 분산 스토리지 시스템을 만들어냈는데 이 시스템이 바로 Dynamo이다. 시스템을 만들고 운영한 경험을 논문으로 발표했고, 이 논문은 분산 스토리지 시스템 생태계에 큰 영향을 주었다. 이 논문에 영향을 받아 오픈소스에서는 Cassandra가 개발되었고 AWS 서비스의 SimpleDB, DynamoDB를 만드는 기초가 되었다. DynamoDB의 구조가 완전히 Dynamo와 같지는 않지만, 뿌리가 되는 Dynamo 시스템에 대해 먼저 알아보자.</p><span id="more"></span><h1 id="RDB가-적절하지-않았던-이유"><a href="#RDB가-적절하지-않았던-이유" class="headerlink" title="RDB가 적절하지 않았던 이유"></a>RDB가 적절하지 않았던 이유</h1><p><img src="/images/2022-04-16-dynamodb-internals-1/dynamo-title.png?style=centerme" alt="Dynamo 논문"></p><p>Dynamo는 아주 가용성 높은 키 값 스토어이다. 애초에 이러한 시스템을 구성하게 된 이유가 뭘까? 아마존은 Oracle이 제공하는 엔터프라이즈 데이터베이스 스케일을 넘어선 지구 단위의 글로벌 서비스로 성장했다. 이런 스케일을 감당하기 위해 아마존은 직접 DB를 설계하고 관리하기로 결정했다. 이를 위해 아마존은 그동안의 데이터베이스 사용 패턴에 대해 조사했고 다음과 같은 특징들을 확인할 수 있었다.</p><ul><li>스케일아웃 처리를 위한 <code>JOIN</code> 사용 제거 (<code>JOIN</code>을 사용하지 않음)</li><li>인덱스를 통한 단일한 데이터 검색이 대부분</li><li>복수의 데이터를 가져오는 패턴도 있었지만, 이 경우 보통 하나의 테이블에서만 데이터를 가져옴</li></ul><p>이런 특징들은 Key-Value 형태로 매칭되는 쿼리 조건으로 충분히 해결할 수 있고, RDB의 아주 강력한 쿼리들을 사용할 필요가 없었다. 즉, 규모있는 데이터 처리를 위해 RDB가 요구하는 컴퓨팅 리소스를 준비할 필요가 없다. 따라서 아마존에서는 RDB 사용이 데이터베이스 접근 패턴에 적합하지 않다고 판단했다.</p><hr><p>아마존에서 필요한 데이터베이스는 일반적인 RDB와 다르게 다음과 같은 특징을 가져야 한다</p><ul><li><p><strong>간단한 쿼리 모델</strong>: 프라이머리 키를 통해 데이터를 정의하고 읽어올 수 있는 간단한 쿼리 모델이 필요하고, 관계형 스키마라든지, 복잡한 테이블 연결은 불필요하다.</p></li><li><p><strong>느슨한 ACID</strong>: 트랜잭션의 ACID, 특히 일관성을 보장하는 것은 데이터 가용성 측면에서 좋지 않다. 약한 일관성을 가지고 동작하는 애플리케이션에 적합한 데이터베이스여야 한다.</p></li><li><p><strong>효율성</strong>: 아마존 플랫폼의 엄격한 지연율 제한을 여러 서비스 도메인에서 충족할 수 있어야 한다. 아마존 플랫폼은 일반적으로 500TPS 기준으로 99.9% 백분위가 300ms 안에 처리되어야 한다는 지연율 기준이 있다. 서비스마다 읽기와 쓰기 비율이 다르기 때문에 데이터베이스의 설정을 통해 어떻게 분산 환경의 읽기 쓰기를 수행할지 결정할 수 있어야 한다.</p></li></ul><blockquote><p>효율성은 조금 모호한 단어같이 보일 수 있는데, 설정값을 통해 범용적으로 인프라에 도입할 수 있다는 측면에서의 효율성을 뜻하는 것 같다.</p></blockquote><h1 id="시스템-디자인-고민"><a href="#시스템-디자인-고민" class="headerlink" title="시스템 디자인 고민"></a>시스템 디자인 고민</h1><p>위와 같은 목표를 달성하기 위해 필요한 몇 가지 고민이 있었다. 이 고민을 해결한다면 시스템 디자인의 목표를 달성할 수 있다.</p><hr><p>전통적인 RDB의 데이터 복제 알고리즘은 강력한 일관성을 위해 동기적으로 데이터를 복제한다. 이 수준의 일관성을 얻기 위해서는 특정 시나리오에서 데이터 가용성을 포기해야 한다. 즉, 높은 수준의 일관성은 정확한 답을 공유하지 못하는 불확실한 상황일 때 차라리 데이터를 사용 불가능하게 만들어버린다. 분산 시스템에서 아주 유명한 이론인 <a href="https://en.wikipedia.org/wiki/CAP_theorem">CAP 이론</a>에서 말하듯 일관성, 가용성, 그리고 네트워크 파티션 내구성 세 가지를 모두 충족시킬 수 없다. 네트워크 파티션 상황은 반드시 발생하게 되어있고, 데이터 일관성을 유지하기 위해서는 가용성을 포기해야 한다.</p><p><img src="/images/2022-04-16-dynamodb-internals-1/cap-theorem.png?style=centerme" alt="CAP 이론, 가운데는 유니콘임"></p><blockquote><p>강력한 일관성을 유지하는 데이터 복제는 “전통적”이라고 표현했지만, 이 논문이 쓰이기 전의 상황인듯 싶다. 정확한 히스토리는 잘 모르지만, 최근 RDB에서 레플리카를 운영하는 방법이 꼭 완전한 일관성을 요구하지는 않았던 것 같다.</p></blockquote><h2 id="핵심-고민"><a href="#핵심-고민" class="headerlink" title="핵심 고민"></a>핵심 고민</h2><p>Dynamo는 네트워크 장애가 무조건 발생한다는 가정 아래 가용성을 최대로 높이도록 디자인되었다. 이를 위해 데이터가 레플리카에 동기적으로 전파되도록 처리하지 않고, 비동기적으로 전파되도록 했다. 구체적으로 어떻게 전파되고, 어떤 상황에서 성공으로 판단하는지 등은 이후 후술한다. 아무튼 이러한 비동기 전파 상황에서는 데이터의 충돌 문제가 발생할 수 있다. 하나의 데이터를 수정한 값이 모종의 이유로 두 가지 이상의 버전으로 분기되는 것을 데이터 충돌 상황이라고 표현한다.</p><p><code>A</code> 노드에 <code>a</code> → <code>a'</code>가 되도록 수정했는데 비동기 전파로 인해 다른 노드에 전파가 되기 전에 성공 응답을 보내고 <code>A</code> 노드에 네트워크 파티션이 발생했다고 가정해보자. 그다음 같은 데이터 <code>a</code>를 갖고 있던 <code>B</code> 노드에 <code>a</code> → <code>a''</code>로 수정을 가했다면 시스템은 두 가지 버전의 <code>a</code> 데이터를 갖게 되는 것이다.</p><p><img src="/images/2022-04-16-dynamodb-internals-1/versioning.png?style=centerme" alt="예시 상황"></p><p>이 상황을 해결하는 방법은 다음 두 가지 방향의 문제를 해결하는 것이다.</p><ul><li>언제 해결할 것이냐? (쓰기 시점 or 읽기 시점)</li><li>누가 해결할 것이냐? (클라이언트 or 데이터베이스)</li></ul><p>전통적으로는 쓰기 시점에 이 문제를 해결하며 읽기 작업의 복잡도를 단순하게 유지한다. 이런 시스템에서는 주어진 타임아웃 시간 내에 모든(혹은 특정 정족수) 데이터 저장소에 닿지 못하면 쓰기가 실패할 수 있다. Dynamo는 “<strong>항상 쓰기 가능한</strong>“ 데이터 스토어를 목표로 한다. 아마존의 많은 서비스에서 고객의 업데이트 작업을 거절하는 경우 고객 경험을 해치는 결과를 가져온다. 예를 들어서 쇼핑 카트는 반드시 소비자가 담거나 지운 아이템을 네트워크 파티션이 발생하더라도 반영할 수 있어야 한다. 따라서 쓰기 작업의 가용성을 위해 Dynamo는 읽기 작업에 이 충돌 해결 역할을 맡겼다.</p><blockquote><p>쓰기 작업에서 이 문제를 해결한다면 버저닝이 발생하지 않도록 회피하는 형태로 문제를 해결한다고 볼 수 있고, 읽기 시점에서 해결한다면 문제 발견 후 복구하는 과정이 있다고 볼 수 있다.</p></blockquote><p>그다음 “누가 해결할 것인지” 선택해야 한다. 데이터베이스에서 일관적으로 처리하도록 하는 방법은 굉장히 제한적이다. 예를 들어서 마지막 업데이트가 발생한 시점을 기준으로 데이터를 덮어씌우는 방법처럼 아주 간단하고 단순한 방법으로만 문제를 해결할 수 있다. 반대로 애플리케이션에서 이를 해결하도록 두면, 데이터가 어떻게 비즈니스 로직과 연결되는지 이해하고 있기 때문에 더 적절한 방법을 선택할 수 있다. 어떻게 버저닝 문제를 해결하는지 구체적인 내용은 후술한다.</p><h2 id="기타-고려사항"><a href="#기타-고려사항" class="headerlink" title="기타 고려사항"></a>기타 고려사항</h2><p>위 가장 큰 두 가지 설계 고민 외에 다음과 같은 고민을 하며 시스템을 설계했다.</p><ul><li><p><strong>증분 확장성 (Incremental Scalability)</strong>: 데이터 스토리지 노드를 추가 또는 삭제할 때 데이터베이스 운영 및 시스템에 최소한의 영향만 주면서 이를 수행해야 한다.</p></li><li><p><strong>대칭성</strong>: 모든 스토리지 노드가 동일한 역할 수행해야 한다. 이유는 특수한 역할을 하는 노드를 지정하기 시작하면 프로비저닝 시스템의 복잡도가 증가하기 때문이다.</p></li><li><p><strong>탈중앙성</strong>: 중앙에서 컨트롤하는 시스템보다 P2P를 통한 분산된 컨트롤이 더 선호되는 방향으로 시스템을 설계한다. 이에 대해서는 구체적인 이유보다는 과거에 중앙 시스템의 문제로 인한 운영 중단 사태가 발생한 적이 있기 때문에 이를 피하기 위해서 이러한 고민을 했다고 한다.</p></li><li><p><strong>이질성 (Heterogeneity)</strong>: 시스템에서 동작하는 노드들이 불균일하다는 특성을 이용할 수 있는 시스템이어야 한다. 예를 들어 작업 분배가 각 노드의 Capacity에 비례해 분배될 수 있는 시스템이어야 한다. 이를 통해 보다 효율적으로 작업 분배가 이뤄질 수 있다.</p></li></ul><blockquote><p>시스템 목표와 그 목표를 위한 고민이 잘 매칭되는지 확인해보자. 간단한 쿼리 모델은 Key-Value 스토리지라는 점, 느슨한 ACID는 비동기적 데이터 전파를 통해 가용성을 높인다는 점(그리고 데이터 충돌을 해결하기 위한 핵심 고민 두 가지), 효율성의 경우 여러 설정값으로 위 고민을 해결하도록 했다. 이 여러 설정값에 대해서는 나중에 더 자세히 다룬다.</p></blockquote><hr><h1 id="시스템-아키텍처"><a href="#시스템-아키텍처" class="headerlink" title="시스템 아키텍처"></a>시스템 아키텍처</h1><p>위 고민을 어떻게 해결했는지 구체적으로 살펴보자. Dynamo는 독자적인 기술의 탄생이 아니고 그 전부터 논의되어온 분산 시스템을 구성하기 위한 기술들의 집합이다. 요약하자면 다음 방법들을 사용하고 있다.</p><table><thead><tr><th align="center">문제</th><th align="center">해결책</th><th align="center">해결 목표</th></tr></thead><tbody><tr><td align="center">파티셔닝 (노드 추가 or 제거)</td><td align="center">Consistency Hashing</td><td align="center">증분 확장성을 보장할 수 있음</td></tr><tr><td align="center">쓰기 HA 구성</td><td align="center">vector clock 개념과 클라이언트에서 데이터 충돌 해결</td><td align="center">쓰기 가용성이 증가</td></tr><tr><td align="center">일시적 실패</td><td align="center">쿼럼 (Quorum)과 Hinted Handoff</td><td align="center">범용적인 시스템을 위한 일시적인 실패와 복구 처리 방법</td></tr><tr><td align="center">영구적 실패</td><td align="center">Merkle trees</td><td align="center">데이터 동기화를 위한 데이터 전송량을 최소화</td></tr><tr><td align="center">멤버십, 실패 탐지</td><td align="center">가십 기반 멤버십 프로토콜</td><td align="center">노드 기능의 동일성을 유지하면서 탈중앙화 시스템을 유지할 수 있는 시스템</td></tr></tbody></table><p>글에서는 고민을 해결하기 위해 핵심적이라고 생각되는 파티셔닝문제, 쓰기 HA, 일시적 실패에 대해 다룰 예정이다.</p><h2 id="파티셔닝"><a href="#파티셔닝" class="headerlink" title="파티셔닝"></a>파티셔닝</h2><p>데이터 또는 트랜잭션이 증가함에 따라 스토리지 노드가 감당해야 하는 트랜잭션이 점점 늘어나면 이를 처리하기 위한 스토리지 노드가 추가로 붙어야 한다. 즉, 동적인 파티셔닝을 위한 방법이 필요하다. Dynamo에서는 이를 위해 <strong><a href="https://ko.wikipedia.org/wiki/%EC%9D%BC%EA%B4%80%EB%90%9C_%ED%95%B4%EC%8B%B1">Consistent Hashing</a></strong> 방법을 사용하고 있다.</p><p>Consistent Hashing은 해시 함수의 결과 범위가 고정된 원형 공간(Ring) 안에서 다뤄진다. 링 형태라는 것은 가장 큰 해시값의 마지막이 가장 작은 해시값으로 연결된다는 것을 의미한다. 해당 시스템 안에서 각 스토리지 노드는 랜덤 값이 할당되고, 이 랜덤 값의 해시값을 가지고 링 위에 위치 시키는 방법이다. 스토리지 노드 안에 들어갈 데이터 역시 키를 해싱한 값을 기준으로 링 위에 배치된다. 이때 데이터의 위치에서 시계 방향으로 돌았을 때 처음 만나게 되는 스토리지 노드에 실제 데이터가 저장되게 된다.</p><p><img src="/images/2022-04-16-dynamodb-internals-1/consistent-hashing.png?style=centerme" alt="데이터는 기본적으로 B에 담긴다"></p><p>일반적인 방법으로 데이터의 키를 해싱한 값으로 샤딩을 진행한 경우, 노드가 추가되거나 삭제될 때마다 데이터를 다시 해싱해야 하는 큰 비용이 있다. Consistent Hashing은 데이터를 균일하게 분산하면서도 노드가 추가되어도 이런 재해싱 과정 필요 없이 이웃한 노드에만 영향을 주는 방식으로 스토리지 노드를 추가한다.</p><p>동적인 스케일링을 위해 아주 적합한 방법이지만, 스토리지 노드의 이질성을 고려하지 않는다. 위에서 언급했던 “<strong>이질성</strong>“을 고려한 시스템을 위해 Dynamo는 하나의 물리적인 노드가 여러 개의 가상 노드(Virtual Node)와 매칭되며 가상 노드들이 링 위에 올라가도록 설계되었다. 각 가상 노드는 실제 물리 노드와 연결되고 가상 노드의 개수는 물리적 노드의 실제 성능에 맞게 조절된다.</p><p>즉, 성능이 좋은 스토리지 노드는 많은 가상 노드와 연결되어 링 위의 비교적 많은 영역을 담당한다. 반대의 경우 적은 수의 가상 노드와 연결되게 된다.</p><blockquote><p>랜덤하게 배치된다는 점 역시 분산이 고르지 못하다는 단점이 있다. 운영하면서 몇 가지 버전을 거친 이후 링을 고르게 나눠 노드를 배치하는 방법으로 이 문제를 해결할 수 있었다고 한다. 이 글에서는 해당 내용에 대해 설명하고 있지 않다. 궁금하다면 이 <a href="https://www.waitingforcode.com/general-big-data/dynamo-paper-consistent-hashing/read">링크</a>에서 설명을 더 읽어보자.</p></blockquote><h2 id="HA-High-Availability"><a href="#HA-High-Availability" class="headerlink" title="HA(High Availability)"></a>HA(High Availability)</h2><p>HA를 위해서는 복제 시스템이 필요하다. 즉, 데이터를 본래 저장해야 하는 스토리지 노드 외에 다른 스토리지 노드에도 저장할 수 있어야 한다. Dynamo는 몇 개의 호스트에 데이터를 복제할 것인지 설정 파라미터로 결정할 수 있도록 만들었다.</p><blockquote><p>몇 개의 호스트에 복제하는지 결정하는 파라미터는 이 글에서 <code>N</code>으로 표기한다.</p></blockquote><p><img src="/images/2022-04-16-dynamodb-internals-1/dynamo-consistent-hashing.png?style=centerme" alt="N = 3일 때, 데이터는 B, C, D 노드에 담긴다"></p><p>위 이미지에서 키 <code>K</code>를 가진 데이터는 <code>(A, B]</code> 사이에 위치하게 되고, <code>B</code> 노드에 기본적으로 저장된다. 그다음 데이터가 복제되어야 하는 노드는 <code>N - 1</code> 만큼 시계 방향으로 돌며 만나는 노드이다. <code>N = 3</code> 이라면, <code>B</code> 외 <code>C</code>, <code>D</code>도 이 데이터를 저장하는 대상이 된다.</p><p>이렇게 특정 데이터에 대해 저장할 책임이 있는 노드 리스트를 <strong>preference list</strong>라고 부른다. preference list 안에 가상 노드로 인해 물리 노드가 중복되어 들어가지 않도록 같은 물리 노드를 가리키는 가상 노드를 스킵하면서 리스트를 채운다.</p><hr><p>데이터가 여러 노드에 전파되는 것은 비동기적으로 (아직 어떻게 비동기적으로 동작하는지 설명하지 않았다. 후술할 예정) 발생한다. 이러한 이유로 Dynamo는 결과적 일관성(Eventual Consistency) 특성을 갖게 된다. 결과적 일관성은 일시적으로 데이터가 일관적이지 않을 수 있지만 결국 같은 데이터를 보장한다는 뜻이다. 예를 들어 <code>Put</code>을 호출해 데이터를 쓰는 작업을 할 때, 필요한 모든 노드에 데이터가 복제되는 것을 기다렸다가 응답을 보내주지 않는다. 아직 몇 개의 노드는 데이터를 쓰기 전이지만 <code>Put</code>에 대한 성공 응답을 보낸다. 만약 이런 상황에서 연속적으로 <code>Get</code> 요청을 보내면 어떤 노드의 데이터를 읽느냐에 따라 최신 버전의 데이터를 가져오지 못할 수도 있다는 것을 뜻한다. 위에서 살짝 예시를 들었는데, 쓰기 상황에서 이렇게 최신 데이터가 아닌 오브젝트를 기반으로 업데이트를 진행하게 되면 데이터 버저닝(분기, 데이터 충돌)이 발생한다.</p><p>앞서 언급한 것처럼 분기된 데이터를 통합하기 위해서 두 가지 결정이 필요하다. 언제 통합할 것인지? 누가 통합할 것인지? 그리고 Dynamo에서는 읽기 시점에 클라이언트에서 해결한다고 설명한다.</p><p>데이터 버저닝과 이를 해결하는 방법을 설명하기 전에 먼저 간단하게 Dynamo의 시스템 인터페이스를 확인해보자. 데이터에 접근하기 위한 <code>Get</code>과 업데이트를 위한 <code>Put</code>이 있다. 각각은 다음과 같이 호출된다.</p><ul><li><code>get(key)</code>: 스토리지 시스템에서 키와 연결된 오브젝트 복제본을 찾아 컨텍스트가 담긴 단일 오브젝트 또는 컨텍스트가 담긴 오브젝트 리스트를 반환한다.</li><li><code>put(key, context, object)</code>: 오브젝트 복제본이 연관된 키에 의존해서 어디에 위치해야 하는지 결정하고, <code>object</code>를 디스크에 쓴다.</li></ul><p><code>get</code>의 결과를 “<strong>컨텍스트가 담긴 오브젝트</strong>“라고 표현하고 있다. 컨텍스트는 <strong>데이터의 버전 정보</strong>를 포함한 메타데이터를 의미한다. 복수가 될 수 있다는 것은 하나의 데이터에 대해 여러 갈래로 나눠진 데이터 버전이 있는 경우 해당 버전들을 모두 가져오기 때문이다. <code>put</code>의 인자에서 <code>context</code>를 통해 수정 대상인 오브젝트 컨텍스트를 전달하고 기존에 나뉘어있던 버전들을 마지막 <code>put</code>을 기준으로 통합하게 한다.</p><blockquote><p>Dynamo의 쓰기 과정은 읽기 이후 쓰기를 수행하는 식으로 클라이언트에서 사용해야 하는 구조인 것으로 보인다.</p></blockquote><p>Dynamo의 오브젝트 버전 관리는 <code>vector clock</code>을 사용한다. 이는 어떤 노드에 저장되어 있는지, 몇 번째 데이터 수정인지를 담고 있는지, 이렇게 두 가지를 갖는 튜플 리스트이다.</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vector clock = [(Node, Counter)...]</span><br></pre></td></tr></table></figure><p>컨텍스트 안의 <code>vector clock</code>을 비교해서 인과적인 순서(앞선 버전인지, 동시에 나눠진 버전인지)를 밝힐 수 있다. 많은 경우 새 버전이 과거 버전을 포함하고 있기 때문에 시스템 안에서 정규 버전(나눠진 두 버전을 조정한 새로운 버전)을 결정할 수 있다. 이렇게 시스템에서 버전을 조정하는 과정을 “<strong>syntactic reconciliation</strong>“이라고 한다. 그러나 동시 업데이트에 의한 분기는 클라이언트에 의해 조정이 된다. 위에서 언급했던 것처럼 <code>get(key)</code>를 통해 복수의 오브젝트 버전을 받으면 클라이언트에서 적절한 로직을 통해 이를 합치고 업데이트해 줘야 한다. 이렇게 클라이언트에 의해서 조정하는 것은 “<strong>semantic reconciliation</strong>“이라고 한다.</p><blockquote><p>따라서 정확하게는 Dynamo는 데이터베이스에 의한 조정과 클라이언트에 의한 조정, 두 가지 방법 모두 사용하고 있다고 볼 수 있다.</p></blockquote><h2 id="일시적-실패"><a href="#일시적-실패" class="headerlink" title="일시적 실패"></a>일시적 실패</h2><p>쓰기 동작 중 특정 노드의 장애로 인해 일시적인 실패가 발생할 수 있다. 이에 대한 처리를 <strong>Hinted Handoff</strong>라고 불리는 방식으로 해결하고 있다. 일단 이 설명을 하기 전에 먼저 “<strong>쓰기 실패 상황</strong>“ 또는 “<strong>읽기 실패 상황</strong>“을 정의해야 한다. 구체적으로 어떻게 비동기 복제를 하면서 응답을 전달해주는지 확인해보자.</p><h3 id="Sloppy-Quorum"><a href="#Sloppy-Quorum" class="headerlink" title="Sloppy Quorum"></a>Sloppy Quorum</h3><p>쿼럼(Quorum)은 “정족수”라는 뜻이다. 조금 단순하게 설명하자면 읽기의 최소 성공 수를 <code>R</code>, 쓰기의 최소 성공 수를 <code>W</code>라는 변수를 사용해 시스템에서 읽기와 쓰기의 실패 여부를 확인하는 방법이다. Dynamo 시스템에서 <code>Get</code>과 <code>Put</code> 요청은 어떤 스토리지 노드든 받을 수 있다. Read 또는 Write 요청을 처음 받은 노드를 “<strong>coordinator</strong>“라고 부른다. 일반적으로 coordinator는 preference list를 만들 때 첫 번째 노드이다.</p><blockquote><p>요청은 HTTP를 통해 전달되는데, 로드 밸런싱을 통해 앞단에서 요청을 관리한다면 coordinator는 맨 첫 번째 노드가 아닐 수도 있다. 로드 밸런서를 사용하지 않는 경우 Partition-Aware 클라이언트 라이브러리를 사용해 어떤 노드에 요청을 보내야 하는지 클라이언트에 의해 결정하도록 한다.</p></blockquote><p><img src="/images/2022-04-16-dynamodb-internals-1/quorum.png?style=centerme" alt="쓰기 성공 상태"></p><p>위 이미지처럼 복제해야 하는 노드 수만큼 preference list가 지정되고 coordinator는 이 리스트 안의 나머지 노드에 데이터를 전파한다. 그리고 정족수만큼의 정상 응답을 받으면 클라이언트에게 성공 응답을 보내준다. 예시에서는 <code>W = 2</code>로 설정되었기 때문에 위 이미지의 녹색 박스 노드에서 정상적으로 값을 저장했다면 이 요청은 성공한 요청이 된다.</p><p>이 방법은 “Strict Quorum” 시스템으로, 만약 복제되어야 하는 노드들 중에 최소 정족수만큼의 성공을 만들지 못하면 실패 처리 된다. 만약 preference list 안에 있는 <code>N</code>개의 노드 중 <code>N - W - 1</code>개만큼의 노드가 장애 상황이라면 쓰기 실패 상황이 발생한다. 그러나 아마존은 실패 처리를 극단으로 최소화하고 싶어 했다. 그래서 도입한 시스템이 “Sloppy Quorum”이다. 이름에서도 알 수 있듯 조금 느슨하게 쿼럼을 만족시키는 방식이다.</p><p>본래 preference list는 키를 해시한 다음 링 위에서 시계 방향으로 봤을 때 첫 번째로 만나는 노드를 포함해 상위 <code>N</code>개의 노드가 속하게 된다. 그런데 이 노드를 단순히 상위 <code>N</code>개가 아니라 “건강한 노드 상위 <code>N</code>개”로 만드는 것이다.</p><p><img src="/images/2022-04-16-dynamodb-internals-1/dynamo-consistent-hashing.png?style=centerme" alt="N = 3일 때, 원래 데이터는 B, C, D에 복제된다"></p><p>위 예시에서 설정 파라미터값이 <code>N = 3</code>, <code>W = 3</code>이고 만약 노드 <code>D</code>에 장애가 발생했다고 가정해보자. 그렇다면 preference list는 실제로 <code>B</code>, <code>C</code>, <code>E</code> 노드를 담고 있게 된다. 이런 방식에서 장점은 가용성을 높일 수 있다는 장점이 있지만, 문제는 약속과 다른 노드가 데이터를 저장할 수 있다. 레플리카의 범위는 노드 <code>D</code>까지인데, <code>E</code>에 데이터가 저장된 상황이다.</p><h3 id="Hinted-Handoff"><a href="#Hinted-Handoff" class="headerlink" title="Hinted Handoff"></a>Hinted Handoff</h3><p>이 문제를 해결하기 위해 <strong>Hinted Handoff</strong>라는 전략을 사용한다. <code>E</code>로 전달된 복제본 데이터는 메타 데이터 안에 원래 저장될 타겟 노드 정보를 포함하고 있다. 이 정보를 “힌트”라고 표현한 건데, 힌트가 박힌 복제본을 받으면 노드는 원래 저장소와 분리된 다른 로컬 임시 저장소에 해당 데이터를 저장한다. 본래 노드가 다시 복구되면 임시 저장소에 담긴 데이터를 보내주고 임시 저장소에서는 삭제한다.</p><hr><p>이러한 Sloppy Quorum과 Hinted Handoff를 가지고 가용성을 크게 높일 수 있게 된다. 만약 <code>W = 1</code>이라면, 모든 노드가 장애 상태여야 쓰기가 실패한다. 하지만 이 방법은 일시적인 장애 상황에서 처리를 위한 방법이고, 영구적인 실패 이후 데이터 동기화 과정을 위해서는 다른 방법이 필요하다. 간단히 설명하자면, 가지고 있는 데이터를 <a href="https://ko.wikipedia.org/wiki/%ED%95%B4%EC%8B%9C_%ED%8A%B8%EB%A6%AC">머클 트리</a>로 구성해 변경이 발생한 지점을 특정하고 변경된 부분만 데이터를 전송할 수 있게 해준다. 머클 트리로 두 노드의 데이터가 같은지 확인하는 케이스는 분산 시스템에서 자주 등장한다. 가장 유명해진 예시로는 블록체인이 있다. 이 <a href="https://medium.com/coinmonks/merkle-trees-concepts-and-use-cases-5da873702318">링크</a>에서 머클 트리에 대한 설명과 머클트리를 블록체인과 Dynamo에 적용한 유즈 케이스 설명을 확인할 수 있다.</p><h1 id="실제-운영"><a href="#실제-운영" class="headerlink" title="실제 운영"></a>실제 운영</h1><p>Dynamo는 몇 가지 서비스에서 다양한 설정값으로 사용되고 있다. 각 서비스는 데이터 버전을 합의하는 로직을 독립적으로 구성하고, 서비스의 특성에 따라서 쿼럼 파라미터인 <code>R</code>, <code>W</code>값을 설정한다. 일관성이 중요하다면 <code>W</code>값을 높게 설정하고, 읽기(쓰기) 성능 자체가 더 중요하다면 <code>R</code>(<code>W</code>)값을 낮춘다. Dynamo의 일반적인 (<code>N</code>, <code>R</code>, <code>W</code>)는 (3, 2, 2)로 구성되어있다.</p><h1 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h1><ul><li><a href="https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf">https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf</a></li><li><a href="https://en.wikipedia.org/wiki/CAP_theorem">https://en.wikipedia.org/wiki/CAP_theorem</a></li><li><a href="https://ko.wikipedia.org/wiki/%EC%9D%BC%EA%B4%80%EB%90%9C_%ED%95%B4%EC%8B%B1">https://ko.wikipedia.org/wiki/%EC%9D%BC%EA%B4%80%EB%90%9C_%ED%95%B4%EC%8B%B1</a></li><li><a href="https://www.waitingforcode.com/general-big-data/dynamo-paper-consistent-hashing/read">https://www.waitingforcode.com/general-big-data/dynamo-paper-consistent-hashing/read</a></li><li><a href="https://www.allthingsdistributed.com/2017/10/a-decade-of-dynamo.html">https://www.allthingsdistributed.com/2017/10/a-decade-of-dynamo.html</a></li><li><a href="https://jimdowney.net/2012/03/05/be-careful-with-sloppy-quorums/">https://jimdowney.net/2012/03/05/be-careful-with-sloppy-quorums/</a></li><li><a href="https://medium.com/coinmonks/merkle-trees-concepts-and-use-cases-5da873702318">https://medium.com/coinmonks/merkle-trees-concepts-and-use-cases-5da873702318</a></li></ul>]]></content:encoded>
<category domain="https://changhoi.kim/categories/database/">database</category>
<category domain="https://changhoi.kim/tags/dynamodb/">dynamodb</category>
<category domain="https://changhoi.kim/tags/distributed-system/">distributed_system</category>
<comments>https://changhoi.kim/posts/database/dynamodb-internals-1/#disqus_thread</comments>
</item>
<item>
<title>Learning Go 간단 리뷰</title>
<link>https://changhoi.kim/posts/books/learning-go/</link>
<guid>https://changhoi.kim/posts/books/learning-go/</guid>
<pubDate>Mon, 28 Mar 2022 15:00:00 GMT</pubDate>
<description><p>이번년도에도 한빛 미디어의 <strong>나는 리뷰어다</strong>에 선정되어 매달 책 한 권씩을 읽을 수 있게 됐다. 3월에는 Learning Go 책을 받아서 보게 되었다. 이 글은 해당 책에 대한 간단한 리뷰이다.</p></description>
<content:encoded><![CDATA[<p>이번년도에도 한빛 미디어의 <strong>나는 리뷰어다</strong>에 선정되어 매달 책 한 권씩을 읽을 수 있게 됐다. 3월에는 Learning Go 책을 받아서 보게 되었다. 이 글은 해당 책에 대한 간단한 리뷰이다.</p><span id="more"></span><p>일단 Go를 자주 사용하는 개발자로서 Go 책이 자주 보이고 있다는 점에서 굉장히 기분이 좋다. 학습을 해왔던 여러 리소스들과 비교하면서 책을 읽어봤다. 일단 책의 난도는 Go를 접한 적 없는 개발자를 위한 책이었다. 물론 개발자가 아니더고 새롭게 Go를 접하는 사람들 역시 이 책으로학습해도 좋을 것 같은 생각이 들었다. 원래 언어 학습을 위한 책들은 이 정도의 난도로 만들어지는 것 같은데 유독 Go 책은 기존에 개발을 하던 사람들 만을 대상으로 쓴 책 처럼 느껴지는 경우가 많았다. 예를 들어서 The Go Programming Language와 같은 책이 그랬다. 이 책은 내부 동작에 대한 구체적인 설명까지 하지는 않았다. 다만 아주 기초적인 CS 지식의 경우 추가 설명처럼 박스를 만들어서 해당 CS 배경 지식을 설명해준다. 예를 들어서 Map 자료구조를 이해하기 위한 Hash에 대한 기본적인 설명 등… 가장 장점으로 느껴졌던 부분은 꽤 실용적인 내용도 담고, 최신 버전의 정보들이 잘 반영되어 있다는 점이다. 초보자가 언어 학습을 위한 교재로 선택하면 좋을 것 같다고 생각했다.</p>]]></content:encoded>
<category domain="https://changhoi.kim/categories/books/">books</category>
<category domain="https://changhoi.kim/tags/review/">review</category>
<comments>https://changhoi.kim/posts/books/learning-go/#disqus_thread</comments>
</item>
<item>
<title>Go GC</title>
<link>https://changhoi.kim/posts/go/go-gc/</link>
<guid>https://changhoi.kim/posts/go/go-gc/</guid>
<pubDate>Thu, 24 Mar 2022 15:00:00 GMT</pubDate>
<description><p>Go는 메모리 관리를 런타임에서 해주는 프로그래밍 언어이다. 메모리 관리라고 하면 일반적으로 힙 영역에 할당하는 메모리들 더이상 스택에서 접근할 수 없는 상태가 되면, 할당 해제하는 가비지 콜렉팅을 의미한다. 이번 글에서는 GC에 대한 전반적인 이야기와 함께, Go에서는 구체적으로 어떤지 알아보았다.</p></description>
<content:encoded><![CDATA[<p>Go는 메모리 관리를 런타임에서 해주는 프로그래밍 언어이다. 메모리 관리라고 하면 일반적으로 힙 영역에 할당하는 메모리들 더이상 스택에서 접근할 수 없는 상태가 되면, 할당 해제하는 가비지 콜렉팅을 의미한다. 이번 글에서는 GC에 대한 전반적인 이야기와 함께, Go에서는 구체적으로 어떤지 알아보았다.</p><span id="more"></span><blockquote><p>💡 우선 글은 1.17 버전의 코드를 보면서 작성되었다.<br>💡 글에서 <code>GC</code>라는 말은 “가비지 콜렉터”를 의미하기도 하고 “가비지 콜렉터의 동작”을 의미하기도 한다. 동사로 사용되었으면 콜렉터의 동작, 명사면 콜렉터라고 이해하면 좋을 것 같다.</p></blockquote><h1 id="흔히-알려진-설명"><a href="#흔히-알려진-설명" class="headerlink" title="흔히 알려진 설명"></a>흔히 알려진 설명</h1><p>GC에 대한 아주 개략적인 Overview이다. 현대 많은 언어는 GC와 함께 메모리 관리를 도와주고 있다. 일반적으로 프로그램에서 동적 할당을 하게 되면 프로세스의 힙(Heap) 영역에 메모리를 할당하게 되어있다.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">Person</span> <span class="variable">p</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Person</span>(); <span class="comment">// 힙 사용</span></span><br></pre></td></tr></table></figure><p>이때, 힙 영역에 할당된 메모리를 할당 해제 해줘야 하는데, 이 과정을 개발자가 직접 하는 경우가 있고, 언어의 런타임 레벨에서 자동으로 해주는 경우가 있다. 이때 자동으로 해주는 컴포넌트의 이름이 GC이다.</p><p>GC는 대략 다음과 같은 흐름을 갖는다.</p><ol><li>GC 수행 시간 동안 GC 스레드를 제외하고 모든 스레드 정지</li><li>GC는 참조할 수 없는 객체를 확인하고 메모리 할당 해제</li><li>GC가 끝난 후 정지된 애플리케이션 스레드를 다시 재개</li></ol><p>1번의 정지되는 순간을 <strong>STW</strong> (<code>Stop The World</code>)라고 부른다. 이때 STW가 발생하는 순간은 GC 수행의 전체 과정이 아닐 수 있다. 어떤 알고리즘을 사용하는지에 따라 어떤 구간에서 STW가 발생할지 달라진다. 아무튼 GC가 발전하는 과정은 이 STW 시간을 줄이는 과정이고 GC를 튜닝하는 이유도 대부분 STW를 줄이기 위함이다.</p><h1 id="알려진-방법들"><a href="#알려진-방법들" class="headerlink" title="알려진 방법들"></a>알려진 방법들</h1><p>GC의 핵심적인 동작을 수행하는 두 가지 알고리즘을 가져왔다. 첫 번째는 <strong>Mark & Sweep</strong> 방식이고, 두 번째는 <strong>Reference Counting</strong>이다.</p><h2 id="Mark-Sweep"><a href="#Mark-Sweep" class="headerlink" title="Mark & Sweep"></a>Mark & Sweep</h2><p>이름이 아주 직관적인데, 말 그대로 지워야 하는 오브젝트를 마킹하고 청소하는 방법이다. 스택에서 힙을 참조하고 있는 루트 포인터를 찾아서 해당 루트 노드부터 체이닝 하면서 접근할 수 있는 오브젝트를 제거 대상에서 제외한다. 모두 순회하고 나서는 아직 제거 대상에 있는 오브젝트를 할당 해제하는 방식이다. Go와 JVM, JS에서 이 알고리즘을 사용한다.</p><p><img src="/images/2022-03-25-go-gc/marksweep.gif?style=centerme" alt="Mark & Sweep"><br><small style="center">이미지 출처: <a href="https://deepu.tech/memory-management-in-programming/">링크</a></small></p><h2 id="Reference-Counting"><a href="#Reference-Counting" class="headerlink" title="Reference Counting"></a>Reference Counting</h2><p>모든 오브젝트들이 참조 횟수 카운터를 갖고, 카운터가 0이 되는 오브젝트를 GC가 지우는 방식이다. 이 방법은 Python, PHP에서 사용 중인데, 근본적으로 순환 참조하고 있는 오브젝트에 대한 GC가 이루어질 수 없다. 이를 처리하기 위한 추가적인 컴포넌트와 함께 동작해야 한다.</p><hr><p>여기 <a href="https://spin.atomicobject.com/2014/09/03/visualizing-garbage-collection-algorithms/">링크</a>에서 여러 GC들의 할당과 해제 모습을 시각화해서 보여주고 있다. 여기 작성된 알고리즘 외, 추가로 몇 가지가 더 설명되어 있으니 궁금하다면 위에서 간략하게 소개된 방법들에 대해 알아보면 좋을 것 같다.</p><h1 id="GC를-구성하는-것들"><a href="#GC를-구성하는-것들" class="headerlink" title="GC를 구성하는 것들"></a>GC를 구성하는 것들</h1><p>아! Go, JVM, JS는 Mark & Sweep! 끄덕 끄덕, 하고 끝나면 좋겠지만 편한 프로그래밍의 뒷면은 그렇게 단순하지는 않다. 위에서 “<strong>알려진 방법들</strong>“로 소개한 방법들은 핵심적인 콜렉터의 동작 알고리즘에 관한 내용이고, GC를 구현한 언어에 따라 추가적인 기술이나 컴포넌트가 존재한다. Java의 GC가 굉장히 대표적이고 유명하다는 생각이 들어서, Go의 GC에 대한 구체적인 내용을 설명하기 전에 JVM에서 사용하고 있는 GC의 구성을 조금 더 살펴보고 이를 Go와 비교해보려고 한다.</p><h2 id="세대별-GC"><a href="#세대별-GC" class="headerlink" title="세대별 GC"></a>세대별 GC</h2><p>Generational GC라고 불리는 GC 방법이다. 세대별이라는 말은 힙 영역을 세대별로 나눠 관리한다는 것을 의미한다. “세대”는 오래 살아남은 객체와 그렇지 않은 객체를 구분 짓는 것을 의미한다. 이 GC는 다음과 같은 대전제를 바탕으로 설계되었다.</p><ol><li>대부분의 객체는 금방 접근 불가능 상태가 된다.</li><li>오래된 객체에서 새로운 객체를 참조하는 일은 드물게 발생한다.</li></ol><p>위 대전제의 이름은 <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/generations.html">Weak Generational Hypothesis</a>라고 한다. 이 가설을 이용해 <code>Old</code> 객체를 담는 영역과 <code>Young</code> 영역의 객체를 담는 영역으로 힙을 나눈다.</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"> <---- Tenured ----></span><br><span class="line">+----------+---+---+---------+---------+</span><br><span class="line">| Eden | S | S | | Virtual |</span><br><span class="line">+----------+---+---+---------+---------+</span><br><span class="line"><----- Young ------></span><br><span class="line"></span><br><span class="line">S: Survivor</span><br></pre></td></tr></table></figure><ul><li>Young 영역: 새롭게 생성된 객체가 위치한다. 가설대로 많은 객체가 이곳에서 새로 만들어졌다가 사라진다. 이곳에서 발생하는 GC는 <strong>Minor GC</strong>라고 불린다.</li><li>Old 영역: Young 영역에서 살아남은 객체가 여기 복사된다. Young 영역에 비해 크기가 크고, GC는 덜 자주 발생한다. 이곳에서 발생하는 GC는 <strong>Major GC</strong> 또는 <strong>Full GC</strong>라고 한다.</li></ul><p>이 방법을 통해 일반적인 상황에서는 Minor GC로 간단하게 GC를 수행하게 된다. 큰 힙 영역을 다 확인할 필요 없이 일부만 확인할 수 있으므로 GC 속도가 빠르다.</p><blockquote><p>💡 따라서 넓은 범위를 확인해야하는 Full GC가 자주 발생하는 상황은 문제가 있는 상황일 수 있다.</p></blockquote><p>만약 2번 전제 상황이 발생하였을 때 GC가 어떻게 Old 영역이 참조하고 있는 Young 영역의 객체를 할당 해제하지 않을 수 있을까? 이를 위해 Old 영역에서 Young 영역의 객체를 참조하고 있는지 기록하는 Card Table을 사용한다. 이 테이블은 512 바이트의 청크로, Old 영역을 모두 확인하지 않고도 이 부분을 확인함으로써 Young 영역의 객체가 지워지는 것을 방지할 수 있다.</p><h2 id="Compaction"><a href="#Compaction" class="headerlink" title="Compaction"></a>Compaction</h2><p>힙 영역에 메모리를 할당하고 해제하는 과정이 반복되면 단편화 문제가 발생할 수 있다. 짧게 단편화에 대해 설명하자면, 전체적인 메모리 양은 요청된 메모리를 할당하기에 충분한 양인데, 연속되지 않아서 할당할 수가 없는 상황을 <strong>외부 단편화</strong>라고 부른다. 메모리가 비효율적으로 사용되고 있는 상황이고, 이런 파편화된 메모리 상태에서는 메모리 할당을 위해 메모리 공간을 찾는 시간도 늘어난다.</p><blockquote><p>💡 <a href="https://en.wikipedia.org/wiki/Mark-compact_algorithm">Mark-Compact 방식</a>을 쉽게 찾아볼 수 있었는데, 위에서 간단히 설명한 Mark & Sweep 방식에서 컴팩팅을 추가한 방식이다. 마킹 페이즈 이후 컴팩팅 페이즈가 존재해서 데이터들을 압축하고 이동한 오브젝트의 포인터를 업데이트 하는 과정을 거치게 된다.</p></blockquote><h1 id="Go의-GC"><a href="#Go의-GC" class="headerlink" title="Go의 GC"></a>Go의 GC</h1><p>이제 Go에서 어떻게 GC를 구성하고 있는지 확인해보자. <a href="https://cs.opensource.google/go/go/+/refs/tags/go1.17.8:src/runtime/mgc.go">Go의 코드</a> 주석으로 설명된 바에 따르면 Go는 비세대별, 비압축, Concurrent Tri-color Mark & Sweep이라고 한다.</p><ul><li>비세대별: 힙 영역을 세대별로 관리하지 않는다.</li><li>비압축: 힙 영역의 Compaction을 수행하지 않는다.</li><li>Concurrent Tri-color Mark & Sweep: 마킹과 해제 과정이 STW 없이 애플리케이션과 동시에 동작하고, 삼색 마킹 알고리즘으로 구현되어 있다.</li></ul><h2 id="Collector"><a href="#Collector" class="headerlink" title="Collector"></a>Collector</h2><p>Go GC는 세 개의 페이즈를 수행한다. 이 페이즈들 중 두 개는 STW를 유발하고, 다른 한 페이즈는 애플리케이션의 CPU 처리량을 느리게 만든다. 세 개의 페이즈는 다음과 같다.</p><ul><li>Mark 준비 - STW</li><li>Marking - Concurrent</li><li>Mark 종료 - STW</li></ul><h3 id="Mark-준비-STW"><a href="#Mark-준비-STW" class="headerlink" title="Mark 준비 - STW"></a>Mark 준비 - STW</h3><p>GC가 시작되면서 가장 먼저 해야 할 일은 <strong>Write Barrier</strong>가 동작하도록(Enabled) 만드는 것이다. Go에서 Write Barrier는 동시적인 GC 마킹 과정에서도 힙 영역의 데이터 정합성을 유지해주는 장치이다. 위에서 살짝 써놨는데, 마킹 단계는 애플리케이션 고루틴과 GC 고루틴이 동시에 동작한다. 마킹을 하던 도중 애플리케이션 고루틴에서 힙 영역에 대한 변경 작업을 하게 되면 GC도 이를 인지하고 적절한 조치를 취해야 한다. 이것을 가능하게 해주는 것이 Write Barrier이다. 구체적으로 어떻게 해주는지는 이후 설명한다.</p><blockquote><p>💡 Write Barrier라는 용어나 컴포넌트가 Go GC의 특수한 개념은 아니다. 동시적인 힙 영역에 대한 접근을 하기에 앞서 필요한 전처리 작업을 해주는 장치 정도로 사용이 되는 것 같은데, Java에서는 Old 영역에서 Young 영역을 참조할 때 Card Table에 기록하는 역할을 Write Barrier가 한다.</p></blockquote><p>Write Barrier가 시작되려면 모든 애플리케이션의 고루틴들이 멈춰야 한다. 일반적으로 이 동작은 아주 빨라서 STW가 거의 발생하지 않는 것처럼 보인다.</p><h3 id="Marking-Concurrent"><a href="#Marking-Concurrent" class="headerlink" title="Marking - Concurrent"></a>Marking - Concurrent</h3><p>Write Barrier가 켜지고 나면 마킹이 시작된다. GC가 이 단계에서 처음 하는 일은 25% 정도의 CPU 처리량을 가져오는 것이다. 예를 들어 4개의 P가 있으면 그중 하나는 GC를 수행하기 위해 점유(dedicated)된다.</p><p><img src="/images/2022-03-25-go-gc/gc-dedicated.png?style=centerme" alt="Dedicated Goroutine"></p><blockquote><p>💡 위 이미지는 Go의 고루틴 스케줄링에 대해 알고 있으면 이해가 편한데, 만약 모른다면 사용 중인 스레드 중 하나가 점유된 이미지라고 이해하자. 그러나 엄밀히 말하면 틀린 소리기 때문에 시간이 된다면 Go GMP 구조에 대해 알아보자.</p></blockquote><p>그다음 진짜 마킹을 하게 된다. 일단 현재 존재하는 모든 애플리케이션 고루틴 스택을 확인하면서 힙을 참조하고 있는 포인터를 확인한다. 스택을 스캔하는 과정은 해당 고루틴을 멈추게 한다. 하지만 그 이후 힙 안에서 오브젝트들을 따라가는 과정은 애플리케이션 고루틴과 동시에 동작한다. 다만 25%가량의 CPU 처리량을 사용하지 못하기 때문에 그만큼의 성능 저하가 발생한다.</p><p>만약 할당 속도가 너무 빨라서 고루틴이 사용 중인 힙 메모리 한계에 도달 전에 마킹 작업이 완료되지 못한다면 어떻게 될까? 할당이 지속되어 해당 오브젝트를 마킹 하느라 마킹 작업이 끝나지 않는다면? 이 상황이면 고루틴의 할당 속도를 낮출 필요가 있다.</p><p>GC가 힙 할당 속도를 제어해야 하는 상황이 되면 애플리케이션 고루틴 중에서 마킹 작업을 도와줄 어시스트 고루틴을 선정한다. 이를 <strong>Mark Assist</strong>라고 부른다. 애플리케이션 고루틴이 Mark Assist 역할을 하는 시간은 힙 영역에 추가되는 데이터 양에 비례한다. Mark Assist가 선정되면 그만큼 애플리케이션의 할당 속도는 줄고, 마킹 작업 속도가 빨라지는 효과가 있다. 그러나 애플리케이션 로직을 수행하는 비율이 더 줄어드는 것이기 때문에 속도 저하의 원인이 되기도 한다.</p><hr><p>Tri-color Mark & Sweep에 대해 자세히 알아보자. 아래 이미지가 알고리즘 방식이다.</p><p><img src="/images/2022-03-25-go-gc/tricolor.gif?style=centerme" alt="Tri-color Mark & Sweep"><br><small>이미지 출처: <a href="https://programming.vip/docs/deep-understanding-of-go-garbage-recycling-mechanism.html">링크</a></small></p><ol><li>먼저 모든 오브젝트는 하얀색 집합에서 시작한다.</li><li>루트 오브젝트를 회색 마킹한다.</li><li>회색으로 마킹된 오브젝트를 순회하면서 참조하고 있는 오브젝트들을 회색으로 칠한다.</li><li>순회를 마친 회색 오브젝트는 검은색으로 마킹한다.</li><li>3, 4번 스탭을 회색 오브젝트가 없어질 때까지 반복한다.</li><li>여전히 흰색 집합에 있는 오브젝트를 할당 해제한다.</li></ol><p>위 과정은 STW 상태가 아니기 때문에 동시에 오브젝트 변경이 지속해서 발생한다. 위에서 언급한 것처럼 GC가 동작하는 도중에 애플리케이션 고루틴이 힙에 변경을 가하면 Write Barrier가 적절한 조치를 취한다. 예를 들어서 GC 도중 스택에서 새롭게 할당하는 오브젝트는 바로 검은색으로 마킹한다.</p><p>이미 존재하는 오브젝트 트리 구조에서 변경점이 생기면 Write Barrier에서는 변경이 생기기 전 <code>Original Pointer</code>와 변경이 생긴 <code>New Pointer</code>를 기록하고 두 포인터 모두 마킹 처리를 한다.</p><p><code>Original Pointer</code>에 마킹처리를 하는 이유는 포인터 값을 스택이나 레지스터에 복사해두는 경우, Write Barrier를 거치지 않기 때문이다. Write Barrier는 힙 영역을 대상으로 발생하는 변경 점에 대한 전처리 작업을 하는 것이기 때문에, 로컬 스택이나 레지스터에 복사가 발생했는지 알 수 없다.</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">[<span class="keyword">go</span>] b = obj</span><br><span class="line">[<span class="keyword">go</span>] oldx = <span class="literal">nil</span></span><br><span class="line">[gc] scan oldx...</span><br><span class="line">[<span class="keyword">go</span>] oldx = b.x <span class="comment">// b.x를 Write Barrier를 거치지 않고 로컬 변수 oldx에 복사한다.</span></span><br><span class="line">[<span class="keyword">go</span>] b.x = ptr <span class="comment">// Write Barrier는 원래 b.x 값 역시 체크한다.</span></span><br><span class="line">[gc] scan b...</span><br><span class="line"><span class="comment">//만약 Write Barrier가 원래 값을 마킹하지 않는다면 oldx가 스캔 되지 않는다.</span></span><br></pre></td></tr></table></figure><p>위와 같은 상황처럼, 스택에 복사된 상태로 사용할 때, 스캔하면서 할당 해제되는 상황을 막아준다.</p><p><code>New Pointer</code> 역시 마킹 처리하는 이유는 다른 고루틴에서 포인터의 위치를 바꿀 수 있기 때문이다.</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">[<span class="keyword">go</span>] a = ptr</span><br><span class="line">[<span class="keyword">go</span>] b = obj</span><br><span class="line">[gc] scan b...</span><br><span class="line">[<span class="keyword">go</span>] b.x = a <span class="comment">// Write Barrier는 새로운 b.x 값을 마킹하도록 한다.</span></span><br><span class="line">[<span class="keyword">go</span>] a = <span class="literal">nil</span></span><br><span class="line">[gc] scan a...</span><br><span class="line"><span class="comment">//만약 새로운 값을 마킹하지 않는다면, ptr 값은 스캔 되지 않는다.</span></span><br></pre></td></tr></table></figure><p>위 상황처럼 만약 Write Barrier가 없다면 이미 스캔을 진행한 오브젝트에 아직 스캔을 진행하지 않은 포인터가 붙고 기존의 포인터를 담던 변수에서 제거되면 해당 힙 오브젝트가 스캔 되지 않을 수 있다.</p><p>이런 이유로 Write Barrier가 <code>Original Pointer</code>, <code>New Pointer</code> 모두 마킹 작업을 수행하도록 만들어주고, 동시적인 상황에서도 안전하게 힙 마킹을 유지할 수 있다.</p><h3 id="Mark-종료-STW"><a href="#Mark-종료-STW" class="headerlink" title="Mark 종료 - STW"></a>Mark 종료 - STW</h3><p>마킹 작업이 끝나면 Write Barrier와 Mark Assist를 종료하고 다음 GC가 동작할 목표치를 계산하게 된다. 이 과정은 STW 없이 동작할 수 있는데, 구현 시 코드 복잡성이 과하게 증가하는 반면 그에 비해 얻는 이점이 너무 작아 STW 상태로 진행된다고 한다.</p><p>다음 GC 수행을 위한 목표치 계산 알고리즘을 <strong>Pacing Algorithm</strong>이라고 부른다. 알고리즘은 콜렉터가 실행 중인 애플리케이션의 힙 사이즈 정보와, 힙에 가해지는 강도(Stress)에 의해 정의된다. Go에서는 GC Percent 값을 Go 환경 변숫값으로 설정해 GC가 동작하는 속도를 조절할 수 있다. 이 환경 변수 이름은 <code>GOGC</code>인데, 기본값은 100이다. 이는 현재 정리된 이후 힙 메모리보다 100% 커지면 다시 GC가 동작한다는 것을 의미한다. 즉, 기본값으로는 대략 2배 사이즈가 될 때마다 GC가 동작한다.</p><h3 id="Sweep-과정"><a href="#Sweep-과정" class="headerlink" title="Sweep 과정?"></a>Sweep 과정?</h3><p>어떤 글에서는 Sweep 페이즈에 대해 따로 페이즈로 나눠서 설명하기도 하는데, 이는 GC 사이클과 조금 독립적으로 동작하기 때문에 GC의 페이즈로 설명하지 않았다. Sweep은 애플리케이션과 함께 동시적으로 동작하는데, 애플리케이션에서 힙 영역에 할당을 요청했을 때 필요한 경우 삭제 처리된 오브젝트를 게으르게 할당 해제한다. 즉, 할당 시점에 Sweep이 발생하고 GC 수행 시간과는 무관하다. 그리고 다음 GC가 수행되기 전까지 아직 청소되지 않은 메모리 영역이 있다면, 모두 클린업 처리해주면서 다음 GC가 시작된다.</p><h2 id="비압축-방식"><a href="#비압축-방식" class="headerlink" title="비압축 방식"></a>비압축 방식</h2><p>압축을 통해 단편화 문제를 해결할 수 있는데, Go는 이 방법을 사용하고 있지 않다. 그렇다면 이 문제는 어떻게 해결하고 있을까? 이 문제는 현대 메모리 할당 방식에서 많이 해결해주고 있다고 한다. 전통적으로 프로세스 안에서 힙을 공유해 메모리를 할당해주는 방식은 멀티 스레드 프로그래밍에서는 그다지 적합한 방식이 아니다. 힙에 접근해 할당하는 과정에 Lock이 필요하기 때문이다. Go는 Google에서 만든 <strong>TCMalloc</strong>이라는 메모리 할당 방식을 활용하고 있다.</p><blockquote><p>💡 “TCMalloc Like”라고 표현하던데, <a href="http://goog-perftools.sourceforge.net/doc/tcmalloc.html">TCMalloc</a> 방법을 사용했다고 이해해도 무방할 것 같다.</p></blockquote><h3 id="메모리-할당-방법-Overview"><a href="#메모리-할당-방법-Overview" class="headerlink" title="메모리 할당 방법 Overview"></a>메모리 할당 방법 Overview</h3><p>조금 개괄적으로 설명하자면, TCMalloc은 중앙 힙과 함께 스레드마다 로컬 스레드 캐시를 가지고 있고, 작은 할당은 로컬 스레드 캐시에서 해결한다. 필요에 따라 로컬 스레드 캐시에 새로운 메모리 영역을 할당해주거나, 중앙 힙에서 직접 큰 메모리 덩어리를 떼어 사용하기도 한다. 로컬 스레드 캐시로 인해 Lock이 필요 없는 할당이 빠르게 진행되기도 하고, 힙의 파편화된 영역을 최소화할 수 있는 원리로 작용하는 것 같다.</p><hr><p>아래 구체적인 내용은 몰라도 남은 내용들을 이해하는 데 문제가 없다. 궁금한 사람들은 보기로 하자.</p><h3 id="작은-메모리-할당"><a href="#작은-메모리-할당" class="headerlink" title="작은 메모리 할당"></a>작은 메모리 할당</h3><p>위에서 짧게 설명했지만, 작은 메모리를 할당하는 전략과 큰 메모리를 할당하는 전략이 다르다. 작은(32kb 이하) 할당을 할 때는 로컬 캐시인 <code>mcache</code>라고 불리는 메모리를 가져오려고 한다. 이 캐시는 32kb 짜리 청크 리스트인 <code>mspan</code> 리스트를 가지고 있다.</p><p><img src="/images/2022-03-25-go-gc/mcache.png?style=centerme" alt="mcache & mspan"><br><small>이미지 출처: <a href="https://medium.com/a-journey-with-go/go-memory-management-and-allocation-a7396d430f44">링크</a></small></p><p>고루틴 G를 처리하는 P에서 물고 있는 <code>mspan</code> 중 하나의 캐시를 사용해서 작은 범위의 할당을 한다. 이 과정은 힙 영역이 아니라서 Lock이 불필요하다. <code>mspan</code>은 32kb를 여러 사이즈로 나눈 여러 종류로 가지고 있다. 8bytes부터 32kb까지 클래스가 나눠진다.</p><p><img src="/images/2022-03-25-go-gc/mspan-class.png?style=centerme" alt="mspan 클래스"><br><small>이미지 출처: <a href="https://medium.com/a-journey-with-go/go-memory-management-and-allocation-a7396d430f44">링크</a></small></p><p>그럼 만약 할당하려고 할 때 이 <code>mspan</code> 리스트에 충분한 슬롯이 없다면 어떻게 될까? Go는 중앙에 <code>mcentral</code>이라고 하는 메모리 공간을 관리한다. <code>mcentral</code>에는 두 가지 종류의 스판 리스트가 있다. 하나는 꽉 찬 스판과 다른 하나는 그렇지 않은 스판 리스트이다.</p><p><img src="/images/2022-03-25-go-gc/mcentral.png?style=centerme" alt="mcentral"><br><small>이미지 출처: <a href="https://medium.com/a-journey-with-go/go-memory-management-and-allocation-a7396d430f44">링크</a></small></p><p><code>mcentral</code>에서는 스판 리스트가 양방향 연결 리스트로 되어있다. <code>mcache</code>에서 <code>mspan</code>이 꽉차게 되면 <code>mcentral</code>에서 빈 스판 리스트를 가져온다.</p><p><img src="/images/2022-03-25-go-gc/new-mspan.png?style=centerme" alt="새로운 mspan"><br><small>이미지 출처: <a href="https://medium.com/a-journey-with-go/go-memory-management-and-allocation-a7396d430f44">링크</a></small></p><p>만약 <code>mcentral</code>에서 제공할 수 있는 리스트가 없으면 힙에서 새로 할당받는다.</p><p><img src="/images/2022-03-25-go-gc/new-mcentral.png?style=centerme" alt="새로운 mcentral 스판 리스트"><br><small>이미지 출처: <a href="https://medium.com/a-journey-with-go/go-memory-management-and-allocation-a7396d430f44">링크</a></small></p><p>힙이 메모리가 더 필요한 경우 OS로부터 메모리를 가져온다. 이때 새롭게 할당하는 영역은 <code>arena</code>라고 불리는 커다란 메모리 덩어리이다. 64bits 아키텍처일 때 64MB를 할당받고, 32bits인 경우 4MB를 할당받는다.</p><h3 id="큰-메모리-할당"><a href="#큰-메모리-할당" class="headerlink" title="큰 메모리 할당"></a>큰 메모리 할당</h3><p>32kb보다 큰 메모리를 할당하게 되면 로컬 캐시를 사용하지 않는다. 할당되는 메모리 사이즈는 페이즈 사이즈로 올림 처리해 힙에 직접 할당한다.</p><hr><p>대략적인 전체 흐름 이미지는 다음과 같다.<br><img src="/images/2022-03-25-go-gc/memory-overview.png?style=centerme" alt="Overview"><br><small>이미지 출처: <a href="https://medium.com/a-journey-with-go/go-memory-management-and-allocation-a7396d430f44">링크</a></small></p><h2 id="비세대별-GC"><a href="#비세대별-GC" class="headerlink" title="비세대별 GC"></a>비세대별 GC</h2><p>힙 메모리를 스캔하는 범위를 좁히는 방법으로 비세대별 GC에 관해 설명했었다. Go에서는 이 부분이 <a href="https://groups.google.com/g/golang-nuts/c/KJiyv2mV2pU/m/wdBUH1mHCAAJ">도입되면 충분히 장점이 있을 것이라고 하지만</a>, 현재는 도입된 상태가 아니라고 한다.</p><p>Go에서는 컴파일 최적화 과정인 Escape Analysis 단계에서 다른 언어와 다르게 실제 동적 할당하는 많은 부분을 스택에 할당하도록 한다. 세대별 알고리즘의 대전제인 “많은 오브젝트들은 수명이 짧다”에 해당하는 부분을 스택에 할당함으로써 GC의 대상이 아니게 만들어준다. 따라서 다른 언어에 비해 세대별 GC를 사용하는 것으로 생길 수 있는 장점이 비교적 작다.</p><hr><p>일단 여기까지 내용이 Go의 GC가 어떻게 동작하는지, 그리고 왜 이런지에 관한 내용이다. 이후는 GC를 컨트롤하려는 케이스를 예시로 가져왔다. 위 내용을 모두 포함하고 있어서, 잘 이해했다면 아래 내용이 재밌다.</p><h1 id="Case-Study"><a href="#Case-Study" class="headerlink" title="Case Study"></a>Case Study</h1><h2 id="GC-Tuning-옵션에-관한-이야기"><a href="#GC-Tuning-옵션에-관한-이야기" class="headerlink" title="GC Tuning 옵션에 관한 이야기"></a>GC Tuning 옵션에 관한 이야기</h2><p><a href="https://youtu.be/uyifh6F_7WM">dotGo 2019 컨퍼런스</a>에서 Go GC를 어떻게 쓸 수 있는지 설명한 얘기가 있다. Go는 GC 관련 설정을 할 수 있는 방법이 위에서 언급한 <code>GOGC</code> 환경 변숫값 하나뿐이다. 다음 두 가지 상황에서 <code>GOGC</code>가 어떻게 될지 설명하고 있다.</p><ul><li><p>상황 1: 안정적인 큰 데이터셋이 있다면?<br>예를 들어서 20GB가 고정된 사이즈의 데이터라고 해보자. <code>GOGC=100</code>이라면 다음 GC는 40GB가 될 때 발생한다. 메모리 낭비가 굉장히 심한 상황인데 <code>GOGC=50</code>으로 바꾸면 30GB에 동작하게 바뀐다.</p></li><li><p>상황 2: 고정된 데이터 사이즈가 없는 애플리케이션 (작은 힙을 가지고 시작)<br>10MB의 힙 사이즈를 들고 시작했다고 가정해보자. GC는 20MB에 발생할 것이고, 정리되고 나서도 금방 다음 GC 사이클이 돌아온다. 이런 경우 <code>GOGC</code> 사이즈를 조금 여유있게 잡아주면 GC가 덜 발생한다.</p></li></ul><hr><p>위 컨퍼런스의 내용을 대충 요약하면 고정 메모리 소비량이 많으면 메모리 효율성을 위해 <code>GOGC</code> 값을 줄이고, 그 반대 상황에서는 GC 사이클을 줄이기 위해 <code>GOGC</code> 값을 크게 만들자는 내용이다. 굉장히 단순한 방법.</p><h2 id="Twitch에서-Go-애플리케이션의-힙-사이즈를-수동으로-조절해-GC-OPS를-줄인-이야기"><a href="#Twitch에서-Go-애플리케이션의-힙-사이즈를-수동으로-조절해-GC-OPS를-줄인-이야기" class="headerlink" title="Twitch에서 Go 애플리케이션의 힙 사이즈를 수동으로 조절해 GC OPS를 줄인 이야기"></a>Twitch에서 Go 애플리케이션의 힙 사이즈를 수동으로 조절해 GC OPS를 줄인 이야기</h2><p>Twitch는 Visage라는 프론트앤드가 바라보고 있는 API Gateway 앱을 가지고 있다. 이 앱은 EC2 + LoadBalancer 위에서 돌고있는 Go 애플리케이션이다. AWS 컴포넌트로 기본적인 스케일링 처리가 가능하지만, 애플리케이션 자체적으로 CPU 처리량이 급격히 떨어지는 상황이 있었다고 한다. Twitch에서는 이를 “리프레시 스톰”이라고 불렀다. 인기 있는 방송인의 인터넷 상태가 안 좋아지는 경우 시청자들이 다 같이 새로고침을 연타하는 경우 생기는 문제이기 때문이다. 이 경우에는 평소보다 약 20배가 넘는 트래픽을 유발한다고 한다.</p><p>트위치는 Go 프로파일링 옵션을 프로덕션에서도 켜놔서 쉽게 프로파일링 결과를 얻을 수 있었는데, 다음과 같은 보고를 얻었다고 한다.</p><ul><li>안정적인 상태에서는 GC가 초당 8 - 10회 발생 (8 ~ 10 OPS)</li><li>30%의 CPU 사이클이 GC와 유관한 함수를 호출하기 위해 사용</li><li>리프래시 스톰 상황에서는 GC OPS 급증</li><li>평균적인 힙 사이즈는 450MiB</li></ul><blockquote><p>💡 프로파일링 옵션을 켜두는 것이 그렇게 오버헤드가 있지는 않다고 한다. Excution tracer는 오버헤드가 있을 수 있는데 시간당 몇 초 정도 수행할 정도로 수행 빈도가 별로 안된다고 한다.</p></blockquote><p>GC OPS를 줄이고 STW 시간을 줄일 목적으로 밸러스트(바닥짐, Ballast)를 수동으로 만들어줬다. 앱이 시작할 때 아주 큰 메모리 사이즈를 힙에 할당해버리는 방법이었다.</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="comment">// 10 GiB 할당 해버리기</span></span><br><span class="line"> ballast := <span class="built_in">make</span>([]<span class="type">byte</span>, <span class="number">10</span><<<span class="number">30</span>)</span><br><span class="line"> <span class="comment">// 앱 실행 진행</span></span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>기본 <code>GOGC</code>를 유지한 상태였기 때문에, 밸러스트를 만듦으로써 약 10GB의 할당이 더 발생해야 GC가 동작했다. 결과적으로는 GC OPS가 99% 감소했다.</p><p><img src="/images/2022-03-25-go-gc/twitch-gc-rate.png?style=centerme" alt="GC Rate"><br><small>이미지 출처: <a href="https://blog.twitch.tv/en/2019/04/10/go-memory-ballast-how-i-learnt-to-stop-worrying-and-love-the-heap/">링크</a></small></p><p>CPU 활용도 30%가량 내려갔다.</p><p><img src="/images/2022-03-25-go-gc/twitch-cpu-util.png?style=centerme" alt="CPU Utilization"><br><small>이미지 출처: <a href="https://blog.twitch.tv/en/2019/04/10/go-memory-ballast-how-i-learnt-to-stop-worrying-and-love-the-heap/">링크</a></small></p><p><code>GOGC</code>를 설정하지 않고 직접 밸러스트를 만든 이유는 다음과 같다.</p><ul><li>GC 발생 비율은 관계가 없고, 총 메모리 사용량이 더 중요한 상황</li><li>밸러스트와 같은 효과를 발생시키려면 아주 큰 <code>GOGC</code>가 필요한데, 그렇게 하면 힙에 유지되는 메모리의 크기 변경에 아주 민감해짐</li><li>라이브 메모리와 변화하는 비율을 추론하는 것 보다, 전체 메모리를 추론하는 것이 훨씬 쉬움</li></ul><p>그렇다면 소중한 10GiB 메모리가 그대로 소비되는 것은 아닐까? 실제 시스템 메모리는 OS에 의해 페이지 테이블을 통해 가상 주소가 지정되고 물리 메모리와 매핑된다. 위 밸러스트를 설정하는 코드가 실행되면 가상 메모리에 배열이 할당되고 실제 읽기 쓰기를 시도하면 페이지 폴트가 발생하면서 실제 메모리에 적재하는 과정이 발생한다. 따라서, 밸러스트가 물리 메모리를 차지하고 있지는 않다.</p><p>API 레이턴시 역시 많이 향상되었는데, Twitch는 처음에는 STW 자체가 줄어서라고 생각했지만, 실제로 STW가 줄어든 절대적인 시간 자체는 아주 짧았다. 실제로 성능 향상에 많은 영향을 줬던 것은 Mark Assist가 줄었기 때문이다. 위에서 언급했던 것처럼 Mark Assist가 동작하면 애플리케이션 입장에서는 CPU 처리량을 더 뺏기는 것이기 때문에 처리량이 줄어든다.</p><h1 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h1><ul><li><a href="https://deepu.tech/memory-management-in-programming/">https://deepu.tech/memory-management-in-programming/</a></li><li><a href="https://spin.atomicobject.com/2014/09/03/visualizing-garbage-collection-algorithms/">https://spin.atomicobject.com/2014/09/03/visualizing-garbage-collection-algorithms/</a></li><li><a href="https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/generations.html">https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/generations.html</a></li><li><a href="https://d2.naver.com/helloworld/1329">https://d2.naver.com/helloworld/1329</a></li><li><a href="https://en.wikipedia.org/wiki/Mark-compact_algorithm">https://en.wikipedia.org/wiki/Mark-compact_algorithm</a></li><li><a href="https://cs.opensource.google/go/go/+/refs/tags/go1.17.8:src/runtime/mgc.go">https://cs.opensource.google/go/go/+/refs/tags/go1.17.8:src/runtime/mgc.go</a></li><li><a href="https://www.ardanlabs.com/blog/2018/12/garbage-collection-in-go-part1-semantics.html">https://www.ardanlabs.com/blog/2018/12/garbage-collection-in-go-part1-semantics.html</a></li><li><a href="https://programming.vip/docs/deep-understanding-of-go-garbage-recycling-mechanism.html">https://programming.vip/docs/deep-understanding-of-go-garbage-recycling-mechanism.html</a></li><li><a href="http://goog-perftools.sourceforge.net/doc/tcmalloc.html">http://goog-perftools.sourceforge.net/doc/tcmalloc.html</a></li><li><a href="https://medium.com/a-journey-with-go/go-memory-management-and-allocation-a7396d430f44">https://medium.com/a-journey-with-go/go-memory-management-and-allocation-a7396d430f44</a></li><li><a href="https://groups.google.com/g/golang-nuts/c/KJiyv2mV2pU/m/wdBUH1mHCAAJ">https://groups.google.com/g/golang-nuts/c/KJiyv2mV2pU/m/wdBUH1mHCAAJ</a></li><li><a href="https://en.wikipedia.org/wiki/Escape_analysis">https://en.wikipedia.org/wiki/Escape_analysis</a></li><li><a href="https://youtu.be/uyifh6F_7WM">https://youtu.be/uyifh6F_7WM</a></li><li><a href="https://blog.twitch.tv/en/2019/04/10/go-memory-ballast-how-i-learnt-to-stop-worrying-and-love-the-heap/">https://blog.twitch.tv/en/2019/04/10/go-memory-ballast-how-i-learnt-to-stop-worrying-and-love-the-heap/</a></li></ul>]]></content:encoded>
<category domain="https://changhoi.kim/categories/go/">go</category>
<category domain="https://changhoi.kim/tags/gc/">gc</category>
<comments>https://changhoi.kim/posts/go/go-gc/#disqus_thread</comments>
</item>
</channel>
</rss>