<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Devsisters 기술 블로그]]></title><description><![CDATA[데브시스터즈의 서비스를 만드는 개발자들이 경험한 실제 개발 사례를 중심으로 기술과 노하우를 생생하게 전달합니다.]]></description><link>https://tech.devsisters.com</link><generator>GatsbyJS</generator><lastBuildDate>Fri, 04 Sep 2026 03:32:04 GMT</lastBuildDate><item><title><![CDATA[Kafka Streams를 k8s로 옮기며 얻은 스케일링 설계: 비용 80% 절감까지]]></title><description><![CDATA[EC2에서 운영하던 Kafka Streams 애플리케이션을 Kubernetes로 옮기면서, CPU 기반 HPA의 한계를 확인하고 KEDA와 consumer lag 기반 스케일링으로 인프라 비용을 80% 이상 절감한 과정을 공유합니다.]]></description><link>https://tech.devsisters.com/posts/kafka-streams-keda-scaling</link><guid isPermaLink="false">https://tech.devsisters.com/posts/kafka-streams-keda-scaling</guid><pubDate>Thu, 03 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요. 기술본부 데이터엔지니어링셀 김태완입니다.&lt;/p&gt;
&lt;p&gt;이 글에서는 AWS EC2 기반으로 운영되던 Log Transformer(Kafka Streams 애플리케이션)를 Kubernetes(이하 k8s)로 마이그레이션하면서, Kafka consumer 워크로드에 맞는 스케일링 정책을 설계한 과정과 그 결과를 공유합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;이 글에서 다루는 내용은 다음과 같습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CPU 기반 HPA가 Kafka consumer 워크로드에 부적합한 이유&lt;/li&gt;
&lt;li&gt;KEDA와 consumer lag을 활용한 도메인 메트릭 기반 스케일링 설계&lt;/li&gt;
&lt;li&gt;자정 트래픽 급증에 cron trigger로 선제 대응하는 방법&lt;/li&gt;
&lt;li&gt;pod당 thread 다중화로 JVM 오버헤드와 rebalance 이벤트를 줄이는 방법&lt;/li&gt;
&lt;li&gt;그 결과로 얻은 EC2 대비 &lt;strong&gt;80% 이상의 인프라 비용 절감&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;배경-ec2-기반-log-transformer의-한계&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B0%B0%EA%B2%BD-ec2-%EA%B8%B0%EB%B0%98-log-transformer%EC%9D%98-%ED%95%9C%EA%B3%84&quot; aria-label=&quot;배경 ec2 기반 log transformer의 한계 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;배경: EC2 기반 Log Transformer의 한계&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Log Transformer&lt;/strong&gt;는 여러 Kafka topic에서 원본 로그를 읽어 검증·변환한 뒤, &lt;strong&gt;게임별 Kafka topic으로 다시 produce&lt;/strong&gt;하는 Kafka Streams 애플리케이션입니다. 여러 source topic의 원본 로그를 게임·용도별 topic으로 분기시키므로, 후속 파이프라인은 필요한 topic만 골라 소비할 수 있습니다.&lt;/p&gt;
&lt;p&gt;기존에는 &lt;strong&gt;c6g.2xlarge 인스턴스 72대&lt;/strong&gt; 위에서 운영되고 있었으며, 다음과 같은 한계를 안고 있었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;트래픽 패턴과 어긋나는 인스턴스 운영&lt;/strong&gt;: 원래는 ASG(Auto Scaling Group)에 cron 기반 스케줄을 걸어 시간대에 따라 36대와 72대를 오가도록 운영했습니다. 그러나 이 방식이 불안정해 결국 피크 기준인 72대로 고정하게 되었고, 트래픽이 적은 시간대에도 같은 규모를 그대로 유지하고 있었습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;스케일링의 경직성&lt;/strong&gt;: 트래픽 변화에 맞추려면 그때마다 사람이 ASG 설정을 직접 바꿔야 했습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;배포 파이프라인의 복잡성&lt;/strong&gt;: Spinnaker 기반 EC2 배포는 단계가 많고 반영까지 오래 걸려 단순한 설정 변경에도 부담이 컸습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 한계들은 결국 트래픽에 실시간으로 반응하는 자동 스케일링과 빠르고 단순한 배포가 없다는 데서 비롯됐습니다. k8s는 HPA, KEDA 같은 오토스케일러로 트래픽 변화에 자동으로 반응할 수 있고, 선언형 매니페스트와 GitOps 배포로 설정 변경 부담도 줄일 수 있습니다. 그래서 저희는 Log Transformer를 k8s 기반으로 전환하기로 결정했습니다.&lt;/p&gt;
&lt;p&gt;마이그레이션의 첫 단계로, 하나의 애플리케이션이 여러 source topic을 함께 consume하던 구조를 source topic별 독립 Kafka Streams 애플리케이션으로 분리했습니다. topic마다 트래픽 패턴이 다르므로 스케일링 정책을 독립적으로 적용하고, 장애와 배포를 topic 단위로 격리하기 위함이었습니다. 이렇게 구조 분리를 마친 뒤, 본격적으로 스케일링 정책 설계에 들어갔습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;첫-시도-cpu-기반-hpa가-실패한-이유&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B2%AB-%EC%8B%9C%EB%8F%84-cpu-%EA%B8%B0%EB%B0%98-hpa%EA%B0%80-%EC%8B%A4%ED%8C%A8%ED%95%9C-%EC%9D%B4%EC%9C%A0&quot; aria-label=&quot;첫 시도 cpu 기반 hpa가 실패한 이유 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;첫 시도: CPU 기반 HPA가 실패한 이유&lt;/h2&gt;
&lt;p&gt;k8s로 올린 뒤 가장 먼저 시도한 것은 기본적인 CPU 기반 HPA(Horizontal Pod Autoscaler)였습니다. 이 글의 YAML에 나오는 replica 수와 임계값은 설명을 위한 예시 값입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; autoscaling/v2
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; HorizontalPodAutoscaler
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; log&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;transformer&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;hpa
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;minReplicas&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;maxReplicas&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;metrics&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Resource
      &lt;span class=&quot;token key atrule&quot;&gt;resource&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; cpu
        &lt;span class=&quot;token key atrule&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Utilization
          &lt;span class=&quot;token key atrule&quot;&gt;averageUtilization&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;70&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;결과는 기대와 달랐습니다. 하나의 partition은 consumer group 안에서 한 consumer(여기서는 pod)에만 배정되므로, partition이 consumer들에 고르게 나뉘어야 부하도 고르게 퍼집니다. 그러나 실제로는 partition이 consumer에 고르게 분배되지 않아, 일부 pod가 CPU 100%에 도달하는 동안에도 평균 CPU는 target 아래에 머물러 HPA가 반응하지 않았습니다.&lt;/p&gt;
&lt;h3 id=&quot;나누어떨어지지-않는-partition과-부하-집중&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%82%98%EB%88%84%EC%96%B4%EB%96%A8%EC%96%B4%EC%A7%80%EC%A7%80-%EC%95%8A%EB%8A%94-partition%EA%B3%BC-%EB%B6%80%ED%95%98-%EC%A7%91%EC%A4%91&quot; aria-label=&quot;나누어떨어지지 않는 partition과 부하 집중 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;나누어떨어지지 않는 partition과 부하 집중&lt;/h3&gt;
&lt;p&gt;Kafka Streams는 partition을 task 단위로 묶어 &lt;code&gt;StreamsPartitionAssignor&lt;/code&gt;로 분배하며, 각 인스턴스가 가진 stream thread 수에 비례해 task를 나눠줍니다. 문제는 그다음입니다. task 수가 전체 thread 수로 나누어떨어지지 않으면 남는 task는 어딘가로 가야 하므로, 일부 consumer가 task를 1개씩 더 받고 그만큼 부하가 집중됩니다.&lt;/p&gt;
&lt;p&gt;이것은 특정 할당 전략의 약점이 아닙니다. task 수가 전체 thread 수로 나누어떨어지지 않는 한, 어떤 할당 전략을 쓰더라도 남는 편차입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;[상황 예시: partition 6개, consumer 4대]

6 / 4 = 1 나머지 2 → consumer 2대가 partition을 1개씩 더 가져갑니다.

Consumer A: partition 0, 1  → CPU 80%
Consumer B: partition 2, 3  → CPU 80%
Consumer C: partition 4     → CPU 40%
Consumer D: partition 5     → CPU 40%

평균 CPU: 60%
HPA target: 70%
→ 스케일링 조건 미충족. Consumer A, B는 처리가 밀립니다.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;cpu-평균-지표의-전제-조건&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#cpu-%ED%8F%89%EA%B7%A0-%EC%A7%80%ED%91%9C%EC%9D%98-%EC%A0%84%EC%A0%9C-%EC%A1%B0%EA%B1%B4&quot; aria-label=&quot;cpu 평균 지표의 전제 조건 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;CPU 평균 지표의 전제 조건&lt;/h3&gt;
&lt;p&gt;여기서 &lt;strong&gt;lag&lt;/strong&gt;은 topic에 쌓인 메시지 중 consumer가 아직 처리하지 못한 메시지 수를 뜻합니다. partition이 많이 할당된 consumer에서는 처리가 밀려 lag이 쌓이고 해당 pod의 CPU도 100%까지 치솟지만, pod 수가 많아 평균 CPU는 target 이하에 머물기 때문에 HPA는 끝내 반응하지 않습니다.&lt;/p&gt;
&lt;p&gt;CPU 평균은 워크로드가 애플리케이션의 pod 전체에 고르게 분산된다는 전제에서만 유효한 지표입니다. Kafka consumer처럼 partition 단위로 부하가 묶이는 워크로드에는 이 전제가 성립하지 않습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;해결-keda와-consumer-lag-기반-스케일링&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%B4%EA%B2%B0-keda%EC%99%80-consumer-lag-%EA%B8%B0%EB%B0%98-%EC%8A%A4%EC%BC%80%EC%9D%BC%EB%A7%81&quot; aria-label=&quot;해결 keda와 consumer lag 기반 스케일링 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;해결: KEDA와 consumer lag 기반 스케일링&lt;/h2&gt;
&lt;p&gt;문제의 핵심은 CPU가 이 워크로드의 실제 부하를 제대로 대변하지 못한다는 점이었습니다. 부하를 직접 나타내는 지표인 lag으로 스케일링해야 했습니다. HPA도 external metric을 지원하기는 하지만, Kafka lag을 노출하는 metrics adapter를 따로 붙이고 운영해야 합니다. 그래서 다양한 이벤트 소스를 스케일링 기준으로 삼을 수 있는 &lt;a href=&quot;https://keda.sh/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;KEDA(Kubernetes Event-Driven Autoscaling)&lt;/a&gt;를 도입했습니다.&lt;/p&gt;
&lt;h3 id=&quot;keda-도입과-lagthreshold-설정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#keda-%EB%8F%84%EC%9E%85%EA%B3%BC-lagthreshold-%EC%84%A4%EC%A0%95&quot; aria-label=&quot;keda 도입과 lagthreshold 설정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;KEDA 도입과 lagThreshold 설정&lt;/h3&gt;
&lt;p&gt;KEDA의 Kafka scaler는 consumer group의 lag을 직접 조회해 그 값을 기준으로 스케일링합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; keda.sh/v1alpha1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ScaledObject
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; log&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;transformer
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;scaleTargetRef&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; log&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;transformer
  &lt;span class=&quot;token key atrule&quot;&gt;minReplicaCount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;maxReplicaCount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;triggers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; kafka
      &lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;bootstrapServers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; kafka&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;broker&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;9092&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;consumerGroup&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; log&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;transformer&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;group
        &lt;span class=&quot;token key atrule&quot;&gt;topic&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; log&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;topic
        &lt;span class=&quot;token key atrule&quot;&gt;lagThreshold&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;100&apos;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;lagThreshold&lt;/code&gt;는 replica 1개가 감당할 lag의 기준값입니다. KEDA는 전체 lag을 이 값으로 나눈 만큼 replica를 늘리며, replica 수는 partition 수를 넘지 않습니다.&lt;/p&gt;
&lt;h3 id=&quot;lag-기반-스케일링의-장점&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#lag-%EA%B8%B0%EB%B0%98-%EC%8A%A4%EC%BC%80%EC%9D%BC%EB%A7%81%EC%9D%98-%EC%9E%A5%EC%A0%90&quot; aria-label=&quot;lag 기반 스케일링의 장점 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;lag 기반 스케일링의 장점&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;도메인 메트릭 기반&lt;/strong&gt;: 범용 지표가 아닌, Kafka consumer 워크로드에서 비롯한 지표로 판단합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;partition 할당 개수의 불균형에도 반응&lt;/strong&gt;: 개별 consumer의 CPU가 아니라 전체 lag 총량을 기준으로 하므로, partition이 불균등하게 분배되어 평균 CPU가 낮게 나오는 상황에서도 스케일 판단이 이뤄집니다. 다만 특정 partition 하나에 트래픽이 몰리는 경우라면 replica를 늘려도 그 partition의 처리량은 늘지 않으므로, 이때는 partition 설계 자체를 다시 봐야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;scale-to-zero 지원&lt;/strong&gt;: source topic에 데이터가 들어오지 않는 시간대에는 pod 수를 0까지 줄였다가, 메시지가 다시 쌓이기 시작하면 그때 pod를 띄워 처리할 수 있습니다. 저희는 트래픽이 간헐적인 개발 환경 topic에 이 방식을 적용해 idle 비용을 제거했고, 운영 환경 topic은 처리 지연을 피하기 위해 최소 replica를 유지합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;보완-자정-트래픽-급증과-cron-trigger&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B3%B4%EC%99%84-%EC%9E%90%EC%A0%95-%ED%8A%B8%EB%9E%98%ED%94%BD-%EA%B8%89%EC%A6%9D%EA%B3%BC-cron-trigger&quot; aria-label=&quot;보완 자정 트래픽 급증과 cron trigger permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;보완: 자정 트래픽 급증과 cron trigger&lt;/h2&gt;
&lt;p&gt;KEDA를 적용한 뒤 대부분의 시간대는 안정적으로 운영됐지만, lag 기반 스케일링만으로는 대응이 늦는 구간이 하나 있었습니다.&lt;/p&gt;
&lt;p&gt;많은 게임이 자정(0시)을 기준으로 일일 미션·출석 같은 콘텐츠를 초기화하는데, 그 시점에 관련 배치 작업과 일간 집계 로그가 한꺼번에 쏟아지면서 트래픽이 급증하는 패턴이 있었습니다. consumer lag 기반 스케일링은 lag이 쌓인 뒤에 반응하므로, 이렇게 급격한 트래픽 증가에는 대응이 늦을 수밖에 없습니다. 게다가 새 pod가 ready 상태가 되어 consumer group rebalance까지 마쳐야 비로소 처리가 시작되고, 여기에 JVM warm-up 시간까지 더해지므로, scale out이 완료되기 전까지 lag이 계속 치솟고 그만큼 다운스트림 파이프라인으로의 로그 전달이 지연됩니다.&lt;/p&gt;
&lt;h3 id=&quot;cron-trigger로-선제-대응&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#cron-trigger%EB%A1%9C-%EC%84%A0%EC%A0%9C-%EB%8C%80%EC%9D%91&quot; aria-label=&quot;cron trigger로 선제 대응 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;cron trigger로 선제 대응&lt;/h3&gt;
&lt;p&gt;KEDA는 Kafka scaler 외에도 cron trigger를 지원합니다. 0시 직전에 미리 pod 수를 늘려놓는 방식으로 해결했습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; keda.sh/v1alpha1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ScaledObject
&lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; log&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;transformer
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;scaleTargetRef&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; log&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;transformer
  &lt;span class=&quot;token key atrule&quot;&gt;minReplicaCount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;maxReplicaCount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;triggers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; kafka
      &lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;bootstrapServers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; kafka&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;broker&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;9092&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;consumerGroup&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; log&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;transformer&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;group
        &lt;span class=&quot;token key atrule&quot;&gt;topic&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; log&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;topic
        &lt;span class=&quot;token key atrule&quot;&gt;lagThreshold&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;100&apos;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; cron
      &lt;span class=&quot;token key atrule&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;timezone&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Asia/Seoul
        &lt;span class=&quot;token key atrule&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;50 23 * * *&apos;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# 23:50에 scale out&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;30 0 * * *&apos;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# 00:30에 cron trigger 해제&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;desiredReplicas&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;8&apos;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Kafka lag trigger와 cron trigger를 함께 사용하면, KEDA는 두 trigger 중 더 높은 replica 수를 채택합니다. 23:50에 미리 8대로 늘려놓고 0시 트래픽을 흡수한 뒤, 00:30 이후에는 lag 기반으로 자연스럽게 scale in됩니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;최적화-thread-다중화로-rebalance-이벤트-줄이기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B5%9C%EC%A0%81%ED%99%94-thread-%EB%8B%A4%EC%A4%91%ED%99%94%EB%A1%9C-rebalance-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EC%A4%84%EC%9D%B4%EA%B8%B0&quot; aria-label=&quot;최적화 thread 다중화로 rebalance 이벤트 줄이기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;최적화: thread 다중화로 rebalance 이벤트 줄이기&lt;/h2&gt;
&lt;p&gt;스케일링 정책과 자정 대응까지 정리한 뒤, 마지막으로 pod 구성 자체를 재설계했습니다. pod 1대에 Kafka Streams thread 1개를 두는 단순한 구조는 운영이 쉽지만, JVM 기동 오버헤드가 pod 수만큼 선형으로 늘고, pod 수명 주기 이벤트(재시작, rolling deploy, eviction)가 곧 consumer group rebalance의 트리거로 이어집니다.&lt;/p&gt;
&lt;p&gt;이를 줄이기 위해 저희는 pod 1대가 여러 개의 Kafka Streams thread를 동시에 실행하도록 구성했습니다. Kafka Streams는 &lt;code&gt;num.stream.threads&lt;/code&gt; 설정으로 인스턴스(=pod)당 여러 stream thread를 띄우는 것을 지원하며, 각 thread는 독립적으로 task(partition 묶음)를 할당받아 처리합니다(&lt;a href=&quot;https://kafka.apache.org/documentation/streams/architecture&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Kafka Streams Architecture&lt;/a&gt;). consumer group 입장에서는 thread 하나하나가 독립 consumer처럼 동작하므로, pod당 thread 수를 늘려도 partition 분배와 처리 정합성은 그대로 유지됩니다. 즉 전체 consumer thread 수는 동일하게 두고 pod 수만 줄이는 변경이어서, JVM 인스턴스 수와 메모리 오버헤드, 그리고 수명 주기 이벤트의 절대 빈도를 함께 낮출 수 있었습니다.&lt;/p&gt;
&lt;p&gt;다만 pod 1대에 더 많은 thread가 묶이는 만큼 pod 1대의 장애 영향 범위는 커지므로, pod당 thread 수는 무한정 늘리지 않고 아래의 CPU 할당 기준에 맞춰 제한했습니다.&lt;/p&gt;
&lt;p&gt;thread 수는 thread 간 CPU 경합을 피하기 위해 &lt;strong&gt;thread당 CPU core 1개&lt;/strong&gt;를 기준으로 잡고, pod의 CPU request/limit을 거기에 맞췄습니다. 예를 들어 6 thread를 실행하는 pod에는 6 core를 할당합니다. Kafka Streams의 &lt;a href=&quot;https://docs.confluent.io/platform/current/streams/sizing.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;capacity planning 가이드&lt;/a&gt;도 stream thread 수를 가용 CPU core 규모에 맞춰 잡도록 권하는데, 저희는 처리량을 예측 가능하게 만들기 위해 thread당 1 core로 다소 보수적으로 운영했습니다. 이렇게 하면 pod당 처리량이 안정적으로 유지되고, lag 기반 스케일링이 기준으로 삼는 &quot;pod 1대가 흡수할 수 있는 lag 처리량&quot;도 예측 가능한 값으로 수렴합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;결과-트래픽에-맞춘-리소스-운영과-비용-절감&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%B0%EA%B3%BC-%ED%8A%B8%EB%9E%98%ED%94%BD%EC%97%90-%EB%A7%9E%EC%B6%98-%EB%A6%AC%EC%86%8C%EC%8A%A4-%EC%9A%B4%EC%98%81%EA%B3%BC-%EB%B9%84%EC%9A%A9-%EC%A0%88%EA%B0%90&quot; aria-label=&quot;결과 트래픽에 맞춘 리소스 운영과 비용 절감 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;결과: 트래픽에 맞춘 리소스 운영과 비용 절감&lt;/h2&gt;
&lt;h3 id=&quot;비용-절감-수치&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B9%84%EC%9A%A9-%EC%A0%88%EA%B0%90-%EC%88%98%EC%B9%98&quot; aria-label=&quot;비용 절감 수치 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;비용 절감 수치&lt;/h3&gt;
&lt;p&gt;EC2 고정 인스턴스 운영 대비, k8s + KEDA 기반으로 전환한 후 80% 이상의 인프라 비용을 절감할 수 있었습니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;EC2 기반&lt;/th&gt;
&lt;th&gt;k8s + KEDA&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;스케일링&lt;/td&gt;
&lt;td&gt;72대 고정 (스케줄 조정은 중단)&lt;/td&gt;
&lt;td&gt;자동 (lag 기반 + cron)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;비용&lt;/td&gt;
&lt;td&gt;24시간 고정 비용&lt;/td&gt;
&lt;td&gt;트래픽에 비례하는 비용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;운영 부담&lt;/td&gt;
&lt;td&gt;게임 추가 시 인스턴스 증설 등 인프라 작업 필요&lt;/td&gt;
&lt;td&gt;설정 배포로 완료&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;80%라는 수치 자체보다 중요한 것은 운영 방식의 변화입니다. EC2에서는 피크 기준으로 인스턴스를 상시 유지해야 했지만, KEDA 기반에서는 새벽에 2대까지 줄었다가 피크에만 scale out되어 그 순간의 부하에 맞는 만큼만 pod를 할당합니다. 80%는 현재 트래픽 패턴에서 측정된 결과이며, 트래픽 패턴이 바뀌어도 리소스가 그에 따라 움직인다는 점이 핵심입니다.&lt;/p&gt;
&lt;h3 id=&quot;운영-부담-감소&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9A%B4%EC%98%81-%EB%B6%80%EB%8B%B4-%EA%B0%90%EC%86%8C&quot; aria-label=&quot;운영 부담 감소 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;운영 부담 감소&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;게임 추가 시 Helm chart에 값만 추가하면 됩니다.&lt;/li&gt;
&lt;li&gt;스케일링 정책을 Helm chart로 관리하므로 리뷰와 이력 추적이 가능합니다.&lt;/li&gt;
&lt;li&gt;image tag와 환경 변수 변경만으로 ArgoCD를 통해 배포할 수 있어, 기존 Spinnaker 기반 EC2 파이프라인 대비 배포 준비와 반영 시간이 크게 줄었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;남은-과제-thread-단위까지-고려한-partition-균등-분배&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%82%A8%EC%9D%80-%EA%B3%BC%EC%A0%9C-thread-%EB%8B%A8%EC%9C%84%EA%B9%8C%EC%A7%80-%EA%B3%A0%EB%A0%A4%ED%95%9C-partition-%EA%B7%A0%EB%93%B1-%EB%B6%84%EB%B0%B0&quot; aria-label=&quot;남은 과제 thread 단위까지 고려한 partition 균등 분배 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;남은 과제: thread 단위까지 고려한 partition 균등 분배&lt;/h2&gt;
&lt;p&gt;현재 구성에서도 lag 기반 스케일링은 안정적으로 동작하지만, partition이 consumer에 균등하게 분배되지 않는 근본적인 문제는 남아 있습니다.&lt;/p&gt;
&lt;p&gt;KEDA의 &lt;code&gt;ensureEvenDistributionOfPartitions&lt;/code&gt; 옵션은 전체 partition 수의 약수로 replica 수를 제한해 이 문제를 해소합니다. 다만 이 옵션이 검사하는 것은 &lt;code&gt;partition 수 / replica 수&lt;/code&gt;뿐입니다. pod 1대가 여러 thread를 띄우는 구성에서 실제로 partition을 나눠 갖는 주체는 pod가 아니라 thread이므로, 성립해야 할 조건은 &lt;code&gt;partition 수 / (replica 수 × pod당 thread 수)&lt;/code&gt;가 정수인지입니다. 앞서 source topic별로 배포를 분리해둔 덕분에 pod 1대가 topic 하나를 전담하므로, 여기서 pod당 thread 수는 곧 그 topic을 처리하는 thread 수가 됩니다.&lt;/p&gt;
&lt;p&gt;24 partition을 pod당 4 thread로 처리한다면, 옵션이 허용하는 replica는 24의 약수인 1, 2, 3, 4, 6, 8, 12, 24이지만, thread 단위까지 균등한 replica는 1, 2, 3, 6뿐입니다. 나머지 값에서는 두 가지 증상이 나타납니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;replica 4&lt;/strong&gt;: pod 단위로는 6 task씩 균등하지만, 전체 16 thread에 24 task가 나뉘어 thread 8개는 task 2개를, 나머지 8개는 1개를 받습니다. thread 간 처리량이 최대 2배까지 벌어집니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;replica 8 이상&lt;/strong&gt;: thread 수가 task 수를 넘어섭니다. replica 8이면 32개 thread 중 8개가, replica 24라면 96개 thread 중 72개가 아무 task도 받지 못한 채 빈 poll 루프만 돌게 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;replica 상한도 마찬가지입니다. KEDA는 replica 수를 partition 수 이하로 제한하는데, 이 상한 역시 pod당 thread 수를 모르는 채 계산됩니다. 결국 균등 분배를 thread 수준까지 끌어내리려면 KEDA가 pod당 thread 수까지 알고 replica를 결정해야 합니다.&lt;/p&gt;
&lt;p&gt;이 공백을 메우기 위해 KEDA 저장소에 &lt;a href=&quot;https://github.com/kedacore/keda/issues/7577&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;옵션 추가 issue&lt;/a&gt;를 올려두었고, 현재 &lt;a href=&quot;https://github.com/kedacore/keda/pull/7765&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;&lt;code&gt;threadsPerConsumer&lt;/code&gt;라는 이름의 구현 PR&lt;/a&gt;이 올라와 리뷰가 진행 중입니다. &lt;code&gt;ensureEvenDistributionOfPartitions&lt;/code&gt;와 함께 이 값에 pod당 thread 수를 지정하면, KEDA가 &lt;code&gt;partition 수&lt;/code&gt;가 아니라 &lt;code&gt;partition 수 / threadsPerConsumer&lt;/code&gt;의 약수 중에서 replica를 고르게 됩니다. 앞의 예시라면 24가 아닌 6의 약수, 즉 1, 2, 3, 6만 후보가 되어 pod 단위와 thread 단위 모두에서 균등한 분배를 보장할 수 있습니다.&lt;/p&gt;
&lt;p&gt;한편 더 근본적으로는, Scala로 작성된 Log Transformer를 대체할 확장된 역할의 컴포넌트를 Rust로 작성하는 작업도 진행 중입니다. JVM 기동 오버헤드와 GC 지연 감소, CPU·메모리 사용량 절감, throughput 향상을 기대하고 있으며, &lt;code&gt;threadsPerConsumer&lt;/code&gt; 옵션의 진행 상황과 함께 결과가 정리되는 대로 별도의 글로 공유드리겠습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;마치며-워크로드-특성에-맞는-스케일링-메트릭&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0-%EC%9B%8C%ED%81%AC%EB%A1%9C%EB%93%9C-%ED%8A%B9%EC%84%B1%EC%97%90-%EB%A7%9E%EB%8A%94-%EC%8A%A4%EC%BC%80%EC%9D%BC%EB%A7%81-%EB%A9%94%ED%8A%B8%EB%A6%AD&quot; aria-label=&quot;마치며 워크로드 특성에 맞는 스케일링 메트릭 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;마치며: 워크로드 특성에 맞는 스케일링 메트릭&lt;/h2&gt;
&lt;p&gt;이번 마이그레이션에서 저희가 다시 확인한 것은 한 가지였습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;스케일링 메트릭은 워크로드의 부하 특성에 맞춰 골라야 합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;CPU 사용률은 부하가 pod 전반에 고르게 퍼지는 워크로드라면 충분히 유효한 지표입니다. 다만 Kafka consumer처럼 partition 단위로 부하가 묶이는 워크로드에서는 평균 CPU만으로 실제 처리 지연을 포착하기 어려웠습니다. 저희는 consumer lag을 기준으로 삼아 스케일링의 정확도를 높일 수 있었습니다.&lt;/p&gt;
&lt;p&gt;KEDA는 Kafka 외에도 RabbitMQ, AWS SQS, Prometheus metrics 등 다양한 이벤트 소스에 대한 scaler를 제공합니다. CPU 외의 지표를 스케일링 기준으로 손쉽게 가져올 수 있다는 점이 저희에게는 가장 큰 장점이었습니다.&lt;/p&gt;
&lt;p&gt;Kafka consumer 스케일링에 비슷한 고민이 있으시다면, consumer lag 기반 스케일링을 검토해보시길 권합니다.&lt;/p&gt;
&lt;p&gt;감사합니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[킹덤 서버! 튜토리얼]]></title><description><![CDATA[킹덤 서버 합류기를 공유합니다.]]></description><link>https://tech.devsisters.com/posts/ck-server-tutorial</link><guid isPermaLink="false">https://tech.devsisters.com/posts/ck-server-tutorial</guid><pubDate>Tue, 02 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, 킹덤 서버셀에서 소프트웨어 엔지니어로 근무하고 있는 노수지입니다. 이번 글에서는 킹덤 서버셀에 합류한 이후, 지난 1년 동안 팀의 기술 문화를 체득하고 실제 서비스 코드에 기여하기까지의 과정을 정리해 보려 합니다. 킹덤 서버팀은 &lt;strong&gt;Scala를 활용한 함수형 프로그래밍(FP) 지향적인 코드베이스&lt;/strong&gt;를 보유하고 있습니다. 저 또한 합류 당시에는 함수형 프로그래밍에 익숙하지 않았기에, 이러한 기술 스택은 큰 도전이자 생소한 과제로 다가왔습니다.&lt;/p&gt;
&lt;p&gt;이 글에서는 저처럼 해당 패러다임에 낯선 개발자들을 위해 &lt;strong&gt;킹덤 팀이 어떤 체계적인 온보딩 시스템과 가이드를 제공하고 있는지&lt;/strong&gt; 소개하려 합니다. 신규 입사자가 이러한 팀의 지원을 통해 어떻게 기술적 장벽을 넘고 개발 프로세스에 안정적으로 합류하게 되는지 그 여정을 공유하고자 합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id=&quot;stage-1-함수형-프로그래밍-이해하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#stage-1-%ED%95%A8%EC%88%98%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0&quot; aria-label=&quot;stage 1 함수형 프로그래밍 이해하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Stage 1. 함수형 프로그래밍 이해하기&lt;/h1&gt;
&lt;p&gt;입사 후 가장 먼저 테크리드님이 직접 작성하신 스칼라 학습 레포지토리를 전달받았습니다. 기본 조건문과 반복문부터 시작해 Scala 고유의 문법과 함수형 프로그래밍(FP)의 정수를 익힐 수 있도록 설계된 실습용 코드들이 가득했습니다. 이 과정에서 &lt;strong&gt;“기존 값을 수정하지 않고 새로운 상태를 생성한다”&lt;/strong&gt; 는 함수형 패러다임을 체득하게 됩니다.&lt;/p&gt;
&lt;h2 id=&quot;part-1-순수-함수&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#part-1-%EC%88%9C%EC%88%98-%ED%95%A8%EC%88%98&quot; aria-label=&quot;part 1 순수 함수 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;part 1. 순수 함수&lt;/h2&gt;
&lt;p&gt;Java와 같은 명령형 언어에서는 객체 내부의 필드를 직접 수정(&lt;strong&gt;Mutation&lt;/strong&gt;)하여 상태를 변경합니다. 반면, 스칼라의 순수함수는 &lt;strong&gt;불변성(Immutability)&lt;/strong&gt; 을 원칙으로 합니다. 예를 들어, Java에서 Map의 값을 수정하면 기존 Map 객체 자체가 변하지만, 스칼라에서는 기존 Map은 그대로 둔 채 변경사항이 반영된 &lt;strong&gt;새로운 Map 객체&lt;/strong&gt;를 반환합니다. 이 구조는 동일한 입력에 대해서 항상 동일한 출력을 보장합니다. 수학에서 많이 보이는 &lt;code&gt;y = f(x)&lt;/code&gt;의 &lt;code&gt;f&lt;/code&gt;가 순수 함수입니다.&lt;/p&gt;
&lt;p&gt;그렇다면 &quot;새롭게 만들어진 객체는 어디에 적용(set)되는가?&quot;라는 의문이 생길 수 있습니다. 순수함수의 세계에서 함수는 외부의 상태를 직접 바꾸는 &apos;액션&apos;을 취하지 않습니다. 대신 &lt;strong&gt;&quot;입력값(A)을 넣으면 결과값(B)이 나온다&quot;&lt;/strong&gt; 는 정의에만 집중합니다. 이 방식에서 상태값은 어딘가에 고정되어 변하는 것이 아닙니다. 대신, &lt;strong&gt;&apos;현재 상태를 토대로 다음 상태를 계산하는 함수들&apos;&lt;/strong&gt; 이 체인처럼 연결되어 흐릅니다.
각 함수는 이전 단계에서 계산된 최신 상태를 전달받아 새로운 상태를 만들어내고, 이 과정이 끝까지 흐르면 &lt;strong&gt;최종적으로 계산된 상태값&lt;/strong&gt;이 도출됩니다. 이렇게 완성된 설계도는 프로그램의 가장 바깥쪽 레이어(Runtime)에 도달해서야 비로소 실제 저장소에 반영(set)되거나 처리됩니다.&lt;/p&gt;
&lt;figure&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Library&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Set&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;BookId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; books &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;HashSet&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;BookId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; borrowedBooks &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;HashMap&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    
    &lt;span class=&quot;token comment&quot;&gt;// 기존 객체 내부의 상태(Map)를 직접 수정 (Side Effect)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;borrowBook&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;BookId&lt;/span&gt; bookId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; username&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        borrowedBooks&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;bookId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; username&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;figcaption&gt;가변 객체를 이용하는 Java&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; Library&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    books&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Set&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;BookId&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    borrowedBooks&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Map&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;BookId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 기존 상태를 수정하지 않고, 변경된 데이터가 담긴 새 Library 인스턴스를 반환&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; borrowBook&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;bookId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; BookId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; username&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Library &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// copy를 통해 변경된 부분만 교체한 새 객체 생성&lt;/span&gt;
        copy&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;borrowedBooks &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; borrowedBooks&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;updated&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;bookId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; username&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;figcaption&gt;불변 객체를 이용하는 스칼라&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;part-2-함수를-일급-객체로-다루기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#part-2-%ED%95%A8%EC%88%98%EB%A5%BC-%EC%9D%BC%EA%B8%89-%EA%B0%9D%EC%B2%B4%EB%A1%9C-%EB%8B%A4%EB%A3%A8%EA%B8%B0&quot; aria-label=&quot;part 2 함수를 일급 객체로 다루기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;part 2. 함수를 일급 객체로 다루기&lt;/h2&gt;
&lt;p&gt;스칼라에서는 함수를 &lt;strong&gt;일급 객체(First-class Object)&lt;/strong&gt; 로 취급합니다. 이는 함수를 변수에 담을 수도, 다른 함수의 인자로 전달할 수도, 혹은 함수의 결과값으로 반환할 수도 있음을 의미합니다. 이러한 특징을 가장 직관적으로 보여주는 것이 바로 &lt;strong&gt;람다식(익명 함수)&lt;/strong&gt; 입니다. 람다식은 복잡한 선언 없이 &lt;code&gt;(입력값) =&gt; 로직&lt;/code&gt;의 형태로 즉석에서 함수를 정의합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 1. 함수를 변수에 할당&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; double &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;x&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;=&gt;&lt;/span&gt; x &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; four &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; double&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 2. 고차 함수에서의 활용&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 별도의 메서드 정의 없이, 람다식을 통해 &apos;어떻게 처리할지&apos;의 로직만 가볍게 전달합니다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; numbers &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; List&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; doubledList &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; numbers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;map&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;x &lt;span class=&quot;token keyword&quot;&gt;=&gt;&lt;/span&gt; x &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 각 요소에 람다 적용&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;람다식을 사용하면 코드가 극도로 간결해질 뿐만 아니라, 로직 자체를 값으로 취급해 이리저리 전달할 수 있게 됩니다. 이는 앞서 언급한 순수함수와 결합해, 거대한 비즈니스 로직을 작은 함수들의 조립식 결합으로 해결합니다.&lt;/p&gt;
&lt;p&gt;더 자세한 스칼라와 함수형 프로그래밍 이야기를 듣고 싶다면 &lt;a href=&quot;https://tech.devsisters.com/posts/functional-programming-in-scala-01/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;현수님의 기술블로그&lt;/a&gt;를 추천합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id=&quot;stage-2-ziozpure-이해하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#stage-2-ziozpure-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0&quot; aria-label=&quot;stage 2 ziozpure 이해하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Stage 2. ZIO/ZPure 이해하기&lt;/h1&gt;
&lt;p&gt;스칼라 기초를 다진 후에는 학습서 &lt;em&gt;Zionomicon&lt;/em&gt;을 통해 킹덤 아키텍처의 핵심인 &lt;strong&gt;ZIO&lt;/strong&gt;와 &lt;strong&gt;ZPure&lt;/strong&gt;를 학습합니다. ZIO는 실제 실행하는 계층(DB I/O, 통신 등)에서 ZPure는 결과를 계산하는 계층에서 사용됩니다.&lt;/p&gt;
&lt;h2 id=&quot;part-1-순수한-레이어를-위한-zpure&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#part-1-%EC%88%9C%EC%88%98%ED%95%9C-%EB%A0%88%EC%9D%B4%EC%96%B4%EB%A5%BC-%EC%9C%84%ED%95%9C-zpure&quot; aria-label=&quot;part 1 순수한 레이어를 위한 zpure permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;part 1. 순수한 레이어를 위한 ZPure&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;ZPure&lt;/strong&gt;는 부수 효과와 완전히 격리된 로직을 설계할 때 사용합니다. &lt;code&gt;ZPure[W, S1, S2, R, E, A]&lt;/code&gt;라는 복잡한 타입을 가지는데, 이는 &lt;strong&gt;&quot;환경 &lt;code&gt;R&lt;/code&gt;과 초기 상태 &lt;code&gt;S1&lt;/code&gt;이 주어졌을 때, 로그(Chunk[W])를 남기고 상태를 &lt;code&gt;S2&lt;/code&gt;로 변화시키며, 에러 &lt;code&gt;E&lt;/code&gt; 혹은 결과 &lt;code&gt;A&lt;/code&gt;를 반환함&quot;&lt;/strong&gt; 을 의미합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;(S1, R) =&gt; (Chunk[W], E | A)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;ZPure의 가장 큰 특징은 &lt;strong&gt;부수 효과(Side-Effect)가 전혀 없다&lt;/strong&gt;는 점입니다. 오직 입력된 값(&lt;code&gt;R, S&lt;/code&gt;)만을 이용해 결과를 만들어내기 때문에, 동일한 입력에는 항상 동일한 결과가 보장됩니다. 예를 들어, 재고 상태(&lt;code&gt;S1&lt;/code&gt;)를 받아 모든 재고를 0으로 만드는 로직은 아래와 같이 작성할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 상태 S1 내의 stocks 전 재고를 0으로 만드는 순수 로직&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;lazy&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; reset&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ZPure&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;Nothing&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; S1&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; S2&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Nothing&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Unit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    s1 &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; ZPure&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;get&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;S1&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 기존 상태를 복사하여 새로운 상태 s2를 생성 (순수 계산)&lt;/span&gt;
    s2 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; s1&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;copy&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;stocks &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; s1&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;stocks&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;mapValuesCompat&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;_ &lt;span class=&quot;token keyword&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    _  &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; ZPure&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;set&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;s2&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;여기서 &lt;code&gt;reset&lt;/code&gt;은 실제로 DB를 수정하는 것이 아니라, &quot;상태가 이렇게 변해야 한다&quot;라는 &lt;strong&gt;계산 결과&lt;/strong&gt;만을 안전하게 만들어냅니다.&lt;/p&gt;
&lt;h2 id=&quot;part-2-실행을-위한-effect인-zio&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#part-2-%EC%8B%A4%ED%96%89%EC%9D%84-%EC%9C%84%ED%95%9C-effect%EC%9D%B8-zio&quot; aria-label=&quot;part 2 실행을 위한 effect인 zio permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;part 2. 실행을 위한 Effect인 ZIO&lt;/h2&gt;
&lt;p&gt;우리가 ZPure로 짠 로직은 그 자체로는 아무런 일도 일으키지 않는 설계도와 같습니다. 비유하자면, ZPure는 &quot;요리 레시피&quot;를 작성하는 과정이고, 이 레시피(로직)가 실제 요리(실행)로 이어지기 위해서는 이를 감싸 안는 &apos;효과&apos;의 단위로 변환되어야 합니다. 킹덤 아키텍처는 이 &apos;순수한 레시피&apos;를 먼저 완성한 뒤, 이를 실제로 실행시켜 우리가 아는 게임 서비스를 제공합니다.&lt;/p&gt;
&lt;p&gt;ZPure가 만든 순수한 결과물은 최종적으로 &lt;strong&gt;ZIO&lt;/strong&gt;를 통해 현실에서 실행됩니다. &lt;code&gt;ZIO[R, E, A]&lt;/code&gt; 환경 &lt;code&gt;R&lt;/code&gt;에서 에러 &lt;code&gt;E&lt;/code&gt; 혹은 결과 &lt;code&gt;A&lt;/code&gt;를 반환하는 &apos;효과&apos;를 나타내며, ZPure와 달리 &lt;strong&gt;DB 조회, 네트워크 통신, 로깅 등 실제 부수 효과(Side-Effect)&lt;/strong&gt; 를 관리하고 실행합니다.
이러한 계층 구조를 갖는 이유는 &lt;strong&gt;부수 효과에 대한 통제력&lt;/strong&gt; 때문입니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;먼저 &lt;strong&gt;ZPure&lt;/strong&gt;에서 복잡한 도메인 로직을 오류 없이 계산합니다. (테스트가 쉽고 안전함)&lt;/li&gt;
&lt;li&gt;계산된 결과를 바탕으로 &lt;strong&gt;ZIO&lt;/strong&gt;가 외부 시스템(DB 등)과 상호작용합니다.&lt;/li&gt;
&lt;li&gt;이 과정에서 ZIO는 동시성 제어(Fiber), 재시도 로직(Retry), 리소스 관리 등의 런타임 기능을 제공합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;결과적으로 &lt;strong&gt;&quot;순수하게 계산된 결과를 기반으로, 부수 효과가 있는 ZIO를 실행시킨다&quot;&lt;/strong&gt; 는 원칙을 지킴으로써, 코드의 예측 가능성을 높이고 버그가 파고들 틈을 최소화합니다. 이 아키텍처에 대해 더 깊이 알고 싶다면 &lt;a href=&quot;https://blog.pierre-ricadat.com/the-tri-z-architecture-a-pattern-for-layering-zio-applications-in-scala/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;테크리드님의 블로그 글&lt;/a&gt;을 참고해 보시기 바랍니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 401px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 110.83333333333334%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAWCAIAAABPIytRAAAACXBIWXMAAAsTAAALEwEAmpwYAAAC/0lEQVR42oVUbW/TMBDu//8bIIGQEBsfQNoGGwixNknfmzRJu61Nm7eWtLWT0Kaxz5ydlo0yhGMlfnt8zz13l5r4o8HhI9seYCegAI4DODlQtdoJkvM92855ZuXLer74lsVfdyuN5xbb+sDLE/xvsLycFT8YbXNyK1IjurtqXL/Qv7xce9ciM/imzkiHFWt4gn+0zLYhJ3VBdaBNkWoi1aO7T4vJtRprcpFqfNMod/Fv+7Xqw3YJJw2RIszgREsWzjRazpNtkGzn8XKzdIBqghrYcZfv1xW+pvwsGWmjTdwrSct58D58s2+Mybur1vnnzsev1sXt6G7mMdKUeKIx2gPgB9pIGNAsbQLRJkFkB8IJyoGXX1w3L29aA++nE+xx0QsDoOoY+q/I11Aolg3RH3RstbRtn49jGPm7Rc/etTvbdifuj0Z+gYu2zzZLE7VAGyxzQIFLRis+jVkUOpFwIx73bGhqvN+DXhda+sK6d2PuhGIeBUIaNxjtIPMa8IIRXSmhP4QrNxZ3QXF/eTl8f6a9fqW/eT04e+tcXNxHDO+dhEs0K8EbA4BVYO0IThCMJIlu0Pr3/tm7wflZWv++0pqjoHRPwSXSZpgYKgyNeeRL2jEPzHum13m3zbstHAS258aAtIN4pmjrjHYVbVQ7deR9qU5+mKgKauMGpW9N1kYH+2zouVEpBQv2adJTgjXKbAyHUO0WsJExQM39aDYMhMRjD5kbljjAKS6G0YMy28Q8Y0VyiDMWDaN9IZ1pYgIF0RTjjCRdqbzAge0XiOTqAAalTM2qzI7pWVKluV7lcJb0w3jqhTH2KJ7mSRdtAm1Jb4nByuwxPQ/4/RoFFyrV0DHFUFO9oabSpkQeE/uPkpRJXmYsNbH6UAIgMpOEfOs4lX6mQ85y8WxJVkuAD6OwfYDMhKwHWRd+WrCboipQ7T73MzhWNUadZmmWZ1lO6SalhFCK0zzfPvkZ/QNcFIVpDsbj0Xg8Hg6Hg4HZbrU7nbZlWZzz/4AffVDt6fTvM78A19nLzTPeKBUAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/acaca24ce41e8f5bc581d0cc397e0981/33ecc/tri-z.webp 360w,
/static/acaca24ce41e8f5bc581d0cc397e0981/44eaf/tri-z.webp 401w&quot;
              sizes=&quot;(max-width: 401px) 100vw, 401px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/acaca24ce41e8f5bc581d0cc397e0981/53918/tri-z.png 360w,
/static/acaca24ce41e8f5bc581d0cc397e0981/25946/tri-z.png 401w&quot;
            sizes=&quot;(max-width: 401px) 100vw, 401px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/acaca24ce41e8f5bc581d0cc397e0981/25946/tri-z.png&quot;
            alt=&quot;ZPure로 순수하게 계산한 결과를 ZIO로 실행하는 Tri-Z 구조&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;ZPure로 순수하게 계산한 결과를 ZIO로 실행하는 Tri-Z 구조&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h1 id=&quot;final-stage-라이브-서비스에-코드-작성하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#final-stage-%EB%9D%BC%EC%9D%B4%EB%B8%8C-%EC%84%9C%EB%B9%84%EC%8A%A4%EC%97%90-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0&quot; aria-label=&quot;final stage 라이브 서비스에 코드 작성하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Final Stage. 라이브 서비스에 코드 작성하기&lt;/h1&gt;
&lt;h2 id=&quot;part-1-테스트-짜기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#part-1-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%A7%9C%EA%B8%B0&quot; aria-label=&quot;part 1 테스트 짜기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;part 1. 테스트 짜기&lt;/h2&gt;
&lt;p&gt;Scala와 ZIO에 익숙해졌다면, 이제 메인 레포지토리에 직접 코드를 작성할 차례입니다. 처음부터 수많은 동시성 처리와 대규모 데이터 모델이 포함된 핵심 시스템(또는 대형 인게임 콘텐츠) 개발에 바로 투입되기보다는, 시스템의 맥락과 팀 내 컨벤션을 파악하기 위해 &lt;strong&gt;테스트 코드 작성&lt;/strong&gt;부터 시작하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1928px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 46.666666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAAAsTAAALEwEAmpwYAAACiUlEQVR42g3R20tTAQDH8fNXBAaF3cxpm1O3dtV058x5jnPbcZ41l9u8LSe5Qs1dZFlWitYkuihYGHghyYiQ7jfrpSx6qJegHuopCvoDein4tqff248fv49QVq7DUe+kZOcutu3QUWEVsYWGMDbGqXRqVLtiWE0WGm31HFG8WBwyLV4Jn+TAbGnC7ApiOKSir6hFVxdEcOwtQ3VU0R+RaVZa8SdOED21RHj2Fc1aFqV7mlD/WbIhCaltDE9bjobmJEaTH6PBjRaIILb04JGcRPxWhHithamExtJEL9PT4wxfX2fo8ir9y1tERq+h5W/Sd3adC8sPSd94htxToCWYIRQ5iajEMduD7LdpxNvcjHa6EPQVDkS1nyOBDMOFFeLzL8i+/k568yvKmQ1c6UWSa5958usvj3/+4djsI+TOGWLxHIMnpzA1drBnnxnR6KHDpiKUN/gp14ZRfXmU/ALRhfeMPP7B1Q+/SV16SezcHRZvv+Xul39cfvaZaG+WRjVNIlVAlMOY7B7C7QPYixlVFYRE2MvhgQ40LUlAPEduZIXg+fuMXftIYf4NY3PPyfQOUFh9R1fhJYnZDYKpCQxFgP3mImARr1XN4ZD7kL0+hNMxkblxH/meVjT7eY7LwwymlmgfuslgMs3a5jdGclOMz97i6OQD0itbZOY20FeLHKhpwuUZ5IDFT1mNQsn2nQgTKY3JuIuLx4x0NoeKmjPF0kmiwXmUZIGZhXssPf1E/spdXFoGZ9sIYnsWw0Ef9Q3dOKUAVrcXk9WNvnQXQp/vYHFdNWPdNhKSiWxXF4ojXDw5gd2XocYdoU5SqaqVKK2sY7fOiq7Wg952GEOlDclcRazFRndApksW+Q+vTIHRD+c2pAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/1c261d4717f9f66140c6365ed8dde06a/33ecc/episode.webp 360w,
/static/1c261d4717f9f66140c6365ed8dde06a/263a4/episode.webp 480w,
/static/1c261d4717f9f66140c6365ed8dde06a/0c8fb/episode.webp 640w,
/static/1c261d4717f9f66140c6365ed8dde06a/521f7/episode.webp 860w,
/static/1c261d4717f9f66140c6365ed8dde06a/cbd37/episode.webp 1200w,
/static/1c261d4717f9f66140c6365ed8dde06a/a04fb/episode.webp 1928w&quot;
              sizes=&quot;(max-width: 1928px) 100vw, 1928px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/1c261d4717f9f66140c6365ed8dde06a/53918/episode.png 360w,
/static/1c261d4717f9f66140c6365ed8dde06a/9aebd/episode.png 480w,
/static/1c261d4717f9f66140c6365ed8dde06a/0f09e/episode.png 640w,
/static/1c261d4717f9f66140c6365ed8dde06a/3e3fe/episode.png 860w,
/static/1c261d4717f9f66140c6365ed8dde06a/64756/episode.png 1200w,
/static/1c261d4717f9f66140c6365ed8dde06a/b06e5/episode.png 1928w&quot;
            sizes=&quot;(max-width: 1928px) 100vw, 1928px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/1c261d4717f9f66140c6365ed8dde06a/b06e5/episode.png&quot;
            alt=&quot;킹덤의 대표 대형 컨텐츠인 에피소드, 한 컨텐츠에 여러 기능이 섞여있습니다.&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;킹덤의 대표 대형 컨텐츠인 에피소드, 한 컨텐츠에 여러 기능이 섞여있습니다.&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;킹덤 서버의 테스트는 크게 &lt;strong&gt;유닛 테스트(Unit Test)&lt;/strong&gt; 와 &lt;strong&gt;시나리오 테스트(Scenario Test)&lt;/strong&gt; 두 가지 계층으로 나뉩니다. 킹덤의 소원나무 기능을 예시로 설명하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 46.111111111111114%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAAAsTAAALEwEAmpwYAAACnElEQVR42g2S20vTAQCF9z/0UBkWqTmn5S2p7MKmm21upKiLZWa1TJZhIWYpNe+y1J9jatu8oPOum9MZaumYTtuwK4E+VFApFNRTEfQUZH7t4cB5+jh8HFFW/OCOTKxHKtYgTbrBmSO5yJNUaOJ0lEecxScpZdbQhUNnxiuuwHJGwXGpkrTUbKTK26QlFaDIKEZ3zoNS3PlPlBBdSubREh7IhshQVxMWr6ZCO4q7coNGfT+etjY2/FaqWi1MNgl8GOmgtL6Rzo46grMmivJvcjjFQIa8HVmCgOhE9EXSNPcxON5RUOsk9kAuM6ZlPgfWcA3PM+LsZnVpCs+IjdWVST5vvWLeO0bAN83HtacseF+gKhnnYL6ARNuIaP9eJVlxMuob+jCW1HIhOQeftZylFgXddTfQ5ui4Uqil3HCZ9ZU5Pgaf4fI48A55+eKcxdo9R6R+kH0FNvbntiAypqXTU3KboivtaDSVnDysxHXPzLwwi6PFSeCxnTl/D/dqy5gfa+br+jCLT/rwOx+xtWTDYu1kV3oDMdoGxJoKRNkSFfIcAcX5Mk6prxInK2Pc6OfpwDusNR7eLg3z48975haHCfoGgE2Wp+08X+jj78839NoF9hzKIiIxhyjFHUQRikrC08uoKsyjUJ1IeORF7C0O3FNdCAYra8uhVb87mBg34n9mYfNXO86hSgIBM9/+2DALt9i9O5PY2HzCEvWIorUPiVTd5XjCsVBSkassNLT2IriqMda0MTRexOTLvJCrYhwT13G/zMXmuEn/VDEzr/NoMulJjLpFcuh64hhdCJh+d0eiriLlRCYy+TWMY9u4nZ8Ier/Tbl1g2d3M9tYoE3YTflcrfzfHcHY3E/SYQ32ULosJ+aVRTp+uRhJ7aec/PqO8QuXkusEAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/70cf4ac58fa20469e92c1610e89d0db0/33ecc/wishtree.webp 360w,
/static/70cf4ac58fa20469e92c1610e89d0db0/263a4/wishtree.webp 480w,
/static/70cf4ac58fa20469e92c1610e89d0db0/0c8fb/wishtree.webp 640w,
/static/70cf4ac58fa20469e92c1610e89d0db0/521f7/wishtree.webp 860w,
/static/70cf4ac58fa20469e92c1610e89d0db0/cbd37/wishtree.webp 1200w,
/static/70cf4ac58fa20469e92c1610e89d0db0/548e7/wishtree.webp 2400w,
/static/70cf4ac58fa20469e92c1610e89d0db0/0ab17/wishtree.webp 2556w&quot;
              sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/70cf4ac58fa20469e92c1610e89d0db0/53918/wishtree.png 360w,
/static/70cf4ac58fa20469e92c1610e89d0db0/9aebd/wishtree.png 480w,
/static/70cf4ac58fa20469e92c1610e89d0db0/0f09e/wishtree.png 640w,
/static/70cf4ac58fa20469e92c1610e89d0db0/3e3fe/wishtree.png 860w,
/static/70cf4ac58fa20469e92c1610e89d0db0/64756/wishtree.png 1200w,
/static/70cf4ac58fa20469e92c1610e89d0db0/98e2c/wishtree.png 2400w,
/static/70cf4ac58fa20469e92c1610e89d0db0/10cd9/wishtree.png 2556w&quot;
            sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/70cf4ac58fa20469e92c1610e89d0db0/98e2c/wishtree.png&quot;
            alt=&quot;킹덤의 소원나무 기능&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;킹덤의 소원나무 기능&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;유닛 테스트는 이름 그대로 &lt;strong&gt;기능의 최소 단위&lt;/strong&gt;가 의도대로 작동하는지 검증합니다. 앞서 배운 순수함수가 실제 도메인 규칙을 잘 지키고 있는지 확인하는 단계입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&apos;소원 하나 들어주기&apos; 기능을 테스트한다면 아래 항목들을 개별적으로 확인합니다.&lt;/li&gt;
&lt;li&gt;요구 품목인 아이템이 정확히 소모되었는가?&lt;/li&gt;
&lt;li&gt;개별 소원에 따른 보상이 유저 인벤토리에 정확히 들어왔는가?&lt;/li&gt;
&lt;li&gt;일일 보상 게이지가 로직에 맞게 상승했는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;시나리오 테스트는 여러 기능이 맞물려 돌아가는 &lt;strong&gt;사용자 시나리오&lt;/strong&gt;에서 시스템이 올바르게 반응하는지 확인합니다. 킹덤의 소원나무처럼 특정 횟수 이상의 소원을 들어주었을 때 발생하는 &apos;일일 보상(보물상자) 획득&apos; 같은 흐름이 대표적인 대상입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;N&lt;/code&gt;개의 소원을 들어주었을 때, 누적 횟수가 정확히 카운트되는가?&lt;/li&gt;
&lt;li&gt;일일 게이지가 목표치에 도달했을 때 보상 획득 활성화 상태가 정상적으로 변경되는가?&lt;/li&gt;
&lt;li&gt;실제 보상 획득 시, 보상 아이템이 지급됨과 동시에 게이지가 초기화되거나 다음 단계로 넘어가는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;part-2-작은-버그-수정해보기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#part-2-%EC%9E%91%EC%9D%80-%EB%B2%84%EA%B7%B8-%EC%88%98%EC%A0%95%ED%95%B4%EB%B3%B4%EA%B8%B0&quot; aria-label=&quot;part 2 작은 버그 수정해보기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;part 2. 작은 버그 수정해보기&lt;/h2&gt;
&lt;p&gt;테스트 작성에 어느 정도 익숙해질 즈음부터는, 실제 코드에 존재하는 사소한 버그를 수정하거나 데이터 검증 로직을 강화하는 업무를 맡게 됩니다. 예를 들어, &lt;strong&gt;&apos;개발맛 쿠키의 젤리 15종 물리치기&apos;&lt;/strong&gt; 라는 콘텐츠가 있다고 가정해 봅시다. 서버와 클라이언트는 각각 1번부터 15번 젤리에 대한 정보를 관리하고 있습니다. 만약 유저가 단 하나의 젤리도 처치하지 못한 채 게임이 종료되면, 기존 클라이언트에서는 기본값으로 &lt;strong&gt;&apos;0&apos;&lt;/strong&gt; 을 전송하고 있었습니다.&lt;/p&gt;
&lt;p&gt;하지만 서버에는 &apos;0번 젤리&apos;에 대한 데이터가 존재하지 않았기에 에러가 발생했고, 결과적으로 유저는 &quot;&lt;strong&gt;해당 요청을 처리하는 데 문제가 발생했습니다&quot;&lt;/strong&gt; 라는 문구를 마주하며 정상적으로 게임을 마칠 수 없는 상황이었습니다. 킹덤 팀은 이 문제를 해결하기 위해 &lt;strong&gt;&quot;1번 젤리조차 처치하지 못한 경우 클라이언트는 0 대신 Empty(None)를 보내고, 서버는 콘텐츠 참여 보상만 지급한다&quot;&lt;/strong&gt; 는 결론을 내렸습니다. 이때 제가 맡은 업무가 바로 이 예외 상황을 안전하게 처리하도록 기존 로직을 수정하는 것이었습니다.&lt;/p&gt;
&lt;figure&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; finishJellyWave&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;wave&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; List&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Reward&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; 
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// wave가 0일 경우, 데이터를 찾지 못해 에러(Exception) 발생&lt;/span&gt;
    clearRewardDatas &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; inquireJellyWaveClearRewards&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;wave&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; 
    playRewardDatas  &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; inquireJellyWavePlayRewards       &lt;span class=&quot;token comment&quot;&gt;// 게임 입장 보상 조회&lt;/span&gt;
    rewards          &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; rewards&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;clearRewardDatas &lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt; playRewardDatas&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;yield&lt;/span&gt; rewards&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;figcaption&gt;수정전: 젤리를 하나도 물리치지 못했을 때, 에러 발생&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; finishJellyWave&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;waveOpt&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Option&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; List&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Reward&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; 
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 패턴 매칭을 통해 클리어한 웨이브가 있을 때만 보상을 조회하도록 수정&lt;/span&gt;
    clearRewardDatas &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; waveOpt &lt;span class=&quot;token keyword&quot;&gt;match&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; Some&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;wave&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;=&gt;&lt;/span&gt; inquireJellyWaveClearRewards&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;wave&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; None       &lt;span class=&quot;token keyword&quot;&gt;=&gt;&lt;/span&gt; ZPure&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;succeed&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Nil&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 처리한 웨이브가 없어도 에러 없이 통과&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    playRewardDatas &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; inquireJellyWavePlayRewards
    rewards         &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; rewards&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;clearRewardDatas &lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt; playRewardDatas&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;yield&lt;/span&gt; rewards&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;figcaption&gt;수정 후: 젤리를 하나도 물리치지 못했을 때, 기본 보상 지급&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;part-3-본격적인-콘텐츠-개발&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#part-3-%EB%B3%B8%EA%B2%A9%EC%A0%81%EC%9D%B8-%EC%BD%98%ED%85%90%EC%B8%A0-%EA%B0%9C%EB%B0%9C&quot; aria-label=&quot;part 3 본격적인 콘텐츠 개발 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;part 3. 본격적인 콘텐츠 개발&lt;/h2&gt;
&lt;p&gt;특정 콘텐츠의 담당자로 배정되면, 먼저 킥오프 회의에서 기획에 대해 설명을 듣습니다. 해당 기획을 바탕으로 기획자 및 클라이언트 개발자분들과 협의하여 데이터 구조를 설계합니다. 데이터 구조 설계와 기능 정의가 완료되면 본격적인 구현에 들어갑니다.&lt;/p&gt;
&lt;p&gt;예를 들어, 조커 뽑기와 유사한 &lt;strong&gt;&apos;카드 뽑기 게임&apos;&lt;/strong&gt; 을 만든다고 가정해 봅시다. 기획안이 간단하여 서버는 결과 조회 및 보상 지급 기능만 구현하면 되는 상황입니다. 클라이언트에서 &quot;n번째 카드를 뽑겠다&quot;는 요청을 보내면, 서버는 슬롯 데이터를 무작위로 섞은 뒤 당첨된 보상이 무엇인지 판정하여 응답을 내려줍니다.&lt;/p&gt;
&lt;figure&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 잘못된 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;lazy&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; finishShellGame&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Result&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;List&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;ShellGameIndex&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; List&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Reward&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; 
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    shellGameData &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; inquireShellGameData &lt;span class=&quot;token comment&quot;&gt;// 게임 설정값 조회&lt;/span&gt;
    slots         &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;  shellGameData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;slots
    _             &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; shuffle&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;slots&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 각 슬롯을 섞음&lt;/span&gt;
    selectedSlot  &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;  Random&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;randInt&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; slots&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;size&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 당첨 슬롯 결정&lt;/span&gt;
    rewards       &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; rewards&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;shellGameData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;slot&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;selectedSlot&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;rewards&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 보상 획득&lt;/span&gt;
    _             &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; liftEvent&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ShellGameFinished&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;slots&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; selectedSlot&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; rewards&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 상태 변경 이벤트 발생&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;slots&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; selectedSlot&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; rewards&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;figcaption&gt;초안&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;구현을 마친 후 서버를 띄워 테스트를 진행하다 보면, 동료 개발자의 코드 리뷰나 QA, 기획팀의 피드백을 받게 됩니다.&lt;/p&gt;
&lt;aside style=&quot;border: 1px solid rgba(128, 128, 128, 0.3); border-left: 4px solid #f6a609; background: rgba(128, 128, 128, 0.06); padding: 16px 20px; border-radius: 8px; margin: 16px 0;&quot;&gt;
&lt;p&gt;💡  &lt;strong&gt;기획자 피드백&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&quot;안녕하세요 수지님! 여러 번 테스트해 보았는데, 슬롯들이 전혀 섞이지 않는 것 같습니다. 확인 부탁드려요! 🙏&quot;&lt;/p&gt;
&lt;/aside&gt;
&lt;p&gt;확인 결과, &lt;code&gt;shuffle&lt;/code&gt; 함수는 섞인 &lt;strong&gt;새로운 리스트를 반환&lt;/strong&gt;할 뿐 기존 리스트를 직접 수정(Mutation)하지 않는 스칼라의 불변성 원칙을 간과한 것이 원인이었습니다. 이를 아래와 같이 수정하여 다시 배포합니다.&lt;/p&gt;
&lt;figure&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;lazy&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; finishShellGame&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Result&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;List&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;ShellGameIndex&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; List&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Reward&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; 
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    shellGameData &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; inquireShellGameData 
    slots         &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; shuffle&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;shellGameData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;slots&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 섞인 새 리스트를 받도록 수정&lt;/span&gt;
    selectedSlot  &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;  Random&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;randInt&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; slots&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;size&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; 
    &lt;span class=&quot;token comment&quot;&gt;// ... 후략&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;figcaption&gt;카드가 섞이지 않는 버그 픽스&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;이후 코드 리뷰 과정에서 또 다른 피드백이 들어옵니다.&lt;/p&gt;
&lt;aside style=&quot;border: 1px solid rgba(128, 128, 128, 0.3); border-left: 4px solid #f6a609; background: rgba(128, 128, 128, 0.06); padding: 16px 20px; border-radius: 8px; margin: 16px 0;&quot;&gt;
&lt;p&gt;🥅  &lt;strong&gt;리뷰어 피드백&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&quot;수지님, &lt;code&gt;Random.randInt&lt;/code&gt;는 호출되는 즉시 값을 고정해 버리기 때문에, 함수형 구조 안에서 재사용할 때 매번 새로운 랜덤 값을 얻을 수 없습니다. 킹덤 서버는 예측 가능하고 안전한 로직을 위해, 실행 시점에 매번 새로운 무작위 수를 생성하는 &lt;code&gt;getRandInt&lt;/code&gt;를 사용하도록 아키텍처가 설계되어 있습니다. 이 코드로 변경해 주세요! 😄&quot;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;보충설명&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Random.randInt&lt;/code&gt;와 &lt;code&gt;getRandInt&lt;/code&gt; 둘 다 결국 &apos;랜덤 정수&apos;를 만들어 낸다는 목적은 같습니다. 하지만 &quot;값이 결정되는 시점&quot;이 완전히 다릅니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Random.randInt&lt;/code&gt;&lt;/strong&gt;: 변수에 할당되는 &lt;strong&gt;그 순간(선언부) 즉시&lt;/strong&gt; 무작위 숫자를 뽑아서 채워 넣습니다. 이후에 이 변수를 아무리 여러 번 호출해도 이미 처음에 뽑힌 숫자가 그대로 재사용됩니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;getRandInt&lt;/code&gt;&lt;/strong&gt;: 선언할 때는 &quot;앞으로 실행하면 랜덤 숫자를 뽑아줄게&quot;라는 &lt;strong&gt;동작만 정의&lt;/strong&gt;합니다. 실제로 이 변수를 &lt;code&gt;.run&lt;/code&gt;으로 실행할 때마다 매번 새로운 주사위를 굴립니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 1. Random.randInt를 사용했을 때 (값 고정)&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 선언하는 순간 이미 42라는 값이 결정되어 상수가 됩니다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; randomValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; pure&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Random&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;randInt&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; 

randomValue&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;run &lt;span class=&quot;token comment&quot;&gt;// 결과: 42&lt;/span&gt;
randomValue&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;run &lt;span class=&quot;token comment&quot;&gt;// 결과: 42 (매번 똑같은 값이 나옴)&lt;/span&gt;

&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 2. getRandInt를 사용했을 때 (매번 실행)&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 선언할 때는 동작만 정의하고, 실행할 때마다 주사위를 새로 굴립니다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; randomAction &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; getRandInt&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

randomAction&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;run &lt;span class=&quot;token comment&quot;&gt;// 결과: 91&lt;/span&gt;
randomAction&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;run &lt;span class=&quot;token comment&quot;&gt;// 결과: 7  (매번 새로운 값이 나옴)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/blockquote&gt;
&lt;/aside&gt;
&lt;p&gt;아까 수정했던 shuffle 메서드도 getRandInt와 마찬가지로 side-effect가 통제된 함수입니다. 이를 사용함으로써, 선언부에서 미리 결과를 계산해 저장하는 것이 아니라 실행될 때마다 다르게 섞이게 할 수 있습니다.&lt;/p&gt;
&lt;figure&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;lazy&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; finishShellGame&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Result&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;List&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;ShellGameIndex&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; List&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Reward&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; 
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    shellGameData &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; inquireShellGameData 
    slots         &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; shuffle&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;shellGameData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;slots&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; 
    selectedSlot  &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; getRandInt&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; slots&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;size&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Side-effect가 통제된 자체 randInt 사용&lt;/span&gt;
    rewards       &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; rewards&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;shellGameData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;slot&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;selectedSlot&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;rewards&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    _             &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; liftEvent&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ShellGameFinished&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;slots&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; selectedSlot&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; rewards&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;slots&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; selectedSlot&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; rewards&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;figcaption&gt;자체 메서드 사용&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;이러한 수정 과정을 반복하며 코드의 퀄리티를 점진적으로 높여 나갑니다. 단순히 동작하는 코드를 짜는 것을 넘어, 이벤트 발생 후의 요구사항 검증, 로그 기록 등 다양한 예외 케이스를 고려하는 법을 배웁니다. 특히 특정 콘텐츠의 수정이 시스템 전체나 다른 기능에 미칠 영향까지 유의하며 개발하는 역량을 키우게 됩니다.&lt;/p&gt;
&lt;p&gt;모든 QA와 리뷰를 마치면 마침내 코드가 라이브 서버에 배포됩니다. 배포 이후에도 본인이 맡은 기능에서 버그가 발생하지 않는지 세밀하게 모니터링합니다. 라이브 환경에서 발생하는 이슈들은 QA 단계에서 잡히지 않은 까다로운 케이스가 많습니다. 만약 재화 지급 오류 등이 발생했다면, 이미 재화를 소모한 유저의 회수 여부나 전 서버 보상 지급 등을 기획팀과 긴밀히 논의하여 후속 대응 방향을 결정하게 됩니다.&lt;/p&gt;
&lt;h2 id=&quot;clear-어깨너머로-배우는-성장의-즐거움&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#clear-%EC%96%B4%EA%B9%A8%EB%84%88%EB%A8%B8%EB%A1%9C-%EB%B0%B0%EC%9A%B0%EB%8A%94-%EC%84%B1%EC%9E%A5%EC%9D%98-%EC%A6%90%EA%B1%B0%EC%9B%80&quot; aria-label=&quot;clear 어깨너머로 배우는 성장의 즐거움 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Clear! 어깨너머로 배우는 성장의 즐거움&lt;/h2&gt;
&lt;p&gt;킹덤 서버팀은 콘텐츠 개발 외에도 성능 개선을 위한 다양한 시도들이 끊임없이 일어나는 곳입니다. 스칼라 프로젝트 서버 안정화를 위한 &apos;shardcake&apos; 같은 라이브러리 개발이 대표적인데, 동료분들이 치열하게 고민하시는 모습을 어깨너머로 지켜보는 것만으로도 공부가 많이 됩니다. 가끔씩은 작게나마 일손을 보태면서 프로젝트의 맥락을 이해하고, 우리 팀이 어떤 방향을 지향하는지 자연스럽게 익혀나가고 있습니다. 기술적인 부분뿐만 아니라 팀의 전반적인 분위기나 동료들의 관심사를 알아가는 것도 소소한 재미 중 하나고요.&lt;/p&gt;
&lt;h3 id=&quot;마치며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0&quot; aria-label=&quot;마치며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;마치며&lt;/h3&gt;
&lt;p&gt;이렇게 킹덤 서버에서 보낸 시간들을 정리해 보았습니다. 사실 생소한 테크 스택으로 첫 커리어를 시작한다는 게 조금 겁나기도 했지만, 뛰어난 분들과 함께 호흡하며 배울 수 있는 기회는 그 두려움보다 훨씬 값졌습니다.
혹시 Scala나 함수형 프로그래밍(FP)이 낯설어 지원을 망설이고 계신 분이 있다면, 너무 주저하지 말고 도전해 보세요! 이곳에서의 경험은 다른 곳에서 얻기 드문 즐겁고 든든한 자산이 될 거예요. 킹덤 서버에서 여러분과 함께 일할 수 있는 날을 기다리고 있겠습니다.&lt;/p&gt;
&lt;hr&gt;</content:encoded></item><item><title><![CDATA[쿠키런: 킹덤의 전투 데이터, JSON Schema로 검증하고 자동완성하기]]></title><description><![CDATA[JSON 형태로 된 게임 데이터를 검증하기 위해 JSON Schema를 이용하는 방법을 소개합니다. 클라이언트 코드에서 JSON Schema를 생성하고, 후처리하고, 검증 및 자동완성에 이용하는 과정을 순서대로 다룹니다.]]></description><link>https://tech.devsisters.com/posts/ck-json-validation</link><guid isPermaLink="false">https://tech.devsisters.com/posts/ck-json-validation</guid><pubDate>Tue, 19 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, 데브시스터즈 스튜디오 킹덤에서 전투 클라이언트 프로그래머로 일하고 있는 김용욱이라고 합니다. 게임 데이터를 JSON 형태로 엑셀에 수기 입력할 때, 오타나 잘못 입력된 값을 런타임 실행 전에 발견하기 어렵다는 문제가 있었습니다. 이 문제를 해결하기 위해 &lt;a href=&quot;https://json-schema.org/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;JSON Schema&lt;/a&gt;를 이용해 JSON 데이터를 자동 검증하게 되었는데, 그 과정에서 JSON Schema를 역직렬화되는 게임 데이터 클래스로부터 자동 생성하고 Contract Resolver를 이용하거나 JSON Schema 자체를 후처리하여 이용했습니다. 또한 텍스트 에디터인 Visual Studio Code와 연동하여 자동완성과 실시간 검증이 가능한 시스템을 만들었고, 그 결과 생산성을 높이고 오류를 줄일 수 있었습니다.&lt;/p&gt;
&lt;p&gt;이 글에서는 (1) 왜 JSON으로 데이터를 입력하는지 (2) JSON Schema의 필요성 (3) Schema 생성 및 커스터마이징 방법 (4) VS Code 연동 활용까지 순서대로 다룹니다.&lt;/p&gt;
&lt;h1 id=&quot;json으로-데이터를-입력하는-이유&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#json%EC%9C%BC%EB%A1%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%EC%9E%85%EB%A0%A5%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0&quot; aria-label=&quot;json으로 데이터를 입력하는 이유 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;JSON으로 데이터를 입력하는 이유&lt;/h1&gt;
&lt;p&gt;게임 개발을 할 때, 많이 쓰이는 패턴이 “로직을 데이터로 작성하는” 것입니다. 예를 들어&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;민트사탕맛 쿠키가 스킬을 사용하면, 스킬 사용 직후 아군에게 공격력 증가, 방어력 증가 버프를 부여하고, 0.5초 뒤에 가로 세로 4 범위의 적군에게 공격력 비례 피해를 입힌다. 1.5초 뒤에 가장 체력이 낮은 아군에게 최대 체력 비례 회복을 부여한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;는 스킬이 있다고 하면, 이를 코드에서 모두 처리할 수도 있지만 아래와 같이 데이터로 표현할 수도 있습니다. (설명을 위해 만든 가상의 데이터 구조입니다)&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SkillId&lt;/th&gt;
&lt;th&gt;SkillType&lt;/th&gt;
&lt;th&gt;SkillValues&lt;/th&gt;
&lt;th&gt;Timing&lt;/th&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MintCandy-001&lt;/td&gt;
&lt;td&gt;Add_Buff&lt;/td&gt;
&lt;td&gt;AttackPoint, 0.2, DefencePoint, 0.2&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Allies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MintCandy-002&lt;/td&gt;
&lt;td&gt;Attack&lt;/td&gt;
&lt;td&gt;AttackPoint, 0.5&lt;/td&gt;
&lt;td&gt;0.5&lt;/td&gt;
&lt;td&gt;Enemies, Rectangle, 4x4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MintCandy-003&lt;/td&gt;
&lt;td&gt;Heal&lt;/td&gt;
&lt;td&gt;AttackPoint, 0.25&lt;/td&gt;
&lt;td&gt;1.5&lt;/td&gt;
&lt;td&gt;Allies, LowestHP, 1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;데이터 형식으로 로직을 분리할 경우, 기획자가 데이터만 수정해 가면서 빠른 이터레이션이 가능하다는 장점이 있으며, 데이터 수정만으로 버그 수정이나 콘텐츠 업데이트를 진행 가능하다는 장점이 있습니다. 테이블 형식을 이용할 경우 기획자에게 익숙한 인터페이스인 스프레드시트를 활용할 수 있고, 데이터 역직렬화(텍스트 혹은 바이너리 형태로 된 데이터 포맷에서 코드에서 활용 가능한 데이터를 생성하는 것을 말합니다)도 용이하다는 장점이 있습니다.&lt;/p&gt;
&lt;p&gt;이런 테이블 형식의 데이터는, 아래와 같은 경우에 적합하다고 생각합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;순서가 없거나, 시계열에 따라 순서대로 발생하는 경우&lt;/li&gt;
&lt;li&gt;입력해야 할 값의 종류가 적고 그 중 전체 혹은 대다수를 입력해야 하는 경우&lt;/li&gt;
&lt;li&gt;다수의 데이터를 한눈에 살펴보면서 값을 정해야 하는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;하지만 가상의 예시로 그 뒤에 다음과 같은 기능을 만들게 되었다고 합시다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;민트사탕맛 쿠키가 마법사탕을 착용하면, 스킬이 다음과 같이 바뀐다. 게임 시작 시 민트 스택을 1개(최대 10스택) 가지고 시작하며, 스킬을 사용하거나 아군이 전투불능이 되면 스택을 1개 획득한다. 스킬을 사용하면 기존 스킬에 더해, 현재 민트 스택이 5개 이상이면 아군 전체에게 치명타 증가 버프를 부여한다. 1.5초 시점에, 민트 스택이 10개 이상이면 민트 스택을 모두 소진하고 적군 전체에게 민트 중독(전용 아이콘과 이펙트가 있음)을 부여한다. 민트 중독을 가지고 있다면 일정 시간마다 피해를 입으며, 대상이 스킬을 사용할 때마다 해로운 효과 증가 디버프와 큰 피해를 입는다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;그 자체로는 아무 역할도 하지 않지만, 전투 중 특정 조건에 따라 증감해야 하는 “스택” 개념이 생겼습니다.&lt;/li&gt;
&lt;li&gt;다른 경우에는 쓰이지 않지만 특정한 경우에만 쓰이는 “최대 스택”, “전용 아이콘”이나 “이펙트” 를 입력할 수 있어야 합니다.&lt;/li&gt;
&lt;li&gt;반복문, 분기처리 등 상대적으로 복잡한 상태를 관리해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이를 테이블 형태로만 입력하자니, 선택적으로 입력해야 하는 필드의 양이 너무 많아지고 가독성이 떨어집니다. 코드로 로직과 상태 관리를 작성할 수도 있겠지만, 로직을 데이터로 옮겼을 때의 장점을 유지하고 싶을 때가 있습니다. 이런 경우 JSON 형태로 데이터를 입력하고, 그 데이터를 런타임에 역직렬화하여 사용하는 것을 고려할 수 있습니다.&lt;/p&gt;
&lt;p&gt;JSON 포맷은 값에 숫자나 문자열, 배열이나 다른 JSON 객체를 담을 수 있어 유연한 적용이 가능하며 그러면서 가독성이 좋은 편이므로 수제로 작성하는 식으로도 활용할 수 있습니다. “민트 스택이 10스택 이상이 되면 다른 행동을 한다”는 동작을 JSON 포맷으로 표현하자면 아래와 같은 형태가 될 것입니다(마찬가지로 설명을 위한 가상의 데이터입니다).&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Check&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;version&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;check&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;condition&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Stack&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;target&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;MintStack&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;operator&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&gt;=&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;value&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;then&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;&quot;FirstAction&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;&quot;SecondAction&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;다음과 같은 조건에서&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;시계열이 아니라, 특정 조건에 따라 복잡한 분기처리를 해야 하는 경우&lt;/li&gt;
&lt;li&gt;입력해야 할 값의 종류가 많지만 실제로는 개중 일부만 입력해서 이용해야 하는 경우&lt;/li&gt;
&lt;li&gt;상대적으로 소수의 데이터를 복잡하게 설정하는 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;JSON 형태로 값을 입력하는 것을 고려해볼 수 있습니다.&lt;/p&gt;
&lt;h1 id=&quot;json-schema의-필요성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#json-schema%EC%9D%98-%ED%95%84%EC%9A%94%EC%84%B1&quot; aria-label=&quot;json schema의 필요성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;JSON Schema의 필요성&lt;/h1&gt;
&lt;p&gt;JSON Schema를 도입하기 전에는, 위 전투 시스템을 구현하는 각 필드를 기획자가 수동으로 입력하고 간단한 포매팅만 거쳐 Microsoft Excel에 입력하여 연동을 하고 있었습니다. JSON 포맷은 손으로 직접 작성할 수 있을 만큼 간단한 편이기 때문에 가능한 일입니다.&lt;/p&gt;
&lt;p&gt;그러나 JSON 포맷의 텍스트를 메모장이나 Microsoft Excel에 직접 입력하기에는 상당히 번거롭습니다. 또한 쿠키런: 킹덤에서는 JSON 포맷을 이용할 때 &lt;a href=&quot;https://docs.unity3d.com/2022.3/Documentation/Manual/JSONSerialization.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;유니티 내장 JSON 파서&lt;/a&gt;를 이용하여 역직렬화를 하고 있습니다. 속도가 빠르고 별도의 외부 라이브러리를 사용할 필요가 없다는 장점이 있지만, 아래와 같은 단점도 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JSON 포맷에 맞기만 하면 데이터에 잘못된 값이 들어가더라도 최대한 Graceful하게 살리려 한다.
&lt;ul&gt;
&lt;li&gt;예를 들어 존재하지 않는 필드를 입력하더라도, 조용히 무시한다.&lt;/li&gt;
&lt;li&gt;존재하는 필드의 타입을 잘못 입력하더라도, 혹은 잘못된 값을 입력하더라도 조용히 무시한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그 때문에&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;테이블 형식에 비해 오히려 입력이 어려워진다.&lt;/li&gt;
&lt;li&gt;잘못 입력하더라도 어디가 잘못되었는지 확인하기 어렵다.&lt;/li&gt;
&lt;li&gt;잘못된 필드에 값을 입력하고, 어디가 문제인지 찾는데 오래 걸린다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;라는 문제가 생깁니다.&lt;/p&gt;
&lt;p&gt;이럴 때 사용할 수 있는 것이 &lt;a href=&quot;https://json-schema.org/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;JSON Schema&lt;/a&gt;입니다. 스키마(Schema)란 특정한 데이터가 가져야 할 구조나 제약조건을 말하는 것으로 JSON Schema는 JSON 타입의 데이터를 정의하고 제약하는 문서라고 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;예를 들어 JSON 데이터에 아래와 같은 제약 조건을 설정하고 싶을 때&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“key” 필드는 문자열이어야 하며 필수적으로 존재해야 한다.&lt;/li&gt;
&lt;li&gt;“id” 필드는 정수여야 한다.&lt;/li&gt;
&lt;li&gt;위 필드 외에 다른 값을 입력하면 안 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;다음과 같은 JSON Schema를 이용할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;object&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;properties&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;key&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;string&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;integer&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;additionalProperties&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;required&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;key&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;JSON Schema는 JSON 포맷을 검증한다는 그 자체의 기능 외에도, 아래와 같은 장점이 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;그 자체로도 JSON 파일이기 때문에 가독성이 좋고 다양한 프로그램에서 로드와 수정이 쉽다.&lt;/li&gt;
&lt;li&gt;특정 값이 포함되어야 하는지, JSON Schema에서 정의한 키값 외에 다른 값들이 존재해도 되는지와 같은 메타적 요소도 검사 가능하다.&lt;/li&gt;
&lt;li&gt;복잡한 조건도 원한다면 검증 가능하다. (JSON Schema에는 &lt;a href=&quot;https://json-schema.org/understanding-json-schema/reference&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;각 JSON 필드에 사용 가능한 다양한 조건&lt;/a&gt;들이 정의되어 있습니다.)&lt;/li&gt;
&lt;li&gt;사용 환경에 독립적으로, JSON과 JSON Schema를 지원하기만 한다면 검증을 할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;C#에서는 &lt;a href=&quot;https://www.newtonsoft.com/json&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;&lt;code&gt;Json.NET&lt;/code&gt; 라이브러리&lt;/a&gt;를 이용해 JSON 포맷을 다룰 수 있으며 유니티 최신버전에 포함되어 있어 바로 이용 가능합니다. &lt;code&gt;Json.NET&lt;/code&gt;에서 스키마 검사를 하는 부분은 별도의 유료 라이브러리로 분리되어 있지만, 기본적인 스키마 검사 기능은 오픈소스 &lt;code&gt;Json.NET&lt;/code&gt;에 포함되어 있습니다. 현재 JSON 형태로 데이터를 입력하는 곳이 소수이기 때문에, 별도의 라이브러리 구입 및 임포트를 하는 대신 &lt;code&gt;Json.NET&lt;/code&gt;의 내장 스키마 검증 기능을 활용하기로 하였습니다.&lt;/p&gt;
&lt;h1 id=&quot;json-schema-생성하고-활용하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#json-schema-%EC%83%9D%EC%84%B1%ED%95%98%EA%B3%A0-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0&quot; aria-label=&quot;json schema 생성하고 활용하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;JSON Schema 생성하고 활용하기&lt;/h1&gt;
&lt;h2 id=&quot;스키마-작성하고-검증하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8A%A4%ED%82%A4%EB%A7%88-%EC%9E%91%EC%84%B1%ED%95%98%EA%B3%A0-%EA%B2%80%EC%A6%9D%ED%95%98%EA%B8%B0&quot; aria-label=&quot;스키마 작성하고 검증하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;스키마 작성하고 검증하기&lt;/h2&gt;
&lt;p&gt;스키마를 손으로 직접 작성하는 것은, 위 예시와 같이 그렇게 어렵지 않습니다. 이렇게 작성한 스키마를 텍스트 파일에서 읽어와, 검증하는 것도 간단하게 가능합니다. 어려운 부분은, 우리 목적에 맞는 올바른 스키마를 생성하는 것입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;csharp&quot;&gt;&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code class=&quot;language-csharp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// &amp;lt;https://www.newtonsoft.com/JSON/help/html/JTokenIsValidWithMessages.htm&gt;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 적절한 곳(이 경우는 C:\\schema.JSON)에서 읽어 온다.&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;JSONSchema&lt;/span&gt; schema &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; JSONSchema&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Parse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;File&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;ReadAllText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;@&quot;C:\\schema.json&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// JSON string을 파싱한다.&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;JObject&lt;/span&gt; person &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; JObject&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Parse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;@&quot;{
  &apos;name&apos;: null,
  &apos;hobbies&apos;: [&apos;Invalid content&apos;, 0.123456789]
}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token class-name&quot;&gt;IList&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; messages&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 검증한다.&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;bool&lt;/span&gt;&lt;/span&gt; valid &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; person&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;IsValid&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;schema&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;out&lt;/span&gt; messages&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

Console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;WriteLine&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;valid&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// false&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 에러 메시지가 나온다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;foreach&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;string&lt;/span&gt;&lt;/span&gt; message &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; messages&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    Console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;WriteLine&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;스키마-자동-생성하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8A%A4%ED%82%A4%EB%A7%88-%EC%9E%90%EB%8F%99-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0&quot; aria-label=&quot;스키마 자동 생성하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;스키마 자동 생성하기&lt;/h2&gt;
&lt;p&gt;JSON Schema를 손으로 작성하는 것은 필드가 한 두 개 정도 있을 때 가능한 일이며, 그보다는 코드에서 자동으로 생성해주는 것이 바람직합니다. Json.NET에서는 &lt;code&gt;JSONSchemaGenerator&lt;/code&gt; 클래스를 이용하여 특정 클래스 타입으로부터 스키마를 생성할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;csharp&quot;&gt;&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code class=&quot;language-csharp&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt;&lt;/span&gt; generator &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token constructor-invocation class-name&quot;&gt;JSONSchemaGenerator&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt;&lt;/span&gt; schema &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; generator&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Generate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;type&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token constructor-invocation class-name&quot;&gt;JSONSchemaResolver&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;실제 예시를 통해 설명하겠습니다. 아래와 같은 클래스가 있다고 합시다. JSON 값은 이 클래스로 역직렬화되고, 이후 &lt;code&gt;Initialize()&lt;/code&gt;가 불린 뒤 활용됩니다. 클래스는 다음과 같습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;csharp&quot;&gt;&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code class=&quot;language-csharp&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token attribute&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;Serializable&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CookieData&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token attribute&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;Serializable&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CookieType&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        Normal &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        Uncommon &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        Bright &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        Crispy &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token return-type class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt;&lt;/span&gt; Id &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; id&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token return-type class-name&quot;&gt;CookieType&lt;/span&gt; TypeOfCookie &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; typeOfCookie&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token return-type class-name&quot;&gt;Vector2&lt;/span&gt; Position &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; position&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token attribute&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;SerializeField&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt;&lt;/span&gt; id&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token attribute&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;SerializeField&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;string&lt;/span&gt;&lt;/span&gt; type&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CookieType&lt;/span&gt; typeOfCookie&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token attribute&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;SerializeField&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;float&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt; positionValue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Vector2&lt;/span&gt; position&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token return-type class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Initialize&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        typeOfCookie &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Enum&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token generic-method&quot;&gt;&lt;span class=&quot;token function&quot;&gt;Parse&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;CookieType&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;type&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        position &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token constructor-invocation class-name&quot;&gt;Vector2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;positionValue&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; positionValue&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;아래는 위 클래스로 역직렬화 가능한 JSON 예시 데이터입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Crispy&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;token property&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 
  &lt;span class=&quot;token property&quot;&gt;&quot;positionValue&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
		&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
		&lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 클래스에서 그대로 JSON Schema를 아무 처리 없이 &lt;code&gt;JSONSchemaGenerator()&lt;/code&gt; 로 생성하면 다음과 같이 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;CookieData&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;object&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;null&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;properties&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;Id&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;required&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;integer&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;TypeOfCookie&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;required&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;integer&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;enum&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;Position&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;UnityEngine.Vector2&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;required&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;&quot;object&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;&quot;null&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;additionalProperties&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;properties&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;x&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token property&quot;&gt;&quot;required&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;number&quot;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;y&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token property&quot;&gt;&quot;required&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;number&quot;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;normalized&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token property&quot;&gt;&quot;$ref&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;#/properties/Position&quot;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;magnitude&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token property&quot;&gt;&quot;required&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;number&quot;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;sqrMagnitude&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token property&quot;&gt;&quot;required&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;number&quot;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;검증을 해 봅시다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;csharp&quot;&gt;&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code class=&quot;language-csharp&quot;&gt;Required properties are missing &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; TypeOfCookie&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Position&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이상하게도 제대로 된 JSON 데이터를 오류라고 인식합니다. JSON Schema가 잘못된 것 같습니다. 대표적으로,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;우리가 입력할 때 이용하는 키값은 &lt;code&gt;camelCase&lt;/code&gt;인데, 키값이 &lt;code&gt;PascalCase&lt;/code&gt;에 대해 생성되었습니다.&lt;/li&gt;
&lt;li&gt;역직렬화에 사용할 수 없는 퍼블릭 프로퍼티에 대해서 키값이 생성되었습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;typeOfCookie&lt;/code&gt;는 Enum 타입인데, 불편하게 숫자를 직접 입력하도록 하고 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이럴 경우, 스키마를 생성하는 Generator를 직접 수정하는 대신 크게 두 가지 방법으로 생성되는 스키마를 커스터마이징할 수 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Contract Resolver&lt;/code&gt;: 역직렬화 대상이 되는 클래스에서 JSON Schema를 생성할 때, 어떤 값에 대해 어떻게 동작해야 하는지를 재정의합니다. 예를 들어 데이터의 키값이 클래스의 어떤 값과 매칭되어야 하는지, 키값의 데이터 타입이 어떤 것이 되어야 하는지 역직렬화되는 클래스를 읽어들이며 수정합니다.&lt;/li&gt;
&lt;li&gt;JSON Schema 후처리: JSON Schema도 결국 JSON 형태의 데이터이므로, 원본 데이터와 각 필드의 타입을 가지고 Schema를 재정의할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;1-contract-resolver-튜닝하기-입력해야-하는-값을-가져오는-문제&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-contract-resolver-%ED%8A%9C%EB%8B%9D%ED%95%98%EA%B8%B0-%EC%9E%85%EB%A0%A5%ED%95%B4%EC%95%BC-%ED%95%98%EB%8A%94-%EA%B0%92%EC%9D%84-%EA%B0%80%EC%A0%B8%EC%98%A4%EB%8A%94-%EB%AC%B8%EC%A0%9C&quot; aria-label=&quot;1 contract resolver 튜닝하기 입력해야 하는 값을 가져오는 문제 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;1. Contract Resolver 튜닝하기: 입력해야 하는 값을 가져오는 문제&lt;/h2&gt;
&lt;p&gt;기본적으로 &lt;code&gt;JSONSchemaGenerator&lt;/code&gt;는 해당 타입의 퍼블릭 필드와 프로퍼티가 직렬화된다고 가정하며, 퍼블릭 프로퍼티의 값에서 JSON 오브젝트를 생성합니다. 또한 모든 값들을 필수적으로 입력해야 한다고 간주합니다. 그런데 우리는 &lt;code&gt;[SerializeField]&lt;/code&gt; 어트리뷰트가 붙은 (camelCase인) private 변수 이름을 가져오고, 타입은 이름이 일치하는 퍼블릭 프로퍼티를 따르게 하고 싶습니다. 그리고 대부분의 경우는 필수로 입력할 필요는 없고, 대신 다른 값을 잘못 입력하는 것을 막고 싶습니다.&lt;/p&gt;
&lt;p&gt;첫 번째 문제부터 해결합시다. 간단히는 퍼블릭 프로퍼티의 이름이 private 변수와 대응하도록 수정해 주는 것입니다. 예를 들어 &lt;code&gt;public SomeNumber ⇒ someNumberProperty.Value&lt;/code&gt; 혹은 &lt;code&gt;public SomeClass ⇒ someClass&lt;/code&gt; 과 같이 지정해주는 것으로 해결이 가능합니다.&lt;/p&gt;
&lt;p&gt;그러나 레거시 데이터 중에서 데이터의 키값 이름을 수정하기 어려운 경우가 있습니다. 이럴 때 Contract resolver를 커스터마이징하여 클래스가 있을 때 어떤 식으로 값을 인식할지를 튜닝할 수 있습니다.&lt;/p&gt;
&lt;p&gt;위에서도 말했듯이, Contract Resolver는, 각 변수를 읽어들일 때 그 변수에 해당하는 JSON 값이 어떻게 매핑되어야 하는지 결정하는 클래스입니다. 예를 들어서, 해당 변수가 &lt;code&gt;int&lt;/code&gt; 타입이라면 정수로 매핑이 되고, &lt;code&gt;string&lt;/code&gt; 타입이면 문자열로 매핑이 되는 식입니다. 타입과 함께 다른 세부 정보들도 읽어들일 수 있습니다.&lt;/p&gt;
&lt;p&gt;Contract Resolver를 오버라이드하여 커스터마이징을 해 줍시다. 커스터마이징의 예시로, 전용 어트리뷰트를 하나 만들어, 이 어트리뷰트에서 정보를 가져오게 할 수 있습니다. 예를 들어, 명명 규칙이 대응하지 않는 경우 해당 프로퍼티에 어트리뷰트 &lt;code&gt;[GenerateSchemaAs(”someOtherName”)]&lt;/code&gt;를 붙인 뒤 Contract Resolver 단계에서 인식하는 식으로 대응이 가능합니다. 타입이 대응하지 않는 경우(입력은 &lt;code&gt;double[]&lt;/code&gt;로, 출력은 &lt;code&gt;Vector3&lt;/code&gt;으로 하는 경우 등)는 어트리뷰트 &lt;code&gt;[GenerateSchemaAs(typeof(OtherType))]&lt;/code&gt; 식으로 하여 &lt;code&gt;OtherType&lt;/code&gt;을 타입으로 가져오게 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;이 Contract Resolver가 값을 읽을 수 있게 클래스를 수정해주면 다음과 같이 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;csharp&quot;&gt;&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code class=&quot;language-csharp&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token attribute&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;Serializable&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CookieData&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token attribute&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;Serializable&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CookieType&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        Normal &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        Uncommon &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        Bright &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        Crispy &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token return-type class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt;&lt;/span&gt; Id &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; id&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token return-type class-name&quot;&gt;CookieType&lt;/span&gt; Type &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; typeOfCookie&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;// 명명 규칙이 대응하지 않는 경우&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token attribute&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;GenerateSchemaAs&lt;/span&gt;&lt;span class=&quot;token attribute-arguments&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;typeof&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token type-expression class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;float&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;positionValue&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 명명 규칙과 타입이 모두 불일치하는 경우&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token return-type class-name&quot;&gt;Vector2&lt;/span&gt; Position &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; position&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token attribute&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;SerializeField&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt;&lt;/span&gt; id&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token attribute&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;SerializeField&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;string&lt;/span&gt;&lt;/span&gt; type&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CookieType&lt;/span&gt; typeOfCookie&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token attribute&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;SerializeField&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;float&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt; positionValue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Vector2&lt;/span&gt; position&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token return-type class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Initialize&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        typeOfCookie &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Enum&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token generic-method&quot;&gt;&lt;span class=&quot;token function&quot;&gt;Parse&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;CookieType&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;type&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        position &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token constructor-invocation class-name&quot;&gt;Vector2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;positionValue&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; positionValue&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이제 JSON Schema는 다음과 같이 변경되었습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;CookieData&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;object&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;null&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;properties&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;Id&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;required&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;integer&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;required&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;integer&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;enum&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;positionValue&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;System.Float[]&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;required&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;&quot;array&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;&quot;null&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;items&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;number&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;하지만 아직 enum 등에 대해 제대로 대응이 되어 있지 않으므로, Contract Resolver만을 이용해 수정이 불가능한 경우 커스텀 Contract Resolver를 이용해 생성한 JSON Schema에 대해 추가적으로 후처리를 해 주어야 합니다.&lt;/p&gt;
&lt;h2 id=&quot;2-json-schema-후처리하기-enum-as-string&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-json-schema-%ED%9B%84%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0-enum-as-string&quot; aria-label=&quot;2 json schema 후처리하기 enum as string permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;2. JSON Schema 후처리하기: Enum as String&lt;/h2&gt;
&lt;p&gt;전투 데이터에서 열거형(Enum) 타입을 입력할 때는 string으로 입력하고, 이 값을 나중에 파싱을 해 집어 넣고 있습니다. 그런데 내장된 Generator에서는 열거형 타입을 숫자로 대응하고 있었습니다. 열거형 데이터를 숫자로 입력하는 것은 의도한 것도 아니고, 실제로 입력하는 것도 직관적이지 않습니다. Schema Generator 자체를 커스터마이징하는 방법도 생각할 수 있지만, 외부 라이브러리에 존재하는 Deprecated된 클래스를 직접 수정하는 것은 배보다 배꼽이 더 큰 일이라고 생각했습니다.&lt;/p&gt;
&lt;p&gt;그래서 Enum이 숫자로 매핑된 스키마를 가져와, 원본 타입 데이터를 이용해 그 타입이 다시 string 타입에 매핑되도록 후처리를 해주었습니다. 스키마를 재귀적으로 탐색하면서 Enum을 만나면 해당 Enum의 문자열을 가져와, 숫자 대신 그 문자열만 입력이 가능하도록 제한해주면 됩니다.&lt;/p&gt;
&lt;p&gt;JSON Schema를 후처리하면서 다른 문제도 해결해봅시다. JSON Schema에서는 각 값의 메타데이터를 지정할 수 있습니다. 각 object나 array, enum 등에 &lt;code&gt;additionalProperties = false&lt;/code&gt;를 추가하고(정의되지 않은 불필요한 값을 입력한 경우 에러가 발생함), 불필요한 &lt;code&gt;required&lt;/code&gt;는 Null로 설정해 제거해줍시다(일부 값을 미입력하더라도 에러로 취급하지 않음).&lt;/p&gt;
&lt;p&gt;이렇게 후처리를 하게 되면 결과적으로 아래와 같은 스키마가 생성됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;CookieData&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;object&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;null&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;additionalProperties&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;properties&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;integer&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;string&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;enum&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;&quot;Normal&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;&quot;Uncommon&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;&quot;Bright&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;&quot;Crispy&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;positionValue&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;id&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;System.Single[]&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;&quot;array&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;&quot;null&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&quot;items&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;number&quot;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h1 id=&quot;json-데이터를-이용하는-방법&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#json-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95&quot; aria-label=&quot;json 데이터를 이용하는 방법 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;JSON 데이터를 이용하는 방법&lt;/h1&gt;
&lt;p&gt;일단 JSON Schema를 이용해 전투 데이터를 제대로 입력했는지 검증할 수 있습니다. 쿠키런: 킹덤에서는 매 전투 테스트마다 별도의 스레드에서 JSON Schema 검증을 실행하고, 오류가 존재하는 경우 유니티 로그로 출력해주는 방식을 이용하고 있습니다.&lt;/p&gt;
&lt;p&gt;여기서 데이터를 검증하는 데서 그치지 않고, 데이터를 만들 때  &lt;a href=&quot;https://code.visualstudio.com/docs/languages/json&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Visual Studio Code의 JSON Schema 지원&lt;/a&gt;을 활용할 수 있습니다. Visual Studio Code는 마이크로소프트에서 만든 범용 텍스트 에디터로, 다양한 포맷을 지원하는 것이 특징 중 하나입니다. VS Code에서는 JSON Schema를 읽어 오고, 이를 이용해 원하는 JSON 파일에 대해 자동완성 및 값 검증을 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1756px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 80.27777777777779%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAQCAYAAAAWGF8bAAAACXBIWXMAABYlAAAWJQFJUiTwAAACcUlEQVR42q2Uz2sTURDHU4zoXQNSk5iUFiTk5qWBQg7+CwU96CVItf+EtbnYm16kTf0TPJhfbWJiF8EFLYg/MApKDDk0u/n9O9lk9+2OM2+zaYpILTjw2eyb2fd982aG2GxoUP6x8OHz/krm7ftlUTwMiG/EgCiKgVQqxclms1MsH8WJTCazjP4VZMFm2dNHD9OHn15DtV5lsnQEsixBqVSCQqEA+XweisXiFPIRFCcwziRJglwul+ZiGzab/Yr3uvAitg/ABqzfrUGv14PBUAFN0/6KqqoWDNAqlYqAcudswWDQ7nG7hGj0Jfm1SmtotOQjQ2nWDFz/CxoXLP08mBF0C9FYjPxMqjWg2aiBNujyr08zwzDMDMvSTIYejxCLx7mgogzpIwRA1/VTYWi0sVqtHgt6vV4hkUhwQVXTrKMBzpBhs9k0BdHsfr9f2EsmQUfB0XgMDEXpYAszYwPGGBuNRvyXGjJ554L1ev1YcHFpSYjFotAda6xaq0O304EO0u/3eUfpapagJUZMDvhT0OfzCen0K3TrrPArD5IsQ7lcxnmUaRzoOtBut2EwGICiKDAcDnl2dJgliPHZprixhkleww5upMxoFgnrirMlsJhtylRwAwXnnc5plzWNnRw0w0THhz7pvrk2fUzXuWBj2hQUXPS4DvYSfA5VjOt09zOg0kas+WSwsYbOGzeFnbgIRRXYxwqDL3U4wdcGwLcmQG4CrYnvLfQ3DFbEHN8VGtOmzF1c3bo9vxZ57AjtbJ6/8yx8YQZaO+5th73ru+Gra5Gw634kfCm0HXaETN+1B7ubzvXnW5fvPrll/dnMTfgv9huA3qvAW0gcDAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/67caae93d8b0fcdcc7b3b2c90d5791b4/33ecc/json-validation-in-vs-code.webp 360w,
/static/67caae93d8b0fcdcc7b3b2c90d5791b4/263a4/json-validation-in-vs-code.webp 480w,
/static/67caae93d8b0fcdcc7b3b2c90d5791b4/0c8fb/json-validation-in-vs-code.webp 640w,
/static/67caae93d8b0fcdcc7b3b2c90d5791b4/521f7/json-validation-in-vs-code.webp 860w,
/static/67caae93d8b0fcdcc7b3b2c90d5791b4/cbd37/json-validation-in-vs-code.webp 1200w,
/static/67caae93d8b0fcdcc7b3b2c90d5791b4/643a2/json-validation-in-vs-code.webp 1756w&quot;
              sizes=&quot;(max-width: 1756px) 100vw, 1756px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/67caae93d8b0fcdcc7b3b2c90d5791b4/53918/json-validation-in-vs-code.png 360w,
/static/67caae93d8b0fcdcc7b3b2c90d5791b4/9aebd/json-validation-in-vs-code.png 480w,
/static/67caae93d8b0fcdcc7b3b2c90d5791b4/0f09e/json-validation-in-vs-code.png 640w,
/static/67caae93d8b0fcdcc7b3b2c90d5791b4/3e3fe/json-validation-in-vs-code.png 860w,
/static/67caae93d8b0fcdcc7b3b2c90d5791b4/64756/json-validation-in-vs-code.png 1200w,
/static/67caae93d8b0fcdcc7b3b2c90d5791b4/0aea6/json-validation-in-vs-code.png 1756w&quot;
            sizes=&quot;(max-width: 1756px) 100vw, 1756px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/67caae93d8b0fcdcc7b3b2c90d5791b4/0aea6/json-validation-in-vs-code.png&quot;
            alt=&quot;json-validation-in-vs-code&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;json-validation-in-vs-code&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;video src=&quot;/c3fcdf0a56801c44d15c08339d70eb68/json-validation-in-vs-code-example.mp4&quot; controls=&quot;&quot;&gt;&lt;/video&gt;&lt;/p&gt;
&lt;p&gt;이때 기획자나 QA도 별도의 클라이언트 조작 없이 편리하게 JSON 데이터를 입력할 수 있도록 일종의 “올인원 패키지”를 구성해 배포해주었습니다. 필드나 Enum의 값이 추가되는 등 데이터 구조가 바뀌면, JSON Schema를 사용자가 정의한 폴더로 내보내고, 업데이트합니다. 아래와 같이 정의하면, 특정 폴더 내의 모든 JSON 파일에 대해 해당 경로의 JSON Schema로 검증합니다. 이때 git 저장소 형태로 배포를 한다고 하면, 폴더의 하위 경로를 &lt;code&gt;.gitignore&lt;/code&gt; 파일에 추가해주면 각 사용자는 원하는 대로 폴더를 만들고 하위에 JSON 파일을 생성하여 콘텐츠 작업에 활용할 수 있습니다. 이제 이 폴더와 JSON Schema, &lt;code&gt;.gitignore&lt;/code&gt;를 합쳐서 git 저장소나 그 일부로 배포해주면 됩니다. 이때, settings.json 파일 내의 주석은 제거해야 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;# 폴더 내 구성 예시
- .vscode/settings.json
- .gitignore
- json-schema.json
- SandBox/
- README.md

# .gitignore의 예시
!.vscode/* # VS Code 워크스페이스 설정은 동기화되어야 함
SandBox/**/*.json # SandBox 폴더 내의 모든 JSON 파일을 Git에서 무시함

# settings.json의 예시
{
  &quot;[Json]&quot;: {
    &quot;editor.tabSize&quot;: 2 # 탭 사이즈를 통일해줍니다
  },
  &quot;editor.unicodeHighlight.nonBasicASCII&quot;: false, # 한글이 입력된 것을 에러로 판단하지 않게 합니다
  &quot;json.schemas&quot;: [
    {
      # SandBox 폴더 내의 모든 JSON 파일에 대해 해당 스키마를 적용합니다 (자기 자신 제외)
      &quot;fileMatch&quot;: [&quot;SandBox/**/*.json&quot;, &quot;!json-schema.json&quot;],
      &quot;url&quot;: &quot;./json-schema.json&quot;
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;결과적으로, 자동완성을 이용하여 빠르고 정확하게 콘텐츠 추가가 가능해집니다. 이미 입력한 JSON 데이터에서 어떤 문법적 문제가 있는지도 쉽게 체크할 수 있습니다. 필요한 경우, 자주 쓰이는 구문은 Code Snippet으로 정의하여 상용구와 같이 입력하는 것도 가능합니다.&lt;/p&gt;
&lt;h1 id=&quot;결론&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%B0%EB%A1%A0&quot; aria-label=&quot;결론 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;결론&lt;/h1&gt;
&lt;p&gt;게임 개발을 진행하다 보면 JSON 포맷으로 데이터를 손수 입력하고 이를 역직렬화하여 사용해야 할 때가 있습니다. 이럴 때 역직렬화 대상이 되는 클래스에서 JSON Schema를 추출하고, 이를 이용해 수제 데이터를 검증할 수 있었습니다. JSON Schema가 생성되는 방식을 튜닝하고 후처리를 해주어 우리가 원하는 대로 JSON Schema를 정의할 수 있었고, 이를 바탕으로 Visual Studio Code와 연동하여 콘텐츠 작성의 효율을 극대화할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;해당 작업은 2023년부터 쿠키 캐릭터 및 여타 전투 콘텐츠의 규모와 복잡도가 높아지는 것에 발맞추어 진행하였으며 이후 수 차례의 고도화를 거쳐 현재 클라이언트 프로그래머와 기획자, QA 담당자가 모두 사용하고 있는 시스템으로 발전시켰습니다. 이 작업을 통해 개개인의 생산성을 높인 것은 물론 전투 콘텐츠에서 실수와 오류 발생을 줄일 수 있어서 보람이 있는 작업이었습니다.&lt;/p&gt;
&lt;p&gt;앞으로도 JSON Schema 시스템과 같은 소소한 개선과 함께 플레이어 여러분께 즐거운 경험을 선사할 수 있도록 노력해 나가겠습니다. 감사합니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[데브시스터즈 엔지니어링 데이 - 게임 서버 돌아보기]]></title><description><![CDATA[데브시스터즈에서 개최한 세 번째 엔지니어링 데이 - 게임 서버를 소개합니다.]]></description><link>https://tech.devsisters.com/posts/2025-engineering-day-game-server</link><guid isPermaLink="false">https://tech.devsisters.com/posts/2025-engineering-day-game-server</guid><pubDate>Thu, 30 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요. 데브시스터즈 기술본부에서 조직·기술 문화를 가꾸고 있는 전경아입니다. 어느덧 세 번째 엔지니어링 데이 돌아보기 글로 인사를 드립니다. 이번 글에서는 2025년 9월 25일에 진행했던 &amp;#x3C;제3회 데브시스터즈 엔지니어링 데이 - 게임 서버&gt;를 소개합니다. 온라인 다시 보기 영상 링크도 함께 제공해드립니다.&lt;/p&gt;
&lt;h2 id=&quot;행사-소개&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%96%89%EC%82%AC-%EC%86%8C%EA%B0%9C&quot; aria-label=&quot;행사 소개 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;행사 소개&lt;/h2&gt;
&lt;p&gt;‘데브시스터즈 엔지니어링 데이’란, 데브시스터즈에서 마주하는 기술적인 문제를 공유하고, 어떻게 풀어가는지 외부에 경험을 나누는 행사입니다. 본 행사를 통해 개발자 간에 소통할 수 있는 교류의 장을 만들고, 개발자 생태계에 긍정적인 영향을 미치기 위해 노력하고 있습니다.&lt;/p&gt;
&lt;p&gt;지난 주제인 Infra/SRE, 데이터에 이어 세 번째 행사는 ‘게임 서버’를 주제로 진행하였습니다. 데브시스터즈는 ‘쿠키런: 킹덤’, ‘쿠키런: 모험의 탑’ 등 여러 게임을 서비스하고 있고, 또 신작을 새로 개발 중이기도 한데요, 전 세계 수천만 유저가 우리의 게임을 재미있게 즐길 수 있도록 대규모 트래픽 속에서도 안정적인 라이브 서비스를 제공하며 기술적인 도전을 멈추지 않는 서버 직군의 이야기를 들어보았습니다.&lt;/p&gt;
&lt;p&gt;지난 발표 영상은 데브시스터즈 테크 유튜브 채널에서 다시 보실 수 있습니다. &lt;a href=&quot;https://www.youtube.com/@DEVSISTERS_TECH&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://www.youtube.com/@DEVSISTERS_TECH&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 43.333333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAABYlAAAWJQFJUiTwAAACgklEQVR42g2SSU9TYRiF7xqnIDGRyOCCqEGmagGhrZaxdIDOtxNtaXvpRCkNtCBaqiAUZBDHqFFMSEyUqDFudGFcuHAjcSGu3PgjTFw/fos3Oasn57znSIq5i5JiZ8pvIuPu42Y6QiYcIGAbIh5wko2FSAdlUkJPhJxCjzDuMzDm1DNq0zOXzeIdMeEx60n4LEg5Tx8ph04Ah8gpCpl4jtiogmx14Rk2EZUt5GIe8olRphUvuYhDgC3E5H4B6WbcbyfmtRMwa7g7F0aKOAYZHuxH09FMXW0NapWaZDTMZCKFEooScDrxC7djLiMRt0mATARtPchGDWa9mrDbwOZigVI+xc6jMpLPqifksWMd6uHQoWMcPlKFbBeR/FaifgfxoJupcfGG6CjFmTSl6TgTAQuToRHmUl4erM3zeHOB7ZUCK9cnkNzCatBxhaCrn/PNrQwYrRSvzbJUmieTGUcWcbY2Ftnduc9icYbF+Qy3SzluCPiYY4DhvkvYDDqyUSdhRy/SbCZJYSJB1OPGq0xS/vqbW19+svF5n7kbC7iM/SR9LpKyFUunir1ijIdTIYwdbWjbGulWt9KpakTX3kRTQx3So/VVHt9dZyUdZ237Ccs//lIUt/LrH6XVDfJjHsr5KW4trfLs3jYvy1cJi7JqauqpP1VD9clqqiorOXG8koqKo0hnLzbgLgwTMqnZe7HO6/0/vPp2wMfvB3x6u8vT1Ws82yrz7v0H8oWrXO7S4jIZmE6n0Kqa0KhaMPboaD5zGm1rE1LQ18ts1kbEpmW7mGLzzhrLSwvks0nRtpf2llbyfjN5JUhjbTU9F84xK2Bvnj8RU+llRK9jMhkiIRswdmv4D+Q3fd1WNZnVAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/5ed3fe945b4b368df8767c981fe080ea/33ecc/image_01.webp 360w,
/static/5ed3fe945b4b368df8767c981fe080ea/263a4/image_01.webp 480w,
/static/5ed3fe945b4b368df8767c981fe080ea/0c8fb/image_01.webp 640w,
/static/5ed3fe945b4b368df8767c981fe080ea/521f7/image_01.webp 860w,
/static/5ed3fe945b4b368df8767c981fe080ea/cbd37/image_01.webp 1200w,
/static/5ed3fe945b4b368df8767c981fe080ea/548e7/image_01.webp 2400w,
/static/5ed3fe945b4b368df8767c981fe080ea/f93c5/image_01.webp 2680w&quot;
              sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/5ed3fe945b4b368df8767c981fe080ea/53918/image_01.png 360w,
/static/5ed3fe945b4b368df8767c981fe080ea/9aebd/image_01.png 480w,
/static/5ed3fe945b4b368df8767c981fe080ea/0f09e/image_01.png 640w,
/static/5ed3fe945b4b368df8767c981fe080ea/3e3fe/image_01.png 860w,
/static/5ed3fe945b4b368df8767c981fe080ea/64756/image_01.png 1200w,
/static/5ed3fe945b4b368df8767c981fe080ea/98e2c/image_01.png 2400w,
/static/5ed3fe945b4b368df8767c981fe080ea/065ce/image_01.png 2680w&quot;
            sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/5ed3fe945b4b368df8767c981fe080ea/98e2c/image_01.png&quot;
            alt=&quot;image 01&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;h2 id=&quot;발표-1---scala-리얼타임-게임-백엔드-해부하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B0%9C%ED%91%9C-1---scala-%EB%A6%AC%EC%96%BC%ED%83%80%EC%9E%84-%EA%B2%8C%EC%9E%84-%EB%B0%B1%EC%97%94%EB%93%9C-%ED%95%B4%EB%B6%80%ED%95%98%EA%B8%B0&quot; aria-label=&quot;발표 1   scala 리얼타임 게임 백엔드 해부하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;발표 1 - &lt;strong&gt;Scala 리얼타임 게임 백엔드 해부하기&lt;/strong&gt;&lt;/h2&gt;
&lt;picture&gt;
&lt;iframe width=&quot;560&quot; height=&quot;315&quot; style=&quot;width: 100%; height: 100%; aspect-ratio: 16/9;&quot; src=&quot;https://www.youtube.com/embed/5IfvUaaMBuI?si=tH2x9q6WKCGlEakY&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerpolicy=&quot;strict-origin-when-cross-origin&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;figcaption&gt;
쿠키런: 킹덤 서버 엔지니어 손명배님
&lt;/figcaption&gt;
&lt;/picture&gt;
&lt;p&gt;쿠키런: 킹덤은 함수형 프로그래밍 패러다임에 기반한 Scala로 백엔드를 구현한 프로젝트입니다. 2021년 출시 이후 지금도 활발하게 개발되고 있는 킹덤 백엔드는 여전히 명료하고 읽기 쉬운 코드, 강력한 확장성과 성능, 유지보수의 편의를 모두 만족하고자 끊임없이 노력하고 있습니다.&lt;/p&gt;
&lt;p&gt;쿠키런: 킹덤이 발전하면서 새롭게 등장한 기술적 도전, 리얼타임 게임 서버를 어떻게 구현하였는지 회고하며, 함수형 프로그래밍으로 명료한 코드와 충분한 성능을 동시에 확보하기 위한 기법들을 소개해 주셨습니다.&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 44.99999999999999%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAIAAAC9o5sfAAAACXBIWXMAAAsTAAALEwEAmpwYAAACMElEQVR42gElAtr9AC8kGjAlGy8jGjAmHS8lHiwkHSIbFTs2MmVjYl9eXF9bVmFeWFZVUFNPSE1JQUxHPUxIPkxIPDEtKTIsJgA5LSU7LyY4LCQ5Lic3LCUyKCMhFhBBT1eFv+N9stN3rMpxpcFtn7htnbNrlahvlKRYZGc6LB8+MSQ3Kh4APjIuQzYxPjIvRTg1QTQzPjIwKR0aTFRcltH6icn1hMn1gsv1gM73f8v0fcfvhNL8apWqNygdNysiKR8YAEs8OE9APEo8OE0+Olo+OVNBPjcnJFZWW6DR4YzI0YbH0oTH04C42XCKnnWVpoGbrHWHkUI3MUQ4LzMnIABYR0FhTUhUQz5/ZV7SnpF6X11FMzRdWV6i0POQyOqLyu2Lz/GEvuKCpLqTts2ducaEkppLPzlNQDdDNy8Ab1ZQa1JLRkcviXFjvIyDg15NdlE9bmNmm876ksv2hMPli832jMjvgJisipqopquok5+pVUY/ZFJHWUo/AFtHPFtIPDJIHE9SOGlQTGFAJ3xUNmhkWJjG64u81nyywYG3zY/F55HA35e+2KjJ45ytuWVQRUI1L1pJQAA6LyJGPyotSRkgMxMODhFVQS1bNiZKQi+Wj4uMhoCGf3mDe3OGenKNf3Wbh3utloihiHxzWkojHRlOPDIAZ05GQzkrLiwVKCYWEg8OUjspXywXa09BkIB1i3pvinpwinpvjnxxmYN4nYZ7nYZ6kndqPy8nDg0PGBYTbxfai0R+VT0AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/ee7872cf3d845864d5ab1eba608e9676/33ecc/image_02.webp 360w,
/static/ee7872cf3d845864d5ab1eba608e9676/263a4/image_02.webp 480w,
/static/ee7872cf3d845864d5ab1eba608e9676/0c8fb/image_02.webp 640w,
/static/ee7872cf3d845864d5ab1eba608e9676/521f7/image_02.webp 860w,
/static/ee7872cf3d845864d5ab1eba608e9676/cbd37/image_02.webp 1200w,
/static/ee7872cf3d845864d5ab1eba608e9676/548e7/image_02.webp 2400w,
/static/ee7872cf3d845864d5ab1eba608e9676/d182c/image_02.webp 4032w&quot;
              sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/ee7872cf3d845864d5ab1eba608e9676/53918/image_02.png 360w,
/static/ee7872cf3d845864d5ab1eba608e9676/9aebd/image_02.png 480w,
/static/ee7872cf3d845864d5ab1eba608e9676/0f09e/image_02.png 640w,
/static/ee7872cf3d845864d5ab1eba608e9676/3e3fe/image_02.png 860w,
/static/ee7872cf3d845864d5ab1eba608e9676/64756/image_02.png 1200w,
/static/ee7872cf3d845864d5ab1eba608e9676/98e2c/image_02.png 2400w,
/static/ee7872cf3d845864d5ab1eba608e9676/c2a38/image_02.png 4032w&quot;
            sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/ee7872cf3d845864d5ab1eba608e9676/98e2c/image_02.png&quot;
            alt=&quot;image 02&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;h2 id=&quot;발표-2---unity-에셋-번들-패치로-용량-줄이기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B0%9C%ED%91%9C-2---unity-%EC%97%90%EC%85%8B-%EB%B2%88%EB%93%A4-%ED%8C%A8%EC%B9%98%EB%A1%9C-%EC%9A%A9%EB%9F%89-%EC%A4%84%EC%9D%B4%EA%B8%B0&quot; aria-label=&quot;발표 2   unity 에셋 번들 패치로 용량 줄이기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;발표 2 - &lt;strong&gt;Unity 에셋 번들, 패치로 용량 줄이기&lt;/strong&gt;&lt;/h2&gt;
&lt;picture&gt;
&lt;iframe width=&quot;560&quot; height=&quot;315&quot; style=&quot;width: 100%; height: 100%; aspect-ratio: 16/9;&quot; src=&quot;https://www.youtube.com/embed/CtZ1-n6PbFM?si=pg2KibyWk7Py3Lie&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerpolicy=&quot;strict-origin-when-cross-origin&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;figcaption&gt;
쿠키런: 모험의 탑 서버 엔지니어 배경민님
&lt;/figcaption&gt;
&lt;/picture&gt;
&lt;p&gt;유니티(Unity) 엔진의 에셋 번들(Asset Bundle)은 플랫폼마다 최적화된 리소스 묶음을 빌드하고 배포하기 위한 기능입니다. 그러나 함께 묶인 리소스가 많으면 일부만 수정해도 사용자가 받아야 하는 추가 다운로드 용량이 커질 수 있습니다. 모바일 게임의 데이터 사용량은 사용자에게 큰 부담이 되므로 이를 최소화해야 합니다.&lt;/p&gt;
&lt;p&gt;쿠키런: 모험의 탑에서 이 문제를 해결하기 위해 바이너리 패치를 적용하여 리소스 업데이트 시 다운로드 용량을 크게 줄이고 CDN 비용을 절감한 사례를 공유해 주셨습니다.&lt;/p&gt;
&lt;h2 id=&quot;패널톡--채용-부스&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%8C%A8%EB%84%90%ED%86%A1--%EC%B1%84%EC%9A%A9-%EB%B6%80%EC%8A%A4&quot; aria-label=&quot;패널톡  채용 부스 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;패널톡 &amp;#x26; 채용 부스&lt;/h2&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 44.99999999999999%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAIAAAC9o5sfAAAACXBIWXMAAAsTAAALEwEAmpwYAAACLUlEQVR42gXBWW/SAAAA4P4JY3xdjHNxKUw5BqOBlkJb2tKD9aDlKmVl63RsHG51kghy6NQh7N5Ao24e0eiD8cHEhz3wsBl12ZP+ARN/ht8HMGFvNOShQt4o5qcxWBViK8WCKjI8GeIITI8LSZHLxnkzI+lxJiNSaZ5IsnhWimoiDUSDDjrowgNOkUOLC9qTdq1qlSicZIloSpLSsjCrCbkkb6Sm82nRTMvzmqiJEQr2+JwOAPO7Isik127T55Gjobzzmd74SDcGVKvPbL1Vai01xWFJFskIkRSLy5hfxiCZCFCID51yAQpHajJHBeCSxb8473V+DLpnm92T9e3zzsGfh2uHWZnE4gyOuiegqyNh+2jEOcZOgZwPTCI3gAVDbVvlom7U65ndn6f3v/+r//rbPBk2j7/tn50evKuZasxITKtMaGYaX9LF5Zvp24s5yyq12k2gshTtt+cMPb65VXzz+5jKV1YPX29/6rz6YA6/NmqV/IpprK0WSrpSLZnPdx/tbT/u9db2nu0Ojl4CqubKsuCYbySh0Lk5ZvzyleWy0bi3smhqX953eYItGqnDjWZWic3yeH919ml1sV0tPWhYmzs9wEoiRXIiK6E4AoUhJEaipqHlE5IHnGBxQpPo5Xlu/a4Wo3wibMuFwUQEvlNewB2jhNcOzAiwQvvMNGmVK/v9o1sFE7SNT153gmPXLl645HU7QihEERACOfCwJ4xOeZ2gIhBBN0j53f8Ba9HQa0npM0kAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/88089eb548beb2609453ad7429f912b8/33ecc/image_03.webp 360w,
/static/88089eb548beb2609453ad7429f912b8/263a4/image_03.webp 480w,
/static/88089eb548beb2609453ad7429f912b8/0c8fb/image_03.webp 640w,
/static/88089eb548beb2609453ad7429f912b8/521f7/image_03.webp 860w,
/static/88089eb548beb2609453ad7429f912b8/cbd37/image_03.webp 1200w,
/static/88089eb548beb2609453ad7429f912b8/548e7/image_03.webp 2400w,
/static/88089eb548beb2609453ad7429f912b8/d182c/image_03.webp 4032w&quot;
              sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/88089eb548beb2609453ad7429f912b8/53918/image_03.png 360w,
/static/88089eb548beb2609453ad7429f912b8/9aebd/image_03.png 480w,
/static/88089eb548beb2609453ad7429f912b8/0f09e/image_03.png 640w,
/static/88089eb548beb2609453ad7429f912b8/3e3fe/image_03.png 860w,
/static/88089eb548beb2609453ad7429f912b8/64756/image_03.png 1200w,
/static/88089eb548beb2609453ad7429f912b8/98e2c/image_03.png 2400w,
/static/88089eb548beb2609453ad7429f912b8/c2a38/image_03.png 4032w&quot;
            sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/88089eb548beb2609453ad7429f912b8/98e2c/image_03.png&quot;
            alt=&quot;image 03&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;정규 발표 세션이 마무리된 후, 지식의 깊이를 더해줄 패널톡 시간이 50분간 이어졌습니다. 발표자 전원이 무대에 올라, 참석자분들이 보내주신 질문에 진솔하게 답변하는 시간으로 꾸려졌습니다. ‘장애 발생 시 대응 매뉴얼과 실제 경험’, ‘서버 관련 추천 학습 방법’, ‘게임별 프로그래밍 언어 선택의 기술적 배경’ 등 실무와 맞닿아있는 구체적이고 깊이 있는 질문들을 남겨주셨습니다. 게임 업계뿐만 아니라 다양한 업계의 서버 엔지니어들이 오셔서 더욱 다채로운 시각에서 이야기를 나눌 수 있었습니다.&lt;/p&gt;
&lt;p&gt;발표장 뒤쪽에서는 데브시스터즈와 좀 더 가까워질 수 있는 채용 부스가 운영되었습니다. 이번 채용 부스는 참석자 개개인에게 조금 더 집중하고자, 사전 신청 내용을 기반으로 한 맞춤형 상담으로 진행되었습니다. 참석자분들은 본인이 가장 관심 있는 프로젝트의 담당 서버 개발자와 직접 만나 평소 궁금했던 점들을 편하게 질문할 수 있었습니다. 프로젝트의 기술적 과제와 비전, 팀의 개발 문화와 성장 가능성에 대한 현실적인 이야기부터, 서버 개발자로서의 커리어 패스, 이직과 취업에 대한 전반적인 고민 상담까지 채용 담당자와 현직 개발자가 함께 자리하여 다양한 시각에서 조언드릴 수 있었습니다. 참석자분들은 이번 채용 부스를 통해 데브시스터즈라는 회사와 각 프로젝트에 대한 관심과 이해도를 한층 더 높이는 계기가 되었고, 저희는 지원자분들의 열정과 고민을 가까이에서 느낄 수 있었던 의미 있는 시간이었습니다. 이 자리를 빌려 흔쾌히 참석해 주신 채용팀 분들과 각 프로젝트의 서버 개발자분들께도 감사의 말씀을 드립니다.&lt;/p&gt;
&lt;h2 id=&quot;마치며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0&quot; aria-label=&quot;마치며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;마치며&lt;/h2&gt;
&lt;p&gt;2025년 2월, 기술 공유와 성장을 목표로 선보인 데브시스터즈 엔지니어링 데이가 여러분의 뜨거운 성원 속에 어느덧 세 번째 행사를 성공적으로 마무리했습니다. 무엇보다도 꾸준한 관심과 참여로 함께 해주신 여러분 덕분입니다.&lt;/p&gt;
&lt;p&gt;지난 3번의 행사를 통해 저희는 Infra/SRE, 데이터, 게임 서버 등 데브시스터즈의 기술 분야에 대한 깊이 있는 이야기를 나누었습니다. 매번 세션이 끝난 후 보내주시는 여러 질문과 피드백 덕분에 저희도 더 성장할 수 있었습니다. 이를 바탕으로 2026년에는 더욱 다양한 직군을 모시고 인사이트를 교류할 수 있는 시간을 준비하겠습니다.&lt;/p&gt;
&lt;p&gt;개인적으로는 이번에 AI와 함께 코딩하는 새로운 경험을 한 것이 기억에 많이 남습니다. 앞선 두 번의 행사에서는 행사 소개 페이지를 노션으로 만들어서 공개했었는데요, 최근 사내에서 AI 사용이 활발해짐에 따라 ‘AI로 행사 소개 페이지도 만들 수 있지 않을까?’라는 생각으로 Claude Code를 이용하여 만들어봤습니다. 외부에 보이는 웹 사이트 개발이 아예 처음이다 보니 시작은 다소 막막하고 쉽지 않았습니다. 하지만 주변에서 도와주신 덕분에 금방 기본 형태를 잡을 수 있었고, 그 이후에는 원하는 기능을 조금씩 덧붙이며 현재의 모습이 완성되었습니다. 이번 경험을 통해 히어로 섹션, 워드 브레이크, 푸터 등 다양한 키워드와 함께 웹 개발자분들의 대단함을 조금이나마 이해하게 되었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;행사 소개 페이지: &lt;a href=&quot;https://devsisters.github.io/engineeringday-2025-gameserver/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://devsisters.github.io/engineeringday-2025-gameserver/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 57.22222222222223%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAABYlAAAWJQFJUiTwAAACfUlEQVR42hWS2XLbRhBF+f8fk1TlQSmXlUixYjtFSVxB7Asx2FcCBAmKhMPj9kPXrXmY7tt97qwZRvrhjN+OFFVOGXtkTUt2gfh0J5EqxjuH653+duf0MXG5nGmHK6c657Z64GOaOI8Tzfg/s0QpAj/A6j4Y25ChDMg8EydvCEbIxh+Y3V0G3CnPN45ty3go6cuKWt79+UoVpxRS0V4xqwKTODAwXYuhcCm8HYmlkUUV0TBRXSbG20X0St8f+egLpktD16Y0eS4uM4rIow0tLrU0nDIHpb2zff0PZaxx3uZYyxXezsbQLNwgJEoUfqjIY4VnWRSJQx6F5HuPVNl05Z5aeUTGRhy60mTxTmLo2O+vmIsFLw+f+PzH73x7fuTl6wsPz194+vKVl6dPPP35G98fP1Mon9Rco3ZLzK2JbStc3WEWbJaE2xWDbopusNYr7PWC3jPoDzWdNadcPdKZ3zh3Kafcl5OY1LFD5hhEpkmyt1C+zt5yBYqrodavKH1LJI0S0aWzJk1kjUOHHsRojk8UOgx1xLEMKeTzIdBoU1eGlpzOA1Xd4BnisLN0uqdn+s0blbMRKFsBJKosTsecoVO0bUEmFBPfJnI0KntBpzSmIaKMHLmriWHu2c4X4tDY0fzzN/nujWNqMp0lAtkclf/FoVhxy3WGVCcIfTLfEFceSbKjzWwO8va0NdZqhbvTKAWMxGZLsp0T2xqNoD9XIeNJlyi90CaGrGZRRQuOB58fFwl+oOMEcoI0ErqKyN2xFnC/YnP/aJjVzpIutpiOMY2/pnRWnPq9hFuj0AxyFUsmM1K7IJVz+K7JXtz2dcjtWnGs9yy//0tXxwxVxE9jNyDmcqBCUgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/2c545800dda62a6f1c28f084165a15a5/33ecc/image_04.webp 360w,
/static/2c545800dda62a6f1c28f084165a15a5/263a4/image_04.webp 480w,
/static/2c545800dda62a6f1c28f084165a15a5/0c8fb/image_04.webp 640w,
/static/2c545800dda62a6f1c28f084165a15a5/521f7/image_04.webp 860w,
/static/2c545800dda62a6f1c28f084165a15a5/cbd37/image_04.webp 1200w,
/static/2c545800dda62a6f1c28f084165a15a5/548e7/image_04.webp 2400w,
/static/2c545800dda62a6f1c28f084165a15a5/185c8/image_04.webp 2984w&quot;
              sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/2c545800dda62a6f1c28f084165a15a5/53918/image_04.png 360w,
/static/2c545800dda62a6f1c28f084165a15a5/9aebd/image_04.png 480w,
/static/2c545800dda62a6f1c28f084165a15a5/0f09e/image_04.png 640w,
/static/2c545800dda62a6f1c28f084165a15a5/3e3fe/image_04.png 860w,
/static/2c545800dda62a6f1c28f084165a15a5/64756/image_04.png 1200w,
/static/2c545800dda62a6f1c28f084165a15a5/98e2c/image_04.png 2400w,
/static/2c545800dda62a6f1c28f084165a15a5/a14bf/image_04.png 2984w&quot;
            sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/2c545800dda62a6f1c28f084165a15a5/98e2c/image_04.png&quot;
            alt=&quot;image 04&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;저의 작은 경험이 바로 기술과 인간이 점점 더 가까워지고 있음을 보여주는 예시가 아닐까 싶습니다. 많은 것이 급변하는 이 시대를 살아가고 있는 모든 분의 성장을 응원하며, 앞으로도 데브시스터즈 엔지니어링 데이와 기술직군에 많은 관심 부탁드립니다. 감사합니다.&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 44.99999999999999%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAIAAAC9o5sfAAAACXBIWXMAAAsTAAALEwEAmpwYAAACMElEQVR42gElAtr9AF1aUzs3OkE7OVdNSFVOTHlwa3dsZmRaUlZLQUM5MW9mXkM5MEU7MlBGPXlsZXdrZTs2Mk1HQkY3Mk47NABoZVooKS8pKCxmYlZbVU1ORz5cUktnYFhjY19kaWh0e39qbGxeVk1nXFJdUEdHOzUVFx1ELyqgY0qkZ08AKCksGR8iNj9Bc4aMg4+Ug4qMgYaNg4mSkLbChLS/frXCjbm9hpeahISGe4CEXUxGSTgzXkY90ph72aiRAA4UHA8SFiMvOYajt6a/zKG4xKOxs6O1vaXE2JnB15PH3p7I2K/D0rPA0rLH15ifppWanIuSk0hRUpaGfQBBLClzRC4oLDFgbnZjc253jY2Ef2NpeGCqrLCvtrq2ubrDvbq+rKKroJekl46ah32bk498gYIpMDBoZF8AKB4fTzAoJCIkLjM3REM9ZGJaXVRRWVNQV1BKYVhSZFpUZVhRaVpPaV9SZ1lKVEI3bV1XXkQ7RDIvZUpBAEpDPDYxMjo2LzIoI0MyKj8tJUExKWRfUl1aUnVvamdiYF1ZWG1kX0EqHUApGj43NiooKl1QSEk7NF5IPQBJNiVWUURCPz9FPztEPjtgVVGWf3CfiXuQfHKymIqgiX2Kd26hiHuOdmVdQS1HKBZRRTlrVUBNNCBkUT4AMyIXWUEoX1RIQDk1SUA3hW9fiHJhkX1tqpB/p418pIp6moN0i3ZnkHpneWNQXUk5UUU5JRoQKB4WaltJw9vQegf6A0oAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/b0643b9a3884daa58dd51d2ef596a788/33ecc/image_05.webp 360w,
/static/b0643b9a3884daa58dd51d2ef596a788/263a4/image_05.webp 480w,
/static/b0643b9a3884daa58dd51d2ef596a788/0c8fb/image_05.webp 640w,
/static/b0643b9a3884daa58dd51d2ef596a788/521f7/image_05.webp 860w,
/static/b0643b9a3884daa58dd51d2ef596a788/cbd37/image_05.webp 1200w,
/static/b0643b9a3884daa58dd51d2ef596a788/548e7/image_05.webp 2400w,
/static/b0643b9a3884daa58dd51d2ef596a788/d182c/image_05.webp 4032w&quot;
              sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/b0643b9a3884daa58dd51d2ef596a788/53918/image_05.png 360w,
/static/b0643b9a3884daa58dd51d2ef596a788/9aebd/image_05.png 480w,
/static/b0643b9a3884daa58dd51d2ef596a788/0f09e/image_05.png 640w,
/static/b0643b9a3884daa58dd51d2ef596a788/3e3fe/image_05.png 860w,
/static/b0643b9a3884daa58dd51d2ef596a788/64756/image_05.png 1200w,
/static/b0643b9a3884daa58dd51d2ef596a788/98e2c/image_05.png 2400w,
/static/b0643b9a3884daa58dd51d2ef596a788/c2a38/image_05.png 4032w&quot;
            sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/b0643b9a3884daa58dd51d2ef596a788/98e2c/image_05.png&quot;
            alt=&quot;image 05&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[게임 회사에서 클라이언트 프로그래머가 하는 일]]></title><description><![CDATA[게임 회사에서 클라이언트 프로그래머가 하는 일을 게임 업데이트 루틴과 함께 소개합니다.]]></description><link>https://tech.devsisters.com/posts/what-do-client-programmers-do</link><guid isPermaLink="false">https://tech.devsisters.com/posts/what-do-client-programmers-do</guid><pubDate>Tue, 12 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1 id=&quot;들어가며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%93%A4%EC%96%B4%EA%B0%80%EB%A9%B0&quot; aria-label=&quot;들어가며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;들어가며&lt;/h1&gt;
&lt;p&gt;안녕하세요. 저는 쿠키런 킹덤의 4년차 클라이언트 프로그래머로 일하고 있는 박지선입니다.&lt;/p&gt;
&lt;p&gt;제가 취업준비생 시절, 클라이언트 프로그래머가 무슨 일을 하는지 구체적으로 잘 알지 못했습니다.&lt;/p&gt;
&lt;p&gt;검색 엔진에 찾아봐도,&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“서버 프로그래머는 유저 데이터를 저장하는 서버를,
클라이언트 프로그래머는 눈에 보이는 게임을 구현하는 일을 합니다.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;와 같은 간단한 정의만 흔히 접할 수 있었죠.&lt;/p&gt;
&lt;p&gt;하지만 실제 현업에서는 클라이언트 프로그래머가 하는 일은 이보다 훨씬 다양합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;눈에 보이는 게임을 구현하는 것 뿐만 아니라,&lt;/li&gt;
&lt;li&gt;다른 직군과 누구보다 긴밀히 협업하고,&lt;/li&gt;
&lt;li&gt;툴을 개발하거나,&lt;/li&gt;
&lt;li&gt;리소스를 관리하고,&lt;/li&gt;
&lt;li&gt;게임 엔진을 다루는 등&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;게임 개발 전반에 걸쳐 다양한 업무를 맡고 있습니다.&lt;/p&gt;
&lt;p&gt;그래서 이 글에서는 클라이언트 프로그래머를 꿈꾸며 공부하는 취업 준비생 분들에게 조금이라도 도움이 되기를 바라는 마음으로, 제가 실제로 구현한 컨텐츠를 예시로 들어서 컨텐츠 개발 흐름을 따라 클라이언트 프로그래머가
어떤 일을 하는지 구체적으로 이야기해보려 합니다.&lt;/p&gt;
&lt;p&gt;물론 회사나 팀마다 개발 문화와 방식이 다를 수 있기 때문에 이 글에서 소개하는 내용이 모든 게임 회사에 똑같이 적용되지는 않을 수 있습니다.
같은 팀 내에서도 쿠키 제작이냐, 전투 컨텐츠 제작이냐, 왕국 컨텐츠 제작이냐에 따라서 달라질 수 있습니다. 이 점 참고해서 하나의 실무 예시로 읽어주시면 감사하겠습니다.&lt;/p&gt;
&lt;br&gt;
&lt;h1 id=&quot;게임-회사의-조직-구조와-직무-소개&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%8C%EC%9E%84-%ED%9A%8C%EC%82%AC%EC%9D%98-%EC%A1%B0%EC%A7%81-%EA%B5%AC%EC%A1%B0%EC%99%80-%EC%A7%81%EB%AC%B4-%EC%86%8C%EA%B0%9C&quot; aria-label=&quot;게임 회사의 조직 구조와 직무 소개 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;게임 회사의 조직 구조와 직무 소개&lt;/h1&gt;
&lt;p&gt;이야기를 하기에 앞서, 게임 회사에 어떤 직군이 있는지 잘 모르시는 분들을 위해 간단한 설명을 하고 넘어가겠습니다.
지금 설명드리는 직군들이 앞으로 나올 실무 예시에 등장하게 됩니다.
이 정도는 안다! 하시는 분들은 다음 챕터로 바로 넘어가시면 됩니다.&lt;/p&gt;
&lt;p&gt;게임 개발팀에는 다양한 직군이 속해있습니다.
회사나 프로젝트에 따라 명칭이나 세부 구성이 다르겠지만, 보통 다음과 같은 직군으로 나뉩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PD (Project Director) : 프로젝트 전체의 방향성과 퀄리티를 총괄합니다.&lt;/li&gt;
&lt;li&gt;PM (Project Manager) : 프로젝트의 개발 관리와 출시, 운영에 이르는 모든 매니지먼트를 담당하며, 개발팀 외부 조직들과의 커뮤니케이션도 함께 맡고 있습니다.&lt;/li&gt;
&lt;li&gt;서버 프로그래머 : 게임의 백엔드를 구축하고, 유저 데이터 관리 및 서버-클라이언트 간 통신을 담당합니다.&lt;/li&gt;
&lt;li&gt;클라이언트 프로그래머 : 게임 구조를 설계하고, 여러 직군이 만든 리소스와 데이터를 게임 속에서 자연스럽게 연결해서 게임이 실제로 동작하도록 구현합니다.&lt;/li&gt;
&lt;li&gt;스토리 : 게임의 전반적인 스토리부터 캐릭터를 포함한 서사 전반을 구성합니다.
쿠키런 킹덤에서는 데코 아이템, 패키지 상품, 이벤트 문구 등 게임 곳곳에 들어가는 모든 텍스트가 스토리 팀의 손을 거칩니다.&lt;/li&gt;
&lt;li&gt;아트 : 스토리에 어울리는 컨셉과 캐릭터를 디자인하고, 게임에 사용되는 리소스를 제작합니다.
쿠키런 킹덤에서는 배경 아트, 캐릭터 아트, 일러스트 아트 세 팀으로 구성되어 있습니다.&lt;/li&gt;
&lt;li&gt;UIUX : 아트의 리소스와 기획서를 바탕으로 인터페이스와 유저 경험을 설계합니다.
프로젝트 내의 리소스를 관리하고 조립해서 &lt;strong&gt;프리팹&lt;/strong&gt;을 제작하기도 합니다.
&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;프리팹(prefab)&lt;/strong&gt; 이란?&lt;/p&gt;
&lt;p&gt;Unity에서 재사용 가능한 오브젝트 템플릿을 의미합니다. 일관된 UI 구성이나 오브젝트 배치에 유용하게 사용됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;이펙트 : 전투 연출이나 UI 효과 등 시각적인 효과를 제작합니다.&lt;/li&gt;
&lt;li&gt;모션 : 캐릭터나 오브젝트에 생동감 있는 모션을 적용합니다.&lt;/li&gt;
&lt;li&gt;기획 : 게임의 전반적인 시스템, 컨텐츠, 밸런스를 설계합니다.
쿠키런 킹덤에서는 왕국 컨텐츠 기획, 전투 컨텐츠 기획, 시스템 기획, 캐릭터 기획 네 팀으로 구성되어 있습니다.&lt;/li&gt;
&lt;li&gt;QA (Quality Assurance) : 기획 의도에 맞게 기능이 동작하는지 테스트하고, 유저의 실제 경험을 바탕으로 피드백을 제공합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이처럼 하나의 게임을 만들기 위해 다양한 직군이 함께 긴밀히 협업하며 프로젝트를 완성해 나갑니다.&lt;/p&gt;
&lt;br&gt;
&lt;h1 id=&quot;업데이트와-업무-루틴&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8%EC%99%80-%EC%97%85%EB%AC%B4-%EB%A3%A8%ED%8B%B4&quot; aria-label=&quot;업데이트와 업무 루틴 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;업데이트와 업무 루틴&lt;/h1&gt;
&lt;p&gt;클라이언트 프로그래머의 업무를 어떻게 더 잘 설명할 수 있을까 고민하다가, 실무를 예시로 설명드리면 좋을 것 같아서 게임 업데이트와 업무 루틴을 따라 설명드리려 합니다.&lt;/p&gt;
&lt;video controls width=&quot;640&quot;  aria-label=&quot;점핑 플랫폼 미니게임 플레이 영상&quot;&gt;
  &lt;source src=&quot;/d9ffc88a7c6d55c0597cf40d038ad4b4/jumping-platform.mp4&quot; type=&quot;video/mp4&quot;&gt;
  점핑 플랫폼 미니게임 플레이 영상입니다. 영상을 볼 수 없는 경우, 쿠키가 플랫폼을 점프하며 장애물을 피하는 게임 플레이를 상상해 주세요.
&lt;/video&gt;
&lt;p&gt;쿠키런 킹덤을 꾸준히 플레이 해보셨다면 이 미니게임을 아실 수도 있을 것 같습니다.
2025년 6월 4일에 업데이트 되었던 ‘정글전사 쿠키의 베리베리 높이높이!’ 로 제가 작업했던 미니게임 컨텐츠입니다.&lt;/p&gt;
&lt;p&gt;이 컨텐츠가 어떤 과정을 통해 개발되었는지를 한 번 돌아보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;참고로 ‘점핑 플랫폼’이라는 명칭이 계속 등장할텐데, 이는 임의로 작업자들끼리 정한 개발 명칭입니다.
컨텐츠의 이름은 개발 후반부에 스토리 팀에서 IP 특유의 톤앤매너와 함께 시스템상의 직관성이 전달될 수 있게끔 작성해주십니다.&lt;/p&gt;
&lt;h2 id=&quot;-컨텐츠-결정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%BB%A8%ED%85%90%EC%B8%A0-%EA%B2%B0%EC%A0%95&quot; aria-label=&quot; 컨텐츠 결정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;📋 컨텐츠 결정&lt;/h2&gt;
&lt;p&gt;쿠키런 킹덤은 PD와 스토리팀, 아트팀을 중심으로 1년 단위로 대략적인 업데이트 플랜을 정해두는데요, 그 플랜에 맞춰서 이번 업데이트에 어떤 스토리가 들어갈 것인지, 어떤 컨텐츠를 넣을 것인지를 결정합니다.&lt;/p&gt;
&lt;p&gt;이번 업데이트에 들어가는 비스트 에피소드 스토리에서 정글전사 쿠키가 나무에 올라 베리를 따는 장면이 등장했고 그걸 토대로 미니게임으로 만들기로 결정하였습니다.&lt;/p&gt;
&lt;h2 id=&quot;-상위방향성--상세기획-회의&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%83%81%EC%9C%84%EB%B0%A9%ED%96%A5%EC%84%B1--%EC%83%81%EC%84%B8%EA%B8%B0%ED%9A%8D-%ED%9A%8C%EC%9D%98&quot; aria-label=&quot; 상위방향성  상세기획 회의 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;🤝🏻 상위방향성 &amp;#x26; 상세기획 회의&lt;/h2&gt;
&lt;p&gt;이 회의들은 PD님과 기획팀이 주로 진행합니다.
컨텐츠에서 어떤 경험을 줄 지에 대한 방향성, 미니게임의 구성을 논의하는 회의입니다.&lt;/p&gt;
&lt;p&gt;이 단계까지는 클라이언트 프로그래머의 개입이 크게 없습니다.
이런 컨텐츠가 들어갈 예정이구나 정도만 파악해서 담당자를 배정하거나 구현 가능성을 확인합니다.&lt;/p&gt;
&lt;p&gt;여기서 말하는 구현 가능성이란 제작할 일정이 충분한지, 기술적으로 가능한지 체크하는 것을 말합니다.&lt;/p&gt;
&lt;h2 id=&quot;-킥오프-회의&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%ED%82%A5%EC%98%A4%ED%94%84-%ED%9A%8C%EC%9D%98&quot; aria-label=&quot; 킥오프 회의 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;🏃🏻 킥오프 회의&lt;/h2&gt;
&lt;p&gt;앞에서 방향성을 정한 후 기획팀에서는 킥오프 기획서를 작성합니다.
킥오프 회의는 실무자들을 위한 회의로, 이 회의를 진행한 후 본격적으로 작업을 시작합니다.
회의에서는 기획자를 중심으로 서버&amp;#x26;클라이언트 프로그래머, 아트, UIUX, 이펙트, 모션팀 모두가 참석해서 컨텐츠 구현을 위해 구체적인 스펙을 체크하고, 회의가 끝나면 PM팀에서 각 직군별로 작업 일정을 확인해서
조율해주십니다.&lt;/p&gt;
&lt;p&gt;킥오프 회의에서의 클라이언트 프로그래머의 역할은 매우 중요합니다.&lt;/p&gt;
&lt;p&gt;클라이언트 프로그래머는 데이터, 서버, 리소스 작업이 모두 완료된 이후에 건담을 조립하는 것처럼 작업을 하는데, 이 때 부품이 맞지 않아 조립을 못하는 일이 생기면 안 되겠죠.
그래서 미리 설계에 참여해서 리드해야 이후 작업에 차질이 없습니다.&lt;/p&gt;
&lt;p&gt;다음과 같이 원하는 형태로 리소스를 제작해주시게끔 작업 방향성을 제시합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;아트 ⇒ “이 정도 사이즈의 이미지 리소스를 반복해서 사용할 예정이라 이렇게 제작 부탁드립니다.”&lt;/p&gt;
&lt;p&gt;UIUX ⇒ “이런 구조로 프리팹 제작 부탁드립니다.”&lt;/p&gt;
&lt;p&gt;이펙트 ⇒ “이 오브젝트는 Canvas가 아니라 WorldSpace에 들어갈 예정이니, UI 이펙트가 아닌 일반 이펙트로 제작 부탁드립니다.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;-데이터-구조-회의&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B5%AC%EC%A1%B0-%ED%9A%8C%EC%9D%98&quot; aria-label=&quot; 데이터 구조 회의 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;💽 데이터 구조 회의&lt;/h2&gt;
&lt;p&gt;쿠키런 킹덤은 게임 데이터를 위해 엑셀을 사용합니다.
서버와 클라이언트가 모두 엑셀 데이터를 가져다 사용하기 때문에 어떤 식으로 엑셀에 데이터를 입력할 것인지 논의가 필요합니다.&lt;/p&gt;
&lt;p&gt;새로운 컨텐츠를 위해 데이터 구조를 새로 설계해야 하는 상황에 주로 진행하는 회의입니다.&lt;/p&gt;
&lt;h2 id=&quot;️-툴-개발&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EF%B8%8F-%ED%88%B4-%EA%B0%9C%EB%B0%9C&quot; aria-label=&quot;️ 툴 개발 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;🛠️ 툴 개발&lt;/h2&gt;
&lt;p&gt;본격적인 점핑 플랫폼 게임 개발을 시작하기에 앞서, 기획팀에게 맵 툴이 필요한 상황이 되어 툴을 개발하게 되었습니다.&lt;/p&gt;
&lt;p&gt;이 툴은 정해진 규칙에 따라 맵 데이터를 랜덤으로 생성하고, 결과를 유니티 에디터를 통해 가시적으로 확인하며 플랫폼의 위치나 타입을 원하는 대로 조정할 수 있도록 만들어졌습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1460px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 33.888888888888886%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAABYlAAAWJQFJUiTwAAABXElEQVR42o2S206DQBCGeQhTqi204aKlQApYDuUgRSpQDqUYNWobjRe+jQ/9uztJe+GVF192d2Z3Dv+sYJomlssldF3HYrGAYRgEP3M0TSN0xmg0gqqqiKIInufB932sGbe+i6Jo4TlrCHEcIwxDcq5WKwRBcLnM947jkN11PYjjMXTLxuFwQFVVRN11eD1+oXs7IYrvIPBswTpAGIT0UFEUqkSSJIxZAFmWITGU6RTajYjMMtA2DfI8R7Hb4fTyib57QlnX8FwXQnafYbPZIN2ktLrMaNv2BYtLwnAsE8fJFX6sKRoWsCgKlGWJet8jb1tU+z11JnDHdruljLwFfvlMzbJyPydnj6/lCQymd8MCUIVnX/6AggdkEgnqXMVsNsN8Pqf1L9zO4cO4Hg6p6r7vsWPt8gLKusLj8zuOH99wQxZQFEX8l8FgQBNPkoQGyfUPmUxxmiFNUvotv9Jk438bcfeFAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/369a1e0bb79f0946d611367f383524ab/33ecc/img01.webp 360w,
/static/369a1e0bb79f0946d611367f383524ab/263a4/img01.webp 480w,
/static/369a1e0bb79f0946d611367f383524ab/0c8fb/img01.webp 640w,
/static/369a1e0bb79f0946d611367f383524ab/521f7/img01.webp 860w,
/static/369a1e0bb79f0946d611367f383524ab/cbd37/img01.webp 1200w,
/static/369a1e0bb79f0946d611367f383524ab/a69b9/img01.webp 1460w&quot;
              sizes=&quot;(max-width: 1460px) 100vw, 1460px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/369a1e0bb79f0946d611367f383524ab/53918/img01.png 360w,
/static/369a1e0bb79f0946d611367f383524ab/9aebd/img01.png 480w,
/static/369a1e0bb79f0946d611367f383524ab/0f09e/img01.png 640w,
/static/369a1e0bb79f0946d611367f383524ab/3e3fe/img01.png 860w,
/static/369a1e0bb79f0946d611367f383524ab/64756/img01.png 1200w,
/static/369a1e0bb79f0946d611367f383524ab/a2906/img01.png 1460w&quot;
            sizes=&quot;(max-width: 1460px) 100vw, 1460px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/369a1e0bb79f0946d611367f383524ab/a2906/img01.png&quot;
            alt=&quot;원하는대로 확률을 입력하면 그대로 맵을 찍어줍니다.&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;원하는대로 확률을 입력하면 그대로 맵을 찍어줍니다.&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;기존에는 데이터를 하나하나 입력해가며 맵을 완성해야 했지만, 툴을 통해 클릭 한 번으로 랜덤 생성이 가능해졌고, 원하는 확률을 실시간으로 조정해 바로 결과를 확인할 수 있게 되었습니다.&lt;/p&gt;
&lt;p&gt;툴 개발은 이렇게 단순한 편의 기능을 넘어, 프로젝트 전체의 개발 효율과 안정성을 높이는 클라이언트 프로그래머의 주요 업무 중 하나입니다.&lt;/p&gt;
&lt;p&gt;지금은 점핑 플랫폼 게임을 위한 맵 툴 하나만을 예시로 들었지만, 쿠키런 킹덤에서는 이 외에도 수많은 툴들을 제작 및 사용하고 있습니다.&lt;/p&gt;
&lt;p&gt;툴이 가져다주는 이점은 다음과 같습니다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;반복 작업 자동화: 수동으로 해야하는 반복 작업을 클릭 몇 번으로 처리할 수 있게 만들어줍니다.&lt;/li&gt;
&lt;li&gt;실수 방지: 사람이 직접 작업하다 보면 실수가 생기기 마련인데, 툴을 통해 데이터나 리소스를 다룰 때 발생할 수 있는 실수를 미리 탐지할 수 있습니다.&lt;/li&gt;
&lt;li&gt;협업 효율 향상: 작업자가 다른 직군에게 요청하지 않고도 스스로 테스트해볼 수 있어, 반복적인 커뮤니케이션 비용이 줄어듭니다.&lt;/li&gt;
&lt;li&gt;밸런싱 용이: 데이터를 빠르게 수정하고 테스트할 수 있어, 밸런싱과 퀄리티 향상에 큰 도움이 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;️-작업-시작&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EF%B8%8F-%EC%9E%91%EC%97%85-%EC%8B%9C%EC%9E%91&quot; aria-label=&quot;️ 작업 시작 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;🏗️ 작업 시작&lt;/h2&gt;
&lt;p&gt;이제 본격적인 개발이 시작되었습니다.&lt;/p&gt;
&lt;p&gt;숙련된 클라이언트 프로그래머라면, 서버나 데이터 작업이 이미 끝났다고 가정하고! 리소스도 다 준비됐다고 생각하고! 미리 코드를 구상해보며 작업을 진행할 수도 있지만, 보통은 다음과 같은 순서를 거칩니다.&lt;/p&gt;
&lt;p&gt;직군별로 독립적으로 진행되는 작업이 있고, 아닌 작업이 있습니다. 간단하게 정리해보면 이렇습니다.&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1266px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 32.22222222222222%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAABYlAAAWJQFJUiTwAAAA1klEQVR42m2RbQuDMAyE/f//0H0S1Cq+1apVVDTrk63gYIX06qW5XGpirZW2bQWM0fe94jAMGuM4KsKD3O+67ieaptGahMO+70qURSFVVcmyLPpNsXNOijxXMXjivm+5rkuRYKFBo4SN5DzP6mSaJk0iBLdtm3b2QWhdVz1nWSbGGDFlKXUwwNmFWpwmbMdxqLr7joYYvA3nLvB1XUsbHOOOPAI0pyEGQHIqGEdmJAISQQpAnuCVpjo2Dr338m+d5/kZOYo8fwouwPjgsRkF8f4z4OKbvwEQt8g9DucrpQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/8ca4c1e6293fdfc14f47938a3221b156/33ecc/img02.webp 360w,
/static/8ca4c1e6293fdfc14f47938a3221b156/263a4/img02.webp 480w,
/static/8ca4c1e6293fdfc14f47938a3221b156/0c8fb/img02.webp 640w,
/static/8ca4c1e6293fdfc14f47938a3221b156/521f7/img02.webp 860w,
/static/8ca4c1e6293fdfc14f47938a3221b156/cbd37/img02.webp 1200w,
/static/8ca4c1e6293fdfc14f47938a3221b156/f80fb/img02.webp 1266w&quot;
              sizes=&quot;(max-width: 1266px) 100vw, 1266px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/8ca4c1e6293fdfc14f47938a3221b156/53918/img02.png 360w,
/static/8ca4c1e6293fdfc14f47938a3221b156/9aebd/img02.png 480w,
/static/8ca4c1e6293fdfc14f47938a3221b156/0f09e/img02.png 640w,
/static/8ca4c1e6293fdfc14f47938a3221b156/3e3fe/img02.png 860w,
/static/8ca4c1e6293fdfc14f47938a3221b156/64756/img02.png 1200w,
/static/8ca4c1e6293fdfc14f47938a3221b156/03d4c/img02.png 1266w&quot;
            sizes=&quot;(max-width: 1266px) 100vw, 1266px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/8ca4c1e6293fdfc14f47938a3221b156/03d4c/img02.png&quot;
            alt=&quot;img02&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;클라이언트 프로그래머는 이 과정에서 모든 직군과 끊임없이 소통하며 작업을 조율합니다.&lt;/p&gt;
&lt;p&gt;각 직군의 작업이 완료되면, 그 결과물을 모아서 실제 게임 안에서 동작하도록 구현하는 것이 클라이언트 프로그래머의 역할입니다.&lt;/p&gt;
&lt;h3 id=&quot;1-데이터--서버-작업&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EB%8D%B0%EC%9D%B4%ED%84%B0--%EC%84%9C%EB%B2%84-%EC%9E%91%EC%97%85&quot; aria-label=&quot;1 데이터  서버 작업 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;1. 데이터 &amp;#x26; 서버 작업&lt;/h3&gt;
&lt;p&gt;작업이 시작되면 일단 기획팀에서 가데이터 작업을 올려줍니다.&lt;/p&gt;
&lt;p&gt;가데이터 작업이 완료되면 서버에서 작업을 시작해서, 작업 내용을 반영한 서버를 띄워줍니다.
이후 클라이언트 프로그래머는 서버에 접속해서 서버 작업에 문제가 없는지 확인합니다.&lt;/p&gt;
&lt;h3 id=&quot;2-리소스-작업&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%EB%A6%AC%EC%86%8C%EC%8A%A4-%EC%9E%91%EC%97%85&quot; aria-label=&quot;2 리소스 작업 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;2. 리소스 작업&lt;/h3&gt;
&lt;p&gt;리소스 작업은 데이터 및 서버 작업과는 &lt;strong&gt;독립적으로&lt;/strong&gt; 진행됩니다.&lt;/p&gt;
&lt;p&gt;먼저 &lt;strong&gt;아트팀&lt;/strong&gt;에서 게임에 들어갈 리소스를 제작합니다.&lt;/p&gt;
&lt;p&gt;이후 &lt;strong&gt;UIUX팀&lt;/strong&gt;이 해당 리소스와 기획서를 바탕으로 UI/UX를 설계하고, 실제 Unity에서 사용할 수 있는 &lt;b&gt;프리팹(prefab)&lt;/b&gt;을 조립합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1450px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 27.500000000000004%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAABYlAAAWJQFJUiTwAAABw0lEQVR42g2QW09SAQCA+TW9tXK6MlpbubnRfKj1Wg/1gJvVclnkBQvwwsFIuXPkjA4BCnE/BXjk0inN5SVFa5brobFWWw+5jF56/OIPfPu+Tzc2HkYwP2TCPEDfiI0B0yLD0zMY3TfoN4vcnFI41nGGc116xsZdPPblUYMaolXCF5JZCFWpOApIUzJ2VwSdKL9jJ/aMr7EAd8Qyly1hRvxBxKVJbNIrxGST46cucLbnCkrtD+nyAY30Po3iN0obLXZX/vFLPWK1fohS/43ukjeDMDeBGpihz+XF5NEw2kK4MxYiUp41qYih8zT9PQZyy0fkK00+JN+znw1R2vzLi/Q225F1tGqTYtaJ7sl8lPu2IW6ZrVjmqswPZ5GkIJHaJA5/Bbd4QG9nN8bzBspai+dLX9DkHT4WPpFqw7PJTbbiW9Qr34m93EM36nvA69hdokE7tz0O8vkN0nmF0pqTQELBqaicbCfre69SUwskcgkK0Tek4sssKrs01B/8bJu/1Q5Z/dxCd/2ahZDpEYowRCw3SkT24nPbcYqDCLNmBl336DjRRbf+IlZhmln/U0r+FbyCTHhhnagnQ9yRIhystX8X+Q94RU619c1f9wAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/80f57046062017d9946b24997c73c506/33ecc/img03.webp 360w,
/static/80f57046062017d9946b24997c73c506/263a4/img03.webp 480w,
/static/80f57046062017d9946b24997c73c506/0c8fb/img03.webp 640w,
/static/80f57046062017d9946b24997c73c506/521f7/img03.webp 860w,
/static/80f57046062017d9946b24997c73c506/cbd37/img03.webp 1200w,
/static/80f57046062017d9946b24997c73c506/5a38c/img03.webp 1450w&quot;
              sizes=&quot;(max-width: 1450px) 100vw, 1450px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/80f57046062017d9946b24997c73c506/53918/img03.png 360w,
/static/80f57046062017d9946b24997c73c506/9aebd/img03.png 480w,
/static/80f57046062017d9946b24997c73c506/0f09e/img03.png 640w,
/static/80f57046062017d9946b24997c73c506/3e3fe/img03.png 860w,
/static/80f57046062017d9946b24997c73c506/64756/img03.png 1200w,
/static/80f57046062017d9946b24997c73c506/74cb2/img03.png 1450w&quot;
            sizes=&quot;(max-width: 1450px) 100vw, 1450px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/80f57046062017d9946b24997c73c506/74cb2/img03.png&quot;
            alt=&quot;왼쪽이 기획서, 오른쪽이 프리팹 시안&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;왼쪽이 기획서, 오른쪽이 프리팹 시안&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;br&gt;
프리팹 제작이 완료되면 이펙트팀이 이펙트를 제작하여 시각적 효과를 추가하고,
&lt;p&gt;&lt;img src=&quot;/f1371211a1fba7c9efc504e90b47d130/video01.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;br&gt;
모션 팀에서는 아트에서 작업해준 리소스에 움직임을 부여하여 생동감을 더해줍니다.
&lt;p&gt;&lt;img src=&quot;/37a0d94e0d68524678215fd1bf93381d/video02.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;br&gt;
작업 중간중간, 각 리소스가 기획 의도나 설계대로 잘 제작되고 있는지 확인하는 절차도 중요합니다.
&lt;figure style=&quot;max-width: 640px; margin: 0 auto;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1060px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 146.66666666666669%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAdCAYAAACqhkzFAAAACXBIWXMAABYlAAAWJQFJUiTwAAAEoUlEQVR42p2W60/TZxTH+9fs7d7MLS6bui1ZTJyZkGy6zYE6r0NlitDSC723lDkR5uIr50iW3UBakMuAF7qZLNtihsIKUrC0WmiBXmjphdLLZ8/vB5IpsahNvjnnPL/n+T7nnOc856libm4Or3eaYDCE3+9nZmaG0GyQsD9AciFKLBIlHIkQjUaJCJnP5yn1UywlErRdvYzJoMJkMaEzGLHYGxmbmCCTyVAsFh9bUKT4f2ODrojH44yMDOMevYNnaop7Hg/jEx6SqdQTC4qrYF1s0GVCV2cH6noNNocDh9VM8/lm7GYzPb3XkYILJsPcC0+TyecoCDtfKFAoCq0gmFbWZE7I3CqzwtnRjkapxmbRYjc0YDc20Wiy4HK5UF1Rs+3sDl6r2kql7SM+bNjLgcunmA8uEh0qEh7IE/mtQFTIhd4cy5Eiir6+HnR6Ld+2XqTlq/M0GLWCvI7+fhcB331Gbv/BxMg/uIf/5Pbfv/PjjQ5SySzph5AMQErIlJDJB8L7jPDwWnsbNWcrqVXrUGvUKFVK1Lpz9PR0r+XoiUMR9pNjj+VwYHCApi8dXLrUSktrM19faqHpfBM3b9yUJ+RzIneiVGQU8nLZyKQibRsg9lFIi5LpLKn0ikB2XS+U8KJkHf579w7Vhw9Rc+K4wFFqq07ItnvkrjyhUCg8H2E6ucSMz8Osf4oZAVn6JkmnkhsL+VkIn/ah+KIhx1MZoskM8XSGWCpNVNiJdJp4ZlkgQ3o5y8rKCtls9tkI74fCeKMpIukE90UjGJ0NEQgv4J2fY2LGy0PxPbwwT0g0ESmfj8rmaVDMzs4yPT2NZ3KKQCAgjr9ATi6RAi8StaL14gXKyspxOBppa2sjFAqJVhaUIbWrZDJJLBZjcXGRtJQK0UyWlpZkSOOJeEKeI9kJ0bkUd4aHGRoakvug1+tlcmoSt9uNxzPJhGhho6OjjI+PMzY2JsY8BEWvlAikxRKhTCr0R5soJEPyyufz4ff5mZ+f54H/gRibQ0qH5Okjr3Li1mx2+gqpE0s5lBZKYclYCzEWk/Q48cW4TCptvry8XPJeK653d6NVa7BbbVjNlnVYTGasooNb7XqsVqPoQFZ0Gi2DA4OrZE+5QQpnpxNVnRJ9gwFNvQ69To9Bgl6HQWmn+o1uvtjbIp6HBjFPRV9vX8krqehyOqmtqedyqxL3XyfE26LGqDdjNhtQKfUcfukmte+1Y7Ob0Cp1/NrXvwmhy8m5GhXftGgYvnWaRkFUcaaCXWd38nb1Nt45uZ2PasspO/0+O0/uoqPnWmlCZ2eXCKUem+kMdfWfYTLWcPrUMfZX7KayopwDB8oE9vBp5R52VLxF+/XO0oS9rl+oVao4qTyOQ/UJqnrxnBrEgZisqzBasQjYzXbUynr6N8vhYNcPuJr30G7eys91W+i6UI5JHJBBL9CgX4dR2GqVINwshz85u0SJHOGhawu+nu1M3rLT6LCh1WoxCRLjGkziD8AzETquWnh1/8t88Pkr7K56nd3H3+XNg9vYW/cxZr3p+Qm//+4KRyr2UXXsCKeOHqL62CEqj+7jYM1hLAaTCF3/XIT/ATSk0TYPaftYAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/8526e87b6e1b554c61d8ddc9d7190848/33ecc/img04.webp 360w,
/static/8526e87b6e1b554c61d8ddc9d7190848/263a4/img04.webp 480w,
/static/8526e87b6e1b554c61d8ddc9d7190848/0c8fb/img04.webp 640w,
/static/8526e87b6e1b554c61d8ddc9d7190848/521f7/img04.webp 860w,
/static/8526e87b6e1b554c61d8ddc9d7190848/668f0/img04.webp 1060w&quot; sizes=&quot;(max-width: 1060px) 100vw, 1060px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/8526e87b6e1b554c61d8ddc9d7190848/53918/img04.png 360w,
/static/8526e87b6e1b554c61d8ddc9d7190848/9aebd/img04.png 480w,
/static/8526e87b6e1b554c61d8ddc9d7190848/0f09e/img04.png 640w,
/static/8526e87b6e1b554c61d8ddc9d7190848/3e3fe/img04.png 860w,
/static/8526e87b6e1b554c61d8ddc9d7190848/ea60f/img04.png 1060w&quot; sizes=&quot;(max-width: 1060px) 100vw, 1060px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/8526e87b6e1b554c61d8ddc9d7190848/ea60f/img04.png&quot; alt=&quot;img04&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;/figure&gt;
&lt;h3 id=&quot;3-클라이언트-작업&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EC%9E%91%EC%97%85&quot; aria-label=&quot;3 클라이언트 작업 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;3. 클라이언트 작업&lt;/h3&gt;
&lt;p&gt;필연적으로 클라이언트 프로그래머는 모든 직군의 작업이 완료되어야 작업을 끝마칠 수 있기 때문에 작업 순서 상 가장 마지막을 담당하고 있습니다.&lt;/p&gt;
&lt;p&gt;앞에서 작업해주신 리소스, 서버, 데이터를 가지고 게임이 실제로 동작하도록 작업해줍니다.
기술적인 내용을 설명하는 글을 아니므로 자세한 작업 내용은 생략하도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;작업을 완료할 때 영상이나 캡쳐를 찍어서 리소스 제작자의 의도에 맞게 사용되었는지 확인해야 합니다.&lt;/p&gt;
&lt;figure style=&quot;max-width: 640px; margin: 0 auto;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1134px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 80.55555555555556%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAQCAYAAAAWGF8bAAAACXBIWXMAABYlAAAWJQFJUiTwAAADrklEQVR42m2Ue1MaZxSH+Wj9GvkQ/acz6aQzTme0xiqOdlRUEIggKEFN1IIGY0IuNhY1VAFvyGUXFuSO2ihyf/ruav9ox505s+++857nnPPu+R2dYWqaX/XDDAzpGRgYwO/3I8sy5+cxkkkJSZLIZDIoaYVCoUAulyOfz5NKpcgX8xSKBa6urqhWq1QqFXSqs6KkNUdZ2NnZGYlEgng8LtZRDV4ulikWipRUK5bI5wqUK2Vie0lCwTBGsxmjaUbz1aWVDClZIa0IE1lkxLdq6XSaYqlApVpByiXJ5MVe8UJ75wppcsUspYsKlVKF02hUMzVLnXV8jv6xRWzuCN7lQ3Ysh+zag2wtx/Gur3Po+YLDsMr6jBd3Tx+Dg4P8+PNTvu8fISKXgA6dTge6XZrNJrrfxrYZN33Fua7wdiVFxCwRnZfxzJ3S65hl1e4lPJsnOqewNxllZSPDqwkJ00iI0ElWgFrc3ta4u7ujXq+jM83u82I+getNiq0VcSfWJMeuJBvOKGPzn7A6A+zb0hw5Jd5bjnC/yeJ155iZOibyAKzV7mEacNqyh8VxLoBp/L+nCBgiOPps2Ce2WP+jhWfzG0FbRgNO652M2jf5ZdBBT99Ljk/zjwCtKjAmgArv1mRC5gRHIoDXfsKQaxOTa5e/BDBoO+HF8DLGxQAbLgWzIXSfIf8DTll2mbGfMb8hSl6VCIuSo64U3rljemfdjNo2RYAye7YjpocdjDs/4HOVsU6ePJTc/i/QbDvAuXTBq/clkaFExCpxspDAv5hl7XONtc0Kw6Nm3k59JDR/yusPMts+GYfxiPDxIxn+1GPhyZMfeNprwbcsETKJn2JPsOlI4fBd4FpR+Dh5ylf7HovD3+HeHuJdMMj42GfCR48AzdZVnvcbmJxZYstzQGDhkP2lEP61E9b853h8ZwRfx/i0vINhqg+Te5YV3yE25w7xxAXtdlMA79tGNV3rLk/z70tal5fUa0WhjCLV60u6omHbrQYd4dDuCGu36Ir+7bQ7tFpNWu2GaOSGllWj0RB7rfvGnjEaGXz2DL1eTywWp1qpCvGrgr8WB5vc3Nxw8+1GRK+JBr7ltlbXnDudtgjS1tb1eoNv4oxWclZoNhv4k3MhbHWqyHJKGwzSw6RRB4WUTGr7KTmOEgvwcsHOnN2K3eFgyjhN5OALNyKYGlyXyWTJlUp4vR4mxkao315qUlLvRT3w7/1oF38nsrgW8gwfsLcrFCR+TiAQoJxPoD7dbpd/ABj0Sr19oiPuAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/f2c8fbe320b83681d0821c0b78641149/33ecc/img05.webp 360w,
/static/f2c8fbe320b83681d0821c0b78641149/263a4/img05.webp 480w,
/static/f2c8fbe320b83681d0821c0b78641149/0c8fb/img05.webp 640w,
/static/f2c8fbe320b83681d0821c0b78641149/521f7/img05.webp 860w,
/static/f2c8fbe320b83681d0821c0b78641149/81d2f/img05.webp 1134w&quot; sizes=&quot;(max-width: 1134px) 100vw, 1134px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/f2c8fbe320b83681d0821c0b78641149/53918/img05.png 360w,
/static/f2c8fbe320b83681d0821c0b78641149/9aebd/img05.png 480w,
/static/f2c8fbe320b83681d0821c0b78641149/0f09e/img05.png 640w,
/static/f2c8fbe320b83681d0821c0b78641149/3e3fe/img05.png 860w,
/static/f2c8fbe320b83681d0821c0b78641149/3d024/img05.png 1134w&quot; sizes=&quot;(max-width: 1134px) 100vw, 1134px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/f2c8fbe320b83681d0821c0b78641149/3d024/img05.png&quot; alt=&quot;img05&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;/figure&gt;
&lt;br&gt;
&lt;hr&gt;
&lt;p&gt;작업을 하면서 같은 클라이언트 프로그래머끼리의 소통도 매우 중요합니다.&lt;/p&gt;
&lt;p&gt;쿠키런 킹덤은 약 4년 반 동안 라이브 서비스를 이어오며 수많은 컨텐츠를 추가해왔고, 그만큼 프로젝트의 구조도 점점 깊고 복잡해졌습니다.&lt;/p&gt;
&lt;p&gt;이런 기능이 필요하다 싶으면 80%는 이미 존재하는 기능입니다.
이미 구현된 기능을 모르고 또 추가하게 되면 코드가 중복되므로 불필요한 리팩토링이 이후 발생할 수 있습니다.
그래서 작업을 하면서 유사한 사례를 먼저 찾아보는 것이 중요합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“이 기능, 이미 있을까요?”&lt;/p&gt;
&lt;p&gt;“비슷하게 작업한 컨텐츠가 있을까요?”&lt;/p&gt;
&lt;p&gt;”이 작업, 어떻게 하면 좋을까요?”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;위와 같은 질문들이 실무에서는 클라이언트 프로그래머 사이에 거의 매일 오갑니다.&lt;/p&gt;
&lt;h2 id=&quot;-리소스-관리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EB%A6%AC%EC%86%8C%EC%8A%A4-%EA%B4%80%EB%A6%AC&quot; aria-label=&quot; 리소스 관리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;📦 리소스 관리&lt;/h2&gt;
&lt;p&gt;개발 작업 마무리 단계에 해야하는 일이 있습니다.&lt;/p&gt;
&lt;p&gt;바로 리소스 관리입니다.&lt;/p&gt;
&lt;p&gt;리소스 관리를 어떻게 하는지 설명하기에 앞서, 쿠키런 킹덤에서는 Unity AssetBundle을 사용하고 있습니다.&lt;/p&gt;
&lt;p&gt;AssetBundle이란, 게임 내 리소스를 따로 패키징해서 필요할 때 불러올 수 있도록 만든 Unity의 리소스 관리 시스템입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.unity3d.com/kr/530/Manual/AssetBundlesIntro.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;AssetBundles - Unity 매뉴얼&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;앱의 용량이 크면 처음 다운로드 받을 때 오래 걸리기 때문에, 앱 자체는 가볍게 만들고 아래와 같은 타이틀 화면에서 추가적인 리소스를 다운받습니다.&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 750px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 56.388888888888886%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACqklEQVR42gGfAmD9AIJUjZFrtt7L5dzG3Z+Lq6aWq/LSwfXa4vG84vCp2/DR5vHd5vnh3O/UwKiXr6OUsNrK1OTK2JdvuINRjABqNlmWT6itjbyomKrNl5uwhabX2M3st9nlcczgc8bkY73vv9nx5ujzzcK6g6i4Y4a7iIeukbSXUaxpNFcAhkRtpmKsin+duJ1o6dd80bajt7LU8ZTF4prT6cfk4nLD6IHa27HL2n/I4Kmv64aOzouRgnyZrmSvg0BrAGwwX5BYkJmEoKyrgM7Lm764kr+DyPKa3+602fnS3/a93uOwq9CGsayFxMKyptCfmLKIgpeDn4xclWwvXQBSJVNzUG+LeIe5gKHBl4+ygZ/vm9373Pb859376d797+P58Lb2w9XpktyteqCpfILRunyji6pwVpFTJU4AQyA+dEiBb2WKxFmrmnCNucPc/8r2+tze+e/a9e/l9e3j/PHm/c7p/8vwvtfkmmyBnYKQZXGKbj15RSFAAHVKYs+FxMA/oMREpbo7lc2nzsTPz9/t0+/s0e2/y+7Czfv00d3jybPe3JWvsJA0X2pVfJ2FfJ5HnmMcTgCSbH/vhN7eXsLoWMfTKKLXfr+2aXmVlo+e0LDGprO+qa6szbN+lIxDdHp4YXp2NV6SN2mnYVnAV5tlHEUAU0NI3HfI1Ea32mm/ykGvzYu6lVponoejw6PTkVZ8pWKUso6milVDe0AvgEs3hD2Pkj5mv4oykVg1HwUPAGBbaoRNcnYUTJ8sfoxBiqF5sHhbjpiekLTMuFhETFlVYqi7pY1QNXhUgo9bm45CnY9MaZppSHVMQkc6SACje5yCVmpnLD1wPmVUM2BJKVleLmZkMlRzUm5HO09IPUttVXJdNlpcLWVRMGZVL1qDSGOCQleNWG6WaYMZ4YbplUcV4gAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/9d4da3307b34e56c1d7bac4b3a41118a/33ecc/img06.webp 360w,
/static/9d4da3307b34e56c1d7bac4b3a41118a/263a4/img06.webp 480w,
/static/9d4da3307b34e56c1d7bac4b3a41118a/0c8fb/img06.webp 640w,
/static/9d4da3307b34e56c1d7bac4b3a41118a/3ea8a/img06.webp 750w&quot;
              sizes=&quot;(max-width: 750px) 100vw, 750px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/9d4da3307b34e56c1d7bac4b3a41118a/53918/img06.png 360w,
/static/9d4da3307b34e56c1d7bac4b3a41118a/9aebd/img06.png 480w,
/static/9d4da3307b34e56c1d7bac4b3a41118a/0f09e/img06.png 640w,
/static/9d4da3307b34e56c1d7bac4b3a41118a/e9985/img06.png 750w&quot;
            sizes=&quot;(max-width: 750px) 100vw, 750px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/9d4da3307b34e56c1d7bac4b3a41118a/e9985/img06.png&quot;
            alt=&quot;img06&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;br&gt;
이 때 앱에 포함되어 있는 리소스가 빌드 리소스, 타이틀 화면에서 추가로 다운받는 리소스가 AssetBundle 리소스(번들 리소스)입니다.
&lt;p&gt;작은 규모의 프로젝트에서는 리소스를 빌드에 포함시켜도 큰 문제가 없습니다.
하지만 게임이 점점 커지고 업데이트가 누적되면 빌드 크기가 심각한 이슈가 됩니다.
그렇게 되면 빌드 내 리소스를 전부 번들로 옮기는 작업을 해야합니다.&lt;/p&gt;
&lt;p&gt;만약 빌드에 들어있는 리소스가 번들 리소스를 참조하게 되면 다음과 같은 이슈가 발생하게 됩니다.
리소스를 제대로 불러오지 못해서 흰색 네모박스로 보이는 이슈입니다.&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 53.61111111111111%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACqklEQVR42gGfAmD9ABcPDFpCZ5d+iGlSbUw8XT0yWE1DZkUygkAZozAFrykHujIIzUsP0EgLyjYHuEgsnjIofRcSTjQvJzEtEgARCQp8ZJC+oLd3YIheTG1GN2ZMQWlDJ5U6CcouAN0yAOpDB/F8GfZeAPZFAO5ZKddDMckhF5ZHPkxAPB0AEgwKeWeHtp6ycVyNX057Rzlqd3CKalyaYz7IYTHedjTsjUHrs0vxli/wXA3qXzDiSSW9Mw6gUz95QDseABEOC4FzgbGhpmFQeFFAbmNbf/j79unr6tze3N3e3d3f4Nvc3+Lj6uPl7YVL7nVC7FUktkUGp2Y7mUI9IwAWEw+Den+voKFYSXY+M1tnYXTo6eTV1dbOzdHT0dDPy9LPxtnc1ubUzeS+s+S7uOG4v9dvULg/FXxAPCIAGxgYj4KZqZ2uVUV/QjdgcGt6/v/46uzq6ern7vDu6OXo49/j6u/o4Obi0tXg097i2vLke222IQBkPDkhACIeJpF/vWNEvjoYoR0EdEM3Y6ajqY6Poo2HoomCj5GGmaaHyKiH25qEzI13zI562ZGH1GNDwSQDciMhFgAsKDFvVLwuCK5BIas1GYckElkzKlYXE0kfEWg6HYNKGadZDNtDAL84ALUwALQnALgnALItCLEeBnoJBw4AJB0vkX7HpKCvoJ+empiaqaOtu7PAubK8v7jEvrTOwLLWwbPUzb3fyrziw7rcu7jUtbbQrbPLS0KFAQALAAwBK7Sq1v3/8+rp6fLv7PHs5/Hr5PPt5/n07/Lu5O7s4Pb06f//8fv+7/j88PP78On26vD/9ImWogAABAAJAh06KXuFgJ6Kg6SNhKuRha+Sh6iRiJ6QiJWUipmVip+birGhi8SgkMGclbWWk6qIipx9g5IxL1ACAAgYaVTwQPHX8AAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/5bd8c867141fccc759bedcc2298fe55c/33ecc/img10.webp 360w,
/static/5bd8c867141fccc759bedcc2298fe55c/263a4/img10.webp 480w,
/static/5bd8c867141fccc759bedcc2298fe55c/0c8fb/img10.webp 640w,
/static/5bd8c867141fccc759bedcc2298fe55c/521f7/img10.webp 860w,
/static/5bd8c867141fccc759bedcc2298fe55c/cbd37/img10.webp 1200w,
/static/5bd8c867141fccc759bedcc2298fe55c/548e7/img10.webp 2400w,
/static/5bd8c867141fccc759bedcc2298fe55c/9189c/img10.webp 4030w&quot;
              sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/5bd8c867141fccc759bedcc2298fe55c/53918/img10.png 360w,
/static/5bd8c867141fccc759bedcc2298fe55c/9aebd/img10.png 480w,
/static/5bd8c867141fccc759bedcc2298fe55c/0f09e/img10.png 640w,
/static/5bd8c867141fccc759bedcc2298fe55c/3e3fe/img10.png 860w,
/static/5bd8c867141fccc759bedcc2298fe55c/64756/img10.png 1200w,
/static/5bd8c867141fccc759bedcc2298fe55c/98e2c/img10.png 2400w,
/static/5bd8c867141fccc759bedcc2298fe55c/6c900/img10.png 4030w&quot;
            sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/5bd8c867141fccc759bedcc2298fe55c/98e2c/img10.png&quot;
            alt=&quot;img10&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;br&gt;
반대로 번들에서 빌드 리소스를 참조할 경우 빌드에 있는 리소스가 복사되어 번들로 들어갑니다.
결과적으로는 중복 리소스로 인해 번들의 용량이 늘어나게 됩니다.
&lt;p&gt;최악의 경우 빌드를 다시 해서 업데이트를 다시 진행해야 하기 때문에 위와 같은 이슈를 방지하기 위해 클라이언트 프로그래머는 아래처럼 외부 리소스 참조 여부를 알려주는 툴을 제작해서 리소스를 관리합니다.&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1484px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 28.333333333333332%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAABYlAAAWJQFJUiTwAAABPUlEQVR42j2QW0vDQBSE82NsffCtuTWbNLa7m4uWpJVeFPGpIBZB/A9Nr6IViv93nF2LDx8z7AmTM8fJ6jsEngsRRYi6XcRCII4FhFES8d34JEkQhiFc14Xnef90Oh07k1La75xi9oh0IFFPZxiR7HYImReQ5Q1UUSIrCovOMug8R0YGSv3BkD4x82FV2VBnLDxMrgXuZYIHlVo/SSNMyZgMej0obtDn343qNIXuJVDm/YyMYzszLZ29uMLWa2HltbHtXGDntrDx29jZt0vMWasOAgx5jsooa9dnNVQGzkbmNFTneTHD4XjC4esbH2f2n0ccTz94fXuHZjXJusrULUvrJb1mzZjbRua23D5gYMBwZ/E0x2a7wbpZkQbrdYOG/rDfYbl8sXcyoZnWyE0QfUE1KHrFecLKIbf2fR+/CoTedMIL2YsAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/b4711531a725c9783982bf2eb5c43786/33ecc/img07.webp 360w,
/static/b4711531a725c9783982bf2eb5c43786/263a4/img07.webp 480w,
/static/b4711531a725c9783982bf2eb5c43786/0c8fb/img07.webp 640w,
/static/b4711531a725c9783982bf2eb5c43786/521f7/img07.webp 860w,
/static/b4711531a725c9783982bf2eb5c43786/cbd37/img07.webp 1200w,
/static/b4711531a725c9783982bf2eb5c43786/0e833/img07.webp 1484w&quot;
              sizes=&quot;(max-width: 1484px) 100vw, 1484px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/b4711531a725c9783982bf2eb5c43786/53918/img07.png 360w,
/static/b4711531a725c9783982bf2eb5c43786/9aebd/img07.png 480w,
/static/b4711531a725c9783982bf2eb5c43786/0f09e/img07.png 640w,
/static/b4711531a725c9783982bf2eb5c43786/3e3fe/img07.png 860w,
/static/b4711531a725c9783982bf2eb5c43786/64756/img07.png 1200w,
/static/b4711531a725c9783982bf2eb5c43786/ed887/img07.png 1484w&quot;
            sizes=&quot;(max-width: 1484px) 100vw, 1484px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/b4711531a725c9783982bf2eb5c43786/ed887/img07.png&quot;
            alt=&quot;img07&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;h2 id=&quot;️-피드백-회의&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EF%B8%8F-%ED%94%BC%EB%93%9C%EB%B0%B1-%ED%9A%8C%EC%9D%98&quot; aria-label=&quot;️ 피드백 회의 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;↩️ 피드백 회의&lt;/h2&gt;
&lt;p&gt;마감 기간이 끝나면 해당 업데이트에 들어갈 모든 컨텐츠를 모아서 피드백 하는 회의가 있습니다.
PD가 회의에 참여하여 구현이 처음 제시한 방향성대로 잘 되었는지, 더 필요한 부분은 없을지를 피드백 해주십니다.&lt;/p&gt;
&lt;p&gt;이 회의 이후 3~4일 정도 피드백 반영 기간을 거치고, 본격적인 QA를 시작합니다.&lt;/p&gt;
&lt;h2 id=&quot;-qa--빌드-제출&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-qa--%EB%B9%8C%EB%93%9C-%EC%A0%9C%EC%B6%9C&quot; aria-label=&quot; qa  빌드 제출 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;📝 QA &amp;#x26; 빌드 제출&lt;/h2&gt;
&lt;p&gt;이제 QA팀의 작업이 본격적으로 시작됩니다.&lt;/p&gt;
&lt;p&gt;QA팀이 컨텐츠 개발이 진행되는 동안 미리 해두었던 기획서 리뷰와 테스트 케이스를 토대로 테스트를 진행합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;기획서 리뷰 (인스펙션 작성)
&lt;ul&gt;
&lt;li&gt;인스펙션 : 기획 문서를 바탕으로, 초기 문제점을 사전에 탐색하고 보완하는 질의응답 과정을 말합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;TC(Test Case) 작성 : 인스펙션 내용을 반영한 기획서를 기반으로, 테스트 항목을 정리한 테스트 케이스(TC)를 작성합니다.&lt;/li&gt;
&lt;li&gt;테스트 진행 : 테스트는 일반적으로 다음 순서로 진행됩니다 : 유니티 에디터 테스트 → 실제 기기 빌드 테스트&lt;/li&gt;
&lt;li&gt;컨텐츠 플레이 및 피드백 제출&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 과정을 통해 실제 구현물이 기획 의도에 맞게 작동하는지 확인하고, 컨텐츠의 완성도를 높입니다.&lt;/p&gt;
&lt;p&gt;클라이언트 프로그래머는 이 과정에서 발생한 피드백과 이슈를 수정하고, 다시 빌드하여 반영합니다.&lt;/p&gt;
&lt;p&gt;QA 이후, 테스트를 마친 게임 빌드를 검수한 뒤 각 앱스토어(Google Play, Apple App Store 등)에 제출합니다.&lt;/p&gt;
&lt;h2 id=&quot;-사후-관리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%82%AC%ED%9B%84-%EA%B4%80%EB%A6%AC&quot; aria-label=&quot; 사후 관리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;🪦 사후 관리&lt;/h2&gt;
&lt;p&gt;앱스토어에 제출했다고 끝난 것이 아닙니다. 이후에는 모니터링을 진행합니다.&lt;/p&gt;
&lt;h3 id=&quot;1-라이브-이슈-수정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EB%9D%BC%EC%9D%B4%EB%B8%8C-%EC%9D%B4%EC%8A%88-%EC%88%98%EC%A0%95&quot; aria-label=&quot;1 라이브 이슈 수정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;1. 라이브 이슈 수정&lt;/h3&gt;
&lt;p&gt;QA팀에서는 업데이트 이후에도 지속적으로 라이브 모니터링을 진행하고, 이슈가 나올 경우 원인을 확인해서 수정합니다.&lt;/p&gt;
&lt;figure style=&quot;max-width: 640px; margin: 0 auto;&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1052px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 104.72222222222223%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAVCAYAAABG1c6oAAAACXBIWXMAABYlAAAWJQFJUiTwAAAEdUlEQVR42pWVWW9TVxDH/QZIqJX6gJBQpX6IqlKfEKWgPvQTULU8FKG2UquqtFCplSooICgta2khUBFC4sROYpP4xk68O/GeOHbikHi5XuI4JguOszQJWX899xJHBPHSI43+55458585c8+Z0YwVxsjn88gZmYycYWRkRMVMJkMsFiOdTpNKpVSMx+PIaRlZlkkmk8zPz6OMzc1NqkOjKFSSTJb5xXnW1tdZW11jXeDGxoayWzXYIRubqq6652WdRqfXceHiBW7cuI5cSrO6vsrS4iIrK8/ZEAb/d2hGBpMM9A2RT5UopCeRE0Xk4XESArOZcZ7lSkzIoxTlHPnEKPH+JGFfGJe5B7srwGBq7MWxq4R1f+jQXm1EqvfgaO/HIfUT7AjjtkW5o23n97/vIxk6eeIME7KE6GoJcr+mgY+Pfcen35xF3+LcyuMWYXeDjagxTJ8zRTw4Sj6aZfKJzFQyT0/3ADqTF5dvgHhvkbivyHC4RH93jg5DkDqtA7vJszPCLr2TSFeShLdEKjJBaajI03iRcYGZ5BSpzIxYF2iZJe2aQY6V8btS3Lxn4EZNEx7pVULJLRR+av6MkhmcJh0VRn0zDMfLBJ6MY+0TzqJF0pFnpMJCN1AhFiwitYWxNpkJCfsdP8XTbOVajZ5bNSb6g2Nk43PIkQrJ4QrdsRy3H9nI9uaZGF6gMPgvE4kVooEit/4xcqdGS2DryNuE9hYXekuYdmlQRDFNKiYi6SuTGCpjdMc5fd5JvSNMU4cfo9SHTqDDOSxSNEmndRC71LPzyE6TmyvXT3Pv2g8kep8SDxUZ8peIRUpY3WEMeivGx25adF50+h4aGx0Y23qIRRNIdh+u9ldyePPSXb76/EPOnDyCU1ybSHeWkD1N2JamSaTj7p0Gah+0cPWuDkOrn4GePL7OGD1DdoxeA2Zt585ro3+o59efLnPx3F8YhFIyuJFa3DhavNgeWWirfUx7bRvGWklEacNjjYi/HMPriGASl9vv6d1JeP7sJb7+8lvOnPqR48dPcvLEF3xy7DMOHjzMoUNHOPzBUVWOHvmI9959nwcPHqqG6jt/3dM78/3PItl+Ar4E7q4YTu8I+joz+/YdYP/+t7flwIF32LPnTS5fvqIarm+985crzQvCU7/Q0NiJtrELmzmKttnCb5fusWvXXnbvfmNb9u59C41Gw7lzF1TD1dVVNcpqpFViTTTaTzaVIClKmJwskEko5SyNzerAarVvi83moKPDQjabe21k2xE63RJ19bdp0tXzsK6W+oYGLBYzTpcTl1OIQOcWut0uzGYzra2tGI1GmpubhSObWmhnZmaYm5sTxcHTjaldEt47MJlM+Hw+AoGgin6/X5Xq/IUuQCgUIhgMqmtKVV9aWmJhYYFFUUc15XKZ6elp1cvk5CSzlVlmZ2fVb8Wj4llZU+aVSkXVKagQVPcp9lNTU+pco3woPULpIUp/KRQK5HI5tYco7UHRjY6OMj4+rqKiU8ifLz9XI1peXlZlZWWFtbU1/gO+NYqW7aeMrwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/22d963a12438b2c4b04f38232841f8dd/33ecc/img08.webp 360w,
/static/22d963a12438b2c4b04f38232841f8dd/263a4/img08.webp 480w,
/static/22d963a12438b2c4b04f38232841f8dd/0c8fb/img08.webp 640w,
/static/22d963a12438b2c4b04f38232841f8dd/521f7/img08.webp 860w,
/static/22d963a12438b2c4b04f38232841f8dd/eacd9/img08.webp 1052w&quot; sizes=&quot;(max-width: 1052px) 100vw, 1052px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/22d963a12438b2c4b04f38232841f8dd/53918/img08.png 360w,
/static/22d963a12438b2c4b04f38232841f8dd/9aebd/img08.png 480w,
/static/22d963a12438b2c4b04f38232841f8dd/0f09e/img08.png 640w,
/static/22d963a12438b2c4b04f38232841f8dd/3e3fe/img08.png 860w,
/static/22d963a12438b2c4b04f38232841f8dd/5a4f3/img08.png 1052w&quot; sizes=&quot;(max-width: 1052px) 100vw, 1052px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/22d963a12438b2c4b04f38232841f8dd/5a4f3/img08.png&quot; alt=&quot;img08&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;/figure&gt;
&lt;br&gt;
어떤 이슈가 발생했느냐에 따라 대응 방식이 달라집니다.
&lt;p&gt;리소스를 수정하는 것으로 고칠 수 있는 이슈는 패치를 진행하고, 코드를 수정해야 하는 이슈는 중요도에 따라 알려진 버그로 공지를 올리거나 빌드를 다시 해서 업데이트를 진행합니다.&lt;/p&gt;
&lt;h3 id=&quot;2-메모리-관리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EA%B4%80%EB%A6%AC&quot; aria-label=&quot;2 메모리 관리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;2. 메모리 관리&lt;/h3&gt;
&lt;p&gt;메모리 관련 이슈는 지금도 종종 저사양 기기를 사용하는 유저에게 발생하기 때문에, 지속적으로 모니터링해서 최적화를 해주어야 합니다.
모든 유저가 고사양 기기를 쓰는 것이 아니기 때문에, 저사양 환경에서도 원활하게 동작하도록 메모리 사용을 조절해야 합니다.&lt;/p&gt;
&lt;p&gt;기기 사양에 따라 앱이 사용할 수 있는 메모리 양에 제한이 걸려있기 때문입니다.&lt;/p&gt;
&lt;p&gt;예를 들어 iPhone 8이나 XR같은 일부 기기에서는 실행 중 앱이 사용할 수 있는 메모리가 약 2GB로 제한되어 있어서 이 이상으로 메모리를 사용할 경우 iOS는 메모리 보호를 위해 예고 없이 앱을 종료합니다.&lt;/p&gt;
&lt;p&gt;게임이 안정적으로 실행되도록, 번들 리소스를 무제한으로 로드하지 않고 일정 시점마다 메모리에서 내리는 작업이 필요합니다.&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1234px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 46.666666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAAAsTAAALEwEAmpwYAAACkklEQVR42hXQ7TPTAQDA8d+/0Nv+gOSu6+FF6a4XxZXkIVZ0NWGhbcSMYW2T2bCRDc1q0zLMajR52tEkQ5yHTp4qrl0qL91FXL3s7pv+ge997iucVuRzKDWRiFI9R/VWourt2N7NML+1j35ij8qxHR682aEwsIs+tEvl+C7ylyGsIwGKfB5c0zOcUhs5UqzmcFocQoTmDmKbEsXzDrK6e2iYCtG2tIFvJczQzDxlbzeRjG2TO7ZPzuswL5bnaJ3/gnV6BevsN5xLqzxb/k6mf5bIMjFC5hMF8m41Jo+GjvZSmlvyaO9t5mXffSY8atrd5QT8ehxBL4ZGFc0+Hc88JlztZtTOUlRGCdX9WpReFckmKYIqR06jooxeSxW6unguFaXTYOtiqkXMRIeeBZeStQE7kyuzeE3ltClE/Fl6za9hH506Oc+1OYhrVKTJUhgs1yAUpmXQn1XDlsFBj9OILOciTVopdffL+BJ8TDhk48fHEXY2fSz0qfDdTmBdo2J7sI1Ffylzo9cIDEpwWC9juXsFwZyUjj9BykZJHX86XzH31MjauJb9Tw6ChhL81oPwBwV/f3oZ9xbQWZjLukPN5qiRfnc0/tZIhntOMDtyljxDLIL9ejKWuHiWCoxMN2Yyb5ax3lXM70U7y10aVkc1fF0pgN8D7H1uI+TI5P1CJeE5E05NPI81J3llOYa5NooYQwaCKz8bS+wVJiQHkr5ixnrzqNOJqZWLcFalY9VJeGSR43tagfthNlWFl7hVnUJlRSJNouNIU8/QIIpCKT7HsZsXELTSJKoNCbgaxdS3irAP3SQ46WE4MITLXYLZlkqzI4Mh9/9fNrotMnKVMVzTx2FIvsi9q9HckMVQVZRCfPZ5/gGCG945bJKQCgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/7a4c1269dc2fd5a5a0f4eef4176a86e6/33ecc/img09.webp 360w,
/static/7a4c1269dc2fd5a5a0f4eef4176a86e6/263a4/img09.webp 480w,
/static/7a4c1269dc2fd5a5a0f4eef4176a86e6/0c8fb/img09.webp 640w,
/static/7a4c1269dc2fd5a5a0f4eef4176a86e6/521f7/img09.webp 860w,
/static/7a4c1269dc2fd5a5a0f4eef4176a86e6/cbd37/img09.webp 1200w,
/static/7a4c1269dc2fd5a5a0f4eef4176a86e6/9f431/img09.webp 1234w&quot;
              sizes=&quot;(max-width: 1234px) 100vw, 1234px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/7a4c1269dc2fd5a5a0f4eef4176a86e6/53918/img09.png 360w,
/static/7a4c1269dc2fd5a5a0f4eef4176a86e6/9aebd/img09.png 480w,
/static/7a4c1269dc2fd5a5a0f4eef4176a86e6/0f09e/img09.png 640w,
/static/7a4c1269dc2fd5a5a0f4eef4176a86e6/3e3fe/img09.png 860w,
/static/7a4c1269dc2fd5a5a0f4eef4176a86e6/64756/img09.png 1200w,
/static/7a4c1269dc2fd5a5a0f4eef4176a86e6/649dd/img09.png 1234w&quot;
            sizes=&quot;(max-width: 1234px) 100vw, 1234px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/7a4c1269dc2fd5a5a0f4eef4176a86e6/649dd/img09.png&quot;
            alt=&quot;img09&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
위와 같은 로딩 화면을 띄워놓고 메모리 정리를 진행합니다.&lt;/p&gt;
&lt;p&gt;주로 전투 화면에 진입하기 직전에 메모리를 정리하는데요, 전투 씬은 리소스를 많이 사용하는 씬이기 때문에 진입 직전에 사용하지 않는 리소스를 메모리에서 정리하여 여유 공간을 확보합니다.&lt;/p&gt;
&lt;h3 id=&quot;3-리팩토링&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81&quot; aria-label=&quot;3 리팩토링 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;3. 리팩토링&lt;/h3&gt;
&lt;p&gt;작은 기술적 결함을 &quot;시간이 없어서&quot;, &quot;나중에 고치지 뭐&quot; 하고 방치하다 보면, 그 문제가 점점 스노우볼처럼 굴러가 결국에는 팀 전체에 영향을 주는 큰 장애로 이어질 수 있습니다.
일명 기술 부채(Technical Debt)가 쌓인 것이죠.
이런 기술 부채를 예방하기 위해서는 리팩토링을 습관적으로, 꾸준히 진행하는 자세가 필요합니다.&lt;/p&gt;
&lt;p&gt;지금은 아무 문제가 없어 보여도, 새로운 기능이 계속 추가되면서 코드의 복잡도는 점점 올라갑니다.
구조가 복잡해질수록 작은 수정이 큰 영향을 끼칠 수 있어, 어느 순간에는 리팩토링 없이는 손을 대기 어려운 지경에 이를 수 있습니다.&lt;/p&gt;
&lt;p&gt;다만 리팩토링은 기능 변경을 동반하지 않더라도, 기존 동작에 영향을 주는 일이 자주 발생합니다.
따라서 리팩토링 전에 반드시 유관 부서와 협의하고, QA 및 PM 팀에 충분히 공유하여 리스크를 최소화해야 합니다.
또한 리팩토링 작업을 하는 사람은 테스트 코드를 짜두어 기존 기능이 잘 동작하는지 확인할 수 있어야 합니다.&lt;/p&gt;
&lt;br&gt;
&lt;h1 id=&quot;클라이언트-프로그래머가-되고-싶은-사람에게-하고-싶은-이야기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EA%B0%80-%EB%90%98%EA%B3%A0-%EC%8B%B6%EC%9D%80-%EC%82%AC%EB%9E%8C%EC%97%90%EA%B2%8C-%ED%95%98%EA%B3%A0-%EC%8B%B6%EC%9D%80-%EC%9D%B4%EC%95%BC%EA%B8%B0&quot; aria-label=&quot;클라이언트 프로그래머가 되고 싶은 사람에게 하고 싶은 이야기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;클라이언트 프로그래머가 되고 싶은 사람에게 하고 싶은 이야기&lt;/h1&gt;
&lt;p&gt;게임 클라이언트 프로그래머를 꿈꾸는 분들에게, 신입으로서 알아두면 좋은 기술과 실무에서 중요하게 여기는 태도를 소개드리고 싶습니다.&lt;/p&gt;
&lt;h3 id=&quot;-신입으로서-알아두면-좋은-기술&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%8B%A0%EC%9E%85%EC%9C%BC%EB%A1%9C%EC%84%9C-%EC%95%8C%EC%95%84%EB%91%90%EB%A9%B4-%EC%A2%8B%EC%9D%80-%EA%B8%B0%EC%88%A0&quot; aria-label=&quot; 신입으로서 알아두면 좋은 기술 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;✅ 신입으로서 알아두면 좋은 기술&lt;/h3&gt;
&lt;p&gt;신입으로서 알아야 하는 기술과 더불어, Unity 기반의 프로젝트에서 실무에 도움이 되는 기술들을 정리해보았습니다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;자료구조와 알고리즘 : 기본 중의 기본입니다. 면접에서도 자주 다뤄지는 만큼, 프로그래머라면 반드시 익혀야 합니다.&lt;/li&gt;
&lt;li&gt;디자인 패턴 (Factory, Singleton, State, Observer 등) : 대규모 프로젝트에서 코드 유지보수성과 확장성을 높이기 위해 반드시 필요합니다.&lt;/li&gt;
&lt;li&gt;Git : 버전 관리는 팀 프로젝트의 기본입니다. Git을 통해 브랜치 개념을 이해하고, 충돌 해결이나 커밋 이력을 관리하는 능력은 실무에 필요합니다.&lt;/li&gt;
&lt;li&gt;Unity Profiler / Memory Profiler : GC 스파이크, 메모리 누수를 잡는 데에 핵심적인 도구입니다.
&lt;blockquote&gt;
&lt;p&gt;GC 스파이크 (Garbage Collection Spike)란?&lt;/p&gt;
&lt;p&gt;C#에서 메모리 정리를 담당하는 Garbage Collector가 실행될 때, 게임 실행이 잠시 멈추는 현상을 말합니다.
큰 오브젝트 생성·삭제가 많으면 이 현상이 눈에 띄게 발생해, 프레임 드랍이나 끊김처럼 느껴집니다.
모바일 게임에서는 이를 최소화하는 것이 중요합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;UniTask 라이브러리 : coroutine, task보다 훨씬 가볍게 동작하여 GC 부담을 줄이는 데 유리합니다. 비동기 처리를 자주 하는 모바일 게임에서는 필수적으로 사용됩니다.&lt;/li&gt;
&lt;li&gt;DOTween 라이브러리 : UI 애니메이션이나 연출 구현을 빠르게 도와주는 트윈 라이브러리입니다. 연출 퀄리티를 높이는 데 유용하면서도 사용이 간편합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;또한, 직접 게임을 처음부터 끝까지(출시까지) 만들어 본 경험만큼 좋은 포트폴리오는 없습니다.
출시한 게임이 있거나, 툴을 만들어 본 경험이 있다면 실무에 바로 도움이 되며, 팀에서도 높은 평가를 받습니다.&lt;/p&gt;
&lt;h3 id=&quot;-실무에서-중요하게-여기는-태도&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#-%EC%8B%A4%EB%AC%B4%EC%97%90%EC%84%9C-%EC%A4%91%EC%9A%94%ED%95%98%EA%B2%8C-%EC%97%AC%EA%B8%B0%EB%8A%94-%ED%83%9C%EB%8F%84&quot; aria-label=&quot; 실무에서 중요하게 여기는 태도 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;🧠 실무에서 중요하게 여기는 태도&lt;/h3&gt;
&lt;p&gt;기술만큼 중요한 것이 ‘어떤 자세로 업무에 임하는가’입니다.&lt;/p&gt;
&lt;p&gt;제가 현업에서 느낀, 클라이언트 프로그래머로서 중요한 자질들을 정리해보았습니다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;게임에 대한 애정과 몰입&lt;/strong&gt; : 게임을 좋아하는 사람은 결과물에 더 애정을 담게 됩니다. 결국 이런 마음가짐이 퀄리티와 디테일로 이어집니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;끊임없이 배우려는 자세와 피드백을 수용하고 개선하는 마인드&lt;/strong&gt; : 기술은 계속 발전하기 때문에 끊임없이 배우려는 자세가 필요한 것 같습니다.
또 리뷰를 주고받는 일이 많다 보니, 방어적인 태도보다는 개선을 위한 수용력이 중요합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;끈기와 탐구심&lt;/strong&gt; : 실무에서는 ‘왜 이런 문제가 생겼는지’를 찾는 데 하루 이상 걸릴 때도 있습니다.
당장 눈에 보이지 않아도 포기하지 않고 끝까지 파고드는 태도가 필요합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;협업 중심 사고&lt;/strong&gt; : 클라이언트는 기획, 서버, 아트, UIUX, QA 등 모든 직군과 협업합니다.
타 직군의 흐름과 업무를 이해하려는 노력이 곧 원활한 개발로 이어집니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br&gt;
&lt;h1 id=&quot;마치며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0&quot; aria-label=&quot;마치며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;마치며&lt;/h1&gt;
&lt;p&gt;제가 이 일을 하면서 가장 크게 느낀 건, 클라이언트 프로그래머는 게임을 살아 움직이게 만드는 사람이라는 점입니다.
그리고 그 과정에는 기술뿐 아니라, 소통, 문제 해결, 꾸준한 배움이 꼭 필요하다는 것도 함께 배웠습니다.&lt;/p&gt;
&lt;p&gt;게임을 좋아하고, 계속 배우려는 마음이 있다면 누구든 이 일을 해낼 수 있습니다.
처음에는 부족할 수 있어도, 하나하나 익혀가다 보면 실력은 반드시 쌓이고, 언젠가 자신 있게 “나도 팀의 일원이다”라고 말할 수 있게 될 거예요.&lt;/p&gt;
&lt;p&gt;지금 이 글을 읽고 있는 여러분이 그 첫걸음을 내딛고 있다면, 그 용기와 시작을 진심으로 응원하며, 이 글이 여러분의 여정에 작은 도움이 되기를 바랍니다.&lt;/p&gt;
&lt;p&gt;읽어주셔서 감사합니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[주니어 클라이언트 개발자의 성장 노트]]></title><description><![CDATA[주니어 클라이언트 개발자로서 겪어본 경험을 나눠봅니다.]]></description><link>https://tech.devsisters.com/posts/junior-client-note</link><guid isPermaLink="false">https://tech.devsisters.com/posts/junior-client-note</guid><pubDate>Fri, 25 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1 id=&quot;들어가며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%93%A4%EC%96%B4%EA%B0%80%EB%A9%B0&quot; aria-label=&quot;들어가며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;들어가며&lt;/h1&gt;
&lt;p&gt;안녕하세요. 스튜디오 킹덤 클라이언트 개발자 이한나입니다. 이 글은 제가 신입으로 입사하여 3년 차인 지금까지 일하며 경험한 것들을 돌아보고 되새겨 보는 글입니다. 이 글이 저와 같은 신입 또는 주니어 클라이언트 개발자분들께 작게나마 도움이 되기를 바라며 저의 경험을 나누고자 합니다.&lt;/p&gt;
&lt;h1 id=&quot;제발-담당자한테-물어보고-작업하자&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%9C%EB%B0%9C-%EB%8B%B4%EB%8B%B9%EC%9E%90%ED%95%9C%ED%85%8C-%EB%AC%BC%EC%96%B4%EB%B3%B4%EA%B3%A0-%EC%9E%91%EC%97%85%ED%95%98%EC%9E%90&quot; aria-label=&quot;제발 담당자한테 물어보고 작업하자 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;제발 담당자한테 물어보고 작업하자&lt;/h1&gt;
&lt;p&gt;일하다 보면 종종 내가 이해한 것과 기획자가 의도하는 방향이 다른 경우가 생깁니다. 확신이 가지 않는 부분은 당연히 물어봐야 합니다. 설계를 하고 있을 때, 작업을 진행하고 있을 때, 작업을 완료했을 때까지, 늦었다고 생각할 때가 가장 빠른 질문 타이밍입니다.&lt;/p&gt;
&lt;p&gt;&quot;10번 뽑기 기능&quot;을 만드는 상황을 예시로 들어보겠습니다.&lt;/p&gt;
&lt;p&gt;여기서 질문을 하지 않는 개발자가 1번 뽑기를 10번 반복하는 것으로 이해하고 (음! 전부 이해했어!) 작업을 진행합니다.&lt;/p&gt;
&lt;br&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 360px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 92.77777777777777%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAATCAIAAAAf7rriAAAACXBIWXMAAAsTAAALEwEAmpwYAAAEOUlEQVR42lWUa0xbZRjHm5ioRBY33UUTnIuaOKefTGZMFk3mN0PiNxMyb4kSGcGBc2GodLrIhmRsWqha2kJLaXtoT8/p7bTlnNPb6WnHrffLaUuhLbSAdIW1yLVCOZ5SL/HN8354k/f3PM/7z/99WBAZHrX4B2CrUG35Va7jSdViUD8CGWSwsRpipZ4nU3OlGr4K46nMMtwDkxHQ5gftAZZAbQM06KTHH45QxWKBpulyef/g4IA+OCjv7zH78FgulXaz2UV/iFLqMQFsYXgVAxvN9tRcIreywmD5fN7tds/OzgUPVz6fm08n06nk3NwsRVGJRCKbzaz8voSYMJGehMkwayGT7evrb2trCwQCGIa1trZ2dLQ3Nn7edPkyYjD29HI7b3Q1NrWw2exLlxra26/xBQKv2y3WWGBnhBUMRwqFh9lMlqnMtE1F4/nVDdc4YTToCIRjUVw1Sb/Uyns9Hl8iuZRKLc0mYqOwbthwH3KEWfxRhHCOF4vFnZ2t0l55ORV04p8GrOfn1We3TEdK6OO7aO0Odiytfd3v6Hc47vw8qBpBpyFHSGnzs0AiyNcQAkD9fQ9XwOvCOadtwycTwJm8upYmT9LEqT3LkVXdcZqszatr4q46pfg9iZ6EDgVnMYrDZEjtinXzlRHgBcvdGnP/hUmoe2r0s0XwqX3r8Xmk3jn4ThI8W0KfTo29HNceNw6+L7HEIMJXgUGbV0kmNUDHNl6TUp1KmLvsBm3UhWcct6LGDoN8aMIIzJiubRqfGB+o41w5ltGcFkIY7IwewnafwjHDuf2hqffMvOaVtQVvqUzPRabKy6H8QmJ5ZTkeiWSI77ZNNUnF8+DNE+uGo8NKCUjOVGAVEYQI/5ApJBzoTEJv9HT1NnzQhMJSOuefIMxmK6mUK4vTnbtjj0WldUHxiU3k0WH5gNIxw2LISth94P0MAnyRgC/Wv3vutVdZuKKfzkXeevNF1iOslsZPtqZbSvhRs7iZ2/c198bbQ6AOYtr+D3alZPymtJm9X8BLq6LCDLkeH5/z3I1ON6e9+jXzxQf654b4PAkeHEaZl4Yq3lYdtl2FJaJvF7GG7XSYfkDtLPnyQUsp66M3kw+ZROgzqnsX+iEH7KIg5nJFqcC/lQOwMyZWG8OiZ7kK4W+4xyjhxKwK75QVcEUNwJVJ4UuDGkdU+AOCEqAzqqrCVcFUREDtDItMHgS4ozPKz982XP+m88d7P30kwK8CLoOCPSjiSe0xVPSLQTIkwTxKi/d/lRmvytAJfGyMwuR2wlbYKuU3djlae9xD/rFelCg0zddvNnbcamN3f9z8FVeGqF3UPzARZL7YyNgEbiPSydmNzc1CbjkRo/zhaG6tyMB25zgzCUbxKTk2JUacAO5mkL/hqkklpkkEtW79uZ8mtP76J4Mq/k6ZXs3nmMHAZBEhTo2LqpiCDFeRvwCSxx6X0YXHpQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/4d950ac9c06511846ea874291f611190/33ecc/image1.webp 360w&quot; sizes=&quot;(max-width: 360px) 100vw, 360px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/4d950ac9c06511846ea874291f611190/53918/image1.png 360w&quot; sizes=&quot;(max-width: 360px) 100vw, 360px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/4d950ac9c06511846ea874291f611190/53918/image1.png&quot; alt=&quot;image1&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        (이미지 출처 : 팝 팀 에픽)
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;br&gt;
&lt;p&gt;그리고 마감날이 되어 테스트를 돌려본 기획자가 물어봅니다.&lt;/p&gt;
&lt;p&gt;기획자 : 이거 뽑기 10번을 전부 보여주고 있네요? 뽑기는 1번만 보여주고 보상을 10개를 줘야 하는데 다르게 나오는 것 같아요. 확인 부탁드립니다.&lt;/p&gt;
&lt;p&gt;클라이언트 개발자: !!!&lt;/p&gt;
&lt;p&gt;그동안의 시간과 노력을 지우고 다시 작업을 하는 상황이 되어버렸습니다. 눈물을 머금고 다시 작업을 하고 싶지 않다면 자신이 이해한 것 그리고 작업 방향이 기획자가 의도하는 방향과 맞는지 물어보고, 기록하고, 작업을 진행하도록 합시다.&lt;/p&gt;
&lt;p&gt;추가로 작업하면서 중간중간 결과물을 영상으로 찍거나 스크린샷으로 캡쳐해 공유합시다. 구현 방향을 공유하는 의미도 있고, 나중에 리팩터링하거나 기능이 추가될 때 원본 기능이 어땠는지 참고하기 좋아서 기록 차원에서라도 남겨두는 게 좋습니다. 그리고 귀여운 작업물을 만들었다면 자랑할 수 있다는 장점도 있습니다.&lt;/p&gt;
&lt;p&gt;다른 프로그래머의 코드를 수정할 때도 비슷합니다. 코드에는 의도와 맥락이 있습니다. 왜 이런 코드가 있는지 물어보고 의도에 맞는 방향으로 작업해야 합니다. 그리고 수정 방향에 대해 얘기하다 보면 생각지 못한 더 좋은 코드가 떠오르거나 어딘가에 있는 동일한 코드를 가져다 쓸 수 있음을 알게 될 수 있어서, 어떤 코드에 대해 수정이 고민이 된다면 담당 프로그래머와 얘기해 보는 것이 좋습니다.&lt;/p&gt;
&lt;h1 id=&quot;데이터-수정은-기획자에게-요청하자&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%88%98%EC%A0%95%EC%9D%80-%EA%B8%B0%ED%9A%8D%EC%9E%90%EC%97%90%EA%B2%8C-%EC%9A%94%EC%B2%AD%ED%95%98%EC%9E%90&quot; aria-label=&quot;데이터 수정은 기획자에게 요청하자 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;데이터 수정은 기획자에게 요청하자&lt;/h1&gt;
&lt;p&gt;&quot;쿠키런: 킹덤&quot;에서는 기획자가 퀘스트 내용이나 쿠키의 능력치 같은 게임 데이터를 작업합니다. 적은 양의 데이터 수정일지라도 어떤 데이터는 서로 연결되어 있어 규칙에 맞게 입력해야 하고, 또 그 데이터를 수정하고 번역 요청을 하는 등 관리가 필요합니다. 따라서 데이터 수정은 담당 기획자에게 요청하는 것이 가장 안전합니다. (담당자한테 물어보자는 위 문단의 내용과도 유사합니다.)&lt;/p&gt;
&lt;h1 id=&quot;네트워크-환경이-항상-최선의-상태가-아닐-수-있음을-고려하자&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%ED%99%98%EA%B2%BD%EC%9D%B4-%ED%95%AD%EC%83%81-%EC%B5%9C%EC%84%A0%EC%9D%98-%EC%83%81%ED%83%9C%EA%B0%80-%EC%95%84%EB%8B%90-%EC%88%98-%EC%9E%88%EC%9D%8C%EC%9D%84-%EA%B3%A0%EB%A0%A4%ED%95%98%EC%9E%90&quot; aria-label=&quot;네트워크 환경이 항상 최선의 상태가 아닐 수 있음을 고려하자 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;네트워크 환경이 항상 최선의 상태가 아닐 수 있음을 고려하자&lt;/h1&gt;
&lt;p&gt;예를 들어서, 클라이언트는 서버와 통신을 할 때, 클라이언트가 서버에 보내는 모든 요청이 실패할 수 있음을 고려해야 합니다. 유저의 네트워크 환경이 다양하기 때문입니다. 요청이 실패하면 경우에 따라 유저가 말을 걸어도 NPC 가 반응을 하지 않는 등 의도하지 않게 게임에 갇힐 수 있습니다. 그래서, 요청이 실패했을 때 화면이 어떻게 나와야 하는지, 다음 요청을 보내도 되는지, 요청이 실패하면 자동으로 다시 요청을 보내야 하는지 등 상황에 따라서 처리해야 합니다.&lt;/p&gt;
&lt;p&gt;예를 들어, 유저가 버튼을 눌러 A 퀘스트를 클리어했다는 요청을 보냈는데 실패했다고 합시다. A 퀘스트는 클리어 통신 후 바로 다음 B 퀘스트를 시작하는 통신 요청을 (A 퀘스트 클리어 통신 성공 여부와 상관없이) 클라이언트에서 하게 되어 있습니다. 설상가상으로 B 퀘스트의 클리어 조건으로 걸려있는 게 A 퀘스트의 완료입니다. 이렇게 되면 유저는 B 퀘스트가 시작되어 있지만 영원히 클리어하지 못하게 될 수 있습니다.&lt;/p&gt;
&lt;p&gt;이를 방지하기 위해 클라이언트는,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;요청 실패 시, 성공할 때까지 재요청하기&lt;/li&gt;
&lt;li&gt;요청 실패 시, 이후 처리를 하지 않도록 리턴하기(그래서 유저가 다시 버튼을 누르게 하기)&lt;/li&gt;
&lt;li&gt;퀘스트를 완료해야 다음 퀘스트를 받을 수 있게 조건 걸기&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;등 팀의 개발 기조에 따라 예외 처리를 둘 수 있습니다.&lt;/p&gt;
&lt;p&gt;이번에는 예방 방법이 아닌, 이미 유저가 통신 실패로 게임에 갇힌 상황에서 클라이언트 코드를 수정하는 방법에 대해 이야기해 보겠습니다.&lt;/p&gt;
&lt;p&gt;예를 들어, 특정 조건을 달성하면 자동으로 요청을 보내는 기능이 있고, 요청이 실패했으나 유저가 따로 액션을 취할 방법이 없는 상황이 되었다고 해봅시다. 이렇게 되면, 분명 기능이 있는데 상호작용을 하지 못해서 좋지 못한 플레이 경험을 얻게 될 수 있습니다. 얼른 문제를 예방하기 위한 방어 코드를 추가하고 구출용 방안을 모색해야 합니다.&lt;/p&gt;
&lt;p&gt;이때 클라이언트가 시도해 볼 수 있는 방안으로는,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;유저가 버튼을 누르면 요청을 보내는 비상 탈출 기능 넣기&lt;/li&gt;
&lt;li&gt;재진입 시 조건을 만족한 상태라면 요청을 자동 다시 보내기&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;등 상황에 맞게 수정을 해볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;이처럼 네트워크는 항상 실패할 수 있다는 전제를 두고 개발해야 합니다.&lt;/p&gt;
&lt;h1 id=&quot;라이브-서비스-중인-리소스는-조심히-다루자&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%9D%BC%EC%9D%B4%EB%B8%8C-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%A4%91%EC%9D%B8-%EB%A6%AC%EC%86%8C%EC%8A%A4%EB%8A%94-%EC%A1%B0%EC%8B%AC%ED%9E%88-%EB%8B%A4%EB%A3%A8%EC%9E%90&quot; aria-label=&quot;라이브 서비스 중인 리소스는 조심히 다루자 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;라이브 서비스 중인 리소스는 조심히 다루자&lt;/h1&gt;
&lt;p&gt;이 주제를 얘기하려면 우선 빌드와 에셋번들에 대한 설명이 간단하게 필요합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;빌드&lt;/strong&gt;는 코드, 프리팹, 텍스처 이미지 등의 리소스를 실행 가능한 파일로 묶은 것으로, 수정 시 전체 빌드를 새로 하고 스토어에 배포하는 과정을 거칩니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;에셋번들&lt;/strong&gt;은 리소스를 빌드와 분리된 외부 파일로, 빌드에 포함되지 않고 게임 내에서 패치 형식으로 다운로드하여 적용할 수 있게 해줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;코드 수정이 필요하다면 빌드 수정이 필요하고, 에셋번들에 들어있는 리소스(프리팹 혹은 이미지 등) 수정이 필요하다면 에셋번들 수정으로 반영이 됩니다. 이 차이를 이해하고 있어야 다음 상황을 예방할 수 있습니다:&lt;/p&gt;
&lt;p&gt;라이브 중인 기능을 수정하면서 어떤 변수가 더 이상 필요 없어 보여 코드상에서 제거했다고 합시다. 이 변수는 에셋번들로 관리되는 프리팹 A에서 사용하고 있습니다. 개발 환경의 코드에서는 더 이상 필요하지 않은 변수이지만, 라이브에 나가 있는 코드와 프리팹 A에서는 새 빌드를 스토어에 올리지 않는 한 이 변수를 사용하고 있을 것입니다. 개발 환경에서 변수가 제거된 프리팹 A&apos;가 다른 라이브 수정이 필요해서 라이브에 나가게 되는 경우, 새 에셋번들의 프리팹 A’에서는 해당 변수가 사라지게 됩니다. 라이브의 코드는 제거된 변수를 참조하려 하므로 의도하지 않은 동작을 하게 됩니다.&lt;/p&gt;
&lt;p&gt;이를 방지하기 위해 변수를 에셋번들의 프리팹에서 쓰고 있지는 않은지 먼저 확인하고 제거해도 될지, 남겨두어야 할지 고려하여 작업을 하는 게 필요합니다.&lt;/p&gt;
&lt;h1 id=&quot;마무리하며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EB%AC%B4%EB%A6%AC%ED%95%98%EB%A9%B0&quot; aria-label=&quot;마무리하며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;마무리하며&lt;/h1&gt;
&lt;p&gt;시행착오는 누구나 겪을 수 있는 일입니다.
중요한 것은 그러한 경험을 혼자 감추지 않고 팀과 공유하며, 재발하지 않도록 개선해나가는 자세입니다.&lt;/p&gt;
&lt;p&gt;이 글을 읽는 여러분도 한 번의 경험에 좌절하지 않고, 그 순간을 기회로 삼아 한 걸음 더 성장해 나가시길 진심으로 바랍니다.
우리 모두의 개발 여정을 응원합니다.
감사합니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[결정론적인 알고리즘]]></title><description><![CDATA[게임에서 결정론적인 알고리즘에 대해 살펴봅니다.]]></description><link>https://tech.devsisters.com/posts/deterministic-algorithm</link><guid isPermaLink="false">https://tech.devsisters.com/posts/deterministic-algorithm</guid><pubDate>Mon, 07 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요.
소프트웨어 엔지니어 권형우입니다.
이 글에서는 게임 개발에서 결정론적인 알고리즘을 살펴봅니다.&lt;/p&gt;
&lt;h1 id=&quot;결정론적인-알고리즘에-대해&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%B0%EC%A0%95%EB%A1%A0%EC%A0%81%EC%9D%B8-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%EC%97%90-%EB%8C%80%ED%95%B4&quot; aria-label=&quot;결정론적인 알고리즘에 대해 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;결정론적인 알고리즘에 대해&lt;/h1&gt;
&lt;p&gt;결정론적인 알고리즘은 같은 맥락에서 같은 입력을 수행했을 때 같은 결과를 냅니다.
다음은 용감한 쿠키의 움직임으로 결정론적인 알고리즘을 표현한 예시입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;용감한 &lt;span class=&quot;token function&quot;&gt;쿠키가&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; 위치에서 출발
&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; 용감한 &lt;span class=&quot;token function&quot;&gt;쿠키가&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;만큼 이동 &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;위치&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; 용감한 &lt;span class=&quot;token function&quot;&gt;쿠키가&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;만큼 이동 &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;위치&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; 용감한 &lt;span class=&quot;token function&quot;&gt;쿠키가&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;만큼 이동 &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;위치&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
용감한 쿠키의 최종 &lt;span class=&quot;token function&quot;&gt;위치는&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;용감한 쿠키가 같은 위치에서 출발해서 같은 양만큼 움직였다면 몇 번을 반복해도 용감한 쿠키의 최종 위치는 같습니다. 게임이 이런 결정론적인 알고리즘으로 구현되어 있다면 같은 입력은 같은 결과를 나타낸다는 근거로 여러 기능들을 구현할 수 있습니다.&lt;/p&gt;
&lt;h1 id=&quot;결정론적인-알고리즘의-구현&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%B0%EC%A0%95%EB%A1%A0%EC%A0%81%EC%9D%B8-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98%EC%9D%98-%EA%B5%AC%ED%98%84&quot; aria-label=&quot;결정론적인 알고리즘의 구현 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;결정론적인 알고리즘의 구현&lt;/h1&gt;
&lt;h2 id=&quot;고정-프레임-게임-루프&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B3%A0%EC%A0%95-%ED%94%84%EB%A0%88%EC%9E%84-%EA%B2%8C%EC%9E%84-%EB%A3%A8%ED%94%84&quot; aria-label=&quot;고정 프레임 게임 루프 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;고정 프레임 게임 루프&lt;/h2&gt;
&lt;p&gt;게임에는 게임 루프가 존재하고 대체로 입력 → 게임 로직 → 화면 그리기 순서로 수행됩니다.
이 단위를 프레임 이라고 하며 초당 프레임이 지나간 횟수를 &lt;code&gt;FPS (Frames Per Second)&lt;/code&gt;라는 단위로 표현합니다.&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 187px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 224.59893048128345%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAtCAIAAAAhntw/AAAACXBIWXMAAAsTAAALEwEAmpwYAAAD0ElEQVR42r1Wa3OaUBD137edfu60/ROZTG0aE6NJ8Rk0iIqggIiiIpo0gooCanrgVpqmqG0/1Nm5s+7dcx/s2d0bWy5XnU6v2+0/l35/OBwauj7SdWM0GmvaUFW1Fz6O48QUpZtK5Todrdlst1oKRlnuXl1RicR1Nkun09lUKsOyDUlSMUscFEX7+rXYbEqxblfDzk9PT9vtNhyxIsNUE4lUPk+fnJypvzl0uwOBF2M4D9aDZbl0ViuXiOetN5ut43jr9dbzNquVB51MwQ3OOIggSD5YFDtYDBPwgJjmnIhlLXzFCsScu+6aLAFnXO0XsG2vMNZqAu5zd1enaZamK2Qsl2sUddtoiOSAe8Hwe3w0w7uFynRqFgrlzeYgmGHqs9kceFFU1I42Ht9DQTgta3Z7WzkCxoED8LRS4c7P0+USm8+XFws7ADPHwdM9xy4W746D4SQIYr3exBUYpsZxTZ4XsS2OvfeDwYpgzGZ2p9MHh4bDyfV1Npm81vWJovRgNM0ZHKJDReKMaSwPhgQesiA0oazXGxjBlr1xJuCQRrDwfLNarZMbhRQ6BH7uIQitWo0jN3oxdRzM8wDX/xEsinKjwf8FeHdnP2y5XIGisuGdQ4fIULmu6+cgBE6wvHr15v37j1BIbhJ7NBhBeny0yqUqW2lUGK7K8jRdRVaxLF+pNGAslarz+ZKs+ws4iO1Tu91jWQ73m88X4LNtL4mA7ThUJkMPBiPsHw1GJWu3VcyZpgWM5dcAazq1sByMSJVDYLBSllXUIJS+k5MvuSz9OX6ZSmXzuVJA+9qRnWWZfHkH5wZJsRC4iTMf3xlgZK+uj/t9fTgcwRUjdNRtw5gkL6nBwIgAk3jgY+4ysc5xrbOzy0+fzlG6AguH6j2bLSK+dhhnUgBIVkmSzPNC8HdD7EcYhiUgtu0XZ77xI6sWixWx/6+s+r/g5w0pANf/KKvIHCm4YAbG16/fvnv3IfyLGnwIDEHvRK2EgA/pdIaiCqAKsWiavheM4jiZfEsmKbRrkLTVapMXABS/9aj9i4sbw7iHWzR4bDyAYS/aBX6EJOiBoGo0PQE2jAe0WOjx+GU8ngQr06ns6WlCknyHUomNTowfO48f0Fygo9HBDyPyAQpqw6GU3O18j57kOKAniI5vs4LueR7SE2Mmc4tj75pJRFbZuVypWGQghQLzQsHjAFUFpe5Q9cQR9klEqPAawudEJpF8Ar32i+8A55+vIRDgeXgO/3bvsD6adqzXG+AFCCYgwqgeRwVueATe3BQkSYmBt3hpqiqen9ofCpw1beC67ndz/qZ07HrJ4AAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/1cc8a3140c0550693df8063d1df65c00/ae6a1/graph-1.webp 187w&quot;
              sizes=&quot;(max-width: 187px) 100vw, 187px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/1cc8a3140c0550693df8063d1df65c00/29aeb/graph-1.png 187w&quot;
            sizes=&quot;(max-width: 187px) 100vw, 187px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/1cc8a3140c0550693df8063d1df65c00/29aeb/graph-1.png&quot;
            alt=&quot;graph 1&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 120px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 350%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAABGCAIAAABDrSOmAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFC0lEQVR42t1YWW8aVxT2b2grRZXSvxBValUp6u+L+hjlIYpQ7HoJeIBgs9rDsBjihViOw+LgsBgwi9mXYV8MuP2YC9djzGb7qR0dj+7cuR9nued894yX/nnCtYQ/u/2zQqE1GMw6HTdXdnbMDKP58sU9BDOMNhKJ12qNUqnM85UZggWNRsvnu5BKVUOwXmcq85VF7Ly5ucE9m80rlbohWKMx8gIY7xqNZq/Xuw+7vu622x0yzmRycPMWDHsw4PmyQqE7O/PBMK/X7/UGLOZ9v//C7fHi8fj4KwUrlSIwPwLbbY6TE/fR4YlEsqFW7yoV2u3t3fV1pcvlZdm9KWDB7H6/3+32cG+32/VaHXZeC1er1W40GsViifg8bnaxWBbANxBykRCQIRFcvV4f91Qqe6tZvc3CYGH1QtGGZoZRD8HEK+jf2tqholaz62sKmWwLnovntVrjygoDyBAMr5LJdCKRpBKLJYolfmODYVlTPl+Mx6/Eb6+uUrVaHaFZmmgeXuBuMlk8njNq7eTcFsVmeJE8MRpNLqeb/NbYgjvgiZo5zuxyeeZrnghmWe7rV9cjwYv6fH8zq9Xaq1d/SSTvO53raSmwRPSILxKtUol//vyXly//FII1mBSvIQpmmW0w7Dgcn+njZM0OxykqUa83a7UcFSSckbWxu3sYiOexTC7XetznQ7BMpvb7w6iNbLYAlhAEgwJyGELGZD6TyaP+PJ7vYhoyl8tPoCHCJPCt2Wx2u937MJQ0hIDT6ewEJoF+FJPfdxEMhPy+UDAYOTg4jkRiHo83Go2bLftgsqk0BP0Wy/7xsRMhXF2Vo+7kjEalMqB67fajw8OTWTSEzewMqKdbrdZRiai7eh3804RF2WwO1hJwOn2XhgoFnrAMSQZkBQmPuJ4wA4bDPZnM3AGPeHtRGpLLNUMwNh00hASAk1QQObiK5MFAPK/TmUBDt5qxB9FoAlGlEgpdIh9WV6V6PQuuDIej4rcQROF/S0NO56NpiHssDeGse/tWolCo+r3+rJ5EoJ5bIWmEunv27Offf/uj0+kiXpgUr5lPQwwjt1qtc2jo9NTz8aNB6IZMVJBwLGiItWEgnscyLPZ9vxh1Q5vq8/NALldIpTJiwekHGZspFIpO5zeZbHsI1j2mG9JOoKFWqzWRhtqDC93QdBqqVGqwAmUgVEI0Hk+i00slM8FAGMVgte6TjZhCQ0XexH0CB4GJQK7oAxAO0BCqNxgMo3eE9qk0BINBOqhQND5QXsiXcOjAI3QDyav0ZSQ2mcMoDY2OIkpDtCEao6ERk2An+YdEW+jDRmDphy2pdBsNHs4hKgaDBQSk1XB6g1k8bzTa1taUm5ujfQbFIh6BQIiKzxdMJFLLy+sajSEWu0L7KX7r94eQKnNpyPxfpCGq+TFgaH4wDVH8hw3Z7i4359NoDEOihe70xx9+evHi12azRQ7Q+3wyy+x37yRqtW4ODYk+Ya5xuHNGm4mzQ/b2jiB4NJs+Gdk9fHiMOb8kTlpU+cryZjaTD4eil5E4OdMGg3AsEU/9vbKZzxfE8bsDBkuhM6LfEmP1gPJGIGaBUWGk9EgmkdQiPoMVQIBTwShU0NBUzQuC8Qg+qVSq5XKVtE8PAQ/IsDrg6mSG7PN8MPUZ2mgB0bRRqfRP8XlmtHHivH79Hp2exXJgtR6KxWZzvHmznMvlJ4DpT4BxXa5vbrd3TDAJAiK8Pb+qFvmHw78NUgz9sQXYRgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/18b64214503294154928635c3c6d2097/31f24/graph-2.webp 120w&quot;
              sizes=&quot;(max-width: 120px) 100vw, 120px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/18b64214503294154928635c3c6d2097/be895/graph-2.png 120w&quot;
            sizes=&quot;(max-width: 120px) 100vw, 120px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/18b64214503294154928635c3c6d2097/be895/graph-2.png&quot;
            alt=&quot;graph 2&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;게임에는 시간에 의존적인 코드 작성이 필요할 수 있습니다.
물체의 이동이나 시간 계산처럼 시간의 흐름에 따라 변화하는 게임 상태를 구현하기 위해 게임 루프에서 프레임이 처리되는 시간을 계산하고 활용합니다. 이를 프레임 처리 시간이라고 하겠습니다.&lt;/p&gt;
&lt;p&gt;프레임 처리 시간은 기기 성능이나 게임 상태에 따라 매 프레임 다른 시간을 가집니다. 프레임 처리 시간이 매 프레임 달라진다면 게임 양상이 비슷하게 흘러갈 수는 있으나 결정론적으로 진행되지는 않습니다. 프레임 처리 시간이 달라짐에 따라 매 프레임 게임 상태에 다른 변화가 생기게 되고 달라진 게임 상태는 다른 결과를 만들 수 있기 때문입니다.&lt;/p&gt;
&lt;p&gt;같은 입력에 대해 매번 같은 결과를 가지게 하는 결정론적인 알고리즘을 위해 게임 로직을 고정된 횟수대로 수행하는 고정 프레임 게임 루프를 사용할 수 있습니다. 고정된 시간 주기로 수행되는 게임 루프를 만들어서 항상 같은 프레임 처리 시간을 가지게 해 기기 성능에 상관없이 같은 결과를 가지게 합니다.&lt;/p&gt;
&lt;p&gt;게임 루프를 고정된 주기로 수행하는 고정 프레임 게임 루프를 사용하면 게임 로직 수행 주기와 화면 갱신 주기에 차이가 생깁니다. 화면 갱신 주기는 하드웨어에서 정해진 주기를 따라가는데 게임 로직 수행 주기는 하드웨어에 관계 없이 고정 프레임 게임 루프에서 정해진 주기대로 수행되기 때문입니다. 화면 갱신 주기에 맞춰 고정 프레임 게임 루프를 사용하기 위해 예측을 구현할 수 있습니다.&lt;/p&gt;
&lt;p&gt;예측이란 마지막으로 갱신된 게임 로직 수행 상태에서 다음 프레임 사이 화면 갱신 주기에 현재 상태를 예측하여 화면에 표현하는 작업입니다. 물체의 움직임이나 애니메이션처럼 화면에 보여지는 것들에 대해 예측을 구현해 고정 프레임과 다른 화면 갱신 주기를 가지는 기기에서도 화면 갱신 주기에 맞춰진 게임 상태를 표현할 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;랜덤&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%9E%9C%EB%8D%A4&quot; aria-label=&quot;랜덤 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;랜덤&lt;/h2&gt;
&lt;p&gt;게임에는 확률적으로 진행되는 것들이 있습니다.
확률적으로 진행되는 요소들을 결정론적으로 구현하기 위해 시드값을 넘겨받아 초기화하는 랜덤을 사용합니다. 시드값이 같다면 생성되는 난수도 같은 결과를 가지게 되어 확률적인 동작들을 결정론적으로 구현할 수 있습니다.&lt;/p&gt;
&lt;h1 id=&quot;마무리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EB%AC%B4%EB%A6%AC&quot; aria-label=&quot;마무리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;마무리&lt;/h1&gt;
&lt;p&gt;게임 코드를 결정론적으로 작성하면 기기 성능에 영향을 덜 받는 결과를 만들 수 있고 개발 환경에 도움을 줄 수 있습니다. 또한 결정론적인 알고리즘이 뒷받침되어야 구현할 수 있는 기능들도 존재합니다.&lt;/p&gt;
&lt;p&gt;게임을 개발할 때 필수적인 요소는 아니지만 상황에 따라 많은 도움을 줄 수 있습니다.
현재 진행중인 프로젝트에 어울리는지 혹은 활용할 여지가 있는지 살펴보고 적용하여 프로젝트를 발전시킬 수 있습니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[데브시스터즈 엔지니어링 데이 - Data 돌아보기]]></title><description><![CDATA[데브시스터즈에서 개최한 두 번째 엔지니어링 데이 - Data를 소개합니다.]]></description><link>https://tech.devsisters.com/posts/2025-engineering-day-data</link><guid isPermaLink="false">https://tech.devsisters.com/posts/2025-engineering-day-data</guid><pubDate>Mon, 23 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요. 데브시스터즈 기술본부에서 조직·기술 문화를 가꾸고 있는 전경아입니다. 이번 글에서는 2025년 5월 29일에 진행했던 &amp;#x3C;제2회 데브시스터즈 엔지니어링 데이 - Data&gt; 에 대해 소개해드리고, 온라인 다시보기 영상도 공유하고자 합니다.&lt;/p&gt;
&lt;h3 id=&quot;행사-소개&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%96%89%EC%82%AC-%EC%86%8C%EA%B0%9C&quot; aria-label=&quot;행사 소개 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;행사 소개&lt;/h3&gt;
&lt;p&gt;‘데브시스터즈 엔지니어링 데이’란, 데브시스터즈에서 마주하는 기술적인 문제를 공유하고, 어떻게 풀어가는지 외부에 경험을 나누는 행사입니다. 본 행사를 통해 개발자 간에 소통할 수 있는 교류의 장을 만들고, 개발자 생태계에 긍정적인 영향을 주기 위해 노력을 기울이고 있습니다.&lt;/p&gt;
&lt;p&gt;데브시스터즈의 서비스를 견고히 지탱해나가는 Infra/SRE 분야에서 일하고 계시는 세 명의 발표자를 모시고 발표와 패널톡을 진행했던 제1회 데브시스터즈 엔지니어링 데이 - Infra/SRE(&lt;a href=&quot;https://tech.devsisters.com/posts/2025-engineering-day-infra-sre/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;링크&lt;/a&gt;)가 지난 2025년 2월 큰 관심 속에 마무리되었는데요, 다음으로 데이터와 관련된 행사도 있으면 좋겠다는 피드백을 반영하여 제2회 엔지니어링 데이는 &apos;데이터&apos;를 핵심 주제로 정했습니다. 데브시스터즈의 게임은 240여개국, 누적 2억 명 이상의 많은 유저분들에게 큰 사랑을 받고 있으며, 그 결과 매일 10 TB 이상의 데이터가 수집됩니다. 이 데이터를 효과적으로 관리하고 게임 기획부터 라이브 서비스까지 다양한 의사결정에 활용하기 위해, 데브시스터즈의 데이터 분야에서는 어떤 문제들을 고민하고 해결해 나가고 있는지 소개해 드리고자 합니다.&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 50%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAIAAAA7N+mxAAAACXBIWXMAAAsTAAALEwEAmpwYAAACaUlEQVR42gXBWU/TAAAA4P4HEyUBTMxQOQZlbOwqa7uuW7f1WrvuYqXr6C7WdbCDjW3CHMdUMnwT8AElMURjjCRqshhjVHggQRMTw4vPxif/hN8HUDhKuhCOQOZpbIHCFjk3bDYPDuomR8dTgluNU2UllBcZTaTaGT7FoAJmUQJYRebbBREgYBvpcpBuhCZwjvInBEZkfZDJFCBcapzLzvMJnoh45zJh78OyfPK4+Xx37dmj+tOtlb1aEuBYliQIAkOtMwaHE185PFne6ZVa6xuN1Z12o9OqldSUlhKrmXg1FVkM+puZSCsXK8TodIgEtNrmcvPBUmndH4h7wknt8m/h57/y5Z/Syz5D+lGrJeT1rOaUiiIWI0zGC+1F0VrQFYWmmTkjUOvsKtrqcu1+LFsP5xrbl1eNix/N71ebF78W5TQ6NcU70XQ4mOapJ0vh3y86V4f1pM9p1I9DNhgIxSWcwJKS6BGSZChxdHa+/+Xb4dn5Qf+jJKWLqtrtdnv7R69O3704PsovRFATSOB4IS372RCwlFVG9Ea7HRJd1ihD9z+8+dR/+/Xz+9PXx52KGqZJFoNXcsr+RjkfF2IkvpmNpYKkFmWcZhDIdw+8bBBGUC3OF3LJe61qtVLUivm6KhejtOmODrfNavJ8iUV6JameSzgtJhaH1yQ6y7uBUZ3OaABhyIqgMOlzBHmsoEUCnPv28M05cMwG6v0YYhkboezTWYGYmRizTE20VSnDOXmXDfA5wAVhNidDlSKy3vRsbwV6uzLiMA4PDAg04oMMhG3m1tCQWX8XMU7euHZdNzykcLgdHHWaDf8Blq/bA1c2TEgAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/fe214c86cc42c22382cb66d184ad4275/33ecc/image1.webp 360w,
/static/fe214c86cc42c22382cb66d184ad4275/263a4/image1.webp 480w,
/static/fe214c86cc42c22382cb66d184ad4275/0c8fb/image1.webp 640w,
/static/fe214c86cc42c22382cb66d184ad4275/521f7/image1.webp 860w,
/static/fe214c86cc42c22382cb66d184ad4275/cbd37/image1.webp 1200w,
/static/fe214c86cc42c22382cb66d184ad4275/548e7/image1.webp 2400w,
/static/fe214c86cc42c22382cb66d184ad4275/56ac2/image1.webp 3168w&quot;
              sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/fe214c86cc42c22382cb66d184ad4275/53918/image1.png 360w,
/static/fe214c86cc42c22382cb66d184ad4275/9aebd/image1.png 480w,
/static/fe214c86cc42c22382cb66d184ad4275/0f09e/image1.png 640w,
/static/fe214c86cc42c22382cb66d184ad4275/3e3fe/image1.png 860w,
/static/fe214c86cc42c22382cb66d184ad4275/64756/image1.png 1200w,
/static/fe214c86cc42c22382cb66d184ad4275/98e2c/image1.png 2400w,
/static/fe214c86cc42c22382cb66d184ad4275/1b65f/image1.png 3168w&quot;
            sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/fe214c86cc42c22382cb66d184ad4275/98e2c/image1.png&quot;
            alt=&quot;image1&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;h3 id=&quot;발표-1---스키마-버전-그리고-로그-안정적인-로그-파이프라인-구축기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B0%9C%ED%91%9C-1---%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%B2%84%EC%A0%84-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EB%A1%9C%EA%B7%B8-%EC%95%88%EC%A0%95%EC%A0%81%EC%9D%B8-%EB%A1%9C%EA%B7%B8-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%B6%95%EA%B8%B0&quot; aria-label=&quot;발표 1   스키마 버전 그리고 로그 안정적인 로그 파이프라인 구축기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;발표 1 - 스키마, 버전 그리고 로그: 안정적인 로그 파이프라인 구축기&lt;/h3&gt;
&lt;iframe width=&quot;560&quot; height=&quot;315&quot; style=&quot;width: 100%; height: 100%; aspect-ratio: 16/9;&quot; src=&quot;https://www.youtube.com/embed/20U67XrjUTk?si=0faag723ozDJc0vc&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerpolicy=&quot;strict-origin-when-cross-origin&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;figcaption&gt;
데이터엔지니어링셀 김규탁님
&lt;/figcaption&gt;
&lt;p&gt;JSON 형식의 로그는 높은 자유도 및 편의성을 제공하지만, 한편으로 다양한 문제도 있습니다. JSON 로그의 문제를 해결하기 위해 도입된 ‘스키마 기반 로그 파이프라인’의 전반적인 설계와, 여러 버전의 스키마를 동시에 지원하기 위한 방법을 공유하고 JSON 로그의 자유도와 복잡함 속에서 어떻게 일관된 구조를 지키고 안정적인 분석 환경을 만들 수 있는지 소개해주셨습니다.&lt;/p&gt;
&lt;h3 id=&quot;발표-2---준실시간-파이프라인-구축기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B0%9C%ED%91%9C-2---%EC%A4%80%EC%8B%A4%EC%8B%9C%EA%B0%84-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EA%B5%AC%EC%B6%95%EA%B8%B0&quot; aria-label=&quot;발표 2   준실시간 파이프라인 구축기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;발표 2 - 준실시간 파이프라인 구축기&lt;/h3&gt;
&lt;iframe width=&quot;560&quot; height=&quot;315&quot; style=&quot;width: 100%; height: 100%; aspect-ratio: 16/9;&quot; src=&quot;https://www.youtube.com/embed/ruheXEh9viQ?si=YuxfPw-ZUHo84pG9&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerpolicy=&quot;strict-origin-when-cross-origin&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;figcaption&gt;
데이터엔지니어링셀 서혜인님
&lt;/figcaption&gt;
&lt;p&gt;데브시스터즈는 게임 런칭과 같은 핵심 순간에 필요한 지표를 신속하게 제공하기 위한 준실시간 데이터 파이프라인을 구축했습니다. 본 세션에서는 기존 시스템의 정확도와 안정성 문제를 해결하기 위해 어떤 고민을 하고 설계했는지 그 과정을 공유하고, 기술적인 이야기뿐만 아니라, 팀과 고객의 요구사항을 반영하여 실용적인 데이터 서비스를 만들어나가는 데브시스터즈의 일하는 가치관도 함께 소개해주셨습니다.&lt;/p&gt;
&lt;p&gt;조금 더 기술적인 내용은 발표에서 QR 코드로 소개해주셨던 기술블로그 포스팅 ‘&lt;a href=&quot;https://tech.devsisters.com/posts/near-real-time/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;지금 매출 얼마인가요&lt;/a&gt;’를 참고해주세요.&lt;/p&gt;
&lt;h3 id=&quot;발표-3---dw는-계획대로-되지-않아---게임-데이터-공통화-이상과-현실-사이에서의-여정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B0%9C%ED%91%9C-3---dw%EB%8A%94-%EA%B3%84%ED%9A%8D%EB%8C%80%EB%A1%9C-%EB%90%98%EC%A7%80-%EC%95%8A%EC%95%84---%EA%B2%8C%EC%9E%84-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B3%B5%ED%86%B5%ED%99%94-%EC%9D%B4%EC%83%81%EA%B3%BC-%ED%98%84%EC%8B%A4-%EC%82%AC%EC%9D%B4%EC%97%90%EC%84%9C%EC%9D%98-%EC%97%AC%EC%A0%95&quot; aria-label=&quot;발표 3   dw는 계획대로 되지 않아   게임 데이터 공통화 이상과 현실 사이에서의 여정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;발표 3 - DW는 계획대로 되지 않아! - 게임 데이터 공통화, 이상과 현실 사이에서의 여정&lt;/h3&gt;
&lt;iframe width=&quot;560&quot; height=&quot;315&quot; style=&quot;width: 100%; height: 100%; aspect-ratio: 16/9;&quot; src=&quot;https://www.youtube.com/embed/IbEugRJe4wA?si=jfmypS6Wgs1lMNIf&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerpolicy=&quot;strict-origin-when-cross-origin&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;figcaption&gt;
DI셀 김가은님
&lt;/figcaption&gt;
&lt;p&gt;데브시스터즈에서 서비스하는 게임이 늘어남에 따라, 모든 게임 데이터를 하나의 통합된 DW로 관리하는 &apos;게임 DW 공통화&apos;를 추진했습니다. 하지만 이상적으로 보였던 계획은 예측하기 어려운 데이터 앞에서 여러 난관에 부딪혔습니다. 이 과정에서 겪은 현실적 문제들과 이를 해결하는 과정, 그리고 앞으로의 과제와 새로운 도전 방향을 소개해주셨습니다.&lt;/p&gt;
&lt;h3 id=&quot;발표-4---level-up-cookierun-kingdom-data&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B0%9C%ED%91%9C-4---level-up-cookierun-kingdom-data&quot; aria-label=&quot;발표 4   level up cookierun kingdom data permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;발표 4 - Level Up! CookieRun: Kingdom Data&lt;/h3&gt;
&lt;iframe width=&quot;560&quot; height=&quot;315&quot; style=&quot;width: 100%; height: 100%; aspect-ratio: 16/9;&quot; src=&quot;https://www.youtube.com/embed/trpydwPvYcw?si=VwqBqEhHRek7JFBB&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerpolicy=&quot;strict-origin-when-cross-origin&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;figcaption&gt;
쿠키런: 킹덤 데이터 이지용님
&lt;/figcaption&gt;
&lt;p&gt;쿠키런: 킹덤 팀은 더 좋은 게임, 더 나은 유저 경험을 만들기 위해 게임 곳곳에서 데이터를 활용하고자 노력해왔습니다. 데이터 기반의 게임 디자인 의사 결정, 업데이트 성과 리포팅과 다양한 라이브 상황 대응 등 게임 곳곳에서 데이터를 사용하는 사례들을 중심으로, 데이터를 더 잘 활용하는 조직이 되기 위해 고민했던 많은 생각과 ‘게임팀에서 데이터를 잘 활용한다는 것’은 무엇일지 공유해주셨습니다.&lt;/p&gt;
&lt;h3 id=&quot;패널톡--채용-부스&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%8C%A8%EB%84%90%ED%86%A1--%EC%B1%84%EC%9A%A9-%EB%B6%80%EC%8A%A4&quot; aria-label=&quot;패널톡  채용 부스 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;패널톡 &amp;#x26; 채용 부스&lt;/h3&gt;
&lt;p&gt;발표가 끝난 뒤에는 약 40분간 참관객과 발표자가 직접 소통하는 패널톡이 이어졌습니다. 참관객들께서 궁금한 점을 말씀해주시면 발표자분들께서 돌아가면서 답변해주셨는데요, ‘현재 사용하는 DW 제품의 선택 이유’, ‘오랜 기간 서비스 중인 게임을 운영할 때 현재와 명세가 다른 레거시 데이터 처리’, ‘GDPR같은 컴플라이언스 이슈’, ‘LLM과 DW 활용’ 등 다양한 질문을 남겨주셨고, 발표자분들께서도 질문 하나하나 진지하게 고민하며 답변해주셨습니다. 서로 인사이트를 나누는 패널톡은 앞선 발표들과는 또 다른 분위기의 의미있는 시간으로 꾸려졌습니다.&lt;/p&gt;
&lt;p&gt;한편, 행사장 한쪽에는 채용 상담 부스가 운영되었습니다. 데브시스터즈 데이터 직무에 관심 있는 참관객들을 위해 채용 담당자와 데이터 직군 스태프들이 직접 궁금증을 해소해주었습니다. &apos;데브시스터즈 데이터 직군은 어떻게 일하는지&apos;, &apos;어떤 역할을 맡게 되는지&apos;, ‘어떤 문화를 가지고 있는지’ 등 깊이 있는 대화를 나누는 자리로, 방문하신 많은 분들께서 긍정적인 피드백을 남겨주셨습니다.&lt;/p&gt;
&lt;h3 id=&quot;마치며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0&quot; aria-label=&quot;마치며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;마치며&lt;/h3&gt;
&lt;p&gt;행사 당일, 소나기가 오는 변덕스러운 날씨에도 많은 분들이 발걸음하셔서 자리를 빛내주셨습니다. 또한, 발표가 끝나고 진행된 패널톡에서 정말 많은 질문을 해주셔서 현장에서의 열기를 다시 한번 느낄 수 있었습니다.&lt;/p&gt;
&lt;p&gt;행사를 마치고 피드백을 정리하는데 행사 개최에 대한 감사부터 발표, 패널톡 등 행사 전반에 대한 긍정적인 후기까지 마음이 정말 따뜻해지는 내용이 많았습니다. 이런 피드백을 보며 저도 이번 행사를 통해 한단계 성장한 느낌이 들었습니다.&lt;/p&gt;
&lt;p&gt;데브시스터즈 엔지니어링 데이 - Data에 큰 성원을 보내주셔서 감사드리며, 남겨주신 피드백을 바탕으로 발표자도, 참석하신 분도 서로 더욱 성장할 수 있도록 더욱 알찬 엔지니어링 데이를 준비하여 돌아올 예정이니 많은 관심 부탁 드립니다. 감사합니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[데브시스터즈 엔지니어링 데이 - Infra/SRE 돌아보기]]></title><description><![CDATA[‘데브시스터즈 엔지니어링 데이’를 소개합니다.]]></description><link>https://tech.devsisters.com/posts/2025-engineering-day-infra-sre</link><guid isPermaLink="false">https://tech.devsisters.com/posts/2025-engineering-day-infra-sre</guid><pubDate>Thu, 03 Apr 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;데브시스터즈에서 마주하고 있는 기술적인 문제를 공유하고 어떻게 풀어가고 있는지 경험을 나누는 행사 ‘데브시스터즈 엔지니어링 데이’를 소개합니다.&lt;/p&gt;
&lt;p&gt;안녕하세요. 데브시스터즈 기술본부에서 조직문화, 개발문화를 가꾸고 있는 전경아입니다. 이번 글에서는 2025년 2월 13일에 진행했던 데브시스터즈 엔지니어링 데이 - Infra/SRE에 대해 소개해드리고, 발표 영상도 공유하고자 합니다.&lt;/p&gt;
&lt;h3 id=&quot;행사-소개&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%96%89%EC%82%AC-%EC%86%8C%EA%B0%9C&quot; aria-label=&quot;행사 소개 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;행사 소개&lt;/h3&gt;
&lt;p&gt;데브시스터즈에서 서비스중인 게임은 유저 여러분들께서 많은 사랑을 보내주시는 만큼 매일 대규모 트래픽이 발생하고 있습니다. 그 속에서 견고히 서비스를 지탱해나가는 Infra/SRE 분야에서 마주치는 기술적인 문제를 어떻게 해결해나가고 있는지 소개해 드리고자 이번 데브시스터즈 엔지니어링 데이 - Infra/SRE 행사를 기획하였습니다.&lt;/p&gt;
&lt;p&gt;데브시스터즈와 같은 게임업계부터 하드웨어 제조사까지 다양한 업계에서 관심있는 분들이 모여 적극적으로 소통해주셔서 저희도 준비한 행사를 잘 진행할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1720px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAIAAABr+ngCAAAACXBIWXMAAAsTAAALEwEAmpwYAAADfklEQVR42iWPW2/bBACF/QqCNm1KW6dJm6sbO87VcWI7iePcnMRxnDgX5+rcvLRpG9aGhYHSTgxRNjZaNqZJaHRi0jZ1LfwDJniZEBpvvPAwDRDaEBK/gkhIR0fn5dOnAyRwo0jDlbi9m8GVHKFk8e0itVUgN0Wil8XbnLcUQYZ19sHtq2LEUWTQD3vp7QrdyxEdwQ+4IDDs0MY8eo6EqglXhoSEICwErf8PnrTstjJn9+989K6cIqwxwjlus1NHJeESQlYgMsUIy5QUw0gvR5Zjjkba2+S8VdZdCNtqLLZZTm5VkjsSLaZjCYYRws48DRej9gaHA3wAqsQdVdbZzfomfV7OYIpI9gtUI+m6OurdONht50KXupmBxFwZ1vZ36jJPdnN+JU9OHQCNmZ0Wrc2o4Wm0LVCKGOwIVDND7PXKI6Va5WMSF79QK7QKXL8qdEqsUk52RGZHFsopEsBsa9oltWpmJkKhxZS/kCLYoL0l18ZXrrUHo8N75/vHJ5M7p+9/fNQq8e2aqMhSVUi0S2m3ywMEMCiEI0Ec5aI4F/PlUnQs6Gm2O7e//f7+D78+/OnF459fnj7/49Gz3zpyLZ+kCukgR2NRGGRQPRDC7QUuXBPZRolvSoJcFSUhefG9y09++ffk6cuTpy+++fH3h8/+Onv+93C40yjyfVnqN4rbVX4yqAP2db3PiYQpvCkkyvmE3MiX8mx3Y+/Wd//cOH1188nro/PXN89fHZ//ORyOFLk5uiDvb8jXP9j9+u6XQDjkMOhNK+8sWHSgx4kIjHdUCuVTkU65uN3rNiVJqde25PqlTYXy+Q/Gm/1cdK+SGGajQowGxAyTZQmDZhHRaxkviurBJI4MatkMTbqsUADH3ObVYjTUELMOyNisZBCLxbYOwWaDWQsClMts0S2bdBoPZExRvoFIGVaWQz7MicAGnU4HLuMozBEeLwLdO/60VSoYdFoMMRtWwDCJA6MNiXKj8yq1RrNm0ptdVpNmYQ5Zt3ocDoNubW52LhGO3P3sk1uHB18dXTvcv3x9cnHSywyrbCLkBx5/Pgp70eC6Om5fXJidmUY9+/aSet64qltaBOdVCyrVks/jYxmaJvyRABmh/DThC/gwAnMDZ1+M5XxcYuBx0eXUq1c1YCTgZ5nQKgi+9cabbjsaJPymNb0NguzW9WnDFsv0Nmw2wWbzf62rCvkiTG/1AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/fa776face779d32aa9c78f6a16f85f26/33ecc/image1.webp 360w,
/static/fa776face779d32aa9c78f6a16f85f26/263a4/image1.webp 480w,
/static/fa776face779d32aa9c78f6a16f85f26/0c8fb/image1.webp 640w,
/static/fa776face779d32aa9c78f6a16f85f26/521f7/image1.webp 860w,
/static/fa776face779d32aa9c78f6a16f85f26/cbd37/image1.webp 1200w,
/static/fa776face779d32aa9c78f6a16f85f26/fcade/image1.webp 1720w&quot;
              sizes=&quot;(max-width: 1720px) 100vw, 1720px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/fa776face779d32aa9c78f6a16f85f26/53918/image1.png 360w,
/static/fa776face779d32aa9c78f6a16f85f26/9aebd/image1.png 480w,
/static/fa776face779d32aa9c78f6a16f85f26/0f09e/image1.png 640w,
/static/fa776face779d32aa9c78f6a16f85f26/3e3fe/image1.png 860w,
/static/fa776face779d32aa9c78f6a16f85f26/64756/image1.png 1200w,
/static/fa776face779d32aa9c78f6a16f85f26/92aea/image1.png 1720w&quot;
            sizes=&quot;(max-width: 1720px) 100vw, 1720px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/fa776face779d32aa9c78f6a16f85f26/92aea/image1.png&quot;
            alt=&quot;데브시스터즈 엔지니어링 데이 진행중인 모습&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;데브시스터즈 엔지니어링 데이 진행중인 모습&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h3 id=&quot;발표-1---데브시스터즈의-장애-대응-원칙과-방법&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B0%9C%ED%91%9C-1---%EB%8D%B0%EB%B8%8C%EC%8B%9C%EC%8A%A4%ED%84%B0%EC%A6%88%EC%9D%98-%EC%9E%A5%EC%95%A0-%EB%8C%80%EC%9D%91-%EC%9B%90%EC%B9%99%EA%B3%BC-%EB%B0%A9%EB%B2%95&quot; aria-label=&quot;발표 1   데브시스터즈의 장애 대응 원칙과 방법 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;발표 1 - 데브시스터즈의 장애 대응 원칙과 방법&lt;/h3&gt;
&lt;iframe width=&quot;560&quot; height=&quot;315&quot; style=&quot;width: 100%; height: 100%; aspect-ratio: 16/9;&quot; src=&quot;https://www.youtube.com/embed/xKmrHDoNzZ0?si=9dSLRnQLLkvjXQPA&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerpolicy=&quot;strict-origin-when-cross-origin&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;figcaption&gt;
인프라셀 셀장 이창원님
&lt;/figcaption&gt;
&lt;p&gt;우리가 생각하는 ‘서비스 장애’란 무엇인지부터, 많은 수의 게임을 운영하는 데브시스터즈의 모든 개발팀에 통용되는 장애 대응 원칙을 크게 장애 대응 원칙과 방법, 알람 티어링 권장 체계, 효과적인 장애 대응 방법 세 부분으로 나누어서 데브시스터즈에서 발생했던 실제 사례와 함께 자세히 소개해주셨습니다.&lt;/p&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1720px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAIAAABr+ngCAAAACXBIWXMAAAsTAAALEwEAmpwYAAADnklEQVR42gGTA2z8AIV1V4R0VYh3WLitmaKaio2HdZuXiY6Kent1YoZ+a42HdnBoVlNJM2FXQF9TO25rW4yWlYiOiXBuYUdBNAB8bE58blN8blKpmX3DuaiooI+KhHOTkoaRlI2EiYOAemiRkop6fnZeWEZbVkVWUD5vc26DlJ18hYNZWVMAa2FJTUc+S0Y/cmZNcGZPdG1bd3Bfsre1kpeSZGZhbnFrgoqIfoiMcnh1cXh1bnVxbHRzd4ybdoqab36HAE9MPkNDRTs7QEZDOkhFOEVFPkpIQ3JwZ2prYy0mIGmHjnK+1XG3zna70Hm8z3290H690H25ynOvyH6+1QBiXUyBfXB9eW5fW09aVklRUEpGR0pLSUFGRTlGPTZklZxV5v9b3vdj3/Vp4fZv7v129v959v98+P9+9v4AXFpSV1ZQV1ZSXFxYXFxZZWdlbnJxVFZVSkxLRT8/aZifT+P7VN30WN30XOH1X+f5Y+n5Z+v5bvL9bu76AGVlX2VmYmRlYmVnY2hpZl9hX2VqaF9iYFNWVU5JSXKdpF3f/GHc9mLd92Xi92fk+Wni92zl+HXt+3nw/QBsbGhsbWlsbWlvcGtwcm9jaGdpcG9mbGpdYV9WUlF+n6Zx3/xz3PV03vZz4Pd14PZ34vd44vZ85feB7PwAdXZydHVxdHVwdnl1dnx6bHFwcnl3bnVzYmppXVxZhaasftLqgMTWfcjafczdfuf8f+j8guj7hen8h+r7AHt8d3t8eHh6dnh9fG11dGdtbnqAf3R7empxcWNjX42tsITP5YWzv4G5x4LJ2IPp/IPq/Ifq/Yrs/Y3q/AB9f3t+fnl9gH1td3ZqY1x/fHJ1f355gH5hb2hjY1+TsbKJ0OSNvsqIv8yJzt2L6/2M6/2Q6/2S7f6U7P0AmYFxjoV6b3Z1dHx3joJ4qKWfjpaXYXFmRmZOU11Sp8G+leHwltHfmdnnnuDul+P0l+LymeX2m+T0nub0ALyQdH1mWWNkYnB2aHZpVZ+ViHF2cjRJOjhUQEVWRXR6eIGcoK/IxIunp22JjpOsqZauq5avq5ewrJStqwA5ODpoVEpcVVBRUk92VkNlTkE1NzQ+VUI0TT5HTEciICRza2RpZmFCOjkjGx1+dmmcloWTjX2blIOCe28Ab1VEf2teJykvXVlUyotaNyckLDEyMkk5VVJFQjU0Pjo3V1RRDxIbTjw1m3JahH5ynJuMmJaHnZuLV1dVRiDNgpI9tz4AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/9a5a4849516ff2d0bde724897ef6c127/33ecc/image2.webp 360w,
/static/9a5a4849516ff2d0bde724897ef6c127/263a4/image2.webp 480w,
/static/9a5a4849516ff2d0bde724897ef6c127/0c8fb/image2.webp 640w,
/static/9a5a4849516ff2d0bde724897ef6c127/521f7/image2.webp 860w,
/static/9a5a4849516ff2d0bde724897ef6c127/cbd37/image2.webp 1200w,
/static/9a5a4849516ff2d0bde724897ef6c127/fcade/image2.webp 1720w&quot; sizes=&quot;(max-width: 1720px) 100vw, 1720px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/9a5a4849516ff2d0bde724897ef6c127/53918/image2.png 360w,
/static/9a5a4849516ff2d0bde724897ef6c127/9aebd/image2.png 480w,
/static/9a5a4849516ff2d0bde724897ef6c127/0f09e/image2.png 640w,
/static/9a5a4849516ff2d0bde724897ef6c127/3e3fe/image2.png 860w,
/static/9a5a4849516ff2d0bde724897ef6c127/64756/image2.png 1200w,
/static/9a5a4849516ff2d0bde724897ef6c127/92aea/image2.png 1720w&quot; sizes=&quot;(max-width: 1720px) 100vw, 1720px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/9a5a4849516ff2d0bde724897ef6c127/92aea/image2.png&quot; alt=&quot;image2&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;p&gt;발표에서 QR 코드로 소개해주셨던 기술블로그 포스팅도 함께 공유 드립니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://tech.devsisters.com/posts/incident-management-principles/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;🔗 https://tech.devsisters.com/posts/incident-management-principles/&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;발표-2---dalgona-쿠버네티스-인프라-표준화의-여정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B0%9C%ED%91%9C-2---dalgona-%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%EC%9D%B8%ED%94%84%EB%9D%BC-%ED%91%9C%EC%A4%80%ED%99%94%EC%9D%98-%EC%97%AC%EC%A0%95&quot; aria-label=&quot;발표 2   dalgona 쿠버네티스 인프라 표준화의 여정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;발표 2 - &lt;strong&gt;Dalgona: 쿠버네티스 인프라 표준화의 여정&lt;/strong&gt;&lt;/h3&gt;
&lt;iframe width=&quot;560&quot; height=&quot;315&quot; style=&quot;width: 100%; height: 100%; aspect-ratio: 16/9;&quot; src=&quot;https://www.youtube.com/embed/oQQIauhvw7w?si=mPUOvxSuaxveUDxY&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerpolicy=&quot;strict-origin-when-cross-origin&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;figcaption&gt;
인프라셀 용찬호님
&lt;/figcaption&gt;
&lt;p&gt;데브시스터즈는 다양한 게임을 많은 수의 쿠버네티스 클러스터에서 관리하고 있습니다. 그러나 클러스터 수가 점차 증가하면서 쿠버네티스 레벨에서의 권한 관리가 어려워지거나 클러스터 유지보수 비용이 증가하는 문제가 있었습니다. 또한, 중앙 인프라 조직은 초기에 AWS 에서 로드 밸런서로 Classic Load Balancer(ELB)를 사용하다가 점차 Global Accelerator(GA)와 Network Load Balancer(NLB)로 전환했습니다. 그러나 이러한 최신 기술과 노하우가 조직 전체에 효과적으로 공유되지 않아, 게임 라이브팀은 여전히 기능이 제한적인 ELB를 계속 사용하는 등 노하우가 제대로 전파되지 않는 등의 상황도 발생하였습니다. 이런 현상을 해결하고 중앙 인프라 조직이 전사 쿠버네티스 클러스터를 효율적으로 관리하기 위해 인프라 형상을 표준화한 이야기와 함께 그 과정에서 어떤 고민을 하였는지, 데브시스터즈의 플랫폼 엔지니어링은 앞으로 어떤 계획이 있는지 소개해주셨습니다.&lt;/p&gt;
&lt;h3 id=&quot;발표-3---게임팀을-위한-궁극의-배포-시스템-만들기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B0%9C%ED%91%9C-3---%EA%B2%8C%EC%9E%84%ED%8C%80%EC%9D%84-%EC%9C%84%ED%95%9C-%EA%B6%81%EA%B7%B9%EC%9D%98-%EB%B0%B0%ED%8F%AC-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EB%A7%8C%EB%93%A4%EA%B8%B0&quot; aria-label=&quot;발표 3   게임팀을 위한 궁극의 배포 시스템 만들기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;발표 3 - 게임팀을 위한 궁극의 배포 시스템 만들기&lt;/h3&gt;
&lt;iframe width=&quot;560&quot; height=&quot;315&quot; style=&quot;width: 100%; height: 100%; aspect-ratio: 16/9;&quot; src=&quot;https://www.youtube.com/embed/elyUxt3W9qo?si=iFs93TwflkB39WNE&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerpolicy=&quot;strict-origin-when-cross-origin&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;figcaption&gt;
인프라셀 노정훈님
&lt;/figcaption&gt;
&lt;p&gt;Argo Workflow로 기획자와 서버 개발자 모두 중앙 인프라 조직의 도움을 받지 않고 사용할 수 있는 배포 시스템을 개발한 과정을 소개해주셨습니다. 기존 배포 툴은 도커 이미지 태그 형태가 고정되고 서버팀에서 환경 변수 수정이 직접 불가능하여 중앙 인프라 조직의 개입이 필수였고, 차트 설치 한 단계로 모든 배포를 해야 하여 Hook으로 제한적인 분리만 가능한 등의 한계점이 있었는데요, 이런 어려움 속에서 어떤 점을 고려하여 데브시스터즈만의 배포 시스템 ‘플랑’을 개발했는지 공유해주셨습니다.&lt;/p&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1720px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAIAAABr+ngCAAAACXBIWXMAAAsTAAALEwEAmpwYAAADe0lEQVR42hXQy2/aBgDHcZ+qaMkparZ1bbokDaE8YxLeeAkPY8wjgI1fgLEBQ3gUjHk4vBJoCClZXg3JLtu07LZKk6Zq0S47dJq0w7bbTpN6n/Z3LLv8bl/9pA9QTYV6L5LfXB19ezO63me/POQuusyrFjWRqUmLOmvHJzI5eBESU85kUEd5FClUWcJ0FVx7LpNAu4i3C5jIeYcV7HOZHkn46R593mHum+MmOW4SIykm5/wlGiqRpjZnl5hNFlnlUUU5BgKHNaxXCh83qaM6WePdBdpWTW33ioH7z3Ej1i/v1NOeGucaV9CrduyqS0+78Qbr4HxrFQIErvcTZy3itBUbieFuwS+mXAJuEvCNTNTY4J3dXZ8s+ER266ybOmym2iXq9WFlOkiXY4Y9zgGctpnLXuKiQ5/JxFgM7QmwgG3SPv2GdpVCtCXK/MUr6fZ6NKwn9qvk/Y4a9D2BLHglzgU0kvZWxl1PwwdVbNxk9ss7/SLaLhP73VYh4ZF4Z4Xd7pTw04E4lOIS7y4yjiJtLTHWLLYJxP1gjkHyMUuZgQ4qUZFzVdntdh65l3v5P0ewzruzuJlE9EzAlAy7BvVsgXYLuJlCDUAmYk9jUCZiJX0bQUhJOZYSsIpE1jEYtBsUGGwQSGc+7mHDNiGByFLoZQcddkkxj6OQBuCiHp1GbbXYlFrjk48WhrgpYlM9WXy68OHH8/PzBvWKBVQFYLvLZj6fpt7+O/rq/dVP/1R7LXrbZgJ2gv7lp49tBlXIDYHq5wKyYVQtryw+Nj3/1GE1IrBLsbKEOi0cHfnuzeVvf//6w1+///n+bnoi4zgBUAFndEslMluED9Jr1Pq1ZSyIEmFUo1To1pYauXC/UeTJEKjTomi0Xyv067uv+5VJr1pIIIAD2u5I5aoQM+tW83xyXa385NEjAo8EUHhNqYRtup6Uy3Pk2vIi6vWMDw9KleazZwqGQIvpMMDjnnd33//x7sf+Xv34aJTNZrcsIOq0RnyfBf1e5xY0PelyTHRm5gPY7artskwQyrA0E9tBPBAwaAq3N6d3b24vzydpnkN9qFjK9auslIlOjgaw13tzMU5gXgAAZmfnjKCaxX1fn3Umo0RXDgBZlho2dwd7YoZPWi0WUA/q9YZ1nT4YCJSLhVAw5IXdDBF++HDhwYOZudk507rm+oT/+a30y538H6MlOs8h/SKRAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/960c1cac1c1783d3b6d8124795aa1348/33ecc/image3.webp 360w,
/static/960c1cac1c1783d3b6d8124795aa1348/263a4/image3.webp 480w,
/static/960c1cac1c1783d3b6d8124795aa1348/0c8fb/image3.webp 640w,
/static/960c1cac1c1783d3b6d8124795aa1348/521f7/image3.webp 860w,
/static/960c1cac1c1783d3b6d8124795aa1348/cbd37/image3.webp 1200w,
/static/960c1cac1c1783d3b6d8124795aa1348/fcade/image3.webp 1720w&quot; sizes=&quot;(max-width: 1720px) 100vw, 1720px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/960c1cac1c1783d3b6d8124795aa1348/53918/image3.png 360w,
/static/960c1cac1c1783d3b6d8124795aa1348/9aebd/image3.png 480w,
/static/960c1cac1c1783d3b6d8124795aa1348/0f09e/image3.png 640w,
/static/960c1cac1c1783d3b6d8124795aa1348/3e3fe/image3.png 860w,
/static/960c1cac1c1783d3b6d8124795aa1348/64756/image3.png 1200w,
/static/960c1cac1c1783d3b6d8124795aa1348/92aea/image3.png 1720w&quot; sizes=&quot;(max-width: 1720px) 100vw, 1720px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/960c1cac1c1783d3b6d8124795aa1348/92aea/image3.png&quot; alt=&quot;image3&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;h3 id=&quot;마치며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0&quot; aria-label=&quot;마치며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;마치며&lt;/h3&gt;
&lt;p&gt;데브시스터즈 엔지니어링 데이에 보내주신 관심과 성원 감사드리며, 발표부터 네트워킹 세션까지 참석해주신 분들과 함께 현장에서 직접 교류할 수 있어 더욱 의미있는 시간이었습니다.&lt;/p&gt;
&lt;p&gt;앞으로 Infra/SRE 외에도 다양한 분야의 엔지니어링 데이를 진행할 예정이니, 계속해서 많은 관심 가져주시길 바라며 이만 마치겠습니다.&lt;/p&gt;
&lt;p&gt;감사합니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[새로운 팀의 코드베이스 적응기: 내 코드로 만들어가는 과정]]></title><description><![CDATA[이직 / 팀 이동 후 어색한 코드를 내 코드로 녹여내기까지의 여정을 공유합니다.]]></description><link>https://tech.devsisters.com/posts/adapt-to-new-codebase</link><guid isPermaLink="false">https://tech.devsisters.com/posts/adapt-to-new-codebase</guid><pubDate>Wed, 05 Mar 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;서론&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%84%9C%EB%A1%A0&quot; aria-label=&quot;서론 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;서론&lt;/h2&gt;
&lt;p&gt;안녕하세요! 저는 쿠키런: 마녀의 성 서버를 담당하고 있는 5년 차 게임 서버 개발자 주윤아입니다.&lt;/p&gt;
&lt;p&gt;이직을 고민하고 계신가요? 새로운 팀에 합류하거나, 전혀 다른 코드베이스에서 일하게 된다는 건 많은 개발자에게 큰 도전으로 다가올 수 있습니다. 저 역시 같은 고민을 했습니다. 전 직장에서는 하나의 프로젝트에 4년 동안 몸 담으며 처음부터 끝까지 모든 과정을 경험했기 때문에, 새로운 환경에서 타인의 코드를 이해하고, 빠르게 적응해 성과를 낼 수 있을지 걱정이 컸습니다.&lt;/p&gt;
&lt;p&gt;하지만 그런 두려움에도 불구하고, 저는 데브시스터즈에서 1년 동안 두 개의 프로젝트에 참여해 각기 다른 코드베이스를 학습하고, 저의 것으로 만드는 과정을 거쳤습니다. 그 과정에서 크고 작은 기능 구현과 업데이트를 성공적으로 완료 하며 팀에 완전히 녹아들 수 있었죠.&lt;/p&gt;
&lt;p&gt;본론에 들어가기 앞서, 첫 번째 팀이 아닌 두 번째 팀인 쿠키런: 마녀의 성 코드베이스 적응 과정에 집중하려고 합니다. 그 이유는, 두 번째 팀에서 경험한 것이 저에게 더욱 큰 도전이었기 때문입니다. 이전 팀에서는 익숙했던 C# 기반의 객체 지향 프로그래밍 언어를 사용했지만, 두 번째 팀에서는 함수형과 객체 지향 멀티 패러다임 언어인 Scala 를 처음 접하게 되었고, 더불어 새로운 아키텍처를 학습해야 했습니다. 또한 당시 혼자서 학습을 해야 했던 특수한 상황이었기에 그 과정은 더욱 까다로웠습니다.&lt;/p&gt;
&lt;p&gt;이번 글에서는 제가 새로운 코드베이스에 적응하고, 그것을 내 것으로 만드는 과정에서 느꼈던 고민들과 배운 점들을 공유하려 합니다. 이직 후 새로운 팀에 적응하는 것이 막연하게 느껴지는 주니어 개발자들에게 이 글이 작은 도움이 되길 바랍니다.&lt;/p&gt;
&lt;h2 id=&quot;본론&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B3%B8%EB%A1%A0&quot; aria-label=&quot;본론 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;본론&lt;/h2&gt;
&lt;h3 id=&quot;공부의-순서-정하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B3%B5%EB%B6%80%EC%9D%98-%EC%88%9C%EC%84%9C-%EC%A0%95%ED%95%98%EA%B8%B0&quot; aria-label=&quot;공부의 순서 정하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;공부의 순서 정하기&lt;/h3&gt;
&lt;p&gt;새로운 팀에 합류하면서 낯선 코드 베이스와 프로그래밍 언어를 어떻게 효율적으로 익힐 수 있을지 고민이 컸습니다. 사람 마다 학습 방법이 다르겠지만, 저처럼 정석보다는 꼼수에 의존해온 사람에게는 특히나 공부의 순서를 정하는 것이 어려웠습니다. 제가 정한 학습 흐름은 크게 세 가지 단계로 나눌 수 있었습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;문법 학습 : 새로운 언어의 기본 문법을 익히는 것이 가장 중요했습니다. 이 과정에서 사내에서 모든 스칼라 개발자가 완독했다는 빨간 책이라는 scala 관련 서적을 참고했습니다.
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://jpub.tistory.com/477&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;스칼라로 배우는 함수형 프로그래밍 (제이펍, 2015)&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;프레임워크 학습 : 서버 개발을 위해 사용되는 ZIO 프레임워크의 구조와 동작 방식을 파악했습니다.&lt;/li&gt;
&lt;li&gt;상업 코드 분석 : 실무에서 작성된 코드를 분석하고 작은 퀘스트들을 수행하며 경험치를 쌓았습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;기존에 알고 있던 객체지향 언어들과 달리 새로운 패러다임을 익히는 과정은 고통스럽기도 했지만, 사내 상업 코드를 보며 월급을 받으며 새로운 언어를 학습할 수 있다니, 완전 럭키비키🍀 였습니다.&lt;/p&gt;
&lt;h3 id=&quot;새로운-팀의-코드-아키텍처&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%83%88%EB%A1%9C%EC%9A%B4-%ED%8C%80%EC%9D%98-%EC%BD%94%EB%93%9C-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98&quot; aria-label=&quot;새로운 팀의 코드 아키텍처 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;새로운 팀의 코드 아키텍처&lt;/h3&gt;
&lt;p&gt;현재 팀의 코드베이스는 제가 익숙했던 기본 아키텍처와는 달랐습니다. 헥사고날 아키텍처와 도메인 주도 설계(DDD)를 기반으로 한 방식은 개념으로만 알고 있었고, 실제로 적용해 본 적은 없었습니다.&lt;/p&gt;
&lt;p&gt;처음에는 낯선 함수형 언어와 새로운 아키텍처에 당황하기도 했습니다. 마치 대학 시절 객체 지향 프로그래밍 언어를 처음 배웠을 때의 혼란스러움이 떠오르더군요. 그 당시 “그래서 인터페이스가 뭔데?”라는 질문을 던졌다면, 지금은 “모나드가 뭔데?”라고 묻고 있는 상황이죠. 이러한 개념들은 여전히 머리로는 이해하기 어렵지만, 경험을 통해 가슴으로 이해하게 되는 순간들이 있었습니다.&lt;/p&gt;
&lt;h3 id=&quot;낯선-코드-속에서-길-찾기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%82%AF%EC%84%A0-%EC%BD%94%EB%93%9C-%EC%86%8D%EC%97%90%EC%84%9C-%EA%B8%B8-%EC%B0%BE%EA%B8%B0&quot; aria-label=&quot;낯선 코드 속에서 길 찾기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;낯선 코드 속에서 길 찾기&lt;/h3&gt;
&lt;p&gt;새로운 코드베이스에 적응하기 위해 협업 미팅을 통해 약 일주일간 스칼라 문법과 프레임워크를 학습했고, 그 후 3주간 실제 퀘스트를 진행했습니다. 이 과정에서 가장 많이 사용한 방법은 코드 분석과 디버깅이었습니다. 디버깅 방법도 과거의 저와 달라졌습니다. 예전에는 오로지 &lt;code&gt;printf&lt;/code&gt; 디버깅만으로 문제를 해결했었는데, 이제는 디버거 없이는 작업하기 어려운 상황이 되었죠. 물론 콜스택이 많이 쌓이는 경우에는 여전히 &lt;code&gt;printf&lt;/code&gt;를 찍어가며 분석하는 경우도 있긴 합니다. 😊&lt;/p&gt;
&lt;p&gt;새로운 언어, 새로운 패러다임, 새로운 프로젝트이기 때문에 모든 것이 낯설었습니다. 특히, 이미 출시된 게임 프로젝트의 방대한 코드와 구조를 빠르게 파악하는 것은 쉬운 일이 아니었습니다.&lt;/p&gt;
&lt;p&gt;그래도 게임 서버 개발자로서의 핵심 역할은 유사할 것이라는 믿음으로, 우선 &lt;code&gt;SignIn&lt;/code&gt; 기능에 집중했습니다. 로그인은 어떤 게임이든 필수적인 통신이며, 이 프로토콜을 완벽히 이해하면 팀의 구현 프로세스를 파악하는 데 도움이 될 거라 생각했습니다. 기획 문서를 파고들 필요 없이, 한 가지 프로토콜만 잘 이해해도 프로젝트 전반에 대한 감을 잡을 수 있으니까요!&lt;/p&gt;
&lt;h3 id=&quot;첫-번째-퀘스트--코드-분석과-기본-익히기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B2%AB-%EB%B2%88%EC%A7%B8-%ED%80%98%EC%8A%A4%ED%8A%B8--%EC%BD%94%EB%93%9C-%EB%B6%84%EC%84%9D%EA%B3%BC-%EA%B8%B0%EB%B3%B8-%EC%9D%B5%ED%9E%88%EA%B8%B0&quot; aria-label=&quot;첫 번째 퀘스트  코드 분석과 기본 익히기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;첫 번째 퀘스트 : 코드 분석과 기본 익히기&lt;/h3&gt;
&lt;p&gt;가장 먼저 한 작업은 서버 컴파일 및 서버 실행이었습니다.&lt;/p&gt;
&lt;p&gt;모든 개발의 첫 단계는 프로그램이 로컬 환경에서 컴파일되고 실행되는 것입니다. 팀마다 개발환경이 다르기에 이 과정에서 많은 시간을 할애하게 되었습니다. 로컬 문제인줄 알았던 것이 팀 문제였고, 팀 문제인 줄 알았던 것이 로컬 문제로 판명되는 재밌는 상황도 펼쳐졌죠. 일반적이지 않은 의존성 문제나 데이터베이스 프로토콜화 변환이 익숙지 않아 처음에 많이 헤매기도 했습니다.&lt;/p&gt;
&lt;h3 id=&quot;두-번째-퀘스트--로그인-보상-기능-구현&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%91%90-%EB%B2%88%EC%A7%B8-%ED%80%98%EC%8A%A4%ED%8A%B8--%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EB%B3%B4%EC%83%81-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84&quot; aria-label=&quot;두 번째 퀘스트  로그인 보상 기능 구현 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;두 번째 퀘스트 : 로그인 보상 기능 구현&lt;/h3&gt;
&lt;p&gt;로그인 시 보상을 주는 기능은 단순해 보이지만, 함수형 프로그래밍 패러다임에 익숙해질 좋은 기회였습니다. 이 퀘스트의 핵심은 이벤트를 발생시키는 &lt;code&gt;liftEvent&lt;/code&gt; 함수의 사용이었는데, 이는 함수형 프로그래밍에서 사이드 이펙트를 관리하는 일반적인 패턴 중 하나입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://tech.devsisters.com/posts/functional-programming-in-scala-01/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;스칼라로 배우는 함수형 프로그래밍 책을 읽어봅시다 1편&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;liftEvent&lt;/code&gt; 는 이벤트를 처리하기 위한 사이드 이펙트를 함수형 패러다임 내에서 안전하게 발생시키기 위해 사용됩니다. 함수형 패러다임의 기본 개념인 &lt;code&gt;함수는 하나의 기능을 수행해야 한다&lt;/code&gt; 는 원칙을 따르면서도, 하나의 함수 내에서 이벤트 발생이나 로깅 등의 사이드 이펙트를 처리할 수 있는 방법입니다. 즉, 순수한 계산의 흐름 속에서 이벤트 발생과 같은 부수 효과를 끼워 넣을 수 있는 형태로 구현 되는 것이죠.&lt;/p&gt;
&lt;p&gt;Scala 에서 암묵적 매개변수(implicit parameter)가 컴파일러에 의해 자동으로 전달되는데, &lt;code&gt;liftEvent&lt;/code&gt; 와 같은 함수에서 이러한 암묵적 매개변수가 함수의 동작에 필요한 Context 를 제공할 때 특히 유용합니다. &lt;code&gt;liftEvent&lt;/code&gt; 함수는 보상 이벤트를 처리하는 데 필요한 환경을 암묵적으로 받기 때문에, 코드가 간결해지고, 함수형 설계 원칙을 유지하면서도 필요한 사이드 이펙트를 효과적으로 관리할 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;세-번째-퀘스트--grpc-프로토콜-이해와-연습&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%84%B8-%EB%B2%88%EC%A7%B8-%ED%80%98%EC%8A%A4%ED%8A%B8--grpc-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C-%EC%9D%B4%ED%95%B4%EC%99%80-%EC%97%B0%EC%8A%B5&quot; aria-label=&quot;세 번째 퀘스트  grpc 프로토콜 이해와 연습 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;세 번째 퀘스트 : gRPC 프로토콜 이해와 연습&lt;/h3&gt;
&lt;p&gt;운영툴 프로토콜에서 gRPC 생성 후 print 찍기는 간단한 수정 작업이었지만, Scala 에서 타입 일관성을 유지하는 스칼라 로직의 특징을 익히는 데 중요한 역할을 했습니다.&lt;/p&gt;
&lt;p&gt;특히, &lt;code&gt;ZIO&lt;/code&gt; 에서 사용되는 &lt;code&gt;for&lt;/code&gt; 표현식에서의 타입 불일치 문제를 이해하고 프로토콜 작업의 기본 구조를 익혔습니다.&lt;/p&gt;
&lt;p&gt;scala 에서 &lt;code&gt;println&lt;/code&gt; 과 같은 사이드 이펙트는 &lt;code&gt;zio.succeed&lt;/code&gt; 로 감싸서 ZIO 모나드 형태로 만들어야 합니다. &lt;code&gt;zio.succeed(println(&quot;example&quot;))&lt;/code&gt; 는 &lt;code&gt;ZIO[Any, Nothing, Unit]&lt;/code&gt; 타입의 값을 변환하여 안전하게 사이드 이펙트를 처리하는데, 이는 함수형 프로그래밍의 원칙을 유지하면서도 로그 출력과 같은 작업을 가능하게 합니다. 특히 &lt;code&gt;for&lt;/code&gt; 표현식에서는 &lt;code&gt;flatMap&lt;/code&gt;, &lt;code&gt;map&lt;/code&gt;, &lt;code&gt;withFilter&lt;/code&gt; 등의 연산이 순차적으로 올바른 타입을 반환해야 하므로, 타입 불일치를 방지하기 위해 각 연산에 맞는 형태를 유지하는 것이 중요합니다.&lt;/p&gt;
&lt;h3 id=&quot;네-번째-퀘스트--운영-grpc-에서-운영로그-남기기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%84%A4-%EB%B2%88%EC%A7%B8-%ED%80%98%EC%8A%A4%ED%8A%B8--%EC%9A%B4%EC%98%81-grpc-%EC%97%90%EC%84%9C-%EC%9A%B4%EC%98%81%EB%A1%9C%EA%B7%B8-%EB%82%A8%EA%B8%B0%EA%B8%B0&quot; aria-label=&quot;네 번째 퀘스트  운영 grpc 에서 운영로그 남기기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;네 번째 퀘스트 : 운영 gRPC 에서 운영로그 남기기&lt;/h3&gt;
&lt;p&gt;gRPC로 로그를 남기는 작업에서는 특히 로그의 중요성을 새삼 느꼈습니다. 게임 운영 중 발생하는 CS 요청이나 버그 분석을 위해서 꼼꼼한 로그 작성은 필수적입니다. 처음에는 분석 로그 구조와 삽입 위치를 몰라 디버깅이 어려웠지만, 지금은 분석 로그 클래스 계층을 상속받아 &lt;code&gt;liftEvent&lt;/code&gt; 로 필요한 시점에 이벤트를 발생시키고, 이를 처리하는 함수에서 케이스를 추가하는 패턴을 익혔습니다.&lt;/p&gt;
&lt;p&gt;이제까지의 과정은 새로운 코드베이스와 프로그래밍 패러다임에 적응하며 프로젝트 전반에 걸쳐 필요한 기초를 다지기 위한 시간이었습니다.&lt;/p&gt;
&lt;p&gt;Scala 와 함수형 프로그래밍에 익숙해지기 위해 첫 주는 문법과 개발 환경을 설정하는 데 집중했고, 이후 실제 퀘스트를 수행하며 코드를 분석하고 로컬 환경에서 컴파일과 실행을 반복했습니다. 특히 &lt;code&gt;SignIn&lt;/code&gt; 기능 구현을 통해 기초 프로토콜의 흐름을 파악하고, 이벤트 처리를 위한 &lt;code&gt;liftEvent&lt;/code&gt; 를 활용하면서 사이드 이펙트를 제어하는 패턴도 실습했습니다.&lt;/p&gt;
&lt;p&gt;약 3주 간의 집중적인 학습과 디버깅 과정을 통해 새로운 언어와 시스템에 적응하고, 코드 베이스의 핵심 구조와 팀의 개발 방식을 이해하는 중요한 발판을 마련할 수 있었습니다.&lt;/p&gt;
&lt;h3 id=&quot;작은-피처에서-큰-피처까지&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9E%91%EC%9D%80-%ED%94%BC%EC%B2%98%EC%97%90%EC%84%9C-%ED%81%B0-%ED%94%BC%EC%B2%98%EA%B9%8C%EC%A7%80&quot; aria-label=&quot;작은 피처에서 큰 피처까지 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;작은 피처에서 큰 피처까지&lt;/h3&gt;
&lt;p&gt;팀 이동을 성공적으로 마친 뒤, 작업의 첫 단계에서는 광고 시스템 관련 작은 피처를 수정했습니다. 비록 간단한 수정 작업이었지만, 프로토콜 추가 및 기획 데이터 테이블 연동 과정에서 코드베이스의 구조를 이해하는 데 많은 도움이 되었습니다. 이후 조금 더 큰 작업인 &lt;strong&gt;쿠키 퍼즐 챌린지&lt;/strong&gt; 작업에 착수했을 때는 더 복잡한 로직을 다뤄야 했고, 아키텍처 간의 상호작용을 이해하는 것이 필수적이었습니다.&lt;/p&gt;
&lt;p&gt;작은 피처들을 작업하면서, 제가 한 동안 사내 개발자들 분께 입버릇 처럼 하는 말이 있었죠. “작업 하긴 하는데, 머리가 아니라 본능대로 손이 코드를 짜는 기분이에요-”&lt;/p&gt;
&lt;p&gt;프로젝트 내에 원하는 로직을 조립하는 것에는 익숙해 졌지만, 코드 전체 흐름은 전혀 보이지 않던 상황이었습니다.&lt;/p&gt;
&lt;p&gt;점차 작업의 난이도가 높아졌고, 특히 &lt;strong&gt;유적 탐험 이벤트&lt;/strong&gt; 작업은 워크타임만 약 2-3주가 걸렸습니다. 이 작업을 통해 코드베이스의 큰 그림을 완전히 파악할 수 있었습니다. 그 이전에는 나무는 보였지만 숲은 보이지 않았던 상황이었다면, 이제는 큰 틀 안에서 작은 피처들을 이해하고 구현할 수 있게 되었습니다.&lt;/p&gt;
&lt;p&gt;이렇게 차근차근 작은 작업부터 큰 프로젝트까지 경험하면서, 팀의 코드를 내 것으로 만들 수 있었고, 새로운 환경에 대한 두려움도 서서히 사라졌습니다.&lt;/p&gt;
&lt;h3 id=&quot;도전과-극복&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8F%84%EC%A0%84%EA%B3%BC-%EA%B7%B9%EB%B3%B5&quot; aria-label=&quot;도전과 극복 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;도전과 극복&lt;/h3&gt;
&lt;p&gt;함수형 프로그래밍 패러다임 자체가 낯설어 개념 이해에 시간이 많이 소요되었습니다. 기존에 익숙했던 객체지향 패러다임과 달리, 스칼라는 문법이 복잡하여 한 가지를 이해했다 생각해도 다른 부분을 보는 순간 백지가 된 기분이었습니다.&lt;/p&gt;
&lt;p&gt;가장 어려웠던 점은 문서화의 부족이었습니다. 게임 프로젝트 팀이라면 다들 공감하실 텐데, 초기에는 탄탄한 아키텍처와 문서화 작업을 해놓아도, 출시가 가까워지면 급변하는 요구 사항으로 인해 계획이 엎어지기 마련이죠. 그러다 보니 복잡한 코드와 생소한 언어를 문서화 없이 익히는 데 꽤 고전했습니다.&lt;/p&gt;
&lt;p&gt;학습 과정에서 많은 팀원분들의 도움을 받았으며, 특히 파트장님께 질문을 자주 던져 문제를 해결했습니다. 질문을 할 때 꿀팁이 있다면, 무작정 안된다고만 하지 않고 스스로 ‘이건 왜 작동이 안되는 걸까-?’ 사고해보며 이런 저런 액션을 취한 후, 본인이 생각했을 때 동작이 되지 않는 이유 혹은 동작하기 위해 어떤 작업을 했는지 머릿속에 정리 한 후 질문을 드린다면 좀 더 명쾌한 답을 얻으실 수 있으실 겁니다.&lt;/p&gt;
&lt;p&gt;다행히도 저는 출시 이후에 합류했기 때문에 이러한 혼란 속에서도 새로운 코드베이스를 익히면서 어려운 부분들을 체크해 가이드 문서를 작성할 수 있었습니다.&lt;/p&gt;
&lt;h3 id=&quot;팀과의-협업--코드-통합과-소통의-중요성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%8C%80%EA%B3%BC%EC%9D%98-%ED%98%91%EC%97%85--%EC%BD%94%EB%93%9C-%ED%86%B5%ED%95%A9%EA%B3%BC-%EC%86%8C%ED%86%B5%EC%9D%98-%EC%A4%91%EC%9A%94%EC%84%B1&quot; aria-label=&quot;팀과의 협업  코드 통합과 소통의 중요성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;팀과의 협업 : 코드 통합과 소통의 중요성&lt;/h3&gt;
&lt;p&gt;코드 리뷰와 피드백 과정을 통해 성장할 수 있었습니다. 코드 리뷰를 두려워하지 않고, 오히려 팀원들과의 소통을 통해 배워나가는 과정에서 많은 발전을 이룰 수 있었습니다.&lt;/p&gt;
&lt;p&gt;또한, IT 업계 중 특히나 게임업계에서는 타 직군과의 소통 또한 중요한 요소로 작용했습니다. 게임 개발은 다양한 직군들이 협력해야 하기 때문에, 원활한 의사소통이 프로젝트 성공의 중요한 키가 되기도 하죠.&lt;/p&gt;
&lt;h2 id=&quot;마무리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EB%AC%B4%EB%A6%AC&quot; aria-label=&quot;마무리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;마무리&lt;/h2&gt;
&lt;p&gt;팀의 코드베이스를 처음 접했을 때는 모든 것이 생소하고, 새로운 환경과 도구에 적응하는 데도 시간이 걸렸습니다. 초기에는 작은 작업을 하나 할 때마다 기획서부터 꼼꼼히 준비해야 할 만큼 낯설었지만, 6개월이 지난 지금은 팀의 코드 흐름과 아키텍처에 대해 이해하게 되어 비교적 자유롭게 작업할 수 있게 되었습니다. 심지어 팀 내에서 가이드 문서를 작성할 수 있을 정도로 익숙해졌습니다.&lt;/p&gt;
&lt;p&gt;앞으로는 요구사항 구현을 넘어 시스템 개편에도 도전하고 싶습니다. 특히, DB 및 캐시 구조를 개선해 게임 성능을 최적화하는 프로젝트를 진행해 보고 싶습니다. 또한, 지금처럼 학습할 수 있는 기회가 있을 떄 근본적인 질문을 던지며 새로운 호기심도 품어보고 싶습니다. 적응 초기 단계의 질문은 성장의 원동력이 되기도 하니까요.&lt;/p&gt;
&lt;p&gt;이번 적응 과정을 통해 깨달은 점도 많습니다. &lt;code&gt;게임 서버 개발자가 근본적으로 해야 할 일은 크게 다르지 않다-&lt;/code&gt; 라는 것처럼요. 결국 개발자는 소통하고, 필요한 프로토콜을 설계하고, 이슈를 해결해가며 끊임없이 나아가야 합니다.&lt;/p&gt;
&lt;p&gt;또한, 팀에 새롭게 적응하는 데는 두 가지 접근법이 있다고 생각합니다.&lt;/p&gt;
&lt;p&gt;시간적 여유가 있다면, 언어와 아키텍처를 충분히 학습한 뒤 작은 기능을 하나씩 구현하며 점진적으로 팀에 적응하는 것이 정석적인 방법이라 생각합니다.&lt;/p&gt;
&lt;p&gt;그러나 빠른 기여가 요구되는 상황에서는 프로젝트와 언어를 동시에 익히면서 진행할 수 밖에 없는 경우도 많습니다. 이렇게 빠른 적응법을 사용하면 즉각적인 팀의 기여는 가능해지지만, 기초적인 개념이 머리에 남지 않을 위험도 크기 때문에 이후에도 지속적인 학습이 필요합니다.&lt;/p&gt;
&lt;p&gt;마지막으로, 팀 적응과 언어, 아키텍처에 대한 깊은 이해는 별개의 문제임을 항상 기억했으면 합니다. 팀 적응에만 안주하지 말고, 내가 속한 프로젝트의 구조와 이론적 부분을 놓치지 않으려는 노력이 중요합니다. 그러한 지속적인 관심과 학습이 개발자로서의 성장을 이끌어 낼 것이라 믿습니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[데브시스터즈의 장애 대응 원칙과 방법]]></title><description><![CDATA[데브시스터즈에서 적용하고 있는 장애 대응 원칙과 방법을 공개합니다.]]></description><link>https://tech.devsisters.com/posts/incident-management-principles</link><guid isPermaLink="false">https://tech.devsisters.com/posts/incident-management-principles</guid><pubDate>Thu, 13 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, 저는 데브시스터즈 기술본부 인프라셀에서 DevOps 엔지니어로 일하고 있는 이창원입니다. 이 글에서는 데브시스터즈에서 전사적으로 적용하고 있는 기술적 관점에서의 장애 대응 원칙과 방법 전문을 공개합니다.&lt;/p&gt;
&lt;p&gt;문서에 대한 설명은 &lt;a href=&quot;https://www.youtube.com/watch?v=xKmrHDoNzZ0&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;데브시스터즈 엔지니어링 데이 Infra/SRE 행사&lt;/a&gt; 영상에서도 확인하실 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;개요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B0%9C%EC%9A%94&quot; aria-label=&quot;개요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;개요&lt;/h2&gt;
&lt;p&gt;이 문서는 우리가 운영하는 각종 서비스의 장애에 대한 탐지, 대응, 예방을 위해 가져야 할 자세와 방법론에 대해 서술합니다.&lt;/p&gt;
&lt;h2 id=&quot;원칙&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9B%90%EC%B9%99&quot; aria-label=&quot;원칙 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;원칙&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;📌 이 원칙들은 구성원들이 상시 장애 상황을 모니터링하라는 의도에서 만들어진 것이 아니며, 꼭 필요한 순간에만 기민한 대응을 할 수 있도록 원칙과 방법을 설정한 것입니다.&lt;/p&gt;
&lt;p&gt;📌 &lt;strong&gt;모두가 기본적인 응급 조치를 할 수 있는 역량을 확보&lt;/strong&gt;해야 합니다. 그래야 담당자가 쉬거나 부재 중이더라도 다른 사람이 대응할 수 있고, 서로를 믿고 편하게 쉴 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;[장애 대응의 목표]&lt;/strong&gt; 장애 대응의 최우선 목표는 &lt;strong&gt;서비스가 가능한 정상적으로 동작하게 하는 것&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;지식이나 경험의 많고 적음에 관계 없이, 서비스 담당자인지 아닌지에 상관 없이 &lt;strong&gt;대응이 가능한 사람이라면&lt;/strong&gt; 그 자리에서 할 수 있는 최선의 노력을 통해 문제 해결에 적극적으로 기여해야 합니다.&lt;/li&gt;
&lt;li&gt;문제의 원인을 찾아 내려가는 것에만 집중해서는 안됩니다. 개발자라면 문제 발생 시 원인을 파내려가고 싶다는 걸 이해합니다. 그러나 &lt;strong&gt;비행 중 문제 발생 시 가장 먼저 할 것은 승객들이 안전하게 대피할 수 있도록 비행기를 잘 착륙시키는 것&lt;/strong&gt;입니다.&lt;/li&gt;
&lt;li&gt;스스로 알고 있는 지식으로 문제를 빠르게 해결할 수 있을지 판단해야 하며, &lt;strong&gt;어려울 것 같은 경우 다른 사람에게 부탁&lt;/strong&gt;해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;[에스컬레이션과 상황 전파]&lt;/strong&gt; 필요할 경우 &lt;strong&gt;적극적으로 도움을 요청&lt;/strong&gt;해야 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;장애가 발생한 사실을 뒤늦게 깨닫는 것보다는, &lt;strong&gt;오탐이라도 장애 상황을 의심하고 다시 한 번 점검하는 것이 훨씬 더 좋습니다&lt;/strong&gt;. 장애 인지를 했는데 이게 장애인지 아닌지, 어떻게 해야 할지 판단이 안 서는 경우에는 시간 장소 관계 없이 &lt;strong&gt;즉시&lt;/strong&gt; 다른 사람들을 부르도록 합시다.&lt;/li&gt;
&lt;li&gt;호출을 받는 사람은 이런 종류의 호출에 &lt;strong&gt;기분 나빠하거나 부담스러워하지 말고&lt;/strong&gt;, 이런 호출이 있을 수 있다는 사실을 인지하고 문제를 같이 파악합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;같은 사람을 호출하는 상황이 반복&lt;/strong&gt;된다면 해당 알람 관련 맥락을 &lt;strong&gt;다른 사람에게 잘 전파&lt;/strong&gt;해주고 있는지를 점검해 보아야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;[장애 대응을 위한 환경 준비]&lt;/strong&gt; 장애에 대응할 수 있도록 &lt;strong&gt;적절한 장비와 세팅을 최대한 유지&lt;/strong&gt;합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;랩탑을 항상 휴대&lt;/strong&gt;합니다. (사전에 입사자의 멘토가 랩탑 장비 신청 시 휴대성을 고려하여 신청하는 것을 권장합니다)&lt;/li&gt;
&lt;li&gt;언제든 대응할 수 있도록 랩탑에도 평소 &lt;strong&gt;개발/운영 환경&lt;/strong&gt;(Wireguard, SSH, AWS Console, kubectl, 간단한 스크립트 등)을 셋업하고, OS 버전업/기기 변경 등 다양한 변화에도 항상 잘 동작하게끔 준비하여야 합니다.&lt;/li&gt;
&lt;li&gt;Wi-Fi가 없는 환경에서도 대응할 수 있도록 사용하는 개인 폰에 테더링 환경(요금제 포함)을 구축하거나, 여의치 않은 경우 회사로부터 LTE 모뎀 / LTE 법인폰 등을 지급받아 준비합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;[장애 감지의 방법]&lt;/strong&gt; 장애를 감지할 수 있는 &lt;strong&gt;알람 시스템을 구축&lt;/strong&gt;하고 모두가 받을 수 있도록 항상 준비합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;장애 슬랙 채널의 알람은 티어에 맞게 Notification 설정을 &lt;strong&gt;All new messages&lt;/strong&gt; 로 설정, 문제를 빠르게 인지할 수 있도록 합니다.&lt;/li&gt;
&lt;li&gt;장애 문자, 전화 발신자는 &lt;strong&gt;Do not Disturb(방해 금지) 예외&lt;/strong&gt;로 설정해둡니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;적절한 티어링 체계&lt;/strong&gt;를 갖추면 알람 체계를 잘 유지할 수 있습니다. 문서의 아래 항목에서 그러한 티어링 방안을 제안하고 있으니, 그에 맞춰서 알람 시스템을 정비하길 권장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;[알람 건전성]&lt;/strong&gt; &lt;strong&gt;모든 알람은 약식으로라도 대응&lt;/strong&gt;을 하여야 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Feed 채널과 알람 채널의 목적은 다릅니다. 확인 결과 &lt;strong&gt;대응이 필요하지 않은 알람&lt;/strong&gt;이었을 경우 &lt;strong&gt;알람 조건 수정, 티어 재배치&lt;/strong&gt; 등을 진행합니다.&lt;/li&gt;
&lt;li&gt;양치기 소년 이야기의 교훈을 인지해야합니다. 알람을 보고 2차적인 판단을 거쳐 대응하지 않는 케이스가 생기게 되면 이후 심각한 문제 발생 시에도 제대로 대응하지 못하는 경우가 있을 수 있으며, 새로운 사람이 맥락을 따라가기도 어렵게 됩니다.&lt;/li&gt;
&lt;li&gt;알람에 대응하는 방식에는 여러가지가 있을 수 있습니다. 인지하였음을 스레드로 메시지를 남기거나, 관련한 이슈를 생성하거나, 알람 조건을 조정하고자 하는 논의를 시작하는 등 비교적 낮은 적극성의 액션부터 장애 선언을 하는 높은 적극성의 액션까지가 모두 포함됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;[기록과 커뮤니케이션]&lt;/strong&gt; 장애 대응 중 &lt;strong&gt;실행된 액션들은 객관적으로 공유&lt;/strong&gt;되어야 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;수행하는 조치가 멱등하지 않을 수 있으므로 &lt;strong&gt;시스템에 대한 변경 내역&lt;/strong&gt;은 장애 대응에 참여하고 있는 사람들에게 반드시 공유되어야 하며, 채팅 메세지 혹은 장애 대응 문서에 판단 근거, 조치 시간과 함께 기록을 남아야 합니다.&lt;/li&gt;
&lt;li&gt;장애 상황에서 조치 내용은 시스템에 대한 이해를 바탕으로 문제 해결을 위한 가설을 세우고, 이를 시험하기 위해 실행한 작업이 실제 안정화에 도움이 되는지 확인하는 과정을 거치기 때문에, 엔지니어로서의 역량을 빠르게 성장시킬 수 있는 기회이기도 합니다. 기록을 잘 남겨둘수록 장애 대응에 참여하지 않았던 다른 구성원들의 역량 강화에도 도움이 되며, 이는 우리가 더 견고한 팀이 되는 가장 효율적인 방법입니다.&lt;/li&gt;
&lt;li&gt;장애 대응에 반드시 기술 직군만 개입하는 것이 아니므로, 유관 부서에 커뮤니케이션할 때에는 &lt;strong&gt;기술적 배경이 적은 사람들도 이해할 수 있도록 작성&lt;/strong&gt;하는 것을 권장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;[장애 회고의 중요성]&lt;/strong&gt; 모든 인프라는 다양한 이유로 장애가 날 수 있지만, &lt;strong&gt;같은 이유로 같은 장애를 겪는 것은&lt;/strong&gt; &lt;strong&gt;우리의 잘못&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;장애 원인&lt;/strong&gt;과 &lt;strong&gt;조치 내용&lt;/strong&gt;을 기록으로 남기고 동일한 유형의 장애를 예방할 수 있는 방법을 모색합니다.&lt;/li&gt;
&lt;li&gt;되짚어볼 필요가 있는 장애라면 업무시간 중 시간을 잡아 &lt;strong&gt;포스트모템을 진행&lt;/strong&gt;합니다. 포스트모템에서의 논의가 건설적일 수 있도록 사전에 논의할 내용은 미리 작성해두도록 합니다.&lt;/li&gt;
&lt;li&gt;장애 회고 미팅의 참여자들은 &lt;strong&gt;비난하지 않는 포스트모템&lt;/strong&gt;이 될 수 있도록 주의를 기울여야 합니다. 이를 위해서는 장애 대응 과정 중의 모든 조치들이 각자의 상황에서 최선을 다했음을 신뢰하여야 합니다. 만약 결과적으로 잘못된 의사결정 혹은 실수가 있었더라도, 그러한 결과로 이어지게 된 시스템적 원인에 대해 논의할 수 있어야 합니다. &lt;strong&gt;어떠한 실패라도 시스템을 강화할 수 있는 기회&lt;/strong&gt;로 이어질 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&quot;알람-티어링-권장-체계&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%95%8C%EB%9E%8C-%ED%8B%B0%EC%96%B4%EB%A7%81-%EA%B6%8C%EC%9E%A5-%EC%B2%B4%EA%B3%84&quot; aria-label=&quot;알람 티어링 권장 체계 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;알람 티어링 권장 체계&lt;/h2&gt;
&lt;p&gt;알람을 분류할 때 &lt;strong&gt;최초 대응 시간(FRT, First Response Time: 문제 상황을 인지하고 파악하는 시작 시점)&lt;/strong&gt; 을 기준으로 삼을 것을 권장합니다. 대응의 완료를 포함하는 시간이 아닌, 최초 인지 및 파악을 시작하기까지의 시간을 말합니다.&lt;/p&gt;
&lt;p&gt;알람을 설정할 때 가급적 알람이 울린 채널만 보고 바로 대응 조치가 필요한지를 직관적으로 알 수 있도록 설정하는 것이 좋습니다.&lt;/p&gt;
&lt;p&gt;아래 장애 유형에 부합하지 않는 알람이 있다면, 팀에서 FRT 기준에 맞게 자율적으로 판단하여 적절한 티어에 추가할 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;tier-0-언제든지-대응해야-하는-최우선순위-알람--frt-15min&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#tier-0-%EC%96%B8%EC%A0%9C%EB%93%A0%EC%A7%80-%EB%8C%80%EC%9D%91%ED%95%B4%EC%95%BC-%ED%95%98%EB%8A%94-%EC%B5%9C%EC%9A%B0%EC%84%A0%EC%88%9C%EC%9C%84-%EC%95%8C%EB%9E%8C--frt-15min&quot; aria-label=&quot;tier 0 언제든지 대응해야 하는 최우선순위 알람  frt 15min permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;strong&gt;[Tier 0] 언제든지 대응해야 하는 최우선순위 알람 (&amp;#x3C;~ FRT 15min)&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;티어 정의&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;고객이 체감 가능한 문제&lt;/strong&gt;가 발생하거나, 또는 &lt;strong&gt;서비스와 연관된 내부 컴포넌트의 안정성에 위험&lt;/strong&gt;이 발생하는 경우입니다. 진행 중인 대부분의 일상 업무보다 높은 우선순위를 가지고 &lt;strong&gt;최대한 빠르게 대응&lt;/strong&gt;합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;장애 인지&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;시스템을 통한 문제별 주요 담당자에게 전화, 문자&lt;/li&gt;
&lt;li&gt;Tier 0 알람 채널으로의 메시지 수신&lt;br/&gt;
만일 Tier 0수준의 장애에 대하여 위와 같은 시스템을 통한 선제적 인지를 하지 못하고 사람에 의한 인지나 우연에 기반한 파악을 하였다면, 모니터링 체계의 정비가 필요함을 의미합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;장애 유형&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;유저가 체감할 수 있는 이상 징후가 발생하는 경우
&lt;ul&gt;
&lt;li&gt;e.g., API failure rate N% 증가, Latency XXms 지연 등&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;시스템의 일부가 복구 불가능한 상황을 맞이하여 시스템 자체 안전성에 대한 위험이 예상되는 경우
(당장의 문제가 일어나지 않는 경우를 포함 e.g., HA 구성된 시스템의 일부 장애)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;tier-1-최선의-속도로-대응해야-하는-차순위-알람-frt-평일--15m-밤일-경우-다음날-오전까지인-약-12h&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#tier-1-%EC%B5%9C%EC%84%A0%EC%9D%98-%EC%86%8D%EB%8F%84%EB%A1%9C-%EB%8C%80%EC%9D%91%ED%95%B4%EC%95%BC-%ED%95%98%EB%8A%94-%EC%B0%A8%EC%88%9C%EC%9C%84-%EC%95%8C%EB%9E%8C-frt-%ED%8F%89%EC%9D%BC--15m-%EB%B0%A4%EC%9D%BC-%EA%B2%BD%EC%9A%B0-%EB%8B%A4%EC%9D%8C%EB%82%A0-%EC%98%A4%EC%A0%84%EA%B9%8C%EC%A7%80%EC%9D%B8-%EC%95%BD-12h&quot; aria-label=&quot;tier 1 최선의 속도로 대응해야 하는 차순위 알람 frt 평일  15m 밤일 경우 다음날 오전까지인 약 12h permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;strong&gt;[Tier 1] 최선의 속도로 대응해야 하는 차순위 알람 (FRT 평일 &amp;#x3C;~ 15m, 밤일 경우 다음날 오전까지인 약 12h)&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;티어 정의&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tier 0과 같은 직접적인 영향은 감지하지 않았지만 &lt;strong&gt;기술적인 연관성을 통해 Tier 0으로 퍼질 수 있는 경우&lt;/strong&gt;입니다. &lt;strong&gt;업무 시간 중에는 Tier 0과 같은 수준의 경각심과 우선순위로 빠르게 대응&lt;/strong&gt;하나, 늦은 밤에 일어날 경우 다음날 오전에 확인하는 것을 목표로 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;장애 인지&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tier 1 알람 채널으로의 메시지 수신&lt;br/&gt;
Tier 1도 Tier 0과 마찬가지로, 시스템을 통한 선제적 인지를 하지 못하고 사람에 의한 인지나 우연에 기반한 파악을 하였다면 모니터링 체계의 정비가 필요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;장애 유형&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tier 0 문제로 발전할 가능성이 있는 기술적 문제
&lt;ul&gt;
&lt;li&gt;e.g., CPU Usage X% 이상 사용, DB latency API XXms 지연, 어플리케이션의 재시작 등&lt;/li&gt;
&lt;li&gt;e.g., Atlantis(인프라 배포 툴), Vault(사용자 인증), 사내 운영툴 장애 등&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;tier-2-즉각적인-대응이-필요하지는-않지만-개입을-필요로-하는-알람-frt-평일--23h-밤이나-주말일-경우-다음-업무일&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#tier-2-%EC%A6%89%EA%B0%81%EC%A0%81%EC%9D%B8-%EB%8C%80%EC%9D%91%EC%9D%B4-%ED%95%84%EC%9A%94%ED%95%98%EC%A7%80%EB%8A%94-%EC%95%8A%EC%A7%80%EB%A7%8C-%EA%B0%9C%EC%9E%85%EC%9D%84-%ED%95%84%EC%9A%94%EB%A1%9C-%ED%95%98%EB%8A%94-%EC%95%8C%EB%9E%8C-frt-%ED%8F%89%EC%9D%BC--23h-%EB%B0%A4%EC%9D%B4%EB%82%98-%EC%A3%BC%EB%A7%90%EC%9D%BC-%EA%B2%BD%EC%9A%B0-%EB%8B%A4%EC%9D%8C-%EC%97%85%EB%AC%B4%EC%9D%BC&quot; aria-label=&quot;tier 2 즉각적인 대응이 필요하지는 않지만 개입을 필요로 하는 알람 frt 평일  23h 밤이나 주말일 경우 다음 업무일 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;strong&gt;[Tier 2] 즉각적인 대응이 필요하지는 않지만 개입을 필요로 하는 알람 (FRT 평일 &amp;#x3C;~ 2~3h, 밤이나 주말일 경우 다음 업무일)&lt;/strong&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;티어 정의&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;작업자의 개입이 필요하지만, &lt;strong&gt;작업의 마감이 여유롭게 설정&lt;/strong&gt;되었거나 또는 &lt;strong&gt;서비스 내외부적으로 직접적인 영향을 미치지 않아&lt;/strong&gt; &lt;strong&gt;시급성은 낮춰 대응해도 되는 경우&lt;/strong&gt;입니다. &lt;strong&gt;최대 1업무일 이내에 문제 해결계획을 수립&lt;/strong&gt;하며, 실제 대응 작업을 더 늦게 진행할 경우 진행상황을 파악할 수 있도록 작업 티켓 등으로 기록을 남겨두는 것을 목표로 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;장애 인지&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tier 2 알람 채널으로의 메시지 수신&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;장애 유형&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;충분한 마감기한이 주어지는 작업
&lt;ul&gt;
&lt;li&gt;e.g., 인증서 만료, AWS instance retirement, AWS API deprecation&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;주기적으로 문제를 인지하여 대응이 필요한 작업
&lt;ul&gt;
&lt;li&gt;e.g., 정책적인 위반사항&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;비용에 대한 알람
&lt;ul&gt;
&lt;li&gt;e.g., 데이터독 사용량, cost enforcer&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&quot;효과적인-장애-대응-방법&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%9A%A8%EA%B3%BC%EC%A0%81%EC%9D%B8-%EC%9E%A5%EC%95%A0-%EB%8C%80%EC%9D%91-%EB%B0%A9%EB%B2%95&quot; aria-label=&quot;효과적인 장애 대응 방법 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;strong&gt;효과적인 장애 대응 방법&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;💡 이 내용은 모든 것을 반드시 지켜야 한다기보다는 일종의 가이드라인입니다.&lt;/p&gt;
&lt;p&gt;실제 장애 발생시 큰 틀 (장애 발생 공유 → 팀 구성 → 해결 → 종료 공유) 을 해치지 않는 선에서 융통성 있게 적용하시되, 포스트 모템을 진행할 때 어떤 것들이 어떤 이유로 가이드라인과 달라졌는지 설명해주세요.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;아래의 예시를 따라 장애를 대응하면 보다 문제를 효과적, 효율적으로 대응할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;이 내용은 가장 심각도가 높은 장애 상황을 가정하여 작성되었으므로 모든 장애 상황을 아래와 같이 대응하진 않아도 되고, 상황에 따라선 일부 항목을 생략할 수 있습니다.&lt;/li&gt;
&lt;li&gt;위 티어링 체계 기준으로 Tier 0 및 1의 문제는 아래같이 대응하면 효과적입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;장애 최초 전파 및 대응 팀 구성&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;장애를 인지한 모든 인원은 &lt;strong&gt;즉시&lt;/strong&gt; &lt;strong&gt;팀 채널에 장애를 인지하였음을 알려야&lt;/strong&gt; 합니다.&lt;/li&gt;
&lt;li&gt;이후 &lt;strong&gt;장애 대응 팀을 구성&lt;/strong&gt;합니다. 장애 대응 팀은 &lt;strong&gt;최소 2인 이상으로 구성&lt;/strong&gt;함을 원칙으로 합니다.&lt;/li&gt;
&lt;li&gt;장애 대응 팀은 지휘자와 기록가 역할 포함하도록 구성합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;각 역할은 특정 사람에게 명확하게 할당되어야 합니다.&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ㅇㅇ님은 장애 기록 해주실 수 있을까요? 장애 문제는 제가 정리할게요&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ㅁㅁ님은 서버 증설 준비해주세요&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;각 역할에 따라 해야 할 일은 다음과 같습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A. 공통&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;장애 발생 원인 및 해결 방안을 탐색&lt;/strong&gt;합니다.&lt;/li&gt;
&lt;li&gt;의심이 되는 지점을 발견하면 다른 사람들에게 캡쳐나 링크 등의 근거자료와 함께 공유합니다.&lt;/li&gt;
&lt;li&gt;장애 대응 과정에서 모든 의사 결정은 논의 하에 함께 수행합니다.&lt;/li&gt;
&lt;li&gt;장애가 종료된 후, 포스트 모템 문서를 정리하여 공유합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;B. 지휘자&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;장애 상황을 &lt;strong&gt;주도적으로 파악&lt;/strong&gt;하고, &lt;strong&gt;대응 팀을 구성&lt;/strong&gt;하여 &lt;strong&gt;역할을 분배&lt;/strong&gt;합니다.&lt;/li&gt;
&lt;li&gt;보통은 최초 인지자가 지휘자를 담당하는 경우가 많으며, 상황에 따라서 더 적절한 대상자가 있으면 다른 사람에게 지휘자를 인계할 수 있습니다.&lt;/li&gt;
&lt;li&gt;추가 인원이 필요한 경우 다른 구성원들에게 적극적으로 연락하여 대응을 요청합니다.
&lt;ul&gt;
&lt;li&gt;특히 문제 해결에 어려움을 겪을 경우, 혼자서 고민하지 말고 최대한 빨리 다른 사람들의 도움을 요청하도록 합시다.&lt;/li&gt;
&lt;li&gt;상황에 따라선 주 담당자 외에 외부의 인원이 문제를 함께 보는 것이 도움될 수 있습니다. (e.g., 인프라셀, 제품 팀 엔지니어, 동일 직군 다른 부서의 사람들, 그룹장 등)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;대부분은 공동작업이 가능하나, 논의가 필요한 사항이 있다면 지휘자가 적극적으로 검토하고 결정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;C. 기록가&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;원인, 고려한 해결 방안, 실제로 실행한 해결 방안 등 &lt;strong&gt;모든 것을 그 때 그 때 시간 순서로 기록&lt;/strong&gt;합니다.
&lt;ul&gt;
&lt;li&gt;각자가 의심하는 지점 및 장애 조치 내용은 각자가 직접 공유해주는 것이 원칙이며, &lt;strong&gt;기록가는 그 내용들을 정리하고 수합하는 것이 주 목적&lt;/strong&gt;입니다.&lt;/li&gt;
&lt;li&gt;장애 복구에 성공적인 시도만 기록하는 것보다는 실패했던 시도도 기록하는 것이 나중을 위한 자산이 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;장애 시점의 각종 모니터링 지표, 자료, 그래프, 로그 등을 보이는 대로 모두 모아 가능한 실시간으로 기록합니다. 실시간으로 기록이 정리되면 문제가 복잡해져 여러 사람의 개입이 필요한 경우 정보 전달 속도에 이점이 생깁니다.&lt;/li&gt;
&lt;li&gt;장애 상황의 공유를 주로 담당합니다. 장애가 길어지는 경우에는 (30분 이상 소요되는 경우) &lt;strong&gt;대응 현황, 예상 복구 시간을 공유&lt;/strong&gt;합니다. 주기적으로 계속 업데이트합니다. 종료된 후에도 공유합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;장애 대응 채널 구성&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;지휘자는 &lt;strong&gt;Datadog Incident&lt;/strong&gt; 기능을 사용하여 장애를 선언하고, 별도의 장애 대응 채널을 구성합니다. 장애를 선언하는 방법은 다음과 같습니다.
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://app.datadoghq.com/incidents/new&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://app.datadoghq.com/incidents/new&lt;/a&gt; 를 통해 UI에서 선언&lt;/li&gt;
&lt;li&gt;슬랙의 장애 인지 채널에서 &lt;code&gt;/datadog incident&lt;/code&gt; 명령어를 사용하여 선언&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Incident를 선언하면 해당 incident 전용 채널이 생성됩니다. 장애 대응에 관여하는 모든 인원은 해당 채널에서 커뮤니케이션합니다.&lt;/li&gt;
&lt;li&gt;화면공유를 위해 Slack huddle 또는 Google Meet 등을 개설해도 되지만, &lt;strong&gt;주 커뮤니케이션은 슬랙 메세지를 이용해&lt;/strong&gt; 추후 타임라인 분석이 용이하도록 잘 기록합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;장애 발생 공유&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;기록가가 장애 상황에 대해 &lt;strong&gt;인지한 즉시 슬랙을 통해 공유&lt;/strong&gt;합니다. 만일 유관 부서와도 연관되는 내용이라면 메일 등 적절한 수단을 통해 유관부서로도 공유합니다. 공유 시에는 다음 내용이 들어가야 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;서비스 명&lt;/li&gt;
&lt;li&gt;발생 시간&lt;/li&gt;
&lt;li&gt;장애 규모와 범위&lt;/li&gt;
&lt;li&gt;장애 대응 채널&lt;/li&gt;
&lt;li&gt;장애 발생 메시지 예시:
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;장애@ 쿠키런 킹덤 PVP 부분 장애&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;조금 전 8월 11일 새벽 2시 35분경 부터 쿠키런 킹덤 PVP 서비스에 장애가 관측되고 있습니다.
약 25% 정도의 접속자가 영향을 받은 것으로 추정되고 있습니다.
용감한 쿠키, 명랑한 쿠키 2인이 #incident-XY 채널에서 대응 중이며, 추후 경과는 다시 공유드리겠습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;장애 원인 파악 및 해결&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;장애 대응은 기본적으로 &lt;strong&gt;원인 파악 → 해결 방안 탐색 → 해결 방안 시행 → 해결 여부 확인&lt;/strong&gt;의 과정으로 진행됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;앞서 명시한 장애 대응 원칙에 따라, 사용자들이 서비스 이용에 영향을 받는 상황이라면 &lt;strong&gt;근본 원인을 찾는 것보다 서비스를 가능한 빨리 복구하는 것을 우선시하여 대응&lt;/strong&gt;해야 합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;어떠한 변경 작업 진행 시에는 반드시 작업 내용을 공유하여 함께 진행합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;시스템에 따라 멱등성이 보장되지 않을 수도 있으므로 일관성을 위해 시스템 변경은 1인이 진행해야 합니다. &lt;strong&gt;그러나, 이 1인이 독단적으로 시스템을 변경해서는 안됩니다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;위급한 상황에서 개인이 단독으로 대응하는 것은 &lt;strong&gt;구성원 간 맥락의 전파를 저해&lt;/strong&gt;할 뿐더러 긴급한 순간에서 빠르게 결정된 사항으로 &lt;strong&gt;더 나쁜 결과로 이어질 수&lt;/strong&gt; 있습니다. 따라서 &lt;strong&gt;변경 작업 진행 시에는 커뮤니케이션을 통해 작업 내용을 같이 리뷰해나가며 진행하는 것이 우리의 방침입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;장애 종료 공유&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;장애 종료 메시지를 공유하는 것도 장애 대응의 일부입니다. 장애 대응이 끝나면 명확한 종료 선언을 하여 혼란을 방지합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Root cause 분석이 끝나지 않았더라도, 일단 당장의 문제가 해결되면 장애 상황은 종료된 것입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;장애 상황이 종료되면 &lt;strong&gt;장애 상황 종료에 대해 슬랙을 통해 공유&lt;/strong&gt;합니다. 공유 내용으로는 다음 항목을 포함하여 작성해야 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;장애 종료 시간&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;장애 발생 (추정) 원인&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;조치 내역 혹은 해결 방안&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;포스트모템 미팅 일시&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;장애 종료 메시지 예시:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;장애@ 쿠키런 킹덤 PVP 부분 장애&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;2시 55분경 안정화되었습니다.
PVP 서버 4대 중 1대에 이상이 생겨 해당 서버에 접속한 유저들에게 장애가 있었던 것으로 추정됩니다.&lt;/p&gt;
&lt;p&gt;대응 조치로 해당 서버를 서비스에서 제외하고 새 서버 1대를 추가하였습니다.&lt;/p&gt;
&lt;p&gt;서버에 왜 갑자기 이상이 생겼는지는 내일 중으로 추가 조사를 진행할 예정입니다.&lt;/p&gt;
&lt;p&gt;장애 포스트모템은 내일 오후 3시에 퓨어바닐라왕국 연회장 회의실에서 진행하겠습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;후속 액션을 위해 다양한 직군이 관여되어야 할 수 있으므로 장애 종료 공지를 할 때에는 가급적 프로그래머가 아닌 직군들도 이해할 수 있는 표현을 사용하고, 자세한 기술적인 내용이 궁금하면 포스트모템을 참고하도록 안내합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;장애 종료가 공유되면 지휘자가 Datadog incident status를 조정합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;포스트모템 미팅이 예정되어 있을 경우: &lt;strong&gt;Active&lt;/strong&gt; → &lt;strong&gt;Stable&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;포스트모템 미팅이 예정되어 있지 않을 경우: &lt;strong&gt;Active&lt;/strong&gt; → &lt;strong&gt;Resolved&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;포스트모템 준비&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;효율적인 포스트모템을 위해 논의가 필요한 내용, 대응 과정을 미리 갈무리 해둡니다.
&lt;ul&gt;
&lt;li&gt;지휘자는 포스트모템 준비, root cause 분석, root cause 해결 등의 남은 작업을 특정 사람에게 명확하게 할당합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;포스트모템 템플릿을 참고하여 정보를 수집합니다.&lt;/li&gt;
&lt;li&gt;포스트모템 문서 작성의 주 담당은 장애 대응 당시 지휘자였던 사람이 맡습니다.
&lt;ul&gt;
&lt;li&gt;지휘자가 작성을 온전히 다 하는 것은 아니고, 다른 사람에게도 도움을 받을 수 있습니다.&lt;/li&gt;
&lt;li&gt;그러나 작성 진행 상황을 체크하고 잘 작성될 수 있도록 drive하는 것은 지휘자의 역할입니다.&lt;/li&gt;
&lt;li&gt;기록가의 기록을 잘 참고하고, 슬랙 히스토리와 대조하여 보충할 내용을 추가합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;당시 시스템의 상황을 잘 알 수 있도록 대시보드도 캡처해옵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;포스트모템 미팅&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;가까운 업무일에 포스트모템 미팅을 진행합니다. 진행 시에는 아래와 같은 항목들을 고민합니다.
&lt;ul&gt;
&lt;li&gt;장애 대응 팀 뿐 아니라 관련된 구성원이 있다면 모두 소집합니다. 미팅에 초대받지 못하였더라도 관심있는 구성원들이 참여할 수 있도록 미팅 링크를 미팅 시작 시간에 공유합니다.&lt;/li&gt;
&lt;li&gt;장애 대응 과정에 대해 리뷰하고 놓친 것은 없는지 공유합니다.&lt;/li&gt;
&lt;li&gt;앞으로 같은 장애를 겪지 않도록 액션 아이템을 정리하고 계획을 수립하여 실행합니다. 근본적인 문제 파악에 대해서는 &lt;strong&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Five_whys&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;5 Whys&lt;/a&gt;&lt;/strong&gt; 같은 좋은 방법들이 있으니 참고합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;미팅이 끝나면 포스트모템 문서 형태로 정리하여 공유합니다.
&lt;blockquote&gt;
&lt;p&gt;⚠️ 포스트모텀 미팅은 형식적인 자리여서는 안됩니다. 미팅이 끝나면 아래 질문에 답할 수 있도록 적극적으로 논의되어야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;오늘 미팅 이후, 같은 문제가 안 생긴다 / 마주했을 때 잘 대처할 수 있다는 자신감이 드나요?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&quot;포스트모템-문서-양식&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%8F%AC%EC%8A%A4%ED%8A%B8%EB%AA%A8%ED%85%9C-%EB%AC%B8%EC%84%9C-%EC%96%91%EC%8B%9D&quot; aria-label=&quot;포스트모템 문서 양식 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;포스트모템 문서 양식&lt;/h2&gt;
&lt;h3 id=&quot;yyyymmdd-aaa로-인한-bbb-장애&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#yyyymmdd-aaa%EB%A1%9C-%EC%9D%B8%ED%95%9C-bbb-%EC%9E%A5%EC%95%A0&quot; aria-label=&quot;yyyymmdd aaa로 인한 bbb 장애 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;YYYY/MM/DD AAA로 인한 BBB 장애&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://tech.devsisters.com/posts/incident-management-principles/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;(장애 대응 원칙과 방법 문서로의 링크)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;[장애 대응의 목표]&lt;/strong&gt; 장애 대응의 최우선 목표는 &lt;strong&gt;문제를 정상화하는 것&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;summary&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#summary&quot; aria-label=&quot;summary permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Summary&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;해당 장애를 요약합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;impact&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#impact&quot; aria-label=&quot;impact permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Impact&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;해당 장애로 발생한 영향을 기술합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;root-causes&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#root-causes&quot; aria-label=&quot;root causes permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Root Causes&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;해당 장애가 발생한 근본적 원인을 기술합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;trigger&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#trigger&quot; aria-label=&quot;trigger permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Trigger&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;해당 장애가 촉발된 직접적 원인을 기술합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;resolution&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#resolution&quot; aria-label=&quot;resolution permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Resolution&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;해당 장애를 어떻게 대응하고 해결 하였는지 기술합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;detection&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#detection&quot; aria-label=&quot;detection permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Detection&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;해당 장애를 어떻게 감지 하였는지 기술합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;timeline&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#timeline&quot; aria-label=&quot;timeline permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Timeline&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&quot;[시간] 사건에 대한 기술&quot;을 시간 순으로 기록합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;discussion&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#discussion&quot; aria-label=&quot;discussion permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Discussion&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;포스트모템 미팅을 진행하며 &lt;strong&gt;서로를 비난해서는 안됩니다&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;포스트모템 미팅을 마치면 아래 질문에 답할 수 있어야 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;같은 문제가 생기지 않는다 / 마주했을 때 잘 대처할 수 있다는 자신감이 드나요?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;포스트모템 미팅에서 논의할 만한 주제들을 미리 기록해두고, 미팅 내용을 속기합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;lesson-learned&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#lesson-learned&quot; aria-label=&quot;lesson learned permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Lesson Learned&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;잘 된 일&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;이번 대응에서 잘 된 것을 기술합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;잘 안된 일&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;이번 대응에서 잘 되지 않은 것을 기술합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;운이 좋았던 일&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;이번 대응에서 운이 좋았던 것을 기술합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;action-items&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#action-items&quot; aria-label=&quot;action items permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Action Items&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;해당 장애를 위해 앞으로 개선해야 할 것을 구체적으로 기술합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;💁
앞으로 같은 문제가 생기지 않는다 / 마주했을 때 잘 대처할 수 있다는 자신감이 드나요?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[그래서 계정 연동 하면 뭐가 좋은데?]]></title><description><![CDATA[DevPlay 계정에 대해 알아보고 이를 통해 얻을 수 있는 다양한 혜택을 알아봅니다.]]></description><link>https://tech.devsisters.com/posts/devplay-account</link><guid isPermaLink="false">https://tech.devsisters.com/posts/devplay-account</guid><pubDate>Tue, 21 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, 저는 데브시스터즈 기술본부 백엔드셀에서 소프트웨어 엔지니어로 일하고 있는 안정현입니다. 이 글에서는 DevPlay 계정에 대해 알아보고 이를 통해 얻을 수 있는 다양한 혜택을 알아봅니다.&lt;/p&gt;
&lt;h3 id=&quot;devplay-소개&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#devplay-%EC%86%8C%EA%B0%9C&quot; aria-label=&quot;devplay 소개 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;DevPlay 소개&lt;/h3&gt;
&lt;p&gt;DevPlay는 데브시스터즈의 글로벌 게임 서비스 플랫폼으로, 새로운 기기로 바꾸어도 기존에 플레이하던 게임 데이터 그대로 플레이할 수 있게 하거나, 데브시스터즈의 다양한 게임간 이벤트에 참여할 수 있습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1624px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 46.111111111111114%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAJABQDASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAIDBP/EABUBAQEAAAAAAAAAAAAAAAAAAAAB/9oADAMBAAIQAxAAAAHDfM6oSF//xAAXEAEBAQEAAAAAAAAAAAAAAAABADER/9oACAEBAAEFAiMV6Tjf/8QAFREBAQAAAAAAAAAAAAAAAAAAEBH/2gAIAQMBAT8Bh//EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oACAECAQE/Aaj/xAAZEAABBQAAAAAAAAAAAAAAAAAAASAxQoH/2gAIAQEABj8CKahLP//EABwQAAICAgMAAAAAAAAAAAAAAAARATEQIUFxkf/aAAgBAQABPyFUS4a9sjheC0Ybn//aAAwDAQACAAMAAAAQ1N//xAAVEQEBAAAAAAAAAAAAAAAAAAAQEf/aAAgBAwEBPxCD/8QAFREBAQAAAAAAAAAAAAAAAAAAEBH/2gAIAQIBAT8Qof/EAB4QAQACAgEFAAAAAAAAAAAAAAEAESExEEFhcYGh/9oACAEBAAE/EHbYX0oimuMgHsbimIFzgF9ifPw6PE//2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/bc4c4508ea317b71e9e068d01faf9eac/33ecc/devplay-login-horizontal.webp 360w,
/static/bc4c4508ea317b71e9e068d01faf9eac/263a4/devplay-login-horizontal.webp 480w,
/static/bc4c4508ea317b71e9e068d01faf9eac/0c8fb/devplay-login-horizontal.webp 640w,
/static/bc4c4508ea317b71e9e068d01faf9eac/521f7/devplay-login-horizontal.webp 860w,
/static/bc4c4508ea317b71e9e068d01faf9eac/cbd37/devplay-login-horizontal.webp 1200w,
/static/bc4c4508ea317b71e9e068d01faf9eac/7ef45/devplay-login-horizontal.webp 1624w&quot; sizes=&quot;(max-width: 1624px) 100vw, 1624px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/bc4c4508ea317b71e9e068d01faf9eac/543cd/devplay-login-horizontal.jpg 360w,
/static/bc4c4508ea317b71e9e068d01faf9eac/a3e66/devplay-login-horizontal.jpg 480w,
/static/bc4c4508ea317b71e9e068d01faf9eac/56d4e/devplay-login-horizontal.jpg 640w,
/static/bc4c4508ea317b71e9e068d01faf9eac/2f567/devplay-login-horizontal.jpg 860w,
/static/bc4c4508ea317b71e9e068d01faf9eac/03ffe/devplay-login-horizontal.jpg 1200w,
/static/bc4c4508ea317b71e9e068d01faf9eac/de972/devplay-login-horizontal.jpg 1624w&quot; sizes=&quot;(max-width: 1624px) 100vw, 1624px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/bc4c4508ea317b71e9e068d01faf9eac/de972/devplay-login-horizontal.jpg&quot; alt=&quot;devplay login horizontal&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;게임 진입 시 로그인 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;DevPlay 계정 구조를 간단하게 나타내면 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;하나의 DevPlay 계정(Member)으로 여러 게임을 즐기고 있는 사용자는, 내부적으로 각 게임별 계정(Player)을 연결하여 저장하고 있습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2048px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 31.11111111111111%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAGABQDASIAAhEBAxEB/8QAFwABAAMAAAAAAAAAAAAAAAAAAAIDBf/EABQBAQAAAAAAAAAAAAAAAAAAAAL/2gAMAwEAAhADEAAAAdyAjaCv/8QAFxAAAwEAAAAAAAAAAAAAAAAAAAERAv/aAAgBAQABBQJpk1T/xAAVEQEBAAAAAAAAAAAAAAAAAAABEP/aAAgBAwEBPwFn/8QAFREBAQAAAAAAAAAAAAAAAAAAARD/2gAIAQIBAT8BJ//EABkQAAIDAQAAAAAAAAAAAAAAAAERABAhQf/aAAgBAQAGPwLCoEc7X//EABoQAAICAwAAAAAAAAAAAAAAAAEhETEAEEH/2gAIAQEAAT8hMjoavCJrK2v/2gAMAwEAAgADAAAAEPPP/8QAFxEAAwEAAAAAAAAAAAAAAAAAARBBcf/aAAgBAwEBPxAbi//EABcRAAMBAAAAAAAAAAAAAAAAAAEQQXH/2gAIAQIBAT8QOav/xAAZEAEBAAMBAAAAAAAAAAAAAAABEQAhQTH/2gAIAQEAAT8QVdwH0g5gttSCVJoIYCE5n//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/1e26c6c4ba0a5a16261a6280cbcc11f0/33ecc/struct-auth-member-player.webp 360w,
/static/1e26c6c4ba0a5a16261a6280cbcc11f0/263a4/struct-auth-member-player.webp 480w,
/static/1e26c6c4ba0a5a16261a6280cbcc11f0/0c8fb/struct-auth-member-player.webp 640w,
/static/1e26c6c4ba0a5a16261a6280cbcc11f0/521f7/struct-auth-member-player.webp 860w,
/static/1e26c6c4ba0a5a16261a6280cbcc11f0/cbd37/struct-auth-member-player.webp 1200w,
/static/1e26c6c4ba0a5a16261a6280cbcc11f0/96d48/struct-auth-member-player.webp 2048w&quot; sizes=&quot;(max-width: 2048px) 100vw, 2048px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/1e26c6c4ba0a5a16261a6280cbcc11f0/543cd/struct-auth-member-player.jpg 360w,
/static/1e26c6c4ba0a5a16261a6280cbcc11f0/a3e66/struct-auth-member-player.jpg 480w,
/static/1e26c6c4ba0a5a16261a6280cbcc11f0/56d4e/struct-auth-member-player.jpg 640w,
/static/1e26c6c4ba0a5a16261a6280cbcc11f0/2f567/struct-auth-member-player.jpg 860w,
/static/1e26c6c4ba0a5a16261a6280cbcc11f0/03ffe/struct-auth-member-player.jpg 1200w,
/static/1e26c6c4ba0a5a16261a6280cbcc11f0/09658/struct-auth-member-player.jpg 2048w&quot; sizes=&quot;(max-width: 2048px) 100vw, 2048px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/1e26c6c4ba0a5a16261a6280cbcc11f0/09658/struct-auth-member-player.jpg&quot; alt=&quot;struct auth member player&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;계정이 연동된 유저의 데이터 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h3 id=&quot;과거-로그인-기록을-통해-유저에게-편리함을-제공하는-방법&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B3%BC%EA%B1%B0-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B8%B0%EB%A1%9D%EC%9D%84-%ED%86%B5%ED%95%B4-%EC%9C%A0%EC%A0%80%EC%97%90%EA%B2%8C-%ED%8E%B8%EB%A6%AC%ED%95%A8%EC%9D%84-%EC%A0%9C%EA%B3%B5%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95&quot; aria-label=&quot;과거 로그인 기록을 통해 유저에게 편리함을 제공하는 방법 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;과거 로그인 기록을 통해 유저에게 편리함을 제공하는 방법&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;오랜만에 들어왔더니 어떤 계정으로 가입되어 있었는지 기억이 나지 않아요&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;DevPlay는 다양한 로그인 수단을 제공하고 있어서, Google, Apple 등 이미 가지고 있는 여러 로그인 수단 중 하나를 선택하여 계정 연동을 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;또한 게임에 접속할 때마다 매번 로그인해야 하거나 게임 도중에 인증수단이 만료되는 것은 사용자에게 좋지 않은 경험을 주기 때문에, DevPlay는 토큰을 일정 기간 동안(refresh token이 만료되기 전까지) 주기적으로 교체해 줘서 인증이 끊기지 않도록 도와줍니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;신규 게임에 처음 접속했을 때, 이전에 사용했던 계정이 무엇이었는지 기억이 나지 않아요&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;위의 인증을 편하게 할 수 있도록 해주는 두 가지 기능의 영향인지는 모르겠지만 가끔 사용자분들이 자신의 계정을 기억하지 못하시는 경우도 있는데요, 플레이를 오랜 기간 쉬었던 사용자분들의 경우 더욱 그러합니다.&lt;/p&gt;
&lt;p&gt;데브시스터즈에서 새로운 게임이 출시했을 때, 사용자분이 이전에 다른 게임에서 사용했던 DevPlay 계정이 무엇이었는지 기억하지 못하고 새로운 로그인 수단으로 DevPlay 계정을 새로 생성할 수 있습니다. 이는 궁극적으로 사용자의 불편으로 이어지기 때문에, DevPlay는 동일 디바이스의 다른 게임에서 DevPlay 로그인을 사용한 적이 있다면 디바이스의 고유 식별자를 통해 &lt;strong&gt;“이미 연동 가능한 DevPlay 계정이 있군요!&quot;&lt;/strong&gt; 라고 제안하며 이전의 로그인 수단으로 연동할 수 있도록 도와줍니다.&lt;/p&gt;
&lt;p&gt;이는 한 게임에 대해 여러 계정을 사용하고 있는 사용자들에게도 좋은 경험을 제공합니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1624px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 46.111111111111114%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAJABQDASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAIDBP/EABUBAQEAAAAAAAAAAAAAAAAAAAAB/9oADAMBAAIQAxAAAAHDSLqhIX//xAAYEAEBAAMAAAAAAAAAAAAAAAABABEhMf/aAAgBAQABBQItYVieN//EABURAQEAAAAAAAAAAAAAAAAAABAR/9oACAEDAQE/AYf/xAAVEQEBAAAAAAAAAAAAAAAAAAAAEf/aAAgBAgEBPwFH/8QAFxAAAwEAAAAAAAAAAAAAAAAAARAgIf/aAAgBAQAGPwJYY//EABoQAAICAwAAAAAAAAAAAAAAAAABETEQIVH/2gAIAQEAAT8hjGx8ESAssNz/2gAMAwEAAgADAAAAEJTf/8QAFhEAAwAAAAAAAAAAAAAAAAAAARAR/9oACAEDAQE/EIC//8QAFhEBAQEAAAAAAAAAAAAAAAAAARAR/9oACAECAQE/ENWP/8QAGxAAAwACAwAAAAAAAAAAAAAAAAERIXEQMaH/2gAIAQEAAT8Qe8ZuGhktA07gtw8vB0aP/9k=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/653d5d1aafde61f9212ffe7ac483a850/33ecc/devplay-idp-horizontal.webp 360w,
/static/653d5d1aafde61f9212ffe7ac483a850/263a4/devplay-idp-horizontal.webp 480w,
/static/653d5d1aafde61f9212ffe7ac483a850/0c8fb/devplay-idp-horizontal.webp 640w,
/static/653d5d1aafde61f9212ffe7ac483a850/521f7/devplay-idp-horizontal.webp 860w,
/static/653d5d1aafde61f9212ffe7ac483a850/cbd37/devplay-idp-horizontal.webp 1200w,
/static/653d5d1aafde61f9212ffe7ac483a850/7ef45/devplay-idp-horizontal.webp 1624w&quot; sizes=&quot;(max-width: 1624px) 100vw, 1624px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/653d5d1aafde61f9212ffe7ac483a850/543cd/devplay-idp-horizontal.jpg 360w,
/static/653d5d1aafde61f9212ffe7ac483a850/a3e66/devplay-idp-horizontal.jpg 480w,
/static/653d5d1aafde61f9212ffe7ac483a850/56d4e/devplay-idp-horizontal.jpg 640w,
/static/653d5d1aafde61f9212ffe7ac483a850/2f567/devplay-idp-horizontal.jpg 860w,
/static/653d5d1aafde61f9212ffe7ac483a850/03ffe/devplay-idp-horizontal.jpg 1200w,
/static/653d5d1aafde61f9212ffe7ac483a850/de972/devplay-idp-horizontal.jpg 1624w&quot; sizes=&quot;(max-width: 1624px) 100vw, 1624px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/653d5d1aafde61f9212ffe7ac483a850/de972/devplay-idp-horizontal.jpg&quot; alt=&quot;devplay idp horizontal&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;접속 이력이 있는 디바이스에서의 로그인 화면&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h3 id=&quot;guest-계정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#guest-%EA%B3%84%EC%A0%95&quot; aria-label=&quot;guest 계정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Guest 계정&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;회원가입을 진행하지 않고 게임을 진행하고 싶어요&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;DevPlay 계정을 이용하면 디바이스를 변경하는 상황에서도 게임 데이터를 안전하게 보호할 수 있는 장점이 있지만, 로그인 수단을 연동하는 과정 자체가 당장 플레이를 즐기고 싶은 사용자에게는 하나의 진입장벽이 될 수 있습니다. 이러한 경우 사용자는 Guest로 로그인 하여, DevPlay 연동 과정을 생략하고 계정을 생성하여 서비스를 이용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;Guest 계정의 경우에도 위에서 설명한 Auth-Member-Player의 구조를 가지지만, Guest 계정은 사용자를 기기 또는 앱의 생애주기에 맞춰서 구분하기 때문에 사용자가 핸드폰을 변경할 경우 게임 계정을 잃어버릴 수도 있습니다.&lt;/p&gt;
&lt;p&gt;⚠️ DevPlay 연동을 하지 않은 채로 계정을 잃어버리더라도 계정을 찾을 수는 있습니다. &lt;a href=&quot;https://cs.devsisters.com/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;고객지원 페이지&lt;/a&gt;를 통해 계정을 복구받을 수 있지만, 그 때엔 결제내역 등을 통해 본인임을 증명해야 합니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2048px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 25.83333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAFABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAEF/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEAMQAAAB3gUH/8QAFBABAAAAAAAAAAAAAAAAAAAAEP/aAAgBAQABBQJ//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPwE//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPwE//8QAFBABAAAAAAAAAAAAAAAAAAAAEP/aAAgBAQAGPwJ//8QAFxABAAMAAAAAAAAAAAAAAAAAAQAQEf/aAAgBAQABPyEJlf/aAAwDAQACAAMAAAAQ8A//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/ED//xAAVEQEBAAAAAAAAAAAAAAAAAAABEP/aAAgBAgEBPxAn/8QAGBABAQEBAQAAAAAAAAAAAAAAAREAIVH/2gAIAQEAAT8QQ6tfdIQAMEJv/9k=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/d610511b60439217955e535af2139c41/33ecc/struct-auth-guest-player.webp 360w,
/static/d610511b60439217955e535af2139c41/263a4/struct-auth-guest-player.webp 480w,
/static/d610511b60439217955e535af2139c41/0c8fb/struct-auth-guest-player.webp 640w,
/static/d610511b60439217955e535af2139c41/521f7/struct-auth-guest-player.webp 860w,
/static/d610511b60439217955e535af2139c41/cbd37/struct-auth-guest-player.webp 1200w,
/static/d610511b60439217955e535af2139c41/96d48/struct-auth-guest-player.webp 2048w&quot; sizes=&quot;(max-width: 2048px) 100vw, 2048px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/d610511b60439217955e535af2139c41/543cd/struct-auth-guest-player.jpg 360w,
/static/d610511b60439217955e535af2139c41/a3e66/struct-auth-guest-player.jpg 480w,
/static/d610511b60439217955e535af2139c41/56d4e/struct-auth-guest-player.jpg 640w,
/static/d610511b60439217955e535af2139c41/2f567/struct-auth-guest-player.jpg 860w,
/static/d610511b60439217955e535af2139c41/03ffe/struct-auth-guest-player.jpg 1200w,
/static/d610511b60439217955e535af2139c41/09658/struct-auth-guest-player.jpg 2048w&quot; sizes=&quot;(max-width: 2048px) 100vw, 2048px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/d610511b60439217955e535af2139c41/09658/struct-auth-guest-player.jpg&quot; alt=&quot;struct auth guest player&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;게스트 유저의 데이터 구조&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;위와 같이 Guest 계정은 당장 플레이하기에 편리함을 제공하지만, DevPlay 연동을 하면 게임 데이터를 안전하게 보관할 수 있는 장점과 더불어 아래에서 설명할 크로스 게임 프로모션의 혜택을 받을 수 있게 됩니다.&lt;/p&gt;
&lt;h3 id=&quot;크로스-게임-프로모션&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%81%AC%EB%A1%9C%EC%8A%A4-%EA%B2%8C%EC%9E%84-%ED%94%84%EB%A1%9C%EB%AA%A8%EC%85%98&quot; aria-label=&quot;크로스 게임 프로모션 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;크로스 게임 프로모션&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;새로운 게임을 런칭했는데, 기존 게임들의 유저풀을 이용할 수 없나요?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;서비스 런칭 후, 유저들에게 이를 알리는 방법은 굉장히 많습니다. 기존 라이브 서비스 게임을 통해서 홍보를 하는 방법도 그 중 하나인데, 이를 크로스 게임 프로모션(CGP)이라 부르고 있습니다.&lt;/p&gt;
&lt;p&gt;CGP는 앱 마케팅 및 고객 유치를 위한 주요한 전략입니다. 고객이 새롭게 출시하는 게임에서 일정한 기준을 충족하는 경우, 연계된 기존 라이브 서비스 게임에 보상을 지급하는 방식으로 진행합니다. 이럴 경우 새롭게 출시하는 게임에 기존 게임의 고객을 유입시켜 런칭 초기 고객을 확보할 수 있는 효과를 노릴 수 있습니다.&lt;/p&gt;
&lt;p&gt;아래는 기존에 진행했던 CGP 예시입니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2048px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 140.2777777777778%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAcABQDASIAAhEBAxEB/8QAGAAAAwEBAAAAAAAAAAAAAAAAAAMEAQL/xAAXAQADAQAAAAAAAAAAAAAAAAAAAgMB/9oADAMBAAIQAxAAAAFdEuxokqHxVM7nn2LBf//EABwQAQEAAgIDAAAAAAAAAAAAAAECACEDEhETMv/aAAgBAQABBQLhJY6CfNE9j13k8dZIubya8hOykmDX/8QAFxEBAAMAAAAAAAAAAAAAAAAAAAERgf/aAAgBAwEBPwFSWv/EABkRAAIDAQAAAAAAAAAAAAAAAAABEjFBYf/aAAgBAgEBPwFiZWEuH//EABwQAAICAwEBAAAAAAAAAAAAAAABESECMUEQMv/aAAgBAQAGPwK87llZyZKelJHDSHNGz6HbI8//xAAeEAACAgICAwAAAAAAAAAAAAAAAREhMVFBcWGhwf/aAAgBAQABPyGUQYUlIhuxpy6ticaFseRy+iE3WOkWCl65Kpua9l5e9MgTmORvLLHZ/9oADAMBAAIAAwAAABCfBI7/xAAZEQEBAQADAAAAAAAAAAAAAAABABEhMVH/2gAIAQMBAT8QCeUte2d9X//EABgRAAMBAQAAAAAAAAAAAAAAAAABETFR/9oACAECAQE/EIoiDaXBHB//xAAdEAEBAAIDAQEBAAAAAAAAAAABEQAhMUFRYaHB/9oACAEBAAE/ENnVZEZfuI8N1yV77waQnpa0YaJ7FZseeMI7A4iH0X3BJA1SPR8xvMRt/bOKZNoIe+jWO6SIqH8yYRq1Hu+YiKWJAUpMIIYeJ4Z//9k=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/b4a85823b0ca8a7804306bc075e0d790/33ecc/cgp-examples.webp 360w,
/static/b4a85823b0ca8a7804306bc075e0d790/263a4/cgp-examples.webp 480w,
/static/b4a85823b0ca8a7804306bc075e0d790/0c8fb/cgp-examples.webp 640w,
/static/b4a85823b0ca8a7804306bc075e0d790/521f7/cgp-examples.webp 860w,
/static/b4a85823b0ca8a7804306bc075e0d790/cbd37/cgp-examples.webp 1200w,
/static/b4a85823b0ca8a7804306bc075e0d790/96d48/cgp-examples.webp 2048w&quot; sizes=&quot;(max-width: 2048px) 100vw, 2048px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/b4a85823b0ca8a7804306bc075e0d790/543cd/cgp-examples.jpg 360w,
/static/b4a85823b0ca8a7804306bc075e0d790/a3e66/cgp-examples.jpg 480w,
/static/b4a85823b0ca8a7804306bc075e0d790/56d4e/cgp-examples.jpg 640w,
/static/b4a85823b0ca8a7804306bc075e0d790/2f567/cgp-examples.jpg 860w,
/static/b4a85823b0ca8a7804306bc075e0d790/03ffe/cgp-examples.jpg 1200w,
/static/b4a85823b0ca8a7804306bc075e0d790/09658/cgp-examples.jpg 2048w&quot; sizes=&quot;(max-width: 2048px) 100vw, 2048px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/b4a85823b0ca8a7804306bc075e0d790/09658/cgp-examples.jpg&quot; alt=&quot;cgp examples&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;각 게임의 크로스 게임 프로모션 예시&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;A게임에서 조건 달성 후 B게임에서 보상을 받기 위해서는 A게임의 Player와 B게임의 Player가 하나의 계정으로 묶여있어야 하는데, 이는 DevPlay 연동을 통해 가능합니다.&lt;/p&gt;
&lt;p&gt;CGP 조건 달성 시 보상을 받는 플로우는 아래와 같습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;아래에서는 쉬운 설명을 위해, &lt;code&gt;쿠키런: 모험의 탑&lt;/code&gt;, &lt;code&gt;쿠키런: 킹덤&lt;/code&gt; 을 예시로 듭니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;모험의 탑에서 &lt;code&gt;스토리 모드 1-1 클리어&lt;/code&gt; 라는 CGP 조건을 달성하면,&lt;/li&gt;
&lt;li&gt;DevPlay 서버에서 해당 DevPlay 계정의 킹덤 계정에 보상이 생성됩니다.&lt;/li&gt;
&lt;li&gt;이후 유저는 킹덤에 다시 접속하여 보상을 획득합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1858px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 47.77777777777777%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAADHElEQVR42g3M609TZwAH4PNfmH3YF5YZswm7mE5UCi10LbSknPbQG+0o1Jae0lIKHnukFlqqlhUmKiwoF2HotriQyEyMbmY4M7KwwVHsRNTh2HAG5I3skmxZMnnf3/b8AQ9nadD9EYpbN/mAgbQ2e8iRiJckMl5ir7OQ1ECC3Fs+S56TS2Tuu9MkkWsnvoYaUqXXEItDT0yWcmLgy0hdqOqZ1an/KxQJfMW9tvslTJzSsay0Bx/nSvDleTWmx3TYZziAUCKIK9Myrl3rw9hEN9yhAASXBWahGuF2N6yNZhidlbB7KqjTUQjebchzRSU74OB30TZ3MYsHylhzk4aVWtRMI+hZTK5mX189xNYffMYer3zLHt2fYQ9/nGM/rd1ma7/cYasr8+zR3Vts9ouR7bikRovI3+XeshagWNjFCmu0KKjgUVBqxKslZaiy6ZDrK8bSLRl/PrmB9SdLWF+7jeUHs1h5PI+tzWVsPbuPp6sLmJ4+R/cbd6NBtOe5nS4zwt8PstrLPdD9n2ocJuzUW+EJVuLDITUWr7diY2kKPz+cw8bqLP7+LY+t53ewubGA339dwNOVbzBzY5RGj5mQHYnluQKvD/UJJwt32HBzOIDFKRFy8iC8MTP6B9QYPm7CUbEWqcMudMdrkJVdiLSZ4BY1iNaVouWgCSdkO/2k34nLE0fy3I5EBupgM/tooAP//PsC2+QePjh3HHw7j1SvGZfOCLD1FyHYq8X18QDak1p44iq8J72DjoY30endh0zGTE+dtGL8/OE8V5jMIHxhYttRY6SfTs3QlkiSFtcKVEwLtC2tounuClrfq6LhngP0zGA1jR4to8bGPfRdn4q6/Xup2LiX1svlL3hpP+R+3yKX7vDiWPYQ89rL4RJseOV1NYo8dhgDWhjqCyE0aXGhL4CExMMWfBtzkxrcHDHh6pAKDqkJ9q4h2ONp+kblyxBl4QduqNOdP52NLqROxJRUV1jpyUlKNO1X/Cmf4u9qUqT3o8qVyYwyOpxWRs+mlM/H/crkoEMZO2lRpM5WJdKTU8RUcr5ONC8lsrGL/wGv/trsqTd09gAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/35541469254267324cfda8a5fb655de3/33ecc/clear-mission.webp 360w,
/static/35541469254267324cfda8a5fb655de3/263a4/clear-mission.webp 480w,
/static/35541469254267324cfda8a5fb655de3/0c8fb/clear-mission.webp 640w,
/static/35541469254267324cfda8a5fb655de3/521f7/clear-mission.webp 860w,
/static/35541469254267324cfda8a5fb655de3/cbd37/clear-mission.webp 1200w,
/static/35541469254267324cfda8a5fb655de3/a311b/clear-mission.webp 1858w&quot; sizes=&quot;(max-width: 1858px) 100vw, 1858px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/35541469254267324cfda8a5fb655de3/53918/clear-mission.png 360w,
/static/35541469254267324cfda8a5fb655de3/9aebd/clear-mission.png 480w,
/static/35541469254267324cfda8a5fb655de3/0f09e/clear-mission.png 640w,
/static/35541469254267324cfda8a5fb655de3/3e3fe/clear-mission.png 860w,
/static/35541469254267324cfda8a5fb655de3/64756/clear-mission.png 1200w,
/static/35541469254267324cfda8a5fb655de3/bf7a7/clear-mission.png 1858w&quot; sizes=&quot;(max-width: 1858px) 100vw, 1858px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/35541469254267324cfda8a5fb655de3/bf7a7/clear-mission.png&quot; alt=&quot;clear mission&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;쿠키런: 모험의 탑 - 스토리 모드 1-1 클리어&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1490px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 49.16666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAABb0lEQVR42o2Sy07DMBBF89Xs+QTYsGUBKkUJ7Y4FGzYseAghoQpampaEJA2PoDptMraT+GKb8i5Sr3Ska2dmPGPH2dzYQKfTgeu68DwPrsb75ADe3i7cnW20Wi202+1v39wFns3tdrswtZyRfwcjpRR+6GOdXKMZnSBNU+R5jmVqmgZ13SC8H8PxhwO7KaXUmzU4FxBCWG+pKtRNjcp4k2hRqAVHLYX1UscYjccjOKbqxylGjOVg+exvt5V4R3IdXANZArxOrFecbFg89uHEQbB85N8FCz1uwb4o8wUMyqwrQurfwukPRjjvBQgfM1BZolxARFhFlZ5MyPeR00mi77B/g8OtNZxdnuPp6QVJEiPQXUdR9KtrtZT5bI7plEEKaXOdJI6U4KUSVaX+zVqROEmVEwY+rvbXMbw4wvNrjkf9e2RZhlVFxDGfF/ZR43iiXzl8oOB4m8LeKRUkiU2nxBgzF7gSnHMqilL7hqJ4Qm8OhPUspLiI/wAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/9a26d8f9d6d59365a3f09df8527d2fb4/33ecc/devplay-games.webp 360w,
/static/9a26d8f9d6d59365a3f09df8527d2fb4/263a4/devplay-games.webp 480w,
/static/9a26d8f9d6d59365a3f09df8527d2fb4/0c8fb/devplay-games.webp 640w,
/static/9a26d8f9d6d59365a3f09df8527d2fb4/521f7/devplay-games.webp 860w,
/static/9a26d8f9d6d59365a3f09df8527d2fb4/cbd37/devplay-games.webp 1200w,
/static/9a26d8f9d6d59365a3f09df8527d2fb4/1f7d3/devplay-games.webp 1490w&quot; sizes=&quot;(max-width: 1490px) 100vw, 1490px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/9a26d8f9d6d59365a3f09df8527d2fb4/53918/devplay-games.png 360w,
/static/9a26d8f9d6d59365a3f09df8527d2fb4/9aebd/devplay-games.png 480w,
/static/9a26d8f9d6d59365a3f09df8527d2fb4/0f09e/devplay-games.png 640w,
/static/9a26d8f9d6d59365a3f09df8527d2fb4/3e3fe/devplay-games.png 860w,
/static/9a26d8f9d6d59365a3f09df8527d2fb4/64756/devplay-games.png 1200w,
/static/9a26d8f9d6d59365a3f09df8527d2fb4/ad2a0/devplay-games.png 1490w&quot; sizes=&quot;(max-width: 1490px) 100vw, 1490px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/9a26d8f9d6d59365a3f09df8527d2fb4/ad2a0/devplay-games.png&quot; alt=&quot;devplay games&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;계정 연동 상태&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1858px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 47.77777777777777%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAADD0lEQVR42hXD7U8bdQAH8NsrNS5mEAa4sQ1K1+KQESa01wcobWmP613Xa7mmFloKhRY2PKAl0FIibJ1MsAMcM5M9Z1mci0PjYxZSlhBfz5vVF/4Leq984Tv3+32Nn+TDdHaa/u7nhb8cPQ6N7xM1MT6vnR8paFx6XROnb2j+6W1N+GBb817Y0mzJDc068v9NzRlf0dzxguaKFbT+1KU/2cTaP8PLD/eZQ4deQ1VVLa2pPoqqI2/DYA/DcT4J7/sZCMkljGdWMaxcRWDsCt4NrqCRX0CDsAK9awqsPYTj5hTqzSnyRu8SutPXK8zhw9VoONFEmvVG2nBCR1Mzt+nG5hO6uvOUrm59RZeXirRQ+Ihms0Uamd2kEWWVXlz4mLpSJRpNZKhLXqD9g/OvLMk1SAu3fmVqT7N4x+Gj5mAYvrEYSpuf4psnO/jp2T38/MPneP5oFvsPcvj62gy28wqyIxNIRZIQxUm4veOwOGPodERJi0mCNKRUmFYuC8tgnnovLkLKL6GUDeLbHyexd20EL7/P4cW9Oew9VrB/fQ7lq+t4ULqB8tpTFJUiPNYAbF0BtLXzpOUsD3lopsJ0cR+iW75M3bEc3Ok8tu5vYa/8CXa/vImD8md4tnsHj3ZL+O7xTVQODrB79w7KO8+xXtyA4AmBc8owW/2kw+RGJJGuMEY2jQ5Php4Tp9ETn8PA/DIm84vIXM5h8VIOK0UFV5YVZDJxLM4lMD2exNTEOBLDg+B9QTh7+mE2O0n7OQui8USFqa43otFoetXcaiX6syyxBwTSbuWInQuSXl4iDn+Y2EQPGR3zkdmZAElNcGRKCZDRSZk4vE7S0+sgFiv7L2t+D7F45CXz+pt1qKlrojV1OtTU63C0rQ1vNXbhyKku1DaZcUxvwakzJpgsneiwt6CZ1cFg08FoasVxgx4nWww4aTxNjhl08Az4f2NcbvF3n2/gF0EIqxwvqb2yT/XG/CqfCKrBREiVB/1qeEhSpWhAFaOS6ogIKitzqi3kVe1yn2oL9qndob4XZtn1x2juwhf/AYSekVjQAYk1AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/a9ba69dc9e53b01a045b223fa00e821d/33ecc/kingdom-reward.webp 360w,
/static/a9ba69dc9e53b01a045b223fa00e821d/263a4/kingdom-reward.webp 480w,
/static/a9ba69dc9e53b01a045b223fa00e821d/0c8fb/kingdom-reward.webp 640w,
/static/a9ba69dc9e53b01a045b223fa00e821d/521f7/kingdom-reward.webp 860w,
/static/a9ba69dc9e53b01a045b223fa00e821d/cbd37/kingdom-reward.webp 1200w,
/static/a9ba69dc9e53b01a045b223fa00e821d/a311b/kingdom-reward.webp 1858w&quot; sizes=&quot;(max-width: 1858px) 100vw, 1858px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/a9ba69dc9e53b01a045b223fa00e821d/53918/kingdom-reward.png 360w,
/static/a9ba69dc9e53b01a045b223fa00e821d/9aebd/kingdom-reward.png 480w,
/static/a9ba69dc9e53b01a045b223fa00e821d/0f09e/kingdom-reward.png 640w,
/static/a9ba69dc9e53b01a045b223fa00e821d/3e3fe/kingdom-reward.png 860w,
/static/a9ba69dc9e53b01a045b223fa00e821d/64756/kingdom-reward.png 1200w,
/static/a9ba69dc9e53b01a045b223fa00e821d/bf7a7/kingdom-reward.png 1858w&quot; sizes=&quot;(max-width: 1858px) 100vw, 1858px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/a9ba69dc9e53b01a045b223fa00e821d/bf7a7/kingdom-reward.png&quot; alt=&quot;kingdom reward&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;쿠키런: 킹덤 - CGP 보상 수령&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;모험의 탑과 킹덤 계정을 하나의 DevPlay 계정으로 연동하지 않은 상태라면, 상황은 조금 더 복잡해집니다.
당장은 CGP 조건을 달성 하더라도 보상을 받을 수 없는것이 당연하지만, 나중에 계정 연동을 했을 경우 그 즉시 보상을 받을 수 있도록 처리를 해줘야 합니다.&lt;/p&gt;
&lt;p&gt;조건 달성을 먼저 한 후, 계정 연동을 하는 경우 보상을 받는 플로우는 아래와 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;모험의 탑에서 &lt;code&gt;스토리 모드 1-1 클리어&lt;/code&gt; 라는 CGP 조건을 달성합니다.&lt;/li&gt;
&lt;li&gt;아직 연동된 킹덤 계정이 없으므로, 모험의 탑 계정의 Member에 대해 지급되지 않은 킹덤 보상이 있음을 기록해둡니다.&lt;/li&gt;
&lt;li&gt;이후 CGP 기간 내 해당 계정에 킹덤 계정이 생성되어 연동된다면, 그 때 지급하지 못한 킹덤 보상을 지급합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;cgp를-통해-이득을-취하려는-시도와-해결방법&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#cgp%EB%A5%BC-%ED%86%B5%ED%95%B4-%EC%9D%B4%EB%93%9D%EC%9D%84-%EC%B7%A8%ED%95%98%EB%A0%A4%EB%8A%94-%EC%8B%9C%EB%8F%84%EC%99%80-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%95&quot; aria-label=&quot;cgp를 통해 이득을 취하려는 시도와 해결방법 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;CGP를 통해 이득을 취하려는 시도와 해결방법&lt;/h3&gt;
&lt;p&gt;계정을 쉽게 초기화 할 수 있는 경우, 유저는 최초의 뽑기를 이론상 무한정 시도할 수 있습니다.
이것을 일본어로 리셋 마라톤, 줄여서 &lt;code&gt;리세마라&lt;/code&gt; 라고 부르고 있고 한국에서도 리세마라 라고 부르는 편입니다.
데브시스터즈의 게임에서도 보상을 극대화 하기 위해 유저들이 리세마라를 하는 경우가 꽤 있습니다.&lt;/p&gt;
&lt;p&gt;CGP에 대해서도 리세마라를 시도하는 유저가 있을 수 있습니다. 기본적으로 DevPlay 플랫폼 입장에서는 중복지급 방지를 위한 최소한의 방어 로직을 제외하고는 리세마라에 대한 제한을 두고 있지 않고, 각 게임사에 선택지를 주고 있습니다.&lt;/p&gt;
&lt;p&gt;지난 CGP에서는 리세마라를 막기로 결정했고, 위의 설명과 동일하게 &lt;code&gt;쿠키런: 킹덤&lt;/code&gt; 과 &lt;code&gt;쿠키런: 모험의 탑&lt;/code&gt; 을 예시로 설명하겠습니다.&lt;/p&gt;
&lt;h4 id=&quot;모험의-탑-계정-리세마라-조건-달성-게임&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AA%A8%ED%97%98%EC%9D%98-%ED%83%91-%EA%B3%84%EC%A0%95-%EB%A6%AC%EC%84%B8%EB%A7%88%EB%9D%BC-%EC%A1%B0%EA%B1%B4-%EB%8B%AC%EC%84%B1-%EA%B2%8C%EC%9E%84&quot; aria-label=&quot;모험의 탑 계정 리세마라 조건 달성 게임 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;모험의 탑 계정 리세마라 (조건 달성 게임)&lt;/h4&gt;
&lt;p&gt;조건을 달성하는 쪽인 모험의 탑 계정을 리세마라 하면서 하나의 킹덤 계정에 CGP 보상을 지급하려는 경우, DevPlay 서버에서 하나의 킹덤 계정에 중복 보상이 지급되지 않도록 처리를 하고 있습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;img src=&quot;/c367351945dccee4c63304278e52e5e5/reward.svg&quot;&gt;
    &lt;figcaption&gt;reward id의 구조를 통해 중복 지급을 막는 과정&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;만일 DevPlay 서버에서 이러한 처리를 하지 않는다면, 어떻게 해야할까요?
지난 CGP에선 별석탄이라는 CGP 전용 재화를 둠으로써 추가적인 방어 로직을 만들었습니다. 유저가 CGP 보상을 중복으로 받았어도 별석탄이라는 재화는 CGP 보상을 교환할 때에만 &apos;1개일 경우&apos; ~ &apos;4개일 경우&apos; 등 제한적으로 사용 가능하게 하였으므로, 유저가 일정한 수량을 초과하여 별석탄을 소지하고 있더라도 더 이상의 이득을 취할 수 없습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2008px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 48.33333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAACMklEQVR42k2SXW/ScBTG+810MTOALhqvvDK7MGaazHlnYrzbjX4BTcx0myhYxtsoL2WFFctbX+BfSkuBZS8IJBtcqN/g8bSg7uKX55w2efqc08PdCm5gJfQMNwMbPivBp76urm0Sz3HnwRZevX6Lh49eYvXuJoL3txC45/GC3j9BYO0xblN/I7SO0Po2ODERQS4ZQ+YgijQfgRD/RkQhFzJQjnKoiAKMmgy3Y6Bn6ks06tsw6jysxmdYTIOsJuG2VXByPolyLomSkPDx6nI2SWYCqlLW165RxYltYNjV/2O30NPJROMx6LbBWkX0LTJsiCkySaGYjkFM8ShlE5AycbDaMWxNQach48I1MR+fYPZjuGRA/RlOnQNM7F1MR0OovYT/nCsvDY6WSMICuZCmcQ99NZsVuKaKHmteQ6MPpuFocThUM6MEp10HV8vH/yUseAlpbM+Y1cuwdQVWU8a5yzCfUMLxcAklnFDCbhST7gdMKXXd4XE17lNCb3/ZFBktdndMtadynhIWDn1llLDHVErQvAYl1AU4ehp2S0PHkGG3G+BUKe0v3jOWKJ0/Jpk4lG5oNtFv1TA9tfF7NsKvq4sl59RPMO7HMHd3MLscQR/G8fPyDJxCBkqRTsTTvzVRJWrenya1yHxgaeh31GvocAyRyC1qVqVTaoLLRPbAfwnj6+4nhD/ugA/vI7q/h3JegCKJkMUc3ZsC12Lo0e0taFHfhaG8g/39DUzGkKy8h2Ma+AMaLHpwcYzPDgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/80a60589bbec9739dba0cc6b9322b0fc/33ecc/ck-star-coal-reward.webp 360w,
/static/80a60589bbec9739dba0cc6b9322b0fc/263a4/ck-star-coal-reward.webp 480w,
/static/80a60589bbec9739dba0cc6b9322b0fc/0c8fb/ck-star-coal-reward.webp 640w,
/static/80a60589bbec9739dba0cc6b9322b0fc/521f7/ck-star-coal-reward.webp 860w,
/static/80a60589bbec9739dba0cc6b9322b0fc/cbd37/ck-star-coal-reward.webp 1200w,
/static/80a60589bbec9739dba0cc6b9322b0fc/23e01/ck-star-coal-reward.webp 2008w&quot; sizes=&quot;(max-width: 2008px) 100vw, 2008px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/80a60589bbec9739dba0cc6b9322b0fc/53918/ck-star-coal-reward.png 360w,
/static/80a60589bbec9739dba0cc6b9322b0fc/9aebd/ck-star-coal-reward.png 480w,
/static/80a60589bbec9739dba0cc6b9322b0fc/0f09e/ck-star-coal-reward.png 640w,
/static/80a60589bbec9739dba0cc6b9322b0fc/3e3fe/ck-star-coal-reward.png 860w,
/static/80a60589bbec9739dba0cc6b9322b0fc/64756/ck-star-coal-reward.png 1200w,
/static/80a60589bbec9739dba0cc6b9322b0fc/4e1ad/ck-star-coal-reward.png 2008w&quot; sizes=&quot;(max-width: 2008px) 100vw, 2008px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/80a60589bbec9739dba0cc6b9322b0fc/4e1ad/ck-star-coal-reward.png&quot; alt=&quot;ck star coal reward&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;쿠키런: 킹덤 - 별석탄 보상 획득&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h4 id=&quot;킹덤-계정-리세마라-보상-수령-게임&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%82%B9%EB%8D%A4-%EA%B3%84%EC%A0%95-%EB%A6%AC%EC%84%B8%EB%A7%88%EB%9D%BC-%EB%B3%B4%EC%83%81-%EC%88%98%EB%A0%B9-%EA%B2%8C%EC%9E%84&quot; aria-label=&quot;킹덤 계정 리세마라 보상 수령 게임 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;킹덤 계정 리세마라 (보상 수령 게임)&lt;/h4&gt;
&lt;p&gt;반대로 보상을 받는 쪽인 킹덤 계정을 리세마라 하여 이미 미션을 완료한 모험의 탑 계정으로부터 중복된 CGP 보상을 얻어가려는 경우는 어떻게 처리할 수 있을까요?&lt;/p&gt;
&lt;p&gt;모험의 탑 서버에서 DevPlay 서버에 미션 달성 API를 단 한번만 호출할 수 있도록 제한을 두어 보상을 중복지급하지 않게 만들 수 있습니다. CGP 조건 달성 및 보상 여부에 대해 서버가 매 번 중복을 체크하는 것은 부담이 될 수도 있지만, 잘 구현할 수만 있다면 가장 확실한 방법입니다. 지난 CGP에서는 모험의 탑 서버 개발자들이 이러한 방어 로직을 직접 구현하기도 했습니다.&lt;/p&gt;
&lt;p&gt;이러한 작업들을 통해 유저들이 예상치 못한 방법으로 이득을 취하는 것을 방지하고, 게임의 공정성을 유지할 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;마치며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0&quot; aria-label=&quot;마치며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;마치며&lt;/h3&gt;
&lt;p&gt;지금까지 DevPlay 계정의 구조와, 계정 연동과 Guest 계정의 장단점, 크로스 게임 프로모션에 대해 알아보았습니다. 데브시스터즈의 게임을 이용해주시는 분들의 수 만큼 계정을 사용하는 방법이 다양하다고 느끼는데요, 이러한 다양한 방법들을 통해 사용자들에게 더 나은 경험을 제공할 수 있도록 노력하고 있습니다.&lt;/p&gt;
&lt;p&gt;이 글을 통해 DevPlay 계정에 대한 궁금증이 어느정도 해소되었으면 좋겠습니다. 긴 글 읽어주셔서 감사합니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[스칼라 컴파일 속도 빠르게 하기]]></title><description><![CDATA[스칼라 프로젝트의 고질적인 문제인 컴파일 속도를 향상한 경험을 공유합니다.]]></description><link>https://tech.devsisters.com/posts/improve-scala-compilation-speed</link><guid isPermaLink="false">https://tech.devsisters.com/posts/improve-scala-compilation-speed</guid><pubDate>Tue, 17 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요. 스튜디오 킹덤에서 쿠키런: 킹덤의 서버 개발자로 재직하고 있는 전윤재입니다. 이번 글에서는 스칼라로 개발된 쿠키런: 킹덤 서버의 빌드 속도를 개선하기 위해 수행했던 과정과 결과를 소개해 드리려 합니다.&lt;/p&gt;
&lt;h2 id=&quot;도입부&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8F%84%EC%9E%85%EB%B6%80&quot; aria-label=&quot;도입부 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;도입부&lt;/h2&gt;
&lt;p&gt;스칼라는 정교한 타입 시스템, 매크로, implicit과 같은 강력한 기능들을 제공하여, 더 안전하고 생산적인 코드를 작성할 수 있도록 합니다. 이러한 기능들은 프로그램의 안정성을 높이고, 복잡한 로직을 간결하게 표현할 수 있게 해줍니다. 하지만 공짜 점심은 없듯이 프로젝트의 규모가 커질수록 컴파일러에 전가된 많은 작업이 오히려 컴파일 속도를 저하해 개발 생산성을 떨어뜨리는 결과를 초래할 수 있습니다.&lt;/p&gt;
&lt;p&gt;쿠키런: 킹덤 서버는 국내에 몇 안 되는 스칼라 및 함수형 프로그래밍 패러다임을 적용한 서버로 Scala2로 작성되어 있으며 sbt로 빌드하고 있습니다. 시간이 흐르며 프로젝트의 규모가 커지다 보니 이에 따라 느려진 컴파일 속도로 인한 불편함을 저희도 체감하게 되었습니다.&lt;/p&gt;
&lt;p&gt;컴파일 속도를 저하하는 요인은 다양합니다. 이 글에서는 typeclass 를 활용할 때 빌드 성능 저하를 피하는 방법, 그리고 build pipelining 을 적용하기 위한 과정과 그 과정에서 경험한 트러블슈팅을 담았습니다.&lt;/p&gt;
&lt;h2 id=&quot;컴파일러&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC&quot; aria-label=&quot;컴파일러 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;컴파일러&lt;/h2&gt;
&lt;p&gt;본론으로 들어가기에 앞서 내용 이해를 돕기 위해 컴파일러에 대한 간단한 설명을 드리겠습니다. 컴파일은 여러 단계로 이루어지는데 이를 페이즈(phase)라고 합니다. 스칼라 컴파일러에서 수행되는 여러 페이즈는 네 가지 카테고리로 분류할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;프론트엔드(frontend) - 소스코드로부터 초기 구조를 생성하는 과정
&lt;ul&gt;
&lt;li&gt;parser - 소스 코드를 트리 형태로 변환 (타입 정보는 없음)&lt;/li&gt;
&lt;li&gt;typer - 트리에 타입 정보를 보충&lt;/li&gt;
&lt;li&gt;그 외에 여러 페이즈 포함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;피클러(pickler) - 프론트엔드 단계에서 생성된 트리를 직렬화하여 저장하는 단계&lt;/li&gt;
&lt;li&gt;트랜스폼(transform) - 런타임에 적합한 로우레벨 형태로 변환 (패턴매치 변환, 타입 이레이저 등이 포함)&lt;/li&gt;
&lt;li&gt;백엔드(backend) - 최종적으로 바이트 코드를 생성하는 단계&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 중 핵심적인 페이즈는 프론트엔드와 백엔드입니다. 프론트엔드 페이즈는 주로 코드를 분석하여 컴파일러가 이해할 수 있는 구조로 변환하는 데 집중하고, 백엔드 페이즈는 이러한 구조를 최종 실행 가능한 코드로 변환합니다.&lt;/p&gt;
&lt;h2 id=&quot;typeclass&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#typeclass&quot; aria-label=&quot;typeclass permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Typeclass&lt;/h2&gt;
&lt;p&gt;스칼라를 사용하다 보면 typeclass를 자주 접하게 됩니다. 이때 typeclass의 인스턴스를 어디에 정의하는지에 따라 컴파일 속도에 많은 영향이 있을 수 있습니다. 이는 컴파일러가 인스턴스를 검색할 때, 인스턴스의 정의된 위치에 따라 소요되는 시간이 다르기 때문입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Show.scala&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;trait&lt;/span&gt; Show&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;T&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; show&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;t&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; T&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;String&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;위와 같은 typeclass가 있다고 할 때 인스턴스를 정의하고 사용하는 방법은 대략 세 가지로 나눌 수 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Companion 오브젝트에 정의&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Show.scala&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;object&lt;/span&gt; Show &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;implicit&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; intShow&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Show&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; _&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;toString
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Foo.scala&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 컴패니언 오브젝트의 인스턴스는 기본적으로 스코프에 포함됨&lt;/span&gt;
println&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;implicitly&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Show&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;show&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;별도 trait에 정의하고, 이를 extends해서 스코프로 가져오기&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Show.scala&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;trait&lt;/span&gt; ShowInstances &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;implicit&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; intShow&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Show&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; _&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;toString
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Foo.scala&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;object&lt;/span&gt; Foo &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; ShowInstances &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  println&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;implicitly&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Show&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;show&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;별도 object에 정의하고, 이를 import해서 스코프로 가져오기&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Show.scala&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;object&lt;/span&gt; ShowInstances &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;implicit&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; intShow&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Show&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; _&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;toString
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Foo.scala&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; ShowInstances&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;_

println&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;implicitly&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Show&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;show&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;세 가지 모두 유효한 방법이지만 빌드 속도에 유의미한 차이를 가져옵니다. 그러면 프로파일링 결과를 보면서 어떤 차이가 있는지 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;아래 프로파일링 결과는 typeclass를 많이 사용하는 서브 프로젝트를 컴파일해서 얻은 결과를 약간 편집한 것입니다. 컴파일 시 &lt;code&gt;-Vstatistics:typer&lt;/code&gt; 옵션을 활성화하면 아래와 같은 결과를 얻을 수 있습니다. 왼쪽 결과는 typeclass 인스턴스 정의 시 별도 object나 trait에 정의하는 2,3번 방법을 사용했을 때 결과입니다. 오른쪽 결과는 인스턴스를 companion object에 정의했을 때의 결과입니다. 프로파일링 결과를 보시면 각 페이즈별 소요된 시간 및 페이즈별 상세 스탯을 확인할 수 있습니다. 저희가 관심 있는 부분은 typeclass 검색에 사용된 시간이므로 typer 페이즈 내에서 &lt;code&gt;time spent in implicits&lt;/code&gt; 부분을 확인하면 됩니다. &lt;code&gt;-Vstatistiscs&lt;/code&gt; 의 더 자세한 사용법은 이 &lt;a href=&quot;https://docs.scala-lang.org/overviews/compiler-options/#Verbose_Settings&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;사이트&lt;/a&gt;에서 확인할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;small-text-code&quot;&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;	                             baseline&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;		      companion &lt;span class=&quot;token keyword&quot;&gt;object&lt;/span&gt;

&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;info&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; #total compile time   &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;65561.041&lt;/span&gt;ms          &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;45911.915&lt;/span&gt;ms
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;info&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;   parser              &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;115.791&lt;/span&gt;ms &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0.2&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;     &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;128.306&lt;/span&gt;ms &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0.3&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;info&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;   typer               &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;43962.134&lt;/span&gt;ms &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;67.1&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;24221.797&lt;/span&gt;ms &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;52.8&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;info&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;   pickler             &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;54.552&lt;/span&gt;ms &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0.1&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;      &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;54.096&lt;/span&gt;ms &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0.1&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;info&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;   patmat              &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;9903.798&lt;/span&gt;ms &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;15.1&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;   &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;10066.074&lt;/span&gt;ms &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;21.9&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;info&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;   uncurry             &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2354.928&lt;/span&gt;ms &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3.6&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;    &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2692.558&lt;/span&gt;ms &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;5.9&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;info&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;   fields              &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;479.04&lt;/span&gt;               &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;450.513&lt;/span&gt;ms &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1.0&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;info&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;   jvm                 &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1717.737&lt;/span&gt;ms &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2.6&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;    &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1554.491&lt;/span&gt;ms &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3.4&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;                                       baseline&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;		             companion &lt;span class=&quot;token keyword&quot;&gt;object&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;info&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; time spent typechecking       &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;43962.129&lt;/span&gt;ms                  &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;24221.792&lt;/span&gt;ms
&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;info&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; time spent in implicits       &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;55444&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;34748.208&lt;/span&gt;ms &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;79.0&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;      &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;55464&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;15114.999&lt;/span&gt;ms &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;62.4&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;info&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;   successful in scope         &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;21527&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;20733.404&lt;/span&gt;ms &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;47.2&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;      &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10435&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;5559.827&lt;/span&gt;ms &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;23.0&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;info&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;   failed in scope             &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;33917&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;15298.409&lt;/span&gt;ms &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;34.8&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;      &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;45029&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4790.923&lt;/span&gt;ms &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;19.8&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;info&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;   successful of &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt;          &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;15548&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3092.438&lt;/span&gt;ms &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;7.0&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;        &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;26658&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4990.437&lt;/span&gt;ms &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;20.6&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;info&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;   failed of &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt;              &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;18369&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;198.034&lt;/span&gt;ms &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0.5&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;         &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;18371&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;400.566&lt;/span&gt;ms &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1.7&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;info&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;   assembling parts            &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5435&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;120.933&lt;/span&gt;ms &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0.3&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;          &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;12852&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;194.988&lt;/span&gt;ms &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0.8&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;info&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;   matchesPT                   &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3675811&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3503.863&lt;/span&gt;ms &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;8.0&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;      &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3678299&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3527.704&lt;/span&gt;ms &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;14.6&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;info&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; time spent in macroExpand     &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;7172&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;9645.873&lt;/span&gt;ms &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;21.9&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;        &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;7174&lt;/span&gt; spans&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;7884.628&lt;/span&gt;ms &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;32.6&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;%&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;baseline 대비 companion 오브젝트 사용 시 typer 페이즈에서 소요된 시간은 67.1% → 52.8%로 감소했습니다. 그리고 typer 페이즈 내에서 implicits 검색에 든 시간도 확인 가능한데 79% → 62.4%로 감소했습니다. 즉 typeclass 인스턴스 검색에 걸린 시간이 줄어든 것을 확인할 수 있습니다. 그리고 더 자세히 들여다볼 것은 &lt;code&gt;time spent in implicits&lt;/code&gt; 의 세부 항목입니다. 여기서 ~~scope, ~~ of type으로 끝나는 항목이 있는데 각각 현재 스코프 내부에서 검색한 것인지, companion 오브젝트에서 검색한 것인지 나타냅니다. 변경 전 프로파일링 결과와 비교해 보면 successful in scope, failed in scope에서 소모된 시간은 큰 폭으로 감소했고, sucessful of type, failed of type에서 소모된 시간이 소폭 상승한 것을 확인할 수 있습니다. 이로써 스코프 내에서 인스턴스를 찾은 횟수보다 companion 오브젝트에서 검색한 횟수가 증가했고, 전체적으로 검색에 든 시간은 감소했다는 것을 알 수 있습니다. 아래 그래프를 참고 하시면 전후 차이점을 쉽게 파악하실 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 57.22222222222223%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABpklEQVR42q2SO2/UQBSF/V8po1DxC5BCgejoAFFEERHZ1UJBQUmBFIREUqRACRGrrHdt1u+ZWY93/PoY25soRBspBVc6Gs+M75lz7z0O/yHadkAXzrA0LMOKMDEoXSLzh0HpipU2rMvqhtwZmEv2jwq+nDSEMmce5PixXcP74dp/vKQgChTuNCBKBVVV4xjTsVe8nxR8nZwRC8XVsuBiZhOinEWk70dcYLSFkiRixdqUOELklE3B4WHGt52niCzi5Kfh1buKQBmr5ppgO7kpm3/66QhbYmtLHo0kx4+fDYS/at68dAmvXOa2r15sk215PXl4l7AeBnPTw/6BioODhOPdPUQS8GMGbx8dkXwaM9cw8xVzX+CnBYvU9ETdI166xlR3FDab/Wgs+P7kOSqNOHVhf+cD8eePLK2AyTji7MVrfp//YXG56Hs39TTTy4Dy1oRv2QbyvCb2QoRckamCNJRoqaw97CStmtD1yWJ7FyVIqRFCI1NF29TbCXs3Wg+plSZOBHXT9n1p7VmHZoP+fPO9LZxtbu/81CV15WSdYmsJaYf3kPgLSOhFbbtOmMMAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/2a153bca8e82fa1f350fc5a6f17f43ea/33ecc/time_per_phase.webp 360w,
/static/2a153bca8e82fa1f350fc5a6f17f43ea/263a4/time_per_phase.webp 480w,
/static/2a153bca8e82fa1f350fc5a6f17f43ea/0c8fb/time_per_phase.webp 640w,
/static/2a153bca8e82fa1f350fc5a6f17f43ea/521f7/time_per_phase.webp 860w,
/static/2a153bca8e82fa1f350fc5a6f17f43ea/cbd37/time_per_phase.webp 1200w,
/static/2a153bca8e82fa1f350fc5a6f17f43ea/08048/time_per_phase.webp 1400w&quot;
              sizes=&quot;(max-width: 1400px) 100vw, 1400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/2a153bca8e82fa1f350fc5a6f17f43ea/53918/time_per_phase.png 360w,
/static/2a153bca8e82fa1f350fc5a6f17f43ea/9aebd/time_per_phase.png 480w,
/static/2a153bca8e82fa1f350fc5a6f17f43ea/0f09e/time_per_phase.png 640w,
/static/2a153bca8e82fa1f350fc5a6f17f43ea/3e3fe/time_per_phase.png 860w,
/static/2a153bca8e82fa1f350fc5a6f17f43ea/64756/time_per_phase.png 1200w,
/static/2a153bca8e82fa1f350fc5a6f17f43ea/3643c/time_per_phase.png 1400w&quot;
            sizes=&quot;(max-width: 1400px) 100vw, 1400px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/2a153bca8e82fa1f350fc5a6f17f43ea/3643c/time_per_phase.png&quot;
            alt=&quot;time per phase&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 57.22222222222223%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABzElEQVR42q2TXWvUUBCG85vFKy+9F2/0QhAFKa1awWqtCC3WD1qrFqWrWN1d0f3I4m6STU5OcnLysXmcbbqVvbB44YSBw2TyZPK+E4f/EHXd5DycpjBjNC4IVElqS+L031KbisTk2KI8gzsNuWRlPeNdy9CfGAaSQ+/8nPeMphZ/kjD4McEPFWVZ4ZisoKwM65uW5+8r7m1k+HGKGzQPuf45GWTkRlLHTFWCzQucMEpI85QHdz1eXNlkdWtG28353k8JtIC9vwNHfkpeVEt6OkY0qOR6uOLy8uI1Ht1o8fhOh50PsPcxx40KRqoQQHYG+hXJWT53KPW8atyolzWE1Zs9di/dYuPCGk8ub/H0/k9uX+/TOx4wOGqLafoEOvQt/X7I/oFi++o2xOMGdgpyFujOsaL76oivex06B126rz/J+Rvdt216+y2CiY8/1YxDw5dDl/abNp+fHVKG3p/dWazNIvKqRJkEX4nIWhPEMaFOmCaip7joBSFRFKMSjRcpssISaUOSGqpqtgxsXlBTz2bE4nIwVcTSbIyV5kZ4a0VvuV9LcyGO1qdTZVL3gwiT2eUJl9efk4ZQaVQsU8pkC8DCguYPac7zCed7+BvQWEKeYY4ptgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/a74389fc93489632315c8a963695a903/33ecc/typer_phase_stats.webp 360w,
/static/a74389fc93489632315c8a963695a903/263a4/typer_phase_stats.webp 480w,
/static/a74389fc93489632315c8a963695a903/0c8fb/typer_phase_stats.webp 640w,
/static/a74389fc93489632315c8a963695a903/521f7/typer_phase_stats.webp 860w,
/static/a74389fc93489632315c8a963695a903/cbd37/typer_phase_stats.webp 1200w,
/static/a74389fc93489632315c8a963695a903/08048/typer_phase_stats.webp 1400w&quot;
              sizes=&quot;(max-width: 1400px) 100vw, 1400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/a74389fc93489632315c8a963695a903/53918/typer_phase_stats.png 360w,
/static/a74389fc93489632315c8a963695a903/9aebd/typer_phase_stats.png 480w,
/static/a74389fc93489632315c8a963695a903/0f09e/typer_phase_stats.png 640w,
/static/a74389fc93489632315c8a963695a903/3e3fe/typer_phase_stats.png 860w,
/static/a74389fc93489632315c8a963695a903/64756/typer_phase_stats.png 1200w,
/static/a74389fc93489632315c8a963695a903/3643c/typer_phase_stats.png 1400w&quot;
            sizes=&quot;(max-width: 1400px) 100vw, 1400px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/a74389fc93489632315c8a963695a903/3643c/typer_phase_stats.png&quot;
            alt=&quot;typer phase stats&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;이런 시도를 하게 된 계기는 cats 라이브러리에서 typeclass 인스턴스 정의 위치를 리팩토링했던 경험을 공유한 글을 보고입니다(&lt;a href=&quot;https://github.com/typelevel/cats/pull/3043&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;관련 PR&lt;/a&gt;). 해당 글에서 리팩토링 후 예상치 못하게 컴파일 속도가 향상된 경험을 공유하고 있고 이를 보고 저희 프로젝트에도 시도 해보게 되었습니다.&lt;/p&gt;
&lt;p&gt;결과적으로 해당 서브 프로젝트의 컴파일 속도를 대략 65초에서 45초로 1.44배 빠르게 할 수 있었습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;앞서 typeclass 인스턴스 검색에 사용된 시간은 &lt;code&gt;time spent in implicits&lt;/code&gt; 로 확인 가능하다고 말씀드렸습니다. 하지만 여기에는 한계점이 있습니다. 스칼라에선 typeclass를 네이티브 하게 지원하지 않기 때문입니다. 결국 implicit을 이용해서 typeclass 패턴을 구현하게 되는데 그러기 때문에 프로파일러 레벨에선 어떠한 implicit 검색이 typeclass 인스턴스를 검색한 것인지, 아니면 다른 용도인 것인지 구분할 수 없습니다. 따라서 implicit 검색에 걸린 시간중 typeclass 인스턴스 검색의 비중은 코드베이스에서 typeclass를 얼마나 사용 중인지를 통해 유추해야 합니다. 더 자세히 검증하고 싶다면 &lt;code&gt;scala-profiling&lt;/code&gt; 을 이용해서 implicit search flamegraph를 확보하는 것입니다. Flamegraph를 확인하면 현재 typeclass 인스턴스 검색에 많은 시간이 소비되고 있는지 쉽게 파악이 가능합니다. 물론 이 역시 정량적으로 확인하기는 힘들지만, 프로파일링 결과만 보는 것보다 더 많은 단서를 얻을 수 있습니다. 아래는 flamegraph의 예시입니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/0de199df66c6c253bf63b8538205bd75/implicit_flamegraph.gif&quot; alt=&quot;기술블로그 플레임그래프.gif&quot;&gt;&lt;/p&gt;
&lt;p&gt;칸 위에 마우스 포인터를 올리면 어떤 타입을 implicit 하게 검색했고 검색에 든 시간이 나옵니다. 따라서 전체적으로 확인해 보면 implicit 검색이 타입클래스 인스턴스를 많이 검색한 것인지 그 외의 경우인 것인지 파악이 가능합니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/scalacenter/scalac-profiling&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;&lt;code&gt;scala-profiling&lt;/code&gt;&lt;/a&gt; 의 자세한 사용법이 궁금하신 분을 위해 링크를 첨부했습니다. 추가로 &lt;code&gt;scalac-profiling&lt;/code&gt;을 사용하여 Scala 빌드 서버인 bloop을 최적화한 사례를 소개한 글도 상당히 유익합니다. (&lt;a href=&quot;https://www.scala-lang.org/blog/2018/06/04/scalac-profiling.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;링크&lt;/a&gt;)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;build-pipelining&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#build-pipelining&quot; aria-label=&quot;build pipelining permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Build Pipelining&lt;/h2&gt;
&lt;p&gt;스칼라 컴파일러는 build pipelining을 제공합니다. 이 기능은 서브 프로젝트 간에 의존성이 있을 때 백엔드 단계까지 실행해야 나오는 결과물을 기다리는 게 아닌, 프론트엔드 단계까지 실행 후 나온 결과물을 이용하여 바로 다음 서브 프로젝트를 컴파일할 수 있도록 하는 기능입니다. 예를 들어 다음과 같은 의존성 그래프를 가진 프로젝트가 있다고 하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2150px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 18.333333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAECAYAAACOXx+WAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAyElEQVR42kWP226DMBBE+f8vbAoBJYX4ioFyazHOqW0pqaV98ZmZ3Ske7Yl6BER30t092xJ4PWc8XzePlgElAm3kg/VvnrRt5MmbMrqYVQwDVFWPUgfWQG9mtn3DOUt5kUwTlKWhrh3jCFqsLMvCss5Un3e0ekavzxkpq+h7uHyo/Gk0TMOOPz3z/E1dmSy6Xh1NM8UlYPUPx/Gb59ZIlHwFuswLFetY/cy1ZDx9X/8rj/2JiDUSTyO6wOjON0/axJPXxkulCPwBsPIw6lTvfDwAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/d87a82146e1ed82794dc98d27a7bc5bf/33ecc/dependency.webp 360w,
/static/d87a82146e1ed82794dc98d27a7bc5bf/263a4/dependency.webp 480w,
/static/d87a82146e1ed82794dc98d27a7bc5bf/0c8fb/dependency.webp 640w,
/static/d87a82146e1ed82794dc98d27a7bc5bf/521f7/dependency.webp 860w,
/static/d87a82146e1ed82794dc98d27a7bc5bf/cbd37/dependency.webp 1200w,
/static/d87a82146e1ed82794dc98d27a7bc5bf/e99cb/dependency.webp 2150w&quot;
              sizes=&quot;(max-width: 2150px) 100vw, 2150px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/d87a82146e1ed82794dc98d27a7bc5bf/53918/dependency.png 360w,
/static/d87a82146e1ed82794dc98d27a7bc5bf/9aebd/dependency.png 480w,
/static/d87a82146e1ed82794dc98d27a7bc5bf/0f09e/dependency.png 640w,
/static/d87a82146e1ed82794dc98d27a7bc5bf/3e3fe/dependency.png 860w,
/static/d87a82146e1ed82794dc98d27a7bc5bf/64756/dependency.png 1200w,
/static/d87a82146e1ed82794dc98d27a7bc5bf/0c0fb/dependency.png 2150w&quot;
            sizes=&quot;(max-width: 2150px) 100vw, 2150px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/d87a82146e1ed82794dc98d27a7bc5bf/0c0fb/dependency.png&quot;
            alt=&quot;dependency&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;이 경우는 C를 컴파일해야 B를 컴파일 할 수 있는 상황입니다. 하지만 pipelining을 사용 시 C의 백엔드 단계까지 기다릴 필요가 없습니다. B를 컴파일 할 때 필요한 정보는 C의 타입 트리로 충분하며 &lt;code&gt;.class&lt;/code&gt; 파일까지 필요로 하지 않습니다. Pipelining은 이 점을 이용하여 C의 typer 페이즈가 끝난 후 생성된 AST(Abstract Syntax Tree)를 이용하여 B를 컴파일합니다. 이때 typer에서 나온 트리를 직렬화해서 저장하는 단계를 pickler라고 합니다. 그리고 이때 생성된 결과물은 sbt에서 early output이라고 합니다.&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 13.88888888888889%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAADCAYAAACTWi8uAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAdklEQVR42k1NWQpEIQzz/qcU8UdBBfe9QwpPplAS0yQKrTVJKUkpReA5Z7r3vsV8+M/nnOzFnHOeX6y1mEAEL6VQa43GGLT3fogC6PBBq7VSSol675z7vMJaywS/ee+5HBhj5HAIgQO4O+f4Dg1vlBhjOI+F/gPtDel6pUp3agAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/0820afca52a5909ab9075e7bfc760e10/33ecc/without_pipelining.webp 360w,
/static/0820afca52a5909ab9075e7bfc760e10/263a4/without_pipelining.webp 480w,
/static/0820afca52a5909ab9075e7bfc760e10/0c8fb/without_pipelining.webp 640w,
/static/0820afca52a5909ab9075e7bfc760e10/521f7/without_pipelining.webp 860w,
/static/0820afca52a5909ab9075e7bfc760e10/cbd37/without_pipelining.webp 1200w,
/static/0820afca52a5909ab9075e7bfc760e10/548e7/without_pipelining.webp 2400w,
/static/0820afca52a5909ab9075e7bfc760e10/d309b/without_pipelining.webp 2948w&quot;
              sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/0820afca52a5909ab9075e7bfc760e10/53918/without_pipelining.png 360w,
/static/0820afca52a5909ab9075e7bfc760e10/9aebd/without_pipelining.png 480w,
/static/0820afca52a5909ab9075e7bfc760e10/0f09e/without_pipelining.png 640w,
/static/0820afca52a5909ab9075e7bfc760e10/3e3fe/without_pipelining.png 860w,
/static/0820afca52a5909ab9075e7bfc760e10/64756/without_pipelining.png 1200w,
/static/0820afca52a5909ab9075e7bfc760e10/98e2c/without_pipelining.png 2400w,
/static/0820afca52a5909ab9075e7bfc760e10/07c6b/without_pipelining.png 2948w&quot;
            sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/0820afca52a5909ab9075e7bfc760e10/98e2c/without_pipelining.png&quot;
            alt=&quot;without pipelining&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 23.888888888888886%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAmElEQVR42p2Q2QrEIBAE/f+PjIGAiIhRIYfx6KVdDCFPywqDTcNUiWKeZ0zThGVZIKWEMQa1Vvx7xHVdaK11CGffd2zbhvM8kXNGKQXHcfT7J6BS6l7WWt8AZsrYO+d6TzGPtRYppZ7ZPUewHK97Z0Io8973jhBmikIIWNe1Q/iAGOMXOKzD9swcQrhMOPNYHBCKKOVXMX8ApaWH/zLAdyIAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/0711f4c1b33b4d7bea5275e757e39ebf/33ecc/with_pipelining.webp 360w,
/static/0711f4c1b33b4d7bea5275e757e39ebf/263a4/with_pipelining.webp 480w,
/static/0711f4c1b33b4d7bea5275e757e39ebf/0c8fb/with_pipelining.webp 640w,
/static/0711f4c1b33b4d7bea5275e757e39ebf/521f7/with_pipelining.webp 860w,
/static/0711f4c1b33b4d7bea5275e757e39ebf/cbd37/with_pipelining.webp 1200w,
/static/0711f4c1b33b4d7bea5275e757e39ebf/548e7/with_pipelining.webp 2400w,
/static/0711f4c1b33b4d7bea5275e757e39ebf/b443b/with_pipelining.webp 2592w&quot;
              sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/0711f4c1b33b4d7bea5275e757e39ebf/53918/with_pipelining.png 360w,
/static/0711f4c1b33b4d7bea5275e757e39ebf/9aebd/with_pipelining.png 480w,
/static/0711f4c1b33b4d7bea5275e757e39ebf/0f09e/with_pipelining.png 640w,
/static/0711f4c1b33b4d7bea5275e757e39ebf/3e3fe/with_pipelining.png 860w,
/static/0711f4c1b33b4d7bea5275e757e39ebf/64756/with_pipelining.png 1200w,
/static/0711f4c1b33b4d7bea5275e757e39ebf/98e2c/with_pipelining.png 2400w,
/static/0711f4c1b33b4d7bea5275e757e39ebf/eaf97/with_pipelining.png 2592w&quot;
            sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/0711f4c1b33b4d7bea5275e757e39ebf/98e2c/with_pipelining.png&quot;
            alt=&quot;with pipelining&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;sbt에서 build pipelining을 활성화하는 방법은 간단합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;ThisBuild &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; usePipelining &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;얼핏 보면 간단한 설정 하나만으로 큰 컴파일 속도 개선을 얻을 수 있을 거처럼 보입니다. 하지만 내부적으로는 빌드 과정이 복잡해지는 만큼 신경 써줘야 할 부분이 생기기 마련입니다. 따라서 저희 프로젝트에서 적용하면서 겪은 문제와 그 해결 방안을 공유해드리려 합니다.&lt;/p&gt;
&lt;h3 id=&quot;pipelining과-매크로&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#pipelining%EA%B3%BC-%EB%A7%A4%ED%81%AC%EB%A1%9C&quot; aria-label=&quot;pipelining과 매크로 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;pipelining과 매크로&lt;/h3&gt;
&lt;p&gt;앞서 build pipelining은 pickler 페이즈까지만 실행 후 나온 결과물만을 이용하여 다음 의존성을 컴파일한다고 하였습니다. 여기서 한 가지 신경 써야 할 것은 매크로입니다. 스칼라는 프로그래머가 매크로를 정의하고 이를 호출하여 컴파일 타임에 코드를 생성할 수 있습니다. 매크로 코드는 컴파일 타임에 실행된다는 특성 때문에, 매크로 확장(macro expand)을 위해서는 실행할 수 있는 단계까지 컴파일된 스칼라 프로그램이 필요합니다. 이 특성으로 인해 매크로는 build pipelining을 방해하는 경우가 있습니다.&lt;/p&gt;
&lt;p&gt;먼저 C에 어떤 매크로 코드가 정의되어 있고 B에서 이 매크로를 호출하는 상황을 가정 해보겠습니다. 이럴 경우 C의 pickler 페이즈에서 나온 결과물만 가지고 B를 컴파일 할 수 없습니다. 왜냐하면 B를 컴파일 할 때 매크로 확장을 해야 하기 때문입니다. 다른 말로 하면 B를 컴파일할 때 C에 정의된 매크로 코드를 실행할 수 있어야 하기 때문입니다. 그렇기 때문에 타입 검사 후 얻은 AST 트리만으로는 부족하며 이 경우는 pipelining이 불가능합니다.&lt;/p&gt;
&lt;p&gt;반대의 경우도 있습니다. 매크로 확장 시 사용자가 정의한 코드에 따라 동작을 다르게 하고 싶은 상황을 예로 들어보겠습니다. 이 경우 매크로 코드가 실행될 때 사용자가 정의한 코드를 실행할 수 있어야 합니다. 따라서 이 경우 역시 typer 페이즈까지만 실행해서는 정보가 부족합니다.&lt;/p&gt;
&lt;p&gt;예를 들어 &lt;a href=&quot;https://github.com/scalalandio/chimney&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;chimney&lt;/a&gt;에서는 라이브러리 사용자가 직접 &lt;code&gt;TransformedNamesComparison&lt;/code&gt; 을 정의하여 매크로가 transformer를 생성하는 동작을 제어 가능합니다. 이때 &lt;code&gt;TransformedNamesComparison&lt;/code&gt; 의 타입 정보만 가지고는 매크로를 확장할 수 없기 때문에 pipelining이 불가능합니다.&lt;/p&gt;
&lt;div class=&quot;small-text-code&quot;&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;chimney 예시 코드

&lt;span class=&quot;token comment&quot;&gt;// CustomTransformerConfiguration.scala&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// chimney에서 변환하려는 메시지간 필드 이름 비교시 사용될 함수를 정의 가능&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;object&lt;/span&gt; CustomTransformerConfiguration &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;object&lt;/span&gt; CustomNamesComparison &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; TransformedNamesComparison &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; namesMatch&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fromName&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; toName&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Boolean&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Transformers.scala&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;object&lt;/span&gt; Transformers &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;implicit&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; cfg &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; TransformerConfiguration&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;default&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;enableCustomSubtypeNameComparison&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;CustomTransformerConfiguration&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;CustomNamesComparison&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;implicit&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; aToB&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Transformer&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;A&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; B&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Transformer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;derive &lt;span class=&quot;token comment&quot;&gt;// 매크로 확장시 CustomNamesComparison 사용&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;위 사례들처럼 pipelining이 불가능한 의존성을 가지고 있을 때 사용하는 옵션이 &lt;code&gt;exportPipelining&lt;/code&gt; 입니다. 해당 옵션을 false로 준 서브 프로젝트의 경우 early output을 산출하지 않습니다. 따라서 이 옵션으로, 순차적으로 컴파일해야 하는 부분을 조절할 수 있습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;매크로 정의에 의존하는 경우(전자의 경우) sbt가 자동으로 감지하고 순차적으로 컴파일합니다. 이는 암묵적으로 이루어집니다. 따라서 의식하지 못한 매크로 정의로 인해 pipelining을 활용하지 못하는 경우가 발생할 수 있어 주의가 필요합니다. 아래와 같이 sbt 디버그 로그를 활성화하면 매크로로 인해 early output 산출이 안 되는 경우를 확인할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// sbt의 로그레벨을 (set logLevel := Level.Debug) Debug로 해야 볼수 있는 메시지&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 그렇기 때문에 매크로 정의로 early output 산출에 실패해도 바로 알아차리기 힘들다&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;projectA &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; compile&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; early output can&apos;t be made because &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; macros&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;또 다른 확인 방법은 end-to-end 컴파일 과정을 지켜보는 것입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; Compile &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; compileIncremental 30s
&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;B&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; Compile &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; compileIncremental 20s
&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; Compile &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; compileIncremental 10s&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Build pipelining으로 인해 병렬적으로 컴파일되고 있다면, 컴파일 과정을 실시간으로 지켜보는 것만으로 바로 알 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Build pipelining은 typer 페이즈에서 걸리는 시간이 적을수록 많은 이득을 누릴 수 있습니다. 따라서 프로젝트의 페이즈별 시간마다 pipelining의 효과는 다를 수 있습니다. 킹덤 서버의 경우 전체 프로젝트 컴파일 속도를 1.22배 빠르게 할 수 있었습니다.&lt;/p&gt;
&lt;h2 id=&quot;결론&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%B0%EB%A1%A0&quot; aria-label=&quot;결론 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;결론&lt;/h2&gt;
&lt;p&gt;지금까지 총 두 가지 컴파일 속도 개선 방법을 알아보았습니다. 첫 번째는 typeclass 인스턴스를 companion 오브젝트에 정의함으로써 인스턴스 검색 시간을 줄이는 방법입니다. 두 번째는 build pipelining을 통해 병렬성을 확보하는 것입니다. Pipelining 사용 시 신경 써야 할 점은 두 가지입니다. 첫 번째는 매크로 코드 또는 매크로 확장 시 사용되는 코드는 pipelining이 불가능하다는 것입니다. 따라서 이를 고려하여 서브 프로젝트를 분리하고 exportPipelining을 알맞게 설정하는 것이 중요합니다. 두 번째는 typer phase에서 시간을 많이 쓸수록 pipelining으로 얻을 수 있는 병렬성은 감소한다는 것입니다. 앞으로도 새로운 방법을 찾으면 공유해드리도록 하겠습니다. 많은 관심 부탁 드립니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[그래서 QA가 왜 필요한데?]]></title><description><![CDATA[QA가 프로젝트 내에서 어떤 가치를 창출하는지를 생각하고 나눠봅니다.]]></description><link>https://tech.devsisters.com/posts/why-qa-matters</link><guid isPermaLink="false">https://tech.devsisters.com/posts/why-qa-matters</guid><pubDate>Tue, 26 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;저에게는 게임 개발에 QA라는 직군이 있는 줄도 몰랐던 시절이 있었습니다. 사실 게임은 그냥 코드를 뚝딱하고 짜면 짠! 하고 만들어지는 것이라고 생각을 했다는 것이 더 정확할 겁니다. ‘개발자 = 똑쟁이’ 라서 저절로 양질의 제품이 만들어지겠지... 하는 바보 같은 생각이었죠. 우연한 기회로 QA를 업으로 삼고 나니, 테스트라는 것이 정말 소프트웨어 개발에 있어 상당히 중요하구나라는 것을 소름 돋을 정도로 느끼게 되었습니다. 생각보다 게임에는 고쳐야 할 것들이 엄청나게 많거든요.&lt;/p&gt;
&lt;p&gt;우선 QA 활동이 게임 개발에 왜 필요한지는 아래 문장으로 간단하게 요약할 수 있을 것 같습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“QA활동을 통해 게임 내 잠재위험과 문제점을 사전에 찾아내어, 제품의 품질을 보증하고 릴리즈 이후 발생하는 비용을 감소시킬 수 있다.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;위 문장이 전반적인 QA활동에 대해 적절히 잘 표현한 것 같습니다. 그렇지만 이렇게 정리하면 너무 식상합니다. 마치 면접장에서 많이 들을 수 있는 대답 같아 보이지 않나요?&lt;/p&gt;
&lt;p&gt;그렇다면 좀 더 깊게 생각해봅시다. 만약 &lt;strong&gt;“그래서 너의 업무가 프로젝트 안에서 어떤 가치를 창출하는데?”&lt;/strong&gt;라는 공격적인 질문을 갑자기 받게 되었다면, 빠르고 정확하게 저희의 가치를 상대방의 뇌리에 각인 시킬 수 있으신가요? 몇 년 전의 저였다면, 아마 흔들리는 동공과 떨고 있는 꽉 쥔 주먹만 보여주었을 것 같습니다.&lt;/p&gt;
&lt;p&gt;게임 QA로 오랫동안 일하다 보니 가끔 시간이 날 때 마다 ‘나는 프로젝트에서 어떤 가치를 가질까?’하는 질문을 떠올립니다. 그래서 이번 포스팅은 그것에 대한 나름의 답을 같이 이야기하고자 합니다. 테크니컬한 부분은 없습니다만, 특별히 게임 QA를 직업으로 삼고 애정을 가진 많은 분들이 자부심을 가질 수 있도록 조금이나마 도움이 되었으면 합니다.&lt;/p&gt;
&lt;h2 id=&quot;1-게임에서의-품질-기준&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EA%B2%8C%EC%9E%84%EC%97%90%EC%84%9C%EC%9D%98-%ED%92%88%EC%A7%88-%EA%B8%B0%EC%A4%80&quot; aria-label=&quot;1 게임에서의 품질 기준 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;1. 게임에서의 품질 기준&lt;/h2&gt;
&lt;p&gt;QA가 Quality Assurance 인 것은 모두 잘 알고 계실겁니다. 직역하면 역시나 ‘품질 보증’이구요. 어쨌든 단어풀이는 이해가 되는데, 그에 비해 예전부터 명확치 않았던 것이 있었습니다. 우리의 테스트 활동이 어떻게 품질을 &lt;strong&gt;‘보증’&lt;/strong&gt;하는 것일까요? 농축산품처럼 품질등급을 도장으로 쾅 찍는 것도 아니고, 공산품처럼 6시그마(σ) 품질관리를 활용해 불량률을 정확하게 평가할 수 있는 것도 아닌데 말이죠. 소프트웨어 제품이라는 두루뭉술한 대상에 대해서 우리는 어떻게 품질을 평가하고 있는걸까요?&lt;/p&gt;
&lt;p&gt;이에 대한 정의는 사람에 따라 조금씩 다르겠지만, Software Testing Help 사이트에서 설명한 내용이 오늘 포스팅의 주제에 부합할 것 같습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;소프트웨어 품질보증(Software quality assurance, SQA)은 모든 소프트웨어 공학 프로세스, 방법 및 작업 결과물을 모니터링하여 정의된 표준을 준수하는지 확인하는 수단이자 관행이다.&lt;br/&gt;&lt;br/&gt;
출처 : &lt;a href=&quot;https://www.softwaretestinghelp.com/software-quality-assurance/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://www.softwaretestinghelp.com/software-quality-assurance/&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;여기서 말하는 QA는 “정의된 표준을 준수하였는가?”에 초점을 맞추고 있습니다. 그렇다면 우리는 먼저 게임 서비스에서 ‘정의된 표준’이 무엇인지 정리해야 할 것 같습니다.&lt;/p&gt;
&lt;p&gt;ISTQB Foundation을 공부하셨다면 ‘요구사항’에 대한 내용을 기억하실 겁니다. 이는 사용자가 제품에 원하는 여러가지 기능/비기능적 사양을 뜻합니다. 이 요구사항이 제대로 충족되었는지 인수 테스트를 거친 다음 비로소 제품이 사용자에게 전달되게 됩니다. 즉, 사용자 요구사항이 명확히 정의되면서 표준(Standards)이 되고, 이 기준선을 통과한 상태를 우린 품질이 보증된 상태라고 볼 수 있을 것 같습니다.&lt;/p&gt;
&lt;p&gt;그럼 라이브 서비스되는 게임에서 유저 요구사항은 무엇일까요? 이에 대해 &lt;strong&gt;‘확실하게 정의할 수 없다’&lt;/strong&gt;는 것이 제 의견입니다. 게임을 이용하는 유저들은 다양한 의견을 표출합니다. 서로 상반되어 충돌이 야기되는 의견도 심심치 않게 나오곤 하죠. 주류의견은 있을 수 있어도 통일된 명확한 요구조건은 나오기 힘든 환경이라고 볼 수 있습니다. 게다가 라이브 서비스라는 특징 때문에 이 요구사항들은 각종 조건에 따라 변경되거나 철회될 수도 있습니다. 이는 게임의 ‘재미’가 주관적인 측면이 강한 평가 요소이기도 하고, 또한 게임 서비스는 특정 기능이 필요한 사용자에게만 제공되는 것이 아닌 불특정 다수에게 배포되기 때문에 발생하는 필연적인 현상이라고 생각합니다.&lt;/p&gt;
&lt;p&gt;결국 이런 상황에서 개발진은 선택의 기로에 놓이게 됩니다. 모든 의견은 소중하지만 우리의 시간은 유한하고 일정은 빡빡합니다. 따라서 먼저 VOC들의 우선순위를 설정하여 목록화 하고 분석 해야 할 필요성이 생깁니다. 어쩌면 개발 여건에 따라 약간의 타협이 필요할 수도 있습니다. 이렇게 되면 유저의 요구사항에는 ‘개발진의 의도’가 자연스럽게 반영됩니다. 요구사항을 명확하게 정의할 수 없기 때문에 하릴없이 자체적으로 가치를 부여하게 되기 때문이죠.&lt;/p&gt;
&lt;p&gt;그래서 게임 개발진은 유저들과 소통해야 하는 의무를 동시에 지게 됩니다. 이런 의도가 어떤 방식으로 반영되었고 어떤 이점이 있는지, 어떤 문제점을 예상하는지, 앞으로의 계획은 어떤 지에 대한 상세한 소명이 있어야 한다는 뜻 입니다.&lt;/p&gt;
&lt;h2 id=&quot;2-qa활동의-가치&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-qa%ED%99%9C%EB%8F%99%EC%9D%98-%EA%B0%80%EC%B9%98&quot; aria-label=&quot;2 qa활동의 가치 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;2. QA활동의 가치&lt;/h2&gt;
&lt;p&gt;그럼 이제 QA활동이 제품에 어떤 기여를 하는가에 대한 내용을 고민해볼 차례입니다.&lt;/p&gt;
&lt;p&gt;사실 제 결론은 간단합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“게임 개발진과 유저 간의 원활한 소통을 위해 QA는 다양한 활동을 통하여 제품의 장애(Failure)를 최소화 하고, 소통의 질을 높이는데 기여한다.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이것을 한번 풀어서 설명해보겠습니다.&lt;/p&gt;
&lt;p&gt;유저분들이 보내주시는 피드백 중 가장 기분이 좋은 건 단연 게임이 재밌다는 의견일 겁니다. 또는 ‘유저를 배려한 UI’, ‘게임 그래픽이 너무 귀엽다’ 같은 의견도 입가에 미소를 짓게 합니다. 이런 긍정적인 피드백이 있는 반면 ‘밸런스가 엉망이다’, ‘보상을 얻기 너무 힘들다’ 같은 따끔한 질책의 피드백도 다수 받게 됩니다. 다만 이 두 상반된 피드백들의 공통점이 있다면 적어도 &lt;strong&gt;‘제품(게임)’&lt;/strong&gt;에 기반한 피드백이라는 것 입니다.&lt;/p&gt;
&lt;p&gt;만약 여기에 ‘버그’가 눈치 없이 끼어든다면 어떻게 될까요? 아마도 저희는 제품기반 피드백을 받기 어렵게 될 확률이 높아지게 될 것입니다. 소프트웨어, 특히 게임에서 버그는 사용자 경험을 해치는 가장 명백한 원인입니다. 사소한 버그는 (어쩌면 웃고 넘어갈 수도 있겠지만) 팀의 전문성을 낮게 평가하게 되는 계기가 됩니다. 거기에 이용이 불가능하거나 밸런스를 무너트리는 심각한 버그는 유저의 게임 플레이 의욕을 현저히 저하시킵니다.&lt;/p&gt;
&lt;p&gt;이처럼 버그는 경중에 관계없이 유저에게 부정적인 경험을 전달할 수 밖에 없습니다. 이런 상태에서 유저가 제품에 기반한 피드백을 온전히 전달할 수 있을까요? 아마도 버그에 대한 제보와 하소연, 분노를 표출하느라 정작 게임성은 눈에 들어오지도 않을 것 같습니다. 물론 버그가 많아도 제품에 대한 적절한 피드백을 주는 유저분들이 반드시 계실 것 입니다. 하지만 개발진은 이슈에 대한 관리없이, 요행에 가까운 의견을 보고 판단해서는 안됩니다.&lt;/p&gt;
&lt;p&gt;앞서 설명했듯이 게임 서비스는 다수의 유저에게서 다양한 피드백을 전달받기 때문에, 확실한 품질 표준을 기대하기 힘든 환경입니다. 개발진은 유저와 서로 개발 위탁계약을 통해 제품을 제공하고 있는 것이 아니므로, 적어도 유저들에게 선보일 사양들에 대해 내부적으로 명확한 품질 기준을 설정해야 합니다. 여러가지 방법이 있겠지만 가장 쉬운 방법은 ‘기획서 기반 테스트 케이스’를 작성하는 것 입니다. 여튼 이렇게 설정된 기준을 넘었다면, 우선은 최소한의 품질이 보증된 상태라고 할 수 있을 것 같습니다.&lt;/p&gt;
&lt;p&gt;품질이 보증된 상태의 제품은 개발진의 의도를 명확히 설명할 수 있는 훌륭한 기반이 됩니다. 상점에서 물건을 사는 상황을 생각해봅시다. 물건의 기능을 상점주인이 열심히 설명하고 있는데 들고 있는 물건을 보니 여기저기 흠집이 보이고, 심지어 손때까지 타서 꼬질한 상태라면, 그 물건이 기능상 아무리 좋아도 사고 싶은 마음이 사그라듭니다. 그러면 그 때부터는 말이 반대쪽 귀로 빠져나가면서 ‘그냥 빨리 집으로 가야겠다…’ 라고 되뇌이며 어색한 웃음과 함께 재빨리 자리를 피할겁니다.&lt;/p&gt;
&lt;p&gt;위 예시처럼 제품이 말끔해야 개발진도 유저에게 명확한 개발의도를 전달할 수 있는 기회가 생깁니다. 버그 때문에 여기저기 너덜너덜한 게임을 보여준다면, 제 아무리 멋지고 재미있는 게임을 만들어도 유저들은 너덜너덜한 부분에 대해서만 토로하게 될 것 입니다.&lt;/p&gt;
&lt;p&gt;더 나아가 라이브 서비스 게임은 ‘발전 가능성’이라는 특성을 품고 있습니다. 이 가능성은 양질의 피드백으로부터 활기를 부여받기 때문에, 이런 피드백이 없다면 게임은 방향을 잃고 누구도 만족시키지 못하는 쪽으로 흘러갈 가능성이 높아질 것이라 생각합니다. 결론적으로 QA란 ‘소통’의 질을 극대화한다는 가치를 창출하기 위해 범단계적인 종합 품질 보증 활동을 수행하는 것이라고 정리할 수 있겠습니다.&lt;/p&gt;
&lt;h2 id=&quot;3-마치며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%EB%A7%88%EC%B9%98%EB%A9%B0&quot; aria-label=&quot;3 마치며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;3. 마치며&lt;/h2&gt;
&lt;p&gt;이처럼 QA활동이 단순 ‘버그 찾기’에서 쓰임을 다하는 것이 아닌, 개발진과 유저가 제품에 기반하여 건강한 피드백을 주고받을 수 있는 기틀이 되어줄 수 있다는 것을 나름대로 설명해보았습니다. 저는 그래서 QA가 개발 단계에서는 릴리즈 전 &lt;strong&gt;‘마지막 수문장’&lt;/strong&gt;이지만, 릴리즈 후에는 유저에게 선보이는 &lt;strong&gt;‘첫인상’&lt;/strong&gt; 포지션을 지니게 된다는 점을 알리고 싶습니다. 그러므로 테스트 업무가 모두 종료되었을 때 손 털고 끝났다며 휴식하는 것도 좋지만, 버그로 인해 받을 수도 있었던 소중한 의견들을 받지 못하는 사태가 일어나지 않게끔 모니터링과 후속 대응에도 노력을 기울여야 할 것 입니다.&lt;/p&gt;
&lt;p&gt;이렇게 되면 포스팅 초반 QA활동의 요약에서 언급한 &lt;strong&gt;‘비용 감소’&lt;/strong&gt;에 실질적으로 도움이 됩니다. 비용은 단순히 ‘돈’이 아닌 개발진의 시간과 노력을 포함한 개념입니다. 기획 방향성을 초반에 잘 갖출 수 있다면, 남은 리소스는 개발 품질에 더 투자할 수 있게됩니다. 이 때 유저들의 피드백이 좋은 참고자료로서 사용됩니다. 제품에 대한 진솔한 피드백이 많으면 많을수록 기획 방향성 설정에 이점이 되는 것은 당연한 이치가 아닐 수 없겠습니다. 결국 좋은 QA활동의 결과가 좋은 유저 피드백을 생산하고, 이것을 바탕으로 개발진은 좋은 기획을 창출하여 더 품질에 노력을 쏟을 수 있는 선순환 구조를 만들어 냅니다.&lt;/p&gt;
&lt;p&gt;그러므로 QA로서 일하는 사람들은 버그를 찾는 것은 물론이거니와, 제품에 대한 깊은 관심을 가지고 유저관점에서 분석하려는 노력이 필요합니다. 이는 프리-프로덕션 단계부터 포스트-프로덕션까지 전단계에 걸쳐 유저에게 불편함을 줄 수 있는 모든 요소를 점검 해야함을 방증합니다.&lt;/p&gt;
&lt;p&gt;물론 이런 활동은 쉽지 않습니다. 생각보다 게임은 크고, 방대하며, 복잡합니다. 고도로 발달한 AI가 아닌 이상 실수하며 놓치는 부분도 분명히 생기기 마련입니다. 그러므로 더더욱 유저들의 의견에 대해 귀 기울일 필요가 있습니다. 실수는 용서해도 방치와 기만은 용서가 되지 않을테니까요.&lt;/p&gt;
&lt;p&gt;이 밖에도 세상에는 훌륭한 QA 엔지니어들이 더 좋은 가치를 만들어가고 있습니다. 그저 드리고 싶은 말씀은, QA의 활동이 프로젝트와 유저에게 적지 않은 가치를 창출하고 있다는 점, 그리고 유저 관점에서 생각하는 마음가짐을 가지자는 점입니다. 언제나 좋은 업데이트를 위해 고생하는 모든 QA 직군과 개발진에게 이 글을 바칩니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[지금 매출 얼마인가요?]]></title><description><![CDATA[데브시스터즈 데이터플랫폼에서 준실시간 지표 서비스를 도입한 과정에 대해서 소개합니다.]]></description><link>https://tech.devsisters.com/posts/near-real-time</link><guid isPermaLink="false">https://tech.devsisters.com/posts/near-real-time</guid><pubDate>Mon, 18 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요. 데브시스터즈 데이터플랫폼그룹의 박기범, 서혜인입니다.&lt;/p&gt;
&lt;p&gt;여러분은 사업 지표를 얼마나 신속하고 정확하게 확인하고 싶으신가요? 아마 대부분 가능한 빠른 시점에 정확한 지표를 확인하고 싶어 하실 겁니다. 하지만, 데이터팀 입장에서는 사용자가 원하는 수준으로 정확하고 신속한 지표 서비스를 제공하기 위해 기술적으로 해결해야 하는 많은 문제들에 직면하게 됩니다.
이번 글에서는 데브시스터즈가 지난 수개월간 준실시간 지표 서비스를 도입하는 과정에서 마주하게 된 여러 기술적 도전 과제들과 해결 과정에 대해 소개해드리고자 합니다.&lt;/p&gt;
&lt;h1 id=&quot;게임-런칭-당일에-어떤-지표를-볼-수-있나요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%8C%EC%9E%84-%EB%9F%B0%EC%B9%AD-%EB%8B%B9%EC%9D%BC%EC%97%90-%EC%96%B4%EB%96%A4-%EC%A7%80%ED%91%9C%EB%A5%BC-%EB%B3%BC-%EC%88%98-%EC%9E%88%EB%82%98%EC%9A%94&quot; aria-label=&quot;게임 런칭 당일에 어떤 지표를 볼 수 있나요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;게임 런칭 당일에 어떤 지표를 볼 수 있나요?&lt;/h1&gt;
&lt;p&gt;신규 게임이 출시되는 날, 모든 구성원의 관심은 게임 지표에 집중됩니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;현재 게임을 접속한 유저 수가 어떻게 되나요?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;지금까지 발생한 매출 중에 가장 구매가 많은 상품은 무엇인가요?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;출시 직후 촌각을 다투는 상황에서는, 게임팀에서 유저의 접속 현황과 매출 성과에 따라 빠르게 의사결정을 내릴 수 있도록 “정확하고 신속하게” 지표를 확인할 수 있어야 합니다. 정확하지 않은 지표는 자칫하면 잘못된 의사결정을 초래할 수 있으며, 지표 조회가 지연되면 중요한 순간에 빠른 결정을 내리기 어려워지기 때문입니다.&lt;/p&gt;
&lt;p&gt;이런 이유로 대부분의 데이터 조직은 짧은 지연 시간으로 주요 지표를 확인할 수 있는 &lt;strong&gt;준실시간(near real-time) 지표&lt;/strong&gt; 서비스 도입을 고려하게 됩니다. 하지만, 왜 많은 회사들이 준실시간 지표 서비스 도입에 어려움을 겪고 있을까요?&lt;/p&gt;
&lt;p&gt;각 회사마다 마주한 상황은 다르겠지만, 데브시스터즈에서 그동안 준실시간 지표 서비스를 도입하지 못한 데는 다양한 이유들이 있었습니다.&lt;/p&gt;
&lt;h2 id=&quot;기존-지표-환경&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B8%B0%EC%A1%B4-%EC%A7%80%ED%91%9C-%ED%99%98%EA%B2%BD&quot; aria-label=&quot;기존 지표 환경 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;기존 지표 환경&lt;/h2&gt;
&lt;figure style=&quot;width: 80%; margin: 0 auto;&quot;&gt;
    &lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1962px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 63.61111111111112%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAABYlAAAWJQFJUiTwAAACmElEQVR42oVT2W7TUBD1D/MBPPDEC28IPgAJJCQWUVGEQKKAGlFaUWcjTWKndpw4tuPd8R7bWQ5zb2iFeICRruRl7pk5Z84ImqZhMpkgTVPsdjvMZjOMx2PM53Nst1vUdY0wXEHTZuj2BhgOR7B9F3Ecg0VRFPw+u6PrOgTTNCHJE1TrNZqmgeu6BBDyw8CaZoe5eonrSRuqfA5FlaCedhA5Prb7Hc8xCGM0lvgdwbZtXoH9iKIIR0dHODk54RVrKrCrG7xqfcbFVIXY+4T+VMb70zPMlhb2vxk4jktNXRPL5ACoKAr/UZYlBzcMA+z7ZrPBji4VVYU1MUjzElleIFsXKOmdxYaKLhYL9H8OqMMAwpx4tzs9Dsg0y/McWZZxTZkE/wuWEwQBb2C1WhHgXMel2CHAiicw0H8B7ff72/M3MAvBMBaQpTF1VvApJ0mClHUYr2CT2IyBSRO/HF3B8lz6HvPub1xxE0weDhiEEWyaWFkWnLbneXBmOnzLgiLLGFyRVWiCXyYytLnGqTmuAz/wCWTLgWLSbmmZh6Go0wCtMwtNXZKnSrKNA1daILB9BJSYximqrEYereD7PpIo48dxHE4zJx9+H0j49kOErKgQFFVBW2xzDZtmA3Np4knrIbrTc9RZg74q4tnZY5iegTwpcCw+x3HnKcIgIr03WFc1LhYhLCpiBzSUji7hxeAr9gTGKEx1BXdf3kF31keTb/BRfIv77+4hiEKkUYpHHx7gjfga6SpBRXZqSKYleXIwHMJzPQiSeo1W9wJr8iBLsEg7Q7fg2B5cGgKjZupLSvb5FrkkhWsfbMI0r9hqEsao34O1XELI04yoZByMCcw8dVi94HYF/3xmugahz5+ZdZiOCYF7VJht2i8cnshJ3n2SdgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/ceaab0d13ee6e44505c0a6cb68da96a9/33ecc/data_platform.webp 360w,
/static/ceaab0d13ee6e44505c0a6cb68da96a9/263a4/data_platform.webp 480w,
/static/ceaab0d13ee6e44505c0a6cb68da96a9/0c8fb/data_platform.webp 640w,
/static/ceaab0d13ee6e44505c0a6cb68da96a9/521f7/data_platform.webp 860w,
/static/ceaab0d13ee6e44505c0a6cb68da96a9/cbd37/data_platform.webp 1200w,
/static/ceaab0d13ee6e44505c0a6cb68da96a9/c4364/data_platform.webp 1962w&quot; sizes=&quot;(max-width: 1962px) 100vw, 1962px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/ceaab0d13ee6e44505c0a6cb68da96a9/53918/data_platform.png 360w,
/static/ceaab0d13ee6e44505c0a6cb68da96a9/9aebd/data_platform.png 480w,
/static/ceaab0d13ee6e44505c0a6cb68da96a9/0f09e/data_platform.png 640w,
/static/ceaab0d13ee6e44505c0a6cb68da96a9/3e3fe/data_platform.png 860w,
/static/ceaab0d13ee6e44505c0a6cb68da96a9/64756/data_platform.png 1200w,
/static/ceaab0d13ee6e44505c0a6cb68da96a9/8c930/data_platform.png 1962w&quot; sizes=&quot;(max-width: 1962px) 100vw, 1962px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/ceaab0d13ee6e44505c0a6cb68da96a9/8c930/data_platform.png&quot; alt=&quot;간소화한 데이터 파이프라인 구조&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;간소화한 데이터 파이프라인 구조&lt;/figcaption&gt;
  &lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;데브시스터즈에서는 2015년부터 Spark 과 Elastic Stack 기반의 빅데이터 플랫폼을 운영해 왔습니다.&lt;/p&gt;
&lt;p&gt;2021년에는 Databricks를 도입하여 &lt;a href=&quot;https://www.databricks.com/product/data-lakehouse&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Lakehouse Architecture&lt;/a&gt;와 &lt;a href=&quot;https://www.databricks.com/glossary/medallion-architecture&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Medallion Architecture&lt;/a&gt; 기반의 데이터 환경을 구축했습니다. 이를 통해, DB와 로그 등 게임 서비스와 여러 플랫폼에서 발생하는 다양한 데이터를 가공 및 집계하여 지표 서비스에 활용 중이었습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://youtu.be/1gunX1iwEJM?t=2960&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Databricks Data+AI World Tour 2023 Keynote: Databricks 도입 후의 Devsisters&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;해당 환경에서 지표를 서비스하는 방법은 크게 세 가지로 구성되어 있었습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;게임 서버에 연결된 세션 수를 기반으로 동시 접속자 (이하 &apos;동접&apos;) 지표를 집계합니다.&lt;/li&gt;
&lt;li&gt;시간별 배치로 운영계 DB 테이블을, 일별 배치로 로그를 ETL하여 Data Warehouse와 Data Mart에 적재합니다.&lt;/li&gt;
&lt;li&gt;Elasticsearch 에 적재된 로그 원장을 기반으로 Kibana 에서 준실시간으로 갱신과 조회가 가능한 대시보드를 구성하여 활용합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;기존-지표-환경의-한계&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B8%B0%EC%A1%B4-%EC%A7%80%ED%91%9C-%ED%99%98%EA%B2%BD%EC%9D%98-%ED%95%9C%EA%B3%84&quot; aria-label=&quot;기존 지표 환경의 한계 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;기존 지표 환경의 한계&lt;/h2&gt;
&lt;p&gt;이처럼 기존 지표 환경에서도 게임 런칭 당일 제공할 수 있는 지표들이 존재하였지만, 사용자 요구를 완전히 충족시키기엔 한계가 있었습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;지금 국가별 동접은 볼 수는 없나요? iOS 동접은 어떤가요?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;기존 동접 지표는 서버 입장에서의 세션 수만 수집했기 때문에 국가별, 스토어별 세부 동접 지표는 확인할 수 없었습니다. 특히 게임 런칭 초기에는 특정 국가의 반응, 스토어 및 OS별 접속 현황, 10분 단위의 접속자 추이를 빠르게 파악할 필요가 있었지만, 이를 확인할 수 있는 방법이 없어 불편했습니다.
반면 DB 기반의 시간별 지표에서는 여러 차원에서 지표를 조회할 수는 있었지만, 지표가 집계되기까지 한시간 가량을 기다려야 했기에 신속하게 의사결정을 내리기 어려웠습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;키바나 대시보드가 동작하지 않아요&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Elastic Stack 기반으로 제공되는 키바나 준실시간 대시보드를 통해 여러 지표를 빠르게 확인할 수 있었지만, 게임 런칭과 같이 대규모 트래픽이 몰릴 때는 운영에 어려움이 있었습니다. 게임 CBT 시기에는 문제가 없었지만, 런칭 당일 초당 수십만 건의 로그를 처리할 수 있는 수준으로 Elastic Stack을 운영하기에는 높은 비용이 발생하게 되었습니다. 이로 인해, 게임 런칭 때는 키바나 기반의 대시보드를 제한적으로 활용할 수 밖에 없었습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;키바나 대시보드 지표가 시간별, 일별 지표와 달라요&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;제한적으로 Kibana 대시보드를 활용할 때에도 대시보드의 일부 지표가 실제와 다르게 집계되어 혼선이 발생하기도 했습니다. Elastic Stack에서는 로그 원장을 단순 집계하여 지표를 제공했기 때문에, 국가별 환율을 반영한 원화 환산 매출이나 게임 상품 이름과 같은 메타 정보를 대시보드에서 활용하기에 어려움이 있었습니다. &lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/enrich-setup.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Elasticsearch Ingest Pipelne&lt;/a&gt; 과 같은 기능을 활용해 이를 개선할 수는 있었으나 구현과 관리가 복잡했고, 기존 시간별/일별 지표는 SQL로 ETL되므로 동일한 로직을 새로 구현하는 것 역시 부담이 컸습니다.&lt;/p&gt;
&lt;h1 id=&quot;새로운-준실시간-지표-파이프라인&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%83%88%EB%A1%9C%EC%9A%B4-%EC%A4%80%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%A7%80%ED%91%9C-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8&quot; aria-label=&quot;새로운 준실시간 지표 파이프라인 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;새로운 준실시간 지표 파이프라인&lt;/h1&gt;
&lt;p&gt;위와 같은 한계로 인해 게임 런칭 시 데이터 기반 의사결정이 지연되는 문제를 여러 차례 경험하면서, 이를 해결하기 위한 &lt;strong&gt;새로운 준실시간 지표 파이프라인&lt;/strong&gt;을 개발하게 되었습니다.&lt;/p&gt;
&lt;p&gt;이 목표를 달성하기 위해 다음과 같은 요구사항을 설정했습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;지표의 범위&lt;/strong&gt;: 기존의 일별/시간별 지표로 대체할 수 없는 즉각 확인이 필요한 지표만 준실시간 지표 파이프라인에 포함하기로 했습니다. 그간의 사용자 요청 사항과 인터뷰를 기반으로, 국가, OS, 스토어 등 다양한 기준에서 유저 회원가입 수, 동접, 매출과 같이 핵심 지표를 확인할 수 있는 것을 목표로 설정했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;클라이언트 로그를 적극적으로 활용&lt;/strong&gt;: 설정된 지표를 수집하기 위해서는 서버에서 수집하지 않던 클라이언트의 다양한 맥락 데이터를 활용할 필요가 있었습니다. 기존 클라이언트 로그 SDK의 기능을 확장하여, 주기적으로 로그 수집 서버에 현재의 상태를 보내는 &apos;하트비트 로그&apos;를 추가 하였고, 클라이언트에서 보내주는 정보의 양을 늘렸습니다. 그리고 준실시간 지표 파이프라인에서 클라이언트 로그를 적극적으로 활용하기로 했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;10분 이내의 지연 시간&lt;/strong&gt;: 신속하게 지표를 확인해야 한다는 목적을 달성할 수 있도록, SLO(Service Level Objective) 를 10분으로 설정하였고, 게임 런칭과 같은 고부하 상황에서도 이 요구사항을 만족하는 것을 목표로 설정했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;적절한 데이터 인프라 비용&lt;/strong&gt;: 준실시간 지표는 주로 게임 출시 시 활용되지만, 정기 업데이트나 이슈 상황에서도 사용될 수 있어야 했습니다. 이를 위해, 파이프라인 운영 비용이 게임 인프라에 부담되지 않도록 설계 단계에서부터 기존 파이프라인의 비용을 참고하여 적정 비용 목표를 설정했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;SQL 기반의 ETL&lt;/strong&gt;: 매출을 비롯한 준실시간 지표는 정확하게 집계되어야 하는 동시에 여러 복잡한 비즈니스 로직이 반영되어야 했습니다. 예를 들어, 국가별 매출을 집계할 때는 통화 환율 정보를 반영해야 하며, 환불이나 쿠폰 사용 등 스토어별 결제 플로우에 맞는 복잡한 집계 공식이 반영되어야 했습니다. 이러한 비즈니스 로직을 지표 집계에 반영하기 위해서는 SWE 또는 데이터 엔지니어가 로직을 직접 개발하기보다는 DW 엔지니어 또는 데이터 분석가가 로직 개발에 참여할 수밖에 없었습니다. 또한, 이들의 개발 효율성을 높이기 위해서는 PySpark과 같은 범용적 프로그래밍 언어보다는 SQL로 집계 로직을 개발할 수 있어야 했습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;figure style=&quot;width: 80%; margin: 0 auto;&quot;&gt;
    &lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2348px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 59.72222222222222%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAABYlAAAWJQFJUiTwAAABrUlEQVR42o1SWa7bMBDzFeJFy2iX1zh2/JIWvf/N2LGdFi3Q95oPQgIxooacKdro8PH9B6Z5xv35Deu2QUmFS1mirE5UVYW6qo/zT65i7q+aukYhpEDyBtFb9GlEsBHSCjhHiC4i2RaOHARz1kgEExFNgtYaxiiuD1yTYbimaQSKpmkgGJmLH+OKe//AbVoQgkdk8WQy7C5oTsGd2wUNWSjij4k/tgmBIrQiFHubpCRSDBjyiDkvyLZDWZa/LX5t+VfNyRWWNKaRheYr2pwRY4JS6hSsq9fDM593UKTgsK4PXKcV2SdY5Y6hePKMwJbOc4/mLcHoLO7LHdMwY8wJKXBGPoK0gRSCIQ+83eEQCM/bhq1/YpmuWNaFs5ygBR22L+XllVH9wtcRFJEtz+MVK1u+zSw6bpA8PUE121W8OucEd85oAW88nPYQSvxbkMjAe4e2zRjaBW0cYKIF8d4lx7zr4VhEOQHaP+IYBO9b3XzSoTGGV8aj6zrkNIDcuXOSH5C2MMpCSN5VxaBTaJ/+p5adIbQpop8mxL4DcbfOWu46HCK1rM4IBN/L/w/lJykRN8D39F0rAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/396f2d88471975593df0f7ac175cd8c7/33ecc/old_sql.webp 360w,
/static/396f2d88471975593df0f7ac175cd8c7/263a4/old_sql.webp 480w,
/static/396f2d88471975593df0f7ac175cd8c7/0c8fb/old_sql.webp 640w,
/static/396f2d88471975593df0f7ac175cd8c7/521f7/old_sql.webp 860w,
/static/396f2d88471975593df0f7ac175cd8c7/cbd37/old_sql.webp 1200w,
/static/396f2d88471975593df0f7ac175cd8c7/00dae/old_sql.webp 2348w&quot; sizes=&quot;(max-width: 2348px) 100vw, 2348px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/396f2d88471975593df0f7ac175cd8c7/53918/old_sql.png 360w,
/static/396f2d88471975593df0f7ac175cd8c7/9aebd/old_sql.png 480w,
/static/396f2d88471975593df0f7ac175cd8c7/0f09e/old_sql.png 640w,
/static/396f2d88471975593df0f7ac175cd8c7/3e3fe/old_sql.png 860w,
/static/396f2d88471975593df0f7ac175cd8c7/64756/old_sql.png 1200w,
/static/396f2d88471975593df0f7ac175cd8c7/45d2b/old_sql.png 2348w&quot; sizes=&quot;(max-width: 2348px) 100vw, 2348px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/396f2d88471975593df0f7ac175cd8c7/45d2b/old_sql.png&quot; alt=&quot;시간별/일별 매출 집계 쿼리의 일부분&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;시간별/일별 매출 집계 쿼리의 일부분&lt;/figcaption&gt;
  &lt;/figure&gt;
    &lt;figcaption&gt;실제 쿼리는 훨씬 복잡한 형태로 되어있어, SQL이 아닌 PySpark으로 관리하기에는 기대한 만큼의 생산성을 확보하기 어렵습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;스펙&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8A%A4%ED%8E%99&quot; aria-label=&quot;스펙 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;스펙&lt;/h2&gt;
&lt;p&gt;앞서 설명한 요구사항을 만족할 수 있도록 아래와 같이 구성된 스트리밍 기반의 ETL 파이프라인을 구성하여 준실시간 지표를 서비스하기로 했습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;스트리밍&lt;/strong&gt;: Kafka로부터 로그를 실시간으로 읽을 수 있어야 합니다. 앞서 언급한 클라이언트 로그를 비롯해 데브시스터즈에서 발생한 다양한 로그들은 모두 Kafka로 인입되고 있으며, 로그를 스트리밍 처리하여 가공할 수 있어야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;전처리&lt;/strong&gt;: Kafka로부터 읽어온 로그를 집계에 활용할 수 있는 형태로 가공하는 작업이 필요합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;집계&lt;/strong&gt;: 가공된 데이터를 지표에 활용할 수 있도록 다양한 차원의 조합으로 지표를 집계해야 합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;시각화&lt;/strong&gt;: 집계가 완료된 데이터를 사용자들이 조회할 수 있도록 대시보드로 서비스해야 합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;아래는 준실시간 지표 서비스의 흐름을 각 단계별로 추상화한 그림입니다. 그럼, 각 단계별로 구현해야 하는 상세한 스펙에 대해서 알아보겠습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2110px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 23.055555555555557%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAxklEQVR42m1PywqEMBDr/3+THjwogicRRJRqFRRfrfoFWTNSdw97GEoySTpR1lo459D3PbTW2Pcdx3HISzyOo2DOuq5o21Z29HCGYUDXdS9W13WBoWmaIggCzPMMcsuyIAxDFEWB8zwlkOYoikRDTF+WZYjj+P1EJUmCqqokhCJjDMqylMv8Z03ToK5rbNsm4dwRm7sVQ8jxcn2PyvNcDBTzKlanmCbPsTo1rExumibB1HruCdRPZd+fw4t8HY+d/e7+aX53H0V+cHt7JznnAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/2ba145e0cc1dccdbee03c7842580ba52/33ecc/requirements.webp 360w,
/static/2ba145e0cc1dccdbee03c7842580ba52/263a4/requirements.webp 480w,
/static/2ba145e0cc1dccdbee03c7842580ba52/0c8fb/requirements.webp 640w,
/static/2ba145e0cc1dccdbee03c7842580ba52/521f7/requirements.webp 860w,
/static/2ba145e0cc1dccdbee03c7842580ba52/cbd37/requirements.webp 1200w,
/static/2ba145e0cc1dccdbee03c7842580ba52/7013d/requirements.webp 2110w&quot; sizes=&quot;(max-width: 2110px) 100vw, 2110px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/2ba145e0cc1dccdbee03c7842580ba52/53918/requirements.png 360w,
/static/2ba145e0cc1dccdbee03c7842580ba52/9aebd/requirements.png 480w,
/static/2ba145e0cc1dccdbee03c7842580ba52/0f09e/requirements.png 640w,
/static/2ba145e0cc1dccdbee03c7842580ba52/3e3fe/requirements.png 860w,
/static/2ba145e0cc1dccdbee03c7842580ba52/64756/requirements.png 1200w,
/static/2ba145e0cc1dccdbee03c7842580ba52/1d917/requirements.png 2110w&quot; sizes=&quot;(max-width: 2110px) 100vw, 2110px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/2ba145e0cc1dccdbee03c7842580ba52/1d917/requirements.png&quot; alt=&quot;준실시간 지표 서비스의 추상화된 구조&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;준실시간 지표 서비스의 추상화된 구조&lt;/figcaption&gt;
  &lt;/figure&gt;
&lt;/figure&gt;
&lt;h3 id=&quot;1-스트리밍&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EC%8A%A4%ED%8A%B8%EB%A6%AC%EB%B0%8D&quot; aria-label=&quot;1 스트리밍 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;1. 스트리밍&lt;/h3&gt;
&lt;p&gt;스트리밍 단계에서 구현해야 하는 주요 스펙은 하트비트 로그의 Kafka topic을 분리하는 것이었습니다. 일반적인 로그는 다음날 자정 일 단위 배치를 통해 S3에 저장됩니다. 하지만, 준실시간 지표 서비스에 사용되는 하트비트 로그는 데이터 양이 많아 S3로 저장하는 데 드는 비용이 매우 많이 들지만 분석에 활용되는 빈도는 낮았습니다. 따라서 하트비트 로그를 별도의 Kafka topic으로 분리하여 기존 파이프라인에서 적재되지 않도록 Log Collection 모듈의 로직을 수정해야 했습니다.&lt;/p&gt;
&lt;h3 id=&quot;2-전처리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%EC%A0%84%EC%B2%98%EB%A6%AC&quot; aria-label=&quot;2 전처리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;2. 전처리&lt;/h3&gt;
&lt;p&gt;전처리 단계에서는 매출 집계에 사용되는 로그를 추출하기 위한 작업이 필요했습니다. 매출 로그는, 서버와 클라이언트에서 발생하는 로그 각각에 집계에 필요한 정보가 저장되어 있기 때문에 서로 다른 두 개의 Kafka topic에 담긴 로그들 간의 JOIN 연산을 진행해야 했습니다.&lt;/p&gt;
&lt;p&gt;얼핏 보면 데이터 파이프라인에서 쉽게 구현이 가능해 보이는 스펙이지만 스트리밍 데이터로부터 이를 구현할 때는 몇 가지 어려움들이 숨어있었습니다.
스트리밍에서는 Kafka topic마다 하나의 stream을 생성하는데, 서로 다른 두 Kafka topic의 로그를 조인하려면 stream-stream JOIN 방식이 필요합니다. 본래 JOIN은 데이터가 확정된 (INSERT가 완료된) 테이블들의 레코드를 조합하여 하나의 컬럼으로 표현할 때 사용됩니다. 따라서 계속해서 데이터가 인입되는 스트리밍에서는 각각의 데이터가 확정되는 (INSERT가 완료되는) 시점을 지정하여 JOIN의 기준점으로 사용해야 합니다. 이를 위해, 특정 시점을 기준으로 늦게 들어오는 데이터는 연산에서 DROP 할 수 있는 &lt;a href=&quot;https://spark.apache.org/docs/3.5.1/structured-streaming-programming-guide.html#handling-late-data-and-watermarking&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;watermark&lt;/a&gt; 시간을 정해주어야 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# log 에 찍힌 timestamp 를 기준으로 watermark 를 1분으로 지정한다.&lt;/span&gt;
silver_df &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    bronze_df&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;select&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        to_timestamp&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;col&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;timestamp&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;alias&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;log_datetime&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;withWatermark&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;log_datetime&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;1 minutes&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;또한 각각의 stream 의 데이터 인입 완료 시점이 다를 수 있기 때문에 JOIN 할 다른 stream 의 데이터를 대기하는 시간을 지정해주어야 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;revenue_df &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    server_df&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;join&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        client_df&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
            server_df&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;log_datetime &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; client_df&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;log_datetime &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; expr&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;INTERVAL 5 MINUTES&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            server_df&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;log_datetime &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; expr&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;INTERVAL 5 MINUTES&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; client_df&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;log_datetime&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            server_df&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;id&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; client_df&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;&quot;leftOuter&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;select&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# column from client_df,&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# column from server_df&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;
 &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
 &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 설정값들은 최종적인 지연시간을 고려하여 적절하게 설정해야 합니다. 너무 큰 값으로 설정하면 데이터가 미처 들어오기 전에 JOIN이 이루어져 null 값으로 처리될 위험이 있고, 반대로 너무 작은 값으로 설정하면 지연시간이 커져 최종 지표 제공에 영향을 줄 수 있습니다. 따라서 적절한 균형을 찾아야 합니다.&lt;/p&gt;
&lt;p&gt;또한, 이 과정에서 중요한 요소는 State (&lt;a href=&quot;https://spark.apache.org/docs/3.5.2/structured-streaming-programming-guide.html#state-store&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;StateStore&lt;/a&gt;) 입니다. 파이프라인 내부에서는 JOIN이 이루어지기까지, 즉 watermark를 기준으로 데이터가 모두 들어왔다고 판단하기까지 데이터가 들어올 때마다 연산하여 중간값인 state를 저장하고 업데이트합니다. 데이터의 인입이 완료되면 state 업데이트도 완료되고 테이블에 레코드가 업데이트됩니다. 따라서 state 연산의 복잡도 그리고 state 저장 메모리를 스트리밍 구현 단계에서 염두에 두어야 합니다.&lt;/p&gt;
&lt;h3 id=&quot;3-집계&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%EC%A7%91%EA%B3%84&quot; aria-label=&quot;3 집계 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;3. 집계&lt;/h3&gt;
&lt;p&gt;집계 단계에서는, 위에서 언급한 복잡한 집계 로직을 효율적으로 운영하기 위한 요구사항들이 존재했습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;SQL을 활용할 수 있는가&lt;/li&gt;
&lt;li&gt;내부 환율 테이블과 메타 테이블에 접근하여 집계 과정에서 활용할 수 있는가&lt;/li&gt;
&lt;li&gt;하트비트 로그와 같이, 양이 매우 많은 경우에도 집계 시간이 지연되지 않게 클러스터를 효율적으로 관리하는 것이 가능한가&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;4-시각화&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#4-%EC%8B%9C%EA%B0%81%ED%99%94&quot; aria-label=&quot;4 시각화 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;4. 시각화&lt;/h3&gt;
&lt;p&gt;마지막 시각화 단계에서는, 준실시간 지표를 조회하기 위해 일반적인 BI 대시보드와 달리 아래 특징적인 요구사항들이 존재했습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;자동으로 대시보드 내의 데이터가 짧은 간격으로 갱신될 수 있는가&lt;/li&gt;
&lt;li&gt;분 단위 또는 초 단위의 시계열 그래프를 표현하기에 적합한 대시보드인가&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이처럼, 준실시간 파이프라인을 구축하기 위해서는 각 단계 별로 요구되는 스펙들이 존재하였고 이러한 요구사항들을 구현할 수 있는 플랫폼을 선택하기 위해 다양한 프레임워크들을 검토해 보았습니다.&lt;/p&gt;
&lt;h1 id=&quot;적절한-플랫폼-선택하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%81%EC%A0%88%ED%95%9C-%ED%94%8C%EB%9E%AB%ED%8F%BC-%EC%84%A0%ED%83%9D%ED%95%98%EA%B8%B0&quot; aria-label=&quot;적절한 플랫폼 선택하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;적절한 플랫폼 선택하기&lt;/h1&gt;
&lt;h2 id=&quot;스트리밍-파이프라인&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8A%A4%ED%8A%B8%EB%A6%AC%EB%B0%8D-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8&quot; aria-label=&quot;스트리밍 파이프라인 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;스트리밍 파이프라인&lt;/h2&gt;
&lt;h3 id=&quot;시도-1-ksql&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8B%9C%EB%8F%84-1-ksql&quot; aria-label=&quot;시도 1 ksql permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;시도 1. KSQL&lt;/h3&gt;
&lt;p&gt;위 1. 스트리밍, 2. 전처리, 3. 집계 단계를 수행하는 스트리밍 파이프라인의 플랫폼으로 검토한 첫 번째 프레임워크는 KSQL 이었습니다.&lt;/p&gt;
&lt;p&gt;KSQL은 Kafka streams를 기반으로 한 프레임워크로, 기존 데이터 플랫폼에서 운영 중인 Kafka 클러스터를 그대로 사용할 수 있다는 장점이 있었습니다. 또한, SQL과 유사한 문법으로 DW 엔지니어/분석가의 러닝커브가 낮다는 점도 큰 장점이었습니다.&lt;/p&gt;
&lt;p&gt;하지만 KSQL을 운영데이터에 적용해 보니 데이터 처리 성능 이슈로 인해 정확성 측면에서 문제가 발생했습니다.
KSQL에서는 별도의 Kafka topic을 생성해 aggregation, join에서 발생하는 state를 저장합니다. 이때 Kafka message의 key는 group by에 포함된 column 들로 지정되며 사용자가 임의로 변경할 수 없고, 이를 기준으로 partition을 나누게 됩니다.&lt;/p&gt;
&lt;p&gt;데브시스터즈의 게임들은 한국 사용자 비율이 매우 높기 때문에, 국가별 지표를 계산하는 과정에서 (&lt;code&gt;group by country_code&lt;/code&gt;) 파티션이 편향되는 문제가 발생했습니다. 개발 환경에서는 데이터의 양이 적어 문제가 되지 않았지만 운영데이터는 양이 많아 파티션의 편향이 데이터 처리 속도에 큰 영향을 주었습니다.
특정 파티션에서 많은 데이터가 처리되며 Lag이 커졌고, 결국 집계되는 시점에 이미 KSQL 에 들어온 데이터가 누락되어 지표에 큰 오차가 발생했습니다. 이로 인해, KSQL은 후보에서 제외되었습니다.&lt;/p&gt;
&lt;figure style=&quot;width: 80%; margin: 0 auto;&quot;&gt;
    &lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1146px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 67.77777777777777%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAAAsSAAALEgHS3X78AAABRHRFWHRSYXcACmV4aWYKICAgICAxNTAKNDU3ODY5NjYwMDAwNDk0OTJhMDAwODAwMDAwMDA1MDAxYTAxMDUwMDAxMDAwMDAwNGEwMDAwMDAxYjAxMDUwMDAxMDAwMDAwCjUyMDAwMDAwMjgwMTAzMDAwMTAwMDAwMDAyMDAwMDAwMzEwMTAyMDAwYjAwMDAwMDVhMDAwMDAwNjk4NzA0MDAwMTAwMDAwMAo2NjAwMDAwMDAwMDAwMDAwNDgwMDAwMDAwMTAwMDAwMDQ4MDAwMDAwMDEwMDAwMDA1MDY4NmY3NDZmNTM2MzYxNzA2NTAwMDAKMDMwMDAxYTAwMzAwMDEwMDAwMDAwMTAwMDAwMDAyYTAwNDAwMDEwMDAwMDA3YTA0MDAwMDAzYTAwNDAwMDEwMDAwMDAwODAzCjAwMDAwMDAwMDAwMArggAMyAAABVklEQVR42oWU246CUAxFz///oj4oKoqCirfwQjpZJ1mkzjwMSVPoZXe3PYeyXq/jfr9H3/exWq3i+XzG4XCI3W4X2+22yvv9jq7rgud8Psdms6kxTdNUTQx2nnK5XGIcxxiGoSY9Ho+qKYAN0f/5fOJ6vVY/eWrAiKdwAcAEgmFLABofgl2fgMRkkUQBDCFYEJK0K9gZx+12W4rbge/ElXmea+9Q3+/3ta3j8Riv1yt8pmmqMyWBtog7nU41Dmbotm0raAEAkSEJVARQH+8wQ9sBsdgUbUUDDJkBDmaC1kdlfCS4vDw/F0TOMkM+qG41l+V8c5wgWbCRuwB6TNxyXgyBbv5fQGcHIA4BBRdE3++WBVtazknOKbN1+H4LLgG0718tY7BN6eeWnWu+Kbn1PwypkreqPR8P3+1AdvkUFG+BQ5eh185t4+Om4ONnwOFG/JHww4DlD5t4IqHUIvzlAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/f3c7d50da547de64ca1b536b77a0ef60/33ecc/ksql_partition.webp 360w,
/static/f3c7d50da547de64ca1b536b77a0ef60/263a4/ksql_partition.webp 480w,
/static/f3c7d50da547de64ca1b536b77a0ef60/0c8fb/ksql_partition.webp 640w,
/static/f3c7d50da547de64ca1b536b77a0ef60/521f7/ksql_partition.webp 860w,
/static/f3c7d50da547de64ca1b536b77a0ef60/9db17/ksql_partition.webp 1146w&quot; sizes=&quot;(max-width: 1146px) 100vw, 1146px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/f3c7d50da547de64ca1b536b77a0ef60/53918/ksql_partition.png 360w,
/static/f3c7d50da547de64ca1b536b77a0ef60/9aebd/ksql_partition.png 480w,
/static/f3c7d50da547de64ca1b536b77a0ef60/0f09e/ksql_partition.png 640w,
/static/f3c7d50da547de64ca1b536b77a0ef60/3e3fe/ksql_partition.png 860w,
/static/f3c7d50da547de64ca1b536b77a0ef60/5f301/ksql_partition.png 1146w&quot; sizes=&quot;(max-width: 1146px) 100vw, 1146px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/f3c7d50da547de64ca1b536b77a0ef60/5f301/ksql_partition.png&quot; alt=&quot;KSQL 적용 후 KSQL consumer 의 offset lag&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;KSQL 적용 후 KSQL consumer 의 offset lag&lt;/figcaption&gt;
  &lt;/figure&gt;
    &lt;figcaption&gt;파티션이 편향되어, 하나의 파티션의 LogSize와 Lag가 지나치게 커졌던 상태&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h3 id=&quot;시도-2-databricks의-delta-live-table-이하-dlt&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8B%9C%EB%8F%84-2-databricks%EC%9D%98-delta-live-table-%EC%9D%B4%ED%95%98-dlt&quot; aria-label=&quot;시도 2 databricks의 delta live table 이하 dlt permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;시도 2. Databricks의 Delta Live Table (이하 DLT)&lt;/h3&gt;
&lt;figure&gt;
    &lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 51.94444444444445%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAABYlAAAWJQFJUiTwAAABpklEQVR42nVS2W7cMAz0//9ZCzTPfQiSzW689sqH7su2PCVlbxqgqICxKYoih0M10yQhHgP4P4waxnrw2vf9v1i3DSElLOtGWJFThnERv28KTTdKaGexlBVb2egCTpwJcOwLfUphFBhjCA6vF1ETYT/8HN9IOnxvP6C8gY8Jk1JQ1iDkBG0tZq3gU0RaFqS8YKWLXddBa42ZYq112IjxQucbFWy0c/gUHWar4ULE5+NOaOFiqMmu/Q0mWkRKlpeV2i0kzQghBIZBkFQTQgh4rsb6gE48Khu+lDJpsmwVbKdqP30raVYqu2EYoIghJ2QJjnW2fO3br5Zv/R3tcIclhqOccSE5JiPrns95EMzOe19bZd2+rybShGalqfrRkiEJLAWz7WPEJCVdBF7efuHn64/Kur1TQdJZaQvnMvlo6nE9NJyUgycNnPMVkViEmM8nsVGyUv8hZ9hAhchmEsyOiz4lYmk4rnmQFpJY8JSut7bqo6SqvpHETzHi6y2dOv2zP7HvBU0vBozTTBVWvF2uEOOMaZboeoH+QVqFVAfBzL7jr6+c9oE/5loGYxyPf7gAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/7f61e48bfcbe632072e9e906d8c9af6f/33ecc/dlt.webp 360w,
/static/7f61e48bfcbe632072e9e906d8c9af6f/263a4/dlt.webp 480w,
/static/7f61e48bfcbe632072e9e906d8c9af6f/0c8fb/dlt.webp 640w,
/static/7f61e48bfcbe632072e9e906d8c9af6f/521f7/dlt.webp 860w,
/static/7f61e48bfcbe632072e9e906d8c9af6f/cbd37/dlt.webp 1200w,
/static/7f61e48bfcbe632072e9e906d8c9af6f/548e7/dlt.webp 2400w,
/static/7f61e48bfcbe632072e9e906d8c9af6f/8ccaf/dlt.webp 2666w&quot; sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/7f61e48bfcbe632072e9e906d8c9af6f/53918/dlt.png 360w,
/static/7f61e48bfcbe632072e9e906d8c9af6f/9aebd/dlt.png 480w,
/static/7f61e48bfcbe632072e9e906d8c9af6f/0f09e/dlt.png 640w,
/static/7f61e48bfcbe632072e9e906d8c9af6f/3e3fe/dlt.png 860w,
/static/7f61e48bfcbe632072e9e906d8c9af6f/64756/dlt.png 1200w,
/static/7f61e48bfcbe632072e9e906d8c9af6f/98e2c/dlt.png 2400w,
/static/7f61e48bfcbe632072e9e906d8c9af6f/904ef/dlt.png 2666w&quot; sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/7f61e48bfcbe632072e9e906d8c9af6f/98e2c/dlt.png&quot; alt=&quot;Databricks DLT 서비스로 구축한 스트리밍 파이프라인 (Medallion Architecture 활용)&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Databricks DLT 서비스로 구축한 스트리밍 파이프라인 (Medallion Architecture 활용)&lt;/figcaption&gt;
  &lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;다음으로 살펴본 것은, &lt;a href=&quot;https://docs.databricks.com/en/delta-live-tables/index.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Databricks의 DLT 서비스&lt;/a&gt;였습니다. DLT 서비스는 Databricks에서 제공하는 스트리밍 프레임워크로, 스트리밍 및 배치 ETL 방식을 간편하게 처리할 수 있는 기능을 제공합니다.&lt;/p&gt;
&lt;p&gt;기본적으로 DLT는 Spark Streaming을 기반으로 구현되며, Python뿐만 아니라 SQL 언어도 지원합니다.&lt;/p&gt;
&lt;p&gt;이로 인해, SQL 개발자가 손쉽게 파이프라인에 집계 지표(Gold 영역)를 추가할 수 있다는 점이 큰 장점이었습니다. 또한, Web UI를 통해 테이블 간 구조를 시각적으로 쉽게 확인할 수 있고, 실행로그 및 실행시간 등을 한 곳에서 볼 수 있어 파이프라인 운영 측면에서 매우 유용했습니다. 스트리밍 파이프라인을 UI를 통해 간편하게 생성할 수 있고 Restart 버튼을 통해 재실행할 수 있어 유지보수 관점에서도 간편한 점이 많았습니다.&lt;/p&gt;
&lt;p&gt;하지만 PoC 과정에서, DLT는 저희가 요구한 일부 조건을 충족하는 데 어려운 부분이 있었습니다.&lt;/p&gt;
&lt;p&gt;지표 집계(Gold 영역)에서, 많은 양의 하트비트 로그와 복잡한 매출 집계 쿼리를 처리하는 과정에서 state 연산으로 인한 부하가 발생했고, 이로 인해 파이프라인의 전반적인 지연이 초래되었습니다. 이 문제로 인해 stream 데이터를 처리하는 Silver 영역과 지표를 집계하는 Gold 영역의 파이프라인을 별도로 분리해야 할 필요성이 제기되었습니다.&lt;/p&gt;
&lt;p&gt;하지만, DLT 플랫폼에서는 각 파이프라인별에 개별 클러스터가 할당되며, 다른 작업에서 해당 클러스터를 공유해서 사용하는 것이 불가능했습니다. 파이프라인을 분리하게 되면, Silver 영역과 Gold 영역 각각의 파이프라인에서 발생하는 클러스터 비용의 총합이 파이프라인을 운영하는 데 배정된 목표 금액 치를 넘어서는 문제가 발생했습니다. 이에 따라 조금 더 저렴하게 운영할 수 있는 방법을 찾아 나서게 되었습니다.&lt;/p&gt;
&lt;h3 id=&quot;시도-3-spark-streaming&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8B%9C%EB%8F%84-3-spark-streaming&quot; aria-label=&quot;시도 3 spark streaming permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;시도 3. Spark Streaming&lt;/h3&gt;
&lt;p&gt;마지막으로 검토한 방식은 DLT 서비스를 사용하지 않고 Spark Streaming을 직접 운영하는 것이었습니다.&lt;/p&gt;
&lt;p&gt;해당 방식에서는 DLT와 달리 managed 되어 제공되는 뷰가 없기 때문에 Spark 설정, 클러스터 관리, 그리고 모니터링 방법 등을 작업자가 모두 직접 구축해야 했습니다.&lt;/p&gt;
&lt;p&gt;하지만 Spark Streaming을 직접 관리하면서 얻을 수 있는 장점도 있었습니다. 가장 큰 장점은, 단일 Spark 클러스터를 stream 데이터를 처리하는 Silver 영역과 지표를 집계하는 Gold 영역의 파이프라인에서 공유하는 것이 가능해졌고 해당 클러스터를 파이프라인 내의 별도의 Spark 작업에서도 사용하는 것이 가능해졌습니다. 이를 통해, 비용 목표 금액 치를 충족하는 동시에 지표 서비스 운영에 필요한 다양한 작업들을 효율적으로 운영하는 것이 가능해졌습니다.&lt;/p&gt;
&lt;p&gt;이러한 장점들을 고려하여, 저희는 Spark Streaming을 직접 운영하는 방식을 스트리밍 파이프라인의 플랫폼으로 최종적으로 선택하게 되었습니다.&lt;/p&gt;
&lt;h2 id=&quot;대시보드&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8C%80%EC%8B%9C%EB%B3%B4%EB%93%9C&quot; aria-label=&quot;대시보드 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;대시보드&lt;/h2&gt;
&lt;p&gt;대시보드의 경우, 기존 데이터 플랫폼에서도 활용 중인 Tableau, Quicksight 등 다양한 선택지가 있었지만, 위에서 언급한 4. 시각화 단계에 요구되는 두 가지 요건을 충족하면서 사내 구성원들에게 친숙한 환경인 Kibana를 선택하게 되었습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;1.자동으로 대시보드 내의 데이터가 짧은 간격으로 갱신될 수 있는가&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;2.분 단위 또는 초 단위의 시계열 그래프를 표현하기에 적합한 대시보드인가&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Kibana 에서 제공하는 대시보드의 경우, Auto Refresh 기능을 통해 대시보드에서 조회하는 데이터를 원하는 간격으로 갱신할 수 있습니다. 또한, 그래프상의 timeseries 축을 자유롭게 조정할 수 있기에 위 두 가지 요건을 모두 충족할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;윗글에서는 ELK 스택으로 준실시간 지표 서비스를 구현하는 데 어려움이 있었다고 언급되어 있어, 신규 플랫폼에서 Kibana 대시보드의 역할에서 어떤 점이 달라지는 것인지 궁금한 분들도 계실 텐데요. 기존 데이터 플랫폼 구조에서는 Kibana에서 많은 양의 로그를 집계하는 연산을 사용하게 되면 클러스터에 큰 부하가 발생해 Elasticsearch 검색 기능과 같은 다른 서비스에도 문제를 유발하였습니다. 또한, 집계에 필요한 환율 테이블과 다른 메타 테이블에 대한 접근도 불가능하여 정확한 지표 집계에 어려움이 있었습니다.&lt;/p&gt;
&lt;p&gt;하지만, Spark Streaming 기반의 신규 플랫폼에서는 집계 파이프라인에서 이미 집계가 완료된 데이터(Gold)를 Kibana 대시보드에서 조회하게 되면서 복잡한 연산 과정 없이 지표를 조회할 수 있게 되었습니다. 이로 인해 정확한 지표 집계와 부하 문제 해결이 동시에 가능해졌습니다.
이러한 개선된 구조는 다음 파트에서 자세히 다룰 예정입니다.&lt;/p&gt;
&lt;h1 id=&quot;최종-구조&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B5%9C%EC%A2%85-%EA%B5%AC%EC%A1%B0&quot; aria-label=&quot;최종 구조 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;최종 구조&lt;/h1&gt;
&lt;figure style=&quot;width: 80%; margin: 0 auto;&quot;&gt;
    &lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1434px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 89.44444444444444%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAYAAABb0P4QAAAACXBIWXMAABYlAAAWJQFJUiTwAAADAklEQVR42pVU2W7TUBD1N8ILD3xBn+BPkJAQEg88gIRUeIWkKQSkVmogaUL2pGkWZ3O8L7ETx1kOZ4wqFSG1cKWxr++dOzPnnLlWsFoBUQSs13/YIY4he0b/GvViEeNuF/swBGR9vfrLP41DU3ZBgI3rwpvP4dPc2Sw1ZzpF7DjwONd6PRjDIRzOfU2jLeDxfWPiGywW2C2XUEIGNAwD9XoNjWYDtVoN1WoV5UoZ0zkduX9g5oQohiMVJ7kTZL/l0Wo0UGLlF4UCSqUSuldXkFhK6PvwPA8hD8nhkLCWzBRHMaJgDddxcaBPxGpDP0BjYaGrEY1hYjIa4YqBXCKUs0v6KfJQJxNM2lVY+gIaSzd0A83hT1xen8O0dDAD1p4Lm8F74xrqgwsYtg6PCSRQj5TMSFckkJesLp/P43PmI+qEK1BHAxVP3jzG0bsHGM572Dk+XCZzLR9Hrx9x/SFUrQ9Tt1iAhkwmk8KORRSBbJgmdPK4YHWWbSPwlniVe45Pla8I/BB7JhXIPtdfZp8hVzsjTUvY9BVE5UoFP8hnWmEaUNexYMkeuXAsi7w5hG+RozF81wHos+aerNuyPlThMphDM1nIcNDHjLQF9FGw32O33SLZbIDDAbskSW3L722ySefg/oH2e49+PHNjW/blnnty9sBvRVTV2EsTZpD3XhzvGLvdDrPZnKhMzImqPxhgRh4noryobJK/CjlotVppC2wl2x0j4X69WsFZ/gU63TquxyoqxyeoHudg+oQsUbPZLE5PTzEej1n54d4K+/0enn54gy4FETre577g7fl5OldWlFqnKNLc0lP3QRYEDsnXTJsqs9mp7MK1MTF0xLzTyk3WRMj/zyFoNiLmraHIglQo91mq/BdRYipr2xbFmaU0iTgR77rESjksFL6j2WymwsiBO0UhEpO92my10el0MGAPli7L/O5A6FN88lbkwkhV07/HfQG32yS9UcXLCqRDpLJ2p5vGiNiCqSjSf1P+06TK+1QWUQTVbSHl59BqtxFGEX4BSjFIUPcJyaoAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/4c19d564e809e0774538ca2f4058d02d/33ecc/final_pipeline.webp 360w,
/static/4c19d564e809e0774538ca2f4058d02d/263a4/final_pipeline.webp 480w,
/static/4c19d564e809e0774538ca2f4058d02d/0c8fb/final_pipeline.webp 640w,
/static/4c19d564e809e0774538ca2f4058d02d/521f7/final_pipeline.webp 860w,
/static/4c19d564e809e0774538ca2f4058d02d/cbd37/final_pipeline.webp 1200w,
/static/4c19d564e809e0774538ca2f4058d02d/1a3fe/final_pipeline.webp 1434w&quot; sizes=&quot;(max-width: 1434px) 100vw, 1434px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/4c19d564e809e0774538ca2f4058d02d/53918/final_pipeline.png 360w,
/static/4c19d564e809e0774538ca2f4058d02d/9aebd/final_pipeline.png 480w,
/static/4c19d564e809e0774538ca2f4058d02d/0f09e/final_pipeline.png 640w,
/static/4c19d564e809e0774538ca2f4058d02d/3e3fe/final_pipeline.png 860w,
/static/4c19d564e809e0774538ca2f4058d02d/64756/final_pipeline.png 1200w,
/static/4c19d564e809e0774538ca2f4058d02d/d31ed/final_pipeline.png 1434w&quot; sizes=&quot;(max-width: 1434px) 100vw, 1434px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/4c19d564e809e0774538ca2f4058d02d/d31ed/final_pipeline.png&quot; alt=&quot;준실시간 지표 서비스를 위한 최종 데이터 파이프라인&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;준실시간 지표 서비스를 위한 최종 데이터 파이프라인&lt;/figcaption&gt;
  &lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;해당 파이프라인은 준실시간 지표 서비스에 요구되는 조건과 스펙들을 모두 충족하기 위해 구성된 최종 아키텍쳐입니다.
이 아키텍쳐는 데브시스터즈에서 운영 중인 게임 및 플랫폼에서 발생하는 로그 데이터가 준실시간 수준의 짧은 레이턴시 안에 사용자들에게 지표로 조회되기까지의 과정을 포함하고 있습니다. 그럼, 각 영역들의 역할과 구현 방식에 관해서 확인해 보겠습니다.&lt;/p&gt;
&lt;figure style=&quot;width: 80%; margin: 0 auto;&quot;&gt;
    &lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1438px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 88.88888888888889%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAYAAABb0P4QAAAACXBIWXMAABYlAAAWJQFJUiTwAAACXElEQVR42p1U227TQBTMTyK+AYm/4ZEHXvmBCFEhykulQpGbJmmKEjUQNzenvsWXOOvbMGfjDUklaMRKR3vx8fjMzFm3kKZAkgBZBmw2qGVmLEZDWBcXsO/uUMrzuj4pWmUUIfM8+NMp1osFgtlMrx+GQ8zGY3gPD5AcSa6K4ijKJ1FXFVrReg1ntUK320W/38cN526vx/mG5w5igtVSOZPzPCeJlLGBUgrb7XYfsq80YBgiZCRMivhyQnpxHCNLt0jWG4SeT8BUA5ZlCVVI7Coq+AH5iJxLNIABJraN2Y8uXFY0n8/hOCt07q9wNfoCz3O0ppUBzFlRnqEQ0AZIQGWvAUPfx9nZR3z60EaPNK3ra/wc/8Krdy/w+v1L2PN7VHHKF6WqEl78CC95JMAORCKlsUJbA8bU0KUpj66rKwuCAOswwtuzN/jcv0QcJqgog6EcpTRRbbne7QXQ6KgBE+rnOA4cOhwSzCOwVO06LqYTGxHP6vSPhoaiWZu9OWtBEhuBpTVKeSCa0LWiWTNTP6ub0KPpu/2+OWvFpLNYLjFl/0n7SNn/GgJgDJHZGLI3ZUkwy7LQY++NRiOd+BygoobZJiaY0oAqSXWUFSl7NKTdbuP8/Fy3zHNDGOaUIyCzvLkd0sOxNL9QFstXpOrTCGnsI03+UuGhIaY/C4a+epJknPqf8bSAVsZbsFwuIFpKDz5X4WGVxgxTqaYsVC+/fsPt7S3u+Ks61WUlZqidKUc/B98P8N26hm1P0Ol0TtJQXlYHza1UfnD1+GcRdyeTCQaDwUl0D38MRn9T4W+IAW9RIKBgmQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/7cf444f5d2d288f48f5e12e6db551437/33ecc/final_pipeline_streaming.webp 360w,
/static/7cf444f5d2d288f48f5e12e6db551437/263a4/final_pipeline_streaming.webp 480w,
/static/7cf444f5d2d288f48f5e12e6db551437/0c8fb/final_pipeline_streaming.webp 640w,
/static/7cf444f5d2d288f48f5e12e6db551437/521f7/final_pipeline_streaming.webp 860w,
/static/7cf444f5d2d288f48f5e12e6db551437/cbd37/final_pipeline_streaming.webp 1200w,
/static/7cf444f5d2d288f48f5e12e6db551437/e40a7/final_pipeline_streaming.webp 1438w&quot; sizes=&quot;(max-width: 1438px) 100vw, 1438px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/7cf444f5d2d288f48f5e12e6db551437/53918/final_pipeline_streaming.png 360w,
/static/7cf444f5d2d288f48f5e12e6db551437/9aebd/final_pipeline_streaming.png 480w,
/static/7cf444f5d2d288f48f5e12e6db551437/0f09e/final_pipeline_streaming.png 640w,
/static/7cf444f5d2d288f48f5e12e6db551437/3e3fe/final_pipeline_streaming.png 860w,
/static/7cf444f5d2d288f48f5e12e6db551437/64756/final_pipeline_streaming.png 1200w,
/static/7cf444f5d2d288f48f5e12e6db551437/e9562/final_pipeline_streaming.png 1438w&quot; sizes=&quot;(max-width: 1438px) 100vw, 1438px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/7cf444f5d2d288f48f5e12e6db551437/e9562/final_pipeline_streaming.png&quot; alt=&quot;Spark Streaming을 통한 데이터 전처리 파이프라인&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Spark Streaming을 통한 데이터 전처리 파이프라인&lt;/figcaption&gt;
  &lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;(Spark Streaming)&lt;/strong&gt; Kafka topic에 저장된 원본 로그는 Spark Streaming에서 전처리 과정을 거치게 됩니다. topic으로부터 읽어오는 원본 로그는 위에서 언급했듯이 양은 많지만, 활용 빈도는 낮아 Bronze Layer 테이블로 적재하지 않았습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;(Silver Layer)&lt;/strong&gt; 원본 로그들은 타입 지정과 같은 기본적인 전처리와 함께 앞서 언급된 서로 다른 stream 로그 간의 JOIN 작업이 이루어져 Delta Table 형태로 저장되게 됩니다.&lt;/p&gt;
&lt;figure style=&quot;width: 80%; margin: 0 auto;&quot;&gt;
    &lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1432px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 89.44444444444444%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAYAAABb0P4QAAAACXBIWXMAABYlAAAWJQFJUiTwAAACkklEQVR42p1Uy27TUBDN//EX8AWIL2EJQmxQN6yQuiBCpIUNTSEJVZOatHk3L8fP2E5sx3Z8OHPzoLRAKyyNJhl7zp05Z+YWsF5DWZ7fNcZzeZdlsLodNCoVjK6ukC0WQBTdtTBEId8mrZl023IxAUxTBNMpWvU62ufncEYjeLqOOWPinfEYHn+nvo+CJKZMiOMY8Wq18VuTeCaAjKdBgPFkiuOjEoqfSmg0Gqiy4pOTE1TotWYTvudtADOpiIm3/TpbbwCTBJHrIGCCppu4kIpMC8NBH5ekwLZtBDzQn883gAkT0lWMjBWlW1slrDaJCJgpwJgfO46LzvACjV4ZM0uH5/kE8tHpdDAiDYtdyyHJDJfLfatygBNYsAIDSZpgzdh8Rs5sH88OHuPJm0fojJswZxYM00CxWES5XEZEjH3Lu8pUu2zVD32EcahalwpDxyZHC7wsPsdh9QPcecBWBdBErVZD+fRUUbIB3AL9ZumGih2HsevCdRzYhoXr3gAueXNolmGg1+1ieH0Nj/8L+Y2ZE5//wdQ8CrioTa7VSAm3jKeMZXynchkvSEvpDTEUwD8eAZty9gzThj7V0e/3MSa/Y4Mci8rSngix4kli9wHKoRpn8MvxC2haDa3BAGfvSjg7eA/dsTYVLqmOKC2c3QcoBQwHPTx9+xr16US1fXj0Ga8+lvgy/dXyToyHADoUaEhxTMtSszdlZd3ZBJHssny0J/8/HqHp5qNU3q+aqHmfKPxGQFxWqVMc2RDxYbhUlKmWI149DxVFkmR3fzQvKYqGdrvNy6HKy0JTWijA3bqJf4jKJrfja/U79NlMiXlF0NNvVXVB7EURwNt8/A3Q44pNeP853By5slqtFuocJd/38ROs62V3EDPrnAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/65308111784c8c6c415fe6ea53dc78dc/33ecc/final_pipeline_dashboard.webp 360w,
/static/65308111784c8c6c415fe6ea53dc78dc/263a4/final_pipeline_dashboard.webp 480w,
/static/65308111784c8c6c415fe6ea53dc78dc/0c8fb/final_pipeline_dashboard.webp 640w,
/static/65308111784c8c6c415fe6ea53dc78dc/521f7/final_pipeline_dashboard.webp 860w,
/static/65308111784c8c6c415fe6ea53dc78dc/cbd37/final_pipeline_dashboard.webp 1200w,
/static/65308111784c8c6c415fe6ea53dc78dc/1cf4a/final_pipeline_dashboard.webp 1432w&quot; sizes=&quot;(max-width: 1432px) 100vw, 1432px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/65308111784c8c6c415fe6ea53dc78dc/53918/final_pipeline_dashboard.png 360w,
/static/65308111784c8c6c415fe6ea53dc78dc/9aebd/final_pipeline_dashboard.png 480w,
/static/65308111784c8c6c415fe6ea53dc78dc/0f09e/final_pipeline_dashboard.png 640w,
/static/65308111784c8c6c415fe6ea53dc78dc/3e3fe/final_pipeline_dashboard.png 860w,
/static/65308111784c8c6c415fe6ea53dc78dc/64756/final_pipeline_dashboard.png 1200w,
/static/65308111784c8c6c415fe6ea53dc78dc/260b9/final_pipeline_dashboard.png 1432w&quot; sizes=&quot;(max-width: 1432px) 100vw, 1432px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/65308111784c8c6c415fe6ea53dc78dc/260b9/final_pipeline_dashboard.png&quot; alt=&quot;데이터 집계 &amp;amp; Elasticsearch 데이터 전송 파이프라인&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;데이터 집계 &amp;amp; Elasticsearch 데이터 전송 파이프라인&lt;/figcaption&gt;
  &lt;/figure&gt;
&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;(Gold Layer)&lt;/strong&gt; Spark Streaming에서 Delta Lake 형태의 테이블로 소스데이터가 저장되었기 때문에 데이터 집계 과정은 일반적인 집계 파이프라인과 유사한 구조로 구성할 수 있었습니다. 이 과정에서 Spark Streaming의 큰 장점인 단일 클러스터 공유를 통해 집계 작업을 효율적으로 운영할 수 있었습니다. 배치 작업의 경우, 기존에도 팀 내에서 활발하게 사용하고 있는 Airflow를 사용하여 2분 간격으로 Micro batch 방식으로 실행되도록 설정했습니다.&lt;/p&gt;
&lt;p&gt;앞서 살펴보았듯이, 지표 집계를 위해서는 복잡한 집계 로직 구현과 환율 테이블을 사용하는 등 SQL 개발자가 자유롭게 개발이 가능한 환경이 필요했는데요, 현재의 환경, Delta Lake 형태로 저장된 로그 데이터를 Databricks SQL로 집계하고 Airflow 배치를 통해 실행하는 환경은 SQL 개발자에게 매우 친숙한 환경이었습니다. (실제로도 기존 데이터 플랫폼에서 사용 중인 집계 쿼리를 재활용하는 것이 가능했습니다.) 이를 통해, 5% 이내의, 준실시간 지표로써는 상당히 낮은 오차율의 지표를 집계하는 것이 가능해졌고 새로운 지표가 추가되거나 로직 수정이 필요한 경우에도 SQL에 익숙한 DW 엔지니어 또는 데이터 분석가가 아주 쉽게 코드를 수정할 수 있는 환경이 갖춰지게 되었습니다.&lt;/p&gt;
&lt;figure style=&quot;width: 80%; margin: 0 auto;&quot;&gt;
    &lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2334px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 60.277777777777786%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAABYlAAAWJQFJUiTwAAAB5ElEQVR42nWTa3KjMBCEfQUjobfQAwzEAeN1dmvvf7LeNnGtUynnx1cjUUyjHjWHmiK222/M5zOW6y+s2wajNZqmgRACjfisQsid+/75THzZN5BS4KDYfBfNKWCsM3IsUEEhRIsUEorvEVyA8i2818ghI/sC6wyCt+h82vfORbStoqBquZAoTuM2rbiebljmBSl1bP4myHdKKMiucO1hnEJkTT6TAmM9DqJtYYxGygmnMuGtLqhxwJGWj1ISsdeGdo87Yqd5WP7KbnmyGh/jgL+XC/5MIy61InAMuTli4FwSKWRgQ3nQE8uPCCK/cahdwHW5Yp03jF2l9Q5O271W27GmfW3pRLPhjiHtC7FdMHUe23LBNJ4xlYyaC0qqHHLYh6yU3hGPE4kfTvZfcEwOt/cN2/iB9/mMdV05yxFWuT06Da3fYyEfsXnO6wfBTMtv48ybXfF+vmGdNmjLkznJSFjedEXnMoxX8Jx3R/vRRiitXgt6z2tnDvu+xzRc0DOLoUQ4ZrHEiCGeEH2EjYpj4IfaT+5ReynoKJi7iDoMKOUEEwKkbdkk4UyA19zrFtIwr66F4POG8Wh+uuUpeJyZwWWaecKB4h6951+TEhItBiMQKZYMQ8wZFjZVMhDPm5ft/cd48g8kvjkhgxhUeQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/8f1a3f0647c206b17019e9c0f6188748/33ecc/new_sql.webp 360w,
/static/8f1a3f0647c206b17019e9c0f6188748/263a4/new_sql.webp 480w,
/static/8f1a3f0647c206b17019e9c0f6188748/0c8fb/new_sql.webp 640w,
/static/8f1a3f0647c206b17019e9c0f6188748/521f7/new_sql.webp 860w,
/static/8f1a3f0647c206b17019e9c0f6188748/cbd37/new_sql.webp 1200w,
/static/8f1a3f0647c206b17019e9c0f6188748/99a15/new_sql.webp 2334w&quot; sizes=&quot;(max-width: 2334px) 100vw, 2334px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/8f1a3f0647c206b17019e9c0f6188748/53918/new_sql.png 360w,
/static/8f1a3f0647c206b17019e9c0f6188748/9aebd/new_sql.png 480w,
/static/8f1a3f0647c206b17019e9c0f6188748/0f09e/new_sql.png 640w,
/static/8f1a3f0647c206b17019e9c0f6188748/3e3fe/new_sql.png 860w,
/static/8f1a3f0647c206b17019e9c0f6188748/64756/new_sql.png 1200w,
/static/8f1a3f0647c206b17019e9c0f6188748/ead9f/new_sql.png 2334w&quot; sizes=&quot;(max-width: 2334px) 100vw, 2334px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/8f1a3f0647c206b17019e9c0f6188748/ead9f/new_sql.png&quot; alt=&quot;준실시간 집계 SQL 쿼리&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;준실시간 집계 SQL 쿼리&lt;/figcaption&gt;
  &lt;/figure&gt;
    &lt;figcaption&gt;기존 시간별/일별 집계 쿼리에서 사용하는 SQL 코드에서 1. 소스 테이블과 2. 집계 시간 단위만을 변경하여 재활용할 수 있습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;(Send Metric Task)&lt;/strong&gt; 마지막 작업은, 집계된 데이터를 Kibana 대시보드에서 조회하기 위해 Elasticsearch로 데이터를 전송하는 작업입니다. 해당 작업에서도 마찬가지로 Spark Streaming에서 사용한 클러스터를 사용하여 비용 효율화를 달성할 수 있었습니다. 각각의 Gold 지표 테이블에서 최근 업데이트된 레코드들을 Elasticsearch로 보내는 spark job을 병렬적으로 제출하여 신규 레코드가 최대한 빠르게 Kibana 에서 조회될 수 있도록 구현했습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;send_to_es&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;table_name&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;# 최근 20분 내에 업데이트된 레코드를 ES로 보내는 작업&lt;/span&gt;
	df &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; spark&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;read&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-interpolation&quot;&gt;&lt;span class=&quot;token string&quot;&gt;f&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;table_name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;where&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;updated_at&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; current_timestamp&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; expr&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;INTERVAL 20 MINUTE&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      df&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;write&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;org.elasticsearch.spark.sql&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;option&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;es.nodes.wan.only&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;option&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;es.net.ssl&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;option&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;es.nodes&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{ES_HOST}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;option&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;es.net.http.auth.user&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{ES_ID}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;option&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;es.net.http.auth.pass&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;{ES_PASSWORD}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;option&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;es.mapping.id&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;document_id&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;mode&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;append&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;save&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;{ES_INDEX}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 쓰레드 풀을 이용한 병렬 작업&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;with&lt;/span&gt; ThreadPoolExecutor&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;max_workers&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;thread_pool_size&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; executor&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;# Spark 작업 제출&lt;/span&gt;
  futures &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; table_name &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; gold_table_list&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
		futures&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;executor&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;submit&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;send_to_es&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; df&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; table_name&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; table_name
	
	&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; future &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; as_completed&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;futures&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      table_name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; futures&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;future&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          future&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
          &lt;span class=&quot;token keyword&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-interpolation&quot;&gt;&lt;span class=&quot;token string&quot;&gt;f&quot;Finish: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;table_name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; send to ES&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;except&lt;/span&gt; Exception &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token keyword&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-interpolation&quot;&gt;&lt;span class=&quot;token string&quot;&gt;f&quot;Failed: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;table_name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; send to ES\n&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이처럼, 집계가 완료된 메트릭을 Kibana 대시보드에서 조회하게 되면서 기존 데이터 플랫폼의 ELK 스택에서 발생했던 문제들을 모두 해결하고 Kibana 대시보드의 장점만을 활용할 수 있게 되었습니다.&lt;/p&gt;
&lt;h1 id=&quot;준실시간-지표-서비스를-도입하고-일어난-일&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A4%80%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%A7%80%ED%91%9C-%EC%84%9C%EB%B9%84%EC%8A%A4%EB%A5%BC-%EB%8F%84%EC%9E%85%ED%95%98%EA%B3%A0-%EC%9D%BC%EC%96%B4%EB%82%9C-%EC%9D%BC&quot; aria-label=&quot;준실시간 지표 서비스를 도입하고 일어난 일 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;준실시간 지표 서비스를 도입하고 일어난 일&lt;/h1&gt;
&lt;p&gt;오랜 준비 과정 끝에, 마침내 준실시간 지표 서비스는 지난 2024년 6월 &lt;a href=&quot;https://cookierun-towerofadventures.com/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;쿠키런: 모험의 탑&lt;/a&gt;이 출시되는 날 구성원들에게 처음으로 공개되었습니다.&lt;/p&gt;
&lt;p&gt;서비스에 대한 관심은 가히 폭발적이었으며, 런칭 당일 준실시간 대시보드에 접속한 사내 구성원의 비율은 무려 30%에 육박했습니다. (접속 권한이 없는 사용자들을 고려한다면, 대략 3명 중 2명의 구성원이 준실시간 대시보드에 접속한 셈입니다.)&lt;/p&gt;
&lt;p&gt;준실시간 대시보드 지표는 게임팀의 마케팅 캠페인 전략 수립, 앱 푸시 송출 등 비즈니스 성과에 직접적으로 기여할 수 있는 의사결정들에 활용될 수 있었고, 다양한 분들이 지표 대시보드에 대해 긍정적 피드백을 보내주셨습니다.&lt;/p&gt;
&lt;p&gt;쿠키런: 모험의 탑은 &lt;a href=&quot;https://www.devsisters.com/stories/news/CTA-10B-sales&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;앱 출시 일주일 만에 200만 다운로드 달성 및 누적 매출 100억 원 돌파&lt;/a&gt;라는 놀라운 성과를 달성하게 되었는데, 준실시간 대시보드를 통해 이 성과를 만드는 데에 간접적으로 기여할 수 있게 되어 뿌듯했습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 600px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 99.44444444444443%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsSAAALEgHS3X78AAAFdklEQVR42i2UW1RUZRiG56riNDAMc2BmGJyRQUBEBAXE1NSgJZktUgM0FUHRwOFg4QERJBAQBBsUhbEwPKRihaAQ5aFwEOU8zHDS0rKSSu2uy9Z6+nN18az/ar/7+b7331vS6xilb2KcoakJ+sfsnDttoaYgnSP706jM34jVUkLHzS5u3rVhGx6gW2CzD9E9MkD/lIPjR8spTX2L9vw36Slfh6RnZIiBiTEGBf1OOxfO1lO9exPlH6ZQlJVIY00Bewr3Ud9k5Z5zkFt9Nq73fs8NcfZPjlJXdZDcNXGczV9Nb8VaJPdGBul3jDAgwobHR7lwrpFdxZVYGs9TW3+W8uZrlDZ/TXXLbSydwxztGOFI2wgnLt8QIkPkf5iD0d9IxOxgajcuQtJps9Fhu02n4EbfHT5tqufCmVYeTTzjR+dT7KPT9I79ztDEcxz2J4yOPmHS/pxvu/rpdg5xuHgPK0KDMBqMVCVHIRl+8JzeyWdcnXqO7ek/NLdfo/nEpzwYesTk4CPs4uyzP2bYOc2P439wX4TfH/6DW912rnS2Ur5tPQffWUzK0jCu5C5D0lRVTO1HB1i+Kom9hcXU15Ryqu4kPT0OHPfuMzn8Mw8dvzHpfMKd0V+ZEC9/OP4XXd/1Yyndx7uxUSyLWcCC0GCOZ7yGpOHKTdLzCvBW+pOYuI6m5lOcPt7I8J1xHk/9KcaeZkqEOcem6Rz5CcfkU357+DeXLrWTsCoB/YxZGI0m9H4GdiVGI2nrGWTn3iJCgmYTERFNZlYmVUUFWCuqaLBYsZ5s5tSZFr5su07d5XZOX7xKR7uNE9ZmwhfE4Ks14C9K8df7kxE3B8n5Ly6yMCYWkykIgyGA8NAwGkvNHDBvYOWKVzFvfpvqigKONVqwNp0kMzuLqppDFJUU4KP0Q631R6vVo9Xp2bQoEMnFAQcBwXPRCWWNzkBYSAh1JWaaKnNIWh1PzrZkzDu3Yd6+kRzBe2lplNadoOHceTZs2Yq3whe9sNNp/UhdEoSk2z5GWupmlEo1i+aYiJ1tIjo6iqLsFBLiRHuJCeQUl2MuqWRL3m5SMvMpOnudy0OP2V9Rg7uHtxjZgJ9Wx45lItB67GPK92SxMjKAzOWzCDLq8fBSkLx6GfFLo0hfG8/B6lquOn/lmiio5OR5Prlhp8PxC2nvZ+MlU6ERYwfO0JP7htjhvLAQVCqVQIlC4OurFo3pCA4KZMnCSNLXJ7I+NZ1rvSPc/WGayoZm2nvE9zz+AwUHy8RkGlGMH4vDTLw5z4DEoFWh9VWiVMhRK31Qq3zQalTI5D5Ezg1l644s6lpu0do9QJeth/1HP8PacpXPW6+wu7AIpUqLRqNjYaCWcH8FEqNOmMm9kMk88X6BF97eMnQaNTqdHwGmQKJjFhIprsjsOeFELn6dsuOnuTP5M2W1dSiEoUqtYYlJQ6RejsRX4YVKLkUtkHt64OHujqdUitTTi/khM0hZtZiMbWmYcz8gfcdO4lcmUGZppPOekwNlh1GrtcgVaoL91ETofZBovN1Rerkhl7oik7rh7uaKm5sb4UZfdq2dy760OBo++5yvbvZx6ZteGi+00dTaxSctbRwSZc0MCETm7YNerC1UJwxVnq4oBT4erni6vyIMRaCrK5tijWx/w0hSXDj79+2lQjx86IiFwtJKDtUeo+zIxxQWF7MoNhap9L+VyZmlE4YKdxfk/yNzE4ECd2E4d6aK+Hk+rImLIC9zq/jv5ZKXm0222UxuXh7ZOdlsF6t4NTZG3EVPUagCk16FRC1zQSl1wcvtZTxdXkLq8jJSYRksFhxlkpG0Yh5bk1ezY3MSGZuTSXtvHVs2rCUjdT1ZGVtYFb/8hd38EAMmnYJ/AWGbh1M2RNeWAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/8a4ab240fc3d294cddf80063d9572872/33ecc/launching.webp 360w,
/static/8a4ab240fc3d294cddf80063d9572872/263a4/launching.webp 480w,
/static/8a4ab240fc3d294cddf80063d9572872/411c4/launching.webp 600w&quot; sizes=&quot;(max-width: 600px) 100vw, 600px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/8a4ab240fc3d294cddf80063d9572872/53918/launching.png 360w,
/static/8a4ab240fc3d294cddf80063d9572872/9aebd/launching.png 480w,
/static/8a4ab240fc3d294cddf80063d9572872/dface/launching.png 600w&quot; sizes=&quot;(max-width: 600px) 100vw, 600px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/8a4ab240fc3d294cddf80063d9572872/dface/launching.png&quot; alt=&quot;사무실 모니터에 띄워진 준실시간 지표 대시보드&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;사무실 모니터에 띄워진 준실시간 지표 대시보드&lt;/figcaption&gt;
  &lt;/figure&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;데이터-임팩트를-함께-만들어갈-인재를-찾고-있습니다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%9E%84%ED%8C%A9%ED%8A%B8%EB%A5%BC-%ED%95%A8%EA%BB%98-%EB%A7%8C%EB%93%A4%EC%96%B4%EA%B0%88-%EC%9D%B8%EC%9E%AC%EB%A5%BC-%EC%B0%BE%EA%B3%A0-%EC%9E%88%EC%8A%B5%EB%8B%88%EB%8B%A4&quot; aria-label=&quot;데이터 임팩트를 함께 만들어갈 인재를 찾고 있습니다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;데이터 임팩트를 함께 만들어갈 인재를 찾고 있습니다.&lt;/h2&gt;
&lt;p&gt;지표 서비스가 사용자들에게 이렇게까지 큰 관심을 받고 비즈니스 성과에 직접적으로 기여할 수 있는 경험은 매우 특별하고 드문 일이라 생각됩니다.&lt;/p&gt;
&lt;p&gt;데브시스터즈 데이터플랫폼 그룹에서는 쿠키런: 킹덤, 쿠키런: 모험의 탑과 같이 운영 중인 게임은 물론, 런칭을 앞둔 다양한 게임 서비스에도 고도화된 데이터 플랫폼과 지표 서비스를 제공하기 위한 다양한 업무를 수행하고 있습니다.&lt;/p&gt;
&lt;p&gt;데이터를 통해 비즈니스 성과를 만들어 나가는, 데이터 임팩트를 만드는 경험을 하고 싶으신 분들께서는 아래 채용 공고를 꼭 확인하셔서 저희의 여정에 함께할 수 있기를 기다리겠습니다.&lt;/p&gt;
&lt;p&gt;감사합니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[『스칼라로 배우는 함수형 프로그래밍』 책을 읽어봅시다: 1편 - 순수 함수와 참조 투명성]]></title><description><![CDATA[쿠키런: 킹덤 서버 개발자와 함께 읽어보는 『스칼라로 배우는 함수형 프로그래밍』]]></description><link>https://tech.devsisters.com/posts/functional-programming-in-scala-01</link><guid isPermaLink="false">https://tech.devsisters.com/posts/functional-programming-in-scala-01</guid><pubDate>Fri, 01 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요? 저는 스튜디오킹덤에서 &lt;a href=&quot;https://www.cookierun-kingdom.com/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;쿠키런: 킹덤&lt;/a&gt; 게임 서버 개발을 담당하고 있는 이현수입니다.&lt;/p&gt;
&lt;p&gt;쿠키런: 킹덤 서버는 스칼라와 함수형 프로그래밍을 활용하고 있으며, 이는 국내에서 정말 보기 드문 개발 환경입니다. 스칼라를 좋아하는 저는 이러한 독특한 환경에 흥미를 느껴 팀에 합류하게 되었고, 지금은 스칼라와 함수형 프로그래밍이라는 좋은 연장을 활용해 만족스럽게 기능을 개발하고 있습니다.&lt;/p&gt;
&lt;p&gt;사실 지난 10년 동안 개인적으로 스칼라를 공부해왔지만 그동안 스칼라를 사용할 업무 기회가 거의 없었는데, 이렇게 킹덤 서버팀에 합류하게 된 것은 저의 개발자 인생 중에서 특별하고 중요한 전환점이 되었습니다. 취미로 하던 스칼라 학습자에서 이제 프로페셔널 스칼라 개발자가 되었으니, 앞으로는 실무 경험을 바탕으로 스칼라와 함수형 프로그래밍의 저변을 넓히는 데 공헌하겠다는 마음을 먹었습니다. 그래서 그 일환으로 지금부터 기술 블로그에 스칼라와 함수형 프로그래밍에 관한 글을 연재하고자 합니다.&lt;/p&gt;
&lt;h2 id=&quot;스칼라로-배우는-함수형-프로그래밍-책-소개&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8A%A4%EC%B9%BC%EB%9D%BC%EB%A1%9C-%EB%B0%B0%EC%9A%B0%EB%8A%94-%ED%95%A8%EC%88%98%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%EC%B1%85-%EC%86%8C%EA%B0%9C&quot; aria-label=&quot;스칼라로 배우는 함수형 프로그래밍 책 소개 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;『스칼라로 배우는 함수형 프로그래밍』 책 소개&lt;/h2&gt;
&lt;p&gt;스칼라와 함수형 프로그래밍은 워낙 방대한 주제라 어디서부터 글을 시작해야 할지 고민이 되었습니다. 그러다 문득, 이미 출간된 좋은 책을 하나 참고하여 실무 경험을 더해 소개하면 좋겠다는 생각이 들었습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://jpub.tistory.com/477&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 663px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 100%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAACXBIWXMAAAsTAAALEwEAmpwYAAADAklEQVR42oWUSUwTURiAa1Igim1nOkNpOzNt2RJClANB4sGTxqNBY0CIggQRTUxIPJl4gYSbIS7xgiaecCkaStQolWJLC6VaS2ejFMouW4IsLQ1LO9Opj4KkNAVf/kwmL/PN9/7/vfeLoocPAUQkAiIKguejgpDwgSgZJER3ARBxP+J4Pji/EJr5fSQcG3w0uhWJ+JeXl2h2tt3ge9jUf6Wik8jruVq5J0iAhdjUH6fL0/pq9NET9kYtfe4Cpc0n5Vh3GvQ5DfpyLH34WrWQHI6tc1zf/lWCMpCKSkepk5kUhLGI1ojgA5lZhlSZs6I6egQ8/aHDjOIMjFPqHFqZTWN5AwjRA6sNCK4XS3orq/fXmASe6TD0anLGSi/7KsrGyst8l0r7FdpuSGVAsI9imfN6zSFmsB/ArH9vVhC+8xc9xWcZXT6t0NFywgip7ajGIJZ6q+qSwxGOA3MT79oBTEtUbomSlGEUjDNywgRjPShhTJHZqm4lwuA1HOaCgcD61uZw2+teTS6DaChUQ4GnnADm7zD2M0PrTZWP1tTHPHEwz/NraytzM1PT87Ouly+sAJYT1L8g5cQnSPUjQ2tMkTpi8G519uDNjY3pqQmWHCQZt+3ZU1t2PrDtaCGchHFWrumDsU4E7xBL6Nq7MXMc7PeveYYoh83S5+jrbmqywGoaYBBGqbNBwQAPcraiGpNY5q27lwivra6wtLvfYrLYzMbGRotMOZRbMFR8xlNSQhO5wN8Fq40o4UiFR+80JMLB4LpvZPhnf6/Vbv0GzAjmKSpmCgp3coZwGtW6M3SUIssuOj5683ZizqFQaHFhbogadJK/epqbzbAKVJuEcArR0hk6UqoaFEPUCYXrVNFi29tEM7i2QD47PTkyOW5vaTHLlIwyh1JlkdJMgLG5hTP1Df4uU2RjY3+TDxwSjuMCq6vLQb+rtbVTJGaxLCbv9ER51cobPbe0tM8IcS1BFN8CwuHwdjgUWFz03n+w9Pj59vhU/LHfPbz/6yQHb7iQrAHtwn8B8Fl1osGEb4cAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/3101cedd25ac22236b7acc94f168d4b1/33ecc/book.webp 360w,
/static/3101cedd25ac22236b7acc94f168d4b1/263a4/book.webp 480w,
/static/3101cedd25ac22236b7acc94f168d4b1/0c8fb/book.webp 640w,
/static/3101cedd25ac22236b7acc94f168d4b1/f21c8/book.webp 663w&quot; sizes=&quot;(max-width: 663px) 100vw, 663px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/3101cedd25ac22236b7acc94f168d4b1/53918/book.png 360w,
/static/3101cedd25ac22236b7acc94f168d4b1/9aebd/book.png 480w,
/static/3101cedd25ac22236b7acc94f168d4b1/0f09e/book.png 640w,
/static/3101cedd25ac22236b7acc94f168d4b1/5c8e6/book.png 663w&quot; sizes=&quot;(max-width: 663px) 100vw, 663px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/3101cedd25ac22236b7acc94f168d4b1/5c8e6/book.png&quot; alt=&quot;출처: 『스칼라로 배우는 함수형 프로그래밍』 (제이펍, 2015)&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;출처: 『스칼라로 배우는 함수형 프로그래밍』 (제이펍, 2015)&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;현재 절판 상태이지만, 원서는 &lt;a href=&quot;https://www.manning.com/books/functional-programming-in-scala-second-edition&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;여기&lt;/a&gt;에서 구매할 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이 책은 강렬한 표지 색깔 덕분에 빨간책(The Red Book)이라고도 불리며, 스칼라와 함수형 프로그래밍이라는 주제를 깊이 있게 다루는 유명한 책입니다. 저도 처음에는 충분한 배경 지식 없이 이 책을 읽으려고 시도했는데, 당시에 책의 내용을 이해하는 데에는 제법 어려움이 있었습니다. 다행히 그동안 스칼라와 함수형 프로그래밍을 다양한 경로로 익히고, 지금에서는 실무에서 매일 사용하다 보니 비로소 이 책의 내용을 제대로 이해하며 읽을 수 있게 되었습니다. 초반에 이 책을 읽으면서 겪었던 어려움이 기억나기에, 이 블로그 시리즈를 통해 처음 읽는 분들께 조금이나마 도움이 되었으면 합니다.&lt;/p&gt;
&lt;p&gt;그래서 이번 블로그 시리즈의 제목을 &lt;strong&gt;『스칼라로 배우는 함수형 프로그래밍』 책을 읽어봅시다&lt;/strong&gt; 로 정했습니다. 저와 함께 책을 읽으며 스칼라와 함수형 프로그래밍을 더 깊이 이해해보는 시간이 되길 바랍니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h2 id=&quot;당부의-말씀&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8B%B9%EB%B6%80%EC%9D%98-%EB%A7%90%EC%94%80&quot; aria-label=&quot;당부의 말씀 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;당부의 말씀&lt;/h2&gt;
&lt;p&gt;책에 이미 나온 내용을 굳이 옮겨쓰고 하나하나 설명하지는 않을 생각입니다. 글의 방향은 중요한 핵심 포인트만 간단히 요약하고, 제가 경험한 실무적인 팁을 조금 가미한 형태가 될 것 같습니다. 제목과 같이 책을 함께 읽어보시는 것을 추천드립니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;함수형-프로그래밍이란-무엇인가&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%A8%EC%88%98%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%EC%9D%B4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80&quot; aria-label=&quot;함수형 프로그래밍이란 무엇인가 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;함수형 프로그래밍이란 무엇인가?&lt;/h2&gt;
&lt;blockquote&gt;
&lt;h3 id=&quot;주요-키워드&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A3%BC%EC%9A%94-%ED%82%A4%EC%9B%8C%EB%93%9C&quot; aria-label=&quot;주요 키워드 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;주요 키워드&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;계산 vs. 액션(computation vs. action)&lt;/li&gt;
&lt;li&gt;순수 함수(pure function)&lt;/li&gt;
&lt;li&gt;부수 효과(side effect)&lt;/li&gt;
&lt;li&gt;참조 투명성(referential transparency), 치환 모형(substitution model)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;이 책의 1장을 읽어보셨다면, 함수형 프로그래밍의 기본 개념을 어느 정도 이해하실 수 있을 겁니다.&lt;/p&gt;
&lt;p&gt;간단한 예제를 통해 함수형 프로그래밍이 어떤 느낌인지 간단히 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;먼저 &lt;strong&gt;순수 함수&lt;/strong&gt;라는 개념에 대해서 알아볼 건데, 다음과 같이 &lt;u&gt;순수하지 않은 함수에서 순수 함수를 추출&lt;/u&gt;해내는 코드 리팩터링 과정의 예제를 살펴봅시다.&lt;/p&gt;
&lt;p&gt;다음은 정수 n을 입력으로 받아 그 제곱 값을 &lt;strong&gt;계산&lt;/strong&gt;하고, 문자열 템플릿을 적용하여 결과를 표준출력으로 출력하는 코드입니다. 여기서는 &lt;strong&gt;계산&lt;/strong&gt;과 &lt;strong&gt;액션&lt;/strong&gt;(콘솔 출력)이 혼합된 형태입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; printSquare&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;n&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Unit&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; square &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; n &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; n
  println&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-interpolation&quot;&gt;&lt;span class=&quot;token id function&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;The square of &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;n&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; is &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;square&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 코드에서 &lt;code&gt;n * n&lt;/code&gt; 부분을 별도의 &lt;code&gt;calculateSquare()&lt;/code&gt; 함수로 추출해, 다음과 같이 리팩터링할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; calculateSquare&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;n&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; n &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; n

&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; printSquare&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;n&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Unit&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; square &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; calculateSquare&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;n&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  println&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-interpolation&quot;&gt;&lt;span class=&quot;token id function&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;The square of &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;n&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; is &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;square&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 과정을 조금 더 발전시켜, 문자열 템플릿 적용 부분도 &lt;code&gt;calculateMessage()&lt;/code&gt; 함수로 추출하여 이렇게 리팩터링할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; calculateSquare&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;n&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; n &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; n

&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; calculateMessage&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;n&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; square&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  &lt;span class=&quot;token string-interpolation&quot;&gt;&lt;span class=&quot;token id function&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;The square of &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;n&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; is &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;square&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; printSquare&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;n&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Unit&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; square &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; calculateSquare&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;n&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; message &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; calculateMessage&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;n&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; square&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  println&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 리팩터링을 통해, &lt;strong&gt;액션&lt;/strong&gt;(콘솔 출력)과 &lt;strong&gt;계산&lt;/strong&gt; 부분을 분리하여 &lt;strong&gt;순수 함수&lt;/strong&gt;를 추출해낼 수 있었습니다.&lt;/p&gt;
&lt;p&gt;이러한 과정이 자연스럽게 느껴지시나요?&lt;/p&gt;
&lt;blockquote&gt;
&lt;h3 id=&quot;관심사의-분리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B4%80%EC%8B%AC%EC%82%AC%EC%9D%98-%EB%B6%84%EB%A6%AC&quot; aria-label=&quot;관심사의 분리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;관심사의 분리&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;순수 함수&lt;/strong&gt;는 입력을 오직 함수의 인자로부터 받고, 출력은 반환 값으로만 이루어집니다. 즉, 인자가 아닌 외부 값을 읽거나, 외부 변수에 출력을 쓰는 등의 동작은 하지 않습니다. &lt;strong&gt;순수 함수&lt;/strong&gt;는 그 자체로 완결적이며, 함수 외부의 상태나 변화에 신경 쓸 필요가 없습니다. 이로 인해 함수가 독립적으로 동작하게 되어 코드의 이해와 유지보수가 훨씬 용이해집니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이렇게 입력에 대한 &lt;strong&gt;계산&lt;/strong&gt;과 그 결과 출력만으로 이루어져있고, 함수 호출을 해도 아무 상태도 바꾸지 않는 위 &lt;code&gt;calculateSquare()&lt;/code&gt;, &lt;code&gt;calculateMessage()&lt;/code&gt; 같은 함수를 &lt;strong&gt;순수 함수&lt;/strong&gt;라고 합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;순수 함수&lt;/strong&gt;는 &lt;u&gt;수학적 함수&lt;/u&gt;를 말합니다. 즉, &lt;u&gt;수학에서 말하는 함수&lt;/u&gt;라는 개념이 바로 &lt;strong&gt;순수 함수&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h3 id=&quot;함수function-프로시저procedure-서브루틴subroutine-메서드method-차이&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%A8%EC%88%98function-%ED%94%84%EB%A1%9C%EC%8B%9C%EC%A0%80procedure-%EC%84%9C%EB%B8%8C%EB%A3%A8%ED%8B%B4subroutine-%EB%A9%94%EC%84%9C%EB%93%9Cmethod-%EC%B0%A8%EC%9D%B4&quot; aria-label=&quot;함수function 프로시저procedure 서브루틴subroutine 메서드method 차이 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;함수(function), 프로시저(procedure), 서브루틴(subroutine), 메서드(method) 차이?&lt;/h3&gt;
&lt;p&gt;프로그래밍 언어에서 부 프로그램 코드를 호출할 수 있는 부분을 정의하는 여러가지 방법들이 있습니다.
엄밀히 말하면 서로 다른 성격의 것들이지만 대개 프로그래밍 언어의 문법상으로는 같은 취급을 하는 경우가 많습니다.
입력 값이나 반환 값이 있느냐, 부수 효과를 동반하느냐, 객체나 클래스에 종속되어 있느냐에 따라 조금씩 의미를 달리합니다.
하지만 함수형 프로그래밍 언어에서 함수를 언급할 때는 기본적으로 수학적 &lt;strong&gt;순수 함수&lt;/strong&gt;라고 생각하면 됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;u&gt;함수를 수학에서 정의하는 함수처럼 쓰는 것&lt;/u&gt;은 함수형 프로그래밍의 실천 사항 중의 하나라고 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;앞으로 이 공식을 잘 기억해 두세요.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;계산으로만 이루어짐 = 부수 효과 없음 = 순수성 = 참조 투명성&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;각각의 개념을 간단히 설명하자면 다음과 같습니다.&lt;/p&gt;
&lt;h3 id=&quot;계산으로만-이루어짐-composed-purely-of-computation&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B3%84%EC%82%B0%EC%9C%BC%EB%A1%9C%EB%A7%8C-%EC%9D%B4%EB%A3%A8%EC%96%B4%EC%A7%90-composed-purely-of-computation&quot; aria-label=&quot;계산으로만 이루어짐 composed purely of computation permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;계산으로만 이루어짐 (&lt;strong&gt;Composed purely of computation)&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;함수의 동작이 오로지 입력에 기반한 &lt;strong&gt;계산&lt;/strong&gt; 결과를 반환한다는 의미입니다. 함수는 외부 상태나 다른 요인에 의존하지 않으며, 주어진 입력에 대해 항상 동일한 출력을 반환합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;calculateSquare()&lt;/code&gt;: 입력 &lt;code&gt;n&lt;/code&gt;만을 사용하여 &lt;strong&gt;계산&lt;/strong&gt;합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;calculateMessage()&lt;/code&gt;: 입력 &lt;code&gt;n&lt;/code&gt;과 &lt;code&gt;square&lt;/code&gt; 값만으로 &lt;strong&gt;계산&lt;/strong&gt;합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;부수-효과-없음-no-side-effects&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B6%80%EC%88%98-%ED%9A%A8%EA%B3%BC-%EC%97%86%EC%9D%8C-no-side-effects&quot; aria-label=&quot;부수 효과 없음 no side effects permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;부수 효과 없음 (&lt;strong&gt;No side effects&lt;/strong&gt;)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;부수 효과&lt;/strong&gt;는 부작용(副作用) 혹은 영어로 사이드 이펙트(side effect)라고도 부릅니다.&lt;/p&gt;
&lt;p&gt;함수가 반환하는 &lt;strong&gt;계산&lt;/strong&gt; 결과를 작용(effect) 혹은 주작용(main effect)이라고 한다면, 함수의 실행으로 인해 초래되는 작용 이외의 부수적인 변화는 모두 부작용(&lt;strong&gt;부수 효과&lt;/strong&gt;)에 해당합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;부수 효과&lt;/strong&gt;가 없다는 것은 함수 실행 중에 외부 상태를 변경하거나 외부에서 관찰 가능한 다른 효과를 발생시키지 않는 상태를 의미합니다.&lt;/p&gt;
&lt;p&gt;예를 들어, 파일 시스템, 데이터베이스, 네트워크 입출력이나 이와 유사한 동작이 없는 함수를 가리킵니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;calculateSquare()&lt;/code&gt;, &lt;code&gt;calculateMessage()&lt;/code&gt;는 &lt;strong&gt;계산&lt;/strong&gt; 결과만 반환하며, 외부 상태를 변경하지 않습니다.&lt;/li&gt;
&lt;li&gt;반면에 &lt;code&gt;printSquare()&lt;/code&gt;는 &lt;code&gt;println()&lt;/code&gt;을 호출해 표준 출력에 값을 쓰므로 &lt;strong&gt;부수 효과&lt;/strong&gt;가 발생합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;순수성-purity&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%88%9C%EC%88%98%EC%84%B1-purity&quot; aria-label=&quot;순수성 purity permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;순수성 (Purity)&lt;/h3&gt;
&lt;p&gt;위의 두 가지 조건을 만족하는 함수를 순수한 함수라고 합니다. 즉, 동일한 입력에 대해 항상 동일한 출력을 반환하고, 외부에 영향을 미치지 않는 함수입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;따라서 &lt;code&gt;calculateSquare()&lt;/code&gt;와 &lt;code&gt;calculateMessage()&lt;/code&gt;는 &lt;strong&gt;순수 함수&lt;/strong&gt;입니다.&lt;/li&gt;
&lt;li&gt;반대로 &lt;code&gt;printSquare()&lt;/code&gt;는 순수하지 않은 &lt;code&gt;println()&lt;/code&gt;을 중간에 실행하므로 순수하지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;참조-투명성-referential-transparency&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B0%B8%EC%A1%B0-%ED%88%AC%EB%AA%85%EC%84%B1-referential-transparency&quot; aria-label=&quot;참조 투명성 referential transparency permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;참조 투명성 (&lt;strong&gt;Referential transparency&lt;/strong&gt;)&lt;/h3&gt;
&lt;p&gt;이 용어는 분석 철학에서 어떤 표현이나 문장이 전체 글 내에서 항상 동일한 의미를 가지는 경우를 가리키는 의미로 사용되기도 합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;참조 투명성&lt;/strong&gt;은 같은 입력 표현(함수 호출)에 대해 항상 같은 결과를 내는 성질을 의미하며, 프로그램에서 어떤 표현식을 그 평가 값으로 대체해도 그 프로그램의 결과가 동일하다는 것을 뜻합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;예를 들어, &lt;code&gt;calculateSquare(2)&lt;/code&gt;는 &lt;code&gt;2 * 2 = 4&lt;/code&gt;이므로, &lt;code&gt;calculateSquare(2)&lt;/code&gt;를 &lt;code&gt;4&lt;/code&gt;로 대체해도 결과는 변하지 않습니다. 또한, &lt;code&gt;calculateSquare()&lt;/code&gt;와 &lt;code&gt;calculateMessage()&lt;/code&gt;는 몇 번을 호출해도 동일한 결과를 반환하므로 참조 투명합니다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;printSquare()&lt;/code&gt;는 콘솔에 같은 메시지를 출력하지만, 호출할 때마다 콘솔에 출력되는 횟수가 더해지므로 참조 투명하지 않습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;결국, 이 모든 개념은 서로 긴밀하게 연결되어 있습니다. 이후에 더 깊이 다룰 예정이지만, 1장에서는 이 용어들과 개념을 간단히 요약해 두시면 충분합니다.&lt;/p&gt;
&lt;p&gt;함수형 프로그래밍은 이렇게 &lt;strong&gt;부수 효과&lt;/strong&gt;가 없는 &lt;strong&gt;순수 함수&lt;/strong&gt;를 사용하고, &lt;strong&gt;참조 투명성&lt;/strong&gt;을 지키는 방향으로 코드를 작성합니다. 그렇다면 이렇게 하는 이유는 무엇이고, 그로 인해 얻을 수 있는 이점은 무엇일까요?&lt;/p&gt;
&lt;h3 id=&quot;함수형-프로그래밍의-이점&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%A8%EC%88%98%ED%98%95-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%EC%9D%98-%EC%9D%B4%EC%A0%90&quot; aria-label=&quot;함수형 프로그래밍의 이점 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;함수형 프로그래밍의 이점&lt;/h3&gt;
&lt;p&gt;지금까지 설명한 내용을 바탕으로, 함수형 프로그래밍의 주요 이점을 다음과 같이 정리할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;코드의 가독성 및 유지보수성&lt;/strong&gt;: 순수성과 &lt;strong&gt;참조 투명성&lt;/strong&gt; 덕분에 코드의 의도가 명확히 드러납니다. 순수 함수는 함수 시그니처(함수 이름 + 인자 + 반환 타입)대로 동작하는 것이 보장되고, 함수 시그니처로는 파악할 수 없는 숨겨진 동작(&lt;strong&gt;부수 효과&lt;/strong&gt;)을 하지 않으므로 가독성이 높아지고, 코드 변경 시에도 영향을 쉽게 추적할 수 있어 유지보수가 수월해집니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;디버깅 및 테스트 용이성&lt;/strong&gt;: &lt;strong&gt;순수 함수&lt;/strong&gt;는 외부 상태를 변경하지 않기 때문에, 함수가 예상과 다른 결과를 내더라도 해당 함수만 집중적으로 살펴보면 됩니다. 이는 프로그램의 특정 부분을 독립적으로 테스트할 수 있게 만들어주며, &lt;strong&gt;부수 효과&lt;/strong&gt;가 있는 함수보다 &lt;strong&gt;순수 함수&lt;/strong&gt;가 테스트하기에 훨씬 적합합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;참조 투명성과 컴파일러&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;참조 투명성이 지켜지는 코드는 컴파일러가 분석하기 쉬워져 코드 블럭을 재구성하여 중복 &lt;strong&gt;계산&lt;/strong&gt;을 제거하거나 실행 순서를 최적화할 수 있는 가능성을 열어주고, 더 나아가서 병렬 처리를 용이하게 하는 등의 도움을 줍니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이 외에도 여러 이점이 있겠지만, 이번에는 1장에서 언급된 내용만 다루고 넘어가겠습니다.&lt;/p&gt;
&lt;h2 id=&quot;치환-모형substitution-model과-등식적-추론equational-reasoning&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B9%98%ED%99%98-%EB%AA%A8%ED%98%95substitution-model%EA%B3%BC-%EB%93%B1%EC%8B%9D%EC%A0%81-%EC%B6%94%EB%A1%A0equational-reasoning&quot; aria-label=&quot;치환 모형substitution model과 등식적 추론equational reasoning permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;치환 모형(substitution model)과 등식적 추론(equational reasoning)&lt;/h2&gt;
&lt;p&gt;조금 거창하게 들리는 &lt;strong&gt;치환 모형&lt;/strong&gt;과 &lt;strong&gt;등식적 추론&lt;/strong&gt;(&lt;strong&gt;등식 추론&lt;/strong&gt;이라고도 부릅니다)에 대해 알아보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;참조 투명성&lt;/strong&gt;이 유지된다면, 아래와 같이 &lt;strong&gt;치환 모형&lt;/strong&gt;을 사용한 &lt;strong&gt;등식적 추론&lt;/strong&gt;이 가능합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 함수 정의가 이렇고&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; calculateSquare&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;n&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; n &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; n

calculateSquare&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 이 표현식이 주어졌을 때,&lt;/span&gt;

&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;n &lt;span class=&quot;token keyword&quot;&gt;=&gt;&lt;/span&gt; n &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; n&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;// 1. calculateSquare()를 본문(n * n)으로 치환&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;            &lt;span class=&quot;token comment&quot;&gt;// 2. n=2 인자를 적용하여 계산 표현식으로 치환&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;                &lt;span class=&quot;token comment&quot;&gt;// 3. 표현식을 계산한 결과로 치환&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;즉, &lt;code&gt;f(x) = x * x&lt;/code&gt;일 때, &lt;code&gt;f(2)&lt;/code&gt;는 &lt;code&gt;2 * 2&lt;/code&gt;로 치환할 수 있고, 또 &lt;code&gt;2 * 2&lt;/code&gt;는 &lt;code&gt;4&lt;/code&gt;로 치환되어 최종적으로 &lt;code&gt;4&lt;/code&gt;라는 결과로 유도됩니다.&lt;/p&gt;
&lt;p&gt;이게 별로 특별할 것이 없는 당연한 이야기처럼 느껴지시나요?&lt;/p&gt;
&lt;p&gt;이렇게 할 수 있는 이유는 바로 &lt;code&gt;calculateSquare()&lt;/code&gt; 함수가 &lt;strong&gt;순수 함수&lt;/strong&gt;이며, 그래서 &lt;strong&gt;참조 투명성&lt;/strong&gt;이 지켜지는 덕분에 가능한 것입니다.&lt;/p&gt;
&lt;p&gt;그렇다면 반대로 &lt;u&gt;&lt;strong&gt;등식적 추론&lt;/strong&gt;이 불가능한 함수&lt;/u&gt;의 경우를 생각해봅시다. 그리고 왜 &lt;strong&gt;등식적 추론&lt;/strong&gt;이 성립하지 않는지도 생각해봅시다.&lt;/p&gt;
&lt;h3 id=&quot;예시1-외부-상태에-의존하거나-상태를-변경하는-함수&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%88%EC%8B%9C1-%EC%99%B8%EB%B6%80-%EC%83%81%ED%83%9C%EC%97%90-%EC%9D%98%EC%A1%B4%ED%95%98%EA%B1%B0%EB%82%98-%EC%83%81%ED%83%9C%EB%A5%BC-%EB%B3%80%EA%B2%BD%ED%95%98%EB%8A%94-%ED%95%A8%EC%88%98&quot; aria-label=&quot;예시1 외부 상태에 의존하거나 상태를 변경하는 함수 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;예시1 (외부 상태에 의존하거나 상태를 변경하는 함수)&lt;/h3&gt;
&lt;p&gt;아래는 순수하지 않은 함수의 예시입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; counter &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; incrementCounter&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  counter &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; counter &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
  counter
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이와 같은 상황에서는 &lt;strong&gt;등식적 추론&lt;/strong&gt;을 적용할 수 없습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;incrementCounter&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 처음 호출에서 1을 반환&lt;/span&gt;
incrementCounter&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 두 번째 호출에서 2를 반환&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 함수는 &lt;u&gt;외부 상태인 counter 변수를 변경&lt;/u&gt;하기 때문에, &lt;u&gt;같은 함수를 호출하더라도 매번 결과가 달라&lt;/u&gt;집니다. 따라서 &lt;strong&gt;참조 투명성&lt;/strong&gt;이 유지되지 않습니다. &lt;strong&gt;등식적 추론&lt;/strong&gt;을 하기 위해서는 함수 호출이 &lt;strong&gt;참조 투명성&lt;/strong&gt;을 가져야 하며, 이는 동일한 입력에 대해 언제나 동일한 출력을 반환해야 함을 의미합니다.&lt;/p&gt;
&lt;h3 id=&quot;예시2-순수하지-않은-동작을-포함하는-함수&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%88%EC%8B%9C2-%EC%88%9C%EC%88%98%ED%95%98%EC%A7%80-%EC%95%8A%EC%9D%80-%EB%8F%99%EC%9E%91%EC%9D%84-%ED%8F%AC%ED%95%A8%ED%95%98%EB%8A%94-%ED%95%A8%EC%88%98&quot; aria-label=&quot;예시2 순수하지 않은 동작을 포함하는 함수 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;예시2 (순수하지 않은 동작을 포함하는 함수)&lt;/h3&gt;
&lt;p&gt;또 다른 형태의 &lt;strong&gt;참조 투명하지 않은 함수&lt;/strong&gt;는 &lt;strong&gt;부수 효과&lt;/strong&gt;를 동반하는 함수입니다. 예를 들어, 함수가 콘솔에 값을 출력하거나 파일에 데이터를 기록하는 등의 입출력(I/O) 작업을 할 때 &lt;strong&gt;참조 투명성&lt;/strong&gt;이 깨집니다.&lt;/p&gt;
&lt;p&gt;예를 들어, 콘솔에 값을 출력하는 함수의 경우를 생각해봅시다:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; printHello&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Unit&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  println&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Hello, world!&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

printHello&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;// 이 표현식에 대해 등식적 추론을 적용해 봅시다&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 함수를 &lt;strong&gt;등식적 추론&lt;/strong&gt;으로 치환해 본다면 다음과 같은 문제가 생깁니다:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;printHello&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;// () 리턴, 콘솔에 &quot;Hello, world!&quot; 출력&lt;/span&gt;
printHello&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;// () 리턴, 또다시 &quot;Hello, world!&quot; 출력&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 함수는 &lt;code&gt;Unit&lt;/code&gt;를 리턴하지만, 출력하는 행위 자체가 &lt;strong&gt;부수 효과&lt;/strong&gt;이기 때문에 참조 투명하지 않습니다. 같은 값을 반환하는 함수라 하더라도 그 함수가 수행하는 &lt;strong&gt;액션&lt;/strong&gt;(콘솔 출력) 때문에 &lt;strong&gt;등식적 추론&lt;/strong&gt;이 불가능합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;printHello()&lt;/code&gt;는 단순히 값을 반환하는 것이 아니라 콘솔에 출력을 발생시키는 &lt;strong&gt;부수 효과&lt;/strong&gt;가 포함되어 있습니다.&lt;/li&gt;
&lt;li&gt;즉, 같은 입력(Unit)에 대해 호출할 때마다 같은 결과(Unit)가 나오더라도, 그 과정에서 발생하는 &lt;strong&gt;부수 효과&lt;/strong&gt; 때문에 &lt;strong&gt;등식적 추론&lt;/strong&gt;을 할 수 없습니다. 함수 호출이 반복될 때마다 콘솔에 새로운 문자열이 찍히는 &lt;strong&gt;액션&lt;/strong&gt;이 일어나기 때문입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;예시3-예외를-던지는-함수&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%88%EC%8B%9C3-%EC%98%88%EC%99%B8%EB%A5%BC-%EB%8D%98%EC%A7%80%EB%8A%94-%ED%95%A8%EC%88%98&quot; aria-label=&quot;예시3 예외를 던지는 함수 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;예시3 (예외를 던지는 함수)&lt;/h3&gt;
&lt;p&gt;예외를 던지는 함수는 특정 상황에서 정상적으로 값을 반환하지 않고, 프로그램 흐름을 중단시키며 예외를 발생시키므로 &lt;strong&gt;참조 투명성&lt;/strong&gt;을 유지하지 않습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; divide&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; b&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;b &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; ArithmeticException&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Division by zero&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; b
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;위 함수는 &lt;code&gt;divide(4, 2)&lt;/code&gt;를 호출하면 &lt;code&gt;2&lt;/code&gt;를 반환하지만, &lt;code&gt;divide(4, 0)&lt;/code&gt;을 호출하면 예외를 던집니다. 따라서 동일한 입력에 대해 언제나 동일한 출력을 보장하지 않습니다. 즉, 특정 입력에서 예외가 발생하므로 &lt;strong&gt;등식적 추론&lt;/strong&gt;이 성립하지 않습니다.&lt;/p&gt;
&lt;h3 id=&quot;예시4-종료되지-않는-함수&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%88%EC%8B%9C4-%EC%A2%85%EB%A3%8C%EB%90%98%EC%A7%80-%EC%95%8A%EB%8A%94-%ED%95%A8%EC%88%98&quot; aria-label=&quot;예시4 종료되지 않는 함수 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;예시4 (종료되지 않는 함수)&lt;/h3&gt;
&lt;p&gt;이런 함수는 호출될 때 정상적으로 값을 반환하지 않고 영원히 실행을 지속하거나 실행이 멈추지 않습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; infiniteLoop&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 함수는 언제나 &lt;code&gt;0&lt;/code&gt;을 반환하는 것으로 보일 수 있지만, 실제로는 무한 루프에 빠져 실행이 끝나지 않습니다. 따라서 &lt;strong&gt;등식적 추론&lt;/strong&gt;을 통해 &lt;code&gt;infiniteLoop()&lt;/code&gt;를 &lt;code&gt;0&lt;/code&gt;으로 대체할 수 없습니다. 호출 결과가 무한 실행으로 이어지기 때문에 &lt;strong&gt;참조 투명성&lt;/strong&gt;이 깨집니다.&lt;/p&gt;
&lt;h3 id=&quot;예시5-현재-시간-또는-임의의-값을-반환하는-함수&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%88%EC%8B%9C5-%ED%98%84%EC%9E%AC-%EC%8B%9C%EA%B0%84-%EB%98%90%EB%8A%94-%EC%9E%84%EC%9D%98%EC%9D%98-%EA%B0%92%EC%9D%84-%EB%B0%98%ED%99%98%ED%95%98%EB%8A%94-%ED%95%A8%EC%88%98&quot; aria-label=&quot;예시5 현재 시간 또는 임의의 값을 반환하는 함수 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;예시5 (현재 시간 또는 임의의 값을 반환하는 함수)&lt;/h3&gt;
&lt;p&gt;현재 시간이나 임의의 값을 반환하는 함수는 매 호출마다 다른 결과를 반환하기 때문에 &lt;strong&gt;참조 투명성&lt;/strong&gt;을 유지하지 않습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; getCurrentTime&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Long&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; System&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;currentTimeMillis&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; getRandomNumber&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; scala&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;util&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Random&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;nextInt&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;getCurrentTime()&lt;/code&gt; 함수는 호출할 때마다 다른 값을 반환하며, &lt;code&gt;getRandomNumber()&lt;/code&gt; 함수도 임의의 값을 생성하여 반환합니다. 이러한 함수들은 같은 입력에 대해 다른 결과를 반환하므로 &lt;strong&gt;등식적 추론&lt;/strong&gt;을 적용할 수 없습니다.&lt;/p&gt;
&lt;h3 id=&quot;예시6-외부-자원에-의존하는-함수&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%88%EC%8B%9C6-%EC%99%B8%EB%B6%80-%EC%9E%90%EC%9B%90%EC%97%90-%EC%9D%98%EC%A1%B4%ED%95%98%EB%8A%94-%ED%95%A8%EC%88%98&quot; aria-label=&quot;예시6 외부 자원에 의존하는 함수 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;예시6 (외부 자원에 의존하는 함수)&lt;/h3&gt;
&lt;p&gt;외부 자원에 접근하는 함수, 예를 들어 파일을 읽거나 데이터베이스에서 값을 조회하는 함수는 외부 상태나 환경에 따라 다른 결과를 반환할 수 있기 때문에 &lt;strong&gt;참조 투명성&lt;/strong&gt;을 유지하지 않습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; readFile&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;filename&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  scala&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;io&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Source&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fromFile&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;filename&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;mkString
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 함수는 파일의 내용을 읽어 반환하지만, 파일 내용이 변경될 경우 같은 &lt;code&gt;filename&lt;/code&gt;을 인자로 넘겨도 다른 결과를 반환할 수 있습니다. 외부 자원 상태에 따라 결과가 달라지기 때문에 &lt;strong&gt;등식적 추론&lt;/strong&gt;이 불가능합니다.&lt;/p&gt;
&lt;h3 id=&quot;예시7-사용자-입력에-의존하는-함수&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%88%EC%8B%9C7-%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%9E%85%EB%A0%A5%EC%97%90-%EC%9D%98%EC%A1%B4%ED%95%98%EB%8A%94-%ED%95%A8%EC%88%98&quot; aria-label=&quot;예시7 사용자 입력에 의존하는 함수 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;예시7 (사용자 입력에 의존하는 함수)&lt;/h3&gt;
&lt;p&gt;사용자 입력을 요청하는 함수는 호출 시마다 사용자의 입력이 달라질 수 있어 같은 호출에서도 다른 결과를 반환할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; getUserInput&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  scala&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;io&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;StdIn&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;readLine&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;getUserInput()&lt;/code&gt; 함수는 사용자가 입력하는 값에 따라 달라지므로 &lt;strong&gt;참조 투명성&lt;/strong&gt;을 유지하지 않습니다. 동일한 입력에 대해 동일한 출력을 보장할 수 없기 때문에 &lt;strong&gt;등식적 추론&lt;/strong&gt;을 적용할 수 없습니다.&lt;/p&gt;
&lt;h3 id=&quot;예시8-네트워크-호출을-수행하는-함수&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%88%EC%8B%9C8-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%ED%98%B8%EC%B6%9C%EC%9D%84-%EC%88%98%ED%96%89%ED%95%98%EB%8A%94-%ED%95%A8%EC%88%98&quot; aria-label=&quot;예시8 네트워크 호출을 수행하는 함수 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;예시8 (네트워크 호출을 수행하는 함수)&lt;/h3&gt;
&lt;p&gt;네트워크 통신을 통해 외부 API에 접근하거나 네트워크 요청을 수행하는 함수는 네트워크 상태나 외부 서버의 응답에 따라 다른 결과를 반환할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; fetchDataFromApi&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 네트워크 요청을 보낸다고 가정&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 실제 구현은 생략&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;네트워크 상태나 API 서버의 상태에 따라 결과가 달라질 수 있으며, 네트워크 장애나 지연이 발생할 수도 있기 때문에 이 함수는 &lt;strong&gt;등식적 추론&lt;/strong&gt;이 불가능합니다.&lt;/p&gt;
&lt;p&gt;이상으로 &lt;strong&gt;등식적 추론&lt;/strong&gt;이 불가능한 함수의 유형을 알아보았고, 다음과 같이 정리할 수 있습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;등식적 추론이 안 되는 함수 = 참조 투명하지 않은 함수 = 순수하지 않은 함수 = 부수 효과가 있는 함수&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;순수-함수만-사용해서-일반적으로-유용한-프로그램을-작성할-수-있을까요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%88%9C%EC%88%98-%ED%95%A8%EC%88%98%EB%A7%8C-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%84%9C-%EC%9D%BC%EB%B0%98%EC%A0%81%EC%9C%BC%EB%A1%9C-%EC%9C%A0%EC%9A%A9%ED%95%9C-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8%EC%9D%84-%EC%9E%91%EC%84%B1%ED%95%A0-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C%EC%9A%94&quot; aria-label=&quot;순수 함수만 사용해서 일반적으로 유용한 프로그램을 작성할 수 있을까요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;순수 함수만 사용해서 일반적으로 유용한 프로그램을 작성할 수 있을까요?&lt;/h2&gt;
&lt;p&gt;답은 &lt;strong&gt;아니다&lt;/strong&gt; 입니다. 함수형 프로그래밍 언어로 작성한 프로그램이라도 단순 &lt;strong&gt;계산&lt;/strong&gt; 이상의 기능(데이터베이스 입출력, 네트워크 호출 등)이 필요한 경우 결국 어느 시점에는 순수하지 않는 동작을 실행하게 됩니다.&lt;/p&gt;
&lt;p&gt;그러면 도대체 왜 함수형 프로그래밍에서는 이렇게 &lt;strong&gt;순수 함수&lt;/strong&gt;를 강조할까요? 그것은 프로그램의 코드 대부분에 실제로 &lt;strong&gt;순수 함수&lt;/strong&gt;를 사용하는 것이 일반적이기 때문입니다. &lt;strong&gt;순수 함수&lt;/strong&gt;를 합성해서 더 큰 단위의 &lt;strong&gt;순수 함수&lt;/strong&gt;를 만들고, 그 함수들을 또 합성하여 기능을 수행하는 프로그램 코드가 됩니다. 그리고 마지막에 가서야 그 프로그램을 &lt;u&gt;딱 한 번 순수하지 않게 실행&lt;/u&gt;합니다.&lt;/p&gt;
&lt;p&gt;예를 들어, 한 번 실행하고 바로 종료하는 콘솔 프로그램이라면 주로 메인 함수에서, 반면에 반복적인 요청을 처리하는 서버라면 요청을 처리하는 컨트롤러 부분에서 그렇게 할 가능성이 높습니다. (그러나 이 경우 아마 함수형 웹 프레임워크나 라이브러리에 의해 순수하지 않는 동작을 실행하는 부분이 숨겨지게 되는 것이 일반적입니다)&lt;/p&gt;
&lt;p&gt;즉, 가능하면 &lt;strong&gt;순수 함수&lt;/strong&gt;를 합성해서 프로그램을 작성하고, &lt;strong&gt;부수 효과&lt;/strong&gt;를 마지막 순간까지 유예하는 방식으로 프로그래밍하는 것이 함수형 프로그래밍의 일반적인 전략입니다.&lt;/p&gt;
&lt;p&gt;그리하여 오늘도 쿠키런: 킹덤 서버는 잘 돌아가고 있습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;이에 관한 자세한 내용은 나중에 책의 후반부, &lt;strong&gt;제13장 외부 효과와 입출력&lt;/strong&gt; 편에서 다루어집니다.
여러분들이 아마 어딘가에서 들어봤을 &lt;strong&gt;모나드(Monad)&lt;/strong&gt; 같은 것들이 바로 함수형 프로그래밍에서 함수 합성을 가능하게 하는 도구 중의 하나입니다. 앞으로 이런 새롭고 흥미로운 개념들을 많이 만나보게 될 것입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;정리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%95%EB%A6%AC&quot; aria-label=&quot;정리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;정리&lt;/h2&gt;
&lt;p&gt;이번에 &lt;strong&gt;『스칼라로 배우는 함수형 프로그래밍』 제1장 함수형 프로그래밍이란 무엇인가?&lt;/strong&gt; 를 함께 읽어보았습니다.&lt;/p&gt;
&lt;p&gt;이번 장에서 다룬 주요 키워드는 다음과 같습니다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;계산 vs. 액션&lt;/strong&gt; (computation vs. action)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;순수 함수&lt;/strong&gt; (pure function)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;부수 효과&lt;/strong&gt; (side effect)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;참조 투명성&lt;/strong&gt; (referential transparency)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;치환 모형&lt;/strong&gt; (substitution model)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;등식적 추론&lt;/strong&gt; (equational reasoning)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;지금까지 간단한 코드 예제들을 통해서 각각의 용어의 의미가 무엇인지 살펴보았습니다.&lt;/p&gt;
&lt;p&gt;이 용어와 개념을 잘 이해해두면 앞으로 책을 읽어가는 데 도움이 될 것입니다. 함수형 프로그래밍의 기본을 차근차근 다져 나가는 과정에서, 이러한 개념들이 어떻게 서로 연결되는지 확인하는 것이 중요합니다.&lt;/p&gt;
&lt;p&gt;이제 막 첫 장을 마쳤고, 앞으로 점점 더 흥미롭고 심화된 내용이 나올 것입니다. 저도 계속해서 책을 다시 읽어보고 내용을 정리하며 다음 글에서 더 유익한 설명을 드리도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;그럼 다음 편에서 뵙겠습니다. 감사합니다.&lt;/p&gt;
&lt;h2 id=&quot;더보기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8D%94%EB%B3%B4%EA%B8%B0&quot; aria-label=&quot;더보기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;더보기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%ED%95%A8%EC%88%98%ED%98%95_%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://ko.wikipedia.org/wiki/함수형_프로그래밍&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%EC%B0%B8%EC%A1%B0_%ED%88%AC%EB%AA%85%EC%84%B1&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://ko.wikipedia.org/wiki/참조_투명성&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://ko.wikipedia.org/wiki/%EB%B6%80%EC%9E%91%EC%9A%A9_(%EC%BB%B4%ED%93%A8%ED%84%B0_%EA%B3%BC%ED%95%99)&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://ko.wikipedia.org/wiki/부작용_(컴퓨터_과학)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[타입스크립트스럽게 성능과 생산성 두 마리 토끼 모두 잡기]]></title><description><![CDATA[최고의 자동완성 플러그인 타입스크립트를 사용하여 개발자를 속이고 코드를 최적화한 경험을 공유합니다.]]></description><link>https://tech.devsisters.com/posts/typescript-ish-way-to-improve-performance</link><guid isPermaLink="false">https://tech.devsisters.com/posts/typescript-ish-way-to-improve-performance</guid><pubDate>Wed, 04 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, 이번 글에서는 javascript의 Proxy API를 사용해 웹 기반 &lt;code&gt;게임 Admin&lt;/code&gt;에서 gRPC 게임 서버와 효과적으로 통신하는 방법을 소개합니다.&lt;/p&gt;
&lt;h1 id=&quot;introduction&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#introduction&quot; aria-label=&quot;introduction permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Introduction&lt;/h1&gt;
&lt;p&gt;요즘 웹 서비스들은 통신 방식으로 대부분 REST API를 사용합니다. 하지만 실시간으로 많은 데이터를 주고받아야 하는 게임의 특성상, REST API를 사용하기에는 어려운 부분이 있습니다. &lt;strong&gt;쿠키런: 킹덤&lt;/strong&gt;을 포함한 데브시스터즈의 여러 게임은 효율적인 통신을 위해 API 프로토콜로 gRPC를 채택하고 있습니다. 아마 gRPC를 처음 들어보시는 분들도 있을 것 같은데요, gRPC의 특징 몇 가지 짚고 가겠습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;gRPC는 HTTP/2에 커스텀 헤더를 추가한 프로토콜로 통신합니다.&lt;/li&gt;
&lt;li&gt;메시지 데이터는 protocol buffers(이하 protobuf)를 사용해 바이너리 형태로 직렬화하여 전송합니다.&lt;/li&gt;
&lt;li&gt;protobuf를 사용하여 데이터 구조를 정의할 수 있고, code-gen 라이브러리로 &lt;code&gt;.proto&lt;/code&gt; 스키마에 대응하는 &lt;code&gt;.d.ts&lt;/code&gt; 파일과 &lt;code&gt;.js&lt;/code&gt;파일을 생성할 수 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;(&lt;a href=&quot;https://protobuf.dev/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;protobuf&lt;/a&gt;와 &lt;a href=&quot;https://grpc.io/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;gRPC&lt;/a&gt;에 대한 자세한 내용은 이 링크를 참고해 주세요)&lt;/p&gt;
&lt;p&gt;위의 세 가지 특징으로 &lt;code&gt;게임 Admin&lt;/code&gt;의 아키텍처를 아래와 같이 구성했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 968px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 20.833333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAECAYAAACOXx+WAAAACXBIWXMAAAsTAAALEwEAmpwYAAABD0lEQVR42mP4ryIk899Gzvk/AwMTAxCoqKiwy8vLczBA+TIyMpz1++1ZzDzM+IyNjZXtGexZwOpyK0W1QpOFQOz6/fM5lm6f5LJ01zxVhrrKgq17Zk/5/38HTwBI0kJfRlpJSUkMaDAfiK8mpekc3OzR453rNlFfS3+ipa2lHUhc3S9Cytg9UBLEPjkn2/Lx4oz/Oxu8VzJILMwtatuad/T/PTZ1kKSpqZSwnJycIMhlQC4jA9BFEZMjpJKmxgeYOpsKq3iosGtpabFJ8vCIGKhIiALVsDw8ckTq8pyUxRMqfOOBXmLg9JdnEGBAABag11hhXgYDLQY2PVc9Mag4CDCCDGUAYbClQHGQA7RC2QAIlEFHLeBKVQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/3218d2476faf1a5df2464788fe019259/33ecc/admin-archi.webp 360w,
/static/3218d2476faf1a5df2464788fe019259/263a4/admin-archi.webp 480w,
/static/3218d2476faf1a5df2464788fe019259/0c8fb/admin-archi.webp 640w,
/static/3218d2476faf1a5df2464788fe019259/521f7/admin-archi.webp 860w,
/static/3218d2476faf1a5df2464788fe019259/ece56/admin-archi.webp 968w&quot;
              sizes=&quot;(max-width: 968px) 100vw, 968px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/3218d2476faf1a5df2464788fe019259/53918/admin-archi.png 360w,
/static/3218d2476faf1a5df2464788fe019259/9aebd/admin-archi.png 480w,
/static/3218d2476faf1a5df2464788fe019259/0f09e/admin-archi.png 640w,
/static/3218d2476faf1a5df2464788fe019259/3e3fe/admin-archi.png 860w,
/static/3218d2476faf1a5df2464788fe019259/2876f/admin-archi.png 968w&quot;
            sizes=&quot;(max-width: 968px) 100vw, 968px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/3218d2476faf1a5df2464788fe019259/2876f/admin-archi.png&quot;
            alt=&quot;Admin Frontend(이하 Admin FE), Admin Backend(이하 Admin BE), 게임 서버로 구성된 게임 Admin 아키텍처&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Admin Frontend(이하 Admin FE), Admin Backend(이하 Admin BE), 게임 서버로 구성된 게임 Admin 아키텍처&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;gRPC에 필요한 커스텀 헤더를 브라우저가 지원하지 않아 &lt;code&gt;게임 서버&lt;/code&gt;로 직접 gRPC 호출을 할 수 없기 때문에 &lt;code&gt;Admin BE&lt;/code&gt;를 프록시 서버로 두어야 합니다.&lt;sup&gt;&lt;a href=&quot;#otr1&quot; name=&quot;otrsrc1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt; code-gen으로 생성된 &lt;code&gt;.js&lt;/code&gt; 파일은 직렬화/역직렬화 로직으로 &lt;code&gt;Admin BE&lt;/code&gt;와 &lt;code&gt;게임 서버&lt;/code&gt; 간의 gRPC 통신에 사용되고, &lt;code&gt;Admin FE&lt;/code&gt;와 &lt;code&gt;Admin BE&lt;/code&gt; 간의 통신은 직접 HTTP endpoint를 작성해야 합니다.&lt;/p&gt;
&lt;p&gt;그런데 이 구조에서는 한가지 문제점이 있습니다. 대부분의 protobuf code-gen 라이브러리가 Node.js 런타임을 대상으로 하기에 FE ↔ BE 통신의 구현체는 code-gen을 활용하지 못하고, gRPC method가 추가될 때마다 REST API의 endpoint를 직접 추가해야 하는 번거로움이 있습니다. 즉, protobuf를 쓰는 이유가 퇴색되고 있던 것입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ApiClient&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;/* auto-generated */&lt;/span&gt;
	serviceA&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token function&quot;&gt;method1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
		&lt;span class=&quot;token function&quot;&gt;method2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
	serviceB&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token function&quot;&gt;method1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Admin BE&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; beClient&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ApiClient &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* auto-generated (생략) */&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

app&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;/grpc/serviceA/method1&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;req&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; res&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; beClient&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;serviceA&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;method1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;req&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;body&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;payload&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  res&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

app&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;/grpc/serviceA/method2&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;req&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; res&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; beClient&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;serviceA&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;method2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;req&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;body&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;payload&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  res&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

app&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;/grpc/serviceB/method1&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;req&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; res&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; beClient&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;serviceB&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;method1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;req&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;body&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;payload&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  res&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Admin FE&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; beClient&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ApiClient &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	serviceA&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token function-variable function&quot;&gt;method1&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;payload&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;/gprc/serviceA/method1&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; body&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; payload &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
		&lt;span class=&quot;token function-variable function&quot;&gt;method2&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;payload&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;/gprc/serviceA/method2&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; body&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; payload &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
	serviceB&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	  &lt;span class=&quot;token function-variable function&quot;&gt;method1&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;payload&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;/gprc/serviceB/method1&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; body&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; payload &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h1 id=&quot;idea&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#idea&quot; aria-label=&quot;idea permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Idea&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;오늘도 어김없이 개발자 김용쿠는 gRPC와 똑같은 REST API를 추가하다 문득 이런 생각이 들었습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;BE용 api client가 타입만 보면 브라우저에서도 사용할 수 있을 것 같은데,
저 타입처럼 동작하는 브라우저용 구현체를 &lt;strong&gt;자동&lt;/strong&gt;으로 만들 수 없을까?&lt;sup&gt;&lt;a href=&quot;#otr2&quot; name=&quot;otrsrc2&quot;&gt;[2]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;protobuf parser로 직접 FE용 code-gen을 만들어 이 문제를 해결할 수도 있습니다만, 이미 타입 정보가 존재하는데 각 endpoint와 1대1 대응하는 js 코드를 또 만들어야 할까요? 아래의 pseudo code를 상상하며 다음의 목표를 세웠습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ApiClient&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* auto-generated */&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Admin BE&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; beClient&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ApiClient &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* auto-generated */&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// single &amp;amp; unique BE endpoint&lt;/span&gt;
app&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;/grpc&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;req&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; res&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; paths &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; req&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;body&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;paths&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; payload &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; req&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;body&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;payload&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; rpc &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; paths&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;reduce&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;prev&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; prev&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; beClient&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;rpc&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;payload&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  res&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;body&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Admin FE&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; feClient&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ApiClient &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
feClient&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;serviceA&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;method1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;payload&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// fetch(&apos;/grpc&apos;, { body: JSON.stringify({ path: [&apos;serviceA&apos;, &apos;method1&apos;], payload }) })&lt;/span&gt;
feClient&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;serviceA&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;method2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;payload&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// fetch(&apos;/grpc&apos;, { body: JSON.stringify({ path: [&apos;serviceA&apos;, &apos;method2&apos;], payload }) })&lt;/span&gt;
feClient&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;serviceB&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;method1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;payload&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// fetch(&apos;/grpc&apos;, { body: JSON.stringify({ path: [&apos;serviceB&apos;, &apos;method1&apos;], payload }) })&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;JavaScript 구현체와 code-gen으로 생성된 타입을 합쳐, 기존에 BE에서 사용하던 모습 그대로 마치 FE에도 api client 존재하듯 개발자를 속이자&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feClient&lt;/code&gt; 구현체는 &lt;code&gt;beClient&lt;/code&gt; 타입 시그니처대로 동작해야 한다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feClient&lt;/code&gt;는 code-gen을 사용하지 않고 &lt;strong&gt;런타임에 동적으로&lt;/strong&gt; &lt;code&gt;beClient&lt;/code&gt;의 함수를 호출하도록 설계하자&lt;/li&gt;
&lt;/ol&gt;
&lt;h1 id=&quot;implementation&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#implementation&quot; aria-label=&quot;implementation permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Implementation&lt;/h1&gt;
&lt;p&gt;그런데 어떻게 code-gen 없이 &lt;code&gt;feClient&lt;/code&gt; 구현체를 만들 수 있을까요? 답은 &lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Proxy&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;JavaScript Proxy&lt;/a&gt;에서 찾을 수 있습니다.&lt;/p&gt;
&lt;p&gt;JavaScript의 Proxy는 Reflection과 함께 JavaScript의 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Meta_programming&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;meta-programming 기법&lt;/a&gt;에 사용하는 API로, 객체의 여러가지 기본 동작을 재정의 할 수 있습니다. 아래 코드는 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/get&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;get() handler&lt;/a&gt;와 두 번째 인자인 &lt;code&gt;property&lt;/code&gt;를 사용하여 &lt;strong&gt;객체의 프로퍼티 접근&lt;/strong&gt;을 재정의해, &lt;strong&gt;실제 프로퍼티 값&lt;/strong&gt; 대신 &lt;strong&gt;프로퍼티 접근 경로&lt;/strong&gt;를 반환하도록 &lt;strong&gt;기본 동작을 재정의&lt;/strong&gt; 한 예시입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; obj &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; proxy &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Proxy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;obj&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;_target&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; property&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;access: &apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; property&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;proxy&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;service&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// &quot;access: service&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;proxy.service&lt;/code&gt; 같은 1단계 자식에 대한 접근은 쉽게 구현할 수 있었는데요, &lt;code&gt;proxy.service.method&lt;/code&gt;와 같이 2단계 이상의 자식에 대한 접근은 어떻게 구현할 수 있을까요? 바로 재귀함수를 통해서 이를 쉽게 구현할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createProxy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;target&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; path &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Proxy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;target&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;_&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; property&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; newPath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; property&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;newPath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createProxy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;target&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; newPath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 재귀!!&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; obj &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; proxy &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createProxy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;obj&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
proxy&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;service&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;method&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// [&apos;service&apos;]&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// [&apos;service&apos;, &apos;method&apos;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이제 거의 다 왔습니다.&lt;/p&gt;
&lt;p&gt;지금까지는 프로퍼티 접근에 대해서 다루었는데요, 어떻게 하면 &lt;code&gt;proxy.service.method&lt;/code&gt; 호출시 실제 &lt;code&gt;rpc&lt;/code&gt;가 호출되게 할 수 있을까요? 이를 위해서 Proxy API 중 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/apply&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;apply() handler&lt;/a&gt;를 사용해 &lt;strong&gt;함수의 호출&lt;/strong&gt;을 재정의 해보겠습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;rpc&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;path&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; payload&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;.&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; payload&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createProxy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;target&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; path &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Proxy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;target&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;_target&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; property&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createProxy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;target&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; property&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;_target&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; _thisArg&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; argumentsList&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; argumentsList&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 실제 rpc 호출&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; proxy &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createProxy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;rpc&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

proxy&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;service&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;payload&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// &quot;service.method&quot;, &quot;payload&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;마치 실제로 &lt;code&gt;proxy.service&lt;/code&gt; 라는 객체 안에 &lt;code&gt;method&lt;/code&gt;라는 함수가 있는것처럼 보이지 않나요?&lt;/p&gt;
&lt;p&gt;이렇게 저희는 실제로는 &lt;code&gt;rpc&lt;/code&gt;라는 하나의 함수로, 개발자에게는 &lt;code&gt;proxy.service.method&lt;/code&gt; 라는 api 인스턴스가 있는 것 같은 개발 경험을 선사할 수 있었습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;callGrpc&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; payload&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;fetch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;/grpc&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; body&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; payload &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token function-variable function&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; payload&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  path&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;any&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Proxy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;target&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;_target&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; property&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;target&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; property&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;apply&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;_target&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; _thisArg&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; argumentsList&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; argumentsList&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 실제 rpc 호출&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; feClient&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ApiClient &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createClient&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;callGrpc&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

feClient&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;serviceA&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;method1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;payload&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// OK&lt;/span&gt;
feClient&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;serviceA&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;method2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;payload&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// OK&lt;/span&gt;
feClient&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;serviceA&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;method3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;payload&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// type error&lt;/span&gt;
feClient&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;serviceB&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;method1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;payload&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// OK&lt;/span&gt;
feClient&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;serviceB&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;method2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;payload&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// type error&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h1 id=&quot;furthermore&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#furthermore&quot; aria-label=&quot;furthermore permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Furthermore&lt;/h1&gt;
&lt;p&gt;지금까지 동적으로 동작하는 웹 gRPC 클라이언트에 대해 알아보았습니다. 이번 단락에서는 위의 구현체를 활용하여 프록시 서버를 최적화하는 방법을 간단히 소개합니다.&lt;/p&gt;
&lt;p&gt;grpc-node에서 제공하는 code-gen 예제를 보면 &lt;a href=&quot;https://github.com/grpc/grpc-node/blob/%40grpc/grpc-js%401.9.0/examples/protos/route_guide.proto&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;간단한 스키마&lt;/a&gt;에 대해서도 &lt;a href=&quot;https://github.com/grpc/grpc-node/blob/%40grpc/grpc-js%401.9.0/examples/routeguide/static_codegen/route_guide_pb.js&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;수백 줄의 코드&lt;/a&gt;가 생성되는 것을 확인 할 수 있는데요. 게임이 출시되고 시간이 흐름에 따라 게임이 갖는 스키마는 굉장히 거대하고 복잡해집니다. 그리고 code-gen으로 생성한 코드의 양도 비례해서 많아지죠. 무려 코드가 너무 무거워서 &lt;code&gt;Admin BE&lt;/code&gt;의 부팅시간에 영향이 갈 정도가 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=ff4fgQxPaO0&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Faster apps with JSON.parse (Chrome Dev Summit 2019)&lt;/a&gt; 발표에서 최적화 방안을 찾았습니다. code-gen의 결과물이 꼭 JavaScript일 필요는 없습니다. 직렬화/역직렬화 로직을 표현하는 JSON을 생성하고 이 JSON을 기반으로 동작하는 함수 하나만 있다면 같은 방법으로 성능을 올릴 수 있겠다고 판단했습니다.&lt;sup&gt;&lt;a href=&quot;#otr3&quot; name=&quot;otrsrc3&quot;&gt;[3]&lt;/a&gt;&lt;/sup&gt; 그리고 앞에서 작성한 Proxy 객체를 재사용하면 기존의 code-gen으로 생성된 코드와 동일한 타입과 동작을 갖는 함수가 완성됩니다.&lt;/p&gt;
&lt;h1 id=&quot;conclusion&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#conclusion&quot; aria-label=&quot;conclusion permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;이 방법으로 쿠키런: 킹덤 운영툴은 FE와 BE를 합쳐 약 100만 줄의 JavaScript 코드를 삭제했고, 개발자는 endpoint를 작성하는 번거로운 작업에서 벗어났습니다. 혹자는 너무 안전하지 못한 코드가 아니냐고 할 수 있겠지만 TypeScript가 컴파일타임을, meta-data가 런타임을 지켜주는 상황에서 JavaScript스럽게 문제를 해결했다는 말로 마무리합니다.&lt;/p&gt;
&lt;h1 id=&quot;off-the-record&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#off-the-record&quot; aria-label=&quot;off the record permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Off the Record&lt;/h1&gt;
&lt;p&gt;&lt;a name=&quot;otr1&quot; href=&quot;#otrsrc1&quot;&gt;[1]&lt;/a&gt; &lt;a href=&quot;https://github.com/grpc-ecosystem/grpc-gateway&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;grpc-gateway&lt;/a&gt;를 사용할 수도 있지만 &lt;a href=&quot;https://nodejs.org/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Node.js&lt;/a&gt;를 지원하지 않습니다. 이미 &lt;a href=&quot;https://nextjs.org/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Next.js&lt;/a&gt;를 사용하고 있어서 고려하지 않았습니다.&lt;/p&gt;
&lt;p&gt;&lt;a name=&quot;otr2&quot; href=&quot;#otrsrc2&quot;&gt;[2]&lt;/a&gt; &lt;a href=&quot;https://trpc.io&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;tRPC&lt;/a&gt;에서 아이디어를 얻었습니다.&lt;/p&gt;
&lt;p&gt;&lt;a name=&quot;otr3&quot; href=&quot;#otrsrc3&quot;&gt;[3]&lt;/a&gt; &lt;a href=&quot;https://github.com/protobufjs/protobuf.js&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;protobufjs&lt;/a&gt;는 런타임에 동적으로 proto → js 변환을 가능하게 해주는 reflection 기능을 제공합니다. 이를 활용하여 쉽게 구현할 수 있었습니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[Datadog Live with Devsisters 돌아보기]]></title><description><![CDATA[데브시스터즈의 데이터독 사용 노하우를 공유하였던 행사를 소개합니다.]]></description><link>https://tech.devsisters.com/posts/datadog-live-talk</link><guid isPermaLink="false">https://tech.devsisters.com/posts/datadog-live-talk</guid><pubDate>Thu, 29 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요. 데브시스터즈 기술본부에서 조직문화, 개발문화를 가꾸고 있는 전경아입니다.&lt;/p&gt;
&lt;p&gt;데브시스터즈에서는 오랜 기간동안 Datadog(이하 데이터독)을 모니터링 서비스로 사용하고 있는데요. 지난 2024년 6월에 데브시스터즈에서 축적해온 데이터독의 사용 노하우를 다양한 분들께 공유해드리고자 개발자 분들을 초청하여 &lt;a href=&quot;https://www.datadoghq.com/ko/event/datadoglive-devsisters-od/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Datadog Live with Devsisters 행사&lt;/a&gt;를 진행하였습니다.&lt;/p&gt;
&lt;p&gt;데이터독 담당자 분들과 함께 준비, 진행하였던 행사의 영상은 &lt;a href=&quot;https://www.youtube.com/watch?v=hPYlMRXYfwY&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Datadog Korea 유튜브&lt;/a&gt;에 업로드되어 있으며, 본문에도 첨부해드립니다.&lt;/p&gt;
&lt;iframe width=&quot;560&quot; height=&quot;315&quot; style=&quot;width: 100%; height: 100%; aspect-ratio: 16/9;&quot; src=&quot;https://www.youtube.com/embed/hPYlMRXYfwY?si=8gqhMCDrC-45dz8e&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; referrerpolicy=&quot;strict-origin-when-cross-origin&quot; allowfullscreen&gt;&lt;/iframe&gt;
&lt;p&gt;이번 글에서는 전반적인 행사 내용과 소감과 함께, 행사에서 다루었던 내용을 요약해보고자 합니다.&lt;/p&gt;
&lt;h2 id=&quot;행사-소개&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%96%89%EC%82%AC-%EC%86%8C%EA%B0%9C&quot; aria-label=&quot;행사 소개 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;행사 소개&lt;/h2&gt;
&lt;p&gt;Datadog Live with Devsisters은 데브시스터즈와 데이터독이 함께 준비한 라이브 톡 행사입니다. 데브시스터즈에서 과거에 데이터독을 왜 도입하였는지와 지금까지 어떻게 이용하고 있는지 실제 사례를 공유하고 Q&amp;#x26;A 시간을 가지며 참가하신 분들께 데이터독에 대한 이해를 높여드리는 것이 목적이었습니다.&lt;/p&gt;
&lt;p&gt;패널톡 형태로 게임, 인프라, 데이터, 플랫폼 팀을 리드하고 계신 4분의 엔지니어를 모시고 진행했는데요, 게임업계 담당자뿐만 아니라 다양한 산업군에서 많은 분들이 모여주셔서 저희의 이야기를 더욱 열정적으로 전달할 수 있었던 귀중한 시간이었습니다.
&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2000px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 63.055555555555564%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAABYlAAAWJQFJUiTwAAADqklEQVR42hWRaUzTBwDF/5+3ZMIcjqHDySESB6zSFpCMu6ANR4UiIDe0goSW+ygtR/+UttBylVNuAWVF50LmRvBYNGNumYvJshn9MKcmyzAx2WY0LjHLfus+vLz36eUdQqOuivYOA21N8+jPTFNbbaFB34O+toOmJhOtLW3U6xuo0lRTXlJK2ak8DLoSLO3VNJ0uwd5Wj9VQj6GumjptKYKhWUdPj0hv58+Yu17SbfwLU8tTjK33MTZ9h7H5mgduOpqHMbaJdDc3MNuqQSw/QWVODsPGGmZ7a1gQNbhMWoROQx2i5X/DO4hdzzF3v6C3x8Ndf2Mx/IutDWyGf7C3v2bQ/BiXuZXbtTGMZkWQFODHulHJo0+K2BaluA0pCKKpCVu/SJ8nodW8g915y6NfMDT0K3bzn1h6nmEzPcfR+YrJgWecdVq4oYtjUi2lVB7CZ93Z/H75NF/3xXLZnIlgFw04h63YxZ+YmLzH6uYcg67fcH97nYmVJ4xduMHYwgMm3d+wsPiQ8xMurmplTHkM245FsiHm88d1Iz9OZ7HpzENwWE0sLc8wN/WLp+5DROstBm2vcA7+gFV8wvjcYyYnd5hbvc/KskcPDfNlVQzDGeFYVHI+N6XzYLmCzY5oFhsTEEYHe9E16DxPXkSb56AsrZJRJ7QXD9FeeBbXyEvGB54y5dxhdW4HfZ6eC/mR9GXFMlaUwFTNcYrSjjI+PU9rcyPChGf1sviPUCVp0JW2YNY66TWCMllLxfFMhmz3mB59zbjjDjPjn+KyDGA4pSR0rz/9J49Sm5FI4P4IHGtbTKxvIGTHJKBXZaApMLDrbR/kUhW6kg0+OCBhn28A+jIzl2a/59zIFFXFefRZ+xjotyGRfkyVKo1KRTQH/QOIS0jhmDIL4dD7MtRpmWjUCfj7+pOamMTF0RqOhMmQSBJZ6K/h7hdWts6188ab3iTEJlGgykUukRAcGESsTEZsVCyBvnvw3uWFcDIqBmu5Gr1aRWqknMJUBRXqAkJCwjmwP4TDBz9kaaSF2+u9KMKCSYkKxc93H7vfeY+3vHyQSmXkFpfTXKNFKYtACHk3mHS5jPLkVCKCD3tq+uHl5YciPhplYgy7vfew5Xbw6O4aGwsONi7NUp6dTnzQXhKjZZxfWSQ3P5+JMRdT3TqE6oJsNIUnCAo4hCQsHLUyGV8fX66t2SnLURAXI+PmlUWWl1y41xfZ3r7KjKuP5NAAFNIjaDRn6Gqpwz0/xM2vrvAflNtiY5BEFekAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/c33f3eb7416500ccfd4995a5f0112bdb/33ecc/datadoglive_1.webp 360w,
/static/c33f3eb7416500ccfd4995a5f0112bdb/263a4/datadoglive_1.webp 480w,
/static/c33f3eb7416500ccfd4995a5f0112bdb/0c8fb/datadoglive_1.webp 640w,
/static/c33f3eb7416500ccfd4995a5f0112bdb/521f7/datadoglive_1.webp 860w,
/static/c33f3eb7416500ccfd4995a5f0112bdb/cbd37/datadoglive_1.webp 1200w,
/static/c33f3eb7416500ccfd4995a5f0112bdb/fab06/datadoglive_1.webp 2000w&quot;
              sizes=&quot;(max-width: 2000px) 100vw, 2000px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/c33f3eb7416500ccfd4995a5f0112bdb/53918/datadoglive_1.png 360w,
/static/c33f3eb7416500ccfd4995a5f0112bdb/9aebd/datadoglive_1.png 480w,
/static/c33f3eb7416500ccfd4995a5f0112bdb/0f09e/datadoglive_1.png 640w,
/static/c33f3eb7416500ccfd4995a5f0112bdb/3e3fe/datadoglive_1.png 860w,
/static/c33f3eb7416500ccfd4995a5f0112bdb/64756/datadoglive_1.png 1200w,
/static/c33f3eb7416500ccfd4995a5f0112bdb/f97d7/datadoglive_1.png 2000w&quot;
            sizes=&quot;(max-width: 2000px) 100vw, 2000px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/c33f3eb7416500ccfd4995a5f0112bdb/f97d7/datadoglive_1.png&quot;
            alt=&quot;발표에 앞서 인사를 드리고 있는 데브시스터즈 동료분들&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;발표에 앞서 인사를 드리고 있는 데브시스터즈 동료분들&lt;/figcaption&gt;
  &lt;/figure&gt;
&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2000px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 48.888888888888886%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAABYlAAAWJQFJUiTwAAAC4klEQVR42g3SXU9TBwCA4fMPXLxaiBsRUmmhIDAFrHUFYQqlmVAqcEY/obRrwdJSKriWWu0otqcCBY7EMygIUROdeLFki/tIlsVkRjNNXLKxZdnFNjF+ZMzdbVnyrj/gfa5eIT4+zNBAD++dbOOUx0bQ52fEF8LjCuC2j+B3JDnj2STVv0XW+Sdz/X8he/7m8tgTMqmLxGIRvr0ywG+f2Hmw7kBInRsjOj5SwPrwOs1Yu1sZsHUSCZ4hFfkOKfSUrPcVkmuHOfcrFk9vo5x9yUpsm7mZHFJ6EiXRwXK8la8v9yJM+MRC6GLMZ8EuvkvQb2U87CCdVMjFt1jM/Mil7K/k138gLz9H+fQuq7d/Qpn8nbXVFRZyFzDq6nizqIhUsA1h/EQ9C34DZ0dFwuEgk5MJErE485nrfCTfZeXKQ/Krv+APLjE1cY+bt1+ydPERZr2VrDTNipLjgt1IoPMIX655ENKDBnIBPVZrHJO4SUffZww67zAzsczqzFOU5BPWUju4moeJuT9m8xKsn/+ZiLmT9FSCjXyGqbCF/dr9zMZtCIM9h4gOtqA72IXJFODAWx1U1TrxuqPIiS3mT++wlv4XeXoDOS2zPvs90cASquIa2o3tZKejVFZU8PqeUpr1BxGGxSbslhPUaapoPKSnUaejZG8t77scfL6R55Z8lWvyLWalHEtSmmVJwmLpZNdrb3Cs8Qg28SS7dhdRXFJGdWUVgtnYRJlag6p0H8XFKtSqcrQVh5FiHv7bvsEXNz4k6LIWFpkiNnqKx3cUzodDWJqPsZgM0ddtorrQa7WVaMoLoOftBuyGBnos7TQb9KhUaj4YHeLBVwr/vLjPkNdPfX0LYm8IjaoVU6MRseUoWnUDPleYsMPMuYEuDlTXoKmqQ+iv2ce820ImGSEZHcFpE5ldkPjm0X2ePf+DRVnmaNNxuk1eHF1pDhuOo9ZU09vbTcTnoqftHfaWlrGnpJxaXQv/A0E60DKcLvu7AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/5945e56f5a366e12e05e39c7d3ef0af8/33ecc/datadoglive_2.webp 360w,
/static/5945e56f5a366e12e05e39c7d3ef0af8/263a4/datadoglive_2.webp 480w,
/static/5945e56f5a366e12e05e39c7d3ef0af8/0c8fb/datadoglive_2.webp 640w,
/static/5945e56f5a366e12e05e39c7d3ef0af8/521f7/datadoglive_2.webp 860w,
/static/5945e56f5a366e12e05e39c7d3ef0af8/cbd37/datadoglive_2.webp 1200w,
/static/5945e56f5a366e12e05e39c7d3ef0af8/fab06/datadoglive_2.webp 2000w&quot;
              sizes=&quot;(max-width: 2000px) 100vw, 2000px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/5945e56f5a366e12e05e39c7d3ef0af8/53918/datadoglive_2.png 360w,
/static/5945e56f5a366e12e05e39c7d3ef0af8/9aebd/datadoglive_2.png 480w,
/static/5945e56f5a366e12e05e39c7d3ef0af8/0f09e/datadoglive_2.png 640w,
/static/5945e56f5a366e12e05e39c7d3ef0af8/3e3fe/datadoglive_2.png 860w,
/static/5945e56f5a366e12e05e39c7d3ef0af8/64756/datadoglive_2.png 1200w,
/static/5945e56f5a366e12e05e39c7d3ef0af8/f97d7/datadoglive_2.png 2000w&quot;
            sizes=&quot;(max-width: 2000px) 100vw, 2000px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/5945e56f5a366e12e05e39c7d3ef0af8/f97d7/datadoglive_2.png&quot;
            alt=&quot;발표에 앞서 인사를 드리고 있는 데브시스터즈 동료분들&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;발표에 앞서 인사를 드리고 있는 데브시스터즈 동료분들&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h2 id=&quot;데브시스터즈의-데이터독-도입-배경&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8D%B0%EB%B8%8C%EC%8B%9C%EC%8A%A4%ED%84%B0%EC%A6%88%EC%9D%98-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%8F%85-%EB%8F%84%EC%9E%85-%EB%B0%B0%EA%B2%BD&quot; aria-label=&quot;데브시스터즈의 데이터독 도입 배경 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;데브시스터즈의 데이터독 도입 배경&lt;/h2&gt;
&lt;p&gt;데브시스터즈는 AWS의 한국 리전이 출시되기 훨씬 이전인 2011년부터 AWS 클라우드를 활용하여 서비스를 운영해왔습니다. 2013년에 출시한 &lt;a href=&quot;https://aws.amazon.com/ko/solutions/case-studies/devsisters/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;쿠키런 For Kakao는 AWS 클라우드를 적극적으로 활용&lt;/a&gt;하여 적은 인력으로도 수백만 명의 유저분들에게 안정적으로 서비스를 제공할 수 있게 되었지만, 모니터링 측면에서는 여전히 보완할 필요가 있는 부분들이 있었습니다. 예를 들어, 당시 사용하던 Zabbix, CloudWatch, StatsD, Graphite 등의 기존 모니터링 도구들은 설정과 유지보수를 위해 전문적인 지식과 경험이 필요했고, Stateful 한 특성 때문에 트래픽 변동성이 큰 클라우드 환경에서 효율적인 관리가 어려웠습니다. 또한, 여러 애플리케이션을 운영하는 과정에서 각 애플리케이션에 맞춘 메트릭을 세밀하게 분석하고 수집하기에 지나치게 복잡했습니다.&lt;/p&gt;
&lt;p&gt;이와 같은 고민을 하며 차기작 출시를 준비하다보니, 서비스 안정성 면에서 모니터링 스택에 대한 니즈는 커져갔습니다. 그러한 상황에서, 승용님께서 참석했던 컨퍼런스에서 우연한 기회로 &lt;a href=&quot;https://www.youtube.com/watch?v=mG4ZpEhRKHA&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;데이터독 한국인 엔지니어께서 진행하신 세션&lt;/a&gt;을 듣게 되었고, anomaly 등의 다양한 기능이 매력적이고 안정적이라고 느껴 빠르게 차기작의 모니터링 스택으로 도입하게 되었습니다. 결과적으로, 2016년 쿠키런: 오븐브레이크가 출시되었을 때, 수 만명의 유저가 동시에 회원가입을 하는 과정에 발생했던 병목을 감지하여 빠르게 해결하는 등 많은 도움을 받을 수 있었습니다.&lt;/p&gt;
&lt;p&gt;데브시스터즈에서의 데이터독 주요 사용 사례는 아래 자료들에서 상세하게 확인하실 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.datadoghq.com/case-studies/devsisters/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Datadog Case Study: Taking monitoring to the next level&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=CacvKq68cnc&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Datadog Observability Day 2023: Devsisters 와 함께하는 쿠키런 세계의 모니터링 노하우&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이후로도 지속적으로 데이터독을 사용하며 아직도 다양한 장점을 체감하고 있습니다. 데브시스터즈에서 체감하는 주요 효용으로는, APM과 같이 니즈에 알맞게 필요한 모니터링 스택을 제공하고 있다는 점, 신규 Integration 요청에 빠르게 대응해주어 사용자로서 편리하다는 점, 그리고 무엇보다 모니터링 플랫폼으로써 핵심 기능과 사용성이 한결 같이 잘 유지되고 있다는 점을 꼽을 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;팀-별-데이터독-활용-사례&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%8C%80-%EB%B3%84-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%8F%85-%ED%99%9C%EC%9A%A9-%EC%82%AC%EB%A1%80&quot; aria-label=&quot;팀 별 데이터독 활용 사례 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;팀 별 데이터독 활용 사례&lt;/h2&gt;
&lt;p&gt;다음으로는 게임 개발팀, 게임 플랫폼 팀, 데브옵스 팀에서 데이터독을 어떻게 활용하고 계신지 사례를 공유해주셨습니다. 내용을 간략히 요약하면 다음과 같습니다.&lt;/p&gt;
&lt;h3 id=&quot;1-게임-개발팀&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EA%B2%8C%EC%9E%84-%EA%B0%9C%EB%B0%9C%ED%8C%80&quot; aria-label=&quot;1 게임 개발팀 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;1. 게임 개발팀&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;실시간 지표 모니터링&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;RPS 등의 시스템 지표를 모니터링하여 서버가 얼마만큼의 부하를 받고 어떤 상태에 있는지를 실시간으로 확인하여 트래픽 변동 상황에서 서버의 안정성을 유지합니다.&lt;/li&gt;
&lt;li&gt;매출, 플레이 통계 등 게임 내 주요 지표를 커스텀 메트릭으로 설정하고 실시간으로 모니터링하여 매출 변동이나 플레이 패턴 변화를 빠르게 파악하고 대응할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;장애 상황 대응&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;장애 상황 모니터를 통해 특정 조건(ex. Go 서버에서 panic이 발생)이 일어나면 알람이 울리도록 Custom Metric과 조합하여 설정하여 빠르게 장애를 인지하고 신속히 대응할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;프로파일링을 통한 서버 성능 개선&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;APM을 통한 주기적으로 성능 분석을 시도하면서, 신규 기능 도입을 통한 성능 문제를 파악하거나 개발 단계의 실수 등을 확인합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;2-플랫폼-팀&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%ED%94%8C%EB%9E%AB%ED%8F%BC-%ED%8C%80&quot; aria-label=&quot;2 플랫폼 팀 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;2. 플랫폼 팀&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;서비스 맵을 통한 MSA 모니터링&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;서비스 간의 의존성을 시각화하여 각 서비스가 어떻게 상호작용하는지 파악합니다. 이를 통해 복잡한 서비스 구조에서도 문제를 쉽게 추적할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;데이터독 기능을 사용한 모니터링&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.datadoghq.com/product/apm/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;APM&lt;/a&gt;, &lt;a href=&quot;https://www.datadoghq.com/product/synthetic-monitoring/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Synthetic Monitoring&lt;/a&gt;, &lt;a href=&quot;https://docs.datadoghq.com/developers/service_checks/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Service Check&lt;/a&gt; 등을 통해 서비스 상태를 확인하고 관리합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;서드파티 API 모니터링&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;Google 혹은 Apple 로그인과 같은 외부 API에서 문제, 지연 등이 발생하고 있는지 인지하고 대응하기 위해 데이터독 모니터를 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;3-데브옵스-팀&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%EB%8D%B0%EB%B8%8C%EC%98%B5%EC%8A%A4-%ED%8C%80&quot; aria-label=&quot;3 데브옵스 팀 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;3. 데브옵스 팀&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;기본적인 리소스 사용량 및 이벤트 모니터링&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;CPU, 메모리 사용률, 네트워크 트래픽 등을 모니터링하여 서버 상태를 파악하고, 리소스를 효율적으로 사용하고 있는지도 확인하여 비용을 절감하는 시도로도 이어지고 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;쿠버네티스 환경 모니터링&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;쿠버네티스 클러스터에서 발생하는 다양한 이벤트를 모니터링하여 문제 발생 시 빠르게 파악합니다. 또한 Pod가 할당되지 않는다거나 반복적으로 재시작하는 등 단순한 지표가 아니라 현상도 확인할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SLO 관리&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.datadoghq.com/service_management/service_level_objectives/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;데이터독 SLO&lt;/a&gt;를 통해 에러율, 지연 시간, 서비스 상태 등을 관리 및 확인하고 알람을 받고 있습니다.&lt;/li&gt;
&lt;li&gt;유저에게 서비스를 제공하는 입장에서 SLO는 무조건 높여야 하지만, SLO를 높히는 데에는 많은 비용이 들어갑니다. 이같은 기술적 어려움에 대해 담당 부서에 충분히 공유하여 이해를 구하고, 합리적인 게임 서비스 안정성을 협의하는 과정을 거치고 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;인프라-관리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9D%B8%ED%94%84%EB%9D%BC-%EA%B4%80%EB%A6%AC&quot; aria-label=&quot;인프라 관리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;인프라 관리&lt;/h2&gt;
&lt;p&gt;다음으로는 데이터독과는 크게 관련이 없을 수 있지만, 행사를 준비하며 사전에 수집하였던 질문 중 쿠버네티스와 인프라 관리에 대한 질문이 많았기 때문에 데브시스터즈에서는 인프라 관리를 어떤 방식으로 하고 있는지 데브옵스 팀에서 소개해주셨습니다.&lt;/p&gt;
&lt;p&gt;쿠버네티스를 관리하는 조직이라면 모두 클러스터를 배포, 관리하는 방식이 있으실텐데요.
데브시스터즈에서 과거에는 하나의 클러스터에 여러 개의 게임, 어플리케이션을 배포하는 형태로 사용하고 있었습니다. 그러나 이러한 패턴에서는 구조적으로 특정 어플리케이션에서 발생한 장애가 인프라 혹은 클러스터 단으로 번져 다른 어플리케이션과 게임으로 전파될 위험성이 있고, 또 각 게임에서 적용하고 싶은 설정이 달라 관리해야할 설정셋이 점점 늘어나는 등의 어려움이 있었습니다. 그렇기에 데브시스터즈에서는 이제 각 게임마다 각각의 클러스터를 사용하는 패턴으로 변경하였습니다.&lt;/p&gt;
&lt;p&gt;데브시스터즈에서 배포를 필요로 하는 모든 인프라는 코드로 관리될 것을 원칙으로 하고 있으며, 이는 쿠버네티스 클러스터 또한 예외가 아닙니다. 각 개발팀에서 필요로 하는 쿠버네티스 클러스터는 기본적으로 테라폼 코드를 통해 관리하고 있는데, 쿠버네티스를 분리하면서 클러스터와 애드온들을 클러스터 별로 개별 설치하고 관리하는 과도기가 있었습니다. 짐작하시다시피 설치 방법과 버전 별로 필요한 작업들이 달라지기 때문에 클러스터 수가 많아질수록 관리 상 어려움이 크게 발생하였기 때문에, 지금은 대부분의 클러스터를 직접 작성한 EKS 테라폼 모듈을 통해 관리하고 있습니다. 이 모듈에는 데이터독 등의 애드온부터 배포를 위한 도구 등까지, 기본적으로 데브시스터즈에서 EKS를 사용하는 패턴에서 필요한 것들을 직접 설정하여 사용할 수 있게 구현해두었습니다. 설정을 명시하지 않을 경우 미리 설정해둔 기본값이 자동으로 적용 및 업데이트 되기 때문에, 결과적으로 데브옵스 팀에서 축적한 경험이 적은 노력으로 전파되는 구조가 되었습니다.&lt;br&gt;
&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1279px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 56.111111111111114%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAIBAwX/xAAWAQEBAQAAAAAAAAAAAAAAAAAAAQL/2gAMAwEAAhADEAAAAdwaCsY1P//EABgQAAMBAQAAAAAAAAAAAAAAAAABERIg/9oACAEBAAEFAoRGVx//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/AT//xAAVEQEBAAAAAAAAAAAAAAAAAAAAEf/aAAgBAgEBPwFX/8QAFxAAAwEAAAAAAAAAAAAAAAAAARAgMf/aAAgBAQAGPwJYI//EABgQAQEAAwAAAAAAAAAAAAAAAAABEDHx/9oACAEBAAE/IbDU5y4j/9oADAMBAAIAAwAAABCw/wD/xAAWEQEBAQAAAAAAAAAAAAAAAAAAEQH/2gAIAQMBAT8Qq6//xAAVEQEBAAAAAAAAAAAAAAAAAAAAEf/aAAgBAgEBPxBT/8QAGxABAAMAAwEAAAAAAAAAAAAAAQARMSFBYVH/2gAIAQEAAT8Qu5r9ZZYN9Yo2oviAUmBXRxEpzP/Z&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/599436c221ebe651c3dc7692148e203d/33ecc/datadoglive_3.webp 360w,
/static/599436c221ebe651c3dc7692148e203d/263a4/datadoglive_3.webp 480w,
/static/599436c221ebe651c3dc7692148e203d/0c8fb/datadoglive_3.webp 640w,
/static/599436c221ebe651c3dc7692148e203d/521f7/datadoglive_3.webp 860w,
/static/599436c221ebe651c3dc7692148e203d/cbd37/datadoglive_3.webp 1200w,
/static/599436c221ebe651c3dc7692148e203d/3943f/datadoglive_3.webp 1279w&quot;
              sizes=&quot;(max-width: 1279px) 100vw, 1279px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/599436c221ebe651c3dc7692148e203d/543cd/datadoglive_3.jpg 360w,
/static/599436c221ebe651c3dc7692148e203d/a3e66/datadoglive_3.jpg 480w,
/static/599436c221ebe651c3dc7692148e203d/56d4e/datadoglive_3.jpg 640w,
/static/599436c221ebe651c3dc7692148e203d/2f567/datadoglive_3.jpg 860w,
/static/599436c221ebe651c3dc7692148e203d/03ffe/datadoglive_3.jpg 1200w,
/static/599436c221ebe651c3dc7692148e203d/29ab6/datadoglive_3.jpg 1279w&quot;
            sizes=&quot;(max-width: 1279px) 100vw, 1279px&quot;
            type=&quot;image/jpeg&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/599436c221ebe651c3dc7692148e203d/29ab6/datadoglive_3.jpg&quot;
            alt=&quot;데브시스터즈에서 사용중인 쿠버네티스 테라폼&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;데브시스터즈에서 사용중인 쿠버네티스 테라폼&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;br&gt;
&lt;br/&gt;
또한 개발팀에서 개발을 진행하게 되면서 여러가지 환경의 QA가 필요하게 되었습니다. 그런 상황에서 새로운 환경의 서버를 쉽게 배포할 수 있는 CD 툴도 내부적으로 개발하여 사용하고 있습니다. (다소 갑작스럽게 CD툴에 대한 이야기가 나오지만, 관련해서 인프라셀에서 다른 기술블로그 글로 소개를 준비하고 있다고 합니다.)&lt;br&gt;
&lt;br/&gt;
&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1279px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 56.111111111111114%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAIDBf/EABUBAQEAAAAAAAAAAAAAAAAAAAAB/9oADAMBAAIQAxAAAAHuKGalf//EABgQAAIDAAAAAAAAAAAAAAAAAAARARIg/9oACAEBAAEFAkIrGP/EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8BP//EABYRAQEBAAAAAAAAAAAAAAAAAAABEf/aAAgBAgEBPwFtf//EABcQAAMBAAAAAAAAAAAAAAAAAAEQIDH/2gAIAQEABj8CWCP/xAAaEAACAgMAAAAAAAAAAAAAAAAAEQExECHB/9oACAEBAAE/Ib2K9kv5ZZ//2gAMAwEAAgADAAAAECMv/8QAFREBAQAAAAAAAAAAAAAAAAAAABH/2gAIAQMBAT8Qqv/EABYRAQEBAAAAAAAAAAAAAAAAAAABUf/aAAgBAgEBPxBNH//EABoQAQACAwEAAAAAAAAAAAAAAAEAESExYUH/2gAIAQEAAT8Qq5K3MBVNdY+2R4juPsE7Z//Z&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/57553a1cebe660770f87351bf6853397/33ecc/datadoglive_4.webp 360w,
/static/57553a1cebe660770f87351bf6853397/263a4/datadoglive_4.webp 480w,
/static/57553a1cebe660770f87351bf6853397/0c8fb/datadoglive_4.webp 640w,
/static/57553a1cebe660770f87351bf6853397/521f7/datadoglive_4.webp 860w,
/static/57553a1cebe660770f87351bf6853397/cbd37/datadoglive_4.webp 1200w,
/static/57553a1cebe660770f87351bf6853397/3943f/datadoglive_4.webp 1279w&quot;
              sizes=&quot;(max-width: 1279px) 100vw, 1279px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/57553a1cebe660770f87351bf6853397/543cd/datadoglive_4.jpg 360w,
/static/57553a1cebe660770f87351bf6853397/a3e66/datadoglive_4.jpg 480w,
/static/57553a1cebe660770f87351bf6853397/56d4e/datadoglive_4.jpg 640w,
/static/57553a1cebe660770f87351bf6853397/2f567/datadoglive_4.jpg 860w,
/static/57553a1cebe660770f87351bf6853397/03ffe/datadoglive_4.jpg 1200w,
/static/57553a1cebe660770f87351bf6853397/29ab6/datadoglive_4.jpg 1279w&quot;
            sizes=&quot;(max-width: 1279px) 100vw, 1279px&quot;
            type=&quot;image/jpeg&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/57553a1cebe660770f87351bf6853397/29ab6/datadoglive_4.jpg&quot;
            alt=&quot;자체 개발하여 사용하고 있는 CD도구 화면&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;자체 개발하여 사용하고 있는 CD도구 화면&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h2 id=&quot;부하테스트-및-최적화&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B6%80%ED%95%98%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%B0%8F-%EC%B5%9C%EC%A0%81%ED%99%94&quot; aria-label=&quot;부하테스트 및 최적화 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;부하테스트 및 최적화&lt;/h2&gt;
&lt;p&gt;여러 게임들을 출시해온 데브시스터즈가 부하테스트에 대해 가진 노하우에 대한 질문을 주신 분도 있었습니다. 마침 행사 당시에 쿠키런: 모험의 탑 런칭을 준비하고 있었기 때문에 비교적 생동감 있는 답변을 드릴 수 있었습니다.&lt;/p&gt;
&lt;p&gt;런칭 전 부하테스트는 목표 부하를 수용하기 위해 필요한 인프라 규모에 대한 가설을 세우고 검증하는 과정의 반복이기 때문에, RPS, 특정 컴포넌트의 서버 대수 등 다양한 설정을 변인으로 두고 성능에 발생하는 변화를 측정하는 방식으로 진행됩니다. 변인에 따라 여러 가지 시나리오를 준비하고, 이에 대한 지표와 결과를 분석하여 서버의 한계점과 적정점을 찾아가는 과정을 거칩니다. 보통 데브옵스 팀에서 테스트를 위한 격리된 인프라를 준비해서 제공하고, 게임 개발팀에서 이를 사용하여 테스트를 진행하며, 결과에 대해서는 서로 같이 논의하며 개선해나가는 사이클을 거칩니다. 부하 테스트 과정에서도 Datadog APM을 사용하여 어떤 API가 어떤 지점에서 문제를 일으키는지를 flame graph 등을 통해 손쉽게 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;물론 API에 따른 성능 최적화는 게임 서비스 중에도 진행이 필요합니다. 라이브 서비스 중에는 단순히 한 시점의 지표만 확인하는 것이 아니라, 데이터독 메트릭의 &lt;code&gt;calendar_shift&lt;/code&gt; 함수를 활용하여 지난 주 등 과거 시점의 지표와도 비교하면 서비스에 어떤 변동이 있어서 새로운 상황이 발생하는지 모니터링할 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;장애-알람-수신&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9E%A5%EC%95%A0-%EC%95%8C%EB%9E%8C-%EC%88%98%EC%8B%A0&quot; aria-label=&quot;장애 알람 수신 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;장애 알람 수신&lt;/h2&gt;
&lt;p&gt;데이터독을 사용하면 모니터링에 필요한 도구들을 적은 공수로 설정할 수 있지만, 모니터링만큼 중요한 것이 문제 상황에 대해 적절한 시점에 개입을 할 수 있도록 알람을 받는 것입니다. 행사에 참석하신 분들도 이 부분에 관심을 가지고 계신 것을 확인할 수 있었는데요.&lt;/p&gt;
&lt;p&gt;데브시스터즈는 현재 데브옵스 팀을 주축으로 알람 가이드라인을 만들어두었고, 일관된 규칙의 알람에 대한 원칙을 홍보, 교육하여 게임팀에서 적용할 수 있도록 돕고 있습니다.&lt;/p&gt;
&lt;p&gt;가장 기본적인 컨셉은 데이터독과 슬랙을 연동하여 슬랙으로 알람을 받는 것입니다. 과거에는 배포 환경에 따라 production 환경 알람은 중요 채널로, dev 환경은 서브 채널로 보내는 등의 규칙을 적용하였으나, 이 과정에서 내부 운영툴이나 크게 신경쓰지 않는 외부 서비스 등에 대한 알람이 섞여 들어와 발생한 알람에 대한 중요도를 판단하는데 혼란을 주었습니다. 그래서 지금은 알람을 받은 시점을 기준으로 15분 내에 대응해야 하는 알람, 1시간 내에 대응해야 하는 알람 등 대응시간에 따라 기준을 나누었습니다.&lt;/p&gt;
&lt;p&gt;그렇게 발송된 알람에 대하여 빠르고 체계적인 대응이 필요할 경우 &lt;a href=&quot;https://docs.datadoghq.com/service_management/incident_management/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Datadog Incident&lt;/a&gt; 기능을 통해 별도의 슬랙 채널을 만들어 커뮤니케이션 파편화를 방지하고, 장애 상황과 타임라인은 노션에 기록하여 해결된 이후에도 참고할 수 있도록 관리하고 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;마치며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0&quot; aria-label=&quot;마치며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;마치며&lt;/h2&gt;
&lt;p&gt;2023년 2월에 진행한 스칼라 개발자 밋업 이후 오랜만에 외부 개발자분들과 지식을 나누고 서로 교류하는 시간을 가졌는데요, 데브시스터즈의 개발 문화를 알릴 수 있는 이번 행사를 함께 준비하고 진행할 수 있어서 뜻깊은 시간이었습니다.&lt;/p&gt;
&lt;p&gt;좋은 기회로 행사 전반을 기획하고 진행해주신 데이터독 분들을 비롯하여, 짧은 기간 속에 매주 성실히 준비하면서 패널톡을 진행해주신 창원님, 재영님, 승용님, 대영님, 정훈님께 감사드립니다. 무엇보다, 데브시스터즈에서는 데이터독을 어떻게 사용하는지 궁금증을 가지고 긴 시간동안 집중하며 적극적인 질문으로 의미있는 시간을 만들어주신 참가자분들께 감사드립니다.&lt;/p&gt;
&lt;p&gt;다음에도 개발 행사를 진행할 예정이니 많은 관심 가져주시고 다시 만나뵐 수 있기를 기대합니다.
&lt;we-are-hiring /&gt;&lt;/p&gt;</content:encoded></item><item><title><![CDATA[고성능을 위한 ZIO 튜닝]]></title><description><![CDATA[프로덕션 환경에서 ZIO 애플리케이션을 최대한 빠르게 만드는 방법.]]></description><link>https://tech.devsisters.com/posts/tuning-zio-for-high-performance</link><guid isPermaLink="false">https://tech.devsisters.com/posts/tuning-zio-for-high-performance</guid><pubDate>Mon, 05 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;알림: 이 글은 &lt;a href=&quot;https://blog.pierre-ricadat.com/tuning-zio-for-high-performance&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Tuning ZIO for high performance&lt;/a&gt;의 한국어 번역본입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;안녕하세요, 스튜디오 킹덤에서 서버 소프트웨어 엔지니어로 일하고 있는 삐에르입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;우선, 이 글에서 논의하는 내용이 절대적인 진리는 아니라는 점을 밝힙니다. 무엇을 수행하는 애플리케이션인지에 따라 애플리케이션을 빠르게 만드는 방법도 크게 좌우됩니다. 사용 사례에 따라 ZIO의 오버헤드는 완전히 무시할 수 있는 수준일 수도 있지만, 상당히 클 수도 있습니다. 이를 알 수 있는 유일한 방법은 프로파일러와 함께 부하 테스트를 수행하여 애플리케이션 성능을 분석하고 측정하는 것입니다. 하지만 이 글을 통해 ZIO 앱을 프로덕션에 배포할 때 생각할 법한 몇 가지 흥미로운 점과 고려할 점을 알려드리고자 합니다.&lt;/p&gt;
&lt;h2 id=&quot;런타임-플래그&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%9F%B0%ED%83%80%EC%9E%84-%ED%94%8C%EB%9E%98%EA%B7%B8&quot; aria-label=&quot;런타임 플래그 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;런타임 플래그&lt;/h2&gt;
&lt;p&gt;우선 쉬운 것부터 시작하겠습니다. ZIO에는 애플리케이션 실행 시 쉽게 켜거나 끌 수 있는 몇 가지 플래그가 있습니다. 이 중 모든 플래그를 상세히 설명하지는 않겠지만, 프로덕션에서 어느 정도 영향을 미칠 수 있는 두 가지 플래그에 주목하고자 합니다.&lt;/p&gt;
&lt;p&gt;첫 번째 플래그는 &lt;code&gt;FiberRoots&lt;/code&gt;라는 이름을 가지고 있으며 기본적으로 활성화되어 있습니다. 이 플래그가 활성화되면 루트 fiber가 생성될 때마다(일반적으로 &lt;code&gt;forkDaemon&lt;/code&gt; 연산자를 사용할 때) 이 fiber는 모든 루트 fiber를 추적하는 목록에 추가됩니다. 이 메커니즘은 두 가지 목적을 가지고 있습니다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fiber 덤프를 수행할 수 있습니다. 스레드 덤프와 마찬가지로 애플리케이션 프로세스에 &lt;code&gt;kill -s INFO&lt;/code&gt;를 보내면 루트 fiber의 전체 목록, 상태, 현재 수행 중인 작업(실행 중인 함수)을 출력합니다.&lt;/li&gt;
&lt;li&gt;애플리케이션을 중지할 때 ZIO는 모든 루트 fiber를 중단하려고 시도합니다. 이를 추적하지 않으면 메인 fiber에서 생성된 &quot;자식&quot; fiber만 중단되지만, &lt;code&gt;forkDaemon&lt;/code&gt;을 통해 생성된 다른 루트 fiber는 깨끗하게 종료되지 않을 것입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그러나 많은 루트 fiber를 생성하는 경우 이러한 추적에는 비용이 발생할 수 있습니다. 가장 극단적인 사례로, 아무 작업도 하지 않는 루트 fiber만 포크하는 경우 &lt;code&gt;FiberRoots&lt;/code&gt; 플래그를 비활성화하면 성능이 2.5배 향상됩니다(벤치마크 결과는 &lt;a href=&quot;https://github.com/zio/zio/pull/8745&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;여기&lt;/a&gt; 참조). 비록 코드에서 &lt;code&gt;forkDaemon&lt;/code&gt;을 많이 사용하지 않더라도 사용하는 라이브러리에 의해 호출될 수 있습니다. 예를 들어, zio-grpc는 &lt;a href=&quot;https://github.com/scalapb/zio-grpc/blob/25c467156110b11ee520d9925fbb7a50013c70eb/core/src/main/scalajvm/scalapb/zio_grpc/server/ListenerDriver.scala#L38&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;모든 gRPC 요청&lt;/a&gt;에서 &lt;code&gt;forkDaemon&lt;/code&gt;을 호출합니다. 일부 ZIO 연산자도 내부적으로 이를 사용합니다.&lt;/p&gt;
&lt;p&gt;개인적으로 로컬 환경에서는 가끔 fiber 덤프를 사용하지만, 프로덕션에서는 정말로 필요하지 않기 때문에 불필요한 추적을 피하기 위해 해당 플래그를 비활성화했습니다. 이를 비활성화하려면 ZIO 앱의 &lt;code&gt;bootstrap&lt;/code&gt; 함수에 다음 코드를 추가하면 됩니다:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; bootstrap &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Runtime&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;disableFlags&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;RuntimeFlag&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;FiberRoots&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;기본적으로 비활성화되어 있는 또 다른 흥미로운 플래그가 있습니다: &lt;code&gt;RuntimeMetrics&lt;/code&gt;. 이 플래그를 활성화하면 몇몇 성능 메트릭이 직접 ZIO 메트릭으로 노출됩니다. Prometheus, Datadog 등 원하는 백엔드로 메트릭을 보내고 있다면, 이들은 자동으로 송출될 것입니다. 수집되는 메트릭은 다음과 같습니다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;zio_fiber_failure_causes&lt;/code&gt;는 fiber 실패의 개략적인 원인을 수집합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;zio_fiber_fork_locations&lt;/code&gt;는 코드에서 fiber를 가장 많이 포크하는 위치를 알려줍니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;zio_fiber_started&lt;/code&gt;, &lt;code&gt;zio_fiber_successes&lt;/code&gt;, &lt;code&gt;zio_fiber_failures&lt;/code&gt;는 시작된 fiber의 수와 성공하거나 실패한 fiber의 수를 계산합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;zio_fiber_lifetimes&lt;/code&gt;는 fiber가 얼마나 오래 실행되는지를 측정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;프로덕션 시스템을 모니터링할 때 이러한 메트릭은 매우 유용할 수 있으므로 런타임 오버헤드를 감안하더라도 이를 고려해볼만 합니다. 그 오버헤드는 매우 작겠지만, 적용하기 전에 항상 테스트하고 측정해야 한다는 점을 명심해주세요.&lt;/p&gt;
&lt;p&gt;이를 활성화하려면 &lt;code&gt;bootstrap&lt;/code&gt; 함수에 다음 코드를 추가하면 됩니다:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; bootstrap &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Runtime&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;enableRuntimeMetrics&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;병렬-처리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B3%91%EB%A0%AC-%EC%B2%98%EB%A6%AC&quot; aria-label=&quot;병렬 처리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;병렬 처리&lt;/h2&gt;
&lt;p&gt;이제 ZIO를 사용할 때 흔히 발생하는 문제인 무제한 병렬 처리에 대해 이야기해보겠습니다. ZIO의 일반적인 연산자 중 하나는 &lt;code&gt;ZIO.foreach&lt;/code&gt;로, 컬렉션의 각 항목에 대해 효과를 실행할 수 있게 해줍니다. 결과를 수집할 필요가 없을 때는(예: 효과가 &lt;code&gt;Unit&lt;/code&gt;을 반환하는 경우) 더 빠른 &lt;code&gt;ZIO.foreachDiscard&lt;/code&gt;도 있습니다. 병렬로 작업을 수행하려면 &lt;code&gt;ZIO.foreachPar&lt;/code&gt; 및 &lt;code&gt;ZIO.foreachParDiscard&lt;/code&gt;와 같은 병렬 대응 연산자가 있습니다.&lt;/p&gt;
&lt;p&gt;하지만 어떤 상황에서는 &lt;code&gt;foreachPar&lt;/code&gt;가 &lt;code&gt;foreach&lt;/code&gt;보다 느릴 수 있습니다. 어떻게 그럴 수 있을까요?&lt;/p&gt;
&lt;p&gt;1,000개 요소가 있는 컬렉션에서 &lt;code&gt;foreachPar&lt;/code&gt;를 실행하면 ZIO는 1,000개의 fiber를 생성하여 모두 CPU 시간을 놓고 경쟁하게 됩니다. 각 fiber가 수행하는 작업이 CPU 중심적이라면 코어 수보다 많은 fiber를 생성하는 것은 이점이 없습니다. 반대로, 모든 fiber를 생성하고 연결하는 데 필요한 기계적 오버헤드로 인해 성능 저하가 발생할 것입니다. Fiber가 I/O를 수행하는 경우 한 번에 많은 수를 생성하는 것이 의미가 있을 수 있지만, 다른 제한(예: DB 연결, 네트워크 등)에 부딪힐 가능성이 높으므로 제한을 두는 것이 안전할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;foreachPar&lt;/code&gt;를 사용할 때 생성되는 fiber 수를 제어하려면 &lt;code&gt;ZIO&lt;/code&gt;의 &lt;code&gt;withParallelism&lt;/code&gt; 연산자를 사용할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;ZIO
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;foreachPar&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; to &lt;span class=&quot;token number&quot;&gt;1000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;_ &lt;span class=&quot;token keyword&quot;&gt;=&gt;&lt;/span&gt; doSomething&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;withParallelism&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 예제에서는 16개의 fiber가 생성되어 &lt;code&gt;myList&lt;/code&gt;의 요소를 동시에 처리합니다. &lt;code&gt;doSomething&lt;/code&gt;이 CPU 중심적이고 16개의 코어가 있다고 가정하면, 1,000개의 fiber가 동일한 작업을 수행하는 것보다 훨씬 더 나은 성능을 발휘할 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;withParallelism&lt;/code&gt; 연산자는 그 범위 내에서 실행되는 코드의 병렬 처리 수준을 조정합니다. 여기에는 부모로부터 이 설정을 상속받는 포크된 모든 자식 fiber가 포함됩니다. 따라서 애플리케이션 전반에 걸쳐 합리적인 기본 병렬 처리 수준을 설정하기 위해 메인 함수에서 &lt;code&gt;withParallelism&lt;/code&gt;을 사용할 수 있으며, 필요에 따라 지역적으로 조정할 수 있습니다.&lt;/p&gt;
&lt;p&gt;재미있는 사실: 이 글을 작성하면서 &lt;code&gt;foreachPar&lt;/code&gt;의 구현을 확인했는데, 컬렉션 크기보다 높은 숫자 &lt;code&gt;n&lt;/code&gt;으로 &lt;code&gt;withParallelism&lt;/code&gt;을 사용할 때 여전히 &lt;code&gt;n&lt;/code&gt;개의 fiber를 생성하고 있음을 발견했습니다. 이는 불필요한 작업입니다. 컬렉션의 항목 수만큼 fiber를 생성하면 됩니다. 이를 최적화하기 위해 &lt;a href=&quot;https://github.com/zio/zio/pull/8847&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;PR을 열었으며&lt;/a&gt;, 다음 ZIO의 마이너 릴리스에 반영될 것입니다.&lt;/p&gt;
&lt;h2 id=&quot;executor&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#executor&quot; aria-label=&quot;executor permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Executor&lt;/h2&gt;
&lt;p&gt;Fiber를 생성하고 작업을 할당하면 이 작업은 결국 실제 스레드에서 실행됩니다. ZIO에서는 이러한 스레드를 생성하고 작업을 할당하여 CPU 효율성을 최대화하는 논리를 담당하는 것이 &lt;em&gt;executor&lt;/em&gt;의 역할입니다. 여기서 선택할 수 있는 다양한 옵션을 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;ZIO 2.1.0부터 기본 executor는 &lt;code&gt;ZScheduler&lt;/code&gt;라고 불리며, 이는 본질적으로 Rust의 Tokio 스케줄러의 포트입니다. &lt;a href=&quot;https://tokio.rs/blog/2019-10-scheduler&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;이 글&lt;/a&gt;은 알고리즘을 자세히 설명하지만, 간략하게 요약하자면:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;사용 가능한 코어 수와 동일한 수의 스레드를 생성합니다.&lt;/li&gt;
&lt;li&gt;각 스레드는 실행할 작업의 로컬 큐를 가지며, 모든 스레드가 공유하는 전역 큐도 존재합니다.&lt;/li&gt;
&lt;li&gt;Fiber가 effect를 포크할 때 새 fiber를 생성하여 현재 스레드의 로컬 큐에 추가합니다. 현재 스레드가 ZIO 스레드가 아닌 경우(예: 외부 스레드에서 &lt;code&gt;unsafe.run&lt;/code&gt;을 호출하는 경우), 전역 큐에 추가합니다. 로컬 큐가 꽉 차면(크기가 제한됨) 작업의 절반을 가져와 전역 큐로 이동합니다.&lt;/li&gt;
&lt;li&gt;그런 다음 슬리핑 중인 스레드가 있는지 확인하고, 찾으면 깨웁니다.&lt;/li&gt;
&lt;li&gt;깨어나거나 현재 작업을 마친 후 스레드는 다음 작업을 다음과 같은 방식으로 찾습니다:
&lt;ul&gt;
&lt;li&gt;먼저 로컬 큐에서 가져옵니다.&lt;/li&gt;
&lt;li&gt;로컬 큐가 비어 있으면 전역 큐에서 가져옵니다.&lt;/li&gt;
&lt;li&gt;전역 큐가 비어 있으면 다른 스레드의 로컬 큐에서 작업을 훔쳐올 수 있는지 확인합니다. 훔칠 때는 다른 스레드의 로컬 큐의 절반을 가져옵니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 프로세스는 입증되었고 ZIO 2가 출시된 이후로 효율적으로 작동하고 있습니다. 최근에 &lt;a href=&quot;https://github.com/zio/zio/pull/8745&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;향상되기까지 했습니다&lt;/a&gt;. 그렇다면 왜 변경해야 할까요? 알아봅시다.&lt;/p&gt;
&lt;h3 id=&quot;블로킹&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B8%94%EB%A1%9C%ED%82%B9&quot; aria-label=&quot;블로킹 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;블로킹&lt;/h3&gt;
&lt;p&gt;ZIO 2.0.x에서 기본 스케줄러는 기본적으로 자동 블로킹 기능이 활성화되어 있었습니다. 2.1.x에서는 비활성화되어 있으며 명시적으로 선택해야 합니다:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; bootstrap &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Runtime&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;enableAutoBlockingExecutor
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 기능은 fiber가 블로킹 작업을 수행하고 있는지 감지하려고 하며, 감지되면 그 코드 위치를 기억해 두었다가, 나중에 그 코드가 다시 실행되면 자동으로 fiber 를 블로킹 스레드풀로 이동시켜 줍니다. 사용자가 블로킹 코드를 &lt;code&gt;ZIO.blocking&lt;/code&gt; 또는 &lt;code&gt;ZIO.attemptBlocking&lt;/code&gt;으로 래핑하는 것을 잊었을 때 executor가 대신 처리해주는 것이 목표입니다.&lt;/p&gt;
&lt;p&gt;기본 executor에서 블로킹 코드를 실행하는 것은 좋지 않습니다. 왜냐하면 제한된 수의 스레드를 사용하므로 쉽게 모든 스레드가 블로킹되어 아무 작업도 수행하지 못하는 상황에 처할 수 있기 때문입니다.&lt;/p&gt;
&lt;p&gt;이 기능의 문제는 fiber가 블로킹 코드를 실행하고 있는지 여부를 감지하는 것이 완벽하지 않은 휴리스틱이라는 점입니다. 간단히 말해서, 작업이 특정 임계값보다 오래 실행되면 &quot;블로킹&quot;으로 간주됩니다. 그러나 이는 실제로 블로킹된 것이 아니라 단순히 CPU 중심적일 수 있으며, 다른 이유로 인해 느릴 수도 있습니다(시작 직후 JVM 이 warm-up 이 되지 않았을 때 클래스 로딩이 시간이 걸려 블로킹 스레드풀로 이동된 경우를 본 적이 있습니다). 또 다른 문제는 블로킹 위치를 추적하고 매번 작업을 실행할 때 이를 확인하는 것이 오버헤드를 유발한다는 것입니다.&lt;/p&gt;
&lt;p&gt;가능한 최고의 성능을 얻으려면 이 모드를 사용하지 않고 블로킹 코드를 블로킹 스레드풀로 명시적으로 이동하는 것이 좋습니다. 의심이 간다면 스레드 상태를 살펴보고 잠재적인 블로킹을 찾아내기 위해 프로파일러를 사용하십시오. 하지만 당신의 코드나 사용하는 라이브러리에 대해 확신이 없고, 더 안전하게 하기 위해 성능 페널티를 기꺼이 감수할 수 있다면, 자동 블로킹을 활성화하는 것도 충분히 괜찮은 선택지입니다.&lt;/p&gt;
&lt;h3 id=&quot;executor-오버라이드&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#executor-%EC%98%A4%EB%B2%84%EB%9D%BC%EC%9D%B4%EB%93%9C&quot; aria-label=&quot;executor 오버라이드 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Executor 오버라이드&lt;/h3&gt;
&lt;p&gt;ZIO에서 꽤 놀랍고 많은 사람들이 알지 못하는 동작이 하나 있습니다. 블로킹 스레드풀로 작업을 이동할 때, fiber는 해당 코드를 실행한 후 기본 executor로 자동으로 돌아오지 않는다는 것입니다. 한번 fiber 가 블로킹 스레드풀에 들어가면, 그 fiber 는 끝날 때까지, 또는 내부 루프를 충분히 많이 실행하여 기본 executor 로 돌아갈 (yield) 때까지 블로킹 스레드풀에서 계속 실행됩니다.&lt;/p&gt;
&lt;p&gt;사용 사례에 따라 이러한 동작이 좋을 수도 나쁠 수도 있습니다. Fiber가 블로킹 호출 후 즉시 종료되는 경우(예: DB 호출을 수행하고 클라이언트에 반환하는 경우), 스레드 이동을 피하는 것이 더 효율적입니다. 그러나 fiber가 장기 실행되는 경우(예: &lt;a href=&quot;https://github.com/devsisters/shardcake&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Shardcake&lt;/a&gt;의 엔터티 동작), 블로킹 스레드풀에서 많은 코드가 실행되어 많은 수의 스레드가 생성되고 성능이 저하될 수 있습니다. 그에 반해 &lt;a href=&quot;https://github.com/typelevel/cats-effect&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Cats Effect&lt;/a&gt;는 기본적으로 자동으로 돌아오도록 선택한 것으로 보입니다.&lt;/p&gt;
&lt;p&gt;하지만 이를 해결하는 간단한 방법이 있으며, executor를 명시적으로 설정하는 것입니다. 기본 executor를 오버라이드하면 블로킹 코드가 실행된 후 작업이 항상 해당 executor로 돌아옵니다. 그러나 기본 executor를 유지하고 싶다면 어떻게 해야 할까요? 간단히 말해서 기본 executor를 자체적으로 오버라이드할 수 있습니다!&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; bootstrap &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  Runtime&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;setExecutor&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Executor&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;makeDefault&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;autoBlocking &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 간단한 줄은 블로킹 코드 실행 후 항상 기본 executor로 돌아오도록 보장합니다.&lt;/p&gt;
&lt;p&gt;ZIO 2.1.2부터는 블로킹 후 이전 executor로 돌아갈지를 제어하는 플래그가 생겼습니다. 위의 해결책 대신 다음과 같이 할 수 있습니다:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; bootstrap &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
   Runtime&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;enableFlags&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;RuntimeFlag&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;EagerShiftBack&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;대안-executor&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8C%80%EC%95%88-executor&quot; aria-label=&quot;대안 executor permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;대안 Executor&lt;/h3&gt;
&lt;p&gt;기본 executor를 변경하는 또 다른 이유는 새로운 대체 executor를 시도해보는 것입니다. 첫째, ZIO 2.1.x에서는 Loom에 기반한 새로운 executor가 추가되었습니다 (JDK21+에서만 사용 가능). 이 executor는 각 fiber 작업에 Loom의 가상 스레드를 할당합니다. 실제 스레드에서 이러한 가상 스레드를 실행하는 효율성은 Loom에 맡겨집니다.&lt;/p&gt;
&lt;p&gt;이를 사용하려면 &lt;code&gt;bootstrap&lt;/code&gt; 함수에 다음 코드를 추가하십시오:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; bootstrap &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Runtime&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;enableLoomBasedExecutor
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이는 반드시 테스트해봐야 할 흥미로운 대안입니다. 그러나 많은 fiber를 생성하고 연결하는 것을 테스트하는 ZIO 벤치마크에서 기본적으로 Tokio 기반 스케줄러가 Loom 기반 스케줄러보다 상당히 빠르다는 점을 참고하십시오.&lt;/p&gt;
&lt;p&gt;최근 또 다른 가능성은 Kyo의 스케줄러입니다. 이는 최근에 &lt;a href=&quot;https://github.com/getkyo/kyo/releases/tag/v0.9.3&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;kyo 0.9.3&lt;/a&gt;에서 독립적인 의존성으로 추출되었습니다. 아주 최근에 릴리즈 되었고 아직 실전에서 증명된 물건은 아니지만, 그 창시자 &lt;a href=&quot;https://twitter.com/fbrasisil&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Flavio Brasil&lt;/a&gt;은 JVM에서 다양한 effect 시스템을 사용한 경험을 바탕으로 한 전문가이며 그의 스케줄러는 이러한 경험을 바탕으로 만들어졌습니다. 향후에 이를 시도해볼 계획입니다.&lt;/p&gt;
&lt;h3 id=&quot;datadog에-대한-마지막-주의-사항&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#datadog%EC%97%90-%EB%8C%80%ED%95%9C-%EB%A7%88%EC%A7%80%EB%A7%89-%EC%A3%BC%EC%9D%98-%EC%82%AC%ED%95%AD&quot; aria-label=&quot;datadog에 대한 마지막 주의 사항 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Datadog에 대한 마지막 주의 사항&lt;/h3&gt;
&lt;p&gt;Datadog을 사용하는 사용자에게 마지막 팁을 드리겠습니다. Datadog은 &lt;a href=&quot;https://docs.datadoghq.com/profiler/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;코드를 자동으로 프로파일링&lt;/a&gt;하여 별다른 오버헤드 없이 작동하는 에이전트를 제공합니다. 저는 프로덕션에서 ZIO와 관련된 두 가지 플래그를 변경했습니다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-Ddd.integration.throwables.enabled=false&lt;/code&gt;는 예외 프로파일링을 비활성화합니다. 왜 필요할까요? ZIO 2.0.x 버전부터 ZIO는 내부적으로 제어 흐름을 위한 예외를 사용합니다. 이는 매우 많은 예외를 발생시켜 프로파일러를 과부하시킬 수 있습니다. ZIO 2.1.x에서는 이러한 사용이 감소했지만 완전히 제거되지는 않았기 때문에 여전히 유효합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-Ddd.integration.zio.experimental.enabled=true&lt;/code&gt;는 성능을 향상시키지는 않지만 Datadog 추적기가 ZIO fiber 간에 컨텍스트를 전달할 수 있게 해줍니다. 이를 통해 더 나은 추적을 할 수 있으며, 예를 들어 DB 호출을 API 호출에 연결할 수 있습니다. 저희도 프로덕션에서 이를 사용하고 있으며, 오버헤드가 없음을 확인했습니다. 아직 실험적인 옵션이고 공식 문서에도 없지만 잘 작동하고 있어 추천합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;오늘은 여기까지입니다! 도움이 되었기를 바랍니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[머신러닝 엔지니어가 퍼즐 게임을 더 재미있게 만드는 방법]]></title><description><![CDATA[머신러닝 엔지니어가 더 재미있는 퍼즐 게임을 위해 진행했던 프로젝트를 소개합니다.]]></description><link>https://tech.devsisters.com/posts/ml-engineer-better-puzzle-game</link><guid isPermaLink="false">https://tech.devsisters.com/posts/ml-engineer-better-puzzle-game</guid><pubDate>Wed, 29 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h1 id=&quot;쿠키런-마녀의-성과-퍼즐봇&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%BF%A0%ED%82%A4%EB%9F%B0-%EB%A7%88%EB%85%80%EC%9D%98-%EC%84%B1%EA%B3%BC-%ED%8D%BC%EC%A6%90%EB%B4%87&quot; aria-label=&quot;쿠키런 마녀의 성과 퍼즐봇 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;쿠키런: 마녀의 성과 퍼즐봇&lt;/h1&gt;
&lt;p&gt;쿠키런: 마녀의 성(이하 &apos;마녀의 성&apos;)은 탭(Tap to blast) 방식의 퍼즐을 풀어 쿠키들을 마녀의 성에서 탈출시키는 퍼즐 게임입니다. 데브시스터즈에서는 마녀의 성을 AI가 학습하여 플레이하는 강화학습 기반의 퍼즐봇을 만들어 게임 레벨 밸런싱에 사용해왔습니다. 전작이었던 또 다른 퍼즐게임인 쿠키런: 퍼즐월드에서도 퍼즐봇을 만들어 게임 밸런싱에 사용했었는데요, 이 때의 경험을 바탕으로 한층 더 정교한 퍼즐봇을 만들어서 사용할 수 있었습니다. 마녀의 성은 각 레벨마다 독창적인 매력을 가진 스테이지들로 이루어져있고, 퍼즐봇은 이러한 각각의 스테이지 (레벨) 를 하나의 단위로 하여 학습하고 플레이를 하게 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://tech.devsisters.com/posts/crp-puzzle-bot/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;쿠키런: 퍼즐월드의 퍼즐봇&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://wandb.ai/site/customers/devsisters-levels-up-game-development-with-wb&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;쿠키런: 마녀의 성의 퍼즐봇&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id=&quot;밸런싱과-폴리싱-그리고-스테이지-평가-지표&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B0%B8%EB%9F%B0%EC%8B%B1%EA%B3%BC-%ED%8F%B4%EB%A6%AC%EC%8B%B1-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%8A%A4%ED%85%8C%EC%9D%B4%EC%A7%80-%ED%8F%89%EA%B0%80-%EC%A7%80%ED%91%9C&quot; aria-label=&quot;밸런싱과 폴리싱 그리고 스테이지 평가 지표 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;밸런싱과 폴리싱, 그리고 스테이지 평가 지표&lt;/h1&gt;
&lt;p&gt;퍼즐 게임에서 밸런싱의 핵심은 각 스테이지에서 유저들이 사용할 수 있는 제한 무브 수를 결정하는 것입니다. 각 스테이지에는 주어진 미션이 있으며, 이 미션을 제한된 무브 수 안에 모두 해결해야 다음 스테이지로 넘어갈 수 있습니다. 따라서 제한 무브 수가 적으면 게임이 어려워지고, 많이 주어지면 비교적 쉬워집니다. 기획팀과 게임 디자인팀에서 제작한 스테이지는 이러한 밸런싱 과정을 거쳐 유저들에게 선보여집니다.&lt;/p&gt;
&lt;p&gt;폴리싱 단계도 유저들에게 스테이지를 선보이기 전에 필수적으로 거쳐야 합니다. 폴리싱(Polishing; 연마) 단계란 제작된 스테이지들을 직접 플레이해 보며, 유저들에게 더 나은 플레이 경험을 제공하기 위해 스테이지를 다듬는 과정입니다. 스테이지를 플레이해보고 아쉬운 점을 개선하는 작업입니다. 또한, 폴리싱 단계는 제작된 스테이지를 그대로 사용할 수 있을지 판단하는 중요한 과정이기도 합니다.&lt;/p&gt;
&lt;p&gt;저희는 퍼즐봇의 이점을 극대화하기 위해 폴리싱 단계에서도 퍼즐봇 데이터를 활용하고자 했습니다. 결과적으로, 기존의 폴리싱 과정에서 발생할 수 있는 문제점을 보완하고 더 효율적으로 스테이지 제작 과정을 수행하기 위해 퍼즐봇 데이터를 활용하여 각 스테이지를 평가할 수 있는 &quot;스테이지 평가 지표&quot;를 만들었습니다. 본 글에서는 그 이야기를 들려드리려고 합니다.&lt;/p&gt;
&lt;p&gt;요약하자면, 저희는 &quot;스테이지 평가 지표&quot; 를 통해 폴리싱 단계에서 제작된 스테이지를 그대로 출시할 수 있을지 판단할 기준을 제공하고자 했습니다. &quot;좋은 스테이지&quot;라고 말할 수 있는 유저의 행동을 기준으로 잡고, 그 행동 비율과 높은 상관관계를 보이는 지표를 만들었습니다. 이렇게 만들어진 스테이지 평가 지표는 실제로 데브시스터즈 내부에서 진행된 사내 테스트 데이터를 기준으로도 유의미하게 높은 상관관계를 보였습니다. 이 지표는 기존 수작업으로 진행되던 폴리싱 단계에서 데이터 관점의 시각을 제공하여 더 효율적인 폴리싱 수행이 가능하게 했습니다.&lt;/p&gt;
&lt;h1 id=&quot;지표의-필요성과-목표&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A7%80%ED%91%9C%EC%9D%98-%ED%95%84%EC%9A%94%EC%84%B1%EA%B3%BC-%EB%AA%A9%ED%91%9C&quot; aria-label=&quot;지표의 필요성과 목표 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;지표의 필요성과 목표&lt;/h1&gt;
&lt;p&gt;기존의 폴리싱 과정은 &lt;code&gt;[ 퍼즐맵 제작 → 플레이 후 피드백 → 부족한 점 보완 → 이후 밸런싱 과정 진행 ]&lt;/code&gt; 의 순서로 이루어집니다. 이러한 작업은 소수의 폴리싱 작업자의 직감에 의존적일 수 밖에 없고, 이로 인해 개인 성향에 따라 편향되기 쉽습니다. 또한 스테이지의 개수가 늘어남에 따라 폴리싱에 소모되는 시간이 비례하여 증가될 수 있다는 문제가 있습니다.&lt;/p&gt;
&lt;p&gt;물론, 기획자 및 게임 디자이너분들의 감은 아주아주 중요한 요소이지만, 스테이지 제작 과정을 보다 효율적으로 확장하기 위해 &lt;strong&gt;폴리싱 단계에서의 추가적인 판단 기준이 될 수 있는 지표를 제공&lt;/strong&gt;하고자 했습니다. 이는 보다 객관적인 성격의 지표로 사용이 가능하며 폴리싱 과정을 효율화 시키는데 도움이 될 수 있습니다. 정성적인 평가에 대한 의존성을 낮춤으로써, 하나의 레벨에 대해서 &lt;code&gt;[ 여러 맵을 제작 → 지표를 통한 1차적인 판단 → 기획자 / GD 분들의 정성적 평가 → 폴리싱 ]&lt;/code&gt; 과 같은 방식으로 &lt;strong&gt;프로세스를 개선&lt;/strong&gt;시킬 수 있습니다.&lt;/p&gt;
&lt;h1 id=&quot;스테이지-평가-지표&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8A%A4%ED%85%8C%EC%9D%B4%EC%A7%80-%ED%8F%89%EA%B0%80-%EC%A7%80%ED%91%9C&quot; aria-label=&quot;스테이지 평가 지표 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;스테이지 평가 지표&lt;/h1&gt;
&lt;h2 id=&quot;기준이-되는-유저의-행동&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B8%B0%EC%A4%80%EC%9D%B4-%EB%90%98%EB%8A%94-%EC%9C%A0%EC%A0%80%EC%9D%98-%ED%96%89%EB%8F%99&quot; aria-label=&quot;기준이 되는 유저의 행동 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;기준이 되는 유저의 행동&lt;/h2&gt;
&lt;p&gt;스테이지 평가 지표는 유저들이 스테이지를 플레이했을 때의 반응을 예측할 수 있는 지표로 생각해볼 수 있습니다. 이를 위해 기준이 되는 유저의 행동을 설정해야 했습니다. 기획팀과의 논의 끝에 &quot;게임을 클리어하지 못했을 때, 얼마나 높은 비율로 &lt;strong&gt;이어하기 및 하트 구매 후 재시도&lt;/strong&gt;가 이루어지는지&quot;를 기준으로 삼게 되었습니다. 이는 곧 게임 클리어에 실패했을 때, 결제를 해서라도 클리어하고 싶은 스테이지라는 뜻으로 해석할 수 있습니다.&lt;/p&gt;
&lt;p&gt;게임 성공 시의 행동에 대해서는 실패 경험이 유저 경험에 더 큰 영향을 미치며 레벨이 진행될수록 실패 케이스의 비중이 커지기 때문에 기준에 포함시키지 않는 방향으로 결정했습니다.&lt;/p&gt;
&lt;p&gt;스테이지 평가 지표는 이러한 &apos;유저의 행동&apos;을 예측할 수 있는 지표여야 한다(= 상관관계가 높다) 는 것을 목표로 두고 지표 제작 과정을 시작하게 되었습니다.&lt;/p&gt;
&lt;h2 id=&quot;스테이지-평가-지표를-위한-eda---쿠키런-퍼즐월드-데이터&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8A%A4%ED%85%8C%EC%9D%B4%EC%A7%80-%ED%8F%89%EA%B0%80-%EC%A7%80%ED%91%9C%EB%A5%BC-%EC%9C%84%ED%95%9C-eda---%EC%BF%A0%ED%82%A4%EB%9F%B0-%ED%8D%BC%EC%A6%90%EC%9B%94%EB%93%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0&quot; aria-label=&quot;스테이지 평가 지표를 위한 eda   쿠키런 퍼즐월드 데이터 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;스테이지 평가 지표를 위한 EDA - 쿠키런: 퍼즐월드 데이터&lt;/h2&gt;
&lt;p&gt;우선, 마녀의 성의 전작인 &amp;#x3C;쿠키런: 퍼즐월드&gt; 의 실제 유저 플레이 데이터를 활용하여 스테이지 평가 지표를 만들기 위한 힌트를 얻고자 했습니다. 마녀의 성을 플레이한 유저 데이터를 사용할 수 있다면 더 좋았겠지만, 게임 런칭 전에 스테이지 평가 지표가 제작되어야 했기에, 오랜 기간 운영되어 유저 분석이 가능한 기존의 쿠키런: 퍼즐월드 데이터를 활용했습니다. 디테일한 인게임 로직은 차이가 있지만, &quot;미션&quot;과 &quot;유저의 행동&quot;은 퍼즐 게임이라면 어느 정도 공통적인 특성이 있을 것으로 판단했습니다.&lt;/p&gt;
&lt;p&gt;우선, 유저가 게임 클리어에 실패했을 때 남는 기록은 &quot;남은 미션 수&quot; 밖에 없습니다. 제한된 무브 수를 다 사용했지만 남은 미션의 수가 0이 되지 못했다면 게임에서 실패한 것이기 때문입니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;p align=&quot;center&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1702px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 26.38888888888889%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA30lEQVR42o3Qu07DMBhA4T4pA6/CysLzgLoxwQaUJRWU2KlzbeJr2qYcnATUASExfLJk2Uf+vQj9gMotdWspW0cVNdpjw/FfWtOz0z3aBk6nTxZ5OXD3AEXcFDEsSxd5RFzP/J9kNcvqwDDEoMoMtzdLpKjZBsi6gaw9kO2iZj8dPIsXq9/GoPoJ9nXD6uKS16trPpb3pI9PiFWCTFLkRiG2HTI3UXx9MRPTFN+KeYoxPAWHwxGzfkM/v2Aim6yx7xtsKnAyw6mCTuXossa3Gt8ZnPE4Owo4N+rxfj/94RfIWHg+n3ZtMAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/cafc3063ca9415478f4b7af2491427a0/33ecc/total_mission_left_missions.webp 360w,
/static/cafc3063ca9415478f4b7af2491427a0/263a4/total_mission_left_missions.webp 480w,
/static/cafc3063ca9415478f4b7af2491427a0/0c8fb/total_mission_left_missions.webp 640w,
/static/cafc3063ca9415478f4b7af2491427a0/521f7/total_mission_left_missions.webp 860w,
/static/cafc3063ca9415478f4b7af2491427a0/cbd37/total_mission_left_missions.webp 1200w,
/static/cafc3063ca9415478f4b7af2491427a0/d9bc9/total_mission_left_missions.webp 1702w&quot; sizes=&quot;(max-width: 1702px) 100vw, 1702px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/cafc3063ca9415478f4b7af2491427a0/53918/total_mission_left_missions.png 360w,
/static/cafc3063ca9415478f4b7af2491427a0/9aebd/total_mission_left_missions.png 480w,
/static/cafc3063ca9415478f4b7af2491427a0/0f09e/total_mission_left_missions.png 640w,
/static/cafc3063ca9415478f4b7af2491427a0/3e3fe/total_mission_left_missions.png 860w,
/static/cafc3063ca9415478f4b7af2491427a0/64756/total_mission_left_missions.png 1200w,
/static/cafc3063ca9415478f4b7af2491427a0/526f1/total_mission_left_missions.png 1702w&quot; sizes=&quot;(max-width: 1702px) 100vw, 1702px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/cafc3063ca9415478f4b7af2491427a0/526f1/total_mission_left_missions.png&quot; alt=&quot;total mission left missions&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
      &lt;/p&gt;&lt;figcaption align=&quot;center&quot;&gt;쿠키런: 퍼즐월드의 상위 50%, 하위 50% 스테이지들에 대한 남은 미션 수 분포 비교&lt;/figcaption&gt;
    &lt;p&gt;&lt;/p&gt;
&lt;/figure&gt;
&lt;p&gt;위 차트는 기존에 정한 &quot;유저의 행동&quot; 비율을 기준으로 상위 50%에 해당하는 스테이지들(Good, 파란색)과 하위 50%에 해당하는 스테이지들(Bad, 빨간색)의 남은 미션 수 분포를 나타낸 것입니다. 남은 미션 수가 1인 지점에서 파란색이 약 25%였고, 이는 상위 50%에 해당하는 모든 스테이지의 평균을 냈을 때 남은 미션 수가 1개인 플레이 비율이 약 25%였다는 것을 의미합니다.&lt;/p&gt;
&lt;p&gt;위 차트를 그리면서 상위, 하위의 극단적인 스테이지들을 가져온 것이 아니라 모든 스테이지를 포함시켜 단순 평균을 낸 것임에도 유의미한 차이가 발생했다고 판단되었습니다. 즉, 유저 행동의 비율이 높은 경우, 남은 미션 수가 적은 실패 플레이 비율이 더 높다는 것을 의미합니다. 전체 스테이지가 아닌 성공률을 기준으로 보았을 때나 미션별로 스테이지를 구분하여 보았을 때도 대부분의 경우 위 차트와 유사한 경향이 나타났습니다. 다만, 미션에 따라 총 미션 수에 큰 차이가 있었고, Good과 Bad(파란색, 빨간색)가 달라지는 지점에도 조금씩 차이가 있었습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;p align=&quot;center&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1844px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 24.444444444444443%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA2UlEQVR42o2Qu26DQBBF+c58QqoU+ZOkSZ9PsdxQO1IkF8Q2Q5IFFlj2NQu+GUCu4+LojrSPObpZiAm1Nqhbg6azwoi2d7A+wYXpX0bP0EPAYAKYJ2TaRFz0hO8+gWoPUlbSoVT3UzUOF0krC7KPg8H7W4HjgfDTyYV+BrUR1ATBr5/fQykii3Gm9jnyh0d8Pj2jeHnFaZejOp5AlQb9jpvxjdvjJdV2ts6LYW03w2QtIpXwJcEVX/DnM6JS4L4Dm0F6SfAurHC6gqVzTjM4xJW4zEKU/ub5ij8QUnlmRTP1swAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/160df90b7b27fe6189ede7e744bd4932/33ecc/milk_mission_left_missions.webp 360w,
/static/160df90b7b27fe6189ede7e744bd4932/263a4/milk_mission_left_missions.webp 480w,
/static/160df90b7b27fe6189ede7e744bd4932/0c8fb/milk_mission_left_missions.webp 640w,
/static/160df90b7b27fe6189ede7e744bd4932/521f7/milk_mission_left_missions.webp 860w,
/static/160df90b7b27fe6189ede7e744bd4932/cbd37/milk_mission_left_missions.webp 1200w,
/static/160df90b7b27fe6189ede7e744bd4932/6a1cb/milk_mission_left_missions.webp 1844w&quot; sizes=&quot;(max-width: 1844px) 100vw, 1844px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/160df90b7b27fe6189ede7e744bd4932/53918/milk_mission_left_missions.png 360w,
/static/160df90b7b27fe6189ede7e744bd4932/9aebd/milk_mission_left_missions.png 480w,
/static/160df90b7b27fe6189ede7e744bd4932/0f09e/milk_mission_left_missions.png 640w,
/static/160df90b7b27fe6189ede7e744bd4932/3e3fe/milk_mission_left_missions.png 860w,
/static/160df90b7b27fe6189ede7e744bd4932/64756/milk_mission_left_missions.png 1200w,
/static/160df90b7b27fe6189ede7e744bd4932/b88a2/milk_mission_left_missions.png 1844w&quot; sizes=&quot;(max-width: 1844px) 100vw, 1844px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/160df90b7b27fe6189ede7e744bd4932/b88a2/milk_mission_left_missions.png&quot; alt=&quot;milk mission left missions&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
      &lt;/p&gt;&lt;figcaption align=&quot;center&quot;&gt;밀크 로드 미션 스테이지들 기준&lt;/figcaption&gt;
    &lt;p&gt;&lt;/p&gt;
&lt;/figure&gt;
&lt;figure&gt;
    &lt;p align=&quot;center&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1844px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 24.444444444444443%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAr0lEQVR42pXQzQ2DMAwF4GzfVboAC3SBXquqyolAy7/AISYBXk3gXnr4ZDuKnqMoxwFVQ6haQt1ZQSibAT1NcDxj/IGcR9sz+oHh/QzVk0Ne2cMoZK4dMulNeU5ej0ilWlmgii5AlzNyWpG1XoL2sH+Z0sYXq+e9wO1yhUkSmIeG+Qw7uWCq89IYGKACDWD9gtMa/H6DmxZMFswTeApg+Rfrpmjr41m09+6Yt7osK74jkHx/nYXregAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/7ff592fe130afa9ddde87f5ee2a75624/33ecc/popping_mission_left_missions.webp 360w,
/static/7ff592fe130afa9ddde87f5ee2a75624/263a4/popping_mission_left_missions.webp 480w,
/static/7ff592fe130afa9ddde87f5ee2a75624/0c8fb/popping_mission_left_missions.webp 640w,
/static/7ff592fe130afa9ddde87f5ee2a75624/521f7/popping_mission_left_missions.webp 860w,
/static/7ff592fe130afa9ddde87f5ee2a75624/cbd37/popping_mission_left_missions.webp 1200w,
/static/7ff592fe130afa9ddde87f5ee2a75624/6a1cb/popping_mission_left_missions.webp 1844w&quot; sizes=&quot;(max-width: 1844px) 100vw, 1844px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/7ff592fe130afa9ddde87f5ee2a75624/53918/popping_mission_left_missions.png 360w,
/static/7ff592fe130afa9ddde87f5ee2a75624/9aebd/popping_mission_left_missions.png 480w,
/static/7ff592fe130afa9ddde87f5ee2a75624/0f09e/popping_mission_left_missions.png 640w,
/static/7ff592fe130afa9ddde87f5ee2a75624/3e3fe/popping_mission_left_missions.png 860w,
/static/7ff592fe130afa9ddde87f5ee2a75624/64756/popping_mission_left_missions.png 1200w,
/static/7ff592fe130afa9ddde87f5ee2a75624/b88a2/popping_mission_left_missions.png 1844w&quot; sizes=&quot;(max-width: 1844px) 100vw, 1844px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/7ff592fe130afa9ddde87f5ee2a75624/b88a2/popping_mission_left_missions.png&quot; alt=&quot;popping mission left missions&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
      &lt;/p&gt;&lt;figcaption align=&quot;center&quot;&gt;팝핑 젤리 벌레 미션 스테이지들 기준&lt;/figcaption&gt;
    &lt;p&gt;&lt;/p&gt;
&lt;/figure&gt;
&lt;p&gt;위 분석을 통해 &quot;남은 미션 수가 적게 남은 플레이 비율이 얼마나 많은가&quot;를 스테이지 평가 지표에서 활용하는 것이 합리적이라는 결론을 내렸습니다. 다음 문제는 &quot;그러한 정도를 어떻게 측정할까&quot;와 &quot;총 미션 수에 따라 달라지는 디테일을 어떻게 반영할까&quot;였습니다.&lt;/p&gt;
&lt;h2 id=&quot;스테이지-평가-지표-설계-및-분석&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8A%A4%ED%85%8C%EC%9D%B4%EC%A7%80-%ED%8F%89%EA%B0%80-%EC%A7%80%ED%91%9C-%EC%84%A4%EA%B3%84-%EB%B0%8F-%EB%B6%84%EC%84%9D&quot; aria-label=&quot;스테이지 평가 지표 설계 및 분석 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;스테이지 평가 지표 설계 및 분석&lt;/h2&gt;
&lt;p&gt;“남은 미션 수가 적게 남은 플레이 비율이 얼마나 많은가”를 두 가지 방식으로 활용해 보고자 했습니다. 첫 번째는 &lt;strong&gt;남은 미션 수의 분포가 얼마나 0에 가깝게 존재&lt;/strong&gt;하는가, 두 번째는 &lt;strong&gt;총 미션 수 대비 남은 미션 수의 비율&lt;/strong&gt; 혹은 &lt;strong&gt;남은 미션 수가 특정 갯수 이하인 플레이의 비율&lt;/strong&gt;이 어느 정도인가의 기준이었습니다.&lt;/p&gt;
&lt;h4 id=&quot;첫-번째-기준-남은-미션-수의-전체적인-분포가-얼마나-0에-가깝게-존재하는가&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B2%AB-%EB%B2%88%EC%A7%B8-%EA%B8%B0%EC%A4%80-%EB%82%A8%EC%9D%80-%EB%AF%B8%EC%85%98-%EC%88%98%EC%9D%98-%EC%A0%84%EC%B2%B4%EC%A0%81%EC%9D%B8-%EB%B6%84%ED%8F%AC%EA%B0%80-%EC%96%BC%EB%A7%88%EB%82%98-0%EC%97%90-%EA%B0%80%EA%B9%9D%EA%B2%8C-%EC%A1%B4%EC%9E%AC%ED%95%98%EB%8A%94%EA%B0%80&quot; aria-label=&quot;첫 번째 기준 남은 미션 수의 전체적인 분포가 얼마나 0에 가깝게 존재하는가 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;첫 번째 기준: 남은 미션 수의 전체적인 분포가 얼마나 0에 가깝게 존재하는가&lt;/h4&gt;
&lt;p&gt;첫 번째 기준으로 남은 미션 수의 전체적인 분포가 얼마나 0에 가깝게 존재하는가를 평가하기 위해 다음과 같은 과정을 거쳤습니다. 먼저, 스테이지마다 총 미션 수가 다르고, 실패한 플레이 횟수도 다르기 때문에 정규화 과정을 거쳤습니다. 그리고 표준편차를 계산하는 방법에서 아이디어를 얻어, 남은 미션 수가 적은 경우에 높은 가중치를 주었습니다. 이를 단순하게 표현하면, 하나의 스테이지에 대해 남은 미션 수 별로 (남은 미션 수 별 가중치) X (플레이 종료 시 해당 미션 수가 남은 유저 플레이의 횟수=플레이 카운트)를 합한 값을 정규화하여 첫 번째 지표로 삼았습니다.&lt;/p&gt;
&lt;h4 id=&quot;두-번째-기준-특정-비율-혹은-특정-개수-관점의-지표&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%91%90-%EB%B2%88%EC%A7%B8-%EA%B8%B0%EC%A4%80-%ED%8A%B9%EC%A0%95-%EB%B9%84%EC%9C%A8-%ED%98%B9%EC%9D%80-%ED%8A%B9%EC%A0%95-%EA%B0%9C%EC%88%98-%EA%B4%80%EC%A0%90%EC%9D%98-%EC%A7%80%ED%91%9C&quot; aria-label=&quot;두 번째 기준 특정 비율 혹은 특정 개수 관점의 지표 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;두 번째 기준: 특정 비율 혹은 특정 개수 관점의 지표&lt;/h4&gt;
&lt;p&gt;두 번째 기준은 특정 비율 또는 특정 개수 관점의 지표입니다. 예를 들어, 20%를 기준으로 한다면 스테이지마다 주어진 총 미션 수 대비 20% 이하의 미션 수만 남긴 플레이의 비율을 계산하는 것입니다. 총 미션 수가 100개인 스테이지라면, 제한된 무브 수를 다 사용했을 때 20개 이하의 미션만 남긴 플레이의 비율을 계산합니다.&lt;/p&gt;
&lt;p&gt;그런데 총 미션 수 자체가 적은 경우에는 비율을 기반으로 설정하는 것이 어렵습니다. 예를 들어 총 미션 수가 2개인 스테이지에서 남은 미션 수가 1개라면 비율을 50% 로 설정하여 지표화하는 것이 의도를 잘 반영하기 힘듭니다. 따라서 총 미션 수가 특정 개수 이상이면 비율을 기준으로, 특정 개수 이하라면 절대적인 미션 수를 기준으로 사용하고자 했습니다.&lt;/p&gt;
&lt;p&gt;결과적으로 지표의 기준으로 사용할 [총 미션 수], [기준 미션 수], [기준 미션 수 비율]의 세 가지 값을 찾아야 했습니다. 기획팀의 도움으로 각 기준의 적당한 범위를 설정하고 Grid Search를 통해 최적화를 진행했습니다. 이 세 가지 값을 바꿔가면서 나온 플레이 비율과 유저의 행동 비율 사이의 상관관계가 높은 것을 찾는 것이 목표였습니다.&lt;/p&gt;
&lt;h4 id=&quot;상관관계-분석&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%83%81%EA%B4%80%EA%B4%80%EA%B3%84-%EB%B6%84%EC%84%9D&quot; aria-label=&quot;상관관계 분석 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;상관관계 분석&lt;/h4&gt;
&lt;p&gt;위의 두 관점에서 설계한 지표들과 유저의 행동 사이의 상관관계는 아래와 같았습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;전체 분포 관점 지표와 “유저의 행동” 사이의 상관관계 : 0.49 ~ 0.51&lt;/li&gt;
&lt;li&gt;특정 비율 or 개수 관점 지표와 “유저의 행동” 사이의 상관관계:
&lt;ul&gt;
&lt;li&gt;총 미션 수 기준 = 12 / 기준 미션 수 = 2 / 기준 미션 비율 = 0.2 일 때 &lt;strong&gt;0.51 ~ 0.54&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;이는 곧, 총 미션 수가 12 이하일 때는 남은 미션 수의 개수가 2 이하인 플레이 비율에 해당하며, 총 미션 수가 12 초과일 땐 남은 미션 수의 비율이 (총 미션 수 대비) 20% 이하인 플레이 비율에 해당합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;figure&gt;
    &lt;p align=&quot;center&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2368px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 30.555555555555554%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAABYlAAAWJQFJUiTwAAABX0lEQVR42j2R227UMBBA8/+vfAKvCHireKCtoKCWVi0LoSybbQJkc9kkziapc7WTwyxUWBodjcdzpBk7y7Kgdc+hbskPDVnVEClFWCgOXUfdD3+ZDxptRuwMk4SZF4knyuUsHjvPOMZYklITFBV+VrGNCu79PTduyPVqy8oLWK1/4m4CfkcZUXIg2pUkcSlUxMJUTejKpWs2OFp37FTDOsnZyuP1LuXLD5fTkzMuTt5wd/6Bz+++4rs+YRATiyxKZYp9Q5Jr4uyRpJYpv19hwm84jyIsqkGkLQ+/Um7PPnLx/BVvn73m8sUV7nsPb50RJppE9UR5J+yI85a47CRvyTpDeO6jVjGOtZZMCum+Znu94fblDfeXD3huROAVJEXLvuyFIimeZOofj7WjUHUTn053eHc5DnIm2e4wjBKGYbJMdpHFz4yy32Ne1pq2l/poGOVH+uFI+5/DJH2jxZiZP1mDwB5VCxwcAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/bcffa5e56b649a54bc25658b725d1e49/33ecc/kpi2.webp 360w,
/static/bcffa5e56b649a54bc25658b725d1e49/263a4/kpi2.webp 480w,
/static/bcffa5e56b649a54bc25658b725d1e49/0c8fb/kpi2.webp 640w,
/static/bcffa5e56b649a54bc25658b725d1e49/521f7/kpi2.webp 860w,
/static/bcffa5e56b649a54bc25658b725d1e49/cbd37/kpi2.webp 1200w,
/static/bcffa5e56b649a54bc25658b725d1e49/c4540/kpi2.webp 2368w&quot; sizes=&quot;(max-width: 2368px) 100vw, 2368px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/bcffa5e56b649a54bc25658b725d1e49/53918/kpi2.png 360w,
/static/bcffa5e56b649a54bc25658b725d1e49/9aebd/kpi2.png 480w,
/static/bcffa5e56b649a54bc25658b725d1e49/0f09e/kpi2.png 640w,
/static/bcffa5e56b649a54bc25658b725d1e49/3e3fe/kpi2.png 860w,
/static/bcffa5e56b649a54bc25658b725d1e49/64756/kpi2.png 1200w,
/static/bcffa5e56b649a54bc25658b725d1e49/8e903/kpi2.png 2368w&quot; sizes=&quot;(max-width: 2368px) 100vw, 2368px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/bcffa5e56b649a54bc25658b725d1e49/8e903/kpi2.png&quot; alt=&quot;kpi2&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;/p&gt;
&lt;/figure&gt;
&lt;p&gt;하지만 총 미션 수가 너무 적은 경우(초록색 원) 경향에서 벗어나는 케이스가 다수 발생했습니다. (색이 어두울 수록 총 미션 수가 적은 스테이지) 따라서 이를 보완하기 위해 총 미션 수가 너무 적을 때는 전체 분포 관점 지표를 사용하는 것으로 변경하였습니다. 최종적으로, 총 미션 수가 3개 이하일 때 전체 분포 관점 지표를 사용했을 때 상관관계가 0.56 ~ 0.57의 값을 보였습니다.&lt;/p&gt;
&lt;h2 id=&quot;스테이지-평가-지표-정의&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8A%A4%ED%85%8C%EC%9D%B4%EC%A7%80-%ED%8F%89%EA%B0%80-%EC%A7%80%ED%91%9C-%EC%A0%95%EC%9D%98&quot; aria-label=&quot;스테이지 평가 지표 정의 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;스테이지 평가 지표 정의&lt;/h2&gt;
&lt;p&gt;위의 과정으로부터 얻은 스테이지 평가 지표의 정의를 정리해보자면 다음과 같습니다. 아래에서 언급하는 &quot;클리어 실패&quot;란 제한된 무브 수를 모두 소모했음에도 남은 미션 수가 하나 이상인 경우를 의미합니다. 그리고 스테이지별로 총 미션 수가 각각 다르기 때문에 총 미션 수 기준으로 구분하여 정의합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;스테이지 평가 지표&lt;/strong&gt; = 아래 각 조건별로&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;총 미션 수가 3 이하인 스테이지&lt;/code&gt; :
&lt;ul&gt;
&lt;li&gt;전체적인 남은 미션 수의 분포가 얼마나 0에 가까운지 (전체 분포 관점 지표)
&lt;ul&gt;
&lt;li&gt;[ 남은 미션 수 별 가중치 x 플레이 카운트 ]의 합&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;계산 방법은 아래와 같습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;figure&gt;
    &lt;p align=&quot;center&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1330px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 40.833333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAABYlAAAWJQFJUiTwAAABQklEQVR42n1SyXaDMAzk/z+xbULCajBgFrMGmGpEc+glvCdkydZoNHawLgusLVGWBeq6gmsaFIXRXJalarpXVTAmR9/3+PQF8zyh61oFYWGeZ0jTROJrTZDo+cA4ehzHgX3fsW0b9tfr8hIfx46XxOd5IuDP+wFxFMEKIJldgEZB4yTWmEzfe01ToxLGdV1rbd93aNtWGwTs4L3HU1iUZalAJs+RpSkSASNDMs2zTOPb7Qd+GDBPEyYxAo5S7wePdV2vkQn2CEOE4R3OOR3PuQatrHlwkKJh6FU/MmmE2XtN3XmOfppGAs4XA7Eoesoo9t8FEZg56vwGaPTiCs1bmYo5a60S0ZHjOML9fsP395eyTJJEPTViQ461LLNotIpf/mwGp7vym+Z4MQrYdZ12pfDUiU+kbZ0a94wxCnp958dn8wtod2EL8mplpAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/accb5880624feb5a68e94b1f31f7b407/33ecc/kpi1_calculation.webp 360w,
/static/accb5880624feb5a68e94b1f31f7b407/263a4/kpi1_calculation.webp 480w,
/static/accb5880624feb5a68e94b1f31f7b407/0c8fb/kpi1_calculation.webp 640w,
/static/accb5880624feb5a68e94b1f31f7b407/521f7/kpi1_calculation.webp 860w,
/static/accb5880624feb5a68e94b1f31f7b407/cbd37/kpi1_calculation.webp 1200w,
/static/accb5880624feb5a68e94b1f31f7b407/2e315/kpi1_calculation.webp 1330w&quot; sizes=&quot;(max-width: 1330px) 100vw, 1330px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/accb5880624feb5a68e94b1f31f7b407/53918/kpi1_calculation.png 360w,
/static/accb5880624feb5a68e94b1f31f7b407/9aebd/kpi1_calculation.png 480w,
/static/accb5880624feb5a68e94b1f31f7b407/0f09e/kpi1_calculation.png 640w,
/static/accb5880624feb5a68e94b1f31f7b407/3e3fe/kpi1_calculation.png 860w,
/static/accb5880624feb5a68e94b1f31f7b407/64756/kpi1_calculation.png 1200w,
/static/accb5880624feb5a68e94b1f31f7b407/9bae8/kpi1_calculation.png 1330w&quot; sizes=&quot;(max-width: 1330px) 100vw, 1330px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/accb5880624feb5a68e94b1f31f7b407/9bae8/kpi1_calculation.png&quot; alt=&quot;kpi1 calculation&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;/p&gt;
&lt;/figure&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;총 미션 수가 4 이상 12 이하인 스테이지&lt;/code&gt; :
&lt;ul&gt;
&lt;li&gt;남은 미션 수가 2 이하인 플레이의 비율&lt;/li&gt;
&lt;li&gt;계산 예시
&lt;ul&gt;
&lt;li&gt;총 미션 수가 8개인 스테이지에서 (4 ≤ &lt;strong&gt;8&lt;/strong&gt; ≤ 12)&lt;/li&gt;
&lt;li&gt;실패한 플레이가 100번이고, 이 중 남은 미션 수가 2 이하인 플레이가 25번 있었다면&lt;/li&gt;
&lt;li&gt;스테이지 평가 지표는 25/100 = 0.25 로 계산&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;총 미션 수가 12 초과인 스테이지&lt;/code&gt; :
&lt;ul&gt;
&lt;li&gt;남은 미션 수가 총 미션 수 대비 20% 이하인 플레이의 비율&lt;/li&gt;
&lt;li&gt;계산 예시
&lt;ul&gt;
&lt;li&gt;총 미션 수가 100개인 스테이지에서 ( &lt;strong&gt;100&lt;/strong&gt; &gt; 12 )&lt;/li&gt;
&lt;li&gt;그리고 실패한 플레이가 100번 있을 때,&lt;/li&gt;
&lt;li&gt;남은 미션 수가 20개 (100개 * 0.2) 이하인 플레이가 30번이라면,&lt;/li&gt;
&lt;li&gt;스테이지 평가 지표는 30 / 100 = 0.3 으로 계산&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;보조-지표&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B3%B4%EC%A1%B0-%EC%A7%80%ED%91%9C&quot; aria-label=&quot;보조 지표 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;보조 지표&lt;/h3&gt;
&lt;p&gt;위에서 설계된 스테이지 평가 지표는 기본적으로 게임에 실패했을 때 남은 미션 수의 분포를 기준으로 합니다. 그런데 모든 실패한 경우에 남은 미션 수가 하나이거나, 남은 미션 수에 상관없이 한 무브만 더 사용하면 클리어할 수 있는 특성을 가지는 스테이지라면 어떻게 될까요? 당연히 남은 무브 수의 분포를 통해 만든 스테이지 평가 지표의 신뢰도가 떨어질 수밖에 없습니다. 이를 위해, 스테이지 평가 지표의 신뢰도를 표현할 수 있는 보조 지표를 설계하게 되었습니다.&lt;/p&gt;
&lt;p&gt;단, 이는 별도의 분석 과정이 아닌 논리적인 흐름에 따라 설계한 지표입니다. 처음 발단이 된 생각처럼, 이 보조 지표가 아주 낮을 때 스테이지 평가 지표를 신뢰하기 힘들 수 있다는 식으로 접근하는 것이 맞다고 생각되었습니다. 또한, 실제 런칭 이후 효과적인지에 대한 여부 판단이 필요합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;보조 지표&lt;/strong&gt; = 퍼즐봇 데이터 기준, &apos;제한된 무브 수 시점에서의 남은 미션 수&apos;와 &apos;모든 미션을 클리어할 때까지 추가로 소모된 무브 수&apos; 간의 상관관계&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pearson correlation (value based)와 Spearman correlation (rank based)의 평균값을 사용&lt;/li&gt;
&lt;li&gt;(참고) 퍼즐봇은 제한된 무브 수를 넘어서까지 게임을 플레이할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h1 id=&quot;결과&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%B0%EA%B3%BC&quot; aria-label=&quot;결과 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;결과&lt;/h1&gt;
&lt;p&gt;본 스테이지 평가 지표가 폴리싱 단계에서 사용되는 것을 목표한다면 실제로 스테이지들이 출시되기 전에 스테이지 평가 지표의 계산이 가능해야 합니다. 따라서 먼저 퍼즐봇의 플레이 데이터를 통해 스테이지 평가 지표를 계산하고, 사내 테스트 데이터에서 유저의 행동을 확인하는 과정을 통해 지표를 검증하였으며, 그 결과는 아래와 같습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;p align=&quot;center&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1454px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 29.72222222222222%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA5ElEQVR42pWRu3LDIBBF9f+fkipdupQpU6VM4cIghCJhMC8h4GaFyIxLpzhzd+8+BmaHUiv03UNph5txpLbpnus/KCgVKKVgyJTMtwixRkwH6ozHxT8FnwzNZbj1A95KDNYfC6jw1/DUonBCc0zeIUyF+3xD1rTQhw2zCuBUGGf7AOXSNOXd49PhaYp1e9k4KrCLhNQBl9cvePKGlDPE4sCZag0NvnaWppM49AcjI49JYga/SnDymNBQLuL95RuLcBgqHSVuGTEVBCJ2WrzTV+KOlQ5mQ8K291rvj+nURH3Gbu04v7qAyfjzuo9yAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/ab8ec29fa038b7dfa53ca02761e671d7/33ecc/kpi_all_stages.webp 360w,
/static/ab8ec29fa038b7dfa53ca02761e671d7/263a4/kpi_all_stages.webp 480w,
/static/ab8ec29fa038b7dfa53ca02761e671d7/0c8fb/kpi_all_stages.webp 640w,
/static/ab8ec29fa038b7dfa53ca02761e671d7/521f7/kpi_all_stages.webp 860w,
/static/ab8ec29fa038b7dfa53ca02761e671d7/cbd37/kpi_all_stages.webp 1200w,
/static/ab8ec29fa038b7dfa53ca02761e671d7/ebc82/kpi_all_stages.webp 1454w&quot; sizes=&quot;(max-width: 1454px) 100vw, 1454px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/ab8ec29fa038b7dfa53ca02761e671d7/53918/kpi_all_stages.png 360w,
/static/ab8ec29fa038b7dfa53ca02761e671d7/9aebd/kpi_all_stages.png 480w,
/static/ab8ec29fa038b7dfa53ca02761e671d7/0f09e/kpi_all_stages.png 640w,
/static/ab8ec29fa038b7dfa53ca02761e671d7/3e3fe/kpi_all_stages.png 860w,
/static/ab8ec29fa038b7dfa53ca02761e671d7/64756/kpi_all_stages.png 1200w,
/static/ab8ec29fa038b7dfa53ca02761e671d7/2a660/kpi_all_stages.png 1454w&quot; sizes=&quot;(max-width: 1454px) 100vw, 1454px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/ab8ec29fa038b7dfa53ca02761e671d7/2a660/kpi_all_stages.png&quot; alt=&quot;kpi all stages&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;/p&gt;
&lt;/figure&gt;
&lt;p&gt;위 차트에서 하나의 점은 하나의 스테이지를 의미하며, x 축은 스테이지 평가 지표 값, y 축은 기준이 되는 “유저의 행동” 비율을 의미합니다.&lt;/p&gt;
&lt;p&gt;실제로, 스테이지 평가 지표가 증가함에 따라 기준으로 삼은 유저의 행동 비율이 증가함을 볼 수 있었습니다. 또한, &lt;strong&gt;스테이지 평가 지표가 0.5 이상인 스테이지&lt;/strong&gt;들만 제한해서 본다면,&lt;/p&gt;
&lt;figure&gt;
    &lt;p align=&quot;center&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1427px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 30%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA+0lEQVR42o2RTW7EIAyFuf9dWqknaHe9QTdVpUrTgfxBSGJ+AmFeDVE13bWLp888gy0bkVKGsQS7Omi7wSyEzQWko/xP+UBmHiWyDggfMzoT0LMaZ6YOUJP/U3JwkD1htDvo8oToRohpJsianNwp7aC0P8UNlP6Rv9Pc4+tAGGwEvb6g0Aph7FlA/pIauUm/QQ0rc2EuLa6UPXuKz52FkgZfnyNP5/H+8IbAFOQTrvURJ5XSkHJictwtJ9nrOqacW1w9eRnPgnWiYeORA54fP7DOEaKUG8KesVelAzEmZsGeb4i5wPGO7RZQd1295nM+tjul3amfQ7Sj1voG1MbJfg6I9hEAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/070eb8655593d7e01184e81f17685f65/33ecc/kpi_over_0.5.webp 360w,
/static/070eb8655593d7e01184e81f17685f65/263a4/kpi_over_0.5.webp 480w,
/static/070eb8655593d7e01184e81f17685f65/0c8fb/kpi_over_0.5.webp 640w,
/static/070eb8655593d7e01184e81f17685f65/521f7/kpi_over_0.5.webp 860w,
/static/070eb8655593d7e01184e81f17685f65/cbd37/kpi_over_0.5.webp 1200w,
/static/070eb8655593d7e01184e81f17685f65/5e6d6/kpi_over_0.5.webp 1427w&quot; sizes=&quot;(max-width: 1427px) 100vw, 1427px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/070eb8655593d7e01184e81f17685f65/53918/kpi_over_0.5.png 360w,
/static/070eb8655593d7e01184e81f17685f65/9aebd/kpi_over_0.5.png 480w,
/static/070eb8655593d7e01184e81f17685f65/0f09e/kpi_over_0.5.png 640w,
/static/070eb8655593d7e01184e81f17685f65/3e3fe/kpi_over_0.5.png 860w,
/static/070eb8655593d7e01184e81f17685f65/64756/kpi_over_0.5.png 1200w,
/static/070eb8655593d7e01184e81f17685f65/569fb/kpi_over_0.5.png 1427w&quot; sizes=&quot;(max-width: 1427px) 100vw, 1427px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/070eb8655593d7e01184e81f17685f65/569fb/kpi_over_0.5.png&quot; alt=&quot;kpi over 0 5&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;/p&gt;
&lt;/figure&gt;
&lt;p&gt;스테이지 평가 지표와 유저의 행동 사이에 &lt;strong&gt;더 뚜렷한 상관관계&lt;/strong&gt;가 있음을 확인할 수 있었습니다. (Pearson, Spearman 상관관계 기준) 0.55~0.65 정도의 상관관계 값을 가졌습니다.&lt;/p&gt;
&lt;p&gt;즉, &lt;strong&gt;퍼즐봇 데이터로만 계산된 스테이지 평가 지표가 사내테스트 결과 유저의 행동과 유의미한 상관관계&lt;/strong&gt;를 보였습니다. 다만, 보조 지표의 경우 경향성을 충분히 확인하기 위한 데이터는 부족한 상태라고 결론지었습니다. 비교 대상 스테이지들 간에 &quot;스테이지 평가 지표의 수치가 비슷할 때에, 보조 지표의 값에 차이가 난다면 어떠한 현상이 있는가&quot;가 중요하므로 지금보다 더 많은 스테이지가 있을 때 확인이 가능할 것이라고 판단합니다.&lt;/p&gt;
&lt;h1 id=&quot;discussion&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#discussion&quot; aria-label=&quot;discussion permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Discussion&lt;/h1&gt;
&lt;p&gt;사내테스트 데이터 기준으로, &lt;strong&gt;퍼즐봇 데이터로 만든 스테이지 평가 지표와 유저의 행동 비율 사이의 상관관계가 유의미했습니다.&lt;/strong&gt; 이 결과를 바탕으로 기존의 프로젝트 수행 목적인 “폴리싱 단계에서의 도움”을 주기 위한 역할을 수행할 수 있을 것으로 생각됩니다.&lt;/p&gt;
&lt;p&gt;쿠키런: 퍼즐월드 데이터로 만든 스테이지 평가 지표와 유저의 행동 비율 사이의 상관관계와 거의 유사한 정도를 보였습니다. (다만 게임이지만) 유저 데이터로 만든 지표와 퍼즐봇 데이터로 만든 지표가 거의 유사한 정도의 성능을 보였다는 점은, 성공률을 떠나서 퍼즐봇의 플레이 패턴이 유저의 플레이 패턴과 유사한 지점이 있고 이는 곧 플레이 패턴이 맵의 고유한 특성 자체(맵, 기믹, 미션 등)에서 나올 수 있음을 시사합니다. 즉, &lt;strong&gt;더 좋은 맵&lt;/strong&gt;이라는 개념이 존재할 수 있을 것이라 생각됩니다.&lt;/p&gt;
&lt;h2 id=&quot;한계&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%9C%EA%B3%84&quot; aria-label=&quot;한계 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;한계&lt;/h2&gt;
&lt;p&gt;본 모델링 과정에는 한계가 다수 존재합니다. 퍼즐월드 유저 데이터를 기준으로 최적화한 지표가 마녀의 성 퍼즐봇 데이터에서도 똑같이 유의미할 것이라고 보장할 수 없습니다. 따라서 스테이지 평가 지표 계산 시 필요한 총 미션 수 기준, 남은 미션 개수 기준 등을 새롭게 최적화할 필요가 있습니다. 또한, 사내 테스트 데이터에 대한 검증 결과가 실제로도 유효할 것이라고 판단하기 어렵습니다. 유저의 행동 기준이 “결제”와 직접적으로 연관되어 있는 것에 비해, 사내 테스트는 결제에 대한 심리적 허들이 낮았을 것입니다. 또한 기준이 되었던 유저의 행동, “게임 실패 시 이어하기 및 하트 구매 후 재시도”가 분명 게임 운영 상 좋은 판단 기준이 될 수 있지만, 절대적이고 유일한 기준은 아닙니다. 이러한 유저의 행동을 100% 예측할 수 있다고 해서, 예측 결과가 높은 스테이지”만” 사용하는 것이 항상 좋다고 볼 수는 없습니다.&lt;/p&gt;
&lt;h3 id=&quot;마무리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EB%AC%B4%EB%A6%AC&quot; aria-label=&quot;마무리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;마무리&lt;/h3&gt;
&lt;p&gt;위와 같이 한계가 분명히 존재했던 스테이지 평가 지표의 제작 과정이었지만, 실험 과정에서 유의미하다고 판단될 정도의 결과를 확인할 수 있었습니다. 또한 이 지표의 제작은 마녀의 성 게임의 런칭 이전 시점에 이루어진 만큼, 지표 자체의 개선과 유저 데이터의 지속적인 모니터링을 통해 점진적으로 개선 중입니다.&lt;/p&gt;
&lt;p&gt;머신러닝 엔지니어로서 게임에 대한 이해도나 게임을 바라보는 시각에 한계가 있을 수 있다고 생각합니다. 하지만 오히려 그렇기 때문에 데이터의 관점으로 게임을 바라보는 시각은 마녀의 성 팀에게 큰 도움이 될 수 있었을 것이라 생각합니다. 부족한 부분도 많았지만, 직군과 역할에 상관없이 게임의 성공이라는 공통된 목적을 가지고 새로운 지표를 만들어낸 경험은 정말 뜻깊은 경험이었습니다. 이처럼 다양한 사람들의 노력이 들어간 쿠키런: 마녀의 성이 더 많은 사람들에게 사랑받을 수 있다면 그것만큼 보람찬 일도 없을 것입니다. 앞으로도 데브시스터즈와 쿠키런: 마녀의 성에 많은 관심 바랍니다!&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[세계 최초로 cert-manager 버그를 발견하고 해결하기]]></title><description><![CDATA[cert-manager의 버그로 의도와 다른 인증 체인이 사용되는 문제를 디버그하고, 업스트림에 기여한 과정을 소개합니다.]]></description><link>https://tech.devsisters.com/posts/finding-and-fixing-cert-manager-bug</link><guid isPermaLink="false">https://tech.devsisters.com/posts/finding-and-fixing-cert-manager-bug</guid><pubDate>Tue, 27 Feb 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, 데브시스터즈 진저랩 인프라셀에서 데브옵스 엔지니어로 일하고 있는 이상유입니다.&lt;/p&gt;
&lt;p&gt;여러분은 Let’s Encrypt를 알고 계시나요? 자세히 아는 것이 아니라도, SSL/TLS 인증서를 무료로 발급해 주는 서비스라고 많은 분들께서 한 번쯤은 들어 보았을 것이라고 생각합니다.&lt;/p&gt;
&lt;p&gt;데브시스터즈에서는 쿠버네티스 환경에서 통신 암호화를 위해 Let’s Encrypt 인증서를 사용하고 있는데요, 최근 흥미로운 이슈를 발견하여 오픈소스 기여까지 하게 된 사례가 있어 이 글을 통해 이야기를 풀어보고자 합니다.&lt;/p&gt;
&lt;h2 id=&quot;네가-왜-거기서-나와&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%84%A4%EA%B0%80-%EC%99%9C-%EA%B1%B0%EA%B8%B0%EC%84%9C-%EB%82%98%EC%99%80&quot; aria-label=&quot;네가 왜 거기서 나와 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;네가 왜 거기서 나와?&lt;/h2&gt;
&lt;p&gt;지난 2월 13일, 설 연휴를 보내고 돌아온 인프라셀에 한 가지 요청이 들어옵니다. 개발 서버에 접속이 되지 않는데, 확인을 부탁드린다는 요청이었습니다.&lt;/p&gt;
&lt;p&gt;연휴 직후였기 때문에 사람이 개입한 변경사항도 존재하지 않던 시점이었기에, 무엇이 문제일까 확인하던 도중 아래와 같은 화면을 보게 되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1164px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 41.388888888888886%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAABcSAAAXEgFnn9JSAAABX0lEQVR42l1R2W6DQBDjG5JAwpHQcN83JCHK/39RHxCV3PEqtGofrDHsznrs0eIo+mzq+quu6zWO49U0zXW/36+Hw+EH/N7tdgob/39ng6YbxlIUBZIkQZqm8H0feZ4jiiIU72oYBhzHgWWacGwb7uWC4/EIefgPRATa6XRaHvc77rcbxmHANI24TRP6vsfzOSsuDtC1LYahR1WV6iwIAiXOSlEOZIuYJhaXrmtRs6nr0DYNeqmN1FYeYbPneQik+Xr9UM0SjcJFJtV1XTkgyDXxvZRiORO7eZahLEtk77oJkFOAolVViUiHMAhxPp/Vo4yDnDFotmUtnGgcRzXRIBOVZaEiIJ/kP2PYzihA0PqWMR34vgeJ7zdDgs2v1wvz/FCZzQ+pEkfXdipPimxwXVfZ5CIIcaqgie8ly1Jw02EYKjBgZsRt0z7jSOUf70RyTs7JuJQoCtXdJIlhWza+AdyZ5hmxxLO2AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/0fe81d1dd9d492bbb8236fa66075fb45/33ecc/01-wrong-certificate.webp 360w,
/static/0fe81d1dd9d492bbb8236fa66075fb45/263a4/01-wrong-certificate.webp 480w,
/static/0fe81d1dd9d492bbb8236fa66075fb45/0c8fb/01-wrong-certificate.webp 640w,
/static/0fe81d1dd9d492bbb8236fa66075fb45/521f7/01-wrong-certificate.webp 860w,
/static/0fe81d1dd9d492bbb8236fa66075fb45/ae3d9/01-wrong-certificate.webp 1164w&quot;
              sizes=&quot;(max-width: 1164px) 100vw, 1164px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/0fe81d1dd9d492bbb8236fa66075fb45/53918/01-wrong-certificate.png 360w,
/static/0fe81d1dd9d492bbb8236fa66075fb45/9aebd/01-wrong-certificate.png 480w,
/static/0fe81d1dd9d492bbb8236fa66075fb45/0f09e/01-wrong-certificate.png 640w,
/static/0fe81d1dd9d492bbb8236fa66075fb45/3e3fe/01-wrong-certificate.png 860w,
/static/0fe81d1dd9d492bbb8236fa66075fb45/1d227/01-wrong-certificate.png 1164w&quot;
            sizes=&quot;(max-width: 1164px) 100vw, 1164px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/0fe81d1dd9d492bbb8236fa66075fb45/1d227/01-wrong-certificate.png&quot;
            alt=&quot;DST Root CA X3이 포함된 Chain을 사용하고 있는 Cert Chain 스크린샷&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;DST Root CA X3이 포함된 Chain을 사용하고 있는 Cert Chain 스크린샷&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;2년 전인 2021년 9월 말 인증서가 만료된 &lt;code&gt;DST Root CA X3&lt;/code&gt;가 서명한 인증서가 서빙되고 있었는데, 저희는 후술할 접속 불가 이슈 때문에 명시적으로 해당 체인을 사용하지 않도록 설정해 둔 상태였습니다.&lt;/p&gt;
&lt;p&gt;그렇다면 왜 2년 전에 만료된 CA 인증서를 Trust Anchor으로 하는 인증 체인이 지금, 그것도 명시적으로 해당 체인을 사용하지 않도록 설정한 곳에 나타난 걸까요? 그 이야기를 하기 위해서는 전후 맥락을 알아보아야 합니다.&lt;/p&gt;
&lt;h2 id=&quot;lets-encrypt의-chain-of-trust&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#lets-encrypt%EC%9D%98-chain-of-trust&quot; aria-label=&quot;lets encrypt의 chain of trust permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Let’s Encrypt의 Chain of Trust&lt;/h2&gt;
&lt;p&gt;2024년 2월 13일 현재, Let’s Encrypt의 Chain of Trust는 다음과 같습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 66.66666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAsTAAALEwEAmpwYAAACSElEQVR42nVTu27UUBDdb6JBoqNDKanTIBpKqBDUCNEgUUR0iAakgFgFgiIERSgoUlBEIQoQVvuw12/jx7Wv3/YeZu4+YFdhpNH1eDzHM2fO7UHZ7B8HqqpCkiRIhKBTQIgEaZpCSknPAnEcoyiKeeW8ZGW99XCezfMcnufB9Xz4dHoLD8MQjuPAth1I+sF/AdumwqytIbNCdZIkEl3XcQZVXSOnbmaLyprikuK6blCWpeqazxVgkcXQ9rcxfbOFn5+fIBQ5BqdH+N6/AXtvCz/e36FOPQLsVEFZFnj5+gC37u6QP8Vu/yOauvoL2HUtEuMrzLMPiNwhfgeRKsr8c2gn+/AmJ3D9CGEkFjymSGWG428DfDo8gj41UWTZirAVhw2N1HQz5JlEnRNoLuAQUEwLSQIDgashikIUZaWoyYhnEUcIqAHT8WCYFtq2nQPyOLOuUcDB6AuMV1egvbiE4fEB5OAt4r3LcN9dQ+6d4tnuIa5ev4ebtx8rkEc7fWxtP8D9h88Vv73NHZe5hKWfI/I0GiWhZZVoixBdJdTywijGr8EIpuWQvGpkRYlISGRZqRa3BshyGY3GGI4m0HQT4/FEbZiYUM5y0XUdpmlgMhnDMAxs2hogi9W2bbiuq9whZ+2laUKCjuEt3rMmWY++76uuWFFLWa0BMqnsyyS6mrhtsdTuvHjd53rd6HAJwFfKsizaXqxif3oGazpUm2XLSB78TRAEahqWEU/ATVw4MnPIY7D62QwrgO36BCTVT5umUTkG5ft+kf0BAKzV/USBUK0AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c65d551fce3c9932cc5a3ced30404938/33ecc/02-lets-encrypt-chain-of-trust.webp 360w,
/static/c65d551fce3c9932cc5a3ced30404938/263a4/02-lets-encrypt-chain-of-trust.webp 480w,
/static/c65d551fce3c9932cc5a3ced30404938/0c8fb/02-lets-encrypt-chain-of-trust.webp 640w,
/static/c65d551fce3c9932cc5a3ced30404938/521f7/02-lets-encrypt-chain-of-trust.webp 860w,
/static/c65d551fce3c9932cc5a3ced30404938/cbd37/02-lets-encrypt-chain-of-trust.webp 1200w,
/static/c65d551fce3c9932cc5a3ced30404938/548e7/02-lets-encrypt-chain-of-trust.webp 2400w,
/static/c65d551fce3c9932cc5a3ced30404938/53dbb/02-lets-encrypt-chain-of-trust.webp 3360w&quot; sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c65d551fce3c9932cc5a3ced30404938/53918/02-lets-encrypt-chain-of-trust.png 360w,
/static/c65d551fce3c9932cc5a3ced30404938/9aebd/02-lets-encrypt-chain-of-trust.png 480w,
/static/c65d551fce3c9932cc5a3ced30404938/0f09e/02-lets-encrypt-chain-of-trust.png 640w,
/static/c65d551fce3c9932cc5a3ced30404938/3e3fe/02-lets-encrypt-chain-of-trust.png 860w,
/static/c65d551fce3c9932cc5a3ced30404938/64756/02-lets-encrypt-chain-of-trust.png 1200w,
/static/c65d551fce3c9932cc5a3ced30404938/98e2c/02-lets-encrypt-chain-of-trust.png 2400w,
/static/c65d551fce3c9932cc5a3ced30404938/20759/02-lets-encrypt-chain-of-trust.png 3360w&quot; sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c65d551fce3c9932cc5a3ced30404938/98e2c/02-lets-encrypt-chain-of-trust.png&quot; alt=&quot;02 lets encrypt chain of trust&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        Ref: &lt;a href=&quot;https://letsencrypt.org/certificates/&quot;&gt;Let&apos;s Encrypt Chain of Trust&lt;/a&gt;
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;ECDSA key를 사용하는 인증서(파란색)들은 이 내용을 풀어나가는데 큰 관계가 없기 때문에, 여기에서는 RSA key를 사용하는 인증서(주황색)에 조금 더 주목해보겠습니다. 아래와 같이, 2가지의 Chain of Trust가 만들어지는 것을 알 수 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;DST Root CA X3&lt;/code&gt; → &lt;code&gt;ISRG Root X1&lt;/code&gt; → &lt;code&gt;Let&apos;s Encrypt R3&lt;/code&gt; → &lt;code&gt;Client Certificate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ISRG Root X1&lt;/code&gt; → &lt;code&gt;Let&apos;s Encrypt R3&lt;/code&gt; → &lt;code&gt;Client Certificate&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;특기할 만한 점은 바로, 앞서 만료되었다고 이야기했던 &lt;code&gt;DST Root CA X3&lt;/code&gt;이 아직도 Chain에 들어 있다는 점입니다. 만료된 인증서라면 두 체인 모두에서 Trust Anchor는 &lt;code&gt;ISRG Root X1&lt;/code&gt; 일 것인데, 왜 굳이 제거하지 않는 것일까요?&lt;/p&gt;
&lt;h2 id=&quot;무시할-수-없는-과거&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%B4%EC%8B%9C%ED%95%A0-%EC%88%98-%EC%97%86%EB%8A%94-%EA%B3%BC%EA%B1%B0&quot; aria-label=&quot;무시할 수 없는 과거 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;무시할 수 없는 과거&lt;/h2&gt;
&lt;p&gt;왜 &lt;code&gt;DST Root CA X3&lt;/code&gt;가 지금까지 체인에 포함되는지를 알기 위해서는 Let’s Encrypt의 역사를 조금 살펴보아야 합니다.&lt;/p&gt;
&lt;p&gt;2014년 11월 대중에 Let’s Encrypt가 처음으로 공개되었지만, 이 시점의 Let’s Encrypt는 아직 Trusted CA가 아니었습니다. 때문에 당시 이미 널리 신뢰받고 있던 IdenTrust로부터 Cross-Sign을 받아 인증서를 발급하는 한편, 자신들의 CA를 만들어 Trusted CA에 등록하는 절차를 밟아나갔습니다.&lt;/p&gt;
&lt;p&gt;그렇게 시간이 지나 2020년 말, IdenTrust로부터 서명받을 때 사용된 &lt;code&gt;DST Root CA X3&lt;/code&gt;인증서가 만료되기까지 1년 정도가 남은 시점이 되었습니다. 이 당시 Let’s Encrypt의 Root CA 인증서, &lt;code&gt;ISRG Root X1&lt;/code&gt;은 주요 소프트웨어에서 Trusted CA가 되어 있었습니다.&lt;/p&gt;
&lt;p&gt;하지만 그게 모든 기기에서 신뢰한다는 의미는 아니었습니다. Trusted CA가 소프트웨어 업데이트로만 갱신되는 경우, 업데이트 자체가 아예 발생하지 않았을 경우에는 아직 &lt;code&gt;ISRG Root X1&lt;/code&gt;이 신뢰받고 있지 않을 가능성이 있습니다.&lt;/p&gt;
&lt;p&gt;그리고 안타깝게도 위와 같은 사례로 인해 영향을 받는 경우는 적지 않았습니다. 안드로이드 7.1 미만에서 해당 문제가 발생하는데, 2020년 12월 기준 영향을 받는 사용자는 전체 안드로이드 사용자의 2/3 수준이었습니다. 무시할 수 없는 정도의 기기에서 문제가 발생하는 것이었기에, Let’s Encrypt 커뮤니티에서는 이를 해결할 수 있는 창의적인 방법을 고안해 내었습니다.&lt;/p&gt;
&lt;h2 id=&quot;root-ca-인증서와-trust-anchor&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#root-ca-%EC%9D%B8%EC%A6%9D%EC%84%9C%EC%99%80-trust-anchor&quot; aria-label=&quot;root ca 인증서와 trust anchor permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Root CA 인증서와 Trust Anchor&lt;/h2&gt;
&lt;p&gt;우리는 흔히 인증 체인의 가장 위에 있는 것을 Root CA 인증서라고 부르고, 이것 자체가 인증을 해 준다고 오해합니다. 하지만 실제로는 조금 다릅니다.&lt;/p&gt;
&lt;p&gt;실제로 인증 체인을 검증하는 데에는 Trust Anchor를 사용합니다. 이것은 일반적으로는 Root CA의 public key와 해당 Key의 Identity의 조합입니다. 우리가 Root CA의 인증서를 사용하는 것은, 이것이 Trust Anchor로서 필요한 모든 데이터를 다 가지고 있는 포맷이기 때문입니다. 그렇기에, 인증서 파일을 불러와서 Trust Anchor를 형성할 때 그 인증서의 만료일 검증을 하는 것은 구현체의 자율입니다. 인증서가 만료되었다는 것은 해당 Public Key와 Identity의 pair에 대한 서명의 유효기한이 다했다는 것이지, 우리가 필요로 하는 Public Key와 Identity가 더 이상 유효하지 않다는 의미는 아니니까요.&lt;/p&gt;
&lt;p&gt;때문에 Trust Anchor으로 사용되는 CA 인증서의 만료일을 검증하는 구현체도 있고, 아닌 구현체도 있는데, &lt;a href=&quot;https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/security/net/config/CertificatesEntryRef.java;drc=2e50991320cbef77d3e8504a4b284adae8c2f4d2;l=37&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;안드로이드의 경우&lt;/a&gt;에는 이를 검증하지 않습니다. 적어도 안드로이드에 한해서는, &lt;code&gt;DST Root CA X3&lt;/code&gt; 라는 Trust Anchor는 업데이트 등으로 인증서 자체가 제거되지 않는 이상 영구히 유효한 것입니다.&lt;/p&gt;
&lt;p&gt;이러한 창의적인 방법을 사용해서, 만료되는 &lt;code&gt;DST Root CA X3&lt;/code&gt;이 &lt;code&gt;ISRG Root X1&lt;/code&gt;에 서명하여 인증 체인을 구성한 것이 위 (1)번 체인입니다. 이 cross-sign은 DST Root CA X3이 만료되는 21년 9월부터 3년간, 즉 2024년 9월까지 유효하도록 설정되었습니다.&lt;/p&gt;
&lt;p&gt;반대로 CA 인증서의 만료일을 검사하는 경우에는 동작이 조금 다릅니다. 이들의 경우에는 &lt;code&gt;DST Root CA X3&lt;/code&gt;은 더 이상 유효한 Trust Anchor가 아니지만, 그 하위에 &lt;code&gt;ISRG Root X1&lt;/code&gt; 이 있고 이는 Trust Anchor에 포함되어 있습니다. 때문에 일반적인 경우 (1)번 체인의 인증서를 내려보내도 (2)번 체인처럼 동작하여 정상적으로 신뢰 체인을 구성합니다. 일반적인 경우라면요.&lt;/p&gt;
&lt;p&gt;문제는 일반적이지 않은 경우가 있다는 것입니다. 앞에서 &lt;code&gt;접속 불가 이슈가 있었다&lt;/code&gt;는 이야기를 기억하시나요?
구버전 boringSSL의 경우 기본 설정상 (1)번 체인을 받았을 때 &lt;code&gt;DST Root CA X3&lt;/code&gt;이 valid하지 않다면 무조건 신뢰 체인 구성에 실패합니다. &lt;a href=&quot;https://boringssl.googlesource.com/boringssl/+/8f5eb80b810ff63d14ad3535cb16f7cb8271a4f5&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;이 동작을 다른 구현체와 유사하게 설정하는 커밋&lt;/a&gt;이 있지만, boringSSL과 같은 라이브러리는 보통 직접 사용하는 경우가 드물죠. 저희같은 경우에는 사용중인 라이브러리에서 boringSSL 구버전을 사용하여 문제가 발생하였습니다. 다행히도, 이 케이스에서는 가장 상위 계층의 인증서가 &lt;code&gt;DST Root CA X3&lt;/code&gt;에 의해 서명되지 않도록 하여 문제를 해결할 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;문제로-돌아와서&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%B8%EC%A0%9C%EB%A1%9C-%EB%8F%8C%EC%95%84%EC%99%80%EC%84%9C&quot; aria-label=&quot;문제로 돌아와서 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;문제로 돌아와서&lt;/h2&gt;
&lt;p&gt;이러한 접속 불가 히스토리가 있었기 때문에, 저희는 cert-manager에서 인증서의 인증 체인을 명시적으로 (2)번 체인인&lt;code&gt;preferredChain: ISRG Root X1&lt;/code&gt;으로 설정하여 사용하고 있었습니다.&lt;/p&gt;
&lt;p&gt;그런데 지금 보이는 동작은 이 &lt;code&gt;preferredChain&lt;/code&gt; 설정값이 무시되고 &lt;code&gt;DST Root CA X3&lt;/code&gt; 을 포함하는 (1)번 체인을 사용하는 동작입니다.&lt;/p&gt;
&lt;p&gt;테스트 결과, &lt;code&gt;preferredChain&lt;/code&gt; 설정값에 따라 다음과 같이 동작이 달라지는 것을 확인하였습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;preferredChain&lt;/code&gt;이 아예 설정되지 않는다면 &lt;code&gt;ISRG Root CA X1&lt;/code&gt; 인증서 체인을 사용&lt;/li&gt;
&lt;li&gt;&lt;code&gt;preferredChain: DST Root CA X3&lt;/code&gt;이라면 정상적으로 &lt;code&gt;DST Root CA X3&lt;/code&gt; 인증서 체인을 사용&lt;/li&gt;
&lt;li&gt;&lt;code&gt;preferredChain: ISRG Root X1&lt;/code&gt;이라면 &lt;code&gt;DST Root CA X3&lt;/code&gt; 인증서 체인을 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;명시적으로 &lt;code&gt;preferredChain: ISRG Root X1&lt;/code&gt;을 지정하는데도 &lt;code&gt;DST Root CA X3&lt;/code&gt; 체인을 사용하는데, &lt;code&gt;preferredChain&lt;/code&gt; 을 설정하지 않는다고 늘 &lt;code&gt;ISRG Root CA X1&lt;/code&gt;체인을 반환할 것이라고 확신할 수 없다고 판단하였습니다. 그렇다면 왜 이런 문제가 발생하는지 찾아봐야겠지요.&lt;/p&gt;
&lt;h2 id=&quot;안녕-dst-root-ca-x3&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%95%88%EB%85%95-dst-root-ca-x3&quot; aria-label=&quot;안녕 dst root ca x3 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;안녕, DST Root CA X3&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;이 섹션의 내용에 대해 더 자세하게 알고 싶으시다면, &lt;a href=&quot;https://letsencrypt.org/2023/07/10/cross-sign-expiration&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Let’s Encrypt의 공식 블로그&lt;/a&gt;를 참고해 주세요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;앞서 이야기했던 Cross-sign의 유효기한은 2024년 9월 30일까지입니다. 원래 2021년 9월까지 서명했던 것을 하위 호환성 문제로 인해 3년 연장했던 것이고, 이제는 문제가 되는 기기의 점유율이 많이 줄어 (안드로이드 점유율 중 6% 수준) 더 이상 이 cross sign의 필요성이 크지 않습니다.&lt;/p&gt;
&lt;p&gt;cross sign 인증서를 매번 통신을 수립할 때마다 서버에서 클라이언트에 내려주는 것도 불필요한 오버헤드이기 때문에, 더 이상 cross-sign을 하지 않고 &lt;code&gt;ISRG Root X1&lt;/code&gt;을 사용하는 것이 목표인데, 그렇다고 한순간에 이걸 발급하지 않아버리면 혼란이 올 가능성이 높겠죠.&lt;/p&gt;
&lt;p&gt;그래서 Let’s Encrypt에서는 단계적인 &lt;code&gt;DST Root CA X3&lt;/code&gt; deprecate 계획을 수립해 두었는데, 그 첫 번째 페이즈가 지난 2월 8일 적용되었습니다. 구체적으로, 기존에는 &lt;code&gt;DST Root CA X3&lt;/code&gt;이 Default chain이었고, &lt;code&gt;ISRG Root X1&lt;/code&gt;이 Alternate Chain 인증서로 제공되었는데, 이를 2월 8일부터 &lt;code&gt;ISRG Root X1&lt;/code&gt;을 Default Chain으로 변경하고 &lt;code&gt;DST Root CA X3&lt;/code&gt; 를 Alternate으로 제공하도록 변경되었습니다.&lt;/p&gt;
&lt;p&gt;시기적으로 이 변경사항이 문제를 발생시킨 원인으로 가장 의심됩니다. 하지만 Default Chain과 Alternate Chain이 무엇이길래 지금 겪는 문제를 발생시키는지 조금 더 깊이 확인해 볼 필요가 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;automated-certificate-management-environment&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#automated-certificate-management-environment&quot; aria-label=&quot;automated certificate management environment permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Automated Certificate Management Environment&lt;/h2&gt;
&lt;p&gt;Default Chain과 Alternate Chain이 어떻게 내려오는지 알려면 인증서 발급 과정이 실제로 어떻게 이루어지는지를 보아야 합니다. Let’s Encrypt와 같은 자동 인증서 발급 서비스는 ACME(Automated Certificate Management Environment) API를 사용하는데요, 이는 &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc8555&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;RFC 8555&lt;/a&gt;로 정의되어 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc8555#section-7.4.2&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;해당 문서 7.4.2번 섹션&lt;/a&gt;을 보면, ACME 서버에서는 동일한 end-entity 인증서(=Let’s Encrypt에서의 Subscriber Certificate)를 포함하여 서로 다른 인증 체인을 사용하는 &lt;code&gt;alternate&lt;/code&gt; 인증서를 지정할 수 있다고 되어 있습니다. 이 중 어떤 인증서를 사용할 것인지는 각 클라이언트의 재량입니다.&lt;/p&gt;
&lt;p&gt;실제로 해당 방식으로 통신이 이루어지는지 확인하기 위해, Let’s Encrypt 커뮤니티에서 가장 폭넓게 사용되는 Certbot을 사용하여 새 인증서를 발급하여 봅니다.&lt;/p&gt;
&lt;p&gt;Certbot은 기본적으로 디버그 로그를 남기는데, 이 안에서 ACME 서버와 통신하는 HTTP request와 response를 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;로그를 확인하면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1112px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 35.833333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAABcSAAAXEgFnn9JSAAAA/klEQVR42p3Qy07DMBAFUP8Aj9ouzoMkThNHTloqNRSkhEXF4/8/hqyIhC8TUwW6QnRxdD3W2NIMu2/bj8qYcd003nQ2phxzrcc4jsc4inxGU5IppZQngiDwPUqpka03d26/f8DL8yv6/gld10HrHBeXV1gsuHftU8w15xJc/MLFdw8lezu07tBv8bgz2G002m2Opopgy4Aon7Uhx2xItVqiyPjM5PLYq8AqU7h1Y1GsNJLbEDpLEKjln9SN/KHkfM+UCj5pfke7cdbWjsZ1NJYTQp6FJUnijDEoyxJZliFNU4RhCFr2WRg9ftdaD3VdD/TxYK0dpppzPggh/u0LhIu2GfSHxZsAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/542435d3a640747e0507a9d2aa1e9756/33ecc/03-isrg-root-x1-is-topmost-issuer-cn.webp 360w,
/static/542435d3a640747e0507a9d2aa1e9756/263a4/03-isrg-root-x1-is-topmost-issuer-cn.webp 480w,
/static/542435d3a640747e0507a9d2aa1e9756/0c8fb/03-isrg-root-x1-is-topmost-issuer-cn.webp 640w,
/static/542435d3a640747e0507a9d2aa1e9756/521f7/03-isrg-root-x1-is-topmost-issuer-cn.webp 860w,
/static/542435d3a640747e0507a9d2aa1e9756/81cde/03-isrg-root-x1-is-topmost-issuer-cn.webp 1112w&quot;
              sizes=&quot;(max-width: 1112px) 100vw, 1112px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/542435d3a640747e0507a9d2aa1e9756/53918/03-isrg-root-x1-is-topmost-issuer-cn.png 360w,
/static/542435d3a640747e0507a9d2aa1e9756/9aebd/03-isrg-root-x1-is-topmost-issuer-cn.png 480w,
/static/542435d3a640747e0507a9d2aa1e9756/0f09e/03-isrg-root-x1-is-topmost-issuer-cn.png 640w,
/static/542435d3a640747e0507a9d2aa1e9756/3e3fe/03-isrg-root-x1-is-topmost-issuer-cn.png 860w,
/static/542435d3a640747e0507a9d2aa1e9756/78ab6/03-isrg-root-x1-is-topmost-issuer-cn.png 1112w&quot;
            sizes=&quot;(max-width: 1112px) 100vw, 1112px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/542435d3a640747e0507a9d2aa1e9756/78ab6/03-isrg-root-x1-is-topmost-issuer-cn.png&quot;
            alt=&quot;ISRG Root X1을 최상위 인증서로 사용하는 체인&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;ISRG Root X1을 최상위 인증서로 사용하는 체인&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ISRG Root X1&lt;/code&gt;을 최상위 인증서의 Issuer Common Name으로 하는 인증서 체인이 하나 내려오고, &lt;code&gt;Link&lt;/code&gt; 헤더를 통해 alternate 인증서가 내려오는 것을 확인할 수 있습니다&lt;/p&gt;
&lt;p&gt;이어지는 로그에서 Alternate 인증서를 불러오는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1139px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 43.05555555555555%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAABcSAAAXEgFnn9JSAAABA0lEQVR42qWRy3aDIBBA+YOKiloFHyAmVLNIT7oyXbX9//+ZzZShxiRdmEUW9wyMw3UGmDEGuq6D3TBA2zRgtIbBWqiVAiklqCUO5hXGoQDbpWCaeIFD34q1TggB7ON0wp/vL/w8z3ieZ3TOYRRFGMdxgC9EPMaXyK954vfpHaGOc0ySBNn78YjENI44TRNaa7GqqoCUhAwoQvm1z4s0vSIElmXpvynM8xzZwUu01msy8wVZlm2SetEtlzzJWdPUQeTcWxiX2r4tpv2F/6K7uiUy/yjoH2UdsyiKzYOPYHQ/dG9934dIP3hKqH13JKrrv9Gpy0fjbQpJ5Pb7tbu2bZ8S/gJg6+gszybDbgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/810998c89aff4d3ffff9cb5525b82f7e/33ecc/04-dst-root-ca-x3-is-topmost-issuer-cn.webp 360w,
/static/810998c89aff4d3ffff9cb5525b82f7e/263a4/04-dst-root-ca-x3-is-topmost-issuer-cn.webp 480w,
/static/810998c89aff4d3ffff9cb5525b82f7e/0c8fb/04-dst-root-ca-x3-is-topmost-issuer-cn.webp 640w,
/static/810998c89aff4d3ffff9cb5525b82f7e/521f7/04-dst-root-ca-x3-is-topmost-issuer-cn.webp 860w,
/static/810998c89aff4d3ffff9cb5525b82f7e/ba747/04-dst-root-ca-x3-is-topmost-issuer-cn.webp 1139w&quot;
              sizes=&quot;(max-width: 1139px) 100vw, 1139px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/810998c89aff4d3ffff9cb5525b82f7e/53918/04-dst-root-ca-x3-is-topmost-issuer-cn.png 360w,
/static/810998c89aff4d3ffff9cb5525b82f7e/9aebd/04-dst-root-ca-x3-is-topmost-issuer-cn.png 480w,
/static/810998c89aff4d3ffff9cb5525b82f7e/0f09e/04-dst-root-ca-x3-is-topmost-issuer-cn.png 640w,
/static/810998c89aff4d3ffff9cb5525b82f7e/3e3fe/04-dst-root-ca-x3-is-topmost-issuer-cn.png 860w,
/static/810998c89aff4d3ffff9cb5525b82f7e/7a863/04-dst-root-ca-x3-is-topmost-issuer-cn.png 1139w&quot;
            sizes=&quot;(max-width: 1139px) 100vw, 1139px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/810998c89aff4d3ffff9cb5525b82f7e/7a863/04-dst-root-ca-x3-is-topmost-issuer-cn.png&quot;
            alt=&quot;DST Root CA X3을 최상위 인증서로 사용하는 체인&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;DST Root CA X3을 최상위 인증서로 사용하는 체인&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;다음과 같이 최상위 인증서의 &lt;code&gt;Issuer Common Name&lt;/code&gt;이 &lt;code&gt;DST Root CA X3&lt;/code&gt;인 인증서 체인이 내려옵니다.&lt;/p&gt;
&lt;p&gt;여기까지의 분석을 통해, Let’s Encrypt에서는 의도한 대로 인증서를 발급하고 있음을 확인할 수 있었습니다. 그렇다면 이제는 이 인증서를 받아와 실제로 secret을 만들어주는 클라이언트를 살펴볼 때입니다.&lt;/p&gt;
&lt;h2 id=&quot;구현별로-서로-다른-휴리스틱&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B5%AC%ED%98%84%EB%B3%84%EB%A1%9C-%EC%84%9C%EB%A1%9C-%EB%8B%A4%EB%A5%B8-%ED%9C%B4%EB%A6%AC%EC%8A%A4%ED%8B%B1&quot; aria-label=&quot;구현별로 서로 다른 휴리스틱 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;구현별로 서로 다른 휴리스틱&lt;/h2&gt;
&lt;p&gt;ACME 클라이언트는 Certbot이나 cert-manager 이외에도 &lt;code&gt;acme.sh&lt;/code&gt; 등이 존재하고, &lt;code&gt;traefik&lt;/code&gt;이나 &lt;code&gt;caddy&lt;/code&gt;와 같은 HTTP reverse proxy 소프트웨어에서 자체적으로 제공하고 있기도 합니다.&lt;/p&gt;
&lt;p&gt;여기에서는 &lt;code&gt;certbot&lt;/code&gt;과 지금 인프라셀에서 사용중인 &lt;code&gt;cert-manager&lt;/code&gt;를 서로 비교하여 보겠습니다.&lt;/p&gt;
&lt;p&gt;Certbot에서 Preferred Chain을 고르는 로직은 다음과 같습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1426px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 76.94444444444444%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAABYlAAAWJQFJUiTwAAACtUlEQVR42lVT2XLaQBD0g40PDqELXasLISQEEjFgbFecSv7/pzo9o9iuPHTtIW3P0T03YdFg7sZw/BS+abFKW3hJAy+uUe5O6C4f2A4vaIczyu6EdHcZsR3gmg2enBj31goPy0Bx4/FSNlMvRda/Ie2uiJtnhPURSXuG6V6Q9+8wuxek+yvvLtxfEVYD8sM7Iv63CPIvUhJWeLRDEiawk43CSbcKN9vqeUm4PPvFjujg5TvYccXvDaywYJbRd4Z+usGjZmg0mjwKNwMcsx2RjqSOqbGM10pi/wsgRJO593/JQd4o4WyVoTx+wLDMbP/K0l9Y+gnZ4U1XIfIy9nh9UDIvb/UssKIS9wt/JIzKViNJlM8M5MGq3OtjKTHgKn2Sh1ZYYu5nep4HGZHr3dQ3eCDHjZ/VY8lUecykhc3y5kGhAYREVu2Z9JbfFuybFY3fFiT7vJOkVJQHiiLWWVHxOUu3GNmOckzdCLdzB3dzF3czF7czW1cpb7LwtH+ThavfJbvHL1FEJf5kWL74q6xqnJ+P2G1bxKaDKXqYdYck6xGXnf4/dRLMXIOZlxMp7kg4kQzF2NLDO0aLuR+uH9j0Z2wOZ+wpUv/8C8fzH7TdTxyG3+iGD5TNhesv9MMrfnQ99rsOTZUzePpNKPVP7QBFsUXTDkibIyz2zs8qgrZJKqzYbxFDhkFaNfUilunjyV5h4YaYOaHYZqvGnsn4xVRxlXKlICFVpNkXvpSVKKYMPO5l3KSPhBVoMrJXUdxkPRISRX1Afbig3nPM6gFLqmgzy6XapaChK81QziLeiPxr/+Qm36MnPZwxG4+Pln4J05xpo7NOj9lddJbF8HF7Gueadzln2/AcVD0SWk7ItYdTlmuxjDjK2Js1kqpDcbiqqR2OmUyFjKTOM+dY4NEd8p/YSzQQDrHZX3i3i9DPUD5JAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/6b365849723da42e416dc9c15dae74be/33ecc/05-preferred-chain-logic-in-certbot.webp 360w,
/static/6b365849723da42e416dc9c15dae74be/263a4/05-preferred-chain-logic-in-certbot.webp 480w,
/static/6b365849723da42e416dc9c15dae74be/0c8fb/05-preferred-chain-logic-in-certbot.webp 640w,
/static/6b365849723da42e416dc9c15dae74be/521f7/05-preferred-chain-logic-in-certbot.webp 860w,
/static/6b365849723da42e416dc9c15dae74be/cbd37/05-preferred-chain-logic-in-certbot.webp 1200w,
/static/6b365849723da42e416dc9c15dae74be/50e10/05-preferred-chain-logic-in-certbot.webp 1426w&quot; sizes=&quot;(max-width: 1426px) 100vw, 1426px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/6b365849723da42e416dc9c15dae74be/53918/05-preferred-chain-logic-in-certbot.png 360w,
/static/6b365849723da42e416dc9c15dae74be/9aebd/05-preferred-chain-logic-in-certbot.png 480w,
/static/6b365849723da42e416dc9c15dae74be/0f09e/05-preferred-chain-logic-in-certbot.png 640w,
/static/6b365849723da42e416dc9c15dae74be/3e3fe/05-preferred-chain-logic-in-certbot.png 860w,
/static/6b365849723da42e416dc9c15dae74be/64756/05-preferred-chain-logic-in-certbot.png 1200w,
/static/6b365849723da42e416dc9c15dae74be/0f849/05-preferred-chain-logic-in-certbot.png 1426w&quot; sizes=&quot;(max-width: 1426px) 100vw, 1426px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/6b365849723da42e416dc9c15dae74be/0f849/05-preferred-chain-logic-in-certbot.png&quot; alt=&quot;05 preferred chain logic in certbot&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        Ref: &lt;a href=&quot;https://github.com/certbot/certbot/blob/f4e031f5055fc6bf8c87eb0b18f927f7f5ba36a8/certbot/certbot/crypto_util.py#L619&quot;&gt;Github Certbot Repository&lt;/a&gt;
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;각 인증서 체인에서, 가장 마지막 (인증 체인에서 root와 가장 가까운) 인증서를 골라 해당 인증서 체인에서 Issuer의 Common Name을 추출, 이를 입력받은 값과 비교하는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;반면 cert-manager는 구현이 조금 다른데요,&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1978px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 60.55555555555555%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAABYlAAAWJQFJUiTwAAAB80lEQVR42o1SyY7bMBTLJY2TeLck2/K+JE6a6SwtChTo/38Xy6csndzmQOhZgCmSj6usmhCmJd5eLvj75zdOl58Yl1/oD28w9RFFf0Q5LEjKDknRIcobZNVAjJxb3vcPhKbBSoZ1qLHPKqR2grIzonJGWnIuD0j5YFrL3YggJ2E5ICwGBKZFoCp84793bCKDlby0iXOEykLXE4ruCMO7erogI8k+s/B1jb2qsU0LosQutdgmJVHAi40j8sghWMW0IcOOMM2EbnlF1Z9o8wJVHal0gKLCsOgd+U6Q3s5P8OlQHnCWhVBU7ngRUo2xA2LaSqlU1wfmMyK2gsmdYjty522mKHHqM4KHQiH0acfQdj2/wNL6fHpH2X+n9cnF4UUaaz/DOrjBV7dZMUNF6/q/QkfKLLZJToIeeUNl1cEpNWK9km8uSxRz+wlPN8sdFxfoxll/JhTbVNmMZxTjK/T8gWL4gXYiTh8w3eI2njRHZO2ZGR+Qcb5GQTCCJ8I7hNQOZ7TLOyz72PNsSKqa+dY5ZkyIyoQOfHVtgU+Vq/jTUjZ3Qi7HMjPdnWCZp24WLqtzP0iFAhZ4r1v4eU+rLQtNsKMCp3ATsYdZiZjNl5d2rMDalVV9Ctw8wXv61o95lZJQ+uOztKFId+FW8Lgcj/fX8uZfxj8Kzy7oE1jlxAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/dc242a28dc0639f166ffa935b02b61f6/33ecc/06-preferred-chain-logic-in-cert-manager.webp 360w,
/static/dc242a28dc0639f166ffa935b02b61f6/263a4/06-preferred-chain-logic-in-cert-manager.webp 480w,
/static/dc242a28dc0639f166ffa935b02b61f6/0c8fb/06-preferred-chain-logic-in-cert-manager.webp 640w,
/static/dc242a28dc0639f166ffa935b02b61f6/521f7/06-preferred-chain-logic-in-cert-manager.webp 860w,
/static/dc242a28dc0639f166ffa935b02b61f6/cbd37/06-preferred-chain-logic-in-cert-manager.webp 1200w,
/static/dc242a28dc0639f166ffa935b02b61f6/1e99c/06-preferred-chain-logic-in-cert-manager.webp 1978w&quot; sizes=&quot;(max-width: 1978px) 100vw, 1978px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/dc242a28dc0639f166ffa935b02b61f6/53918/06-preferred-chain-logic-in-cert-manager.png 360w,
/static/dc242a28dc0639f166ffa935b02b61f6/9aebd/06-preferred-chain-logic-in-cert-manager.png 480w,
/static/dc242a28dc0639f166ffa935b02b61f6/0f09e/06-preferred-chain-logic-in-cert-manager.png 640w,
/static/dc242a28dc0639f166ffa935b02b61f6/3e3fe/06-preferred-chain-logic-in-cert-manager.png 860w,
/static/dc242a28dc0639f166ffa935b02b61f6/64756/06-preferred-chain-logic-in-cert-manager.png 1200w,
/static/dc242a28dc0639f166ffa935b02b61f6/56e71/06-preferred-chain-logic-in-cert-manager.png 1978w&quot; sizes=&quot;(max-width: 1978px) 100vw, 1978px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/dc242a28dc0639f166ffa935b02b61f6/56e71/06-preferred-chain-logic-in-cert-manager.png&quot; alt=&quot;06 preferred chain logic in cert manager&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        Ref: &lt;a href=&quot;https://github.com/cert-manager/cert-manager/blob/57de0fc4d2e1e6856600773a4ab4605ed50ceab0/pkg/controller/acmeorders/sync.go#L703&quot;&gt;Github cert-manager Repository&lt;/a&gt;
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;cert-manager의 경우 default chain을 제외한 alt chain 중에서, 각 체인의 모든 인증서를 순회하며 Issuer CN을 추출, 이를 입력받은 값과 비교하는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;로직을 분석함으로서 두 가지 디자인 선택이 cert-manager에서 잘못된 인증서 체인을 선택하도록 하는 문제를 만들었다는 것을 알 수 있었습니다.&lt;/p&gt;
&lt;p&gt;우선 &lt;code&gt;Alternate Chain을 선택할 때 Default Chain은 탐색 대상에서 제외한다&lt;/code&gt;입니다. 2월 8일 이전에는 Default Chain이 &lt;code&gt;DST Root CA X3&lt;/code&gt;이었기 때문에 &lt;code&gt;preferredChain&lt;/code&gt;이 &lt;code&gt;ISRG Root X1&lt;/code&gt;인 경우 정상적으로 의도한 인증서 체인이 발급되었습니다. 하지만 지금은 Default Chain이 &lt;code&gt;ISRG Root X1&lt;/code&gt;이기에, 이 체인의 인증서가 발급되기를 기대하였으나 &lt;code&gt;preferredChain&lt;/code&gt;을 지정할 경우 이 체인은 아예 탐색 대상이 되지 않습니다.&lt;/p&gt;
&lt;p&gt;다음으로, &lt;code&gt;모든 중간 인증서의 Issuer CN을 Alternate Chain 탐색 대상으로 삼는다&lt;/code&gt; 입니다. 지금 Alternate Chain인 &lt;code&gt;DST Root CA X3&lt;/code&gt; chain에도 &lt;code&gt;ISRG Root X1&lt;/code&gt;을 Issuer으로 가지는 인증서가 존재합니다. 때문에 &lt;code&gt;preferredChain&lt;/code&gt;을 &lt;code&gt;ISRG Root X1&lt;/code&gt;으로 설정한 경우 해당 인증서가 조건을 충족시켜 &lt;code&gt;DST Root CA X3&lt;/code&gt; 체인의 인증서를 반환하게 됩니다.&lt;/p&gt;
&lt;p&gt;결론적으로,&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Let’s Encrypt에서 하위 호환을 위해 제공하던 인증 체인을 deprecate하였고&lt;/li&gt;
&lt;li&gt;해당 변경사항이 &lt;code&gt;cert-manager&lt;/code&gt;에서 버그가 발생할 수 있는 조건을 트리거하였고&lt;/li&gt;
&lt;li&gt;해당 버그가 발생해도 일반적으로는 문제가 없지만, 하필이면 Trust Anchor 인증서의 유효기간을 검증하고 무조건 topmost 인증서를 Trust Anchor을 통해 인증하려고 하는 구현체를 사용하였기에&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;발견하게 된 버그라고 할 수 있겠습니다.&lt;/p&gt;
&lt;h2 id=&quot;첫-오픈소스-기여&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B2%AB-%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4-%EA%B8%B0%EC%97%AC&quot; aria-label=&quot;첫 오픈소스 기여 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;첫 오픈소스 기여&lt;/h2&gt;
&lt;p&gt;보통 제가 문제를 찾았을 때 즈음에는 누군가 이슈를 만들어 두던데, 이번 케이스에서는 아니었습니다. 문제를 확인했을 당시 Let’s Encrypt 커뮤니티 포럼이나 cert-manager 레포 중 어느 곳에도 리포팅이 되지 않은 상태였는데, 아마도 굉장히 특정한 상황에만 서비스 장애로 이어지는 문제였기에 영향을 받은 사람이 그리 많지 않았던 것으로 추정중입니다.&lt;/p&gt;
&lt;p&gt;문제 리포팅을 하려고 보니, 이미 코드 분석도 완료한 시점에서 그리 어려운 변경사항이 아니리라고 판단되었습니다. &lt;code&gt;그렇다면 굳이 이슈 만들어두고 수정될 때까지 기다리는 것보다, 내가 PR을 올리는 게 낫지 않을까?&lt;/code&gt; 하는 생각에, 바로 PR을 올려 보았습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2106px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 53.61111111111111%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAABYlAAAWJQFJUiTwAAAB3ElEQVR42l2SyY6jMBCGOWUgCXsAgzFg1oCN2SEh6mlNn+YJ5v0fZYrOraVPJVuq/S9p/PpD2XgfXoXY2PSqhr3q91JsZTuXYgHq4QFU3RJXoupWNu0ZG3HW2DiTyn+LSkoj6vSwUVFxPIgww8ZEsWKis+X/4HrDYGXDAyQHZzecGl50thBY9YZVJ9RdAk66F1tBqrlE1j3FQOCtQIx+hL0TSYVYqfhI25eXDigb3bRH2YDy0SRcx40RMrBaUL85vv4denZJfrEDSUdJ3i6UrQBKuWwTQLHCsxVcbhi4OtBF+EZzI9n0637zaSWbngS95e0asT3hr7B++OUc872ZP9nwKMWcNj2t+7Tu46qNSh4VXIHpUOKQQvfoEXzv10rMlZj49JyeELbBo1t/T/sXhKGkIgX36T3MmiCtFcO10sVv/wbjJmleDKrk7Xzv1mZ8ghJAmDPwgzrwAAtlYU5IEZXt2Q4QZbnYSFNL0EO/ffLplbMRsoCHQ3IzoAaKYfmmn1gBtYK3PYBdqIgYKLpaWLrYGKd1UgmwKClhhyDvxQY9j4WBYCDMSXPe/NLdk+b6lPF5ByTZQCRncDpwNzmfoH7Bx6jZXMocksFMqoMh5lvkQ+qT7uK44xNc4fIfQS9ScbeicLEAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/5574ea36fa7cea0d8f25b54db6e700d6/33ecc/07-pr-to-cert-manager.webp 360w,
/static/5574ea36fa7cea0d8f25b54db6e700d6/263a4/07-pr-to-cert-manager.webp 480w,
/static/5574ea36fa7cea0d8f25b54db6e700d6/0c8fb/07-pr-to-cert-manager.webp 640w,
/static/5574ea36fa7cea0d8f25b54db6e700d6/521f7/07-pr-to-cert-manager.webp 860w,
/static/5574ea36fa7cea0d8f25b54db6e700d6/cbd37/07-pr-to-cert-manager.webp 1200w,
/static/5574ea36fa7cea0d8f25b54db6e700d6/47a4f/07-pr-to-cert-manager.webp 2106w&quot; sizes=&quot;(max-width: 2106px) 100vw, 2106px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/5574ea36fa7cea0d8f25b54db6e700d6/53918/07-pr-to-cert-manager.png 360w,
/static/5574ea36fa7cea0d8f25b54db6e700d6/9aebd/07-pr-to-cert-manager.png 480w,
/static/5574ea36fa7cea0d8f25b54db6e700d6/0f09e/07-pr-to-cert-manager.png 640w,
/static/5574ea36fa7cea0d8f25b54db6e700d6/3e3fe/07-pr-to-cert-manager.png 860w,
/static/5574ea36fa7cea0d8f25b54db6e700d6/64756/07-pr-to-cert-manager.png 1200w,
/static/5574ea36fa7cea0d8f25b54db6e700d6/4cd40/07-pr-to-cert-manager.png 2106w&quot; sizes=&quot;(max-width: 2106px) 100vw, 2106px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/5574ea36fa7cea0d8f25b54db6e700d6/4cd40/07-pr-to-cert-manager.png&quot; alt=&quot;07 pr to cert manager&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        개인 계정이어서 프로필을 살짝 가렸습니다. 원본이 궁금하시다면 &lt;a href=&quot;https://github.com/cert-manager/cert-manager/pull/6755&quot;&gt;여기&lt;/a&gt;를 참고해 주세요.
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;리뷰를 기다리며 저는 제가 코드 컨벤션 같은 것을 잘 지켰는지 등을 걱정했는데요, 이는 기우로 끝났습니다.&lt;/p&gt;
&lt;p&gt;오히려 cert-manager의 메인테이너 분들은 사용자들이 기존 buggy한 구현에 의존하고 있을 가능성과 그로 인한 호환성 문제를 더 주의깊게 고려하시더라고요. 저는 생각지도 못한 부분이었고, 이런 고민을 한다는 것이 굉장히 인상깊었습니다.&lt;/p&gt;
&lt;p&gt;여담으로, 이 PR을 올리고 하루 뒤 동일한 이슈가 cert-manager 레포에 리포팅되고, 5일정도 지나서 Let’s Encrypt 커뮤니티에도 사례가 공유되었습니다. 저희가 문제를 좀 더 빠르게 경험한 것은 맞는 것 같습니다.&lt;/p&gt;
&lt;h2 id=&quot;wrap-up&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#wrap-up&quot; aria-label=&quot;wrap up permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Wrap-Up&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;cert-manager 사용 중, 설정값과 실제 발급된 인증서의 인증 체인이 다른 문제를 발견하여 분석하여 보았습니다.&lt;/li&gt;
&lt;li&gt;분석 결과 cert-manager에 존재하였으나 지금까지는 발현되지 않은 버그가 최근 Let’s Encrypt의 Default Certificate Chain 변경에 의해 발현되는 것을 확인하였고, 이를 ACME 스펙, 실제 통신 내용, 그리고 코드 확인으로 검증하였습니다.&lt;/li&gt;
&lt;li&gt;분석한 결과를 바탕으로, cert-manager에 PR을 제출하여 머지되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;커뮤니티 리포팅된 사례 중 하나는 해당 버그로 장애가 발생했다고도 적혀 있었는데, 저희는 운 좋게 개발 환경에서 이슈를 발견하여 서비스 영향이 없었다는 점이 정말 다행이었습니다.&lt;/p&gt;
&lt;p&gt;문제를 조사하면서 평소에 개인적으로도, 업무적으로도 많이 사용하는 Let’s Encrypt가 어떻게 동작하는지 조금 더 깊이있게 볼 수 있었던 것이 특히 기억에 남습니다.&lt;/p&gt;
&lt;p&gt;긴 글 읽어주셔서 감사합니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[스크립트 툴의 장점만 모았다! zx로 업무 자동화하기]]></title><description><![CDATA[기존 쉘 스크립트의 빠른 성능과 자바스크립트의 편의성을 모두 가지고 있는 zx 라이브러리를 소개합니다.]]></description><link>https://tech.devsisters.com/posts/the-best-way-to-write-scripts</link><guid isPermaLink="false">https://tech.devsisters.com/posts/the-best-way-to-write-scripts</guid><pubDate>Tue, 17 Oct 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요. 데브시스터즈에서 프론트엔드 엔지니어로 일하고있는 김준영입니다. 이번 글에서는 자바스크립트에 익숙한 개발자가 자동화 스크립트를 편하게 작성할 수 있게 해주는 &lt;code&gt;zx&lt;/code&gt; 라이브러리를 소개하려고 합니다.&lt;/p&gt;
&lt;h1 id=&quot;들어가기에-앞서&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%93%A4%EC%96%B4%EA%B0%80%EA%B8%B0%EC%97%90-%EC%95%9E%EC%84%9C&quot; aria-label=&quot;들어가기에 앞서 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;들어가기에 앞서&lt;/h1&gt;
&lt;p&gt;저는 &lt;code&gt;업무 자동화&lt;/code&gt; 작업을 참 좋아합니다. 아래와 같은 여러 장점이 있기 때문입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;지루한 반복 업무를 더 이상 하지 않아도 된다는 &lt;strong&gt;편안함&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;개발자 다운 방식으로 문제를 해결했다는 &lt;strong&gt;뿌듯함&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;더 중요한 문제에 집중&lt;/strong&gt;할 수 있음&lt;/li&gt;
&lt;li&gt;오타 등 &lt;strong&gt;단순 실수&lt;/strong&gt;에 신경쓰지 않아도 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;스크립트를 통해 어떤 작업을 자동화 할 수 있고, &lt;code&gt;zx&lt;/code&gt; 는 다른 선택지와 비교하여 어떤 장점이 있는지 살펴보겠습니다.&lt;/p&gt;
&lt;p&gt;이 글은 아래와 같은 분들을 대상으로 작성하였습니다. 🙍‍♀️🙎‍♂️&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;- 반복 업무를 자동화하고 핵심 로직 개선에 집중하고 싶은 분
- 스크립트 작성은 처음이지만 새롭게 도입하고 싶은 분
- 쉘 스크립트 사용 경험이 있으나 더 편한 도구를 찾고 있는 분&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h1 id=&quot;쉘--쉘-스크립트&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%89%98--%EC%89%98-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8&quot; aria-label=&quot;쉘  쉘 스크립트 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;쉘 &amp;#x26; 쉘 스크립트&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;쉘(shell)&lt;/code&gt;은 사용자가 &lt;code&gt;OS&lt;/code&gt; 의 기능을 사용할 수 있도록 하는 사용자 인터페이스입니다. 사용자가 작성한 명령어를 커널이 이해할 수 있는 형태로 변환해 줍니다. 대표적인 쉘로는 &lt;code&gt;bash&lt;/code&gt;, &lt;code&gt;csh&lt;/code&gt;, &lt;code&gt;zsh&lt;/code&gt; 등이 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;쉘 스크립트(shell script)&lt;/code&gt;는 쉘의 명령어들을 이용해 작성하는 프로그래밍 스크립트 언어입니다. 이를 통해 반복적인 작업을 자동화하거나 복잡한 작업을 단순화할 수 있습니다. 그러나 다소 복잡하고 생소한 문법을 가지고 있다는 단점도 있습니다. 아래 내용에서 &lt;code&gt;zx&lt;/code&gt; 는 이러한 단점을 어떻게 보완하였는지 소개하려고 합니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;p align=&quot;center&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 300px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 97.00000000000001%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAATCAYAAACQjC21AAAACXBIWXMAAA7EAAAOxAGVKw4bAAAC7ElEQVR42l1UXYvTQBTNb1dkf4Eu4oOIiA/iyyoIPi8srKIgq7stbfPZtGnz0XynaZvjPZNOtt2BkMnkzrnnnntmDJyMruuG92azged5sCwLju3AczzYtg3HcbBardC27dkePYynYFmWwTRNBEGAsirVWntosWmLfi5AYRgqcAJz3ymocQq2Xq8Vo91uNwTs93ukZYZ5GqDdbs82h6s+/nA4PAKegrmuO/xomgbz+Rz+3Me9PVbPYrFQMVEU4dAdkO4LxEmiQM8YFkWB2Ww2ZE+OQbmsN1WN0XKCP+4DapkzEeVgyc1uq+L57fv+I6BlWiiLXq8szWBbtiqbQWS0iROUWaG+CVRVlWoa92kSKoEkM5q6wVjK+b0ZIa1zPExGyOtCBadpiqeDiabTKeq6Fg1DrEVHDjaITI04irFcLnuR1yFGiynuvDHiMOoRRO9YNAuCJbp9z2YrzSFo3KR4mI6H7lMmgyUxGwdFp06u42K37zt9597hzeQSr+1XuJ5dqwQc9KMVi089G0G6hpl5MC2zB2TtSgfRTmWy+67VVYtPt5f4/OsZvvx8gXfXF4jTRP2jhnRGnuWwfBvJPlf7DGbSgJwTkAKrauUwXP3+iLf3z/F+fIEPty9Rln01cRwrg9MRfKvmsmRm4cNBLfM8V8DUSW3MEnyzvuPK+go3mssp6EvmaaJUOpbN4ppRliVsV1hipwxLUGbXvuoksFuIkf0IXdvrSt+SDashiGbMHigfTmdTROVG/aDBmYSnJBArgIDLBTomkHkpHmSHyYjsWLKWi/40tMCe6w1Hjhv4k3ZxpGmhXBixSOFJEiYi2Pw41+y07sNZVozEmBrUGm6cCsHtD/g3NyhEs/x4G9HIHNWRMS+RAVCD8v4btJM1nhRX1jwB8IUp2QaisW4Y/xNMf58x1G9mJoNUZFAeFj27v/8oNI+DanItrJicuumrThMzTu83PWdGdoy6uLylJxM443E/FxAeBnb6FEjP/wMvMqNuFzLFeQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/7cb810e2c18fca11328ae263f5744a63/77434/linux-shell.webp 300w&quot; sizes=&quot;(max-width: 300px) 100vw, 300px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/7cb810e2c18fca11328ae263f5744a63/a8a0d/linux-shell.png 300w&quot; sizes=&quot;(max-width: 300px) 100vw, 300px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/7cb810e2c18fca11328ae263f5744a63/a8a0d/linux-shell.png&quot; alt=&quot;linux shell&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
      &lt;/p&gt;&lt;figcaption align=&quot;center&quot;&gt;Linux Shell&lt;/figcaption&gt;
    &lt;p&gt;&lt;/p&gt;
&lt;/figure&gt;
&lt;h1 id=&quot;zx란&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#zx%EB%9E%80&quot; aria-label=&quot;zx란 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;zx란?&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;zx&lt;/code&gt;는 자바스크립트를 기반으로 쉘 명령어를 동시에 사용할 수 있는 라이브러리입니다. (&lt;a href=&quot;https://github.com/google/zx&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;ZX 공식 페이지&lt;/a&gt;) 그렇기에 자바스크립트와 쉘 스크립트가 각각 가지고 있는 장점을 동시에 누릴 수 있는 유용한 도구 입니다. &lt;code&gt;bash&lt;/code&gt; 또는 &lt;code&gt;node.js&lt;/code&gt; 를 단독으로 사용하여 스크립트를 작성하는 상황과 비교해서 &lt;code&gt;zx&lt;/code&gt; 의 장점을 보여드리도록 하겠습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;p align=&quot;center&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 720px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.388888888888886%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAhOAAAITgGMMQDsAAABnElEQVR42mP4jwp+/fr16NHjmzdv37x1G8gAocePHz58BARAKTTFDHDW379/geT1GzfFpJX8A0Kd3bz5RWWEJeSExGXFZJRlFDWuXb8BV4Zd882bt3iEpSryi+9dOO/s7BERlxqfkiUuoywlr0ZY89Wr1/hFZEREpbPiE4KCwnSNrU2sHHmFJAVFZa5cvYZTMwR8+Phpx87dW7bt2LB52+at29dv3LxhI5C3a8euPR8/fgIq+PfvH7rmv//+ff32DdlUrACo4N9/oOZ/CM1Aw968ffft+/fXb95cvnrtzPkL5y9eApr1799fkGqwVd++fbtz5xZIMVD5/78IzT9+/vz+48fpsxcCIuKv3bh17sKlGzdv//79Gyi1Yd26XUfWHP8yeePmVb29HWvWrLj/8tLz75chJoI0//z16+fPXz2TpkWnZbx8+frO3ftPnj0Dev7v3z+nTp4+fH7L3rfNm7etWb9+zcZNa559vPH46zkUPwPtuXPvPtBtQIN+/Pj59es3tCTxD2QX2L///2EPbeSQROgBa4MDiDBEMwBWADPzliG34AAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/1538134e204d17fe8a488331f3a67d51/33ecc/logos.webp 360w,
/static/1538134e204d17fe8a488331f3a67d51/263a4/logos.webp 480w,
/static/1538134e204d17fe8a488331f3a67d51/0c8fb/logos.webp 640w,
/static/1538134e204d17fe8a488331f3a67d51/df77d/logos.webp 720w&quot; sizes=&quot;(max-width: 720px) 100vw, 720px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/1538134e204d17fe8a488331f3a67d51/53918/logos.png 360w,
/static/1538134e204d17fe8a488331f3a67d51/9aebd/logos.png 480w,
/static/1538134e204d17fe8a488331f3a67d51/0f09e/logos.png 640w,
/static/1538134e204d17fe8a488331f3a67d51/242a6/logos.png 720w&quot; sizes=&quot;(max-width: 720px) 100vw, 720px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/1538134e204d17fe8a488331f3a67d51/242a6/logos.png&quot; alt=&quot;logos&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;/p&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;장점-1-json-조작의-편의성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9E%A5%EC%A0%90-1-json-%EC%A1%B0%EC%9E%91%EC%9D%98-%ED%8E%B8%EC%9D%98%EC%84%B1&quot; aria-label=&quot;장점 1 json 조작의 편의성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;장점 1) JSON 조작의 편의성&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;JSON&lt;/code&gt;은 개발 영역 전반에서 쓰이고 있는 데이터 포맷입니다. 그러나 &lt;code&gt;bash&lt;/code&gt; 에서는 &lt;code&gt;JSON&lt;/code&gt; 데이터를 다루기에는 불편한 부분이 있습니다. 예시를 통해 &lt;code&gt;zx&lt;/code&gt; 를 사용할 때와 비교해 보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;bash&lt;/code&gt; 에서는 다양한 자료구조를 지원하고 있지 않아 &lt;code&gt;JSON&lt;/code&gt; 데이터도 문자열의 형태로 관리하게 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;DATA &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;{
  &quot;users&quot;: [
    {
      &quot;id&quot;: 1,
      &quot;name&quot;: &quot;John&quot;,
      &quot;contacts&quot;: {
        &quot;email&quot;: &quot;john@example.com&quot;,
        &quot;phone&quot;: &quot;123-456-7890&quot;
      }
    }
  ]
}&apos;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;DATA&lt;/code&gt;에서 이름이 John인 객체를 찾아 email을 출력하기 위해서는 아래와 같은 코드가 필요합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;JOHN_EMAIL &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$DATA&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; jq &lt;span class=&quot;token parameter variable&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;.users[] | select(.name == &quot;John&quot;) | .contacts.email&apos;&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;John&apos;&lt;/span&gt;s Email: &lt;span class=&quot;token variable&quot;&gt;$JOHN_EMAIL&lt;/span&gt;&apos;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;예시처럼, 쉘 스크립트를 통해 &lt;code&gt;JSON&lt;/code&gt; 데이터를 다룰 때에는 몇 가지 단점이 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;데이터 구조가 다양하지 않아 &lt;code&gt;jq&lt;/code&gt; 라는 별도의 도구 필요&lt;/li&gt;
&lt;li&gt;도구를 사용해도 여전히 복잡한 코드 작성 (여러 개의 &lt;code&gt;|&lt;/code&gt; 사용 필요)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;문자열&lt;/code&gt; 로 명령어를 작성하는 방식의 불편함&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;반면에, &lt;code&gt;zx&lt;/code&gt; 를 사용하는 경우 자바스크립트 매서드를 활용하여 익숙하고 편리한 방식으로 작성할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; data &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;users&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;John&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;contacts&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;email&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;john@example.com&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;phone&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;123-456-7890&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; johnEmail &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;users&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; user&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;John&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;contacts
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;email&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;John&apos;s Email:&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; johnEmail&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;장점-2--함수를-통한-쉘-스크립트-명령어-병행-사용&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9E%A5%EC%A0%90-2--%ED%95%A8%EC%88%98%EB%A5%BC-%ED%86%B5%ED%95%9C-%EC%89%98-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EB%AA%85%EB%A0%B9%EC%96%B4-%EB%B3%91%ED%96%89-%EC%82%AC%EC%9A%A9&quot; aria-label=&quot;장점 2  함수를 통한 쉘 스크립트 명령어 병행 사용 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;장점 2) &lt;code&gt;$&lt;/code&gt; 함수를 통한 쉘 스크립트 명령어 병행 사용&lt;/h2&gt;
&lt;p&gt;앞선 단락에서는 쉘 명령어를 사용할 때에 단점에 대해 설명드렸지만, 반대로 쉘 명령어가 강점을 발휘하는 순간도 있습니다. 많은 양의 처리가 필요한 작업에서는 쉘 명령어가 강한 성능을 발휘하기도 하고, 이미 쉘 스크립트로 작성 된 다양한 레퍼런스를 쉽게 참고할 수 있기 때문입니다.&lt;/p&gt;
&lt;p&gt;필요한 경우에 &lt;code&gt;zx&lt;/code&gt; 에서는 &lt;code&gt;$&lt;/code&gt; 함수를 사용해 쉘 명령어를 직접 호출할 수 있습니다. &lt;code&gt;$&lt;/code&gt; 함수를 불러와 문자열 형식으로 명령어를 작성해주면 자바스크립트와 쉘 명령어의 장점을 동시에 활용할 수 있게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ls *.js | wc -l&lt;/code&gt; 는 현재 디렉토리에 있는 &lt;code&gt;.js&lt;/code&gt; 파일의 갯수를 세는 명령어입니다. &lt;code&gt;$&lt;/code&gt; 함수를 사용해 쉘 스크립트 구문을 실행하고, 자바스크립트의 &lt;code&gt;console.log&lt;/code&gt; 를 통해 결과를 출력하고 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; $ &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;zx&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; files &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; $&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;ls *.js | wc -l&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;files&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;stdout&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;장점-3-nodejs-생태계를-활용한-작업-처리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9E%A5%EC%A0%90-3-nodejs-%EC%83%9D%ED%83%9C%EA%B3%84%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%9E%91%EC%97%85-%EC%B2%98%EB%A6%AC&quot; aria-label=&quot;장점 3 nodejs 생태계를 활용한 작업 처리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;장점 3) Node.js 생태계를 활용한 작업 처리&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;zx&lt;/code&gt; 를 사용하면 스크립트 내에서 &lt;a href=&quot;https://www.npmjs.com/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;npm&lt;/a&gt; 패키지를 사용할 수 있습니다. HTTP 요청, 파일 압축, 이미지 처리 등 각 상황에 맞는 도구를 사용하여 원하는 작업을 쉽게 처리할 수 있게 됩니다.&lt;/p&gt;
&lt;p&gt;예를 들어, HTTP 요청 시 자바스크립트 사용자에게 익숙한 &lt;code&gt;axios&lt;/code&gt; 패키지를 동일하게 사용할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; axios &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;axios&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; axios&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;https://api.example.com/data&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그 외에도 &lt;code&gt;zx&lt;/code&gt; 는 여러 가지 &lt;code&gt;장점&lt;/code&gt; 을 가지고 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Promise와 async/await를 통해 &lt;strong&gt;비동기 작업&lt;/strong&gt;을 쉽게 처리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;필요한 경우 타입 시스템(TypeScript)을 활용하여 &lt;strong&gt;안정적이고 정확한 코드 작성&lt;/strong&gt;이 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id=&quot;프론트엔드-개발자가-zx를-활용하는-방법&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-zx%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95&quot; aria-label=&quot;프론트엔드 개발자가 zx를 활용하는 방법 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;프론트엔드 개발자가 zx를 활용하는 방법&lt;/h1&gt;
&lt;p&gt;그러면 &lt;code&gt;zx&lt;/code&gt; 는 어떤 상황에서 유용하게 사용할 수 있을까요? &lt;code&gt;Next.js&lt;/code&gt; 로 만들어진 게임 어드민에서 이미지 파일을 다루기 위해 스크립트를 사용하는 사례를 통해 알아보겠습니다.&lt;/p&gt;
&lt;p&gt;게임 어드민에는 프로젝트 내에 &lt;code&gt;많은 이미지 삽입&lt;/code&gt;이 필요합니다. 게임 클라이언트에서 보여주는 많은 이미지를 운영툴에서도 동일하게 보여 주어야 하기 때문입니다. 보안 관련 번거로운 부분이 있기에 원격 이미지 저장소를 활용하는 대신 프로젝트에 직접 커밋을 하는 형태를 가지고 있습니다.&lt;/p&gt;
&lt;p&gt;이미지 추가는 아래와 같은 방식으로 이루어집니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;p align=&quot;center&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1618px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 17.22222222222222%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAADCAYAAACTWi8uAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAl0lEQVR42j2O2QqFMAxE/f9P00cfpCBK0RZaF+qCaKWOTC7XQCCTCWeSPc+DruuwLAtYMUbUdY1t20CPXVUVhmHAv5RS8N5/WmsNa63M2XVdKMsSx3HI0bquKIpCAOzzPNG2LZxzmOdZdJ7n6Pte9L7v4jdN8wMSwASaKSUBM40wfstdCOHTBBhjJIBwPjSOI6Zpwn3feAH2H+N3bMaL1AAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/438c7e0b8100488b5a32fff9eabd8f97/33ecc/image-update-process.webp 360w,
/static/438c7e0b8100488b5a32fff9eabd8f97/263a4/image-update-process.webp 480w,
/static/438c7e0b8100488b5a32fff9eabd8f97/0c8fb/image-update-process.webp 640w,
/static/438c7e0b8100488b5a32fff9eabd8f97/521f7/image-update-process.webp 860w,
/static/438c7e0b8100488b5a32fff9eabd8f97/cbd37/image-update-process.webp 1200w,
/static/438c7e0b8100488b5a32fff9eabd8f97/46650/image-update-process.webp 1618w&quot; sizes=&quot;(max-width: 1618px) 100vw, 1618px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/438c7e0b8100488b5a32fff9eabd8f97/53918/image-update-process.png 360w,
/static/438c7e0b8100488b5a32fff9eabd8f97/9aebd/image-update-process.png 480w,
/static/438c7e0b8100488b5a32fff9eabd8f97/0f09e/image-update-process.png 640w,
/static/438c7e0b8100488b5a32fff9eabd8f97/3e3fe/image-update-process.png 860w,
/static/438c7e0b8100488b5a32fff9eabd8f97/64756/image-update-process.png 1200w,
/static/438c7e0b8100488b5a32fff9eabd8f97/f4146/image-update-process.png 1618w&quot; sizes=&quot;(max-width: 1618px) 100vw, 1618px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/438c7e0b8100488b5a32fff9eabd8f97/f4146/image-update-process.png&quot; alt=&quot;image update process&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
      &lt;/p&gt;&lt;figcaption align=&quot;center&quot;&gt;이미지 업데이트 프로세스&lt;/figcaption&gt;
    &lt;p&gt;&lt;/p&gt;
&lt;/figure&gt;
&lt;p&gt;이 과정에는 몇 가지 &lt;code&gt;어려움&lt;/code&gt;이 있었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;수 만개의 이미지 파일과 수천 개의 폴더로 구성된 &lt;strong&gt;이미지 저장소의 크기&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;많은 이미지 파일 중 어드민에 필요한 &lt;strong&gt;이미지의 저장 경로 파악&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;게임 업데이트에 따라 &lt;strong&gt;주기적으로&lt;/strong&gt; 새로운 이미지 추가 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이렇게 이미지를 옮기는 것은 지루하고 시간도 많이 소요됩니다. 다음 단락에서 귀찮은 작업을 어떻게 스크립트로 바꾸었는지 알아보겠습니다.&lt;/p&gt;
&lt;h2 id=&quot;주요-스크립트-내용&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A3%BC%EC%9A%94-%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EB%82%B4%EC%9A%A9&quot; aria-label=&quot;주요 스크립트 내용 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;주요 스크립트 내용&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;아래 내용은 독자의 이해를 돕고 외부에 공개하기 위해 핵심 로직을 남기고 부가적인 부분은 추상화 하였습니다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;먼저, 스크립트 내에서는 &lt;code&gt;prefix&lt;/code&gt; 규칙을 활용해 원하는 이미지를 검색합니다. 어드민에서 필요한 이미지들은 이름에서 공통적인 &lt;code&gt;prefix&lt;/code&gt; 규칙을 사용하고 있기 때문입니다. &lt;code&gt;prefix&lt;/code&gt; 를 기준으로 스크립트를 작성하면 새로운 아이템이 추가 되어도 동일한 로직를 활용해 이미지를 가져올 수 있습니다.&lt;/p&gt;
&lt;p&gt;각 단계별 함수를 호출하는 &lt;code&gt;main&lt;/code&gt; 함수를 통해 전체적인 흐름을 소개하겠습니다. (&lt;code&gt;main&lt;/code&gt; 함수 내 들어가는 개별 함수의 내용은 설명으로 대체하였습니다. 세부 내용이 궁금하시다면 아래에 첨부한 ‘스크립트 전체 코드’를 통해 확인해 주세요!)&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;main&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; targetPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; sourcePath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; prefixes &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; config&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;targetPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;recursive&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; findPatterns &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; prefixes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;prefix&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;-name &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;prefix&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos; -o &apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; files &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;findFiles&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;sourcePath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; findPatterns&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;copyFiles&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;files&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; targetPath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;.png 파일들이 모두 &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;targetPath&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;로 복사되었습니다.&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;먼저, &lt;code&gt;Node.js&lt;/code&gt; 의 &lt;code&gt;fs&lt;/code&gt; 모듈을 사용하여 파일을 복사할 디렉토리를 생성합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// &apos;recursive: true&apos; 는 targetPath 경로에 있는 디렉토리가 없을 경우 생성해 주는 옵션입니다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;targetPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;recursive&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;파일을 찾을 규칙을 &lt;code&gt;findPatterns&lt;/code&gt; 변수로 정의하고, 복사할 파일의 경로를 리스트업 해주는 함수 &lt;code&gt;findFiles&lt;/code&gt; 를 호출합니다. (함수의 내부에서는 &lt;code&gt;$&lt;/code&gt; 연산자를 통해 쉘 스크립트 문법을 사용하여 파일을 검색합니다.)&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; findPatterns &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; prefixes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;prefix&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;-name &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;prefix&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos; -o &apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; files &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;findFiles&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;sourcePath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; findPatterns&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;다음으로는 &lt;code&gt;copyFiles&lt;/code&gt; 함수를 사용해 &lt;code&gt;FindFiles&lt;/code&gt; 에서 찾은 파일을 복사하는 동작을 수행합니다. (함수 내부에서 파일 복사는 &lt;code&gt;fs&lt;/code&gt; 모듈의 &lt;code&gt;copyFile&lt;/code&gt; 을 사용하였습니다. 또한, &lt;code&gt;Promise.all()&lt;/code&gt; 을 사용하여 복사 작업을 병렬적으로 처리해 주었습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;copyFiles&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;files&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; targetPath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;마지막으로 모든 작업이 포함 된 main 함수를 실행하고 또 에러 처리를 위해 후속 메서드 &lt;code&gt;catch&lt;/code&gt; 를 추가해 주었습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token function&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;catch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;details&gt;
  &lt;summary style=&quot;font-weight: bold;&quot;&gt; Appendix) 스크립트 전체 코드&lt;/summary&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; promises &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; fs &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;fs&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; path &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;path&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; $ &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;zx&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; config &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;targetPath&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;./target&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;sourcePath&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;../source&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;prefixes&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;prefix1_.*\\.png&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;prefix2_.*\\.png&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;prefix3_.*\\.png&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;main&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; targetPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; sourcePath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; prefixes &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; config&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;targetPath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;recursive&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; findPatterns &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; prefixes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;prefix&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;-name &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;prefix&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos; -o &apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; files &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;findFiles&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;sourcePath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; findPatterns&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;copyFiles&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;files&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; targetPath&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;.png 파일들이 모두 &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;targetPath&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;로 복사되었습니다.&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;findFiles&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;sourcePath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; pattern&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; command &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;find&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; sourcePath&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;-type&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;f&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;concat&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pattern&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos; &apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; files &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; $&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;command&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; files&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;stdout&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;\n&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Boolean&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;copyFiles&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;files&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; targetPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; copyTasks &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; files&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
    fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;copyFile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;file&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;targetPath&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;basename&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;file&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; Promise&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;copyTasks&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;catch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;
&lt;h2 id=&quot;zx-사용을-통한-변화&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#zx-%EC%82%AC%EC%9A%A9%EC%9D%84-%ED%86%B5%ED%95%9C-%EB%B3%80%ED%99%94&quot; aria-label=&quot;zx 사용을 통한 변화 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;zx 사용을 통한 변화&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;zx&lt;/code&gt; 로 작성한 스크립트를 사용하면서 생긴 변화를 단계별로 표현해 보았습니다. 기존의 5단계의 과정이 필요했던 작업이 2단계로 줄어든 것을 알 수 있습니다. 또한, &lt;code&gt;prefix&lt;/code&gt; 를 추가해야 되는 상황은 가끔 발생하는 일이기에 부담이 더욱 줄어들게 됩니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;p align=&quot;center&quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1716px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 55.27777777777778%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAABYlAAAWJQFJUiTwAAABMElEQVR42o1SyY6DUAzr/38Ud449IaAgQGUp+74VyIwjBXUonRlLr230XL84zoW+sW0bH8G6rlTXNTVNw6coCirLktq2paqqaBgGesXrfy9nF8uy0O12I8uy+NswDNJ1nTzPI03TKEmSN6EfgugGHQjp8XhwN4Isy6jrur0Ow3CvwY+iaO+aBdEBXocd2EMXruuyECzbtk33+53yPGeOqqoskqYpC4OPmgWfzyeT5ZVxHMn3fYrjmPq+J9xDGF3jHm7wOO6naWLBIAiYg24v+JjnmY+EA1GpZaYISn4LXwIU/lsoR4B4vV7ZMmyhs09hnKZ8BF7FzCAMaxjBX/hV0DRNng1s4TiOQ4qi7Il+XJszYFYQxDrJWmH4SPi42P8ShD2sBVYFgGXUEMdqSUhHwS8kcVV1CJtM/QAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/d19a8d4889a5bca26b30032b204a222c/33ecc/image-update-process-renewal.webp 360w,
/static/d19a8d4889a5bca26b30032b204a222c/263a4/image-update-process-renewal.webp 480w,
/static/d19a8d4889a5bca26b30032b204a222c/0c8fb/image-update-process-renewal.webp 640w,
/static/d19a8d4889a5bca26b30032b204a222c/521f7/image-update-process-renewal.webp 860w,
/static/d19a8d4889a5bca26b30032b204a222c/cbd37/image-update-process-renewal.webp 1200w,
/static/d19a8d4889a5bca26b30032b204a222c/4b981/image-update-process-renewal.webp 1716w&quot; sizes=&quot;(max-width: 1716px) 100vw, 1716px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/d19a8d4889a5bca26b30032b204a222c/53918/image-update-process-renewal.png 360w,
/static/d19a8d4889a5bca26b30032b204a222c/9aebd/image-update-process-renewal.png 480w,
/static/d19a8d4889a5bca26b30032b204a222c/0f09e/image-update-process-renewal.png 640w,
/static/d19a8d4889a5bca26b30032b204a222c/3e3fe/image-update-process-renewal.png 860w,
/static/d19a8d4889a5bca26b30032b204a222c/64756/image-update-process-renewal.png 1200w,
/static/d19a8d4889a5bca26b30032b204a222c/010a2/image-update-process-renewal.png 1716w&quot; sizes=&quot;(max-width: 1716px) 100vw, 1716px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/d19a8d4889a5bca26b30032b204a222c/010a2/image-update-process-renewal.png&quot; alt=&quot;image update process renewal&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
      &lt;/p&gt;&lt;figcaption align=&quot;center&quot;&gt;이미지 업데이트 프로세스 비교&lt;/figcaption&gt;
    &lt;p&gt;&lt;/p&gt;
&lt;/figure&gt;
&amp;nbsp;
&lt;div style=&quot;text-align: center; font-weight: bold;&quot;&gt;&quot;zx를 사용해 이미지 변경 사항에 거의 신경쓰지 않고 프로젝트를 관리할 수 있게 되었습니다.&quot;&lt;/div&gt;
&lt;h1 id=&quot;글을-마치며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B8%80%EC%9D%84-%EB%A7%88%EC%B9%98%EB%A9%B0&quot; aria-label=&quot;글을 마치며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;글을 마치며&lt;/h1&gt;
&lt;p&gt;단순 작업을 반복하게 되면 종종 ‘나 대신 작업을 해 줄 사람이 없을까?’ 하는 생각이 들 때가 있습니다. 그러나 &lt;code&gt;업무 자동화&lt;/code&gt; 를 선뜻 시작하기에는 부담스러운 경우가 많은 것 같습니다. 대부분의 개발자들에게 스크립트 작업은 주요 업무가 아니기 때문에, 생소한 문법을 배우고 구현하는 과정에서 여러 시행착오가 있을 수 있기 때문입니다.&lt;/p&gt;
&lt;p&gt;이런 점에서 많은 개발자에게 &lt;code&gt;zx&lt;/code&gt;는 유용한 도구가 될 것이라 생각합니다. 자바스크립트에서 제공하는 편의성과 쉘 스크립트의 뛰어난 성능과 다양한 자료를 동시에 활용할 수 있어, 빠르게 원하는 자동화 작업을 구현할 수 있기 때문입니다.
 &lt;/p&gt;
&lt;div style=&quot;text-align: center; font-weight: bold;&quot;&gt;“지루한 반복 업무가 있다면 zx를 통한 업무 자동화를 적극 추천합니다.”&lt;/div&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[MySQL online alter부터 CPU 100% 장애까지]]></title><description><![CDATA[쿠키런: 오븐브레이크 팀에서 MySQL을 사용하면서 있었던 사례에 대해 공유합니다]]></description><link>https://tech.devsisters.com/posts/cro-mysql-online-alter</link><guid isPermaLink="false">https://tech.devsisters.com/posts/cro-mysql-online-alter</guid><pubDate>Tue, 05 Sep 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요. [쿠키런: 오븐브레이크]의 서버 개발자 황재영입니다.&lt;/p&gt;
&lt;p&gt;Database는 많은 개발자들이 유용하게 사용하면서 동시에 많은 문제를 겪는 요소라고 생각합니다. [쿠키런: 오븐브레이크] 팀 역시 그렇습니다. 이 글에서는 제가 22년 7월 경에 겪었던 AWS Aurora MySQL과 관련된 online alter과 CPU 100% 장애에 대해서 이야기해보려고 합니다.&lt;/p&gt;
&lt;h1 id=&quot;신규-기능과-table-alter&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8B%A0%EA%B7%9C-%EA%B8%B0%EB%8A%A5%EA%B3%BC-table-alter&quot; aria-label=&quot;신규 기능과 table alter permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;신규 기능과 Table Alter&lt;/h1&gt;
&lt;p&gt;작업의 시작은 22년 8월에 추가된 [쿠키런: 오븐브레이크]의 신규 기능, &lt;code&gt;패키지 매출 랭킹&lt;/code&gt;에서 비롯되었습니다. &lt;code&gt;패키지 매출 랭킹&lt;/code&gt;은 현재 판매 중인 패키지들에 대해서 지난 N시간동안의 매출을 집계해서 등수를 매기고 보여주는 기능입니다. 이 기능의 내부적인 구현을 위해서는 지난 N시간동안의 패키지별 판매 수량을 집계해야 했었습니다. 그러나 기존에 구매 기록을 관리하고 있던 AWS Aurora MySQL table에는 &lt;code&gt;구매 시점&lt;/code&gt; 정보가 없었기에 &quot;지난 N시간동안&quot;이라는 정보를 조회하기 어려웠습니다. 이를 우회하기 위한 방법도 고려해보았지만, 결론적으로는 &lt;code&gt;구매 시점&lt;/code&gt; 정보를 table에 추가하기로 했습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; $TABLE_NAME
&lt;span class=&quot;token keyword&quot;&gt;ADD&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;COLUMN&lt;/span&gt; purchase_dt &lt;span class=&quot;token keyword&quot;&gt;datetime&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;NULL&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;ADD&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;KEY&lt;/span&gt; purchase_dt &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;purchase_dt&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;구매 시점&lt;/code&gt; 정보를 purchase_dt라는 칼럼으로 생성하고 index까지 추가하기 위해서는 위와 같은 쿼리를 필요로 했습니다. 그리고 여기서 문제가 발생했습니다. 구매 기록을 관리하고 있던 table은 유저가 실제로 구매하는 로직과 직결되어 있기 때문에, 일반적인 ALTER TABLE로는 table lock이 잡혀서 문제가 될 것이기 때문에 진행할 수 없었습니다. 그렇다고 [쿠키런: 오븐브레이크] 팀에서 일반적으로 진행하는 점검 2시간동안 적용하기에는, 서비스 시작 이래의 모든 구매 기록을 가지고 있는 큰 크기의 테이블이었기에, 실행에 시간이 너무 오래 걸려서 어려웠습니다. 이러한 제한점 때문에 이 쿼리는 online alter를 통해 적용하는 것으로 결정하게 되었습니다.&lt;/p&gt;
&lt;h1 id=&quot;online-alter-plan-1---inplace-algorithm&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#online-alter-plan-1---inplace-algorithm&quot; aria-label=&quot;online alter plan 1   inplace algorithm permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Online Alter Plan 1 - Inplace Algorithm&lt;/h1&gt;
&lt;p&gt;처음으로 고려해본 것은 MySQL Database에서 지원하는 기능이었습니다. 바로 lock을 잡지 않는 alter인 inplace algorithm이었는데요. 적용이 가능한지 간단히 테스트해보기 위해서, 개발팀에서 테스트로 사용하는 서버의 Database를 복제하여 alter를 진행해보았습니다. 결과는 다음과 같았습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;mysql&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; $TABLE_NAME
    &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ADD&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;COLUMN&lt;/span&gt; purchase_dt &lt;span class=&quot;token keyword&quot;&gt;datetime&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;NULL&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ADD&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;KEY&lt;/span&gt; purchase_dt &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;purchase_dt&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ALGORITHM&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;inplace&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;LOCK&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;none&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
ERROR &lt;span class=&quot;token number&quot;&gt;1062&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;23000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;: &lt;span class=&quot;token keyword&quot;&gt;Duplicate&lt;/span&gt; entry &lt;span class=&quot;token string&quot;&gt;&apos;73817&apos;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;PRIMARY&apos;&lt;/span&gt;

mysql&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; $TABLE_NAME
    &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ADD&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;COLUMN&lt;/span&gt; purchase_dt &lt;span class=&quot;token keyword&quot;&gt;datetime&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;NOT&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;NULL&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;DEFAULT&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ADD&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;KEY&lt;/span&gt; purchase_dt &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;purchase_dt&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;ALGORITHM&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;inplace&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;LOCK&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;none&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
Query OK&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;rows&lt;/span&gt; affected &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2.72&lt;/span&gt; sec&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; Records: &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; Duplicates: &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;Warnings&lt;/span&gt;: &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;최초 시도에서는 실패하고, 2번째 시도에서는 성공하는 특이한 결과를 확인할 수 있었습니다. 최초 시도에서 발생한 에러에 대해 검색하니 &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/innodb-online-ddl-limitations.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;MySQL의 공식 문서&lt;/a&gt;를 확인할 수 있었습니다. 문서에서는 위 에러가 inplace algorithm의 한계로, 간헐적으로 발생할 수 있는 문제라고 설명하고 있었습니다. 실제 서비스 환경에서 진행할 때는 더 발생할 가능성이 높은 에러로 고려되었기 때문에, inplace algorithm을 사용한 online alter plan은 폐기되었습니다.&lt;/p&gt;
&lt;h1 id=&quot;online-alter-plan-2---percona-toolkit&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#online-alter-plan-2---percona-toolkit&quot; aria-label=&quot;online alter plan 2   percona toolkit permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Online Alter Plan 2 - Percona Toolkit&lt;/h1&gt;
&lt;p&gt;MySQL의 기본 기능이 아닌 외부 툴에 대해서 찾아보니, &lt;strong&gt;Percona Toolkit&lt;/strong&gt;이라는 툴에서 제공하는 online schema change라는 기능으로 실제 서비스 환경에서 online alter를 진행하는 사례를 찾았습니다. 어떤 원리로 동작하는지 궁금하여서 찾아봤더니 아래와 같이 정리할 수 있었습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;기존 table과 동일한 스키마를 가진 신규 table 생성&lt;/li&gt;
&lt;li&gt;신규 table에 schema change를 바로 적용&lt;/li&gt;
&lt;li&gt;기존 table의 변경사항이 신규 table에도 적용될 수 있게 trigger 설정&lt;/li&gt;
&lt;li&gt;기존 table에서 일정 크기의 데이터만큼 신규 table로 복사&lt;/li&gt;
&lt;li&gt;모든 데이터를 복사할 때까지 4번을 계속 반복&lt;/li&gt;
&lt;li&gt;check sum 검사를 통해 신규 table과 기존 table이 동일한 상태에 있는지 검사&lt;/li&gt;
&lt;li&gt;신규 table의 이름을 기존 table의 이름으로 변경&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;위 과정에 따른 online schema change에는 기존에 설정된 trigger가 없어야 한다거나 schema change에 위반되는 변경사항이 있어서는 안된다와 같은 제약사항이 있었지만, 이번에 적용하려고 하는 table에서는 문제가 없어 이 기능을 사용하여 online alter를 진행하기로 하였습니다.&lt;/p&gt;
&lt;p&gt;해당 툴의 사용 테스트와 실제 적용은 아래와 같은 4단계로 진행하였습니다. 후술된 Staging은 내부 테스트 환경, Production은 실제 서비스 환경을 의미합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Staging 복제본 테스트&lt;/li&gt;
&lt;li&gt;Production 복제본 테스트&lt;/li&gt;
&lt;li&gt;Staging 적용&lt;/li&gt;
&lt;li&gt;Production 적용&lt;/li&gt;
&lt;/ol&gt;
&lt;h1 id=&quot;staging-복제본-테스트&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#staging-%EB%B3%B5%EC%A0%9C%EB%B3%B8-%ED%85%8C%EC%8A%A4%ED%8A%B8&quot; aria-label=&quot;staging 복제본 테스트 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Staging 복제본 테스트&lt;/h1&gt;
&lt;p&gt;툴을 사용할 수 있는지 검증하고 실제로 일어날 변경사항을 확인하는 단계입니다.&lt;/p&gt;
&lt;p&gt;먼저 dry run부터 사용해보았습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;pt-online-schema-change &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
    --dry-run
    &lt;span class=&quot;token parameter variable&quot;&gt;--host&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$DBHOST&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--user&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$USERNAME&lt;/span&gt; --ask-pass &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;token parameter variable&quot;&gt;--alter&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;ADD COLUMN purchase_dt datetime NOT NULL DEFAULT 0, ADD KEY purchase_dt (purchase_dt)&apos;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;token assign-left variable&quot;&gt;D&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$DB_NAME&lt;/span&gt;,t&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$TABLE_NAME&lt;/span&gt;

Starting a dry run.  &lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;DB_NAME&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;TABLE_NAME&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;&lt;/span&gt; will not be altered.  Specify &lt;span class=&quot;token parameter variable&quot;&gt;--execute&lt;/span&gt; instead of --dry-run to alter the table.
Creating new table&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.
Created new table DB_NAME._TABLE_NAME_new OK.
Altering new table&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.
Altered &lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;DB_NAME&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;_TABLE_NAME_new&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;&lt;/span&gt; OK.
Not creating triggers because this is a dry run.
Not copying rows because this is a dry run.
Not swapping tables because this is a dry run.
Not dropping old table because this is a dry run.
Not dropping triggers because this is a dry run.
&lt;span class=&quot;token number&quot;&gt;2022&lt;/span&gt;-07-13T11:52:58 Dropping new table&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.
&lt;span class=&quot;token number&quot;&gt;2022&lt;/span&gt;-07-13T11:52:58 Dropped new table OK.
Dry run complete. &lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;DB_NAME&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;TABLE_NAME&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;&lt;/span&gt; was not altered.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;dry run에서는 trigger 설정도, copy도, swap도 없이 정말 툴을 사용할 수 있는지의 기본적인 검사만 이루어졌는데요. 특별한 문제가 없었기에 바로 real run으로 넘어가 보았습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;pt-online-schema-change &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;token parameter variable&quot;&gt;--execute&lt;/span&gt;
    &lt;span class=&quot;token parameter variable&quot;&gt;--host&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$DBHOST&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--user&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$USERNAME&lt;/span&gt; --ask-pass &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;token parameter variable&quot;&gt;--alter&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;ADD COLUMN purchase_dt datetime NOT NULL DEFAULT 0, ADD KEY purchase_dt (purchase_dt)&apos;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;token assign-left variable&quot;&gt;D&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$DB_NAME&lt;/span&gt;,t&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$TABLE_NAME&lt;/span&gt;

Altering &lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;DB_NAME&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;TABLE_NAME&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.
Creating new table&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.
Created new table DB_NAME._TABLE_NAME_new OK.
Altering new table&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.
Altered &lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;DB_NAME&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;_TABLE_NAME_new&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;&lt;/span&gt; OK.
&lt;span class=&quot;token number&quot;&gt;2022&lt;/span&gt;-07-13T11:55:11 Creating triggers&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.
&lt;span class=&quot;token number&quot;&gt;2022&lt;/span&gt;-07-13T11:55:11 Created triggers OK.
&lt;span class=&quot;token number&quot;&gt;2022&lt;/span&gt;-07-13T11:55:11 Copying approximately &lt;span class=&quot;token number&quot;&gt;9980&lt;/span&gt; rows&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.
&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;중략&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token number&quot;&gt;2022&lt;/span&gt;-07-13T11:55:34 Copied rows OK.
&lt;span class=&quot;token number&quot;&gt;2022&lt;/span&gt;-07-13T11:55:34 Analyzing new table&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.
&lt;span class=&quot;token number&quot;&gt;2022&lt;/span&gt;-07-13T11:55:34 Swapping tables&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.
&lt;span class=&quot;token number&quot;&gt;2022&lt;/span&gt;-07-13T11:55:34 Swapped original and new tables OK.
&lt;span class=&quot;token number&quot;&gt;2022&lt;/span&gt;-07-13T11:55:34 Dropping old table&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.
&lt;span class=&quot;token number&quot;&gt;2022&lt;/span&gt;-07-13T11:55:34 Dropped old table &lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;DB_NAME&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;_TABLE_NAME_old&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;&lt;/span&gt; OK.
&lt;span class=&quot;token number&quot;&gt;2022&lt;/span&gt;-07-13T11:55:34 Dropping triggers&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.
&lt;span class=&quot;token number&quot;&gt;2022&lt;/span&gt;-07-13T11:55:34 Dropped triggers OK.
Successfully altered &lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;DB_NAME&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;TABLE_NAME&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;real run에서는 원래 기대했던 기능들이 적용된 모습을 볼 수 있었습니다. 실행시간을 보면 내부 테스트용 Database라서 데이터가 많지 않아 매우 빠르게 끝났음을 알 수 있었습니다.&lt;/p&gt;
&lt;h1 id=&quot;production-복제본-테스트&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#production-%EB%B3%B5%EC%A0%9C%EB%B3%B8-%ED%85%8C%EC%8A%A4%ED%8A%B8&quot; aria-label=&quot;production 복제본 테스트 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Production 복제본 테스트&lt;/h1&gt;
&lt;p&gt;실제 Production에서 실행 때 일어날 변경사항과 소요 시간을 확인하는 단계입니다.&lt;/p&gt;
&lt;p&gt;서비스의 Database 사용에 문제를 일으키지 않을 정도의 자원을 사용하면서 최대한 빠르게 실행하기 위해, Production 복제본에 적용하는 테스트는 &lt;code&gt;chunk-size&lt;/code&gt; 옵션을 바꾸어 가며 여러 번 테스트를 진행했습니다. chunk-size 옵션은 위에서 설명한 작동 원리 4번의 복사 스텝에서 얼마만큼의 크기의 데이터를 복사할지를 조절하는 값으로, 이 값에 따라 툴의 실행 시간과 자원 소모량이 결정됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 891px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 57.22222222222223%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABl0lEQVR42m1TS47VQAzM/S/AAqS5BEdghUCzZgdiBMzAm5f0vzvtLspO8hTxiGS57S7bZbczXS6vUOki0M85j+u8QMbYbB/w/ekJMWWzr8uCX88vKKVC9pjzN+WU8PPP/NIE74h4S99DD68fZG0PhL+h/V6BYw8eLKSJxl7wLmEgg5BLIOgzkZ+oH6WVb9L7I1l+pP3lSHQI9mSjN0hJ2Fybb2qt3VU5Vz8nusnOUGoCu8HYO1DfVGu1OJBeF5skSYliVGuf8r/WDFAiJHtjeninwoRDOmR5Rolsv/bTnICeI1p06DWdWIo9mhQPzhprmKmrFZnWdTW6/fdX1LDgxzXd6Ks2BlqQ+ngMK6hCn/Fqhe3nLWHlDDWgh6vNxOV+WyFtpbOtdWztgbh9sKbW5NG6GMtxJGzKsK8bkxIQg+cuLsjBIfuZe+htDCFGhPmC4ByiXxCJcdxJn5tJ4rhshpHAnBMdHplnvXAMCt5Z8hgT9OEUp4UCC3jea7KcMxe82F3ZHndbG53jof+Vu/ve7XzYdT8ff81fVstcjnGWmKgAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/d91f020438b2e1dd34eaa76d0482f15b/33ecc/production_clone_cpu.webp 360w,
/static/d91f020438b2e1dd34eaa76d0482f15b/263a4/production_clone_cpu.webp 480w,
/static/d91f020438b2e1dd34eaa76d0482f15b/0c8fb/production_clone_cpu.webp 640w,
/static/d91f020438b2e1dd34eaa76d0482f15b/521f7/production_clone_cpu.webp 860w,
/static/d91f020438b2e1dd34eaa76d0482f15b/3e9ea/production_clone_cpu.webp 891w&quot;
              sizes=&quot;(max-width: 891px) 100vw, 891px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/d91f020438b2e1dd34eaa76d0482f15b/53918/production_clone_cpu.png 360w,
/static/d91f020438b2e1dd34eaa76d0482f15b/9aebd/production_clone_cpu.png 480w,
/static/d91f020438b2e1dd34eaa76d0482f15b/0f09e/production_clone_cpu.png 640w,
/static/d91f020438b2e1dd34eaa76d0482f15b/3e3fe/production_clone_cpu.png 860w,
/static/d91f020438b2e1dd34eaa76d0482f15b/c7d76/production_clone_cpu.png 891w&quot;
            sizes=&quot;(max-width: 891px) 100vw, 891px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/d91f020438b2e1dd34eaa76d0482f15b/c7d76/production_clone_cpu.png&quot;
            alt=&quot;Production 복제본 테스트를 실행하였을 때 확인한 CPU 그래프&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Production 복제본 테스트를 실행하였을 때 확인한 CPU 그래프&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;그래프에서 총 3개의 피크를 확인할 수 있는데, 첫번째 피크는 Database를 생성했을 때의 피크로 무시하고, 나머지 2개의 피크에서 결과를 확인할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;2번째 피크는 chunk-size를 250으로 설정했을 때이고, 3번째 피크는 chunk-size를 500으로 설정했을 때입니다. 안정적으로 45% 정도의 CPU를 유지하는 chunk-size 250과 다르게, chunk-size 500은 최초에 CPU가 거진 80%까지 올라가고 이후에 50% 정도로 안정되는 모습을 보여주었습니다. 상시적으로 약 8%의 CPU를 사용 중인 복제본 Database와 다르게, 실제 서비스 Database는 상시적으로 약 20%의 CPU를 사용하고 있습니다. 그리고 툴 사용 중에도 CPU 70%를 넘지 않는 것을 목표로 생각하고 있었기에, chunk-size는 250으로 결정하여 Production Database에 적용하는 것으로 결정했습니다.&lt;/p&gt;
&lt;h1 id=&quot;staging-환경-적용&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#staging-%ED%99%98%EA%B2%BD-%EC%A0%81%EC%9A%A9&quot; aria-label=&quot;staging 환경 적용 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Staging 환경 적용&lt;/h1&gt;
&lt;p&gt;유저가 활동하는 상황에서 online alter가 잘 되는지 테스트하는 단계입니다.&lt;/p&gt;
&lt;p&gt;유저의 사용 패턴을 mocking하여 여러 종류의 복잡한 구매를 시도하고, Production에 비해 너무 빠르게 끝나는 실행을 보완하기 위해 sleep 옵션을 사용해서 테스트도 진행하였습니다. 다행히도 문제 없이 적용되었습니다.&lt;/p&gt;
&lt;h1 id=&quot;production-환경-적용&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#production-%ED%99%98%EA%B2%BD-%EC%A0%81%EC%9A%A9&quot; aria-label=&quot;production 환경 적용 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Production 환경 적용&lt;/h1&gt;
&lt;p&gt;마침내 실제 서비스에 적용하는 단계입니다.&lt;/p&gt;
&lt;p&gt;혹시 모를 상황에 대비하여 별도의 백업도 준비한 상태로 진행하였습니다. 미리 검증을 해서인지, 툴이 잘 만들어져 있어서인지, 아래와 같은 CPU 그래프와 함께 문제 없이 잘 실행되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 866px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 58.333333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAY3RFWHRSYXcACmdlbmVyaWMgcHJvZmlsZQogICAgICAzNAo0OTQ5MmEwMDA4MDAwMDAwMDEwMDMxMDEwMjAwMDcwMDAwMDAxYTAwMDAwMDAwMDAwMDAwNTA2OTYzNjE3MzYxMDAwMAozQmzXAAABhUlEQVR42m1TW1IEIQzcs/vlffzyJB5BS3d3nGEgBAixk3nIWlLVlUeHJmSGy/V209qaSu/aYN8/PpRydt/W1/XqucLF4+951vs0qYg4/q7LBHJCEZdiIh0HtCWEnhF3HBLW1QUSkQsU5K0Ji0utLmJ1By63adavWF600nObP1/BPwFvVihDC+OmLX7MnYIpEa4sK7q791aTdp2gU5r4Du3H7tH+s07BY1YWRK5KVTRQ0XsgDYnhsy4pu7/EDJ8dB2d2Bm/fwQVL8Tl0JPocudscS23dOkTnDsQP9jcvmLtoQiMx20eD4BLJO7TiFSdasnfZZtWPa8uZN6sDd+Sta+9wCpugXZfrdn3ZZ3dij4+8jNyeX2gXtPnkghMwNy5ItqpSi0obUAc7wvmt/uyw0Kph/nY0iiqctOXkdkPc47gj7TWPPtteEwzLgiBpTlGJ0C1eSULMeBnkPiHH4Mg55/GrMbNzlMlrU4z+41+s0Eh7KW4B22QvwiyP/sDXob7ixZi1K/8A9oGs0lrshssAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/43bd1bbaae33c7aad65355836c411d44/33ecc/production_cpu.webp 360w,
/static/43bd1bbaae33c7aad65355836c411d44/263a4/production_cpu.webp 480w,
/static/43bd1bbaae33c7aad65355836c411d44/0c8fb/production_cpu.webp 640w,
/static/43bd1bbaae33c7aad65355836c411d44/521f7/production_cpu.webp 860w,
/static/43bd1bbaae33c7aad65355836c411d44/addc7/production_cpu.webp 866w&quot;
              sizes=&quot;(max-width: 866px) 100vw, 866px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/43bd1bbaae33c7aad65355836c411d44/53918/production_cpu.png 360w,
/static/43bd1bbaae33c7aad65355836c411d44/9aebd/production_cpu.png 480w,
/static/43bd1bbaae33c7aad65355836c411d44/0f09e/production_cpu.png 640w,
/static/43bd1bbaae33c7aad65355836c411d44/3e3fe/production_cpu.png 860w,
/static/43bd1bbaae33c7aad65355836c411d44/4219d/production_cpu.png 866w&quot;
            sizes=&quot;(max-width: 866px) 100vw, 866px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/43bd1bbaae33c7aad65355836c411d44/4219d/production_cpu.png&quot;
            alt=&quot;Production에서 실행하였을 때 확인한 CPU 그래프&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Production에서 실행하였을 때 확인한 CPU 그래프&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h1 id=&quot;cpu-100&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#cpu-100&quot; aria-label=&quot;cpu 100 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;CPU 100%&lt;/h1&gt;
&lt;p&gt;그렇게 online alter 작업을 성공적으로 마치고, 기능 구현도 완료한 상태로 기능 배포 당일을 맞이하였습니다. 배포를 진행하면서 살짝 불안해진 CPU 그래프를 보고 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1600px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 44.99999999999999%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAIAAAC9o5sfAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA7klEQVR42o2Pi2rDMAxF/f+fuQVWtpYusWxZz+zWCaOMMWYO4tq6eris2/p5vwm3PX0P+0k+xSnCtFGlujL34taTl52X7K//ZJ/+nd9KaAt6yb5MjvS3Xv4CxWZYJk52kOkGHiI8TY/rSSBq6pjai7tnoDIBRKqcoOw5ThGqoRZ+Ni2mxrUN6kzcmwiriMMjA8JU3exRgschLhIzuig8jrW9tVGJAQQ1ButG7x/3t8v9clmX5Xa9rkSjVq61EzHzoGku5uiq+J07ls5jd3ScA+cQgfbjZ/AcqSOWjTSQ+e08Rog8v6DHVqm1Xolqa1+ebww/EAJZ0gAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/20d0a2af5ca157b5806b91cd36451b2d/33ecc/cpu_1.webp 360w,
/static/20d0a2af5ca157b5806b91cd36451b2d/263a4/cpu_1.webp 480w,
/static/20d0a2af5ca157b5806b91cd36451b2d/0c8fb/cpu_1.webp 640w,
/static/20d0a2af5ca157b5806b91cd36451b2d/521f7/cpu_1.webp 860w,
/static/20d0a2af5ca157b5806b91cd36451b2d/cbd37/cpu_1.webp 1200w,
/static/20d0a2af5ca157b5806b91cd36451b2d/64296/cpu_1.webp 1600w&quot;
              sizes=&quot;(max-width: 1600px) 100vw, 1600px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/20d0a2af5ca157b5806b91cd36451b2d/53918/cpu_1.png 360w,
/static/20d0a2af5ca157b5806b91cd36451b2d/9aebd/cpu_1.png 480w,
/static/20d0a2af5ca157b5806b91cd36451b2d/0f09e/cpu_1.png 640w,
/static/20d0a2af5ca157b5806b91cd36451b2d/3e3fe/cpu_1.png 860w,
/static/20d0a2af5ca157b5806b91cd36451b2d/64756/cpu_1.png 1200w,
/static/20d0a2af5ca157b5806b91cd36451b2d/42cbc/cpu_1.png 1600w&quot;
            sizes=&quot;(max-width: 1600px) 100vw, 1600px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/20d0a2af5ca157b5806b91cd36451b2d/42cbc/cpu_1.png&quot;
            alt=&quot;기존보다 평균치가 살짝 상승한 CPU&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;기존보다 평균치가 살짝 상승한 CPU&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;기존에 20% 정도만 사용하던 것에 비해 약 10~20% 정도 오른 그래프에서, 기능 구현을 위해 추가한 쿼리로 인한 영향을 인지했습니다. 그러나 아직 큰 문제는 아니기 때문에, 같이 배포된 다른 기능들에서 발견된 문제들을 먼저 해결하고 있었습니다.&lt;/p&gt;
&lt;p&gt;그런데 상황이 바뀌기 시작했습니다. 여러 버그 픽스를 추가로 배포하고 유저들을 재접속시키니, 어느 순간 Database 알람이 울리고 다시 본 그래프는 아래와 같은 모습을 하고 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1600px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 44.99999999999999%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAIAAAC9o5sfAAAACXBIWXMAAAsTAAALEwEAmpwYAAABBUlEQVR42m2Q627DIAyFef+H7HqbpklTu4INxjdmkrTNj1mfzOGYJCeknPP99tNbHa5u8mK8cI3uyi7duQlh+b2V+61BSSrV63XUi+PZ8bKxbuHk5WiPw0Y+TAdOA8+T9pWUq0Gcvnr9dLw6nL2c7PGxkY/TwfXAjnDoO4mICZt0622BjMk0kqu7vYnt6tjCND1ii/buqqY2PPDhI2qui9iXj3W4TVM8xZ2FpbMpizNJF+YwlTleO+E9vImIm8ys1lYbAWAIrARIOddS6tTQQgQA9amjNwAi4vnlTqw6g8e/TGEe9xAi+lL+7HMaKectTTQVZDMd/1VrRL3vHVUFxIhYIAr/AFiGDO0iD59VAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/878bc64211d7cba3a45c3127fbe25a39/33ecc/cpu_2.webp 360w,
/static/878bc64211d7cba3a45c3127fbe25a39/263a4/cpu_2.webp 480w,
/static/878bc64211d7cba3a45c3127fbe25a39/0c8fb/cpu_2.webp 640w,
/static/878bc64211d7cba3a45c3127fbe25a39/521f7/cpu_2.webp 860w,
/static/878bc64211d7cba3a45c3127fbe25a39/cbd37/cpu_2.webp 1200w,
/static/878bc64211d7cba3a45c3127fbe25a39/64296/cpu_2.webp 1600w&quot;
              sizes=&quot;(max-width: 1600px) 100vw, 1600px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/878bc64211d7cba3a45c3127fbe25a39/53918/cpu_2.png 360w,
/static/878bc64211d7cba3a45c3127fbe25a39/9aebd/cpu_2.png 480w,
/static/878bc64211d7cba3a45c3127fbe25a39/0f09e/cpu_2.png 640w,
/static/878bc64211d7cba3a45c3127fbe25a39/3e3fe/cpu_2.png 860w,
/static/878bc64211d7cba3a45c3127fbe25a39/64756/cpu_2.png 1200w,
/static/878bc64211d7cba3a45c3127fbe25a39/42cbc/cpu_2.png 1600w&quot;
            sizes=&quot;(max-width: 1600px) 100vw, 1600px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/878bc64211d7cba3a45c3127fbe25a39/42cbc/cpu_2.png&quot;
            alt=&quot;100%를 달성해버린 CPU&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;100%를 달성해버린 CPU&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;처음으로 보는 CPU 100% 그래프였는데요. 아니나 다를까, Database 접속에 문제가 생기면서 유저들이 정상적인 게임 접속/플레이가 불가능해졌습니다. 우선 서버 점검을 건 후, 문제가 되는 패키지 매출 랭킹 기능의 활성화 여부를 설정할 수 있는 기능을 빠르게 추가해 비활성화 상태로 설정한 후 서비스를 정상화시켰습니다.
서비스를 정상화시킨 다음 침착하게 기능 구현을 파악한 결과, 밝혀진 문제 원인은 2개였는데요. 바로 쿼리와 캐시 사용 패턴이었습니다.&lt;/p&gt;
&lt;h1 id=&quot;문제-원인1-쿼리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%B8%EC%A0%9C-%EC%9B%90%EC%9D%B81-%EC%BF%BC%EB%A6%AC&quot; aria-label=&quot;문제 원인1 쿼리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;문제 원인1. 쿼리&lt;/h1&gt;
&lt;p&gt;먼저 쿼리 자체가 최적화가 되어 있지 않아서, CPU를 더 많이 소모하게 되었습니다. 사용한 쿼리는 아래와 같았습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; product_id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;COUNT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;AS&lt;/span&gt; count
&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; $DB_NAME
&lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;product_id &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; ‘aaa’ &lt;span class=&quot;token operator&quot;&gt;AND&lt;/span&gt; {판매 시작 시간} &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; purchase_dt &lt;span class=&quot;token operator&quot;&gt;AND&lt;/span&gt; purchase_dt &lt;span class=&quot;token operator&quot;&gt;&amp;lt;=&lt;/span&gt; {현재}&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;OR&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;product_id &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; ‘bbb’ &lt;span class=&quot;token operator&quot;&gt;AND&lt;/span&gt; {판매 시작 시간} &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; purchase_dt &lt;span class=&quot;token operator&quot;&gt;AND&lt;/span&gt; purchase_dt &lt;span class=&quot;token operator&quot;&gt;&amp;lt;=&lt;/span&gt; {현재}&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;첫 번째 문제는 (product_id, purchase_dt)을 묶어서 쿼리를 하는데 이를 기준으로 한 index가 없는 것이었습니다. 두 번째 문제는 조회 과정에서 매번 바뀌는 &quot;현재&quot;가 조건으로 걸려 있기 때문에 Database 내부적으로 최적화하기 어렵다는 것이었습니다. 이 2개의 문제에 대해서 각각의 해결 방안을 고민해보았습니다.&lt;/p&gt;
&lt;p&gt;첫 번째 문제인 index는, (product_id, purchase_dt)을 묶은 composite index로 해결하려고 하였습니다. online alter와 동일하게 Staging 복제본 Database에서 테스트해보았습니다. 그러나 아쉽게도 쿼리 성능 개선은 10% 정도로 미미하였고, index를 추가하는 비용에 비해 효과가 유의미하지 않다고 판단하여 현 상태를 유지하기로 하였습니다.&lt;/p&gt;
&lt;p&gt;두 번째 문제인 최적화는, 쿼리의 컨디션을 최대한 묶고 현재 시간 기반 쿼리를 지우는 방식으로 해결하려고 하였습니다. 업데이트 때마다 새로운 상품이 출시되는 [쿠키런: 오븐브레이크] 특성상, 판매 시작 시간을 공유하는 상품들이 많아 해당 product_id의 컨디션을 묶는 방법으로 개선을 할 수 있었습니다. 또한 현재 시간 기반 쿼리는, 미래 시점에서 구매된 상품이 있을 수는 없으므로 무의미한 컨디션이었기 때문에 삭제하였습니다. 그 결과 다음과 같은 쿼리로 개선하였습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;SELECT&lt;/span&gt; product_id&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;COUNT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;AS&lt;/span&gt; count
&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; $DB_NAME
&lt;span class=&quot;token keyword&quot;&gt;WHERE&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;product_id &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; ‘aaa’ &lt;span class=&quot;token operator&quot;&gt;or&lt;/span&gt; product_id &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; ‘bbb’&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;AND&lt;/span&gt; {판매 시작 시간} &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; purchase_dt&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;기존 쿼리에 비해 한결 짧아진 것이 눈으로도 보이는 쿼리가 되었는데요. 아쉽게도 이 쿼리 역시 성능 개선은 크지 않았지만, 부정적인 점이 없는 변경사항이었기 때문에 바로 적용하였습니다.&lt;/p&gt;
&lt;h1 id=&quot;문제-원인2-캐시-사용-패턴&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%B8%EC%A0%9C-%EC%9B%90%EC%9D%B82-%EC%BA%90%EC%8B%9C-%EC%82%AC%EC%9A%A9-%ED%8C%A8%ED%84%B4&quot; aria-label=&quot;문제 원인2 캐시 사용 패턴 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;문제 원인2. 캐시 사용 패턴&lt;/h1&gt;
&lt;p&gt;사실 쿼리보다 주요하게 다룬 문제는 서버 내부의 캐시의 사용 패턴이었는데요. 이를 이해하기 위해서는 [쿠키런: 오븐브레이크]의 서버 구조에 대한 설명이 필요합니다. [쿠키런: 오븐브레이크]는 AWS autoscaling group을 활용한 수평 분산이 가능한 서버이고, 각 서버마다 독립적으로 캐시를 만들고 관리합니다.
그리고 기획팀은 필요할 때마다 게임데이터를 수정해서 업로드합니다. 이로 인해 여러 버전의 게임 데이터가 공존할 수 있습니다. 그리고 게임데이터마다 캐시 결과가 달라질 수 있으므로 게임데이터 버전에 따라 새롭게 캐시해야 합니다. 즉, 게임데이터마다 별개의 캐시가 만들어집니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAPABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAIF/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEAMQAAABwbkSD//EABgQAQEBAQEAAAAAAAAAAAAAAAABESEx/9oACAEBAAEFAsq9eNjYr//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8BP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8BP//EABcQAAMBAAAAAAAAAAAAAAAAAAAQMRH/2gAIAQEABj8CJihF/8QAGxABAAMAAwEAAAAAAAAAAAAAAQARITFRccH/2gAIAQEAAT8hGrOZlmReERVJTAA+pl47iFsKn//aAAwDAQACAAMAAAAQgM//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/ED//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/ED//xAAZEAEAAwEBAAAAAAAAAAAAAAABABExQSH/2gAIAQEAAT8QMVqsnm1sKSIBOg9Y5cGjDBagi3LmC/G47oHl3P/Z&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/5cff03b822ff16375f4ef2e45193cd65/33ecc/cache.webp 360w,
/static/5cff03b822ff16375f4ef2e45193cd65/263a4/cache.webp 480w,
/static/5cff03b822ff16375f4ef2e45193cd65/0c8fb/cache.webp 640w,
/static/5cff03b822ff16375f4ef2e45193cd65/521f7/cache.webp 860w,
/static/5cff03b822ff16375f4ef2e45193cd65/d71bc/cache.webp 1024w&quot;
              sizes=&quot;(max-width: 1024px) 100vw, 1024px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/5cff03b822ff16375f4ef2e45193cd65/543cd/cache.jpg 360w,
/static/5cff03b822ff16375f4ef2e45193cd65/a3e66/cache.jpg 480w,
/static/5cff03b822ff16375f4ef2e45193cd65/56d4e/cache.jpg 640w,
/static/5cff03b822ff16375f4ef2e45193cd65/2f567/cache.jpg 860w,
/static/5cff03b822ff16375f4ef2e45193cd65/63217/cache.jpg 1024w&quot;
            sizes=&quot;(max-width: 1024px) 100vw, 1024px&quot;
            type=&quot;image/jpeg&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/5cff03b822ff16375f4ef2e45193cd65/63217/cache.jpg&quot;
            alt=&quot;기존 캐시 구조. 동일한 값을 사용하는 캐시가 많다&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;기존 캐시 구조. 동일한 값을 사용하는 캐시가 많다&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;문제는 패키지의 판매 시작 시간도 게임데이터에 의존한다는 점입니다. 결과적으로는 캐싱을 하고 있지만, 쿼리를 (분산 서버 수) * (게임데이터 가짓수)만큼 실행하게 되었습니다. 게다가 서버를 업데이트하면 캐시가 비어있는 서버가 늘어나 쿼리를 더더욱 많이 실행하여 순식간에 Database에 부하를 가하게 된 것이었습니다. 개발 환경에서는 결제 데이터가 많지 않아 쿼리가 빨랐고, 분산 서버 수도 적어 이러한 문제를 발견하기 어려웠습니다.&lt;/p&gt;
&lt;p&gt;이를 해결하기 위해서, 모든 서버들이 공통으로 사용할 수 있는 캐시를 별도로 운영하는 것으로 결정하였습니다. 그렇게 만들어진 단일 캐시는 &quot;스케쥴러 서버&quot;라는 이름의 별도의 서버에서 관리합니다. 이 서버는 주기적으로 주어진 요청을 처리합니다. 이를 활용하여 아래와 같은 스텝으로 구현을 변경하였습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;서버는 결과가 필요할 때 2가지 로직을 실행합니다
&lt;ul&gt;
&lt;li&gt;Database에 직접 쿼리하는 대신 원격 저장소에 저장된 결과를 조회합니다. 저장된 결과가 없으면 빈 값으로 처리합니다&lt;/li&gt;
&lt;li&gt;스케쥴러 서버로의 요청을 큐에 저장합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;스케줄러 서버는 주기적으로 큐에서 모든 요청을 꺼내고, 마지막 요청을 기반으로 쿼리를 하여 결과를 계산합니다&lt;/li&gt;
&lt;li&gt;스케줄러 서버가 계산한 결과를 원격 저장소에 저장(캐싱)합니다&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;쿼리를 각각의 서버가 실행하는 것이 아닌, 스케줄러 서버가 일괄적으로 실행함에 따라서 쿼리의 실행 횟수를 줄여서 부하를 줄일 수 있었습니다.&lt;/p&gt;
&lt;h1 id=&quot;문제-해결-완료&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0-%EC%99%84%EB%A3%8C&quot; aria-label=&quot;문제 해결 완료 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;문제 해결 완료&lt;/h1&gt;
&lt;p&gt;쿼리 최적화와 캐시 사용 패턴 개선을 적용한 다음, 조심스럽게 &lt;code&gt;패키지 매출 랭킹&lt;/code&gt; 기능을 다시 활성화였을 때, 변경된 CPU 그래프는 다음과 같았습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1600px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 44.99999999999999%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAIAAAC9o5sfAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA10lEQVR42p2Q224DIQxE+f/PbNKHNrubZhcbg7HpQPJApEipio7MeGyZS9iP47qtkqi5upW3VBWKe4y7CIeqydO58cnp40/wqXHvb/IVTJMR8k+faINZzwz/3OQSVLW5/QsPtZqaV7OqtTqE9ziEDWGD7j8DEyfbwRq5RMqR9aBBd3rs4pGWR/VeosJSg7tzSomTSCaClCwFYlm2bf1Z1+v3Zbnd8LeZmXPOaABYpZSg1ZAoNnNEvOIOdC6aM3p09pF2rygIxGpm7dXCePTNDmYQM4jUb/kL5CYMqXoNHaMAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/9feb4605357b49daf256c2c2c6502e7b/33ecc/cpu_3.webp 360w,
/static/9feb4605357b49daf256c2c2c6502e7b/263a4/cpu_3.webp 480w,
/static/9feb4605357b49daf256c2c2c6502e7b/0c8fb/cpu_3.webp 640w,
/static/9feb4605357b49daf256c2c2c6502e7b/521f7/cpu_3.webp 860w,
/static/9feb4605357b49daf256c2c2c6502e7b/cbd37/cpu_3.webp 1200w,
/static/9feb4605357b49daf256c2c2c6502e7b/64296/cpu_3.webp 1600w&quot;
              sizes=&quot;(max-width: 1600px) 100vw, 1600px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/9feb4605357b49daf256c2c2c6502e7b/53918/cpu_3.png 360w,
/static/9feb4605357b49daf256c2c2c6502e7b/9aebd/cpu_3.png 480w,
/static/9feb4605357b49daf256c2c2c6502e7b/0f09e/cpu_3.png 640w,
/static/9feb4605357b49daf256c2c2c6502e7b/3e3fe/cpu_3.png 860w,
/static/9feb4605357b49daf256c2c2c6502e7b/64756/cpu_3.png 1200w,
/static/9feb4605357b49daf256c2c2c6502e7b/42cbc/cpu_3.png 1600w&quot;
            sizes=&quot;(max-width: 1600px) 100vw, 1600px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/9feb4605357b49daf256c2c2c6502e7b/42cbc/cpu_3.png&quot;
            alt=&quot;돌아온 CPU&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;돌아온 CPU&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;중간중간 작은 피크들을 통해 스케쥴러 서버가 쿼리를 실행하고 있다는 것이 알 수 있습니다. 이러한 피크를 제외하면 전체적으로는 문제 없이 CPU가 유지되었고 기능 자체도 성공적으로 작동하였습니다. 마침내 모든 문제를 해결한 것입니다.&lt;/p&gt;
&lt;h1 id=&quot;결론&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%B0%EB%A1%A0&quot; aria-label=&quot;결론 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;결론&lt;/h1&gt;
&lt;p&gt;Database에 진행한 작업이 많지 않지만, 그 중에서도 &lt;code&gt;패키지 매출 랭킹&lt;/code&gt; 관련 작업은 저에게 많이 인상 깊은 작업이었습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Production 환경에서의 online alter를 위해 &lt;strong&gt;Percona Toolkit&lt;/strong&gt;이라는 툴을 사용하여 성공적으로 진행한 경험을 쌓았습니다. 이후 몇 번 더 online alter를 해야하는 작업이 있었는데, 그 때마다 이 툴을 사용하여 문제 없이 진행할 수 있었습니다. 이 과정에서 유용한 툴을 잘 활용하는 것에 대한 중요성을 깨달았습니다.&lt;/li&gt;
&lt;li&gt;무거운 쿼리와 분산 서버 환경에서의 캐시 시스템이 합쳐져 예상치 못한 문제를 야기하였는데, 사전에 문제될 만한 지점을 잘 파악하는 것 또한 중요하다는 것을 배웠습니다&lt;/li&gt;
&lt;li&gt;장애가 발생하였을 때 원인 파악도 중요하지만 서비스 정상화를 우선시 해야 한다는 것을 새삼 깨달았습니다. 이번에는 문제 되는 기능의 활성화 여부를 설정할 수 있는 기능을 적용하여 해결했는데, 이러한 안전 장치를 미리 구현해두면 보다 안정적인 서비스를 운영하는데 도움이 될 것입니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 글이 Database 작업을 하는 다른 분들께도 도움이 되길 바라며 마무리하겠습니다. 긴 글 읽어주셔서 감사합니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[웹 개발자의 데이터 애플리케이션 flow 효율화하기]]></title><description><![CDATA[Dash 기반으로 데이터 대시보드를 구축한 이야기를 공유합니다.]]></description><link>https://tech.devsisters.com/posts/dash-data-flow</link><guid isPermaLink="false">https://tech.devsisters.com/posts/dash-data-flow</guid><pubDate>Wed, 02 Aug 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, 저는 데브시스터즈 진저랩 웹서비스셀에서 소프트웨어 엔지니어로 일하고 있는 안정현입니다. 이 글에서는 데이터 앱 프레임워크인 Dash를 도입하면서 겪었던 아키텍처 측면의 이슈와 개선에 대해 공유해 드리고자 합니다.&lt;/p&gt;
&lt;p&gt;데브시스터즈에서는 이미 &lt;a href=&quot;https://mode.com/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Mode&lt;/a&gt;나 &lt;a href=&quot;https://www.databricks.com/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Databricks&lt;/a&gt;를 사용해 데이터의 수집, 적재, 시각화까지 모든 프로세스가 이미 잘 되어있었습니다.
하지만 이 많은 데이터를 시각화하기 위한 성능적인 측면과, 데이터를 보는 사용자에게 더욱 많은 인사이트를 제공하기 위한 다양한 기능들이 필요했습니다.&lt;/p&gt;
&lt;p&gt;이를 위해 저희는 순수하게 Python만 사용해서 데이터 엔지니어도 쉽게 개발이 가능하면서도 직접 만든 React Component를 연동할 수 있어서 웹 개발자들에게도 친화적인 data app framework인 &lt;a href=&quot;https://plotly.com/dash/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Dash&lt;/a&gt;를 사용해 자체적인 서비스를 구현하기로 했습니다.&lt;/p&gt;
&lt;h1 id=&quot;why-dash&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#why-dash&quot; aria-label=&quot;why dash permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;strong&gt;Why Dash?&lt;/strong&gt;&lt;/h1&gt;
&lt;p&gt;data app 개발을 위한 프레임워크가 많이 존재하고 있지만, Dash를 선택한 이유는 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;차트 라이브러리인 &lt;strong&gt;plotly.js를 개발한 Plotly에서 개발&lt;/strong&gt;했기 때문에 차트 시각화 대시보드를 만들기에 궁합이 잘 맞습니다.&lt;/li&gt;
&lt;li&gt;직접 만든 React 컴포넌트를 만들어서 사용할 수 있기 때문에 웹 개발자가 익숙한 방식으로 개발이 가능하고 &lt;strong&gt;사내 디자인시스템을 쉽게 적용&lt;/strong&gt;할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;stateless한 디자인&lt;/strong&gt;으로 scale out을 자유롭게 할 수 있어서 High Availability 구성이 가능합니다.&lt;/li&gt;
&lt;li&gt;Top of flask로 개발되어 있어서 아래의 예시와 같은 &lt;strong&gt;web app의 기능을 그대로 사용&lt;/strong&gt;할 수 있습니다.
&lt;ul&gt;
&lt;li&gt;middleware 연동 (Single Sign-On, ExceptionHandler, Cache 등)&lt;/li&gt;
&lt;li&gt;추가적인 API 구현&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/HTTP/Compression&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;HTTP 압축&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id=&quot;dash-internals&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#dash-internals&quot; aria-label=&quot;dash internals permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;strong&gt;Dash Internals&lt;/strong&gt;&lt;/h1&gt;
&lt;p&gt;Dash의 동작 방식을 이해하면 callback 설계를 더 효율적으로 할 수 있습니다. 기본적인 렌더링 프로세스는 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;브라우저에서 Dash 페이지 접속&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;서버는 기본 HTML 템플릿 반환&lt;/li&gt;
&lt;li&gt;필요한 JavaScript 번들 로드&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;레이아웃 데이터 요청 (&lt;code&gt;_dash-layout&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;서버에서 정의된 레이아웃 구조 로드&lt;/li&gt;
&lt;li&gt;컴포넌트 트리 구성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;의존성 정보 요청 (&lt;code&gt;_dash-dependencies&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Input/Output 관계 매핑&lt;/li&gt;
&lt;li&gt;콜백 그래프 구성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;초기 상태 설정&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;컴포넌트 초기값 설정&lt;/li&gt;
&lt;li&gt;초기 렌더링 수행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이후 사용자 인터랙션이 발생하면 &lt;code&gt;/_dash-update-component&lt;/code&gt; API를 통해 해당하는 callback이 실행되며 화면이 업데이트됩니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2048px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 35.833333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAHABQDASIAAhEBAxEB/8QAFwABAAMAAAAAAAAAAAAAAAAAAAECBf/EABYBAQEBAAAAAAAAAAAAAAAAAAEAAv/aAAwDAQACEAMQAAAB3KmGRH//xAAUEAEAAAAAAAAAAAAAAAAAAAAQ/9oACAEBAAEFAn//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/AT//xAAVEQEBAAAAAAAAAAAAAAAAAAABEP/aAAgBAgEBPwEn/8QAFBABAAAAAAAAAAAAAAAAAAAAEP/aAAgBAQAGPwJ//8QAFxABAQEBAAAAAAAAAAAAAAAAAQARIf/aAAgBAQABPyGV7G3/2gAMAwEAAgADAAAAEHf/AP/EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8QP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8QH//EABkQAQEBAAMAAAAAAAAAAAAAAAERACFBUf/aAAgBAQABPxBoPLDAPRO7krbv/9k=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/4a27780ed0c0ce4699a844cd9cc4e416/33ecc/dash_internal.webp 360w,
/static/4a27780ed0c0ce4699a844cd9cc4e416/263a4/dash_internal.webp 480w,
/static/4a27780ed0c0ce4699a844cd9cc4e416/0c8fb/dash_internal.webp 640w,
/static/4a27780ed0c0ce4699a844cd9cc4e416/521f7/dash_internal.webp 860w,
/static/4a27780ed0c0ce4699a844cd9cc4e416/cbd37/dash_internal.webp 1200w,
/static/4a27780ed0c0ce4699a844cd9cc4e416/96d48/dash_internal.webp 2048w&quot; sizes=&quot;(max-width: 2048px) 100vw, 2048px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/4a27780ed0c0ce4699a844cd9cc4e416/543cd/dash_internal.jpg 360w,
/static/4a27780ed0c0ce4699a844cd9cc4e416/a3e66/dash_internal.jpg 480w,
/static/4a27780ed0c0ce4699a844cd9cc4e416/56d4e/dash_internal.jpg 640w,
/static/4a27780ed0c0ce4699a844cd9cc4e416/2f567/dash_internal.jpg 860w,
/static/4a27780ed0c0ce4699a844cd9cc4e416/03ffe/dash_internal.jpg 1200w,
/static/4a27780ed0c0ce4699a844cd9cc4e416/09658/dash_internal.jpg 2048w&quot; sizes=&quot;(max-width: 2048px) 100vw, 2048px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/4a27780ed0c0ce4699a844cd9cc4e416/09658/dash_internal.jpg&quot; alt=&quot;dash internal&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;Dash 아키텍처 다이어그램&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h1 id=&quot;dash-callback&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#dash-callback&quot; aria-label=&quot;dash callback permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;strong&gt;Dash callback&lt;/strong&gt;&lt;/h1&gt;
&lt;p&gt;Dash는 브라우저의 이벤트를 받아서 callback이라고 하는 추상화된 API를 호출합니다. callback의 Input과 Output으로는 Dash로 작성한 HTML element의 property를 받을 수 있습니다.&lt;/p&gt;
&lt;p&gt;callback은 Python으로 작성한 callback과 Javascript로 작성한 clientside callback으로 구분됩니다.&lt;/p&gt;
&lt;p&gt;아래는 하나의 callback으로 데이터 요청 후 차트를 그리는 가장 간단한 구조를 도식화한 것입니다. 4단계에서 반환하는 figure는 차트를 그리는 데 필요한 데이터를 담고 있는 객체입니다.&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 25.27777777777778%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAFABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAEF/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAH/2gAMAwEAAhADEAAAAd6CUL//xAAWEAEBAQAAAAAAAAAAAAAAAAAAARH/2gAIAQEAAQUCYj//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/AT//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/AT//xAAWEAEBAQAAAAAAAAAAAAAAAAAAMRH/2gAIAQEABj8CVdf/xAAYEAADAQEAAAAAAAAAAAAAAAAAAREhMf/aAAgBAQABPyGb0TK2hWlof//aAAwDAQACAAMAAAAQ/A//xAAVEQEBAAAAAAAAAAAAAAAAAAAQEf/aAAgBAwEBPxCH/8QAFREBAQAAAAAAAAAAAAAAAAAAEBH/2gAIAQIBAT8Qp//EABgQAQEBAQEAAAAAAAAAAAAAAAERADFB/9oACAEBAAE/EBibb5igqeKc0eyvUm//2Q==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/1381282f5882e664600c75508116f1a1/33ecc/architecture_basic.webp 360w,
/static/1381282f5882e664600c75508116f1a1/263a4/architecture_basic.webp 480w,
/static/1381282f5882e664600c75508116f1a1/0c8fb/architecture_basic.webp 640w,
/static/1381282f5882e664600c75508116f1a1/521f7/architecture_basic.webp 860w,
/static/1381282f5882e664600c75508116f1a1/cbd37/architecture_basic.webp 1200w,
/static/1381282f5882e664600c75508116f1a1/548e7/architecture_basic.webp 2400w,
/static/1381282f5882e664600c75508116f1a1/94af5/architecture_basic.webp 4294w&quot;
              sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/1381282f5882e664600c75508116f1a1/543cd/architecture_basic.jpg 360w,
/static/1381282f5882e664600c75508116f1a1/a3e66/architecture_basic.jpg 480w,
/static/1381282f5882e664600c75508116f1a1/56d4e/architecture_basic.jpg 640w,
/static/1381282f5882e664600c75508116f1a1/2f567/architecture_basic.jpg 860w,
/static/1381282f5882e664600c75508116f1a1/03ffe/architecture_basic.jpg 1200w,
/static/1381282f5882e664600c75508116f1a1/fdb5a/architecture_basic.jpg 2400w,
/static/1381282f5882e664600c75508116f1a1/8ca4c/architecture_basic.jpg 4294w&quot;
            sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
            type=&quot;image/jpeg&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/1381282f5882e664600c75508116f1a1/fdb5a/architecture_basic.jpg&quot;
            alt=&quot;architecture basic&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;비교적 간단해 보이는 이 구조는 아래와 같은 문제점이 있습니다.&lt;/p&gt;
&lt;p&gt;callback의 Input과 Output이 많아질수록 복잡해지기 쉽고, 유지보수가 힘들어집니다. 하나의 callback 내에서 query부터 render까지의 모든 로직을 넣게 되면 다음과 같은 문제가 발생합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;시각화에 필요한 모든 로직을 넣어야 해서 &lt;strong&gt;코드 덩어리가 커지고 경계가 모호해집니다.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;이미 가져온 데이터의 &lt;strong&gt;보여주는 방식만 변경하고 싶은 경우에도 새롭게 query&lt;/strong&gt;를 해야 합니다.&lt;/li&gt;
&lt;li&gt;하나의 쿼리로 여러 차트를 그리는 경우 모든 로직이 순서대로 실행되기 때문에, &lt;strong&gt;n 배의 시간&lt;/strong&gt;이 걸리게 됩니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;실제 제품에서는 아래와 같은 추가적인 비즈니스 로직이 필요합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;데이터가 없는 경우 차트 대신 넘어진 용감한쿠키를 출력&lt;/li&gt;
&lt;li&gt;데이터 캐싱&lt;/li&gt;
&lt;li&gt;선택한 통화에 알맞은 표기로 전환 (ex. USD → $10.06만, KRW → ￦ 1.27억)&lt;/li&gt;
&lt;li&gt;선택한 언어에 알맞은 표기로 전환 (ex. EN → ￦ 127.35M, KR → ￦ 1.27억)&lt;/li&gt;
&lt;/ul&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 526px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 49.44444444444444%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAKABQDASIAAhEBAxEB/8QAFwABAAMAAAAAAAAAAAAAAAAAAAMEBf/EABQBAQAAAAAAAAAAAAAAAAAAAAH/2gAMAwEAAhADEAAAAdyGyEE//8QAGBABAAMBAAAAAAAAAAAAAAAAAgABESD/2gAIAQEAAQUCSyC3vH//xAAVEQEBAAAAAAAAAAAAAAAAAAAQEf/aAAgBAwEBPwGn/8QAFREBAQAAAAAAAAAAAAAAAAAAEBH/2gAIAQIBAT8Bh//EABkQAAEFAAAAAAAAAAAAAAAAAAABESAhIv/aAAgBAQAGPwKkc1H/xAAbEAABBQEBAAAAAAAAAAAAAAABABARUXEhMf/aAAgBAQABPyEg+nVInga0ClApv//aAAwDAQACAAMAAAAQQ8//xAAWEQEBAQAAAAAAAAAAAAAAAAARARD/2gAIAQMBAT8QggZ//8QAFhEBAQEAAAAAAAAAAAAAAAAAEQEQ/9oACAECAQE/EKquf//EABsQAQACAgMAAAAAAAAAAAAAAAEAETGRIWGB/9oACAEBAAE/ECC9lIArccBYPCPKgjhGWZGp1NQAwBP/2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/6ef17183c4d94dd7cb5a96f60341369f/33ecc/falldown_brave.webp 360w,
/static/6ef17183c4d94dd7cb5a96f60341369f/263a4/falldown_brave.webp 480w,
/static/6ef17183c4d94dd7cb5a96f60341369f/7ecc4/falldown_brave.webp 526w&quot; sizes=&quot;(max-width: 526px) 100vw, 526px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/6ef17183c4d94dd7cb5a96f60341369f/543cd/falldown_brave.jpg 360w,
/static/6ef17183c4d94dd7cb5a96f60341369f/a3e66/falldown_brave.jpg 480w,
/static/6ef17183c4d94dd7cb5a96f60341369f/d258d/falldown_brave.jpg 526w&quot; sizes=&quot;(max-width: 526px) 100vw, 526px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/6ef17183c4d94dd7cb5a96f60341369f/d258d/falldown_brave.jpg&quot; alt=&quot;falldown brave&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;데이터가 없는 경우 차트 대신 넘어진 용감한쿠키를 출력&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;이렇게 다양한 로직을 하나의 callback에 넣게 되면, 코드가 길어지고, 유지보수가 힘들어집니다.&lt;/p&gt;
&lt;h2 id=&quot;callback을-분리하고-연결하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#callback%EC%9D%84-%EB%B6%84%EB%A6%AC%ED%95%98%EA%B3%A0-%EC%97%B0%EA%B2%B0%ED%95%98%EA%B8%B0&quot; aria-label=&quot;callback을 분리하고 연결하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;callback을 분리하고 연결하기&lt;/h2&gt;
&lt;p&gt;위의 문제를 해결하기 위해 아래와 같은 방법을 사용했습니다.&lt;/p&gt;
&lt;p&gt;하나의 callback을 각 역할에 맞는 callback으로 분리하는 작업을 진행하였고, 각 callback에는 &lt;strong&gt;역할에 맞는 로직&lt;/strong&gt;만 작성할 수 있도록 규칙을 정했습니다.&lt;/p&gt;
&lt;p&gt;일반적인 웹 애플리케이션 개발과 비슷한 경험을 이어 나가고자, &lt;strong&gt;MVC 패턴과 같이 크게 3가지 영역&lt;/strong&gt;으로 나누었습니다. Dash가 top of React이고 React에서는 Provider라는 컴포넌트 형태의 wrapper를 사용하는 것에서 착안하여 각각의 이름을 FetchProvider, TransformProvider, RenderProvider로 정했고, 각 역할은 아래와 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;FetchProvider: Databricks, Redis 등의 datastore로부터 데이터를 가져옵니다.&lt;/li&gt;
&lt;li&gt;TransformProvider: 가져온 데이터를 차트를 그리기 쉬운 형태로 가공합니다.&lt;/li&gt;
&lt;li&gt;RenderProvider: 가공된 데이터를 기반으로 차트를 그립니다. (Figure 객체를 생성합니다)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;모든 callback은 API이므로, Output은 브라우저로 넘어가고, 다음 callback의 Input과 State로 재사용할 수 있습니다. 다시 말하자면, 언어 변경이나 누적값으로의 표현 방법 변경 등의 단순 view 변경 시 query를 다시 요청하지 않고, 이미 &lt;strong&gt;브라우저에 저장된 데이터를 재사용&lt;/strong&gt;할 수 있다는 뜻입니다.&lt;/p&gt;
&lt;p&gt;저장된 데이터를 재사용하는 경우, transform(아래 그림의 녹색 화살표)부터 실행되어, &lt;strong&gt;db를 거치지 않고도 변경된 차트를 화면에 반영&lt;/strong&gt;할 수 있게 됩니다.&lt;/p&gt;
&lt;p&gt;브라우저와 서버 간에 여러 번의 데이터 핑퐁이 있기 때문에 데이터를 빠르게 주고받는 것이 중요합니다. 저희는 &lt;a href=&quot;https://developer.chrome.com/ko/docs/lighthouse/performance/uses-text-compression/#%EC%84%9C%EB%B2%84%EC%97%90%EC%84%9C-%ED%85%8D%EC%8A%A4%ED%8A%B8-%EC%95%95%EC%B6%95%EC%9D%84-%ED%99%9C%EC%84%B1%ED%99%94%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;gzip, br 두 가지의 압축&lt;/a&gt;을 적용하여 데이터를 약 1/5 크기로 줄였습니다.&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 53.05555555555556%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAEF/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEAMQAAAB3lBR/8QAFBABAAAAAAAAAAAAAAAAAAAAIP/aAAgBAQABBQJf/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPwE//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPwE//8QAFBABAAAAAAAAAAAAAAAAAAAAIP/aAAgBAQAGPwJf/8QAGBABAAMBAAAAAAAAAAAAAAAAEQABISD/2gAIAQEAAT8hSbKTeP/aAAwDAQACAAMAAAAQMA//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/ED//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/ED//xAAbEAADAQADAQAAAAAAAAAAAAABESEAEDFRYf/aAAgBAQABPxA0N78eRGGZFAT8yGS4/9k=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/64471cf0e4e0d8f7155d9117965dd4db/33ecc/architecture_callback_chained.webp 360w,
/static/64471cf0e4e0d8f7155d9117965dd4db/263a4/architecture_callback_chained.webp 480w,
/static/64471cf0e4e0d8f7155d9117965dd4db/0c8fb/architecture_callback_chained.webp 640w,
/static/64471cf0e4e0d8f7155d9117965dd4db/521f7/architecture_callback_chained.webp 860w,
/static/64471cf0e4e0d8f7155d9117965dd4db/cbd37/architecture_callback_chained.webp 1200w,
/static/64471cf0e4e0d8f7155d9117965dd4db/548e7/architecture_callback_chained.webp 2400w,
/static/64471cf0e4e0d8f7155d9117965dd4db/171a5/architecture_callback_chained.webp 3688w&quot;
              sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/64471cf0e4e0d8f7155d9117965dd4db/543cd/architecture_callback_chained.jpg 360w,
/static/64471cf0e4e0d8f7155d9117965dd4db/a3e66/architecture_callback_chained.jpg 480w,
/static/64471cf0e4e0d8f7155d9117965dd4db/56d4e/architecture_callback_chained.jpg 640w,
/static/64471cf0e4e0d8f7155d9117965dd4db/2f567/architecture_callback_chained.jpg 860w,
/static/64471cf0e4e0d8f7155d9117965dd4db/03ffe/architecture_callback_chained.jpg 1200w,
/static/64471cf0e4e0d8f7155d9117965dd4db/fdb5a/architecture_callback_chained.jpg 2400w,
/static/64471cf0e4e0d8f7155d9117965dd4db/13254/architecture_callback_chained.jpg 3688w&quot;
            sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
            type=&quot;image/jpeg&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/64471cf0e4e0d8f7155d9117965dd4db/fdb5a/architecture_callback_chained.jpg&quot;
            alt=&quot;architecture callback chained&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;h2 id=&quot;mixin으로-차트-그리는-로직-합치기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#mixin%EC%9C%BC%EB%A1%9C-%EC%B0%A8%ED%8A%B8-%EA%B7%B8%EB%A6%AC%EB%8A%94-%EB%A1%9C%EC%A7%81-%ED%95%A9%EC%B9%98%EA%B8%B0&quot; aria-label=&quot;mixin으로 차트 그리는 로직 합치기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Mixin으로 차트 그리는 로직 합치기&lt;/h2&gt;
&lt;p&gt;callback을 통해 요구사항은 충족되었지만, 차트를 그리는 코드가 재사용 되지 않는 문제가 여전히 남아있습니다.&lt;/p&gt;
&lt;p&gt;또한 차트에서 데브시스터즈의 다양한 이벤트(대규모 업데이트, 마케팅 등)를 차트에서 같이 확인할 수 있는 history 기능도 제공하고 있는데요, 이러한 차트를 그리는 코드가 render 콜백 여기저기서 반복된다면 유지보수가 힘들기 때문에 저희는 사용하는 패턴별로 차트를 모듈화하고자 했습니다.&lt;/p&gt;
&lt;p&gt;디자인시스템의 톤앤매너에 맞게 차트의 스타일을 잡아야 했기 때문에 Dash의 Figure를 상속받아서 기본적인 디자인이 들어간 Figure 클래스를 생성하였고, Bar, Line, Area 등 다양한 시각화 로직을 Mixin 단위로 상속받는 방식을 택했습니다. Mixin에 대한 자세한 설명은 &lt;a href=&quot;https://ko.wikipedia.org/wiki/%EB%AF%B9%EC%8A%A4%EC%9D%B8&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;링크&lt;/a&gt;를 참고해주세요.&lt;/p&gt;
&lt;figure&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 46.666666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAJABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAgABBf/EABUBAQEAAAAAAAAAAAAAAAAAAAAB/9oADAMBAAIQAxAAAAHvlYGUn//EABcQAQADAAAAAAAAAAAAAAAAAAEAIDH/2gAIAQEAAQUCjpT/xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/AT//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/AT//xAAUEAEAAAAAAAAAAAAAAAAAAAAg/9oACAEBAAY/Al//xAAZEAACAwEAAAAAAAAAAAAAAAAAMQEQQYH/2gAIAQEAAT8hEsXezWkH/9oADAMBAAIAAwAAABCj/wD/xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/ED//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/ED//xAAcEAEAAgEFAAAAAAAAAAAAAAABADHBEBEhUYH/2gAIAQEAAT8QiMN49iFbF+zOjWUn/9k=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/50d123b6677963d55e665c279791aa68/33ecc/mixin-figure.webp 360w,
/static/50d123b6677963d55e665c279791aa68/263a4/mixin-figure.webp 480w,
/static/50d123b6677963d55e665c279791aa68/0c8fb/mixin-figure.webp 640w,
/static/50d123b6677963d55e665c279791aa68/521f7/mixin-figure.webp 860w,
/static/50d123b6677963d55e665c279791aa68/cbd37/mixin-figure.webp 1200w,
/static/50d123b6677963d55e665c279791aa68/548e7/mixin-figure.webp 2400w,
/static/50d123b6677963d55e665c279791aa68/93611/mixin-figure.webp 8744w&quot; sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/50d123b6677963d55e665c279791aa68/543cd/mixin-figure.jpg 360w,
/static/50d123b6677963d55e665c279791aa68/a3e66/mixin-figure.jpg 480w,
/static/50d123b6677963d55e665c279791aa68/56d4e/mixin-figure.jpg 640w,
/static/50d123b6677963d55e665c279791aa68/2f567/mixin-figure.jpg 860w,
/static/50d123b6677963d55e665c279791aa68/03ffe/mixin-figure.jpg 1200w,
/static/50d123b6677963d55e665c279791aa68/fdb5a/mixin-figure.jpg 2400w,
/static/50d123b6677963d55e665c279791aa68/608db/mixin-figure.jpg 8744w&quot; sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/50d123b6677963d55e665c279791aa68/fdb5a/mixin-figure.jpg&quot; alt=&quot;mixin figure&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;figcaption&gt;가상의 데이터로, 실제 지표와는 무관합니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;아래는 Mixin 클래스의 예시입니다. (복잡한 구현 부분은 생략하였습니다.)&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# ChartMixin의 예시&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;LineChartMixin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;BaseMixin&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add_cutsom_lines&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;self&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; traces&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Trace&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; trace &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; traces&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;add_custom_line&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;trace&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add_custom_line&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;self&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; trace&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Trace&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;add_trace&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
            graph_objects&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Scatter&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;# Line 차트를 그려준다&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;HistoryMixin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;BaseMixin&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add_histories&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;self&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; histories&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;History&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;_add_background&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;_add_history_dots&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;histories&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;histories&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;_add_background&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;add_shape&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;_add_history_dots&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;histories&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;History&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;add_trace&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
            graph_objects&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Scatter&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;LineFigure&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;BaseAxisFigure&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; LineChartMixin&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; HistoryMixin&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        traces&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Trace&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        histories&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;History&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token builtin&quot;&gt;super&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;__init__&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;add_custom_lines&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;traces&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; histories &lt;span class=&quot;token keyword&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;add_histories&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;histories&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;histories&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h1 id=&quot;정리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%95%EB%A6%AC&quot; aria-label=&quot;정리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;정리&lt;/h1&gt;
&lt;p&gt;데브시스터즈에서는 Dash를 도입하여 목적에 맞게 로직을 분리한 후, chaining 하여 복잡한 인터렉션에도 간단한 코드 구조를 가질 수 있게 되었습니다.
또한 Mixin 클래스를 활용하여 차트마다 사용되는 공통 기능을 관리하기 쉽게 되었고,
gzip, br을 통한 데이터 압축을 통해 많은 데이터를 빠르게 주고받을 수 있게 되었습니다.
Dash를 사용 중인 분들이 계신다면, 이번 글이 도움이 되셨으면 좋겠고, 더 좋은 방법이 있다면 공유해 주시면 좋을 것 같습니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[사운드 리소스 전달 WebApp 만들기]]></title><description><![CDATA[단순한 설계라는 원칙을 바탕으로 게임 개발에 필요한 웹앱을 개발했던 경험을 공유합니다.]]></description><link>https://tech.devsisters.com/posts/building-a-sound-vcs-webapp</link><guid isPermaLink="false">https://tech.devsisters.com/posts/building-a-sound-vcs-webapp</guid><pubDate>Tue, 04 Jul 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, 저는 데브시스터즈에서 Frontend R&amp;#x26;D Engineer로 근무중인 문태근 이라고 합니다. 게임 개발과 운영에 필요한 다양한 백오피스 개발 및 기술 연구를 담당하고 있습니다.&lt;/p&gt;
&lt;p&gt;이 글에서는 게임 개발에 필요한 웹앱을 개발했던 경험을 공유하고, 그 과정에서 가장 중요하게 생각했던 가치관인 “단순한 설계”에 대해 이야기해 보고자 합니다.&lt;/p&gt;
&lt;h1 id=&quot;문제-상황&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%B8%EC%A0%9C-%EC%83%81%ED%99%A9&quot; aria-label=&quot;문제 상황 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;문제 상황&lt;/h1&gt;
&lt;p&gt;게임 클라이언트를 만드는 데는 소스 코드 외에도 게임에 필요한 사운드나 이미지 같은 리소스들이 필요합니다. 일반적으로 리소스 제작팀은 Dropbox, Google Drive 등의 웹 드라이브를 통해서 클라이언트팀에게 리소스를 전달합니다.&lt;/p&gt;
&lt;p&gt;하지만 웹 드라이브를 통한 전달에는 단점이 있습니다. 바로 &lt;code&gt;이미지_최종_진짜최종_final(2).png&lt;/code&gt;과 같이 버전 컨트롤이 어렵다는 것입니다. 특히 게임 개발처럼 업데이트가 빌드 단위로 진행되는 프로젝트의 경우, 파일 개별의 버전이 아니라 전체 파일 단위에 대한 버전 컨트롤이 필요합니다. (특정 시점으로 되돌려야 하는 경우 있음)&lt;/p&gt;
&lt;p&gt;그렇다면 왜 처음부터 Git과 같은 VCS를 바로 쓰지 않고 웹 드라이브를 통해 전달하는 것일까요? 여러 가지 이유가 있지만, 가장 큰 이유는 프로그래머가 아닌 팀원들에게 &lt;strong&gt;Git의 진입장벽이 높기 때문&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;Sourcetree나 Fork 같은 GUI 클라이언트를 쓰면 어느 정도 해결할 수 있으나, 여전히 아래와 같은 문제점이 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Git을 써야 한다는 심리적 부담감&lt;/li&gt;
&lt;li&gt;Merge Conflict 해결의 어려움&lt;/li&gt;
&lt;li&gt;비용 발생 (GUI Client 라이센스, Github 계정 → 사용자 수에 비례)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이렇듯 프로그래머가 아닌 분들이 Git을 사용하기 위해서는 많은 심리적, 물질적 부담이 불가피해 보입니다.&lt;/p&gt;
&lt;p&gt;그래서 쿠키런: 킹덤 개발팀은 &lt;strong&gt;버전 컨트롤이 가능한 사운드 파일 전달 Web App&lt;/strong&gt;(a.k.a Sonic) 을 직접 만들기로 결정했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;del&gt;Git을 써야 한다는 심리적 부담감&lt;/del&gt; → 사용자에게는 최소한의 필요한 interface만 노출 (push, checkout)&lt;/li&gt;
&lt;li&gt;&lt;del&gt;Merge Conflict 해결 어려움&lt;/del&gt; → 무조건 덮어쓰도록 설정&lt;/li&gt;
&lt;li&gt;&lt;del&gt;비용 발생&lt;/del&gt; → 최소한의 서비스 호스팅 비용만 발생 (사용자 수 비례 X)&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id=&quot;0-설계-원칙&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#0-%EC%84%A4%EA%B3%84-%EC%9B%90%EC%B9%99&quot; aria-label=&quot;0 설계 원칙 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;0. 설계 원칙&lt;/h1&gt;
&lt;p&gt;전 마이크로소프트 CTO인 Ray Ozzie는 &lt;a href=&quot;https://www.computerworld.com/article/2513705/ozzie-s--doomsday--memo-warns-microsoft-of-post-pc-days.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;2005년 내부 문건&lt;/a&gt;에서 다음과 같이 말했습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;복잡성&lt;/strong&gt;은 살인적입니다. (Complexity Kills)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;복잡성&lt;/strong&gt;은 사용자, 개발자, IT 부서의 생명을 앗아갑니다. &lt;strong&gt;복잡성&lt;/strong&gt;은 제품을 계획, 구축, 테스트 및 사용하기 어렵게 만듭니다. &lt;strong&gt;복잡성&lt;/strong&gt;은 보안 문제를 초래합니다. &lt;strong&gt;복잡성&lt;/strong&gt;은 관리자의 좌절을 초래합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;실제로 저도 불필요하게 복잡하게 작성된 코드와 시스템으로 인한 문제를 느꼈던 적이 굉장히 많았습니다. 그래서 Sonic을 제작할 때는 &lt;code&gt;UI&lt;/code&gt;와 &lt;code&gt;Architecture&lt;/code&gt; 두 가지 측면에서 복잡성을 최소화하기 위해 노력했습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;User Interface : 사용자에게 노출되는 인터페이스 최소화&lt;/li&gt;
&lt;li&gt;Architecture : 코드 및 컴포넌트 최소화&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이제 각각의 관점에서 어떻게 서비스를 구현했는지 알아보겠습니다.&lt;/p&gt;
&lt;h1 id=&quot;1-user-interface&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-user-interface&quot; aria-label=&quot;1 user interface permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;1. User Interface&lt;/h1&gt;
&lt;p&gt;Sonic의 사용자는 사운드팀과 클라이언트팀 두 가지 그룹으로 나뉘어 집니다. 요구사항을 정리한 결과, 사운드팀은 &lt;code&gt;전달 (push)&lt;/code&gt;, 클라이언트팀은 &lt;code&gt;동기화 (checkout)&lt;/code&gt; 인터페이스만 있으면 충분하다는 결론을 내렸습니다.&lt;/p&gt;
&lt;p&gt;그 외에 잘 사용하지 않으면서 서비스의 복잡도만 증가시키는 기능들은 모두 제외하기로 하였는데, 자세한 내용은 뒤쪽(2. 스펙 줄이기)에서 다루어 보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1898px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 46.666666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAABYlAAAWJQFJUiTwAAABNklEQVR42oVS7W6DMAzk/d9qD7Af1Tr1R4fECoFErCSBAglfNzsdG6u67iQn2OCL70IEwrIsIVbc5uM4QikFKSWcc3iE6Lb5FvM80wkzju8F9m8p5dO1tsGWI+JFa42CTl8JjLXYnwRM3dBzDVFIJELiVJQozxXarkc/THDeB6Kq0j+EnopJkiCOY1giYknl+YynwxFV06LpPLJC4fnlgB3V0lyibi4obQtL743RSLMsDMXDRLxM01UG7wze26b58m9Crj7wekxwiFMIWcL74Vvq2ss+cx7d823rCTdbkn5puxCVtsGGrnchD5JpumXeePgX6UrIzQNNwNE7T0Q9qosnOxzZZCBETtLNVTL+gR8G5FLhlGYQeUGXp8hn/0syx0PJW4z0sSWJ2liSXge57GsgvPMffgIglL1CbzMhhQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/068c0647fe96b164d9d1333396535ed0/33ecc/architecture.webp 360w,
/static/068c0647fe96b164d9d1333396535ed0/263a4/architecture.webp 480w,
/static/068c0647fe96b164d9d1333396535ed0/0c8fb/architecture.webp 640w,
/static/068c0647fe96b164d9d1333396535ed0/521f7/architecture.webp 860w,
/static/068c0647fe96b164d9d1333396535ed0/cbd37/architecture.webp 1200w,
/static/068c0647fe96b164d9d1333396535ed0/44d22/architecture.webp 1898w&quot;
              sizes=&quot;(max-width: 1898px) 100vw, 1898px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/068c0647fe96b164d9d1333396535ed0/53918/architecture.png 360w,
/static/068c0647fe96b164d9d1333396535ed0/9aebd/architecture.png 480w,
/static/068c0647fe96b164d9d1333396535ed0/0f09e/architecture.png 640w,
/static/068c0647fe96b164d9d1333396535ed0/3e3fe/architecture.png 860w,
/static/068c0647fe96b164d9d1333396535ed0/64756/architecture.png 1200w,
/static/068c0647fe96b164d9d1333396535ed0/a9258/architecture.png 1898w&quot;
            sizes=&quot;(max-width: 1898px) 100vw, 1898px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/068c0647fe96b164d9d1333396535ed0/a9258/architecture.png&quot;
            alt=&quot;User Interface&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;User Interface&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💡 용어 정의&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Bundle&lt;/code&gt; : 모든 사운드 파일의 묶음&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Revision&lt;/code&gt; : 번들의 버전 (Git의 commit에 대응). 사운드팀이 내부적으로 관리&lt;/li&gt;
&lt;li&gt;&lt;code&gt;push&lt;/code&gt; : 특정 revision을 서버에 업로드하는 행위 (by 사운드팀)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;checkout&lt;/code&gt; : 특정 revision으로 리소스 파일을 동기화 하는 행위 (by 클라이언트팀)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h1 id=&quot;2-system-architecture&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-system-architecture&quot; aria-label=&quot;2 system architecture permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;2. System Architecture&lt;/h1&gt;
&lt;p&gt;서비스는 &lt;a href=&quot;https://nextjs.org/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Next.js&lt;/a&gt;를 사용한 풀스택 웹앱으로 docker image를 통해 배포하였습니다. API로는 &lt;a href=&quot;https://trpc.io/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;tRPC&lt;/a&gt;를 사용했는데, 백엔드에 엔드포인트를 추가하면 프론트엔드에 이에 대응되는 함수를 자동 생성해주어 API 개발 공수를 크게 줄일 수 있었습니다. 풀스택 앱 개발을 할 일이 있다면 한번 사용해 보시길 추천합니다.&lt;/p&gt;
&lt;h2 id=&quot;21-핵심-기능&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#21-%ED%95%B5%EC%8B%AC-%EA%B8%B0%EB%8A%A5&quot; aria-label=&quot;21 핵심 기능 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;2.1. 핵심 기능&lt;/h2&gt;
&lt;p&gt;우리의 목적은 사운드팀이 업로드한 여러 파일중 &lt;strong&gt;(1) 변경된 파일만 추려내어&lt;/strong&gt; &lt;strong&gt;(2) 시스템에 기록하고&lt;/strong&gt;, &lt;strong&gt;(3) 이 파일들만 클라이언트팀에서 다운로드&lt;/strong&gt;하는 것입니다 (변경사항이 있는 파일만 전달하는게 중요한 이유는, 클라이언트팀에서는 전달받은 파일에 암호화 등 추가 가공을 하기 때문에 변경사항이 없는 파일까지 불필요하게 전달되면 오버헤드가 크게 늘어나기 때문입니다).&lt;/p&gt;
&lt;p&gt;위 3가지 핵심 기능을 어떻게 구현했는지 사용 플로우와 함께 전체 흐름을 알아보겠습니다.&lt;/p&gt;
&lt;h3 id=&quot;211-변경된-파일-추려내기-diff-files&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#211-%EB%B3%80%EA%B2%BD%EB%90%9C-%ED%8C%8C%EC%9D%BC-%EC%B6%94%EB%A0%A4%EB%82%B4%EA%B8%B0-diff-files&quot; aria-label=&quot;211 변경된 파일 추려내기 diff files permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;2.1.1. 변경된 파일 추려내기 (Diff Files)&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/Daninet/hash-wasm/blob/master/src/xxhash32.c&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;xxHash32의 WASM 빌드&lt;/a&gt;를 사용해 이전에 업로드 된 파일의 해시값과 첨부된 파일의 해시값을 비교했습니다. SHA-256은 전체 파일 업로드시 약 1분, xxHash32는 약 3초 정도가 걸려서 최종적으로 xxHash32를 선택했습니다.&lt;/p&gt;
&lt;h3 id=&quot;212-번들-데이터-시스템에-기록하기-persistence&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#212-%EB%B2%88%EB%93%A4-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%97%90-%EA%B8%B0%EB%A1%9D%ED%95%98%EA%B8%B0-persistence&quot; aria-label=&quot;212 번들 데이터 시스템에 기록하기 persistence permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;2.1.2. 번들 데이터 시스템에 기록하기 (Persistence)&lt;/h3&gt;
&lt;p&gt;번들 데이터는 크게 두가지로 구성됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;사운드 파일들&lt;/li&gt;
&lt;li&gt;번들 메타데이터
&lt;ul&gt;
&lt;li&gt;제목&lt;/li&gt;
&lt;li&gt;세부 설명&lt;/li&gt;
&lt;li&gt;Revision&lt;/li&gt;
&lt;li&gt;작성자&lt;/li&gt;
&lt;li&gt;생성 시간&lt;/li&gt;
&lt;li&gt;파일명 및 VersionId&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;사운드 파일들&lt;/strong&gt;은 AWS S3의 &lt;a href=&quot;https://docs.aws.amazon.com/AmazonS3/latest/userguide/Versioning.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Versioning Bucket&lt;/a&gt; 에 저장하였습니다. 메타데이터와 별도로 저장한 이유는 사운드 리소스는 바이너리 파일이기 때문에, 텍스트 형태인 메타데이터와 함께 저장하기 어렵기 때문입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;일반 Bucket : 이름만으로 파일 식별&lt;/li&gt;
&lt;li&gt;Versioning Bucket : Versioning 옵션이 켜진 버킷에서 파일들은 이름 외에 VersionId라는 추가 식별자를 부여받습니다. 다운로드시 이름만 제공하면 최신 버전을, 특정 versionId까지 명시하면 해당 버전의 파일 다운&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그리고 파일 목록 (이름, versionId)을 메타데이터에 넣기로 하였습니다.&lt;/p&gt;
&lt;p&gt;이를 통해 메타데이터만 있으면 해당 번들에 어떤 파일이 있는지와 이들의 S3 versionId를 알 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;메타데이터&lt;/strong&gt;의 경우, DBMS를 사용해 직접 저장하지 않고 local git을 DB처럼 사용하기로 했습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;git에 저장되는 데이터 schema가 Sonic 요구사항에 충분함 → schema 관리 필요 X
```bash
$ echo $CONTENTS &gt; sound_file_meta.csv
$ git add sound_file_meta.csv
$ git commit &lt;br&gt;
--author $YOUR_EMAIL &lt;br&gt;
-am $TITLE &lt;br&gt;
-m $DESCRIPTION&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;$ git tag -a $REVISION -m $REVISION
```
- &lt;strong&gt;제목, 세부 설명&lt;/strong&gt; → Git commit message (multi line)
- &lt;strong&gt;Revision&lt;/strong&gt; → Git tag
- &lt;strong&gt;작성자&lt;/strong&gt; → Git author
- &lt;strong&gt;생성 시간&lt;/strong&gt; → Git commit time
- &lt;strong&gt;파일들의 이름 및 Version Id&lt;/strong&gt; → CSV 로 만들어 파일로 커밋
2.  DBMS에는 없는 버전 컨트롤 기능 제공 → 어플리케이션 로직 (Revision 간 변경사항 찾는 코드) 작성 필요 X
- &lt;code&gt;git diff Rev.98 Rev.100&lt;/code&gt;
3.  git cli 명령어로 DBMS가 제공해주는 query 기능 대체 가능 → 조회 성능 이슈 X
- &lt;code&gt;select * from limit 10&lt;/code&gt; → &lt;code&gt;git log -n 10&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;물론 성능 등 여러 측면에서 차이가 있겠지만, 당장의 요구사항에서 이런 DBMS의 기능들은 큰 의미가 없는 반면, DBMS 도입으로 인한 복잡성을 줄일 수 있는 이점이 더 크다고 판단하였습니다.&lt;/p&gt;
&lt;p&gt;Next.js 에서 git cli 접근을 위해, &lt;code&gt;node:child_process&lt;/code&gt; 모듈의 &lt;code&gt;exec&lt;/code&gt; 을 사용했습니다. 이때 git의 &lt;a href=&quot;https://git-scm.com/docs/pretty-formats&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;pretty formats&lt;/a&gt; 을 사용해 하나의 커밋에 들어가는 정보는 한 줄에 출력되도록 설정했습니다. 이를 통해 stdout으로 출력된 결과물을 쉽게 파싱할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;즉, Sonic은 &lt;code&gt;S3 파일 업로드 + Git 커밋&lt;/code&gt;을 해주는 웹앱으로, 사운드팀의 push flow를 정리하면 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1570px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 74.16666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAABYlAAAWJQFJUiTwAAACdElEQVR42m1TW27bMBD0FYtcJb89QY/SUxRN0X6lBdJH6rSVFFuGJdt6UCRFPac7a8VI0BJYiFwtd2dnlivImucZTdPAGKNf2vF4hLUW/1uM52JcXVUoy+ISu+r7Ht57ZFmmSfI8V9tut+pr2xaM+Sep2CD+L1/v8e7TZ82hCZ1zerEsSzUmfTL6mZxInq9pmtB5i9ZZJLlBlBkt3HUdVtzs93u9SCQMPhwOiKIIaZpKUK+BbHNeEjrf4s3HFN9zh/W3O9zc3KifKBVhmu6w2Wyw2+0UGbnkl4iNqRFCeIHQC4i3tzHypsPr62u8urpaCklCkslkSZIgimOkkpSV+JMX2zZoOy8SSqHDNtFip1OBfZxgrA2sUKMIvT/3f74cYMVnrYNRxe2FcBVjHDE6j6KsJM7reZCkIcvhRPEVgzebraIkZ0VRXBR/Gg+qqft5Qt+e22/lnpFihfB/jCNY6YLdqignSVLXDdxSuRXO+n5Abax+dWxErD50+LET3zhrwto0sNEDzN0tvCheEWGQoKE1GHurKjIodOe5q2Q0au/Q8yxFGt/jVybI2hGdnIvTCdskxp+f9zgs47Uims6VmnSStsqqVk65StegagWRtDI/G+55mUVyzbaNvrIGwzCw5SDoPKYxgDRx7sZxOvM09Ah8KdZeZvDCq1xm0tAFmQinflKjopBMHRMxziLFofqN+Gvhlwk7OZM3qk4Q/M+1jh/w/vaD7jUhRaGyJJQD/fj4iFjmkeeCT1FejReuOtlbUd/tM3gxt0xBukux/r2Wrsbz0yNsHtgCK3BP455G/zCMcFK4EVQUzbdh+d/pK6JoBEYO/wImG3/mhp3NrwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/0b1fdeb4f7630a1b76ce4cd21f5f1d8a/33ecc/push_flow.webp 360w,
/static/0b1fdeb4f7630a1b76ce4cd21f5f1d8a/263a4/push_flow.webp 480w,
/static/0b1fdeb4f7630a1b76ce4cd21f5f1d8a/0c8fb/push_flow.webp 640w,
/static/0b1fdeb4f7630a1b76ce4cd21f5f1d8a/521f7/push_flow.webp 860w,
/static/0b1fdeb4f7630a1b76ce4cd21f5f1d8a/cbd37/push_flow.webp 1200w,
/static/0b1fdeb4f7630a1b76ce4cd21f5f1d8a/0d1c2/push_flow.webp 1570w&quot;
              sizes=&quot;(max-width: 1570px) 100vw, 1570px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/0b1fdeb4f7630a1b76ce4cd21f5f1d8a/53918/push_flow.png 360w,
/static/0b1fdeb4f7630a1b76ce4cd21f5f1d8a/9aebd/push_flow.png 480w,
/static/0b1fdeb4f7630a1b76ce4cd21f5f1d8a/0f09e/push_flow.png 640w,
/static/0b1fdeb4f7630a1b76ce4cd21f5f1d8a/3e3fe/push_flow.png 860w,
/static/0b1fdeb4f7630a1b76ce4cd21f5f1d8a/64756/push_flow.png 1200w,
/static/0b1fdeb4f7630a1b76ce4cd21f5f1d8a/958fa/push_flow.png 1570w&quot;
            sizes=&quot;(max-width: 1570px) 100vw, 1570px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/0b1fdeb4f7630a1b76ce4cd21f5f1d8a/958fa/push_flow.png&quot;
            alt=&quot;Push flow&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Push flow&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;첨부 파일 해시값 비교를 통해 변경사항이 있는 파일만 S3에 업로드 후&lt;/li&gt;
&lt;li&gt;S3 업로드 response에 담긴 S3 file versionId를 포함해 번들 메타데이터 생성&lt;/li&gt;
&lt;li&gt;Sonic 서버에 메타데이터 push&lt;/li&gt;
&lt;li&gt;Sonic 서버 내 local git 에 커밋&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;213-클라이언트팀에서-다운로드-zip-streaming&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#213-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8%ED%8C%80%EC%97%90%EC%84%9C-%EB%8B%A4%EC%9A%B4%EB%A1%9C%EB%93%9C-zip-streaming&quot; aria-label=&quot;213 클라이언트팀에서 다운로드 zip streaming permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;2.1.3. 클라이언트팀에서 다운로드 (Zip Streaming)&lt;/h3&gt;
&lt;p&gt;쿠키런: 킹덤은 Unity를 통해 개발되었습니다. Unity는 IDE 내에 커스텀 UI를 추가할 수 있는데, 클라이언트 개발자 한 분께서 이 기능을 활용해 번들 목록을 표시하고 특정 Revision으로 checkout 하는 UI를 만들어 주셨습니다.&lt;/p&gt;
&lt;p&gt;Unity UI는 REST API를 통해 Sonic 서버에서 전체 Revision 목록을 받아와 표시합니다. 이때 Sonic 서버는 내부적으로 &lt;code&gt;git history -n 20&lt;/code&gt; 커맨드를 사용해, 최근 20개 히스토리만 보내줍니다.&lt;/p&gt;
&lt;p&gt;현재 Unity 에서 가지고 있는 사운드 파일이 &lt;code&gt;Rev.98&lt;/code&gt;, 새로 Sonic을 통해 checkout 하고 싶은 번들을 &lt;code&gt;Rev.100&lt;/code&gt;라 가정해 보겠습니다. &lt;code&gt;Rev.98&lt;/code&gt;에서 &lt;code&gt;Rev.100&lt;/code&gt;으로 checkout하는 플로우는 아래 그림과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1626px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 60%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAABYlAAAWJQFJUiTwAAAB7ElEQVR42pVT224TMRDN//WBn4zKW/kFogoeGqq2uSAR9gHaxNn7zV5774djJwutWoQYaXYsey5nzszOQOm6Dkkco8hSpGmKv8mIf8tsHE9u3o89rm8eoFSFYRjQ9z3s2/T+KjnvjTFO67r+7Tdr25YXBmFe4SnR0HRQVYWiLIk2g5SSHfRo+xG57qC7Ef0wuqJ5nqOkn/W1Pi6hMbU7rFYPuJzPMbQduqZBGATYbDbwPA8yCJGlEkmuaRWMIgDeLRbXWK83uLu/R0TKLMaZUsolnF++x7uLCzRFScQNfCGw3m6x2+2QhSFGdqJlid5osE+X8OrqA5bLW3xcLCDE8YyQ/RtCb7MMbV4gixKIIEIZRkgOgohSx2dL5JL8NrTjmcPnaimwdmZJ1eRKRxGMVKgq8lhK1FT13UOj9TQFwAZxI3rGDLRTksmeEGoDySAZkadvW1Q8J+Injo9riMevqJTEyEI9ObUDzFk8J+qCam1Dvqepn4fCqZJHKZ4Q392gKtl2zoCM7cYHtxKMwlAULFbB9wME5C9gAXH0SYN6mdBxw9UISfyBg4jjiKg0TM0hmMahmsQWt757sccx8N3K6ImSabHtx3Iw8WAH8FzHP5t88uEOfrr9jOXqC8+nuFcJ/0ds0TiJkSTJm7/eL0FEm50bXsUNAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/07a686d5a371ea42555331f59b72f69f/33ecc/checkout_flow.webp 360w,
/static/07a686d5a371ea42555331f59b72f69f/263a4/checkout_flow.webp 480w,
/static/07a686d5a371ea42555331f59b72f69f/0c8fb/checkout_flow.webp 640w,
/static/07a686d5a371ea42555331f59b72f69f/521f7/checkout_flow.webp 860w,
/static/07a686d5a371ea42555331f59b72f69f/cbd37/checkout_flow.webp 1200w,
/static/07a686d5a371ea42555331f59b72f69f/3ff99/checkout_flow.webp 1626w&quot;
              sizes=&quot;(max-width: 1626px) 100vw, 1626px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/07a686d5a371ea42555331f59b72f69f/53918/checkout_flow.png 360w,
/static/07a686d5a371ea42555331f59b72f69f/9aebd/checkout_flow.png 480w,
/static/07a686d5a371ea42555331f59b72f69f/0f09e/checkout_flow.png 640w,
/static/07a686d5a371ea42555331f59b72f69f/3e3fe/checkout_flow.png 860w,
/static/07a686d5a371ea42555331f59b72f69f/64756/checkout_flow.png 1200w,
/static/07a686d5a371ea42555331f59b72f69f/5d9a4/checkout_flow.png 1626w&quot;
            sizes=&quot;(max-width: 1626px) 100vw, 1626px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/07a686d5a371ea42555331f59b72f69f/5d9a4/checkout_flow.png&quot;
            alt=&quot;Checkout flow&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Checkout flow&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Sonic 서버에 &lt;code&gt;Rev.100&lt;/code&gt; checkout 요청 (REST GET).&lt;/li&gt;
&lt;li&gt;Sonic 서버는 &lt;code&gt;git diff Rev.98 Rev.100&lt;/code&gt;를 통해 차이가 있는 파일 리스트업.&lt;/li&gt;
&lt;li&gt;S3에서 해당 Revision에 있는 파일들의 Download stream들을 열고
→ 이를 Archiver의 input stream으로 넣고
→ Archiver의 output stream을 클라이언트로 향하는 Response로 pipe.&lt;/li&gt;
&lt;li&gt;Unity는 Response로 &lt;code&gt;Rev.98&lt;/code&gt;과 &lt;code&gt;Rev.100&lt;/code&gt; 사이 변경된 파일들의 zip 파일을 받음.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;그다음 다운로드 받은 파일에 암호화 + 최적화 작업을 한 후, 소스코드에 commit 하게 됩니다.&lt;/p&gt;
&lt;h1 id=&quot;3-스펙-줄이기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%EC%8A%A4%ED%8E%99-%EC%A4%84%EC%9D%B4%EA%B8%B0&quot; aria-label=&quot;3 스펙 줄이기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;3. 스펙 줄이기&lt;/h1&gt;
&lt;h2 id=&quot;31-push된-번들-수정--삭제&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#31-push%EB%90%9C-%EB%B2%88%EB%93%A4-%EC%88%98%EC%A0%95--%EC%82%AD%EC%A0%9C&quot; aria-label=&quot;31 push된 번들 수정  삭제 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;3.1. Push된 번들 수정 / 삭제&lt;/h2&gt;
&lt;p&gt;Sonic은 단순한 구조와 안정성을 위해 (마치 블록체인처럼) 수정/삭제가 불가능하게 설계되었습니다.&lt;/p&gt;
&lt;p&gt;수정/삭제는 자주 있는 상황이 아닌데 반해, 이를 구현하려면 복잡성이 너무 커진다고 판단했기 때문입니다.&lt;/p&gt;
&lt;p&gt;내부적으로 Git 을 쓰기 때문에 수정/삭제는 force push를 사용해야 하는데, 아래와 같은 문제가 생깁니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;수정된 commit 이후의 commit hash 가 모두 변함
&lt;ul&gt;
&lt;li&gt;git tag 로 관리되는 revision 정보가 꼬임&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이미 발송된 slack 알림과 정보 불일치 발생 (진실의 근원이 한곳이 아니게 됨)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;대신 아래와 같은 우회법으로 대응하였습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;수정 : 해당 revision에 추가적인 suffix를 붙여 한번 더 push하도록 요청 (ex. &lt;code&gt;100-2&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;삭제 : 삭제 전용 revision을 만드는 기능 추가 (ex. &lt;code&gt;100-DEL&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;32-race-condition-merge-conflict&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#32-race-condition-merge-conflict&quot; aria-label=&quot;32 race condition merge conflict permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;3.2. Race Condition (Merge Conflict)&lt;/h2&gt;
&lt;p&gt;만약 사운드팀에서 두 명의 팀원이 동시에 push를 하면 어떻게 될까요?&lt;/p&gt;
&lt;p&gt;(push가 꼬여버리는 상황 방지를 위해 critical section에 lock은 걸어 두었습니다.)&lt;/p&gt;
&lt;p&gt;백엔드 시점으로 먼저 요청이 도착한 유저를 A, 늦게 도착한 유저를 B라 하겠습니다.&lt;/p&gt;
&lt;p&gt;Git이었다면 A는 요청이 성공하고, B는 요청이 실패해 다시 최신 pull을 받은 다음 push 해야 합니다.&lt;/p&gt;
&lt;p&gt;Sonic도 이렇게 구현할 수 있지만, 사용자가 이런 경우를 신경쓰지 않아도 되도록 backend에서 B를 자동으로 rebase한 뒤 commit 하게 하였습니다.&lt;/p&gt;
&lt;p&gt;이런 구조가 가능했던 이유는 Sonic에서 데이터는 단방향으로만 흐르기 때문입니다.&lt;/p&gt;
&lt;p&gt;Merge Conflict 는 push 한 팀이 다시 pull 을 받는 경우에 필요한데, Architecture Diagram을 다시 보면 사운드팀이 push한 파일을 사운드팀이 다시 pull 받지는 않습니다.&lt;/p&gt;
&lt;p&gt;실제로 Revision 이라는 단위는 사운드팀 내부적으로 버전 컨트롤이 끝난 단위이며, Sonic은 단순히 이를 전달해 주기만 할 뿐입니다. 때문에 Merge conflict는 Sonic에서는 불필요한 개념인 것입니다.&lt;/p&gt;
&lt;h1 id=&quot;마치며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0&quot; aria-label=&quot;마치며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;마치며&lt;/h1&gt;
&lt;p&gt;여러분들은 맥도날드에서 키오스크로 주문을 해 보신 적이 있으신가요? 주문 완료까지 10번에 가까운 터치를 해야 하는 키오스크를 보며 *“이 키오스크는 내가 할 수 있는 게 많아서 좋아!”*라고 생각하셨나요, *“햄버거 하나 주문하는게 왜 이리 복잡해!”*라고 생각하셨나요? 아마 후자가 더 많을 것이라 생각합니다.&lt;/p&gt;
&lt;p&gt;Sonic은 필요한 인터페이스만 사용자에게 노출했으며, 내부 구현도 바닥부터 로직을 구현한 것이 아니라 이미 있는 시스템을 잘 연결해 주기만 했습니다.&lt;/p&gt;
&lt;p&gt;단순하고 제한적인 스펙에도 불구하고 사용자분들께 사운드 리소스 동기화를 위한 공수가 거의 없어졌다는 피드백을 들을수 있었으며, 실제로 첫 배포 이후 8개월간 큰 이슈 없이 쿠키런: 킹덤에 들어가는 모든 사운드 리소스의 전달을 수행해 낼 수 있었습니다. 여러분들께서도 앞으로 서비스 기획이나 코드를 작성하실 때, 정말로 필요한 것이 무엇인지 고민해 보실 수 있으면 좋을 것 같습니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[조직문화 담당자가 개발조직에 뛰어든 이야기]]></title><description><![CDATA[테크 컬처 매니저라는 직무를 소개하고, 어떤 일을 하는지 공유합니다.]]></description><link>https://tech.devsisters.com/posts/what-does-a-culture-manager-do</link><guid isPermaLink="false">https://tech.devsisters.com/posts/what-does-a-culture-manager-do</guid><pubDate>Fri, 26 May 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요. 저는 데브시스터즈 진저랩 테크컬처셀에서 일하고 있는 전경아입니다. 평범한 인사담당자였던 저는 2021년부터 테크 컬처 매니저로 근무하고 있습니다. 이 글을 통해, 테크 컬처 매니저가 무엇인지와 어떤 일을 하는지를 소개해 드리려고 합니다.&lt;/p&gt;
&lt;h1 id=&quot;테크-컬처-매니저는-무엇인가요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%85%8C%ED%81%AC-%EC%BB%AC%EC%B2%98-%EB%A7%A4%EB%8B%88%EC%A0%80%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80%EC%9A%94&quot; aria-label=&quot;테크 컬처 매니저는 무엇인가요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;strong&gt;테크 컬처 매니저는 무엇인가요?&lt;/strong&gt;&lt;/h1&gt;
&lt;p&gt;이 글을 읽는 대부분의 독자는 소프트웨어 개발과 관련 있는 직업을 가진 분이라 생각하는데요, 혹시 조직문화 담당자가 개발조직 소속인 걸 보신 적 있으신가요? 요즘은 IT기업을 중심으로 DevRel(Developer Relations)이라는 직무가 점점 많아지고 있는데, 제가 입사할 당시만 해도 아직 많이 알려지지는 않은 직무였습니다. 저 역시도 이전에는 일반적으로 인사팀, 경영지원, 경영관리, HR 등의 이름이 붙여진 조직에서 인사 업무를 해왔었고, 직접 개발조직 소속으로 그리고 조직문화 담당자로 일하는 것은 이번이 처음입니다.&lt;/p&gt;
&lt;p&gt;정의하는 곳마다 다르겠지만, 저는 테크 컬처 매니저를 ‘개발 조직에 특화된 문화를 만들고 가꾸어 나가는 사람’이라고 생각합니다. 그러기 위해, 우리만의 뛰어난 기술 수준을 내외부에 적극적으로 알리는 것뿐만 아니라 지속적으로 구성원들이 업무에 몰입하고 성장할 수 있도록 구성원들과 함께 우리 조직에 가장 알맞은 조직문화를 기획하고 운영해 나가고 있습니다.&lt;/p&gt;
&lt;p&gt;직무에 ‘테크&apos;가 들어간다고 해서 기술적인 부분을 굉장히 잘 알아야 하는 것은 아닙니다. 예를 들어 기술적인 요청을 직접 처리하거나, 점검이나 장애 대응을 하지는 않습니다. 다만, 우리의 동료들이 어떤 일을 하는지를 알아야 현재 어떤 고충이 있고 어떻게 행동하면 좋을지 생각할 수 있기 때문에 꾸준히 관심을 두어야 합니다.&lt;/p&gt;
&lt;p&gt;제가 오늘 새로 들은 용어는 DLQ(Dead Letter Queue)인데요, 이것이 무슨 원리로 어떻게 동작하는지, 어떻게 해결해야 하는지는 몰라도 괜찮습니다. 단지 이런 이슈를 어떤 분들이 대응한 지 기억해 두었다가 ‘그거 잘 해결하셨나요?’하고 한마디 붙여볼 수단으로 사용할 수 있겠죠.&lt;/p&gt;
&lt;h1 id=&quot;테크-컬처-매니저는-어떤-일을-하나요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%85%8C%ED%81%AC-%EC%BB%AC%EC%B2%98-%EB%A7%A4%EB%8B%88%EC%A0%80%EB%8A%94-%EC%96%B4%EB%96%A4-%EC%9D%BC%EC%9D%84-%ED%95%98%EB%82%98%EC%9A%94&quot; aria-label=&quot;테크 컬처 매니저는 어떤 일을 하나요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;strong&gt;테크 컬처 매니저는 어떤 일을 하나요?&lt;/strong&gt;&lt;/h1&gt;
&lt;p&gt;먼저 데브시스터즈 진저랩을 소개해 드리자면, 데브시스터즈의 모든 게임과 제품을 효율적이고 안정적으로 제공하여 사용자들에게 최고의 즐거움을 전달할 수 있도록, 데이터/QA/인프라/웹/플랫폼 등 다양한 직군의 전문가들이 모여 서비스를 개발/운영하고, 탄탄한 플랫폼을 구축하는 조직입니다.&lt;/p&gt;
&lt;p&gt;테크 컬처 매니저는 이런 각 분야의 전문가들과 함께 지내며 문화와 관련된 문제들을 풀어나가고 있습니다. 구성원들의 심리적 안전감(psychological safety)을 조성하고, 서로 성장할 수 있도록 피드백 프로그램을 운영하거나, 행동강령을 만드는 일 등 건강한 문화가 정착될 수 있도록 일하고 있습니다. 또한 ‘테크’ 컬처 매니저다 보니, 회사의 개발문화도 함께 가꿔나가고 있습니다. 일반적으로 중앙조직에 있는 조직문화 담당자보다 구성원들을 더 가까이에서 만나며 일하고 있다고 생각합니다.&lt;/p&gt;
&lt;p&gt;저도 이 직무를 시작한 지 이제 1년이 조금 넘었기 때문에, 아직 어떤 일을 어떻게 해야 하는지 찾아가는 중입니다. 그래서 그동안 저에게 의미 있었던 몇 가지 일을 소개해 드리고자 합니다.&lt;/p&gt;
&lt;h2 id=&quot;심리적-안전감-설문지&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8B%AC%EB%A6%AC%EC%A0%81-%EC%95%88%EC%A0%84%EA%B0%90-%EC%84%A4%EB%AC%B8%EC%A7%80&quot; aria-label=&quot;심리적 안전감 설문지 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;strong&gt;심리적 안전감 설문지&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;첫 출근 날, 코로나로 인해 사무실이 휑했지만, 화상으로 다들 반겨주셨던 점이 기억납니다. 그리고 CTO인 그룹장님이 각 셀이 하는 일과 클라이언트와 서버가 어떻게 동작하는지 간단히 알려주셨는데 ‘지금 들으셔도 잘 기억 안 날 수 있는데 다음에 궁금하시면 또 물어보세요.&apos; 라고 말씀해 주신 것이 가장 기억에 남습니다. 지금 돌이켜보면 개발조직에서 어쩌면 기본적인 부분(클라이언트와 서버 동작 원리 등)인데도, 모르는 것이 생길 때마다 여쭤보면 친절하게 알려주셨습니다. 물론, 지금도 궁금한 것이 생기면 옆 동료나 그룹장님께 물어보고 있습니다.&lt;/p&gt;
&lt;p&gt;어떻게 이런 일이 가능할까요? 그건 바로 심리적 안전감이 조성된 조직이기 때문이라고 믿습니다. 심리적 안전감이란 질문을 하거나 의견을 제시하거나 부족한 점을 보일 때 무시당하거나, 불이익 또는 비난을 받지 않을 것이라는 믿음입니다. 이를 통해 서로 눈치 보지 않고 아이디어를 내거나, 실수를 통해 배운 점을 공유하며 효율적으로 일할 수 있습니다.&lt;/p&gt;
&lt;p&gt;우리 조직은 심리적 안전감이 잘 조성된 편이라고 느끼지만, 모두에게 계속해서 잘 작동하고 있는지 설문을 통해 주기적으로 살펴보고 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;분기별로 설문을 진행합니다.&lt;/li&gt;
&lt;li&gt;설문지 답변을 보고 특이사항이 있는 분들은 따로 1on1(1대1 면담) 일정을 잡습니다.&lt;/li&gt;
&lt;li&gt;요즘 어떤 것이 몰입을 방해하거나 어려움을 주는지 물어봅니다.&lt;/li&gt;
&lt;li&gt;후속 조치가 필요하다면 그룹장, HR 조직, 심리상담가 등 적절한 사람과 연결해 줍니다.&lt;/li&gt;
&lt;li&gt;이후 지속적으로 모니터링합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 설문과 후속 면담을 통해 퇴사위험을 미리 발견하고 방지한 사례도 있어서 이 글을 읽는 다른 분들도 진행해 보시면 조직 운영에 도움이 될 것이라고 생각합니다. 다만 구성원들이 설문지에 솔직하게 답변하거나 1대1 면담을 부담스러워하지 않도록 사전에 신뢰를 잘 구축해 두는 것 또한 중요하다고 생각합니다.&lt;/p&gt;
&lt;p&gt;또한, 신규입사자를 대상으로 심리적 안전감에 대한 이해를 높일 수 있는 교육 시간을 마련하여 조직에 새로 합류한 분들도 빠르게 적응할 수 있도록 돕고 있습니다.&lt;/p&gt;
&lt;p&gt;이외에도 저희 조직에서는 다양한 방법으로 독특하고 건강한 문화를 만들어 나가고 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;행동강령&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%96%89%EB%8F%99%EA%B0%95%EB%A0%B9&quot; aria-label=&quot;행동강령 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;strong&gt;행동강령&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;개인의 노력이 모여 조직의 문화를 만들기도 하지만, 조직에서 문화를 먼저 조성하는 방법도 있습니다. 바로 다른 회사나 컨퍼런스에서도 사용하는 행동강령입니다. 제가 입사할 때 가장 큰 미션으로 생각하던 일이기도 한데요, 행동강령이란 우리가 믿는 방향으로 말하고 행동하자는 약속입니다. 이를 통해 잘 몰라서 실수하거나 도움이 필요한 동료에게 적절히 개입할 수 있습니다.&lt;/p&gt;
&lt;p&gt;진저랩만의 문화를 만드는 것에 앞서, 데브시스터즈의 문화와 정렬을 맞추는 것도 중요하기 때문에 데브시스터즈의 조직문화를 살펴보았습니다. ‘정보의 공유’, ‘역할 기반 조직 문화’, ‘자율과 책임을 바탕으로 스스로 책임을 다하는 문화’, ‘용기 있는 소통을 통한 탁월한 문제 해결’, ‘신뢰와 열린 커뮤니케이션’이 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1600px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 29.72222222222222%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAyklEQVR42m2QSQqFQAxE+/7H8RouXbkSUVwoiPOs9XmBlkb+okg6SVUq7aqqUlmWyrJMeZ5rGAaN46h5nrWuq5Zl0TRNBnJqgDcz+74btm0znkuSRFEUKY5jId51nQ08z6O2bS2/71vneZoINYToIwynKIqX50iaplHf9zZIZBsE4nVdJnYch0VqRIDjuq5fIyxwnMhmImKAhj8DUpiHZ8PBhP8W3iYYgk1sRzhNU/tfXOKQHmCOJd6Ax19Bf7p3FDrzTonMfLkI/gCuNsxpI4fLNgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/6543b6b9d3eb2f285b89cc97c990c530/33ecc/01-team-culture.webp 360w,
/static/6543b6b9d3eb2f285b89cc97c990c530/263a4/01-team-culture.webp 480w,
/static/6543b6b9d3eb2f285b89cc97c990c530/0c8fb/01-team-culture.webp 640w,
/static/6543b6b9d3eb2f285b89cc97c990c530/521f7/01-team-culture.webp 860w,
/static/6543b6b9d3eb2f285b89cc97c990c530/cbd37/01-team-culture.webp 1200w,
/static/6543b6b9d3eb2f285b89cc97c990c530/64296/01-team-culture.webp 1600w&quot;
              sizes=&quot;(max-width: 1600px) 100vw, 1600px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/6543b6b9d3eb2f285b89cc97c990c530/53918/01-team-culture.png 360w,
/static/6543b6b9d3eb2f285b89cc97c990c530/9aebd/01-team-culture.png 480w,
/static/6543b6b9d3eb2f285b89cc97c990c530/0f09e/01-team-culture.png 640w,
/static/6543b6b9d3eb2f285b89cc97c990c530/3e3fe/01-team-culture.png 860w,
/static/6543b6b9d3eb2f285b89cc97c990c530/64756/01-team-culture.png 1200w,
/static/6543b6b9d3eb2f285b89cc97c990c530/42cbc/01-team-culture.png 1600w&quot;
            sizes=&quot;(max-width: 1600px) 100vw, 1600px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/6543b6b9d3eb2f285b89cc97c990c530/42cbc/01-team-culture.png&quot;
            alt=&quot;데브시스터즈의 조직 문화&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;데브시스터즈의 조직 문화&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;그다음, 진저랩 문화 가꾸기에 관심 있는 구성원들과 데브시스터즈 조직문화를 바탕으로 행동강령 TF(Task Force)를 만들었습니다. TF 구성원들과 함께 우리가 해야 할 행동과 하지 말아야 할 행동을 나열한 다음 치열한 토론 끝에 행동강령을 완성하였고, 구성원들의 쉬운 이해를 돕기 위해 각각 현실적인 예시도 추가하였습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2000px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 105.83333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAVCAYAAABG1c6oAAAACXBIWXMAABYlAAAWJQFJUiTwAAAC00lEQVR42o1ViXajQAzL/3/gpg1NuK/hTDgDpK5kCttu233lPWcgTDSyZDuHabrL6fQkl8tFXqyTnF8seX5+QhzF81yxbVuOxz9yPr+IhffW6SRNc5OfrsNwn+USFxKYUm5NK0PfS4/ouk6GYUCsz+v3ncY8z/L6+volFHCaBjHRUfzAAyMHYYvjXCSOA3FdW1zcX84WmJ4lDD3xfVe/L8tcbrdartdqj8djkcNj7KVKfCmrUoxJJcuMrmmaSlHkYNjtzMZx0Oj7Fmu/P29BlgfwlwZgcRLjdE+CwAe7CGwCva/wrmka1c3gkLIswOTxs4b8mKYJzDIFMsZIiojjWEIAEpjmEHgcyGRUwO9iB+RF4YMgkDRBALgoCk0/zzPoc5Uc6ZNdVZZSAzwzmTKmLNyT5/l7yve7tNhYVpWWTuhb+JGRuq7lBqC6rqRtGz30O2c3drvLqiFOjVQ36pdAS19sx9415Uq2BE+TBIxrlYBGfash64qaOShiakaDMhwQA1TTQjqu6yg49WRwXxSFkKHUdD9pSEAy8ALWnbMyQ/iep7pu5pAdzaO+McDIdFkWjU8pj9Do2tQSG08SMKXbZLwaUu96bj/6qOfXlO+j1Dg5QWoh2JCdrkiLqdD9FnXINvzNtdYhnCYItYkiX9ONo0jBE+jJVMmajPU5XTuq0FIq1SBmsQIuswzoAm4iqyr31b2ua3XdBsRWHtSLmi/LvOvH+GsKxldG0SGyth2YkQVdJasIZjhwm+3HA9hV/08ZgEZdDDVVLRuA0kXtZ9QkD6IkW+mQ4c+A86QpJ9BJwcCKmkU4gCXEWmMtUueZeyHBsjz+6ZyPgBg7ZRzC5QQ6JgrAIMtt8tAA6vQ7lyFuB4dSzECdg2C3FrSvwGTN6a1TmwZt6zC8z8l1ot+RwfoXgP70rGfJUHMFyqLUQcFpYvTevJcIO4Rls92vse5h+XBectq8AXtnQt9C1ywfAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/96563f77f7bb464b60fc856ff18af1bf/33ecc/02-team-code-of-conduct.webp 360w,
/static/96563f77f7bb464b60fc856ff18af1bf/263a4/02-team-code-of-conduct.webp 480w,
/static/96563f77f7bb464b60fc856ff18af1bf/0c8fb/02-team-code-of-conduct.webp 640w,
/static/96563f77f7bb464b60fc856ff18af1bf/521f7/02-team-code-of-conduct.webp 860w,
/static/96563f77f7bb464b60fc856ff18af1bf/cbd37/02-team-code-of-conduct.webp 1200w,
/static/96563f77f7bb464b60fc856ff18af1bf/fab06/02-team-code-of-conduct.webp 2000w&quot; sizes=&quot;(max-width: 2000px) 100vw, 2000px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/96563f77f7bb464b60fc856ff18af1bf/53918/02-team-code-of-conduct.png 360w,
/static/96563f77f7bb464b60fc856ff18af1bf/9aebd/02-team-code-of-conduct.png 480w,
/static/96563f77f7bb464b60fc856ff18af1bf/0f09e/02-team-code-of-conduct.png 640w,
/static/96563f77f7bb464b60fc856ff18af1bf/3e3fe/02-team-code-of-conduct.png 860w,
/static/96563f77f7bb464b60fc856ff18af1bf/64756/02-team-code-of-conduct.png 1200w,
/static/96563f77f7bb464b60fc856ff18af1bf/f97d7/02-team-code-of-conduct.png 2000w&quot; sizes=&quot;(max-width: 2000px) 100vw, 2000px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/96563f77f7bb464b60fc856ff18af1bf/f97d7/02-team-code-of-conduct.png&quot; alt=&quot;02 team code of conduct&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        진저랩 행동강령의 일부
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;초기 버전을 만드는데 10여 명이 모여 많은 시간을 썼는데요, 모두의 노력으로 어렵게 만든 만큼 한번 만들고 끝내는 것이 아니라 오늘의 우리에 맞게 계속해서 업데이트 해나가고 있습니다.&lt;/p&gt;
&lt;p&gt;행동강령의 존재는 기존 구성원들에게도 중요하지만, 조직에 새롭게 합류한 신규입사자분들께도 진저랩의 분위기와 일하는 방법 등을 익히는 데 도움이 되기 때문에 입사 후 가장 먼저 읽어보길 권하고 있습니다. 그럼으로써 더욱 빠르게 우리의 문화와 정렬을 맞추고 행동할 수 있게 됩니다.&lt;/p&gt;
&lt;h2 id=&quot;기술-블로그-프로모션-이벤트&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B8%B0%EC%88%A0-%EB%B8%94%EB%A1%9C%EA%B7%B8-%ED%94%84%EB%A1%9C%EB%AA%A8%EC%85%98-%EC%9D%B4%EB%B2%A4%ED%8A%B8&quot; aria-label=&quot;기술 블로그 프로모션 이벤트 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;strong&gt;기술 블로그 프로모션 이벤트&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;위 두 가지가 제가 소속되어 있는 진저랩의 문화를 위한 일감이라면, 이번엔 데브시스터즈 전체를 대상으로 진행한 개발 문화 관련 일감을 소개해 드리겠습니다.&lt;/p&gt;
&lt;p&gt;우리가 기술적으로 어떤 문제를 멋지게 해결해 왔는지 글을 통해 외부에 널리 알리고, 잠재적 지원자들에게 매력적인 인상을 남길 수 있는 기술 블로그를 활성화해 보고자 하였는데요, 이를 위해 기술 블로그 원고 투고 행사를 진행하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2000px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 39.72222222222222%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAABYlAAAWJQFJUiTwAAABOklEQVR42nWS227CMAyGef9H2yU3dNBTTk1b2gho01Kh8s82YhvTiOTYcpvP9p9sOmWhSoUkSZAeUrR1QB8C4jRhmmfxM/mHLRjGiOt1xru1WeIMow2yLMOOoJ7iNlNoqVCrDMrkAK0V8lyhyj8QmhQjnbnfV7I71nV9BXLSV15MKYXae5hCwVDXlXUPa44wVY0y/4QzJbyvEWiK7tih73rcbrcfIG+cLMsSzjmBWvJFUcAYQzmL3FAx15A0WuTouw7Lsnx3yP4FOAwD6XeAb1pk7ihAhu33exo1h9WaJqhEx//WE8hegNz+drslrTTSLJf4crngdDpJMV5/tXp7KbzFGJGmKay1aJpGLqgmncZxFGPoRLd9Pp+lEOc45jwb59gzR4CsB2sY+kBP4iofnod+//yM3wGnOOEL9BthO82uRfIAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/5b7fb388eb7041fe42916fc5f49d2f54/33ecc/03-techblog-event-msg.webp 360w,
/static/5b7fb388eb7041fe42916fc5f49d2f54/263a4/03-techblog-event-msg.webp 480w,
/static/5b7fb388eb7041fe42916fc5f49d2f54/0c8fb/03-techblog-event-msg.webp 640w,
/static/5b7fb388eb7041fe42916fc5f49d2f54/521f7/03-techblog-event-msg.webp 860w,
/static/5b7fb388eb7041fe42916fc5f49d2f54/cbd37/03-techblog-event-msg.webp 1200w,
/static/5b7fb388eb7041fe42916fc5f49d2f54/fab06/03-techblog-event-msg.webp 2000w&quot;
              sizes=&quot;(max-width: 2000px) 100vw, 2000px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/5b7fb388eb7041fe42916fc5f49d2f54/53918/03-techblog-event-msg.png 360w,
/static/5b7fb388eb7041fe42916fc5f49d2f54/9aebd/03-techblog-event-msg.png 480w,
/static/5b7fb388eb7041fe42916fc5f49d2f54/0f09e/03-techblog-event-msg.png 640w,
/static/5b7fb388eb7041fe42916fc5f49d2f54/3e3fe/03-techblog-event-msg.png 860w,
/static/5b7fb388eb7041fe42916fc5f49d2f54/64756/03-techblog-event-msg.png 1200w,
/static/5b7fb388eb7041fe42916fc5f49d2f54/f97d7/03-techblog-event-msg.png 2000w&quot;
            sizes=&quot;(max-width: 2000px) 100vw, 2000px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/5b7fb388eb7041fe42916fc5f49d2f54/f97d7/03-techblog-event-msg.png&quot;
            alt=&quot;기술 블로그 원고 투고 행사&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;기술 블로그 원고 투고 행사&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;라이브 서비스, 신작 개발 등 업무를 진행하느라 바빠서 글 쓸 시간이 부족한 구성원들의 부담을 줄여드리고자 많은 분의 의견을 듣고 고민하였습니다. 최종적으로는 여러 유관부서와 협업하여 행사를 워케이션 형태로 구성해 보았습니다. 잠시 일에서 떠나 그동안 해결했던 문제나 기존에 관심 있었던 분야를 잠자고 있던 글솜씨로 마음껏 풀어내고, 서로의 글을 검수하고, 글쓰기 노하우도 전수하며 진행되었습니다.&lt;/p&gt;
&lt;p&gt;당시 이벤트에 참여한 글들은 총 9편으로, 기술 블로그 뒤 페이지에서 확인하실 수 있습니다. 그 이후로도 데브시스터즈에 있었던 다양한 경험을 담은 글이 올라오고 있으니, 꾸준히 방문하셔서 둘러보시면 좋은 인사이트를 얻을 수 있으리라 생각합니다.&lt;/p&gt;
&lt;h2 id=&quot;월간-직군-모임&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9B%94%EA%B0%84-%EC%A7%81%EA%B5%B0-%EB%AA%A8%EC%9E%84&quot; aria-label=&quot;월간 직군 모임 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;strong&gt;월간 직군 모임&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;데브시스터즈에는 월간 서버, 월간 데이터, 웹 직군의 밤 등 월 단위로 진행하는 직군 모임이 존재합니다. 이름에서도 유추할 수 있듯이 각각 서버, 데이터, 웹 관련 업무를 하며 마주했던 문제를 해결하는 과정이나 컨퍼런스 방문기 등을 세미나 형식으로 공유하고 있습니다.&lt;/p&gt;
&lt;p&gt;직군별로 지식 공유라는 필요를 느끼고 자율적으로 구성된 직군 모임이지만, 참석하시는 분들께 더욱 의미 있는 시간을 만들고 우리의 개발 문화 활성화를 위해 테크 컬처 매니저도 함께 고민하고 논의하며 운영해 나가고 있습니다. 직군 모임 진행을 도와드리기도 하고, 참석을 못 하신 분들을 위해 녹화를 하거나, 피드백을 수집하여 더 나은 모임으로 발전시키는 일 등을 하고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1082px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 95%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAATCAYAAACQjC21AAAACXBIWXMAABYlAAAWJQFJUiTwAAACA0lEQVR42p1Ua2+bQBD0//9XUSLkVm0+NV+aGmPeHNgB83QwTG8WmxCS1KgnnZdbfOOZ2V2v8tDGD+MBhmHg6ekX7u/v8O37Gg86Pj//xuPjT6zXBpIkxpK1qooMsVLwHQtFWWK/T3A47PHyckBVVcjzHMfjEW37ugzweOqRNj26rpdE3dQoigJN0+B/1oofbdsKC7L5avV9vxyQbNI0FZmllk2GRZGjrmuJpT53XTdugn+2+U4Aecn3Pex2FoIgQBgGcF0Hcawkz9yU6U3AsiwQhaGAuI4jwKE+M+e5rvyA49jYmqZYw/UZ03cMlWaTxPFQcd8XMN/zBIzeUj7toN8LJA8MeZlgKoqgVCRy2X+n02mUPPdxen6TrAEJ5nkubHuHLEsREFiztW1b+nHJmnhYijxbe0evgsCHtd0K01AXhMBseObpKYtEi6giSYY8CbFbxqJIMgrlS0qpUS5jpPOx9peRFxkJtE84VQc5884IKB5G4Vjp6yVGSidr9umtRR8nDAdpwlQ/E5zPQx8OsarKd1MzLcysD8vBrzAYWXFfC0XptCLLMikQW4hx3otvbaP7THzSDAlwlU0gsnV1ISxri83mD0y9+d40Nzifz19PCqvs+a74RQD3MiGs7vXC/I/i3419qRxZxdISSs51XX1o4EWz7F+k0nzOM6Xz+RbIHPAvQ5i2uz8LqFIAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/3ddc814ef94f064eceb051c0fed48620/33ecc/04-monthly-seminar-list.webp 360w,
/static/3ddc814ef94f064eceb051c0fed48620/263a4/04-monthly-seminar-list.webp 480w,
/static/3ddc814ef94f064eceb051c0fed48620/0c8fb/04-monthly-seminar-list.webp 640w,
/static/3ddc814ef94f064eceb051c0fed48620/521f7/04-monthly-seminar-list.webp 860w,
/static/3ddc814ef94f064eceb051c0fed48620/89e1d/04-monthly-seminar-list.webp 1082w&quot;
              sizes=&quot;(max-width: 1082px) 100vw, 1082px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/3ddc814ef94f064eceb051c0fed48620/53918/04-monthly-seminar-list.png 360w,
/static/3ddc814ef94f064eceb051c0fed48620/9aebd/04-monthly-seminar-list.png 480w,
/static/3ddc814ef94f064eceb051c0fed48620/0f09e/04-monthly-seminar-list.png 640w,
/static/3ddc814ef94f064eceb051c0fed48620/3e3fe/04-monthly-seminar-list.png 860w,
/static/3ddc814ef94f064eceb051c0fed48620/e4ddf/04-monthly-seminar-list.png 1082w&quot;
            sizes=&quot;(max-width: 1082px) 100vw, 1082px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/3ddc814ef94f064eceb051c0fed48620/e4ddf/04-monthly-seminar-list.png&quot;
            alt=&quot;월간 서버 세미나 리스트&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;월간 서버 세미나 리스트&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;또한, 간혹 연사가 없을 때 주변 동료들을 열심히 찌르며 발표를 독려하기도 하고, 발표하신 동료들에게 드릴 선물도 준비하고 나눠드리며 우리의 지식이 더 잘 공유될 수 있도록 계속해서 개선해 나가고 있습니다. 최근에는 다른 팀과의 교류 시간이 좀 더 있으면 좋겠다는 피드백을 받아서 모든 팀이 함께 어우러져 친목을 다질 수 있는 시간도 준비 중입니다.&lt;/p&gt;
&lt;h2 id=&quot;scala-개발자-meet-up&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#scala-%EA%B0%9C%EB%B0%9C%EC%9E%90-meet-up&quot; aria-label=&quot;scala 개발자 meet up permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;strong&gt;Scala 개발자 MEET-UP&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;데브시스터즈 전체뿐만 아니라 개발팀과 함께 개발 커뮤니티를 위한 업무도 진행했습니다. 쿠키런: 킹덤의 서버는 Scala(이하 스칼라)로 개발하고 있는데요, 그중 한 개발자분께서 제안해 주셔서 스칼라 개발자 커뮤니티를 활성화하고 쿠키런: 킹덤의 멋진 기술력을 홍보하고자 스칼라 개발자 밋업을 개최하였습니다.&lt;/p&gt;
&lt;p&gt;쿠키런: 킹덤 서버 개발자 총 3분을 연사로 모셔서 아래와 같은 주제로 발표를 진행했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;쿠키런 킹덤이 Spark를 쓰는 조금 특별한 방법 - Event Sourcing Architecture에서 데이터 추출하기&lt;/li&gt;
&lt;li&gt;Migration to ZIO 2&lt;/li&gt;
&lt;li&gt;순수히 아름다운 쿠키런 킹덤 도메인 로직&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;스칼라 커뮤니티에 행사를 홍보하기 위해 진저랩 테크니컬라이팅셀의 도움으로 행사 노션 페이지를 만들었으며(&lt;a href=&quot;http://coo.kie.run/scala-meetup&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;coo.kie.run/scala-meetup&lt;/a&gt;; 깨알 정보-진저랩 인프라셀에서 만든 url 단축 내부 서비스 ‘쇼트닝’을 이용하였습니다.) 행사에 방문하시는 분들을 위해 대외 커뮤니케이션을 담당하는 임팩트그룹의 도움을 받아서 많은 분이 귀여워해 주셨던 스티커도 만들었습니다.&lt;/p&gt;
&lt;p&gt;또한, 많은 인원이 모이는 행사인 만큼 혹시 모를 실수나 사고를 방지하기 위해 행동강령을 만들어 게시하였습니다. 다행히 참석하신 분들 모두 행동강령을 공감해 주시고 잘 따라주셔서 특별히 문제가 발생하지 않았습니다. 마지막으로는 네트워킹 시간을 마련하여 저녁 식사와 함께 참석자들이 각자 다채로운 스칼라 사용 경험을 공유하며 자유롭게 이야기를 나누는 시간으로 꾸렸습니다.&lt;/p&gt;
&lt;p&gt;데브시스터즈에서 일하면서 가장 많은 부서의 동료들과 협업하며 준비한 행사인 동시에 처음으로 외부에 공개하는 행사다 보니 준비 과정에서 걱정이 많았는데요, 많은 분과 힘을 합쳐 무사히 마무리할 수 있어 개인적으로도 굉장히 의미 있던 시간이었습니다. 또한, 데브시스터즈에는 회고 문화가 일상화되어 있어서 행사 다음 날 진행자들이 모여 회고를 하였고, 잘한 점과 개선해야 할 점을 도출하여 문서로 정리하여 내부에 공유하였습니다. 앞으로도 스칼라를 비롯하여 다양한 테크 밋업을 개최할 예정이니, 관심 있으신 분들은 다음에 방문하셔서 다양한 경험을 나눠주시면 좋겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1210px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 68.88888888888889%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAABYlAAAWJQFJUiTwAAAD7UlEQVR42g2Ta0ybBRSGv6jRyEQSNmUwnWMu41IY0FK6rozLgFKglF5o6b2FQlsKhQ4YUhgXYaMMaMWxSwYs04TExd0yZ2ZmnBFxlyzMeUnmnJclMyb+WOJff2gevx8nOSfnJOc5b84r+L122pwWAq1OOn1e/F437R4XoVCQYMCP1+0g6HPja3Xhdjnwt7cRDvnpCnbQGwnj93fQ0e7F1GymsqoWISQOu8x6zHodzQY9Rl0deq0Gj9OBtz2I0eZEq9NRX1eLyebA5gviavNh97ahNZio0tTTZLagbtSxV7kPYe7IJLHDA0yPjzISHROpwjgdNqwWPS3GRqwtJgwOFya7g5Y2P3Z/F47OHsxi3qA30Gix0mCxkVckQ5q/B2Gm28yRoIH4WDcnZwaJRQMMdtoIuIx4zFos+loq9qto7wkTGp3ANzqNb2qeQGyR4PwZgsdOozFZSU/PoDAnF+FEr4Yzg41cPjvB4tInTJ/+gviH1zn58RXeO3+JkfEoleX76R0e490vHzB2+w+m7jzlyINnzD6B4U+/RdPUzNaMHchyshHiES1TgSpuXowz9MEzus7D4c/+49Tjf7n4N5w4t4yrVk7/5Ayj3zxlfP0J0+u/M3n/L44++oehK3fQGs28sT0ThSQLIRrpoFKpwKLTYPH0440kmLz0M3P3/6TtnUkmjk2TlZ1Pz/A4ncvXGF/7FWdMPPX9FU5s3GZw9QYyqZQt6dsoLxAJo4EWqlUKCvJyRVElSCV5GI0G+gZ6UZSo6A61klOoIBwOsk+hZGB4CKWqhiKZCnV9Aza3G1VRPgWSXKqK8xFGBsIUy+VkZe3k+RdTSNuSgaZUSVaOOCRVUl5RwZ5CGerKMmoOVKCQy8Ra7GeX8HpqBtvefJtof4SDHR4WJg4hSArLUZXVIJdLee6FFPZk7uRUv53YRAS3x85bmbvZtn0Xm15NJSV1q7g0mULxReoa6qmurhJh9rI4O85SYozVpQTCbkkJOr0bZale3FxLaWkTanUj6up6EvGjnFs5ztnlRcrLyxCEl8RLJPT2dnEsdoj5mX46fDYi4VYS031MjfYhDPb1iS6wkpNfjbSkgey8A6RvF0VOyyM+n+DurZtcu3qB7q4gSUnJOOxmnvz2A/fvrXFr/Qb37t7ku42vuPX1dW58fhVhYXaEgqIyNqfls/m1bF5J2cGm5DReTtqMVtsketkt0qqRFReLdFmUKpUk5mKcWozz8Ps7/PLTBo8fbfBwY40fr11AuPzRPD09PRhMHhq0ZopLDlCjrhZ1raDF3EwoGMDjdqFvasIp2s8lhtXcQt/BVqYmuzi+MMzK8lEWZqKsLkzxP6SRXShckGrRAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/04c642b999498b63b03336b6bcc78af6/33ecc/05-scalar-meet-up.webp 360w,
/static/04c642b999498b63b03336b6bcc78af6/263a4/05-scalar-meet-up.webp 480w,
/static/04c642b999498b63b03336b6bcc78af6/0c8fb/05-scalar-meet-up.webp 640w,
/static/04c642b999498b63b03336b6bcc78af6/521f7/05-scalar-meet-up.webp 860w,
/static/04c642b999498b63b03336b6bcc78af6/cbd37/05-scalar-meet-up.webp 1200w,
/static/04c642b999498b63b03336b6bcc78af6/437c9/05-scalar-meet-up.webp 1210w&quot;
              sizes=&quot;(max-width: 1210px) 100vw, 1210px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/04c642b999498b63b03336b6bcc78af6/53918/05-scalar-meet-up.png 360w,
/static/04c642b999498b63b03336b6bcc78af6/9aebd/05-scalar-meet-up.png 480w,
/static/04c642b999498b63b03336b6bcc78af6/0f09e/05-scalar-meet-up.png 640w,
/static/04c642b999498b63b03336b6bcc78af6/3e3fe/05-scalar-meet-up.png 860w,
/static/04c642b999498b63b03336b6bcc78af6/64756/05-scalar-meet-up.png 1200w,
/static/04c642b999498b63b03336b6bcc78af6/75b60/05-scalar-meet-up.png 1210w&quot;
            sizes=&quot;(max-width: 1210px) 100vw, 1210px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/04c642b999498b63b03336b6bcc78af6/75b60/05-scalar-meet-up.png&quot;
            alt=&quot;스칼라 밋업 현장&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;스칼라 밋업 현장&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h1 id=&quot;마치며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0&quot; aria-label=&quot;마치며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;마치며&lt;/h1&gt;
&lt;p&gt;테크 컬처 매니저가 진행하고 있는 몇 가지 일을 소개해 드렸습니다. 이외에 우리의 좋은 문화나 일하는 방법을 글로 남겨서, 기존 구성원이 퇴사하거나 새로운 분들이 입사해도 계속해서 진저랩의 문화와 구성원의 정렬을 맞출 수 있도록 하고 있습니다. 또한, 구성원들의 업무 몰입을 높이고, 우리만의 독특한 문화를 발전시키기 위해 동료들과 함께 고민하고 연구하며 하나씩 실행해 보고 있습니다. 마지막으로, 기술 지식 공유 활동도 강화하여 우리 모두 함께 성장할 수 있는 환경을 조성하고자 노력하고 있습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1210px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 30.555555555555554%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAABYlAAAWJQFJUiTwAAABJklEQVR42jWRiW6DMBBE+f/faytVldoklBAbn5wmYDMdmxTJQqx3Zt8s1TzPaJoG9e0GYwyO4ygnpgNX1ePj2uLtckc/B6QUkZ+wr3ivPyGdZC2V2r+u8s7i0d4hpYDWCssyl4tEw+Ym0IgOdStgrEOKp+E2jTDNBdpb9N5jGHps23YaOqOKYdcpSPGA9w6RwrhHtN+/cKS2RpdhWZQtdxqsvzW0tQVEqY4gS6GtDjY4Ut4ZW9BwmqZCEWPCqH6gOsG6YH08o1G0rCskBykpCSEQQnhpIqppHEknSWHKDq01WCnIl45k+dvwnYfmaDsH7euIyXDnJFRKlVQ5ek5QzdPL0J6GuSGEpUzsh4HNnnf2dQx/FpCeAxb9xX4Hw1Xkutaahk/8AZJmyvOTzSsqAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/57df027e52bf7c50da7fca2dc5853429/33ecc/06-tech-manager-todo-list.webp 360w,
/static/57df027e52bf7c50da7fca2dc5853429/263a4/06-tech-manager-todo-list.webp 480w,
/static/57df027e52bf7c50da7fca2dc5853429/0c8fb/06-tech-manager-todo-list.webp 640w,
/static/57df027e52bf7c50da7fca2dc5853429/521f7/06-tech-manager-todo-list.webp 860w,
/static/57df027e52bf7c50da7fca2dc5853429/cbd37/06-tech-manager-todo-list.webp 1200w,
/static/57df027e52bf7c50da7fca2dc5853429/437c9/06-tech-manager-todo-list.webp 1210w&quot; sizes=&quot;(max-width: 1210px) 100vw, 1210px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/57df027e52bf7c50da7fca2dc5853429/53918/06-tech-manager-todo-list.png 360w,
/static/57df027e52bf7c50da7fca2dc5853429/9aebd/06-tech-manager-todo-list.png 480w,
/static/57df027e52bf7c50da7fca2dc5853429/0f09e/06-tech-manager-todo-list.png 640w,
/static/57df027e52bf7c50da7fca2dc5853429/3e3fe/06-tech-manager-todo-list.png 860w,
/static/57df027e52bf7c50da7fca2dc5853429/64756/06-tech-manager-todo-list.png 1200w,
/static/57df027e52bf7c50da7fca2dc5853429/75b60/06-tech-manager-todo-list.png 1210w&quot; sizes=&quot;(max-width: 1210px) 100vw, 1210px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/57df027e52bf7c50da7fca2dc5853429/75b60/06-tech-manager-todo-list.png&quot; alt=&quot;06 tech manager todo list&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
				테크컬처셀 일감 중 일부
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;글을 다 읽으신 지금, ‘개발조직에서 일하는 조직문화 담당자’가 있기 때문에 함께 일하는 동료들이 그들의 문제 해결에 집중할 수 있다고 말씀드릴 수 있습니다. 여러분의 조직에도 테크 컬처 매니저를 모시는 것은 어떠신가요? 혹은 지금, 이 글을 읽고 계신 여러분도 테크 컬처 매니저에 있는 조직에서 한번 일해보고 싶지 않으신가요? 데브시스터즈와 진저랩의 문은 언제나 열려있습니다. 직무나 조직문화 관련하여 궁금한 점이 있거나 이야기 나누고 싶은 부분이 있으면 &lt;a href=&quot;mailto:tech_blog@devsisters.com&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;tech_blog@devsisters.com&lt;/a&gt;로 언제든 연락해 주시길 바라며, 다음에 또 흥미로운 조직문화를 소개해 드리도록 하겠습니다. 감사합니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[쿠키런: 킹덤 길드 업데이트 이후 서비스 이슈 되돌아보기]]></title><description><![CDATA[쿠키런: 킹덤에 길드 기능이 추가된 이후 발생했던 서비스 이슈에 대해 되돌아봅니다.]]></description><link>https://tech.devsisters.com/posts/crk-hot-range-postmortem</link><guid isPermaLink="false">https://tech.devsisters.com/posts/crk-hot-range-postmortem</guid><pubDate>Wed, 19 Apr 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;이 글과 관련된 다른 글 보기&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/posts/cookierun-kingdom-launch-revisited/&quot;&gt;쿠키런: 킹덤 런칭 회고&lt;/a&gt; - DevOps 관점에서 본 쿠키런: 킹덤 런칭 회고&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/cockroachdb-in-production/&quot;&gt;CockroachDB in Production&lt;/a&gt; - CockroachDB에 대한 소개와 킹덤에서 CockroachDB를 사용한 이유 설명&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/crk-launch-storage-postmortem/&quot;&gt;쿠키런: 킹덤 데이터베이스 스토리지 레이어 복원기&lt;/a&gt; - 엔지니어 관점 기록한 개괄적인 장애 대응 과정과 회고&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/bit-level-database-hacking&quot;&gt;CTO가 커리어를 걸고 비트 레벨까지 내려가서 DB를 해킹했던 이야기&lt;/a&gt; - CockroachDB의 소스코드를 참고하며 바이너리 파일로부터 데이터를 복구한 이야기&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/36h-downtime-on-first-day/&quot;&gt;입사 첫날에 36시간 점검 경험하기&lt;/a&gt; - 신규입사자의 관점에서 서술된 장애 대응 기록&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/crk-aws-az-failure-postmortem/&quot;&gt;쿠키런: 킹덤 AWS AZ 장애 아웃라인&lt;/a&gt; - AWS AZ 장애로부터 서비스를 복구한 이야기&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;안녕하세요, 데브시스터즈 진저랩 인프라셀에서 데브옵스 엔지니어로 일하고 있는 이상유입니다.&lt;/p&gt;
&lt;p&gt;지난 NDC22의 &lt;a href=&quot;https://www.youtube.com/watch?v=AZbCZ2KOcwU&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;쿠키런 킹덤: 총 56시간의 긴급 점검 회고&lt;/a&gt;를 보셨던 분이라면, 그 당시 시간 관계로 미처 이야기하지 못했던 주제가 하나 있었다는 것을 기억하실 것이라고 생각합니다.&lt;/p&gt;
&lt;p&gt;이 글에서는 바로 그 주제, 21년 3월 길드 업데이트 이후 발생했던 서비스 이슈에 관해 다루고자 합니다.&lt;/p&gt;
&lt;h2 id=&quot;hot-range혹은-hot-spot&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#hot-range%ED%98%B9%EC%9D%80-hot-spot&quot; aria-label=&quot;hot range혹은 hot spot permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Hot Range(혹은 Hot Spot)&lt;/h2&gt;
&lt;p&gt;이전에 게시된 글을 읽어보신 분들이라면,&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;쿠키런: 킹덤은 CockroachDB라는 데이터베이스를 사용하고 있다&lt;/li&gt;
&lt;li&gt;CockroachDB는 SQL 인터페이스를 제공하는 분산 데이터베이스이다&lt;/li&gt;
&lt;li&gt;분산 처리와 고가용성을 위해, CockroachDB는 데이터를 쪼개 ‘Range’라는 단위로 저장한다&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;는 부분을 이미 알고 계실 것입니다.&lt;/p&gt;
&lt;p&gt;혹 이전 게시글을 읽어보지 않으셨더라도, 위 3개 내용을 알고 계신다면 이 글을 무리없이 이해하실 수 있도록 구성했습니다. CockroachDB에 대해 더 깊게 알아보고 싶으시거나, 데브시스터즈에서 어째서 CockroachDB를 사용하도록 결정했는지 궁금하신 분은 &lt;a href=&quot;/posts/cockroachdb-in-production/&quot;&gt;CockroachDB in Production&lt;/a&gt;을 읽어보시기를 추천합니다.&lt;/p&gt;
&lt;p&gt;CockroachDB는 Range별 용량과 초당 쿼리수를 종합해 자율적으로 Range를 병합하거나 분리합니다. 초기 설정에서 Range 용량은 128MB에서 512MB 사이를 유지하도록&lt;sup&gt;&lt;a name=&quot;ftsrc_1&quot; href=&quot;#ft_1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt;, 그리고 Range에 초당 2500쿼리 미만의 요청이 가해지도록&lt;sup&gt;&lt;a name=&quot;ftsrc_2&quot; href=&quot;#ft_2&quot;&gt;[2]&lt;/a&gt;&lt;/sup&gt; 설정되어 있습니다. 때문에 CockroachDB 클러스터를 관리하다 보면, 쿼리가 자주 일어나는 Range는 알아서 잘 분산되어 여러 노드에서 작업을 처리하고, 쿼리가 자주 일어나지 않는 Range는 합쳐지는 동작을 관측할 수 있습니다.&lt;/p&gt;
&lt;p&gt;이러한 동작 덕분에, 부하가 장기간에 걸쳐 점진적으로 늘어나는 경우에는 관리자가 CockroachDB 클러스터에 대해서 해주어야 할 동작은 많지 않습니다. CPU나 메모리, 스토리지와 같은 리소스가 부족한 경우, 이에 맞춰 새 노드를 추가해주면 CockroachDB 클러스터에서 우아하게 Range를 분산하여, 모든 노드가 비슷한 양의 데이터와 부하를 처리하도록 조절합니다.&lt;/p&gt;
&lt;p&gt;하지만 이런 아름다운 그림이 모든 상황에서 그려지는 것은 아닙니다. CockroachDB에서는 Primary Key를 go의 빌트인 인코딩/디코딩 메소드를 사용하여 정렬한 뒤 일정 간격으로 나누어 Range를 만들어냅니다&lt;sup&gt;&lt;a name=&quot;ftsrc_3&quot; href=&quot;#ft_3&quot;&gt;[3]&lt;/a&gt;&lt;/sup&gt;. 이러한 알고리즘에 따라 유사한 키 값은 동일하거나 인접한 Range에서의 작업이 될 가능성이 높으며, 이렇게 대부분의 워크로드가 몰린 Range를 &lt;code&gt;Hot Range&lt;/code&gt; 또는 &lt;code&gt;Hot Spot&lt;/code&gt;이라고 합니다.&lt;/p&gt;
&lt;p&gt;Hot Range는 결과적으로 해당 Range를 관할하는 노드들에서만 전체 부하를 처리하는 결과를 초래해, 클러스터가 제 성능을 낼 수 없도록 만들게 됩니다.&lt;/p&gt;
&lt;p&gt;이러한 배경 지식을 가지고, 장애가 발생한 날로 떠나 보겠습니다.&lt;/p&gt;
&lt;h2 id=&quot;0500-길드-업데이트&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#0500-%EA%B8%B8%EB%93%9C-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8&quot; aria-label=&quot;0500 길드 업데이트 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;[05:00] 길드 업데이트&lt;/h2&gt;
&lt;p&gt;길드 기능 업데이트 점검에서, 쿠키런: 킹덤의 데이터베이스 클러스터에는 2가지 작업이 진행되었습니다.&lt;/p&gt;
&lt;p&gt;첫째로 길드 기능에서 사용하는 테이블을 추가하고, 사전에 부하를 분산하도록 설정하였습니다. 길드 기능에서 사용하는 테이블들은 아직 CockroachDB가 부하를 분산할 수 있는 정보를 가지고 있지 않기 때문에, 서비스 오픈 직후 길드 기능에서 사용하는 테이블에서 Hot Range가 발생할 가능성이 높습니다. 이를 미연에 방지하기 위해, 길드 관련 테이블들의 Range를 강제로 분할하고, 이를 클러스터 전체에 나누어주도록 작업하였습니다.&lt;/p&gt;
&lt;p&gt;둘째로 당시 점검 며칠 전 데이터베이스 노드 한 대가 물리 장비 이슈로 제거된 상태였는데, 새 노드를 추가하여 데이터베이스 규모를 원래 의도한 규모로 복원하였습니다. 길드 업데이트를 위해 점검이 필요했고, 이를 위해 데이터베이스 백업 등 안전한 작업에 필요한 제반 준비가 되어야 하는 상황이었기에, 이 기회를 틈타 함께 작업을 진행하였습니다.&lt;/p&gt;
&lt;p&gt;점검 중 인프라 외적인 사유로 점검이 2시간가량 연장되었으나, 2021년 3월 6일 11시, 모든 문제를 해결하고 점검이 마무리되었습니다. 이후 모니터링 중, 데이터베이스의 CPU 사용량이 예상보다 조금 높다는 것을 확인했으나 당시 접속중인 유저가 평시보다 많은 점, 그리고 새롭게 상호작용하는 기능이 추가되었다는 것을 고려했을 때 우려하지 않아도 될 것으로 판단했습니다.&lt;/p&gt;
&lt;h2 id=&quot;1130-불길한-신호는-항상-현실이-되고&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1130-%EB%B6%88%EA%B8%B8%ED%95%9C-%EC%8B%A0%ED%98%B8%EB%8A%94-%ED%95%AD%EC%83%81-%ED%98%84%EC%8B%A4%EC%9D%B4-%EB%90%98%EA%B3%A0&quot; aria-label=&quot;1130 불길한 신호는 항상 현실이 되고 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;[11:30] 불길한 신호는 항상 현실이 되고&lt;/h2&gt;
&lt;p&gt;그렇지만 우려하지 않아도 된다는 것이 특별한 조치를 취하지 않아도 된다는 것이지, 마음을 놓을 수 있다는 의미는 아니죠. 많은 유저분들이 새 업데이트를 즐겨주시고 있다는 것에 기뻐하며 서버 상태를 지켜보던 중, 굉장히 슬픈 소식이 들려오고 맙니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 434px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 77.22222222222223%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAAAsTAAALEwEAmpwYAAACj0lEQVR42nWU2VMaQRCH+f8fk4c8CHIo4F4zewkmXhGJgCCJooaosRJLU+aBeICcu/tLzywrGCtDffQxPV3dc2zs+uYG7XYb/X4fYgRBIPF9X9q3t79xefkDZ+cXuL75hekICH+OIFoX6z09YTwe439DJJ54HkYUMxqNXicMfzKhGLHBYIDJxHuubL5CQRQYVS/9Qej3J6SPZ8gKo9b+HVHisJi5pAKf/kWCMUlPQD4varn3RO1M0J8EuBtM8DCk9jwRFwaQCpqiLgKMRz6GND8k6VEdnid8ZNO6Xm8UJhT7d/H9Cl8Oz9D6+hOt9hVap1c4PLpE6/AcR40W6uUaqqUayjsN7Gw1sPuxiUrlGPXaCT43v6Gxf4Kj5glV7iMm2jhuNuEoDK7qgC/rYBkVejILNZ4BSy6DZ7Kw8ipsxYCrGbA1Haaig6saLLL5Uh4FWu/T4cXEgawX1/DuzVskkykkMykkUoSQS2SniUQC6fgCUvE4EiQXE3Gpp6ckFuLQNY22wAsrLFUqSCsKVNOCwjlyhoEsVZEjsrqBHOPI05xq29AcR2KsEgUXRtEl20Rh4/0sYe3gAHlmwHAd6I4N3Z7iOBK26ko0mtNsi6QVSkLEK5YJd2M9TDiky1qu7GHFUMBsDsNihAFm0h5xDYyrYEyVkksUmHwFJhPkYRFMz2KtYM0qrFd3YSgpODwHh2XhGMuwBSw3g+de2s/kYepL+FDks4T71RJWsnFwmmBaZo55Oy3hekYS2Yx0XVlE0Z0eikc39LRVx+6mhUqpgL0dl1hFedtBadOWeoiLClHesijWJN2RVIm9bRP1T2v0TRiF1yZ6s7PnBvn16XQ6r/zdbhedP3cvnqfg8bGL+/sH/AUuUBPV/DPMRAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/3b6b8cb10207243aceabcfe7782820e1/33ecc/01-crdb-cpu-usage.webp 360w,
/static/3b6b8cb10207243aceabcfe7782820e1/f75d5/01-crdb-cpu-usage.webp 434w&quot; sizes=&quot;(max-width: 434px) 100vw, 434px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/3b6b8cb10207243aceabcfe7782820e1/53918/01-crdb-cpu-usage.png 360w,
/static/3b6b8cb10207243aceabcfe7782820e1/66227/01-crdb-cpu-usage.png 434w&quot; sizes=&quot;(max-width: 434px) 100vw, 434px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/3b6b8cb10207243aceabcfe7782820e1/66227/01-crdb-cpu-usage.png&quot; alt=&quot;01 crdb cpu usage&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        24코어 머신에서 24코어를 몽땅 다 사용중인 안타까운 모습
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;그것은 바로 한 노드의 CPU 사용량이 천장을 찍었다는 소식이었습니다. 이와 동시에 서버 지표에서도 유의미한 양의 에러가 관측되기 시작했습니다. 모두가 탄식하며 일시적인 현상이기를 기도하던 그때, 야속하게도 다른 두 노드에서도 CPU 사용량 이상이 확인되었습니다.&lt;/p&gt;
&lt;p&gt;모종의 이유로 로드가 고르게 분배되지 않고 있으며, 이 때문에 특정 노드가 과도한 로드로 데이터베이스 클러스터 안정성에 악영향을 주고 있다는 사실을 받아들여야 하는 순간이었습니다.&lt;/p&gt;
&lt;h2 id=&quot;저희가-무엇을-잘못했나요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%80%ED%9D%AC%EA%B0%80-%EB%AC%B4%EC%97%87%EC%9D%84-%EC%9E%98%EB%AA%BB%ED%96%88%EB%82%98%EC%9A%94&quot; aria-label=&quot;저희가 무엇을 잘못했나요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;저희가 무엇을 잘못했나요&lt;/h2&gt;
&lt;p&gt;이야기를 더 진행하기 앞서, 무엇이 이런 상황을 만들었는지 정리해 봅시다.&lt;/p&gt;
&lt;p&gt;근본적인 원인은 저희가 사전에 인지하지 못한 인덱스의 prefix 구조 추가와 변경이었습니다.&lt;/p&gt;
&lt;p&gt;쿠키런: 킹덤의 게임 서버는 실제 플레이 데이터 이외에도, 더 나은 플레이 경험을 위한 데이터 분석을 위해 여러 가지 정보를 DB에 기록합니다. 이 때 사용되는 테이블은 용도에 따라 &lt;code&gt;&amp;#x3C;feature&gt;-&amp;#x3C;uuid&gt;&lt;/code&gt; 형식의 Primary Key를 가집니다. 이 테이블은 런칭 초기에도 Split을 하지 않았었고, 그 상태로도 문제가 발생하지 않았기 때문에 명시적인 Split이 필요없는 테이블로 여겨졌습니다.&lt;/p&gt;
&lt;p&gt;나중에 알고 보니, 런칭 초기에는 모든 유저들이 게임 시작 후 거치는 튜토리얼이 거대한 Funnel로 작용해 유저들이 이 테이블에 쓰기를 하는 시점이 제각각 달랐고, 이 덕분에 Hot Range 없이 자동적인 Split이 자연스럽게 수행될 수 있었습니다. 이와 달리 길드 업데이트의 경우 모든 킹덤 유저가 기다려오던 기능이었고 초기 Funnel 또한 거의 없도록 설계되었습니다. 따라서 업데이트 직후 모든 유저들이 이 테이블에 쓰기 활동을 하게 되어 아주 많은 수의 &lt;code&gt;guild-&amp;#x3C;uuid&gt;&lt;/code&gt; 형식의 Primary Key가 매우 빠르게 쓰여졌고, 결과적으로 시스템에 의한 Split이 미처 수행되지 못해 Hot Range가 발생한 것이 첫번째 원인이었습니다.&lt;/p&gt;
&lt;p&gt;두번째로, 신규 기능이 추가되며 코드 리팩토링이 진행되었고, 겸사겸사 Primary Key의 형태가 변경된 경우도 있었습니다. 이 테이블의 경우 기존에 활발하게 쓰이고 있었고 Split도 잘 되어있었지만, PK의 포맷이 달라지면서 Split되어 있던 Range의 극히 일부만 실제로 사용되도록 변경되었고, 이 또한 마찬가지로 Hot Range를 발생시켰습니다.&lt;/p&gt;
&lt;p&gt;종합적으로 이 테이블들은 &lt;code&gt;1. 기존에 이미 존재하던 테이블이고, 스키마 변경이 없었기 때문에&lt;/code&gt; 잘 눈에 띄지 않았고, &lt;code&gt;2. 플레이에 직접적인 트래픽을 받지 않기 때문에&lt;/code&gt; Hot Range가 발생할 것이라고 미처 판단하지 못했습니다.&lt;/p&gt;
&lt;p&gt;그러나 여기까지만 해도 상황이 심각해지지는 않았을 수도 있습니다. CockroachDB에는 자체적인 부하 기반 Range 분산 로직이 있기 때문에, 시간이 지남에 따라 클러스터가 자가 회복하기를 기대해볼 수 있기 때문입니다.&lt;/p&gt;
&lt;p&gt;이 문제를 더욱 키운 것은 &lt;code&gt;새 DB 노드를 추가한 것&lt;/code&gt;이었습니다. CockroachDB는 새 노드를 클러스터에 추가하는 경우 기존 노드들이 가지고 있던 Range를 자동으로 분산하여, 모든 노드들이 비슷한 양의 작업을 할 수 있도록 만들고자 시도합니다. 이 때 Range가 이동하는 작업은 기존 Range를 분할하여 이동시키는 작업과 같은 큐를 사용합니다. 점검 중에 새 노드가 추가되며 발생한 Replication Queue가 오픈 때까지 충분히 해소되지 않았고, 새 노드에 Range들이 모두 분산된 뒤에야 Hot Range가 분산될 수 있었습니다.&lt;/p&gt;
&lt;p&gt;정리하면, 사전에 부하를 분산하지 않은 테이블에서 Hot Range가 발생하여, CockroachDB에서 부하 분산을 시도했으나 노드 추가로 트리거된 Range 분할로 인해 해당 부하를 분산하지 못하였고, 이로 인해 DB가 과부하되어 발생한 문제였습니다.&lt;/p&gt;
&lt;h2 id=&quot;1155-점검만은-안-돼&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1155-%EC%A0%90%EA%B2%80%EB%A7%8C%EC%9D%80-%EC%95%88-%EB%8F%BC&quot; aria-label=&quot;1155 점검만은 안 돼 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;[11:55] 점검만은 안 돼&lt;/h2&gt;
&lt;p&gt;하지만 이번 문제는 앞서 게시된 두 번의 이슈&lt;sup&gt;&lt;a name=&quot;ftsrc_4&quot; href=&quot;#ft_4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;와는 조금 달랐습니다. 이전의 상황은 데이터베이스가 데이터 로드를 거부했거나 데이터를 검증해줄 수 있는 서버 자체가 사라져 데이터의 정합성을 검증하지 못한 문제로, 이를 복구하기 위해 데이터베이스를 종료하고 storage layer에서 데이터를 조각모음 하는 등의 액션이 필요했습니다. 하지만 지금은 단순히 데이터베이스의 일부 노드에 일시적으로 과도한 부하가 가해져 CockroachDB 내부 로직이 동작할 시간이 부족한 것이어서 이 부하만 해결되면 상황이 호전될 것으로 기대되었습니다.&lt;/p&gt;
&lt;p&gt;이러한 기대를 안고, 저희는 이제 점검 없이 서버에 가해지는 부하를 줄일 수 있는 방법을 찾기 시작했습니다.&lt;/p&gt;
&lt;p&gt;다만 문제는, 저희가 선택할 수 있는 방법이 많지 않다는 점이 었습니다. 순간적인 부하를 줄이기 위해서 사용하는 가장 일반적인 방법은 접속 대기열을 운영하는 것인데, 쿠키런: 킹덤의 서버 아키텍처는 수평적인 확장이 가능하도록 특히 많은 고민과 노력을 기울인 만큼 접속 대기열이 필요할 상황 자체가 존재하지 않아야 했기에 대기열이 마련되어 있지 않았습니다. 그렇다면 유저 접근을 제한할 수 있는 방법은 점검뿐인데, 점검 없이 서버에 가해지는 부하를 줄인다는 고민과는 공존할 수 없는 방법이었습니다.&lt;/p&gt;
&lt;p&gt;이러지도 저러지도 못하고 시간이 지나가던 중, ‘점검이 아닌 점검을 하자’라는 의견이 나왔습니다.&lt;/p&gt;
&lt;h2 id=&quot;1300-점검-아닌-점검&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1300-%EC%A0%90%EA%B2%80-%EC%95%84%EB%8B%8C-%EC%A0%90%EA%B2%80&quot; aria-label=&quot;1300 점검 아닌 점검 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;[13:00] 점검 아닌 점검&lt;/h2&gt;
&lt;p&gt;쿠키런: 킹덤의 점검은 새로 접속하는 유저의 접속을 거부하는 부분과, 이미 게임을 플레이중인 유저를 강제로 접속 종료시키는 부분이 완전히 분리되어 존재합니다. 이 아래부터는 전자를 ‘로그인 블록’, 후자를 ‘유저 킥’으로 표현하겠습니다.&lt;/p&gt;
&lt;p&gt;일반적인 점검 때에는 로그인을 블록한 뒤 기존 유저를 킥하는 동작을 함께 진행하지만, 두 동작은 앞서 언급한 대로 분리되어 존재하기 때문에 따로 실행하는 것 또한 가능합니다.&lt;/p&gt;
&lt;p&gt;접속 대기열은 기본적으로 유저의 접속을 인위적으로 제한하면서, 동시에 원하는 만큼만 유저가 접속할 수 있도록 만드는 기능입니다. 킹덤의 점검에도 로그인하는 유저만 블록할 수 있는 방법이 존재합니다. 그럼 점검을 대기열처럼 사용할 수 있지 않을까요?&lt;/p&gt;
&lt;p&gt;기존 유저를 킥하지 않고 로그인을 블록하는 것으로, 입장 인원을 0명으로 설정한 대기열과 같은 효과를 낼 수 있습니다. 이렇게 되면 이미 게임에 접속해 플레이하던 유저는 아무런 영향을 받지 않으면서, 서버에 가해지는 부하를 적절한 수준으로 관리할 수 있습니다. 접속에 성공한 유저가 게임에 접속하여 하려던 행동을 모두 완료하면 게임을 종료할 것이므로, 새로 게임에 접속하려는 유저를 제어하면 동시 접속자는 자연적으로 감소할 수밖에 없기 때문입니다.&lt;/p&gt;
&lt;p&gt;그렇다고 계속 로그인을 블록하면 안 됩니다. 우리가 원하는 것은 대기열 기능으로, 유저분들이 기다리다 보면 어느 순간 게임에 접속되어 플레이하는 경험을 기대하기 때문입니다. 걸어둔 점검을 종료하면, 기다리시던 유저 분들께서 게임에 접속할 것이고, 지표를 참고하며 이를 반복하면 결국 동시에 원하는 만큼만 유저가 접속할 수 있도록 하는 대기열의 역할을 수행할 수 있게 됩니다. 점검이라는 방법을 사용하여 서비스 점검을 피한, ‘점검 아닌 점검’인 셈입니다.&lt;/p&gt;
&lt;p&gt;이렇게 임시로 만든 대기열을 가지고 동시 접속자 수를 조절한 결과, 데이터베이스 클러스터가 상당히 안정되는 것을 확인할 수 있었습니다. 우아한 방법은 아니지만, 아무도 쿠키런: 킹덤에 접속하지 못하는 상황을 만들지 않고도 장애 상황을 해결할 수 있을 것이라는 희망이 보이기 시작했고, 이대로 지켜보며 클러스터 상황이 정상으로 돌아오기를 기다리자고 결정하게 되었습니다.&lt;/p&gt;
&lt;h2 id=&quot;1450-진짜-긴급점검&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1450-%EC%A7%84%EC%A7%9C-%EA%B8%B4%EA%B8%89%EC%A0%90%EA%B2%80&quot; aria-label=&quot;1450 진짜 긴급점검 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;[14:50] 진짜 긴급점검&lt;/h2&gt;
&lt;p&gt;그렇게 안정화 작업을 진행하던 중, 오후 2시 50분, 클러스터를 구성하는 노드 여러 대가 갑자기 health check를 통과하지 못하기 시작했습니다. 이때는 점검 직후 상황과 비교했을 때 더 많은 노드들이 영향을 받았고, 때문에 이 이상 문제가 전파된다면 데이터 안정성에도 영향이 갈 수 있다고 보여졌습니다. 때문에 클러스터가 불안정해진 직후, 긴급히 아무도 게임을 플레이할 수 없는 진짜 긴급점검을 수행하기로 결정했습니다.&lt;/p&gt;
&lt;p&gt;점검을 시작한 직후부터 클러스터 상태가 눈에 띄게 좋아지는 것을 확인할 수 있었습니다. 당연한 결과이기도 했는데, 문제의 원인이 Hot Range에 있다고 예상된 이상 더 이상 유저 요청을 받지 않는다면 문제가 되는 Range를 사용하지도 않을 것이기 때문입니다. 다만 이를 실제로 확인함으로서 저희가 정의한 문제 상황과 해결 방법이 맞을 것이라는 확신을 가질 수 있었습니다.&lt;/p&gt;
&lt;p&gt;하지만 그렇다고 이 클러스터를 그대로 계속 쓸 수 있다는 확신은 없는 상태였습니다. 클러스터가 불안정해지며 CockroachDB에서 데이터 정합성을 보증할 수 없어 접근 자체를 막은 Unavailable Range가 발생했고, 클러스터가 안정되며 해당 Range가 다시 접근 가능하다고 표시되었지만 이 클러스터에 문제가 발생하지 않을 것이라고 확답할 수 없었기 때문입니다. 여전히 데이터베이스의 여러 지표들은 클러스터가 아직 완전히 정상으로 되돌아오지 않았다고 말하고 있었고, 이전의 경험을 바탕으로 더욱 더 신중히 의사결정을 하고자 하였습니다.&lt;/p&gt;
&lt;p&gt;일련의 데이터베이스 연관 장애를 겪으면서 저희 팀의 목표는 늘 유저 분들이 정성들여 키우신 소중한 왕국의 데이터를 지키는 것이었습니다. 언제 무너질지 모르는 데이터베이스 클러스터를 지켜보며, 저희는 우선 클러스터가 조금이라도 안정적일 때 데이터를 백업해 두기로 결정했습니다. 정기적으로 백업이 진행되고 있지만 실시간 백업이 아닌 이상 이는 최후의 수단이고, 점검 이후 데이터를 백업해 두면 클러스터가 돌아올 수 없는 강을 건너더라도 유저 임팩트는 없을 것이니까요. 백업 이후에 클러스터가 완벽히 정상화되어 백업한 데이터를 쓸 필요가 없으면 더 좋고요.&lt;/p&gt;
&lt;h2 id=&quot;1505-미션-데이터-백업하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1505-%EB%AF%B8%EC%85%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B0%B1%EC%97%85%ED%95%98%EA%B8%B0&quot; aria-label=&quot;1505 미션 데이터 백업하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;[15:05] 미션: 데이터 백업하기&lt;/h2&gt;
&lt;p&gt;그렇지만 저희의 간절한 마음을 아는지 모르는지, CockroachDB 클러스터는 데이터 백업을 거부했습니다. CockroahchDB의 BACKUP statement를 사용해 전체 데이터베이스 백업을 시도했으나, 백업이 진행되다 어느 시점에 멈춰버리는 것을 볼 수 있었습니다. 혹시나 알 수 없는 문제 때문에 BACKUP 구문이 동작하지 않는 것일까 싶어 CSV 파일로 export도 시도해 보았으나, 지표상으로 클러스터가 불안정해지는 것을 확인해 중단했습니다.&lt;/p&gt;
&lt;p&gt;이때 점검 시간이 길어진 주 원인이 바로 이 백업이 진행되지 않는 문제 때문이었습니다. 클러스터 상태가 불안정한 것이 눈에 보이는데 백업을 안 하고 방치하는 것은 선택지가 되지 않습니다. 그렇다고 백업을 하니 백업이 과연 완료되기는 할지, 된다면 얼마나 기다려야 할지 알 수 없는 상황이었습니다.&lt;/p&gt;
&lt;p&gt;어떤 방법을 사용해야 안전하게 데이터를 백업할 수 있을지 찾아내던 중, 전체 데이터베이스가 아닌 테이블 단위 백업을 시도해 보자는 의견이 나왔습니다. 높은 부하가 걸린 것은 특정 테이블의 일부 Range였기 때문에, 이와 관련없는 다른 테이블은 문제없이 백업이 될 것이라는 가설으로부터 도출된 의견이었습니다. 또한 이때 문제가 되는 테이블은 다른 테이블의 정보를 바탕으로 복구 가능한 테이블이었기에, 다른 더 중요한 테이블을 백업할 수 있다면 이 테이블 또한 자연적으로 백업되었다고 볼 수 있는 점도 이러한 결정에 힘을 실어 주었습니다.&lt;/p&gt;
&lt;p&gt;정말 다행히도 이 방법을 통해 킹덤 게임 데이터의 백업에 성공했습니다. 이 방법이 동작한다는 것을 확인한 뒤에는, 일단 필요한 모든 테이블을 테이블 단위로 백업한 뒤 클러스터 안정을 기다린 다음 서비스를 오픈하고, 서버에 일정 수준 이상의 부하가 가해질 경우 킥 없는 점검을 통해 부하를 조절하는 것으로 서비스 오픈 계획을 세울 수 있었습니다.&lt;/p&gt;
&lt;p&gt;백업 중 진행도가 우려스러울 정도로 올라가지 않는 경우도 몇 번 있었으나, CockroachDB 서포트 팀의 도움으로 무사히 백업을 마칠 수 있었습니다.&lt;/p&gt;
&lt;p&gt;이제 손에 데이터를 들고 있기 때문에, 클러스터에 조금 더 도전적인 작업을 시켜볼 수 있게 되었습니다. 이제는 클러스터가 완전히 사용 불능이 되더라도 백업 데이터에서 복원하면 데이터 손실은 없을 것이니까요.&lt;/p&gt;
&lt;h2 id=&quot;1835-오픈-준비하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1835-%EC%98%A4%ED%94%88-%EC%A4%80%EB%B9%84%ED%95%98%EA%B8%B0&quot; aria-label=&quot;1835 오픈 준비하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;[18:35] 오픈 준비하기&lt;/h2&gt;
&lt;p&gt;서비스 오픈을 준비하며, 플레이 기록을 남기는 것은 일시 중지하기로 결정했습니다. 문제가 발생하는 원인은 규명되었고, 오픈 전 해당 테이블의 split을 진행하기로 했으나, 이와 별개로 최대한 데이터베이스에 부하를 가하지 않기 위해 진행한 조치였습니다.&lt;/p&gt;
&lt;p&gt;하지만 예측되는 부하를 줄인다고 해도 쿠키런: 킹덤을 플레이해주시는 유저분들이 많다면 Range 분리는 다시 발생합니다. 그리고 앞선 경험을 기반으로, 우리는 이것이 일어나야 하는, 방해받으면 안 되는 작업임을 잘 알고 있습니다.&lt;/p&gt;
&lt;p&gt;때문에 이 이후 오픈 준비는&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;문제가 되었던 테이블이 모두 잘 Split되어 모든 노드에 골고루 로드가 분산되며&lt;/li&gt;
&lt;li&gt;CockroachDB의 Range Replication Queue가 0이 되어, 오픈 후 Range Split이 발생하더라도 방해받지 않도록&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;만드는 것에 초점을 맞추게 되었습니다.&lt;/p&gt;
&lt;p&gt;그렇게 오후 6시 반 경, 문제가 되었던 테이블에 대해 Split 액션을 수행하기 시작했습니다&lt;sup&gt;&lt;a name=&quot;ftsrc_5&quot; href=&quot;#ft_5&quot;&gt;[5]&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;h2 id=&quot;1920-좋은-노드-기다리는-노드-떠나간-노드&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1920-%EC%A2%8B%EC%9D%80-%EB%85%B8%EB%93%9C-%EA%B8%B0%EB%8B%A4%EB%A6%AC%EB%8A%94-%EB%85%B8%EB%93%9C-%EB%96%A0%EB%82%98%EA%B0%84-%EB%85%B8%EB%93%9C&quot; aria-label=&quot;1920 좋은 노드 기다리는 노드 떠나간 노드 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;[19:20] 좋은 노드, 기다리는 노드, 떠나간 노드&lt;/h2&gt;
&lt;p&gt;메트릭을 지켜보며 Split이 완료되기를 기다리고 있던 중, 오후 7시 20분 경 슬랙 알림이 한 건 날아옵니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 606px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 39.72222222222222%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsTAAALEwEAmpwYAAABQElEQVR42pVRC46CMBTkZt7Mi3gXr7DJxkRcPygIFEoLteUn0dn3mmA2brLJTjJhaKfT4RGMqoKoDPZ5g1Q5RIVBJAxiefP6WjnkuvWcNfuk6RGXFmfy8numHZ5PILCXMy7XEmGiEMYSnyeBTVTg45ATMwhl4Loezc2hNhbGtmisg3UdrbVohzv6cUJ/nwA8KTCOcTkd8bXfY7vdIiQeSFdSQlF7SU9jGsiyRCEEVFV5aqWgtaZWVOsHKDCBzHOIokBJh3LSbPwvOJjpG1aiQE63x6TTNKVgbmUwTdPL+N5kDnlH4JIEx93OB1lrUde1J7fkwBnDMCAMQ2RZ5tfny34FttRMZMI35Hkx+VBBIxjH8WVkHUWR3+PRsI9DH4+H31+tVlgsFgg6+jsn3aFUNbXo//yc93kx5sD1eo3lcolvZX9gsIOzJl0AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/b9caf4f47fa4705e087527c82a540fd1/33ecc/02-instance-status-fail.webp 360w,
/static/b9caf4f47fa4705e087527c82a540fd1/263a4/02-instance-status-fail.webp 480w,
/static/b9caf4f47fa4705e087527c82a540fd1/e5c51/02-instance-status-fail.webp 606w&quot; sizes=&quot;(max-width: 606px) 100vw, 606px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/b9caf4f47fa4705e087527c82a540fd1/53918/02-instance-status-fail.png 360w,
/static/b9caf4f47fa4705e087527c82a540fd1/9aebd/02-instance-status-fail.png 480w,
/static/b9caf4f47fa4705e087527c82a540fd1/cec09/02-instance-status-fail.png 606w&quot; sizes=&quot;(max-width: 606px) 100vw, 606px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/b9caf4f47fa4705e087527c82a540fd1/cec09/02-instance-status-fail.png&quot; alt=&quot;02 instance status fail&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        OH NO...
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;바로 데이터베이스 노드 한 대가 Host Status Check를 통과하지 못했다는 알림이었습니다. 이 알림이 발생했을 때 운이 정말 좋지 않은 경우 EC2 인스턴스 자체가 대응할 시간 없이 셧다운되는 상황까지 가는 경우가 있지만, 이 때는 인스턴스가 셧다운되는 상황은 아니었습니다.&lt;/p&gt;
&lt;p&gt;동시에 서버들의 이상 로그를 감지하는 채널에 한 메시지가 날아왔는데요, 해당 인스턴스에서 &lt;code&gt;filemap_fdatawait_range_keep_errors&lt;/code&gt;라는 로그가 감지되었다는 알림이었습니다.&lt;/p&gt;
&lt;p&gt;리눅스 커널에서 &lt;code&gt;filemap_fdatawait&lt;/code&gt;은 Write-back Page를 다시 디스크에 작성하는 계통의 메소드인데요, 지금 Range를 옮기고 분리하는 것과 같이 디스크 Write 작업이 많은 상황에서 이러한 로그가 보인다는 것은 좋은 징조는 아니었습니다. Host Status Check이 실패했으니 이 인스턴스 자체도 신뢰할 수 없어졌다는 것 또한 문제가 되었습니다.&lt;/p&gt;
&lt;p&gt;때문에 저희는 이 노드를 클러스터에서 제거하기로 결정하고, Range 분리와 이 노드의 Decommission을 함께 진행했습니다. 이 노드의 Decommission이 완료되면서 복제되지 않은 Range는 0이 되었고, 클러스터의 각종 지표 또한 안정적인 상태로 돌아왔습니다.&lt;/p&gt;
&lt;h2 id=&quot;2200-대망의-오픈&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2200-%EB%8C%80%EB%A7%9D%EC%9D%98-%EC%98%A4%ED%94%88&quot; aria-label=&quot;2200 대망의 오픈 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;[22:00] 대망의 오픈&lt;/h2&gt;
&lt;p&gt;데이터베이스 클러스터가 정상으로 돌아온 것처럼 보였어도 마음을 놓기에는 넘어야 할 산이 조금 더 있었습니다. 우선 점검이 끝나기만을 기다리던 유저 분들이 한꺼번에 게임에 접속하실 것이 자명했고, 로그인 보너스를 수령하기 위해 12시경 접속하는 유저 분들이 매우 많을 것인데다, 이렇게 많은 트래픽이 흘러들어갔을 때 데이터베이스가 안정적일지는 지켜보아야만 알 수 있었습니다.&lt;/p&gt;
&lt;p&gt;모든 엔지니어들이 숨죽여 지켜보는 가운데 점검이 종료되었고, 서버는 트래픽을 잘 받아내는 것처럼 보였습니다. 그냥 그렇게 트래픽을 잘 받아내서 처리해줬으면 더할 나위 없이 좋았겠으나, 현실은 그렇지 않았습니다. 또 한 데이터베이스 노드에서 CPU를 100% 사용하는 것이 확인되었고, 다시 대기열을 통해 노드의 CPU 사용량을 안정시켰습니다.&lt;/p&gt;
&lt;p&gt;이 때까지는 한 두 노드가 상태 회복을 위해 이럴 수 있다고 판단할 수 있는 내용이었는데, 이 뒤 얼마 지나지 않아 정확히 해당 노드가 다시 CPU를 100% 사용하기 시작했습니다. 몇 차례 대기열을 활용해 트래픽을 제어했으나, 문제가 발생하는 노드가 같다는 점에서 노드 자체의 문제일 가능성이 높아졌고, 결국 이 노드를 종료시킴으로서 문제 상황이 모두 해결되었습니다.&lt;/p&gt;
&lt;h2 id=&quot;몸으로-체득한-경험&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AA%B8%EC%9C%BC%EB%A1%9C-%EC%B2%B4%EB%93%9D%ED%95%9C-%EA%B2%BD%ED%97%98&quot; aria-label=&quot;몸으로 체득한 경험 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;몸으로 체득한 경험&lt;/h2&gt;
&lt;p&gt;일반적으로 RDBMS에서 Primary Key의 Prefix의 변경은 큰 문제가 되지 않습니다. 작업이 한 대의 서버에서만 처리되기 때문에 애초에 분산 처리를 상정하지 않는 이유도 있지만, RDBMS의 인덱스 자료형이 Prefix의 구조 변화에 크게 영향을 받지 않기 때문이기도 합니다.&lt;/p&gt;
&lt;p&gt;하지만 CockroachDB의 분산 처리 특성상, 프로덕션 클러스터에서 이러한 변경이 가해지는 것은 상당한 주의가 필요한 작업임을 체득할 수 있었습니다. 팀 내에서 CockroachDB 스터디를 진행하며, 문서와 관련 자료들을 리뷰하며 충분히 알고 있던 내용이었지만, 실제 체험은 글로 얻은 정보에 비해 훨씬 큰 사건이었습니다.&lt;/p&gt;
&lt;h2 id=&quot;마무리하며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EB%AC%B4%EB%A6%AC%ED%95%98%EB%A9%B0&quot; aria-label=&quot;마무리하며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;마무리하며&lt;/h2&gt;
&lt;p&gt;이렇게 킹덤 런칭 초창기의 점검 이야기는 모두 마무리됩니다.&lt;/p&gt;
&lt;p&gt;이 당시 상황을 돌아보면, 많은 유저분들이 쿠키런: 킹덤을 사랑해주신 덕분에 전사적으로 처음 맞이해보는 규모의 트래픽을 다루게 되었고, 이 큰 트래픽을 잘 다루기 위해 끊임없이 노력을 기울이던 때였던 것 같습니다. 저희 예상보다 훨씬 더 많은 분들께서 쿠키런: 킹덤을 좋아해주셨고, 때문에 이에 맞추어 정말 많은 것을 개선하고 보수해야 했던, 정말 쏜살같이 빠르게 지나간 시간이기도 했습니다.&lt;/p&gt;
&lt;p&gt;하지만 그런 환경 속에서도, 어떤 챌린지가 닥쳐왔을 때 타협하는 대신 최고의 고객 경험을 위해 끊임없이 다양한 방법을 시도하고 길을 찾아내는 것은, 데브시스터즈의 모든 구성원이 각자의 분야에서 이러한 노력을 뒷받침할 수 있는 경험과 스킬을 가지고 있기 때문에 가능했던 일이 아닐까 싶습니다.&lt;/p&gt;
&lt;p&gt;긴 이야기의 끝까지 함께해 주셔서 감사합니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;a name=&quot;ft_1&quot; href=&quot;#ftsrc_1&quot;&gt;[1]&lt;/a&gt;: &lt;a href=&quot;https://www.cockroachlabs.com/docs/v22.2/configure-replication-zones#replication-zone-variables&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://www.cockroachlabs.com/docs/v22.2/configure-replication-zones#replication-zone-variables&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a name=&quot;ft_2&quot; href=&quot;#ftsrc_2&quot;&gt;[2]&lt;/a&gt;: &lt;a href=&quot;https://www.cockroachlabs.com/docs/v22.1/load-based-splitting&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://www.cockroachlabs.com/docs/v22.1/load-based-splitting&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a name=&quot;ft_3&quot; href=&quot;#ftsrc_3&quot;&gt;[3]&lt;/a&gt;: &lt;a href=&quot;https://www.cockroachlabs.com/blog/sql-in-cockroachdb-mapping-table-data-to-key-value-storage/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://www.cockroachlabs.com/blog/sql-in-cockroachdb-mapping-table-data-to-key-value-storage/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a name=&quot;ft_4&quot; href=&quot;#ftsrc_4&quot;&gt;[4]&lt;/a&gt;: &lt;a href=&quot;/posts/crk-launch-storage-postmortem/&quot;&gt;쿠키런: 킹덤 데이터베이스 스토리지 레이어 복원기&lt;/a&gt;, &lt;a href=&quot;/posts/crk-aws-az-failure-postmortem/&quot;&gt;쿠키런: 킹덤 AWS AZ 장애 아웃라인&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a name=&quot;ft_5&quot; href=&quot;#ftsrc_5&quot;&gt;[5]&lt;/a&gt;: 이 오퍼레이션에 관해 더 자세히 알고 싶으시다면 &lt;a href=&quot;/posts/cockroachdb-in-production/&quot;&gt;CockroachDB in Production&lt;/a&gt;의 &lt;code&gt;Production 배포시 고려해야 할 사항 &gt; Range Split&lt;/code&gt;을 참고해주세요&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[쿠키런: 킹덤 AWS AZ 장애 아웃라인]]></title><description><![CDATA[쿠키런: 킹덤 런칭 후 1달도 지나지 않아 발생했던 AWS AZ 장애로 인한 서비스 복구기에 대해 전해드리고자 합니다.]]></description><link>https://tech.devsisters.com/posts/crk-aws-az-failure-postmortem</link><guid isPermaLink="false">https://tech.devsisters.com/posts/crk-aws-az-failure-postmortem</guid><pubDate>Thu, 16 Feb 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;이 글과 관련된 다른 글 보기&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/posts/cookierun-kingdom-launch-revisited/&quot;&gt;쿠키런: 킹덤 런칭 회고&lt;/a&gt; - DevOps 관점에서 본 쿠키런: 킹덤 런칭 회고&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/cockroachdb-in-production/&quot;&gt;CockroachDB in Production&lt;/a&gt; - CockroachDB에 대한 소개와 킹덤에서 CockroachDB를 사용한 이유 설명&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/crk-launch-storage-postmortem/&quot;&gt;쿠키런: 킹덤 데이터베이스 스토리지 레이어 복원기&lt;/a&gt; - 엔지니어 관점 기록한 개괄적인 장애 대응 과정과 회고&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/bit-level-database-hacking&quot;&gt;CTO가 커리어를 걸고 비트 레벨까지 내려가서 DB를 해킹했던 이야기&lt;/a&gt; - CockroachDB의 소스코드를 참고하며 바이너리 파일로부터 데이터를 복구한 이야기&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/36h-downtime-on-first-day/&quot;&gt;입사 첫날에 36시간 점검 경험하기&lt;/a&gt; - 신규입사자의 관점에서 서술된 장애 대응 기록&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/crk-hot-range-postmortem/&quot;&gt;쿠키런: 킹덤 길드 업데이트 이후 서비스 이슈 되돌아보기&lt;/a&gt; - 쿠키런: 킹덤에 길드 기능이 추가된 이후 발생한 서비스 이슈 회고&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;안녕하세요, 데브시스터즈 진저랩 플랫폼셀에서 소프트웨어 엔지니어로 일하고 있는 박새미, 인프라셀에서 일하고 있는 데브옵스 엔지니어 이창원입니다. 이 글에서는 2022년 NDC에서 함께 발표한 &lt;a href=&quot;https://www.youtube.com/watch?v=AZbCZ2KOcwU&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;쿠키런: 킹덤, 총 56시간의 긴급 점검 회고 - 그때 그 명검은 왜 뽑아야 했는가&lt;/a&gt; 세션에서도 다룬 바 있는, 2021년 2월 19일에 발생한 AWS 도쿄 리전 데이터센터 장애에 대해 공유해 드리고자 합니다. 만약 정보를 접할 때 글보다 영상을 선호하는 편이시라면, 해당 세션 영상을 보시는 것을 권해드립니다.&lt;/p&gt;
&lt;h2 id=&quot;cockroachdb-분산-데이터베이스&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#cockroachdb-%EB%B6%84%EC%82%B0-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4&quot; aria-label=&quot;cockroachdb 분산 데이터베이스 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;CockroachDB? 분산 데이터베이스!&lt;/h2&gt;
&lt;p&gt;어느 평화로운 봄날, 회식 자리에서 DB 노드 한 대가 내려갔다는 에러 알람이 슬랙에 나타났지만, 데브옵스 엔지니어들은 크게 당황하지 않았습니다. AWS 같은 거대한 클라우드 벤더사라고 해서 100% SLA를 보장하지는 못하기도 하고요. 무엇보다도 CockroachDB는 이러한 장애를 견딜 수 있게 설계된 분산 데이터베이스이기 때문입니다.&lt;/p&gt;
&lt;p&gt;기본적으로 분산 DB는 일부 노드가 정상적으로 동작하지 않아도 견딜 수 있게끔 설계됩니다. CockroachDB는 기본적으로 3대의 레플리카를 두게 되어 있으며, 각 Range&lt;sup&gt;&lt;a name=&quot;ftsrc_1&quot; href=&quot;#ft_1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt; 는 레플리카들에 복사되어 저장됩니다. CockroachDB는 Raft 알고리즘을 채택하였고 이에 따라 DB가 N대 있다고 했을 때 (N/2) + 1 이상의 값, 즉 과반수를 정족수로 사용합니다. 결과적으로 정족수가 깨지지 않는 한 일부 노드가 죽어도 영향을 받지 않게 설계된 것이지요. 예를 들어 Replication을 5대로 설정한다면, (5/2)-1대 미만의 노드가 죽어도 DB를 이용하는 데는 문제가 없습니다. CockroachDB에 더 자세히 알고싶으시다면 진저랩 인프라셀 이준성 님이 쓰신 &lt;a href=&quot;../cockroachdb-in-production&quot;&gt;CockroachDB in Production&lt;/a&gt;을 읽어보시는 것을 추천드립니다.&lt;/p&gt;
&lt;p&gt;다만 분산 DB라 하더라도 3분 만에 또 2대, 6분 뒤에 2대, 20분 후에 2대… 22분 만에 총 60대&lt;sup&gt;&lt;a name=&quot;ftsrc_2&quot; href=&quot;#ft_2&quot;&gt;[2]&lt;/a&gt;&lt;/sup&gt; 중 6대의 노드가 작동 불능이 되는 것은 조금 다른 이야기입니다. 엔지니어들은 빠르게 상황 파악 및 대응에 나섰습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;img src=&quot;/a59dece4aea5e442bd869ddf1a820b33/01-ptsd-simulator.gif&quot;&gt;
    &lt;figcaption&gt;
        다시는 보고 싶지 않은 그 날의 Ops 채널 알람을 모사해보았습니다.
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;무슨-일이-일어나고-있나요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%B4%EC%8A%A8-%EC%9D%BC%EC%9D%B4-%EC%9D%BC%EC%96%B4%EB%82%98%EA%B3%A0-%EC%9E%88%EB%82%98%EC%9A%94&quot; aria-label=&quot;무슨 일이 일어나고 있나요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;무슨 일이 일어나고 있나요?&lt;/h2&gt;
&lt;p&gt;애플리케이션 레벨에서 DB 호출과 관련된 치명적인 로직이 없고 DB 설계 자체에도 결함이 없다면, 12분만에 6대의 노드가 장애를 겪는 건 뭔가 다른 곳에 원인이 있다는 뜻일 것입니다. 이렇게 다발적이고 급격하게 Failure가 발생할 경우에는 문제의 Scope를 정확히 파악하는 것이 중요하다고 생각합니다. 여러 커뮤니티 사이트들을 살펴보니 저희만 겪는 문제가 아닌 것도 같았습니다. 또한 AWS 고객사로 알려진 다른 서비스들도 문제가 발생했다는 정황을 다양한 경로에서 파악할 수 있었습니다. 상황을 파악해본 결과, 해당 장애의 원인은 &lt;a href=&quot;https://news.mt.co.kr/mtview.php?no=2021022218143711748&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;AWS 도쿄리전 데이터센터의 냉각 시스템 고장&lt;/a&gt;이었습니다. 서버 머신들이 과열되어 작동에 문제가 발생한 것이었습니다. 안타까운 점은 저희의 경우 고장 난 냉각 시스템의 범위에 있던 서버들이 게임 서버가 아니라 하필 DB였고, AZ 장애에 대한 고려가 배제되었었다는 점입니다. 게임 서버였다면 큰 문제 없이 자동으로 새 서버 인스턴스가 떴을 것이고, 순간적인 서비스 불안정 정도는 발생했을 수 있었겠지만, 점검을 걸 필요까지는 없었을 것입니다.&lt;/p&gt;
&lt;h2 id=&quot;az-장애는-왜-고려하지-않았는가&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#az-%EC%9E%A5%EC%95%A0%EB%8A%94-%EC%99%9C-%EA%B3%A0%EB%A0%A4%ED%95%98%EC%A7%80-%EC%95%8A%EC%95%98%EB%8A%94%EA%B0%80&quot; aria-label=&quot;az 장애는 왜 고려하지 않았는가 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;AZ 장애는 왜 고려하지 않았는가?&lt;/h2&gt;
&lt;p&gt;CockroachDB는 AZ 혹은 Region 장애까지도 대비할 수 있는 &lt;a href=&quot;https://www.cockroachlabs.com/docs/v22.2/multiregion-overview#database-regions&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Locality&lt;/a&gt;라는 기능이 있습니다. 하지만 자동으로 지정되는 것이 아니고, 당연하게도 (사람) Operator가 알맞은 Topology 설정을 따로 해줘야 합니다. 그러면 노드가 클러스터에 합류한 이후에 Replica를 분배받을 때, 하나의 Topology가 완전히 날아가더라도 데이터 유실이 발생하지 않도록 분배받게 됩니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 960px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.388888888888886%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB/ElEQVR42lVSS2/TQBDOv+TGESG4cSkHQFxAIA6gSAgUCSoVVRFFglZFQEEplZq0qRyI6zh2/LbjJK4Sv+19GDs265ig9tPsa3a/mdmZqRUrLOauPfeDEACM0zxPs7SSJEmyLCvWWC6XZAYA2LZN9DVyyLOlzcrOuezzCtQ1aI5hBOIwjiMQRkEQ+I7juJ5bWUEYxXFMtHmel2QModdsuo1GsLkZNrbQ3jdDHtNn/KDPCbwkC7q9cFRJc+ce8KAzd33fXywWhFiSIULx46fgzgbauOvduh8/eaNe8Cfipz5z/Js/opQvhm4wlEBTQ5YWBEbnuRFC8B85CkPYfA/rL+PndVSvF7sHrPV9S7n28deDvdG9pnZ9MjVb4usOv3PC7rakV+qMLT9brMIOXDfp9fDhT+fdNmod5oPhdKKLCmeajGEMJXU0nZkf9JufuUf7/Wc72g1+drxKX14lLMMDNmOH8LT7hxkUiiqLUvf07OHB4O0RfU5RqqYrkm6Z4wuTXGnWzKqSX6sWjDGAMF+XxLFtVVW+8mZHNKypOTYnDNV98aN3e59ud9qqopQuq2yvQsgTjEkNABlRRGwVl+AHAc8N+6LWHumGYURReNUzKkGMVW0AIfR9D+EEYex5HumKMCwJaZqSI+mcK+QqjMt7AmKCPCWEtbIUovxP/gsk6lG/O8vHZQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/6d3cde88634564e7187eb4be892300cb/33ecc/02-crdb-locality-topology.webp 360w,
/static/6d3cde88634564e7187eb4be892300cb/263a4/02-crdb-locality-topology.webp 480w,
/static/6d3cde88634564e7187eb4be892300cb/0c8fb/02-crdb-locality-topology.webp 640w,
/static/6d3cde88634564e7187eb4be892300cb/521f7/02-crdb-locality-topology.webp 860w,
/static/6d3cde88634564e7187eb4be892300cb/a6361/02-crdb-locality-topology.webp 960w&quot; sizes=&quot;(max-width: 960px) 100vw, 960px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/6d3cde88634564e7187eb4be892300cb/53918/02-crdb-locality-topology.png 360w,
/static/6d3cde88634564e7187eb4be892300cb/9aebd/02-crdb-locality-topology.png 480w,
/static/6d3cde88634564e7187eb4be892300cb/0f09e/02-crdb-locality-topology.png 640w,
/static/6d3cde88634564e7187eb4be892300cb/3e3fe/02-crdb-locality-topology.png 860w,
/static/6d3cde88634564e7187eb4be892300cb/a91f8/02-crdb-locality-topology.png 960w&quot; sizes=&quot;(max-width: 960px) 100vw, 960px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/6d3cde88634564e7187eb4be892300cb/a91f8/02-crdb-locality-topology.png&quot; alt=&quot;02 crdb locality topology&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        Region 장애에서도 올바르게 설정된 &lt;a href=&quot;https://www.cockroachlabs.com/docs/stable/topology-follow-the-workload.html&quot;&gt;Follow-the-Workload Topology&lt;/a&gt; 덕분에 데이터 유실을 방지하는 시나리오
    &lt;/figcaption&gt;
&lt;figure&gt;&lt;/figure&gt;&lt;/figure&gt;
&lt;p&gt;저희는 Kubernetes 위에 CockroachDB를 배포하였고, 데이터베이스라는 하나의 논리적인 단위이니만큼 하나의 통일된 Helm Chart를 사용해서 배포를 관리하고자 했습니다. Multi-AZ 배포를 위한 Topology는 CockroachDB Pod이 생성되는 시점에 배정이 되어야 하는데, 이 정보는 배포를 준비하는 차트 작성 단계에는 알 수가 없고, Pod이 특정 노드에 물리적으로 배정이 되고 난 이후에야 비로소 정해지며 Kubernetes API를 통해서도 제공이 되지 않습니다. 작성에 필요한 정보가 부재한 상황을 우회하려면 기술적 복잡성이 과도하게 가중되는 상황이었기 때문에, 자연스레 이 정도의 중요도가 있는 작업인지를 따져보게 되었습니다. 데브시스터즈는 2013년 쿠키런 for Kakao 시절부터 AWS를 사용해왔지만, AZ 장애가 한번도 발생하지 않았다는 점을 고려했을때, 차라리 보다 시급한 조치가 필요한 다른 일부터 진행하기로 결정했습니다. 대표적으로, Instance Retirement 같이 보다 흔하고 동시다발적으로 발생할 수 있는 시나리오에 대비하기로 하였습니다. 이전 기록들을 기반으로 동시에 여러 노드가 Retire될 확률을 경험적으로 계산하여 3대까지는 동시에 내려가도 데이터 손실이 발생하지 않도록 Replication Factor를 7로 설정해두었습니다. 물론 이 결정은 오랜 기간 안정적으로 사용해왔던 클라우드 프로바이더에 대한 신뢰가 있었기에 할 수 있었던 결정이었습니다. 으레 플래그들이 그렇듯이, 그때는 이것이 아주 완벽한 플래그로 작용하게 될 결정이라는 것을 몰랐습니다…&lt;/p&gt;
&lt;h2 id=&quot;장애-영향-범위&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9E%A5%EC%95%A0-%EC%98%81%ED%96%A5-%EB%B2%94%EC%9C%84&quot; aria-label=&quot;장애 영향 범위 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;장애 영향 범위&lt;/h2&gt;
&lt;p&gt;결과적으로 안전 마진으로 설정해둔 3대를 넘어서는 6대의 노드에서 Host Status Failure가 발생했기 때문에 전체 25,000개의 Range 중 Data Range 2개, System Range 32개가 소실되었습니다. Range의 레플리카는 7로 설정되어 있었고, 각 Range의 레플리카 중 과반인 4개 이상의 Replica가 하필 문제의 노드 6대에 들어있을 확률을 계산해보면 대략 0.09%입니다. Range 각각에 대해 독립사건이라고 가정했을 때, 동전을 25000번 던지는 것과 유사하니 B(25000, 0.09)라고 할 수 있습니다. 평균적으로는 약 23개가 영향을 받는 정도의 확률이고, 32개의 Range가 소실될 확률까지 계산해보면 약 2.3% 정도에 불과합니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1294px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 58.05555555555555%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAIAAADtbgqsAAAACXBIWXMAAAsTAAALEwEAmpwYAAABmUlEQVR42mVSy3KCQBD0/3/EBF9lPBkPXswhWoqkrBxEREQoYEEXEBbklRajYNkF1MzsDD2zPQ3TNK0SMAzDsG17vV73+/3BYNBqtTiOgyGKok2I+QxCSONYw6kEDFmW8YvFYjGfz2Ej4jhOPRMupbRxuSNJEtd1dV1TVcWjFJGiKHzfBwOYLUKiKHpk3oynYs+ly19l9K1PfqhmHNW9YpiWOJ1+NZvSeMzO50dZVVwLxUc30KxQt5nvh2e8YegRy9kpaCm+M1fFqET3aO/mZ2mSZ0meXrIsi+IYP86KIs1zPJcXXJkxPe4pRuqNPYqTPD8Zxnb0qUwmYRAkaVqnrWYGLXTCF8VJCVAhtPnoK73urtPejsdhFOUvzMismOGgngUB2UqbXmfHvSvdttLtyG9NaTg82c51ijhO0/RpZigE6bAelmkdtpLK8/qS1wRBF4TDkjdWq/1stl9LOlaoXBVoduv8XypwRiWuc2e4sRwUSGFoDzYicBlDAmPscTuVzjhACy6lUCgIAiyR73l+qRaCOPE8D/G61H/Pvp00sEqaRgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/227316d902dbca3e4fb2b9d1507f752a/33ecc/03-if-only-this-were-my-gacha.webp 360w,
/static/227316d902dbca3e4fb2b9d1507f752a/263a4/03-if-only-this-were-my-gacha.webp 480w,
/static/227316d902dbca3e4fb2b9d1507f752a/0c8fb/03-if-only-this-were-my-gacha.webp 640w,
/static/227316d902dbca3e4fb2b9d1507f752a/521f7/03-if-only-this-were-my-gacha.webp 860w,
/static/227316d902dbca3e4fb2b9d1507f752a/cbd37/03-if-only-this-were-my-gacha.webp 1200w,
/static/227316d902dbca3e4fb2b9d1507f752a/42400/03-if-only-this-were-my-gacha.webp 1294w&quot; sizes=&quot;(max-width: 1294px) 100vw, 1294px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/227316d902dbca3e4fb2b9d1507f752a/53918/03-if-only-this-were-my-gacha.png 360w,
/static/227316d902dbca3e4fb2b9d1507f752a/9aebd/03-if-only-this-were-my-gacha.png 480w,
/static/227316d902dbca3e4fb2b9d1507f752a/0f09e/03-if-only-this-were-my-gacha.png 640w,
/static/227316d902dbca3e4fb2b9d1507f752a/3e3fe/03-if-only-this-were-my-gacha.png 860w,
/static/227316d902dbca3e4fb2b9d1507f752a/64756/03-if-only-this-were-my-gacha.png 1200w,
/static/227316d902dbca3e4fb2b9d1507f752a/80132/03-if-only-this-were-my-gacha.png 1294w&quot; sizes=&quot;(max-width: 1294px) 100vw, 1294px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/227316d902dbca3e4fb2b9d1507f752a/80132/03-if-only-this-were-my-gacha.png&quot; alt=&quot;03 if only this were my gacha&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        32개의 Range에 대해서 완전 유실이 발생했을 확률을 계산해본 정규분포 그래프
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;System Range의 경우 클러스터에 저장된 데이터들의 위치를 포함한 각종 메타 정보가 포함되어 있습니다. Cockroach Labs 서포트 엔지니어 측에 확인해보니 System Range는 소실되어도 복구할 수 있다고 하여, 저희는 실제 유저분들의 데이터가 저장된 Data Range 2개를 복구하는 데 최대한 집중하였습니다.&lt;/p&gt;
&lt;h2 id=&quot;우리는-해결할-것이다-늘-그랬듯이&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9A%B0%EB%A6%AC%EB%8A%94-%ED%95%B4%EA%B2%B0%ED%95%A0-%EA%B2%83%EC%9D%B4%EB%8B%A4-%EB%8A%98-%EA%B7%B8%EB%9E%AC%EB%93%AF%EC%9D%B4&quot; aria-label=&quot;우리는 해결할 것이다 늘 그랬듯이 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;우리는 해결할 것이다 늘 그랬듯이&lt;/h2&gt;
&lt;p&gt;지난번 장애에서 세 가지 해결 방법을 동시에 진행했듯이, 이번에는 두 가지 방법을 동시에 진행하였습니다. 두 방법은 생존한 54대 노드의 데이터와 Consistency 확인이 필요하다는 점에서는 동일했고, 54대가 들고 있지 않은 나머지 데이터를 가져오는 방법에 있어서 차이가 있었는데요. 해결 방법1은 Quorum이 깨진 레플리카에 남아있는 데이터들을 긁어오는 것이고, 해결 방법2는 백업에서 퍼오는 것이었습니다. 일단 공통적으로 필요한 작업인 54대의 생존한 노드로부터 테이블 백업 기능을 이용하여 데이터를 꺼내려고 시도했지만, 잘되지 않았습니다. 깨진 Range가 있어서 잘 안된 게 아닐까 추측할 수 있었습니다. 따라서 일종의 우회전략을 사용했는데요. &lt;code&gt;INSERT INTO new_table SELECT * FROM old_table WHERE {unaffected_ranges}&lt;/code&gt; 같은 식으로 깨진 Range를 제외한 데이터를 새 테이블에 이사시켰습니다.
한편 백업 데이터를 활용하는 해결 방법2의 경우에는 주기적인 백업 데이터를 활용하는데, 이 백업 데이터를 퍼오는 작업 속도가 매우 느렸습니다. 8시간이 지나도 완료가 될 기미가 보이지 않았습니다. 저희는 조금이라도 점검 시간을 줄이고 싶었고, 어떻게 하면 더 빨리 데이터를 이사시킬 수 있을지 고민하던 찰나 생각지 못한 해결 방법3이 등장하게 됩니다.&lt;/p&gt;
&lt;p&gt;CockroachDB 소스코드를 살펴보던 중 &lt;code&gt;debug unsafe-remove-dead-replicas&lt;/code&gt;&lt;sup&gt;&lt;a name=&quot;ftsrc_3&quot; href=&quot;#ft_3&quot;&gt;[3]&lt;/a&gt;&lt;/sup&gt;라는 명령어를 발견한 것이었습니다. Quorum이 깨진 Range를 복구 시도하는 것인데, &lt;code&gt;unsafe&lt;/code&gt;라는 키워드에서도 짐작하실 수 있듯 일종의 흑마법(?) 같은 명령어입니다. 해당 명령어에는 긴 주석이 달려있었는데요. 주석을 거칠게 번역하자면, “이 명령은 복구를 위한 최후의 수단이다. 데이터가 만약 복구된다고 하더라도 그 데이터가 정합성이 맞다는 보장도 없다. 이 명령은 꼭 Cockroach Labs 엔지니어가 보는 앞에서만 사용해라&quot; 정도로 번역할 수 있을 겁니다. 어쩌면 당연하게도 공식 문서에서는 찾아볼 수 없는 기능입니다. 저희는 Cockroach Labs 엔지니어들과 줌으로 화면 공유를 하면서 해당 명령어를 실행했습니다. 결과적으로 흑마법은 성공적이었으며 소실되었던 2개 Range 중 하나를 살릴 수 있었습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1600px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 36.388888888888886%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAIAAACHqfpvAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAY3RFWHRSYXcACmdlbmVyaWMgcHJvZmlsZQogICAgICAzNAo0OTQ5MmEwMDA4MDAwMDAwMDEwMDMxMDEwMjAwMDcwMDAwMDAxYTAwMDAwMDAwMDAwMDAwNTA2OTYzNjE3MzYxMDAwMAozQmzXAAABKUlEQVR42i1QCXKDMAzkEQ3hCGADNrY5jGOHq7l7Tv//oC60Mzs7kiyttPYIqbg6Sj0oM9dmBrfuLPtJ9SOpet5Noj0pM1XdEFERERFmHEjKNswqLyoaZh9UmP7yI929XT7b+UPaK5h1s3S3bvkS7qZO93p8qtNDumtSNlTZmErPP5SpsH6c70LyAgTZLgKnSP8RgbPtNdtSuvtrC4mXso5Ky/WScV22I9MzkaZoBhSJMABVDtgCm9eILW7eHwrs8zJhMFmZ17KbKnPGeWuHPKIbQkxPqPN+gRzA+rUTuli7DjO9wAyswrCwFwyvnt0NxXp8q4encqtbqDTTuz5/i+MlzhWO3zZzXbQjxHLlgpTjnjDlQcrwpUHC9kmJCtKVEwb4Uf631o/zX9kXQsxIw8NqAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/d47c7a4b7fe2f296f7b1e5fc76fb888b/33ecc/04-cockroach-debug-unsafe-remove-dead-replicas.webp 360w,
/static/d47c7a4b7fe2f296f7b1e5fc76fb888b/263a4/04-cockroach-debug-unsafe-remove-dead-replicas.webp 480w,
/static/d47c7a4b7fe2f296f7b1e5fc76fb888b/0c8fb/04-cockroach-debug-unsafe-remove-dead-replicas.webp 640w,
/static/d47c7a4b7fe2f296f7b1e5fc76fb888b/521f7/04-cockroach-debug-unsafe-remove-dead-replicas.webp 860w,
/static/d47c7a4b7fe2f296f7b1e5fc76fb888b/cbd37/04-cockroach-debug-unsafe-remove-dead-replicas.webp 1200w,
/static/d47c7a4b7fe2f296f7b1e5fc76fb888b/64296/04-cockroach-debug-unsafe-remove-dead-replicas.webp 1600w&quot; sizes=&quot;(max-width: 1600px) 100vw, 1600px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/d47c7a4b7fe2f296f7b1e5fc76fb888b/53918/04-cockroach-debug-unsafe-remove-dead-replicas.png 360w,
/static/d47c7a4b7fe2f296f7b1e5fc76fb888b/9aebd/04-cockroach-debug-unsafe-remove-dead-replicas.png 480w,
/static/d47c7a4b7fe2f296f7b1e5fc76fb888b/0f09e/04-cockroach-debug-unsafe-remove-dead-replicas.png 640w,
/static/d47c7a4b7fe2f296f7b1e5fc76fb888b/3e3fe/04-cockroach-debug-unsafe-remove-dead-replicas.png 860w,
/static/d47c7a4b7fe2f296f7b1e5fc76fb888b/64756/04-cockroach-debug-unsafe-remove-dead-replicas.png 1200w,
/static/d47c7a4b7fe2f296f7b1e5fc76fb888b/42cbc/04-cockroach-debug-unsafe-remove-dead-replicas.png 1600w&quot; sizes=&quot;(max-width: 1600px) 100vw, 1600px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/d47c7a4b7fe2f296f7b1e5fc76fb888b/42cbc/04-cockroach-debug-unsafe-remove-dead-replicas.png&quot; alt=&quot;04 cockroach debug unsafe remove dead replicas&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        &lt;a href=&quot;https://github.com/cockroachdb/cockroach/blob/8107342458893bedc8a9353555d148596b7a5e16/pkg/cli/debug.go#L1105&quot;&gt;CockroachDB 소스코드&lt;/a&gt;에서 찾아볼 수 있는 해당 명령어에 대한 무서운 주석
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;나머지 1개의 Range도 당연히 복원을 시도했습니다. Range가 완전히 소실된 것이 아니므로 강제로 특정 Replica를 원본으로 지정하고 다시 복제하는 기능이 있었으면 좋았으련만, 당시만 해도 그런 기능이 없었습니다.&lt;sup&gt;&lt;a name=&quot;ftsrc_4&quot; href=&quot;#ft_4&quot;&gt;[4]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;따라서 살아나지 못한 Range 1개의 복원을 위해 1차 장애와 동일한 방법을 사용하였습니다. 어떤 식으로 복원했는지는 저희 기술 블로그에 있는 &lt;a href=&quot;../bit-level-database-hacking&quot;&gt;CTO가 커리어를 걸고 비트 레벨까지 내려가서 DB를 해킹했던 이야기&lt;/a&gt;, &lt;a href=&quot;../crk-launch-storage-postmortem&quot;&gt;쿠키런: 킹덤 데이터베이스 스토리지 레이어 복원기&lt;/a&gt; 글을 보시면 자세한 내용을 확인하실 수 있습니다. 참고로 해당 Range에 영향받을 수 있는 유저 수는 추측하건대 적으면 500명, 많으면 2천 명 정도로 예상됩니다. 결과적으로 데이터 유실 없이 온전하게 유저 데이터를 복구할 수 있었습니다.&lt;/p&gt;
&lt;h2 id=&quot;데브시스터즈가-얻은-교훈&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8D%B0%EB%B8%8C%EC%8B%9C%EC%8A%A4%ED%84%B0%EC%A6%88%EA%B0%80-%EC%96%BB%EC%9D%80-%EA%B5%90%ED%9B%88&quot; aria-label=&quot;데브시스터즈가 얻은 교훈 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;데브시스터즈가 얻은 교훈&lt;/h2&gt;
&lt;p&gt;AWS 도쿄 리전 데이터센터 장애는 저희 데브시스터즈에 매우 중요한 교훈을 주었습니다. 쿠키런: 킹덤 정도 규모의 게임을 서비스하기 위해서는 멀티 AZ 전략은 선택이 아니라 필수라는 것이죠. 이후 저희는 Locality 설정을 통해 데이터를 여러 지역에 걸쳐 고르게 분산시켰습니다.
낮은 확률이었지만 겪고 나니 보기에 흡족한 Helm 차트를 만들어 관리하고자 했던 마음은 쏙 들어갔습니다. 배포할 AZ마다 values 파일만 다르게 사용하는 한 벌의 차트를 사용하는, 다소 반복적이지만 확실한 방법으로 Locality를 설정하였습니다. 노드를 교체할 때는 Locality를 설정한 노드를 기존 클러스터에 편입시킨 이후 모든 AZ에 안정적으로 Replica가 복제된 것을 확인하고, 이후 Locality가 설정되지 않은 기존 노드들을 퇴역시키는 방법으로 진행했습니다.&lt;/p&gt;
&lt;p&gt;클라우드 인프라를 제공하는 AWS 측에서도 고객인 우리를 위해서 만반의 대비를 해두었을 것입니다. 그래도 세상에는 물리적으로 어쩔 수 없는 요인, 혹은 인적 요인으로 인해서라도 문제가 발생할 수 있습니다. 이번 사건으로 저희 또한 저희의 고객을 위해서, 제삼자의 어떤 무언가에 기대는 것이 아니라 저희가 할 수 있는 최대한의 방비를 해두어야한다는 것을 새삼 깨닫게 되었습니다. 그러한 준비가 없었을 때 영향을 받는 것은, 바로 소중한 우리 고객들의 경험이니까요.&lt;/p&gt;
&lt;h2 id=&quot;글을-마치며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B8%80%EC%9D%84-%EB%A7%88%EC%B9%98%EB%A9%B0&quot; aria-label=&quot;글을 마치며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;글을 마치며&lt;/h2&gt;
&lt;p&gt;2022년 NDC 세션이 공개되고 나서, 몇몇 쿠키런: 킹덤 유저분들이 “서버실 에어컨 고장이라니 공포영화가 따로 없다&quot;고 평해주신 것이 아직도 기억에 남습니다. 공포영화와 현실의 다른 점이라면 영화는 각본대로 흘러가지만, 현실에서는 온갖 좋지 못한 것들을 막기 위해 최대한 대비를 할 수 있다는 점이겠지요. 개인적으로 AWS 도쿄 리전 AZ 장애는 “대비”라는 단어가 어디까지 무게감을 가질 수 있는지 다시금 생각하게 해주었습니다. 긴 글 읽어주셔서 감사드립니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;a name=&quot;ft_1&quot; href=&quot;#ftsrc_1&quot;&gt;[1]&lt;/a&gt;: CockroachDB 내부적으로 사용하는 데이터 저장 단위입니다.&lt;/p&gt;
&lt;p&gt;&lt;a name=&quot;ft_2&quot; href=&quot;#ftsrc_2&quot;&gt;[2]&lt;/a&gt;: 36시간 점검 이후에 DB 용량을 추가적으로 확보하기 위해 총 60대 규모의 클러스터로 확장했었습니다.&lt;/p&gt;
&lt;p&gt;&lt;a name=&quot;ft_3&quot; href=&quot;#ftsrc_3&quot;&gt;[3]&lt;/a&gt;: 이 명령어는 지금은 &lt;a href=&quot;https://github.com/cockroachdb/cockroach/pull/88765&quot;&gt;deprecate&lt;/a&gt; 되고 debug recover 명령어로 대체되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;a name=&quot;ft_4&quot; href=&quot;#ftsrc_4&quot;&gt;[4]&lt;/a&gt;: 새로 추가된 &lt;a href=&quot;https://github.com/cockroachdb/cockroach/issues/71860&quot;&gt;recover&lt;/a&gt; 명령어가 이러한 기능을 지원하기 위해서 개발되었다고 합니다. 잘 동작하는지 저희가 확인해보진 않았고, 앞으로도 그냥 모르고 살았으면 좋겠습니다 😅&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[9가지 프로그래밍 언어로 배우는 개념: 5편 - 동시성 프로그래밍]]></title><description><![CDATA[프로그래밍 언어들을 비교해가며 동시성 프로그래밍을 알아보고 더 나은 코드를 작성하는 방법을 고민해봅니다.]]></description><link>https://tech.devsisters.com/posts/programming-languages-5-concurrent-programming</link><guid isPermaLink="false">https://tech.devsisters.com/posts/programming-languages-5-concurrent-programming</guid><pubDate>Tue, 31 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;프로그래밍-언어-시리즈&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%EC%96%B8%EC%96%B4-%EC%8B%9C%EB%A6%AC%EC%A6%88&quot; aria-label=&quot;프로그래밍 언어 시리즈 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;프로그래밍 언어 시리즈&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;/posts/programming-languages-1-type-theory&quot;&gt;9가지 프로그래밍 언어로 배우는 개념: 1편 - 타입 이론&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/programming-languages-2-polymorphism&quot;&gt;9가지 프로그래밍 언어로 배우는 개념: 2편 - 다형성&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/programming-languages-3-metaprogramming&quot;&gt;9가지 프로그래밍 언어로 배우는 개념: 3편 - 메타프로그래밍&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/programming-languages-4-high-level-languages-and-dynamically-typed-languages&quot;&gt;9가지 프로그래밍 언어로 배우는 개념: 4편 - 하이 레벨 언어와 동적 타입 언어&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;9가지 프로그래밍 언어로 배우는 개념: 5편 - 동시성 프로그래밍&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;서론&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%84%9C%EB%A1%A0&quot; aria-label=&quot;서론 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;서론&lt;/h2&gt;
&lt;p&gt;안녕하세요, 쿠키런 킹덤의 서버팀에 속해있는 지민규입니다. CPU 코어의 성능이 한계점에 가까워지고 보급형 CPU도 4코어 이상인 요즘 시대에 높은 반응성 및 성능을 요구하는 프로그램을 만들려면 동시성 프로그래밍은 필수입니다. 이러한 추세에 많은 프로그래밍 언어들도 각종 동시성 기능을 내장해가고 있습니다. 이 글에서는 각 동시성 기능 간의 관계와 몇몇 언어들의 특이한 동시성 기능들을 소개하려 합니다.&lt;/p&gt;
&lt;p&gt;이전 편들이 하나의 프로그래밍 언어를 깊게 이해하고 있는 분들을 대상으로 쓴 것처럼, 이번 동시성 프로그래밍 편에서는 동시성 프로그래밍에 대해 어느 정도 이해하고 있는 분들을 대상으로 작성했습니다.&lt;/p&gt;
&lt;h2 id=&quot;용어-비교&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9A%A9%EC%96%B4-%EB%B9%84%EA%B5%90&quot; aria-label=&quot;용어 비교 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;용어 비교&lt;/h2&gt;
&lt;p&gt;시작하기에 앞서 먼저 동시성 프로그래밍 용어들을 정리해볼 필요가 있습니다&lt;/p&gt;
&lt;h3 id=&quot;병렬처리parallel-computing&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B3%91%EB%A0%AC%EC%B2%98%EB%A6%ACparallel-computing&quot; aria-label=&quot;병렬처리parallel computing permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;병렬처리(Parallel Computing)&lt;/h3&gt;
&lt;p&gt;병렬처리는 간단히 말해서 여러 명령을 같은 순간에 처리하는 것입니다. 일반적으로 하나의 CPU 코어는 한 번에 하나의 명령을 처리합니다. 만약 하나의 프로그램에서 다중 코어를 이용해 여러 명령을 한 번에 처리한다면 병렬 처리라고 할 수 있습니다. 반드시 CPU 코어를 사용해야만 병렬처리를 할 수 있는 것은 아닙니다. 수백, 수천 개의 코어로 이루어진 그래픽카드(GPU)를 활용하여 병렬처리를 하는 방법도 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;멀티쓰레딩multithreading&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A9%80%ED%8B%B0%EC%93%B0%EB%A0%88%EB%94%A9multithreading&quot; aria-label=&quot;멀티쓰레딩multithreading permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;멀티쓰레딩(Multithreading)&lt;/h3&gt;
&lt;p&gt;운영체제가 제공하는 쓰레드를 이용하여 병렬처리를 하는 기법입니다. 일반적으로 컴퓨터 프로그램은 하나 이상의 프로세스들로 이루어져 있고 프로세스는 하나 이상의 쓰레드로 이루어져 있습니다. 하나의 쓰레드는 로컬 변수들을 저장할 스택 메모리와 실행 중인 명령의 위치 등으로 이루어져 있습니다. 같은 프로세스 내의 쓰레드끼리는 서로 힙 메모리를 공유합니다.&lt;/p&gt;
&lt;h3 id=&quot;동시성-프로그래밍concurrent-programming&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8F%99%EC%8B%9C%EC%84%B1-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8Dconcurrent-programming&quot; aria-label=&quot;동시성 프로그래밍concurrent programming permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;동시성 프로그래밍(Concurrent Programming)&lt;/h3&gt;
&lt;p&gt;동시성 프로그래밍은 기존의 순서대로 명령들이 실행되는 구조에서 벗어나 명령들이 불규칙한 순서대로 실행될 수 있게 해주는 프로그래밍 기법입니다. 멀티쓰레딩은 동시성 프로그래밍의 한 방법이지만, 동시성 프로그래밍이 꼭 병렬처리 및 멀티쓰레딩을 의미하지는 않습니다.&lt;/p&gt;
&lt;h3 id=&quot;비동기-프로그래밍asynchronous-programming&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B9%84%EB%8F%99%EA%B8%B0-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8Dasynchronous-programming&quot; aria-label=&quot;비동기 프로그래밍asynchronous programming permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;비동기 프로그래밍(Asynchronous Programming)&lt;/h3&gt;
&lt;p&gt;비동기 프로그래밍은 Future, Promise 및 Async-Await와 같은 추상화된 기능들을 이용하여 동시성 프로그래밍을 하는 방법입니다. 마찬가지로 비동기 프로그래밍을 사용한다고 해서 꼭 병렬처리 및 멀티쓰레딩인 것은 아닙니다.&lt;/p&gt;
&lt;h2 id=&quot;쓰레드&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%93%B0%EB%A0%88%EB%93%9C&quot; aria-label=&quot;쓰레드 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;쓰레드&lt;/h2&gt;
&lt;p&gt;**쓰레드(Thread)**가 명령 흐름을 관리하는 기본 단위인 만큼 가장 대표적인 병렬처리 방식입니다. 쓰레드는 운영체제가 제공하기 때문에 운영체제마다 사용해야 하는 라이브러리, 쓰레드 생성 방식, 세부적인 기능들이 미묘하게 다릅니다. 다행히도, 대부분의 프로그래밍 언어는 운영체제를 신경쓰지 않아도 되도록 추상화한 쓰레딩 라이브러리를 제공합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;csharp&quot;&gt;&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code class=&quot;language-csharp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C# 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 쓰레드가 돌릴 함수&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;token return-type class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;SayHi&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;string&lt;/span&gt;&lt;/span&gt; message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  Console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;WriteLine&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Hi, &quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 쓰레드 생성&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;Thread&lt;/span&gt; thread &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token constructor-invocation class-name&quot;&gt;Thread&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;SayHi&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Mango&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 쓰레드 시작&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// &quot;Hi, Mango&quot;를 출력한다&lt;/span&gt;
thread&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Start&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 쓰레드 종료 기다리기&lt;/span&gt;
thread&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;쓰레드를 생성할 때 운영체제와 소통하고 메모리를 할당해야 하므로 시간과 메모리 리소스가 들어갑니다. 쓰레드 개수가 너무 많아져도 문제인데, 운영체제가 관리해야 하는 쓰레드의 수가 늘어나고 코어를 더 많은 쓰레드들이 나눠서 사용해야 하며, CPU 코어를 사용하는 쓰레드가 교체될 때 레지스터를 교체하는 &lt;strong&gt;Context Switching&lt;/strong&gt;이 더 자주 발생하여 성능도 하락하게 됩니다.&lt;/p&gt;
&lt;h3 id=&quot;쓰레드-풀&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%93%B0%EB%A0%88%EB%93%9C-%ED%92%80&quot; aria-label=&quot;쓰레드 풀 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;쓰레드 풀&lt;/h3&gt;
&lt;p&gt;과도하게 잦은 쓰레드 생성과 Context Switching을 피하는 방법 중 하나는 **쓰레드 풀(Thread Pool)**을 사용하는 것입니다. 쓰레드가 필요할 때마다 매번 생성하는 것이 아닌, 쓰레드 풀에 저장된 쓰레드를 할당받아 사용하고 쓰레드 사용이 끝났을 때 다시 쓰레드 풀로 반환하는 방식입니다. 이때 쓰레드 풀로 넘겨진 함수를 일반적으로 **태스크(Task)**라고 부릅니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;csharp&quot;&gt;&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code class=&quot;language-csharp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C# 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 기본 제공되는 쓰레드 풀, SayHi 함수를 실행하는 태스크를 전달한다.&lt;/span&gt;
ThreadPool&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;QueueUserWorkItem&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;_&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;SayHi&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Mango&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;다만 쓰레드 풀이 장점만 있는 것은 아닙니다. 리소스만 충분하다면 즉각적으로 생성되고 실행되는 쓰레드와 달리, 쓰레드 풀은 남은 쓰레드가 없다면 실행 중인 태스크 중 하나가 끝나 쓰레드를 반환할 때까지 시작하지 않습니다. 그렇기 때문에 무거운 태스크들로 쓰레드들을 소진시켰다면 다음 태스크는 아무리 가볍더라도 무거운 태스크 중 하나가 끝날 때까지 시작하지 않습니다. 이로 인해 반응성이 떨어집니다.&lt;/p&gt;
&lt;p&gt;태스크가 무거워지는 이유는 크게 2가지로 나눌 수 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;파일 읽기/쓰기, 네트워크 송신 등 I/O 작업이 포함되어있어서&lt;/li&gt;
&lt;li&gt;순수하게 CPU 연산이 많아서&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;1번 케이스로 인해 쓰레드 풀의 반응성이 떨어지는 경우는 CPU 성능의 문제가 아닙니다. I/O 작업은 CPU가 연산을 하느라 느려지는 것이 아니라 하드디스크/SSD와 같은 하드웨어의 작업을 기다리고 있거나 네트워크 응답을 기다리고 있는 것이기 때문입니다. 이 경우, 운영체제에서 쓰레드를 대기 상태로 설정하여 CPU를 다른 쓰레드에게 양보하기 때문에 쓰레드 개수가 늘어나더라도 Context Switching 횟수가 크게 늘지 않아 CPU 성능에 큰 문제가 발생하지 않습니다. 그렇기 때문에 I/O 작업을 처리하는 쓰레드 풀은 남은 쓰레드가 없으면 새 쓰레드를 만들어 쓰레드 풀에 포함시키고 오랫동안 사용하지 않은 쓰레드를 자동으로 제거하는 방식으로 쓰레드 개수를 가변적으로 운용하여 반응성을 높이는 것이 일반적입니다.&lt;/p&gt;
&lt;p&gt;2번 케이스는 CPU 성능의 문제입니다. 일반적으로 CPU 작업을 위한 쓰레드 풀은 CPU 코어의 개수에 비례해서 쓰레드 개수를 고정으로 운용하는데, 만약 더 많은 쓰레드를 생성하면 Context Switching의 횟수가 증가하여 전체적인 성능 하락이 발생합니다. 이 경우에는 태스크 자체를 더 작은 단위로 쪼개서 다른 태스크가 돌아갈 기회를 주는 방법 밖에는 없습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;csharp&quot;&gt;&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code class=&quot;language-csharp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C# 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 기존 태스크&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;token return-type class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;OriginalTask&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;/* 매우 무거운 CPU 작업 1 */&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;DoTheDishes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;/* 매우 무거운 CPU 작업 2 */&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;DoTheLaundry&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 쪼개진 태스크들&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;token return-type class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;CpuTask1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;DoTheDishes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  ThreadPool&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;QueueUserWorkItem&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;_&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;CpuTask2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;token return-type class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;CpuTask2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;DoTheLaundry&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 I/O 작업이 있는 태스크와 CPU 작업 위주인 태스크는 서로 다른 쓰레드 풀을 사용해야 하며 태스크를 쪼개야 하는 경우도 있다는 것을 보았습니다. 만약 한 태스크가 CPU 작업과 I/O 작업을 함께 할 경우 각 작업은 서로 다른 쓰레드 풀에 할당되어야 하므로 계속해서 태스크를 쪼개야 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;csharp&quot;&gt;&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code class=&quot;language-csharp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C# 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 기존 태스크&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;token return-type class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;OriginalTask&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;/* 매우 무거운 CPU 작업 1 */&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;DoTheDishes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;/* 매우 무거운 I/O 작업 */&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;MakeDoctorsAppointment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;/* 매우 무거운 CPU 작업 2 */&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;DoTheLaundry&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 쪼개진 태스크들&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;token return-type class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;CpuTask1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;DoTheDishes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  ioThreadPool&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;QueueUserWorkItem&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;_&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;IoTask&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;token return-type class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;IoTask&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;MakeDoctorsAppointment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  cpuThreadPool&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;QueueUserWorkItem&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;_&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;CpuTask2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;token return-type class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;CpuTask2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;DoTheLaundry&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;위 코드에서도 보이듯이, 태스크를 직접 쪼개는 건 비직관적입니다. 이러한 비직관성을 회피하는 방법 중 하나는 바로 경량쓰레드를 사용하는 것입니다.&lt;/p&gt;
&lt;h3 id=&quot;경량쓰레드&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%BD%EB%9F%89%EC%93%B0%EB%A0%88%EB%93%9C&quot; aria-label=&quot;경량쓰레드 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;경량쓰레드&lt;/h3&gt;
&lt;p&gt;**경량쓰레드(Light-weight Thread)**는 운영체제가 아닌 프로그램 레벨에서 구현된 쓰레드입니다. 생성하고 제거할 때 운영체제와 소통할 필요가 없고 리소스의 사용량도 최적화하여 훨씬 가볍기 때문에 경량쓰레드라고 부릅니다. 경량쓰레드를 언어 차원에서 제공하는 대표적인 언어는 Go입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;go&quot;&gt;&lt;pre class=&quot;language-go&quot;&gt;&lt;code class=&quot;language-go&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Go 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;/* 매우 무거운 CPU 작업 1 */&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;doTheDishes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;/* 매우 무거운 I/O 작업 */&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;makeDoctorsAppointment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;/* 매우 무거운 CPU 작업 2 */&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;doTheLaundry&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 경량쓰레드 위에서 실행&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;go&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;task&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;경량쓰레드는 쓰레드 풀과 깊은 연관성이 있습니다. 사실 경량쓰레드는 쓰레드 풀 위에서 작동하고, 프로그래밍 언어가 태스크를 대신 쪼개주는 것이기 때문입니다.&lt;/p&gt;
&lt;p&gt;위에서 보이는 task 함수는 어떤 면에서는 하나의 태스크가 아니라 세 개로 나눠진 태스크라고도 볼 수 있습니다. 나눠진 지점마다 다른 경량쓰레드가 작동할 기회를 주는 것입니다. 쓰레드가 명령의 흐름을 추적한다면 경량쓰레드는 태스크의 흐름을 추적한다고 할 수 있습니다. 그렇기 때문에 Context Switching의 비용 또한 운영체제의 쓰레드에 비하면 훨씬 낮습니다.&lt;/p&gt;
&lt;p&gt;경량쓰레드를 사용하면 태스크를 어느 쓰레드 풀로 넘겨야 하는지, 어떻게 쪼갤지, 쓰레드가 충분한지에 대한 고민을 크게 덜 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;비동기-프로그래밍&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B9%84%EB%8F%99%EA%B8%B0-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D&quot; aria-label=&quot;비동기 프로그래밍 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;비동기 프로그래밍&lt;/h2&gt;
&lt;p&gt;비동기 프로그래밍은 태스크 쪼개기 문제를 해결하는 방법 중 하나입니다. 비동기 프로그래밍은 태스크 쪼개기를 최대한 자연스럽게 만들어 코드의 직관성을 살립니다.&lt;/p&gt;
&lt;p&gt;다만 비동기 프로그래밍이 꼭 멀티쓰레딩을 동반하지 않습니다. 태스크를 별도의 쓰레드나 쓰레드 풀로 넘긴다면 멀티쓰레딩을 활용하는 비동기 프로그래밍이고, 하나의 쓰레드가 모두 처리한다면 병렬성이 없는 비동기 프로그래밍입니다.&lt;/p&gt;
&lt;h3 id=&quot;promise와-future&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#promise%EC%99%80-future&quot; aria-label=&quot;promise와 future permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Promise와 Future&lt;/h3&gt;
&lt;p&gt;비동기 프로그래밍을 배울 때 가장 먼저 &lt;strong&gt;Promise&lt;/strong&gt;와 &lt;strong&gt;Future&lt;/strong&gt;에 대해서 알아보아야 합니다. 쓰레드를 돌릴 때 대부분의 경우 실행 결과도 필요합니다. 이때 사용하는 것은 Promise와 Future입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 피보나치 함수&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fibonacci&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt; n&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;n &lt;span class=&quot;token operator&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; n&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fibonacci&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;n &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fibonacci&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;n &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 피보나치 계산 결과를 promise로 전달하는 태스크&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;runFibonacci&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt; n&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; promise&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; fibonacciPromise&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fibonacci&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;n&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  fibonacciPromise&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set_value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 결과 값을 받아줄 promise 생성&lt;/span&gt;
promise&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; fibonacciPromise&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 결과 값을 조회할 수 있는 future 생성&lt;/span&gt;
future&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; fibonacciFuture &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fibonacciPromise&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get_future&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 쓰레드 생성 후 태스크 전달&lt;/span&gt;
thread t &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;thread&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;runFibonacci&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;50&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;move&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fibonacciPromise&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// future에 값이 도달할 때까지 기다리기&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fibonacciFuture&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Promise는 값을 받을 수 있고 Future는 값을 조회할 수 있습니다. 위에서 보이듯이 C++에서는 Promise를 먼저 만든 뒤 Promise에 대응되는 Future를 가져옵니다. &lt;code&gt;promise&lt;/code&gt;에 &lt;code&gt;set_value&lt;/code&gt; 함수가 호출되었을 때 &lt;code&gt;future&lt;/code&gt;의 &lt;code&gt;wait&lt;/code&gt;가 끝나는 것입니다.&lt;/p&gt;
&lt;p&gt;물론 이렇게 직접 Promise를 다뤄야 하는 경우는 많지 않습니다. 일반적으로는 Promise는 라이브러리 내부에서 만들어지고, Future만 받아서 처리하는 경우가 많습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fibonacci&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt; n&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;n &lt;span class=&quot;token operator&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; n&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fibonacci&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;n &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fibonacci&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;n &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 새 쓰레드를 만들고 그 위에 태스크를 굴려서 결과를 future로 받는다.&lt;/span&gt;
future&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; fibonacciFuture &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;async&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fibonacci&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;50&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// future에 값이 도달할 때까지 기다리기&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fibonacciFuture&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Promise를 직접 다뤘을 때는 &lt;code&gt;set_value&lt;/code&gt;를 호출하는 것을 잊어버리면 &lt;code&gt;wait&lt;/code&gt; 함수가 영원히 끝나지 않겠지만 &lt;code&gt;async&lt;/code&gt; 함수를 활용하여 간접적으로 사용하면 이러한 위험성을 피할 수 있습니다. &lt;code&gt;async&lt;/code&gt;를 통해 만들어낸 Future와 직접 Promise로부터 만들어낸 Future는 안전성 측면에서 크게 다릅니다.&lt;/p&gt;
&lt;p&gt;그렇기 때문에 많은 언어는 주인 없는 Future가 존재하지 않도록 Future를 만들 때부터 태스크를 묶고 실행하는 경우가 많습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Java 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 태스크를 기본 쓰레드 풀에 실행시켜 결과를 future로 받는다.&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;CompletableFuture&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Long&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; fibonacciFuture &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  &lt;span class=&quot;token class-name&quot;&gt;CompletableFuture&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;supplyAsync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fibonacci&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;50&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// future에 값이 도달할 때까지 기다리기&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;Long&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fibonacciFuture&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;C#에서는 아예 Future를 &lt;code&gt;Task&amp;#x3C;T&gt;&lt;/code&gt;라고 부르고 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;csharp&quot;&gt;&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code class=&quot;language-csharp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C# 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 태스크를 기본 쓰레드 풀에 실행시켜 결과를 future로 받는다.&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;Task&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; fiboncciFuture &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  Task&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Run&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Fibonacci&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;50&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// future에 값이 도달할 때까지 기다리기&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt;&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fiboncciFuture&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Result&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;재밌는 것은 Javascript/Typescript에서는 이런 Future들을 &lt;code&gt;Promise&lt;/code&gt;라고 부른다는 것입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Typescript&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 태스크를 실행시켜 결과를 future로 받는다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; fiboncciFuture&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;resolve&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fibonacci&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;50&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;태스크가 묶인 Future의 특징 중 하나는 태스크 연계가 가능하다는 것입니다. Future의 결과에 새로운 태스크를 묶어서 또 다른 Future를 만드는 것입니다. 이것 잘 활용하면 태스크가 자연스럽게 쪼개지게 되어 반응성 문제를 어느 정도 회피할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Typescript 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Future들을 출력하는 함수들&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getHired&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Job&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;earnMoney&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;job&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Job&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Money&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;buyHouse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;money&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Money&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;House&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Promise의 then 메소드를 이용해서 Future들을 합친다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; houseFuture&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;House&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getHired&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;job&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;earnMoney&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;job&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;money&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;buyHouse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;money&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이러한 방식으로 Future 합치는 것에 큰 단점이 있습니다. 각 태스크의 의존 관계가 복잡해지면 Future 중첩이 복잡해져 가독성이 떨어집니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Typescript 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; future&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Job&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Money&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; House&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getHired&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;job&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;earnMoney&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;job&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;money&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;buyHouse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;money&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;house&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;job&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; money&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; house&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이러한 중첩 문제를 해결할 방법 중 하나는 Async-Await 기능을 이용하는 것입니다.&lt;/p&gt;
&lt;h3 id=&quot;async-await&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#async-await&quot; aria-label=&quot;async await permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Async-Await&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Async-Await&lt;/strong&gt;은 Future의 중첩으로 인한 가독성 하락 문제를 해결해줍니다. Typescript의 Async-Await 기능은 &lt;code&gt;async&lt;/code&gt; 키워드와 &lt;code&gt;await&lt;/code&gt; 키워드를 사용합니다. 먼저 &lt;code&gt;async&lt;/code&gt;에 대해서 배워봅시다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Typescript 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getHired&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Job&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;earnMoney&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;job&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Job&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Money&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;buyHouse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;money&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Money&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;House&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;먼저 &lt;code&gt;async&lt;/code&gt; 함수는 반드시 &lt;code&gt;Promise&lt;/code&gt;를 출력해야 한다는 제약 조건이 있습니다. Typescript에서는 출력 타입에 &lt;code&gt;Promise&amp;#x3C;Job&gt;&lt;/code&gt;과 같은 형식으로 적지만 &lt;code&gt;Promise&lt;/code&gt; 부분을 생략하는 프로그래밍 언어도 몇몇 존재합니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;async&lt;/code&gt; 함수를 사용하면 &lt;code&gt;await&lt;/code&gt; 키워드를 사용할 수 있게 해줍니다. &lt;code&gt;await&lt;/code&gt; 키워드를 활용하면 중첩 문제를 해결할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Typescript 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;collectAllFutures&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Job&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Money&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; House&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; job&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Job &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getHired&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; money&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Money &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;earnMoney&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;job&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; house&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; House &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;buyHouse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;money&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;job&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; money&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; house&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 위 코드는 아래와 동일&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// fucntion runAll(): Promise&amp;lt;[Job, Money, House]&gt; {&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//   getHired()&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//     .then(job =&gt;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//       earnMoney(job)&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//         .then(money =&gt;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//           buyHouse(money)&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//             .then(house =&gt;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//               [job, money, house]&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//             )&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//         )&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//     )&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// }&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Async-Await는 완전히 새로 만들어진 기능이 아닌 &lt;code&gt;Promise&lt;/code&gt;의 &lt;code&gt;then&lt;/code&gt;을 이용해서 만들어진 것입니다. 물론 &lt;code&gt;then&lt;/code&gt;으로 직접 중첩하는 것과는 세세한 차이점이 존재하지만 조금 넓게 보면 일종의 **문법적 설탕(Syntatic Sugar)**입니다. 문법적 설탕은 기존에 존재하는 기능에 새로운 문법을 추가하여 가독성을 높인 언어 기능을 뜻합니다.&lt;/p&gt;
&lt;p&gt;그렇기 때문에 어떻게 보면 Async-Await는 눈속임입니다. 위 &lt;code&gt;runAll&lt;/code&gt;의 3개의 함수 호출은 전부 다른 태스크에 속해있습니다. &lt;code&gt;await&lt;/code&gt;를 하는 지점마다 다른 태스크가 실행될 기회를 주는 것이고 사실 한 함수의 몸체 내에서 모두 호출하는 것이 아닙니다. 이런 면에서 경량쓰레드랑 어느 정도 비슷한 점이 존재한다고 볼 수 있습니다. 경량쓰레드는 문법을 추가하지 않고 이러한 문제를 해결한다면, Async-Await는 새로운 키워드와 문법을 추가하여 해결합니다.&lt;/p&gt;
&lt;p&gt;Kotlin에서는 Async-Await 대신 **코루틴(Coroutine)**을 사용하는데, &lt;code&gt;yield&lt;/code&gt;등 몇 가지 특수한 기능들이 제공되는 것을 제외하면 Async-Await와 동일하기 때문에 이번 글에서는 다루지 않습니다.&lt;/p&gt;
&lt;h3 id=&quot;io모나드&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#io%EB%AA%A8%EB%82%98%EB%93%9C&quot; aria-label=&quot;io모나드 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;IO모나드&lt;/h3&gt;
&lt;p&gt;무시무시한 단어가 등장했습니다. **모나드(Monad)**입니다. 모나드는 함수형 프로그래밍 패러다임의 개념 중 가장 이해하기 어려운 개념으로 악명 높습니다. 이 글은 동시성 프로그래밍에 관한 글이기 때문에 분량상 모나드에 대해서 전부 설명할 수는 없지만, IO모나드를 대략적으로 이해하는데 필요한 수준까지 설명합니다.&lt;/p&gt;
&lt;p&gt;먼저 IO모나드에 해당하는 Scala의 &lt;code&gt;Future[T]&lt;/code&gt;를 살펴봅시다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Scala 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 대략적인 Future 인터페이스&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;trait&lt;/span&gt; Future&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;T&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; flatMap&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;U&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;f&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; T &lt;span class=&quot;token keyword&quot;&gt;=&gt;&lt;/span&gt; Future&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;U&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Future&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;U&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 대략적인 Future 정적 객체&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;object&lt;/span&gt; Future &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Future의 정적 함수&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; successful&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;T&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;result&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; T&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Future&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;T&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;어떠한 타입이 모나드가 되려면 정확히 2개의 함수가 필요합니다. 대상 타입이 &lt;code&gt;F&lt;/code&gt;이고. 타입 파라미터 &lt;code&gt;T&lt;/code&gt;를 받는다면 &lt;code&gt;T ⇒ F[U]&lt;/code&gt; 타입의 함수를 입력받아 &lt;code&gt;F[U]&lt;/code&gt;를 출력할 수 있는 메소드와 &lt;code&gt;T&lt;/code&gt;를 입력받아 &lt;code&gt;F[T]&lt;/code&gt;를 출력할 수 있는 정적 함수가 필요합니다. &lt;code&gt;Future&lt;/code&gt;의 경우 &lt;code&gt;Future[T].flatMap&lt;/code&gt; 메소드가 전자에 해당하고 &lt;code&gt;Future.sucessful&lt;/code&gt; 함수가 후자에 해당합니다.&lt;/p&gt;
&lt;p&gt;이 두 함수를 가진 타입들을 가졌다면 해당 타입을 모나드라고 할 수 있습니다. 모든 모나드는 Typescript의 &lt;code&gt;Promise&lt;/code&gt;와 같이 중첩된 형태로 함수를 호출하는 상황이 발생할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Typescript 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Typescript에서는 이렇게 중첩된다&lt;/span&gt;
fucntion &lt;span class=&quot;token function&quot;&gt;collectAllFutures&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Job&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Money&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;House&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;getHired&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;job &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;earnMoney&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;job&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;money &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
          &lt;span class=&quot;token function&quot;&gt;buyHouse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;money&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;house &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
              &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;job&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; money&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; house&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Scala 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Typescript의 Promise가 then을 사용한다면&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Scala의 Future는 flatMap을 사용한다&lt;/span&gt;
def &lt;span class=&quot;token function&quot;&gt;collectAllFutures&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Future&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Job&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Money&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;House&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;getHired&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;flatMap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;job &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;earnMoney&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;job&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;flatMap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;money &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
          &lt;span class=&quot;token function&quot;&gt;buyHouse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;money&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
              &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;flatMap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;house &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
                &lt;span class=&quot;token class-name&quot;&gt;Future&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;successful&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;job&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; money&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; house&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
              &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
         &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;여러 Future의 결과를 모아서 출력하는 경우 Typescript의 &lt;code&gt;Promise&lt;/code&gt;가 그러했듯 Scala의 &lt;code&gt;Future&lt;/code&gt;도 그런 문제를 겪게 됩니다. Scala의 Nullable 타입에 해당하는 &lt;code&gt;Option&lt;/code&gt;도 모나드이기 때문에 마찬가지의 문제를 겪을 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Scala 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;trait&lt;/span&gt; Option&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;T&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 현재 값이 null일 때는 null을 출력한다&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 현재 값이 null이 아닐 때는 f를 실행한 출력 값을 출력한다&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; flatMap&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;U&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;f&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; T &lt;span class=&quot;token keyword&quot;&gt;=&gt;&lt;/span&gt; Option&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;U&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Option&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;U&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;object&lt;/span&gt; Option &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; some&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;T&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; T&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Option&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;T&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 마찬가지로 중첩된 형태로 함수를 호출한다&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 여기서 각각의 getXXX 함수는 Option을 출력한다고 가정한다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; collectAllOptions&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Option&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Job&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Money&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; House&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  getJob&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;flatMap&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;job &lt;span class=&quot;token keyword&quot;&gt;=&gt;&lt;/span&gt;
      getMoney&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;job&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;flatMap&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;money &lt;span class=&quot;token keyword&quot;&gt;=&gt;&lt;/span&gt;
          getHouse&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;money&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;flatMap&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;house &lt;span class=&quot;token keyword&quot;&gt;=&gt;&lt;/span&gt;
              Option&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;some&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;job&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; money&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; house&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;함수형 프로그래밍에서 모나드가 널리 쓰이는 만큼 함수형 프로그래밍에 굉장히 고전적인 문제라서 모나드를 위한 문법적 설탕이 이미 존재합니다. 이를 Scala에서는 &lt;strong&gt;For-Comprehension&lt;/strong&gt;이라고 부릅니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Scala 코드&lt;/span&gt;
def &lt;span class=&quot;token function&quot;&gt;collectAllFutures&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Future&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Job&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Money&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; House&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    job   &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getHired&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    money &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;earnMoney&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;job&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    house &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;houseMoney&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;job&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;job&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; money&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; house&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

def &lt;span class=&quot;token function&quot;&gt;collectAllOptions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Option&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Job&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Money&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; House&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    job   &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getJob&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    money &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getMoney&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;job&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    house &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getHouse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;job&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;job&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; money&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; house&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Typescript 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 비교를 위해 가져온 Typescript의 Async-Await 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;collectAllFutures&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Job&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Money&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; House&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; job &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getHired&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; money &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;earnMoney&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;job&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; house &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;buyHouse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;money&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;job&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; money&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; house&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;뭔가 익숙하지 않으십니까? Async-Await와 상당 부분 비슷합니다. 차이점은 Async-Await는 비동기 처리 맥락에서만 사용할 수 있는 문법적 설탕이지만, For-Comprehension은 모나드들에 전반적으로 적용할 수 있는 문법적 설탕입니다. 제 동료가 집필한 &lt;a href=&quot;/posts/scala-for-game-server-development-kr&quot;&gt;게임 서버 개발에 스칼라 사용하기&lt;/a&gt;에서 비동기 처리가 전혀 없는 비즈니스 로직에 For-Comprehension을 사용한 것을 볼 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;메모리-공유&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A9%94%EB%AA%A8%EB%A6%AC-%EA%B3%B5%EC%9C%A0&quot; aria-label=&quot;메모리 공유 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;메모리 공유&lt;/h2&gt;
&lt;p&gt;동시성 프로그래밍을 할 때 생겨나는 본격적 고민은 메모리 공유에서 시작됩니다. 동시성 프로그래밍은 기존 순서대로 진행되는 코드의 형태에서 벗어나 명령들이 불규칙한 순서로 실행된다고 했었습니다.&lt;/p&gt;
&lt;p&gt;명령 처리 순서가 불규칙해지면 어떠한 문제가 발생할까요? 예를 들어 전역 변수에 1씩 100,000번 더하는 함수가 있다고 합시다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; globalValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add100_000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    globalValue&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 함수를 2개의 쓰레드에 돌려서 200,000번 더해봅시다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 함수를 2개에 쓰레드에 나눠서 호출한다&lt;/span&gt;
thread thread1 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;thread&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;add100_000&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
thread thread2 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;thread&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;add100_000&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

thread1&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
thread2&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 과연 무엇이 출력될까?&lt;/span&gt;
cout &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; globalValue &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; endl&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;같은 전역 변수에 200,000번 더했으니 200,000이 될 것 같지만, 실제로는 100,000과 200,000 사이의 숫자가 출력됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 위 프로그램을 10번 실행한 결과&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 참고: 실행 환경에 따라 결과가 크게 다를 수 있음&lt;/span&gt;
&lt;span class=&quot;token number&quot;&gt;108725&lt;/span&gt;
&lt;span class=&quot;token number&quot;&gt;104778&lt;/span&gt;
&lt;span class=&quot;token number&quot;&gt;102279&lt;/span&gt;
&lt;span class=&quot;token number&quot;&gt;103750&lt;/span&gt;
&lt;span class=&quot;token number&quot;&gt;100470&lt;/span&gt;
&lt;span class=&quot;token number&quot;&gt;101885&lt;/span&gt;
&lt;span class=&quot;token number&quot;&gt;102690&lt;/span&gt;
&lt;span class=&quot;token number&quot;&gt;102263&lt;/span&gt;
&lt;span class=&quot;token number&quot;&gt;102678&lt;/span&gt;
&lt;span class=&quot;token number&quot;&gt;102362&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;어째서 이런 일이 발생하는 것일까요? 일단 &lt;code&gt;add100_000&lt;/code&gt; 함수를 확인해봅시다. CPU 명령어 관점에서 &lt;code&gt;globalValue++&lt;/code&gt;는 3개의 명령으로 이루어져 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; globalValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add100_000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// globalValue++; 는 다음 세 명령으로 변환된다.&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; oldValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; globalValue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 1번&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; newValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; oldValue &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 2번&lt;/span&gt;
    globalValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; newValue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 3번&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 3개의 명령을 2개의 쓰레드, A 쓰레드와 B 쓰레드에서 동시에 실행한다고 생각해봅시다. 쓰레드는 운영체제가 관리하므로 언제 명령을 수행하고 언제 CPU 코어를 양보할지 모릅니다. 만약 A 쓰레드에서 1, 2, 3번 명령을 완료한 뒤 B 쓰레드에서 1, 2, 3번 명령을 수행하면 결과는 예상대로 이루어집니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; globalValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 쓰레드 A에서 명령 1, 2, 3번 수행&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 전역 변수로부터 0을 읽는다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; threadA_oldValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; globalValue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 0에 1을 더해 1이라는 계산 결과가 나온다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; threadA_newValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; threadA_oldValue &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 1이라는 결과를 전역 변수에 대입한다&lt;/span&gt;
globalValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; threadA_newValue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 쓰레드 B에서 명령 1, 2, 3번 수행&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 전역 변수로부터 1을 읽는다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; threadB_oldValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; globalValue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 1에 1을 더해 2라는 계산 결과가 나온다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; threadB_newValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; threadB_oldValue &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 2이라는 결과를 전역 변수에 대입한다&lt;/span&gt;
globalValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; threadB_newValue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 2가 출력된다&lt;/span&gt;
cout &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; globalValue &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; endl&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;하지만 만약 A 쓰레드에서 1, 2번째 명령을 수행한 뒤 3번째 명령을 수행하기 전에 B 쓰레드가 3개의 명령을 수행하면 어떻게 될까요?&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; globalValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 쓰레드 A에서 명령 1, 2번 수행&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 전역 변수로부터 0을 읽는다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; threadA_oldValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; globalValue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 0에 1을 더해 1이라는 계산 결과가 나온다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; threadA_newValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; threadA_oldValue &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 쓰레드 B에서 명령 1, 2, 3번 수행&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 전역 변수로부터 0을 읽는다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; threadB_oldValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; globalValue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 0에 1을 더해 1라는 계산 결과가 나온다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; threadB_newValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; threadB_oldValue &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 1이라는 결과를 전역 변수에 대입한다&lt;/span&gt;
globalValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; threadB_newValue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 쓰레드 A에서 명령 3번 수행&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 1이라는 쓰레드A의 결과를 전역 변수에 대입한다&lt;/span&gt;
globalValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; threadA_newValue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 1이 출력된다&lt;/span&gt;
cout &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; globalValue &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; endl&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;아직 A 쓰레드에서 1이라는 값을 대입하지 않았기 때문에 B 쓰레드에서도 1이라는 결과가 도출됩니다. 1이 아닌 다른 결과가 나왔더라도 어차피 A 쓰레드의 3번 명령에서 1로 덮어씌워질 것입니다.&lt;/p&gt;
&lt;p&gt;이렇게 똑같은 코드임에도 명령의 실행 순서에 따라 결과가 달라지는 것을 **경쟁 상태(Race Condition)**라고 합니다. 두 쓰레드가 경쟁하여 어느것이 먼저 수행되는지에 따라 결과가 달라지는 것이죠.&lt;/p&gt;
&lt;p&gt;위 예시에서는 쓰레드간 작업 전환이 일어날 수 있었기 때문에 경쟁 상태가 발생합니다. 만약 병렬성이 없는, 싱글쓰레드 환경이였다면 발생하지 않습니다. 싱글쓰레드 환경에서는 반드시 태스크 단위로 실행되기 때문에 A 쓰레드의 1, 2, 3번 명령 사이에 B 쓰레드의 1, 2, 3번 명령이 끼어들 수 없습니다. 대신 연산 및 대입이 여러 태스크에 걸쳐서 일어난다면 경쟁 상태가 발생할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Typescript 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add100_000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;int i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; oldValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; globalValue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; newValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; oldValue &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fireGlobalValueChangeEvent&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 태스크를 나누는 지점&lt;/span&gt;
    globalValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; newValue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;compare-and-swap&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#compare-and-swap&quot; aria-label=&quot;compare and swap permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Compare and Swap&lt;/h3&gt;
&lt;p&gt;현대 프로그래밍에서 경쟁 상태 해결 방법은 &lt;strong&gt;Compare and Swap&lt;/strong&gt;에서 시작합니다. Compare and Swap은 계산한 뒤 변수에 대입하여 확정하기 전에 변수의 값을 한 번 더 확인해서 기존 값과 같은지 확인하고 나서 대입합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 변경할 변수와, 계산에 사용했었던 변수의 값, 그리고 새 값을 넘긴다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;bool&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;compareAndSwap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; target&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; oldValue&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; newValue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 변경할 변수의 값을 가져온다&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; targetValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;target&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 변경할 변수의 값이 처음 계산에 사용했던 값과 같다면 변경한 뒤 성공으로 출력한다&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;targetValue &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; oldValue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;target &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; newValue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 계산한 사이에 값이 바뀌었다면 실패로 출력한다.&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;만약 계산하는 데 처음 사용했던 값과 현재 변수의 값이 다르다면 중간에 다른 태스크가 값을 변경한 것이기 때문에 다시 처음부터 계산해야 합니다. 만약 계산에 사용한 값과 현재 변수 값이 같다면 값이 변경되지 않았다고 가정하고 진행해도 되는 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;compareAndSwap&lt;/code&gt; 함수도 여러 명령으로 이루어져 있기 때문에 결국 경쟁 상태에 영향받지 않을까 생각될 수 있는데, 대부분의 현대 CPU에서는 &lt;code&gt;compareAndSwap&lt;/code&gt;이 하나의 CPU 명령으로써 제공되기 때문에 이러한 문제에서 자유롭습니다. &lt;code&gt;compareAndSwap&lt;/code&gt; 실행 도중 다른 쓰레드가 &lt;code&gt;target&lt;/code&gt;의 값을 변경할 일은 없다는 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;compareAndSwap&lt;/code&gt; 명령을 활용하여 &lt;code&gt;add100_000&lt;/code&gt;를 다시 구현한 것은 다음과 같습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add100_000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// compareAndSwap이 실패하면 다시 시도한다&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; oldValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; globalValue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; newValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; oldValue &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// globalValue = newValue; 대신 compareAndSwap을 활용한다&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;compareAndSwap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;globalValue&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; oldValue&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; newValue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이런 경우에는 A 쓰레드에서 1, 2번 명령을 수행하고 B 쓰레드에서 1, 2, 3번 명령을 수행했을 경우 A 쓰레드의 3번 명령은 실패하게 되어 다시 1, 2, 3번 명령을 재시도합니다. 이렇게 함수나 타입에서 경쟁 상태를 제거했다면 &lt;strong&gt;Thread-safe&lt;/strong&gt;하다고 표현합니다.&lt;/p&gt;
&lt;p&gt;Compare and Swap이 가진 단점이 두 가지 있는데, 첫 번째로는 재시도를 기반으로 사용하기 때문에 성능이 조금 떨어질 수 있다는 것이며, 두 번째로는 재시도할 명령들에 **부수 효과(Side-effect)**이 있으면 의도되지 않은 결과가 발생할 수 있습니다.&lt;/p&gt;
&lt;p&gt;부수 효과란 쉽게 말해 함수 외부에 있는 환경에 영향을 끼치는 것입니다, 대표적인 부수 효과로는 네트워크 통신, 콘솔 출력이 있습니다. 만약 위의 &lt;code&gt;do-while&lt;/code&gt;에 콘솔 출력이 있다면 &lt;code&gt;compareAndSwap&lt;/code&gt;을 실패할 때마다 추가로 콘솔 출력을 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; oldValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; globalValue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; newValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; oldValue &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 부수 효과! compareAndSwap이 실패하면 여러번 출력함!&lt;/span&gt;
  cout &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Changing globalValue to &quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; newValue &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; endl&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;compareAndSwap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;globalValue&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; oldValue&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; newValue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;atomic&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#atomic&quot; aria-label=&quot;atomic permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Atomic&lt;/h3&gt;
&lt;p&gt;변수를 수정하는 작업에 Compare and Swap을 사용하는 것을 일반화한 것이 &lt;strong&gt;Atomic&lt;/strong&gt;입니다. Atomic은 타입을 감싸서 Compare and Swap을 활용하여 Thread-safe한 메소드만을 노출시킵니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Java 코드&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;AtomicInt&lt;/span&gt; atomicInt &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;AtomicInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Thread-safe한 함수들&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; value1 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; atomicInt&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; value2 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; atomicInt&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getAndIncrement&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; value2 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; atomicInt&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addAndGet&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Atomic에 단점이 몇 가지 있는데 값 타입에는 atomic을 사용할 수 없다는 것입니다. Compare and Swap 명령어는 &lt;code&gt;int&lt;/code&gt;, &lt;code&gt;bool&lt;/code&gt;이나 참조 타입(포인터)과 같은 기초적인 타입만을 넘길 수 있기 때문입니다.&lt;/p&gt;
&lt;p&gt;Atomic 또한 Compare and Swap을 기반으로 만들었기 때문에 부수 효과를 포함하는 수정은 할 수 없습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add100_000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; oldValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; globalValue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; newValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; oldValue &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 부수 효과가 있어서 globalValue를 atomic으로 만들 수 없다!&lt;/span&gt;
    cout &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Changing globalValue to &quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; newValue &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; endl&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    globalValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; newValue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이런 경우에 사용하는 것이 **뮤텍스(Mutex)**입니다.&lt;/p&gt;
&lt;h3 id=&quot;뮤텍스&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AE%A4%ED%85%8D%EC%8A%A4&quot; aria-label=&quot;뮤텍스 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;뮤텍스&lt;/h3&gt;
&lt;p&gt;뮤텍스는 특정 코드 영역을 여러 쓰레드가 동시에 접근할 수 없게 만드는 데 사용합니다. &lt;code&gt;add100_000&lt;/code&gt;에서 경쟁 상태가 발생하는 코드 부분만을 뮤텍스로 감싼 코드는 다음과 같습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; globalValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
mutex m&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add100_000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    m&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;lock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 이 영역에는 동시에 하나의 쓰레드만 진입할 수 있다&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; oldValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; globalValue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; newValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; oldValue &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    globalValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; newValue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    m&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;unlock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;뮤텍스를 활용할 경우 A 쓰레드의 1, 2, 3번 명령 사이에 B 쓰레드의 명령이 끼어들 수 없기 때문에 Thread-safey를 얻습니다. A 쓰레드가 먼저 &lt;code&gt;lock&lt;/code&gt;을 호출하고 진입했다면 B 쓰레드는 A가 모든 작업을 끝내고 &lt;code&gt;unlock&lt;/code&gt;을 호출할 때까지 대기합니다.&lt;/p&gt;
&lt;p&gt;Atomic과 달리 재시도를 하지 않기 때문에 부수 효과가 있는 코드가 포함되어 있어도 문제없이 작동합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; globalValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
mutex m&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add100_000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    m&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;lock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; oldValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; globalValue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; newValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; oldValue &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 부수 효과가 있어도 괜찮다!&lt;/span&gt;
    cout &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Changing globalValue to &quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; newValue &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; endl&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    globalValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; newValue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    m&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;unlock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;뮤텍스의 구현 방식은 여러 가지가 있는데, 가장 간단한 것은 **스핀락(Spinlock)**입니다. 스핀락은 Compare and Swap 명령을 이용하여 구현할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 참고: 실제 스핀락은 더 최적화 되어있음&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;SpinLock&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;bool&lt;/span&gt; isLocked &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;lock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// isLocked가 false라면 true를 대입한다.&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// isLocked가 true라면 false가 될 때까지 재시도한다.&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;compareAndSwap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;isLocked&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;unlock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    isLocked &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;스핀락의 단점은 락이 해제될 때까지 반복적으로 명령을 수행하기 때문에 CPU를 낭비한다는 것입니다. 그렇기 때문에 보편적으로 사용하는 뮤텍스는 &lt;code&gt;compareAndSwap&lt;/code&gt; 시도가 실패하면 운영체제와 소통하여 쓰레드를 대기 상태로 설정합니다. 재우는 동안에는 CPU를 점거하지 않으며 &lt;code&gt;unlock&lt;/code&gt;이 호출되었을 때 일어나서 다시 &lt;code&gt;compareAndSwap&lt;/code&gt;을 시도합니다. 단점으로는 운영체제와 소통하는 시간이 들어가기 때문에 스핀락보다 반응성이 조금 떨어지고 락이 풀리는데 걸리는 시간이 짧을 경우 스핀락보다 성능 면에서도 더 떨어질 수도 있습니다.&lt;/p&gt;
&lt;p&gt;이러한 이유로 &lt;code&gt;lock&lt;/code&gt; 함수 자체가 어느 정도 성능 또는 반응성을 희생하기 때문에 &lt;code&gt;lock&lt;/code&gt;을 너무 자주 해제하고 거는 것은 좋지 않습니다. &lt;code&gt;add100_000&lt;/code&gt; 함수의 뮤텍스는 사실 좀 더 넓게 잡아주는 게 좋습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; globalValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
mutex m&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add100_000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// For 루프 시작 지점에 걸어서 100,000번이 아닌 1번만 lock을 건다&lt;/span&gt;
  m&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;lock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; oldValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; globalValue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; newValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; oldValue &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    globalValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; newValue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  m&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;unlock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;뮤텍스가 장점만 있는 것은 아닙니다. 장점만큼 단점도 꽤 많습니다.&lt;/p&gt;
&lt;p&gt;첫 번째로는 이미 lock이 걸렸을 때 unlock이 될 때까지 쓰레드가 대기해야 하기 때문에 CPU 쓰레드 풀의 쓰레드는 뮤텍스를 대기해서는 안 됩니다. 뮤텍스를 대기하는 순간 쓰레드 풀의 쓰레드 하나를 쓰지 못하는 것이기 때문에 CPU 코어를 제대로 활용할 수 없게 됩니다.&lt;/p&gt;
&lt;p&gt;두 번째 문제는 락을 이용한 프로그래밍은 어렵다는 것입니다. 락을 걸어야할 필요가 있는지, 락을 얼마나 넓게 잡아야 할지 철저히 고민하면서 프로그래밍해야 하고 락을 거는 내부의 코드가 어떤 경쟁 상태를 만드는지도 꿰고 있어야 합니다.&lt;/p&gt;
&lt;p&gt;세 번째 문제가 가장 큰 문제입니다, 바로 **데드락(Deadlock)**이 발생할 수 있다는 것입니다. 데드락은 두 개의 쓰레드가 서로의 뮤텍스가 해제되는 것을 기다릴 때 발생합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
mutex m1&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
mutex m2&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;task1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 뮤텍스 1, 2에 락을 건다&lt;/span&gt;
  m1&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;lock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  m2&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;lock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  m2&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;unlock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  m1&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;unlock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;task2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 뮤텍스 1, 2에 락을 건다, 다만 2부터 건다&lt;/span&gt;
  m2&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;lock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  m1&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;lock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  m1&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;unlock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  m2&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;unlock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

thread t1 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;thread&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;task1&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
thread t2 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;thread&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;task2&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

t1&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
t2&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;만약 각 쓰레드가 동시에 &lt;code&gt;m1&lt;/code&gt;과 &lt;code&gt;m2&lt;/code&gt; 뮤텍스에 락을 걸면 데드락 현상이 발생합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// t1 쓰레드에서 m1 뮤텍스에 락을 건다&lt;/span&gt;
m1&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;lock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// t2 쓰레드에서 m2 뮤텍스에 락을 건다&lt;/span&gt;
m2&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;lock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// t1 쓰레드에서 m2 뮤텍스에 락을 건다&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 하지만 t2 쓰레드가 이미 걸어버려서 대기한다&lt;/span&gt;
m2&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;lock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// t2 쓰레드에서 m1 뮤텍스에 락을 건다&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 하지만 t1 쓰레드가 이미 걸어버려서 대기한다&lt;/span&gt;
m2&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;lock&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;두 쓰레드 다 서로의 락이 풀릴 때까지 기다리게 됩니다. 아무런 진전 없이 영원히 대기하게 되는 것입니다. 프로그램이 제대로 작동하지 않게 됩니다.&lt;/p&gt;
&lt;p&gt;데드락을 약간이나마 예방하는 방법은 여러 뮤텍스를 언제나 순서대로 락을 거는겁니다. 위 예시에서는 &lt;code&gt;t1&lt;/code&gt; 쓰레드는 &lt;code&gt;m1&lt;/code&gt; 뮤텍스에 락을 건 뒤 &lt;code&gt;m2&lt;/code&gt; 뮤텍스에 락을 걸었고 &lt;code&gt;t2&lt;/code&gt; 쓰레드는 &lt;code&gt;m2&lt;/code&gt;, 그다음 &lt;code&gt;m1&lt;/code&gt; 뮤텍스에 락을 걸어서 발생합니다. 만약 두 쓰레드 모두 뮤텍스를 같은 순서대로 걸었다면 데드락 현상은 발생하지 않습니다.&lt;/p&gt;
&lt;p&gt;하지만 이 예방 방법은 단점이 있습니다. 앞서서 락을 걸어야하기 때문에 락을 조건부로 거는 방법은 없습니다. &lt;code&gt;m1&lt;/code&gt;, &lt;code&gt;m2&lt;/code&gt; 순서대로 락을 걸도록 정책을 잡으면 &lt;code&gt;m2&lt;/code&gt;에 락을 걸고 나서 &lt;code&gt;m1&lt;/code&gt;을 조건부로 거는 방법은 없습니다. &lt;code&gt;m2&lt;/code&gt; 락을 걸고 나서 뒤늦게 조건 체크하여 &lt;code&gt;m1&lt;/code&gt; 락을 거는 것은 정책을 위반한 것이기 때문이죠. 여기서 &lt;code&gt;m1&lt;/code&gt; 뮤텍스도 실제 필요한 순간보다 앞서서 락을 걸기 때문에 락을 거는 범위가 늘어나 쓰레드의 대기시간이 늘어난다는 단점도 있습니다.&lt;/p&gt;
&lt;p&gt;그래도 뮤텍스가 부수 효과 있는 코드 영역에서 안전하게 경쟁 상태를 제거할 수 있는 몇 안 되는 방법이기에 뮤텍스를 완전히 배제할 수는 없습니다. 그나마 할 수 있는 것은 뮤텍스의 사용을 최소화하고 가능하면 락이 없는 메모리 공유 방식을 사용하는 것입니다.&lt;/p&gt;
&lt;h3 id=&quot;동시성-자료구조&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0&quot; aria-label=&quot;동시성 자료구조 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;동시성 자료구조&lt;/h3&gt;
&lt;p&gt;**동시성 자료구조(Concurrent Data Structures)**는 Thread-safe한 자료구조들을 일컫습니다. 존재하는 동시성 자료구조들의 예시는 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;스택(Stack)&lt;/li&gt;
&lt;li&gt;큐(Queue)&lt;/li&gt;
&lt;li&gt;리스트(List)&lt;/li&gt;
&lt;li&gt;맵(Map, aka. Dictionary)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;각각의 자료구조들은 다양한 구현 방식이 존재하고 그에 따라 장점과 단점이 나눠집니다. 특히 맵의 경우 락을 이용해 방법이 있고 락 없이 구현한 방법도 있습니다. Java의 &lt;code&gt;ConcurrerntHashMap&lt;/code&gt; 경우 락을 이용해서 구현되었습니다.&lt;/p&gt;
&lt;p&gt;락 없이 구현된 동시성 자료구조들은 주로 Compare and Swap을 이용해서 구현되었습니다. 그렇기 때문에 많은 쓰레드가 동시에 접근하고 자료구조를 수정한다면 Compare and Swap이 더 자주 실패하기 때문에 성능이 조금 떨어질 수 있습니다.&lt;/p&gt;
&lt;p&gt;동시성 자료구조 중에 가장 중요한 자료구조를 뽑아보라고 한다면 **큐(Queue)**라고 할 수 있습니다. 큐를 활용하면 쓰레드 간의 소통을 할 수 있습니다. 그중에서도 **채널(Channel)**은 큐에 더 이상 요소를 추가할 수 없도록 닫는 기능과 채널의 메시지를 대기하는 기능을 추가한 것입니다. 채널을 활용하면 비동기적으로 처리되는 스트림을 구현할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;rust&quot;&gt;&lt;pre class=&quot;language-rust&quot;&gt;&lt;code class=&quot;language-rust&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Rust 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 주어진 디렉토리에 있는 파일들을 읽어서 채널로 보내주는 함수&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;stream_files_in_directory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;token lifetime-annotation symbol&quot;&gt;&apos;static&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; sender&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Sender&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token namespace&quot;&gt;io&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Result&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 디렉토리 내 디렉토리/파일들 나열하기&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; entries &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token namespace&quot;&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;read_dir&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 디렉토리/파일들 순회&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; entry &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; entries &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 파일이면 읽고 string으로 변환해서 채널에 삽입&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; entry &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; entry&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; entry&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;file_type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;is_file&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; bytes &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token namespace&quot;&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;entry&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      sender&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from_utf8&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;bytes&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;unwrap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;unwrap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 작업 완료 (채널은 자동으로 닫힘)&lt;/span&gt;
  &lt;span class=&quot;token class-name&quot;&gt;Ok&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 채널 만들기&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;sender&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; receiver&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;channel&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 생성 후 실행&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; thread &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token namespace&quot;&gt;thread&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;spawn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token closure-params&quot;&gt;&lt;span class=&quot;token closure-punctuation punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token closure-punctuation punctuation&quot;&gt;|&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;stream_files_in_directory&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;src&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; sender&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 채널이 닫힐때까지 파일 데이터를 받는다&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; bytes &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; receiver&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;iter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 파일 데이터를 콘솔로 출력한다&lt;/span&gt;
    &lt;span class=&quot;token macro property&quot;&gt;println!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;{bytes}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이외에도 쓰레드 풀에 태스크를 넘길 때 사용되는 것이 큐입니다. 태스크를 넘길 때 태스크 대기열에 들어가게 되는데 이때 Thread-safey를 가진 큐를 사용하여 여러 쓰레드가 동시에 태스크를 추가해도 문제없도록 구현합니다. 어떤 면에서는 쓰레드 풀의 큐도 쓰레드간 소통을 위해 사용한다고 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;큐의 대표적 사용처가 한 가지 더 있는데, 바로 **액터(Actor)**입니다&lt;/p&gt;
&lt;h3 id=&quot;액터&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%95%A1%ED%84%B0&quot; aria-label=&quot;액터 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;액터&lt;/h3&gt;
&lt;p&gt;액터는 메모리를 아예 공유하지 않고 쓰레드 하나에 완전히 맡겨버리는 방법입니다. 만약 다른 쓰레드에서 메모리에 접근하거나 수정하고 싶다면 반드시 해당 쓰레드와 소통하여 조회하거나 수정하게 해야 합니다. &lt;code&gt;add100_000&lt;/code&gt;를 액터를 활용해서 구현한 코드는 다음과 같습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;rust&quot;&gt;&lt;pre class=&quot;language-rust&quot;&gt;&lt;code class=&quot;language-rust&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Rust 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 참고: 9가지 언어 중에 내장 라이브러리만으로&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 액터를 간단하게 구현할 수 있는 언어가 Rust밖에 없었음&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 액터에게 보낼 수 있는 명령들&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;token type-definition class-name&quot;&gt;Command&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token class-name&quot;&gt;Increment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 값 1 올리기&lt;/span&gt;
  &lt;span class=&quot;token class-name&quot;&gt;Get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Sender&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;i32&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 값 조회하기&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 액터 명령을 받아줄 쓰레드용 함수&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;counter_actor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;command_receiver&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Receiver&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Command&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;mut&lt;/span&gt; value &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 무한 반복&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;loop&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 명령을 받아서...&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; command &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; command_receiver&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;recv&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;unwrap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;match&lt;/span&gt; command &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// Increment면 value를 1 상승시킨다&lt;/span&gt;
      &lt;span class=&quot;token class-name&quot;&gt;Command&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Increment&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; value &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// Get이면 같이 온 채널에 값을 전달한다&lt;/span&gt;
      &lt;span class=&quot;token class-name&quot;&gt;Command&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;sender&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; sender&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;unwrap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;add100_000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;actor_sender&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Sender&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Command&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; _ &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;100000&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 값을 직접 올리는 것이 아닌, 액터에게 값을 올리는 명령을 전달한다 일을 시킨다&lt;/span&gt;
    actor_sender&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Command&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Increment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;unwrap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 액터 명령용 채널 생성&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;command_sender&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; command_receiver&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;channel&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 액터 쓰레드 돌리기&lt;/span&gt;
  &lt;span class=&quot;token namespace&quot;&gt;thread&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;spawn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token closure-params&quot;&gt;&lt;span class=&quot;token closure-punctuation punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token closure-punctuation punctuation&quot;&gt;|&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;counter_actor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;command_receiver&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// add100_000를 돌릴 2개의 쓰레드&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; t1 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; command_sender &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; command_sender&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;clone&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token namespace&quot;&gt;thread&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;spawn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token closure-params&quot;&gt;&lt;span class=&quot;token closure-punctuation punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token closure-punctuation punctuation&quot;&gt;|&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add100_000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;command_sender&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; t2 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; command_sender &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; command_sender&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;clone&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token namespace&quot;&gt;thread&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;spawn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token closure-params&quot;&gt;&lt;span class=&quot;token closure-punctuation punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token closure-punctuation punctuation&quot;&gt;|&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add100_000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;command_sender&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// add100_000 작업이 끝날 때까지 기다리기&lt;/span&gt;
  t1&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;unwrap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;unwrap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  t2&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;unwrap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;unwrap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 조회 결과를 받을 채널 생성&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;result_sender&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; result_receiver&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;channel&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 액터에게 조회 명령을 내린다&lt;/span&gt;
  command_sender&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Command&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;result_sender&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;unwrap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 조회 결과는 채널에서 받는다&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; result_receiver&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;recv&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;unwrap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 200000이 출력된다&lt;/span&gt;
  &lt;span class=&quot;token macro property&quot;&gt;println!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;{result}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;실제 액터를 사용할 때는 직접 구현하지 않고 라이브러리를 통해서 액터를 사용하는 것이 일반적입니다. 그리고 액터를 실행할 때 쓰레드를 사용하면 액터의 수와 쓰레드의 수가 비례하므로 운영체제 쓰레드가 아닌 경량쓰레드를 사용하는 것이 일반적입니다.&lt;/p&gt;
&lt;p&gt;메모리를 공유하지 않으면 경쟁 상태가 발생할 일도 없습니다. 액터에 명령이 쌓이면 내부의 쓰레드가 명령들을 하나씩 순서대로 처리하여 값을 수정합니다. 그런 이유로 뮤텍스도 필요 없고 액터 내에서 Compare and Swap을 할 필요가 없기 때문에 부수 효과가 있는 명령들을 수행해도 괜찮습니다.&lt;/p&gt;
&lt;p&gt;액터의 가장 빛날 때는 액터에 응답이 필요 없는 명령을 내리는 경우입니다. 위 예시에서는 &lt;code&gt;Increment&lt;/code&gt; 명령이 그러한데요, &lt;code&gt;Increment&lt;/code&gt;의 경우 기존 뮤텍스 기반 방식과 달리 쓰레드 대기가 전혀 필요없기 때문입니다. 그런 면에서 가장 CPU 사용 효율이 좋다고 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;이처럼 액터가 가진 강점이 여러 가지이기 때문에 액터가 언어에 내장되어 제공되기도 합니다. 그중 하나는 Swift입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;swift&quot;&gt;&lt;pre class=&quot;language-swift&quot;&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Swift 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 액터 정의&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;actor&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CounterActor&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Int&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;increment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Int&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;add100_000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;counterActor&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CounterActor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token omit keyword&quot;&gt;_&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;..&amp;lt;&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;100000&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 그냥 함수 호출하듯이 사용하면 된다&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; counterActor&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;increment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 액터 생성&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; counterActor &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CounterActor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 쓰레드 풀에 태스크 전달&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; task1 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Task&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add100_000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;counterActor&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; counterActor&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; task2 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Task&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add100_000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;counterActor&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; counterActor&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 태스크들이 끝날 때까지 대기&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; task1&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value
&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; task2&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value

&lt;span class=&quot;token comment&quot;&gt;// 조회 및 출력&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; value &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; counterActor&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;액터를 자체 지원하는 다른 두 언어는 Erlang과 Elixir입니다. 두 언어 모두 &lt;strong&gt;BEAM&lt;/strong&gt;이라는 **VM(Virtual Machine)**으로 실행하는데, 이 VM의 특징은 운영체제 쓰레드 대신 경량쓰레드만 존재한다는 것과 모든 경량쓰레드에 Mailbox라는 채널이 달려있다는 것입니다. Elixir에서 액터를 구현하는 방법은 다음과 같습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;elixir&quot;&gt;&lt;pre class=&quot;language-elixir&quot;&gt;&lt;code class=&quot;language-elixir&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Elixir 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# 액터 정의&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;token module class-name&quot;&gt;CounterActor&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;token module class-name&quot;&gt;GenServer&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;_init_arg&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token atom symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;handle_cast&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token atom symbol&quot;&gt;:increment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token atom symbol&quot;&gt;:noreply&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; value &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;handle_call&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token atom symbol&quot;&gt;:get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; _from&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token atom symbol&quot;&gt;:reply&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;defmodule&lt;/span&gt; &lt;span class=&quot;token module class-name&quot;&gt;Test&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;do&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add100_000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;actor_pid&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;token module class-name&quot;&gt;Enum&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;each&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;100000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;_&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token module class-name&quot;&gt;GenServer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;cast&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;actor_pid&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token atom symbol&quot;&gt;:increment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 액터 생성&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token atom symbol&quot;&gt;:ok&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; actor_pid&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token module class-name&quot;&gt;GenServer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;start_link&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token module class-name&quot;&gt;CounterActor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 경량쓰레드 생성하여 태스크 전달&lt;/span&gt;
task1 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token module class-name&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;async&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token module class-name&quot;&gt;Test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add100_000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;actor_pid&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
task2 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token module class-name&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;async&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token module class-name&quot;&gt;Test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add100_000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;actor_pid&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 태스크들이 끝날 때까지 대기&lt;/span&gt;
&lt;span class=&quot;token module class-name&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;await&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;task1&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token module class-name&quot;&gt;Task&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;await&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;task2&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 조회 및 출력&lt;/span&gt;
result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token module class-name&quot;&gt;GenServer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;call&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;actor_pid&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token atom symbol&quot;&gt;:get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token module class-name&quot;&gt;IO&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;puts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;하지만 응답이 필요한 요청을 보냈을 때 응답을 받으려면 액터의 쓰레드가 요청을 처리할 때까지 대기해야하므로 어떻게 보면 액터 자체가 락으로써 작용한다고 할 수 있습니다. 이러한 응답 대기가 꼬이면 뮤텍스처럼 액터도 데드락이 발생할 수 있다는 것이 단점입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;swift&quot;&gt;&lt;pre class=&quot;language-swift&quot;&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Swift 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 액터 정의&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;actor&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CounterActor&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 짝지어진 액터 변수 추가&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; otherActor&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CounterActor&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Int&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;setOtherActor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;otherActor&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CounterActor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;otherActor &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; otherActor
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;increment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Int&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 짝지어진 액터 변수의 값을 가져와서 평균을 계산하여 출력하는 함수&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;getAverage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Double&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 짝지어진 액터에게 요청하고 응답을 기다림&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; otherValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;otherActor&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Double&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;otherValue &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2.0&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 액터 2개 생성&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; actor1 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CounterActor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; actor2 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CounterActor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 액터 짝지어주기&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; actor1&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setOtherActor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;otherActor&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actor2&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; actor2&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;setOtherActor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;otherActor&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; actor1&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 만약 양쪽 액터에 동시에 getAverage를 호출한다면..&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; asyncResult1 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Task&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; actor1&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getAverage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; asyncResult2 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Task&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; actor2&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getAverage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 과연 응답이 돌아올까?&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; result1 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; asyncResult1
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; result2 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; asyncResult2&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;만약 양쪽 액터가 &lt;code&gt;getAverage&lt;/code&gt;을 동시에 실행하면 두 액터는 서로에게 &lt;code&gt;get&lt;/code&gt; 명령을 전달하게 될 것입니다. 이 &lt;code&gt;get&lt;/code&gt; 명령은 &lt;code&gt;getAverage&lt;/code&gt;가 완료되었을 때 실행합니다. 하지만 &lt;code&gt;getAverage&lt;/code&gt;가 완료되려면 서로의 &lt;code&gt;get&lt;/code&gt; 함수가 수행되어 값을 전달받아야 한다는 모순에 빠지게 됩니다. 데드락에 걸리는 것입니다.&lt;/p&gt;
&lt;p&gt;Swift는 이 데드락 문제를 &lt;strong&gt;Reentrancy&lt;/strong&gt;로 해결했습니다. 액터에서의 Reentrancy는 액터 쓰레드가 대기할 상황이 생기면 대기하는 대신 다른 명령들을 실행하는 기능입니다. 위 예시에서는 다른 액터의 &lt;code&gt;get&lt;/code&gt;을 호출하면 응답을 기다리는 게 아니라 큐에 들어온 명령을 틈틈이 수행하게 됩니다. 이 과정에서 큐에 있었던 다른 액터가 보낸 &lt;code&gt;get&lt;/code&gt; 명령을 처리하게 되고 데드락은 완전히 제거됩니다.&lt;/p&gt;
&lt;p&gt;다만 이 방법은 단점이 없지는 않은데, 다른 명령을 틈틈히 수행하는 과정에서 필드가 수정되어 상태가 변화할 가능성도 있다는 것입니다. &lt;code&gt;await&lt;/code&gt; 전후로 상태가 바뀌는 것입니다. 이러한 이유로 &lt;code&gt;await&lt;/code&gt;마다 상태가 바뀔 수도 있음을 감안하고 로직을 구현해야하기 때문에 로직 구현 난이도가 급상승합니다.&lt;/p&gt;
&lt;h3 id=&quot;stm&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#stm&quot; aria-label=&quot;stm permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;STM&lt;/h3&gt;
&lt;p&gt;**STM(Software Transactional Memory)**은 데이터베이스의 트랜잭션과 유사한 방법으로 메모리 공유 문제를 해결하는 방법입니다.&lt;/p&gt;
&lt;p&gt;데이터베이스의 트랜젝션은 조회 및 수정 이후 커밋 처리하면 성공 또는 롤백으로 응답하게 되는데, 성공이 되었으면 여러 데이터베이스 테이블에 대한 수정이 한 번에 적용되며 실패했으면 롤백 되어 수정 사항을 반영하지 않습니다.&lt;/p&gt;
&lt;p&gt;STM도 비슷하게 여러 STM 변수와 STM 전용 자료구조를 수정한 뒤 일종의 커밋 과정을 거쳐 갑니다. 이때 조회 또는 수정한 자료구조들이 다른 쓰레드에 의해서 수정되었다면 실패 처리하고 처음부터 다시 수정 및 커밋 시도하는 구조입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Scala 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// ZIO 라이브러리를 이용해서 작성된 STM 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 주의: 실제로는 동작하지 않는 코드&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// STM 기초 타입들/자료구조들&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; playerIds&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; TSet&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; lastJoinedPlayerId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; TRef&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Option&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; log&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; TQueue&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// STM 조회 및 수정사항 정의&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; notifyPlayerJoined&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;newPlayerId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; USTM&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;Unit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 아래의 모든 요청들은 전부 한 번에 수정된다&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    _                &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; playerIds&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;put&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;newPlayerId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    previousPlayerId &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; lastJoinedPlayerId&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;getAndUpdate&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Some&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;newPlayerId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    _                &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; previousPlayerId &lt;span class=&quot;token keyword&quot;&gt;match&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                          &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; Some&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;previousPlayerId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;=&gt;&lt;/span&gt;
                            log&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;offer&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-interpolation&quot;&gt;&lt;span class=&quot;token id function&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;previousId&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; is no longer the last player&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                          &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; None &lt;span class=&quot;token keyword&quot;&gt;=&gt;&lt;/span&gt;
                            STM&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;unit
                        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    _                &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; log&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;offer&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-interpolation&quot;&gt;&lt;span class=&quot;token id function&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;newPlayerId&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; is the new last player&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// commit으로 수정사항 확정&lt;/span&gt;
notifyPlayerJoined&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Mango&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;commit&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이런 면에서 Atomic과 유사한 점이 많은데, Atomic은 Compare and Swap으로 대입이나 덧셈 뺄셈과 같은 기본적인 연산들에 대한 Thread-saftey를 제공한다면 STM은 캐 여러 개의 자료구조나 변수에 대한 수정에 대한 Thread-safety를 제공합니다. 또 양쪽 기법 모두 재시도 기반으로 작동하기 때문에 부수 효과가 존재하면 안 된다는 점도 비슷합니다.&lt;/p&gt;
&lt;p&gt;STM이 가진 단점은 몇 가지가 더 있는데 재시도 기반이라서 쓰레드간 수정 경쟁이 치열하다면 성능이 하락합니다. 특히 수정 및 조회가 많아서 영향 끼치는 자료구조가 많거나 커밋을 하기 전 계산이 오래 걸린다면 수정 경쟁이 일어날 확률이 더 높아져 효율성이 더 떨어질 수도 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;결론&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%B0%EB%A1%A0&quot; aria-label=&quot;결론 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;결론&lt;/h3&gt;
&lt;p&gt;이번 동시성 프로그래밍 편은 작성하다 보니 프로그래밍 시리즈 5편 중 가장 긴 편이 되었습니다. 그럼에도 분량상 빠트린 내용도 이것저것 많이 있어서 아쉽습니다. 동시성 프로그래밍은 확실히 완벽한 해결책이 존재하지 않아 어렵습니다. 그래도 다행인 건 반드시 한 프로그램에서 한 가지 동시성 기법을 사용해야 하는 건 아닙니다. 어떤 동시성 프로그래밍 기법이 적절한지 판단하여 높은 성능과 반응성을 가진 아키텍처를 만드는 것에 이번 글, 모자라지만 조금이라도 도움이 되었으면 좋겠습니다.&lt;/p&gt;
&lt;p&gt;여담으로 이번 편에서 사실 Haskell과 Zig이라는 언어도 소개하려고 했었는데 안 그래도 높은 글의 난이도가 더 높아질 것 같아서 Swift와 Elixir로 참았습니다. 언젠가 기회가 되신다면 Zig의 Colorblind Async-Await에 대해서 찾아보시는 것을 추천해 드립니다.&lt;/p&gt;
&lt;p&gt;이렇게 프로그래밍 언어 시리즈가 끝났습니다. 프로그래밍 언어 시리즈는 사실 제 친구들이나 쿠키런 킹덤 서버팀 동료들과 프로그래밍 언어에 대해서 논쟁을 하면서 나왔었던 주제들을 총집합해서 엮어낸 시리즈입니다. 제 친구들과 동료들이 없었으면 작성할 수 없었을 것입니다. 제 친구들과 쿠키런 킹덤 서버팀에게 이 시리즈를 바칩니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[CTO가 커리어를 걸고 비트 레벨까지 내려가서 DB를 해킹했던 이야기]]></title><description><![CDATA[DB를 만든 회사에서 복구는 불가능하다고 얘기하는 극한의 상황 속에서 CTO가 커리어를 걸고 비트 레벨까지 내려가서 DB를 해킹했던 이야기를 소개합니다.]]></description><link>https://tech.devsisters.com/posts/bit-level-database-hacking</link><guid isPermaLink="false">https://tech.devsisters.com/posts/bit-level-database-hacking</guid><pubDate>Tue, 17 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;이 글과 관련된 다른 글 보기&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/posts/cookierun-kingdom-launch-revisited/&quot;&gt;쿠키런: 킹덤 런칭 회고&lt;/a&gt; - DevOps 관점에서 본 쿠키런: 킹덤 런칭 회고&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/cockroachdb-in-production/&quot;&gt;CockroachDB in Production&lt;/a&gt; - CockroachDB에 대한 소개와 킹덤에서 CockroachDB를 사용한 이유 설명&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/crk-launch-storage-postmortem/&quot;&gt;쿠키런: 킹덤 데이터베이스 스토리지 레이어 복원기&lt;/a&gt; - 엔지니어 관점 기록한 개괄적인 장애 대응 과정과 회고&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/36h-downtime-on-first-day/&quot;&gt;입사 첫날에 36시간 점검 경험하기&lt;/a&gt; - 신규입사자의 관점에서 서술된 장애 대응 기록&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/crk-aws-az-failure-postmortem/&quot;&gt;쿠키런: 킹덤 AWS AZ 장애 아웃라인&lt;/a&gt; - AWS AZ 장애로부터 서비스를 복구한 이야기&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/crk-hot-range-postmortem/&quot;&gt;쿠키런: 킹덤 길드 업데이트 이후 서비스 이슈 되돌아보기&lt;/a&gt; - 쿠키런: 킹덤에 길드 기능이 추가된 이후 발생한 서비스 이슈 회고&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;소프트웨어 엔지니어들은 수많은 추상화 속에서 살아갑니다. 하위 레벨에서 제공하는 추상화는 공기처럼 당연한 것으로 여겨지고, 그 위에 무엇을 어떻게 쌓아 올릴지 고민하는 게 소프트웨어 엔지니어링이라 할 수 있습니다. 오늘은 이렇게 당연하게 여겼던 추상화를 하나하나 파고들어간 이야기를 소개해 볼까 합니다. 수십 명의 개발자들이 만들었던 게임. 36시간 장애 속에서 수백만 명이 서비스 오픈만을 기다리고 있고, DB를 만든 회사에서 복구는 불가능하다고 얘기하는 극한의 상황 속에서 CTO가 커리어를 걸고 비트 레벨까지 내려가서 DB를 해킹했던 이야기를 소개합니다.&lt;/p&gt;
&lt;p&gt;이야기를 시작하기에 앞서 해커 문화(Hacker Culture)를 알고 갑시다. 대중적으로 ‘해킹’이라는 단어는 시스템을 불법적으로 침입하는 행위로 알려져 있습니다. 원래 ‘해킹’의 개념은 MIT에서 시작되었는데, 소프트웨어 시스템이나 하드웨어의 제약사항을 창조적으로 함께 극복해 내는 행위를 뜻합니다. 문제를 해결하거나 시스템의 한계를 극복하기 위해 시스템의 밑바닥까지 파고들고, 그 시스템에 대한 완벽한 이해를 추구합니다. 이러한 해커 문화를 이끌어간 리더들로는 GNU 창시자 RMS, C 언어를 개발한 Dennis Ritchie, Go 언어를 개발한 Ken Thompson, 리눅스를 개발한 Linus Torvalds 등이 있습니다. 보시면 아시겠지만 해커 문화와 오픈소스와는 밀접한 연관이 있습니다.&lt;/p&gt;
&lt;p&gt;데브시스터즈의 엔지니어링 조직은 뛰어난 엔지니어링을 지향함과 동시에 해커 문화와 오픈소스를 좋아합니다. 사용하는 오픈소스 제품의 코드 레벨까지 뛰어드는 걸 주저하지 않습니다. 여기서 소개되는 이야기는 이러한 문화가 얼마나 극한까지 갈 수 있는지 보여주며, 극한의 데이터베이스 해킹을 통해 장애를 극복하는 기술적 이야기를 다룹니다.&lt;/p&gt;
&lt;p&gt;이야기의 시작은 데브시스터즈 역사상 가장 길었던 쿠키런: 킹덤의 36시간 장애에서 시작합니다. 쿠키런: 킹덤은 메인 데이터베이스로 CockroachDB를 사용합니다. CockroachDB는 우리가 익숙한 전통적인 RDBMS처럼 ACID 특성을 가지고 있으며, SQL 기반의 트랜잭션 처리가 완벽하게 작동하는 분산 데이터베이스입니다. 런칭 전부터 대규모 사용자 유입을 대비하여 데이터베이스는 24대의 노드, 12TB의 스토리지, 7개의 복제본을 두는 설정으로 만반의 준비를 했는데요. 실제로 오픈 직후 어마어마한 사용자 유입이 있었고, 한 주가 채 지나기도 전에 스토리지가 약 8TB 정도로 꽉꽉 차기 시작합니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 455px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 48.888888888888886%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAIAAAA7N+mxAAAACXBIWXMAAAsTAAALEwEAmpwYAAABO0lEQVR42o1RXU+DQBDk/8ek/h9NGpvGNPGhPmhSrfZbRCiFHnDc19453GFT9UE3E5iFnd29uUgTkbXOOWttT5wjz/9EpLVZrDaPs/nTy2K9fevIfOl7/cTviNBAKc3bVuKlDedcto37X0SGbMlq3oosL4uSNdK+5m6ekYcJ5DmllGkhVMNleUSx7MVCqqvR3WhyP7ydXt9MpstiMNaDsbgcyy+Ii5Eczpo8Z5u4eJit4+QQPOrWhmXYmLxzGC6lCPZVjGmtwIlMUVZKG2ikMlnOjKH+zOfHCDZ6w53SFDgqDajrLwVK2xtGFi2174RQSlV1k+0PVV37VAMQtELAl31eMFZ/M6yqW3gcchTFSbpcbfFEyipeHBu0fk8+NrsYd7mLkzC/E2Ms9AAqOniObcOXjp9+mc4Vc5Z+AvvnQAi2RkZbAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/cd51cd1df6120bb14af8322e3210dfa5/33ecc/01-crdb-capacity-usage.webp 360w,
/static/cd51cd1df6120bb14af8322e3210dfa5/e2468/01-crdb-capacity-usage.webp 455w&quot; sizes=&quot;(max-width: 455px) 100vw, 455px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/cd51cd1df6120bb14af8322e3210dfa5/53918/01-crdb-capacity-usage.png 360w,
/static/cd51cd1df6120bb14af8322e3210dfa5/c27a4/01-crdb-capacity-usage.png 455w&quot; sizes=&quot;(max-width: 455px) 100vw, 455px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/cd51cd1df6120bb14af8322e3210dfa5/c27a4/01-crdb-capacity-usage.png&quot; alt=&quot;01 crdb capacity usage&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;/figure&gt;
&lt;p&gt;일반적으로 공휴일이 가장 큰 허들인데요. DevOps 조직인 인프라셀은 런칭 첫 주 주말을 장애 없이 넘어갑니다. 이후 월요일부터 스토리지가 차는 문제를 대응하기 위한 작업에 들어갑니다. 여기서 문제의 발단이 시작됩니다. 데이터베이스 확장 작업 전에 안전장치를 설정하기 위한 작업을 하던 중 의도치 않은 설정 미스로 인해 데이터베이스 노드 중 절반 이상이 다운되게 됩니다.&lt;/p&gt;
&lt;p&gt;앞서 CockroachDB는 트랜잭션을 지원한다고 했는데요. CockroachDB 클러스터는 기본적으로 Raft 기반의 Consensus Algorithm 을 사용하고, 가장 높은 수준의 트랜잭션 일관성을 목표로 설정되어 있습니다. 앞서 언급한 작업 중 발생한 의도하지 않은 설정 이슈로 인해 절반 이상의 노드들이 비일관성을 탐지하고 에러가 발생하면서 클러스터에서 제외되기 시작합니다. 이에 따라 데이터베이스는 트랜잭션 일관성을 지켜줄 수 없다고 판단하고 트랜잭션 처리를 중단하는 일종의 보호 모드로 들어갑니다. 이 이후 모든 종류의 SQL 쿼리가 처리되지 않게 되고, 게임 서버는 당연히 데이터베이스가 응답이 없으므로, 클라이언트의 모든 요청 처리를 못하게 됩니다. 즉, 서비스 전체 장애가 발생하게 됩니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2000px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 70%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAABYlAAAWJQFJUiTwAAADtUlEQVR42o2S61PUZRTHf39Ab6s3TVMvaqZhzAqDTRIcLCKCAGcKDbwUSqgZoBbkRMoqtNIicUkQWEATWHZdmfKCeEkcLsrNRbwuuz92WWyLyy6ysSywC58eyOlFrzoz33m+3/M8c75nnnMk59QUNqeDmVkv09PTTHum8Xg8y5iZmfkn9wRLObfb/S9fXFzkvyGN2Mz0G3uwyDJ379xhaGgIm82GzWrFbrcLbmXQPLh8jghtMj0Ub+Tle59vnvn5JcwJ7hMGC0huSydjlluMudw4nS5mvF7+b3i9szhdLiYfP+Yv0fVSUWnBLxy8buY8U/iEk3/JbXZaaBc+7xT+uRmhPfjmPII/gc/Lgn8OFkRXPr/APIv+WRaFlsb7TmH/ZQf3KlK4diCB+/qvkPWbcajysVXvRT6dhK3pU4YatwpswarbhLnsI26XRmEsjeZmWQT9J+KQT65nwliHNGXUcF0djT43mYvfpnGrdh9yXSy/awqxNuxGrnqfrvR3MJ34EEttLBbdFu6oounIDKY1I4yzyrfprIrGVBXDWHcVknytgoLI5/gmXsGFwxkYspI4lxOOOnktRxLf4mr2NozZGciNCZhrYxi5epTRgfOMDTQzPtCC80EHji4Djk4NYzfKkbTqHHRb38Cwex2liYHsj1tFsmIVu8IUpKwJ5LuYCCzHdjJwMoUH9ekMnv2BB015ODo0WFsO8OhGCY+uqBi+oBQFy8TanC9Cu3UF2RFBtP2YSH/dLlLXBVHxWQhXMlaj37OadlUoveo4HOcOMmLIYuRyOQ/1n2PaF8vtHZEMNn6MqfZdJnpqkPrO5HM04Rm+jnyBiynBmCpS6S3fzM9JATTsDORK7jr0aYEMl6xnuGondjG0P+u3054fg25DKJ1rxYDC4+hLUzDao0Fq0+VRsOlpqtMDKE54kV9z4tFmhlKc/DJaUahbGUaPSnShjsKcs4bRM3u4VxjFmS9WcLUwnJOZr2GIWkPb8yH8UXkCqeO0CnXSs5SmvkLh9pcoErikCqP3p2iaM0OQj29k/Fw29poNtClDad4XjCHtdYzlsTxq3Mj96ngulURQmxKIsaYYqV17kKJPnqJF+SZNWQG0fh9Ea56C5oyV/Lb3Ve4VRNKrDOH6fgV9eQlcPLQNy6kvsdRE8LDyPcyaSOSaD+hSr+RuiwppwtyGvbWQyVu1uHo1AtXcOJ1LZ8NhuhuO4LhcTF/9IQZbdMiXtdy/UMlkt4GxruOMi70b765kXPzdxM1jeOw3+RsWBFMEdRrpbQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/2bde078b47c06155536a9ba1d8845aaf/33ecc/02-crk-community-maintenance-notice.webp 360w,
/static/2bde078b47c06155536a9ba1d8845aaf/263a4/02-crk-community-maintenance-notice.webp 480w,
/static/2bde078b47c06155536a9ba1d8845aaf/0c8fb/02-crk-community-maintenance-notice.webp 640w,
/static/2bde078b47c06155536a9ba1d8845aaf/521f7/02-crk-community-maintenance-notice.webp 860w,
/static/2bde078b47c06155536a9ba1d8845aaf/cbd37/02-crk-community-maintenance-notice.webp 1200w,
/static/2bde078b47c06155536a9ba1d8845aaf/fab06/02-crk-community-maintenance-notice.webp 2000w&quot; sizes=&quot;(max-width: 2000px) 100vw, 2000px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/2bde078b47c06155536a9ba1d8845aaf/53918/02-crk-community-maintenance-notice.png 360w,
/static/2bde078b47c06155536a9ba1d8845aaf/9aebd/02-crk-community-maintenance-notice.png 480w,
/static/2bde078b47c06155536a9ba1d8845aaf/0f09e/02-crk-community-maintenance-notice.png 640w,
/static/2bde078b47c06155536a9ba1d8845aaf/3e3fe/02-crk-community-maintenance-notice.png 860w,
/static/2bde078b47c06155536a9ba1d8845aaf/64756/02-crk-community-maintenance-notice.png 1200w,
/static/2bde078b47c06155536a9ba1d8845aaf/f97d7/02-crk-community-maintenance-notice.png 2000w&quot; sizes=&quot;(max-width: 2000px) 100vw, 2000px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/2bde078b47c06155536a9ba1d8845aaf/f97d7/02-crk-community-maintenance-notice.png&quot; alt=&quot;02 crk community maintenance notice&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;/figure&gt;
&lt;p&gt;장애를 인지하자마자 저를 포함해 회사의 모든 가용인력이 장애 대응을 위한 비상 대응에 들어갑니다. 비상 대응에 들어가면서 두 가지 계획을 병행해서 운영하게 됩니다(상세한 장애 대응 기록은 별도의 기술블로그 글을 참고하시면 좋습니다.). 그 중 DevOps 팀의 최우선 과제는 데이터베이스를 정상 상태로 복원하는 것이었습니다.&lt;/p&gt;
&lt;p&gt;쿠키런: 킹덤에는 장애 시 기술적인 지원을 받을 수 있는 CockroachDB Enterprise를 사용 중이었는데요. 신속히 기술 지원 요청을 보냈고, 온 콜 대기 중이던 서포트 엔지니어와 연락을 하게 됩니다. 상황을 설명하고, 노드의 로그 메시지를 공유하는 등 해당 업체와 함께 트러블슈팅 및 복구 방안을 찾기 위한 작업을 하게 됩니다. 하지만 결론적으로 몇 시간 뒤 상황에 대한 진단이 완료되고 나니, 업체 쪽에서는 복구가 불가능하다는 결론이 납니다. 마지막 백업본으로 데이터베이스를 복구하고 서비스를 재개하는 것을 추천합니다.&lt;/p&gt;
&lt;p&gt;서포트 엔지니어가 이러한 추천을 한 기술적인 배경에는 CockroachDB의 설계 철학이 있다고 생각됩니다. CRDB는 Consensus Algorithm을 기반으로 Majority Node에 커밋 된 데이터를 통하여 일관성을 확보하기 때문인데요. 다수의 노드가 설정 미스로 인하여 클러스터 참여가 불가해진 시점에선 이미 데이터가 일관되게 저장되어 있음을 보장할 수 없기 때문에 불완전한 현재 데이터를 복구 하는 것 보다는 일관성이 보장된 상태의 백업으로 복구하는 것을 권장한 것으로 판단됩니다. 만약 CRDB 기반으로 은행 서비스를 개발했다면, 실제로 데이터베이스 복구 작업은 불가하다고 보는 게 맞고, 백업에서 복원하는 게 가장 좋은 선택일지도 모르겠습니다.&lt;/p&gt;
&lt;p&gt;하지만 우리의 경우는 상황이 많이 달랐는데요. 쿠키런: 킹덤 서버의 구조는 Snapshot과 Journal을 사용해 데이터를 커밋 하는 Event Sourcing 기반의 아키텍처를 사용합니다.&lt;sup&gt;&lt;a name=&quot;ftsrc_1&quot; href=&quot;#ft_1&quot;&gt;[1]&lt;/a&gt;&lt;/sup&gt; 따라서 데이터베이스에 있는 Snapshot, Journal 데이터를 가져올 수만 있다면 확률적으로 거의 완벽한 데이터 복구가 가능하다고 판단했습니다. 이에 따라 CockroachDB에 저장되어 있는 원시 데이터에서 테이블에 저장된 row들을 복구하는 방법에 대해 문의하였는데요. 서포트 엔지니어는 그런 마땅한 방법은 존재하지 않는다는 답변을 했습니다. 답변을 듣고 좌절하긴 했지만 (…) 분명히 데이터는 스토리지에 저장되어 있고, 이를 끄집어낼 수 있는 방법이 있을 거라고 생각했습니다. 이론적으로 가능하면, 실제로도 가능하니까요. &lt;sup&gt;&lt;a name=&quot;ftsrc_2&quot; href=&quot;#ft_2&quot;&gt;[2]&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;저와 함께 몇몇 엔지니어들은 CRDB에 저장되어 있는 데이터를 끄집어낼 수 있는 방법을 리서치하기 시작합니다. 우선 CRDB의 아키텍처에 대해서 일부 파악하고 있던 지식들이 있었는데요. 하위 스토리지 레이어는 Pebble로 구성된 Key-Value 스토리지를 사용한다는 사실, 그 위에 Raft 기반의 Consensus Algorithm, 이를 통해 ACID 및 트랜잭션 구현을 했고, 그 위에 SQL 레이어를 구현했다는 것을 알고 있었습니다. 런칭 전에 CockroachDB 스터디를 함께 했었거든요.&lt;/p&gt;
&lt;figure style=&quot;width: 70%; margin: 0 auto;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2000px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 48.33333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAABYlAAAWJQFJUiTwAAABdElEQVR42mVSi5KDIBDz//+yZ6soisjLR5W9zeK10x4zGXCRkGSpHk1Lx3ESRtdpatuehnGi262mlBap55y/8K6d5ycqYyOOCEJIQuJ8ID2MFFOidd24VuZt22hZVlqv+bk/Py4SwjEQK8y0M7ZtF0XOBWpVR12vRTWgVF++GYpdwJlz/sOBEGqXqbOZRl8IsTHzj9NkyTJwSI+G5tmRtbMod96T4T0o/kdYD5naKVNcs9hCnpYPaz0IoLTnGVCsFBf1/SAwxl6EBThbNaYoXHdWyITneZIPURSMZqLJWlGGb3MpRh1rXBxjosD4c1eNHMPEOT4PkpAxQABlbatYiZa16kpu9f1BDdebRsn+T32XOhxAZUXXQJ9PVHggLxySBjBRwwc8d37lzvoQpOshRlGHdeQ1cj3YXeUSW91LqPv1DPBsjNgtTYA9PKdlWTin49WId35ZFElTfnQmnwphWjYpvohMyRF2Ru40FIO4dPSQvL/xCy8pBNB843q2AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/0a8f2cf34c36efadd4e4b60a862b705f/33ecc/03-cockroach-labs-architecture-overview.webp 360w,
/static/0a8f2cf34c36efadd4e4b60a862b705f/263a4/03-cockroach-labs-architecture-overview.webp 480w,
/static/0a8f2cf34c36efadd4e4b60a862b705f/0c8fb/03-cockroach-labs-architecture-overview.webp 640w,
/static/0a8f2cf34c36efadd4e4b60a862b705f/521f7/03-cockroach-labs-architecture-overview.webp 860w,
/static/0a8f2cf34c36efadd4e4b60a862b705f/cbd37/03-cockroach-labs-architecture-overview.webp 1200w,
/static/0a8f2cf34c36efadd4e4b60a862b705f/fab06/03-cockroach-labs-architecture-overview.webp 2000w&quot; sizes=&quot;(max-width: 2000px) 100vw, 2000px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/0a8f2cf34c36efadd4e4b60a862b705f/53918/03-cockroach-labs-architecture-overview.png 360w,
/static/0a8f2cf34c36efadd4e4b60a862b705f/9aebd/03-cockroach-labs-architecture-overview.png 480w,
/static/0a8f2cf34c36efadd4e4b60a862b705f/0f09e/03-cockroach-labs-architecture-overview.png 640w,
/static/0a8f2cf34c36efadd4e4b60a862b705f/3e3fe/03-cockroach-labs-architecture-overview.png 860w,
/static/0a8f2cf34c36efadd4e4b60a862b705f/64756/03-cockroach-labs-architecture-overview.png 1200w,
/static/0a8f2cf34c36efadd4e4b60a862b705f/f97d7/03-cockroach-labs-architecture-overview.png 2000w&quot; sizes=&quot;(max-width: 2000px) 100vw, 2000px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/0a8f2cf34c36efadd4e4b60a862b705f/f97d7/03-cockroach-labs-architecture-overview.png&quot; alt=&quot;03 cockroach labs architecture overview&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        &lt;a href=&quot;https://www.cockroachlabs.com/docs/stable/architecture/overview.html&quot;&gt;Cockroach Labs Architecture Overview&lt;/a&gt;
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;우리는 저장되어 있는 스토리지에서 데이터를 뽑아낼 수 있는 방법이 없을까 고민하기 시작했습니다. 실제 노드에 물리적으로 저장된 파일들을 파보기 시작합니다. sst 확장자의 파일들이 보입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;[ec2-user@ip-172-21-76-209 /]$ cd /mnt/nvme1n1/
[ec2-user@ip-172-21-76-209 nvme1n1]$ ls -al
total 1749736
drwxrwsr-x 5 root root    12288 Jan 25 08:09 .
drwxr-xr-x 3 root root       21 Dec 22 15:15 ..
-rw-r----- 1 root root  4179539 Dec 24 09:35 001335.sst
-rw-r----- 1 root root  4194844 Dec 24 09:35 001336.sst
-rw-r----- 1 root root  1412741 Dec 24 09:58 001364.sst
-rw-r----- 1 root root  8405018 Dec 25 14:11 003425.sst
-rw-r----- 1 root root  8337500 Dec 25 14:11 003426.sst&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;여기서 sst 파일은 무엇일까요? CockroachDB에서 사용하는 Pebble은 Facebook에서 만든 RocksDB의 후계자이고, RocksDB는 Google에서 만든 LevelDB의 후계자입니다.&lt;sup&gt;&lt;a name=&quot;ftsrc_3&quot; href=&quot;#ft_3&quot;&gt;[3]&lt;/a&gt;&lt;/sup&gt; sst 파일은 LevelDB가 스토리지에 데이터를 물리적으로 저장할 때 사용하는 확장자입니다. Pebble의 sst 파일이 RocksDB, LevelDB의 sst 파일과 호환이 될지 살펴봅니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cockroachlabs.com/docs/stable/architecture/storage-layer.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://www.cockroachlabs.com/docs/stable/architecture/storage-layer.html&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;CockroachDB uses the Pebble storage engine. Pebble is intended to be bidirectionally compatible with the RocksDB on-disk format, but differs in that it is written in Go and implements a subset of RocksDB&apos;s large feature set.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;우선 Pebble은 RocksDB와 on-disk format 호환을 목표로 한다고 되어있습니다. 하지만 RocksDB의 경우 LevelDB와 API 레벨에서의 호환성만 얘기하지, on-disk format 에 대한 이야기는 찾아볼 수 없었습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/facebook/rocksdb/wiki&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://github.com/facebook/rocksdb/wiki&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It was developed at Facebook based on LevelDB and provides backwards-compatible support for LevelDB APIs.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이에 따라 on-disk format이 동일한 RocksDB 의 구조를 파악하기 시작합니다. 조금 다르긴 하지만 기본적인 구성은 LevelDB와 유사하며, 확장한 형태로 보였습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/facebook/rocksdb/wiki/Rocksdb-BlockBasedTable-Format&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://github.com/facebook/rocksdb/wiki/Rocksdb-BlockBasedTable-Format&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;&amp;lt;beginning_of_file&gt;
[data block 1]
[data block 2]
...
[data block N]
[meta block 1: filter block]                  (see section: &quot;filter&quot; Meta Block)
[meta block 2: index block]
[meta block 3: compression dictionary block]  (see section: &quot;compression dictionary&quot; Meta Block)
[meta block 4: range deletion block]          (see section: &quot;range deletion&quot; Meta Block)
[meta block 5: stats block]                   (see section: &quot;properties&quot; Meta Block)
...
[meta block K: future extended block]  (we may add more meta blocks in the future)
[metaindex block]
[Footer]                               (fixed size; starts at file_size - sizeof(Footer))
&amp;lt;end_of_file&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;실제 온전한 CRDB sst 파일들을 받아서 분석해 보기 시작합니다. hexdump 명령으로 보니까 내가 원하던 데이터가 들어있었습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2000px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 78.33333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAQCAYAAAAWGF8bAAAACXBIWXMAABYlAAAWJQFJUiTwAAACeUlEQVR42m1TyXLaQBAVm0BIIAkhsZt9CQVUsVkmzimWjY1dueSU//+IpJLDy7zGQ0xVDq+m1erlvZ4e48fXz7+T73fY3saIdzvE+x2ekge8nU54eTnh8Zjg9dsrnk/PSB4ecXw6ij9JHrHf314Qx3e4v//yx6iW6oiCJgLXRsOz0Y9ctLwCQteCWy4jaNnwmhkU/TTS6X/IZDJIpVJXyOVyMCbTKWazGcIwRLPZRKvdRr1eF9RqNfieD8/zLvB9X2DbtsQHQYAoilBWzdnIOBwOWK1WktxqtSRoMpmg0+mg1+tJYfq73RvxDYdDZXeFQKPRwHg8xna7RbVaPRekgwGlUkm68QeLsCuZaFb002YMv8mIRQnGuq6LbDYLY71eYz6fSwI7kilZ0qaPCR99BG0WIHOtgKcU3O/3WCwWlxkyQUvnyaK0KVf7aZPtYDAQdlTIPF6USOas2JHJmlWlUhF5HAUl0qZPy7Qs64otC18xpCx21gxpM1jLZVPNmmDcaDSSZtwSSubqGJvN5rI2bbUyWhIDeOrbpJ8s2IBxZMX/juOApBgvt8wV4Qwo6eOtaYlauh4HwaKUzH9kywb0yWLHcYzlcnnprIev5bOzHgMbE/zHeO7kVD0MNqF0uRTOr9/vv7Pj6tTPeH8pZBvVosvr0aBk3jILcRysIQVnn6a46XZQCdTtRjVVVCUrRiGLKVR8Jd2vip8N9EwpmczIXD8OKRi46r2WqyjbBXhFE4FjIizl4Vo52MUinIqJcmTC8U2ZEWGapiSzaFHFFAoF8UvBfnv808xayOcyyKcNFLNn5DMGsiogZxkwHWUXDBjG/yHrok61s7/+Au8zmCKB40B4AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/7ece48d07701241f6a100f31a51764e1/33ecc/04-hexdump-of-sst-file.webp 360w,
/static/7ece48d07701241f6a100f31a51764e1/263a4/04-hexdump-of-sst-file.webp 480w,
/static/7ece48d07701241f6a100f31a51764e1/0c8fb/04-hexdump-of-sst-file.webp 640w,
/static/7ece48d07701241f6a100f31a51764e1/521f7/04-hexdump-of-sst-file.webp 860w,
/static/7ece48d07701241f6a100f31a51764e1/cbd37/04-hexdump-of-sst-file.webp 1200w,
/static/7ece48d07701241f6a100f31a51764e1/fab06/04-hexdump-of-sst-file.webp 2000w&quot; sizes=&quot;(max-width: 2000px) 100vw, 2000px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/7ece48d07701241f6a100f31a51764e1/53918/04-hexdump-of-sst-file.png 360w,
/static/7ece48d07701241f6a100f31a51764e1/9aebd/04-hexdump-of-sst-file.png 480w,
/static/7ece48d07701241f6a100f31a51764e1/0f09e/04-hexdump-of-sst-file.png 640w,
/static/7ece48d07701241f6a100f31a51764e1/3e3fe/04-hexdump-of-sst-file.png 860w,
/static/7ece48d07701241f6a100f31a51764e1/64756/04-hexdump-of-sst-file.png 1200w,
/static/7ece48d07701241f6a100f31a51764e1/f97d7/04-hexdump-of-sst-file.png 2000w&quot; sizes=&quot;(max-width: 2000px) 100vw, 2000px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/7ece48d07701241f6a100f31a51764e1/f97d7/04-hexdump-of-sst-file.png&quot; alt=&quot;04 hexdump of sst file&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;/figure&gt;
&lt;p&gt;파일을 어떻게 파싱해서 우리가 원하는 데이터를 읽어올지 고민해 봅니다. sst 파일의 뒷부분에 metadata를 읽어야 data block들을 읽을 수 있습니다. 다만 실제 data block 들에 들어있는 CRDB의 row 데이터를 저장하는 방식도 파악이 필요해 보였습니다. 얘네들은 어떻게 구성되어 있는 걸까요?&lt;/p&gt;
&lt;figure style=&quot;width: 70%; margin: 0 auto;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2000px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 19.444444444444443%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAECAYAAACOXx+WAAAACXBIWXMAABYlAAAWJQFJUiTwAAAA8UlEQVR42jXM6U7CUBCG4d7/LRiM2zUY2SohoP4xAQqIUKXLaXvac2hLiLIEeW2rTPLkm0xmxqgPV7SsFfWBomlpzElKY5hgjv+yOVQ8TnMag5jWKKFtKTpvGWa5W+jO1rSL+f2r5D3aYlx1fO56IbfdIvuay/6ei+4Xtd6Om5cd189bak8HHkYbpraHtRCVsR1Uyn7mSGS2Y7M9YsztJZ+u+BdguxJbpMwdhe1pFq7mw5OISCMCydLxCAKB8P1KnmVwOnEuQ8oYpXRBIaUkTlZ46ohM9wR6j68OpGlGmOS46odIf1PehFFMvl5XT8p/Z7/P1CGVBI01RgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/87211bfd7d94912c673c976c0cfa3f1a/33ecc/05-crdb-column-family-documentation.webp 360w,
/static/87211bfd7d94912c673c976c0cfa3f1a/263a4/05-crdb-column-family-documentation.webp 480w,
/static/87211bfd7d94912c673c976c0cfa3f1a/0c8fb/05-crdb-column-family-documentation.webp 640w,
/static/87211bfd7d94912c673c976c0cfa3f1a/521f7/05-crdb-column-family-documentation.webp 860w,
/static/87211bfd7d94912c673c976c0cfa3f1a/cbd37/05-crdb-column-family-documentation.webp 1200w,
/static/87211bfd7d94912c673c976c0cfa3f1a/fab06/05-crdb-column-family-documentation.webp 2000w&quot; sizes=&quot;(max-width: 2000px) 100vw, 2000px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/87211bfd7d94912c673c976c0cfa3f1a/53918/05-crdb-column-family-documentation.png 360w,
/static/87211bfd7d94912c673c976c0cfa3f1a/9aebd/05-crdb-column-family-documentation.png 480w,
/static/87211bfd7d94912c673c976c0cfa3f1a/0f09e/05-crdb-column-family-documentation.png 640w,
/static/87211bfd7d94912c673c976c0cfa3f1a/3e3fe/05-crdb-column-family-documentation.png 860w,
/static/87211bfd7d94912c673c976c0cfa3f1a/64756/05-crdb-column-family-documentation.png 1200w,
/static/87211bfd7d94912c673c976c0cfa3f1a/f97d7/05-crdb-column-family-documentation.png 2000w&quot; sizes=&quot;(max-width: 2000px) 100vw, 2000px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/87211bfd7d94912c673c976c0cfa3f1a/f97d7/05-crdb-column-family-documentation.png&quot; alt=&quot;05 crdb column family documentation&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        &lt;a href=&quot;https://www.cockroachlabs.com/docs/stable/column-families.html&quot;&gt;Cockroach Labs Column Families&lt;/a&gt;
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;CRDB 문서를 읽어보니 Column Family라는 개념으로 K-V Store 저장되는 것을 확인했습니다. 구체적으로 어떻게 저장되는 걸까요? CRDB 소스코드 레포지토리 내에 있는 Design Document 들을 읽어보기 시작했습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/&amp;lt;tableID&gt;/&amp;lt;indexID&gt;/&amp;lt;primaryKeyColumns...&gt;/&amp;lt;columnID&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;소스코드를 탐색하다보니 실제로 저장에 사용되는 Protocol Buffer 파일을 발견합니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/cockroachdb/cockroach/blob/v21.1.19/pkg/roachpb/data.proto#L77&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://github.com/cockroachdb/cockroach/blob/v21.1.19/pkg/roachpb/data.proto&lt;/a&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;// Value specifies the value at a key. Multiple values at the same key are
// supported based on timestamp. The data stored within a value is typed
// (ValueType) and custom encoded into the raw_bytes field. A custom encoding
// is used instead of separate proto fields to avoid proto overhead and to
// avoid unnecessary encoding and decoding as the value gets read from disk and
// passed through the network. The format is:
//
//   &amp;lt;4-byte-checksum&gt;&amp;lt;1-byte-tag&gt;&amp;lt;encoded-data&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/cockroachdb/cockroach/blob/v21.1.19/pkg/roachpb/data.proto#L77&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;주석&lt;/a&gt;을 읽어보니 Value 값이 &lt;code&gt;&amp;#x3C;4-byte-checksum&gt;&amp;#x3C;1-byte-tag&gt;&amp;#x3C;encoded-data&gt;&lt;/code&gt; 형태로 저장되고 있습니다. 스토리지에 저장된 파일을 직접 파싱해보고 싶어집니다. 파싱하는 코드도 발견합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;go&quot;&gt;&lt;pre class=&quot;language-go&quot;&gt;&lt;code class=&quot;language-go&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;v Value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;PrettyPrint&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// ...&lt;/span&gt;
 &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; buf bytes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Buffer
 t &lt;span class=&quot;token operator&quot;&gt;:=&lt;/span&gt; v&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;GetTag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// ...&lt;/span&gt;

 &lt;span class=&quot;token keyword&quot;&gt;switch&lt;/span&gt; t &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
 &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; ValueType_TUPLE&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  b &lt;span class=&quot;token operator&quot;&gt;:=&lt;/span&gt; v&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;dataBytes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; colID &lt;span class=&quot;token builtin&quot;&gt;uint32&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;b&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// ...&lt;/span&gt;
   &lt;span class=&quot;token boolean&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; colIDDiff&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; typ&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; err &lt;span class=&quot;token operator&quot;&gt;:=&lt;/span&gt; encoding&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;DecodeValueTag&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;b&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;token comment&quot;&gt;// ...&lt;/span&gt;
   b&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; s&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; err &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; encoding&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;PrettyPrintValueEncoded&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;b&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;token comment&quot;&gt;// ...&lt;/span&gt;
   fmt&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Fprintf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;buf&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;%d:%d:%s/%s&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; colIDDiff&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; colID&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; typ&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; s&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// ...&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;코드를 쭉 읽어보면 앞서 주석에서 읽었던 순서대로 (checksum 은 생략하고) &lt;code&gt;&amp;#x3C;1-byte-tag&gt;&lt;/code&gt;를 읽고 case 문으로 Value Type 별로 별도의 파싱 루틴을 타서 예쁘게 출력을 하는 구조를 볼 수 있습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2000px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 12.5%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAADCAYAAACTWi8uAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAiElEQVR42j2POw4DIQxE6UGw/Fk+olkpRa6QMmXuf53JDhJbWLbG88YgWmvQWiOXguMwuF5vfL4/eGfRx4BSCiklnOcJ7/2qEALIOedgjHk6fWLcUIwR5Q6kqfW+4Jwzaq3LzJl7du6oE2aXUi5tHxUbYihFgpz5kn2ZGnc7hEWPtXb9bs75cH/mJEvWG6DypwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/cf8ebd29d4de6ce20b1886da18bf98d2/33ecc/06-hexdump-one-byte-tag.webp 360w,
/static/cf8ebd29d4de6ce20b1886da18bf98d2/263a4/06-hexdump-one-byte-tag.webp 480w,
/static/cf8ebd29d4de6ce20b1886da18bf98d2/0c8fb/06-hexdump-one-byte-tag.webp 640w,
/static/cf8ebd29d4de6ce20b1886da18bf98d2/521f7/06-hexdump-one-byte-tag.webp 860w,
/static/cf8ebd29d4de6ce20b1886da18bf98d2/cbd37/06-hexdump-one-byte-tag.webp 1200w,
/static/cf8ebd29d4de6ce20b1886da18bf98d2/fab06/06-hexdump-one-byte-tag.webp 2000w&quot; sizes=&quot;(max-width: 2000px) 100vw, 2000px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/cf8ebd29d4de6ce20b1886da18bf98d2/53918/06-hexdump-one-byte-tag.png 360w,
/static/cf8ebd29d4de6ce20b1886da18bf98d2/9aebd/06-hexdump-one-byte-tag.png 480w,
/static/cf8ebd29d4de6ce20b1886da18bf98d2/0f09e/06-hexdump-one-byte-tag.png 640w,
/static/cf8ebd29d4de6ce20b1886da18bf98d2/3e3fe/06-hexdump-one-byte-tag.png 860w,
/static/cf8ebd29d4de6ce20b1886da18bf98d2/64756/06-hexdump-one-byte-tag.png 1200w,
/static/cf8ebd29d4de6ce20b1886da18bf98d2/f97d7/06-hexdump-one-byte-tag.png 2000w&quot; sizes=&quot;(max-width: 2000px) 100vw, 2000px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/cf8ebd29d4de6ce20b1886da18bf98d2/f97d7/06-hexdump-one-byte-tag.png&quot; alt=&quot;06 hexdump one byte tag&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;/figure&gt;
&lt;p&gt;실제 데이터가 들어있는 sst 파일을 하나 hexdump로 열어봅니다. 우리가 관심 있는 데이터는 보통 string 형태로 저장된게 보입니다. 그 부근으로 이동하여 &lt;code&gt;&amp;#x3C;4-byte-checksum&gt;&amp;#x3C;1-byte-tag&gt;&amp;#x3C;encoded-data&gt;&lt;/code&gt; 형태의 바이너리 패턴이 보이는지 확인해 봅니다. Checksum을 건너뛰고, &lt;code&gt;1-byte-tag&lt;/code&gt; 섹션을 체크해 봅니다. 값이 &lt;code&gt;0x0a&lt;/code&gt;, 10진수로 &lt;code&gt;10&lt;/code&gt;입니다. &lt;code&gt;ValueType_TUPLE&lt;/code&gt;이네요. Tuple 은 내부 값을 어떻게 저장하고 있을까요? &lt;a href=&quot;https://github.com/cockroachdb/cockroach/blob/v21.1.19/pkg/roachpb/data.proto#L60-L61&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;주석&lt;/a&gt;을 확인해 보니 다음과 같이 설명합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;TUPLE represents a DTuple, encoded as repeated pairs of varint field number followed by a value encoded Datum.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;DTuple 이 뭔지는 모르겠으나 장애 상황이니 일단 넘어갑니다. 인코딩을 설명하는데 varint 타입으로 field number를 저장하고, 그 이후에는 다시 Value 인코딩이라는 얘기입니다. 인코딩된 데이터를 확인해 봅니다. 위 경우는 첫 번째 아이템은 필드 번호 &lt;code&gt;0x34&lt;/code&gt;이고 그다음에 다시 태그 값 &lt;code&gt;0x08&lt;/code&gt;이 나옵니다. 문서를 보면 &lt;code&gt;DELIMITED_BYTES&lt;/code&gt;라네요. DELIMITED가 붙어있지만 일단 BYTES라고 가정하고 파싱을 해봅시다. 그다음 바이트들을 체크해 봅니다. &lt;code&gt;0x01 0x12&lt;/code&gt;는 뭔지 잘 모르겠지만 그다음 &lt;code&gt;0x30&lt;/code&gt; 은 왠지 Byte의 길이를 나타내는 값이 나오는 것 같네요. 실제로 10진수로 48인데, 뒤에 나오는 아스키 값들을 카운팅 해보니 길이가 48이 맞습니다. &lt;a href=&quot;https://developers.google.com/protocol-buffers/docs/encoding#strings&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Protocol Buffer Encoding 문서&lt;/a&gt;를 보니 다음과 같이 설명합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A wire type of 2 (length-delimited) means that the value is a varint encoded length followed by the specified number of bytes of data.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Protocol Buffer의 varint 타입은 최대한 많은 숫자들을 압축해서 저장하기 위한 protocol buffer에서 고안된 기법입니다. 자세한 내용은 &lt;a href=&quot;https://developers.google.com/protocol-buffers/docs/encoding#varints&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Protocol Buffer Encoding 문서의 Base 128 Varints&lt;/a&gt;를 참고합니다. 필자의 경우도 이게 뭔지는 대략 알고 있었지만 기억이 확실하지 않게 때문에 문서를 확인하면서 파싱을 해봅니다. 파싱 중인 예시를 varint 기법에 맞게 파싱을 해보니 여전히 48이 나옵니다. Wire type 2 (LEN) 파싱 관련 문서 &lt;a href=&quot;https://developers.google.com/protocol-buffers/docs/encoding#length-types&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Length-Delimited Records&lt;/a&gt;의 예시를 보니 익숙한 패턴이 보입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;12 07 [74 65 73 74 69 6e 67]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;즉, &lt;code&gt;0x12&lt;/code&gt;는 Wire type 2 (LEN) 을 알려주는 태그이며, 그다음 나오는 값이 varint로 인코딩된 길이인 것이었습니다. 우리의 실제 데이터의 경우에는 &lt;code&gt;0x30&lt;/code&gt;, 즉 48이었던 것이죠. 여전히 &lt;code&gt;0x01&lt;/code&gt;의 의미를 모르겠는데, 일단 넘어갑니다.&lt;/p&gt;
&lt;p&gt;길이 이후에는 우리가 그토록 꺼내고 싶었던 저장된 데이터 값들이 보입니다. 희망이 생겼습니다. 이론적으로 파싱이 가능한 것에서, 실제로 손 파싱을 통해 데이터를 파싱해올 수 있음을 증명해냈습니다. 이제 데이터가 어디에 어떻게 저장되어 있는지 알고 있습니다. 7TB의 데이터를 전부 꺼내오는 방법을 고민하면 됩니다.&lt;/p&gt;
&lt;p&gt;장애 상황이기 때문에 빠르게 구현해야 했습니다. 사용자의 데이터이기 때문에 정확하기도 해야 합니다. 저장된 데이터가 약 7TB 정도 됩니다, 따라서 처리 속도도 좋아야 합니다. 가장 어렵다는 세 가지를 만족해야 했습니다: 빨리 만들고, 오류 없어야 하고, 성능도 좋아야 했던 것이죠. 우선순위를 따져봅니다. 우선 빠르게 구현 가능하면서 정확해야 합니다. 처리 성능은 데브시스터즈 데이터플랫폼셀의 데이터 분산처리 역량과 AWS의 클라우드 컴퓨팅 자원을 믿고 돈으로 성능을 살 수 있다고 판단했습니다.&lt;/p&gt;
&lt;p&gt;빠르고 정확하게 구현하기 위한 방법을 팀과 치열하게 고민했습니다. 다양한 아이디어가 나왔지만, 결과적으로 채택한 것은 소스코드를 읽어보면서 발견한 PrettyPrint 함수입니다. 이게 어디에서 사용되나 보니까 CLI 툴에서 디버깅을 위한 기능에서 사용됩니다. 결국 바이너리 sst 파일을 파싱해서 엔지니어가 읽을 수 있는 형태로 터미널에 출력해 주는 함수입니다. 그렇다면 이걸 변경해서 csv를 출력하게 하면 어떨까요?&lt;/p&gt;
&lt;p&gt;CockroachDB CLI 소스코드를 수정해 줄 사람을 찾아봅니다. 데브시스터즈에서 Go 언어에 가장 뛰어나고, 스트레스 상황 속에서도 손이 빠르고, 정확한 사람을 찾아야 합니다. 인프라셀 셀장님을 선정합니다. 스펙을 논의하여 명확히 하고 가칭 crdb2csv 프로그래밍 작업에 착수합니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2000px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 28.055555555555557%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAIAAABM9SnKAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAg0lEQVR42j2PiRFEIQhDacnRBVH6r2sD8ZNx9EnkUOZa7q6qY4w556+0CsxMS1YiII6zlsopIf/e23xaXxDae9M1A+J0iYhM85eGa5cgw3XfrMgG+dyTBQbKsDBFxlheSu5edSVjl65HIH594k10TtyewnsK4dP+8/PK7s8zp92criJ/rNNQClo7c1EAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/0bb98bab137ba245a629cdcd5cee7793/33ecc/07-crdb-example-csv.webp 360w,
/static/0bb98bab137ba245a629cdcd5cee7793/263a4/07-crdb-example-csv.webp 480w,
/static/0bb98bab137ba245a629cdcd5cee7793/0c8fb/07-crdb-example-csv.webp 640w,
/static/0bb98bab137ba245a629cdcd5cee7793/521f7/07-crdb-example-csv.webp 860w,
/static/0bb98bab137ba245a629cdcd5cee7793/cbd37/07-crdb-example-csv.webp 1200w,
/static/0bb98bab137ba245a629cdcd5cee7793/fab06/07-crdb-example-csv.webp 2000w&quot; sizes=&quot;(max-width: 2000px) 100vw, 2000px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/0bb98bab137ba245a629cdcd5cee7793/53918/07-crdb-example-csv.png 360w,
/static/0bb98bab137ba245a629cdcd5cee7793/9aebd/07-crdb-example-csv.png 480w,
/static/0bb98bab137ba245a629cdcd5cee7793/0f09e/07-crdb-example-csv.png 640w,
/static/0bb98bab137ba245a629cdcd5cee7793/3e3fe/07-crdb-example-csv.png 860w,
/static/0bb98bab137ba245a629cdcd5cee7793/64756/07-crdb-example-csv.png 1200w,
/static/0bb98bab137ba245a629cdcd5cee7793/f97d7/07-crdb-example-csv.png 2000w&quot; sizes=&quot;(max-width: 2000px) 100vw, 2000px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/0bb98bab137ba245a629cdcd5cee7793/f97d7/07-crdb-example-csv.png&quot; alt=&quot;07 crdb example csv&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;/figure&gt;
&lt;p&gt;필자는 그 사이에 7TB의 데이터를 어떻게 하면 분산처리하여 csv 파일을 만들 수 있을지 고민해 봅니다. 데이터 분산처리에 경험이 많은 데이터플랫폼셀 엔지니어 분들과 해당 방법에 대한 논의를 시작합니다. PySpark 샘플 코드를 작성해 보면서 crdb2csv 프로그램이 왔을 때를 대비하여 분산처리 준비를 해둡니다.&lt;/p&gt;
&lt;p&gt;프로토타입이 완성되어 이를 PySpark 코드를 통해 7TB의 sst 파일들에 대해서 돌려봅니다. 에러가 발생합니다. 에러 메시지를 공유하여 crdb2csv 소스코드를 디버깅하고 고칩니다. 이러한 작업을 여러 번 수행하고 나니 점점 완성도가 높아집니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;download_then_parse_sst&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;row&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token triple-quoted-string string&quot;&gt;&apos;&apos;&apos;
    파라미터로 입력된 S3 버킷에 있는 sst 파일을 다운받아 내용물을 crdb2csv로 변환하여 문자열 반환
    &apos;&apos;&apos;&lt;/span&gt;
    key_name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; row
    &lt;span class=&quot;token keyword&quot;&gt;with&lt;/span&gt; NamedTemporaryFile&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        Key&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Bucket&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;connect_s3&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; INPUT_BUCKET_NAME&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; key_name&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;get_contents_to_filename&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        run &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; subprocess&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Popen&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;CLI&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;crdb2csv&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; stdout&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;subprocess&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;PIPE&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; stderr&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;subprocess&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;DEVNULL&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; run&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;poll&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            line &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; run&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;stdout&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;readline&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;yield&lt;/span&gt; Row&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                key_name&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;key_name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                stdout&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;line&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;decode&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;utf-8&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;strip&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        output &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; run&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;communicate&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; line &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; output&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;decode&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;utf-8&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;split&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;\n&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;yield&lt;/span&gt; Row&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                key_name&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;key_name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                stdout&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;line&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;처리시간이 오래 걸립니다. 아무래도 csv 파일은 텍스트 포맷이고, stdout 출력물을 받으니 느릴 수밖에 없습니다. 예상되던 부분입니다. AWS 클라우드에서 컴퓨팅 자원을 끌어모을 수 있을 만큼 끌어모읍니다.&lt;/p&gt;
&lt;figure style=&quot;width: 60%; margin: 0 auto;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2000px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 76.11111111111111%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAIAAABr+ngCAAAACXBIWXMAAAsTAAALEwEAmpwYAAABl0lEQVR42o1SaU/jMBD1//9JoP24CIS65S6lB81tx27SNHYcn2WcQlVVwPZpFI3Hk5n3XoIWi+XF5Z/b29G/8d3fq+ssy713Sil9BtC6qiav0+n0Daa8zeaMMWOMOg/IObc7gt8DkkMeTrvD83DlvEdN00RxgjHJC/y+ilZRHCfpfLFcvq8gg2OSpPtiFMWYECiAtDTNCkwQbKdlWVdAf40xLgmp64pSWsAhTISeMssLuKaMVVUFFZDGKK03G6SMa7XfKsd1iK2yTW87HbT4YzlHuTCh0zqHtlI/FPwub19K8VqKZ8IfsZiUYi/Q+W+wrLr7gmtrkbF26AnhwrgQUNTG/BR26AGgjdTjnI+y9gGLVtnB0t3vmK/lOBefm6U2nTYSTtbtBgowGeo/hVC6UyZobnudNj3jCm97yhUdEqHMoOIb2PBfeOALKxFXZsbkpOyesJix7oV0z0TU0pw4fGJ7LQNfNJB1bm/Yl3P2V8OANrwZDAPao4yDZ+C+88Gt/xoWb/qbpAWPEKztDXCwMlhwLvTwuT4A9dVgpOw9IBgAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/19e90dd36a2f9ea19270a37276f5cda5/33ecc/08-aws-console-screenshot.webp 360w,
/static/19e90dd36a2f9ea19270a37276f5cda5/263a4/08-aws-console-screenshot.webp 480w,
/static/19e90dd36a2f9ea19270a37276f5cda5/0c8fb/08-aws-console-screenshot.webp 640w,
/static/19e90dd36a2f9ea19270a37276f5cda5/521f7/08-aws-console-screenshot.webp 860w,
/static/19e90dd36a2f9ea19270a37276f5cda5/cbd37/08-aws-console-screenshot.webp 1200w,
/static/19e90dd36a2f9ea19270a37276f5cda5/fab06/08-aws-console-screenshot.webp 2000w&quot; sizes=&quot;(max-width: 2000px) 100vw, 2000px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/19e90dd36a2f9ea19270a37276f5cda5/53918/08-aws-console-screenshot.png 360w,
/static/19e90dd36a2f9ea19270a37276f5cda5/9aebd/08-aws-console-screenshot.png 480w,
/static/19e90dd36a2f9ea19270a37276f5cda5/0f09e/08-aws-console-screenshot.png 640w,
/static/19e90dd36a2f9ea19270a37276f5cda5/3e3fe/08-aws-console-screenshot.png 860w,
/static/19e90dd36a2f9ea19270a37276f5cda5/64756/08-aws-console-screenshot.png 1200w,
/static/19e90dd36a2f9ea19270a37276f5cda5/f97d7/08-aws-console-screenshot.png 2000w&quot; sizes=&quot;(max-width: 2000px) 100vw, 2000px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/19e90dd36a2f9ea19270a37276f5cda5/f97d7/08-aws-console-screenshot.png&quot; alt=&quot;08 aws console screenshot&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;/figure&gt;
&lt;p&gt;기록을 확인하니 Spark 클러스터에만 r5.8xlarge 350대를 사용했습니다. CPU 11,200코어 / 메모리 89,600GB 정도 되는 사양입니다. 이마저도 AWS의 도쿄 리전 캐퍼시티를 다 사용해버려서 350대를 운용하는 데에 큰 어려움을 겪었습니다. 결과적으로 어떻게든 확보한 노드들로 7TB의 데이터를 CSV 파일로 변환하는 데만 4시간이 걸렸습니다. crdb2csv 프로그램의 오류를 발견하여 이 4시간이 소요되는 작업도 여러 번 반복합니다.&lt;/p&gt;
&lt;p&gt;변환한 파일에 모든 사용자 데이터가 담겨있는지 검증하는 작업도 꽤 오래 걸렸습니다. 데이터베이스가 사용하는 MVCC 구조상 필요 없는 사용자의 데이터가 다수 있었기 때문에 이를 제거하고, 최신 값을 뽑아내고, 뽑아낸 데이터를 백업본, 분석 로그와 교차 검증을 하면서 빠진 데이터가 없는지 체크를 진행했습니다. 결과적으로 뽑아낸 파일에 모든 사용자 데이터가 정확하게 담겨있음을 확인할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;이것들을 확인하는 중에 인프라 조직에서는 새로운 CockroachDB 클러스터를 준비하고 있었는데요. AWS 노드들을 다 끌어다 쓴 탓에, 해당 클러스터를 셋업하는 것도 여간 힘든 일이 아니었습니다. 결과적으로 클러스터 셋업이 완료되고, 확인이 완료된 CSV 파일을 클러스터에 적재하였습니다. 불러올 데이터가 크다 보니 예상외로 이것도 상당한 시간이 소요되었습니다.&lt;/p&gt;
&lt;p&gt;이렇게 다시 정상 작동하는 데이터베이스를 만들었고, 이를 전사적으로 테스트를 거쳐서 36시간 만에 서비스를 다시 오픈하게 되었습니다(사실 오랜 시간 기다려주신 고객분들의 열렬한 성원에 힘입어 추가적인 장애들이 발생했는데요. 이에 대해서는 다음 기회에 다뤄보도록 하겠습니다.).&lt;/p&gt;
&lt;p&gt;일련의 장애 복구 과정을 회고해 볼 때, 이러한 서비스 복구 방식은 전혀 일반적인 방식은 아니라는 생각이 들었습니다. 데이터베이스 업체에서 복구 불가 판정을 내렸으면, 이미 그 시점부터 백업본으로 서비스를 복구하는 소위 ‘백섭&apos;을 하는 게 일반적이었을 겁니다. 시간은 다소 오래 걸렸을지라도 데브시스터즈 엔지니어들이 이런 방식으로 장애를 복구한 것에는 다음과 같은 이유가 있다고 생각합니다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;고객에게 최고의 경험을 선사하는 게 우리의 미션이고, 이를 진심으로 실천하기 위해 노력했습니다. 그 누구도 낙담하고 포기하지 않았습니다.&lt;/li&gt;
&lt;li&gt;해커 문화를 가지고 있기 때문에 엔지니어들이 로우 레벨로 뛰어드는 것에 익숙했습니다. 데이터베이스 레벨부터 커널, VM 레벨까지 다양한 경험이 있는 엔지니어들이 있었습니다. 상호 지식공유를 통해 데이터베이스의 다양한 추상화 레벨에 대한 전방위적인 이해가 가능했습니다.&lt;/li&gt;
&lt;li&gt;남의 소스코드를 읽고 고치는 것에 익숙한 조직입니다. 오픈소스 프로젝트들을 매일 이용하고, 문제가 있으면 소스코드를 읽고, 필요하다면 직접 고치면서 기여하기 때문에 가능했습니다.&lt;/li&gt;
&lt;li&gt;데브시스터즈 산하 CTO 조직인 진저랩은 데이터와 인프라 관련 팀들이 한 조직에 속해있습니다. 일반적으로 분산처리 플랫폼은 지표 등의 데이터 분석에 주로 사용되는데요. 데이터와 인프라가 한 조직에 있다 보니 분산처리 역량으로 인프라 장애를 극복하는 특별한 시너지가 가능했습니다.&lt;/li&gt;
&lt;li&gt;확장성 있는 인프라를 다루는 경험, 대용량의 데이터를 분산처리하는 경험이 풍부했습니다. 물론 7TB 단위의 데이터베이스 바이너리 데이터를 이렇게 극한 상황에서 다루는 건 처음이었고, 이를 통해 더 많이 성장했습니다.&lt;/li&gt;
&lt;li&gt;장애 대응 경험이 풍부했기 때문에 Plan A/B/C를 동시에 가동하는 등 여러가지 복구 전략을 함께 진행했고, 실제로 두 가지 복구 전략을 혼합하여 완벽하게 장애 복구를 할 수 있었습니다.&lt;/li&gt;
&lt;li&gt;조를 나누어서 여러 가지 복구 전략을 동시 실행했기 때문에, 내가 실패하더라도 플랜 B를 실행하는 동료를 믿고 과감하게 앞으로 전진할 수 있는 용기를 낼 수 있었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;엔지니어로서 커리어의 최대 위기가 왔었고, 팀 분들과 함께 쌓아왔던 우리의 소중한 문화, 핵심가치, 경험 덕분에 이를 극복해낼 수 있었던 것 같습니다. 다시는 경험하고 싶지 않은 장애지만, 또 한편으로는 데브시스터즈의 팀워크가 가장 빛났던 순간이었던 것 같습니다. 뛰어난 동료분들과 최악의 장애를 함께 극복하는 경험은 평생 절대 잊을 수 없는 경험이었습니다. 저는 우리가 이 장애로 또 한 번 성장했음을 느낍니다.&lt;/p&gt;
&lt;p&gt;지금까지의 글이 흥미로우셨다면, 이러한 문화와 핵심가치가 공감이 되신다면, 저희와 함께 일해보시는 건 어떨까요? 채용의 문은 활짝 열려있습니다. 지향점이 같은 훌륭한 미래의 동료분들을 모십니다. 읽어주셔서 감사합니다!&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;a name=&quot;ft_1&quot; href=&quot;#ftsrc_1&quot;&gt;[1]&lt;/a&gt;: 쿠키런: 킹덤의 서버 구조는 &lt;a href=&quot;https://www.youtube.com/watch?v=FUEZibcZEkg&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;NDC 발표&lt;/a&gt;를 참고하면 됩니다&lt;/p&gt;
&lt;p&gt;&lt;a name=&quot;ft_2&quot; href=&quot;#ftsrc_2&quot;&gt;[2]&lt;/a&gt;: 다만 이론적으로 가능하다 해도, 장애 상황이라는 아주 특수한 상황 속에서 얼마나 걸릴지 알 수 없는 이런 접근을 선택하는 게 쉽지는 않았는데요. DB 직접 복구가 불가능할 때를 대비한 로그 기반의 복구를 Plan B로 잡고 데이터 직군 엔지니어들과 서버 개발자분들이 해당 작업을 진행하고 있었습니다. 내가 실패해도 이들이 성공할 거라는 믿음을 갖고 공격적으로 진행할 수 있었습니다. 자세한 이야기는 &lt;a href=&quot;../crk-launch-storage-postmortem&quot;&gt;다른 글&lt;/a&gt;에서.&lt;/p&gt;
&lt;p&gt;&lt;a name=&quot;ft_3&quot; href=&quot;#ftsrc_3&quot;&gt;[3]&lt;/a&gt;: 여담이지만 LevelDB도 구글의 전설적인 소프트웨어 엔지니어 Jeff Dean의 작품입니다&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[9가지 프로그래밍 언어로 배우는 개념: 4편 - 하이 레벨 언어와 동적 타입 언어]]></title><description><![CDATA[프로그래밍 언어들을 비교해가며 하이 레벨 언어와 동적 타입 언어를 알아보고 더 나은 코드를 작성하는 방법을 고민해봅니다.]]></description><link>https://tech.devsisters.com/posts/programming-languages-4-high-level-languages-and-dynamically-typed-languages</link><guid isPermaLink="false">https://tech.devsisters.com/posts/programming-languages-4-high-level-languages-and-dynamically-typed-languages</guid><pubDate>Thu, 12 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;프로그래밍-언어-시리즈&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%EC%96%B8%EC%96%B4-%EC%8B%9C%EB%A6%AC%EC%A6%88&quot; aria-label=&quot;프로그래밍 언어 시리즈 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;프로그래밍 언어 시리즈&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;/posts/programming-languages-1-type-theory&quot;&gt;9가지 프로그래밍 언어로 배우는 개념: 1편 - 타입 이론&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/programming-languages-2-polymorphism&quot;&gt;9가지 프로그래밍 언어로 배우는 개념: 2편 - 다형성&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/programming-languages-3-metaprogramming&quot;&gt;9가지 프로그래밍 언어로 배우는 개념: 3편 - 메타프로그래밍&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;9가지 프로그래밍 언어로 배우는 개념: 4편 - 하이 레벨 언어와 동적 타입 언어&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/programming-languages-5-concurrent-programming&quot;&gt;9가지 프로그래밍 언어로 배우는 개념: 5편 - 동시성 프로그래밍&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;서론&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%84%9C%EB%A1%A0&quot; aria-label=&quot;서론 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;서론&lt;/h2&gt;
&lt;p&gt;안녕하세요, 킹덤 서버 소프트웨어 엔지니어 지민규입니다. 이번에는 아키텍처 디자인에 있어 하이 레벨 언어와 동적 타입 언어에 대한 저의 견해들을 나누기 위해 글을 쓰게 되었습니다.&lt;/p&gt;
&lt;h2 id=&quot;하이-레벨-언어&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%98%EC%9D%B4-%EB%A0%88%EB%B2%A8-%EC%96%B8%EC%96%B4&quot; aria-label=&quot;하이 레벨 언어 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;하이 레벨 언어&lt;/h2&gt;
&lt;p&gt;프로그래밍 언어를 비교할 때, 하이 레벨과 로우 레벨로 나눌 수 있습니다. 다만 이 기준은 상대적인 경우가 많습니다. 가령 어셈블리와 C를 비교했을 때 어셈블리 언어는 로우 레벨, C는 하이 레벨이라고 할 수 있고 C와 Javascript를 비교했을 때는 C는 로우 레벨, Javascript를 하이 레벨이라고 할 수 있죠.&lt;/p&gt;
&lt;p&gt;이 글에서는 메모리를 직접적으로 관리하는지 혹은 **가비지 컬렉터(Garbage Collector)**를 사용하여 관리하는지에 따라 로우 레벨, 하이 레벨로 나눕니다. 직접 메모리를 할당하고 풀어줘야하는 C는 로우 레벨 언어이고 이를 가비지 컬렉터가 자동적으로 관리해주는 Javascript은 하이 레벨 언어이죠. 로우 레벨과 메모리 관리의 안전성은 관련 없습니다. C++에서는 **RAII(Resource Acquisition is Initialization)**를 통해서 메모리 관리를 조금 더 안전하게 만들었지만 여전히 로우 레벨 언어로 취급됩니다. Rust는 특유의 레퍼런스 관리 시스템인 **버로우 체커(Borrow Checker)**를 통해 메모리 관리의 위험성을 거의 제거 했음에도 로우 레벨 언어입니다.&lt;/p&gt;
&lt;p&gt;로우 레벨 언어와 하이 레벨 언어가 어떠한 차이를 보여주는지는 누구나 잘 알고 있습니다. 로우 레벨 언어는 일반적으로 성능이 더 좋지만 번거롭고 하이 레벨 언어는 일반적으로 성능이 상대적으로 낮은 대신에 메모리 관리에 대해 고민하지 않아도 됩니다. 가비지 컬렉터가 어떠한 장단점이 있는 지도 꽤 재밌는 주제이지만, 이 글에서는 메모리 관리에 대한 번거로움이나 성능이 아닌, 아키텍처 디자인에 있어 하이 레벨 언어들이 어떠한 장점들이 있는지를 집중해보려고 합니다.&lt;/p&gt;
&lt;h3 id=&quot;모든-것은-레퍼런스&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AA%A8%EB%93%A0-%EA%B2%83%EC%9D%80-%EB%A0%88%ED%8D%BC%EB%9F%B0%EC%8A%A4&quot; aria-label=&quot;모든 것은 레퍼런스 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;모든 것은 레퍼런스&lt;/h3&gt;
&lt;p&gt;하이 레벨 언어의 가장 큰 특징을 꼽아본다면 대부분의 타입들이 레퍼런스 형태로 저장된다는 것입니다. 흔히 Java에서 “모든 클래스 인스턴스는 레퍼런스로 저장된다”라는 말을 들어보셨을 수도 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Java 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Human&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Human 인스턴스에 대한 레퍼런스를 human1에 저장한다&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;Human&lt;/span&gt; human1 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Human&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Mango&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// human1에 저장된 레퍼런스를 복사하여 human2에 저장한다&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;Human&lt;/span&gt; human2 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; human1&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// human2에 저장된 레퍼런스를 따라가서 Human 인스턴스의 name 필드를 수정한다&lt;/span&gt;
human2&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Tango&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// human1에 저장된 레퍼런스를 따라가서 Human 인스턴스의 name 필드를 출력한다&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 여기서 human1에 저장된 레퍼런스와 human2에 저장된 레퍼런스는&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 같은 인스턴스에 대한 레퍼런스 이므로&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Tango가 출력된다&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;System&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;out&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;human1&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Java에서 클래스 인스턴스를 만들려면 &lt;code&gt;new&lt;/code&gt;를 해야 합니다. 그렇게 만들어진 인스턴스는 변수에 저장할 때 레퍼런스로 저장되죠. 다른 변수에 레퍼런스를 대입할 때는 인스턴스가 복사되는 것이 아닌 레퍼런스가 복사됩니다. 위 예시에서는 human2에 human1을 대입했기 때문에 이제 두 변수는 같은 레퍼런스를 가지게 됩니다. human1을 통해 인스턴스를 수정하는 것과 human2를 통해 인스턴스를 수정하는 것은 같은 작업입니다.&lt;/p&gt;
&lt;p&gt;C/C++에서 Struct 변수 대입은 다릅니다. C/C++에서는 기본적으로 인스턴스를 복사하게 됩니다. 이러한 인스턴스 자체를 저장하는 타입을 값 타입이라고 합시다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Human&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  string name&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Human 인스턴스를 human1에 저장한다&lt;/span&gt;
Human human1 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Mango&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// human1에 저장된 인스턴스를 복사하여 human2에 저장한다&lt;/span&gt;
Human human2 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; human1&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// human2에 저장된 인스턴스의 필드를 수정한다&lt;/span&gt;
human2&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Tango&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// human1과 human2는 다른 인스턴스를 저장하고 있으므로&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// human2의 필드 수정이 human1에 영향을 끼치지 않는다&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 따라서 Mango가 출력된다&lt;/span&gt;
cout &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; human1&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; endl&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;C++에서는 레퍼런스를 저장하고 싶다면 레퍼런스 타입을 사용해야 합니다. 위 예시에서 변수의 타입을 &lt;code&gt;Human&lt;/code&gt;에서 &lt;code&gt;Human&amp;#x26;&lt;/code&gt;로 바꾸어야 하죠&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Human&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  string name&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

Human&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt; human1 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Mango&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 사실 Human&amp;amp;&amp;amp;를 써야한다 (C++ rvalue reference 참조)&lt;/span&gt;
Human&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt; human2 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; human1&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
human2&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Tango&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Tango가 출력된다&lt;/span&gt;
cout &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; human1&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; endl&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;로우 레벨 언어에서 레퍼런스 타입이 아닌 값 타입을 사용하는 이유는 성능 최적화와 메모리 관리의 간편성 때문입니다. 레퍼런스 타입은 레퍼런스를 “따라가는” 연산이 필요하지만 값 타입은 그러한 작업이 필요 없고 메모리를 할당하고 풀 필요가 없기 때문입니다.&lt;/p&gt;
&lt;p&gt;물론 하이 레벨 언어도 C#이나 Go 같이 값 타입을 지원해주는 경우도 있습니다. Java도 int나 float 같은 기초 타입들은 값 타입으로 취급되기도 하고요. 하지만 로우 레벨 언어에 비하면 레퍼런스 타입을 활용하는 빈도가 더 높습니다.&lt;/p&gt;
&lt;h3 id=&quot;타입-호환-문제&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%83%80%EC%9E%85-%ED%98%B8%ED%99%98-%EB%AC%B8%EC%A0%9C&quot; aria-label=&quot;타입 호환 문제 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;타입 호환 문제&lt;/h3&gt;
&lt;p&gt;값 타입은 단점이 하나 있습니다. 값 타입 변수들은 제각각 다른 크기의 메모리를 요구합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드, 64비트 환경&lt;/span&gt;
cout &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;sizeof&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; endl&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// int의 값 타입, 4가 출력된다&lt;/span&gt;
cout &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;sizeof&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; endl&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// int의 값 타입, 8이 출력된다&lt;/span&gt;

cout &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;sizeof&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; endl&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// int의 레퍼런스 타입, 8이 출력된다&lt;/span&gt;
cout &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;sizeof&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; endl&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// long의 레퍼런스 타입, 8이 출력된다&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;레퍼런스 타입은 일반적으로 32비트 환경에서 4바이트, 64비트 환경에서 8바이트를 차지합니다. 따라가야 할 인스턴스의 메모리 주소를 저장해야 하기 때문이죠. 대상 인스턴스 타입의 크기가 4바이트든 8바이트든, 몇백 메가바이트가 되든 64비트 환경에서는 메모리 주소를 저장할 8바이트만 있으면 됩니다.&lt;/p&gt;
&lt;p&gt;여기서 생겨나는 문제점이 뭘까요? 바로 업캐스팅 및 다운캐스팅에 제약이 걸린다는 것입니다. &lt;a href=&quot;/posts/programming-languages-2-polymorphism&quot;&gt;다형성 편&lt;/a&gt;과 &lt;a href=&quot;/posts/programming-languages-3-metaprogramming&quot;&gt;메타프로그래밍 편&lt;/a&gt;에서 업캐스팅과 다운캐스팅에 대해 언급했었는데요, 간략하게 설명하자면 타입의 계층 간 타입 캐스팅입니다. 상위(부모) 타입으로 캐스팅하면 업캐스팅이고 하위(자식) 타입으로 캐스팅하면 다운캐스팅입니다. 서로 요구하는 메모리 공간이 다를 수도 있는 타입끼리의 캐스팅이기 때문에 제약이 걸립니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 4 바이트 크기의 struct&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Fruit&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; weight&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 8(4 + 4) 바이트 크기의 struct&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Apple&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token base-clause&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Fruit&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; redness&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

Apple apple &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;280&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 4 바이트 크기의 변수에 8 바이트 크기의 변수를 대입했다?&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// redness 필드는 버린 상태로 복사한다&lt;/span&gt;
Fruit fruit &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; apple&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 8 바이트 크기의 변수에 4 바이트 크기의 변수를 대입했다?&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// redness 필드를 채울 방법이 없으므로 컴파일 에러&lt;/span&gt;
Apple apple2 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fruit&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;어떻게 보면 당연한 것이기도 합니다. 레퍼런스 타입들은 같은 메모리 주소를 담는 타입들이기에 계층 구조간 캐스팅이 자유롭지만, 값 타입들은 같은 계층 구조에 속하더라도 서로 다른 타입이기에 캐스팅 시 일부 정보를 버려야 하거나 불가능합니다.&lt;/p&gt;
&lt;p&gt;여기서 비롯되는 실질적인 문제는 어떤 것들이 있을까요? 바로 &lt;a href=&quot;/posts/programming-languages-2-polymorphism&quot;&gt;다형성 편&lt;/a&gt;에서 언급하였던 Variance가 적용되지 않는다는 것입니다. Variance는 업캐스팅을 기반으로 작동하므로 애초에 제네릭 타입 &lt;code&gt;T&lt;/code&gt;가 업캐스팅 될 수 없다면 Variance 규칙을 만족할 수 없겠죠. 그렇기 때문에 값 타입을 사용하였을 경우 Variance를 활용하기 어렵습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;csharp&quot;&gt;&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code class=&quot;language-csharp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C# 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Covariant한 T&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;IEnumerable&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;out&lt;/span&gt; T&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 값 타입&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Human&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;string&lt;/span&gt;&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 내부적으로 Human 배열을 가진 List를 인터페이스로 캐스팅한다&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;IEnumerable&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;Human&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; humans &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token constructor-invocation class-name&quot;&gt;List&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;Human&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 내부의 Human 배열과 object 배열의 요소 단위 크기가 호환되지 않는다&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// object이 Human의 상위 타입이라서 Covariant 규칙을 만족하지만 컴파일 실패한다&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;IEnumerable&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; objects &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; humans&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 레퍼런스 타입인 string은 잘되는데...&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;IEnumerable&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; strings &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token constructor-invocation class-name&quot;&gt;List&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;IEnumerable&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; objects &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; strings&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;C#에서 &lt;code&gt;struct&lt;/code&gt;의 상속을 금지하는 이유도 여기에 포함되어있을 겁니다. &lt;code&gt;struct&lt;/code&gt;의 상속을 허용하여 타입 계층 구조를 확장할 수 있도록 한다면 &lt;code&gt;class&lt;/code&gt;의 Variance 규칙과 &lt;code&gt;struct&lt;/code&gt;의 Variance 규칙이 달라져 언어의 일관성이 떨어집니다. 언어의 복잡도가 늘어난다는 것이죠&lt;/p&gt;
&lt;p&gt;C#을 자주 다뤄보신 분이라면 뭔가 이상한 점을 눈치챘을지도 모릅니다. &lt;code&gt;struct&lt;/code&gt; 타입들은 &lt;code&gt;object&lt;/code&gt; 타입에 대입할 수 있거든요. 과연 무슨 일이 일어나는 것일까요?&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;csharp&quot;&gt;&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code class=&quot;language-csharp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C# 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 값 타입&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Human&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;string&lt;/span&gt;&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token class-name&quot;&gt;Human&lt;/span&gt; human &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token constructor-invocation class-name&quot;&gt;Human&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 값 타입인 Human은 다른 타입으로 캐스팅 될 수 없는 거 아니었나?&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;object&lt;/span&gt;&lt;/span&gt; humanObject &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; human&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;사실 이건 단순한 캐스팅이 아닙니다. &lt;strong&gt;Boxing&lt;/strong&gt;을 하고 있는 것이죠. Boxing이란 새로운 메모리를 할당한 뒤 기존 메모리를 복사하는 것을 말합니다. 위 예시에서는 &lt;code&gt;Human&lt;/code&gt; 값 타입을 위한 메모리 공간을 마련하고 복사한 뒤 그 메모리에 대한 레퍼런스를 &lt;code&gt;object&lt;/code&gt; 레퍼런스 타입에 대입한 것입니다. 값 타입을 레퍼런스 타입으로 변화시켜 캐스팅을 가능하게 만든 것이죠.&lt;/p&gt;
&lt;p&gt;Rust를 사용해보신 분들이라면 Boxing이라는 단어가 익숙하실 겁니다. 바로 &lt;code&gt;Box&amp;#x3C;T&gt;&lt;/code&gt; 타입이죠.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;rust&quot;&gt;&lt;pre class=&quot;language-rust&quot;&gt;&lt;code class=&quot;language-rust&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Rust 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Box에 대한 정말 대략적인 구현&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;pub&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token type-definition class-name&quot;&gt;Box&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  value&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;pub&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token type-definition class-name&quot;&gt;Human&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  name&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; human&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Box&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Human&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Box&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Human&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Mango&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;to_owned&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; any&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Box&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;dyn&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; human&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;Box&amp;#x3C;T&gt;&lt;/code&gt;는 &lt;code&gt;T&lt;/code&gt;타입을 요구하지만 실제로는 필드로써 레퍼런스 타입인 &lt;code&gt;*mut T&lt;/code&gt;를 들고 있고 값 타입인 &lt;code&gt;T&lt;/code&gt;를 가지고 있지 않습니다. 따라서 &lt;code&gt;Box&amp;#x3C;T&gt;&lt;/code&gt;의 &lt;code&gt;T&lt;/code&gt;는 Variance 규칙이 적용될 수 있습니다. 위 예시에서는 &lt;code&gt;Box&amp;#x3C;Human&gt;&lt;/code&gt;을 Covariance를 이용하여 &lt;code&gt;Box&amp;#x3C;dyn Any&gt;&lt;/code&gt;로 바꿀 수 있었습니다.&lt;sup&gt;&lt;a name=&quot;ftsrc_1&quot; href=&quot;#ft_1&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;값 타입을 자주 활용하는 로우 레벨 언어에서는 Variance를 자주 활용하기 어렵습니다. C++에서는 아예 지원하지 않고 Rust에서는 특정 타입에 대해서만 사용 가능하다거나 많은 제약 조건이 걸려 있습니다. 이 때문에 거의 모든 것을 레퍼런스 타입으로 저장하는 하이 레벨 언어에 비해 표현력이 더 떨어질 수 있습니다. 성능을 위해 값 타입을 사용한 댓가이죠. 그렇기 때문에 현대적인 로우 레벨 언어에서는 C++의 템플릿이나 Rust의 매크로 같은 막강한 메타프로그래밍 기능을 제공하여 대체적인 방안을 제공하는 경우가 많은 것 같습니다.&lt;/p&gt;
&lt;h2 id=&quot;동적-타입-언어&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8F%99%EC%A0%81-%ED%83%80%EC%9E%85-%EC%96%B8%EC%96%B4&quot; aria-label=&quot;동적 타입 언어 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;동적 타입 언어&lt;/h2&gt;
&lt;p&gt;동적 타입 언어는 변수나 필드의 타입이 고정되어있지 않으며 런타임에 타입이 변경될 수 있는 언어를 가리킵니다. 동적 타입 언어인 Javascript에서는 String이었던 변수를 number, object로 바꿀 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Javascript 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; value &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;Mango&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// String 대입&lt;/span&gt;
value &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;7337&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// number 대입&lt;/span&gt;
value &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;Tango&apos;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// object 대입&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이걸 위의 타입 호환 문제와 엮어서 잘 생각해보시면 뭔가 느낌 오는 것이 있을 겁니다. 그렇습니다, 동적 타입 언어의 모든 타입은 레퍼런스 형태로 저장됩니다. 실제로는 성능 최적화가 적용되었기 때문에 &lt;code&gt;number&lt;/code&gt;와 같은 기초 타입들은 레퍼런스가 아닌 채로 저장되기도 하지만 레퍼런스 타입처럼 동작합니다. 레퍼런스 타입을 많이 활용하는 만큼 대다수의 동적 타입 언어들은 가비지 컬렉터 또한 사용하고 있는데요, 자연스럽게 동적 타입 언어는 하이 레벨 언어의 장단점을 물려받게 됩니다.&lt;/p&gt;
&lt;p&gt;동적 타입이 가져오는 문제점은 잘 아실 것이라고 생각합니다. 바로 타입 오류가 런타임에 발생한다는 것과 문서화가 필수라는 것입니다. 이로 인해 null 문제도 발생한다는 문제점도 존재합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Javascript 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 과연 무슨 타입을 입력 받고 출력할까...&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;evaluateGrade&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;score&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;동적 타입 언어에서는 변수나 인수에 모든 타입을 넣을 수 있다고 해서 모든 타입을 넣어도 작동하도록 함수를 짜지 않습니다. 각 함수나 클래스마다 적절한 타입을 요구하도록 구현합니다. Javascript에서는 함수에서 출력 타입이나 인수 타입을 명시적으로 적지 않기 때문에 오로지 함수 이름과 인수 이름만으로 인수 타입과 출력 타입을 예상해야 합니다.&lt;/p&gt;
&lt;p&gt;프로젝트 규모가 작을 때는 큰 문제가 되지 않지만 프로젝트의 규모가 커지고 복잡도가 높아질수록 주석을 통해서 문서화하는 것이 반쯤 필수적입니다. 정적 타입 언어에서는 명시적으로 적는 타입이 그 역할을 했었지만, 동적 타입 언어에서는 코멘트들로 처리해야 합니다. 어느 정도 동적 타입 언어의 장점을 포기하는 일이지만, 다른 방법이 없습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Javascript 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;/**
 * @param {number} score - 0~100점 대의 점수
 * @return {number} 1~5점 대의 등급
 */&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;evaluateGrade&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;score&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;만약 잘못된 타입을 넘기게 되면 해당 함수 호출을 실행하기 전까지 버그가 있는지 알 수 없습니다. 이 문제를 완화 시키려면 테스트를 통해 수정 후 실행까지의 간격을 줄여야 합니다.&lt;/p&gt;
&lt;p&gt;한 가지 더 아쉬운 점이라면 타입의 흐름을 확인하기 어렵다는 것입니다. 어디에서 생성된 타입이며 어떠한 흐름으로 어떤 함수에 도달하는지 파악하기 어렵다는 것이죠. 이는 디버깅이나 신규 인원이 코드를 파악할 때 좀 더 어렵게 만드는 요소 중 하나입니다.&lt;/p&gt;
&lt;p&gt;이런 문제점들을 넘어간다면 풍부한 표현력을 가진 언어가 됩니다. 타입클래스나 Variance, 메타프로그래밍 지원이 약한 정적 타입 언어들에 비해서는 할 수 있는 것이 많은 언어가 되죠.&lt;/p&gt;
&lt;h3 id=&quot;합타입&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%A9%ED%83%80%EC%9E%85&quot; aria-label=&quot;합타입 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;합타입&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;/posts/programming-languages-1-type-theory&quot;&gt;타입 이론 편&lt;/a&gt;에서 소개해 드렸던 합 타입에 대해서 생각해봅시다. 당연하게도 모든 타입이 합 타입입니다. 같은 변수에 여러 타입을 대입할 수 있고 컴파일러가 그걸 거부하지 않기 때문이죠.&lt;/p&gt;
&lt;p&gt;다만 아쉬운 점은 합 타입과 함께 자주 사용되는 완전 패턴 매칭 또한 사용할 수 없습니다. 이론적으로 매칭의 대상이 되는 값이 모든 타입이 될 수 있는데 모든 타입에 대응되는 케이스들을 작성할 수 없기 때문입니다. 그렇다고 default 케이스를 작성하거나 그냥 무시한다면 합 타입에 새 타입을 추가했을 때 런타임 에러가 발생할 가능성이 높습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Javascript 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;applyDiscount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;originalPrice&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; discount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Javascript에서는 패턴 매칭이 없어서 간접적으로 해야한다&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 여기서는 클래스 이름으로 매칭한다&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;discount&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;constructor&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;FixedDiscount&apos;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;originalPrice &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; discount&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;amount&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;RatioDiscount&apos;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;originalPrice &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; discount&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ratio&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 실수로 하나 빠트렸다&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 이로 인해 발생하는 에러는 실행전까지 볼 수 없다&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// case &apos;Voucher&apos;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;//   return 0&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;완전 패턴 매칭이 존재하는 언어에서는 케이스를 빠트리면 컴파일 에러가 납니다. 그렇기 때문에 합 타입에 새 타입을 추가해도 수정이 강제되기 때문에 안전하죠. 그렇기 때문에 동적 타입 언어에서 합타입이 사용 가능하다 한들 부주의하게 사용할 경우 버그로 이어질 가능성이 높습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Scala 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; applyDiscount&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;originalPrice&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; discount&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Discount&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 완전 패턴 매칭&lt;/span&gt;
  discount &lt;span class=&quot;token keyword&quot;&gt;match&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; Fixed&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;amount&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;=&gt;&lt;/span&gt; max&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;originalPrice &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; amount&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; Ratio&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ratio&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;token keyword&quot;&gt;=&gt;&lt;/span&gt; max&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;originalPrice &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; ratio&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 실수로 하나 빠트렸다&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 그렇지만 컴파일 할 때 컴파일 에러가 잡아준다&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// case Voucher    =&gt; 0&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;덕-타이핑&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8D%95-%ED%83%80%EC%9D%B4%ED%95%91&quot; aria-label=&quot;덕 타이핑 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;덕 타이핑&lt;/h3&gt;
&lt;p&gt;정적 타입 언어에 인터페이스, 타입클래스, 제네릭이 있다면 동적 타입 언어에는 **덕 타이핑(Duck Typing)**이 있습니다. 덕 타이핑은 서로 다른 타입들이 가진 메소드와 필드들이 같다는 것을 이용해 다형성을 이루는 것입니다. 위의 Discount 예시를 덕 타이핑 방식으로 해결한다면 다음과 같습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Javascript 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;FixedDiscount&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;applyDiscount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;originalPrice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;originalPrice &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;amount&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;RatioDiscount&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;applyDiscount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;originalPrice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;originalPrice &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ratio&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;VoucherDiscount&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;applyDiscount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;originalPrice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; originalPrice &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;15000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; discount &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getDiscount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
discount&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;applyDiscount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;originalPrice&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;여기서 중요한 점은 각 Discount 타입들이 공통 부모를 가졌거나 특정 인터페이스를 지정하고 있지 않다는 것 입니다. 그저 메소드가 같은 이름을 공유할 뿐입니다. 실제로 넘겨지는 인수가 어떤 타입인지, 같은 타입을 출력하고 있는지도 신경 쓰지 않습니다. 이 점에서 덕 타이핑은 정적 타입 언어의 인터페이스와 제네릭이 담당하는 다형성 영역을 커버할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/posts/programming-languages-2-polymorphism&quot;&gt;다형성 편&lt;/a&gt;에서 인터페이스의 2가지 한계점에 대해 다뤘습니다. 당시에 제시했던 문제점은 다음 두 가지입니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;자기 자신의 타입을 사용하는 메소드를 선언할 수 없다.&lt;/li&gt;
&lt;li&gt;정적 메소드를 추상 메소드로 선언할 수 없다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;덕 타이핑은 추상 메소드 선언을 기반으로 하고 있지 않기 때문에 이 두 가지 문제점을 회피할 수 있습니다. 인수 타입이나 출력 타입을 신경 쓰지 않기 때문에 자기 자신 타입이든 그 어떤 타입이든 사용할 수 있으며, 정적 메소드로도 덕 타이핑이 가능하기 때문에 추상 메소드의 기능을 수행할 수 있습니다. 이 점에서 타입클래스의 역할도 수행할 수 있다는 것을 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;동적 타입 언어에서 모든 타입은 호환되기 때문에 Variance의 영향 또한 받지 않습니다. 다만 이점은 오히려 단점이라고도 볼 수 있는 것이, Variance 규칙을 만족하지 않는 타입 캐스팅 또한 허용하기 때문에 주의를 기울여서 캐스팅해야 합니다.&lt;/p&gt;
&lt;p&gt;이렇게 동적 타입 언어에서는 덕 타이핑 하나로 다형성이 전부 처리됩니다. 이 모든 게 가능한 이유는 동적 타입 언어가 타입 안정성을 가지고 있지 않기 때문입니다. 만약 잘못된 타입으로 덕 타이핑을 시도하면 런타임 에러로 받게 되죠. 인터페이스와 달리 명시적인 타입이 존재하지 않아 덕 타이핑으로 묶인 타입들을 추적하기 어렵다는 단점도 있습니다. 그렇기 때문에 프로젝트의 복잡도가 상승할수록 덕 타이핑은 더욱 조심히 사용해야 합니다.&lt;/p&gt;
&lt;p&gt;재밌는 것은 C++ 템플릿으로도 덕 타이핑이 가능하다는 것입니다. 타입을 치환하는 방식으로 처리하는 C++ 템플릿 특성상 함수나 필드를 이름으로 호출하는 것이 가능합니다. 심지어 동적 타입 언어와 다르게 타입 안정성도 가져갈 수 있습니다. 이는 C++ 템플릿이 메타프로그래밍의 특성도 띠고 있기 때문에 가능한 것으로, 다른 언어로도 메타프로그래밍을 활용하면 덕 타이핑을 할 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;리플렉션&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A6%AC%ED%94%8C%EB%A0%89%EC%85%98&quot; aria-label=&quot;리플렉션 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;리플렉션&lt;/h3&gt;
&lt;p&gt;동적 타입 언어는 리플렉션 기능이 항상 존재합니다. 동적 타입 언어 그 자체가 리플렉션을 이용하여 다형성 코드들을 실행하기 때문입니다. 동적 타입 언어의 합타입과 덕 타이핑에 대해서 고민해봅시다. 변수에 그 어떤 타입도 대입할 수 있다는 이야기는 최소한 대입된 타입이 무엇인지 런타임에도 알고 있다는 것입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Javascript 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 50% 확률로 string이 대입되고 나머지 50% 확률로 object가 대입된다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;random&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; value &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;Mango&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; value &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;score&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 결과적으로 무슨 타입일지 컴파일 타임에 알 방법이 없다&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 그렇다는 것은 타입을 런타임에 저장된 타입을 출력하고 있다는 것이다&lt;/span&gt;
console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;typeof&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그렇기 때문에 리플렉션의 타입 성찰 조건을 갖추고 있다는 것을 알 수 있습니다. 필드나 메소드는 어떨까요?&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Javascript 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; value &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 50% 확률로 name 필드에 대입한다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;random&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;Mango&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 필드의 존재 여부 또한 런타임에 정해진다&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 그렇다는 것은 필드 존재 여부 또한 런타임에 저장되어있는 정보를 이용하는 것이다&lt;/span&gt;
console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그렇습니다. 정적 타입 언어는 리플렉션 기능이 없어도 구현 가능하기 때문에 리플렉션 유무가 갈리지만, 동적 타입 언어는 각종 언어 기능의 작동 원리부터가 리플렉션이기 때문에 리플렉션이 거의 항상 제공됩니다. 각종 필드를 이름으로 찾는 것과 필드들을 순회하는 것은 정적 타입 언어에 비해 매우 쉽습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Javascript 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; human &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;mango&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;getName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// human의 필드를 이름으로 찾는다. mango가 출력된다&lt;/span&gt;
console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;human&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;name&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// human의 필드와 메소드들을 순회한다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; prop &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; human&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// name과 getName이 출력된다&lt;/span&gt;
  console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;prop&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;동적-타입-생성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8F%99%EC%A0%81-%ED%83%80%EC%9E%85-%EC%83%9D%EC%84%B1&quot; aria-label=&quot;동적 타입 생성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;동적 타입 생성&lt;/h3&gt;
&lt;p&gt;동적 타입 언어는 런타임에 타입을 생성하고 수정할 수 있습니다. 모든 타입과 메소드들을 컴파일 타임에 알고 있을 필요가 없기 때문입니다. 이를 이용하면 클래스 타입을 특정 로직에 따라 생성하고 마음대로 메소드를 추가하고 조작할 수 있다는 것이죠.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Javascript 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 주어진 필드명들로 생성자 및 print 메소드들을 추가한 클래스 타입을 만드는 함수&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createClassWithPrinters&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;fields&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 필드들을 인수로 받는 클래스 타입 생성&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; clazz &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;constructor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;params&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;index&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; field&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; fields&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;entries&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;field&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; params&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;index&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 클래스에 print 메소드 추가하기&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; field &lt;span class=&quot;token keyword&quot;&gt;of&lt;/span&gt; fields&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    clazz&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;prototype&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;print_&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;field&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;field&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; clazz&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 런타임 타입 생성&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; Human &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createClassWithPrinters&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;name&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;age&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;role&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; mango &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Human&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;mango&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;23&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;software engineer&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 생성된 메소드 호출&lt;/span&gt;
mango&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;print_name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
mango&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;print_age&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
mango&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;print_role&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;사실 이 예시는 &lt;a href=&quot;/posts/programming-languages-3-metaprogramming&quot;&gt;메타프로그래밍 편&lt;/a&gt;에서도 정적 타입 언어로 이미 보여드렸었습니다. 그렇습니다, 동적 타입 생성은 메타프로그래밍에 해당하는 기능입니다. 정적 타입 언어에서는 복잡한 매크로를 사용했었지만 동적 타입 언어에서는 이러한 타입 조작이 훨씬 더 쉽고 자연스럽게 이루어집니다.&lt;/p&gt;
&lt;p&gt;매크로가 존재하지도 않아서 타입 생성이 어려운 언어들도 있다는 것을 생각해보면 이렇게 쉽게 타입 생성과 메소드 조작을 할 수 있다는 것은 확실한 장점입니다. 특히 Java나 C#에서 타입에 특정 로직을 따라 임의의 메소드들을 추가하는 것은 꽤나 어렵습니다.&lt;/p&gt;
&lt;p&gt;물론 모든 곳에 리플렉션과 동적 타입 생성을 남용해서는 안 됩니다. 먼저 다형성을 우선으로 사용해서 재사용성을 높인 뒤, 다형성으로 해결되지 않는 보일러플레이트에 메타프로그래밍을 사용한다는 규칙은 동적 타입 언어에서도 유지됩니다. 아무리 동적 타입 언어의 메타프로그래밍이 쉽더라도, 일반적인 다형성을 통해 구현하는 방안이 더 직관적입니다.&lt;/p&gt;
&lt;p&gt;하나 동적 타입 생성이 아쉬운 점이 있다면, 사실상 리플렉션과 동일한 방식으로 작동하기 때문에, 런타임에 요구되는 연산의 양이 더 늘어납니다. 그렇기 때문에 정적 타입 언어의 코드 생성, 타입 생성에 비해서 좀 더 느립니다.&lt;/p&gt;
&lt;h3 id=&quot;typescript에-대해서&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#typescript%EC%97%90-%EB%8C%80%ED%95%B4%EC%84%9C&quot; aria-label=&quot;typescript에 대해서 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Typescript에 대해서&lt;/h3&gt;
&lt;p&gt;Typescript는 Javascript를 기반으로 타입 안정성을 추가하여 만들어진 정적 타입 언어입니다. Typescript는 일반적으로 Javascript으로 컴파일(트랜스파일)된 뒤 실행됩니다. 그렇기 때문에 동적 타입 언어이기에 가능한 것들의 대다수가 Typescript에서도 가능합니다.&lt;/p&gt;
&lt;p&gt;합타입 측면에서는 애초에 타입 호환 문제를 겪고 있지 않기 때문에 자유롭게 가능하면서도 타입 안정성을 챙길 수 있고, 덕 타이핑의 경우 Javascript의 덕 타이핑에 비해 좀 더 제한되었지만 어느 정도 활용할 수 있으며, 리플렉션과 동적 타입 생성 기능 또한 계승됩니다.&lt;/p&gt;
&lt;p&gt;그렇기 때문에 &lt;a href=&quot;/posts/programming-languages-2-polymorphism&quot;&gt;다형성 편&lt;/a&gt;에서 보여드렸던 언어 기능 표대로, Typescript는 Rust, Scala 못지않게 많은 기능들을 제공하고 있습니다. Javascript를 기반으로 만들어졌기 때문에 계승되는 단점은 동적 타입 언어 수준으로 성능이 낮다는 것입니다.&lt;/p&gt;
&lt;h2 id=&quot;결론&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%B0%EB%A1%A0&quot; aria-label=&quot;결론 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;결론&lt;/h2&gt;
&lt;p&gt;솔직하게 말씀드리자면 저는 동적 타입 언어를 그다지 선호하지 않습니다. 규모가 작은 프로젝트나 아키텍처의 복잡도가 높지 않거나 스크립팅 용도로 사용하기에는 꽤 나쁘지 않지만, 규모가 커지면 커질수록 유지보수성에 크게 타격을 입으며 버그는 잡기 어려워지고 새 인원이 프로젝트에 참여했을 때 아키텍처를 이해하는데 들어가는 시간이 늘어난다고 생각합니다.&lt;/p&gt;
&lt;p&gt;그러나 현대적인 프로그래밍 언어가 아닌, C++, C#, Java와 같이 언어 기능이 약한 언어들보다는 어느 정도 확실한 강점이 있을 것 같습니다. 특히 메타프로그래밍 측면에서 상당히 강력하고 진입장벽이 낮다는 점을 생각해보면, 이러한 언어들로 만들어진 프레임워크들이 어째서 많은지 이해가 가게 됩니다. 이러한 관점들을 나누기 위해서 이 글을 작성하게 되었습니다.&lt;/p&gt;
&lt;p&gt;이번 글에서 Javascript가 등장하면서 더 이상 9가지 언어라는 컨셉을 유지할 수 없게 되었습니다. 그렇다고 시리즈 제목을 수정하기도 애매하기 때문에 일단은 그대로 유지했습니다. 사실 다음 편은 다양한 프로그래밍 언어들이 어떤 방식으로 비동기 처리 기능을 제공하는지를 다루고자 하는데, 이 과정에서 여러 가지 프로그래밍 언어들을 소개할 예정이기 때문에 이미 9가지 언어라는 컨셉은 불가능했을지도 모르겠습니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;
&lt;p&gt;&lt;a name=&quot;ft_1&quot; href=&quot;#ftsrc_1&quot;&gt;1&lt;/a&gt;: 사실 Variance가 아닌 Rust의 Coercion을 사용했다고 하는 것이 조금 더 정확한 표현입니다. Rust의 &lt;code&gt;dyn Any&lt;/code&gt;와 같은 &lt;code&gt;dyn&lt;/code&gt; 레퍼런스 타입은 메모리 주소 2개로 이루어져 있기 때문에 &lt;code&gt;dyn&lt;/code&gt; 레퍼런스 타입과 메모리 주소 1개짜리인 &lt;code&gt;struct&lt;/code&gt; 레퍼런스 타입은 호환되지 않습니다. 그렇기 때문에 C#의 Boxing처럼 Rust가 Coercion을 통해 변환 코드를 몰래 삽입한 것에 가깝습니다. Coercion 적용 규칙은 여전히 Variance 규칙을 기반으로 하고 있기 때문에 Variance라고 표현했습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[입사 첫날에 36시간 점검 경험하기]]></title><description><![CDATA[2021년 1월 25일, 입사 첫날에 36시간 점검을 경험한 이야기를 전해드리고자 합니다.]]></description><link>https://tech.devsisters.com/posts/36h-downtime-on-first-day</link><guid isPermaLink="false">https://tech.devsisters.com/posts/36h-downtime-on-first-day</guid><pubDate>Fri, 06 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;이 글과 관련된 다른 글 보기&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/posts/cookierun-kingdom-launch-revisited/&quot;&gt;쿠키런: 킹덤 런칭 회고&lt;/a&gt; - DevOps 관점에서 본 쿠키런: 킹덤 런칭 회고&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/cockroachdb-in-production/&quot;&gt;CockroachDB in Production&lt;/a&gt; - CockroachDB에 대한 소개와 킹덤에서 CockroachDB를 사용한 이유 설명&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/crk-launch-storage-postmortem/&quot;&gt;쿠키런: 킹덤 데이터베이스 스토리지 레이어 복원기&lt;/a&gt; - 엔지니어 관점 기록한 개괄적인 장애 대응 과정과 회고&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/bit-level-database-hacking&quot;&gt;CTO가 커리어를 걸고 비트 레벨까지 내려가서 DB를 해킹했던 이야기&lt;/a&gt; - CockroachDB의 소스코드를 참고하며 바이너리 파일로부터 데이터를 복구한 이야기&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/36h-downtime-on-first-day/&quot;&gt;입사 첫날에 36시간 점검 경험하기&lt;/a&gt; - 신규입사자의 관점에서 서술된 장애 대응 기록&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/crk-aws-az-failure-postmortem/&quot;&gt;쿠키런: 킹덤 AWS AZ 장애 아웃라인&lt;/a&gt; - AWS AZ 장애로부터 서비스를 복구한 이야기&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/crk-hot-range-postmortem/&quot;&gt;쿠키런: 킹덤 길드 업데이트 이후 서비스 이슈 되돌아보기&lt;/a&gt; - 쿠키런: 킹덤에 길드 기능이 추가된 이후 발생한 서비스 이슈 회고&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;들어가기에-앞서&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%93%A4%EC%96%B4%EA%B0%80%EA%B8%B0%EC%97%90-%EC%95%9E%EC%84%9C&quot; aria-label=&quot;들어가기에 앞서 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;들어가기에 앞서&lt;/h2&gt;
&lt;p&gt;안녕하세요, 저는 진저랩 데이터플랫폼셀에 재직 중인 2년 차 소프트웨어 엔지니어 김도현입니다.&lt;/p&gt;
&lt;p&gt;데브시스터즈에 처음으로 출근한 날이 2021년 1월 25일인데, 글을 쓰는 지금 벌써 2년 가까운 시간이 지났다는 것이 아직도 실감이 잘 나지 않습니다. 오늘은, 입사 첫날에 36시간 점검을 경험한 이야기를 전해드리고자 합니다.&lt;/p&gt;
&lt;p&gt;근 2년 동안 데브시스터즈에서 많은 일들이 있었지만, 제게 가장 강렬했던 기억은 역시 입사 첫날에 마주했던 36시간 점검입니다. 게임을 플레이하는 유저에서 하루 만에 유저 경험을 고려해야 하는 입사 1일 차 신입 엔지니어가 되었고, 또 그 점검의 규모 또한 상당히 컸습니다. 이런 특별한 상황이었다 보니 아직도 제 기억 속에 강렬하게 남아있습니다.&lt;/p&gt;
&lt;p&gt;저 역시 입사 하루 전까지만 하더라도 일반 유저의 입장에서 게임을 즐기고 있었기 때문에, 아무래도 제 관점이 그 이전부터 팀 내부에 계셨던 엔지니어 분들의 관점보다는 친숙하게 느껴지실 것입니다. 이 글을 통해 당시의 제가 보았던 상황과 느꼈던 감정을 여러분께 공유 드림으로써 기술적인 맥락 없이도 점검 타임라인에 대해 보다 쉽게 이해하실 수 있었으면 좋겠습니다.&lt;/p&gt;
&lt;h2 id=&quot;첫-출근-점검-8시간-전&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B2%AB-%EC%B6%9C%EA%B7%BC-%EC%A0%90%EA%B2%80-8%EC%8B%9C%EA%B0%84-%EC%A0%84&quot; aria-label=&quot;첫 출근 점검 8시간 전 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;첫 출근: 점검 8시간 전&lt;/h2&gt;
&lt;p&gt;2021년 1월 25일, 데브시스터즈에 기념비적인 첫 출근을 했습니다. 신규입사자를 대상으로 진행해주신 간단한 OT를 듣고, 사무실로 이동하여 데이터플랫폼셀 팀원분들과 인사를 나눴습니다. 당시 팀에는 저를 제외하고 3분이 있으셨는데(지금은 훨씬 더 큰 팀이 되었습니다), 슬프게도 팀원분들 모두가 런칭 이후 과도한 업무에 시달리고 계셨습니다.&lt;/p&gt;
&lt;br&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1280px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.111111111111114%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAADGElEQVR42hWS7VPaBQDHf/9BZdqZXm6lTS+fmA58SHM4n4YMVBiiA0RxTHHIhSAC0iwePUZp6krwaRNdKHPBtvKatctqdb6ou171L/Sm6z/4RC++bz/3/XzvKxQ2mshrsVB4xUHlYIQqxUc0qALU9QeQGL7kA9MW7ZO7qDxPGJg7yiWNesSPqlyMKv8t9MUX+FAxjt65h6hZg1AiHqda7qFtYpX3bixRO/wZIlWYVv0yUuM6bksYxe0417zf0GFP0WrepD/6I57VY26JWpDnFXP9tSK8Ug26VhmC+KqDVqWHPudXXL/ziO7RFW5YNpAZltFe0+OwhTAFfsLgP8bifYR5IYMveEDcHsMvH6U3vwRZrun/bY1lNQjTOj2XBqN0jcXx34nyNLrIn6nHHIQ2MRlnmXfMk9lLEt16QTJxQvreIQmDGWdDE7aGi+gv1NL1aiF9b5xHkYML63Y9pkE3W7fsJIaHOI14+Oe3Q35PLLO/tkJ2SsYvkVFihy9ZO/mLZ+ubxBUadjwagvpSfFIlvQUldLz+Jt0FxQhPfDq2xw0899g5jXvJbnxMZvUT9mc7OXJ3cRQwkAjZyT484fThz2RXv2Z3yEF62MFybw9LSjnatytQVZYhLz2H8MVEHweuAY43Q2TSOR2nll9TIZ7GbERkDQTVvXzuW+T7B4/59+yMs/QBWasNX30zHlUHKVs7Y6IKwkMKwgY1QtIyQMLUieumGufCIo5hBdtuLc+WHOwHrSTHe3g5bc+pevnjeI21aQ0jkioSNfWkdZV8O1fOzsi7pOovcVtci+C9KGZF0oFMVINV0cJ9m44ln4vwqIzgfIyZ8TgvtCaCEzbCM2PsjvVgFVfjaBBx6m/kh0ALz0MS7koruJxfhGCrF2PPkTM9Mv42uliQNKGUtjLRVsXUYDNtGiVT1k5m3Gpic0oC5qskZw245FI2XBJ2I+18t93NpLiUurwChPqq84jr3sGbu8DO+1K8lxvxqq6wPtnHnk/Lg08H2dkwkr5vJnvvJnfNCqLG3K7mfma6mzBUlyMrP0dzWSFFBa/wH6yw7oG2AnKOAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/efd16bab2008b0349833ed33b417d2ce/33ecc/01-ck.webp 360w,
/static/efd16bab2008b0349833ed33b417d2ce/263a4/01-ck.webp 480w,
/static/efd16bab2008b0349833ed33b417d2ce/0c8fb/01-ck.webp 640w,
/static/efd16bab2008b0349833ed33b417d2ce/521f7/01-ck.webp 860w,
/static/efd16bab2008b0349833ed33b417d2ce/cbd37/01-ck.webp 1200w,
/static/efd16bab2008b0349833ed33b417d2ce/94b1e/01-ck.webp 1280w&quot; sizes=&quot;(max-width: 1280px) 100vw, 1280px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/efd16bab2008b0349833ed33b417d2ce/53918/01-ck.png 360w,
/static/efd16bab2008b0349833ed33b417d2ce/9aebd/01-ck.png 480w,
/static/efd16bab2008b0349833ed33b417d2ce/0f09e/01-ck.png 640w,
/static/efd16bab2008b0349833ed33b417d2ce/3e3fe/01-ck.png 860w,
/static/efd16bab2008b0349833ed33b417d2ce/64756/01-ck.png 1200w,
/static/efd16bab2008b0349833ed33b417d2ce/bbbf7/01-ck.png 1280w&quot; sizes=&quot;(max-width: 1280px) 100vw, 1280px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/efd16bab2008b0349833ed33b417d2ce/bbbf7/01-ck.png&quot; alt=&quot;01 ck&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        2021년 1월 21일에 쿠키런: 킹덤이 출시되었습니다.
    &lt;/figcaption&gt; 
&lt;/figure&gt;
&lt;br&gt;
&lt;p&gt;입사 4일 전인 1월 21일에 출시된 쿠키런: 킹덤은 역대급 이용자 수를 기록하며 많은 유저분의 관심과 사랑을 받고 있었고, 그만큼 큰 규모의 로그를 만들어내고 있었습니다. 킹덤은 제가 상상조차 해보지 못했던 크기의 로그를 매초 쏟아내고 있었고, 그로 인해 로그를 수집 및 적재하는 파이프라인의 부하가 커지고 있었습니다. 당연히 시급히 해결해야 할 문제들이 생겨났고, 팀원분들은 거의 밤을 새우다시피 파이프라인 관련 대응 작업을 하고 계셨습니다.&lt;/p&gt;
&lt;p&gt;하지만 그렇게 바쁜 와중에도 멘토님을 비롯한 팀원분들 모두 온보딩 절차 및 가이드들을 잘 설명해주시고 전달해주셔서 긴장감이 많이 덜해졌습니다. 당시에 팀장님께서 우스갯소리로 “런칭 당일에 오셨어야 더 재밌는 일들이 많았을 텐데”라는 말씀을 해주셨는데, 정말로 한 달 정도 더 일찍 입사했다면 어땠을까 하는 생각을 해봤습니다. 게임 런칭이 그리 자주 있는 일도 아닌 데다가, 비록 아무것도 알지 못하더라도 런칭 과정을 지켜보면서 많이 성장할 수 있지 않았을까 하는 생각이었습니다.&lt;/p&gt;
&lt;p&gt;그리고 그날 오후, 그 생각을 기억 속에서 날려버릴 일이 일어났습니다.&lt;/p&gt;
&lt;h2 id=&quot;오후-4시-52분-점검-시작&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%A4%ED%9B%84-4%EC%8B%9C-52%EB%B6%84-%EC%A0%90%EA%B2%80-%EC%8B%9C%EC%9E%91&quot; aria-label=&quot;오후 4시 52분 점검 시작 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;오후 4시 52분: 점검 시작&lt;/h2&gt;
&lt;p&gt;퇴근 시간을 1시간 정도 남겨두고 온보딩 가이드를 열심히 따라가던 중, 킹덤을 플레이하던 친구로부터 메시지를 하나 받았습니다.&lt;/p&gt;
&lt;br&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 348px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 63.2183908045977%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAsTAAALEwEAmpwYAAABoklEQVR42p2Ty07CQBiF5+GMvoG+gBsfwrhwbzSycCPeEihSMViNQaHc2iJyaaWtFKiltEJBTXwDj8OYqDtbFyd/Mk1Pzpz/G7K+k8C+UEP8ooK4IOH4SgGXbyJd7iBdeogssrayhFhaQt2wIWt98GIbXKGFs8rD/wx3jrMoaw7GvgffG0E2XBzlmkgWVKSKWnTDjPyIC8mAG7xh8voOxXSRbw0gqjaysonTiKYkkW+xhNPpFMOhgyfbpkk9zCbP6LljcGIYQw18qc0m4UQVufsevOAFPpXjTzCkeg5maPe8EIYarcZEQnTYJPPDFP2Jp1vNVI0fVfQQHWq0EhOCVIDS3KLzFiRFE7KPRY319Vt/XZUvqUiKNkx9FR8eQUPdBMkqFkVE/9Y82VlIBueGXNFC5X4PI2sB10oWZCPGI35ZZ2AfCDLTSa5BE4bd7LzDLk3a/+pwbXkRu6dV1PQBqmof1baFO93BVa0bgUONpWVb3j48R4li8zoL8Gh0GOBjz4X+NGZw8+WoYEtdCIoJtTdCZ+Ch6wbQbR83DYttP+pL+QRMUE5It10eDgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/189d19acc2e4600deb68338403c69fd2/b4669/02-talk-1.webp 348w&quot; sizes=&quot;(max-width: 348px) 100vw, 348px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/189d19acc2e4600deb68338403c69fd2/9c78a/02-talk-1.png 348w&quot; sizes=&quot;(max-width: 348px) 100vw, 348px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/189d19acc2e4600deb68338403c69fd2/9c78a/02-talk-1.png&quot; alt=&quot;02 talk 1&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        아직도 이 메시지를 받을 때의 기억이 생생합니다.
    &lt;/figcaption&gt; 
&lt;/figure&gt;
&lt;br&gt;
&lt;p&gt;뭔가 싶어서 킹덤을 켜보니 실제로 접속이 되지 않았습니다. 사내 와이파이에 문제가 있나 싶어 셀룰러 모드로 다시 접속해보았지만, 똑같이 접속이 불가능하다는 팝업이 보였습니다. 싸한 느낌이 들어 팝업에서 눈을 떼니, 사색이 되어 움직이시는 그룹 분들과 모니터 속에서 시작되는 무수히 많은 화상회의가 보였습니다.&lt;/p&gt;
&lt;p&gt;명백하게 문제로 보이는 상황에 저 역시 몹시 당황했습니다. 무슨 일이 벌어진 건가 여쭤보고 싶었지만, 당시 팀원분들이 다른 회의로 인해 자리를 비우신 상황이라 바로 맥락을 여쭤볼 순 없었습니다. 심상치 않은 분위기 속에서 당황한 채로 주변에서 들리는 이야기를 듣던 중, ‘데이터베이스’라는 단어가 들려왔습니다. 자연스럽게 ‘데이터베이스에 문제가 생겼나?’라는 의문이 머릿속에서 떠올랐고, 순간 오만가지 생각이 다 들기 시작했습니다.&lt;/p&gt;
&lt;br&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1280px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.388888888888886%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAABWUlEQVR42o2RvUvDUBTFM4ju/gluxcVJ9P9wdtJRarEdRBEVwVHqIuIuOLip/Rhiq/QrUocWqYsgNBIw2CQ0eU3fS/I8L2k7iGIOycu9l/xy7iESFwrC6x8FQeCNxHCjlSZoECp6iY/bH/J9f8z7I5hzyjnjMRSZMzb0PMoYFbBjKdZXnZC+67rDsRzHGQwGKAghqCmFAYchpQwzxlxGhwJut1q53J2iNAqFQqlUqtVq1Wq1UqmgQFssFmVZ7nQ6k0R4MvjS0BnftizLNE3DMHq9HgyRbZIwqiPsXf3Ml5uN5gtgbC/FjIrz9e1jL3udPjhL7x7lH5oYx4IjXd4q2zvHW+speXVjM3lo2ETA2FbXdSyM5W3b/sv54krOJDMrS8trU7PziQXLdgSsaZqqqt1uF+F/9URsnPWn59T++VxiUZqeOcmeYhJ3bWEe+Eaf3uTvH8vl8Lexb3clYBFiSIWSAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/720ce4f0a5686e59d2b1805fe91543e3/33ecc/03-ndc.webp 360w,
/static/720ce4f0a5686e59d2b1805fe91543e3/263a4/03-ndc.webp 480w,
/static/720ce4f0a5686e59d2b1805fe91543e3/0c8fb/03-ndc.webp 640w,
/static/720ce4f0a5686e59d2b1805fe91543e3/521f7/03-ndc.webp 860w,
/static/720ce4f0a5686e59d2b1805fe91543e3/cbd37/03-ndc.webp 1200w,
/static/720ce4f0a5686e59d2b1805fe91543e3/94b1e/03-ndc.webp 1280w&quot; sizes=&quot;(max-width: 1280px) 100vw, 1280px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/720ce4f0a5686e59d2b1805fe91543e3/53918/03-ndc.png 360w,
/static/720ce4f0a5686e59d2b1805fe91543e3/9aebd/03-ndc.png 480w,
/static/720ce4f0a5686e59d2b1805fe91543e3/0f09e/03-ndc.png 640w,
/static/720ce4f0a5686e59d2b1805fe91543e3/3e3fe/03-ndc.png 860w,
/static/720ce4f0a5686e59d2b1805fe91543e3/64756/03-ndc.png 1200w,
/static/720ce4f0a5686e59d2b1805fe91543e3/bbbf7/03-ndc.png 1280w&quot; sizes=&quot;(max-width: 1280px) 100vw, 1280px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/720ce4f0a5686e59d2b1805fe91543e3/bbbf7/03-ndc.png&quot; alt=&quot;03 ndc&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        점검에 대한 자세한 기술적 내용은 지난 NDC22에서 확인하실 수 있습니다.
    &lt;/figcaption&gt; 
&lt;/figure&gt;
&lt;br&gt;
&lt;p&gt;당시 점검의 이유를 요약하자면, 데이터는 정상적으로 남아있으나 데이터베이스 클러스터에서 데이터를 인식할 수 없는 문제였습니다. 더욱 자세한 기술적 설명은 &lt;a href=&quot;https://www.youtube.com/watch?v=AZbCZ2KOcwU&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;위 NDC 프레젠테이션&lt;/a&gt;과 &lt;a href=&quot;https://tech.devsisters.com/posts/crk-launch-storage-postmortem/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;데브시스터즈 기술 블로그의 이 글&lt;/a&gt;에서 확인하실 수 있습니다.&lt;/p&gt;
&lt;p&gt;하지만 당시에는 이슈에 대한 자세한 상황을 몰랐고, 불안감은 잔뜩 커져만 갔습니다. 데이터가 날아갔다면 원본을 복구할 수 있는 것인지, 서버 롤백을 하게 될지, 만약 롤백을 한다면 후속 대응은 어떻게 진행될지 등등의 생각이 꼬리에 꼬리를 물며 떠올랐습니다. 저는 이제 막 신입 엔지니어로 입사한 데다가 아직 팀의 맥락은커녕 회사의 OT도 다 받지 못했던 상황이었고, 게다가 점검의 제대로 된 원인도 모르는 상황에서 혼자서 무언가를 할 수는 없었습니다. 그렇다 보니 팀원분들이 오실 때까지는 긴장한 채 주변의 상황을 지켜봤었습니다.&lt;/p&gt;
&lt;p&gt;위의 NDC 프레젠테이션과 기술 블로그 게시글에서도 나온 내용이지만, 점검 시작 후 팀에서는 AWS 및 CockroachDB 엔지니어와 기술 지원을 위한 화상회의를 시작하였습니다. 많은 사람이 한꺼번에 대응하다 보니 회의는 스피커폰으로 진행되었는데, 전망이 좋지 않아 보이는 이야기들이 많이 들렸습니다. AWS 측에서는 마땅히 대응 방안을 제공해주지 못했다는 말과, CockroachDB 측에서는 롤백(백섭)이 최선의 방법일 것이라고 답변해주었다는 얘기도 들렸습니다.&lt;/p&gt;
&lt;br&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1374px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 26.111111111111107%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAABYlAAAWJQFJUiTwAAABL0lEQVR42m2Ma0vCYACF95sjiBCswMx+SJ8MulDfgizpoqVzm5tue3fxRs7KSiNrt6cFCX3ogcOBA+eRUveKVDsmVQ9I5DKICoPhkJai4bh9bOHR648Qjo/RFbQNH1kxacgGrj9G1bJNN9H1NkEQIM2mrzz4HmPfIei5TMcjJpNHbLNJz6njiSaefYeftTDO0C5XUKs5WpUcVn0L/WoNS93nK0yIohApilPCKCGMs0QxSZryNP2gK+9hXK/TrRUwbvJ0apt0b1ax7gq4VhXfvmAxN4k/BeHigSUSf8lkPwQjBeW+TEc5QW+dYLdP0eVDtMYRlnFOMGzwMmnxMbN4f+kQR/Ol4H/h8+AIt5mnr5Xo3Jbw5CIDbRunWUQ0drJ9l4lVoq/u4txvsHgzfv8J39s2ZLZ+SEK0AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/efd3874e3ae89ede20b95bfc9f827a5a/33ecc/04-ndc-crdb.webp 360w,
/static/efd3874e3ae89ede20b95bfc9f827a5a/263a4/04-ndc-crdb.webp 480w,
/static/efd3874e3ae89ede20b95bfc9f827a5a/0c8fb/04-ndc-crdb.webp 640w,
/static/efd3874e3ae89ede20b95bfc9f827a5a/521f7/04-ndc-crdb.webp 860w,
/static/efd3874e3ae89ede20b95bfc9f827a5a/cbd37/04-ndc-crdb.webp 1200w,
/static/efd3874e3ae89ede20b95bfc9f827a5a/4b03a/04-ndc-crdb.webp 1374w&quot; sizes=&quot;(max-width: 1374px) 100vw, 1374px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/efd3874e3ae89ede20b95bfc9f827a5a/53918/04-ndc-crdb.png 360w,
/static/efd3874e3ae89ede20b95bfc9f827a5a/9aebd/04-ndc-crdb.png 480w,
/static/efd3874e3ae89ede20b95bfc9f827a5a/0f09e/04-ndc-crdb.png 640w,
/static/efd3874e3ae89ede20b95bfc9f827a5a/3e3fe/04-ndc-crdb.png 860w,
/static/efd3874e3ae89ede20b95bfc9f827a5a/64756/04-ndc-crdb.png 1200w,
/static/efd3874e3ae89ede20b95bfc9f827a5a/7a163/04-ndc-crdb.png 1374w&quot; sizes=&quot;(max-width: 1374px) 100vw, 1374px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/efd3874e3ae89ede20b95bfc9f827a5a/7a163/04-ndc-crdb.png&quot; alt=&quot;04 ndc crdb&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        NDC 2022 - 쿠키런: 킹덤, 총 56시간의 긴급 점검 회고 中
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;br&gt;
&lt;h2 id=&quot;1일차-종료-점검-1시간-30분-경과&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1%EC%9D%BC%EC%B0%A8-%EC%A2%85%EB%A3%8C-%EC%A0%90%EA%B2%80-1%EC%8B%9C%EA%B0%84-30%EB%B6%84-%EA%B2%BD%EA%B3%BC&quot; aria-label=&quot;1일차 종료 점검 1시간 30분 경과 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;1일차 종료: 점검 1시간 30분 경과&lt;/h2&gt;
&lt;p&gt;그렇게 절망적인 이야기들을 듣던 중 자리를 비우셨던 팀원분들이 돌아오셨습니다. 회의 도중에 대응을 진행하시다 자리로 돌아오셨다고 말씀해주셨는데, 아침에 뵀던 것보다 더욱 수척해지신 모습이셨습니다. 혹여나 제가 자리에 남는다면 조금이나마 도와드릴 수 있는 일이 있지 않을까 하는 마음이었지만, 팀원분들은 강력하게 저를 퇴근길에 올려보내셨습니다.&lt;/p&gt;
&lt;p&gt;걱정 반 불안 반의 마음으로 집에 가는 와중에도 몇 번씩 킹덤을 실행해봤지만, 여전히 점검 안내 팝업이 저를 반겨주었습니다. 집에 도착하고서도, 저녁을 먹고서도, 씻고 잘 준비를 하면서도 계속해서 킹덤을 실행해봤지만, 점검은 계속되고 있었고 제 불안감도 커져만 갔습니다.&lt;/p&gt;
&lt;p&gt;결국 오늘 막 입사한 제가 딱히 집에서 할 수 있는 일도 없었고(재택근무를 위한 노트북 VPN 설정조차 끝내지 못했었습니다), 깨어있다면 계속해서 불안한 생각만 들 것 같았기에 이른 잠자리에 들기로 하였습니다.&lt;/p&gt;
&lt;h2 id=&quot;두-번째-출근-점검-16시간-경과&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%91%90-%EB%B2%88%EC%A7%B8-%EC%B6%9C%EA%B7%BC-%EC%A0%90%EA%B2%80-16%EC%8B%9C%EA%B0%84-%EA%B2%BD%EA%B3%BC&quot; aria-label=&quot;두 번째 출근 점검 16시간 경과 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;두 번째 출근: 점검 16시간 경과&lt;/h2&gt;
&lt;p&gt;1월 26일, 잠을 설치고 새벽에 눈을 떴습니다. 일어나자마자 킹덤을 실행해봤으나 야속한 점검 팝업만 보였습니다.&lt;/p&gt;
&lt;br&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 445px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 40.55555555555556%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsTAAALEwEAmpwYAAABM0lEQVR42o2R3U7CQBBGef+HsEA00gIGShNRA9020FILtFhAVKSVEH8AY40XaDT5nF3S6AUKF2dnsztzZrpN5VkPBeaDxwS57iGtGshWGDIqQ7pMke8r/MwQdzznd01CSqYlxwLkjQsUzUDAL6SyTiJdRKlUF+IEiRr8KdQMD6bhQD5rY09rCiTNgqz3oLA1efazV+icN98kE0K70cFYV2C5I9jBNTrDW3RHIU7dKyr2UfyneKNQNfuomgOcX06xmM8RxzE+31fwbmbInLRxWPNoWn93oUI/5KDWhdYaIrxfCKKHJfzxDM4wgjuaQm0O1p9u7CAsUBKfwu6HWDw9IgonuItCvDwv8fYa4+tjBSuYIHvsiCfYKhQPTomlRh/2IEKLpuIxwSKOqOl+1UGu1tkq/AZKVwy4n9FkIAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/d7400321d46545ee6ea4045bb5d5efa5/33ecc/05-talk-2.webp 360w,
/static/d7400321d46545ee6ea4045bb5d5efa5/28d4d/05-talk-2.webp 445w&quot; sizes=&quot;(max-width: 445px) 100vw, 445px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/d7400321d46545ee6ea4045bb5d5efa5/53918/05-talk-2.png 360w,
/static/d7400321d46545ee6ea4045bb5d5efa5/a6969/05-talk-2.png 445w&quot; sizes=&quot;(max-width: 445px) 100vw, 445px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/d7400321d46545ee6ea4045bb5d5efa5/a6969/05-talk-2.png&quot; alt=&quot;05 talk 2&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        새벽까지 킹덤 오픈을 기다리던 친구도 있었습니다.
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;br&gt;
&lt;p&gt;출근 준비를 마치고 회사에 도착하니 멘토님이 어제와 똑같은 옷차림으로 퇴근할 때 뵌 모습과 똑같이 자리에 앉아계셨습니다. 밤새 이슈 대응을 진행하고 계셨다고 말씀해주셨고, 멘토님뿐만 아니라 저희 팀을 포함한 전사의 수많은 분이 대응을 위해 밤을 새우신 상태였습니다.&lt;/p&gt;
&lt;p&gt;멘토님께서 간단하게 상황 정리를 해주셔서 현재 점검의 원인에 대한 자세한 설명과 진행 중인 대응의 기조를 들을 수 있었습니다. 롤백(백섭)을 하는 것이 단순하게 타협해볼 수 있는 선택이긴 하지만, 런칭하자마자 많은 분께서 게임을 즐겨주고 계셨던 상황에서 롤백으로 인한 데이터 손실이 몇 시간, 몇 분이라도 발생한다면 많은 유저분이 상실감을 느끼실 것을 우려하고 계셨습니다. 그 때문에 완벽한 복구를 계획하고 있다고 하셨고, 이를 위해 데이터베이스 클러스터 간 데이터 이동 작업을 밤새 준비하셨다고 알려주셨습니다.&lt;/p&gt;
&lt;p&gt;덕분에 회사 차원에서 어떤 기조로 현 상황에 대처하고 있는지 알게 되어 무지에 대한 불안감은 어느 정도 줄어들었습니다. 다만 여전히 점검이 계속 길어지는 것에 대한 불안은 남아있었는데, 팀장님의 배려로 실제로 대응을 진행하시는 것을 가까이서 지켜볼 수 있었습니다.&lt;/p&gt;
&lt;p&gt;처음으로 봤던 작업은 CockroachDB의 스토리지 레이어에서 사용하는 sst 파일을 csv 파일로 변환하는 과정이었습니다. Spark를 통해서 약 7TB나 되는 분량의 데이터를 병합하는 작업이었는데, 대략 11,200코어의 CPU와 89,600GB의 메모리를 가지는 클러스터임에도 불구하고 워낙에 데이터의 규모가 크다 보니 작업이 오래 걸릴 수밖에 없었습니다. 시간은 시간대로 흘러가고 Spark job 프로그레스 바는 너무나도 느리게 차오르다 보니, 계속 마음속으로 제발 잘 좀 옮겨지라고 기도하고 있었습니다.&lt;/p&gt;
&lt;p&gt;다행히도 CSV 파일로의 변환은 잘 마무리되었고, 이어서 새로운 CockroachDB 클러스터에 변환된 데이터를 옮겨주기 시작했습니다. 하지만 추가로 한 가지 생겼던 문제가 더 생겼었는데, 바로 EC2 인스턴스 부족이었습니다. 데이터 전처리를 위해서 Spark 노드를 최대한 많이 띄웠었는데, AWS 도쿄 리전의 가용 가능한 모든 R계열 인스턴스를 끌어다 사용하다 보니 인스턴스가 부족해서 새 노드를 띄울 수 없었습니다.&lt;/p&gt;
&lt;p&gt;결국 적재 및 분석에 사용하던 다른 인스턴스들까지 끌어와 데이터 이주 작업에 동원되었고, 오후 5시 반쯤이 되어서 데이터 이주가 모두 완료되었습니다. 다만 데이터베이스 클러스터 간의 데이터 정합성 역시 확인해야 했었기에, 최종적으로 오후 8시 정도가 되어서야 서비스 오픈을 위한 데이터베이스 준비가 완료되었습니다. 이때 작업을 지켜보면서도 시간이 이렇게 많이 흐른 줄 몰랐는데, 어느새 창밖이 어두워진 것을 보고 놀랐던 기억이 납니다.&lt;/p&gt;
&lt;h2 id=&quot;첫-야근-점검-27시간-경과&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B2%AB-%EC%95%BC%EA%B7%BC-%EC%A0%90%EA%B2%80-27%EC%8B%9C%EA%B0%84-%EA%B2%BD%EA%B3%BC&quot; aria-label=&quot;첫 야근 점검 27시간 경과 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;첫 야근: 점검 27시간 경과&lt;/h2&gt;
&lt;p&gt;그리고 또다시 퇴근 시간이 되었습니다. 팀원분들은 오늘도 저를 집으로 보내려 하셨지만, 무지에서 오는 불안감도 있었고, 남아서 도와드릴 수 있는 일들이 하나쯤은 있지 않을까 싶었기에 잠깐의 논의 끝에 회사에 남아 자잘한 작업을 더 도와드리기로 하였습니다.&lt;/p&gt;
&lt;p&gt;오후 8시부터는 실제 유저 중 일부를 샘플링하여 데이터 정합성을 다시 한번 확인하였고, 이후 오후 10시경 전사 대상 내부 테스트가 시작되었습니다. 저도 아직 퇴근하지 않아서 내부 테스트에 참여했었는데, 다행히도 점검 시작 전까지의 제 킹덤 계정이 잘 보존된 것을 확인했습니다. 정말 안도의 한숨을 푹 쉬었던 기억이 아직도 생생합니다.&lt;/p&gt;
&lt;p&gt;내부 테스트가 진행되는 동안 잠시 숨을 돌릴 틈이 있었고, 이 기회에 입사 첫날에 뵙지 못한 데이터 직군의 다른 분들과 안면을 트는 등 잠깐이지만 평범한(이미 오후 10시가 넘은 시점에서 사실 평범하지 않긴 했습니다) 온보딩을 진행했었습니다 🙃&lt;/p&gt;
&lt;p&gt;테스트가 끝난 후, 오후 11시 30분경 점검 해제를 대비하여 데이터플랫폼의 로깅 인프라 모니터링을 준비하기로 하였습니다. 저는 Kafka와 Elastic Stack 메트릭을 모니터링하며 저희의 데이터플랫폼에 이상이 없는지 지켜보았습니다.&lt;/p&gt;
&lt;br&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 445px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 74.72222222222221%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB8UlEQVR42p2TT4/SUBTF+2H8NH4Dd8aF0Y2Jidtx4cY4mY2J8U/MMJEpfwaoYxxwQKCUmVbo0NLSdgqUmrZAKDPF2bs6vleMK1OFxX3Jy3v55Zx77mXeHEt49vYYBxUZ+yfnSJUlHJ52UOA1FIXBxsU83Uvh4Z3bePepi47hgO/ZyDeU7YHpioSXHz5DNH1EVyEWixDVixHYurodkDs3wYk2yp1LzK5u4Ic/IJkeGqqzHZAe2YZKFHrwPQ9BEMC+tLBaRTGUvhVbeiKkJPRJqaS0NfCI12Ob7mwJZxrC/j7FeLrEx7aGNAmIrXYTgVneBdsMkOfHa2AMJSqOWgMU6L1tkEcdhQRlpTb9t/7bUXZhDu6jJacoUCdS9b/YSO5VvqkiW9fAtUSsJrfw02Uwsu4RYNtEThiCOzOIokGiqj9FVGXrPWRqMtiaAkHeh9LfQVmsgnn+/gSPnuzgVfEM6YoYD/VBWSQK+v+Eln5Xjp+A5X3SMhvMi9cZPH5wF5xgIZiHsEYujMmUJPt/Y0Id0XRp0rR9TIlYZpsGJMvDdRRh7DjwF9dbzWA8h7T5BZLol+4QwXKFMLqBPpnH/aR2ioKxIZBEn2tq+Gb5mAU+LEOHP1/EKWZqF2Sv1c03hc4eTVnQ3XhjTuUhMl97JEllY+AvwHXMRl/jK80AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/6d1e62c0d323a30ac86a528ce621a460/33ecc/06-talk-3.webp 360w,
/static/6d1e62c0d323a30ac86a528ce621a460/28d4d/06-talk-3.webp 445w&quot; sizes=&quot;(max-width: 445px) 100vw, 445px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/6d1e62c0d323a30ac86a528ce621a460/53918/06-talk-3.png 360w,
/static/6d1e62c0d323a30ac86a528ce621a460/a6969/06-talk-3.png 445w&quot; sizes=&quot;(max-width: 445px) 100vw, 445px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/6d1e62c0d323a30ac86a528ce621a460/a6969/06-talk-3.png&quot; alt=&quot;06 talk 3&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        사실 자발적인 야근이긴 했습니다.
    &lt;/figcaption&gt; 
&lt;/figure&gt;
&lt;br&gt;
&lt;p&gt;그리고 대망의 오후 11시 37분, 점검 시작 후 약 31시간 만에 서비스가 오픈되었습니다.&lt;/p&gt;
&lt;p&gt;대략 1시간 정도 모니터링을 진행하였고, 정말 다행히도 Kafka와 Elastic Stack 모두 부하가 조금 걸리긴 했으나 서서히 줄어들고 마침내 다시 정상 범주 안으로 복귀하는 것을 확인하였습니다. 데이터플랫폼셀 뿐만 아니라 전사적으로도 어느 정도 복구가 완료된 것 같아 마음이 조금 놓였고, 새벽 1시경 모니터링을 마치고 퇴근하게 되었습니다.&lt;/p&gt;
&lt;p&gt;다만 제가 퇴근하고 잠든 사이 점검이 한 차례 더 있었다는 이야기를 일어난 후에 듣게 되었습니다 😢&lt;/p&gt;
&lt;h2 id=&quot;점검-이후-후일담&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%90%EA%B2%80-%EC%9D%B4%ED%9B%84-%ED%9B%84%EC%9D%BC%EB%8B%B4&quot; aria-label=&quot;점검 이후 후일담 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;점검 이후: 후일담&lt;/h2&gt;
&lt;p&gt;서버가 오픈된 새벽 1시에 느꼈던 기분은 안도감과 경외감이었습니다. 팀뿐만 아니라 그룹 전체가 나서서 완벽한 사용자 경험을 복구하기 위해서 최선의 노력을 다하였고, 비록 시간은 오래 걸렸지만 CockroachDB 엔지니어도 불가능하다고 말했던 “온전한 유저 경험”을 되살릴 수 있었다는 점이 너무나도 경이로웠습니다.&lt;/p&gt;
&lt;p&gt;대응과 복구를 지켜보고 또 맥락을 이해하면서 팀에서 내렸던 결정 및 선택의 이유 역시 알 수 있었고, 데이터플랫폼의 인프라를 이렇게도 활용할 수 있구나라는 생각이 들었습니다. CockroachDB 클러스터의 데이터를 살리는 것과 로그 기반으로 데이터의 정합성을 확인하는 것 모두 데이터플랫폼셀에서 Spark를 사용해 모든 로그를 적재하지 않고 있었더라면 불가능했을 것입니다.&lt;/p&gt;
&lt;p&gt;마지막으로, 외부에서 점검을 구경하던 것과 내부에서 점검을 마주했을 때의 느낌은 완전히 다르다는 것을 알았습니다. 많은 분이 치열하게 서비스 재개를 위해 노력하셨고, 대응이라는 것이 얼마나 바쁜 작업의 연속인지를 알게 되었습니다. 저 역시 이러한 과정의 한 부분을 맡을 수 있도록 더욱 노력해야겠다는 생각도 들었습니다.&lt;/p&gt;
&lt;p&gt;이 글에서는 간략하게 다루었지만, 더욱 자세한 대응에 관한 기록과 이야기는 아래의 글에서 더 자세하게 확인하실 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://tech.devsisters.com/posts/cookierun-kingdom-launch-revisited/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;쿠키런: 킹덤 런칭 회고&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tech.devsisters.com/posts/crk-launch-storage-postmortem/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;쿠키런: 킹덤 데이터베이스 스토리지 레이어 복원기&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;특히, 36시간 점검 당시 비트 레벨까지 내려가서 CockroachDB를 해킹한 CTO님의 이야기도 조만간 공개될 예정이니, 많은 관심을 부탁드리겠습니다.&lt;/p&gt;
&lt;p&gt;읽어주셔서 감사합니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[9가지 프로그래밍 언어로 배우는 개념: 3편 - 메타프로그래밍]]></title><description><![CDATA[프로그래밍 언어들을 비교해가며 메타프로그래밍을 알아보고 더 나은 코드를 작성하는 방법을 고민해봅니다.]]></description><link>https://tech.devsisters.com/posts/programming-languages-3-metaprogramming</link><guid isPermaLink="false">https://tech.devsisters.com/posts/programming-languages-3-metaprogramming</guid><pubDate>Tue, 03 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;프로그래밍-언어-시리즈&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%EC%96%B8%EC%96%B4-%EC%8B%9C%EB%A6%AC%EC%A6%88&quot; aria-label=&quot;프로그래밍 언어 시리즈 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;프로그래밍 언어 시리즈&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;/posts/programming-languages-1-type-theory&quot;&gt;9가지 프로그래밍 언어로 배우는 개념: 1편 - 타입 이론&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/programming-languages-2-polymorphism&quot;&gt;9가지 프로그래밍 언어로 배우는 개념: 2편 - 다형성&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;9가지 프로그래밍 언어로 배우는 개념: 3편 - 메타프로그래밍&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/programming-languages-4-high-level-languages-and-dynamically-typed-languages&quot;&gt;9가지 프로그래밍 언어로 배우는 개념: 4편 - 하이 레벨 언어와 동적 타입 언어&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/programming-languages-5-concurrent-programming&quot;&gt;9가지 프로그래밍 언어로 배우는 개념: 5편 - 동시성 프로그래밍&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;서론&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%84%9C%EB%A1%A0&quot; aria-label=&quot;서론 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;서론&lt;/h2&gt;
&lt;p&gt;안녕하세요, 킹덤 서버 소프트웨어 엔지니어 지민규입니다. 다형성을 아무리 잘 쓰더라도 메타프로그래밍 없이는 최소한의 보일러플레이트가 생깁니다. 인터페이스/타입클래스를 통해 다형성을 이루더라도 최소한 해당 인터페이스/타입클래스가 요구하는 함수들을 구현해야하며 제네릭을 사용하더라도 타입을 선언한 이상 최소한 해당 타입을 생성하는 코드를 구현해야합니다. 어떤 면에서는 이렇게 매번 추가적인 보일러플레이트를 구현하는 것은 마땅히 해야할 일로 받아들일 수도 있습니다. 최대한의 확장성을 가질 수 있는 코드는 아무런 암묵적 처리 없이 우리가 모든 것을 직접 수동적으로 조절할 수 있는 코드겠죠.&lt;/p&gt;
&lt;p&gt;하지만 저는 확장성이라는 것은 주관적이라고 생각합니다. 모든 프로젝트와 도메인은 개별적인 요구 사항이 있고 요구 사항이 중간에 변경되어 확장성을 요구하는 상황이 오더라도 요구 사항이 변하는 방향은 정해져있다는 것입니다. 그렇다면 확장성이라는 것도 프로젝트와 도메인에 따라 의미가 있는 확장성과 의미가 없는 확장성이 나눠질 수 있습니다. **메타프로그래밍(Metaprogramming)**은 이러한 의미 없는 확장성을 포기하고 보일러플레이트 코드를 줄일 수 있을 때 가장 유용합니다. 코드를 경직화하는 대신 반복적인 코드를 없애는 것입니다. 비용에 비해 이러한 이점이 너무나 강력해서 메타프로그래밍을 활용하지 않는 프레임워크는 찾기 어려울 정도입니다.&lt;/p&gt;
&lt;p&gt;메타프로그래밍은 언제나 다형성을 보조하는 기능으로서만 사용해야합니다. 메타프로그래밍 코드는 더 복잡할 뿐만이 아니라 이러한 확장성 비용도 있고 성능 혹은 컴파일 타임의 비용이 함께 다가옵니다. 그러므로 메타프로그래밍은 비용보다 이점이 더 클 때에만 사용해야합니다. 이번 포스트는 메타프로그래밍을 두려워하지 않고 필요에 따라 사용할 수 있도록 다양한 메타프로그래밍 기법들을 소개합니다.&lt;/p&gt;
&lt;h2 id=&quot;타입-성찰&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%83%80%EC%9E%85-%EC%84%B1%EC%B0%B0&quot; aria-label=&quot;타입 성찰 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;타입 성찰&lt;/h2&gt;
&lt;p&gt;하위 타입에서 상위 타입으로 캐스팅하는 것은 업캐스팅이라고 부릅니다. 그리고 상위 타입에서 하위 타입으로 캐스팅하는 것은 다운캐스팅이라고 부릅니다. C++에서는 다운캐스팅을 다음과 같이 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 상위 타입 parent&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Parent&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 하위 타입 Child, Parent를 상속받음&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Child&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token base-clause&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Parent&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Child 타입을 Parent 타입으로 업캐스팅한다. 그냥 대입하면 끝.&lt;/span&gt;
Parent&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; parent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Child&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Parent 타입을 Child 타입으로 다운캐스팅한다. dynamic_cast를 사용해야한다.&lt;/span&gt;
Child&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; child &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;dynamic_cast&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Child&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;parent&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;다운캐스팅은 업캐스팅과 다르게 &lt;code&gt;dynamic_cast&lt;/code&gt;라는 특별한 키워드를 사용합니다. 이는 다운캐스트가 유효하지 않을 수도 있기 때문입니다. &lt;code&gt;parent&lt;/code&gt;에 담긴 타입이 &lt;code&gt;Child&lt;/code&gt;가 아닐경우 &lt;code&gt;dynamic_cast&lt;/code&gt;는 null을 내립니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Parent&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 2개의 하위 타입&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ChildA&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token base-clause&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Parent&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ChildB&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token base-clause&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Parent&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// ChildA 타입을 업캐스팅한다.&lt;/span&gt;
Parent&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; parent &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;ChildA&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// (ChildA가 있는) Parent 타입을 ChildB로 다운캐스팅한다.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// ChildA 타입은 ChildB 타입이 아니므로 null이 내려온다.&lt;/span&gt;
ChildB&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; child &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;dynamic_cast&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;ChildB&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;parent&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;달리 말하자면 &lt;code&gt;ChildA&lt;/code&gt; 타입이 &lt;code&gt;Parent&lt;/code&gt; 타입으로 업캐스팅 되었어도 본래의 타입을 기억하고 있다는 것입니다. 그리고 중요한 점은 이것이 컴파일 타임이 아닌 런타임에 존재하는 정보라는 것입니다. 이를 **런타임 타입 정보(Run-time type information, RTTI)**라고 합니다. C++에서 런타임 타입 정보는 컴파일러 옵션으로 끌 수 있으며 끄게되면 더 이상 dynamic_cast를 사용할 수 없게 되죠. 더 높은 유연성을 위해 직접 타입 정보 시스템을 구현하는 몇몇 프레임워크는 런타임 타입 정보를 끄고 컴파일하기도 합니다.&lt;/p&gt;
&lt;p&gt;C++에서 런타임 타입 정보는 타입으로부터 &lt;code&gt;typeid&lt;/code&gt; 키워드를 이용해 &lt;code&gt;type_info&lt;/code&gt; 클래스도 가져올 수 있게 해줍니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// type_info의 대략적인 정의&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;type_info&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 복사 생성 불가&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;type_info&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; type_info&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt; rhs&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 타입의 해쉬&lt;/span&gt;
  size_t &lt;span class=&quot;token function&quot;&gt;hash_code&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 타입간의 비교 연산자&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;bool&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;operator&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; type_info&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt; rhs&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 복사 불가&lt;/span&gt;
  type_info&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;operator&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; type_info&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt; rhs&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 이름을 가져오는 함수, 컴파일러마다 결과가 다름&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;char&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// A 클래스 정의&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;A&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// A 클래스 생성&lt;/span&gt;
A a &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// a의 type_info 가져오기&lt;/span&gt;
type_info info &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;typeid&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이러한 &lt;code&gt;type_info&lt;/code&gt; 클래스를 이용해서 만들어진 &lt;code&gt;type_index&lt;/code&gt; 클래스를 이용하면 타입을 &lt;code&gt;unordered_map&lt;/code&gt;(해쉬 테이블, 딕셔너리의 C++ 버전)의 키 값으로 사용할 수 있습니다. 이를 이용하면 C++의 타입들에 추가적인 정보들을 담을 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 각 타입들을 생성한 횟수&lt;/span&gt;
unordered_map&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;type_index&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; call_count&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 타입의 인스턴스를 생성하는 함수, 타입별로 생성 횟수를 센다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;typename&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
T &lt;span class=&quot;token function&quot;&gt;create&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  creation_count&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;type_index&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;typeid&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;T&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Java를 비롯한 다른 JVM 계열 언어들에도 이러한 타입 정보를 가져올 수 있습니다. &lt;code&gt;Class&amp;#x3C;T&gt;&lt;/code&gt;가 이에 해당합니다. Java의 모든 오브젝트는 &lt;code&gt;getClass()&lt;/code&gt; 메소드로 &lt;code&gt;Class&amp;#x3C;T&gt;&lt;/code&gt;를 가지고 있습니다. 다만 &lt;a href=&quot;/posts/programming-languages-2-polymorphism&quot;&gt;다형성 편&lt;/a&gt;에서 다뤄졌듯, Java 제네릭의 한계로 인해 위 C++의 템플릿처럼 제네릭한 생성 함수를 만들 수 없기에 제네릭 없이 &lt;code&gt;Object&lt;/code&gt;로 받은 인수에 &lt;code&gt;getClass()&lt;/code&gt;를 해서 횟수를 세어보겠습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Java 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Class는 Invariant하기 때문에 `Class&amp;lt;?&gt;`로 키타입을 설정해야한다.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 여기서 ?는 타입이 일관적이지 않음을 의미한다.&lt;/span&gt;
HashMap&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Class&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Integer&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; call_count &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;HashMap&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 인수로 받은 object를 타입별로 세는 함수&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;increaseCallCount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Object object&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  Integer integer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; call_count&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getOrDefault&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;object&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getClass&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  call_count&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;object&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getClass&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; integer &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 타입에 대한 정보를 가져오는 기능들을 **타입 성찰(Type Introspection)**이라고 부릅니다. 언어마다 타입 성찰은 개별적으로 구현되어있으며 어떤 면에서는 언어들이 가지는 가장 구별되는 특징중 하나라고 볼 수도 있습니다.&lt;/p&gt;
&lt;p&gt;타입 정보를 이렇게만 사용하는 것은 사실 좀 심심하긴 합니다. 다행히도, C++와 다르게, Java의 타입 정보는 좀 더 많은 정보를 가지고 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;리플렉션&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A6%AC%ED%94%8C%EB%A0%89%EC%85%98&quot; aria-label=&quot;리플렉션 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;리플렉션&lt;/h2&gt;
&lt;p&gt;Java의 &lt;code&gt;Class&amp;#x3C;T&gt;&lt;/code&gt;의 정의를 확인해봅시다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Java 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Class&amp;lt;T&gt;의 대략적인 정의&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Class&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 타입 이름을 가져오는 메소드&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getTypeName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 필드 정보들을 가져오는 메소드&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Field&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getFields&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 메소드 정보들을 가져오는 메소드&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Method&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getDeclaredMethods&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;많은 메소드가 존재하지만 대표적인 메소드들만 추려낸 정의인데요, Java의 &lt;code&gt;Class&amp;#x3C;T&gt;&lt;/code&gt;에는 단순히 타입 이름뿐만이 아니라 클래스 내 메소드들과 필드들 또한 가져올 수 있습니다. 각각의 필드와 메소드 정보에는 타입, 이름등의 정보가 담겨져 있지요. 이러한 타입 정보들은 일반적으로는 컴파일할 때 모두 최적화되어 런타임에서는 접근할 수 없는 경우가 많지만, JVM의 경우 타입에 대한 정보를 남겨두어 런타임에서도 접근할 수 있게 해줍니다. 이렇게 컴파일 타임에만 존재하던 정보를 런타임에서도 제공해주는 것을 **리플렉션(Reflection)**이라고 합니다.&lt;/p&gt;
&lt;p&gt;리플렉션을 활용하면 &lt;code&gt;Object&lt;/code&gt;를 필드에 따라 JSON으로 직렬화 할 수 있는 함수도 만들 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Java 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Json으로 직렬화하는 정말 대략적인 함수&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;jsonify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Object&lt;/span&gt; object&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token class-name&quot;&gt;Class&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; clazz &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; object&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getClass&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token class-name&quot;&gt;Field&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; fields &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; clazz&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getFields&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token class-name&quot;&gt;StringBuilder&lt;/span&gt; stringBuilder &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;StringBuilder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  stringBuilder&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;{\n&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; fields&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token class-name&quot;&gt;Field&lt;/span&gt; field &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; fields&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    stringBuilder&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;  \&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    stringBuilder&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;field&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    stringBuilder&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;\&quot;: &quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// 필드가 Integer 타입일 때&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;field&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;equals&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Integer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      stringBuilder&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;field&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;object&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// 필드가 String 타입일 때&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;field&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;equals&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      stringBuilder&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;\&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      stringBuilder&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;field&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;object&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      stringBuilder&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;\&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 마지막 필드가 아닐 때는 콤마 달기&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;i &lt;span class=&quot;token operator&quot;&gt;!=&lt;/span&gt; fields&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      stringBuilder&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token char&quot;&gt;&apos;,&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    stringBuilder&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token char&quot;&gt;&apos;\n&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  stringBuilder&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; stringBuilder&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Item 클래스 정의&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Item&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; itemTag&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; amount&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Item 생성&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;Item&lt;/span&gt; item &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Item&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;tag &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Rare&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;amount &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Item json화&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;jsonify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;item&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;/* result에는 다음 데이터가 담겨진다
{
  &quot;tag&quot;: &quot;Rare&quot;,
  &quot;amount&quot;: 5
}
*/&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;기존 프로그램에서는 타입과 필드를 추가하면 웬만해서는, 타입과 필드를 활용해야만 프로그램의 동작이 달라집니다. 메타프로그래밍을 사용하면 타입과 필드를 추가하는 것 만으로도 프로그램이 다른 동작을 하게 됩니다. 반복적으로 작성해야하는 보일러플레이트 “활용” 코드를 작성할 필요 없이 최소한의 코드 작업으로도 새로운 기능을 추가할 수 있습니다.&lt;/p&gt;
&lt;p&gt;다만 메타프로그래밍은 언제나 단점이 존재합니다. 리플렉션의 경우 보일러플레이트 코드를 작성하는 것에 비해 런타임에 해야할 작업이 더 많기 때문에 성능이 더 떨어지고, 타입 정보가 프로그램에 추가되어야하기 때문에 프로그램의 크기가 더 커집니다. 무엇보다 런타임에서 처리가 이루어지기 때문에 오류가 있어도 컴파일 타임에 잡을 수 없다는 것입니다.&lt;/p&gt;
&lt;p&gt;가끔은 타입 정보만으로는 원하는 리플렉션 코드를 구현하기엔 부족할 때가 있는데요, 그런 경우에는 **어노테이션(Annotation)**이라는 기능으로 좀 더 많은 정보를 주어 좀 더 많은 일을 할 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;어노테이션&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%96%B4%EB%85%B8%ED%85%8C%EC%9D%B4%EC%85%98&quot; aria-label=&quot;어노테이션 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;어노테이션&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Class&amp;#x3C;T&gt;&lt;/code&gt;를 소개할 때 생략한 메소드가 하나 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Java 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Class&amp;lt;T&gt;의 대략적인 정의&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Class&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 어노테이션 정보들을 가져오는 메소드&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Annotation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getAnnotations&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;어노테이션은 타입, 필드 등에 추가적인 정보들을 추가해서 메타프로그래밍을 좀 더 조절 할 수 있게 해줍니다. 엄연히 말하면 어노테이션은 리플렉션만을 위한 기능은 아닙니다. 어노테이션을 통해서 컴파일러의 동작을 바꾸거나 하는 경우도 있는데요, 이번 포스트에서는 어노테이션을 메타프로그래밍을 중심으로 다뤄볼 것 입니다.&lt;/p&gt;
&lt;p&gt;Java에서 직접 어노테이션을 선언하는 방법을 확인해봅시다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Java 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CasingType&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; camelCase&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; snake_case &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token annotation punctuation&quot;&gt;@Target&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;ElementType&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Field&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token annotation punctuation&quot;&gt;@Rentention&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;RententionPolicy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;RUNTIME&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token annotation punctuation&quot;&gt;@interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Casing&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token class-name&quot;&gt;CasingType&lt;/span&gt; casingType &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CasingType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;camelCase&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;가장 먼저 눈에 띄는 것은 &lt;code&gt;@Target(ElementType.Field)&lt;/code&gt;입니다. 이는 해당 어노테이션이 어디에 붙을 수 있는지를 정합니다. 여기에서는 필드에 붙을 수 있음을 지정하고 있습니다. 두번째는 &lt;code&gt;@Rentention(RententionPolicy.RUNTIME)&lt;/code&gt;으로, 해당 어노테이션 정보가 언제까지 존재하는지를 표시하는데, &lt;code&gt;RententionPolicy&lt;/code&gt;에는 총 3가지 값이 있습니다. &lt;code&gt;SOURCE&lt;/code&gt;는 해당 어노테이션이 프로그램 바이너리로 들어가지 않아 컴파일 타임에만 머문다는 것을 의미하고 &lt;code&gt;CLASS&lt;/code&gt;는 프로그램 바이너리로 들어가지만 리플렉션으로 제공되지 않는다는 것을 의미하며, &lt;code&gt;RUNTIME&lt;/code&gt;은 리플렉션으로 제공된다는 것을 의미합니다.&lt;/p&gt;
&lt;p&gt;이렇게 정의한 어노테이션을 다음과 같이 사용할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;//Java 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Item&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token annotation punctuation&quot;&gt;@Casing&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;CasingType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;snake_case&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; itemTag&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; amount&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이러한 어노테이션을 활용하여 리플렉션 코드에서 조회를 하면 좀 더 다양한 상황에 맞추어 사용할 수 있습니다. Typescript, Go에도 JVM의 어노테이션에 해당하는 기능은 일반적으로 지원해주고 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// JVM 계열 언어인 Kotlin, Scala는 생략&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// C# 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// attribute라고 부른다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ECasingType&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CamelCase&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; snake_case &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CasingTypeAttribute&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;System&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Attribute&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ECasingType&lt;/span&gt; casingType&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Typescript 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Decorator라고 부른다. 2022년 7월 기준 experimental&lt;/span&gt;
type &lt;span class=&quot;token class-name&quot;&gt;CasingType&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;camelCase&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;snake_case&quot;&lt;/span&gt;
function &lt;span class=&quot;token function&quot;&gt;casing&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;casingType&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CasingType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Reflect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Symbol&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;casingType&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; casingType&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Go 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Struct tag라고 부른다.&lt;/span&gt;
type &lt;span class=&quot;token class-name&quot;&gt;Item&lt;/span&gt; struct &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  itemTag string `casing&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;snake_case&quot;&lt;/span&gt;`
  amount &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Go의 struct tag에 대해서 유의해야할 것은 리플렉션으로 넘길 수 있는 데이터는 단순한 String 하나뿐이라는 것입니다. 따라서 &lt;code&gt;casing:&quot;snake_case&quot;&lt;/code&gt;와 같은 형태로 넘기는 것이 컨벤션으로 잡혀 있습니다. 또한 좀 더 범용적으로 사용할 수 있는 어노테이션과 다르게, struct tag는 필드에만 달 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;템플릿-메타프로그래밍&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%85%9C%ED%94%8C%EB%A6%BF-%EB%A9%94%ED%83%80%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D&quot; aria-label=&quot;템플릿 메타프로그래밍 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;템플릿 메타프로그래밍&lt;/h2&gt;
&lt;p&gt;런타임에만 메타프로그래밍할 수 있는 것이 아닙니다. 메타프로그래밍은 코드 그 자체를 이용하여 프로그래밍하는 만큼, 컴파일 타임에도 코드를 이용하여 메타프로그래밍을 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/posts/programming-languages-2-polymorphism&quot;&gt;다형성 편&lt;/a&gt;에서 C++의 템플릿에 대해 간략하게 소개드렸습니다. C++의 템플릿은 제네릭을 구현하는 것 뿐만 아니라 메타프로그래밍 또한 가능합니다. 이는 C++의 템플릿은 컴파일타임에 지정된 타입으로 코드를 틀로 찍어내듯 복사하기 때문입니다.&lt;/p&gt;
&lt;p&gt;다형성 편에서 이미 템플릿을 통해 필드 접근을 일반화시킬 수 있다는 것을 보여드렸습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;typename&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;printValue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;T t&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  cout &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; t&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; endl&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// A.value는 int 타입&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;A&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// B.value는 float 타입&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;B&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;float&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

A a &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
B b &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10.5f&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;printValue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;printValue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;b&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이때 타입 &lt;code&gt;T&lt;/code&gt;가 &lt;code&gt;value&lt;/code&gt;를 요구한다는 것을 표시하지 않아도 됩니다.. C++ 템플릿에서는 일단 지정된 타입을 대입한 코드를 생성하고 이후에 해당 코드를 컴파일하기 때문에 만약 &lt;code&gt;value&lt;/code&gt; 필드가 없는 타입을 넘긴다면 생성된 코드에 대한 컴파일 에러 메시지가 나타납니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;C&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

C c&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;printValue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;c&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 컴파일러(clang++) 에러 메시지&lt;/span&gt;
error&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; no member named &lt;span class=&quot;token char&quot;&gt;&apos;value&apos;&lt;/span&gt; in &lt;span class=&quot;token char&quot;&gt;&apos;C&apos;&lt;/span&gt;
                cout &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; t&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; endl&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
                        &lt;span class=&quot;token operator&quot;&gt;~&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;^&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이는 곧 컴파일러가 템플릿 코드를 생성할 때 제공된 타입 T의 필드들과 그 이름, 그리고 필드들의 타입을 알고 있고 그러한 정보들을 이용해 컴파일이 성공하는지 확인하며 코드를 생성한다는 것을 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;이때 필드를 이름으로 조회하는 것을 일반화 시킬 수 있다는 점에서 리플렉션과 유사합니다. 차이점은 리플렉션은 런타임에서 타입 정보를 가져와 처리하고 템플릿은 컴파일 타임에 타입 정보를 가져와 코드를 생성합니다. 그렇기 때문에 유효하지 않은 타입을 넘겼을 때 에러가 컴파일 타임에 발생하기 때문에 에러를 좀 더 빠르게 발견하고 수정할 수 있습니다.&lt;/p&gt;
&lt;p&gt;C++의 템플릿을 이용하면 좀 더 특이한 기능도 구현할 수 있습니다. 예를들어 &lt;code&gt;std::is_base_of&lt;/code&gt;는 템플릿을 이용해 특정 타입이 다른 타입의 상위 타입인지 알려줍니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;A&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// B의 상위 타입은 A다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;B&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token base-clause&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;A&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// result는 true&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;bool&lt;/span&gt; result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; std&lt;span class=&quot;token double-colon punctuation&quot;&gt;::&lt;/span&gt;is_base_of&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;A&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; B&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token double-colon punctuation&quot;&gt;::&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;다른 언어에서는 이러한 기능은 컴파일러에 내장된 기능으로 제공해주는 경우가 많습니다. 어떻게 C++은 이것을 어떻게 템플릿만으로 구현할 수 있을까요? 해답은 **템플릿 특별화(Template Specialization)**에 있습니다. 먼저 특별화가 무엇인지 알아보아야 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 구현 1&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;template&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;typename&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Base&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;bool&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;is_convertible&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Base&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; base&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 구현 2&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 구현 1을 적용할 수 없을 경우 사용된다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;template&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;typename&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;bool&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;is_convertible&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; unknown&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;A&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;B&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token base-clause&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;A&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
B&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; b &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;B&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// A는 B의 상위 타입이기에 A*에 B*를 대입할 수 있다&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 구현 1을 적용해도 문제없기에 is_convertible&amp;lt;A&gt;(b)은 구현 1을 사용한다&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 따라서 true가 출력된다&lt;/span&gt;
&lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;is_convertible&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;A&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;b&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;C&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
C&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; c &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// A는 C의 상위 타입이 아니기에 A*에 C*를 대입할 수 없어&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 구현 1을 적용하면 컴파일 에러가 나기에 is_convertible&amp;lt;A&gt;(b)은 구현 2을 사용한다&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 따라서 false가 출력된다&lt;/span&gt;
&lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;is_convertible&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;A&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;c&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;템플릿 특별화는 템플릿 함수 또는 타입을 사용했을 때 가장 적절한 구현을 자동으로 사용하는 기능입니다. 위 예시에서는 주어진 타입에 따라 컴파일 에러나는 함수 구현과 모든 타입을 사용할 수 있는 함수 구현 둘다 제공하여 상황에 맞는 구현을 사용하게 됩니다. &lt;code&gt;is_convertible&lt;/code&gt;에 부모 타입을 넣었을 때 특별한 구현을 사용하는 것입니다.&lt;/p&gt;
&lt;p&gt;이러한 특별화 기능은 다른 프로그래밍 언어들에게서는 쉽게 찾을 수 없는 기능입니다. C++가 강력한 언어라고 불리는 이유중 하나가 바로 이 템플릿 특별화에서 기인합니다. C++에는 이러한 템플릿 메타 프로그래밍을 활용한 함수들을 모아놓은 &lt;a href=&quot;https://en.cppreference.com/w/cpp/meta&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;&amp;#x3C;type_traits&gt;&lt;/a&gt; 라이브러리가 존재합니다. C++20에서는 이러한 템플릿 활용을 더 강화하기 위해 Concepts라는 개념을 추가하기도 하였죠.&lt;/p&gt;
&lt;p&gt;다음은 리플렉션과 템플릿의 코드 생성을 비교한 표입니다.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;리플렉션&lt;/th&gt;
&lt;th&gt;코드 생성&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;성능 저하&lt;/td&gt;
&lt;td&gt;런타임&lt;/td&gt;
&lt;td&gt;컴파일 타임&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;에러 발생 시점&lt;/td&gt;
&lt;td&gt;런타임&lt;/td&gt;
&lt;td&gt;컴파일 타임&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;바이너리 크기&lt;/td&gt;
&lt;td&gt;상대적으로 작음&lt;/td&gt;
&lt;td&gt;상대적으로 큼&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;하지만 C++의 템플릿에는 한계점이 있습니다. 리플렉션과 다르게 필드들의 목록을 가져올 방법이 없습니다. C++의 템플릿은 코드만을 생성하고 유효한지 검증해줄 뿐, 타입의 모든 정보를 가져오지는 못합니다. 리플렉션을 소개할때 작성한 임의의 데이터 타입을 &lt;code&gt;JSON&lt;/code&gt; 형식으로 변환시키는 &lt;code&gt;jsonify&lt;/code&gt;함수를 만들었었지만 C++에서는 템플릿만을 이용해서는 구현할 수 없습니다.&lt;/p&gt;
&lt;h2 id=&quot;매크로&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%A4%ED%81%AC%EB%A1%9C&quot; aria-label=&quot;매크로 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;매크로&lt;/h2&gt;
&lt;p&gt;C++ 에서의 메타프로그래밍은 템플릿만이 있는 것은 아닙니다. 매크로라는 템플릿보다 더욱 강력한 메타프로그래밍 기능이 존재합니다. 여기서는그중에서도 코드 일부를 치환해서 생성할 수 있는 매크로 함수에 대해서 다룹니다. 매크로 함수는 코드를 생성할 때 부분적으로 치환할 수 있다는 점에서 템플릿과 비슷합니다. 위 value 필드를 출력하는 템플릿을 매크로로 구현하면 다음과 같습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 매크로 함수 선언&lt;/span&gt;
&lt;span class=&quot;token macro property&quot;&gt;&lt;span class=&quot;token directive-hash&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;token directive keyword&quot;&gt;define&lt;/span&gt; &lt;span class=&quot;token macro-name function&quot;&gt;DEF_PRINT_VALUE&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;T&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;    &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token expression&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;printValue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;T t&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;      &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;token expression&quot;&gt;cout &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; t&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; endl&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token expression&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// A.value는 int 타입&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;A&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// B.value는 float 타입&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;B&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;float&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// A와 B 버전에 대한 오버로딩 함수 생성&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;DEF_PRINT_VALUE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;A&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;DEF_PRINT_VALUE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;B&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

A a &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
B b &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10.5f&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;printValue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;printValue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;b&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;만약 유효하지 않은 타입으로 생성할 경우, 함수 호출 코드를 작성한 시점이 아닌 함수를 생성하는 시점에 에러가 발생합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;C&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;DEF_PRINT_VALUE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;C&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 컴파일러(clang++) 에러 메시지&lt;/span&gt;
error&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; no member named &lt;span class=&quot;token char&quot;&gt;&apos;value&apos;&lt;/span&gt; in &lt;span class=&quot;token char&quot;&gt;&apos;C&apos;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;DEF_PRINT_VALUE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;C&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;^&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;~&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;~&lt;/span&gt;
note&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; expanded from macro &lt;span class=&quot;token char&quot;&gt;&apos;DEF_PRINT_VALUE&apos;&lt;/span&gt;
                        cout &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; t&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; endl&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  \&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;C++의 템플릿에 비해 더 나은 점이 있는가 싶을 수도 있습니다. 하지만 매크로의 진가는 타입만을 치환시키는 것이 아닌, 코드의 모든 부분들을 치환시킬 수 있다는 것입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;
&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// C/C++ 매크로 정의&lt;/span&gt;
&lt;span class=&quot;token macro property&quot;&gt;&lt;span class=&quot;token directive-hash&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;token directive keyword&quot;&gt;define&lt;/span&gt; &lt;span class=&quot;token macro-name function&quot;&gt;DEF_PRINT_FIELD&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;T&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; fieldName&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;  &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token expression&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; print_&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;##&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;&lt;span class=&quot;token function&quot;&gt;fieldName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;T t&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;        &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;token expression&quot;&gt;cout &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; t&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fieldName &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; endl&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;       &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token expression&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Human&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  string name&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; age&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  string role&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;DEF_PRINT_FIELD&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Human&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 아래 코드가 생성된다.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//  void print_name(T t) {&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//    cout &amp;lt;&amp;lt; t.name &amp;lt;&amp;lt; endl;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//  }&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;DEF_PRINT_FIELD&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Human&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; age&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 아래 코드가 생성된다.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//  void print_age(T t) {&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//    cout &amp;lt;&amp;lt; t.age &amp;lt;&amp;lt; endl;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//  }&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;DEF_PRINT_FIELD&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Human&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; role&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 아래 코드가 생성된다.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//  void print_role(T t) {&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//    cout &amp;lt;&amp;lt; t.role &amp;lt;&amp;lt; endl;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//  }&lt;/span&gt;

Human human &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mango&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;23&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;software engineer&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;print_name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;human&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;print_age&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;human&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;print_role&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;human&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;역설적이게도 이렇게 강력하기 때문에 C/C++의 매크로를 기피하시는 분들도 많습니다. 강력한만큼 잘못된 방식으로 매크로를 사용하면 어떠한 동작을 할 지 예상하기 어려워져 더더욱 읽기 어려워지고 관리하기 어려워집니다.&lt;/p&gt;
&lt;p&gt;하지만 리플렉션이 없고 컴파일 타임에서도 타입 정보가 부족한 C++에서는 보일러플레이트 코드를 줄이기 위해서는 매크로는 필수부가결합니다. 매크로의 단점을 감수하고 많은 C++ 프레임워크는 매크로를 사용합니다.&lt;/p&gt;
&lt;h2 id=&quot;추상-구문-트리-매크로&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B6%94%EC%83%81-%EA%B5%AC%EB%AC%B8-%ED%8A%B8%EB%A6%AC-%EB%A7%A4%ED%81%AC%EB%A1%9C&quot; aria-label=&quot;추상 구문 트리 매크로 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;추상 구문 트리 매크로&lt;/h2&gt;
&lt;p&gt;Scala와 Rust에도 매크로가 존재합니다. 다만 C/C++의 매크로와는 차이점이 많습니다. 먼저 Scala 부터 살펴봅시다. Scala의 경우 매크로 실행할 때 소스의 모든 타입 정보들이 제공되며 **추상 구문 트리(Abstract Syntax Tree, AST)**에 접근하고 수정할 수 있습니다.&lt;/p&gt;
&lt;p&gt;Scala의 매크로를 이용하면 메소드나 클래스를 자동으로 생성하고 타입클래스를 자동으로 구현해주거나 기존에 존재하는 클래스도 수정할 수 있습니다. 위 C++ 매크로를 Scala 방식으로 구현한 것은 다음과 같습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Scala 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 메소드 생성하는 매크로 어노테이션 정의&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; Printer &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; StaticAnnotation &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; macroTransform&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;annottees&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Any&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; macro Printer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;impl
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 매크로 정의&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;object&lt;/span&gt; Printer &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; impl&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;c&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; whitebox&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Context&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;annottees&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Expr&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Expr&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token namespace&quot;&gt;c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;universe&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;/span&gt;_

    &lt;span class=&quot;token comment&quot;&gt;// 어노테이션을 받은 클래스의 추상 구문 트리를 가져온다&lt;/span&gt;
    annottees&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;map&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;_&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;tree&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;match&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;clazz&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ClassDef&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt; Nil &lt;span class=&quot;token keyword&quot;&gt;=&gt;&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// 모든 필드들을 조회해 print 메소드들을 생성한다&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; printMethods &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; clazz&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;impl&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;body&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;collect &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; valDef&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ValDef &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; valDef&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;mods&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;hasFlag&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Flag&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;CASEACCESSOR&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;=&gt;&lt;/span&gt;
            &lt;span class=&quot;token string-interpolation&quot;&gt;&lt;span class=&quot;token id function&quot;&gt;q&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&quot;&quot;
            def &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;TermName&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-interpolation&quot;&gt;&lt;span class=&quot;token id function&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;print_&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;valDef&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;(): Unit =
              println(&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;valDef&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;)
            &quot;&quot;&quot;&lt;/span&gt;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

        &lt;span class=&quot;token comment&quot;&gt;// 생성된 print 메소드들을 이어붙힌다&lt;/span&gt;
        c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Expr&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
          ClassDef&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
            clazz&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;mods&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            clazz&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            clazz&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;tparams&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            Template&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
              clazz&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;impl&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;parents&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
              clazz&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;impl&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
              clazz&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;impl&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;body &lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt; printMethods
            &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; _ &lt;span class=&quot;token keyword&quot;&gt;=&gt;&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// class가 아닐 경우 에러를 낸다&lt;/span&gt;
        c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;abort&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;c&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;enclosingPosition&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;only classes can use @Printer&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 매크로 어노테이션을 적용한 클래스를 정의한다&lt;/span&gt;
&lt;span class=&quot;token annotation punctuation&quot;&gt;@Printer&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; Human&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  age&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  role&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;String&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 생성된 메소드들을 마음껏 활용한다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; human &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Human&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;mango&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;23&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;software engineer&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
human&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;print_name&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
human&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;print_age&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
human&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;print_role&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Rust의 경우 매크로가 타입 정보를 제공해주지는 않습니다만, 여전히 추상 구문 트리에 접근할 수 있게 해주기 때문에 매크로를 적용 받는 타입의 필드 이름이나 필드 타입까지는 가져올 수 있습니다.&lt;/p&gt;
&lt;p&gt;이를 이용하면 타입을 추가하거나 메소드를 추가하는 건 쉽습니다. Scala가 매크로로 타입클래스를 자동으로 구현해줄 수 있는 것처럼 Rust도 매크로를 이용하면 trait를 자동으로 구현해줄 수 있습니다. 위 C++ 매크로를 Rust 방식으로 구현한 것은 다음과 같습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;rust&quot;&gt;&lt;pre class=&quot;language-rust&quot;&gt;&lt;code class=&quot;language-rust&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Rust 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// attribute 매크로 정의&lt;/span&gt;
&lt;span class=&quot;token attribute attr-name&quot;&gt;#[proc_macro_attribute]&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;pub&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;make_printers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  _attr&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token namespace&quot;&gt;proc_macro&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;TokenStream&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  item&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token namespace&quot;&gt;proc_macro&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;TokenStream&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token namespace&quot;&gt;proc_macro&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;TokenStream&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 추상 구문 트리를 분석한다&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; derived_item &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token namespace&quot;&gt;syn&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token macro property&quot;&gt;parse_macro_input!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;item &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;DeriveInput&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; item_ident &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;derived_item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ident&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// attribute를 받은 struct의 구조를 가져온다&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; printers &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;match&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;derived_item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 모든 필드들을 조회해 print 메소드들을 생성한다&lt;/span&gt;
    &lt;span class=&quot;token namespace&quot;&gt;syn&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Data&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Struct&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fields&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;iter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token closure-params&quot;&gt;&lt;span class=&quot;token closure-punctuation punctuation&quot;&gt;|&lt;/span&gt;field&lt;span class=&quot;token closure-punctuation punctuation&quot;&gt;|&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; field_ident &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; field&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ident&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;as_ref&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;unwrap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; printer_ident &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token namespace&quot;&gt;proc_macro2&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Ident&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;token macro property&quot;&gt;format!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;print_{}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; field_ident&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token namespace&quot;&gt;proc_macro2&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Span&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;call_site&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token macro property&quot;&gt;quote!&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;pub&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fn&lt;/span&gt; #&lt;span class=&quot;token function&quot;&gt;printer_ident&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token macro property&quot;&gt;println!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;{}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;#field_ident&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// sturct가 아닐 경우 에러를 낸다&lt;/span&gt;
    _ &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token macro property&quot;&gt;compile_error!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;only structs can use make_printers&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 생성된 print 메소드들을 추가한다&lt;/span&gt;
  &lt;span class=&quot;token macro property&quot;&gt;quote!&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    #derived_item
    &lt;span class=&quot;token keyword&quot;&gt;impl&lt;/span&gt; #item_ident &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      #&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;#printers&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;into&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// attribute 매크로를 적용한 struct를 정의한다&lt;/span&gt;
&lt;span class=&quot;token attribute attr-name&quot;&gt;#[make_printers]&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token type-definition class-name&quot;&gt;Human&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;pub&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;pub&lt;/span&gt; age&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;i32&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;pub&lt;/span&gt; role&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 생성된 메소드들을 마음껏 활용한다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; human &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Human&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  name&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mango&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;to_owned&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  age&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;23&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  role&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;software engineer&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;to_owned&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

human&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;print_name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
human&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;print_age&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
human&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;print_role&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;코드가 긴 만큼 단점도 명확하게 보일거라고 생각합니다. 일반적으로 이러한 강력한 기능을 제공하는 매크로는 복잡합니다. 이러한 문법 구문 트리를 수정할 수 있게 하는 매크로를 지원하려면 언어 차원에서 제공해야하는 기능이 많고 알아야 할 것도 많으며, 더 많은 코드가 컴파일 타임에 실행되는 만큼 컴파일 속도도 느려집니다. 하지만 그런 만큼 적재적소에 사용할 경우 많은 양의 보일러플레이트 코드를 없앨 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;메타프로그램&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A9%94%ED%83%80%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8&quot; aria-label=&quot;메타프로그램 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;메타프로그램&lt;/h2&gt;
&lt;p&gt;사실 모든 프로그래밍 언어에 메타프로그래밍을 사용할 수 있습니다. 툴이나 스크립트로 코드를 읽고 코드를 생성하는 것도 메타프로그래밍이기 때문입니다. 유명 라이브러리와 프레임워크들도 언어에서 자체적으로 지원되는 기능 대신 외부 프로그램으로 코드를 생성하기도 합니다. 이러한 프로그램들을 **메타프로그램(Metaprogram)**이라고 부릅니다.&lt;/p&gt;
&lt;p&gt;Java은 Annotation Processor, Kotlin은 Kotlin Symbol Prcoessor를 통해 코드를 생성할 수 있으며 JVM의 특성을 이용해 기존에 존재하는 클래스도 수정할 수 있고 C# 또한 코드를 생성할 수 있는 다양한 툴이 존재하며 Go는 다형성 기능이 부족한 만큼 메타프로그램에 의존하는 경향이 높습니다. 매크로가 코드의 일부분만을 생성하고 다른 매크로 호출에 간섭할 수 없는 만큼 때로는 메타프로그램이 매크로보다 나은 경우도 있으며 추상 구문 트리에 접근 가능한 매크로가 존재하는 Scala도 파일 단위로 추상 구문 트리에 접근 할 수 있게 해주는 Scalameta라는 라이브러리가 존재합니다.&lt;/p&gt;
&lt;p&gt;코드 생성이 대표적인 기능인 라이브러리들로는 GraphQL과 gRPC/Protobuf가 대표적입니다. GraphQL은 언어 라이브러리마다 다르지만 GraphQL 스키마로부터 API 코드를 생성하거나 API 코드로부터 GraphQL 스키마를 생성할 수 있고 gRPC/Protobuf는 API나 데이터 타입을 protobuf 코드로 정의하고 다양한 언어들을 대상으로 코드를 생성할 수 있는 기능이 있습니다.&lt;/p&gt;
&lt;p&gt;반드시 라이브러리와 툴을 사용해서 코드를 생성해야만 메타프로그램인 것이 아닙니다. Python과 같은 스크립팅 언어로 소스 파일을 생성해주는 프로그램 또한 메타프로그램입니다. 소스 파일의 추상 구문 트리를 분석하거나 스키마 파일을 읽을 수 있는 툴들에 비해 접근할 수 있는 정보는 적지만 프로젝트에 맞는 소스 코드를 자유롭게 생성할 수 있다는 장점이 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;결론&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%B0%EB%A1%A0&quot; aria-label=&quot;결론 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;결론&lt;/h2&gt;
&lt;p&gt;그만큼 메타프로그래밍은 큰 규모의 프로젝트에서는 필수적인 기능입니다. 다형성을 통해 코드 재사용성을 늘리는 것이 가장 좋지만 고도화된 아키텍처에서는 도메인의 요구 사항에 맞는 메타프로그래밍이 필요한 순간이 옵니다. 그럴 때 이 글이 더 좋은 아키텍처를 디자인하는데에 조금이라도 도움이 되었으면 합니다.&lt;/p&gt;
&lt;p&gt;프로그래밍 언어 시리즈에는 의도적으로 정적 타입 언어들만 포함하고 있었는데요, 이유는 정적 타입 언어와 동적 타입 언어를 정확하게 비교하려면 정적 타입 언어의 다형성, 메타프로그래밍까지 전부 이해하고 있어야 하기 때문입니다. 언젠가 기회가 된다면 정적 타입 언어와 동적 타입 언어를 비교하는 글을 쓸 수 있으면 좋겠습니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[쿠키런: 킹덤 데이터베이스 스토리지 레이어 복원기]]></title><description><![CDATA[쿠키런: 킹덤 런칭 후 4일 만에 기술적인 문제로 인해 발생했던 약 36시간의 장애에 대해 전해드리고자 합니다.]]></description><link>https://tech.devsisters.com/posts/crk-launch-storage-postmortem</link><guid isPermaLink="false">https://tech.devsisters.com/posts/crk-launch-storage-postmortem</guid><pubDate>Thu, 22 Dec 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;이 글과 관련된 다른 글 보기&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/posts/cookierun-kingdom-launch-revisited/&quot;&gt;쿠키런: 킹덤 런칭 회고&lt;/a&gt; - DevOps 관점에서 본 쿠키런: 킹덤 런칭 회고&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/cockroachdb-in-production/&quot;&gt;CockroachDB in Production&lt;/a&gt; - CockroachDB에 대한 소개와 킹덤에서 CockroachDB를 사용한 이유 설명&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/bit-level-database-hacking&quot;&gt;CTO가 커리어를 걸고 비트 레벨까지 내려가서 DB를 해킹했던 이야기&lt;/a&gt; - CockroachDB의 소스코드를 참고하며 바이너리 파일로부터 데이터를 복구한 이야기&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/36h-downtime-on-first-day/&quot;&gt;입사 첫날에 36시간 점검 경험하기&lt;/a&gt; - 신규입사자의 관점에서 서술된 장애 대응 기록&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/crk-aws-az-failure-postmortem/&quot;&gt;쿠키런: 킹덤 AWS AZ 장애 아웃라인&lt;/a&gt; - AWS AZ 장애로부터 서비스를 복구한 이야기&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/crk-hot-range-postmortem/&quot;&gt;쿠키런: 킹덤 길드 업데이트 이후 서비스 이슈 되돌아보기&lt;/a&gt; - 쿠키런: 킹덤에 길드 기능이 추가된 이후 발생한 서비스 이슈 회고&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;안녕하세요, 저는 데브시스터즈 진저랩 인프라셀에서 데브옵스 엔지니어로 일하고 있는 이창원입니다. 지난 글인 &lt;a href=&quot;../cookierun-kingdom-launch-revisited&quot;&gt;쿠키런: 킹덤 런칭 회고&lt;/a&gt;에 이어 이번에는 런칭 후 4일 만에 기술적인 문제로 인해 발생했던 약 36시간 동안의 서비스 장애에 대해 전해드리려고 합니다. 여러 편의 글을 통해 다각도에서 이야기를 풀어드리고자 하는데, 이번 글에서는 서비스 장애의 원인과 해결 과정, 그리고 회고를 다루려고 합니다.&lt;/p&gt;
&lt;h2 id=&quot;disclaimer&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#disclaimer&quot; aria-label=&quot;disclaimer permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Disclaimer&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;이 이야기는 박새미님과 NDC22에서 공동 발표한 &lt;a href=&quot;https://www.youtube.com/watch?v=AZbCZ2KOcwU&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;쿠키런: 킹덤, 총 56시간의 긴급 점검 회고 - 그때 그 명검은 왜 뽑아야 했는가&lt;/a&gt;와 CockroachDB 고객사 컨퍼런스 행사인 &lt;a href=&quot;https://www.youtube.com/watch?v=iAWU-eGIYsk&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Roachfest 세션&lt;/a&gt;에서 발표되기도 하였습니다. 보다 생동감 있게 혹은 짧은(?) 버전으로 일련의 이야기를 접하고자 하는 분들은 발표 영상을 보시는 것을 권해드립니다.&lt;/li&gt;
&lt;li&gt;데브시스터즈에서 어째서 CockroachDB라는 다소 생소한 데이터베이스를 사용하기로 결정했는지 이유나, 이 글을 읽으면서 CockroachDB의 내부 동작이나 설계가 궁금하신 분들은 &lt;a href=&quot;../cockroachdb-in-production&quot;&gt;CockroachDB in Production&lt;/a&gt;을 읽으시는 것을 추천합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;지난-글에서&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A7%80%EB%82%9C-%EA%B8%80%EC%97%90%EC%84%9C&quot; aria-label=&quot;지난 글에서 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;지난 글에서…&lt;/h2&gt;
&lt;p&gt;지난 글에서 여러 장치를 두어 &amp;#x3C;쿠키런: 킹덤&gt;을 런칭했고, 첫 주말을 안정적으로 넘겼지만 모니터링 중에 사용자 수가 단조증가하여 스토리지 사용량이 크게 늘고 있었다는 이야기로 마무리되었는데요. &amp;#x3C;쿠키런: 킹덤&gt;에서 사용한 CockroachDB가 배포되어 있는 리눅스 운영체제의 특성 상 디스크 공간이 가득 차게 되면 더 이상의 쓰기 작업이 불가능하게 됩니다. 이렇게 되면 당연하게도 사용자 상태의 변경사항을 기록하는 데이터베이스 본연의 기능을 할 수 없을 뿐더러, 분산 데이터베이스로써 변경사항을 여러 노드 간에 항시 복제, 전파하는 안전장치가 동작할 수 없게 됩니다. CockroachDB는 &lt;a href=&quot;https://en.wikipedia.org/wiki/CAP_theorem&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;CP 시스템&lt;/a&gt;이므로 이런 경우 가용성, 즉 문제가 발생한 노드를 희생하여 데이터 일관성을 보존하도록 설계되어 있으며, &amp;#x3C;쿠키런: 킹덤&gt;의 경우 7개의 복제본을 저장하도록 설정되어 가용성이 보장되지 않는 상태에서도 데이터 영향도가 가장 적도록 의도하였습니다.&lt;/p&gt;
&lt;p&gt;런칭 후 첫 주말을 보내고 월요일 오전 출근하여 클러스터 상태를 확인하니 사용자 인입이 현재 상태를 유지할 경우 위에서 묘사한 상황이 닥치기까지 약 36시간이 남은 것으로 계산할 수 있었습니다. 오픈 이후 유저 수는 단조증가하고 있었지만 마케팅은 서버 안정성이 확보된 이후에 집행하기로 결정되어 있던 상황이었습니다. 따라서 마케팅 집행이 시작되면 지금보다 더 가파른 규모로 유저가 증가할 수 있다는 사실을 인지하고 있었기 때문에 팀 내 위기감이 더욱 고조되었습니다. 당장 발등에 불이 떨어진 상황이기 때문에 저희는 여러 지표들을 따져보며 클러스터 규모를 얼마나 더 크게 키워야할지를 계산을 하는 동시에 공식 가이드를 따라 모든 노드에 Ballast 파일을 생성해두기로 결정했습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1600px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 28.888888888888886%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAIAAABM9SnKAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA4UlEQVR42lWO3XKEIAyFef/X602rglrtrruI/GkLCkmDdi+a+TicJMwcWFVzLrr3j3oYb2TGr1s/jG33OZC7T30/NLxtu75uhPMrIgIAvorNSsl5fj4lMVOjFrrlrKQ8h7LMH+dWLYvWRtMxWpkQdmQUQBGkw5lFXKEk9NgYa6yz1lGVxtpzYp3f6AdMfUd9gElgjotcfEIyLsMK6E9cLtj8Z0zGn4zsrZ8q6cSyXSr0xtXKlec6CoONhkqBsDAFeEScImkx9wA+IfMH6JiIJRyXeZH1jjoWzI4u/cMmDBl/AYqOUfttlQpWAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c738c18f37cd56d872a309c399d315c8/33ecc/01-crdb-ballast-file-documentation.webp 360w,
/static/c738c18f37cd56d872a309c399d315c8/263a4/01-crdb-ballast-file-documentation.webp 480w,
/static/c738c18f37cd56d872a309c399d315c8/0c8fb/01-crdb-ballast-file-documentation.webp 640w,
/static/c738c18f37cd56d872a309c399d315c8/521f7/01-crdb-ballast-file-documentation.webp 860w,
/static/c738c18f37cd56d872a309c399d315c8/cbd37/01-crdb-ballast-file-documentation.webp 1200w,
/static/c738c18f37cd56d872a309c399d315c8/64296/01-crdb-ballast-file-documentation.webp 1600w&quot; sizes=&quot;(max-width: 1600px) 100vw, 1600px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c738c18f37cd56d872a309c399d315c8/53918/01-crdb-ballast-file-documentation.png 360w,
/static/c738c18f37cd56d872a309c399d315c8/9aebd/01-crdb-ballast-file-documentation.png 480w,
/static/c738c18f37cd56d872a309c399d315c8/0f09e/01-crdb-ballast-file-documentation.png 640w,
/static/c738c18f37cd56d872a309c399d315c8/3e3fe/01-crdb-ballast-file-documentation.png 860w,
/static/c738c18f37cd56d872a309c399d315c8/64756/01-crdb-ballast-file-documentation.png 1200w,
/static/c738c18f37cd56d872a309c399d315c8/42cbc/01-crdb-ballast-file-documentation.png 1600w&quot; sizes=&quot;(max-width: 1600px) 100vw, 1600px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c738c18f37cd56d872a309c399d315c8/42cbc/01-crdb-ballast-file-documentation.png&quot; alt=&quot;01 crdb ballast file documentation&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        당시 사용 중이던 19.1.11 버전 공식 가이드에 적힌 내용.
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;사실 파일 시스템이 가득 찰 경우 데이터베이스 쓰기 작업이 문제가 아니라, 리눅스 시스템에서는 대부분의 작업을 할 수 없게 됩니다. 이러한 상황에 대처할 수 있도록 CockroachDB의 공식 가이드에서는 Ballast 파일을 미리 생성해둘 것을 공식적으로 가이드하고 있습니다. 여기서 Ballast 파일이란 정확히 이런 경우에 대비한 보험으로, 미리 공간을 점유해두는 더미 파일을 의미합니다. 유사시 Ballast 파일을 삭제하면 스토리지 증설에 필요한 명령 등을 실행할 수 있게 되고, 이후 다시 클러스터에 Join할 수도 있어 재해 복구에 도움이 됩니다.&lt;/p&gt;
&lt;h2 id=&quot;데이터베이스-작업과-이슈-발생&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%9E%91%EC%97%85%EA%B3%BC-%EC%9D%B4%EC%8A%88-%EB%B0%9C%EC%83%9D&quot; aria-label=&quot;데이터베이스 작업과 이슈 발생 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;데이터베이스 작업과 이슈 발생&lt;/h2&gt;
&lt;p&gt;그러나 Ballast 파일을 생성하기 위해 미리 작성해둔 스크립트를 실행하였는데 Ballast 파일의 경로가 잘못 설정되어 Block Device를 가리키고 있었고, 마침 Ballast 파일을 만들어주는 커맨드에는 대상 경로에 파일이 이미 있는지 확인하는 안전장치가 없었기 때문에 스크립트의 실행 결과 파티션을 덮어 쓰게 되었습니다. 이 영향으로 데이터베이스의 일부 노드에서 파일 시스템 불안정이 발생하게 되어 쓰기 작업이 실패하게 되고, 클러스터에서 자동적으로 격리되었습니다.&lt;/p&gt;
&lt;p&gt;저희가 사용했던 AWS EC2 인스턴스 타입은 m5d.8xlarge로, 2개의 로컬 스토리지 디바이스를 제공하는 유형입니다. 여러 개의 로컬 스토리지 디바이스가 존재할 경우 EC2에서는 랜덤한 것을 Primary Mount로 삼는데, 이 때 설정 이슈로 인해 지정된 경로가 Primary Mount에 해당할 경우 문제가 발생하였습니다.&lt;sup&gt;&lt;a name=&quot;ftsrc_1&quot; href=&quot;#ft_1&quot;&gt;1&lt;/a&gt;&lt;/sup&gt; 총 24대의 노드로 클러스터를 구성하였으므로 확률적으로는 12대의 노드가 영향을 받아야 했겠지만, 저희의 경우는 과반 이상인 16대의 노드가 영향을 받게 되었습니다. 설계상 CockroachDB는 Raft라는 컨센서스 알고리즘을 차용하므로 동작하려면 적어도 과반 이상의 노드가 필요한데, 하필 그 이상의 노드가 영향을 받게 되었으므로 노드 단위 장애에서 클러스터 전역적인 장애로 번지게 됩니다.&lt;/p&gt;
&lt;p&gt;지금에서야 정리된 형태로 사건을 서술할 수 있게 되었지만, 당시에는 설정 이슈가 있다는 사실을 인지하지 못하던 상태였으므로 작업 수행 직후 미리 설정해두었던 메트릭을 통해 상황을 인지하게 되었습니다. 삽시간에 에러율, 데이터베이스 연결, 그리고 Health Check 알람까지 수백 건의 알람이 발생했는데, 알람의 메세지를 읽기도 전에 여러 알람이 동시다발적으로 발생하는 상황은 일반적으로 생각해도 아주 굉장히 큰일 난 상황일 확률이 높습니다. 정상적인 게임 이용이 어려운 상황임을 인지하자마자 운영팀에 긴급 점검을 요청하여 16시 52분부터 긴급 점검이 시작되었습니다. 이후 데브옵스 팀 전체가 달려들어 약 30분 정도 셸 히스토리와 CRDB 로그, 커널 로그, 그리고 CRDB 소스코드를 분석한 이후 정확히 어떤 일이 일어나고 있는지를 파악할 수 있었습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1600px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 68.05555555555554%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAC5UlEQVR42lWT2ZLkNBBF1XjVvstLVXX1VA/BCx9AfwJRfBtQfC0x9uiSroYHHjJkWemrmyfTbMrmdyHEX8M4/jkK8RiFeihjHkJ9rsZaejaPl5eXR/PSPLqme8bQDY+mOfbN44WxR9d1f3jvL2yJcr94iSC66vWIYDkSrU72sFZiCCOk4uCcwzoHlwwC5TtrYDyF49WIBkpJaK0/2JLl3+9F7Um135Ygtrdz2W6naXud563kuI1x2KzrNqPFZord1My3nN0WQtz0pDc3i29TSZuxbmvb9he2erNfo0ORvGarsU4JM+2DUQjOwgcPawT6voVzdF4cLinAkSMbLEL2NVAOYQMJfrCYhv2WBX7Ksr4nifek8GOQOOkekxfgiSNnDefpkqBwpdyvkeOVUOjAoeJYnT3wmM+SS1Dk0OJiZZ2J2WsOuASHRNyCV3CzRfSaGCkkcviWHb5QflYj8aOLgq2GKhj5+OmwUe0u9EDgVbXGohDwYgW8HEhIYCHXV1pv5Ppw7M2ISXMsJOKCho2SvhvoQhIdxw/Gg9iN66FkVwOVdT1PeDvNOM8L5ingVCRuUeJrIHEqMdsRJ8NxPibhRIKLrFMp0GTm6dBHv6fsD+DVWotpiihUUjAS3knk4rHmhOwox2gcuZnOPTHzhCI6WZNV0LwjQfbBZNF7KoKA00FKeF083teM61QwnRLkWWI9TSQ8Q1FX7apRMvGlzq4l4Lb4eqXJuFD4oyk2qt0TMy2GGo+xWGbqanw6iMFgpo8mCk9jdDiOxG3UHTrVIM8JZZmrTxFCS7Sc5vA00Z8SRxTTfl9NX88EucS+JtXUyTZ1NqKus6iz62qxbV3NUA3tx/hDpTk9GvmdUFXBRX0yXJLeC90sh66KriVhhagFFB8gugZfJDWBGCkxHjnPd5K6LaizhrgKrSqnHD4On01hgv3K+uY3xpo7Y+zfaO7/7X9u2F039PwM9v+cvr8zQdGze9OQDmP2HztkJRPNTwhbAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/b5f1b5d0c1f7d744943f56897b5430ba/33ecc/02-terminal-screen.webp 360w,
/static/b5f1b5d0c1f7d744943f56897b5430ba/263a4/02-terminal-screen.webp 480w,
/static/b5f1b5d0c1f7d744943f56897b5430ba/0c8fb/02-terminal-screen.webp 640w,
/static/b5f1b5d0c1f7d744943f56897b5430ba/521f7/02-terminal-screen.webp 860w,
/static/b5f1b5d0c1f7d744943f56897b5430ba/cbd37/02-terminal-screen.webp 1200w,
/static/b5f1b5d0c1f7d744943f56897b5430ba/64296/02-terminal-screen.webp 1600w&quot; sizes=&quot;(max-width: 1600px) 100vw, 1600px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/b5f1b5d0c1f7d744943f56897b5430ba/53918/02-terminal-screen.png 360w,
/static/b5f1b5d0c1f7d744943f56897b5430ba/9aebd/02-terminal-screen.png 480w,
/static/b5f1b5d0c1f7d744943f56897b5430ba/0f09e/02-terminal-screen.png 640w,
/static/b5f1b5d0c1f7d744943f56897b5430ba/3e3fe/02-terminal-screen.png 860w,
/static/b5f1b5d0c1f7d744943f56897b5430ba/64756/02-terminal-screen.png 1200w,
/static/b5f1b5d0c1f7d744943f56897b5430ba/42cbc/02-terminal-screen.png 1600w&quot; sizes=&quot;(max-width: 1600px) 100vw, 1600px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/b5f1b5d0c1f7d744943f56897b5430ba/42cbc/02-terminal-screen.png&quot; alt=&quot;02 terminal screen&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        빨간 글씨가 많이 있어 딱 봐도 뭔가 잘못되었다는 것은 알 수 있는 당시 커널 작업 화면.
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;상황을 인지한 이후부터 팀의 최우선순위는 데이터베이스 클러스터의 상태를 복원하는 것으로 설정되었습니다. 문제에 대한 대응 방법을 탐색하는 과정에서 AWS와 CockroachDB 기술지원을 요청해보자는 의견이 있었고, 지푸라기 잡는 심정으로 양 측 기술지원팀과 동시에 화상 회의를 진행하였습니다. 설정 오류로 인해 커널 시스템 장애로 이어졌던만큼, 먼저 AWS EC2 팀에 기대어 보았습니다. 저희보다 리눅스 시스템에 대한 이해도가 조금이라도 더 깊을 것이라는 막연한 기대와 더불어 혹시라도 우리가 알지 못하는 사이에 모종의 가상머신 스냅샷을 찍어 보관하고 있지는 않을까 하는 희망을 가져보았지만, 역시나 세상에 그런건 없었고, 별다른 소득 없이 EC2 팀과의 서포트는 종료되었습니다. 이제 심각한 상황을 뭔가 매지컬한 방법으로 빠르게 없던 일로 되돌릴 수 있는 방법이 없다는 사실을 받아들이고 실질적으로 해결할 수 있는 전략을 세워야 했습니다.&lt;/p&gt;
&lt;p&gt;CockroachDB 서포트 엔지니어와도 문의를 이어가고 있었는데, 저희는 먼저 영향 받지 않은 노드들을 바탕으로 과반 이상의 구성원을 잃어 클러스터 형성이 깨져버린 이 상황을 복구할 수 있는지 알아보고 있었습니다. 서포트 엔지니어도 이러한 상황이 흔하지는 않았던 모양인지 내부 엔지니어들과 지속적으로 소통하며 도움을 주려고 노력하였지만, 내부 논의가 썩 긍정적이지는 않았는지, 가장 빠르게 서비스를 복구하는 방법은 이전에 만들어둔 백업 데이터를 바탕으로 복원하는 것이라는 의견을 제시해 주었습니다. 이 경우 백업 데이터가 생성된 시점부터 장애 발생 시점까지의 모든 데이터는 유실되어, 흔히 백섭이라고 말하는 상황이 발생합니다. 냉정하게 생각하면, 주어진 상황을 고려했을 때 일반적으로 떠올릴 수 있는 방법인 것은 맞습니다. 그리고 직접 머신에 접근할 수 없는 CockroachDB 서포트팀 입장에서 원론적인 답변을 하는 것 또한 이해할 수 있습니다.&lt;/p&gt;
&lt;p&gt;그러나 저희 팀은 이에 대해 쿠키런: 킹덤 개발팀과 깊게 논의한 끝에, 시간이 오래 걸리더라도 다른 방법이 존재하는 한 백섭이라는 선택지는 배제하기로 가장 먼저 결정하였습니다. 게임 운영 관점에서도 여러 이유가 있었겠지만, 엔지니어이기 이전에 한 명의 유저로서 적어도 제가 이해한 배경은 ‘유저들이 쿠키들을 막 만나가며 자신만의 왕국을 꾸며가는 과정에서 쌓은 추억을 그렇게 쉽게 없던 것으로 만들 수는 없다’는 것이었습니다. 또 모든 단서를 종합해봤을 때, 저희 팀은 그렇게 높은 확률은 아닐지언정 방법이 아예 존재하지 않는 것은 아니라고 판단했습니다. 따라서 저희는 서비스 중단이 발생하기 바로 이전까지의 모든 데이터를 복원하는 것을 장애 복구의 최종 목표로 설정하고 세 가지 방법을 동시에 진행하게 됩니다.&lt;/p&gt;
&lt;h2 id=&quot;plan-a-운영체제-레벨에서-노드-복원하기-그러나-영-좋지-않은-노드가&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#plan-a-%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-%EB%A0%88%EB%B2%A8%EC%97%90%EC%84%9C-%EB%85%B8%EB%93%9C-%EB%B3%B5%EC%9B%90%ED%95%98%EA%B8%B0-%EA%B7%B8%EB%9F%AC%EB%82%98-%EC%98%81-%EC%A2%8B%EC%A7%80-%EC%95%8A%EC%9D%80-%EB%85%B8%EB%93%9C%EA%B0%80&quot; aria-label=&quot;plan a 운영체제 레벨에서 노드 복원하기 그러나 영 좋지 않은 노드가 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Plan A, 운영체제 레벨에서 노드 복원하기, 그러나 영 좋지 않은 노드가…&lt;/h2&gt;
&lt;p&gt;먼저 Plan A는 앞서 시도해보고 있었던 데이터베이스 클러스터의 상태를 복원하는 것이었습니다. 클러스터의 상태를 복원할 수만 있다면 기존에 보유하고 있던 데이터도 모두 복원할 수 있을 것이기 때문에 따르는 사이드 이펙트가 가장 적을 것으로 예상되었습니다. 이를 위해 일단 영향을 받은 모든 머신에 ebs를 붙이고 dd 커맨드를 사용해 파티션을 복제하였습니다. 이후 파티션을 복원하기 위해 복제된 EBS 파티션에 fsck 명령어를 돌려보았는데, 오류 항목이 너무 많아 실질적으로 복원이 잘 되고 있다고 생각하기 어려웠습니다. 어쨌든 fsck 이후의 결과물을 바탕으로 클러스터를 재구성하려고 시도해보았지만, 여전히 특정 SST 파일을 찾을 수 없다는 등, 정황상 파일시스템 오류가 해결되지 않았습니다.&lt;/p&gt;
&lt;p&gt;여러 가지 시도를 이어가던 중, 23시 02분 경 영향을 받지 않아 정상적인 파일 시스템을 소유하고 있던 노드 하나가 Host Status Failure를 맞고 그대로 내려가 버리고 말았습니다. 물론 이런 경우를 상정하고 Replication Factor를 높게 설정하기도 했지만, 하필이면 장애 복구 중에, 그것도 문제가 생긴 노드도 아닌 멀쩡한 노드에 발생하다니요. 안타깝게도 이때까지 대부분의 작업은 망가진 노드를 복구하는 것에 초점이 맞추어져 있었기 때문에 정상 작동하던 노드는 EBS 복원도 해두지 않았고, 따라서 이제 남아있는 노드는 8대가 아닌 7대가 되어버렸습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1248px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 29.72222222222222%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAIAAABM9SnKAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA80lEQVR42m2Nu07EMBBF9xMokNjY3vEjtuNXvI6TOIHtUECiQFT8BB01P4+3gF0hpFPM1cy5s/v8+Hp7efe5aJ+EiQeuEZUN09IdXSouzVz7GrEwmHeYtvjAf9k9ba+ntPJ1o8vGlo3eP7PyyMIEbqB+BJfBDjUSl7FJWHSYsIt8c7t3wKdpzeVhXE7GJ2V7ra1S1trQdU4qozsHwBGCa/MsIxCBir4fQsx9HIC2DYKGsModggqqM6aI1GtxbZ7lPWGRy5RGZYL1R8oV+qmX2rbKNpj9cS5y/RC4zHk2PrqQKsBkXdSKlMs4r8ZFoSwi/1R8A+v+NIgnxiheAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/a32af6d4e2a8d62b66f95f3b6bf35c67/33ecc/03-ec2-host-failure-datadog-alert.webp 360w,
/static/a32af6d4e2a8d62b66f95f3b6bf35c67/263a4/03-ec2-host-failure-datadog-alert.webp 480w,
/static/a32af6d4e2a8d62b66f95f3b6bf35c67/0c8fb/03-ec2-host-failure-datadog-alert.webp 640w,
/static/a32af6d4e2a8d62b66f95f3b6bf35c67/521f7/03-ec2-host-failure-datadog-alert.webp 860w,
/static/a32af6d4e2a8d62b66f95f3b6bf35c67/cbd37/03-ec2-host-failure-datadog-alert.webp 1200w,
/static/a32af6d4e2a8d62b66f95f3b6bf35c67/92e18/03-ec2-host-failure-datadog-alert.webp 1248w&quot; sizes=&quot;(max-width: 1248px) 100vw, 1248px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/a32af6d4e2a8d62b66f95f3b6bf35c67/53918/03-ec2-host-failure-datadog-alert.png 360w,
/static/a32af6d4e2a8d62b66f95f3b6bf35c67/9aebd/03-ec2-host-failure-datadog-alert.png 480w,
/static/a32af6d4e2a8d62b66f95f3b6bf35c67/0f09e/03-ec2-host-failure-datadog-alert.png 640w,
/static/a32af6d4e2a8d62b66f95f3b6bf35c67/3e3fe/03-ec2-host-failure-datadog-alert.png 860w,
/static/a32af6d4e2a8d62b66f95f3b6bf35c67/64756/03-ec2-host-failure-datadog-alert.png 1200w,
/static/a32af6d4e2a8d62b66f95f3b6bf35c67/f03ba/03-ec2-host-failure-datadog-alert.png 1248w&quot; sizes=&quot;(max-width: 1248px) 100vw, 1248px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/a32af6d4e2a8d62b66f95f3b6bf35c67/f03ba/03-ec2-host-failure-datadog-alert.png&quot; alt=&quot;03 ec2 host failure datadog alert&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        아니 물론 저희가 잘못을 하긴 했지만 이렇게까지 시련을 겪을 필요는 없잖아요… ㅜㅜ
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;plan-b-데이터가-여기에-있는데-이걸-못-쓸-리가-없잖아&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#plan-b-%EB%8D%B0%EC%9D%B4%ED%84%B0%EA%B0%80-%EC%97%AC%EA%B8%B0%EC%97%90-%EC%9E%88%EB%8A%94%EB%8D%B0-%EC%9D%B4%EA%B1%B8-%EB%AA%BB-%EC%93%B8-%EB%A6%AC%EA%B0%80-%EC%97%86%EC%9E%96%EC%95%84&quot; aria-label=&quot;plan b 데이터가 여기에 있는데 이걸 못 쓸 리가 없잖아 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Plan B, 데이터가 여기에 있는데 이걸 못 쓸 리가 없잖아!&lt;/h2&gt;
&lt;p&gt;일련의 과정에서 저희는 CockroachDB가 널리 알려진 RocksDB와 유사한 스토리지 엔진을 차용하고 있다는 점과 데이터가 7개의 복제본을 가지도록 설정해두었던 사실에서 착안하여 스토리지 이슈가 발생하지 않은 노드에 남아있는 데이터를 활용하여 복원할 수 있지 않을까 하는 아이디어를 떠올리고, 두번째 복구 전략인 Plan B를 수립하였습니다. 남아있는 노드가 7대이므로 우선 데이터의 완전 손실율이 C(16, 7) / C(24, 7) ~= 3.3%에 불과한데다, Event Sourcing 아키텍쳐를 사용한 킹덤 서버 구조, 그리고 MVCC 모델을 사용하여 데이터를 증분 저장하는 CockroachDB의 스토리지 레이어 구조를 종합적으로 고려할 때 이 정도 손실율이면 충분히 투자할 가치가 있다고 판단했습니다.&lt;/p&gt;
&lt;p&gt;CockroachDB 서포트 팀에도 이 아이디어를 공유했지만 몹시 회의적인 반응이었습니다. 먼저 수동으로 Low-level Storage Layer에서 데이터를 추출할 수 있는 방법이 있는지를 물었지만 수 주 이상 소요될 것이고 성공하리라는 보장도 없다는 답변을 들었습니다. 이런 상황에 활용할 수 있는 내부툴이 있는지 역시 물었는데, 별로 쓸만하진 않을 것이라고 덧붙이긴 했지만 다행히 디버그 용도로 사용하는 명령어가 있다는 사실을 알 수 있었습니다. 저희는 아예 CockroachDB의 소스코드를 탐색하였고, 이윽고 CockroachDB 스토리지 레이어에서 사용하는 파일 확장자인 SST 파일 내부를 볼 수 있는 명령어를 찾아냈습니다. 저희는 떠올렸던 접근방식이 유효한지 검증하기 위해 PoC를 진행하기로 했지만, 서포트 팀에서는 여전히 귀중한 시간을 불확실한 확률에 낭비하기보다 백업 데이터에서 복원하는 것을 추천하는 등, 분위기가 썩 고무적이지는 않았습니다.&lt;/p&gt;
&lt;h2 id=&quot;plan-c-헨젤과-그레텔도-집을-찾아갈-수-있었다면-우리도&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#plan-c-%ED%97%A8%EC%A0%A4%EA%B3%BC-%EA%B7%B8%EB%A0%88%ED%85%94%EB%8F%84-%EC%A7%91%EC%9D%84-%EC%B0%BE%EC%95%84%EA%B0%88-%EC%88%98-%EC%9E%88%EC%97%88%EB%8B%A4%EB%A9%B4-%EC%9A%B0%EB%A6%AC%EB%8F%84&quot; aria-label=&quot;plan c 헨젤과 그레텔도 집을 찾아갈 수 있었다면 우리도 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Plan C, 헨젤과 그레텔도 집을 찾아갈 수 있었다면 우리도?&lt;/h2&gt;
&lt;p&gt;실제로 이 방법이 성공하더라도 이 방법을 통해 데이터를 복구해내기에는 걸려 넘어질 수 있는 구석이 많아 보였기 때문에, Plan C도 세워둘 필요가 있었습니다. 데이터 완전 복구를 위한 세번째 방법인 Plan C는 가장 최신의 백업 데이터를 사용해 데이터베이스를 어느정도 복원해둔 상태에서, 백업 시점부터 장애시점까지 발생한 분석용 로그 데이터를 바탕으로 사용자의 최종 상태를 복원하는 것이었습니다. 이 방법은 마치 &lt;a href=&quot;https://technology.riotgames.com/news/determinism-league-legends-introduction&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;LoL 중계에서 크로노 브레이크가 발생할 때 동작하는 원리&lt;/a&gt;와도 비슷한데요. 사용자의 행동을 다시감기하는 방법이므로 모든 로그를 완벽히 잘 남기고 있어야만 가능한 방법입니다. 언뜻 그럴듯 해 보이기는 하지만, 이 방법은 수백 종류가 넘는 로그의 정합성이 모두 보장되어야 한다는 전제 조건이 따르며, 사실상 모든 서버 API에 대해서 gRPC로 요청을 받는 방식에서 로그를 바탕으로 처리하는 방식으로 다시 구현해야 한다는 의미였습니다. 또 여러 가지 일반적이지 않은 엣지 케이스들도 처리해야 했고, 테스트를 통해 결과의 동일성 또한 보장할 수 있어야 합니다. 수백가지나 되는 모든 서버 API에 대해서 이러한 작업을 하기에는 시간과 인력, 정신력 모두 부족했기 때문에 우선순위를 두어 가장 핵심적이라고 생각되는 API에 대해서부터 대응을 시작하였습니다.&lt;/p&gt;
&lt;p&gt;Plan A, B, C 모두 어느 하나 쉬운 방법이 없었습니다. 작업자들 모두 자신이 가장 잘 기여할 수 있는 방법에 최선을 다해 기여하면서도, 각자 진행하는 작업이 성공하리라는 확신은 없는 채로 그저 다른 쪽에서 진행하는 방법이 꼭 성공해줄 것이라 믿고 있는 상태였습니다.&lt;/p&gt;
&lt;p&gt;Plan A 쪽에서는 몇 가지 시도를 더 이어간 끝에, 안타깝게도 결국 디스크 복구를 통해서도 리눅스 파일 시스템을 복구하는 것은 영영 불가능하다고 결론내렸습니다. 남아있는 7대의 노드 만으로 클러스터를 재구성할 수 있는지 또한 시도를 해보았는데, CockroachDB 클러스터가 시작할 때 Raft Quorum을 검증하는 로직이 있었고, 영향을 받은 노드들이 존재했던 기록이 남아있어서 Healthy State로 진행하지 못하고 있었습니다. 문서화되지 않은 CockroachDB 내부 툴까지도 총동원하며 이 로직을 우회할 방법이 없는지를 연구해보기도 하였지만, 다음날 오전 5시까지 시도해본 끝에 이 단계를 우회할 수 있는 방법은 찾지 못했고, 따라서 기존 클러스터를 재사용할 수 있을 확률은 매우 희박하다는 결론에 다다랐습니다.&lt;/p&gt;
&lt;p&gt;다행히 Plan B 쪽에서 시도하던 방법이 어느 정도 성공을 거둬 SST 파일이 어떤 구조로 작성되어 있는지 해독하는 데에 성공했습니다. 또 해독한 바이너리 코드를 인코딩 규약에 따라 손으로 파싱을 해보니 그 결과가 그럴듯 했습니다. 이 경험을 바탕으로 장애 발생 다음날 오전 4시 경 Spark 기반으로 SST 파일의 내용물을 CSV 포맷의 텍스트 파일로 추출하는 샘플 코드를 작성할 수 있었습니다. 이 작업의 실행속도를 미리 계산해보니 32-core 머신 150대 규모의 Spark 클러스터를 사용해서 24개 데이터베이스 노드에 존재하는 모든 SST 파일을 변환한다고 할 때 약 24시간이 걸리는 것으로 예상되었습니다. 이는 너무도 긴 시간이었기 때문에 우리가 필요한 작업만을 고려하여 CockroachDB CLI 소스코드를 최적화 할 여지가 없는지 연구를 했고, 개조에 성공하여 결과적으로 약 10배 이상 빨라지도록 개선하기도 하였습니다. 이 과정에 대해 자세하고 기술적인 내용을 담은 글도 작성될 예정이니 참고를 해보셔도 좋겠습니다.&lt;/p&gt;
&lt;h2 id=&quot;다음-날-일단-데이터는-읽었다-그런데-너무-많다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8B%A4%EC%9D%8C-%EB%82%A0-%EC%9D%BC%EB%8B%A8-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%8A%94-%EC%9D%BD%EC%97%88%EB%8B%A4-%EA%B7%B8%EB%9F%B0%EB%8D%B0-%EB%84%88%EB%AC%B4-%EB%A7%8E%EB%8B%A4&quot; aria-label=&quot;다음 날 일단 데이터는 읽었다 그런데 너무 많다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;다음 날, 일단 데이터는 읽었다. 그런데 너무 많다&lt;/h2&gt;
&lt;p&gt;이 방법이 성공할 경우 SST 파일에 존재하는 행(Row) 단위 내용물을 CSV로 추출할 수 있었고, CockroachDB는 &lt;a href=&quot;https://www.cockroachlabs.com/docs/v19.2/import-into&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;CSV의 데이터로부터 테이블로 들여오는 기능&lt;/a&gt;이 있었기 때문에 데이터를 다시 복구해낼 수 있다는 희망이 보였습니다. 마침 저희가 사용하던 버전에 새로 추가된 기능이기도 했습니다. 오전 10시 30분 경부터 최후 생존한 7개 노드에 남아 있던 약 7.5TB 분량의 SST 파일을 CSV로 변환하며 발생하는 에러를 수정하는 사이클을 진행하였습니다.&lt;/p&gt;
&lt;p&gt;이 때 실제로 테이블 스키마를 작성하고 복원하는 작업을 진행해보니 몇 가지 제약사항을 발견했습니다. 먼저 IMPORT INTO 명령어는 데이터를 특정 테이블에 대입하는 방식으로 동작하는데, 명령이 수행되는 동안은 테이블이 마치 존재하지 않는 것처럼 오프라인 상태가 되었고, 단일 Transaction으로 처리되어 Primary Key나 Unique Key Constraint 등에서 충돌이 발생할 경우 모두 Revert 되었습니다. 이러한 동작의 이유는 아마도 동시성 환경에서 테이블을 원자적으로 생성하기 위한 의도였을 것으로 추측되는데, 이 때문에 하나의 SST 파일, 즉 &lt;a href=&quot;https://www.cockroachlabs.com/docs/v22.1/architecture/overview#architecture-range&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Range&lt;/a&gt; 단위로 병렬적으로 데이터를 들여오려던 원래의 계획대로 진행할 수 없게 되었습니다.&lt;/p&gt;
&lt;p&gt;저희가 복원하고자 하는 것은 백만 단위의 사용자에 대한 이벤트소싱 아키텍쳐 하의 유저 데이터였으므로 아무리 성능 좋은 분산 데이터베이스 시스템이더라도 처리하는데 많은 시간이 소요될 수 밖에 없었는데요. 충돌이 발생하여 Transaction Rollback 되게 된다면 그만큼 복구시점이 늦어진다는 의미였으므로, 7.5TB 분량의 데이터에 대해서 신중하고 완벽한 중복처리 작업이 필요하게 되었습니다. 각 Range 파일은 최대 512메가, 혹은 Range Split 알고리즘에 따라 그보다 더 작은 크기로 관리되는데요. 이튿날 오후 12시부터는 발견한 제약사항에 따라 중복을 제거하여 실제 DB에 넣을 수 있는 형태로 된 하나의 거대한 CSV 파일로 병합하는 Spark 코드를 작성하였습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1600px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAPABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAMBAgT/xAAVAQEBAAAAAAAAAAAAAAAAAAAAAv/aAAwDAQACEAMQAAABciKyDQK//8QAGhAAAgMBAQAAAAAAAAAAAAAAAQMAAhIRE//aAAgBAQABBQJrc2HNeixLqzfhMKqtn//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8BP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8BP//EAB8QAAIBAgcAAAAAAAAAAAAAAAABEQISAxMhIjFxgf/aAAgBAQAGPwJUJ7iaofRphL0zJlyXrguaP//EAB4QAAICAQUBAAAAAAAAAAAAAAERACExQVFhcZGx/9oACAEBAAE/IUrArv5CYsORkITyFn3muB6hQKiT5lhmdjmf/9oADAMBAAIAAwAAABCfD//EABYRAQEBAAAAAAAAAAAAAAAAAAABEf/aAAgBAwEBPxCMf//EABcRAAMBAAAAAAAAAAAAAAAAAAABESH/2gAIAQIBAT8Qb0p//8QAHhABAAICAgMBAAAAAAAAAAAAAREhAEFRYTFxkcH/2gAIAQEAAT8QMiAUJr9JZ67wCNg1Rmq1zxeQXpWATzEORCIisIpdnj5GMBuRKdoesVIOhRKbz//Z&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/a116049a8b11f3552597b54e9be1b08d/33ecc/04-zombie-cookie.webp 360w,
/static/a116049a8b11f3552597b54e9be1b08d/263a4/04-zombie-cookie.webp 480w,
/static/a116049a8b11f3552597b54e9be1b08d/0c8fb/04-zombie-cookie.webp 640w,
/static/a116049a8b11f3552597b54e9be1b08d/521f7/04-zombie-cookie.webp 860w,
/static/a116049a8b11f3552597b54e9be1b08d/cbd37/04-zombie-cookie.webp 1200w,
/static/a116049a8b11f3552597b54e9be1b08d/64296/04-zombie-cookie.webp 1600w&quot; sizes=&quot;(max-width: 1600px) 100vw, 1600px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/a116049a8b11f3552597b54e9be1b08d/543cd/04-zombie-cookie.jpg 360w,
/static/a116049a8b11f3552597b54e9be1b08d/a3e66/04-zombie-cookie.jpg 480w,
/static/a116049a8b11f3552597b54e9be1b08d/56d4e/04-zombie-cookie.jpg 640w,
/static/a116049a8b11f3552597b54e9be1b08d/2f567/04-zombie-cookie.jpg 860w,
/static/a116049a8b11f3552597b54e9be1b08d/03ffe/04-zombie-cookie.jpg 1200w,
/static/a116049a8b11f3552597b54e9be1b08d/04bec/04-zombie-cookie.jpg 1600w&quot; sizes=&quot;(max-width: 1600px) 100vw, 1600px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/a116049a8b11f3552597b54e9be1b08d/04bec/04-zombie-cookie.jpg&quot; alt=&quot;04 zombie cookie&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        길고 고된 복구 작업 중 잠시 누워 휴식을 취하는 동료의 모습
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;장애 발생 후 약 22시간이 지나 둘째날 오후 2시 30분 경이 되었을 때 위 작업이 모두 완료되었고, 보다 넉넉한 스토리지를 차용한 새로운 데이터베이스 클러스터에 데이터를 들이붓는 작업을 시작하였는데요. 그럼에도 Row Count와 유저 데이터의 크기가 물리적으로 너무 방대해 지나치게 긴 시간이 소요될 것으로 예상되었기에 각 유저당 킹덤 이벤트소싱 구조 처리에 필요한 최소 단위인 가장 최근 스냅샷 1개와 가장 최근 저널 300개만 복원하기로 다시금 조건을 좁혔습니다.&lt;sup&gt;&lt;a name=&quot;ftsrc_2&quot; href=&quot;#ft_2&quot;&gt;2&lt;/a&gt;&lt;/sup&gt; 데이터 전처리 작업을 위해 Spark Worker Node를 최대한 끌어쓰다보니, 저희가 사용하는 AWS 도쿄 리전의 모든 AZ에서 가용한 R계열 인스턴스를 모두 소진하는 일도 있었습니다. 당시 회사의 최우선 과제는 데이터베이스 복원 작업이었으므로 일상의 다른 작업을 위해 사용 중이던 인스턴스들까지 모두 종료하고 끌어모아 전처리 작업에 활용하기도 하였습니다.&lt;/p&gt;
&lt;p&gt;마침내 오후 4시 20분, 저널 테이블의 복원이 성공적으로 완료되었고 오후 4시 57분에는 스냅샷 테이블의, 오후 5시 22분에는 유저 테이블의 복원에 성공하였습니다. 그러나 기쁨도 잠시, 복원 방법 자체가 사실 정상적인 데이터베이스 작업은 아니고 저희가 설정한 몇가지 가설들을 토대로 하고 있었기 때문에 완벽한 검증을 필요로 했습니다.&lt;/p&gt;
&lt;p&gt;일단 가장 최근의 “정상적인” 데이터베이스 백업으로부터 또다른 데이터베이스를 구성해 정합성 검증에 나섰습니다. 모든 테이블에 대해 고유키를 대상으로 DISTINCT COUNT를 계산해 보았을 때, 자체적으로 복원해낸 데이터베이스(이하 ”리커버 본”)의 Row Count가 데이터베이스 백업으로부터 복원해낸 데이터베이스(이하 “백업 본”)의 Row Count보다 모두 더 많았습니다. 데이터베이스 백업은 실시간이 아니므로 장애 시점의 데이터를 토대로 복원해낸 데이터는 데이터베이스 백업보다 무조건 더 미래시점의 데이터입니다. 런칭 후 얼마 지나지 않아 사용자 탈퇴로 인한 데이터 보관 기한이 도래하지도 않았으므로, Row Count가 더 많다는 것은 일단 긍정적인 신호로 볼 수 있습니다. 또, 백업 본의 사용자 고유 식별자의 집합에서 리커버 본의 사용자 고유 식별자의 집합의 차집합을 구하도록 계산을 해보니 공집합을 얻었습니다. 이것은 백업 본이 리커버 본의 부분 집합이라는 의미이므로, 일단 백업 본에 존재하던 데이터들은 모두 복원에 성공한 것으로 해석할 수 있습니다! 이 날 들은 소식 중에 단연 가장 기쁜 소식이었습니다!&lt;/p&gt;
&lt;p&gt;그러나 기쁜 소식만 있었던 것은 아니었던 것이, 간단한 검증 과정을 통해 유저 테이블에 오염이 심각한 것을 발견할 수 있었습니다. 왕국 이름에 중복되는 항목이 많다거나, 변화가 생겨야 하는 필드들이 그대로 남아있는다거나 하는 이상한 항목들도 많았고, 탈퇴한 유저들의 경우 또한 제대로 복구되지 않았습니다. 이 유저 테이블은 필수적인 테이블은 아니지만 유저 정보를 보여주기 위한 일종의 Projection Table인데, 이 테이블의 정보가 제대로 되어 있지 않으면 유저의 실제 상태와 프로필 등에서 보여지는 상태가 달라지는 이슈가 발생할 수 있습니다. 다행히 Plan C 작업에 몰두하시던 서버 엔지니어 분들께서 진실의 근원인 스냅샷과 저널 테이블을 기반으로 유저 테이블을 처음부터 재구성할 수 있도록 로직을 추가해주셔서 복원 대상에서 제외하기로 결정하였습니다. 나중에 알고 보니 INSERT/DELETE 쿼리 위주로 사용하는 저널과 스냅샷 테이블과 다르게 유저 테이블은 UPDATE가 자주 발생하는 테이블이었고, MVCC 데이터 처리 과정에서 중점적으로 고려한 시나리오는 킹덤 서버 구동에 필수적인 저널과 스냅샷 테이블의 복원 방식에 초점이 맞추어져 있어 전처리 과정에서 문제가 있었던 것으로 밝혀지기는 하였습니다.&lt;/p&gt;
&lt;h2 id=&quot;마침내-서비스-복구-가시화와-만일을-위한-정합성-검증&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%A8%EB%82%B4-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%B3%B5%EA%B5%AC-%EA%B0%80%EC%8B%9C%ED%99%94%EC%99%80-%EB%A7%8C%EC%9D%BC%EC%9D%84-%EC%9C%84%ED%95%9C-%EC%A0%95%ED%95%A9%EC%84%B1-%EA%B2%80%EC%A6%9D&quot; aria-label=&quot;마침내 서비스 복구 가시화와 만일을 위한 정합성 검증 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;마침내! 서비스 복구 가시화와 만일을 위한 정합성 검증&lt;/h2&gt;
&lt;p&gt;오후 7시부터는 데이터 복원이 모두 성공되었을 때를 가정하고 서비스를 재개할 때 사용할 깨끗한 데이터베이스를 준비하기 시작했습니다. 지금까지 사용하던 데이터베이스는 여러 번의 실패도 겪었던 클러스터인지라 아무래도 정리되지 않은 쓰레기값을 내부적으로 들고 있을 수 있어 성능에 영향을 줄 수도 있기 때문에 프로덕션에 그대로 사용하기는 찝찝합니다. 복원에 성공한 2개의 필수 테이블 외에도 언급되지 않은 다양한 메타데이터성 테이블이나 데이터의 최신성이 크게 중요하지 않은 보조 테이블들도 있었는데, 이런 것들은 단순히 백업 시점의 데이터를 활용하는 것으로 타협하였습니다. 최종적으로 저널, 스냅샷, 유저 테이블 이외의 테이블은 백업에서 복원하였고, 저널과 스냅샷 테이블은 SST 파일에서 추출하여 전처리를 거친 CSV 파일에서 Import 하였으며, 유저 테이블은 서버 로직으로 재구성하는 방식으로 진행하여 마침내 오후 8시에는 모든 작업이 완료되었습니다.&lt;/p&gt;
&lt;p&gt;그리하여 최종적으로 서비스를 받을 수 있는 준비가 되었지만, 서비스 오픈 이후 문제가 발생한다면 더 큰 문제로 이어질 수도 있기 때문에 조금 더 신중을 기하였습니다. 일부 유저를 샘플링하여 주요 상태에 대해서 분석 로그에 기록된 상태와 최종값이 같은지를 검증하였습니다. 모든 유저에 대해서 진행할 수 있었다면 좋았겠지만, 이 작업은 한 건당 매우 오래 걸리기 때문에 부득이 샘플링을 할 수 밖에 없었습니다. 또, 누락된 저널이 있다면 스냅샷 리플레이가 제대로 되지 않고 게임 플레이 진행에 문제가 생길 수 있다고 판단하여, 모든 유저에 대해서 스냅샷 리플레이가 정상적으로 작동하는지를 검증까지 한 결과 모든 유저의 리플레이가 정상적으로 가능한 것을 확인했습니다. 이로써 모든 사용자 데이터의 정합성이 일치한다는 확신을 얻을 수 있었습니다. 마지막으로 간단한 기능 테스트까지 진행하여 서비스 오픈까지도 할 수 있는 상황임을 확인하였습니다.&lt;/p&gt;
&lt;p&gt;QA 상황보다 테스트 모수를 더 많이 확보하기 위하여 오후 10시에는 사내망 테스트까지 진행하여 사내 구성원들이 마지막으로 기억하고 있는 계정의 상태와 일치하는지 또 한번 검증하기도 하였습니다. 이론적으로도 일치해야 했고, 실제로 코드를 통해 일치하는 것도 확인한 것은 맞지만, 이 과정이 모두 사람이 진행한 일인지라 직접 눈으로 확인하고 이상이 없다는 말을 듣고 나서야 복구에 참여한 엔지니어들도 비로소 안심할 수 있었습니다. 이 때 늦은 시간임에도 굉장히 많은 사내 구성원들이 사내망 테스트에 참여해주셔서 오랜 복구 작업이었음에도 모든 구성원들이 관심을 가지고 응원해주고 계셨다는 것을 몸소 느낄 수 있어 감동적이었습니다.&lt;/p&gt;
&lt;h2 id=&quot;긴급-점검-해제-해치웠나&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B8%B4%EA%B8%89-%EC%A0%90%EA%B2%80-%ED%95%B4%EC%A0%9C-%ED%95%B4%EC%B9%98%EC%9B%A0%EB%82%98&quot; aria-label=&quot;긴급 점검 해제 해치웠나 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;긴급 점검 해제, 해치웠나?&lt;/h2&gt;
&lt;p&gt;최종적으로 이튿날 23시 37분, 30시간 50분 만에 서비스를 다시 오픈할 수 있게 되었습니다. 감사하게도 인내심을 가지고 기다려주신 유저분들께서 커뮤니티에 많은 글을 남겨주셨는데요. 작업 중간중간 제보받은 센스 넘치는 게시글들을 보며 많은 힘을 얻을 수 있었습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1562px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 59.166666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAAAsTAAALEwEAmpwYAAADk0lEQVR42k1TbUybVRh9+UGyhGRkS2fd9Icmxs1shRJKMQaQwmDtMkpWCkUiJGYaYzLYWhbDp5uC6OjiB6aMYjcpEJwd5XPtIEuGietCGYK4iRZldh2FhI1+0Q/KfW+Pb/vLm5w/J+c59znPcy/z6+K/783NP/7OPrd6eXFpVQtAq9Nd7eTzX9DyeDxtUlKSNjk5WcswjPalgwevLD982BONRns43f9xlRB63Waz5TEeb9gS45gtT4j1ByKIH6PRCIEgDRKJBEKhMAGBQACZVArXEyf8gW2Ew2FsBwPw+T2IREJsvG5iYuIC43K5JjgCz55t7mxsuAnHk8GBAZIhFBBJfg7Jy32LnCguJPds98mffznImnudOJ+4yNr6BmlpVHOaTKKqyo/W1inQ9Y1WzYRCIUvcfXs7yAYCgUSHA4NDeONoAY5lFuLQq1yHWVI833oOX8CDYNAHlo3fy+KL1ouQ5Bai6HgR7Whrhb67u57xeDyWWCwGr9fLbnk8CXHvtX4cFZUjv6AS5zQduPRZD35fXcKNkWvoNnyJ68avoev6FFc6PsfFpiaMmfUU7CYmx4frmaXfFi1utxsrDgfrcDiw9vQpLnd+BZFAhKJiBX65N4aFR1aUyGU4VSJBXf0ZlL9bipMKGS61tmJhzgbKblLEfBifHK1n/l5xWOMxdwnLRnd347NEU1MzhOlCHMs9DWGOFLmlKvTfNOH7PgPa2j/BzeEfcctixuo/KzSRnSuPe1it1vMMy7LjAc8GZu9OknDQi0d/LEOUJYY4+01kZYkSxmJxNvR6Pb7t6oKxvx8/z8xgdtYO+4M5mIeHMTU1RWc4TqfTaRi/b2v08fIcbhi0ZOa2Cbb7szjAPwS5vBQNDQ3QaDRobGxEXV0dztaehUZ9HkcOvw6VSoWRkVsokOQjLS2dKhQKKJXKemY3Gh2NhAJYXlqI/jTYRycnJmnqfj6tPaemTqeTrq9vUK/XR70+H93Z2aF2u53m5L1Nm2s/ooOdbdT4Qx/l3inJyMhAdXW1holEo2Px/AG/jxBuhg/m53HgxZfxymtHIBRl43jxSZyQlUAqkyM9U4xT8tP48IP30fqxmlteJ9rb29Hc3MxWVVWhpqbmAjM0NCTq7e19R28wlJnNY0pOUJaaur88JWVvxZ49KRX7ePyKvft4CTBMcoVIlF1psVhUd+/cKTOZTEqz2azkfkjZ9PR0ZUtLy+H/AKJqf3iP/gIEAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/76a5699c4567eb433d0d1c3bc5e6267d/33ecc/05-community-reactions.webp 360w,
/static/76a5699c4567eb433d0d1c3bc5e6267d/263a4/05-community-reactions.webp 480w,
/static/76a5699c4567eb433d0d1c3bc5e6267d/0c8fb/05-community-reactions.webp 640w,
/static/76a5699c4567eb433d0d1c3bc5e6267d/521f7/05-community-reactions.webp 860w,
/static/76a5699c4567eb433d0d1c3bc5e6267d/cbd37/05-community-reactions.webp 1200w,
/static/76a5699c4567eb433d0d1c3bc5e6267d/63f33/05-community-reactions.webp 1562w&quot; sizes=&quot;(max-width: 1562px) 100vw, 1562px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/76a5699c4567eb433d0d1c3bc5e6267d/53918/05-community-reactions.png 360w,
/static/76a5699c4567eb433d0d1c3bc5e6267d/9aebd/05-community-reactions.png 480w,
/static/76a5699c4567eb433d0d1c3bc5e6267d/0f09e/05-community-reactions.png 640w,
/static/76a5699c4567eb433d0d1c3bc5e6267d/3e3fe/05-community-reactions.png 860w,
/static/76a5699c4567eb433d0d1c3bc5e6267d/64756/05-community-reactions.png 1200w,
/static/76a5699c4567eb433d0d1c3bc5e6267d/545d7/05-community-reactions.png 1562w&quot; sizes=&quot;(max-width: 1562px) 100vw, 1562px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/76a5699c4567eb433d0d1c3bc5e6267d/545d7/05-community-reactions.png&quot; alt=&quot;05 community reactions&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        당시 서버 오픈을 기다려주시던 유저 분들의 소중한 반응들
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;이렇게 서비스를 다시 오픈하여 쿠키런: 킹덤은 안정적으로 운영을 해오고 있습니다. 라고 마무리를 할 수 있다면 좋겠지만 그렇지 못했죠. 바로 이렇게 긴 시간 점검을 했던 것도 처음이었고, 그만큼 많은 유저분들께서 서비스 오픈을 기다리고 계셨다는 것을 간과하였다는 것인데요. 쿠키런 킹덤에 로그인을 하게 되면 데브시스터즈 게임 플랫폼인 DevPlay로 먼저 로그인, 인증 관련 요청이 들어오는 구조로 되어있습니다.&lt;/p&gt;
&lt;p&gt;중요한 컴포넌트이지만 정상적인 상황에서는 요청량도 많지 않고 오토스케일링도 적용되어 있기 때문에 문제가 되는 일이 많지 않았지만, 서버가 열려있지 않은 상태에서 많은 유저 분들께서 언제 서버가 열리는지 지속적으로 확인하시던 것이 게임 진입 플로우상 DevPlay 플랫폼에 대한 부하로 이어지게 되었고, 플랫폼 데이터베이스 장애로 이어지는 Cascading Failure가 발생했습니다. 당시 플랫폼 서버에 발생했던 순간 API 수는 글을 작성하는 지금까지도 역대 최고 기록으로 남아 있습니다. DevPlay 플랫폼의 데이터베이스는 AWS RDS로 구성되어 있어 일시적 순단을 각오하고 Failover를 수행한 뒤에 정상화되었습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1600px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 27.500000000000004%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAIAAABM9SnKAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAyklEQVR42mP4/fv3P0LgPxhgijNgFUVo+/fv288/j95/e/3lJ1DhXyAEg79AANGMC/wF27n/3vuAZZe7Dj8Esv/8BekDIoiVDEDuH7A5mOj3H5CiCcce+yy51HzgPsi0/38hxv768x1oMD6bIX5tOfjAd+nF9E03Tp87d+/soXtXTj+8fv7IgcPX79xnePLh24F7b++9+fLkw3cIAvrwwbuvN159Pvf046rLL8p33i7ecbtix625+y+s2n9u+faTK7afXbb9/KZDpwCuW07sFjmTYwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/b4a877363c4dec1bfe40ea51ecce31e7/33ecc/06-devplay-api-call-count.webp 360w,
/static/b4a877363c4dec1bfe40ea51ecce31e7/263a4/06-devplay-api-call-count.webp 480w,
/static/b4a877363c4dec1bfe40ea51ecce31e7/0c8fb/06-devplay-api-call-count.webp 640w,
/static/b4a877363c4dec1bfe40ea51ecce31e7/521f7/06-devplay-api-call-count.webp 860w,
/static/b4a877363c4dec1bfe40ea51ecce31e7/cbd37/06-devplay-api-call-count.webp 1200w,
/static/b4a877363c4dec1bfe40ea51ecce31e7/64296/06-devplay-api-call-count.webp 1600w&quot; sizes=&quot;(max-width: 1600px) 100vw, 1600px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/b4a877363c4dec1bfe40ea51ecce31e7/53918/06-devplay-api-call-count.png 360w,
/static/b4a877363c4dec1bfe40ea51ecce31e7/9aebd/06-devplay-api-call-count.png 480w,
/static/b4a877363c4dec1bfe40ea51ecce31e7/0f09e/06-devplay-api-call-count.png 640w,
/static/b4a877363c4dec1bfe40ea51ecce31e7/3e3fe/06-devplay-api-call-count.png 860w,
/static/b4a877363c4dec1bfe40ea51ecce31e7/64756/06-devplay-api-call-count.png 1200w,
/static/b4a877363c4dec1bfe40ea51ecce31e7/42cbc/06-devplay-api-call-count.png 1600w&quot; sizes=&quot;(max-width: 1600px) 100vw, 1600px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/b4a877363c4dec1bfe40ea51ecce31e7/42cbc/06-devplay-api-call-count.png&quot; alt=&quot;06 devplay api call count&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        1차 오픈 이후 DevPlay 플랫폼 서버에 발생했던 API Call Count.
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;한편 얼마 지나지 않아 킹덤의 데이터베이스에도 부하가 누적되기 시작했습니다. 쿠키런: 킹덤은 게임 특성상 자정에 많은 요청이 발생하는데, 이 때를 기점으로 DB Timeout으로 인한 에러율이 관측되기 시작했습니다. 당시 클러스터 일부 노드의 CPU는 100%를 사용하는 등 매우 불안정한 상황이 이어졌고, DB가 감당할 수 있는 수준의 부하를 넘어섰다고 판단하고 있었습니다. 원래라면 적절한 수준의 부하가 유지될 때 분산 데이터베이스의 특성을 활용해 Scale out을 할 수 있어야 하겠지만, 이번 경우에는 워낙 빠르게 악화되었기 때문에 뒤늦게 Scale out을 진행하는 것은 적절하지 않았습니다. 새벽 시간대임을 감안하여 점차 요청이 줄어들 것으로 예상하고 모니터링을 하고 있었지만 새벽 2시 42분, 다시 데이터베이스에서 모든 쿼리와 요청이 실패하는 것을 관측하자마자 2차 긴급 점검에 들어갈 수 밖에 없었습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1980px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.666666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAQACBf/EABUBAQEAAAAAAAAAAAAAAAAAAAAB/9oADAMBAAIQAxAAAAHtoRuav//EABgQAAMBAQAAAAAAAAAAAAAAAAABMRAR/9oACAEBAAEFAuocxw//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/AT//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/AT//xAAXEAEAAwAAAAAAAAAAAAAAAAAAICEx/9oACAEBAAY/AlMh/8QAHBAAAQQDAQAAAAAAAAAAAAAAAQAQEUEhMZGh/9oACAEBAAE/IYjXVZ4hh2iIwb//2gAMAwEAAgADAAAAEJzP/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPxA//8QAFREBAQAAAAAAAAAAAAAAAAAAEBH/2gAIAQIBAT8Qp//EAB8QAAIBBAIDAAAAAAAAAAAAAAERACExQVFh0XGhsf/aAAgBAQABPxAoCR8h3HNRts+xk5eu4rUF9SuytjmCwn//2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/b86912ec234d20ae926d8c363d815a2e/33ecc/07-crdb-dashboard.webp 360w,
/static/b86912ec234d20ae926d8c363d815a2e/263a4/07-crdb-dashboard.webp 480w,
/static/b86912ec234d20ae926d8c363d815a2e/0c8fb/07-crdb-dashboard.webp 640w,
/static/b86912ec234d20ae926d8c363d815a2e/521f7/07-crdb-dashboard.webp 860w,
/static/b86912ec234d20ae926d8c363d815a2e/cbd37/07-crdb-dashboard.webp 1200w,
/static/b86912ec234d20ae926d8c363d815a2e/5509a/07-crdb-dashboard.webp 1980w&quot; sizes=&quot;(max-width: 1980px) 100vw, 1980px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/b86912ec234d20ae926d8c363d815a2e/543cd/07-crdb-dashboard.jpg 360w,
/static/b86912ec234d20ae926d8c363d815a2e/a3e66/07-crdb-dashboard.jpg 480w,
/static/b86912ec234d20ae926d8c363d815a2e/56d4e/07-crdb-dashboard.jpg 640w,
/static/b86912ec234d20ae926d8c363d815a2e/2f567/07-crdb-dashboard.jpg 860w,
/static/b86912ec234d20ae926d8c363d815a2e/03ffe/07-crdb-dashboard.jpg 1200w,
/static/b86912ec234d20ae926d8c363d815a2e/d1888/07-crdb-dashboard.jpg 1980w&quot; sizes=&quot;(max-width: 1980px) 100vw, 1980px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/b86912ec234d20ae926d8c363d815a2e/d1888/07-crdb-dashboard.jpg&quot; alt=&quot;07 crdb dashboard&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        2차 긴급 점검 당시 CockroachDB 클러스터 대시보드
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;새로운 데이터베이스 클러스터를 구성할 때는 i3계열 인스턴스를 사용하는 것으로 변경했었는데요, i3계열 인스턴스는 스토리지 특화 인스턴스임에도 지표를 보았을 때 Disk Write Ops로 인한 부하가 CPU 부하로까지 이어진 것으로 추측할 수 있었습니다. 이로 인한 컨텐션이 충분히 빠르게 해소되지 않고 있다고 판단했기 때문에 다시 한 번 새 데이터베이스 클러스터를 구성하기로 결정했고, 이번에는 공식 지원하는 데이터베이스 백업 및 복원 기능을 사용하여 데이터를 안전하게 이전할 수 있었습니다. 워크로드에 대해 충분히 테스트를 사전에 진행하지 않은 채로 다른 인스턴스 타입을 선택한 것은 지나친 모험이었다는 의견이 있었기 때문에, 기존에 사용하던 m5d 인스턴스를 사용하되 더 많은 인스턴스 대수를 사용하여 3번째 데이터베이스 클러스터를 구성하기로 결정하였습니다. 데이터 규모가 큰 만큼 이전에 많은 시간이 걸렸는데, CockroachDB 서포트 엔지니어들과 논의하여 아예 TPC-C 벤치마크에 사용되는 것과 비슷한 설정값을 적용하여 많은 시간을 단축하기도 하였습니다.&lt;/p&gt;
&lt;p&gt;이와 같은 과정을 거쳐 오전 8시 30분, 총 점검 시간 36시간 29분 만에 임시점검을 종료하게 되었습니다.&lt;/p&gt;
&lt;h2 id=&quot;요약&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9A%94%EC%95%BD&quot; aria-label=&quot;요약 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;요약&lt;/h2&gt;
&lt;p&gt;점검 경과를 요약하면 다음과 같은 내용이 되겠습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;쿠키런: 킹덤 런칭 이후 지속적인 이용자 증가로 인해 데이터베이스 저장장치 공간 부족 현상 가속화&lt;/li&gt;
&lt;li&gt;안정적인 데이터베이스 운영을 위해 필요한 작업을 수행하려 했으나, 이 과정에 발생한 인적 오류로 인해 전체 데이터베이스 클러스터의 작동 중지&lt;/li&gt;
&lt;li&gt;해결을 위해 데이터베이스 소스 코드와 기술문서 등 가용 자원을 총동원하여 노드에 퍼져있는 바이너리 형식의 데이터 파일을 모아 해독 가능한 형태로 변환할 수 있음을 확인&lt;/li&gt;
&lt;li&gt;데이터베이스 소스 코드를 개량하고 데이터 분석용 분산 인프라를 활용해 이진 데이터 파일을 데이터베이스 복원에 사용할 수 있는 형태로 재가공.
&lt;ul&gt;
&lt;li&gt;이 과정에서 일부 데이터 유실에 대한 위험이 있기는 하였으나, 약간의 운과 게임 아키텍처의 특성을 활용해 모든 데이터 복원 및 재구축에 성공&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;복원해낸 데이터를 바탕으로 새로운 데이터베이스 클러스터를 구축하여 정합성 검증&lt;/li&gt;
&lt;li&gt;서비스 오픈을 결정했으나, 미증유의 트래픽을 받아 다시금 서비스 품질 불안정 발생, 두번째 데이터베이스 클러스터도 서비스 수용 불가 상태&lt;/li&gt;
&lt;li&gt;위 상황을 고려해 여러 설정을 추가 적용한 세 번째 데이터베이스 클러스터를 구축하여 게임 서비스를 최종 복구&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;회고&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%9A%8C%EA%B3%A0&quot; aria-label=&quot;회고 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;회고&lt;/h2&gt;
&lt;p&gt;회사 역사상 유래가 없었던 당시의 점검을 회고해보자면, 가장 먼저 사람의 실수로 시스템의 오류가 발생한 만큼 인프라 작업 시의 프로세스를 전면적으로 개선하였습니다. 프로덕션 인프라에 어떤 사소한 작업을 하더라도 반드시 2명 이상의 작업자가 참여해야 한다고 규칙을 개선했고, 작업 중 사용할 스크립트나 진행할 작업에 대한 시나리오를 엄격하게 작성하여 사전에 팀 내에 공유하고 리뷰를 받도록 하였습니다. 아무리 숙련되고 경험이 많은 작업자라고 하더라도 사람인 이상 실수를 할 수도 있으며, 이를 예방하기 위해서는 시스템을 갖추어야 한다고 생각했기 때문입니다.&lt;/p&gt;
&lt;p&gt;아무리 회고해보아도 아슬아슬한 지점이 많았지만, 그럼에도 복구를 가능하게 했던 몇 가지 요인들이 있다고 생각합니다. 먼저 데브시스터즈에는 장애 대응에 대해 확고한 원칙이 이미 사전이 정립되어 있었고, 발생한 장애에 대해 분석하고 회고하는 문화가 있었습니다. 이 장애 대응 원칙에는 작업자 개인을 탓하는 것보다는 상황 해결을 위해 모두가 적극적으로 기여하고, 대응 과정을 상세히 기록으로 남겨서 동일한 유형의 장애를 예방할 수 있는 액션아이템을 설정하여야 한다고 되어 있습니다. 이번 경우에도 발생한 장애에 대해 숨김 없이 공유가 된 덕분에 해결 방안을 고안하는데 모두가 머리를 맞댈 수 있었습니다. 또, 다년간의 게임 운영 경험에서 이미 축적되어 있는 크고 작은 장애 경험이 풍부했고, 그 경험들이 기록으로 남아있었기 때문에 장애를 직접 경험하지 않은 구성원들도 대형 장애 상황에서 어떤 일을 수행해야 하는지 간접적으로나마 알고 있었습니다. 이런 문화적인 배경 덕분에 여러 가지 복구 전략을 시도해 볼 수 있었다고 생각합니다.&lt;/p&gt;
&lt;p&gt;또 다른 요소로는 장애 해결에 필요한 인프라와 전문성이 모두 미리 갖추어져 있었고, 인프라 운영과 데이터 분석을 담당하는 팀들이 한 조직에 속해 있어 서로의 역량이 시너지를 내며 복구 시간을 획기적으로 단축할 수 있었습니다. 그러나 무엇보다 중요한 것으로 구성원들의 꺾이지 않는 마음을 꼽지 않을 수 없습니다. 오랜 시간동안 체력적, 정신적으로 한계에 부딪힌 상황 속에서도 모두가 최선을 다해 더 긴 장애로 이어지거나 데이터 유실이 발생하는 최악의 상황을 피할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;한 명의 엔지니어로서 솔직히 다시 기억을 떠올리기 싫은 사건이었습니다만, 일련의 과정을 거치면서 우리가 이 정도로 심각한 문제도 극복해낼 수 있는 팀이라는 것을 느끼는 순간이었습니다. 이 같은 문제를 숨기는 것이 아니라 널리 공유하는 문화가 전파되어야 업계 전체의 역량이 성장할 수 있다고 생각하여 꽤 긴 시간이 지났지만 글의 형태로 공유하게 되었습니다. 긴 글 읽어주셔서 감사드립니다. 하지만 저희 발표를 들으셨거나, 이 당시 킹덤을 플레이하고 계시던 분들은 다른 사건도 있었다는 것을 기억하고 계시겠죠? 이 다음에 있었던 이야기에도 많은 관심 가져주시면 감사하겠습니다.&lt;/p&gt;
&lt;p&gt;덧1) CockroachDB 측에서도 안전장치의 필요성에 대해 공감하여 빠르게 개선해 주었습니다. (&lt;a href=&quot;https://github.com/cockroachdb/cockroach/pull/59995&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;관련 PR&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;덧2) &lt;a href=&quot;https://www.cockroachlabs.com/docs/v21.2/cockroach-debug-ballast&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;21.2 버전 이상의 CRDB&lt;/a&gt;에서는 &lt;a href=&quot;https://github.com/cockroachdb/cockroach/issues/66493&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Ballast 파일이 자동으로 생성되어&lt;/a&gt;, 저희가 진행하려던 작업을 진행할 필요가 더 이상 없습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;a name=&quot;ft_1&quot; href=&quot;#ftsrc_1&quot;&gt;1&lt;/a&gt;: 이 대목을 읽으면 그러면 RAID는…? 이라는 질문을 하실 수도 있으실텐데, 런칭을 준비하며 막바지에 인스턴스 타입이 결정되는 바람에 RAID 설정에 대한 우선순위를 낮게 설정하고 작업하지 않았습니다. 돌이켜 생각해보면 아무런 설정도 하지 않았기 때문에 확률적으로 살아있는 스토리지가 있었다고도 할 수 있고, 반대로 RAID 설정을 했다면 Storage Pressure가 발생하지 않았다고도 할 수 있겠습니다. 제 의견으로는 RAID0 설정을 진행해두어 장시간 점검이 없었던 평행우주에서도, 당시 Replication Factor 등 용량 측면에서 비효율적인 설정들이 많았기 때문에 스토리지 압박은 시간 문제였을 것 같기는 합니다 😅&lt;/p&gt;
&lt;p&gt;&lt;a name=&quot;ft_2&quot; href=&quot;#ftsrc_2&quot;&gt;2&lt;/a&gt;: 킹덤 아키텍쳐와 관련된 자세한 기술적 배경은 &lt;a href=&quot;https://www.youtube.com/watch?v=FUEZibcZEkg&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;권태국님의 NDC 발표&lt;/a&gt;를 참고하시면 좋겠습니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[CockroachDB in Production]]></title><description><![CDATA[CockroachDB의 여러 가지 고유한 특성을 알아보고, 왜 CockroachDB를 메인 데이베이스로 선택했는지, Production 서비스에서 CockroachDB를 운용하기 위해 어떤 점들을 주의하고 신경써야 하는지에 대해 다룹니다.]]></description><link>https://tech.devsisters.com/posts/cockroachdb-in-production</link><guid isPermaLink="false">https://tech.devsisters.com/posts/cockroachdb-in-production</guid><pubDate>Tue, 13 Dec 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;이 글과 관련된 다른 글 보기&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/posts/cookierun-kingdom-launch-revisited/&quot;&gt;쿠키런: 킹덤 런칭 회고&lt;/a&gt; - DevOps 관점에서 본 쿠키런: 킹덤 런칭 회고&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/crk-launch-storage-postmortem/&quot;&gt;쿠키런: 킹덤 데이터베이스 스토리지 레이어 복원기&lt;/a&gt; - 엔지니어 관점 기록한 개괄적인 장애 대응 과정과 회고&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/bit-level-database-hacking&quot;&gt;CTO가 커리어를 걸고 비트 레벨까지 내려가서 DB를 해킹했던 이야기&lt;/a&gt; - CockroachDB의 소스코드를 참고하며 바이너리 파일로부터 데이터를 복구한 이야기&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/36h-downtime-on-first-day/&quot;&gt;입사 첫날에 36시간 점검 경험하기&lt;/a&gt; - 신규입사자의 관점에서 서술된 장애 대응 기록&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/crk-aws-az-failure-postmortem/&quot;&gt;쿠키런: 킹덤 AWS AZ 장애 아웃라인&lt;/a&gt; - AWS AZ 장애로부터 서비스를 복구한 이야기&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/crk-hot-range-postmortem/&quot;&gt;쿠키런: 킹덤 길드 업데이트 이후 서비스 이슈 되돌아보기&lt;/a&gt; - 쿠키런: 킹덤에 길드 기능이 추가된 이후 발생한 서비스 이슈 회고&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;summary&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#summary&quot; aria-label=&quot;summary permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Summary&lt;/h2&gt;
&lt;p&gt;세상에는 다양한 종류의 데이터베이스 소프트웨어가 존재합니다. 특히 이용자가 많은 모바일 게임의 메인 데이터베이스로 사용하려면 쓰기 위주의 (Write-Heavy) 워크로드에 적합해야 하고, 이용자가 급격히 늘어날 때 빠르게 확장할 수 있어야 합니다. 대표적으로 MySQL, PostgreSQL과 같은 전통적인 관계형 데이터베이스를 고려해볼 수도 있고, Couchbase나 MongoDB, Cassandra와 같은 문서형 분산 데이터베이스를 고려해볼 수도 있고, DynamoDB와 같은 관리형 데이터베이스를 고려해볼 수도 있습니다.&lt;/p&gt;
&lt;p&gt;데브시스터즈에서는 &amp;#x3C;쿠키런 킹덤&gt; 의 데이터베이스로 CockroachDB라는 데이터베이스를 운용하고 있습니다. 이 글에서는 CockroachDB의 여러 가지 고유한 특성을 알아보고, 왜 CockroachDB를 메인 데이터베이스로 선택했는지, 그리고 Production 서비스에서 CockroachDB를 운용하기 위해 어떤 점들을 주의하고 신경써야 하는지에 대해 다룹니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1260px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 41.388888888888886%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsTAAALEwEAmpwYAAACJUlEQVR42g2SW1NSARSFD1evSSmKEgECKgdEjoAHjqIStwpQJAHBO0GMZYWGhjqNYz41zXR5aHzqv36dh2/2ntmz1+xZawtjJhc2s4egKLNW7FFs/SRV+sicO0N26pyQqYZet4RGK6HVRhE0GQz6XcbNH7D5uthX3+MsfkYqn5ESFYRnY6qYf5VkosjD6Ssuu13imy18jjxHllsc+hw6Ywi9QUQQ3Coyw8YSbusJvsAVgUQPsXbHWveB9uEVwlPzNBllmYu0j9+lAO03TcS1CtLkHm3rLbK4z8pKncTzGl5vCoNuEcvgCyTHW5TFHon0PYW9O2r5Fo3lbQTHuI0t2ct1Pc31SZnjagW/L8eu+4rKZAclWkeWcyhKnlisyOPhKH5rgXWxSS5+S630h3f5T5SiWeqKhOC2zbI+7aLm9fBl/4jSZpNC6pR75S+bwQ7z82lmZkIqS4hiklCwSjLSZDt1Tn3rB82DfzTipzRe7tDZi6kXWlykIlF2ExIbTkldylCxnlEduCTsKSMtRJnzqoKzMVU8h9Wa55E+g9VUwDVxTEz6zk7omo7c4mK1jDA+YsHvXiCymGNuOk5mqkHDeEN46ADzkBOdfgCtblRN2a4SRKOJq0kn6etLYZ2oosjfaDy/4dCZpRJWZ5OmWeyjHiwTMsv2Q5r9vyiMfCUwluPJoE19ETtGfUCtqyp5td+m3/Ba9XKDGWeNeKRH1tsm6yiwEc7yH33cDPIU161cAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/a79ca340bd95a1957cf02e518bcc4c5b/33ecc/01-devsisters-cockroachdb-case-study.webp 360w,
/static/a79ca340bd95a1957cf02e518bcc4c5b/263a4/01-devsisters-cockroachdb-case-study.webp 480w,
/static/a79ca340bd95a1957cf02e518bcc4c5b/0c8fb/01-devsisters-cockroachdb-case-study.webp 640w,
/static/a79ca340bd95a1957cf02e518bcc4c5b/521f7/01-devsisters-cockroachdb-case-study.webp 860w,
/static/a79ca340bd95a1957cf02e518bcc4c5b/cbd37/01-devsisters-cockroachdb-case-study.webp 1200w,
/static/a79ca340bd95a1957cf02e518bcc4c5b/bd21a/01-devsisters-cockroachdb-case-study.webp 1260w&quot; sizes=&quot;(max-width: 1260px) 100vw, 1260px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/a79ca340bd95a1957cf02e518bcc4c5b/53918/01-devsisters-cockroachdb-case-study.png 360w,
/static/a79ca340bd95a1957cf02e518bcc4c5b/9aebd/01-devsisters-cockroachdb-case-study.png 480w,
/static/a79ca340bd95a1957cf02e518bcc4c5b/0f09e/01-devsisters-cockroachdb-case-study.png 640w,
/static/a79ca340bd95a1957cf02e518bcc4c5b/3e3fe/01-devsisters-cockroachdb-case-study.png 860w,
/static/a79ca340bd95a1957cf02e518bcc4c5b/64756/01-devsisters-cockroachdb-case-study.png 1200w,
/static/a79ca340bd95a1957cf02e518bcc4c5b/58c38/01-devsisters-cockroachdb-case-study.png 1260w&quot; sizes=&quot;(max-width: 1260px) 100vw, 1260px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/a79ca340bd95a1957cf02e518bcc4c5b/58c38/01-devsisters-cockroachdb-case-study.png&quot; alt=&quot;01 devsisters cockroachdb case study&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        참고: &lt;a href=&quot;https://www.cockroachlabs.com/customers/devsisters/&quot;&gt;데브시스터즈 CockroachDB Case Study&lt;/a&gt;
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;what-is-cockroachdb&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#what-is-cockroachdb&quot; aria-label=&quot;what is cockroachdb permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;What is CockroachDB?&lt;/h2&gt;
&lt;p&gt;CockroachDB란 &lt;strong&gt;SQL 인터페이스&lt;/strong&gt;를 지원하는 &lt;strong&gt;분산&lt;/strong&gt; &lt;strong&gt;데이터베이스&lt;/strong&gt;입니다. 수평 확장이 가능하고, 다양한 종류의 물리적 장애에서 살아남을 수 있으며, 전 지구적인 스케일로 데이터베이스를 구축하고 관리할 수 있는 다양한 기능을 지원합니다. GitHub에 오픈소스로 공개되어 있고, 무료로 사용할 수 있으나 몇몇 기능은 엔터프라이즈 라이센스를 필요로 합니다.&lt;/p&gt;
&lt;p&gt;“SQL 인터페이스를 지원하면서 수평 확장이 가능한 강력한 분산 데이터베이스” 라고 하면 Google Cloud의 Spanner를 떠올리시는 분들도 계실텐데요. 실제로 CockroachDB의 세 설립자는 모두 구글의 전 직원이었고, 구글에서 퇴사한 후 Spanner와 비슷한 시스템을 세상에 내놓고자 프로젝트를 시작했습니다. 그래서 사실 CockroachDB의 핵심적인 설계는 많은 부분에서 Spanner로부터 영감을 받았다고 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;그렇지만 Spanner와 다른 부분들도 많습니다. 대표적으로, Spanner의 경우 트랜잭션 사이의 실행 순서를 엄격히 보장하기 위해 Google 데이터센터의 전용 하드웨어에 달려있는 원자 시계 (Atomic Clock) 를 활용한다고 알려져 있습니다. 하지만…. 저희를 포함해 대부분의 개발팀은 구글이 아니고, 전용 하드웨어도 원자 시계도 없죠 😅. CockroachDB의 경우 원자 시계를 사용하지 않는 (정확히는, 시간의 동기화에 의존하긴 하지만 원자 시계만큼의 정확도까지는 요구하지 않는) 알고리즘을 구현하여 소프트웨어적인 방법으로 같은 로직을 처리하고 있습니다.&lt;/p&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 220px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 100%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAACXBIWXMAAAsTAAALEwEAmpwYAAACLUlEQVR42oWT3UsUURTAfQ/6W3qK3vIpEIK+HgwhsizpAzJC0aWCFot2QaRksdYsXbO2EHYLkixDISof0mjxKVmDXFmVHdud7/t5bnPn2pTLjB7uw7lnzu98zm0QYcK5PADyMBAcQr1EQ6BZuthYgfUSmMaWr4KVaDYUNV6sQNWBbbDymOinvS04cRnd6ECxXndiBiMCBoLUAj6atw9l7aZxZ/+gc3uGyNJge2bbAMcUyBGVDZj7SuMZ90TabMqanbPOhxItW2BTqCHQEYSU/a9h/2vmEzqZMQ9PGc9WUWCM7HlrPEzq+VncmrJcJjTEG79XczXJMzXCUDhofm2Nt163imWmjHMuaaz83gQuHaIyqw15kh5E7T0WV6WCsAEOONo9YsvkO5TtiWtB9wWnpcP6ue57glhAdJ9WOW3UGIi65P/BftofBZ7swm+/kPP3TYdABfFjheqkjtp/6cuIRcKq5s/TrO8W9pThd6jtiXnqjT5edL3rlXljfpPUjb0e/jjNknGsLBcfW7GcrfTYpPVtle4CLxb4tatIWQbyaPS9DESo6ExZK/4UAKIHVqvB2UuoVJaX/ud4+LWEl5Z57KZDCETCQfKeBE5nZXuJEfzgJfGXh4cGZBTOdtvz4hI/eM6lTCRHyVCOeo+sudktl+rThvzbzOfvjJEzSRwfIXef0rZuMvaCBqF3goPwXY/onuPu3iMomWGhZAT8l+97xR5ORZLhsOKDfUaRQog/Lk1YfbHAABMAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/7de59789651244baab4c3a730b5edd73/df843/02-crdb-logo.webp 220w&quot; sizes=&quot;(max-width: 220px) 100vw, 220px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/7de59789651244baab4c3a730b5edd73/8c324/02-crdb-logo.png 220w&quot; sizes=&quot;(max-width: 220px) 100vw, 220px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/7de59789651244baab4c3a730b5edd73/8c324/02-crdb-logo.png&quot; alt=&quot;02 crdb logo&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;p&gt;그들은 왜 데이터베이스에 Cockroach (바퀴벌레) 라는 징그러운 이름을 붙였을까요? 왜냐하면 CockroachDB는 시스템에 크고 작은 문제가 생겼을 때 생존하고 스스로 회복하는 것 (Resiliency) 에 가장 큰 강점이 있기 때문입니다. 분산 시스템을 설계할 때에 가장 어려운 문제 중 하나가 바로 장애 상황에서 일관성이나 가용성을 최대한 유지하면서 복구해내는 것인데요. 특히 분산 데이터베이스 시스템의 경우 복구에 많은 시간이 걸리거나 많은 데이터가 소실되면 어플리케이션에 큰 영향을 끼칠 수 있습니다.&lt;/p&gt;
&lt;p&gt;CockroachDB의 경우 디스크, 물리적 머신, 심지어 데이터 센터 단위의 장애가 생기더라도 생존할 수 있는 다양한 옵션을 제공하고 있습니다. 또한 문제 상황이 생기면 스스로 감지하고 사람의 개입 없이도 자동으로 서비스를 최대한 복구해냅니다. 실제로 CockroachDB를 운영하다보면 하드웨어 문제로 데이터베이스 인스턴스 중 하나가 꺼졌는데 서비스 지표에는 영향이 전혀 없고 조금 기다리면 인스턴스도 알아서 교체되어 있는 놀라운 경험을 하게 되기도 합니다.&lt;/p&gt;
&lt;p&gt;생존과 자가 회복 외에도 CockroachDB에는 여러 개의 핵심적인 특성 (Killer Features) 이 있는데요. 그 중 저희가 주목했던 몇 가지를 소개합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PostgreSQL Compatibility&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;PostgreSQL 프로토콜의 거의 대부분을 지원하기 때문에, 기존에 커뮤니티에 존재하던 다양한 DB 접속 라이브러리나 ORM 등을 거의 그대로 사용할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Serializable Transactions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SQL 표준에 정의되어 있는 트랜잭션 격리 수준 4단계 중 가장 강력한 SERIALIZABLE 격리를 모든 트랜잭션에서 사용하도록 되어 있습니다. 즉, 여러 개의 트랜잭션이 동시에 실행되어도 한 번에 하나씩 실행한 것과 동일한 결과를 보장합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;수평 확장성&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;데이터베이스 확장이 필요한 경우 노드를 추가로 확보하고 붙여주기만 하면 그대로 처리량과 용량을 늘릴 수 있습니다. 노드를 확장/축소하거나 교체할 때 별도의 서비스 중단이 필요하지 않고 무중단으로 작업할 수 있는 것 또한 강력한 강점입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Multi-Region 클러스터 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;전 세계를 대상으로 하는 어플리케이션의 경우 데이터베이스의 위치와 이용자의 위치 사이의 물리적 거리로 인해 응답 시간이 길어질 수 밖에 없는데요. 이것을 극복하기 위해 데이터베이스를 여러 개로 나누어 운영할 수도 있지만, 그러면 이제 그 데이터베이스들 사이의 싱크를 맞추는 일과 장애를 대비하는 일에 많은 공을 들여야 합니다.&lt;/p&gt;
&lt;p&gt;CockroachDB에는 클러스터를 굳이 쪼개고 샤딩하지 않고도 하나의 거대한 클러스터를 여러 개의 리전에 걸쳐 운영할 수 있는 기능이 내장되어 있습니다. 이 기능을 활용하면 데이터베이스/테이블/row 단위로 메인 리전을 결정하고 최적화할 수 있게 됩니다.&lt;/p&gt;
&lt;h2 id=&quot;why-we-chose-cockroachdb&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#why-we-chose-cockroachdb&quot; aria-label=&quot;why we chose cockroachdb permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Why we chose CockroachDB?&lt;/h2&gt;
&lt;p&gt;데브시스터즈에서는 &amp;#x3C;쿠키런: 킹덤&gt; 의 개발 초창기부터 CockroachDB를 염두에 두고 설계했습니다. 그 전까지 데브시스터즈에서는 주로 JSON 문서 기반의 데이터베이스를 사용했었고, 원하는 정도의 성능 기준은 충분히 충족할 수 있었습니다. 하지만 이러한 NoSQL의 경우 문제 발생 시 대응하기 위해서 상당한 노력이 필요했고, Transaction 구현을 위해 어플리케이션 개발 시 별도의 복잡한 로직을 구현해야 했습니다. 반면 CockroachDB는 RDBMS의 장점 (Transaction 지원) 과 NoSQL의 장점 (샤딩 없이 수평 확장 가능) 을 모두 가지고 있어 상당히 매력적인 포지션에 놓여 있었습니다.&lt;/p&gt;
&lt;p&gt;그렇지만 당시에는 CockroachDB가 세상에 나온지 얼마 되지 않은 시점이었기 때문에 대규모 제품에 사용한 케이스가 거의 존재하지 않았고, 아직 검증되지 않은 데이터베이스를 사용하는 것은 백엔드 엔지니어 입장에서 매우 도전적인 일이었습니다. 다행히도 CockroachDB가 PostgreSQL 인터페이스를 지원했기 때문에, 추후 성능 테스트를 진행해서 원하는 성능과 안정성이 나오지 않을 경우 최소한의 수정을 거쳐 PostgreSQL과 호환되는 다른 DB로 빠르게 전환하는 것을 플랜 B로 두고 설계를 진행할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;이후 제품 개발 과정을 거쳐 런칭 준비에 들어서면서, 본격적으로 데이터베이스에 대한 검증을 시작했습니다. 당시 검토했던 최종 후보는 1. Amazon DynamoDB, 2. Amazon Aurora (PostgreSQL 호환), 3. CockroachDB 총 세 가지였습니다. 성능, 확장 가능성, 비용 등 주요 요인에 대해 세 가지 데이터베이스를 벤치마크해보았습니다.&lt;/p&gt;
&lt;h3 id=&quot;성능&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%84%B1%EB%8A%A5&quot; aria-label=&quot;성능 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;성능&lt;/h3&gt;
&lt;p&gt;로드를 소화하기에는 충분한, 비슷한 규모의 인프라를 기준으로 킹덤 서버의 특정 기능을 반복 수행하여 테스트를 진행했습니다. 다른 어플리케이션이나 다른 워크로드 수행시 충분히 양상이 달라질 수 있으므로 참고만 해주세요!&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;latency 중위값 (p50): CockroachDB = Aurora &amp;#x3C;&amp;#x3C; DynamoDB&lt;/li&gt;
&lt;li&gt;tail latency (p99): CockroachDB &amp;#x3C;&amp;#x3C; Aurora = DynamoDB&lt;/li&gt;
&lt;li&gt;Error Count: CockroachDB와 Aurora는 에러가 검출되지 않았고, DynamoDB에서 0.01% 정도의 에러 카운트 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;중위 latency를 볼 때 세 엔진 모두 저희가 원하는 최소 성능은 만족시켰으나, 전반적인 분포로 볼 때에는 CockroachDB가 성능 면에서 가장 뛰어난 것으로 나타났습니다.&lt;/p&gt;
&lt;h3 id=&quot;확장-가능성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%99%95%EC%9E%A5-%EA%B0%80%EB%8A%A5%EC%84%B1&quot; aria-label=&quot;확장 가능성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;확장 가능성&lt;/h3&gt;
&lt;p&gt;Aurora의 경우 가장 큰 인스턴스가 최대 96개의 vCPU를 제공하므로 확장에 한계가 있었습니다. 현재 실제 운용중인 쿠키런 킹덤 데이터베이스가 수백~수천 개 단위의 vCPU 코어를 사용하고 있음을 감안하면 (당시엔 몰랐지만) 선택할 수 있는 옵션이 아니었죠.&lt;/p&gt;
&lt;p&gt;DynamoDB의 경우 내부적으로 자동으로 수평 확장이 발생하므로 이 부분에서는 이점을 가지고 있었습니다. 그러나 사용량에 따라 다이나믹하게 수평확장이 발생하므로 확장 시점을 컨트롤하기 어렵고, hot partition이 발생하지 않도록 신경써야 합니다.&lt;/p&gt;
&lt;p&gt;CockroachDB의 경우 수평적 확장이 가능하므로, 필요한 만큼 인스턴스를 띄워 확장할 수 있었습니다. 다만 DynamoDB와 마찬가지로 hot partition이 발생하지 않도록 신경써야 합니다.&lt;/p&gt;
&lt;h3 id=&quot;비용&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B9%84%EC%9A%A9&quot; aria-label=&quot;비용 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;비용&lt;/h3&gt;
&lt;p&gt;DynamoDB의 경우 OnDemand와 Provisioned 사이의 가격 차이가 크므로 1년 Provisioned 기준으로 대략적인 가격을 산정했습니다.&lt;/p&gt;
&lt;p&gt;Aurora의 경우에도 1년 Reserved Instance 기준으로 가격을 산정했습니다.&lt;/p&gt;
&lt;p&gt;CockroachDB의 경우 인스턴스 컴퓨팅 비용에 더해 라이센스 비용을 고려해야 했습니다. 물론 엔터프라이즈 라이센스 없이 오픈소스 버전으로 서비스하는것도 고려했지만, 상대적으로 새로운 엔진을 도입한다고 가정할 때 문제 상황 대처나 최적화 튜닝을 위해 CockroachLabs의 기술 지원이 꼭 필요하다는 판단을 내렸습니다. 실제로 서비스를 운영하면서 여러 가지 기술적으로 유용한 지원을 여러 번 받을 수 있었습니다.&lt;/p&gt;
&lt;p&gt;전반적인 가격의 구체적인 비교는 사정상 자세하게 적기에는 어렵지만, 내부에서 계산해 본 결과 대체로 비슷하거나 CockroachDB의 가격이 (라이센스 비용을 포함해서) 조금 더 경쟁력 있는 것으로 나타났습니다.&lt;/p&gt;
&lt;p&gt;종합적으로 비교해 본 결과 비용, 성능, 안정성, 확장성 등등 대부분의 지표에서 CockroachDB가 우위에 있었고, 최종적으로 킹덤에서 사용할 데이터베이스로 선정했습니다.&lt;/p&gt;
&lt;h2 id=&quot;cockroachdb-internals&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#cockroachdb-internals&quot; aria-label=&quot;cockroachdb internals permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;CockroachDB Internals&lt;/h2&gt;
&lt;p&gt;어떤 데이터베이스나 분산 시스템을 사용하건 그것의 동작 방식을 어느 정도 파악해두는 것은 매우 중요한 일입니다. 물론 기본 설정으로 배포해두어도 일단 적당히 사용할 수는 있겠지만, 최적화와 문제 상황 분석 및 트러블슈팅에 있어서 한계가 있을 수 밖에 없습니다.&lt;/p&gt;
&lt;p&gt;CockroachDB를 사용하기로 결정한 이후로 저희는 이 시스템의 동작 방식을 이해하는 데에 많은 시간과 노력을 쏟았는데요. 그 중 운용을 위해 반드시 알아야 하는 중요한 컨셉을 몇 가지 소개하고자 합니다.&lt;/p&gt;
&lt;h3 id=&quot;key-value-store&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#key-value-store&quot; aria-label=&quot;key value store permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Key-Value Store&lt;/h3&gt;
&lt;p&gt;CockroachDB는 PostgreSQL 인터페이스를 사용해 데이터를 주고받기 때문에 관계형 데이터베이스처럼 보이지만, 사실 내부적으로는 분산 Key-Value 저장소로 구현되어 있습니다. SQL 쿼리문이 입력되면 이를 이에 맞는 Key-Value 연산으로 변환하여 수행 가능한 노드로 보낸 뒤 다시 결과를 취합하여 보여 주는 것이죠.&lt;/p&gt;
&lt;p&gt;CockroachDB의 각 노드는 로컬 Key-Value 스토리지 엔진으로 &lt;a href=&quot;https://github.com/cockroachdb/pebble&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Pebble&lt;/a&gt; 을 사용합니다. 초창기에는 RocksDB 를 엔진으로 사용 했지만, CockroachDB에 더 적합한 엔진을 사용하기 위해 RocksDB를 기반으로 직접 개발한 것이 Pebble 입니다(CockroachDB v20.2 버전부터 Pebble을 디폴트 엔진으로 사용하고 있습니다).&lt;/p&gt;
&lt;p&gt;RocksDB나 Pebble 은 내부적으로 LSM tree (Log-Structured Merge tree) 구조를 사용하며, 디스크에는 SST (Sorted String Table) 포맷으로 저장합니다. 실제로 각 노드의 데이터 디렉토리에 들어가면 &lt;code&gt;.sst&lt;/code&gt; 파일들이 존재하는 것을 확인할 수 있습니다. &lt;code&gt;cockroach&lt;/code&gt; 바이너리에는 각종 디버그 툴이 포함되어 있는데, 그 중에서 sst 파일의 내용을 key-value 단위로 확인할 수 있는 커맨드들도 있습니다. (e.g. &lt;code&gt;cockroach debug sst_dump&lt;/code&gt;)&lt;/p&gt;
&lt;h3 id=&quot;range&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#range&quot; aria-label=&quot;range permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Range&lt;/h3&gt;
&lt;p&gt;큰 데이터베이스를 모두 Key-Value 형태로 저장하기 위해서는 적당한 크기로 나누어 저장해야 합니다. 기본적으로는 최대 512MiB 단위로 쪼개어 저장하는데, 이 단위를 &lt;code&gt;Range&lt;/code&gt; 라고 부릅니다.&lt;/p&gt;
&lt;p&gt;다시 말해서 CockroachDB의 각각의 노드는 여러 개의 Range를 저장하는 저장소이고, 하나의 Range에는 여러 개의 연속한 Key와 Value가 들어있으며, 여러 노드에 복제되어 있어 각각 복제본 (Replica)의 역할을 합니다.&lt;/p&gt;
&lt;p&gt;아래 쿼리를 통해 어떤 키가 어느 range에 속하는지, 해당 range를 복제하고 있는 노드 목록은 무엇인지 등을 확인할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; crdb_internal&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ranges_no_leases &lt;span class=&quot;token keyword&quot;&gt;limit&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;replication-leaseholder-raft-leader&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#replication-leaseholder-raft-leader&quot; aria-label=&quot;replication leaseholder raft leader permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Replication: Leaseholder, Raft Leader&lt;/h3&gt;
&lt;p&gt;CockroachDB는 각 range를 Replication factor 에 맞춰 여러 노드에 복제합니다. 이때 여러 노드로 분산된 환경에서 Read 와 Write 를 올바르게 처리하기 위해, 각 range 별로 Leaseholder, Raft Leader 를 하나씩 선출합니다.&lt;/p&gt;
&lt;p&gt;Leaseholder는 해당 range에 대한 모든 읽기 요청을 처리하고 raft leader에게 쓰기 연산을 제안하는 (즉 해당 range에 대한 읽기/쓰기 연산을 최전선에서 담당하는) 노드입니다. Raft Leader는 해당 range에 대한 쓰기 연산이 항상 합의된 상태로 유지되도록 관리하는 노드입니다. 구조적으로 Leaseholder 와 Raft Leader 가 동일한 노드인 경우 쓰기 연산의 rtt가 줄어들기 때문에, CockroachDB는 가급적 이 둘을 같은 노드로 배정하려고 하지만 항상 그렇게 배정되지는 않을 수도 있습니다. 읽기, 쓰기 연산에서 각각의 역할에 대해서는 아래의 다이어그램을 통해 더 자세히 설명합니다.&lt;/p&gt;
&lt;p&gt;이름에서 알 수 있듯 range 내에서의 합의에는 &lt;a href=&quot;https://raft.github.io/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Raft consensus algorithm&lt;/a&gt; 을 사용합니다. 이 알고리즘은 과반 이상이 투표해야 합의가 이루어지는 알고리즘이므로, 특정 range 의 레플리카 중 과반 이상이 작동하지 않는 경우 해당 range 를 포함하는 모든 쿼리는 거부됩니다. 즉 CockroachDB는 &lt;code&gt;floor(replication factor / 2)&lt;/code&gt; 만큼의 노드 장애 까지는 서비스 품질 저하 없이 수용하지만, 이후부터는 확률에 따라 (특정 range의 replication이 장애난 노드에 몰려 있을 확률) 일부 쿼리 (장애 range 를 포함하는 쿼리) 가 작동하지 않게 됩니다.&lt;/p&gt;
&lt;h3 id=&quot;read-scenario&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#read-scenario&quot; aria-label=&quot;read scenario permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Read Scenario&lt;/h3&gt;
&lt;p&gt;range에 대한 읽기 연산은 아래와 같이 행해집니다. r1, r2, r3 각각은 하나의 range를 의미합니다.&lt;/p&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 669px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 50.55555555555556%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAABr0lEQVR42oVSWU/bQBD2Xy9qJRCq+liJRwSoEn2o1JYrYKeQ0qptFAghgRByEYckThzi9bVZ219nNyZFtIVZj3f2m53ZuTQkCf7JjyghTHAOHoZgLlMcBIHCkjie22jyFxPAiS3Xg+0HSpaYvCJEhJBkc2SjMxrD5VPEUaSMpW7o+oTbcAjndNZEvw+v3kB4XcMgsw3byIA3rjFNX+QRPVa9QHfnE5wjA7xrIp7FrBx6hTx6ZDchXTgaQgvKZXjfjlH6nMPB8jrxGopbX5VJaxigNmDIv9ehv95AZmkVF7kTzGKXEQocrnxA9s077C2u4rbahpakyt3LCl7o23hp7OJj+UxhJdPFr2ofm8WCwhf0HRxT9GwyxpVVQtuu4+33HF5l95Tust+jGiaUAH1tq4rTG4M4i+agIjOiOkdgjKFpnSPf2EehpaM3vsGUC7TsGm7vOqiYP3Da+YKTtoGRY82act/FtDRzWUQCDnOUzBxXpfi488mDiZDyH4fpwnyfXZARSgoCH1PB/3L2cM3H5iniNGd3kwmKHRNd2p+jZx1K8nwfBz8LOK830+j+7/A38bj00bUiDkcAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/aa458eedf71b21886fb5f48f4ead5f35/33ecc/03-crdb-read-scenario.webp 360w,
/static/aa458eedf71b21886fb5f48f4ead5f35/263a4/03-crdb-read-scenario.webp 480w,
/static/aa458eedf71b21886fb5f48f4ead5f35/0c8fb/03-crdb-read-scenario.webp 640w,
/static/aa458eedf71b21886fb5f48f4ead5f35/6c81e/03-crdb-read-scenario.webp 669w&quot; sizes=&quot;(max-width: 669px) 100vw, 669px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/aa458eedf71b21886fb5f48f4ead5f35/53918/03-crdb-read-scenario.png 360w,
/static/aa458eedf71b21886fb5f48f4ead5f35/9aebd/03-crdb-read-scenario.png 480w,
/static/aa458eedf71b21886fb5f48f4ead5f35/0f09e/03-crdb-read-scenario.png 640w,
/static/aa458eedf71b21886fb5f48f4ead5f35/66b6b/03-crdb-read-scenario.png 669w&quot; sizes=&quot;(max-width: 669px) 100vw, 669px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/aa458eedf71b21886fb5f48f4ead5f35/66b6b/03-crdb-read-scenario.png&quot; alt=&quot;03 crdb read scenario&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;ol&gt;
&lt;li&gt;r3 내의 값을 읽으려는 요청이 클러스터에 도달합니다. 이때 CockroachDB 에서 사용자의 요청은 어떤 노드에 도달하더라도 상관 없습니다. 처음으로 요청을 받은 노드(이를 게이트웨이 노드라 합니다)는 해당 요청을 처리할 수 있는 적합한 노드에게 해당 요청을 라우팅해줍니다.&lt;/li&gt;
&lt;li&gt;r3의 leaseholder는 Node 3 이므로, 게이트웨이 노드인 Node 2 는 이 요청을 Node 3 으로 전송합니다.&lt;/li&gt;
&lt;li&gt;Node 3이 해당하는 데이터를 게이트웨이 노드에 전달합니다.&lt;/li&gt;
&lt;li&gt;사용자에게 응답이 내려갑니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;aside&gt;
💡 간혹 읽기 연산이 반드시 최신 데이터를 가져올 필요는 없는 경우가 있습니다. 이럴 때는 leaseholder 가 받는 부하를 줄이기 위해 &lt;a href=&quot;https://www.cockroachlabs.com/docs/stable/follower-reads.html&quot;&gt;follower read&lt;/a&gt;를 사용할 수도 있습니다. 아래 MVCC 항목에서 자세히 설명합니다.
&lt;/aside&gt;
&lt;h3 id=&quot;write-scenario&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#write-scenario&quot; aria-label=&quot;write scenario permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Write Scenario&lt;/h3&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 704px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 57.50000000000001%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAAAsTAAALEwEAmpwYAAACTUlEQVR42qWSW08TURSF+1t9wED0Bxjf1GiE+KImYEx80KBGuUjUlMpFwSI2SqEXadqEltJaOqXtzJTO/fK5O1OMvuqZ7Mw5a5+z9m0l+GP5vo/nuvieh3dhvofrOji2jW1Z0Z3o3tgfBEH8OAyjX8JxbIzzIZo+QDctdMtGk7NpmthComkaLaWD0ldpi53KXtd1LCE3DBNtaGAIsS2ErgRKOJ6LF4RYg3OahSLNfAFH03E9n77aZyjERrdPI5ejUy5jSmDLceKAUs15q01l9ytqrYYlARJqNouaTNJbXKA9dZn25AT1mWlOOmc4xkCiO9Qf3EcRX2viEurqalRaIGR9ybR1/RqnVyZRrk4xKBZJ+MIaaCrl/BHLT9dZebbBdiqDZVvUFZVaR2NnK8ebF59Ymt+kVKyNWxZIf31Sy2lWBH/7fJOfDYVE3MqQ3eqQm2s9bq13md9TI+zgZEj2WGdhX+X2Ro8ba13SR4Pxi9EQPR5+7omvG/krbUMIw3hK+cZ3ZtN3md2ZJvljKS6L2LdRescjwefSdzioZyKspOxTO6vwKvuEuZ0ZHn+5x3GnOsowzvGoUyZVWiB1+JpMdZvQj8sarVzjGx8rK6yXl6l2KhHW1Gq0+g22ykne51/yobRIW22RuNBgONZR4IJjeRimEU1yZKOza0vPbLDNeMKe4wsm9wY2p00l0mSkw9+E8gVRRuHYiEQbC9nDcW2RkhPt48dhhBmmaNYSCYnvL8J/WYEEa3d77NVPaKra/xFetEgVoWcKhxyLwEeEvwAM331SMaiW6QAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/ede88fe6418204991ff2837aaad67dff/33ecc/04-crdb-write-scenario.webp 360w,
/static/ede88fe6418204991ff2837aaad67dff/263a4/04-crdb-write-scenario.webp 480w,
/static/ede88fe6418204991ff2837aaad67dff/0c8fb/04-crdb-write-scenario.webp 640w,
/static/ede88fe6418204991ff2837aaad67dff/79f11/04-crdb-write-scenario.webp 704w&quot; sizes=&quot;(max-width: 704px) 100vw, 704px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/ede88fe6418204991ff2837aaad67dff/53918/04-crdb-write-scenario.png 360w,
/static/ede88fe6418204991ff2837aaad67dff/9aebd/04-crdb-write-scenario.png 480w,
/static/ede88fe6418204991ff2837aaad67dff/0f09e/04-crdb-write-scenario.png 640w,
/static/ede88fe6418204991ff2837aaad67dff/3be76/04-crdb-write-scenario.png 704w&quot; sizes=&quot;(max-width: 704px) 100vw, 704px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/ede88fe6418204991ff2837aaad67dff/3be76/04-crdb-write-scenario.png&quot; alt=&quot;04 crdb write scenario&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;ol&gt;
&lt;li&gt;r1 내의 값을 업데이트하려는 요청이 클러스터에 도달합니다.&lt;/li&gt;
&lt;li&gt;해당 요청은 r1의 leaseholder 인 Node 1 으로 전달됩니다.&lt;/li&gt;
&lt;li&gt;r1 의 raft leader 또한 Node 1 이기에, Node 1이 즉시 쓰기 연산에 대한 합의를 다른 노드들에게 전파합니다.&lt;/li&gt;
&lt;li&gt;Node 2 에게 ack 을 받습니다.&lt;/li&gt;
&lt;li&gt;과반 이상이 확인하였으므로 (Node 1, Node 2) 즉시 게이트웨이 노드에 결과를 전달합니다.&lt;/li&gt;
&lt;li&gt;사용자에게 응답이 내려갑니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;mvcc&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#mvcc&quot; aria-label=&quot;mvcc permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;MVCC&lt;/h3&gt;
&lt;p&gt;CockroachDB는 데이터를 저장할 때 &lt;a href=&quot;https://en.wikipedia.org/wiki/Multiversion_concurrency_control&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;MVCC&lt;/a&gt; (Multi-Version Concurrency Control) 를 사용합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;모든 Key-Value 값은 한번 쓰이면 더이상 변경되지 않는 immutable 한 값입니다.&lt;/li&gt;
&lt;li&gt;특정 Key에 대한 업데이트 연산은 동일 Key 에 대해 (더 이후의 타임스탬프를 갖는) 새로운 값을 추가하는 연산입니다.&lt;/li&gt;
&lt;li&gt;특정 Key에 대한 삭제 연산은 해당 Key가 지워졌음을 표시하는 (이 마커를 tombstone 이라 합니다) 값을 추가하는 연산입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이러한 특성을 이용해 읽기 쿼리 뒤에 &lt;code&gt;AS OF SYSTEM TIME ...&lt;/code&gt; 을 붙여 과거 시간을 기준으로 읽기를 수행할 수 있습니다 (time travel query). 현재 진행되고 있는 트랜잭션과도 상관이 없기에, 이러한 time travel query 는 leaseholder 가 아닌 다른 레플리카에서 읽어도 상관이 없으며, 따라서 적절히 활용할 경우 leaseholder가 받는 읽기 부하를 완화할 수 있습니다.&lt;/p&gt;
&lt;p&gt;즉, 어떤 키에 대한 데이터를 UPDATE 하면 예전 값이 사라지는 것이 아니라 MVCC에 의해 일정 시간동안 보존되게 됩니다. 따라서 INSERT 연산 보다 UPDATE 가 더 많은 경우 예상했던 것 보다 스토리지 사용량이 많을 수 있음을 유의해야 합니다. 같은 키를 가지는 오래 된 데이터는 시스템에 설정된 &lt;code&gt;gc.ttlseconds&lt;/code&gt; 보다 오래 된 경우 sst compaction 과정에서 제거되는데, 이 설정의 기본값은 25시간입니다.&lt;/p&gt;
&lt;aside&gt;
💡 특정 테이블에서 UPDATE 연산이 많지만 자주 업데이트 하는 대상이 일부 칼럼으로 제한되어 있는 경우 해당 컬럼 그룹을 &lt;a href=&quot;https://www.cockroachlabs.com/docs/stable/column-families.html&quot;&gt;Column family&lt;/a&gt; 로 묶어 업데이트가 필요한 부분만 새로 추가하도록 할 수 있습니다.
&lt;/aside&gt;
&lt;h2 id=&quot;production-배포시-고려해야-할-사항&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#production-%EB%B0%B0%ED%8F%AC%EC%8B%9C-%EA%B3%A0%EB%A0%A4%ED%95%B4%EC%95%BC-%ED%95%A0-%EC%82%AC%ED%95%AD&quot; aria-label=&quot;production 배포시 고려해야 할 사항 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Production 배포시 고려해야 할 사항&lt;/h2&gt;
&lt;h3 id=&quot;range-split&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#range-split&quot; aria-label=&quot;range split permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Range Split&lt;/h3&gt;
&lt;p&gt;특정 range에 너무 많은 read나 write가 몰리면 병목이 발생하고 쿼리의 응답 속도가 느려질 수 있습니다. 이런 현상이 발생한 range를 hot range 혹은 hotspot 이라고 부릅니다. Hot range가 발생하거나 특정 range의 크기가 너무 커지면 CockroachDB가 자동으로 range를 쪼개어 부하를 분산하게 됩니다 (range split). 반대로 인접한 여러 개의 range가 너무 작거나 쿼리가 별로 발생하지 않는다면 range를 합치게 됩니다 (range merge).&lt;/p&gt;
&lt;p&gt;Range를 합치거나 나누는 과정은 보통 CockroachDB에 의해 자동으로 이루어집니다. 그러나 서비스 런칭 혹은 특정 피쳐 배포 초창기에 수많은 INSERT 문이 발생하게 되면, 자동 split 이 INSERT 속도를 따라가지 못해 hot range가 발생할 수 있습니다. 특히 게임 서비스의 경우 런칭 직후나 패치 직후에 트래픽 피크가 발생하므로 서비스에 매우 큰 위험 요인이 될 수 있습니다.&lt;/p&gt;
&lt;p&gt;이와 같이 테이블이 새로 생겼거나 새로운 패턴의 데이터가 폭발적으로 인입될 것이 예상되는 경우 range 범위를 미리 어느 정도 쪼개두는 것이 좋습니다. range를 수동으로 쪼개려면 다음과 같은 쿼리를 이용하면 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;sql&quot;&gt;&lt;pre class=&quot;language-sql&quot;&gt;&lt;code class=&quot;language-sql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; members SPLIT AT &lt;span class=&quot;token keyword&quot;&gt;VALUES&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;AAAAA0000&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; members SPLIT AT &lt;span class=&quot;token keyword&quot;&gt;VALUES&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;ABAAA0000&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; members SPLIT AT &lt;span class=&quot;token keyword&quot;&gt;VALUES&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;ZYAAA0000&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; members SPLIT AT &lt;span class=&quot;token keyword&quot;&gt;VALUES&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;ZZAAA0000&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;ALTER&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;TABLE&lt;/span&gt; members SCATTER&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;hotspot-피하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#hotspot-%ED%94%BC%ED%95%98%EA%B8%B0&quot; aria-label=&quot;hotspot 피하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Hotspot 피하기&lt;/h3&gt;
&lt;p&gt;CockroachDB를 사용하기로 했다면 테이블을 설계할 때부터 데이터가 잘 분산되고 핫스팟이 생기지 않도록 고려할 필요가 있습니다. 가장 대표적으로 차이가 발생하는 부분이 Primary Key와 Index 입니다.&lt;/p&gt;
&lt;p&gt;RDBMS에서는 auto_increment를 이용한 ID column이나 계속 증가하는 타임스탬프 등을 키로 쓰는 것이 보통 일반적입니다. 그러나 CockroachDB에서는 이렇게 계속 증가하는 column을 key로 활용하는 경우 항상 테이블의 마지막 range에서만 쓰기가 일어나고, 이것이 항상 핫스팟이 될 수 밖에 없습니다.&lt;/p&gt;
&lt;p&gt;CockroachDB에서는 Table의 Primary Key로 UUID를 사용하거나 랜덤성이 있는 컬럼을 활용하는 등 최대한 잘 분산된 키를 사용할 것을 권장합니다. 여러 컬럼을 함께 Primary Key로 묶어서 사용하는 경우 키의 분산에 더 도움을 줄 수도 있습니다.&lt;/p&gt;
&lt;p&gt;만약 꼭 증가하는 int column을 키로 써야만 한다면 &lt;a href=&quot;https://www.cockroachlabs.com/docs/v20.2/hash-sharded-indexes.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;hash-sharded index&lt;/a&gt;를 사용할 수 있습니다. 단 hash-sharded index 를 사용하는 경우 range query (&gt;, &amp;#x3C;) 를 사용할 때 풀스캔이 발생함을 염두에 두어야 합니다.&lt;/p&gt;
&lt;h3 id=&quot;locality-설정하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#locality-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0&quot; aria-label=&quot;locality 설정하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Locality 설정하기&lt;/h3&gt;
&lt;p&gt;Multi-region 혹은 Multi-az 등 여러 개의 데이터센터에 걸쳐 CockroachDB를 배포하는 경우, &lt;code&gt;--locality&lt;/code&gt; 플래그를 설정하여 각 range를 분산할 때 지리적 특성을 고려하도록 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;예를 들어 replication factor가 3 이고 세 개의 AZ (혹은 데이터 센터) 에 걸쳐 DB를 배포한 경우를 가정해보겠습니다. 만약 locality 플래그를 따로 설정하지 않은 상태에서 데이터 센터 장애가 발생한 경우, 특정 range의 replica가 모두 한 데이터 센터에 존재해서 결국 데이터가 소실될 가능성이 충분히 존재합니다.&lt;/p&gt;
&lt;p&gt;반면 locality 플래그를 설정하게 되면 각 range 별로 3개의 레플리카가 전부 다른 az에 분배되도록 분산해줍니다. 이렇게 설정하면 특정 데이터 센터에 문제가 생겨도 나머지 두개의 데이터 센터에 2개의 replica가 남아 있는 것이 보장되므로 데이터 손실도 없고 서비스도 문제 없이 제공할 수 있게 됩니다.&lt;/p&gt;
&lt;p&gt;쿠키런: 킹덤에서 CockroachDB 를 처음에 운영할 때에는 locality 플래그를 설정하지 않았는데, 실제로 이 부분으로 인해 장애를 겪은 적도 있었습니다. 이 부분에 대한 자세한 이야기는 &lt;a href=&quot;http://ndcreplay.nexon.com/NDC2022/sessions/NDC2022_0040.html#c=NDC2022&amp;#x26;t%5B%5D=%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;NDC22 - 쿠키런: 킹덤, 총 56시간의 긴급 점검 회고 - 그때 그 명검은 왜 뽑아야 했는가&lt;/a&gt; 라는 세션에서 다룬 바 있으니 관심 있으신 독자분께서는 꼭 확인해보세요!&lt;/p&gt;
&lt;h2 id=&quot;conclusion&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#conclusion&quot; aria-label=&quot;conclusion permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;CockroachDB는 수평 확장이 가능하고 다양한 장애 시나리오에서도 생존할 수 있는 강력한 분산 데이터베이스 소프트웨어입니다. 데브시스터즈에서는 쿠키런: 킹덤 에서의 도입을 시작으로, 다양한 내/외부 프로젝트에서 CockroachDB를 메인 데이터베이스로서 검토하고 도입하고 있습니다.&lt;/p&gt;
&lt;p&gt;만약 CockroachDB를 검토하고 계시거나 새 프로젝트에 사용할 멋진 데이터베이스를 찾고 있는 분이 계시다면, 이 글을 읽으신 후 꼭 CockroachDB를 한 번 활용해보시길 다시 한 번 권해드립니다.&lt;/p&gt;
&lt;p&gt;감사합니다.&lt;/p&gt;
&lt;h2 id=&quot;읽을-거리--볼-거리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9D%BD%EC%9D%84-%EA%B1%B0%EB%A6%AC--%EB%B3%BC-%EA%B1%B0%EB%A6%AC&quot; aria-label=&quot;읽을 거리  볼 거리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;읽을 거리 / 볼 거리&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://research.google/pubs/pub39966/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Spanner: Google’s Globally-Distributed Database&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cockroachlabs.com/docs/v20.2/topology-patterns.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://www.cockroachlabs.com/docs/v20.2/topology-patterns.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=iAWU-eGIYsk&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Devsisters: Uptime as a core requirement for online gaming application&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=AZbCZ2KOcwU&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;쿠키런: 킹덤, 총 56시간의 긴급 점검 회고 - 그때 그 명검은 왜 뽑아야 했는가&lt;/a&gt;&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[쿠키런: 킹덤 런칭 회고]]></title><description><![CDATA[쿠키런: 킹덤 런칭 과정에서 데브옵스 팀에서 수행한 업무를 회고합니다.]]></description><link>https://tech.devsisters.com/posts/cookierun-kingdom-launch-revisited</link><guid isPermaLink="false">https://tech.devsisters.com/posts/cookierun-kingdom-launch-revisited</guid><pubDate>Wed, 23 Nov 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;이 글과 관련된 다른 글 보기&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;/posts/cockroachdb-in-production/&quot;&gt;CockroachDB in Production&lt;/a&gt; - CockroachDB에 대한 소개와 킹덤에서 CockroachDB를 사용한 이유 설명&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/crk-launch-storage-postmortem/&quot;&gt;쿠키런: 킹덤 데이터베이스 스토리지 레이어 복원기&lt;/a&gt; - 엔지니어 관점 기록한 개괄적인 장애 대응 과정과 회고&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/bit-level-database-hacking&quot;&gt;CTO가 커리어를 걸고 비트 레벨까지 내려가서 DB를 해킹했던 이야기&lt;/a&gt; - CockroachDB의 소스코드를 참고하며 바이너리 파일로부터 데이터를 복구한 이야기&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/36h-downtime-on-first-day/&quot;&gt;입사 첫날에 36시간 점검 경험하기&lt;/a&gt; - 신규입사자의 관점에서 서술된 장애 대응 기록&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/crk-aws-az-failure-postmortem/&quot;&gt;쿠키런: 킹덤 AWS AZ 장애 아웃라인&lt;/a&gt; - AWS AZ 장애로부터 서비스를 복구한 이야기&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/crk-hot-range-postmortem/&quot;&gt;쿠키런: 킹덤 길드 업데이트 이후 서비스 이슈 되돌아보기&lt;/a&gt; - 쿠키런: 킹덤에 길드 기능이 추가된 이후 발생한 서비스 이슈 회고&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;안녕하세요, 저는 데브시스터즈 서비스기술그룹 인프라셀에서 데브옵스 엔지니어로 일하고 있는 이창원입니다. 저희 팀은 데브시스터즈에서 제공하고 있는 모든 게임의 Ops와 기술적 결정에 깊게 관여하고 있습니다. 작년 저희 팀의 가장 큰 미션은 쿠키런: 킹덤을 런칭하고 안정적으로 운영하는 체계를 구축하는 것이었는데요. 당시 어떤 과정을 거쳐 쿠키런: 킹덤이 여러분께 전달되었지를 공유드리고, 이어지는 글들에서 엔지니어로서 특별히 기억에 남는 주요 사건들에 대해 전해드리고자 합니다.&lt;/p&gt;
&lt;p&gt;이 글의 내용은 박새미님과 NDC22에서 공동 발표한 &lt;a href=&quot;http://ndcreplay.nexon.com/NDC2022/sessions/NDC2022_0040.html#c=NDC2022&amp;#x26;t%5B%5D=%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;쿠키런: 킹덤, 총 56시간의 긴급 점검 회고 - 그때 그 명검은 왜 뽑아야 했는가&lt;/a&gt; 세션에서 다뤄진 바 있습니다. 다만, 아무래도 발표는 시간 제한을 고려해야 하는 사정이 있다보니 자세한 정보를 담지 못했다는 아쉬움이 남아 글의 형식으로 다시 한 번 풀게 되었습니다. 보다 생동감 있는 짧은(?) 버전으로 일련의 이야기를 접하고자 하는 분들은 발표 영상을 보시는 것을 권해드립니다.&lt;/p&gt;
&lt;h2 id=&quot;step-1-기술환경질의서-게임-스튜디오와의-첫-소통&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#step-1-%EA%B8%B0%EC%88%A0%ED%99%98%EA%B2%BD%EC%A7%88%EC%9D%98%EC%84%9C-%EA%B2%8C%EC%9E%84-%EC%8A%A4%ED%8A%9C%EB%94%94%EC%98%A4%EC%99%80%EC%9D%98-%EC%B2%AB-%EC%86%8C%ED%86%B5&quot; aria-label=&quot;step 1 기술환경질의서 게임 스튜디오와의 첫 소통 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Step 1. 기술환경질의서: 게임 스튜디오와의 첫 소통&lt;/h2&gt;
&lt;p&gt;데브시스터즈 퍼블리싱을 결정하면 가장 먼저 “기술환경질의서”를 바탕으로 게임 스튜디오에서 선택한 기술스택과 아키텍쳐를 리뷰하게 됩니다. 이 때가 개발 스튜디오의 기술적 실무진들과 게임 Ops를 주로 담당하는 저희 팀이 공식적으로 대면하는 첫 자리입니다. 게임의 기술 스택에 대해서도 이야기를 나누고, 각각의 컴포넌트와 아키텍쳐를 살펴보며 운영 관점에서 취약한 구조 또는 최적화할 여지는 없는지 Scalability와 Resiliency 관점에서 면밀히 살펴보고 개선점이 있다면 함께 보완합니다.&lt;/p&gt;
&lt;figure style=&quot;width: 80%; margin: 0 auto;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1600px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 62.5%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAIAAAAmMtkJAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB1UlEQVR42m1S227bMAzN1+95QP9gv9CXPexl64quA5Y6zlIndes4vss3ybKtm2W7jJ20RRGCIESBhzo81CLPc0JIkiRpmkKM49h1XThnWeY4ThiGnudhjMfJhrPN6aIsy6ZpCKkoraFRURQIoaqClMIBYHAJBeMlW3xM5GScc0BCbNsWYHOsJ4NGM0Gl1BH8c9P+MKpfjzwsFC7z6GxQB0W+78P7fhDAOJwxwANBGLPruiP46jr78m319br8a4txUIxBDRNCTCQUONQBizgOMM6qqmDsfYSFeehWrlw6KsbDJMnHKThrKbzHBd9axm67dF7+IxS+g6VkSgopGKWY0kopOZ57hJFnrm6N5Q0mBRey6yYWAo7yBN672yB4dj33yba2uzXQA3UYa3U/Ylx4B9tz7bqpoZuQOkGh51oJCk7gZ/vBfTFJRRjn1uO/lfEHJXuEAqk06F3XGJxzpvueSx1Fh511H0X7E/goA8lBobdVKWCnNaRR7Fub+836LstTJlTdUBjqKAFnF/Y8/yHA9/3Q6YEQnGdBinzBGXwW07izn9Z7Z1vk8azrZ7DW2rIs+GBzo7cYx97vm++mcQvue7vL4Lm67/tPl7D+sszaloIz3swLfQU1j97Ec0QdvAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/e24270445aaa43b5b779034359ad736f/33ecc/01-technical-environment-questionnaire.webp 360w,
/static/e24270445aaa43b5b779034359ad736f/263a4/01-technical-environment-questionnaire.webp 480w,
/static/e24270445aaa43b5b779034359ad736f/0c8fb/01-technical-environment-questionnaire.webp 640w,
/static/e24270445aaa43b5b779034359ad736f/521f7/01-technical-environment-questionnaire.webp 860w,
/static/e24270445aaa43b5b779034359ad736f/cbd37/01-technical-environment-questionnaire.webp 1200w,
/static/e24270445aaa43b5b779034359ad736f/64296/01-technical-environment-questionnaire.webp 1600w&quot; sizes=&quot;(max-width: 1600px) 100vw, 1600px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/e24270445aaa43b5b779034359ad736f/53918/01-technical-environment-questionnaire.png 360w,
/static/e24270445aaa43b5b779034359ad736f/9aebd/01-technical-environment-questionnaire.png 480w,
/static/e24270445aaa43b5b779034359ad736f/0f09e/01-technical-environment-questionnaire.png 640w,
/static/e24270445aaa43b5b779034359ad736f/3e3fe/01-technical-environment-questionnaire.png 860w,
/static/e24270445aaa43b5b779034359ad736f/64756/01-technical-environment-questionnaire.png 1200w,
/static/e24270445aaa43b5b779034359ad736f/42cbc/01-technical-environment-questionnaire.png 1600w&quot; sizes=&quot;(max-width: 1600px) 100vw, 1600px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/e24270445aaa43b5b779034359ad736f/42cbc/01-technical-environment-questionnaire.png&quot; alt=&quot;01 technical environment questionnaire&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        기술환경질의서의 일부 항목과 초창기 쿠키런: 킹덤 아키텍쳐를 바탕으로 서술된 답변.&lt;br&gt;
        데브시스터즈에서는 효율적인 기술 자문을 위해 게임 서비스를 제작할 때 반드시 고려해야 하는 문제들을 기술환경질의서로 정리해두었습니다.
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;쿠키런: 킹덤 프로젝트의 기술질의서를 바탕으로 데브옵스 조직인 저희 인프라셀에서 파악한 리스크 포인트는 크게 두 가지였습니다. 첫번째는 팀에서 처음 운영해보는 Stateful 서버라는 것입니다. 이 당시의 인프라셀은 이미 쿠키런 For Kakao, 쿠키런: 오븐브레이크, 쿠키워즈, 쿠키런: 퍼즐 월드, 그리고 파티파티 데코플레이까지 총 5개의 모바일 게임에 대한 런칭 경험을 보유한 팀이었습니다. 그러나 이 게임들은 모두 Stateless 서버구조를 차용하였기 때문에 여기서 쌓은 노하우를 쿠키런: 킹덤 런칭에 그대로 적용하기에는 무리가 있었습니다. 저희 팀에서는 이 질의서를 바탕으로 스튜디오 킹덤의 서버 개발자들과 지속적으로 교류하며 Stateful 서버 구조, Akka 클러스터에 대한 이해도를 높였습니다. 또한 클라우드 환경에서 Stateful 서버를 운영하기 위해 필요한 제반 기술들을 연구하여 준비해두었습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1600px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 33.888888888888886%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAIAAACHqfpvAAAACXBIWXMAAAsTAAALEwEAmpwYAAABqElEQVR42g3LYXOSYAAA4JekmYp4jJEhCTqB1gs41EFogtNNBNZkZzekZXaJ1Wzd7LhbV7t127pu665v3XX93Xq+P4ClKrWq2Q+vhtFtvfVcp6dd8v29FeMOWkMQA0H2sNSMZmNOP+W8U2V81hF0I1MQsyyVLYKq1Dns2X8+Dl8df36s+T69tHIRklARhEdRiIAnFBZCPq42z5XRRWf5ex7GFsWXUvRahgGO0fi2r9y8NA/CGVv2pvlzDrFzRMM0R97ea9deNNYnW/yH7fb1/ugyGExDeXeHUbk0vZopAAeW44PefDzqd/3DVrxUbsSiWyprkvSUFyxtM7C1aLT9dRr+vZj/irwXR33REStibp1I06AL4RgWfdmE6vC4cjkgFxSpCIIiy63/uSoFVn2uPYp67Z9Hne8nO28/ud0AymoeEukHoKk23bZTKxkOE4V3v/DYIInmVpJkFhcwTMMwO5NyiZS3UTkLzNsFnMxqu76siAS3hjGAWWXzBb3FTN4kf1j4OxoXUBQHgAFABsAEwE4k+iRhq5snrr58VnJcaWtc3+DvPyRx5h/LqVos1M+TawAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/7f3feccacc774fa023a6038f47c6195d/33ecc/02-devsisters-cockroachdb-case-study.webp 360w,
/static/7f3feccacc774fa023a6038f47c6195d/263a4/02-devsisters-cockroachdb-case-study.webp 480w,
/static/7f3feccacc774fa023a6038f47c6195d/0c8fb/02-devsisters-cockroachdb-case-study.webp 640w,
/static/7f3feccacc774fa023a6038f47c6195d/521f7/02-devsisters-cockroachdb-case-study.webp 860w,
/static/7f3feccacc774fa023a6038f47c6195d/cbd37/02-devsisters-cockroachdb-case-study.webp 1200w,
/static/7f3feccacc774fa023a6038f47c6195d/64296/02-devsisters-cockroachdb-case-study.webp 1600w&quot; sizes=&quot;(max-width: 1600px) 100vw, 1600px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/7f3feccacc774fa023a6038f47c6195d/53918/02-devsisters-cockroachdb-case-study.png 360w,
/static/7f3feccacc774fa023a6038f47c6195d/9aebd/02-devsisters-cockroachdb-case-study.png 480w,
/static/7f3feccacc774fa023a6038f47c6195d/0f09e/02-devsisters-cockroachdb-case-study.png 640w,
/static/7f3feccacc774fa023a6038f47c6195d/3e3fe/02-devsisters-cockroachdb-case-study.png 860w,
/static/7f3feccacc774fa023a6038f47c6195d/64756/02-devsisters-cockroachdb-case-study.png 1200w,
/static/7f3feccacc774fa023a6038f47c6195d/42cbc/02-devsisters-cockroachdb-case-study.png 1600w&quot; sizes=&quot;(max-width: 1600px) 100vw, 1600px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/7f3feccacc774fa023a6038f47c6195d/42cbc/02-devsisters-cockroachdb-case-study.png&quot; alt=&quot;02 devsisters cockroachdb case study&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        쿠키런: 킹덤은 데브시스터즈 최초로 수평확장 가능한 분산 RDBMS인 CockroachDB를&lt;br&gt;
        메인 데이터베이스로 채택하였습니다. (&lt;a href=&quot;https://www.cockroachlabs.com/customers/devsisters/&quot;&gt;쿠키런: 킹덤의 CockroachDB 케이스 스터디&lt;/a&gt;)
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;두번째 리스크 포인트는 데브시스터즈에서 기존까지 사용해본 적 없었던 새로운 데이터베이스인 CockroachDB를 쿠키런 킹덤에서 사용하였다는 것입니다. 쿠키런: 킹덤에서 채택한 CockroachDB는 전통적인 RDBMS와 NoSQL의 장점을 접목한 분산 NewSQL 데이터베이스로, 데브시스터즈에서 사용하던 Couchbase, MySQL 등과는 완전히 다른 특성을 가지고 있습니다. 게임 서비스에서 데이터베이스는 사용자의 추억과 노력이 담겨 있으므로 당연히 가장 안정적이어야 하며, 팀의 경험을 바탕으로 가장 보수적으로 접근하는 것이 일반적입니다. 새로운 데이터베이스의 안정성을 담보하기 위해 데브옵스 엔지니어들은 CockroachDB의 설계와 동작에 초점을 두어 심도 깊은 스터디를 진행했고, 이를 바탕으로 클라우드 환경에서 Cloud-Native 철학과도 맞는 운영 방법을 고안하였습니다.&lt;/p&gt;
&lt;p&gt;이 시리즈물은 CockroachDB와 깊은 연관이 있는데, 앞으로의 이야기를 이해하시는데 도움이 되게끔 당시 스터디했던 자료를 엮어 기술블로그 글로 작성해 두었습니다. 데브시스터즈가 CockroachDB를 선택하게 된 과정에 대해 궁금하시다면 이 글(&lt;a href=&quot;../cockroachdb-in-production&quot;&gt;CockroachDB in Production&lt;/a&gt;)을 읽으시는 것을 권해드립니다.&lt;/p&gt;
&lt;h2 id=&quot;step-2-여러-개의-게임을-안정적으로-서비스하기-kubernetes-인프라-구축의-여정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#step-2-%EC%97%AC%EB%9F%AC-%EA%B0%9C%EC%9D%98-%EA%B2%8C%EC%9E%84%EC%9D%84-%EC%95%88%EC%A0%95%EC%A0%81%EC%9C%BC%EB%A1%9C-%EC%84%9C%EB%B9%84%EC%8A%A4%ED%95%98%EA%B8%B0-kubernetes-%EC%9D%B8%ED%94%84%EB%9D%BC-%EA%B5%AC%EC%B6%95%EC%9D%98-%EC%97%AC%EC%A0%95&quot; aria-label=&quot;step 2 여러 개의 게임을 안정적으로 서비스하기 kubernetes 인프라 구축의 여정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Step 2. 여러 개의 게임을 안정적으로 서비스하기: Kubernetes 인프라 구축의 여정&lt;/h2&gt;
&lt;p&gt;게임 스튜디오에서 희망하는 아키텍쳐에 대해 공유를 받았다면, 그 다음에 풀어야 할 숙제는 이를 어떤 추상화 단계에서 구축할 것인지 결정하는 일입니다. 데브시스터즈에서는 2019년에 이미 향후 출시되는 모든 게임을 Kubernetes 환경에서 운영하기로 결정했습니다. 이러한 결정의 배경에는 2014년 처음 Kubernetes R&amp;#x26;D를 시작하여 2016년부터 게임 개발용 플랫폼으로 지속적으로 사용해오고 있던 경험이 크게 뒷받침되었다고 할 수 있는데요. 게임을 퍼블리싱하는 입장에서 Kubernetes를 사용하면 다양한 아키텍쳐와 컴포넌트를 가진 여러 개의 게임을 비슷한 방식으로 배포하고 운영할 수 있는 장점이 있습니다. 지금 시점에서야 Kubernetes를 프로덕션 서비스에 활용하고 있는 사례들이 많이 있지만, 그 당시에는 아직 Kubernetes의 안정성에 대해 의심스러운 눈길이 많았습니다. 이 부분을 검증하기 위해 저희는 AWS EC2 인스턴스 기반 아키텍쳐로 짜여져 잘 운영 중이던 쿠키런 for Kakao를 2019년 Kubernetes 환경으로 이전하였습니다. 결과적으로 모든 컴포넌트를 최소한의 영향으로 이전하는데 성공하여 인프라 운영에 필요한 기본적인 구성을 EC2와 동일한 안정성 수준에서 Kubernetes 위에서도 구성할 수 있다는 사실을 확인했습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 600px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 56.388888888888886%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACqklEQVR42gGfAmD9AHLh+p7LsaKbhGK+4oXH366/xGqt/4/Y+Z/1/KH6/5vY+oGD8Y2OzI2I6JqK/LKW6cqw+s6a1+B6jeuluwChoX733W/+02KHo5Zmw/6Pv8hprcKU2P+z//6w//e85/u0pv+roOKXiuiVh+yljfmzme6+VXvYQUT1a3YAqXY/8MZR7dJrvYc6fbLIicTCmtOhpbjItYh2u255t36EwJSiwqfitKnpn6fCpHbZ0XR93GtnvU5LznaCAKGAXemWZ/DKdv/gXr2RWnau55/b/65hVMwgRronWrE9eLFZmKJ6iMO79K2r/6ee1u6emdmmhseFV6BhSADAnoqwakDZmCzq1G+os5WF1e649v+hrLSQN1WIODOKZ3CAXGinuLC7/v+nzdy8o67DcnrLfmDEbjvLYVoApLe6cae/nn5Whbm+bNr/sMzPwvb3ucznqXYasXUHsnkJpWYFtsuuqv//q8bVvYqZrbDPsnGGoWZxx1ZRAFijmI25pXK/1Vi34SXL1E62yq/W/cbc3rV7G7JzCrB5E6xhCdDBu67p9rmnlciqi7CdocS208bI8beRqgBglFTKjWCRoY5/wqJhtciDsISmz/y2x8+9eAW2dATBewC+fgbK0trEoaDNrInCsp25iF+8lJ+Qf5NyTEQAaLqzTaKjTNzshc+escydiod2hqS+vMyvoZmHsa+nqI11oYBItMKot4Bky5Rp2JNj24c9wHVXg2R2eks4AH/l/yq67FyBh69tS711RZJdTImbn+jQUbrt17P//7v2+8O//9e2dcKGG89vLeqKN812M72QvdG18d/I7wB02PhP1OpIopWSYjO8bzh1cHiCpsuk0r6b4uGX5/Kb4u6You2kgdOegceWZnrink7Wk1a8qOPXenLYprHQsqIkax5togAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/0c1561b4b8a91aa0567b85fc59ca3b84/33ecc/03-hello-brave-cookies.webp 360w,
/static/0c1561b4b8a91aa0567b85fc59ca3b84/263a4/03-hello-brave-cookies.webp 480w,
/static/0c1561b4b8a91aa0567b85fc59ca3b84/411c4/03-hello-brave-cookies.webp 600w&quot; sizes=&quot;(max-width: 600px) 100vw, 600px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/0c1561b4b8a91aa0567b85fc59ca3b84/53918/03-hello-brave-cookies.png 360w,
/static/0c1561b4b8a91aa0567b85fc59ca3b84/9aebd/03-hello-brave-cookies.png 480w,
/static/0c1561b4b8a91aa0567b85fc59ca3b84/dface/03-hello-brave-cookies.png 600w&quot; sizes=&quot;(max-width: 600px) 100vw, 600px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/0c1561b4b8a91aa0567b85fc59ca3b84/dface/03-hello-brave-cookies.png&quot; alt=&quot;03 hello brave cookies&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        안녕! 용감한 쿠키들(현 쿠키런: 퍼즐월드)은 데브시스터즈 게임 중 최초로 완전히 Kubernetes에서 배포할 것을 염두에 두고 런칭하였습니다.
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;이 경험을 바탕으로 2020년 1월에는 쿠키런: 퍼즐월드(당시 안녕! 용감한 쿠키들)를 런칭했습니다. 저희에게 쿠키런: 퍼즐월드는 인프라 컴포넌트와 배포 방법을 선택할 때부터 Kubernetes 환경을 고려한 최초의 프로젝트로써 일종의 De facto 패턴을 정립한 것에 의의가 있습니다. 기존에는 AWS EC2과 Route53, LoadBalancer를 사용하였다면 Kubernetes 리소스와 externalDNS, Envoy, Istio를 사용하는 식으로 패러다임이 달라졌고, 배포 방법의 경우 과거에는 Terraform과 Spinnaker를 사용하였다면 그에 더해 Helm과 ArgoCD를 추가로 사용하여 Kubernetes 환경에 보다 적합한 방향으로 확장하였습니다. 또 모니터링 관점에서도 Kubernetes 클러스터 내부에서 발생하는 메트릭 역시 추적이 필요했기에 Prometheus와 Thanos를 도입했습니다. 데브시스터즈의 프로덕션 Kubernetes 사용을 향한 여정과 그 과정에 풀어야 했던 고민들은 저희 팀 용찬호님께서 NDC21에서 발표한 &lt;a href=&quot;http://ndcreplay.nexon.com/NDC2021/sessions/NDC2021_0034.html#c=NDC2021&amp;#x26;t%5B%5D=%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;게임 서버를 품은 쿠버네티스&lt;/a&gt; 발표에 보다 자세히 담겨 있습니다.&lt;/p&gt;
&lt;p&gt;쿠키런: 킹덤은 2021년 1월에 런칭되었으므로 안녕! 용감한 쿠키들 런칭 이후로 딱 1년 간의 텀이 있었습니다. 사업적 관점에서도 쿠키런: 킹덤은 안녕! 용감한 쿠키들보다 더 넓은 지역의 더 많은 유저들에게 다가가고자 하였기 때문에 저희가 추가적으로 신경써야 할 부분도 많아졌습니다. 더 넓어진 서비스 지역을 고려하여 AWS Global Accelerator를 도입하여 Istio Service Mesh와 연결하기도 하였고, 더 커진 서비스 규모에 발맞추어 Kubernetes 워커 그룹을 구성할 때 Multi-AZ를 필수적으로 활용하기도 하였습니다. 이 기간 인프라셀에서 진행한 업무는 한 마디로 팀의 내실을 다지며 다룰 수 있는 인프라 컴포넌트를 체계적으로 확장하는 작업이었다고 요약할 수 있겠습니다.&lt;/p&gt;
&lt;h2 id=&quot;step-3-터지지-않는-서비스-인프라-구축하기-적절한-예측과-부하-테스트&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#step-3-%ED%84%B0%EC%A7%80%EC%A7%80-%EC%95%8A%EB%8A%94-%EC%84%9C%EB%B9%84%EC%8A%A4-%EC%9D%B8%ED%94%84%EB%9D%BC-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0-%EC%A0%81%EC%A0%88%ED%95%9C-%EC%98%88%EC%B8%A1%EA%B3%BC-%EB%B6%80%ED%95%98-%ED%85%8C%EC%8A%A4%ED%8A%B8&quot; aria-label=&quot;step 3 터지지 않는 서비스 인프라 구축하기 적절한 예측과 부하 테스트 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Step 3. 터지지 않는 서비스 인프라 구축하기: 적절한 예측과 부하 테스트&lt;/h2&gt;
&lt;p&gt;이제 남은 작업은 어떠한 규모의 트래픽이 들어와도 터지지 않는 서비스 인프라를 구축하는 일입니다. 앞선 절차에서 기술환경질의서를 통해 인프라 규모가 확장될 때 문제가 될 수 있는 부분들을 체크하여 개선해두었으므로 사실 비용을 최대한 끌어다 쓰면 터지지 않는 인프라를 구축할 수는 있을 것입니다. 하지만 오버프로비저닝 된 서버가 일하지 않고 있는 모습을 보는 것은 효율을 중시하는 한 명의 엔지니어로서 눈 뜨고 볼 수 없는 일입니다. 즉 너무 많지도, 너무 적지도 않은 규모의 인프라를 구축해 비용의 낭비를 최대한 줄이면서 안정적인 인프라를 구축하는 것이 중요합니다. 사용자 경험에는 영향을 주지 않으면서 서버는 효율적으로 일하는 규모의 인프라를 산정하기 위해서는 두 가지 정보가 필요한데요, 바로 사용자가 게임 내에서 여러 가지 활동을 할 때 우리 서버에 어느 정도의 부하가 발생하는지 파악하는 일과 우리 유저의 규모가 어느 정도일지에 대한 예상치입니다. 두 가지 모두 실제 서비스를 시작하기 전에는 제대로 알기 어렵지만 역설적으로 비용 효율적으로 안정적인 서비스를 제공하기 위해서는 정식 서비스를 시작하기 전에 반드시 알아야 합니다. 따라서 과거의 경험과 현재의 상황을 바탕으로 하여 적절히 예측할 수 밖에 없었습니다.&lt;/p&gt;
&lt;figure style=&quot;width: 50%; margin: 0 auto;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 100%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAACXBIWXMAAAsTAAALEwEAmpwYAAADyklEQVR42nWU228bRRTG951XnvgD+ggvvFJU1EgUhOhFtIQIpQoqNFBoUVHS0MS5gHpBqiKElAikqEohcVQVpECCU5I4ca62YzuOnfgWZ2/etePrbmLHWe+cnWF27UJ5YDQPo5nzO9+Z+c4uY/zvAAMT2B8BvhNxvYZgw2IvcD2Q+g5KMwbohoGZf4MRMufzpK6CdB+42zjXX5X7KpwNcv2G0A3sTSM3RoOewQCAsTkB6pkobBxzwN7C+/28/driiVPOl16L3G9D6X5guyAzTBmmrkkIyc9PFf+apAtAugHIoIsDD7CdRmFgoekd98k3fS1nHS+8qga7iNyDxHuGrjCAECakLHLRV15caTsrKQfErMOCi9PAdyHepgk2cbVj6eXTSyfPHPM2Q+gBcQA02YSpWmZhxvnppbwQiXoX2b09Ux8TlP0F818C30PUb1abz82deP1AtGGpF3HdIFHlEkMvSUNZnvNvrKQS4b3tzV8nfq5UKnST1LjS7o/qTh8p9c9//L77wRVyMKCzNrOc7E/0thQ2lRPbWxuu+Rn7D8HZ8ZnfnqREgRCcLyhsLCEHHhC5K7XWVQzfxlIf8DYQ+lDZbb42ssoO+72HKVGcvSc5B9O7CUEQDlXFszxfrlRr6ZFjweY4f2q97zzJd6Pd65B9aNpBleuerq8sl6V4Jc2iUiaXDPk861qtdmQVj6Gi6znv8EjCMUGqCygzCod++pzUEaZcrniWF8WIXxVjUBC0/T2jlErHA4lY1Hw2QDQ19dzMQgE6qf90xxpMeGtL5XdKQjS05iSqVMtxZXk37l/2Ls3lCwWLB9M7XW/4T6cBDViWJDkeFHd8VByXRC3LUv1scss9+/vGmsvSsULBat5G/zUGw/N8xOvyuZ5WMkk9z1MYFfhqlp18MhGPRp4p1wsn+L+fDiOlxKfTk2I0YChplOehmKpmklIyUlRUTdMwbsTn/K741NiRUvwnnQnrSD/WaoWszAcX81xUjvnE2OZR9RjXPxKrhw6SYf7hndXuy95H3+Pn4XqH6dqR4B2nB6oUykUcVnq9DtN12e+UxwcDd646715HWtV8dos3fQ5shuam7HOjX01NOxx/TrtHWxOhFdrbVl7s9QcWHw0rjweD37Y97mgNhcN1C+lg6PHIqP3ujTODN5uufHLtavtnnZ9/cOOL9j+mpmmQoigXLrx37u23XINfz9s+ar/47hunm7wet8k3fgYA+9nCyrp3bGzMbh+fmXVu70RUVaUnNMjj8XR03mppbm65dPHD1stDQ0NqsaSSUige+xv/HvpJ3l2RqgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/e7b1ab59c8ee02827f43108afb270c17/33ecc/04-chicken-or-the-egg-problem.webp 360w,
/static/e7b1ab59c8ee02827f43108afb270c17/263a4/04-chicken-or-the-egg-problem.webp 480w,
/static/e7b1ab59c8ee02827f43108afb270c17/0c8fb/04-chicken-or-the-egg-problem.webp 640w,
/static/e7b1ab59c8ee02827f43108afb270c17/521f7/04-chicken-or-the-egg-problem.webp 860w,
/static/e7b1ab59c8ee02827f43108afb270c17/d71bc/04-chicken-or-the-egg-problem.webp 1024w&quot; sizes=&quot;(max-width: 1024px) 100vw, 1024px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/e7b1ab59c8ee02827f43108afb270c17/53918/04-chicken-or-the-egg-problem.png 360w,
/static/e7b1ab59c8ee02827f43108afb270c17/9aebd/04-chicken-or-the-egg-problem.png 480w,
/static/e7b1ab59c8ee02827f43108afb270c17/0f09e/04-chicken-or-the-egg-problem.png 640w,
/static/e7b1ab59c8ee02827f43108afb270c17/3e3fe/04-chicken-or-the-egg-problem.png 860w,
/static/e7b1ab59c8ee02827f43108afb270c17/42a19/04-chicken-or-the-egg-problem.png 1024w&quot; sizes=&quot;(max-width: 1024px) 100vw, 1024px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/e7b1ab59c8ee02827f43108afb270c17/42a19/04-chicken-or-the-egg-problem.png&quot; alt=&quot;04 chicken or the egg problem&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        실제 서비스를 시작하기 전에는 알 수 없는 정보들이지만 역설적으로 실제 서비스를 시작하기 전에 알아야 합니다. (이미지 출처: DALL-E 생성)
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;마침 런칭 인프라 규모를 산정하려고 고민할 즈음 전사 플레이 테스트를 진행하게 되었습니다. 이 테스트의 목적은 초기 경험에 대한 피드백을 수집하고 밸런스 수치를 최종 확정하기 위함이었는데요. 저희에게는 인프라에 발생하는 부하 패턴을 관측할 수 있는 소중한 기회였습니다. 사용자층에 따라 선호하는 게임 플레이 패턴이 다양할 수 있는데, 데브시스터즈의 전사 구성원이라면 그래도 어느 정도 평균성을 가져갈 수 있는 집단이기도 하고요.&lt;/p&gt;
&lt;p&gt;저희는 전사 테스트 기간 동안 수집한 데이터를 시간별 window로 나누어, 특정 시간에 발생한 총 요청 수와 활성 사용자(AU)를 추출하였습니다. window의 크기를 충분히 작게 선택하면 동시 접속자(CCU)가 되겠지만, 테스트 유저 수의 모수가 충분히 크지는 않으므로 적절한 대표값을 취해 단위 사용자가 발생시키는 대략적인 초당 요청 수를 근사하였습니다. 이 값은 게임의 특성에 따라, 또는 사용자의 플레이 패턴에 따라 크게 달라질 수 있는데, 아주 대략적으로 우리 게임을 플레이하는 유저층의 특성을 정의했다고도 할 수 있겠습니다. 사용자의 요청 중에는 으레 무거운 요청도 있고, 가벼운 요청도 있으므로 그렇게 단순하게 근사하는 것은 어렵지 않느냐고 궁금증을 가지실 수도 있는데요. 쿠키런: 킹덤은 다행히도 이벤트 소싱 패턴을 사용하였기 때문에 대부분의 요청에 발생하는 오버헤드가 평이한 편이어서 많은 계산을 단순화해도 큰 문제가 없었습니다. (이벤트 소싱과 쿠키런: 킹덤의 아키텍쳐와 관련된 내용은 권태국님의 NDC21 발표 &lt;a href=&quot;http://ndcreplay.nexon.com/NDC2021/sessions/NDC2021_0006.html#c=NDC2021&amp;#x26;t%5B%5D=%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;〈쿠키런: 킹덤〉 서버 아키텍처 뜯어먹기!&lt;/a&gt;를 참고해주세요.)&lt;/p&gt;
&lt;p&gt;시기적절한 사내 테스트를 통해 서버에 발생할 부하 패턴을 파악할 수 있었으니 이제 남은 일은 사용자의 규모를 예측하는 일입니다. 여기에는 마케팅 목표와 사전예약자 모객 현황을 모니터링하며 다소 감각적으로 설정하였습니다. 또 이 숫자는 기대와 현실 그 사이 어딘가에 존재는 목표치에 가까우므로 만일에 대비해 2~3배의 충분한 버퍼를 두었습니다. 이제 단위 사용자에 대한 초당 요청 수(RPS)를 근사해내었고 목표로 하는 사용자 규모 또한 예측하였으므로 이 두 숫자를 곱하면 목표 RPS를 설정할 수 있습니다. 물론 이 과정이 수학적으로 엄밀하다고 말할 수는 없겠지만, 그래도 인프라 규모를 산정하기 위한 어느 정도의 기준치를 설정하였으니 소기의 목적은 충분히 달성했습니다. 남은 일은 실제로 쿠키런: 킹덤 아키텍쳐의 각 컴포넌트를 성능적으로 고려하여 이 목표 RPS를 문제 없이 소화하면서 비용 측면에서 가장 효율적인 인스턴스 타입과 사이즈의 조합을 찾는 일이고, 이 조합을 확실하게 검증하기 위해서는 부하 테스트가 필요합니다.&lt;/p&gt;
&lt;p&gt;기록을 찾아보니 가장 최선의 조합을 찾기 위해서 인프라셀에서는 총 79회의 부하 테스트를 진행하였습니다. 부하 테스트 스크립트를 작성하여 Load Injector를 만들었는데, 최종적으로 목표로 설정된 부하 수준은 단일 머신에서 물리적으로 만들어낼 수 있는 한계를 넘어서는 수준이었기 때문에 별도 Kubernetes 클러스터에서 fleet을 구성해 부하를 발생시켰습니다. 과학적 방법에 따라 같은 테스트를 조건만 바꾸어가며 개선여부를 검증하는 작업인지라 엔지니어의 손과 시간이 많이 소요되는 고된 작업이었지만, 안정적인 서비스를 효율적으로 제공해야 한다는 엔지니어의 사명의식으로 작업에 매진하였고 결국 만족할 수 있는 검증된 결과를 얻어내었습니다.&lt;/p&gt;
&lt;figure style=&quot;width: 70%; margin: 0 auto;&quot;&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1600px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 83.88888888888889%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAARCAIAAABSJhvpAAAACXBIWXMAAAsTAAALEwEAmpwYAAACOElEQVR42mWT6ZKbMBCE/f7Plx+prDkESJxGF5i1d8H5RrgqlUTFyiPBTHfP9F7KIu/7bt/35/P59fWV9uc/i4vXcbz+WxfnXN/3A88wjKM81BqGnoB96Luua3m3fW6Pxydru98fj8c72XuvFPB5WRZVpbLrR1HkBNpobrLsWillq8pOExijFB29c8e+S3KMkU/JlwTdNE1d1xW7MZpjXSti3Zlu6j1JzgXvlyXu+7ckL8vC6zOnNVq+lsBA3uimUiVVJvhPwzzfnLXOWVLetEMI0BOqqqQCmWdALSJ22Myq9PMMbAygW2vnU7Ykww4xgKNdlSWwTarB3gEPftMAoHSV6xJ8UpjLW/PZKuDhjHjyAaQCdxxBtnVlb7MLwXlZDO9Ne11XGUfX8jCbtjVnME1j18olQRg722s7tn7qvJ3/JHvn8+yK5uvHrzSyjKNKsomLPEPB2FxN/qNXP2/Nx23o/kJugaXRTcMkjfzKnISFMTgGLmYay85Y79f7hsz7utCw4zgu0doqz86GI/VsstimyKmCorP/cDpdwJFDdE6QH+uyWHvfKLfAYtvw38ofS8zI/XoeeR/TB4R3nJo0B98UeVunEdPeSnQDcnpO3AYdppjQ6Z/oqsVUr9dxoeBYV/PQz3a2yUDyWBuC5wdLsi/BB+dobYxce8jsp7eXGBUywFHiCjFDEgyESnMGf9LNTeub2HOmLvY80n8o3o4qJUBaGFVKGlYWNJk4MZRM15oQ0wqBTpyj+g3OXapVvpj70gAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/9c539016466698eb246e8a092475d1ea/33ecc/05-load-test-reports.webp 360w,
/static/9c539016466698eb246e8a092475d1ea/263a4/05-load-test-reports.webp 480w,
/static/9c539016466698eb246e8a092475d1ea/0c8fb/05-load-test-reports.webp 640w,
/static/9c539016466698eb246e8a092475d1ea/521f7/05-load-test-reports.webp 860w,
/static/9c539016466698eb246e8a092475d1ea/cbd37/05-load-test-reports.webp 1200w,
/static/9c539016466698eb246e8a092475d1ea/64296/05-load-test-reports.webp 1600w&quot; sizes=&quot;(max-width: 1600px) 100vw, 1600px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/9c539016466698eb246e8a092475d1ea/53918/05-load-test-reports.png 360w,
/static/9c539016466698eb246e8a092475d1ea/9aebd/05-load-test-reports.png 480w,
/static/9c539016466698eb246e8a092475d1ea/0f09e/05-load-test-reports.png 640w,
/static/9c539016466698eb246e8a092475d1ea/3e3fe/05-load-test-reports.png 860w,
/static/9c539016466698eb246e8a092475d1ea/64756/05-load-test-reports.png 1200w,
/static/9c539016466698eb246e8a092475d1ea/42cbc/05-load-test-reports.png 1600w&quot; sizes=&quot;(max-width: 1600px) 100vw, 1600px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/9c539016466698eb246e8a092475d1ea/42cbc/05-load-test-reports.png&quot; alt=&quot;05 load test reports&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        광기와 좌절의 부하 테스트. 가장 적절한 조합을 찾기 위한 실험과 검증의 반복되는 사이클입니다.
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;런칭을 준비하며 야심차게 준비하였지만 일정 상의 이유로 진행하지 못한 작업으로는 서비스 공방전이 있습니다. 서비스 공방전이란 어떤 시스템이나 프로세스의 실패 사례를 가장하는 작업으로, &lt;a href=&quot;https://aws.amazon.com/gameday/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;AWS GameDay&lt;/a&gt;에서 모티브를 얻어 기획하였습니다. 유저들을 대상으로 하는 실제 서비스를 개시하기 전에 모의 서비스를 운영하며, 공격 측에서는 실제로 일어날 법한 여러 장애 시나리오를 준비하여 발생시키고, 방어 측에서는 이러한 장애 상황이 서비스에 영향을 미치는지 확인하며 필요한 경우 서비스 장애를 복구하는 연습을 하는 것이 의도였습니다. 서비스 장애의 원인은 매우 다양하고, 복구 과정에는 생각보다 많고 다양한 직군에 속한 사람들의 협업이 필요합니다. 마치 우리가 재난 대피 훈련을 하듯, 뜻밖의 상황을 마주했을 때 각자 맡은 역할에 따라 어떤 행동을 해야하는지 경험을 통해 체득하고 체계를 잡아나가는 과정...으로 의도했습니다만 어쩄든 실제로 진행하지 못해 아쉬움이 남습니다. 언젠가 다음 서비스를 런칭할 때는 서비스 공방전을 진행해본 소회 또한 공유드릴 수 있으면 좋겠습니다.&lt;/p&gt;
&lt;h2 id=&quot;final-step-대망의-런칭과-모니터링&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#final-step-%EB%8C%80%EB%A7%9D%EC%9D%98-%EB%9F%B0%EC%B9%AD%EA%B3%BC-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81&quot; aria-label=&quot;final step 대망의 런칭과 모니터링 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Final Step. 대망의 런칭과 모니터링&lt;/h2&gt;
&lt;p&gt;바쁘게 준비하였던 대망의 런칭일은 하루가 다르게 다가왔고, 2021년 1월 21일 쿠키런: 킹덤을 드디어 유저 분들께 선보일 수 있게 되었습니다. 지금까지도 이미 과할 정도로 열심히 일했지만 사실 런칭 이후에도 데브옵스 엔지니어들은 긴장을 풀 수 없습니다. 열심히 준비했음에도 문제는 얼마든지 발생할 수 있고, 만에 하나라도 무언가 문제가 생긴다면 최대한 빠르게 해결할 수 있어야 하니까요. 일반적으로 게임은 그 특성상 평일보다 주말에, 낮 시간대 보다 저녁 시간대에 활성 사용자 수가 더 많습니다. 또, 쿠키런: 킹덤은 게임 설계 상 소원나무와 같이 컨텐츠 일간 보상 획득량 제한이 해제되는 자정에 평소의 2~3배에 달하는 피크가 발생합니다. 따라서 저희는 런칭 후 첫 주말을 보내며 늦은 시간까지도 미리 구성해 둔 모니터링 대시보드를 살피며 리소스 사용량 등 주요 지표에 이상이 없는지 관찰하고 있었습니다.&lt;/p&gt;
&lt;p&gt;앞서 들인 여러 노력 덕분인지 런칭 후 첫 주말까지도 쿠키런: 킹덤은 안정적으로 순항하고 있었습니다. 또 인프라의 주요 리소스 사용량도 정확히 설계한 범위에서 머무르는 것을 확인할 수 있었는데, 치밀하게 계산하고 공들여 테스트한 결과가 맞아떨어져 뿌듯했던 기억이 납니다. 어쨌든 이런 일도 있을 수 있다고 사전에 예상하기도 했었고 뭐가 됐든 숫자가 맞으니 기분은 좋았습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1080px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 33.33333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAIAAACHqfpvAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA0UlEQVR42mWQi67CMAiG+/6PqJnRo26ZurpVa2m5eLDMJSppGgr/RwHn/XUcJ2Z+/lgpJSVIAI+UiGmJi9gtDok0oU87c/opJB/lZLHqKsEs7qsYMWcqQBkZPxgDqzLc8nZ/8df7DLNwIVRGSR3BdAs8+yz3mA9H3zR/XXtSqdOec2VezYt8jW1vrTaGuNufm01/6n0GsKxLOem3r8HrzhCRSQOiju2sbYdm063Wh74flr1aQ85ECscYQwgAgIS3ahq/nIe2G6YpSp1FYXqbiPwDDyaarpCArW0AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/0d4fd8479c558a7136cdab93c079aebb/33ecc/06-au-storage-trend.webp 360w,
/static/0d4fd8479c558a7136cdab93c079aebb/263a4/06-au-storage-trend.webp 480w,
/static/0d4fd8479c558a7136cdab93c079aebb/0c8fb/06-au-storage-trend.webp 640w,
/static/0d4fd8479c558a7136cdab93c079aebb/521f7/06-au-storage-trend.webp 860w,
/static/0d4fd8479c558a7136cdab93c079aebb/5c666/06-au-storage-trend.webp 1080w&quot; sizes=&quot;(max-width: 1080px) 100vw, 1080px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/0d4fd8479c558a7136cdab93c079aebb/53918/06-au-storage-trend.png 360w,
/static/0d4fd8479c558a7136cdab93c079aebb/9aebd/06-au-storage-trend.png 480w,
/static/0d4fd8479c558a7136cdab93c079aebb/0f09e/06-au-storage-trend.png 640w,
/static/0d4fd8479c558a7136cdab93c079aebb/3e3fe/06-au-storage-trend.png 860w,
/static/0d4fd8479c558a7136cdab93c079aebb/b4ad3/06-au-storage-trend.png 1080w&quot; sizes=&quot;(max-width: 1080px) 100vw, 1080px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/0d4fd8479c558a7136cdab93c079aebb/b4ad3/06-au-storage-trend.png&quot; alt=&quot;06 au storage trend&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;
        단조 증가 하고 있는 DAU와 줄어들 기미 없이 차오르는 데이터베이스 스토리지 사용량.
    &lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;이렇게 좋은 경험만 남은 런칭 회고가 되었다면 좋았으련만, 현실은 그렇지 않았습니다. 런칭 후 첫 72시간 동안 유저 수가 단조증가 하고 있었는데요. 서비스 관점에서는 당연히 너무 좋은 일이지만, Ops 관점에서 저희는 두려움에 떨고 있었습니다. 정상적인 경우라면 데이터베이스 스토리지 사용량이 일정 범위에서 안정화되어야 하는데, 저희가 상정한 수치를 한참 지났음에도 추세에 꺾임 없이 증가하고 있었기 때문입니다. 주말 동안의 상승추세를 월요일 출근시간에 그대로 적용하여 계산해보니 데이터베이스 용량이 한계치에 다다를 때까지 약 36시간이 남아있는 것으로 계산되었다는 복선과 함께 쿠키런: 킹덤의 런칭을 준비했던 이야기는 마무리하도록 하겠습니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[Scala for Game Server Development]]></title><description><![CDATA[Why Scala is a great match for developing game servers.]]></description><link>https://tech.devsisters.com/posts/scala-for-game-server-development</link><guid isPermaLink="false">https://tech.devsisters.com/posts/scala-for-game-server-development</guid><pubDate>Mon, 24 Oct 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;알림: &lt;a href=&quot;/posts/scala-for-game-server-development-kr&quot;&gt;한국어로 번역된 글을 보시려면 여기를 클릭하세요.&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;At Studio Kingdom, we use the &lt;a href=&quot;https://www.scala-lang.org/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Scala programming language&lt;/a&gt; to bring our hit game &lt;a href=&quot;https://www.cookierun-kingdom.com/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;CookieRun: Kingdom&lt;/a&gt; to life. Scala is the language used for developing the game servers. Even though it is not the most popular language out there, we think that it’s a perfect fit for us and game server development in general. Let’s see why.&lt;/p&gt;
&lt;p&gt;In this article, we will discuss several aspects of game development where Scala shines particularly:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Game Logic correctness&lt;/strong&gt; is very important. There is nothing worse for a player than seeing their efforts wasted by a programming bug. Depending on the game, this logic can be particularly large and complex.&lt;/li&gt;
&lt;li&gt;Games are &lt;strong&gt;fast-evolving&lt;/strong&gt;, and players are always eager for new features. That means the game server codebase needs to constantly grow and evolve. Being able to bring features at a steady pace is primordial in order to stay on top.&lt;/li&gt;
&lt;li&gt;Designing a multi-player game means dealing with &lt;strong&gt;concurrency and scaling issues&lt;/strong&gt;. Game servers will receive a lot of concurrent requests, some of them dealing with the same entities.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;game-logic-correctness&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#game-logic-correctness&quot; aria-label=&quot;game logic correctness permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Game logic correctness&lt;/h2&gt;
&lt;h3 id=&quot;the-power-of-types&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#the-power-of-types&quot; aria-label=&quot;the power of types permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;The Power of types&lt;/h3&gt;
&lt;p&gt;Scala is a &lt;strong&gt;statically-typed&lt;/strong&gt; language, which means that types are defined and known at compile-time. This prevents a whole range of runtime errors common with dynamically-typed languages (such as Javascript’s &lt;code&gt;TypeError&lt;/code&gt;). But compared to other statically-typed languages, Scala uses types to express a lot of different things.&lt;/p&gt;
&lt;p&gt;A simple example of that is the type &lt;code&gt;Option[A]&lt;/code&gt; that is used to represent an optional value of type &lt;code&gt;A&lt;/code&gt;. Rather than returning &lt;code&gt;A&lt;/code&gt; that can be &lt;code&gt;null&lt;/code&gt; and add the proper null-handling logic everywhere at the risk of missing some places, the idiomatic Scala way is to return &lt;code&gt;Option[A]&lt;/code&gt;. That forces the developer to always handle the case where &lt;code&gt;A&lt;/code&gt; is missing, using pattern matching or functions such as &lt;code&gt;getOrElse&lt;/code&gt; or &lt;code&gt;fold&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// this returns Option[GnomeResearchLevel]&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; currentLevel &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; getGnomeResearchLevel&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;researchDataId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// we have to handle the case where there is no current level&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// in this case, we take the initial level&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; nextLevel &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; currentLevel&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;map&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;_&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;up&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;getOrElse&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;GnomeResearchLevel&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;initial&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We push this idea even further by using &lt;strong&gt;refined&lt;/strong&gt; types to add constraints to the values types can have. For example, a “Kingdom Level” is an integer that can only be 1 or more. Instead of simply using &lt;code&gt;Int&lt;/code&gt;, we use type &lt;code&gt;PosInt&lt;/code&gt; that enforces that the value is positive. We know for sure that the value will never be 0, so we don’t need to handle that case or to be careful with things like division by 0.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; PosInt &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt; Refined Positive &lt;span class=&quot;token comment&quot;&gt;// alias for a positive Int&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; a&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; PosInt &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; b&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; PosInt &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// will fail to compile&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The compiler is able to detect if static values are valid or not. For values coming from the client, an error will be raised immediately after parsing the client request if a value doesn’t respect the type constraint.&lt;/p&gt;
&lt;p&gt;Using &lt;code&gt;PosInt&lt;/code&gt; is great, but if a “Cookie Level” is also a &lt;code&gt;PosInt&lt;/code&gt;, there is a risk that we pass the wrong level to a function. To avoid that, we use a &lt;strong&gt;newtype&lt;/strong&gt;, which is basically a wrapper around our &lt;code&gt;PosInt&lt;/code&gt; to give it a special meaning.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token annotation punctuation&quot;&gt;@newtype&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; KingdomLevel&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; PosInt&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; receiveKingdomLevelRewards&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;level&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; KingdomLevel&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; List&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Reward&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now, the compiler will enforce that when we call &lt;code&gt;receiveKingdomLevelReward&lt;/code&gt;, we pass a &lt;code&gt;KindomLevel&lt;/code&gt; and nothing else.&lt;/p&gt;
&lt;p&gt;In summary, by using types that describe precisely what they hold, we avoid a whole range of silly bugs and improve the quality of our code.&lt;/p&gt;
&lt;h3 id=&quot;types-as-documentation&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#types-as-documentation&quot; aria-label=&quot;types as documentation permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Types as documentation&lt;/h3&gt;
&lt;p&gt;Just like returning an &lt;code&gt;Option&lt;/code&gt; tells you that a function may or may not return a result, we use types that describe the kind of computation that a function runs. Here are a few examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Option[A]&lt;/code&gt;: a function that may or may not return a result of type &lt;code&gt;A&lt;/code&gt;. It does not have any side effect.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Either[E, A]&lt;/code&gt;: a function that may either fail with an &lt;code&gt;E&lt;/code&gt; or succeed with an &lt;code&gt;A&lt;/code&gt;. It does not have any side effect.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ZIO[R, E, A]&lt;/code&gt;: a function that requires an environment &lt;code&gt;R&lt;/code&gt; and may either fail with an &lt;code&gt;E&lt;/code&gt; or succeed with an &lt;code&gt;A&lt;/code&gt;. It may have some side effects (such as I/O). It is equivalent to &lt;code&gt;R =&gt; Either[E, A]&lt;/code&gt; + the notion of side-effecting code.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ZPure[W, S1, S2, R, E, A]&lt;/code&gt;: a function that requires an environment &lt;code&gt;R&lt;/code&gt; and an initial state &lt;code&gt;S1&lt;/code&gt; and may either fail with an &lt;code&gt;E&lt;/code&gt; or succeed with an updated state &lt;code&gt;S2&lt;/code&gt; and an &lt;code&gt;A&lt;/code&gt; along with a log with entries of type &lt;code&gt;W&lt;/code&gt;. It does not have any side effect. It is equivalent to &lt;code&gt;(R, S1) =&gt; (List[W], Either[E, (S2, A)])&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We use these various types to reflect exactly what a function is doing. You may have noticed that there are a lot of type parameters. But in practice we use type aliases to make it much shorter and convenient. Here are a few examples.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// a function that increments a metric&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// the return type means it is side-effecting, and can never fail&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; incrementInFlightMessagesMetric&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; UIO&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;Unit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// a function that converts protobuf data into kingdom level&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// the return type means that it may fail with a non-empty list of errors&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// EitherNel[E, A] = Either[NonEmptyList[E], A]&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; extractKingdomLevel&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;raw&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Protos&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; EitherNel&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;ExtractionError&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; KingdomLevel&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// a function that returns the current pvp (a.k.a. &quot;Kingdom Arena&quot; in the game) state of the user&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// the return type means it may access the user state but not modify it&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; getCurrentPvpState&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; KingdomReader&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Pvp&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Not only do these types enforce the behavior of a function, but they give you a precise idea of what a function is doing just by reading its type signature. All possible errors are clearly declared in return types (as well as the absence of error, which is just as useful), so we are never surprised by an exception thrown unexpectedly. The same goes with side effects.&lt;/p&gt;
&lt;h3 id=&quot;make-your-own-dsl&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#make-your-own-dsl&quot; aria-label=&quot;make your own dsl permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Make your own DSL&lt;/h3&gt;
&lt;p&gt;We’ve seen a few examples where we use types to constrain what a value can be or what a function can do, and rely on the compiler to catch any programming mistake early on. We can go even further and make the whole code constrained to only allow “valid” operations. We do that by creating &lt;strong&gt;custom DSLs&lt;/strong&gt; for specific operations. Because Scala is both powerful and very concise, it excels at creating sub-languages. You can push it pretty far by making the code read almost like a spec document.&lt;/p&gt;
&lt;p&gt;Without going that far, creating a DSL is useful when you want to restrict some parts of your code to only a small subset of operations. Let’s see an example: in our game, we support transactions between multiple entities. That means we can send commands to two different users (or a user and a guild), and commit all changes only if both commands are handled successfully.&lt;/p&gt;
&lt;p&gt;We created a small DSL for transactions, with only the following operations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;identify an entity by ID: &lt;code&gt;kingdom(userId)&lt;/code&gt; , &lt;code&gt;guild(guildId)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;send a command to an entity: &lt;code&gt;entity ! command&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;combine sending operations: &lt;code&gt;&gt;&gt;&gt;&lt;/code&gt; for sequential, &lt;code&gt;&amp;#x26;&amp;#x26;&amp;#x26;&lt;/code&gt; for parallel&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We can then write simple transactions like this:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;transaction &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; context &lt;span class=&quot;token keyword&quot;&gt;=&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;kingdom&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;context&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;userId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt; SendFriendRequest&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;friendId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;kingdom&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;friendId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt; ReceiveFriendRequest&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;context&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;userId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;transaction&lt;/code&gt; block defines where our DSL can be used. We won’t be able to use “normal code” within the boundaries of this block, but only what our DSL allows. It gives us access to a &lt;code&gt;context&lt;/code&gt; that contains information about the current user. We will then send &lt;code&gt;SendFriendRequest&lt;/code&gt; to the current user (that will check that this user is allowed to send a friend request, and save the pending request), and &lt;code&gt;ReceiveFriendRequest&lt;/code&gt; to the other user (which will save the received request). These 2 operations will be done in parallel, and if any one of those 2 fail, nothing will be persisted.&lt;/p&gt;
&lt;p&gt;This little transaction system does not support other types of operations. If we tried to persist something to Redis or Kafka within the &lt;code&gt;transaction&lt;/code&gt; block, it wouldn’t be rollbacked in case of error. But because we use a custom DSL, it is actually &lt;strong&gt;impossible&lt;/strong&gt; to call that kind of code (we would get a compiler error), because we are restricted by our DSL operations.&lt;/p&gt;
&lt;p&gt;Most of our business logic is implemented using a DSL whose basic operations are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;succeed with a result&lt;/li&gt;
&lt;li&gt;fail with a &lt;code&gt;ValidationError&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;access the environment (that is a large object containing all the game configuration: the list of characters, levels, events, etc)&lt;/li&gt;
&lt;li&gt;access the user state&lt;/li&gt;
&lt;li&gt;lift an event (we use &lt;a href=&quot;https://www.eventstore.com/blog/what-is-event-sourcing&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;event sourcing&lt;/a&gt;), which will automatically update the user state&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thanks to this limited set of operations, it is impossible to do something unexpected in the middle of our business logic.&lt;/p&gt;
&lt;h2 id=&quot;evolving-fast&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#evolving-fast&quot; aria-label=&quot;evolving fast permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Evolving fast&lt;/h2&gt;
&lt;h3 id=&quot;focus-on-whats-important&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#focus-on-whats-important&quot; aria-label=&quot;focus on whats important permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Focus on what’s important&lt;/h3&gt;
&lt;p&gt;As we’ve just seen, writing DSLs constrain our code to a smaller subset of valid operations. It has another huge benefit, combined with the conciseness of Scala: it makes the code super short and readable.&lt;/p&gt;
&lt;p&gt;Creating a DSL is a good way to &lt;strong&gt;hide the complexity&lt;/strong&gt; of our program to focus on what really matters which is the business logic. Considering our transaction example above, it actually involves complex low-level code. We need to locate and send messages to entities that may be hosted on different nodes, using a particular messaging protocol. The persistence layer also needs to combine all writing operations into a single transaction that can be committed or rollbacked, communicating with an actual database. But we don’t want to care about any of this when we write our business logic.&lt;/p&gt;
&lt;p&gt;The general idea behind this is to separate the “technical” layers from the business logic so that both of them can be implemented and tested entirely separately. That allows implementing business logic, which represents the majority of the work of adding new features, without any kind of interference, which is much more efficient.&lt;/p&gt;
&lt;p&gt;Here’s a simplified example of business logic function used when a user joins a guild: we get some metadata from the game configuration, get the guild state and check that the request is valid. On success, we lift an event to record the fact that this user joined the guild. Note how we don’t care about messaging protocols, persistence, transactions, locks or whatever technical challenges that actually exist under the hood.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; joinGuild&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; KingdomProgram&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;Unit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	metadata    &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; inquireGuildMetadata
    requesterId &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; inquireRequesterId
    memberCount &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; getGuild&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;_&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;members&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;size&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;          &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; assertThat&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                     memberCount &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; metadata&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;maxMemberCount&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                     ValidationError&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;GuildFull
                   &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;          &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; liftEvent&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;GuildMemberJoined&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;requesterId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;An interesting consequence of this is that it’s easy to onboard new developers to our team: because there are only a few operations available, they will be able to understand the DSL and start writing code easily, without necessarily understanding how the DSL is implemented. They can focus on learning about the domain rather than technical concepts.&lt;/p&gt;
&lt;h3 id=&quot;death-to-boilerplate&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#death-to-boilerplate&quot; aria-label=&quot;death to boilerplate permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Death to boilerplate!&lt;/h3&gt;
&lt;p&gt;Implementing new features usually means writing a lot of code: exposing new APIs to the client, implementing the persistence layer, converting data types between different layers, etc. One thing Scala is great at is keeping that boilerplate to a minimum, thanks to a powerful &lt;strong&gt;macro engine&lt;/strong&gt; and the ability to write &lt;strong&gt;compiler plugins&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;As it is commonly done in many languages, we generate server code from protobuf file definitions. However we don’t use the generated code in our business logic, because this generated code doesn’t use our refined types and we sometimes use more convenient data types than the ones generated (collections are usually an area where the choice of data type is quite significant). It means that we need to convert the proto-generated objects that we receive from the client into our own. We are talking about thousands of objects, which could become a huge source of boilerplate. However, thanks to Scala macros, these transformations are done automatically for the most part, and we can verify it at &lt;strong&gt;compile-time&lt;/strong&gt;!&lt;/p&gt;
&lt;p&gt;All we have to do is make sure we have an instance of &lt;code&gt;Transformer&lt;/code&gt; for each data type. A macro is used to generate a &lt;code&gt;Transformer&lt;/code&gt; automatically between 2 classes, as long as their field names are matching and that we have a &lt;code&gt;Transformer&lt;/code&gt; between each corresponding field types.&lt;/p&gt;
&lt;p&gt;Let’s see an example. We have a protobuf type defined like this:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;protobuf&quot;&gt;&lt;pre class=&quot;language-protobuf&quot;&gt;&lt;code class=&quot;language-protobuf&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;message&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CraftProduction&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; id &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;repeated&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;int32&lt;/span&gt; cookie_data_ids &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token builtin&quot;&gt;int32&lt;/span&gt; completed_job_count &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The generated code will look like this:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; CraftProduction&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  cookieDataIds&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Seq&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  completedJobCount&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;But our own class with “enhanced” types is more like this:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; CraftProduction&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ActivityId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  participants&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; NonEmptyList&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;CookieDataId&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  completedJobCount&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;All IDs use more specific types such as &lt;code&gt;ActivityId&lt;/code&gt; or &lt;code&gt;CookieDataId&lt;/code&gt; so we don’t mix them up, we also use a &lt;code&gt;NonEmptyList&lt;/code&gt; because there must be at least one participant, and finally the &lt;code&gt;cookieDataIds&lt;/code&gt; field is named differently because it makes more sense in the game server code.&lt;/p&gt;
&lt;p&gt;We’ll need transformer for the following types:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a &lt;code&gt;Transformer&lt;/code&gt; between &lt;code&gt;String&lt;/code&gt; and &lt;code&gt;ActivityId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;a &lt;code&gt;Transformer&lt;/code&gt; between &lt;code&gt;Int&lt;/code&gt; and &lt;code&gt;CookieDataId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;a &lt;code&gt;Transformer&lt;/code&gt; between &lt;code&gt;Seq&lt;/code&gt; and &lt;code&gt;NonEmptyList&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Transformer for IDs are very simple and can be created in a generic way since IDs are just wrapper types. A &lt;code&gt;Transformer&lt;/code&gt; between &lt;code&gt;Seq&lt;/code&gt; and &lt;code&gt;NonEmptyList&lt;/code&gt; is also simple, failing if the &lt;code&gt;Seq&lt;/code&gt; is empty, otherwise wrapping it into a &lt;code&gt;NonEmptyList&lt;/code&gt;. The last problem we have is that the field &lt;code&gt;cookieDataIds&lt;/code&gt; was renamed into &lt;code&gt;participants&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In this case, we can create a transformer like this:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;implicit&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; transformer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  Transformer
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;define&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;entities&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;CraftProduction&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; protobuf&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;CraftProduction&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;withFieldRenamed&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;_&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;participants&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; _&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cookieDataIds&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;buildTransformer&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The macro is able to convert all fields automatically, and since we defined a mapping between the &lt;code&gt;participants&lt;/code&gt; field and the &lt;code&gt;cookieDataIds&lt;/code&gt; field, it is able to generate a transformer. Most of the time, fields are named the same way so we don’t need to write this kind of code at all. The great thing about this is that if anything is missing, we will get a compile error telling us which field couldn’t be transformed and why. We can be sure that if the code compiles, it will work as expected.&lt;/p&gt;
&lt;p&gt;Another example of relying on macros and code generation to cut down the boilerplate is our usage of GraphQL. Macros allow us to generate a GraphQL schema from our server data classes automatically, and a compiler plugin automatically generates client code powered by &lt;a href=&quot;https://www.scala-js.org/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Scala.js&lt;/a&gt; (Scala code that is converted into JavaScript). We get both a client and a server from the same code with almost no effort!&lt;/p&gt;
&lt;p&gt;Most of the macros and compiler plugins mentioned here were not even developed by us: they are available in open source libraries. The Scala OSS ecosystem is very dynamic and offers a lot of options for cutting down the boilerplate.&lt;/p&gt;
&lt;h3 id=&quot;safe-refactoring&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#safe-refactoring&quot; aria-label=&quot;safe refactoring permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Safe refactoring&lt;/h3&gt;
&lt;p&gt;An important concept in the world of functional programming with Scala is &lt;strong&gt;referential transparency&lt;/strong&gt;. A function that is referentially transparent means that we can replace that function with its value without changing the behavior of the program.&lt;/p&gt;
&lt;p&gt;Let’s see an example.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; add&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; b&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; b

&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; sum &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; add&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Everywhere in our code, we can use &lt;code&gt;sum&lt;/code&gt; or &lt;code&gt;add(1, 2)&lt;/code&gt; interchangeably. It won’t change anything in the code whether we use one or the other.&lt;/p&gt;
&lt;p&gt;On the other hand:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;object&lt;/span&gt; Test &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; total &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; add&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	total &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; total &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; a
	total
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; sum &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Test&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;add&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Using &lt;code&gt;sum&lt;/code&gt; or &lt;code&gt;Test.add(2)&lt;/code&gt; will lead to totally different behavior: calling &lt;code&gt;sum&lt;/code&gt; will always return the same result, however calling &lt;code&gt;Test.add(2)&lt;/code&gt; will trigger another modification of &lt;code&gt;total&lt;/code&gt; and return a different result each time. In this case we can say that &lt;code&gt;add&lt;/code&gt; is NOT referentially transparent.&lt;/p&gt;
&lt;p&gt;Why does this matter? If we are able to replace pieces of code by functions interchangeably, it means we can do heavy refactorings without risking to break the code’s behavior. In addition to that, static typing will ensure that moving code around always make sense and the compiler will tell us if types don’t match. The ease of refactoring means that our code will be easy to maintain and extend in the feature, allowing it to grow and evolve together with our feature set.&lt;/p&gt;
&lt;p&gt;Referential transparency is not specific to Scala, but this style of programming where all functions are referentially transparent and use immutable data types is heavily encouraged and followed within the Scala community and its library ecosystem.&lt;/p&gt;
&lt;h2 id=&quot;multi-player-challenges&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#multi-player-challenges&quot; aria-label=&quot;multi player challenges permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Multi-player challenges&lt;/h2&gt;
&lt;h3 id=&quot;local-concurrency&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#local-concurrency&quot; aria-label=&quot;local concurrency permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Local concurrency&lt;/h3&gt;
&lt;p&gt;When building a game server for a multiplayer game, we usually need multiple concurrent requests to access the same data. We sometimes need to run tasks concurrently and gather the results. We also need to run tasks in the background for the whole lifecycle of our application. The list goes on. We need to be able to perform a lot of actions &lt;strong&gt;concurrently without exhausting the resources&lt;/strong&gt; that we have.&lt;/p&gt;
&lt;p&gt;Scala comes with the concept of &lt;code&gt;Future&lt;/code&gt; but there are even better alternatives offered by libraries such as ZIO (which is what we use), Cats Effect or Monix. They all have in common to be built on top of &lt;strong&gt;fibers&lt;/strong&gt;, which are lightweight “green threads” managed by a runtime system. Unlike traditional threads, they are very cheap to create and make concurrent programming super simple.&lt;/p&gt;
&lt;p&gt;These libraries provide a bunch of operators to let you run computations in parallel: you can &lt;code&gt;fork&lt;/code&gt; a computation to run it in a different fiber, &lt;code&gt;join&lt;/code&gt; a fiber to wait for its result, &lt;code&gt;interrupt&lt;/code&gt; a fiber, &lt;code&gt;race&lt;/code&gt; multiple fibers, etc. Most of the time, you don’t even need to manipulate them as you can simply use high level operators such as &lt;code&gt;ZIO.foreachPar&lt;/code&gt; or &lt;code&gt;ZIO#zipPar&lt;/code&gt;. There are many convenient data types built on top of fibers such as &lt;code&gt;Promise&lt;/code&gt;, &lt;code&gt;Schedule&lt;/code&gt; or &lt;code&gt;Queue&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Let’s see a simple example. Whenever a player open our game client, a gRPC stream is opened to the server and maintained for as long as the user uses the game. While this stream is alive, we want to regularly update the “last connection date” of the user in the database.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;updateLastConnectionDate&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;userId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;repeat&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Schedule&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;spaced&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; hour&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fork&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That’s it. Running this code means that the &lt;code&gt;updateLastConnectionDate&lt;/code&gt; function will be called every one hour. Because we use &lt;code&gt;fork&lt;/code&gt;, the function will immediately return and run in the background. By default, a fiber is terminated when its parent fiber is terminated, so that background process will automatically be stopped when our gRPC stream is disconnected. If we wanted to keep the fiber running even after the termination of its parent, we could have used &lt;code&gt;.forkDaemon&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Another super useful data type from ZIO is the &lt;code&gt;Hub&lt;/code&gt;. Unlike a &lt;code&gt;Queue&lt;/code&gt; where each item is given to a single consumer, items that are published to a Hub are broadcasted to all subscribers. Let’s imagine that we have global messages that we want to send to all connected users through that gRPC stream we just talked about. On each game server, we can start one fiber that consumes messages from Kafka (where global messages come from), and publishes each received messages to a hub. Then, each connected gRPC stream can simply subscribe to this hub and it will receive all the messages.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; createHub&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;consumer&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Consumer&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; UIO&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Hub&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Message&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
	Hub&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;unbounded&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Message&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;flatMap &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; hub &lt;span class=&quot;token keyword&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// create a hub with no max size&lt;/span&gt;
    consumer
     &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;read                                &lt;span class=&quot;token comment&quot;&gt;// read from Kafka&lt;/span&gt;
     &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;mapZIO&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;event &lt;span class=&quot;token keyword&quot;&gt;=&gt;&lt;/span&gt; hub&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;publish&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;event&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// publish all messages to the hub&lt;/span&gt;
     &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;runDrain                            &lt;span class=&quot;token comment&quot;&gt;// consume the stream to the end&lt;/span&gt;
     &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fork                                &lt;span class=&quot;token comment&quot;&gt;// do this in a separate fiber&lt;/span&gt;
	 &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;as&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;hub&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;                             &lt;span class=&quot;token comment&quot;&gt;// return the hub&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// create a stream that will contain all messages published to the hub&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; stream &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; ZStream&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fromHub&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;hub&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;At the end, our gRPC stream is just the combination of various streams coming from different sources like this one and merged together. This &lt;code&gt;Hub&lt;/code&gt; data type is so convenient that we created our own variant, &lt;code&gt;IndexedPubSub&lt;/code&gt;, that allows subscribing to particular keys only. Thanks to &lt;code&gt;Hub&lt;/code&gt; but also &lt;code&gt;Ref&lt;/code&gt;, &lt;code&gt;Promise&lt;/code&gt;, &lt;code&gt;Queue&lt;/code&gt; and a few others, local concurrency problems become really trivial with ZIO.&lt;/p&gt;
&lt;h3 id=&quot;distributed-concurrency&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#distributed-concurrency&quot; aria-label=&quot;distributed concurrency permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Distributed concurrency&lt;/h3&gt;
&lt;p&gt;In a real application, concurrency problems are often not only local but also distributed. We want to able to scale out our game servers by simply adding more nodes. Having multiple nodes causes some problems: for example, if a guild only has 1 spot left but 2 players send a request to join that guild at the exact same time, only the first one should succeed and the other should fail.&lt;/p&gt;
&lt;p&gt;A typical way to solve that is the &lt;strong&gt;single writer principle&lt;/strong&gt;: at any given time, there should be only a single instance of each entity that can modify its state. In other words, there should be a single process handling messages for any given guild at any time. That way, this process will handle the 2 messages sequentially, succeeding the first one and failing the second.&lt;/p&gt;
&lt;p&gt;There are several ways to implement this, and the Scala ecosystem is rich with options. One of them is using Kafka consumer groups to ensure only a single consumer handles all messages of a given entity ID. In our case, because of the “synchronous” nature of our workflows (the client expects an instant response for every request), we went another route and use the &lt;strong&gt;actor sharding&lt;/strong&gt; pattern. This pattern was popularized by the &lt;a href=&quot;https://akka.io/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Akka&lt;/a&gt; library which is ideal for getting started (we are now using &lt;a href=&quot;https://github.com/devsisters/shardcake&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;our own custom implementation&lt;/a&gt; that follows similar principles).&lt;/p&gt;
&lt;p&gt;An actor is basically a tiny process that consumes messages sequentially from a queue (called “mailbox”). Sharding means that actors are distributed across a group of nodes and that you can send messages to an actor just by knowing its ID. This concept is known as &lt;strong&gt;location transparency&lt;/strong&gt;, meaning that you can send messages to an actor without knowing where the actor is actually located (it might be on the same node or another one).&lt;/p&gt;
&lt;p&gt;Following the DSL introduced earlier to send message, 2 different nodes can call the following code concurrently:&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;guild&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;guildId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt; JoinGuild&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;inviteId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Under the hood, the sharding system will find on which node the actor in charge of &lt;code&gt;guildId&lt;/code&gt; is located, and send a request to that node containing the payload (&lt;code&gt;JoinGuild&lt;/code&gt;). That node will forward those messages to the local actor handling this guild ID. The actor will handle the message that arrived first and respond with a success. It will then handle the second message and respond with a failure.&lt;/p&gt;
&lt;p&gt;Thanks to a few powerful libraries, Scala makes it really easy to handle distributed and concurrent payloads without resorting to things like distributed locks that might clog response times and create issues like deadlocks. The actor pattern, whether it is implemented with Akka or ZIO, makes our life easier once again.&lt;/p&gt;
&lt;p&gt;Through a handful of practical and simple examples, we’ve seen that Scala is a really interesting choice for building a game server. Combining its powerful type system with the best practices of functional programming, we are able to minimize the risk of errors and focus entirely on the business logic. Thanks to Scala’s rich ecosystem of open source libraries, we can cut down the boilerplate and resolve all kinds of concurrency problems.&lt;/p&gt;
&lt;p&gt;Sounds attractive to you? Want to know more? &lt;a href=&quot;https://www.notion.so/88bcb5f7325c4610a5e728b6b899e9ec&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;We are hiring&lt;/a&gt;!&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[게임 서버 개발에 스칼라 사용하기]]></title><description><![CDATA[Scala가 게임 서버 개발에 왜 좋을까요? 그 이유를 알아봅니다.]]></description><link>https://tech.devsisters.com/posts/scala-for-game-server-development-kr</link><guid isPermaLink="false">https://tech.devsisters.com/posts/scala-for-game-server-development-kr</guid><pubDate>Mon, 24 Oct 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;알림: 이 글은 &lt;a href=&quot;/posts/scala-for-game-server-development&quot;&gt;Scala for Game Server Development&lt;/a&gt;의 한국어 번역본입니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;스튜디오 킹덤 팀은 &lt;a href=&quot;https://www.scala-lang.org/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;스칼라 프로그래밍 언어&lt;/a&gt; (이하 스칼라)를 사용하여 인기 게임 &lt;strong&gt;&amp;#x3C;쿠키런: 킹덤&gt;&lt;/strong&gt; 게임 서버를 만들었습니다. 스칼라는 다른 프로그래밍 언어들보다 덜 알려졌지만, 그래도 우리가 만든 게임 서버에는 정말 잘 맞는 언어라고 생각합니다. 킹덤 팀이 왜, 그리고 어떻게 스칼라를 사용하여 게임 서버를 개발했는지 이번 글에서 여러분께 공유하려 합니다.&lt;/p&gt;
&lt;p&gt;이번 글에서, 저는 게임 개발의 몇 가지 측면을 살펴보면서 스칼라가 어떤 장점이 있는지 구체적으로 이야기해보려 합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;게임 로직&lt;/strong&gt;을 정확하게 만드는 것은 매우 중요합니다. 버그 때문에 게임 유저의 노력이 물거품이 된다면, 정말 끔찍한 일이 아닐 수 없습니다. 또한 게임에 따라, 게임의 로직은 얼마든지 크고 복잡해질 수 있기 때문에 게임 로직을 에러 없이 만들기는 쉽지 않습니다.&lt;/li&gt;
&lt;li&gt;게임은 &lt;strong&gt;매우 빠르게 변화하며&lt;/strong&gt;, 게임 유저는 항상 새로운 기능을 원합니다. 이는 곧 게임 서버의 코드가 어쩔 수 없이 거대해질 수 밖에 없고, 또한 계속해서 진화해야 함을 의미합니다. 인기있는 게임이 되기 위해서는 꾸준히 새로운 기능을 출시해야만 합니다.&lt;/li&gt;
&lt;li&gt;여러 플레이어가 참여하는 게임을 설계할 때는 &lt;strong&gt;동시성과 확장성 문제&lt;/strong&gt;를 다뤄야 합니다. 게임 서버는 매우 많은 요청을 동시에 받게 되며, 이 중에는 같은 &lt;a href=&quot;https://en.wikipedia.org/wiki/Entity#In_computer_science&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;엔티티&lt;/a&gt;를 가리키는 요청도 많이 있을 것입니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;게임-로직의-정확성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%8C%EC%9E%84-%EB%A1%9C%EC%A7%81%EC%9D%98-%EC%A0%95%ED%99%95%EC%84%B1&quot; aria-label=&quot;게임 로직의 정확성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;게임 로직의 정확성&lt;/h2&gt;
&lt;h3 id=&quot;타입의-강력함&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%83%80%EC%9E%85%EC%9D%98-%EA%B0%95%EB%A0%A5%ED%95%A8&quot; aria-label=&quot;타입의 강력함 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;타입의 강력함&lt;/h3&gt;
&lt;p&gt;스칼라는 &lt;strong&gt;정적 타입&lt;/strong&gt; 언어로, 컴파일할 때 타입이 정의되고, 타입의 존재를 인식할 수 있습니다. 덕분에 동적 타입 언어에서 흔히 발생하는 수많은 런타임 에러들 (예를 들어 자바스크립트의 &lt;code&gt;TypeError&lt;/code&gt; 같은 에러)을 미리 예방할 수 있습니다. 하지만 다른 정적 타입 언어에 비해, 스칼라는 타입을 사용하여 훨씬 더 많은 일을 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;간단한 예로, &lt;code&gt;Option[A]&lt;/code&gt; 처럼 &lt;code&gt;A&lt;/code&gt; 라는 타입의 값이 있을 수도 있고, 없을 수도 있습니다. 다른 프로그래밍 언어에서는 &lt;code&gt;A&lt;/code&gt; 타입의 값이 &lt;code&gt;null&lt;/code&gt; 값을 담을 경우 코드 이곳 저곳에서 &lt;code&gt;null&lt;/code&gt; 값에 대한 예외 처리 로직을 넣어야 하고, 잘못 동작할 수 있는 위험을 감수해야 하지만, 스칼라에서는 관용적으로 &lt;code&gt;Option[A]&lt;/code&gt; 라는 타입으로 반환하도록 만들 수 있습니다. 개발자는 이 경우 반드시 &lt;code&gt;A&lt;/code&gt; 의 값이 없을 경우를 고려하여 코드를 작성해야 하며, 이 때 패턴 매칭이나 &lt;code&gt;getOrElse&lt;/code&gt; 혹은 &lt;code&gt;fold&lt;/code&gt; 같은 함수를 사용하여 코드를 작성할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Option[GnomeResearchLevel] 를 반환합니다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; currentLevel &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; getGnomeResearchLevel&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;researchDataId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// currentLevel에 값이 없을 경우를 반드시 고려해야 하는데,&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 이 때, 초기 레벨 값을 대신 설정할 수 있습니다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; nextLevel &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; currentLevel&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;map&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;_&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;up&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;getOrElse&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;GnomeResearchLevel&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;initial&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;여기서 더 나아가서, &lt;strong&gt;refined&lt;/strong&gt;라는 스칼라 라이브러리를 이용하여 타입이 가질 수 있는 값의 종류를 제한할 수 있습니다. 예를 들어, “킹덤의 왕국 레벨” 숫자값 타입의 변수에서는 반드시 1 또는 그 이상의 값을 가져야 합니다. 단순히 &lt;code&gt;Int&lt;/code&gt; 타입을 사용하지 않고, &lt;code&gt;PosInt&lt;/code&gt; 라는 타입을 이용하여 값이 반드시 양의 정수가 되도록 제한할 수 있습니다. 이렇게 하면, 값이 절대로 0으로 설정될 일이 없어서, 값이 0이 될 경우를 고려해야 하거나 0으로 나누기 같은 연산 에러를 일으킬 가능성을 걱정하지 않아도 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; PosInt &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt; Refined Positive &lt;span class=&quot;token comment&quot;&gt;// 0보다 큰 정수값을 담는 새로운 타입 정의&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; a&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; PosInt &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; b&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; PosInt &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 컴파일 에러를 발생시킵니다.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;컴파일러는 설정된 값이 유효한지 아닌지 확인할 수 있습니다. 클라이언트로부터 오는 값의 경우, 클라이언트의 요청을 분석한 후, 값이 타입의 제약 조건을 만족하지 않을 경우 즉시 에러를 발생시킵니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PosInt&lt;/code&gt;를 사용할 수도 있지만, 만약 “쿠키 레벨”도 &lt;code&gt;PosInt&lt;/code&gt; 타입을 사용한다면, 잘못된 값을 함수에 전달할 위험성도 지니게 됩니다. 이를 예방하기 위해, &lt;strong&gt;newtype&lt;/strong&gt; 스칼라 라이브러리를 사용하여, &lt;code&gt;PosInt&lt;/code&gt; 타입에 특별한 의미를 부여하는 새로운 타입을 정의할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token annotation punctuation&quot;&gt;@newtype&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; KingdomLevel&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; PosInt&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; receiveKingdomLevelRewards&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;level&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; KingdomLevel&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; List&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Reward&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이제 컴파일러는 &lt;code&gt;receiveKingdomLevelReward&lt;/code&gt; 함수를 호출할 때, &lt;code&gt;KingdomLevel&lt;/code&gt; 타입 외의 데이터는 전달할 수 없도록 호출을 제한할 것입니다.&lt;/p&gt;
&lt;p&gt;지금까지의 내용을 활용하여 타입에 정확한 의미와 제약 조건을 부여함으로서, 불필요한 버그를 줄이고 코드의 품질을 개선할 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;타입을-통한-문서화&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%83%80%EC%9E%85%EC%9D%84-%ED%86%B5%ED%95%9C-%EB%AC%B8%EC%84%9C%ED%99%94&quot; aria-label=&quot;타입을 통한 문서화 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;타입을 통한 문서화&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Option&lt;/code&gt; 타입을 사용하기만 해도 함수가 결괏값을 반환할 수도, 반환하지 않을 수도 있음을 쉽게 알 수 있습니다. 이와 같이 타입을 사용하여 함수가 어떤 종류의 연산을 수행하는지 설명할 수도 있습니다. 몇 가지 예를 들면 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Option[A]&lt;/code&gt;: 함수가 &lt;code&gt;A&lt;/code&gt; 타입의 결괏값을 반환할 수도, 반환하지 않을 수도 있습니다. 다른 부수 효과는 없습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Either[E, A]&lt;/code&gt;: 함수 실행이 실패하면 &lt;code&gt;E&lt;/code&gt; 타입의 에러를 반환하거나, 성공하면 &lt;code&gt;A&lt;/code&gt; 타입의 값을 반환할 수 있습니다. 다른 부수 효과는 없습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ZIO[R, E, A]&lt;/code&gt;: 함수가 실행에 필요한 환경 &lt;code&gt;R&lt;/code&gt; 타입이 필요하며, 실행이 실패하면 &lt;code&gt;E&lt;/code&gt; 타입의 에러를 반환하거나, 실행이 성공하면 &lt;code&gt;A&lt;/code&gt; 타입의 값을 반환할 수 있습니다. 또한 부수 효과 (입출력)가 있을 수 있습니다. 부수 효과가 있을 수 있는 &lt;code&gt;R =&gt; Either[E, A]&lt;/code&gt; 타입이라고 볼 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ZPure[W, S1, S2, R, E, A]&lt;/code&gt;: 함수가 실행에 필요한 환경 &lt;code&gt;R&lt;/code&gt; 타입이 필요하며, 초기 상태로 &lt;code&gt;S1&lt;/code&gt; 타입이 필요하고, 실행이 실패하면 &lt;code&gt;E&lt;/code&gt; 타입의 에러를 반환하거나, 성공하면 &lt;code&gt;S2&lt;/code&gt; 타입의 업데이트된 상태값, &lt;code&gt;A&lt;/code&gt; 타입의 실행 결괏값을 반환하며, 실행 중 발생한&lt;code&gt;W&lt;/code&gt; 타입의 로그 값들을 반환합니다. &lt;code&gt;(R, S1) =&gt; (List[W], Either[E, (S2, A)])&lt;/code&gt; 와 같은 의미이며, 코드에 부수 효과는 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;위의 타입들을 사용하여 함수가 정확히 무슨 일을 하는지 알 수 있습니다. 보시다시피 타입 매개 변수들을 매우 많이 사용하고 있습니다. 그러나 실제로는 타입 별칭을 사용하여 훨씬 더 짧고 간편하게 표현할 수 있습니다. 몇 가지 예를 들어보겠습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 메트릭 값을 증가시키는 함수&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 반환 타입에서 부수 효과가 있으며, 절대 실패하지 않는다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; incrementInFlightMessagesMetric&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; UIO&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;Unit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 프로토버프 데이터를 킹덤 레벨 데이터로 변환하는 함수&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 반환 타입에서 실패 가능성이 있을 수 있습니다. 실패 시 오류 정보로 채워진 리스트가 반환됩니다.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// EitherNel[E, A] = Either[NonEmptyList[E], A]&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; extractKingdomLevel&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;raw&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Protos&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; EitherNel&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;ExtractionError&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; KingdomLevel&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 사용자의 현재 PVP (킹덤 아레나) 상태 정보를 반환하는 함수&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 반환 타입에서 사용자 상태 정보에 접근하지만, 상태 정보를 수정하지 않습니다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; getCurrentPvpState&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; KingdomReader&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Pvp&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이와 같이 타입을 활용하면, 함수의 동작을 강제할 뿐만 아니라 타입의 이름이나 타입 구성 형태를 읽기만 해도 함수가 무슨 일을 하는지 명확하게 알 수 있습니다. 발생할 수 있는 모든 오류가 반환 타입에서 명확하게 선언되므로 (오류가 발생하지 않더라도 그렇습니다.), 예외가 갑자기 발생하여 전파될 걱정을 하지 않아도 됩니다. 부수 효과도 마찬가지입니다.&lt;/p&gt;
&lt;h3 id=&quot;직접-도메인-특화-언어-dsl-만들기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A7%81%EC%A0%91-%EB%8F%84%EB%A9%94%EC%9D%B8-%ED%8A%B9%ED%99%94-%EC%96%B8%EC%96%B4-dsl-%EB%A7%8C%EB%93%A4%EA%B8%B0&quot; aria-label=&quot;직접 도메인 특화 언어 dsl 만들기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;직접 도메인 특화 언어 (DSL) 만들기&lt;/h3&gt;
&lt;p&gt;지금까지 값이 가질 수 있는 유효한 범위를 정하거나, 함수가 무슨 일을 하는지를 타입을 이용해 표현하는 방법을 살펴보았으며, 컴파일러가 프로그래밍 상의 에러를 일찍 잡아내도록 하는 방법도 살펴보았습니다. 여기서 더 나아가 “유효한” 동작만을 수행하도록 코드 수준의 제약을 추가할 수 있습니다. 특정 작업에만 사용하기 위한 **별도의 도메인 특화 언어 (이하 DSL)**을 만드는 방식으로 구현할 수 있습니다. 스칼라는 그 자체로도 매우 강력하고 간결하여 하위 언어를 만들 때에도 좋습니다. 마치 사양 문서를 작성하듯 코드를 만듦으로서 꽤 많은 부분을 이룰 수 있습니다.&lt;/p&gt;
&lt;p&gt;거창한 예를 들지 않더라도 DSL을 만들면, 특정 코드 영역에서 지정된 동작을 수행하도록 제한할 수 있습니다. 예를 하나 들어보겠습니다. 우리 게임에서는 여러 엔티티 사이에 트랜잭션을 지원합니다. 다시 말해 서로 다른 두 사용자 (혹은 사용자와 길드)에 명령어를 각각 보낸 뒤, 모든 명령어의 실행 결과가 성공적으로 끝날 때에만 변경 사항을 반영할 수 있습니다.&lt;/p&gt;
&lt;p&gt;그래서 우리 팀에서는 트랜잭션을 위해 작은 DSL을 만들고, 다음의 작업만 수행할 수 있도록 했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ID로 엔티티를 식별할 수 있게 합니다: &lt;code&gt;kingdom(userId)&lt;/code&gt; , &lt;code&gt;guild(guildId)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;엔티티로 명령어를 보냅니다: &lt;code&gt;entity ! command&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;보내기 작업을 하나로 묶습니다: 순차적으로 보내기 위해 &lt;code&gt;&gt;&gt;&gt;&lt;/code&gt; , 병렬로 보내기 위해 &lt;code&gt;&amp;#x26;&amp;#x26;&amp;#x26;&lt;/code&gt; 연산자를 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이제 다음과 같이 간단한 트랜잭션을 작성할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;transaction &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; context &lt;span class=&quot;token keyword&quot;&gt;=&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;kingdom&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;context&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;userId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt; SendFriendRequest&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;friendId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;kingdom&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;friendId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt; ReceiveFriendRequest&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;context&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;userId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;transaction&lt;/code&gt; 블록은 우리가 사용할 DSL이 사용될 곳을 나타냅니다. 이 블록 안에서 “보통 코드”는 사용할 수 없고, 우리가 만든 DSL만 사용할 수 있습니다. 그런 다음, 현재 사용자에게 &lt;code&gt;SendFriendRequest&lt;/code&gt; 명령 (이 사용자가 친구 요청을 보낼 수 있는지 확인하고, 친구 추가할 상대의 대기 중인 친구 요청 정보를 저장하는 명령)을 보내고, 다른 사용자에게는 &lt;code&gt;ReceiveFriendRequest&lt;/code&gt; 명령 (수신된 요청을 저장하는 명령)을 보냅니다.&lt;/p&gt;
&lt;p&gt;이 작은 트랜잭션 시스템에서 다른 종류의 작업은 지원하지 않습니다. 만약 레디스나 카프카 같은 곳에 데이터를 저장하기 위한 코드를 &lt;code&gt;transaction&lt;/code&gt; 블록 안에서 추가하려 한다면, 문제가 발생했을 때 제대로 롤백되지 않을 것입니다. 하지만 별도의 DSL을 사용하기 때문에 이런 일은 &lt;strong&gt;불가능&lt;/strong&gt;합니다. 코드를 추가하려고 하면, DSL에서 허용하지 않는 작업을 하려고 했기 때문에 컴파일 에러가 발생할 것입니다.&lt;/p&gt;
&lt;p&gt;대부분의 비즈니스 로직은 이와 같이 DSL을 사용하여 구현했으며, 각 DSL에서는 다음의 작업만 할 수 있도록 기능이 제한되어 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;결괏값과 함께 성공 상태를 표현합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ValidationError&lt;/code&gt; 와 함께 실패 상태를 표현합니다.&lt;/li&gt;
&lt;li&gt;환경 (모든 게임 설정 정보를 담는 거대한 개체로, 캐릭터 목록, 레벨, 이벤트 등이 들어있습니다.) 데이터에 접근할 수 있습니다.&lt;/li&gt;
&lt;li&gt;사용자 상태에 접근할 수 있습니다.&lt;/li&gt;
&lt;li&gt;사용자 상태를 변경하는 이벤트를 처리합니다. (&lt;a href=&quot;https://www.eventstore.com/blog/what-is-event-sourcing&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;이벤트 소싱&lt;/a&gt;을 사용합니다.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;약속된 작업만 수행할 수 있도록 제한한 덕분에, 비즈니스 로직 중간에서 예기치 않은 에러가 발생할 가능성을 막을 수 있었습니다.&lt;/p&gt;
&lt;h2 id=&quot;빠르게-변화에-대응하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B9%A0%EB%A5%B4%EA%B2%8C-%EB%B3%80%ED%99%94%EC%97%90-%EB%8C%80%EC%9D%91%ED%95%98%EA%B8%B0&quot; aria-label=&quot;빠르게 변화에 대응하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;빠르게 변화에 대응하기&lt;/h2&gt;
&lt;h3 id=&quot;중요한-것에만-집중하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A4%91%EC%9A%94%ED%95%9C-%EA%B2%83%EC%97%90%EB%A7%8C-%EC%A7%91%EC%A4%91%ED%95%98%EA%B8%B0&quot; aria-label=&quot;중요한 것에만 집중하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;중요한 것에만 집중하기&lt;/h3&gt;
&lt;p&gt;지금까지 본 것처럼, DSL을 작성하여 코드가 올바른 작업만 실행하면서도 작게 유지될 수 있도록 제한할 수 있습니다. 이는 스칼라 프로그래밍 언어의 간편함과 결합되면서 훨씬 더 큰 이점을 가져다 주는데, 코드를 매우 짧고 읽기 쉽게 만들어줍니다.&lt;/p&gt;
&lt;p&gt;DSL을 만들면 프로그램에서 &lt;strong&gt;복잡한 세부 사항을 효과적으로 감출 수 있어&lt;/strong&gt;, 비즈니스 로직에서 정말 중요한 것에만 집중할 수 있게 해줍니다. 위의 트랜잭션 예제는 사실 매우 복잡한 코드가 그 안에서 작동하게 됩니다. 다른 노드 위에 저장된 엔티티로 메시지를 보내기 위해, 노드의 위치를 파악하고, 어떤 메시지 프로토콜을 사용하여 메시지를 보낼 것인지 결정하는 등의 일을 해야 할 것입니다. 영속 레이어에서는 모든 쓰기 작업을 하나의 트랜잭션으로 묶어 한 번에 반영되거나 롤백되도록 처리해야 하고, 실제 데이터베이스와 상호 작용해야 할 것입니다. 그러나 비즈니스 로직을 작성할 때 이런 부분들을 신경쓰기는 너무 복잡합니다.&lt;/p&gt;
&lt;p&gt;즉, DSL를 통해 기술적인 레이어를 비즈니스 로직과 분리할 수 있어 개별적으로 구현하고 테스트할 수 있습니다. 그 결과, 새로운 기능을 추가하는 작업이 대부분인 비즈니스 로직을 어떤 간섭도 없이 구현할 수 있어 훨씬 더 효율적입니다.&lt;/p&gt;
&lt;p&gt;여기 사용자가 길드에 가입할 때 사용되는 간단한 비즈니스 로직 함수 예제가 있습니다. 게임 설정으로부터 메타데이터를 가져오고, 길드 상태를 확인한 후, 요청이 올바른지 확인합니다. 문제가 없다면, 이벤트를 처리하면서 사용자가 길드에 가입했다는 사실을 기록합니다. 여기서 메시징 프로토콜, 영속성, 트랜잭션, 데이터베이스 잠금 등 어떤 것이든 밑에서 일어나는 기술적인 세부 사항은 전혀 신경쓰지 않았다는 점을 주목하세요.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; joinGuild&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; KingdomProgram&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;Unit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	metadata    &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; inquireGuildMetadata
    requesterId &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; inquireRequesterId
    memberCount &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; getGuild&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;_&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;members&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;size&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;          &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; assertThat&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
                     memberCount &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; metadata&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;maxMemberCount&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                     ValidationError&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;GuildFull
                   &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;          &lt;span class=&quot;token keyword&quot;&gt;&amp;lt;-&lt;/span&gt; liftEvent&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;GuildMemberJoined&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;requesterId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;덕분에 새로운 개발자가 우리 팀에 합류하더라도 온보딩 걱정은 자연스럽게 덜 수 있게 됩니다. 왜냐하면 사용할 수 있는 연산자가 얼마 되지 않아 DSL을 쉽게 이해하고 코드를 작성할 수 있습니다. 기술적인 개념보다는 도메인 지식을 배우는 데 집중할 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;보일러플레이트와-헤어질-시간&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B3%B4%EC%9D%BC%EB%9F%AC%ED%94%8C%EB%A0%88%EC%9D%B4%ED%8A%B8%EC%99%80-%ED%97%A4%EC%96%B4%EC%A7%88-%EC%8B%9C%EA%B0%84&quot; aria-label=&quot;보일러플레이트와 헤어질 시간 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;보일러플레이트와 헤어질 시간!&lt;/h3&gt;
&lt;p&gt;새로운 기능을 구현하면 많은 양의 코드를 작성해야 합니다. 클라이언트를 위한 새로운 API를 공개해야 하고, 영속성 레이어를 구현하며, 서로 다른 레이어 사이에 주고 받는 데이터 타입을 변환하는 등의 일이 될 것입니다. 스칼라를 사용하면 보일러플레이트 코드를 최대한 줄일 수 있습니다. 이는 강력한 &lt;strong&gt;매크로 엔진&lt;/strong&gt;과 &lt;strong&gt;컴파일러 플러그인&lt;/strong&gt;을 작성할 수 있는 스칼라의 능력 덕분입니다.&lt;/p&gt;
&lt;p&gt;수많은 프로그래밍 언어들과 마찬가지로, 우리 팀에서도 프로토버프 파일 정의를 사용하여 서버 코드를 생성합니다. 하지만 비즈니스 로직에서 생성된 코드를 사용하지는 않는데, 생성된 코드는 우리가 정의한 refine 타입을 사용하지 않으며, 자동 생성된 코드에서 사용하는 타입보다는 더 사용하기 편리한 데이터 타입을 사용하는 것을 선호하기 때문입니다. (컬렉션은 일반적으로 데이터 타입 선택이 무척 중요한 영역입니다.) 다시 말해, 우리가 클라이언트로부터 받은 자동 생성된 타입으로 이루어진 개체를 우리 자신의 것으로 변환해야 한다는 것입니다. 수천가지 개체들을 사용하여 통신을 진행하게 되며, 이는 많은 양의 보일러플레이트 덩어리와 씨름하는 일일 수 있습니다. 하지만, 스칼라 매크로 덕분에, 이런 변환 작업의 대부분이 자동으로 처리될 뿐 아니라, &lt;strong&gt;컴파일 시점&lt;/strong&gt;에서 데이터의 유효성을 검증할 수도 있습니다.&lt;/p&gt;
&lt;p&gt;여기서 각 데이터 타입마다 &lt;code&gt;Transformer&lt;/code&gt; 인스턴스를 만들었는지 확인해야 합니다. 매크로는 필드 이름이 일치하고 각 해당 필드 유형 사이에 변환기가 있다면, 두 개의 클래스 간에 &lt;code&gt;Transformer&lt;/code&gt;를 자동으로 생성하는 데 사용됩니다.&lt;/p&gt;
&lt;p&gt;예제를 살펴보겠습니다. 다음의 프로토버프 타입이 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;protobuf&quot;&gt;&lt;pre class=&quot;language-protobuf&quot;&gt;&lt;code class=&quot;language-protobuf&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;message&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CraftProduction&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; id &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;repeated&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;int32&lt;/span&gt; cookie_data_ids &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token builtin&quot;&gt;int32&lt;/span&gt; completed_job_count &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;생성된 코드는 다음과 같은 모습일 것입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; CraftProduction&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  cookieDataIds&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Seq&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  completedJobCount&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그러나 앞서 살펴본 매크로를 이용한다면 다음과 같이 더 “개선된&quot; 타입을 만들 수 있을 것입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; CraftProduction&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; ActivityId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  participants&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; NonEmptyList&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;CookieDataId&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  completedJobCount&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;모든 ID는 &lt;code&gt;ActivityId&lt;/code&gt; 나 &lt;code&gt;CookieDataId&lt;/code&gt; 처럼 좀 더 정확한 타입을 사용하여 단순히 정수 값으로만 취급하여 값이 섞이지 않도록 분리하고, 또한 &lt;code&gt;NonEmptyList&lt;/code&gt; 타입을 사용하여 최소한 하나 이상의 값이 들어있는 리스트여야 함을 나타냅니다. 그리고 &lt;code&gt;cookieDataIds&lt;/code&gt; 필드 대신 다른 이름을 사용하여 게임 서버 코드를 이해하기 쉽게 만들었습니다.&lt;/p&gt;
&lt;p&gt;아래 타입들 사이를 변환하기 위해 각각의 &lt;code&gt;Transformer&lt;/code&gt; 들이 필요할 것입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;String&lt;/code&gt; 타입과 &lt;code&gt;ActivityId&lt;/code&gt; 타입 사이를 변환하는 &lt;code&gt;Transformer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Int&lt;/code&gt; 타입과 &lt;code&gt;CookieDataId&lt;/code&gt; 타입 사이를 변환하는 &lt;code&gt;Transformer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Seq&lt;/code&gt; 타입과 &lt;code&gt;NonEmptyList&lt;/code&gt; 타입 사이를 변환하는 &lt;code&gt;Transformer&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ID를 처리하는 &lt;code&gt;Transformer&lt;/code&gt;는 단지 값을 보조하는 타입이므로 구조가 매우 단순하여, 일반화된 방법으로 만들 수 있을 것입니다. &lt;code&gt;Seq&lt;/code&gt; 타입과 &lt;code&gt;NonEmptyList&lt;/code&gt; 타입 사이를 변환하는 &lt;code&gt;Transformer&lt;/code&gt; 역시 구조가 단순하며, 다만 &lt;code&gt;Seq&lt;/code&gt; 안에 들어있는 요소가 하나도 없을 경우에만 실패로 처리하고, 그 외에는 &lt;code&gt;NonEmptyList&lt;/code&gt; 로 쉽게 변환할 수 있을 것입니다.&lt;/p&gt;
&lt;p&gt;위의 예제를 만들기 위해, 다음과 같이 변환기를 정의할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;implicit&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; transformer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  Transformer
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;define&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;entities&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;CraftProduction&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; protobuf&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;CraftProduction&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;withFieldRenamed&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;_&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;participants&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; _&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cookieDataIds&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;buildTransformer&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;매크로는 모든 필드를 자동으로 변환할 수 있고, &lt;code&gt;participants&lt;/code&gt; 필드와 &lt;code&gt;cookieDataIds&lt;/code&gt; 필드 사이의 관계를 정의했기 때문에, 이를 토대로 변환기를 만들 수 있을 것입니다. 대부분의 경우 필드 이름이 같은 방식으로 지정되므로 이런 형태의 코드를 전혀 작성할 필요가 없습니다. 만약 누락된 부분이 있다면, 컴파일 단계에서 변환할 수 없는 필드와 그 이유를 알려주는 에러를 발생시킬 것이므로 문제를 미리 찾을 수 있어 매우 유용합니다. 코드가 에러 없이 컴파일된다면 예상대로 작동할 것을 확신할 수 있는 것입니다.&lt;/p&gt;
&lt;p&gt;보일러플레이트를 줄이기 위해 매크로와 코드 생성에 의존하는 또 다른 예는 그래프 QL을 사용하는 것입니다. 매크로를 사용하면 서버 데이터 클래스에서 그래프 QL 스키마를 자동으로 생성할 수 있으며, 컴파일러 플러그인은 스칼라 JS (자바스크립트로 변환되는 스칼라 코드)로 구동되는 클라이언트 코드를 자동으로 생성합니다. 힘들이는 것 없이 동일한 코드에서 클라이언트와 서버를 모두 얻을 수 있습니다!&lt;/p&gt;
&lt;p&gt;이 글에서 소개한 대부분의 매크로와 컴파일러 플러그인은 저희가 개발한 것이 아닌, 오픈 소스 라이브러리에서 사용할 수 있는 매크로와 컴파일러 플러그인들입니다. 스칼라 오픈 소스 생태계는 매우 역동적이며 보일러플레이트를 줄이기 위한 많은 방안을 제공합니다.&lt;/p&gt;
&lt;h3 id=&quot;안전한-리팩토링&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%95%88%EC%A0%84%ED%95%9C-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81&quot; aria-label=&quot;안전한 리팩토링 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;안전한 리팩토링&lt;/h3&gt;
&lt;p&gt;스칼라를 사용한 함수형 프로그래밍에서 &lt;strong&gt;참조 투명성&lt;/strong&gt;은 중요한 개념입니다. 참조적으로 투명한 함수는 프로그램의 동작을 변경하지 않고 해당 함수를 해당 값으로 대체할 수 있습니다.&lt;/p&gt;
&lt;p&gt;다음의 예를 살펴보겠습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; add&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; b&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; b

&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; sum &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; add&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;코드 어디서나 &lt;code&gt;sum&lt;/code&gt; 또는 &lt;code&gt;add(1, 2)&lt;/code&gt;를 서로 바꿔서 사용할 수 있습니다. 어떤 것을 사용하든 의미는 동일합니다.&lt;/p&gt;
&lt;p&gt;또 다른 예를 살펴보겠습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;object&lt;/span&gt; Test &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; total &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; add&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	total &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; total &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; a
	total
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; sum &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Test&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;add&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;반면 위의 코드에서 &lt;code&gt;sum&lt;/code&gt; 과 &lt;code&gt;Test.add(2)&lt;/code&gt; 는 각각 완전히 다른 결과를 만들 것입니다. &lt;code&gt;sum&lt;/code&gt; 은 항상 같은 결과를 반환하지만, &lt;code&gt;Test.add(2)&lt;/code&gt; 는 &lt;code&gt;total&lt;/code&gt; 에 변경을 일으키게 되고, 실행할 때마다 다른 값을 반환할 것입니다. 이 때 &lt;code&gt;add&lt;/code&gt; 는 &lt;strong&gt;참조 투명성이 없다&lt;/strong&gt;고 말할 수 있습니다.&lt;/p&gt;
&lt;p&gt;참조 투명성을 지키는 것이 왜 중요할까요? 코드 조각을 함수로 교체할 수 있다는 것은, 코드의 동작을 깨뜨릴 위험 없이 리팩토링을 폭넓게 수행할 수 있습니다. 그 외에도 정적 타입 지정은 코드 이동이 항상 의미가 있는지 확인하고, 타입이 다르면 컴파일러에서 알려주어 에러를 쉽게 찾을 수 있습니다. 리팩토링이 쉬우면, 만들었던 기능과 함께 계속해서 성장하고 발전하면서, 코드를 유지 관리하거나, 기능을 확장하기 쉽기 때문입니다.&lt;/p&gt;
&lt;p&gt;참조 투명성은 스칼라에만 국한된 것은 아니지만, 스칼라 커뮤니티와 라이브러리 생태계 내에서는 모든 기능이 참조적으로 투명하고 불변 데이터 유형을 사용하는 방식의 프로그래밍 방식을 사용할 것을 적극 권장합니다. 또한, 스칼라 프로그래밍 언어를 사용하는 많은 개발자들이 참조 투명성을 지키기 위해 노력합니다.&lt;/p&gt;
&lt;h2 id=&quot;멀티-유저-문제&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A9%80%ED%8B%B0-%EC%9C%A0%EC%A0%80-%EB%AC%B8%EC%A0%9C&quot; aria-label=&quot;멀티 유저 문제 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;멀티 유저 문제&lt;/h2&gt;
&lt;h3 id=&quot;로컬-동시성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A1%9C%EC%BB%AC-%EB%8F%99%EC%8B%9C%EC%84%B1&quot; aria-label=&quot;로컬 동시성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;로컬 동시성&lt;/h3&gt;
&lt;p&gt;멀티 유저 게임을 위한 게임 서버를 만들 때, 여러 요청이 동시에 같은 데이터를 활용하는 기능을 자주 개발하게 됩니다. 그리고 종종 동시에 작업을 실행하고 결과를 수집하기도 합니다. 또한 애플리케이션 생애주기 전체에 걸쳐 백그라운드에서 작업을 실행하도록 만들 필요가 있습니다. 그 외에도 정말 많습니다. &lt;strong&gt;서버 자원을 소진하지 않고 동시에 많은 작업을 해내야&lt;/strong&gt; 할 필요가 있습니다.&lt;/p&gt;
&lt;p&gt;스칼라에서는 &lt;code&gt;Future&lt;/code&gt; 라는 개념을 제공하지만, ZIO (우리 팀에서 사용 중인 라이브러리), Cats Effect, Monix 등 다양한 라이브러리들을 대신 사용할 수도 있습니다. 이런 라이브러리들은 모두 &lt;strong&gt;파이버&lt;/strong&gt; 위에서 만들어졌다는 공통점이 있는데, 파이버란 런타임 시스템이 관리하는 경량화된 “가상 스레드&quot;입니다. 전통적인 스레드와 달리, 동시성 프로그래밍을 매우 단순하게 만들어주고, 원하는대로 만들어 사용하기 쉽습니다.&lt;/p&gt;
&lt;p&gt;동시성 라이브러리들은 계산을 병렬로 실행할 수 있는 여러가지 오퍼레이터를 제공합니다. 예를 들어, &lt;code&gt;fork&lt;/code&gt; 를 이용하여 다른 파이버에서 계산이 실행되도록 만들거나, &lt;code&gt;join&lt;/code&gt; 을 사용하여 기존 스레드가 결과를 반환할 때까지 기다리게 하거나, &lt;code&gt;interrupt&lt;/code&gt; 로 파이버를 중단시키거나, &lt;code&gt;race&lt;/code&gt; 로 여러 파이버들을 경합시키는 등의 작업을 할 수 있습니다. 그러나 대부분은 고차원의 오퍼레이터인 &lt;code&gt;ZIO.foreachPar&lt;/code&gt; 나 &lt;code&gt;ZIO#zipPar&lt;/code&gt; 같은 오퍼레이터를 활용할 수 있으므로 스레드 작업을 직접 제어할 필요가 없습니다. 또한 파이버를 기반으로 만든 편리한 여러 데이터 타입, 예를 들어 &lt;code&gt;Promise&lt;/code&gt;, &lt;code&gt;Schedule&lt;/code&gt;, &lt;code&gt;Queue&lt;/code&gt; 같은 데이터 타입을 활용할 수도 있습니다.&lt;/p&gt;
&lt;p&gt;간단한 예를 하나 살펴보겠습니다. 유저가 우리가 만든 게임 클라이언트를 실행하면, 사용자가 게임을 종료하기 전까지 서버로 스트림이 연결된 후 계속 유지될 것입니다. 스트림이 유지되는 동안, 주기적으로 “마지막 연결일시&quot; 정보를 사용자 데이터베이스에 기록하려고 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;updateLastConnectionDate&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;userId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;repeat&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Schedule&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;spaced&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; hour&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fork&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;어렵지 않습니다. 이 코드를 실행하면, &lt;code&gt;updateLastConnectionDate&lt;/code&gt; 함수가 매 시간마다 호출됩니다. &lt;code&gt;fork&lt;/code&gt;를 사용했으므로, 함수를 호출하는 코드는 즉시 반환되고, 작업은 백그라운드에서 실행될 것입니다. 기본적으로, 파이버는 상위 파이버가 종료되면 자동으로 종료되므로, 백그라운드 프로세스는 gRPC 스트림의 연결이 끊어지면 자동으로 종료됩니다. 상위 파이버가 종료된 후에도 계속 파이버를 유지하려면, &lt;code&gt;.forkDaemon&lt;/code&gt; 을 사용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;또한 ZIO에는 무척 유용한 데이터 타입인 &lt;code&gt;Hub&lt;/code&gt; 가 있습니다. &lt;code&gt;Queue&lt;/code&gt; 는 안에 들어있는 항목이 한 곳에만 전달되지만, &lt;code&gt;Hub&lt;/code&gt; 에 들어있는 항목은 이벤트를 수신하는 모든 곳에 전달됩니다. 앞서 살펴본대로 gRPC 스트림이 연결된 모든 사용자들에게 전체 메시지를 보내는 상황을 상상해보겠습니다. 각 게임 서버에서는 카프카로부터 오는 메시지 (글로벌 메시지가 카프카를 통해 전달될 것입니다)를 구독하는 파이버를 시작할 수 있을 것이고, 수신된 각각의 메시지를 허브로 보낼 것입니다. 그다음, 연결된 각각의 gRPC 스트림에서는 허브에서 발생하는 이벤트를 수신하며 들어오는 메시지를 모두 받을 것입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; createHub&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;consumer&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Consumer&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; UIO&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Hub&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Message&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
	Hub&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;unbounded&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Message&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;flatMap &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; hub &lt;span class=&quot;token keyword&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 크기 제한이 없는 허브를 만듭니다.&lt;/span&gt;
    consumer
     &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;read                                &lt;span class=&quot;token comment&quot;&gt;// 카프카에서 데이터를 읽습니다.&lt;/span&gt;
     &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;mapZIO&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;event &lt;span class=&quot;token keyword&quot;&gt;=&gt;&lt;/span&gt; hub&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;publish&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;event&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 모든 메시지를 허브로 보냅니다.&lt;/span&gt;
     &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;runDrain                            &lt;span class=&quot;token comment&quot;&gt;// 스트림을 끝까지 읽습니다.&lt;/span&gt;
     &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fork                                &lt;span class=&quot;token comment&quot;&gt;// 이 작업을 별도의 파이버에서 처리합니다.&lt;/span&gt;
	 &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;as&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;hub&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;                         &lt;span class=&quot;token comment&quot;&gt;// 허브를 반환합니다.&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 허브에 보낸 모든 메시지를 포함할 스트림을 만듭니다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; stream &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; ZStream&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fromHub&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;hub&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;결국 게임 서버에서 관리하는 gRPC 스트림은 이처럼 다양한 곳에서 오는 다양한 스트림을 조합하여 하나로 통합한 것입니다. &lt;code&gt;Hub&lt;/code&gt; 데이터 타입을 활용하여 우리 팀에서는 특정 키만 선택적으로 구독할 수 있는 &lt;code&gt;IndexedPubSub&lt;/code&gt; 라는 또 다른 타입을 만들어 편하게 활용할 수 있었습니다. &lt;code&gt;Hub&lt;/code&gt; 뿐 아니라, &lt;code&gt;Ref&lt;/code&gt;, &lt;code&gt;Promise&lt;/code&gt;, &lt;code&gt;Queue&lt;/code&gt; 등 여러 타입 덕분에, 로컬 동시성 문제를 ZIO를 이용하여 단순하게 만들 수 있었습니다.&lt;/p&gt;
&lt;h3 id=&quot;분산-동시성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B6%84%EC%82%B0-%EB%8F%99%EC%8B%9C%EC%84%B1&quot; aria-label=&quot;분산 동시성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;분산 동시성&lt;/h3&gt;
&lt;p&gt;실제 애플리케이션에서, 동시성 문제는 로컬에서만이 아니라 분산된 환경에서도 자주 발생합니다. 우리 팀은 게임 서버를 단순히 노드를 추가하기만 해도 쉽게 확장할 수 있는 구조를 만들기 원했습니다. 여러 노드를 사용하면 몇 가지 문제점이 발생할 것입니다. 예를 들어, 만약 길드에 자리가 한 개만 남은 상황에서, 두 명의 유저가 동시에 그 길드에 정확히 같은 시간에 가입 요청을 보낸다면, 한 사용자는 가입에 성공할 것이고, 다른 한 명은 실패할 것입니다.&lt;/p&gt;
&lt;p&gt;이 문제를 풀 수 있는 일반적인 방법은 **단일 기록자 원칙 (Single Writer Principle)**을 적용하는 것입니다. 이 원칙은 주어진 시간에 각각의 엔티티의 상태를 변경할 수 있는 인스턴스를 단 하나만 만들어 유지하는 것을 말합니다. 다시 말해, 길드에 대한 메시지를 처리하는 프로세스가 주어진 시간 범위 안에 단 하나만 있게 만드는 것입니다. 이렇게 하면, 프로세스는 두 개의 메시지를 순차적으로 처리할 수 있으므로, 먼저 도착한 사람만 가입에 성공하도록 만들 수 있는 것입니다.&lt;/p&gt;
&lt;p&gt;이 방식을 구현하는 방법은 여러 가지가 있을 수 있지만, 스칼라 에코 시스템에서는 풍부한 옵션을 제공합니다. 그 중 하나는 카프카 컨슈머 그룹을 사용하여 하나의 컨슈머가 특정 엔티티 ID에 대한 모든 메시지를 수신하여 처리하도록 만드는 것입니다. 우리의 경우, 이런 “동기적인 방식&quot;의 워크플로우 때문에 (클라이언트는 모든 요청에 즉각적으로 답을 받도록 설계되었기 때문에), 조금 다른 방법인 &lt;strong&gt;액터 샤딩&lt;/strong&gt; 패턴을 사용했습니다. 이 패턴은 배우기 쉬운 &lt;a href=&quot;https://akka.io/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Akka&lt;/a&gt; 라이브러리 덕분에 많이 유명해졌습니다. (현재 우리 팀에서는 Akka를 대신하여 비슷한 원칙을 따르는 &lt;a href=&quot;https://github.com/devsisters/shardcake&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;독자적인 구현체&lt;/a&gt;를 사용하고 있습니다.)&lt;/p&gt;
&lt;p&gt;액터는 기본적으로 큐 (”메일함&quot;이라고도 부릅니다.)에서 메시지를 순차적으로 수신하는 작은 프로세스입니다. 샤딩은 액터가 노드 그룹에 걸쳐 분산되어, 호출하려는 액터의 ID를 사용하여 메시지를 보내기만 하면 액터를 실행할 수 있습니다. 이 개념은 &lt;strong&gt;위치 투명성&lt;/strong&gt;이라고도 불리며, 액터가 어느 위치에서 실행되고 있는지를 모르더라도 메시지를 보낼 수 있습니다. 액터는 같은 노드에서 실행되고 있을 수도, 다른 노드에서 실행될 수도 있습니다.&lt;/p&gt;
&lt;p&gt;앞서 소개한 다음의 DSL은 메시지를 보내기 위해 두 개의 다른 노드가 아래의 코드를 동시에 호출할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;guild&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;guildId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt; JoinGuild&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;inviteId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;내부적으로 샤딩 시스템에서는 &lt;code&gt;guildId&lt;/code&gt; 에 지정한 값을 처리해야 할 액터가 어느 노드에서 실행되고 있는지 찾아서, 해당 노드에 페이로드 (&lt;code&gt;JoinGuild&lt;/code&gt;)를 포함시켜 메시지를 보낼 것입니다. 메시지를 받은 노드에서는 이 길드 ID를 처리할 액터에게 메시지들을 전달할 것입니다. 가장 먼저 메시지를 받을 액터에서는 작업을 처리한 후 성공 응답을 내보낼 것입니다. 처리가 끝난 후에 도착하는 메시지는 실패로 응답을 내보낼 것입니다.&lt;/p&gt;
&lt;p&gt;강력한 라이브러리 덕분에, 스칼라는 응답 시간에 영향을 주거나, 교착 상태 같은 문제를 일으킬 수 있는 분산 잠금 기술에 의존하지 않고서도 분산 처리를 유연하게 구현할 수 있고, 동시에 도착하는 페이로드를 정말 쉽게 처리할 수 있습니다. 그리고 Akka로 구현하든 ZIO로 구현하든, 액터 패턴은 우리의 삶을 다시 한 번 더 쉽게 만듭니다.&lt;/p&gt;
&lt;p&gt;몇 가지 실용적이고 간단한 예제를 통해 우리는 Scala가 게임 서버를 구축하기 위한 정말 흥미로운 선택인 것을 알 수 있었습니다. 강력한 타입 시스템과 함수형 프로그래밍의 모범 사례를 결합하여, 에러가 발생할 위험을 최소화하고, 비즈니스 논리에만 집중해서 개발할 수 있다는 사실도 알았습니다. 그리고, 스칼라의 풍부한 오픈 소스 라이브러리 생태계 덕분에 보일러 플레이트 코드를 줄이고, 모든 종류의 동시성 문제를 해결할 수 있습니다.&lt;/p&gt;
&lt;p&gt;지금까지의 이야기가 재미있으셨나요? 더 자세한 내용을 알고 싶으신가요? &lt;a href=&quot;https://www.notion.so/88bcb5f7325c4610a5e728b6b899e9ec&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;우리 팀과 함께 해요&lt;/a&gt;!&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[ 바삭한 신입들의 동시성 이야기 - Swift편]]></title><description><![CDATA[Swift 5.5에 도입된 async, await 문법의 등장 배경과 동작 원리를 알아봅니다.]]></description><link>https://tech.devsisters.com/posts/crunchy-concurrency-swift</link><guid isPermaLink="false">https://tech.devsisters.com/posts/crunchy-concurrency-swift</guid><pubDate>Wed, 17 Aug 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요! 마이쿠키런에서 팬플랫폼 iOS 개발을 하고 있는 이인애입니다.
벌써 팀에 합류한지 약 6개월이 되었는데요. 오늘은 신입 iOS 개발자가 Swift 5.5에 새롭게 등장한 동시성, 그중에서도 async/await 키워드의 동작에 대해 공부하면서 깨달았던 내용들을 공유해 보려고 합니다.&lt;/p&gt;
&lt;h2 id=&quot;swift-55-이전의-동시성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#swift-55-%EC%9D%B4%EC%A0%84%EC%9D%98-%EB%8F%99%EC%8B%9C%EC%84%B1&quot; aria-label=&quot;swift 55 이전의 동시성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Swift 5.5 이전의 동시성&lt;/h2&gt;
&lt;p&gt;Swift 5.5에서 새로운 동시성 모델을 제시하기 전까지 우리는 GCD와 completion handler를 사용해 비동기 프로그래밍을 해야 했습니다. GCD API로 비동기 작업을 처리하고, completion handler로 비동기가 끝나는 시점에 필요한 작업을 할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;기존에도 Swift에는 동시성 프로그래밍을 위한 방법들이 존재하고 있었습니다. 그런데 왜 또 다른 동시성 생태계(&lt;a href=&quot;https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Swift Concurrency&lt;/a&gt;)를 도입하고자 했을까요?&lt;/p&gt;
&lt;p&gt;그 배경을 이해하기 위해 &lt;em&gt;바삭한 쿠키🍪&lt;/em&gt;를 함께 구워보겠습니다. 쿠키 만드는 각 단계는 긴 시간이 소요되기 때문에 비동기로 동작한다고 가정하겠습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;도우 만들기&lt;/li&gt;
&lt;li&gt;도우 숙성시키기&lt;/li&gt;
&lt;li&gt;오븐에 굽기&lt;/li&gt;
&lt;li&gt;쿠키에 얼굴 그리기&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;먼저 completion handler를 사용해 구현한 코드입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;swift&quot;&gt;&lt;pre class=&quot;language-swift&quot;&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;makeCrunchyCookie&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;completion&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token attribute atrule&quot;&gt;@escaping&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Cookie&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Void&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    makeDough &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; dough &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// ✅ (1)&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;chillDough&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dough&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; dough&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; ripedDough &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// ✅ (2)&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;bakeCookie&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dough&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ripedDough&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; time&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; bakeTime&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; cookie &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// ✅ (3)&lt;/span&gt;
                &lt;span class=&quot;token keyword&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;drawFace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cookie&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; cookie&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; crunchyCookie &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// ✅ (4)&lt;/span&gt;
                    &lt;span class=&quot;token function&quot;&gt;completion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;crunchyCookie&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;자, 여기서 ‘*개발자의 실수로 인해 발생할 수 있는 오류’*가 발생할 가능성이 있기 때문에 쿠키를 굽는 개발자들이 주의해야 할 점이 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;에러 처리를 위해 모든 case에서 completion handler를 리턴했나요?&lt;/li&gt;
&lt;li&gt;self property에 접근할 때, 어떤 스레드에서 접근하게 될지 생각했나요?&lt;/li&gt;
&lt;li&gt;self property에서 retain cycle이 발생할 가능성은 없나요?&lt;/li&gt;
&lt;li&gt;etc&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;가장 먼저 눈에 띄는 것은 중첩되는 코드 블럭입니다. 들여쓰기가 많아지면 가독성이 떨어질 뿐 아니라, 코드 내부에서 분기 처리 시 &lt;strong&gt;completion handler를 호출하지 않는 실수&lt;/strong&gt;를 할 수도 있습니다.&lt;/p&gt;
&lt;p&gt;또 코드 내부에서 &lt;code&gt;self&lt;/code&gt; 키워드에 접근해야 한다면 &lt;strong&gt;retain cycle(참조 사이클) 발생 가능성&lt;/strong&gt;을 고려해야 합니다. 그렇다고 무분별한 &lt;code&gt;[weak self]&lt;/code&gt;의 사용은 런타임 오버헤드를 발생시킬 수도 있고, nil을 체크해야 하는 수고를 거쳐야 합니다. guard let 문법을 사용한다면 self가 nil 일 때 특정 코드들을 건너뛰게 되는 상황도 발생할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;✅ completion handler의 문제점
    ∙ 중첩되는 들여쓰기로 인한 가독성 저하
    ∙ 개발자의 실수로 인해 발생할 수 있는 오류
        ◦ 복잡한 에러 핸들링
        ◦ closure에서 self capture → retain cycle 발생 가능성&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;컴파일러는 이런 종류의 *‘개발자로 인해 발생할 수 있는 오류’*들을 감지할 수 없습니다. 그래서 문법적인 패턴을 도입해 보다 명확하고 안전하게 문제를 해결하고자 했습니다.&lt;/p&gt;
&lt;h2 id=&quot;async-await-문법의-도입&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#async-await-%EB%AC%B8%EB%B2%95%EC%9D%98-%EB%8F%84%EC%9E%85&quot; aria-label=&quot;async await 문법의 도입 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;async, await 문법의 도입&lt;/h2&gt;
&lt;p&gt;아래의 키워드를 사용해서 비동기 함수를 사용할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;async&lt;/code&gt;: 비동기 함수임을 나타낸다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;await&lt;/code&gt;: async 키워드가 표시된 메소드나 함수의 리턴을 기다린다.
즉, &lt;code&gt;async&lt;/code&gt; 함수는 비동기적으로 동작할 수 있고, &lt;code&gt;await&lt;/code&gt; 키워드를 사용해 비동기 함수의 결과를 대기할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;앞서 살펴본 예제를 async, await로 변경한 코드입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;swift&quot;&gt;&lt;pre class=&quot;language-swift&quot;&gt;&lt;code class=&quot;language-swift&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;makeCrunchyCookie&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Cookie&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; dough &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;makeDough&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// ✅ (1)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; ripedDough &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;chillDough&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dough&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; dough&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// ✅ (2)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; cookie &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;bakeCookie&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dough&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ripedDough&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; time&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; bakeTime&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// ✅ (3)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; crunchyCookie &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;drawFace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cookie&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; cookie&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// ✅ (4)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; crunchyCookie
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;completion handler가 사라지면서, 비동기 함수를 한 줄로 처리할 수 있어 훨씬 간결해 보입니다. 이와 동시에 retain cycle이 발생할 우려도 사라졌습니다. 또, 개발자가 실수로 try await 키워드를 빼먹은 경우에는 컴파일러가 오류를 알려줘 조금 더 안정적인 에러 핸들링이 가능합니다.&lt;/p&gt;
&lt;p&gt;이처럼 가독성이 더 좋아졌을 뿐 아니라 위에서 언급한 여러 문제를 해결하고 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;동작-과정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8F%99%EC%9E%91-%EA%B3%BC%EC%A0%95&quot; aria-label=&quot;동작 과정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;동작 과정&lt;/h2&gt;
&lt;p&gt;최근 새로운 프로그래밍 패러다임으로 자리 잡기 시작한 구조화된 동시성(&lt;a href=&quot;https://en.m.wikipedia.org/wiki/Structured_concurrency&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Structured Concurrency&lt;/a&gt;)이 Swift에도 도입되었습니다. Swift 5.5부터 도입된 구조화된 동시성은 async/await 키워드로서 제공됩니다.&lt;/p&gt;
&lt;p&gt;다른 언어에서도 Swift와 유사하게 구조화된 동시성이 async/await 키워드로 제공됩니다. async 키워드를 사용해서 비동기적으로 동작하는 함수를 만들 수 있는데, 이때 이 비동기 함수는 코루틴(&lt;a href=&quot;https://en.wikipedia.org/wiki/Coroutine&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Coroutine&lt;/a&gt;)으로 만들어집니다. Coroutine은 함수가 동작하는 도중 특정 시점에 suspend(일시정지)할 수 있고, resume(다시 재개)할 수 있게 합니다.&lt;/p&gt;
&lt;p&gt;다른 언어에서는 Coroutine을 이용해서 비동기 함수 매커니즘을 제공하고, Swift의 구조화된 동시성 또한 이와 유사하게 동작합니다.&lt;/p&gt;
&lt;p&gt;함수의 이름 뒤에 &lt;code&gt;async&lt;/code&gt; 키워드를 붙여 해당 함수가 비동기임을 나타낼 수 있고, 비동기 함수가 결과를 반환할 때까지 &lt;strong&gt;suspend&lt;/strong&gt;(일시정지) 될 수 있습니다. 이때 비동기 함수가 일시정지될 수 있는 지점을 알려주는 장치가 &lt;code&gt;await&lt;/code&gt; 키워드입니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;await&lt;/code&gt; 키워드는 호출할 함수의 앞에 붙여 &lt;strong&gt;suspension point&lt;/strong&gt; 임을 나타냅니다. 호출된 함수가 결과값 혹은 오류를 던진 이후, suspension point에서 &lt;strong&gt;resume&lt;/strong&gt;(실행을 재개)할 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;비동기-함수를-처리할-때-suspend--resume이-왜-필요하죠&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B9%84%EB%8F%99%EA%B8%B0-%ED%95%A8%EC%88%98%EB%A5%BC-%EC%B2%98%EB%A6%AC%ED%95%A0-%EB%95%8C-suspend--resume%EC%9D%B4-%EC%99%9C-%ED%95%84%EC%9A%94%ED%95%98%EC%A3%A0&quot; aria-label=&quot;비동기 함수를 처리할 때 suspend  resume이 왜 필요하죠 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;📌 비동기 함수를 처리할 때 suspend / resume이 왜 필요하죠?&lt;/h3&gt;
&lt;p&gt;Swift의 모든 비동기 함수는 여러 &lt;code&gt;task&lt;/code&gt;의 집합으로 이뤄져 있습니다. 비동기 함수가 다른 비동기 함수를 호출했을 때, 해당 호출 또한 task입니다. task는 동기 함수에서 스레드 내 함수 컨텍스트에 해당하는 &lt;code&gt;continuation&lt;/code&gt;을 포함합니다. continuation은 task가 suspend 되었을 때 발생하며, resume 될 때 이를 이용해 suspension point로 돌아갈 수 있습니다.&lt;/p&gt;
&lt;p&gt;해당 과정을 코드와 함께 살펴보겠습니다. 먼저 위의 예시에 등장한 비동기 함수들을 task로 나눠보겠습니다. 우리는 함수를 기다려야 하는 부분과, 비동기로 처리되어야 하는 코드의 일부분을 task로 나눌 수 있습니다. 시스템은 이 task들을 각각 어떤 스레드에서 동작할지 &lt;code&gt;executor&lt;/code&gt;(일종의 스레드 풀)를 통해 결정합니다.&lt;/p&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 26.38888888888889%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAAAsTAAALEwEAmpwYAAABFElEQVR42k2Q626CQBCFfZkWqFxcYAHljqBrFZVGe7FN7Pu/xNfVkNgfX87MJDM5cybO1KeMU6qmJisL0rykyHPWRc6qKNgWFXEckW1qZiLCNG2Ni2E4mJY74t1n1ovH5EkX7TzhOmz5uQxcDorvVcfv7pXhqOjfFJt9izp1tE2F6hqSPCHu5riOxPF8bE/gCP9xsJnHfPVLznr5rPVd1VxUw3nXcjgsGU5rjlo/TorrZ8+wb1gNJV1fUxULRBIQVgmWJZgY2rIUIW2aUeYp7bImzzMWkSQLJbEMiaKQWPeh9HUfkEiJG8xwNNb4tmGML09tbdcOCJwI14sQQYKvsaaC51tWOh/jpmP9bHr3uWk+cvvPH75YpiIV7hvnAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/4bbf4d95e804e6ed37f8a58c16cb7ed7/33ecc/make-crunchy-cookie-func.webp 360w,
/static/4bbf4d95e804e6ed37f8a58c16cb7ed7/263a4/make-crunchy-cookie-func.webp 480w,
/static/4bbf4d95e804e6ed37f8a58c16cb7ed7/0c8fb/make-crunchy-cookie-func.webp 640w,
/static/4bbf4d95e804e6ed37f8a58c16cb7ed7/521f7/make-crunchy-cookie-func.webp 860w,
/static/4bbf4d95e804e6ed37f8a58c16cb7ed7/cbd37/make-crunchy-cookie-func.webp 1200w,
/static/4bbf4d95e804e6ed37f8a58c16cb7ed7/548e7/make-crunchy-cookie-func.webp 2400w,
/static/4bbf4d95e804e6ed37f8a58c16cb7ed7/a5b37/make-crunchy-cookie-func.webp 3320w&quot; sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/4bbf4d95e804e6ed37f8a58c16cb7ed7/53918/make-crunchy-cookie-func.png 360w,
/static/4bbf4d95e804e6ed37f8a58c16cb7ed7/9aebd/make-crunchy-cookie-func.png 480w,
/static/4bbf4d95e804e6ed37f8a58c16cb7ed7/0f09e/make-crunchy-cookie-func.png 640w,
/static/4bbf4d95e804e6ed37f8a58c16cb7ed7/3e3fe/make-crunchy-cookie-func.png 860w,
/static/4bbf4d95e804e6ed37f8a58c16cb7ed7/64756/make-crunchy-cookie-func.png 1200w,
/static/4bbf4d95e804e6ed37f8a58c16cb7ed7/98e2c/make-crunchy-cookie-func.png 2400w,
/static/4bbf4d95e804e6ed37f8a58c16cb7ed7/353b7/make-crunchy-cookie-func.png 3320w&quot; sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/4bbf4d95e804e6ed37f8a58c16cb7ed7/98e2c/make-crunchy-cookie-func.png&quot; alt=&quot;make crunchy cookie func&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;p&gt;함수가 실행되는 과정에서 await 키워드를 만나면 해당 지점을 suspension point로 표시하게 됩니다.&lt;/p&gt;
&lt;p&gt;작업이 &lt;strong&gt;suspend&lt;/strong&gt;되면, task는 작업을 진행 중이던 &lt;strong&gt;스레드의 제어권을 포기&lt;/strong&gt;하게 됩니다. 이때 해당 스레드 제어권은 시스템이 가지게 되는데, 시스템은 이 스레드에서 다른 작업을 수행하다가 task의 수행이 필요하다고 느껴지는 적절한 시점에 스레드 제어권을 반환합니다.&lt;/p&gt;
&lt;p&gt;스레드 제어권은 다시 suspension point가 발생한 지점으로 돌아가 작업이 resume 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;✅ suspend → resume의 과정
    1. await 키워드를 만나면 suspension point로 지정하고 일시정지 (suspend)
    2. 스레드의 제어권을 시스템에게 넘겨줌
    3. 시스템이 다시 비동기 함수에게 스레드 제어권을 넘겨줌
    4. suspension point에서 작업 재개 (resume)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&quot;헉-그럼-이-task는-어떤-스레드에서-실행되는거죠&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%97%89-%EA%B7%B8%EB%9F%BC-%EC%9D%B4-task%EB%8A%94-%EC%96%B4%EB%96%A4-%EC%8A%A4%EB%A0%88%EB%93%9C%EC%97%90%EC%84%9C-%EC%8B%A4%ED%96%89%EB%90%98%EB%8A%94%EA%B1%B0%EC%A3%A0&quot; aria-label=&quot;헉 그럼 이 task는 어떤 스레드에서 실행되는거죠 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;헉 그럼 이 task는 어떤 스레드에서 실행되는거죠..?&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 600px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 50%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAKABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAgMA/8QAFgEBAQEAAAAAAAAAAAAAAAAAAQAC/9oADAMBAAIQAxAAAAEUgCvltn//xAAcEAABAwUAAAAAAAAAAAAAAAAAAQIDEBEhMUH/2gAIAQEAAQUCRxi2zkNP/8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPwE//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPwE//8QAGBAAAgMAAAAAAAAAAAAAAAAAARACETH/2gAIAQEABj8CN4HJ/wD/xAAbEAACAgMBAAAAAAAAAAAAAAABESExABBBkf/aAAgBAQABPyEc2RyzhLhITs+4CAYo65tzGv/aAAwDAQACAAMAAAAQb8//xAAVEQEBAAAAAAAAAAAAAAAAAAAQEf/aAAgBAwEBPxCH/8QAFREBAQAAAAAAAAAAAAAAAAAAEBH/2gAIAQIBAT8Qp//EABwQAQEAAwADAQAAAAAAAAAAAAERACExQVGBwf/aAAgBAQABPxC0IAoKiwAwSR7UUY6M4mvzCPIprDv5gX5gGRDDd0x0oe8//9k=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/a8c0949c5e6f09cb44a59ab488ed7420/33ecc/raccoon-man.webp 360w,
/static/a8c0949c5e6f09cb44a59ab488ed7420/263a4/raccoon-man.webp 480w,
/static/a8c0949c5e6f09cb44a59ab488ed7420/411c4/raccoon-man.webp 600w&quot;
              sizes=&quot;(max-width: 600px) 100vw, 600px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/a8c0949c5e6f09cb44a59ab488ed7420/543cd/raccoon-man.jpg 360w,
/static/a8c0949c5e6f09cb44a59ab488ed7420/a3e66/raccoon-man.jpg 480w,
/static/a8c0949c5e6f09cb44a59ab488ed7420/e224a/raccoon-man.jpg 600w&quot;
            sizes=&quot;(max-width: 600px) 100vw, 600px&quot;
            type=&quot;image/jpeg&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/a8c0949c5e6f09cb44a59ab488ed7420/e224a/raccoon-man.jpg&quot;
            alt=&quot;스레드는 너굴맨이 처리했으니 안심하라구!&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;스레드는 너굴맨이 처리했으니 안심하라구!&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;일반적으로 함수는 정해진 스레드에서 처리해야 합니다. 즉, 처리해야 할 작업이 많아지면 생성되는 스레드도 증가하게 됩니다. 그렇다면 스레드의 수가 많아지면 더 많이 처리할 수 있으니까 더 좋은 거 아닐까요?&lt;/p&gt;
&lt;p&gt;예상외로 그렇지는 않습니다. 스레드의 수가 지나치게 많이 생성되는 현상(thread explosion)이 발생합니다. 스레드의 수가 디바이스의 CPU 코어 수보다 많아지면 실제 용량을 초과해 요구하는 셈(overcommit)이 되어버립니다.&lt;/p&gt;
&lt;p&gt;코어의 수가 제한된 디바이스에서 스레드가 과도하게 생성될 경우 빈번한 컨텍스트 스위칭(context switching)으로 인해 스케줄링 오버헤드가 발생하거나, block된 스레드가 다시 실행되기를 기다리면서 가지고 있는 메모리 및 리소스 때문에 메모리 오버헤드가 발생해 성능을 저하시킬 가능성이 있습니다.&lt;/p&gt;
&lt;p&gt;또, 엄청난 양의 스레드를 개발자들이 직접 관리하는 것은 꽤나 복잡한 작업을 요구하게 될 것입니다.
&lt;br&gt;&lt;br/&gt;&lt;/p&gt;
&lt;p&gt;GCD를 사용할 때는 너무 많은 스레드들이 block 되지 않도록 제어하거나, 세마포어(semaphore)를 사용해 할당되는 스레드의 수를 제한하는 방법을 사용해야 했습니다. 이 점 또한 앞에서 언급한 ‘&lt;em&gt;개발자의 실수로 인해 발생할 수 있는 오류&lt;/em&gt;’를 발생시킬 가능성이 있기 때문에 언어적인 측면에서 안전하다고 판단되는 것은 아니었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 42.77777777777778%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAABYlAAAWJQFJUiTwAAABg0lEQVR42o2Ry07CQBSG20JLpZ06lAhWaEFFitFguW2AxLAQF94aEd/BRHBj4sIY486FijHBGOO7/k6nRiQR4+LLuf5n5swIpVIJ5XIZtVoN1WqV43ketliuXq+jWqnwumVZEAQBoihyOxNN00ATFGbShEHnkTApDMOAruvcBhBCIMvylFCMiFP8qAn4L6I4YWZf0klhpVdGseehwEjtuuxE6e/BsoRU1+X9xZMK8v4GTHsBcTUOQaVxZDoFuHsVrHbX0ei5yGdVOIsx2FYM0Ug4JMrWciwVdlpBzp5D7biI5R0Xm34Dme1VxOgcIlIkXFmSRKhRDVmT4GGwhPF1Du+3BYwuHaSTCmtiNzJjeLnK4+1mBR93a7g/SyNLNaiyzvUz31BiEEWBTvRf1w0+xyDG7Ceh1MHBwSMOD0ecI8Zpb4yT/vgr/4R+f8RtEPv+M4tfWfzMCHX7+w9IJHLhQELSaLUGjGFIe4hm65z552gzP6DTufj2A5rNoDbRBDEhi3zgJ/r39kEPOrUzAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/1194b6d12e0323de4a5e5df2d5e64508/33ecc/async-func-call.webp 360w,
/static/1194b6d12e0323de4a5e5df2d5e64508/263a4/async-func-call.webp 480w,
/static/1194b6d12e0323de4a5e5df2d5e64508/0c8fb/async-func-call.webp 640w,
/static/1194b6d12e0323de4a5e5df2d5e64508/521f7/async-func-call.webp 860w,
/static/1194b6d12e0323de4a5e5df2d5e64508/cbd37/async-func-call.webp 1200w,
/static/1194b6d12e0323de4a5e5df2d5e64508/548e7/async-func-call.webp 2400w,
/static/1194b6d12e0323de4a5e5df2d5e64508/6e85c/async-func-call.webp 3584w&quot;
              sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/1194b6d12e0323de4a5e5df2d5e64508/53918/async-func-call.png 360w,
/static/1194b6d12e0323de4a5e5df2d5e64508/9aebd/async-func-call.png 480w,
/static/1194b6d12e0323de4a5e5df2d5e64508/0f09e/async-func-call.png 640w,
/static/1194b6d12e0323de4a5e5df2d5e64508/3e3fe/async-func-call.png 860w,
/static/1194b6d12e0323de4a5e5df2d5e64508/64756/async-func-call.png 1200w,
/static/1194b6d12e0323de4a5e5df2d5e64508/98e2c/async-func-call.png 2400w,
/static/1194b6d12e0323de4a5e5df2d5e64508/ed6ae/async-func-call.png 3584w&quot;
            sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/1194b6d12e0323de4a5e5df2d5e64508/98e2c/async-func-call.png&quot;
            alt=&quot;Swift concurrency: Behind the scenes&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Swift concurrency: Behind the scenes&lt;/figcaption&gt;
  &lt;/figure&gt;
&lt;a href=&quot;https://developer.apple.com/videos/play/wwdc2021/10132?time=995&quot; target=&quot;_blank&quot;&gt;WWDC 2021&lt;/a&gt;에 따르면, 애플은 스레드 관리를 개발자에게 맡기기보다 시스템 자체에서 처리해 안전성을 보장하고자 했습니다. 그래서 시스템이 넘겨받은 작업의 우선순위와 실행하기에 적절한 스레드를 고려하여 제어권을 넘겨줍니다.&lt;/p&gt;
&lt;p&gt;즉, 함수가 suspend 되는 과정에는 추가적인 스레드의 생성 없이 작업할 수 있도록 executor가 task를 적절한 스레드로 배정하는 과정이 포함되었다고 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;이제 task는 시스템에게 넘겨받은 스레드 제어권으로 작업을 재개할 수 있습니다. 하지만 여기서 task가 재개되는 스레드는 이전과 같지 않을 수 있다는 점을 기억해야 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;✅ suspension point에서 변할 수 있는 것
    ∙ 스레드 제어권
    ∙ resume 되는 스레드
    ∙ suspend → resume 이후 앱의 상태&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;작업이-재개되는-스레드가-왜-이전과-같지-않을-수-있을까요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9E%91%EC%97%85%EC%9D%B4-%EC%9E%AC%EA%B0%9C%EB%90%98%EB%8A%94-%EC%8A%A4%EB%A0%88%EB%93%9C%EA%B0%80-%EC%99%9C-%EC%9D%B4%EC%A0%84%EA%B3%BC-%EA%B0%99%EC%A7%80-%EC%95%8A%EC%9D%84-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C%EC%9A%94&quot; aria-label=&quot;작업이 재개되는 스레드가 왜 이전과 같지 않을 수 있을까요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;📌 작업이 재개되는 스레드가 왜 이전과 같지 않을 수 있을까요?&lt;/h3&gt;
&lt;p&gt;이것을 알기 위해서는 비동기 task가 어떤 식으로 동작하는지 이해해야 합니다. &lt;strong&gt;continuation&lt;/strong&gt;이란, 특정 지점에서의 함수 실행 컨텍스트를 추적할 수 있는 객체입니다. 새로운 Swift 동시성 모델에서는 각 task에 대해서 스레드를 생성하는 대신 continuation을 할당합니다.&lt;/p&gt;
&lt;p&gt;일반적으로 실행 중인 프로세스 내에서 모든 스레드는 독립적인 stack 영역과 프로세스 내에서 공유되는 heap 영역을 가지고 있습니다. 이때 stack은 함수 호출의 상태를 저장하기 위해 사용됩니다. 지역 변수와 반환 주소값 등 함수 호출에 필요한 정보들을 함께 저장하고 있습니다.&lt;/p&gt;
&lt;img src=&quot;/dc3e74c8f69610f499d943d9406dc0cc/sync-stack.gif&quot;&gt;
&lt;p&gt;일반적으로 함수가 호출 및 반환될 때의 stack의 모습입니다. 함수가 호출되면 stack에 &lt;strong&gt;push&lt;/strong&gt;되고, 실행이 끝나 함수가 return 되면 &lt;strong&gt;pop&lt;/strong&gt;을 수행합니다. 기존에 동시성 프로그래밍을 구현하기 위해서는 여러 개의 스레드를 사용해 처리해야 했습니다. 이처럼 여러 개의 스레드 작업을 번갈아가며 처리할 때 컨텍스트 스위칭(context switching)이 발생합니다.&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 56.388888888888886%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABO0lEQVR42p2Tu07DMBSG83o8AI/AhhiZGOnCxNKNASEhIUTFAGIDwtKKiwR0KhWRaEpbcnHIzXUSx/lxQokYmqBwpCMd+5c/nZsVVFiWZRBCFJ7HuYlMFJ4hq3oGpUoIwxC2bYMQAs554YQ4ILZTaI2Bvu/DMAyYpokkSQrgzJhg8qFLzfsfMIdZMktOKZhH0B13cD06gP45+G7LktLrgTLDAihjRj2ca7s4etmCRh4XQNE0Q1mybYEHARjzcTJoYa+/jqF13xwYBhS2SeQQXKQhBZ27aN9uoHWziqd3tZz6n8CfvoydIdS3Q3RHp4iIKYE+di7WsHm8ggftqjnQDHTcTc/wPLsEjxiihKHTa2Nf3cbrtL/Y1QYl58vskgDRPC7vIhYj9BjqTKkTU8FlWemvc4qEx+XPWQb8Amw4UrMVwIxuAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/5a4b02d279cb3864e54fd362dbbb46ac/33ecc/async-stack-heap.webp 360w,
/static/5a4b02d279cb3864e54fd362dbbb46ac/263a4/async-stack-heap.webp 480w,
/static/5a4b02d279cb3864e54fd362dbbb46ac/0c8fb/async-stack-heap.webp 640w,
/static/5a4b02d279cb3864e54fd362dbbb46ac/521f7/async-stack-heap.webp 860w,
/static/5a4b02d279cb3864e54fd362dbbb46ac/cbd37/async-stack-heap.webp 1200w,
/static/5a4b02d279cb3864e54fd362dbbb46ac/0b34d/async-stack-heap.webp 1920w&quot;
              sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/5a4b02d279cb3864e54fd362dbbb46ac/53918/async-stack-heap.png 360w,
/static/5a4b02d279cb3864e54fd362dbbb46ac/9aebd/async-stack-heap.png 480w,
/static/5a4b02d279cb3864e54fd362dbbb46ac/0f09e/async-stack-heap.png 640w,
/static/5a4b02d279cb3864e54fd362dbbb46ac/3e3fe/async-stack-heap.png 860w,
/static/5a4b02d279cb3864e54fd362dbbb46ac/64756/async-stack-heap.png 1200w,
/static/5a4b02d279cb3864e54fd362dbbb46ac/ac7a9/async-stack-heap.png 1920w&quot;
            sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/5a4b02d279cb3864e54fd362dbbb46ac/ac7a9/async-stack-heap.png&quot;
            alt=&quot;async stack heap&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;그러나 Swift Concurrency에 도입된 코루틴에서는 비동기 함수의 실행을 stack과 heap에서 관리합니다. &lt;strong&gt;stack&lt;/strong&gt;에는 비동기 함수를 실행할 때 사용되지 않는 지역변수들을 저장합니다. 추가로 &lt;strong&gt;heap&lt;/strong&gt;에는 suspension point에서 실행하는데 필요한 함수 컨텍스트들을 저장합니다. 이것을 &lt;strong&gt;continuation&lt;/strong&gt;이라고 부르며, 이를 통해 일시정지된 함수의 상태를 추적해 어디서부터 재개할지 알 수 있습니다.&lt;/p&gt;
&lt;p&gt;continuation은 heap에 저장되기 때문에 스레드 간의 함수 컨텍스트를 공유할 수 있습니다. 바삭한 쿠키를 굽는 과정에서 &lt;code&gt;makeDough&lt;/code&gt; 함수가 suspend 된 상황을 가정해 보겠습니다.&lt;/p&gt;
&lt;p&gt;먼저 미래에 사용될 가능성이 있는 변수들이 continuation의 형태로 heap에 저장됩니다. suspend 되었던 함수가 resume 되면, stack의 최상단 frame이 해당 함수 frame으로 교체됩니다. 이미 heap에 함수 컨텍스트가 저장되어 있기 때문에 새로운 stack frame을 생성하지 않고 교체만으로도 동작할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/dec3494b31b014fb885835eb06e7857c/async-continuation.gif&quot; alt=&quot;suspend - resume의 진행 과정&quot;&gt;&lt;/p&gt;
&lt;p&gt;작업의 교체만 이뤄지면 이미 생성된 스레드를 재사용하는 것도 가능합니다. 비동기 함수가 어떤 스레드에서 suspend 되었는지와 관계없이 가장 효율적으로 실행될 수 있는 스레드에서 함수 컨텍스트를 불러와 resume 할 수 있습니다. 또, 이러한 과정은 컨텍스트 스위칭 없이 함수 호출 만으로도 작업의 변경이 가능해 스케줄링 오버헤드를 줄일 수 있는 이점이 있습니다.&lt;/p&gt;
&lt;p&gt;그래서 쿠키를 굽기 위해 비동기 함수 &lt;code&gt;makeDough&lt;/code&gt;를 실행하다가(suspend), 손을 씻어야 할 때가 오면 비동기 함수 &lt;code&gt;washHand&lt;/code&gt;를 실행(resume)할 수 있는 것입니다. 이 말은 곧, 비동기 함수를 실행하다가 UI 업데이트 같은 중요한 작업을 진행할 수 있게 해준다는 뜻입니다.&lt;/p&gt;
&lt;p&gt;비동기 함수가 suspend 되면 해당 스레드 제어권은 시스템이 가지고 있기 때문에, 시스템 판단하에 적절한 스레드를 찾아서 task에게 제어권을 넘겨줍니다. 여기서 말하는 적절한 스레드는 ‘원래 작업 중이던 스레드’ 혹은 ‘작업이 다 완료되어 쉬고 있는 다른 스레드’가 될 수 있습니다. 따라서 작업이 resume 되는 스레드가 같지 않을 수 있다는 특징이 여기서 발생하게 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;✅ continuation 특징
    ∙ 컨텍스트 스위칭을 함수 호출로 대체 → 스케줄링 오버헤드 감소
    ∙ 추가적인 스레드 생성 없이 스레드 재사용 → thread explosion 방지&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;요약&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9A%94%EC%95%BD&quot; aria-label=&quot;요약 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;요약&lt;/h2&gt;
&lt;p&gt;지금까지 Swift에서 async, await가 어떤 원리로 비동기 처리를 하고 있는지 살펴봤습니다. coroutine model을 기반으로 한 Swift Concurrency는 함수의 실행이 스레드에 국한되지 않는 동시성 생태계를 제공하고 있습니다. 요약하자면, Swift 5.5에 반영된 async/await 문법은 컴파일 타임과 런타임에 안전성을 보장하면서 한 번 이상 suspend, resume 될 수 있음을 의미합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;✅ Swift Concurrency에 적용된  변화
    📌 언어적 개선
        ∙ completion handler 제거로 인한 가독성 증가
        ∙ self 참조 사이클이 생길 우려 제거
        ∙ 에러 처리 용이
    📌 성능적 개선
        ∙ 시스템이 스레드 관리
        ∙ continuation으로 스레드 증가 방지
        ∙ 컨텍스트 스위칭을 함수 호출로 대체&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;swift-concurreny의-미래&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#swift-concurreny%EC%9D%98-%EB%AF%B8%EB%9E%98&quot; aria-label=&quot;swift concurreny의 미래 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Swift Concurreny의 미래&lt;/h2&gt;
&lt;p&gt;한편, &lt;a href=&quot;https://developer.apple.com/videos/wwdc2021/&quot; target=&quot;_blank&quot;&gt;WWDC&lt;/a&gt;에서 애플이 소개한 안전한 동시성 생태계의 &lt;a href=&quot;https://github.com/apple/swift/blob/71aa4c983221f3f890163c55c47d8443e05f83c7/docs/ConcurrencyRoadmap.md&quot; target=&quot;_blank&quot;&gt;최종 목표&lt;/a&gt;는 아래와 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;편리하고 명확한 비동기 프로그래밍 환경 제공&lt;/li&gt;
&lt;li&gt;Swift 개발자들이 따를 수 있는 표준 언어와 기술 제공&lt;/li&gt;
&lt;li&gt;컴파일 타임에 비동기 코드의 성능 향상&lt;/li&gt;
&lt;li&gt;데이터 레이스(data race)와 데드락(deadlock)의 제거&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;수차례의 release를 통해 이러한 목표를 달성할 예정인데, 이는 크게 두 단계로 나눌 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;✅ Swift Concurrency의 2단계
    1. async, await 문법과 actor 타입의 등장
    2. full actor isolation (독립성)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;첫 번째 단계&lt;/strong&gt;는 async/await API를 도입해 언어적, 성능적 관점에서의 개선을 이뤄냅니다. 이 과정에서 발생 가능한 data race를 줄이기 위한 방식으로 actor 타입을 제안합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;두 번째 단계&lt;/strong&gt;는 data race를 줄이는 것이 아닌, 완전한 제거를 목표로 full actor isolation 한 생태계를 조성하는 것입니다. 이를 위한 다양한 기능들도 함께 제공하는 것이 목표입니다.&lt;/p&gt;
&lt;p&gt;Swift 5.6까지 release된 현재 시점에서 살펴본 Swift Concurrency에는 첫 번째 단계가 충분히 반영되었다고 느낄 만큼 큰 변화를 체감할 수 있었습니다. 또, 여러 공식 문서를 통해 Swift가 얼마나 &lt;a href=&quot;https://forums.swift.org/t/concurrency-in-swift-5-and-6/49337&quot; target=&quot;_blank&quot;&gt;safe-by-default&lt;/a&gt;함을 중시하고 있는지 옅볼 수 있는 기회였습니다.&lt;/p&gt;
&lt;p&gt;이번 글에서는 async/await의 동작만 살펴봤지만, data race를 회피할 수 있는 동시성 프로그래밍을 위한 actor 타입과 task의 우선순위와 취소 기능을 위해 Structured Concurrency에 도입된 API 등 추가로 이해해야 할 개념이 많습니다.&lt;/p&gt;
&lt;p&gt;&lt;br&gt;&lt;/br&gt;
아직 Swift 동시성 모델 없이 개발된 코드들이 많이 존재합니다. 하지만 시간이 지남에 따라 Swift의 동시성 모델의 제약조건에 맞게 바꿀 일이 생길 것이라고 생각합니다. 마이쿠키런 iOS 팀에서는 이런 변화에 대응하기 위한 기술 스터디를 진행하며 쿠키런 팬들을 위한 플랫폼 서비스를 만들고 있습니다.&lt;/p&gt;
&lt;p&gt;이 글을 흥미롭게 읽으셨거나, 이미지 처리 혹은 서비스 개발에 관심이 많으신 iOS 개발자님이 계시다면 지금 당장 마이쿠키런의 n번째 바삭한 쿠키가 되어주세요! 🍪&lt;/p&gt;
&lt;p&gt;긴 글 읽어주셔서 감사합니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[9가지 프로그래밍 언어로 배우는 개념: 2편 - 다형성]]></title><description><![CDATA[프로그래밍 언어들을 비교해가며 다형성을 알아보고 더 나은 코드를 작성하는 방법을 고민해봅니다.]]></description><link>https://tech.devsisters.com/posts/programming-languages-2-polymorphism</link><guid isPermaLink="false">https://tech.devsisters.com/posts/programming-languages-2-polymorphism</guid><pubDate>Tue, 09 Aug 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;프로그래밍-언어-시리즈&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%EC%96%B8%EC%96%B4-%EC%8B%9C%EB%A6%AC%EC%A6%88&quot; aria-label=&quot;프로그래밍 언어 시리즈 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;프로그래밍 언어 시리즈&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;/posts/programming-languages-1-type-theory&quot;&gt;9가지 프로그래밍 언어로 배우는 개념: 1편 - 타입 이론&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;9가지 프로그래밍 언어로 배우는 개념: 2편 - 다형성&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/programming-languages-3-metaprogramming&quot;&gt;9가지 프로그래밍 언어로 배우는 개념: 3편 - 메타프로그래밍&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/programming-languages-4-high-level-languages-and-dynamically-typed-languages&quot;&gt;9가지 프로그래밍 언어로 배우는 개념: 4편 - 하이 레벨 언어와 동적 타입 언어&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/programming-languages-5-concurrent-programming&quot;&gt;9가지 프로그래밍 언어로 배우는 개념: 5편 - 동시성 프로그래밍&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;서론&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%84%9C%EB%A1%A0&quot; aria-label=&quot;서론 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;서론&lt;/h2&gt;
&lt;p&gt;안녕하세요, 킹덤 서버 소프트웨어 엔지니어 지민규입니다. 프로그래밍에 있어 추상화만큼이나 중요한 것은 다형성입니다. 다형성으로 인해 다양한 데이터 타입과 오브젝트의 재사용이 가능해지며 필요한 코드의 양을 획기적으로 줄입니다. 다형성을 이룩하는 방법에는 여러가지가 있으며 흔히 객체 지향 프로그래밍(Object-oriented Programming)의 특징으로 다형성이 꼽히곤 하지만 사실 절차적 프로그래밍(Procedural Programming) 언어이든 함수형 프로그래밍(Functional Programming) 언어이든 다형성을 어떠한 방식으로든 지원하는 경우가 많습니다. 이번 글에서는 언어 기능으로서의 다형성들을 나열하고 비교합니다.&lt;/p&gt;
&lt;h2 id=&quot;오버로딩&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%A4%EB%B2%84%EB%A1%9C%EB%94%A9&quot; aria-label=&quot;오버로딩 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;오버로딩&lt;/h2&gt;
&lt;p&gt;**오버로딩(Overloading)**은 같은 함수명에 다른 입출력 타입들을 가진 함수를 정의하여 하나의 함수 이름으로 묶을 수 있는 기능입니다. 프로그래밍에서는 이름이 추상화의 기초 요소라는 점을 생각해보면 오버로딩은 다형성 기법이라기보다 추상화 기법에 가깝다고도 생각할 수 있습니다. 두 숫자를 받아서 합을 출력하는 함수를 &lt;code&gt;int&lt;/code&gt;와 &lt;code&gt;float&lt;/code&gt; 버전으로 구현해봅시다. C의 경우 오버로딩이 없기 때문에 서로 다른 함수 이름으로 구현해야 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;c&quot;&gt;&lt;pre class=&quot;language-c&quot;&gt;&lt;code class=&quot;language-c&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;addInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;addFloat&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;float&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;float&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;C++에서는 오버로딩이 추가되었기 때문에 다음과 같이 처리가 가능합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// A함수: int끼리 add 하는 함수&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// B함수: float끼리 add 하는 함수&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;float&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;float&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;       &lt;span class=&quot;token comment&quot;&gt;// 여기서는 int끼리 add 하는 A함수를 쓴다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;float&lt;/span&gt; b &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1.0f&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1.0f&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 여기서는 float끼리 add 하는 B함수를 쓴다.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;하지만 오버로딩은 남용될 가능성이 있습니다. 함수 간의 동작 차이가 클 경우 함수가 어떤 일을 할지 예측하기 어려워지며 직관성을 떨어트리고 실수로 잘못된 함수를 부를 가능성이 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C/C++ 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getGrade&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; score&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getGrade&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;string gradeString&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;toInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;trim&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;   &lt;span class=&quot;token comment&quot;&gt;// 앞뒤 공백 삭제 후 int로 파싱&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; b &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getGrade&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getScore&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 과연 어느 함수가 호출될까?&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Go와 Rust는 오버로딩이 가져오는 이점보다 위험성이 더 높다고 판단하여 오버로딩을 지원하지 않습니다.&lt;/p&gt;
&lt;p&gt;오버로딩과 유사한 기능으로는 인수 기본 값 기능입니다. 인수 기본 값이란 함수를 호출할 때 인수의 값을 지정하지 않았을 경우, 대신 기본 값을 넣어주는 기능입니다. 인수 기본 값은 하나의 함수 블록을 사용하므로 오버로딩에 비해 상대적으로 안전하나, 여전히 잘못 사용하면 버그를 가져옵니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// score의 기본값은 100으로 설정한다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getGrade&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; score &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; score &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getScore&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; grade &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getGrade&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 실수로 score 변수를 사용하지 않았다!&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;인터페이스&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4&quot; aria-label=&quot;인터페이스 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;인터페이스&lt;/h2&gt;
&lt;p&gt;**인터페이스(Interface)**는 대부분의 프로그래밍 언어가 어떠한 형태로든 지원하는 다형성 기법입니다. 하지만 언어마다 걸린 제약과 기능이 조금씩 다릅니다. 과거 Java, C#의 인터페이스에는 추상 메소드만을 포함할 수 있었으나, 최근에는 인터페이스 메소드에 구현을 포함할 수 있게 되었습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Java 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Food&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getCalories&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getWeight&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getCaloriesPerWeight&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getCalories&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getWeight&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;인터페이스를 이용하면 다음과 같은 방식으로 다형성을 이룰 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Java 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Food&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getCalories&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Pizza&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Food&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; sliceCount&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 피자 조각 개수&lt;/span&gt;

  &lt;span class=&quot;token annotation punctuation&quot;&gt;@Override&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getCalories&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; sliceCount &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;300&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Burger&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Food&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;BurgerType&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;CHEESE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;BACON_CHEESE&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token class-name&quot;&gt;BurgerType&lt;/span&gt; burgerType&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;  &lt;span class=&quot;token comment&quot;&gt;// 버거의 종류&lt;/span&gt;

  &lt;span class=&quot;token annotation punctuation&quot;&gt;@Override&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getCalories&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;burgerType&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;CHEESE&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;700&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;BACON_CHEESE&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;900&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token class-name&quot;&gt;Food&lt;/span&gt; pizza &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;cookPizza&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;Food&lt;/span&gt; burger &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;cookBurger&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 하나의 Food 타입으로 두 가지 다른 동작을 한다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; pizzaCalories &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; pizza&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getCalories&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; burgerCalories &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; burger&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getCalories&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이러한 합 타입 데이터에 대한 인터페이스는 &lt;a href=&quot;/posts/programming-languages-1-type-theory&quot;&gt;타입 이론 포스트&lt;/a&gt;에서도 이야기했듯 완전 패턴 매칭으로 대체할 수 있습니다. 완전 패턴 매칭을 이용하면 더 직관적이고 문맥에 맞는 코드를 구현할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;rust&quot;&gt;&lt;pre class=&quot;language-rust&quot;&gt;&lt;code class=&quot;language-rust&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Scala 코드&lt;/span&gt;
sealed &lt;span class=&quot;token keyword&quot;&gt;trait&lt;/span&gt; &lt;span class=&quot;token type-definition class-name&quot;&gt;BurgerType&lt;/span&gt;
case object &lt;span class=&quot;token class-name&quot;&gt;Cheese&lt;/span&gt; extends &lt;span class=&quot;token class-name&quot;&gt;BurgerType&lt;/span&gt;
case object &lt;span class=&quot;token class-name&quot;&gt;BaconCheese&lt;/span&gt; extends &lt;span class=&quot;token class-name&quot;&gt;BurgerType&lt;/span&gt;

sealed &lt;span class=&quot;token keyword&quot;&gt;trait&lt;/span&gt; &lt;span class=&quot;token type-definition class-name&quot;&gt;Food&lt;/span&gt;
case class &lt;span class=&quot;token class-name&quot;&gt;Pizza&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pieceCount&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; extends &lt;span class=&quot;token class-name&quot;&gt;Food&lt;/span&gt;
case class &lt;span class=&quot;token class-name&quot;&gt;Burger&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;burgerType&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;BurgerType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; extends &lt;span class=&quot;token class-name&quot;&gt;Food&lt;/span&gt;

def &lt;span class=&quot;token function&quot;&gt;getCalories&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;food&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Food&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Int&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; food &lt;span class=&quot;token keyword&quot;&gt;match&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  case &lt;span class=&quot;token class-name&quot;&gt;Pizza&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pieceCount&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;  &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
    pieceCount &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;300&lt;/span&gt;
  case &lt;span class=&quot;token class-name&quot;&gt;Burger&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;burgerType&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
    burgerType &lt;span class=&quot;token keyword&quot;&gt;match&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      case &lt;span class=&quot;token class-name&quot;&gt;Cheese&lt;/span&gt;      &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;700&lt;/span&gt;
      case &lt;span class=&quot;token class-name&quot;&gt;BaconCheese&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;800&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

val food&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Food&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getPizza&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
val calories&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Int&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getCalories&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;food&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Rust 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;pub&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;token type-definition class-name&quot;&gt;BurgerType&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token class-name&quot;&gt;Cheese&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token class-name&quot;&gt;BaconCheese&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;pub&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;token type-definition class-name&quot;&gt;Food&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token class-name&quot;&gt;Pizza&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; piece_count&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;i32&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token class-name&quot;&gt;Burger&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; burger_type&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;BurgerType&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;pub&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;get_calories&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;food&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Food&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;i32&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;match&lt;/span&gt; food &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token class-name&quot;&gt;Food&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Pizza&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; piece_count &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;  &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; piece_count &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;300&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token class-name&quot;&gt;Food&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Burger&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; burger_type &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;match&lt;/span&gt; burger_type &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token class-name&quot;&gt;BurgerType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Cheese&lt;/span&gt;      &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;700&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token class-name&quot;&gt;BurgerType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;BaconCheese&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;800&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; food&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Food&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;get_pizza&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; calories&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;i32&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;get_calories&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;food&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;제네릭&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%9C%EB%84%A4%EB%A6%AD&quot; aria-label=&quot;제네릭 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;제네릭&lt;/h2&gt;
&lt;p&gt;**제네릭(Generics)**은 함수나 타입을 선언할 때 고정된 타입 뿐만 아니라 가변적인 타입을 사용할 수 있게 해줍니다. 어떤 타입이 사용되는지는 함수를 호출하거나 타입의 인스턴스를 생성할 때 결정됩니다. 제네릭 타입은 특히 자료 구조 타입에서 자주 쓰입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Java 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ArrayList&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; elementData&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; size&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;boolean&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt;
    elementData&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;size&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token class-name&quot;&gt;ArrayList&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Integer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; intList &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ArrayList&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Integer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;ArrayList&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; stringList &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ArrayList&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// ArrayList&amp;lt;Integer&gt;의 add는 Integer를 받는다&lt;/span&gt;
intList&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
intList&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
intList&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;15&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// ArrayList&amp;lt;String&gt;의 add는 String를 받는다&lt;/span&gt;
stringList&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;one&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
stringList&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;two&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
stringList&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;three&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그러나 제네릭 함수는 Java나 C#과 같은 상속 기반의 객체 지향 프로그래밍 언어에서는 상대적으로 적게 쓰입니다. 상속을 위시로 한 다형성은 데이터 타입을 그대로 인수로 전달하는 경우보다 인터페이스나 추상 클래스를 사용하는 경우가 많습니다. 다만 입출력 타입에 제네릭을 사용하고 있다면 제네릭 함수는 필수적입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Java 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// value를 size 만큼 채워서 ArrayList를 출력하는 함수&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ArrayList&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fill&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; size&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token class-name&quot;&gt;ArrayList&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; ret &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ArrayList&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; size&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    ret&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; ret&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// a와 b를 비교해서 더 높은 것을 출력하는 함수&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// T는 Comparable&amp;lt;T&gt; 인터페이스를 구현하고 있어야 한다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Comparable&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;max&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;compareTo&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;b&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;C++의 제네릭은 **템플릿(Template)**이라고 부릅니다. 템플릿은 다른 언어의 제네릭과 비교하면 특이한 점이 많은데 타입 인수에 들어간 타입이 마치 틀로 찍어낸 듯이 대입되어 함수가 구현된 것과 같습니다. 임의의 타입 2개를 받아서 더하는 &lt;code&gt;add&lt;/code&gt; 함수를 구현할 때 템플릿을 이용하면 인터페이스 없이, 하나의 함수로 구현할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;typename&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
T &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;T a&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; T b&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;          &lt;span class=&quot;token comment&quot;&gt;// 여기서는 T가 int이다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;float&lt;/span&gt; b &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;float&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;7.5f&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2.0f&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 여기서는 T가 float이다.&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// T 위치에 지정된 타입을 치환하여 따로 구현한 것과 동일하다.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// int add(int a, int b) {&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//   return a + b;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// }&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// float add(float a, float b) {&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;//   return a + b;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// }&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;C++의 템플릿은 필드 접근도 일반화시킬 수 있습니다. 심지어 필드가 서로 다른 타입이어도 상관없습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;typename&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;printValue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;T a&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  cout &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; endl&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;A&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
 &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;B&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
 &lt;span class=&quot;token keyword&quot;&gt;float&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

A a &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
B b &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10.5f&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;printValue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;printValue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;b&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이처럼 C++의 템플릿은 C/C++의 매크로와 필적할 정도로의 강력함을 가지고 있습니다. 그러나 단점도 많은데 그중 하나가 Variance가 잘 적용되지 않는다는 점입니다.&lt;/p&gt;
&lt;h2 id=&quot;variance&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#variance&quot; aria-label=&quot;variance permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Variance&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Variance&lt;/strong&gt;는 제네릭 타입간의 자연스러운 타입 캐스팅을 지원해주는 기능입니다. 제네릭 타입을 사용할 때 간혹 내부 &lt;code&gt;T&lt;/code&gt; 타입을 바꿔줘야 원활한 경우가 있는데 구현에 따라 캐스팅이 안전한 타입이 있고 캐스팅이 안전하지 않은 타입들이 있습니다. Variance가 있으면 이러한 타입 캐스팅이 유효한지 컴파일 타임에 검증해줄 수 있습니다.&lt;/p&gt;
&lt;p&gt;Variance는 특이하게도 각각의 프로그래밍 언어들이 서로 다른 방식으로 구현하고 있습니다. C++에서는 템플릿 특성상 Variance를 지원하지 않으며, C#, Kotlin은 인터페이스에서만 Variance를 허용하고 있고 Typescript에서는 모든 제네릭 타입에 Variance를 활용할 수 있습니다. Rust의 경우 로우 레벨 언어 특성상 Variance를 적용할 때의 &lt;a href=&quot;https://doc.rust-lang.org/nomicon/subtyping.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;세세한 규칙&lt;/a&gt;들이 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C# 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 인터페이스에서만 활용 가능하다&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;IEnumerable&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;string&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; stringList &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;string&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;IEnumerable&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;object&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; objectList &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; stringList&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Kotlin 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 인터페이스에서 활용 가능하다&lt;/span&gt;
val intList&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Integer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;listof&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
val objectList&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; intList

&lt;span class=&quot;token comment&quot;&gt;// Scala 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 자유롭게 가능하다&lt;/span&gt;
val intList&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
val anyList&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; intList

&lt;span class=&quot;token comment&quot;&gt;// Typescript 코드&lt;/span&gt;
let intArray&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;number&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
let anyArray&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;any&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; intArray

&lt;span class=&quot;token comment&quot;&gt;// Rust 코드&lt;/span&gt;
let i32Box&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Box&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;i32&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Box&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
let anyBox&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Box&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;dyn &lt;span class=&quot;token class-name&quot;&gt;Any&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; i32Box&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;위 예시에서는 &lt;code&gt;Integer&lt;/code&gt;가 &lt;code&gt;Object&lt;/code&gt;가 되는 예시를 들었는데 반대로 &lt;code&gt;Object&lt;/code&gt;가 &lt;code&gt;Integer&lt;/code&gt;가 되는 것만 허용되는 경우도 있습니다. C#의 &lt;code&gt;IComparer&amp;#x3C;T&gt;&lt;/code&gt;가 그 예시입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;csharp&quot;&gt;&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code class=&quot;language-csharp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C# 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// C#의 IComparer&amp;lt;T&gt;의 대략적인 구현&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;IComparer&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; T&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token return-type class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Compare&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt; x&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt; y&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token class-name&quot;&gt;IComparer&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; objectComparer&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// stringComparer에 objectComparer를 대입할 수 있다.&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;IComparer&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; stringComparer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; objectComparer&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Variance는 기본적으로 **업캐스팅(Upcasting)**을 기반으로 하고 있습니다. 업캐스팅이란 타입의 계층 구조에서 하위 계층에 존재하는 타입은 상위 계층으로 아무 위험성 없이 캐스팅 될 수 있다는 것을 가리킵니다. C#에서 &lt;code&gt;string&lt;/code&gt;은 &lt;code&gt;object&lt;/code&gt;를 상속 받고 있기 때문에 &lt;code&gt;object&lt;/code&gt;에 대입할 수 있습니다&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;csharp&quot;&gt;&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code class=&quot;language-csharp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C# 코드&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;string&lt;/span&gt;&lt;/span&gt; str &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Hi&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;object&lt;/span&gt;&lt;/span&gt; obj &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; str&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이는 곧 &lt;code&gt;String&lt;/code&gt;를 출력하는 함수는 곧 &lt;code&gt;Object&lt;/code&gt;를 출력하는 함수로 취급해도 괜찮다는 것을 의미합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;csharp&quot;&gt;&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code class=&quot;language-csharp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C# 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// int를 string으로 변환시키는 익명 함수&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;Func&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; toString &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; x &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; x&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;ToString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 출력 타입을 object로 취급해도 괜찮다&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;Func&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; func &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; toString&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 어차피 둘 다 출력을 object 타입에 대입시킬 수 있다&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;object&lt;/span&gt;&lt;/span&gt; toStringOut &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;square&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;object&lt;/span&gt;&lt;/span&gt; funcOut &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;func&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이것을 인터페이스의 맥락으로 가져오면 다음과 같습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;csharp&quot;&gt;&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code class=&quot;language-csharp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C# 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;IGenerator&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;out&lt;/span&gt; T&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token return-type class-name&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Generate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// string를 생성하는 인터페이스&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;StringGenerator&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token type-list&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;IGenerator&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token return-type class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;string&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Generate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Hi&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// int generate() 메소드는 Object generate()로 변환이 가능하다&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;IGenerator&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; stringGenerator &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token constructor-invocation class-name&quot;&gt;StringGenerator&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;IGenerator&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; objectGenerator &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; stringGenerator&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 하위 타입 &lt;code&gt;T&lt;/code&gt;가 상위 타입으로 변환될 수 있는 경우를 &lt;strong&gt;Covariant&lt;/strong&gt;이라고 부릅니다.&lt;/p&gt;
&lt;p&gt;그렇다면 반대의 경우는 어떨까요?&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;csharp&quot;&gt;&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code class=&quot;language-csharp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C# 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// object를 string으로 변환시키는 익명 함수&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;Func&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; toString &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; x &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; x&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;ToString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 입력 타입을 string으로 취급해도 괜찮다&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;Func&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; func &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; toString&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 어차피 둘 다 입력으로 string를 받을 수 있다&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;string&lt;/span&gt;&lt;/span&gt; toStringOut &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;toString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;hello&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;string&lt;/span&gt;&lt;/span&gt; funcOut &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;func&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;hi&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 인터페이스에서는...&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;IConsumer&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; T&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token return-type class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;consume&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt; v&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// object를 입력받는 인터페이스&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ObjectConsumer&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token type-list&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;IConsumer&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token return-type class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;consume&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;object&lt;/span&gt;&lt;/span&gt; v&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    Console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;WriteLine&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;v&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;ToString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// void consume(object value) 메소드는&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// void consume(string value)로 변환이 가능하다.&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;IConsumer&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; objectConsumer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token constructor-invocation class-name&quot;&gt;ObjectConsumer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;IConsumer&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; stringConsumer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; objectConsumer&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;상위 타입&lt;code&gt;T&lt;/code&gt;가 하위 타입으로 변환될 수 있는 경우를 &lt;strong&gt;Contravariant&lt;/strong&gt;이라고 부릅니다.&lt;/p&gt;
&lt;p&gt;Variance의 방향은 제네릭을 어떤 방식으로 활용하고 있는지에 따라 갈립니다. &lt;code&gt;T&lt;/code&gt;를 출력하는 메소드가 있다면 Covariant, 입력으로 &lt;code&gt;T&lt;/code&gt;를 받는 메소드가 있다면 Contravariant가 되고 타입의 필드에서 &lt;code&gt;T&lt;/code&gt;를 어떤 방식으로 활용하고 있는지도 중요합니다.&lt;/p&gt;
&lt;p&gt;그렇다면 Java의 &lt;code&gt;ArrayList&amp;#x3C;T&gt;&lt;/code&gt; 같이 &lt;code&gt;T&lt;/code&gt;를 입력 받기도 하고 &lt;code&gt;T&lt;/code&gt;를 출력하기도 하는 타입은 어느 것일까요? 정답은 “둘 다 아니다” 입니다. &lt;code&gt;ArrayList&amp;#x3C;T&gt;&lt;/code&gt;의 &lt;code&gt;T&lt;/code&gt;는 바꿀 수 없고 이 경우를 &lt;strong&gt;Invariant&lt;/strong&gt; 이라고 부릅니다.&lt;/p&gt;
&lt;p&gt;Scala의 합 타입은 상속을 이용하여 구현되어있기 때문에 합 타입의 하위 타입과 상위 타입간의 업캐스팅이 가능합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Scala 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;sealed&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;trait&lt;/span&gt; Fruit
&lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; Apple&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;weight&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; Fruit
&lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; Banana&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;length&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; Fruit

&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; appleList&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; List&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Apple&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; List&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; fruitList&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; List&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Fruit&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; appleList&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;인터페이스의-한계&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4%EC%9D%98-%ED%95%9C%EA%B3%84&quot; aria-label=&quot;인터페이스의 한계 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;인터페이스의 한계&lt;/h2&gt;
&lt;p&gt;제네릭과 Variance에 대해서 충분히 이해했다면 상속이 있는 언어들에서 인터페이스에 담긴 언어적 한계를 파악할 수 있습니다. 인터페이스에는 총 두 가지의 문제점이 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;자기 자신의 타입을 사용하는 메소드를 선언할 수 없다.&lt;/li&gt;
&lt;li&gt;정적 메소드를 추상 메소드로 선언할 수 없다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;먼저 첫 번째 문제를 파헤쳐봅시다, 자기 자신의 타입을 사용하는 메소드를 선언할 수 없다는 것이 무슨 의미일까요? 가령 Java에서 숫자 타입의 덧셈을 인터페이스로 구현해봅시다. &lt;code&gt;int&lt;/code&gt;의 덧셈 메소드는 &lt;code&gt;int&lt;/code&gt; 인수를 받고 덧셈을 하여 &lt;code&gt;int&lt;/code&gt;를 출력 해야합니다. &lt;code&gt;double&lt;/code&gt;의 덧셈 메소드는 &lt;code&gt;double&lt;/code&gt;을 받고 덧셈을 하여 &lt;code&gt;double&lt;/code&gt;을 출력 해야 합니다. 이러한 덧셈을 하나의 인터페이스로 일관화 시킬 수 있을까요? Java는 &lt;code&gt;int&lt;/code&gt;에 메소드를 추가할 수 없으므로 &lt;code&gt;int&lt;/code&gt;를 감싸는 &lt;code&gt;CustomInt&lt;/code&gt; 타입으로 인터페이스를 충족시켜봅시다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Java 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Addable&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token class-name&quot;&gt;Addable&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Addable&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// CustomInt를 사용할 수 없다&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CustomInt&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Addable&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CustomInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    value &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token annotation punctuation&quot;&gt;@Override&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Addable&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Addable&lt;/span&gt; rhs&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CustomInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;CustomInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; rhs&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 캐스팅 해야 함&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token class-name&quot;&gt;Addable&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CustomInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;Addable&lt;/span&gt; b &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CustomInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Addable 타입이 출력된다&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;Addable&lt;/code&gt; 인터페이스에서 메소드에 &lt;code&gt;CustomInt&lt;/code&gt;를 명시할 수 없으므로 어쩔 수 없이 &lt;code&gt;Addable&lt;/code&gt;에 제네릭 &lt;code&gt;T&lt;/code&gt;를 추가하고 &lt;code&gt;CustomInt&lt;/code&gt;가 &lt;code&gt;Addable&amp;#x3C;CustomInt&gt;&lt;/code&gt;를 상속받도록 해야 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Java 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Addable&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CustomInt&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Addable&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;CustomInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CustomInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    value &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token annotation punctuation&quot;&gt;@Override&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CustomInt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;CustomInt&lt;/span&gt; rhs&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CustomInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; rhs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token class-name&quot;&gt;Addable&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;CustomInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CustomInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;CustomInt&lt;/span&gt; b &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CustomInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// CustomInt 타입이 출력된다&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이런 방식으로 구현하면 어느정도 해결되는 것 처럼 보입니다. 하지만 &lt;code&gt;CustomInt&lt;/code&gt;를 상속받기 시작하면 문제가 나타납니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Java 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CustomComplex&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CustomInt&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; imaginaryValue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CustomComplex&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; imaginaryValue&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;super&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 부모의 생성자 활용&lt;/span&gt;
    imaginaryValue &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; imaginaryValue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 나머지 함수는 그대로 상속받는다&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token class-name&quot;&gt;CustomComplex&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CustomComplex&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;CustomInt&lt;/span&gt; b &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CustomComplex&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// CustomInt가 출력된다!!!&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;Addable&amp;#x3C;T&gt;&lt;/code&gt; 인터페이스의 메소드를 곰곰히 생각해보면 &lt;code&gt;T&lt;/code&gt;를 입력으로 받고 &lt;code&gt;T&lt;/code&gt;를 출력하기 때문에 Invariant입니다. 그러므로 하위 타입인 &lt;code&gt;CustomComplex&lt;/code&gt;가 &lt;code&gt;Addable&amp;#x3C;CustomInt&gt;&lt;/code&gt;의 메소드를 사용할 수 있는 것은 옳지 않습니다. 그러나 이를 무시하고 추가한 결과 &lt;code&gt;CustomComplex&lt;/code&gt;의 &lt;code&gt;add&lt;/code&gt; 메소드는 &lt;code&gt;CustomInt&lt;/code&gt; 타입을 출력하고 있습니다. 상속과 Variance를 섞어서 활용할 경우 이러한 모순적인 코드 나타납니다.&lt;/p&gt;
&lt;p&gt;두번째 문제로 넘어갑시다. 9개의 언어 중 Rust를 제외하고 정적 메소드를 추상 메소드로 구현 할 수 있는 언어는 없습니다. 정적 추상 함수는 일반적으로는 의미가 없으나 제네릭과 함께 사용할 때 매우 유용합니다. 예를 들어 인스턴스 생성 함수를 인터페이스 메소드로 추가할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Java 코드, 주의: 컴파일할 수 없는 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Entity&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token class-name&quot;&gt;Entity&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;parseFromString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; string&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Human&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Entity&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token annotation punctuation&quot;&gt;@Override&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Entity&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;parseFromString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; string&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token class-name&quot;&gt;Human&lt;/span&gt; human &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Human&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    human&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; string&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; human&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Sword&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;implements&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Entity&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; attackPoint&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token annotation punctuation&quot;&gt;@Override&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Entity&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;parseFromString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; string&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token class-name&quot;&gt;Sword&lt;/span&gt; sword &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Sword&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    sword&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;attackPoint &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Integer&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parseInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;string&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; sword&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// T는 Entity를 상속받음&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Entity&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Entity&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;parseEntityFromString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; string&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parseFromString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;string&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token class-name&quot;&gt;Entity&lt;/span&gt; human &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; parseEntityFromString&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Human&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;mango&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;Entity&lt;/span&gt; sword &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; parseEntityFromString&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Sword&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;120&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 문제 때문에 일반적으로 객체 지향 프로그래밍 언어에서는 **팩토리 패턴(Factory Pattern)**으로 우회해서 구현합니다. 하지만 이는 코드의 불필요한 복잡도 상승과 오브젝트 간의 강한 결속을 야기합니다.&lt;/p&gt;
&lt;p&gt;Java, C#, Kotlin 등의 객체 지향 프로그래밍 언어에서 제네릭 함수의 활용성이 떨어지는 것은 이러한 인터페이스의 한계점과 연관이 있습니다. 그렇다면 Scala나 Rust에서는 어떠한 방식으로 해결되었을까요?&lt;/p&gt;
&lt;h2 id=&quot;타입클래스&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%83%80%EC%9E%85%ED%81%B4%EB%9E%98%EC%8A%A4&quot; aria-label=&quot;타입클래스 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;타입클래스&lt;/h2&gt;
&lt;p&gt;타입클래스는 인터페이스와 비슷하게, 타입간에 공통된 추상 함수를 지정할 수 있습니다. 다만 인터페이스는 타입의 메소드로 구현하지만 타입클래스는 타입과 분리된 영역에서 함수들을 구현합니다. 그렇기 때문에 상속 문제에서 자유로우며 모든 함수가 자연스럽게 정적이므로 정적 메소드 문제는 피할 수 있습니다.&lt;/p&gt;
&lt;p&gt;Scala는 숫자 타입의 연산 타입클래스를 다음과 같이 일반화하여 제공하고 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scala&quot;&gt;&lt;pre class=&quot;language-scala&quot;&gt;&lt;code class=&quot;language-scala&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Scala 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// &quot;숫자&quot;를 위한 타입클래스&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 인터페이스와 제네릭을 섞어서 타입클래스를 만든다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;trait&lt;/span&gt; Numeric&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;T&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; plus&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;lhs&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; T&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; rhs&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; T&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; T
  &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Numeric를 상속받지 않는다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; CustomInt&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Numeric[CustomInt]의 정의&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;implicit&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; numeric&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Numeric&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;CustomInt&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; Numeric&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;CustomInt&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; plus&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;lhs&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CustomInt&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; rhs&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CustomInt&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CustomInt &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
      CustomInt&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;lhs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; rhs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// T는 Numeric 타입클래스를 가지고 있어야 한다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; addThree&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;T&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Numeric&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;first&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; T&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; second&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; T&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; third&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; T&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  first &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; second &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; third

&lt;span class=&quot;token comment&quot;&gt;// addThree 함수를 자유롭게 사용할 수 있다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; intAdds&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Int&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  addThree&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; customIntAdds&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CustomInt &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  addThree&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;CustomInt&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; CustomInt&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; CustomInt&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; floatAdds&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Float&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  addThree&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0.5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1.2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;500.0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Rust의 인터페이스에 해당하는 &lt;code&gt;trait&lt;/code&gt;는 자기 자신 타입을 지정할 수 있습니다. 이것이 가능한 이유는 Rust는 상속 자체가 없어 부모로부터 잘못된 메소드를 상속받을 일이 없기 때문입니다. 또한 Rust는 인터페이스에서 정적 메소드도 선언할 수 있습니다. 이러한 점들을 살펴보면 Rust의 &lt;code&gt;trait&lt;/code&gt;는 타입클래스와 많이 닮아 있다는 것을 알 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;rust&quot;&gt;&lt;pre class=&quot;language-rust&quot;&gt;&lt;code class=&quot;language-rust&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Rust 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 덧셈을 위한 trait, Self는 자기 자신 타입이다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;pub&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;trait&lt;/span&gt; &lt;span class=&quot;token type-definition class-name&quot;&gt;Add&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Rhs&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;Self&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token type-definition class-name&quot;&gt;Output&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; rhs&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Rhs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;Self&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Output&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Rust에는 상속이 없다&lt;/span&gt;
&lt;span class=&quot;token attribute attr-name&quot;&gt;#[derive(Clone)]&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token type-definition class-name&quot;&gt;CustomInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;i32&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Add trait 정의&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;impl&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Add&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CustomInt&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token type-definition class-name&quot;&gt;Output&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CustomInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;add&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; other&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;Self&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;Self&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token class-name&quot;&gt;CustomInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; other&lt;span class=&quot;token number&quot;&gt;.0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// T는 Add&amp;lt;Output = T&gt;를 구현하고 있어야한다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;pub&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;add_three&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Add&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Output&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;first&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; second&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; third&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  first &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; second &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; third
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Add 함수를 자유롭게 사용할 수 있다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; int_adds&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;i32&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;add_three&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; custom_int_adds&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CustomInt&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;add_three&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;CustomInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CustomInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CustomInt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; float_adds&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;f32&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;add_three&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0.5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1.2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;500.0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 주요 프로그래밍 언어들의 다형성 기능들을 대부분 살펴보았는데요, 이렇듯 언어들이 한 가지 다형성만이 아닌 여러가지 다형성을 제공하는 이유는 다형성마다 장단점이 있고 적절한 장소에 적당한 다형성 기능을 사용할 수 있게 해주기 때문입니다.&lt;/p&gt;
&lt;p&gt;하지만 다형성을 사용한다고 해서 언제나 소프트웨어 아키텍처를 확장할 때 코드의 양이 필요한 만큼만 작성되는 것이 아니며 때로는 위 다형성 기능들로 해결이 안되는 반복적인 **보일러플레이트 코드(Bolierplate code)**가 작성되어야하기도 합니다. 다음 편에서는 이러한 보일러플레이트 코드가 어떻게 **메타 프로그래밍(Metaprogramming)**으로 해결되는지 다루고자 합니다.&lt;/p&gt;
&lt;h2 id=&quot;부록&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B6%80%EB%A1%9D&quot; aria-label=&quot;부록 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;부록&lt;/h2&gt;
&lt;h3 id=&quot;프로그래밍-언어-기능별-지원표-2022년-5월-기준&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%EC%96%B8%EC%96%B4-%EA%B8%B0%EB%8A%A5%EB%B3%84-%EC%A7%80%EC%9B%90%ED%91%9C-2022%EB%85%84-5%EC%9B%94-%EA%B8%B0%EC%A4%80&quot; aria-label=&quot;프로그래밍 언어 기능별 지원표 2022년 5월 기준 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;프로그래밍 언어 기능별 지원표, 2022년 5월 기준&lt;/h3&gt;
&lt;p&gt;언어 자체 기능 혹은 정규 라이브러리만 포함하고 있습니다.&lt;/p&gt;
&lt;div style=&apos;overflow: auto;&apos;&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;C&lt;/th&gt;
&lt;th&gt;C++&lt;/th&gt;
&lt;th&gt;C#&lt;/th&gt;
&lt;th&gt;Java&lt;/th&gt;
&lt;th&gt;Kotlin&lt;/th&gt;
&lt;th&gt;Scala&lt;/th&gt;
&lt;th&gt;Rust&lt;/th&gt;
&lt;th&gt;Typescript&lt;/th&gt;
&lt;th&gt;Go&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Nullable Type&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;합 타입&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;합 타입 완전 패턴 매칭&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;부분적&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;오버로딩&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;상속&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;오버라이딩&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;타입 클래스&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;제네릭&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;타입 선언부 Variance&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;인터페이스만&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;td&gt;인터페이스만&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;td&gt;X&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h3 id=&quot;아쉽게도-다루지-못한-다른-다형성-기능들&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%95%84%EC%89%BD%EA%B2%8C%EB%8F%84-%EB%8B%A4%EB%A3%A8%EC%A7%80-%EB%AA%BB%ED%95%9C-%EB%8B%A4%EB%A5%B8-%EB%8B%A4%ED%98%95%EC%84%B1-%EA%B8%B0%EB%8A%A5%EB%93%A4&quot; aria-label=&quot;아쉽게도 다루지 못한 다른 다형성 기능들 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;아쉽게도 다루지 못한 다른 다형성 기능들&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Wildcard (Java의 개념), Type Projection (Kotlin의 개념)&lt;/li&gt;
&lt;li&gt;Associated Type&lt;/li&gt;
&lt;li&gt;Kind (Higher-kinded types)&lt;/li&gt;
&lt;li&gt;덕 타이핑(Duck typing)&lt;/li&gt;
&lt;li&gt;Constraint (C++의 개념)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;힙스터라면-공부해볼-만한-다른-언어들&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%9E%99%EC%8A%A4%ED%84%B0%EB%9D%BC%EB%A9%B4-%EA%B3%B5%EB%B6%80%ED%95%B4%EB%B3%BC-%EB%A7%8C%ED%95%9C-%EB%8B%A4%EB%A5%B8-%EC%96%B8%EC%96%B4%EB%93%A4&quot; aria-label=&quot;힙스터라면 공부해볼 만한 다른 언어들 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;힙스터라면 공부해볼 만한 다른 언어들&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Assembly
&lt;ul&gt;
&lt;li&gt;바로 그 어셈블리&lt;/li&gt;
&lt;li&gt;C보다도 로우 레벨&lt;/li&gt;
&lt;li&gt;스택, 함수 호출, 다양한 최적화의 원리를 이해해보자&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Carbon
&lt;ul&gt;
&lt;li&gt;정적 타입&lt;/li&gt;
&lt;li&gt;로우 레벨&lt;/li&gt;
&lt;li&gt;Better C++ 계열&lt;/li&gt;
&lt;li&gt;2022년 7월 공개된 언어&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;D
&lt;ul&gt;
&lt;li&gt;정적 타입&lt;/li&gt;
&lt;li&gt;로우 레벨&lt;/li&gt;
&lt;li&gt;Better C++ 계열&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Erlang, Elixir
&lt;ul&gt;
&lt;li&gt;동적 타입&lt;/li&gt;
&lt;li&gt;함수형&lt;/li&gt;
&lt;li&gt;모든 것은 액터 모델&lt;/li&gt;
&lt;li&gt;Fault-tolerant&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Haskell
&lt;ul&gt;
&lt;li&gt;정적 타입&lt;/li&gt;
&lt;li&gt;순수 함수형&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Jai
&lt;ul&gt;
&lt;li&gt;정적 타입&lt;/li&gt;
&lt;li&gt;로우 레벨&lt;/li&gt;
&lt;li&gt;메타 프로그래밍 특화&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Julia
&lt;ul&gt;
&lt;li&gt;동적 타입&lt;/li&gt;
&lt;li&gt;고성능&lt;/li&gt;
&lt;li&gt;계산 수학&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;OCaml
&lt;ul&gt;
&lt;li&gt;정적 타입&lt;/li&gt;
&lt;li&gt;함수형 패러다임 입문용으로 나쁘지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Zig
&lt;ul&gt;
&lt;li&gt;정적 타입&lt;/li&gt;
&lt;li&gt;로우 레벨&lt;/li&gt;
&lt;li&gt;Better C 계열&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[Unity 프로젝트를 위한 Git Hooks 활용]]></title><description><![CDATA[Git으로 관리하는 Unity 게임 클라이언트 프로젝트에서의 Git Hooks 활용 방안에 관한 제안]]></description><link>https://tech.devsisters.com/posts/unity-githooks</link><guid isPermaLink="false">https://tech.devsisters.com/posts/unity-githooks</guid><pubDate>Tue, 02 Aug 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요.
저는 쿠키런 킹덤에서 게임 클라이언트를 개발하고 있는 이규호입니다.
게임 클라이언트란 플레이어 분들의 기기에 설치돼서 게임 서버와 통신하며 게임 화면을 표시하고 플레이어 분들의 입력에 맞게 게임 로직을 실행하는 소프트웨어입니다.&lt;/p&gt;
&lt;p&gt;일반적으로 게임 클라이언트 개발자의 주된 업무는 게임 엔진 등을 활용하여 실제 게임에 들어가는 컨텐츠 및 기능을 구현하는 것입니다.
하지만 게임 개발 조직 안에서 발생하는 다양한 비효율을 해소하고 게임 개발 과정이 빠르고 문제 없이 진행될 수 있도록 여러 가지 노력을 기울이는 것도 클라이언트 개발자의 중요한 임무 중 하나일 것입니다.
이 글에서는 그러한 노력의 일환으로 Unity 엔진과 Git을 사용하는 프로젝트에서 Git Hooks를 활용한 경험을 공유하고자 합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;이 글의 작성 시점에서 Git의 최신 버전은 2.37.1이며 Unity는 2022.1.8까지 릴리즈되었습니다.
추후 각 소프트웨어 및 개발 환경이 업데이트됨에 따라 이 글에 언급된 내용 중 일부가 더이상 유효하지 않게 되거나 더 나은 방법이 생길 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;서론&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%84%9C%EB%A1%A0&quot; aria-label=&quot;서론 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;서론&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;진짜_진짜_최종&lt;/code&gt; 같은 접미사가 붙는 파일이 담긴 USB 메모리를 손에서 손으로 주고받으며 버전 관리를 한다는 괴담이 실없는 농담이 된 시대입니다. 오늘날 대부분의 소프트웨어 개발 조직은 한 개 이상의 버전 관리 시스템을 사용하고 있을 것입니다. 그 중에서도 가장 대중적인 것은 리누스 토르발스가 Linux 커널 프로젝트를 관리하기 위해 만든 Git입니다. 2005년에 처음으로 공개된 이래로 Git은 꾸준히 발전해 왔으며 Git을 활용하는 전략도 점점 진보해 왔습니다.&lt;/p&gt;
&lt;h3 id=&quot;gitflow를-활용한-원활한-버전-관리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#gitflow%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%9B%90%ED%99%9C%ED%95%9C-%EB%B2%84%EC%A0%84-%EA%B4%80%EB%A6%AC&quot; aria-label=&quot;gitflow를 활용한 원활한 버전 관리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;GitFlow를 활용한 원활한 버전 관리&lt;/h3&gt;
&lt;p&gt;대표적인 Git 활용 전략으로는 &lt;a href=&quot;https://techblog.woowahan.com/2553/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;GitFlow&lt;/a&gt;가 있습니다. 일반적인 서비스 백엔드 프로젝트에서 GitFlow를 따르는 개발 과정은 다음과 같을 것입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;새 피쳐 구현을 위해 새 브랜치를 생성합니다. 이 피쳐 브랜치에서는 상대적으로 소수의 소프트웨어 엔지니어가 작업합니다.&lt;/li&gt;
&lt;li&gt;구현이 끝나면 풀 리퀘스트(혹은 머지 리퀘스트)를 생성하고 CI와 다른 동료들의 코드 리뷰를 통해 작업 내용을 꼼꼼히 점검한 후 이를 메인 브랜치에 머지합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 경우 브랜치 히스토리는 마치 각자의 레인에서 안정적인 크롤 영법을 구사하는 수영 숙련자들처럼 깔끔한 평행선을 그립니다. 여러 개의 큰 피쳐를 연이어 머지하는 경우가 아니면 각 커밋은 막힘 없이 나아가고 문제가 생기더라도 수영하는 사람들이 다들 수영을 잘 하기 때문에 크게 고생하는 경우는 적습니다.&lt;/p&gt;
&lt;h3 id=&quot;게임-클라이언트-프로젝트의-개발-양상&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%8C%EC%9E%84-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%9D%98-%EA%B0%9C%EB%B0%9C-%EC%96%91%EC%83%81&quot; aria-label=&quot;게임 클라이언트 프로젝트의 개발 양상 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;게임 클라이언트 프로젝트의 개발 양상&lt;/h3&gt;
&lt;p&gt;그러나 게임 클라이언트 프로젝트의 개발 과정은 이와 다른 경우가 많습니다. 필요에 따라 여러 개의 마이크로 서비스로 분리하는 것을 지향하는 현대적 서비스 백엔드 아키텍쳐와 달리 게임 클라이언트는 기본적으로 모놀리식한 구조이며 상대적으로 더 많은 인원이 하나의 저장소에 커밋을 밀어넣습니다. 인원의 구성도 소프트웨어 엔지니어에 한정되지 않고 게임 디자이너, 아티스트 등 다양한 직군의 동료들이 개발에 참여합니다. 따라서 GitFlow를 따를 경우 다음과 같은 문제가 발생합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;게임 내 각 피쳐 및 각 직군 간의 작업물이 상호 의존적인 경우가 많습니다. 따라서 피쳐 브랜치를 잘게 분리하기 어려우며 한 브랜치에 다양한 팀원이 커밋하게 됩니다.&lt;/li&gt;
&lt;li&gt;구현된 사항을 즉각 시험해보며 최대한 짧은 주기의 피드백 루프를 여러 번 거쳐 재미를 검증해야 합니다. 메인 브랜치에 가급적 여러 기능이 빠르게 반영되어 중간 구현 결과를 시험해 볼 수 있는 편이 유리합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이에 보다 적합한 Git 활용 전략은 모두가 메인 브랜치에 커밋을 밀어넣는 &lt;a href=&quot;https://www.jetbrains.com/ko-kr/teamcity/ci-cd-guide/concepts/trunk-based-development/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;트렁크 기반 개발&lt;/a&gt;일 것입니다. 그래서 개발 과정은 레인이 나뉜 실내 수영장보다는 워터파크의 파도 풀장에 가까운 양상을 띱니다. 수많은 사람이 한 풀장에 들어가며 레인 같은 것은 나뉘지 않으니 수영을 하던 사람들끼리 부딪히는 일도 잦습니다.&lt;/p&gt;
&lt;p&gt;사람들이 모두 수영에 능숙한 것도 아닙니다. Git은 기본적으로 커맨드라인 인터페이스로 작동하는 도구이며 파일 시스템이나 트리 구조, 참조 등의 멘탈 모델에 근거하다 보니 아무래도 소프트웨어 엔지니어 이외 직군의 동료들은 Git에 덜 익숙한 편입니다. 그래서 혼잡한 파도 풀장에서 문제가 발생할 경우 수영에 보다 능숙한 소프트웨어 엔지니어의 도움이 필요할 때가 많습니다.&lt;/p&gt;
&lt;p&gt;더군다나 파도 풀장에는 수영복을 입은 사람들 이외에도 튜브, 스노클, 구명조끼, 풀장 옆 매점에서 파는 츄러스 등 정말 다양한 것들이 들어옵니다. 코드 이외에도 아트워크, 사운드 리소스 등 다양한 에셋이 프로젝트 저장소에 올라오며 이러한 파일이 코드보다 더 큰 비중을 차지합니다. 그리고 다양한 종류의 물건은 다양한 문제를 일으킵니다.&lt;/p&gt;
&lt;h3 id=&quot;파도-풀장의-파수꾼&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%8C%8C%EB%8F%84-%ED%92%80%EC%9E%A5%EC%9D%98-%ED%8C%8C%EC%88%98%EA%BE%BC&quot; aria-label=&quot;파도 풀장의 파수꾼 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;파도 풀장의 파수꾼&lt;/h3&gt;
&lt;p&gt;이런 피서철의 혼잡한 파도풀에는 사고를 예방할 안전 요원이 필요합니다. 일반적으로 &lt;a href=&quot;https://aws.amazon.com/ko/devops/continuous-integration/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;CI(Continuous Integration, 지속적 통합)&lt;/a&gt;가 그러한 역할을 맡지만 게임 클라이언트 프로젝트의 경우에는 CI만으로는 충분하지 않습니다. 이전에 올린 커밋의 CI가 채 돌기도 전에 새 커밋이 끊임없이 올라오기 때문에 중간에 문제가 있는 커밋이 생길 경우 CI는 명절 고속도로 IC에 늘어선 차량들의 브레이크등처럼 기나긴 빨간 불의 행렬을 띄웁니다. 이 경우 어느 커밋에서부터 어떤 문제가 생겼는지 추적하기가 번거롭습니다.&lt;/p&gt;
&lt;p&gt;CI가 띄우는 경고를 읽고 대처하는 것은 대개 소프트웨어 엔지니어의 역할입니다. 프로젝트 저장소에 문제가 생겨서 교통 체증이 발생하면 다른 동료들의 작업이 모두 지장을 받기 때문에 하던 작업을 내려놓고 높은 우선순위로 대응해야 하며, 때문에 문제가 빈번하게 발생할수록 주 업무에 할애할 집중력과 시간이 고갈됩니다. 따라서 소프트웨어 엔지니어 스스로의 생산성 향상을 위해서라도 CI보다 즉각적으로 예방 효과를 발휘할 수 있는 수단이 필요합니다. 그 수단 중 하나로 Git Hooks를 소개하고자 합니다.
물론 Git Hooks는 보편적인 기능이다 보니 소개라고 하기에는 다소 새삼스럽습니다. 이 글이 뻔한 내용이 되지 않도록 쿠키런 킹덤에서 사용하는 Unity 개발 환경에서 Git Hooks를 활용해 온 주관적이고 덜 보편적인 경험과 의견을 공유하겠습니다.&lt;/p&gt;
&lt;h2 id=&quot;git-hooks란&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#git-hooks%EB%9E%80&quot; aria-label=&quot;git hooks란 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Git Hooks란&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://git-scm.com/book/ko/v2/Git%EB%A7%9E%EC%B6%A4-Git-Hooks&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Git Hooks&lt;/a&gt;(이하 “Hook”으로 짧게 적겠습니다)는 Git에서 특정 이벤트가 발생할 때 호출되는 스크립트이며 기본적으로 저장소 루트 디렉토리의 &lt;code&gt;.git/hooks&lt;/code&gt; 경로 안에 위치합니다.&lt;/p&gt;
&lt;p&gt;프로젝트에서 발생할 문제를 예방하기 위해 가장 많이 사용하는 이벤트는 &lt;code&gt;pre-commit&lt;/code&gt; 입니다. 이는 커밋 시 가장 먼저 발생하는 이벤트로 커밋 메시지를 작성하기 전 단계에 발생합니다. (물론 &lt;code&gt;git commit -m&lt;/code&gt; 옵션이나 GUI 클라이언트로 커밋 메시지를 함께 전달하는 경우 이 이벤트가 발생한 후 따로 커밋 메시지 작성을 요구하지 않습니다.) &lt;code&gt;0&lt;/code&gt; 이외의 Exit Code를 리턴하면 커밋이 취소되기 때문에 커밋을 검사 후 문제가 생길 경우 Exit Code를 통해 커밋을 취소하는 방식으로 문제 예방에 활용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;Hook은 대개 셸 스크립트로 작성되지만 다음의 사례처럼 생산성과 유지보수, 혹은 기능적, 성능적 필요에 의해 다른 프로그래밍 언어로 HookHook 작성하는 경우도 많습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/git-lfs/git-lfs&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Git LFS(Git Large File Storage)&lt;/a&gt;: Git은 기본적으로 소스 코드와 같은 평문(Plain Text) 파일의 버전 관리에 용이한 시스템입니다. Git LFS는 이미지나 오디오처럼 용량이 큰 바이너리 등의 파일을 별도의 원격 저장소에 저장해 효율적으로 관리하기 위한 도구로, 일반 Git 저장소에는 해당 파일의 레퍼런스를 저장하고 &lt;code&gt;post-commit&lt;/code&gt;, &lt;code&gt;post-checkout&lt;/code&gt; 등의 이벤트에서 Hook을 실행해 해당 레퍼런스와 원격 저장소의 파일을 동기화하는 방식으로 동작합니다. Go로 작성되었습니다.&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/carloscuesta/gitmoji-cli&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Gitmoji CLI&lt;/a&gt;: &lt;a href=&quot;https://github.com/carloscuesta/gitmoji&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Gitmoji&lt;/a&gt;는 커밋 메시지 앞에 작업의 성격을 나타내는 이모지를 달아 커밋 의도를 더 쉽게 파악할 수 있게 하려는 운동입니다. Gitmoji CLI는 &lt;code&gt;prepare-commit-msg&lt;/code&gt; 이벤트에서 작동해 커밋 앞에 편리하게 이모지를 달 수 있게 해 주는 CLI 도구이며 Node.js로 작성되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Git Hooks는 터미널에서 Git 명령을 실행하는 경우는 물론 Fork, Sourcetree 등의 GUI Git 클라이언트에서도 문제 없이 작동합니다. 다만 Hook에서 출력하는 stdout 등의 스트림을 터미널에서는 바로 확인할 수 있으나 GUI 클라이언트에서는 &lt;code&gt;0&lt;/code&gt; 이외의 Exit Code를 리턴하는 경우에만 로그가 출력된 오류 팝업이 열리기 때문에 그렇지 않은 경우에는 일부러 조회하지 않는 한 확인이 불가능합니다.&lt;/p&gt;
&lt;h2 id=&quot;자주-발생하는-문제와-hook-활용-예시&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9E%90%EC%A3%BC-%EB%B0%9C%EC%83%9D%ED%95%98%EB%8A%94-%EB%AC%B8%EC%A0%9C%EC%99%80-hook-%ED%99%9C%EC%9A%A9-%EC%98%88%EC%8B%9C&quot; aria-label=&quot;자주 발생하는 문제와 hook 활용 예시 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;자주 발생하는 문제와 Hook 활용 예시&lt;/h2&gt;
&lt;p&gt;게임 클라이언트 프로젝트 저장소에서 자주 발생하는 문제와 이를 예방하기 위해 Hook을 활용하는 예시를 몇 가지 소개하겠습니다.&lt;/p&gt;
&lt;h3 id=&quot;파일이나-디렉토리-이름에-잘못된-문자나-문자열이-포함된-경우&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%8C%8C%EC%9D%BC%EC%9D%B4%EB%82%98-%EB%94%94%EB%A0%89%ED%86%A0%EB%A6%AC-%EC%9D%B4%EB%A6%84%EC%97%90-%EC%9E%98%EB%AA%BB%EB%90%9C-%EB%AC%B8%EC%9E%90%EB%82%98-%EB%AC%B8%EC%9E%90%EC%97%B4%EC%9D%B4-%ED%8F%AC%ED%95%A8%EB%90%9C-%EA%B2%BD%EC%9A%B0&quot; aria-label=&quot;파일이나 디렉토리 이름에 잘못된 문자나 문자열이 포함된 경우 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;파일이나 디렉토리 이름에 잘못된 문자나 문자열이 포함된 경우&lt;/h3&gt;
&lt;p&gt;각 운영체제마다 파일 시스템에서 사용할 수 없는 &lt;a href=&quot;https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;문자나 문자열&lt;/a&gt;이 있습니다. 물론 IT 업종에 종사하는 프로들인 만큼 파일이나 디렉토리 명칭에는 가급적 문제 소지가 없는 알파벳 대소문자 및 숫자와 &lt;code&gt;_&lt;/code&gt;(언더스코어)만으로 파일 이름을 짓는 분들이 대부분이나 간혹 실수로 인해 파일을 새로 만들거나 이름을 변경하면서 특정 OS에서 사용할 수 없는 문자나 문자열이 포함된 상태로 커밋을 올리는 경우가 있습니다.&lt;/p&gt;
&lt;p&gt;대표적인 사례는 Windows 외의 운영체제에서 엔터 키를 누르려다가 가까이 있는 &lt;code&gt;\&lt;/code&gt;(백슬래시)를 같이 누르는 경우입니다. MacOS, Linux에서는 파일이나 디렉토리 이름에 &lt;code&gt;\&lt;/code&gt;를 사용할 수 있으나 Windows에서는 &lt;code&gt;C:\Documents\CookieRun&lt;/code&gt; 등과 같이 경로 구분자로 &lt;code&gt;\&lt;/code&gt;를 사용하기 때문에 이를 사용할 수 없습니다. 따라서 이러한 파일명이 포함된 커밋을 Windows 외의 컴퓨터에서 올린 후 Windows 컴퓨터에서 이를 체크아웃할 경우 오류가 발생합니다.&lt;/p&gt;
&lt;p&gt;물론 특정 조건을 만족하는 문자열만 허용하는 방식이나 특정 조건을 만족하지 않는 문자열을 전부 허용하지 않는 방식으로 문제가 되는 문자 및 문자열을 모두 걸러내는 것이 최선이겠지만 간단하게 &lt;code&gt;\&lt;/code&gt;만 걸러내는 &lt;code&gt;pre-commit&lt;/code&gt; Hook을 예시로 작성하겠습니다.&lt;/p&gt;
&lt;p&gt;먼저 &lt;code&gt;.git/hooks/pre-commit&lt;/code&gt; 파일을 생성 후 텍스트 에디터로 다음과 같은 스크립트를 작성합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token shebang important&quot;&gt;#!/bin/sh&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 커밋으로 생성되는 파일을 나열하기 위해 현재 Index와 비교할 HEAD를 지정합니다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; rev-parse &lt;span class=&quot;token parameter variable&quot;&gt;--verify&lt;/span&gt; HEAD &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;/dev/null
&lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt;
	&lt;span class=&quot;token assign-left variable&quot;&gt;against&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;HEAD
&lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt;
	&lt;span class=&quot;token comment&quot;&gt;# 새로 생성해서 커밋이 없는 저장소의 경우 HEAD가 없기 때문에 빈 Tree와 비교합니다&lt;/span&gt;
	&lt;span class=&quot;token assign-left variable&quot;&gt;against&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; hash-object &lt;span class=&quot;token parameter variable&quot;&gt;-t&lt;/span&gt; tree /dev/null&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Index에서 ADD된 파일의 이름만 순회합니다. RENAME도 ADD에 포함됩니다&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;diff&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--cached&lt;/span&gt; --name-only --diff-filter&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;A &lt;span class=&quot;token parameter variable&quot;&gt;-z&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$against&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;&lt;span class=&quot;token environment constant&quot;&gt;IFS&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;read&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;&apos;&lt;/span&gt; filename&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;do&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$filename&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=~&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token entity&quot; title=&quot;\\&quot;&gt;\\&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;# 사용자가 수정할 수 있도록 문제가 무엇인지 제대로 안내해야 합니다&lt;/span&gt;
		&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;&amp;amp;2&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;새로 생성된 파일 &lt;span class=&quot;token entity&quot; title=&quot;\&amp;quot;&quot;&gt;\&quot;&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;${filename}&lt;/span&gt;&lt;span class=&quot;token entity&quot; title=&quot;\&amp;quot;&quot;&gt;\&quot;&lt;/span&gt; 의 이름 혹은 경로에 잘못된 문자 &apos;\&apos;이 포함됐습니다.&quot;&lt;/span&gt;
		&lt;span class=&quot;token comment&quot;&gt;# 0이 아닌 Exit Code를 리턴해 커밋을 중단합니다&lt;/span&gt;
		&lt;span class=&quot;token builtin class-name&quot;&gt;exit&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;done&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;첫 줄의 Shebang이 누락되면 Hook이 실행되지 않음에 주의해야 합니다.&lt;/p&gt;
&lt;p&gt;생성한 파일의 실행 권한을 부여하기 위해 다음 명령을 실행합니다. 이후 이름이나 경로에 &lt;code&gt;\&lt;/code&gt;를 포함한 커밋을 시도할 경우 오류 메시지와 함께 도중에 취소됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;chmod&lt;/span&gt; +x .git/hooks/pre-commit&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;meta-파일의-짝이-맞는지-검사하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#meta-%ED%8C%8C%EC%9D%BC%EC%9D%98-%EC%A7%9D%EC%9D%B4-%EB%A7%9E%EB%8A%94%EC%A7%80-%EA%B2%80%EC%82%AC%ED%95%98%EA%B8%B0&quot; aria-label=&quot;meta 파일의 짝이 맞는지 검사하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;code&gt;.meta&lt;/code&gt; 파일의 짝이 맞는지 검사하기&lt;/h3&gt;
&lt;p&gt;Unity 엔진은 각 에셋, 스크립트, 디렉토리 등을 프로젝트에서 사용하기 위한 추가적인 메타데이터를 정의하기 위해 &lt;code&gt;Assets&lt;/code&gt; 디렉토리 내부의 각 파일 및 디렉토리마다 해당 대상의 이름 뒤에 &lt;code&gt;.meta&lt;/code&gt; 가 붙는 별도의 메타데이터 파일을 생성합니다.&lt;/p&gt;
&lt;p&gt;그러나 종종 이 &lt;code&gt;.meta&lt;/code&gt; 파일과 원 대상이 쌍을 이루지 않는 경우가 발생합니다. 원인이 되는 대표적인 시나리오는 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;동료가 다른 툴로 작업한 파일을 저장소에 새로 가져오면서 운영체제의 파일 탐색기에서만 파일을 옮겨 두고 Unity를 실행하지 않은 채 모든 변경사항을 스테이징한 후 커밋했습니다. 이로 인해 저장소에 파일은 추가되었으나 쌍을 이루는 &lt;code&gt;.meta&lt;/code&gt; 파일이 생성되지 않았습니다. 혹은 Unity를 거치지 않고 원본 파일만 삭제하거나 이름을 변경하는 등의 조작을 통해서도 유사한 문제가 발생합니다.&lt;/li&gt;
&lt;li&gt;동료가 새 디렉토리를 생성했으나 안에 다른 파일을 추가하지 않았습니다. Unity는 해당 디렉토리와 쌍을 이루는 &lt;code&gt;.meta&lt;/code&gt; 파일을 자동으로 생성했고 모든 변경사항을 스테이징한 후 커밋했습니다. Git은 디렉토리가 아니라 파일을 추적하기 때문에 빈 디렉토리는 Git으로 추적되지 않았고 이후 다른 동료가 자신의 기기에서 해당 커밋을 체크아웃했으나 추적되지 않은 빈 디렉토리는 동료의 로컬 저장소에 생성되지 않았습니다. 따라서 &lt;code&gt;.meta&lt;/code&gt; 파일만 생성되었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이러한 불일치는 다음과 같은 번거로운 문제를 야기합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.meta&lt;/code&gt; 파일만 있고 쌍을 이뤄야 할 원 대상이 존재하지 않는 경우 Unity는 자동으로 &lt;code&gt;.meta&lt;/code&gt; 파일을 삭제합니다. 이 경우 다른 동료는 자신이 작업하지 않은 변경사항이 생기는 것을 보고 의아해하며 매번 이 변경사항을 폐기(Discard)하거나 스태시에 밀어넣은 후 풀을 받습니다. 이 번거로운 일은 누군가가 해당 &lt;code&gt;.meta&lt;/code&gt; 파일을 삭제하는 커밋을 올릴 때까지 되풀이됩니다. 안타깝게도 그렇게 일단락되더라도 빈 디렉토리로 인해 생성된 &lt;code&gt;.meta&lt;/code&gt; 파일인 경우 해당 &lt;code&gt;.meta&lt;/code&gt; 빈 디렉토리를 로컬에 가지고 있는 동료가 &lt;code&gt;.meta&lt;/code&gt; 파일을 계속해서 커밋하는 경우 누군가가 정확한 원인을 파악하고 해결하기 전까지는 영원한 핑퐁이 계속됩니다.&lt;/li&gt;
&lt;li&gt;위의 만성적인 사례와 달리 원 대상은 존재하나 쌍을 이뤄야 할 &lt;code&gt;.meta&lt;/code&gt; 파일이 존재하지 않는 경우 좀 더 즉각적인 문제가 발생합니다. 이 경우 Unity는 자동으로 &lt;code&gt;.meta&lt;/code&gt; 파일을 생성하며, 이 &lt;code&gt;.meta&lt;/code&gt; 파일을 여러 동료가 커밋하는 경우 &lt;code&gt;.meta&lt;/code&gt; 파일에 기입된 guid가 각 동료의 로컬 저장소에서 일치하지 않기 때문에 처음으로 푸시한 동료를 제외한 다른 동료는 충돌(Conflict)을 겪게 됩니다. 충돌은 특히 소프트웨어 엔지니어 이외의 직군에게는 당혹스러운 경우가 많으며 “임의로 생성된 guid니 어느 쪽을 채택해서 머지하든 문제가 되지 않을 것”이라고 가볍게 넘기다가 가끔 더 번거로운 문제가 생기기도 합니다. 채택되지 않은 guid를 사용한 쪽에서 해당 에셋을 참조하는 다른 에셋이나 프리팹을 생성한 경우 guid에 맞는 대상을 찾지 못해 참조가 소실되는 문제가 발생합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이러한 &lt;code&gt;.meta&lt;/code&gt; 파일 불일치를 검사하기 위한 &lt;a href=&quot;https://github.com/NYUGameCenter/Unity-Git-Config/blob/master/pre-commit&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;오픈소스 Hook&lt;/a&gt;이 있으며 대부분의 상황에서 잘 작동합니다. 쿠키런 킹덤 프로젝트에서는 해당 Hook이 검출하지 못하거나(빈 폴더는 아니지만 빈 폴더와 빈 폴더의 &lt;code&gt;.meta&lt;/code&gt;만을 포함하는 “재귀적으로 사실상 빈 폴더&quot; 등) 오검출하는(이름이 &lt;code&gt;~&lt;/code&gt; 로 끝나는 디렉토리 및 하위 항목은 Unity에서 무시하며 쌍을 이루는 &lt;code&gt;.meta&lt;/code&gt; 파일도 생성하지 않아 검사해서는 안되는 등) 상황을 정확하게 처리하고 검사 속도 최적화와 좀 더 쉬운 문제 해결을 위해 빠르고 안전하며 현대적인 프로그래밍 언어인 Rust로 Hook을 만들어 사용하고 있습니다. 이에 관해서는 다른 기회에 소개하겠습니다.&lt;/p&gt;
&lt;p&gt;이외에도 컴파일러나 린터를 실행해 코드의 문제를 검사하거나 에셋 간의 잘못된 의존관계(프로덕션에 나갈 에셋이 테스트용 에셋에 의존하는 등)를 검사, 혹은 이러한 &lt;code&gt;pre-commit&lt;/code&gt; 단계에서의 검사 외에도 &lt;code&gt;post-checkout&lt;/code&gt;이나 &lt;code&gt;post-merge&lt;/code&gt; 단계에서 정리 작업 또는 디펜던시 관련 처리를 하는 등 다양한 문제를 예방하거나 편의성 개선을 위해 Hook을 활용할 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;설치-및-버전-관리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%84%A4%EC%B9%98-%EB%B0%8F-%EB%B2%84%EC%A0%84-%EA%B4%80%EB%A6%AC&quot; aria-label=&quot;설치 및 버전 관리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;설치 및 버전 관리&lt;/h2&gt;
&lt;p&gt;기본적으로 Hook 파일은 해당 로컬 저장소에서만 유효하며 공유 및 버전관리가 불가능합니다. 이는 Hook 파일의 디폴트 경로인 &lt;code&gt;.git/hooks&lt;/code&gt; 디렉토리의 상위 디렉토리인 &lt;code&gt;.git&lt;/code&gt; 디렉토리를 Git이 추적하지 않기 때문입니다. 이로 인해 Hook을 작성하더라도 팀 내에서 모두가 설치해서 사용하도록 공유하며 변경이 있을 때마다 이를 업데이트하는 것이 다소 번거로운 경우가 많습니다. 이러한 설치 및 버전 관리에 관한 고민 및 나름대로의 해법을 소개하겠습니다.&lt;/p&gt;
&lt;p&gt;팀 동료들과 Hook을 공유하기 위해서는 먼저 Hook 디렉토리를 Git이 추적하는 경로로 옮길 필요가 있습니다. 이를 위해 프로젝트 저장소 안에 다음과 같이 &lt;code&gt;.githooks&lt;/code&gt; 디렉토리를 새로 생성합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;repository-root
│
├── .git/
│   ├── config
│   │   ...
│
├── .githooks/
│   ├── pre-commit
│   ├── post-checkout
│				...
│
├── .gitconfig&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;.githooks&lt;/code&gt; 디렉토리 아래에는 각 Hook 파일을 작성합니다. 주의할 점은, Git LFS와 같이 팀 내에서 공통적으로 설치해서 사용하는 필수적인 Hook이 있다면 해당 Hook을 실행하는 코드도 같이 가져와야 그 Hook을 다시 설치하는 번거로운 일이 일어나지 않습니다. 예를 들어 Git LFS를 저장소에 설치 시 &lt;code&gt;post-checkout&lt;/code&gt; Hook에는 다음과 같은 두 라인이 추가됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# .git/hooks/post-checkout&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.
&lt;span class=&quot;token builtin class-name&quot;&gt;command&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt; git-lfs &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;/dev/null &lt;span class=&quot;token operator&quot;&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;2&lt;/span&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;&amp;amp;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token file-descriptor important&quot;&gt;&amp;amp;2&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;...생략...&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;exit&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; lfs post-checkout &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$@&lt;/span&gt;&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;새로 작성한 Hook의 경로는 Git에 의해 추적되기 때문에 공유와 버전관리가 가능합니다. 다음 명령을 실행해 각 파일을 실행 권한과 함께 저장소에 추가한 후 커밋합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--chmod&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;+x .githooks/*&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그러나 이 상태로는 새로 작성한 Hook이 실행되지 않습니다. 여전히 Git은 &lt;code&gt;.git/hooks&lt;/code&gt; 디렉토리에서 Hook 파일을 찾으려 하기 때문입니다. 다음과 같이 저장소 루트 디렉토리에 &lt;code&gt;.gitconfig&lt;/code&gt; 파일을 생성해 Hook 경로 설정을 동료 간에 공유할 수 있도록 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# .gitconfig&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;core&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
	hooksPath &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; .githooks&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;.gitconfig&lt;/code&gt; 파일에는 이외에도 다양한 옵션을 추가해 동료 간에 일관적인 설정을 유지할 수 있습니다. 예를 들어 MacOS에서는 &lt;code&gt;core.ignoreCase&lt;/code&gt; 가 기본적으로 &lt;code&gt;false&lt;/code&gt;로 설정돼 있기 때문에 파일명의 대소문자를 바꾸더라도 변경 내역을 추적하지 못하는 경우가 많습니다. 이를 &lt;code&gt;true&lt;/code&gt;로 설정하면 이러한 문제를 피할 수 있습니다.&lt;/p&gt;
&lt;p&gt;하지만 여전히 새로 작성한 Hook은 실행되지 않습니다. Git의 로컬 설정은 기본적으로 &lt;code&gt;.git/config&lt;/code&gt; 파일에 저장되며 임의로 작성한 &lt;code&gt;.gitconfig&lt;/code&gt; 파일은 Git에게 특별할 것이 없는 그냥 평범한 파일일 뿐입니다. 이 파일에서 설정을 읽게 하기 위해서는 다음과 같은 명령을 통해 로컬 설정에 &lt;code&gt;.gitconfig&lt;/code&gt; 파일을 등록해야 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; config &lt;span class=&quot;token parameter variable&quot;&gt;--local&lt;/span&gt; include.path &lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;/.gitconfig&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이제 Hook은 정상적으로 실행됩니다. 그러나 문제는 아직도 끝나지 않았습니다. 각 동료의 로컬 저장소에서 위 명령을 실행해야 하기 때문입니다. 위 명령을 실행하도록 안내하는 것도 방법이지만 이는 생각보다 쉽지 않습니다. 동료의 숫자가 많지 않으면 개별 안내 및 점검이 가능하겠지만 동료의 숫자가 늘어날수록, 재택근무를 하는 인원이 많을수록 안내 및 점검에 필요한 비용이 증가합니다. 또한 동료가 컴퓨터를 교체하거나 저장소를 새로 클론받으면 매번 같은 명령을 실행해야 하며 문서화를 통해 안내하더라도 이는 누락되기 쉽습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 500px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 81.11111111111111%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAQCAYAAAAWGF8bAAAACXBIWXMAAAsTAAALEwEAmpwYAAADVklEQVR42pVUe0hTYRQ/9169Fzf16qZzZrV8L3WKKWkaaeVqPtKpW63ESRmGvcyIXlq3BxVWRJYaBEWUVgxJypIyepE9/lj2hxUJUZFRZA/sMSvddvq2NNQU6sDhnHvud37nfOfxAYxCIQCcGEA2wswQpuE/iBpUfFioJ55GRKBT3EFZ4g28Se8EXEBY9E9o+t8ZwIZIZp4/C0jURyRCJ5E/CL8kvJuioIvIhQMuLv8EuDEcsmUEkKfAWqng8HKcBx6PEmE8z/STSyDJfPkgIDoMgkCTm1DDwEwETEj5HfHCkoDDmvEs3k6U9qHOz2bN8rRjrsTWZwy3qif5ODJPRLWfeKYXKIaWCk0mxhEABFJoYaDYdg1wKya6PNwfwiCWJVg/7ynCz7N8sDs3GnFnoa0lPwY5ji4OdIVqcnwKms181/kTm+8vnhPj8BdItlAVDuM2B0OJEAFyh9HgAW2mVAni+gSr7XEB/txmwL6jSxFbi/FpVtBXKc+2UZRL+01dpr69dFFa+65lEc+u1Mnem68WmMtzVM6cy3yhdI0cLBVBdGWJJ1TXTfPGawalrXhGOG7wZ/GgPtr+Zl0SHo3l39Ei7pGQEoqrpobcLpe4BXSfrou9PzVO11m/Y/vdSgI42IwKPziyXQ64TEo1FwWwDVvjvXChhLKrRBQmcDTWhomwMDmivyk91NanZrGlfE5Vh05tMK/UZ7alJY0b2WT5BAY6Zkpc8E66rBeNAQ/2qdwtrqSJBcTWmBGDrQmB9hcaheXVXJ8vdXHeT5RKpXTUpjiIZSCbiGPRvtzFb7mkfvm+dnMyj0UzCFBqNL7SJmJTlOfeUwqouVe19uWHnh7sutlsZEkfPN3cVo42hs6RoYE2nk0P7f2hEfU0RjLmt/mKbjRG4pe8ydbeDOnHfo3Iilu19u+vn+Ot6i05ZD3nu7OsduSmweDYHFCpgq4sz9S11gjnXj+89+a7IciCeRK0ZHhbnySJP92I4RpOTqJXbwoUq503A4ggInUAhh5rl5nOS2ceIKGeQ2ttbWFw+eBk8ew4gOBg57sBnPMUz3sN7Lbsrwz/bIxe7+z4yZIc7fXaXdcOlS3RDv2/SQ6+UW4QTwo0nXymEPYfE2wsQgFox0uDw53G0uEXBVYy4enN13MAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/78330f5ceb33e6b6e0b65fe45e79fcdd/33ecc/devil-cookie.webp 360w,
/static/78330f5ceb33e6b6e0b65fe45e79fcdd/263a4/devil-cookie.webp 480w,
/static/78330f5ceb33e6b6e0b65fe45e79fcdd/601b1/devil-cookie.webp 500w&quot;
              sizes=&quot;(max-width: 500px) 100vw, 500px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/78330f5ceb33e6b6e0b65fe45e79fcdd/53918/devil-cookie.png 360w,
/static/78330f5ceb33e6b6e0b65fe45e79fcdd/9aebd/devil-cookie.png 480w,
/static/78330f5ceb33e6b6e0b65fe45e79fcdd/0eb09/devil-cookie.png 500w&quot;
            sizes=&quot;(max-width: 500px) 100vw, 500px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/78330f5ceb33e6b6e0b65fe45e79fcdd/0eb09/devil-cookie.png&quot;
            alt=&quot;문제를 해결하려면 가끔 정석적이지 않은 방법이 필요한 때도 있습니다.&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;문제를 해결하려면 가끔 정석적이지 않은 방법이 필요한 때도 있습니다.&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;이 문제를 해결하기 위해 약간의 편법을 사용합니다. &lt;code&gt;UnityEditor&lt;/code&gt; 네임스페이스에는 &lt;a href=&quot;https://docs.unity3d.com/2020.3/Documentation/ScriptReference/InitializeOnLoadMethodAttribute.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;&lt;code&gt;InitializeOnLoadMethod&lt;/code&gt; Attribute&lt;/a&gt;가 있는데, 이 Attribute를 가진 메서드는 Unity 에디터가 해당 메서드를 어셈블리에서 불러올 때마다 호출됩니다. 따라서 그러한 메서드가 &lt;code&gt;.gitconfig&lt;/code&gt; 파일을 Git 설정에 등록하는 명령을 실행하도록 하면 Unity로 프로젝트를 1회 이상 실행한 동료의 저장소에서 Hook 및 기타 설정이 모두 적용되도록 할 수 있습니다. Unity 프로젝트의 &lt;code&gt;Assets/Editor&lt;/code&gt; 디렉토리 하위 경로에 다음과 같은 파일을 작성합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;csharp&quot;&gt;&lt;pre class=&quot;language-csharp&quot;&gt;&lt;code class=&quot;language-csharp&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;using&lt;/span&gt; &lt;span class=&quot;token namespace&quot;&gt;System&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Diagnostics&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;InstallGitHooks&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token attribute&quot;&gt;&lt;span class=&quot;token class-name&quot;&gt;UnityEditor&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;InitializeOnLoadMethod&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;token return-type class-name&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;Install&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        Process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Start&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;git&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;config --local include.path ../.gitconfig&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;위 스크립트의 실행에 걸리는 소요 시간은 Unity 에디터가 로드되는 시간에 비해 지극히 짧으며 여러 번 실행되더라도 문제가 없는, 일종의 멱등적(Idempotent) 명령이기 때문에 어떠한 불편도 체감되지 않습니다. 이제 모든 문제가 해결되었습니다.&lt;/p&gt;
&lt;h2 id=&quot;hook에-관해-주의할-점&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#hook%EC%97%90-%EA%B4%80%ED%95%B4-%EC%A3%BC%EC%9D%98%ED%95%A0-%EC%A0%90&quot; aria-label=&quot;hook에 관해 주의할 점 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Hook에 관해 주의할 점&lt;/h2&gt;
&lt;p&gt;Hook을 사용하며 느낀 주관적인 주의점을 몇 가지 적어 보았습니다.&lt;/p&gt;
&lt;h3 id=&quot;잘못된-hook으로-작업이-중단되지-않도록-유의&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9E%98%EB%AA%BB%EB%90%9C-hook%EC%9C%BC%EB%A1%9C-%EC%9E%91%EC%97%85%EC%9D%B4-%EC%A4%91%EB%8B%A8%EB%90%98%EC%A7%80-%EC%95%8A%EB%8F%84%EB%A1%9D-%EC%9C%A0%EC%9D%98&quot; aria-label=&quot;잘못된 hook으로 작업이 중단되지 않도록 유의 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;잘못된 Hook으로 작업이 중단되지 않도록 유의&lt;/h3&gt;
&lt;p&gt;워터파크의 안전 요원이 잘못된 안전 수칙을 적용해 별 문제가 없는 상황에서도 조치를 가한다면 오히려 물놀이 경험이 훼손됩니다. 특히 &lt;code&gt;pre-commit&lt;/code&gt; Hook이 제대로 작동하지 않아 오류가 발생할 경우 모든 팀원의 커밋이 멈추게 되며 이는 Hook을 도입한 취지와 정면으로 위배되는 최악의 상황입니다. 충분한 테스트 코드 및 수동 검증을 통해 Hook이 팀 내에서 사용하는 모든 환경에서 제대로 작동하는 것을 확인한 후 메인 브랜치에 적용해야 합니다.&lt;/p&gt;
&lt;p&gt;드물게 &lt;a href=&quot;https://ko.wikipedia.org/wiki/%ED%9C%B4%EB%A6%AC%EC%8A%A4%ED%8B%B1_%EC%9D%B4%EB%A1%A0&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;휴리스틱&lt;/a&gt;을 사용해야 하는 경우, 오류 검출에 있어서는 &lt;a href=&quot;https://ko.wikipedia.org/wiki/%EA%B1%B0%EC%A7%93_%EC%96%91%EC%84%B1%EA%B3%BC_%EA%B1%B0%EC%A7%93_%EC%9D%8C%EC%84%B1&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;거짓 음성보다는 거짓 양성&lt;/a&gt;을 줄이는 것이 바람직합니다. Hook이 거짓 음성을 잡지 못하더라도 여전히 CI와 소프트웨어 엔지니어의 관찰력이라는 종전의 수단으로 문제를 해결할 수 있습니다.&lt;/p&gt;
&lt;p&gt;또한 Hook에서 버그 발생 시 대응할 수 있는 소프트웨어 엔지니어가 자리를 비운 상황이나 예외적으로 Hook을 우회해야 하는 상황에서 빠른 대처가 가능하도록 Hook을 건너뛸 수 있는 수단을 안내해야 합니다. Git의 &lt;code&gt;--no-verify&lt;/code&gt; 옵션이나 GUI Git 클라이언트의 Hook을 건너뛰는 메뉴 등의 사용법, 혹은 관련 문서 링크를 에러 메시지와 함께 안내하면 해결법을 애타게 찾는 상황을 피할 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;사용자의-환경에-주는-영향-최소화&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%82%AC%EC%9A%A9%EC%9E%90%EC%9D%98-%ED%99%98%EA%B2%BD%EC%97%90-%EC%A3%BC%EB%8A%94-%EC%98%81%ED%96%A5-%EC%B5%9C%EC%86%8C%ED%99%94&quot; aria-label=&quot;사용자의 환경에 주는 영향 최소화 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;사용자의 환경에 주는 영향 최소화&lt;/h3&gt;
&lt;p&gt;위에서 Unity의 &lt;code&gt;InitializeOnLoadMethod&lt;/code&gt; Attribute를 통해 아무도 몰래 Hook을 로컬 저장소에 설치하는 편법을 소개했습니다. 이러한 방법을 따른다면 해당 저장소를 사용하는 사람들은 Git 작업 시마다 Hook이 작동하는 것을 알기 어려울 것입니다. 모르는 상태에서 의도하지 않은 변경이 일어나지 않도록 Hook의 기능 및 취지에 대해 미리 공유하고 Hook이 작동할 때 로컬 저장소 밖의 파일 시스템, Git 전역 설정 등을 건드리지 않도록 영향 범위를 로컬 저장소 안쪽으로 한정해야 합니다.&lt;/p&gt;
&lt;h3 id=&quot;실수를-제재하는-것이-아니라-보완하는-방향으로&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8B%A4%EC%88%98%EB%A5%BC-%EC%A0%9C%EC%9E%AC%ED%95%98%EB%8A%94-%EA%B2%83%EC%9D%B4-%EC%95%84%EB%8B%88%EB%9D%BC-%EB%B3%B4%EC%99%84%ED%95%98%EB%8A%94-%EB%B0%A9%ED%96%A5%EC%9C%BC%EB%A1%9C&quot; aria-label=&quot;실수를 제재하는 것이 아니라 보완하는 방향으로 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;실수를 제재하는 것이 아니라 보완하는 방향으로&lt;/h3&gt;
&lt;p&gt;안전 요원은 범죄자를 잡는 형사가 아니라 안타까운 사고로 즐거운 경험이 훼손되지 않도록 돕는 사람입니다. 문제가 있는 커밋을 무작정 가로막기보다는 상세하고 이해하기 쉬운 오류 시각화를 통해 빠르게 문제를 해결하고 작업을 이어갈 수 있도록 하는 것이 바람직합니다.&lt;/p&gt;
&lt;h3 id=&quot;성능도-중요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%84%B1%EB%8A%A5%EB%8F%84-%EC%A4%91%EC%9A%94&quot; aria-label=&quot;성능도 중요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;성능도 중요&lt;/h3&gt;
&lt;p&gt;특히 프로젝트 내 파일 시스템을 순회해야 하는 작업은 프로젝트 전체 파일 및 디렉토리 개수가 늘어날수록 더 많은 시간이 소요됩니다. Git에서 발생하는 이벤트마다 긴 시간이 걸리면 이는 팀의 생산성 저하로 이어집니다. Hook 작동에 걸리는 시간이 유의미하게 늘어나기 시작하면 최적화를 고민해야 합니다.&lt;/p&gt;
&lt;h3 id=&quot;복잡한-hook은-다른-프로그래밍-언어로-작성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B3%B5%EC%9E%A1%ED%95%9C-hook%EC%9D%80-%EB%8B%A4%EB%A5%B8-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%EC%96%B8%EC%96%B4%EB%A1%9C-%EC%9E%91%EC%84%B1&quot; aria-label=&quot;복잡한 hook은 다른 프로그래밍 언어로 작성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;복잡한 Hook은 다른 프로그래밍 언어로 작성&lt;/h3&gt;
&lt;p&gt;셸 스크립트는 Unix 계통의 다양한 CLI 도구를 활용해 간단하고 효율적인 코드를 작성하기에는 용이합니다. 그러나 코드 라인 수가 늘어날수록 코드 흐름을 파악하기 어려우며 다양한 기능을 구현하기에도 적합하지 않으므로 복잡한 Hook을 작성할 경우 상황에 맞는 프로그래밍 언어를 사용하는 것이 좋습니다.&lt;/p&gt;
&lt;p&gt;Python, C#, Java 등 다양한 언어를 사용할 수 있겠지만 제 주관적인 선호는 빠르고 안전하며 현대적인 시스템 프로그래밍 언어인 Rust이며 이유는 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;실행에 별도의 런타임을 필요로 하는 언어는 해당 런타임을 같이 설치해야 하기 때문에 개발 환경을 갖추는 과정이 복잡해집니다. 물론 런타임을 같이 번들링하는 방법도 있으나 용량이 커지고 번잡합니다. Shell처럼 거의 모든 환경에서 실행 가능한 경우가 아니라면(Windows의 경우에도 보편적인 Git 클라이언트가 mingw 위의 Git Bash를 기반으로 하므로 Hook 내에서 대부분의 기본적인 Shell 명령을 수행할 수 있습니다.) 바이너리 실행 파일을 컴파일할 수 있는 언어가 편리합니다.&lt;/li&gt;
&lt;li&gt;C나 C++ 등 전통적인 시스템 프로그래밍 언어가 갖추지 못한 현대적인 패키지 매니저와 손쉬운 빌드 및 크로스 컴파일을 제공합니다.&lt;/li&gt;
&lt;li&gt;강력한 타입 시스템 및 값에 기반한 에러 핸들링 등을 통해 런타임에 발생할 수 있는 버그를 상당 부분 컴파일 타임에 예방하기에 좋은 언어입니다.&lt;/li&gt;
&lt;li&gt;기본적으로 성능이 좋고 동시성 프로그래밍에 용이해 최적화에 좋습니다.&lt;/li&gt;
&lt;li&gt;코어 모듈 및 상당수의 라이브러리가 포터블하기 때문에 OS별로 큰 추가 작업 없이 쉽게 크로스 컴파일이 가능합니다. Go도 이를 제외한 위의 장점을 다수 공유하지만 Windows 환경에서 다소 &lt;a href=&quot;https://fasterthanli.me/articles/i-want-off-mr-golangs-wild-ride&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;까다로운 점들이 있습니다&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;rust를-활용한-바이너리-hook-구현-예시&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#rust%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EB%B0%94%EC%9D%B4%EB%84%88%EB%A6%AC-hook-%EA%B5%AC%ED%98%84-%EC%98%88%EC%8B%9C&quot; aria-label=&quot;rust를 활용한 바이너리 hook 구현 예시 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Rust를 활용한 바이너리 Hook 구현 예시&lt;/h2&gt;
&lt;p&gt;마지막으로 원격 저장소로부터 풀을 받았을 때 로컬에 설치된 Unity 버전이 풀을 받은 프로젝트 버전보다 낮으면 Unity Hub에서 다운로드 팝업을 띄우는 간단한 Hook을 Rust로 작성해 보겠습니다.&lt;/p&gt;
&lt;p&gt;프로젝트에서 사용하는 Unity 버전을 업데이트할 때마다 동료들에게 현재 컴퓨터에 새 Unity 버전 설치 안내를 해야 할 필요가 있습니다. 문제는 다른 바쁜 업무나 휴가 등으로 버전 업데이트 소식을 보지 못한 동료는 버전이 업데이트됐다는 것을 놓치기 쉬우며 나중에 업데이트 안내문을 찾으려면 메일함이나 메신저를 뒤져야 해 번거롭습니다.&lt;/p&gt;
&lt;p&gt;이 Hook의 목적은 Unity 버전업이 있을 때마다 자동으로 다운로드 팝업을 띄워 이러한 불편을 해소하고자 하는 것이며 복잡한 Hook은 아니지만 프로젝트에 바이너리 Hook을 적용하는 예제로 적합하다는 생각에 간단히 작성해 보았습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;rust&quot;&gt;&lt;pre class=&quot;language-rust&quot;&gt;&lt;code class=&quot;language-rust&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;//! src/main.rs&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;token namespace&quot;&gt;anyhow&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Context&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;token namespace&quot;&gt;regex&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Regex&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;token namespace&quot;&gt;std&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;/span&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;use&lt;/span&gt; &lt;span class=&quot;token namespace&quot;&gt;std&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;PathBuf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token attribute attr-name&quot;&gt;#[derive(PartialOrd, PartialEq)]&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token type-definition class-name&quot;&gt;UnityVersion&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    major&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;u32&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    minor&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;u32&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    patch&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;u32&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    final_ver&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Option&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;u32&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    full&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    revision&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;extract_version&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;P&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;AsRef&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Path&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;P&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token namespace&quot;&gt;anyhow&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Result&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;UnityVersion&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; text &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token namespace&quot;&gt;fs&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;read_to_string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;as_ref&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; re &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Regex&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;r&quot;m_EditorVersion(WithRevision)*:[ ]*((\d+)\.(\d+)\.(\d+)(f(\d+))?)[ ]*\(([0-9a-f]+)\)&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;unwrap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; caps &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; re&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;captures&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;text&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Version not exists&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token class-name&quot;&gt;Ok&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;UnityVersion&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        major&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; caps&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;unwrap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        minor&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; caps&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;unwrap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        patch&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; caps&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;unwrap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        final_ver&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; caps&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token closure-params&quot;&gt;&lt;span class=&quot;token closure-punctuation punctuation&quot;&gt;|&lt;/span&gt;m&lt;span class=&quot;token closure-punctuation punctuation&quot;&gt;|&lt;/span&gt;&lt;/span&gt; m&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;as_str&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;parse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;unwrap&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        full&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; caps&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;to_string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        revision&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; caps&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;to_string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token namespace&quot;&gt;anyhow&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Result&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; unity_root &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;PathBuf&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;from&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token namespace&quot;&gt;std&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;var&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;UNITY_ROOT&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; remote_ver &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;extract_version&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;unity_root&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;ProjectSettings/ProjectVersion.txt&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; last_local_ver &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;extract_version&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;unity_root&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Library/PackageManager/ProjectCache&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; remote_ver &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; last_local_ver &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token namespace&quot;&gt;open&lt;span class=&quot;token punctuation&quot;&gt;::&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;that&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token macro property&quot;&gt;format!&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;token string&quot;&gt;&quot;unityhub://{}/{}&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            remote_ver&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;full&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; remote_ver&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;revision
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token class-name&quot;&gt;Ok&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;위 Hook은 환경 변수 &lt;code&gt;UNITY_ROOT&lt;/code&gt;를 Unity 프로젝트의 루트 디렉토리로 읽어 해당 디렉토리에서 프로젝트의 Unity 버전이 기록된 파일(&lt;code&gt;ProjectSettings/ProjectVersion.txt&lt;/code&gt;)과 로컬에서 마지막으로 해당 프로젝트를 실행한 버전이 기록된 캐시 파일(&lt;code&gt;Library/PackageManager/ProjectCache&lt;/code&gt;)을 파싱해 두 버전명을 기록하고 로컬 버전이 더 낮으면 Unity 허브의 커스텀 URL 스킴을 실행해 Unity 허브에서 해당 버전의 설치 페이지를 엽니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;.unwrap()&lt;/code&gt;을 여러 번 호출하고 있으나 정규식으로 캡쳐된 결과이므로 panic이 일어날 여지는 없습니다. OS에 종속적인 기능을 사용하지 않았으며 파일 경로가 Unix 스타일이긴 하지만 Rust의 &lt;code&gt;Path&lt;/code&gt;는 포터블하기 때문에 위 코드는 별다른 추가 작업 없이 크로스 컴파일이 가능합니다.&lt;/p&gt;
&lt;p&gt;예시 코드는 Rust &lt;code&gt;1.62.0&lt;/code&gt;에서 컴파일했으며 의존하는 Crate 목록은 다음과 같습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;toml&quot;&gt;&lt;pre class=&quot;language-toml&quot;&gt;&lt;code class=&quot;language-toml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# cargo.toml&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token table class-name&quot;&gt;dependencies&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;regex&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;1&quot;&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;open&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;3.0&quot;&lt;/span&gt;
&lt;span class=&quot;token key property&quot;&gt;anyhow&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;1&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 Hook은 단순해서 굳이 필요하진 않지만 복잡하거나 성능이 중요한 Hook의 경우 &lt;code&gt;lto = true&lt;/code&gt;로 링크 타임 최적화를 통해 추가적인 성능 향상을 노릴 수 있습니다.&lt;/p&gt;
&lt;p&gt;컴파일 결과로 나온 바이너리 파일을 프로젝트 저장소에 등록 후 다음과 같이 &lt;code&gt;post-merge&lt;/code&gt; Hook에서 OS에 맞는 바이너리를 실행하도록 설정합니다. 의도한 대로 원격 저장소에서 Unity 버전을 올릴 경우 풀을 받으면 Unity 허브에서 새 Unity 버전을 다운로드하는 페이지가 열리는 것을 확인할 수 있습니다. (post-pull에 해당하는 Hook은 존재하지 않으나 &lt;code&gt;git pull&lt;/code&gt; 명령 실행 시 &lt;code&gt;post-merge&lt;/code&gt; Hook이 &lt;a href=&quot;https://git-scm.com/docs/githooks#_post_merge&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;발동됩니다&lt;/a&gt;.)&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token shebang important&quot;&gt;#!/bin/bash&lt;/span&gt;

&lt;span class=&quot;token function-name function&quot;&gt;run_binary_hook&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token environment constant&quot;&gt;$OSTYPE&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt;
        linux-gnu*&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Linux 바이너리 Hook 경로&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	    darwin*&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;MacOS 바이너리 Hook 경로&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# 일반적으로 Windows에서는 Git Bash를 통해 접근하므로 msys 외의 경우는 없을 것입니다.&lt;/span&gt;
        win*&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;msys*&lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt;cygwin*&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Windows 바이너리 Hook 경로&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        *&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;${&lt;span class=&quot;token environment constant&quot;&gt;OSTYPE&lt;/span&gt;}&lt;/span&gt;: 지원하지 않는 실행환경입니다.&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;esac&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token builtin class-name&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;UNITY_ROOT&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Unity 프로젝트 루트 디렉토리 경로&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# pre-commit처럼 특정 조건을 검사하는 Hook이 아니기 때문에 에러가 발생하더라도 무시합니다.&lt;/span&gt;
run_binary_hook &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;맺음말&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%BA%EC%9D%8C%EB%A7%90&quot; aria-label=&quot;맺음말 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;맺음말&lt;/h2&gt;
&lt;p&gt;이상으로 Unity와 Git을 사용하는 쿠키런 킹덤 프로젝트에서 Git Hooks를 활용해 온 몇 가지 사례와 고민을 소개했습니다.
다소 장황하게 적다 보니 길어서 스크롤을 내리셨을 분들을 위해 본문을 세 줄로 요약하겠습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;게임 클라이언트 프로젝트에서 자주 발생하는 골칫거리 중에서는 Git Hooks를 활용해 완화할 수 있는 것들이 있습니다.&lt;/li&gt;
&lt;li&gt;여러 인원이 참여하는 프로젝트에서 Git Hooks를 동기화하기 위해 Unity Editor의 &lt;code&gt;InitializeOnLoadMethod&lt;/code&gt; Attribute를 활용하는 편법이 있습니다.&lt;/li&gt;
&lt;li&gt;Rust는 훌륭한 언어입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;읽어주셔서 감사합니다.
다음에도 쿠키런 킹덤에서 여러가지 문제를 해결한 경험을 소개하는 글로 찾아뵙겠습니다.&lt;/p&gt;
&lt;p&gt;데브시스터즈에서는 다양한 문제들을 적절한 기술로 탁월하게 같이 해결할 분들을 모집하고 있습니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[폼 데이터를 우아하게 관리하는 방법]]></title><description><![CDATA[React에 함수형 프로그래밍을 적용하여 여러 개의 데이터 상태들을 효율적으로 관리한 경험을 소개합니다.]]></description><link>https://tech.devsisters.com/posts/functional-react-state-management</link><guid isPermaLink="false">https://tech.devsisters.com/posts/functional-react-state-management</guid><pubDate>Tue, 26 Jul 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요. 저는 데브시스터즈 플랫폼셀에서 백엔드 엔지니어로 일하고 있는 이창희입니다. 저는 그동안 쌓은 대부분의 경력이 백엔드와 프론트엔드 개발을 같이하는, 흔히 말하는 풀 스택 엔지니어였는데요 오늘은 그 중 프론트엔드 개발 경험을 살려 흥미로운 이야기를 해볼까 합니다.&lt;/p&gt;
&lt;p&gt;프론트엔드 개발을 하다 보면 폼을 통해 사용자에게 값을 입력받고, 검증하고 서버로 전송해야 하는 경우가 있습니다. 얼마 전에 새로 단장한 &lt;a href=&quot;https://careers.devsisters.com/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;데브시스터즈 채용 홈페이지&lt;/a&gt;의 채용 지원 폼도 그러한데요. 이 글에서는 채용 홈페이지를 새로 만들며 입력이 많고 복잡한 폼을 함수형 라이브러리인 &lt;a href=&quot;https://gcanti.github.io/fp-ts/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;fp-ts&lt;/a&gt;를 사용하여 폼 데이터 상태 관리를 우아하게 처리한 경험에 대해 소개합니다.&lt;/p&gt;
&lt;h2 id=&quot;기본적인-폼-상태-관리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B8%B0%EB%B3%B8%EC%A0%81%EC%9D%B8-%ED%8F%BC-%EC%83%81%ED%83%9C-%EA%B4%80%EB%A6%AC&quot; aria-label=&quot;기본적인 폼 상태 관리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;기본적인 폼 상태 관리&lt;/h2&gt;
&lt;p&gt;데브시스터즈 채용 홈페이지는 React를 사용하여 개발했습니다. React에서 기본적인 상태 관리는 &lt;code&gt;React.useState&lt;/code&gt; 를 사용하는데요. 예를 들어 &lt;code&gt;React.useState&lt;/code&gt; 를 사용해 채용 지원 폼에 있는 휴대폰 번호가 올바른 휴대폰 번호인지(적어도 아주 이상한 번호는 아닌지) 검증을 한다면 아래와 같이 명령형 코드로 작성할 수 있을 것입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;mobileNumber&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setMobileNumber&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;mobileNumberError&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setMobileNumberError&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 올바른 형식의 휴대폰 번호인지 검증하는 함수&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; validateMobileNumber &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;boolean&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;setMobileNumberError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;휴대폰 번호를 입력해주세요.&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

	&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;mobileNumberRegex&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;startsWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;01&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;11&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	  &lt;span class=&quot;token function&quot;&gt;setMobileNumberError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;휴대폰 번호가 올바르지 않습니다.&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

	&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 휴대폰 번호 input의 onChange 이벤트 핸들러&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;handleMobileNumberChange&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; value &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;target&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token function&quot;&gt;validateMobileNumber&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token function&quot;&gt;setMobileNumber&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// form onsubmit 이벤트 핸들러&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;onSubmit&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; validations &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;validateMobileNumber&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;mobileNumber&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;validations&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;some&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;valid&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;valid&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// Submit Form ...&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;//&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
	&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;onChange&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;handleMobileNumberChange&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;mobileNumber&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;span&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;mobileNumberError&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;span&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;위의 코드만 보면 딱히 문제는 없습니다. 하지만, 검증해야 할 값들이 더 늘어난다면 어떻게 될까요? 원래는 한 입력에 대해 값과 오류 상태를 관리하기 위해 2개의 &lt;code&gt;React.useState&lt;/code&gt;가 필요했으니 만약 3개의 입력을 관리해야 한다면 아래와 같이 관리해야 할 상태와 작성해야 할 함수들이 비례해서 늘어나게 됩니다.&lt;/p&gt;
&lt;p&gt;하지만 3개가 아닌 4개, 5개 혹은 그보다 더 늘어난다면 어떨까요? 하나의 폼을 검증하기 위해 작성해야 하는 코드가 너무 많아질 것 같습니다. 게다가 ‘값이 필수적으로 입력되어 있어야 한다’ 같은 규칙들은 여러 필드가 공통으로 사용할 텐데, 이를 각 필드에서 매번 확인한다면 코드 중복도 높아질 것입니다. 그렇다면 이 문제는 어떻게 해결해야 할까요?&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;mobileNumber&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setMobileNumber&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;mobileNumberError&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setMobileNumberError&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setName&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;nameError&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setNameError&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;email&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setEmail&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;emailError&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setEmailError&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 검증 함수들&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; validateMobileNumber &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;boolean&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; validateName &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;boolean&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; validateEmail &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;boolean&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// input들의 onChange 이벤트 핸들러&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;handleMobileNumberChange&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;handleNameChange&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;handleEmailChange&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// form onsubmit 이벤트 핸들러&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;onSubmit&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; validations &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
		&lt;span class=&quot;token function&quot;&gt;validateMobileNumber&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;mobileNumber&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
		&lt;span class=&quot;token function&quot;&gt;validateName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
		&lt;span class=&quot;token function&quot;&gt;validateEmail&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;email&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;validations&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;some&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;valid&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;valid&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

	&lt;span class=&quot;token comment&quot;&gt;// Submit Form ...&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
	&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;onChange&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;handleMobileNumberChange&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;mobileNumber&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;span&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;mobileNumberError&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;span&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;

	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;onChange&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;handleNameChange&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;name&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;span&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;nameError&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;span&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;

	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;onChange&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;handleEmailChange&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;email&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;span&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;emailError&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;span&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;조금-더-간결하게&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A1%B0%EA%B8%88-%EB%8D%94-%EA%B0%84%EA%B2%B0%ED%95%98%EA%B2%8C&quot; aria-label=&quot;조금 더 간결하게 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;조금 더 간결하게&lt;/h2&gt;
&lt;p&gt;이런 문제를 만난다면 보통은 &lt;a href=&quot;https://react-hook-form.com/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;react-hook-form&lt;/a&gt; 같은 라이브러리를 사용하여 해결합니다. 하지만 저희는 조금 다르게 이 문제를 해결해보고 싶었고, 프로젝트에서 이미 사용 중이던 fp-ts를 사용하여 함수형 프로그래밍으로 이 문제를 해결해보기로 하였습니다.&lt;/p&gt;
&lt;p&gt;문제를 간결하게 정의하기 위해서는 공통 부분 문제와 그렇지 않은 문제를 파악해야합니다. 앞에서 예로 들었던 이름, 휴대폰 번호, 이메일에 대해 입력 상태 흐름을 일반화하면 아래와 같이 표현할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;입력값과 오류 메시지를 관리하는 두 개의 상태가 있습니다.&lt;/li&gt;
&lt;li&gt;사용자가 값을 입력하면 상태를 갱신합니다.&lt;/li&gt;
&lt;li&gt;검증 함수를 통해 사용자가 입력한 값이 올바른지 검증합니다.&lt;/li&gt;
&lt;li&gt;검증에 오류가 있다면 오류 메시지 상태를 갱신합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여기서 &lt;strong&gt;입력값&lt;/strong&gt;과 &lt;strong&gt;검증 함수&lt;/strong&gt;가 공통적이지 않고 나머지는 공통적입니다. 즉, 입력값과 검증 함수만 외부에서 주입받으면, 나머지 부분은 공통 함수에서 처리할 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;함수형-검증-함수&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%A8%EC%88%98%ED%98%95-%EA%B2%80%EC%A6%9D-%ED%95%A8%EC%88%98&quot; aria-label=&quot;함수형 검증 함수 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;함수형 검증 함수&lt;/h3&gt;
&lt;p&gt;검증 함수를 작성하기에 앞서 공통 함수를 구현합니다. 아래 &lt;code&gt;validate&amp;#x3C;T&gt;&lt;/code&gt; 함수는 검증 함수 배열과 오류 메시지를 인자로 입력받고, 검증 함수 배열에 있는 모든 함수에 입력값을 주고 실행하고, 하나라도 검증에 실패하면 오류 메시지를, 모두 성공했다면 다시 입력값을 되돌려주는 함수입니다. 이제 우리는 하나의 오류에 대해 여러 개의 검증 규칙을 묶어 사용할 수 있게 되었습니다.&lt;/p&gt;
&lt;p&gt;fp-ts에 익숙하지 않으신 분들을 위해 fp-ts의 함수와 타입에 대해 간단한 설명을 적어놓았습니다.&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;&lt;code&gt;pipe&lt;/code&gt;&lt;/summary&gt;
&lt;p&gt;&lt;code&gt;pipe&lt;/code&gt; 함수는 이름 그대로 함수들을 파이프처럼 쭉 이어주는 역할을 합니다. 예를 들어 &lt;code&gt;pipe(foo, bar)(value)&lt;/code&gt;는 &lt;code&gt;bar(foo(value))&lt;/code&gt;와 같습니다.&lt;/p&gt;
&lt;p&gt;예를 들어 아래 &lt;code&gt;pipe&lt;/code&gt; 함수를 호출하는 코드는&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token function&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;addOne&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; multiplyByTen&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이런 흐름으로 실행됩니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/e9949d329db70666ef5eb80c96b192b0/pipe.svg&quot; alt=&quot;mermaid-diagram-20220519021844.svg&quot;&gt;&lt;/p&gt;
&lt;/details&gt;
&lt;details&gt;
&lt;summary&gt;&lt;code&gt;Either&amp;lt;E, A&amp;gt;&lt;/code&gt;&lt;/summary&gt;
&lt;p&gt;&lt;code&gt;Either&amp;#x3C;E, A&gt;&lt;/code&gt; 타입은 분리 합집합 혹은 서로소 합집합을 나타내는 타입으로 &lt;code&gt;Left&amp;#x3C;E&gt;&lt;/code&gt; 혹은 &lt;code&gt;Right&amp;#x3C;A&gt;&lt;/code&gt; 타입 중 하나만을 가집니다. &lt;code&gt;T | None&lt;/code&gt; 을 표현하는 &lt;code&gt;Option&amp;#x3C;T&gt;&lt;/code&gt;와 달리 &lt;code&gt;Either&amp;#x3C;E, A&gt;&lt;/code&gt;는 &lt;code&gt;Left&amp;#x3C;E&gt;&lt;/code&gt;와 &lt;code&gt;Right&amp;#x3C;A&gt;&lt;/code&gt; 모두 null이 아닌 값을 가질 수 있어, falsy한 경우에도 값을 표현해야 하는 경우에 사용됩니다.&lt;/p&gt;
&lt;p&gt;일반적으로 Falsy한 경우를 &lt;code&gt;Left&lt;/code&gt;로, Truly한 경우를 &lt;code&gt;Right&lt;/code&gt;로 표현합니다.&lt;/p&gt;
&lt;/details&gt;
&lt;details&gt;
&lt;summary&gt;&lt;code&gt;Predicate&amp;lt;T&amp;gt;&lt;/code&gt;&lt;/summary&gt;
&lt;p&gt;&lt;code&gt;Predicate&amp;#x3C;T&gt;&lt;/code&gt; 는 &lt;code&gt;(a: T) =&gt; boolean&lt;/code&gt; 을 뜻합니다. 즉, 어떤 값에 대해서 참이냐 거짓이냐를 판별하는 제네릭 함수 타입입니다.&lt;/p&gt;
&lt;/details&gt;
&lt;details&gt;
&lt;summary&gt;&lt;code&gt;Either.fromPredicate&lt;/code&gt;&lt;/summary&gt;
&lt;p&gt;&lt;code&gt;Either.fromPredicate&lt;/code&gt; 는 &lt;code&gt;((predicate, onFalse) =&gt; ((a: A) =&gt; Either)&lt;/code&gt; 시그니처를 갖는 함수로 &lt;code&gt;predicate&lt;/code&gt; 의 실행 결과에 따라 &lt;code&gt;Left&lt;/code&gt; 혹은 &lt;code&gt;Right&lt;/code&gt; 타입을 변환시켜줍니다.&lt;/p&gt;
&lt;p&gt;만약 &lt;code&gt;predicate&lt;/code&gt;의 실행 결과가 &lt;code&gt;false&lt;/code&gt;라면 &lt;code&gt;onFalse&lt;/code&gt;의 실행 결과를 &lt;code&gt;Left&lt;/code&gt; 객체에 담아 반환하고, &lt;code&gt;true&lt;/code&gt;라면 &lt;code&gt;Right&lt;/code&gt; 객체에 &lt;code&gt;value&lt;/code&gt;를 담아 반환해 줍니다.&lt;/p&gt;
&lt;/details&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; fromPredicate &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;fp-ts/Either&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; pipe&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Predicate&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;fp-ts/function&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; every&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; map &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;fp-ts/Array&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; validate &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;validators&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Predicate&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; errorMessage&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      value&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;fromPredicate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;val&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
          &lt;span class=&quot;token function&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
            validators&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fn&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;fn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;val&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token function&quot;&gt;every&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Boolean&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; errorMessage
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;검증기-만들기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%80%EC%A6%9D%EA%B8%B0-%EB%A7%8C%EB%93%A4%EA%B8%B0&quot; aria-label=&quot;검증기 만들기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;검증기 만들기&lt;/h3&gt;
&lt;p&gt;이제 검증에 사용할 공통 함수를 만들었으니, 각 입력 필드에서 사용할 검증기를 만들어봅시다. 하지만 모두 적기에는 지루해질 것 같으니 간단하게 휴대폰 번호 필드에 대한 검증기를 만들어 보면서 앞에서 정의했던 &lt;code&gt;validate&lt;/code&gt; 함수를 어떻게 사용하고 어떻게 오류 메시지를 처리하는지 알아봅시다.&lt;/p&gt;
&lt;p&gt;먼저 검증 규칙 함수들을 선언합니다. 어떤 규칙들은 한 필드에서만 사용되지만, 많은 규칙들을 여러 필드에서 사용하기에 작은 단위로 선언하면 재사용성이 높아집니다. 이렇게 검증 규칙 함수들을 공통적으로 사용할 수 있게 정의해놓으면, 다른 값에 대한 검증기를 만들 때 편리합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; startsWith &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;search&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Predicate&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;text&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
    text&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;startsWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;search&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; minLength &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;limit&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Predicate&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;text&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
    text&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; limit&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; maxLength &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;limit&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Predicate&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;text&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
    text&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&amp;lt;=&lt;/span&gt; limit&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;testPhoneNumberPattern&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;text&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;token regex&quot;&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-source language-regex&quot;&gt;[^0-9\\s\\-]&lt;/span&gt;&lt;span class=&quot;token regex-delimiter&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token regex-flags&quot;&gt;gi&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;text&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그리고 원하는 규칙에 맞게 휴대폰 검증 함수를 정의합니다. 여기서 구현하려는 검증 규칙은 아래와 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;휴대폰 번호가 한 글자라도 입력이 되지 않았으면 필수 항목이라는 오류 메시지를 반환합니다.&lt;/li&gt;
&lt;li&gt;아래 함수들을 실행해 모두 조건을 만족하는지 확인합니다.
&lt;ul&gt;
&lt;li&gt;숫자 외에 다른 문자가 있는지 확인합니다.&lt;/li&gt;
&lt;li&gt;휴대폰 번호가 &apos;01&apos;로 시작하는지 확인합니다.&lt;/li&gt;
&lt;li&gt;번호 길이가 10자 이상인지 확인합니다.&lt;/li&gt;
&lt;li&gt;번호 길이가 11자 이하인지 확인합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;모든 조건을 만족한다면 휴대폰 번호를 &lt;code&gt;Right&amp;#x3C;string&gt;&lt;/code&gt; 타입으로 반환합니다.&lt;/li&gt;
&lt;li&gt;하나라도 만족하지 못한다면 올바르지 않은 번호 형식이라는 오류 메시지를 &lt;code&gt;Left&amp;#x3C;string&gt;&lt;/code&gt; 타입으로 반환합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;details&gt;
&lt;summary&gt;&lt;code&gt;Either.chain&lt;/code&gt;&lt;/summary&gt;
&lt;p&gt;&lt;code&gt;Either&lt;/code&gt;와 함께 &lt;code&gt;Either&lt;/code&gt;를 입력받는 콜백 함수를 입력받습니다. 입력받은 &lt;code&gt;Either&lt;/code&gt;가 &lt;code&gt;Left&lt;/code&gt;냐 &lt;code&gt;Right&lt;/code&gt;냐에 따라 함수를 실행할지 말지 결정하는 함수입니다. 만약 입력받은 &lt;code&gt;Either&lt;/code&gt;가 &lt;code&gt;Left&lt;/code&gt;라면 그대로 &lt;code&gt;Left&lt;/code&gt;를 반환하고 &lt;code&gt;Right&lt;/code&gt;라면 &lt;code&gt;Right&lt;/code&gt;에 담긴 값을 실행할 함수의 인자로 넘깁니다.&lt;/p&gt;
&lt;p&gt;아래의 예시를 보시면 이해에 도움이 되실 것 같습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; chain&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; left&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; right&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Either&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;fp-ts/lib/Either&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// left 함수는 입력받은 값을 가지는 Left 타입의 객체를 생성합니다.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// right 함수는 입력받은 값을 가지는 Right 타입의 객체를 생성합니다.&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; multiplyByTen &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;n&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Either&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;right&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;n &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Either&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;chain&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;multiplyByTen&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;func&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;CookieRun!&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 결과: {_tag: &apos;Left&apos;, left: &apos;CookieRun!&apos;}&lt;/span&gt;
&lt;span class=&quot;token builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;func&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;right&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 결과: {_tag: &apos;Right&apos;:, right: 100}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; validatePhoneNumber &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  phoneNumber&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Either&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;validate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;minLength&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;필수항목입니다.&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;phoneNumber&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 아무것도 입력되지 않았는지 검사합니다.&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;chain&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;validate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
          testPhoneNumberPattern&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 숫자 외에 다른 문자가 있는지 확인합니다.&lt;/span&gt;
          &lt;span class=&quot;token function&quot;&gt;startsWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;01&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 휴대폰 번호는 01로 시작해야합니다.&lt;/span&gt;
          &lt;span class=&quot;token function&quot;&gt;minLength&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 휴대폰 번호의 길이는 최소 10자여야합니다.&lt;/span&gt;
          &lt;span class=&quot;token function&quot;&gt;maxLength&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;11&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 휴대폰 번호의 길이는 최대 11자여야합니다.&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;&apos;올바르지 않은 번호형식입니다.&apos;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;커스텀-훅-만들기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%BB%A4%EC%8A%A4%ED%85%80-%ED%9B%85-%EB%A7%8C%EB%93%A4%EA%B8%B0&quot; aria-label=&quot;커스텀 훅 만들기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;커스텀 훅 만들기&lt;/h3&gt;
&lt;p&gt;공통 &lt;code&gt;validate&lt;/code&gt; 함수와 휴대폰 번호를 검증할 때 쓸 검증 함수까지 작성했으니, 값과 오류에 관한 상태를 아래와 같이 처리하던 것을 커스텀 훅을 만들어 간결하게 만들어봅시다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;mobileNumber&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setMobileNumber&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;mobileNumberError&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setMobileNumberError&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;먼저, 실제로 폼을 렌더링할 때 필요한 것을 생각해보면 아래와 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;React.useState&lt;/code&gt;처럼 입력값의 상태를 관리할 수 있어야 합니다.&lt;/li&gt;
&lt;li&gt;값을 검증하고 오류 메시지를 가져올 수 있어야 합니다.&lt;/li&gt;
&lt;li&gt;검증 함수를 직접 호출할 수 있어야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;여기서 검증 함수를 직접 호출할 수 있어야 하는 이유는 사용자가 폼에 어떠한 입력을 하지 않고 바로 폼을 제출하는 경우가 있기 때문에 폼을 제출하기 전 검증을 해야 하기 때문입니다.&lt;/p&gt;
&lt;p&gt;이러한 조건을 바탕으로 커스텀 훅을 만들면 아래와 같은 모양이 됩니다. 입력값과 오류에 대한 상태를 내부에서 관리하고 있으며, 넘겨받은 검증 함수를 호출하며 결과에 따라 상태를 업데이트합니다.&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;&lt;code&gt;Either.match&lt;/code&gt;&lt;/summary&gt;
&lt;p&gt;&lt;code&gt;Either&lt;/code&gt; 타입에 대해 패턴 매칭을 수행합니다. &lt;code&gt;Either&lt;/code&gt;와 콜백 함수 두 개를 인자로 받는데 첫 번째 인자로 들어오는 함수는 &lt;code&gt;Left&lt;/code&gt; 타입일 경우에, 두 번째 인자로 들어오는 함수는 &lt;code&gt;Right&lt;/code&gt; 타입일 경우에 실행됩니다.&lt;/p&gt;
&lt;/details&gt;
&lt;details&gt;
&lt;summary&gt;&lt;code&gt;function.identity&lt;/code&gt;&lt;/summary&gt;
&lt;p&gt;&lt;code&gt;(a: A) =&gt; A&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;인자로 입력받은 값을 그대로 다시 반환하는 함수입니다.&lt;/p&gt;
&lt;/details&gt;
&lt;details&gt;
&lt;summary&gt;&lt;code&gt;string.isEmpty&lt;/code&gt;&lt;/summary&gt;
&lt;p&gt;빈 문자열인지 확인합니다.&lt;/p&gt;
&lt;/details&gt;
&lt;details&gt;
&lt;summary&gt;&lt;code&gt;string.empty&lt;/code&gt;&lt;/summary&gt;
&lt;p&gt;빈 문자열입니다. &lt;code&gt;&apos;&apos;&lt;/code&gt;과 같습니다.&lt;/p&gt;
&lt;/details&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; match&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Either&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;fp-ts/Either&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; identity&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; pipe &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;fp-ts/function&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; isEmpty&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; empty &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;fp-ts/string&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;StateValidator&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function-variable function&quot;&gt;validate&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;boolean&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  error&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; useStateWithValidator &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  initialState&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token function-variable function&quot;&gt;validator&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;v&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; Either&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;v&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; t&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;boolean&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; StateValidator&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setValue&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;initialState&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setError&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;changeError&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;setError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;changeValue&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;v&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;validator&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;v&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; error&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;v&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setValue&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; empty&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      changeError
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; stateValidator&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; StateValidator &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;validate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;boolean&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;validator&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;identity&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; empty&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        changeError&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        isEmpty
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;get&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; error&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; changeValue&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; stateValidator&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 훅을 기존 코드에 적용하면 아래와 같아집니다. 많은 구현이 숨겨져 이제 컴포넌트에서 폼 상태 관리와 검증을 위해 작성해야 하는 코드가 줄었습니다. 또한 작게 나누어진 검증 규칙들을 조합해 원하는 대로 사용할 수 있고, 여기서 더 확장하면 하나의 필드에서 발생한 오류 한 가지만 보여주는 것에서 모든 오류를 보여줄 수 있습니다. (아무짝에도 쓸모없는 수 많은 사이트의 비밀번호 필드 규칙들을 생각해보세요)&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/* --------------- 새로 작성한 코드 --------------- */&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;phoneNumber&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setPhoneNumber&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; phoneNumberValidator&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;useStateWithValidator&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; validatePhoneNumber&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;onPhoneNumberChange&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setPhoneNumber&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;target&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// form onsubmit 이벤트 핸들러&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;onSubmit&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; validators &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;phoneNumberValidator&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; isNotValid &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; validators
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;validator&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; validator&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;validate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;some&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;valid&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;valid&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

	&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;isNotValid&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

	&lt;span class=&quot;token comment&quot;&gt;// Submit Form ...&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
	&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;onChange&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;onPhoneNumberChange&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;phoneNumber&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;span&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;phoneNumberValidator&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;span&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;/* --------------- 기존 코드 --------------- */&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;mobileNumber&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setMobileNumber&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;mobileNumberError&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; setMobileNumberError&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;useState&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 올바른 형식의 휴대폰 번호인지 검증하는 함수&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; validateMobileNumber &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;boolean&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;mobileNumberRegex&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	  &lt;span class=&quot;token function&quot;&gt;setMobileNumberError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;휴대폰 번호가 올바르지 않습니다.&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

	&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;startsWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;01&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	  &lt;span class=&quot;token function&quot;&gt;setMobileNumberError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;휴대폰 번호가 올바르지 않습니다.&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

	&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;11&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	  &lt;span class=&quot;token function&quot;&gt;setMobileNumberError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;휴대폰 번호가 올바르지 않습니다.&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 휴대폰 번호 input의 onChange 이벤트 핸들러&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;handleMobileNumberChange&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; value &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;target&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token function&quot;&gt;validateMobileNumber&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token function&quot;&gt;setMobileNumber&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// form onsubmit 이벤트 핸들러&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;onSubmit&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; validations &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;validateMobileNumber&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;mobileNumber&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;validations&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;some&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;valid&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;valid&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
		&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
	&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

	&lt;span class=&quot;token comment&quot;&gt;// Submit Form ...&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
	&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;input&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;onChange&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;handleMobileNumberChange&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;mobileNumber&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;span&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;mobileNumberError&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;span&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
	&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;정리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%95%EB%A6%AC&quot; aria-label=&quot;정리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;정리&lt;/h2&gt;
&lt;p&gt;따라오느라 고생하셨습니다. 이 글에서 어떤 것들을 했는지 다시 한번 돌아봅시다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fp-ts&lt;/code&gt;를 사용해 공통으로 사용할 검증 유틸 함수를 만들었습니다.&lt;/li&gt;
&lt;li&gt;검증에 사용할 검증 규칙들을 재사용성이 높은 작은 함수로 만들었습니다.&lt;/li&gt;
&lt;li&gt;검증 유틸 함수와 검증 규칙을 조합하여 입력 필드에 사용할 검증 함수를 만들었습니다.&lt;/li&gt;
&lt;li&gt;검증 함수를 쉽게 사용하기 위하여 커스텀 훅을 만들었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;단순히 폼 관련 라이브러리를 사용해도 되지만, 함수형 프로그래밍과 타입으로 같은 문제를 해결하는 과정을 지켜보시면서 어떠셨나요? 프론트엔드 개발을 하다 보면 상태 관리와 전쟁하는 것이 부지기수이지만, 함수형 프로그래밍이라는 무기를 사용하면 그 전쟁을 조금 더 수월하게 이끌어나갈 수 있는 것 같습니다. 이 글을 읽고 흥미가 생기셨다면 여러분의 프로젝트에 함수형 프로그래밍을 적용해보시는 건 어떨까요?&lt;/p&gt;
&lt;p&gt;긴 글 읽어주셔서 감사합니다.&lt;/p&gt;
&lt;p&gt;데브시스터즈에서는 다양한 문제들을 적절한 기술로 탁월하게 같이 해결할 분들을 모집하고 있습니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[바삭한 신입들의 동시성 이야기 - Kotlin 편]]></title><description><![CDATA[마이쿠키런 신입 안드로이드 개발자의 Kotlin Coroutine 탐방기]]></description><link>https://tech.devsisters.com/posts/crunchy-concurrency-kotlin</link><guid isPermaLink="false">https://tech.devsisters.com/posts/crunchy-concurrency-kotlin</guid><pubDate>Tue, 19 Jul 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요. 마이쿠키런 Android 파트 개발을 맡고 있는 박세란입니다.&lt;/p&gt;
&lt;p&gt;마이쿠키런은 데브시스터즈에서 신규 개발중인 쿠키런 팬들을 위한 플랫폼 서비스입니다. 와글와글한 커뮤니티부터 두근두근 쿠키런 오리지널 웹툰까지 다양한 컨텐츠들을 서빙해야하는 마이쿠키런 앱에 쾌적한 사용자 경험을 위한 비동기 처리는 필수적입니다. 이에 마이쿠키런 Android 팀은 Coroutine을 사용하여 신규 개발을 진행하고 있습니다.&lt;/p&gt;
&lt;p&gt;작년 여름 입사한 신입이 Coroutine을 직접 사용하며 차곡차곡 공부한 내용을 공유하고자 합니다. 이번 포스팅에서는 Kotlin Coroutine을 가독성과 성능 측면에서 다른 방법들과 비교해보고, 내부 동작 구조에 대해 알아보겠습니다.&lt;/p&gt;
&lt;h3 id=&quot;summary&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#summary&quot; aria-label=&quot;summary permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Summary&lt;/h3&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Main-Safe하다는 것은 중요한 역할을 맡고 있는 메인스레드를 블락하지 않는다는 것이다.&lt;/li&gt;
&lt;li&gt;막힘 없는 앱 환경을 제공하기 위해서는 Main-Safe하게 개발해야하고, 이를 위해 여러 스레드를 활용하여 동시성 코드를 짜야한다.&lt;/li&gt;
&lt;li&gt;동시성 프로그래밍을 할 때 콜백보다 Coroutine을 사용하는 것이 가독성이 더 좋다.&lt;/li&gt;
&lt;li&gt;CPS는 함수 호출 시에 Continuation을 전달하는 패러다임이며, Kotlin Coroutine 역시 CPS로 구현되었다. Kotlin 컴파일러는 &lt;code&gt;suspend&lt;/code&gt; 함수를 CPS 패러다임을 구현한 코드로 변환해준다.&lt;/li&gt;
&lt;li&gt;멀티 스레드보다 Coroutine을 활용하는 것이 불필요한 스레드 생성과 blocking을 없애 성능이 더 좋다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;intro&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#intro&quot; aria-label=&quot;intro permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Intro&lt;/h2&gt;
&lt;p&gt;본론으로 들어가기 전, 막연한 질문부터 던져보겠습니다. 막힘 없는 앱은 얼마나 빨라야할까요?&lt;/p&gt;
&lt;h3 id=&quot;16ms-마다-한-프레임&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#16ms-%EB%A7%88%EB%8B%A4-%ED%95%9C-%ED%94%84%EB%A0%88%EC%9E%84&quot; aria-label=&quot;16ms 마다 한 프레임 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;16ms 마다 한 프레임&lt;/h3&gt;
&lt;figure&gt;
&lt;img src=&quot;/1641b4624f276f572e99150536556dcc/2.filpbook.gif&quot; alt=&quot;text&quot;&gt;
&lt;figcaption&gt;사람이 빠르게 움직이는 이미지를 동작으로 인식하는 매커니즘 Youtube@Andymation&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;사람의 눈과 뇌는 스냅샷이나 프레임의 개념이 아닌, 사람의 눈이 보내는 신호를 뇌가 끊임없이 받아들이는 구조입니다. 즉, 연속해서 빠르게 보여지는 이미지를 동작으로 인식하는 구조입니다 (어떻게 보면 착시현상이죠). 따라서 빠르게 보여지는 이미지를 빠짐없이 모두 인식하는 것엔 한계가 있는데요. 보통 사람의 눈은 초당 60프레임인 모션과 그 이상의 모션의 큰 차이를 느끼지 못한다고 합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=CaMTIgxCSqU&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;&lt;strong&gt;1000ms/ 60 frames = 16.666 ms/ frame&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;그래서 사용자가 “와 막힘 없다!”라고 느낄 수 있는 매끄러움을 위해선 초당 60 프레임을 그려야 하는 것이고, 이를 역으로 계산해보면, 16ms마다 한 프레임을 그리는 작업(입력, 처리, 통신, 렌더링)을 완료 해야한다는 것을 의미합니다.&lt;/p&gt;
&lt;p&gt;하지만, 앱에 필요한 작업 중에서 16ms 보다 긴 처리 시간을 요구하는 작업을 흔히 찾아볼 수 있습니다. 네트워크 통신, 데이터베이스 작업, JSON 파싱 등등이 그 예 인데요. 이 긴 작업이 메인스레드에서 이뤄진다고 가정하면, 메인스레드의 본래 역할에 영향을 줄 수 있습니다. 메인스레드에서의 긴 작업은 화면 버벅임이나 ANR을 발생시켜 사용자 만족도를 떨어트리는 요인이 되곤 합니다.&lt;/p&gt;
&lt;p&gt;그러므로 앱 개발자에겐 사용자가 앱을 쓰는 동안에는 항상 &lt;strong&gt;초당 60 프레임 업데이트&lt;/strong&gt;를 유지해야 한다는 의무가 주어지고, 이를 지키기 위해 안드로이드 개발에 있어 메인스레드를 blocking하지 않는 것은 매우 중요한 일이 됩니다. 이 중요성은 UI 스레드인 메인스레드에서 네트워킹 작업을 실행하면, &lt;a href=&quot;https://developer.android.com/reference/android/os/NetworkOnMainThreadException&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;&lt;code&gt;NetworkOnMainThreadException&lt;/code&gt;&lt;/a&gt;를 발생시키며 애플리케이션을 중단하는 등 안드로이드 프레임워크가 뒷받침해 주고 있습니다. (안드로이드 3.0 이상 부터)&lt;/p&gt;
&lt;p&gt;한편 메인스레드를 blocking하지 않는 것은 왜 중요할까요 ?&lt;/p&gt;
&lt;h3 id=&quot;main-safety&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#main-safety&quot; aria-label=&quot;main safety permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Main-Safety&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 66.66666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAsTAAALEwEAmpwYAAADLElEQVR42i2T224bVRiF/TLccccVDwCFiwqpbklxk5I4sec845nx+DDOeOz4GMcncsBSUlKklgBCKERKKyGQoFQCoUCLhNQKEAIRqKB9iI9/DBdLe2trz5q11l5/6kJOJW1ZLJgqGUtj2bNZ8SyygnypgFrx0AVG1Ze9jxH+B63ikg1cKu0i39y/wdO/v+efZ49IXVRUrlo6C4bCkqOz6ptzrAnMWvJxUVBCDwOsmiAUctnny0WUsidnHiefjPjrj/s8efIwIVTmZBlbF2UmuUCUCczQRauV8COXdkNhu3Od7obGuJVl2FwmihRWRWE+sHn/qMMvj085Pz8jdUkXhabCm2IxFziizEArW6LIJ6rnuLmZ5uYww7ibxa8X6bdWOehf5d3OZSbNJSH1mc1q/HB2xJ/n35FK6wrXxGo2UefpKCV7no9bNbi1lSaMbR4fPs+D/Rewa2W+2n2R3w6eoxxazNpvEIY5+sMK3365z++/3iP1uti9XpDsPG0uX6965Eouzfoqb4nNVitPs6HhrQfYVZeCZNhp5OnFK+y2M9Qqy1Qjhy9Ox/z84x1SV9Qci5YqhCZKUdQlCksOQU3naHiJ08lLnIxfZtpapN9YY6+7yJ3JK9wdX+Cwk0aVxwvEzd2Pevz08DghXGNJCFfEdmJZF0K76syrEsUat4dXOJWPP966yDu9DJ/vvcrx+DVm3SUceThVquVI5u/dqPHo7ENS8+65UhXPQEleV2yZFUcu2xSiQKy6xLFOI8qz0dRoxzmqdQdbcnbknuZLVK7G/tshD+4dkFowNRal1HmxmdQl7+tEgw3qWy3akzbNQUzUa9CQs6izzsZmg2YvJO7WWO9ERN0Yr16l3bL4+tMdUpdNg2tiOVGpyx/zRYPBbEpvd0Rj1GJ6sMNgu8/m9ib1TsjO/pTR3hZ7hzM60wHxsEd7e0JJMv/sZCQ91NR5qZfFcjIdyv8ZJijGAW7o4IUF/HWPSiOQVSol2fk1yU5eXJHcswUDq2Ry/EFPCGWOMwVrXuxk5NSyzK/YN4TYjYq4QuSUHellgWKyl9UMTMlOlzsOSjKmnoMqlbt9q8e/i716BsGTG1oAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/d8d5f16ed1d35cca6a4f5477a09369c6/33ecc/1.staysafe_unsplash.webp 360w,
/static/d8d5f16ed1d35cca6a4f5477a09369c6/263a4/1.staysafe_unsplash.webp 480w,
/static/d8d5f16ed1d35cca6a4f5477a09369c6/0c8fb/1.staysafe_unsplash.webp 640w,
/static/d8d5f16ed1d35cca6a4f5477a09369c6/521f7/1.staysafe_unsplash.webp 860w,
/static/d8d5f16ed1d35cca6a4f5477a09369c6/cbd37/1.staysafe_unsplash.webp 1200w,
/static/d8d5f16ed1d35cca6a4f5477a09369c6/548e7/1.staysafe_unsplash.webp 2400w,
/static/d8d5f16ed1d35cca6a4f5477a09369c6/4248f/1.staysafe_unsplash.webp 3270w&quot;
              sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/d8d5f16ed1d35cca6a4f5477a09369c6/53918/1.staysafe_unsplash.png 360w,
/static/d8d5f16ed1d35cca6a4f5477a09369c6/9aebd/1.staysafe_unsplash.png 480w,
/static/d8d5f16ed1d35cca6a4f5477a09369c6/0f09e/1.staysafe_unsplash.png 640w,
/static/d8d5f16ed1d35cca6a4f5477a09369c6/3e3fe/1.staysafe_unsplash.png 860w,
/static/d8d5f16ed1d35cca6a4f5477a09369c6/64756/1.staysafe_unsplash.png 1200w,
/static/d8d5f16ed1d35cca6a4f5477a09369c6/98e2c/1.staysafe_unsplash.png 2400w,
/static/d8d5f16ed1d35cca6a4f5477a09369c6/c8b4d/1.staysafe_unsplash.png 3270w&quot;
            sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/d8d5f16ed1d35cca6a4f5477a09369c6/98e2c/1.staysafe_unsplash.png&quot;
            alt=&quot;Unsplash@jannerboy62&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Unsplash@jannerboy62&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;메인스레드가 생성되고 시작되는 곳, main 함수를 찾아가 보겠습니다. &lt;code&gt;android.app.ActivityThread&lt;/code&gt; 클래스의 main 함수에서 메인스레드가 컴포넌트 생명주기 메서드와 그 안의 메서드 호출, UI 작업을 실행한다는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;그 중 &lt;strong&gt;UI 작업은 메인스레드에서만 실행될 수 있다는 특징&lt;/strong&gt;이 있습니다. 이렇게 특정 작업이 하나의 스레드에서만 실행될 수 있게 제한하는 것을 Single-Thread 모델이라고 하는데, 자원 접근에 대한 동기화를 신경 쓰지 않아도 되고, 경쟁 상태(race condition)와 교착 상태(deadlock)도 예방할 수 있다는 이점이 있습니다. 또한 작업 전환(context switching) 비용을 요구하지 않기 때문에 애플리케이션의 응답성을 유지하는 데 도움을 줍니다. 이러한 이점을 얻기 위해, UI 작업은 메인스레드에서만 실행되도록 제한되었으며, &lt;strong&gt;UI 스레드&lt;/strong&gt;라는 용어가 생기게 되었습니다.&lt;/p&gt;
&lt;p&gt;16ms 마다 한 프레임의 이야기를 다른 관점에서 이해해보면,&lt;strong&gt;UI 작업은 메인스레드에서만 이뤄질 수 있기때문에, 메인스레드가 방해되면 원활한 UI 작업이 어려워지게 된다&lt;/strong&gt;는 점을 이해할 수 있습니다. UI는 사용자와 맞닿는 부분으로 앱 응답성과 직결되므로 중요도가 높은 작업입니다. 따라서 메인스레드가 항상 이 역할을 수행할 수 있도록 앱을 설계해야합니다.&lt;/p&gt;
&lt;p&gt;결국 Main-Safe하다는 것은, 중요한 역할을 맡은 메인스레드가 제 역할을 하도록 blocking 하지 않는 것을 의미합니다.&lt;/p&gt;
&lt;h3 id=&quot;android-concurrency&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#android-concurrency&quot; aria-label=&quot;android concurrency permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Android Concurrency&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Main-Safe하게 앱의 여러 작업을 수행하는 방법의 하나로, 동시성을 사용하는 방법&lt;/strong&gt;이 있습니다. 하지만 동시성 코드를 제대로 작성하는 것은 좀처럼 간단한 일이 아닙니다. 경쟁 상태, 원자성 위반, 교착 상태, 라이브락과 같은 동시성 이슈들이 개발자들을 괴롭히기 때문입니다. 한편 언어 자체가 동시성 코드를 작성하는 것을 어렵게 하기도 합니다. 언어 문법이 너무 장황하거나, 다양한 상황에 대응할 수 있는 유연함이 없다면 말이죠.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 56.111111111111114%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAABYlAAAWJQFJUiTwAAAByUlEQVR42qWTW3ObMBCF/f//UCYvTW/xuJlxHWLjOICNY24m2IC5SEIgThcRT9M+tjDfSLsrHZ1hxaTrO1xpVQfRtpCq1bGkuOnGuKG8GOaEznVj/eP+gcnvYNxgRRw28ewzLFwG+yiwTwS2scDa51geGI0MKxpdynXD4VeugqpXmuFRfU+nK3LUaRdjvUMPmqvu3RnVe0AO69/RKwZBqSTOTQa/imBnO82RvyGXBfxUwiKHK4/D9AUccuSnDUQYoF6tUBkGisUC1XIJ/rpH2zaYNKpBLBK45QHm+YWw4NUBEpHBiiWefImZI/Bjy2F4Dey4Ads6KGYzFPf3KL59x2U6RbU20UoxCnpliMMlQFBGmsPFQ8IzGG6G6TrG3EmJs56bXgnm2Mjnc8QPDwhJOJv/RG2SYDMI9hLOyYUZbrAeiMhl+IyoOsF4rTF9yfFpEeDOCDG1LnjyGNhmg7cvX+Hc3sK6uUF49xnV4yMJ8tFhKnOk7Z/ksoRH33B3ktRhrtklUudE4KO2bJT2FQts747f8O97dEV3nnqndP+URse6/jHb0ws96i63naSr8O8MP0JRJKhlhY7u8YTxGv9DzRjSo4+izsE5wy9JzUWgyC4NQwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/96d0683110faaf8ff79cf0901ea738d5/33ecc/3.UXR_googleio.webp 360w,
/static/96d0683110faaf8ff79cf0901ea738d5/263a4/3.UXR_googleio.webp 480w,
/static/96d0683110faaf8ff79cf0901ea738d5/0c8fb/3.UXR_googleio.webp 640w,
/static/96d0683110faaf8ff79cf0901ea738d5/521f7/3.UXR_googleio.webp 860w,
/static/96d0683110faaf8ff79cf0901ea738d5/cbd37/3.UXR_googleio.webp 1200w,
/static/96d0683110faaf8ff79cf0901ea738d5/548e7/3.UXR_googleio.webp 2400w,
/static/96d0683110faaf8ff79cf0901ea738d5/80d4f/3.UXR_googleio.webp 3354w&quot;
              sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/96d0683110faaf8ff79cf0901ea738d5/53918/3.UXR_googleio.png 360w,
/static/96d0683110faaf8ff79cf0901ea738d5/9aebd/3.UXR_googleio.png 480w,
/static/96d0683110faaf8ff79cf0901ea738d5/0f09e/3.UXR_googleio.png 640w,
/static/96d0683110faaf8ff79cf0901ea738d5/3e3fe/3.UXR_googleio.png 860w,
/static/96d0683110faaf8ff79cf0901ea738d5/64756/3.UXR_googleio.png 1200w,
/static/96d0683110faaf8ff79cf0901ea738d5/98e2c/3.UXR_googleio.png 2400w,
/static/96d0683110faaf8ff79cf0901ea738d5/0996e/3.UXR_googleio.png 3354w&quot;
            sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/96d0683110faaf8ff79cf0901ea738d5/98e2c/3.UXR_googleio.png&quot;
            alt=&quot;Understand Kotlin Coroutines on Android (Google I/O&amp;#39;19)&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Understand Kotlin Coroutines on Android (Google I/O&amp;#39;19)&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=BOHK_w09pVA&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Google I/O&apos;19&lt;/a&gt; 에 따르면, 구글은 매년 개발자 대상으로 그들이 어떻게 지내는지, 그들의 문제는 무엇인지 설문 조사를 한다고 합니다. 2018 Developer Survey 를 통해서 많은 개발자가 Threading, Concurrency에 어려움을 겪고 있다는 것을 파악한 구글은 더 나은 액션을 정의하기 위해 Concurrency UXR (User Experience Research)를 진행했습니다. UXR을 통해 RxJava, LiveData, Coroutine과 같은 기존 솔루션들을 검토하며 알게 된 개발자들의 니즈를 바탕으로, 구글은 2가지 액션을 취하기로 했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 56.666666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAABYlAAAWJQFJUiTwAAAC+0lEQVR42h2TTUyTdxzHn/fn6dNXWrG0RVyhxWLFxsA2KNRKgQIjwJgvBNGhoxgl0YWIKc4djCNdFUXLa4hbth22k4vRZNl9Oy3bssuO26K7eTHRi4lRP/t3h2/+h//39/3+XqXaeAhFlpEFNFnCa5r4nC52+eoI2j5CTj91Lj81mod4Ik7ndJauUz1kCr1kz/XT+/EQ+YURRq4epqfQh1T44TS76xpIJVO8E4uQb9nJ9GgXs5M5xruT9KeiDB5oonNfG4cXjnDzyTqlv1coP65w/fEq1/9do/xojZVnd7n44BOkga0viUea6T3Uw1hfG8eyMS5MdHN+KseHQ+2MdMT4INPCcC7HkeIMl//8mku/bFL8bZPF3ze4/Mc6i7/e5to/2xS25pGai4/YG93L+Fie2aNZrpzK8PmloyyeHeHK7BALk91MDXYwNpgme2aD1jvQufqS7vUXZLaec3D7Gdm7T8l884L03DbS+3Nlbs6Ncm95hlvFaR6WT3C/cpYfb33E8myeyewB5k8Oc2ZmmBNL39OyDF2V16TX3pD9Ag4KtFdeEatA6uQWUirewEBbM8fzbRyfGKZcPM3D70r8dK/ExtUC5471MTGQZuX2AjdWrhF9e5LCp5sUPvuW+v4lklNfMV76mdHKX6Teu4ikyAqyJKPKppi0isctptkUZU+imfpImGQigcfp5lCmnXzuXbyWRWtjLUPp/TT4aomHo6yWLnBjaZ5ka0dVUBNro2LpJoaq4NRtAq6AMJFwqDqWauPQd2ILjltRhbGCZdm4TK9YsRpqfWGadjXSEksQCb2FpAuiKjKsvn63F4/hxjYcGIqCrsi4VVOY2ejCLOCw8Ds8uFxe6neEqLO82KpGKBDGoZmYIgnJ43GyOxAUi6vh0g0hpAkIAyFYFVXkaqaiFYZBjeVmT7gR43+eimlaKOLP0HUkIRbwB5GCTpt9kSa8DqcgiWBBdAmCJaCJ6zGFQVQIBIWhz1Ejgk3sKjQDU1SliRZUxarXZukW/wESSnhjeLmy5wAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/7059c9bd25cefdb3373881faedfe42d1/33ecc/4.actions_googleio.webp 360w,
/static/7059c9bd25cefdb3373881faedfe42d1/263a4/4.actions_googleio.webp 480w,
/static/7059c9bd25cefdb3373881faedfe42d1/0c8fb/4.actions_googleio.webp 640w,
/static/7059c9bd25cefdb3373881faedfe42d1/521f7/4.actions_googleio.webp 860w,
/static/7059c9bd25cefdb3373881faedfe42d1/cbd37/4.actions_googleio.webp 1200w,
/static/7059c9bd25cefdb3373881faedfe42d1/548e7/4.actions_googleio.webp 2400w,
/static/7059c9bd25cefdb3373881faedfe42d1/fc9f0/4.actions_googleio.webp 3384w&quot;
              sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/7059c9bd25cefdb3373881faedfe42d1/53918/4.actions_googleio.png 360w,
/static/7059c9bd25cefdb3373881faedfe42d1/9aebd/4.actions_googleio.png 480w,
/static/7059c9bd25cefdb3373881faedfe42d1/0f09e/4.actions_googleio.png 640w,
/static/7059c9bd25cefdb3373881faedfe42d1/3e3fe/4.actions_googleio.png 860w,
/static/7059c9bd25cefdb3373881faedfe42d1/64756/4.actions_googleio.png 1200w,
/static/7059c9bd25cefdb3373881faedfe42d1/98e2c/4.actions_googleio.png 2400w,
/static/7059c9bd25cefdb3373881faedfe42d1/5f4fe/4.actions_googleio.png 3384w&quot;
            sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/7059c9bd25cefdb3373881faedfe42d1/98e2c/4.actions_googleio.png&quot;
            alt=&quot;Understand Kotlin Coroutines on Android (Google I/O&amp;#39;19)&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Understand Kotlin Coroutines on Android (Google I/O&amp;#39;19)&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;First Class Coroutines Support : 제트팩에서 Coroutine에 대한 최고 수준의 품질 지원&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;RxJava Guidance in Docs : 공식문서에 RxJava에 대한 더 많은 지원&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이 중에서 구글으로부터 &lt;strong&gt;최고 수준의 품질 지원을 받는 Coroutine&lt;/strong&gt;에 집중해볼 예정인데요. Kotlin Coroutine은 어떤 특징이 있는지 살펴보면서 왜 Coroutine이 선택되었는지 알아보도록 하겠습니다.&lt;/p&gt;
&lt;h2 id=&quot;kotlin-coroutine&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#kotlin-coroutine&quot; aria-label=&quot;kotlin coroutine permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Kotlin Coroutine&lt;/h2&gt;
&lt;h3 id=&quot;가독성-이야기--callback-vs-coroutine&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B0%80%EB%8F%85%EC%84%B1-%EC%9D%B4%EC%95%BC%EA%B8%B0--callback-vs-coroutine&quot; aria-label=&quot;가독성 이야기  callback vs coroutine permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;가독성 이야기 : Callback vs Coroutine&lt;/h3&gt;
&lt;p&gt;콜백은 백그라운드 스레드를 사용하여 긴 작업을 실행하고, 백그라운드 스레드에서 작업이 완료되면 메인스레드에 정의된 콜백을 호출하여 작업 결과를 알려주는 방법입니다. 긴 작업의 완료 여부를 메인스레드가 busy waiting하고 있지 않기 때문에, 기본적으로 콜백 구조를 사용하면 Main-Safe하게 긴 작업을 처리할 수 있습니다. 한편, 콜백은 Main-Safe한 작업을 하게 해주지만, 반복적인 들여쓰기로 인해 “가독성이 안 좋아질 수 있다”는 단점을 가지고 있습니다. 흔히 콜백 지옥이라고 불리기도 하는데요. 콜백 지옥은 코드를 추론하기 어렵게 만드는 요인 중 하나가 되기도 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// 순차적인 네트워크 호출을 나타내는 코드&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getGingerBrave&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;api&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CookieService&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; gingerBrave &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;makeDough&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; dough &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;
        api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addMagicPowder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dough&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; magicDough
            api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;escapeOven&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;magicDough&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; cookie
                api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fetchGingerBrave&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cookie&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; gingerBrave
                    Log&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;You can&apos;t catch me! I&apos;m the Gingerbre.. I&apos;m Ginger Brave!&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
                    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; gingerBrave
                &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;콜백과 Coroutine은 Main-Safe한 작업을 하게 해준다는 점에서 같은 일을 하지만, Coroutine을 사용한다면 위에서 언급한 콜백의 가독성 문제를 없앨 수 있습니다. &lt;strong&gt;Coroutine은 콜백 기반 코드를 sequential code로 바꾸어주기 때문에 비동기 코드를 단순화할 수 있습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;suspend&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getGingerBrave&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;api&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CookieService&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; gingerBrave &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; dough &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;makeDough&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; magicDough &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addMagicPowder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dough&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; cookie &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;escapeOven&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;magicDough&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; gingerBrave &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fetchGingerBrave&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cookie&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    Log&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;You can&apos;t catch me! I&apos;m the Gingerbre.. I&apos;m Ginger Brave!&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; gingerBrave
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;코드를 보면서 Coroutine과 콜백을 가독성 측면에서 비교해보도록 하겠습니다. 동기 코드처럼 표현된 Coroutine에서는 들여쓰기가 사라져 코드 가독성이 좋아졌으며, 중간의 콜백 람다 블록들이 사라져 현재 위치를 더 추론하기 쉬워졌습니다. 코드의 가독성과 추론성은 예측 가능한 프로그램을 만드는 데에 도움을 준다는 점에서 에러 핸들링과 디버깅과 같은 작업에서 큰 이점을 줍니다.&lt;/p&gt;
&lt;p&gt;그럼 Coroutine은 어떻게 콜백 없이 동작할 수 있는걸까요? 간단히 요약하자면, 놀랍게도 Kotlin의 &lt;code&gt;suspend&lt;/code&gt; 키워드는 내부적으로 콜백을 생성합니다. 즉, &lt;code&gt;suspend&lt;/code&gt; 키워드를 만난 Kotlin 컴파일러는 suspend-resume을 위한 최적화된 콜백 코드를 생성합니다. 한번 변환 과정을 살펴볼까요?&lt;/p&gt;
&lt;h3 id=&quot;suspend의-속사정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#suspend%EC%9D%98-%EC%86%8D%EC%82%AC%EC%A0%95&quot; aria-label=&quot;suspend의 속사정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;code&gt;suspend&lt;/code&gt;의 속사정&lt;/h3&gt;
&lt;p&gt;Kotlin 컴파일러가 &lt;code&gt;suspend&lt;/code&gt; 키워드를 만나면 어떻게 동작하는지 간단하게 모방해보겠습니다. 생소하게 느껴질 수 있는 개념이어서, 임의로 4개의 단계로 나누어 설명합니다.&lt;/p&gt;
&lt;h4 id=&quot;step-1-cps-패러다임---continuation-객체를-사용하다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#step-1-cps-%ED%8C%A8%EB%9F%AC%EB%8B%A4%EC%9E%84---continuation-%EA%B0%9D%EC%B2%B4%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8B%A4&quot; aria-label=&quot;step 1 cps 패러다임   continuation 객체를 사용하다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Step 1. CPS 패러다임 - Continuation 객체를 사용하다!&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Coroutine에는 CPS(Continuation Passing Style) 패러다임이 적용되어 있습니다.&lt;/strong&gt; CPS란 호출되는 함수에 Continuation을 전달하고, 각 함수의 작업이 완료되는 대로 전달받은 Continuation을 호출하는 패러다임을 의미합니다. 이러한 의미에서 Continuation을 일종의 콜백으로 생각할 수 있습니다.&lt;/p&gt;
&lt;p&gt;그럼 이 Continuation이 무엇이고, CPS에서 Continuation을 전달하는 이유는 무엇일까요?&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;/**
 * Interface representing a continuation after a suspension point that returns a value of type `T`.
 */&lt;/span&gt;
&lt;span class=&quot;token annotation builtin&quot;&gt;@SinceKotlin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;1.3&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; Continuation&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; T&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;/**
     * The context of the coroutine that corresponds to this continuation.
     */&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; context&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CoroutineContext

    &lt;span class=&quot;token comment&quot;&gt;/**
     * Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the
     * return value of the last suspension point.
     */&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;resumeWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;result&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Result&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;T&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/src/kotlin/coroutines/Continuation.kt&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/src/kotlin/coroutines/Continuation.kt&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Continuation은 다음에 무슨 일을 해야 할지 담고 있는 확장된 콜백입니다. 명확한 이해를 위해, Kotlin에 정의된 Continuation 인터페이스를 확인해봐야겠죠? Continuation 인터페이스엔 크게 &lt;code&gt;context&lt;/code&gt; 객체와 &lt;code&gt;resumeWith&lt;/code&gt;라는 함수가 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;resumeWith&lt;/code&gt;는 특정 함수 a가 suspend 되어야 할 때, 현재 함수에서 a의 결과 값을 T로 받게 해주는 함수입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;context&lt;/code&gt;는 각 Continuation이 특정 스레드 혹은 스레드 풀에서 실행되는 것을 허용해줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;감이 오시나요? CPS에선 Continuation을 전달하면서, 현재 suspend된 부분에서의 resume을 가능하게 해줍니다. &lt;strong&gt;Continuation은 resume되었을 때의 동작 관리를 위한 객체로, 연속적인 상태간의 communicator&lt;/strong&gt;라고 생각할 수 있습니다.&lt;/p&gt;
&lt;p&gt;요약하자면 Continuation은 호출 함수간의 suspend-resume을 위한 communicator이고, CPS는 함수 호출 시에 이 Continuation을 전달하는 패러다임입니다. Kotlin Coroutine 역시 CPS로 구현되었습니다.&lt;/p&gt;
&lt;h4 id=&quot;step-2-suspend-fun의-시그니처가-변경되다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#step-2-suspend-fun%EC%9D%98-%EC%8B%9C%EA%B7%B8%EB%8B%88%EC%B2%98%EA%B0%80-%EB%B3%80%EA%B2%BD%EB%90%98%EB%8B%A4&quot; aria-label=&quot;step 2 suspend fun의 시그니처가 변경되다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Step 2. &lt;code&gt;suspend&lt;/code&gt; fun의 시그니처가 변경되다!&lt;/h4&gt;
&lt;p&gt;Continuation은 Kotlin 컴파일러가 &lt;code&gt;suspend&lt;/code&gt; 함수의 시그니처를 변경하면서 사용됩니다.&lt;/p&gt;
&lt;p&gt;(1) Continuation 객체가 기존 &lt;code&gt;suspend&lt;/code&gt; 함수의 파라미터에 추가되고 &lt;code&gt;suspend&lt;/code&gt; 키워드가 사라집니다.&lt;/p&gt;
&lt;p&gt;(2) 이 객체는 &lt;code&gt;suspend&lt;/code&gt; 함수 계산의 결과를 호출한 Coroutine에 전달하는 데 사용됩니다.&lt;/p&gt;
&lt;p&gt;(3) 또한 함수의 리턴 타입 또한 gingerBrave에서 Unit으로 변경되었는데요. 결과는 resume 함수의 매개변수를 통해 얻을 수 있게 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;suspend&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getGingerBrave&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;api&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CookieService&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; gingerBrave &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// (1)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; dough &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;makeDough&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; magicDough &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addMagicPowder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dough&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; cookie &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;escapeOven&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;magicDough&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; gingerBrave &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fetchGingerBrave&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cookie&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    Log&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;You can&apos;t catch me! I&apos;m the Gingerbre.. I&apos;m Ginger Brave!&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; gingerBrave &lt;span class=&quot;token comment&quot;&gt;//(2)(3)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getGingerBrave&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;api&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CookieService&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; completion&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Continuation&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Any&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// (1)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; dough &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;makeDough&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; magicDough &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addMagicPowder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dough&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; cookie &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;escapeOven&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;magicDough&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; gingerBrave &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fetchGingerBrave&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cookie&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    Log&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;You can&apos;t catch me! I&apos;m the Gingerbre.. I&apos;m Ginger Brave!&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    completion&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;resume&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;gingerBrave&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;//(2)(3)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&quot;step-3-state-machine---suspension-points를-기준으로-코드-블록이-구분되다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#step-3-state-machine---suspension-points%EB%A5%BC-%EA%B8%B0%EC%A4%80%EC%9C%BC%EB%A1%9C-%EC%BD%94%EB%93%9C-%EB%B8%94%EB%A1%9D%EC%9D%B4-%EA%B5%AC%EB%B6%84%EB%90%98%EB%8B%A4&quot; aria-label=&quot;step 3 state machine   suspension points를 기준으로 코드 블록이 구분되다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Step 3. State machine - Suspension Points를 기준으로 코드 블록이 구분되다!&lt;/h4&gt;
&lt;p&gt;자 그래서 대체 어떻게 suspend, resume 연산이 가능한 걸까요?&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getGingerBrave&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;api&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CookieService&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; completion&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Continuation&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Any&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; dough &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;makeDough&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// suspending fun&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; magicDough &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addMagicPowder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dough&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// suspending fun&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; cookie &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;escapeOven&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;magicDough&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// suspending fun&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; gingerBrave &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fetchGingerBrave&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cookie&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// suspending fun&lt;/span&gt;
    Log&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;You can&apos;t catch me! I&apos;m the Gingerbre.. I&apos;m Ginger Brave!&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    completion&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;resume&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;gingerBrave&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getGingerBrave&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;api&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CookieService&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; completion&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Continuation&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Any&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;label&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Label 0 -&gt; first execution !&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; dough &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;makeDough&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// suspend&lt;/span&gt;

        &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Label 1 -&gt; resume from makeDough&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; magicDough &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addMagicPowder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dough&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// suspend&lt;/span&gt;

        &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Label 2 -&gt; resume from addMagicPowder&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; cookie &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;escapeOven&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;magicDough&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// suspend&lt;/span&gt;

        &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Label 3 -&gt; resume from escapeOven&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; gingerBrave &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fetchGingerBrave&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cookie&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// suspend&lt;/span&gt;

        &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Label 4 -&gt; resume from fetchGingerBrave&lt;/span&gt;
            Log&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;You can&apos;t catch me! I&apos;m the Gingerbre.. I&apos;m Ginger Brave!&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            completion&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;resume&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;gingerBrave&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Kotlin은 모든 중단 가능 지점을 찾아 when으로 표현&lt;/strong&gt;합니다. Kotlin 컴파일러는 함수가 내부적으로 중단 가능 지점을 식별하고, 이 지점으로 코드를 분리합니다. 분리된 코드들은 부여된 각각의 label로 인식되며, when을 사용하여 작성됩니다. (label에 대한 설명은 아래에서 계속됩니다.)&lt;/p&gt;
&lt;p&gt;이렇게 코드를 명확히 구분해두어, 중단 가능 지점에서 suspend되고 해당 작업이 끝나 resume되면 다시 다음 블록을 실행하게 될 수 있게 합니다. 이런 방식은 상태를 관리하는 하나의 방법으로 상태머신(state machine)이라고 불리기도 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 144px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 86.1111111111111%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAARCAYAAADdRIy+AAAACXBIWXMAABYlAAAWJQFJUiTwAAABmklEQVR42q2USUsDQRCF/Q0KbpcRFCGTWbt7RrOpYxSjoCYhCWpunlxBJIIJingXBhURRUREEEQkePAQ3PF/PWca5qAEsziHR/eh+LrqVXW16NSAn2qpGcT8BBIDVGb8/D/QhRgm+haHoAwNQNcYCDO5Ggc6ZVKJoj8fQ6edAOEZmpA1AlWnf0KrA00TRpBB2IpDKIzyu6wTpDM5WPFxBGUV1BioHygqKqReCcLGCITSGGiQQujtw/LqOq6ub2AOhiEpWtVMfwA1wvjLmbl5LGTz0HIW2g7GMGzFkcnOobizi+fXd5ycniE2bFUt/wfQDQhFYngoP+L98wvFQgmtpSjo5jTKt2U8VSq4u3/Ay9sHdvf2IUrK30Avw5lkGulsDrOTSYipCDrsCe7ddnGHw+zDY4TCUR5fl4eu6a4kSYUWMtFtT6FHD2BlaQ3nF5c8K1nVa3voyZs34oyK2/GugwSCMQOz0ynunduQhrrszSJxfknAKbn9KMHBipOVSpqZQw+oMojJCJd7b/6n/IIShfm8bZif66vBffgNEpI7UpH9xNgAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/a4ac882b1f853198c078fcab46bdac16/e72e6/suspend_arrow.webp 144w&quot;
              sizes=&quot;(max-width: 144px) 100vw, 144px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/a4ac882b1f853198c078fcab46bdac16/de2c4/suspend_arrow.png 144w&quot;
            sizes=&quot;(max-width: 144px) 100vw, 144px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/a4ac882b1f853198c078fcab46bdac16/de2c4/suspend_arrow.png&quot;
            alt=&quot;중단 가능 지점을 표현하는 화살표&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;중단 가능 지점을 표현하는 화살표&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;한편 중단 가능 지점은 Android Studio, IntelliJ IDE의 화살표 표시로도 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;요약하면, Kotlin 컴파일러는 &lt;code&gt;suspend&lt;/code&gt; 함수 내부를 상태머신으로 표현합니다. &lt;code&gt;suspend&lt;/code&gt; 함수를 호출할 때마다 구역을 나누고, labeling하여 when문으로 표현합니다. 한편 현재 실행 차례를 가리키는 label과, &lt;code&gt;suspend&lt;/code&gt; 함수 내부에 선언된 변수들을 어떻게 관리될까요?&lt;/p&gt;
&lt;h4 id=&quot;step-4-state-machine---label과-각-suspend-fun-내부-변수들이-관리되다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#step-4-state-machine---label%EA%B3%BC-%EA%B0%81-suspend-fun-%EB%82%B4%EB%B6%80-%EB%B3%80%EC%88%98%EB%93%A4%EC%9D%B4-%EA%B4%80%EB%A6%AC%EB%90%98%EB%8B%A4&quot; aria-label=&quot;step 4 state machine   label과 각 suspend fun 내부 변수들이 관리되다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Step 4. State machine - label과 각 &lt;code&gt;suspend&lt;/code&gt; fun 내부 변수들이 관리되다!&lt;/h4&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getGingerBrave&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;api&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CookieService&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; completion&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Continuation&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Any&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;GetGingerBraveStateMachine&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;completionL Continuation&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Any&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;CoroutineImpl&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;completion&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// 기존 함수 내부에 선언된 변수들&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; dough&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Dough&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; magicDough&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; MagicDough&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; cookie&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Cookie&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; gingerBrave&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; GingerBrave&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;

        &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; result&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Any&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; label&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Int &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;

        &lt;span class=&quot;token keyword&quot;&gt;override&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;invokeSuspend&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;result&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Any&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;result &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; result
            &lt;span class=&quot;token function&quot;&gt;getGingerBrave&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;api&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;this&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Kotlin 컴파일러는 &lt;code&gt;getGingerBrave&lt;/code&gt; 함수안에 &lt;code&gt;GetGingerBraveStateMachine&lt;/code&gt;이라는 클래스를 생성합니다. 이 클래스는 &lt;code&gt;Continuation&lt;/code&gt;의 자식인 &lt;code&gt;CoroutineImpl&lt;/code&gt;을 구현하고 있으며, 원래 suspend 함수에 선언된 변수들과, 실행 결과인 result, 현재의 진행 상태인 label을 가지고 있습니다. 이 클래스가 어떻게 사용되는지 아래 코드에서 확인해보겠습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getGingerBrave&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;api&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CookieService&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; completion&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Continuation&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Any&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; continuation &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; completion &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; GetGingerBraveStateMachine &lt;span class=&quot;token operator&quot;&gt;?:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;GetGingerBraveStateMachine&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;completion&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;when&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;continuation&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;label&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;// Label 0 -&gt; first execution !&lt;/span&gt;
            &lt;span class=&quot;token function&quot;&gt;throwOnFailure&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;continuation&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            continuation&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;label &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
            api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;makeDough&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;continuation&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// suspend&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;// Label 1 -&gt; resume from makeDough&lt;/span&gt;
            &lt;span class=&quot;token function&quot;&gt;throwOnFailure&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;continuation&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            continuation&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;dough &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; continuation&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;result &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; Dough
            continuation&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;label &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;
            api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addMagicPowder&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;continuation&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;dough&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; continuation&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// suspend&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token comment&quot;&gt;// Label 2 -&gt; resume from addMagicPowder&lt;/span&gt;
            &lt;span class=&quot;token function&quot;&gt;throwOnFailure&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;continuation&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            continuation&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;magicDough &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; continuation&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;result &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; MagicDough
            continuation&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;label &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;
            api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;escapeOven&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;continuation&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;magicDough&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; continuation&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// suspend&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Label 3 -&gt; resume from escapeOven&lt;/span&gt;
            &lt;span class=&quot;token function&quot;&gt;throwOnFailure&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;continuation&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            continuation&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cookie &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; continuation&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;result &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; Cookie
            continuation&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;label &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;
            api&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fetchGingerBrave&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;continuation&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;cookie&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; continuation&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// suspend&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Label 4 -&gt; resume from fetchGingerBrave&lt;/span&gt;
            &lt;span class=&quot;token function&quot;&gt;throwOnFailure&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;continuation&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;result&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            continuation&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;gingerBrave &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; continuation&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;result &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; GingerBrave
            Log&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;You can&apos;t catch me! I&apos;m the Gingerbre.. I&apos;m Ginger Brave!&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            completion&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;resume&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;continuation&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;gingerBrave&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; IllegalStateException
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Step 3에서 중단 가능 지점을 기준으로 코드를 나눈 이유가 여기서 더 와닿으실 것 같습니다. 컴파일러가 생성해준 &lt;code&gt;GetGingerBraveStateMachine&lt;/code&gt;의 label이 when 절안에서 사용되는데요. suspend 지점에 따라 코드를 나눴고 그것을 label로 구분했으니, 이제 resume 을 위해 label을 사용할 차례입니다. 각 label 분기에선 label의 값을 증분 시켜 다음 실행 지점을 가리키고, 이를 통해 resume이 됩니다.&lt;/p&gt;
&lt;p&gt;한편 &lt;code&gt;GetGingerBraveStateMachine&lt;/code&gt;을 살펴보면 기존 &lt;code&gt;suspend&lt;/code&gt; 함수 내부에 선언된 변수들이 null로 초기화 되어있는 모습을 확인할 수 있는데요. 또한 when 절안에서 resume시 suspend되고 실행된 작업의 결과가 &lt;code&gt;continuation.result&lt;/code&gt;로 받아오는 모습도 확인할 수 있습니다. &lt;code&gt;continuation.result&lt;/code&gt;를 각 타입으로 캐스팅하여 함수 내부에서 선언된 변수들을 관리하고 있습니다. 이렇게 각 블록을 하나하나 실행할 때마다 함수의 상태가 점점 누적되고 마지막 resume을 통해 최종 결과 값이 전달되게 됩니다. 이렇게 하나의 Coroutine이 끝나게 됩니다.&lt;/p&gt;
&lt;p&gt;지금까지 Kotlin 컴파일러가 &lt;code&gt;suspend&lt;/code&gt; 키워드를 만났을때 어떻게 동작하는지를 4단계로 알아보았습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Coroutine은 Continuation을 주고 받는 CPS 패러다임을 사용합니다.&lt;/li&gt;
&lt;li&gt;Kotlin 컴파일러는 &lt;code&gt;suspend&lt;/code&gt; fun의 시그니처를 변경합니다. 매개변수에 Continuation을 추가합니다.&lt;/li&gt;
&lt;li&gt;Kotlin 컴파일러는 &lt;code&gt;suspend&lt;/code&gt; fun의 내부 코드들을 분석하여 중단 가능 지점을 찾아 구분합니다.&lt;/li&gt;
&lt;li&gt;Kotlin 컴파일러는 다음 실행 지점을 나타내는 label과 내부 변수들을 관리하는 상태머신 클래스를 생성합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1920px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 56.388888888888886%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAACuklEQVR42kWSS09TURSFL20p5T4K1kYQBaXFihFBEChUCrSAFEMFIioiIUqjIEas0CDKyyhPCQIVMCJBosYoGnWiicaBzvwHxr/i5PP0Ajo4j5xkf2etvZekGGQUg4Iszs2ViBwnYzNrpFvt7DQnYTVqqFI8ToeLnunHhOfW6Yy+ZvHFR4LVNSRKCTpDFfVSbFO3gDF4DJZsUggcyaKlJIdAjou9qg1FFDnTMxmcX2Fk6SW3lt4ws/4ef5kXi2TRGaoQI21elK0fFDSDym4lmeY8F7OnA3R483HssGEVCg/sdxC694je0YeMR58x/nQDj7sEJS4GVEWt/B8YW1aDJh4txEsSNS4nT0KttJbksitBZofBRK7TybWBIa4NTdI7tsCDhRX8nuOo/4AKkrJlWTOqok8JZKXsoyc8wfXLt+lvCOIr9GIzGLEbzeQ4srjQ00cw1EX1uSb6J5eprfAhS2bUWJ+3Fep2jbKwZeZmb5RPP/+wsvqd5VvTRFd+EPA3YI+TyMzIorJ7mNbwAJXBOhrF3V/uI0EyoRi3FMY2zaRhFjYLst3MzH1hJvqV5+9+sbbxm1cffjN8d42UeAsHM1xUXJ+id3KR5sgMTZF5yoqKxZQ3FVq3gaqgJ4qmt52/zcTsN4YGllkaWWT+wVsm5z8zNvGCisICqvJy8HrKcbs9HPVWU+RroCj7kEiARXepA3XLIipWo0J7+xgD4QXOHiviaqCZ0cgsrS0RBkdfczHUx7HCekoLqklVbKTJNmqKi8k/4MJuSdLjpk95eyiqeEhRUinJ89F0sou+G2FWn97nSvsprnZ4uNldRkeons5L5XhzTdR6NKpKc0nfmYZdTtaB6rZCVQ+1qkvfvydPZOsMjbU1tDQKeN1hIm0OpiJ+7oTrCfpSOeG20ODX8LkzSUtOJSle1QXFpvwXivVrJNVqJHUAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/f9055a281cc38abb9adedfd6d44e612b/33ecc/5.easy_meme.webp 360w,
/static/f9055a281cc38abb9adedfd6d44e612b/263a4/5.easy_meme.webp 480w,
/static/f9055a281cc38abb9adedfd6d44e612b/0c8fb/5.easy_meme.webp 640w,
/static/f9055a281cc38abb9adedfd6d44e612b/521f7/5.easy_meme.webp 860w,
/static/f9055a281cc38abb9adedfd6d44e612b/cbd37/5.easy_meme.webp 1200w,
/static/f9055a281cc38abb9adedfd6d44e612b/0b34d/5.easy_meme.webp 1920w&quot;
              sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/f9055a281cc38abb9adedfd6d44e612b/53918/5.easy_meme.png 360w,
/static/f9055a281cc38abb9adedfd6d44e612b/9aebd/5.easy_meme.png 480w,
/static/f9055a281cc38abb9adedfd6d44e612b/0f09e/5.easy_meme.png 640w,
/static/f9055a281cc38abb9adedfd6d44e612b/3e3fe/5.easy_meme.png 860w,
/static/f9055a281cc38abb9adedfd6d44e612b/64756/5.easy_meme.png 1200w,
/static/f9055a281cc38abb9adedfd6d44e612b/ac7a9/5.easy_meme.png 1920w&quot;
            sizes=&quot;(max-width: 1920px) 100vw, 1920px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/f9055a281cc38abb9adedfd6d44e612b/ac7a9/5.easy_meme.png&quot;
            alt=&quot;아니요&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;아니요&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;여기서의 의의는 Kotlin 컴파일러가 suspend 키워드를 만나면 CPS 패러다임을 구현하여, Continuation이라는 일종의 콜백을 주고 받도록 코드를 변환해준다는 것입니다.&lt;/p&gt;
&lt;p&gt;한편, 이렇게 모든 suspend 연산은 Continuation을 보내고 받도록 변환됩니다. 복잡한 변환 작업을 컴파일러가 뒤에서 수행해주고 있던 것이죠. 간단한 &lt;code&gt;suspend&lt;/code&gt; 키워드 너머 이렇게 복잡한 변환 과정이 이뤄진다니 놀랍지 않나요? 이렇게 Kotlin 팀은 동시성을 지원하기 위해 가능한 작은 변화를 주도록 노력했고, 단 하나의 &lt;code&gt;suspend&lt;/code&gt; 키워드로 Kotlin 개발자들에게 쉽게 동시성을 사용하도록 했다고 하네요. 거..참 쉽..죠 ..?&lt;/p&gt;
&lt;h3 id=&quot;성능-이야기--thread-vs-coroutine&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%84%B1%EB%8A%A5-%EC%9D%B4%EC%95%BC%EA%B8%B0--thread-vs-coroutine&quot; aria-label=&quot;성능 이야기  thread vs coroutine permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;성능 이야기 : Thread vs Coroutine&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 570px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 107.77777777777777%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAWCAYAAADAQbwGAAAACXBIWXMAAAsTAAALEwEAmpwYAAADY0lEQVR42qVVSS9kURSu1p1096YXve1etMT/sJVYWfgFhI2FsBDTVswEiQhRUmaCCDFWFaoIMcYUYoFSUeZZDOXenD7fUbe6KkpvvOTLPe/c8757xvssFoslivGF8fWD+CxciYmJP09PT62Xl5fuk5MTB8Np4PP5nMfHx0HgPYLOcX5+7maO5piYmB+WtLS06MfHR8/Lywsxqb6/v6e7uzsB6+nh4SGIp6cnAWTsBaChY3tffHz8L0tycnI0K/aurq7o5ubGz8SKiNTOzo5qbm5WPT09qre3V/X19ammpibV1tYmcn9/v+rq6lJTU1N+9pSur6+9CQkJvw2hB54xqeYQyOv1EoyKioqooKCAysvLqaSkhLKysig3N5dKS0upoqKCCgsLsa83NzfhtS82NvYfYSBMTYFnf3+f7HY7TU5O0sTEBDmdTnmHDEA/Pj5O29vbmr2DM74wD0F4e3sbJPR4PDQ0NCREDodDVryPjY2JDFLIGxsbkQlNyEdHR4Swz87OKCcnh9LT0yk/P5+ys7MpKSmJUlJSJPS8vDzKzMykjIwMvb6+/n7ISinSWkvIHR0d1N7eTp2dnSK3tLQQFyWo46KRy+XS3DYoii8uLu79HCLkmZkZWlpaooWFBVpcXKTZ2VlZoVteXqb5+XkUUHN3RA4ZfcgNKhUGysrKqLi4mGpqaqSqCBvhV1ZWUlVVlVQ+tMpvPOSTtGne3d1dqq+vp4aGBuL+k7W6uppqa2tFtlqtVFdXR9yTknf+9v8hPz8/C/x+vwCTZGB02OdHYyiQw2DIvOFBHjhvsokkB4xwcnA1skEgdxrpCfOQT/Mgd9xn2u12E4+TYHh4mAYGBmh0dFTk0D5Eb6IXudk196LMchgh+m5kZESjsiCdnp4WAp7XIFpbW2lwcJDModwymJjIhMZDGMHYjBdkM35YDWAHsLd6a2vrLeHBwYEQIqRQIFz2XFYD6I0O36ysrITnkIuyhzbhfPjZE8UnKw5NNTY2Kp4OZbPZFE+KXGccturu7sa1JWAP/WhyJny9vlJTU/+gyhcXF8QnyVyura3JNJhcYkIM5ubmZHJWV1cF+AZjyhfL6wWLXwCX3cpFcR0eHtpxpRuwXhCqM2BbAY+enW1cXAOb/AL4+RT4SX3/AL4FOKL+Aipi9MMZcgWhAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/339c534bf9898389f76e338939a519f8/33ecc/6.coroutine.webp 360w,
/static/339c534bf9898389f76e338939a519f8/263a4/6.coroutine.webp 480w,
/static/339c534bf9898389f76e338939a519f8/cc736/6.coroutine.webp 570w&quot;
              sizes=&quot;(max-width: 570px) 100vw, 570px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/339c534bf9898389f76e338939a519f8/53918/6.coroutine.png 360w,
/static/339c534bf9898389f76e338939a519f8/9aebd/6.coroutine.png 480w,
/static/339c534bf9898389f76e338939a519f8/27f8f/6.coroutine.png 570w&quot;
            sizes=&quot;(max-width: 570px) 100vw, 570px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/339c534bf9898389f76e338939a519f8/27f8f/6.coroutine.png&quot;
            alt=&quot;스레드에서 실행되지만, 스레드에 종속적이지 않은 Coroutine을 보여주는 자료&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;스레드에서 실행되지만, 스레드에 종속적이지 않은 Coroutine을 보여주는 자료&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;왜 Coroutine은 light-weight thread라고 불릴까요?&lt;/p&gt;
&lt;p&gt;기본적으로 애플리케이션에는 하나 이상의 프로세스가 있고, 각각의 프로세스는 적어도 하나의 스레드를 갖고 있습니다. 그렇다면 Coroutine은 어디서 실행될까요? Coroutine은 스레드 안에서 실행됩니다. 같은 스레드에 10개, 100개의 Coroutine이 있을 수 있습니다. 하지만 이것이 한 시점에 여러 Coroutine이 실행된다는 의미는 아닙니다. &lt;strong&gt;동시성 프로그래밍에서 한 시점에는 하나의 Coroutine만 실행&lt;/strong&gt;됩니다. 한편 main 함수에서 시작되는 메인스레드부터 백그라운드 스레드까지, 앱은 Main-Safe하게 여러 스레드를 활용해야합니다. Coroutine은 메인스레드 뿐만아니라 다른 백그라운드 스레드에서도 동작할 수 있으므로, Coroutine을 활용하여 Main-Safe하게 앱을 만들 수 있게 됩니다.&lt;/p&gt;
&lt;p&gt;즉, Coroutine은 스레드 안에서 실행되고 Coroutine을 활용하여 Main-Safe하게 앱을 만들 수 있습니다.&lt;/p&gt;
&lt;p&gt;스레드는 프로세스에 종속이 되는데, 그렇다면 Coroutine도 스레드에 종속이 될까요? 여기서 멀티스레드 방식과 Coroutine의 큰 차이점이 나타나는데요, &lt;strong&gt;Coroutine은 특정 스레드에 종속되지 않는다는 점이 큰 특징&lt;/strong&gt;입니다. 즉, Coroutine은 resume될 때마다 다른 스레드에서 실행될 수 있습니다.&lt;/p&gt;
&lt;p&gt;작업이 스레드에 종속되지 않는 특징은 스레드를 blocking 하지 않으면서 작업의 실행을 잠시 중단할 수 있게합니다. 즉, 스레드 B가 스레드 A의 작업이 끝날때까지 대기해야하는 작업을 잠시 중단하고, 그동안 다른 작업을 할수 있게 합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;이 특징은 스레드를 blocking하지 않아 더 빠른 연산으로 이어지게하고, 메모리 사용량을 줄여 많은 동시성 작업을 수행할 수 있게 합니다.&lt;/strong&gt; Coroutine 생성 또한 스레드 생성보다 빠르고 적은 비용으로 이뤄질 수 있습니다.&lt;/p&gt;
&lt;p&gt;이를 강조하기 위해 &lt;a href=&quot;https://kotlinlang.org/docs/coroutines-basics.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Kotlin 공식 문서&lt;/a&gt;에선 Coroutine을 light-weight thread라고 부르는 것입니다. 프로세서가 실행할 명령어 집합을 정의한다는 점 및 생명주기가 스레드와 비슷하다는 점에서 “thread&quot;, 하지만 스레드 보다 빠르고 적은 메모리를 사용한다는 점에서 “light-weight”으로 표현되었다고 이해할 수 있습니다.&lt;/p&gt;
&lt;p&gt;자 그럼 정말 light weight 한지 코드로 확인해볼까요?&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;kotlin&quot;&gt;&lt;pre class=&quot;language-kotlin&quot;&gt;&lt;code class=&quot;language-kotlin&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; kotlin&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;system&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;measureTimeMillis
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; kotlinx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;coroutines&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; runBlocking &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; amount &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;✅ Test when the amount is &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;amount&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;🛫 &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;Thread&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;activeCount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; threads active at the start&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; time &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; measureTimeMillis &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createThreads&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;amount&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;⏰ Running time for createThreads is &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;time&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; ms&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;suspend&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createCoroutines&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;amount&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Int&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; coroutineScope &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; jobs &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; ArrayList&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Job&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;i &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;..&lt;/span&gt;amount&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        jobs &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;launch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Dispatchers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Default&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token function&quot;&gt;delay&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1000L&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;🛬 &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;Thread&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;activeCount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; threads active at the end&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    jobs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; it&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;fun&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createThreads&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;amount&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Int&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;val&lt;/span&gt; jobs &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; ArrayList&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Thread&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;i &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;..&lt;/span&gt;amount&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        jobs &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; Thread &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            Thread&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sleep&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1000L&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;also&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; it&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;println&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-literal singleline&quot;&gt;&lt;span class=&quot;token string&quot;&gt;&quot;🛬 &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;&lt;span class=&quot;token expression&quot;&gt;Thread&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;activeCount&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; threads active at the end&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    jobs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; it&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;성능 차이를 측정해보기 위해 스레드와 Coroutine으로 같은 작업을 하는 코드를 작성해보았습니다. create-으로 시작하는 함수는 main 함수에서 실행되어, 1초간 대기하는 작업을 실행하는 스레드 혹은 Coroutine을 amount 개수 만큼 생성하고 실행시킵니다. 아래는 amount가 100, 1000 ,10000 일 때의 실행 결과(시간과 스레드 생성 개수)입니다.&lt;/p&gt;
&lt;h5 id=&quot;1️-amount--100&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1%EF%B8%8F-amount--100&quot; aria-label=&quot;1️ amount  100 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;1️. amount = 100&lt;/h5&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1209px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 20%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAECAYAAACOXx+WAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAy0lEQVR42mWP25KCMBBE+aF1QQQDSYDcgHAJyqr//yctk6r1xYeurk5NTs8kP+aMdC/QcIOy4uCyw7USqEUDVkuIRkW/lNXx1sb8m12Q5eWX0nOB5KSvyILA4Bb0fsb+eEHbAX4OGPyCJdxj7scZtvcIt78IP6X5B0IFpAgUrMNkA7QjyIp12+EGj3FaD59g3AhlekzLFjOVsFqgZDyCyWlzuiovGBItWzy3G6Sy0MfHpjPotEOrTDyPhikTmPxfyjhUXB5zNhZRKUHfzTOK8YlsoC8AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/f1640ad011c452dd3764849f11bd4053/33ecc/createCoroutines100.webp 360w,
/static/f1640ad011c452dd3764849f11bd4053/263a4/createCoroutines100.webp 480w,
/static/f1640ad011c452dd3764849f11bd4053/0c8fb/createCoroutines100.webp 640w,
/static/f1640ad011c452dd3764849f11bd4053/521f7/createCoroutines100.webp 860w,
/static/f1640ad011c452dd3764849f11bd4053/cbd37/createCoroutines100.webp 1200w,
/static/f1640ad011c452dd3764849f11bd4053/37ace/createCoroutines100.webp 1209w&quot;
              sizes=&quot;(max-width: 1209px) 100vw, 1209px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/f1640ad011c452dd3764849f11bd4053/53918/createCoroutines100.png 360w,
/static/f1640ad011c452dd3764849f11bd4053/9aebd/createCoroutines100.png 480w,
/static/f1640ad011c452dd3764849f11bd4053/0f09e/createCoroutines100.png 640w,
/static/f1640ad011c452dd3764849f11bd4053/3e3fe/createCoroutines100.png 860w,
/static/f1640ad011c452dd3764849f11bd4053/64756/createCoroutines100.png 1200w,
/static/f1640ad011c452dd3764849f11bd4053/89578/createCoroutines100.png 1209w&quot;
            sizes=&quot;(max-width: 1209px) 100vw, 1209px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/f1640ad011c452dd3764849f11bd4053/89578/createCoroutines100.png&quot;
            alt=&quot;createCoroutines(100)을 호출했을때&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;createCoroutines(100)을 호출했을때&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1174px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 20.833333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAECAYAAACOXx+WAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAwklEQVR42mWPaxKCMAyEvRGCiMXSUh7lVSg4OJ7A+19hbcLwyx87m2yTb5pL1KWIvne01YiybmH7Caa2rKqx6EcXvOP+WZRwy4ZHXuCa3BHfsj9drkIgqTSasNQNDrN/YZo9GjtgmJYAnFG3PdpuYDBlVEdxylDys2aglgaf5Q3nNx4mra+d4SeQ4OTTvGJ0Hn7bkQkJ8VSQyqDQFXKpD6AM33e2hwgBPdJZNCzV4WmW84naNMEV56qsOSMIzZ97BPwBThaLcSy8+jUAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/b467e63a94c9f9915d5a5ff28a498f97/33ecc/createThreads100.webp 360w,
/static/b467e63a94c9f9915d5a5ff28a498f97/263a4/createThreads100.webp 480w,
/static/b467e63a94c9f9915d5a5ff28a498f97/0c8fb/createThreads100.webp 640w,
/static/b467e63a94c9f9915d5a5ff28a498f97/521f7/createThreads100.webp 860w,
/static/b467e63a94c9f9915d5a5ff28a498f97/6ba46/createThreads100.webp 1174w&quot;
              sizes=&quot;(max-width: 1174px) 100vw, 1174px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/b467e63a94c9f9915d5a5ff28a498f97/53918/createThreads100.png 360w,
/static/b467e63a94c9f9915d5a5ff28a498f97/9aebd/createThreads100.png 480w,
/static/b467e63a94c9f9915d5a5ff28a498f97/0f09e/createThreads100.png 640w,
/static/b467e63a94c9f9915d5a5ff28a498f97/3e3fe/createThreads100.png 860w,
/static/b467e63a94c9f9915d5a5ff28a498f97/683d4/createThreads100.png 1174w&quot;
            sizes=&quot;(max-width: 1174px) 100vw, 1174px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/b467e63a94c9f9915d5a5ff28a498f97/683d4/createThreads100.png&quot;
            alt=&quot;createThreads(100)을 호출했을때&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;createThreads(100)을 호출했을때&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h5 id=&quot;2-amount--1000&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-amount--1000&quot; aria-label=&quot;2 amount  1000 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;2. amount = 1000&lt;/h5&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1226px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 21.111111111111114%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAECAYAAACOXx+WAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAvUlEQVR42mWQ2xKCMAxE+SNRudhCoVah1AJeRh79/89Y2czIiw9nktkmm6RJanPsPhnOtoc2LazrUK2xsReY1sFdPermjFLVooU4IysU0kOO/bH4I0lViTQYOOfRh4jHa8G1DxhukzT7MMoQxs7fMM5P5KXGbp+JKWFOxFCrBktccOkGMbg/39IY4iRwQxKnhwzphwhVNUJxqnDSZruAmydqFcfOy7mVsVuxrlsp4jbU2UTtlxOaEX4N31j7BX0XiqGKFmezAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/3072de0a27cc5bef6ac370a3ddb4e3c7/33ecc/createCoroutines1000.webp 360w,
/static/3072de0a27cc5bef6ac370a3ddb4e3c7/263a4/createCoroutines1000.webp 480w,
/static/3072de0a27cc5bef6ac370a3ddb4e3c7/0c8fb/createCoroutines1000.webp 640w,
/static/3072de0a27cc5bef6ac370a3ddb4e3c7/521f7/createCoroutines1000.webp 860w,
/static/3072de0a27cc5bef6ac370a3ddb4e3c7/cbd37/createCoroutines1000.webp 1200w,
/static/3072de0a27cc5bef6ac370a3ddb4e3c7/2e958/createCoroutines1000.webp 1226w&quot;
              sizes=&quot;(max-width: 1226px) 100vw, 1226px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/3072de0a27cc5bef6ac370a3ddb4e3c7/53918/createCoroutines1000.png 360w,
/static/3072de0a27cc5bef6ac370a3ddb4e3c7/9aebd/createCoroutines1000.png 480w,
/static/3072de0a27cc5bef6ac370a3ddb4e3c7/0f09e/createCoroutines1000.png 640w,
/static/3072de0a27cc5bef6ac370a3ddb4e3c7/3e3fe/createCoroutines1000.png 860w,
/static/3072de0a27cc5bef6ac370a3ddb4e3c7/64756/createCoroutines1000.png 1200w,
/static/3072de0a27cc5bef6ac370a3ddb4e3c7/98ef1/createCoroutines1000.png 1226w&quot;
            sizes=&quot;(max-width: 1226px) 100vw, 1226px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/3072de0a27cc5bef6ac370a3ddb4e3c7/98ef1/createCoroutines1000.png&quot;
            alt=&quot;createCoroutines(1000)을 호출했을때&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;createCoroutines(1000)을 호출했을때&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1896px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 25.555555555555554%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAA50lEQVR42nWOS27DMAxEfaGiriRSv8SNY9c/2YpRoEXR+99jSmnRXRZPnCFFYprY35DyiXFa0Q8Tho+lUvR9nGv/vR/Ri+5uw78vlH/Fl771F7y2Go05Iqa8IW0PTGvCvCRsKddj6ThrNexxiVd4WQpSrY9gF6p3sQPJ/E2zQGjuTPj0Bnt0yBXGKRzBIgVGltlhFVZWOJ3CLnphjVnYRA+kkazGYg1aOdqwIWxO49srfMnCj9TfoKrfHcHHGcFdoY2FNQx+CtWUTXkCEUY26IVBEpfUoyQwtsOLm9AKioIkoJriGeXgHw3jqmAXJ1suAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/80bd73a733fe67a309e09177c63a7766/33ecc/createThreads1000.webp 360w,
/static/80bd73a733fe67a309e09177c63a7766/263a4/createThreads1000.webp 480w,
/static/80bd73a733fe67a309e09177c63a7766/0c8fb/createThreads1000.webp 640w,
/static/80bd73a733fe67a309e09177c63a7766/521f7/createThreads1000.webp 860w,
/static/80bd73a733fe67a309e09177c63a7766/cbd37/createThreads1000.webp 1200w,
/static/80bd73a733fe67a309e09177c63a7766/691ec/createThreads1000.webp 1896w&quot;
              sizes=&quot;(max-width: 1896px) 100vw, 1896px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/80bd73a733fe67a309e09177c63a7766/53918/createThreads1000.png 360w,
/static/80bd73a733fe67a309e09177c63a7766/9aebd/createThreads1000.png 480w,
/static/80bd73a733fe67a309e09177c63a7766/0f09e/createThreads1000.png 640w,
/static/80bd73a733fe67a309e09177c63a7766/3e3fe/createThreads1000.png 860w,
/static/80bd73a733fe67a309e09177c63a7766/64756/createThreads1000.png 1200w,
/static/80bd73a733fe67a309e09177c63a7766/c7953/createThreads1000.png 1896w&quot;
            sizes=&quot;(max-width: 1896px) 100vw, 1896px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/80bd73a733fe67a309e09177c63a7766/c7953/createThreads1000.png&quot;
            alt=&quot;createThreads(1000)을 호출했을때&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;createThreads(1000)을 호출했을때&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h5 id=&quot;3️-amount--10000&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3%EF%B8%8F-amount--10000&quot; aria-label=&quot;3️ amount  10000 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;3️. amount = 10000&lt;/h5&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1248px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 21.666666666666668%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAECAYAAACOXx+WAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAvUlEQVR42m1PSRKDMAzjRQValhAIhCwUwjrtodNL//8LNTHH9qBxJEeyHV1shvTDYOQEIRXMMEJ0Cl1vIJWFvTvivOl8b8J2PFGwGsk1R3orfhDFnCHRDZQayBwMvT6DhtFB29HzgcJObSZjnGYUGuqJc0DU1RLv/QU7OSzbgXV/YHQrpnmjGsLcsmNeD0K4IGxbC4myamj7VmrSKJAVFYwXC9+saoG85GBc+LO4f1e45Yz0oJX0pyX84yHwC8JPimioEkiWAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/a27b56c00cb7210d5641e6c50352a2a2/33ecc/createCoroutines10000.webp 360w,
/static/a27b56c00cb7210d5641e6c50352a2a2/263a4/createCoroutines10000.webp 480w,
/static/a27b56c00cb7210d5641e6c50352a2a2/0c8fb/createCoroutines10000.webp 640w,
/static/a27b56c00cb7210d5641e6c50352a2a2/521f7/createCoroutines10000.webp 860w,
/static/a27b56c00cb7210d5641e6c50352a2a2/cbd37/createCoroutines10000.webp 1200w,
/static/a27b56c00cb7210d5641e6c50352a2a2/92e18/createCoroutines10000.webp 1248w&quot;
              sizes=&quot;(max-width: 1248px) 100vw, 1248px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/a27b56c00cb7210d5641e6c50352a2a2/53918/createCoroutines10000.png 360w,
/static/a27b56c00cb7210d5641e6c50352a2a2/9aebd/createCoroutines10000.png 480w,
/static/a27b56c00cb7210d5641e6c50352a2a2/0f09e/createCoroutines10000.png 640w,
/static/a27b56c00cb7210d5641e6c50352a2a2/3e3fe/createCoroutines10000.png 860w,
/static/a27b56c00cb7210d5641e6c50352a2a2/64756/createCoroutines10000.png 1200w,
/static/a27b56c00cb7210d5641e6c50352a2a2/f03ba/createCoroutines10000.png 1248w&quot;
            sizes=&quot;(max-width: 1248px) 100vw, 1248px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/a27b56c00cb7210d5641e6c50352a2a2/f03ba/createCoroutines10000.png&quot;
            alt=&quot;createCoroutines(10000)을 호출했을때&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;createCoroutines(10000)을 호출했을때&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1984px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 23.888888888888886%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAA7UlEQVR42nWPyXKDMBBE+aJQCEYr+44AY7tcOeT//6MzUuxjDq+6p6WekpJp9ejHBU03Ru2GOfq255l93Q7sJ1RNH8/CHPwwrcwSZ1u1IGmQZgWS8enh/YVp3bFsB/bzjp4vb8eF/fYA6RKOC9bVsWhYla1gywau7qC0Q5YrRjKE5LQSZ2XhK4dbaXEF7zQ29qdT8FJgJoFDCWzBMz3lWFkHzieZY5HFe6FE4lWBpxZ46QzfJsMP8zICd84a06I1NXQh4YhQMkEtyTcUCbn4LAzBzEsX5k8JXuUolcWXWZGaGRlZpPydlAv/8XnhL8I8qkSBvNcIAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/0245564c7843719356402567a4e6a778/33ecc/createThreads10000.webp 360w,
/static/0245564c7843719356402567a4e6a778/263a4/createThreads10000.webp 480w,
/static/0245564c7843719356402567a4e6a778/0c8fb/createThreads10000.webp 640w,
/static/0245564c7843719356402567a4e6a778/521f7/createThreads10000.webp 860w,
/static/0245564c7843719356402567a4e6a778/cbd37/createThreads10000.webp 1200w,
/static/0245564c7843719356402567a4e6a778/4a7a2/createThreads10000.webp 1984w&quot;
              sizes=&quot;(max-width: 1984px) 100vw, 1984px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/0245564c7843719356402567a4e6a778/53918/createThreads10000.png 360w,
/static/0245564c7843719356402567a4e6a778/9aebd/createThreads10000.png 480w,
/static/0245564c7843719356402567a4e6a778/0f09e/createThreads10000.png 640w,
/static/0245564c7843719356402567a4e6a778/3e3fe/createThreads10000.png 860w,
/static/0245564c7843719356402567a4e6a778/64756/createThreads10000.png 1200w,
/static/0245564c7843719356402567a4e6a778/740ad/createThreads10000.png 1984w&quot;
            sizes=&quot;(max-width: 1984px) 100vw, 1984px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/0245564c7843719356402567a4e6a778/740ad/createThreads10000.png&quot;
            alt=&quot;createThreads(10000)을 호출했을때&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;createThreads(10000)을 호출했을때&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;아래 두 표는 위 코드의 실행 결과를 요약한 표입니다. amount 값에 따라 생성된 스레드의 개수와 소요된 실행 시간의 변화를 나타내고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1836px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 17.77777777777778%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAECAYAAACOXx+WAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAoklEQVR42l2PWRLDIAxDuf9JGwph37KpyJ3pJNWXje0noTB1HAdCCHDOoZaCnLPUYwzcdV3Xr+69w3uH4D1qrYgxzN5D8ZhDaw1aq9j3HetqBXie5wNYyzycxq01pJRgzFvCsLfWSgCltZ70iGV5yQMhWi+yeE9EMQH3aR5mOmvM16gWCUGplCJKyQLdtk2AdCfsH8ivccZdQnhLjdEnowjwA7g+NwSH+uqqAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/0684c6a9ce329476db74fe6efa257e6b/33ecc/7.test_result.webp 360w,
/static/0684c6a9ce329476db74fe6efa257e6b/263a4/7.test_result.webp 480w,
/static/0684c6a9ce329476db74fe6efa257e6b/0c8fb/7.test_result.webp 640w,
/static/0684c6a9ce329476db74fe6efa257e6b/521f7/7.test_result.webp 860w,
/static/0684c6a9ce329476db74fe6efa257e6b/cbd37/7.test_result.webp 1200w,
/static/0684c6a9ce329476db74fe6efa257e6b/a9c35/7.test_result.webp 1836w&quot;
              sizes=&quot;(max-width: 1836px) 100vw, 1836px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/0684c6a9ce329476db74fe6efa257e6b/53918/7.test_result.png 360w,
/static/0684c6a9ce329476db74fe6efa257e6b/9aebd/7.test_result.png 480w,
/static/0684c6a9ce329476db74fe6efa257e6b/0f09e/7.test_result.png 640w,
/static/0684c6a9ce329476db74fe6efa257e6b/3e3fe/7.test_result.png 860w,
/static/0684c6a9ce329476db74fe6efa257e6b/64756/7.test_result.png 1200w,
/static/0684c6a9ce329476db74fe6efa257e6b/78496/7.test_result.png 1836w&quot;
            sizes=&quot;(max-width: 1836px) 100vw, 1836px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/0684c6a9ce329476db74fe6efa257e6b/78496/7.test_result.png&quot;
            alt=&quot;amount 값에 따른 createThreads와 createCoroutines의 실행 결과&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;amount 값에 따른 createThreads와 createCoroutines의 실행 결과&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;메모리 측면에서 큰 차이를 보여주고 있습니다.&lt;/strong&gt; Coroutine을 사용하면 불필요한 blocking을 하지 않아, 스레드 생성을 줄일 수 있기 때문인데요. 이는 &lt;code&gt;createThreads&lt;/code&gt;에서 OOM 이 발생하는 원인과 관련이 있습니다. amount가 100일때, &lt;code&gt;createCoroutines&lt;/code&gt;와 &lt;code&gt;createThreads&lt;/code&gt; 호출을 통한 스레드 생성 횟수는 30배가 넘는 차이가 발생했습니다. Coroutine이 멀티스레드 방식보다 30배 적은 thread 객체를 생성하고 있으므로, 30배 적은 메모리를 사용하고 있다고 말할 수 있을 것 같습니다. 한편 실행 시간 측면에선 큰 차이를 보여주진 않았습니다.&lt;/p&gt;
&lt;h2 id=&quot;outro&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#outro&quot; aria-label=&quot;outro permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Outro&lt;/h2&gt;
&lt;p&gt;마이쿠키런에서 Coroutine을 활용하여 신규개발을 진행하면서 개인적으로 느낀 점, 궁금한 점들이 많았는데요. (ex. Coroutine은 정말 최고야! JetBrains 감사합니다.) 그 중 궁금한 점들을 직접 정의하고 답을 찾아갈 수 있던 좋은 기회였습니다.&lt;/p&gt;
&lt;p&gt;저와 같은 물음표를 가진 분들에게 느낌표를 드릴 수 있는 글이 되었으면 좋겠습니다.&lt;/p&gt;
&lt;p&gt;한편 &lt;a href=&quot;https://www.youtube.com/watch?v=BOHK_w09pVA&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Understand Kotlin Coroutines on Android (Google I/O&apos;19)&lt;/a&gt;를 보면서 개발자들에게 큰 편의를 주는 이러한 기술적인 지원이 개발자들의 경험인 UXR와 이를 바탕으로 기획한 것들이라는 것을 알게 되었습니다. 개발자들의 의견에 귀기울이며 발전해가는 안드로이드 생태계의 일원이라는 것에 큰 자부심을 느낀 순간이었습니다.&lt;/p&gt;
&lt;p&gt;더 좋은 방향으로 나아가기 위해 치열하게 고민하는 개발자분들에게 감사하는 마음으로 이번 포스팅을 마치겠습니다. 읽어주셔서 감사합니다.&lt;/p&gt;
&lt;h3 id=&quot;references&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#references&quot; aria-label=&quot;references permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=LguKiUMG6OQ&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;FLIPBOOK - Cloudy with a Chance of Extinction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/Kotlin/KEEP/blob/master/proposals/coroutines.md&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;KEEP/coroutines.md at master · Kotlin/KEEP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://medium.com/androiddevelopers/exceed-the-android-speed-limit-b73a0692abc1&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Exceed the Android Speed Limit!&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=BOHK_w09pVA&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Understand Kotlin Coroutines on Android (Google I/O&apos;19)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.android.com/codelabs/kotlin-coroutines?hl=ko#12&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Use Kotlin Coroutines in your Android App&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kotlinlang.org/docs/coroutines-basics.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Coroutines basics | Kotlin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/PacktPublishing/Learning-Concurrency-in-Kotlin&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Learning Concurrency in Kotlin By Miguel Angel Castiblanco Torres&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[9가지 프로그래밍 언어로 배우는 개념: 1편 - 타입 이론]]></title><description><![CDATA[프로그래밍 언어들을 비교해가며 타입 이론을 알아보고 더 나은 코드를 작성하는 방법을 고민해봅니다.]]></description><link>https://tech.devsisters.com/posts/programming-languages-1-type-theory</link><guid isPermaLink="false">https://tech.devsisters.com/posts/programming-languages-1-type-theory</guid><pubDate>Tue, 12 Jul 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;프로그래밍-언어-시리즈&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%EC%96%B8%EC%96%B4-%EC%8B%9C%EB%A6%AC%EC%A6%88&quot; aria-label=&quot;프로그래밍 언어 시리즈 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;프로그래밍 언어 시리즈&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;9가지 프로그래밍 언어로 배우는 개념: 1편 - 타입 이론&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/programming-languages-2-polymorphism&quot;&gt;9가지 프로그래밍 언어로 배우는 개념: 2편 - 다형성&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/programming-languages-3-metaprogramming&quot;&gt;9가지 프로그래밍 언어로 배우는 개념: 3편 - 메타프로그래밍&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/programming-languages-4-high-level-languages-and-dynamically-typed-languages&quot;&gt;9가지 프로그래밍 언어로 배우는 개념: 4편 - 하이 레벨 언어와 동적 타입 언어&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/programming-languages-5-concurrent-programming&quot;&gt;9가지 프로그래밍 언어로 배우는 개념: 5편 - 동시성 프로그래밍&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;서론&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%84%9C%EB%A1%A0&quot; aria-label=&quot;서론 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;서론&lt;/h2&gt;
&lt;p&gt;안녕하세요, 저는 게임 &quot;쿠키런: 킹덤&quot;의 서버 파트에서 근무하고 있는 지민규라고 합니다.&lt;/p&gt;
&lt;p&gt;시작하기에 앞서 어째서 이 글을 쓰게 되었는지를 말씀드리고 싶은데요, 흔히 언어는 우리의 사고를 형성한다는 말이 있습니다. 언어가 가진 문장 구조과 발성, 그리고 단어들의 형태에 따라 세상에 대한 시각이 바뀐다고 합니다.&lt;/p&gt;
&lt;p&gt;저는 다양한 프로그래밍 언어를 배우면서 비슷한 경험을 했습니다. 첫 프로그래밍 언어로 C++를 배웠을 때 C++가 가진 한계점이 제 프로그래밍 방식의 한계점이 되었었고, Go를 배웠을 때 Go가 가진 언어의 구조가 프로그래밍하기에 충분하다고 생각했었습니다. 하지만 이후 더 많은 프로그래밍 언어들을 배우면서 생각이 더 넓어지고 프로그래밍 언어들 사이의 공통점이 보이기 시작했으며, 어떻게 하면 더욱 깨끗한 코드를 구현할 수 있을지 깨달아가게 되었습니다.&lt;/p&gt;
&lt;p&gt;다양한 프로그래밍 언어를 배워야하는 이유는 단순히 프로그래밍 언어들을 적재적소에 사용할 수 있어서가 아닙니다. 다양한 프로그래밍 언어를 배우면 아키텍처 디자인을 할 때 좀 더 넓은 시각을 가질 수 있게 되며, 어떠한 코드가 이상적인 코드인지 좀 더 알게 되고 좀 더 깊은 고민을 하며 구현할 수 있게 된다고 생각합니다.&lt;/p&gt;
&lt;p&gt;이 글은 주요 프로그래밍 언어들 중 하나를 깊게 이해하고 있는 분들을 대상으로 썼습니다. 그런 분들이 좀 더 다양한 시각을 가질 수 있도록 돕기위해 제가 프로그래밍 언어들을 배우고 사용하면서 알게된 것들을 조금이나마 나눠드리고자 합니다.&lt;/p&gt;
&lt;h2 id=&quot;언어-소개&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%96%B8%EC%96%B4-%EC%86%8C%EA%B0%9C&quot; aria-label=&quot;언어 소개 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;언어 소개&lt;/h2&gt;
&lt;h3 id=&quot;c&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#c&quot; aria-label=&quot;c permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;C&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;1972년에 공개되어 로우 레벨 프로그래밍 언어&lt;/li&gt;
&lt;li&gt;초창기 프로그래밍 언어라서 기능이 많지 않아 단순함&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;c-1&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#c-1&quot; aria-label=&quot;c 1 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;C++&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;1985년에 공개되어 C를 바탕으로 OOP 기능들이 추가된 로우 레벨 언어&lt;/li&gt;
&lt;li&gt;C와 다르게 템플릿을 지원하고 C에 비해 굉장히 많은 기능들이 붙어 복잡도가 높음&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;java&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#java&quot; aria-label=&quot;java permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Java&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;1995년에 공개되어 C++를 바탕으로 OOP 기능을 강화한 JVM 계열 언어&lt;/li&gt;
&lt;li&gt;C++와 다르게 하이 레벨 언어에 가까우며 메모리를 직접 관리하지 않고 가비지 컬렉터를 사용함&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;c-2&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#c-2&quot; aria-label=&quot;c 2 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;C#&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;2000년에 공개되고 Java를 벤치마킹하여 만들어진 하이 레벨 언어&lt;/li&gt;
&lt;li&gt;Java와 많은 유사점을 가지고 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;scala&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#scala&quot; aria-label=&quot;scala permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Scala&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;2004년에 공개된 하이 레벨, 함수형, JVM 계열 언어&lt;/li&gt;
&lt;li&gt;Java가 쓰는 JVM으로 작동하지만 함수형 프로그래밍에서 밴치마킹한 기능들이 많으며 동시에 OOP 개발도 가능함&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;go&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#go&quot; aria-label=&quot;go permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Go&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;2009년에 공개된 하이 레벨 언어&lt;/li&gt;
&lt;li&gt;현대 프로그래밍 언어가 지원하는 기능 대다수가 존재하지 않는 대신 C와 비견될정도로 단순하며, 성능 또한 로우 레벨 언어 수준까지는 아니지만 빠른 편&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;rust&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#rust&quot; aria-label=&quot;rust permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Rust&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;2010년에 공개된 메모리 안전성 문제를 해결하고 타입 시스템을 강화하기 위해 만들어진 로우 레벨 언어&lt;/li&gt;
&lt;li&gt;다른 로우 레벨 언어가 갖지 못한 강화된 타입 시스템과 메모리 안전성을 강화하는 버로우 체커가 특징임&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;kotlin&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#kotlin&quot; aria-label=&quot;kotlin permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Kotlin&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;2011년에 공개된 하이 레벨, JVM 계열 언어&lt;/li&gt;
&lt;li&gt;Scala를 벤치마킹하였기에 Scala의 대다수 기능들을 가지고 있으며, 문법도 유사하지만 좀 더 OOP 계열 언어에 가까움&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;typescript&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#typescript&quot; aria-label=&quot;typescript permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Typescript&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;2012년에 공개된 하이 레벨, Javascript 기반 언어&lt;/li&gt;
&lt;li&gt;Javascript과 달리 타입 시스템을 지원하고 있으며 타입 시스템 또한 다른 주요 언어들에 비해 정교함&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;null-nullable-option&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#null-nullable-option&quot; aria-label=&quot;null nullable option permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Null, Nullable, Option&lt;/h2&gt;
&lt;p&gt;가장 유명하면서도 가장 끔찍한 프로그래밍 언어 기능 중 하나를 꼽으라고 한다면 null이라고 자신있게 말할 수 있습니다. null이라는 값을 데이터에 사용하면 “없음”을 표현할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Java 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Person&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; nickname&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;Person&lt;/span&gt; person &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Person&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
person&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;name &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Min Gyu Chi&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
person&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;nickname &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 별명은 없다&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;null 참조는 “10억 달러짜리 실수”라는 악명이 붙어있습니다. 이는 전세계 IT회사들 통틀어 null을 참조하려다 생겨난 버그로 인해 10억 달러어치의 손실을 낼 정도로 많은 버그를 일으켰다는 관점이 있기 때문입니다. 실수로 null이 들어있는 데이터를 참조하면 값이 존재하지 않아서 프로그램을 진행할 수 없어 런타임 에러를 내게 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Java 코드&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;Integer&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;Integer&lt;/span&gt; b &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; a &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; a&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// null 더하기 null은 어떤 값이지? NullPointerException 에러!&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Java에서는 코드에서 null을 참조할 가능성이 있어도 컴파일을 막지 않습니다. 그렇기에 null 에러를 피하려면 모든 필드와 함수 인수를 문서화하여 null이 들어갈 가능성이 있는 값을 표기해야 합니다. 만약 문서가 잘못되었거나 null 체크하는 것을 잊었다면 곧바로 에러로 이어질 수도 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Java 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Person&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token annotation punctuation&quot;&gt;@Nullable&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; nickname&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 이 필드는 null이 들어갈 수도 있어&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이러한 문제들을 컴파일 타임에 잡기 위해서는 IDE의 도움을 받거나 null이 들어간 필드들의 타입을 감싸야 합니다. 많은 프로그래밍 언어들은 null을 담을 수 있는 타입을 뚜렷하게 구별하고 있으며 Java도 Java 8에서부터 &lt;code&gt;Optional&amp;#x3C;T&gt;&lt;/code&gt; 타입을 추가하여 문서화의 영역이 아닌 타입의 영역으로 가져올 수 있도록 했습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Java 코드&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;Optional&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; nickname &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Optional&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;empty&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Rust 코드&lt;/span&gt;
let nickname&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Option&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Scala 코드&lt;/span&gt;
val nickname&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Option&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이를 &lt;strong&gt;nullable 타입&lt;/strong&gt;이라고 부릅니다. nullable 타입은 프로그래밍에 광범위하게 쓰이는 만큼 C#과 Kotlin은 nullable 타입 전용 문법을 추가하였습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C# 코드&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; nickname &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Kotlin 코드&lt;/span&gt;
val nickname&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;다만 Java, C#, Kotlin, Rust, Scala의 nullable 타입 구현 방식은 서로간에 차이가 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Java 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Optional&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;final&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 필드 하나 그대로 null인지 아닌지 확인&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// C# 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 레퍼런스 타입이 아닌 경우&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; struct &lt;span class=&quot;token class-name&quot;&gt;Nullable&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; bool hasValue&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// null인지 저장하는 필드&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;private&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt; value&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 레퍼런스 타입인 경우&lt;/span&gt;
&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; nickname&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 컴파일 타임에 모든 체크가 강제된다&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Kotlin 코드&lt;/span&gt;
val nickname&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 컴파일 타임에 모든 체크가 강제된다&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Rust 코드&lt;/span&gt;
pub &lt;span class=&quot;token keyword&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Option&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Option&amp;lt;T&gt; 이라는 타입이 있는데&lt;/span&gt;
  &lt;span class=&quot;token class-name&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;           &lt;span class=&quot;token comment&quot;&gt;// None 이거나&lt;/span&gt;
  &lt;span class=&quot;token class-name&quot;&gt;Some&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;         &lt;span class=&quot;token comment&quot;&gt;// T를 가진 Some 이거나&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Scala 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;sealed&lt;/span&gt; trait &lt;span class=&quot;token class-name&quot;&gt;Option&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;                         &lt;span class=&quot;token comment&quot;&gt;// Option[A] 이라는 타입이 있는데&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; object &lt;span class=&quot;token class-name&quot;&gt;None&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Option&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Nothing&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;        &lt;span class=&quot;token comment&quot;&gt;// None 이거나&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Some&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Option&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// A를 가진 Some 이거나&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;여기서 특이하게도 Scala와 Rust는 null에 해당하는 &lt;code&gt;None&lt;/code&gt;과 null이 아님에 해당하는 &lt;code&gt;Some&lt;/code&gt;을 합친 &lt;code&gt;Option&lt;/code&gt; 타입으로 처리하고 있습니다. &lt;code&gt;Some&lt;/code&gt;인 경우 데이터를 저장하고 있는 필드를 가지고 있고 &lt;code&gt;None&lt;/code&gt;이면 null에 해당되는 “없음”이라는 정보를 표현합니다. 이 방식은 Typescript의 nullable 타입 처리 방식과 어느정도 비슷합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Typescript 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; nickname&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// string 일 수도 있고 null 일 수도 있어&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Scala, Rust, Typescript가 이러한 공통점을 보여주는 이유는 세 언어 모두 타입 이론의 &lt;strong&gt;합 타입&lt;/strong&gt;을 기반으로 nullable 타입을 지원하고 있기 때문입니다.&lt;/p&gt;
&lt;h2 id=&quot;타입-이론&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%83%80%EC%9E%85-%EC%9D%B4%EB%A1%A0&quot; aria-label=&quot;타입 이론 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;타입 이론&lt;/h2&gt;
&lt;p&gt;데이터는 표현합니다. 하지만 표현 가능한 정보의 종류와 수는 타입에 따라 결정됩니다. 타입 이론은 데이터의 타입을 어떠한 방식으로 확장시킬 수 있는지를 정립합니다. 타입 이론은 작은 타입들로 더 큰 타입을 만들어내는 방식으로 타입을 확장시키는데, 여기서 가장 기초가 되는 개념은 **곱 타입(Product Type)**과 **합 타입(Sum Type)**입니다.&lt;/p&gt;
&lt;p&gt;곱 타입은 한 데이터에 두 가지 정보를 모두 저장하여 두 정보를 함께 표현합니다. 많은 언어들의 struct, DTO, record들이 이에 해당합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C/C++ 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Human&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  string name&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; age&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Rust 코드&lt;/span&gt;
pub &lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Human&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  age&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; i32&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Kotlin 코드&lt;/span&gt;
data &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Human&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;val name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; val age&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Int&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Scala 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Human&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;name&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; age&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Int&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;위 예시로 들자면 사람에게 이름 정보에 더불어 나이 정보도 함께 표현하게 되므로 훨씬 많은 표현을 할 수 있게 됩니다. 이로써 사람이라는 정보가 가지는 경우의 수는 이름이 가지는 경우의 수와 나이가 가지는 경우의 수의 곱입니다.&lt;/p&gt;
&lt;p&gt;이번에는 합타입을 살펴봅시다. nullable 타입은 데이터가 “없음”이라는 정보를 추가적으로 표현할 수 있게 됩니다. 이는 기존 데이터가 가진 표현력(Expressiveness)에서 null이라는 표현을 “더한” 것이기 때문입니다. 위 예시에서는 수많은 별명들을 표현할 수 있는 것에 더불어 별명이 없다는 것도 추가적으로 표현할 수 있게 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Typescript 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; nickname1&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;mango&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// mango라는 별명도 있을 수 있고&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; nickname2&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;mystery meat&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// mystery meat도 있을 수 있고&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; nickname3&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 별명이 없을 수도 있지&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;합 타입은 null 타입을 더하는 것에 국한되지 않으며 세 개 이상의 타입들을 합할 수도 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Typescript 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CouponCode&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;        &lt;span class=&quot;token comment&quot;&gt;// 쿠폰 번호는 문자열이거나 숫자&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ExcelCell&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; Date  &lt;span class=&quot;token comment&quot;&gt;// 엑셀 셀은 문자열, 숫자, 시간 중 하나&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Rust 코드&lt;/span&gt;
pub &lt;span class=&quot;token keyword&quot;&gt;enum&lt;/span&gt; CouponCode &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;CouponString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;String&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;CouponNumber&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;i32&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Scala 코드&lt;/span&gt;
sealed trait CouponCode
&lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CouponString&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CouponCode&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CouponNumber&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Int&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CouponCode&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이처럼 곱 타입은 많은 언어들이 지원하고 있지만, 합 타입은 일부 프로그래밍 언어들만이 지원하고 있습니다. C/C++, C#은 합 타입을 지원하지 않으며 합 타입을 미약하게나마 구현하려면 추상 클래스나 인터페이스를 이용하여 제한적으로 구현하거나 타입 캐스팅을 하여서 하위 타입을 확정하여 접근해야 합니다.&lt;/p&gt;
&lt;h2 id=&quot;상속의-한계&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%83%81%EC%86%8D%EC%9D%98-%ED%95%9C%EA%B3%84&quot; aria-label=&quot;상속의 한계 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;상속의 한계&lt;/h2&gt;
&lt;p&gt;아래와 같은 요구사항으로 합 타입을 묘사했다고 해봅시다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Java 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;abstract&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Job&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; speedLevel&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// speed level은 공유한다&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Police&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Job&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; strengthLevel&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; fame&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Robber&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Job&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; robberyLevel&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; infamy&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Human&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; health&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Job&lt;/span&gt; job&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;여기서 &lt;code&gt;Job&lt;/code&gt;의 메소드를 추가하여 &lt;code&gt;Police&lt;/code&gt;의 표현력과 &lt;code&gt;Robber&lt;/code&gt;의 표현력을 그대로 가진 인터페이스를 구현할 수 있을까요?&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Java 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;abstract&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Job&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; speedLevel&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 어느 정보가 어디서 왔는지 모른다!&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;abstract&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Optional&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getStrengthLevel&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;abstract&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Optional&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getRobberyLevel&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Int&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getSpeedLevel&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Optional&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;of&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;speedLevel&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;abstract&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Optional&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getFame&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;abstract&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Optional&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getInfamy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;위 방법으로 개별 필드들을 노출하면 각 필드가 어떤 하위 타입에서 오는지 파악하기가 어렵습니다. 또한 유지보수로 인해 하위 타입이 바뀌게 될 경우 메소드는 삭제되지 않고 남아있기 때문에 유지보수성을 해치고 의미 없는 코드로 인해 코드 가독성 또한 낮아집니다.&lt;/p&gt;
&lt;p&gt;그렇다면 타입 캐스팅은 어떨까요?&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;rust&quot;&gt;&lt;pre class=&quot;language-rust&quot;&gt;&lt;code class=&quot;language-rust&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Java 코드&lt;/span&gt;
public int &lt;span class=&quot;token function&quot;&gt;getScore&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Job&lt;/span&gt; job&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;job instanceof &lt;span class=&quot;token class-name&quot;&gt;Police&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Police&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; job&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fame &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;job instanceof &lt;span class=&quot;token class-name&quot;&gt;Robber&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Robber&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; job&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;infamy &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;150&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 이 경우는 불가능하니까 exception을 던진다?&lt;/span&gt;
    throw new &lt;span class=&quot;token class-name&quot;&gt;RuntimeException&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Unreachable code&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;타입 캐스팅을 통해 타입들을 명시할 경우 세 가지 문제가 발생합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;하위 타입이 늘어날 수록 &lt;code&gt;if&lt;/code&gt;문이 많아지고 처리 속도는 느려진다.&lt;/li&gt;
&lt;li&gt;하위 타입이 추가가 되고 &lt;code&gt;if&lt;/code&gt;문이 추가되지 않으면 컴파일 에러 대신 예외가 던져진다.&lt;/li&gt;
&lt;li&gt;코드에 불필요한 예외가 있어서 코드 가독성을 해친다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;여기서 1번 문제는 enum과 switch-case를 이용하여 어느정도 해결이 가능합니다. 하위 타입에서 현재 타입이 어떤 타입인지 enum으로 먼저 알려주고 나서 타입 캐스팅을 하면 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Java 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;JobType&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;POLICE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;ROBBER&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;abstract&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Job&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; speedLevel&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;abstract&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;JobType&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Police&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Job&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; strengthLevel&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; fame&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token annotation punctuation&quot;&gt;@Override&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;JobType&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;JobType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;POLICE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Robber&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Job&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; robberyLevel&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; infamy&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token annotation punctuation&quot;&gt;@Override&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;JobType&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;JobType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;ROBBER&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Human&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; name&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; health&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Job&lt;/span&gt; job&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getScore&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Job&lt;/span&gt; job&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;job&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;POLICE&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Police&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; job&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fame &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;ROBBER&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Robber&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; job&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;infamy &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;150&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 이 경우는 불가능하니까 exception을 던진다.&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;RuntimeException&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Unreachable code&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 경우 하위 타입이 늘어나더라도 switch-case는 한 번만 실행되므로 성능적인 면에서는 손해를 보지 않습니다. 이러한 방식의 데이터 타입은 &lt;strong&gt;Tagged Union&lt;/strong&gt;이라고 부릅니다. 사실 Rust와 Scala의 합 타입들은 마찬가지로 일종의 Tagged Union으로 구현되어 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Rust 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Tagged Union에 필요한 enum 필드가 숨겨져 있다&lt;/span&gt;
pub &lt;span class=&quot;token keyword&quot;&gt;enum&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Job&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token class-name&quot;&gt;Police&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    strength_level&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; i32&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    speed_level&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; i32&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    fame&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; i32&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token class-name&quot;&gt;Robber&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    robbery_level&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; i32&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    speed_level&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; i32&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    infamy&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; i32&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

pub fn &lt;span class=&quot;token function&quot;&gt;get_score&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;job&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Job&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; i32 &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  match job &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 내부적으로는 switch-case와 원리가 같다&lt;/span&gt;
    &lt;span class=&quot;token class-name&quot;&gt;Job&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Police&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; fame&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;   &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; fame &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token class-name&quot;&gt;Job&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Robber&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; infamy&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; infamy &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;150&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Scala 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// JVM의 런타임 타입 정보를 이용한다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;sealed&lt;/span&gt; trait &lt;span class=&quot;token class-name&quot;&gt;Job&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Police&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;strengthLevel&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; speedLevel&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; fame&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Job&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Robber&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;robberyLevel&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; speedLevel&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; infamy&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Job&lt;/span&gt;

def &lt;span class=&quot;token function&quot;&gt;getScore&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;job&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Job&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Int&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  job match &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 내부적으로는 switch-case와 원리가 같다&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Police&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;_&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; _&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; fame&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;   &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; fame &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Robber&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;_&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; _&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; infamy&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; infamy &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;150&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 Scala와 Rust의 &lt;code&gt;match&lt;/code&gt; 키워드로 데이터가 가질 수 있는 여러가지 경우의 수에 대한 정의를 할 수 있는 기능을 **패턴 매칭(Pattern Matching)**이라고 합니다. 그중에서도 합타입으로 패턴 매칭을 하고 하위 타입에 대한 모든 케이스를 정의할 경우 switch-case와 비슷한 연산으로 최적화되어 분기가 이루어집니다. 이것을 **완전 패턴 매칭(Exhuastive Pattern Matching)**이라고 부릅니다. 완전 패턴 매칭을 사용하면 하위 타입이 추가되어도 예외 대신 컴파일 에러를 내고 코드 추가를 유도하게 됩니다. 또한 이론적으로 “불가능한” 상황에 대한 예외를 던지는 코드를 추가할 필요가 없습니다.&lt;/p&gt;
&lt;p&gt;Java는 Java 16에서 패턴 매칭을 추가되었고 Java 17에서 sealed 키워드가 추가되어 합타입이 정식적으로 지원되고 있습니다. C#은 C# 7.0부터 패턴 매칭을 추가했지만 아직 합 타입이 없기 때문에 완전 패턴 매칭이 불가능합니다.&lt;/p&gt;
&lt;p&gt;합 타입 없이는 완전 패턴 매칭이 불가능한 이유는 인터페이스나 클래스는 소스 코드 어디서든 상속 받고 구현할 수 있기 때문입니다. 예를들어 라이브러리 코드 내부에서 완전 패턴 매칭을 했다고 합시다. 애플리케이션 코드에서 해당 타입에 하위 타입을 추가하게 되면 라이브러리 코드에는 해당 하위 타입에 대한 경우가 추가되어 있지 않아 오류가 발생합니다.&lt;/p&gt;
&lt;p&gt;Rust의 합 타입은 하나의 enum 정의라서 하나의 파일에 모든 하위 타입이 정의되어있고, Scala, Kotlin, Java의 경우 합 타입에 &lt;code&gt;sealed&lt;/code&gt; 키워드를 붙이는데 이는 모든 하위 타입을 해당 상위 타입이 선언된 파일에서 구현해야한다는 것을 의미합니다.&lt;/p&gt;
&lt;p&gt;이처럼 합 타입과 곱 타입 위주로 데이터를 정의해가며 더 큰 타입을 만들어내는 방식을 **컴포지션(Composition)**이라고 부릅니다. 상속은 잘못된 데이터 설계를 불러일으키기 때문에 Go와 Rust는 상속을 지원하지 않고 컴포지션 위주의 데이터 설계를 유도합니다.&lt;/p&gt;
&lt;h2 id=&quot;에러-처리와-타입-이론&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%97%90%EB%9F%AC-%EC%B2%98%EB%A6%AC%EC%99%80-%ED%83%80%EC%9E%85-%EC%9D%B4%EB%A1%A0&quot; aria-label=&quot;에러 처리와 타입 이론 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;strong&gt;에러 처리와 타입 이론&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;합 타입과 곱 타입은 자주 쓰이는 만큼 익명으로(anonymously) 정의할 수 있는 경우가 많습니다. 그 중에서도 가장 널리 알려진 것은 곱 타입의 익명 타입인 **튜플(Tuple)**입니다. 튜플은 임의의 타입들을 곱 타입으로 묶습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;rust&quot;&gt;&lt;pre class=&quot;language-rust&quot;&gt;&lt;code class=&quot;language-rust&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
tuple&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;int&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; string&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; double&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; tp&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// C# 코드&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;int&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; string&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; double&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; tp&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Rust 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; tp&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;i32&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;f64&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Scala 코드&lt;/span&gt;
val tp&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Double&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;튜플은 함수에서 여러 값을 함께 리턴해야 할 때 유용합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;rust&quot;&gt;&lt;pre class=&quot;language-rust&quot;&gt;&lt;code class=&quot;language-rust&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C# 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 상품을 구매하면 상품과 영수증이 같이온다.&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Receipt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;PurchaseProduct&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;string productId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Scala 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 상품을 구매하면 상품과 영수증이 같이온다.&lt;/span&gt;
def &lt;span class=&quot;token function&quot;&gt;purchaseProduct&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;productId&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Receipt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;합 타입도 익명 타입 버전이 존재합니다. Scala, Rust, Typescript가 익명적 합 타입을 지원하고 있지만, 3개 이상의 타입을 묶는 것은 Scala 3나 Typescript에서만 가능합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;rust&quot;&gt;&lt;pre class=&quot;language-rust&quot;&gt;&lt;code class=&quot;language-rust&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Scala 코드 (Scala 2)&lt;/span&gt;
sealed &lt;span class=&quot;token keyword&quot;&gt;trait&lt;/span&gt; &lt;span class=&quot;token type-definition class-name&quot;&gt;Either&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;B&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 정규 라이브러리에 정의된 타입&lt;/span&gt;
case class &lt;span class=&quot;token class-name&quot;&gt;Left&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; extends &lt;span class=&quot;token class-name&quot;&gt;Either&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Nothing&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
case class &lt;span class=&quot;token class-name&quot;&gt;Right&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;B&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;B&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; extends &lt;span class=&quot;token class-name&quot;&gt;Either&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Nothing&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;B&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

val couponCode&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Either&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Scala 코드 (Scala 3)&lt;/span&gt;
val couponCode&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Int&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Typescript 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; couponCode&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; number &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; string &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; number&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;익명적 합 타입도 함수 출력에 쓰이는 경우가 많습니다. 다만 보통 여러 데이터를 한번에 내려주는 용도가 아닌, 오류 또는 실행 성공 결과 값을 내려주는 용도로 사용합니다. 이렇게 사용하는 경우가 너무 많아서 Rust에서는 익명적 합 타입의 이름이 &lt;code&gt;Result&lt;/code&gt;입니다.&lt;/p&gt;
&lt;p&gt;Scala에서는 성공 타입을 오른쪽 타입 파라미터에 넣는 경향이 있으나 Rust에서는 성공 값은 왼쪽 타입 파라미터에 넣습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;rust&quot;&gt;&lt;pre class=&quot;language-rust&quot;&gt;&lt;code class=&quot;language-rust&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Scala 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 오류 없이 성공했다면 (Product, Receipt),&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 실패했다면 PurchaseError를 내려준다&lt;/span&gt;
def &lt;span class=&quot;token function&quot;&gt;purchaseProduct&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;productId&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Either&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;PurchaseError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Receipt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Rust 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 함수가 오류 없이 성공했다면 (Product, Receipt)&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 실패했다면 PurchaseError를 내려준다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;pub&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;fn&lt;/span&gt; &lt;span class=&quot;token function-definition function&quot;&gt;purchase_product&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;product_id&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Result&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Receipt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;PurchaseError&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;전통적 프로그래밍 언어들은 이러한 출력을 통한 에러 처리가 아닌 예외를 던지고 잡아서 처리하는 경우가 많습니다. 어째서 Rust와 Scala는 어째서 예외를 선택하지 않았을까요? 이는 예외가 가진 문제점들을 살펴보아야 합니다. 주요 프로그래밍 언어들 중 C++에서 가장 처음으로 예외를 추가하였는데, 가장 눈에 띄는 문제점은 함수 선언부를 확인해도 해당 함수가 예외를 던질지 말지를 알 수 없다는 것입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;cpp&quot;&gt;&lt;pre class=&quot;language-cpp&quot;&gt;&lt;code class=&quot;language-cpp&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// C++ 코드&lt;/span&gt;
Product &lt;span class=&quot;token function&quot;&gt;purchaseProduct&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;string productId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;        &lt;span class=&quot;token comment&quot;&gt;// 나는 예외를 과연 던질까 말까...&lt;/span&gt;

Product &lt;span class=&quot;token function&quot;&gt;purchaseProduct&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;string productId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;productId&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;empty&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;invalid_argument&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Invalid id&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 사실 던졌지롱 ㅋㅋㅋ&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;만약 예외가 발생하면 안되는 곳에서 이 함수를 호출하면 곧바로 프로그램 크래쉬로 이어집니다. 또한 어떤 예외를 던질지 몰라서 어느 예외를 잡아야할지 불분명해집니다. 만약 해당 함수가 수정되어 더 이상 예외를 던지지 않게 되어도 try-catch는 남아있을 것이며 다른 협업자들은 여전히 예외를 고려하며 함수를 호출할 겁니다. null 문제와 비슷하게 C++의 경우 &lt;code&gt;throw()&lt;/code&gt; 키워드나 주석으로 문서화하는 것이 최선입니다.&lt;/p&gt;
&lt;p&gt;이 문제를 해결하기 위해 Java는 반드시 선언부에 예외들을 기록하도록 강제하였습니다. 다만 모든 예외를 기록해야하는 것은 아니고 &lt;code&gt;RuntimeException&lt;/code&gt;을 상속받지 않는 예외 클래스만 기록해야 합니다. 이것을 &lt;strong&gt;Checked Exception&lt;/strong&gt;이라고 부릅니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Java 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;PurchaseException&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;PurchaseException&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// throws PurchaseException을 넣지 않으면 컴파일이 되지 않는다&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Product&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;purchaseProduct&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; productId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;PurchaseException&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;productId&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;isEmpty&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;PurchaseException&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Invalid id&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;/* ... */&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;하지만 Java의 Checked Exception 또한 많은 문제점을 내포하고 있습니다. 이렇게 선언부에 추가해 try-catch를 강제하더라도 부주의한 개발자들이 Checked Exception을 잡아서 &lt;code&gt;RuntimeException&lt;/code&gt;으로 감싸 다시 던지는 방법으로 회피를 한다는 의견도 있으며 선언부에 입력해야할 에러의 종류도 호출하는 함수가 많아질수록 불어납니다. 또한 예외가 어디서 발생하는지 명확하게 알 수 없는 문제는 여전히 남아있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Java 코드&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Product&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;purchaseProduct&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt; productId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;throws&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 이 많은 함수들중에서 언제 어디서 exception이 던져질까?&lt;/span&gt;
  &lt;span class=&quot;token class-name&quot;&gt;ShopSystem&lt;/span&gt; shopSystem &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getShopSystem&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token class-name&quot;&gt;CalendarSystem&lt;/span&gt; calendarSystem &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getCalendarSystem&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token class-name&quot;&gt;Date&lt;/span&gt; date &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; calendarSystem&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getCurrentDate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token class-name&quot;&gt;Wallet&lt;/span&gt; wallet &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getWallet&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token class-name&quot;&gt;Product&lt;/span&gt; product &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; shopSystem&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getProduct&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;productId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  transactionManager&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;makeTransaction&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;product&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; wallet&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; product&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이러한 코드를 유지 보수하는 것은 지뢰밭을 건너는 것과 같습니다. 프로젝트와 코드 베이스에 대한 현저한 이해 없이 수정하는 것은 가능하면 피해야 합니다. 또한 선언부에 Exception이 추가될 때마다 그 함수를 호출하는 다른 모든 함수에도 마찬가지로 Exception을 추가해야하는 것이 대부분입니다. 그리고 코드를 읽어도 언제 어디서 예외가 던져지는지 파악하기가 어렵습니다. Rust의 경우 다음과 같이 완화됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Rust 코드&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// Product 타입이 아닌 Result&amp;lt;Product, MyError&gt; 타입을 출력하기 때문에&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// 에러 처리를 강제합니다.&lt;/span&gt;
pub fn &lt;span class=&quot;token function&quot;&gt;purchaseProduct&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;productId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;str&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Result&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;MyError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Rust의 경우 에러가 발생할 수 있는 함수 호출은 에러가 발생할 경우 &apos;?&apos; 기호로 에러를 리턴한다.&lt;/span&gt;
  let shopSystem &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getShopSystem&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 에러 발생가능&lt;/span&gt;
  let calendarSystem &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getCalendarSystem&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  let date &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; calendarSystem&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getCurrentDate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 에러 발생가능&lt;/span&gt;
  let wallet &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getWallet&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 에러 발생가능&lt;/span&gt;
  let product &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; shopSystem&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getProduct&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;productId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  transactionManager&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;makeTransaction&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;product&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; wallet&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 에러 발생가능&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; product&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이외에도 여러가지 문제점들이 있지만 결론적으로는 Checked Exception을 실패한 개념으로 여기는 것이 정론이며 차세대 OOP 언어들은 Checked Exception을 지원하지 않는 경우가 많습니다.&lt;/p&gt;
&lt;p&gt;그렇기 때문에 Scala와 Rust는 예외의 문제점들을 피하기 위해 에러와 출력 값을 Sum Type으로 컴포지션하여 해결했습니다. 비슷하게 Go도 exception 대신 error를 다중 출력하는 방향으로 언어가 디자인 되었습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;java&quot;&gt;&lt;pre class=&quot;language-java&quot;&gt;&lt;code class=&quot;language-java&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Scala 코드&lt;/span&gt;
def &lt;span class=&quot;token function&quot;&gt;purchaseProduct&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;productId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Either&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;PurchaseError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Rust 코드&lt;/span&gt;
pub fn &lt;span class=&quot;token function&quot;&gt;purchase_product&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;product_id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;str&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Result&lt;/span&gt;&lt;span class=&quot;token generics&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;PurchaseError&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// Go 코드&lt;/span&gt;
func &lt;span class=&quot;token function&quot;&gt;purchaseProduct&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;productId string&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Product&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 언어들이 어떠한 방식으로 타입 이론의 기초적인 요소들을 활용하였는지를 알아봤습니다. 하지만 합 타입과 곱 타입만으로는 아직 이해하기 쉽고 확장하기 쉬운 소프트웨어 아키텍처를 구현할 수 없습니다. &lt;a href=&quot;/posts/programming-languages-2-polymorphism&quot;&gt;다음 편&lt;/a&gt;은 코드의 재사용성을 높이는 **다형성(Polymorphism)**이 어떠한 방식으로 이러한 문제를 해결하는지 알아볼 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;참고자료&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://hinchman-amanda.medium.com/null-pointer-references-the-billion-dollar-mistake-1e616534d485&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Type_theory&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://en.wikipedia.org/wiki/Type_theory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://stackoverflow.com/questions/613954/the-case-against-checked-exceptions&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://stackoverflow.com/questions/613954/the-case-against-checked-exceptions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tech.devsisters.com/posts/gatsby-l10n-with-fp/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://tech.devsisters.com/posts/gatsby-l10n-with-fp/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[개발자에서 기술 작가로 전직하기까지 2년 간의 이야기]]></title><description><![CDATA[소프트웨어 엔지니어에서 테크니컬 라이터로 전직하기까지, 그리고 지금은 테크니컬 라이터로서 어떤 일을 하는지 간단히 정리해 보았습니다.]]></description><link>https://tech.devsisters.com/posts/from-engineer-to-techwriter</link><guid isPermaLink="false">https://tech.devsisters.com/posts/from-engineer-to-techwriter</guid><pubDate>Fri, 08 Jul 2022 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;자기-소개&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9E%90%EA%B8%B0-%EC%86%8C%EA%B0%9C&quot; aria-label=&quot;자기 소개 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;자기 소개&lt;/h2&gt;
&lt;p&gt;안녕하세요. 어느덧 15년째 건강 보험료를 납부하면서 일하고 있고, 아주 어렸을 적 경력까지 포함하면 어느덧 20년째 IT 업계에서 일하고 있는, 흔히 말하는 &quot;고인물&quot; 소프트웨어 엔지니어 남정현입니다. 아니, 최근에는 엔지니어가 아닌 기술 작가로서 새롭게 인사를 드립니다.&lt;/p&gt;
&lt;h2 id=&quot;기술-작가란-무엇인가&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B8%B0%EC%88%A0-%EC%9E%91%EA%B0%80%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80&quot; aria-label=&quot;기술 작가란 무엇인가 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;기술 작가란 무엇인가&lt;/h2&gt;
&lt;p&gt;소프트웨어 엔지니어가 아니라 기술 작가로서 자기 소개를 한다니 의아하게 생각하시는 분들이 많으실 것 같습니다. 이 짧은 소개 한 마디 만으로도 갑자기 주의가 환기되어 저를 유심히 보는 분들도 떠오르네요.&lt;/p&gt;
&lt;p&gt;테크니컬 라이터 (좀 더 쉬운 우리말로 &quot;기술 작가&quot;라고 바꾸어 부르겠습니다.) 라고 불리는 직군의 사람들이 무슨 일을 하는지 생각보다 잘 알려지지 않은 것 같습니다. 전면에 드러나는 일 보다는, 주로 완성된 제품을 실제 사용자들에게 인도하면서 같이 전달되는 사용자 가이드를 작성하는 일에 시간을 많이 쓰기 때문일 것입니다.&lt;/p&gt;
&lt;p&gt;그러다 보니 소프트웨어 엔지니어 일을 하던 사람이 갑자기 본인을 기술 작가라고 소개하는 것의 거리감은 제법 있는 것 같습니다. 클라우드 컴퓨팅 기술이 본격적으로 산업 전반에 영향을 끼치기 전이라면, 확실히 소프트웨어 엔지니어와 기술 작가는 서로 아주 다른 두 가지 영역이라고 보는 것이 타당했을 것입니다.&lt;/p&gt;
&lt;p&gt;하지만 최근에는 이런 경계가 빠른 속도로 옅어지고 있습니다. 소프트웨어 개발자들은 분명 논리적인 체계를 가지고 작동하는 소프트웨어를 만드는 사람이지만, 혼자서 할 수 있는 일의 폭이 작기 때문에 필연적으로 팀을 만들어 협력해야 하며, 팀을 만든다면 자연히 팀원들 사이에 정보를 주고 받을 일이 많아질 수 밖에 없습니다.&lt;/p&gt;
&lt;p&gt;이 과정에서 팀 안에는 다양한 직군의 사람들이 협업 하면서 일을 진행하게 되는데, 각자의 직군에서는 일상적으로 사용하는 언어들이 다른 직군의 사람들에게는 생소하게 들릴 수 있습니다. 그런데 이런 생소함이 꼭 완수해야 하는 일에서 걸림돌이 된다면 어떻게 해야 할까요? 특히 이것이 비즈니스와 비즈니스 사이를 잇는 상황이라면 더욱 가볍게 볼 수 없겠죠.&lt;/p&gt;
&lt;p&gt;즉, 기술 작가는 &lt;strong&gt;복잡한 정보를 독자의 눈높이에 맞추어 다양한 형태의 매체로 알기 쉽게 풀어서 설명해주는 일을 하는 사람&lt;/strong&gt;입니다. 흔히 작가라고 하면 문학적인 재능이 있어야 한다고 이야기하지만, 기술 작가는 그렇지 않습니다.&lt;/p&gt;
&lt;p&gt;기술 작가는 지금까지는 주로 &quot;문서&quot;를 만드는 일에 전념해왔고, 그러다 보니 문서 만을 만드는 사람이라는 고정 관념이 널리 퍼져있었습니다. 뒤에서도 말씀드리겠지만, 이제는 문서 뿐 아니라 다양한 매체로 이런 문제를 해결할 수 있는 것이 중요하다고 말할 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;소프트웨어-엔지니어가-기술-작가로-전향하게-된-이유&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EA%B0%80-%EA%B8%B0%EC%88%A0-%EC%9E%91%EA%B0%80%EB%A1%9C-%EC%A0%84%ED%96%A5%ED%95%98%EA%B2%8C-%EB%90%9C-%EC%9D%B4%EC%9C%A0&quot; aria-label=&quot;소프트웨어 엔지니어가 기술 작가로 전향하게 된 이유 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;소프트웨어 엔지니어가 기술 작가로 전향하게 된 이유&lt;/h2&gt;
&lt;p&gt;소프트웨어 엔지니어로서 일하면서 저는 &quot;무언가를 만든다&quot;는 것에 큰 즐거움과 보람을 느끼며 살아왔습니다.&lt;/p&gt;
&lt;p&gt;그러나 소프트웨어 엔지니어로서 문제를 해결하는 소프트웨어를 만드는 것 이전에, 그 소프트웨어를 만들게 된 상황, 배경, 맥락을 설명하고 기록으로 남기는 &quot;문서화&quot;는 생각만큼 많은 투자를 받지 못한다는 것을 스스로도 깨달았을 뿐 아니라, 현장에서 끊임없이 문제가 반복된다는 사실을 여러 번 접했습니다.&lt;/p&gt;
&lt;p&gt;그러다 보니 IT 업계에서 일하는 모두가 한 목소리로 &quot;문서화가 중요해&quot;라고 마치 약속이라도 한 듯 누구나 같은 말을 하는 모습을 어디서나 접합니다. 하지만, 그토록 중요하다는 &quot;문서화&quot;는 결코 제대로 해결된 적이 없습니다. 왜 일까요?&lt;/p&gt;
&lt;p&gt;제가 보기에 문서화는 소프트웨어를 만들 때 들여야 하는 에너지와 시간을 기준으로 봤을 때 적어도 70% 내외의 에너지와 시간을 들여야 의미 있는 결과물을 만들 수 있기 때문이라고 생각합니다.&lt;/p&gt;
&lt;p&gt;여기에 더해 정말 안타까운 사실은, 문서화 작업으로 만들어진 결과물도 소프트웨어와 마찬가지로 가치가 시간이 지날 수록 하락하기 때문에, 지속적인 유지 보수가 뒷받침되어야 한다는 점입니다.&lt;/p&gt;
&lt;p&gt;다시 말해, 소프트웨어를 만드는 과정과 이 과정을 거치면서 비용을 들이는 것 만큼 문서화에도 투자해야 우리가 생각하는 이상적인 모습을 마주할 수 있는 것입니다.&lt;/p&gt;
&lt;p&gt;저는 이런 문제를 가만히 두고 보는 것이 항상 마음에 걸렸고 늘 안타까웠습니다. 소프트웨어를 &quot;잘 만드는 것&quot;은 차고 넘칠 만큼 충분하게 논의가 되어왔지만 왜 유독 문서화를 포함한 정보 전달은 잘 다루어지지 못하는 가에 대한 안타까움이 항상 있었습니다.&lt;/p&gt;
&lt;p&gt;그래서, 소프트웨어 엔지니어로서의 기술적 역량을 충분히 보유하고 있으면서, 이런 문제를 집중해서 다룰 수 있는 역할을 하는 사람이 있다면 지금보다 더 나은 환경을 만들 수 있지 않을까? 하는 기대로 평소에 문서화와 정보 전달을 위한 노력에 많은 시간을 써왔습니다. 그리고 최근에는 이 방면에 온전히 집중할 수 있도록 기술 작가로서 본격적인 업을 시작하게 되었습니다.&lt;/p&gt;
&lt;h2 id=&quot;달라진-것-달라지지-않은-것&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8B%AC%EB%9D%BC%EC%A7%84-%EA%B2%83-%EB%8B%AC%EB%9D%BC%EC%A7%80%EC%A7%80-%EC%95%8A%EC%9D%80-%EA%B2%83&quot; aria-label=&quot;달라진 것 달라지지 않은 것 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;달라진 것, 달라지지 않은 것&lt;/h2&gt;
&lt;p&gt;소프트웨어 엔지니어로서 일을 할 때는 요구 사항에 맞추어 &quot;동작하는 소프트웨어&quot;를 만드는 것이 중요한 목표였습니다. 기술 작가로 일하는 지금은 &quot;이해하기 쉬운 콘텐츠&quot;를 만드는 것이 중요한 목표가 되었다는 점입니다.&lt;/p&gt;
&lt;p&gt;누가 콘텐츠를 읽거나 접할 것 인지를 생각해보고, 그 사람이 어떤 상황에서 무엇 때문에 콘텐츠를 접하고 있을 것 인지를 상상하며, 어떻게 하면 그 사람에게 최선의 결과를 가져다 줄 것 인지를 고민하며 콘텐츠를 만드는 것입니다.&lt;/p&gt;
&lt;p&gt;소프트웨어를 만들 때는 요구 사항에 맞게 동작하며, 예외적인 입력 값이 주어지더라도 사용자에게 손해가 없도록 안전하게 소프트웨어를 만들고, 사용자가 원하는 목표를 신속하고 효율적으로 달성할 수 있도록 도와주는 것과는 확실히 다른 목표라고 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;사용자가 &quot;원하는 것을 이룰 수 있도록&quot; 해준다는 점에 있어서 둘은 다른 듯 보이지만 하나의 공통된 목표를 지향하고 있으며, 서로 분리되지 않고 긴밀하게 연결되어야 할 것입니다. 그런 맥락에서, 일을 하는 &quot;방법&quot;에는 분명한 변화가 찾아왔지만, 목적지에 도착하는 것에는 여전히 변함이 없다고 생각합니다.&lt;/p&gt;
&lt;p&gt;때로는 글 뿐만 아니라 고도화된 방법으로 정보를 추출하고 가공할 수 있는 일을 해낼 수 있다면 그것이 꼭 글의 형태로 고정되어야 할 이유가 없습니다. 몇 가지 예를 들어보죠.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1688px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 63.055555555555564%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAB2GAAAdhgFdohOBAAAC/0lEQVR42p2Sa0iTURzGX7DoQvWhiLIIopBuRGVmUFgJFl0pqKDoQhERfQuKym5YWVkUMa1IM8tp07xkc3lJLbXNbc6cztrUeUvUVDS7aW5733e/Xmf1WTrwfDjnPOf3/5/nHOHFUxVOi4ba4lRMWjVVBen0936mzphPb+Nr+jvK6HIW09Pwmo9lqTy+ryIlLprU+Htok6PJUt9FnxePOiaSTE0yQuBUgZ3LJnIo2J8TK6agPrWPJlMpmVEniD62isr00+iv76L24h7iDy9l5dxxLA5cyIJ5swia4ccS/9HMn+DHbD+BTevWIARPG0PA2NEcWxvEyeDpqA6EUvQsifelepKirqAL30zy4Y2E797K2SMhnN2/nB0bVhK6bjkh82ewftE0AsaMYsF4gYNbNiAU5urIf5lBtaEMW5mi8lLaW5ro6eimpb6ONoeZhqpy3hkMfLBX0uqowGbWU21WvBYDTqueipIiLCUF1FRWIHz5PkD/QD8ut4v/GV5FjR4v3RJICkMoyNOiSXlGekY6Ol025rpWrM1d1Lf3YrbWkKbNpbq+kXcmC8bmZixf+xR9xdbbQ1OLHafzPUabgZbOVkS3G+Hg7j2smBPIqoAgtqxdz8UnWiI0BeyNySL06BkmTZ7J9q2bWL06hBv3YkgrfMXzHC252jTKrx7HeDwM/f6lfMqKQ1a6FbKTkog5d4H4yGukPIwlMTuft0YLNzMLuZ2YweWISM6HXyLixh1q8nLoUGCdOdl8K8qnW/OITtUV+m6dw20qwTMEdA0OIMnScB5eL6LnF5JXHp4r615Z/JeX2z2IRxR9uXkVj8vjQvTl6P2zr2T4prmBpw47z5uc6Gy1qC5ZeRBVSUKcGbujzWcURQlJkpAVyFBRn2TZJxTJkjhUYRhYVZhDUcJDTIlxWBLUxIbFkhB2F822yzhfGXxASRRH8NZ/Ovz1uY0vjo/8dNYhtnfQXV1Pl7WWH/ZGPH3ffEZZlkcOlCSPLzN5OEQ8sou/x4euNvLfCC7XIL8Bn4xMgrT4B/MAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/1b27458f1eaa703c39159a5cdac9e528/33ecc/image1.webp 360w,
/static/1b27458f1eaa703c39159a5cdac9e528/263a4/image1.webp 480w,
/static/1b27458f1eaa703c39159a5cdac9e528/0c8fb/image1.webp 640w,
/static/1b27458f1eaa703c39159a5cdac9e528/521f7/image1.webp 860w,
/static/1b27458f1eaa703c39159a5cdac9e528/cbd37/image1.webp 1200w,
/static/1b27458f1eaa703c39159a5cdac9e528/f6eaa/image1.webp 1688w&quot;
              sizes=&quot;(max-width: 1688px) 100vw, 1688px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/1b27458f1eaa703c39159a5cdac9e528/53918/image1.png 360w,
/static/1b27458f1eaa703c39159a5cdac9e528/9aebd/image1.png 480w,
/static/1b27458f1eaa703c39159a5cdac9e528/0f09e/image1.png 640w,
/static/1b27458f1eaa703c39159a5cdac9e528/3e3fe/image1.png 860w,
/static/1b27458f1eaa703c39159a5cdac9e528/64756/image1.png 1200w,
/static/1b27458f1eaa703c39159a5cdac9e528/d179e/image1.png 1688w&quot;
            sizes=&quot;(max-width: 1688px) 100vw, 1688px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/1b27458f1eaa703c39159a5cdac9e528/d179e/image1.png&quot;
            alt=&quot;글 뿐만이 아니라 동영상도 정보 교환에서 중요한 수단이라고 생각합니다. (사내 VOD 라이브러리인 OvenTV)&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;글 뿐만이 아니라 동영상도 정보 교환에서 중요한 수단이라고 생각합니다. (사내 VOD 라이브러리인 OvenTV)&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;저는 사내 기술 세미나에서 공유되는 값진 엔지니어들의 지식을 자산화 할 목적으로, 매번 세미나가 있을 때마다 직접 영상을 촬영하고, 동영상을 편집하여 지속적으로 콘텐츠를 올리고 공유하고 있습니다. 이 과정에서 콘텐츠를 알리기 위해 다양한 노력을 기울입니다.&lt;/p&gt;
&lt;p&gt;그리고 매월 제가 속한 팀에서는 정기적으로 타운 홀 미팅을 진행합니다. 팀 내에 속한 각 셀과 태스크 포스 들은 각자의 성과를 공유하고, 우리 팀 뿐 아니라 회사 내의 다른 팀과 자회사들을 위한 기능, 제품, 서비스를 출시하기도 합니다. 이 때 새로운 소식을 취합하여 전사 직원들을 대상으로 누구나 정보를 읽을 수 있도록 소식지를 발행하는 일을 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1519px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 55.27777777777778%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAB2GAAAdhgFdohOBAAABzUlEQVR42pWS30uTYRTH3//BwLsu158heBV03V0g5YVgLQyjYbrctCXNLIuW7MK8SFALuim3dFAQTq1V5IVz9MPRhbr31WLufZ+tfH98fPZuE1kN1oHDgYdzPud7znkUXejk83mEKFIUBcTPDIaexzQtmjHHtrGlW5ZFqVRCye3l0A3DfSib7ThufLuZZmJlgcep1zxMxo88urpIZDnOd1lXNtMyZXPTrS+DlfqONXAw8ZQzUyOcfTJGezRA24Sf05PDnBq9zIngeV6mP1TyJeS4KY5U5FRVuR2rCb7YNK1DnXhGvRLixXP7Ep6wl5OhLloGO3jRCFivsDZy4usad5MxxpPz3F95xYN3C65H3ie4txwjo21VdnhMzD+BVBP+lH7zcSnJ59UU65/W+PHlG9mNDTbXM2xlsw2PpPzNqwDVnMbYrSB3hgPc6L6A/2ovg30+us51cHPAL3dtN6ewlmAUCmTSaZYSMVJzEeLPZ3nzbIqZySiL8XkO5GX/S6Gmqviv9RAOBen39TIS6CfU18P1Kxd5NB5ufuSja0sFO6pGToK3t3dk1FD3fqHu7rK/X2gMrH2berdtC0MY6LqOkB9fCOFGQxcYxQMa1R0CpQocoQIUivUAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/28ee375b9d937b4faf064b0d1ff05869/33ecc/image2.webp 360w,
/static/28ee375b9d937b4faf064b0d1ff05869/263a4/image2.webp 480w,
/static/28ee375b9d937b4faf064b0d1ff05869/0c8fb/image2.webp 640w,
/static/28ee375b9d937b4faf064b0d1ff05869/521f7/image2.webp 860w,
/static/28ee375b9d937b4faf064b0d1ff05869/cbd37/image2.webp 1200w,
/static/28ee375b9d937b4faf064b0d1ff05869/12da5/image2.webp 1519w&quot;
              sizes=&quot;(max-width: 1519px) 100vw, 1519px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/28ee375b9d937b4faf064b0d1ff05869/53918/image2.png 360w,
/static/28ee375b9d937b4faf064b0d1ff05869/9aebd/image2.png 480w,
/static/28ee375b9d937b4faf064b0d1ff05869/0f09e/image2.png 640w,
/static/28ee375b9d937b4faf064b0d1ff05869/3e3fe/image2.png 860w,
/static/28ee375b9d937b4faf064b0d1ff05869/64756/image2.png 1200w,
/static/28ee375b9d937b4faf064b0d1ff05869/28725/image2.png 1519w&quot;
            sizes=&quot;(max-width: 1519px) 100vw, 1519px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/28ee375b9d937b4faf064b0d1ff05869/28725/image2.png&quot;
            alt=&quot;회사 구성원들을 대상으로 발행하는 뉴스레터&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;회사 구성원들을 대상으로 발행하는 뉴스레터&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;또한 콘텐츠를 만들거나, 어딘 가로 이동하는 과정에서 만들어지는 다양한 문서, 이미지, 영상 등의 콘텐츠는 모두 디지털로 저장되는 자산입니다. 디지털로 저장되는 정보는 당연히 개발자가 만드는 코드를 이용하여 처리를 자동화하고, 통찰을 얻을 수 있을 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1889px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 46.944444444444436%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAAA7DAAAOwwHHb6hkAAABCUlEQVR42o1SSZKDMAzkPmGzMfIGBEhIzSH/f16PZJIUTDhw6AJc6k0m09RDu5kxQlGEpg51G/m9R20i8pqQV+0Ol9Kg1A7GXaGtzIWVx8+sVISiXocaN8BfFzSBDfwMEyYmDcmg4uEtFJ/54Z5Mf5h7ESNGlhIw5MP4AYEF384qJe2+IMbEZhSnNCPct85HUFBqDx9vcOEGY8fkXjV+xSadnLd+TKZSfauxE8y5eogLhvkJ3z9AXFvqikCRiO0L9Km4538JEshxFb6kvLYo1B7/Z4+wExRS5IR9/4vWTq+b43TKnRI72KFN+7P+DvmdRKjivZ5Nd5hQ8dJ9tyCEByyLqyakyzor+Af2Sd9aySn4JwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/a4ce9a8042cfb6d29ecb4bc156b01432/33ecc/image3.webp 360w,
/static/a4ce9a8042cfb6d29ecb4bc156b01432/263a4/image3.webp 480w,
/static/a4ce9a8042cfb6d29ecb4bc156b01432/0c8fb/image3.webp 640w,
/static/a4ce9a8042cfb6d29ecb4bc156b01432/521f7/image3.webp 860w,
/static/a4ce9a8042cfb6d29ecb4bc156b01432/cbd37/image3.webp 1200w,
/static/a4ce9a8042cfb6d29ecb4bc156b01432/79645/image3.webp 1889w&quot;
              sizes=&quot;(max-width: 1889px) 100vw, 1889px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/a4ce9a8042cfb6d29ecb4bc156b01432/53918/image3.png 360w,
/static/a4ce9a8042cfb6d29ecb4bc156b01432/9aebd/image3.png 480w,
/static/a4ce9a8042cfb6d29ecb4bc156b01432/0f09e/image3.png 640w,
/static/a4ce9a8042cfb6d29ecb4bc156b01432/3e3fe/image3.png 860w,
/static/a4ce9a8042cfb6d29ecb4bc156b01432/64756/image3.png 1200w,
/static/a4ce9a8042cfb6d29ecb4bc156b01432/240ec/image3.png 1889w&quot;
            sizes=&quot;(max-width: 1889px) 100vw, 1889px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/a4ce9a8042cfb6d29ecb4bc156b01432/240ec/image3.png&quot;
            alt=&quot;기술 작가로서 문서화 엔지니어링을 위한 애플리케이션 개발도 합니다.&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;기술 작가로서 문서화 엔지니어링을 위한 애플리케이션 개발도 합니다.&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;뿐만 아니라 콘텐츠를 분석하고 가공하기 위한 자동화 작업에 프로그래밍 능력을 활용할 수 있다면, 그 가치는 매우 높을 것입니다.&lt;/p&gt;
&lt;p&gt;한정된 인원과 시간이 가끔 걸림돌이 되긴 하나, 저는 기술 작가로 일하는 지금도 여전히 개발자로서 역량을 아낌없이 투자하고 있으며, 동시에 발전시켜 나가고 있습니다.&lt;/p&gt;
&lt;p&gt;이처럼 기술 작가는 누구에게나 열려있고, 어느 한 쪽을 꼭 선택해야 하는 문제가 아닌 다양한 직군이 한 데 어울려 자유롭게 일할 수 있는 새로운 직업이라고 저는 자신 있게 말할 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;기술-작가로서-뿌듯했던-기억&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B8%B0%EC%88%A0-%EC%9E%91%EA%B0%80%EB%A1%9C%EC%84%9C-%EB%BF%8C%EB%93%AF%ED%96%88%EB%8D%98-%EA%B8%B0%EC%96%B5&quot; aria-label=&quot;기술 작가로서 뿌듯했던 기억 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;기술 작가로서 뿌듯했던 기억&lt;/h2&gt;
&lt;p&gt;아직은 기술 작가로서 어떤 일을 할 것 인지를 치열하게 고민하던 중, 스스로의 정체성을 확립하고 일하는 방법을 규정할 수 있도록 도와주었던 중요한 프로젝트 두 가지가 있었습니다.&lt;/p&gt;
&lt;p&gt;두 프로젝트 모두 데브시스터즈 사내 구성원을 위한 내부 서비스 프로젝트였습니다. 그 중 하나는 소프트웨어 엔지니어를 위한 자동화 기능을 제공하는 서비스였으며, 다른 하나는 전사 규모로 진행되는 타운 홀 미팅을 고도화 하는 방송 플랫폼이었습니다. 편의 상 전자를 Cargo, 후자를 DevTalk 이라고 칭하도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1433px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 61.38888888888889%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAAA7DAAAOwwHHb6hkAAABhklEQVR42o2Su07DQBBF/VUIilBQUiDxA0gg0UCFRAF/SICIBpJAAQIUTOLH2l6/17uXGTsGlBiSka5GHnvunJ21hbMd4GIPuNwHzneBkx5wur2WnOMe3g838XawAedoq65Z1fgOanQLPA6A4TX0Q/9b1f0VDNUwuumUGfaBp0HTO25qlh9JeKFEG4XWyCrdZJLB3yHzHFGhoPTPV1YgfPiuCxgDU1WQUQThCwjPQxQGVFMAGS+JIqTe15dnTD9t5Glae1gumbnUXJEZVVAkElkokMYSKX3U1LujKApEBMBSSs0JfR/OdFo3c1RFDpWlKClr/d+Bu8MKnBliz4HhfVG/CALMHAcBTZVSIo7jOrcEKw0ZOyOinBbMkSQJhBAIyJgzv2czPrqmoW02ppveWro5orFtu94L0/EqsiyrB7HaGg/qNFycVJYl0Ra0AkM0DdGimFjPb3olYZ4rIvQwmXzA59+Hju3TxbXiZ/4rpIzXM2RCjxq5iQ3DMERACn+J95vESafhF97SjK7/6TVgAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/4b4d19a29bbf365934aad1bbe999653d/33ecc/image4.webp 360w,
/static/4b4d19a29bbf365934aad1bbe999653d/263a4/image4.webp 480w,
/static/4b4d19a29bbf365934aad1bbe999653d/0c8fb/image4.webp 640w,
/static/4b4d19a29bbf365934aad1bbe999653d/521f7/image4.webp 860w,
/static/4b4d19a29bbf365934aad1bbe999653d/cbd37/image4.webp 1200w,
/static/4b4d19a29bbf365934aad1bbe999653d/e1a9a/image4.webp 1433w&quot;
              sizes=&quot;(max-width: 1433px) 100vw, 1433px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/4b4d19a29bbf365934aad1bbe999653d/53918/image4.png 360w,
/static/4b4d19a29bbf365934aad1bbe999653d/9aebd/image4.png 480w,
/static/4b4d19a29bbf365934aad1bbe999653d/0f09e/image4.png 640w,
/static/4b4d19a29bbf365934aad1bbe999653d/3e3fe/image4.png 860w,
/static/4b4d19a29bbf365934aad1bbe999653d/64756/image4.png 1200w,
/static/4b4d19a29bbf365934aad1bbe999653d/54f3a/image4.png 1433w&quot;
            sizes=&quot;(max-width: 1433px) 100vw, 1433px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/4b4d19a29bbf365934aad1bbe999653d/54f3a/image4.png&quot;
            alt=&quot;Cargo 개발자용 가이드&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Cargo 개발자용 가이드&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Cargo 프로젝트를 진행하는 동안에는 문서화 사이트를 어떻게 제작하여 공유하는 것이 좋을지, 도구와 방법론 적인 관점에서 실력을 가다듬고 최적화 할 수 있는 소중한 시간이었습니다. 당시 제 목표와 의지를 실현할 수 있도록 도와주셨던 인프라 셀의 동료 팀원께 받은 도움은 정말 큰 힘이 되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1389px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 57.50000000000001%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAAA7DAAAOwwHHb6hkAAABg0lEQVR42p1Sy0oDQRDc//EiKnr25smLB0/iQT168eSPREEQ/8CA6LfoLUhI2GST7PuVnZlyqtcJCyYoNhQ93TvVXVOsh5sj4PYY5voQuNyHuToALvZgLLAB7pt/tov30218nGwJ8vMdeHh5gLHQ/R7w+gj99gTTvweee39H/2519pTWcNEYg0JpQa3avvnGuqgaBWU53e9etFhAN41laTR1jTLPUZeFZCV9I98EWtlkoRpZk8YRPgcDjEcjxFEIKAUvCAIsl0uZnmcZJpMJFnbJbDbDfD6XXhzHSNN0dc+YVlNZlhgOhxiPx8IRhb7vo6qq9gk2k5xbdRyQJInUDuxndgHBu1xQFIXALfM4mZtEoSVMp1NEUSQbeaZSvoK1G8T7De1YE15gCbX1jkEC5XMQQfW0gJk1FTuVTlHXAhlYxuGqoHT6RoRhKKrY0/ZPUNZwZpId1irUHekk87nMzj8qcp46/3h2vv8Y2C1aJVpy9zmb8OvAqqqRJTGaPOV0/Ce+AFxqggKBzjNdAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/8b10f99992e0b18d50a9bb9d492e61f8/33ecc/image5.webp 360w,
/static/8b10f99992e0b18d50a9bb9d492e61f8/263a4/image5.webp 480w,
/static/8b10f99992e0b18d50a9bb9d492e61f8/0c8fb/image5.webp 640w,
/static/8b10f99992e0b18d50a9bb9d492e61f8/521f7/image5.webp 860w,
/static/8b10f99992e0b18d50a9bb9d492e61f8/cbd37/image5.webp 1200w,
/static/8b10f99992e0b18d50a9bb9d492e61f8/a2ddc/image5.webp 1389w&quot;
              sizes=&quot;(max-width: 1389px) 100vw, 1389px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/8b10f99992e0b18d50a9bb9d492e61f8/53918/image5.png 360w,
/static/8b10f99992e0b18d50a9bb9d492e61f8/9aebd/image5.png 480w,
/static/8b10f99992e0b18d50a9bb9d492e61f8/0f09e/image5.png 640w,
/static/8b10f99992e0b18d50a9bb9d492e61f8/3e3fe/image5.png 860w,
/static/8b10f99992e0b18d50a9bb9d492e61f8/64756/image5.png 1200w,
/static/8b10f99992e0b18d50a9bb9d492e61f8/eba01/image5.png 1389w&quot;
            sizes=&quot;(max-width: 1389px) 100vw, 1389px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/8b10f99992e0b18d50a9bb9d492e61f8/eba01/image5.png&quot;
            alt=&quot;DevTalk 사용자 (시청자)용 가이드&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;DevTalk 사용자 (시청자)용 가이드&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;그 후 DevTalk 프로젝트에서는 한층 더 폭이 넓어진 독자 층 (방송 시청자, 방송 스탭)을 대상으로 어떻게 해야 의미 있는 사용자 가이드를 만들 수 있을 것 인가에 대한 고민을 할 수 있었던 시기였습니다.&lt;/p&gt;
&lt;p&gt;그리고 제품을 만드는 과정에서 언제 기술 작가가 참여해서 콘텐츠를 저작 하는 것이 좋은지, 어떻게 피드백을 주고 받는 것이 좋은 지를 학습할 수 있었던 소중한 시간이었습니다. 무엇보다도, 이 프로젝트에서 저는 회사 전체에 저를 &quot;기술 작가&quot;로서 알릴 수 있었던 데뷔 프로젝트였기에 뜻 깊은 시간이었습니다.&lt;/p&gt;
&lt;p&gt;두 개의 프로젝트를 마치고 난 후, 제가 작성한 가이드가 실제로 문제 해결에 도움이 되었다는 피드백, 시스템과 서비스를 이해할 때 도움이 되었다는 피드백을 접할 수 있었고, 그 순간 제가 이 일을 올바른 방향으로 잘 해 나가고 있다는 것을 알 수 있어 행복했습니다.&lt;/p&gt;
&lt;h2 id=&quot;기술-작가가-되실-여러분들을-위하여&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B8%B0%EC%88%A0-%EC%9E%91%EA%B0%80%EA%B0%80-%EB%90%98%EC%8B%A4-%EC%97%AC%EB%9F%AC%EB%B6%84%EB%93%A4%EC%9D%84-%EC%9C%84%ED%95%98%EC%97%AC&quot; aria-label=&quot;기술 작가가 되실 여러분들을 위하여 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;기술 작가가 되실 여러분들을 위하여&lt;/h2&gt;
&lt;p&gt;기술 작가는 엔지니어 뿐 아니라, 특정 분야와 기술에 관심이 있다면, 본인이 인문학을 전공했든, 이공계를 전공했든 상관없이 누구나 도전할 수 있는 영역이라고 생각합니다.&lt;/p&gt;
&lt;p&gt;저는 앞에서 &quot;문서&quot;를 만드는 일을 하는 사람이라는 &quot;고정 관념&quot;이 있다고 말했습니다.&lt;/p&gt;
&lt;p&gt;문서를 기술 작가가 제작할 수 있는 하나의 제품이라고 본다면, 문서를 만들기 위해 인문학적 접근이 필요할 수도 있고, 공학적 접근이 필요할 수도 있는 것입니다.&lt;/p&gt;
&lt;p&gt;그리고 &quot;제품&quot;이라면 &quot;문서&quot;만이 최종 결과물이라고 정해 놓을 이유 또한 없습니다. 즉, 저는 이 분야가 특정 분야의 전유물이라고 생각하지 않습니다.&lt;/p&gt;
&lt;p&gt;그러므로 &apos;나는 엔지니어여서 문서를 쓰는 것이 소질이 없다&apos;고 한정 지을 이유가 없고, 인문 계열 전공이라서 &apos;기술을 몰라 문송하다&apos;는 말을 할 이유도 없습니다. 각자가 집중하고 만들어낼 수 있는 결과는 고유한 가치를 지니며, 모두가 소중한 자산이 됩니다.&lt;/p&gt;
&lt;p&gt;옛 속담 중에 &quot;구슬이 서 말이라도 꿰어야 보배다&quot;라는 말이 있듯, 기술 작가에 출신은 중요하지 않다고 생각합니다. 자신의 전공, 적성에 연연하지 마시고 팀 동료, 그리고 새로 들어오는 팀원들을 위해 작게 실천할 수 있는 것부터 조금씩 문서를 써보세요! 틀림없이 누군가는 여러분에게 감사할 것이고, 피드백도 아끼지 않을 겁니다.&lt;/p&gt;
&lt;h2 id=&quot;앞으로의-계획&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%95%9E%EC%9C%BC%EB%A1%9C%EC%9D%98-%EA%B3%84%ED%9A%8D&quot; aria-label=&quot;앞으로의 계획 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;앞으로의 계획&lt;/h2&gt;
&lt;p&gt;우리 회사는 **&amp;#x3C;쿠키런: 킹덤&gt;**의 기록적인 성공과 함께 전세계에서 사랑 받는 IP (지적 재산)을 보유한 회사가 되었다고 생각합니다. 당연히 같이 일하는 사람들이 한국어를 사용하는 사람만 있다고 가정하는 것도 그래서 더는 맞지 않는 말이라고 생각합니다.&lt;/p&gt;
&lt;p&gt;정보를 누구에게나 손쉽게 전달할 수 있으려면, 독자가 한국어 만을 사용할 것이라는 가정에서 벗어나 어떻게 하면 효율적으로 여러 언어로 콘텐츠를 번역해서 출시할 것 인지에 대한 고민도 자연스럽게 하게 됩니다.&lt;/p&gt;
&lt;p&gt;제품을 접하는 고객들을 위한 다국어 작업도 중요하지만, 우리 회사 구성원들을 위한 다국어 정보 공유는 어떻게 하는 것이 좋을 지에 대한 고민이 나날이 깊어지고 있습니다.&lt;/p&gt;
&lt;p&gt;멀지 않은 시일 내에 저는 이 고민을 포함하여 나날이 커지고 있는 회사의 규모에 맞는 비즈니스 테크니컬 라이팅을 효과적으로 수행할 수 있는, 테크니컬 라이팅 내의 세부적인 역할, 직군에 대한 고민도 구체적으로 해보고 싶습니다.&lt;/p&gt;
&lt;p&gt;그리고 고민이 끝날 무렵, 제 자리 옆에 앉아 같이 머리를 맞대고 기술 작문 활동을 고민하는 동료들이 함께 하는 모습을 상상해봅니다.&lt;/p&gt;
&lt;h2 id=&quot;덧붙임&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8D%A7%EB%B6%99%EC%9E%84&quot; aria-label=&quot;덧붙임 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;덧붙임&lt;/h2&gt;
&lt;p&gt;기술 작가로서 일을 시작할 무렵, 라인 (LINE)에서 기술 작가로 일하신다는 분들의 글과 영상을 보며 방향성을 잡고 큰 힘을 얻을 수 있었습니다.&lt;/p&gt;
&lt;p&gt;그 외에도 딱 맞아 떨어지지는 않지만, 테크니컬 라이팅이라는 큰 분야를 이해하는데 도움이 될 만한 커뮤니티에 참여하고, 여러 강의를 보면서, 이 분야에서 일하는 분들이 어떤 일을 하고, 무엇을 중요하게 생각하는지 알아갈 수 있었습니다.&lt;/p&gt;
&lt;p&gt;그 때 제가 도움받았던 자료들 가운데 몇 가지를 골라 기술 작가로 도전하시는 분들을 위해 공유드립니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;블로그&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://engineering.linecorp.com/ko/blog/line-developer-interview-4/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;&apos;문서&apos;로 세상에 도움을 주는 테크니컬 라이터, 강정일&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://koko8829.tistory.com/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;열이아빠의 블로그&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;YouTube 영상&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=kEwdl2B1SQc&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;라인의 전정은 님이 말하는 테크니컬 라이팅의 모든 것&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;온라인, 오프라인 교육&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;http://pen.hanter21.co.kr/jsp/edcourse/edcourse_view.jsp?s_menucd=TF&amp;#x26;s_menu_lcode=0000000001&amp;#x26;moptNo=0&amp;#x26;category=academyGate2&amp;#x26;subjclass=U001G0020002&amp;#x26;tolclass=0003&amp;#x26;subj=F92346&amp;#x26;gryear=2022&amp;#x26;subjseq=0002&amp;#x26;lessclass=0003&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;테크니컬 라이팅과 기술 번역 오프라인 교육&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://olc.kr/course/course_online_view.jsp?id=10108&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;B2B 테크니컬 라이팅, 무료 온라인 교육&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;커뮤니티&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.facebook.com/groups/writethedocsseoul/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;WriteTheDocs Seoul 커뮤니티&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;컨퍼런스&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://summit.stc.org/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Technical Communication Summit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[테스트 케이스 관리를 JIRA로 하자! : 웹서비스셀의 테스트 관리 툴 도입기]]></title><description><![CDATA[엑셀에서 벗어나 JIRA에서 제공하는 테스트 관리 툴을 이용해 테스트 케이스를 작성, 테스트 수행, 결과 리포트 작성, 이슈 관리까지 할 수 있는 방법을 알아봅니다.]]></description><link>https://tech.devsisters.com/posts/jira-tool-qa</link><guid isPermaLink="false">https://tech.devsisters.com/posts/jira-tool-qa</guid><pubDate>Tue, 05 Jul 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요! 웹서비스셀에서 웹 QA를 담당하고 있는 강유경, 송선주입니다. 저희는 데브시스터즈의 웹서비스 품질을 높이는 업무 및 QA 프로세스 개선 업무 등을 진행하고 있습니다. 기존에 엑셀로 관리하던 테스트 케이스(Test Case) 작성을 탈피하고 JIRA에서 제공하는 테스트 관리 툴로 테스트 케이스를 작성부터 테스트 수행, 결과 보고서와 이슈(Issue)를 관리하는 방법에 대해 소개해 드리고자 합니다.&lt;/p&gt;
&lt;h2 id=&quot;엑셀로-테스트-케이스-관리가-점점-어려워져&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%97%91%EC%85%80%EB%A1%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BC%80%EC%9D%B4%EC%8A%A4-%EA%B4%80%EB%A6%AC%EA%B0%80-%EC%A0%90%EC%A0%90-%EC%96%B4%EB%A0%A4%EC%9B%8C%EC%A0%B8&quot; aria-label=&quot;엑셀로 테스트 케이스 관리가 점점 어려워져 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;엑셀로 테스트 케이스 관리가 점점 어려워져&lt;/h2&gt;
&lt;p&gt;대부분의 QA 직군은 테스트를 진행하기 전 테스트 케이스를 작성하며 관리합니다. 테스트를 수행하면서 이슈를 찾고, 어느 부분에서 이슈가 발생했는지 또 수정은 되었는지, 어떤 항목은 이번 환경에서는 제외되었는지, 발생한 이슈로 인해 더는 테스트 진행이 불가한지 등 테스트 케이스에 기록들을 남기고 다양한 활동들을 합니다. 테스트 케이스는 테스트 진행률과 이슈 발생률 등을 한눈에 볼 수 있는 자료로 활용됩니다.&lt;/p&gt;
&lt;p&gt;저희 웹서비스셀에서도 엑셀을 사용해 테스트 케이스를 관리해왔습니다. 게임과 달리 웹서비스셀의 프로젝트는 주기적으로 관리하는 프로젝트보다는 1회성으로 개발 및 검증을 하는 프로젝트가 많습니다. 테스트 케이스 관리 툴로 엑셀을 사용하다 보니 크게 5가지의 불편함을 느낄 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1) 작성해야 하는 테스트 케이스는 많고, 테스트 기간은 너무 짧다&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;웹의 경우 브라우저(Browser)와 기기 별로 검증이 필요하고, 기기 별로 제공되는 브라우저가 달라 해상도까지 고려할 사항이 많습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1896px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 3.6111111111111107%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAABCAYAAADeko4lAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAPElEQVR42jWLSQoAMAgD+/83qRcP4oZPSrHQQ2AyIScz4e5gZixXFcwMIoLufk5VQUSYGUTEy352//z7BYdlQt8oftqGAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/09eaa83b03a1f57a658683f3e8c637da/33ecc/image1.webp 360w,
/static/09eaa83b03a1f57a658683f3e8c637da/263a4/image1.webp 480w,
/static/09eaa83b03a1f57a658683f3e8c637da/0c8fb/image1.webp 640w,
/static/09eaa83b03a1f57a658683f3e8c637da/521f7/image1.webp 860w,
/static/09eaa83b03a1f57a658683f3e8c637da/cbd37/image1.webp 1200w,
/static/09eaa83b03a1f57a658683f3e8c637da/691ec/image1.webp 1896w&quot;
              sizes=&quot;(max-width: 1896px) 100vw, 1896px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/09eaa83b03a1f57a658683f3e8c637da/53918/image1.png 360w,
/static/09eaa83b03a1f57a658683f3e8c637da/9aebd/image1.png 480w,
/static/09eaa83b03a1f57a658683f3e8c637da/0f09e/image1.png 640w,
/static/09eaa83b03a1f57a658683f3e8c637da/3e3fe/image1.png 860w,
/static/09eaa83b03a1f57a658683f3e8c637da/64756/image1.png 1200w,
/static/09eaa83b03a1f57a658683f3e8c637da/c7953/image1.png 1896w&quot;
            sizes=&quot;(max-width: 1896px) 100vw, 1896px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/09eaa83b03a1f57a658683f3e8c637da/c7953/image1.png&quot;
            alt=&quot;테스트 케이스에 기기 별 브라우저 및 해상도 항목을 추가한 모습&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;테스트 케이스에 기기 별 브라우저 및 해상도 항목을 추가한 모습&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;위 이미지에서 보이는 것과 같이 PC는 OS 기준으로 크롬, 사파리, 인터넷 익스플로러 브라우저를 확인하고, 모바일의 경우는 OS 기준으로 크롬, 사파리, 삼성 인터넷, 네이버, 웹 뷰까지 확인합니다.&lt;/p&gt;
&lt;p&gt;그러다 보니 스펙이 크거나 새로운 프로젝트의 경우 Full 테스트 케이스를 작성하게 되면 테스트 케이스가 1,000개가 넘는 경우가 많아 작성 기간도 일주일 이상 소요될 때가 있습니다. 이렇게 되면 짧은 기간 안에 많은 양의 테스트 케이스를 상세하게 쓰기 쉽지 않습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 960px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 32.77777777777778%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAABYlAAAWJQFJUiTwAAABHklEQVR42pVQTUvDQBDN//8F9SoEIigUITn0YIlQQqUJNuwhBxsSTM03JW1iut10n5utNYoK+mDY2Xn73syOkiYx0iRBXW/R43jskIj7auXD8zz4vo8oimS+WCxgWRYcx8FyuYRpmphMJtB1XZ62bUOxXQ+OS7BOCzBh2NBOGKxBCMF4PJYGQRDAdV1M76bQrjQpns/nMAwDqqpiNLqApmmYzWZQmicCZB5YRECDR9BnAs6onDbPc1B6yv8Kpb6/RPtwI+Ia+z6cW3DaSHJbVWCMfTzmnGNXV3ItZxwOHFXFJScNd2z/a7eyLIXg8MUwziN03dCkbSFW1HPvEyYvsSi+nhUDI1AUxf+/HIYhNpvNj2SWZd8M+aeGQ20wfANXQhA5vQgMJQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/faea66537179b3da0d5d9d57c35ede13/33ecc/image2.webp 360w,
/static/faea66537179b3da0d5d9d57c35ede13/263a4/image2.webp 480w,
/static/faea66537179b3da0d5d9d57c35ede13/0c8fb/image2.webp 640w,
/static/faea66537179b3da0d5d9d57c35ede13/521f7/image2.webp 860w,
/static/faea66537179b3da0d5d9d57c35ede13/a6361/image2.webp 960w&quot;
              sizes=&quot;(max-width: 960px) 100vw, 960px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/faea66537179b3da0d5d9d57c35ede13/53918/image2.png 360w,
/static/faea66537179b3da0d5d9d57c35ede13/9aebd/image2.png 480w,
/static/faea66537179b3da0d5d9d57c35ede13/0f09e/image2.png 640w,
/static/faea66537179b3da0d5d9d57c35ede13/3e3fe/image2.png 860w,
/static/faea66537179b3da0d5d9d57c35ede13/a91f8/image2.png 960w&quot;
            sizes=&quot;(max-width: 960px) 100vw, 960px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/faea66537179b3da0d5d9d57c35ede13/a91f8/image2.png&quot;
            alt=&quot;기기와 브라우저를 포함하니 총 테스트 케이스가 4,950개가 된 것을 확인할 수 있습니다.&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;기기와 브라우저를 포함하니 총 테스트 케이스가 4,950개가 된 것을 확인할 수 있습니다.&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 39.166666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAABYlAAAWJQFJUiTwAAABgHRFWHRSYXcACmV4aWYKICAgICAxODAKNDU3ODY5NjYwMDAwNDk0OTJhMDAwODAwMDAwMDA1MDAxYTAxMDUwMDAxMDAwMDAwNGEwMDAwMDAxYjAxMDUwMDAxMDAwMDAwCjUyMDAwMDAwMjgwMTAzMDAwMTAwMDAwMDAyMDAwMDAwMzEwMTAyMDAwYjAwMDAwMDVhMDAwMDAwNjk4NzA0MDAwMTAwMDAwMAo2NjAwMDAwMDAwMDAwMDAwOTAwMDAwMDAwMTAwMDAwMDkwMDAwMDAwMDEwMDAwMDA1MDY4NmY3NDZmNTM2MzYxNzA2NTAwMDAKMDQwMDg2OTIwNzAwMTIwMDAwMDA5YzAwMDAwMDAxYTAwMzAwMDEwMDAwMDAwMTAwMDAwMDAyYTAwNDAwMDEwMDAwMDA4MjBkCjAwMDAwM2EwMDQwMDAxMDAwMDAwNDYwNTAwMDAwMDAwMDAwMDQxNTM0MzQ5NDkwMDAwMDA1MzYzNzI2NTY1NmU3MzY4NmY3NApOwhbkAAABSklEQVR42o2RPU/DMBRFPQR1QNAJgViR2GBgQEhMCIT4H8xI/CgGxNCFhQWxs7CgNnFatQOhNC0N+XBsx3EuzyAx0g5H/orPu89h3e4r+v0Ag0GIMOTg/AVB8ITh8BkB79M8RK/H4fshwf/mQfB79jP67q4798G0rmEMoHUFKYEkucdodAKlryG1hSwJ+T9aA/H0E+PxO1ieP9JGB3F8i9nMcYfJxw1U9YB5JqmARvK1GCk1lFJgQpyiKNZoYwtluYE03cR02oaujiBUjVKahUhlMZ8niKLItXxIMoY8b6HIPUrYQjxhsM0ucmGo2DJQ4VISwgmP0TQehGiTdJVaXwcPaC33kBaaEhukWbUQY2ryNE54AGtdQo8qeSRcoZ/C6A13SFghy5ajqgzqunbCKxKeU8ILYpta3kf0dkbCS6SCPs7NUriE1lp8A+LjQ4CvUmw8AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/c3351e7d31936493f9e188a155337c64/33ecc/image3.webp 360w,
/static/c3351e7d31936493f9e188a155337c64/263a4/image3.webp 480w,
/static/c3351e7d31936493f9e188a155337c64/0c8fb/image3.webp 640w,
/static/c3351e7d31936493f9e188a155337c64/521f7/image3.webp 860w,
/static/c3351e7d31936493f9e188a155337c64/cbd37/image3.webp 1200w,
/static/c3351e7d31936493f9e188a155337c64/548e7/image3.webp 2400w,
/static/c3351e7d31936493f9e188a155337c64/7b33e/image3.webp 3458w&quot;
              sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/c3351e7d31936493f9e188a155337c64/53918/image3.png 360w,
/static/c3351e7d31936493f9e188a155337c64/9aebd/image3.png 480w,
/static/c3351e7d31936493f9e188a155337c64/0f09e/image3.png 640w,
/static/c3351e7d31936493f9e188a155337c64/3e3fe/image3.png 860w,
/static/c3351e7d31936493f9e188a155337c64/64756/image3.png 1200w,
/static/c3351e7d31936493f9e188a155337c64/98e2c/image3.png 2400w,
/static/c3351e7d31936493f9e188a155337c64/aefa9/image3.png 3458w&quot;
            sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/c3351e7d31936493f9e188a155337c64/98e2c/image3.png&quot;
            alt=&quot;아찔한 테스트 케이스, PC와 모바일 별로 작성한 흔적들&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;아찔한 테스트 케이스, PC와 모바일 별로 작성한 흔적들&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;QA직군에 계신 분들이라면 누구나 한 번쯤 이런 테스트 케이스를 작성하고 이를 기반으로 검증을 수행해 보셨을 거라는 생각이 듭니다. 이렇게 테스트 케이스가 길어지면 이를 확인하는데 어려움이 있습니다. 그리고 협업하는 다른 부서의 분들도 확인하는데 어렵다는 피드백을 주셨습니다. 더구나 짧은 기간 안에 브라우저별, PC/모바일별로 기획이 다른 경우라면 시간이 매우 부족한 경우가 많았습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2) 기능 업데이트 시 버전 관리의 어려움이 있다&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;2020년 상반기까지는 in-house 제품 및 사전 예약이 업무의 대부분을 차지했지만, 2020년 하반기부터는 더욱 많은 게임 사이트와 기업 사이트에 대한 업무까지 진행하게 되었습니다. 게임 관련 웹 프로젝트의 경우 게임별, 언어별 등 다양한 케이스가 있다 보니 이걸 엑셀 시트에 추가하더라도 버전 관리가 어려웠습니다. 또한, 완료한 테스트 케이스에서 다음 버전으로 업데이트 시 테스트 케이스를 복제한 뒤 새로 만들어야 합니다. 새로 추가가 된 후 어느 부분이 이번에 새로 추가되었는지 표시까지 해줘야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1216px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 114.16666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAXCAYAAAALHW+jAAAACXBIWXMAABYlAAAWJQFJUiTwAAAEkUlEQVR42l1VSW8jRRQudztODkhzQ/MHWM5c+AEc4IiAM4kGbkgMS+DCL+CAxHYCDihIIyEEGmkGjUYzYjKCjO3EsZ3YWbzGW3vfY3fbvfjjq2onJJT0/KqqX33vve+9Kot4PI5IJKIkFoshHA4jEUsgsZdQe9HoHtLpCuLxPKKRExwclHCQOEMmXac0sB/J43C/gpPDJtKJCkQ+n0ehWIRhGDDqNeoqUqUDxIpRlCsVlMtJHB9v4vBwE7n8JlqNL5DM3cb96C1sH7+Hf/IbiBu3EK+/iwPjM4hWq4VqtYZKpYxqrY2GMcYTI4rfKn+iOzIxHEwVaLWaQrtzCuu8gmjjAX7MfoWt+re40/8Ovw6+x53B17g32ILo9/uo1w2USlX0+7uYTh/AsZ5hMY/CMp9gfP4Inc5DWNYOZlYY83kY9fJjZJN30T57jHEzjGkngknrGcx+EsKyLHjeHIsFYNvrOD8XcNwQZnYAgA7H0elEcC5o4+tqTcd+UuCsLHCaE+j2NQzHAtbsBQjHcWhoQw7X3SCAoIM1ik4JUlaUXiw0ao1WGtMPYXdXR62qMSsNo1EQYwLOZi9BzGazS8DpdJ0f5YdVAmsqQtfVma6MNkA7X1erQeztBVjAAOkIoN3W0WgIZveiH6Hn2cuUN1R6rns1wqCK7GqE5XKIVdfR7WrkX0YZVIFMzWWEkkM5hsMNGgoCr1F0ggQZaZARXgc0jBUkkxqyGQ0nxxpyuSCLKhglAefzuYpQjtlsg6CClVxTxZCArhvk+jpgrbbCC6Ch2dTQ62lM2efQdpaAFxyORutMR8A0Vwn0H4ez2XUODSOInZ0AMpkAU/Y57PVYZet/HDrOLdU2nvccZXVZbSkhAoUutWGsYn9fp3OdqepotVbIo8Bk8jKEaZqXEZqmH6HjhJZRaaraknDblo78PqzXdaRSQlW21RJoNiSvMkL24WAwYP5DVmzCCm/R4H0e/pSAHxHgExrdZpt8QNCPuf8hAT9niq+xeDe4f5NBPE+7m5Qb5PBVCNd1IcVPfUGB0pICf+3P5ZBzOQzjhK/Q78hmt5HPP8XZ2V+URxgMUheN7Rva9pybPa5deVxpz3PI64iVttSeHI1Gk42dZLuUUChUCFzivMIsRxCe5xv5EXhKLhxI7Yt3OZejXq8jkYijWCww7bJ6XGq1Ch2P/cdBts54PFZgtm1fgvGXVNjkdkhtLS/Agjw3+NAeQj597XZbaflqybOi2+0y3Bw7P6keWflxMpnQmyySxWKY3Dd5Z106XXAPTG3IpjZ4CQYs2pR75yzOhOcYofQgX+10Os2WKSuPw+GQ0mOzNriO8vCbBH6be28R9HWm+QP7sISjoyLPdOlwyAgl/3MImepVzjzPXVZ1RIB3KK+wCDq9a6qpZR82m2/w+fqS+9+wvX6i/S/c/5n6LkSn04F//RaKx2w2u2yjOXWGKaVxevo3b8Qe1w3atQkYYx/+wczuMYOn3EvzW5JnTiB6vR5kYSShErBQKKj5fG5D1sc0p+yxh6zkNtdHPJgiYIYUFfjnVaS9Qb5tgoFnPPwLhXYijc9I7DUAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/8137ec5503d4c46c9546b689e7ced5b3/33ecc/image4.webp 360w,
/static/8137ec5503d4c46c9546b689e7ced5b3/263a4/image4.webp 480w,
/static/8137ec5503d4c46c9546b689e7ced5b3/0c8fb/image4.webp 640w,
/static/8137ec5503d4c46c9546b689e7ced5b3/521f7/image4.webp 860w,
/static/8137ec5503d4c46c9546b689e7ced5b3/cbd37/image4.webp 1200w,
/static/8137ec5503d4c46c9546b689e7ced5b3/de35b/image4.webp 1216w&quot;
              sizes=&quot;(max-width: 1216px) 100vw, 1216px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/8137ec5503d4c46c9546b689e7ced5b3/53918/image4.png 360w,
/static/8137ec5503d4c46c9546b689e7ced5b3/9aebd/image4.png 480w,
/static/8137ec5503d4c46c9546b689e7ced5b3/0f09e/image4.png 640w,
/static/8137ec5503d4c46c9546b689e7ced5b3/3e3fe/image4.png 860w,
/static/8137ec5503d4c46c9546b689e7ced5b3/64756/image4.png 1200w,
/static/8137ec5503d4c46c9546b689e7ced5b3/9d8cf/image4.png 1216w&quot;
            sizes=&quot;(max-width: 1216px) 100vw, 1216px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/8137ec5503d4c46c9546b689e7ced5b3/9d8cf/image4.png&quot;
            alt=&quot;노란색, 초록색으로 형형색색 추가/수정된 테스트 케이스를 표시해준 모습&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;노란색, 초록색으로 형형색색 추가/수정된 테스트 케이스를 표시해준 모습&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3) JIRA에 등록한 이슈를 또 엑셀에 복사/붙여넣기를 해야 한다&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;엑셀에 JIRA 이슈 링크를 연동할 때 보통 하나의 케이스에 여러 개 이슈를 추가하게 됩니다. 이때, 가끔 알 수 없는 이유로 최상위에 있는 이슈만 하이퍼링크(Hyperlink)가 연결되는 것을 경험하신 분들이 많으실 겁니다.&lt;/p&gt;
&lt;p&gt;해당 이슈를 보고 싶을 때 직접 URL을 입력해서 들어가거나 아예 JIRA 화면을 띄운 채 들어가야 하는 불편함이 있습니다.&lt;/p&gt;
&lt;p&gt;이슈를 등록하고 하나의 JIRA 티켓 링크를 복사하고, 다시 엑셀에 붙여 넣고, 하이퍼링크는 한 개만 연결되는 이런 과정들이 계속 반복되어 피로감이 있었습니다.&lt;/p&gt;
&lt;p&gt;또한, 별도로 이슈 관리 시트도 만들었는데 이슈가 수정되었을 경우 일일이 수정 확인 후 상태 변경이 필요했습니다. 이런 과정들이 모두 수작업으로 진행되다 보니 소요되는 시간이 상당했습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4) 이전 테스트 결과를 보려면 과거 테스트 케이스를 일일이 열어봐야 한다&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;기능 업데이트 시 버전 관리의 어려움과 연결되는 부분인데, 이전 테스트 결과를 보려면 과거 테스트 케이스를 하나하나 열어서 확인해야 합니다. 과거부터 쌓아온 테스트 케이스이고, 히스토리 관리가 잘 되었다고 하더라도 수작업으로 진행하고 있어 많은 비효율이 발생하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5) 이미지 삽입 등의 첨부 파일을 삽입 할 경우 테스트 케이스 템플릿이 한정적이기 때문에 보기 및 관리가 어렵다&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;아무래도 엑셀을 사용하기 때문에 이미지가 삽입되면 셀의 크기가 커지기 마련입니다. 1,000개가 넘어가는 테스트 케이스에 이미지까지 추가된다면 문서 한 페이지의 크기가 너무 커져 각각의 테스트 케이스를 확인하는 것이 아주 어려워집니다. 셀 내에 이미지를 삽입할 수 있으나 셀의 크기가 작다면 이미지의 크기도 작아져 따로 확대해서 이미지를 확인하고, 다시 줄이는 일련의 과정을 거쳐야 합니다.&lt;/p&gt;
&lt;p&gt;위와 같은 문제들로 인해 저희는 공통으로 한 가지를 생각하게 됐습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;그러면 우리 엑셀에서 벗어나 보는 건 어떨까요?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;엑셀에서-벗어나서-어떤-테스트-관리-툴을-사용해볼까&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%97%91%EC%85%80%EC%97%90%EC%84%9C-%EB%B2%97%EC%96%B4%EB%82%98%EC%84%9C-%EC%96%B4%EB%96%A4-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B4%80%EB%A6%AC-%ED%88%B4%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%B4%EB%B3%BC%EA%B9%8C&quot; aria-label=&quot;엑셀에서 벗어나서 어떤 테스트 관리 툴을 사용해볼까 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;엑셀에서 벗어나서 어떤 테스트 관리 툴을 사용해볼까?&lt;/h2&gt;
&lt;p&gt;여러 가지 많은 테스트 관리 툴이 있지만 저희는 JIRA와 연동이 가능한 QAlity와 Zephyr Scale을 선정했습니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;어떻게 하면 테스트 케이스를 줄이고, 테스트 수행부터 결과까지의 시간을 단축 할 수 있을까?&lt;/code&gt;에 대한 고민을 다른 셀의 QA직군에 계신 분과 이야기를 나누다가 QAlity를 제안받았습니다. QAlity는 쉽게 앱 설치가 가능했고, 다양한 상황에서 유동적으로 대응이 가능하다는 것에 눈길이 갔습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://medium.com/storicity-tech/qality%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%B4%88%EA%B8%B0-%EC%A0%9C%ED%92%88-%EA%B0%9C%EB%B0%9C%ED%8C%80%EC%9D%98-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%A0%81%EC%9A%A9%EA%B8%B0-6f1ffe0a8d8f&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;QAlity를 활용한 초기 제품 개발팀의 테스트 적용기&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;한 가지 툴만 사용하기 보다는 다른 툴도 비교해보고 싶다는 생각이 들어 인터넷 검색을 통해 ‘상위 20개 최고의 테스트 관리 도구 목록’의 블로그 글 내용에서 Zephyr Scale을 알게 되었습니다. 사용하기 쉽고, 유연해 시작하는데 최소한의 노력만 들일 수 있다는 문구가 매력적으로 느껴졌습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://ko.myservername.com/top-20-best-test-management-tools#1_Zephyr_Scale&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;상위 20개 최고의 테스트 관리 도구 목록&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이 두 가지 테스트 관리 툴로 웹서비스셀의 프로세스와 얼마나 잘 맞을지 비교 검증하게 되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1) 테스트 케이스 작성&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;기존과 제일 큰 차이점은 바로 Test Step(재현 방법)과 Expected Result(기대 결과)만으로 작성이 가능한 점이었습니다. 이전에는 대분류/중분류/소분류/사전 조건/재현 방법/기대 결과 이런 식으로 작성했다면, 재현 방법과 기대 결과만 작성하니 작성 시간이 굉장히 줄어들었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1691px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 45.55555555555556%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAABcRAAAXEQHKJvM/AAABRHRFWHRSYXcACmV4aWYKICAgICAxNTAKNDU3ODY5NjYwMDAwNDk0OTJhMDAwODAwMDAwMDA1MDAxYTAxMDUwMDAxMDAwMDAwNGEwMDAwMDAxYjAxMDUwMDAxMDAwMDAwCjUyMDAwMDAwMjgwMTAzMDAwMTAwMDAwMDAyMDAwMDAwMzEwMTAyMDAwYjAwMDAwMDVhMDAwMDAwNjk4NzA0MDAwMTAwMDAwMAo2NjAwMDAwMDAwMDAwMDAwOTYwMDAwMDAwMTAwMDAwMDk2MDAwMDAwMDEwMDAwMDA1MDY4NmY3NDZmNTM2MzYxNzA2NTAwMDAKMDMwMDAxYTAwMzAwMDEwMDAwMDAwMTAwMDAwMDAyYTAwNDAwMDEwMDAwMDA5YjA2MDAwMDAzYTAwNDAwMDEwMDAwMDAwMDAzCjAwMDAwMDAwMDAwMArYTxFBAAABvUlEQVR42nVRwYoUMRDtm3gQ9Kir4L8JXl3xusii4Aeo7MEf8OB9YFlmLgODzMHxtqcRtXE29s50d5JO0p1UdXyJs7IKNryuSlXl9XvVRfH3cxO4Dzzc59frt/b5HeABcBe4DdwDDoAbuSuq7eFG7J4R0WFVVUeLxeLtfD4/Wa/XL2OMT4Cn5+fr4/cf5setcY+Xy+WLyWTybjqdvlmtVq9ms9nrs7PTk4+fvzzfNupR4Z314xhjwv8eojFumz4SM+by4L/TTHj5wGVhrXM+EGMw1ci6nkW1410tyVhHPjWZWGvFxhgCWErJbduytZZS/LERwXQmKq2/Fq4fPBH/+RTuRxDGzcVlrBuVa0mVUjpq3UUQImqcVQRhjt/KDbdSp3NZgM+P1/wGotjKLvb9EIfBZ2+ZECRd95swkUBlJpRSxcvtjtMs+slyPxjjrvYyhhBGpbsRyjNCWiDqbStHEI3GdDmCcAQBcp16WE0m/F54n9d0tewc0zl4ypH3dWMsFDn8IIL6PqsjCtlJ0yp2UDgMQ1lAagnbP3FHJPgQhFRGQJ1AXTBzrkOFgD1hrRFQiFwK2M95XTcXyGvg0y9us1xncqologAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/366dd79a6a45704836588ea17ef49e5f/33ecc/image5.webp 360w,
/static/366dd79a6a45704836588ea17ef49e5f/263a4/image5.webp 480w,
/static/366dd79a6a45704836588ea17ef49e5f/0c8fb/image5.webp 640w,
/static/366dd79a6a45704836588ea17ef49e5f/521f7/image5.webp 860w,
/static/366dd79a6a45704836588ea17ef49e5f/cbd37/image5.webp 1200w,
/static/366dd79a6a45704836588ea17ef49e5f/8c89f/image5.webp 1691w&quot;
              sizes=&quot;(max-width: 1691px) 100vw, 1691px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/366dd79a6a45704836588ea17ef49e5f/53918/image5.png 360w,
/static/366dd79a6a45704836588ea17ef49e5f/9aebd/image5.png 480w,
/static/366dd79a6a45704836588ea17ef49e5f/0f09e/image5.png 640w,
/static/366dd79a6a45704836588ea17ef49e5f/3e3fe/image5.png 860w,
/static/366dd79a6a45704836588ea17ef49e5f/64756/image5.png 1200w,
/static/366dd79a6a45704836588ea17ef49e5f/6210f/image5.png 1691w&quot;
            sizes=&quot;(max-width: 1691px) 100vw, 1691px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/366dd79a6a45704836588ea17ef49e5f/6210f/image5.png&quot;
            alt=&quot;QAlity와 Zephyr Scale로 작성한 테스트 케이스&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;QAlity와 Zephyr Scale로 작성한 테스트 케이스&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2) 테스트 수행&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;작성한 테스트 케이스를 수행하기 위해선 테스트 사이클(Test Cycle)을 설정해야 합니다. 테스트 사이클은 작성한 테스트 케이스를 실행할 수 있게 해주는 기능입니다. 테스트 케이스와 연결하고 실행하면 본격적으로 수행할 수 있는 환경이 만들어집니다.&lt;/p&gt;
&lt;p&gt;테스트하면서 결과를 직관적으로 확인할 수 있고, 이슈와 테스트 케이스 간에 추적성이 높아졌습니다. 이런 방식으로 JIRA 티켓 복사/붙여넣기에서 해방되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2002px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 53.05555555555556%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAABcRAAAXEQHKJvM/AAABRHRFWHRSYXcACmV4aWYKICAgICAxNTAKNDU3ODY5NjYwMDAwNDk0OTJhMDAwODAwMDAwMDA1MDAxYTAxMDUwMDAxMDAwMDAwNGEwMDAwMDAxYjAxMDUwMDAxMDAwMDAwCjUyMDAwMDAwMjgwMTAzMDAwMTAwMDAwMDAyMDAwMDAwMzEwMTAyMDAwYjAwMDAwMDVhMDAwMDAwNjk4NzA0MDAwMTAwMDAwMAo2NjAwMDAwMDAwMDAwMDAwOTYwMDAwMDAwMTAwMDAwMDk2MDAwMDAwMDEwMDAwMDA1MDY4NmY3NDZmNTM2MzYxNzA2NTAwMDAKMDMwMDAxYTAwMzAwMDEwMDAwMDAwMTAwMDAwMDAyYTAwNDAwMDEwMDAwMDBkMjA3MDAwMDAzYTAwNDAwMDEwMDAwMDAyODA0CjAwMDAwMDAwMDAwMAoaf4oiAAACGklEQVR42m1SyW4TQRAdiTsSm+DMn/EBSMmFG+IIOAkHE5QLH8AH5BAFxRIHS8jyYEcmsgZHMR4nXma8z9LTXd3zqJ4sColbejOlrldVr6vKcf4/TxgvGY/u3D9kPLiynzJeXHEfM54xnt8wdZ5vaK03cv7/rNW2D78f7bqNxnsArxXfZ5nadJu/SnGavuO7N41m80u1Wv1ar9f3PM/7ZO3jVqvMvrcx8MphA4oMhNQwOQpcH2U0BCnonJDfdtw9xhhojg8nrjMO5vTXH9Gf0z4NR1OdJEIbw16AwuWcuvOQZMfTQeUHJbU6kedZp2YKcR4qyEmi0Olgdfy75oSTBYbjGS4GIcbhHKmQrPJSTbhaohuOQFmKbL4AggA4OwPS9J5G+0lE5jqT6QI9fwSv4yMoEma4flwkUkySCOl0jLDtQfZ8wO8Deb42ocqU69gk/nmAbm8Iq1awwoLPmEUr9Bcz6EEfSauFuHUCfnJRMOe28SALGNtDKyBNXWe5jBEngpti+6oLWLKNIiLwpBGLCHE4QS7VOnU3CpnvOiKTxElIKSKlDSWZ1MLahv3MYSbF4YiSdpvMbEbcP66iyMbfgrQJhcxqji2oeSXsWM1sDhqMWTtvFNt5/wI8R0gSl4OSHBezr1iC+wq1Nq7Dzy+xbfEB9VpZfN79Rvv7e/Lw6CMqlS0uWxLd063VwcEOtU52mLcO24zyMoo2/wFxf+38GXfqagAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/35a3c67ac5215a59bf231d1587655a40/33ecc/image6.webp 360w,
/static/35a3c67ac5215a59bf231d1587655a40/263a4/image6.webp 480w,
/static/35a3c67ac5215a59bf231d1587655a40/0c8fb/image6.webp 640w,
/static/35a3c67ac5215a59bf231d1587655a40/521f7/image6.webp 860w,
/static/35a3c67ac5215a59bf231d1587655a40/cbd37/image6.webp 1200w,
/static/35a3c67ac5215a59bf231d1587655a40/9ad5f/image6.webp 2002w&quot;
              sizes=&quot;(max-width: 2002px) 100vw, 2002px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/35a3c67ac5215a59bf231d1587655a40/53918/image6.png 360w,
/static/35a3c67ac5215a59bf231d1587655a40/9aebd/image6.png 480w,
/static/35a3c67ac5215a59bf231d1587655a40/0f09e/image6.png 640w,
/static/35a3c67ac5215a59bf231d1587655a40/3e3fe/image6.png 860w,
/static/35a3c67ac5215a59bf231d1587655a40/64756/image6.png 1200w,
/static/35a3c67ac5215a59bf231d1587655a40/2206a/image6.png 2002w&quot;
            sizes=&quot;(max-width: 2002px) 100vw, 2002px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/35a3c67ac5215a59bf231d1587655a40/2206a/image6.png&quot;
            alt=&quot;QAlity와 Zephyr Scale의 테스트 수행 및 이슈 연결한 모습&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;QAlity와 Zephyr Scale의 테스트 수행 및 이슈 연결한 모습&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3) 테스트 결과&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;테스트 사이클로 테스트를 완료하면 자동으로 결과가 생성됩니다. QAlity는 테이블과 상태 그래프, 담당자 그래프 형태로 결과를 볼 수 있었습니다. Zephyr Scale의 경우는 다양하게 결과를 나타낼 수 있는데 그중에서 전체적으로 볼 수 있는 항목(&lt;code&gt;Test execution results (summary)&lt;/code&gt;)을 선택해 종합적인 결과를 확인했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1922px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 78.61111111111111%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAQCAYAAAAWGF8bAAAACXBIWXMAABcRAAAXEQHKJvM/AAABRHRFWHRSYXcACmV4aWYKICAgICAxNTAKNDU3ODY5NjYwMDAwNDk0OTJhMDAwODAwMDAwMDA1MDAxYTAxMDUwMDAxMDAwMDAwNGEwMDAwMDAxYjAxMDUwMDAxMDAwMDAwCjUyMDAwMDAwMjgwMTAzMDAwMTAwMDAwMDAyMDAwMDAwMzEwMTAyMDAwYjAwMDAwMDVhMDAwMDAwNjk4NzA0MDAwMTAwMDAwMAo2NjAwMDAwMDAwMDAwMDAwOTYwMDAwMDAwMTAwMDAwMDk2MDAwMDAwMDEwMDAwMDA1MDY4NmY3NDZmNTM2MzYxNzA2NTAwMDAKMDMwMDAxYTAwMzAwMDEwMDAwMDAwMTAwMDAwMDAyYTAwNDAwMDEwMDAwMDA4MjA3MDAwMDAzYTAwNDAwMDEwMDAwMDBlNjA1CjAwMDAwMDAwMDAwMAoooW5aAAACqUlEQVR42o2TS09TQRTHwZhgXLnQGB8sTPwKLvgC7ljZb+DOoCZKCIEYFy7cuCHEuDVEmyoRozExsekCSKAVoxgQhKJNhYLXWPq4r7l3Zs7fM/fephTxMc2/c+88fvM/58zt6upsR1m9rDOsQ/vmjrMOJ889rHOsk6xjrBOs86xTXXXbHQBgdKVcLo9ks9nxXC43ZlnWkBkzUkoNTkxM9BsSv3dz1z06OnrxxatXD2byc3fz+fydvr6+CxEY/24y6W8mwCNJf0nx33LThtTa4cezkfcwDJWUSmmtlWn8roUINJFWkn88LgyNxzuBEimfAixWV7TUsgEPvRGwVrfJcT3iDcRYYjbxEaShiTfTHocD+4ESGsXmF/DBDq+MHdquj4YjUGs4CEIJUhRhiCiicG8iA5uPgKVSKQIKIVOtnGgNh9fFQKUVDxg/MWC5VsHs93V4MkgWa3WQQ7bNQA2/WubTQptaOYw2JbCl3U0MFp7g6txjZL4W/gPIuWUD0IodHgCc3lnD9XwaNwoZjK/kOkL+DSjjkHV8aDtknkAoQ2ip4AQ+0hvzGFt6g426FeWS5/+aQxMAz7WBpsKeL6jpuBRwpU21gyDgS6MpkCFJc6IBJg5bwJbDpLWBvhCw3QDClyY+kI6r3Gp/qrIBvq6UUbGbqPle+2Kb9Ak28YmTa4qyvGv6Le63sPjzG354zRiITqArRKomJF81zSnbk0NjoiocuvVuirjCNFR4Gml4YZIG5h7RzM5aFHK4z+F2czuV2ZzCvdX7yO5M29h7bWqBh9sfX2Lo/TMMf5iKNLL4HNcWMpi1ipHDMHHYvjYypTk3Nhz+6kT7S+HqWg3ftSaLb630+ryVXouVKRash59nrdVqZdvc30AFl+MUUU8C7mfVWRbDSi7c0wb4C2ZbjlK6TvYNAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/e0c16beac29aa318bc9073b34ba15774/33ecc/image7.webp 360w,
/static/e0c16beac29aa318bc9073b34ba15774/263a4/image7.webp 480w,
/static/e0c16beac29aa318bc9073b34ba15774/0c8fb/image7.webp 640w,
/static/e0c16beac29aa318bc9073b34ba15774/521f7/image7.webp 860w,
/static/e0c16beac29aa318bc9073b34ba15774/cbd37/image7.webp 1200w,
/static/e0c16beac29aa318bc9073b34ba15774/c8b27/image7.webp 1922w&quot;
              sizes=&quot;(max-width: 1922px) 100vw, 1922px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/e0c16beac29aa318bc9073b34ba15774/53918/image7.png 360w,
/static/e0c16beac29aa318bc9073b34ba15774/9aebd/image7.png 480w,
/static/e0c16beac29aa318bc9073b34ba15774/0f09e/image7.png 640w,
/static/e0c16beac29aa318bc9073b34ba15774/3e3fe/image7.png 860w,
/static/e0c16beac29aa318bc9073b34ba15774/64756/image7.png 1200w,
/static/e0c16beac29aa318bc9073b34ba15774/e05f4/image7.png 1922w&quot;
            sizes=&quot;(max-width: 1922px) 100vw, 1922px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/e0c16beac29aa318bc9073b34ba15774/e05f4/image7.png&quot;
            alt=&quot;QAlity와 Zephyr Scale의 결과&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;QAlity와 Zephyr Scale의 결과&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;사용한 경험을 토대로 이제 2가지 툴의 장/단점을 비교해보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;QAlity의 장/단점은 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;QAlity 장점&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;오타 및 새로 변경할 사항이 있으면 바로 수정할 수 있습니다.&lt;/li&gt;
&lt;li&gt;테스트 케이스 작성에 필요한 항목이 줄어들었습니다.&lt;/li&gt;
&lt;li&gt;테스트 케이스에 이슈를 바로 등록/연결할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;QAlity 단점&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;상태가 Not Executed, In Progress, Pass, Fail, Blocked밖에 없습니다.
&lt;ul&gt;
&lt;li&gt;5가지만 사용할 수 있으며, 추가 및 수정 불가합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;테스트 케이스가 새 창이 아닌 현재 페이지에서 열리고 로딩이 깁니다.
&lt;ul&gt;
&lt;li&gt;각 케이스 별로 찾는 시간이 많이 소요됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Zephyr Scale의 장/단점은 아래와 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Zephyr Scale 장점&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;테스트 케이스를 작성하고, 테스트 사이클을 연결하고, 테스트 케이스를 실행하는 것까지 자연스럽게 연결되어 있어 편하게 관리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;테스트를 수행하는데 걸리는 시간을 기록합니다.&lt;/li&gt;
&lt;li&gt;테스트 케이스에 이슈를 바로 등록/연결할 수 있습니다.&lt;/li&gt;
&lt;li&gt;작성한 테스트 케이스를 엑셀이나 다른 파일로 변환할 수 있습니다.&lt;/li&gt;
&lt;li&gt;상태가 Not Executed, In Progress, Pass, Fail, Blocked이 디폴트로 있고, 설정을 통해 상태를 추가할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Zephyr Scale 단점&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;오타 및 새로 변경될 사항이 있으면 테스트 케이스를 수정하고 테스트 사이클을 갱신해야 하는 추가 작업을 해야 합니다.&lt;/li&gt;
&lt;li&gt;이슈 등록 시 재현 방법과 기대 결과가 반영될 줄 알았으나 직접 입력해야 하는 점이 아쉬웠습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;위 2가지 툴을 사용해보니 QAlity와 Zephyr Scale의 장점들을 놓치기 어려웠지만, 저희는 결국 Zephyr Scale을 채택하게 되었습니다. 그 이유는 테스트 케이스를 작성, 수행, 결과를 만들 때까지 연결되며 편하게 관리할 수 있다는 점이 가장 컸고, 무엇보다 상태를 추가할 수 있는 점이 가장 좋았습니다. Not Executed, In Progress, Pass, Fail, Blocked 외에도 N/A, N/I 항목 등 다양하게 커스텀마이징이 가능해 웹서비스셀의 QA 업무 성격과 잘 맞았습니다.&lt;/p&gt;
&lt;h2 id=&quot;zephyr-scale-선택-후-이전보다-나아진-점&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#zephyr-scale-%EC%84%A0%ED%83%9D-%ED%9B%84-%EC%9D%B4%EC%A0%84%EB%B3%B4%EB%8B%A4-%EB%82%98%EC%95%84%EC%A7%84-%EC%A0%90&quot; aria-label=&quot;zephyr scale 선택 후 이전보다 나아진 점 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Zephyr Scale 선택 후 이전보다 나아진 점&lt;/h2&gt;
&lt;p&gt;저희가 사용해보고 나아진 점은 아래 6가지를 나열해 보았습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1) 엑셀에서 벗어나면서 테스트 케이스 작성 시간이 70% 단축되었다&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;앞서 위에서 언급한 것과 같이 재현 방법과 기대 결과만 작성하니 1주일 이상 걸리던 테스트 케이스 작성 시간이 1~2일 정도로 확연히 감소했습니다. 1,000개가 넘던 테스트 케이스가 이제는 25~30개로 충분히 작성할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;또한, 재현 방법이나 기대 결과를 입력하는 곳에 이미지나 다른 첨부 파일 삽입이 가능합니다. 삽입하더라도 템플릿이 일그러지거나 이미지가 작아지지 않습니다. JIRA에서 제공되는 기능이기 때문에 어렵지 않게 작성을 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;마지막으로, 테스트 케이스를 추가할 때 자동으로 순서가 지정되어 번호를 직접 부여하지 않아도 자동으로 번호가 부여됩니다. 그리고 테스트 케이스의 순서도 자유롭게 변경할 수 있어서 다른 곳에 잘못 추가했더라도 쉽게 순서 변경을 할 수 있고, 복제도 가능합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 66.66666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAABYlAAAWJQFJUiTwAAABR0lEQVR42pWS527DMAyE9f7vWLRIMxrH2sMjSBgenbgeDVD/OMimxI8nUsr6RCE1dL1eqet7WW+32791h+7D2nU9qZwL9U/QUl3Xid7FAWh5bdqWWhYMKecDGesol0I555mMMWStncUKn6u1of3hRIfjmXMtOe/JOSd7KoRIKeUVDIoRe2kFNMYK7PRTkWdDMRVyIcuqnPOSiINLea4cQpjFmqYhy24utaHAeZELJi6EwoX3FBKMcWP1qXANQJdAXFFrK+58GMwgH3vKh6GHcXE16OVwHQ8CrFnS/6fDZnAY5Qp/9fAdEDFAAXjFRiAcbgHKlLl/X7vDOJRSJkB8wOWWKVcXTR+f3wLVPHHEfh1yBUx6i8NLrWm3P1JVaX6nix4iCdAtQBg4VzW3ys+GKcAY0/izFGCvylOhFVZuNTwjnJF3WAo9AMZ/60twHxb8AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/25aef3acc1af2371b7ff83d88411d871/33ecc/image8.webp 360w,
/static/25aef3acc1af2371b7ff83d88411d871/263a4/image8.webp 480w,
/static/25aef3acc1af2371b7ff83d88411d871/0c8fb/image8.webp 640w,
/static/25aef3acc1af2371b7ff83d88411d871/521f7/image8.webp 860w,
/static/25aef3acc1af2371b7ff83d88411d871/cbd37/image8.webp 1200w,
/static/25aef3acc1af2371b7ff83d88411d871/548e7/image8.webp 2400w,
/static/25aef3acc1af2371b7ff83d88411d871/bec7f/image8.webp 3392w&quot;
              sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/25aef3acc1af2371b7ff83d88411d871/53918/image8.png 360w,
/static/25aef3acc1af2371b7ff83d88411d871/9aebd/image8.png 480w,
/static/25aef3acc1af2371b7ff83d88411d871/0f09e/image8.png 640w,
/static/25aef3acc1af2371b7ff83d88411d871/3e3fe/image8.png 860w,
/static/25aef3acc1af2371b7ff83d88411d871/64756/image8.png 1200w,
/static/25aef3acc1af2371b7ff83d88411d871/98e2c/image8.png 2400w,
/static/25aef3acc1af2371b7ff83d88411d871/9e320/image8.png 3392w&quot;
            sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/25aef3acc1af2371b7ff83d88411d871/98e2c/image8.png&quot;
            alt=&quot;작성된 테스트 내용&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;작성된 테스트 내용&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2) 이슈 관리가 굉장히 단순해졌다&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;해당 테스트 케이스에서 이슈 발생 시 바로 이슈를 등록할 수 있고, 등록된 이슈의 티켓 번호만 안다면 검색해서 연결할 수도 있습니다. 수동으로 복사/붙여넣기 하던 시절에는 느끼지 못한 감동이었습니다.&lt;/p&gt;
&lt;p&gt;이슈가 수정되면 케이스에도 상태가 반영되어 이슈 확인 및 종료하는데도 수월했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 40.27777777777778%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAABYlAAAWJQFJUiTwAAABgHRFWHRSYXcACmV4aWYKICAgICAxODAKNDU3ODY5NjYwMDAwNDk0OTJhMDAwODAwMDAwMDA1MDAxYTAxMDUwMDAxMDAwMDAwNGEwMDAwMDAxYjAxMDUwMDAxMDAwMDAwCjUyMDAwMDAwMjgwMTAzMDAwMTAwMDAwMDAyMDAwMDAwMzEwMTAyMDAwYjAwMDAwMDVhMDAwMDAwNjk4NzA0MDAwMTAwMDAwMAo2NjAwMDAwMDAwMDAwMDAwOTAwMDAwMDAwMTAwMDAwMDkwMDAwMDAwMDEwMDAwMDA1MDY4NmY3NDZmNTM2MzYxNzA2NTAwMDAKMDQwMDg2OTIwNzAwMTIwMDAwMDA5YzAwMDAwMDAxYTAwMzAwMDEwMDAwMDAwMTAwMDAwMDAyYTAwNDAwMDEwMDAwMDA5YzA5CjAwMDAwM2EwMDQwMDAxMDAwMDAwZTAwMzAwMDAwMDAwMDAwMDQxNTM0MzQ5NDkwMDAwMDA1MzYzNzI2NTY1NmU3MzY4NmY3NArUuLsqAAABCElEQVR42p2S6W7DIBCEef8HrKpWkQ8gxPURDttgY3u6kKSq1D9JkQaQWD7NDrAoJdw0Z/1nHMeBJUbErsOVS7C15hgJNs8ztm3Hvt9Fha+OYdBgmxQQqsXbR4H3zwqnUoLLBiGsNwdPWwWMcWChqtESWZwbSAKrpkPbayzL+rLDafZgc1HCOwc3TrDWUesePiy0BorCY6V8nsnR+wXOjWCxKjGpBqqzqMQFnJymlsX5CyVXMHbEsR+IcSPFnG8CPERTPkux5ZZjWcJWArXSOBW0igZFrTI87RO4v5r8cJouPKL4A6S6G5BzBG2gnUdPWSZH3WAwaHvf69zOA/LzC34pATXVWjfiGxsRbcpbKB29AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/d4b66c6b35983df48f3efe0a756bd2ee/33ecc/image9.webp 360w,
/static/d4b66c6b35983df48f3efe0a756bd2ee/263a4/image9.webp 480w,
/static/d4b66c6b35983df48f3efe0a756bd2ee/0c8fb/image9.webp 640w,
/static/d4b66c6b35983df48f3efe0a756bd2ee/521f7/image9.webp 860w,
/static/d4b66c6b35983df48f3efe0a756bd2ee/cbd37/image9.webp 1200w,
/static/d4b66c6b35983df48f3efe0a756bd2ee/548e7/image9.webp 2400w,
/static/d4b66c6b35983df48f3efe0a756bd2ee/95303/image9.webp 2460w&quot;
              sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/d4b66c6b35983df48f3efe0a756bd2ee/53918/image9.png 360w,
/static/d4b66c6b35983df48f3efe0a756bd2ee/9aebd/image9.png 480w,
/static/d4b66c6b35983df48f3efe0a756bd2ee/0f09e/image9.png 640w,
/static/d4b66c6b35983df48f3efe0a756bd2ee/3e3fe/image9.png 860w,
/static/d4b66c6b35983df48f3efe0a756bd2ee/64756/image9.png 1200w,
/static/d4b66c6b35983df48f3efe0a756bd2ee/98e2c/image9.png 2400w,
/static/d4b66c6b35983df48f3efe0a756bd2ee/c0689/image9.png 2460w&quot;
            sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/d4b66c6b35983df48f3efe0a756bd2ee/98e2c/image9.png&quot;
            alt=&quot;케이스에 나온 이슈 맵핑된 모습&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;케이스에 나온 이슈 맵핑된 모습&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3) 테스트 수행 시 시간을 측정해주는 기능이 있어서 얼마나 걸렸는지 확인할 수 있다&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;테스트 사이클에서 테스트 수행할 때 소요되는 시간을 측정할 수 있습니다. 대신 다른 테스트 케이스로 이동하면 시간은 멈춥니다. 이전에는 테스트 소요 시간을 체감으로만 알 수 있었고, 현재는 이 기능으로 테스트 리소스 측정을 정확하게 할 수 있어 다음 테스트를 계획하는 시간을 확보할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1496px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 33.888888888888886%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAiUlEQVR42q2PYQrCMAyFe/87iacQjyDo2KYsaeOaPJMy/4gyHX7wKDzI1yQREbZmmghMDGbP0iUzw89RhQGg3R7j4YhTf8Wl61GKIOEDMbiG+WbVJTJXzJ6YSeq/ZS91ERS5QzxP6VpeSbUqhvGGnKUVzAXnbmjiLbQNiTPiDWK7kH5z8lsh/swD3c8oX6NCewQAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/17c96fdcdadfe8ff655b4e94b81a4498/33ecc/image10.webp 360w,
/static/17c96fdcdadfe8ff655b4e94b81a4498/263a4/image10.webp 480w,
/static/17c96fdcdadfe8ff655b4e94b81a4498/0c8fb/image10.webp 640w,
/static/17c96fdcdadfe8ff655b4e94b81a4498/521f7/image10.webp 860w,
/static/17c96fdcdadfe8ff655b4e94b81a4498/cbd37/image10.webp 1200w,
/static/17c96fdcdadfe8ff655b4e94b81a4498/6eed8/image10.webp 1496w&quot;
              sizes=&quot;(max-width: 1496px) 100vw, 1496px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/17c96fdcdadfe8ff655b4e94b81a4498/53918/image10.png 360w,
/static/17c96fdcdadfe8ff655b4e94b81a4498/9aebd/image10.png 480w,
/static/17c96fdcdadfe8ff655b4e94b81a4498/0f09e/image10.png 640w,
/static/17c96fdcdadfe8ff655b4e94b81a4498/3e3fe/image10.png 860w,
/static/17c96fdcdadfe8ff655b4e94b81a4498/64756/image10.png 1200w,
/static/17c96fdcdadfe8ff655b4e94b81a4498/6ee55/image10.png 1496w&quot;
            sizes=&quot;(max-width: 1496px) 100vw, 1496px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/17c96fdcdadfe8ff655b4e94b81a4498/6ee55/image10.png&quot;
            alt=&quot;시간 측정 시 나오는 결과&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;시간 측정 시 나오는 결과&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4) 결과를 한눈에 볼 수 있는 툴이 제공되어 편리하다&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;지금까지는 복잡한 엑셀 수식을 사용해 결과 보고서를 작성했습니다. 효과적으로 작성이 되는 것 같지만 수식에 오류라도 있으면 결과는 제대로 나오지 않습니다. 하지만 Zephyr Scale에서는 결과 보고서를 보여주는 툴이 제공됩니다. 원하는 형태의 보고서를 간편하게 만들 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1156px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 122.77777777777779%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAZCAYAAAAxFw7TAAAACXBIWXMAABYlAAAWJQFJUiTwAAACnUlEQVR42pVV147bMBD0/39bgLwEBxxy56LeC9VIUfRkl5Qc52A5ZwILykWjmdlZ6lBUDVrRo+0GZHmNrh+Rl/Rd2yFuDKregNeV6/q39tZhGEY0rYAx641Xtwt6wM83D7/efXycfJwuIfSi1/9cd+sQ5gKfQQHeSyFR9xr1sEAQ0yQtEWUCftIgot+7cUFDv43K7DNMyg4fXgYvrtD0CmLSEHLBMI7ISoFzpuHlM/xiximRiCsFOZt9hvOsrV9KqZscXsPQw48yxLVGXCqktUJYKAKXGOmB2AO8p2sNX6+naUSYkORSIyTAiKrqNGZtsBjH0OwBPuqalBPCtMIlI1bZBJ+YcQ3EjkGNce3mZt7X4SvQJnkkyZcws94xEMtlD3lntsO0QK1esq6Hku/XNA4kuUBUaSqFpHbASe0AA34I7T0Bc/cn2/kngJI8jEiyBaQb4xWIATla7aDtXnYz0ma2nx8CbpKFaHHyU4rNjHM6kZcTeSlt6cXcZG6jwwPx1MO2beDHOXmoCUyuTVF212tTvtZDDzfATghc/ITkLFZu2nAWZyubu/ytHP4L2OIcpGS8dl0uXbCP1Olm9eslQEGSvTDHhcbusuYwKCQBTijEjOuX/P03hwwYxIVlyEDROi0e+cgx2jx7iSHnMKkXO8scl2Sd56LV+4fDc8nZjWFY8pQ4pu8hxYcmiH1clhck8+ixh+fU5Y995GtuzI/jRBl1J9S9/OeS49JOCo8ZM+NOB+ss85TYg/bbkik21kPKYVK585B3BuO5HZV5GJ3nHkY5MdJWLvvI7NhH7vRbIF9vyjnI7PHPOWTAbZY7CracH5/au4BNXeH30UdQOoYslWfayx1T+xr4juRt8TuGX6WDNPbM45Oad649/xjwD2kLmy0VW+XZAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/b3603b7d1e70d63edd82d7c74bf1e35c/33ecc/image11.webp 360w,
/static/b3603b7d1e70d63edd82d7c74bf1e35c/263a4/image11.webp 480w,
/static/b3603b7d1e70d63edd82d7c74bf1e35c/0c8fb/image11.webp 640w,
/static/b3603b7d1e70d63edd82d7c74bf1e35c/521f7/image11.webp 860w,
/static/b3603b7d1e70d63edd82d7c74bf1e35c/694bf/image11.webp 1156w&quot;
              sizes=&quot;(max-width: 1156px) 100vw, 1156px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/b3603b7d1e70d63edd82d7c74bf1e35c/53918/image11.png 360w,
/static/b3603b7d1e70d63edd82d7c74bf1e35c/9aebd/image11.png 480w,
/static/b3603b7d1e70d63edd82d7c74bf1e35c/0f09e/image11.png 640w,
/static/b3603b7d1e70d63edd82d7c74bf1e35c/3e3fe/image11.png 860w,
/static/b3603b7d1e70d63edd82d7c74bf1e35c/07220/image11.png 1156w&quot;
            sizes=&quot;(max-width: 1156px) 100vw, 1156px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/b3603b7d1e70d63edd82d7c74bf1e35c/07220/image11.png&quot;
            alt=&quot;제공되는 결과 리포트&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;제공되는 결과 리포트&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5) 테스트 케이스를 처음 접한 사람도 쉽게 이해할 수 있다&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;사인 오프(Sign-off: “제품이 시장에 출시되어도 됩니다” 라고 선언)를 할 때에도 유관 부서와 함께 해당 케이스들을 내부에 공유하면서 체크를 하는데 사용했습니다. 기존 엑셀의 경우 내용이 너무 길어서 체크하면서 검증하기가 어려움이 있었으나, Zephyr Scale 의 경우 재현 방법/기대 결과/등록한 이슈가 한번에 노출되어 처음 본 사람도 알기 쉬웠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 74.16666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAABYlAAAWJQFJUiTwAAABgHRFWHRSYXcACmV4aWYKICAgICAxODAKNDU3ODY5NjYwMDAwNDk0OTJhMDAwODAwMDAwMDA1MDAxYTAxMDUwMDAxMDAwMDAwNGEwMDAwMDAxYjAxMDUwMDAxMDAwMDAwCjUyMDAwMDAwMjgwMTAzMDAwMTAwMDAwMDAyMDAwMDAwMzEwMTAyMDAwYjAwMDAwMDVhMDAwMDAwNjk4NzA0MDAwMTAwMDAwMAo2NjAwMDAwMDAwMDAwMDAwOTAwMDAwMDAwMTAwMDAwMDkwMDAwMDAwMDEwMDAwMDA1MDY4NmY3NDZmNTM2MzYxNzA2NTAwMDAKMDQwMDg2OTIwNzAwMTIwMDAwMDA5YzAwMDAwMDAxYTAwMzAwMDEwMDAwMDAwMTAwMDAwMDAyYTAwNDAwMDEwMDAwMDBjODBhCjAwMDAwM2EwMDQwMDAxMDAwMDAwZmMwNzAwMDAwMDAwMDAwMDQxNTM0MzQ5NDkwMDAwMDA1MzYzNzI2NTY1NmU3MzY4NmY3NAoae5eNAAABg0lEQVR42pVUi3aDIAzl/790m5Yi8ggPm+XGOWtrzzo9OShCuI9EM7iRcya+yb0syyFaa09ziFIq+zlySsRJ9mL0IfCHH9hYujLJZK2Vb7cb31+P79vVe+eYso5LXw8upcghM5syOc5UTzcDzauE3gdFGaIkBnKZKxfLpn4NclpiIpKTuibpfY1XCZtsHq3ji500am06n6mwaVcrlDNHoQ0a0BMfoA20AvLHwEGPc5BsikJ5TJYL7RvfoVxFMyCLQheGAGFbGo/JsQlpUiRYdJ8Qj69MWRHt6zF22U8igymfA1+c5xCSUt3o/GUKJFmWPSEii1GmuCtHyjzPSTXEQhhypt2u4V6zm3lg6KDhJVoOkoykjv5TNmqgMEKQROuNbRTKUxHboeE/Cls1Q2n9ouya0Ikf6rKfZq3Fd13eNMx5LS8gROt6imxScAfN3kfYn13WThlGrXq00aFT8HOo7TQpvm29fHA5CMIuCdMctHSshPdx7ZZMP473U6fvE8mTegBQ3871nSaauBcrAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/6fc924001bfd907a0c95a951cf8efa53/33ecc/image12.webp 360w,
/static/6fc924001bfd907a0c95a951cf8efa53/263a4/image12.webp 480w,
/static/6fc924001bfd907a0c95a951cf8efa53/0c8fb/image12.webp 640w,
/static/6fc924001bfd907a0c95a951cf8efa53/521f7/image12.webp 860w,
/static/6fc924001bfd907a0c95a951cf8efa53/cbd37/image12.webp 1200w,
/static/6fc924001bfd907a0c95a951cf8efa53/548e7/image12.webp 2400w,
/static/6fc924001bfd907a0c95a951cf8efa53/d0d90/image12.webp 2760w&quot;
              sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/6fc924001bfd907a0c95a951cf8efa53/53918/image12.png 360w,
/static/6fc924001bfd907a0c95a951cf8efa53/9aebd/image12.png 480w,
/static/6fc924001bfd907a0c95a951cf8efa53/0f09e/image12.png 640w,
/static/6fc924001bfd907a0c95a951cf8efa53/3e3fe/image12.png 860w,
/static/6fc924001bfd907a0c95a951cf8efa53/64756/image12.png 1200w,
/static/6fc924001bfd907a0c95a951cf8efa53/98e2c/image12.png 2400w,
/static/6fc924001bfd907a0c95a951cf8efa53/f021b/image12.png 2760w&quot;
            sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/6fc924001bfd907a0c95a951cf8efa53/98e2c/image12.png&quot;
            alt=&quot;전체 케이스 모습&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;전체 케이스 모습&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;6) 기타&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;버전 관리도 쉬운데다가 프로젝트별로 폴더를 생성해 관리를 할 수 있어 좋다&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;테스트 케이스별 엑셀 시트와 같이 LNB 메뉴가 제공되고 폴더 별로 관리가 쉬워졌고 이전 테스트 케이스를 바로 확인할 수 있었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;검증 시간이 많이 절약됐다&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;플로우에 맞게 순서에 따라 작성 및 테스트하는데 시간이 감소하고, 좀 더 엣지 케이스(Edge case) 위주의 검증을 할 수 있는 시간이 늘었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;엑셀을-대체-할-만큼-좋은가&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%97%91%EC%85%80%EC%9D%84-%EB%8C%80%EC%B2%B4-%ED%95%A0-%EB%A7%8C%ED%81%BC-%EC%A2%8B%EC%9D%80%EA%B0%80&quot; aria-label=&quot;엑셀을 대체 할 만큼 좋은가 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;엑셀을 대체 할 만큼 좋은가?&lt;/h2&gt;
&lt;p&gt;테스트 케이스를 작성하는 과정에서 굉장히 시간을 단축할 수 있었고, 결과 보고서 작성도 빨라졌습니다. 하지만 JIRA에서 제공되는 툴이기 때문에 JIRA를 사용하지 않는 유관부서에서 확인하기 어려워 테스트 케이스 및 결과를 Zephyr Scale에서 pdf 파일로 다운받은 후 가공해서 유관부서에 전달하고 있습니다. 결과적으로 리소스는 낭비를 줄이고 이전보다 빠르게 결과 공유가 가능했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1678px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 108.61111111111113%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAWCAYAAADAQbwGAAAACXBIWXMAABYlAAAWJQFJUiTwAAADf0lEQVR42pVUWW8bVRSef8EzD4g3nnitEG/8BiQeELRQCcTSKlKlvlCJIrEVIdSAKEVNUEvUQqtSamgbNWnSNLbjxEtiZ3GwM7Zbx57FHs++3I9zx+MxTlKRHunzXN8z95uzfOcK4mYB9/6Yglheg6J20JYlKIoSQpZlaFoH5XIdDxdKmF9YRyZTJp8c+lVVjaCE6HY7EHRdB4dlWoAfgJsfBAgiGLRvGiYCy4dvu3BcK/L5MC0btmXB9RkM8tm2DYETOLaDx20FP2WWMF2rYGgMtumivCvifG4Ss7VktMuIkKHX07Fea2Mt+wBKrYyu4UJgjIUvfZfP4OdSAV9kFnFje6N/kFxO4OBs8Ryu1m7ik9UvkZJWIl8Al4gXewHajXUgMwHmOf0IPUrh47l7sHwPS60nOJt+GMe4rVVxKvtpuL5Z/ws/bk1GUYLK1IW0fodqZMN9dBGBLkEIoggT1W18MPM3Ts7eRUluh3sD3/dbl3Cm8BVO5z6DaDTiCLm5G/ehPxiHV+tHLgwi4YerkoRmR40jGPoCZKQc2pYc+xg1RaZsKpUKxK08KsVlGK36kHBQS7aHbNTH4rVnG8jM3ML87Sk0y6vYzKbQoob+h7B/cJDmXmODz0R+3zGxMvcn5hNTFGkDjWYLGnVdwLNaHKGF/OJ9pGcT2G3sQKyJobgFx3VJvDZ0EnDPsMKnRbr06SAHexovr23AQpGHmUVPgZOpHQ2mbsIxbDj033e9fQS+78PzvBGwA8ojDLbycgvT9SpmKPwECfvWZhGJrQ1ImhaOl0Up8tEyaT1AOKJ76h7r8O3p23juwrd4cfIHvDAxjucvncdLly/gn0hGT2vWvgg9SoXb2Nw0Xr5yEa/+9gteuTaJI1cn8NqNK9ihG2TQDMU0MZ5bwtc0nucI3ywn8Xl6AdcoG0R1FPgPr0/L6EHUVIgdBRWalFpXRZ2g22Y/RUqZr0tUmjVpF/lmA4XdxyhITVToTEAcnEfoagY01YTdc+DodD1pLgIvGFEJnzIWMOAQWQs23RBZaRVZhWMNK4ScOkS+U0ShUwqRV4ujPsKykkfDfBKLX5BMBe+nTuFY6gTeSZ8McSx1EE7s23s3PYY3Hr2HX3euxzNPhDKOJ8fwZvJDvJX86JlwlD7y+sJxXK7+PiS0fQc5eXUklcPiwJS52tn/aMyjizdsyiF0+C8ITE+39diQEwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/1b5ff1d4e7c4bcdf4e7cc2637a8d4a91/33ecc/image13.webp 360w,
/static/1b5ff1d4e7c4bcdf4e7cc2637a8d4a91/263a4/image13.webp 480w,
/static/1b5ff1d4e7c4bcdf4e7cc2637a8d4a91/0c8fb/image13.webp 640w,
/static/1b5ff1d4e7c4bcdf4e7cc2637a8d4a91/521f7/image13.webp 860w,
/static/1b5ff1d4e7c4bcdf4e7cc2637a8d4a91/cbd37/image13.webp 1200w,
/static/1b5ff1d4e7c4bcdf4e7cc2637a8d4a91/8a15b/image13.webp 1678w&quot;
              sizes=&quot;(max-width: 1678px) 100vw, 1678px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/1b5ff1d4e7c4bcdf4e7cc2637a8d4a91/53918/image13.png 360w,
/static/1b5ff1d4e7c4bcdf4e7cc2637a8d4a91/9aebd/image13.png 480w,
/static/1b5ff1d4e7c4bcdf4e7cc2637a8d4a91/0f09e/image13.png 640w,
/static/1b5ff1d4e7c4bcdf4e7cc2637a8d4a91/3e3fe/image13.png 860w,
/static/1b5ff1d4e7c4bcdf4e7cc2637a8d4a91/64756/image13.png 1200w,
/static/1b5ff1d4e7c4bcdf4e7cc2637a8d4a91/c54b3/image13.png 1678w&quot;
            sizes=&quot;(max-width: 1678px) 100vw, 1678px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/1b5ff1d4e7c4bcdf4e7cc2637a8d4a91/c54b3/image13.png&quot;
            alt=&quot;제공되는 결과 리포트에서 추출해 결과 보고를 작성한 일부 내용&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;제공되는 결과 리포트에서 추출해 결과 보고를 작성한 일부 내용&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;엑셀을 대체할 만큼 좋은가?&lt;/code&gt; 라고 누군가 물어본다면, &lt;code&gt;네&lt;/code&gt;라고 답변할 수 있습니다. 테스트 케이스 작성도 편리했지만, 무엇보다 테스트 케이스 수행부터 결과 보고서 작성, 특히 이슈 관리를 유연하게 할 수 있었습니다. 이렇게 효과적으로 리소스를 관리해서, 예외 케이스가 발생했을 경우 셀 구성원들과 협업을 통해 대응할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;저희와 같은 고민을 하신 분들이 계신다면 소개해 드린 QAlity와 Zephyr Scale을 테스트 관리 툴로 사용해 보시는 건 어떨까요? 30일 무료 체험 버전이 있어서 어떤 툴이 본인의 QA 업무에 맞는지 비교해 보시고 선택하시는 걸 추천해 드립니다.&lt;/p&gt;
&lt;p&gt;긴 글 읽어주셔서 감사드립니다.&lt;/p&gt;
&lt;p&gt;데브시스터즈에서는 작은 불편함을 참지 않고 새로운 기술을 받아들여 개선하려는 도전적인 QA 인재를 기다리고 있습니다. 여러분과 함께 하길 기다리며 글을 끝내도록 하겠습니다. 그럼 이만!&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[“DevOps”의 인적 확장 (The human scalability of “DevOps”)]]></title><description><![CDATA['The human scalability of DevOps' 라는 글을 번역하여 데브옵스 조직에 대해 고민해본 많은 분들에게 알려드리고 싶습니다.]]></description><link>https://tech.devsisters.com/posts/the-human-scalability-of-devops</link><guid isPermaLink="false">https://tech.devsisters.com/posts/the-human-scalability-of-devops</guid><pubDate>Tue, 28 Jun 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, 데이터플랫폼/인프라셀의 데브옵스 엔지니어로 일하고 있는 원대영입니다.&lt;/p&gt;
&lt;p&gt;이 글은 제가 DevOps 엔지니어로 지내면서 읽어본 많은 글 중 가장 마음에 두고 오래오래 읽어보았던 글이었습니다. 2018년 6월의 글로 벌써 4년 전의 글인데, DevOps 세계에선 4년이란 시간이 짧지 않은데도 이 당시에 이런 이야기를 할 수 있었다는 건 참 대단하다고 생각하고, 동시에 지금 읽어봐도 여전히 통찰력있는 글이라고 생각합니다. 언젠가 책 ‘어린 왕자&apos; 를 어릴 때 읽는 것과 성인이 되어 읽을 때 다르게 읽힌다는 말을 들은 적이 있는데, 제게 기술 관련 글에선 이 글이 그랬습니다. 시간을 두고 한 번씩 읽어볼 때마다 다르게 읽히며 마음 저리게 다가온 글이었고, DevOps 엔지니어의 조직구조나 역할, 방향성을 치열하게 고민해봤다면 뼈아픈 글일 거라고도 생각합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;원저자의 허락을 구했으며, 이 글을 번역할 수 있게 허락해준 Matt Klein에게 감사 인사를 남깁니다. 저자에 대한 소개를 안 할 수가 없는데, Matt는 무려 DevOps 엔지니어라면 많이 알 &lt;a href=&quot;https://www.envoyproxy.io/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Envoy&lt;/a&gt; 의 창시자입니다. 그의 &lt;a href=&quot;https://mattklein123.dev/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;개인 홈페이지&lt;/a&gt;에는 읽어보면 좋을 다양한 글도 많으니 한 번 방문해보는 것 또한 추천합니다. Thank you, Matt!&lt;/li&gt;
&lt;li&gt;저는 전문 번역가도 아니고 영어가 그리 능숙하지도 않기 때문에 많은 의역을 거쳤습니다. 한국어 번역이 애매하거나 더 어렵게 느낄 만한 표현들은 영어를 그대로 쓰거나 영어 발음대로의 한국어로 썼으니 양해 부탁드리며, 오역에 대한 의견은 &lt;a href=&quot;mailto:tech_blog@devsisters.com&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;tech_blog@devsisters.com&lt;/a&gt;로 주시면 언제든 감사하게 받겠습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;원문: &lt;a href=&quot;https://medium.com/@mattklein123/the-human-scalability-of-devops-e36c37d3db6a&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://medium.com/@mattklein123/the-human-scalability-of-devops-e36c37d3db6a&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;최근 &lt;a href=&quot;https://twitter.com/mattklein123/status/1001979384968957952&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;트위터&lt;/a&gt;에 썼듯, 저는 요즘 꽤 많은 시간을 들여 “DevOps”의 인적 확장(human scalability)에 대해 생각하고 있습니다. (“DevOps”에다가 따옴표를 붙인 건, 이에 대해 추후 서술할 다양한 정의가 있기 때문입니다.) 그렇게 내려본 결론은, DevOps는 작은 엔지니어링 조직들에서 굉장히 잘 돌아갈 수도 있지만, 신중한 생각과 관리 없이는 큰 인적/조직적 확장(scaling) 문제를 일으킨다는 것입니다.&lt;/p&gt;
&lt;h2 id=&quot;devops란-무엇인가-what-is-devops&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#devops%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80-what-is-devops&quot; aria-label=&quot;devops란 무엇인가 what is devops permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;DevOps란 무엇인가 (What is DevOps)&lt;/h2&gt;
&lt;p&gt;DevOps라는 용어에 대해선 많은 사람들이 다양한 의미로 말합니다. 제가 이 글을 본격적으로 시작하기 전에, 제가 어떤 의미로 DevOps라는 용어를 쓰는지 정리하는 게 중요해 보입니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/DevOps&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Wikipedia&lt;/a&gt;에서 정의하는 DevOps는 다음과 같습니다:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;DevOps(“Development”와 “Operations”의 합성어) 는 소프트웨어 개발(Dev)과 운영(Ops)을 융합한 소프트웨어 개발 문화와 관행(practice)을 말한다. DevOps movement의 주요 특징은 통합, 테스팅, 릴리징부터 배포, 인프라 관리까지 소프트웨어를 만드는 모든 과정에서의 자동화 및 모니터링을 강력하게 추구하고 제안하는 것이다. DevOps는 짧은 개발 사이클, 잦은 배포 주기, 더 신뢰할 수 있는 릴리즈를 추구하여 비즈니스 목표와 방향성을 더 밀접하게 맞춘다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;저는 보다 더 좁고, 살짝 다른 형태로 DevOps를 정의하는데요,&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;DevOps는 개발자가 프로덕션 환경에서 24시간 365일 서비스가 운영되도록 책임지게 하는 방법(practice)이다. 이것은 공용 인프라 기반기술을 사용한 개발, 테스팅, 상시 장애대응, 신뢰성 엔지니어링(reliability engineering), 장애 복구, SLO 정의, 모니터링 셋업 및 알람 설정, 디버깅 및 성능 분석, 장애 근본 원인 분석(root cause analysis), 프로비저닝 및 배포 등을 포함한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Wikipedia의 정의와 제 정의와의 구분(‘개발 철학’ 대 ‘운영 방식’)은 저의 업계 경험을 바탕으로 만든 중요한 내용입니다. DevOps “movement” 일부는 느리게 움직이는 “오래된&quot; 회사들에게 고도로 자동화된 최신 인프라와 개발 방법의 장점들을 설명하는 것입니다. 예를 들면, 서로 느슨하게 연결된(loosely-coupled) 서비스와 API 및 팀들, 지속적 통합(continuous integration), master에서 일어나는 작고 잦은 배포, 애자일 커뮤니케이션 및 플래닝, 클라우드 기반의 유연한 인프라 등입니다.&lt;/p&gt;
&lt;p&gt;제 최근 10년의 커리어를 보면, AWS EC2, 상장 이전의 Twitter, Lyft 같은 급성장하는 인터넷 회사(hyper-growth Internet companies)에서 일을 해왔습니다. 더불어 Envoy를 만들고 알리기 위한 지난 2년간, 수많은 급성장 인터넷 회사들의 기술 아키텍처와 조직 구조를 마주하고 배웠습니다. 그 기업들에게 자동화, 애자일 개발/플래닝, 기타 DevOps “best practice”를 받아들인다는 건 당연한 일인데, 그건 이들이 생산성 향상에 대해 잘 이해하고 있기 때문입니다. 그 대신, &lt;strong&gt;이러한 엔지니어링 조직이 갖는 당면 과제는 ‘비즈니스 성장, 개인의 성장 및 (비즈니스/채용에 대한) 경쟁 등에 대한 극심한 압박과 시스템 신뢰성(reliability) 간의 균형을 어떻게 맞추는가’ 입니다.&lt;/strong&gt; 이 글의 이후 부분은 저의 개인적 경험에 근거한 것으로, 맞지 않는 경우가 있을 수 있고, 특히 분기마다 한 번씩 monolithic 소프트웨어를 배포하는 천천히 움직이는 회사들이나 더 빠르고 애자일한 개발 방법을 도입하고자 하는 회사같은 경우엔 맞지 않을 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;인터넷-어플리케이션-운영의-역사-a-brief-history-of-operating-internet-applications&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9D%B8%ED%84%B0%EB%84%B7-%EC%96%B4%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EC%9A%B4%EC%98%81%EC%9D%98-%EC%97%AD%EC%82%AC-a-brief-history-of-operating-internet-applications&quot; aria-label=&quot;인터넷 어플리케이션 운영의 역사 a brief history of operating internet applications permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;인터넷 어플리케이션 운영의 역사 (A brief history of operating Internet applications)&lt;/h2&gt;
&lt;p&gt;제가 현대 인터넷 시대(modern Internet era)라고 일컫는 최근 30여년 동안, 인터넷 어플리케이션 개발 및 운영은 (제 생각에) 세 가지 뚜렷한 단계를 거쳤습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;첫 번째 단계에선, 인터넷 어플리케이션은 만들어지면 “꼼꼼하게 포장(shrink-wrapped)” 된 프로그램이 배송되는 것처럼 배포됐습니다. 어플리케이션을 개발하고 운영하기까지의 아주 긴 엔지니어링 주기에 걸쳐 세 가지 서로 다른 직무(개발, QA, 운영)가 협업했습니다. 이 기간 동안 각 어플리케이션은 전용 데이터 센터나 공용 데이터 센터(colocation facility)에 배포되므로, 해당 장소의 네트워크/하드웨어/시스템 관리에 익숙한 운영 인력이 추가로 필요했습니다.&lt;/li&gt;
&lt;li&gt;두 번째 단계에선, 90년대 후반 ~ 2000년대 초반의 Amazon, Google을 필두로 빠르게 급성장하는 회사의 인터넷 어플리케이션들이 loosely-coupled 서비스, 빠른 개발 및 배포, 자동화 등 현대 DevOps movement와 비슷한 형태의 시도를 시작했습니다. 이런 기업들은 여전히 큰 자체 데이터 센터를 운영하고 있는데, 이런 규모 때문에 모든 서비스에 필요한 공통 문제(네트워킹, 모니터링, 배포, 프로비저닝, 데이터 스토리지, 캐싱, 물리 인프라 등)를 해결하는 중앙 집중식 인프라 팀들을 만들기도 했습니다. 그러나 Amazon과 Google은 그러한 역할들을 하나의 개발 직무로 완전히 통합하진 않았는데 (이를 테면 Amazon의 Systems Engineer, Google의 &lt;a href=&quot;https://en.wikipedia.org/wiki/Site_reliability_engineering&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Site Reliability Engineer&lt;/a&gt;처럼), 각 직무가 요구하는 능력과 관심사가 다르다고 인식했기 때문입니다.&lt;/li&gt;
&lt;li&gt;세 번째 단계는 클라우드 네이티브 단계라고 할 수 있습니다. 인터넷 어플리케이션은 이제 “Big 3” 퍼블릭 클라우드가 제공하는 유연한 아키텍처에 기반하여 만들어집니다. 시장에서의 높은 실패 가능성을 감안하다보니 제품을 가능한 빠르게 시장에 내놓는 것이 기업의 최우선 목표가 되어왔는데, 클라우드 네이티브 시대에선 “즉각&quot; 제공되는 기반기술들을 통해 이를 이전보다 훨씬 더 빠른 속도로 반복하는 게 가능해졌습니다. 이 시대에 시작한 기업의 또 다른 특징은 소프트웨어 개발 역량을 보유하지 않은 엔지니어 역할을 두는 것을 지양한다는 것입니다. 사용할 수 있는 인프라 기반이 (그들 생각에) 상대적으로 탄탄하다보니, 스타트업의 인건비를 엔지니어링과 운영을 동시에(DevOps) 할 수 있는 소프트웨어 개발자를 위해 쓰는 게 더 낫습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;세 번째 단계의 기업들이 운영 전담 인력을 뽑지 않는 방향으로 변화한다는 건 아주 중요합니다. 분명 이러한 회사가 공용 데이터 센터의 기기를 다룰 풀타임 시스템 관리자를 필요로 하진 않지만, 이전에 그런 업무를 수행했던 사람은 보통 시스템 디버깅, 성능 프로파일링, 운영에 대한 직관(Intuition) 등 다른 20%의 스킬들을 제공했습니다. &lt;strong&gt;신규 회사들은 이제 중요하고 쉽게 대체되지 않는 스킬셋을 보유한 인력 없이도 만들어집니다.&lt;/strong&gt; (역자 주: 여기서 신규 회사가 갖지 않는 스킬셋이란, 데이터 센터의 네트워크 설치, 사전 부팅 실행 환경(PXE) 구성, 장소의 항온항습이나 바닥 관리 등 서버실을 관리하던 운영 전담 엔지니어가 해왔던 것들을 의미한다고 볼 수 있겠습니다.)&lt;/p&gt;
&lt;h2 id=&quot;왜-devops가-현대-인터넷-스타트업에서-잘-돌아갈까-why-does-devops-work-well-for-modern-internet-startups&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%99%9C-devops%EA%B0%80-%ED%98%84%EB%8C%80-%EC%9D%B8%ED%84%B0%EB%84%B7-%EC%8A%A4%ED%83%80%ED%8A%B8%EC%97%85%EC%97%90%EC%84%9C-%EC%9E%98-%EB%8F%8C%EC%95%84%EA%B0%88%EA%B9%8C-why-does-devops-work-well-for-modern-internet-startups&quot; aria-label=&quot;왜 devops가 현대 인터넷 스타트업에서 잘 돌아갈까 why does devops work well for modern internet startups permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;왜 DevOps가 현대 인터넷 스타트업에서 잘 돌아갈까? (Why does DevOps work well for modern Internet startups?)&lt;/h2&gt;
&lt;p&gt;위에서 정의한 DevOps는 현대 인터넷 스타트업에서 정말 잘 동작할 수 있는데, 그 이유는 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;제 경험 상, 성공한 스타트업의 초기 엔지니어는 엔지니어 중에서도 특별한 부류입니다. 그들은 위험을 감내할 수 있고(risk tolerant), 아주 빠르게 배우며, 기술부채의 증가가 있더라도 아주 빠르게 무언가를 만들어내는 것에 익숙하고, 여러 시스템과 언어로 작업할 수 있으며, 보통 시스템 관리나 운영에 대한 경험이 있고, 계속 배우길 원하는 사람입니다. 다시 말해, 보통의 스타트업 엔지니어들은 DevOps 엔지니어가 되기 아주 적합한 사람들입니다. 물론, 그들이 그렇게 불리길 원하는지와는 별개입니다. (전 아닙니다 😉)&lt;/li&gt;
&lt;li&gt;위에서 서술했듯, 현대 퍼블릭 클라우드들은 기반이 되는 인프라들을 아주 훌륭하게 제공합니다. 과거에 진행하던 가장 &lt;strong&gt;기본적인&lt;/strong&gt; 운영 작업은 자동화되어, MVP를 만들어서 시장에 적합한지 확인할 수 있을 만큼의 좋은 기반이 만들어졌습니다.&lt;/li&gt;
&lt;li&gt;저는 개발자들이 자신이 작성한 코드에 대해 책임을 지고 장애대응을 할 때 그 시스템의 퀄리티가 향상된다고 굳게 믿는 사람입니다. 누구도 장애로 호출 받는 걸 좋아하진 않지만, 이런 피드백 루프는 더 나은 제품을 만들어냅니다. 또한 1번에서도 설명했듯 보통 초기 스타트업에서 일하는 것에 매력을 느끼는 엔지니어는 배우는 것과 운영 업무를 하는 걸 꺼리지 않는 사람들입니다. 특히 제품 신뢰성이 낮아도 큰 영향을 받지 않는 초기 스타트업의 경우에는 이런 경향이 더욱 두드러지게 나타납니다. 초기 스타트업의 경우에는 제품이 적절한 시장을 찾아 급성장할 수 있는 국면에 진입할 정도만 제품의 신뢰성이 있어도 충분하기 때문입니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;현대-인터넷-스타트업이-급성장하면-무슨-일이-벌어지는가-what-happens-when-a-modern-internet-startup-undergoes-hyper-growth&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%98%84%EB%8C%80-%EC%9D%B8%ED%84%B0%EB%84%B7-%EC%8A%A4%ED%83%80%ED%8A%B8%EC%97%85%EC%9D%B4-%EA%B8%89%EC%84%B1%EC%9E%A5%ED%95%98%EB%A9%B4-%EB%AC%B4%EC%8A%A8-%EC%9D%BC%EC%9D%B4-%EB%B2%8C%EC%96%B4%EC%A7%80%EB%8A%94%EA%B0%80-what-happens-when-a-modern-internet-startup-undergoes-hyper-growth&quot; aria-label=&quot;현대 인터넷 스타트업이 급성장하면 무슨 일이 벌어지는가 what happens when a modern internet startup undergoes hyper growth permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;현대 인터넷 스타트업이 급성장하면 무슨 일이 벌어지는가? (What happens when a modern Internet startup undergoes hyper-growth?)&lt;/h2&gt;
&lt;p&gt;대부분의 스타트업은 실패합니다. 그게 현실입니다. 초기 스타트업이 Google을 상상하며 인프라를 만들어나간다면, 그건 시간 낭비일 뿐입니다. 제가 사람들에게 늘 말하는 건, &lt;strong&gt;인적&lt;/strong&gt; 확장 문제(의사 소통, 계획 산정, 강한 의존성 등)로 인해 보다 의존성이 적은 아키텍처로 전환하기 전까진, 다른 걱정은 하지 말고 그냥 현재의 monolithic 아키텍처를 유지하라는 것입니다.&lt;/p&gt;
&lt;p&gt;그러면 현대의 (위에서 말한 세 번째 단계의) 스타트업이 성공을 거두며 급성장 시기를 맞이하면, 어떻게 될까요? 몇 가지 흥미로운 일들이 동시에 일어나기 시작합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;인력 증가 속도가 빠르게 늘어나며, 커뮤니케이션과 엔지니어링 효율에 심각한 부담이 오기 시작합니다. 이 주제에 대해서는 &lt;a href=&quot;https://en.wikipedia.org/wiki/The_Mythical_Man-Month&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;맨먼스 미신(The Mythical Man-Month)&lt;/a&gt; 을 꼭 읽어보길 권합니다. (거의 50여년 전 책인데도 여전히 아주 좋은 책입니다.)&lt;/li&gt;
&lt;li&gt;위 내용으로 인해 거의 대부분 monolithic 아키텍처에서 microservice 아키텍처로 전환하며, 개발 팀들을 분리하고 커뮤니케이션 및 엔지니어링 효율을 높이려고 합니다.&lt;/li&gt;
&lt;li&gt;Monolithic 아키텍처에서 microservice 아키텍처로의 전환은 시스템 인프라 복잡성을 몇 배나 증가시킵니다. 네트워킹, 시스템 가시성, 배포, 라이브러리 관리, 보안 등 이전에는 그리 어렵지 않았던 수백가지 문제가 불거지고, 해결해야 할 주요 문제가 됩니다.&lt;/li&gt;
&lt;li&gt;동시에, 급성장으로 인해 트래픽이 증가하고 그로 인한 기술적인 확장 문제를 겪게 되며, 완전한 실패(complete failure)와 작은 사용자 경험 문제들이 생기기 시작, 점점 큰 사용자 영향이 나타납니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;중앙-인프라-팀-central-infrastructure-teams&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A4%91%EC%95%99-%EC%9D%B8%ED%94%84%EB%9D%BC-%ED%8C%80-central-infrastructure-teams&quot; aria-label=&quot;중앙 인프라 팀 central infrastructure teams permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;중앙 인프라 팀 (Central infrastructure teams)&lt;/h2&gt;
&lt;p&gt;이런 초기 스타트업 단계에서, 거의 대부분 현대 인터넷 급성장 회사(modern Internet hyper-growth companies)들은 엔지니어링 조직들을 비슷하게 만들기 시작합니다. 공통적으로 나타나는 구조는 중앙 인프라 팀을 만들어, DevOps랑 비슷한 일을 하고 있는 다양한 제품 팀을 지원하는 구조입니다. 각 팀은 DevOps라고 불리지 않을 수도 있습니다.&lt;/p&gt;
&lt;p&gt;중앙 인프라 팀이 보편적으로 생기는 이유는, 위에서 이야기했듯 급성장이 사람과 기술 양 측면에서 여러 변화를 야기하는데, &lt;strong&gt;모든 제품 엔지니어링 팀이 각각 개별적으로 네트워킹, observability, 배포, 프로비저닝, 캐싱, 데이터 저장 등의 문제를 풀고자 최신의 클라우드 네이티브 기술들을 사용하긴 너무 어렵기 때문입니다.&lt;/strong&gt; 전체 엔지니어링 조직이 거의 비즈니스 로직에만 집중하여, 아주 신뢰할 수 있는, 대규모의, 실시간 인터넷 어플리케이션을 만들도록 탄탄하게 지원하는 그런 “서버리스” 기술은 아직 수십년은 멀었습니다.&lt;/p&gt;
&lt;p&gt;그리하여, 클라우드 네이티브 인프라 기반기술들이 제공해주는 것 너머의 이런 대규모 엔지니어링 조직들이 갖고 있는 문제들을 해결하기 위해 중앙 인프라 팀이 탄생했습니다. 분명 Google의 인프라 팀은 Lyft같은 회사보다는 몇 배는 더 큽니다. Google은 데이터 센터 수준에서의 근본적인 문제들을 해결하는 반면, Lyft는 공개적으로 사용할 수 있는 많은 기반기술들에 의존하고 있기 때문입니다. 그러나, 두 회사 모두 중앙 인프라 조직을 만드는 근본적인 이유는 동일합니다. 바로 &lt;strong&gt;어플리케이션/제품 개발자가 비즈니스 로직에 집중할 수 있도록 가능한 많은 인프라들을 추상화하기 위함&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;h2 id=&quot;대체-가능성에-대한-착오-the-fallacy-of-fungibility&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8C%80%EC%B2%B4-%EA%B0%80%EB%8A%A5%EC%84%B1%EC%97%90-%EB%8C%80%ED%95%9C-%EC%B0%A9%EC%98%A4-the-fallacy-of-fungibility&quot; aria-label=&quot;대체 가능성에 대한 착오 the fallacy of fungibility permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;대체 가능성에 대한 착오 (The fallacy of fungibility)&lt;/h2&gt;
&lt;p&gt;마지막으로 “대체 가능성(Fungibility)&quot;, 조직의 엔지니어 숫자가 어느 수준 이상으로 커지며 단일(pure) DevOps 모델의 실패로 이어지게 되는 핵심에 대해 이야기해보려 합니다. (역자 주: 단일 DevOps 모델이라는 번역이 적절한가 고민했는데, 뒤에서 제안하는 개발팀을 포함하는 복합적인 DevOps 모델과 대비되는 단어로 선택했습니다) 대체 가능성은 모든 엔지니어가 똑같이 태어나 모든 것들을 다 할 수 있다는 개념입니다. (적어도 Amazon이나 다른 회사들처럼) 채용 대상을 아주 분명하게 적어뒀든, 아니면 “부트캠프&quot; 같이 엔지니어를 어떤 팀이나 역할에 염두하지 않고 뽑아 이후에 명확하게 검증해보든, 대체 가능성은 지난 10년-15년간 많은 회사들에서 현대 엔지니어링 철학의 가장 인기 있는 요소입니다. 왜 그럴까요?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;위에서 말했듯, 현대 클라우드 네이티브 기술 및 추상화는 기능이 아주 풍부한 어플리케이션들을 더 정교한 인프라 추상화로 구축할 수 있게끔 합니다. 그래서 자연스럽게, 대부분의 회사들에서는 데이터 센터 설계나 운영 같은 종류의 전문 기술은 더 이상 필요하지 않게 되었습니다.&lt;/li&gt;
&lt;li&gt;지난 15년간, 업계는 소프트웨어 엔지니어링이 모든 분야의 근본이라는 생각에 집중해왔습니다. 예를 들어, Microsoft는 전통적인 QA 엔지니어들을 소프트웨어 테스트 엔지니어로 대체했습니다. 수동으로 진행하는 QA는 더 이상 효율적이지 않고 모든 테스트는 자동화되어야 한다고 생각했기 때문입니다. 마찬가지로, 기존의 운영 역할은 SRE(Site reliability engineering) 등으로 대체되었습니다. 수동으로 운영하는 건 더 이상 효율적이지 않으며, 확장할 수 있는 방법은 소프트웨어 자동화가 유일하다고 생각했기 때문입니다. 물론, 저도 이런 시대적 흐름에 동의합니다. 보다 더 효율적으로 확장하는 방법이, 바로 자동화입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그러나, 많은 신생 스타트업들의 경우, 이런 아이디어를 극한까지 적용하여 제너럴리스트 소프트웨어 엔지니어들“만” 채용하는 결과로 이어지기도 했습니다. 이들이 개발, QA, 운영 모두 담당해줄 수 있다는 기대와 함께 말이지요.&lt;/p&gt;
&lt;p&gt;대체 가능성과 제네럴리스트 채용은 초기 스타트업에서는 대체로 잘 맞는 개념입니다. 그러나, 어느 규모 이상으로 가면 모든 엔지니어가 대체 가능하다는 생각은 다음과 같은 이유로 거의 터무니없는 말이 됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;제네럴리스트 vs 스페셜리스트 (Generalist vs Specialists)&lt;/em&gt;
&lt;ul&gt;
&lt;li&gt;더 복잡한 어플리케이션과 아키텍처들은 프론트엔드, 인프라, 클라이언트, 운영, 테스팅, 데이터 사이언스 등 다양한 분야에서 더 전문화된 기술들을 필요로 합니다. 이건 제네럴리스트가 더 이상 유용하지 않거나 제너럴리스트가 전문가가 될 수 없다는 말이 아니라, 단지 더 큰 엔지니어링 조직이 성공하기 위해선 다양한 유형의 엔지니어들을 필요로 한다는 걸 말합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;em&gt;어떤 엔지니어도 모든 일을 다 하는 것을 좋아하지 않는다 (All engineers do not like doing all things)&lt;/em&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;어떤&lt;/em&gt; 엔지니어들은 제네럴리스트가 되길 원합니다. 어떤 엔지니어는 스페셜리스트가 되길 원합니다. 누구는 코드를 작성하는 걸 좋아하고, 누구는 디버깅을 하는 걸 좋아합니다. 누구는 UI를 좋아하고, 누구는 시스템을 좋아합니다. 스페셜리스트를 지원하는 엔지니어링 조직이라면, 구성원의 행복이 때로는 특정 종류의 문제와는 연관되고 다른 종류의 문제와는 연관되지 않는다는 사실을 극복하려고 노력해야 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;em&gt;어떤 엔지니어도 모든 일을 잘 하지는 못한다 (All engineers are not good at doing all things)&lt;/em&gt;
&lt;ul&gt;
&lt;li&gt;제 커리어 동안 저는 정말 대단한 사람들을 만났습니다. 그 중 일부는 IDE의 빈 파일에서부터 시작해서 놀라운 시스템을 만들어내기도 합니다. 그렇지만 동시에, 그 똑같은 사람들은 신뢰할 수 있는 시스템을 만드는 방법이나 시스템을 디버깅하는 방법, 모니터링하는 방법은 잘 모릅니다. 반대로, 아주 뛰어난 운영 엔지니어로 디버깅에 대한 전문 지식을 가지며 신뢰 가능한 시스템을 어떻게 운영할 수 있는지 선천적인 직관으로 알아 조직 전체에 어마어마한 이점을 줄 수 있는 사람이, 단지 “충분한 코딩 기술&quot;을 보여주지 못했다는 이유로 인터뷰에서 거절당하는 것을 보는 격분할 만한 인터뷰도 많이 겪었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;아이러니하고 위선적이게도, Amazon이나 Facebook같은 조직들은 소프트웨어 엔지니어링 역할의 대체 가능성을 우선시하지만, 개발과 운영 간에 각각 서로 다른 커리어 패스를 제공하며 둘의 나눠진(그렇지만 일부는 겹치는) 스킬셋을 더 중요시합니다.&lt;/p&gt;
&lt;h2 id=&quot;붕괴-the-breakdown&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B6%95%EA%B4%B4-the-breakdown&quot; aria-label=&quot;붕괴 the breakdown permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;붕괴 (The breakdown)&lt;/h2&gt;
&lt;p&gt;단일 DevOps 모델은 어느정도의 조직 규모에서 어떻게 무너질까요? 무엇이 잘못되었을까요?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Move to microservices&lt;/em&gt;. 엔지니어링 조직이 75명쯤 되면, 중앙 인프라 팀이 거의 무조건 존재하여, microservice를 만드는 각 제품팀을 지원하기 위한 공통 기반을 만듭니다.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Pure DevOps&lt;/em&gt;. 동시에, 제품 팀이 DevOps를 하라는 이야기를 듣습니다.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Reliability consultants&lt;/em&gt;. 이런 조직 규모에선 인프라 작업에 관심을 갖는 엔지니어는 이전에 운영 경험을 가진 엔지니어거나 해당 분야에서 뛰어난 직관을 가진 엔지니어일 가능성이 높습니다. 필연적으로 이러한 엔지니어는 사실상의 SRE/프로덕션 엔지니어가 되어, 조직의 나머지 부분을 지원하는 동시에 비즈니스 운영에 필요한 인프라를 지속적으로 구축해나갑니다.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Lack of education&lt;/em&gt;. 이제 우리는 스스로가 직접 개입해서 인터넷 서비스를 개발하고 운영할 수 있는 사람을 고용하길 기대합니다. 그러나, 새로운 채용과 저런 일을 수행하기 위한 지속적인 교육을 같이 하는 걸 우린 보통 너무나도 못합니다. 우리가 그런 기술들을 가르치지 않는데 운영에 대한 직관을 엔지니어들이 가지길 기대할 수 있을까요?&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Support breakdown&lt;/em&gt;. 엔지니어링 조직이 점점 많은 사람을 고용하며, 중앙 인프라 팀이 더 이상 비즈니스 성공에 중요한 인프라를 구축 및 운영하기 버거워지는 지점이 옵니다. 그러나 각 제품 팀의 운영 업무를 지원해야하는 부담은 여전히 유지됩니다. 중앙 인프라 조직은 기존 워크로드에 더해 조직 전체의 SRE 컨설턴트로서의 이중 역할을 수행하기 시작합니다. 교육 및 문서화 작업이 중요하다는 것은 누구나 알고 있지만, 이 두 가지 작업을 위해 그런 시간을 내는 걸 우선시하는 일은 잘 일어나지 않습니다.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Burnout&lt;/em&gt;. 더 문제인 건, 앞서 설명한 상황들이 인적 희생을 유발하며 조직 전체의 사기를 떨어뜨리는 일을 만든다는 것입니다. 제품 엔지니어는 자신들이 하기 싫거나 배우지 않은 일들을 하도록 요구받는다고 느낍니다. 인프라 엔지니어는 회사 전반의 기존 시스템을 높은 신뢰성을 가지고 유지하는 동시에, 교육과 문서화가 필요하다는 것은 인지하지만 이걸 진행하는 걸 우선시하긴 어렵다는 걸 알며 지원 업무의 무게로 점점 번아웃되기 시작합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;엔지니어링 조직의 규모가 일정 수준이 되면 중앙 인프라팀이 단일 DevOps 모델을 진행하며 조직에서 인적 확장 문제를 겪기 시작하고, 이런 문제들을 기점으로 상황이 급격히 나빠집니다. 이 크기는 퍼블릭 클라우드 기반 기술들이 얼마나 성숙한가에 따라 달라지며, 저는 이 글을 쓰는 현재 시점(역자 주: 2018년입니다)에선 전체 엔지니어가 100~200명(low hundreds)인 수준이라고 생각합니다.&lt;/p&gt;
&lt;h2 id=&quot;오래된-방식과-devops-방식의-중간지점-is-there-a-middle-ground-between-the-old-way-and-the-devops-way&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%A4%EB%9E%98%EB%90%9C-%EB%B0%A9%EC%8B%9D%EA%B3%BC-devops-%EB%B0%A9%EC%8B%9D%EC%9D%98-%EC%A4%91%EA%B0%84%EC%A7%80%EC%A0%90-is-there-a-middle-ground-between-the-old-way-and-the-devops-way&quot; aria-label=&quot;오래된 방식과 devops 방식의 중간지점 is there a middle ground between the old way and the devops way permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;“오래된 방식”과 DevOps 방식의 중간지점? (Is there a middle ground between the “old way” and the DevOps way?)&lt;/h2&gt;
&lt;p&gt;약 10년 이상 된 회사의 경우, 사이트 신뢰성(site reliability)이나 제품 엔지니어링 모델은 흔해집니다. 구현 형태는 회사마다 다르지만, 공통적으로 추구하는 바는 제품 관리자에게 매이지 않고 온전히 신뢰성 엔지니어링에 집중할 수 있는 엔지니어를 고용하자는 것입니다. 구현 형태의 디테일도 연관성이 꽤 있는데, 이를테면 이런 부분들입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SRE만 장애 대기를 함께 하는가, 아니면 소프트웨어 엔지니어도 장애 대기를 함께 하는가?&lt;/li&gt;
&lt;li&gt;SRE는 실제 엔지니어링 및 자동화를 하고 있는가, 아니면 배포/반복되는 장애 해소 등 수동으로 진행되고 반복적인 작업들을 수행하는가?&lt;/li&gt;
&lt;li&gt;SRE는 중앙 컨설팅 조직의 일부인가, 아니면 제품 팀에 포함되어 있는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;보통 위의 질문들에 대한 대답에 따라, 프로그램의 성공과 그 성공이 전체 엔지니어링 조직에 미치는 영향은 달라집니다. 하지만, 저는 특정 규모 이상에선 SRE 모델만이 단일 DevOps 모델이 망가지는 시점을 넘어 엔지니어링 조직의 규모를 훨씬 크게 확장할 수 있는 유일한 방법이라고 믿습니다. 보다 솔직하게는, &lt;strong&gt;여기에서 설명한 인적 확장의 한계 시점 이전에 SRE 모델을 성공적으로 도입하는 것이 현대 급성장 인터넷 회사들의 엔지니어링 조직 리더십이 가져야할 아주 중요한 책임이라고 생각합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;올바른-sre-모델이란-무엇인가-what-is-the-right-sre-model&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%98%AC%EB%B0%94%EB%A5%B8-sre-%EB%AA%A8%EB%8D%B8%EC%9D%B4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80-what-is-the-right-sre-model&quot; aria-label=&quot;올바른 sre 모델이란 무엇인가 what is the right sre model permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;올바른 SRE 모델이란 무엇인가? (What is the right SRE model?)&lt;/h2&gt;
&lt;p&gt;현재 업계에서 구현된 수많은 사례들을 볼 때, 이 질문에 대한 정답은 없으며, 모든 모델은 각각의 빈틈과 그로 인한 문제들이 있습니다. 제가 지난 10년간 관찰한 결과를 바탕으로, 제가 생각하는 SRE 모델의 핵심(sweet spot)을 간략히 설명해보면 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;운영 및 신뢰성 엔지니어링은 개별적이고 매우 가치있는 스킬셋이라는 걸 인지합니다. 모든 것을 자동화하려는 시도와 소프트웨어 엔지니어가 대체 가능하다는 생각은, 소프트웨어 엔지니어와 동등한 (어쩌면 더 나은) 가치가 있는 엔지니어링 인력 일부를 소외되게 만듭니다. 소프트웨어 엔지니어가 스트레스가 심한 장애 상황에서 디버깅하고 불을 끄는 일에 편안함을 느껴야할 필요는 없는 것처럼, 똑같이 운영 엔지니어는 빈 소스파일에 편안함을 느끼지 않아도 됩니다. &lt;strong&gt;운영 엔지니어와 소프트웨어 엔지니어는 서로 대체할 수 있는 톱니바퀴같은 존재가 아니라, 파트너입니다.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;SRE는 장애 대기하고, 대시보드를 만들고, 배포를 하는 원숭이가 아닙니다. 이들은 제품 업무가 아닌 신뢰성 업무에 집중하는 소프트웨어 엔지니어들입니다. 이상적인 구조는 &lt;strong&gt;모든&lt;/strong&gt; 엔지니어가 장애 대응, 배포, 모니터링 등의 기본적인 운영 업무를 수행하는 것입니다. 이건 아주 중요한 이야기로, 신뢰성 엔지니어와 소프트웨어 엔지니어 간의 직업 계층화를 방지하고, 소프트웨어 엔지니어가 제품 품질에 대해 보다 직접적으로 책임을 지게 만들기 때문입니다.&lt;/li&gt;
&lt;li&gt;SRE들은 구조적으로는 제품 팀에 속하나, 제품 팀의 엔지니어링 매니저에게 보고하진 않아야 합니다. 이를 통해 SRE는 자신이 속한 팀에서 스크럼을 하고, 상호 신뢰를 얻으며, 상호 적절한 견제와 균형을 유지하며 신뢰성과 기능 사이를 저울질할 수 있는 진짜 대화를 이뤄나갈 수 있습니다.&lt;/li&gt;
&lt;li&gt;각 제품 팀에 포함된 SRE의 목표는 각 제품의 신뢰성을 올리는 것입니다. 이는 신뢰성을 중시하는 기능 및 자동화, 운영 모범 사례에 대해 다른 팀을 멘토링하고 교육하는 것, 제품 팀과 인프라 팀 간의 소통(문서에 대한 피드백, Pain points, 필요 기능 등) 등을 통해 달성할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;위에서 설명한 성장 시기의 초기에 SRE 모델을 성공적으로 구현하면, 신규 채용에 대한 투자와 지속적인 교육 및 문서화를 바탕으로 조직 전체의 엔지니어링 기준을 높이는 동시에, 앞서 설명한 많은 인적 확장 문제들을 완화할 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;결론-conclusion&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%B0%EB%A1%A0-conclusion&quot; aria-label=&quot;결론 conclusion permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;결론 (Conclusion)&lt;/h2&gt;
&lt;p&gt;이 글에 서술한 내용들을 바로 적용할 수 있는 급성장 회사들은 거의 없습니다. 상당수 기업들은 현대 클라우드 네이티브 기반기술에 기초한 단일 DevOps 모델로도 충분한 숫자의 엔지니어 수, 필요한 시스템 안정성, 비즈니스에서 요구하는 제품의 반복 속도를 만들 수도 있습니다.&lt;/p&gt;
&lt;p&gt;이 글을 적용할 수 있는, 비교적 적은 숫자의 회사를 위해 이 글의 주요 내용을 요약해보면 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;경쟁하고자 하는 신기술 회사에는 DevOps 스타일의 애자일한 개발 및 자동화가 필요합니다.&lt;/li&gt;
&lt;li&gt;소규모 중앙 인프라 팀이 퍼블릭 클라우드 네이티브 기반기술을 사용하면, 교육 부족 및 역할 특수성 부족으로 인한 운영 문제가 발생하기 전까지 엔지니어링 조직을 수백명 수준으로 확장할 수 있습니다.&lt;/li&gt;
&lt;li&gt;운영 인력 확장 문제를 위해서는 신규 채용 및 지속적 교육, 문서화, 제품 팀과 인프라 팀 사이의 가교 역할을 할 수 있을만한 제품 팀 내의 SRE팀 만들기 등의 실질적인 투자가 필요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;현대의 급성장 인터넷 회사들은 (제 생각엔) 말도 안되게 많은 숫자의 번아웃을 겪고 있는데, 이는 주로 가혹할 정도의 제품 요구사항과 운영 인프라에 대한 부족한 투자 때문입니다. 이 문제가 더 커져서 조직 안정성에 큰 걸림돌이 되기 전에, 저는 엔지니어링 리더십들이 이 문제를 선제적으로 개선할 수 있을 것이라고 믿습니다.&lt;/p&gt;
&lt;p&gt;신생 기업들은 클라우드 네이티브 자동화의 발전으로 인해 기존의 전통적인 운영 엔지니어가 필요 없어졌다고 착각할 수도 있는데, 이는 사실과 아주 다릅니다. 분명 미래에 가장 최신 기술들을 쓰고 있다 하더라도, 운영 및 신뢰성을 전문으로 하는 엔지니어는 다른 방법으로는 얻을 수 없는 아주 중요한 스킬셋들을 갖고 있다고 인식되고 평가받아야 합니다. 그리고 그들의 이런 중요한 역할은 성장 시기 초기의 엔지니어링 조직에 제대로 구성되어야 할 것입니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;다시 본 글의 옮긴이, 원대영입니다. 데브옵스라는 업무 형태나 일하는 방식은 역사가 수 년밖에 되지 않은 분야라 &apos;어떻게 더 잘 일할 수 있는지&apos;에 대해선 전 세계 수많은 조직이 공통적으로 고민하는 문제라고 생각합니다. 지금 번역한 이 글에서 다루고 있는 문제 또한 저희 데브시스터즈 내부에서도 수 년간 진화를 거듭하며 배운 것과 굉장히 유사하다고 느꼈습니다. 이런 사례가 계속 공유되고 다함께 더 좋은 방법을 찾다보면 데브옵스라는 직무의 일하는 방식이 점차 잡혀갈 것이라 생각합니다.&lt;/p&gt;
&lt;p&gt;더불어 저를 비롯한 데브시스터즈의 여러 데브옵스 엔지니어들이 이 글을 기반으로 많은 생각과 의견을 나눠봤습니다. 그 부분에 대해서는 별도의 글로 찾아오도록 하겠습니다. 다음 글을 기대해주세요!&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[데브시스터즈 서버 직군은 왜 코딩 면접을 볼까?]]></title><description><![CDATA[데브시스터즈 서버 직군에서 코딩 면접을 보는 이유와 방향성에 대해서 말씀드리고 싶습니다.]]></description><link>https://tech.devsisters.com/posts/server-position-coding-test</link><guid isPermaLink="false">https://tech.devsisters.com/posts/server-position-coding-test</guid><pubDate>Fri, 10 Jun 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요. [쿠키런: 오븐브레이크]의 서버 개발자 황재영입니다.&lt;/p&gt;
&lt;p&gt;시간이 흐르면서 참 많은 것들이 바래는데요. 저희가 코딩 면접에 사용했던 문제들 또한 마찬가지였습니다. 코딩 면접 문제를 변경하려고 살펴보다 보니, &lt;strong&gt;왜 우리는 코딩 면접을 진행하는 것인가?&lt;/strong&gt; 라는 근본적인 질문에 대해 생각하게 되었습니다. 이를 위해 코딩 면접을 왜 진행하는지, 그 과정을 통해서 데브시스터즈는 지원자의 어떤 점을 알고 싶은 것인지에 대해 고민하고 논의하였습니다. 논의 이후 곰곰이 생각해보니, 위 질문에 대한 답은 데브시스터즈의 면접관뿐만 아니라, 데브시스터즈의 지원자에게도 유용한 정보가 될 것이라고 생각하였습니다.&lt;/p&gt;
&lt;p&gt;본론에 들어가기에 앞서, 이 글은 데브시스터즈 서버 직군의 논의를 통해 작성되었음을 말씀드립니다. 서버 직군이 아닌 다른 개발 직군에서는 다른 면접 절차와 기조를 가지고 있을 수 있으며, 서버 직군 면접 중에서도 진행하는 포지션에 따라 코딩면접이 생략되는 등, 포지션과 지원자에 따라 다르게 적용될 수 있습니다. 이 글은 그러한 면접 지원자분들이, 특히 &lt;strong&gt;신입 서버 개발자 포지션&lt;/strong&gt;으로 지원하시는 분들께서 이 글을 읽고 &lt;strong&gt;자신의 강점을 더 잘 어필하고 코딩 면접을 잘 진행할 수 있게&lt;/strong&gt; 도움을 드리기 위해 작성하게 되었습니다&lt;/p&gt;
&lt;h2 id=&quot;데브시스터즈-서버-직군의-면접-절차&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8D%B0%EB%B8%8C%EC%8B%9C%EC%8A%A4%ED%84%B0%EC%A6%88-%EC%84%9C%EB%B2%84-%EC%A7%81%EA%B5%B0%EC%9D%98-%EB%A9%B4%EC%A0%91-%EC%A0%88%EC%B0%A8&quot; aria-label=&quot;데브시스터즈 서버 직군의 면접 절차 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;데브시스터즈 서버 직군의 면접 절차&lt;/h2&gt;
&lt;p&gt;데브시스터즈 서버 직군의 면접 절차는 일반적으로 아래와 같습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1720px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 18.611111111111107%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAEABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAEF/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEAMQAAAB3oFB/8QAFhAAAwAAAAAAAAAAAAAAAAAAABAR/9oACAEBAAEFAlD/xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/AT//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/AT//xAAUEAEAAAAAAAAAAAAAAAAAAAAQ/9oACAEBAAY/An//xAAZEAEAAgMAAAAAAAAAAAAAAAABABFBYXH/2gAIAQEAAT8hrbAvLOmf/9oADAMBAAIAAwAAABCAD//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8QP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8QP//EABoQAQACAwEAAAAAAAAAAAAAAAEAESExUWH/2gAIAQEAAT8Qxp0HYwR7TZa79n//2Q==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/7fcb3b017f3239cdec86ddd38c9b2d9e/33ecc/0-server-recruit-process.webp 360w,
/static/7fcb3b017f3239cdec86ddd38c9b2d9e/263a4/0-server-recruit-process.webp 480w,
/static/7fcb3b017f3239cdec86ddd38c9b2d9e/0c8fb/0-server-recruit-process.webp 640w,
/static/7fcb3b017f3239cdec86ddd38c9b2d9e/521f7/0-server-recruit-process.webp 860w,
/static/7fcb3b017f3239cdec86ddd38c9b2d9e/cbd37/0-server-recruit-process.webp 1200w,
/static/7fcb3b017f3239cdec86ddd38c9b2d9e/fcade/0-server-recruit-process.webp 1720w&quot; sizes=&quot;(max-width: 1720px) 100vw, 1720px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/7fcb3b017f3239cdec86ddd38c9b2d9e/543cd/0-server-recruit-process.jpg 360w,
/static/7fcb3b017f3239cdec86ddd38c9b2d9e/a3e66/0-server-recruit-process.jpg 480w,
/static/7fcb3b017f3239cdec86ddd38c9b2d9e/56d4e/0-server-recruit-process.jpg 640w,
/static/7fcb3b017f3239cdec86ddd38c9b2d9e/2f567/0-server-recruit-process.jpg 860w,
/static/7fcb3b017f3239cdec86ddd38c9b2d9e/03ffe/0-server-recruit-process.jpg 1200w,
/static/7fcb3b017f3239cdec86ddd38c9b2d9e/e33ea/0-server-recruit-process.jpg 1720w&quot; sizes=&quot;(max-width: 1720px) 100vw, 1720px&quot; type=&quot;image/jpeg&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/7fcb3b017f3239cdec86ddd38c9b2d9e/e33ea/0-server-recruit-process.jpg&quot; alt=&quot;0 server recruit process&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;온라인 코딩 테스트 이후 총 3개의 면접 과정이 있습니다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;1차 기술 면접에서는 지원자의 기술적인 역량을 확인합니다. 먼저 구술 면접으로 지원자에게 다양한 질문을 드리며, 각각의 기술에 대해 어떤 지식과 관점을 가지고 계신지 확인합니다. 구술 면접이 종료된 후에는 잠시 쉬는 시간을 가진 이후, 주어진 시간 동안 지원자가 문제를 푸는 코딩 면접 시간을 가집니다.&lt;/p&gt;
&lt;h2 id=&quot;데브시스터즈-서버-직군이-코딩-면접을-통해-알고-싶은-것&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8D%B0%EB%B8%8C%EC%8B%9C%EC%8A%A4%ED%84%B0%EC%A6%88-%EC%84%9C%EB%B2%84-%EC%A7%81%EA%B5%B0%EC%9D%B4-%EC%BD%94%EB%94%A9-%EB%A9%B4%EC%A0%91%EC%9D%84-%ED%86%B5%ED%95%B4-%EC%95%8C%EA%B3%A0-%EC%8B%B6%EC%9D%80-%EA%B2%83&quot; aria-label=&quot;데브시스터즈 서버 직군이 코딩 면접을 통해 알고 싶은 것 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;데브시스터즈 서버 직군이 코딩 면접을 통해 알고 싶은 것&lt;/h2&gt;
&lt;p&gt;데브시스터즈 서버 직군의 코딩 면접은 언어나 라이브러리, 프레임워크 사용에 제한이 없고 웹 검색도 자유롭게 허용합니다. 지원자의 역량을 최대로 펼칠 수 있는 환경에서 진행하는 것이 능력을 더 잘 확인할 수 있다고 생각하기 때문입니다. 그런 환경 속에서 저희가 코딩 면접을 통해 알고 싶은 것은 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;먼저 &lt;strong&gt;문제를 정의하고 풀어가는 과정&lt;/strong&gt;입니다. 저희는 완벽한 결과물보다 문제를 풀어나가는 과정에 집중합니다. 문제 풀이 과정의 생각의 흐름과 작성했던 코드를 통해, 지원자에 대해 더 깊게 알 수 있다고 생각하기 때문입니다. 그래서 정답이 정해져 있는 문제가 아닌 사람에 따라 다양하게 생각하고 고민해볼 수 있는 문제들을 사용합니다. 또한 지원자가 문제를 푸는 과정은 면접관들에게 화면 공유되어 전달됩니다. 이렇게 면접관은 지원자가 어떤 고민을 거쳐 해답에 도달하는지를 확인합니다. 이 일련의 과정은 간단하지 않지만, 정답만을 확인하는 것이 아닌 문제 해결에 대한 고민과 난관을 해결하는 모습을 알고자 노력하고 있습니다.&lt;/p&gt;
&lt;p&gt;다음은 &lt;strong&gt;코드를 이해하고 개선해나가는 과정&lt;/strong&gt;입니다. 데브시스터즈에서는 코딩 면접 결과물을 제출한 이후, 면접관과 이야기하는 시간을 가집니다. 이 시간 동안 지원자는 작성한 코드에 관해서 설명하고 개선할 수 있는 점 또는 코드의 한계 등에 대해서 자유롭게 이야기 할 수 있습니다. 단순히 지금의 문제만 해결하는 코드가 아니라 &lt;strong&gt;코드를 작성하고, 소통하고, 정확히 이해하고 유지 보수하는 것&lt;/strong&gt; 또한 개발자의 중요한 역량이라고 생각합니다. 그렇기에 이러한 과정에서 지원자가 하는 말과 생각은 단순한 구술 면접에서는 볼 수 없고 코딩 면접에서만 볼 수 있는 중요한 요소입니다. 다만 이 시간은 면접 과정 중 시간이 부족하다거나 코드의 퀄리티가 뛰어나면 생략하는 때도 있으니 면접 과정 중 오해하지 않아 주셨으면 합니다.&lt;/p&gt;
&lt;p&gt;위 내용이 글 만으로는 잘 와닿지 않을 수 있을 것 같아 다음으로는 과거에 실제로 사용했던 문제들을 예시로 함께 살펴보겠습니다.&lt;/p&gt;
&lt;h2 id=&quot;데브시스터즈-서버-직군의-코딩-면접-문제&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8D%B0%EB%B8%8C%EC%8B%9C%EC%8A%A4%ED%84%B0%EC%A6%88-%EC%84%9C%EB%B2%84-%EC%A7%81%EA%B5%B0%EC%9D%98-%EC%BD%94%EB%94%A9-%EB%A9%B4%EC%A0%91-%EB%AC%B8%EC%A0%9C&quot; aria-label=&quot;데브시스터즈 서버 직군의 코딩 면접 문제 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;데브시스터즈 서버 직군의 코딩 면접 문제&lt;/h2&gt;
&lt;h3 id=&quot;문제1-멀티-스레드-그랩&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%B8%EC%A0%9C1-%EB%A9%80%ED%8B%B0-%EC%8A%A4%EB%A0%88%EB%93%9C-%EA%B7%B8%EB%9E%A9&quot; aria-label=&quot;문제1 멀티 스레드 그랩 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;문제1. 멀티 스레드 그랩&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;용감한 쿠키는 평소 사용하는 파일 내 문자열 검색 툴 grep보다 더 빠른 속도를 자랑하는 ack나 ag같은 다양한 툴들이 있다는 것을 들었다.
자신만의 dgrep을 만들어 널리 세상을 이롭게 하고 싶다고 생각하게 된 용감한 쿠키는 그 첫 단계로 멀티스레드로 동작하는 grep을 직접 만들어보기로 결심하였다.
원하는 스펙에 맞는 파일 내 문자열 검색 툴을 만들어보자.

입력 형식
 - dgrep {keyword} {relative path}

출력 형식
 - 파일의 각 line에 keyword가 있는 경우, 해당 파일과 줄 번호를 출력한다.

조건
1. relative path가 디렉토리인 경우 디렉토리 내 모든 파일에 대해 검사를 진행한다.
2. relative path 내에 또 다른 디렉토리가 존재하는 경우, 각 디렉토리 내 모든 파일에 대한 검사 또한 진행한다.
3. 멀티 스레드를 이용하여 최대한 빠르게 작업을 완료하도록 작성한다.
4. 동일한 파일에 대한 검사 결과는 한 번에 출력되어야 한다.
5. Directory 내 symlink는 없다고 가정한다.
6. 파일들은 모두 UTF8 인코딩으로 작성된 Text파일이라고 가정한다.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 문제의 핵심 풀이 요소는 &lt;strong&gt;멀티 스레드를 이용하여 여러 개의 파일을 동시에 처리하는 것&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;p&gt;이 문제를 통해 데브시스터즈에서 보고 싶은 핵심 요소는 멀티 스레드라는 &lt;strong&gt;하나의 기술에 대해 이해하고 응용할 수 있는지&lt;/strong&gt; 였습니다. 단순히 기술을 사용하여서 문제를 푸는 것이 아닌, 기술을 활용할 때의 장단점 및 유의해야 할 부분에 대해 이해하는지를 보고 싶었습니다. 특히 동시성 프로그래밍은 실제 대규모 서비스에서도 효율적인 처리를 위해 사용되는 중요한 기술 중 하나입니다. 그러나 멀티 스레드 프로그램은 단일 스레드 프로그램에 비해 예측하기 어려운 문제가 발생할 가능성이 높아 세심한 설계가 요구되고, 디버깅에도 깊은 통찰력이 필요합니다. 저희는 지원자에게 이러한 요소를 이해하고 대처하는 모습을 기대하였습니다.&lt;/p&gt;
&lt;p&gt;정리하자면, 이 문제에 대해서 기대하는 건 완벽한 멀티 스레드 그랩을 구현하는 것이 아닙니다. 일단 단순한 그랩을 구현하는 것으로 시작하고, 구현한 그랩의 동작을 확인했다면 그다음 멀티 스레드를 적용하는 풀이 과정에 대해 살펴보고 싶었습니다. 정확한 멀티 스레드를 구현하는 것보다, 멀티 스레드를 어떻게 설계하고 발생할 수 있는 문제에 대해서 어떻게 생각하고 해결하는지를 보고 싶었던 문제였습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;문제2-환상의-쿠키쇼&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%B8%EC%A0%9C2-%ED%99%98%EC%83%81%EC%9D%98-%EC%BF%A0%ED%82%A4%EC%87%BC&quot; aria-label=&quot;문제2 환상의 쿠키쇼 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;문제2. 환상의 쿠키쇼&lt;/h3&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;영화 ‘위대한 쇼맨’을 보고 크게 감동을 받은 치즈볼 새는, 쿠키들과 함께 공연을 준비하기로 마음을 먹었다.
매일매일 고된 연습을 강행한 끝에, 공연의 내용은 전부 완성되었고 세계 각국의 전문가들 앞에서 쇼케이스를 펼치게 된다.
결과는 대성공! 치즈볼 새는 자신의 동전 주머니가 가득찰 생각을 하며 행복감에 젖었다.

 이윽고 치즈볼 새는 사람들에게 온라인으로 티켓을 팔기 시작했는데,
 예매 사이트를 이용하다보니 수수료를 떼이는 것이 불만인 치즈볼 새는 당신에게 예매 API 서버를 만들어달라고 의뢰를 했다.
 치즈볼 새의 돈주머니가 가득찰 수 있게 도와주도록 하자.

공연을 만들고 고객이 예매를 하기까지의 흐름은 다음과 같다.

1. 공연을 서버에 등록한다.
2. 예매 시작 시간이 되면 사람들은 빈 좌석 목록을 보고,
3. 원하는 자리를 ‘찜&apos;한다. 다른 사람이 예매했거나 찜한 자리는 찜할 수 없다.
4. 찜한 자리에 대해서 정해진 시간 내에 결제정보를 마저 등록하여 예매를 완료한다.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 문제의 핵심 풀이 요소는 &lt;strong&gt;API 서버의 구현&lt;/strong&gt;입니다. 임의의 서버 프레임워크를 통해서 API를 구현하고 비즈니스 로직을 구현해야 합니다.&lt;/p&gt;
&lt;p&gt;이 문제를 푸는 과정에서 저희가 관찰하고자 하는 역량은 &lt;strong&gt;풀어야 할 요소를 정의하고 설계하는 부분&lt;/strong&gt;입니다. 실제 서비스 개발에서는 입력과 출력이 정해져 있는 경우가 많지 않습니다. 대부분의 경우에는 이 문제처럼 해결해야 하는 상황만 존재합니다. 이러한 상황에서 명확하게 어떤 요구사항이 있는지를 구체적으로 정의하고, 이를 구현하기 위해 어떤 값들을 사용할지 설계하는 점이 실무와 비슷한 요소가 많은 문제입니다. 실무와 비슷한 문제인 만큼, 동시에 많은 요청이 들어오고 서버가 여러 대일 때도 문제없이 작동하는 등의 구조적인 설계도 고려해볼 수 있었습니다. 그러한 문제 상황을 정의하고 설계하여서 서버를 만드는 모습을 지원자에게 기대하였습니다.&lt;/p&gt;
&lt;p&gt;이 문제 또한 완벽한 API 서버를 구현하는 것이 아닌, 서버를 설계하는 과정을 살펴보고 싶었습니다. 공연이나 좌석 등의 데이터를 어떤 형태로 저장하는지, 동시적인 요청으로 발생할 수 있는 문제들을 예측하고 어떻게 방어하는지의 과정을 확인하고자 하였습니다&lt;/p&gt;
&lt;h2 id=&quot;글을-마무리하며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B8%80%EC%9D%84-%EB%A7%88%EB%AC%B4%EB%A6%AC%ED%95%98%EB%A9%B0&quot; aria-label=&quot;글을 마무리하며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;글을 마무리하며&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;데브시스터즈 면접에선 지원자의 완벽한 모습이 아닌, 지원자가 품은 수많은 가능성을 보는 것을 기대합니다&lt;/strong&gt;
이 글이 지원자에게 와닿는 글이 되고, 코딩 면접에서 지원자의 역량을 최대한으로 확인할 수 있는 기회로 뻗어나갔으면 좋겠습니다. 긴 글 읽어주셔서 감사합니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[GatsbyJS에서 l10n(지역화) 지원하기 (간단한 함수형을 곁들인)]]></title><description><![CDATA[GatsbyJS에서 l10n(지역화)를 진행하는 방법을 소개합니다. (fp-ts를 이용한 함수형 프로그래밍을 곁들여서)]]></description><link>https://tech.devsisters.com/posts/gatsby-l10n-with-fp</link><guid isPermaLink="false">https://tech.devsisters.com/posts/gatsby-l10n-with-fp</guid><pubDate>Tue, 19 Apr 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;데브시스터즈에서는 React 기반의 GatsbyJS를 이용해 웹 사이트를 관리하고 있으며 한국어 외에도 다양한 언어를 지원하고 있습니다. GatsbyJS를 이용한 React 웹 애플리케이션에서 Gatsby API와 React를 이용해 **l10n(localization, 지역화)**를 지원하도록 페이지를 생성하고 관리하는 방법을 소개합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;함수형 프로그래밍&lt;/strong&gt; 라이브러리인 &lt;a href=&quot;https://github.com/gcanti/fp-ts&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;fp-ts&lt;/a&gt;를 같이 이용할 것이며 필요한 개념은 추가로 설명을 진행하겠습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;작성된 전체 코드는 &lt;a href=&quot;https://github.com/alstn2468/gatsby-l10n-example&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;레파지토리&lt;/a&gt;에서 확인할 수 있으며 실제 배포된 웹 애플리케이션은 &lt;a href=&quot;https://alstn2468.github.io/gatsby-l10n-example/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;여기&lt;/a&gt;에서 확인할 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;l10n-이란&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#l10n-%EC%9D%B4%EB%9E%80&quot; aria-label=&quot;l10n 이란 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;l10n 이란?&lt;/h2&gt;
&lt;p&gt;l10n은 &lt;strong&gt;l&lt;/strong&gt;ocalizatio&lt;strong&gt;n&lt;/strong&gt;의 약자로 지역화를 의미하며 간단하게는 다국어 지원을 의미하지만 그 외에도 지역의 문화, 형식, 요건들을 각각의 지역에 맞게 하는 작업을 의미합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;l과 n사이의 알파벳 개수가 10개이며, i18n(Internationalization, 국제화)와 m17n(multilingualization, 다국어화), g11n(Globalization, 세계화)와 같은 것들이 추가로 존재합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이 포스트에서 진행할 지역화는 접속하는 브라우저의 &lt;strong&gt;언어 설정에 따른 페이지 라우팅&lt;/strong&gt;과 간단한 &lt;strong&gt;번역&lt;/strong&gt; 적용을 진행해 보겠습니다.&lt;/p&gt;
&lt;h2 id=&quot;번역-데이터-준비하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B2%88%EC%97%AD-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A4%80%EB%B9%84%ED%95%98%EA%B8%B0&quot; aria-label=&quot;번역 데이터 준비하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;번역 데이터 준비하기&lt;/h2&gt;
&lt;p&gt;번역 데이터는 많은 방법으로 관리할 수 있습니다. 실제 업무를 진행할 때는 &lt;a href=&quot;https://lokalise.com/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Lokalise&lt;/a&gt;와 같은 툴을 이용해 번역을 진행하기도 하며 API를 이용해 데이터를 가져오거나 &lt;code&gt;json&lt;/code&gt;, &lt;code&gt;yml&lt;/code&gt;과 같은 확장자로 파일을 export 해서 사용하기도 합니다.&lt;/p&gt;
&lt;p&gt;이 포스트에서는 간단하게 &lt;code&gt;ts&lt;/code&gt; 파일을 이용해 가짜 번역 데이터를 생성해 보겠습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;GatsbyJS의 설정은 생략하며 중간에 필요한 Gatsby의 Node API에 대해서는 따로 설명을 진행하겠습니다. Gatsby에 대해서 더 자세히 알고 싶으시면 &lt;a href=&quot;https://www.gatsbyjs.com/docs&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;여기&lt;/a&gt;를 확인해 주세요.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;더미 데이터 생성을 위해 아래와 같이 &lt;code&gt;TranslationData&lt;/code&gt; 타입을 선언하고 &lt;code&gt;src/data&lt;/code&gt; 경로에 각각의 &lt;code&gt;{language}.ts&lt;/code&gt; 파일을 생성했습니다.&lt;/p&gt;
&lt;details&gt;
&lt;summary style=&apos;margin-bottom: 0.5em;&apos;&gt;번역 데이터 파일&lt;/summary&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;typings/translation.d.ts&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;declare&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Language&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;ko&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;en&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;ja&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;declare&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;TranslationData&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  language&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Language&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  description&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;실제로 프로젝트를 진행할 때에는 타입을 직접 설정하지 않고 &lt;a href=&quot;https://github.com/cometkim/gatsby-plugin-typegen&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;gatsby-plugin-typegen&lt;/a&gt;과 같은 플러그인 이용해 타입을 동적으로 생성합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src/data/ko.ts&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; TranslationData &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  language&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;ko&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;한국어&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  description&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;안녕 세상!&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src/data/en.ts&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; TranslationData &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  language&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;en&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;English&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  description&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;Hello World!&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src/data/ja.ts&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; TranslationData &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  language&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;ja&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  title&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;日本語&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  description&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;ハロー・ワールド!&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;데이터들은 위의 데이터에서 &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;과 같은 번역키가 존재하며 번역 값을 가져오는 함수에서 &lt;strong&gt;번역키 자동완성을 지원&lt;/strong&gt;하도록 구현을 진행해 보겠습니다.&lt;/p&gt;
&lt;h2 id=&quot;번역-데이터-생성하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B2%88%EC%97%AD-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0&quot; aria-label=&quot;번역 데이터 생성하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;번역 데이터 생성하기&lt;/h2&gt;
&lt;p&gt;번역 데이터 노드를 생성하기 위해서는 Gatsby의 Node API를 이용해야 합니다. 그냥 파일에 있는 데이터를 이용할 수 있지만, Gatsby의 장점인 &lt;strong&gt;Static Site Generation&lt;/strong&gt;을 이용하기 위해 파일에 담겨있는 데이터를 Gatsby의 GraphQL Node로 생성해야 합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Gatsby의 Node API는 &lt;a href=&quot;https://www.gatsbyjs.com/docs/reference/config-files/gatsby-node/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;여기&lt;/a&gt;에서 확인할 수 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;번역 데이터를 생성하기 위해서는 &lt;a href=&quot;https://www.gatsbyjs.com/docs/reference/config-files/gatsby-node/#createSchemaCustomization&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;createSchemaCustomization&lt;/a&gt; 과 &lt;a href=&quot;https://www.gatsbyjs.com/docs/reference/config-files/gatsby-node/#sourceNodes&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;sourceNodes&lt;/a&gt; API를 사용해야 합니다.&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;노드 스키마 생성하기&lt;/summary&gt;
&lt;p&gt;함수 이름에서도 알 수 있듯이 &lt;code&gt;createSchemaCustomization&lt;/code&gt;는 스키마를 생성하는 API입니다. root 디렉토리에 &lt;code&gt;gatsby-node.ts&lt;/code&gt; 파일을 생성하고 아래와 같이 작성합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; GatsbyNode &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;gatsby&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; gql &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; String&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;raw&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; createSchemaCustomization&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; GatsbyNode&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;createSchemaCustomization&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; actions&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createTypes &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; typeDef &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; gql&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
      type TranslationData implements Node @dontInfer {
        language: String!
        title: String!
        description: String!
      }
    &lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;createTypes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;typeDef&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;@dontInfer&lt;/code&gt; 지시어를 사용하면 모든 필드에 대한 타입 정의를 명시적으로 정의해야 합니다. &lt;a href=&quot;https://www.gatsbyjs.com/docs/reference/graphql-data-layer/schema-customization/#opting-out-of-type-inference&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;[더 보기]&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/details&gt;
&lt;p&gt;&lt;code&gt;createSchemaCustomization&lt;/code&gt; 함수를 이용해 &lt;code&gt;TranslationData&lt;/code&gt; 노드의 스키마 타입을 정의했습니다. 이제 &lt;code&gt;sourceNodes&lt;/code&gt; 함수를 이용해 실제 노드를 생성해야 합니다.&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;번역 데이터 노드 생성하기&lt;/summary&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;fp-ts/lib/Array&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; pipe &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;fp-ts/lib/function&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; GatsbyNode&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Actions &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;gatsby&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; koTranslationData &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;./src/data/ko&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; enTranslationData &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;./src/data/en&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; jaTranslationData &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;./src/data/ja&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// ... createSchemaCustomization (생략)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; createTranslationDataNode &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    createNode&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Actions&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;createNode&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token function-variable function&quot;&gt;createNodeId&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;input&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token function-variable function&quot;&gt;createContentDigest&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;input&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; Record&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;unknown&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;translationData&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; TranslationData&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;createNode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;translationData&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createNodeId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;translationData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;language&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      internal&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;TranslationData&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        content&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;translationData&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        contentDigest&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createContentDigest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;translationData&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; sourceNodes&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; GatsbyNode&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;sourceNodes&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  actions&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createNode &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  createNodeId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  createContentDigest&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; translationDatas &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    koTranslationData&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    enTranslationData&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    jaTranslationData&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    translationDatas&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;createTranslationDataNode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;createNode&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; createNodeId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; createContentDigest&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;&lt;code&gt;{language}.ts&lt;/code&gt; 형식으로 만들었던 파일에서 값을 가져와 &lt;code&gt;createNode&lt;/code&gt; 함수를 이용해 노드를 생성해줍니다. 모든 번역 데이터값을 배열로 만들어 하나의 로직으로 처리할 수 있도록 &lt;code&gt;createTranslationDataNode&lt;/code&gt; 함수를 추가로 구현했습니다. &lt;a href=&quot;https://en.wikipedia.org/wiki/Pure_function&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;&lt;strong&gt;순수 함수&lt;/strong&gt;&lt;/a&gt; 원칙을 최대한 위반하지 않기 위해 필요한 Gatsby API들을 받고 그 이후에 번역 데이터값을 인자로 받습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;참고) 이미 &lt;code&gt;createTranslationDataNode&lt;/code&gt; 함수는 순수하지 않을 수 있습니다. Gatsby API인 &lt;code&gt;createNode&lt;/code&gt;, &lt;code&gt;createNodeId&lt;/code&gt;, &lt;code&gt;createContentDigest&lt;/code&gt; 함수는 동일한 입력을 받았을 때 동일한 출력을 반환할지 보장할 수 없습니다. 하지만 함수 내부에서 사용하는 값을 모두 인자로 받는 원칙을 지키기 위해 해당 형식으로 작성하였습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;pipe&lt;/code&gt; 함수는 값들을 순차적으로 **조합(composition)**하는 역할을 합니다. &lt;code&gt;fp-ts&lt;/code&gt;의 &lt;code&gt;Array.map&lt;/code&gt;은 기존 자바스크립트 배열의 &lt;code&gt;map&lt;/code&gt; 함수와 동일한 동작을 합니다. 자세한 내용은 &lt;a href=&quot;https://gcanti.github.io/fp-ts/modules/Array.ts.html#map&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;여기&lt;/a&gt;에서 확인할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fp-ts&lt;/code&gt;의 &lt;code&gt;Array.map&lt;/code&gt; 함수 시그니처&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;declare&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; map&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;B&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function-variable function&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;B&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fa&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;B&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;참고) &lt;code&gt;pipe&lt;/code&gt; 함수는 pipe operator(&lt;code&gt;|&gt;&lt;/code&gt;)로 Javascript 표준 스펙으로 제안되고 있습니다. &lt;a href=&quot;https://github.com/tc39/proposal-pipeline-operator&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;[더 보기]&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;생성된-데이터-노드-확인하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%83%9D%EC%84%B1%EB%90%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%85%B8%EB%93%9C-%ED%99%95%EC%9D%B8%ED%95%98%EA%B8%B0&quot; aria-label=&quot;생성된 데이터 노드 확인하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;생성된 데이터 노드 확인하기&lt;/h2&gt;
&lt;p&gt;개발 서버를 실행하고 GrpahiQL(&lt;code&gt;/__graphql&lt;/code&gt;)에 들어간 뒤, 아래와 같이 모든 &lt;code&gt;TranslationData&lt;/code&gt;를 가져오기 위해 &lt;code&gt;graphql&lt;/code&gt; 쿼리를 작성하고 Excute Query 버튼을 눌러 쿼리를 실행합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;graphql&quot;&gt;&lt;pre class=&quot;language-graphql&quot;&gt;&lt;code class=&quot;language-graphql&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;query&lt;/span&gt; &lt;span class=&quot;token definition-query function&quot;&gt;translationDatas&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token object&quot;&gt;allTranslationData&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token object&quot;&gt;nodes&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;language&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;title&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;description&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;details&gt;
&lt;summary style=&apos;margin-bottom: 0.5em;&apos;&gt;translationDatas 쿼리 결과&lt;/summary&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1172px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 71.11111111111111%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAABYlAAAWJQFJUiTwAAABQElEQVR42qWTyVLDMBBE8//fF+AQCsiGF3nRNpINh6bHTqoouFhwaMtySe1+M9LOWY9Q95AQkacJKa+StOo+36pdkoTm0uJwNjhWA67G4tRY9C4givzBkI+QM071gKpzMIOHj4mJBcLvaSo0lJSRFZE6XHo8nQw6JnVHg+hpquZcU2SoL46br0xY9x61cXAuInIMrVtMtyPTUE2t87AhobMBgXVNmkwXTXMR9orMDUEyHt9aPFDttYd7aeCbca3lDX1zwsTiOx/RDIEJI1pi35Fja5dRjbck/YEs7DKPi/6E+KIG87xqKkKeF+Q9MfevDapzh/G5RmBaUWOmFckFTWETFNmMN2SOQZvib/WzBYYLbowL3vTxuaSdtLPfO3yfbzF01sG+G0hnIbwlmrTkIP8y9LyzYkYESkY2xvt/GX4BM7s5kyPkaBwAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/818b501e718fbb85352305b9ebbd577e/33ecc/0-query-translation-data.webp 360w,
/static/818b501e718fbb85352305b9ebbd577e/263a4/0-query-translation-data.webp 480w,
/static/818b501e718fbb85352305b9ebbd577e/0c8fb/0-query-translation-data.webp 640w,
/static/818b501e718fbb85352305b9ebbd577e/521f7/0-query-translation-data.webp 860w,
/static/818b501e718fbb85352305b9ebbd577e/e0c4a/0-query-translation-data.webp 1172w&quot; sizes=&quot;(max-width: 1172px) 100vw, 1172px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/818b501e718fbb85352305b9ebbd577e/53918/0-query-translation-data.png 360w,
/static/818b501e718fbb85352305b9ebbd577e/9aebd/0-query-translation-data.png 480w,
/static/818b501e718fbb85352305b9ebbd577e/0f09e/0-query-translation-data.png 640w,
/static/818b501e718fbb85352305b9ebbd577e/3e3fe/0-query-translation-data.png 860w,
/static/818b501e718fbb85352305b9ebbd577e/95fbb/0-query-translation-data.png 1172w&quot; sizes=&quot;(max-width: 1172px) 100vw, 1172px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/818b501e718fbb85352305b9ebbd577e/95fbb/0-query-translation-data.png&quot; alt=&quot;0 query translation data&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;/details&gt;
&lt;p&gt;위의 이미지 결과와 같이 &lt;code&gt;TranslationData&lt;/code&gt; 노드들이 정상적으로 잘 생성된 것을 확인할 수 있습니다. 이제 &lt;a href=&quot;https://www.gatsbyjs.com/docs/reference/config-files/gatsby-node/#createPages&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;createPages&lt;/a&gt; API를 이용해 언어별 페이지를 생성해주어야 합니다.&lt;/p&gt;
&lt;h2 id=&quot;언어별-페이지-생성하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%96%B8%EC%96%B4%EB%B3%84-%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0&quot; aria-label=&quot;언어별 페이지 생성하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;언어별 페이지 생성하기&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;createPages&lt;/code&gt; API 안에서는 생성된 Gatsby Node를 &lt;code&gt;query&lt;/code&gt;를 이용해 가져와 사용할 수 있습니다. &lt;code&gt;query&lt;/code&gt;를 이용해 가져온 &lt;code&gt;TranslationData&lt;/code&gt; 데이터를 이용해 아래와 같이 페이지를 생성할 수 있습니다. 코드가 복잡할 수 있으니 나누어 설명하겠습니다.&lt;/p&gt;
&lt;h3 id=&quot;새로-변경된-모듈-선언부&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%83%88%EB%A1%9C-%EB%B3%80%EA%B2%BD%EB%90%9C-%EB%AA%A8%EB%93%88-%EC%84%A0%EC%96%B8%EB%B6%80&quot; aria-label=&quot;새로 변경된 모듈 선언부 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;새로 변경된 모듈 선언부&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Option&lt;/code&gt;, &lt;code&gt;TaskEither&lt;/code&gt; 모듈이 추가되었으며 &lt;code&gt;pipe&lt;/code&gt; 외에도 &lt;code&gt;identity&lt;/code&gt; 함수를 가져왔습니다. &lt;code&gt;path&lt;/code&gt; 모듈은 페이지를 생성할 때 페이지 컴포넌트의 경로를 가져오기 위해 추가했습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; path &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;path&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; identity&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; pipe &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;fp-ts/lib/function&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;fp-ts/lib/Array&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;O&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;fp-ts/lib/Option&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;TE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;fp-ts/lib/TaskEither&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; GatsbyNode&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Actions &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;gatsby&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; koTranslationData &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;./src/data/ko&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; enTranslationData &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;./src/data/en&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; jaTranslationData &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;./src/data/ja&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;기존 &lt;code&gt;fp-ts/lib/Array&lt;/code&gt; 외에도 &lt;code&gt;Option&lt;/code&gt;과 &lt;code&gt;TaskEither&lt;/code&gt; 타입을 사용하게 되었습니다. 관련 타입은 아래에 간단하게 설명하고 넘어가겠습니다.&lt;/p&gt;
&lt;details&gt;
&lt;summary style=&apos;margin-bottom: 0.5em;&apos;&gt;Option 타입 설명&lt;/summary&gt;
&lt;p&gt;&lt;code&gt;Option&lt;/code&gt; 타입은 아래와 같이 구현되어 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Option&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; None &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; Some&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;Option&amp;#x3C;A&gt;&lt;/code&gt;는 선택적인 타입의 값에 대한 컨테이너 입니다. 만약에 &lt;code&gt;A&lt;/code&gt;가 존재한다면 &lt;code&gt;Option&amp;#x3C;A&gt;&lt;/code&gt;는 &lt;code&gt;Some&amp;#x3C;A&gt;&lt;/code&gt; 인스턴스가 될 것이고, &lt;code&gt;A&lt;/code&gt;가 존재하지 않는다면 &lt;code&gt;None&lt;/code&gt;이 될 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Option&lt;/code&gt;은 0개나 1개의 원소를 가진 집합 또는 &lt;strong&gt;접을 수 있는 foldable&lt;/strong&gt;한 구조로 볼 수 있습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;여기서 &lt;code&gt;fold&lt;/code&gt;는 &lt;code&gt;reduce&lt;/code&gt;로도 알려져 있으며 자료 구조를 분석해 전달받은 명령을 통해 반환값을 만들어내는 함수입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;Option&lt;/code&gt;의 &lt;code&gt;fold&lt;/code&gt; 함수의 시그니처는 아래와 같습니다. &lt;code&gt;match&lt;/code&gt; 함수는 &lt;code&gt;fold&lt;/code&gt; 함수와 동일하며 대체할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;declare&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; fold&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;B&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  onNone&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Lazy&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;B&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token function-variable function&quot;&gt;onSome&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;B&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ma&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Option&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;B&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;아래 코드는 &lt;code&gt;Option&lt;/code&gt; 타입에 대한 간단한 예시 코드입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;O&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;fp-ts/lib/Option&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;isAdult&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;age&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; age &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;19&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; getOptionAge &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;O&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fromPredicate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;isAdult&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getOptionAge&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// { _tag: &apos;Some&apos;, value: 20 }&lt;/span&gt;
&lt;span class=&quot;token builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getOptionAge&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;18&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// { _tag: &apos;None&apos; }&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;boolean&lt;/code&gt;을 반환하는 &lt;code&gt;isAdult&lt;/code&gt; 함수를 &lt;code&gt;fromPredicate&lt;/code&gt;에 넘겨주어 &lt;code&gt;Option&amp;#x3C;number&gt;&lt;/code&gt;를 반환하는 &lt;code&gt;getOptionAge&lt;/code&gt; 함수를 만들 수 있습니다. 위의 결과와 같이 조건에 따라 &lt;code&gt;_tag&lt;/code&gt; 속성으로 구분되는 &lt;code&gt;Some&lt;/code&gt; 타입과 &lt;code&gt;None&lt;/code&gt; 타입을 확인할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;getAgeLabelText&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;age&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    age&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    getOptionAge&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token constant&quot;&gt;O&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;fold&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;Not adult&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;age&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;Adult (age: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;age&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getAgeLabelText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Adult (age: 20)&lt;/span&gt;
&lt;span class=&quot;token builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getAgeLabelText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;18&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Not Adult&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;getOptionAge&lt;/code&gt;에서 반환된 &lt;code&gt;Option&amp;#x3C;number&gt;&lt;/code&gt; 타입의 값을 &lt;code&gt;fold&lt;/code&gt; 함수를 이용해 콘솔에 출력했습니다. &lt;code&gt;fold&lt;/code&gt; 함수로 전달된 &lt;code&gt;Option&lt;/code&gt; 타입의 값이 &lt;code&gt;Some&lt;/code&gt;일 경우 두 번째 인자로 전달된 함수가 실행되며 &lt;code&gt;None&lt;/code&gt;일 경우 첫 번째 인자로 전달된 함수가 실행됩니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Option&lt;/code&gt; 타입에 대하여 더 알아보시고 싶으시면 &lt;a href=&quot;https://gcanti.github.io/fp-ts/modules/Option.ts.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;fp-ts 공식 문서&lt;/a&gt;를 참고해 주세요.&lt;/p&gt;
&lt;/details&gt;
&lt;details&gt;
&lt;summary style=&apos;margin-top: 0.5em; margin-bottom: 0.5em;&apos;&gt;TaskEither 타입 설명&lt;/summary&gt;
&lt;p&gt;&lt;code&gt;TaskEither&lt;/code&gt; 타입은 &lt;code&gt;Task&lt;/code&gt;와 &lt;code&gt;Either&lt;/code&gt; 타입이 결합된 타입입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Task&lt;/code&gt; 타입 시그니처 &lt;a href=&quot;https://gcanti.github.io/fp-ts/modules/Task.ts.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;[fp-ts 공식문서 - Task]&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;Task&amp;#x3C;A&gt;&lt;/code&gt; 타입은 &lt;code&gt;A&lt;/code&gt; 타입 값을 반환하는 &lt;strong&gt;절대 실패하지 않는 비동기 연산&lt;/strong&gt;을 의미합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Task&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Either&lt;/code&gt; 타입 시그니처 &lt;a href=&quot;https://gcanti.github.io/fp-ts/modules/Either.ts.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;[fp-ts 공식문서 - Either]&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;Either&lt;/code&gt; 타입은 **두개의 가능한 타입 중 하나의 값(분리 합집합)**을 나타내는 타입입니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;분리 합집합은 서로소 합집합이라고도 하며 교집합을 허락하지 않습니다. &lt;a href=&quot;https://ko.wikipedia.org/wiki/%EB%B6%84%EB%A6%AC%ED%95%A9%EC%A7%91%ED%95%A9&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;[더 보기]&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Either&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;E&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Left&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;E&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; Right&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;Either&lt;/code&gt; 인스턴스는 &lt;code&gt;Left&lt;/code&gt; 또는 &lt;code&gt;Right&lt;/code&gt; 인스턴스 둘 중 하나가 됩니다. &lt;code&gt;Either&lt;/code&gt; 타입을 일반적으로 사용하는 것은 &lt;code&gt;Option&lt;/code&gt; 타입의 대안으로 결측값을 처리할 때 사용되며 &lt;code&gt;None&lt;/code&gt;은 유용한 값을 포함할 수 있는 &lt;code&gt;Left&lt;/code&gt;로 대체됩니다. 관습적으로는 &lt;code&gt;Left&lt;/code&gt;는 실패를 &lt;code&gt;Right&lt;/code&gt;는 성공을 위해 사용됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;TaskEither&lt;/code&gt; 타입 시그니처 &lt;a href=&quot;https://gcanti.github.io/fp-ts/modules/TaskEither.ts.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;[fp-ts 공식문서 - TaskEither]&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;TaskEither&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;E&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Task&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Either&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;E&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;TaskEither&amp;#x3C;E, A&gt;&lt;/code&gt;는 타입 &lt;code&gt;A&lt;/code&gt;의 값을 반환하거나 &lt;code&gt;E&lt;/code&gt; 타입의 &lt;strong&gt;오류가 발생할 수 있는 비동기 연산&lt;/strong&gt;을 표현합니다. 예외 처리를 하기위해서는 &lt;code&gt;tryCatch&lt;/code&gt;를 같이 사용해야 합니다.&lt;/p&gt;
&lt;/details&gt;
&lt;h3 id=&quot;createpages-함수를-구현하기-위한-함수&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#createpages-%ED%95%A8%EC%88%98%EB%A5%BC-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-%EC%9C%84%ED%95%9C-%ED%95%A8%EC%88%98&quot; aria-label=&quot;createpages 함수를 구현하기 위한 함수 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;code&gt;createPages&lt;/code&gt; 함수를 구현하기 위한 함수&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;graphql&lt;/code&gt;을 이용해서 &lt;code&gt;TranslationData&lt;/code&gt; 노드들을 가져오는 &lt;code&gt;query&lt;/code&gt;에 대한 쿼리의 결과 타입입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;QueryResult&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; allTranslationData&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; nodes&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; TranslationData&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;graphql&lt;/code&gt;을 이용해 &lt;code&gt;query&lt;/code&gt;를 하는중 예외가 발생했을 때 프로세스를 종료하기 위해 사용하는 함수입니다. &lt;code&gt;TaskEither&lt;/code&gt;의 &lt;code&gt;tryCatch&lt;/code&gt;는 예외를 잡아주지만, 프로세스를 종료시키지 않습니다. 따라서 &lt;code&gt;NodeJS&lt;/code&gt;인 &lt;code&gt;process.exit&lt;/code&gt; 함수를 이용해 프로세스를 종료시킵니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; exitProcess &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;E&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;E&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;exit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;graphql&lt;/code&gt;을 이용해서 가져온 데이터가 예외 없이 성공했다면 &lt;code&gt;match&lt;/code&gt; 함수를 통해 &lt;code&gt;getTranslationDataNode&lt;/code&gt; 함수가 실행됩니다. &lt;code&gt;Option&lt;/code&gt;의 &lt;code&gt;fromNullable&lt;/code&gt; 함수는 &lt;code&gt;null&lt;/code&gt;, &lt;code&gt;undefined&lt;/code&gt;와 같은 값이 들어오면 &lt;code&gt;None&lt;/code&gt;을 반환하고 그렇지 않으면 &lt;code&gt;Some&lt;/code&gt;을 반환합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;identity&lt;/code&gt;는 항등함수로 받은 값을 그대로 반환하는 &lt;code&gt;(a) =&gt; a&lt;/code&gt;의 시그니처를 갖는 함수입니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;getTranslationDataNode&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;response&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; data&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; QueryResult &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;data&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;allTranslationData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;nodes&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token constant&quot;&gt;O&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fromNullable&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token constant&quot;&gt;O&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; identity&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;TranslationData&lt;/code&gt; 노드들을 가져왔을 경우 노드를 순회하며 &lt;code&gt;createLocalizedPage&lt;/code&gt; 함수가 실행되며 페이지가 생성됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; createLocalizedPage &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;createPage&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Actions&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;createPage&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; component&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;translationData&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; TranslationData&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;createPage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      path&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;translationData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;language&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      component&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      context&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        language&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; translationData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;language&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;createpages-구현하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#createpages-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0&quot; aria-label=&quot;createpages 구현하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;code&gt;createPages&lt;/code&gt; 구현하기&lt;/h3&gt;
&lt;p&gt;위에서 만든 함수들을 조합해 &lt;code&gt;createPages&lt;/code&gt; 함수를 구현합니다. &lt;code&gt;TaskEither&lt;/code&gt;의 &lt;code&gt;tryCatch&lt;/code&gt;를 이용해 &lt;code&gt;graphql&lt;/code&gt; 함수를 실행하고 &lt;code&gt;Promise&lt;/code&gt;가 &lt;code&gt;reject&lt;/code&gt;되어도 그대로 &lt;code&gt;TaskEither&lt;/code&gt;를 반환하도록 &lt;code&gt;identity&lt;/code&gt;를 사용합니다.&lt;/p&gt;
&lt;p&gt;그 후 &lt;code&gt;match&lt;/code&gt; 함수를 이용해 &lt;code&gt;Left&lt;/code&gt;가 반환될 경우 &lt;code&gt;exitProcess&lt;/code&gt;를 &lt;code&gt;Right&lt;/code&gt;가 반환될 경우 &lt;code&gt;getTranslationDataNode&lt;/code&gt; 함수가 실행됩니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;TranslationData&lt;/code&gt; 노드들을 순회하며 &lt;code&gt;createLocalizedPage&lt;/code&gt; 함수가 실행되며 페이지가 생성됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; createPages&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; GatsbyNode&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;createPages&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  actions&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createPage &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  graphql&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; translationDataQuery &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; gql&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
    query translationDatas {
      allTranslationData {
        nodes {
          language
        }
      }
    }
  &lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; translationDataNodes &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token constant&quot;&gt;TE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;tryCatch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;graphql&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;QueryResult&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;translationDataQuery&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; identity&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token constant&quot;&gt;TE&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;exitProcess&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; getTranslationDataNode&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    translationDataNodes&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;createLocalizedPage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;createPage&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;./src/templates/index.tsx&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;생성된 페이지는 &lt;code&gt;dev-404&lt;/code&gt; 페이지나 &lt;code&gt;/__graphql&lt;/code&gt;에서 &lt;code&gt;SitePage&lt;/code&gt; 타입을 쿼리해 확인할 수 있습니다.&lt;/p&gt;
&lt;details&gt;
&lt;summary style=&apos;margin-bottom: 0.5em;&apos;&gt;404 페이지에서 확인한 생성된 페이지&lt;/summary&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 704px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 63.055555555555564%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAABYlAAAWJQFJUiTwAAABP0lEQVR42qWT20rDQBCG9/0v8g6lV3kFwQsRRGtiFIkkYgJtmph2d3O0ac6/2dCWCk3QODDLMsx+O/PPLjEMA3Ecg3OOMAyhaRqCIICwtm3xVyOyLEOSJMzncyiKgtlsBtM0B4EiNnYREUtd16fE4/54uGmak5+DhqBE13XYto21s8ZqtYLnefB9H8vlEpRS5HmOLMtQFEWX4+BJVbF4WMCyrItQIgBCvziJQBkFY6x313V7TQVQwMqyBKMM750cpmHC//QvA0Uwy/bgXopdkg9q9tsBkaquEPAA+p0D1+J9sJNrdCCjQxFD4Izj49lDsEkPJzHZTsC3xy027r7TDMh2bV/lZGAUxVDvG9xclXhRK7xqFcqinVQtaZoaSZJCW9S4vS4RR8c3OLXCDki3HF9R0bc5tdUfz+b8d/zXvgErQvT/pTERywAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c006c5f3fde4147972b0b40048095150/33ecc/1-page-routing.webp 360w,
/static/c006c5f3fde4147972b0b40048095150/263a4/1-page-routing.webp 480w,
/static/c006c5f3fde4147972b0b40048095150/0c8fb/1-page-routing.webp 640w,
/static/c006c5f3fde4147972b0b40048095150/79f11/1-page-routing.webp 704w&quot; sizes=&quot;(max-width: 704px) 100vw, 704px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c006c5f3fde4147972b0b40048095150/53918/1-page-routing.png 360w,
/static/c006c5f3fde4147972b0b40048095150/9aebd/1-page-routing.png 480w,
/static/c006c5f3fde4147972b0b40048095150/0f09e/1-page-routing.png 640w,
/static/c006c5f3fde4147972b0b40048095150/3be76/1-page-routing.png 704w&quot; sizes=&quot;(max-width: 704px) 100vw, 704px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c006c5f3fde4147972b0b40048095150/3be76/1-page-routing.png&quot; alt=&quot;1 page routing&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;/details&gt;
&lt;p&gt;생성된 언어별 페이지로 이동하면 오류가 발생하는 것을 확인할 수 있습니다. 이제 &lt;code&gt;createPage&lt;/code&gt; 함수에 &lt;code&gt;component&lt;/code&gt;로 넘겨준 &lt;strong&gt;페이지 컴포넌트&lt;/strong&gt;를 구현해야 합니다.&lt;/p&gt;
&lt;h2 id=&quot;templates-페이지-컴포넌트-구현하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#templates-%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0&quot; aria-label=&quot;templates 페이지 컴포넌트 구현하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;templates 페이지 컴포넌트 구현하기&lt;/h2&gt;
&lt;p&gt;앞에서 우리는 &lt;code&gt;createPage&lt;/code&gt; 함수로 페이지를 만들 때 인자로 &lt;code&gt;context&lt;/code&gt; 객체를 넘겨주었습니다. 이 &lt;code&gt;context&lt;/code&gt; 객체는 GatsbyJS의 페이지 컴포넌트에서 &lt;code&gt;Page Query&lt;/code&gt;를 하는 데 사용할 수 있으며 또한 &lt;code&gt;props&lt;/code&gt;로 전달됩니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;templates/index.tsx&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;react&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; PageProps &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;gatsby&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;PageContext&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; language&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Props&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; PageProps&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Record&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;unknown&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; PageContext&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;unknown&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; LocalizedIndexPage&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;FC&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Props&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; pageContext &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;pageContext&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;language&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; LocalizedIndexPage&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;위와 같이 템플릿 페이지 컴포넌트를 작성하고 다시 각 언어의 페이지로 이동해 결과를 확인해보면 오류가 발생하지 않고 &lt;code&gt;context&lt;/code&gt;로 전달된 &lt;code&gt;language&lt;/code&gt; 값이 화면에 표시되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;details&gt;
&lt;summary style=&apos;margin-bottom: 0.5em;&apos;&gt;정상적으로 그려지는 언어별 페이지&lt;/summary&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;localhost:8000/ko/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 754px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 43.05555555555555%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAsUlEQVR42s2OOQ6FMAxEcy6QIAuLBC10SEickSRsh+IAkAG7oPplim/pyWPFnozQWiPPc0gpURQFU9c1yrJE13Woqgpt2zLGGN5TSnGnW/NCneYsyyBoKU1THkiTER2T0TRN6PsewzBgHEc0TQMl1bunPzPS/NEbKkkSiHmecRwHtm3Dvu9fX9cV1lp47+GcY70sC7//gm4IcZ4nqEIIiFHivm9c1xUNESvZlxCR6/8NHx3tZSodXv+WAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/511ead21bfc4ce2456ea7acfdc6c499a/33ecc/2-ko-page.webp 360w,
/static/511ead21bfc4ce2456ea7acfdc6c499a/263a4/2-ko-page.webp 480w,
/static/511ead21bfc4ce2456ea7acfdc6c499a/0c8fb/2-ko-page.webp 640w,
/static/511ead21bfc4ce2456ea7acfdc6c499a/c7309/2-ko-page.webp 754w&quot; sizes=&quot;(max-width: 754px) 100vw, 754px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/511ead21bfc4ce2456ea7acfdc6c499a/53918/2-ko-page.png 360w,
/static/511ead21bfc4ce2456ea7acfdc6c499a/9aebd/2-ko-page.png 480w,
/static/511ead21bfc4ce2456ea7acfdc6c499a/0f09e/2-ko-page.png 640w,
/static/511ead21bfc4ce2456ea7acfdc6c499a/a0d62/2-ko-page.png 754w&quot; sizes=&quot;(max-width: 754px) 100vw, 754px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/511ead21bfc4ce2456ea7acfdc6c499a/a0d62/2-ko-page.png&quot; alt=&quot;2 ko page&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;localhost:8000/en/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 774px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 37.5%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAABYlAAAWJQFJUiTwAAAApklEQVR42q2OSQ6EIBREuRfGATAOYcHWGL2nCp7LhVp20elO3EPyUin4PBBVVaEoCjCNMajrOtL3PZxzaJoG1lp0XQelFMqyjLNEfzr3fo48zyFYsiyLgxS1bRsllM3zjGEYME0TxnGMD1JitIbW6g87ZVJKiHVdse87Qgjw3sdk37YNy7K84LkP/ps+vJJ3iDiOAymXuK4L53kmQ9z3nfaHSLySCx8iPRZKOqq21QAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/9cc57cae05f91a0e2c750a4ca79a77ee/33ecc/2-en-page.webp 360w,
/static/9cc57cae05f91a0e2c750a4ca79a77ee/263a4/2-en-page.webp 480w,
/static/9cc57cae05f91a0e2c750a4ca79a77ee/0c8fb/2-en-page.webp 640w,
/static/9cc57cae05f91a0e2c750a4ca79a77ee/04a2c/2-en-page.webp 774w&quot; sizes=&quot;(max-width: 774px) 100vw, 774px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/9cc57cae05f91a0e2c750a4ca79a77ee/53918/2-en-page.png 360w,
/static/9cc57cae05f91a0e2c750a4ca79a77ee/9aebd/2-en-page.png 480w,
/static/9cc57cae05f91a0e2c750a4ca79a77ee/0f09e/2-en-page.png 640w,
/static/9cc57cae05f91a0e2c750a4ca79a77ee/d1716/2-en-page.png 774w&quot; sizes=&quot;(max-width: 774px) 100vw, 774px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/9cc57cae05f91a0e2c750a4ca79a77ee/d1716/2-en-page.png&quot; alt=&quot;2 en page&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;localhost:8000/ja/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 756px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 47.22222222222222%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAtUlEQVR42s2MwQ5EMBiE+1qCKMvFxYFEIrRxIjy0N0CLmxj1b3azD9DDTvLlu8wM8zwPnHO4rosgCBDHMdI0RZIkKIoCZVkiz3NylmXUCUOOyGzIUYSXwfd9OI4DJoTAQ1VVqOsaUkq0bUvu+x7DMBDjOKLrOjSm89kI0ZClkLR9Ptg0TZjnGcdxYNs2QmtNVkphXVdiWRZj9e38ovXb+76DPePzPGEr7Lou2IQObYbBcv7/8AZv5njlV3tlKgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/de3cb906ca37cdecc4625f9cbc699c9b/33ecc/2-ja-page.webp 360w,
/static/de3cb906ca37cdecc4625f9cbc699c9b/263a4/2-ja-page.webp 480w,
/static/de3cb906ca37cdecc4625f9cbc699c9b/0c8fb/2-ja-page.webp 640w,
/static/de3cb906ca37cdecc4625f9cbc699c9b/42545/2-ja-page.webp 756w&quot; sizes=&quot;(max-width: 756px) 100vw, 756px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/de3cb906ca37cdecc4625f9cbc699c9b/53918/2-ja-page.png 360w,
/static/de3cb906ca37cdecc4625f9cbc699c9b/9aebd/2-ja-page.png 480w,
/static/de3cb906ca37cdecc4625f9cbc699c9b/0f09e/2-ja-page.png 640w,
/static/de3cb906ca37cdecc4625f9cbc699c9b/68c0a/2-ja-page.png 756w&quot; sizes=&quot;(max-width: 756px) 100vw, 756px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/de3cb906ca37cdecc4625f9cbc699c9b/68c0a/2-ja-page.png&quot; alt=&quot;2 ja page&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;/details&gt;
&lt;h2 id=&quot;루트-경로로-접근시-언어별-페이지로-redirect-시키기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A3%A8%ED%8A%B8-%EA%B2%BD%EB%A1%9C%EB%A1%9C-%EC%A0%91%EA%B7%BC%EC%8B%9C-%EC%96%B8%EC%96%B4%EB%B3%84-%ED%8E%98%EC%9D%B4%EC%A7%80%EB%A1%9C-redirect-%EC%8B%9C%ED%82%A4%EA%B8%B0&quot; aria-label=&quot;루트 경로로 접근시 언어별 페이지로 redirect 시키기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;루트(&lt;code&gt;/&lt;/code&gt;) 경로로 접근시 언어별 페이지로 Redirect 시키기&lt;/h2&gt;
&lt;p&gt;실제 웹 애플리케이션을 서비스할 때에는 &lt;code&gt;https://www.devsisters.com/&lt;/code&gt;, &lt;code&gt;https://www.cookierun-kingdom.com/&lt;/code&gt;과 같은 도메인에서 대부분 루트(&lt;code&gt;/&lt;/code&gt;) 경로로 접근하게 됩니다. 이때 각 브라우저의 언어 설정에 따라 사용자들을 각각의 언어 페이지로 이동시켜줘야 합니다.&lt;/p&gt;
&lt;p&gt;GatsbyJS는 기본적으로 폴더 기반 라우팅을 지원합니다. &lt;code&gt;src/pages&lt;/code&gt; 폴더 내부에 원하는 경로의 이름을 갖는 폴더를 생성해 페이지를 생성할 수 있습니다. 여기서 우리는 루트 경로를 이용할 것이기 때문에 &lt;code&gt;src/pages&lt;/code&gt; 경로에 &lt;code&gt;index.tsx&lt;/code&gt; 파일을 생성하고 아래와 같이 작성합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src/pages/index.tsx&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;react&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;fp-ts/lib/Array&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; graphql &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;gatsby&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; pipe &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;fp-ts/lib/function&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useLanguageNavigateEffect &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;../utils/useLanguageNavigationEffect&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; PageProps &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;gatsby&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;PageQueryData&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; allTranslationData&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; nodes&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; TranslationData&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;IndexPageProps&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; PageProps&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;PageQueryData&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; IndexPage&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;FC&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;IndexPageProps&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; data &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; languages &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    data&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;allTranslationData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;nodes&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;node&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; node&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;language&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;useLanguageNavigateEffect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;languages&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; IndexPage&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; query &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; graphql&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
  query IndexPage {
    allTranslationData {
      nodes {
        language
      }
    }
  }
&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;페이지 컴포넌트에서 페이지 쿼리를 이용해 모든 &lt;code&gt;TranslationData&lt;/code&gt; 노드들의 &lt;code&gt;language&lt;/code&gt; 필드 값들을 가져옵니다. 그 후 노드들에서 &lt;code&gt;language&lt;/code&gt; 값만 뽑아 문자열 배열을 만들고 구현하게 될 &lt;code&gt;useLanguageNavigationEffect&lt;/code&gt; 훅에 전달합니다. 실제 컴포넌트는 렌더링할 필요가 없어 &lt;code&gt;null&lt;/code&gt;을 반환했지만, 상황에 따라 &lt;code&gt;SEO&lt;/code&gt; 컴포넌트와 같은 컴포넌트들도 렌더링할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src/utils/useLanguageNavigationEffect.ts&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;필요한 모듈들을 가져와 선언합니다. 경로를 이동시키기 위한 &lt;code&gt;navigate&lt;/code&gt; 함수와 React 라이프사이클 훅인 &lt;code&gt;useEffect&lt;/code&gt;를 가져왔으며, &lt;code&gt;fp-ts&lt;/code&gt;에서 이용하는 타입은 &lt;code&gt;Option&lt;/code&gt;이 존재하며 배열에서 값을 찾는 것을 도와주는 유틸 함수인 &lt;code&gt;findFirst&lt;/code&gt; 함수를 추가로 사용합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; navigate &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;gatsby&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useEffect &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;react&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; findFirst &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;fp-ts/lib/Array&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; identity&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; pipe &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;fp-ts/lib/function&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; match&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; fromNullable &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;fp-ts/lib/Option&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;getDefaultLanguageOrElse&lt;/code&gt; 함수는 사용자의 브라우저의 언어를 지원하는 페이지가 없을 경우 실행되는 함수입니다. 선택적으로 &lt;code&gt;defaultLanguage&lt;/code&gt;인자를 받아 있을 경우 그대로 반환하며 없다면 &lt;code&gt;&apos;ko&apos;&lt;/code&gt;라는 문자열을 반환합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;getDefaultLanguageOrElse&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;defaultLanguage&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    defaultLanguage&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    fromNullable&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;ko&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; identity&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;getTargetLanguages&lt;/code&gt; 함수는 지원하는 언어 배열인 &lt;code&gt;languages&lt;/code&gt;와 사용자 브라우저의 언어를 의미하는 &lt;code&gt;navigatorLanguage&lt;/code&gt;를 전달받으며 선택적으로 &lt;code&gt;getDefaultLanguageOrElse&lt;/code&gt; 함수에서 사용하는 &lt;code&gt;defaultLanguage&lt;/code&gt; 값을 전달받습니다. &lt;code&gt;languages&lt;/code&gt; 배열에서 &lt;code&gt;navigatorLanguage&lt;/code&gt; 값으로 시작하는 값이 있는지 찾습니다. &lt;code&gt;findFirst&lt;/code&gt; 함수는 &lt;code&gt;Option&lt;/code&gt; 타입을 반환하며 값이 없을 경우 &lt;code&gt;None&lt;/code&gt;을 값이 있을 경우 &lt;code&gt;Some&lt;/code&gt;을 반환합니다.&lt;/p&gt;
&lt;p&gt;값이 존재하지 않아 &lt;code&gt;None&lt;/code&gt;이 반환되었을 경우 &lt;code&gt;getDefaultLanguageOrElse&lt;/code&gt; 함수로 기본 언어 값을 반환하며 값이 존재한다면 &lt;code&gt;identity&lt;/code&gt; 함수로 그대로 값을 반환합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;getTargetLanguages&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  languages&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  navigatorLanguage&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  defaultLanguage&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    languages&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;findFirst&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;lang&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; lang&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;startsWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;navigatorLanguage&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getDefaultLanguageOrElse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;defaultLanguage&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; identity&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;window.navigator&lt;/code&gt;의 &lt;code&gt;language&lt;/code&gt; 값을 가져와 &lt;code&gt;-&lt;/code&gt;로 분리합니다. 한국어의 경우에는 &lt;code&gt;window.navigator&lt;/code&gt;의 값이 &lt;code&gt;ko-KR&lt;/code&gt;이며 IE와 같은 구형 브라우저에서는 &lt;code&gt;language&lt;/code&gt;가 아닌 &lt;code&gt;userLanguage&lt;/code&gt;로 값을 가져와야 할 수 있습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/ko/docs/Web/API/Navigator/language&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;MDN web docs : Navigator.language&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useLanguageNavigateEffect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;languages&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;useEffect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;navigatorLanguage&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;navigator&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;language&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;split&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;-&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; targetLanguage &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getTargetLanguages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;languages&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; navigatorLanguage&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;navigate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;targetLanguage&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; replace&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;languages&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;가져온 &lt;code&gt;navigatorLanguage&lt;/code&gt; 값을 지원 언어를 가진 &lt;code&gt;languages&lt;/code&gt; 배열과 함께 &lt;code&gt;getTargetLanguages&lt;/code&gt; 함수를 호출해 경로를 이동시킬 언어 문자열을 얻습니다. 그 후 GatsbyJS의 &lt;code&gt;navigate&lt;/code&gt; 함수를 이용해 해당 경로로 이동시킵니다.&lt;/p&gt;
&lt;p&gt;이제 &lt;code&gt;/&lt;/code&gt; 경로로 접근하면 정상적으로 브라우저 설정 언어로 이동하는지 확인해봅시다.&lt;/p&gt;
&lt;details&gt;
&lt;summary style=&apos;margin-bottom: 0.5em;&apos;&gt;&lt;code&gt;/&lt;/code&gt;로 접근시 &lt;code&gt;/ko/&lt;/code&gt;로 이동하는 결과&lt;/summary&gt;
&lt;img src=&quot;/9ac1e7d6bdde80475e6ef362e510f0d8/3-redirect-to-navigator-language.gif&quot;&gt;
&lt;/details&gt;
&lt;p&gt;&lt;code&gt;/&lt;/code&gt;로 접근했을 때 정상적으로 &lt;code&gt;/ko/&lt;/code&gt; 경로로 이동하는 것을 확인할 수 있습니다. 브라우저의 언어 설정을 영어로 변경하게 된다면 &lt;code&gt;/en/&lt;/code&gt;으로 이동하게 될 것이고 일본어로 변경하게 된다면 &lt;code&gt;/ja/&lt;/code&gt;로 이동하게 됩니다.&lt;/p&gt;
&lt;p&gt;이제 &lt;code&gt;TranslationData&lt;/code&gt; 노드의 다른 필드들인 실제 번역 값을 각 페이지에 &lt;code&gt;Context API&lt;/code&gt;를 이용해 보여주도록 해보겠습니다.&lt;/p&gt;
&lt;h2 id=&quot;파일-입출력으로-graphql-fragment-생성하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%8C%8C%EC%9D%BC-%EC%9E%85%EC%B6%9C%EB%A0%A5%EC%9C%BC%EB%A1%9C-graphql-fragment-%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0&quot; aria-label=&quot;파일 입출력으로 graphql fragment 생성하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;파일 입출력으로 GraphQL Fragment 생성하기&lt;/h2&gt;
&lt;p&gt;예시와 같은 적은 양의 데이터의 경우에는 &lt;code&gt;context&lt;/code&gt;를 이용해 값을 넘겨주어 사용해도 문제 될 것이 없지만, 실제 프로젝트에서는 상당히 많은 양의 번역 데이터들이 사용하게 됩니다.&lt;/p&gt;
&lt;p&gt;따라서 GatsbyJS의 &lt;code&gt;sourceNodes&lt;/code&gt; API 함수가 실행되면서 같이 GraphQL의 &lt;code&gt;Fragment&lt;/code&gt;와 타입들을 생성할 수 있도록 개선해 보겠습니다.&lt;/p&gt;
&lt;details&gt;
&lt;summary style=&apos;margin-bottom: 0.5em;&apos;&gt;수정된 &lt;code&gt;gatsby-node.ts&lt;/code&gt; 파일&lt;/summary&gt;
&lt;p&gt;파일 입출력을 위한 &lt;code&gt;fs&lt;/code&gt; 모듈을 추가로 가져왔으며 문자열을 합치기 위해 &lt;code&gt;fp-ts&lt;/code&gt;의 &lt;code&gt;string&lt;/code&gt; 모듈을 추가로 가져왔습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;fp-ts/lib/string&lt;/code&gt; 모듈을 사용하지 않고 &lt;code&gt;+&lt;/code&gt; 연산자를 사용해도 충분합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; path &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;path&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; fs &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;fs&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; identity&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; pipe &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;fp-ts/lib/function&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;fp-ts/lib/Array&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;O&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;fp-ts/lib/Option&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;TE&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;fp-ts/lib/TaskEither&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;S&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;fp-ts/lib/string&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; koTranslationData &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;./src/data/ko&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; enTranslationData &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;./src/data/en&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; jaTranslationData &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;./src/data/ja&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; GatsbyNode&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Actions &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;gatsby&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;createTranslationDataNode&lt;/code&gt; 함수가 인자로 전달받은 &lt;code&gt;translationData&lt;/code&gt;를 그대로 반환하도록 변경했습니다. 반환된 &lt;code&gt;translationData&lt;/code&gt; 값은 파일 IO를 통해 &lt;code&gt;GraphQL Fragment&lt;/code&gt;를 생성하게 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; createTranslationDataNode &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    createNode&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Actions&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;createNode&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token function-variable function&quot;&gt;createNodeId&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;input&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token function-variable function&quot;&gt;createContentDigest&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;input&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; Record&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;unknown&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;translationData&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; TranslationData&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;createNode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;translationData&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createNodeId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;translationData&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;language&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      internal&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;TranslationData&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        content&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;translationData&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        contentDigest&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createContentDigest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;translationData&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; translationData&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;renderTextFragments&lt;/code&gt; 함수는 &lt;code&gt;TranslationData&lt;/code&gt; 노드의 키값을 받아 &lt;code&gt;GraphQL Fragment&lt;/code&gt; 문자열을 생성합니다. &lt;code&gt;reduce&lt;/code&gt; 함수를 이용해 문자열을 하나씩 합치며 문자열 &lt;code&gt;Semigroup&lt;/code&gt;의 &lt;code&gt;concat&lt;/code&gt;을 이용해 닫는 괄호를 추가해줍니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Semigroup&lt;/code&gt;은 &lt;code&gt;concat&lt;/code&gt; 매서드를 포함하고 있으며 &lt;code&gt;concat&lt;/code&gt; 매서드는 &lt;strong&gt;항상 결합 법칙을 만족&lt;/strong&gt;해야 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;renderTextFragments&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fields&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    fields&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;reduce&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token string&quot;&gt;&apos;fragment TranslationDatas_allDatas on TranslationData {&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;a&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;b&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fragment&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;S&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Semigroup&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;concat&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fragment&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;\n}&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;실제 파일 입출력을 실행하는 함수입니다. &lt;code&gt;src/__generated__/allTranslationsFragment.js&lt;/code&gt; 의 경로에 &lt;code&gt;fragments&lt;/code&gt; 상수를 생성합니다. &lt;code&gt;fragments&lt;/code&gt;를 생성하기 위해 위에서 구현한 &lt;code&gt;renderTextFragments&lt;/code&gt; 함수를 사용합니다. 동일하게 &lt;code&gt;Semigroup&lt;/code&gt;을 사용할 수 있지만, 가독성을 위해 &lt;code&gt;+&lt;/code&gt; 연산자를 사용하였습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;writeTranslationsFragmentFile&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;textFields&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;writeFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;src/__generated__&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;allTranslationsFragment.js&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&apos;/* eslint-disable */&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;
      &lt;span class=&quot;token string&quot;&gt;&apos;\n&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;
      &lt;span class=&quot;token string&quot;&gt;&quot;import { graphql } from &apos;gatsby&apos;;&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;
      &lt;span class=&quot;token string&quot;&gt;&apos;\n&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;
      &lt;span class=&quot;token string&quot;&gt;&apos;export const fragments = graphql`&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;
      &lt;span class=&quot;token string&quot;&gt;&apos;\n&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;
      &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;textFields&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;renderTextFragments&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;textFields&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;&apos;&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;
      &lt;span class=&quot;token string&quot;&gt;&apos;`;&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&apos;utf8&apos;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;기존의 &lt;code&gt;sourceNodes&lt;/code&gt;에서 변경된 점은 그대로 반환된 &lt;code&gt;translationDatas&lt;/code&gt;에서 마지막 하나의 &lt;code&gt;TranslationData&lt;/code&gt; 값을 이용해 &lt;code&gt;fragments&lt;/code&gt;를 생성하는 파일 입출력을 진행합니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;last&lt;/code&gt; 함수는 &lt;code&gt;Option&lt;/code&gt;을 반환하며 &lt;code&gt;None&lt;/code&gt;이 반환될 경우 빈 배열을 &lt;code&gt;Some&lt;/code&gt;이 반환될 경우에는 &lt;code&gt;getTranslationDataKey&lt;/code&gt; 함수를 호출해 &lt;code&gt;TranslationData&lt;/code&gt; 객체의 키값을 반환합니다.&lt;/p&gt;
&lt;p&gt;반환된 키값은 &lt;code&gt;writeTranslationsFragmentFile&lt;/code&gt; 함수를 거쳐 파일 입출력을 통해 &lt;code&gt;allTranslationsFragment.js&lt;/code&gt; 파일이 생성되게 됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;getTranslationDataKey&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;translationData&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; TranslationData&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
  Object&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;keys&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;translationData&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; sourceNodes&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; GatsbyNode&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;sourceNodes&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  actions&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createNode &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  createNodeId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  createContentDigest&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; translationDatas &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    koTranslationData&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    enTranslationData&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    jaTranslationData&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    translationDatas&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;createTranslationDataNode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;createNode&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; createNodeId&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; createContentDigest&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;last&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token constant&quot;&gt;O&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; getTranslationDataKey&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    writeTranslationsFragmentFile
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;
&lt;p&gt;개발 서버를 실행하고 GrpahiQL(&lt;code&gt;/__graphql&lt;/code&gt;)에 들어간 뒤, 아래와 같이 모든 &lt;code&gt;TranslationData&lt;/code&gt;를 가져오기 위해 &lt;code&gt;graphql&lt;/code&gt; 쿼리를 작성하고 Excute Query 버튼을 눌러 쿼리를 실행합니다.&lt;/p&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 914px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 46.388888888888886%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAABYlAAAWJQFJUiTwAAABfElEQVR42o2SW3PaMBSE+SUNvss2viAL41vBjoNNkrb0odP//0++SoZkkiEPfVgdjVazOrtHq6Fb8zrn9I8Tp+nEaZ55HEfm8zP9OFE1NVEc4zourvsZnufdYZXkGV3fk2QZUilUWZLJgm0R8/fikcud3ksyzadputS3fRAE94KefklowpBRGJIkG42YPEtRW4EQQnMGHoHwlnu+ge9rfNGhaf1K+qjxjDp0VKVkfv3NcTggq4q06amOO5qxwxcRjm1j6wgs28W2r/UtgpVZgkUwQD29IIeZVrWMXcuPnzOXXzPfu5K6G3iazshtRqgz3aYCmYXIPKTII8IoxH0XvFnYDyP95Q9qX9CVgmYXcaxj+npDVUQMTcx+l5BIw8eaSzg2CUOrz/JUd/lB0NTTNOnpzgid5bcHS9vxWFvuAmPxwbpadBxnsbm2nBvvfLZsujPhj1qwPRyo25Z4E+sL7hL8R3i3Qdydv0/5lmGguyqqmlR/iVzKL7/E/+AfYJ4SymzpsbsAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/5ffa978d1ff12417c8bfce54cc816bb7/33ecc/4-create-graphql-fragments.webp 360w,
/static/5ffa978d1ff12417c8bfce54cc816bb7/263a4/4-create-graphql-fragments.webp 480w,
/static/5ffa978d1ff12417c8bfce54cc816bb7/0c8fb/4-create-graphql-fragments.webp 640w,
/static/5ffa978d1ff12417c8bfce54cc816bb7/521f7/4-create-graphql-fragments.webp 860w,
/static/5ffa978d1ff12417c8bfce54cc816bb7/366a2/4-create-graphql-fragments.webp 914w&quot; sizes=&quot;(max-width: 914px) 100vw, 914px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/5ffa978d1ff12417c8bfce54cc816bb7/53918/4-create-graphql-fragments.png 360w,
/static/5ffa978d1ff12417c8bfce54cc816bb7/9aebd/4-create-graphql-fragments.png 480w,
/static/5ffa978d1ff12417c8bfce54cc816bb7/0f09e/4-create-graphql-fragments.png 640w,
/static/5ffa978d1ff12417c8bfce54cc816bb7/3e3fe/4-create-graphql-fragments.png 860w,
/static/5ffa978d1ff12417c8bfce54cc816bb7/3fcec/4-create-graphql-fragments.png 914w&quot; sizes=&quot;(max-width: 914px) 100vw, 914px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/5ffa978d1ff12417c8bfce54cc816bb7/3fcec/4-create-graphql-fragments.png&quot; alt=&quot;4 create graphql fragments&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;p&gt;이제 우리는 이 &lt;code&gt;fragment&lt;/code&gt;를 이용해 각각의 언어 페이지에서 페이지 쿼리를 한 후 Context API를 이용해 번역 데이터와 번역 데이터를 반환해주는 함수를 구현해야 합니다.&lt;/p&gt;
&lt;h2 id=&quot;context-api로-번역-데이터-설정하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#context-api%EB%A1%9C-%EB%B2%88%EC%97%AD-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0&quot; aria-label=&quot;context api로 번역 데이터 설정하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Context API로 번역 데이터 설정하기&lt;/h2&gt;
&lt;p&gt;가져온 데이터를 페이지에 전체적으로 전달해주기 위해서 React의 Context API를 사용합니다. 만들게 될 &lt;code&gt;L10nContext&lt;/code&gt;의 타입은 아래와 같습니다. &lt;code&gt;createContext&lt;/code&gt; 함수를 이용해 기본값을 &lt;code&gt;null&lt;/code&gt;로 갖는 새로운 &lt;code&gt;Context&lt;/code&gt;를 생성했습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src/context/L10nContext.ts&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; createContext &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;react&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Nullable&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;T&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;L10NContextType&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Nullable&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  messages&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; GatsbyTypes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;TranslationDatas_allDatasFragment&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token function-variable function&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;key&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;keyof&lt;/span&gt; GatsbyTypes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;TranslationDatas_allDatasFragment&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; L10nContext &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;createContext&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;L10NContextType&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;messages&lt;/code&gt;는 번역데이터 객체에 해당하며 &lt;code&gt;t&lt;/code&gt; 함수는 번역 데이터 &lt;code&gt;key&lt;/code&gt;를 받아 번역 데이터값을 반환하는 함수입니다. 여기에서 사용된 &lt;code&gt;GatsbyTypes&lt;/code&gt;는 &lt;a href=&quot;https://github.com/cometkim/gatsby-plugin-typegen&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;gatsby-plugin-typegen&lt;/a&gt;에 의해 생성되었으며 &lt;code&gt;templates/index.tsx&lt;/code&gt;의 템플릿 페이지 컴포넌트에 페이지 쿼리가 실행되며 타입이 생성됩니다.&lt;/p&gt;
&lt;p&gt;우리는 &lt;a href=&quot;4-create-graphql-fragments&quot;&gt;위에서&lt;/a&gt; 만들어진 &lt;code&gt;fragments&lt;/code&gt;를 이용해 템플릿 페이지 컴포넌트에서 사용할 새로운 &lt;code&gt;fragments&lt;/code&gt;를 추가로 생성할 것입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src/context/L10nContext.ts&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; graphql &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;gatsby&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; fragments &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; graphql&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
  fragment TranslationDatas on Query {
    translationData(language: { eq: $language }) {
      ...TranslationDatas_allDatas
    }
  }
&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src/templates/index.tsx&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;위에서 만들어진 &lt;code&gt;fragments&lt;/code&gt;는 아래의 템플릿 페이지 컴포넌트에서 사용됩니다. 페이지 쿼리로 가져와진 데이터는 페이지 컴포넌트의 &lt;code&gt;data&lt;/code&gt;로 전달되며 &lt;code&gt;GatsbyTypes&lt;/code&gt;에서 해당 페이지 쿼리의 타입을 가져올 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;react&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; graphql &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;gatsby&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; PageProps &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;gatsby&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;PageContext&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; language&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Props&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; PageProps&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;
  GatsbyTypes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;LocalizedIndexPageQuery&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  PageContext&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token builtin&quot;&gt;unknown&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// ... 생략&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; query &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; graphql&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
  query LocalizedIndexPage($language: String!) {
    ...TranslationDatas
  }
&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;data&lt;/code&gt; 하위에 있는 &lt;code&gt;translationData&lt;/code&gt; 값을 이용해 &lt;code&gt;L10nContext&lt;/code&gt;의 &lt;code&gt;Provider&lt;/code&gt;에 전달할 값을 반환하는 함수를 생성해야 합니다. 아래와 같이 데이터가 없을 경우 &lt;code&gt;null&lt;/code&gt;을 반환하고 그렇지 않을 경우 &lt;code&gt;L10NContextType&lt;/code&gt;에 전달될 값을 반환하도록 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;react&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;O&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;fp-ts/lib/Option&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; graphql &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;gatsby&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; pipe&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; constNull &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;fp-ts/lib/function&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; L10NContextType &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;../context/L10nContext&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// ... 생략&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;getL10nValue&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; GatsbyTypes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;LocalizedIndexPageQuery&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  t&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;typeof&lt;/span&gt; getTranslationText
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;pipe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    data&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;translationData&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token constant&quot;&gt;O&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fromNullable&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token constant&quot;&gt;O&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;match&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      constNull&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; messages&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;key&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; key&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; L10NContextType&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// ... 생략&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이제 다시 &lt;code&gt;L10nContext.ts&lt;/code&gt; 파일로 이동해 &lt;code&gt;getTranslationText&lt;/code&gt;와 같이 사용할 유틸 함수와 훅들을 구현해 보겠습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src/context/L10nContext.ts&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;아래와 같이 간단하게 구현할 수 있습니다. &lt;code&gt;getTranslationText&lt;/code&gt; 함수는 번역 데이터 객체와 키를 받아 값이 존재할 경우 번역 데이터를 그렇지 않은 경우 오류임을 표시할 수 있는 이모지를 반환합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;여기서 &lt;code&gt;messages&lt;/code&gt; 인자는 페이지 쿼리로 가져와진 번역 데이터로 실제 &lt;code&gt;L10nContext&lt;/code&gt;에 있는 &lt;code&gt;t&lt;/code&gt; 함수를 호출할 때는 &lt;code&gt;key&lt;/code&gt; 인자만 전달받을 수 있도록 구현되어 있습니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getTranslationText&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  messages&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; GatsbyTypes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;TranslationDatas_allDatasFragment&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  key&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;keyof&lt;/span&gt; GatsbyTypes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;TranslationDatas_allDatasFragment
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; messages&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;key&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;⚠️⚠️⚠️&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;useL10N&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; l10n &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useContext&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;L10nContext&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;l10n&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;throw&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;번역 데이터 리소스가 존재하지 않습니다.&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; l10n&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;useTranslation&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; l10n &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useL10N&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; l10n&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;t&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이제 기본적인 함수 구현은 끝났습니다. 템플릿 페이지 컴포넌트에서 &lt;code&gt;L10nContext&lt;/code&gt;를 채워 넣기만 하면 번역 데이터를 애플리케이션 전체에서 이용할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src/templates/index.tsx&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// ... 생략&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; getTranslationText&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; L10nContext &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;../context/L10nContext&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// ... 생략&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; LocalizedIndexPage&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;FC&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Props&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; pageContext &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; l10n &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;useMemo&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;L10NContextType&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getL10nValue&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; getTranslationText&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;L10nContext&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Provider value&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;l10n&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;pageContext&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;language&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;L10nContext&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Provider&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;usetranslation-훅으로-번역-적용하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#usetranslation-%ED%9B%85%EC%9C%BC%EB%A1%9C-%EB%B2%88%EC%97%AD-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0&quot; aria-label=&quot;usetranslation 훅으로 번역 적용하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;useTranslation 훅으로 번역 적용하기&lt;/h2&gt;
&lt;p&gt;실제 번역 데이터를 적용할 수 있도록 도와주는 함수를 반환하는 &lt;code&gt;useTranslation&lt;/code&gt; 훅을 사용하기 위해서 간단한 컴포넌트들을 구현하겠습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src/components/Title.tsx&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;react&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useTranslation &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;../context/L10nContext&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;Title&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; t &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useTranslation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;h1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;title&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;h1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; Title&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src/components/Description.tsx&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;react&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; useTranslation &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;../context/L10nContext&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;Description&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; t &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;useTranslation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;description&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; Description&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;위의 &lt;code&gt;Title&lt;/code&gt;과 &lt;code&gt;Description&lt;/code&gt; 컴포넌트와 같이 &lt;code&gt;useTranslation&lt;/code&gt; 훅을 이용해 반환된 함수를 사용하면 손쉽게 번역데이터를 적용할 수 있습니다. 앞서 구현하기로 했던 &lt;strong&gt;번역키 자동완성을 지원&lt;/strong&gt;도 확인해 보겠습니다.&lt;/p&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1102px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 32.49999999999999%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAABYlAAAWJQFJUiTwAAAA60lEQVR42p2RzU7DMBCE/RaNUNqY4L8kjrFDnNRt0iCIEFJ5/9cZlp56QaUcvtOuZndm2OgTQj8hDCf4uMB5iz5YOFtBCAEp5V2wOa2IJNT4A7rxjZgR96/ouwStNJRSd8FMeEE8TkhRwFQaJS9R7HbgnP+NokBxBTPSwJK9MSnYIeJRG/DK4CHPkW02yLLsd2ie/wiR1W1ZYkuPsKY29FGB1nVY1nd8fJ0pz/5yvaSlWzxRzkIrCBJVkixbsmxsi3E+Yf08Y1pW1DbA1A1lIm/ndlXIJcPnNMF2EW48wPoBrd/DUSFV4/7V8jdYfcxW13dz9AAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/d047e4e85bb95f14a7bf39e562b30645/33ecc/5-translation-key-auto-complete.webp 360w,
/static/d047e4e85bb95f14a7bf39e562b30645/263a4/5-translation-key-auto-complete.webp 480w,
/static/d047e4e85bb95f14a7bf39e562b30645/0c8fb/5-translation-key-auto-complete.webp 640w,
/static/d047e4e85bb95f14a7bf39e562b30645/521f7/5-translation-key-auto-complete.webp 860w,
/static/d047e4e85bb95f14a7bf39e562b30645/e084d/5-translation-key-auto-complete.webp 1102w&quot; sizes=&quot;(max-width: 1102px) 100vw, 1102px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/d047e4e85bb95f14a7bf39e562b30645/53918/5-translation-key-auto-complete.png 360w,
/static/d047e4e85bb95f14a7bf39e562b30645/9aebd/5-translation-key-auto-complete.png 480w,
/static/d047e4e85bb95f14a7bf39e562b30645/0f09e/5-translation-key-auto-complete.png 640w,
/static/d047e4e85bb95f14a7bf39e562b30645/3e3fe/5-translation-key-auto-complete.png 860w,
/static/d047e4e85bb95f14a7bf39e562b30645/39d99/5-translation-key-auto-complete.png 1102w&quot; sizes=&quot;(max-width: 1102px) 100vw, 1102px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/d047e4e85bb95f14a7bf39e562b30645/39d99/5-translation-key-auto-complete.png&quot; alt=&quot;5 translation key auto complete&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;p&gt;위의 이미지와 같이 &lt;code&gt;t&lt;/code&gt; 함수에 인자를 입력하려고 할 때 &lt;strong&gt;번역키가 자동완성이 되도록 지원&lt;/strong&gt;되는 것을 확인할 수 있습니다. 실제 번역키의 경우 &lt;strong&gt;길고 복잡한 경우가 많기 때문에 생산성에 있어 큰 도움&lt;/strong&gt;이 될 것입니다.&lt;/p&gt;
&lt;p&gt;마지막으로 언어별 페이지에 접근해 실제 번역 데이터가 정상적으로 적용되었는지 확인해 보겠습니다.&lt;/p&gt;
&lt;details&gt;
&lt;summary style=&apos;margin-bottom: 0.5em;&apos;&gt;번역 데이터가 적용된 언어별 페이지&lt;/summary&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;localhost:8000/ko/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 734px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 45.833333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAABYlAAAWJQFJUiTwAAAA90lEQVR42q2OvYqDQBRG583EjQYxYCJjFBHBmLeyFwSxsrIWH0JIKYqKjT+VkG+ZMSy7xRa75sJh7p3LPXxEFEVIkgRBEMB6RVGgqipOpxN0XYfruqCUwrZtXK9XHI8yDocPfiNJB8iy/GJzkPP5zA81TQPrDcOAaZqwLAuO4+B+v8PzPPi+j9vtxveGQTcofc0bl8sFZBxHzPOMaZqwLAvY3LYtuq7jb9M0P2D/v9H3PUhRFIjjGHmeI01TZFmG5/OJ/xZhgiAIEEURwjBEkiRY15UvmfivkO/2Pcm+ElZVhbIs8Xg8OHulZBgGMCmjruv9CfHm+gRBm2plTtE01QAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/1ddfbe41ae2041ce7da4149b46c5c535/33ecc/6-ko-page.webp 360w,
/static/1ddfbe41ae2041ce7da4149b46c5c535/263a4/6-ko-page.webp 480w,
/static/1ddfbe41ae2041ce7da4149b46c5c535/0c8fb/6-ko-page.webp 640w,
/static/1ddfbe41ae2041ce7da4149b46c5c535/b6719/6-ko-page.webp 734w&quot; sizes=&quot;(max-width: 734px) 100vw, 734px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/1ddfbe41ae2041ce7da4149b46c5c535/53918/6-ko-page.png 360w,
/static/1ddfbe41ae2041ce7da4149b46c5c535/9aebd/6-ko-page.png 480w,
/static/1ddfbe41ae2041ce7da4149b46c5c535/0f09e/6-ko-page.png 640w,
/static/1ddfbe41ae2041ce7da4149b46c5c535/e42f2/6-ko-page.png 734w&quot; sizes=&quot;(max-width: 734px) 100vw, 734px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/1ddfbe41ae2041ce7da4149b46c5c535/e42f2/6-ko-page.png&quot; alt=&quot;6 ko page&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;localhost:8000/en/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 812px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 44.44444444444444%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAABYlAAAWJQFJUiTwAAABAklEQVR42q2Qu6qDQBiE970kmljEC1FEWxvJQ/kYKQUhViKpBLEWvERQMVgmEXSOuxwDp0xOBj7mh92ZvRCe5yEIAjiOw263gyiKkGWZuWmabNY0DYqiYLvdsr0UOq9sNhuWpxBVVUHZ7/c4HA4sbFkWDMPA8XiEbdtwHIc5XdMXtF90naJDXQ6TJIlBmqZB3/cYhgFd1+F2u6Gua5RliTzPURTFy6uq+sv1yvy6+Aq5XC6IoghhGCIIAniex4qp5nnGuyJxHON8PsN1Xfi+j9Pp9CqcpomVvgNZm2n4GyL0r9I0RZZlSJIEbdt+/FxWOI4jHo8Hns8n7vf7v29K8GX9AHRUZsXGAxF3AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/30382b9f0fbf860d42e029968014d77f/33ecc/6-en-page.webp 360w,
/static/30382b9f0fbf860d42e029968014d77f/263a4/6-en-page.webp 480w,
/static/30382b9f0fbf860d42e029968014d77f/0c8fb/6-en-page.webp 640w,
/static/30382b9f0fbf860d42e029968014d77f/65d6a/6-en-page.webp 812w&quot; sizes=&quot;(max-width: 812px) 100vw, 812px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/30382b9f0fbf860d42e029968014d77f/53918/6-en-page.png 360w,
/static/30382b9f0fbf860d42e029968014d77f/9aebd/6-en-page.png 480w,
/static/30382b9f0fbf860d42e029968014d77f/0f09e/6-en-page.png 640w,
/static/30382b9f0fbf860d42e029968014d77f/d2c78/6-en-page.png 812w&quot; sizes=&quot;(max-width: 812px) 100vw, 812px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/30382b9f0fbf860d42e029968014d77f/d2c78/6-en-page.png&quot; alt=&quot;6 en page&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;localhost:8000/ja/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 814px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 43.611111111111114%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAABYlAAAWJQFJUiTwAAAA3UlEQVR42q2QuwqDQBBF98cUNYIovkDQVv0pGwsrrSys/RmfhaDWouiNu0EhkCrJhcPMLrN3ZpZIkgSe5yEIAmiuaRpkWYbjOLAsC6ZpwrZtKIoCkdaIIouPs5YinmeO426IYRhQVRW6rrPHrusyA9/3GUEQIAxDeJ53NniZX5FxNqVDXJBpmjDPM4PmTdOgbVtUVfXGdf+JrutuSFmWKIoCSZIgTVNQHceBb0WyLEOe54iiCHEc34bfQvBnEfo/4zhiGAb0fY+6rn9beVkWZrDvO9Z1xbZtv03475Wf8xRorAimUIEAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/27ae368ff1a8c7a20cd4f2235bfc56cc/33ecc/6-ja-page.webp 360w,
/static/27ae368ff1a8c7a20cd4f2235bfc56cc/263a4/6-ja-page.webp 480w,
/static/27ae368ff1a8c7a20cd4f2235bfc56cc/0c8fb/6-ja-page.webp 640w,
/static/27ae368ff1a8c7a20cd4f2235bfc56cc/b22d3/6-ja-page.webp 814w&quot; sizes=&quot;(max-width: 814px) 100vw, 814px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/27ae368ff1a8c7a20cd4f2235bfc56cc/53918/6-ja-page.png 360w,
/static/27ae368ff1a8c7a20cd4f2235bfc56cc/9aebd/6-ja-page.png 480w,
/static/27ae368ff1a8c7a20cd4f2235bfc56cc/0f09e/6-ja-page.png 640w,
/static/27ae368ff1a8c7a20cd4f2235bfc56cc/1745e/6-ja-page.png 814w&quot; sizes=&quot;(max-width: 814px) 100vw, 814px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/27ae368ff1a8c7a20cd4f2235bfc56cc/1745e/6-ja-page.png&quot; alt=&quot;6 ja page&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;/details&gt;
&lt;h2 id=&quot;마무리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EB%AC%B4%EB%A6%AC&quot; aria-label=&quot;마무리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;마무리&lt;/h2&gt;
&lt;p&gt;모든 작업이 완료되었습니다. 이 포스트에서 어떤 작업을 진행했는지 살펴보겠습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Gatsby Node API를 이용해 &lt;strong&gt;스키마와 노드를 생성&lt;/strong&gt;했습니다.&lt;/li&gt;
&lt;li&gt;생성된 노드를 이용해 &lt;strong&gt;페이지를 생성&lt;/strong&gt;했습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/&lt;/code&gt; 경로로 접근 시 &lt;strong&gt;브라우저 설정 언어로 이동&lt;/strong&gt;되게 했습니다.&lt;/li&gt;
&lt;li&gt;전체 &lt;strong&gt;번역 데이터를 컴포넌트 전체적으로 사용&lt;/strong&gt;할 수 있게 했습니다.&lt;/li&gt;
&lt;li&gt;번역 데이터를 가져오는 함수에서 &lt;strong&gt;번역키 자동완성을 지원&lt;/strong&gt;하도록 했습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;위와 같은 작업을 진행하며 대부분의 로직을 &lt;code&gt;fp-ts&lt;/code&gt;를 이용한 함수형 프로그래밍 기반으로 작업했습니다. 기존 명령형 코드에 비하여 조금 더 눈에 보기 편하셨나요? 아니면 이해하기 어려우셨나요?&lt;/p&gt;
&lt;p&gt;프론트엔드 개발을 진행하며 자주 겪어보지 못했던 &lt;strong&gt;파일 입출력을 이용한 개발&lt;/strong&gt;이나 익숙하지 않은 패러다임인 &lt;strong&gt;함수형 프로그래밍&lt;/strong&gt;을 데브시스터즈에서 웹 애플리케이션 지역화를 지원하는 방법과 함께 소개해드릴 기회가 된 것 같아 즐겁게 글을 작성할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;긴 글 읽어주셔서 감사드리며 데브시스터즈에서 여러 가지 문제들을 같이 즐겁게 해결해 나갈 개발자들을 모집합니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[WireGuard로 멋진 VPN 서버 구축하기 - 1]]></title><description><![CDATA[AWS 네트워킹 스택과 리눅스 커널 네트워킹 스택을 이용해서 자체 WireGuard VPN의 기반을 구성한 경험을 소개합니다.]]></description><link>https://tech.devsisters.com/posts/wireguard-vpn-1</link><guid isPermaLink="false">https://tech.devsisters.com/posts/wireguard-vpn-1</guid><pubDate>Fri, 03 Sep 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, 저는 인프라셀에서 데브옵스 엔지니어로 일하고 있는 박재현이라고 합니다. 데브시스터즈에서는 사내 인프라 혹은 클라우드 인프라 자원에 접근하기 위해서 OpenVPN을 사용해왔습니다. 이번 글에서는 OpenVPN에서 WireGuard 기반 VPN으로 전환하게 된 이유와 어떻게 VPN 시스템을 직접 구축하게 되었는지에 대해서 설명합니다. 글의 내용이 길고 주제에 대한 연관성으로 2부로 구성하게 되었습니다. 1부에서는 WireGuard에 대한 설명과, 커널 네트워킹 스택, 그리고 AWS의 네트워킹 스택에 대한 이야기를 합니다. 2부에서는 VPN 서버에서 사용하는 인증과 eBPF를 이용한 패킷 필터링, 그리고 eBPF를 이용한 유저 편의기능을 소개합니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;img src=&quot;https://www.wireguard.com/img/wireguard.svg&quot;&gt;
    &lt;figcaption&gt;공식 WireGuard 로고&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;vpn&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#vpn&quot; aria-label=&quot;vpn permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;VPN?&lt;/h2&gt;
&lt;p&gt;VPN이란 Virtual Private Network의 약자로, 가상 사설망이라는 의미입니다. 일반적으로 사설망은 외부에서 접근이 안 되고, 사설망 내부에서만 통신을 할 수 있습니다. 예를 들어, 게임 서버와 데이터베이스는 외부에서 직접 접근하지 못하면서도 서로 통신은 할 수 있어야하기 때문에, 사설망 내부에 함께 두는 것이 적절합니다. 그런데 서버 개발 혹은 운영을 하다보면, 직접 그러한 자원들에 사람이 직접 접근해야할 필요가 종종 생깁니다. 이럴 때 VPN을 사용하게 되면, 외부 네트워크로부터 VPN 서버를 통해 사설망 내부 자원에 접근할 수 있게 됩니다.&lt;/p&gt;
&lt;p&gt;VPN은 외부 네트워크와 사설 네트워크 망을 잇는 통로이기 때문에, 그만큼 보안이 중요합니다. 허가된 사용자만 접근할 수 있어야하며, 통신이 적절히 잘 암호화되어서 중간에서 도청 또한 불가능해야만 합니다. 이러한 요구조건 위에서 다른 편의기능을 추가한 여러 VPN 구현체들이 존재하고, 그 중에서 데브시스터즈는 OpenVPN이라는 솔루션을 사용하고 있었습니다.&lt;/p&gt;
&lt;h2 id=&quot;왜-openvpn--wireguard&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%99%9C-openvpn--wireguard&quot; aria-label=&quot;왜 openvpn  wireguard permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;왜 OpenVPN → WireGuard?&lt;/h2&gt;
&lt;p&gt;OpenVPN으로도 오랫동안 잘 사용하고 있었지만, 사용해오다보니 여러 가지 문제점들이 있었습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Single Sign-On(SSO) 연동이 불편합니다.&lt;/strong&gt; 데브시스터즈는 Keycloak이라는 제품을 사용해 인증 및 권한 제어를 하고, Google 계정을 OAuth 2로 연동하여 사용하고 있습니다. 많은 서비스들을 여기서 제공하는 SSO로 인증을 수행하고 있었는데, OpenVPN에 연결할 때에 사용할 사용자 계정으로 SSO 연동을 하기 어려워 OpenVPN 계정은 따로 관리를 해야만 했고, 신규 입사자가 들어올 때마다 일일이 계정을 발급해주거나 퇴사자 처리를 할 때 따로 계정을 지워줘야 하는 등의 관리 상 불편함이 있었습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;사설망 접근 유저가 누구인지 접근 로그를 보고 특정을 하기가 어렵습니다.&lt;/strong&gt; OpenVPN을 사용하고 있을 때, VPN 서버에서 사설망으로 가는 패킷에 대해 NAT(Network Address Translation)를 적용하고 있었습니다. 이렇게 하게 되면 NAT의 특성 상 사설망 자원들의 IP 로그에는 어떤 유저가 사용했는지에 관계 없이 모두 VPN 서버 자체의 IP가 찍히게 됩니다. 보안 사고 혹은 침해 사고 등에 대비하기 위해서는 VPN을 사용하더라도 어디에 누가 접근을 시도했는지 특정할 수 있으면 좋겠다는 의견이 있었습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenVPN 기업용 라이센스 관리가 불편합니다.&lt;/strong&gt; 유저 수 만큼 비용이 늘어나는 데다가, 라이센스 만료에도 항상 신경쓰고 있어야하기 때문입니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WireGuard와 비교해서 무겁고 느립니다.&lt;/strong&gt; 여러 벤치마크 자료들을 봐도 레이턴시와 대역폭 측면에서 WireGuard가 우세에 있고, 초기 연결 속도 또한 체감이 될 정도로 차이가 납니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이러한 문제점들을 인식하고 있던 중, WireGuard를 사용한 VPN을 써보면 어떻겠냐는 의견이 나왔습니다.&lt;/p&gt;
&lt;h2 id=&quot;wireguard&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#wireguard&quot; aria-label=&quot;wireguard permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;WireGuard?&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://www.wireguard.com/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;WireGuard&lt;/a&gt;는 다른 VPN 솔루션들과는 달리 두 지점(Peer) 사이의 통신을 암호화하는 프로토콜에 대한 구현체입니다. WireGuard의 장점은 여러 가지가 있지만, 그 중 대표적인 것만 꼽아보면 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;속도와 레이턴시가 우수합니다.&lt;/strong&gt; UDP만을 사용하며, 불안정한 연결 환경에서도 잘 작동하도록 설계되었습니다. 또한 Linux에 내장되어 있는 암호화 기법들을 사용하면서 잘 최적화되어 있어 암호화 성능도 좋고, 모바일이나 임베디드 환경에서도 사용하기 좋습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;첨단 암호화 기법&lt;/strong&gt;을 사용하여 안전하며, 성능도 우수합니다. WireGuard에서 내부적으로 사용하는 Noise protocol framework는 Zero round trip, forward secrecy 등을 지원하여 매우 빠른 연결 속도와 더 나은 통신의 보안을 지원합니다. 또한, WireGuard에서 패킷 암호화에 사용하는 Curve25519, ChaCha20 Poly1305 등의 암호화 기법들은 다른 암호화 기법들과 비교하여 훨씬 안전하고 빠른 암호화를 지원합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Peer 배포가 쉽습니다.&lt;/strong&gt; WireGuard에서 Peer끼리 정보를 주고 받을 때 공개키 암호화를 사용합니다. 이는 바꿔말하면, 공개키로 자신의 신원을 증명할 수 있다는 말입니다. 따라서 WireGuard 통신을 세팅할 때에는 공개키만 서로 알고 있다면 가능하기 때문에, 우리가 자주 쓰는 SSH Key의 deploy 만큼이나 Peer 배포가 간단합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;연결이 안정적입니다.&lt;/strong&gt; 일상 생활에서 스마트폰을 사용하다보면 와이파이와 LTE 네트워크 사이를 전환하는 등의 상황을 많이 접하곤 합니다. 이럴 때에 일반적인 TCP 연결이라면 재연결을 해야하는 상황이 벌어지기도 하여 사용 경험이 안 좋은 때가 있습니다. 하지만 WireGuard는 UDP만을 사용하고, 공개키를 기반으로 연결이 정의되면서 내부적으로 상태를 가지지 않는(Stateless) 특성 덕분에, 이러한 상황에서도 끊기지 않고 안정적이게 통신을 이어나갈 수 있고, 데몬 등으로 연결을 관리해주어야할 필요가 없어지기 때문에 연결 관리 코스트에서도 자유롭습니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;리눅스에 내장되어있어 사용하기 편합니다.&lt;/strong&gt; WireGuard는 리눅스 커널 5.6 버전부터 기본으로 포함되어 있어 그 버전 이상의 커널을 사용하면 따로 설치가 필요 없는데다가, 유저 공간의 어플리케이션이 아닌 커널 모듈로서 작동하기 때문에, 서버 어플리케이션이 비정상 종료 되어도 운영체제가 돌아가고 있는 한 WireGuard의 기능들은 모두 정상적으로 동작하여 안정적이기까지 합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;하지만 두 지점 사이의 통신을 구현한 프로토콜에 지나지 않기 때문에 다양한 부가 기능들로 무장한 다른 VPN 솔루션들과 편의성 측면에서는 매우 비교되는 것이 현실입니다. 바꿔말하면, 필요한 부분들을 직접 입맛에 맞게 구현해서 쓰면 유연성과 편의성, 그리고 위에 나열한 장점들까지 모두 가져갈 수 있는 좋은 VPN 솔루션을 구현할 수 있다는 결론에 다다를 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;요구사항-분석&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9A%94%EA%B5%AC%EC%82%AC%ED%95%AD-%EB%B6%84%EC%84%9D&quot; aria-label=&quot;요구사항 분석 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;요구사항 분석&lt;/h2&gt;
&lt;p&gt;OpenVPN을 사용하면서 느꼈던 단점을 개선하면서, 원래 사용하고 있던 기능들도 모두 포함해야하기 때문에, 구현해야하는 요구사항들을 분석해보았습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;그룹 별 접근 가능한 사설망 자원 접근 제어&lt;/li&gt;
&lt;li&gt;유저마다 고유한 아이피로 사설망에 접근&lt;/li&gt;
&lt;li&gt;Keycloak SSO에 인증 연동&lt;/li&gt;
&lt;li&gt;키를 일정 시간만 사용 가능하게 하도록 하기, 활성화되지 않은 키에 대한 경고 알림 보내주기&lt;/li&gt;
&lt;li&gt;기타 보안 및 편의 기능, 관리자 UI&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;서버 구현을 작성할 때 어떤 언어로 작성할 지도 중요합니다. 서버 구현체는 &lt;a href=&quot;https://golang.org/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Go&lt;/a&gt;를 사용하기로 결정했는데, 그 이유는 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;데브시스터즈의 많은 서비스와 컴포넌트들이 Go로 작성되어 있습니다. 따라서 내부 프로그래머들에게 익숙한 언어이기 때문에 쉽게 코드를 읽고 유지보수를 할 수 있습니다.&lt;/li&gt;
&lt;li&gt;구현에 필요한 라이브러리들이 잘 만들어져 있습니다. 예를 들면, 유저스페이스에서 WireGuard 커널 모듈과 통신할 때 &lt;a href=&quot;https://github.com/WireGuard/wgctrl-go&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;wgctrl-go&lt;/a&gt; 라이브러리를 쓸 수 있습니다. 또한, 리눅스 네트워킹 스택과 통신을 도와주는 &lt;a href=&quot;https://github.com/vishvananda/netlink&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;netlink&lt;/a&gt;, &lt;a href=&quot;https://github.com/florianl/go-tc&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;go-tc&lt;/a&gt; 라이브러리, eBPF 프로그램 로딩과 관리에 필요한 &lt;a href=&quot;https://github.com/cilium/ebpf&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;cilium/ebpf&lt;/a&gt; 등, 필요한 라이브러리들이 이미 소스코드가 공개되어 있습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;요구사항들을 잘 분석했으니, 이제 구현 과정을 한번 따라가봅시다.&lt;/p&gt;
&lt;h2 id=&quot;그룹-별-접근-제어&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B7%B8%EB%A3%B9-%EB%B3%84-%EC%A0%91%EA%B7%BC-%EC%A0%9C%EC%96%B4&quot; aria-label=&quot;그룹 별 접근 제어 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;그룹 별 접근 제어&lt;/h2&gt;
&lt;p&gt;데브시스터즈는 클라우드 컴퓨팅 플랫폼으로 AWS를 사용합니다. AWS에는 &lt;strong&gt;논리적으로 분리된 사설망인 VPC&lt;/strong&gt;라는 개념이 있는데, 각 VPC는 IP 주소의 대역을 가지게 됩니다. 데브시스터즈에서는 용도와 권한에 따라 여러 VPC를 구성해서 쓰고 있습니다. 예를 들면, 프로덕션 게임 서버와 개발용 게임 서버는 같은 네트워크에 위치할 필요가 없으므로 VPC를 사용해 분리를 해둔다거나, 민감한 정보들을 담은 데이터베이스를 다른 서버들로 분리하고 관리자만 접근 가능하도록 하는 것입니다. 이처럼 용도 혹은 사용자의 권한에 따라서 VPC를 분리해놓았기 때문에, VPN 서버에 연결한 사용자가 내부 VPC 전부에 접근할 수 있으면 보안 측면에서 허술할 수 있습니다. 따라서 직군마다 접근할 수 있는 망 대역을 분리하는 작업이 필요합니다. Keycloak에는 그룹이라는 개념이 있는데, 이것을 그대로 사용하여 접근 제어에 사용하기로 결정했습니다.&lt;/p&gt;
&lt;p&gt;VPN 서버 내부적으로는 각 그룹 별로 사용하는 네트워크 인터페이스를 나누고, 각자의 라우팅 테이블을 가지도록 설계했습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 659px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 62.77777777777778%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAIAAAAmMtkJAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB8UlEQVR42n1S32vTUBTOv+iToqAPPu1FhMlQFMSXCRN90c6N2RVsR2txwx84i/jgrDDBByda6zRpzdL2Lmma5CY3N7lJ7r3eLEmd0HkI4eNwvnO+850rcc6DIMQYE0L4UURRBMaWrh8eDAHyfZFhjPFZIYERaLffd7vf+70eLwrdvZbdvDZ5vhj5Tp6cxZfGhllav7vcWLx1bx77GACgaQO6tUCa8+TxRdp9K/REJDxGYbxoJFkTu9oot9qbT19thGEIHcfxsNu44pcvwPunyK9dwjgYDU3TFBRGKWNUUI8Ak+I4RghlHTOF6doWsD/Uvc5ONoNSKssyhE5fPdDUvrb/RVE1CKHEpzsVYlKQY1Zsm2eYb4WbV8P6pfhjNZXNZpshqpP0K4ImsfjH+zvBo3Pk2Y2gNsdpLE3nJkleKgDyMSIJxGEQBIUWKgDRFWf5DFo7D7duppM9z+t86/SU/nAwzCSIqkP562i7pL6pOI6ddTQMI3sIgaG6P3YTglOyOTYrtdXm9nqpsuS6nuvCCfRp6zbZmIvKZ9nPd6LX2NCFYYU0NvVVQi5aqz6ov155WFsydDFA/z0AqLkQNC6jldPk8ws/4Z4L/5ohgtKcLG6rKLK4FnTg1GesfJo8uW69vBMj+38v7B+L2fGbseL0J8YfnfrOk9HU6AMAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/71ce51c39f6d0e226b22de585358e32d/33ecc/1-route.webp 360w,
/static/71ce51c39f6d0e226b22de585358e32d/263a4/1-route.webp 480w,
/static/71ce51c39f6d0e226b22de585358e32d/0c8fb/1-route.webp 640w,
/static/71ce51c39f6d0e226b22de585358e32d/f67d4/1-route.webp 659w&quot; sizes=&quot;(max-width: 659px) 100vw, 659px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/71ce51c39f6d0e226b22de585358e32d/53918/1-route.png 360w,
/static/71ce51c39f6d0e226b22de585358e32d/9aebd/1-route.png 480w,
/static/71ce51c39f6d0e226b22de585358e32d/0f09e/1-route.png 640w,
/static/71ce51c39f6d0e226b22de585358e32d/4978c/1-route.png 659w&quot; sizes=&quot;(max-width: 659px) 100vw, 659px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/71ce51c39f6d0e226b22de585358e32d/4978c/1-route.png&quot; alt=&quot;1 route&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;인터페이스 별로 따로 라우팅 테이블을 만들어 관리합니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;네트워크 인터페이스가 각각의 라우팅 테이블을 사용하도록 하기 위해서는 &lt;code&gt;iproute2&lt;/code&gt; 의 &lt;code&gt;ip-rule&lt;/code&gt;을 사용해서 제어하고, 나머지 필요한 &quot;목적지 기반&quot; 라우팅은 &lt;code&gt;ip-route&lt;/code&gt;를 사용하여 제어합니다.&lt;/p&gt;
&lt;p&gt;유저 개개인은 각 직군에 대응되는 네트워크 인터페이스를 배정받아서 사용하게 됩니다. WireGuard에서는 네트워크 인터페이스 별로 다른 비밀키를 사용해서 인증하도록 되어있으므로, 다른 직군에 속한 유저가 더 넓은 권한을 가지는 직군의 네트워크 인터페이스를 사용하는 것은 불가능합니다. 만에 하나 다른 네트워크 인터페이스를 사용하기 위해서 유저가 비밀키를 알아내서 설정 파일을 조작한다고 해도, 서버에서 각 네트워크 인터페이스에서 허용하는 Peer의 공개키의 리스트를 직접 관리하기 때문에, WireGuard 모듈 단에서 패킷이 차단되어 부정적인 사용이 불가능해집니다.&lt;/p&gt;
&lt;p&gt;네트워크 인터페이스를 나누었으니 각 인터페이스 별로 접근 가능한 VPC를 제한해야 합니다. 이는 리눅스에 기본적으로 존재하는 &lt;code&gt;iproute2&lt;/code&gt; 의 라우팅 테이블을 사용해서 구현합니다. 기본적으로 라우팅 테이블은 Destination IP를 보고, 라우팅 테이블에 대응되는 항목이 있다면, 그 항목에 표기된 곳으로 패킷을 보내주는 기능을 수행합니다.&lt;/p&gt;
&lt;p&gt;하지만 우리가 필요한 기능은 특정 대역으로 가는 패킷이 아니라면 &lt;strong&gt;패킷을 드랍&lt;/strong&gt;해야 하는 기능입니다. 기본적으로 &lt;code&gt;ip-route&lt;/code&gt; 는 &quot;목적지 기반&quot; 라우팅을 사용하는데, 이것과 다르게 우리는 그 여집합에 매치하여 패킷을 드랍하기를 원합니다. 이 행동을 구현하기 위해서 특별한 Route Type을 사용하는데요, 바로 &lt;code&gt;throw&lt;/code&gt; 라는 타입입니다. 이 타입을 가진 항목에 패킷이 매칭되게 되면, 마치 &lt;strong&gt;&quot;아무 항목에도 매치되지 않았음&quot;&lt;/strong&gt; 이라는 효과를 내게 합니다. 즉, &lt;code&gt;throw&lt;/code&gt; 타입 라우트에 매치된 패킷은 곧바로 해당 라우팅 테이블에서 빠져나가고, 다음 규칙으로 이동하게 됩니다. 프로그래밍 언어로 표현하자면 마치 &lt;code&gt;break&lt;/code&gt; 문과 비슷한 효과를 내게 됩니다.&lt;/p&gt;
&lt;p&gt;따라서 라우팅 테이블에 허용할 VPC 대역에 대해 모두 &lt;code&gt;throw&lt;/code&gt; 타입을 사용하고, 마지막으로 디폴트 라우트 엔트리의 타입으로 모든 패킷을 즉시 drop해버리는 &lt;code&gt;blackhole&lt;/code&gt; 타입을 사용하게 되면, &lt;strong&gt;&quot;특정&quot; VPC 대역&lt;/strong&gt;들만 허용하는 라우팅 규칙을 만들 수 있습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1125px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 36.94444444444444%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAIAAACHqfpvAAAACXBIWXMAAAsTAAALEwEAmpwYAAABYElEQVR42lWQy0rDQBSG+1LufBE3iriSLqpIVVBqK4iiexERBF+iupLqQhBvtZeYJmYSTZs2kzaZyW0mt3FSpOg/w+HAx8c5MwX2J1EUeZaORgANVdfUwsCboYyxwHXwoGfr/IqcxnFcSFI3oKIXCoNRG5rQer4AN5VevWo0DjFUs18xSxlDQxHe7cvXVbm+M367IjQq+NGtYs9L1pyBaoYx/jBPH9XdB3mvOTywXS0Xp3KWMBOLT7ByDyoNsC1MLinhkzOXJhJJxCQd+wGpwZOlz+KKWFodlGWscJGfvKSsiYTF4eZyr7wglY7gecLl2ZN4giA4tkbFL7D2KW0YfcXFGY0IpQ5CyIICwuUR3ALqOtDOrHFMSCFfiaUpS7js+4Hyrb92ui+ttqRqE9sxTVOexjAMXdcVAN5brU63q/f7ZCr/+23f81yEsOPwhoQhpZSvE4ZhjvwcYk4x5m0cJz9qpHQqBzCY0gAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/345456bd952d8bdf57cd126c729da02c/33ecc/2-packet-filter.webp 360w,
/static/345456bd952d8bdf57cd126c729da02c/263a4/2-packet-filter.webp 480w,
/static/345456bd952d8bdf57cd126c729da02c/0c8fb/2-packet-filter.webp 640w,
/static/345456bd952d8bdf57cd126c729da02c/521f7/2-packet-filter.webp 860w,
/static/345456bd952d8bdf57cd126c729da02c/75745/2-packet-filter.webp 1125w&quot; sizes=&quot;(max-width: 1125px) 100vw, 1125px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/345456bd952d8bdf57cd126c729da02c/53918/2-packet-filter.png 360w,
/static/345456bd952d8bdf57cd126c729da02c/9aebd/2-packet-filter.png 480w,
/static/345456bd952d8bdf57cd126c729da02c/0f09e/2-packet-filter.png 640w,
/static/345456bd952d8bdf57cd126c729da02c/3e3fe/2-packet-filter.png 860w,
/static/345456bd952d8bdf57cd126c729da02c/b1bc2/2-packet-filter.png 1125w&quot; sizes=&quot;(max-width: 1125px) 100vw, 1125px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/345456bd952d8bdf57cd126c729da02c/b1bc2/2-packet-filter.png&quot; alt=&quot;2 packet filter&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;패킷이 허용될 때와 드랍될 때의 다이어그램&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;따라서 각 그룹 별로 네트워크 인터페이스를 할당하고, 각 인터페이스에 대응되는 라우팅 테이블을 위 전략으로 만들어주면, 그룹 별 VPC 대역 접근 제어 기능을 쉽게 만들 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;유저-별-고유한-ip로-통신하게-하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9C%A0%EC%A0%80-%EB%B3%84-%EA%B3%A0%EC%9C%A0%ED%95%9C-ip%EB%A1%9C-%ED%86%B5%EC%8B%A0%ED%95%98%EA%B2%8C-%ED%95%98%EA%B8%B0&quot; aria-label=&quot;유저 별 고유한 ip로 통신하게 하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;유저 별 고유한 IP로 통신하게 하기&lt;/h2&gt;
&lt;p&gt;위 OpenVPN 사용 시 단점으로 들었던 점 중 하나가, 사설망 내 자원들에 찍히는 IP 로그가 &lt;strong&gt;모두 VPN 서버의 IP&lt;/strong&gt;로 나와서 &lt;strong&gt;누가 접근했는지 알 수 없게 된다&lt;/strong&gt;는 것이었습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1060px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 45.55555555555556%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAIAAAC9o5sfAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA90lEQVR42pWQTW6EMAyFc6iqV6l6pN6j2zkEmg3rSiz4EQIGiUQUpiDiOAkh9YS2QuqinW9hRYnfe3aY9x6VM2bz/2MLOOeoMr/5Cz/11/Ph6YvfynVd67ouiqJpmnmemV1Hrh8EPFn7d7i1tqqqPM/TNOWcMwqY8LSYSC4oQU7TNAzDe+BnThegA4mTJInjOIoiymfHZbTWfd+XZUnebdvagA4gIgCQNSVTQ5ZlXdfdxHIxAGZfaRxHUpKrEEIFMEB6CJhvyJfGdrV44ddXpSyApL5dQH37RHssVbonzfEL6MM+uH4U6hnR+ju5jQ36DW25L36X+BMaagXkFeMUKwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/86862ba47eca969a4decdfc3a6bf9d54/33ecc/3-bad-vpn-server-log.webp 360w,
/static/86862ba47eca969a4decdfc3a6bf9d54/263a4/3-bad-vpn-server-log.webp 480w,
/static/86862ba47eca969a4decdfc3a6bf9d54/0c8fb/3-bad-vpn-server-log.webp 640w,
/static/86862ba47eca969a4decdfc3a6bf9d54/521f7/3-bad-vpn-server-log.webp 860w,
/static/86862ba47eca969a4decdfc3a6bf9d54/668f0/3-bad-vpn-server-log.webp 1060w&quot; sizes=&quot;(max-width: 1060px) 100vw, 1060px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/86862ba47eca969a4decdfc3a6bf9d54/53918/3-bad-vpn-server-log.png 360w,
/static/86862ba47eca969a4decdfc3a6bf9d54/9aebd/3-bad-vpn-server-log.png 480w,
/static/86862ba47eca969a4decdfc3a6bf9d54/0f09e/3-bad-vpn-server-log.png 640w,
/static/86862ba47eca969a4decdfc3a6bf9d54/3e3fe/3-bad-vpn-server-log.png 860w,
/static/86862ba47eca969a4decdfc3a6bf9d54/ea60f/3-bad-vpn-server-log.png 1060w&quot; sizes=&quot;(max-width: 1060px) 100vw, 1060px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/86862ba47eca969a4decdfc3a6bf9d54/ea60f/3-bad-vpn-server-log.png&quot; alt=&quot;3 bad vpn server log&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;OpenVPN을 NAT로 사용했을 때 찍히는 접근 로그. 모두 VPN 서버의 아이피가 찍혀서 의미가 없음.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;WireGuard에서는 각 Peer의 통신에 사용할 &lt;strong&gt;IP 주소를 각자 설정&lt;/strong&gt;할 수 있고, 더 나아가 특정 Peer에 대해 허용할 IP를 설정해줄 수 있습니다. 이 기능을 사용하면 WireGuard가 패킷을 받아서 다시 외부로 내보낼 때(포워딩), Source IP로 유저의 고유한 아이피를 달아서 보내줄 수 있습니다. 다만 이런 식으로 유저의 고유한 아이피로 찍히게 만들고 싶다면 VPN 서버에서 외부로 패킷을 송신할 때 &lt;strong&gt;NAT를 수행하면 안 된다&lt;/strong&gt;는 의미입니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1053px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 45.55555555555556%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAIAAAC9o5sfAAAACXBIWXMAAAsTAAALEwEAmpwYAAABBUlEQVR42pVRy2qEMBTNj3TVfy0MdNXP6L7L2bjsTigoIuJjGBW1Gs37mrRXQ6XQMrRnEcI9jxxyCYCVYnXOffwHXk8WdimvLwD6GLkdv3qWZcnzvCzLtm211mTSj1d1v6hgZ+3tp8ZxTJIE/U3TCCGIgNdBPTBRCa6F4PM8U0qHYVBK/TR3XRcEQRiGeNnMB22MweCqqoqiSNMUI/xQ70CplLKu6yiKUND3PeecgLGMGWu3wkhnWRbHMZ5YQexAP7ZAijGGp89CMwoIZW/p5Umqd86RkvglvjkqMA4A1BcwaF1XXxMFm3nSp9rcTewM5taHHVv4vguiIaPy2TrmBX/fs3PuE8daBau5exbcAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/b456a6bd5ac31e69b440795574a7ce51/33ecc/4-good-vpn-server-log.webp 360w,
/static/b456a6bd5ac31e69b440795574a7ce51/263a4/4-good-vpn-server-log.webp 480w,
/static/b456a6bd5ac31e69b440795574a7ce51/0c8fb/4-good-vpn-server-log.webp 640w,
/static/b456a6bd5ac31e69b440795574a7ce51/521f7/4-good-vpn-server-log.webp 860w,
/static/b456a6bd5ac31e69b440795574a7ce51/f8f2d/4-good-vpn-server-log.webp 1053w&quot; sizes=&quot;(max-width: 1053px) 100vw, 1053px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/b456a6bd5ac31e69b440795574a7ce51/53918/4-good-vpn-server-log.png 360w,
/static/b456a6bd5ac31e69b440795574a7ce51/9aebd/4-good-vpn-server-log.png 480w,
/static/b456a6bd5ac31e69b440795574a7ce51/0f09e/4-good-vpn-server-log.png 640w,
/static/b456a6bd5ac31e69b440795574a7ce51/3e3fe/4-good-vpn-server-log.png 860w,
/static/b456a6bd5ac31e69b440795574a7ce51/c2a07/4-good-vpn-server-log.png 1053w&quot; sizes=&quot;(max-width: 1053px) 100vw, 1053px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/b456a6bd5ac31e69b440795574a7ce51/c2a07/4-good-vpn-server-log.png&quot; alt=&quot;4 good vpn server log&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;NAT를 사용하지 않는 경우 유저별로 고유한 아이피가 남는 모습. 이상적인 상황.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;위 그림처럼 WireGuard 서버가 Source 아이피가 본인의 아이피가 아닌 &quot;유저 고유의 아이피&quot;인 패킷을 송신할 수 있다면 사설망 자원 접근 로그에 유저를 특정할 수 있는 유의미한 로그가 남게 되어 유용할 것입니다.&lt;/p&gt;
&lt;p&gt;하지만 여기에 문제점이 두 가지가 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;AWS VPC에서는 Source/Dest IP Check라는 기능이 활성화되어 있어 패킷의 근원지와 목적지 아이피가 실제 인스턴스 아이피와 같은지에 대해서 체크하고, 만약 같지 않다면 조용히 패킷을 드랍하는 기능이 있습니다. 따라서 근원지가 &quot;유저 고유의 아이피&quot;인 패킷을 WireGuard 인스턴스가 보내게 되면, 패킷은 VPC 내부에서 조용히 사라지고 말 것입니다.&lt;/li&gt;
&lt;li&gt;NAT를 수행하지 않는다면 우선은 사설망 자원까지는 패킷이 잘 갈 수 있겠지만, &lt;strong&gt;반대로 VPN 서버로 다시 돌아오는 패킷은 유실&lt;/strong&gt;될 것입니다. 왜냐하면 VPN 서버의 주소가 아닌 유저 별 고유 아이피가 목적지인 패킷을 송신할 것이기 때문입니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1006px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 36.388888888888886%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAIAAACHqfpvAAAACXBIWXMAAAsTAAALEwEAmpwYAAABIUlEQVR42nVQPUvEQBDNDxF7f4Hg7xGxvFKwEBv/hFjZCxbaWNgc2IvEO7iDS3IJMRvN7WY/Z7+cxM/mHuwyO/PezM5L4h8CHtI9rsnlxlxp+zomfdyOxDlnjFFKCsEtxFUzSevdTOz09nro5uE/21qrtZZSKqVQmHDOi6LI83y5XFRV88Fv8vZ0/X7G1TOyXUAOOA9gdQiBUto0TVmWKEFhAgBZlqVpOpvNKd0AOCmcEl4YT+/v2HRqYjQqCO6tdQD4S4PD8cb5g5gQUo9gjGEBVwAtdYz90TG7OC/U0xu57foHsD2qUCOEkCOSwagQvh37CcaHD8aDql+K/XmzV6kD41ZYD8F573EZJCfbvRwaOc+ydlJ0h60+Abv5zX+5/Qn+tokbcKhbmgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/0e5693f720857d176230cb087d79bc4c/33ecc/5-response-dropped.webp 360w,
/static/0e5693f720857d176230cb087d79bc4c/263a4/5-response-dropped.webp 480w,
/static/0e5693f720857d176230cb087d79bc4c/0c8fb/5-response-dropped.webp 640w,
/static/0e5693f720857d176230cb087d79bc4c/521f7/5-response-dropped.webp 860w,
/static/0e5693f720857d176230cb087d79bc4c/b20d2/5-response-dropped.webp 1006w&quot; sizes=&quot;(max-width: 1006px) 100vw, 1006px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/0e5693f720857d176230cb087d79bc4c/53918/5-response-dropped.png 360w,
/static/0e5693f720857d176230cb087d79bc4c/9aebd/5-response-dropped.png 480w,
/static/0e5693f720857d176230cb087d79bc4c/0f09e/5-response-dropped.png 640w,
/static/0e5693f720857d176230cb087d79bc4c/3e3fe/5-response-dropped.png 860w,
/static/0e5693f720857d176230cb087d79bc4c/5cfe2/5-response-dropped.png 1006w&quot; sizes=&quot;(max-width: 1006px) 100vw, 1006px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/0e5693f720857d176230cb087d79bc4c/5cfe2/5-response-dropped.png&quot; alt=&quot;5 response dropped&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;돌아오는 패킷은 목적지를 찾지 못하고 유실될 것입니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;첫 번째 문제점은 비교적 쉽게 해결이 됩니다. AWS에서는 해당 기능을 기본적으로는 활성화해두지만, 유저가 직접 개별 인스턴스에 대해서 기능을 비활성화할 수 있기 때문입니다. 따라서 WireGuard 인스턴스에 대해서 Source / Destination 체크 기능을 비활성화하면 쉽게 문제가 해결됩니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 882px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 43.611111111111114%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAIAAAC9o5sfAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA80lEQVR42pWO606EMBCF+/5vtzFZWVijCCJber8BbSlOARM38Q8nX05nTmeaomtxK8v75fJyfS2K4vZWN9y40Tmb5azL2t1toTuurPczIpTigUiltbHaGDhVrk12bTZyu7uEdJuBZp49MtaGENZNaX1S+vX03B5hSkgIMYFmH2LKwXpCiHEOn1B2nOKpxW2ZUKaUkkJIxpjUYzjxBGow13MwYbE+42KalvUvLqR/gWFUDaZq+rJ9FPV31T7uX/j22QNlA0kHxXtPayw+MAeg2Om4wW5Bg/XNIFoid28poBoioO2YBqfOi2nhY+TgB1HMCxnjDziOBkpMwPG0AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/952f1c9f8382defeaf7533260c98f679/33ecc/6-aws-source-dest-check.webp 360w,
/static/952f1c9f8382defeaf7533260c98f679/263a4/6-aws-source-dest-check.webp 480w,
/static/952f1c9f8382defeaf7533260c98f679/0c8fb/6-aws-source-dest-check.webp 640w,
/static/952f1c9f8382defeaf7533260c98f679/521f7/6-aws-source-dest-check.webp 860w,
/static/952f1c9f8382defeaf7533260c98f679/c340f/6-aws-source-dest-check.webp 882w&quot; sizes=&quot;(max-width: 882px) 100vw, 882px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/952f1c9f8382defeaf7533260c98f679/53918/6-aws-source-dest-check.png 360w,
/static/952f1c9f8382defeaf7533260c98f679/9aebd/6-aws-source-dest-check.png 480w,
/static/952f1c9f8382defeaf7533260c98f679/0f09e/6-aws-source-dest-check.png 640w,
/static/952f1c9f8382defeaf7533260c98f679/3e3fe/6-aws-source-dest-check.png 860w,
/static/952f1c9f8382defeaf7533260c98f679/5f5e5/6-aws-source-dest-check.png 882w&quot; sizes=&quot;(max-width: 882px) 100vw, 882px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/952f1c9f8382defeaf7533260c98f679/5f5e5/6-aws-source-dest-check.png&quot; alt=&quot;6 aws source dest check&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;AWS에서는 Source/Destination check 사용 여부를 유저가 직접 결정할 수 있습니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;두 번째 문제점은 쉽게 해결되는 문제는 아닙니다. 왜냐하면 인스턴스는 한 대이지만 유저는 여러 명일 수 있으므로, 여러 개의 유저 아이피가 목적지인 패킷에 대해서 인스턴스로 패킷을 라우팅해야하기 때문입니다. 다행히도, AWS 네트워크 스택은 충분히 유연해서, 임의의 아이피 대역에 대해 유저가 원하는 대상으로 라우팅을 설정할 수 있습니다. 여기에선 특정 대역을 VPN 서버 인스턴스의 ENI로 직접 보내주는 라우트를 만들면 됩니다.&lt;/p&gt;
&lt;p&gt;따라서, 우선 유저 아이피를 할당할 아이피 대역을 하나 잡아두고, AWS의 다양한 네트워킹 스택을 사용해서 문제를 풀어나가려고 합니다. 주의할 점은 이 아이피 대역은 그 어떤 VPC 대역과도 겹치지 않는 대역이어야 합니다. 만약 겹치게 된다면, 그 겹치는 주소로 패킷이 송신될 때 어느 VPC 쪽으로 패킷이 가야할지 모르기 때문에, 예상치 못한 결과가 나올 수 있습니다. 이렇게 겹치지 않는 고유한 유저 아이피 대역을 하나 생각해두었다면, 통신을 할 수 있도록 하려면 어떤 작업이 필요한지 생각해봅니다.&lt;/p&gt;
&lt;p&gt;AWS VPC는 특별하게 설정을 해주지 않았다면 외부와는 완전히 차단된 네트워크 망이 됩니다. 여기서 우리는 인터넷과 통신하기 위해서 Internet Gateway 혹은 NAT Gateway 등의 컴포넌트를 이용하고, VPC 끼리 통신하기 위해서 &lt;strong&gt;VPC Peering&lt;/strong&gt;이라는 기법을 사용합니다. 데브시스터즈에서 이미 사용하고 있는 VPC가 매우 많고, 서로 통신이 필요한 VPC끼리는 Peering Connection이 이미 맺어져있어 이것을 사용하고 있는데요, 우리의 VPN 서버도 이 Peering Connection을 사용해서 다른 VPC와 패킷을 주고받으면 좋을 것 같습니다.&lt;/p&gt;
&lt;p&gt;추가적으로 통신을 원하는 VPC의 아이피 대역을 Peering Connection으로 보내주어야 하므로, AWS VPC에서 제공하는 Route Table이라는 컴포넌트를 사용할 것입니다. 이 컴포넌트는 VPC 내부에 흐르는 트래픽에 대한 라우팅 규칙을 만들수 있는 기능으로, 리눅스의 라우팅 테이블과 매우 유사한 작동 방식을 가집니다.&lt;/p&gt;
&lt;p&gt;따라서 다음 컴포넌트들이 있으면, VPN 서버에서 대상 VPC로의 단방향 통신이 잘 될 것으로 예상할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VPN 서버의 VPC → 통신을 원하는 VPC의 Peering Connection&lt;/li&gt;
&lt;li&gt;위 Peering Connection에 대한 Route Table Entry&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;하지만 되돌아오는 경로는 어떻게 할까요? 아까 말했듯이 NAT가 되지 않은 패킷이므로, 이제 목적지 IP 주소가 VPN 서버의 주소가 아닌 그 어디에도 존재하지 않는 유저 개별 고유 아이피를 가지게 됩니다. 특별한 처리 없이는 이 패킷을 다시 VPN 서버로 오게 만들 수 없을 것 같군요.&lt;/p&gt;
&lt;p&gt;그렇다면 원격 VPC의 Route Table에서 유저 고유 아이피 대역에 대한 라우팅 테이블 엔트리가 필요할 것 같습니다. 우리가 설정한 고유 유저 아이피 대역을 곧바로 VPN 서버가 있는 VPC의 Peering Connection으로 다시 보내도록 라우팅 테이블 항목을 만들면 되지 않을까요?&lt;/p&gt;
&lt;p&gt;안타깝게도 다시 되돌아오는 경로는 &lt;strong&gt;VPC Peering Connection의 한계에 의해서 불가능&lt;/strong&gt;합니다. VPC Peering Connection은 기본적으로 &lt;strong&gt;&quot;목적지&quot; 주소가 해당 VPC 대역과 불일치하게 되면&lt;/strong&gt; 조용히 패킷을 드랍하는 행동을 보입니다. 유저 고유 아이피 대역은 WireGuard 서버가 있는 VPC 대역과 불일치하므로 되돌아오는 패킷을 Peering Connection으로 보내는 것은 불가능해보입니다.&lt;/p&gt;
&lt;p&gt;여기서 또 다른 컴포넌트를 사용하게 되는데요, 바로 &lt;strong&gt;Transit Gateway&lt;/strong&gt;입니다. Transit Gateway는 기존 Peering Connection을 사용할 때 불편한 점들이 많았는데, 그런 불편한 점들을 개선해주는 매우 유용한 도구입니다. VPC Peering은 1:1의 관계로 정의되기 때문에 VPC가 &lt;code&gt;N&lt;/code&gt;개가 있다면, 모든 VPC가 서로 통신할 수 있게 하려면 &lt;code&gt;N * (N-1) / 2&lt;/code&gt; 개의 Peering Connection이 필요합니다. 관리가 너무 힘들어집니다. 하지만 Transit Gateway는 마치 중간에서 허브처럼 작동해서, 각 VPC를 Transit Gateway와 연결해두고 적절히 라우팅 테이블만 만져주면 VPC 끼리 통신을 하게 만들 수 있습니다. 필요한 커넥션의 개수가 확연하게 줄어들면서 동시에 관리 코스트가 줄어들게 됩니다. 멋지네요.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1185px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 49.44444444444444%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAABhUlEQVR42mNgwALq6+uZgJiNAQcIXbWKGSjPwkAsmDhxG3vflFnaXVOmSECFGJHlp6xaxdM1ZZZB/cSJfNjksQGW8Lhc5cKqFun6/ftRXLJq1So2+9BQnrisAu3c3HrcBv7//58RqJgZxM7NzWVPS0vjArFLOzt5C3t7ORkYjFl7gXT9lCk8UAMYgd7mmDlzJiu6WWDTe6dPl54wdaZp7sSJ7N0Tp2v1z5xrDvL6qlXHOK1c/KQU9S3MLF0Dxebv388BMnT+/P0c/bNmGXRPmqUIdREjioGhibmiUQnZuiB2IpAdHZ0rA/OiU0SyuIK2tb5PZJrImTNnWEG+sbe3Z/EMT1GzdQ+UxBpoWlpabEJCKuAw0dNz5bawCBUCeQcUAcb2PiJKOvYm+v7+AlOmrOIBeTU+Pp5DScmYX05XVzCUIZQZb4yAXDVx7lytjgkT5NCDBuS6/v5+AZB3JxIRyzAJ1tCYLJWM4noxNHEwLS9vzxGbma8ZnZtLdLIBKWDCI8+ESx4Ayc9+F4RrFQEAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/9c6dfcedc662c4aaebb6404e9b17b99e/33ecc/7-aws-transit-gateway.webp 360w,
/static/9c6dfcedc662c4aaebb6404e9b17b99e/263a4/7-aws-transit-gateway.webp 480w,
/static/9c6dfcedc662c4aaebb6404e9b17b99e/0c8fb/7-aws-transit-gateway.webp 640w,
/static/9c6dfcedc662c4aaebb6404e9b17b99e/521f7/7-aws-transit-gateway.webp 860w,
/static/9c6dfcedc662c4aaebb6404e9b17b99e/8d239/7-aws-transit-gateway.webp 1185w&quot; sizes=&quot;(max-width: 1185px) 100vw, 1185px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/9c6dfcedc662c4aaebb6404e9b17b99e/53918/7-aws-transit-gateway.png 360w,
/static/9c6dfcedc662c4aaebb6404e9b17b99e/9aebd/7-aws-transit-gateway.png 480w,
/static/9c6dfcedc662c4aaebb6404e9b17b99e/0f09e/7-aws-transit-gateway.png 640w,
/static/9c6dfcedc662c4aaebb6404e9b17b99e/3e3fe/7-aws-transit-gateway.png 860w,
/static/9c6dfcedc662c4aaebb6404e9b17b99e/4ee41/7-aws-transit-gateway.png 1185w&quot; sizes=&quot;(max-width: 1185px) 100vw, 1185px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/9c6dfcedc662c4aaebb6404e9b17b99e/4ee41/7-aws-transit-gateway.png&quot; alt=&quot;7 aws transit gateway&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;AWS Transit Gateway 다이어그램&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;그와 동시에 Peering Connection과는 달리, Source / Destination에 대한 특별한 제약사항이 없기 때문에, 우리가 원하는 양상의 라우팅을 가능하게 해 줍니다. 따라서 다시 &lt;strong&gt;&quot;되돌아 오는&quot;&lt;/strong&gt; 길은 Transit Gateway를 통해서 라우팅해주면 오고 가는 길 모두 완성해줄 수 있을 것 같습니다. 정리하자면,&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;VPN 서버의 VPC → 대상 VPC (Peering Connection)&lt;/li&gt;
&lt;li&gt;대상 VPC → 내부 자원 (Local Route)&lt;/li&gt;
&lt;li&gt;내부 자원 → Transit Gateway (Route Table)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transit Gateway → VPN 서버 VPC (Transit Gateway Route Table)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;VPN 서버 VPC → VPN 서버 (Route Table)&lt;/li&gt;
&lt;/ol&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 960px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 64.16666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAIAAAAmMtkJAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB3klEQVR42n1Ry27TQBTNRyERYNcFC/6DPb/ABqms2lVXfAVCVQUqCNSAcFuqEpSKyJKTKn7Er3g8L4/nwbEdWioB19ejO/acM+eeO6KUFkWhtVZKtX2gcNY6d5P/jFHTh5TyZgUYS0UlYV0yqShDUBCZPmwfHRhXCSGWy6Xv+0EQZFnWak2uL5LLN8n0KJkeprNjSqpNzUrCKWU4TAjBHcBvwYvFYj6fA5/neac++2mirzo+05HXhOeNakl4tZq8ojXBT84YwJAw+q1WFXkGbaghURuj7bZdaG2dE/67zeunRpJWG8E5WuvAsApkqJiQ6zQNwzCO4zRNkzhK14kFidHQYnWjSAQuGDr40oEHdrzN9QTE/e5O4ENnIR5tGFWEcLQGazjnowFprONvn6nwbBB6Ow2Ase0ppFpm1aSoZptNhekO4O1x6e3rTdBYVxBW1qKsZUE4l2pg580slk8KO07VY96c9qhBNjyBJZMX/NPz2jtI3u+uP7yMj3dRlCd7wttT8VzY80g8DMqdkI/r5hCE1urbnnUdmfjCrC9tNnXIvEubftfRqcVsnOfnDz5+G1/F94Q56lFb8P+js0C0PyKxs2L3V/wRb7/8IfuON3/NznWlF1J/VsYfRuCc+wV/QtxMj7iaqAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/83a608fcf05d7b6a694ed7282fb890ee/33ecc/8-example-route.webp 360w,
/static/83a608fcf05d7b6a694ed7282fb890ee/263a4/8-example-route.webp 480w,
/static/83a608fcf05d7b6a694ed7282fb890ee/0c8fb/8-example-route.webp 640w,
/static/83a608fcf05d7b6a694ed7282fb890ee/521f7/8-example-route.webp 860w,
/static/83a608fcf05d7b6a694ed7282fb890ee/a6361/8-example-route.webp 960w&quot; sizes=&quot;(max-width: 960px) 100vw, 960px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/83a608fcf05d7b6a694ed7282fb890ee/53918/8-example-route.png 360w,
/static/83a608fcf05d7b6a694ed7282fb890ee/9aebd/8-example-route.png 480w,
/static/83a608fcf05d7b6a694ed7282fb890ee/0f09e/8-example-route.png 640w,
/static/83a608fcf05d7b6a694ed7282fb890ee/3e3fe/8-example-route.png 860w,
/static/83a608fcf05d7b6a694ed7282fb890ee/a91f8/8-example-route.png 960w&quot; sizes=&quot;(max-width: 960px) 100vw, 960px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/83a608fcf05d7b6a694ed7282fb890ee/a91f8/8-example-route.png&quot; alt=&quot;8 example route&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;예시 라우팅 다이어그램&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;실제로 위 구성으로 테스트해보면 VPN 서버와 다른 VPC의 자원이 잘 통신이 되는 것을 볼 수 있습니다. 하지만 Transit Gateway도 분명히 한계가 있습니다. 그것은 바로 &lt;strong&gt;같은 AWS 리전 내에서만 구성할 수 있다&lt;/strong&gt;는 것입니다.&lt;/p&gt;
&lt;p&gt;데브시스터즈에서는 고가용성 혹은 지역성을 위해서 여러 리전에 걸쳐서 인프라를 구성하고 있습니다. 따라서 VPN 서버도 여러 리전과 통신을 해야할 필요가 있습니다. 따라서 위 네트워크 구성은 같은 리전 내에서는 잘 작동하지만, 리전을 넘어가서 다시 되돌아오는 패킷의 경로는 구성할 수 없습니다.&lt;/p&gt;
&lt;p&gt;여기서 Transit Gateway에 비교적 최근에 추가된, &lt;strong&gt;Transit Gateway Peering Connection&lt;/strong&gt;을 사용할 수 있습니다. VPC Peering과 비슷하게, Transit Gateway끼리의 Peering을 구성할 수 있는 기능인데요, 이것을 사용하면 각 리전의 Transit Gateway가 서로 통신할 수 있도록 만들 수 있으므로, 리전 간 통신도 잘 구성할 수 있게 됩니다.&lt;/p&gt;
&lt;p&gt;따라서, 리전을 넘어서 통신해야 하는 경우는 다음과 같은 흐름으로 구성하면 됩니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;VPN 서버의 VPC → 대상 VPC (Peering Connection)&lt;/li&gt;
&lt;li&gt;대상 VPC → 내부 자원 (Local Route)&lt;/li&gt;
&lt;li&gt;내부 자원 → Transit Gateway B (Route Table)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transit Gateway B → Transit Gateway A (Transit Gateway Peering Connection)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Transit Gateway A → VPN 서버 VPC (Transit Gateway Route Table)&lt;/li&gt;
&lt;li&gt;VPN 서버 VPC → VPN 서버 (Route Table)&lt;/li&gt;
&lt;/ol&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1069px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 55.833333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB1UlEQVR42mVSTW/TQBD1XwGpd4SIBD+JE1y49DegCo5FcOCAWg4RClx7o6iHqqUlTRonQU1DW8df66zXu97d2Q/GdotAPM1hvbNv3psnB0IIQoi5g9baWuv/Q9sE793flwEA1HVNKWUtpJT4SCuRJqt4FWEVJEMOtnJSCCFxumrRkFEHG2EYjkajn/O5VCCTKR9/YuMBGw3Y+HN11q8XX5XxRTSPL35IpTmvOOe3yug8zzIUKVmJI9G7d9CUVW1JBxKcr07es4MtXAnF0aBzLmhWwS8AyqosS6MoyhAky/IU291ueNBgdblSxcJ6xzlDvUa5y8BKqpJzYz0AzjEAePiTi2sDA2MdFporaZMx8gPfjjdkLo7fdC+trYTZ4/AFzA02dQPcBcPhXO0ldIeyaVni1tUt2ZW/6vN+J0Tlx8Q8Su2DXG2SXE3CyWQ6mc3itRgk8JC4Xq5edKTAWauNq+lqvf+SHr2rL44quzuNHw8ve7nelOlNOdzh4YCH3yq5O8t6p4sniXiG5CYw9FQQkqaY1TJejlWlSv1heLVxcnk/5s895qMKp9dee6b6Z9f3TpcbV/Rpl0Xw74/UuNHmWrht4V5L8/3usrkHG3P7lrtX0hx25N/1tmM7ahogLAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/f1cba2615127deed039c0c941cd02227/33ecc/9-inter-region-example-route.webp 360w,
/static/f1cba2615127deed039c0c941cd02227/263a4/9-inter-region-example-route.webp 480w,
/static/f1cba2615127deed039c0c941cd02227/0c8fb/9-inter-region-example-route.webp 640w,
/static/f1cba2615127deed039c0c941cd02227/521f7/9-inter-region-example-route.webp 860w,
/static/f1cba2615127deed039c0c941cd02227/f5086/9-inter-region-example-route.webp 1069w&quot; sizes=&quot;(max-width: 1069px) 100vw, 1069px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/f1cba2615127deed039c0c941cd02227/53918/9-inter-region-example-route.png 360w,
/static/f1cba2615127deed039c0c941cd02227/9aebd/9-inter-region-example-route.png 480w,
/static/f1cba2615127deed039c0c941cd02227/0f09e/9-inter-region-example-route.png 640w,
/static/f1cba2615127deed039c0c941cd02227/3e3fe/9-inter-region-example-route.png 860w,
/static/f1cba2615127deed039c0c941cd02227/28211/9-inter-region-example-route.png 1069w&quot; sizes=&quot;(max-width: 1069px) 100vw, 1069px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/f1cba2615127deed039c0c941cd02227/28211/9-inter-region-example-route.png&quot; alt=&quot;9 inter region example route&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;리전 간 통신의 경우에 대한 라우팅 다이어그램&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;복잡하지만 실제로 작동하는 네트워크 구성으로 보입니다. 이렇게 하면 실제로도 잘 작동할까요? 안타깝지만 여기서도 현실적인 문제에 부딪히게 됩니다. 같은 리전 내에서 실험을 해보면 아주 잘 작동하지만, &lt;strong&gt;리전을 넘어가는 상황(Inter-region)에서는 1 → 2 경로가 잘 작동하지 않습니다.&lt;/strong&gt; 안타깝지만 &quot;근원지(Source)&quot; IP가 해당 VPC의 대역에 포함되지 않는 경우에는 리전 간 Peering Connection 통신은 잘 지원이 안 되는 현상이 관찰됩니다. Undefined behaviour이거나 VPC Peering의 버그로 보입니다.&lt;/p&gt;
&lt;p&gt;그럼 어떻게 하면 될까요? 답은 간단합니다. 1 → 2를 Peering Connection을 사용하지 않고 똑같이 Transit Gateway를 사용하면 됩니다. 따라서 다음과 같은 경로가 완성됩니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;VPN 서버의 VPC → Transit Gateway A (Route Table)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transit Gateway A → Transit Gateway B (Transit Gateway Peering Connection)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Transit Gateway B → 대상 VPC (Transit Gateway Route Table)&lt;/li&gt;
&lt;li&gt;대상 VPC → 내부 자원 (Local Route)&lt;/li&gt;
&lt;li&gt;내부 자원 → Transit Gateway B (Route Table)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transit Gateway B → Transit Gateway A (Transit Gateway Peering Connection)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Transit Gateway A → VPN 서버 VPC (Transit Gateway Route Table)&lt;/li&gt;
&lt;li&gt;VPN 서버 VPC → VPN 서버 (Route Table)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이렇게 네트워크 경로를 구성해주면 리전 내부든 리전 간이든 모두 커버할 수 있게 됩니다. 실제로 사설망 리소스에 찍히는 IP 로그를 보게 되면, 각 유저별로 고유한 아이피가 찍혀서 나오는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 903px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 75.55555555555556%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAIAAABr+ngCAAAACXBIWXMAAAsTAAALEwEAmpwYAAABrklEQVR42o1TPU8bQRC9fwRSQIqE0qZCNJFSpEuR30DJf6HIP0hBS4OEghMXToiNUQK28fk4bte7O/s5e5nbTYEQnPN0Gt3q5s28ebNXtBuAbRu6GCMdFIDW2ntPMYRQtG1MGTHnxoRnyxi+0CCNMUopxhi9FIjRu+ezqaDxY+m+GPyp2QLKiXVdT6JRdM4VXA1m1TGioX5CrOu6bpqGsSaEaB1fwvvSvVrAW+vmVI0SAIBoFEl8UcHRXO/y+3O7mi5upxeDwXA4nIx/yeWVrC6u64PT79uj2RvFzsXsEiTTSTbn3FpbOL8CO8pzkh6ZQJ/JB0R103wazV9PV+8CljFQgoYEIURH7vc64AOsTzFWT0zMzTq3Y7eMFxBR3P7I6f+eR+jpnAbxYyY+K3dGO6EjWZU37BOKHiYZXsLHe9wu3R5fD0x9p3iV7wkZRmP3kanbTfPh5Gzr2/WONl8tZ0Y2ac0m29ZnWMR2KQ4n1e7vh33j/yQtnVqSjXS3YtzgNkbAOIvt2ikGqzEJpoZUguLmVT2ewrK5lkyqTnC+DsV/0NJvk5ZEmmng7Lkx5i/CQly/fKnwWgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/a154e31a8a9fcf5734a8847bdd64673c/33ecc/10-final-route-diagram.webp 360w,
/static/a154e31a8a9fcf5734a8847bdd64673c/263a4/10-final-route-diagram.webp 480w,
/static/a154e31a8a9fcf5734a8847bdd64673c/0c8fb/10-final-route-diagram.webp 640w,
/static/a154e31a8a9fcf5734a8847bdd64673c/521f7/10-final-route-diagram.webp 860w,
/static/a154e31a8a9fcf5734a8847bdd64673c/b3a9a/10-final-route-diagram.webp 903w&quot; sizes=&quot;(max-width: 903px) 100vw, 903px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/a154e31a8a9fcf5734a8847bdd64673c/53918/10-final-route-diagram.png 360w,
/static/a154e31a8a9fcf5734a8847bdd64673c/9aebd/10-final-route-diagram.png 480w,
/static/a154e31a8a9fcf5734a8847bdd64673c/0f09e/10-final-route-diagram.png 640w,
/static/a154e31a8a9fcf5734a8847bdd64673c/3e3fe/10-final-route-diagram.png 860w,
/static/a154e31a8a9fcf5734a8847bdd64673c/8728b/10-final-route-diagram.png 903w&quot; sizes=&quot;(max-width: 903px) 100vw, 903px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/a154e31a8a9fcf5734a8847bdd64673c/8728b/10-final-route-diagram.png&quot; alt=&quot;10 final route diagram&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;간략화된 최종 라우팅 다이어그램&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;정리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%95%EB%A6%AC&quot; aria-label=&quot;정리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;정리&lt;/h2&gt;
&lt;p&gt;커널의 네트워킹 스택과 AWS의 네트워킹 스택을 잘 조합하여 다음과 같은 요구사항을 충족시켰습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;커널의 네트워킹 스택으로 &lt;strong&gt;그룹 별 접근 제어&lt;/strong&gt;를 구현함&lt;/li&gt;
&lt;li&gt;AWS 네트워크 스택을 잘 이용해서 &lt;strong&gt;유저 별 고유 IP&lt;/strong&gt;로 액세스 로그가 남을 수 있도록 함&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 글은 2부에서 이어집니다. 2부에서는 유저 인증, eBPF, 그리고 eBPF를 이용한 편의 기능과 보안 기능을 다룹니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[WireGuard로 멋진 VPN 서버 구축하기 - 2]]></title><description><![CDATA[eBPF를 이용한 패킷 필터링과 SSO 연동을 통한 자체 WireGuard VPN의 보안과 부가기능을 개발한 경험을 소개합니다.]]></description><link>https://tech.devsisters.com/posts/wireguard-vpn-2</link><guid isPermaLink="false">https://tech.devsisters.com/posts/wireguard-vpn-2</guid><pubDate>Fri, 03 Sep 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, 저는 인프라셀에서 데브옵스 엔지니어로 일하고 있는 박재현이라고 합니다. 이 글은 앞선 1부의 글에서 이어지는 내용입니다. 1부에서는 WireGuard에 대한 설명과, 커널 네트워킹 스택, 그리고 AWS의 네트워킹 스택에 대한 내용을 다루었습니다.&lt;/p&gt;
&lt;p&gt;2부에서는 VPN 서버에서 사용하는 &lt;strong&gt;인증&lt;/strong&gt;과 &lt;strong&gt;eBPF를 이용한 패킷 필터링&lt;/strong&gt;, 그리고 &lt;strong&gt;eBPF를 이용한 유저 편의기능&lt;/strong&gt;을 소개합니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;img src=&quot;https://www.wireguard.com/img/wireguard.svg&quot;&gt;
    &lt;figcaption&gt;공식 WireGuard 로고&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;남은-요구사항&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%82%A8%EC%9D%80-%EC%9A%94%EA%B5%AC%EC%82%AC%ED%95%AD&quot; aria-label=&quot;남은 요구사항 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;남은 요구사항&lt;/h2&gt;
&lt;p&gt;앞서서 1부에서 다루었던 요구사항에 대한 해결 방법은 다음과 같았습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;그룹 별 접근 가능한 사설망 자원 접근 제어&lt;/li&gt;
&lt;li&gt;유저마다 고유한 아이피로 사설망에 접근&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그리고 이번 글에서는 다음과 같은 요구사항들을 다룹니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Keycloak SSO에 인증 연동&lt;/li&gt;
&lt;li&gt;키를 일정 시간만 사용 가능하게 하도록 하기, 활성화되지 않은 키에 대한 경고 알림 보내주기&lt;/li&gt;
&lt;li&gt;기타 보안 및 편의 기능, 관리자 UI&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;인증&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9D%B8%EC%A6%9D&quot; aria-label=&quot;인증 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;인증&lt;/h2&gt;
&lt;p&gt;기존에 사용하던 OpenVPN에서는 VPN 서버에 연결을 시작할 때 인증을 수행합니다. 반면, WireGuard는 기본적으로 그러한 인증 방식이 아닌 &lt;strong&gt;공개키 기반 인증&lt;/strong&gt;을 사용하므로, 서버와 클라이언트 측이 서로를 인지하고 있고(즉, Peer로서 추가가 되어있고), 비밀키를 각자 잘 가지고 있다면 &lt;strong&gt;별다른 유저 개입 없이 바로 연결을 할 수 있습니다.&lt;/strong&gt; 이는 편리함으로 다가올 수 있지만, 보안 허점으로 다가올 수 있습니다. 예를 들면, 키 파일이 유출되었을 때 제 3자가 별다른 인증 없이 바로 사설망에 접근할 수 있게 되고, 유출이 되었다는 사실을 곧바로 알아차리기가 힘듭니다. 그래서 저희의 구현체는 다음과 같은 흐름을 따르기로 했습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Keycloak SSO를 사용해서 &lt;strong&gt;Web UI에 로그인&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Web UI에서 유저의 디바이스를 &lt;strong&gt;&quot;활성화&quot;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;활성화된 디바이스는 &lt;strong&gt;일정 시간동안&lt;/strong&gt; 연결을 유지할 수 있음&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;위와 같은 흐름을 따르면, 키가 유출되었어도 Web UI에 로그인해서 활성화하지 못한다면 사설망에 접속할 수 없게 됩니다. 만약 활성화되어 있는 키라고 할지라도, 일정 시간이 지나면 만료되어 다시 활성화할 때까지 사용할 수 없기 때문에 키 유출에서부터 안전합니다. 그리고 Web UI에서 Keycloak SSO를 연동하는 것은 인증 프록시를 이용해서 매우 쉽고 안전하게 구현할 수 있습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 952px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 40.27777777777778%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAIAAAB2/0i6AAAACXBIWXMAAAsTAAALEwEAmpwYAAABOElEQVR42oVOy0oDQRCc6EWCF4NHQVDBb1IDEXwgEQSF6C949RtkTUxEPHiPuJuYq3gQcjMKQSKRZM3uzHTPtD27oEYEa+pQ01RVt6D9OTpcotICHS1TicUiHcxTQdD6xG8WMrSVpeIM7eaomKPtaYFrk7gzi4Upkxe4mcWNrMlncEX8zVX2f1PEfi1uXkZ+Lbr1dOtct6px4MlGWd9Vxtgs68BTwZnyPacdK8JaYhhrYw2UQAKCsfQfOCgM6JEKUSsZjSwCWdQyHrz3VTxiTQYIlGtvB+b6+PXi5unkSnZ7LguKN1vkZ60xJq1kobX++pJ1wnQesHH6Vr9/8eq6P0zGxoVBA1vDMARwlyulEHHsxKTItgNb3aNhNz2aITjGGXZLKVPBa1PxA4BsG/Tk8yNEH4jGTRA/AZslif0C+4DlAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/4f334a2773a433804ee1be5844821249/33ecc/1-web-ui.webp 360w,
/static/4f334a2773a433804ee1be5844821249/263a4/1-web-ui.webp 480w,
/static/4f334a2773a433804ee1be5844821249/0c8fb/1-web-ui.webp 640w,
/static/4f334a2773a433804ee1be5844821249/521f7/1-web-ui.webp 860w,
/static/4f334a2773a433804ee1be5844821249/493ca/1-web-ui.webp 952w&quot; sizes=&quot;(max-width: 952px) 100vw, 952px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/4f334a2773a433804ee1be5844821249/53918/1-web-ui.png 360w,
/static/4f334a2773a433804ee1be5844821249/9aebd/1-web-ui.png 480w,
/static/4f334a2773a433804ee1be5844821249/0f09e/1-web-ui.png 640w,
/static/4f334a2773a433804ee1be5844821249/3e3fe/1-web-ui.png 860w,
/static/4f334a2773a433804ee1be5844821249/be2fd/1-web-ui.png 952w&quot; sizes=&quot;(max-width: 952px) 100vw, 952px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/4f334a2773a433804ee1be5844821249/be2fd/1-web-ui.png&quot; alt=&quot;1 web ui&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;Web UI에서 디바이스를 활성화하고 관리하는 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;디바이스 단위로 키를 가지도록 나누어놓았기 때문에, 유저는 각자 사용하고 싶은 기기에서 설정 파일을 발급받아서 WireGuard 어플리케이션에서 임포트하여 사용할 수 있습니다. 따라서 여러 기기에서 사용하기 위해 설정 파일을 외부 저장장치에 가지고 다니거나 네트워크로 전송할 필요가 없습니다. 만약 디바이스를 분실하거나 키 파일이 유출되었을 경우, Web UI에 로그인하여 디바이스를 삭제해주기만 하면 간단하게 키를 무효화할 수 있기 때문에, 보안 사고에도 재빠르게 대응이 가능하게 됩니다.&lt;/p&gt;
&lt;h2 id=&quot;키-활성화-및-만료&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%82%A4-%ED%99%9C%EC%84%B1%ED%99%94-%EB%B0%8F-%EB%A7%8C%EB%A3%8C&quot; aria-label=&quot;키 활성화 및 만료 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;키 활성화 및 만료&lt;/h2&gt;
&lt;p&gt;위 인증 단락에서 디바이스를 &quot;활성화&quot; 한다는 표현을 사용했었는데요, 사실은 WireGuard에서는 따로 키를 &lt;strong&gt;&quot;활성화&quot;하는 개념이 존재하지 않습니다.&lt;/strong&gt; Peer로서 추가가 되어있고 공개키 기반으로 인증만 성공한다면 무조건 통신을 할 수 있습니다. 하지만 키 유출 등에 대비하기 위해서 유저가 직접 통신을 시작하고 싶다는 의사를 밝힌 후에 연결을 할 수 있게 하고, 일정 시간이 지나면 더 이상 연결이 불가능하도록 만들면 이상적일 것입니다.&lt;/p&gt;
&lt;p&gt;이 기능 구현의 핵심은 &lt;strong&gt;&quot;어떤 방식으로 활성화되지 않은 디바이스의 패킷을 차단할 것인가&quot;&lt;/strong&gt; 입니다. 이 목적을 달성할 방법은 여러 가지가 있습니다. 리눅스 라우팅 테이블에서 유저의 고유 아이피만 골라서 drop할 수도 있고, 서버 측 WireGuard Peer 리스트에서 대상 Peer을 제외해도 됩니다. 두 가지 방법 모두 효율적이게 패킷을 차단할 수 있지만, VPN 서버를 직접 구현하는 입장에서는 &lt;strong&gt;더 구현하기 편하고 효율적이며 확장성 있는 방법&lt;/strong&gt;을 사용하고 싶었습니다.&lt;/p&gt;
&lt;h2 id=&quot;ebpf&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#ebpf&quot; aria-label=&quot;ebpf permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;eBPF&lt;/h2&gt;
&lt;p&gt;그래서 &lt;a href=&quot;https://ebpf.io/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;eBPF&lt;/a&gt;라는 기술을 사용하기로 결정했습니다. eBPF라는 단어를 처음 들어보시는 분들이 많으실 것입니다. eBPF를 한 줄로 설명하기는 어렵지만, 간단하게 설명하자면, 리눅스 커널의 여러 부분에서 실행할 수 있는 &lt;strong&gt;샌드박스된 바이트코드&lt;/strong&gt;입니다. 원래라면 커널 공간에서 유저의 코드를 실행하는 것은 보안 상으로 매우 위험할 수 있는 데다가, 잘못하면 시스템이 크래시될 수 있기 때문에 금기시되는 행동 중 하나입니다. 하지만 eBPF의 바이트코드 인스트럭션은 샌드박스되어 &lt;strong&gt;안전한 실행을 보장&lt;/strong&gt;하고, 바이트코드가 로드 되기 전에 &lt;strong&gt;eBPF Verifier&lt;/strong&gt;라는 커널의 로직이 코드가 실행되어도 안전한지(out-of-bound read, infinite loop 등)를 검증하기 때문에 유저의 커스텀한 로직을 커널 공간에서 실행하기에 매우 적합한 수단입니다. 또한 eBPF 바이트코드는 간단한 인스트럭션 집합으로 구성되기 때문에, 커널에서 &lt;a href=&quot;https://en.wikipedia.org/wiki/Just-in-time_compilation&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;JIT&lt;/a&gt;을 통해 코드 실행을 가속화해주기도 합니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 850px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 34.72222222222222%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAAAsTAAALEwEAmpwYAAABaUlEQVR42m2R3SuDURzHzznP40HzoDaseRlZz4xcySQuUCtZMZFrSWyJm0VrthSzxHblVnGxJpsmyQU3a7nYPbfi1h+grKwd35OH5uXUp895vuec33l5COGcEr0ZjUYVmgFfmTAjPxvTc1o2TssnVJkURashpIEwtinJ8hGyZjD4e+I/339b9/jsg3c7VvJtxV/NVu0dEWeSdAe3ABNwgVoQAUmwB5yU0nM4DSYZY374EsSJ3eW5dUeTPJrJlHqdrQVLu8ZVVT3U9wuDfTACLkRBFBKbeeEsvArnQFovPke0NtLhGlYK02OkeBBxvIVTeT7v9R1j0IrrJ2A76MGiR3E6+BonWoOfwRUyD3wK58ASqKuPhdjT7gbhA/2W4nIiyxeCOy8YaGJMDsLrwI9FefhEnAwFA/qVPx+W0hTk/n5HQzVbCaxIZxNDJOSYWrzpGnXfI64gNlsl3AkU0Aj6dMvAXPYrRGYQvQ82/U8wKIEmrQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/566cdd5d7d629a5141e1bf89b5eb83f6/33ecc/2-ebpf.webp 360w,
/static/566cdd5d7d629a5141e1bf89b5eb83f6/263a4/2-ebpf.webp 480w,
/static/566cdd5d7d629a5141e1bf89b5eb83f6/0c8fb/2-ebpf.webp 640w,
/static/566cdd5d7d629a5141e1bf89b5eb83f6/16274/2-ebpf.webp 850w&quot; sizes=&quot;(max-width: 850px) 100vw, 850px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/566cdd5d7d629a5141e1bf89b5eb83f6/53918/2-ebpf.png 360w,
/static/566cdd5d7d629a5141e1bf89b5eb83f6/9aebd/2-ebpf.png 480w,
/static/566cdd5d7d629a5141e1bf89b5eb83f6/0f09e/2-ebpf.png 640w,
/static/566cdd5d7d629a5141e1bf89b5eb83f6/d6170/2-ebpf.png 850w&quot; sizes=&quot;(max-width: 850px) 100vw, 850px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/566cdd5d7d629a5141e1bf89b5eb83f6/d6170/2-ebpf.png&quot; alt=&quot;2 ebpf&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;eBPF의 로고&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;이러한 eBPF의 특성 때문에 여러 커널 구성요소가 유저의 eBPF 코드를 내부적으로 실행할 수 있도록 하는 기능을 내장하고 있습니다. 그 중 대표적으로 네트워크 스택의 많은 부분에서 eBPF 코드를 사용할 수 있도록 해주는데, 대표적인 것으로 &lt;code&gt;XDP&lt;/code&gt;와 &lt;code&gt;tc-bpf&lt;/code&gt;가 있습니다. &lt;code&gt;XDP&lt;/code&gt;는 커널에서 &lt;strong&gt;네트워크 버퍼를 할당하기 이전에 패킷에 대한 제어&lt;/strong&gt;를 할 수 있도록 해줍니다. 반면 &lt;code&gt;tc&lt;/code&gt;는 &lt;strong&gt;네트워크 인터페이스&lt;/strong&gt;의 Egress나 Ingress에서 패킷에 대한 제어를 할 수 있게 도와줍니다. 이러한 특성 때문에 &lt;code&gt;XDP&lt;/code&gt;는 &lt;code&gt;tc&lt;/code&gt;에 비해서 매우 빠르게 패킷을 eBPF 코드를 통해서 필터링하거나 조작할 수 있게 해줍니다.&lt;/p&gt;
&lt;p&gt;하지만 우리는 &lt;strong&gt;유저별로 패킷을 드랍하거나 허용&lt;/strong&gt;해주고 싶습니다. 우리가 유저를 이전에 고유한 IP로 통신을 할 수 있도록 해놓았으니 Source IP로 유저를 구분할 수 있을 것입니다. 하지만 이 정보는 외부에서 바로 들어온 패킷이 아닌 WireGuard에서 &lt;strong&gt;암호화된 패킷을 복호화한 뒤에 알 수 있는 특성&lt;/strong&gt;이므로, 아쉽게도 &lt;code&gt;XDP&lt;/code&gt; 단에서는 구분이 힘들 것입니다. 따라서 이전에 우리가 만들어둔 WireGuard 네트워크 인터페이스에서 &lt;code&gt;tc&lt;/code&gt;의 eBPF 기능을 사용해서 구분을 해야할 것으로 보입니다.&lt;/p&gt;
&lt;p&gt;여기서 &lt;code&gt;tc&lt;/code&gt;에 대한 설명을 간단하게 하자면, &lt;code&gt;tc&lt;/code&gt;는 &lt;code&gt;iproute2&lt;/code&gt;에 포함된 요소 중 하나로, 네트워크 인터페이스 단에서 트래픽에 대한 여러 가지 제어를 해줄 수 있도록 도와줍니다. 예를 들면, 네트워크 인터페이스로 들어오고 나가는 &lt;strong&gt;트래픽의 대역폭을 조절&lt;/strong&gt;하거나, 임의로 &lt;strong&gt;레이턴시를 조절&lt;/strong&gt;하거나, 특정 조건에 맞는 패킷에 대해서 &lt;strong&gt;drop, mirror 등의 액션을 취하거나&lt;/strong&gt; 하는 여러 역할을 수행할 수 있게 해줍니다. &lt;code&gt;tc&lt;/code&gt;의 구성 요소는 크게 세 가지로 나눌 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;qdisc&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;네트워크 인터페이스에 붙은 패킷 큐(Queue). 다양한 종류의 큐를 정의할 수 있습니다.&lt;/li&gt;
&lt;li&gt;상태가 없는 classless qdisc와 상태를 가지고 하위 class를 가질 수 있는 classful qdisc로 분류됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;class&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;트래픽 분류 기준을 만들어주는 요소.&lt;/li&gt;
&lt;li&gt;classful qdisc에 붙거나 class 아래에 붙을 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;filter&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;classful qdisc에서 패킷이 어떤 class 쪽으로 인입될지 결정하게 해주는 요소.&lt;/li&gt;
&lt;li&gt;간단하게 패킷 경로 분기점이라고 생각하면 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이러한 구성요소를 특성에 맞게 조합해서 원하는 효과를 내도록 만들 수 있습니다. 그 중 우리가 사용할 기능으로는, &lt;code&gt;tc&lt;/code&gt;에 eBPF 코드를 붙여서, eBPF가 패킷의 운명을 결정할 수 있도록 해주는 기능입니다. 따라서 &lt;code&gt;filter&lt;/code&gt;의 한 종류인 &lt;code&gt;tc-bpf&lt;/code&gt;를 사용합니다.&lt;/p&gt;
&lt;p&gt;원래 tc의 eBPF 코드는 패킷을 &lt;strong&gt;&quot;분류&quot;해주는 역할&lt;/strong&gt;이 주된 업무였습니다. 예를 들면, 특정 목적지로 가는 패킷은 대역폭이 작은 class로, 나머지는 대역폭이 큰 class로 보내는 등의 기능 정도를 수행할 수 있었습니다. 그래서 특정 패킷을 drop하거나 리다이렉션 해주는 등의 &lt;strong&gt;&quot;액션&quot;을 취하는 역할은 원래라면 지원이 되지 않았습니다.&lt;/strong&gt; 하지만 리눅스 커널 버전 4.1 버전부터 &lt;strong&gt;&lt;code&gt;direct-action&lt;/code&gt;&lt;/strong&gt; 이라는 플래그를 사용하여 eBPF 프로그램이 tc에서 액션을 취할 수 있게 되도록 패치되었고, 4.5 버전부터는 &lt;code&gt;clsact&lt;/code&gt; 라는 qdisc를 통해서 더 쉽게 &lt;code&gt;direct-action&lt;/code&gt; eBPF 프로그램을 붙일 수 있게 되었습니다. 따라서 저희의 VPN에서 사용할 eBPF 프로그램 또한 WireGuard 인터페이스에 &lt;code&gt;clsact&lt;/code&gt; qdisc를 추가한 뒤, eBPF 프로그램을 로드시킬 것입니다.&lt;/p&gt;
&lt;p&gt;eBPF 프로그램은 바이트코드이기 때문에, eBPF 타겟으로 컴파일해주는 도구가 필요합니다. 여러 가지 방법이 있지만, 그 중 가장 쉬운 방법은 &lt;strong&gt;LLVM 컴파일러의 BPF 백엔드&lt;/strong&gt;를 사용하는 것입니다. 이 기능은 LLVM 3.7 릴리즈부터 추가되었습니다. 즉, 프로그램을 LLVM으로 컴파일 할 수 있는 도구가 있다면, LLVM 컴파일러가 eBPF 바이트코드를 생성해줄 수 있다는 의미입니다. 따라서 다음과 같은 간단한 &lt;code&gt;tc&lt;/code&gt; eBPF 프로그램으로 실험을 해볼 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;c&quot;&gt;&lt;pre class=&quot;language-c&quot;&gt;&lt;code class=&quot;language-c&quot;&gt;&lt;span class=&quot;token macro property&quot;&gt;&lt;span class=&quot;token directive-hash&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;token directive keyword&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;lt;linux/ip.h&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token macro property&quot;&gt;&lt;span class=&quot;token directive-hash&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;token directive keyword&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;lt;linux/if_ether.h&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token macro property&quot;&gt;&lt;span class=&quot;token directive-hash&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;token directive keyword&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;lt;linux/pkt_cls.h&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token macro property&quot;&gt;&lt;span class=&quot;token directive-hash&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;token directive keyword&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;lt;linux/bpf.h&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token macro property&quot;&gt;&lt;span class=&quot;token directive-hash&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;token directive keyword&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;lt;bpf/bpf_helpers.h&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;SEC&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;classifier_dropper&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;dropper&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;__sk_buff&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;skb&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;data_end &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;unsigned&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;skb&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;data_end&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;data &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;unsigned&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;skb&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ethhdr&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;eth &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; ipsize &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;sizeof&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;eth&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    ipsize &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;sizeof&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;iphdr&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// drop malformed ip packet&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; ipsize &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; data_end&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; TC_ACT_SHOT&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; TC_ACT_OK&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;위 코드가 처음엔 난해해보일 수 있지만, 수행하는 기능은 매우 간단합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;함수 &lt;code&gt;dropper&lt;/code&gt;은 네트워크 버퍼인 &lt;code&gt;skb&lt;/code&gt; 를 인자로 받습니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;skb-&gt;data&lt;/code&gt; 에서부터 이더넷 헤더와 IP 헤더만큼의 사이즈만큼 양의 방향으로 이동한 포인터 값을 계산합니다.&lt;/li&gt;
&lt;li&gt;만약 2의 값이 데이터의 끝부분을 가리키는 &lt;code&gt;skb-&gt;data_end&lt;/code&gt; 보다 크다면, 처리할 수 없는 이상한 모양의 패킷이므로 &lt;code&gt;TC_ACT_SHOT&lt;/code&gt; 이라는 값을 리턴하여 &lt;strong&gt;이 패킷은 드랍해야 함&lt;/strong&gt;을 알려줍니다.&lt;/li&gt;
&lt;li&gt;만약 그렇지 않다면 &lt;code&gt;TC_ACT_OK&lt;/code&gt; 라는 값을 리턴하여 &lt;strong&gt;이 패킷은 허용함&lt;/strong&gt;을 알려줍니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;그리고 위 코드를 다음 명령어로 컴파일해봅니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;clang &lt;span class=&quot;token parameter variable&quot;&gt;-O2&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-g&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-Wall&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-Werror&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-target&lt;/span&gt; bpf &lt;span class=&quot;token parameter variable&quot;&gt;-c&lt;/span&gt; example.c &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; example.o&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그러면 &lt;code&gt;example.o&lt;/code&gt; 라는 오브젝트 파일이 결과물로 생성되는데, 이제 이 오브젝트 파일을 eBPF 프로그램으로서 &lt;code&gt;tc&lt;/code&gt; 에서 로드하여 쓰도록 할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;tc qdisc &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; dev eth0 clsact
tc filter &lt;span class=&quot;token function&quot;&gt;add&lt;/span&gt; dev eth0 ingress bpf direct-action obj example.o sec classifier_dropper&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;위 명령어를 수행했다면 &lt;code&gt;eth0&lt;/code&gt; 의 qdisc에서 들어오는 패킷 하나하나를 우리의 eBPF 프로그램에 통과시켜줄 것입니다.&lt;/p&gt;
&lt;p&gt;위 방식대로 eBPF 코드를 tc에서 사용할 수 있는 것을 검증했으니, 이제 다시 우리의 요구사항으로 돌아와봅시다. 우리가 원하는 것은 &lt;strong&gt;조건에 따라서 유저의 패킷을 드랍하거나 허용&lt;/strong&gt;해주는 것입니다. 하지만 위 예제 코드는 네트워크 버퍼의 모양만 보고 패킷의 운명을 결정해야 하기에 &lt;strong&gt;유저 공간 어플리케이션인 우리의 VPN 서버의 의사가 개입될 여지가 없습니다.&lt;/strong&gt; 이런 요구사항을 위해서 eBPF에는 특별한 자료구조가 하나 있는데, 그것이 바로 &lt;strong&gt;eBPF 맵&lt;/strong&gt;(Map)이라는 자료구조입니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 576px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 66.3888888888889%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAsTAAALEwEAmpwYAAABkElEQVR42o2TV4/CQAyE+f9/KzwgSkSTiADRey+il7n7LDmEg4ezZGU368x4xtmUfuP5fMa5XC41mUzsOZ1OtVqt5DXE7Xaz9/P5XIvFQuPxWIfDIa5JKRH3+12DwUC5XE7ZbFaNRkOz2ewN8HQ6qdls2nmhUFC9Xtd6vX4BwkgXsF2vV223W1t7DodD/SWlng673a4pOR6PL0AYR6OR+v2+HfABJDzJ/X5vABAhjXd8uNvt7N3j8bB6mmGdAqTT6ajdbpsfDujp+8vlos1mY3V0DlGyNgbUP8PtgJShsf8WJhlWEr8YCvIZhk8eZpREUaRWq2WK6DA5rBgQX8rlsiXTzWQyqtVqRuDDoivI8vm80um0giAwD78Coh8pFJAw+wBIvPPpMlmIUMF3XyUnN7Cdz2cD4d9CNntIkE2w5gwyQN2SN0Bvmy6KxWKcYRiqWq1aRwAT2IB8fOz1ekZK537+cVMoxHhuAzeFPQA+VfzllpRKJXtWKhUjdE8/fhuXkJwwRB74y+2gK7rzG+Ve/wAgS+NQNc6KIAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/2666844792a5182a88765317b97f5c56/33ecc/3-ebpf-map.webp 360w,
/static/2666844792a5182a88765317b97f5c56/263a4/3-ebpf-map.webp 480w,
/static/2666844792a5182a88765317b97f5c56/f1c97/3-ebpf-map.webp 576w&quot; sizes=&quot;(max-width: 576px) 100vw, 576px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/2666844792a5182a88765317b97f5c56/53918/3-ebpf-map.png 360w,
/static/2666844792a5182a88765317b97f5c56/9aebd/3-ebpf-map.png 480w,
/static/2666844792a5182a88765317b97f5c56/694da/3-ebpf-map.png 576w&quot; sizes=&quot;(max-width: 576px) 100vw, 576px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/2666844792a5182a88765317b97f5c56/694da/3-ebpf-map.png&quot; alt=&quot;3 ebpf map&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;eBPF Map&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;eBPF 맵은 유저 공간 어플리케이션과 커널 공간에서 실행되는 eBPF 프로그램이 &lt;strong&gt;서로 정보를 공유&lt;/strong&gt;할 수 있도록 해주는 특별한 커널 자료구조로, 유저 공간 어플리케이션이 맵을 할당한 뒤 eBPF 프로그램을 로드할 때 맵의 정보를 넘겨주어서 서로 같은 맵에서 통신할 수 있도록 해줍니다. 맵을 참조하기 위해서는 file descriptor을 사용하게 되고, 같은 file descriptor를 공유하면 같은 맵을 가리킬 수 있습니다. 이름에서 알 수 있듯이, eBPF 맵은 &lt;strong&gt;Key-Value로 자료를 저장하는 자료구조&lt;/strong&gt;입니다. 맵의 종류는 매우 다양하지만 이 글에서는 설명하지 않고, 가장 기본적인 형태의 해시맵인 &lt;code&gt;BPF_MAP_TYPE_HASH&lt;/code&gt; 를 사용할 것입니다.&lt;/p&gt;
&lt;p&gt;eBPF 맵을 만들 때에는 보통 유저 공간에서 &lt;code&gt;bpf&lt;/code&gt; syscall을 이용해서 만들게 됩니다. 하지만 직접 이렇게 system call을 호출해서 만들기보다는 보통 보조 도구들을 이용하게 됩니다. 다음과 같이 프로그램 코드에 우선 맵을 정의해줍니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;c&quot;&gt;&lt;pre class=&quot;language-c&quot;&gt;&lt;code class=&quot;language-c&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;bpf_map_def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;SEC&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;maps&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; allow_map &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;type &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; BPF_MAP_TYPE_HASH&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;key_size &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;sizeof&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;__u32&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value_size &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;sizeof&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;__u32&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;max_entries &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;65536&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;SEC(&quot;maps&quot;)&lt;/code&gt; 부분을 눈여겨보시길 바랍니다. 위처럼 맵 정의를 하고 컴파일을 하고 나면, ELF 섹션의 &lt;code&gt;maps&lt;/code&gt; 에 맵 정의를 포함시키게 됩니다. 그러면 나중에 유저 공간에서 이 eBPF 프로그램을 로딩할 때, 이 섹션의 맵 정의들을 모두 읽고 &lt;code&gt;bpf&lt;/code&gt; syscall을 통해 맵을 생성해서 맵의 file descriptor을 따로 저장하여 나중에 맵을 사용할 때 참조할 수 있게 됩니다. eBPF를 사용하기 쉽게 해 주는 보조 라이브러리들은 여러 가지가 있지만, 결론은 &lt;strong&gt;매우 쉽게 맵을 생성하고 사용할 수 있다는 의미&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;h1 id=&quot;ebpf로-키-만료-구현하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#ebpf%EB%A1%9C-%ED%82%A4-%EB%A7%8C%EB%A3%8C-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0&quot; aria-label=&quot;ebpf로 키 만료 구현하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;eBPF로 키 만료 구현하기&lt;/h1&gt;
&lt;p&gt;그래서 우리의 eBPF 프로그램에서는 맵을 이렇게 사용하기로 합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;allow_map&lt;/code&gt; 맵의 Key는 유저의 고유 IP를 사용하고, Value는 &lt;code&gt;1&lt;/code&gt; or &lt;code&gt;0&lt;/code&gt; 를 가집니다.&lt;/li&gt;
&lt;li&gt;eBPF 프로그램에서는 패킷의 Source IP를 보고 맵을 참조합니다.
&lt;ol&gt;
&lt;li&gt;만약 항목이 존재하지 않는다면, &lt;strong&gt;패킷을 드랍&lt;/strong&gt;합니다.&lt;/li&gt;
&lt;li&gt;만약 Value가 &lt;code&gt;1&lt;/code&gt; 이라면, &lt;strong&gt;패킷을 허용&lt;/strong&gt;합니다.&lt;/li&gt;
&lt;li&gt;만약 &lt;code&gt;0&lt;/code&gt;이거나 다른 값이라면(invalid state), 패킷을 드랍합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;그리고 우리의 VPN 서버에서는 다음과 같이 구현합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;유저가 키를 활성화하면, &lt;code&gt;allow_map&lt;/code&gt;의 항목을 &lt;code&gt;1&lt;/code&gt;로 바꿔줍니다.&lt;/li&gt;
&lt;li&gt;주기적으로 유저의 키가 활성화된 이후에 얼마나 시간이 흘렀는지 체크합니다.&lt;/li&gt;
&lt;li&gt;만약 일정 시간이 지났다면, &lt;code&gt;allow_map&lt;/code&gt; 의 항목을 &lt;code&gt;0&lt;/code&gt; 으로 바꿔줍니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;위 과정을 따르면 아주 쉽게 조건부로 패킷을 허용하거나 드랍할 수 있습니다. 다음 코드는 위 조건을 적용한 eBPF 프로그램 코드의 예시입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;c&quot;&gt;&lt;pre class=&quot;language-c&quot;&gt;&lt;code class=&quot;language-c&quot;&gt;&lt;span class=&quot;token macro property&quot;&gt;&lt;span class=&quot;token directive-hash&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;token directive keyword&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;lt;linux/ip.h&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token macro property&quot;&gt;&lt;span class=&quot;token directive-hash&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;token directive keyword&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;lt;linux/if_ether.h&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token macro property&quot;&gt;&lt;span class=&quot;token directive-hash&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;token directive keyword&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;lt;linux/filter.h&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token macro property&quot;&gt;&lt;span class=&quot;token directive-hash&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;token directive keyword&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;lt;linux/pkt_cls.h&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token macro property&quot;&gt;&lt;span class=&quot;token directive-hash&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;token directive keyword&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;lt;linux/bpf.h&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token macro property&quot;&gt;&lt;span class=&quot;token directive-hash&quot;&gt;#&lt;/span&gt;&lt;span class=&quot;token directive keyword&quot;&gt;include&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&amp;lt;bpf/bpf_helpers.h&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;bpf_map_def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;SEC&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;maps&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; allow_map &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;type &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; BPF_MAP_TYPE_HASH&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;key_size &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;sizeof&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;__u32&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// ipv4&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value_size &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;sizeof&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;__u32&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;max_entries &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;65536&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;unsigned&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;load_word&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;skb&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;unsigned&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt; off&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;asm&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;llvm.bpf.load.word&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;SEC&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;classifier_dropper&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;dropper&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;__sk_buff&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;skb&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;data_end &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;unsigned&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;skb&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;data_end&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;data &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;unsigned&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;long&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;skb&lt;span class=&quot;token operator&quot;&gt;-&gt;&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;ethhdr&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;eth &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;int&lt;/span&gt; ipsize &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;sizeof&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;eth&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    ipsize &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;sizeof&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;iphdr&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// drop malformed ip packet&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; ipsize &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; data_end&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; TC_ACT_SHOT&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    __u32 saddr &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;load_word&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;skb&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; BPF_NET_OFF &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;offsetof&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;iphdr&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; saddr&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    __u32 &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;allow_val&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// read `allow_map` to figure out if source address is allowed&lt;/span&gt;
    allow_val &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;bpf_map_lookup_elem&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;allow_map&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;saddr&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;allow_val&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// key not found, so drop packet&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; TC_ACT_SHOT&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;allow_val &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// value is 1, so allow packet&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; TC_ACT_OK&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// otherwise, drop packet&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; TC_ACT_SHOT&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그리고 &lt;strong&gt;VPN 서버 측에서 맵을 적절히 잘 업데이트해주는 로직&lt;/strong&gt;을 넣어주면, 키 활성화 / 만료 로직을 쉽게 구현할 수 있습니다. 이렇게 하여 유연하면서도 고성능의 패킷 필터링 로직을 쉽게 VPN 서버에 통합시킬 수 있었습니다.&lt;/p&gt;
&lt;h2 id=&quot;활성화되지-않은-연결-경고&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%99%9C%EC%84%B1%ED%99%94%EB%90%98%EC%A7%80-%EC%95%8A%EC%9D%80-%EC%97%B0%EA%B2%B0-%EA%B2%BD%EA%B3%A0&quot; aria-label=&quot;활성화되지 않은 연결 경고 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;활성화되지 않은 연결 경고&lt;/h2&gt;
&lt;p&gt;OpenVPN은 연결을 활성화할 때마다 아이디와 패스워드를 입력해서 인증을 합니다. &lt;strong&gt;따라서 유저는 인증을 했다면 당연히 연결이 활성화되었을 것이라고 생각합니다.&lt;/strong&gt; 이는 OpenVPN을 사용할 때에는 맞지만, 우리가 만든 WireGuard VPN 솔루션에서는 적용되지 않습니다. 왜냐하면 우리의 접근 방식은 다음과 같기 때문입니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;WireGuard Peer 인증에 필요한 키는 유저 개개인이 가지고 있는다.&lt;/li&gt;
&lt;li&gt;연결은 되지만 &lt;strong&gt;&quot;키 만료&quot; 상태라면 패킷은 드랍&lt;/strong&gt;한다.&lt;/li&gt;
&lt;li&gt;키를 활성화하기 위해서는 Web UI에서 Keycloak SSO로 인증을 하는 과정을 거쳐야한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;따라서 WireGuard 앱에서 연결을 활성화하고 연결이 바로 되는 모습을 보고 사설망에 접근할 수 있다고 생각할 수 있지만, &lt;strong&gt;Web UI에서 키를 활성화한 상태가 아니라면 패킷은 조용히 드랍되고, 유저는 통신이 되지 않는다는 사실에 충격과 혼란에 빠지게 될 것입니다.&lt;/strong&gt; 따라서 키가 활성화된 상태가 아닌데 연결을 해서 패킷이 드랍되고 있다는 사실을 유저에게 알려주어야 유저가 불편함을 느끼지 않을 것입니다.&lt;/p&gt;
&lt;p&gt;척 보기에는 꽤 힘든 작업이라고 생각되지만, 위에 만들어둔 eBPF 프로그램을 이용하면 단숨에 해결할 수 있습니다. 왜냐하면 패킷을 드랍하는 주체는 &lt;code&gt;tc&lt;/code&gt;에 붙은 eBPF 프로그램이기 때문에, &lt;strong&gt;패킷을 드랍한 순간 유저 공간에서 돌고 있는 우리의 VPN 서버에게 알려주면 됩니다.&lt;/strong&gt; eBPF가 서버에게 알려주는 수단으로는 역시 eBPF 맵을 사용합니다.&lt;/p&gt;
&lt;p&gt;자세한 구현 방식은 여러 가지가 있겠지만, 다음과 같은 방식을 사용하기로 결정합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;eBPF 프로그램에서 &quot;키 만료&quot;에 의해서 패킷이 드랍되면, &lt;strong&gt;&lt;code&gt;dropped_map&lt;/code&gt; 맵에 드랍된 패킷 개수를 기록&lt;/strong&gt;합니다.&lt;/li&gt;
&lt;li&gt;유저 공간 VPN 서버에서는 &lt;code&gt;dropped_map&lt;/code&gt; 맵의 패킷 개수를 지켜보다가, &lt;strong&gt;드랍된 패킷이 감지되면 그 유저에게 슬랙 알림&lt;/strong&gt;을 보내줍니다.&lt;/li&gt;
&lt;li&gt;만약 유저가 그에 반응해 키를 활성화해서 eBPF 프로그램에서 패킷을 허용하게 되면, &lt;code&gt;dropped_map&lt;/code&gt; 의 엔트리를 초기화해줍니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;위와 같이 구현을 해서 테스트를 하였고, 키가 만료된 상태에서 연결을 한 경우에 슬랙 알림을 잘 받아볼 수 있었습니다.&lt;/p&gt;
&lt;figure&gt;
    &lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 688px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 15%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAADCAIAAAAcOLh5AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAdElEQVR42nWN2QoDIQxF/f9fLJYazYC4oEWt+0xooU/teQg5JJfLWmvPlDiqO6K1lrT33t7UWmmnOcY4f8HWWnR7ANw4BwAhBCo88HDOGWNCCFrrlNI3sPcupeSc55zs4/REtTFGCnjvSc8/UJlSSkpZXuUCe1ysT24wgQ8AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/81da3bb98bda385116f753fef2110d1a/33ecc/4-slack-notification.webp 360w,
/static/81da3bb98bda385116f753fef2110d1a/263a4/4-slack-notification.webp 480w,
/static/81da3bb98bda385116f753fef2110d1a/0c8fb/4-slack-notification.webp 640w,
/static/81da3bb98bda385116f753fef2110d1a/f5c78/4-slack-notification.webp 688w&quot; sizes=&quot;(max-width: 688px) 100vw, 688px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/81da3bb98bda385116f753fef2110d1a/53918/4-slack-notification.png 360w,
/static/81da3bb98bda385116f753fef2110d1a/9aebd/4-slack-notification.png 480w,
/static/81da3bb98bda385116f753fef2110d1a/0f09e/4-slack-notification.png 640w,
/static/81da3bb98bda385116f753fef2110d1a/39eee/4-slack-notification.png 688w&quot; sizes=&quot;(max-width: 688px) 100vw, 688px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/81da3bb98bda385116f753fef2110d1a/39eee/4-slack-notification.png&quot; alt=&quot;4 slack notification&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption&gt;키가 만료된 상태에서 VPN 서버에 패킷을 보내면 오는 슬랙 알림&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;결론&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%B0%EB%A1%A0&quot; aria-label=&quot;결론 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;결론&lt;/h2&gt;
&lt;p&gt;OpenVPN을 사용하며 느낀 불편한 점들을 개선하면서 원래 사용하던 목적에도 잘 맞는 VPN 솔루션을 만들 수 있었습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;WireGuard를 사용하여 안전하고 빠른 연결을 가능하게 함&lt;/li&gt;
&lt;li&gt;KeyCloak SSO와 연동하여 관리 코스트를 줄임&lt;/li&gt;
&lt;li&gt;커널 네트워킹 스택을 활용하여 그룹 별 접근 제어를 구현함&lt;/li&gt;
&lt;li&gt;AWS 네트워크 스택을 잘 이용해서 유저 별 고유 IP로 액세스 로그가 남을 수 있도록 함&lt;/li&gt;
&lt;li&gt;eBPF를 사용하여
&lt;ul&gt;
&lt;li&gt;키 활성화 / 만료 로직을 구현함&lt;/li&gt;
&lt;li&gt;활성화되지 않은 연결에 대해 슬랙 알림을 유저에게 보내줌&lt;/li&gt;
&lt;li&gt;패킷 처리에 대해 고성능성과 확장성까지 갖추게 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;많은 기술들을 동원한 재미있는 경험이었다고 생각합니다. 긴 글 읽어주셔서 감사합니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[고객 접점에서 ‘신뢰’를 만드는 사람들, QA 직군]]></title><description><![CDATA[전 세계 누구나 언제 어디서든 믿고 이용할 수 있는 서비스 ‘품질’은 과연 어떤 사람들의 손을 거쳐 탄생될까요? 서비스의 기획부터, 개발, 라이브, 운영까지 이르는 전 과정에서 함께 호흡하며, 고객과의 신뢰를 보다 두텁게 다져가는 사람들 ‘QA직군’을 만나 이야기 나눴습니다.]]></description><link>https://tech.devsisters.com/posts/sqa-interview</link><guid isPermaLink="false">https://tech.devsisters.com/posts/sqa-interview</guid><pubDate>Mon, 02 Aug 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;누구나 언제 어디서든 믿고 이용할 수 있는 서비스 ‘품질’을 완성시키는 사람들 QA(Quality Assurance). 전 세계 고객에게 최고의 경험을 선사하는 데브시스터즈의 QA는 과연 어떤 가치관과 문화, 그리고 비전 속에서 일하고 있을까요? 서비스의 기획부터, 개발, 라이브, 운영까지 이르는 모든 과정에서 함께 호흡하며 고객과의 신뢰를 보다 두텁게 다져가는 QA 여러분들을 만나 데브시스터즈에서 일한다는 것의 의미와 그 가치에 대해 물었습니다.
&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;h2 id=&quot;틀에-박히지-않은-일-동료-문화-속에서brqa의-가능성을-새롭게-개척하고-있어요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%8B%80%EC%97%90-%EB%B0%95%ED%9E%88%EC%A7%80-%EC%95%8A%EC%9D%80-%EC%9D%BC-%EB%8F%99%EB%A3%8C-%EB%AC%B8%ED%99%94-%EC%86%8D%EC%97%90%EC%84%9Cbrqa%EC%9D%98-%EA%B0%80%EB%8A%A5%EC%84%B1%EC%9D%84-%EC%83%88%EB%A1%AD%EA%B2%8C-%EA%B0%9C%EC%B2%99%ED%95%98%EA%B3%A0-%EC%9E%88%EC%96%B4%EC%9A%94&quot; aria-label=&quot;틀에 박히지 않은 일 동료 문화 속에서brqa의 가능성을 새롭게 개척하고 있어요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;틀에 박히지 않은 일, 동료, 문화 속에서&lt;br/&gt;QA의 가능성을 새롭게 개척하고 있어요&lt;/h2&gt;
&lt;h5 id=&quot;--sqa셀-리드-진석준&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#--sqa%EC%85%80-%EB%A6%AC%EB%93%9C-%EC%A7%84%EC%84%9D%EC%A4%80&quot; aria-label=&quot;  sqa셀 리드 진석준 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;- SQA셀 리드 진석준&lt;/h5&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;sqa셀의-qa는-고객들에게-신뢰를-선사하는-사람&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#sqa%EC%85%80%EC%9D%98-qa%EB%8A%94-%EA%B3%A0%EA%B0%9D%EB%93%A4%EC%97%90%EA%B2%8C-%EC%8B%A0%EB%A2%B0%EB%A5%BC-%EC%84%A0%EC%82%AC%ED%95%98%EB%8A%94-%EC%82%AC%EB%9E%8C&quot; aria-label=&quot;sqa셀의 qa는 고객들에게 신뢰를 선사하는 사람 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;SQA셀의 QA는 고객들에게 ‘신뢰’를 선사하는 사람&lt;/h3&gt;
&lt;p&gt;안녕하세요, &lt;strong&gt;SQA(Service Quality Assurance)셀을 리드하고 있는 진석준&lt;/strong&gt; 입니다. SQA셀은 데브시스터즈가 제공하는 다양한 서비스들의 본질과 품질을 보다 많은 고객 여러분들께서 오롯이 경험하고 신뢰하실 수 있도록 돕는 일을 해요. 가장 대표적인 예로 우리의 서비스를 누구나 언제 어디서든 쾌적하게 즐길 수 있게 최적화하고, 다양한 기술과 툴 그리고 데이터를 활용해 보다 정량적이면서도 분석적으로 품질을 관리하는 업무들을 들 수 있어요.&lt;/p&gt;
&lt;p&gt;뿐만 아니라 우리는 각 서비스가 주요 앱 마켓 정책에 알맞은 조건을 갖췄는지 검증하고, 데이터를 기반으로 발생 가능한 문제들을 한 발 앞서 예방하며, 최적의 의사결정을 위한 품질 지표나 인사이트를 수시로 구성원들에게 공급하는 일도 하고 있어요. 최근에는 보다 효율적인 서비스 운영을 돕는 게임 로그나 데이터, 네트워크 관련 테스트 고도화 및 R&amp;#x26;D 업무도 함께 추진하고 있죠.&lt;/p&gt;
&lt;p&gt;이처럼 다양한 영역에 걸친 업무를 담당하는 우리가 가장 중요하게 생각하는 건 단연 ‘고객 경험과 만족’이에요. 우리의 서비스를 이용하는 고객의 만족이 제품의 품질을 나타내는 가장 핵심적인 척도이기 때문이에요. 뿐만 아니라 우리는 함께 일하는 동료들과 긴밀하고도 원활한 협업을 이어나가는 일 역시 무척 중요하게 여겨요. 내부의 고객인 구성원이 만족할 수 있는 업무가 먼저 이뤄져야 외부의 고객인 유저 여러분들의 니즈 또한 충족시킬 수 있다고 생각해서죠.&lt;/p&gt;
&lt;h3 id=&quot;더-나은-품질을-향해-끊임없이-함께-고민하고-학습하며-나누는-문화&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8D%94-%EB%82%98%EC%9D%80-%ED%92%88%EC%A7%88%EC%9D%84-%ED%96%A5%ED%95%B4-%EB%81%8A%EC%9E%84%EC%97%86%EC%9D%B4-%ED%95%A8%EA%BB%98-%EA%B3%A0%EB%AF%BC%ED%95%98%EA%B3%A0-%ED%95%99%EC%8A%B5%ED%95%98%EB%A9%B0-%EB%82%98%EB%88%84%EB%8A%94-%EB%AC%B8%ED%99%94&quot; aria-label=&quot;더 나은 품질을 향해 끊임없이 함께 고민하고 학습하며 나누는 문화 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;더 나은 품질을 향해 끊임없이 함께 고민하고 학습하며 나누는 문화&lt;/h3&gt;
&lt;p&gt;우리에겐 더 나은 품질을 위한 솔루션을 끊임없이 고민하는 문화가 있어요. 가령 최적의 의사결정을 위해 필요한 품질 정보들을 어떻게 적재적소에 제공할 수 있을지, 새로운 기술 트렌드에 걸맞은 테스트는 과연 어떤 것인지, 이를 위해 우리는 무엇을 준비해야 하는지와 같은 주제들을 모두 함께 연구하는 것이죠.&lt;/p&gt;
&lt;p&gt;뿐만 아니라 이 과정을 통해 발견한 해법을 발빠르게 실행에 옮기고, 그 결과 얻은 인사이트를 동료들과 수시로 나눠요. 사람들이 자칫 놓치거나 비어있는 공백들을 하나하나 채워가며 서비스의 품질을 한차원 높게 다지는 것이 QA의 역할이라고 생각하기 때문이에요.&lt;/p&gt;
&lt;p&gt;그래서 일까요? 데브시스터즈가 지향하는 핵심 가치 10가지 중 우리가 가장 중요하게 생각하는 것 역시 ‘용기 있는 소통’ 이에요. 언뜻 사소해보이는 이슈라고 해도 고객 경험에 부정적인 영향을 미칠 우려가 있다면, 문제의 본질에 근거하여 솔직하게 의견을 제시하고, 실질적인 해결이 이뤄지도록 돕는 것 또한 QA가 해야할 일이기 때문이죠.&lt;/p&gt;
&lt;h3 id=&quot;도전을-장려하는-환경-속에서-qa의-새로운-가능성을-개척할-수-있는-곳&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8F%84%EC%A0%84%EC%9D%84-%EC%9E%A5%EB%A0%A4%ED%95%98%EB%8A%94-%ED%99%98%EA%B2%BD-%EC%86%8D%EC%97%90%EC%84%9C-qa%EC%9D%98-%EC%83%88%EB%A1%9C%EC%9A%B4-%EA%B0%80%EB%8A%A5%EC%84%B1%EC%9D%84-%EA%B0%9C%EC%B2%99%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-%EA%B3%B3&quot; aria-label=&quot;도전을 장려하는 환경 속에서 qa의 새로운 가능성을 개척할 수 있는 곳 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;도전을 장려하는 환경 속에서 QA의 새로운 가능성을 개척할 수 있는 곳&lt;/h3&gt;
&lt;p&gt;데브시스터즈에서 경험한 특별함은 틀에 박히지 않은 일, 동료들과 함께 호흡할 수 있다는 거예요. QA 직무의 업무가 주로 테스트에만 한정되어 있는 탓에, 개인 업무 범위나 역량을 확장하기 힘든 경우가 많아요. 하지만 데브시스터즈에는 &apos;다양한 시각과 의견을 통해 제품의 완성도를 한층 높게 다질 수 있다&apos;는 생각을 지닌 동료들이 일하고 있어요. 따라서 QA 담당자들 역시 서비스의 기획부터 개발, 배포, 운영 등 과정 전반에 직접 참여하죠. 이 과정을 통해 우리는 스스로의 역량과 경험을 다양한 분야에 걸쳐 확장시키고, 나아가 직무의 가능성까지 새롭게 개척하고 있어요.&lt;/p&gt;
&lt;p&gt;뿐만 아니라 구성원들에게 도전과 성장을 장려하고 격려하며 지원하는 문화 또한 빼놓을 수 없는 특별함이에요. 그 덕에 우리는 즐거움의 정서 속에서 업무에 임하고, 자기주도적으로 프로젝트를 수행해나가고 있어요. 이 시간들이 쌓이고 쌓여 우리의 역량이 발전함은 물론, 고객 여러분들에게도 보다 높은 품질의 서비스를 제공해드릴 수 있다고 생각해요.&lt;/p&gt;
&lt;p&gt;이처럼 데브시스터즈에서 QA 직군은 없어서는 안 될 꼭 필요한 동료로 인정받고 있어요. 그만큼 우리는 끊임없이 기술력을 향상시키고, 중요한 의사결정에 필요한 품질 인사이트를 보다 정량적이면서도 효율적으로 구성원들과 공유해나갈 계획이에요. 뿐만 아니라 전 세계 곳곳의 고객 여러분들에게도 보다 높은 품질의 서비스 이용 경험, 그리고 신뢰를 꾸준히 선사해드리고자 최선을 다하겠습니다.
&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;h2 id=&quot;주도적으로-일하는-문화-속에서-기술력과-커리어의-확장을-누리고-있어요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A3%BC%EB%8F%84%EC%A0%81%EC%9C%BC%EB%A1%9C-%EC%9D%BC%ED%95%98%EB%8A%94-%EB%AC%B8%ED%99%94-%EC%86%8D%EC%97%90%EC%84%9C-%EA%B8%B0%EC%88%A0%EB%A0%A5%EA%B3%BC-%EC%BB%A4%EB%A6%AC%EC%96%B4%EC%9D%98-%ED%99%95%EC%9E%A5%EC%9D%84-%EB%88%84%EB%A6%AC%EA%B3%A0-%EC%9E%88%EC%96%B4%EC%9A%94&quot; aria-label=&quot;주도적으로 일하는 문화 속에서 기술력과 커리어의 확장을 누리고 있어요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;주도적으로 일하는 문화 속에서 기술력과 커리어의 확장을 누리고 있어요&lt;/h2&gt;
&lt;h5 id=&quot;--플랫폼셀-qa-담당-한의선&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#--%ED%94%8C%EB%9E%AB%ED%8F%BC%EC%85%80-qa-%EB%8B%B4%EB%8B%B9-%ED%95%9C%EC%9D%98%EC%84%A0&quot; aria-label=&quot;  플랫폼셀 qa 담당 한의선 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;- 플랫폼셀 QA 담당 한의선&lt;/h5&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;플랫폼셀의-qa는-유저들의-기분-좋은-첫-걸음을-돕는-사람&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%94%8C%EB%9E%AB%ED%8F%BC%EC%85%80%EC%9D%98-qa%EB%8A%94-%EC%9C%A0%EC%A0%80%EB%93%A4%EC%9D%98-%EA%B8%B0%EB%B6%84-%EC%A2%8B%EC%9D%80-%EC%B2%AB-%EA%B1%B8%EC%9D%8C%EC%9D%84-%EB%8F%95%EB%8A%94-%EC%82%AC%EB%9E%8C&quot; aria-label=&quot;플랫폼셀의 qa는 유저들의 기분 좋은 첫 걸음을 돕는 사람 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;플랫폼셀의 QA는 유저들의 ‘기분 좋은 첫 걸음’을 돕는 사람&lt;/h3&gt;
&lt;p&gt;안녕하세요 &lt;strong&gt;플랫폼셀의 QA 담당 한의선&lt;/strong&gt; 입니다. 글로벌 각지의 유저 여러분들을 위한 데브시스터즈 자체 게임 플랫폼 ‘데브플레이’의 완성도를 검증하고, 다양한 API 및 기능의 안정성을 높은 수준으로 유지하기 위한 품질 관리 전반을 담당하고 있어요. 유저 여러분들이 데브시스터즈의 게임과 만나는 첫 접점에서 한결 편리한 이용 경험을 누리시고, 게임 내에서 제공되는 혜택들 또한 원활하게 공급받으실 수 있도록 돕는 역할이라고 볼 수 있죠.&lt;/p&gt;
&lt;p&gt;우리는 일할 때 동료들과 ‘공감’을 유지하는 걸 중요하게 생각해요. 데브플레이 플랫폼은 여러 게임에서 함께 사용되는 만큼, 같은 셀 뿐 아니라 다양한 팀 구성원들과도 원활하게 협업하면서도, ‘안정적인 유저 경험 제공’이라는 공동의 목표 아래 함께 나아가야하기 때문이에요.&lt;/p&gt;
&lt;p&gt;그래서 우리는 평소 일할 때 서로 긴밀하게 소통하면서도, 각자 맡은 역할을 통해 모두에게 도움이 되고자 다함께 노력하고 있어요. 이 과정들은 우리가 주도적으로 일에 임하게 해주고, 다양한 업무 흐름과 이슈들을 자연스럽게 파악하게 해주며, 서비스의 완성도 역시 한 단계 높게 다지는데도 큰 도움을 준다고 생각해요.&lt;/p&gt;
&lt;h3 id=&quot;최상의-솔루션을-주도적으로-연구하고-실행하며-발전하는-문화&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B5%9C%EC%83%81%EC%9D%98-%EC%86%94%EB%A3%A8%EC%85%98%EC%9D%84-%EC%A3%BC%EB%8F%84%EC%A0%81%EC%9C%BC%EB%A1%9C-%EC%97%B0%EA%B5%AC%ED%95%98%EA%B3%A0-%EC%8B%A4%ED%96%89%ED%95%98%EB%A9%B0-%EB%B0%9C%EC%A0%84%ED%95%98%EB%8A%94-%EB%AC%B8%ED%99%94&quot; aria-label=&quot;최상의 솔루션을 주도적으로 연구하고 실행하며 발전하는 문화 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;최상의 솔루션을 주도적으로 연구하고 실행하며 발전하는 문화&lt;/h3&gt;
&lt;p&gt;데브플레이 플랫폼은 다양한 게임과 연동되어 있기에, 사소해보이는 이슈라고 해도 그 영향이 미치는 범위는 무척 크고 복잡해요. 그만큼 우리에겐 다양한 경우의 수와 서비스의 요소요소들을 미리 면밀하게 파악하고, 문제를 사전에 예방하며, 이슈가 발생됐을 때는 신속하게 대처하는 일이 굉장히 중요해요.&lt;/p&gt;
&lt;p&gt;따라서 우리는 한 발 더 앞서, 더 빠르게, 더 안정적인 서비스 품질을 유지할 수 있는 ‘최상의 솔루션’을 다함께 연구하고, 유익한 해법은 그 즉시 실행에 옮기고 있어요. 또 함께 협업하는 여러 부서 구성원들의 의견과 피드백을 정기적으로 듣고, 좋은 개선 방안은 빠르게 적용하는 등 보다 나은 품질을 다지고자 끊임없이 노력하죠.&lt;/p&gt;
&lt;p&gt;이러한 ‘주도적 개선’은 데브시스터즈의 핵심가치 10가지 중 하나이자, 제가 입사 이후부터 지금까지 늘 지키고자 노력하는 가치이기도 해요. 업무 중 마주치게 되는 여러 장애를 넘어서기 위해 내가 먼저 노력하고, 혼자 감당할 수 없는 어려움은 동료들과 함께 해법을 찾아나가는 일이 나 스스로나 회사는 물론, 우리 게임을 즐겨주시는 유저 여러분들에게도 분명 큰 유익을 준다고 생각하기 때문이죠.&lt;/p&gt;
&lt;h3 id=&quot;역량-성장과-자부심-커리어의-확장을-고루-누릴-수-있는-곳&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%97%AD%EB%9F%89-%EC%84%B1%EC%9E%A5%EA%B3%BC-%EC%9E%90%EB%B6%80%EC%8B%AC-%EC%BB%A4%EB%A6%AC%EC%96%B4%EC%9D%98-%ED%99%95%EC%9E%A5%EC%9D%84-%EA%B3%A0%EB%A3%A8-%EB%88%84%EB%A6%B4-%EC%88%98-%EC%9E%88%EB%8A%94-%EA%B3%B3&quot; aria-label=&quot;역량 성장과 자부심 커리어의 확장을 고루 누릴 수 있는 곳 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;역량 성장과 자부심, 커리어의 확장을 고루 누릴 수 있는 곳&lt;/h3&gt;
&lt;p&gt;데브시스터즈에서 일하면서 가장 좋았던 점 하나를 꼽자면 스스로의 기술적 역량을 크게 성장시킬 수 있다는 거예요. 플랫폼을 다루는 팀의 QA 담당자로 일하다 보니 여러 형태의 아키텍쳐나 유저 사용성, 기술 트렌드들을 자연스럽게 경험하고 학습하며 응용해볼 수 있기 때문이에요.&lt;/p&gt;
&lt;p&gt;또 전 세계 곳곳의 유저들이 이용하는 플랫폼인 만큼 글로벌하면서도 새로운 과제들에 수시로 도전해볼 수 있어 좋아요. 뿐만 아니라 플랫폼은 게임을 포함한 여러 분야에서 중요하게 다루고 있는 도메인이기 때문에 일할 때 큰 자부심과 메리트를 얻을 수도 있어요. 그만큼 수준 높은 기술력을 터득하고, 프로젝트 커리어의 확장을 누리고픈 QA 담당자에겐 더없이 매력적인 환경이라고 생각해요.&lt;/p&gt;
&lt;p&gt;제겐 보다 안정적인 플랫폼을 통해 우리의 게임을 즐기는 전세계 유저들에게 ‘소중한 추억’을 선사해드리고픈 비전이 있어요. 그리고 구성원들에게는 함께 일하고 싶은, 또 서로 시너지를 낼 수 있는 동료로 기억되고픈 바람도 있는데요. 이를 위해 다양한 사람들의 피드백에 끊임없이 귀 기울이고, 보다 높은 품질의 서비스 이용 경험을 제공해드릴 수 있도록 늘 힘쓰겠습니다.
&lt;br/&gt;&lt;br/&gt;&lt;br/&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;h2 id=&quot;멋진-일을-함께-성취해나가는-문화-속에서br역량의-스펙트럼을-확장시키고-있어요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A9%8B%EC%A7%84-%EC%9D%BC%EC%9D%84-%ED%95%A8%EA%BB%98-%EC%84%B1%EC%B7%A8%ED%95%B4%EB%82%98%EA%B0%80%EB%8A%94-%EB%AC%B8%ED%99%94-%EC%86%8D%EC%97%90%EC%84%9Cbr%EC%97%AD%EB%9F%89%EC%9D%98-%EC%8A%A4%ED%8E%99%ED%8A%B8%EB%9F%BC%EC%9D%84-%ED%99%95%EC%9E%A5%EC%8B%9C%ED%82%A4%EA%B3%A0-%EC%9E%88%EC%96%B4%EC%9A%94&quot; aria-label=&quot;멋진 일을 함께 성취해나가는 문화 속에서br역량의 스펙트럼을 확장시키고 있어요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;멋진 일을 함께 성취해나가는 문화 속에서&lt;br/&gt;역량의 스펙트럼을 확장시키고 있어요&lt;/h2&gt;
&lt;h5 id=&quot;웹서비스셀-qa-담당-강유경&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9B%B9%EC%84%9C%EB%B9%84%EC%8A%A4%EC%85%80-qa-%EB%8B%B4%EB%8B%B9-%EA%B0%95%EC%9C%A0%EA%B2%BD&quot; aria-label=&quot;웹서비스셀 qa 담당 강유경 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;웹서비스셀 QA 담당 강유경&lt;/h5&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;웹서비스셀의-qa는-다양한-고객-접점에서-안락함을-선사하는-사람&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9B%B9%EC%84%9C%EB%B9%84%EC%8A%A4%EC%85%80%EC%9D%98-qa%EB%8A%94-%EB%8B%A4%EC%96%91%ED%95%9C-%EA%B3%A0%EA%B0%9D-%EC%A0%91%EC%A0%90%EC%97%90%EC%84%9C-%EC%95%88%EB%9D%BD%ED%95%A8%EC%9D%84-%EC%84%A0%EC%82%AC%ED%95%98%EB%8A%94-%EC%82%AC%EB%9E%8C&quot; aria-label=&quot;웹서비스셀의 qa는 다양한 고객 접점에서 안락함을 선사하는 사람 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;웹서비스셀의 QA는 다양한 고객 접점에서 ‘안락함’을 선사하는 사람&lt;/h3&gt;
&lt;p&gt;안녕하세요 &lt;strong&gt;웹서비스셀의 QA 담당 강유경&lt;/strong&gt; 입니다. 데브시스터즈 기업 내부와 외부에서 운영 중인 여러 웹 기반 서비스들에 대한 품질 검증과 관리, 프로세스 개선 업무를 주로 담당하고 있어요.&lt;/p&gt;
&lt;p&gt;데브시스터즈의 웹서비스는 글로벌 각지의 유저, 파트너, 입사 지원자, 구성원 등 다양한 니즈를 지닌 고객들을 대상으로 운영돼요. 따라서 우리는 전 세계 누구에게나 편리하고 안락한 이용 경험을 선사하기 위해 여러 형태의 고객 특성을 고려하고, 보다 나은 UX에 대해 끊임없이 고민하며, 서비스의 퀄리티 또한 늘 높은 수준으로 유지하고자 최선을 다하고 있어요.&lt;/p&gt;
&lt;p&gt;웹은 누구나 쉽게 접근 가능하면서도, 활용도 또한 무척 높은 환경이라는 특성이 있어요. 그만큼 우리는 즐겁고 편안하며 유쾌한 공기 속에서 일에 몰입하는 걸 무척 중요하게 여겨요. 일하는 과정에서 우리가 경험하는 정서와 감정이 웹페이지를 통해 사용자 여러분들에게도 고스란히 전해진다고 생각하기 때문이죠.&lt;/p&gt;
&lt;h3 id=&quot;스스로의-전문성을-바탕으로-의미있는-성취를-함께-이뤄나가는-문화&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8A%A4%EC%8A%A4%EB%A1%9C%EC%9D%98-%EC%A0%84%EB%AC%B8%EC%84%B1%EC%9D%84-%EB%B0%94%ED%83%95%EC%9C%BC%EB%A1%9C-%EC%9D%98%EB%AF%B8%EC%9E%88%EB%8A%94-%EC%84%B1%EC%B7%A8%EB%A5%BC-%ED%95%A8%EA%BB%98-%EC%9D%B4%EB%A4%84%EB%82%98%EA%B0%80%EB%8A%94-%EB%AC%B8%ED%99%94&quot; aria-label=&quot;스스로의 전문성을 바탕으로 의미있는 성취를 함께 이뤄나가는 문화 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;스스로의 전문성을 바탕으로 의미있는 성취를 함께 이뤄나가는 문화&lt;/h3&gt;
&lt;p&gt;우리에겐 혼자가 아닌 ‘함께’를 중요하게 생각하는 문화가 있어요. 그래서 같은 셀은 물론 프로젝트와 관련된 여러 구성원 분들과 긴밀히 소통하고, 사용자 관점에서 과연 어떤 판단이 가장 최상의 선택일지 수시로 의견을 교류해요. 이 과정은 프로젝트에 대한 애정을 갖게 해주고, 맡은 일에 주도적으로 임하며, 보다 나은 성취로 우리를 이끌어줘요.&lt;/p&gt;
&lt;p&gt;뿐만 아니라 우리에겐 스스로 맡은 일에 대한 전문성을 통해 서로 동등한 위치에서 소통하는 문화가 있어요. 이 문화 속에서 우리는 각자의 영역에서 자유롭게 의견을 제시하고, 서로의 지식과 재능을 바탕으로 조화로운 시너지를 일으키며, 한층 완성도 높은 품질의 웹서비스를 만들어가고 있죠.&lt;/p&gt;
&lt;h3 id=&quot;다양한-웹-분야와-유저-사용성-그리고-글로벌-프로젝트를-경험할-수-있는-곳&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8B%A4%EC%96%91%ED%95%9C-%EC%9B%B9-%EB%B6%84%EC%95%BC%EC%99%80-%EC%9C%A0%EC%A0%80-%EC%82%AC%EC%9A%A9%EC%84%B1-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EA%B8%80%EB%A1%9C%EB%B2%8C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%EA%B2%BD%ED%97%98%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-%EA%B3%B3&quot; aria-label=&quot;다양한 웹 분야와 유저 사용성 그리고 글로벌 프로젝트를 경험할 수 있는 곳 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;다양한 웹 분야와 유저 사용성, 그리고 글로벌 프로젝트를 경험할 수 있는 곳&lt;/h3&gt;
&lt;p&gt;데브시스터즈에서 경험한 특별함 하나를 꼽자면 QA 담당자로서 기획부터 개발, 배포, 운영까지 이르는 프로젝트 과정 전반에 직접 참여할 수 있다는 거예요. 시작 단계부터 품질의 기반을 탄탄하게 다져야 보다 안락한 고객 경험을 선사하는 웹서비스를 완성할 수 있다는 걸 모두 이해하기 때문이죠.&lt;/p&gt;
&lt;p&gt;그만큼 개발이 시작될 때부터 사용자가 우리의 웹서비스에서 원하는 것은 무엇인지, 과연 어떤 경험을 선사해야하는지, 그러려면 어떻게 해야하는지 등을 함께 고민해요. 이 과정은 QA 담당자의 역량을 한 차원 높게 향상 시켜줄 뿐 아니라, 보다 고객 친화적인 경험을 선사하는 방법에 대해서도 폭넓게 이해하는 제너럴리스트로 성장시켜줘요.&lt;/p&gt;
&lt;p&gt;이와 같은 맥락에서 데브시스터즈의 핵심가치 10가지 중 개인적으로 가장 좋아하고 끊임없이 되뇌이는 가치 역시 ‘고객 경험 중심’이에요. 따라서 평소 모든 웹페이지의 기능이 제대로 작동하는지, UX/UI가 고객 관점에서 편리함을 선사하는지, 디바이스나 브라우저 별 호환성에 문제가 없는지, 여러 국가에 걸친 방문자들 각각의 특성과 사용성을 충분히 만족시켜주는지 등 다양한 요소요소들을 늘 꼼꼼하게 점검하고 체크해요. 또 혹시라도 부족한 부분이 있었다면 셀원들이나 프로젝트와 관련된 타 팀 구성원들과 함께 회고하고 스터디하며 보다 나은 방향으로 나아가고자 노력해요.&lt;/p&gt;
&lt;p&gt;올해는 쿠키런: 킹덤 공식 홈페이지와 우리의 이야기를 담은 &lt;a href=&quot;https://www.devsisters.com/stories/story&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;&apos;아워스토리&apos;&lt;/a&gt; 등 다양한 웹서비스로 여러분들을 찾아 뵙고 있어요. 뿐만 아니라 앞으로 더 다양하고 유용한 웹서비스들을 선보이게 될 예정이에요. 우리의 웹서비스가 여러분들께 끊임없이 안락함을 드리고, 업계와 국가를 불문한 누구나 좋은 레퍼런스로 삼을 수 있는 ‘표준’이 될 수 있도록 늘 최선을 다하겠습니다.&lt;/p&gt;</content:encoded></item><item><title><![CDATA[사전예약 서버 프로파일링으로 서버 병목찾아 개선하기]]></title><description><![CDATA[artillery, linux perf로 node.js 서버 성능을 테스팅하여 flamegraph를 뽑아내고 서버의 병목구간을 찾아 개선한 경험을 소개합니다.]]></description><link>https://tech.devsisters.com/posts/perf-citizen-card</link><guid isPermaLink="false">https://tech.devsisters.com/posts/perf-citizen-card</guid><pubDate>Sun, 28 Feb 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요! 웹서비스셀에서 백엔드 개발자로 근무 중인 윤준성입니다🙇 지난 2020년 12월, 쿠키런:킹덤 사전예약의 &lt;strong&gt;시민권 생성 서비스&lt;/strong&gt;를 개발하였는데요.
이 서비스의 개발 과정에서 서버의 성능을 테스트하고, linux perf를 통한 서버 프로파일링으로 서버의 병목 구간을 찾아 성능을 개선하였던 경험을 소개합니다.&lt;/p&gt;
&lt;h2 id=&quot;시민권-발급-이벤트&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8B%9C%EB%AF%BC%EA%B6%8C-%EB%B0%9C%EA%B8%89-%EC%9D%B4%EB%B2%A4%ED%8A%B8&quot; aria-label=&quot;시민권 발급 이벤트 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;시민권 발급 이벤트&lt;/h2&gt;
&lt;p&gt;웹서비스셀에서는 지난 1월 런칭한 쿠키런:킹덤의 사전예약 서비스를 운영하였습니다.&lt;/p&gt;
&lt;p&gt;사전예약 서비스의 다양한 이벤트 중, &lt;strong&gt;시민권 발급 이벤트&lt;/strong&gt;는 쿠키 이름을 입력하면 랜덤하게 생성된 귀여운 쿠키 이미지와 함께 시민권을 발급해주었습니다. 그리고 이 시민권을 SNS에 공유하면, 쿠키런:킹덤에서 사용할 수 있는 보상을 얻을 스탬프를 획득할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1858px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 40.27777777777778%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAABcSAAAXEgFnn9JSAAAChklEQVR42h3KT0xScQAHcA7dtLWsNVdtttQQ/1CmS8X5FJGnIJoyUkFAIHkIPgXlnyg+QJe5bHVqrlPL5cEObc1LuXVpy2lTamu1lglW+lpbdmz6Xj++tQ6f20dSq1T/rmtUi+rSHIGWZQuasuNCc+kxoT7/iECVnDpUKJtRq6Q/KRqaXtepaDRdzD2gi7IErTzn380R1EXZQn3xiYOaBhVqlOotyUB4SvSGOdDyk2B05Rh3aBF36jFhVqOTkmVi0zMYm4ylmFAsOcrdQAclJZ7OSvhNKni7KbBXZdBdOUOisSlMxBNpiW1kXNTpjaAKjkJflQs7XYjbzmuYY40wqC5l2EgCXTZbyswGku0mF1y6IrJgr8CcrhgJnQz3rHIMdRYTxh9FL+NOS0ajCVHf3QYbnYdp22WEWs8jqCtEpEsKs1aecYYSsI9EUgEukTRZzQj21ZD9ZBQ/3i/g48YS9t4t4uZoG7EMBuEMcmmJN8KJdtaPaU81nse1eDjrw6MhA5ZjzYgwVKbPx8EVjqV8ES7pCUQRsFaQ1HIbviXvYGtjHjsv/Ui4KomFHcNAKJ6WDPiDh0Zrf+buZAtZvc+QpVsT5DHnJi/mbSTurvnT3qRBt2N4eyAQ3nQNe9HRUCpaaCmxaGTESJ39r70iV+zWamAw9W9L3JFp0T4UBNtThZ2tVXzYXMGXzxt4u/4U5sZzGUN1Ma47HSn3xGySHZtEK91E6MFFKHtnUFJUC6m8C1RZAelRFMJuNaUlTCj+1eEd+65XnOZfPVHya8sa/s2Kjn/2oJZvq8rbc3ucP/t8vjUmGF9xBiZ/aaryd1XSLN6mqeRtLeW86kIW31Keu+v29O/3eYfX/wKjw0+MaET68gAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/458bd27371527a0d2518a15d421acd01/33ecc/citizenCardEvent.webp 360w,
/static/458bd27371527a0d2518a15d421acd01/263a4/citizenCardEvent.webp 480w,
/static/458bd27371527a0d2518a15d421acd01/0c8fb/citizenCardEvent.webp 640w,
/static/458bd27371527a0d2518a15d421acd01/521f7/citizenCardEvent.webp 860w,
/static/458bd27371527a0d2518a15d421acd01/cbd37/citizenCardEvent.webp 1200w,
/static/458bd27371527a0d2518a15d421acd01/a311b/citizenCardEvent.webp 1858w&quot;
              sizes=&quot;(max-width: 1858px) 100vw, 1858px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/458bd27371527a0d2518a15d421acd01/53918/citizenCardEvent.png 360w,
/static/458bd27371527a0d2518a15d421acd01/9aebd/citizenCardEvent.png 480w,
/static/458bd27371527a0d2518a15d421acd01/0f09e/citizenCardEvent.png 640w,
/static/458bd27371527a0d2518a15d421acd01/3e3fe/citizenCardEvent.png 860w,
/static/458bd27371527a0d2518a15d421acd01/64756/citizenCardEvent.png 1200w,
/static/458bd27371527a0d2518a15d421acd01/bf7a7/citizenCardEvent.png 1858w&quot;
            sizes=&quot;(max-width: 1858px) 100vw, 1858px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/458bd27371527a0d2518a15d421acd01/bf7a7/citizenCardEvent.png&quot;
            alt=&quot;쿠키런:킹덤 사전예약 페이지의 시민권 생성 이벤트&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;쿠키런:킹덤 사전예약 페이지의 시민권 생성 이벤트&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;시민권에 사용되는 랜덤 쿠키 이미지는 &lt;strong&gt;얼굴, 몸, 눈, 입, 앞머리 모양&lt;/strong&gt; 5개의 필수요소와 &lt;strong&gt;배경, 페이스페인트, 안경, 왕관, 스티커, 뒷머리 모양&lt;/strong&gt; 6개의 선택요소, 그리고 &lt;strong&gt;머리 색깔&lt;/strong&gt;로 결정됩니다. 각 요소의 가짓수를 고려하여 계산해보면... 무려 &lt;code&gt;5,074,167,364,156,800가지&lt;/code&gt;의 쿠키 경우의 수가 존재합니다! 시민권에 들어가는 주소와 입국허가 사유도 랜덤으로 생성되는 것을 고려하면 사실은 더 많은 시민권 가짓수가 존재합니다. 이렇게나 많은 이미지를 미리 만들어 둘 수 없기에, 쿠키 이미지들은 요청이 들어오는 대로 생성하고 있었고, 이는 꽤 많은 리소스를 잡아먹는 서비스였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1360px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 41.11111111111111%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsTAAALEwEAmpwYAAACNElEQVR42h2Sa0uTARTHnw/Q6+grBPWqD1C9kaIIKwgyIiQTSYpAo1KSMMOKUvNSDJdZgm3q0FHecU5LE11uNud9a3fn1N16nmebu/x6fM7hf/i9OOdwzuEIfo8Xq7EXh9NPmiQ+aYmgy4ycDhG3LODsaEFcsUMefN/GiC5tcGj5bF7RgcqynOCv383OXhhhSPOZ+rtFfNG8J57c4UFrG+U3ygg7f7He0Up/eTH+/i6CFhu1x0/RVHARaTtIZMGIs6UHYhlczt980L9jYnYIYcS9hPlTFaPjPXjFCHUvaqiuKMHhW8b1sZm20lv4hw14J8xUn7nEnRMnSaxv4uiqZriojHxAZPLHT25fuYzOMIgwFg9h6GvAsjVPJCeRSAZIyD6kXJx9q52vzU0kvX4QYbxdy6y2U10z1DuNv1FHVkoSjolM9fThcfsQkvtRIr4AUix2eBn2PHa2l2cUSpGOJ0jHIuQPkmQVj+aipPKy2jCVySKmMmpNVok5RVmFBce0E412HtNcgGwuh7biCa0lBURFD3u2V7hnChGDVuJyiIHXhUzoH5NRyh81dFJ8r4qIKDFg6KO2spypxUUE92YAy7ARl8WK/C9D3dO3XL92lkB4jfDkS1Z055E8NnZ3Nug8dxT9wwvqhO3NjVSW3ERSuOX5M04fO4Km4Q1CaDdCt7aR2KpdTTQau+jW1ysvFCO6piO6WoW8vYrMAQtzg2ytmdS8P1YbI4PfVR41m6i5fxXT7Az/AZ8GEmhFUAD2AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/9c3e99985bb21788be3805d175f0050f/33ecc/randomCookies.webp 360w,
/static/9c3e99985bb21788be3805d175f0050f/263a4/randomCookies.webp 480w,
/static/9c3e99985bb21788be3805d175f0050f/0c8fb/randomCookies.webp 640w,
/static/9c3e99985bb21788be3805d175f0050f/521f7/randomCookies.webp 860w,
/static/9c3e99985bb21788be3805d175f0050f/cbd37/randomCookies.webp 1200w,
/static/9c3e99985bb21788be3805d175f0050f/8717b/randomCookies.webp 1360w&quot;
              sizes=&quot;(max-width: 1360px) 100vw, 1360px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/9c3e99985bb21788be3805d175f0050f/53918/randomCookies.png 360w,
/static/9c3e99985bb21788be3805d175f0050f/9aebd/randomCookies.png 480w,
/static/9c3e99985bb21788be3805d175f0050f/0f09e/randomCookies.png 640w,
/static/9c3e99985bb21788be3805d175f0050f/3e3fe/randomCookies.png 860w,
/static/9c3e99985bb21788be3805d175f0050f/64756/randomCookies.png 1200w,
/static/9c3e99985bb21788be3805d175f0050f/9d567/randomCookies.png 1360w&quot;
            sizes=&quot;(max-width: 1360px) 100vw, 1360px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/9c3e99985bb21788be3805d175f0050f/9d567/randomCookies.png&quot;
            alt=&quot;대신귀 여운쿠 키들을 드리겠 습니다&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;대신귀 여운쿠 키들을 드리겠 습니다&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h2 id=&quot;시민권-발급-서비스-로직&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8B%9C%EB%AF%BC%EA%B6%8C-%EB%B0%9C%EA%B8%89-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%A1%9C%EC%A7%81&quot; aria-label=&quot;시민권 발급 서비스 로직 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;시민권 발급 서비스 로직&lt;/h2&gt;
&lt;p&gt;시민권 발급 서비스의 입출력은 단순합니다. Input은 시민권에 들어갈 쿠키의 이름과 언어를 받습니다&lt;span style=&quot;color:grey&quot;&gt;(킹덤 사전예약은 한국어를 포함한 4개의 언어로 진행되었습니다.)&lt;/span&gt;. Output으로는 생성된 시민권 이미지의 URL을 반환합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;input: 쿠키의 이름(준성맛 쿠키), 언어(한국어)&lt;/p&gt;
&lt;p&gt;output: 생성된 시민권 이미지의 URL&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;서버는 node.js 환경에서 typescript로 작성되었으며, 서비스의 내부 로직은 아래와 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;랜덤 요소들(쿠키 증명사진 조합에 쓰일 컴포넌트들 11종, 주소, 입국허가 사유)의 값을 생성한다.&lt;/li&gt;
&lt;li&gt;시민권 배경, 쿠키 이미지 요소들, 시민권 테두리 이미지를 node-canvas로 로드해온다.&lt;/li&gt;
&lt;li&gt;시민권에 필요한 텍스트를 node-canvas로 이미지로 만들고, 이미지들을 합쳐서 시민권 이미지를 완성한다!&lt;/li&gt;
&lt;li&gt;생성된 시민권 이미지를 PNGStream으로 바꿔서 S3 버킷에 업로드한다. 업로드된 이미지들은 CloudFront를 통해 프론트엔드로 서빙된다!&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2031px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 72.5%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAABcSAAAXEgFnn9JSAAACeklEQVR42o1STU8TURTtP/AnGGOiG9m40sSlbowuFOJG3RhFTVwYXbhQgwsT1AWJMTZ+EepHAgEixUIMEAS0tUg0qNhAS+102imlLUzpfHQ6M+/N8fV1iCVC5SZn7n3v3Tnv3HufhxJi5r/OksxUmAgfgyQ29YksjE+S359nCKWUAODepg6hLK6uHcfZALZno2Zxj2UYZPTkIQwd2YGeE7vR1bwLT47uhK9lD0rCMM8SFRWBWBTfxQQcQrCJOfzjOIKHRcQs+mEUOmEoIzC1ESjZfsjCK+jSW549L5fQ29eBdy/bsVZS+R6TDUVRsCSloSpKHaFDiZ4fgLY6yjAOOT2I7FwX5NR72EaSX55RdfR3PsbEYC9UXeOEplFGbimNxeg8EvEYE86VC57qZbrkRz58Clp2AHp2EKWUH8XoQ6xMtzI+ygnkfA6FwgqMigVXDYhtc5iVysaSHasINe5DOTMMczUEsxCCLnajLAVYzyzYlSInoZbGUde2TXrIp+Rs1WdY5TxyP9pQFHzM30Z5NeIe00aE1XM2ec7tAiZDpZZtZ2DKfaBmzP2f/E/h3/1qb6omTdxE6Mp++A/vRU/zPrw+fRBPjzcx34RvncdgqWK90kaEtUgc92LkWgv8bZcwdu8qArfOIXCnFQM3LmCy4zxMJekOpyHheiEERS2KXu9FPH9xHys0i/zSF0Tnx7CcnMTacgjUljdUu7VC1y/OPkK39zqGu71ISBFkYkH8DPrxK9gPaaYdVmlxeyWva8yELyPypgkTvrNIxZ+hMPcA0vRdpD6cQWLoACrywvZKXjfC3p6liHAMEbYmwNKSbJ1gEGCXc+5L+HfKfwDDNgOj2QyZZAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/40fb3b71c191ab93fe1dc8b968534661/33ecc/create-citizen-card.webp 360w,
/static/40fb3b71c191ab93fe1dc8b968534661/263a4/create-citizen-card.webp 480w,
/static/40fb3b71c191ab93fe1dc8b968534661/0c8fb/create-citizen-card.webp 640w,
/static/40fb3b71c191ab93fe1dc8b968534661/521f7/create-citizen-card.webp 860w,
/static/40fb3b71c191ab93fe1dc8b968534661/cbd37/create-citizen-card.webp 1200w,
/static/40fb3b71c191ab93fe1dc8b968534661/7e72d/create-citizen-card.webp 2031w&quot;
              sizes=&quot;(max-width: 2031px) 100vw, 2031px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/40fb3b71c191ab93fe1dc8b968534661/53918/create-citizen-card.png 360w,
/static/40fb3b71c191ab93fe1dc8b968534661/9aebd/create-citizen-card.png 480w,
/static/40fb3b71c191ab93fe1dc8b968534661/0f09e/create-citizen-card.png 640w,
/static/40fb3b71c191ab93fe1dc8b968534661/3e3fe/create-citizen-card.png 860w,
/static/40fb3b71c191ab93fe1dc8b968534661/64756/create-citizen-card.png 1200w,
/static/40fb3b71c191ab93fe1dc8b968534661/2edc3/create-citizen-card.png 2031w&quot;
            sizes=&quot;(max-width: 2031px) 100vw, 2031px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/40fb3b71c191ab93fe1dc8b968534661/2edc3/create-citizen-card.png&quot;
            alt=&quot;필요한 이미지들을 node-canvas로 그려낸다!&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;필요한 이미지들을 node-canvas로 그려낸다!&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h2 id=&quot;artillery로-부하테스트하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#artillery%EB%A1%9C-%EB%B6%80%ED%95%98%ED%85%8C%EC%8A%A4%ED%8A%B8%ED%95%98%EA%B8%B0&quot; aria-label=&quot;artillery로 부하테스트하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Artillery로 부하테스트하기&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://artillery.io/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Artillery&lt;/a&gt;라는 Node.js 부하 테스트 서드파티를 이용하여, 시민권 서버의 성능을 테스트하였습니다. Artillery는 쉽게 말하자면, 서버에 내가 원하는 요청을 마구마구 쏴볼 수 있는 도구인데요. Artillery를 선택한 이유는&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;서버와 같은 Node.js 환경이라 사용이 편리하고&lt;/li&gt;
&lt;li&gt;다양한 시나리오의 테스트를 YAML로 쉽게 관리할 수 있으며&lt;/li&gt;
&lt;li&gt;javascript로 쉽게 로직을 커스텀 할 수 있기 때문이었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;아래의 YAML 파일을 이용하였습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;https://...&apos;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;#시민권 서버 주소&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;processor&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;./artillery-functions.js&apos;&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;phases&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;duration&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;600&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;#n초간 테스트를 진행&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;arrivalRate&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;#초당 n개의 API 요청을 보냄&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;scenarios&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;create citizen card&apos;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;flow&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;function&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;generateRandomData&apos;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;/citizen-card&apos;&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;afterResponse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;logError&apos;&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;json&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token key atrule&quot;&gt;nameOnCard&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;{{ nameOnCard }}&apos;&lt;/span&gt;
            &lt;span class=&quot;token key atrule&quot;&gt;language&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;en&apos;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;대략 요약하자면, &lt;code&gt;600초간 초당 10개의 /citizen-card POST 요청&lt;/code&gt;을 보내는 테스트입니다. 요청에 필요한 nameOnCard 값은 &lt;strong&gt;artillery-functions.js의 generateRandomData&lt;/strong&gt; 함수에서 넘겨주며, 응답으로 에러가 날아오면 &lt;strong&gt;artillery-functions.js의 logError&lt;/strong&gt; 함수가 에러 로그를 남깁니다.&lt;/p&gt;
&lt;p&gt;위의 숫자들을 바꿔가며 요리조리 테스트해본 결과... 2코어 CPU + 메모리 512MB짜리 노드 9개로 고작 &lt;strong&gt;10 RPS&lt;/strong&gt;(Request Per Second)를 겨우 서빙할 수 있다는 절망적인 결과를 확인했습니다😭😭 하나의 요청을 처리하는 데에는 대략 1,400~1,600ms까지도 걸렸으니, 로직의 병목 구간을 찾는 것이 급선무였습니다.&lt;/p&gt;
&lt;h2 id=&quot;nodejs-profiler로-분석하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#nodejs-profiler%EB%A1%9C-%EB%B6%84%EC%84%9D%ED%95%98%EA%B8%B0&quot; aria-label=&quot;nodejs profiler로 분석하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Node.js Profiler로 분석하기&lt;/h2&gt;
&lt;p&gt;우리의 목표는 docker 컨테이너 위에 떠 있는 서버를 프로파일링하여 병목 구간을 찾아내는 것입니다. node에는 이를 가능하게 해주는 &lt;code&gt;--inspect&lt;/code&gt; 옵션이 있습니다. 먼저, &lt;strong&gt;서버를 docker 컨테이너로 띄우고&lt;/strong&gt; 곧바로 &lt;strong&gt;node를 실행&lt;/strong&gt;해봅시다!&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; run &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;9229&lt;/span&gt;:9229 &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;80&lt;/span&gt;:80 &lt;span class=&quot;token parameter variable&quot;&gt;-it&lt;/span&gt; random-cookie-test /bin/bash

&lt;span class=&quot;token comment&quot;&gt;# docker 컨테이너 안에서&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;node&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-r&lt;/span&gt; ts-node/register/transpile-only &lt;span class=&quot;token parameter variable&quot;&gt;--inspect&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0.0&lt;/span&gt;.0.0:9229 src/index.ts&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-p 9229:9229 -p 80:80&lt;/code&gt;: 80번 포트는 API 호출용, 9229번 포트는 inspector를 붙이는 용으로 열어주었습니다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-r ts-node/register/transpile-only&lt;/code&gt;: 저는 typescript를 사용했기에 필요했던 옵션입니다. Javascript 코드라면 생략 가능합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이제 크롬 inspector로 프로파일링을 진행해봅시다. 크롬을 키고 주소창에 &lt;code&gt;chrome://inspect&lt;/code&gt;를 입력하시면 아래 이미지를 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1787px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 36.66666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAABcSAAAXEgFnn9JSAAAA/0lEQVR42oWQPU7EMBCFcwm61FwFOg7BQTgMNXfgAIiGjoYixXodh8T/9hh7mFk2aFGAfdLTaKynb57cLVo35zzKcULnHYYYMaZ0MORS8Eu3d/ev/cOTxceXqe3FDkc1YaJMCIFn4xAAiM5a00pF3M8RF20QaBlHiVJKznwDh0H1PhWcF9uMMWidw5zzwQwk8RQdXWgpZRTjjDu5oAuAtVbkwClQKdV/UOEQfMOt1jfRUc2aMzQhVdPGtAzQjgE2nAILAb0/A1wXLsStjs1Wld+AtW5yW+Af+gHkr4gxnm14/Y+vyDfkC631ZfAW5bvNz28RrItAjYGOsDM3pjl8Aqf9EcejC1+SAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/3d4358013b35d548feba0442f7a658ba/33ecc/chrome_inspector.webp 360w,
/static/3d4358013b35d548feba0442f7a658ba/263a4/chrome_inspector.webp 480w,
/static/3d4358013b35d548feba0442f7a658ba/0c8fb/chrome_inspector.webp 640w,
/static/3d4358013b35d548feba0442f7a658ba/521f7/chrome_inspector.webp 860w,
/static/3d4358013b35d548feba0442f7a658ba/cbd37/chrome_inspector.webp 1200w,
/static/3d4358013b35d548feba0442f7a658ba/677e9/chrome_inspector.webp 1787w&quot;
              sizes=&quot;(max-width: 1787px) 100vw, 1787px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/3d4358013b35d548feba0442f7a658ba/53918/chrome_inspector.png 360w,
/static/3d4358013b35d548feba0442f7a658ba/9aebd/chrome_inspector.png 480w,
/static/3d4358013b35d548feba0442f7a658ba/0f09e/chrome_inspector.png 640w,
/static/3d4358013b35d548feba0442f7a658ba/3e3fe/chrome_inspector.png 860w,
/static/3d4358013b35d548feba0442f7a658ba/64756/chrome_inspector.png 1200w,
/static/3d4358013b35d548feba0442f7a658ba/937f1/chrome_inspector.png 1787w&quot;
            sizes=&quot;(max-width: 1787px) 100vw, 1787px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/3d4358013b35d548feba0442f7a658ba/937f1/chrome_inspector.png&quot;
            alt=&quot;chrome inspector&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;왼쪽 Remote Target에 우리가 방금 띄운 node 서버가 잡히는 것을 확인할 수 있습니다. 또, 그 밑의 inspect를 누르면 오른쪽의 크롬 debugger 창과 컨테이너에 &lt;code&gt;Debugger attached.&lt;/code&gt; 로그 메시지를 확인 할 수 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;이제 Start를 누르고&lt;/li&gt;
&lt;li&gt;API 요청을 쏘고&lt;/li&gt;
&lt;li&gt;Stop 버튼을 누르면&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 34.166666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAAAsTAAALEwEAmpwYAAABXElEQVR42n2Py3KbQBBF+f8PySpVSWxn400q2aT80AsJhECoZJ42g0BIgCxMctwg26WVF7fOdM/0mRlt8PsL5vAry9lPnNklC/0Hi8l3nOkFtn7R13bf+4Y7vCIYXwt/Mfr7h8XoXjLAHg+Z3t/geWs0fWBgWXMmusV0JpyaQpP53GIwMjAME116S3vC4u4W926MNzF4sMbEK6Gjo9YW89kNq9BFc/yMvG7xVIkv6RjlB5KiwQm3PMraTysOzX/q55K0SsjLimMLdfOPqmkREKgIN7bR3Kgg2VasAoUbKpZBwjreEKod5irAT3K5WbGvX6iPnVQkwkrYryXHBqKnDa4Xoy29R6pDSbZNyYtUmLDdZezKgjSLKPa59JUMtiI4CXvxu7wTvkCc7XAihdYJDnJD96WPvA2c9z4k53kTPotQZXvWfojWHe6ffzxtvudUt2f1J0J50KYQYejxCldxCmNo8TplAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/c6cf27e05279a9f31f9d5ee4e0d217d2/33ecc/chrome-before.webp 360w,
/static/c6cf27e05279a9f31f9d5ee4e0d217d2/263a4/chrome-before.webp 480w,
/static/c6cf27e05279a9f31f9d5ee4e0d217d2/0c8fb/chrome-before.webp 640w,
/static/c6cf27e05279a9f31f9d5ee4e0d217d2/521f7/chrome-before.webp 860w,
/static/c6cf27e05279a9f31f9d5ee4e0d217d2/cbd37/chrome-before.webp 1200w,
/static/c6cf27e05279a9f31f9d5ee4e0d217d2/548e7/chrome-before.webp 2400w,
/static/c6cf27e05279a9f31f9d5ee4e0d217d2/84df7/chrome-before.webp 2892w&quot;
              sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/c6cf27e05279a9f31f9d5ee4e0d217d2/53918/chrome-before.png 360w,
/static/c6cf27e05279a9f31f9d5ee4e0d217d2/9aebd/chrome-before.png 480w,
/static/c6cf27e05279a9f31f9d5ee4e0d217d2/0f09e/chrome-before.png 640w,
/static/c6cf27e05279a9f31f9d5ee4e0d217d2/3e3fe/chrome-before.png 860w,
/static/c6cf27e05279a9f31f9d5ee4e0d217d2/64756/chrome-before.png 1200w,
/static/c6cf27e05279a9f31f9d5ee4e0d217d2/98e2c/chrome-before.png 2400w,
/static/c6cf27e05279a9f31f9d5ee4e0d217d2/4acd5/chrome-before.png 2892w&quot;
            sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/c6cf27e05279a9f31f9d5ee4e0d217d2/98e2c/chrome-before.png&quot;
            alt=&quot;chrome before&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;이렇게 첫 flamegraph를 확인할 수 있습니다! 왼쪽의 뾰족뾰족한 기둥들은 시민권에 필요한 이미지들을 하나씩 그리는 부분, 그리고 그 옆의 이미지들을 합치는 부분, S3 버킷에 이미지를 업로드하는 부분입니다. 여기서 확인할 수 있는 사실은, 대부분의 시간이 그림을 불러오고 조립해서 시민권을 그려내는 데 사용된다는 것이었습니다. 하지만 정확히 왜 그림을 그리는데 이토록 오래 걸리는지는 알 수 없었습니다. 자세하게 파고들고 싶어도 단순히 그리기 함수 호출에 시간이 많이 걸린다 외에는 알 수 있는 사실이 없었습니다. 사용하고 있는 라이브러리 node-canvas의 설명을 보니 Cairo 라는 백엔드를 사용하고 있었습니다. Cairo는 C로 구현된 라이브러리이므로 더 로우레벨로 내려가서.. C로 작성된 코드까지 파고들어 병목을 확인하고 싶었습니다.&lt;/p&gt;
&lt;h2 id=&quot;linux-perf로-분석하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#linux-perf%EB%A1%9C-%EB%B6%84%EC%84%9D%ED%95%98%EA%B8%B0&quot; aria-label=&quot;linux perf로 분석하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Linux Perf로 분석하기&lt;/h2&gt;
&lt;p&gt;다시 우리의 목표는 서버를 띄운 docker 컨테이너 안에서 linux perf(이하 perf)를 통해, 서버의 병목 구간을 찾아내는 것입니다.&lt;/p&gt;
&lt;p&gt;가장 먼저 할 일은 서버를 &lt;strong&gt;docker 컨테이너로 띄우기&lt;/strong&gt;입니다! docker 이미지는 본래 서버 배포해 사용하기 위해 만들어둔 이미지를 그대로 사용하였습니다. 그리고, 컨테이너 안에서 node와 perf를 동시에 실행하기 위해 터미널 두 개로 컨테이너에 접속했습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;
&lt;span class=&quot;token comment&quot;&gt;# 터미널 1에서&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; run &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;80&lt;/span&gt;:80 &lt;span class=&quot;token parameter variable&quot;&gt;-it&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--privileged&lt;/span&gt; random-cookie-test /bin/bash
&lt;span class=&quot;token comment&quot;&gt;# random-cookie-test는 이미지 이름&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 터미널 2에서&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;ps&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;exec&lt;/span&gt; 0579 &lt;span class=&quot;token parameter variable&quot;&gt;-it&lt;/span&gt; /bin/bash
&lt;span class=&quot;token comment&quot;&gt;# 0579는 터미널 1에서 띄운 컨테이너의 id 일부&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--privileged&lt;/code&gt;: 이 옵션이 있어야 perf가 필요한 곳에 모두 접근할 수 있습니다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;컨테이너를 띄웠으니 perf를 설치해야 합니다. 그런데 perf가 OS 버전마다 다른 버전을 사용해야 하여 설치가 어려울 수 있습니다&lt;span style=&quot;color:grey&quot;&gt;(저는 여기서 많은 삽질을 했습니다😢)&lt;/span&gt;. 저는 아래 방법으로 perf 설치에 성공했습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; update
&lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; linux-tools-common linux-tools-generic
&lt;span class=&quot;token function&quot;&gt;ln&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-fs&lt;/span&gt; /usr/lib/linux-tools/*/perf /usr/bin/perf

&lt;span class=&quot;token comment&quot;&gt;#perf가 잘 설치됐는지 확인!&lt;/span&gt;
perf &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;perf는 리눅스 시스템의 커널 성능을 측정할 수 있는 도구입니다. perf는 커널의 다양한 요소를 모니터링할 수 있는데요. 자세한 내용은 &lt;a href=&quot;http://www.brendangregg.com/perf.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;링크&lt;/a&gt;를 통해 확인하실 수 있고, 우리가 설치한 perf가 어떤 요소들을 모니터링하는지는 &lt;code&gt;perf list&lt;/code&gt; 명령어를 통해 확인할 수 있습니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;이제 준비물들이 다 갖추어졌으니 컨테이너 안에서 성능을 테스트하는 일만 남았습니다! 앞서 열어놓은 두 개의 터미널에서 아래의 명령어를 실행하면 됩니다. (사실 저는 이 이전에 &lt;code&gt;docker cp&lt;/code&gt; 명령어로 node 서버 실행에 필요한 환경변수를 관리하는 파일을 옮겨주었으니 참고해주세요.)&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;#터미널 1에서&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;node&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-r&lt;/span&gt; ts-node/register/transpile-only --perf-basic-prof src/index.ts

&lt;span class=&quot;token comment&quot;&gt;#터미널 2에서&lt;/span&gt;
perf record &lt;span class=&quot;token parameter variable&quot;&gt;-i&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-g&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-e&lt;/span&gt; cycles:u &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;pgrep &lt;span class=&quot;token parameter variable&quot;&gt;-n&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;node&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;`&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# API 요청 쏘고 control+c로 perf record 종료&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;node -r ts-node/register/transpile-only --perf-basic-prof src/index.ts&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-r ts-node/register/transpile-only&lt;/code&gt;: 저는 typescript를 사용했기에 필요했던 옵션입니다. Javascript 코드라면 생략 가능합니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--perf-basic-prof&lt;/code&gt;: perf가 javascript symbol을 매핑하는데 필요한 옵션입니다. 이 옵션을 사용하지 않으면 나중에 빈칸이 뻥뻥 뚫리거나 모든 칸이 **[unknown]**으로 칠해진 flamegraph를 만날 수 있습니다. 이 옵션이 필요한 이유는 v8 엔진이 &lt;strong&gt;JIT 방식&lt;/strong&gt;의 컴파일을 하여 &lt;strong&gt;perf가 javascript symbol들을 읽을 수 없기&lt;/strong&gt; 때문인데요. &lt;code&gt;--perf-basic-prof&lt;/code&gt; 옵션은 javascript symbol과 메모리 주소를 매핑해주는 /tmp/perf-{PID}.map 파일을 생성하게 함으로써 이를 해결합니다. 자세한 내용을 &lt;a href=&quot;http://www.brendangregg.com/blog/2014-09-17/node-flame-graphs-on-linux.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;링크&lt;/a&gt;를 통해 확인하실 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;perf record -i -g -e cycles:u -p `pgrep -n node`&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-p `pgrep -n node` &lt;/code&gt;: 원래 -p는 PID를 넘겨주는 옵션입니다. 저는 node 서버의 PID를 넘겨주기 위해 해당 방법을 사용했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이렇게 하고 나면 perf record는 &lt;strong&gt;perf.data&lt;/strong&gt;라는 파일을 남깁니다. 이 파일을 vim으로 열어보면 사람이 읽을 수 없는 외계어로 되어있지만 &lt;code&gt;perf report&lt;/code&gt;를 이용하면 이 내용을 확인할 수 있습니다. 사실 이 perf report를 확인하면 서버의 병목 구간을 이미 확인하는 데에는 성공했지만 이대로는 한눈에 결과가 들어오지 않으니, flamegraph로 이 결과를 확인해봅시다.&lt;/p&gt;
&lt;h2 id=&quot;flamegraph-그리기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#flamegraph-%EA%B7%B8%EB%A6%AC%EA%B8%B0&quot; aria-label=&quot;flamegraph 그리기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Flamegraph 그리기&lt;/h2&gt;
&lt;p&gt;Flamegraph 그리기는 &lt;a href=&quot;https://github.com/brendangregg/FlameGraph&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;이 레포지토리&lt;/a&gt;를 이용했습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;apt&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; clone https://github.com/brendangregg/FlameGraph.git

perf script &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; out.perf-folded
&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; FlameGraph
./stackcollapse-perf.pl &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;/out.perf-folded &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; ./flamegraph.pl &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;/graph.svg

&lt;span class=&quot;token comment&quot;&gt;#컨테이너 밖에서&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;cp&lt;/span&gt; 0579:/app/graph.svg &lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# 0579는 터미널 1에서 띄운 컨테이너의 id 일부&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 perf 프로파일링 결과를 graph.svg로 변환하고, 컨테이너 밖으로 빼내는데 성공했습니다! 그리고 시민권 발급 서비스의 flamegraph는...&lt;/p&gt;
&lt;div style=&quot;
    width: 100%;
    height: 1000px;
    overflow: scroll;
&quot;&gt;
    &lt;iframe 
    name=&quot;img&quot;
    src=&quot;https://tech.devsisters.com/perf-citizen-card-before.svg&quot;
    style=&quot;width:1200px; height:2118px; border:0; border-radius: 4px; overflow:hidden;&quot;
&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;짜잔! 이렇게 나왔습니다! (마우스를 올려보세요) 이제 이 flamegraph를 분석해야 했는데요. &lt;code&gt;bits_image_fetch_convolution_affine_reflect_a8r8g8b8&lt;/code&gt;라는 함수가 약 30%를 차지하고 있는 것을 확인할 수 있습니다. 이 함수는 제가 직접 이름 짓거나 호출한 함수는 아니기에 node-canvas가 어디선가 호출하고 있는 함수라는 것을 짐작할 수 있습니다. 아쉽게도 node-canvas가 언제 이 함수를 호출하는지는 찾지 못하였습니다. 다만, convolution은 흔히 이미지 프로세싱에 사용되는 연산의 이름이기에 언제 호출되었는지를 유추할 수 있었습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; canvas &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createCanvas&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;CARD_WIDTH&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;CARD_HEIGHT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ctx &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; canvas&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getContext&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;2d&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; cardBackground &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
cardBackground&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;src &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;data:image/png;base64,iVBORw0KGgoAAAANS...&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;drawImage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;cardBackground&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;CARD_WIDTH&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;CARD_HEIGHT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;바로 이 부분이었습니다! ctx.drawImage 함수는 이름에서도 알 수 있듯이 canvas 객체 위에 이미지를 그리는 함수입니다. &lt;a href=&quot;https://github.com/freedesktop/pixman/blob/d93ec5713821106df6d57952dcf169308c7c4f0e/pixman/pixman-bits-image.c&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;공식 문서&lt;/a&gt;를 참고하면 parameter로는 3개 또는 5개를 받을 수 있는데요.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;javascript&quot;&gt;&lt;pre class=&quot;language-javascript&quot;&gt;&lt;code class=&quot;language-javascript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;drawImage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;image&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; dx&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; dy&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt; ctx&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;drawImage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;image&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; dx&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; dy&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; dWidth&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; dHeight&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;dx, dy는 이미지를 canvas 위에 그리기 시작할 좌푯값, dWidth, dHeight는 이미지의 사이즈를 의미합니다. 만약 원래 이미지의 크기가 dWidth, dHeight와 다르다면, 이 안에서 이미지의 resizing이 일어나고 있었습니다. 실제 제 코드에서도 원본 이미지가 dWidth, dHeight보다 커서 이미지의 축소가 일어나고 있었습니다.&lt;/p&gt;
&lt;p&gt;그래서 코드에 사용된 모든 drawImage 함수들의 dWidth, dHeight를 원본 이미지 값보다 더 크게 주어 보았더니...30%를 잡아먹던 &lt;code&gt;bits_image_fetch_convolution_affine_reflect_a8r8g8b8&lt;/code&gt;가 사라지고, &lt;code&gt;bits_image_fetch_bilinear_affine_reflect_a8r8g8b8&lt;/code&gt; 함수가 그 자리를 차지하고 있었습니다! &lt;span style=&quot;color:grey&quot;&gt;(convolution이 bilinear로 바뀌었습니다)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;pixman이라는 라이브러리의 아마도 관련 함수일 것으로 추정되는 비슷한 이름의 함수 &lt;a href=&quot;https://github.com/freedesktop/pixman/blob/d93ec5713821106df6d57952dcf169308c7c4f0e/pixman/pixman-bits-image.c&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;코드&lt;/a&gt;를 보아하니, image의 크기에 상관없이 2중 for문으로 모든 픽셀을 순회하며 연산을 수행하는 듯 보였습니다. 시민권 발급 서비스는 나중에 시민권을 실물로 인쇄할 목적으로 꽤 고해상도의 이미지를 사용하고 있었고, 이 이미지들을 쿠키의 눈, 입, 머리 등에 대해 10차례가 넘게 resizing하고 있었으니 병목이 생기고 있었던 것입니다. 실제 소스 이미지들은 1000x1000정도의 큰 이미지였으나, 시민권에 들어가는 이미지의 크기는 300x400면 충분하였기에, 모든 소스 이미지들을 미리 resizing하고, drawImage 함수에 넘겨주던 dWidth, dHeight를 최대한 지우고 나니...&lt;/p&gt;
&lt;div style=&quot;
    width: 100%;
    height: 1000px;
    overflow: scroll;
&quot;&gt;
    &lt;iframe 
    name=&quot;img&quot;
    src=&quot;https://tech.devsisters.com/perf-citizen-card-after.svg&quot;
    style=&quot;width:1200px; height:2118px; border:0; border-radius: 4px; overflow:hidden;&quot;
&gt;&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;시간을 30% 잡아먹던 &lt;code&gt;bits_image_...&lt;/code&gt; 함수가 사라진 것을 확인할 수 있었습니다. 위의 flamegraph와 직접 비교하기는 어렵지만, 왼쪽의 가장 굵은 기둥의 두께를 통해 얼마나 개선되었는지 가늠할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1110px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 70.83333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAABcSAAAXEgFnn9JSAAACy0lEQVR42rWT20tUQRjAD0E9REH0VNjFSkIweil6iy5/Qi9C9NJLEERQUFFUKpZrWVpZQcESVJh0tyzDNi+rLuV61vvKpqvu/Xh2V5d0vezumV/juqFUb9HA78wwzPdjvu87owBTEkOOlJFKLJBMpFLJuSUkfmNh/9d5GZ+UCElckZ8Z/n2IzDwrhUZamIiHGHeVE/teif+LidGPhQy+uchQbRG+tlK8raW4G0wMN5nw2q7ht5Xxw31Pni+Xsf6M0JBCI5UWzk700Fmehb04h+pty7i/UuHmKgXTWoWy7QpF2Qpn5Pr0aoWTKxSu5i7n6908WkxZMlZdEBqpReG03kfz8Z28yMvh1e4dvD2Yy7N92Tzcu4WKPRsp2ZnF7QNbuSX3TLs2cDlnPQVb11N1ZDuTQUdaaKSFmZTnYgNEPLUMRXtxNF6nzdmIGgsQHetmcsxBxPcNz0AjIXcLMc1BeMSKPtSK1l/NbMyZuaGxKCQZIqrX02Ipp+ZcHmfz93PnaweB8QHikU58bisNtWb67O/w9n9iRH0ueU2grwojEVpSw4xQzIwyFm3j0snDVBcfpvToIa4VFxCecMqbt+IfasbzvQmXWkOXxcyg/Tm6u1nygdScb2lTMimHe3C3n+LeyydYzYWoL4po6PiEFnIQcn1msLuOXvt7bB8fYK2pRG2qxjtgQVdPMR1x/KUpslZ9j7L55qrA+uQC9TdP8Ng9jDYZJC7TDgc60bwORjvq8Hd+Rhu24LFdxmneQlxr/1M4FVRxv89h5PU6VPMmPhxbw7uqE4R8T4n0nieknkHrvkOwvQBPfT6jdbvxWnIZfLWZqYBtUSiEMZ35sYXedUP42q4IvaNETDhLRKSrUER6TGLcVSGi/WVCd5QIXS0Umv2i5IrEJMI9N0RyOmhkajjzP54esflFujlGckY+djn/IvkXEn8ghJjPcm7e9RNBNsIma7uRtAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/725c62b867614dc48c1bb5af8746e7c0/33ecc/card-june.webp 360w,
/static/725c62b867614dc48c1bb5af8746e7c0/263a4/card-june.webp 480w,
/static/725c62b867614dc48c1bb5af8746e7c0/0c8fb/card-june.webp 640w,
/static/725c62b867614dc48c1bb5af8746e7c0/521f7/card-june.webp 860w,
/static/725c62b867614dc48c1bb5af8746e7c0/d209b/card-june.webp 1110w&quot;
              sizes=&quot;(max-width: 1110px) 100vw, 1110px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/725c62b867614dc48c1bb5af8746e7c0/53918/card-june.png 360w,
/static/725c62b867614dc48c1bb5af8746e7c0/9aebd/card-june.png 480w,
/static/725c62b867614dc48c1bb5af8746e7c0/0f09e/card-june.png 640w,
/static/725c62b867614dc48c1bb5af8746e7c0/3e3fe/card-june.png 860w,
/static/725c62b867614dc48c1bb5af8746e7c0/ffc38/card-june.png 1110w&quot;
            sizes=&quot;(max-width: 1110px) 100vw, 1110px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/725c62b867614dc48c1bb5af8746e7c0/ffc38/card-june.png&quot;
            alt=&quot;개선 성공!&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;개선 성공!&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h2 id=&quot;마무리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EB%AC%B4%EB%A6%AC&quot; aria-label=&quot;마무리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;마무리&lt;/h2&gt;
&lt;p&gt;이 작업 후, 비슷한 리소스 조건에서 artillery 테스트를 다시 해보았더니 &lt;strong&gt;70RPS&lt;/strong&gt;까지 서버가 버텨주는 것을 확인하였습니다. 원래 10RPS를 겨우 버텨냈으니 &lt;strong&gt;무려 7배&lt;/strong&gt;나 많은 부하를 견딜 수 있게 된 것이었죠. 실행 시간도 요청 하나에 1,400~1,600ms 정도 걸리던 것이 700~800ms까지 감소하는 것을 확인하였습니다. Node.js profiler로 다시 분석해보았을 때도 개선 이전의 flamegraph와의 차이를 한눈에 알아볼 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1235px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 47.5%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAABcSAAAXEgFnn9JSAAABMElEQVR42pVQ/U+DMBDt///PmBh/1GQBTIzLpjNRsuyDOArMjGEc+6LQPnsFtuqmyS68e7279vVR1rl5wsALMOwmmDxnmA4+0b1/x9V1D7d3I7heBMcN4bocnl63bMNxQvXwmGIczDnrdlxEvSHiIcfE9xG/jPDhjxEFr8gXM2RJgCyeYp1ybJah5lAz/4F8EapilWC/zzlb5gmqSuBNDyZzH1/rFJUsoZTSgAlaSylNrdpmE3oXICkp7Io1Z+18s83NIQpzWJo9tQB9um5BdQtTG0Foh3sSPN5YuyI36tRJMzvXN6TTthBHQXvzRYINlTqtBAniV5BDdblDSrui4IwM2TjEX/36oa0+3a8qEiyEmLFTg6cu/nNo/7kQpXGY2NBXGpzr23zo1xyLskw1+t9vSgYl4Y28xQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/1cd59dfb04adb27599f16c6bb12e18a0/33ecc/chrome-after.webp 360w,
/static/1cd59dfb04adb27599f16c6bb12e18a0/263a4/chrome-after.webp 480w,
/static/1cd59dfb04adb27599f16c6bb12e18a0/0c8fb/chrome-after.webp 640w,
/static/1cd59dfb04adb27599f16c6bb12e18a0/521f7/chrome-after.webp 860w,
/static/1cd59dfb04adb27599f16c6bb12e18a0/cbd37/chrome-after.webp 1200w,
/static/1cd59dfb04adb27599f16c6bb12e18a0/01365/chrome-after.webp 1235w&quot;
              sizes=&quot;(max-width: 1235px) 100vw, 1235px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/1cd59dfb04adb27599f16c6bb12e18a0/53918/chrome-after.png 360w,
/static/1cd59dfb04adb27599f16c6bb12e18a0/9aebd/chrome-after.png 480w,
/static/1cd59dfb04adb27599f16c6bb12e18a0/0f09e/chrome-after.png 640w,
/static/1cd59dfb04adb27599f16c6bb12e18a0/3e3fe/chrome-after.png 860w,
/static/1cd59dfb04adb27599f16c6bb12e18a0/64756/chrome-after.png 1200w,
/static/1cd59dfb04adb27599f16c6bb12e18a0/a85d4/chrome-after.png 1235w&quot;
            sizes=&quot;(max-width: 1235px) 100vw, 1235px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/1cd59dfb04adb27599f16c6bb12e18a0/a85d4/chrome-after.png&quot;
            alt=&quot;원래 보였던 수많은 불기둥이 없어졌다!&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;원래 보였던 수많은 불기둥이 없어졌다!&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;이렇게&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Artillery로 서버의 성능을 테스트하고&lt;/li&gt;
&lt;li&gt;Linux perf를 통해 docker 컨테이너 환경에서 서버를 프로파일링하고&lt;/li&gt;
&lt;li&gt;Flamegraph로 서버의 병목 구간을 확인하여&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;시민권 이미지 서버의 성능을 획기적으로 개선할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;저희 웹서비스셀은 이외에도 &lt;code&gt;어떤 환경에서나 통용되는 웹 기술을 통해 데브시스터즈 안팎으로 접근성을 향상시킨다&lt;/code&gt;는 목표하에 다양한 업무를 수행하고 있습니다.
저희 셀이 더 궁금하시다면 &lt;a href=&quot;https://careers.devsisters.com/position/detail/?jobPosition=75&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;데브시스터즈 채용 페이지&lt;/a&gt;를 확인해주세요.&lt;/p&gt;
&lt;p&gt;감사합니다🙏&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[CSS in JS 라이브러리에서 Typesafe하게 Theme 관리하기]]></title><description><![CDATA[CSS in JS 라이브러리를 사용할 때 React Context API를 사용해 Theme을 Typesafe하게 관리하는 방법을 소개합니다.]]></description><link>https://tech.devsisters.com/posts/react-extend-theme</link><guid isPermaLink="false">https://tech.devsisters.com/posts/react-extend-theme</guid><pubDate>Tue, 16 Feb 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;데브시스터즈에서는 React 기반의 GatsbyJS와 Typescript를 이용해 웹 사이트를 관리하고 있으며 프로젝트의 스타일링 방법론으로는 &lt;strong&gt;CSS in JS&lt;/strong&gt; 방식을 사용하고 있습니다. Typescript 기반의 React 어플리케이션에서 &lt;strong&gt;CSS in JS&lt;/strong&gt;를 사용하는 방식과 React의 Context API를 사용해 효과적으로의 Theme를 관리하는 방법을 소개합니다.&lt;/p&gt;
&lt;h2 id=&quot;css-in-js란&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#css-in-js%EB%9E%80&quot; aria-label=&quot;css in js란 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;CSS in JS란?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;CSS in JS&lt;/strong&gt;는 Javascript 코드에서 CSS를 작성하는 방법을 말합니다. Facebook의 개발자인 vjeux는 아래와 같은 CSS 관리의 문제점을 &lt;strong&gt;CSS in JS&lt;/strong&gt;를 도입함으로써 해결한 사례를 설명했습니다. 자세한 내용은 &lt;a href=&quot;http://blog.vjeux.com/2014/javascript/react-css-in-js-nationjs.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;링크&lt;/a&gt;에서 확인할 수 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Global namespace&lt;/li&gt;
&lt;li&gt;Dependencies&lt;/li&gt;
&lt;li&gt;Dead Code Elimination&lt;/li&gt;
&lt;li&gt;Minification&lt;/li&gt;
&lt;li&gt;Sharing Constants&lt;/li&gt;
&lt;li&gt;Non-deterministic Resolution&lt;/li&gt;
&lt;li&gt;Isolation&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;대표적인 &lt;strong&gt;CSS in JS&lt;/strong&gt; 라이브러리로는 &lt;a href=&quot;https://cssinjs.org/?v=v10.4.0&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;JSS&lt;/a&gt;, &lt;a href=&quot;https://styled-components.com/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;styled-components&lt;/a&gt;, &lt;a href=&quot;https://emotion.sh/docs/introduction&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;emotion&lt;/a&gt;이 존재합니다. 현재 프로젝트에서는 &lt;strong&gt;emotion&lt;/strong&gt;을 사용해 개발을 진행하고 있으며 &lt;strong&gt;emotion&lt;/strong&gt;의 대표적인 사용 방법은 아래와 같습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;jsx&quot;&gt;&lt;pre class=&quot;language-jsx&quot;&gt;&lt;code class=&quot;language-jsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;react&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; ReactDOM &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;react-dom&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; styled &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;@emotion/styled&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// object 스타일&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; Container &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; styled&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;180&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;180&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;backgroundColor&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;#fd7622&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// template literal 스타일&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; Title &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; styled&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;h1&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;
  color: #ffffff;
&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;Example&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Container&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Title&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;Devsisters&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Title&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Container&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; rootElement &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;root&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
ReactDOM&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;render&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Example&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; rootElement&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;emotion&lt;/strong&gt;은 &lt;code&gt;object&lt;/code&gt;나 &lt;code&gt;template literal&lt;/code&gt;로 자바스크립트 코드를 작성해 스타일을 표현할 수 있습니다. 또한 자바스크립트 코드를 통해 실제 CSS가 생성되기 때문에 가상선택자와 같은 CSS의 기능을 대부분 이용할 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;css-in-js-라이브러리에서의-theme&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#css-in-js-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EC%97%90%EC%84%9C%EC%9D%98-theme&quot; aria-label=&quot;css in js 라이브러리에서의 theme permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;CSS in JS 라이브러리에서의 Theme&lt;/h2&gt;
&lt;p&gt;React에서 사용할 수 있는 대부분의 &lt;strong&gt;CSS in JS&lt;/strong&gt; 라이브러리는 테마(Theme)와 관련된 기능을 제공합니다.&lt;/p&gt;
&lt;figure&gt;
  &lt;video autoplay loop muted playsinline&gt;
    &lt;source src=&quot;/a8a4e58389298014a14a5b7bf15e8abc/theme-example.mp4&quot; type=&quot;video/mp4&quot;&gt;
  &lt;/video&gt;
  &lt;figcaption&gt;데브시스터즈에서는 이런식으로 테마 기능을 사용합니다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;emotion&lt;/strong&gt;은 React의 Context API를 기반으로 만들어진 &lt;code&gt;ThemeProvider&lt;/code&gt;를 제공하며 &lt;a href=&quot;https://emotion.sh/docs/theming&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;emotion 공식문서&lt;/a&gt;가 소개하는 &lt;code&gt;ThemeProvider&lt;/code&gt;를 이용하는 방법 3가지 중 데브시스터즈에서는 아래의 &lt;code&gt;styled&lt;/code&gt; 기법을 사용합니다.&lt;/p&gt;
&lt;iframe src=&quot;https://codesandbox.io/embed/emotion-playground-forked-z2jjd?codemirror=1&amp;fontsize=14&amp;hidenavigation=1&amp;theme=dark&quot;
  style=&quot;width:100%; height:450px; border:0; border-radius: 4px; overflow:hidden;&quot;
  title=&quot;emotion playground (forked)&quot;
  allow=&quot;accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking&quot;
  sandbox=&quot;allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts&quot;
&gt;&lt;/iframe&gt;
&lt;p&gt;&lt;code&gt;ThemeProvider&lt;/code&gt;로 감싸진 자식 컴포넌트들은 &lt;code&gt;theme&lt;/code&gt;에 정의된 스타일을 &lt;code&gt;props&lt;/code&gt;으로 전달받아 사용할 수 있습니다. 위의 예제에서는 &lt;code&gt;Title&lt;/code&gt;, &lt;code&gt;SubTitle&lt;/code&gt;, &lt;code&gt;Link&lt;/code&gt;가 &lt;code&gt;theme&lt;/code&gt;에 존재하는 같은 &lt;code&gt;fontColor&lt;/code&gt;를 사용합니다. &lt;code&gt;ThemeProvider&lt;/code&gt;를 이용해 공통으로 사용하는 스타일을 관리할 수 있어 각각의 컴포넌트에 &lt;code&gt;color&lt;/code&gt; 속성값을 따로 지정할 때 보다 스타일을 관리하기 간편해졌습니다.&lt;/p&gt;
&lt;h2 id=&quot;typescript와-css-in-js-라이브러리에서의-theme&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#typescript%EC%99%80-css-in-js-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EC%97%90%EC%84%9C%EC%9D%98-theme&quot; aria-label=&quot;typescript와 css in js 라이브러리에서의 theme permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Typescript와 CSS in JS 라이브러리에서의 Theme&lt;/h2&gt;
&lt;p&gt;실제 코드를 작성할 때에는 타입스크립트를 사용하고 있음으로 &lt;code&gt;ThemeProvider&lt;/code&gt;에 전달되는 &lt;code&gt;theme&lt;/code&gt; 속성값들 또한 타입 검사를 진행해야 합니다. 아래와 같이 기존의 &lt;code&gt;ThemeProvider&lt;/code&gt;를 가져와 &lt;code&gt;theme&lt;/code&gt; 속성에 들어갈 타입을 의미하는 &lt;code&gt;Theme&lt;/code&gt; 타입을 정의해 확장하는 방식으로 사용할 수 있습니다.&lt;/p&gt;
&lt;iframe src=&quot;https://codesandbox.io/embed/naughty-haslett-843jx?codemirror=1&amp;fontsize=14&amp;hidenavigation=1&amp;module=%2Fsrc%2Futils%2Ftheme.tsx&amp;theme=dark&amp;highlights=25&quot;
  style=&quot;width:100%; height:450px; border:0; border-radius: 4px; overflow:hidden;&quot;
  title=&quot;naughty-haslett-843jx&quot;
  allow=&quot;accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking&quot;
  sandbox=&quot;allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts&quot;
&gt;&lt;/iframe&gt;
&lt;p&gt;또한 &lt;code&gt;styled&lt;/code&gt; 방식으로 생성되는 컴포넌트의 &lt;code&gt;props&lt;/code&gt;의 &lt;code&gt;theme&lt;/code&gt;에도 &lt;code&gt;ThemeProvider&lt;/code&gt;의 &lt;code&gt;theme&lt;/code&gt;와 동일한 타입을 전달하기 25번 라인의 코드를 추가했습니다. &lt;code&gt;styled.div&lt;/code&gt;와 같이 생성되는 컴포넌트의 &lt;code&gt;props&lt;/code&gt;의 &lt;code&gt;theme&lt;/code&gt;에도 지정해준 타입이 모두 적용되었음을 확인할 수 있습니다. 작성한 &lt;code&gt;ThemeProvider&lt;/code&gt;와 &lt;code&gt;styled&lt;/code&gt;는 기존의 Javascript로 작성된 코드와 동일하게 사용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1236px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 44.99999999999999%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAABYlAAAWJQFJUiTwAAABo0lEQVR42lVSWY7bMBTLGRpbsrVvlpc0g8xMEqBoBy3Q+5+JpZx0in7QT3qGqEdSh2IraizITrFGbNOEKQRkazDHgOIdkrWo7Om+I45Q3f9o/bHv0RMH6yw0DwUp8XOKOAeL9xxxrwWX6Pb1dcr7/jolnK3ee6/J87/HrWa88MxX8QWDIKFRlswCcRC4lASbMrRz8JzWp7Qj5Azj/QPOQ3HimAs0q2VPh4gqOWUjbJJnv+FkLC68tS4LIuWVkpEbWTugFIzWO6wxn9WRUA0DRqWx/CWUcsRp2nDZ5TrkZUUuBYnI9LPVxNomyrVy8sTehGXb4DhZ+++oqorjg/DYdVBa4r7StxSwcKqZvs6UvQaPNEpMmoHRu7Z2DMWL7hNpkPCyxyaPDw87EjY0GWtZsaYZW16whISNSV/nwstmvDOUVwbw1irDuS0VLy0YBrSaEYYXtSwOhj54TuNots4rNAk1n5EuJ4Ky4r9AWhitjryoyZWjQsfX0dATI/08DPw0BAbx/cdv4hdu1zu+fXwgr2ccOwFBBT0hnm9tr8+9FGKHeOIPxVH1pzvMzocAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/817e0f9f83b11d7fd4faec40283fc430/33ecc/0-typescript-themeprovider-type-error.webp 360w,
/static/817e0f9f83b11d7fd4faec40283fc430/263a4/0-typescript-themeprovider-type-error.webp 480w,
/static/817e0f9f83b11d7fd4faec40283fc430/0c8fb/0-typescript-themeprovider-type-error.webp 640w,
/static/817e0f9f83b11d7fd4faec40283fc430/521f7/0-typescript-themeprovider-type-error.webp 860w,
/static/817e0f9f83b11d7fd4faec40283fc430/cbd37/0-typescript-themeprovider-type-error.webp 1200w,
/static/817e0f9f83b11d7fd4faec40283fc430/0c34c/0-typescript-themeprovider-type-error.webp 1236w&quot;
              sizes=&quot;(max-width: 1236px) 100vw, 1236px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/817e0f9f83b11d7fd4faec40283fc430/53918/0-typescript-themeprovider-type-error.png 360w,
/static/817e0f9f83b11d7fd4faec40283fc430/9aebd/0-typescript-themeprovider-type-error.png 480w,
/static/817e0f9f83b11d7fd4faec40283fc430/0f09e/0-typescript-themeprovider-type-error.png 640w,
/static/817e0f9f83b11d7fd4faec40283fc430/3e3fe/0-typescript-themeprovider-type-error.png 860w,
/static/817e0f9f83b11d7fd4faec40283fc430/64756/0-typescript-themeprovider-type-error.png 1200w,
/static/817e0f9f83b11d7fd4faec40283fc430/baa93/0-typescript-themeprovider-type-error.png 1236w&quot;
            sizes=&quot;(max-width: 1236px) 100vw, 1236px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/817e0f9f83b11d7fd4faec40283fc430/baa93/0-typescript-themeprovider-type-error.png&quot;
            alt=&quot;Typescript가 ThemeProvider의 필요한 값의 타입을 알고있다.&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Typescript가 ThemeProvider의 필요한 값의 타입을 알고있다.&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;기존의 자바스크립트와 다른 점은 &lt;code&gt;theme&lt;/code&gt;의 타입을 Typescript가 알고 있기 때문에 타입에 지정되지 않은 속성을 추가하거나 지정된 속성의 값의 타입이 맞지 않는 경우 타입 오류가 발생하는 것을 볼 수 있습니다. 이처럼 &lt;code&gt;theme&lt;/code&gt;의 타입을 지정함으로써 불필요한 속성값이 &lt;code&gt;ThemeProvider&lt;/code&gt;에 들어가는 것을 방지할 수 있게 되었습니다.&lt;/p&gt;
&lt;h2 id=&quot;react-context-api를-사용해-theme-확장-유틸-만들기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#react-context-api%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4-theme-%ED%99%95%EC%9E%A5-%EC%9C%A0%ED%8B%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0&quot; aria-label=&quot;react context api를 사용해 theme 확장 유틸 만들기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;React Context API를 사용해 Theme 확장 유틸 만들기&lt;/h2&gt;
&lt;p&gt;현재 진행 중인 React 프로젝트는 &lt;a href=&quot;https://hackernoon.com/fractal-a-react-app-structure-for-infinite-scale-4dab943092af&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Fractal 패턴&lt;/a&gt;을 사용하여 폴더 구조를 관리하고 있습니다. 폴더의 깊이가 깊어지면서 하위 컴포넌트에서 부모 컴포넌트에서 사용하지 않던 테마 속성을 추가적으로 사용해야 할 경우 어플리케이션의 최상단에 존재하는 &lt;code&gt;ThemeProvider&lt;/code&gt;에는 많은 속성값이 생겨날 것입니다. 또한 아래의 그림과 같이 부모에서 사용하는 속성값과 자식에서 사용하는 테마 속성값을 다르게 주고 싶은 경우가 생길 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1348px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 68.05555555555554%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAABYlAAAWJQFJUiTwAAACdklEQVR42pWS23PSQBTG+Wv9W/TN8UkfHJ8644y1VVvtBZBLgZaWVrHacgmFkpILkAQImxDS5PNLQrXtgzNm5je7e/Z8357snlS7LUHXVSiDK5imjuiTZZn0oRgGRraN//lSlcohpI4EJfcG5vYzYKKienKKWu0M2oc1jJ4+QfDxNTB3EkUY/ttw6bnwSLAQCIQF37WhairGmoLg7RqCV8/hv3wBv3cFnwJ/uXgItdEY+QS3t0jZegmueQxhVCHMEzgcPTkdI4YFiOkxnBnj/T04nS04Nxk44yo5SqDWpW7ay0BYKlK3Wh6QcsBVkRRIHmFjF2FzD7jcTWjsJbQyQDu7ymN+94D7O7ymKnzrEO58REOJAkVmcAJYJkK1z+RfCLsXCb0GX6kDXDcZ/xnvQVcA06BmyoPqwM1XLGkaG/ptnlA/BuzI0EA4oFj6hrBTJ99pek7DFqs5j+MxOguwaSbxkHoFGJawtI5WFTa/MHgGOAtgOksqjU6PMeOq76qHOV6taTYXgDYEzmuAWrxXoZLD8nQbXvkdvMp6wuHGI96v2IR3tJnMo7wy+cE/tKpYmtEdjpESwyLEuAI3Kjki2lAycG/2k1FJw1WzcLUc3AFjg3QyZ+5iWsW8n4Vn8lH+GGoFTAdFOMMyhB5RgpC2IJobEK1NjqT9CaK7k8Qu1yE6n9lSFczlPOZaCZ5xz9Cm4URhC9inCGa1hNEBAj2PgNUH7MV4PS4zVkjgOmS+N6pgSq3HXvTjR4kq1HMYdvcfcp0h2dVIehHph/Eor7cPS87G9+cZB3BsPoptXMKZtNjlzb+YdzTuzR9xl2e1qG9jNr7Awp3hN0rXBb9oaJh8AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/f929f3053a7e7cc5edd12ebedbaa6523/33ecc/1-nested-theme-example.webp 360w,
/static/f929f3053a7e7cc5edd12ebedbaa6523/263a4/1-nested-theme-example.webp 480w,
/static/f929f3053a7e7cc5edd12ebedbaa6523/0c8fb/1-nested-theme-example.webp 640w,
/static/f929f3053a7e7cc5edd12ebedbaa6523/521f7/1-nested-theme-example.webp 860w,
/static/f929f3053a7e7cc5edd12ebedbaa6523/cbd37/1-nested-theme-example.webp 1200w,
/static/f929f3053a7e7cc5edd12ebedbaa6523/d3725/1-nested-theme-example.webp 1348w&quot;
              sizes=&quot;(max-width: 1348px) 100vw, 1348px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/f929f3053a7e7cc5edd12ebedbaa6523/53918/1-nested-theme-example.png 360w,
/static/f929f3053a7e7cc5edd12ebedbaa6523/9aebd/1-nested-theme-example.png 480w,
/static/f929f3053a7e7cc5edd12ebedbaa6523/0f09e/1-nested-theme-example.png 640w,
/static/f929f3053a7e7cc5edd12ebedbaa6523/3e3fe/1-nested-theme-example.png 860w,
/static/f929f3053a7e7cc5edd12ebedbaa6523/64756/1-nested-theme-example.png 1200w,
/static/f929f3053a7e7cc5edd12ebedbaa6523/f7833/1-nested-theme-example.png 1348w&quot;
            sizes=&quot;(max-width: 1348px) 100vw, 1348px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/f929f3053a7e7cc5edd12ebedbaa6523/f7833/1-nested-theme-example.png&quot;
            alt=&quot;중첩된 테마에 관한 그림 예시&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;중첩된 테마에 관한 그림 예시&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;위의 이미지에서 A 컴포넌트의 부모에는 아래 타입을 갖는 테마 객체를 요구하는 &lt;code&gt;ThemeProvider&lt;/code&gt;가 존재할 것입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Theme&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  color&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    bgColor&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    textColor&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;해당 &lt;code&gt;ThemeProvider&lt;/code&gt;의 테마 속성이 갖는 &lt;code&gt;bgColor&lt;/code&gt;와 &lt;code&gt;textColor&lt;/code&gt;는 A, B, C, D, E 컴포넌트에서 모두 사용할 수 있습니다. 그러나 A 컴포넌트의 하위에 있는 B 컴포넌트와 D 컴포넌트는 A 컴포넌트와 &lt;code&gt;theme&lt;/code&gt;에 존재하는 &lt;code&gt;bgColor&lt;/code&gt; 속성값이 다릅니다. 또한 최상위에 있는 &lt;code&gt;ThemeProvider&lt;/code&gt;에 존재하지 않는 &lt;code&gt;border&lt;/code&gt;라는 속성값이 존재합니다. B 컴포넌트와 D 컴포넌트가 사용하고 있는 테마의 속성값의 타입은 아래와 같아야 정상적으로 동작할 것입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Theme&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  color&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    bgColor&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    textColor&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    border&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이런 경우 React의 Context API를 사용해 &lt;code&gt;ThemeProvider&lt;/code&gt;를 확장할 수 있습니다. 무한하게 중첩이 가능한 Context API의 성질을 이용해 기존에 작성한 &lt;code&gt;ThemeProvider&lt;/code&gt;를 확장할 것입니다. 중첩되는 &lt;code&gt;ThemeProvider&lt;/code&gt;에 주입되는 테마의 타입은 아래와 같을 것입니다.&lt;/p&gt;
&lt;h4 id=&quot;srcutilsmakethemetsx&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#srcutilsmakethemetsx&quot; aria-label=&quot;srcutilsmakethemetsx permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;code&gt;src/utils/makeTheme.tsx&lt;/code&gt;&lt;/h4&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;MergedTheme&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;BaseTheme &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; object&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Theme &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; object&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; BaseTheme &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;
  Theme&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;여기서 &lt;code&gt;BaseTheme&lt;/code&gt;는 부모 &lt;code&gt;ThemeProvider&lt;/code&gt;의 테마 타입이며 &lt;code&gt;Theme&lt;/code&gt;는 부모에 합칠 새로운 &lt;code&gt;ThemeProvider&lt;/code&gt;의 속성값 타입입니다. Intersection Type을 표현하는 &lt;code&gt;&amp;#x26;&lt;/code&gt; 연산자를 이용해 두 테마의 타입을 합쳐줍니다. 그 후 기존의 새로운 &lt;code&gt;theme&lt;/code&gt;을 받아 부모의 &lt;code&gt;ThemeProvider&lt;/code&gt;와 합쳐주는 &lt;code&gt;makeTheme&lt;/code&gt; 함수를 작성합니다.&lt;/p&gt;
&lt;h4 id=&quot;srcutilsmakethemetsx-1&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#srcutilsmakethemetsx-1&quot; aria-label=&quot;srcutilsmakethemetsx 1 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;code&gt;src/utils/makeTheme.tsx&lt;/code&gt;&lt;/h4&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;makeTheme&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;BaseTheme &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; object&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Theme &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; object&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// BaseTheme와 Theme 타입을 병합한 타입인 NewTheme를 정의&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;NewTheme&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; MergedTheme&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;BaseTheme&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Theme&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// NewTheme 타입을 갖는 styled 함수를 생성&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; styled &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; _styled &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; CreateStyled&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;NewTheme&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// NewTheme 타입의 객체를 받는 ThemeProvider Context 생성&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ThemeProvider&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;FC&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; theme&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; NewTheme &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    theme&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    children&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;BaseThemeProvider&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;theme&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;theme&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;BaseThemeProvider&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; styled&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ThemeProvider &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;기존에 작성했던 &lt;code&gt;ThemeProvider&lt;/code&gt;를 &lt;code&gt;makeTheme&lt;/code&gt; 함수에서 반환하는 형태로 코드를 재사용해 사용할 수 있습니다. &lt;code&gt;styled&lt;/code&gt; 방식으로 생성되는 컴포넌트에도 새로운 테마의 타입이 적용되도록 &lt;code&gt;CreateStyled&lt;/code&gt;의 제네릭 타입에 &lt;code&gt;NewTheme&lt;/code&gt; 타입을 전달해 줍니다. &lt;code&gt;ThemeProvider&lt;/code&gt;는 기존의 테마를 확장해 사용할 수 있도록 매개변수로 전달받는 새로운 &lt;code&gt;theme&lt;/code&gt;의 타입을 &lt;code&gt;NewTheme&lt;/code&gt;로 설정합니다. 위의 코드를 보다 보면 의문이 드는 부분이 생길 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;BaseThemeProvider&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;theme&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;theme&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;BaseThemeProvider&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;BaseTheme&lt;/code&gt; 타입을 갖는 부모 레벨에 있는 &lt;code&gt;ThemeProvider&lt;/code&gt;가 갖는 실제 속성값을 넘겨주는 부분이 따로 작성되어 있지 않습니다. 예를 들어 아래의 &lt;code&gt;mergeTheme&lt;/code&gt;과 같은 &lt;code&gt;theme&lt;/code&gt; 값들을 병합해주는 함수를 생각해볼 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;mergeTheme&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;BaseTheme &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; object&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Theme &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; object&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  baseTheme&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; BaseTheme&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  theme&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Theme
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; MergedTheme&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;BaseTheme&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Theme&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;baseTheme&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;theme &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;ThemeProvider&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;theme&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;mergeTheme&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;baseTheme&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; theme&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;ThemeProvider&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;기본적으로 &lt;code&gt;emotion-theming&lt;/code&gt; 패키지의 &lt;code&gt;ThemeProvider&lt;/code&gt;에서 &lt;code&gt;Object.assign&lt;/code&gt; 수준으로 &lt;code&gt;theme&lt;/code&gt; 객체들을 복사해주므로 테마를 합쳐주는 기능을 하는 &lt;code&gt;mergeTheme&lt;/code&gt; 함수를 따로 작성할 필요는 없습니다. 자세한 내용은 &lt;a href=&quot;https://emotion.sh/docs/emotion-theming#themeprovider-reactcomponenttype&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;emotion-theming 공식문서&lt;/a&gt;에서 확인할 수 있습니다. 최종적으로 &lt;code&gt;ThemeProvider&lt;/code&gt;를 확장하기 위해서는 &lt;code&gt;makeTheme&lt;/code&gt; 함수와 &lt;code&gt;MergedTheme&lt;/code&gt; 타입만 존재하면 됩니다.&lt;/p&gt;
&lt;h2 id=&quot;작성한-theme-확장-유틸-사용하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9E%91%EC%84%B1%ED%95%9C-theme-%ED%99%95%EC%9E%A5-%EC%9C%A0%ED%8B%B8-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0&quot; aria-label=&quot;작성한 theme 확장 유틸 사용하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;작성한 Theme 확장 유틸 사용하기&lt;/h2&gt;
&lt;p&gt;작성한 &lt;code&gt;makeTheme&lt;/code&gt; 함수를 사용하기 위해 간단한 예제를 작성했습니다. 최종적인 결과는 아래의 이미지로 위에서 그려보았던 그림과 동일한 형태로 구현되어 있습니다. A, C, E 컴포넌트는 가장 최상단의 &lt;code&gt;ThemeProvider&lt;/code&gt;를 사용하며 B 컴포넌트와 D 컴포넌트는 최상단의 &lt;code&gt;ThemeProvider&lt;/code&gt;를 확장한 새로운 &lt;code&gt;ThemeProvider&lt;/code&gt;를 사용합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1190px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 48.33333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAABYlAAAWJQFJUiTwAAABfUlEQVR42oXO228SQRjG4f3/bxsvbGKi0cYLjU1MY60n0gK7naXdskEqh8ICwi7ljHRxQLs/v3aixJjFiyfvfN9k3owVBxninkvcPyMOlcmosBHKXSQm5XTTSxZDn8W0g0XpEfjiyx5cPhWS3kNQOyYLD+BiFyY5GGdT5EhGGb7Pr6SwLEPNJanYJPWCpCOzIqk6RuOCpPWZpOmnKN7f/yxlWQZFKayUoNWQhxVoN0AyCa6gWTW7MIThGAZDMfrXtezHU27rdZY1TwqPP8iDnpTJ464U9prwtS5FAXSqZjefp5vN4CbmNi8/LCusH+8PWLs2a+eE9WnWcH7niaHy27kO+u1r4toZ1vL8Odp5YqhnaPsx2t3b7LwX6OK+eLXFPkv/JZNeCWsRZlj1P6HDj+h+Bh3J+frY5N1ubKO/eeI81WrhcTPKM4rKWNPgkFX7EB0coO/yjzdm132HHmS3WkvZrHPEoOtj9Vs2UUsZbbU5/zWf/ocibOaYDQN+ATBt06Xthx5wAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/21ec2244fce117cef93514100141e93c/33ecc/2-make-theme-example-result.webp 360w,
/static/21ec2244fce117cef93514100141e93c/263a4/2-make-theme-example-result.webp 480w,
/static/21ec2244fce117cef93514100141e93c/0c8fb/2-make-theme-example-result.webp 640w,
/static/21ec2244fce117cef93514100141e93c/521f7/2-make-theme-example-result.webp 860w,
/static/21ec2244fce117cef93514100141e93c/c89d2/2-make-theme-example-result.webp 1190w&quot;
              sizes=&quot;(max-width: 1190px) 100vw, 1190px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/21ec2244fce117cef93514100141e93c/53918/2-make-theme-example-result.png 360w,
/static/21ec2244fce117cef93514100141e93c/9aebd/2-make-theme-example-result.png 480w,
/static/21ec2244fce117cef93514100141e93c/0f09e/2-make-theme-example-result.png 640w,
/static/21ec2244fce117cef93514100141e93c/3e3fe/2-make-theme-example-result.png 860w,
/static/21ec2244fce117cef93514100141e93c/ba099/2-make-theme-example-result.png 1190w&quot;
            sizes=&quot;(max-width: 1190px) 100vw, 1190px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/21ec2244fce117cef93514100141e93c/ba099/2-make-theme-example-result.png&quot;
            alt=&quot;Theme를 확장하는 유틸 함수 사용 결과&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Theme를 확장하는 유틸 함수 사용 결과&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;최상단에 존재하는 &lt;code&gt;ThemeProvider&lt;/code&gt;는 아래와 같은 형태로 작성됩니다. &lt;code&gt;string&lt;/code&gt; 타입의 값을 갖는 &lt;code&gt;bgColor&lt;/code&gt;와 &lt;code&gt;textColor&lt;/code&gt; 속성을 갖는 &lt;code&gt;color&lt;/code&gt; 객체를 갖는 &lt;code&gt;theme&lt;/code&gt; 속성이 작성되어 있으며 &lt;code&gt;makeTheme&amp;#x3C;{}, Theme&gt;&lt;/code&gt; 형태로 사용합니다. 여기에서 &lt;code&gt;MergedTheme&lt;/code&gt; 타입은 &lt;code&gt;{} &amp;#x26; Theme&lt;/code&gt;이므로 &lt;code&gt;Theme&lt;/code&gt; 타입과 동일합니다.&lt;/p&gt;
&lt;h4 id=&quot;srccomponentsthemecontextts&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#srccomponentsthemecontextts&quot; aria-label=&quot;srccomponentsthemecontextts permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;code&gt;src/components/themeContext.ts&lt;/code&gt;&lt;/h4&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; makeTheme &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;../utils/theme&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Theme&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  color&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    bgColor&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    textColor&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; styled&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ThemeProvider &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;makeTheme&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Theme&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;themeContext.ts&lt;/code&gt; 파일에 작성된 &lt;code&gt;ThemeProvider&lt;/code&gt;는 &lt;code&gt;App.tsx&lt;/code&gt;에서 아래와 같이 사용됩니다. App 컴포넌트는 물론 B와 C 컴포넌트 모두 App 컴포넌트의 최상단에 위치하는 &lt;code&gt;ThemeProvider&lt;/code&gt;에 존재하는 &lt;code&gt;theme&lt;/code&gt; 속성값을 사용합니다.&lt;/p&gt;
&lt;h4 id=&quot;srccomponentsapptsx&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#srccomponentsapptsx&quot; aria-label=&quot;srccomponentsapptsx permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;code&gt;src/components/App.tsx&lt;/code&gt;&lt;/h4&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;react&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;B&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;./B&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;./C&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; styled&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ThemeProvider &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;./themeContext&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; Container &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; styled&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;props&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  backgroundColor&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; props&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;theme&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;color&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;bgColor&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; Title &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; styled&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;h1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;props&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  color&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; props&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;theme&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;color&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;textColor&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;App&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;ThemeProvider&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token attr-name&quot;&gt;theme&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        color&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          textColor&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;#FD7622&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          bgColor&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;#FFF2CC&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Container&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;className&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&apos;&lt;/span&gt;App&lt;span class=&quot;token punctuation&quot;&gt;&apos;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Title&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;This is A component&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Title&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;B&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;C&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Container&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;ThemeProvider&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;하지만 B와 D 컴포넌트는 App 컴포넌트 내부에서 사용된 &lt;code&gt;ThemeProvider&lt;/code&gt;의 테마 속성값을 사용하지 않고 &lt;code&gt;border&lt;/code&gt; 속성을 추가로 갖는 새로운 &lt;code&gt;ThemeProvider&lt;/code&gt;의 테마 속성값을 사용해야 합니다. 따라서 &lt;code&gt;B&lt;/code&gt;라는 폴더를 생성한 후 폴더 내부에 새로운 &lt;code&gt;themeContext.ts&lt;/code&gt; 파일을 생성합니다. 이 파일에서 생성된 &lt;code&gt;ThemeProvider&lt;/code&gt;의 타입은 &lt;code&gt;BaseTheme &amp;#x26; Theme&lt;/code&gt;가 됩니다.&lt;/p&gt;
&lt;h4 id=&quot;srccomponentsbthemecontextts&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#srccomponentsbthemecontextts&quot; aria-label=&quot;srccomponentsbthemecontextts permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;code&gt;src/components/B/themeContext.ts&lt;/code&gt;&lt;/h4&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Theme &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; BaseTheme &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;../themeContext&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; makeTheme &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;../../utils/theme&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Theme&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  color&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    border&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; styled&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ThemeProvider &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;makeTheme&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;BaseTheme&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Theme&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;결론적으로 App 컴포넌트 내부에 있는 &lt;code&gt;ThemeProvider&lt;/code&gt;의 테마 속성값인 &lt;code&gt;bgColor&lt;/code&gt;, &lt;code&gt;textColor&lt;/code&gt; 외에도 추가로 &lt;code&gt;border&lt;/code&gt;라는 속성을 갖게 됩니다. 따라서 B, D 컴포넌트는 이 &lt;code&gt;ThemeProvider&lt;/code&gt;와 &lt;code&gt;styled&lt;/code&gt;를 가져와 사용할 수 있습니다.&lt;/p&gt;
&lt;h4 id=&quot;srccomponentsbindextsx&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#srccomponentsbindextsx&quot; aria-label=&quot;srccomponentsbindextsx permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;code&gt;src/components/B/index.tsx&lt;/code&gt;&lt;/h4&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;tsx&quot;&gt;&lt;pre class=&quot;language-tsx&quot;&gt;&lt;code class=&quot;language-tsx&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;react&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;D&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;./D&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; styled&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ThemeProvider &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;./themeContext&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; Container &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; styled&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;props&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  backgroundColor&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; props&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;theme&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;color&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;bgColor&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  border&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; props&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;theme&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;color&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;border&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; Text &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; styled&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;props&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  color&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; props&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;theme&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;color&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;textColor&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;B&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;ThemeProvider&lt;/span&gt;&lt;/span&gt;
      &lt;span class=&quot;token attr-name&quot;&gt;theme&lt;/span&gt;&lt;span class=&quot;token script language-javascript&quot;&gt;&lt;span class=&quot;token script-punctuation punctuation&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        color&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          textColor&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;#FD7622&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          bgColor&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;#FFFFFF&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          border&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;4px solid red&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Container&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Text&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;A component&apos;s child B component&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Text&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
        &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;D&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;Container&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token plain-text&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;token class-name&quot;&gt;ThemeProvider&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;B&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이처럼 프로젝트의 테마 확장이 필요한 경우 하위 폴더에 새로운 &lt;code&gt;themeContext.ts&lt;/code&gt; 파일을 생성한 후 부모 경로에 있는 &lt;code&gt;themeContext.ts&lt;/code&gt;에서 &lt;code&gt;Theme&lt;/code&gt; 타입만 가져와 합치게 되면 기존의 &lt;code&gt;ThemeProvider&lt;/code&gt;를 확장할 수 있게 됩니다.&lt;/p&gt;
&lt;h4 id=&quot;theme-확장-유틸을-사용하는-예시&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#theme-%ED%99%95%EC%9E%A5-%EC%9C%A0%ED%8B%B8%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EC%98%88%EC%8B%9C&quot; aria-label=&quot;theme 확장 유틸을 사용하는 예시 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Theme 확장 유틸을 사용하는 예시&lt;/h4&gt;
&lt;iframe src=&quot;https://codesandbox.io/embed/frosty-dawn-st99w?codemirror=1&amp;fontsize=14&amp;hidenavigation=1&amp;theme=dark&quot;
  style=&quot;width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;&quot;
  title=&quot;frosty-dawn-st99w&quot;
  allow=&quot;accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking&quot;
  sandbox=&quot;allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts&quot;
&gt;&lt;/iframe&gt;
&lt;p&gt;이로써 간단하게 React의 Context API의 중첩 가능한 특성을 이용해 &lt;strong&gt;CSS in JS&lt;/strong&gt; 라이브러리인 &lt;strong&gt;emotion&lt;/strong&gt;의 테마 기능을 Typesafe하게 확장할 수 있게 되었습니다. Fractal 패턴을 이용하지 않은 프로젝트에서도 사용 가능하며 복잡하게 중첩된 &lt;code&gt;ThemeProvider&lt;/code&gt;를 사용하면서 테마 속성값에 잘못된 값을 넣게 되는 상황을 피하여 테마를 확장할 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;emotion-11버전-마이그레이션&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#emotion-11%EB%B2%84%EC%A0%84-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98&quot; aria-label=&quot;emotion 11버전 마이그레이션 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Emotion 11버전 마이그레이션&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;emotion&lt;/strong&gt;의 버전이 메이저 버전이 11로 올라가게 되면서 &lt;strong&gt;emotion&lt;/strong&gt;의 많은 타입 정의들이 변경되었습니다. 타입 정의가 크게 변경되었지만, &lt;strong&gt;emotion&lt;/strong&gt;의 타입을 확장하는 방식으로 타입을 관리하고 있었기 때문에 약간의 타입 정의만 추가해 동일하게 사용할 수 있습니다. 우선 예시 코드에서 사용했던 타입 중 변경된 타입을 알아보겠습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;11버전 이전의 &lt;code&gt;CreateStyled&lt;/code&gt; 인터페이스&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CreateStyled&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Theme &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; object &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;any&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;BaseCreateStyled&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Theme&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    StyledTags&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Theme&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;11버전의 &lt;code&gt;CreateStyled&lt;/code&gt; 인터페이스&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CreateStyled&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;BaseCreateStyled&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; StyledTags &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;CreateStyled&lt;/code&gt; 인터페이스가 더는 &lt;code&gt;Theme&lt;/code&gt; 제네릭을 사용하지 않습니다. 따라서 기존과 같이 &lt;code&gt;CreateStyled&lt;/code&gt;를 사용하기 위해서는 새로운 &lt;code&gt;CreateStyled&lt;/code&gt;가 확장하고 있는 &lt;code&gt;BaseCreateStyled&lt;/code&gt;와 &lt;code&gt;StyledTags&lt;/code&gt;를 확장해 사용해야 합니다. &lt;code&gt;BaseCreateStyled&lt;/code&gt; 인터페이스의 정의는 아래와 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;emotion/styled&lt;/strong&gt;의 &lt;code&gt;CreateStyled&lt;/code&gt; 인터페이스 정의&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CreateStyled&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;
    &lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;React&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ComponentClass&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ComponentProps&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    ForwardedProps &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;keyof&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ComponentProps&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;keyof&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ComponentProps&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    component&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    options&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; FilteringStyledOptions&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ComponentProps&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ForwardedProps&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CreateStyledComponent&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;
    Pick&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;PropsOf&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ForwardedProps&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      theme&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Theme&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ElementType&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      ref&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Ref&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;InstanceType&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;React&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ComponentClass&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ComponentProps&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    component&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    options&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; StyledOptions&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ComponentProps&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CreateStyledComponent&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;
    PropsOf&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      theme&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Theme&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ElementType&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      ref&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Ref&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;InstanceType&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;
    &lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;React&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ComponentType&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ComponentProps&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    ForwardedProps &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;keyof&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ComponentProps&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;keyof&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ComponentProps&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    component&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    options&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; FilteringStyledOptions&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ComponentProps&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ForwardedProps&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CreateStyledComponent&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;
    Pick&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;PropsOf&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ForwardedProps&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      theme&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Theme&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ElementType&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;React&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ComponentType&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ComponentProps&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    component&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    options&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; StyledOptions&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ComponentProps&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CreateStyledComponent&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;
    PropsOf&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      theme&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Theme&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ElementType&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;
    Tag &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;keyof&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;IntrinsicElements&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    ForwardedProps &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;keyof&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;IntrinsicElements&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Tag&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;keyof&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;IntrinsicElements&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Tag&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    tag&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Tag&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    options&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; FilteringStyledOptions&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;JSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;IntrinsicElements&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Tag&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ForwardedProps&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CreateStyledComponent&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; theme&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Theme&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ElementType &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    Pick&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;JSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;IntrinsicElements&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Tag&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ForwardedProps&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Tag &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;keyof&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;IntrinsicElements&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    tag&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Tag&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    options&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; StyledOptions&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;JSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;IntrinsicElements&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Tag&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CreateStyledComponent&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; theme&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Theme&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ElementType &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token constant&quot;&gt;JSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;IntrinsicElements&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Tag&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;BaseCreateStyled&lt;/code&gt; 인터페이스 중간중간 &lt;code&gt;Theme&lt;/code&gt; 타입을 갖는 &lt;code&gt;theme&lt;/code&gt;를 사용하는 것을 볼 수 있습니다. 이 부분을 이전과 같이 제네릭으로 만들어 아래와 같이 확장하여 사용할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;확장한 &lt;code&gt;CreateStyled&lt;/code&gt; 인터페이스 정의&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; CreateStyledComponent&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; StyledOptions &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;@emotion/styled&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; FilteringStyledOptions &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;@emotion/styled/types/base&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; PropsOf &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;@emotion/react&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;CreateStyled&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Theme&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;
    &lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;React&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ComponentType&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ComponentProps&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    ForwardedProps &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;keyof&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ComponentProps&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;keyof&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ComponentProps&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    component&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    options&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; FilteringStyledOptions&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ComponentProps&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ForwardedProps&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CreateStyledComponent&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;
    Pick&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;PropsOf&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ForwardedProps&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      theme&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Theme&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ElementType&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;React&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ComponentType&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ComponentProps&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    component&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    options&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; StyledOptions&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ComponentProps&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CreateStyledComponent&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;
    PropsOf&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;C&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      theme&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Theme&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ElementType&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;
    Tag &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;keyof&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;IntrinsicElements&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    ForwardedProps &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;keyof&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;IntrinsicElements&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Tag&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;keyof&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;IntrinsicElements&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Tag&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    tag&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Tag&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    options&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; FilteringStyledOptions&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;JSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;IntrinsicElements&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Tag&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ForwardedProps&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CreateStyledComponent&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; theme&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Theme&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ElementType &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    Pick&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;JSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;IntrinsicElements&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Tag&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ForwardedProps&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Tag &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;keyof&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;IntrinsicElements&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    tag&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Tag&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    options&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; StyledOptions&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;JSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;IntrinsicElements&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Tag&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CreateStyledComponent&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; theme&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Theme&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ElementType &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token constant&quot;&gt;JSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;IntrinsicElements&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Tag&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;마찬가지로 &lt;code&gt;StyledTags&lt;/code&gt; 타입 또한 &lt;strong&gt;emotion&lt;/strong&gt; 내부에 정의된 타입들을 가져와 직접 관리해야 합니다. 11버전의 &lt;code&gt;StyledTags&lt;/code&gt; 타입은 아래와 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;emotion/styled&lt;/strong&gt;의 &lt;code&gt;StyledTags&lt;/code&gt; 타입 정의&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;StyledTags&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Tag &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;keyof&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;IntrinsicElements&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CreateStyledComponent&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      theme&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Theme&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ElementType&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token constant&quot;&gt;JSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;IntrinsicElements&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Tag&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;StyledTags&lt;/code&gt; 역시 &lt;strong&gt;emotion&lt;/strong&gt; 내부에 존재하는 &lt;code&gt;Theme&lt;/code&gt; 타입을 사용하고 있습니다. 이 부분 역시 제네릭을 이용해 타입을 확장할 수 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;확장한 &lt;code&gt;StyledTags&lt;/code&gt; 타입 정의&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;StyledTags&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Theme&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Tag &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;keyof&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;JSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;IntrinsicElements&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; CreateStyledComponent&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      theme&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Theme&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ElementType&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token constant&quot;&gt;JSX&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;IntrinsicElements&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;Tag&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;최종적으로 기존에 작성했던 &lt;code&gt;makeTheme&lt;/code&gt; 함수는 아래와 같이 변경됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token generic-function&quot;&gt;&lt;span class=&quot;token function&quot;&gt;makeTheme&lt;/span&gt;&lt;span class=&quot;token generic class-name&quot;&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;BaseTheme &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; object&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Theme &lt;span class=&quot;token keyword&quot;&gt;extends&lt;/span&gt; object&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;NewTheme&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; MergeTheme&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;BaseTheme&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Theme&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token comment&quot;&gt;// 변경된 부분&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; styled &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; _styled &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; CreateStyled&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;NewTheme&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt; StyledTags&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;NewTheme&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; ThemeProvider&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;FC&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; theme&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; NewTheme &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    theme&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    children&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;BaseThemeProvider theme&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;theme&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;BaseThemeProvider&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; styled&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ThemeProvider &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;emotion&lt;/strong&gt;이 11로 메이저 버전이 올라가며 치명적인 타입 이슈가 생길 수 있었지만, 중간에서 다리 역할을 해주는 &lt;code&gt;makeTheme&lt;/code&gt;과 같은 함수가 존재하였기 때문에 어렵지 않게 패키지 내부의 타입들을 확장해 마이그레이션을 할 수 있습니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[강화학습으로 더 재미있는 게임 만들기]]></title><description><![CDATA[플레이 테스트 및 밸런싱 자동화 그리고 업데이트 방향성 설정까지, 강화학습을 통해 보다 재미있는 게임을 만들어나간 과정을 이야기해봅니다.]]></description><link>https://tech.devsisters.com/posts/crp-puzzle-bot</link><guid isPermaLink="false">https://tech.devsisters.com/posts/crp-puzzle-bot</guid><pubDate>Mon, 18 Jan 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;데이터 어플리케이션 셀에서는 머신 러닝을 통해 반복 작업을 자동화하여 사람은 사람만이 할 수 있는 일에 집중할 수 있는 환경을 만들어 나가고 있습니다.
사람이 직접 하려면 수천시간이 걸리는 게임 밸런싱을 강화학습을 통해 자동화한 경험과 업데이트 이전에 유저의 경험을 정량화하여 비지니스적인 의사 결정에 도움을 주었던 경험을 공유해보려고 합니다.&lt;/p&gt;
&lt;h2 id=&quot;생각보다도-퍼즐-게임의-밸런스는-중요합니다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%83%9D%EA%B0%81%EB%B3%B4%EB%8B%A4%EB%8F%84-%ED%8D%BC%EC%A6%90-%EA%B2%8C%EC%9E%84%EC%9D%98-%EB%B0%B8%EB%9F%B0%EC%8A%A4%EB%8A%94-%EC%A4%91%EC%9A%94%ED%95%A9%EB%8B%88%EB%8B%A4&quot; aria-label=&quot;생각보다도 퍼즐 게임의 밸런스는 중요합니다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;생각보다도 퍼즐 게임의 밸런스는 중요합니다.&lt;/h2&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1768px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 46.388888888888886%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAABYlAAAWJQFJUiTwAAACHElEQVR42mWS60uTYRjG/VeCguhAqNSoUEkrmDIsWbWVWhHkquE8bGgqS2fNYVpSapGiaIFltQg7+aUlDnNliuWpGlNsmzupHfRDwfv+2uabTby/Xje/+7qu54kTBIHIOByfqS9vpa+pgZF7ZgbaO3lzx4YgiGFVjO4s/VikvVBJd0UO7RVnsRlSGSpJYcnvjuqiKBInSsC5gQk6zrVhLTXSW5ZIV0ERT6/2IYaBkcXI+ELz7Ny8CXW6AlWBmYLjaj6otvHbN8MKUfgPXB51YtdpsNWk8K52D50XU9Ffao56+wf0eHzsykhGp89FoytBayjjcqaa4PSU5DAG6B+bZPB+Iy+ulfK19xY13RZ0La2EM4ehEtDrYZ9SxeNaPY62al42W9Co8wm4xmOA0vXFhRCfhrpwv29icbaHqWd6Xt/NW+Mw4P3GbvkpiosMVBm0lOfruFmsZjnoXN+he8zO8/IDDJkyGazLZcJ6nl99Z1YcrvAIBQMcjt/CBWU2x4otlBRm8/bGXv58d0sdxgAXnMOMXj9Eb8Np+p80YrccYepB9erlaAq/n8KEjdxWJWHcL6Pu4FbqM7YTnJ2OjSy98szH8HdIYsSUiKsmjX79Dh5WnFwTOeidQyvbgFUnpyU3gTKNHGNWOqEZ13pg0DfOsLWSyY4cll/l4Xyk5kuPBSGiS8Cf8yEqFTKqMuIxZym4ciIN09FkQp7Z1Q7/AtCpPkx7wqDoAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/60819078883bdcfce2c8331757dcb33b/33ecc/main.webp 360w,
/static/60819078883bdcfce2c8331757dcb33b/263a4/main.webp 480w,
/static/60819078883bdcfce2c8331757dcb33b/0c8fb/main.webp 640w,
/static/60819078883bdcfce2c8331757dcb33b/521f7/main.webp 860w,
/static/60819078883bdcfce2c8331757dcb33b/cbd37/main.webp 1200w,
/static/60819078883bdcfce2c8331757dcb33b/10600/main.webp 1768w&quot;
              sizes=&quot;(max-width: 1768px) 100vw, 1768px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/60819078883bdcfce2c8331757dcb33b/53918/main.png 360w,
/static/60819078883bdcfce2c8331757dcb33b/9aebd/main.png 480w,
/static/60819078883bdcfce2c8331757dcb33b/0f09e/main.png 640w,
/static/60819078883bdcfce2c8331757dcb33b/3e3fe/main.png 860w,
/static/60819078883bdcfce2c8331757dcb33b/64756/main.png 1200w,
/static/60819078883bdcfce2c8331757dcb33b/70035/main.png 1768w&quot;
            sizes=&quot;(max-width: 1768px) 100vw, 1768px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/60819078883bdcfce2c8331757dcb33b/70035/main.png&quot;
            alt=&quot;main&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;재미있는 퍼즐 게임을 만들기 위해서는 정말 많은 요소들이 필요합니다.
용감한 쿠키처럼 귀여운 캐릭터도 필요하고, 재미있는 맵들, 매력있는 스토리, 이외에도 사용하기 편한 UI등 수많은 요소들이 조화를 이루어야 좋은 게임이 만들어 집니다.
그런데, 유저들이 정말 즐길 수 있는 게임을 만들기 위해서는 밸런싱에 대한 이야기를 빼 놓을 수가 없습니다.
특히 쿠키런: 퍼즐 월드(이하 퍼즐 월드)는 같은 색을 가진 젤리를 3개 매칭해 나가면서 퍼즐을 푸는 3매치 게임인 만큼 적절한 밸런스를 잡는 것은 매우 중요합니다.
만약 아이템을 사용하지 않으면 도무지 클리어할 수 없는 어려운 맵들만 등장 한다면 유저는 금방 질려서 게임을 떠나게 될 것 이고, 손쉽게 깨지는 난이도로 밸런스를 맞추어도 게임이 너무 시시해져서 이탈하게 될 것 입니다.
그러면 퍼즐 게임의 밸런스는 어떻게 잡을 수 있을까요?&lt;/p&gt;
&lt;h2 id=&quot;일단-플레이-해보기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%9D%BC%EB%8B%A8-%ED%94%8C%EB%A0%88%EC%9D%B4-%ED%95%B4%EB%B3%B4%EA%B8%B0&quot; aria-label=&quot;일단 플레이 해보기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;일단 플레이 해보기&lt;/h2&gt;
&lt;p&gt;퍼즐 월드의 맵들은 레벨 제작팀에서 제작이 되지만 사실 이 맵이 얼마나 어려운지, 클리어하는 데 몇 턴이 필요할 지는 만든 사람조차도 예상하는데 어려움이 있습니다.
워낙 다양한 장애물들과 쿠키들이 있기도 하고, 기본적으로 운에 의해 좌우되는 요소를 가지고 있는 게임이기 때문에 맵을 단순히 눈으로 보는 것 만으로는 밸런스를 예측할 수 없습니다.
그러므로 밸런스를 정하는 유일한 방법은 맵을 제작하는 분들이 직접 플레이를 해보고 해당 맵을 클리어하는 데에 평균적으로 몇 턴이 필요한 지를 체크하는 방법 뿐이었습니다.
게임을 플레이하는 게 일이라니 좋아 보이지만 실제로는 그리 즐거운 작업이 아닙니다.
앞서 이야기했듯이 많은 부분이 랜덤 요소에 좌우되기에 밸런싱을 정확하게 진행하기에 한 두판의 플레이 경험은 전혀 충분하지 않습니다.
적어도 수십 판 정도는 플레이를 해 보아야지만 어느 정도 정확하게 밸런싱을 할 수 있으며 심지어 퍼즐 월드에는 다양한 게임 모드와 독특한 개성을 가진 쿠키와 펫들이 있기 때문에 버프에 의한 효과들과 강화 보너스 그리고 조합 보너스까지 고려하면서 밸런싱을 진행해야 합니다.
또한 2020년 말 기준으로 퍼즐 월드에 약 1500개 가량의 맵들이 포함되어 있고 일주일 마다 30개의 신규 맵들이 등장하고 있으며 주기적으로 이탈률이 높은 맵들을 개선하는 작업 또한 병렬적으로 수행하고 있기 때문에 이 맵들을 모두 직접 플레이 하며 밸런싱 한다는 것은 엄청난 일을 필요로 합니다.
실제 수치로 계산을 해 본다면 현재 퍼즐월드에는 40종 가량의 쿠키와 1500개 정도의 맵, 그리고 강화 시스템이 있으므로 &lt;code&gt;40(쿠키 수) * 1500(맵 수) * 3(테스트할 버프 수) * 64(한 조합당 플레이 횟수) = 1152만 판&lt;/code&gt; 정도의 플레이를 수행하여야 합니다.
한 게임을 플레이하는 데 1분 정도가 걸린다고 가정하여도 &lt;strong&gt;모든 조합을 시도해보려면 꼬박 8000일&lt;/strong&gt;이 걸리게 됩니다!&lt;/p&gt;
&lt;h2 id=&quot;강화학습을-이용해서-해결해-볼-수-있지-않을까&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B0%95%ED%99%94%ED%95%99%EC%8A%B5%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%B4%EC%84%9C-%ED%95%B4%EA%B2%B0%ED%95%B4-%EB%B3%BC-%EC%88%98-%EC%9E%88%EC%A7%80-%EC%95%8A%EC%9D%84%EA%B9%8C&quot; aria-label=&quot;강화학습을 이용해서 해결해 볼 수 있지 않을까 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;강화학습을 이용해서 해결해 볼 수 있지 않을까?&lt;/h2&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1810px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 45.27777777777778%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAABYlAAAWJQFJUiTwAAACC0lEQVR42m3RX0uTYRjHcV9Eb6CzTjqNKDooIkwJQ8OmUW7MIc7pyKmburXNKY4ZOvTZZpiFZkJ/p1PUIiRMCRLqJDpoCEOK6dxsf3x83Gb7Zrq2Qd6n18WH33X/isg+KZVm6u0s3oWXPPWO8dVn4f1kNyEx/W8FcS+Jd2qOxWkfXxbmeeUeZdLjI5KIH84zmQxFmexyPJWiYWaVJucIZfbnmHtnUL1eIiglc2AsGUXjucGg6SYey2W0TRUoH91iSwwWgFlRPEiidazicHnpnRjHbO2iq/MN8Ug+4XYygm5Qg88sx31PzwOXHbn5PGubgWNACYyKIG3CJ4qXVxD6J5kt/Uzo1+HqEShGqeqvRj9UgU0wYnf3ITOXs7a1/j+4swsavZ+eh2PYp19gnPCiFQJsSnkwIoWRtRq4q7RRbKqkR++horGB78eB4sFfqcdX6DCOoNcNoXENc+fZB4Lx3dzJW/EYpSoVLcp6ymwumo3VlOiu4f+xeTj//Rck18o+w6OLnBt4h2FunitL3ygx+Ylu7+XARDrN1ZZ2TmmsnH6yzMk+D2eVMn5GwoUJj0RJTPJ4oJkat0DHx2HUswJnDIsEY/lSdtL7VLb3cKK4hguWWi7ZDVysryewUZAw33IKa8cYbZ2t1DrvU+d0IO8eJRzLn5wQRaqa1JTLSqmTX+e2qhxFo4KNUCiX8A8VLUVgwDKnywAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/ff7c89bb9df24076852294168ee1b4e6/33ecc/booster.webp 360w,
/static/ff7c89bb9df24076852294168ee1b4e6/263a4/booster.webp 480w,
/static/ff7c89bb9df24076852294168ee1b4e6/0c8fb/booster.webp 640w,
/static/ff7c89bb9df24076852294168ee1b4e6/521f7/booster.webp 860w,
/static/ff7c89bb9df24076852294168ee1b4e6/cbd37/booster.webp 1200w,
/static/ff7c89bb9df24076852294168ee1b4e6/a8444/booster.webp 1810w&quot;
              sizes=&quot;(max-width: 1810px) 100vw, 1810px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/ff7c89bb9df24076852294168ee1b4e6/53918/booster.png 360w,
/static/ff7c89bb9df24076852294168ee1b4e6/9aebd/booster.png 480w,
/static/ff7c89bb9df24076852294168ee1b4e6/0f09e/booster.png 640w,
/static/ff7c89bb9df24076852294168ee1b4e6/3e3fe/booster.png 860w,
/static/ff7c89bb9df24076852294168ee1b4e6/64756/booster.png 1200w,
/static/ff7c89bb9df24076852294168ee1b4e6/418a5/booster.png 1810w&quot;
            sizes=&quot;(max-width: 1810px) 100vw, 1810px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/ff7c89bb9df24076852294168ee1b4e6/418a5/booster.png&quot;
            alt=&quot;booster&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;퍼즐 월드는 기본적으로는 같은 색을 가지는 젤리를 3개 매칭시켜 장애물을 제거해 나가는 게임이지만 4개 이상의 젤리를 매칭하면 위와 같이 부스터가 생성됩니다.
부스터는 그 자체만으로도 강력한 효과를 가지고 있는 동시에 2개의 부스터를 서로 매칭 시키면 더욱 강력한 효과를 발휘하게 되므로 퍼즐 월드를 잘 하기 위해서는 이 부스터들을 적절히 조합하고 사용하여야만 합니다.
그런데 이렇게 복잡한 규칙의 퍼즐 게임을 플레이하는 봇을 규칙 기반으로 만드는 것은 굉장히 어렵습니다.
젤리의 패턴을 모두 기록하는것도 쉽지 않으며, 워낙 다양한 장애물들이 있기 때문에 사람처럼 유연한 플레이를 하도록 만들기는 쉽지 않습니다.
심지어 매 번 게임이 업데이트 될 때마다 새로운 로직을 추가해 주어야 하는 번거로움이 있기 때문에 직접 봇을 만드는 것은 그리 좋은 생각이 아닙니다.
그러나, 강화 학습은 스스로 시행 착오를 거치면서 배워 나가는 방식이기 때문에, 강화학습을 이용한다면 복잡한 규칙을 직접 알려줄 필요도 없고 생각하지 못한 예외적인 상황을 처리하지 못해 골치가 아플 일도 없습니다.&lt;/p&gt;
&lt;h2 id=&quot;학습-절차&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%95%99%EC%8A%B5-%EC%A0%88%EC%B0%A8&quot; aria-label=&quot;학습 절차 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;학습 절차&lt;/h2&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 27.22222222222222%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAz0lEQVR42k1QyQqFMAzs/3+avpN6cEFa9wVRVFDBZXwTUAyEJE0yM6nC367rYsA0TSiKAlVViTPPsgxhGEJrjTzP335ZlhKP48DX1DAMSNNUBizLQpIkAkJf1xXneQohF5mP4whjDLqug+M48DwPfd8LBucVExoL3/fRNM2rtq5rfC+gkWjbNgGlx3EsyknInuIDGajQ/tnQRgsYa6p4AB9QLrVtiyiKRIDruvIlJCewIts8z1iWRZr8q+dskgVB8C4zcol9XsJ5zu37LiIYb1u9dhBDTCGCAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/2f8c8f10c4b04d39df5af0f68ec5e67b/33ecc/rl.webp 360w,
/static/2f8c8f10c4b04d39df5af0f68ec5e67b/263a4/rl.webp 480w,
/static/2f8c8f10c4b04d39df5af0f68ec5e67b/0c8fb/rl.webp 640w,
/static/2f8c8f10c4b04d39df5af0f68ec5e67b/521f7/rl.webp 860w,
/static/2f8c8f10c4b04d39df5af0f68ec5e67b/cbd37/rl.webp 1200w,
/static/2f8c8f10c4b04d39df5af0f68ec5e67b/548e7/rl.webp 2400w,
/static/2f8c8f10c4b04d39df5af0f68ec5e67b/bac22/rl.webp 2438w&quot;
              sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/2f8c8f10c4b04d39df5af0f68ec5e67b/53918/rl.png 360w,
/static/2f8c8f10c4b04d39df5af0f68ec5e67b/9aebd/rl.png 480w,
/static/2f8c8f10c4b04d39df5af0f68ec5e67b/0f09e/rl.png 640w,
/static/2f8c8f10c4b04d39df5af0f68ec5e67b/3e3fe/rl.png 860w,
/static/2f8c8f10c4b04d39df5af0f68ec5e67b/64756/rl.png 1200w,
/static/2f8c8f10c4b04d39df5af0f68ec5e67b/98e2c/rl.png 2400w,
/static/2f8c8f10c4b04d39df5af0f68ec5e67b/7b4c8/rl.png 2438w&quot;
            sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/2f8c8f10c4b04d39df5af0f68ec5e67b/98e2c/rl.png&quot;
            alt=&quot;rl&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;그런데 강화학습을 통해 이 문제를 해결하기 위해서는 몇 가지 준비물이 필요합니다.
강화 학습은 직접 시행착오를 겪어 보면서 학습을 해 나가는 방법이기 때문에, 게임을 플레이 하는 사람(Agent)와 게임이 진행될 환경(Environment)이 필요합니다.
Agent의 경우에는 게임을 플레이하는 사람 처럼 주어진 게임판의 상태(State)를 보고 이 상황에서 어떤 행동(Action)을 취할 것인지 결정을 해야 합니다.
Enviroment에서는 Agent에서 결정한 Action을 바탕으로 게임판의 상태를 업데이트 하고, 행동에 대한 보상(Reward)을 다시 Agent에게 돌려 줍니다.
이 보상을 이용하여 Agent는 자신이 취한 Action이 좋았는지, 아니면 좋지 않았는 지를 판단하게 됩니다.
그러면, Agent에 대해 알아보기 전에 퍼블 봇의 Enviroment는 어떤 방식으로 구성되어 있을까요?&lt;/p&gt;
&lt;p&gt;당연히 유저들이 플레이 하는 환경과 완전히 동일한 환경에서 Agent가 플레이를 하는 것이 가장 일반적이면서도 좋은 방법일 것입니다.
실제로도 대부분의 학습은 유저들이 플레이하는 환경과 완전히 동일한 환경(Unity ML Agent)에서 진행이 되고 있지만 이 환경에는 치명적인 단점이 있습니다.
바로 속도가 느리다는 단점인데요, 아무래도 다양한 장애물들이 모두 들어있기도 하고 unity자체가 조금 느리기 때문에 학습에 오랜 시간이 소요되는 문제가 있습니다.
강화학습이 아닌 다른 ML 분야의 경우에는 대부분의 시간을 GPU를 사용하여 모델의 파라미터들을 업데이트 하면서 보내지만, 강화학습의 경우에는 Environment에서 플레이를 하는 데 대부분의 시간을 보냅니다.
성능을 개선하기 위해서는 ML 특성상 다양한 모델을 시도 해 보아야 하지만 실제 Unity환경에서는 한 번의 실험을 할 때마다 &lt;strong&gt;3일&lt;/strong&gt;이 걸리기 때문에 그만큼 개발 속도가 느려지게 됩니다.&lt;br&gt;
보다 빠르게 실험 결과를 얻고 지속적으로 모델을 개선해 나가기 위해서는 Unity 환경보다 빠른 속도로 게임을 돌릴 수 있는 Mock 환경을 Cython으로 제작을 했습니다.
물론 실제 게임에 포함된 장애물들과 맵들을 모두 그대로 재현하기에는 어려움이 있지만 그래도 게임의 가장 기본적인 요소인 3매치나 부스터 그리고 단순한 형태의 장애물들은 직접 구현을 했습니다.
테스트 결과 Unity에서는 한 번의 행동을 수행하는데 약 0.1초가 걸리는데 반해 Cython으로 제작한 Mock환경에서는 0.01초 이내에 한 번의 스텝(한 번의 Action)이 진행되기 때문에 &lt;strong&gt;반나절&lt;/strong&gt;이면 학습의 결과를 얻을 수 있게 되었습니다.
따라서 모델을 개선하는 작업을 진행할 때는 이 Mock환경을 이용하고 있으며 실제 밸런싱을 진행할 때는 시간이 오래 걸리더라도 정확한 결과를 얻기 위해 실제 유니티 빌드에서 학습을 수행합니다.&lt;/p&gt;
&lt;p&gt;다음으로 드디어 Agent에 대한 내용입니다.
먼저, 다양한 강화학습 모델 중에서 퍼즐 봇에 사용되고 있는 모델은 Policy-Gradient 기반의 PPO(Proximal Policy Optimization)를 이용하고 있습니다.
DQN과 같은 Value-Based한 모델을 이용해서 학습이 가능 하기는 하지만, 퍼즐 월드에서 DQN을 사용하기 위해서는 405개의 가능한 Action들에 대해서 Q-Value들을 계산해 나가야 하기 때문에 효율적이지 못합니다.
그러므로 고차원의 Action Space에서 더 잘 수렴하는 Policy-Gradient가 좋은 선택입니다.
또한 3매치를 잘 학습하기 위해서는 주변에 어떤 색의 젤리가 있는지를 살펴 보는 것이 중요한 데 이것은 이미지를 인식할 때 주변 픽셀들을 이용하는 것과 유사하므로 이미지 인식에 사용되는 ResNet을 Policy Network로 사용할 수 있습니다.
물론 게임의 상태는 게임판 위에 있는 젤리들의 상태(Visual Feature) 이외에도 현재 플레이중인 게임을 클리어하기 위한 목표나 현재 사용중인 쿠키, 남은 턴수 등에 대한 정보가 필요하기 때문에 이런 정보들은 별도로 처리하게 됩니다.&lt;/p&gt;
&lt;p&gt;그런데, 이렇게 모델을 구성했을 때는 생각보다 잘 수렴하지 않는 문제가 있었습니다.
아무래도 강화학습 에이전트가 학습해야 하는 게임 자체가 복잡하기도 하고, 수행할 수 있는 Action의 수가 너무 많기 때문에 강화학습 에이전트가 잘 학습하지 못했었습니다.
사실 지금까지는 퍼즐 월드의 물리적 특성을 이용하지는 않았고 어떠한 문제에서도 적용 가능한 형태로 모델을 구성하였습니다.
그러므로 퍼즐 월드에서만 적용되는 몇 가지 특성들을 적절한 형태로 모델에 녹여 낸다면 훨씬 더 잘 수렴할 것이라는 것을 예상할 수 있습니다.&lt;/p&gt;
&lt;p&gt;모델의 성능을 개선하는 데 가장 큰 도움을 준 특성은 사실 젤리의 색은 별로 중요한 것이 아니라는 점입니다.
젤리의 색이 빨간 색인지 아니면 파란 색인지는 별로 중요한 것이 아니고, 같은 색의 젤리 3개를 일렬로 만들 수 있는 지에 대한 여부가 더 중요합니다.&lt;/p&gt;
&lt;p&gt;&lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1680px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 44.99999999999999%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAABYlAAAWJQFJUiTwAAABk0lEQVR42oVSS0sCURgdaNWqTX+hZYsWLaOgTYuCXPcgemAFhW2igl4YUYsUeomppWBRpqNiuRB6WUKUFYHROqLHpkXJzODccU5XZ5wmMfrg3vvdy/kO557vY6CGrC5lz2q30vGDkYuQjJZl6TMhIBTHEeRPrVyW86sQkooRcxgiKQ96QjlDYAmcozecxOaGDRe7Q7hNsDo1BWKlMHV3huPtYfhcVhiD1zD7E5CEjI4wzaPOewkm9oERsxGvgTLEo9MgskIoCAJ4nqeplC+6SdjxtsdgacEA5uAJNb4riF/cD6FEJZum1tDcM4HOvln0d7TBZtvRlLEsC4fDod093lM0dE+itX0R1QNOGEY9IHqFggg8Hjbh2V+BkLMe7+Fy3J+OKx5RhRzHIZ1OQ1J/b40kwSReUDUXQmXsAbWBJMRPncIMJXS7nXCsjGFm3g7XsglsMFowTtcMJd8/SaFx9Qhdlgha1k8wuBUHyZHouyxSe/iM0mxOPf/qMrJUes5TOhXUYFoslhgbbaLkf2bwd++Lkd+AAIiET8F3ggAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/a9cc05ccc599aae71b86c8cb2ee8b398/33ecc/deepset.webp 360w,
/static/a9cc05ccc599aae71b86c8cb2ee8b398/263a4/deepset.webp 480w,
/static/a9cc05ccc599aae71b86c8cb2ee8b398/0c8fb/deepset.webp 640w,
/static/a9cc05ccc599aae71b86c8cb2ee8b398/521f7/deepset.webp 860w,
/static/a9cc05ccc599aae71b86c8cb2ee8b398/cbd37/deepset.webp 1200w,
/static/a9cc05ccc599aae71b86c8cb2ee8b398/4250d/deepset.webp 1680w&quot;
              sizes=&quot;(max-width: 1680px) 100vw, 1680px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/a9cc05ccc599aae71b86c8cb2ee8b398/53918/deepset.png 360w,
/static/a9cc05ccc599aae71b86c8cb2ee8b398/9aebd/deepset.png 480w,
/static/a9cc05ccc599aae71b86c8cb2ee8b398/0f09e/deepset.png 640w,
/static/a9cc05ccc599aae71b86c8cb2ee8b398/3e3fe/deepset.png 860w,
/static/a9cc05ccc599aae71b86c8cb2ee8b398/64756/deepset.png 1200w,
/static/a9cc05ccc599aae71b86c8cb2ee8b398/c1e63/deepset.png 1680w&quot;
            sizes=&quot;(max-width: 1680px) 100vw, 1680px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/a9cc05ccc599aae71b86c8cb2ee8b398/c1e63/deepset.png&quot;
            alt=&quot;deepset&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;그러므로 위에 있는 두 상태는 다른 색깔이지만 Agent의 입장에서는 완전히 동등한 상태라고 생각할 수 있고, 이 특성을 이용하게 된다면 Agent가 탐색해야 할 공간의 크기가 크게 감소합니다.
이렇게 입력되는 순서와는 상관 없이(Permutation Invariant) 같은 특성을 보이는 Feature들의 경우에는 한 가지 조합만 탐색해도 되므로, 탐색 공간의 크기가 1/(색깔의 수)! 만큼으로 감소하게 되고, 퍼즐 월드에서는 총 여섯가지 색의 젤리가 있으므로 탐색할 범위가 720분의 1로 줄어들게 됩니다.
두 번째로 사용한 퍼즐 월드만의 물리적 특성은 어떤 행동을 수행할 때 그 행동으로 인해 변화가 생기는 지점들의 위치를 미리 알 수 있으며 이 위치들을 모델로 바로 전달해 줄 수 있다는 점입니다.
모델을 이루고 있는 Neural Net은 당연히 처음에는 랜덤한 값으로 초기화가 되어 있기 때문에 특정한 행동을 수행했을 때 게임판에 어떠한 변화가 생기게 되는 지에 대해서는 전혀 예측을 하지 못합니다.
아무 것도 모르는 상태에서 수천만 번 젤리를 스왑해 보면서 &lt;code&gt;아~ 같은 색깔의 젤리 3개를 일렬로 배치하면 터지는구나~&lt;/code&gt;, &lt;code&gt;이 부스터는 이렇게 터지는구나~&lt;/code&gt;와 같은 사실을 천천히 배워 나가게 되는데 반해서 우리는 이러한 사실들을 학습을 시작하기 전부터 알고 있으므로 이 정보들을 학습이 시작하는 시점부터 바로 모델에 전달해 줄 수 있습니다.
그러므로 모델은 3개를 매칭하면 젤리가 터지고, 부스터를 사용하면 이러이러한 모양으로 터진다는 사실을 알고 있는 상태로 학습을 시작할 수 있으며 불필요한 탐색을 수행하지 않게 되기 때문에 훨씬 더 빠르게 학습을 수행하게 됩니다.
물론 퍼즐 게임 특성상 랜덤성을 가지고 있기 때문에 직접 실행해보지 않고는 어떤 블록들이 터지는 지 정확하게 알 수 는 없지만 그래도 어느 정도 정확한 값을 전달해 주는 것 만으로도 성능 향상에 많은 기여를 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/5cc50ac988179ac0d61e128d135b44c8/good.gif&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;이렇게 만들어진 모델은 위 영상처럼 몇 수 앞을 내다보는 플레이를 보여 주기도 합니다.&lt;/p&gt;
&lt;p&gt;게임을 기획할 때는 유저가 원하는 것을 세심하게 파악하면서도, 게임의 밸런스를 무너뜨리지는 않아야 합니다.
그런데, 사실 기획 단계에서는 게임에 어느 정도로 변화를 주는 것이 최선인지 알기가 매우 어렵고 많은 경우에는 감에 의존해서 의사 결정을 진행하는 경우가 많았습니다.
물론 적절하게 게임이 개선되는 경우도 있지만, 대부분의 게임들은 몇 차례의 추가적인 패치가 이루어 지기도 하고 심지어는 유저들에게 패치 내용을 알려주지도 않고 몰래 패치가 이루어지기도 합니다.
이러한 잦은 패치는 유저의 경험을 상당 부분 해치기 때문에 적절한 기획은 매우 중요합니다.
그런데 실제로 업데이트가 이루어지기 전에는 기획이 적절한지 알기 어렵다는 문제는 퍼즐 봇을 이용하면 해결할 수 있습니다.
퍼즐 봇을 단순히 게임을 플레이하는 봇이라는 관점에서 벗어나, 한 명의 유저라는 관점에서 바라보게 된다면 퍼즐 봇의 플레이 결과를 바탕으로 비지니스적인 의사 결정을 하는 데도 많은 도움을 줄 수 있었습니다.
가령 40종의 다양한 쿠키들중에서 특정 쿠키만 아주 강력한 효과를 가지고 있다면 유저들이 해당 쿠키만 구매하게 될 수 있을 수 있는데, 퍼즐 봇을 이용한다면 각 쿠키들이 얼마나 좋은지를 정량화할 수 있게 되어 쿠키들의 개성을 살릴 수 있는 방향으로 게임을 개선해 나갈 수 있었습니다.
이외에도 강화 시스템이 추가되면서 게임이 너무 쉬워질 수 있다는 우려가 있었는데, 퍼즐 봇을 이용한다면 강화시스템이 도입된 후 게임이 얼마나 쉬워지는지, 그에 따라서 재화 사용량과 매출은 어느 정도로 감소하는지에 대한 통찰도 미리 얻을 수 있었습니다.&lt;/p&gt;
&lt;p&gt;퍼즐 게임을 제작하고, 운영하면서 발생하는 다양한 문제들을 강화 학습을 통해 해결할 수 있었습니다.
데이터 어플리케이션셀에서는 &quot;Data-driven&quot;이라는 미션 아래, 통계와 머신 러닝 등의 도구를 활용하여 게임 산업에서의 문제를 능동적으로 해결해 나가고 있습니다.
우리 팀과 함께 &quot;Data-driven&quot;으로 의사결정하는 조직을 만들어나가실 분을 적극적으로 기다리고 있으니 주저 말고 지원 부탁드립니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[데이터 분석 라이브러리 개발기 (2) - 통합 테스팅과 문서화를 동시에 잡는 방법]]></title><description><![CDATA[MinIO, pytest, doctest, Spinx 등을 조합한 통합 테스팅과 문서화 사례를 소개합니다.]]></description><link>https://tech.devsisters.com/posts/testing-devplay-analytics-library</link><guid isPermaLink="false">https://tech.devsisters.com/posts/testing-devplay-analytics-library</guid><pubDate>Fri, 04 Dec 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요, 데이터플랫폼셀 김민수입니다. 이번 글은 &lt;a href=&quot;/posts/devplay-analytics-library&quot;&gt;데이터 분석 라이브러리 개발기 (1)&lt;/a&gt;에서 이어지는 &amp;#x3C;데이터 분석 라이브러리 개발기&gt; 두번째 글입니다. DevPlay Analytics는 PySpark을 활용하여 Amazon S3에 적재된 데이터 분석을 도와주는 파이썬 라이브러리입니다. 데이터 분석이라는 특성상 보통의 라이브러리와 같은 방법으로 테스트를 구성하기에는 까다로운 부분이 많았는데요, 이 글에서는 DevPlay Analytics 라이브러리에서 테스트 환경을 구축하고 문서 기반으로 테스트를 작성한 과정을 소개하겠습니다.&lt;/p&gt;
&lt;h2 id=&quot;개요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B0%9C%EC%9A%94&quot; aria-label=&quot;개요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;개요&lt;/h2&gt;
&lt;p&gt;DevPlay Analytics의 모듈은 S3에 적재된 특정한 데이터를 PySpark DataFrame으로 로드하고 가공하여 다시 S3로 저장하는 역할을 담당합니다. 그러므로 각 모듈에 대한 테스트를 작성한다면 미리 S3에 테스트용 샘플 데이터 셋을 저장해둔 후 로드하고 다시 테스트용 S3 버킷에 저장하여 의도한 대로 동작하는지 확인해야 합니다.&lt;/p&gt;
&lt;figure style=&quot;max-width: 500px; margin: 1em auto&quot;&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1338px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 70%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAAAsTAAALEwEAmpwYAAACHUlEQVR42pWT6W7aUBCF85L938fo775DpT5BKyVtRQoUUFsoIZCEzRiSCLPYMciAMXZJMfvy9dotJFRCoiON7tyx5viemTMnHLD1es1o5DJ4dPklztlsxnyxYDaf+/FCxHMRu67Lcrnc1Z0cAlwu5tw1NC4qDZoPOslkktDnAF+jYeLxOKFQiGg0Sq1WYzweHwO4wOiZaPYQw+xj6Crxsk5ENjBaDyhKDVVVMQyD6XR6BKCg3K4pZF+/QrnOYDs28YLCp1SZevWebrfre7vdZjKZHAG42fBwWyH58gW3iW/0HYfY9ySngS9cX2VotVq+67p+JKCgbFoW9k8Hs9vBNjuk9RGx5oTJo4M1sOn3+3Q6nSMpi8kVpRLR4Dn3NykU0fyPZ6dEwiHCYiCBQIBsNoumaf6kd4AbQW21WvnuScV3776ci5d1UUVBr2/hDm0SikXwzmE46NHt9bAEA++Vnox2gJ6WJEmiUqmQyWRIpS6QyxXijRGFuzplWRITVZCKBSKJNLHLPCURX6bT5PN5Go3Gfg89gcqyTLVa9UFLpRLVWp2EOkauqhTyWZpCHrdlmfCPK4LJHIVc1tdlLpfzpbPXQ4/ylqoXb7eEjZdb/W2HuLOh2FmQ0Dx6KzbPNmpbtxvKNvH8/APxlBu5YyLSDbFyDnPgPP34Hzs45a1tAQfOkDfvznj7/gNay9j79l+Az4Fta4Ap1nElWnHIfgMeyB6mIaYZGQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/061e11314f875ce689adcf25c3131648/33ecc/1-test-structure.webp 360w,
/static/061e11314f875ce689adcf25c3131648/263a4/1-test-structure.webp 480w,
/static/061e11314f875ce689adcf25c3131648/0c8fb/1-test-structure.webp 640w,
/static/061e11314f875ce689adcf25c3131648/521f7/1-test-structure.webp 860w,
/static/061e11314f875ce689adcf25c3131648/cbd37/1-test-structure.webp 1200w,
/static/061e11314f875ce689adcf25c3131648/f3741/1-test-structure.webp 1338w&quot;
              sizes=&quot;(max-width: 1338px) 100vw, 1338px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/061e11314f875ce689adcf25c3131648/53918/1-test-structure.png 360w,
/static/061e11314f875ce689adcf25c3131648/9aebd/1-test-structure.png 480w,
/static/061e11314f875ce689adcf25c3131648/0f09e/1-test-structure.png 640w,
/static/061e11314f875ce689adcf25c3131648/3e3fe/1-test-structure.png 860w,
/static/061e11314f875ce689adcf25c3131648/64756/1-test-structure.png 1200w,
/static/061e11314f875ce689adcf25c3131648/5e075/1-test-structure.png 1338w&quot;
            sizes=&quot;(max-width: 1338px) 100vw, 1338px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/061e11314f875ce689adcf25c3131648/5e075/1-test-structure.png&quot;
            alt=&quot;테스트 구조&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;테스트 구조&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;파이썬-테스트-툴-소개&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%8C%8C%EC%9D%B4%EC%8D%AC-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%88%B4-%EC%86%8C%EA%B0%9C&quot; aria-label=&quot;파이썬 테스트 툴 소개 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;파이썬 테스트 툴 소개&lt;/h2&gt;
&lt;h3 id=&quot;unittest&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#unittest&quot; aria-label=&quot;unittest permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;unittest&lt;/h3&gt;
&lt;p&gt;파이썬에서 널리 사용하는 테스트 툴로는 &lt;a href=&quot;https://docs.python.org/3/library/unittest.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;unittest&lt;/a&gt;와 &lt;a href=&quot;https://docs.pytest.org/en/stable/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;pytest&lt;/a&gt;가 있습니다. unittest는 &lt;a href=&quot;https://junit.org/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;JUnit&lt;/a&gt;이라는 자바 테스팅 프레임워크로부터 강력한 영향을 받은 파이썬 내장 유닛테스트 모듈입니다. 파이썬 자체 내부 테스트에서도 사용되는 등 여러 대형 프로젝트에서 널리 사용되는 툴이기도 합니다. unittest는 JUnit과 유사하게 모든 테스트를 &lt;code&gt;unittest.TestCase&lt;/code&gt;를 상속받는 클래스 형태로 정의합니다. 예를 들어 &lt;code&gt;str.upper&lt;/code&gt;의 동작을 테스트한다면,&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; unittest

&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;TestStringMethods&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;unittest&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;TestCase&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;test_upper&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;self&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;assertEqual&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;foo&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;upper&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;FOO&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;TestStringMethods&lt;/code&gt; 라는 클래스를 선언하고 &lt;code&gt;test_upper&lt;/code&gt;라는 메소드를 선언하는 방식으로 테스트를 작성합니다. &lt;code&gt;unittest.TestCase&lt;/code&gt;에는 &lt;code&gt;assertEqual&lt;/code&gt;과 같이 테스트에 필요한 메소드들이 선언되어 있고, 테스트 작성 시 상황에 맞는 테스트 메소드를 골라서 사용합니다.&lt;/p&gt;
&lt;p&gt;DevPlay Analytics 라이브러리의 경우 S3 데이터를 입출력하기 위해 거의 모든 동작에서 Spark Session이 필요합니다. 데이터 모듈 테스트의 관심사는 데이터 모듈이 정확한 S3 경로에서 데이터를 로드하는지를 확인하는 것이므로 Spark Session을 초기화하고 종료하는 과정은 테스트에서 확인하려는 부분이 아닙니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; devplay_analytcs &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; build
&lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; pyspark&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;sql &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; SparkSession

&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;TestDWMethods&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;unittest&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;TestCase&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setUp&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;self&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;spark &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; SparkSession \
                       &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;builder \
                       &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;master&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;local[*]&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; \
                       &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;getOrCreate&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;devplay_analytics &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; build&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;test_game&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;prod&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;tearDown&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;self&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;spark&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;stop&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;test_load&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;self&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        df &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;devplay_analytics&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;dw&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;load&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;f_economy&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; \
                &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;where&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;F&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;col&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;date&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;2019-01-01&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;assertEqual&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;df&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;count&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1104&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;unittest에서는 테스트에 필요한 자원의 생성과 반환을 &lt;code&gt;setUp&lt;/code&gt;과 &lt;code&gt;tearDown&lt;/code&gt;이라는 메소드에 정의하여 실제 테스트 코드를 분리하여 작성합니다.&lt;/p&gt;
&lt;h3 id=&quot;pytest&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#pytest&quot; aria-label=&quot;pytest permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;pytest&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.pytest.org/en/stable/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;pytest&lt;/a&gt;는 파이썬 내장 프레임워크가 아닌 써드 파티 파이썬 테스팅 툴입니다. pytest는 간결한 문법을 지원해서 unittest를 사용하는 것보다 테스트 코드를 쉽게 작성할 수 있도록 도와줍니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;test_upper&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;assert&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;foo&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;upper&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;FOO&apos;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;pytest는 함수 단위로 테스트를 작성합니다. 그리고 unittest에서 &lt;code&gt;assertEqual&lt;/code&gt; 같은 테스트용 메소드를 사용했던 것과 달리, 파이썬의 &lt;code&gt;assert&lt;/code&gt; 문을 사용하여 대부분의 테스트를 할 수 있습니다. 그리고 테스트 실패 시 단순히 &lt;code&gt;assert&lt;/code&gt; 문의 값이 틀렸다는 것 뿐만 아니라, 구문의 구조를 분석하여 어떤 식으로 테스트에 실패하였는지에 대한 자세한 정보를 출력해줍니다. 예를 들어, &lt;code&gt;assert&lt;/code&gt; 문에서 &lt;code&gt;list&lt;/code&gt; 타입의 값을 비교했다면 &lt;code&gt;list&lt;/code&gt; 내부의 어떤 값이 존재하지 않았는지를 자세히 출력합니다.&lt;/p&gt;
&lt;p&gt;pytest가 unittest 대비 가장 편리하다고 생각되는 점은 자원의 생성과 반환을 정의하는 fixture 문법이 매우 편하다는 것입니다. pytest에서 fixture는 &lt;code&gt;@pytest.fixture&lt;/code&gt; decorator를 사용하여 함수 형태로 정의합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; pytest
&lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; devplay_analytcs &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; build
&lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; pyspark&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;sql &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; SparkSession

&lt;span class=&quot;token decorator annotation punctuation&quot;&gt;@pytest&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fixture&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;devplay_analytics&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    spark &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; SparkSession \
           &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;builder \
           &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;master&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;local[*]&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; \
           &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;getOrCreate&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    devplay_analytics &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; build&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;test_game&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;prod&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;yield&lt;/span&gt; devplay_analytics
    spark&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;stop&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;test_load&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;devplay_analytics&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    df &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; devplay_analytics&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;dw&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;load&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;f_economy&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; \
            &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;where&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;F&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;col&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;date&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;2019-01-01&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;assert&lt;/span&gt; df&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;count&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1104&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;위 예시에서 &lt;code&gt;devplay_analytics&lt;/code&gt; 함수는 DevPlay Analytics 객체를 생성하는 pytest fixture입니다. &lt;code&gt;devplay_analytics&lt;/code&gt; 함수에서 &lt;code&gt;yield&lt;/code&gt; 전의 동작을 보면, Spark Session을 초기화하고, DevPlay Analytics 객체를 &lt;code&gt;yield&lt;/code&gt; 합니다. &lt;code&gt;test_load&lt;/code&gt; 테스트에서 파라미터 이름을 &lt;code&gt;devplay_analytics&lt;/code&gt;라고 작성하면, 테스트 실행 시 &lt;code&gt;devplay_analytics&lt;/code&gt; fixture를 실행하여 &lt;code&gt;yield&lt;/code&gt;에서 반환하는 값을 테스트 파라미터로 넘겨줍니다. 그리고 테스트 완료 시 &lt;code&gt;devplay_analytics&lt;/code&gt;의 &lt;code&gt;yield&lt;/code&gt; 이후의 동작을 실행하여 자원을 반환합니다.&lt;/p&gt;
&lt;p&gt;데이터 분석 라이브러리에서는 여러 가지 fixture가 복잡하게 얽힌 형태로 사용되며 자원의 생성과 반환의 시점을 잘 제어되어야 하기에 pytest의 간결하고 강력한 fixture 문법을 사용하여 개발하는 게 효율적이었습니다. 또한, 셀에서 만드는 다른 파이썬 프로젝트에서 pytest를 이미 사용하고 있었기에 경험이 많기도 했고요. 따라서 DevPlay Analytics 라이브러리 개발에서도 pytest를 활용하여 테스트를 작성하게 되었습니다. Spark Session을 비롯하여 여러 자원을 정의하는 데 유용하게 pytest를 사용할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;하지만 라이브러리를 계속 개발하다 보니 DevPlay Analytics 라이브러리는 보통의 파이썬 라이브러리처럼 테스트하기에는 까다로운 부분이 많았습니다.&lt;/p&gt;
&lt;h2 id=&quot;문제-정의---데이터-분석-라이브러리는-테스트가-쉽지-않다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%B8%EC%A0%9C-%EC%A0%95%EC%9D%98---%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B6%84%EC%84%9D-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EB%8A%94-%ED%85%8C%EC%8A%A4%ED%8A%B8%EA%B0%80-%EC%89%BD%EC%A7%80-%EC%95%8A%EB%8B%A4&quot; aria-label=&quot;문제 정의   데이터 분석 라이브러리는 테스트가 쉽지 않다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;문제 정의 - 데이터 분석 라이브러리는 테스트가 쉽지 않다&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;/posts/devplay-analytics-library&quot;&gt;데이터 분석 라이브러리 개발기&lt;/a&gt;에서 소개해 드린 것과 같이, DevPlay Analytics 라이브러리의 데이터 계층은 IO 계층과 설정 계층을 호출하는 식으로 구현되어 있습니다. 따라서, 데이터 계층 함수의 테스트를 작성하기 위해서는 IO 계층과 설정 계층을 mocking 한 후 데이터 계층 함수 기능에 대한 테스트를 작성해야 합니다. 설정 계층은 단순히 특정한 형태의 파일을 읽어서 설정값을 반환해주므로 해당 파일에 대한 fixture를 정의하면 쉽게 원하는 형태의 설정값들을 만들 수 있었습니다. 그런데, IO 계층은 비즈니스 로직 자체가 PySpark와 S3에 기대고 있는 부분이 많아 mocking 하기에는 까다로운 부분이 많았습니다.&lt;/p&gt;
&lt;p&gt;그래서 처음에는 S3에 테스트용 버킷과 fixture 데이터를 올려 두는 방식으로 테스트를 구성했습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; pyspark&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;sql &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; functions &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; F

&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;test_load&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;devplay_analytics&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; DevPlayAnalytics&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    df &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; devplay_analytics&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;dw&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;load&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;f_economy&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; \
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;where&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;F&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;col&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;date&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;2019-01-01&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;assert&lt;/span&gt; df&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;count&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1104&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;위 테스트는 f_economy라는 DW 테이블을 로드한 후 해당 테이블의 row 수가 예상한 대로 있는지를 확인합니다. DevPlay Analytics 객체인 &lt;code&gt;devplay_analytics&lt;/code&gt;도 fixture로 구성을 했는데, 해당 fixture에서 SparkSession 초기화 / 종료, 설정 계층의 파일에 대한 mocking 등도 담당하게 하여 &lt;code&gt;devplay_analytics&lt;/code&gt;만 fixture로 참조하여 테스트를 작성할 수 있게 했습니다. 설정 파일 mock에 각 데이터 계층의 테스트 데이터에 해당하는 경로들을 적어두었기에 테스트 경로에 테스트용 데이터를 미리 적재해뒀습니다.&lt;/p&gt;
&lt;p&gt;이렇게 실제로 S3에 데이터를 쌓아두고 이 데이터 기반으로 테스트를 작성하는 방식으로 각 데이터 계층 모듈의 기능을 어느 정도 테스트할 수 있었습니다. 하지만, 실제로 S3에 접근하여 데이터를 입출력해야 하다 보니 접근 권한이 있는 경우에만 테스트 코드를 실행해볼 수 있었습니다. 간단한 CI를 구성하여 테스트를 실행하고 싶었는데 개발 당시에는 &lt;a href=&quot;https://github.com/features/actions&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;GitHub Actions&lt;/a&gt;에서 테스트용 S3 버킷 접근 권한을 부여하기가 쉽지 않았습니다. 한편, 매번 테스트마다 새로운 데이터를 생성해야 했고, 테스트 자체에도 시간이 오래 걸려 좋은 접근 방법은 아닌 것 같았습니다. 또한, 테스트용 데이터는 S3에 한 벌만 존재하므로 각 테스트 실행 별로 환경을 격리할 수가 없는 문제도 있었습니다.&lt;/p&gt;
&lt;h2 id=&quot;minio를-사용한-로컬-테스트-환경-구축&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#minio%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-%EB%A1%9C%EC%BB%AC-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95&quot; aria-label=&quot;minio를 사용한 로컬 테스트 환경 구축 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;MinIO를 사용한 로컬 테스트 환경 구축&lt;/h2&gt;
&lt;p&gt;따라서 S3 API 자체를 테스트마다 따로 띄울 수 있으면 좋을 것 같았습니다. 그러던 중 &lt;a href=&quot;https://min.io/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;MinIO&lt;/a&gt;라는 프로젝트를 발견했습니다. MinIO는 오픈소스 오브젝트 스토리지 프로젝트인데요, S3 SDK와 호환이 되는 특징이 있습니다.&lt;/p&gt;
&lt;figure style=&quot;max-width: 500px; margin: 1em auto&quot;&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 280px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 100%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAACRUlEQVR42qWTy28SURTG+esa19WkJq2m7jSWRxN5qSyKNVBsApak4KJVhKmFYWCsGBbAgkdCIaGIqcUN3QAbVpAQh/fn3MtAUai0eJPJOZlzzu+ee893ZfjXGgyo6fV6ojvATZbsWla/T23jZwGm53ocfnD9B1DqRvieQceqxftdCx5vKNDv9xYASrBW+RK9RAh+xgPNi5eolMtSeHD7Dsmdof0L3gCPZxodfpyfD/8tAhwVNBoNPH2ygW6nA5NpB+vrj1CtVudCp4GSbbfbUKo2cXaWx/6+Ay7XRxgMhrlQ2eyZDJOLxSLUai2Wl+8hGOQRDodhNL5aUDYSlFi5XA6V2O329mssLd1BKPSVxvqStG4EnCwwmy3I579RP5PJIBaL3b7DyS4tll16lwvLZhr4Ztwhkc+so86VDbEj32zeQaFQGF/DrJwp4Cg460ilUgmtVmvmxn/7FNjtdiEIwvDJiYVEgx1R0OQThBbtiuQMYx3Rtsfir1Sq08BsNou1tYdoNpvQanVgmE/gOI7qjkyYYRiqyYODQ/D8ZwQCQbHrSyiVm2K+Hjab7U9gOp3Gysp9mkBEzHFBCuF5HlbrW6yuPoDD8Y6+Fr+fE2NHdCNSR5ZOp8fpaeYKGIlExEIr7cjpdOL42Au32w2WZbG3Z0c0GhM3ukt9lvXD4zmC3T70yakUChUuLopXwFwuR3cmK5VKiYAoFW8ymYTX60OtVkM8HofPxyKRSNBYvV7H1pYRGrUOJydfxsORXTe5m/wfDWtSNr8BSFO9q8lvIGUAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/7b4c428da7b1f267024f092aef3c424e/7ee82/2-minio-logo.webp 280w&quot;
              sizes=&quot;(max-width: 280px) 100vw, 280px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/7b4c428da7b1f267024f092aef3c424e/2a48b/2-minio-logo.png 280w&quot;
            sizes=&quot;(max-width: 280px) 100vw, 280px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/7b4c428da7b1f267024f092aef3c424e/2a48b/2-minio-logo.png&quot;
            alt=&quot;MinIO&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;MinIO&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;/figure&gt;
&lt;p&gt;MinIO를 활용하면 직접 S3에 접근하지 않고도 로컬에서 S3 SDK와 호환되는 API 서버를 띄운 후 테스트에 필요한 데이터를 로컬에서 정의할 수 있습니다. 그래서 S3에 데이터를 미리 저장해두는 방식 대신 MinIO를 사용하여, 테스트 시작 시 MinIO 서버를 띄운 후 이 서버에 테스트에 필요한 버킷과 데이터를 생성한 후 테스트를 실행하도록 구성했습니다.&lt;/p&gt;
&lt;h3 id=&quot;minio-서버-fixture-사용&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#minio-%EC%84%9C%EB%B2%84-fixture-%EC%82%AC%EC%9A%A9&quot; aria-label=&quot;minio 서버 fixture 사용 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;MinIO 서버 fixture 사용&lt;/h3&gt;
&lt;p&gt;pytest에서 사용할 수 있는 MinIO 서버 fixture는 &lt;a href=&quot;https://pypi.org/project/pytest-server-fixtures/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;pytest-server-fixtures&lt;/a&gt; 라는 프로젝트에 구현되어 있어 DevPlay Analytics의 dev 디펜던시에 &lt;code&gt;pytest-server-fixtures[s3]&lt;/code&gt;를 추가했습니다. 그리고 &lt;code&gt;pytest-server-fixtures&lt;/code&gt;에 구현된 &lt;code&gt;s3_server&lt;/code&gt; fixture를 사용하여 MinIO 서버를 띄운 후, MinIO 서버 접속 정보를 주입해 Spark Session을 생성했습니다. 이런 방식으로 생성한 Spark Session을 fixture로 정의하여 각 테스트에서 MinIO + Spark Session을 사용할 수 있도록 했습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token decorator annotation punctuation&quot;&gt;@pytest&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fixture&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;scope&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;session&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;spark&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;s3_server&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; MinioServer&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    spark &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;SparkSession
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;builder
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;master&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;local[*]&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# s3a endpoint를 로컬의 MinIO 서버로 설정하여 S3 API 호출을 MinIO서버로 하도록 합니다.&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;config&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;spark.hadoop.fs.s3a.endpoint&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;http://localhost:{}&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;s3_server&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;port&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# s3_server fixture에서 MinIO 서버 실행 시 설정한 access key, secret key를 설정해줍니다.&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;config&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;spark.hadoop.fs.s3a.access.key&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; s3_server&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;aws_access_key_id&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;config&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;spark.hadoop.fs.s3a.secret.key&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; s3_server&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;aws_secret_access_key&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;config&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;spark.hadoop.fs.s3a.path.style.access&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;true&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;config&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;spark.hadoop.fs.s3a.impl&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;org.apache.hadoop.fs.s3a.S3AFileSystem&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;getOrCreate&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# 여기서 반환된 Spark Session을 각 테스트에서 사용합니다.&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;yield&lt;/span&gt; spark
    &lt;span class=&quot;token comment&quot;&gt;# 테스트 완료 시 Spark Session을 종료합니다.&lt;/span&gt;
    spark&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;stop&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# (spark fixture 자원 반환 시 s3_server fixture에서 MinIO 서버를 종료하고 리소스를 삭제합니다)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그리고 테스트에 필요한 bucket과 데이터들도 역시 fixture로 정의하여 테스트에서 레퍼런스하여 사용할 수 있도록 하였습니다.&lt;/p&gt;
&lt;figure&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token decorator annotation punctuation&quot;&gt;@pytest&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fixture&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;scope&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;session&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;bucket_logging_test_game&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;s3_server&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; MinioServer&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# MinIO 서버에 create_bucket API를 호출하여 버킷을 생성합니다.&lt;/span&gt;
    s3_server&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;get_s3_client&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;create_bucket&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Bucket&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;logging-test-game&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token decorator annotation punctuation&quot;&gt;@pytest&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fixture&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;scope&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;session&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;bucket_analytics_test_game&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;s3_server&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; MinioServer&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    s3_server&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;get_s3_client&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;create_bucket&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Bucket&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;analytics-test-game&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;버킷 fixture 예시&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token decorator annotation punctuation&quot;&gt;@pytest&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fixture&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;scope&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;session&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;data_dw_f_churn&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;spark&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; SparkSession&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; bucket_analytics_test_game&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# 여기서 spark는 앞서 정의한 spark fixture입니다.&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# 따라서 DataFrame을 생성한 후 Parquet로 적재를 하면&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# fixture에서 설정한 대로 MinIO 서버를 호출하여 저장됩니다.&lt;/span&gt;
    spark&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;createDataFrame&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
            Row&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;date&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;2020-02-01&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; countryCode&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;KR&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; os&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;android&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; userCnt&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;6040&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            Row&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;date&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;2020-02-01&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; countryCode&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;KR&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; os&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;ios&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; userCnt&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1962&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;write&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;partitionBy&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;date&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;parquet&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;s3a://analytics-test-game/dw/env=prod/f_churn&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;figcaption&gt;&lt;데이터 fixture 예시&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;마지막으로, DevPlay Analaytics 객체를 생성할 때, Spark Session과 설정값들 등의 fixture를 전부 모아 객체를 생성하도록 하였습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token decorator annotation punctuation&quot;&gt;@pytest&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fixture&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;scope&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;session&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;devplay_analytics&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# SparkSession&lt;/span&gt;
        spark&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# data_analytics_config 설정&lt;/span&gt;
        data_analytics_config&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# minio server&lt;/span&gt;
        s3_server&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# boto3 resource (글에서 다루고 있지는 않지만, boto3 라이브러리의 S3 리소스도 MinIO로 통신하도록 monkey patching 했습니다)&lt;/span&gt;
        boto3_resource&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; build&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        game_code&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;test_game&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        env_code&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;prod&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;minio-서버-fixture를-사용하여-테스트-실행&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#minio-%EC%84%9C%EB%B2%84-fixture%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%8B%A4%ED%96%89&quot; aria-label=&quot;minio 서버 fixture를 사용하여 테스트 실행 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;MinIO 서버 fixture를 사용하여 테스트 실행&lt;/h3&gt;
&lt;p&gt;이렇게 만든 fixture들을 조합하여 각 모듈에 대한 테스트를 작성했습니다. DW 모듈 데이터 로드에 대해서 테스트를 작성하기 위하여 &lt;code&gt;devplay_analytics&lt;/code&gt; , &lt;code&gt;data_dw_f_churn&lt;/code&gt; 두 fixture를 사용하여 아래와 같이 테스트 코드를 작성할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;test_load&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;devplay_analytics&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; DevPlayAnalytics&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; data_dw_f_churn&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    df &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; devplay_analytics&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;dw&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;load&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;f_churn&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;assert&lt;/span&gt; df&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;collect&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
        Row&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;date&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;2020-02-01&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; countryCode&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;KR&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; os&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;android&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; userCnt&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;6040&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        Row&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;date&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;2020-02-01&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; countryCode&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;KR&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; os&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;ios&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; userCnt&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1962&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 테스트는 &lt;code&gt;devplay_analytics&lt;/code&gt; fixture를 첫번째 인자로 사용하고 있으므로, &lt;span style=&quot;color:red; font-weight:bold&quot;&gt;①&lt;/span&gt; DevPlay Analytics Fixture를 생성하고, &lt;span style=&quot;color:red; font-weight:bold&quot;&gt;②&lt;/span&gt; Spark Session을 초기화 시키며, &lt;span style=&quot;color:red; font-weight:bold&quot;&gt;③&lt;/span&gt; MinIO 서버를 실행시켜 Spark Session에서 연결할 수 있게 합니다. 그리고 두번째 인자로 &lt;code&gt;data_dw_f_churn&lt;/code&gt; fixture를 사용하므로, &lt;span style=&quot;color:red; font-weight:bold&quot;&gt;④&lt;/span&gt; Log Data Fixture를 생성하기 위해 &lt;span style=&quot;color:red; font-weight:bold&quot;&gt;⑤&lt;/span&gt; Spark Session Fixture와 &lt;span style=&quot;color:red; font-weight:bold&quot;&gt;⑥&lt;/span&gt; Bucket Fixture를 생성합니다. Bucket Fixture를 생성하기 위해서도 &lt;span style=&quot;color:red; font-weight:bold&quot;&gt;⑦&lt;/span&gt; MinIO Server Fixture가 필요합니다.&lt;/p&gt;
&lt;figure style=&quot;max-width: 650px; margin: 1em auto&quot;&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1596px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 56.388888888888886%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAABYlAAAWJQFJUiTwAAAB50lEQVR42n1SaZOaUBDkr+enpLYqqeRTPmzlcBPvaxFRQVCOiAo88EC82KSyJZ03L+umTLmhqnkHQ09Pz0jL5RJJkiBN0wtsNpu/2G6x5djtdgJZluFwPOJwOAgc+Z7iVqsVpCiKQE+e53jpyR9/IdmkCIIAIWOwLAu+9x2TqQfP8+C6riBcLBaQ4jh+Jjxz7vd7sMBHxEKEwRzjoQXZDmDpPeiahsLXb3hflNEolSF3ZFSrVcznc1C1ErESO2XRdQ3zkGEWRjCVNlrtDj59uYNsOJhFS7wuqHBnPAmL8KYbwB1omE6nCMNQWCIUEitdynIHlVIRCv9Z9VNMzSEKNRXGaITPuo9mS8OrmzJU00W/p+JdoY5mrc7VVbgQXfh3UfLpdHr2ccJ9ad3fQ1VVFIoVDM0xNC+GyxZg3PPJzMdNN4KjKMJXEkSNFYSMm3ytKfnTe8AybB9z+PESDdsH4wKUTgeNWhVNnrRer4vEZBtxiZLH4zFs2xbdo/UM17bQVfoomQwflQlalQaPHYlvjuPAsR2x0tkwjD9jQyW/NIdk9Gq1xge1h9u+hoTv6e5iRjkonjhEl6lDV0t+OmfZD7wtl3HbbOPh4ed/Y2mmpfV6DcK/Cs9IuAJ9MOSdNLia6zGkmjhI5W8fqikpWcmzHwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/a041762b1caf9d30f5c4a44bc893ec5b/33ecc/3-fixture-structure-1.webp 360w,
/static/a041762b1caf9d30f5c4a44bc893ec5b/263a4/3-fixture-structure-1.webp 480w,
/static/a041762b1caf9d30f5c4a44bc893ec5b/0c8fb/3-fixture-structure-1.webp 640w,
/static/a041762b1caf9d30f5c4a44bc893ec5b/521f7/3-fixture-structure-1.webp 860w,
/static/a041762b1caf9d30f5c4a44bc893ec5b/cbd37/3-fixture-structure-1.webp 1200w,
/static/a041762b1caf9d30f5c4a44bc893ec5b/c5b4b/3-fixture-structure-1.webp 1596w&quot;
              sizes=&quot;(max-width: 1596px) 100vw, 1596px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/a041762b1caf9d30f5c4a44bc893ec5b/53918/3-fixture-structure-1.png 360w,
/static/a041762b1caf9d30f5c4a44bc893ec5b/9aebd/3-fixture-structure-1.png 480w,
/static/a041762b1caf9d30f5c4a44bc893ec5b/0f09e/3-fixture-structure-1.png 640w,
/static/a041762b1caf9d30f5c4a44bc893ec5b/3e3fe/3-fixture-structure-1.png 860w,
/static/a041762b1caf9d30f5c4a44bc893ec5b/64756/3-fixture-structure-1.png 1200w,
/static/a041762b1caf9d30f5c4a44bc893ec5b/d866c/3-fixture-structure-1.png 1596w&quot;
            sizes=&quot;(max-width: 1596px) 100vw, 1596px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/a041762b1caf9d30f5c4a44bc893ec5b/d866c/3-fixture-structure-1.png&quot;
            alt=&quot;MinIO를 활용한 테스트 fixture 구조&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;MinIO를 활용한 테스트 fixture 구조&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;/figure&gt;
&lt;p&gt;따라서 테스트 실행 시 MinIO 서버가 생성되고, MinIO 서버에 필요한 bucket과 데이터가 적재되며, 해당 MinIO 서버에 Spark로 접근하여 테스트 코드가 진행되게 됩니다.&lt;/p&gt;
&lt;p&gt;다른 테스트를 한 번에 실행시킬 경우 MinIO 서버 fixture는 공용으로 사용하면서 (다른 버킷과 데이터가 필요할 경우) 버킷과 데이터를 생성합니다.&lt;/p&gt;
&lt;figure style=&quot;max-width: 650px; margin: 1em auto&quot;&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1604px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 56.111111111111114%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAABYlAAAWJQFJUiTwAAACOElEQVR42mVTa4vTQBTN39b/IH4TXPwgi+4TUfABC12LK82jD3ebJmnzbJImbZKmryzartAe751SRR04mbkzZ87MnHsjlWWJoigwn88xm80EeLxYLASqqiLcY0XYjyusVqv/+KzBY2kymYDbbrfDoW02G2Q0z3A9D3HgCvhBAI/iKIoE59AOe7Msg8SffwXX6zUMwyQYaGkqXl3JOK2pMG/bkBsNdLu64Bz2HfbyLaUiz5DmJY6vewjjBOHQhzswYVkWDNNEPAzwafgD75wKlzcG/DiFYw/g2X24rgPH2WO5XP4R9KMEj17WYfZtyM0WzmsKOp0OVFVFq9XCvMgwThM8Jk6n14csy7isN6FqTTQ1DRqBxYQg+zQuSlijDEU2gelFeN1J4Pk++eVjFEeYz0pkRDaJkxNn4AU4bo1g03pAvJA85YQIwUmaIs6naPsJkiSBSjf8UPtCp2tQ5AaaXQMJrfdJ7KuTIKWbKqqGj9c3kBUFiiJD13UhyAmWWNUekCeui4A8MbwYR3dLhGGINBqibka4MJY41eeQDQ+h58AdxnhOHHcYkeeByP6ANLgEpcPbpxSsqJ5kSsQLtQnL9bCkmKvgpP0N7+8MTIm3IvPb/T6OZBU6J4M40+l0r0G9lNKTuW23W9F/bt/i6dkFBkEs4ur+O569eYvzqxoeHn6KOUU38eTkDLrt/7V3PB5D4irnAd8kz3N6QgirZ4EP4ph9sS2yxPF+c2JKgtUzKWEjEfM88/nP+gW1OxyhnErvcAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/50c376b2a02812e753054b3b2a767da1/33ecc/4-fixture-structure-2.webp 360w,
/static/50c376b2a02812e753054b3b2a767da1/263a4/4-fixture-structure-2.webp 480w,
/static/50c376b2a02812e753054b3b2a767da1/0c8fb/4-fixture-structure-2.webp 640w,
/static/50c376b2a02812e753054b3b2a767da1/521f7/4-fixture-structure-2.webp 860w,
/static/50c376b2a02812e753054b3b2a767da1/cbd37/4-fixture-structure-2.webp 1200w,
/static/50c376b2a02812e753054b3b2a767da1/50ff9/4-fixture-structure-2.webp 1604w&quot;
              sizes=&quot;(max-width: 1604px) 100vw, 1604px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/50c376b2a02812e753054b3b2a767da1/53918/4-fixture-structure-2.png 360w,
/static/50c376b2a02812e753054b3b2a767da1/9aebd/4-fixture-structure-2.png 480w,
/static/50c376b2a02812e753054b3b2a767da1/0f09e/4-fixture-structure-2.png 640w,
/static/50c376b2a02812e753054b3b2a767da1/3e3fe/4-fixture-structure-2.png 860w,
/static/50c376b2a02812e753054b3b2a767da1/64756/4-fixture-structure-2.png 1200w,
/static/50c376b2a02812e753054b3b2a767da1/fe147/4-fixture-structure-2.png 1604w&quot;
            sizes=&quot;(max-width: 1604px) 100vw, 1604px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/50c376b2a02812e753054b3b2a767da1/fe147/4-fixture-structure-2.png&quot;
            alt=&quot;여러 테스트를 실행시킬 경우&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;여러 테스트를 실행시킬 경우&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;/figure&gt;
&lt;p&gt;모든 테스트가 완료되면 MinIO 서버와 관련된 리소스들이 삭제됩니다.&lt;/p&gt;
&lt;h3 id=&quot;소결론-minio-fixture를-사용하면-쉽고-가볍게-로컬-테스트를-할-수-있습니다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%86%8C%EA%B2%B0%EB%A1%A0-minio-fixture%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EB%A9%B4-%EC%89%BD%EA%B3%A0-%EA%B0%80%EB%B3%8D%EA%B2%8C-%EB%A1%9C%EC%BB%AC-%ED%85%8C%EC%8A%A4%ED%8A%B8%EB%A5%BC-%ED%95%A0-%EC%88%98-%EC%9E%88%EC%8A%B5%EB%8B%88%EB%8B%A4&quot; aria-label=&quot;소결론 minio fixture를 사용하면 쉽고 가볍게 로컬 테스트를 할 수 있습니다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;소결론: MinIO fixture를 사용하면 쉽고 가볍게 로컬 테스트를 할 수 있습니다.&lt;/h3&gt;
&lt;p&gt;정리하자면 MinIO fixture를 통하여 Amazon S3에 의존성이 없으면서도, 테스트 시 완전히 격리된 환경에서 S3 입출력이 필요한 기능의 테스트를 빠르게 실행할 수 있었습니다.&lt;/p&gt;
&lt;h2 id=&quot;테스트-코드를-작성하면서-레퍼런스-문서도-만들-수-있다면&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C%EB%A5%BC-%EC%9E%91%EC%84%B1%ED%95%98%EB%A9%B4%EC%84%9C-%EB%A0%88%ED%8D%BC%EB%9F%B0%EC%8A%A4-%EB%AC%B8%EC%84%9C%EB%8F%84-%EB%A7%8C%EB%93%A4-%EC%88%98-%EC%9E%88%EB%8B%A4%EB%A9%B4&quot; aria-label=&quot;테스트 코드를 작성하면서 레퍼런스 문서도 만들 수 있다면 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;테스트 코드를 작성하면서 레퍼런스 문서도 만들 수 있다면&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.python.org/3/library/doctest.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;doctest&lt;/a&gt;는 파이썬 코드 중 &lt;a href=&quot;https://www.python.org/dev/peps/pep-0257/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;docstring&lt;/a&gt;에 작성된 파이썬 인터프리터 코드를 실행하여 결과가 정확하게 작동하는지 확인해주는 파이썬 모듈입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;factorial&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;n&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token triple-quoted-string string&quot;&gt;&quot;&quot;&quot;Return the factorial of n, an exact integer &gt;= 0.
    &gt;&gt;&gt; [factorial(n) for n in range(6)]
    [1, 1, 2, 6, 24, 120]
    &quot;&quot;&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;위 코드를 doctest 모듈로 실행시키면 파이썬 인터프리터 부분(&lt;code&gt;&gt;&gt;&gt;&lt;/code&gt; 오른쪽 코드)을 실행하여 아래와 같은 결과가 나오는지 검증해줍니다. doctest는 파이썬 내장 기능이기도 하지만, pytest의 doctest integration을 사용해서 실행할 수도 있습니다.&lt;/p&gt;
&lt;p&gt;DevPlay Analytics 라이브러리는 데이터 분석가분들이 쉽게 사용하실 수 있도록 docstring에 각 함수의 용례를 포함한 자세한 설명을 작성해두고 있습니다. 또한, &lt;a href=&quot;https://www.sphinx-doc.org/en/master/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Sphinx&lt;/a&gt;라는 파이썬 문서화 도구를 사용해서 docstring을 함수 레퍼런스 문서로 빌드하여 제공하고 있습니다. 그런데, 모듈의 동작이나 형상이 바뀔 때마다 docstring을 최신화되지 않고 빠뜨리기도 하여 관리가 쉽지 않았습니다.&lt;/p&gt;
&lt;figure style=&quot;max-width: 500px; margin: 1em auto&quot;&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1509px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 25.27777777777778%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAFCAYAAABFA8wzAAAACXBIWXMAAAsTAAALEwEAmpwYAAABZElEQVR42h1QzS/CcRz+zcVbGVkuakkxbm4ObDYOkliySaWpC1am+qlE72tkWiJbbK2orGXelsgcbGZrzI2tcTD8J4/v73d49nxen88LxVWsQqIPwLCTg+f4FqpAEu1qN5i4gDCHcNu0C61TTtSOrqBmxIwGOc3muQoadTILmifs4MisaJq0g+pbDOPl8xfXlXe8fv2h/FbF08c3ZkIpbOUfoPAcwZUsYiN1A/rwCqb9AqyJC5jiZ/CSBcyE9eEMW8MMoboNQQRz99BvZ3FQfEal+sOKDNliiJ4/wp0uwUIExtYTWIjl0b8cIcKXcKdKGLTuQh1KY2mvgEC2TK6hQWk2T2CMnLKFDPszd+yJfIIBSxRd80GIdT7wlA6ItF4IZz3oMYbQS8C8RqL3s7aUvI2vWgPFUzrZjYYdcXDlNkjn/Kgn3ELiYp0fAiLAgGkQajwQk7xI62NtRryT+B3EZ9A4TuMfS17jBC5J67QAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/f074fe4f44fa01195eb2fddd81b3f8c8/33ecc/5-sphinx-logo.webp 360w,
/static/f074fe4f44fa01195eb2fddd81b3f8c8/263a4/5-sphinx-logo.webp 480w,
/static/f074fe4f44fa01195eb2fddd81b3f8c8/0c8fb/5-sphinx-logo.webp 640w,
/static/f074fe4f44fa01195eb2fddd81b3f8c8/521f7/5-sphinx-logo.webp 860w,
/static/f074fe4f44fa01195eb2fddd81b3f8c8/cbd37/5-sphinx-logo.webp 1200w,
/static/f074fe4f44fa01195eb2fddd81b3f8c8/7acfb/5-sphinx-logo.webp 1509w&quot;
              sizes=&quot;(max-width: 1509px) 100vw, 1509px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/f074fe4f44fa01195eb2fddd81b3f8c8/53918/5-sphinx-logo.png 360w,
/static/f074fe4f44fa01195eb2fddd81b3f8c8/9aebd/5-sphinx-logo.png 480w,
/static/f074fe4f44fa01195eb2fddd81b3f8c8/0f09e/5-sphinx-logo.png 640w,
/static/f074fe4f44fa01195eb2fddd81b3f8c8/3e3fe/5-sphinx-logo.png 860w,
/static/f074fe4f44fa01195eb2fddd81b3f8c8/64756/5-sphinx-logo.png 1200w,
/static/f074fe4f44fa01195eb2fddd81b3f8c8/11a5b/5-sphinx-logo.png 1509w&quot;
            sizes=&quot;(max-width: 1509px) 100vw, 1509px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/f074fe4f44fa01195eb2fddd81b3f8c8/11a5b/5-sphinx-logo.png&quot;
            alt=&quot;Sphinx&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Sphinx&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;/figure&gt;
&lt;p&gt;그러다 보니 docstring에 작성한 용례들이 실제로 실행이 되는지 체크를 하면 좋겠다는 생각이 들었습니다. 한편, fixture 기반으로 각 모듈 테스트를 작성하다 보니 사실 테스트 코드 대부분이 실제 함수 동작을 설명하는 예시 코드와 비슷하다고 느껴졌습니다. 그래서 pytest doctest 모듈을 활용하여 docstring 코드를 실행하는 방식으로 테스트를 구성하게 되었습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;DW&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;load&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
            self&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            table&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token triple-quoted-string string&quot;&gt;&quot;&quot;&quot;
        DW 테이블을 로드하여 불러옵니다.

        .. testsetup::
            &gt;&gt;&gt; getfixture(&apos;data_dw_f_churn&apos;)

        .. doctest::
            &gt;&gt;&gt; f_churn = devplay_analytics.dw.load(&apos;f_churn&apos;)
            &gt;&gt;&gt; f_churn \
                    .where(F.col(&apos;date&apos;) == devplay_analytics.env.target_date) \
                    .orderBy(&quot;countryCode&quot;, &quot;os&quot;) \
                    .show(truncate=False)
            +-----------+-------+-------+----------+
            |countryCode|os     |userCnt|date      |
            +-----------+-------+-------+----------+
            |KR         |android|6040   |2020-02-01|
            |KR         |ios    |1962   |2020-02-01|
            +-----------+-------+-------+----------+
            &amp;lt;BLANKLINE&gt;

        :param table: 불러올 테이블의 이름
        :return: DataFrame
        &quot;&quot;&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이렇게 작성된 doctest를 pytest에서 &lt;code&gt;--doctest-modules&lt;/code&gt; 옵션을 줘서 실행시키면 &lt;code&gt;&gt;&gt;&gt;&lt;/code&gt; 오른쪽에 작성된 코드를 실행하여 결과가 docstring과 같은지 테스트합니다. 함수로 정의했던 테스트와 다르게 작성해야 하는 점은, 테스트 실행 시 필요한 fixture를 &lt;code&gt;getfixture&lt;/code&gt; 라는 헬퍼 함수를 사용해서 호출해야 한다는 점입니다.&lt;/p&gt;
&lt;p&gt;작성된 doctest는 sphinx automodule을 사용하여 아래와 같은 파이썬 문서로 빌드할 수 있습니다.&lt;/p&gt;
&lt;figure style=&quot;max-width: 600px; margin: 1em auto&quot;&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1504px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 76.66666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAYAAADkmO9VAAAACXBIWXMAAAsTAAALEwEAmpwYAAABz0lEQVR42o2UC3eiMBCF+///XrsWi5W1QHgJ8owIhLtzw2FdK+dsPV4SksmXO8noyznLkCYJ+JmNkccs36V9lHnQbCZM07NeyrbHPsjx9unDOUXYeSHcIMPuGMLx+K7g/I7wfkqwVxWcsLKtumgM4wbwJoOeH8P5OGDnfOCX6HXn4O19b/uOe7T945dC3vRIS42s0mivw7ZD8Y84UnBdF3EcQXcdpnGEkfSNpEVNsqmZxnu68zK3CeTCTiCB7+N0+kIYBEjTDGEYoigumCzYSLD5Z+Hyvox/A/IxiqNzXovDBHmeI+NFpSnaokBT5GiaRjZtoVuR1tZAK/1lvMMwDOj73nLuwHMJpSJpzzaoqio0ZQlWQSxVUMhGtcwRcrlcxH1hY0uJud1uuF6vdyBFZwxa3TE4jmMkAmNrNxJ3dLOKIIrunoCEcJA7RlFknRBOoC/n63keanG9ulkhbDeBdMBBpkpILUC6pgK5KELruv7r6LsegOzQIc+OaSulLHhNne4Oh4Md+zGQt8cJQikGdJ1+uNU15r9AI1rPjqkygM7olunTKVPngq1ifqpDiulw0eqSpUFXq2Otu58DWfFrQbNPGPuEz/JPY8z2r2IL+Afp9oPUVzkFZAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/577d0b8350768306d28a1dded0f7ad26/33ecc/6-reference-document.webp 360w,
/static/577d0b8350768306d28a1dded0f7ad26/263a4/6-reference-document.webp 480w,
/static/577d0b8350768306d28a1dded0f7ad26/0c8fb/6-reference-document.webp 640w,
/static/577d0b8350768306d28a1dded0f7ad26/521f7/6-reference-document.webp 860w,
/static/577d0b8350768306d28a1dded0f7ad26/cbd37/6-reference-document.webp 1200w,
/static/577d0b8350768306d28a1dded0f7ad26/13d37/6-reference-document.webp 1504w&quot;
              sizes=&quot;(max-width: 1504px) 100vw, 1504px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/577d0b8350768306d28a1dded0f7ad26/53918/6-reference-document.png 360w,
/static/577d0b8350768306d28a1dded0f7ad26/9aebd/6-reference-document.png 480w,
/static/577d0b8350768306d28a1dded0f7ad26/0f09e/6-reference-document.png 640w,
/static/577d0b8350768306d28a1dded0f7ad26/3e3fe/6-reference-document.png 860w,
/static/577d0b8350768306d28a1dded0f7ad26/64756/6-reference-document.png 1200w,
/static/577d0b8350768306d28a1dded0f7ad26/3046c/6-reference-document.png 1504w&quot;
            sizes=&quot;(max-width: 1504px) 100vw, 1504px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/577d0b8350768306d28a1dded0f7ad26/3046c/6-reference-document.png&quot;
            alt=&quot;doctring으로부터 빌드된 레퍼런스 문서&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;doctring으로부터 빌드된 레퍼런스 문서&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;/figure&gt;
&lt;figure style=&quot;max-width: 600px; margin: 1em auto&quot;&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2330px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 52.5%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABFUlEQVR42p1SyY6DUAzj/z8PiaW0INay89iXuXjGkUAV7Yw6PVig5MVxnGjL8oW6UciyDH03YJqmJ4zj+DL+6p2mkhhlWQihapUklmX5COu6QnNvNyRJgqIoMAyDdJnn+XNCkjVNg7ZtoZRC13XYtk2SZ5yLzznWaff7HXmeC2Fd1zI6m5RlKQ2IqqoEVL+TsTHfMM461vd9Dy0IAhAk8jwPlmXBtm2kaXqMvxv+aAX/z8ti/Bj5crmIj2EYwnVdXK/XQ9Ffnj15SGUsNE1T5LMBFeq6/u+NCyE9pEKOyC9VRlEE3/dlrH1BbxMahgHHcYQgjmMxm4ugyQSb8JzeV/hz2PSLm+bIOwlBYm6ShL+d0iN4Nt/KsEmvPCgoyQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/f33a3d8ed52879f33c5931bc3a017444/33ecc/7-docstring.webp 360w,
/static/f33a3d8ed52879f33c5931bc3a017444/263a4/7-docstring.webp 480w,
/static/f33a3d8ed52879f33c5931bc3a017444/0c8fb/7-docstring.webp 640w,
/static/f33a3d8ed52879f33c5931bc3a017444/521f7/7-docstring.webp 860w,
/static/f33a3d8ed52879f33c5931bc3a017444/cbd37/7-docstring.webp 1200w,
/static/f33a3d8ed52879f33c5931bc3a017444/0999c/7-docstring.webp 2330w&quot;
              sizes=&quot;(max-width: 2330px) 100vw, 2330px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/f33a3d8ed52879f33c5931bc3a017444/53918/7-docstring.png 360w,
/static/f33a3d8ed52879f33c5931bc3a017444/9aebd/7-docstring.png 480w,
/static/f33a3d8ed52879f33c5931bc3a017444/0f09e/7-docstring.png 640w,
/static/f33a3d8ed52879f33c5931bc3a017444/3e3fe/7-docstring.png 860w,
/static/f33a3d8ed52879f33c5931bc3a017444/64756/7-docstring.png 1200w,
/static/f33a3d8ed52879f33c5931bc3a017444/f79ca/7-docstring.png 2330w&quot;
            sizes=&quot;(max-width: 2330px) 100vw, 2330px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/f33a3d8ed52879f33c5931bc3a017444/f79ca/7-docstring.png&quot;
            alt=&quot;Jupyter 노트북에서 docstring 확인&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Jupyter 노트북에서 docstring 확인&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;/figure&gt;
&lt;h3 id=&quot;소결론-doctest를-사용하여-테스트-코드를-작성하는-것만으로-레퍼런스-문서를-만들-수-있습니다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%86%8C%EA%B2%B0%EB%A1%A0-doctest%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C%EB%A5%BC-%EC%9E%91%EC%84%B1%ED%95%98%EB%8A%94-%EA%B2%83%EB%A7%8C%EC%9C%BC%EB%A1%9C-%EB%A0%88%ED%8D%BC%EB%9F%B0%EC%8A%A4-%EB%AC%B8%EC%84%9C%EB%A5%BC-%EB%A7%8C%EB%93%A4-%EC%88%98-%EC%9E%88%EC%8A%B5%EB%8B%88%EB%8B%A4&quot; aria-label=&quot;소결론 doctest를 사용하여 테스트 코드를 작성하는 것만으로 레퍼런스 문서를 만들 수 있습니다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;소결론: doctest를 사용하여 테스트 코드를 작성하는 것만으로 레퍼런스 문서를 만들 수 있습니다.&lt;/h3&gt;
&lt;figure style=&quot;max-width: 250px; margin: 1em auto&quot;&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 594px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 209.44444444444446%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAqCAYAAACz+XvQAAAACXBIWXMAAAsTAAALEwEAmpwYAAAEBElEQVR42q1WWU9bRxidX1OCIYQ1EFqxZaFAIgpYRKXqYyq1L5X63j5UecgiVepjGzciRqoCZbGJjcH7jrHdYnYExhuLQFCIWpqoasvp9w01AkQg97pXOpq5d+aee75vzsx3Rf1XL/DFUz++McbwaGiS8Ase/9c+HPz5QjwgPKa5X/4Ywq2vzRBtD0ax9/I3/LX/K17v7+H17y/x5/4uDv75G29zHRwc9fC5zgtx/b4d9w2z+MEVx3eOZXzvPGyfuBLQuZMX4okriaeeFL4dXcLthy6I6o7PUNRyD0XN93Cl5ZOjVg2q2j+FKL+sQc3VMtRWlePd8mLql6K67AoqiwsVo6KoACI/Lw+1NTW429mJ1jut6PqwC02Njbj0Th4KNQUoyNcogjAMDaGvtxe9z5/jp74+iYH+fhgNBlUQf7x6hUQigfn5edkmU0msr6/T6h1AzSU2NzfBWFtbk0in07Ld2NiQYPKT/XVk3zkLggl4YigUgtPhgMfrhc1qg8fjgc1mh8NhR3B8XN5bzBZqvchkMvLDZ0Gsrq7KCRxuPB7H0tISVlZW5LNUMoUMTTpNwO+8CYLDS6VSUmE0EsWIyYz5xUVsbW3J0Fn9aWTTcxbE3NwcZmZmMDU1hVgshsnJSYnp6WnwmFIIVma329FHdtHpdBgYGIBer0d3dzcmJiYQDAYxTjl8Wwh+iUmPtzwQCARkXylEthMOh6VSA5mTifk+J0KG3++H0+k8UpqzQpvNhn7adgHK2/+m0OfzyRwez6k6hZGIVDg4OES5dCAajapSeVIhrSyTummbeWkLul1uaRvVOXSQMvbj8PAwXhCMRiPcbrcipady6JOHgIdI2ELsRaVhn1DocjlhNpsxNjYmVTJphHKrZHGOKQzLFWYfskoX5Y+f5eRDzpfFYoHVaoWJTp2sfVQTejyHubPbbTBR6EEVBj9B6KVQrTarJB2lPOZmbA7Z5aLj30ohj8nFyZnQx/WEjO10OGU9GQ+pyCGryCrh4sTK2DLmkRGEJg7HlEBkj3wuAew5PiCCwYBckOyYEsiql0gkESAiP/kwRErZNul0Rhad8yrcmVUvW0a5dC5StVtejsu/iIvK5bmEyWQSkXCEjO2h/RuEnRaEP6KKkJVwXV5YWMAslcE4KZydnZXP3vR3cB7Ezs6O/CfhcLk+x2KTsuDv7u5he3sbPK4EwmwyyXOvp0cP/bNnsib3EEbIPjymFEJDP5w36hvwcddHaG/9ANr2dlRVVKAw/xIuazSKIcpLSvDetWu42dCA+ppaXK+rQ1lxsURFaaliiJKiIty6cRMdbW3QEu5qtejUdqC68qok5Q8qgeCXqiur0FBbhzstzbjd1ITm9xvpWaUqwn8BJ5IyYPZ1yv4AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/1ebf781bfe4d0e91a40ca0a574e9da61/33ecc/8-document-index.webp 360w,
/static/1ebf781bfe4d0e91a40ca0a574e9da61/263a4/8-document-index.webp 480w,
/static/1ebf781bfe4d0e91a40ca0a574e9da61/96755/8-document-index.webp 594w&quot;
              sizes=&quot;(max-width: 594px) 100vw, 594px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/1ebf781bfe4d0e91a40ca0a574e9da61/53918/8-document-index.png 360w,
/static/1ebf781bfe4d0e91a40ca0a574e9da61/9aebd/8-document-index.png 480w,
/static/1ebf781bfe4d0e91a40ca0a574e9da61/d32a3/8-document-index.png 594w&quot;
            sizes=&quot;(max-width: 594px) 100vw, 594px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/1ebf781bfe4d0e91a40ca0a574e9da61/d32a3/8-document-index.png&quot;
            alt=&quot;이 모든 모듈의 Reference 문서를 썼습니다&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;이 모든 모듈의 Reference 문서를 썼습니다&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;/figure&gt;
&lt;p&gt;라이브러리를 제작하면서 작성해야 했던 매우 많은 레퍼런스 문서를 테스트 코드로 활용하다 보니, 문서화 커버리지(즉, pytest 테스트 커버리지)를 거의 100%에 가깝게 넓힐 수 있었고, outdate 되지 않는 실제로 작동하는 코드만 작성된 레퍼런스 문서를 얻을 수 있었습니다.&lt;/p&gt;
&lt;h2 id=&quot;결론&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%B0%EB%A1%A0&quot; aria-label=&quot;결론 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;결론&lt;/h2&gt;
&lt;p&gt;MinIO 기반의 fixture를 활용하는 pytest + doctest기반의 테스트를 구성하여&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Amazon S3 의존성 없이 데이터 분석 라이브러리의 테스트 코드를 작성하였고&lt;/li&gt;
&lt;li&gt;모든 API 레퍼런스 문서가 테스트 코드로도 활용되는&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;테스트 환경을 만들 수 있었습니다. 이런 테스트 환경을 갖추었기에 라이브러리를 개발하는 데 있어서 적은 공수로도 탄탄하게 테스트를 할 수 있었으며 방대한 문서를 쉽게 작성하였습니다.&lt;/p&gt;
&lt;p&gt;저희 데이터플랫폼셀은 탄탄한 엔지니어링을 통해 이런 데이터가 잘 흐르게 하기 위한 기반을 만드는 일들을 하고 있습니다. 저희 셀에서 하는 일들에 관심이 있으시면 &lt;a href=&quot;https://careers.devsisters.com/position/?jobGroup%5B0%5D=18&amp;#x26;text=%EB%8D%B0%EC%9D%B4%ED%84%B0&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;데브시스터즈 채용 페이지&lt;/a&gt;를 확인해주세요 :)&lt;/p&gt;
&lt;p&gt;감사합니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[데이터 분석 라이브러리 개발기 (1)]]></title><description><![CDATA[데이터 분석가들이 PySpark를 통하여 여러가지 데이터를 쉽게 분석할 수 있도록 편의를 제공하는 데이터 분석 라이브러리인 DevPlay Analytics 라이브러리를 만든 과정을 소개합니다.]]></description><link>https://tech.devsisters.com/posts/devplay-analytics-library</link><guid isPermaLink="false">https://tech.devsisters.com/posts/devplay-analytics-library</guid><pubDate>Fri, 27 Nov 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;안녕하세요! 저는 데이터플랫폼셀에서 데이터 엔지니어로 일하고 있는 김민수입니다. 데이터플랫폼셀은 데브시스터즈의 모든 데이터가 흐르는 DevPlay Data Platform 을 만드는 팀입니다. 저희는 엔지니어링을 통하여 제품에서 발생하는 여러 가지 로그와 데이터가 잘 흐르도록 하는 일을 담당합니다. 이번 글에서는 데이터 분석가가 PySpark SQL을 잘 사용할 수 있도록 DevPlay 플랫폼 데이터 분석에 편의를 제공하는 라이브러리인 DevPlay Analytics를 만든 과정을 몇 편에 걸쳐서 소개하려고 합니다.&lt;/p&gt;
&lt;h2 id=&quot;pyspark를-통한-빅데이터-분석&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#pyspark%EB%A5%BC-%ED%86%B5%ED%95%9C-%EB%B9%85%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B6%84%EC%84%9D&quot; aria-label=&quot;pyspark를 통한 빅데이터 분석 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;PySpark를 통한 빅데이터 분석&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://spark.apache.org/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Apache Spark&lt;/a&gt;는 빅데이터를 위한 병렬 분산 처리 프레임워크 입니다. 그 중, Spark SQL 모듈을 사용하면 &lt;a href=&quot;https://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-hdfs/HdfsDesign.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;HDFS&lt;/a&gt;, &lt;a href=&quot;https://aws.amazon.com/s3/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;AWS S3&lt;/a&gt; 등 여러 가지 데이터 소스에서 &lt;a href=&quot;https://www.json.org/json-en.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;JSON&lt;/a&gt;, &lt;a href=&quot;https://parquet.apache.org/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Parquet&lt;/a&gt;, &lt;a href=&quot;https://cwiki.apache.org/confluence/display/Hive/LanguageManual+ORC&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;ORC&lt;/a&gt;, &lt;a href=&quot;https://docs.oracle.com/javase/8/docs/technotes/guides/jdbc/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;JDBC&lt;/a&gt; 등의 포맷으로 데이터 입출력을 지원하며 SQL 인터페이스를 통해 데이터를 쿼리할 수 있습니다.&lt;/p&gt;
&lt;figure style=&quot;max-width: 500px; margin: 1em auto&quot;&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1200px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 52.222222222222214%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAACRUlEQVR42pVSS2gTURR9jV8sppgiJcmMiloXibTEMZm8332ZTNOOUOpCSn7YIlLBpRu3AVeKblwoxYrFXxG0aKGhVBdaXChd2YUbpdRpKy5C3eh+vG+0Ii4EBy7vzNzzzrmfIeQ/n4CQtkCRrRqv143p1ZrRDL83SGT1rBH7k9u2eQYk0DjyM0L8O4JfvK/nDna0RswnX+rJubVacsqvGY/9qrH0t9i/npDzeTC+y6/ES34lOfGplrzxsZwwUXDMryUX/UpiNGQqpXZSSvdrbNvqAGPqOOcqn8/LHKWQEbbocQEy7b0D6aWyefLbmX3Bxunk5AeP7NB3/Ko5slY1JkOxVCq1nVO4yalsCaYuCCH2ShQCG7oRuxhHOOdpbcIYUEKGtwRj0ez4YHpGMnlqOJ+PvS+b59drxhu/YiaIw51eBc4yXrQw9gCDITR4hgZXCA4axY9xBlMYd7QZpXLgaK7QyOQLt9DkdpHJ6yvYPra94dfN/rBKzuQ9wWUTyUW8uIAtWpKpacHgKr4/ZTYMYTXXBIUHIJxZV8pRxdRzyeE7Y84hrfG2fjiqN02AQta27S4HinMgCu8EK1zSBCnUZeBq3gFnIjTlcEJw1cSYlVLGMbeAncyj6UWdbzQakbA6yeCFYHIF3cdxhncRt7CqV0i+77pup6CyibnXgooZxA/1vB2sCoVesvCUi5hzvZwXDf+EPquvQy+hp1RqFxweYWv9NkC33rw2ZIzt5jhn3QWuPeZ5XtSyrG3aTOfdrNtZtItdm/wf8dO6lp+Ib5wAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/c4685fc179e03f76a11f65ed60d5f18c/33ecc/1-spark-logo.webp 360w,
/static/c4685fc179e03f76a11f65ed60d5f18c/263a4/1-spark-logo.webp 480w,
/static/c4685fc179e03f76a11f65ed60d5f18c/0c8fb/1-spark-logo.webp 640w,
/static/c4685fc179e03f76a11f65ed60d5f18c/521f7/1-spark-logo.webp 860w,
/static/c4685fc179e03f76a11f65ed60d5f18c/cbd37/1-spark-logo.webp 1200w&quot;
              sizes=&quot;(max-width: 1200px) 100vw, 1200px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/c4685fc179e03f76a11f65ed60d5f18c/53918/1-spark-logo.png 360w,
/static/c4685fc179e03f76a11f65ed60d5f18c/9aebd/1-spark-logo.png 480w,
/static/c4685fc179e03f76a11f65ed60d5f18c/0f09e/1-spark-logo.png 640w,
/static/c4685fc179e03f76a11f65ed60d5f18c/3e3fe/1-spark-logo.png 860w,
/static/c4685fc179e03f76a11f65ed60d5f18c/64756/1-spark-logo.png 1200w&quot;
            sizes=&quot;(max-width: 1200px) 100vw, 1200px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/c4685fc179e03f76a11f65ed60d5f18c/64756/1-spark-logo.png&quot;
            alt=&quot;Apache Spark&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Apache Spark&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;/figure&gt;
&lt;p&gt;Spark는 기본적으로는 Scala API를 통하여 사용할 수 있는데요, PySpark라는 Python API도 제공하고 있어서 Python 환경에 익숙한 데이터 분석가분들도 어느 정도 편하게 사용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;DevPlay의 여러 가지 데이터는 Spark를 통하여 병렬 분산 처리를 하여 적재와 분석을 하고 있습니다. 그렇다면 데이터 분석가들이 어떻게 PySpark SQL을 통하여 데이터를 분석할 수 있을까요? 우선, 각 게임에서 발생하는 로그는 데이터 파이프라인을 통해 AWS S3에 최종적으로 적재되고 있습니다. 데이터 분석가는 PySpark를 통하여 이렇게 저장된 로그들(과 데이터)에 접근하여 쿼리를 해볼 수 있고, 그 결과를 다시 S3로 적재할 수 있습니다.&lt;/p&gt;
&lt;p&gt;PySpark API를 활용하여 데이터를 로드하고, 처리 후, 다시 저장하는 예시입니다.&lt;/p&gt;
&lt;figure id=&quot;code-example&quot;&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; pyspark&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;sql &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; SparkSession&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; functions &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; F

&lt;span class=&quot;token comment&quot;&gt;# 1. PySpark SQL의 API는 SparSession을 entry point로 접근할 수 있습니다.&lt;/span&gt;
spark&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; SparkSession &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; SparkSession&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;builder&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;getOrCreate&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 2. SparkSession을 통하여 AWS S3에 Parquet 포맷으로 저장된 로그를&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;#    DataFrame으로 로드합니다.&lt;/span&gt;
economy_log&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; DataFrame &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; spark&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;read&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;parquet&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;s3a://logging-test-game/log/partition=economy&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 3. DataFrame을 temp table로 등록해서 SQL로 쿼리합니다.&lt;/span&gt;
economy_log&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;registerTempTable&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;economy_log&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
economy_table&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; DataFrame &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; spark&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;sql&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token triple-quoted-string string&quot;&gt;&quot;&quot;&quot;
      SELECT mid, sum(properties.amountDelta) AS amount
        FROM economy_log
    GROUP BY mid
&quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 3-1. 또는 DataFrame API를 통하여 쿼리할 수도 있습니다.&lt;/span&gt;
economy_table&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; DataFrame &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    economy_log
      &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;groupBy&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;mid&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;agg&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;F&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;sum&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;F&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;col&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;properties.amountDelta&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;alias&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;amount&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;select&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;mid&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;amount&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 4. 결과물을 S3로 다시 저장합니다.&lt;/span&gt;
economy_table&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;write&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;parquet&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;s3a://analytics-test-game/dw/f_economy&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;이 예시와 같은 PySpark를 사용한 데이터 분석은 주로 &lt;a href=&quot;https://jupyter.org/hub&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Jupyterhub&lt;/a&gt;을 통하여 진행하고 있습니다.(이것과 관련된 자세한 부분은 추후 Kubernetes 기반의 데이터 플랫폼과 관련된 글에서 다룰 예정입니다)&lt;/p&gt;
&lt;figure style=&quot;max-width: 500px; margin: 1em auto&quot;&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 38.611111111111114%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB6klEQVR42mN4bKH4+oG1ysl7Vir1j9y0hBhg4PjlTIYD51TB7P//GRmIBQ/MFEq/GYr8/Wsp8/+pp9Gx//X2PAzHrpYynr31geHKB6FoJxstd39/hcBAD5mAAE+1gIAANR8fHy6odkYoRgL//zOFT956cHew9///bir/d/RX32I4d+8v45nr19OMjVmD/PwcgYYEBfr6evj7+zsE+vl5R/r4iEB1MyEZCsFcnW+nMCz+/9+46+zfHUVJf/X3HvrNcPPFf+bjV/pBOnxCQ22CgoI0vL29pUNDQ9lCQz1EgQYngAwHudTPz48XKM4Mwvb29iwMjI1//zM3Pf/LvLb1D8uxJX+ZH/39z3bu1mGGHcfA4ekf6N8AdFUiyKX+/r4JwcEBkSEhQTUhIYFZQPHAIH//8ABfX/+AAL+kgAAfTwbGOqCfV6++xnzV/hfz+qD/vKeXvuK7clYFZFhAeLhmoL9/LhCnA3EW0OuRQUHeSoGBgU4gS4D8+IAA3zAgzgTKtwD5MQyM1e9WgmKRcbf/BIbtnv8Ztjn+Z1jjtIHhTBpXmrEPF8hbIAw0RBLoLR4PDw92IOYDiYEiycsrVAIkDsSiIO8zMBQ/5wYH76pVzAxr3GMZ1npuZVjr/pBhrVszWLweGPCEkw08UgDqBrzW1y/tZwAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/49a51d493c62089916c0c90eeebf2f12/33ecc/2-airflow-logo.webp 360w,
/static/49a51d493c62089916c0c90eeebf2f12/263a4/2-airflow-logo.webp 480w,
/static/49a51d493c62089916c0c90eeebf2f12/0c8fb/2-airflow-logo.webp 640w,
/static/49a51d493c62089916c0c90eeebf2f12/521f7/2-airflow-logo.webp 860w,
/static/49a51d493c62089916c0c90eeebf2f12/d71bc/2-airflow-logo.webp 1024w&quot;
              sizes=&quot;(max-width: 1024px) 100vw, 1024px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/49a51d493c62089916c0c90eeebf2f12/53918/2-airflow-logo.png 360w,
/static/49a51d493c62089916c0c90eeebf2f12/9aebd/2-airflow-logo.png 480w,
/static/49a51d493c62089916c0c90eeebf2f12/0f09e/2-airflow-logo.png 640w,
/static/49a51d493c62089916c0c90eeebf2f12/3e3fe/2-airflow-logo.png 860w,
/static/49a51d493c62089916c0c90eeebf2f12/42a19/2-airflow-logo.png 1024w&quot;
            sizes=&quot;(max-width: 1024px) 100vw, 1024px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/49a51d493c62089916c0c90eeebf2f12/42a19/2-airflow-logo.png&quot;
            alt=&quot;Apache Airflow&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Apache Airflow&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;/figure&gt;
&lt;p&gt;이렇게 진행된 분석 결과를 재사용할 필요가 있거나 매일 실행해서 저장할 필요가 있으면, 로그 적재 후 매일 분석 작업을 실행해야 합니다. 데이터 플랫폼에서는 &lt;a href=&quot;https://airflow.apache.org/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Apache Airflow&lt;/a&gt;를 통하여 이런 분석 작업들의 워크플로우를 관리하고 스케줄링합니다.&lt;/p&gt;
&lt;h2 id=&quot;문제-정의&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%B8%EC%A0%9C-%EC%A0%95%EC%9D%98&quot; aria-label=&quot;문제 정의 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;문제 정의&lt;/h2&gt;
&lt;p&gt;점점 다양한 게임들이 DevPlay Data Platform을 사용하게 되고, 거의 모든 분석에서 PySpark를 사용하게 되면서 몇 가지 문제점을 발견할 수 있었습니다.&lt;/p&gt;
&lt;h3 id=&quot;문제-1-많은-경로를-일일히-입력해야-해서-불편하다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%B8%EC%A0%9C-1-%EB%A7%8E%EC%9D%80-%EA%B2%BD%EB%A1%9C%EB%A5%BC-%EC%9D%BC%EC%9D%BC%ED%9E%88-%EC%9E%85%EB%A0%A5%ED%95%B4%EC%95%BC-%ED%95%B4%EC%84%9C-%EB%B6%88%ED%8E%B8%ED%95%98%EB%8B%A4&quot; aria-label=&quot;문제 1 많은 경로를 일일히 입력해야 해서 불편하다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;문제 1: 많은 경로를 일일히 입력해야 해서 불편하다&lt;/h3&gt;
&lt;p&gt;DevPlay 플랫폼의 게임 종류가 점점 다양해지고, 또한, 데이터 계층도 많아지면서 각각의 데이터 입출력 경로를 관리하기 어려워졌습니다. &lt;a href=&quot;#code-example&quot;&gt;위의 예시&lt;/a&gt;에서 &lt;code&gt;s3a://logging-test-game/log/partition=economy&lt;/code&gt; 와 같은 형태로 데이터의 경로를 설정하는데, 데이터 분석가는 각 게임과 데이터의 계층에 따라 다른 이 수많은 경로를 기억하고 사용해야 합니다. 그리고 만약 이 데이터 경로의 형태를 migration 할 경우, 데이터 엔지니어는 모든 분석 파일들을 일일이 확인하여 기존 경로의 사용 여부를 확인해야 합니다. 특정 제품만 담당하는 데이터 엔지니어링 팀의 경우 이런 경로 복잡성에 대한 고민을 약간은 덜 수 있을 것 같은데요, 플랫폼에서는 많은 데이터 소비자에게 쉬운 방식으로 데이터 접근을 제공할 필요가 있어서 고민이 되었습니다.&lt;/p&gt;
&lt;h3 id=&quot;문제-2-credential을-포함한-설정값들을-관리해야-한다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%B8%EC%A0%9C-2-credential%EC%9D%84-%ED%8F%AC%ED%95%A8%ED%95%9C-%EC%84%A4%EC%A0%95%EA%B0%92%EB%93%A4%EC%9D%84-%EA%B4%80%EB%A6%AC%ED%95%B4%EC%95%BC-%ED%95%9C%EB%8B%A4&quot; aria-label=&quot;문제 2 credential을 포함한 설정값들을 관리해야 한다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;문제 2: credential을 포함한 설정값들을 관리해야 한다&lt;/h3&gt;
&lt;p&gt;또한, 어떤 데이터에 접근하기 위해서는 다양한 설정값이 필요합니다. 이런 설정은 GitHub private repository에 그냥 적어도 되는 경우도 있지만, 절대 커밋을 하면 안 되고, 심지어는 슬랙과 같은 메시지를 통해서 전달하면 안 되는 경우도 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;문제-3-분석-프로젝트가-점점-복잡해져서-노트북과-batch-환경이-달라진다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%B8%EC%A0%9C-3-%EB%B6%84%EC%84%9D-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EA%B0%80-%EC%A0%90%EC%A0%90-%EB%B3%B5%EC%9E%A1%ED%95%B4%EC%A0%B8%EC%84%9C-%EB%85%B8%ED%8A%B8%EB%B6%81%EA%B3%BC-batch-%ED%99%98%EA%B2%BD%EC%9D%B4-%EB%8B%AC%EB%9D%BC%EC%A7%84%EB%8B%A4&quot; aria-label=&quot;문제 3 분석 프로젝트가 점점 복잡해져서 노트북과 batch 환경이 달라진다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;문제 3: 분석 프로젝트가 점점 복잡해져서 노트북과 batch 환경이 달라진다&lt;/h3&gt;
&lt;p&gt;분석 프로젝트의 규모가 점점 커지면서 개발 효율성을 위해 공통 동작에 대해서는 유틸 함수를 정의하여 사용하게 되었습니다. 예를 들어, 아래 예시에서처럼 Spark DataFrame을 저장할 때 &lt;code&gt;repartition&lt;/code&gt;, &lt;code&gt;saveMode&lt;/code&gt;, &lt;code&gt;partitionBy&lt;/code&gt; 등의 옵션을 지정해주어야 하는데, 매번 이런 옵션들을 지정하여 저장하는게 귀찮고 실수로 잘못 설정할 여지도 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;economy_table
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;repartition&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# 적절한 개수로 파티션을 나눠줘야 이후 데이터 분석에서 효율적으로 사용&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;write
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;parquet&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;&quot;s3a://analytics-test-game/dw/f_economy&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        mode&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;overwrite&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# overwrite 모드로 저장하지 않으면 재적재 시 에러&lt;/span&gt;
        partitionBy&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;date&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# fact table의 경우 date 컬럼으로 파티션됨&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그래서 이 문제를 해결하기 위해, Airflow 위에서 돌아가는 batch 프로젝트에서는 &lt;code&gt;save_dataframe&lt;/code&gt;와 같은 유틸 함수를 정의하여 사용하게 되었습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;save_dataframe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    df&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; DataFrame&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    s3a_address&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    partition_by&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; List&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    repartition&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    save_mode&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;str&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;overwrite&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    df \
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;repartition&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;repartition&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; \
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;write \
        &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;parquet&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
            s3a_address&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            mode&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;save_mode&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            partitionBy&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;partition_by&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그런데 이런 Airflow batch 분석 코드에서 사용되는 유틸 함수가 점점 늘어나면서, 같은 유틸 함수를 참조할 수 없는 Jupyter Notebook 환경에서 불편함이 생겼습니다. 결과적으로 1. Jupyter 환경에서 코드를 작성하여 테스트한 후 → 2. 다시 Airflow 환경 버전으로 작성한 다음 → 3. batch 환경에서 테스트를 마쳐야 분석 코드를 batch 환경으로 배포할 수 있게 되면서, 이 사이클이 너무 길고 어려워져 많은 시간이 소요되었습니다. 따라서, 두 환경에서 최대한 같은 방법으로 데이터 분석을 가능케 할 방법이 필요해졌습니다.&lt;/p&gt;
&lt;h3 id=&quot;문제-4-애초에-pyspark이-분석가에게-친화적이지-않다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%AC%B8%EC%A0%9C-4-%EC%95%A0%EC%B4%88%EC%97%90-pyspark%EC%9D%B4-%EB%B6%84%EC%84%9D%EA%B0%80%EC%97%90%EA%B2%8C-%EC%B9%9C%ED%99%94%EC%A0%81%EC%9D%B4%EC%A7%80-%EC%95%8A%EB%8B%A4&quot; aria-label=&quot;문제 4 애초에 pyspark이 분석가에게 친화적이지 않다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;문제 4: 애초에 PySpark이 분석가에게 친화적이지 않다&lt;/h3&gt;
&lt;p&gt;처음 데브시스터즈에서 Spark을 도입했을 때에는 소프트웨어 엔지니어가 데이터 분석 업무를 같이 담당하고 있었습니다. 따라서 PySpark 의 reference 문서나 소스를 기반으로 분석 코드를 작성하는 게 어색하지 않았습니다. 하지만 점점 SQL 기반의 분석이 익숙하신 분석가분들이 많이 합류하시면서, PySpark를 통한 데이터 분석 방법을 개선시킬 필요가 있었습니다. 단적으로, 제가 이 블로그 글을 쓰기 위해서 PySpark API 문서를 조금 살펴봤는데요, 이 문서들은 소프트웨어 엔지니어에게 PySpark의 컨셉을 설명해주기 위한 문서들로 분석가가 레퍼런스 삼아 분석에 활용하기에는 친화적이지 않다는 생각이 강하게 들었습니다. 그렇기에, 데이터 분석가분들이 이런 척박한 환경에서 데이터 분석을 진행하면서 쌓은 노하우를 사내 Notion에 많이 정리해두기는 했으나, 여전히 러닝 커브가 컸습니다.&lt;/p&gt;
&lt;h2 id=&quot;devplay-analytics-라이브러리-개발&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#devplay-analytics-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EA%B0%9C%EB%B0%9C&quot; aria-label=&quot;devplay analytics 라이브러리 개발 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;DevPlay Analytics 라이브러리 개발&lt;/h2&gt;
&lt;p&gt;데이터플랫폼셀은 이러한 문제를 해결하기 위해 DevPlay Analytics 라이브러리를 개발했습니다. 이 라이브러리를 통해 일관된 방법으로 데이터 입출력이 가능할 뿐만 아니라, 각종 설정을 데이터 분석 코드상에 남기지 않고 관리할 수 있습니다. 무엇보다도 DevPlay Analytics는 계층별 데이터를 테이블 형태로 추상화하여 제공함으로써, 데이터 분석가에게 가장 친숙한 인터페이스인 SQL를 통한 데이터 접근을 가능케 합니다. 풍부한 분석 예시를 곁들인 레퍼런스 문서 또한 데이터 분석의 편의성을 높여 줍니다.&lt;/p&gt;
&lt;h3 id=&quot;개발-목표&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B0%9C%EB%B0%9C-%EB%AA%A9%ED%91%9C&quot; aria-label=&quot;개발 목표 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;개발 목표&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. 데이터 계층 추상화&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;각 계층별 데이터를 모듈로 접근하며 테이블에 쿼리하는 듯한 인터페이스 형태를 구상했습니다. &lt;a href=&quot;#code-example&quot;&gt;앞선 예시 코드&lt;/a&gt;에서 데이터 분석가에게 필요한 데이터 API는 &lt;code&gt;&quot;log 계층&lt;/code&gt;의 &lt;code&gt;economy 테이블&lt;/code&gt;을 불러와서 &lt;code&gt;Data Warehouse 계층&lt;/code&gt;의 &lt;code&gt;f_economy 테이블&lt;/code&gt;로 저장한다&quot; 라고 할 수 있습니다. 분석 예시 사례를 편하게 작성할 수 있도록 DevPlay Analytics에서는 데이터 경로 대신 데이터 계층에서 테이블을 불러오는 형태로,&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;devplay_analytics&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;log&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;load&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;economy&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# log 계층의 economy 테이블을 불러온다&lt;/span&gt;
devplay_analytics&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;dw&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;load&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;f_economy&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# DW 계층의 f_economy 테이블을 불러온다&lt;/span&gt;
devplay_analytics&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;dw&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;save&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;df&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;f_economy&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# DataFrame을 DW 계층의 f_economy 테이블로 저장한다&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;위와 같은 형상으로 데이터를 접근하도록 하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. SQL 인터페이스&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;데이터 분석가에게 가장 익숙한 인터페이스인 SQL만으로도 데이터 로드를 할 수 있게 쿼리 인터페이스를 제공하며,&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;devplay_analytics&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;sql&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;query&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token triple-quoted-string string&quot;&gt;&quot;&quot;&quot;
      SELECT mid, sum(properties.amountDelta) AS amount
        FROM log_economy
    GROUP BY mid
&quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;save&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;devplay_analytics&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;dw&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;f_economy&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;쿼리의 결과를 데이터 계층 모듈과 테이블 이름을 지정하여 저장할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. 레퍼런스 문서 제공&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;라이브러리의 모든 API를 레퍼런스 문서로 제공하고, 주로 사용하는 분석 패턴들을 기반으로 API 사용 예시를 추가하여, 데이터 분석가분들이나 관련 지식이 있는 엔지니어라면 누구나 레퍼런스 문서만으로도 충분히 데이터 분석을 할 수 있도록 만들고자 하였습니다.&lt;/p&gt;
&lt;h3 id=&quot;라이브러리의-구조&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC%EC%9D%98-%EA%B5%AC%EC%A1%B0&quot; aria-label=&quot;라이브러리의 구조 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;라이브러리의 구조&lt;/h3&gt;
&lt;figure style=&quot;max-width: 500px; margin: 1em auto&quot;&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 519px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 84.72222222222221%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAARCAYAAADdRIy+AAAACXBIWXMAAAsTAAALEwEAmpwYAAADTElEQVR42nWUiW7iSBCGef8H2BdYaaTVjjKbWTK5QyYHEA6bwxjMfdgctsGAwRyBfFs4ySiRdgr9dPff7b+qq8qOeIlHJunUL4wFXjYTjh/5yVOSIJVkm0nhJxOc3ab5fq8SvVc4eVA5uklR7w6JeLUqL4i9hP+hOaMhz8/PvNKv/GY2o/IjSuHrX2ROTvlbaXOZLHL5pHGVKolojpZli+DBu6bhCWYCs6ARK7UpFyvMtQKTYkH2inh5lZncxk8+Mo7HuXjMcy6CF2+iUYm2aYpgtWbTrA+pGgOatSGqbhEt28T1vvAurZZHs+lRqw8wWirVdo5qM8NdRuEsoYWCB/x7p4jgiEjNmmK7Do3+hF5/QHfgMpRxZLsojRk32aZEUOeh1KUyqVH1DErjCqdplbOHPGePOW4yZS4kynbfJaJ3Z9R7LuXulIbpUelJNDLWxZHWC1AbUwqthcynxHIxrtJn/PN4wXG+hWmamJbF0ektf/x5RLFSJ1LsNqj3R5TNHsVOm5o1kIc7aOHcQrdaaIOGoE66kSJdS3CrxznOlikZjVAkGkvyJRqj0uwRqferTFwP0zYlB02m4wlDp02708AeORh9g3w/hzbU0O0yZadCyda5lBye3Of4IdW9TuucJopyZYdIxcrKFXMYpkLVzFLoFDgVz/G6JnkV3hJ+IBjm3pDHkPVPRZFilMIKH9rntcpSlKWdYO6o+ILAVen2JNkVi2JLZ+lk8W2V+Uhl5aTYT5PsPWnwcZLzTI7ztyp/ahvfNdhJ776EP9hJQztWl/1+F64P7GE/mDZYDH5STHzBKF9zVOhykchzlcxznfoQYSAR+iOFha2wFLiDLEqrRqeXl6gOESrh/tpNwfyANOvJE1eSw+O7PF+fDL7FS3y/zbwKsl/DfsWLANY4izmX7RmGOw3XIR/iMBfsXrnn7ZrhLCAmbVcc+azWG553OxH8YJvNlrVsbNcrNjKuVptf7/L/2XoVSIq2BAtfnt2E3CdBbzLB930Wy6Vgged54mD9W8GZfDA6nQ6dbpf5fP5Z8BDJUoS22y2O47BYLML5arX6reDhfLVaDUXfHX+KMAiC8JBhGLiuG4q9f8Y+2nsaer0euq5TKpXC8wfB/wDNNtf4/XGLGAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/b6348a25833c4f7cb885f0c33db9de57/33ecc/3-devplay-analytics-overview.webp 360w,
/static/b6348a25833c4f7cb885f0c33db9de57/263a4/3-devplay-analytics-overview.webp 480w,
/static/b6348a25833c4f7cb885f0c33db9de57/ed983/3-devplay-analytics-overview.webp 519w&quot;
              sizes=&quot;(max-width: 519px) 100vw, 519px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/b6348a25833c4f7cb885f0c33db9de57/53918/3-devplay-analytics-overview.png 360w,
/static/b6348a25833c4f7cb885f0c33db9de57/9aebd/3-devplay-analytics-overview.png 480w,
/static/b6348a25833c4f7cb885f0c33db9de57/d6ae3/3-devplay-analytics-overview.png 519w&quot;
            sizes=&quot;(max-width: 519px) 100vw, 519px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/b6348a25833c4f7cb885f0c33db9de57/d6ae3/3-devplay-analytics-overview.png&quot;
            alt=&quot;라이브러리 구조 Overview&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;라이브러리 구조 Overview&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;/figure&gt;
&lt;p&gt;개발 목표 요구사항을 만족시키기 위하여, 라이브러리에서 제공하여야 하는 기능을 모듈로 정의하고, 각 모듈을 계층 단위로 나누어 설계하였습니다. 여기서 가장 중요한 계층은 데이터 계층인데요, 데이터 계층은 &lt;code&gt;Log 모듈&lt;/code&gt;, &lt;code&gt;Data Warehouse 모듈&lt;/code&gt; 등 데이터를 테이블 형태로 조회, 저장하는 기능을 제공합니다. 각 계층은 아래 계층의 기능을 호출하는 방식으로 연동되게 구현하였습니다. 예를 들어, SQL 계층은 데이터 계층을 호출하여 테이블을 로드하는데요, 데이터 계층은 IO 계층을 호출하여 S3로 입출력합니다.&lt;/p&gt;
&lt;p&gt;그리고 각 계층은 설정 계층을 호출하여 필요한 설정과 credential을 불러오고 있습니다. 이 설정 파일은 &lt;a href=&quot;https://www.vaultproject.io/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Vault&lt;/a&gt;를 활용하여 credential을 각자 권한에 맞게 렌더링할 수 있는 형태입니다. 게임 종류와 환경에 대한 맥락은 &lt;code&gt;devplay_analytics&lt;/code&gt;라는 객체에 저장하고 있기에, 맥락에 따라 달라지는 설정을 설정 계층에서 불러올 수 있게 구현되어 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; devplay_analytics &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; build

&lt;span class=&quot;token comment&quot;&gt;# test_game의 dev환경에 대한 devplay_analytics 객체를 객체 생성하고&lt;/span&gt;
devplay_analytics &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; build&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;test_game&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;dev&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# 해당 환경의 f_economy라는 DW를 로드합니다.&lt;/span&gt;
devplay_analytics&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;dw&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;load&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;f_economy&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# 이때, 어떤 경로로부터 데이터를 불러올지는&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# DW 모듈에서 적절한 설정을 설정 계층으로부터 읽어서 IO 계층을 호출합니다.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;계층별 기능을 구현하면서 기존 분석 환경에서 여러 곳으로 흩어져있던 많은 유틸 기능을 각 역할에 맞게 배치하였습니다. 예를 들어, 데이터 계층에서는 공통적으로 데이터 저장 전에 적절한 파티션 개수를 계산하여 repartition하는 로직을 구현했습니다. 이외에도 분석에서 사용되는 유틸 함수들의 경우 &lt;code&gt;devplay_analytics.util&lt;/code&gt; 에서 제공해주어 노트북 환경과 batch 환경 모두에서 사용할 수 있도록 하였습니다.&lt;/p&gt;
&lt;p&gt;라이브러리 모듈 구조 최상단의 SQL 모듈에서는 데이터 계층 모듈을 테이블 형태로 SQL로 접근할 수 있게 하였습니다. SQL 모듈에서 데이터 계층 테이블을 참조하면 Spark SQL temp table로 등록한 후 쿼리를 실행합니다. 예를 들어, &lt;code&gt;log_economy&lt;/code&gt; 라는 테이블로 쿼리하면 &lt;code&gt;devplay_analytics.log&lt;/code&gt; 모듈에서 로드를 하며, &lt;code&gt;dw_billing&lt;/code&gt;이라는 테이블로 쿼리하면 &lt;code&gt;devplay_analytics.dw&lt;/code&gt; 모듈에서 데이터를 로드합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;devplay_analytics&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;sql&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;query&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token triple-quoted-string string&quot;&gt;&quot;&quot;&quot;
      SELECT mid, sum(properties.amountDelta) AS amount
        FROM log_economy
    GROUP BY mid
&quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;save&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;devplay_analytics&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;dw&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;f_economy&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그리고 쿼리 결과를 데이터 경로를 따로 설정하지 않고, &lt;code&gt;devplay_analytics.dw&lt;/code&gt;모듈 과 &lt;code&gt;&quot;f_economy&quot;&lt;/code&gt;라는 테이블 이름만 지정하여 저장되도록 하였습니다.&lt;/p&gt;
&lt;h3 id=&quot;레퍼런스-문서-제공&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A0%88%ED%8D%BC%EB%9F%B0%EC%8A%A4-%EB%AC%B8%EC%84%9C-%EC%A0%9C%EA%B3%B5&quot; aria-label=&quot;레퍼런스 문서 제공 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;레퍼런스 문서 제공&lt;/h3&gt;
&lt;p&gt;모든 API에서 docstring으로 함수 사용 방법에 대한 스펙과 예시 코드를 작성하고 &lt;a href=&quot;https://www.sphinx-doc.org/en/master/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Sphinx&lt;/a&gt;를 사용하여 레퍼런스 문서로 제공합니다. 또한, pytest doctest 모듈을 사용해서 모든 docstring을 테스트 코드로도 활용하면서, 레퍼런스로 작성된 코드가 outdate되지 않고 실제로 작동하는 것을 보장합니다.&lt;/p&gt;
&lt;figure style=&quot;max-width: 800px; margin: 1em auto&quot;&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1752px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 52.5%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABxElEQVR42o2STUsbURSG80fajZTGVCgxJkK3oTs3XZS6aSNKNFnpRuiuXbQL1xYE29/QjdRSyNJ/IFgqxs185cZ8zEySmTufr+fcSBpoNB14OXfOuTycc+6bKX84wdqnU6x9PkVp/zt2v50hjiP0bQeu6yKKonsVkzw/QM/xYA98hGGEzJcf5zhuXOJr4w8Of17g6NdvBFSQvoSUkuDxg2KwDEIEJP7PbNb28PptFeuVHbx6U8H7jwdUSCCMHlx7hCSJ50KTqXPmRamIpadP8Dy3iIXHj7C18Q78WR2XxpBI04S6mA+dAF+Wy1hZXsZqkcC5HOq1GnzfR7N5heb1NUajEeKEoHH04D7VThlYJNhKPo9SoYBn2Szq29sIgwCiLdSlhGBpmv6XFJBh08AaAQMCapoOy7JgkizTRKvVgknRMi2VV7W7vKEbEGLcwD9A7lBKH4ZhwLZt9Hp9FR2ykd3vw3FsspNDcWwrFtc9z1PTzATyDoVoq07ZOizOKTuFsfKeTwCGTOqkmR3yyFzQNE1Jp3HGUYdhtqCbApa4mXQ5GAyUhsPhPcBqVS232+2i0+mqLnnhPA57kpXePdQ49/fMj3ILLcrrMAYbCNwAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/8d00ba2a1dcf3efc0e9f3c5691262a0a/33ecc/4-sphinx-example-1.webp 360w,
/static/8d00ba2a1dcf3efc0e9f3c5691262a0a/263a4/4-sphinx-example-1.webp 480w,
/static/8d00ba2a1dcf3efc0e9f3c5691262a0a/0c8fb/4-sphinx-example-1.webp 640w,
/static/8d00ba2a1dcf3efc0e9f3c5691262a0a/521f7/4-sphinx-example-1.webp 860w,
/static/8d00ba2a1dcf3efc0e9f3c5691262a0a/cbd37/4-sphinx-example-1.webp 1200w,
/static/8d00ba2a1dcf3efc0e9f3c5691262a0a/519b2/4-sphinx-example-1.webp 1752w&quot;
              sizes=&quot;(max-width: 1752px) 100vw, 1752px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/8d00ba2a1dcf3efc0e9f3c5691262a0a/53918/4-sphinx-example-1.png 360w,
/static/8d00ba2a1dcf3efc0e9f3c5691262a0a/9aebd/4-sphinx-example-1.png 480w,
/static/8d00ba2a1dcf3efc0e9f3c5691262a0a/0f09e/4-sphinx-example-1.png 640w,
/static/8d00ba2a1dcf3efc0e9f3c5691262a0a/3e3fe/4-sphinx-example-1.png 860w,
/static/8d00ba2a1dcf3efc0e9f3c5691262a0a/64756/4-sphinx-example-1.png 1200w,
/static/8d00ba2a1dcf3efc0e9f3c5691262a0a/c0b05/4-sphinx-example-1.png 1752w&quot;
            sizes=&quot;(max-width: 1752px) 100vw, 1752px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/8d00ba2a1dcf3efc0e9f3c5691262a0a/c0b05/4-sphinx-example-1.png&quot;
            alt=&quot;레퍼런스 문서 예시 - 1&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;레퍼런스 문서 예시 - 1&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1762px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 71.11111111111111%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAAAsTAAALEwEAmpwYAAACgklEQVR42o2Ty2sTURTG+6+UFt21tLRoQQq6rLh26bLupLbBjQt3ogtFcOWmtVjduHChoFCCYGkg0NIEhTQ1j6aZzEzmce+8MpNH83nOTadUNzbwYxiS+eU757szdvvpNu6+/I57r38obj35io+5GrqhRK1WR6/XA3+Gw+EFTs8ZDAbn9Pt9jD149QnPN7/h2bttvPiQxeM3X/A5V0IY+NAtB9IPIIMQwie8YIQfQYaJotvr/y1cXc0gk3mElZWHWFvLYHn5PrLZLJIB8POwil/lKsq1Bkq/66gca2i1XWiWhC470EUHSW+A4SknPR0JG40GqtUKWpqGttmGEAJx3EG3m+C4XodpGNANC5phExZ0EhqWgG7aaBG+76PT4d93R8LCQQH5fB7lchlHR0colQ7hOA4tDTg5aaJSqaJKu2w2NdiOC5f+UEqPrnKEK+B5HqIoGglzuRx2dnZQLBZRKBSwt78Pg1LxJ4xiuKYJzzIRRyESeqiXJKoops+QJL1nxjjd7u4uiiQrHBxgf28Phq4roU9FWCR02234rguP8KVEGIYIgkAl4yvfM0p4Z2kJzI2FBdxcXMTM1BTeb20poSt8mM0mbCLkXcUxjdZR4/0L71EJWTA7Pa2Yn53F5Pg4NtbXlVAIDzalk7TTiBKkD3MaFqTJwrPvlJAlKdfn53F1chKbGxtKKOnMObRPQYQ0KkuYmJKmV243RQnnZmaQcm1uDlcmJvD2TOhSQkNrwW61EJAwTZfC0oRKSvmvUHo+HCpF0tgBFRCmI18YnUtR4sskFFSKRY27bXO0QyomolcyoiPE417kUgkFHWBOKLgY24JH5Xg2vR1SqHT8lqQp02PzB0HowuVyhtl1AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/c2f892d5cafbb42116070ec568d749d8/33ecc/5-sphinx-example-2.webp 360w,
/static/c2f892d5cafbb42116070ec568d749d8/263a4/5-sphinx-example-2.webp 480w,
/static/c2f892d5cafbb42116070ec568d749d8/0c8fb/5-sphinx-example-2.webp 640w,
/static/c2f892d5cafbb42116070ec568d749d8/521f7/5-sphinx-example-2.webp 860w,
/static/c2f892d5cafbb42116070ec568d749d8/cbd37/5-sphinx-example-2.webp 1200w,
/static/c2f892d5cafbb42116070ec568d749d8/b3e2f/5-sphinx-example-2.webp 1762w&quot;
              sizes=&quot;(max-width: 1762px) 100vw, 1762px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/c2f892d5cafbb42116070ec568d749d8/53918/5-sphinx-example-2.png 360w,
/static/c2f892d5cafbb42116070ec568d749d8/9aebd/5-sphinx-example-2.png 480w,
/static/c2f892d5cafbb42116070ec568d749d8/0f09e/5-sphinx-example-2.png 640w,
/static/c2f892d5cafbb42116070ec568d749d8/3e3fe/5-sphinx-example-2.png 860w,
/static/c2f892d5cafbb42116070ec568d749d8/64756/5-sphinx-example-2.png 1200w,
/static/c2f892d5cafbb42116070ec568d749d8/e1c02/5-sphinx-example-2.png 1762w&quot;
            sizes=&quot;(max-width: 1762px) 100vw, 1762px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/c2f892d5cafbb42116070ec568d749d8/e1c02/5-sphinx-example-2.png&quot;
            alt=&quot;레퍼런스 문서 예시 - 2&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;레퍼런스 문서 예시 - 2&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;/figure&gt;
&lt;p&gt;(DevPlay Analytics 라이브러리의 테스팅과 문서화에 대해서는 다음으로 이어지는 글에서 좀 더 자세히 설명하려고 합니다. 데이터 분석 라이브러리의 테스트 방법에 대해 궁금하신 분들은 다음 글도 꼭 읽어주세요!)&lt;/p&gt;
&lt;h3 id=&quot;devplay-analytics로-작성한-분석-코드&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#devplay-analytics%EB%A1%9C-%EC%9E%91%EC%84%B1%ED%95%9C-%EB%B6%84%EC%84%9D-%EC%BD%94%EB%93%9C&quot; aria-label=&quot;devplay analytics로 작성한 분석 코드 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;DevPlay Analytics로 작성한 분석 코드&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;#code-example&quot;&gt;앞선 예시의 분석 코드&lt;/a&gt;를 DevPlay Analytics 버전으로 작성하면 아래와 같습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;python&quot;&gt;&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; devplay_analytics &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; build
&lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; pyspark&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;sql &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; SparkSession&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; functions &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; F

&lt;span class=&quot;token comment&quot;&gt;# 1. DevPlay Analytics 객체를 생성합니다.&lt;/span&gt;
devplay_analytics &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; build&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;test_game&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;prod&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 2. Log 계층에서 economy 로그를 로드하여 가공한 후 테이블로 만듭니다.&lt;/span&gt;
economy_table &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; devplay_analytics&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;sql&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token triple-quoted-string string&quot;&gt;&quot;&quot;&quot;
      SELECT mid, sum(properties.amountDelta) AS amount
        FROM log_economy
    GROUP BY mid
&quot;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 2-1. 또는 Log 계층에서 economy 로그를 DataFrame으로 로드한 후 가공합니다.&lt;/span&gt;
economy_df &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    devplay_analytics&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;log&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;load&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;economy&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;groupBy&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;mid&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;agg&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;F&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;sum&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;F&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;col&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;properties.amountDelta&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;alias&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;amount&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;select&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;mid&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;amount&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# 3. 쿼리 결과를 DW 계층의 economy 테이블로 저장합니다.&lt;/span&gt;
economy_table&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;save&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;devplay_analytics&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;dw&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;f_economy&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# 3-1. DataFrame의 경우&lt;/span&gt;
devplay_analytics&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;dw&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;save&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;economy_df&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;f_economy&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;데이터 분석가는 어떤 경로에서 데이터를 읽어와서 저장해야 하는지를 신경 쓰지 않고, 각 데이터 계층에서 테이블을 로드/저장하는 방식으로 분석 코드를 작성할 수 있습니다. 그리고 이 분석 코드를 그대로 복사하여 Airflow batch 프로젝트에서 사용해도 동일한 동작을 보장합니다.&lt;/p&gt;
&lt;h2 id=&quot;결과&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%B0%EA%B3%BC&quot; aria-label=&quot;결과 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;결과&lt;/h2&gt;
&lt;p&gt;DevPlay Analytics 라이브러리 1.0 버전을 릴리즈한 후, 거의 모든 DevPlay 분석 코드가 라이브러리를 통하여 작성되었습니다. 현재는 라이브러리를 통해 매일 약 200종이 넘는 테이블을 적재하고 있습니다.&lt;/p&gt;
&lt;figure style=&quot;max-width: 600px; margin: 1em auto&quot;&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1790px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 78.05555555555554%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAQCAYAAAAWGF8bAAAACXBIWXMAABYlAAAWJQFJUiTwAAABrklEQVR42qVT7XKCQAz0/d+gj9SZzrS21q/aggoH8iVwoEKBbRKE2tE/2JsJCSG3t7cJI9AqqxLRJESdNZB1dvesET+S4IinhwWSx0ySddWgaRrUdX0fYFXUOKUl8P0PapeAnuditVpiPH7G5O0Vm81a/PjlGafTaThgFIUCMp9P8fW5grJtzGZTvE/eUBbFcMAg8HugYiDATcB4v4dhfEIpG8pRJMEODnnXdlAXl41pNeaGdb6zv11OEmEZBgGyTEtBVVViQ0fo3BQPH8sFFosZbNuC1qkAZ1pDZxnFGeW05HWanmONNE0QxzH5FHme/wLyvHm7nbDkDVJErNsNe7nBnmRh482xxBHlY2poJE29AgzDEK7rttq5DhylJLa2G9jWFqZhiM58sEXvbIpqFN1oSzWc7wHLsqQC/rAm4EBO42vmeWvH41FyzJjf22+5SMA5No57wLYxMXbEkDVkTflUZsTzaRpfME2jZ2sQW2bo+76QuWqKjA7ptlmbYrbVXsP3PdGVJeC4Mx4r1vHW6PSAPNAsbgcw9Je7YsgnMMjhcBCNZAYvGFyuW7kO8Ac1J9gEOsglLAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/001edf4f535e43baeaf429e05f8885d9/33ecc/6-okr.webp 360w,
/static/001edf4f535e43baeaf429e05f8885d9/263a4/6-okr.webp 480w,
/static/001edf4f535e43baeaf429e05f8885d9/0c8fb/6-okr.webp 640w,
/static/001edf4f535e43baeaf429e05f8885d9/521f7/6-okr.webp 860w,
/static/001edf4f535e43baeaf429e05f8885d9/cbd37/6-okr.webp 1200w,
/static/001edf4f535e43baeaf429e05f8885d9/cb4b0/6-okr.webp 1790w&quot;
              sizes=&quot;(max-width: 1790px) 100vw, 1790px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/001edf4f535e43baeaf429e05f8885d9/53918/6-okr.png 360w,
/static/001edf4f535e43baeaf429e05f8885d9/9aebd/6-okr.png 480w,
/static/001edf4f535e43baeaf429e05f8885d9/0f09e/6-okr.png 640w,
/static/001edf4f535e43baeaf429e05f8885d9/3e3fe/6-okr.png 860w,
/static/001edf4f535e43baeaf429e05f8885d9/64756/6-okr.png 1200w,
/static/001edf4f535e43baeaf429e05f8885d9/e01d3/6-okr.png 1790w&quot;
            sizes=&quot;(max-width: 1790px) 100vw, 1790px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/001edf4f535e43baeaf429e05f8885d9/e01d3/6-okr.png&quot;
            alt=&quot;1.0 릴리즈 목표 OKR&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;1.0 릴리즈 목표 OKR&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;/figure&gt;
&lt;p&gt;저희는 데이터 분석가분들의 불편함을 덜어드리기 위하여 라이브러리 개발 목표 OKR을 위와 같이 설정하여 릴리즈 목표로 삼았습니다.&lt;/p&gt;
&lt;p&gt;1.0 버전 릴리즈 이후 라이브러리의 주요 사용자인 데이터 분석가분들께서 여러 가지 피드백을 주셨는데요, 그중 장점으로는&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;바닥부터 data를 불러오지 않아도 되어 편리하다&lt;/li&gt;
&lt;li&gt;라이브러리의 각 기능의 경계가 명확하다&lt;/li&gt;
&lt;li&gt;문서를 통해서 대부분의 기능을 파악할 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;등을 꼽아주셨습니다. 그래서 최초 설정한 OKR의 Key Result를 어느 정도 달성했다고 결론 내릴 수 있었습니다!!&lt;/p&gt;
&lt;p&gt;하지만, 여전히 데이터 분석가분들이 이 라이브러리를 잘 활용하기에는 부족한 부분이 많았습니다. 특히,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;문서만으로는 어떻게 분석을 할 수 있는지는 알 수가 없다&lt;/li&gt;
&lt;li&gt;여전히 Python 문법을 익히지 않고는 작업이 어렵다 (실제로 대략 이렇게 실행하면 된다, 정도로 이해하고 사용하는 경우가 많다고 합니다)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;등의 부분에서 여전히 많은 불편을 겪고 계셨습니다.&lt;/p&gt;
&lt;p&gt;그렇기에, 저희는 여전히 DevPlay Analytics 라이브러리를 포함하여 여러가지 툴들을 활발히 개발하고 있습니다. 데이터플랫폼셀에서는 이런 데이터를 잘 활용하기 위한 도움을 주기 위해 재미있는 문제들을 엔지니어링으로 풀고 있습니다. 데이터가 잘 흐르게 하는 엔지니어링에 관심 있으신 분들은 저희 채용 페이지도 한번 꼭 확인 부탁드려요. :)&lt;/p&gt;
&lt;p&gt;긴 글 읽어주셔서 감사합니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[Data-Driven 문화를 만드는 사람들 - 데이터플랫폼셀]]></title><description><![CDATA[우리가 Data-Driven 문화를 만들어가는 과정 속에는 과연 어떤 가치관과 고민, 그리고 노력이 깃들어 있을까요? 구성원들이 일하는 환경 곳곳에 데이터가 ‘제대로’ 흐르게 만드는 엔지니어들, ‘데이터플랫폼셀’의 이야기를 지금 여러분과 공유합니다.]]></description><link>https://tech.devsisters.com/posts/data-driven-story-1-data-platform-cell</link><guid isPermaLink="false">https://tech.devsisters.com/posts/data-driven-story-1-data-platform-cell</guid><pubDate>Thu, 15 Oct 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;데브시스터즈는 &apos;데이터&apos;로 대화하고, 결정합니다. 고객을 정확히 이해하고, 최상의 의사를 결정하며, 최고의 경험을 선사하기 위한 모든 해법이 데이터에 있다고 믿기 때문인데요. 이 믿음을 기반으로 함께 일하며 성장해나가는 방식을 우리는 &apos;Data-Driven 문화&apos;라고 부릅니다.&lt;/p&gt;
&lt;p&gt;데브시스터즈의 Data-Driven 문화는 데이터 엔지니어, 데이터 과학자, 머신 러닝 엔지니어 등 다양한 구성원들의 협업을 통해 다져지고 있습니다. 그만큼 데이터 관련 프로젝트와 &lt;a href=&quot;https://careers.devsisters.com/position&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;전문 인재 채용&lt;/a&gt; 또한 활발하게 이뤄지고 있는데요. 우리가 Data-Driven 문화를 만들어가는 과정 속에는 과연 어떤 가치관과 고민, 그리고 노력이 깃들어 있을까요? 구성원들이 일하는 환경 곳곳에 데이터가 &apos;제대로&apos; 흐르게 만드는 엔지니어들, &lt;strong&gt;&apos;데이터플랫폼셀&apos;&lt;/strong&gt; 의 이야기를 지금 여러분과 공유합니다.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;본 인터뷰는 코로나19 확산에 따른 &apos;전사 자율 재택근무&apos; 시행 기간 중 화상 통화로 진행 됐습니다.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 70%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAOABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAABAACBf/EABUBAQEAAAAAAAAAAAAAAAAAAAAB/9oADAMBAAIQAxAAAAFZFcxdzpP/xAAbEAACAgMBAAAAAAAAAAAAAAABAwIEERIiE//aAAgBAQABBQKax6vyqQQWCR6e/qtZ2V//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/AT//xAAWEQEBAQAAAAAAAAAAAAAAAAAAETH/2gAIAQIBAT8BxX//xAAeEAACAgICAwAAAAAAAAAAAAABEQASAkEhIjGBkf/aAAgBAQAGPwIdj9mqnZDjovUsfIlMsWIGEuJ//8QAGxAAAgMBAQEAAAAAAAAAAAAAAREAITFhQXH/2gAIAQEAAT8hOGgpqCFsMKAx8j9FfYQ8AxssABx2oC8vKf/aAAwDAQACAAMAAAAQcC//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/ED//xAAVEQEBAAAAAAAAAAAAAAAAAAAQEf/aAAgBAgEBPxCB/8QAHhABAAEEAgMAAAAAAAAAAAAAAREAITFBUeFhkaH/2gAIAQEAAT8QgxQFqTcnynYC4EU24v3R5zjJ29pqWRMwogOJob1VCymu+aIlwLkQCG/hr//Z&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/b1984789673f6d8397caab521e1d0bfa/33ecc/team-photo.webp 360w,
/static/b1984789673f6d8397caab521e1d0bfa/263a4/team-photo.webp 480w,
/static/b1984789673f6d8397caab521e1d0bfa/0c8fb/team-photo.webp 640w,
/static/b1984789673f6d8397caab521e1d0bfa/521f7/team-photo.webp 860w,
/static/b1984789673f6d8397caab521e1d0bfa/cbd37/team-photo.webp 1200w,
/static/b1984789673f6d8397caab521e1d0bfa/548e7/team-photo.webp 2400w&quot;
              sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/b1984789673f6d8397caab521e1d0bfa/543cd/team-photo.jpg 360w,
/static/b1984789673f6d8397caab521e1d0bfa/a3e66/team-photo.jpg 480w,
/static/b1984789673f6d8397caab521e1d0bfa/56d4e/team-photo.jpg 640w,
/static/b1984789673f6d8397caab521e1d0bfa/2f567/team-photo.jpg 860w,
/static/b1984789673f6d8397caab521e1d0bfa/03ffe/team-photo.jpg 1200w,
/static/b1984789673f6d8397caab521e1d0bfa/fdb5a/team-photo.jpg 2400w&quot;
            sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
            type=&quot;image/jpeg&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/b1984789673f6d8397caab521e1d0bfa/fdb5a/team-photo.jpg&quot;
            alt=&quot;데이터플랫폼셀 승용님, 찬수님, 태겸님, 민수님(좌측 상단부터 시계 방향으로)&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;데이터플랫폼셀 승용님, 찬수님, 태겸님, 민수님(좌측 상단부터 시계 방향으로)&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h2 id=&quot;엔지니어링을-통해-데이터가-제대로-흐르는-조직을-만들어요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A7%81%EC%9D%84-%ED%86%B5%ED%95%B4-%EB%8D%B0%EC%9D%B4%ED%84%B0%EA%B0%80-%EC%A0%9C%EB%8C%80%EB%A1%9C-%ED%9D%90%EB%A5%B4%EB%8A%94-%EC%A1%B0%EC%A7%81%EC%9D%84-%EB%A7%8C%EB%93%A4%EC%96%B4%EC%9A%94&quot; aria-label=&quot;엔지니어링을 통해 데이터가 제대로 흐르는 조직을 만들어요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;엔지니어링을 통해 데이터가 &apos;제대로&apos; 흐르는 조직을 만들어요.&lt;/h2&gt;
&lt;h3 id=&quot;q-안녕하세요-간단한-자기소개-부탁드립니다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#q-%EC%95%88%EB%85%95%ED%95%98%EC%84%B8%EC%9A%94-%EA%B0%84%EB%8B%A8%ED%95%9C-%EC%9E%90%EA%B8%B0%EC%86%8C%EA%B0%9C-%EB%B6%80%ED%83%81%EB%93%9C%EB%A6%BD%EB%8B%88%EB%8B%A4&quot; aria-label=&quot;q 안녕하세요 간단한 자기소개 부탁드립니다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Q. 안녕하세요, 간단한 자기소개 부탁드립니다!&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;태겸님&lt;/strong&gt;
데이터 플랫폼 엔지니어 김태겸입니다. 데이터 수집-가공-분석 과정에서 필요한 모든 엔지니어링을 담당하고 있어요. 최근에는 구성원들이 업무 중 보다 효율적으로 데이터를 활용할 수 있게 도와주는 플랫폼을 만드는 일에 특히 집중하고 있죠.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;민수님&lt;/strong&gt;
데이터플랫폼셀이 처음 만들어진 2018년부터 지금까지 데이터 플랫폼 엔지니어로 일하고 있는 김민수입니다. 이전에는 데이터 기반 A/B 테스팅 플랫폼을 구축하는 일을 주로 했었고, 요즘은 스키마(Schema) 정보에 기반을 둔 데이터 수집과 적재를 효율적으로 처리하는 문제에 집중하고 있어요. 이외에도 엔지니어링을 통해 데이터의 &apos;숨은 가치&apos;를 발견하는 일에 관심이 많고요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;찬수님&lt;/strong&gt;
인턴으로 처음 입사하여 지금까지 약 1년 반 동안 데이터플랫폼셀에서 일하고 있는 데이터 플랫폼 엔지니어 박찬수입니다. 저는 데브시스터즈가 첫 회사에요. 산업 디자인을 공부했지만, 개발자가 되고 싶어 진로를 전향했죠. 디자인을 전공할 때 유저들의 니즈를 분석하는 공부를 했었는데요. 당시 데이터라는 개념에 대해 매력을 느꼈고, 앞으로의 커리어도 데이터 분야에서 쌓아 나가야겠다고 결심했어요. 지금은 그 결심을 이뤄나가는 과정에 있고, 저 또한 데이터 수집-가공-제공에 필요한 여러 엔지니어링 업무를 담당하고 있어요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;승용님&lt;/strong&gt;
데이터플랫폼셀을 리드하고 있는 오승용입니다. 데브시스터즈에서 일한 지 벌써 6년 가까이 됐네요(웃음). 쿠키런 for KAKAO의 게임 서버 개발자로 입사했고, 이후 쿠키런: 오븐브레이크 론칭 멤버로 서버 개발을 담당했어요. 쿠키런: 오븐브레이크의 서버를 개발할 당시, 데이터 파이프라인을 만드는 일도 같이 했었는데요. 성공적으로 출시를 마친 이후부터는 데이터 파이프라인을 앞으로 출시할 다른 제품에도 적용하는 일을 맡게 됐죠. 그렇게 지금은 데이터플랫폼셀을 이끌고 있고, 엔지니어링을 통해 데브시스터즈를 &apos;데이터가 제대로 흐르는 조직&apos;으로 만들기 위한 모든 과정에 함께 참여하고 있어요&lt;/p&gt;
&lt;h3 id=&quot;q-데이터가-제대로-흐르는-조직을-만든다는-말이-인상적인데요-더-구체적으로-어떤-일을-하고-계신지-소개해-주세요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#q-%EB%8D%B0%EC%9D%B4%ED%84%B0%EA%B0%80-%EC%A0%9C%EB%8C%80%EB%A1%9C-%ED%9D%90%EB%A5%B4%EB%8A%94-%EC%A1%B0%EC%A7%81%EC%9D%84-%EB%A7%8C%EB%93%A0%EB%8B%A4%EB%8A%94-%EB%A7%90%EC%9D%B4-%EC%9D%B8%EC%83%81%EC%A0%81%EC%9D%B8%EB%8D%B0%EC%9A%94-%EB%8D%94-%EA%B5%AC%EC%B2%B4%EC%A0%81%EC%9C%BC%EB%A1%9C-%EC%96%B4%EB%96%A4-%EC%9D%BC%EC%9D%84-%ED%95%98%EA%B3%A0-%EA%B3%84%EC%8B%A0%EC%A7%80-%EC%86%8C%EA%B0%9C%ED%95%B4-%EC%A3%BC%EC%84%B8%EC%9A%94&quot; aria-label=&quot;q 데이터가 제대로 흐르는 조직을 만든다는 말이 인상적인데요 더 구체적으로 어떤 일을 하고 계신지 소개해 주세요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Q. &apos;데이터가 제대로 흐르는 조직을 만든다&apos;는 말이 인상적인데요. 더 구체적으로 어떤 일을 하고 계신지 소개해 주세요.&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;태겸님&lt;/strong&gt;
데이터가 제대로 흐르는 조직을 만들기 위해서는 다양한 형태의 데이터들을 쉽고 안정적으로 수집할 수 있어야 해요. 뿐만 아니라 구성원들이 언제 어디서나 편리하게 접근하고 활용할 수 있는 간편한 형태로 처리-분석-제공되어야 하죠.&lt;/p&gt;
&lt;p&gt;이 맥락 아래 우리는 데브시스터즈의 제품에서 발생되는 다양한 데이터들을 구성원 누구나 손쉽게 수집하고 검색하며 활용할 수 있는 &apos;로깅 플랫폼&apos;을 개발하고 있어요. 또 여러 형태와 규모의 데이터를 빅데이터 환경 내에서 효율적으로 가공하고 분석할 수 있는 &apos;분석 플랫폼&apos;도 개발하고 있죠. 한편 최적의 의사결정을 위한 A/B 테스팅 플랫폼, 자동화된 지표 분석을 위한 웹/쿼리 서비스 등도 개발하여 구성원들에게 제공하고 있고요. 이 모든 과정에서 Kafka, Elasticsearch, Terraform, Spark, Kubernetes 등의 프레임워크와 기법을 주로 사용하고, Spark, Airflow, Amazon S3 등 기반의 Data Lake 인프라와 Python, Scala, Go 등의 언어를 활용하고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;승용님&lt;/strong&gt;
즉 데브시스터즈의 제품에서 발생되는 데이터들을 효율적으로 수집하고 가공한 후, 구성원들이 손쉽게 활용할 수 있도록 돕는 &apos;플랫폼&apos;으로 제공하기까지 필요한 모든 엔지니어링을 담당하고 있어요. 이 일을 우리는 &apos;엔지니어링을 통해 데이터가 제대로 흐르는 조직을 만든다&apos;라고 표현하고요. 한편으론 &apos;엔지니어링을 통해 Data-Driven 문화가 구성원들의 업무 환경 곳곳에 녹아들도록 돕는다&apos;고 표현할 수도 있어요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 56.666666666666664%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAECBf/EABQBAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhADEAAAAd0cjKD/xAAWEAEBAQAAAAAAAAAAAAAAAAAAAUH/2gAIAQEAAQUCbEa//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAwEBPwE//8QAFBEBAAAAAAAAAAAAAAAAAAAAEP/aAAgBAgEBPwE//8QAFBABAAAAAAAAAAAAAAAAAAAAIP/aAAgBAQAGPwJf/8QAGRAAAgMBAAAAAAAAAAAAAAAAAAERMXFB/9oACAEBAAE/IeLTDEibsoIo/9oADAMBAAIAAwAAABAQD//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8QP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8QP//EABwQAQADAAIDAAAAAAAAAAAAAAEAESExQVFhsf/aAAgBAQABPxApo8oVrRUhcqtO7PuxOfcAXXbP/9k=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/26f3d9539d190018311b5b75308e511e/33ecc/engineering.webp 360w,
/static/26f3d9539d190018311b5b75308e511e/263a4/engineering.webp 480w,
/static/26f3d9539d190018311b5b75308e511e/0c8fb/engineering.webp 640w,
/static/26f3d9539d190018311b5b75308e511e/521f7/engineering.webp 860w,
/static/26f3d9539d190018311b5b75308e511e/cbd37/engineering.webp 1200w,
/static/26f3d9539d190018311b5b75308e511e/548e7/engineering.webp 2400w&quot;
              sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/26f3d9539d190018311b5b75308e511e/543cd/engineering.jpg 360w,
/static/26f3d9539d190018311b5b75308e511e/a3e66/engineering.jpg 480w,
/static/26f3d9539d190018311b5b75308e511e/56d4e/engineering.jpg 640w,
/static/26f3d9539d190018311b5b75308e511e/2f567/engineering.jpg 860w,
/static/26f3d9539d190018311b5b75308e511e/03ffe/engineering.jpg 1200w,
/static/26f3d9539d190018311b5b75308e511e/fdb5a/engineering.jpg 2400w&quot;
            sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
            type=&quot;image/jpeg&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/26f3d9539d190018311b5b75308e511e/fdb5a/engineering.jpg&quot;
            alt=&quot;데이터플랫폼셀이 담당하고 있는 데이터 엔지니어링 범위 개요&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;데이터플랫폼셀이 담당하고 있는 데이터 엔지니어링 범위 개요&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h3 id=&quot;q-정말-다양한-일들이-이뤄지고-있군요-이-일들이-추구하는-목표는-무엇인가요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#q-%EC%A0%95%EB%A7%90-%EB%8B%A4%EC%96%91%ED%95%9C-%EC%9D%BC%EB%93%A4%EC%9D%B4-%EC%9D%B4%EB%A4%84%EC%A7%80%EA%B3%A0-%EC%9E%88%EA%B5%B0%EC%9A%94-%EC%9D%B4-%EC%9D%BC%EB%93%A4%EC%9D%B4-%EC%B6%94%EA%B5%AC%ED%95%98%EB%8A%94-%EB%AA%A9%ED%91%9C%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80%EC%9A%94&quot; aria-label=&quot;q 정말 다양한 일들이 이뤄지고 있군요 이 일들이 추구하는 목표는 무엇인가요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Q. 정말 다양한 일들이 이뤄지고 있군요. 이 일들이 추구하는 목표는 무엇인가요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;민수님&lt;/strong&gt;
데브시스터즈처럼 다양한 콘텐츠와 서비스를 제공하는 회사에서 데이터를 중요하게 생각하는 이유는 유저들의 &apos;진짜 니즈&apos;를 파악할 수 있고, 이를 통해 향상된 이용 경험을 선사할 수 있기 때문인데요. 하지만 데이터를 수집하고, 가공하고, 또 활용하는 일은 특정한 &apos;기술&apos;을 필요로 하는 만큼, 사람들이 섣불리 다루기가 쉽지 않죠. 이 맥락에서 우리는 엔지니어링을 통해 구성원들이 언제 어디서나 데이터 기반으로 일할 수 있게 해주고, 더 나은 의사결정은 물론, 이전까지는 생각할 수 없었던 새로운 시도를 펼치도록 돕고자 해요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;찬수님&lt;/strong&gt;
저 또한 비슷한 의견인데요. 우리는 데브시스터즈의 &apos;눈&apos;이 되어주는 일을 추구한다고 생각해요. 우리가 만드는 데이터 서비스를 통해 구성원들이 유저 여러분들의 니즈를 효과적으로 확인하고, 이를 통해 최적의 의사 결정을 내리며, 마침내 최고의 경험을 선사할 수 있기 때문이죠.&lt;/p&gt;
&lt;h3 id=&quot;q-가장-기억에-남는-프로젝트는-무엇인가요-그-이유도-궁금해요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#q-%EA%B0%80%EC%9E%A5-%EA%B8%B0%EC%96%B5%EC%97%90-%EB%82%A8%EB%8A%94-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80%EC%9A%94-%EA%B7%B8-%EC%9D%B4%EC%9C%A0%EB%8F%84-%EA%B6%81%EA%B8%88%ED%95%B4%EC%9A%94&quot; aria-label=&quot;q 가장 기억에 남는 프로젝트는 무엇인가요 그 이유도 궁금해요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Q. 가장 기억에 남는 프로젝트는 무엇인가요? 그 이유도 궁금해요.&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;민수님&lt;/strong&gt;
데이터 기반 A/B 테스팅 플랫폼을 만든 프로젝트가 가장 기억에 남아요. 데브시스터즈에 합류 후, 과연 어떻게 해야 우리 회사가 데이터로 일하는 조직이 될 수 있을까? 라는 고민에서 출발한 프로젝트인데요. 저는 테스팅 플랫폼의 전반적인 설계와 개발을 맡았어요. 데이터 과학자분들과의 협업을 통해 다양한 실험 통계치 추출이 가능한 모델을 만든다거나, 플랫폼 개발자를 도와 테스팅 플랫폼을 서버에 붙이기 위한 원격 설정 기능 개발을 주로 담당했죠.&lt;/p&gt;
&lt;p&gt;그렇게 탄생된 A/B 테스팅 플랫폼은 쿠키런: 퍼즐 월드 론칭 전 밸런싱 단계에서 처음 사용됐어요. 게임의 난이도와 운영 방식을 결정하는 과정에서 고려 중인 몇 가지 옵션들이 있었는데요. 이 옵션들을 A/B 테스트하여 과연 어떤 의사결정이 가장 최상의 선택인지를 증명하는 것이 과제였죠. 다행히 우리의 테스트로 확인된 인사이트를 통해 모두가 납득 가능한 의사결정을 할 수 있었고, 실제 서비스가 라이브된 후에도 긍정적인 성과를 거뒀기에 기억에 남아요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;찬수님&lt;/strong&gt;
앞서 태겸님이 말씀해주신 프레임워크 중 하나인 Spark를 구성원들이 편리하게 사용할 수 있도록 개발한 프로젝트가 기억에 남아요. 먼저 Spark는 개발자 관점으로 볼 때 무척 훌륭하고 효율적인 데이터 처리 프레임워크인데요. 하지만 비개발자인 구성원들의 입장에서는 다소 친절하지 않은 도구죠. 그래서 우리는 Spark의 여러 유용한 기능을 구성원들이 충분히 누릴 수 있도록 자체적으로 라이브러리를 개발하여 제공하기로 했어요. 비록 이미 존재하는 프레임워크를 누구나 사용하기 쉬운 방식으로 감싸는 과정이긴 했지만, 라이브러리 구축의 A to Z를 직접 경험하고, 하나의 완성된 서비스로 구성원들에게 제공했던 프로젝트라는 점에서 의미가 커요.&lt;/p&gt;
&lt;h3 id=&quot;q-최근-집중하고-있는-rd는-무엇인가요-앞으로-계획-중인-프로젝트에-대해-알려주셔도-좋아요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#q-%EC%B5%9C%EA%B7%BC-%EC%A7%91%EC%A4%91%ED%95%98%EA%B3%A0-%EC%9E%88%EB%8A%94-rd%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80%EC%9A%94-%EC%95%9E%EC%9C%BC%EB%A1%9C-%EA%B3%84%ED%9A%8D-%EC%A4%91%EC%9D%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EB%A0%A4%EC%A3%BC%EC%85%94%EB%8F%84-%EC%A2%8B%EC%95%84%EC%9A%94&quot; aria-label=&quot;q 최근 집중하고 있는 rd는 무엇인가요 앞으로 계획 중인 프로젝트에 대해 알려주셔도 좋아요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Q. 최근 집중하고 있는 R&amp;#x26;D는 무엇인가요? 앞으로 계획 중인 프로젝트에 대해 알려주셔도 좋아요.&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;태겸님&lt;/strong&gt;
구성원들에게 &apos;클라우드&apos;와 &apos;*Kubernetes&apos;에 기반을 둔 데이터 플랫폼을 제공하기 위한 R&amp;#x26;D에 집중하고 있어요. 최근 많은 IT 서비스들이 때와 장소에 제약을 받지 않으면서도, 확장성/안정성 또한 고루 갖춘 클라우드 기반 인프라로 대거 전환하고 있는데요. 이 클라우드 환경에 무척 친화적인 시스템인 Kubernetes를 우리가 다루는 데이터와 결합한다면 구성원들이 더욱 효율적으로 인사이트를 얻고, 의사결정에 활용할 수 있기 때문이에요. 사실 이미 Kubernetes와 클라우드를 활용한 서비스를 구성원들에게 제공 중인데요. 앞으로 더 많은 연구와 고민을 통해 서비스의 질을 향상 시켜나갈 계획이에요.&lt;/p&gt;
&lt;p&gt;*Kubernetes: 컨테이너화된 어플리케이션을 실행하고 관리하기 위한 플랫폼으로, 유연하고 확장성 있는 구조가 클라우드 환경에 무척 친화적이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;승용님&lt;/strong&gt;
태겸님께서 말씀하신 Kubernetes는 현재 어느 정도 업계의 표준이 됐는데요. 데브시스터즈는 2015년부터 이미 Kubernetes에 대한 R&amp;#x26;D를 지속적으로 해오고 있어요. 쿠키런: 오븐브레이크를 개발할 때도 Kubernetes를 적극 활용했고, 그 과정에서 확인한 인사이트를 NDC 2017에서 공유한 적도 있죠.&lt;/p&gt;
&lt;p&gt;이러한 경험과 노하우를 통해 지금은 다양한 프로덕션 환경에서 Kubernetes를 성공적으로 활용하고 있어요. 또 데이터플랫폼셀에서도 작년부터 Kubernetes 기반의 새로운 데이터플랫폼을 꾸준히 만들어가고 있고, 그중 일부 성과를 &apos;Kubernetes Forum Seoul 2019&apos;에서 공유했는데요. 관련 내용은 이곳 데브시스터즈 기술 블로그를 통해서도 소개해드릴 예정이오니 여러분들의 많은 방문 부탁드립니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 107.22222222222221%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAVABQDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAUBBP/EABYBAQEBAAAAAAAAAAAAAAAAAAABAv/aAAwDAQACEAMQAAABlZSJJdTNs4bk4H//xAAdEAACAQQDAAAAAAAAAAAAAAAAAQIEERIUAxMx/9oACAEBAAEFAo+5yOyQ6Fmg7LigzaJVV45o/8QAFREBAQAAAAAAAAAAAAAAAAAAESD/2gAIAQMBAT8BY//EABgRAAIDAAAAAAAAAAAAAAAAAAABEBES/9oACAECAQE/AcopT//EAB4QAAICAAcAAAAAAAAAAAAAAAABMkEREiExkZKh/9oACAEBAAY/ApJaWV1K4N0YtlkPRrKQP//EAB0QAAMAAgIDAAAAAAAAAAAAAAABETFBIZFhofH/2gAIAQEAAT8hfHzi0sqvQP5Jtd7IOjErw2VIMsAzYVMkdO2f/9oADAMBAAIAAwAAABDHPz7/xAAWEQADAAAAAAAAAAAAAAAAAAAAECH/2gAIAQMBAT8QFf8A/8QAGREBAAIDAAAAAAAAAAAAAAAAAQAQETFh/9oACAECAQE/EFthOcxX/8QAHxABAAICAgIDAAAAAAAAAAAAAQARITFB8GFxgcHh/9oACAEBAAE/EH1m2CcaNcylS9XX4xblx66mjYOmoWWZlLreyAJ0FMsspWXriU+A9vM9QJR8Jfc//9k=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/6ae50767e8782b139f853164ba451eae/33ecc/ndc.webp 360w,
/static/6ae50767e8782b139f853164ba451eae/263a4/ndc.webp 480w,
/static/6ae50767e8782b139f853164ba451eae/0c8fb/ndc.webp 640w,
/static/6ae50767e8782b139f853164ba451eae/521f7/ndc.webp 860w,
/static/6ae50767e8782b139f853164ba451eae/cbd37/ndc.webp 1200w,
/static/6ae50767e8782b139f853164ba451eae/548e7/ndc.webp 2400w&quot;
              sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/6ae50767e8782b139f853164ba451eae/543cd/ndc.jpg 360w,
/static/6ae50767e8782b139f853164ba451eae/a3e66/ndc.jpg 480w,
/static/6ae50767e8782b139f853164ba451eae/56d4e/ndc.jpg 640w,
/static/6ae50767e8782b139f853164ba451eae/2f567/ndc.jpg 860w,
/static/6ae50767e8782b139f853164ba451eae/03ffe/ndc.jpg 1200w,
/static/6ae50767e8782b139f853164ba451eae/fdb5a/ndc.jpg 2400w&quot;
            sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
            type=&quot;image/jpeg&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/6ae50767e8782b139f853164ba451eae/fdb5a/ndc.jpg&quot;
            alt=&quot;승용님의 NDC 2017 &amp;lt;Kubernetes로 개발 서버 간단히 찍어내기&amp;gt;, Kubernetes Forum Seoul 2019 &amp;lt;Re-architecting Data Platform with Kubernetes&amp;gt; 발표 현장 [출처] NDC Replay, CNCF 유튜브&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;승용님의 NDC 2017 &amp;lt;Kubernetes로 개발 서버 간단히 찍어내기&amp;gt;, Kubernetes Forum Seoul 2019 &amp;lt;Re-architecting Data Platform with Kubernetes&amp;gt; 발표 현장 [출처] NDC Replay, CNCF 유튜브&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h2 id=&quot;공동체의-미션을-위해-적극적으로-참여하고-협력하는-문화를-중요하게-생각해요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B3%B5%EB%8F%99%EC%B2%B4%EC%9D%98-%EB%AF%B8%EC%85%98%EC%9D%84-%EC%9C%84%ED%95%B4-%EC%A0%81%EA%B7%B9%EC%A0%81%EC%9C%BC%EB%A1%9C-%EC%B0%B8%EC%97%AC%ED%95%98%EA%B3%A0-%ED%98%91%EB%A0%A5%ED%95%98%EB%8A%94-%EB%AC%B8%ED%99%94%EB%A5%BC-%EC%A4%91%EC%9A%94%ED%95%98%EA%B2%8C-%EC%83%9D%EA%B0%81%ED%95%B4%EC%9A%94&quot; aria-label=&quot;공동체의 미션을 위해 적극적으로 참여하고 협력하는 문화를 중요하게 생각해요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;공동체의 미션을 위해 적극적으로 참여하고, 협력하는 문화를 중요하게 생각해요.&lt;/h2&gt;
&lt;h3 id=&quot;q-함께-일할-때-무엇을-가장-중요하게-생각하나요-여러분들의-업무-문화를-대표하는-사례를-알려주셔도-좋아요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#q-%ED%95%A8%EA%BB%98-%EC%9D%BC%ED%95%A0-%EB%95%8C-%EB%AC%B4%EC%97%87%EC%9D%84-%EA%B0%80%EC%9E%A5-%EC%A4%91%EC%9A%94%ED%95%98%EA%B2%8C-%EC%83%9D%EA%B0%81%ED%95%98%EB%82%98%EC%9A%94-%EC%97%AC%EB%9F%AC%EB%B6%84%EB%93%A4%EC%9D%98-%EC%97%85%EB%AC%B4-%EB%AC%B8%ED%99%94%EB%A5%BC-%EB%8C%80%ED%91%9C%ED%95%98%EB%8A%94-%EC%82%AC%EB%A1%80%EB%A5%BC-%EC%95%8C%EB%A0%A4%EC%A3%BC%EC%85%94%EB%8F%84-%EC%A2%8B%EC%95%84%EC%9A%94&quot; aria-label=&quot;q 함께 일할 때 무엇을 가장 중요하게 생각하나요 여러분들의 업무 문화를 대표하는 사례를 알려주셔도 좋아요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Q. 함께 일할 때 무엇을 가장 중요하게 생각하나요? 여러분들의 업무 문화를 대표하는 사례를 알려주셔도 좋아요.&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;태겸님&lt;/strong&gt;
&apos;문제 해결을 위한 집념&apos;을 중요하게 생각해요. 일하다 보면 누구나 때때로 풀기 어려운 문제나, 넘어설 수 없을 것 같은 장벽을 마주칠 때가 있는데요. 이때 우리는 끊임없이 새로운 기술과 정보를 습득하고, 공유하고, 연구하고, 시도하면서 문제를 극복해 나가려고 해요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;민수님&lt;/strong&gt;
&apos;고객 경험 향상을 중시하는 사고&apos;를 중요하게 생각해요. 먼저 우리에게 고객이란, 데브시스터즈 내부의 &apos;구성원&apos;과 외부의 &apos;유저&apos; 여러분 모두를 의미해요. 우리가 개발하는 데이터 서비스들은 구성원들이 보다 효율적으로 일할 수 있게 돕고, 이는 결국 유저들의 제품 이용 경험 향상으로 이어지기 때문이죠.&lt;/p&gt;
&lt;p&gt;사내에서 주로 사용되는 서비스들을 살펴보면, 발휘하는 기능 자체는 대부분 같아도 이용 과정 상의 편리함이나 효율성은 다소 아쉬운 경우가 종종 있는데요. 우리는 내부의 고객인 구성원들의 경험 향상을 위해 서비스를 구성하는 작은 디테일도 세심하게 신경 쓰며 개발을 이어나가고 있어요. 뿐만 아니라 서비스를 제공한 이후에도 구성원들의 피드백을 다양한 채널들을 통해 받고, 실제 품질 개선을 위한 실행으로 적극 옮기고 있어요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;승용님&lt;/strong&gt;
우리에겐 &apos;공동체의 미션을 위해 적극적으로 참여하고 협력하는 문화&apos;가 있어요. 이건 우리 셀 뿐 아니라 데브시스터즈 전체에 깃든 문화이기도 한데요. 좀 더 풀어 설명하자면 우리는 각자 맡은 R&amp;#x26;R(Roles &amp;#x26; Responsibility; 역할과 책임)을 제한하지 않고, 그 이상의 영역을 자유롭게 넘나드는 일에 두려움이 없어요. 즉 데브시스터즈 공동체가 지향하는 공동의 목표를 달성하기 위해 우리 셀은 물론, 타 셀, 그룹 등 구성원들과도 서로 주도적으로 의견들을 공유하고, 협업하며 보다 나은 방향으로 나아가고자 하죠.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;찬수님&lt;/strong&gt;
데브시스터즈와 셀, 그리고 개인의 성장을 위해 주도적으로 학습하고 공유하며 개선하는 문화라고 생각해요. 우리 셀이 지향하는 건전성 지표 중 하나는 바로 &quot;반복되는 장애가 없도록&quot; 인데요. 그래서 우리는 제공 중인 서비스에서 장애나 뜻하지 않은 결과가 발생됐을 때, 그 이유에 대해 회고하고 앞으로의 솔루션을 고민하는 &apos;포스트 모템&apos; 시간을 꼭 갖고 있어요. 그 다음에는 타 셀이나 그룹, 나아가 데브시스터즈 전체의 구성원들이 우리와 같은 문제를 마주쳤을 때 도움을 얻을 수 있도록 공유하고 있죠.&lt;/p&gt;
&lt;p&gt;또 우리는 코딩 과정에서 구성원 각 개인이 보유한 지식과 노하우가 서로 시너지를 만들어낼 수 있도록 &apos;페어 프로그래밍&apos;을 자주 하고 있어요. 페어 프로그래밍은 둘 이상의 엔지니어가 한 화면을 함께 보며 하는 코딩 작업을 말하는데요. 각자가 지닌 코드 레벨의 의사결정과 노하우를 공유할 수 있어 서비스 개발 효율 향상에 큰 도움을 줘요.&lt;/p&gt;
&lt;p&gt;한편 우리는 함께 일하는 셀원이나 협업하는 다른 셀원들과 일대일로 만나 프로젝트에 대한 고민, 노하우 등을 교류하는 &apos;원온원(One on One)&apos; 시간도 정기적으로 갖고 있는데요. 코로나19로 인한 전사 자율 재택근무 시행 기간 중에도 화상 통화를 통해 끊임없이 이뤄지고 있어요.&lt;/p&gt;
&lt;h3 id=&quot;q-데브시스터즈에서-일하면서-가장-좋았던-점은-무엇인가요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#q-%EB%8D%B0%EB%B8%8C%EC%8B%9C%EC%8A%A4%ED%84%B0%EC%A6%88%EC%97%90%EC%84%9C-%EC%9D%BC%ED%95%98%EB%A9%B4%EC%84%9C-%EA%B0%80%EC%9E%A5-%EC%A2%8B%EC%95%98%EB%8D%98-%EC%A0%90%EC%9D%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80%EC%9A%94&quot; aria-label=&quot;q 데브시스터즈에서 일하면서 가장 좋았던 점은 무엇인가요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Q. 데브시스터즈에서 일하면서 가장 좋았던 점은 무엇인가요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;태겸님&lt;/strong&gt;
입사한지 이제 약 1년이 되어가는데요. 이 시간 동안 데이터플랫폼셀이 엔지니어링적으로 마주한 다양한 문제들을 하나 둘씩 해결하면서 &apos;역량의 성장&apos;을 경험할 수 있었어요. 데브시스터즈는 문제를 해결하는 과정에서 미리 정해둔 답에 맞춰 사고하거나, 기존의 관습을 반복하지 않아요. 오히려 보다 나은 방향이나 답을 찾아 적극적으로 움직이는 문화죠. 덕분에 저 또한 스스로 해법을 찾기 위해 여러 노력을 기울였는데요. 그 과정에서 전에는 알지 못했던 지식과 경험을 풍부하게 쌓을 수 있었어요. 한편 회사의 지원 아래 다양한 개발 관련 세미나나 교육 프로그램에 참석할 수 있어 좋았어요. 특히 Elastic 본사의 Kibana 제품 PM을 오피스로 초청하여 인사이트를 공유받고 저녁식사까지 함께하며 대화를 나눴던 일이 기억에 많이 남아요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;승용님&lt;/strong&gt;
요즘은 코로나19로 인해 아쉽게도 제약이 있지만, 평소 외부 발표나 컨퍼런스 참여 기회가 적극 주어지고 있어요. 저 또한 수차례 해외 컨퍼런스에 참여할 기회가 있었는데요. 덕분에 글로벌 엔지니어링 트렌드를 빠르게 파악하고, 지금 우리가 만들고 있는 데이터 플랫폼 서비스의 기반이 되는 기술들을 비교적 일찍 도입할 수 있었어요.&lt;/p&gt;
&lt;p&gt;이외에도 저는 데브시스터즈가 주는 가장 큰 혜택 중 하나가 &apos;같이 일하고 싶은 동료들&apos;이라고 생각해요. 약 6년의 시간 동안 일하면서 사실 이직 제안도 몇번 받았었는데요. 그때마다 &quot;지금 함께 일하는 사람들 같은 동료들과 내가 앞으로도 또 일할 수 있을까?&quot; 라고 스스로에게 물었을 때, 항상 확신이 서지 않았어요(웃음).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;찬수님&lt;/strong&gt;
저 또한 승용님과 마찬가지로 &apos;사람&apos;이 데브시스터즈가 주는 가장 큰 혜택인 것 같아요. 먼저 제가 업무에 어려움을 겪을 때 풍부한 경험과 역량을 갖춘 구성원들로부터 적극적인 도움을 받을 수 있어요. 또 새로 도입 중인 기술이나 지식 등을 이해하기 쉽고 흥미롭게 풀어주셔서 유익하고요.&lt;/p&gt;
&lt;p&gt;한편 저는 평소 다양한 구성원 여러분들과 회사 안과 밖 모두에서 교류를 이어나가는 편인데요. 제게는 굉장히 자연스러운 일이었지만, 다른 회사에서 일하는 주변 지인들은 듣고 놀라더라고요. 지금 생각해보면, 데브시스터즈의 구성원들이 언제 어디서나 &apos;함께 있을 때 좋고 즐거운 사람들&apos;이기 때문에 가능한 일인 것 같아요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;민수님&lt;/strong&gt;
일하며 발생될 수 있는 여러 불편들을 덜어주고, 최대한 프로젝트에 몰입할 수 있도록 도와줘서 좋았어요. 예를 들어 건강한 식사를 매일매일 사내 레스토랑에서 제공해 주거나, 업무에 필요한 장비와 인프라를 적극적으로 지원해 주는데요. 덕분에 편리하고 효율적으로 온전히 일에 집중할 수 있었어요.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 70%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAOABQDASIAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAMEAf/EABUBAQEAAAAAAAAAAAAAAAAAAAIA/9oADAMBAAIQAxAAAAFUKsKcTCv/xAAaEAACAwEBAAAAAAAAAAAAAAAAAQIDERIi/9oACAEBAAEFAo2ZXq3yKxCkdH//xAAXEQEAAwAAAAAAAAAAAAAAAAAAARES/9oACAEDAQE/Abbl/8QAFhEAAwAAAAAAAAAAAAAAAAAAARAh/9oACAECAQE/ARF//8QAHBAAAwABBQAAAAAAAAAAAAAAAAEhAhARMlGh/9oACAEBAAY/Amo32XcqzOPo5p//xAAaEAEAAgMBAAAAAAAAAAAAAAABABEhMWFB/9oACAEBAAE/IbxICmG3KgV3OBAFQzpDfjHLVT//2gAMAwEAAgADAAAAECjf/8QAFxEAAwEAAAAAAAAAAAAAAAAAARARMf/aAAgBAwEBPxAziP/EABgRAAIDAAAAAAAAAAAAAAAAAAABITFR/9oACAECAQE/EEsyMP/EABwQAQEAAgIDAAAAAAAAAAAAAAERACExUUFxof/aAAgBAQABPxAIBGMddamIqb2SvowPwQyCJ15WJIcrV4xFIB0LrP/Z&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/9230574b3e21504b874b9a5908b866fe/33ecc/conference.webp 360w,
/static/9230574b3e21504b874b9a5908b866fe/263a4/conference.webp 480w,
/static/9230574b3e21504b874b9a5908b866fe/0c8fb/conference.webp 640w,
/static/9230574b3e21504b874b9a5908b866fe/521f7/conference.webp 860w,
/static/9230574b3e21504b874b9a5908b866fe/cbd37/conference.webp 1200w,
/static/9230574b3e21504b874b9a5908b866fe/548e7/conference.webp 2400w&quot;
              sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/9230574b3e21504b874b9a5908b866fe/543cd/conference.jpg 360w,
/static/9230574b3e21504b874b9a5908b866fe/a3e66/conference.jpg 480w,
/static/9230574b3e21504b874b9a5908b866fe/56d4e/conference.jpg 640w,
/static/9230574b3e21504b874b9a5908b866fe/2f567/conference.jpg 860w,
/static/9230574b3e21504b874b9a5908b866fe/03ffe/conference.jpg 1200w,
/static/9230574b3e21504b874b9a5908b866fe/fdb5a/conference.jpg 2400w&quot;
            sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
            type=&quot;image/jpeg&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/9230574b3e21504b874b9a5908b866fe/fdb5a/conference.jpg&quot;
            alt=&quot;승용님과 동료들이 함께 다녀온 해외 주요 개발 관련 컨퍼런스 현장&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;승용님과 동료들이 함께 다녀온 해외 주요 개발 관련 컨퍼런스 현장&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 70%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAOABQDASIAAhEBAxEB/8QAGAAAAgMAAAAAAAAAAAAAAAAAAAQBAwX/xAAWAQEBAQAAAAAAAAAAAAAAAAABAAL/2gAMAwEAAhADEAAAAVrWIJM0zL//xAAaEAEBAAIDAAAAAAAAAAAAAAACAQASAwQR/9oACAEBAAEFAoBcV0l49866uiRRL9z/xAAWEQEBAQAAAAAAAAAAAAAAAAABEBH/2gAIAQMBAT8BHZ//xAAXEQADAQAAAAAAAAAAAAAAAAABAhAR/9oACAECAQE/AWGT/8QAGhAAAQUBAAAAAAAAAAAAAAAAAQACEBESA//aAAgBAQAGPwKxzKtaEBmY/8QAGRABAAMBAQAAAAAAAAAAAAAAAQARIUEx/9oACAEBAAE/IX8UzsC1QKZEqBSRApra87HhcQdgDpWZP//aAAwDAQACAAMAAAAQqA//xAAWEQEBAQAAAAAAAAAAAAAAAAABEBH/2gAIAQMBAT8QYDk//8QAFxEAAwEAAAAAAAAAAAAAAAAAAAERYf/aAAgBAgEBPxCxql0//8QAGhABAQEBAQEBAAAAAAAAAAAAAREhADFhgf/aAAgBAQABPxC7xfAhewmsMkBuXsqSjrfvE5lQy2LX84yjInx9yfeUgC5ATDv/2Q==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/0ae2cec6c27a4ecab7c67f4b34cd878a/33ecc/company.webp 360w,
/static/0ae2cec6c27a4ecab7c67f4b34cd878a/263a4/company.webp 480w,
/static/0ae2cec6c27a4ecab7c67f4b34cd878a/0c8fb/company.webp 640w,
/static/0ae2cec6c27a4ecab7c67f4b34cd878a/521f7/company.webp 860w,
/static/0ae2cec6c27a4ecab7c67f4b34cd878a/cbd37/company.webp 1200w,
/static/0ae2cec6c27a4ecab7c67f4b34cd878a/548e7/company.webp 2400w&quot;
              sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/0ae2cec6c27a4ecab7c67f4b34cd878a/543cd/company.jpg 360w,
/static/0ae2cec6c27a4ecab7c67f4b34cd878a/a3e66/company.jpg 480w,
/static/0ae2cec6c27a4ecab7c67f4b34cd878a/56d4e/company.jpg 640w,
/static/0ae2cec6c27a4ecab7c67f4b34cd878a/2f567/company.jpg 860w,
/static/0ae2cec6c27a4ecab7c67f4b34cd878a/03ffe/company.jpg 1200w,
/static/0ae2cec6c27a4ecab7c67f4b34cd878a/fdb5a/company.jpg 2400w&quot;
            sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
            type=&quot;image/jpeg&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/0ae2cec6c27a4ecab7c67f4b34cd878a/fdb5a/company.jpg&quot;
            alt=&quot;데브시스터즈는 프리미엄 사내 레스토랑, 리프레시룸, 쾌적한 사무 공간, 고성능의 장비 등 구성원들의 건강한 삶과 업무 몰입을 위한 여러 복지 혜택을 제공하고 있다&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;데브시스터즈는 프리미엄 사내 레스토랑, 리프레시룸, 쾌적한 사무 공간, 고성능의 장비 등 구성원들의 건강한 삶과 업무 몰입을 위한 여러 복지 혜택을 제공하고 있다&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h2 id=&quot;데이터-엔지니어링을-통해-구성원들이-새로운-가능성을-발견하고-개척하도록-돕는-일이-우리의-미션&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A7%81%EC%9D%84-%ED%86%B5%ED%95%B4-%EA%B5%AC%EC%84%B1%EC%9B%90%EB%93%A4%EC%9D%B4-%EC%83%88%EB%A1%9C%EC%9A%B4-%EA%B0%80%EB%8A%A5%EC%84%B1%EC%9D%84-%EB%B0%9C%EA%B2%AC%ED%95%98%EA%B3%A0-%EA%B0%9C%EC%B2%99%ED%95%98%EB%8F%84%EB%A1%9D-%EB%8F%95%EB%8A%94-%EC%9D%BC%EC%9D%B4-%EC%9A%B0%EB%A6%AC%EC%9D%98-%EB%AF%B8%EC%85%98&quot; aria-label=&quot;데이터 엔지니어링을 통해 구성원들이 새로운 가능성을 발견하고 개척하도록 돕는 일이 우리의 미션 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;데이터 엔지니어링을 통해 구성원들이 &apos;새로운 가능성&apos;을 발견하고, 개척하도록 돕는 일이 우리의 미션.&lt;/h2&gt;
&lt;h3 id=&quot;q-많은-회사들-중-데브시스터즈를-선택한-이유는-무엇인가요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#q-%EB%A7%8E%EC%9D%80-%ED%9A%8C%EC%82%AC%EB%93%A4-%EC%A4%91-%EB%8D%B0%EB%B8%8C%EC%8B%9C%EC%8A%A4%ED%84%B0%EC%A6%88%EB%A5%BC-%EC%84%A0%ED%83%9D%ED%95%9C-%EC%9D%B4%EC%9C%A0%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80%EC%9A%94&quot; aria-label=&quot;q 많은 회사들 중 데브시스터즈를 선택한 이유는 무엇인가요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Q. 많은 회사들 중 &apos;데브시스터즈&apos;를 선택한 이유는 무엇인가요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;태겸님&lt;/strong&gt;
저는 민수님의 권유로 데브시스터즈에 합류했어요. 이전 회사에서는 데이터 파이프라인을 개발하고 관리하는 업무를 했었죠. 하지만 단순 개발/관리에서 한 걸음 더 나아가, 플랫폼 측면에서의 시스템을 개발하고픈 개인적인 목표가 있었는데요. 데브시스터즈가 유수 컨퍼런스에서 발표한 세션들을 보며, 수준 높은 기술력과 도전할 수 있는 환경이 있고, 무엇보다 제가 성장하고 싶은 방향과 결을 같이 한다는 것을 느낀 후 합류를 결정했어요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;민수님&lt;/strong&gt;
먼저 데브시스터즈는 대학생 시절 가장 가고 싶은 회사 중 한 곳이었어요. 비록 다른 직장에 가게 됐지만, 이후 우연한 기회에 데브시스터즈 데이터 관련 직군 분들과 식사를 하게 됐죠. 당시 대화를 나누는 과정에서 데이터 직군 여러분들이 엔지니어링을 통해 풀고자 하는 여러 문제들과 고민들을 접할 수 있었는데요. 제게 그 고민들이 무척 매력적으로 느껴졌어요. 그래서 결국 지원을 결정하게 됐고, 지금은 고민을 함께 풀어나가고 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;찬수님&lt;/strong&gt;
인문학 동아리에서 만난 개발자 지인을 통해 처음 데브시스터즈를 알게 됐어요. 당시 대학생이었던 저는 회사 생활에 대해 부정적인 인식을 갖고 있었어요. 아마도 여러 미디어와 사람들이 말하는 회사 생활의 &apos;답답함&apos;이 제게 영향을 준 것 같아요.&lt;/p&gt;
&lt;p&gt;하지만 개발자 지인으로부터 접한 데브시스터즈의 이야기는 굉장히 흥미로웠어요. 특히 구성원들에게 다양한 권한이 주어진다는 것, 그리고 보다 효율적으로 일하기 위한 방식을 능동적으로 찾아나간다는 이야기가요. 저는 충분한 책임감과 권한을 바탕으로 일하는 것을 좋아했는데요. 데브시스터즈의 업무 환경과 문화라면 제가 제대로 일할 수 있겠다는 생각이 들었어요. 이후 실제 데브시스터즈의 구성원들을 직접 만나볼 수 있는 기회를 얻었는데요. 그때 회사의 문화와 사람, 환경들을 잠시나마 직접 경험하고 나니 지원하지 않을 이유가 없었죠(웃음).&lt;/p&gt;
&lt;h3 id=&quot;q-일하면서-보람을-느끼는-순간은-언제인지-궁금해요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#q-%EC%9D%BC%ED%95%98%EB%A9%B4%EC%84%9C-%EB%B3%B4%EB%9E%8C%EC%9D%84-%EB%8A%90%EB%81%BC%EB%8A%94-%EC%88%9C%EA%B0%84%EC%9D%80-%EC%96%B8%EC%A0%9C%EC%9D%B8%EC%A7%80-%EA%B6%81%EA%B8%88%ED%95%B4%EC%9A%94&quot; aria-label=&quot;q 일하면서 보람을 느끼는 순간은 언제인지 궁금해요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Q. 일하면서 보람을 느끼는 순간은 언제인지 궁금해요.&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;태겸님&lt;/strong&gt;
우리가 개발한 데이터 서비스가 구성원들 업무, 나아가 데브시스터즈의 성장에 큰 도움이 될 때 보람을 느껴요. 구성원들이 쉽고 편리하게 데이터를 기반으로 일하고, 새로운 가능성과 인사이트를 발견해 나가며, 그 결과가 마침내 고객 경험의 향상으로 이어지는 선순환을 경험할 때 무척 뿌듯해요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;민수님&lt;/strong&gt;
저 또한 태겸님과 마찬가지로, 우리가 만든 데이터 서비스를 통해 구성원들이 이전보다 훨씬 편리하게 데이터를 기반으로 일하는 걸 볼 때 큰 보람을 느껴요. 한편 구성원 여러분들께서 우리가 보다 나은 Data-Driven 문화를 만들어 나갈 수 있도록 여러 아이디어를 제안해 주시는데요. 그때 역시도 큰 보람과 Grit을 느껴요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;찬수님&lt;/strong&gt;
데이터 엔지니어는 다른 직무 구성원들과 소통하고 협업해야 하는 순간이 굉장히 많아요. 그 과정에서 사람들이 어떤 불편을 겪고 있고 있는지를 자연스럽게 알게 되는데요. 우리가 제시한 해결책이 조직 내 다양한 비효율들을 효과적으로 해결하고, 구성원들의 불편을 해소시킬 때 보람과 효능감을 느껴요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;승용님&lt;/strong&gt;
우리의 데이터 서비스가 제공하는 기능들이 구성원들의 업무에 잘 활용되어, 데브시스터즈 제품을 이용하는 고객 여러분들에게 긍정적인 영향을 전할 때 큰 보람을 느껴요. 또 과거 동료들이 지금 다니고 있는 회사에는 데이터플랫폼셀과 같은 조직이나, 서비스들이 없어서 아쉽고 불편하다고 연락을 줄 때도 큰 보람을 느끼고 있어요.&lt;/p&gt;
&lt;h3 id=&quot;q-데이터플랫폼셀이-데브시스터즈에서-이루고픈-비전은-무엇인가요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#q-%EB%8D%B0%EC%9D%B4%ED%84%B0%ED%94%8C%EB%9E%AB%ED%8F%BC%EC%85%80%EC%9D%B4-%EB%8D%B0%EB%B8%8C%EC%8B%9C%EC%8A%A4%ED%84%B0%EC%A6%88%EC%97%90%EC%84%9C-%EC%9D%B4%EB%A3%A8%EA%B3%A0%ED%94%88-%EB%B9%84%EC%A0%84%EC%9D%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80%EC%9A%94&quot; aria-label=&quot;q 데이터플랫폼셀이 데브시스터즈에서 이루고픈 비전은 무엇인가요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Q. 데이터플랫폼셀이 데브시스터즈에서 이루고픈 비전은 무엇인가요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;승용님&lt;/strong&gt;
데이터플랫폼셀이 제공하는 가치들을 통해 구성원들이 더 많은 가능성을 꿈꾸고, 데브시스터즈에서 꼭 좋은 콘텐츠와 서비스를 만들어내고 싶다는 의지를 불러일으키는 &apos;든든한 조력자&apos;가 되고 싶어요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;민수님&lt;/strong&gt;
구성원들이 더욱 효율적으로 Data-Driven 문화 속에서 일하며, 이전에는 생각하지 못했던 새로운 시도와 가치를 창출할 수 있도록 돕고 싶어요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;태겸님&lt;/strong&gt;
&apos;Facebook Scale&apos;, &apos;Google Scale&apos;이라는 표현처럼 글로벌 IT기업은 자신들이 보유한 데이터를 활용하는 것 자체가 새로운 도전이 되고, 모두에게 공유할 만큼의 노하우가 되는데요. 우리도 &apos;Devsisters Scale&apos; 이라는 주제로, 데브시스터즈가 데이터를 유의미하게 활용하기 위한 여러 고민들이 언제나 새로운 도전이 되고, 또 그 도전이 다시 데브시스터즈의 성장에 기여할 수 있길 바랍니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;찬수님&lt;/strong&gt;
데이터플랫폼셀은 장기적으로 데이터 생산자(게임을 개발하는 구성원)와 데이터 소비자(분석가, 의사결정자)는 물론, 데브시스터즈의 제품을 사랑하는 유저 여러분들까지 우리의 서비스를 유용하게 사용할 수 있도록 돕고자 해요. 이를 위해 사람들이 데이터를 활용하는 과정에 존재하는 허들을 낮추고, 누구나 쉽게 새로운 가능성을 발견할 수 있는 데이터 환경을 더욱 완성도 높게 만들어가고 싶습니다.&lt;/p&gt;
&lt;h3 id=&quot;q-함께-일할-셀원을-찾고-있다고-들었어요-어떤-일을-담당할-동료를-찾고-계신가요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#q-%ED%95%A8%EA%BB%98-%EC%9D%BC%ED%95%A0-%EC%85%80%EC%9B%90%EC%9D%84-%EC%B0%BE%EA%B3%A0-%EC%9E%88%EB%8B%A4%EA%B3%A0-%EB%93%A4%EC%97%88%EC%96%B4%EC%9A%94-%EC%96%B4%EB%96%A4-%EC%9D%BC%EC%9D%84-%EB%8B%B4%EB%8B%B9%ED%95%A0-%EB%8F%99%EB%A3%8C%EB%A5%BC-%EC%B0%BE%EA%B3%A0-%EA%B3%84%EC%8B%A0%EA%B0%80%EC%9A%94&quot; aria-label=&quot;q 함께 일할 셀원을 찾고 있다고 들었어요 어떤 일을 담당할 동료를 찾고 계신가요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Q. 함께 일할 셀원을 찾고 있다고 들었어요. 어떤 일을 담당할 동료를 찾고 계신가요?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;승용님&lt;/strong&gt;
다양한 포지션을 열어 두고 있는데요. 먼저 다양한 데이터플랫폼 요소들의 DevOps와 서비스 개발을 담당해 주실 &apos;데이터 서비스/DevOps 엔지니어&apos;를 찾고 있어요. 또 분석 플랫폼과 데이터의 활용에 기여해 주실 &apos;데이터 플랫폼 엔지니어&apos;도 찾고 있죠. 그리고 최근에는 &apos;신입/인턴 데이터 소프트웨어 엔지니어&apos;까지 다양한 포지션을 통해 여러분들의 지원을 기다리고 있습니다.&lt;/p&gt;
&lt;p&gt;경력직 엔지니어 포지션의 경우 우리가 주로 사용하는 기술 스택에 대한 경험도 좋지만, 꼭 그렇지 않더라도 &apos;관심이 있고 기본적인 개발 역량을 갖춘 분&apos;들도 환영해요. 가령 백엔드 개발을 주로 해왔지만, 데이터 엔지니어링 분야를 경험해보고 싶으신 분을 들 수 있어요. 또는 전통적인 데이터 웨어하우스나 Hadoop 기반의 데이터 엔지니어링을 해오셨지만, 클라우드 기반의 데이터 엔지니어링 분야로 커리어를 쌓아나가고 싶으신 분도 좋아요.&lt;/p&gt;
&lt;p&gt;신입과 인턴 포지션의 경우 특별한 경력 요구 조건 없이 &apos;기본적인 개발 역량을 갖추고 있고, 우리의 미션을 함께 이뤄나가고 싶은 분&apos;들을 적극 모시고 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;q-끝으로-지원을-고려-중이신-분들이-참고하면-좋을-말씀-한마디씩-부탁드립니다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#q-%EB%81%9D%EC%9C%BC%EB%A1%9C-%EC%A7%80%EC%9B%90%EC%9D%84-%EA%B3%A0%EB%A0%A4-%EC%A4%91%EC%9D%B4%EC%8B%A0-%EB%B6%84%EB%93%A4%EC%9D%B4-%EC%B0%B8%EA%B3%A0%ED%95%98%EB%A9%B4-%EC%A2%8B%EC%9D%84-%EB%A7%90%EC%94%80-%ED%95%9C%EB%A7%88%EB%94%94%EC%94%A9-%EB%B6%80%ED%83%81%EB%93%9C%EB%A6%BD%EB%8B%88%EB%8B%A4&quot; aria-label=&quot;q 끝으로 지원을 고려 중이신 분들이 참고하면 좋을 말씀 한마디씩 부탁드립니다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Q. 끝으로 지원을 고려 중이신 분들이 참고하면 좋을 말씀 한마디씩 부탁드립니다.&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;태겸님&lt;/strong&gt;
데이터플랫폼셀은 특정 역할에 국한되지 않고, 데이터가 제대로 흐르게 하기 위한 모든 엔지니어링적 문제를 해결해요. 그만큼 합류하시게 된다면 다양한 영역에 걸친 경험을 쌓을 수 있다고 생각해요. 또 주도적으로 문제를 해결하고 성장을 추구하는 우리의 문화는 스스로의 역량을 발전시키는데 큰 도움이 될 것이라고 생각합니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;민수님&lt;/strong&gt;
데이터플랫폼셀은 데이터 과학자, BI 분석가, 플랫폼 개발자 등 여러 구성원들과 협업해요. 그만큼 엔지니어링에 기반을 둔 문제들을 폭넓게 경험하고 해결해 볼 수 있어요. 또 다양한 사람들과 인사이트를 공유하며 한층 넓은 시야를 다질 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;찬수님&lt;/strong&gt;
우리는 데브시스터즈의 Data-Driven 문화가 지향하는 비전을 이루기 위해 새로운 프로젝트를 다양하게 준비하고 있어요. 그만큼 프로젝트의 처음부터 끝까지 직접 참여하고 경험해볼 기회가 많을 텐데요. 이 기회들을 통해 여러분의 엔지니어링적 역량 성장은 물론, 스스로의 가치와 효능감 또한 생생하게 경험하실 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;승용님&lt;/strong&gt;
이번 인터뷰에서 언급된 내용 외에도, 아래 자료들을 통해 우리가 지금까지 어떤 일을 해왔고, 하고 있는지를 확인하실 수 있어요. 또 이곳 데브시스터즈 기술 블로그에서도 우리의 고민과 연구, 그리고 인사이트가 풍부하게 소개될 예정이오니 많은 방문과 열람, 그리고 지원 부탁드립니다!&lt;/p&gt;
&lt;h3 id=&quot;데이터플랫폼셀-발표-자료-보러-가기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8D%B0%EC%9D%B4%ED%84%B0%ED%94%8C%EB%9E%AB%ED%8F%BC%EC%85%80-%EB%B0%9C%ED%91%9C-%EC%9E%90%EB%A3%8C-%EB%B3%B4%EB%9F%AC-%EA%B0%80%EA%B8%B0&quot; aria-label=&quot;데이터플랫폼셀 발표 자료 보러 가기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;데이터플랫폼셀 발표 자료 보러 가기&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.slideshare.net/seungyongoh3/rearchitecting-data-platform-with-kubernetes&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Kubernetes Forum Seoul 2019, Re-architecting Data Platform with Kubernetes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=iN-pQXQRzjI&amp;#x26;feature=youtu.be&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;NDC 2019, 어머님, A/B 테스트를 댁으로 들이십시오 - 〈DevPlay〉 A/B 테스트 플랫폼 개발기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=tzTrKEBjFFI&amp;#x26;feature=youtu.be&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;NDC 2019, 좋은 쿠키 있으면 소개시켜줘 - 데이터 기반 조합 추천&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.slideshare.net/seungyongoh3/elastic-stack-elasticon-2019-seoul-160821529&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;2019 Elastic{on} Seoul, Elastic Stack을 이용한 게임 서비스 통합 로깅 플랫폼&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.slideshare.net/awskorea/data-lake-architecture-case-study-bi-gaming-on-aws-2018&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Gaming on AWS 2018, 데브시스터즈 데이터 레이크 구축 이야기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=hVamSQogEuk&amp;#x26;feature=youtu.be&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;NDC 2018, Spark, Flintrock, Airflow로 구현하는 탄력적이고 유연한 데이터 분산 처리 자동화 인프라 구축&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://ndcreplay.nexon.com/NDC2015/sessions/NDC2015_0067.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;NDC 2015, &amp;#x3C;쿠키런&gt; 바쁘고 가난한 개발자를 위한 S3 기반 로그 시스템&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content:encoded></item><item><title><![CDATA[[Windows 컨테이너] 4: 베이스 이미지, 격리 방식에 대한 이해]]></title><description><![CDATA[Windows 컨테이너로 애플리케이션을 개발하거나 사용할 때 알아야 할 베이스 이미지, 격리 방식을 짚어보고, 실제로 사용할 때 고민해야 할 내용들을 짚어봅니다.]]></description><link>https://tech.devsisters.com/posts/windows-container-basics</link><guid isPermaLink="false">https://tech.devsisters.com/posts/windows-container-basics</guid><pubDate>Mon, 07 Sep 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;데브시스터즈는 &lt;a href=&quot;https://www.slideshare.net/seungyongoh3/ndc17-kubernetes&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Kubernetes 기반의 게임 개발 및 테스트 인프라를 성공적으로 구축&lt;/a&gt;하여 게임 출시에 이르는 모든 과정들을 효율적으로 처리하고 있습니다.&lt;/p&gt;
&lt;p&gt;다년간의 성공적인 운영 경험을 바탕으로 Windows Server 기반의 Workload를 지원하기 위한 R&amp;#x26;D 작업을 진행하였고, 그 경험담을 공유하는 글을 몇 편에 걸쳐 DevTech 기술 블로그에 연재할 예정입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Windows 컨테이너 시리즈 글 보기&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;/posts/intro-windows-container&quot;&gt;Windows 컨테이너에 대한 이해&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/start-windows-container-dev&quot;&gt;Windows 컨테이너 개발 환경 구축하고 테스트하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/migrate-nt-service-to-container&quot;&gt;NT 서비스를 Windows 컨테이너로 마이그레이션하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/windows-container-basics&quot;&gt;베이스 이미지, 격리 방식에 대한 이해&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;베이스-이미지의-종류&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%9D%B4%EB%AF%B8%EC%A7%80%EC%9D%98-%EC%A2%85%EB%A5%98&quot; aria-label=&quot;베이스 이미지의 종류 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;베이스 이미지의 종류&lt;/h2&gt;
&lt;p&gt;Windows 컨테이너를 사용할 때에는 베이스 이미지로 무엇을 어떻게 사용할 지 결정해야 합니다. 리눅스와 달리 Windows 컨테이너는 기존의 Windows Server용 애플리케이션을 컨테이너에서도 안정적으로 실행할 수 있도록 만드는 것이 목표였습니다. 그렇기 때문에 베이스 이미지를 얼마나 잘 이해하는지가 매우 중요합니다.&lt;/p&gt;
&lt;p&gt;모두들 아시는 것처럼 Windows는 리눅스와 달리 Microsoft가 독점하여 개발하고 배포하는 소프트웨어입니다. 그렇기 때문에 Docker에서도 Microsoft가 관리하는 레지스트리에서만 이미지를 다운로드할 수 있습니다. (물론 기술적으로 예외는 얼마든지 있을 수 있지만 논외로 합니다.)&lt;/p&gt;
&lt;p&gt;Windows 컨테이너 베이스 이미지는 여러 가지가 있지만, 대표적으로는 두 가지를 고려합니다. 서버 코어 이미지와 나노 서버 이미지 중 하나를 고르는 것이 일반적입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;서버 코어 이미지 &lt;code&gt;mcr.microsoft.com/windows/servercore:YYMM&lt;/code&gt;: Windows Server의 코어 버전과 대응되는 컨테이너 이미지로, GUI 애플리케이션을 제외한 거의 모든 부분에서 호환성이 보장됩니다. 대신 이미지 크기가 큰 편으로, 최신 버전의 경우 약 4~5GiB 정도 됩니다.&lt;/li&gt;
&lt;li&gt;나노 서버 이미지 &lt;code&gt;mcr.microsoft.com/windows/nanoserver:YYMM&lt;/code&gt;: 핵심적인 Windows의 기능만을 담고 있는 컨테이너 이미지로, x64 아키텍처로 컴파일된 애플리케이션만 실행할 수 있으며, 핵심적인 Win32 API만 사용하는 애플리케이션 (예: Python, Golang, .NET Core 등)을 실행할 수 있도록 최적화되어있어 크기가 200~500MiB 정도 됩니다. Windows PowerShell과 .NET Framework는 이 이미지에서는 사용할 수 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그 외에도 다음의 이미지들을 목적과 상황에 따라 찾습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Windows 이미지 &lt;code&gt;mcr.microsoft.com/windows:YYMM&lt;/code&gt;: Windows Server에 데스크톱 경험 기능 (DirectX, Internet Explorer 등)까지 얹어 실행할 수 있도록 만든 실제 Windows 운영 체제와 거의 대등한 기능 집합을 포함한 이미지로, 메시지 루프 처리가 필요한 일반적인 Win32 애플리케이션도 화면 출력 수단은 없지만 실행 자체는 지원합니다. 최신 버전의 경우 약 11~12GiB 정도 됩니다.&lt;/li&gt;
&lt;li&gt;IoT 이미지 &lt;code&gt;mcr.microsoft.com/windows/iotcore&lt;/code&gt;: 라즈베리 파이 등의 임베디드 컴퓨팅 디바이스 상에서 실행되는 Windows 10 IoT 버전에서도 Docker를 사용할 수 있으며, 여기에 배포할 애플리케이션은 IoT 베이스 이미지로 빌드해야 합니다.&lt;/li&gt;
&lt;li&gt;머신 러닝 이미지 &lt;code&gt;mcr.microsoft.com/windows/ml/insider&lt;/code&gt;: Windows 10에 내장되어있는 머신 러닝 API인 WinML을 컨테이너에서 사용할 수 있도록 제공하는 것으로 2020년 여름 현재 아직 인사이더 프리뷰 이미지입니다.&lt;/li&gt;
&lt;li&gt;인사이더 프리뷰 채널 &lt;code&gt;mcr.microsoft.com/windows/insider&lt;/code&gt;: 컨테이너 베이스 이미지도 인사이더 프리뷰 프로그램을 운영하며, Windows Server의 인사이더 프리뷰 버전이 SAC 채널 내에서 출시되면 약간의 시차가 존재하지만 대체로 빠른 시일 내에 일치하는 커널 버전을 포함하는 컨테이너 베이스 이미지가 같이 MCR에 게시됩니다.&lt;/li&gt;
&lt;li&gt;IIS 이미지 &lt;code&gt;mcr.microsoft.com/windows/servercore/iis&lt;/code&gt;: IIS 서버를 컨테이너 안에서 실행하기 원한다면 이 버전의 이미지를 사용할 수 있습니다. 인사이더 버전의 Windows Server는 &lt;code&gt;mcr.microsoft.com/windows/servercore/iis/insider&lt;/code&gt; 이미지를 사용할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;사용할 이미지의 종류를 결정하면, OS 버전을 선택해야 합니다. OS 버전을 선택할 때에는 컨테이너가 실행될 환경을 같이 고려해야 합니다. OS 버전을 적절하게 선택하지 않으면 컨테이너 이미지를 만들어도 실행하지 못할 수 있기 때문입니다.&lt;/p&gt;
&lt;h2 id=&quot;격리-방식에-대한-개요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%A9%EB%A6%AC-%EB%B0%A9%EC%8B%9D%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B0%9C%EC%9A%94&quot; aria-label=&quot;격리 방식에 대한 개요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;격리 방식에 대한 개요&lt;/h2&gt;
&lt;p&gt;이전 글에서도 간단히 언급했는데, Windows 컨테이너는 Windows 운영 체제 제품들 중에서 Windows Server를 위하여 만들어진 기능입니다. 그래서 컨테이너 실행 환경을 격리하는 방법이 처음부터 두 가지로 나뉘어져 있습니다.&lt;/p&gt;
&lt;p&gt;프로세스 격리 방식과 Hyper-V 격리 방식으로 나뉘어지는데, 프로세스 격리 방식은 가볍고 효율적인 대신 호스트와의 경계가 엄격하게 구분되어있지 않고, Hyper-V 격리 방식은 완전한 가상 컴퓨터를 만들지는 않는 대신, 커널 수준에서부터 실행 환경을 분리하여 구동하기 때문에 다소 메모리 사용량이 높습니다.&lt;/p&gt;
&lt;p&gt;그러나 이 내용 뿐 아니라, 격리 방식을 어디서 어떤 조건 아래에 사용할 수 있는지 정확히 이해하는 것이 좋습니다. 격리 방식의 적용 조건을 잘 이해하지 못하면 Windows 컨테이너 도입을 위한 개발에 아예 진입할 수 없을 수도 있고, 기대했던 성능에 못미치는 결과가 나타날 수도 있기 때문입니다.&lt;/p&gt;
&lt;h2 id=&quot;격리-방식의-차이점&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B2%A9%EB%A6%AC-%EB%B0%A9%EC%8B%9D%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90&quot; aria-label=&quot;격리 방식의 차이점 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;격리 방식의 차이점&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 890px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 27.77777777777778%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAIAAABM9SnKAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGHRFWHRTb2Z0d2FyZQBwYWludC5uZXQgNC4xLjQTQGjEAAAAzElEQVR42o3O20rDQBAG4H3/lxOkWJVKSJsmm+xusqfMPzup6DYFwSuFj5+5mJNKGTEj7PRCU0BlAuyeD6PHUdNR42zROVwsWos5sfKRmLAxF3Bj0DpuLdsojaGT4etSXJZu5jeNj6k28PuIQ1+Hi0uiOptioptwdZr4MhcdxKXNRJl2dq+1vxvDPQcvvZewborBDICwrnV3fYyHhadQHsxPEX8ZQ8kkynhKmZZIRLgV3goL/62e/Pos6umcDtf03KWXPr8O/1WbG7N+A+60UZGe41QlAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/7fcdb5cdf2886fc4729e804e30d2f25c/33ecc/container-arch-process.webp 360w,
/static/7fcdb5cdf2886fc4729e804e30d2f25c/263a4/container-arch-process.webp 480w,
/static/7fcdb5cdf2886fc4729e804e30d2f25c/0c8fb/container-arch-process.webp 640w,
/static/7fcdb5cdf2886fc4729e804e30d2f25c/521f7/container-arch-process.webp 860w,
/static/7fcdb5cdf2886fc4729e804e30d2f25c/ffcc1/container-arch-process.webp 890w&quot;
              sizes=&quot;(max-width: 890px) 100vw, 890px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/7fcdb5cdf2886fc4729e804e30d2f25c/53918/container-arch-process.png 360w,
/static/7fcdb5cdf2886fc4729e804e30d2f25c/9aebd/container-arch-process.png 480w,
/static/7fcdb5cdf2886fc4729e804e30d2f25c/0f09e/container-arch-process.png 640w,
/static/7fcdb5cdf2886fc4729e804e30d2f25c/3e3fe/container-arch-process.png 860w,
/static/7fcdb5cdf2886fc4729e804e30d2f25c/f3cf6/container-arch-process.png 890w&quot;
            sizes=&quot;(max-width: 890px) 100vw, 890px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/7fcdb5cdf2886fc4729e804e30d2f25c/f3cf6/container-arch-process.png&quot;
            alt=&quot;A diagram showing a container full of applications being isolated from the OS and hardware.&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;A diagram showing a container full of applications being isolated from the OS and hardware.&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;프로세스 격리 방식&lt;/strong&gt;은 Windows Server 컨테이너라고도 불리며, 리눅스에서 흔히 사용되는 컨테이너처럼 커널에 네임스페이스 컨셉을 도입하여 보안 계층, 자원을 분리 할당하는 방식으로 실행됩니다. DirectX를 통한 GPU 가속 등 하드웨어 연동에 최적화되어있으며, 실행되는 모양에서 차이가 있을 뿐, 사실상 직접 EXE 파일을 띄워 실행하는 것과 비슷한 성능과 양상을 나타냅니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 890px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 27.77777777777778%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAIAAABM9SnKAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGHRFWHRTb2Z0d2FyZQBwYWludC5uZXQgNC4xLjQTQGjEAAAA2UlEQVR42o2P62rDMAyF/f4vNxjdlS2hburUt9SObEu2NzYldIz9G3yIg25HEpAxAM7XYiLaHbfeBKNDkS6PJj2o8jSjdDh5PDk8OlyARIQSAd8tSV+ZS6yDLYMlFapPTV0RUkkZ3wxxcrT0qvGg8OirhyYmByYUHp6WamJz0O3a9No4sr4EkgZsKC+a9tVNxzaHjZi7qEiQcLB48qQDz2/YeBPnZbPdnVFyA5fWDRapNMG2fBi/jQU7UftL/4kflXr9zRPS12cVdxIOZ7if4FGl5/m/cPNo8zf0M1CSPtrpGgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/0ab08e70bbc1986a718bee185255ef75/33ecc/container-arch-hyperv.webp 360w,
/static/0ab08e70bbc1986a718bee185255ef75/263a4/container-arch-hyperv.webp 480w,
/static/0ab08e70bbc1986a718bee185255ef75/0c8fb/container-arch-hyperv.webp 640w,
/static/0ab08e70bbc1986a718bee185255ef75/521f7/container-arch-hyperv.webp 860w,
/static/0ab08e70bbc1986a718bee185255ef75/ffcc1/container-arch-hyperv.webp 890w&quot;
              sizes=&quot;(max-width: 890px) 100vw, 890px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/0ab08e70bbc1986a718bee185255ef75/53918/container-arch-hyperv.png 360w,
/static/0ab08e70bbc1986a718bee185255ef75/9aebd/container-arch-hyperv.png 480w,
/static/0ab08e70bbc1986a718bee185255ef75/0f09e/container-arch-hyperv.png 640w,
/static/0ab08e70bbc1986a718bee185255ef75/3e3fe/container-arch-hyperv.png 860w,
/static/0ab08e70bbc1986a718bee185255ef75/f3cf6/container-arch-hyperv.png 890w&quot;
            sizes=&quot;(max-width: 890px) 100vw, 890px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/0ab08e70bbc1986a718bee185255ef75/f3cf6/container-arch-hyperv.png&quot;
            alt=&quot;A diagram of a container being isolated within an OS on a visual machine that&amp;#39;s running on an OS within a physical machine.&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;A diagram of a container being isolated within an OS on a visual machine that&amp;#39;s running on an OS within a physical machine.&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hyper-V 격리 방식&lt;/strong&gt;은 Hyper-V 컨테이너라고도 불리며, 가상화 기술을 이용하여 호스트와 분리된 실행 환경을 만들고 여기에 컨테이너 베이스 이미지에 내장된 커널을 불러와 별도의 실행 환경을 만들고 실행합니다. 이 안에서 실행되는 애플리케이션은 호스트 OS의 존재를 네트워크가 아닌 프로세스나 API 레벨에서는 인지할 수 없습니다. 다만 완전한 가상 컴퓨터를 만들어 실행하는 것과 다르게 특수하게 최적화된 가벼운 가상 계층을 만드는 것이므로 호스트 운영 체제의 커널과 완전히 분리된 것은 아니어서 실행할 수 있는 베이스 이미지의 버전이 호스트 OS보다 높을 수는 없습니다.&lt;/p&gt;
&lt;h2 id=&quot;알기-쉽게-정리한-격리-방식-적용-조건&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%95%8C%EA%B8%B0-%EC%89%BD%EA%B2%8C-%EC%A0%95%EB%A6%AC%ED%95%9C-%EA%B2%A9%EB%A6%AC-%EB%B0%A9%EC%8B%9D-%EC%A0%81%EC%9A%A9-%EC%A1%B0%EA%B1%B4&quot; aria-label=&quot;알기 쉽게 정리한 격리 방식 적용 조건 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;알기 쉽게 정리한 격리 방식 적용 조건&lt;/h2&gt;
&lt;p&gt;어떤 격리 방식을 사용하던 간에 Windows 컨테이너는 기본적으로 다음의 전제를 가지고 사용할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;컨테이너 베이스 이미지의 OS 버전 &amp;lt;= 호스트 OS 버전&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그리고 격리 방식에 따라 세부 조건이 달라집니다. 프로세스 격리 방식의 경우 다음의 조건과 일치해야 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;컨테이너 베이스 이미지의 OS 버전 == 호스트 OS 버전&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Hyper-V 컨테이너 격리 방식을 사용하는 경우에는 다음의 조건과 일치해야 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;컨테이너 베이스 이미지의 OS 버전 &amp;lt;= 호스트 OS 버전 &amp;amp;&amp;amp;
호스트는 가상화 또는 중첩 가상화를 지원해야 합니다.&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그리고 위의 조건은 Windows Server에서 Docker 컨테이너를 사용할 때만이 아니라 Windows 10에서 컨테이너를 사용할 때에도 동일하게 적용되는 조건들입니다. Windows 10의 경우에도 Windows Server처럼 커널 버전과 일치하는 베이스 이미지를 사용하면 프로세스 격리 방식의 컨테이너를 사용할 수 있기 때문입니다. &lt;a href=&quot;https://github.com/moby/moby/pull/38000&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;관련 Pull Request&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;실제-사례-살펴보기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8B%A4%EC%A0%9C-%EC%82%AC%EB%A1%80-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0&quot; aria-label=&quot;실제 사례 살펴보기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;실제 사례 살펴보기&lt;/h3&gt;
&lt;p&gt;위의 조건에 따라 실제 상황을 가정해보면 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;게임 A의 게임 서버 Docker 이미지를 Windows Server Core 1809 베이스 이미지를 사용하여 만들었다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;해당 이미지를 실행할 서버는 Windows Server 2019이다. Windows Server 2019는 릴리스 버전이 1809이므로 Windows Server 1809이다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;프로세스 격리 방식으로 해당 서버에서 게임 서버 Docker 이미지를 레지스트리로부터 Pull하여 사용할 수 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;다른 한편으로 아래와 같은 문제를 만날 수도 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;게임 B의 게임 서버 Docker 이미지를 Windows Server Core 1903 베이스 이미지를 사용하여 만들었다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;해당 이미지를 실행할 서버는 Windows Server 2004 (2020년 4월 릴리스된 버전)이고, AWS에서 t3.large 인스턴스로 실행하려고 한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;OS 버전과 컨테이너 베이스 이미지 버전이 달라서 Hyper-V 격리 방식을 사용하려고 했지만, AWS EC2는 중첩 가상화가 지원되지 않는다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이미지의 OS 릴리스 버전을 정확하게 선택하지 않은 상태에서 퍼블릭 클라우드 환경에서 Windows 컨테이너를 실행하려고 하면 이와 같은 상황에 봉착하게 됩니다. 이런 문제를 피하기 위해서는 다음의 사항을 고려해야 합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;실행하려는 환경이 가상화나 중첩 가상화를 지원하는지 확인합니다. (Hyper-V로 가상 컴퓨터를 실행할 수 있는지의 여부를 확인하면 쉽습니다.)&lt;/li&gt;
&lt;li&gt;가상화나 중첩 가상화가 지원되지 않는 환경이라면 컨테이너를 실행할 OS의 릴리스 버전 (1809, 1903 등)을 확인하여 일치하는 베이스 이미지로 컨테이너 이미지를 만들어 등록합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;os-릴리스-버전에-대한-이해&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#os-%EB%A6%B4%EB%A6%AC%EC%8A%A4-%EB%B2%84%EC%A0%84%EC%97%90-%EB%8C%80%ED%95%9C-%EC%9D%B4%ED%95%B4&quot; aria-label=&quot;os 릴리스 버전에 대한 이해 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;OS 릴리스 버전에 대한 이해&lt;/h2&gt;
&lt;p&gt;Windows 10과 Windows Server 2016부터는 클라이언트 OS 버전과 서버 OS 버전이 동일한 릴리스 버전을 사용합니다. 예를 들어 Windows 10으로 2020년 5월 업데이트가 출시되면, Windows Server의 경우에도 일치하는 커널 버전을 사용하는 2004 (2020년 4월이라는 의미) 버전이 같이 출시됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 56.388888888888886%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAALABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAEF/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEAMQAAAB17QUf//EABQQAQAAAAAAAAAAAAAAAAAAACD/2gAIAQEAAQUCX//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQMBAT8BP//EABQRAQAAAAAAAAAAAAAAAAAAABD/2gAIAQIBAT8BP//EABQQAQAAAAAAAAAAAAAAAAAAACD/2gAIAQEABj8CX//EABsQAAIBBQAAAAAAAAAAAAAAAAAQEQExUWGB/9oACAEBAAE/IY2WwdooX//aAAwDAQACAAMAAAAQ88//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAEDAQE/ED//xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/ED//xAAbEAEBAQADAQEAAAAAAAAAAAABEQAhMVFBYf/aAAgBAQABPxCOWb4OKeGn91foZC9GBejQ8N//2Q==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/74fa9aa03671b8288371c41aaadfb4eb/33ecc/slide_17.webp 360w,
/static/74fa9aa03671b8288371c41aaadfb4eb/263a4/slide_17.webp 480w,
/static/74fa9aa03671b8288371c41aaadfb4eb/0c8fb/slide_17.webp 640w,
/static/74fa9aa03671b8288371c41aaadfb4eb/521f7/slide_17.webp 860w,
/static/74fa9aa03671b8288371c41aaadfb4eb/d71bc/slide_17.webp 1024w&quot;
              sizes=&quot;(max-width: 1024px) 100vw, 1024px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/74fa9aa03671b8288371c41aaadfb4eb/543cd/slide_17.jpg 360w,
/static/74fa9aa03671b8288371c41aaadfb4eb/a3e66/slide_17.jpg 480w,
/static/74fa9aa03671b8288371c41aaadfb4eb/56d4e/slide_17.jpg 640w,
/static/74fa9aa03671b8288371c41aaadfb4eb/2f567/slide_17.jpg 860w,
/static/74fa9aa03671b8288371c41aaadfb4eb/63217/slide_17.jpg 1024w&quot;
            sizes=&quot;(max-width: 1024px) 100vw, 1024px&quot;
            type=&quot;image/jpeg&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/74fa9aa03671b8288371c41aaadfb4eb/63217/slide_17.jpg&quot;
            alt=&quot;Windows Server Release Cadence by Jeff Wolsey, Slide #17&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Windows Server Release Cadence by Jeff Wolsey, Slide #17&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;다만 Windows 10과 달리 Windows Server의 경우 같이 출시가 되더라도 사용 중인 OS를 그대로 업그레이드 하는 기능은 빠진 상태로 출시됩니다. 이렇게 나오는 버전 사이의 간격이 대략 6개월에 한 번이므로 반기 채널 (SAC, Semi-Annual Channel)이라고 부릅니다.&lt;/p&gt;
&lt;p&gt;SAC 버전은 데스크톱 경험이 들어있지 않으며, Server Core 이미지와 컨테이너 이미지만 판올림하여 출시됩니다. 각각의 SAC 버전에는 작던 크던 변화 사항들이 포함되고, 각 버전은 지원 기간이 LTSC 버전에 비해 짧습니다.&lt;/p&gt;
&lt;p&gt;반면 장기 지원 채널 (LTSC, Long-Term Servicing Channel) 버전은 이전에 출시된 LTSC 버전의 OS를 그대로 업그레이드할 수 있으며, 데스크톱 경험을 포함하고, SAC 버전보다 훨씬 오랫동안 기술 지원이 되는 버전입니다. Windows 10 출시 이후로 Windows Server는 2016, 2019 두 번에 걸쳐 LTSC 버전이 출시되었습니다. 약 3~4년에 한 번 간격으로 출시됩니다.&lt;/p&gt;
&lt;p&gt;AWS, Azure 같은 퍼블릭 클라우드의 경우 복잡한 요구 사항을 충족시키기 위하여, 그리고 Kubernetes와 같은 오픈 소스 기술과의 협업을 위하여 SAC 버전의 Windows Server에 대한 지원을 꾸준히, 자주 업데이트합니다. 반면 GitHub이나 CircleCI 등의 환경에서는 LTSC 버전의 Windows Server만을 지원합니다.&lt;/p&gt;
&lt;p&gt;이런 조건 속에서 어떻게 실행 환경을 선택할 것인지에 대한 고민이 따르기 마련입니다.&lt;/p&gt;
&lt;h2 id=&quot;컨테이너-실행-환경의-선택과-관리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EC%8B%A4%ED%96%89-%ED%99%98%EA%B2%BD%EC%9D%98-%EC%84%A0%ED%83%9D%EA%B3%BC-%EA%B4%80%EB%A6%AC&quot; aria-label=&quot;컨테이너 실행 환경의 선택과 관리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;컨테이너 실행 환경의 선택과 관리&lt;/h2&gt;
&lt;p&gt;고려해야 할 것이 많은게 Windows 컨테이너 개발과 운영 환경이지만, 아래의 내용에 따라 적절한 선택지를 골라 활용하면 고민을 줄이거나 좀 더 효율적으로 대응할 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;개발-환경과-ci&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B0%9C%EB%B0%9C-%ED%99%98%EA%B2%BD%EA%B3%BC-ci&quot; aria-label=&quot;개발 환경과 ci permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;개발 환경과 CI&lt;/h3&gt;
&lt;p&gt;개발자가 컨테이너 이미지를 만들어야 하는 경우에는 항상 최신 버전의 Windows OS 버전과 Windows 10용 Docker를 설치하는 것이 좋습니다. 항상 최신 버전의 Windows 호스트 OS 버전을 유지하면 Hyper-V 방식을 통해 Windows 10용 Docker에서 하위 컨테이너 이미지 OS 버전을 모두 사용할 수 있기 때문입니다. (단, OS를 Insider Preview 참가 상태, 즉 Dev 채널이나 Beta 채널로 유지할 필요는 없습니다.) Hyper-V 컨테이너로 &lt;code&gt;docker build&lt;/code&gt; 명령을 실행하면 빌드 속도가 다소 느리지만, 정식으로 출시되어 MCR 레지스트리에 등재된 모든 Windows 컨테이너 이미지를 버전에 관계없이 완벽하게 다룰 수 있다는 장점이 있습니다.&lt;/p&gt;
&lt;p&gt;만약 빌드 자동화를 고려하고 계시다면, 애플리케이션의 유형에 따라 빌드 자동화 방법을 다르게 선택하는 것이 유리합니다. 그리고 컨테이너 베이스 이미지를 SAC 버전으로 정했다면, 시중에서 제공되는 호스팅 방식의 Windows 빌드 노드는 모두 LTSC 버전을 기준으로 하므로 별도로 빌드 노드를 구축하고 운영해야 합니다. 아울러 베어메탈이나 중첩 가상화를 쓸 수 있는 인스턴스 타입이 아닌 경우 빌드 노드의 OS 버전과 컨테이너 이미지의 OS 버전이 일치해야 함을 항상 주의합니다.&lt;/p&gt;
&lt;h3 id=&quot;컨테이너-오케스트레이션&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EC%98%A4%EC%BC%80%EC%8A%A4%ED%8A%B8%EB%A0%88%EC%9D%B4%EC%85%98&quot; aria-label=&quot;컨테이너 오케스트레이션 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;컨테이너 오케스트레이션&lt;/h3&gt;
&lt;p&gt;Kubernetes 환경이 아닌 다른 컨테이너 오케스트레이터 (예: Docker Swarm)를 사용한다면 LTSC 버전을 사용하는 것을 추천합니다. Kubernetes를 제외한 다른 오케스트레이터들은 이미 Windows Server 2016 출시 직후 몇 번의 업데이트로 검증된 성능과 안정성을 충분히 확보한지 오랜 시간이 지났으므로, SAC 버전을 따라서 올라가야 할 필요성이 높지 않습니다.&lt;/p&gt;
&lt;p&gt;반면 Kubernetes 환경에서 Windows 컨테이너를 실행하려는 경우 새로운 SAC 버전이 나올 때 마다 업그레이드하여 테스트하시는 것이 유리합니다. Kubernetes에 추가되는 풍부한 기능을 Windows OS에서도 사용할 수 있도록 마이크로소프트에서 Windows OS 자체를 계속 개발하고 있을 뿐만 아니라, 요즈음의 Windows Server 개발 방향 또한 컨테이너와 Kubernetes에 집중하고 있기 때문입니다.&lt;/p&gt;
&lt;p&gt;따라서 윈도우 컨테이너를 Kubernetes에서 사용할 때에는 LTSC 버전의 Windows Server 노드와 SAC 버전의 노드를 나누어 관리하고, 적절한 Toleration 및 Node Label 설정을 추가하여 워크로드를 분리하는 것이 좋습니다.&lt;/p&gt;
&lt;h2 id=&quot;참고-자료&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B0%B8%EA%B3%A0-%EC%9E%90%EB%A3%8C&quot; aria-label=&quot;참고 자료 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;참고 자료&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://unrealcontainers.com/docs/concepts/windows-containers&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Key Concept for Windows Containers (Unreal Container)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/virtualization/windowscontainers/manage-containers/hyperv-container&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Isolation Modes (Microsoft Docs)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://slideplayer.com/slide/14452401/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Windows Server Release Cadence by Jeff Wolsey&lt;/a&gt;&lt;/p&gt;
&lt;style&gt;
article#article.pc span.gatsby-resp-image-wrapper {
    max-width: 70% !important;
}
&lt;/style&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[Istio와 Spinnaker를 활용한 Blue-Green + Canary 자동 배포 전략 도입기]]></title><description><![CDATA[기존 배포 전략보다 더 효율적이고 안전한 자동 배포 전략을 실제 게임 서비스에 도입한 사례를 소개합니다.]]></description><link>https://tech.devsisters.com/posts/blue-green-canary-deployment</link><guid isPermaLink="false">https://tech.devsisters.com/posts/blue-green-canary-deployment</guid><pubDate>Wed, 19 Aug 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;데브시스터즈에서는 쿠키런: 오븐브레이크 외에도 많은 내부 개발팀과 외부 개발사가 개발한 많은 게임들을 운영, 관리하고 있습니다. 유저분들께 더 재밌고 풍성한 컨텐츠와 경험을 제공하기 위해서는 잦은 업데이트가 필연적인데요. 이렇듯 빈번하게 일어나는 배포를 조금 더 효율적이고 안전하게 하기 위해서 여러 배포 전략과 기술들을 연구하고 있습니다. 이번 글에서는 그 중 하나인 Kubernetes 환경 하에서의 Blue-Green 전략과 Canary 전략의 장점을 조합해서 실제 서비스에 적용한 사례를 소개합니다.&lt;/p&gt;
&lt;h2 id=&quot;0-심장-건강을-지키고-싶어요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#0-%EC%8B%AC%EC%9E%A5-%EA%B1%B4%EA%B0%95%EC%9D%84-%EC%A7%80%ED%82%A4%EA%B3%A0-%EC%8B%B6%EC%96%B4%EC%9A%94&quot; aria-label=&quot;0 심장 건강을 지키고 싶어요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;0. 심장 건강을 지키고 싶어요.&lt;/h2&gt;
&lt;p&gt;서버 엔지니어, 특히 DevOps 엔지니어라면 실제 서비스 하고 있는 제품을 업데이트 하는 일은 언제나 떨리고 부담스럽습니다. 아무리 많은 테스트와 QA를 거치더라도 실제 배포하려는 순간은 심장이 두근대기 마련이죠. 특히, 여러 게임을 배포해야 하는 팀 특성 상 배포가 몰리거나, 핫픽스를 해야 하는 순간이 닥치면 모두가 힘든 상황이 벌어졌습니다. 이런 상황이 반복되면서, 내부적으로 이런 물음에 다달았습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;어떻게 하면 고통 받지 않고 편안하게 게임을 업데이트할 수 있을까?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;1-요구-사항&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EC%9A%94%EA%B5%AC-%EC%82%AC%ED%95%AD&quot; aria-label=&quot;1 요구 사항 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;1. 요구 사항&lt;/h2&gt;
&lt;p&gt;지금까지는 Kubernetes가 자체적으로 지원하는 하나를 띄우고 하나를 끄는 형식의 Rolling Update 방식을 사용하고 있었습니다. 이 배포 전략으로는 위에 있는 물음을 해결하지 못한다는 결론에 다달았습니다.&lt;/p&gt;
&lt;p&gt;그래서 팀 내부적으로 모여 어떤 요구 사항들이 이루어지면 위의 물음을 해결할 수 있을 지 논의하였고, 회의 끝에 모은 요구 사항은 다음과 같았습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;구 서버로 Rollback이 쉽게 가능해야 한다.&lt;/li&gt;
&lt;li&gt;신 서버가 정상 작동하는지 모니터링 및 디버깅 할 수 있어야 한다.&lt;/li&gt;
&lt;li&gt;신 서버의 Warm-up이 가능해야한다.&lt;/li&gt;
&lt;li&gt;업데이트에 걸리는 시간이 단축되어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;회의가 끝난 후 위의 요구 사항을 만족하는 배포 전략을 연구하기 시작했습니다.&lt;/p&gt;
&lt;h2 id=&quot;2-무중단-배포&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%EB%AC%B4%EC%A4%91%EB%8B%A8-%EB%B0%B0%ED%8F%AC&quot; aria-label=&quot;2 무중단 배포 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;2. 무중단 배포&lt;/h2&gt;
&lt;p&gt;배포 전략은 크게 두 분류로 나눌 수 있습니다. 첫째는 중단 배포 방식입니다. 트래픽이 없는 상황에서 업데이트가 일어나기 때문에 가장 간단한 전략인 구 서버를 모두 종료시킨 후 신 서버를 배포하는 방식인 Recreate 전략이 가능합니다. 하지만 이 방식은 점검을 수반해야 하기에 유저 경험 측면에서 좋지 않습니다. 그래서 데이터베이스의 스키마 변경, 서버 간 프로토콜 변경과 같은 구 서버와 신 서버가 공존할 수 없을 때에만 제한적으로 사용합니다.&lt;/p&gt;
&lt;p&gt;둘째는 무중단 배포 방식입니다. 무중단 배포 방식은 말 그대로 서비스의 중단 없이 구 서버에서 신 서버로 업데이트하는 방식입니다. 어떻게 이게 가능할까요? 무중단 배포 방식에는 많은 전략이 있지만 이 중에 3개, Rolling Update, Blue-Green, Canary 전략을 소개합니다.&lt;/p&gt;
&lt;h3 id=&quot;rolling-update&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#rolling-update&quot; aria-label=&quot;rolling update permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Rolling Update&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 771px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 28.055555555555557%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGjHRFWHRteGZpbGUAJTNDbXhmaWxlJTIwaG9zdCUzRCUyMmFwcC5kaWFncmFtcy5uZXQlMjIlMjBtb2RpZmllZCUzRCUyMjIwMjAtMDgtMDRUMDYlM0EyNyUzQTEyLjk1N1olMjIlMjBhZ2VudCUzRCUyMjUuMCUyMChNYWNpbnRvc2glM0IlMjBJbnRlbCUyME1hYyUyME9TJTIwWCUyMDEwXzE1XzUpJTIwQXBwbGVXZWJLaXQlMkY1MzcuMzYlMjAoS0hUTUwlMkMlMjBsaWtlJTIwR2Vja28pJTIwQ2hyb21lJTJGODQuMC40MTQ3LjEwNSUyMFNhZmFyaSUyRjUzNy4zNiUyMiUyMGV0YWclM0QlMjJwYXdDT1p1c1dOT2FDdEVSb2QyOSUyMiUyMHZlcnNpb24lM0QlMjIxMy41LjklMjIlMjB0eXBlJTNEJTIyZGV2aWNlJTIyJTNFJTNDZGlhZ3JhbSUyMGlkJTNEJTIyem85eXJtbmI5bHU5QlN6bC14aUElMjIlMjBuYW1lJTNEJTIyUGFnZS0xJTIyJTNFN1ZwZGs1b3dGUDAxUE5hQkJCQWZ1Mmkzbldtbm5kbnB0UHNZNFNwMGtUQXhydHBmM3dEaDIxMjBYVmZjeG5GR2NwSmN5RDBuNU41RURidXIzUzBqU2ZDRiUyQmhCcFNQZDNHcDVxQ0NIZHRzVlBpdXh6eERCc25DTkxGdm9TcTRDNzhEZElVSmZvSnZSaDNXaklLWTE0bURSQmo4WXhlTHlCRWNib3R0bHNRYVBtWFJPeWhBNXc1NUdvaSUyRjRJZlI2VXc1aFVGUjhoWEFieTFnNGE1eFVyVWpTV0kxa0h4S2ZiR29SbkduWVpwVHklMkZXdTFjaUZMdkZYN0olMkIzMTRvclo4TUFZeFA2WURXcyUyRnZqTzNVJTJCaDUlMkZ2VFh1RTNhJTJGdWNYdnBKVkhFbTNrZ09YRDhuM2hnWUN2SW5GbGFQakdKJTJCc0FVbk82S0d5RGtNTTZJVjdhYml2b0Y5Z2lqQ0tYUnBSbFhiRlB3Rmw0QWw5elJoJTJCZ1ZtTjdEc3dYb2lhaFljd3pqcXdiOGRWSHVtNXB5TlZIcVU1Y1hiTkVhemVyTURJWXRkREpRVFF6MFc0NWVjTHdPT3N0Nmc4WU1Wb1ljbHB0eFZjTUx5Qko2b1hWYnBsT2c5SERaZzRzQnVHZVVTaUVtUTZUZmNvdXBnbjFwY3VCY2RnOXlhVlJLa1RNTGFBcjRHd3Ztc2dPWTZtcGZiTzRyUlJxU1Npb2FkTjA1THlRYzJKWjJxMWtJeTZrY2s1UUVWSXF1a1lWR2Vhd1pJU1ZqSzVSUnVWYVBSQVpXUjNWZ0MlMkZXZEZta2pBZDBTV01TelNyMGh0Rk43SmRxcXRwOHBqU1JldnNGbk85bGdFSTJuQXFvcGtiaEs3YiUyRm1mWVhWTW5pZmIxdXVwUEc4OUslMkJYdm9HTEJSakJ5YkJmQURwVXo5UGlSZ2szVEFQbm5HR0tXTW13cGJBJTJCd0tCTHNVTUlzTER4JTJCWnp2RGhqdG1Mc2RNYlFKUmtiSzhaT1p3eGZrakh6cklzcmNzYldEQjFhWEJmWjV6OWNYTmVQM2hsaU5NTzU4T3JxcUNEdEdvTTBiQThyU0pzb0dWMmpqRXc4TEJrVnFjZTVkR1NCNDV1SGRPU2dPYlp0cGFPJTJGMUpHbEQweEhSa2MyYnlTZzdZMWVqV1BEViUyQmRva2w4bm9qVzZVMTF4ZG5pWkhRNW5XSEhXdXhXakQ0MDBsVHRlWmU3WUR0WXVuanNhVnIlMkJRSVBiZnAlMkJlMm9oVFRHSm9UT1ZkSWNSQnJuYkNOMDNWU2p4TUs3T2dwSnUlMkZ3TFpWcUxlOXFiWTZYZGdzVCUyQlh0RDlrSzE0OXNlUTJiTFR2NWE2ZGpKZUNwSCUyRlE5SGZFY2N6cnd4Nm16OWhhaHJHM3B0NnM3NyUyQmxZNTBwbHlKTnNhVm82RWpuaDdLeDBOVDBkak5EQWQyVXBIVjZtanljQjA5R1lQSVh0enlTS3c3ejhrTm85bSUyQlhWeVNlUW8wbnBKczRaRzJrU1Ixa3VhUFREU2lpZFh1emJYdFd2VER0Zk91R3NqaXRYJTJGeiUyRk4wcyUyRm9iUDU3OUFRJTNEJTNEJTNDJTJGZGlhZ3JhbSUzRSUzQyUyRm14ZmlsZSUzRSOmEFMAAAFsSURBVHjaY9i/fz/Lqt13+ffv/w+k3/EzQIBifX0906oz7/gR8vtZ1p+fLwCTB2Km3XdXgcV3n5kJpOuB9Cp+hjVHnoQv3n3nxOrDT8IW77p75eST/8JAxZLr999XWLT77qV1hx+GLdz54PjGM4dD15ydfeXE4/0yQHmJjZc2iq8+O+fS1surwlednXt08+UVEavOzDnJsO7kR+GZqw9ardv9RHj66sMO////ZwJqUJq4bRv7jDVH7XaffCI8czlQ/uRu4dnbJwPl97OA5OuBLpq7baL9pjNLReZs6rfcfW2d8KzNk2zB7ldR8WCHegVGKwMxI4xPWD4XIR/afsk4be7rm0HNR+WBAixQCZXQ0FXMIE1+rSe00ua8vBnecVYTFG5AH4AMUqmvB/uEybf6oGLa3FfXo3quG4N1xvefF4id+cbFY+I2mC0goAB1AUNo/RWe2GlvXGK6L3BjlS88xhk947VzWscZcIQCACHAqYFz9T70AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/0b13eacaf2bece2190812d68ff3ce59d/33ecc/0-rolling-update.webp 360w,
/static/0b13eacaf2bece2190812d68ff3ce59d/263a4/0-rolling-update.webp 480w,
/static/0b13eacaf2bece2190812d68ff3ce59d/0c8fb/0-rolling-update.webp 640w,
/static/0b13eacaf2bece2190812d68ff3ce59d/88613/0-rolling-update.webp 771w&quot;
              sizes=&quot;(max-width: 771px) 100vw, 771px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/0b13eacaf2bece2190812d68ff3ce59d/53918/0-rolling-update.png 360w,
/static/0b13eacaf2bece2190812d68ff3ce59d/9aebd/0-rolling-update.png 480w,
/static/0b13eacaf2bece2190812d68ff3ce59d/0f09e/0-rolling-update.png 640w,
/static/0b13eacaf2bece2190812d68ff3ce59d/f0a60/0-rolling-update.png 771w&quot;
            sizes=&quot;(max-width: 771px) 100vw, 771px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/0b13eacaf2bece2190812d68ff3ce59d/f0a60/0-rolling-update.png&quot;
            alt=&quot;Rolling Update Overview&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Rolling Update Overview&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Ramped, Incremental 이라고도 불리는 Rolling Update는 위와 같이, 서버가 조금씩 교체되면서 모든 서버가 교체될 때까지 진행하는 배포 전략입니다.&lt;/p&gt;
&lt;p&gt;해당 배포 전략은 Kubernetes에서 지원하는 배포 전략으로 maxSurge, maxUnavailable 파라미터 값을 통해 의도한 파드의 수에 대해 생성할 수 있는 최대 파드 수와 업데이트 도중 사용할 수 없는 최대 파드의 수를 지정할 수 있습니다.&lt;/p&gt;
&lt;p&gt;무중단 배포를 할 때 저희가 기존에 사용하고 있었던 전략이며, 서비스의 안정성을 위해 maxUnavailable 값을 최소로 하여 최대한 천천히 rolling update 하고 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;blue-green&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#blue-green&quot; aria-label=&quot;blue green permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Blue-Green&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1011px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 21.38888888888889%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAECAYAAACOXx+WAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGVnRFWHRteGZpbGUAJTNDbXhmaWxlJTIwaG9zdCUzRCUyMmFwcC5kaWFncmFtcy5uZXQlMjIlMjBtb2RpZmllZCUzRCUyMjIwMjAtMDgtMDRUMDYlM0ExOCUzQTMxLjI0MVolMjIlMjBhZ2VudCUzRCUyMjUuMCUyMChNYWNpbnRvc2glM0IlMjBJbnRlbCUyME1hYyUyME9TJTIwWCUyMDEwXzE1XzUpJTIwQXBwbGVXZWJLaXQlMkY1MzcuMzYlMjAoS0hUTUwlMkMlMjBsaWtlJTIwR2Vja28pJTIwQ2hyb21lJTJGODQuMC40MTQ3LjEwNSUyMFNhZmFyaSUyRjUzNy4zNiUyMiUyMGV0YWclM0QlMjJOdGRoWXJnclFWd0pqMWtEM0lGUCUyMiUyMHZlcnNpb24lM0QlMjIxMy41LjklMjIlMjB0eXBlJTNEJTIyZGV2aWNlJTIyJTNFJTNDZGlhZ3JhbSUyMGlkJTNEJTIyem85eXJtbmI5bHU5QlN6bC14aUElMjIlMjBuYW1lJTNEJTIyUGFnZS0xJTIyJTNFN1ZyUmJwc3dGUDBhSGhlQkRRUWVWNUoxa3phdFVqVnRmWFRnSnJBU2pCeW5DZnY2R1hBZ2tMUnAxTkNBNmloUzhMRjl3ZmNjNnNzcEd2YVcyMXRHMHZBSERTRFdrQjVzTlR6UkVFSzZiWXVmSE1sS3hEQnNYQ0lMRmdVU3E0SDc2QjlJVUpmb09ncGcxUmpJS1kxNWxEWkJueVlKJTJCTHlCRWNib3BqbHNUdVBtV1ZPeWdBUGczaWZ4SWZvN0NuaFlMY090Tzc1Q3RBamxxUjAwTGp1V1pEZFlybVFWa29CdTlpQTgxYkRIS09YbDBYTHJRWnhuYjVlWGN0NlhaM3FyQzJPUThOZE1RS3ZadmJHWldMJTJCU243ZkdROG9lMXJmNGs0enlST0sxWExDOFdKN3RNaER5WlN5T0RBM2ZCR1FWUWg1T0Y0MU5HSEZZcGNUUHgyMEUlMkZRS2JSM0hzMFppeVlpb09DRGh6WCUyQkFyenVnajdQWFl2Z096dWVoSmFaVHdnaVByUm56MWthNWJHdkwwVWE0VFQ5Y3NNZG9yT293Q1JpM1VQWW9XSWRvajNXY0NqNHZab3Y5SUVLT0ZJYWMxVm56RjhrS1M1bGxZYmhmNWJUQjZYTSUyQkFKU0RTTTRxRU1QTmxzbSUyRkZ3U1NsZ1V3NU1BN2JaN2swS29XSWV3dm9FampMeEJBNVlTdzFsVFdibTFxaGxvVENQVzJhanJ3djVEMnhxT0xXc2hFSFVqbG5xQWdwRlExUlJZYlpMeGxoSmFNaHlxamFxM3NpSSUyQnRBTlJDSVBWMDJLZU1oWGRDRXhOTWF2V0YwblFTVm11b3gzeWxOcGQ3JTJCQXVlWkxGREltbE1CN2FsUjVJcGxmJTJGTDVnaXJaZk5qdm0yeGw4TEtWN2JmdWdFVmk3Y0FrV0M0Z3YlMkJxWEtSR0xwR3Ztd3d2Sk1HWE5STmdDJTJCS2xDNEpCaUJqSGgwVlB6T2k3T21LMFlPNTh4ZEUzR3hvcXg4eG5EMTJUTTdIUnpSYzdZbXFKam0lMkJ1OCUyQkh6QXpYWDE1RjlpYzIyViUyQnNhMWQxZW4yeUxOQWljd2olMkJuSVFUTnMyeDlRUjVjcDBqRHVWNUhtS2hrTlVVYW0zaThaN1I0OWxJNEdwcU8lMkJPVml2OEI0Z0NUN25qckpvSlRTQlptMWFDbVJuRVZ0bkZKaUhPVHFSaEIzMjZycFJudUV1VjJwTmdkV2lvSXE3QzFIV3ZYTFd2ckY4SXBEWmlsUFd4UWR4Q3A2cVZiJTJGQk5WTHU0eUQlMkZCTmc5cTBpd3NoOEhxYU54ejBvUzNPMlR0dEpSVnpycVdVbGlIcWxBQnVheE5Zc2k0N0t1RzVhSlAyMjd1Y2Q1ZnlmZkRTc2FMMEtqcVYlMkJWUmxQUmVCa2FyJTJGcWZKdHl0ZmFsczhJNXNjS2Yxc0hCMUd4d3JBM09RVlo3ajlxektVd2JtSUhYazJqM1RVY2R2Y2lvZGRmUU9udjV1TnBobzFtOGJsJTJCNXIlMkZkSTJudjRIJTNDJTJGZGlhZ3JhbSUzRSUzQyUyRm14ZmlsZSUzRVoSlSsAAAEaSURBVHjaY1i2/37kqsNPXVcfeuCz4sBjL4b//xlXn5vnv+HSMu915xZ4br26IoBPhk9I18ZGcNWBJ36rDj7wXrLnjufyA/eDGIBg4+UlPpsvLfVaD8aLAhgWbLsaM3/LFY+le275Lt5+zRukaM3Zeb7rL63wWnNmnvuWG6v9+fj4hCws3IRWH3oOtPS+19J999xXHLofAFK77PhMv1Vn5nuvBeJFR2YGg8RYgJgdiBld0lbxx057GWtfvx8kBgKMUFoAiHmRxUJX/WeLnfY0yiZzqSCQywTErGC86v9/5rS0NNZ6oGBaxxn+9DkvoutXXWH7D/T6qlWrmKGG8MMMXLXqPzNIrn7/f460eW/DQXrq64F6gWaA1AMAYpB+BczmOAAAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/45e5e55e909ad1c9c5074f73ed5f065a/33ecc/1-blue-green.webp 360w,
/static/45e5e55e909ad1c9c5074f73ed5f065a/263a4/1-blue-green.webp 480w,
/static/45e5e55e909ad1c9c5074f73ed5f065a/0c8fb/1-blue-green.webp 640w,
/static/45e5e55e909ad1c9c5074f73ed5f065a/521f7/1-blue-green.webp 860w,
/static/45e5e55e909ad1c9c5074f73ed5f065a/372ad/1-blue-green.webp 1011w&quot;
              sizes=&quot;(max-width: 1011px) 100vw, 1011px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/45e5e55e909ad1c9c5074f73ed5f065a/53918/1-blue-green.png 360w,
/static/45e5e55e909ad1c9c5074f73ed5f065a/9aebd/1-blue-green.png 480w,
/static/45e5e55e909ad1c9c5074f73ed5f065a/0f09e/1-blue-green.png 640w,
/static/45e5e55e909ad1c9c5074f73ed5f065a/3e3fe/1-blue-green.png 860w,
/static/45e5e55e909ad1c9c5074f73ed5f065a/0d97d/1-blue-green.png 1011w&quot;
            sizes=&quot;(max-width: 1011px) 100vw, 1011px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/45e5e55e909ad1c9c5074f73ed5f065a/0d97d/1-blue-green.png&quot;
            alt=&quot;Blue-Green Overview&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Blue-Green Overview&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Red-Black 이라고도 불리는 Blue-Green 전략은 구 서버와 똑같은 스펙으로 신 서버를 띄운 후 신 서버로 트래픽을 돌리는 전략입니다. 이 전략은 구 서버와 신 서버가 모두 떠 있어 신 서버에 문제가 있는 경우 트래픽만 구 서버로 전환하면 되기 때문에 Rollback이 쉽고, 업데이트 과정에서 소요되는 시간이 적습니다. 하지만 원래 서버의 스펙의 2배로 띄워야 하고 서버셋(구 서버, 신 서버)이 같이 뜰 수 있도록 환경을 조성해야 합니다.&lt;/p&gt;
&lt;h3 id=&quot;canary&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#canary&quot; aria-label=&quot;canary permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Canary&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 771px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 28.055555555555557%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGjHRFWHRteGZpbGUAJTNDbXhmaWxlJTIwaG9zdCUzRCUyMmFwcC5kaWFncmFtcy5uZXQlMjIlMjBtb2RpZmllZCUzRCUyMjIwMjAtMDgtMDRUMDYlM0EyNyUzQTIyLjg0OFolMjIlMjBhZ2VudCUzRCUyMjUuMCUyMChNYWNpbnRvc2glM0IlMjBJbnRlbCUyME1hYyUyME9TJTIwWCUyMDEwXzE1XzUpJTIwQXBwbGVXZWJLaXQlMkY1MzcuMzYlMjAoS0hUTUwlMkMlMjBsaWtlJTIwR2Vja28pJTIwQ2hyb21lJTJGODQuMC40MTQ3LjEwNSUyMFNhZmFyaSUyRjUzNy4zNiUyMiUyMGV0YWclM0QlMjIxczJZX2h3N19UUUlXaVZVUzc2QyUyMiUyMHZlcnNpb24lM0QlMjIxMy41LjklMjIlMjB0eXBlJTNEJTIyZGV2aWNlJTIyJTNFJTNDZGlhZ3JhbSUyMGlkJTNEJTIyem85eXJtbmI5bHU5QlN6bC14aUElMjIlMjBuYW1lJTNEJTIyUGFnZS0xJTIyJTNFN1ZwZGs1b3dGUDAxUE5hQkJCQWZ1Mmkzbldtbm5kbnB0UHNZNFNwMGtUQXhydHBmM3dEaDIxMjBYVmZjeG5GR2NwSmN5RDBuNU41RURidXIzUzBqU2ZDRiUyQmhCcFNQZDNHcDVxQ0NIZHRzVlBpdXh6eERCc25DTkxGdm9TcTRDNzhEZElVSmZvSnZSaDNXaklLWTE0bURSQmo4WXhlTHlCRWNib3R0bHNRYVBtWFJPeWhBNXc1NUdvaSUyRjRJZlI2VXc1aFVGUjhoWEFieTFnNGE1eFVyVWpTV0kxa0h4S2ZiR29SbkduWVpwVHklMkZXdTFjaUZMdkZYN0olMkIzMTRvclo4TUFZeFA2WURXcyUyRnZqTzNVJTJCaDUlMkZ2VFh1RTNhJTJGdWNYdnBKVkhFbTNrZ09YRDhuM2hnWUN2SW5GbGFQakdKJTJCc0FVbk82S0d5RGtNTTZJVjdhYml2b0Y5Z2lqQ0tYUnBSbFhiRlB3Rmw0QWw5elJoJTJCZ1ZtTjdEc3dYb2lhaFljd3pqcXdiOGRWSHVtNXB5TlZIcVU1Y1hiTkVhemVyTURJWXRkREpRVFF6MFc0NWVjTHdPT3N0Nmc4WU1Wb1ljbHB0eFZjTUx5Qko2b1hWYnBsT2c5SERaZzRzQnVHZVVTaUVtUTZUZmNvdXBnbjFwY3VCY2RnOXlhVlJLa1RNTGFBcjRHd3Ztc2dPWTZtcGZiTzRyUlJxU1Npb2FkTjA1THlRYzJKWjJxMWtJeTZrY2s1UUVWSXF1a1lWR2Vhd1pJU1ZqSzVSUnVWYVBSQVpXUjNWZ0MlMkZXZEZta2pBZDBTV01TelNyMGh0Rk43SmRxcXRwOHBqU1JldnNGbk85bGdFSTJuQXFvcGtiaEs3YiUyRm1mWVhWTW5pZmIxdXVwUEc4OUslMkJYdm9HTEJSakJ5YkJmQURwVXo5UGlSZ2szVEFQbm5HR0tXTW13cGJBJTJCd0tCTHNVTUlzTER4JTJCWnp2RGhqdG1Mc2RNYlFKUmtiSzhaT1p3eGZrakh6cklzcmNzYldEQjFhWEJmWjV6OWNYTmVQM2hsaU5NTzU4T3JxcUNEdEdvTTBiQThyU0pzb0dWMmpqRXc4TEJrVnFjZTVkR1NCNDV1SGRPU2dPYlp0cGFPJTJGMUpHbEQweEhSa2MyYnlTZzdZMWVqV1BEViUyQmRva2w4bm9qVzZVMTF4ZG5pWkhRNW5XSEhXdXhXakQ0MDBsVHRlWmU3WUR0WXVuanNhVnIlMkJRSVBiZnAlMkJlMm9oVFRHSm9UT1ZkSWNSQnJuYkNOMDNWU2p4TUs3T2dwSnUlMkZ3TFpWcUxlOXFiWTZYZGdzVCUyQlh0RDlrSzE0OXNlUTJiTFR2NWE2ZGpKZUNwSCUyRlE5SGZFY2N6cnd4Nm16OWhhaHJHM3B0NnM3NyUyQmxZNTBwbHlKTnNhVm82RWpuaDdLeDBOVDBkak5EQWQyVXBIVjZtanljQjA5R1lQSVh0enlTS3c3ejhrTm85bSUyQlhWeVNlUW8wbnBKczRaRzJrU1Ixa3VhUFREU2lpZFh1emJYdFd2VER0Zk91R3NqaXRYJTJGeiUyRk4wcyUyRm9iUDU3OUFRJTNEJTNEJTNDJTJGZGlhZ3JhbSUzRSUzQyUyRm14ZmlsZSUzRXZnnFUAAAFsSURBVHjaY9i/fz/Lqt13+ffv/w+k3/EzQIBifX0906oz7/gR8vtZ1p+fLwCTB2Km3XdXgcV3n5kJpOuB9Cp+hjVHnoQv3n3nxOrDT8IW77p75eST/8JAxZLr999XWLT77qV1hx+GLdz54PjGM4dD15ydfeXE4/0yQHmJjZc2iq8+O+fS1surwlednXt08+UVEavOzDnJsO7kR+GZqw9ardv9RHj66sMO////ZwJqUJq4bRv7jDVH7XaffCI8czlQ/uRu4dnbJwPl97OA5OuBLpq7baL9pjNLReZs6rfcfW2d8KzNk2zB7ldR8WCHegVGKwMxI4xPWD4XIR/afsk4be7rm0HNR+WBAixQCZXQ0FXMIE1+rSe00ua8vBnecVYTFG5AH4AMUqmvB/uEybf6oGLa3FfXo3quG4N1xvefF4id+cbFY+I2mC0goAB1AUNo/RWe2GlvXGK6L3BjlS88xhk947VzWscZcIQCACHAqYFz9T70AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/8f91328566759996e6ccf17a5f7c33bc/33ecc/2-canary.webp 360w,
/static/8f91328566759996e6ccf17a5f7c33bc/263a4/2-canary.webp 480w,
/static/8f91328566759996e6ccf17a5f7c33bc/0c8fb/2-canary.webp 640w,
/static/8f91328566759996e6ccf17a5f7c33bc/88613/2-canary.webp 771w&quot;
              sizes=&quot;(max-width: 771px) 100vw, 771px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/8f91328566759996e6ccf17a5f7c33bc/53918/2-canary.png 360w,
/static/8f91328566759996e6ccf17a5f7c33bc/9aebd/2-canary.png 480w,
/static/8f91328566759996e6ccf17a5f7c33bc/0f09e/2-canary.png 640w,
/static/8f91328566759996e6ccf17a5f7c33bc/f0a60/2-canary.png 771w&quot;
            sizes=&quot;(max-width: 771px) 100vw, 771px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/8f91328566759996e6ccf17a5f7c33bc/f0a60/2-canary.png&quot;
            alt=&quot;Canary Overview&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Canary Overview&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Canary의 사전적 의미는 카나리아라는 참새목의 새입니다. 유독가스에 굉장히 민감한 동물이라 광산에서 유독가스의 누출을 확인하는 용도로 사용되었다고 합니다. 이처럼 일부만 신 서버로 교체하여 모니터링과 디버깅을 한 후, 문제가 없는 경우 모든 서버를 교체하는 방식으로 이루어집니다.&lt;/p&gt;
&lt;h2 id=&quot;3-blue-green--canary-도입&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-blue-green--canary-%EB%8F%84%EC%9E%85&quot; aria-label=&quot;3 blue green  canary 도입 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;3. Blue-Green + Canary 도입&lt;/h2&gt;
&lt;p&gt;여러 연구 끝에 Blue-Green 전략처럼 구 서버와 같은 스펙으로 신 서버를 미리 프로비저닝하고, Canary 전략처럼 신 서버에 조금의 트래픽을 흘려 검증 과정을 거친 후 모든 트래픽을 옮기는 방식을 도입하기로 결정하였습니다. 파이프라인의 개요는 다음과 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;i. 구 서버와 같은 스펙으로 신 서버를 프로비저닝한다.&lt;/li&gt;
&lt;li&gt;ii. 약간의 트래픽을 신 서버로 보낸다.&lt;/li&gt;
&lt;li&gt;iii. 신 서버를 집중 모니터링, 디버깅 후 신 서버로의 트래픽 양을 높힌다.&lt;/li&gt;
&lt;li&gt;iv. 신 서버가 충분히 안정되면 구 서버를 종료한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;파이프라인이 개념적으로는 간단하지만, 이를 구현하는 것에는 몇 가지 기술적 어려움이 있으며, 파이프라인을 제공하기 위해서는 여러 툴들이 필요합니다. 하나 하나씩 살펴보겠습니다.&lt;/p&gt;
&lt;h3 id=&quot;트래픽-제어-istio&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%8A%B8%EB%9E%98%ED%94%BD-%EC%A0%9C%EC%96%B4-istio&quot; aria-label=&quot;트래픽 제어 istio permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;트래픽 제어; Istio&lt;/h3&gt;
&lt;p&gt;위 파이프라인에서 핵심적인 기술 요소는 트래픽 제어입니다. 구 서버와 신 서버 간의 트래픽 양을 제어할 수 있어야 합니다. 트래픽 양을 제어하는 방식은 Kubernetes의 서비스 네트워킹(kube-proxy)를 이용하거나, Nginx와 같은 Ingress Gateway를 사용하는 방식, Istio와 같은 Service Mesh Provider를 사용하는 방식 등이 있습니다.&lt;/p&gt;
&lt;p&gt;저희는 Istio를 이미 도입하여 사용하고 있어, 아래와 같이 Virtual Service를 통하여 특정 host에 가중치를 정의할 수 있었습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; networking.istio.io/v1alpha3
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; VirtualService
&lt;span class=&quot;token punctuation&quot;&gt;...&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;...&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;http&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;route&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;destination&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; old&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;server
      &lt;span class=&quot;token key atrule&quot;&gt;weight&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;90&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;destination&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; new&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;server
      &lt;span class=&quot;token key atrule&quot;&gt;weight&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그리고 파이프라인의 자동화를 위하여 Virtual Service를 조작하는 이미지를 만들어 Kubernetes의 Job을 통해 쉽게 트래픽을 제어할 수 있도록 했습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;go&quot;&gt;&lt;pre class=&quot;language-go&quot;&gt;&lt;code class=&quot;language-go&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; targetService &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; flag&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;String&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;targetService&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Target Service Name. (required)&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; percentage &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; flag&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Int&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;percentage&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Target gets $percentage of traffic.&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;
    vsClient &lt;span class=&quot;token operator&quot;&gt;:=&lt;/span&gt; ic&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;NetworkingV1alpha3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;VirtualServices&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;namespace&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    vs&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; err &lt;span class=&quot;token operator&quot;&gt;:=&lt;/span&gt; vsClient&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;virtualService&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; metav1&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;GetOptions&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; http &lt;span class=&quot;token operator&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;range&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;vs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Spec&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Http&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;

      &lt;span class=&quot;token comment&quot;&gt;// 변동된 weight에 의해 rebalancing&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;var&lt;/span&gt; sumOfWeight &lt;span class=&quot;token builtin&quot;&gt;int32&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; route &lt;span class=&quot;token operator&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;range&lt;/span&gt; http&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Route &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;previousWeight &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          route&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Weight &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;int32&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;percentage&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;http&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Route&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          route&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Weight &lt;span class=&quot;token operator&quot;&gt;*=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;int32&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;percentage&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; previousWeight&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        sumOfWeight &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; route&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Weight
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; route &lt;span class=&quot;token operator&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;range&lt;/span&gt; http&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Route &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; sumOfWeight &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;int32&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;percentage&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token keyword&quot;&gt;break&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        route&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Weight&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;
        sumOfWeight&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

      &lt;span class=&quot;token comment&quot;&gt;// target service 생성&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;percentage &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        http&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Route &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;http&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Route&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;istiov1alpha3&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;HTTPRouteDestination&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          Destination&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;istiov1alpha3&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Destination&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            Host&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;targetService&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          Weight&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;int32&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt;percentage&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

     &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;

      &lt;span class=&quot;token boolean&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; updateErr &lt;span class=&quot;token operator&quot;&gt;:=&lt;/span&gt; vsClient&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Update&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;vs&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; updateErr &lt;span class=&quot;token operator&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; updateErr
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;continue&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# requires rendering of spinnaker&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; batch/v1
&lt;span class=&quot;token key atrule&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Job
&lt;span class=&quot;token punctuation&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;serviceAccountName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; istio&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;traffic&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;shifter
      &lt;span class=&quot;token key atrule&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; istio&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;traffic&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;shifter
          &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &amp;lt;REDACTED&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;/bin/ash&apos;&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;-ecx&apos;&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &quot;/istio&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;traffic&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;shifter \
              &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;virtualService vs \
              &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;targetService new&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;server \
              &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;percentage 10&quot;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&quot;파이프라인-만들기-helm-spinnaker&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8-%EB%A7%8C%EB%93%A4%EA%B8%B0-helm-spinnaker&quot; aria-label=&quot;파이프라인 만들기 helm spinnaker permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;파이프라인 만들기; Helm, Spinnaker&lt;/h3&gt;
&lt;h4 id=&quot;helm&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#helm&quot; aria-label=&quot;helm permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Helm&lt;/h4&gt;
&lt;p&gt;게임 서버와 관련된 Kubernetes 리소스는 모두 Helm Chart로 관리하고 있습니다. 즉 게임을 서비스하기 위한 백엔드 컴포넌트는 차트 하나를 설치함으로써 프로비저닝이 가능합니다. 이를 이용하여 Blue와 Green를 쉽게 동시에 공존하게 만들 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;
├── Chart.yaml
├── charts
│   ├── couchbase-0.1.2.tgz
│   ├── devplay-0.2.0.tgz
│   ├── mysql-0.13.1.tgz
│   └── redis-4.2.10.tgz
├── requirements.lock
├── requirements.yaml
├── templates
│   ├── game
│   │   ├── deployment.yaml
│   │   ├── ingress.yaml
│   │   &lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.
│   │   └── service.yaml
│   └── optool
│       ├── deployment.yaml
│       ├── ingress.yaml
│       &lt;span class=&quot;token punctuation&quot;&gt;..&lt;/span&gt;.
│       └── service.yaml
├── test_values.yaml
└── values.yaml&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&quot;spinnaker&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#spinnaker&quot; aria-label=&quot;spinnaker permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Spinnaker&lt;/h4&gt;
&lt;p&gt;위에서 언급한 일련의 과정을 직접 사람이 한다면 배포의 리스크가 크게 줄지 않을 것입니다. 배포 시간은 오히려 더 오래 걸릴테고, 아무리 체크리스트를 만든다해도 사람이 하는 이상 휴먼 에러가 발생할 수 밖에 없습니다. 또한, 개발사에게 파이프라인을 제공할 수도 없습니다.&lt;/p&gt;
&lt;p&gt;저희는 배포 파이프라인을 위해 Netflix에서 만들고 Google이 확장한 오픈소스 프로젝트인 &lt;a href=&quot;https://spinnaker.io/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Spinnaker&lt;/a&gt;를 도입했습니다. Spinnaker에서는 Amazon Web Services, Azure 등 여러 Cloud Provider를 지원하는데, 저희는 Kubernetes v2를 Provider로 선택했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1000px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 15.833333333333332%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAADCAYAAACTWi8uAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAjUlEQVR42oWNuw7CMBRD8/8fVAEbquiGBGVgKQxQJJohIZDQ5nFjkiwdOmDpyB4smwkhoJRC1ttI1IcK9bFC067RnDbYtSvsz1v0/Ir7cCn0fOY2dODyAUcRH+fBvt5jTBARnHdpVEGPCtZPcMEW98Hhn2IipFEmtMFTvqCNgZ3sspUtxnK4INJcyokIPyRG5nCvkENrAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/ca13092448266c12eb8a57c13687a00e/33ecc/3-spinnaker-overview.webp 360w,
/static/ca13092448266c12eb8a57c13687a00e/263a4/3-spinnaker-overview.webp 480w,
/static/ca13092448266c12eb8a57c13687a00e/0c8fb/3-spinnaker-overview.webp 640w,
/static/ca13092448266c12eb8a57c13687a00e/521f7/3-spinnaker-overview.webp 860w,
/static/ca13092448266c12eb8a57c13687a00e/80c60/3-spinnaker-overview.webp 1000w&quot;
              sizes=&quot;(max-width: 1000px) 100vw, 1000px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/ca13092448266c12eb8a57c13687a00e/53918/3-spinnaker-overview.png 360w,
/static/ca13092448266c12eb8a57c13687a00e/9aebd/3-spinnaker-overview.png 480w,
/static/ca13092448266c12eb8a57c13687a00e/0f09e/3-spinnaker-overview.png 640w,
/static/ca13092448266c12eb8a57c13687a00e/3e3fe/3-spinnaker-overview.png 860w,
/static/ca13092448266c12eb8a57c13687a00e/1263b/3-spinnaker-overview.png 1000w&quot;
            sizes=&quot;(max-width: 1000px) 100vw, 1000px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/ca13092448266c12eb8a57c13687a00e/1263b/3-spinnaker-overview.png&quot;
            alt=&quot;Spinnaker Pipeline Overview&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Spinnaker Pipeline Overview&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;또한, Spinnaker에서는 &lt;em&gt;Manual Judgement&lt;/em&gt;라는 Stage를 지원합니다. Canary를 띄운 후 집중 모니터링하고 디버깅하는 동안 해당 Stage를 활용하여 파이프라인을 펜딩할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 419px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 45.27777777777778%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAAAsTAAALEwEAmpwYAAACJUlEQVR42n2SO28TURCF968ggRD/IAUlDRINKAQIAolHRYN4dBEJ0NJAAwghFBIQjXGC8lAcIMFAQuKQxl78CH5he+NH7LXxrtfP9cdcAwUNVzoa3Zm7Z86ZWa3RaFCr1ahWq9Tr9QFUTkWVU7Vms0mv16Pb7f6DdrtNq9UaRAXbttF6UshkMmwGAgSDQba3t8nlciQSCXRdJ5VKYVkW6vT7ff53lBDNshvk83li0SghIVTkrusOHnRUZ1HgOM6guyKuC5Rq5UBF848z5UJBSxTLGIbB92SShKgp7e3x0xK78lgPh4nu7GDsGlQkX5DGxUKBvJGjXCpJNDDEjcrbtvWbMFmqUFFFUVYpFskJsRGPU5EPjXSakpA5YqWrFIvyjth23D5216XZcwf5ttx7fy3HrQalyWdEjh0lNjpC/MJ50tevsnP2NNEzI6RPneCzZ4ZDU8vsf+jl4JM5Dk/7OPLqHUPPlzjw+A37Hs0yNOXDMGtohtPBnJ/nx42bZMcnyN65S/7+A7ITt8ncGqcwNkbo0wbn/DGGZ9Y46f3A5YUvXFna5OLCOsPejxz3+Lm0uEnerKLFUkHCWZ3wbpSwESGyGyEkuYjcQ5lv6Nkwya1FqoEZGpkotNp07TotmZkjC2lWzQEcU0ZXLqN5l6d5H/Ayt/qCuZWXvN3w8HppktWvs/jWPKxszbL+9Br+e6MUdf9gVqZs3JXtt9odLJmbrf4CgVrKL5mqe9DOTgnWAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/3628b31c56ed0666a3ce676dddd18e4b/33ecc/4-spinnaker-manual-judgement.webp 360w,
/static/3628b31c56ed0666a3ce676dddd18e4b/82ebf/4-spinnaker-manual-judgement.webp 419w&quot;
              sizes=&quot;(max-width: 419px) 100vw, 419px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/3628b31c56ed0666a3ce676dddd18e4b/53918/4-spinnaker-manual-judgement.png 360w,
/static/3628b31c56ed0666a3ce676dddd18e4b/96e9b/4-spinnaker-manual-judgement.png 419w&quot;
            sizes=&quot;(max-width: 419px) 100vw, 419px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/3628b31c56ed0666a3ce676dddd18e4b/96e9b/4-spinnaker-manual-judgement.png&quot;
            alt=&quot;Spinnaker Manual Judgement&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Spinnaker Manual Judgement&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;이 사이에 Grafana, Jaeger 등을 이용하여 의도한 대로 동작하는 지 확인합니다. 이번 사례의 경우 서버가 JVM 위에서 구동되었기 때문에 JMX를 이용하여 더 자세히 디버깅할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 33.611111111111114%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAAAsTAAALEwEAmpwYAAABbklEQVR42iWRWXarMBBE2UZsA2ISk4SEGGLjeIjPy/6XdF8bf/TprkJdXFCkKkuuLdP3naLxpNqh9EBaB9JmorYr0/UP7a6k7YISv+ktrXUMYWY+X7Bj2HVZNURZ7clrhw1X8mYkawNKuupk2W5UZsGuL0p/J7NXMvF76+l92APH5SyBM0Z0pVuiuDBk1fAJlPC4dKSlEIqX6FG8ERN+5EUzcRX2L2haQ90KpfF7WNM7tOgs10S/z4X7bWa7ztwunud9YvYdzg1sW2CZHes8sE6Gyzbhh1a8kb/Xyush9VxlZ+Hf6xtvayKVJuS5EtySIs9469PxyOkUkyYJSinKsiDLUpkTjocD8ftZGu97ZZnvXYk+HL6IQnCMYcSHgBcq5x1t19H1n/lN6r1nsAbj5N/1RggnHreFx13oHmduP5/Z9nIpg9VyqPpUV2KNpsiETIi6LkdXilp/qm0LkvhLqBRhbASgxg1675PoMo/5D15C0ytyETJ/AAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/b463d8d092175c9462f97d607e6d5564/33ecc/9-grafana.webp 360w,
/static/b463d8d092175c9462f97d607e6d5564/263a4/9-grafana.webp 480w,
/static/b463d8d092175c9462f97d607e6d5564/0c8fb/9-grafana.webp 640w,
/static/b463d8d092175c9462f97d607e6d5564/521f7/9-grafana.webp 860w,
/static/b463d8d092175c9462f97d607e6d5564/cbd37/9-grafana.webp 1200w,
/static/b463d8d092175c9462f97d607e6d5564/548e7/9-grafana.webp 2400w,
/static/b463d8d092175c9462f97d607e6d5564/84941/9-grafana.webp 2472w&quot;
              sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/b463d8d092175c9462f97d607e6d5564/53918/9-grafana.png 360w,
/static/b463d8d092175c9462f97d607e6d5564/9aebd/9-grafana.png 480w,
/static/b463d8d092175c9462f97d607e6d5564/0f09e/9-grafana.png 640w,
/static/b463d8d092175c9462f97d607e6d5564/3e3fe/9-grafana.png 860w,
/static/b463d8d092175c9462f97d607e6d5564/64756/9-grafana.png 1200w,
/static/b463d8d092175c9462f97d607e6d5564/98e2c/9-grafana.png 2400w,
/static/b463d8d092175c9462f97d607e6d5564/912ae/9-grafana.png 2472w&quot;
            sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/b463d8d092175c9462f97d607e6d5564/98e2c/9-grafana.png&quot;
            alt=&quot;Grafana&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Grafana&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1905px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 63.055555555555564%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAAAsTAAALEwEAmpwYAAABSElEQVR42p1T2W7DMAzz/3/mhjWn7zOnRjkJOhR7aGOACC0FDCk7Yts22vedNgCk8mVd6/NT8BIsxPQSrfujd3zkXE/2XPvfPvgKcyKlRBscMZZ5pnmaaF2WCuZcY76c+6vH9ZIzTaVUnmJEfybx1XX0Mwz03bbUSEkaDXUDMgTyEBcs9Oh7aiE2GkPa+1sYnSMDUeFAIqvHRAkRyg1MgIGohVOhlCZnTS1EiOYYKKVIPNt3kQF1OWykok4baq2jwQdS4RDL+OrHgjAlpFLkoWzhzlehQgHWeRSfuOQ5WnY44jAiXAWcUALmKZNHU6Gu5AAe3nKor8iPYazEQcyXiQruWsBew7k1qh7YOw6NOx02owRBRET1QC7HifVw2AHMeZ6v+G+GHFu0uNSW86Po+IUcYd9WsV6ONUrETF/BogWpGPy36PPa/AJ18O+77ex5/QAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/103227244e27770232e3218685b3d392/33ecc/10-jaeger.webp 360w,
/static/103227244e27770232e3218685b3d392/263a4/10-jaeger.webp 480w,
/static/103227244e27770232e3218685b3d392/0c8fb/10-jaeger.webp 640w,
/static/103227244e27770232e3218685b3d392/521f7/10-jaeger.webp 860w,
/static/103227244e27770232e3218685b3d392/cbd37/10-jaeger.webp 1200w,
/static/103227244e27770232e3218685b3d392/d8644/10-jaeger.webp 1905w&quot;
              sizes=&quot;(max-width: 1905px) 100vw, 1905px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/103227244e27770232e3218685b3d392/53918/10-jaeger.png 360w,
/static/103227244e27770232e3218685b3d392/9aebd/10-jaeger.png 480w,
/static/103227244e27770232e3218685b3d392/0f09e/10-jaeger.png 640w,
/static/103227244e27770232e3218685b3d392/3e3fe/10-jaeger.png 860w,
/static/103227244e27770232e3218685b3d392/64756/10-jaeger.png 1200w,
/static/103227244e27770232e3218685b3d392/7ad9d/10-jaeger.png 1905w&quot;
            sizes=&quot;(max-width: 1905px) 100vw, 1905px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/103227244e27770232e3218685b3d392/7ad9d/10-jaeger.png&quot;
            alt=&quot;Jaeger&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Jaeger&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;또한, 이 모든 과정을 Email, Slack, SMS를 통해서 알람 받을 수 있습니다. 해당 게임사와 저희 팀은 Slack으로 커뮤니케이션하고 있었기에 아래처럼 실제 배포되고 있는 상황을 전달받을 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 615px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 122.22222222222221%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAYCAYAAAD6S912AAAACXBIWXMAAAsTAAALEwEAmpwYAAACpklEQVR42p1Vi3KjMAzk/7+vTaB5NJQ3GAwEyDt7WiVkaKfX3sUzGhuwV9JqZRwvKJDEIdI0g7fyMV9u0HR71P0Bhd0iLwyyLENZlrInxXa7xX6//6s5bmDQtTUKOehHKfw4x9oPsQ4S2G6HytYCZtA0DYwx2O12OBwOPwBGJcIkRRSGKPIctipR1xaN2OV8wvl8xuVywel00vl4PP4coZEIPsJI0k4QyhwEIdq2xeV6lUiOCkTQqRF4tPPp/NjDZ6eSNOazGebzOTzXw+x1huVyiTiOlbu+78FxFQfjPBpHLlmRW0bO4TCaoij0sK2skp9JgRj+M8OhB8/z8PLygq7rPkXDAtCBOrlXmmatVacMhu/ruoaRojJ1h9XjBlZuCsZBXoZhUOu7/rGmIzpnFpz5TGqUQwJWVaXG1EtTqjzo5KmUic5IGO5oJJjv/meMmTn9/oB6EA3dBTtW62v6PwFN9zl10yKIYkQRNRiokexnh8O2Wry9qfZo1ON6vUYYhaqxWoT/VX/TqFhhapbU3SKUF77vKwgBirzQDayedsIvXE7bUgFZXaZLsKmYuYk3C+VAJXDNmY7G9fhtlNw95RKbzQatcEm5kEOKNfgIsFqt4HouXNfF+/s7ZtKiXC8WC82I9JAm7f1phGPKnDVt0SNnOkiSRB2wE/Ls9o3v2b80nuce6pa8OlyEcnXRxgozFXbEd4X4rjCfqkxOeHjKDQvF9VOy2Yuw6eh2v131HnyI9hvx/hphM/RIbYOh294aX+x+8rkIE+MjDmP4clu/vq31J5XLz6k/XNAMR7T9TsRtH5Kx9zW55pry+gSYlhvs+gGZqRBkJZLCIsoMYlnzz0fApqkfmhz5JTiBp72vgJH5QJTKlVXIRWkrjWZglfvuny6Hryn/AbdOP61UcfdLAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/12895e0d3316a4dbf79284301fb3bc33/33ecc/5-spinnaker-slack.webp 360w,
/static/12895e0d3316a4dbf79284301fb3bc33/263a4/5-spinnaker-slack.webp 480w,
/static/12895e0d3316a4dbf79284301fb3bc33/a6108/5-spinnaker-slack.webp 615w&quot;
              sizes=&quot;(max-width: 615px) 100vw, 615px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/12895e0d3316a4dbf79284301fb3bc33/53918/5-spinnaker-slack.png 360w,
/static/12895e0d3316a4dbf79284301fb3bc33/9aebd/5-spinnaker-slack.png 480w,
/static/12895e0d3316a4dbf79284301fb3bc33/8ab79/5-spinnaker-slack.png 615w&quot;
            sizes=&quot;(max-width: 615px) 100vw, 615px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/12895e0d3316a4dbf79284301fb3bc33/8ab79/5-spinnaker-slack.png&quot;
            alt=&quot;Spinnaker Slack Notification&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Spinnaker Slack Notification&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h2 id=&quot;4-before-after&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#4-before-after&quot; aria-label=&quot;4 before after permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;4. Before-After&lt;/h2&gt;
&lt;p&gt;이제 배포 전략이 개선 되었으니 앞서 세웠던 요구 사항을 확인해보겠습니다.&lt;/p&gt;
&lt;h3 id=&quot;구-서버로-rollback이-쉽게-가능해야-한다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B5%AC-%EC%84%9C%EB%B2%84%EB%A1%9C-rollback%EC%9D%B4-%EC%89%BD%EA%B2%8C-%EA%B0%80%EB%8A%A5%ED%95%B4%EC%95%BC-%ED%95%9C%EB%8B%A4&quot; aria-label=&quot;구 서버로 rollback이 쉽게 가능해야 한다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;구 서버로 Rollback이 쉽게 가능해야 한다.&lt;/h3&gt;
&lt;p&gt;마지막 구 서버를 Teardown 하기 전에는 언제든지 위에서 구현해놓은 트래픽 제어 툴을 이용해서 트래픽을 구 서버로 원복할 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;신-서버가-정상-작동하는지-모니터링-및-디버깅-할-수-있어야-한다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8B%A0-%EC%84%9C%EB%B2%84%EA%B0%80-%EC%A0%95%EC%83%81-%EC%9E%91%EB%8F%99%ED%95%98%EB%8A%94%EC%A7%80-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-%EB%B0%8F-%EB%94%94%EB%B2%84%EA%B9%85-%ED%95%A0-%EC%88%98-%EC%9E%88%EC%96%B4%EC%95%BC-%ED%95%9C%EB%8B%A4&quot; aria-label=&quot;신 서버가 정상 작동하는지 모니터링 및 디버깅 할 수 있어야 한다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;신 서버가 정상 작동하는지 모니터링 및 디버깅 할 수 있어야 한다.&lt;/h3&gt;
&lt;p&gt;Grafana, Jaeger를 통해서 일부 트래픽이 흘러간 신 서버에 대해서 모니터링이 가능합니다. 서버의 기본적인 메트릭 뿐만 아니라, Prometheus의 커스텀 메트릭을 통해 새로 추가된 기능 등이 잘 작동하는 지 확인할 수 있습니다. 또한, Jaeger를 통해 서버에 들어오는 요청들을 Tracing 하고, 병목 지점이 없는지 확인할 수 있습니다. 이번 사례의 경우 JMX를 통해 자세한 디버깅도 가능했습니다.&lt;/p&gt;
&lt;h3 id=&quot;신-서버의-warm-up이-가능해야한다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8B%A0-%EC%84%9C%EB%B2%84%EC%9D%98-warm-up%EC%9D%B4-%EA%B0%80%EB%8A%A5%ED%95%B4%EC%95%BC%ED%95%9C%EB%8B%A4&quot; aria-label=&quot;신 서버의 warm up이 가능해야한다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;신 서버의 Warm-up이 가능해야한다.&lt;/h3&gt;
&lt;p&gt;JVM의 경우 충분한 성능을 보장받기 위해서는 Warm-up이 중요합니다. 사실 JVM 뿐만 아니라 일반적인 상황에서도 Warm-up 할 시간이 있다면 서버의 안정성은 충분히 올라갑니다. 개선된 배포 전략은 일부 트래픽을 새로 프로비저닝 된 서버에 부여함으로서 유저 평균적인 레이턴시와 경험을 위협하지 않고 Warm-up이 가능합니다.&lt;/p&gt;
&lt;h3 id=&quot;업데이트에-걸리는-시간이-단축되어야-한다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8%EC%97%90-%EA%B1%B8%EB%A6%AC%EB%8A%94-%EC%8B%9C%EA%B0%84%EC%9D%B4-%EB%8B%A8%EC%B6%95%EB%90%98%EC%96%B4%EC%95%BC-%ED%95%9C%EB%8B%A4&quot; aria-label=&quot;업데이트에 걸리는 시간이 단축되어야 한다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;업데이트에 걸리는 시간이 단축되어야 한다.&lt;/h3&gt;
&lt;p&gt;기존의 Rolling Update 전략의 경우 서비스의 안정성을 위하여 1대씩 천천히 교체해야 했습니다. 그러다보니, 수십대를 교체해야 하는 경우 업데이트하는데만 걸리는 시간이 거의 1시간 이상이 소요되었습니다. 하지만, 이번 배포 전략은 크게 2단계(트래픽 10% 이동, 트래픽 100% 이동)로 업데이트의 걸리는 시간이 거의 1/4로 줄었습니다.&lt;/p&gt;
&lt;h2 id=&quot;5-앞으로-더-나아가야-할-방향은&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#5-%EC%95%9E%EC%9C%BC%EB%A1%9C-%EB%8D%94-%EB%82%98%EC%95%84%EA%B0%80%EC%95%BC-%ED%95%A0-%EB%B0%A9%ED%96%A5%EC%9D%80&quot; aria-label=&quot;5 앞으로 더 나아가야 할 방향은 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;5. 앞으로 더 나아가야 할 방향은&lt;/h2&gt;
&lt;p&gt;이번 개선된 배포 전략을 도입하면서 배포 관련하여 많은 프로세스의 효율성과 안정성이 올라가는 경험을 했습니다. 하지만 아직도 더 자동화할 수 있는 부분이, 개선할 수 있는 부분이 많다고 생각합니다.&lt;/p&gt;
&lt;h3 id=&quot;automated-canary-analysis&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#automated-canary-analysis&quot; aria-label=&quot;automated canary analysis permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Automated Canary Analysis&lt;/h3&gt;
&lt;p&gt;신 서버가 정상 작동하는지 확인 하는 부분은 사람이 관여해야만 하는 작업일까요? 위 파이프라인을 도입 한 이후 경험을 돌이켜보면 암묵적으로 체크리스트를 만들고 해당 기준을 넘어설 때 안정성을 확인하는 것을 느낄 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 787px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 39.72222222222222%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsTAAALEwEAmpwYAAABbUlEQVR42o1SSU4cQRDs//8GPmHhgw8WYDzAMJpeal+71h5QOKfx0bZ8SFVklnKJyBzm6QguZsSgyQyU4jCGwxq5x4LXv/+uWP0Rxx1bRDdiYGyGkBqX7R1h9Xge71FLg7UerW+otaG1vts/cb+gZodhHE8YpxlpzVBO4mH6hpILvHP0ZoQQKKHuVkr5O6bCJRmacJmwMAapiK5c8Pz4Ze+oyF/XhBQ9tq2jt/8sqEkzyQUSBfM8Y7m9Rd02WK2xjDMYUxAmwsf8mUyWSyWqDb1To36lXtFJspIthvvXF9w9HPDENQ6HVzze3GC9JuZEmqSduuASkglqLCGY3JsoZeGtg7GBFuhosZb+zhjeSL+XkUP4glkqfP/xlabd4P1KXT9omkZLuk5yIT0T0SNqFJPCwGi/v1ZbKroiOIUhuRN64miJoZLltCAHOiN3Jp/vpxD9SPrQOS0/oeURWp9h1RG9CORICw0TYYUST/gFalhfd6Cq2vUAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/4f678ebeb4195cbfd2290e253b603893/33ecc/8-spinnaker-automated-analysis.webp 360w,
/static/4f678ebeb4195cbfd2290e253b603893/263a4/8-spinnaker-automated-analysis.webp 480w,
/static/4f678ebeb4195cbfd2290e253b603893/0c8fb/8-spinnaker-automated-analysis.webp 640w,
/static/4f678ebeb4195cbfd2290e253b603893/68152/8-spinnaker-automated-analysis.webp 787w&quot;
              sizes=&quot;(max-width: 787px) 100vw, 787px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/4f678ebeb4195cbfd2290e253b603893/53918/8-spinnaker-automated-analysis.png 360w,
/static/4f678ebeb4195cbfd2290e253b603893/9aebd/8-spinnaker-automated-analysis.png 480w,
/static/4f678ebeb4195cbfd2290e253b603893/0f09e/8-spinnaker-automated-analysis.png 640w,
/static/4f678ebeb4195cbfd2290e253b603893/19dcc/8-spinnaker-automated-analysis.png 787w&quot;
            sizes=&quot;(max-width: 787px) 100vw, 787px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/4f678ebeb4195cbfd2290e253b603893/19dcc/8-spinnaker-automated-analysis.png&quot;
            alt=&quot;Spinnaker Automated Canary Analysis&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Spinnaker Automated Canary Analysis&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;그래서 이 기준을 여러 메트릭에 걸쳐 명시적으로 정의하고, 해당 조건을 충족하면 다음 과정을 진행하도록 하여 Manual Judgement 부분을 자동화할 수 있습니다. 이렇게 되면 여러 관리자가 해당 파이프라인에 관여할 때 서로 다른 기준들을 일원화 할 수 있고, 실수 또한 줄일 수 있어 안정성을 증대시킬 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;argo&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#argo&quot; aria-label=&quot;argo permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Argo&lt;/h3&gt;
&lt;p&gt;가장 최근에 개념증명(PoC)를 진행했던 프로젝트는 CNCF Foundation에서 incubating project로 관리되고 있는 &lt;a href=&quot;https://argoproj.github.io/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Argo&lt;/a&gt;입니다. 특히 이 프로젝트에서 Argo-rollouts이 Blue-Green, Canary 배포 전략을 지원합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1298px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 63.888888888888886%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAABYlAAAWJQFJUiTwAAABkElEQVR42pVTW47cIBDkBOMXYBs/x2CwFW/2J/e/WsXVE080Una0+Wg1NFBdVYDa9x1930PrCnmeY5omhBAwjiPqupZaURTfDuW9x7IsKMsSeZFjGAewZoz5bzABHIZXgPv9/sKM+SWy900UJaaURCaBmNu2RVVV0oRhrX2MrYZttaj5EnDbNmGotZYCwZxzAs5x0zQyZ66dgZvqs1n5XjJBeZgFssmy7EXmc559U3KMURhRCvNb48t/1y/PFZl1XfcE0kZ/eVhewp+D9PiqlacF11zJrdoa1liJ7PZXGsdXdza8nhcPe788/Wa0jZN9iuxC8Ih7gI93WFedoVEPBs2ghSXBecv0Wz6BqdD1Tsa8rOr8FLbTZy6hiryA6xzGuce6eaQfEdvxiM9fhzwpvgIqmedZLs02BmsK4j1/VNM22D8S5mWGIk1S7/sBwQesYUVc05kjfn584jgOeQWUS4a32w2VKbHGx14BPFmmmKShoh/0hz5wkbfOhetPE4h1yuMeEqC0cXrspWVkTQXc8xsIe0pHX+icggAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/d11d2791151f633ce6cf52babe5bcbbd/33ecc/6-argo-rollout.webp 360w,
/static/d11d2791151f633ce6cf52babe5bcbbd/263a4/6-argo-rollout.webp 480w,
/static/d11d2791151f633ce6cf52babe5bcbbd/0c8fb/6-argo-rollout.webp 640w,
/static/d11d2791151f633ce6cf52babe5bcbbd/521f7/6-argo-rollout.webp 860w,
/static/d11d2791151f633ce6cf52babe5bcbbd/cbd37/6-argo-rollout.webp 1200w,
/static/d11d2791151f633ce6cf52babe5bcbbd/6d1d1/6-argo-rollout.webp 1298w&quot;
              sizes=&quot;(max-width: 1298px) 100vw, 1298px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/d11d2791151f633ce6cf52babe5bcbbd/53918/6-argo-rollout.png 360w,
/static/d11d2791151f633ce6cf52babe5bcbbd/9aebd/6-argo-rollout.png 480w,
/static/d11d2791151f633ce6cf52babe5bcbbd/0f09e/6-argo-rollout.png 640w,
/static/d11d2791151f633ce6cf52babe5bcbbd/3e3fe/6-argo-rollout.png 860w,
/static/d11d2791151f633ce6cf52babe5bcbbd/64756/6-argo-rollout.png 1200w,
/static/d11d2791151f633ce6cf52babe5bcbbd/11824/6-argo-rollout.png 1298w&quot;
            sizes=&quot;(max-width: 1298px) 100vw, 1298px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/d11d2791151f633ce6cf52babe5bcbbd/11824/6-argo-rollout.png&quot;
            alt=&quot;Argo Rollouts&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Argo Rollouts&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;특히 CRD로 관리되기 때문에 Kubernetes와 궁합이 잘 맞습니다. 잘 적용할 수 있다면 Spinnaker에서의 매우 긴 Pipeline을 정리하고 간단명료하게 매니페스트로 리소스를 관리할 수 있습니다. 그리고 Nginx와 같은 Ingress Controller, Istio와 같은 Service Mesh Provider와의 Integration 역시 기본적으로 지원하는 점도 도입하는 장벽을 낮춥니다. 그리고 Golang으로 구성되어 있어 Kubernetes와 같은 언어 베이스를 갖는 것도 장점으로 볼 수 있습니다. 또한, Analysis 기능을 지원하여 위에서 언급한 &lt;em&gt;Automated Canary Analysis&lt;/em&gt;도 달성할 수 있습니다.&lt;/p&gt;
&lt;h3 id=&quot;clutch&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#clutch&quot; aria-label=&quot;clutch permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Clutch&lt;/h3&gt;
&lt;p&gt;운영을 하다보면 많은 기술적 요청을 받게 되는데, 이는 회사 혹은 프로젝트마다 구성된 아키텍쳐와 스택마다 상이하기 때문에 비정형적인 업무를 많이 하게 됩니다. 그리고 개발자 뿐만 아니라 PM, QA, 기획 같은 비개발자 분들과도 많이 협업하게 됩니다. 이러한 요청을 매번 인프라 팀에서 매뉴얼하게 처리하면 일의 효율이 떨어지기 때문에, 저희는 백오피스 툴을 개발하고 있습니다.&lt;/p&gt;
&lt;p&gt;개발 진행 중에서 연구한 툴이자 프레임워크가 Lyft에서 공개한 &lt;a href=&quot;https://clutch.sh/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Clutch&lt;/a&gt;입니다. 분산되어 있는 인프라의 컨트롤 타워 같은 셈입니다. 자주하고 필요로 하는 업무들을 Task 단위로 정규화하고, 이를 UI를 통해서 수행할 수 있도록 자동화 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1418px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 55.27777777777778%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAACCElEQVR42mOIj0v0CwoLDI7y9g328AxwcvZ0dPJwdPZ0QkceDk4e9o4ezq6+Lm5+VnYu/qHxDHZeobouAdauAQbWbnqWLjrmzroWLioGNrJaZnLa5kASxIAhWU1TTVNHfSs3E2fvgPgMhmXrty/btGPXvv37jp09ePwcCJ04t/fI6Z2HTuw5cmr/sbP7j53Zd/T03qNn9h8FMfYcPrnr4Inj5y7vOnCC4en9W//////3799/MPUfBoAiv3/+/PPnz7//CEEI+Pv3L5C8duUqw4Pr5xev3T51ydqpa7ZO3rJ3w74jMxatmr5qc+/qLbN3HFy5YfvEeSumrNzctmrLxkMnZy9aPWXZhu7lGxYfOn3o0GGGm5fOqMaX8EbmcWbW8lT3+7dPEwnOEEytZMiq1+6Y5V7czBWWxZdZy5DfGjF5oVpsvnBKOUNqlVb/ol37DzLcunKucevB3LU7s7cdKjh8bsbxc0WrtuVv3pe282jLqSsTdh3OWr09f/uhjH2n5565Wr56W9GW/albD7Vfvnvo6HGG+zcug3wI9gaR4O+fP0Dy/JnTDPeuXwSyfv369RsI/vz5jQB/gFyw6O9fv4GMP3DZHz9+ArWcPXWK4emdq+CwJdnms6dOM5zYvvzSqaMXjh84tG8vMjqwb+/W7Tu2bd6yd/fu/Xv3QtEeCNpzYN/+6ZMmAwBjq8m2TbU2hQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/e9e30215792ed693b54d648741e6352e/33ecc/7-clutch.webp 360w,
/static/e9e30215792ed693b54d648741e6352e/263a4/7-clutch.webp 480w,
/static/e9e30215792ed693b54d648741e6352e/0c8fb/7-clutch.webp 640w,
/static/e9e30215792ed693b54d648741e6352e/521f7/7-clutch.webp 860w,
/static/e9e30215792ed693b54d648741e6352e/cbd37/7-clutch.webp 1200w,
/static/e9e30215792ed693b54d648741e6352e/fdbfa/7-clutch.webp 1418w&quot;
              sizes=&quot;(max-width: 1418px) 100vw, 1418px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/e9e30215792ed693b54d648741e6352e/53918/7-clutch.png 360w,
/static/e9e30215792ed693b54d648741e6352e/9aebd/7-clutch.png 480w,
/static/e9e30215792ed693b54d648741e6352e/0f09e/7-clutch.png 640w,
/static/e9e30215792ed693b54d648741e6352e/3e3fe/7-clutch.png 860w,
/static/e9e30215792ed693b54d648741e6352e/64756/7-clutch.png 1200w,
/static/e9e30215792ed693b54d648741e6352e/f967b/7-clutch.png 1418w&quot;
            sizes=&quot;(max-width: 1418px) 100vw, 1418px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/e9e30215792ed693b54d648741e6352e/f967b/7-clutch.png&quot;
            alt=&quot;Clutch&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Clutch&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h2 id=&quot;6-마치며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#6-%EB%A7%88%EC%B9%98%EB%A9%B0&quot; aria-label=&quot;6 마치며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;6. 마치며&lt;/h2&gt;
&lt;p&gt;이번 도입기를 거치며 다시 한번 우리 팀의 철학이자 존재 이유를 되새길 수 있었습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;동료들이 편하고 행복하게 일하도록 만들자!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;이 배포 방식이 적용된 이후로 배포 완료 후 핫픽스를 하는 케이스가 많이 줄었고, 배포를 위해 새벽까지 남아 있는 일이 많이 사라졌습니다. 그리고 모두가 배포에 대해 신뢰성이 높아져 공격적인 업데이트의 자신감도 많이 높아졌습니다.&lt;/p&gt;
&lt;p&gt;혹시 배포 때문에 잠 못 이루는 분들이 계시다면, 한 번쯤 도입을 고려해보시길 권해드립니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[EKS에서 쿠버네티스 포드의 IAM 권한 제어하기: Pod Identity Webhook]]></title><description><![CDATA[EKS에서 포드의 IAM 권한을 개별적으로 부여할 수 있는 Pod Identity Webhook의 사용 방법과 동작 원리를 설명합니다.]]></description><link>https://tech.devsisters.com/posts/pod-iam-role</link><guid isPermaLink="false">https://tech.devsisters.com/posts/pod-iam-role</guid><pubDate>Tue, 02 Jun 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 200px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 100%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAAAsTAAALEwEAmpwYAAAEq0lEQVR42mWUbUxTZxTH+bJPS5Ysy5a5zJjMJcsyE7N9cIogvhQFocBURKtM0DgVFUFEVF5bwNZSmJm4ORAUUJho0OEUFQVF8S2dMBUL5aVAGQFKC0jBUgq/3RbpyvYkJ/fe3Ht+z/8859y/G2/X5OQkNpuN/PwzpMuSuXKxGGXeNVR55Vy9dI70NBnx8QlkZ2ej0WgcORMTE4481+Xm+iBLTaPkfCEvuqysOdXPfGk381K6ibk4yODoJAMmI/dqagRwPCqVipGRESfYCZx+sO+ck3vace93rBV3WSu/3DZwtLyH2dFaYoraYXyYN2PjjI+PU1JSQlRUFD09Pc4KnQrVajVSmcy+Fw26fuYebOHYH31U1A9R+WKIbXndrFDqsIwMYTSZMBpNjuQaQW1kZCRms3lmyVlZWaj/rMNmtTiAn8W1sKeom9LHRiEGEB/Xs1SuY/i1UPrgVBgMBgegrKwMuVzuVOkAZmZm0qVvx2odA5uZnCoDs/bpWJbRhZeik08im7nX0I9ZABr6TfQbp6K3t0/IGUcqlVFbW/uvwt+E80hWnOS5pk0AdzBk0HP6hpa9+Y1E5LXySNMrfDWMzSKUNv5GOBmLIMfCpP0qrOZmraNJTuDIaxPuARHM313FqrhK1qfc5MCJKtLz75Fx9i6ynEqkudUo8u6QkV+JPK8SWe4d0nNvcbywikMpKjJVGTO7XKd+yteeQXyxOpEvN+Yxd0MBc4LzmR2YzWw/FZ+ulPGxVywffbuDd+dtwm2OmA++CuKd9+excJEXer1+CjjdbvtMbZEEszN8M6EbQ1gbGMAaIdYFBSAJXkPYphB2CO/27Qhjz5YQIdYT5OvN8s/fo76yeGoehR/DCZQr5IRKQjiakoAs4SBpiYdIjY8lJS6KIwciiY3cReTunYRt3YpYHIi3x0L2rl5AnSqc+oIj2FzHZmzMSuhmCf7eXqxbLUIsWoKPaCn+AYLC4PVI1gYSvjmEiA1iYr3mUHg4jIabxZifXKK/TMGDtLVYLKMzSzb1dnFp1yJuS0Oplm2iLtGP5z9u51VRKq2lSnrKlJhLExj4XcnQ/fMM3DpFc1Yo2sNLqJUGMjo6/Fbh9Bmah3icGoDpJwl1T4Tult2g+mIOHSe20ZHmx6uKAn44W8Gs5FIUV6oxHAvgZbyI5hQfHiklWMbezDQHe69rT+7lb6mIny9fx21XMYtyH9JxPpm2hGWor53jw/QbuEWXUHztOj3K72iU+vPy0BLUBQnTljUFnJyYOtLmR+U8jV6A7kwchSWF3C1SoUsTo0nwplO5jvqrv1JVXkhn9nYaE71pSw/kSawH7XV3pnjCCM6wL7sfPjgRQUPMAvoUYnTxywUVvmjlYhqF0nTRHnSHfoP24DJaBNjLOA8eZkcw4eKJbq4Ga1+Dhi5qFBKexbqj2y+iM9yT9rDFtH/vTluoO9oYEVpBdVPSSh4cDcHYo/u/ff0XOjzYR/0FObWJq6jb74nmwAqajqxEm+4vwPxpSRXzPM6TZxcUb/NcDNZulq5htVodE28T2MZePU8vH6di/2L+SlpFY5IvmiQfNMm+NCX7cF/YpLezCbsMe549/x9L1dtNiTc3tgAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/725f9fa72f3018240ed37c1b4d9bb4ed/a5e6d/k8s-brave-cookie.webp 200w&quot;
              sizes=&quot;(max-width: 200px) 100vw, 200px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/725f9fa72f3018240ed37c1b4d9bb4ed/56d15/k8s-brave-cookie.png 200w&quot;
            sizes=&quot;(max-width: 200px) 100vw, 200px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/725f9fa72f3018240ed37c1b4d9bb4ed/56d15/k8s-brave-cookie.png&quot;
            alt=&quot;쿠버네티스 모자를 쓰고 있는 용감한 쿠키&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;쿠버네티스 모자를 쓰고 있는 용감한 쿠키&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;데브시스터즈에서는 여러 개의 쿠버네티스 클러스터를 구축해 게임 개발 및 운영, 데이터 분석, 로깅 등을 위한 인프라로서 활용하고 있습니다. 각 클러스터에서는 쿠버네티스의 기능을 확장하기 위해 약 20여개 이상의 애드온을 운영하고 있는데, 이번 글에서는 AWS EKS에서 포드(Pod)의 IAM 권한을 세밀하게 제어할 수 있는 Pod Identity Webhook 애드온의 동작 원리와 사용 방법을 설명해보려 합니다.&lt;/p&gt;
&lt;h2 id=&quot;aws에서-쿠버네티스-포드의-iam-권한-제어&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#aws%EC%97%90%EC%84%9C-%EC%BF%A0%EB%B2%84%EB%84%A4%ED%8B%B0%EC%8A%A4-%ED%8F%AC%EB%93%9C%EC%9D%98-iam-%EA%B6%8C%ED%95%9C-%EC%A0%9C%EC%96%B4&quot; aria-label=&quot;aws에서 쿠버네티스 포드의 iam 권한 제어 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;AWS에서 쿠버네티스 포드의 IAM 권한 제어&lt;/h2&gt;
&lt;p&gt;AWS EC2에서 인프라를 운영한다면 EC2 인스턴스 하나에 IAM 역할(Role) 하나를 대응시켜 해당 인스턴스에게 특정 AWS 권한을 부여할 수 있습니다. 하지만 EC2 인스턴스 상에서 쿠버네티스를 운영한다면 하나의 인스턴스에 여러 개의 포드 또는 컨테이너가 실행될 수 있으며, 이는 곧 해당 인스턴스에 부여한 IAM 역할을 인스턴스 상의 모든 포드들이 동일하게 사용할 수 있다는 것을 의미합니다. 각 포드의 권한들이 집중되어 있는 하나의 IAM 역할을 워커 그룹 내의 모든 포드가 공유하는 것은 보안적으로 바람직하지 않습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1147px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 41.11111111111111%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsTAAALEwEAmpwYAAABbUlEQVR42k1RCZKjMAzk/4/a+cHWvmCmkpBAwIAv5APjHlnZOVzlkkpqt7rl7u1fwGYjck6IMSCmFqPcEDOOHKFMwZ+/JJjQMPEH88IFibVWdNdnQjJ31LjgPLhoLyiloHAO+4FMFiEdWA33zDtwEM6wAe6GswIlGs6viIFQzhNdygei7nH4GTgzgnpH5UYtnOs7UnCsLIs6v1wQds0kDlZdhODMBKOuIPI4GyFRgHMOficEIm5w5GnNvvMe/fABtU5SX7YV4zyxmoBpmfmdFdw4j1jXFWDFQki7R3YKRm/S0FpLpP8DMu9155jIsZMV67bBGCO4jfPmIFB8KWzLLEcGosLC0299j2ma8Hg8kJiIwo6dB8aUUY8AkOLegGEYMI4jxmGUnWu7MT6ia+zNsvNOVLwsB7mtPjx7GKtFhecVtBpxj35hrTV4zncZ3rWvttYKsMmf51nyL9uN5Ou0vGGXZYFS6ttyw7dTUfEJUKposYA4VdQAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/c36857fd1ba149de50a6a62189d18033/33ecc/pod-sharing-iam-with-ec2.webp 360w,
/static/c36857fd1ba149de50a6a62189d18033/263a4/pod-sharing-iam-with-ec2.webp 480w,
/static/c36857fd1ba149de50a6a62189d18033/0c8fb/pod-sharing-iam-with-ec2.webp 640w,
/static/c36857fd1ba149de50a6a62189d18033/521f7/pod-sharing-iam-with-ec2.webp 860w,
/static/c36857fd1ba149de50a6a62189d18033/b4f90/pod-sharing-iam-with-ec2.webp 1147w&quot;
              sizes=&quot;(max-width: 1147px) 100vw, 1147px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/c36857fd1ba149de50a6a62189d18033/53918/pod-sharing-iam-with-ec2.png 360w,
/static/c36857fd1ba149de50a6a62189d18033/9aebd/pod-sharing-iam-with-ec2.png 480w,
/static/c36857fd1ba149de50a6a62189d18033/0f09e/pod-sharing-iam-with-ec2.png 640w,
/static/c36857fd1ba149de50a6a62189d18033/3e3fe/pod-sharing-iam-with-ec2.png 860w,
/static/c36857fd1ba149de50a6a62189d18033/c17af/pod-sharing-iam-with-ec2.png 1147w&quot;
            sizes=&quot;(max-width: 1147px) 100vw, 1147px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/c36857fd1ba149de50a6a62189d18033/c17af/pod-sharing-iam-with-ec2.png&quot;
            alt=&quot;포드 별로 IAM 권한을 분리하는 경우와 그렇지 않은 경우&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;포드 별로 IAM 권한을 분리하는 경우와 그렇지 않은 경우&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;이러한 상황을 방지하기 위해서는 각 포드마다 애플리케이션에 맞는 최소한의 IAM 역할만을 부여하되, 인스턴스의 IAM 역할과는 분리되어 있어야 합니다. 하지만 쿠버네티스가 자체적으로 이러한 기능을 제공하지는 않으며, kube2iam, kiam, Pod Identity Webhook 등의 애드온을 추가적으로 사용해야만 합니다.&lt;/p&gt;
&lt;h1 id=&quot;kube2iam과-kiam을-통한-포드의-iam-권한-제어&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#kube2iam%EA%B3%BC-kiam%EC%9D%84-%ED%86%B5%ED%95%9C-%ED%8F%AC%EB%93%9C%EC%9D%98-iam-%EA%B6%8C%ED%95%9C-%EC%A0%9C%EC%96%B4&quot; aria-label=&quot;kube2iam과 kiam을 통한 포드의 iam 권한 제어 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;kube2iam과 kiam을 통한 포드의 IAM 권한 제어&lt;/h1&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/jtblin/kube2iam&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;kube2iam&lt;/a&gt;과 &lt;a href=&quot;https://github.com/uswitch/kiam&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;kiam&lt;/a&gt;은 포드에 IAM 역할을 부여할 수 있는 쿠버네티스 애드온입니다. 정확히 말하자면, 포드가 특정 IAM 역할을 사용하도록 Assume Role을 대신 수행하는 역할을 담당합니다. 원래대로라면 포드는 인스턴스의 IAM 역할을 그대로 물려받아야 하지만, kube2iam과 kiam을 사용하면 포드가 특정 IAM 역할인 것처럼 변장(Assume)시켜주는 것이 가능합니다.&lt;/p&gt;
&lt;p&gt;이 때, kube2iam과 kiam은 다른 포드에 부여될 IAM 역할을 Assume할 수 있어야 하므로 인스턴스에 부여된 IAM 역할을 통해 Assume Role 권한을 사용할 수 있어야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1056px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 33.611111111111114%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAHCAYAAAAIy204AAAACXBIWXMAAAsTAAALEwEAmpwYAAABtElEQVR42mMIXRXKbBFqwakVqsXGQDxgCkzRlVFR4RE1NpbkQpHxS1LntfZRklNSEuQH8etn/udKSzvDCmQy/v/PwAiiQ0NDmVetWsX8//9/xtyJ//j4+eUF+PnZleTk+AXr6xmYQPL19fVMIHmQGZJALMigBXYhY0zFDhW30F4he3kGDgkJHlEPMyE+kDgQs4AUp3RfUAwNLeS0Embg1ZLhE8r1UGEHCjNDMQSYakgJg1wIsg3I5VgF9P7EXAb2TqDrQd67fna/ya1bd3pu3rxZoqXFwHYrV4V9po8k15wkK5A847UzhwOvX7859dq1yxEMxkpKIK8ynjy/0+Du/WsVJ8+e9Tg/P1zh5p7p0peXlyif2VQvcmpDu+Gebdv09uw5Lq7FwMB2cX2T7qU9E8Qvbu9UvwGUP711uvXBM2ckX79+DbIA4tQrV66wvX//XmD+/PkcTR01ums3r9DZu3eb8pa9W+T3H92hsWXLFgmoh1gnTe8127Nns/TevXvlz984bX391tXZt+5e88EWe6zg8GRggIUbA5RmhETSfxibCaqO197eXuDWrVvsjx494gSpBgAjyZWPdkFNDQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/d1ee40b9e1479b1a10bf34b7cf92563d/33ecc/kiam-vs-kube2iam.webp 360w,
/static/d1ee40b9e1479b1a10bf34b7cf92563d/263a4/kiam-vs-kube2iam.webp 480w,
/static/d1ee40b9e1479b1a10bf34b7cf92563d/0c8fb/kiam-vs-kube2iam.webp 640w,
/static/d1ee40b9e1479b1a10bf34b7cf92563d/521f7/kiam-vs-kube2iam.webp 860w,
/static/d1ee40b9e1479b1a10bf34b7cf92563d/dca56/kiam-vs-kube2iam.webp 1056w&quot;
              sizes=&quot;(max-width: 1056px) 100vw, 1056px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/d1ee40b9e1479b1a10bf34b7cf92563d/53918/kiam-vs-kube2iam.png 360w,
/static/d1ee40b9e1479b1a10bf34b7cf92563d/9aebd/kiam-vs-kube2iam.png 480w,
/static/d1ee40b9e1479b1a10bf34b7cf92563d/0f09e/kiam-vs-kube2iam.png 640w,
/static/d1ee40b9e1479b1a10bf34b7cf92563d/3e3fe/kiam-vs-kube2iam.png 860w,
/static/d1ee40b9e1479b1a10bf34b7cf92563d/cb866/kiam-vs-kube2iam.png 1056w&quot;
            sizes=&quot;(max-width: 1056px) 100vw, 1056px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/d1ee40b9e1479b1a10bf34b7cf92563d/cb866/kiam-vs-kube2iam.png&quot;
            alt=&quot;kiam과 kube2iam의 동작 원리&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;kiam과 kube2iam의 동작 원리&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;kube2iam과 kiam의 동작 원리는 매우 간단합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;EC2 인스턴스 메타데이터 API를 호출할 때 사용되는 주소인 169.254.169.254로 향하는 트래픽을 kube2iam이나 kiam으로 라우팅하도록 iptables DNAT 규칙을 워커 호스트에 추가합니다. 이 작업은 보통 애드온이 자동으로 수행합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 900px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 12.777777777777777%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAADCAYAAACTWi8uAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAzUlEQVR42h2LUUuDUACF1Xvd6GH2ZMthaegltDsbjtQ2i1pjRewpepBBtI36/z/hy+7D4YPvnGOlaULT1NwWBVprlEoJgnNc10VKge/7jMdneJ6HFALbtrEsy1BKieid4zgmppvNSvS0IIqu+mPAZBKSZTfEccJgeMJodEqSKMr5HZdRjHSHCDkgDC9o7hdm+x+lrslzjVXWO6rljmn5SbXoWL/9ULdfpNmWNN/StB1P6wOrzZFi/mFctexYvR55fDnQPn/33PPQc/P+yx905lc6F0LFgAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/32ce48434dea04a27e254d183d5bb122/33ecc/aws-metadata-iptables.webp 360w,
/static/32ce48434dea04a27e254d183d5bb122/263a4/aws-metadata-iptables.webp 480w,
/static/32ce48434dea04a27e254d183d5bb122/0c8fb/aws-metadata-iptables.webp 640w,
/static/32ce48434dea04a27e254d183d5bb122/521f7/aws-metadata-iptables.webp 860w,
/static/32ce48434dea04a27e254d183d5bb122/d0742/aws-metadata-iptables.webp 900w&quot;
              sizes=&quot;(max-width: 900px) 100vw, 900px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/32ce48434dea04a27e254d183d5bb122/53918/aws-metadata-iptables.png 360w,
/static/32ce48434dea04a27e254d183d5bb122/9aebd/aws-metadata-iptables.png 480w,
/static/32ce48434dea04a27e254d183d5bb122/0f09e/aws-metadata-iptables.png 640w,
/static/32ce48434dea04a27e254d183d5bb122/3e3fe/aws-metadata-iptables.png 860w,
/static/32ce48434dea04a27e254d183d5bb122/e7448/aws-metadata-iptables.png 900w&quot;
            sizes=&quot;(max-width: 900px) 100vw, 900px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/32ce48434dea04a27e254d183d5bb122/e7448/aws-metadata-iptables.png&quot;
            alt=&quot;169.254.169.254로 향하는 요청을 가로채는 iptables 규칙&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;169.254.169.254로 향하는 요청을 가로채는 iptables 규칙&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;포드가 사용하는 서비스 어카운트(ServiceAccount)의 주석(Annotation)에 해당 포드가 사용할 IAM 역할을 명시합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 621px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 15.555555555555555%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAADCAYAAACTWi8uAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAgUlEQVR42p2MsQqDMAAF41BJumSwRINKqjEOHSWDDq0gFOrW//+Za80ftMNx8DieiHFhnu9MU2QYAs45tNYIIciy7GfEON5Ylgdd51FKIaUkz0/p8C+UOrOuT/b9zba96PuBpnFfB9r2SlFcMMZgraUsy4T3nhACVVVR13XysR/dBzqvSDjrKaSWAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/1e5102f49918f7735f59fbd3507d8478/33ecc/serviceaccount-annotation.webp 360w,
/static/1e5102f49918f7735f59fbd3507d8478/263a4/serviceaccount-annotation.webp 480w,
/static/1e5102f49918f7735f59fbd3507d8478/e4092/serviceaccount-annotation.webp 621w&quot;
              sizes=&quot;(max-width: 621px) 100vw, 621px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/1e5102f49918f7735f59fbd3507d8478/53918/serviceaccount-annotation.png 360w,
/static/1e5102f49918f7735f59fbd3507d8478/9aebd/serviceaccount-annotation.png 480w,
/static/1e5102f49918f7735f59fbd3507d8478/41b39/serviceaccount-annotation.png 621w&quot;
            sizes=&quot;(max-width: 621px) 100vw, 621px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/1e5102f49918f7735f59fbd3507d8478/41b39/serviceaccount-annotation.png&quot;
            alt=&quot;포드가 사용할 IAM 역할을 서비스 어카운트에 명시&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;포드가 사용할 IAM 역할을 서비스 어카운트에 명시&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;포드에서 AWS API를 호출할 경우, 1번에서 추가된 iptables 규칙에 의해 API 요청은 kube2iam이나 kiam으로 라우팅됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;kube2iam 또는 kiam은 해당 포드의 서비스 어카운트에 설정된 IAM 역할로 Assume Role을 수행해 해당 AWS API 요청을 전송합니다. 따라서 최종적으로 포드는 해당 IAM 역할에 정의된 권한만을 사용할 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Assume Role을 위한 임시 자격 증명(credential)은 kiam이나 kube2iam이 관리하기 때문에, 포드의 입장에서는 이러한 일련의 과정들이 모두 투명하게 수행됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;kiam과 kube2iam의 핵심 기능이나 동작 원리는 대체로 비슷하지만, 두 애드온의 가장 큰 차이점은 그 구조에 있습니다.&lt;/p&gt;
&lt;p&gt;kube2iam은 데몬셋으로 배포하기 때문에 모든 워커 노드의 IAM 역할에 Assume Role 권한을 부여해야 합니다. 이와 반대로 kiam은 서버-에이전트 구조로 되어 있으며, kiam 에이전트는 AWS API 요청을 가로채 kiam 서버로 전달할 뿐입니다. 따라서 실제적인 Assume Role은 kiam 서버에서 발생하기 때문에 kiam-server가 위치한 노드에만 Assume Role 권한을 부여하면 된다는 보안적인 장점도 있습니다.&lt;/p&gt;
&lt;p&gt;데브시스터즈는 kubeadm, kops로 관리하는 클러스터에서 두 애드온을 사용했었으며, 간단히 정리해본 장단점은 아래와 같습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;kube2iam은 모든 워커 인스턴스에 Assume Role을 부여해야 하지만, 설치 및 관리가 상대적으로 간편하다는 장점이 있었습니다.&lt;/li&gt;
&lt;li&gt;kiam은 kiam 서버가 위치한 워커 인스턴스에만 Assume Role 권한을 부여하면 되지만, 서버-에이전트 간 TLS 통신을 위해 인증서를 별도로 관리하거나 kiam 서버가 위치할 워커 그룹을 nodeSelector로 따로 지정해야 하는 등 별도로 신경써야 할 부분이 있었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;무엇보다도 두 애드온 모두 오픈소스다보니 Helm 차트 및 프로젝트가 빠르게 업데이트되지 않는 등의 애로사항이 있었습니다. 뿐만 아니라 많은 애드온을 함께 운영하고 있다보니, 최대한 외부 의존적인 애드온의 갯수를 줄일 수 있었으면 하는 바람도 없지않아 있었습니다.&lt;/p&gt;
&lt;h1 id=&quot;eks-pod-identity-webhook을-통한-포드의-iam-권한-제어&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#eks-pod-identity-webhook%EC%9D%84-%ED%86%B5%ED%95%9C-%ED%8F%AC%EB%93%9C%EC%9D%98-iam-%EA%B6%8C%ED%95%9C-%EC%A0%9C%EC%96%B4&quot; aria-label=&quot;eks pod identity webhook을 통한 포드의 iam 권한 제어 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;EKS Pod Identity Webhook을 통한 포드의 IAM 권한 제어&lt;/h1&gt;
&lt;p&gt;EKS에서는 kube2iam이나 kiam 대신 Pod Identity Webhook이라는 애드온을 사용해 포드의 IAM 역할을 관리할 수 있습니다. Pod Identity Webhook은 EKS에서만 사용할 수 있는 특별한 애드온으로, 쿠버네티스와 AWS의 자체 기능을 활용해 포드에 IAM 역할을 부여합니다.&lt;/p&gt;
&lt;p&gt;데브시스터즈에서는 기존에 운영하던 kubeadm 및 kops 기반의 쿠버네티스 인프라를 EKS로 교체하였는데, 이에 따라 kube2iam와 kiam은 자연스럽게 Pod Identity Webhook으로 대체되었습니다. 하지만 Pod Identity Webhook은 한국어는 물론 영어로 되어 있는 레퍼런스가 그다지 많지 않은 편입니다. 때문에 Pod Identity Webhook의 사용 방법과 자세한 동작 원리 등을 이해하기가 어려울 수 있는데, 이번 글에서는 이를 명확하게 설명해보려 합니다.&lt;/p&gt;
&lt;h2 id=&quot;pod-identity-webhook-이미지-빌드-및-설치&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#pod-identity-webhook-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%B9%8C%EB%93%9C-%EB%B0%8F-%EC%84%A4%EC%B9%98&quot; aria-label=&quot;pod identity webhook 이미지 빌드 및 설치 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Pod Identity Webhook 이미지 빌드 및 설치&lt;/h2&gt;
&lt;p&gt;Pod Identity Webhook을 사용하기 위해서는 아래의 준비물이 필요합니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1.14 버전 이상의 EKS 쿠버네티스 클러스터&lt;/li&gt;
&lt;li&gt;Pod Identity Webhook 도커 이미지&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pod Identity Webhook의 Github 저장소에서 Dockerfile을 제공하고는 있지만, 도커 이미지 자체가 Public하게 공개되어 있지는 않으므로 직접 이미지를 빌드해 사용해야 합니다. 아래 명령어로 적절히 도커 이미지를 빌드해 Docker Hub, ECR 등의 레지스트리에 Push합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;git&lt;/span&gt; clone https://github.com/aws/amazon-eks-pod-identity-webhook.git

&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; amazon-eks-pod-identity-webhook &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; build &lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-t&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;도커 이미지 이름&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
    &lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; push &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;도커 이미지 이름&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;공식 문서에서 가이드하는대로 Makefile을 실행하면 설치에 필요한 각종 리소스들이 자동으로 생성됩니다. 단, 쿠버네티스의 root CA 인증서 서명 요청을 위한 certificate 리소스가 Pending 상태일 수 있으므로 직접 certificate를 approve 해주는 작업이 필요할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;make&lt;/span&gt; cluster-up &lt;span class=&quot;token assign-left variable&quot;&gt;IMAGE&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;이미지 이름&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Generating certs and deploying into active cluster...&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# ...&lt;/span&gt;

kubectl certificate approve &lt;span class=&quot;token punctuation&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;kubectl get csr &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;jsonpath&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;{.items[?(@.spec.username==&quot;system:serviceaccount:default:pod-identity-webhook&quot;)].metadata.name}&apos;&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# certificatesigningrequest.certificates.k8s.io/csr-b6bd2 approved&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;pod-identity-webhook의-역할&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#pod-identity-webhook%EC%9D%98-%EC%97%AD%ED%95%A0&quot; aria-label=&quot;pod identity webhook의 역할 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Pod Identity Webhook의 역할&lt;/h2&gt;
&lt;p&gt;kube2iam이나 kiam과는 달리 Pod Identity Webhook은 Assume Role과 같은 동작을 수행하지 않습니다. 단지 어드미션 컨트롤러(Admission Controller) 중 mutating webhook이라는 쿠버네티스의 기능을 이용해 특정 IAM 역할을 사용할 포드에게 몇 가지 설정을 추가해줄 뿐입니다. make cluster-up 명령어를 실행했을 때 출력 결과를 유심히 살펴보았다면 mutating webhook도 함께 생성된 것을 알 수 있었을 것입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;make&lt;/span&gt; cluster-up &lt;span class=&quot;token assign-left variable&quot;&gt;IMAGE&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;devsisters/eks-pod-identity-webhook:latest
&lt;span class=&quot;token comment&quot;&gt;# Generating certs and deploying into active cluster...&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# ...&lt;/span&gt;

kubectl apply &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; deploy/mutatingwebhook-ca-bundle.yaml
&lt;span class=&quot;token comment&quot;&gt;# mutatingwebhookconfiguration.admissionregistration.k8s.io/pod-identity-webhook created&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그렇다면 Pod Identity Webhook은 mutating webhook을 통해 어떠한 내용을 포드 스펙에 추가하는걸까요? 이를 직접 확인해보기 위해 간단한 포드를 직접 생성해 보겠습니다. kube2iam이나 kiam과 마찬가지로, 포드가 사용할 IAM 역할의 ARN을 서비스 어카운트의 주석에 추가했다는 점에 유의합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token function&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;EOF&lt;span class=&quot;token bash punctuation&quot;&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; kubectl apply &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; -&lt;/span&gt;
apiVersion: v1
kind: ServiceAccount
metadata:
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::1234567890:role/my-test-role # 적절한 값으로 수정해 사용합니다.
  name: debug-sa
  namespace: default
EOF&lt;/span&gt;

$ &lt;span class=&quot;token function&quot;&gt;cat&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;EOF&lt;span class=&quot;token bash punctuation&quot;&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; kubectl apply &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; -&lt;/span&gt;
apiVersion: v1
kind: Pod
metadata:
  name: debug
spec:
  serviceAccount: debug-sa
  containers:
  - name: debug
    image: quay.io/devsisters/awscli
    args: [&quot;tail&quot;, &quot;-f&quot;, &quot;/dev/null&quot;]
EOF&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;kubectl get po debug -o yaml&lt;/code&gt; 명령어를 실행해서 포드 스펙을 자세히 들여다보면 아래와 같은 설정이 추가되었음을 알 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# mutate webhook에 의해 추가된 부분만을 표시하였습니다.&lt;/span&gt;
 &lt;span class=&quot;token punctuation&quot;&gt;...&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; AWS_ROLE_ARN
      &lt;span class=&quot;token key atrule&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; arn&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;aws&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;iam&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;1234567890&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;role/my&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;test&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;role
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; AWS_WEB_IDENTITY_TOKEN_FILE
      &lt;span class=&quot;token key atrule&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /var/run/secrets/eks.amazonaws.com/serviceaccount/token
 &lt;span class=&quot;token punctuation&quot;&gt;...&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;volumeMounts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;mountPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /var/run/secrets/eks.amazonaws.com/serviceaccount
      &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; aws&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;iam&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;token
      &lt;span class=&quot;token key atrule&quot;&gt;readOnly&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;...&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; aws&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;iam&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;token
    &lt;span class=&quot;token key atrule&quot;&gt;projected&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;defaultMode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;420&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;sources&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;serviceAccountToken&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;audience&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; sts.amazonaws.com
          &lt;span class=&quot;token key atrule&quot;&gt;expirationSeconds&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;86400&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; token&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Pod Identity Webhook이 포드에 추가한 설정은 2개의 환경 변수, 1개의 토큰 마운트가 전부입니다. 하지만 위 설정만으로도 포드가 특정 IAM 역할만을 사용하도록 만들기에는 충분합니다.&lt;/p&gt;
&lt;h2 id=&quot;token-projection과-eks-openid-connect-provider&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#token-projection%EA%B3%BC-eks-openid-connect-provider&quot; aria-label=&quot;token projection과 eks openid connect provider permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Token Projection과 EKS OpenID Connect Provider&lt;/h2&gt;
&lt;p&gt;쿠버네티스에서 서비스 어카운트(ServiceAccount)를 식별하는 가장 대표적인 방법은 아마 JWT 토큰일 것입니다. 서비스 어카운트를 생성하면 그에 대응하는 시크릿(Secret) 리소스가 자동으로 생성되며, 쿠버네티스 API 서버는 이 시크릿의 JWT 토큰을 통해 서비스 어카운트의 인증을 수행합니다. 하지만 이 JWT 토큰은 어디까지나 쿠버네티스 내에서 서비스 어카운트를 식별하기 위해 사용되는 것이기 때문에 OAuth2에서 쓰이는 exp, aud 등과 같은 속성이 들어가있지 않습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 569px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 47.5%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAABH0lEQVR42qVS0VKEMAzk/7/Q8UU99fDuVJCjFNoma7YFBeYenLHMTjLbdJtNqer6iLZt4L0HoPnDDaj+DVWYIuIYkWJCHATRrzBtc1GxQxalxCVfo4q9wteK0ADDq+VviuHF4rEgc4yG0CqSU0gAwpci9tZ7wmZV0QrcQUxEstj0zgssPxn/LPmy8aJ5f6gLFzvjrK5/FEyNbgXHs22eWWhi1oFEII2ATHOc84z4y+tSF3Yd8iZ3MLEPs3ClDbPWlZwWl3zZyzztmrPpU7PoRpAk58TWWZSG+aBbibgyu7w380udvcvtDjkjzpJz5Jz6J8OD4Hov6Ii7MtcfB84se/4qO8E0t87ZUJygfXJ8DCIwPxU+NIVjl7dWtW/5v+sb76YQ8t/Ij2sAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/db83a8fed66468bc5bbf8147abfe543a/33ecc/k8s-serviceaccount-jwt.webp 360w,
/static/db83a8fed66468bc5bbf8147abfe543a/263a4/k8s-serviceaccount-jwt.webp 480w,
/static/db83a8fed66468bc5bbf8147abfe543a/fa7e8/k8s-serviceaccount-jwt.webp 569w&quot;
              sizes=&quot;(max-width: 569px) 100vw, 569px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/db83a8fed66468bc5bbf8147abfe543a/53918/k8s-serviceaccount-jwt.png 360w,
/static/db83a8fed66468bc5bbf8147abfe543a/9aebd/k8s-serviceaccount-jwt.png 480w,
/static/db83a8fed66468bc5bbf8147abfe543a/7ca0c/k8s-serviceaccount-jwt.png 569w&quot;
            sizes=&quot;(max-width: 569px) 100vw, 569px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/db83a8fed66468bc5bbf8147abfe543a/7ca0c/k8s-serviceaccount-jwt.png&quot;
            alt=&quot;서비스 어카운트의 토큰을 jwt.io에서 decode한 데이터&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;서비스 어카운트의 토큰을 jwt.io에서 decode한 데이터&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;하지만 쿠버네티스 버전 1.12 이상부터는 projectedServiceAccountToken이라는 기능을 사용함으로써 토큰에 exp, aud등의 항목을 덧붙힐 수 있는데, Pod Identity Webhook이 mutate webhook을 통해 포드에 마운트한 토큰이 바로 이것입니다. 즉, 포드 내부에 마운트된 aws-iam-token 파일에는 JWT 토큰이 담겨져 있으며, 이 토큰을 jwt.io에서 decode 해보면 exp, aud 등의 속성이 추가로 들어있는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;yaml&quot;&gt;&lt;pre class=&quot;language-yaml&quot;&gt;&lt;code class=&quot;language-yaml&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# ...생략...&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;volumeMounts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;mountPath&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; /var/run/secrets/eks.amazonaws.com/serviceaccount
      &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; aws&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;iam&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;token
      &lt;span class=&quot;token key atrule&quot;&gt;readOnly&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# ...생략...&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; aws&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;iam&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;token
    &lt;span class=&quot;token key atrule&quot;&gt;projected&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;defaultMode&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;420&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;sources&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;serviceAccountToken&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;audience&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; sts.amazonaws.com
          &lt;span class=&quot;token key atrule&quot;&gt;expirationSeconds&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;86400&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; token&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 500px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 95.27777777777779%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAATCAYAAACQjC21AAAACXBIWXMAAAsTAAALEwEAmpwYAAABqElEQVR42q1Ui07DMAzs///YBEKgISGeQytivAbb2vSVNrbxxd1DqCDYqHRLmqXXO5/TZLFYyHz+JlmWifdecDHz3kgokBCR3th4KBIOIuQNmB96JfgJDUvzzuKXihVLmykcS6j3JIwKWxAYUVfYHIq1LD9ikDAqUmVdzlKmLNXMVO5pmaVektQfJNlrJ7dHTiZnTu7HTtJLoJD0yubTC53fOXmYbjF7LCLaloywLoJMz3XjuJRUH7g5zeTmJDeMcrk+0rHH9ahfP7b1ib60yDup66AJ847lksU9kriUpHpRu4WF05X85+ST9aSZq/U3Jam0CKoeoYRfEH4NZ0MINdUTxcSrZ5BTJKOuR2sInjd9i/thhWwb0DIgWatt8z59Zz3afOj6nGLPYg/WBglhMTZ0/yCIgjdlezV2PC1au+KB1LqOGo6bmhLUEupjTSvU2MLCiBP2LWG0nJvlaO/dlMIuCGAdLvxiC/z3zdHbSZP/4ePQLre1g91SexIB4EXUWD2jZaCxOdaHWipZ9xxsRdur7QcC59lnfdqZISa+4k15BgkPtblL+AluatTl+fT2RQAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/55e6cf46239c57698bc690b451e6b9e1/33ecc/k8s-projected-token.webp 360w,
/static/55e6cf46239c57698bc690b451e6b9e1/263a4/k8s-projected-token.webp 480w,
/static/55e6cf46239c57698bc690b451e6b9e1/601b1/k8s-projected-token.webp 500w&quot;
              sizes=&quot;(max-width: 500px) 100vw, 500px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/55e6cf46239c57698bc690b451e6b9e1/53918/k8s-projected-token.png 360w,
/static/55e6cf46239c57698bc690b451e6b9e1/9aebd/k8s-projected-token.png 480w,
/static/55e6cf46239c57698bc690b451e6b9e1/0eb09/k8s-projected-token.png 500w&quot;
            sizes=&quot;(max-width: 500px) 100vw, 500px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/55e6cf46239c57698bc690b451e6b9e1/0eb09/k8s-projected-token.png&quot;
            alt=&quot;OIDC의 몇 가지 속성이 추가된 쿠버네티스 토큰&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;OIDC의 몇 가지 속성이 추가된 쿠버네티스 토큰&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;이 토큰에서 눈여겨 보아야할 부분은 iss 속성의 값입니다. &lt;code&gt;https://oidc.eks..&lt;/code&gt; 로 시작하는 이 URL은 EKS가 자체적으로 제공하는 OpenID Connect Provider (이하 EKS IdP) 주소입니다. EKS 쿠버네티스 버전 1.14 이상을 사용하면 자동으로 EKS IdP가 함께 생성되며, 이 EKS IdP를 통해 쿠버네티스가 발급한 토큰이 유효한지를 검증할 수 있습니다. EKS IdP 주소는 EKS 정보 페이지에서도 쉽게 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 500px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 20.833333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAECAYAAACOXx+WAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAt0lEQVR42o2PwQ6CMBBE+f9P04NA0AQTKkJjKdJCW7igMG438ebBTSYz06Sv28SHGY18QPdPlup6aE2uNOcoO46o7w3ibNuGfd9/Kk4S5hkPpWDsiJ6Aw2AoWxhjCKzJLV6vN4P+mcTS5VOa4ny+IMtylOUVWZ6jKAqkdH44HCGEYLiih4W4QUpJuUNVVbjVNdq2RUedgeu6YqYtQwjsy7Kwe+/ZnXOs2J3zsPSTaZq4x2W+ij0CP1MWL0t8PSCxAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/8bf6a56b68c977e7b4039153a7a75770/33ecc/eks-oidc.webp 360w,
/static/8bf6a56b68c977e7b4039153a7a75770/263a4/eks-oidc.webp 480w,
/static/8bf6a56b68c977e7b4039153a7a75770/601b1/eks-oidc.webp 500w&quot;
              sizes=&quot;(max-width: 500px) 100vw, 500px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/8bf6a56b68c977e7b4039153a7a75770/53918/eks-oidc.png 360w,
/static/8bf6a56b68c977e7b4039153a7a75770/9aebd/eks-oidc.png 480w,
/static/8bf6a56b68c977e7b4039153a7a75770/0eb09/eks-oidc.png 500w&quot;
            sizes=&quot;(max-width: 500px) 100vw, 500px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/8bf6a56b68c977e7b4039153a7a75770/0eb09/eks-oidc.png&quot;
            alt=&quot;EKS 정보에서 확인할 수 있는 IdP 주소&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;EKS 정보에서 확인할 수 있는 IdP 주소&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;AWS는 이 기능을 이용해 포드에게 IAM 역할을 부여할 수 있는지를 검사합니다. 포드가 특정 IAM 역할으로 Assume 할 때 위 토큰을 AWS로 전송하게 되고, AWS의 입장에서는 이 토큰과 EKS IdP를 통해 포드가 해당 IAM 역할을 사용할 수 있는지를 검증합니다.&lt;/p&gt;
&lt;p&gt;projectedServiceAccountToken 기능은 아래의 파라미터를 API 서버에 추가해야 사용할 수 있지만, EKS의 경우에는 자동으로 값이 설정되기 때문에 별도의 작업이 필요하지 않습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;bash&quot;&gt;&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;--service-account-singing-key-file&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;서비스 어카운트 토큰 발급에 사용된 비밀 키. 쿠버네티스의 root CA가 아닙니다&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
--service-account-issuer&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;https://alicek106.devsisters.com
--api-audiences&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;devsisters&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;jwt-토큰을-통한-assumerolewithwebidentity&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#jwt-%ED%86%A0%ED%81%B0%EC%9D%84-%ED%86%B5%ED%95%9C-assumerolewithwebidentity&quot; aria-label=&quot;jwt 토큰을 통한 assumerolewithwebidentity permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;JWT 토큰을 통한 AssumeRoleWithWebIdentity&lt;/h2&gt;
&lt;p&gt;AWS SDK는 &lt;code&gt;AWS_ROLE_ARN&lt;/code&gt; 및 &lt;code&gt;AWS_WEB_IDENTITY_TOKEN_FILE&lt;/code&gt; 라는 이름의 환경변수 값이 설정되어 있을 경우 해당 환경변수를 읽어들여 Web Identity 토큰으로 Assume Role을 시도합니다. 즉, &lt;code&gt;$(AWS_ROLE_ARN)&lt;/code&gt; 라는 역할을 Assume하기 위한 자격 증명으로서 &lt;code&gt;$(AWS_WEB_IDENTITY_TOKEN_FILE)&lt;/code&gt; 경로의 Web Identity 토큰 파일을 사용하겠다는 뜻입니다. 포드 내부의 AWS SDK는 이 토큰을 통해 &lt;a href=&quot;https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;AssumeRoleWithWebIdentity&lt;/a&gt;를 호출함으로써 임시 자격 증명을 획득하게 되고, 비로소 특정 IAM 역할로 변신할 수 있게 됩니다. 이 때, Assume Role 동작을 위한 인증은 AWS가 아닌 외부 Web IdP (이 경우에는 위에서 언급한 EKS IdP) 에 위임하게 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;${AWS_WEB_IDENTITY_TOKEN_FILE)&lt;/code&gt; 경로에 있는 Web Identity 토큰 파일은 바로 앞에서 설명한 aws-iam-token 토큰입니다. 이 토큰의 issuer는 EKS의 IdP임을 잊지 말아야 합니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;하지만 이렇게 환경 변수를 읽어들여 Assume Role을 수행하는 기능은 특정 버전 이상의 AWS SDK를 필요로 한다는 사실에 유의해야 합니다. 따라서 애플리케이션의 AWS SDK 버전이 낮을 경우 Pod Identity Webhook을 사용할 수 없을수도 있습니다. web identity token을 사용할 수 있는 AWS SDK 버전은 &lt;a href=&quot;https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts-minimum-sdk.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;AWS 문서&lt;/a&gt;에서 확인할 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;eks-openid-connect-provider-등록-및-trust-relationship-설정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#eks-openid-connect-provider-%EB%93%B1%EB%A1%9D-%EB%B0%8F-trust-relationship-%EC%84%A4%EC%A0%95&quot; aria-label=&quot;eks openid connect provider 등록 및 trust relationship 설정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;EKS OpenID Connect Provider 등록 및 Trust Relationship 설정&lt;/h2&gt;
&lt;p&gt;다음 단계는 EKS IdP를 identity provider로서 등록하고, 포드가 Web Identity 토큰을 통해 IAM 역할을 Assume할 수 있도록 Trust Relationship을 추가하는 일만이 남았습니다. 앞서 설명했던 것처럼, 토큰을 전송한 클라이언트(AWS SDK)가 Assume Role을 할 수 있는지 판단하는 주체는 EKS IdP가 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 893px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 31.944444444444443%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAGCAYAAADDl76dAAAACXBIWXMAAAsTAAALEwEAmpwYAAABTUlEQVR42mNggIH//xlBVFrXS+WsliMGkgwMXB5mKnz76+1ZzqQZs86E4nh7eQ6gMmaIFgawnqvtzv5vpnppQsypZ2CqB2KQpLExAys/P4OAvZYoD1BKAYhlgVhQXFxcTEpKSlhLS4sNaiALzB29oRacZY7K2vHm8goTPVTYGaJs+AVVJHhE1SR5RDxUhPjk5fkFtGT4hIBqlaCYF+oiJiAGGsgAMpAVjO0ZWMCujpfn0NAQkbRwkxFiWLWKgRmEga5kAbkyNJSBOdRChpOfn18QqInz6dOnIo8fP17/8NGjsx8+fIgGuUpISEgGSKkDsUBsvamabbgyyCcsHmnaygzoAGRgmg8D139omD65tlv4wb27/Xdv31r+5OFdX6AQo729PciVIHnmqGpLeb8kdSkgm907UVeJAQsAKZQE2Q73GgKAvM4G9T6yejgbAGCgUg+MD9KZAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/f472dbdf41689b3fa7a1f0b99a9914f3/33ecc/pod-identity-webhook.webp 360w,
/static/f472dbdf41689b3fa7a1f0b99a9914f3/263a4/pod-identity-webhook.webp 480w,
/static/f472dbdf41689b3fa7a1f0b99a9914f3/0c8fb/pod-identity-webhook.webp 640w,
/static/f472dbdf41689b3fa7a1f0b99a9914f3/521f7/pod-identity-webhook.webp 860w,
/static/f472dbdf41689b3fa7a1f0b99a9914f3/612e0/pod-identity-webhook.webp 893w&quot;
              sizes=&quot;(max-width: 893px) 100vw, 893px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/f472dbdf41689b3fa7a1f0b99a9914f3/53918/pod-identity-webhook.png 360w,
/static/f472dbdf41689b3fa7a1f0b99a9914f3/9aebd/pod-identity-webhook.png 480w,
/static/f472dbdf41689b3fa7a1f0b99a9914f3/0f09e/pod-identity-webhook.png 640w,
/static/f472dbdf41689b3fa7a1f0b99a9914f3/3e3fe/pod-identity-webhook.png 860w,
/static/f472dbdf41689b3fa7a1f0b99a9914f3/59080/pod-identity-webhook.png 893w&quot;
            sizes=&quot;(max-width: 893px) 100vw, 893px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/f472dbdf41689b3fa7a1f0b99a9914f3/59080/pod-identity-webhook.png&quot;
            alt=&quot;Pod Identity Webhook을 사용하기 위한 워크플로우&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Pod Identity Webhook을 사용하기 위한 워크플로우&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;EKS IdP를 Identity Provider로 등록하는 작업은 AWS 웹 콘솔에서 진행할 수도 있지만 (IAM 페이지 → 왼쪽 메뉴 중 Identity Provider), 데브시스터즈의 모든 인프라는 테라폼을 통해 코드화되어 있으므로 이 또한 테라폼으로 구성했습니다. 단, thumbprint_list 항목은 예외적으로 직접 입력해줘야 하는 &lt;a href=&quot;https://github.com/terraform-providers/terraform-provider-aws/issues/10104&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;이슈&lt;/a&gt;가 있으므로 참고하기 바랍니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;terraform&quot;&gt;&lt;pre class=&quot;language-terraform&quot;&gt;&lt;code class=&quot;language-terraform&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;resource &lt;span class=&quot;token type variable&quot;&gt;&quot;aws_iam_openid_connect_provider&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;my_eks_oidc&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;# 데브시스터즈는 EKS 테라폼 모듈을 사용하고 있으며, output으로부터 issuer url을 가져올 수 있습니다.&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;url&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; module.cluster.cluster_oidc_issuer_url

  &lt;span class=&quot;token property&quot;&gt;client_id_list&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;sts.amazonaws.com&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

  &lt;span class=&quot;token property&quot;&gt;thumbprint_list&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;9e99a48a9960b14926bb7f3b02e22da2b0ab7280&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;마지막으로, AssumeRoleWithWebIdentity과 EKS IdP를 통해 IAM 역할을 Assume할 수 있도록 Trust Relationship을 추가합니다. Conditions에서는 어떠한 네임스페이스의 서비스 어카운트가 Assume Role을 할 수 있는지 별도로 제한할 수 있습니다. 아래의 예시에서는 StringEquals를 통해 default 네임스페이스의 debug-sa 서비스 어카운트에게만 권한을 Assume Role 권한을 부여했습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;terraform&quot;&gt;&lt;pre class=&quot;language-terraform&quot;&gt;&lt;code class=&quot;language-terraform&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;data &lt;span class=&quot;token type variable&quot;&gt;&quot;aws_iam_policy_document&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;assume_role_policy&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;statement&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;actions&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;sts:AssumeRoleWithWebIdentity&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;principals&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;type&lt;/span&gt;        &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;federated&quot;&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;identifiers&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;aws_iam_openid_connect_provider.my_eks_oidc.arn&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;condition&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;test&lt;/span&gt;     &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;StringEquals&quot;&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;variable&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;$&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;replace&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;aws_iam_openid_connect_provider&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;my_eks_oidc&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;https://&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;:sub&quot;&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;values&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;&quot;system:serviceaccount:default:debug-sa&quot;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;
      ]
    }
  }
}

resource &quot;&lt;/span&gt;aws_iam_role&lt;span class=&quot;token string&quot;&gt;&quot; &quot;&lt;/span&gt;my-test-role&lt;span class=&quot;token string&quot;&gt;&quot; {
  name = &quot;&lt;/span&gt;my-test-role&quot;
  &lt;span class=&quot;token property&quot;&gt;assume_role_policy&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;=&lt;/span&gt; data.aws_iam_policy_document.assume_role_policy.json
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;필요하다면 condition에서 StringEqual 대신 StringLike를 사용하고, 권한을 부여할 서비스 어카운트를 system:serviceaccount:default:* 처럼 와일드카드로 명시할 수도 있습니다.&lt;/p&gt;
&lt;p&gt;지금까지 Pod Identity Webhook을 사용하는 전체적인 워크플로우를 간단하게 설명했습니다. 위 내용을 문제없이 잘 따라했다면 포드는 특정 IAM 역할로 Assume되어 AWS API를 호출하게 됩니다. 비록 이러한 과정들이 귀찮게 느껴질지라도, 포드에는 가능한 한 최소한의 권한만을 부여하는 것이 바람직하다는 사실을 잊지 말아야 합니다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[JIRA를 하자! - 쿠키런 : 오븐브레이크의 JIRA 도입기]]></title><description><![CDATA[쿠키런 오븐브레이크에서 JIRA를 선택한 이유와, 이를 효과적으로 사용하기 위해 고민했던 부분들을 알려드립니다.]]></description><link>https://tech.devsisters.com/posts/jira-of-qa</link><guid isPermaLink="false">https://tech.devsisters.com/posts/jira-of-qa</guid><pubDate>Mon, 16 Mar 2020 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;1-시작하며&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EC%8B%9C%EC%9E%91%ED%95%98%EB%A9%B0&quot; aria-label=&quot;1 시작하며 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;1. 시작하며&lt;/h2&gt;
&lt;p&gt;이슈(Issue)를 추적관리하는 업무는 QA 직군이 반드시 해야 하는 필수 업무 중 하나입니다. 탄생(생성)부터 죽음(완료)까지 이슈의 생애를 각종 정보들을 통해 지켜봄으로서 이들의 처리가 원활하도록 관리해야 합니다. 이슈는 또 일종의 일감으로, 전체 프로젝트 마일스톤(Milestone) 안에 있는 또 하나의 미시적 마일스톤으로도 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;활발하게 개발이 이루어지는 기간에는 이러한 일감들이 끊임없이 쏟아져 나옵니다. 그럼 수많은 일감을 어떻게 전달해야 효과적일까요? 귀로 듣는 것이 확실하니까 직접 말로 설명할까요? 아니면 담당자 모니터에 포스트잇으로 붙여둘까요? 그것도 아니면 메신저로 전달할까요?&lt;/p&gt;
&lt;p&gt;여러 가지가 있겠지만, 가장 보편적인 방법으로 대부분 **&apos;BTS(Bug Tracking System)&apos;**라는 도구를 사용하여 이슈와 일감들을 관리합니다. (요즘 방탄소년단 덕분에 BTS라는 단어가 국위선양의 콘텐츠로 자리매김했지만 어쨌든...) BTS를 사용하여 팀원들은 수많은 일감을 시각적으로 확인하고, 현재 상태를 직관적으로 알 수 있어서 업무 효율성이 비약적으로 상승합니다. 구두 또는 문서전달의 방식 보다 훨씬 더 효과적이죠.&lt;/p&gt;
&lt;p&gt;그러나 당장 &apos;BTS를 사용하자!&apos;라고 결정해도 한 가지가 논의가 더 필요합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;그럼 어떤 BTS 서비스를 사용해야 할까?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;BTS로 사용할 수 있는 프로그램이나 서비스들은 매우 많지만, 아무렇게나 사용하면 원하는 만큼의 효율성을 이끌어내지 못할 확률이 큽니다. 이는 개발 프로세스, 팀 성향, 무료 / 유료 툴 여부, 원하는 기능을 제공하는지 등 세부적으로 커스터마이징이 필요한 부분들이 많기 때문이죠. 어떤 툴을 어떤 방식으로 사용하느냐는 역시나 사람에게 달려 있기 때문에, 우리는 좋은 툴을 효과적으로 사용하기 위한 방법을 마련해야 합니다. 그렇지 않다면 일감 관리 업무는 &lt;strong&gt;Bug Tracking System&lt;/strong&gt;이 아닌 **Burn The Stage(하얗게 불태웠어...)**가 될 테니까요.&lt;/p&gt;
&lt;h2 id=&quot;2-jira-이전의-bts&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-jira-%EC%9D%B4%EC%A0%84%EC%9D%98-bts&quot; aria-label=&quot;2 jira 이전의 bts permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;2. JIRA 이전의 BTS&lt;/h2&gt;
&lt;p&gt;이 포스트에서는 &lt;strong&gt;쿠키런 오븐브레이크&lt;/strong&gt; 팀에서 JIRA를 도입하는 과정과, 이를 효과적으로 사용하기 위해 노력했던 것들에 대해 서술합니다. 그럼 우리가 왜 JIRA를 사용했는지를 알기 위해서는 그 이전에 사용했었던 BTS에 대해서 한번 되짚어보아야 할 것 같습니다.&lt;/p&gt;
&lt;h3 id=&quot;1-구글-스프레드시트-feat-excel&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EA%B5%AC%EA%B8%80-%EC%8A%A4%ED%94%84%EB%A0%88%EB%93%9C%EC%8B%9C%ED%8A%B8-feat-excel&quot; aria-label=&quot;1 구글 스프레드시트 feat excel permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;1) 구글 스프레드시트 (Feat. Excel)&lt;/h3&gt;
&lt;p&gt;협업을 하는 회사라면 적어도 한 번씩 사용해보았을 구글 스프레드시트입니다. 구글 계정만 있다면 기본적으로 웹페이지에서 접근이 가능하기 때문에 따로 요금을 지불하거나 복잡하게 계정을 생성해야 하는 등의 프로세스는 필요하지 않습니다. 또한 Microsoft의 Excel과 유사하여 사용성 측면에서도 비교적 간단합니다. 구글 스프레드 시트는 쿠키런 for KAKAO 서비스 초반과 쿠키런 오븐브레이크 일부 이슈관리에 사용했습니다.&lt;/p&gt;
&lt;p&gt;구글 스프레드시트의 장단점은 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;장점:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;접근성과 사용성이 좋습니다.&lt;/li&gt;
&lt;li&gt;다양한 양식으로 템플릿을 제작할 수 있습니다.&lt;/li&gt;
&lt;li&gt;메일이나 링크 전달만으로도 손쉽게 다른 사람에게 공유가 가능합니다.&lt;/li&gt;
&lt;li&gt;무료입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;단점:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;처음에는 가벼우나, 데이터가 쌓이면 그 데이터를 관리하는 리소스가 매우 많이 듭니다.&lt;/li&gt;
&lt;li&gt;이슈 상태를 직관적으로 파악하기 어렵습니다.&lt;/li&gt;
&lt;li&gt;상용 BTS 서비스 만큼의 검색, 자동 분류 기능을 기본적으로 제공하지 않습니다.&lt;/li&gt;
&lt;li&gt;스크린샷, 동영상 등의 첨부파일을 업로드하는 것이 매우 제한적입니다.&lt;/li&gt;
&lt;li&gt;보안이 취약합니다.&lt;/li&gt;
&lt;li&gt;무엇보다도 툴이 멋지지가 않습니다. (사실 제일 중요함)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;장점도 있긴 한데, 단점이 매우 치명적이어서 이슈를 추적관리하는데 부적합한 툴이었습니다. 이슈를 추적관리하는 것보다 시트의 내용을 정리하고 수동으로 최적화 작업을 해야 했기 때문에 작업자의 신경이 이슈의 관리보다는 &apos;시트 정리&apos;에 초점이 맞추어져 있어서 &lt;strong&gt;일을 위한 일&lt;/strong&gt;이 되어버리는 결과를 낳았습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 47.77777777777777%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAACM0lEQVR42l2SSXPTQBCF9TvhRHIC/hBVXHMJRRWXcKIgBKqMbXmRd1myLY0k27Jk7bK12Xn0DDlx6OpZv3n9eiR7y+DubYThEXmeojjnL/kkcpYlyNIEl6bC5VJT/hd1XeJK84byc9NgaE5wJ3+BlOcVbV5RlhXO5zOKokBV8bWa5gVBc6RpSvMGDV2saL2qasRJgpJyRvvXZ6C5XFDWFSTbeMDRfICjriibiF0LpjZFGR9Qpz7qzEeVeAh3Bpg2wc5Qkfo2mvyIKvXQ0L6zbCFgYzgbHZJpvoLNbuDYB/gHCu8AVV1QmSnSJMYpz8BMA91OG532H8jdDibjEcKALMoy5EkIS7mDv/5O9zRIzLqB7byDZe1xIFgQBFitVkioJM/3EUUR1psNRqMROt0u5F4P8/kCe9elsxGFB2f2iar7BU1fE5C9gW2/BWMEPLgCqGma8PJCvnAPF4sFugTr9fsYDAaQZRnbrYOaPG3qArvpPQLjJ5baf0CPFHqeh9lsJhrEm8MVTqdTtFot9F+AHG4YBsqixLk444fyFUO9A22pcQ9vwaz3BNyRQvKQyuSKTqeTGDuOIx7gwZUpiiLGuq6T39SsOML98AmPuoK1voLkea9J1S1cN6K/GCCOYwHhJe/3e6FkuVxiOByi3W4LpVwxX7cYQxCF+Dx6xNNqAMswIf1WPqA1/kiXeUOOAsjoYFmWwj9ugUnfaTKZYDweC7CqquLRIAjJmhxz9RtV2aNzFv4C60jXxgwoMkMAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/2672285090dd8e936c9ff68afd487529/33ecc/using-spreadsheet-as-bts.webp 360w,
/static/2672285090dd8e936c9ff68afd487529/263a4/using-spreadsheet-as-bts.webp 480w,
/static/2672285090dd8e936c9ff68afd487529/0c8fb/using-spreadsheet-as-bts.webp 640w,
/static/2672285090dd8e936c9ff68afd487529/521f7/using-spreadsheet-as-bts.webp 860w,
/static/2672285090dd8e936c9ff68afd487529/cbd37/using-spreadsheet-as-bts.webp 1200w,
/static/2672285090dd8e936c9ff68afd487529/548e7/using-spreadsheet-as-bts.webp 2400w,
/static/2672285090dd8e936c9ff68afd487529/32f2e/using-spreadsheet-as-bts.webp 2752w&quot;
              sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/2672285090dd8e936c9ff68afd487529/53918/using-spreadsheet-as-bts.png 360w,
/static/2672285090dd8e936c9ff68afd487529/9aebd/using-spreadsheet-as-bts.png 480w,
/static/2672285090dd8e936c9ff68afd487529/0f09e/using-spreadsheet-as-bts.png 640w,
/static/2672285090dd8e936c9ff68afd487529/3e3fe/using-spreadsheet-as-bts.png 860w,
/static/2672285090dd8e936c9ff68afd487529/64756/using-spreadsheet-as-bts.png 1200w,
/static/2672285090dd8e936c9ff68afd487529/98e2c/using-spreadsheet-as-bts.png 2400w,
/static/2672285090dd8e936c9ff68afd487529/3fb98/using-spreadsheet-as-bts.png 2752w&quot;
            sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/2672285090dd8e936c9ff68afd487529/98e2c/using-spreadsheet-as-bts.png&quot;
            alt=&quot;구글 스프레드시트 화면. 보기만 해도 아찔하다...&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;구글 스프레드시트 화면. 보기만 해도 아찔하다...&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h3 id=&quot;2-트렐로trello&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%ED%8A%B8%EB%A0%90%EB%A1%9Ctrello&quot; aria-label=&quot;2 트렐로trello permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;2) &lt;strong&gt;트렐로(Trello)&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;그다음으로 사용했던 트렐로는 전형적인 프로젝트 관리용 서비스입니다. 애자일 프로세스에서 흔히 볼 수 있는 칸반(Kanban), 즉 열(List) 단위 연속 처리 방식에 따라 이슈가 시각화되는 것이 특징입니다. 이 서비스는 쿠키런 for KAKAO와 더불어 쿠키런 오븐브레이크 서비스 중반, 그 외 다양한 내부 프로젝트에서 범용적으로 사용하였습니다. 불과 작년 3월까지 사용했던 툴이라 아직도 기억이 생생하네요.&lt;/p&gt;
&lt;p&gt;트렐로의 장단점은 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;장점:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;직관적으로 이슈의 상태를 확인할 수 있습니다.&lt;/li&gt;
&lt;li&gt;각 열 상태를 마음대로 지정할 수 있습니다.&lt;/li&gt;
&lt;li&gt;열 이외에 레이블(Label)을 사용하여 추가적으로 이슈의 상태를 표시할 수 있습니다.&lt;/li&gt;
&lt;li&gt;담당자 지정이 가능합니다.&lt;/li&gt;
&lt;li&gt;(제한적이지만) 첨부파일 업로드가 가능합니다.&lt;/li&gt;
&lt;li&gt;상대적으로 다른 서비스에 비해 가볍습니다.&lt;/li&gt;
&lt;li&gt;필터링을 통해 원하는 이슈만 볼 수 있습니다.&lt;/li&gt;
&lt;li&gt;무료로 사용 가능합니다.&lt;/li&gt;
&lt;li&gt;승인된 계정만 접속 가능하므로 일정 수준의 보안이 확보됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;단점:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;이슈 검색 기능이 원하는 만큼 정확하지 않았습니다.&lt;/li&gt;
&lt;li&gt;생성된 이슈 카드 수가 많으면 엄청나게 느려집니다.&lt;/li&gt;
&lt;li&gt;워크플로(Workflow) 기능이 없어서, 잘못된 상태에 있는 이슈를 확인하기 쉽지 않습니다.&lt;/li&gt;
&lt;li&gt;무료 버전의 경우 첨부파일 용량 및 생성 가능한 보드의 수에 제한이 있습니다.&lt;/li&gt;
&lt;li&gt;이슈들의 히스토리 관리가 어렵습니다.&lt;/li&gt;
&lt;li&gt;통계 기능이 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;트렐로는 많은 장점을 가진 도구입니다. 팀의 규모가 작았을 때 가볍게 사용할 수 있으면서 이슈들의 상태를 관리할 수 있었기 때문에, 꽤나 오랫동안 사용했던 서비스입니다. 무엇보다도 무료였기 때문에 비용의 부담 없이 쓸 수 있어서 더 좋았었네요.&lt;/p&gt;
&lt;p&gt;그러나 무료 버전은 첨부파일 용량 제한으로 항상 동영상이나 스크린샷을 올릴 때 용량을 확인해야 했고, 경우에 따라 용량을 줄이기 위한 인코딩까지 필요했습니다. 이러한 작업을 거치면 첨부파일의 품질이 낮아져, 막상 파일을 실행해도 화면이 나오지 않거나 저화질로 보아야 하는 경우가 비일비재했습니다. 무엇보다도 이슈 검색 기능이 생각보다 좋지 않았기에, 이전에 등록했었던 특정 이슈 카드를 찾기 힘들었습니다. 검색 결과 화면에서 일일이 카드를 눌러보며 찾아야 했던 아픔이 떠오르네요.&lt;/p&gt;
&lt;p&gt;또한 버전 단위로 이슈를 확인하기 어려웠습니다. 저희는 이를 개선하고자 버전별로 보드를 계속 생성해서 관리하였으나, 2019년 5월을 기점으로 무료 사용자의 보드 생성 개수가 제한되면서 이 방법도 결국은 한계에 다다르고 말았습니다.&lt;/p&gt;
&lt;p&gt;좋은 서비스였습니다만, 여러 가지 한계점으로 인해 이별을 하게 되었다는것이 참 마음이 아픕니다. 하지만 이걸 기점으로 전사 QA 직군에서는 BTS 서비스를 바꿔보는 게 어떨까 하는 의견을 낼 수 있었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 54.72222222222223%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAADH0lEQVR42hWS20+bBRjG+w8Yk3nhhd44r4yHmG2CboUNmS5TdsiiojEzM1NB5owXTFi4USKxWCxlUEq7A5QVWjsGlJVDlfOplEPb7/t6PvBRoH7tmMsu5uHu57c3efPe/PK8T97n1QS7LxDq/YqAOoM9F/G3n2Hhl2Ms6MpYbqtg2fg+K9dO4TedY1FfzlTrB8y2f8KG+TRL+qPMNhWr3CmCvVVs3LiAJrHoYlucIRedJxu4R3j5LvG1CZTwAorgRY4soqT85NRO+Ye457zOqMPKpMtEaMLK9oqDgjCCEhxie30IzWYywv2Hf7OZVZA30yzFfSQyUTJihB1pmbiQJic/RI4W8NwZ5MXnnuH00bd4t1TLzbYmHqzZ2JtrJj+nJ+ofQyPH1snn86RSSbLpCNlMmkLhPnv5HLm4iGD/g5g7THxaxmvvoer8QfrNn2NsrGSiX09OmmLXb2dPHGbUZVUdRgXyhUek0tuqYBJZ3mJHUciG/CRXvdi9dszum1wfszI63smIo44599cM26uZd9YTcdUhOq+QcV9lxHIVTVq0sZVxkYjZSG3YkOyXkPq+Iea8jDhQz2rUz1LSR+xPCc+gmef3PUXRq/t5dt/TtFwqJ9PxBoHm14kZXmPw54/QTN5uYHW0junfLuLuqCFsfBOx5QCC7mUkyxlyOwr/Pv6PJ7W0MEPJZ59S1VRLaXUVXbovSZlKCBpLibSqaffVorlS/grtFS+gO7Efw+VKwp3vIBq1CIZiwrc+JpPdJbOb59HjfxDU8/w0NkWNZ5ba8RnG7zQSNRQhtJUi6g8Q8TSi0TW8TVfjIRwtZYx1f0v4WhmC8TCiCkqWc0TmZxGnp1FCATy3rZQVv4S7/0fOf3gcXfVxNi3HCPx6mKTpCI4fzqJZkW4Qyg7z4K91tkSn6uwIYusTwWIk81mk3l6i3d3s3h1QF1p5r0KLxfY9X9RU0tVQSaqzhFCrlrDhECt99WrKc7eQfQ4yvgESk12IphOInScJGLQEOk6SmLGpL9NDer6P2O8mAh4DvgEdgtekpvsd6/oiNlR2rfkgMUcN/wN7joW8n1eX5gAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/badfd2ee99434af99b71b979c1550346/33ecc/using-trello-as-bts.webp 360w,
/static/badfd2ee99434af99b71b979c1550346/263a4/using-trello-as-bts.webp 480w,
/static/badfd2ee99434af99b71b979c1550346/0c8fb/using-trello-as-bts.webp 640w,
/static/badfd2ee99434af99b71b979c1550346/521f7/using-trello-as-bts.webp 860w,
/static/badfd2ee99434af99b71b979c1550346/cbd37/using-trello-as-bts.webp 1200w,
/static/badfd2ee99434af99b71b979c1550346/548e7/using-trello-as-bts.webp 2400w,
/static/badfd2ee99434af99b71b979c1550346/31d32/using-trello-as-bts.webp 2878w&quot;
              sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/badfd2ee99434af99b71b979c1550346/53918/using-trello-as-bts.png 360w,
/static/badfd2ee99434af99b71b979c1550346/9aebd/using-trello-as-bts.png 480w,
/static/badfd2ee99434af99b71b979c1550346/0f09e/using-trello-as-bts.png 640w,
/static/badfd2ee99434af99b71b979c1550346/3e3fe/using-trello-as-bts.png 860w,
/static/badfd2ee99434af99b71b979c1550346/64756/using-trello-as-bts.png 1200w,
/static/badfd2ee99434af99b71b979c1550346/98e2c/using-trello-as-bts.png 2400w,
/static/badfd2ee99434af99b71b979c1550346/f621c/using-trello-as-bts.png 2878w&quot;
            sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/badfd2ee99434af99b71b979c1550346/98e2c/using-trello-as-bts.png&quot;
            alt=&quot;트렐로(Trello) 화면. 시각적으로 확인하기 좋았으나, 기능상 제한이 많았다.&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;트렐로(Trello) 화면. 시각적으로 확인하기 좋았으나, 기능상 제한이 많았다.&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h2 id=&quot;3-bts-춘추-전국-시대&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-bts-%EC%B6%98%EC%B6%94-%EC%A0%84%EA%B5%AD-%EC%8B%9C%EB%8C%80&quot; aria-label=&quot;3 bts 춘추 전국 시대 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;3. BTS 춘추 전국 시대&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;트렐로 다음으로 어떤 서비스를 이용해야 하지?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;트렐로와 이별하기 전, 저희 QA 엔지니어들은 BTS로 사용할 수 있는 서비스들을 주욱 나열해보았습니다. Redmine, Mantis, Asana, Wrike, Monday, JIRA 등 정말 많은 서비스들이 목록화되었고, 이들 중 몇몇은 실제로 초기 설정까지 마치고 시험적으로 사용도 해보았으나, 만족할 만큼의 효율성을 산출해내지는 못하였습니다. 이는 각 툴별로 QA 엔지니어들의 숙련도와 각자가 추구하는 BTS 최소 기능의 수준이 달랐기 때문이었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2056px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 56.388888888888886%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAABYlAAAWJQFJUiTwAAAB1ElEQVR42pWSzW7TQBSF81i8BI/AA1Sw6AJYIbawY9MFEpWQWNBKrYqEBAJKSSC0iopDolASlNKkCU3VOj/22E5sjz35GNuJWiAIcaQjj+7PmXPHN8cM0+k0pZp9L8cWcZ6/DCkluXlCyogg8pEq4n8waTYQtc+JyIVgcvj4qcpurUy+bOC5LmMJxUNJu9Pj6LhH41ubw1aH75q9UzMVs4o7tB6t0HqyyunzDZSO5eY3eWMfb+LjuGNkJIn1NH1HYQuXoSUY6ZwYh/TtMK3TlqjdWUZ020wDH+PWEk7fJBfoZj/09LgC4TvIOCaQIUPHQqk46cuQ12M195i665mBTovSzSX6pSKTqkH93l0GX79kDveOnvEgf4O1D5tsvn/L49db3F9fpdqsp81xuUZ89ToUrnDSvsZAx9SgT3fnFT80zwrbWEaJYGBmgl37mP0TA3NoMrIsRrbFmXmOECIT3HiD3FqDwW0enj/l5biSxiM9gZptxi9/eSGS9bDt9NjudikZBu8Ku9QPGsgwzEqUSuv4XVC36lvUxZ4lrrS7aPsFseel7+pPJvh+RpUILcAfDueLKlyP0n6FsmaleoBw3IWL/E/BORIHgR4rCCWh5t8cLRL8CSulQRcy4qVTAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/5b49727d6a45c5742b9a730fcae4d410/33ecc/trackers.webp 360w,
/static/5b49727d6a45c5742b9a730fcae4d410/263a4/trackers.webp 480w,
/static/5b49727d6a45c5742b9a730fcae4d410/0c8fb/trackers.webp 640w,
/static/5b49727d6a45c5742b9a730fcae4d410/521f7/trackers.webp 860w,
/static/5b49727d6a45c5742b9a730fcae4d410/cbd37/trackers.webp 1200w,
/static/5b49727d6a45c5742b9a730fcae4d410/d97e9/trackers.webp 2056w&quot;
              sizes=&quot;(max-width: 2056px) 100vw, 2056px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/5b49727d6a45c5742b9a730fcae4d410/53918/trackers.png 360w,
/static/5b49727d6a45c5742b9a730fcae4d410/9aebd/trackers.png 480w,
/static/5b49727d6a45c5742b9a730fcae4d410/0f09e/trackers.png 640w,
/static/5b49727d6a45c5742b9a730fcae4d410/3e3fe/trackers.png 860w,
/static/5b49727d6a45c5742b9a730fcae4d410/64756/trackers.png 1200w,
/static/5b49727d6a45c5742b9a730fcae4d410/78f36/trackers.png 2056w&quot;
            sizes=&quot;(max-width: 2056px) 100vw, 2056px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/5b49727d6a45c5742b9a730fcae4d410/78f36/trackers.png&quot;
            alt=&quot;세상엔 다양한 서비스가 있다. 그야말로 춘추전국시대.&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;세상엔 다양한 서비스가 있다. 그야말로 춘추전국시대.&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;그래서 그다음으로 나왔던 의견은, 그냥 각 프로젝트에서 사용하는 BTS는 해당 프로젝트 소속 QA가 결정하여 독자적으로 운용하자는 것이었습니다. 예를 들어 A 프로젝트는 Redmine을, B 프로젝트는 Mantis를, C 프로젝트는 Wrike를 쓰는 식입니다. 이는 각 프로젝트의 업무 프로세스와 QA 엔지니어의 툴 숙련도에 최적화하여 사용할 수 있어서 업무 효율성을 극대화하는 장점이 있습니다.&lt;/p&gt;
&lt;p&gt;그러나 위 방식은 각 서비스마다 계정을 각각 따로 관리했어야 했기 때문에, 전사적으로 계정을 관리하는 입장에서 리소스가 너무 많이 들게 되고, 입사자나 퇴사자 처리 등 계정의 생성/제거 절차가 매우 복잡해지는 단점이 있었습니다. 또한, A 프로젝트에서 B 프로젝트의 BTS를 보려고만 해도 (View) 새로 계정을 생성해야 했기 때문에, 이게 만약 유료툴이라면 1계정 비용이 추가로 지불됩니다. 프로젝트 단위 업무 효율성은 상승할지는 몰라도 관리/비용 측면에서는 비효율적이기 때문에 이 방안은 실행하기 어려웠습니다.&lt;/p&gt;
&lt;p&gt;그래서 우리는 다시 처음으로 되돌아왔습니다만, 여전히 어떤 툴을 사용해야 하는가에 대해서는 의견이 분분했습니다. 사실 JIRA가 다른 모든 서비스의 기능을 대체적으로 포함하고 있어서 가장 최적의 선택이었지만, 유료 서비스였기 때문에 쉽사리 결정하기 어려웠습니다. 그러나 우리의 업무 효율성을 높이는 방법이라면 유료 서비스라도 사용해야 한다는 쪽으로 의견이 모아졌고, 최종적으로 전사 모든 프로젝트가 JIRA를 사용하는 것으로 결정하였습니다.&lt;/p&gt;
&lt;h2 id=&quot;4-괴수-길들이기--초기-환경설정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#4-%EA%B4%B4%EC%88%98-%EA%B8%B8%EB%93%A4%EC%9D%B4%EA%B8%B0--%EC%B4%88%EA%B8%B0-%ED%99%98%EA%B2%BD%EC%84%A4%EC%A0%95&quot; aria-label=&quot;4 괴수 길들이기  초기 환경설정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;4. 괴수 길들이기 : 초기 환경설정&lt;/h2&gt;
&lt;p&gt;우여곡절 끝에 이제 저희는 JIRA라는 아주 거대하고 복잡한 서비스를 사용하게 되었습니다. 처음에는 마치 새로운 전자기기를 산 것처럼 신나게 이것저것 뜯어보고 만져보고 했지만, 나중에는 **&apos;이 복잡하고 어려운 서비스를 우리가 과연 효율적으로 잘 사용할 수 있을 것인가..?&apos;**에 대해서 심각하게 고민하게 되었습니다. 저를 포함한 3명의 QA 엔지니어들은, **&apos;2019년 4월 업데이트에 실사용&apos;**이라는 목표를 세우고 이 무시무시한 괴수를 어떻게 길들일지 논의하였습니다.&lt;/p&gt;
&lt;figure style=&quot;max-width: 200px; margin: 1em auto&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 600px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 158.88888888888889%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAgCAYAAAASYli2AAAACXBIWXMAABYlAAAWJQFJUiTwAAAHtUlEQVR42n2VeWyU1xXFBwgQg42NPZ59H8/iGY89491jjLHxPjaLKTWOwQbCeAO8BhsTDAQKKTRRE6ChKklLCJCCbVwgFNRWapN0UVepqZo/oqqqFLUioSklJSXe+PXOOEKgSP2kpzffe987755z7r2jODn2C7YcukDn8xdpP3yO8KEzdH/jHPuOf59jr/6AgWNnaRs6Qbj3COGd+wnv2Ed4+7OEOwdp7Rigtb2Pto5+mja28Z3Tr6OoHz7H3MJ+klbuJq6oD3NNP9nrB8hfP8iKpt2UhZ8nLdRHrLGYRZpsFqsyiVX6iE10E5vgYHG8mSVLLSgUS2jZvB1F8+GLLK0exrrmOQyrDhBs6MeXV01uxSYyShrJqtpKefgIzmWb0LlK0UeGYxkGex4Gaw56cwCTNUCsAHdu34Vi08E3SSjfg6luH/qqIax1g1gL6klYasSgd2Ky+3EVrMNf3YnWVYnWWYrWthydJQ+tKRONwYfelEFMrIF2oa7Y9Nx5EsqGMNcNYw4NoCnvI9A4TEX1eqpDTThzqjG7C8mt68KWtQ6VvRhtynI01ghgAI0+TQB9xCzWR7UUwHMslQjNq/dirB1AV9mHv2GIwpJ6UgIVLNW4hV42vtKNZNftQOcuI9GYR5IxMwqoM6Y/BGzv6IsAvkFixQDmNUMYQ7uEdj/2ULdEkY1S58HoLcGeXSu65ZBT205uqA3/igbSg3VC149K7UZr8PLkYh1t7b0oWg58D2VZr2g3gLG6H71E6KzajtUdRC23a0V8S0YVWkuAQGkjJQ27CO87RceeY6T6SwkEQ7jSljF/oUooS4TN+06TVLITa80zmKt60Zf1kLG6C4szF5XRh9bsjwKrjV7SSxooXNVOcHUna9sO0HPkFV5+8xqe9ELmPZFER6douPnZb5Nc1I69qgdz2XZSqvrIrGxBo3Wi1nvQSVpoLX40AphZtlHoNuLIqiQv1EH3C+c4dOo8VpEnLsFCuLULxZbdJ1EHt+Go3ImxuJ3i5mEyl61CKWboJDqdPReNzMl6NyaRwZ5Zidm7HIe/gtKGHoaOn2XH3hcxufLZvfeIAA4eR5PbjL2kFXdVJ7k1zeglIr05QleGNRNPVgmurArsGeUYnAVYBNDsKcKdv46NA99k6KWzrG8bYvDZr4kpPV9HlbEOm1RCakkzKRlFqA2p6EweVDpJGWsG3kAxOWVNpJdKtZjFKBlW30qc2SExaYDhb43SJHXd07cHRcfgC1gCtTgKNuARQJuvkGStQxLWJRq6ZsHNXhxpBWSubMbsyCUpySiV4sedU0tg5Wae3nOS+i09dPUOoejac5REnRO7twBbWiFqcwbJapvklltAZZjS0Mi+RufC7ishNbeWFN9yrELbk7+GrIpWSurbZS1IOCymbN0xLN3DTrJKz9JENc7MFaKfB6WAJqtTouCRoVRZoxF78lfjF+o5FZtJL/oKWZVhgjUt5Jas5diJ0yie7tjFEmWKRGAlMUkrzSANg9mNUmkiOdkSBVKqZdYIsMYhdR2MRhqh68isxrOsgaI1O6nY0MXQ/mMoNrZ0Ep+UgtGSQZJSJ1GqBFgjOulQaayoVAKWbJ6NUmalyiYl58XoyBM21fhXbiGrfBuW1OW0tu6QWm5uZ4kkpVGc04kBKpUpCqzR26Sk8iRSi8hhE3AHWkluteipUtvRSzo5skKS7FvJWNaIv3AVzwzuQ9G0qZW4eOl9Ji96OaCX27VigE5MCQSrUInjSckmKT0PNu8KTOJyJJ0snuWiZQup+Wtx+MqwppVz8OgJFI1PbWVRrDYaXcRJrcETnVVqB9nFa7DY06NU1bKmt+VgSMnB5IzQrREty6LUfQVrcQU3cPilMwLYuIWYRWqJSmpXoomMSJrUb+ulpkWabVGdmOR5SNPkLsIgIDqb1Li0fotUjE+cTzbn0r37KIoNjc3ExCSiFhdVInx8vI6Cqq+yQdLJIylkSc2WdWs0uXVSijpbpmhqjbpudAVx+svx5tSglIbbPXgYxVNNzcTGJaLX29HIRzq9g2BtM66MQgwGu4AZ0Gqtom8k0YWBOK+JXCBJb5GGYPcWCWAIlTTb7r79KEKh1fIXqGDBgljmKBYSr7Rh8hYyZ84C5s5bKH0uhnnzFsvvJcydGyfvcdH3+fPjeTJWz6IEK8mGLJ6Ic7A1LA323Xd+zoULFxkdvczIpVFGx69z7coVxkbHuDQyxkh0yN7IOOPjVxmR7y7Jd5H10cvXomP8yk1Gxm/w29/9EQWPPA++mD/8D/x3mi89n92fYHpmhv/3KKanp5mammJiYkpeZ/jJe3eJafuI1oszTE1PMjM9xf3PP2f8h29z5cY7/P3WbW59/Al3/v0pH/z1Qz65c5fJKcEQnOnpmdkIZ74I7Vd/A9PeCQ5evUf5Kdh6YXZjSg68fvEG49ff5vzYjzh7SShe+2l07eXTI3LhxCzDBw8eB/zTPyD7RegamSRw8A67r85u3PvsPq9duM6r567xypnLvPf+X6JgH93+FydfG+Pup/ceB3wU9IPb4Do4QffZ2w91GX3rZ1y5+S6/+cP7vPXjX/LxP+/w69//mZPfHeONkZtMTk59GfBR0Ft34f7kg4frkQMzMw8eEz9y+L6YFJkfNeV/Y+g5Nvs1glMAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/8c0830a03a6ea5680c584411dcfae152/33ecc/jira-gojira.webp 360w,
/static/8c0830a03a6ea5680c584411dcfae152/263a4/jira-gojira.webp 480w,
/static/8c0830a03a6ea5680c584411dcfae152/411c4/jira-gojira.webp 600w&quot; sizes=&quot;(max-width: 600px) 100vw, 600px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/8c0830a03a6ea5680c584411dcfae152/53918/jira-gojira.png 360w,
/static/8c0830a03a6ea5680c584411dcfae152/9aebd/jira-gojira.png 480w,
/static/8c0830a03a6ea5680c584411dcfae152/dface/jira-gojira.png 600w&quot; sizes=&quot;(max-width: 600px) 100vw, 600px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/8c0830a03a6ea5680c584411dcfae152/dface/jira-gojira.png&quot; alt=&quot;jira gojira&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;figcaption&gt;실제로 JIRA 서비스명은 &apos;고지라&apos;에서 따왔다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h3 id=&quot;1-운용-방식-board-template&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EC%9A%B4%EC%9A%A9-%EB%B0%A9%EC%8B%9D-board-template&quot; aria-label=&quot;1 운용 방식 board template permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;1) 운용 방식 (Board Template)&lt;/h3&gt;
&lt;p&gt;기존처럼 칸반(Kanban) 방식을 유지할 것인가, 아니면 새로운 방식을 사용할 것인가에 대해서 논의한 사항입니다. 결과적으로 저희는 칸반 방식을 유지하기로 하였습니다. 우선 QA 팀을 포함한 프로젝트 내 모든 인원들이 트렐로에 매우 익숙해져 있고, 새로운 툴에 적응하는 데에도 스트레스가 있을 텐데 BTS의 처리 방식과 UI가 기존과 다르다면 익숙해지는데 더 많은 시간이 들 것이라 예상했습니다. 또한 칸반 자체가 직관적이고 리드미컬한 이슈 처리가 가능했기 때문에 협업에 더 유리할 것이라는 판단을 하였습니다.&lt;/p&gt;
&lt;h3 id=&quot;2-워크플로-workflow&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%EC%9B%8C%ED%81%AC%ED%94%8C%EB%A1%9C-workflow&quot; aria-label=&quot;2 워크플로 workflow permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;2) 워크플로 (Workflow)&lt;/h3&gt;
&lt;p&gt;트렐로를 사용할 때에 저희는 &lt;strong&gt;&apos;등록이슈&apos; → &apos;수정완료&apos; → &apos;QA완료&apos;&lt;/strong&gt; 라는 매우 단순한 이슈 상태만 각 열에 지정해두고, &lt;strong&gt;&apos;수정 중&apos;&lt;/strong&gt;, **&apos;QA 확인 중&apos;**과 같은 상태는 레이블로 대체하여 운용하였습니다. 그러나 칸반은 카드를 드래그 &amp;#x26; 드롭하여 신속하게 이슈를 처리하는 데에 목적이 있는데, 카드에 일일이 레이블을 달거나 변경하는 작업은 비효율적입니다. 따라서 저희는 기존에 레이블로 처리하던 방식까지 모두 열(List) 단위로 세분화하여 워크플로를 설계하기로 하였습니다. 이렇게 하면 굳이 카드를 팝업 하여 레이블을 입력하는 것 대신 마우스로 카드를 끌어다 놓기만 하면 되어서 신속하게 이슈의 상태를 변경할 수 있습니다.&lt;/p&gt;
&lt;p&gt;추가로 쿠키런 오븐브레이크 개발 프로세스 내 특수한 상황들을 워크플로에 반영하기로 하였습니다. 이슈인 줄 알고 등록하였으나 기획서에 해당 내용이 명시된 경우(이슈아님), 이슈로 등록했으나 담당자들의 합의를 통해 이를 **사양(Spec)**으로 변경하는 경우(사양으로 변경), 중복으로 등록된 이슈인 경우입니다(중복이슈). QA 완료 외 3가지의 &lt;strong&gt;&apos;처리 완료(Close)&apos;&lt;/strong&gt; 상태를 추가함으로써, 좀 더 저희 팀에 최적화된 처리 절차를 구축할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;마지막으로 &lt;strong&gt;&apos;보류&apos;&lt;/strong&gt; 상태를 추가하여 현재 업데이트에서 수정하기에는 시간적 / 인적 리소스가 부족하거나 생각하지 못한 엣지케이스 같은, 스펙 확정의 시간이 걸리는 이슈의 경우 보류 처리하여 장기적으로 관리해야 할 이슈의 상태를 따로 만들어 두었습니다. 이들은 30일 스프린트마다 수정 가능한지 확인하면서 처리하기로 하였습니다.&lt;/p&gt;
&lt;p&gt;최종적으로 쿠키런 오븐브레이크의 워크플로를 다이어그램으로 나타내면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2070px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 44.44444444444444%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAYAAAAywQxIAAAACXBIWXMAABYlAAAWJQFJUiTwAAABTklEQVR42m1STUsDMRDNz/foxYM3r3osUtCTheKhKIpCRatgaa3a7na73W42X5Nnkm3j9mNgITuZN2/eyzDshLUWxlA4ExGUUtDahPx+bf01g5HLkKUIMMZASBWbK6UxTTIYoj3iQ8EOTbhb7Kfc5D2hJ1GywnAm8DLi0FTf+eFY++MRnc8+BK8COJ3nmPzMAmiRFyGnNTn5NYkQApWQ4dy+y3B6NQWXFC1gnWEfvdEA1knyEDIKpGVouCq5Lzs4dVP2OE9wdNvC2VMHzDpmI1U0921coveaRdC80OgNCiy5Cc2J/gmMOwuhkC4yjJcpflc5WMk5pPOI1h1vnnOcd9P4//5dBVmTtJa5+zh7j+JNpkZRXggkWQmKCi20rLZAHuP9SXmBr+U8kG+mZs2d8nHRTXDcmqyNrmVJtb2HXhXc9eX7A07ur1Hp9ZoB+APZs7wTKJL69gAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/30f64adc267fd9e346606b2e25315b43/33ecc/cro-workflow.webp 360w,
/static/30f64adc267fd9e346606b2e25315b43/263a4/cro-workflow.webp 480w,
/static/30f64adc267fd9e346606b2e25315b43/0c8fb/cro-workflow.webp 640w,
/static/30f64adc267fd9e346606b2e25315b43/521f7/cro-workflow.webp 860w,
/static/30f64adc267fd9e346606b2e25315b43/cbd37/cro-workflow.webp 1200w,
/static/30f64adc267fd9e346606b2e25315b43/74a96/cro-workflow.webp 2070w&quot;
              sizes=&quot;(max-width: 2070px) 100vw, 2070px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/30f64adc267fd9e346606b2e25315b43/53918/cro-workflow.png 360w,
/static/30f64adc267fd9e346606b2e25315b43/9aebd/cro-workflow.png 480w,
/static/30f64adc267fd9e346606b2e25315b43/0f09e/cro-workflow.png 640w,
/static/30f64adc267fd9e346606b2e25315b43/3e3fe/cro-workflow.png 860w,
/static/30f64adc267fd9e346606b2e25315b43/64756/cro-workflow.png 1200w,
/static/30f64adc267fd9e346606b2e25315b43/e584c/cro-workflow.png 2070w&quot;
            sizes=&quot;(max-width: 2070px) 100vw, 2070px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/30f64adc267fd9e346606b2e25315b43/e584c/cro-workflow.png&quot;
            alt=&quot;쿠키런 오븐브레이크의 워크플로우&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;쿠키런 오븐브레이크의 워크플로우&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;보시다시피 **&apos;수정 필요&apos;**의 **&apos;모두&apos;**를 제외한 나머지 상태들은 이전 상태로 돌아갈 수 없도록 설정하였습니다. 이는 화살표가 일방적으로 다음 상태로만 이어진 것으로 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;프로젝트 보드 등록된 모든 사람들에게는 이슈 카드의 상태를 전환시킬 수 있는 권한이 부여되어 있습니다. 따라서 실수로 카드의 상태를 변경하게 되면 상태 기반으로 커뮤니케이션하는 QA 팀 특성상 해당 카드의 상태를 잘못 인지하게 되고, 이를 바로잡는데 추가적인 리소스가 들게 됩니다. 이런 인적 오류를 최소화하고 싶었기 때문에 정해진 순서대로만 카드가 이동하도록 설정하였습니다.&lt;/p&gt;
&lt;p&gt;다만 **&apos;수정 필요&apos;**의 경우는 여러 가지 돌발적인 상황에 대비하기 위하여 일부러 모든 상태에서 다시 수정 필요 상태로 돌아갈 수 있도록 하였습니다. 개발 및 QA를 하다 보면 이슈가 수정된 줄 알았으나 실제로는 수정되지 않은 경우가 자주 발생했기 때문에, 손쉽게 다시 수정 필요 상태로 돌릴 수 있어야 했습니다.&lt;/p&gt;
&lt;p&gt;모든 이슈들은 수정 필요 상태로 돌아가면 다시 다른 상태로 변경이 가능합니다. 수정 필요 상태는 이슈의 상태를 &apos;초기화&apos; 시키는 것이라고 생각하면 편할 것 같네요. 심지어 이미 완료 처리된 이슈들도 다시 수정 필요 상태로 전환시켜 다시 처리할 수도 있습니다. JIRA 초기 설정 시 상황에 따라 가변적이고 유연하게 대처해야 하는 게임 소프트웨어와 팀 내 문화를 보장할 수 있도록 고려한 부분 중 하나입니다.&lt;/p&gt;
&lt;h3 id=&quot;3-이슈-유형-issue-type&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%EC%9D%B4%EC%8A%88-%EC%9C%A0%ED%98%95-issue-type&quot; aria-label=&quot;3 이슈 유형 issue type permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;3) 이슈 유형 (Issue Type)&lt;/h3&gt;
&lt;p&gt;**&apos;이슈 유형&apos;**은 이슈의 종류를 말합니다. JIRA에서는 사용자의 니즈에 따라 이슈 유형을 추가할 수 있어서, 회사 사람들이 만들어둔 이슈 유형이 엄청나게 많았습니다. 저희는 복잡하게 모든 이슈 유형을 사용하기 보다는 가장 자주 쓰는 유형인 **&apos;버그&apos;**와 &lt;strong&gt;&apos;개선&apos;&lt;/strong&gt; 두 가지만 사용하여 간소화하기로 하였습니다.&lt;/p&gt;
&lt;figure style=&quot;max-width: 300px; margin: 1em auto&quot;&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 654px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 133.33333333333331%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAbCAYAAAB836/YAAAACXBIWXMAABYlAAAWJQFJUiTwAAADKUlEQVR42p2Va4/bRBSG/f//AhKV+ARIBSEkRLddoAgte2tD6jhOHN/vdny/xBN7q5czs6WqUHc37odHZzxRjt9zzvgdKYtqLH7JYHshNMOBbrpo2haMMQzDMBupKnqsrw8I41Qk3Ows1E0jfjwcDrMQCds4RH32HKbti4SaYaMsqy9P2Bc5mqsL7LMCpuNjo9mo6uaLSub/kYKiwY8Wg6LqIuHdNIm39X3/KA8qLJsO74IaSZohTvYYj0eM4xHTNGKiKJ4/MI2j+G2k+LmWiIRZWOPt84KGYWO9NQgT/8hbLIgVqVa1+2exv9Ig0x6PVfX5tkhN1cLeZoiiFEGYkEpa08QtJ4DrR0J1xBF7voge7fOj9X+VImGYezgzv8XOcMWR4efx7m4SZYkSPy15Gj+WztgD57BuK1jpjkqokeUlTTs/aSgPDUqKCg/n9vcwLB+XtzLSfS7U/VfC7C+l6Vo4WYqiqIgSBR3qtutmH+qPPQyyGt8tS9huAGWj49fzv+GHMalkVAZP3J/MMFDJ7aFD3MYo2w553VLsRewGhgMNYA4DIUXBDr/LX0NbLKFc3cJYyrj87U+Uro0xizGkEdgpJCFYkUHqlFuE2iUyVUGpqTg4hmCYhY7BtcC0FaTk5hVerL6CcvMWtqJiKvakKgHbzyCNwcocR21NU3Y0WMofKLQNss0a5W4D5ln0RnMmBphvQ4ojHWfyNzBXa2ypj6asYMz38xR+olIabv5C9/IFUlLXWLp4y4H6MQiVH/CegCsMXDB1SQZ79RrND8+gv1shMQ1MpI738ZglYj3mKfU0fpwkwpBnYDvqYaXb2P60RSovYd9cI1jQcK6vUagrWr9BtFxg5P2x9cexdlSdAykvBsgXEzQnxNrw4JF9be0AZpBA0V0YPvXm/Xv0Izn59DgDIbUktz7/GQl5nk0eaFqecA5G3+WRLIrHnn/b3E2eYOBu01OfuovX8EgRd2VVs9A0rfC+I7u/Drj3nWwOUdnilc9g0AXPr1Jusjtac/vXdEfYP7c0nrQ/JWHR9FCTDn4Qk8sk4uaz3RAOOXdI1wJPnpOt3StlT/IvOGX0HafuzN0AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/5f7df5f8f004db0ee3cf082f5169cfe6/33ecc/jira-issue-types.webp 360w,
/static/5f7df5f8f004db0ee3cf082f5169cfe6/263a4/jira-issue-types.webp 480w,
/static/5f7df5f8f004db0ee3cf082f5169cfe6/0c8fb/jira-issue-types.webp 640w,
/static/5f7df5f8f004db0ee3cf082f5169cfe6/c0e87/jira-issue-types.webp 654w&quot; sizes=&quot;(max-width: 654px) 100vw, 654px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/5f7df5f8f004db0ee3cf082f5169cfe6/53918/jira-issue-types.png 360w,
/static/5f7df5f8f004db0ee3cf082f5169cfe6/9aebd/jira-issue-types.png 480w,
/static/5f7df5f8f004db0ee3cf082f5169cfe6/0f09e/jira-issue-types.png 640w,
/static/5f7df5f8f004db0ee3cf082f5169cfe6/ba4a2/jira-issue-types.png 654w&quot; sizes=&quot;(max-width: 654px) 100vw, 654px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/5f7df5f8f004db0ee3cf082f5169cfe6/ba4a2/jira-issue-types.png&quot; alt=&quot;jira issue types&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;figcaption&gt;이 많은 유형 중 두 개만 사용 (밑으로 더 있음)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;**&apos;버그&apos;**는 말 그대로 게임 시스템(기능)과 그에 관련된 환경들(비기능)에 대한 결함 / 장애들을 뜻합니다. 버그에 대해서는 더 이상 설명할 필요가 없을 것 같네요.&lt;/p&gt;
&lt;p&gt;**&apos;개선&apos;**은 버그는 아니지만, 유저 관점에서 경험이 좋지 못하거나 사소하게 수정이 필요한 부분에 대해서 등록합니다. 버그보다 우선순위는 낮지만 수정되면 더 편리하고 멋있어 보일 수 있는 부분들에 대해 담당자가 한번 생각해 볼 수 있는 문제들을 제시하게 됩니다. 단순히 테스팅이 아닌 정말로 게임의 품질을 높이기 위해서는, 제 생각엔 버그만큼이나 중요한 이슈 유형이라는 생각이 듭니다.&lt;/p&gt;
&lt;h3 id=&quot;4-이슈-화면-screen&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#4-%EC%9D%B4%EC%8A%88-%ED%99%94%EB%A9%B4-screen&quot; aria-label=&quot;4 이슈 화면 screen permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;4) 이슈 화면 (Screen)&lt;/h3&gt;
&lt;p&gt;이슈 화면은 카드의 상세 정보가 팝업 되었을 때, 해당 화면에서 어떤 정보들이 보이게 할지 결정하는 것입니다.&lt;/p&gt;
&lt;p&gt;대부분의 정보는 이슈 보고자가 입력하게 됩니다. 이슈 유형과 마찬가지로 이슈 화면도 JIRA에서 제공하는 필드가 매우 다양합니다. 모두 쓰고는 싶지만, 이 수많은 필드들을 사용하게 될 경우 직관적으로 이슈의 정보를 파악하기 힘들어질 것입니다. 정말 정말 필요한 정보만 한눈에 딱 보여주고, 굳이 몰라도 되는 (사실 알아도 의미 없는) 정보들은 가리기로 하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1568px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 71.66666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAAAsTAAALEwEAmpwYAAACD0lEQVR42pVTy27TQBT177DmB/oH7ADxAag7fgFW8AGtBDsWgKgQEmVBShvHhi0CtQhFoopEXnL9fsWPscf24c7YSZsUBRjpaO68zpx75o5iOz5My4Nx4UDE07kJ027Hc8PGhekiCGP4QST7pmmwbDkv8OzbCfa/niAtmJxTWFGirhuJilDyCtSB81qOrxJstozVOJ8ESJMFCuKRhAcHb/Du8D36/T7UDiIeqJexQK/Xw3A4lIeWlxQlh2EHYEVBguqW8MPRR/SOjqHpn6CqAww0jaBDHWgyVgmarhOpitFotKawLEu4nkeZ1Ks5xTRt8tBBHCckm8tFiarteVWt4s3ka5q3XPI3ymB5CYI4h7JIM6SsBKcDUvbKs6aL275prvtZ0WWMtd7Jd6B15e3L13jxdA+Hr57Dtqw1j/7WBKHrhchZcZny4/u7uHNzB/du7OD72Wl3W/1PhGJftEgl8Ypw79FDPLh1G0/u7mIy/vWfCmsEUbK2X3Edl4rXghv45Aej+uMS4gVLKott5ELhghRezUiZ0m9QP3/B2Y9z+gkRHNeD5wX0a1yENN6qltaiKALriloS6qc2fo7GGE9mmM0NienMQJK23gjUXQVsQmTi+aH8kktSxfUDqSSMYuQ5IxTIslymK2pwacGfINRHaQGP6jBnvCW0HYvUZFJRyfm1lLY1QZhkjM7myEiMEPAbRTknUcse6qMAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/569c6b1c9fb9e87bf5afb96fdde87598/33ecc/jira-issue-screen.webp 360w,
/static/569c6b1c9fb9e87bf5afb96fdde87598/263a4/jira-issue-screen.webp 480w,
/static/569c6b1c9fb9e87bf5afb96fdde87598/0c8fb/jira-issue-screen.webp 640w,
/static/569c6b1c9fb9e87bf5afb96fdde87598/521f7/jira-issue-screen.webp 860w,
/static/569c6b1c9fb9e87bf5afb96fdde87598/cbd37/jira-issue-screen.webp 1200w,
/static/569c6b1c9fb9e87bf5afb96fdde87598/7ac34/jira-issue-screen.webp 1568w&quot;
              sizes=&quot;(max-width: 1568px) 100vw, 1568px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/569c6b1c9fb9e87bf5afb96fdde87598/53918/jira-issue-screen.png 360w,
/static/569c6b1c9fb9e87bf5afb96fdde87598/9aebd/jira-issue-screen.png 480w,
/static/569c6b1c9fb9e87bf5afb96fdde87598/0f09e/jira-issue-screen.png 640w,
/static/569c6b1c9fb9e87bf5afb96fdde87598/3e3fe/jira-issue-screen.png 860w,
/static/569c6b1c9fb9e87bf5afb96fdde87598/64756/jira-issue-screen.png 1200w,
/static/569c6b1c9fb9e87bf5afb96fdde87598/00f4c/jira-issue-screen.png 1568w&quot;
            sizes=&quot;(max-width: 1568px) 100vw, 1568px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/569c6b1c9fb9e87bf5afb96fdde87598/00f4c/jira-issue-screen.png&quot;
            alt=&quot;JIRA 이슈 화면&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;JIRA 이슈 화면&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;이는 총 11개로, 필드는 다음과 같습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;요약 (이슈 제목)&lt;/strong&gt;: 이슈 전체 내용을 요약하여 하나의 간단한 문장으로 제목을 지정합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;보고자&lt;/strong&gt;: 이슈를 보고하는 사람입니다. 생성하는 사용자가 자동으로 등록되도록 설정하였습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;담당자&lt;/strong&gt;: 이슈를 수정할 사람입니다. 해당 프로젝트에 등록된 사용자를 기반으로 등록할 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;그룹&lt;/strong&gt;: 해당 이슈를 직군 전체에서 봐야 하는 경우, 미리 지정된 그룹을 등록할 수 있습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;설명&lt;/strong&gt;: 이슈의 상세 설명을 기재합니다. 저희는 테스트 환경/이슈 설명/기대 결과를 작성합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;우선순위&lt;/strong&gt;: 이슈의 수정 우선순위를 등록합니다. &lt;code&gt;Lowest&lt;/code&gt;/&lt;code&gt;Low&lt;/code&gt;/&lt;code&gt;Medium&lt;/code&gt;/&lt;code&gt;High&lt;/code&gt;/&lt;code&gt;Highest&lt;/code&gt;의 5단계를 사용합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;레이블&lt;/strong&gt;: 이슈의 추가 정보를 나타내는 레이블을 등록합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;첨부파일&lt;/strong&gt;: 이슈의 상태를 설명할 수 있는 스크린샷이나 동영상을 첨부합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;영향을 받는 버전&lt;/strong&gt;: 해당 이슈가 발생하는 버전을 기재합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;수정 버전&lt;/strong&gt;: 이슈가 수정된 경우, 해당 이슈가 수정된 버전을 기재합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;연결 이슈 (관련된 이슈)&lt;/strong&gt;: 어떤 이슈에서 파생되었거나, 영향을 주는 경우 연결된 이슈 번호를 기재합니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;이 이외의 컴포넌트, 에픽, 작업 로그, 시간 추적과 같은 봐도 잘 모르겠는 정보들은 과감하게 포함하지 않았습니다. BTS의 최초 사용 목적인 직관적으로 일감을 식별하는 효율성을 확보하기 위해 복잡함보다 심플함을 선택한 것은 좋은 결정이었다는 생각이 듭니다.&lt;/p&gt;
&lt;h3 id=&quot;5-레이블&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#5-%EB%A0%88%EC%9D%B4%EB%B8%94&quot; aria-label=&quot;5 레이블 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;5) 레이블&lt;/h3&gt;
&lt;p&gt;기존 트렐로에서는 레이블을 통해 이슈의 상태를 추가적으로 보여주었으나, 이는 JIRA에서 워크플로로 대체되었기 때문에 저희는 레이블을 다른 방향으로 사용할 수 있었습니다. 따라서 저희는 이 레이블을 이슈와 관련 있는 콘텐츠 정보 표시로 사용하였습니다.&lt;/p&gt;
&lt;p&gt;초반 설계에서는 직군, 콘텐츠 카테고리를 매우 세분화하였습니다. 예를 들면 &lt;code&gt;클라이언트&lt;/code&gt;, &lt;code&gt;쿠키&lt;/code&gt; 등으로 레이블을 지정한 것을 들 수 있습니다. 특히나 콘텐츠 카테고리는 게임 시스템 내에서도 경기, 떼탈출, 기억의 섬, 우정런, 인게임, 쿠키, 펫, 보물, IAP, 플랫폼, 계정 연동 등등 세분화할 수 있는 요소가 많았기 때문에 새로운 콘텐츠마다 이를 나누어 레이블을 부여했습니다.&lt;/p&gt;
&lt;h3 id=&quot;6-기타&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#6-%EA%B8%B0%ED%83%80&quot; aria-label=&quot;6 기타 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;6) 기타&lt;/h3&gt;
&lt;p&gt;이 이외에도 저희는 논의를 통해 세부적인 사항들까지 의사결정하여 반영했습니다. 너무 많아서 다 기억은 안 나지만 몇 가지 기억나는 것들을 한번 적어보려 합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;이슈의 제목 (요약):&lt;br&gt;
제목은 한눈에 어떤 이슈인지 파악할 수 있도록 하는 매우 중요한 부분입니다. 따라서 저희는 대 괄호 안에 해당 이슈의 콘텐츠를 적고, 그 뒤에 이슈의 설명을 적는 방식으로 결정하였습니다. 예를 들자면 다음과 같습니다:&lt;br&gt;
&lt;code&gt;[용감한쿠키] 용감한 쿠키 점프버튼 터치 시, 크래시 나는 문제&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;완료된 이슈의 처리:&lt;br&gt;
완료된 이슈는 히스토리 관리를 위해 &apos;처리 완료&apos; 열 안에 계속 쌓아두기로 하였습니다. 일일이 이슈를 찾는 것보다 리스트 안에 두는 것이 관리 측면에서 더 효율적일 것이라 생각했기 때문입니다. 따라서 버전이 올라가면 올라갈수록 &apos;처리 완료&apos; 열에는 이슈가 계속 누적되어 나타나게 됩니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;가이드북 제작:&lt;br&gt;
JIRA는 저희에게도 매우 복잡한 서비스였기 때문에, 다른 직군에서 사용하는데 어려움이 있을 것이라 생각하고 가이드북을 제작하고 배포하기로 하였습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;이슈의 알림:&lt;br&gt;
이슈는 메일링으로 받고 싶지 않다는 개발자들의 의견을 수용하여 (메일함이 지저분해지므로) 회사에서 사용하는 Slack 메신저를 통하여 알림을 받도록 설정하였습니다. 다만, 개개인이 일일이 설정해야 하기 때문에 위 가이드북에 설명을 기재하여 배포하였습니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;5-대망의-릴리즈--그러나-끝나지-않은-작업&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#5-%EB%8C%80%EB%A7%9D%EC%9D%98-%EB%A6%B4%EB%A6%AC%EC%A6%88--%EA%B7%B8%EB%9F%AC%EB%82%98-%EB%81%9D%EB%82%98%EC%A7%80-%EC%95%8A%EC%9D%80-%EC%9E%91%EC%97%85&quot; aria-label=&quot;5 대망의 릴리즈  그러나 끝나지 않은 작업 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;5. 대망의 릴리즈 : 그러나 끝나지 않은 작업&lt;/h2&gt;
&lt;p&gt;드디어 모든 초기 설정이 끝이 났습니다. 처음 사용해보는 툴이라 복잡한 설정과 JQL 등 어려운 난관이 많았지만, 많은 분들의 도움과 구글링으로 처음 치고는 그럴듯하게 BTS 환경이 구축되었습니다.&lt;/p&gt;
&lt;p&gt;사용자 등록과 가이드북 배포, 기존 트렐로에서 History를 옮겨오는 후속작업까지 완료하고 2019년 4월 정식으로 쿠키런 오븐브레이크 팀에서 JIRA를 사용하게 되었습니다! (짝짝짝~)&lt;/p&gt;
&lt;p&gt;생각보다 순조로운 출발에 저희 QA 팀은 안도의 한숨을 내쉬었습니다. 이슈를 올리고, 수정하고, 처리하는 과정이 트렐로보다는 무거웠지만 그래도 큰 어려움 없이 진행되는 것을 보며, 직관적으로 환경을 설정하는 것이 정말로 중요하다는 것을 다시 한번 깨닫게 되었습니다. 그 외 어려운 부분들은 저희 팀에서 직접 튜토리얼과 커뮤니케이션을 통해 해결했고, 반대로 사용자 입장에서 받을 수 있는 사용성 피드백들을 받았습니다.&lt;/p&gt;
&lt;p&gt;사실 초기 시스템 구축은 **&apos;이렇게 사용할 것으로 예상된다&apos;**와 같은 추측에 가까웠다고 해도 과언이 아닙니다. 저희도 릴리즈 직전까지 결정할 사항들이 산더미 같았지만, 결국에는 **&apos;써보고 수정합시다!&apos;**로 결정한 이유는, 사용해보고 개선하는 것이 사용 전 추측으로 구축하는 것보다 더 효율적일 것이라 판단하였기 때문입니다. 게다가 사내에서 사용하는 서비스이므로, 차분히 개선할 수 있는 시간적 여유도 있었습니다. (우리끼리 사용하는 것이니 조금 불편해도 참아주시겠지..?)&lt;/p&gt;
&lt;p&gt;결과적으로 충분한 사용 후 받은 피드백들이 영양가 있어서 좋았습니다. 처음엔 편리할 것이라 생각하여 추가한 기능이 실제로는 비효율적인 부분도 있었고, 릴리즈 하고 보니 조금만 더 개선하면 좋을 것 같은 미숙한 부분들도 계속해서 튀어나왔습니다. 특히나 QA 팀에서 생각하지 못한 부분들을 다른 직군의 팀원들께서 짚어 주시는 부분들이 굉장히 많은 도움이 되었습니다.&lt;/p&gt;
&lt;p&gt;그리하여 저희에게는 &apos;불편한 점들을 개선하라!&apos;는 미션이 다시 주어졌습니다. 흐규...&lt;/p&gt;
&lt;p&gt;2019년 4월부터 지금까지 JIRA를 약 1년간 사용하면서 저희는 총 25건의 크고 작은 개선사항이 있었습니다. 이를 체크리스트에 등록하고 시간이 될 때마다 조금씩 개선해 나갔습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 796px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 88.88888888888889%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAYAAABb0P4QAAAACXBIWXMAAA7DAAAOwwHHb6hkAAABsElEQVR42pVUAW7DIAzk/7+stnVTlaarkgawSZjPYBKlaqNFOkEUOO58Do4i5XF85GEYMjFne5ZlyUfPPM85EmUScEoKd+GQfYhKBpL/AqQABGF05xRzFDLipKQsY0qzvuPEJIvePUZ67Xudu1OY8uhDngQPjKLWQMR6iIEFCYfsQEy5u3Yqwp38mIfJK5lhnLbwOuKAIPWOcsgeKNnP5aJr3BdqGEnV4CM2AbALK2lrG0HtYJZ7saw1/GCvhCBTUpnjHR8t6aNQkHR/6/Vgd64KoUQXbBYL1XMLvTgEobCE6T6FMGgvpWKXitpmWW3Xsc7nTfIgw1oo1JRRwwmWtXax1dBI2dqpzg17hSCczXKgtQ+tllBy0ICb0mQhvAnhUkKZgiks8KK0hVR7sfxi3JrfVEMV3q+W8jmVUJRw01tQvO83qG7pblOOnK+wbDUMvFEjmA9+t/0FgQ7pVOGClKUPabXsa9KwlLjYoVRsWm8+9yHn22+t4RoKt1AKSJH0NikblzcXBEJpbePr5kZQawSio3sR32H5t+8K4Td5TXXtP1oTraPdea8Awvv9rqH9AT7iiT42eGeeAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/af6c11bc2ad9a0b5f951cdfda9572d9e/33ecc/jira-enhancements.webp 360w,
/static/af6c11bc2ad9a0b5f951cdfda9572d9e/263a4/jira-enhancements.webp 480w,
/static/af6c11bc2ad9a0b5f951cdfda9572d9e/0c8fb/jira-enhancements.webp 640w,
/static/af6c11bc2ad9a0b5f951cdfda9572d9e/c6463/jira-enhancements.webp 796w&quot;
              sizes=&quot;(max-width: 796px) 100vw, 796px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/af6c11bc2ad9a0b5f951cdfda9572d9e/53918/jira-enhancements.png 360w,
/static/af6c11bc2ad9a0b5f951cdfda9572d9e/9aebd/jira-enhancements.png 480w,
/static/af6c11bc2ad9a0b5f951cdfda9572d9e/0f09e/jira-enhancements.png 640w,
/static/af6c11bc2ad9a0b5f951cdfda9572d9e/c0214/jira-enhancements.png 796w&quot;
            sizes=&quot;(max-width: 796px) 100vw, 796px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/af6c11bc2ad9a0b5f951cdfda9572d9e/c0214/jira-enhancements.png&quot;
            alt=&quot;JIRA 개선사항. 현재까지 총 완료된 항목은 20건이다.&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;JIRA 개선사항. 현재까지 총 완료된 항목은 20건이다.&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;이 중 중요하다고 생각되는 몇 가지를 골라 한번 이야기해보겠습니다.&lt;/p&gt;
&lt;h3 id=&quot;1-프로필-이름-한글로-바꾸기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%ED%94%84%EB%A1%9C%ED%95%84-%EC%9D%B4%EB%A6%84-%ED%95%9C%EA%B8%80%EB%A1%9C-%EB%B0%94%EA%BE%B8%EA%B8%B0&quot; aria-label=&quot;1 프로필 이름 한글로 바꾸기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;1) 프로필 이름 한글로 바꾸기&lt;/h3&gt;
&lt;p&gt;JIRA는 기본적으로 영어 이름으로 등록되기 때문에 (Gunwoo Park 같은 식으로) 담당자를 검색하기가 조금 불편했습니다. 영어보다 그냥 &apos;박건우&apos;를 치는 게 더 빠르기 때문에 아틀라시안 계정의 이름을 전부 한글로 바꾸도록 팀원 분들께 요청하였습니다. 놀라운 건 보고자 및 담당자 검색을 한글로 할 수 있게 되면서 생각보다 업무가 훨씬 더 편리해졌다는 겁니다. 별거 아닌 것 같은 부분에서 생각지도 못한 효율성 상승을 이끌어 냈다는 점이 재미있었습니다.&lt;/p&gt;
&lt;h3 id=&quot;2-수정-버전-미입력-시-처리-완료close가-되지-않도록-수정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%EC%88%98%EC%A0%95-%EB%B2%84%EC%A0%84-%EB%AF%B8%EC%9E%85%EB%A0%A5-%EC%8B%9C-%EC%B2%98%EB%A6%AC-%EC%99%84%EB%A3%8Cclose%EA%B0%80-%EB%90%98%EC%A7%80-%EC%95%8A%EB%8F%84%EB%A1%9D-%EC%88%98%EC%A0%95&quot; aria-label=&quot;2 수정 버전 미입력 시 처리 완료close가 되지 않도록 수정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;2) 수정 버전 미입력 시, 처리 완료(Close)가 되지 않도록 수정&lt;/h3&gt;
&lt;p&gt;이는 QA 팀 내부에서 자주 하던 실수를 개선하기 위해 직접 수정하였습니다. 카드를 빠르게 처리하다 보면, 카드 내부의 &apos;수정 버전&apos;을 누락하고 처리하는 경우가 많았습니다. 당장 크게 문제가 되지는 않지만, 나중에 이슈들을 목록화하거나 테스트 리포트를 작성할 때 수정 버전이 누락된 이슈들을 빼먹는 경우가 종종 생겼습니다. 따라서 처음에는 일일이 찾아 누락된 수정 버전을 입력했지만, 이것 자체가 비효율적이었기 때문에 워크플로 자체에 수정 버전을 입력하지 않으면 처리 완료되지 않도록 설정하였습니다. JIRA 개선사항 중 가장 효율성이 높아진 사례로 꼽을 수 있을 것 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 637px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 50.27777777777778%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAA7DAAAOwwHHb6hkAAABbklEQVR42n1R2XKCQBDk/38nlfInjPEtUYECUTy4dpdzFejMrAZXK5WpanbYY6a7xxmGAb8YxxEcvNr4a8/et2s4fd9Ph2/zArOF4Gvo+0cTpRSklM+P6XzmLvHuLnB7jVtB/txixIdXYxk0986YGrVdh47wxI7weQwwP/iAXVCVFdJc0gMNrVvoroFQJUraZ/aw5HHTV8mvdjhN0yLNCpKkkAuFgsD/QpbotMb1eqVG2jDU+oKHImLEFliFORzuejjECLcRkiRDHO8RH07GN0FQqkIURQiCANtoj7Zt8V8Qw4ZYScNQ0CrIfP4vCkHFSlPARscwnrYoq2aCIrACJ01TfK99eH4I1wsMvlYeVhsPG8rPSYIsyyakKa+5yd3wDI8QxRl2xxxVVdNQSFaWC+z2J5IUww/3WLsBgm1MnWtire6Qxlf2mAfGuVQMhcvlauTyEB02vGk7ulTfCsjS5DxpewDji/n23jCMBjzlHw6bBXnRtlHGAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/1777e9e9984e721b402820a0a936329d/33ecc/transition-validation-1.webp 360w,
/static/1777e9e9984e721b402820a0a936329d/263a4/transition-validation-1.webp 480w,
/static/1777e9e9984e721b402820a0a936329d/bfe67/transition-validation-1.webp 637w&quot;
              sizes=&quot;(max-width: 637px) 100vw, 637px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/1777e9e9984e721b402820a0a936329d/53918/transition-validation-1.png 360w,
/static/1777e9e9984e721b402820a0a936329d/9aebd/transition-validation-1.png 480w,
/static/1777e9e9984e721b402820a0a936329d/90cda/transition-validation-1.png 637w&quot;
            sizes=&quot;(max-width: 637px) 100vw, 637px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/1777e9e9984e721b402820a0a936329d/90cda/transition-validation-1.png&quot;
            alt=&quot;처리 완료(Close) 전환(Transition)에 &amp;#39;유효성 검사&amp;#39; - &amp;#39;수정 버전 입력&amp;#39;을 추가하면 된다.&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;처리 완료(Close) 전환(Transition)에 &amp;#39;유효성 검사&amp;#39; - &amp;#39;수정 버전 입력&amp;#39;을 추가하면 된다.&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h3 id=&quot;3-필수-입력-정보-누락-시-카드가-생성되지-않도록-수정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%ED%95%84%EC%88%98-%EC%9E%85%EB%A0%A5-%EC%A0%95%EB%B3%B4-%EB%88%84%EB%9D%BD-%EC%8B%9C-%EC%B9%B4%EB%93%9C%EA%B0%80-%EC%83%9D%EC%84%B1%EB%90%98%EC%A7%80-%EC%95%8A%EB%8F%84%EB%A1%9D-%EC%88%98%EC%A0%95&quot; aria-label=&quot;3 필수 입력 정보 누락 시 카드가 생성되지 않도록 수정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;3) 필수 입력 정보 누락 시, 카드가 생성되지 않도록 수정&lt;/h3&gt;
&lt;p&gt;2번과 비슷한 문제인데, 카드를 생성할 때 필수 정보를 입력하지 않아 나중에 이슈가 완료 처리되고 나서 필수 정보를 부랴부랴 넣는 상황이 종종 발생했습니다. 따라서 이것도 카드를 생성하는 시점에서 필수 입력 필드를 지정하고, 해당 필드가 빈 값인 경우에는 카드가 생성되지 않도록 조치하였습니다. 2번 개선사항을 포함하여, 이제는 카드에 누락된 정보가 있는 경우는 발생하지 않습니다. 카드 정보가 정확한지 판별하는 시간과 리소스를 온전히 이슈 처리에 집중할 수 있게 되어 업무가 훨씬 수월해진 것 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 631px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 58.88888888888889%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAAA7DAAAOwwHHb6hkAAABk0lEQVR42pVTi26DMAzk//9u0rSpXVdSaAMFQhICAfq62eloqTRVa6STE5PY57OJzuczJlwul5cwrXmMaArEyzmHYRjCeZ6E/cbaRz8h31dYrBL0w3gL/BDweDxeL9N+/pj9h8PhgQmDk7etx+l04hfXgOM4onEtKmXCR2sdam3h+wH3op6vuQxRRrS3WRkCMmReoqw0atMQLNquxysrGqh+13boiVHXdcROo2kcOt8HlsN4eMIMtyqmHkXssiS44VLrGkIISClRluqhk/9myMEqRSVqE6xSNQoKVpQVVK1J1w7e+8B+Ds/VWNJeExFztS35IpnlWIsE3/EGq7WA2KRYxwmWqxixSCmJgiYZFLFnOTQl5rNrGuSlQSI1skJDEgxpHrnQYY19oVDVdJmawSjJx5a/yawgxjWqin0W2jb3EZvNK49PxKXudjlSuSe7J7ZbYpti8SWI8dW+vS/xuYyxpP3HIoZIdsSmCTIpmgxDCS7TYHOXfd+HrvK+p2HljrOPbdv5MKfMyvyyZ63++hU54A9gkKGgP2WHugAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/23ff1527ac5f329afa69f4fc7b4f9ecc/33ecc/transition-validation-2.webp 360w,
/static/23ff1527ac5f329afa69f4fc7b4f9ecc/263a4/transition-validation-2.webp 480w,
/static/23ff1527ac5f329afa69f4fc7b4f9ecc/265f3/transition-validation-2.webp 631w&quot;
              sizes=&quot;(max-width: 631px) 100vw, 631px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/23ff1527ac5f329afa69f4fc7b4f9ecc/53918/transition-validation-2.png 360w,
/static/23ff1527ac5f329afa69f4fc7b4f9ecc/9aebd/transition-validation-2.png 480w,
/static/23ff1527ac5f329afa69f4fc7b4f9ecc/077d3/transition-validation-2.png 631w&quot;
            sizes=&quot;(max-width: 631px) 100vw, 631px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/23ff1527ac5f329afa69f4fc7b4f9ecc/077d3/transition-validation-2.png&quot;
            alt=&quot;마찬가지로 생성(Create) 전환(Transition)에 &amp;#39;유효성 검사&amp;#39; - &amp;#39;[입력 필수 필드들] 입력&amp;#39;을 추가하면 된다.&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;마찬가지로 생성(Create) 전환(Transition)에 &amp;#39;유효성 검사&amp;#39; - &amp;#39;[입력 필수 필드들] 입력&amp;#39;을 추가하면 된다.&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h3 id=&quot;4-릴리즈된-버전의-카드는-보드에서-보이지-않도록-수정&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#4-%EB%A6%B4%EB%A6%AC%EC%A6%88%EB%90%9C-%EB%B2%84%EC%A0%84%EC%9D%98-%EC%B9%B4%EB%93%9C%EB%8A%94-%EB%B3%B4%EB%93%9C%EC%97%90%EC%84%9C-%EB%B3%B4%EC%9D%B4%EC%A7%80-%EC%95%8A%EB%8F%84%EB%A1%9D-%EC%88%98%EC%A0%95&quot; aria-label=&quot;4 릴리즈된 버전의 카드는 보드에서 보이지 않도록 수정 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;4) 릴리즈된 버전의 카드는 보드에서 보이지 않도록 수정&lt;/h3&gt;
&lt;p&gt;이는 한 개발자분의 JIRA 사용 중 불편한 점을 듣고 개선한 사례입니다. 위에서 설명했듯이 저희는 처음에 모든 카드를 한 보드에서 모두 관리할 수 있도록 처리 완료 열에 현재까지 생성된 카드들을 누적해왔습니다. 그러나 카드가 쌓이면 쌓일수록 안 그래도 무거운 툴이 점점 느려지더니, 나중에는 웹페이지 새로 고침이 안될 정도로 느려지기 시작했습니다. 트렐로에서처럼 칸반에 카드가 많으면 생기는 이슈일 것이라 생각하고 수정 버전에 입력된 버전이 릴리즈 된 경우, 해당 카드들을 보드에서 보이지 않도록 하였습니다. (실제로 사라지는 것은 아니고 보이지 않게만) 다행히도 카드가 열에서 사라지자 다시 JIRA 서비스가 쾌적해졌고 모두가 행복하게 잘 사용할 수 있었다는 해피엔딩으로 끝났습니다.&lt;/p&gt;
&lt;p&gt;위 개선사항을 적용하기 위해서 보드 설정의 &apos;보조 필터&apos; 기능을 사용하였는데, 이는 JQL을 이용해야 합니다. 코드의 코자도 모르는 제가 어떻게 JQL을 이용했을까요? 이는 JIRA 고급 검색의 &apos;JQL 변환&apos; 기능이 그 답을 주었습니다.&lt;/p&gt;
&lt;p&gt;보조 필터는 입력된 JQL에 해당하는 이슈만 보드에 보여주는 기능입니다. 릴리즈된 이슈들은 보드에서 안 보이도록 해야 하므로, 보조 필터에는 &apos;처리 완료된 이슈 중, 릴리즈되지 않은 이슈만 보여준다.&apos;라는 값을 입력해야 합니다. 따라서 고급 검색에서 &lt;code&gt;unreleased version&lt;/code&gt;을 체크한 다음, 이를 JQL로 변환 후 보조 필터에 그대로 복사 + 붙여넣기 하여 간단하게 필터를 구성할 수 있었습니다. 보조 필터는 따로 작업 없이 자동으로 동작하기 때문에 해당 버전을 릴리즈 처리하면, 릴리즈 대상 버전의 이슈가 한 번에 사라지게 되어 매우 편리했습니다.&lt;/p&gt;
&lt;figure&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1946px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 48.33333333333333%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAABcRAAAXEQHKJvM/AAABzklEQVR42o2Rv2sUQRzFVwPG4oI2wcLaPyOFWFoLV4iFCBLIWaTLP2Bj4TZprKxsLCIqGEGjQQubs0k6F45IzN7u3f6a2Z3Z2ZnZ53fnDuUSJD6YfcuXmQ9v3ngn4cSfJLnPSuFba32ttS+V8kshfCWVD+Apjo62sb6+9evN+ydv94d4+fqTfffxm8FMNz0S+VLnnlQGrZu3aFssyFo3sNB0dm/vQA+HjztK0zSWzAGz8fjWAjCeZprxSldCaqm0rhujKakmjmaV0jmvVXewtPhMNmhMi5xx08kBpVxMWLAKBERZCZwK+Ceo+xrzta7rR5VQyIvSaEPxd3aAzc1nkcSrEcOHjOt97zhMQR0SVOAfRDv3LxRqo9uitWk6Q5LC7O7e5z/DPn/+YpsDd700Zy6hkDV12J4HHMzCWkN73ZWJvAbPu4jl5YfzDnNkOUclzgV+pyvfEELdU8b0hVL97p8BqwS8gl5vBpymBSUv/gd44J0SgS7M/SqtB24YRonrkF75DLCd0eycejga4XIE9PB3rRwCl6rbd65j9drAAZOUda92psPaWJwklD6TSHiDOJNqnIggTqsgjFkQT3gQhkUQko+n5Y8ozPIolxu/AQWIll3VZY8bAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/c5546cbc64cae1f1678125515dfb2861/33ecc/jql-filter.webp 360w,
/static/c5546cbc64cae1f1678125515dfb2861/263a4/jql-filter.webp 480w,
/static/c5546cbc64cae1f1678125515dfb2861/0c8fb/jql-filter.webp 640w,
/static/c5546cbc64cae1f1678125515dfb2861/521f7/jql-filter.webp 860w,
/static/c5546cbc64cae1f1678125515dfb2861/cbd37/jql-filter.webp 1200w,
/static/c5546cbc64cae1f1678125515dfb2861/93bdb/jql-filter.webp 1946w&quot; sizes=&quot;(max-width: 1946px) 100vw, 1946px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/c5546cbc64cae1f1678125515dfb2861/53918/jql-filter.png 360w,
/static/c5546cbc64cae1f1678125515dfb2861/9aebd/jql-filter.png 480w,
/static/c5546cbc64cae1f1678125515dfb2861/0f09e/jql-filter.png 640w,
/static/c5546cbc64cae1f1678125515dfb2861/3e3fe/jql-filter.png 860w,
/static/c5546cbc64cae1f1678125515dfb2861/64756/jql-filter.png 1200w,
/static/c5546cbc64cae1f1678125515dfb2861/204ec/jql-filter.png 1946w&quot; sizes=&quot;(max-width: 1946px) 100vw, 1946px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/c5546cbc64cae1f1678125515dfb2861/204ec/jql-filter.png&quot; alt=&quot;jql filter&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;figcaption&gt;
릴리즈 되지 않은 버전이 있는 경우,&lt;br&gt;
고급 검색 - 해결 버전의 &apos;미출시 버전&apos;을 선택하고 JQL로 전환하면 간단하게 JQL 문장을 만들어낼 수 있다.
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h3 id=&quot;5-레이블-정리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#5-%EB%A0%88%EC%9D%B4%EB%B8%94-%EC%A0%95%EB%A6%AC&quot; aria-label=&quot;5 레이블 정리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;5) 레이블 정리&lt;/h3&gt;
&lt;p&gt;레이블은 아직도 진행 중인 개선사항 중 하나입니다. 아까 이야기했듯이, 처음에는 모든 콘텐츠를 레이블화 하려고 시도했습니다만, 띄어쓰기 하나로 같은 내용의 레이블이 중복으로 생성되기도 하고, 한번 쓰고 버려지는 레이블들이 너무 많아서, 결국 이도 저도 아닌 상태가 되어버린 것 같습니다. 따라서 이를 초 단순화시켜 간단하게 사용할 수 있게 정리하는 작업이 필요했습니다.&lt;/p&gt;
&lt;p&gt;그리하여 기존 콘텐츠들을 우선 크게 &apos;아웃게임&apos;과 &apos;인게임&apos;으로 구분하고, 그 외에는 &apos;직군&apos; 단위의 레이블만 활성화시켜 이슈에 적용하였습니다. &apos;아웃게임&apos;의 &apos;클라이언트&apos; 이슈, &apos;인게임&apos;의 &apos;게임디자인&apos; 이슈 등 직관적으로 어디의 어떤 직군의 담당 이슈인가를 파악하는 쪽으로 개선하니, 더 깔끔한 구성으로 이슈를 생성할 수 있었습니다.&lt;/p&gt;
&lt;p&gt;기본적으로 내부 프로젝트이기 때문에 사실 레이블을 보지 않고도 어떤 이슈인지는 모두가 이미 알고 있습니다. 따라서 복잡하게 레이블을 구성하는 것보다는, 실질적으로 어디에서 어떤 문제가 발생하였는가만 대략적으로만 알아도 업무 진행에 큰 방해가 되지는 않았습니다. 다만, 현재도 레이블을 입력하는 방식이 QA 팀 내부에서도 각각 조금씩 달라서 상위 방향성을 개선하는 작업은 더 필요해 보입니다.&lt;/p&gt;
&lt;h3 id=&quot;6-적용하지-못한-개선사항들&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#6-%EC%A0%81%EC%9A%A9%ED%95%98%EC%A7%80-%EB%AA%BB%ED%95%9C-%EA%B0%9C%EC%84%A0%EC%82%AC%ED%95%AD%EB%93%A4&quot; aria-label=&quot;6 적용하지 못한 개선사항들 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;6) 적용하지 못한 개선사항들&lt;/h3&gt;
&lt;p&gt;물론 아쉽게도 개선하지 못한 사항들도 있습니다. QA 직군 12명 모두가 원했던 설명 필드의 &lt;strong&gt;&apos;기본 템플릿 적용&apos;&lt;/strong&gt; 은 구글링과 아틀라시안 커뮤니티, 관련 서적을 읽어도 뚜렷한 답을 찾을 수 없어 결국 적용하지 못하였습니다. 유료 애드온 프로그램 중에 관련 기능을 제공하는 프로그램도 있었으나, 이미 유료 서비스를 사용하는 입장에서 또 하나의 유료 서비스를 이용하는 것이 과연 옳은가라는 의구심이 있어 그냥 저희가 이슈를 생성할 때마다 입력하는 것으로 마무리하였습니다.&lt;/p&gt;
&lt;p&gt;또한 이슈의 상태에 따라 카드 썸네일에 색상을 표시하는 기능을 넣고 싶었으나 이것도 마찬가지로 JIRA에서 지원하지 않는 기능이고, 애드온 프로그램 중에서도 이런 기능을 제공하는 프로그램이 없어 그냥 넘어가기로 하였습니다. 색깔을 부여하면 좀 더 직관적으로 이슈의 상태를 알 수 있지 않을까 하여 생각한 아이디어였는데, 지원하지 않는 기능이라니... JIRA에 가지고 있는 불만사항 중 하나입니다.&lt;/p&gt;
&lt;h2 id=&quot;6-마치며--회고&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#6-%EB%A7%88%EC%B9%98%EB%A9%B0--%ED%9A%8C%EA%B3%A0&quot; aria-label=&quot;6 마치며  회고 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;6. 마치며 : 회고&lt;/h2&gt;
&lt;p&gt;지금 JIRA를 사용하는 것은 너무나 당연한 일이 되었지만, 사실 처음에는 신기하기도 하고 어렵기도 했던 툴이었습니다. 특히나 저는 처음으로 JIRA를 사용해보았기 때문에 더더욱 부담스러웠던 것 같습니다. 이미 JIRA를 잘 알고 있는 분들에게 그냥 맡길까 하는 생각도 있었지만, 결국엔 제가 실질적으로 사용해야 하는 툴이라는 것을 깨닫고 노력으로 어느 정도 환경 구축에 성공한 것 같습니다.&lt;/p&gt;
&lt;p&gt;그렇다면 과연 JIRA는 쓸만한 BTS 서비스일까요? 이 질문에는 이렇게 답변할 수 있을 것 같습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;BTS의 정석.&lt;/p&gt;
&lt;p&gt;그러나 초기 설정 여부에 따라 양날의 검이 될 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;JIRA는 트렐로처럼 가볍게 슉슉 던지듯이 일처리가 가능한 툴은 아닙니다. 그럼에도 JIRA가 더 효율적이라고 생각하는 이유는 사용자의 작업환경을 편하게 만들어주는 기능들이 트렐로보다 훨씬 더 많기 때문입니다. 트렐로를 쓰면서 가장 불만이었던 &quot;편리하고 정확한 검색(추적)&quot;과 &quot;버전별 이슈 관리(관리)&quot;의 니즈가 충족된 것만으로도 JIRA는 트렐로보다 더 효율적인 툴이라고 말할 수 있을 것 같습니다.&lt;/p&gt;
&lt;p&gt;다만, JIRA는 사용자의 숙련도에 따라서 효율성이 천차만별이기 때문에 어떤 경우엔 트렐로보다 더 비효율적일 수도 있을 것 같습니다. 따라서 환경설정이나 수많은 레퍼런스 등 &apos;공부&apos;가 필요한 서비스이기도 하고, 개인적/팀적으로 많은 노력을 기울여야 어느 정도 쓸만한 환경 구축이 가능합니다. JIRA 설정이 어렵다면 간단하게 사용할 수 있는 다른 서비스를 이용하는 것이 오히려 더 효과적일지도 모릅니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&apos;어떤 툴을 어떤 방식으로 사용하느냐는 역시나 사람에게 달려 있다.&apos;라는&lt;/strong&gt; 첫 부분의 문장을 기억하시나요? 그래도 성공적으로 JIRA가 쿠키런 오븐브레이크에 안착한 것은 QA를 비롯한 많은 팀원분들께 공이 있다고 생각합니다. 이 글을 읽는 모든 분들이 각 팀에서 사용하는 서비스들에 대해 사용 피드백을 적극적으로 주는 것이, 그 내용이 사소할지라도 사용성 개선에 큰 힘을 줄 수 있다는 것을 꼭 아셨으면 좋겠습니다. 그럼 이만!&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[[Windows 컨테이너] 3: NT 서비스를 Windows 컨테이너로 마이그레이션하기]]></title><description><![CDATA[NT 서비스를 Windows 컨테이너로 마이그레이션하는 방법, 로그를 수집하는 방법에 대해 살펴봅니다.]]></description><link>https://tech.devsisters.com/posts/migrate-nt-service-to-container</link><guid isPermaLink="false">https://tech.devsisters.com/posts/migrate-nt-service-to-container</guid><pubDate>Fri, 10 Jan 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;데브시스터즈는 &lt;a href=&quot;https://www.slideshare.net/seungyongoh3/ndc17-kubernetes&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Kubernetes 기반의 게임 개발 및 테스트 인프라를 성공적으로 구축&lt;/a&gt;하여 게임 출시에 이르는 모든 과정들을 효율적으로 처리하고 있습니다.&lt;/p&gt;
&lt;p&gt;다년간의 성공적인 운영 경험을 바탕으로 Windows Server 기반의 Workload를 지원하기 위한 R&amp;#x26;D 작업을 진행하였고, 그 경험담을 공유하는 아티클 시리즈를 몇 편에 걸쳐 DevTech 기술 블로그에 연재할 예정입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Windows 컨테이너 시리즈 글 보기&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;/posts/intro-windows-container&quot;&gt;Windows 컨테이너에 대한 이해&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/start-windows-container-dev&quot;&gt;Windows 컨테이너 개발 환경 구축하고 테스트하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/migrate-nt-service-to-container&quot;&gt;NT 서비스를 Windows 컨테이너로 마이그레이션하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/windows-container-basics&quot;&gt;베이스 이미지, 격리 방식에 대한 이해&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;들어가기에-앞서&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%93%A4%EC%96%B4%EA%B0%80%EA%B8%B0%EC%97%90-%EC%95%9E%EC%84%9C&quot; aria-label=&quot;들어가기에 앞서 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;들어가기에 앞서&lt;/h2&gt;
&lt;p&gt;리눅스와는 달리 Windows 서버 애플리케이션의 경우 독립적인 서버에서 실행되던 애플리케이션을 컨테이너로 옮기는 작업은 매우 많은 고민을 필요로 합니다.&lt;/p&gt;
&lt;p&gt;이 때의 고민들은 아키텍처 측면에서의 고민도 있겠지만, 기술적인 문제에 해당되는 경우도 많습니다.&lt;/p&gt;
&lt;p&gt;이번에 소개하려는 내용은 컨테이너로 옮기기 까다로운 유형에 해당하는 NT Service 타입의 서버 애플리케이션을 옮기는 방법을 상세하게 알아보겠습니다. 또한, NT Service가 생성하는 다양한 종류의 로그나 기록을 어떻게 표준 출력으로 내보내는지에 대해서도 알아봅니다.&lt;/p&gt;
&lt;p&gt;이 글의 내용을 따라하기 위해서는 &lt;a href=&quot;/posts/start-windows-container-dev&quot;&gt;Windows 컨테이너 개발 환경 구축하고 테스트하기&lt;/a&gt;편을 참고해 환경을 설정해야 합니다.&lt;/p&gt;
&lt;h2 id=&quot;서비스를-컨테이너화하는방법&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%84%9C%EB%B9%84%EC%8A%A4%EB%A5%BC-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%ED%99%94%ED%95%98%EB%8A%94%EB%B0%A9%EB%B2%95&quot; aria-label=&quot;서비스를 컨테이너화하는방법 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;서비스를 컨테이너화하는 방법&lt;/h2&gt;
&lt;p&gt;서버 응용프로그램을 컨테이너화하는 것은 보통 단독으로 실행되는 프로그램을 기준으로 합니다. 리눅스 및 유닉스 환경에서는 거의 모든 응용프로그램들이 그 즉시 다른 도우미 프로그램을 경유하지 않고 실행되므로 컨테이너화를 하는데 큰 고민을 할 것이 없습니다.&lt;/p&gt;
&lt;p&gt;반면 Windows의 경우 서비스 제어 관리자 (Service Control Manager, 이하 SCM)에 의하여 백그라운드 서비스를 다루게 됩니다. 그리고 SCM이 인식할 수 있는 서비스는 통상적인 응용프로그램들과는 동작 방식이 다릅니다. 컨테이너화를 하는데 있어서 허들이 되는 부분은 즉 SCM이라고 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;Windows 서비스를 컨테이너화한다는 것은, 컨테이너화할 서비스가 정상적으로 시작되어 SCM에 의하여 종료를 맞이할 때까지 컨테이너가 계속 실행되어야 함을 뜻합니다. 하지만 컨테이너 런타임은 SCM의 실행 상태를 고려하지 않고, 또한 SCM을 고려한다는 것은 플랫폼에 지나치게 종속적인 일입니다.&lt;/p&gt;
&lt;p&gt;이 문제를 해결하기 위하여 Microsoft에서는 IIS용으로 ServiceMonitor 라는 오픈 소스 유틸리티를 공개하였습니다. 하지만 IIS만이 아니라 SCM에 의하여 제어할 수 있는 서비스에는 모두 적용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/Microsoft/IIS.ServiceMonitor&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://github.com/Microsoft/IIS.ServiceMonitor&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;위의 리포지터리에서 제공하는 릴리스된 버전의 ServiceMonitor 유틸리티를 Docker로 이미지를 빌드하는 도중에 추가하여 ENTRYPOINT로 지정하면 손쉽게 활용할 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;실제-dockerfile-예시살펴보기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8B%A4%EC%A0%9C-dockerfile-%EC%98%88%EC%8B%9C%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0&quot; aria-label=&quot;실제 dockerfile 예시살펴보기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;실제 Dockerfile 예시 살펴보기&lt;/h2&gt;
&lt;p&gt;이번 글에서의 관심사는 순수하게 ServiceMonitor의 활용에 관한 것이므로 저는 Windows 운영 체제에 이미 포함되어있는 예제 TCP 서비스를 컨테이너화하는 것으로 시나리오를 잡았습니다.&lt;/p&gt;
&lt;p&gt;예제 TCP 서비스를 활성화하기 위한 제반 사항을 포함하는 SKU는 Windows Server Core이므로, 해당 이미지를 기본 이미지로 사용하고, PowerShell 명령어를 자주 써야 하므로 기본 Shell을 PowerShell로 변경하기 위하여 Dockerfile의 첫 시작은 다음과 같이 만들었습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;docker&quot;&gt;&lt;pre class=&quot;language-docker&quot;&gt;&lt;code class=&quot;language-docker&quot;&gt;&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; mcr.microsoft.com/windows/servercore:1809&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;SHELL&lt;/span&gt; [&lt;span class=&quot;token string&quot;&gt;&quot;powershell&quot;&lt;/span&gt;, &lt;span class=&quot;token string&quot;&gt;&quot;-Command&quot;&lt;/span&gt;, &lt;span class=&quot;token string&quot;&gt;&quot;$ErrorActionPreference = &apos;Stop&apos;; $ProgressPreference = &apos;Continue&apos;; $verbosePreference=&apos;Continue&apos;;&quot;&lt;/span&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;앞에서 이야기한 ServiceMonitor를 미리 빌드한 버전을 다음과 같이 루트 디렉터리 (C:)에 추가합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;docker&quot;&gt;&lt;pre class=&quot;language-docker&quot;&gt;&lt;code class=&quot;language-docker&quot;&gt;&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;ADD&lt;/span&gt; https://dotnetbinaries.blob.core.windows.net/servicemonitor/2.0.1.3/ServiceMonitor.exe /&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;예제 TCP 서비스를 Windows 부가 기능에서 찾아 활성화하고, 서비스를 시작 상태로 변경하는 PowerShell 명령을 다음과 같이 추가합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;docker&quot;&gt;&lt;pre class=&quot;language-docker&quot;&gt;&lt;code class=&quot;language-docker&quot;&gt;&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; Enable-WindowsOptionalFeature -Online -FeatureName SimpleTCP&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; Start-Service -ServiceName SimpTcp&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Echo (7/tcp), Discard (9/tcp), Daytime (13/tcp), QotD (17/tcp), chargen (19/tcp)가 SimpTcp 서비스로 인하여 한 번에 시작됩니다. 컨테이너 외부에서 이들 서비스에 접속할 수 있도록 설정합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;docker&quot;&gt;&lt;pre class=&quot;language-docker&quot;&gt;&lt;code class=&quot;language-docker&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Echo&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;EXPOSE&lt;/span&gt; 7&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Discard&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;EXPOSE&lt;/span&gt; 9&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Daytime&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;EXPOSE&lt;/span&gt; 13&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Quote of the Day (QOTD)&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;EXPOSE&lt;/span&gt; 17&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Character Generator (chargen)&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;EXPOSE&lt;/span&gt; 19&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그리고 SimpTcp 서비스가 실행되는 동안 컨테이너가 종료되지 않도록 ServiceMonitor가 SimpTcp 서비스를 바라보도록 ENTRYPOINT 설정을 다음과 같이 추가합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;docker&quot;&gt;&lt;pre class=&quot;language-docker&quot;&gt;&lt;code class=&quot;language-docker&quot;&gt;&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;ENTRYPOINT&lt;/span&gt; C:\ServiceMonitor SimpTcp&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;지금까지 만든 Dockerfile은 다음과 같습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;docker&quot;&gt;&lt;pre class=&quot;language-docker&quot;&gt;&lt;code class=&quot;language-docker&quot;&gt;&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; mcr.microsoft.com/windows/servercore:1809&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;SHELL&lt;/span&gt; [&lt;span class=&quot;token string&quot;&gt;&quot;powershell&quot;&lt;/span&gt;, &lt;span class=&quot;token string&quot;&gt;&quot;-Command&quot;&lt;/span&gt;, &lt;span class=&quot;token string&quot;&gt;&quot;$ErrorActionPreference = &apos;Stop&apos;; $ProgressPreference = &apos;Continue&apos;; $verbosePreference=&apos;Continue&apos;;&quot;&lt;/span&gt;]&lt;/span&gt;

&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;ADD&lt;/span&gt; https://dotnetbinaries.blob.core.windows.net/servicemonitor/2.0.1.3/ServiceMonitor.exe /&lt;/span&gt;

&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; Enable-WindowsOptionalFeature -Online -FeatureName SimpleTCP&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; Start-Service -ServiceName SimpTcp&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Echo&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;EXPOSE&lt;/span&gt; 7&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Discard&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;EXPOSE&lt;/span&gt; 9&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Daytime&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;EXPOSE&lt;/span&gt; 13&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Quote of the Day (QOTD)&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;EXPOSE&lt;/span&gt; 17&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Character Generator (chargen)&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;EXPOSE&lt;/span&gt; 19&lt;/span&gt;

&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;ENTRYPOINT&lt;/span&gt; C:\ServiceMonitor SimpTcp&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;컨테이너-이미지-빌드-및테스트&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%B9%8C%EB%93%9C-%EB%B0%8F%ED%85%8C%EC%8A%A4%ED%8A%B8&quot; aria-label=&quot;컨테이너 이미지 빌드 및테스트 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;컨테이너 이미지 빌드 및 테스트&lt;/h2&gt;
&lt;p&gt;컨테이너 이미지를 빌드하기 위하여 Dockerfile이 있는 디렉터리에서 다음과 같이 명령어를 실행합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;powershell&quot;&gt;&lt;pre class=&quot;language-powershell&quot;&gt;&lt;code class=&quot;language-powershell&quot;&gt;docker build &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;t simpletcp:latest &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그 후, 다음과 같이 컨테이너를 실행합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;powershell&quot;&gt;&lt;pre class=&quot;language-powershell&quot;&gt;&lt;code class=&quot;language-powershell&quot;&gt;docker run &lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;d &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;p 7:7 &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;p 9:9 &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;p 13:13 &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;p 17:17 &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;p 19:19 &lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;name=simpletcp simpletcp:latest &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;컨테이너가 정상적으로 계속 실행되는지 확인한 후에, 13, 17, 19번 포트 중 하나를 골라서 telnet 명령으로 접속해봅니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;powershell&quot;&gt;&lt;pre class=&quot;language-powershell&quot;&gt;&lt;code class=&quot;language-powershell&quot;&gt;telnet localhost 13
telnet localhost 17
telnet localhost 19&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;각각에 해당되는 서비스가 정상 작동하는 모습을 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;참고로 telnet 유틸리티는 기본으로 설치되지 않으므로 호스트 PC에서 관리자 권한으로 아래 명령을 실행하여 해당 기능을 설치해야 합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;powershell&quot;&gt;&lt;pre class=&quot;language-powershell&quot;&gt;&lt;code class=&quot;language-powershell&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Windows 10의 경우&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;Enable-WindowsOptionalFeature&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;Online &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;FeatureName &lt;span class=&quot;token string&quot;&gt;&apos;TelnetClient&apos;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Windows Server의 경우&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;Install-WindowsFeature&lt;/span&gt; Telnet-Client&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;로그-수집을-위한해결책&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A1%9C%EA%B7%B8-%EC%88%98%EC%A7%91%EC%9D%84-%EC%9C%84%ED%95%9C%ED%95%B4%EA%B2%B0%EC%B1%85&quot; aria-label=&quot;로그 수집을 위한해결책 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;로그 수집을 위한 해결책&lt;/h2&gt;
&lt;p&gt;일단 컨테이너화된 Windows 서비스가 잘 작동하는 것을 볼 수 있었습니다. 하지만 stdout으로 로그를 내보내는 기능이 없어 동작 상태를 즉시 파악할 수 없는 문제가 있습니다.&lt;/p&gt;
&lt;p&gt;이 문제를 해결하기 위하여 최근에 Microsoft에서는 LogMonitor라는 보조 유틸리티를 추가로 릴리스하였습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/microsoft/windows-container-tools/tree/master/LogMonitor&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://github.com/microsoft/windows-container-tools/tree/master/LogMonitor&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;LogMonitor를 사용하는 방법은 단순합니다. 실행하려는 NT 서비스나 Win32 프로세스를 실행하기 앞서 LogMonitor.exe 유틸리티가 대신 실행할 수 있도록 Docker의 SHELL 지시자로 연결하거나, LogMonitor.exe의 명령줄 인자로 실행할 프로그램을 지정하는 것입니다.&lt;/p&gt;
&lt;h2 id=&quot;적용-및테스트&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%81%EC%9A%A9-%EB%B0%8F%ED%85%8C%EC%8A%A4%ED%8A%B8&quot; aria-label=&quot;적용 및테스트 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;적용 및 테스트&lt;/h2&gt;
&lt;p&gt;앞서 만든 Dockerfile에 다음의 내용을 추가하면 ServiceMonitor.exe와 PowerShell 인터프리터를 LogMonitor.exe가 대신 실행해주면서 로그 파일, ETW 로그, 이벤트 로그 수집에 필요한 전처리 과정을 가로채어 stdout으로 내보낼 수 있게 준비합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;docker&quot;&gt;&lt;pre class=&quot;language-docker&quot;&gt;&lt;code class=&quot;language-docker&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# Add LogMonitor and wrap command with LogMonitor.exe&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;ADD&lt;/span&gt; https://github.com/microsoft/windows-container-tools/releases/download/v1.0/LogMonitor.exe /LogMonitor/&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;ADD&lt;/span&gt; LogMonitorConfig.json /LogMonitor/&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;WORKDIR&lt;/span&gt; /LogMonitor&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;SHELL&lt;/span&gt; [&lt;span class=&quot;token string&quot;&gt;&quot;C:\\LogMonitor\\LogMonitor.exe&quot;&lt;/span&gt;, &lt;span class=&quot;token string&quot;&gt;&quot;powershell.exe&quot;&lt;/span&gt;]&lt;/span&gt;

&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;ENTRYPOINT&lt;/span&gt; Start-Service SimpTcp; C:\ServiceMonitor.exe SimpTcp&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;LogMonitorConfig.json 파일은 LogMonitor.exe를 위한 설정 파일이며, Simple TCP 서비스의 로그를 수집하기 위해 이벤트 로그와 특정 ETW 로그를 선택적으로 수집하도록 구성했습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;json&quot;&gt;&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;LogConfig&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;sources&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;EventLog&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;startAtOldestRecord&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;eventFormatMultiLine&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;channels&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token property&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;system&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token property&quot;&gt;&quot;level&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Information&quot;&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token property&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;application&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token property&quot;&gt;&quot;level&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Error&quot;&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;type&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;ETW&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;eventFormatMultiLine&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;providers&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token property&quot;&gt;&quot;providerName&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Microsoft-Windows-Services&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token property&quot;&gt;&quot;providerGuid&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;0063715B-EEDA-4007-9429-AD526F62696E&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token property&quot;&gt;&quot;level&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Information&quot;&lt;/span&gt;
          &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;위와 같이 수정한 컨테이너 이미지를 시작한 후 로그 출력을 보면 한 줄씩 XML 형태로 상세 로그가 나타나는 것을 볼 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;xml&quot;&gt;&lt;pre class=&quot;language-xml&quot;&gt;&lt;code class=&quot;language-xml&quot;&gt;...
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;Source&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;EtwEvent&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;Source&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;Time&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;2020-01-10T12:10:26.000Z&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;Time&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;Provider&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;idGuid&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;{0063715B-EEDA-4007-9429-AD526F62696E}&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;DecodingSource&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;DecodingSourceXMLFile&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;DecodingSource&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;Execution&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;ProcessID&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;624&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;ThreadID&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;3704&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;Level&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Information&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;Level&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;Keyword&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;0x8000000000010000&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;Keyword&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;EventID&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;Qualifiers&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;105&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;105&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;EventID&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;EventData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;ExecutionPhase&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;1&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;ExecutionPhase&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;CurrentState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;2&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;CurrentState&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;StartType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;2&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;StartType&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;PID&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;0&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;PID&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;ServiceName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;simptcp&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;ServiceName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;ImageName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;ImageName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;EventData&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
...
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;Source&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;EventLog&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;Source&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;Time&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;2020-01-10T11:50:31.000Z&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;Time&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;LogEntry&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;Channel&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;System&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;Channel&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;Level&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;Information&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;Level&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;EventId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;7036&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;EventId&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;Message&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;The simptcp service entered the running state.&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;Message&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;LogEntry&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
...&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;만약 다른 종류의 이벤트 로그를 추가 수집하려면 &lt;a href=&quot;https://gist.github.com/guitarrapc/35a94b908bad677a7310&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://gist.github.com/guitarrapc/35a94b908bad677a7310&lt;/a&gt; 에 나와있는 ETW 공급자 목록에 해당하는 이름과 GUID 값을 LogMonitorConfig.json 파일에 추가할 수 있습니다.&lt;/p&gt;
&lt;p&gt;만약 서비스가 로그 파일 기록을 지원한다면, 로그 파일의 내용을 직접 읽어 출력하도록 만드는 것도 가능하며, LogMonitorConfig.json 파일의 레퍼런스는 &lt;a href=&quot;https://github.com/microsoft/windows-container-tools/blob/master/LogMonitor/src/LogMonitor/sample-config-files/IIS/LogMonitorConfig.json&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://github.com/microsoft/windows-container-tools/blob/master/LogMonitor/src/LogMonitor/sample-config-files/IIS/LogMonitorConfig.json&lt;/a&gt; 에서 볼 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;정리&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%95%EB%A6%AC&quot; aria-label=&quot;정리 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;정리&lt;/h2&gt;
&lt;p&gt;지금 소개한 두 가지 유틸리티를 조합하여 NT 서비스가 만들어낼 수 있는 다양한 기록 (이벤트 로그, 로그 파일, ETW 로그)를 표준 출력으로 내보내면서도, Docker 컨테이너가 서비스 제어 관리자의 흐름과 컨테이너의 생명 주기에 맞출 수 있도록 만들 수 있습니다.&lt;/p&gt;
&lt;p&gt;그럼에도 불구하고, 이 방법은 IIS, ASP, ASP.NET 처럼 NT 서비스를 직접 콘솔 애플리케이션으로 변환하기 어려운 상황에서만 고려해야 하는 대안입니다.&lt;/p&gt;
&lt;p&gt;만약 애플리케이션을 컨테이너화 하는 것에 투자할 시간적 여유가 충분하다면 콘솔 애플리케이션 형태로 작동하는 독립적인 서버 애플리케이션으로 리팩토링하는 것이 더 효과적인 방법일 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;다음-글&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8B%A4%EC%9D%8C-%EA%B8%80&quot; aria-label=&quot;다음 글 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;다음 글&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;/posts/windows-container-basics&quot;&gt;다음 글 보기: 베이스 이미지, 격리 방식에 대한 이해&lt;/a&gt;&lt;/p&gt;
&lt;style&gt;
article#article.pc span.gatsby-resp-image-wrapper {
    max-width: 70% !important;
}
&lt;/style&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[스펙에 대처하는 QA의 자세]]></title><description><![CDATA[QA가 늘 듣지만 항상 반갑지만은 않은 “스펙입니다”라는 말에 어떻게 대처하는게 좋을까요?]]></description><link>https://tech.devsisters.com/posts/attitude-of-qa</link><guid isPermaLink="false">https://tech.devsisters.com/posts/attitude-of-qa</guid><pubDate>Tue, 24 Sep 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;QA가 이슈를 발견하고 개발자가 이를 수정해 제품이나 서비스에 반영하는 과정은 다양한 커뮤니케이션을 통해 이루어 집니다.
QA가 테스트를 수행하다가 이슈로 추정되는 증상을 발견하면 해당 증상이 이슈인지 아닌지 개발자 혹은 기획자에게 확인하는 과정을 거칩니다.
이 과정에서 QA가 개발자 혹은 기획자로부터 흔히 듣게 되는 답변 중에 아래와 같은 말이 있습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“이거 스펙(Spec)입니다.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;‘아니 이게 스펙이라니. 이렇게 발견하기 힘든 이슈를 어렵게 재현해 냈는데, 이게 스펙이라니.
이 문제가 그냥 라이브에 배포되면 사용자가 많이 불편해 할텐데!’라는 속마음을 뒤로하고 QA는 터덜터덜 자리로 돌아갈 수 밖에 없습니다.
자리에 돌아와 생각해보니 마음 한 켠에는 ‘수정하기 힘든 이슈라 수정하는데 시간은 많이 걸릴 것 같고 또 이걸 이슈라고 인정하면 왠지 부끄러워서 그냥 스펙이라고 하는 거 아냐?’라는 의문이 듭니다.&lt;/p&gt;
&lt;p&gt;하지만 어쩌겠어요. 테스트하고 있는 기능이나 콘텐츠에 대해 가장 잘 알고 있는 개발자와 기획자가 스펙이라고 하는 걸.&lt;/p&gt;
&lt;p&gt;자, 그럼 그냥 이렇게 아쉬움을 뒤로 하고 해당 증상을 스펙이니까, 라고 그냥 정리하고 마무리하면 될까요?
스펙이라는 것이 과연 어떤 것인지, 그리고 QA 입장에서 어떻게 대처하면 좋을 지 한 번 고민해 봤습니다.&lt;/p&gt;
&lt;h2 id=&quot;스펙의-정의&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8A%A4%ED%8E%99%EC%9D%98-%EC%A0%95%EC%9D%98&quot; aria-label=&quot;스펙의 정의 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;스펙의 정의&lt;/h2&gt;
&lt;p&gt;필자 역시 처음 테스트 업무를 수행했던 곳에서부터 스펙이라는 단어를 자주 접할 수 있었습니다.
우선 스펙이라는 말을 영문 위키피디아에서 한 번 검색해 봤습니다.
한글로 검색하면 전혀 다른 의미가 주로 검색됩니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h2 id=&quot;spec&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#spec&quot; aria-label=&quot;spec permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Spec&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Spec&lt;/strong&gt; may refer to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Specification_(technical_standard)&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Specification (technical standard)&lt;/a&gt;, an explicit set of requirements to be satisfied by a material, product, or service
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Datasheet&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;datasheet&lt;/a&gt;, or &quot;spec sheet&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;sub&gt;출처: &lt;a href=&quot;https://en.wikipedia.org/wiki/Spec&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://en.wikipedia.org/wiki/Spec&lt;/a&gt;&lt;/sub&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;보시다시피 Spec은 Specification의 줄임말이며, 재료, 제품 혹은 서비스가 충족시켜야 하는 일련의 명확한 요구 사항이라고 설명하고 있습니다.
Specification 항목을 찾아보면 좀 더 정확한 정의를 찾을 수 있습니다.
물론 위키피디아의 단어 정의가 100% 정확한 것은 아니지만 일상적으로 큰 거부감 없이 통용되는 수준으로 볼 수 있을 것 같습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A &lt;strong&gt;specification&lt;/strong&gt; often refers to a set of documented &lt;a href=&quot;https://en.wikipedia.org/wiki/Requirement&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;requirements&lt;/a&gt; to be satisfied by a material, design, product, or service.&lt;/p&gt;
&lt;p&gt;&lt;sub&gt;출처: &lt;a href=&quot;https://en.wikipedia.org/wiki/Specification%5C_(technical%5C_standard)#firstHeading&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://en.wikipedia.org/wiki/Specification\_(technical\_standard)#firstHeading&lt;/a&gt;&lt;/sub&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;여기서 Specificaiton은 종종 재료, 제품 혹은 서비스가 충족시켜야 하는 일련의 &lt;strong&gt;문서화된&lt;/strong&gt; 요구 사항이라고 정의하고 있습니다.
이 문장을 좀 더 간단하게 표현하면 문서화된 요구 사항이라고 할 수 있겠습니다.
게임 업계에서 흔히 말하는 기획서가 여기에 해당될 것 같습니다.
즉, **“스펙입니다”**라고 이야기하는 것은 **‘기획서에 그 내용이 있습니다’**라고 이야기하는 것이라고 볼 수 있을 것 같네요.&lt;/p&gt;
&lt;p&gt;제 개인적으로는 &lt;strong&gt;Spec in Close&lt;/strong&gt;라는 단어가 스펙을 처리하는 가장 좋은 단어가 아닐까 합니다.
Spec in close는 발견한 증상이 Spec의 범위 안에 있으므로 결함으로 분류하지 않는다는 의미입니다.
여기서 Spec은 앞서 살펴본 Specification, 직역하면 명세 혹은 명세서, 의역하면 기획서라고 할 수 있겠습니다.
즉, QA가 발견한 증상이 기획서에 명세되어 있는 기대 결과에 포함되어 있다는 것을 뜻합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 516px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 72.22222222222221%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAAAsSAAALEgHS3X78AAACCUlEQVR42p2Tv2/TQBTHLTZWQEKImT+AAbGyMrDzD/AnMHdBLEgFVAIRpYShTUpbijrQDhWMhValVZPGqe20Dkrckl8lTox/ne0v9+ykcsGuUJ/1vTvfvffxO787oWdYOB6YqeroBvqWDcvzoZs2un0T3UGyiCXohg3HYrAt95Qs00HgAZvfiyjMLWFhcRmqWofPAjDu79kevFE/lE5ArdfHO0lEXtnDTEzTcgWFqoTs9ibGVpbx+PMqJne2kJNFTOxuI1MuYqJSQkYs4kV5B694f8hZwldNwxNxCw3TwG6vi4p+fCKRv8uGDtX+DcUcoGmbGCtt4ELuKa6+Hse1yWe4wvtLuee4/GEKYqsNYa3RwGxNBpkX+Egz5kdrWakEIZ/BjeksZu4/wO2X47i4OIXrC28htTsRMK9KobPre/CDIFG0RjZXU3Bz5T3urn7Ew9wb3Ps0jztflnCLz5WbrWGGapQhOyPDJHNjMcQgViKQMQaXy3F5xR0nHLsuA/O88H00B545G2Ze4Lv8B+hxh4BLlKqoa0eoHtSwp+zjR12DVD3Az2Yb8r7K15VwPf5vz8zQ504EJqM2GH6Ipkbjk2IlbfnvopB7vCDRg7BNK1b+1Jb/49ik2SiGGCFwnR/sR8UNlH91sNY6xHr7KFXfWpHicxRDscQgltDUB5ina8av3+w5RbHEINYfa0772Sp5T7oAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/e25036f3266485cb73ca39f042a1f19b/33ecc/spec-in-close.webp 360w,
/static/e25036f3266485cb73ca39f042a1f19b/263a4/spec-in-close.webp 480w,
/static/e25036f3266485cb73ca39f042a1f19b/67d7c/spec-in-close.webp 516w&quot;
              sizes=&quot;(max-width: 516px) 100vw, 516px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/e25036f3266485cb73ca39f042a1f19b/53918/spec-in-close.png 360w,
/static/e25036f3266485cb73ca39f042a1f19b/9aebd/spec-in-close.png 480w,
/static/e25036f3266485cb73ca39f042a1f19b/fbb32/spec-in-close.png 516w&quot;
            sizes=&quot;(max-width: 516px) 100vw, 516px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/e25036f3266485cb73ca39f042a1f19b/fbb32/spec-in-close.png&quot;
            alt=&quot;SPEC - QA가 인지하고 있는 기대 결과&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;SPEC - QA가 인지하고 있는 기대 결과&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;여기서 중요한 것은 해당 증상이 &lt;strong&gt;명세되어 있다&lt;/strong&gt;는 것입니다.
‘기대 결과 안에 포함된다’, 혹은 ‘그 예외 사항은 허용된다’와 같이 다양하게 표현할 수 있겠습니다.
이런 내용이 글이나 그림 같은 인지할 수 있는 형태로 기획서에 기록되어 있어야 한다는 것이 가장 중요합니다.
즉, 개발자 혹은 기획자가 특정 증상을 스펙으로 분류했다는 것은 기획서와 같은 문서에 기재되어 있는 내용을 근거로 해당 증상을 스펙이라고 정의 혹은 분류한다는 것을 뜻합니다.
만일 그렇지 않다면 특정 이슈를 스펙으로 분류하는 것에 대한 설득력이 떨어질 수 밖에 없습니다.&lt;/p&gt;
&lt;p&gt;이렇게 수정되지 않고 라이브 환경까지 전달된 이슈를 사용자가 발견하게 된다면 어떨까요.
사용자 역시 이 증상이 정상적인 범위에 포함되는지 궁금해 하지 않을까요.
이 경우에도 스펙이라고 설명한다면 사용자가 충분히 납득할 수 있을까요.
개발 과정에 참여한 QA조차 문제인지 아닌지 의아해하는 이슈를 사용자가 봤을 때 정상적이라고 납득할 가능성이 과연 얼마나 될까요.
스펙으로 추정되는 이슈를 제대로 정리하지 않는 것은 고객에게 제품을 전달하기 전에 이슈를 찾아 개선하고 이를 통해 최고의 경험을 전달해야 하는 QA의 미션에도 어긋납니다.&lt;/p&gt;
&lt;p&gt;Spec in Close 라는 단어에서 좀 더 눈여겨볼 만한 부분은 Close 입니다.
결함 추적 시스템을 사용하는 분들은 잘 아시겠지만 Close는 BTS에 등록된 이슈 티켓이 도달하는 최종 단계 중 하나 입니다.
Spec in close라는 말은 등록된 이슈가 스펙으로 처리되어 폐쇄 처리한다는 뜻입니다.
즉, 폐쇄 처리되기 위해서는 해당 이슈가 이미 BTS에 등록되어 있어야 한다는 것을 의미합니다.
이슈가 이슈로 판명되기 전에 QA가 이슈라고 의심할만한 증상도 우선은 BTS에 등록되고, 확인을 거쳐 스펙이라고 판명이 되면 Spec in close 처리가 된다는 것입니다.
이슈라고 의심되는 것 자체가 이미 BTS에 등록될 자격을 갖게 되는 것입니다.
따라서 Spec in close 자체도 의미를 가지게 됩니다.
Spec in close 처리되는 이슈가 많다는 것은 명세서가 제대로 정리되어 있지 않거나 명세된 내용을 QA가 제대로 파악하지 못했다는 것을 의미하게 됩니다.
반면, Spec in close 처리되는 것이 적다는 것은 명세서에 예외 처리되어야 하는 부분이 확실하게 명시 되어 있으며 명세된 내용을 QA가 확실하게 파악하고 있다는 것을 의미하게 됩니다.&lt;/p&gt;
&lt;p&gt;자, 이제 앞의 내용을 정리해 봅시다. 이슈인지 아닌지 명확하게 판단하기 힘든 이슈의 경우 아래와 같은 절차를 따라 처리합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;우선 결함 추적 시스템에 해당 증상으로 티켓을 생성합니다. 티켓의 상태는 &apos;Open&apos;을 유지합니다.&lt;/li&gt;
&lt;li&gt;해당 티켓의 증상을 개발자 혹은 기획자에게 문의합니다.&lt;/li&gt;
&lt;li&gt;개발자 혹은 기획자가 스펙으로 분류한다면, 전달받은 기획서 중 어떤 부분에 해당하는지 문의합니다.&lt;/li&gt;
&lt;li&gt;전달받은 기획서에서 해당 내용을 확인했다면 해당 티켓의 상태를 &apos;Spec in close&apos;로 변환합니다.&lt;/li&gt;
&lt;li&gt;만일 명세된 내용이 없다면 해당 증상을 스펙으로 분류해서 기획서에 추가할지, 이슈로 판단하여 수정할지 확인합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;스펙으로 추정되는 이슈가 기획서의 어느 부분에 해당하는지 확인할 때도 개발자 혹은 기획자에게 ‘스펙이라는 것을 증명해봐’가 아니라, 내가 어떤 부분을 추가로 확인하면 되는지 묻는 것이 중요합니다.
또한 명세되지 않은 내용이라면 QA는 이 증상을 &apos;사용자가 봤을 때 어떻게 받아들일지&apos;를 감안해서 스펙으로 분류할지 논의하는 것이 좋습니다.
문제를 수정하는 과정은 누군가의 잘잘못을 따지는 것이 핵심이 아니라, 최대한 빠르게 원인을 파악하고 수정해 제품의 품질을 높이고 이를 통해 고객이 최고의 경험을 할 수 있게 도와주는 과정이기 때문입니다.
이런 과정을 하나하나 거치는 것이 번거롭고 비효율적이라고 생각할 수도 있습니다.
하지만, 누군가 원칙과 품질을 고수해야 한다면 그게 바로 QA가 해야할 일이겠죠. Happy Testing! :)&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[[Windows 컨테이너] 2: Windows 컨테이너 개발 환경 구축하고 테스트하기]]></title><description><![CDATA[Windows 컨테이너를 실제로 개발하기 위하여 개발 환경을 구축하는 방법을 살펴봅니다. 그리고 첫 Windows 컨테이너를 만들어봅니다.]]></description><link>https://tech.devsisters.com/posts/start-windows-container-dev</link><guid isPermaLink="false">https://tech.devsisters.com/posts/start-windows-container-dev</guid><pubDate>Mon, 10 Jun 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;데브시스터즈는 &lt;a href=&quot;https://www.slideshare.net/seungyongoh3/ndc17-kubernetes&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Kubernetes 기반의 게임 개발 및 테스트 인프라를 성공적으로 구축&lt;/a&gt;하여 게임 출시에 이르는 모든 과정들을 효율적으로 처리하고 있습니다.&lt;/p&gt;
&lt;p&gt;다년간의 성공적인 운영 경험을 바탕으로 Windows Server 기반의 Workload를 지원하기 위한 R&amp;#x26;D 작업을 진행하였고, 그 경험담을 공유하는 아티클 시리즈를 몇 편에 걸쳐 DevTech 기술 블로그에 연재할 예정입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Windows 컨테이너 시리즈 글 보기&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;/posts/intro-windows-container&quot;&gt;Windows 컨테이너에 대한 이해&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/start-windows-container-dev&quot;&gt;Windows 컨테이너 개발 환경 구축하고 테스트하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/migrate-nt-service-to-container&quot;&gt;NT 서비스를 Windows 컨테이너로 마이그레이션하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/windows-container-basics&quot;&gt;베이스 이미지, 격리 방식에 대한 이해&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;시작하기에-앞서&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0%EC%97%90-%EC%95%9E%EC%84%9C&quot; aria-label=&quot;시작하기에 앞서 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;시작하기에 앞서&lt;/h2&gt;
&lt;p&gt;앞의 글에서는 Windows 컨테이너가 무엇이고, 왜 사용하는지, 그리고 리눅스 컨테이너와 기술적으로 어떤 차이가 있는지 살펴보았습니다. 이번 글에서는 실제로 Windows 컨테이너를 사용하기 위해 어떻게 환경을 만들고 준비할 수 있는지 살펴볼 것입니다.&lt;/p&gt;
&lt;h2 id=&quot;준비물&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A4%80%EB%B9%84%EB%AC%BC&quot; aria-label=&quot;준비물 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;준비물&lt;/h2&gt;
&lt;p&gt;Windows 컨테이너를 개발하고 테스트하기 위하여 선택할 수 있는 가장 쉬운 방법은 Hyper-V를 사용할 수 있는 Windows 10 호환 컴퓨터나 노트북을 사용하는 것입니다. 다만 주의할 것이 있습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;일부 저가형 컴퓨터나 구형 컴퓨터는 CPU 가상화 기능을 처음부터 제공하지 않도록 기능이 막혀있는 경우가 있을 수 있으므로 반드시 CPU 가상화 기술을 사용할 수 있는 컴퓨터인지 확인합니다.&lt;/li&gt;
&lt;li&gt;Windows 10 S Mode가 켜져있을 경우 S Mode를 해제해야 합니다. S Mode를 해제하는 자세한 방법은 &lt;a href=&quot;https://support.microsoft.com/ko-kr/help/4456067/windows-10-switch-out-of-s-mode&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;이 문서&lt;/a&gt;를 참고하세요. 단, 한 번 해제하면 다시 S Mode로 돌아올 수 없습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;만약 Windows 10 Home 버전을 사용 중이거나, macOS나 리눅스 환경 안에서 작업하기를 원한다면 Windows 10과 개발 도구가 모두 설치된 가상 컴퓨터 이미지를 Microsoft에서 내려받아서 이용할 수 있습니다.&lt;/p&gt;
&lt;p&gt;이 방법을 사용하면 별도의 라이선스를 취득하지 않아도 지정된 기한 내에 개발 목적으로 자유롭게 Windows 10 개발 환경을 이용할 수 있습니다. 다만 컨테이너 빌드 과정에서 오버헤드가 더 클 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://developer.microsoft.com/ko-kr/windows/downloads/virtual-machines&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Windows 10 개발 환경 다운로드&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;웹 사이트에서 가상 머신 소프트웨어 종류 별로 이미지가 각각 제공되므로 적절한 파일을 다운로드해서 사용할 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;가상화-또는-중첩-가상화-지원-여부-확인하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B0%80%EC%83%81%ED%99%94-%EB%98%90%EB%8A%94-%EC%A4%91%EC%B2%A9-%EA%B0%80%EC%83%81%ED%99%94-%EC%A7%80%EC%9B%90-%EC%97%AC%EB%B6%80-%ED%99%95%EC%9D%B8%ED%95%98%EA%B8%B0&quot; aria-label=&quot;가상화 또는 중첩 가상화 지원 여부 확인하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;가상화 또는 중첩 가상화 지원 여부 확인하기&lt;/h2&gt;
&lt;p&gt;실제 컴퓨터의 경우 사용 중인 프로세서가 가상화를 지원하는지 여부, 가상 컴퓨터를 사용하는 경우 하이퍼바이저가 중첩 가상화를 지원하는지 여부를 다시 한 번 확인하도록 합니다.&lt;/p&gt;
&lt;p&gt;가상화나 중첩 가상화를 어떻게 활성화하고 사용할 수 있는지에 대해서는 방법이 매우 다양하기 때문에 이 글에서는 다루기 어렵습니다. 하지만 Docker for Desktop을 사용하려면 반드시 가상화 기능이 호스트에서 지원되어야합니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ 만약 UEFI와 BitLocker를 사용하는 컴퓨터에서 UEFI 상의 가상화 지원 설정을 변경할 경우 다음번 시스템 시작 시 Windows OS가 잠길 수 있습니다. 이 때는 BitLocker 복구 키를 입력해주어야만 하므로, 계속 하기 전에 BitLocker 복구 키를 미리 메모한 후 작업하시는 것이 안전합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;docker-for-desktop-설치하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#docker-for-desktop-%EC%84%A4%EC%B9%98%ED%95%98%EA%B8%B0&quot; aria-label=&quot;docker for desktop 설치하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Docker for Desktop 설치하기&lt;/h2&gt;
&lt;br /&gt;
&lt;blockquote&gt;
&lt;p&gt;ℹ️ 이 글에서 설명하는 Windows 컨테이너 개발은 반드시 Docker Desktop 2.0 이상의 버전이 필요합니다. Docker Toolbox for Windows는 Windows 컨테이너 개발을 지원하지 않는 버전이므로 혼동해서 설치하지 않도록 주의해야 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Windows 10 실행 환경이 준비되면, Docker for Desktop을 설치하고 구성할 차례입니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://hub.docker.com/editions/community/docker-ce-desktop-windows&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Docker for Desktop 다운로드하기&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;이후에는 설치 마법사의 안내에 따라 설치를 진행하도록 합니다. 특별히 변경할 옵션 없이 설치를 완료하고, 시스템을 다시 시작하거나 로그오프 등이 필요하다는 안내 문구가 나오면 그대로 따릅니다.&lt;/p&gt;
&lt;p&gt;설치한 후에는 Docker Desktop을 시작 메뉴의 검색 기능을 이용하여 아래 그림과 같이 실행할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1372px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 79.16666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAQCAIAAACZeshMAAAACXBIWXMAABnWAAAZ1gEY0crtAAABqElEQVR42p2T22rCQBCG8xAWvPAEPnBvq1B6p0I00vaqpbUUjSewN1ZMQcFzNLs51GCNicb+JiRE0IL9GCazs/vv7M4SJhqLRaPRSCQCH4/H4ZPJJIaJRCIcDodCoSsPN0TGXYaYuU3fpFOpnEs2l81m8c1kMvD5AByXf7zPc4UCy+aLxSLHcSzLMk914e29XK/yzWbjs90Wut0vofvRaiHeO9j2wY9V87pEVX3rZwDz8FwrVyo8X+YrlVqVr9eqzUa99PqC4dbBsiys78/0u1dJ/7H29s6yDvnNZsOMxwNRnGmatlwuvwPoum57JQzDUCjRZGk+n0PmJhEw49lcEARZlrUAqqrC73Y72wEB6q8NA972OIg7nc5oNEJZV+CLURzTq9XKL+W1ICCWJIkQoh0DMbazTJMQiVKKc2Gv/TEHsXvDk2LTNBVFEUUR3r//kVg7hSvGNOLJZDKdTp2eXyLGY+DMqgNOcZkY1fBIaPX+FH+J0Qj3nn57/yM+B07EEEpglBJZpkFTFBl3Ns+zXq+Zfq+Hn2E0HBK8+GIRNGROGqZkSoeDwS8qLkZ2mzdi+wAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/4f1e547c22b6ff9b43204a28ce99b5ee/33ecc/run-docker-desktop.webp 360w,
/static/4f1e547c22b6ff9b43204a28ce99b5ee/263a4/run-docker-desktop.webp 480w,
/static/4f1e547c22b6ff9b43204a28ce99b5ee/0c8fb/run-docker-desktop.webp 640w,
/static/4f1e547c22b6ff9b43204a28ce99b5ee/521f7/run-docker-desktop.webp 860w,
/static/4f1e547c22b6ff9b43204a28ce99b5ee/cbd37/run-docker-desktop.webp 1200w,
/static/4f1e547c22b6ff9b43204a28ce99b5ee/a9a70/run-docker-desktop.webp 1372w&quot;
              sizes=&quot;(max-width: 1372px) 100vw, 1372px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/4f1e547c22b6ff9b43204a28ce99b5ee/53918/run-docker-desktop.png 360w,
/static/4f1e547c22b6ff9b43204a28ce99b5ee/9aebd/run-docker-desktop.png 480w,
/static/4f1e547c22b6ff9b43204a28ce99b5ee/0f09e/run-docker-desktop.png 640w,
/static/4f1e547c22b6ff9b43204a28ce99b5ee/3e3fe/run-docker-desktop.png 860w,
/static/4f1e547c22b6ff9b43204a28ce99b5ee/64756/run-docker-desktop.png 1200w,
/static/4f1e547c22b6ff9b43204a28ce99b5ee/ce85c/run-docker-desktop.png 1372w&quot;
            sizes=&quot;(max-width: 1372px) 100vw, 1372px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/4f1e547c22b6ff9b43204a28ce99b5ee/ce85c/run-docker-desktop.png&quot;
            alt=&quot;Docker Desktop for Windows 실행&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Docker Desktop for Windows 실행&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;Docker Desktop을 실행하면 작업 표시줄의 트레이 아이콘에 고래 모양의 아이콘이 아래 그림과 같이 표시됩니다. 처음에 시작하는 과정에서 트레이 아이콘에 애니메이션이 계속 재생되는데, 멈출 때까지 기다리도록 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 625px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 11.11111111111111%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAACCAIAAADXZGvcAAAACXBIWXMAABnWAAAZ1gEY0crtAAAAeElEQVR42g3Myw6CMBCFYV6HXui0M4WOsUU0YGLRnSZu3BlN8P3Xzvb8+U7TqpasuSldie5Ip+PEKZW8t0Z7cMEDYRj6iMFLWuZZxrHk37YZrRrBqNXDwWs5Pyle18qcSsmusyLBdX2kHfN0GEWu9SK/nIbv540B/ldsG5hN5/7IAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/ecfa7453d47fffc28333cfaec8404681/33ecc/docker-desktop-tray-icon.webp 360w,
/static/ecfa7453d47fffc28333cfaec8404681/263a4/docker-desktop-tray-icon.webp 480w,
/static/ecfa7453d47fffc28333cfaec8404681/529ab/docker-desktop-tray-icon.webp 625w&quot;
              sizes=&quot;(max-width: 625px) 100vw, 625px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/ecfa7453d47fffc28333cfaec8404681/53918/docker-desktop-tray-icon.png 360w,
/static/ecfa7453d47fffc28333cfaec8404681/9aebd/docker-desktop-tray-icon.png 480w,
/static/ecfa7453d47fffc28333cfaec8404681/b1b4f/docker-desktop-tray-icon.png 625w&quot;
            sizes=&quot;(max-width: 625px) 100vw, 625px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/ecfa7453d47fffc28333cfaec8404681/b1b4f/docker-desktop-tray-icon.png&quot;
            alt=&quot;Docker Desktop 트레이 아이콘&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;Docker Desktop 트레이 아이콘&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h2 id=&quot;windows-컨테이너-호스트로-전환하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#windows-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%ED%98%B8%EC%8A%A4%ED%8A%B8%EB%A1%9C-%EC%A0%84%ED%99%98%ED%95%98%EA%B8%B0&quot; aria-label=&quot;windows 컨테이너 호스트로 전환하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Windows 컨테이너 호스트로 전환하기&lt;/h2&gt;
&lt;p&gt;보편적으로 사람들은 리눅스 컨테이너 개발 환경을 필요로 하기 때문에, 처음 Docker for Desktop을 설치하면 리눅스 컨테이너 호스트로 실행됩니다. 그래서 Windows 컨테이너 모드로 전환을 해야 합니다.&lt;/p&gt;
&lt;p&gt;Windows 컨테이너 모드로 전환을 하려면 트레이 아이콘을 오른쪽 마우스 버튼으로 클릭하고, 나타나는 팝업 메뉴에서 &lt;code&gt;Switch to Windows Container&lt;/code&gt; 메뉴를 클릭합니다. 이 메뉴가 보이지 않으면 이미 전환이 완료된 것입니다.&lt;/p&gt;
&lt;figure&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 838px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 91.66666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAIAAADUsmlHAAAACXBIWXMAAAsTAAALEwEAmpwYAAADwklEQVR42lVTbWxTZRR+u25uU9fuo2uHS/ZB19F90rHNwSB0w0jLIIAxoiyGGBITjZFoSPhjDP4T2Q8DQgyJ/lDDD3/ANBhHSMjGZBXuNoYMu3bt7r1rbz9uS3vv7b39vh+etozEJ09Oznvynvec9z3Pi1iGcbncJLkJWHe7BUGQtpAvQpLkfDp1FksMz4u2hfzbDvH0onjusTT1JIX4RMK7sUGQJEVRa2trRBEerwcncAg6nc4wTUu57JfLwnuY8tGydGZF/mJVuuhSrroyiOd5n2+TJEmW5URJEkURqmWKyOVy2Ww2l8+L6fTnS3y/Qxl6II1h0kFMnsCUU1gWcRxHECQU91OBeJwJBIMMyyqKAt2KogTH5WVFyWQmFgT0p1L2h1j5W1b3a6ru52T7TwxKCQmoGwr6o3QoSPlI3OMjCY6NZdIpOEKWIVWBBuzTUfX3TNXlZ+qpZ2UXo+ibWO2lKFoimJv3ntyc/2cG89xyOGew9d/vP52eX4WlwxXGCHY1lHXgfNsUic4H1Bfo8u+4ih8z6l/y224k0YI3dutv9+1l4vYjcmbRe+cRMfc0OOcM3VkpLO+tRZao9Nw62/ItBdVUV5LoB1F1XUHTSv1MttA25ffT4VAoFIhGIjAhaPYFi23LfCrTeZVGF7iyS0l0JaO6lkeXk7VfbyAuweE4AUMmSAIG4/f74XVzuTwQAEaWJVZI9Xx1H308W3lmtuKTu+pTN1ST1+smr6FEgvN6vTiO+ykKBgXDkrdQ0gk4PC+MH3+/dfBg557Dpl37j5x4t3HwiHbiPCQnfJubNE1HIpFwEeCXnHQ6DWN/nnz0ZIvlgGnEbty5d+zEW7oBm/aNc0WRwJ0jNOyIxWLRaDQej0ciUYZhQCfFJ1Bgz/jhd7aZLO09Q23dwx1dA7puq9b6KVTmoWgoHGY5DuQkQbclFtQmwc1BLbBnv+1Y7+vWEatth2X0g7On9X37NHs+RJlMmonHEhwDVhB45f8oiQR+y7j9eFNbV5vZ0g7sNDcYh7VDJ9Fjp2fWsTi7sHT3L+zhyr/rZNBNBF7QhVO4L0AFAqNjtp2jB/aO25p3DI5NfqY3j2r7jqLq3mPIaFd12FVGO+qYQCbgIdQBPkRsaLutzji8e5+1taPHZNndvWukqd3cbz2k227Rmt9E1VoDKq9BFRr0kqZgS4RI+asFqoHVSKVG5VWqiiqkrtTU619+BYKVuqYWZGhuralrhJCmwaBp0GvAf07dFhu19YaaWp3O0FzboO/tH+js6jO81tJnGfwPonSykR1y2D8AAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/48c29c4290be470211cb15d51e0be6de/33ecc/switch-to-windows-containers.webp 360w,
/static/48c29c4290be470211cb15d51e0be6de/263a4/switch-to-windows-containers.webp 480w,
/static/48c29c4290be470211cb15d51e0be6de/0c8fb/switch-to-windows-containers.webp 640w,
/static/48c29c4290be470211cb15d51e0be6de/25dae/switch-to-windows-containers.webp 838w&quot; sizes=&quot;(max-width: 838px) 100vw, 838px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/48c29c4290be470211cb15d51e0be6de/53918/switch-to-windows-containers.png 360w,
/static/48c29c4290be470211cb15d51e0be6de/9aebd/switch-to-windows-containers.png 480w,
/static/48c29c4290be470211cb15d51e0be6de/0f09e/switch-to-windows-containers.png 640w,
/static/48c29c4290be470211cb15d51e0be6de/0591a/switch-to-windows-containers.png 838w&quot; sizes=&quot;(max-width: 838px) 100vw, 838px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/48c29c4290be470211cb15d51e0be6de/0591a/switch-to-windows-containers.png&quot; alt=&quot;switch to windows containers&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;figcaption&gt;Switch to Windows Container 메뉴 선택 - &lt;a href=&quot;https://docs.microsoft.com/en-us/virtualization/windowscontainers/quick-start/quick-start-windows-10&quot; target=&quot;_blank&quot;&gt;출처&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;잠시 기다리면 Windows 컨테이너 호스트 모드로 로딩이 완료됩니다. Hyper-V 가상 머신을 기반으로 하는 리눅스 컨테이너 호스트 모드와는 달리 Windows OS의 기능을 직접 사용하므로 훨씬 로딩이 빠릅니다.&lt;/p&gt;
&lt;p&gt;설치가 잘 되었는지 확인해보기 위하여 다음 명령어를 실행합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;docker run -it --rm hello-world&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;아래 그림과 같이 메시지가 표시되면 정상적으로 Windows 컨테이너 호스트 구동이 완료된 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1832px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 58.333333333333336%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAIAAADtbgqsAAAACXBIWXMAABnWAAAZ1gEY0crtAAABWklEQVR42n2RbW+CMBSFScxUSouAQlEoUl4iTPw4BdySbbpl2f//QTuUOF2mPrkptyGn5/ZUOx7eP46Hw9vry76tt0/NbluralS19Q61b+rLem6btq6/vz61IAiFELPZzLJsZ9qB3lGd4ziWZTHGhsPRaDT+rfFYHwwGeZZpWZZJKXO0ijRNi6LAx/d9RzGxJpRSdoFpmoSQNE20qqo2m81aURQl9EuFEFEcxxjK8zwI6AXYwh0GGnzKslytVnmeJx0yiqJl3IEp5vMF5z6G/yfWO+cwDGWSwIFz7rqu57poFgtoODzRYMWc18X4HQQBVnECtv3Y6HG0bTv0L2cx0lLTJtAj5z5k27b7xjAMZHNTHIYC14sUsIIMYRgnGL3CWYwr9ddTbzzlPlfxmuw2iEAnpEtbHYQ8mEGVFV6FMV0nd4oQ4+FhKGPZOStcOPLTht8tn3NkU60ffwAptUJeKHBn4QAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/34074c24974249fbed6874e8ce622b9b/33ecc/docker-run-hello-world-in-wcow.webp 360w,
/static/34074c24974249fbed6874e8ce622b9b/263a4/docker-run-hello-world-in-wcow.webp 480w,
/static/34074c24974249fbed6874e8ce622b9b/0c8fb/docker-run-hello-world-in-wcow.webp 640w,
/static/34074c24974249fbed6874e8ce622b9b/521f7/docker-run-hello-world-in-wcow.webp 860w,
/static/34074c24974249fbed6874e8ce622b9b/cbd37/docker-run-hello-world-in-wcow.webp 1200w,
/static/34074c24974249fbed6874e8ce622b9b/0cc5a/docker-run-hello-world-in-wcow.webp 1832w&quot;
              sizes=&quot;(max-width: 1832px) 100vw, 1832px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/34074c24974249fbed6874e8ce622b9b/53918/docker-run-hello-world-in-wcow.png 360w,
/static/34074c24974249fbed6874e8ce622b9b/9aebd/docker-run-hello-world-in-wcow.png 480w,
/static/34074c24974249fbed6874e8ce622b9b/0f09e/docker-run-hello-world-in-wcow.png 640w,
/static/34074c24974249fbed6874e8ce622b9b/3e3fe/docker-run-hello-world-in-wcow.png 860w,
/static/34074c24974249fbed6874e8ce622b9b/64756/docker-run-hello-world-in-wcow.png 1200w,
/static/34074c24974249fbed6874e8ce622b9b/432df/docker-run-hello-world-in-wcow.png 1832w&quot;
            sizes=&quot;(max-width: 1832px) 100vw, 1832px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/34074c24974249fbed6874e8ce622b9b/432df/docker-run-hello-world-in-wcow.png&quot;
            alt=&quot;docker run it --rm hello-world 실행 결과&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;docker run it --rm hello-world 실행 결과&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;이제 Windows 컨테이너를 사용하여 개발할 준비를 모두 마쳤습니다.&lt;/p&gt;
&lt;h2 id=&quot;첫-windows-컨테이너-애플리케이션-만들기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B2%AB-windows-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EB%A7%8C%EB%93%A4%EA%B8%B0&quot; aria-label=&quot;첫 windows 컨테이너 애플리케이션 만들기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;첫 Windows 컨테이너 애플리케이션 만들기&lt;/h2&gt;
&lt;p&gt;Windows 컨테이너 이미지 중 Core 컨테이너에 기본으로 포함되어있는 PowerShell을 이용하여 외부에서 웹 요청을 받아들일 수 있는 간단한 웹 서버 컨테이너를 만들어보겠습니다.&lt;/p&gt;
&lt;p&gt;이 글에서 설명하는 모든 명령줄은 PowerShell 문법을 사용합니다.&lt;/p&gt;
&lt;p&gt;우선 작업의 편의를 위하여 사용자 홈 디렉터리 아래에 새로운 작업용 디렉터리 &lt;code&gt;dockertest&lt;/code&gt;를 생성합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;powershell&quot;&gt;&lt;pre class=&quot;language-powershell&quot;&gt;&lt;code class=&quot;language-powershell&quot;&gt;cd &lt;span class=&quot;token variable&quot;&gt;$env&lt;/span&gt;:USERPROFILE
mkdir dockertest
cd dockertest&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;샘플 컨테이너에서 표시할 웹 페이지를 방금 만든 디렉터리 안에 추가합니다. 파일 이름은 &lt;code&gt;index.html&lt;/code&gt;로 만들고 원하는 내용으로 채워넣습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;powershell&quot;&gt;&lt;pre class=&quot;language-powershell&quot;&gt;&lt;code class=&quot;language-powershell&quot;&gt;&lt;span class=&quot;token function&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;&amp;lt;h1&gt;Hello, World! from Windows Container&amp;lt;/h1&gt;&apos;&lt;/span&gt; &gt; index&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;html&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;그리고 새로운 &lt;code&gt;Dockerfile&lt;/code&gt; 파일을 만들어 원하는 텍스트 편집기로 엽니다. 확장자를 붙이지 않도록 만들어야 하기 때문에, 명령어로 새 파일을 먼저 만드는 것이 정확합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;powershell&quot;&gt;&lt;pre class=&quot;language-powershell&quot;&gt;&lt;code class=&quot;language-powershell&quot;&gt;&lt;span class=&quot;token function&quot;&gt;New-Item&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;Type&lt;/span&gt; File &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;\Dockerfile&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Dockerfile의 내용은 다음과 같이 작성합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;text&quot;&gt;&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;FROM mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2019

WORKDIR C:\\Inetpub\\wwwroot
ADD index.html .

EXPOSE 80/tcp&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;위의 &lt;code&gt;Dockerfile&lt;/code&gt;은 웹 서버가 보여줄 &lt;code&gt;index.html&lt;/code&gt; 파일을 추가하는 것 이외에 특별한 내용이 없습니다. 다만 Windows 환경을 접할 기회가 많지 않으신 분들은 코드 중 일부가 무슨 의미인지 이해가 되지 않을 수 있습니다. 그 중 핵심적인 것 두 가지만 짚고 넘어가겠습니다.&lt;/p&gt;
&lt;p&gt;리눅스는 드라이브라는 개념이 없고 모든 것이 루트 디렉터리로부터 출발하지만, 모두가 잘 알고 있듯이 Windows는 루트 디렉터리라는 개념이 없고 드라이브라는 개념을 사용합니다. Windows 컨테이너 안에서도 이는 마찬가지입니다. 다만 호스트 OS의 파티션 설정을 공유하지 않으므로 컨테이너에는 C 드라이브만이 존재합니다. 그리고 왜 C 드라이브가 시작점인가 하면, 관습적으로 Windows OS는 시스템 파티션의 드라이브 문자를 C로 정하고 이를 기본값으로 택합니다.&lt;/p&gt;
&lt;p&gt;그리고 &lt;code&gt;WORKDIR&lt;/code&gt; 지시자를 사용하여 &lt;code&gt;C:\Inetpub\wwwroot&lt;/code&gt; 디렉터리를 기본 경로로 잡은 부분에 대해 설명하면 이렇습니다. 예제에서 사용하는 컨테이너는 IIS 컨테이너이며, IIS 컨테이너는 Windows OS가 설치된 파티션에 할당된 드라이브에 &lt;code&gt;\Inetpub\wwwroot&lt;/code&gt; 디렉터리를 만들어 첫 웹 사이트를 띄웁니다. 이 경로를 원하는대로 바꿀 수는 있지만, 예제에서는 빠른 이해를 돕기 위해 굳이 변경하지 않았습니다.&lt;/p&gt;
&lt;p&gt;이제 위의 &lt;code&gt;Dockerfile&lt;/code&gt;을 이용하여 웹 서버 컨테이너를 빌드해보겠습니다.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️ 빌드를 하기 전에, 현재 작업 중인 컴퓨터가 종량제 인터넷을 사용하고 있다면 접속 수단을 먼저 바꾸는 것이 좋습니다. Windows 컨테이너의 Base 이미지 크기는 종류에 따라 다르지만 최소 1GiB 내외에서 최대 4GiB까지 매우 크기 때문에 종량제 플랜을 사용할 경우 사용량을 모두 소진하거나 추가 과금을 일으킬 수 있으므로 주의해야 합니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;powershell&quot;&gt;&lt;pre class=&quot;language-powershell&quot;&gt;&lt;code class=&quot;language-powershell&quot;&gt;docker build &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;t wwwtest:latest &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;처음 빌드를 할 때에는 이미지를 받아올 때 시간이 많이 소요될 수 있습니다.&lt;/p&gt;
&lt;p&gt;빌드가 완료된 후에는 다음과 같이 명령어를 실행하여 IIS 컨테이너를 실행합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;powershell&quot;&gt;&lt;pre class=&quot;language-powershell&quot;&gt;&lt;code class=&quot;language-powershell&quot;&gt;docker run &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;d &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;p 8080:80 &lt;span class=&quot;token operator&quot;&gt;--&lt;/span&gt;name=wwwtest wwwtest:latest&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이제 웹 서버가 잘 실행되었는지 확인하기 위하여 웹 브라우저로 다음 주소에 접속합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;powershell&quot;&gt;&lt;pre class=&quot;language-powershell&quot;&gt;&lt;code class=&quot;language-powershell&quot;&gt;http:&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;localhost:8080&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;아래 그림과 같이 여러분이 만든 &lt;code&gt;index.html&lt;/code&gt; 페이지가 잘 나타나면 성공한 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1249px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 73.88888888888889%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAIAAABr+ngCAAAACXBIWXMAABnWAAAZ1gEY0crtAAABQElEQVR42u2RS0/CQBSF+4/ckFpaKYWCj0RNbIX0AY3+NKOJQB+0aKJu8LFScQNDwUUTIKEbFpVdQ0tbB4huJCTi1i9nTu5MzsldDMLm8nv7B4dHDJ3dwRMkQSYz2e1TqSCJfFHgJCiRX8zCTDlR5KSCUBT5k6KIEFQ6TqagUJwkkmksQW3EMBSLoxi+GSegQ8VQbIukjjOZPM2x9G4iRbMMk0rTSFXXL8ulm9u7UkWuGoasqIpWVVRNVrWz84tSuVKRFePqGl5rMKoYNd2oPz49v7zW7x8Qy7LeGo3hcGiaJgAAer/Xe+92B4N+G4A2PKBl2zaMgTZotppmp+P7fhRFnuch0+k0DMOFQ4IgCH8QzFnM0VfYdV3ku7a0s/Q9mjPbDBPRWvyX1yiv+KoVwPJkMvnbZsdxPn7PeDwejUafUDUL4pEQYFUAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/9cedb3b6d85cc30b5c73b6d8f2710fa9/33ecc/first-iis-container.webp 360w,
/static/9cedb3b6d85cc30b5c73b6d8f2710fa9/263a4/first-iis-container.webp 480w,
/static/9cedb3b6d85cc30b5c73b6d8f2710fa9/0c8fb/first-iis-container.webp 640w,
/static/9cedb3b6d85cc30b5c73b6d8f2710fa9/521f7/first-iis-container.webp 860w,
/static/9cedb3b6d85cc30b5c73b6d8f2710fa9/cbd37/first-iis-container.webp 1200w,
/static/9cedb3b6d85cc30b5c73b6d8f2710fa9/db019/first-iis-container.webp 1249w&quot;
              sizes=&quot;(max-width: 1249px) 100vw, 1249px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/9cedb3b6d85cc30b5c73b6d8f2710fa9/53918/first-iis-container.png 360w,
/static/9cedb3b6d85cc30b5c73b6d8f2710fa9/9aebd/first-iis-container.png 480w,
/static/9cedb3b6d85cc30b5c73b6d8f2710fa9/0f09e/first-iis-container.png 640w,
/static/9cedb3b6d85cc30b5c73b6d8f2710fa9/3e3fe/first-iis-container.png 860w,
/static/9cedb3b6d85cc30b5c73b6d8f2710fa9/64756/first-iis-container.png 1200w,
/static/9cedb3b6d85cc30b5c73b6d8f2710fa9/8b667/first-iis-container.png 1249w&quot;
            sizes=&quot;(max-width: 1249px) 100vw, 1249px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/9cedb3b6d85cc30b5c73b6d8f2710fa9/8b667/first-iis-container.png&quot;
            alt=&quot;첫 웹 서버 컨테이너 실행 모습&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;첫 웹 서버 컨테이너 실행 모습&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;축하합니다! 이제 성공적으로 Windows 컨테이너를 개발할 수 있습니다.&lt;/p&gt;
&lt;p&gt;이제 만들어진 컨테이너를 제거하기 위하여 다음 명령을 실행합니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;powershell&quot;&gt;&lt;pre class=&quot;language-powershell&quot;&gt;&lt;code class=&quot;language-powershell&quot;&gt;docker &lt;span class=&quot;token function&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;f wwwtest&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;다음-시간에는&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8B%A4%EC%9D%8C-%EC%8B%9C%EA%B0%84%EC%97%90%EB%8A%94&quot; aria-label=&quot;다음 시간에는 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;다음 시간에는&lt;/h2&gt;
&lt;p&gt;이번 글에서는 IIS 웹 서버 컨테이너를 테스트하는 과정을 살펴보았습니다. 다음 글에서는 Windows 기반 백엔드에서 많이 채택되는 NT 서비스 기반의 프로세스를 컨테이너화하는 방법에 대해 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;h2 id=&quot;참고-자료&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B0%B8%EA%B3%A0-%EC%9E%90%EB%A3%8C&quot; aria-label=&quot;참고 자료 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/virtualization/windowscontainers/quick-start/quick-start-windows-10&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Windows 10에서 Windows Container 시작하기 (영어)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;/posts/migrate-nt-service-to-container&quot;&gt;다음 글 보기: NT 서비스를 Windows 컨테이너로 마이그레이션하기&lt;/a&gt;&lt;/p&gt;
&lt;style&gt;
article#article.pc span.gatsby-resp-image-wrapper {
    max-width: 70% !important;
}
&lt;/style&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[[Windows 컨테이너] 1: Windows 컨테이너에 대한 이해]]></title><description><![CDATA[Windows 컨테이너를 실무에 사용하기 위해서 알아야 할 기본적인 내용들을 설명합니다.]]></description><link>https://tech.devsisters.com/posts/intro-windows-container</link><guid isPermaLink="false">https://tech.devsisters.com/posts/intro-windows-container</guid><pubDate>Fri, 31 May 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;데브시스터즈는 &lt;a href=&quot;https://www.slideshare.net/seungyongoh3/ndc17-kubernetes&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Kubernetes 기반의 게임 개발 및 테스트 인프라를 성공적으로 구축&lt;/a&gt;하여 게임 출시에 이르는 모든 과정들을 효율적으로 처리하고 있습니다.&lt;/p&gt;
&lt;p&gt;다년간의 성공적인 운영 경험을 바탕으로 Windows Server 기반의 Workload를 지원하기 위한 R&amp;#x26;D 작업을 진행하였고, 그 경험담을 공유하는 글을 몇 편에 걸쳐 DevTech 기술 블로그에 연재할 예정입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Windows 컨테이너 시리즈 글 보기&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;/posts/intro-windows-container&quot;&gt;Windows 컨테이너에 대한 이해&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/start-windows-container-dev&quot;&gt;Windows 컨테이너 개발 환경 구축하고 테스트하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/migrate-nt-service-to-container&quot;&gt;NT 서비스를 Windows 컨테이너로 마이그레이션하기&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/posts/windows-container-basics&quot;&gt;베이스 이미지, 격리 방식에 대한 이해&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;누구를-위한-글인가&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%88%84%EA%B5%AC%EB%A5%BC-%EC%9C%84%ED%95%9C-%EA%B8%80%EC%9D%B8%EA%B0%80&quot; aria-label=&quot;누구를 위한 글인가 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;누구를 위한 글인가&lt;/h2&gt;
&lt;p&gt;이 글은 Windows 기반의 서버 인프라를 개발하고 운영하는 DevOps 엔지니어들, 그리고 Windows 기반의 소프트웨어 개발에 익숙한 개발자들 중 컨테이너 기술에 관심을 가지는 개발자들을 위하여 작성하였습니다.&lt;/p&gt;
&lt;p&gt;또한 이 글은 컨테이너의 기본 개념이나 Docker, Kubernetes의 사용법을 어느정도 숙지한 분들을 위한 글입니다. 만약 컨테이너, Docker 및 Kubernetes의 기본 개념을 처음 접하신다면 먼저 해당 개념들을 숙지하신 후에 이 글을 읽으실 것을 권장합니다.&lt;/p&gt;
&lt;h2 id=&quot;windows-컨테이너-소개&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#windows-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EC%86%8C%EA%B0%9C&quot; aria-label=&quot;windows 컨테이너 소개 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Windows 컨테이너 소개&lt;/h2&gt;
&lt;p&gt;Windows 컨테이너 기술은 한창 컨테이너 기술이 업계에서 주목을 받았던 시기일 때 부터 Microsoft가 직접 개발을 진행했습니다. 그리고 Windows Server 2016이 출시되면서 처음 선보였습니다. 그 후 2019년 봄에는 Kubernetes 1.14에서 Kubernetes Worker Node를 &lt;a href=&quot;https://kubernetes.io/blog/2019/03/25/kubernetes-1-14-release-announcement/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Windows Server 위에서 실행할 수 있도록&lt;/a&gt; 정식으로 지원하게 되었습니다.&lt;/p&gt;
&lt;p&gt;왜 Windows 컨테이너를 사용할까요? 장황하게 설명할 것도 없이 리눅스 컨테이너를 사용하는 것과 같은 이유일 것입니다. 그리고 왜 컨테이너 기술이 필요한지 반문을 하는 사람들 또한 거의 없을 것입니다.&lt;/p&gt;
&lt;figure&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 912px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 49.44444444444444%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAA7EAAAOxAGVKw4bAAACY0lEQVR42l1STU8TURSdpVEXmGhiYqIrt8bE/+EvUBcu3Lg1GqOiSVFCohCpEOsHQggExGpQkKQptUIVS9sYQwqlLS3tdJjW6ce0M2U6n8f7BjSRm9y83Hlv7j3nnsM5DmDbDixKUGFYNuLFNgp1A3zDgCAbSJY1bNd1ugdMy3Hf66aNtm6BhbOfLDilY6KlmVC0vVOlR8G0Ap1+lFRW25DaFsajEnxhHt4gj/5AEWPfdiDvmm5ThwaYBETpWODUjoW/wabG8k3MJ5tu3di1oBk2nTYmYlWMLAuUO/B9FTAdqxB6Ha9+1HBxrIgrUwI+rjXBpcQ2Etst/CwqiOaaGI3sYDgiYXZdxWxSwdyGgvGEjDWx4w6x97k5tB6RGj5dquG4J4MLg3m8/0UN/YnfuD6xiVvvMrg5k8WHRAWXJ3hwN9Zx+mEGXd0pHLu/iaGlCkaJ5sslAUMhHpPRMlayMt58L2MkUsbjAI+ZhAQuuFEjCiWMr4h4QVT88TKuTvE4dDeF8wM5nOnN4FRvFgMhkfZXwJNAAZ5PeTz/UkKcmLEh3kUej+YJIYHjFtaqeDCbw21/Fj1zeZeyd7mK1zEZvmgDo3EZg5E6Kf0/ZZsoS4pBrnD+aaBoJIrY1JEUVKySGDlJQzwn49Ikj8P3UjjXv4WTPWmc8KQxTJSZsiM08BlRnl6tuA1b++5gCtdUAxzzVZsK9kEz9hS/9lbAkTspnO3L4Cg17urexMCi6O6O0etbKLiNW4So3jZQqncgNDpoUmMOB4IZPJxV3QxlFYS3VHzeaJHRdfeeAWAu1k0CQh49GH8AXwy8NKflyYYAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/3914c269a68194a0a104fe2406b48e60/33ecc/containerfund.webp 360w,
/static/3914c269a68194a0a104fe2406b48e60/263a4/containerfund.webp 480w,
/static/3914c269a68194a0a104fe2406b48e60/0c8fb/containerfund.webp 640w,
/static/3914c269a68194a0a104fe2406b48e60/521f7/containerfund.webp 860w,
/static/3914c269a68194a0a104fe2406b48e60/d1de2/containerfund.webp 912w&quot; sizes=&quot;(max-width: 912px) 100vw, 912px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/3914c269a68194a0a104fe2406b48e60/53918/containerfund.png 360w,
/static/3914c269a68194a0a104fe2406b48e60/9aebd/containerfund.png 480w,
/static/3914c269a68194a0a104fe2406b48e60/0f09e/containerfund.png 640w,
/static/3914c269a68194a0a104fe2406b48e60/3e3fe/containerfund.png 860w,
/static/3914c269a68194a0a104fe2406b48e60/4a2a8/containerfund.png 912w&quot; sizes=&quot;(max-width: 912px) 100vw, 912px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/3914c269a68194a0a104fe2406b48e60/4a2a8/containerfund.png&quot; alt=&quot;containerfund&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;figcaption&gt;Windows 컨테이너 개요 - &lt;a href=&quot;https://docs.microsoft.com/en-us/virtualization/windowscontainers/about/&quot; target=&quot;_blank&quot;&gt;출처&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Windows 컨테이너는 리눅스 컨테이너와 유사한, 그리고 호환되는 아키텍처를 사용합니다. 그리고 기존의 리눅스 컨테이너를 빌드하는 방법과 동일하게 Dockerfile을 이용하여 원하는 내용을 추가할 수 있습니다.&lt;/p&gt;
&lt;p&gt;다만 Windows 컨테이너는 OS 이미지는 &lt;a href=&quot;https://hub.docker.com/_/microsoft-windows-base-os-images&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;mcr.microsoft.com 이미지 레지스트리&lt;/a&gt;로부터 내려받는다는 것, 그리고 컨테이너를 빌드하고 실행하기 위해서는 Windows 운영 체제가 필요하다는 것이 다른 점입니다.&lt;/p&gt;
&lt;p&gt;이렇게 만들어진 이미지로 새로운 Windows 컨테이너를 곧바로 실행하거나, 또는 Docker Hub나 직접 관리하는 이미지 레지스트리에 게시하여 다른 사람들에게 공유할 수 있습니다.&lt;/p&gt;
&lt;h2 id=&quot;windows-컨테이너-아키텍처&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#windows-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98&quot; aria-label=&quot;windows 컨테이너 아키텍처 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Windows 컨테이너 아키텍처&lt;/h2&gt;
&lt;p&gt;앞에서 살펴본 것처럼 Windows 컨테이너의 사용 방법은 리눅스 컨테이너와 큰 차이가 없습니다. 그러나 세부적인 내용을 살펴보면 기술적으로는 큰 차이를 가지고 있습니다.&lt;/p&gt;
&lt;p&gt;Windows 컨테이너에서 핵심이 되는 시스템은 크게 두 가지입니다. 바로 Host Compute Service (HCS)와 Host Network Service (HNS) 입니다.&lt;/p&gt;
&lt;figure&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1196px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 73.88888888888889%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAPCAIAAABr+ngCAAAACXBIWXMAAB2HAAAdhwGP5fFlAAACNUlEQVR42o2Q/08ScRjH+dOyH0qGiI5k0S85qFb5Q1s/NBva+qEhFGzBnHNrsaZNoLlatckcyATlS0tZVA5RgZOOL57I3XF3cF+4ux48ctoMe/bZs8/neX9en/fneVTyn5AkCXIsFnO5XB6PB3IgEICKKIryP0L1F5zP5yORSDQahZzJZE7qF8Cng6Zp+T9CddoWx3G32223261Wq8PhsNls1Wq1h3kXVhoroejQkE7d36/RaLQDA5f7+rLZbI+2z8Ll8qj59k3TLZPJbL5zV28w7uzuXgxLx3KlWDAbBo3aK1qN+rpObVBfym2le8FKPxzLEjRXjy/uvxhGXDdSy/7c7D1kSlP75MAZnmNb53bedeY4rtViZYmXBUrmKFkUZI7u7EUW6qCe73xQb2E4V6wQSAk/JIUDgufb4CEKbQmOGNmGerFKYjgLN3nhjLlKNx7XTyRgXZtI6i2JwfFYDqVAQMqk3rIOxZHJpKJefbgWT6M8y5CNRoOiRFFSLQSL/hXUF9z3BhH/yi9vqIg3Op88PGr4QwV/GPUGCyC9C6NvlxEEPVI8GYYRBKHbc7PZomnmZCrwPYBZjoc9RTNNGMfxzLEawfJgKdfqJN1kO3B4EzM+/WJ4klhNYcpbMx9yI5PxBy+/LSUqY87N+87Ux7XSo5nvw49jU/PbcwFk9FlizLnRgX/kCKdv5/lC9meeUGBg4Pjqc35juz79fs+9uJfcqr1ZQqxzGd9qdT2a/jptCb2e/Q0b5QmvV5RnuAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/35a4280cda4310ca67af58463b18d355/33ecc/hns-management-stack.webp 360w,
/static/35a4280cda4310ca67af58463b18d355/263a4/hns-management-stack.webp 480w,
/static/35a4280cda4310ca67af58463b18d355/0c8fb/hns-management-stack.webp 640w,
/static/35a4280cda4310ca67af58463b18d355/521f7/hns-management-stack.webp 860w,
/static/35a4280cda4310ca67af58463b18d355/3cb73/hns-management-stack.webp 1196w&quot; sizes=&quot;(max-width: 1196px) 100vw, 1196px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/35a4280cda4310ca67af58463b18d355/53918/hns-management-stack.png 360w,
/static/35a4280cda4310ca67af58463b18d355/9aebd/hns-management-stack.png 480w,
/static/35a4280cda4310ca67af58463b18d355/0f09e/hns-management-stack.png 640w,
/static/35a4280cda4310ca67af58463b18d355/3e3fe/hns-management-stack.png 860w,
/static/35a4280cda4310ca67af58463b18d355/ee2d9/hns-management-stack.png 1196w&quot; sizes=&quot;(max-width: 1196px) 100vw, 1196px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/35a4280cda4310ca67af58463b18d355/ee2d9/hns-management-stack.png&quot; alt=&quot;hns management stack&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;figcaption&gt;HCS와 HNS - &lt;a href=&quot;https://docs.microsoft.com/en-us/virtualization/windowscontainers/container-networking/architecture&quot; target=&quot;_blank&quot;&gt;출처&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;HCS와 HNS에 대한 이야기를 계속하기 위하여 몇 가지 소 주제로 나누어서 이야기를 해보겠습니다.&lt;/p&gt;
&lt;h3 id=&quot;컨테이너-런타임&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EB%9F%B0%ED%83%80%EC%9E%84&quot; aria-label=&quot;컨테이너 런타임 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;컨테이너 런타임&lt;/h3&gt;
&lt;p&gt;리눅스에서는 컨테이너가 아래 그림과 같은 아키텍처 위에서 실행됩니다.&lt;/p&gt;
&lt;figure&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 68.05555555555554%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAAAsTAAALEwEAmpwYAAADHElEQVR42kWTyW9bdRDH37/HgRttLygHKHCAC0LqoSoCIUpZmgSKQFQUUCWWILpRRAnEIY4dNU7jOE68xXYd7/bLs9/qJd4/zO85EU/6arbvjOc3M9amM/wv0YL1EoSq8KQAa6JvVud2pAF7OoRFL9gQFX2jArtN0Lvz/LMyaMPxlIlUNXoz0q0phydTjtozUq0Zh4bgZEbSmHJsTSlYMykwIyO8hPjiwlV5M8k/r6NZ3inNtothefT6p7TcUxyvi+V42ALT6WA4fb+Hum7SMBxfVz7H69GSvGZbuJ0BvcEYLVOxydcdcjVHpEuqrHSXrNgKmYqCS/mkSzR7QixnUJR3psV/VD3PE67opjdAW4s1ODi2iObb7AliedOXCru5Fv9EK/y9W2ZVEHtusl8Qbk7F5zyVt18wCSV0/we0F66tcemjIBeuz/HShxtcFHnx+gYLN8O89XWEN2495c2vtln4LOzHL0hsnjOXl24EefG9gF9UWwkW+WO7xC+BI1YCWZ7sVFjZLPLdX8rOcC+Y585qjoC85F6owMOtIqvRGrf/PGJ9v8HP6zkePy3x+3aFRNFCO5TnxmUukUSNZ+kGiWOTAwlkqzalhuMjW5vPsNBwKekeZaPrP6/a6vK8rnwdmWtHFjNECyd1/t2rEHhWYiNWJRSvs5U6IVGy/QUly/PFpMvnC3LmCxEkhaP8qbJNUprweiM0NdCG2afS6qFbfTbPhlsTuyRdKbkarftbVB3majYZKbAWq/sx1ZmSqnC+7qHpVg+306Np2IzHI3LiNKyuf196y2E4HBGXsdTl/vJlnVypSbPl+t2NJxMqDZO27eH1R1RkFFpbbkcd6mgw8P862aqL2x2Ka8pwNPZjcTmVwWCEZZg4pi3+CYfyRPUpzmQypqMKyky1d+/us/wwxc37KRYfpLh2N8aNXxMsiX7rcYYvBe//GOeLR2lfV/5Pf0tw9Yc9Fu/P85YepPngpwO52zbayx8HeW05zKtLIS4vhXn98y0un9nv3I5w5dsdrn6/y5U7O7z9zTavLP7PUxw/bznEwidBImmd/wAAGtOyQ2fmrAAAAABJRU5ErkJggg==&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/f4ecbec24caee389d3cafcd6e392f4fd/33ecc/container-arch-linux.webp 360w,
/static/f4ecbec24caee389d3cafcd6e392f4fd/263a4/container-arch-linux.webp 480w,
/static/f4ecbec24caee389d3cafcd6e392f4fd/0c8fb/container-arch-linux.webp 640w,
/static/f4ecbec24caee389d3cafcd6e392f4fd/521f7/container-arch-linux.webp 860w,
/static/f4ecbec24caee389d3cafcd6e392f4fd/d71bc/container-arch-linux.webp 1024w&quot; sizes=&quot;(max-width: 1024px) 100vw, 1024px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/f4ecbec24caee389d3cafcd6e392f4fd/53918/container-arch-linux.png 360w,
/static/f4ecbec24caee389d3cafcd6e392f4fd/9aebd/container-arch-linux.png 480w,
/static/f4ecbec24caee389d3cafcd6e392f4fd/0f09e/container-arch-linux.png 640w,
/static/f4ecbec24caee389d3cafcd6e392f4fd/3e3fe/container-arch-linux.png 860w,
/static/f4ecbec24caee389d3cafcd6e392f4fd/42a19/container-arch-linux.png 1024w&quot; sizes=&quot;(max-width: 1024px) 100vw, 1024px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/f4ecbec24caee389d3cafcd6e392f4fd/42a19/container-arch-linux.png&quot; alt=&quot;container arch linux&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;figcaption&gt;리눅스 컨테이너 아키텍처 - &lt;a href=&quot;https://docs.microsoft.com/en-us/virtualization/windowscontainers/deploy-containers/containerd&quot; target=&quot;_blank&quot;&gt;출처&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;그리고 Windows에서는 컨테이너가 아래 그림과 같은 아키텍처 위에서 실행됩니다.&lt;/p&gt;
&lt;figure&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1024px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 70%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAYAAAAvxDzwAAAACXBIWXMAAAsTAAALEwEAmpwYAAADPklEQVR42kWTi09bdRTH7x9oTHzMMZfMBWUZQbfFzJngizAHzLEXAhPmBkNiOqvETNxK19LS0gcIDIFWytpS2lLa0uftCyjl47kXjDf55Ps753zPr8255yrIkyzD8xB4E2CPwmQQHDHwbMNsXPKiCzvwKgUGP5jC4Iwdqyt+3HPQQH+U+mGDfK3BRvaItfQRgUyD1VSDFTn7do901XKRQoMtIZSHdYlXxLO4I94TX/XgCO0upVA+IFeokMkVUUsVcmqNQkkoqOQFTVP5CtX9OuXqHpuxNPV6nVL1gKxalXqJbF4lW6xQKO+jBLeLaARiBTZE/dECgXiRjbimkpO8L5InvFMSTx7XalxqUt9W8Yn3tZw1n3ZHeEdFmVqM49vKsxzKshzOsqKT0+O/RWfXEkwvRTEvRPD4k+It8OrEqxM6ZnUzh2M1ifLxgJc3Oyyc6rLx7g0b73xr4+3r07wncVOPndZ+Dxe/d9MqXOxzc7rbzukuO03dx2jnMz0zvNU5TfvoIsr4dIg/vFGemPyMvvDxszXAn3Mx+p9psZ9xs5+RqXUMM5s8n4vy2PQPpr/iPDJtYLCHmZgN81h8Ws/v7i2UrXRZhl4jnlaJp1QSuyVShRqvZSZbySIxIZSQOct8ouLdTJaIZSr6DMNJlVi6JDnR3bLUSyhxMQVlsJvSoBGW5pBcFjmJNSI64kmc5P9TuSik9QhBySWyFZSJCRc24xQvf3mJRbD+asH80ySmh0ZeDBsxDT3FPPYMi9Gi1zWfzWjGaLDjDWTk36v6VgRFXf4USq9hiSt9DtqHXHw2OMuXw246Rzx8Ido1Nq/rVw+1nJerA07aJb426KRzdJ7qXl12uERkO8Oh7GYkVUZpH1nkjQ4rLfflDfY4ONfr1N/qmZsOLg/Nc/aWk3NCm2xDS5+HtkEvzffcXHowR02WvXF4SG1vXz66I32+yuc/znHquoX3u6003bBy/jsbH962SzzNR3dmaJZz632H4NTjD27ZOSu15t4Zep6ucvs3H3cmfNw0rvFgch2lc3yJtn4XVwbdXBY+Gfifq8NeOsYWdb5+ssA3Ywt8+oOHSyf1C3edtNx1cOGeg/PyQz2GZf4Ferqoac8qrioAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/ab9fbf72f6a4f375eaaf69577c6ce8af/33ecc/container-arch-windows.webp 360w,
/static/ab9fbf72f6a4f375eaaf69577c6ce8af/263a4/container-arch-windows.webp 480w,
/static/ab9fbf72f6a4f375eaaf69577c6ce8af/0c8fb/container-arch-windows.webp 640w,
/static/ab9fbf72f6a4f375eaaf69577c6ce8af/521f7/container-arch-windows.webp 860w,
/static/ab9fbf72f6a4f375eaaf69577c6ce8af/d71bc/container-arch-windows.webp 1024w&quot; sizes=&quot;(max-width: 1024px) 100vw, 1024px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/ab9fbf72f6a4f375eaaf69577c6ce8af/53918/container-arch-windows.png 360w,
/static/ab9fbf72f6a4f375eaaf69577c6ce8af/9aebd/container-arch-windows.png 480w,
/static/ab9fbf72f6a4f375eaaf69577c6ce8af/0f09e/container-arch-windows.png 640w,
/static/ab9fbf72f6a4f375eaaf69577c6ce8af/3e3fe/container-arch-windows.png 860w,
/static/ab9fbf72f6a4f375eaaf69577c6ce8af/42a19/container-arch-windows.png 1024w&quot; sizes=&quot;(max-width: 1024px) 100vw, 1024px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/ab9fbf72f6a4f375eaaf69577c6ce8af/42a19/container-arch-windows.png&quot; alt=&quot;container arch windows&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;figcaption&gt;Windows 컨테이너 아키텍처 - &lt;a href=&quot;https://docs.microsoft.com/en-us/virtualization/windowscontainers/deploy-containers/containerd&quot; target=&quot;_blank&quot;&gt;출처&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;리눅스와는 다르게 HCS라는 핵심 구성 요소가 리눅스의 Control Group (cgroup), 네임스페이스, 파일 시스템 계층화 기능을 모두 담당합니다.&lt;/p&gt;
&lt;p&gt;containerd 기반의 런타임을 사용하는 리눅스와는 달리, 현재 docker.com 웹 사이트에서 회원 가입 후 내려받을 수 있는 Docker Community Editon (CE) 및 Enterprise Edition (EE)의 Windows 버전에서의 Windows Container는 HCS v1 API를 직접 지원하도록 수정된 Docker Engine을 사용합니다.&lt;/p&gt;
&lt;p&gt;위의 그림에는 표현이 되어있지 않지만, Windows의 경우에도 containerd의 Windows 버전이 현재 개발 중이며, containerd의 기능을 지원할 수 있도록 기능이 강화된 HCS v2 API가 새로 추가되었습니다. 추후 Kubernetes Windows Worker Node와 Windows용 Docker는 containerd를 경유하여 HCS v2와 상호작용하는 방식으로 개선될 것입니다.&lt;/p&gt;
&lt;h3 id=&quot;컨테이너-이미지&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EC%9D%B4%EB%AF%B8%EC%A7%80&quot; aria-label=&quot;컨테이너 이미지 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;컨테이너 이미지&lt;/h3&gt;
&lt;p&gt;HCS가 관리하는 영역 중에 큰 의미를 가지는 또 다른 부분인 컨테이너 이미지에 대한 이야기는 내용이 조금 길어서 따로 이야기해보겠습니다.&lt;/p&gt;
&lt;p&gt;기술적으로 Windows OS 컨테이너 이미지는 Hyper-V가 사용하는 VHDX (Hyper-V가 사용하는 가상 하드 디스크 이미지 포맷) 형식을 이용하여 차등분을 저장하는 형태로 관리됩니다.&lt;/p&gt;
&lt;p&gt;하지만 Open Container Initiative (OCI) 사양을 따르도록 되어있기 때문에 Windows OS만을 위한 별도의 이미지 레지스트리를 구축할 필요가 없습니다. 단, 뒤에서 설명할 라이선스 상의 이슈로 최초의 루트 이미지는 &lt;code&gt;mcr.microsoft.com&lt;/code&gt; 서버에서 받도록 되어있습니다.&lt;/p&gt;
&lt;p&gt;다음에 올릴 블로그 글에서 다시 설명하겠지만, 컨테이너 OS 이미지는 Windows Server 1809 (2019) 기준으로 용도에 맞게 총 3가지의 서버용 이미지가 제공됩니다.&lt;/p&gt;
&lt;p&gt;컨테이너화 하려는 대상 애플리케이션의 특성에 맞는 이미지를 찾아 Base 이미지로 사용하면 됩니다.&lt;/p&gt;
&lt;h3 id=&quot;컨테이너-네트워킹&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%82%B9&quot; aria-label=&quot;컨테이너 네트워킹 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;컨테이너 네트워킹&lt;/h3&gt;
&lt;p&gt;HCS에 의하여 컨테이너 런타임이 관리되는 것과 별도로 HNS라는 구성 요소가 네트워킹을 담당합니다.&lt;/p&gt;
&lt;p&gt;HNS는 컨테이너가 외부 네트워크와 정상적으로 통신할 수 있도록 가상 네트워크를 만들고, 그 네트워크에 컨테이너를 부착하는 일도 해줍니다.&lt;/p&gt;
&lt;p&gt;HNS에 대해 설명하기에 앞서 Hyper-V Virtual Switch에 대한 이야기를 먼저 해보겠습니다.&lt;/p&gt;
&lt;figure&gt;
&lt;span class=&quot;gatsby-resp-image-wrapper&quot; style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1060px; &quot;&gt;
      &lt;span class=&quot;gatsby-resp-image-background-image&quot; style=&quot;padding-bottom: 88.05555555555556%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAIAAADUsmlHAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAD+ElEQVR42m3Se0xaVxwH8Puv+2vLtj/2x7L+sbQqKlBAnKuFqijoypX31VpbJ+iVimWIoI22FljQWR9w7wVBa+oDNl3nE6pURYFZtTJbk2XZ0yzL/tDNuq3YytCOXdgj2ePkk5Nzfiff80tODpC7epAdjOTg1mNy1yMFgf1C32OOP5TvC3EWf2Hhi6XH+f4Qcy38hzNr4azV8Nn7T4Hn9SsJ1ZMJte7n4hIUrle0d1+9Mv9a08JxnS9J7z9h8J/Q+481e1/UzL2kjXm5Yf6FutljnZ8ArOaZDmRejy3qzV4D4r1uWrhhD1iHVzv7AlxpPwOy5JT0sEptp8UW6llTOjcONFHZ7Zn1k4AAXXn49U8Lnz6aWNtxBX+Yur8zsbrt/2xv7uE2UzaYCKJEUU+KwJom7CGKbURRDBmyp/FQaqMb4JuWNcgd6Y1xJeZSmKeUqEuFuaXt4429XjY8nMrHshU3xVed0FWHuNkhaXZA15xs9QANQqlaNyAw3yvXf5BZ42SpZnOUblx+3eypmgF51zSn2pEqwHJUfWzNJKfew66f4Wg8hVpPnnaYXvpXWGYcZdXdedu4WWbYKNUFi1seZCtvK8xuDuxME2DZypvglaXzhs0S3YMSXVDYFGTVj9DPIX+Gq1pHmcoZUctmRetGuXEDbAji4ctmNxt24p1z3+lnq+dKWz6Gjb4LhvUC9Vre32Ghefm8bvRMRZuspuktWdsp+SRD4abD/VUdU/HOaLbqVlFdf5W8tqiyJV81nae+m6cZyihFKfHwvdYh/NZBlQG5bJ6qQVwKxFXdPW10ruTDQ6l8lCHvZVajjAt6ptzOvNTLlNsYir50CIt1Lmj3TS1+45j/asjzxYdL344ubo14tz7ybQ3MfJ5VPkTg2VJBEwE0pwjsZKGNJLAQ+VaSyJbGRagNbiC5diydj50UW04KESrUS4XsOArUlwEhHImaDWlyBUp2sZYtUWWI2omiXorEThFbiFxTLJyoGCNzTfirpnA7UsGuuE68FRW8XlbMg6sq5LAUhmVlxeCbRQ1kgakCVvHLVBRuG6VhBkiqHaeACFlsIwktJCEWhxKFVpqo/ZxEPFzLXTBKx9RFlRD4hlBHFllPi67RIVMaiNHwH5Z06TalsIvMx8g89J8QGr+bITFnCrsYEEIXdOMVMg8j8nrIfAuxsIummQZeb/QQLg4mV76fLHP+y3HpSEa5JYunoUnaEqUjBJmTUOkkyBz4nHhxkKT3AoEfI/7/COweBnYja/vRWxMu03vvfvnd94G9Z4s7Ee82fnq4/OgosHu0uncERP93/HYUjRxEn/36ZD8UCu3HKofhp6EnoZ9Dh+GD6FEY30YPw78DjBoxIGA0eVgAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;&gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source srcset=&quot;/static/e3b1c5175b9dbc55142bd13707f34231/33ecc/network-compartment-visual.webp 360w,
/static/e3b1c5175b9dbc55142bd13707f34231/263a4/network-compartment-visual.webp 480w,
/static/e3b1c5175b9dbc55142bd13707f34231/0c8fb/network-compartment-visual.webp 640w,
/static/e3b1c5175b9dbc55142bd13707f34231/521f7/network-compartment-visual.webp 860w,
/static/e3b1c5175b9dbc55142bd13707f34231/668f0/network-compartment-visual.webp 1060w&quot; sizes=&quot;(max-width: 1060px) 100vw, 1060px&quot; type=&quot;image/webp&quot;&gt;
          &lt;source srcset=&quot;/static/e3b1c5175b9dbc55142bd13707f34231/53918/network-compartment-visual.png 360w,
/static/e3b1c5175b9dbc55142bd13707f34231/9aebd/network-compartment-visual.png 480w,
/static/e3b1c5175b9dbc55142bd13707f34231/0f09e/network-compartment-visual.png 640w,
/static/e3b1c5175b9dbc55142bd13707f34231/3e3fe/network-compartment-visual.png 860w,
/static/e3b1c5175b9dbc55142bd13707f34231/ea60f/network-compartment-visual.png 1060w&quot; sizes=&quot;(max-width: 1060px) 100vw, 1060px&quot; type=&quot;image/png&quot;&gt;
          &lt;img class=&quot;gatsby-resp-image-image&quot; src=&quot;/static/e3b1c5175b9dbc55142bd13707f34231/ea60f/network-compartment-visual.png&quot; alt=&quot;network compartment visual&quot; title=&quot;&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
&lt;figcaption&gt;Host Network Service 아키텍처 - &lt;a href=&quot;https://docs.microsoft.com/ko-kr/virtualization/windowscontainers/container-networking/network-isolation-security&quot; target=&quot;_blank&quot;&gt;출처&lt;/a&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;컨테이너를 사용하는 환경이 아니더라도, Windows는 Hyper-V를 사용하도록 구성하면 OS 차원에서 Hyper-V Virtual Switch라는 것을 만듭니다. 그 후에는 실제 NIC와 직접 통신하지 않고 이 Virtual Switch에 부착되는 vNIC을 통해 호스트 컴퓨터가 NIC와 통신하도록 시스템 구성이 바뀌게 됩니다.&lt;/p&gt;
&lt;p&gt;이 Virtual Switch에는 Virtual Filtering Platform (VFP)이라는 구성 요소가 내장되어있습니다. 이 구성 요소는 Software Defined Networking을 구현하면서, 가상 네트워크 환경과 외부 네트워크 사이의 Ingress/Egress를 처리하게 됩니다.&lt;/p&gt;
&lt;p&gt;정리하면 Windows 서버 상에서는 Virtual Switch에 연결되는 방식에는 조금씩 차이가 있지만, 컨테이너이건 가상 컴퓨터이건 Hyper-V Virtual Switch를 통해 연결되는 방식입니다.&lt;/p&gt;
&lt;p&gt;이렇게 만들어지는 복잡한 네트워크 구성을 프로그래밍 방식으로 쉽게 제어할 수 있도록 해주는 것이 HNS입니다.&lt;/p&gt;
&lt;h2 id=&quot;windows-컨테이너의-라이선싱&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#windows-%EC%BB%A8%ED%85%8C%EC%9D%B4%EB%84%88%EC%9D%98-%EB%9D%BC%EC%9D%B4%EC%84%A0%EC%8B%B1&quot; aria-label=&quot;windows 컨테이너의 라이선싱 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;Windows 컨테이너의 라이선싱&lt;/h2&gt;
&lt;p&gt;Windows Server에서 가장 까다롭고 골치아픈 부분은 역시 라이선스에 대한 부분입니다.&lt;/p&gt;
&lt;p&gt;리눅스와 달리 Windows는 오직 Microsoft만이 이미지를 제작하고 배포할 수 있는 독점권을 가지고 있습니다. 생각해보면 이것은 당연한 이야기입니다. 그리고 이것은 컨테이너 OS 이미지에 대해서도 마찬가지입니다.&lt;/p&gt;
&lt;p&gt;이 때문에 Windows 컨테이너 OS 이미지를 실제 서비스용으로 사용하기 위해서는 Windows Server 호스트에 대한 라이선스를 보유하고 있어야 합니다.&lt;/p&gt;
&lt;p&gt;하지만 클라우드 컴퓨팅 환경 (AWS, Azure, GCP 등)에서 Windows Server 인스턴스를 사용하고 계시다면 대체로 라이선스에 대한 고민은 하지 않아도 됩니다.&lt;/p&gt;
&lt;p&gt;그러나 직접 Windows 서버를 데이터센터에 구축해서 사용하고 있거나, 그럴 계획이라면 반드시 &lt;a href=&quot;https://www.microsoft.com/en-us/licensing/product-licensing/windows-server&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;이 페이지&lt;/a&gt;의 내용을 검토하시는 것이 좋습니다.&lt;/p&gt;
&lt;p&gt;기본적으로 보유하고 있는 Windows Server OS의 라이선스의 상품 종류에 상관 없이 프로세스 격리 방식의 컨테이너는 실행 개수에 제한이 없습니다.&lt;/p&gt;
&lt;p&gt;다만 Standard Edition로 실행 중인 Windows Server에서는 라이선스 상 Hyper-V 컨테이너 방식의 컨테이너는 최대 2개만 실행이 허용됩니다. 라이선스 상 갯수 제약 없이 Hyper-V 컨테이너를 실행하려면 Datacenter Edition에 대한 라이선스를 구매하여 적용해야 합니다.&lt;/p&gt;
&lt;p&gt;아울러 Windows 10 PC에서 컨테이너를 만들고 테스트하는 것은 어떤 형태로 Windows 10 라이선스를 사용하고 있던 관계없이 &lt;a href=&quot;https://docs.microsoft.com/en-us/virtualization/windowscontainers/about/faq#can-i-run-windows-containers-in-process-isolated-mode-on-windows-10-enterprise-or-professional&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;개발과 테스트 목적으로만 기능과 라이선스가 제한&lt;/a&gt;됩니다.&lt;/p&gt;
&lt;h2 id=&quot;다음-글에서는&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%8B%A4%EC%9D%8C-%EA%B8%80%EC%97%90%EC%84%9C%EB%8A%94&quot; aria-label=&quot;다음 글에서는 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;다음 글에서는&lt;/h2&gt;
&lt;p&gt;개발 환경에서 Windows 컨테이너를 빌드하고, 테스트하여, Docker Hub에 게시하는 과정을 살펴보도록 하겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;/posts/start-windows-container-dev&quot;&gt;다음 글 보기: Windows 컨테이너 개발 환경 구축하고 테스트하기&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;참고-자료&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%B0%B8%EA%B3%A0-%EC%9E%90%EB%A3%8C&quot; aria-label=&quot;참고 자료 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;참고 자료&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.microsoft.com/ko-kr/virtualization/windowscontainers/about/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Windows 컨테이너 정보&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.microsoft.com/en-us/licensing/product-licensing/windows-server&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Windows Server Volume License 정보 (영어)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/windows/desktop/apiindex/umbrella-lib-onecore&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;OneCore Umbrella Library 레퍼런스 (영어)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/virtualization/windowscontainers/deploy-containers/system-requirements&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Windows 컨테이너 요구 사항 (영어)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://techcommunity.microsoft.com/t5/Containers/Introducing-the-Host-Compute-Service-HCS/ba-p/382332&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Host Compute Service 소개 (영어)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/virtualization/windowscontainers/container-networking/architecture&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Windows 컨테이너 네트워킹 (영어)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/virtualization/windowscontainers/container-networking/network-isolation-security&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;네트워크 격리와 보안 (영어)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;style&gt;
article#article.pc span.gatsby-resp-image-wrapper {
    max-width: 70% !important;
}
&lt;/style&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[‘테스트 케이스’로만 테스트하면 안되나요?]]></title><description><![CDATA[테스트 케이스 기반 테스팅의 허점과
쉽게 탐색적 테스팅을 수행하는 방법을 알아봅니다.]]></description><link>https://tech.devsisters.com/posts/not-enough-testcase</link><guid isPermaLink="false">https://tech.devsisters.com/posts/not-enough-testcase</guid><pubDate>Mon, 27 May 2019 00:00:00 GMT</pubDate><content:encoded>&lt;h2 id=&quot;안전하고-안심할-수-있는-게임을-만들고-싶어&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%95%88%EC%A0%84%ED%95%98%EA%B3%A0-%EC%95%88%EC%8B%AC%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-%EA%B2%8C%EC%9E%84%EC%9D%84-%EB%A7%8C%EB%93%A4%EA%B3%A0-%EC%8B%B6%EC%96%B4&quot; aria-label=&quot;안전하고 안심할 수 있는 게임을 만들고 싶어 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;안전하고 안심할 수 있는 게임을 만들고 싶어&lt;/h2&gt;
&lt;p&gt;게임은 소프트웨어 프로그램 중에서도 특히나 여러가지 요소가 유기적으로 작용하는 아주 복잡한 콘텐츠다. 그만큼 엮어진 부분들이 많기 때문에 버그도 많이 발생하는 편이다. 슬프게도 테스트를 통해 찾는 버그도 매우 많지만 그만큼 찾지 못하는 버그도 꽤나 많다. **‘버그가 없는 것이 아니다. 다만 발견하지 못했을 뿐이다.’**라는 말이 있듯이, 게임과 버그는 떼려야 뗄 수 없는 애증의 관계가 아닐까 싶다.&lt;/p&gt;
 &lt;br/&gt;
&lt;p&gt;개인적으로 가장 행복한 상황을 생각해 본다면 &lt;strong&gt;모든 기능이 정상 동작하는 것&lt;/strong&gt;과 &lt;strong&gt;CS 버그리포트가 들어오지 않는 것&lt;/strong&gt;이다. 하지만...&lt;/p&gt;
&lt;p&gt;테스트할 땐 문제없더니 프로덕션에 릴리즈 하기만 하면 버그가 빵빵 터지는 빌드도 있고 갑작스러운 서버 이슈, 데이터 누락 같은 예상 못한 문제가 왕왕 발생한다. 심지어 어떻게 한거지 싶을 정도로 기상천외한 버그 사면초가에 빠진 CS 리포트를 보면서 ‘아, 완벽한 테스트는 거의 불가능하구나…’ 라는 것을 느낀다.&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;하지만 현실이 이렇다고 해서 게임 서비스를 포기할 수는 없다. 그러므로 우리가 게임을 통해 유저들에게 좋은 경험을 선사하려면 QA팀은 항상 일정 수준의 품질을 보증해야 한다. 그래서 나는 우리가 유저들에게 **“우리 게임은 안전합니다! 안심하고 플레이하십시오!”**라고 자신있게 이야기하는 방법에 대해서 한번 고민해 보려고 한다.&lt;/p&gt;
&lt;h2 id=&quot;완벽한-문서들로-구원받을-수-있을까&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%99%84%EB%B2%BD%ED%95%9C-%EB%AC%B8%EC%84%9C%EB%93%A4%EB%A1%9C-%EA%B5%AC%EC%9B%90%EB%B0%9B%EC%9D%84-%EC%88%98-%EC%9E%88%EC%9D%84%EA%B9%8C&quot; aria-label=&quot;완벽한 문서들로 구원받을 수 있을까 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;완벽한 문서들로 구원받을 수 있을까?&lt;/h2&gt;
&lt;p&gt;게임 콘텐츠는 기획을 통해 만들어진다. 예를 들어 새로운 쿠키를 출시하기 위해 기획팀이 기획서를 만든다고 가정하자. 다양한 세부 기획까지 만들어야 하기 때문에 몇날 며칠을 열심히 기획한다. 일주일 후에 잔짜잔!! 드디어 부족하거나 넘치는 부분도 없고 누락된 곳도 없는 완벽한 기획서가 탄생했다. 기획팀이 뿌듯한 마음으로 기획서를 프로젝트 팀 내부에 공유해 준다.&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;그러면 기획서가 완벽하게 나왔으니 QA팀이 일할 차례다. 이 기획서들을 바탕으로 테스트 케이스(Test Case, TC)를 꼼꼼하게 작성한다. QA 엔지니어가 몇일 동안 신나게 테스트 케이스를 작성하고 수백개의 항목이 나열된 TC를 보며 흐뭇한 미소와 함께 나지막이 혼자 말한다.&lt;/p&gt;
&lt;br/&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;크~ 완벽한 기획서로 만든 꼼꼼한 테스트 케이스니까&lt;/strong&gt; &gt; &lt;strong&gt;케이스 수행만 정확하게 하면 완벽하게 테스트 되겠네. ㅎㅎㅎ&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br/&gt;
&lt;p&gt;
답은 &lt;code&gt;그렇지 않다&lt;/code&gt;이다.
&lt;p&gt;음? 완벽한 기획서랑 테스트 케이스가 있는데 왜 테스트 결과는 완벽하지 않을까?&lt;/p&gt;
&lt;p&gt;그건 우리가 &lt;code&gt;Happy Path&lt;/code&gt;의 오류를 범하고 있기 때문이다.&lt;/p&gt;
&lt;/p&gt;
&lt;h2 id=&quot;행복의-위험성&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%96%89%EB%B3%B5%EC%9D%98-%EC%9C%84%ED%97%98%EC%84%B1&quot; aria-label=&quot;행복의 위험성 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;‘행복’의 위험성&lt;/h2&gt;
&lt;p&gt;먼저 Happy Path Testing이라는 개념을 조금 짚고 넘어가야 할 것 같다. 직역하자면 &lt;strong&gt;행복 경로 테스팅&lt;/strong&gt;이다. 이 개념은 테스트 결과가 반드시 통과이거나 통과할 것이라 예상하는 기능을 테스트하는 것인데 정상적인 결과값이 반드시 나와야 하는 핵심 기능에 대해 테스트할 때 이 개념이 사용된다. 한번쯤은 들어 봤을 All-Pass Scenario, Positive Testing과 비슷하다.&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;어쨌든 말만 들어서는 왠지 좋은 의미 같아서 문제 없을 것 같은 느낌이 든다.&lt;/p&gt;
&lt;p&gt;그런데 이 개념은 무시무시하게도 다음과 같은 가정을 깔고 시작한다.&lt;/p&gt;
&lt;br/&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;단, (환경, 조건, 입력 값, 사용자) 등의 변수가 가장 이상적인 상태일 때만&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br/&gt;
&lt;p&gt;그렇다. Happy Path Testing이란 **‘모든 외부 변수나 조건들이 정상인 상태에서, 기능들이 정상적으로 동작하는지를 확인하는 것’**이다. 여기서 정상적인 상태란 이해관계자들과 유저, 그리고 기획팀이 설정한 이상적인 기준을 의미한다. 쉽게 예를 들자면 기획, 개발, 서버, 데이터, 밸런스가 누락 및 실수 없이 우리가 생각한대로 100% 잘 반영되고 게임 외부의 환경이 완벽한 상태에서 테스트를 진행한다는 것이다.
이렇게 완벽한 상태라면 버그가 있을수 없다.&lt;/p&gt;
&lt;p&gt;그래서 QA팀이 집중적으로 테스트가 필요한 순간에 이런 생각을 가지고 테스트하면 매우 위험하다. 이는 Happy Path Testing이 다음과 같은 위험성을 내포하기 때문이다.&lt;/p&gt;
&lt;h3 id=&quot;1-게임-외부-환경-변수를-무시한다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-%EA%B2%8C%EC%9E%84-%EC%99%B8%EB%B6%80-%ED%99%98%EA%B2%BD-%EB%B3%80%EC%88%98%EB%A5%BC-%EB%AC%B4%EC%8B%9C%ED%95%9C%EB%8B%A4&quot; aria-label=&quot;1 게임 외부 환경 변수를 무시한다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;1. 게임 외부 환경 변수를 무시한다.&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;모바일 디바이스는 그 종류와 성능이 매우 다양하다. 만약 성능이 너무 낮거나 특정 칩셋에 오류가 있다면?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;각 나라별로 서비스하는 통신사의 통신망은 속도와 환경, 품질이 천차만별이다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;유저가 사용하는 디바이스의 용량이 부족해서 오류가 생길 수도 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;각 OS별로 발생하는 알려진 이슈(Known-Issue)를 파악하지 못했다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;2-예측하지-못한-유저의-사용-패턴-및-반응을-고려하지-않는다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-%EC%98%88%EC%B8%A1%ED%95%98%EC%A7%80-%EB%AA%BB%ED%95%9C-%EC%9C%A0%EC%A0%80%EC%9D%98-%EC%82%AC%EC%9A%A9-%ED%8C%A8%ED%84%B4-%EB%B0%8F-%EB%B0%98%EC%9D%91%EC%9D%84-%EA%B3%A0%EB%A0%A4%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94%EB%8B%A4&quot; aria-label=&quot;2 예측하지 못한 유저의 사용 패턴 및 반응을 고려하지 않는다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;2. 예측하지 못한 유저의 사용 패턴 및 반응을 고려하지 않는다.&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;모든 유저가 게임 내 기능을 우리 의도대로 사용하지 않는다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;게임 외적으로도 그렇다. 모바일 게임이지만 모바일 디바이스로 플레이 하지 않는다면?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;기능상 허점을 이용해서 어뷰징(Abusing), 크래킹(Cracking)을 하는 못된 유저들도 존재한다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;우리가 좋다고 생각한 기능이 유저들에게 항상 좋은 결과를 가져다 주는 것은 아니다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;3-사람이-항상-완벽하게-일을-처리할-것이라-가정한다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-%EC%82%AC%EB%9E%8C%EC%9D%B4-%ED%95%AD%EC%83%81-%EC%99%84%EB%B2%BD%ED%95%98%EA%B2%8C-%EC%9D%BC%EC%9D%84-%EC%B2%98%EB%A6%AC%ED%95%A0-%EA%B2%83%EC%9D%B4%EB%9D%BC-%EA%B0%80%EC%A0%95%ED%95%9C%EB%8B%A4&quot; aria-label=&quot;3 사람이 항상 완벽하게 일을 처리할 것이라 가정한다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;3. 사람이 항상 완벽하게 일을 처리할 것이라 가정한다.&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;코드에 오타가 생겨 버그가 생길 수도 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;데이터 작업 시 빈 값, 엉뚱한 값을 넣을 수도 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;깜빡하고 데이터를 반영시키지 않을 수도 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;실수로 잘못 눌러서 반영하면 안되는 데이터가 반영될 수도 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;파일 이름을 잘못 설정해서 값을 불러오지 못할 수도 있다.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;br/&gt;
&lt;p&gt;일반적인 테스트 케이스는 기획서를 기반으로 재현 조건과 기대 결과를 도출하여 기대 결과가 정상적으로 출력되는지 확인하는 Positive Test에 가깝다. 그렇기 때문에 정상 범주 밖에서 발생하는 버그에 대해서는 테스트 케이스로 보증하지 못한다. TC 자체가 Happy Path의 위험성을 그대로 가지고 있다는 뜻이다.&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;물론 일부러 비정상적인 상황들을 케이스화 하여 테스트하는 &lt;strong&gt;Negative Test&lt;/strong&gt; 같은 반대되는 테스팅 기법도 존재한다. 하지만 비정상 상황은 너무 다양하기 때문에 모두 케이스화 하는 것은 현실적으로 불가능하다. 또한 게임처럼 잦은 업데이트가 필연적인 콘텐츠는 이런 케이스를 만들기에 인적, 시간적 여유가 부족하다.&lt;/p&gt;
&lt;br/&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;“그러면 테스트 케이스를 더 상세하게 쓰면 되는 거 아니에요? ㅇㅅㅇ?”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br/&gt;
&lt;p&gt;안타깝게도 기획서와 요구사항은 완벽하지 않을 가능성이 높다. 이건 기획팀이 열심히 일하지 않아서가 아니다. 똑똑하고 꼼꼼한 사람도 복잡한 게임 내 요소들을 전부 고려하기는 힘들다. 그리고 개인 컨디션에 따라 작은 실수도 종종 하기 마련이기 때문에 기획은 허점이 생기고, 허점이 있는 기획을 보고 쓴 테스트 케이스는 그만큼 구멍이 생길 수 밖에 없다.&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;그리고 테스트 케이스는 쓰는 것만 시간이 드는 것이 아니라 테스트를 수행하는 데에도 시간이 걸린다. 테스트 케이스가 상세하면 상세 할수록 테스트 시간이 오래 걸리고, QA 엔지니어의 피로도가 높아진다. 바쁜 일정 중에 사소한 케이스 수행을 위해서 많은 시간을 할애한다면 테스트 케이스는 충실히 실행했을 지 몰라도 효율성 측면에서는 빵점이다.&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;그러므로 우리는 시각을 좀 바꿔볼 필요가 있다. 테스트 케이스의 바깥을 한번 살펴보는 것이다.&lt;/p&gt;
&lt;p&gt;활발히 개발중인 프로젝트 내 소속된 QA팀의 임무는 ‘잠재적 위험을 제거하여 소프트웨어에 대한 일정 수준의 품질을 보증하고, 사용자에게 최고의 경험을 선사한다.’이다. 잠재적 위험은 TC 바깥의 미지의 세계에 도사리고 있다. 우리가 안전하고 안심할 수 있는 게임을 만들기 위해서는 마음을 굳게 먹고 TC 바깥 세상으로 떠나야 한다.&lt;/p&gt;
&lt;h2 id=&quot;테스트-케이스에서-탈출하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BC%80%EC%9D%B4%EC%8A%A4%EC%97%90%EC%84%9C-%ED%83%88%EC%B6%9C%ED%95%98%EA%B8%B0&quot; aria-label=&quot;테스트 케이스에서 탈출하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;테스트 케이스에서 탈출하기&lt;/h2&gt;
&lt;p&gt;테스트 케이스안에서 발생하는 버그의 비율은 과연 얼마나 될까? 대략 0.5% 안팎으로 발생한다. 쿠키런 오븐브레이크 프로젝트는 한 번 업데이트에서 대략 200건 안팎의 버그가 발생하는데 그 중에서 TC로 확인한 버그는 1~2건이 채 안된다. 게다가 이런 버그들은 대부분 빠르게 수정이 가능해서 프로덕션 환경까지 버그를 가지고 가는 경우는 극히 드물다.&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;이는 대부분의 버그가 테스트 케이스 바깥에서 발생한다는 것을 반증한다. 그렇기 때문에 만약 QA팀이 TC안에서만 웅크리고 있다면 게임을 온실속의 화초처럼 키우는 꼴 밖에 안된다. 바깥세상은 험하고 냉혹한데 세상물정 모르고 자란 게임이 바깥에 나가면 탈이 날 수 밖에 없다.&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;그러면 TC밖으로 떠나야 한다는 것은 알겠는데, 어떻게 떠나야 할까?
예전에 내가 공유받은 재미있는 이야기가 있다. TC밖으로 떠난다는 게 어떤 건지 쉽게 알 수 있다.&lt;/p&gt;
&lt;br/&gt;
&lt;blockquote&gt;
&lt;p&gt;한 QA 엔지니어가 술집에 들어왔다.&lt;/p&gt;
&lt;p&gt;먼저 맥주를 주문한다.&lt;/p&gt;
&lt;p&gt;그 다음 0개의 맥주를 주문해본다.&lt;/p&gt;
&lt;p&gt;999999999999개의 맥주도 주문해본다.&lt;/p&gt;
&lt;p&gt;맥주 대신 도마뱀도 주문해본다.&lt;/p&gt;
&lt;p&gt;-1개의 맥주도 주문해본다.&lt;/p&gt;
&lt;p&gt;&apos;ㅕ댜츄ㅏ너옹&apos;도 주문해본다.&lt;/p&gt;
&lt;p&gt;드디어 진짜 첫 손님이 술집에 들어와서 이렇게 물었다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;“화장실이 어디에요?”&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;그러자 술집이 불에 휩싸여 모두가 죽고 말았다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br/&gt;
&lt;p&gt;
우리가 TC를 떠난다는 것은 &lt;code&gt;맥주를 주문하는 기능&lt;/code&gt;에서 &lt;code&gt;도마뱀&lt;/code&gt;을 주문해보는 생각을 의미한다.
그러나 위 엔지니어는 그럼에도 불구하고 손님이 “화장실이 어디냐”고 묻는 상황까지는 생각하지 못해서 안타깝게도 술집이 불타고 말았다.&lt;/p&gt;
&lt;p&gt;이 이야기를 통해 우리가 알 수 있는 것은, 게임의 잠재적 위험을 최대한 많이 제거하기 위해서 우리가 최대한 많이 비정상적인 상황을 생각해야 한다는 것이다.
적어도 내가 테스트 케이스에 적힌 상황 말고 다른 상황도 생각해봐야 하지 않을까? 하는 마인드라도 가져야 한다.&lt;/p&gt;
&lt;p&gt;하지만 준비없이 케이스 밖의 상황을 테스트하는 것은 심각한 비효율을 야기한다.
목적 없이 같은자리만 뱅뱅 돌면서 “여기는 문제없음! ㅋ” 이라고 결론을 내리는 실수를 저지를 수 있다. 그래서 TC 밖으로 잘 나가려면 준비를 잘 해야 한다.&lt;/p&gt;
&lt;h2 id=&quot;qa팀--미운-4살&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#qa%ED%8C%80--%EB%AF%B8%EC%9A%B4-4%EC%82%B4&quot; aria-label=&quot;qa팀  미운 4살 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;QA팀 = 미운 4살&lt;/h2&gt;
&lt;p&gt;QA직군이라면 탐색적 테스팅(Exploratory Testing)에 대해서 한번쯤은 들어봤을 것이다.
테스터의 경험과 인지능력(Heuristic)을 최대한 활용하여 테스트를 수행하는 것을 의미한다.
일각에서는 ‘기획서와 TC없이 마구잡이로 테스트하는 것’과 같은 부정적인 인식도 있지만 알아차리기 힘든 영역의 버그를 찾는데 매우 유용하다.&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;
탐색적 테스팅을 효과적으로 수행하기 위해서는 ‘목적’이 있어야 한다.
‘내가 어떤 콘텐츠의 어떤 부분에 대한 이슈를 최대한 찾겠다.’와 같이 먼저 목적을 설정해 두고 테스트를 해야 짧은 시간에 질 높은 테스트 결과가 도출된다.
하지만 프로젝트에 처음 들어온 신입 QA 테스터라면 해당 프로젝트의 스펙도 잘 모르고 업무 프로세스도 아직 숙지하지 못한 상태다.
이런 경우에는 어떻게 하면 될까? 아주 간단한 방법이 떠올랐다. 바로 &lt;code&gt;미운 4살&lt;/code&gt; 방법이다. 엄마아빠말을 지지리도 안 듣는 4살 아이처럼 모든 케이스에 반대되는 행동을 해보면 된다. 예를 들자면 이런 식이다.&lt;/p&gt;
&lt;br/&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;케이스 1&lt;/strong&gt; : 튜토리얼 도중 점프 버튼 터치 가이드 출력 시, 점프 버튼을 터치&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;미운 4살&lt;/strong&gt; : 반대로 슬라이드 버튼을 터치&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;케이스 2&lt;/strong&gt; : 튜토리얼 도중 슬라이드 버튼 터치 가이드 출력 시, 슬라이드 버튼을 터치&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;미운 4살&lt;/strong&gt; : 버튼을 터치하지 않음&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;케이스 3&lt;/strong&gt; : 당근푸딩 트램폴린 보물의 능력발동 쿨타임 완료 후 구멍에 빠지기&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;미운 4살&lt;/strong&gt; : 쿨타임 완료 전 구멍에 빠지기&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;미운 4살&lt;/strong&gt; : 쿨타임 완료 후 구멍에 빠지지 않기&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;케이스 4&lt;/strong&gt; : 다이노사워 쿠키 능력발동 시 상단에서 생성되는 고기 젤리를 획득&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;미운 4살&lt;/strong&gt; : 고기젤리를 일부러 획득하지 않음&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;br/&gt;
&lt;p&gt;이렇게 각 케이스마다 반대되는 행동을 하는 것이, 탐색적 테스팅의 방향성을 설정할 수 있는 가장 쉬운 방법일 것 같다.
물론 모든 케이스에 반대되는 행동이 존재하는 것은 아니지만 TC 바깥으로 벗어나는 첫걸음으로서는 나름 간편한 것 같다.&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;
그럼 미운 짓을 좀 해보았으니 더 미운 짓을 해볼 차례다. 바로 테스트할 때 &lt;code&gt;트롤링(Trolling)&lt;/code&gt;을 해보는 것이다. 트롤링은 게임의 진행을 일부러 방해하거나 의도와 반대되는 행동을 하는 것을 의미한다. 선량한 게이머들에게는 치가 떨리는 행동이지만 버그와 예외상황을 찾는 데에는 유용하다. 다만 이 트롤링이라는 단어가 테스팅과는 좀 어울리지는 않는 개념이라 약간의 거리감이 있지만 쉽게 이해하기 위해서 적절하다고 판단된다. 마찬가지로 몇 가지 예를 들어보자.&lt;/p&gt;
&lt;br/&gt;
&lt;blockquote&gt;
&lt;p&gt;튜토리얼이 너무 길어 짜증나서 게임을 껐다가 켜본다.&lt;/p&gt;
&lt;p&gt;튜토리얼 중 지정된 터치영역을 제외하고 나머지 영역을 전부 눌러본다.&lt;/p&gt;
&lt;p&gt;상품결제 직전에 급하게 취소버튼을 누른다.&lt;/p&gt;
&lt;p&gt;버튼 2개를 동시에 눌러본다.&lt;/p&gt;
&lt;p&gt;로비와 게임 리스트 화면을 빠른 속도로 왔다갔다 해본다.&lt;/p&gt;
&lt;p&gt;핸드폰 용량을 꽉 채운 상태에서 게임을 다운로드 해본다.&lt;/p&gt;
&lt;p&gt;다운로드 도중에 Wifi에서 LTE로 변경해 본다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br/&gt;
&lt;p&gt;게임에서 트롤링의 &lt;strong&gt;악의성&lt;/strong&gt;이 짙지만, 테스팅에서 트롤링은 악의성보다는 &lt;strong&gt;다양성&lt;/strong&gt;에 중점이 맞춰져 있다.
내가 현재 테스트하고 있는 기능에서 최대한 다양한 비정상 상황을 연출해 내는 것이다.&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;
또 다른 방법으로 &lt;code&gt;페르소나(Persona)&lt;/code&gt;를 이용하는 방법이 있다.&lt;/p&gt;
&lt;p&gt;단어가 참 거창하지만 그냥 역할놀이라 생각하면 쉽다.&lt;/p&gt;
&lt;br/&gt;
&lt;blockquote&gt;
&lt;p&gt;야자 시간에 몰래 게임하는 학생이라면?&lt;/p&gt;
&lt;p&gt;일이 너무 바빠 게임을 켜두고 일하는 직장인이라면?&lt;/p&gt;
&lt;p&gt;성격이 너무 급해서 1초 이상 못 버티는 사람이라면?&lt;/p&gt;
&lt;p&gt;아무거나 눌러보는 어린 아이라면?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;br/&gt;
&lt;p&gt;시간, 돈, 신분 등의 요소들은 유저에게 영향을 끼치기 때문에 각 사용자들은 다양한 사용 패턴을 가지고 있다. 따라서 페르소나를 씌워 **&apos;내가 저런 상황에 처했다면 어떻게 행동했을까?&apos;**에 대해 생각해보면 놀랍게도 생각지도 못한 다양한 상황들을 머릿속에 그려내 볼 수 있다. 다만 QA 엔지니어가 모든 상황을 전부 설정할 수 없다는 한계점은 분명히 인지해야 한다.&lt;/p&gt;
&lt;h2 id=&quot;마치며--그렇다고-테스트-케이스를-버리지는-말자&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A7%88%EC%B9%98%EB%A9%B0--%EA%B7%B8%EB%A0%87%EB%8B%A4%EA%B3%A0-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BC%80%EC%9D%B4%EC%8A%A4%EB%A5%BC-%EB%B2%84%EB%A6%AC%EC%A7%80%EB%8A%94-%EB%A7%90%EC%9E%90&quot; aria-label=&quot;마치며  그렇다고 테스트 케이스를 버리지는 말자 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;마치며 : 그렇다고 테스트 케이스를 버리지는 말자&lt;/h2&gt;
&lt;p&gt;지금까지 테스트 케이스 외 영역을 탐색해야 하는 이유에 대해 알아 보았다.&lt;/p&gt;
&lt;p&gt;하지만 테스트 케이스는 여전히 중요한 문서다. 이는 테스트 케이스가 상위 방향성을 가리키는 &lt;strong&gt;나침반&lt;/strong&gt;이자 &lt;strong&gt;기준점&lt;/strong&gt; 역할을 하기 때문이다.&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;기획서는 “우리는 이쪽 방향으로 나아갑시다!” 같은 지침서다. 우리가 올바른 방향으로 나아가고 있는가를 검토하기 위해서는 이 지침서로 만든 테스트 케이스가 필요하다. 따라서 게임이 아닌 일반 소프트웨어의 경우에는 고객 또는 이해관계자의 요구사항인 인수 기준(Acceptance Criteria)이 제품에 제대로 반영되었는지 확인하는 인수 테스트 절차를 밟기도 한다.&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;또한 테스트 케이스 없는 테스트는 테스트가 미궁에 빠질 확률을 높인다. **‘테스트 케이스 바깥’**이라는 표현을 쓰는 것은 기본적으로 테스트 케이스가 있어야 한다는 것 의미한다. 그래서 &lt;strong&gt;기대 결과 값&lt;/strong&gt;을 기준으로 &lt;strong&gt;기대하지 않은 결과&lt;/strong&gt;를 판단할 수 있다. 테스트를 통해 무언가 결과값을 얻었어도 이게 맞는건지 틀린건지 알 수 없다면 테스트 결과의 신뢰도가 매우 하락할 것 이다. 이 밖에 TC는 새로 프로젝트에 합류한 신규 QA 엔지니어에게 세부 스펙을 알려주는 교육의 기능과 정량적인 테스트 결과를 도출할 수 있게 해주는 도구로도 사용된다. 이처럼 순기능도 많기 때문에 테스트 케이스를 멀리 하는 것은 옳지 않다.&lt;/p&gt;
&lt;br/&gt;
&lt;p&gt;테스트 케이스를 사용하는 것과 테스트 케이스를 벗어나는 것은 어떤 방법이 더 옳은가를 판단하는 것이 아닌, 각자가 사용되는 영역과 기능이 다름을 알고 시작해야 한다. 이 두가지를 적절히, 조화롭게 사용하면 우리가 그토록 바라던 **‘안전하고 안심할 수 있는 게임’**을 만들어 유저들에게 최고의 경험을 선사하게 될 것이다.&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[페이지 주소가 바뀌어도 자연스럽게 이어지는 애니메이션 만들기]]></title><description><![CDATA[GatsbyJS와 react-transition-group을 사용해서 간단한 페이지 전환 애니메이션을 구현하는 법을 짚어봅니다.]]></description><link>https://tech.devsisters.com/posts/page-navigation-animation</link><guid isPermaLink="false">https://tech.devsisters.com/posts/page-navigation-animation</guid><pubDate>Wed, 10 Apr 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;웹사이트를 SPA로 만들 때의 장점 중 하나는 자바스크립트의 실행 컨텍스트가 날아가지 않기 때문에
화면이 전환될 때에도 애니메이션 처리를 할 수 있다는 점입니다.&lt;/p&gt;
&lt;figure&gt;
&lt;video autoplay loop muted playsinline&gt;
    &lt;source src=&quot;/3f7dd065fd0f3a21e64530d07d2146c7/animation.mp4&quot; type=&quot;video/mp4&quot;&gt;
&lt;/video&gt;
&lt;figcaption&gt;이런 것을 할 수 있다는 뜻이에요. 변화하는 주소창에 주목!&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&quot;gatsbyjs&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#gatsbyjs&quot; aria-label=&quot;gatsbyjs permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;GatsbyJS&lt;/h2&gt;
&lt;p&gt;링크: &lt;a href=&quot;https://www.gatsbyjs.org/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://www.gatsbyjs.org/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;GatsbyJS는 리액트 정적 웹사이트 생성기입니다.&lt;/p&gt;
&lt;p&gt;정적파일로 떨궈주기 때문에 &lt;a href=&quot;https://aws.amazon.com/s3/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;S3&lt;/a&gt;에 올리기만 하면 웹사이트 배포가 끝난다는 장점이 있고, &lt;br&gt;
여러가지 기능들을 마법같이 붙여주는 플러그인 시스템이 잘 갖춰져 있어서 저희 웹사이트 개발에 많이 사용됐죠.&lt;/p&gt;
&lt;p&gt;그리고, 대부분의 SPA 접근처럼 GatsbyJS도 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/History_API&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;History API&lt;/a&gt;를 활용하여 페이지 히스토리를 처리하기 때문에
자바스크립트 실행 컨텍스트가 유지되어 페이지 전환 애니메이션 처리가 가능합니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://nextjs.org/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;Next.js&lt;/a&gt; 등의 프레임웍을 활용해도 같은 처리가 가능하지만, 페이지 전환 애니메이션은
프레임웍마다 작성해야 하는 코드가 조금씩 다르므로 이 글에서는 GatsbyJS 기준으로 이야기 하겠습니다.&lt;/p&gt;
&lt;h2 id=&quot;페이지-준비&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%A4%80%EB%B9%84&quot; aria-label=&quot;페이지 준비 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;페이지 준비&lt;/h2&gt;
&lt;p&gt;우선 &lt;code&gt;src/pages&lt;/code&gt; 폴더에 페이지 두 개를 만듭니다.&lt;/p&gt;
&lt;p&gt;(&lt;code&gt;page-a.js&lt;/code&gt;, &lt;code&gt;page-b.js&lt;/code&gt; 내용은 적당히 바꿔서 만듭니다.)&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;react&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Link &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;gatsby&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;p&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;A&lt;/span&gt; 페이지 입니다&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;p&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Link to&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;/page-b&apos;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;B&lt;/span&gt; 페이지 가기&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;Link&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;페이지 전환 애니메이션을 구현하려면 &lt;code&gt;&amp;#x3C;a&gt;&lt;/code&gt; 태그 대신 Link 컴포넌트를 사용해야합니다.&lt;/p&gt;
&lt;p&gt;Link 컴포넌트는 내부적으로 &lt;code&gt;&amp;#x3C;a&gt;&lt;/code&gt; 태그를 감싸서,
클릭 시 브라우저 기본동작을 건너뛰고 History API를 사용하는 방식으로 동작합니다.&lt;/p&gt;
&lt;p&gt;애니메이션 처리를 따로 하지 않더라도 페이지 전환 속도 등 이점이 있기 때문에,
웹사이트 내부 페이지 링크는 전부 Link 컴포넌트를 사용하시면 좋습니다.&lt;/p&gt;
&lt;h2 id=&quot;레이아웃-준비&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%A0%88%EC%9D%B4%EC%95%84%EC%9B%83-%EC%A4%80%EB%B9%84&quot; aria-label=&quot;레이아웃 준비 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;레이아웃 준비&lt;/h2&gt;
&lt;p&gt;GatsbyJS v2에서는 레이아웃 구성을 페이지 컴포넌트 안에서 Layout 엘리먼트로 감싸는 방식을 권장하지만,
레이아웃을 이렇게 구성하면 페이지가 전환될때 페이지 인스턴스와 함께 레이아웃 인스턴스가 언마운트돼서
애니메이션을 유지할 수가 없습니다.&lt;/p&gt;
&lt;p&gt;페이지 전환 애니메이션 처리를 위한 레이아웃 컴포넌트의 인스턴스는 페이지 전환간에 언마운트되지 않는 단계에 있어야 하는데,
GatsbyJS에서는 이 단계에 개입할 수 있는 &lt;a href=&quot;https://www.gatsbyjs.org/docs/browser-apis/#wrapPageElement&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;&lt;code&gt;wrapPageElement&lt;/code&gt;&lt;/a&gt; api를 제공합니다.&lt;/p&gt;
&lt;p&gt;이 API를 직접 사용할 수도 있지만 같은 코드를 &lt;code&gt;gatsby-browser.js&lt;/code&gt;에서 한 번, &lt;code&gt;gatsby-ssr.js&lt;/code&gt;에서 한 번씩 적어야 해서 약간 번거롭습니다.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.gatsbyjs.org/packages/gatsby-plugin-layout/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;gatsby-plugin-layout&lt;/a&gt; 플러그인을 사용하면 그 처리를 조금 더 간편하게 할 수 있습니다.&lt;/p&gt;
&lt;p&gt;다음과 같이 플러그인을 설정하고 Layout 컴포넌트를 작성해줍니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;gatsby-config.js&lt;/code&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;exports &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;plugins&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;gatsby-plugin-layout&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;component&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; require&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;resolve&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;./src/components/Layout.js&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;src/components/Layout.js&lt;/code&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;react&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// `page-a.js`, `page-b.js`에서 반환한 내용이&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// &amp;lt;Layout&gt; 컴포넌트에 `children` prop으로 들어옵니다.&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// 이 레이아웃 컴포넌트는 페이지가 전환돼도 마운트된 상태가 유지됩니다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; children &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;div className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;page&apos;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;div&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;애니메이션을-넣어봅시다&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%95%A0%EB%8B%88%EB%A9%94%EC%9D%B4%EC%85%98%EC%9D%84-%EB%84%A3%EC%96%B4%EB%B4%85%EC%8B%9C%EB%8B%A4&quot; aria-label=&quot;애니메이션을 넣어봅시다 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;애니메이션을 넣어봅시다&lt;/h2&gt;
&lt;p&gt;애니메이션은 Layout 컴포넌트에서 구현합니다.&lt;/p&gt;
&lt;p&gt;페이지 전환 애니메이션을 구현하는 것은 기본적으로 항목 개수가 변화하는 목록의 애니메이션을 구현하는 것과 같습니다.&lt;/p&gt;
&lt;p&gt;리액트 커뮤니티에서는 이러한 전환 애니메이션을 구현하기 위해 보통 &lt;code&gt;react-transition-group&lt;/code&gt; 라이브러리를 사용합니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;react-transition-group&lt;/code&gt; 라이브러리를 사용해서 전환 애니메이션을 구현하려면
여기서 제공하는 TransitionGroup 컴포넌트의 엘리먼트 자식으로 Transition 엘리먼트들을 넣으면 됩니다.&lt;/p&gt;
&lt;p&gt;TransitionGroup 컴포넌트는 엘리먼트 자식으로 넘겨준 Transition 엘리먼트가 갑자기 자식 목록에서 빠지더라도
애니메이션이 종료되기 전까지 Transition 인스턴스의 마운트 상태를 유지시켜줍니다.&lt;/p&gt;
&lt;p&gt;Transition 엘리먼트는 전환 상태를 인자로 받는 렌더 함수를 자식으로 받습니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;src/components/Layout.js&lt;/code&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;js&quot;&gt;&lt;pre class=&quot;language-js&quot;&gt;&lt;code class=&quot;language-js&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; React &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;react&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; Transition&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; TransitionGroup &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;react-transition-group&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;./Layout.css&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;// `location` prop으로는 현재 브라우저의 주소 상태를 알 수 있는 객체가 들어옵니다.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;// gatsby-plugin-layout 플러그인이 주입해준 값입니다.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;export&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;default&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; children&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; location &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;TransitionGroup
      &lt;span class=&quot;token comment&quot;&gt;// `component` prop에 `null`을 넘겨주지 않으면 `&amp;lt;div&gt;`로 한단계 감싸집니다.&lt;/span&gt;
      component&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Transition
        &lt;span class=&quot;token comment&quot;&gt;// 현재 pathname을 key prop으로 넘겨주어&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// 페이지가 전환될 때 이전 페이지가 TransitionGroup에서 빠지고&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// 새 페이지가 들어온 것으로 간주되도록 합니다.&lt;/span&gt;
        key&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;location&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pathname&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// timeout prop으로 넘겨준 시간(ms 단위)에 따라 `status`가 변화합니다.&lt;/span&gt;
        timeout&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;enter&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;300&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;exit&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;300&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
          &lt;span class=&quot;token comment&quot;&gt;// 전환 상태(`status` 인자)는&lt;/span&gt;
          &lt;span class=&quot;token comment&quot;&gt;// `unmounted`, `exited`, `entering`, `entered`, `exiting`&lt;/span&gt;
          &lt;span class=&quot;token comment&quot;&gt;// 이렇게 들어오는데, Transition 엘리먼트가 아예 자식 목록에서 빠져버리면&lt;/span&gt;
          &lt;span class=&quot;token comment&quot;&gt;// `unmounted` 상태가 들어올 일이 없습니다.&lt;/span&gt;
          &lt;span class=&quot;token comment&quot;&gt;// 따라서 뒤쪽의 네가지 상태만 들어온다고 생각하면 됩니다.&lt;/span&gt;
          &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;div className&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;page &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;status&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;children&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;div&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;Transition&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
    &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;TransitionGroup&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;src/components/Layout.css&lt;/code&gt;&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;css&quot;&gt;&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.page&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;top&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;transition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; opacity 0.3s&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.page.entering,
.page.entered&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; relative&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;opacity&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.page.exiting,
.page.exited&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;position&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; absolute&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;opacity&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;짜잔&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A7%9C%EC%9E%94&quot; aria-label=&quot;짜잔 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;짜잔~&lt;/h2&gt;
&lt;p&gt;완성된 코드는 CodeSandbox에 올려두었습니다: &lt;a href=&quot;https://codesandbox.io/s/n08qvvxpj0&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://codesandbox.io/s/n08qvvxpj0&lt;/a&gt;&lt;/p&gt;
&lt;iframe
  src=&quot;https://codesandbox.io/embed/n08qvvxpj0&quot;
  title=&quot;gatsby-page-navigation-animation-example&quot;
  style=&quot;width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;&quot;
  sandbox=&quot;allow-modals allow-forms allow-popups allow-scripts allow-same-origin&quot;&gt;&lt;/iframe&gt;
&lt;we-are-hiring /&gt;</content:encoded></item><item><title><![CDATA[쪼그라드는 웹페이지]]></title><description><![CDATA[모바일 반응형 웹페이지를 쉽고 빠르게 구현하기 위해
저희가 고안한 방법을 소개합니다.]]></description><link>https://tech.devsisters.com/posts/shrinking-webpage</link><guid isPermaLink="false">https://tech.devsisters.com/posts/shrinking-webpage</guid><pubDate>Tue, 26 Feb 2019 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;고객과 직접 마주하는 웹페이지는 다양한 크기의 장치에서 잘 보여야 합니다.&lt;br&gt;
특히나 모바일 게임 회사의 웹페이지는 &lt;strong&gt;다양한 크기의 휴대폰&lt;/strong&gt;에서 잘 보여야 합니다.&lt;/p&gt;
&lt;h2 id=&quot;괴로운-지점&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B4%B4%EB%A1%9C%EC%9A%B4-%EC%A7%80%EC%A0%90&quot; aria-label=&quot;괴로운 지점 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;괴로운 지점&lt;/h2&gt;
&lt;p&gt;보통 대응해야되는 화면마다 미디어쿼리를 작성해서 반응형 처리를 합니다.
하지만 그렇게 구현하면 대응하지 않은 중간 크기에서 예상치 못한 디자인 버그가 터지곤 합니다.&lt;/p&gt;
&lt;p&gt;가장 많이 겪게되는 디자인 버그는 텍스트가 의도한 공간을 넘어가서 줄이 넘치는 문제죠.
텍스트 내용을 변경해보기도 하고, 미디어쿼리를 추가해서 문제가 생기는 화면 크기에만 스타일을 추가해보고..&lt;/p&gt;
&lt;p&gt;웹페이지에 보여줘야 될 내용도 많고 자주 바뀌는데.. 지속적으로 다양한 화면 크기에서 이런 문제를 잡아야 합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 640px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 75%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAPABQDASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAAAAAAAAAAAAQBAgMF/8QAFAEBAAAAAAAAAAAAAAAAAAAAAv/aAAwDAQACEAMQAAABnVJsKxzwP//EABoQAQADAAMAAAAAAAAAAAAAAAEAAgMREiH/2gAIAQEAAQUCDieh2ml0uaDnaxV//8QAFhEBAQEAAAAAAAAAAAAAAAAAAQAR/9oACAEDAQE/AcRtL//EABcRAAMBAAAAAAAAAAAAAAAAAAABEiH/2gAIAQIBAT8Bxok//8QAGxAAAgEFAAAAAAAAAAAAAAAAAAEQERIhIlH/2gAIAQEABj8C65wXI2VWf//EABkQAAIDAQAAAAAAAAAAAAAAAAABESFhUf/aAAgBAQABPyFUm9ei1YQEw3YqofSOsZbP/9oADAMBAAIAAwAAABBnL//EABcRAQEBAQAAAAAAAAAAAAAAAAEAESH/2gAIAQMBAT8QeRIOX//EABcRAQEBAQAAAAAAAAAAAAAAAAEAESH/2gAIAQIBAT8QHqtt/8QAHhABAAIBBAMAAAAAAAAAAAAAAQARMSFBUXFhkaH/2gAIAQEAAT8QMSrpbJ46+TGB3VXTG/IeHaOyChNdyOgTqZDhcaB4Yaq9vU//2Q==&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/293705cae81440c2fc9c4d72808c4ca1/33ecc/hell-o.webp 360w,
/static/293705cae81440c2fc9c4d72808c4ca1/263a4/hell-o.webp 480w,
/static/293705cae81440c2fc9c4d72808c4ca1/0c8fb/hell-o.webp 640w&quot;
              sizes=&quot;(max-width: 640px) 100vw, 640px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/293705cae81440c2fc9c4d72808c4ca1/543cd/hell-o.jpg 360w,
/static/293705cae81440c2fc9c4d72808c4ca1/a3e66/hell-o.jpg 480w,
/static/293705cae81440c2fc9c4d72808c4ca1/56d4e/hell-o.jpg 640w&quot;
            sizes=&quot;(max-width: 640px) 100vw, 640px&quot;
            type=&quot;image/jpeg&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/293705cae81440c2fc9c4d72808c4ca1/56d4e/hell-o.jpg&quot;
            alt=&quot;안녕하세...요&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;안녕하세...요&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;이러자니 QA해야할 것도 많아지고, 어떤 방식으로 고쳐야 할지 디자이너와 일일이 상의해야 하니
커뮤니케이션 비용이 상당히 들어갑니다.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;PC 화면이야 대부분의 모니터들이 어떤 적당한 가로폭보다 더 넓을 것이라고 가정할 수 있습니다.
대충 &lt;code&gt;1000px&lt;/code&gt; 전후로 가로폭을 잡고 웹페이지를 만든 다음,
웹페이지 내용을 가운데 정렬하여 더 큰 화면에서는 좌우 여백만 늘려주면 됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 2400px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 51.66666666666666%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAABr0lEQVR42oXSzU4TURjGcS5CZeNdeBNegIkblhoW2EWVRUnYFpIiSVukM8YYSVtoZzofTDu1MSoL3BiIiRoFMcqikCAfIUhSUpT0z3tmGmrNiItnMTPn/OY578wAziK9eGAaUC7h7VWYxULvGGgY5Dplcmcl0izg7hr07+tloO+G20U9n9LmHIlmiscdE02hEoVnBa/Ky/4P2m6Qju2A/5zs2ylu2rfRsaVhRWIKaApo4u1b9O2z3Aiw6odRC6Slt1Nkpq3xpKWhn80LaEWDtTrUG3+ACnDlmN++w8oqGHIct4q7U2Lq11MyrWdkT/LMni4IWCEjbQPQ7oKfPsPaekTD10vw8pXUdwLQ2ioyeZAlKXlwnGFUkjrSmP6p4/0ohyVUmcYLWH4TXveBCurOEa9G7YtO4d0oY2sxrrXvMMg9xr7GKazep7ahB2vC+Tlh/vmVA9AXUCP/PsH4eoyrrVtc+T3E+EY8uKeeqTXhvC/7bZwe6Msm1Wbi4wjXD28ESX4YobASFzAXAarTORFg0FQ+SjNPZjdNaj/N3VaS4fYkDw9neCQzrW4XL2b2d8Nzww+IFjDrvbUAAAAASUVORK5CYII=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/96cfbd25133519449e8690740e3d05c9/33ecc/example.webp 360w,
/static/96cfbd25133519449e8690740e3d05c9/263a4/example.webp 480w,
/static/96cfbd25133519449e8690740e3d05c9/0c8fb/example.webp 640w,
/static/96cfbd25133519449e8690740e3d05c9/521f7/example.webp 860w,
/static/96cfbd25133519449e8690740e3d05c9/cbd37/example.webp 1200w,
/static/96cfbd25133519449e8690740e3d05c9/548e7/example.webp 2400w,
/static/96cfbd25133519449e8690740e3d05c9/2d0e9/example.webp 5120w&quot;
              sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/96cfbd25133519449e8690740e3d05c9/53918/example.png 360w,
/static/96cfbd25133519449e8690740e3d05c9/9aebd/example.png 480w,
/static/96cfbd25133519449e8690740e3d05c9/0f09e/example.png 640w,
/static/96cfbd25133519449e8690740e3d05c9/3e3fe/example.png 860w,
/static/96cfbd25133519449e8690740e3d05c9/64756/example.png 1200w,
/static/96cfbd25133519449e8690740e3d05c9/98e2c/example.png 2400w,
/static/96cfbd25133519449e8690740e3d05c9/66e9f/example.png 5120w&quot;
            sizes=&quot;(max-width: 2400px) 100vw, 2400px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/96cfbd25133519449e8690740e3d05c9/98e2c/example.png&quot;
            alt=&quot;좌우 여백만 늘려준 예시&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;좌우 여백만 늘려준 예시&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;h2 id=&quot;아이디어&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%95%84%EC%9D%B4%EB%94%94%EC%96%B4&quot; aria-label=&quot;아이디어 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;아이디어&lt;/h2&gt;
&lt;p&gt;그래서 다음과 같이 작업하기로 합의를 보았습니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PC 최소 사이즈보다 작은 화면에서는 모바일(태블릿) 화면을 보여준다&lt;/li&gt;
&lt;li&gt;태블릿 정도 크기에서는 모바일 화면을 보여주고 좌우에 여백을 준다 (PC 화면처럼)&lt;/li&gt;
&lt;li&gt;모바일 화면은 하나만 작업해서 기준 사이즈보다 작은 기기에서는 화면을 쪼그라뜨린다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;일반적인 방식은 아니라서 구체적으로 어떻게 작업하겠다는 건지 글로는 설명하기가 어렵더라고요.
저희끼리 내부적으로 커뮤니케이션 하는데도 시간이 좀 걸렸습니다.&lt;/p&gt;
&lt;p&gt;이제는 만들어놓은게 있으니 직접 보여드리면 되겠죠!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/fa22502e391a68e3a33c5e135e23bd90/shrink.gif&quot; alt=&quot;화면이 줄어들었다 늘어나는 영상&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;저렇게-만들어-보니-어떤가&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EC%A0%80%EB%A0%87%EA%B2%8C-%EB%A7%8C%EB%93%A4%EC%96%B4-%EB%B3%B4%EB%8B%88-%EC%96%B4%EB%96%A4%EA%B0%80&quot; aria-label=&quot;저렇게 만들어 보니 어떤가 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;저렇게 만들어 보니 어떤가&lt;/h2&gt;
&lt;p&gt;지난 1년간 여러 게임사이트들, 회사 사이트, 채용 사이트, 이 블로그까지 대부분의
대외적으로 작업한 웹페이지에서 이 기법을 사용했는데요,
전체 화면을 선형적으로 쪼그라뜨리니 봐야하는 화면 크기의 종류가 확 줄어들었습니다.&lt;/p&gt;
&lt;p&gt;브라우저 종류마다 PC 화면, 모바일 화면에서 한번씩 테스트를 거치고 나면,
나중에 특정 모바일 기기의 화면 크기 때문에 들어오는 이슈는 전혀 없었습니다.&lt;/p&gt;
&lt;p&gt;다만 스타일 코드를 작성하는 방식에 변경이 좀 있었는데요,
약간의 제약을 감수하면 생산성의 차이는 크게 없었습니다.
아래 기술적인 부분 설명에서 자세히 다루겠습니다.&lt;/p&gt;
&lt;h2 id=&quot;기술적인-부분&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B8%B0%EC%88%A0%EC%A0%81%EC%9D%B8-%EB%B6%80%EB%B6%84&quot; aria-label=&quot;기술적인 부분 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;기술적인 부분&lt;/h2&gt;
&lt;p&gt;PC 화면에서 태블릿 화면으로 전환하는 부분은 미디어쿼리를 써도 되고 그냥 자바스크립트로 해도 되죠..
일반적인 반응형 처리는 설명을 생략하고,
쪼그라드는 부분을 작업하기 위해 무슨 고민들을 했고 어떤 방법을 선택했는지 설명하려고 합니다.&lt;/p&gt;
&lt;p&gt;예시에 react와 sass(scss) 코드가 사용됩니다.
이 글에서는 이들의 기능에 대해 따로 설명하지 않습니다.&lt;/p&gt;
&lt;h3 id=&quot;1-zoom-사용하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#1-zoom-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0&quot; aria-label=&quot;1 zoom 사용하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;1. &lt;code&gt;zoom&lt;/code&gt; 사용하기&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;MDN &lt;code&gt;zoom&lt;/code&gt; 문서: &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/zoom&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://mdn.io/zoom&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;가장 처음에 생각한 방법입니다. 하지만 저희 웹페이지 반응형 처리에는 &lt;code&gt;zoom&lt;/code&gt; 속성을 사용하지 못했습니다.
&lt;strong&gt;비표준 속성이라 파이어폭스가 지원하지 않거든요.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;파이어폭스를 지원하지 않아도 된다면 그냥 이 방법을 사용하면 될 것 같습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;typescript&quot;&gt;&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; mobileWidth &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;640&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 쪼그라들기 시작할 시점&lt;/span&gt;
React&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;useEffect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;onResize&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; zoom &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;min&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;innerWidth &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; mobileWidth&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    document&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;documentElement&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;style&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;zoom &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;zoom&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;onResize&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;resize&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; onResize&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; window&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;removeEventListener&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;resize&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; onResize&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;h3 id=&quot;documentelement에-zoom-속성-사용시-주의&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#documentelement%EC%97%90-zoom-%EC%86%8D%EC%84%B1-%EC%82%AC%EC%9A%A9%EC%8B%9C-%EC%A3%BC%EC%9D%98&quot; aria-label=&quot;documentelement에 zoom 속성 사용시 주의 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;&lt;code&gt;documentElement&lt;/code&gt;에 &lt;code&gt;zoom&lt;/code&gt; 속성 사용시 주의&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;zoom&lt;/code&gt; 속성을 사용하면 &lt;code&gt;window&lt;/code&gt; 객체의 좌표계와 &lt;code&gt;documentElement&lt;/code&gt; 이하 좌표계의 스케일이 달라져서
문서 전체의 가로크기를 생각하고 &lt;code&gt;window.innerWidth&lt;/code&gt;의 값을 사용하면 예상과 다르게 작동할 수 있습니다.&lt;/p&gt;
&lt;p&gt;예를 들어 &lt;code&gt;document.documentElement.style.zoom = &apos;0.5&apos;;&lt;/code&gt;가 적용되어있다면
문서 안쪽 좌표계는 절반 크기로 축소되어있는 것이기 때문에,
문서 전체의 가로크기를 의미하려면 &lt;code&gt;window.innerWidth * 2&lt;/code&gt;를 사용해야 합니다.&lt;/p&gt;
&lt;p&gt;js로 화면 전체 크기를 계산하여 사용하는 코드 등에 문제가 생길 수 있습니다.
캐러셀이나 모달 등 전체화면으로 보여줄 일이 많은 UI에서 조심하면 됩니다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;2-transform-scale-사용하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#2-transform-scale-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0&quot; aria-label=&quot;2 transform scale 사용하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;2. &lt;code&gt;transform: scale(...)&lt;/code&gt; 사용하기&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;MDN &lt;code&gt;transform&lt;/code&gt; 문서: &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/transform&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://mdn.io/transform&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;애니메이션이나 가운데 정렬 등의 처리에 엄청 자주 사용되는 &lt;code&gt;transform&lt;/code&gt; 속성을
이런 용도로도 활용할 수 있지 않을까 고민해봤는데요.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;transform&lt;/code&gt; 속성은 적용된 element의 bounding rect에 아무런 영향을 끼치지 않는 문제가 있습니다.&lt;/strong&gt;
문서 전체에 적용하면 문서 크기만 쪼그라들어 보이고,
브라우저 창의 스크롤 영역은 문서가 줄어들기 전 상태로 보이는 것입니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1076px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 84.16666666666667%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAARCAYAAADdRIy+AAAACXBIWXMAAAsTAAALEwEAmpwYAAACu0lEQVR42pWUS09TURDH+Vp+Dndu/BAuXRATE01MjHGhSzVxoTwK5Q2l5VWgQIEIpNIWpEClLe1tqX3ftufnzL3WloILb3NyT8+c85s5M/+5A2Z/H3N6iolEMHt77vzyEhONumupFObkBDM7i1laAp2rbW0Nc3GBicUwBwcQDJLzeBhgfR2SSTg7g6MjUKOO42P3HY+7Y3ERtrYgkYDDQ9jddSB8+4bZ2YFwGMsB6uL2trtZIlQDumFDHIVCzgHHPj7uziUqB9ixbWxglCEOHKBZWYHhYZiagvl5N5KweF9dBb/fhemB6WnXpqBAAObmYHMT5LzRvWKzxOmAUc+TkzAxAeLBgZ7HaCcll3kLbopQLNIsFLAtCzubpSHzluTZiXpsDOP1OuctGQOdPw5UN0hUpmVj5Nf7GOOu/H0LtBNIh3Et8C5QDbKAVLJWLPNy8DmJkx8urN1Wkjt0rk8+Dz6fkxpVB6EdMuLgNtAjwOVlfuUKvHg62AUa0xtqFyh7UdmFtagTAvTeBmo+8EvC6xVMOQ2WSCQT1xDvAvXKMzOw4MPMz0ruPWT6r+wCJYeNOo1KiUa1QkPmdRl2074L1DOSdzOujGkyjmz6gSIJu1bnp1Tz6vqa9FWG9GUKK5WjXKlSLt3QaDWl+jfdVOlbZHc/UIpiRCp10V47sMrWuZ8RO4BHRvx4hdaCOFRBaw61ynLe/FFJ1tFhP1ATnc3JNeT/6CRf99/xqvmBt4xwsDsCQ15xJD2dy7nN0AO8rcN+oCSbqQUCkU88Kb3mWeU90W3pqJlFjPb/fwFzFmZ4FL545NvxhgfphzxKPOYs8BE+D9H2SQtK1/Rf+X6gXqdcwnyXL080znk+QqAaJFgLSWGiEIk5n7f7cvgPYIBWrUohm6Qo4Eq5RrPSpN1o02i2qNo2joC0yp3W6wH+Bmbdt2UQPohiAAAAAElFTkSuQmCC&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/860537c656e67cbd7c58b6e5329d06d1/33ecc/scale.webp 360w,
/static/860537c656e67cbd7c58b6e5329d06d1/263a4/scale.webp 480w,
/static/860537c656e67cbd7c58b6e5329d06d1/0c8fb/scale.webp 640w,
/static/860537c656e67cbd7c58b6e5329d06d1/521f7/scale.webp 860w,
/static/860537c656e67cbd7c58b6e5329d06d1/d563d/scale.webp 1076w&quot;
              sizes=&quot;(max-width: 1076px) 100vw, 1076px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/860537c656e67cbd7c58b6e5329d06d1/53918/scale.png 360w,
/static/860537c656e67cbd7c58b6e5329d06d1/9aebd/scale.png 480w,
/static/860537c656e67cbd7c58b6e5329d06d1/0f09e/scale.png 640w,
/static/860537c656e67cbd7c58b6e5329d06d1/3e3fe/scale.png 860w,
/static/860537c656e67cbd7c58b6e5329d06d1/1e0c2/scale.png 1076w&quot;
            sizes=&quot;(max-width: 1076px) 100vw, 1076px&quot;
            type=&quot;image/png&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/860537c656e67cbd7c58b6e5329d06d1/1e0c2/scale.png&quot;
            alt=&quot;transform scale 적용한 모양&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;transform scale 적용한 모양&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;p&gt;애니메이션에 사용하기엔 유용한 성질이지만 우리와 같은 요구사항에 적용하기는 어렵습니다.&lt;/p&gt;
&lt;p&gt;js를 사용해서 컨테이너 크기를 조절하는 방법이 있겠지만
깔끔하게 해결될 거란 기대가 들지 않아서 이 방법은 빠르게 선택지에서 제거하였습니다.&lt;/p&gt;
&lt;h3 id=&quot;3-rem-사용하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#3-rem-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0&quot; aria-label=&quot;3 rem 사용하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;3. &lt;code&gt;rem&lt;/code&gt; 사용하기&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;MDN &lt;code&gt;rem&lt;/code&gt; 문서: &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/length#rem&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://mdn.io/rem&lt;/a&gt;&lt;br&gt;
MDN &lt;code&gt;:root&lt;/code&gt; 문서: &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/:root&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://mdn.io/root&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;rem&lt;/code&gt; 단위는 문서의 최상위 엘리먼트의 &lt;code&gt;font-size&lt;/code&gt;를 나타냅니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scss&quot;&gt;&lt;pre class=&quot;language-scss&quot;&gt;&lt;code class=&quot;language-scss&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;:root &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 30px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.my-component &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 30px * 2 = 60px&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 2rem&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;저희는 웹페이지를 작업할 때 상대적인 길이(&lt;code&gt;%&lt;/code&gt;, &lt;code&gt;em&lt;/code&gt; 등)를 제외하면
대부분의 길이에 &lt;code&gt;px&lt;/code&gt; 단위(웹에서 &lt;code&gt;px&lt;/code&gt; 단위는 실제 픽셀이 아닌, 1/96 인치를 의미합니다.)를 사용하였는데요,
이를 전부 &lt;code&gt;rem&lt;/code&gt; 단위로 교체한 다음,
최상위 엘리먼트의 폰트 사이즈를 조정해서 웹페이지 전체를 스케일 시켜보기로 했습니다.&lt;/p&gt;
&lt;p&gt;역시 코드를 보여드리는게 빠르겠죠.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scss&quot;&gt;&lt;pre class=&quot;language-scss&quot;&gt;&lt;code class=&quot;language-scss&quot;&gt;&lt;span class=&quot;token property&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$mobile-contents-width&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 640px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 쪼그라들기 시작할 시점&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;:root &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 640px부터 그 아래로는&lt;/span&gt;
  &lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@media&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;max-width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$mobile-contents-width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 화면 전체 가로크기(100vw)에서 640을 나눠줍니다.&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 100vw &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$mobile-contents-width&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; 1px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 이러면 화면 전체 가로크기가 640px일 때 font-size는 1px이 되고,&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// 화면 전체 가로크기가 320px일 때는 font-size가 0.5px이 됩니다.&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.my-component &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// px 단위 대신에 rem 단위를 사용하였습니다.&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 123rem&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 이러면 쪼그라들기 전에는 123px을 뜻하고,&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 쪼그라드는 동안에는 최상위 엘리먼트의 font-size가 곱해진 값이 적용됩니다.&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 예를들어 화면 전체 가로크기가 320px일 때,&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 123 * 0.5px 해서 61.5px이 되는 것이죠.&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이걸로 해결됐으면 아주 행복했을 텐데.. &lt;strong&gt;이번에는 웹킷 계열 브라우저가 말썽이었습니다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;사파리와 크롬 등 웹킷계열 브라우저들에는 CJK 등의 로케일에 적용되는 최소 폰트 사이즈라는 개념이 있습니다.&lt;br&gt;
단순하게 생긴 글자 모양을 사용하는 서양에서는 폰트사이즈가 작아도 문서 내용을 읽는데 무리가 없는 반면,
복잡한(특히 한자) 글자 모양을 사용하는 동양에서는 폰트 사이즈가 너무 작으면 글자를 읽을 수가 없다는 것이죠.&lt;/p&gt;
&lt;p&gt;브라우저 설정에서 사용자마다 이 최소 폰트 사이즈를 어느정도 조절이 가능하긴 합니다만 어쨌든 기본값은 &lt;code&gt;12px&lt;/code&gt;입니다.
&lt;code&gt;font-size: 6px&lt;/code&gt;라고 작성할 경우 &lt;code&gt;font-size: 12px&lt;/code&gt;로 해석됩니다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;rem&lt;/code&gt; 단위를 사용할 때에 이 최소 폰트 사이즈 제약에 걸리는 문제가 있었는데요.&lt;br&gt;
&lt;code&gt;font-size&lt;/code&gt;에서 &lt;code&gt;rem&lt;/code&gt; 단위를 사용할 때에는 &lt;strong&gt;rem이 적용된 뒤의 폰트사이즈에서&lt;/strong&gt; 최소 폰트 사이즈가 적용되지만,&lt;br&gt;
그 외의 css 속성에서 &lt;code&gt;rem&lt;/code&gt; 단위를 사용할 때에는 &lt;strong&gt;최상위 엘리먼트의 폰트 사이즈에 먼저&lt;/strong&gt; 최소 폰트 사이즈를 적용한 다음
&lt;code&gt;rem&lt;/code&gt; 계산이 이루어집니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scss&quot;&gt;&lt;pre class=&quot;language-scss&quot;&gt;&lt;code class=&quot;language-scss&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// $minimum-font-size: 12px;&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;:root &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;.my-component &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// font-size: 1px * 30 = 30px 👍&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 30rem&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// width: max(1px, 12px) * 30 = 360px 🤔❓❓❓&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 30rem&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이 문제를 피하자면 최상위 엘리먼트의 폰트 사이즈를 한 &lt;code&gt;1000px&lt;/code&gt; 정도 잡아놓고
실제로 사용할 때는 &lt;code&gt;0.03rem&lt;/code&gt;(&lt;code&gt;1000px * 0.03 = 30px&lt;/code&gt;) 같이 작성하는 방법이 있겠지만 너무 비직관적이라 이렇게 쓰고싶지는 않았습니다.&lt;/p&gt;
&lt;p&gt;으... 괜찮은 방법인 것 같았는데... 현 시점에서 크롬은 세상을 정복하고 있는 브라우저고,
iOS에서는 모든 브라우저가 사파리나 다름없으므로 사파리도 지원을 안할 수 없습니다.
다음 방법으로 넘어갑시다.&lt;/p&gt;
&lt;h3 id=&quot;4-vw-사용하기&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#4-vw-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0&quot; aria-label=&quot;4 vw 사용하기 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;4. &lt;code&gt;vw&lt;/code&gt; 사용하기&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;MDN &lt;code&gt;vw&lt;/code&gt; 문서: &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/CSS/length#vw&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;https://mdn.io/vw&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;화면 크기가 줄어들 때 내용이 줄어들게 하고싶은 것이므로,
화면 가로크기를 나타내는 단위인 &lt;code&gt;vw&lt;/code&gt;를 바로 사용해보기로 하였습니다.&lt;/p&gt;
&lt;p&gt;앞서 &lt;code&gt;rem&lt;/code&gt;을 사용한 접근에서 최상위 엘리먼트의 폰트사이즈에 이미 &lt;code&gt;vw&lt;/code&gt; 단위를 사용했었죠.
각각 엘리먼트의 속성에다가 바로 &lt;code&gt;vw&lt;/code&gt; 단위를 사용하면 최소 폰트 사이즈 문제를 피할 수 있을 것입니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scss&quot;&gt;&lt;pre class=&quot;language-scss&quot;&gt;&lt;code class=&quot;language-scss&quot;&gt;&lt;span class=&quot;token property&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$mobile-contents-width&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 640px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 쪼그라들기 시작할 시점&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.my-component &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 화면 크기가 640px보다 큰 경우엔 30px 사용&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 30px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 640px 이전 화면크기 까지는 vw 단위 사용&lt;/span&gt;
  &lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@media&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;max-width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$mobile-contents-width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 30px &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$mobile-contents-width&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; 100vw&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;잘 작동합니다만, 모든 속성을 일일이 저렇게 쓰면 코드가 너무 번잡해지겠죠.&lt;/p&gt;
&lt;p&gt;sass mixin으로 묶어보았습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scss&quot;&gt;&lt;pre class=&quot;language-scss&quot;&gt;&lt;code class=&quot;language-scss&quot;&gt;&lt;span class=&quot;token property&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$mobile-contents-width&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 640px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 쪼그라들기 시작할 시점&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;@mixin&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;scale&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$property&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$px&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 화면 크기가 640px보다 큰 경우엔 30px 사용&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;#{$property}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$px&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 640px 이전 화면크기 까지는 vw 단위 사용&lt;/span&gt;
  &lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@media&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;max-width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$mobile-contents-width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;#{$property}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$px&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$mobile-contents-width&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; 100vw&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.my-component &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;@include&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;scale&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;width&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 30px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;이제 &lt;code&gt;속성: 값&lt;/code&gt; 방식으로 작성하던 css 코드를 &lt;code&gt;@include scale(속성, 값)&lt;/code&gt;으로 전부 바꿔 적으면
&lt;strong&gt;쪼그라드는 웹페이지가 됩니다!&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id=&quot;그런데-속성-값-값-값-값-방식이-나타난다면&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EA%B7%B8%EB%9F%B0%EB%8D%B0-%EC%86%8D%EC%84%B1-%EA%B0%92-%EA%B0%92-%EA%B0%92-%EA%B0%92-%EB%B0%A9%EC%8B%9D%EC%9D%B4-%EB%82%98%ED%83%80%EB%82%9C%EB%8B%A4%EB%A9%B4&quot; aria-label=&quot;그런데 속성 값 값 값 값 방식이 나타난다면 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;그런데 &lt;code&gt;속성: 값 값 값 값&lt;/code&gt; 방식이 나타난다면?&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;margin: 1px 2px 3px 4px&lt;/code&gt; 이렇게 값이 한번에 여러개 들어있는 경우는 위 mixin으로 처리가 안되죠.&lt;br&gt;
&lt;code&gt;border: 1px solid black&lt;/code&gt; 같이 숫자가 아닌게 섞여있는 경우도 있습니다.&lt;/p&gt;
&lt;p&gt;sass의 기능을 좀 더 활용해서 그런 경우를 처리해보았습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scss&quot;&gt;&lt;pre class=&quot;language-scss&quot;&gt;&lt;code class=&quot;language-scss&quot;&gt;&lt;span class=&quot;token property&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$mobile-contents-width&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 640px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 쪼그라들기 시작할 시점&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;@mixin&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;scale&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$property&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$px&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$w&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1px &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$mobile-contents-width&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; 100&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token property&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$pxs&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token null keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$vws&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token null keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 여러 값에 대한 처리로 일반화&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;@each&lt;/span&gt; &lt;span class=&quot;token selector&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$v&lt;/span&gt; in &lt;span class=&quot;token variable&quot;&gt;$px&lt;/span&gt; &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// border: 1px solid black 같은 코드에서 숫자(1px)만 처리&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;@if&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;type-of&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$v&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token selector&quot;&gt;== &apos;number&apos; &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$px&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$v&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; 1px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$pxs&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$pxs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$px&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; 0&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 0&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$px&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$vw&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$px&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$w&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$vws&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$vws&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;if&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$vw&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; 0&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 0&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$vw&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; vw&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;@else&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$pxs&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$pxs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$v&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token property&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$vws&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$vws&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$v&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;#{$property}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$pxs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@media&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;max-width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$mobile-contents-width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;#{$property}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$vws&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token selector&quot;&gt;.my-component &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;@include&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;scale&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;width&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 30px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 전부&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;@include&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;scale&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;margin&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 1px 2px 3px 4px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 잘&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;@include&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;scale&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;border&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 1px solid black&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 처리됩니다&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&quot;빌드된-css-파일에-미디어-쿼리가-너무-많이-보여요&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#%EB%B9%8C%EB%93%9C%EB%90%9C-css-%ED%8C%8C%EC%9D%BC%EC%97%90-%EB%AF%B8%EB%94%94%EC%96%B4-%EC%BF%BC%EB%A6%AC%EA%B0%80-%EB%84%88%EB%AC%B4-%EB%A7%8E%EC%9D%B4-%EB%B3%B4%EC%97%AC%EC%9A%94&quot; aria-label=&quot;빌드된 css 파일에 미디어 쿼리가 너무 많이 보여요 permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;빌드된 css 파일에 미디어 쿼리가 너무 많이 보여요&lt;/h4&gt;
&lt;p&gt;sass는 기본적으로 &lt;a href=&quot;https://github.com/sass/sass/issues/116&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;서로 떨어져있는 media query를 합쳐주지 않습니다&lt;/a&gt;.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scss&quot;&gt;&lt;pre class=&quot;language-scss&quot;&gt;&lt;code class=&quot;language-scss&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.my-component &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;@include&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;scale&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;width&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 640px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;@include&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;scale&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;height&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 640px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;위 코드를 &lt;a href=&quot;https://sass.js.org/&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;SASS 놀이터&lt;/a&gt; 등에서 돌려보면 다음과 같이 변환됩니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;css&quot;&gt;&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.my-component&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 640px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 640px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@media&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;max-width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 640px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token selector&quot;&gt;.my-component&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 100vw&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@media&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;max-width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 640px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token selector&quot;&gt;.my-component&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 100vw&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;미디어 쿼리를 하나로 합쳐줄 법도 한데 속성별로 떨어져있는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p&gt;css 최적화 도구인 &lt;a href=&quot;https://github.com/css/csso&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;CSSO&lt;/a&gt;에는
같은 규칙의 미디어 쿼리가 서로 떨어져있으면 병합시켜주는 기능이 있는데요.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://css.github.io/csso/csso.html&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;CSSO 놀이터&lt;/a&gt;에서
위 결과물을 넣고 돌려보면 다음과 같이 병합되는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;css&quot;&gt;&lt;pre class=&quot;language-css&quot;&gt;&lt;code class=&quot;language-css&quot;&gt;&lt;span class=&quot;token selector&quot;&gt;.my-component&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 640px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 640px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@media&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;max-width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 640px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token selector&quot;&gt;.my-component&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 100vw&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 100vw&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;기본적으로는 안전하게 합쳐지는 것이 보장되는 경우에만 합쳐줍니다만,
&lt;code&gt;forceMediaMerge&lt;/code&gt; 옵션을 사용해서 좀 더 공격적인 최적화를 할 수도 있습니다.&lt;/p&gt;
&lt;h4 id=&quot;caveat&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#caveat&quot; aria-label=&quot;caveat permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;caveat&lt;/h4&gt;
&lt;p&gt;사실 다 해결된 건 아니고 한가지 해결하지 못한 문제가 있습니다.&lt;/p&gt;
&lt;p&gt;앞서 설명한 mixin은 &lt;code&gt;transform&lt;/code&gt;, &lt;code&gt;filter&lt;/code&gt; 등 추가 문법이 있는 속성을 처리하지 못하는데요,
예를 들면 &lt;code&gt;transform: translate(1px, 2px)&lt;/code&gt;와 같은 코드를 처리할 수 없습니다.&lt;/p&gt;
&lt;p&gt;transform 문법을 따로 파싱해야 하는데, sass 코드만으로는 처리하기 까다로웠습니다.&lt;br&gt;
&lt;a href=&quot;https://github.com/sass/node-sass#functions--v300---experimental&quot; target=&quot;_blank&quot; rel=&quot;nofollow noopener&quot;&gt;커스텀 함수&lt;/a&gt;와 같은 실험 확장 기능을 사용하면
해결할 수 있을 듯 한데, 지금까지는 적당히 우회 처리를 해주는 것으로 충분했기에 따로 시간을 들여 작업하지 않았습니다.&lt;/p&gt;
&lt;h2 id=&quot;one-more-thing&quot; style=&quot;position:relative;&quot;&gt;&lt;a href=&quot;#one-more-thing&quot; aria-label=&quot;one more thing permalink&quot; class=&quot;anchor before&quot;&gt;&lt;span&gt;#&lt;/span&gt;&lt;/a&gt;One more thing...&lt;/h2&gt;
&lt;p&gt;처음에 빠르게 버려진 &lt;code&gt;zoom&lt;/code&gt; 속성은 의외의 쓸모가 있었습니다.&lt;/p&gt;
&lt;p&gt;웹페이지를 세로 화면으로 볼 것을 가정하고 만들다보니
가로화면에서는 중요한 정보들이 한 눈에 들어오지 않는 문제가 있었는데요.&lt;/p&gt;
&lt;p&gt;저희는 간단하게 &lt;code&gt;zoom: 0.5&lt;/code&gt;를 주어서 가로화면에서도 적당히 잘 보이도록 만들었습니다.&lt;br&gt;
이 트릭은 파이어폭스에서는 작동하지 않습니다만 애초에 사소한 개선이라 크게 신경쓰지 않았습니다.&lt;/p&gt;
&lt;div class=&quot;gatsby-highlight&quot; data-language=&quot;scss&quot;&gt;&lt;pre class=&quot;language-scss&quot;&gt;&lt;code class=&quot;language-scss&quot;&gt;&lt;span class=&quot;token property&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$pc-contents-width&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 1000px&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// pc 화면이 시작되는 시점&lt;/span&gt;
&lt;span class=&quot;token selector&quot;&gt;:root &lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// 모바일 화면이면서 가로화면일 경우 내용 크기를 줄여줍니다.&lt;/span&gt;
  &lt;span class=&quot;token atrule&quot;&gt;&lt;span class=&quot;token rule&quot;&gt;@media&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;max-width&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$pc-contents-width&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; 1px&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;orientation&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; landscape&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;zoom&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 0.5&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 0.5 ~ 0.6 정도가 적당한 것 같습니다.&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;gatsby-resp-image-figure&quot; style=&quot;&quot;&gt;
    &lt;span
      class=&quot;gatsby-resp-image-wrapper&quot;
      style=&quot;position: relative; display: block; margin-left: auto; margin-right: auto; max-width: 1280px; &quot;
    &gt;
      &lt;span
    class=&quot;gatsby-resp-image-background-image&quot;
    style=&quot;padding-bottom: 37.5%; position: relative; bottom: 0; left: 0; background-image: url(&apos;data:image/jpeg;base64,/9j/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wgARCAAIABQDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAIE/8QAFQEBAQAAAAAAAAAAAAAAAAAAAQL/2gAMAwEAAhADEAAAAYgms4H/xAAXEAEBAQEAAAAAAAAAAAAAAAABAgAD/9oACAEBAAEFAruZJ6iNG//EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oACAEDAQE/AVf/xAAUEQEAAAAAAAAAAAAAAAAAAAAQ/9oACAECAQE/AT//xAAZEAEAAgMAAAAAAAAAAAAAAAABACERQWH/2gAIAQEABj8CAvEFrhNz/8QAGxAAAgEFAAAAAAAAAAAAAAAAAAERITFBUXH/2gAIAQEAAT8hRRwsVqyCRlOz/9oADAMBAAIAAwAAABB33//EABcRAAMBAAAAAAAAAAAAAAAAAAARIUH/2gAIAQMBAT8QqIw//8QAFxEAAwEAAAAAAAAAAAAAAAAAAAERIf/aAAgBAgEBPxDE7CH/xAAdEAEAAQMFAAAAAAAAAAAAAAABEQAhMUFRYaHB/9oACAEBAAE/EIj9Soum653pukbGuPvVSqmRjJxX/9k=&apos;); background-size: cover; display: block;&quot;
  &gt;&lt;/span&gt;
  &lt;picture&gt;
          &lt;source
              srcset=&quot;/static/909167116ec9dcba12903dc1fa85f6f0/33ecc/zoom.webp 360w,
/static/909167116ec9dcba12903dc1fa85f6f0/263a4/zoom.webp 480w,
/static/909167116ec9dcba12903dc1fa85f6f0/0c8fb/zoom.webp 640w,
/static/909167116ec9dcba12903dc1fa85f6f0/521f7/zoom.webp 860w,
/static/909167116ec9dcba12903dc1fa85f6f0/cbd37/zoom.webp 1200w,
/static/909167116ec9dcba12903dc1fa85f6f0/94b1e/zoom.webp 1280w&quot;
              sizes=&quot;(max-width: 1280px) 100vw, 1280px&quot;
              type=&quot;image/webp&quot;
            /&gt;
          &lt;source
            srcset=&quot;/static/909167116ec9dcba12903dc1fa85f6f0/543cd/zoom.jpg 360w,
/static/909167116ec9dcba12903dc1fa85f6f0/a3e66/zoom.jpg 480w,
/static/909167116ec9dcba12903dc1fa85f6f0/56d4e/zoom.jpg 640w,
/static/909167116ec9dcba12903dc1fa85f6f0/2f567/zoom.jpg 860w,
/static/909167116ec9dcba12903dc1fa85f6f0/03ffe/zoom.jpg 1200w,
/static/909167116ec9dcba12903dc1fa85f6f0/62aaf/zoom.jpg 1280w&quot;
            sizes=&quot;(max-width: 1280px) 100vw, 1280px&quot;
            type=&quot;image/jpeg&quot;
          /&gt;
          &lt;img
            class=&quot;gatsby-resp-image-image&quot;
            src=&quot;/static/909167116ec9dcba12903dc1fa85f6f0/62aaf/zoom.jpg&quot;
            alt=&quot;zoom 적용한 모양&quot;
            title=&quot;&quot;
            loading=&quot;lazy&quot;
            decoding=&quot;async&quot;
            style=&quot;width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0;&quot;
          /&gt;
        &lt;/picture&gt;
    &lt;/span&gt;
    &lt;figcaption class=&quot;gatsby-resp-image-figcaption&quot;&gt;zoom 적용한 모양&lt;/figcaption&gt;
  &lt;/figure&gt;&lt;/p&gt;
&lt;we-are-hiring /&gt;</content:encoded></item></channel></rss>