<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>짜투리 코딩</title>
    <link>https://leo-bb.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Wed, 1 Jul 2026 14:31:44 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>London_</managingEditor>
    <image>
      <title>짜투리 코딩</title>
      <url>https://tistory1.daumcdn.net/tistory/3512887/attach/c45cc3dfbc4c45e4ae2339184b7e4dde</url>
      <link>https://leo-bb.tistory.com</link>
    </image>
    <item>
      <title>Airflow(GCP Composer) 에서 KubernetesPodOperator 사용</title>
      <link>https://leo-bb.tistory.com/89</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;Airflow(Google Composer2) 를 활용해 워크플로우 오케스트레이션을 하고 있습니다. 최근 사내 파이프라인에 DBT 를 도입하다보니 composer 2의 기본 패키지 의존성과 DBT의 패키지 의존성간에 충돌이 있기에 컨테이너 기반으로 DBT 를 활용하도록 KubernetesPodOperator를 고려하였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 문서는 비공개 환경의 k8s(GKE) 기반 airflow(GCP Composer)에서 KubernetesPodOperator를 사용하는 방법에 대해 안내합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 composer 1 과 composer2 에서 사용 설정이 일부 상이합니다. 본 문서는 autopilot 으로 관리되는 composer2 환경에서 KubernetesPodOperator 사용법을 설명합니다.&lt;/p&gt;
&lt;h1&gt;환경&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;gcp composer 2.1 (airflow -v 2.4.3)&lt;/li&gt;
&lt;li&gt;클러스터 환경
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클러스터 : 비공개&lt;/li&gt;
&lt;li&gt;VPC 기반 트래픽 라우팅 : 사용&lt;/li&gt;
&lt;li&gt;제어 영역 승인 네트워크 : 서비스 한정&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;gcloud -v
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Google Cloud SDK 412.0.0&lt;/li&gt;
&lt;li&gt;core 2022.12.09&lt;/li&gt;
&lt;li&gt;gcloud-crc32c 1.0.0&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;KubernetesPodOperator란?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KubernetesPodOperator는 Composer 환경의 일부인 GKE 로 배포하여 pod을 실행하는 operator 입니다. google composer 2 가 배포되는 클러스터는 google autopilot ( Google에서 노드, 확장, 보안, 기타 사전 구성된 설정을 포함한 클러스터 구성을 관리하는 GKE의 작동 모드 ) 으로 관리되기 때문에 KubernetesPodOperator 으로 여러 pod을 배포해도 전체 시스템 성능에 영향을 주지 않기 때문에 k8s cluster 를 손쉽게 사용할 수 있는 대안이 될 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 다른 환경 클러스터에 배포 및 관리를 하고 싶다면 Google Kubernetes Engine 연산자를 활용한 GKECreateClusterOperator, GKEStartPodOperator, GKEDeleteClusterOperator 와 같은 operator 를 활용할 수 있습니다.&lt;/p&gt;
&lt;h1&gt;KubernetesPodOperator 사용 설정&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;k8s 클러스터 사용자 인증&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클러스터에서 액션을 위해 아래와 같이 사용자 인증 정보를 가져옵니다.&lt;/p&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;gcloud container clusters get-credentials {CLUSTER_NAME} --region {CLUSTER_REGION} --project {GCP_PROJECT_NAME}

-&amp;gt;
Fetching cluster endpoint and auth data.
kubeconfig entry generated for {CLUSTER_NAME}.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 &lt;code&gt;kubectl&lt;/code&gt; 명령어 들을 날릴 때 다음과 같은 에러가 발생할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Unable to connect to the server: dial tcp IP_ADDRESS: connect: connection timed out&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Unable to connect to the server: dial tcp IP_ADDRESS: i/o timeout&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 경우 클러스터 인증 정보를 가져오는 과정에서 잘못된 것 일 수 있으므로 다음과 같은 명령어를 통해 클러스터의 config 설정에 cluster context 와 외부 IP 주소 존재 여부를 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;kubectl config view&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정에 문제가 없다면 비공개 클러스터 환경에 연결하려는 머신의 발신 IP 가 승인된 기존 네트워크 목록 ( 제어 영역 승인 네트워크 ) 에 포함되어 있지 않기 때문일 수 있습니다. 다음과 같은 명령어를 통해 승인된 네트워크를 확인합니다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;gcloud container clusters describe {CLUSTER_NAME} \
    --region={COMPUTE_REGION} \
    --project={PROJECT_ID} \
    --format &quot;flattened(masterAuthorizedNetworksConfig.cidrBlocks[])&quot;

-&amp;gt; 
masterAuthorizedNetworksConfig.cidrBlocks[0].cidrBlock: *.**.**.**/32
masterAuthorizedNetworksConfig.cidrBlocks[1].cidrBlock: **.**.***.**/32&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제어 영역 승인 네트워크에 ip가 등록되어 있지 않다면 다음과 같은 절차를 따릅니다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;dig +short myip.opendns.com @resolver1.opendns.com

-&amp;gt;
my.machine.ip.address&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;dust&quot;&gt;&lt;code&gt;gcloud container clusters update {CLUSTER_NAME} --region {CLUSTER_REGION} --project {PROJECT_ID}\                                                                                                             --enable-master-authorized-networks \
--master-authorized-networks {exist_ip},{my.machine.ip.address/32}

-&amp;gt;
Updating {CLUSTER_NAME}...done.     
To inspect the contents of your cluster, go to: https://console.cloud.google.com/kubernetes/workload_/gcloud/{CLUSTER_REGION}/{CLUSTER_NAME}?project={PROJECT_ID}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 한번 아래 명령어로 클러스터의 사용자 인증정보를 가져옵니다.&lt;/p&gt;
&lt;pre class=&quot;dsconfig&quot;&gt;&lt;code&gt;gcloud container clusters get-credentials {CLUSTER_NAME} --region {CLUSTER_REGION} --project {GCP_PROJECT_NAME}

# Cloud shell 에서 진행하였거나 여전히 connect server error 아래 명령어로 재시도
gcloud container clusters get-credentials {CLUSTER_NAME} --region {CLUSTER_REGION} --project {GCP_PROJECT_NAME} --internal-ip&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;워크로드 아이덴티티 설정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Composer 2 에서 클러스터는 워크로드 아이덴티티를 사용합니다. 따라서 새로 생성된 namespace 나 default 에서 실행되는 pod은 google cloud resource에 접근하지 못합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 2가지 옵션을 선택할 수 있습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기 구성된 &lt;code&gt;composer-user-workloads&lt;/code&gt; namespace 를 사용한다&lt;/li&gt;
&lt;li&gt;자체 namespace 를 구성하는 경우 적절한 IAM 바인딩이 생성되도록 하namespace 연결된 k8s 서비스 계정과 google service 계정을 매핑한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 내용은 2번째 옵션인 자체 namespace 를 구성하는 예입니다. 구글 서비스 계정은 이미 존재한다고 가정합니다.&lt;/p&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;# namespace 생성
kubectl create namespace data-engineering&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;haskell&quot;&gt;&lt;code&gt;# Kubernetes 서비스 계정을 namespace 에 생성
kubectl create serviceaccount data-engineering \                     
    --namespace data-engineering&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;smali&quot;&gt;&lt;code&gt;# Kubernetes 서비스 계정과 IAM 서비스 계정 사이에 IAM 정책 바인딩.
# 이렇게 하면 Kubernetes 서비스 계정이 IAM 서비스 계정처럼 동작할 수 있음
gcloud iam service-accounts add-iam-policy-binding {GOOGLE_SA} \ 
    --role roles/iam.workloadIdentityUser \
    --member &quot;serviceAccount:project-id.svc.id.goog[namespace/k8s_SA]&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# Kubernetes 서비스 계정에 주석 달아 놓기
kubectl annotate serviceaccount {k8s_SA} \
    --namespace {NAMESPACE} \
    iam.gke.io/gcp-service-account={GOOGLE_SA}&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;KubernetesPodOperator 사용&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;KubernetesPodOperator 설치&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;composer2 로 구성되는 airflow 에는 기본적으로 KubernetesPodOperator 를 사용하기 위한 패키지가 사전에 설치되어 있지만 패키지가 없는 경우 PYPI 패키지에 apache-airflow-providers-cncf-kubernetes 를 추가하거나 pip install 합니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;KubernetesPodOperator 선언&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KubernetesPodOperator를 사용하려면 DAG 파일에서 다음과 같이 import합니다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;from airflow.providers.cncf.kubernetes.operators.kubernetes_pod import KubernetesPodOperator&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 다음과 같이 KubernetesPodOperator를 정의합니다.&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;KubernetesPodOperator(
        task_id = &quot;task_id&quot;, # airflow 워크로드에 생성될 task id
        name = &quot;pod_id&quot;, # 생성될 Pod id
        namespace = &quot;my_namesapce&quot;, # 기 생성한 namesapce 이름
        image = &quot;image path&quot;, # 사용할 docker container image,
        image_pull_policy = &quot;Always&quot;, # 이미지를 매번 새로 pull 할 것인지 cache 를 사용할 것인지에 대한 옵션
        cmds= [&quot;entriy point&quot;], # 이미지의 Entrypoint. 선언하지 않으면 이미지의 Entrypoint 를 사용
        arguments= [&quot;my&quot;, &quot;command&quot;], # Entrypoint argument. 선언하지 않으면 이미지의 CMD 를 사용 
        get_logs=True,  # pod 의 로그 출력 여부
        log_events_on_failure=True,  # pod 동작 실패시 로그 출력 여부
        is_delete_operator_pod = True, # 동작 이후 pod 제거 여부
        service_account_name= &quot;k8s_servce_account&quot;,
        config_file=&quot;/home/airflow/composer_kube_config&quot;,
        kubernetes_conn_id=&quot;kubernetes_default&quot;,
    )&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;실적용 예제&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;제 경우 KubernetesPodOperator 를 활용해 DBT 를 동작시키는게 목적이었습니다. 따라서 다음과 같이 구성하여 활용하고 있습니다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dags/dependencies/k8s_operator.py&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import json
import logging
from typing import Union
from airflow.providers.cncf.kubernetes.operators.kubernetes_pod import KubernetesPodOperator

def generate_dbt_on_kubernetes(mode : str, target : str, tag : Union[str,list] = None , **context):
    &quot;&quot;&quot;
        args:
            - mode (str) : 실행 옵션(&quot;run&quot;, &quot;test&quot;)을 받음. 그 외의 경우 valueerror
            - target (str) : 실행 model(&quot;stage&quot;, &quot;dw&quot;, &quot;mart&quot;)를 받음. 그 외의 경우 valueerror
            - tag (str or list) : 모델에 정의된 tag 옵션
    &quot;&quot;&quot;
    target_datetime = context.get(&quot;data_interval_end&quot;)
    date = target_datetime.strftime(&quot;%Y-%m-%d&quot;)    
    var_dict = {&quot;execution_date&quot; : date}
    vars = json.dumps(var_dict)

    if mode not in [&quot;run&quot;, &quot;test&quot;]:
        raise ValueError(&quot;generate_dbt_on_kubernetes() : mode must be 'run' or 'test'&quot;)
    if target not in [&quot;stage&quot;, &quot;dw&quot;, &quot;mart&quot;] : 
        raise ValueError(&quot;generate_dbt_on_kubernetes() : target must be 'stage', 'test' or 'mart'&quot;)

    args = [f&quot;{mode}&quot;, &quot;--vars&quot; , vars, &quot;--profiles-dir&quot;, f&quot;profile/{target}&quot;, &quot;--select&quot;, f&quot;path:models/{target}&quot;]

    if tag is not None:
        if type(tag) == str : tag = [tag]
        tag_args =''
        for v in tag:
            tag_args += f&quot;tag:{v},&quot;
        args.append(tag_args.rstrip(','))    

    logging.info(f&quot;generate_dbt_on_kubernetes() : dbt args : [{args}]&quot;)
    KubernetesPodOperator(
        task_id=f&quot;{mode}-DBT-{target}&quot;,
        name=f&quot;{mode}-DBT-{target}&quot;,
        ...
    ).execute(context)

...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;dags/my_dag.py&lt;/p&gt;
&lt;pre style=&quot;background-color: #f8f8f8;&quot;&gt;&lt;code&gt;...

from dependencies import k8s_operator

from airflow import models
from airflow.decorators import task
from airflow.utils.task_group import TaskGroup

...

with models.DAG(
    ...
) as dag:

    with TaskGroup(&quot;EL_Job&quot;) as airbyte_job:
        ...

    with TaskGroup(&quot;T_job&quot;) as dbt_job:
        run_dbt_stage_task = PythonOperator(
            task_id='dbt_run_stage_model',
            provide_context=True,
            python_callable=k8s_operator.generate_dbt_on_kubernetes,
            op_kwargs={
                &quot;mode&quot;: &quot;run&quot;,
                &quot;target&quot;: &quot;stage&quot;,
                &quot;tag&quot; : [&quot;hourly&quot;,&quot;staging&quot;]
                },
            on_failure_callback = slack_alert
        )

                ...&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://cloud.google.com/kubernetes-engine/docs/troubleshooting#connection_refused&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://cloud.google.com/kubernetes-engine/docs/troubleshooting#connection_refused&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://cloud.google.com/kubernetes-engine/docs/how-to/private-clusters#public_cp&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://cloud.google.com/kubernetes-engine/docs/how-to/private-clusters#public_cp&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity?hl=ko&amp;amp;cloudshell=false#authenticating_to&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity?hl=ko&amp;amp;cloudshell=false#authenticating_to&lt;/a&gt;&lt;/p&gt;</description>
      <category>Data/Data Engineering</category>
      <category>Airflow</category>
      <category>Composer</category>
      <category>Connection timed out</category>
      <category>DBT</category>
      <category>GKE</category>
      <category>Kubernetes</category>
      <category>KubernetesPodOperator</category>
      <category>Unable to connect to the server</category>
      <author>London_</author>
      <guid isPermaLink="true">https://leo-bb.tistory.com/89</guid>
      <comments>https://leo-bb.tistory.com/89#entry89comment</comments>
      <pubDate>Sun, 23 Apr 2023 13:27:37 +0900</pubDate>
    </item>
    <item>
      <title>좋은 에러메세지 작성 방법</title>
      <link>https://leo-bb.tistory.com/88</link>
      <description>&lt;p&gt;이 글은 구글에서 작성된 &lt;a href=&quot;https://developers.google.com/tech-writing/error-messages/error-handling&quot;&gt;Writing Helpful Error Messages&lt;/a&gt; 을 정리한 내용입니다.&lt;/p&gt;
&lt;h1&gt;에러 메세지는 왜 중요한가?&lt;/h1&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;h1&gt;공통 오류 처리 규칙&lt;/h1&gt;
&lt;h2&gt;실패 상황을 넘어가선 안된다.&lt;/h2&gt;
&lt;p&gt;Failure 는 어떠한 상황에서든 발생할 수 있기에 100% 성공은 언제나 보장 할 수 없다. 만약 실패한 상황을 reporting 하지 않고 넘어간다면 2가지 문제를 야기한다.&lt;/p&gt;
&lt;p&gt;첫째, 사용자는 문제 원인을 정확히 파악할 수 없다.&lt;/p&gt;
&lt;p&gt;둘째, 고객 지원팀도 문제의 원인을 정확히 파악할 수 없다.&lt;/p&gt;
&lt;p&gt;고객이 본인의 문제 상황을 정확히 파악할 수 없기 때문에 지원요청의 내용만으로는 문제를 명확히 파악할 수 없다. 나아가 실패 상황을 기록하지 않고 넘어간다는 것은 어떠한 기록(log) 에도 해당 상황에 대한 정보가 없다는 것을 의미 하기 때문에 개발자 입장에서도 문제 재현에 어려움을 겪을 수 밖에 없다.&lt;/p&gt;
&lt;h2&gt;각 언어별 가이드라인을 명확히 지킨다&lt;/h2&gt;
&lt;p&gt;Python, go, java, java script 등 다양한 언어들은 각 언어마다 개발 규칙이 있다. 이러한 문서에 에러 처리에 대한 규칙이 명시되어 있으므로 이것을 지키도록 노력한다.&lt;/p&gt;
&lt;h2&gt;근본 원인을 무시하도록 해선 안된다.&lt;/h2&gt;
&lt;p&gt;API 형태의 구현은 단순히 사용자 코드의 문제를 넘어 백엔드상에서 발생할 수 있는 근본적 원인이 존재할 수 있다. 예를 들어 “서버 오류” 라고 불리는 에러 내부에는 네트워크 끊김, 권한 문제, 서비스 실패 등이 있다. 이러한 상황에서 단순히 “서버 오류” 라고 표기하는 것은 사용자가 문제를 이해하고 수정하기 위해서는 지나치게 일반화 되어 있다고 볼 수 있다. 각 에러 상황 마다 추가적인 정보를 반드시 제공하도록 한다.&lt;/p&gt;
&lt;h2&gt;에러를 모델링 한다&lt;/h2&gt;
&lt;p&gt;여러 오류 상황을 일반화하고 그 하위에 세부적인 오류 항목이 포함된 에러 모델링은 사용자와 고객 지원 모든 상황에서 유용하다. 각각의 에러를 코드화하여 관리하고 각각의 구조와 상속관계를 문서화하여 제공한다면 훨씬 효율적인 오류 처리가 가능하다.&lt;/p&gt;
&lt;h2&gt;오류는 모두 즉시 발생시킨다&lt;/h2&gt;
&lt;p&gt;오류의 경중에 상관 없이 발생하면 바로 발생시킨다. 시스템에 미치는 영향이 적은 오류라고 하여 발생시키지 않고 넘어간다면 오히려 디버깅에 더 큰 어려움을 줄 수 있다.&lt;/p&gt;
&lt;h1&gt;좋은 오류 메세지의 기본&lt;/h1&gt;
&lt;h2&gt;명확한 원인을 전달한다&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Invalid field &amp;#39;picture&amp;#39;.
picture 필드가 잘못되었습니다.

The &amp;#39;picture&amp;#39; field can only appear once on the command line; this command line contains the &amp;#39;picture&amp;#39; field &amp;lt;N&amp;gt; times.
Note: Prior to version 2.1, you could specify the &amp;#39;picture&amp;#39; field more than once, but more recent versions no longer support this.

picture 필드는 1회만 호출됩니다. 해당 명령줄에는 picture 필드가 n번 포함되어 있습니다.
참고 : 2.1버전 이전에는 picture 필드를 2번 이상 지정할 수 있었으나 최신 버전에서는 더 이상 지원하지 않습니다.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;위 두 메세지는 모두 picture 필드의 오용을 지적하고 있으나 후자가 에러에 대해 훨씬 소상하고 즉시 조치 가능하도록 알려주고 있는 것을 볼 수 있다. 좋은 오류 메세지는 아래와 같이 명확하게 원인을 전달하여야 한다.&lt;/p&gt;
&lt;h2&gt;사용자의 잘못된 입력을 명확히 전달하고 예제를 전달한다&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Invalid input &amp;quot;bid&amp;quot;
지정된 입찰가가 잘못되었습니다.

The specified bid ($5) is below the minimum bid ($8).
지정된 입찰가($5)가 최소 입찰가($8)보다 낮습니다.

Invalid input
입력이 잘못되었습니다.

Enter the pathname of a Windows executable file. An executable file ordinarily ends with the .exe suffix. For example: C:\Program Files\Custom Utilities\StringFinder.exe
Windows 실행 파일을 경로 이름으로 입력하십시오. 실행 파일은 일반적으로 .exe 접미사로 끝납니다. 예: C:\Program Files\Custom Utilities\StringFinder.exe&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;오류 메세지에 사용자가 입력하거나 수정할 수 있는 값이 있다면 오류 메세지에 이를 반드시 표기해주는게 오류 처리에 큰 도움이 된다. 위와 같이 사용자가 지정한 입력이 유효하지 않은 이유를 명확히 전달하고 해결 방법을 예제로 보충 설명해주면 사용자가 즉각적인 오류 처리가 가능하다.&lt;/p&gt;
&lt;p&gt;만약 잘못된 입력이 여러줄에 걸쳐서 나타난다면 잘못된 입력을 점진적으로 제공하거나 중요한 부분만 유지하고 잘못된 입력을 잘라서 보여주는 것이 좋다.&lt;/p&gt;
&lt;h2&gt;요구 사항과 제약 조건을 명확히 지정해준다&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Permission denied.
권한이 없습니다.

Permission denied. Only users in &amp;lt;group name&amp;gt; have access. [Details about adding users to the group.]
권한이 없습니다. &amp;lt;그룹 이름&amp;gt; 의 사용자만 접근할 수 있습니다. [그룹에 사용자를 추가하는 세부정보]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;오류가 발생했을 때 사용자는 시스템이 요구하는 조건을 아예 모르고 있음을 가정하고 오류 메세지를 작성한다. 시스템이 요구하는 사항과 제약하는 상황을 명확히 알려주는 것이 좋다.&lt;/p&gt;
&lt;h2&gt;사용자가 실행 가능한 오류 메세지를 제안합니다&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;The client app on your device is no longer supported.
장치의 클라이언트 앱은 더이상 지원되지 않습니다.

The client app on your device is no longer supported. To update the client app, click the Update app button.
장치의 클라이언트 앱은 더이상 지원되지 않습니다. 클라이언트 앱을 업데이트하려면 업데이트 버튼을 클릭합니다.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;단순히 문제의 원인을 기술하는 것을 넘어서 사용자가 해당 문제를 해결하기 위해 실제로 실행 가능한 방법을 함께 설명한다.&lt;/p&gt;
&lt;h2&gt;일관된 용어로 작성합니다..&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Can&amp;#39;t connect to cluster at 127.0.0.1:56. Check whether minikube is running.
127.0.0.1:56 에서 클러스터에 연결할 수 없습니다. minikube가 실행중인지 확인하세요.

Can&amp;#39;t connect to minikube at 127.0.0.1:56. Check whether minikube is running.
127.0.0.1:56 에서 minikube에 연결할 수 없습니다. minikube가 실행중인지 확인하세요.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;단일 제품에 대해서 모든 오류 메세지에 일관된 용어를 사용하는 것이 좋다. 개발자 입장에서 두 단어가 같은 제품을 의미하더라도 사용자 역시 그렇게 이해할 것으로 생각해선 안된다&lt;/p&gt;
&lt;h2&gt;적절한 톤 앤 매너로 작성합니다.&lt;/h2&gt;
&lt;p&gt;메세지를 작성하는 어조에 따라 사용자가 받아들이는 방식에 큰 영향을 미칠 수 있다.&lt;/p&gt;
&lt;h3&gt;되도록 긍정문을 활용한다&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;You didn&amp;#39;t enter a name. 이름을 입력하지 않았습니다. Enter a name. 이름을 입력하세요.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;무언가를 잘못했다고 지적하는 것보다 해결 방법을 알리는데 집중한다.&lt;/p&gt;
&lt;h3&gt;유머는 자제합니다.&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Is the server running? Better go catch it :D. 
서버가 실행 중(달리는 중)입니까? 잡으러 가세요ㅋㅋㅋㅋ 

The server is temporarily unavailable. Try again in a few minutes. 
일시적으로 서버를 사용할 수 없습니다. 몇 분 후에 다시 시도하십시오.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;유머러스한 오류 메세지는 오류 메세지의 목적을 손상시키고 사용자로 하여금 불쾌감을 느끼게 할 수 있다.&lt;/p&gt;
&lt;h1&gt;가독성을 높이는 오류 메세지 형식&lt;/h1&gt;
&lt;p&gt;아무리 좋은 정보를 담고 있어도 오류 메세지에 불필요한 텍스트가 많고 길다면 사용자는 중요한 부분을 찾지 못하고 집중력을 잃을 수 밖에 없다.&lt;/p&gt;
&lt;h2&gt;불필요한 텍스트를 제거합니다.&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Unable to establish connection to the SQL database. [Explanation of how to fix the issue.]

Can&amp;#39;t connect to the SQL database. [Explanation of how to fix the issue.]&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;에러 메세지는 최대한 쉬운 단어로 짧게 작성될수록 효과적이다. 둘은 같은 의미의 문장이지만 아래 문장이 훨씬 간결하다.&lt;/p&gt;
&lt;h2&gt;수동태보다 능동태를 사용합니다.&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;The Froobus operation is no longer supported by the Frambus app.
Froobus 작업은 Frambus 앱에서 더이상 지원되지 않습니다.

The Frambus app no longer supports the Froobus operation.
Frambus 앱은 더이상 Froobus 작업을 지원하지 않습니다.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;한국어와 다르게 영어는 수동태로 작성될 때 문장 구조가 훨씬 복잡하고 길어진다. 되도록 능동형으로 표현하여 문장을 단순하게 만드는게 좋다.&lt;/p&gt;
&lt;h2&gt;이중 부정을 피합니다.&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;The App Engine service account must have permissions on the image, except the Storage Object Viewer role, unless the Storage Object Admin role is available.
Storage Object Admin 역할을 사용할 수 있는 경우가 아니면 App Engine 서비스 계정에 Storage Object Viewer 역할을 제외하고 이미지에 대한 권한이 있어야 합니다.

The App Engine service account must have one of the following roles:
 * Storage Object Admin
 * Storage Object Creator
App Engine 서비스 계정에는 다음 역할 중 하나가 있어야 합니다.
 * 저장소 개체 관리자
 * 저장소 개체 생성자&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;not, no (can’t, won’t 와 같은 단어 포함) 를 2개 이상 포함하는 문장을 피한다. 이는 독자에게 이중 부정이 부정을 강조하기 위한 목적인지, 부정의 부정이 되어 긍정을 의미하는 것인지 혼란스럽게 만든다. 이중 부정을 피하는 것 만으로도 오류 메세지를 훨씬 명확하고 간결하게 바꿀 수 있다.&lt;/p&gt;
&lt;h2&gt;사과 표현은 피합니다.&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;We&amp;#39;re sorry, a server error occurred and we&amp;#39;re temporarily unable to load your spreadsheet. We apologize for the inconvenience. Please wait a while and try again.
죄송합니다. 서버 오류가 발생하여 일시적으로 스프레드시트를 로드할 수 없습니다. 불편을 끼쳐드려 죄송합니다. 잠시 기다린 후 다시 시도하십시오.

Google Docs is temporarily unable to open your spreadsheet. In the meantime, try right-clicking the spreadsheet in the doc list to download it.
Google 문서도구에서 일시적으로 스프레드시트를 열 수 없습니다. 그동안 문서 목록에서 스프레드시트를 마우스 오른쪽 버튼으로 클릭하여 다운로드해 보세요.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;이는 대상자의 특성(문화적 차이)에 영향을 받지만 일반적으로는 사과의 표현보다 문제와 해결방안에 집중하는 것이 효과적이다. 사용자는 지금 당장 문제를 회피하는 것이 가장 중요하기 때문이다. 또한 사과 표현은 오류 메세지를 필연적으로 길어지게 만든다.&lt;/p&gt;
&lt;h2&gt;자세한 정보는 링크를 통해 리디렉션합니다.&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Post contains unsafe information.
게시물에 안전하지 않은 정보가 포함되어 있습니다.

Post contains unsafe information. Learn more about safety at &amp;lt;link to documentation&amp;gt;.
게시물에 안전하지 않은 정보가 포함되어 있습니다. &amp;lt;문서 링크&amp;gt;에서 안전에 대해 자세히 알아보세요.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;오류에 대해 긴 설명이 필요하거나 이미 적절한 설명서가 있는 경우 이를 에러 메세지에 모두 표현하기 보다 해당 링크를 제공하여 사용자가 보다 자세한 설명서를 볼 수 있도록 리디렉션 하는게 유용하다.&lt;/p&gt;
&lt;h2&gt;적절한 접어두기 사용하기&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;TextField widgets require a Material widget ancestor, but none were located. In material design, most widgets are conceptually “printed” on a sheet of material. To introduce a Material widget, either directly include one or use a widget that contains a material itself.

TextField widgets require a Material widget ancestor, but none were located.
...(Click to see more.)
&amp;gt;&amp;gt; In material design, most widgets are conceptually &amp;quot;printed&amp;quot; on a sheet of material. To introduce a Material widget, either directly include one or use a widget that contains a material itself.&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;일부 오류 메세지는 문제의 원인과 해결 방법을 설명하는 데 어쩔 수 없이 긴 텍스트가 필요할 수 있다. 이러한 경우 앞서 설명한 링크를 통한 리디렉션을 고려할 수 있지만 모든 상황에서 다 가능하지는 않다. 이 때 오류 메세지의 요약 버전을 표시한 다음 전체 내용을 보기 위한 옵션을 제공하는 것이 좋은 대안이 될 수 있다.&lt;/p&gt;</description>
      <category>Programming</category>
      <category>Debug</category>
      <category>error</category>
      <category>error code</category>
      <category>error message</category>
      <category>programming</category>
      <category>Python</category>
      <author>London_</author>
      <guid isPermaLink="true">https://leo-bb.tistory.com/88</guid>
      <comments>https://leo-bb.tistory.com/88#entry88comment</comments>
      <pubDate>Wed, 25 Jan 2023 23:52:48 +0900</pubDate>
    </item>
    <item>
      <title>Airflow 사용 시 AssertionError: daemonic processes are not allowed to have children 의 해결</title>
      <link>https://leo-bb.tistory.com/87</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (6).png&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;320&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmyC8s/btrOdhlRfuA/LAYb6Gm8IRVgG3Mg8zu2k1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmyC8s/btrOdhlRfuA/LAYb6Gm8IRVgG3Mg8zu2k1/img.png&quot; data-alt=&quot;사진 1. concurrent.futures 사용시 경험한 Error ( written by author )&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmyC8s/btrOdhlRfuA/LAYb6Gm8IRVgG3Mg8zu2k1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmyC8s%2FbtrOdhlRfuA%2FLAYb6Gm8IRVgG3Mg8zu2k1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;810&quot; height=&quot;320&quot; data-filename=&quot;Untitled (6).png&quot; data-origin-width=&quot;810&quot; data-origin-height=&quot;320&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;사진 1. concurrent.futures 사용시 경험한 Error ( written by author )&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현상 파악&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Airflow 2.0 버전 대에서 concurrent.futures.ProcessPoolExecutor 사용 시 &lt;code&gt;AssertionError: daemonic processes are not allowed to have children&lt;/code&gt; 에러를 경험하였다. (local , celery, kubernates 모두 같은 현상을 재현할 수 있었음 )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리서치 결과 &lt;b&gt;정확히는 &lt;u&gt;multiprocessing 패키지에서 발생하는 문제&lt;/u&gt;&lt;/b&gt;라는 것을 알 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;concurrent.futures&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;python 작업 시 multiprocessing이나 multithread 작업을 위해 자주 사용하는 패키지로 사진의 에러를 발생시킨 것은 ProcessPoolExecutor 사용 시 경험했다. 이 &lt;a href=&quot;https://docs.python.org/ko/3/library/concurrent.futures.html#concurrent.futures.ProcessPoolExecutor&quot;&gt;ProcessPoolExecutor&lt;/a&gt; 클래스는 프로세스 풀을 사용하여 호출을 비동기적으로 실행 가능하게 한다. ( &lt;a href=&quot;https://github.com/python/cpython/blob/3.10/Lib/concurrent/futures/process.py&quot;&gt;code&lt;/a&gt; )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pyhton에서 multiprocessing을 구현하는 경우 다음과 같은 특징을 갖는다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GIL의 영향을 받지 않고 병렬성을 구현하기 좋다! &amp;rarr; 시스템적으로 보면 멀티 프로세싱이 아니라 멀티프로그래밍으로 처리해야 한다!&lt;/li&gt;
&lt;li&gt;이러한 이유로 CPU 부하가 큰 작업일수록 해당 클래스를 활용해서 작업하면 이점을 갖는다&lt;/li&gt;
&lt;li&gt;다만 개별 프로세스는 서로 데이터(메모리)를 공유하지 않고 간섭할 수 없기 때문에 copy 가 필요하여 리소스 낭비가 발생할 수 있다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 multiprocessing은 보통 대량의 데이터를 한번에 처리할 때 많이 쓴다. 이미지 처리( 화소 값 분리 혹은 전환 (CMYK &amp;harr; RGB), convolution 등) 나 신호 처리 ( FFT 등) 작업을 할 때 또는 어떤 연산 결과들을 병합하는 작업 등에 적합하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현상 분석&lt;/h2&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;def start(self):
        '''
        Start child process
        '''
        assert self._popen is None, 'cannot start a process twice'
        assert self._parent_pid == os.getpid(), \
               'can only start a process object created by current process'
        assert not _current_process._daemonic, \
               'daemonic processes are not allowed to have children'
        _cleanup()
        if self._Popen is not None:
            Popen = self._Popen
        else:
            from .forking import Popen
        self._popen = Popen(self)
        # Avoid a refcycle if the target function holds an indirect
        # reference to the process object (see bpo-30775)
        del self._target, self._args, self._kwargs
        _current_process._children.add(self)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;AssertionError: daemonic processes are not allowed to have children&lt;/code&gt; 에러는 &lt;code&gt;assert not _current_process._config.get(&amp;rsquo;daemon&amp;rsquo;)&lt;/code&gt; 조건을 만족시키지 못해서 발생 하는 것으로 데몬 프로세스는 자식 프로세스를 만들 수 없는데 현재 process 가 데몬 프로세스이므로 새로운 프로세스를 만드는 작업을 할 수 없다는 의미이다. 다만 이해할 수 없는 것은 airflow 1.x 버전에서는 문제가 생긴 적이 한 번 도 없다는 것&amp;hellip;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로세스와 데몬 프로세스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;1) 프로세스&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스란 컴퓨터에서 실제로 실행되고 있는 프로그램 또는 명령을 의미한다. 언제나 유니크한 ID 값(PID) 를 갖는데, 때문에 같은 프로그램이나 명령을 반복적으로 요청하면 PID는 항상 바뀐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 프로세스는 계층 구조를 형성할 수 있는데, 사용자가 최초에 생성한(실행한) 프로세스가 상위 프로세스 이며 이 상위 프로세스 동작 과정에서 필요시 하위 프로세스(자식 프로세스)를 생성하여 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 자식 프로세스는 n&amp;gt;0 개일 수 있지만 자식 프로세스가 자신의 자식을 생성하는 것은 불가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2) 데몬 프로세스&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OS적으로는 사용자가 실제로 실행시키지 않아도 지속적으로 background에 존재하는 프로세스로 둘 이상의 작업자 또는 프로세스가 공유할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템(프로세스)의 실행 시 생성되며 종료 시 제거되는 것으로 이때 작업이 끝나지 않았어도 종료(SIGTERM.KILLED) 되기 때문에 데몬 프로세스를 활용하는 경우 중도에 강제 종료되어도 문제가 없도록 고려해야 한다. (자식 프로세스는 다르다. 모든 자식 프로세스의 종료가 상위 프로세스의 종료보다 선행되기 때문)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프린터 연결 드라이버나 캡쳐보드와 같은 프로세스를 생각하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파이썬은 인터프리터 언어로 모든 애플리케이션은 파이썬 인터프리터의 new main instance(process)에서 실행되고 os에서 관리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.python.org/2/library/multiprocessing.html#multiprocessing.Process.daemon&quot;&gt;파이썬의 multiprocessing 모듈 에서는 프로세스를 start() 하기 전에 daemon = True/False를 선언&lt;/a&gt;하여 생성할 수 있으며 그 개념과 같게 프로세스가 종료될 때 daemon 으로 선언된 모든 프로세스를 종료시킨다.( 당연히 이때 생성되는 데몬 프로세스는 &lt;a href=&quot;https://en.wikipedia.org/wiki/List_of_Unix_daemons&quot;&gt;unix daemon&lt;/a&gt; 이나 서비스와는 다르다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 것은 daemon=True 인 프로세스는 자식 프로세스를 생성할 수 없는데, 상위 프로세스가 종료되어 daemon 프로세스가 제거될 때 자식 프로세스가 있다면 그 프로세스는 종료되지 않고 고아 상태로 남겨지기 때문이다. (고아는 좀비와는 조금 다른 개념이다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Airflow에서 Process&lt;/h3&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;....
def check_pid():
    print(f&quot;start checking pid task 1&quot;)
    print(
        f&quot;parent process : {os.getppid()} is daemon? : {multiprocessing.parent_process().daemon if multiprocessing.parent_process() is not None else None}&quot;
    )
    print(
        f&quot;process : {os.getpid()} is daemon? : {multiprocessing.current_process().daemon}&quot;
    )
....
pp_task = PythonOperator(
        task_id=&quot;pp_task&quot;,
        python_callable=check_pid,
    )&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;{standard_task_runner.py:52} INFO - Started process 67058 to run task
{standard_task_runner.py:52} INFO - Job 54375: Subtask pp_task 
...

start checking pid task 1
parent process : 67046 is daemon? : None
process : 67058 is daemon? : True&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;os.getppid()&lt;/code&gt; 와 &lt;code&gt;os.geitpid()&lt;/code&gt; 를 통해 실제로 작업이 실행되고 있는 process를 알 수 있고, &lt;code&gt;multiprocessing.&amp;lt;&amp;gt;.daemon&lt;/code&gt; 을 통해 각각의 프로세스의 데몬 세팅이 어떻게 되어 있는지 보고자 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실제 작업이 실행되고 있는 current process 가 daemon process 임을 알 수 있는데, 이는 DAG의 정상 종료가 아닌 다른 이유로 상태가 바뀔 때 강제로 종료되어야 하기 때문으로 이해할 수 있다&lt;/b&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;여기서 의문이었던 점은 왜 부모 프로세스인 67046의 daemon이 None 인가? 였다. (이는 multiprocessing.parent_process = None 임을 뜻한다.) 찾고 찾다가 답을 얻을 수 없어 직접 질문을 올린 결과 &lt;a href=&quot;https://stackoverflow.com/questions/74010614/why-os-getppid-and-multiprocessing-parent-process-pid-got-different-result-u/74010700?noredirect=1#comment130685644_74010700&quot;&gt;답변을&lt;/a&gt; 받을 수 있었다. &lt;br /&gt;multiprocessing.parent_process()는 자식 프로세스가 선언될 때 결정되는데 이 경우 multiprocessing 은 별도의 프로세스를 선언한 적이 없기 때문이며 os를 통해 나온 결과는 실제 부모 거나 또 다른 프로세스 일 수 있다는 것이다.&lt;/blockquote&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;...
def check_pid():
    print(f&quot;start checking pid task 1&quot;)
    print(
        f&quot;parent process : {os.getppid()} is daemon? : {multiprocessing.parent_process().daemon if multiprocessing.parent_process() is not None else None}&quot;
    )
    print(
        f&quot;process : {os.getpid()} is daemon? : {multiprocessing.current_process().daemon}&quot;
    )

def process_function(i):
    parent_process = multiprocessing.parent_process().pid
    parent_process_daemon = (
        multiprocessing.parent_process().daemon
        if multiprocessing.parent_process() is not None
        else None
    )
    current_process = multiprocessing.current_process().pid
    is_daemon = multiprocessing.current_process().daemon
    result = (
        f&quot;{i}th task : &quot;
        + &quot;partent_process : &quot;
        + str(parent_process)
        + &quot; is daemon : &quot;
        + str(parent_process_daemon)
        + &quot; current_process : &quot;
        + str(current_process)
        + &quot; is daemon : &quot;
        + str(is_daemon)
    )
    time.sleep(3)
    return result

def mp(run_n: int):
    print(f&quot;start checking multiprocessing pid task&quot;)
    print(&quot;[1] check pid using os modlue&quot;)
    print(f&quot;parent process : {os.getppid()} current process : {os.getpid()}&quot;)
    print(&quot;[2] check pid using multiprocessing modlue&quot;)
    print(
        f&quot;parent process : {multiprocessing.parent_process().pid if multiprocessing.parent_process() is not None else None} is daemon? : {multiprocessing.parent_process().daemon if multiprocessing.parent_process() is not None else None} process : {multiprocessing.current_process().pid} is daemon? : {multiprocessing.current_process().daemon}&quot;
    )

    results = []
    print(f&quot;start job&quot;)
    with concurrent.futures.ProcessPoolExecutor() as process_executor:
        for pp_res in process_executor.map(process_function, [i for i in range(run_n)]):
            results.append(pp_res)
    print(f&quot;job done&quot;)
    for c in results:
        print(c)
...

pp_task = PythonOperator(
        task_id=&quot;pp_task&quot;,
        python_callable=check_pid,
    )

mpp_job = PythonOperator(
        task_id=&quot;mpp_job&quot;,
        python_callable=mp,
        op_kwargs={
            &quot;run_n&quot;: 5,
        },
    )

pp_task &amp;gt;&amp;gt; mpp_job&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;{standard_task_runner.py:52} INFO - Started process 67070 to run task
{standard_task_runner.py:52} INFO - Job 54376: Subtask mpp_job
...
start checking multiprocessing pid task
[1] check pid using os modlue
parent process : 67069 current process : 67070

[2] check pid using multiprocessing modlue
parent process : None is daemon? : None process : 67070 is daemon? : True
...
0th task : partent_process : 67070 is daemon : False current_process : 67071 is daemon : True
1th task : partent_process : 67070 is daemon : False current_process : 67072 is daemon : True
2th task : partent_process : 67070 is daemon : False current_process : 67073 is daemon : True
3th task : partent_process : 67070 is daemon : False current_process : 67074 is daemon : True
4th task : partent_process : 67070 is daemon : False current_process : 67071 is daemon : True&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금 더 상황을 확장하면 어떨까? (여기서도 multiprocessing 에서 parent가 None인 것은 위에 기술한 이유와 같다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 먼저 눈에 보이는 차이는 pp_task 와 mpp_job은 1개의 파이썬 코드 내에서 실행되는 task이지만 별도의 job으로 분리되며 별도의 프로세스에서 동작함을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 것은 &lt;b&gt;해당 프로세스는 실제로 daemon process가 맞으나 ProcessPoolExecutor로 인해 프로세스가 생성될 때 daemon이 아닌 것으로 취급받는다.&lt;/b&gt; 현재 프로세스는 multiprocessing 기준 start() 되기 전에 daemon=True으로 설정되어 있지 않은 프로세스이기 때문에 False로 되어 있는 것으로 의심할 수 있다. &lt;span style=&quot;color: #9d9d9d;&quot;&gt;(주의. 제가 틀렸거나 잘못된 설명 일 수 있습니다.)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 관찰한 결과를 정리하면 아래와 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DAG이 실행될 때 각각의 PythonOperator 는 Task 마다 새로운 Daemon process를 생성하여 작업한다&lt;/li&gt;
&lt;li&gt;실제로 프로세스가 daemon이기 때문에 debuging 시 &lt;code&gt;AssertionError: daemonic processes are not allowed to have children&lt;/code&gt; 를 발생시킨다.&lt;/li&gt;
&lt;li&gt;&amp;nbsp;multiprocessing 사용 시 해당 process 가 start() 될 때 daemon=True를 선언하지 않았기 때문에 daemon 프로세스로 취급되지 않고 자식 프로세스를 생성하는 것으로 의심된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그럼 지금까진 왜 됐다가 이제 안되는 건데..?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의문에 대한 해답은 실제로 같은 문제를 겪어 &lt;a href=&quot;https://github.com/celery/celery/issues/1709&quot;&gt;raise 된 issue를&lt;/a&gt; 통해 해소될 수 있었다. 이 issue는 celery 사용 시 동일한 에러 현상을 겪었던 사용자의 issue로 celery contributor인 &lt;a href=&quot;https://github.com/ask&quot;&gt;Ask Solem&lt;/a&gt; 는 다음과 같이 설명한다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;I figured out what caused the change in the behaviour.
Tasks are run using daemon processes both in 3.0 and 3.1, but until celery/billiard@4c32d2e and celery/billiard@c676b94 multiprocessing module wasn't aware of that and hence was allowing creating subprocesses.

To my understanding, there was a bug prior to version 3.1 (tasks were allowed to create subprocesses, which could result in orphaned state) and now this bug has been fixed.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 재현한 대로 multiprocessing 모듈이 현재 실행 중인 프로세스가 데몬 프로세스인지 체크하지 못하고 있고 이 것이 assert 조건이 추가되면서 문제를 발생시켰다는 것이다. 즉 쉽게 말해 버그였다는 것이다. (WTF)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;문제 해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 airflow contributor 인 &lt;a href=&quot;https://github.com/potiuk&quot;&gt;Jarek Potiuk&lt;/a&gt; 은 이 문제에 대해 부정적인 의견을 표했다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;It broke because of optimisations implemented in airflow that make use of multiprocessing. The best way for you to proceed will be to turn your multiprocessing jobs into separate Airflow tasks&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생각해보면 기본적으로 각각의 task 가 서로 다른 프로세스로 움직이니까&amp;hellip; 분리하는 것이 더 좋아 보이기도 한다. 다만 이러면 구현하는 과정이 매우 까다로워(귀찮아) 지고 생각해보면 쪼갠 거 안에서도 더 쪼갤 수 있다면&amp;hellip; 이건 참을 수 없다. GPU 스레드랑 코어도 쪼개 쓰는 세상이니까!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 방법을 시도해보고 적용할 법한 방법은 다음과 같다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Airflow 환경변수 바꾸기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 간단한 방법이다. Airflow의 환경변수에 &lt;code&gt;PYTHONOPTIMIZE = 1&lt;/code&gt; 옵션을 주어 &lt;code&gt;PYTHONOPTIMIZE -O&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 되도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://docs.python.org/3/using/cmdline.html#envvar-PYTHONOPTIMIZE&quot;&gt;PYTHONOPTIMIZE&lt;/a&gt; 에 &lt;a href=&quot;https://docs.python.org/3/using/cmdline.html#cmdoption-O&quot;&gt;-O&lt;/a&gt; 옵션이 들어가면 &lt;a href=&quot;https://docs.python.org/3/library/constants.html#debug__&quot;&gt;&lt;b&gt;debug&lt;/b&gt;&lt;/a&gt; 값에서 assert statements를 제거할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이 방법을 사용하면 데몬 프로세스가 자식 프로세스를 형성하는 문제는 여전히 존재하기 때문에 추천할 방법이라고 보기는 어렵다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;virtualenv에서 실행&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PythonOperator 가 실행될 때 viretual env 에 진입하여 실행되도록 DAG을 짠다. 이 경우 가상 환경에서 코드가 동작하면서 파이썬 인터프리터는 데몬이 아닌 새 프로세스를 생성하기 때문에 문제를 해결할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;code&gt;billiard&lt;/code&gt; 패키지를 사용한다. ( 권장 사항 )&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;python2.7 버전의 multiprocessing package에서 포크 돼 만들어진 패키지로 위에서 언급한 &lt;a href=&quot;https://github.com/celery/celery/issues/1709&quot;&gt;celery issue&lt;/a&gt; 등 의 문제 해결을 위해 celery 팀에서 개발하고 관리하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 &lt;code&gt;import multiprocessing&lt;/code&gt;을&amp;nbsp;&lt;code&gt;import billiard as multiprocessing&lt;/code&gt; 으로 처리해주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;billiard&lt;/code&gt; 는 기존 &lt;code&gt;multiprocessing&lt;/code&gt; 패키지보다 더 많은 변수를 사용 가능 및 요구하는 등 더 적극적으로 커스터마이즈를 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이 방법은 concurrent.futures를 꼭 사용하겠다!라고 할 때 해결 방법으로는 적합하지 않아 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;IV. 참고자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/apache/airflow/issues/14896&quot;&gt;https://github.com/apache/airflow/issues/14896&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/celery/celery/issues/1709&quot;&gt;https://github.com/celery/celery/issues/1709&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://stackoverflow.com/questions/46188531/what-does-pythonoptimize-do-in-the-python-interpreter/57493983#57493983&quot;&gt;https://stackoverflow.com/questions/46188531/what-does-pythonoptimize-do-in-the-python-interpreter/57493983#57493983&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://stackoverflow.com/questions/74010614/why-os-getppid-and-multiprocessing-parent-process-pid-got-different-result-u/74010700?noredirect=1#comment130685644_74010700&quot;&gt;https://stackoverflow.com/questions/74010614/why-os-getppid-and-multiprocessing-parent-process-pid-got-different-result-u/74010700?noredirect=1#comment130685644_74010700&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pypi.org/project/billiard/&quot;&gt;https://pypi.org/project/billiard/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/3/using/cmdline.html&quot;&gt;https://docs.python.org/3/using/cmdline.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Process.daemon&quot;&gt;https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Process.daemon&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/python/cpython/blob/main/Lib/multiprocessing/process.py&quot;&gt;https://github.com/python/cpython/blob/main/Lib/multiprocessing/process.py&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Data/Data Engineering</category>
      <category>Airflow</category>
      <category>AssertionError</category>
      <category>AssertionError: daemonic processes are not allowed to have children</category>
      <category>billiard</category>
      <category>celery</category>
      <category>Composer</category>
      <category>daemon process</category>
      <category>multiprocessing</category>
      <category>Python</category>
      <category>PYTHONOPTIMIZE</category>
      <author>London_</author>
      <guid isPermaLink="true">https://leo-bb.tistory.com/87</guid>
      <comments>https://leo-bb.tistory.com/87#entry87comment</comments>
      <pubDate>Mon, 10 Oct 2022 23:37:17 +0900</pubDate>
    </item>
    <item>
      <title>[RL] 마로코프 의사 결정 과정</title>
      <link>https://leo-bb.tistory.com/86</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;강화 학습이란&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인공지능 학습의 방법론인 머신러닝(ML)의 한 계통으로 일반적인 지도 학습과 비지도 학습과는 다른 계통의 학문으로 DP(dymamic programming), MDP(markov decison process)와 같은 개념에 뿌리를 두고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;일반적으로 AI 의 발전은 단순한 계산(computing) 이 아니라 판단(estimation), 의사결정(decison), 창작(creation)을 기계가 행하도록 기대하는 행위인데, 강화 학습은 특히 의사결정(decison)에 집중한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지도 학습
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터와 레이블의 쌍이 주어지면 기계가 새로운 데이터에 레이블을 붙이는 방법을 학습하는 것&lt;/li&gt;
&lt;li&gt;즉 문제와 정답을 제공하고, 새로운 문제가 등장하면 &quot;판단&quot;하게 만들고자 함&lt;/li&gt;
&lt;li&gt;대부분의 회귀모델 CNN과 같은 방법이 여기 속함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;비지도 학습
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;레이블 없이 데이터만 주어졌을 때 이를 분류하거나 밀도를 추정하도록 학습하는 것&lt;/li&gt;
&lt;li&gt;즉 데이터의 특징을 분류하고 요약하여 새로운 가치를 생산하도록 기대함&lt;/li&gt;
&lt;li&gt;PCA, 임베딩, 클러스터링 알고리즘 등이 여기 속함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;강화 학습
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;행동에 대한 보상을 주고 어떠한 상태에서의 행동을 결정하도록 학습하여 최선의 행동을 결정하도록 학습하는 것&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;동적 계획법(DP)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;큰 문제를 작은 문제로 세분화하여 문제를 해결하는 방법론&lt;/li&gt;
&lt;li&gt;세분화된 작은 문제가 &quot;반복적으로 발생&quot; 하며 그에 대한 &quot;답이 항상 같아야 함&quot;&lt;/li&gt;
&lt;li&gt;즉 세분화된 문제를 1회만 해결하여 답을 기억하고 문제 해결 과정에서 세분화된 문제가 나타날 때는 이미 저장해둔 답을 사용하여 효율을 높이는 방식&lt;/li&gt;
&lt;li&gt;CS 측면에서 메모리와 연산 효율 증가에 큰 도움이 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마르코프 결정 과정(MDP)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;의사결정자(agent)의 의사결정을 아래 5가지의 인자를 통해 모델링한 방법
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 상태(Status)&lt;/li&gt;
&lt;li&gt;각 상태에서 취할 수 있는 행동(Action)&lt;/li&gt;
&lt;li&gt;각 상태에서 취한 행동으로 인해 다음 상태로 넘어갈 확률(Probability)&lt;/li&gt;
&lt;li&gt;다른 상태로 전이되면서 얻게 되는 보상의 기댓값(Reward)&lt;/li&gt;
&lt;li&gt;현재 얻는 보상과 미래에 얻을 보상의 관계에 따른 할인율(discount factor)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;마르코프 결정 과정 풀이를 위해서는 다음 상태로 전이될 확률은 오로지 현재 상태만이 영향을 미친다(즉 이전의 모든 상태와 독립적이다)는 마르코프 특성을 만족해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;데이트 시나리오.png&quot; data-origin-width=&quot;1066&quot; data-origin-height=&quot;489&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/djcbzf/btrLLx57bYV/JB0lO9rquENYvUT6VEMqtk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/djcbzf/btrLLx57bYV/JB0lO9rquENYvUT6VEMqtk/img.png&quot; data-alt=&quot;사진 1. 마르코프 결정 과정으로 모델링 된 데이트 계획 ( by author )&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/djcbzf/btrLLx57bYV/JB0lO9rquENYvUT6VEMqtk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdjcbzf%2FbtrLLx57bYV%2FJB0lO9rquENYvUT6VEMqtk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1066&quot; height=&quot;489&quot; data-filename=&quot;데이트 시나리오.png&quot; data-origin-width=&quot;1066&quot; data-origin-height=&quot;489&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;사진 1. 마르코프 결정 과정으로 모델링 된 데이트 계획 ( by author )&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;모델링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마르코프 결정 과정을 이해하기 위해 분석가 A 씨가 좋아하는 이성 B에게 고백하기 위해 흔하디 흔한 데이트 코스를 짜고 있는 모습을 관찰하며 분석가 A의 고백 계획을 시뮬레이션해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 행동의 주체인 분석가 A 씨를 에이전트(agent)라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최초에 에이전트는 본인 집에 있다.($S_1$) 이 상태에서는 에이전트가 데이트 신청을 하는 행동($a_{1-1}$)과 데이트 신청을 포기하는 행동($a_{1-2}$)을 갖는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이트 신청을 하는 행동($a_{1-1}$)을 취했을 때 에이전트는 영화를 보러 가는 상태($S_2$), 저녁을 함께하는 상태($S_4$), 카페에서 얘기를 나누는 상태($S_3$) 를 선택할 수 있다. 이때 각 상태로 전이되는 확률을 $P(S_1, S_2)$, $P(S_1, S_4)$, $P(S_1, S_3)$ 로 표현할 수 있고 확률의 합은 1이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 선택 과정을 거쳐 에이전트가 고백을 하여 성공한 상태($S_6$) 의 보상 $R(S_6) = 1$이고, 실패한 상태($S_7$) 의 보상은 $R(S_7) = -1$ 이 된다. 고백과 무관한 다른 상태의 보상은 0으로 표현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현실에서는 데이트가 실패했다고 과거로 시간을 돌릴 수 없기 때문에 에이전트는 고백에 성공할 수 있는 데이트 코스(보상이 최대가 되는 최적의 행동 순서)를 찾기 위해 데이트 과정을 반복해서 상상한다. 이 상상 한 번이 곧 한 번의 에피소드 수행이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최적의 행동 순서를 갖는 에피소드를 찾는 과정에서 보상의 총합이 발산하는 경우가 있을 수 있다. 때문에 시간에 따른 할인 개념($\gamma$)이 필요하다. $\gamma$ 는 0~1 사이의 값을 채택하며 1에 가까울수록 미래 보상을 더 중요하게 평가한다고 볼 수 있다. (보통 0.9 이상으로 설정한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이득(return)과 가치(value)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제를 마르코프 결정 과정의 기본 요소로 모델링하게 되면 주어진 상태에서 미래에 얻을 수 있는 보상의 총합. 즉 이익을 계산할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이익은 모든 행동이 일어났다고 생각하고 현재에서부터 과거로 돌아가며 보상을 더하는 것과 같기 때문에 수식으로 표현하면 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$G_n = \sum_{k=0}^\inf \gamma^k R(S_n+k+1)&lt;br /&gt;$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이익의 개념을 확장해서 특정 상태에서 지나갈 수 있는 전체 경로의 평균과 이익의 기댓값을 구할 수 있다면 그 값을 특정 상태에서 미래에 얻을 수 있는 보상의 총합이라고 할 수 있다. 이를 가치 함수라 하며 아래와 같이 표현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;$$V(s) = E[G_n|S_n=s]&lt;br /&gt;$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 분석가 A 씨 고백 계획으로 돌아가 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 상태인 포기($S_5$), 성공($S_6$), 실패($S_7$) 의 가치는 항상 해당 상태의 보상과 같다. 만약 카페($S_3$) 에서 고백하는 행동($a_{3-2}$)이 일어날 때 가치를 구한다면 $\gamma * (1 * P(S_3,S_6) + (-1) * P(S_3,S_7))$ 이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 현재 상태의 가치를 계산하기 위해서는 반드시 다음 상태의 가치를 알아야 한다. 때문에 어떤 상태의 가치를 알기 위해서는 최종 상태까지 계산을 반복해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것을 수식화 하면 아래와 같이 표현할 수 있으며 벨만 방정식이라 부르며, 각 상태의 가치를 알기 위해서 모든 상태 S에 대해 $Vn(s) - Vn-1(s) = 0$ 이 될 때까지 반복하여 가치 함수를 산출하는 작업을 가치 반복법이라 한다.&lt;br /&gt;$$V(s) = R(s) + V(s^)&lt;br /&gt;$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제가 복잡해질수록 가치 함수의 갱신이 빈번하기 때문에 갱신 값의 차이가 일정 임계치 안에 들어오면 반복을 종료하도록 구현하기도 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;정책(policy)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 상태의 가치를 모두 계산하고 나면 비로소 미래 보상을 최대화하는 행동을 선택할 수 있게 된다. 이는 곧 상태를 입력받으면 최적의 행동을 출력하는 함수로 볼 수 있으며 이 것을 정책($\pi$)이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;강화 학습의 목표는 기계가 각 상황에 대해 최선의 선택을 하도록 하는 것이므로 정책의 성능을 극대화하는 것이 곧 강화 학습의 목표라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가치 반복법에 의해 수렴된 최적의 가치 함수 $V\pi(s)$ 를 알고 있을 때 최적의 정책 함수를 찾는 문제는 현 상태를 입력받아 다음 상태의 가치 함수가 극대화되는 행동을 찾는 문제로 단순화된다.&lt;br /&gt;$$ \pi(s) = \gamma * \sum_{s^{\prime}} p(s,a) * V\pi(s^{\prime})&lt;br /&gt;$$&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최적의 정책 함수를 찾기 위해 가치 함수를 계속 업데이트하는 것($V\pi(s)$를 찾는 것) 이 아니라 정책 함수 자체를 업데이트하는 것 도 방법이 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정책 $\pi(s)$ 가 주어진다면 에이전트가 어떤 상태에 어떤 행동을 하는지 정해져 있다는 것이므로 정책이 쓰일 때마다 모든 상태의 가치 함수를 계산할 필요 없이 $\pi(s)$ 에 의해 발생한 이익의 기댓값만 계산해도 된다. 이렇게 정책 함수에 의해 결정된 가치 함수를 구하는 것을 정책 평가(evaluation) 라 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정책을 평가하면 $\pi(s)$ 를 따랐을 때 와 따르지 않았을 때의 가치의 기댓값을 비교할 수 있는데 이를 통해 가치가 더 큰 방향으로 정책을 수정하는 것을 정책 개선(improvement)라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정책은 최초에 무작위로 초기화되고 정책의 평가 - 개선이 반복되면서 점차 강화된다. 이렇게 평가와 개선을 무한히 반복하다 보면 가치 함수와 정책 함수 모두가 변화하지 않는 시점. 즉 수렴하여 정책 함수가 최적화된 시점이 오는데 이러한 작업을 정책 반복법이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정책 반복법은 가치 반복법에 비해 알고리즘도 복잡하고 반복문 하나가 더 추가되기 때문에 수행 과정도 더 길다. (가치 반복법은 정책 반복법 중 정책 평가 과정에서 최댓값을 고르는 과정으로 볼 수 있다.) 단 언제나 가치 반복법이 최적의 정책 함수를 찾는 더 빠른 방법이라고 할 수는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1) Do-it 강화 학습 입문&lt;br /&gt;2) 위키피디아&lt;/blockquote&gt;</description>
      <category>Data/ML</category>
      <category>DP</category>
      <category>dynamic programming</category>
      <category>markov decison process</category>
      <category>Reinforcement Learning</category>
      <category>RL</category>
      <category>가치반복법</category>
      <category>강화학습</category>
      <category>마르코프의사결정</category>
      <category>정책</category>
      <category>정책반복법</category>
      <author>London_</author>
      <guid isPermaLink="true">https://leo-bb.tistory.com/86</guid>
      <comments>https://leo-bb.tistory.com/86#entry86comment</comments>
      <pubDate>Fri, 9 Sep 2022 22:27:36 +0900</pubDate>
    </item>
    <item>
      <title>Snowflake 가 준비하고 있는 새로운 패러다임. Unistore</title>
      <link>https://leo-bb.tistory.com/85</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span class=&quot;imageblock&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/R1uaQ/btrLqdr9IK4/KfDhxhfrc8uEjr7OMPnHO1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/R1uaQ/btrLqdr9IK4/KfDhxhfrc8uEjr7OMPnHO1/img.png&quot; data-alt=&quot;1.3&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/R1uaQ/btrLqdr9IK4/KfDhxhfrc8uEjr7OMPnHO1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FR1uaQ%2FbtrLqdr9IK4%2FKfDhxhfrc8uEjr7OMPnHO1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;금번 snowflake에서 주관한 &lt;a href=&quot;https://www.snowflake.com/?lang=ko&amp;amp;utm_source=google&amp;amp;utm_medium=paidsearch&amp;amp;utm_campaign=ap-kr-ko-brand-core-phrase&amp;amp;utm_content=go-eta-evg-ss-free-trial&amp;amp;utm_term=c-g-snowflake-p&amp;amp;_bt=579103397440&amp;amp;_bk=snowflake&amp;amp;_bm=p&amp;amp;_bn=g&amp;amp;_bg=128328470543&amp;amp;gclsrc=aw.ds&amp;amp;gclid=Cj0KCQjw08aYBhDlARIsAA_gb0ckzV8ObT8vW9bOWILjJyaRmV3t3UlmFXvycy95kIlxXULXkVR70TEaAsLIEALw_wcB&quot;&gt;Data cloud world tour&lt;/a&gt;에 참가했다가 인상 깊은 세션을 보게 되어 해당 세션 내용을 기록한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;unistore는 snowflake와 adobe 가 함께 개발 중인 idea로 작성하는 현재는 priviate test 가 진행 중이며 한국에는 2023년 상반기 beta , GA는 2023년 후반기에 예정되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재까지 일궈낸 성과는 아래와 같다고 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;분석 워크로드에 대한 고성능을 유지&lt;/li&gt;
&lt;li&gt;높은 동시성(100 ~ 1000 QPS)과 응답 시간(50~100ms)&lt;/li&gt;
&lt;li&gt;GDPR 규정 준수 지원&lt;/li&gt;
&lt;li&gt;시스템의 확장성과 손쉬운 관리 및 사용기능 제공&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;현세대 데이터 베이스 운영 전략과 문제&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;대부분의 회사는 자사 서비스와 데이터의 특징과 환경에 맞게 cloud 환경을 사용하거나 On-premise 환경을 구축하지만 OLTP를 위한 database (이하 Transactional DB)와 OLAP를 위한 database (이하 Analytical DB)를 따로 운영하고 있다는 공통점이 있다. OLTP와 OLAP에 대한 개념을 간단히 정리하면 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OLTP
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OnLine Transactional Processing&lt;/li&gt;
&lt;li&gt;데이터가 &amp;ldquo;건&amp;rdquo; 단위로 들어오는 것으로 이해하면 쉬움&lt;/li&gt;
&lt;li&gt;key값을 기준으로 millisecond 단위로 database에 (특히) Insert, Update, Delete 작업이 발생&lt;/li&gt;
&lt;li&gt;데이터의 무결성 보장이 굉장히 중요함&lt;/li&gt;
&lt;li&gt;Row database 가 유리함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;OLAP
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OnLine Analytical Processing&lt;/li&gt;
&lt;li&gt;대용량 데이터를 한 번에 처리하는 것을 목표로 하며 굉장히 복잡하고 많은 집계가 포함되는 Select 작업 처리가 중요함&lt;/li&gt;
&lt;li&gt;데이터의 처리보다 조회(분석)가 중요하기 때문에 처리 속도의 중요성이 상대적으로 떨어짐&lt;/li&gt;
&lt;li&gt;Column database 가 유리함&lt;/li&gt;
&lt;li&gt;일반적으로 OLAP는 Transactional DB의 데이터를 batch job으로 ETL 한 DW(Analytical DB)에서 이뤄지기 때문에 기본적으로 무결하다는 것을 전제하고 진행됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 두 데이터 처리방식의 특징과 목적이 명확히 다르기 때문에 그에 맞는 DB를 선택하여 운용하게 되는 것인데, Transactional DB와 Analytical DB를 별도로 운영하면서 생기는 문제는 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시스템 운영 및 관리의 어려움
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;운영 관리 영역이 분리되며 자연스럽게 데이터 사일로가 생성되는 문제가 있음&lt;/li&gt;
&lt;li&gt;일관된 보안/정책 관리 유지가 어렵고 거버넌스 관리가 어려워짐&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;대용량 데이터 통신의 지연 및 정합성 이슈
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서로 다른 시스템 간의 데이터 이동에서 필연적으로 발생되는 복잡도의 증가&lt;/li&gt;
&lt;li&gt;양 데이터베이스 간의 최신화 격차(이로 인해 실시간 분석이 불가능하다)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;신규 기능 개발의 복잡도 증가
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;사업 운영에 따른 새로운 기능/서비스/데이터의 추가로 인해 시스템은 갈수록 복잡해지며 이로 인해 신규 기능의 개발과 데이터 활용 난이도가 점점 증가함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Snowflake의 제안 : Unistore&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Unistore 소개&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;snowflake1.drawio (1).png&quot; data-origin-width=&quot;485&quot; data-origin-height=&quot;131&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cynlBj/btrLqedwHGN/wHWszT0WttXMN01INCriT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cynlBj/btrLqedwHGN/wHWszT0WttXMN01INCriT0/img.png&quot; data-alt=&quot;사진 2. unistore 기본 개념 ( by author )&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cynlBj/btrLqedwHGN/wHWszT0WttXMN01INCriT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcynlBj%2FbtrLqedwHGN%2FwHWszT0WttXMN01INCriT0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;485&quot; height=&quot;131&quot; data-filename=&quot;snowflake1.drawio (1).png&quot; data-origin-width=&quot;485&quot; data-origin-height=&quot;131&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;사진 2. unistore 기본 개념 ( by author )&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&amp;nbsp;Transactional Data와 Analytical Data를 하나의 단일 플랫폼(데이터베이스)에서 &lt;br /&gt;함께 사용하고자 하는 가장 현대적인 방식&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 말은 굉장히 쉬운데 이런 환경이 등장하지 않았던 것은 앞서 언급했듯 OLTP를 위한 환경과 OLAP를 위한 환경의 특성이 너무나도 상이해서 쉽게 섞일 수 없기 때문이다. snowflake에서는 unistore를 통해 이를 해결하고자 하며 아래와 같은 이점을 취할 수 있다고 소개한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Transactional Workloads를 직접 연결하여 엔터프라이즈 앱을 개발하고 데이터를 수집 및 활용 가능하게 함&lt;/li&gt;
&lt;li&gt;Transactional data를 즉시 분석할 수 있도록 하여 &amp;ldquo;실시간 분석&amp;rdquo;이 가능하도록 함&lt;/li&gt;
&lt;li&gt;시스템 전반의 아키텍처를 극히 단순화할 수 있으며 데이터의 이동 흐름을 제거하기 때문에 확장성과 거버넌스 관리 등 많은 부분에서 이점을 가져갈 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;snowflake 가 이러한 기술 구현의 근거로 드는 것은 자사의 Optimizer이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 SQL 수행 명령(Execution order)을 내리면 DB는 입력된 SQL을 가장 빠르고 효율적으로 수행할 최적(최저비용)의 처리경로(Execution Plan)를 생성하는데, 이러한 역할을 수행하는 DBMS 내부의 핵심 엔진을 Optimizer 라 부른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Optimizer는 크게 Rule based , Cost based가 있고, 최근에는 Self learning의 방식이 포함된 DBMS가 많다. 평가 요소는 여러 액세스 방법(full scan or index scan), 다양한 조인 방법(hash join, loop join etc)과 조인 순서 및 가능한 변환 등을 검사하여 평가한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Rule based
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;평가요소를 통해 이미 정해진 규칙을 따라 plan을 선정함&lt;/li&gt;
&lt;li&gt;정해진 규칙을 따르기 때문에 Heuristic optimizer라고도 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Cost based
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 계획과 실행 단계에 점수를 부여하여 최종합이 가장 낮은 plan을 선정함&lt;/li&gt;
&lt;li&gt;대부분의 DBMS 가 이러한 방법을 사용하고 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Self learning
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;앞선 모든 방법은 결국 &amp;ldquo;예상되는 비용&amp;rdquo;이 기반인데, 실제 기록과 예상 기록의 차이를 기반으로 optimizer 스스로 지속적으로 보정해 성능을 강화하는 방식&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 알려져 있듯이 snowflake는 Column database를 제공하고 있다. 실제 데이터는 OLTP 과정을 거치며&amp;nbsp; row 단위로 저장하지만 이를 background에서 변환하여 사용자에게는 column DB 형태로 제공하고 있다고 한다. (쿼리를 날리는 유저 입장에서는 row 단위로 저장된 DB가 background 가 된다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;unistore 개념이 적용된다면 snowflake 는 요청된 SQL을 분석하여 optimizer 가 어떤 작업인지 판단하고, 어떤 처리 방식이 효과적이기 때문에 어디서 데이터를 조회하고 처리해야 최적의 결과를 얻을 수 있는지 선정할 수 있다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, OLTP 성 요청이라면 row base로 저장된 state에, OLAP성 요청이라면 column base로 저장된 state에 요청한다는 것!&amp;nbsp; 물론 이 모든 것은 snowflake의 시스템 안에서 수행되기 때문에 사용자는 알아서 잘 수행된 쿼리의 결과만 확인하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Hybrid table&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Unistore를 구현하기 위한 snowflake에서 제안하는 새로운 형태의 테이블&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;Create HYBRID TABLE CustomerTable (
    customer_id int primary key,
    name varchar(256),
    ...
)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능을 구현하는데 필요한 구문이 기존 DDL, DML과 동일하다는 것이 굉장히 인상적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 Hybrid table의 핵심 기능은 아래 5가지 항목이 있다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PK 존재
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각 테이블은 Primary key를 갖기 때문에 기존 PK를 통해 table을 관리 및 검증하던 방식을 그대로 가져갈 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;FK 존재
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;테이블 간의 관계성을 지속적으로 유지하여 잘못된 수정을 방지할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;참조 무결성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데이터 적재 전 데이터의 유효성을 검증함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Secondary indexes의 존재
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;성능 향상을 위해 PK 이외의 index를 활용할 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Join free
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다른 Hybrid table 뿐 아니라 기존에 snowflake에서 제공하는 DW table 과의 Join 도 지원함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;감상&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;optimizer 가 sql 요청을 보고 OLTP 작업인지 OLAP 작업인지 확인하여 데이터를 관리(정확히는 관리하는 것은 아니지만 어디서 데이터를 꺼내올지, 업데이트할 지 결정한다는 측면에서) 한다는 것이 가능한게 놀라웠다. 다만 처음엔 완전히 새로운 개념과 구조의 DB 를 상상했는데 실제로는 두개의 DB를 같이 두고 선택적으로 골라 쓰는 느낌에 가까워 보여서 조금은 아쉽다. 물론 서버리스로 제공되니 사용자 입장에서는 두 기능을 모두 잘 소화하는 새로운 개념의 DB인 것은 맞는 것 같고 이러한 아이디어를 실제로 구현해냈다는 것 자체가 참 대단한 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 적용하게 된다면 데이터 분석가/과학자 입장에서는 기존 환경의 변화의 체감 없이 실시간 데이터 분석과 모델의 개발 및 서빙이 가능할 것이며 이를 통해 현재보다 다양한 가치의 창출이 가능할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;엔지니어 입장에서는 시스템 아키텍처를 획기적으로 줄여 운영 관리를 더욱 효율적으로 할 수 있어 긍정적이다. (물론 초기 아키텍쳐 재구축 작업은 다소 불편하겠지만) unistore 개념 자체가 기존과 완전히 다른 방법으로 제어하는 것이 아니기 때문에 러닝 커브가 크지는 않을 것 같다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blpWkH/btrLiiPgog6/RU5VKHDKvVprBSf67TZ8Qk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blpWkH/btrLiiPgog6/RU5VKHDKvVprBSf67TZ8Qk/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;1292&quot; data-origin-height=&quot;517&quot; data-filename=&quot;arci.drawio.png&quot; style=&quot;width: 46.6061%; margin-right: 10px;&quot; data-widthpercent=&quot;47.15&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blpWkH/btrLiiPgog6/RU5VKHDKvVprBSf67TZ8Qk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblpWkH%2FbtrLiiPgog6%2FRU5VKHDKvVprBSf67TZ8Qk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1292&quot; height=&quot;517&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pVbsj/btrLhQyQVT3/Ow52Br6r2eWm7ALV78lCK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pVbsj/btrLhQyQVT3/Ow52Br6r2eWm7ALV78lCK0/img.png&quot; data-is-animation=&quot;false&quot; data-origin-width=&quot;871&quot; data-origin-height=&quot;311&quot; data-filename=&quot;arci.drawio (1).png&quot; style=&quot;width: 52.2311%;&quot; data-widthpercent=&quot;52.85&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pVbsj/btrLhQyQVT3/Ow52Br6r2eWm7ALV78lCK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpVbsj%2FbtrLhQyQVT3%2FOw52Br6r2eWm7ALV78lCK0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;871&quot; height=&quot;311&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;사진 3. Unistore( hybrid table ) 을 적용함으로 기대되는 아키텍쳐 변화의 예시. 왼쪽을 오른쪽처럼 바꿀 수 있을 것으로 기대한다. ( by author )&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 개발 중이기 때문에 속단하기는 이르고 직접 trial을 사용해봐야 확실히 체감할 수 있을 것 같다. 다만 public trial이 공개되려면 여전히 멀었기 때문에 그때는 이번 발표에서 들었던 것보다 훨씬 더 발전 및 보완하여 등장할 것은 확실해 보인다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;포스팅을 통해 Unistore에 관심이 생긴 분은&amp;nbsp;&lt;a href=&quot;https://www.snowflake.com/en/data-cloud/workloads/unistore/&quot;&gt;공식 introduction site의&lt;/a&gt; 내용도 참고하는 것을 추천한다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Cloud&amp;amp;Tools</category>
      <category>data</category>
      <category>Database</category>
      <category>db</category>
      <category>Hybrid table</category>
      <category>OLAP</category>
      <category>oltp</category>
      <category>optimizer</category>
      <category>snowflake</category>
      <category>unistore</category>
      <author>London_</author>
      <guid isPermaLink="true">https://leo-bb.tistory.com/85</guid>
      <comments>https://leo-bb.tistory.com/85#entry85comment</comments>
      <pubDate>Sun, 4 Sep 2022 18:15:42 +0900</pubDate>
    </item>
    <item>
      <title>[Airflow] 자주 쓰는 Branch Task</title>
      <link>https://leo-bb.tistory.com/84</link>
      <description>&lt;h1&gt;I. 개요&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일련의 작업 진행 시 상황에 따라 다른 작업으로 이어져야 하는 경우는 굉장히 빈번하게 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Airflow 는 기본적으로 DAG 으로 작업을 구조화해서 작업을 진행하기 때문에, 자동화할 때 이러한 조건부 작업을 구현하지 못한다면 매번 실패 후 재처리하는 작업이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적이지만 자주 사용되는 Branch task 인 BranchPythonOperator 와 BranchSQLOperator 의 사용법과 예제를 기록해둔다.&lt;/p&gt;
&lt;h1&gt;II. Branch Task&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. BranchPythonOperator&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PythonOperator 기반으로 구성되어 task_id(s) 를 output 으로 하는 Python callable 을 통해 바로 다음에 이어지는 작업 요소를 결정한다.&lt;/p&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;BranchPythonOperator(
    python_callable : method, 
    op_args : dict, 
    op_kwargs : dict,
    templates_dict : dict [optional],
    templates_exts : list [optional]
)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;환경에 따라 다른 모델의 학습을 실행시킨다고 가정할 때 아래와 같이 작성할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실행시키는 task 는 Dummy task 로 대체&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;
from airflow.models import DAG
from airflow.operators.dummy_operator import DummyOperator
from airflow.operators.python_operator import BranchPythonOperator
from utils.util import get_airflow_env # 현재 동작중인 airflow 환경을 가져오는 custom function 
from datetime import datetime

def branch_callable(env):
    if env == &quot;production&quot; :
        return &quot;production_task&quot;
    else :
        return &quot;staging_task&quot;

env = get_airflow_env()

with DAG(
    dag_id='example_BranchPythonOperator',
    start_date=datetime(2022, 7, 15),
    schedule_interval= '* * * * *'
    ) as dag:

    branch_task = BranchPythonOperator(
        task_id = &quot;env_branching&quot;,
        python_callable = branch_callable,
        op_kwargs = {&quot;env&quot; : env}
        )

    production_task = DummyOperator(
            task_id = &quot;production_task&quot;
    )

    staging_task = DummyOperator(
            task_id = &quot;staging_task&quot;
    )

    branch_task &amp;gt;&amp;gt; [production_task, staging_task]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (2).png&quot; data-origin-width=&quot;287&quot; data-origin-height=&quot;114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cXkLfu/btrHNFgRM8P/MyWq32DPyjWl6dvZUpAkA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cXkLfu/btrHNFgRM8P/MyWq32DPyjWl6dvZUpAkA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cXkLfu/btrHNFgRM8P/MyWq32DPyjWl6dvZUpAkA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcXkLfu%2FbtrHNFgRM8P%2FMyWq32DPyjWl6dvZUpAkA0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;340&quot; height=&quot;135&quot; data-filename=&quot;Untitled (2).png&quot; data-origin-width=&quot;287&quot; data-origin-height=&quot;114&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. BranchSQLOperator&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ETL/ELT 작업중에 무결성 검증을 진행하거나 DB에 저장된 데이터에 따라 다른 작업이 진행돼야 하는 등 SQL 의 결과를 이용해 분기 작업이 필요할 수 있다. 이럴 때 BaseSQLOperator 기반의 BranchSQLOperator를 사용할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;BranchPythonOperator 와 다르게 결과에 따라 어떤 task 를 실행시킬지 &amp;ldquo;명시적으로 선언한다&amp;rdquo;&lt;/li&gt;
&lt;li&gt;실행 결과는 반드시 Boolean (True/False), integer (0 = False, Otherwise = 1) , string (true/y/yes/1/on/false/n/no/0/off) 이어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;BranchSQLOperator(
    sql : str or templete ends with .sql ,  
    follow_task_ids_if_true : str, 
    follow_task_ids_if_false : str,
    conn_id : str,
    database : str,
    parameters : mapping/iter [optional]
)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DW 에 저장된 데이터를 기반으로 새로운 table 을 만드는 태스크가 있다고 할 때 아래와 같이 작업할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import os 
from datetime import datetime
from dependencies import utils
from airflow import models
from airflow.contrib.operators.bigquery_operator import BigQueryOperator
from airflow.providers.google.cloud.sensors.bigquery import BigQueryTableExistenceSensor
from airflow.providers.google.cloud.operators.bigquery import BigQueryValueCheckOperator
from airflow.operators.sql import BranchSQLOperator
from airflow.operators.python_operator import PythonOperator

yesterday = &quot;{{ macros.ds_add(ds, -0) }}&quot;
yesterday_suffix = &quot;{{ macros.ds_format(macros.ds_add(ds, -0), '%Y-%m-%d', '%Y%m%d') }}&quot;

with models.DAG(
        dag_id='sql_branch_example',
        description='sql_branch_example',
        schedule_interval='0 1 * * *',
        start_date=datetime(2022, 7, 15),
    ) as dag:

        # 원천 테이블의 존재를 확인한다
    check_origin_table_update = BigQueryTableExistenceSensor(
        task_id=&quot;check_origin_table_update&quot;, 
        project_id={project_id}, 
        dataset_id={dataset}, 
        table_id={table_name},
        gcp_conn_id={your_conn_key},     
    )

        # 원천 테이블 테이블 수준 정합성을 확인한다.
    check_table_level_quality_of_origin_table = BigQueryValueCheckOperator(
        task_id=&quot;check_table_level_quality_of_origin_table&quot;,
        sql=f&quot;SELECT COUNT(DISTINCT type) FROM `project_id.dataset_id.table_name` WHERE date_kr = {yesterday}&quot;,
        pass_value=1,
        use_legacy_sql=False,
    )

        # ELT 테이블이 이미 생성되어 있다면 task 를 종료하기 위한 branch
    check_target_table_exsits = BranchSQLOperator(
        task_id = &quot;check_target_table_exsits&quot;,
        conn_id = {your_conn_key},
        sql = f'SELECT IF(COUNT(1) &amp;gt; 0, True, False) FROM `project_id.dataset_id.target_table_name` WHERE date_kr = &quot;{yesterday}&quot;',
        follow_task_ids_if_true = &quot;pass_update_target_table&quot;,
        follow_task_ids_if_false = &quot;update_target_table_task&quot;,
        parameters={&quot;use_legacy_sql&quot;:False} #optional
    )

    pass_update_target_table = PythonOperator(
        task_id='pass_update_target_table',
        python_callable = utils.pass_update_table_data_callable,
    )

    update_target_table_task = BigQueryOperator(
        task_id=f&quot;update_target_table_task&quot;,
        sql=utils.read_sql(os.path.join(os.environ['DAGS_FOLDER'], 'sql/example.sql')).format(execute_date=yesterday_suffix),
        bigquery_conn_id={your_conn_key},
        use_legacy_sql=False,
        destination_dataset_table=f'project_id.dataset_id.target_table_name${yesterday_suffix}',
        write_disposition='WRITE_TRUNCATE',
        time_partitioning={'type': 'DAY', 'field': 'date_kr'},
    )

    check_origin_table_update &amp;gt;&amp;gt; check_table_level_quality_of_origin_table &amp;gt;&amp;gt; check_target_table_exsits &amp;gt;&amp;gt; [update_target_table_task, pass_update_target_table]&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Untitled (3).png&quot; data-origin-width=&quot;892&quot; data-origin-height=&quot;105&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7v6hW/btrHNFgRQxV/oZZkkZVmtiqIaryO4IPJNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7v6hW/btrHNFgRQxV/oZZkkZVmtiqIaryO4IPJNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7v6hW/btrHNFgRQxV/oZZkkZVmtiqIaryO4IPJNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7v6hW%2FbtrHNFgRQxV%2FoZZkkZVmtiqIaryO4IPJNk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;892&quot; height=&quot;105&quot; data-filename=&quot;Untitled (3).png&quot; data-origin-width=&quot;892&quot; data-origin-height=&quot;105&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;BigQueryTableExistenceSensor 는 task 실행 시 1분 주기로 target table 의 존재를 확인한다. 이때 별도의 escape 처리를 하지 않으면 존재가 확인되기 전까지 계속 실행상태를 유지한다.&lt;/li&gt;
&lt;li&gt;BigQueryValueCheckOperator 는 실행된 sql 의 결과가 pass_value 와 일치하는지 확인한다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Data/Data Engineering</category>
      <category>Airflow</category>
      <category>airflow branch</category>
      <category>bigquery</category>
      <category>BigQueryTableExistenceSensor</category>
      <category>BigQueryValueCheckOperator</category>
      <category>Branch</category>
      <category>BranchPythonOperator</category>
      <category>BranchSQLOperator</category>
      <category>Composer</category>
      <author>London_</author>
      <guid isPermaLink="true">https://leo-bb.tistory.com/84</guid>
      <comments>https://leo-bb.tistory.com/84#entry84comment</comments>
      <pubDate>Thu, 21 Jul 2022 13:05:31 +0900</pubDate>
    </item>
    <item>
      <title>우리는 PK 에 왜 int 형 데이터를 고집할까</title>
      <link>https://leo-bb.tistory.com/83</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;우리는 PK 에 왜 int 형 데이터를 고집할까&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근에는 분석용 DB는 Bigquery 나 Snowflake, Redshift 등을 많이 쓰는 것으로 보이나 운영용 DB(원천 DB)는 여전히 Aurora, RDS 또는 자체 서버를 구축하여 사용하는 경우가 많다고 생각한다. 데이터 인프라나 DB 관련 얘기를 나누다 보면 &amp;ldquo;Server 에서 생성하는 unique ID(nchar) 값을 PK로 잡아 저장하는 게 데이터 분석가 입장에선 훨씬 편할 텐데 굳이 운영 DB에 int type id(idx) 컬럼을 만들어 PK를 따로 잡아야 할까요?&amp;rdquo;라는 질문을 받곤 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 주제가 나올 때마다 int 형을 추천했지만 정작 완벽하게 이유를 설명하지는 못했던 것 같다. 차후에 이러한 대화가 다시 생길 땐 적어도 더 논리적으로 설명 할 수 있도록 갖고 있는 지식 + 검색한 내용을 바탕으로 내용을 정리하고자 포스팅을 작성한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;본 글은 기본적으로 MySQL 을 베이스로 내용이 전개됩니다.&lt;/li&gt;
&lt;li&gt;잘 모르기에 더 잘 알고자 기록하는 글로 일부 잘못된 정보가 포함될 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기본키(Primary Key)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Primary Key(이하 PK) 는 해당 테이블 내에서 각 행을 구별하는 가장 기본적인 값이 된다. 때문에 PK는 1개 이상 column의 조합으로 구성할 수 있지만 RDB 특성상 테이블 하나에 PK 가 다수일 수는 없다.PK 는 기본적으로 해당 테이블 내에서 절대적으로 Unique 하며, Null 값을 가질 수 없다. 이때 PK로 지정할 수 있는 column 또는 column의 조합을 candidate key라고 부른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가령 회원 정보 table이라면 회원 ID, 핸드폰 번호, 주민등록번호, 가입일, . 이 존재할 텐데 이때 candidate key는 회원 ID 와 주민등록번호를 꼽을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PK로 지정된 컬럼은 자동으로 B-tree Index 로 설정되게 되어 있고, 이 때문에 PK가 있는 테이블과 없는 테이블 조회에서 성능 차이를 보인다. 또한 unique라는 특성 때문에 데이터의 무결성을 확보하는데도 의의가 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Index 와 B-tree&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Index&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에서 index 는 단어 그대로 색인자의 역할을 한다. 보통 한 개의 테이블에는 수십 개의 column이 존재하며, MySQL은 데이터 조회 시 가장 첫 번째 column부터 차례대로 검색하기 시작한다. 만약 index 가 존재하지 않는다면 데이터를 조회할 때 언제나 테이블 전체를 Full scan 해야 하는데 이는 비용과 성능 면에서 매우 비효율적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 데이터가 Create Update Delete 되는 경우 Index도 함께 수정돼야 하기 때문에 index가 없는 경우보단 효율이 떨어진다. 즉 read 성능을 살리고 cud를 성능을 희생한다고 볼 수 있다. 따라서 잦은 수정이 필요한 테이블의 경우는 오히려 index를 활용하지 않는게 더 이로울 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;B-tree / B+ -tree&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;878&quot; data-origin-height=&quot;964&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/owWH5/btrz5o6LWqt/OxHbaLY5OHBNiwCyguHEX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/owWH5/btrz5o6LWqt/OxHbaLY5OHBNiwCyguHEX0/img.png&quot; data-alt=&quot;&amp;amp;lt;made by author&amp;amp;gt;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/owWH5/btrz5o6LWqt/OxHbaLY5OHBNiwCyguHEX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FowWH5%2Fbtrz5o6LWqt%2FOxHbaLY5OHBNiwCyguHEX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;622&quot; height=&quot;683&quot; data-origin-width=&quot;878&quot; data-origin-height=&quot;964&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;&amp;lt;made by author&amp;gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 은 PK 를 설정하면 해당 값을 index 로 잡아 데이터를 B-tree 구조로 저장한다.(실제로는 B+tree)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B-tree 는 Root - Branch - Leaf 형태를 갖는데, 이 때 모든 Leaf 노드가 같은 레벨(수준)에 위치하도록 Balance 를 맞추기 때문에 B-tree 라고 칭한다. B-tree 에서 각각의 노드는 N 개의 자식을 갖을 수 있고, N이 홀수냐 짝수냐에 따라 다른 알고리즘이 적용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B-tree 에서 데이터를 찾는 경우 Root 노드부터 시작하여 검색하려는 값과 key point 값을 비교하며 Branch - Leaf 로 차례대로 찾아 내려간다. 따라서 원하는 값을 찾을 때 까지 모든 자료를 순회해야 하는 단점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;B+ tree는 각각의 노드들은 index 값만 가진 상태에서 최하단 leaf 에 실제 데이터가 존재하며 linked list 로 연결되어 있다. B+ tree는 각 상위 node에는 index 값(key)만 저장하고 데이터는 leaf 노드에 존재하기 때문에 하나의 저장 공간(page)에 더 많은 key 값을 저장할 수 있어 효율적이며 범위 검색에 훨씬 유리하지만 언제나 최하단 leaf 까지 접근해야 하는 단점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;PK 를 int 로 잡으면 좋은 이유&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2320&quot; data-origin-height=&quot;1328&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/P3OVF/btrz5n04QOj/bzDC6KxXXD5V2Qv393OmM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/P3OVF/btrz5n04QOj/bzDC6KxXXD5V2Qv393OmM0/img.png&quot; data-alt=&quot;&amp;amp;lt;made by author&amp;amp;gt;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/P3OVF/btrz5n04QOj/bzDC6KxXXD5V2Qv393OmM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FP3OVF%2Fbtrz5n04QOj%2FbzDC6KxXXD5V2Qv393OmM0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2320&quot; height=&quot;1328&quot; data-origin-width=&quot;2320&quot; data-origin-height=&quot;1328&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;&amp;lt;made by author&amp;gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/innodb-architecture.html&quot;&gt;MySQL 은 INNO DB storage engine 으로 되어 있으며&lt;/a&gt;, Index 와 Data 는 On-Disk 구조위에 존재한다. 디스크 특성상 데이터를 읽어낼 때 블럭 단위로 읽게 된다. 만약 블럭 크기가 1,024kb 라면 1kb만 들어있던 1,024kb가 가득 차있던 읽는 사이즈는 1,024kb 가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INNO DB에서 각 데이터를 저장하는 공간(page) 는 기본적으로 &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/innodb-parameters.html#sysvar_innodb_page_size&quot;&gt;innodb_page_size setting&lt;/a&gt; 을 건들지 않는다면 16KB의 크기를 갖는다. 또한 전체 공간을 fully 사용하지 않고 Insert 또는 Update 상황을 대비해 1/16의 공간(1KB) 는 비워둔다. 때문에 한 page에 저장할 수 있는 데이터의 양은 15,000Byte 정도 된다고 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1436&quot; data-origin-height=&quot;250&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WPgKd/btrz61bV23S/3JSFXjGrokwfOIKddBi2W0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WPgKd/btrz61bV23S/3JSFXjGrokwfOIKddBi2W0/img.png&quot; data-alt=&quot;&amp;amp;lt; MySQL Documentation &amp;amp;gt;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WPgKd/btrz61bV23S/3JSFXjGrokwfOIKddBi2W0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWPgKd%2Fbtrz61bV23S%2F3JSFXjGrokwfOIKddBi2W0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1436&quot; height=&quot;250&quot; data-origin-width=&quot;1436&quot; data-origin-height=&quot;250&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;&amp;lt; MySQL Documentation &amp;gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;int type은 항상 4byte의 크기를 갖고, UNSIGNED 속성을 가질 때 4,294,967,294개의 unique 한 값을 표현할 수 있다. char type은 1글자당 1byte를 사용하는데 만약 int type과 같은 개수의 unique 한 데이터를 알파벳 조합으로 표현하기 위해서는 최소 6byte의 크기가 요구된다. 따라서 1개 page 에 저장할 수 있는 index의 갯수는 int index table = 15,000/4 = 3,750 개, char index table = 15,000/6 = 2,500 개가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 똑같이 3,000개의 데이터를 조회할 때 int 를 index로 잡으면 1개의 page만 조회하면 되지만, char를 index로 잡은 테이블은 2개의 page를 조회해야 하는 비효율이 발생한다. index 저장을 위한 storage 공간 역시 같은 데이터를 저장할 때 int를 사용한 테이블이 더 적은 page가 필요하므로 훨씬 효율적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새롭게 index가 추가되거나 update 되는 경우 정렬과 node balance 를 맞춰야 하는데, 이 과정에서 page split이 발생할 수 있다. 문제는 split 과정에서 비효율이 발생할 수 있다는 점이다. 가령 page 2 중간에 데이터가 insert 돼야 하고 이미 해당 page가 꽉 찬 상태라면 한 칸씩 뒤로 밀면서 다음 page로 데이터를 넘기면 될 것 같으나 실제로는 아예 새로운 page를 만들게 된다. 이 때문에 위에서 언급한 page split 상황에서 공간을 제대로 활용하지 못하는 비효율이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;int type 은 char type에 비해 조회시에도 이점을 갖는다. int type은 &amp;lt;,&amp;gt;,= 으로 빠르게 비교가 가능하지만 char type은 &amp;ldquo;%something&amp;rdquo; 이나 &amp;ldquo;something%&amp;rdquo; 와 같은 방법으로 비교해야 하는데, 이 경우 가장 앞글자부터 순회비교를 하기에 성능이 떨어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 char type은 Whit space (&amp;rsquo; &amp;rsquo;) 도 1 byte 로 취급한다.(ver 5.0 &amp;ge;) 따라서 &amp;lsquo;a&amp;rsquo; &amp;ne; &amp;lsquo;a &amp;lsquo; 라는 결과가 나타나 공간 효율성을 망칠뿐 아니라 데이터의 무결성에도 영향을 미친다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;varchar 같은 가변형 값을 PK로 지정해 index로 쓴다면 앞서 말한 공간 확보 문제 등에서 조금 더 자유로워지니 괜찮지 않은가? 라고 생각할 수 있다. 만약 user id라는 컬럼이 &amp;lsquo;a&amp;rsquo; 부터 가입한 이용자에게 차례로 부여되고 이것을 PK로 잡은 테이블이 있다고 생각해보자. 초기에는 분명 int로 사용할 때보다 더 효율적인 부분이 존재할 수 있다. 하지만 신규 이용자가 늘어날수록 user id는 길어질 수밖에 없다. 이는 곧 신규 이용자 증가에 비례해 Index field size가 커지는 것을 뜻하며 DB 성능도 비례해 하락할 것을 의미하기 때문에 좋다고 볼 수 없다. 또한 현실적으로 대부분 회사에서 생성되는 ID 값은 (꽤 긴) 고정된 길이를 갖는 경우가 많기에 Int보다 효율을 내는 경우는 거의 없다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인덱스 정리 및 팁(&lt;a href=&quot;https://jojoldu.tistory.com/243&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://jojoldu.tistory.com/243&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;[SQL server] Page Split(페이지 스플릿) 이해하기 (&lt;a href=&quot;https://datalibrary.tistory.com/125&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://datalibrary.tistory.com/125&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;The basics of InnoDB space file layout(&lt;a href=&quot;https://blog.jcole.us/2013/01/03/the-basics-of-innodb-space-file-layout/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://blog.jcole.us/2013/01/03/the-basics-of-innodb-space-file-layout/&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;MySQL INNO DB는 어떻게 생겼나요?(&lt;a href=&quot;https://cipleme.tistory.com/20&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://cipleme.tistory.com/20&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;MySQL Document(&lt;a href=&quot;https://dev.mysql.com/doc/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://dev.mysql.com/doc/)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Optimizing Schema and Data Types(&lt;a href=&quot;https://www.oreilly.com/library/view/high-performance-mysql/9781449332471/ch04.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.oreilly.com/library/view/high-performance-mysql/9781449332471/ch04.html&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;</description>
      <category>Data/Data Engineering</category>
      <category>B Tree</category>
      <category>db</category>
      <category>index</category>
      <category>int vs char</category>
      <category>mysql</category>
      <category>PK</category>
      <author>London_</author>
      <guid isPermaLink="true">https://leo-bb.tistory.com/83</guid>
      <comments>https://leo-bb.tistory.com/83#entry83comment</comments>
      <pubDate>Thu, 21 Apr 2022 19:43:06 +0900</pubDate>
    </item>
    <item>
      <title>2021년 회고</title>
      <link>https://leo-bb.tistory.com/82</link>
      <description>&lt;h1&gt;2021년 회고록&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;런던은 지금 쏘카에서 2년째&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2020년은 마이크로 한 비즈니스 분석과 제품 개선의 역할을 중점으로 활약했다. 정규직 채용도 처음, 데이터 사이언티스트 업무도 처음이었기 때문에 무엇이든 배우는 게 중요하다고 생각했고 때문에 팀 동료들과 다른 직군 사람들의 일하는 방식과 센스를 배우기 위해 페어로 일하는 경우가 많았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 그러한 노력이 빛을 발한 것인지 2021년에는 나의 Own 서비스나 Project 를 갖고 주도적으로 더 많은 결정권을 갖고 업무를 수행할 기회가 많았다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;신사업의 탄생과 임종을 지켜보며&lt;/h3&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;img src=&quot;https://user-images.githubusercontent.com/58500111/148680811-702aeba8-e83a-4a2d-98ba-bd30986483bd.jpg&quot; alt=&quot;PS20102800136&quot; width=&quot;485&quot; height=&quot;485&quot; /&gt;&lt;/h3&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작년까지 VCNC(타다)는 쏘카의 자회사로 쏘카 데이터 그룹 중 내가 속한 팀은 VCNC 의 데이터팀 역할을 병행하고 있었다. 2020년 후반기에 타다에서 경쟁력 재고를 위한 대리 서비스를 신사업으로 오픈하게 되었고, 그룹장님과 팀장님의 권유로 신사업에 참여하게 되었다. 맡았던 역할은 크게&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가격 시스템의 설계와 운영&lt;/li&gt;
&lt;li&gt;제품의 개선점 발굴 및 문제해결&lt;/li&gt;
&lt;li&gt;효과적인 마케팅을 위한 분석 및 전략 수립&lt;/li&gt;
&lt;li&gt;데이터 접근 장벽을 낮추기 위한 마트 테이블, 대시보드 제작&lt;/li&gt;
&lt;li&gt;데이터 로깅을 위한 정의, 서버팀-클라팀-데이터팀 컨센서스 조율 및 파이프라인 관리&amp;nbsp;&lt;/li&gt;
&lt;li&gt;데이터 QA&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;였다. 돌이켜보면 업무 커버 레인지가 정말 넓었다. 기본적으로 항상 3개 이상의 태스크를 수행하면서 동시에 여러 Cell(신기능 추가 등을 위한 목적 조직)의 데이터 담당자를 병행하기도 했다. 어쩐지 매일이 야근이었다. 정말 치열하게 일했지만 서비스 운영은 순탄치 못했다. 어느순간 수요의 성장세가 둔화되고 있었고, 회사 입장에서는 내부 사정으로 추가적인 투자를 보류하고 오히려 비용 감소를 목표했다. 플랫폼 중개 사업은 수요와 공급 모두 특정 수준 이상이 되어 안정 궤도에 안착해야 비로소 안정적인 수익을 내고 운영이 가능한데 그러기 위해서는 더 공격적인 투자(비용)가 필요했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 회사 차원에서 더 큰 성장을 위해 과감히 신사업 철수를 결정했고, 이 시기에 내가 맡았던 대리를 포함 캐스팅이라는 중고차 거래 사업도 함께 종료 되었다. 당시 이 결정에 대해 나는 굉장한 실망 했었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째로 쏘카-타다의 슬로건은 이동의 기본... 종합 모빌리티 회사로의 성장이었고 그러한 목표에서 대리와 중고차 시장으로의 진출은 타당하고 의미 있는 진출이라 생각했다. 그러나 회사 차원에서 한 번 접어버린 사업 분야를 다시 진출하는 것은 거의 일어나지 않기에 중요한 사업 다각화 포인트 두 개를 완전히 잃는다고 생각했다. (대리 전체 시장규모는 조 단위 시장이고, 중고차 판매는 쏘카의 카쉐어링 사업 구조상 차량 L.T 에서 핵심적 역할을 할 수 있는 서비스였다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째, 또한 타운홀에서 신사업 출시에 대해 적극적으로 지원할 거고, 코로나 위기에 맞춰 회사의 생존을 위한 과감한 투자라는 발표가 있었는데 그러한 포부에 비해 철수 결정이 너무 빨랐다는 점이다. (1년을 못 버티고 철수했으니...)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;셋째는 나의 부족함이 이러한 결과를 만들었다는 자책감이 컸다. 내가 더 퍼포먼스를 냈으면... 생존할 수 있는 중장기 전략을 더 잘 그려왔으면... 수요와 공급을 폭발시킬 수 있는 아디디어를 생각해 냈으면... 등등 모든 책임이 나에게 있는듯한 기분이었다. 내가 분석한 자료가 서비스 종료 결정의 백데이터로 사용되기도 했으니 자책감이 더욱 컸었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;혹시 번아웃(?)&lt;/h3&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;2021042302312_0.jpg&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;555&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dcKJBS/btrqd43To7D/JWDlHz9auzKfH9kwBPgll1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dcKJBS/btrqd43To7D/JWDlHz9auzKfH9kwBPgll1/img.jpg&quot; data-alt=&quot;클립아트 코리아&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dcKJBS/btrqd43To7D/JWDlHz9auzKfH9kwBPgll1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdcKJBS%2Fbtrqd43To7D%2FJWDlHz9auzKfH9kwBPgll1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;458&quot; height=&quot;397&quot; data-filename=&quot;2021042302312_0.jpg&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;555&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;클립아트 코리아&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타다 대리 서비스 종료 후 타다의 다른 서비스인 라이드헤일링(라이트, 플러스) 관련 업무를 진행했다. 주로 마케팅팀과 협업하여 데이터팀 도움 없이도 쉽게 마케팅 관련 정보를 수집하고, 모니터링할 수 있는 시스템을 만들기 위해 노력했다. BigQuery SQL 작성 팁이나 홀리스틱스라는 BI 엔터프라이즈 사용법, 효과적인 시각화 방법 등을 교육자료로 만들어 전파했다. 또한 지표 아이디에이션을 통해 주요 지표와 부가 지표로 세분화하고 부족한 부분을 추가하는 등 효과적인 모니터링과 조직 내/외부 커뮤니케이션을 위한 지표를 정의했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 패스포트라는 쏘카 - 타다 통합 멤버십이 기획되었고 나는 쏘카와 타다 모두의 데이터를 알고 있는 몇 안 되는 사람 중 한 명이었기에 타다 쪽 데이터 담당자로 참여하게 되었다. 다만 이 때는 BI 제작, 퍼널 대시보드 등 모니터링 시트 제작, 데이터 적재 관리 및 가입 증진을 위한 쿠포 닝 전략, 넛지/푸시 노출 추가 등의 활동만 하고 인수인계한 뒤 완전히 쏘카 업무로 복귀하였는데, &lt;a href=&quot;https://www.joongang.co.kr/article/25013368#home&quot;&gt;토스의 타다 지분 인수&lt;/a&gt;로 인해 쏘카-타다 두 회사 간의 분리가 필요했기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나로서도 좋았던 부분은 스스로 평가하기에 번아웃을 겪고 있다 판단했던 시기였기 때문이다. 다만 그 원인이 대리 때 업무가 너무 많고 스트레스였어서 보다 오히려 서비스 종료 후 상대적으로 너무 여유로워져서 오는 번아웃이었다. 반년 이상을 정신없이 살다가 갑자기 일을 하고 있는데도 휴가인 것처럼 여유로워지니까 오히려 불안하고 지루했다. 어떤 일을 잡아도 이미 해본일이었고 &amp;ldquo;내 일&amp;rdquo;이 아니라는 기분도 들었다. (이 시기에는 전과 달리 회사와 서비스 &amp;ne; 나로 느꼈던 것 같다.) 점점 더 마음속 불꽃이 사그라지는 기분이었다. 쏘카 업무로 복귀한다면 처음 입사해서 할 때와는 많이 달라진 환경과 새로운 문제들을 통해 맘 속에 불을 지필 수 있을 것 같았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;IPO...! 그리고 퇴사&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거 내가 처음 입사했을 땐 카일, 캐롯, 윤, 야마니, 네이썬 총 5명의 사람들과 함께 타다 데이터팀에 소속되어 있었다. 처음 팀에 와서 놀랐고 좋았던 부분은 5명의 사람들의 능력치를 오각형으로 그린다면 빠지는 역량 없이 고루 분포하는데, 한 명 한 명이 특정 분야에서 특히 차별화된 능력치를 갖고 있어서 상호 커뮤니케이션이 잘되면서 동시에 업무분장도 확실하게 되고 있다는 부분이었다. 나이 또래도 다 비슷해서 업무 외에도 통하는 부분이 많았다. 이런 팀에서 커리어를 시작하는 게 다행이라고 느끼기도 했고, 실제로 지금도 다행이었다 생각한다. 하지만 코로나 위기 극복을 위해 몇 번의 팀 리빌딩이 있었고, 그 과정에서 카일, 캐롯을 제외하고는 모두 다른 팀/조직으로 이동하거나 퇴사를 하게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쏘카 업무로 복귀하며 옛날 팀원이었던 윤을 포함하여 옛 타다 데이터팀이 다시 뭉쳐 일할 수 있게 되었다.(이 외에 모델링팀의 세레나, 브루노가 함께 하였다.) 팀은 TF 형식으로 구성되었고 &lt;a href=&quot;https://www.shinhaninvest.com/siw/ib/ecm/ib_ecm_ipo_tab1_3/contents.do&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;2022년 IPO를 대비 목적&lt;/a&gt;이었다. 대부분 업무를 혼자 맡아 진행하다가 믿을 수 있는 동료와 함께 일할 수 있다는 점은 설레는 일이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 실제로 TF 에 참가하여 진행했던 업무들은내가 지향하는 방향과는 상이한 부분이 많았고, 결과적으로 내게 충분한 만족감을 주지 못했다. 이 시기에 새로운 도전을 진지하게 고민해보기 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조금만 참고 IPO 성공적으로 마무리되어 상장되면 지금까지 고생에 대한 보상도 충분할 텐데 고생은 다하고 보상은 못 받는 건 너무 아깝지 않냐는 말을 많이 들었다. 또한 지금의 훌륭한 동료들이 있는 환경을 벗어나면 후회하지 않겠느냐는 우려도 많이 받았다.(이 역시 여러 퇴사자 또는 지인과 대화해 보면 쏘카의 데이터본부 역량은 대기업, 스타트업 가릴것 없이 국내 10손가락 안에 든다고 생각한다.) 하지만 여러 고민 끝에 결심은 굳어졌고 12/24일부로 퇴사를 결정하게 되었다. (관련 내용은 후반부에 언급하겠다. )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1년 차 데이터 사이언티스트 런던의 회고&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1112.jpg&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cjQY5G/btrqbGWLrkg/ZKoKs1fYoq0gJf4tczZdz0/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cjQY5G/btrqbGWLrkg/ZKoKs1fYoq0gJf4tczZdz0/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cjQY5G/btrqbGWLrkg/ZKoKs1fYoq0gJf4tczZdz0/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcjQY5G%2FbtrqbGWLrkg%2FZKoKs1fYoq0gJf4tczZdz0%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;484&quot; height=&quot;363&quot; data-filename=&quot;1112.jpg&quot; data-origin-width=&quot;4032&quot; data-origin-height=&quot;3024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비즈니스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대리를 맡게 될 때 나의 포부는 모두가 만족하는 서비스를 만들겠다는 것이었다. 그러나 나의 이상(ideal)은 정말 이상이라는 점을 인정하게 됐다. 아무리 첫 이벤트에 좋은 경험을 느끼게 신경 쓰고 2차 이용을 유도해도 체리피킹 하는 사람이 많았다. 소비자 중에는 적정 가격을 알아도 더 싸게 이용하고 싶어 하는 이용자가 있었고, 공급자는 반대로 더 비싸게 받고 싶어 하기도 했다. 협의가 되는 중간점을 제시하면 오히려 둘 다 거부하는 내 상식과 예측을 벗어나는 현상도 관찰했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 단순히 주저앉고 포기하는 건 내 스타일이 아니었다. 모두를 만족시킬 수 없지만 대다수를 만족시키기 위해 노력했다. 일례로 &amp;ldquo;쓰고 싶어도 못쓰는 사람들의 니즈 해결&amp;rdquo;에 초점을 맞춘 &amp;ldquo;업단가&amp;rdquo;라는 기능을 제안하여 제품에 반영했다. 대부분 플랫폼 서비스는 내부 로직에 의해 특정 가격이 제시되고 이용자에게 조정권을 제공하지 않는데 이 틀을 벗어나는 기능이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당시 우리 서비스는 세부 지역 단위로는 가격 모델에 반영할 만큼의 수요가 없는 지역이 많았고, 대리 특성상 거리두기로 인해 특정 시간에 수요가 집중되는 현상도 존재했다. 때문에 가격 시스템은 안전성을 위해 최소 수요 수준을 고려하고 다소 넓은 지역의 수요/공급을 통해 시장 상황을 판단했다. 이는 상대적으로 모델에 영향력이 낮은 지역은 제시된 가격과 시장 상황의 괴리가 커지는 문제를 야기했다. 특히 가격 수준이 낮게 제시되는 경우 유저가 아무리 이용하고 싶어도 공급자와 매칭이 되지 못해 이탈하는 문제가 있었다. 업단가는 이러한 소비자를 위해 제시된 가격에서 유동적으로 조절하여 재호출 할 수 있게 해주는 기능이었다. 실제로 해당 기능 출시 후 매칭률과 이용 완료율은 출시 전보다 평균 15%p 이상 상승했다. 전보다 훨씬 더 많은 수요와 공급을 만족시킬 수 있었고, 특정 시간, 지역에서 실제로 어느 정도 가격이 적정한 수준인지 백데이터를 얻어 가격 시스템에 반영할 수 도 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사람의 행동을 예측하고 유도하는 것은 생각보다 (정말, 엄청, 매우) 굉장히 어려운 일이라는 점 또한 배웠다. 이용자의 판단을 최소화하고, 흐름만 따라가도록 만들었다고 생각한 이용 퍼널도 상상도 못 한 과정으로 이용하는 유저들이 있었으며, 시스템 허점을 파고 드려는 어뷰징 유저와 드라이버들도 있었다. 할인 혜택, 적립 혜택 등 현금성 이벤트를 한다고 무조건 소비자와 공급자가 유도되는 것은 아니라는 점도 배웠다. (더불어 이 부분은 나뿐 아니라 모든 구성원에게 의미 있는 배움이었다 생각한다.) 몇 번의 A/B test에서 오히려 푸시/넛지 등으로 제안하는 경우가 그렇지 않은 경우보다 참여율이 떨어지는 점 등을 통해 노티가 주는 부정적인 영향이 존재한다는 것을 배울 수 도 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;커뮤니케이션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커뮤니케이션에 대해서도 배운 점이 많았다. 나는 보통 업무 진행 시 기록을 굉장히 중요하게 생각한다. 회의 때 공유할 자료부터 업무 진행 간 진행사항과 개인적인 메모까지 되도록 세세하게 문서화하여 남겨둔다. 가장 큰 장점은 문서 하나로 모든 설명이 가능하기 때문에 별도로 미팅을 잡아 시간을 뺏기는 일이 적다.(런던 탬플릿이라고 별도로 만들어 사용할 정도)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 다양한 사람들과 협업을 하다 보니 생각보다 이런 문서화 작업이나 문서 자체에 스트레스를 받는 사람이 많다는 점을 알 수 있었다. 이런 분들에게는 오히려 주요 그래프 같은 시각자료 몇 개를 두고 대화를 통해 디벨롭하는 게 훨씬 일의 진행이나 결과가 좋았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 생각보다 많은 사람들이 데이터를 선택적으로 관찰 및 이용하는 경향이 있음을 배웠다. 이는 크게 두 가지 케이스로 나뉘었는데 데이터 추출 과정이 미숙하거나 지표에 대한 이해도가 부족해서인 경우와 자기주장에 유리하게 보이는 숫자만 골라 쓰려는 경향 때문이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일화로 과거에 작성했던 문서중 A라는 결론을 보충 설명하기 위해 다양한 지표가 담긴 B라는 자료를 제작한 적이 있는데, 훗날 누군가 A라는 결론과 무관한 C를 주장하기 위해 B 자료에서 입맛에 맞는 자료만 가져다 짜깁기 한 뒤 레이징&amp;nbsp;했고, 당연히 자료의 출처로 나를 지목하여 곤란한 연락을 받은 경우가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 두 케이스 모두 데이터 리터러시 능력 부족이 원인이라고 생각한다. 데이터팀으로써 어떻게 하면 구성원의 리터러시 능력을 키울 수 있을지 계속 고민하고 있는데, 나 혼자의 열정만으로는 어려움도 존재하는 것 같다. 번뜩이는 아이디어가 얼른 생각나길..!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2년 차 런던은 이랬으면 좋겠다&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;확고한 방향성 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작년 동안 내게 가장 의미 있는 일은 &amp;ldquo;나는 어떤 데이터 쟁이가 되고 싶은가?&amp;rdquo;에 대한 고민을 하기 시작했다는 점이라 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;졸업 후 전공과는 접점이 하나도 없던 데이터 분석가에 도전해서 일을 시작했다. 동료들은 정말 너무 똑똑하고 잘하는 사람들이 많았고, 내가 이 사람들처럼 될 수 있을까? 가 아니라 이 사람들의 발목을 잡는 건 아닐까?라는 걱정이 들었다. 그러다 보니 동료들과 눈높이를 맞추는 사람이 되자! 가 목표가 되었고 반년 넘게 퇴근하면 각종 강의와 도서를 활용해 평균 3시간씩은 공부한 것 같다. (수험생 때도 안 해본 출퇴근 지하철에서 인강 듣기를...) 노력하다 보니 어느새 조금씩 인정을 받기도 했고, 나도 여유가 생기고 특정 분야들에 대해서는 오히려 알려줄 수 있는 입장이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 근본적으로 나는 어떤 데이터 사이언티스트가 되고 싶은지에 대해선 생각해본 적이 없었다. 카일(팀장)은 나에게 항상 &amp;ldquo;런던이 앞으로 가고 싶은 길을 고민해보고 선택해야 할 것 같다. 뭐가 되고 싶은지 생각해보라. 그래야 그에 맞는 일을 더 잘할 수 있도록 도와줄 수 있다&amp;rdquo; 는 말을 종종 해주었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 나는 (지금도) 제품 분석을 통해 사용자 경험을 증진시키는 것도 너무 재밌었고, 비즈니스 관점에서 수요 분석을 통해 가격 시스템을 개선하는 과정도 너무 즐거웠다.&amp;nbsp; 솔직히 마케팅 분석은 너무 즐거웠다 까지는 아니었다. 그래도 항상 내 대답은 &amp;ldquo;제너럴리스트로써 성장하면 안 되는 걸까요? 특출나진 않겠지만 어떤 일이든 수행 가능한. 눈에 확 띄진 않아도 묵묵히 1인분 이상을 하는 사람이 되는 것도 좋은 거 같아요&amp;rdquo;였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;역시 지금도 답은 못 내렸고, 이건 2년 차를&amp;nbsp;달리는 런던에게 맡겨야 할 문제인 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;대화하는 사람이 되기&lt;/h3&gt;
&lt;h3 style=&quot;text-align: center;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/58500111/148680833-ee3ed459-3272-4d11-a96f-8716d5e67ebe.jpg&quot; alt=&quot;대화&quot; width=&quot;692&quot; height=&quot;296&quot; /&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올해 관심을 갖는 부분은 데이터 분석가나 과학자가 되고 싶어 하는 학생들이나 전직을 희망하는 현직자 분들에게 내가 어떠한 도움을 드릴 수 있을까? 이다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최근 데이터 분석가나 과학자가 되고 싶어 하는 사람들은 점점 늘어나는 것 같다. 많은 개발자 지망생들도 데이터 엔지니어를 꿈꾸는 사람들이 많아졌고, 실제로 기업에서도 데이터 엔지니어에 대한 수요는 매해 폭발적으로 늘고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내 생각에 실제로 타 산업직군에 비해 데이터 분야는 취업 정보가 매우 적다. 나 역시 처음 데이터 분석가를 꿈꿀 때 뭘 알아야 하고, 뭘 공부해야 하고, 뭘 준비해야 할지 정말 알 방법이 없었다. 무작정 패스트 캠퍼스에서 데이터 엔지니어 과정을 들었었다. 돌이켜보면 실제로 데이터 분석가나 사이언티스트가 되고 싶다는 사람치고 취업 준비를 잘했다고 평가할 수 없었던 것 같다. (진짜 어떻게 뽑혔던거지 나는...)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비록 나도 여전히 주니어 데이터 사이언티스트이지만 이렇게 고민이 많은 (예비)데이터 쟁이들에게 도움이 되고 싶다. 특히 남을 도울때는 그 주제가 무엇이든 최소 98%는 알고 있는 상태여야 한다고 생각한다. 때문에 멘토링은 나 스스로의 성장도 이뤄낼 수 있는 원동력이 될 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;타다 쑥쑥 키우기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 쏘카를 퇴사했다고 언급했는데 갑자기 타다 쑥쑥 키우기라고? 싶을 것 같다. 앞에서 퇴사 이후 행적은 언급하지 않았는데, 결론만 말하면 1월 3일부로 쏘카에서 타다로 옮기게 되었다. 사실 이직이라고 말하기도 애매한데... 이제 다른 회사가 됐으니 이직이 맞지 싶다. 실제로 다른 회사에 갈 생각도 있었고, 얘기도 되었으나 건강 문제와 과거 대리 종료의 아쉬움이 있어 재도전하고 싶은 마음도 있었기에 고민 끝에 타다로 넘어오게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타다를 선택한 이유를 더 구체적으로 얘기하자면 2가지 정도가 있는데 첫 번째 이유는 퇴사 이유와도 얼추 통용된다. 어쩔 수 없이 일반적인 분석가나 과학자의 업무는 서비스의 특정 어떤 부분에 집중하게 된다. 하지만 대리 사업을 하다 보니 점점 서비스의 한 부분뿐 아니라 서비스 자체 관점에서 많은 고민을 하게 됐다. 이는 곧 &amp;ldquo;내가 어떤 서비스를 만들고 싶은가?&amp;rdquo;에 대한 고민으로 이어졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 데이터 분석가로 일하며 가장 보람을 느낄 때는 내가 세운 가설과 발견한 문제점을 토대로 개선책을 제안하고 증명하여 실제로 프로덕션에 적용될 때와 액션이 잘 동작하여 문제가 해결되는 것이다. 그래서 나는 이 서비스 진짜 좋아, 이 서비스의 이 기능 너무 편해! 와 같은 얘기를 듣고 싶고, 나는 자랑스럽게 그거 내 아이디어야, 내가 그거 만드는 거 같이 했어!라고 말하고 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타다는 현재 넥스트(대형승합)와 호출 예약 등 신사업을 론칭하고 시작하는 단계에 있다. 이미 업력이 오래된 서비스는 어쩔 수 없이 어느 정도 서비스의 방향과 관행 등이 굳어져 있어 &amp;ldquo;개혁&amp;rdquo;하기 어렵다.(난 내 몸을 29년째 제어하고 있지만 여전히 아침에 잘 일어나는 사람으로 바꿔놓지 못했다. 하물며 기업과 서비스는 더욱 어려울 수 밖에 없다.) 때문에 지금 타다에 합류하면 &amp;ldquo;내가 옳다고 생각하는 서비스, 내가 좋아하는 서비스가 될 수 있도록 하는데에 훨씬 더 큰 영향력을 줄 수 있을 것 같다&amp;rdquo; 는 판단을 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 이유는 지금 타다의 데이터팀은 변혁기를 겪고 있다. 새로 합류하신 분들도 많고, 나보다 연차가 낮으신 분들도 계시며 여전히 새로운 인원에 대한 채용도 적극적으로 이루어지고 있다. 2. 대화하는 사람 되기 에서 언급했듯 최근 내 최대 관심사는 멘토링이다. 지금 타다 데이터팀에 합류하면 내가 배웠던 좋은 데이터 팀 문화를 전파하고 좋은 팀원, 팀장에게 배운 부분을 벤치 마크하여 새로운 팀원에게 도움을 줄 수 있을 거라 기대했다. 그래서 타다의 데이터팀을 다른 팀, 회사에서 벤치마크 하고 싶은 팀으로 만들고 싶다는 포부를 가졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 나는 신사업 중 하나에 참여하여 첫날부터 바쁘게 업무를 보고 있다. 쏘카만큼 타다 문화를 잘 알고 있었지만 토스에서 인수된 이후 확실히 문화나 업무 방식이 토스와 많이 닮아진 듯해 어색하기도 하다. 나에겐 낯선 부분도 많고 업무량도 엄청나지만(나도 신규 입사자 대우해주지.. ㅠ) , 팀원들도 합류를 반겨주고 업무 자체도 Own 하여 할 수 있도록 배려해줘서 만족스럽다. 더불어 틈틈이 팀원들과 많은 대화를 할 수 있도록 시간을 내려고 노력하고 있고, 팀 전체 관점에서 성장하기 위해 어떤 방안이 좋을지 고민하고 있다. (스터디 의견도 제시했는데 생각보다 관심 갖는 분들이 계셔서 추후에 참여자를 종합하여 빌드업할 예정이다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞으로 수많은 난관과 문제에 봉착하겠지만, 올해에 타다 신사업을 잘 성장시켜서 타다 대리 실패에 대한 마음의 짐도 풀고, 나를 포함한 타다 데이터팀의 능력치도 지금보다 월등히 높여 어디서나 인정받는 데이터 팀이 될 수 있도록 하겠다. 연말 회고엔 꼭 다짐한 바를 모두 이뤘다고 쓸 수 있길...!!&lt;/p&gt;</description>
      <category>개인사</category>
      <category>2021</category>
      <category>2021 회고</category>
      <category>2021회고</category>
      <category>2022</category>
      <category>쏘카</category>
      <category>연말회고</category>
      <category>타다</category>
      <category>회고</category>
      <author>London_</author>
      <guid isPermaLink="true">https://leo-bb.tistory.com/82</guid>
      <comments>https://leo-bb.tistory.com/82#entry82comment</comments>
      <pubDate>Sun, 9 Jan 2022 20:46:36 +0900</pubDate>
    </item>
    <item>
      <title>Opt in Rate 을 늘리기 위한 노력</title>
      <link>https://leo-bb.tistory.com/81</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;I. Opt in rate 이란?&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;144168542-e53edc17-eced-42e4-8924-dd1af4e9019a.png&quot; data-origin-width=&quot;2340&quot; data-origin-height=&quot;788&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k0cQ2/btrmGWuRazy/dwK1EpmzKeAjxtEUlT9lKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k0cQ2/btrmGWuRazy/dwK1EpmzKeAjxtEUlT9lKK/img.png&quot; data-alt=&quot;Netflix&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k0cQ2/btrmGWuRazy/dwK1EpmzKeAjxtEUlT9lKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk0cQ2%2FbtrmGWuRazy%2FdwK1EpmzKeAjxtEUlT9lKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2340&quot; height=&quot;788&quot; data-filename=&quot;144168542-e53edc17-eced-42e4-8924-dd1af4e9019a.png&quot; data-origin-width=&quot;2340&quot; data-origin-height=&quot;788&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Netflix&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Opt in view는 사진과 같이 어떠한 서비스를 제공하기 전에 &quot;구독/가입&quot;을 유도하고, 사용자의 정보를 요청하는 모든 view를 말한다. Opt in Rate 은 이러한 목적의 view 노출 유저 중 실제 동의한 유저의 비율을 나타낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Opt in view 는 주로 Web based 인 경우가 많은데, App based로 오는 경우 마케팅 수신 동의 , 광고성 정보 수신 동의가 이에 해당한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;종합하여 정리하면 &quot;&lt;b&gt;서비스 가입이나 가입 유도(적극적) 또는 서비스에 관한 정보를 제공할 수 있는 동의(소극적)를 얻고자 하는 목적의 모든 화면&quot;&lt;/b&gt;과 &quot;&lt;b&gt;해당 화면에서 동의한 유저의 비율&lt;/b&gt;&quot;을 말한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;II. Opt in rate의 중요성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Opt in view 는 데이터 분석가에게도 꽤나 생소한 지표인데, 가장 큰 이유는 결제보다는 가입시키는 데에 더 중점을 두기 때문에 상대적으로 중요도가 떨어지기 때문으로 보인다. (결국 회사에서 분석가에게 요구하는 것은 인사이트를 통해 최종적으로 매출을 극대화하는 작업이기 때문) 하지만 Opt in rate 역시 절대 간과할 수 없는 부분인데, 그 이유는 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오늘날 많은 서비스가 구독의 형태로 제공되고 있기 때문에 가입과 결제의 연결고리가 두터워지고 있다.&lt;/li&gt;
&lt;li&gt;Opt view 는 우리 서비스와 만나는(영업하는) 가장 첫 화면이다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;고객이 서비스에 갖는 첫인상이 전환율에 끼치는 영향은 여러 연구와 경험적으로 정립된 사항이다.&lt;/li&gt;
&lt;li&gt;Opt in view &amp;rarr; 실제 가입 전환까지 이탈률을 통해 마케팅 분석가는 문제점을 도출하고 개선안을 강구할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;소극적인 Opt view 에서 conversion 된 유저는 우리 서비스를 사용하기 위한 의향이 있는 유저로 &quot;대기 수요&quot;로 추정할 수 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;즉 단순히 신규 유저 증감율 뿐아니라 opt in user의 추이도 보조 지표로 활용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;(특히 앱 서비스에서) 유저와 직접 소통할 수 있는 유일한 창구를 만드는 과정이다.&lt;/li&gt;
&lt;li&gt;앱 서비스에서 대부분의 CRM 마케팅 수단은 마케팅/광고성 정보 수신동의를 수락한 유저에게만 가능하다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;푸시, 혜택 안내 메세지(sms) 등 대부분 CRM 마케팅의 대상 자체가 수신동의자로 한정된다&lt;/li&gt;
&lt;li&gt;dormant, resurrected user (휴면 또는 이탈 유저)를 깨우기 위해 혜택 알림 푸시를 보내는 등 행위가 성공적으로 이루어지기 위해서는 높은 opt in rate 이 선행돼야 한다.&lt;/li&gt;
&lt;li&gt;Opt in user가 적으면 CRM 개선을 위한 A/B test 등 실험시에 모수가 제한되고, 결과가 부정확할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;III. Opt in rate 을 늘리기 위한 노력&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dArfcV/btrmGiyc4Yr/GLoSKm8H8a2Ej5ghWCRtUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dArfcV/btrmGiyc4Yr/GLoSKm8H8a2Ej5ghWCRtUK/img.png&quot; data-origin-width=&quot;1568&quot; data-origin-height=&quot;1143&quot; data-filename=&quot;img.png&quot; style=&quot;width: 49.4166%; margin-right: 10px;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dArfcV/btrmGiyc4Yr/GLoSKm8H8a2Ej5ghWCRtUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdArfcV%2FbtrmGiyc4Yr%2FGLoSKm8H8a2Ej5ghWCRtUK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1568&quot; height=&quot;1143&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dQzTPh/btrmEj5nzBJ/cbrh6Bf3A4D1GcAVTH7azk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dQzTPh/btrmEj5nzBJ/cbrh6Bf3A4D1GcAVTH7azk/img.png&quot; data-origin-width=&quot;1232&quot; data-origin-height=&quot;898&quot; data-filename=&quot;img.png&quot; width=&quot;860&quot; height=&quot;627&quot; style=&quot;width: 49.4206%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dQzTPh/btrmEj5nzBJ/cbrh6Bf3A4D1GcAVTH7azk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdQzTPh%2FbtrmEj5nzBJ%2Fcbrh6Bf3A4D1GcAVTH7azk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1232&quot; height=&quot;898&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;좌 AB180 우 thirdandgrove&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 더 효과적인 Opt in view 노출 방법 찾기&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;웹 페이지 모서리 노출(Corner of Page)&lt;/li&gt;
&lt;li&gt;배너 또는 오버레이&lt;/li&gt;
&lt;li&gt;팝업 (Pop over)&lt;/li&gt;
&lt;li&gt;별도의 랜딩페이지 등&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 외에도 언급되지 않은 많은 형태의 Opt in view 가 존재한다. 자사의 서비스에 맞는 효과적인 view와 노출 방법을 고려해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 노출 타이밍 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구독 화면을 언제 노출 시키는가도 굉장히 중요한 포인트이다. 가령 아티클 위주의 서비스라면(ex. medium, thirdandfrove .etc) 유저가 아티클 페이지 접근 후 n초가 되는 시점에 노출시키는 게 가장 효과적일 것이다. 서비스 특성상 유저에게 우리의 아티클의 전문성과 퀄리티를 충분히 인지할 수 있는 시간을 주는 것이 전환의 핵심 팩터이기 때문.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 얼마나 자주 노출될지도 고려해야 한다. 만약 배너나 모서리에 노출되는 경우라면 유저의 사용성을 망치지 않는선에서 항상 노출되어도 문제가 되지 않을 것이다. 하지만 화면 전체를 가리는 팝업이나 새로운 랜딩페이지로 이동은 유저의 사용성을 악화시킬 수 있고 이는 Opt in rate을 오히려 저하시키는 원인이 될 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 간결하지만 전문성있게 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;광고론에서도 적용되는 simple is best는 여기서도 동일하게 적용된다. 따라서 Opt in view 에서는 자사 서비스를 강조할 수 있는 아주 강렬한 이미지와 핵심이 되는 짧은 문장들로 구성하는 게 좋다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 유저의 활동은 제한시키는게 좋다. 작성 내용이 많다면 작성 도중 변심할 수도 있고, 여러 항목에 작성하는 행위 자체에 거부감을 느끼기도 한다. 따라서 Opt in view에서 유저가 입력하는 정보를 최소화할수록 더욱 쉽게 전환된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본문에 예로 들어있는 netfilx 나 thirdandgrove 의 opt in view를 보면 E-mail 주소 단 하나만을 요구하는 것을 볼 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 혜택 뿌리기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Opt in rate 을 늘리기 위해서는 결국 유저도 얻어가는 게 있어야 한다. 가입 시 이용할 수 있는 할인 쿠폰을 제공하거나 한정판 굿즈 또는 서비스를 제공하는 행위 등이 opt in rate을 늘리고 가입을 증진시키는 방법이 될 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. A/B test&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 데이터 분석가 입장에서 가장 최적화된 opt in view 를 만들기 위해서는 앞서 언급한 여러 노력을 결합하여 무엇이 가장 효과적인지 test 하는 방법뿐이다. 이때 Opt in view 에는 다양한 조합이 가능하기 때문에 다양한 화면을 이용해 검증하는 게 중요하다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;IV. Reference&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://www.netflix.com/kr/&quot;&gt;netfilx official wepage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.thirdandgrove.com/insights/increase-opt-in-rates/&quot;&gt;Key Strategies for Increasing Your Website's Opt-In Rates&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.thebalancesmb.com/opt-in-rate-for-email-marketing-2531833&quot;&gt;What is a Good Opt-In Rate for Email Marketing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.campaignmonitor.com/resources/knowledge-base/what-is-a-good-conversion-rate-for-an-opt-in-landing-page/&quot;&gt;Is There an Ideal Conversion Rate for an Opt-In Landing Page?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Data/Data Analysis</category>
      <category>crm</category>
      <category>MKT</category>
      <category>Opt in rate</category>
      <category>Opt in view</category>
      <category>Opt view</category>
      <category>가입 유도</category>
      <category>마케팅 데이터 분석</category>
      <category>마케팅 정보 수신 동의</category>
      <category>정보 동의</category>
      <author>London_</author>
      <guid isPermaLink="true">https://leo-bb.tistory.com/81</guid>
      <comments>https://leo-bb.tistory.com/81#entry81comment</comments>
      <pubDate>Wed, 1 Dec 2021 12:53:41 +0900</pubDate>
    </item>
    <item>
      <title>다양한 SQL 스타일을 활용하여 계층형(hierarchy)  쿼리를 표현하는 방법</title>
      <link>https://leo-bb.tistory.com/80</link>
      <description>&lt;h1&gt;Difference between bigquery sql and other sql to write hierarchy sql&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;I. 계층형 데이터(Hierarchical data)&lt;/h2&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;WITH

employee AS (
    SELECT 40 AS id , 'london' AS name,  50 AS boss_id
        UNION ALL
    SELECT 50 AS id , 'lee' AS name,  10 AS boss_id
        UNION ALL
    SELECT 10 AS id , 'harry' AS name,  20 AS boss_id
        UNION ALL
    SELECT 20 AS id , 'leo' AS name,  NULL AS boss_id
        UNION ALL
    SELECT 70 AS id , 'lucas' AS name,  10 AS boss_id
        UNION ALL
    SELECT 60 AS id , 'james' AS name,  70 AS boss_id
        UNION ALL
    SELECT 30 AS id , 'danny' AS name,  50 AS boss_id
)

...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;다음과 같이 직원의 ID와 이름, 해당 직원의 직속상관(Direct manager) ID를 갖는 테이블이 있다고 가정해봅시다. 테이블에 의하면 10(harry)은 50(lee)과 70(lusas)의 상사입니다. 60(james)의 상사는 70(lucas)이 되고, 따라서 10(harry)은 60(james)의 간접 상사(Indirect manager) 이기도 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/58500111/139568549-9428bb41-a974-4dce-990c-43f55e4df0fd.png&quot; alt=&quot;스크린샷 2021-10-31 오후 1 58 00&quot; width=&quot;419&quot; height=&quot;394&quot; /&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 위와 같은 데이터 테이블을 도식화하면 사진과 같은 형태로 구성됩니다. 데이터 양이 적다면 한 두번의 self-join으로 관계를 표현하는데 문제가 없습니다. 하지만 만약 employee id 가 1,000명, 10,000명이 넘는다면 어떻게 해결할 수 있을까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;II. Loop In query&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분 독자님들이 위와 같은 구조를 보자마자 for/while을 이용해 다 연결하면 되지!라고 생각하셨을겁니다. 일반적인 프로그래밍 언어와 마찬가지로 sql문 역시 lopp문을 이용해 해당 계층구조를 쿼리로 표현해낼 수 있습니다. 자주 사용되는 Snowflake SQL, Postgre SQL, MySQL, Oracel SQL 은 모두 같은 방법을 사용하기 때문에 계층구조를 만드는 방법에 대한 이해만 있다면 어떤 SQL을 이용하더라도 당황하지 않고 쿼리를 작성할 수 있습니다. (제가 써 본 것 중에는 Bigquery sql 만 문법이 조금 다릅니다.)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. General SQL&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대중적인 SQL 에 사용되는 방법입니다.&lt;/li&gt;
&lt;li&gt;Recursive 문을 활용하여 CTE 셋에 반복적으로 Self-join 하여 계층을 구현합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;WITH

RECURSIVE hierarchy AS (
    SELECT
        0 AS num, # id기준 계층 0 시작
        id, # employee id
        boss_id # employee id의 boss id
    FROM
        employee 

        UNION ALL(DISTINCT)

    SELECT
        num +1 AS num,
        h.id,
        e.id,
    FROM
        hierarchy AS h
    LEFT JOIN
        employee AS e
        ON h.boss_id = e.id
)

SELECT * FROM hierarchy ORDER BY id, num&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Bigquery SQL&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Bigquery는 공식적으로 Recursive를 지원하지 않습니다. (다만 Loop/While 표현을 제공합니다.)&lt;/li&gt;
&lt;li&gt;가장 낮은 계층과 최상위 계층까지 depth 가 알려져있는가에 따라 접근 방식이 달라집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) depth 를 알고 있고, depth가 깊지 않은 경우&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 경우 left (self) join을 반복하는 게 가장 이상적입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT
    ot.id,
    t1.id,
FROM employee AS ot
LEFT JOIN employee AS t1 ON t1.id = ot.boss_id
LEFT JOIN employee AS t2 ON t2.id = t1.boss_id
UNION DISTINCT
...
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) depth 를 모르는 경우&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;계층도는 끝과 끝이 연결되지 않기 때문에(단방향이기 때문에) depth 가 아무리 깊어도 전체 unique key value 개수와 같은 점을 이용하여 while 문을 사용합니다.&lt;/li&gt;
&lt;li&gt;저도 빅쿼리에서 hierarchy 를 표현하기 위한 recursive query 작성 시 많은 고민을 했는데 &lt;a href=&quot;https://stackoverflow.com/questions/61234264/bigquery-construct-hierarchy-array-from-key-master-slave&quot;&gt;이 글&lt;/a&gt; 의 도움을 많이 받았습니다.&lt;/li&gt;
&lt;li&gt;논리는 Recursive 구문을 사용할 때와 동일합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;DECLARE counter INT64 DEFAULT 0;
DECLARE max_nums INT64;

SET max_nums = (SELECT COUNT(DISTINCT id) FROM employee); # 계층의 최대 길이는 전체 unique key 값과 같다.

CREATE TEMP TABLE result ( 
  id int64, 
  boss_ids ARRAY&amp;lt;int64&amp;gt;
);

WHILE counter &amp;lt;= max_nums # loop 회전수에 제한조건 
DO
  SET counter = counter + 1;

  CREATE OR REPLACE TEMP TABLE result AS (
    WITH
    base AS (
        SELECT 
            id, 
            boss_id
        FROM employee AS e
        LEFT JOIN result AS r USING(id)
        WHERE r.id IS NULL
    ),

    hierarchy_base AS (
        SELECT 
            l.id, 
            l.boss_id
        FROM base AS l
        LEFT JOIN base As r
        ON l.id = r.boss_id
        WHERE r.boss_id IS NULL
    ),

    hierarchy AS (
        SELECT 
            id, 
            ARRAY_AGG(boss_id IGNORE NULLS) AS boss_ids, 
        FROM hierarchy_base
        GROUP BY id

        UNION ALL

        SELECT 
            r.id, 
            ANY_VALUE(r.boss_ids) || COALESCE(ARRAY_AGG(DISTINCT hb.boss_id IGNORE NULLS), ARRAY&amp;lt;int64&amp;gt;[]) as boss_ids,
        FROM result AS r
        CROSS JOIN UNNEST(r.boss_ids) AS bis
        LEFT JOIN hierarchy_base AS hb
        ON hb.id = bis
        GROUP BY r.id
    )

    SELECT 
        id,
        boss_ids,
    FROM hierarchy
  );

END WHILE;

INSERT INTO result (id, boss_ids)
WITH 
bottom_level AS (
    SELECT 
        m1.boss_id AS id
    FROM employee AS m1
    LEFT JOIN employee AS m2
        ON m1.boss_id = m2.id
    WHERE m2.id IS NULL
)
SELECT 
    bl.id, 
    ARRAY&amp;lt;int64&amp;gt;[] AS boss_ids, 
FROM bottom_level AS bl
JOIN result AS r
ON r.id = bl.id
;

SELECT 
    id,
    b AS boss_id,
    COUNT(b) OVER (PARTITION BY id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS num
FROM result
CROSS JOIN UNNEST(boss_ids) AS b
WHERE id IS NOT NULL
ORDER BY id, num&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Programming/SQL</category>
      <category>bigquery</category>
      <category>bigquery hierarchy</category>
      <category>hierarchy query</category>
      <category>mysql hierarchy</category>
      <category>oracle hierarchy</category>
      <category>postgre hierarchy</category>
      <category>recursion query</category>
      <category>recursive</category>
      <category>recursive query</category>
      <category>snowflake hierarchy</category>
      <author>London_</author>
      <guid isPermaLink="true">https://leo-bb.tistory.com/80</guid>
      <comments>https://leo-bb.tistory.com/80#entry80comment</comments>
      <pubDate>Sun, 31 Oct 2021 21:48:44 +0900</pubDate>
    </item>
  </channel>
</rss>