<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>home/danie/study.log</title>
        <link>https://velog.io/</link>
        <description>wanna be idéal DE</description>
        <lastBuildDate>Sun, 13 Oct 2024 19:54:09 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>home/danie/study.log</title>
            <url>https://velog.velcdn.com/images/idle-danie/profile/8afab747-7124-4cae-b107-06bac8395b00/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. home/danie/study.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/idle-danie" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[알고 있으면 유용한 SQL function (feat. With문, 윈도우 함수)]]></title>
            <link>https://velog.io/@idle-danie/SQL-function-feat.-With%EB%AC%B8-%EC%9C%88%EB%8F%84%EC%9A%B0-%ED%95%A8%EC%88%98</link>
            <guid>https://velog.io/@idle-danie/SQL-function-feat.-With%EB%AC%B8-%EC%9C%88%EB%8F%84%EC%9A%B0-%ED%95%A8%EC%88%98</guid>
            <pubDate>Sun, 13 Oct 2024 19:54:09 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>최근 근무를 하면서나 개인적으로 사이드 프로젝트를 진행할 때도 Python 기반의 DataOps 업무를 주로 진행했기에, SQL 관련공부에 소홀해졌던 것 같다. 물론 데이터를 전처리하거나 데이터마트를 생성할 때, DW 상에서 SQL 쿼리를 꽤 작성했지만, 유사한 구조를 지속적으로 사용하다보니 이전에 자격증을 공부할 때 학습한 스킬이나, 이미 사용하던 것들도 점점 개념이 잊혀져 간 것 같아, 유용하게 사용한 SQL function에 대해 복기해보려고 합니다 🔥</p>
</blockquote>
<h1 id="with문">With문</h1>
<blockquote>
<p>With문을 사용하는 이유에 대해서 물어본다면, 가장 먼저 떠오르는 것은 ‘<strong>가독성</strong>’이다. 당장 <em>‘왜 사용했지?’</em> 라는 물음에, 기억을 더듬어보면 2개 이상의 서브쿼리(Subquery) 개수로 인한 가독성 이슈를 해결하고 싶어 활용했던 이유가 가장 컸던 것 같다. 개념을 설명하기 위해, 특정한 상황을 가정해봅시다 :)</p>
</blockquote>
<p>예를 들어, 직원 테이블에서 부서별 평균 급여를 계산한 후, 각 직원의 급여가 해당 부서 평균보다 높은 직원을 조회하는 <strong>서브쿼리</strong>가 포함된 질의문이 있다고 해봅시다.</p>
<pre><code class="language-sql">SELECT employee_id, salary, department_id
FROM employees e
WHERE salary &gt; (
    SELECT AVG(salary)
    FROM employees
    WHERE department_id = e.department_id
);</code></pre>
<p>위 쿼리에 With문을 적용하여 변환해보면 아래와 같을 것입니다 :)</p>
<pre><code class="language-sql">WITH DeptAvgSalaries AS (
    SELECT department_id, AVG(salary) AS avg_salary
    FROM employees
    GROUP BY department_id
)
SELECT e.employee_id, e.salary, e.department_id
FROM employees e
JOIN DeptAvgSalaries d
ON e.department_id = d.department_id
WHERE e.salary &gt; d.avg_salary;</code></pre>
<p>이런식으로, 일명 CTE(Common Table Expression)를 사용하여 쿼리를 작성하면 복잡한 쿼리를 더 읽기 쉽게 만들고, 무엇보다 쿼리의 각 부분을 특정 이름으로 <strong>재사용</strong>할 수 있게 해줍니다.</p>
<p>특히 서브쿼리를 2개 이상 사용했을 때, With문을 적용한다면 이러한 재사용성의 장점이 더욱 부각됩니다.
아래는 부서별 평균 급여를 계산한 후, 이 데이터를 두 번 참조하여 각각 급여가 평균보다 높은 직원과 낮은 직원을 조회하는 예제입니다.</p>
<pre><code class="language-sql">-- 서브쿼리로 평균보다 높은 직원 조회
SELECT e.employee_id, e.department_id, e.salary,
       (SELECT AVG(salary)
        FROM employees
        WHERE department_id = e.department_id) AS avg_salary
FROM employees e
WHERE e.salary &gt; (SELECT AVG(salary)
                  FROM employees
                  WHERE department_id = e.department_id)

UNION ALL

-- 서브쿼리로 평균보다 낮은 직원 조회
SELECT e.employee_id, e.department_id, e.salary,
       (SELECT AVG(salary)
        FROM employees
        WHERE department_id = e.department_id) AS avg_salary
FROM employees e
WHERE e.salary &lt; (SELECT AVG(salary)
                  FROM employees
                  WHERE department_id = e.department_id);</code></pre>
<p>이를 아래와 같이 With문을 적용하여, 사전에 정의한 <strong><code>DeptAvgSalaries</code></strong> (쿼리 실행 시 메모리 상에 생성된 가상의 테이블)를 재사용한다면, 쿼리 실행 횟수를 2회에서 1회로 줄일 수 있기 때문에 효율성(쿼리 성능)을 높일 것이라고 쉽게 예상할 수 있을 것입니다 🙂</p>
<pre><code class="language-sql">WITH DeptAvgSalaries AS (
    SELECT department_id, AVG(salary) AS avg_salary
    FROM employees
    GROUP BY department_id
)
-- 평균보다 높은 직원 조회
SELECT e.employee_id, e.department_id, e.salary, d.avg_salary
FROM employees e
JOIN DeptAvgSalaries d ON e.department_id = d.department_id
WHERE e.salary &gt; d.avg_salary

UNION ALL

-- 평균보다 낮은 직원 조회
SELECT e.employee_id, e.department_id, e.salary, d.avg_salary
FROM employees e
JOIN DeptAvgSalaries d ON e.department_id = d.department_id
WHERE e.salary &lt; d.avg_salary;</code></pre>
<blockquote>
<p>하지만 여기서 중요한 점은 <strong>With문의 동작 방식</strong>에 따라 효율성이 높아질 수도, 낮아질 수도 있다는 점입니다!</p>
</blockquote>
<p>일단, 앞서 설명했던 흐름에 따라 우리가 예상하는 With문의 동작 방식은 <strong><code>Materialize</code></strong> 일 것입니다. 해당 방식은 With문을 통해 일종의 임시 테이블을 생성하고, 서브쿼리를 매번 실행하는 대신, 사전에 생성한 임시 테이블을 재사용하는 경우입니다.</p>
<p>물론 옵티마이저의 최적화 과정에 따라 달라질 수 있겠지만, 경험 상 일반적으로 대부분의 DBMS 상에서의 With문의 동작은 <strong><code>Inline View</code></strong> 방식으로 처리됩니다. 해당 방식은 말그대로 <strong><code>View</code></strong> 와 같이 쿼리 그 자체만 저장되기 때문에, With문을 통해 정의한 테이블을 조회할 때마다 쿼리를 실행합니다. 따라서, <strong>실제로는 서브쿼리를 사용했을 때와 동일한 성능을 보일 것입니다.</strong> 대신, 가독성을 높일 수 있다는 점은 가져갈 수 있겠죠?</p>
<p><strong>그렇다면, <code>Materialize</code> 방식을 채택한다면 늘 성능 개선을 이끌 수 있을까요?
**
정답은 예상하셨다시피 **&#39;NO&#39;</strong> 입니다. 아래의 2가지 상황에서는 위 동작방식의 With문의 적용을 지양해야 할 수 있습니다 😅</p>
<ol>
<li><p><strong>결과 rows가 매우 클 경우</strong>: <strong><code>Materialize</code></strong> 방식의 쿼리 결과가 매우 큰 데이터셋을 포함하게 되면, 임시 테이블을 메모리에 생성하고 유지하는 데 드는 자원이 크게 증가하여 시스템의 부하를 가져올 수 있습니다. 상식적으로 <strong>임시 테이블을 생성(CREATE)</strong>하고, 쿼리가 끝난 후 <strong>임시 테이블을 삭제(DROP)</strong>하는 과정을 거치기 때문에, rows가 너무 많을 경우에는 이 과정 자체에서 시스템의 부하가 발생할 것입니다. 이때는 CTAS로 임시테이블을 생성하여, 인덱스나 파티셔닝을 적용하는 등의 별도의 최적화 과정을 고려하는 것이 좋을 것으로 예상됩니다 :)</p>
</li>
<li><p><strong>필터링이나 조건이 변경될 때</strong>: <strong><code>Materialize</code></strong> 방식으로 임시 테이블을 생성하면, CTE가 한 번 실행된 후 결과를 재사용합니다. 하지만 각 참조 시마다 조건이 달라질 경우, 그리고 심지어 rows가 많을 경우에는 임시 테이블이 효율적이지 않고 원하는 필터링이 잘 이루어지지 않을 확률이 높기 때문에 불필요한 데이터를 읽게 될 수 있습니다.</p>
</li>
</ol>
<p>그렇다면, 언제 With문을 <strong><code>Materialize</code></strong> 방식으로 처리하는 것이 좋을까요?</p>
<blockquote>
<p>*<em>I/O 비용이 크지만, 결과 dataset이 작을 경우 매우 유용하게 사용할 수 있습니다! 🎉
*</em></p>
</blockquote>
<p>예를 들어, 결과를 도출하는 과정에서 (예: join문 중첩) 복잡한 연산이 진행될 경우, 단 한번의 비용소모로 재사용의 이점을 극대화할 수 있고, 결과 rows가 작기 때문에 디스크나 메모리에 부하를 주지 않다는 점도 장점으로 작용할 것입니다.</p>
<h3 id="tip💡">TIP💡</h3>
<p>앞서 언급했듯이 대부분의 DBMS에서는 기본적으로 Inline View를 채택하기 때문에 (물론, 옵티마이저의 최적화 과정에 따라 다를 수 있음!), <strong><code>Materialize</code></strong> 방식을 강제하기 위해서는 각 DBMS 별로 최척화 방법에 대해 이해하고, 이에 따른 대처를 달리 해야 합니다. 
예를 들어, Oracle에서는 <code>/*+ MATERIALIZE */</code>를, MySQL에서는 <code>/*+ NO_MERGE(cte) */</code> 와 같은 방식으로 옵티마이저에게 힌트를 제시해야 합니다. 따라서, 쿼리 성능 개선을 위해 With문을 적용하려고 한다면, 먼저 사용하는 DBMS이 With문을 어떻게 처리하는지에 대한 이해가 선행되어야 합니다 :)</p>
<h1 id="window-함수">Window 함수</h1>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/9afce910-9529-47f9-9dfc-b50b826127ec/image.png" alt=""></p>
<p>Window 함수는 쿼리 내에서 특정 레코드 집합을 기준으로 데이터를 처리할 수 있게 하는 SQL 함수입니다. 
일반적인 집계 함수와 달리, 결과를 그룹으로 묶지 않고도 각 행에 대해 연산을 수행할 수 있기 때문에, 유용하게 사용할 상황이 꽤 발생합니다. 이번 포스팅에서는 대표적인 윈도우 함수인 <strong>RANK(), ROW_NUMBER(), LAG(), LEAD()</strong>에 대해서 소개하겠습니다.</p>
<h2 id="rank-순위-계산">RANK(): 순위 계산</h2>
<blockquote>
</blockquote>
<p><strong><code>RANK()</code></strong> 함수는 <strong>특정 기준</strong>에 따라 데이터의 <strong>순위를 계산</strong>합니다. 동일한 값에 대해 <strong>같은 순위</strong>를 부여하고, <strong>동일한 순위 이후에는 건너뛰는 순위</strong>가 적용됩니다. 예를 들어, 1위가 여러 개 있으면 다음 순위는 2위가 아닌 3위가 됩니다.</p>
<h3 id="예시-쿼리">예시 쿼리</h3>
<p>아래 쿼리는 직원의 급여를 기준으로 내림차순으로 정렬하여 순위를 계산합니다. 또한, 동일한 급여를 가진 직원들은 같은 순위를 부여받고, 다음 순위는 건너뛰게 될 것입니다.</p>
<pre><code class="language-sql">SELECT employee_id, salary, 
       RANK() OVER (ORDER BY salary DESC) AS salary_rank
FROM employees;</code></pre>
<h3 id="쿼리-실행-결과">쿼리 실행 결과</h3>
<table>
<thead>
<tr>
<th><strong>employee_id</strong></th>
<th><strong>salary</strong></th>
<th><strong>salary_rank</strong></th>
</tr>
</thead>
<tbody><tr>
<td>101</td>
<td>10000</td>
<td>1</td>
</tr>
<tr>
<td>102</td>
<td>10000</td>
<td>1</td>
</tr>
<tr>
<td>103</td>
<td>9500</td>
<td>3</td>
</tr>
<tr>
<td>104</td>
<td>9000</td>
<td>4</td>
</tr>
</tbody></table>
<p>앞서 설명한 동작 원리에 따라, 급여가 10,000인 직원들은 1위를 공유하고, 그 다음 순위는 2가 아니라 3이 됩니다.</p>
<h2 id="row_number-행-번호-반환">ROW_NUMBER(): 행 번호 반환</h2>
<blockquote>
</blockquote>
<p><strong><code>ROW_NUMBER()</code></strong> 함수는 <strong>특정 기준</strong>에 따라 <strong>각 행에 고유한 번호</strong>를 부여합니다. 동일한 값에 대해서도 <strong>중복 없이</strong> 번호가 매겨지며, 데이터의 순서에 따라 고유 번호가 부여됩니다.</p>
<h3 id="예시-쿼리-1">예시 쿼리</h3>
<p>아래 쿼리는 직원의 급여를 내림차순으로 정렬하고, 각 직원에게 고유한 행 번호를 부여합니다. 또한, 동일한 급여 값이라도 번호는 중복되지 않습니다.</p>
<pre><code class="language-sql">SELECT employee_id, salary, 
       ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_num
FROM employees;</code></pre>
<h3 id="쿼리-실행-결과-1">쿼리 실행 결과</h3>
<table>
<thead>
<tr>
<th><strong>employee_id</strong></th>
<th><strong>salary</strong></th>
<th><strong>salary_rank</strong></th>
</tr>
</thead>
<tbody><tr>
<td>101</td>
<td>10000</td>
<td>1</td>
</tr>
<tr>
<td>102</td>
<td>10000</td>
<td>2</td>
</tr>
<tr>
<td>103</td>
<td>9500</td>
<td>3</td>
</tr>
<tr>
<td>104</td>
<td>9000</td>
<td>4</td>
</tr>
</tbody></table>
<p>결과를 보면, ROW_NUMBER()는 순위와 달리 동일한 값에 대해 중복된 번호를 부여하지 않고, 고유한 번호를 차례대로 부여하는 것을 알 수 있습니다. </p>
<h3 id="upsert">UPSERT</h3>
<blockquote>
<p><strong>UPSERT</strong>는 Update + Insert를 합친 데이터 업데이트 방식입니다. 이름에서도 알 수 있듯이, <strong>UPSERT</strong>는 중복되는 값이 없다면 삽입(Insert)을 하고, 중복되는 값이 있다면 최신화(Update)를 하는 쿼리를 뜻합니다. 
데이터 엔지니어로서 ROW_NUMBER()를 처음 접했던 때는 <strong>UPSERT</strong> 방식을 DW 상에서 구현하려고 시도했을 때였던 것 같아, ROW_NUMBER()에 대한 이해를 돕기 위해 추가로 설명해보려고 합니다 🔥</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/66aa12e2-7514-44c1-9e51-c650b51d0423/image.png" alt=""></p>
<p>보통의 데이터웨어하우스 솔루션에서는 나름의 방식으로 UPSERT 방식을 지원하지만 (예: BigQuery의 merge into 문), 일단 이를 활용하지 않고 Incremental Update를 한다고 가정해봅시다 🫡 (참고로 Full refresh를 하는 경우는, 어차피 모두 재호출하여 적재하기 때문에 상관없을 것)</p>
<p>예를 들어, 최근 7일 간의 날씨 데이터에 대해 refresh하는 실행문을 작성해야하는 상황을 생각해본다면 아래와 같은 고려사항이 있을 것입니다.</p>
<p>보통의 RDBMS의 경우에는 DATE에 PK가 걸려있기 때문에 중복이 일어나지 않을 수 있지만, 데이터 웨어하우스 솔루션은 빅데이터에서의 성능을 보장하기 위해 일반적으로 PK를 지원하지 않기 때문에, 오늘 호출하고 내일 호출하면 데이터가 겹칩니다. 추가로, 같은 날짜에 대한 데이터라도 최근에 호출한 데이터의 신뢰도가 높기 때문에, 비교적 최근 데이터로 refresh 하려는 수요 또한 존재할 것입니다.</p>
<p>따라서, 이를 해결하기 위해서는 DATE가 동일한 것들끼리 grouping을 하고, created_date(DATE)가 큰 것부터 역순으로 sorting한 뒤, 일련번호를 붙여서 가장 최근 값만 채택을 해야합니다. 이때, <strong>ROW_NUMBER()</strong> 함수를 적용하여 아래와 같이 처리하면 됩니다.</p>
<pre><code class="language-sql">INSERT INTO weather_daily_table
SELECT date, temp, min_temp, max_temp, created_date
FROM (
    SELECT *, ROW_NUMBER() OVER (PARTITION BY date ORDER BY created_date DESC) seq
    FROM t
)
WHERE seq = 1</code></pre>
<p>위와 같이 처리하면, 아래 사진과 같은 과정을 통해 Primary Key Uniqueness 보장하면서 신규 데이터로 refresh할 수 있습니다.
<img src="https://velog.velcdn.com/images/idle-danie/post/3dbb7969-6680-4433-8e1f-b8edfe0ddc33/image.png" alt=""></p>
<h2 id="lag-이전-행의-값-참조">LAG(): 이전 행의 값 참조</h2>
<blockquote>
<p><strong><code>LAG()</code> *<em>함수는 *</em>현재 행 이전의 행 값</strong>을 가져옵니다. 주로 <strong>시계열 데이터</strong>나 <strong>이전 값과 비교</strong>가 필요한 상황에서 사용됩니다. 기본적으로 <strong>이전 행</strong>을 참조하지만, 참조할 행의 간격(몇 번째 이전 행)을 지정할 수 있습니다.</p>
</blockquote>
<h3 id="예시-쿼리-2">예시 쿼리</h3>
<p>아래 쿼리는 직원의 고용일(hire_date)을 기준으로 이전 직원의 급여를 가져옵니다. 또한, 이전 행이 없을 경우 기본값으로 0을 반환합니다.</p>
<pre><code class="language-sql">SELECT employee_id, salary,
       LAG(salary, 1, 0) OVER (ORDER BY hire_date) AS prev_salary
FROM employees;</code></pre>
<h3 id="쿼리-실행-결과-2">쿼리 실행 결과</h3>
<table>
<thead>
<tr>
<th><strong>employee_id</strong></th>
<th><strong>salary</strong></th>
<th><strong>prev_salary</strong></th>
</tr>
</thead>
<tbody><tr>
<td>101</td>
<td>8000</td>
<td>0</td>
</tr>
<tr>
<td>102</td>
<td>9000</td>
<td>8000</td>
</tr>
<tr>
<td>103</td>
<td>10000</td>
<td>9000</td>
</tr>
<tr>
<td>104</td>
<td>9500</td>
<td>10000</td>
</tr>
</tbody></table>
<p>첫 번째 행의 이전 행이 없기 때문에 prev_salary는 0을 반환하고, 이후 행들은 각각 이전 직원의 급여를 참조합니다.</p>
<blockquote>
<p>추가로, 이해를 돕기 위해 <strong>주식 가격 데이터</strong>에서 각 주식의 <strong>전일 대비 가격 변동</strong>을 계산해야 하는 상황을 가정한다면, *<em><code>LAG()</code> *</em>함수를 활용하여 아래와 같은 쿼리를 작성할 수 있을 것입니다.</p>
</blockquote>
<pre><code class="language-sql">SELECT stock_id, date, price, 
       LAG(price, 1) OVER (PARTITION BY stock_id ORDER BY date) AS prev_price,
       price - LAG(price, 1) OVER (PARTITION BY stock_id ORDER BY date) AS price_change
FROM stock_prices;</code></pre>
<h2 id="lead-다음-행의-값-참조"><strong>LEAD(): 다음 행의 값 참조</strong></h2>
<blockquote>
<p><strong><code>LEAD()</code></strong> 함수는 <strong>현재 행 이후의 값</strong>을 가져옵니다. 주로 <strong>시계열 데이터</strong>나 <strong>다음 값과 비교</strong>가 필요한 상황에서 사용됩니다. LAG()와 반대로 <strong>다음 행</strong>을 참조하며, 몇 번째 이후의 값을 참조할지 지정할 수 있습니다.</p>
</blockquote>
<h3 id="예시-쿼리-3">예시 쿼리</h3>
<p>아래 쿼리는 직원의 고용일을 기준으로 다음 직원의 급여를 가져옵니다. 또한 *<em><code>LAG()</code> *</em>함수와 비슷하게, 다음 행이 없을 경우 기본값으로 0을 반환합니다.</p>
<pre><code class="language-sql">SELECT employee_id, salary,
       LEAD(salary, 1, 0) OVER (ORDER BY hire_date) AS next_salary
FROM employees;</code></pre>
<table>
<thead>
<tr>
<th><strong>employee_id</strong></th>
<th><strong>salary</strong></th>
<th><strong>next_salary</strong></th>
</tr>
</thead>
<tbody><tr>
<td>101</td>
<td>8000</td>
<td>9000</td>
</tr>
<tr>
<td>102</td>
<td>9000</td>
<td>10000</td>
</tr>
<tr>
<td>103</td>
<td>10000</td>
<td>9500</td>
</tr>
<tr>
<td>104</td>
<td>9500</td>
<td>0</td>
</tr>
</tbody></table>
<h3 id="쿼리-실행-결과-3">쿼리 실행 결과</h3>
<p>마지막 직원의 경우 다음 행이 없기 때문에 next_salary는 0을 반환하고, 다른 행들은 각각 다음 직원의 급여를 참조합니다.</p>
<blockquote>
<p>추가로, 이해를 돕기 위해 <strong>로그 데이터</strong>에서 <strong>사용자 행동의 다음 단계</strong>를 분석해야 하는 상황을 가정한다면, <strong><code>LEAD()</code></strong> 함수를 적용한 아래와 같은 쿼리를 활용하여, 사용자가 현재 페이지를 방문한 후에 <strong>다음 페이지</strong>로 어디를 방문했는지 추적할 수 있을 것입니다. </p>
</blockquote>
<pre><code class="language-sql">SELECT user_id, page, timestamp,
       LEAD(page, 1) OVER (PARTITION BY user_id ORDER BY timestamp) AS next_page
FROM web_logs;</code></pre>
<h3 id="tip💡-1">TIP💡</h3>
<p><strong><code>LAG()</code> *<em>와 *</em><code>LEAD()</code></strong> 함수는 <strong>SCD Type 2</strong>와 같은 <strong>데이터 히스토리 관리</strong> 방식에서 데이터를 시간순으로 비교하거나 추적하는 할 때도 유용합니다. 이러한 함수들은 데이터의 변경 내역을 시간별로 분석하고, 각 데이터의 이전 상태나 다음 상태를 확인하는 데 활용될 수 있습니다.</p>
<p>참고로, 이전 올렸던 DBT 관련 포스팅에서 <strong>SCD Type</strong>에 대해 설명하였으니, 아래 포스팅을 확인해주시면 감사하겠습니다 :) 
<a href="https://velog.io/@idle-danie/Why-DBT-feat.-AB-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%92%88%EC%A7%88#scd-type-2">Why DBT? (feat. A/B 테스트, 데이터 품질)</a></p>
<h1 id="참고문헌">참고문헌</h1>
<ul>
<li><a href="https://livesql.oracle.com/ords/livesql/file/tutorial_GMLYIBY74FPBS888XO8F1R95I.html">https://livesql.oracle.com/ords/livesql/file/tutorial_GMLYIBY74FPBS888XO8F1R95I.html</a></li>
<li><a href="https://livesql.oracle.com/ords/livesql/file/content_CZUCT0MCOQZMJM7TI553HC8S9.html">https://livesql.oracle.com/ords/livesql/file/content_CZUCT0MCOQZMJM7TI553HC8S9.html</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.4/en/subquery-materialization.html">https://dev.mysql.com/doc/refman/8.4/en/subquery-materialization.html</a></li>
<li><a href="https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html">https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html</a></li>
<li><a href="https://www.stratascratch.com/blog/the-ultimate-guide-to-sql-window-functions/">https://www.stratascratch.com/blog/the-ultimate-guide-to-sql-window-functions/</a></li>
<li><a href="https://towardsai.net/p/l/databricks-upsert-to-azure-sql-using-pyspark">https://towardsai.net/p/l/databricks-upsert-to-azure-sql-using-pyspark</a></li>
<li><a href="https://schatz37.tistory.com/46">https://schatz37.tistory.com/46</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Why DBT? (feat. A/B 테스트, 데이터 품질)]]></title>
            <link>https://velog.io/@idle-danie/Why-DBT-feat.-AB-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%92%88%EC%A7%88</link>
            <guid>https://velog.io/@idle-danie/Why-DBT-feat.-AB-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%92%88%EC%A7%88</guid>
            <pubDate>Tue, 23 Jul 2024 21:06:39 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/idle-danie/post/8029411a-567a-45b5-b142-3a7a250e7071/image.png" alt=""></p>
<blockquote>
<p>ETL을 하는 이유는 결국 ELT를 하기 위함이며, 이때 <strong>데이터에 대한 품질 검증</strong>이 중요해집니다.</p>
</blockquote>
<p>데이터가 점점 대용량으로 가게되면, 데이터의 품질 이슈가 발생합니다. 이는 인사이트를 뽑아낼 때 속도가 지연되거나, 잘못된 결론을 내리는 이슈로 이어지기도 하므로, 데이터의 신뢰성과 향후의 인력 및 리소스 소모를 막기 위해서는 필수적으로 해결되어야 하는 문제라고 생각합니다.</p>
<p>일단 <strong>‘데이터 품질’</strong>에 대한 정의를 정리하고 가려고 합니다.</p>
<h1 id="데이터-품질이란">데이터 품질이란?</h1>
<p>최근 읽고 있는 책인 조 라이스와 맷하우슬리의 ‘견고한 데이터 엔지니어링’에 따르면, 데이터 관리는 원천 시스템 단계에서 필수적이지만, 특히 변환 단계에서는 더 중요하다고 말합니다. 저자는 크게 <strong>3가지 이유에서 데이터 품질의 중요성</strong>을 강조합니다. </p>
<p>(참고로 해당 책은 데이터 엔지니어링 업무를 하고 계시거나, 준비 중이신 분들 이라면 꼭 추천드립니다! 현업에서 일부 경험했지만 굉장히 모호했던 개념들이 많았는데, 이를 모두 깔끔하게 정리해주는 좋은 책입니다 ^^)</p>
<h2 id="정의적-정확성-ex-semantic-metric">정의적 정확성 (ex: semantic, metric)</h2>
<p>일단, 변환 단계에서는 <strong>정의적 정확성</strong>을 고려하는 것에 대한 중요성을 강조합니다. 이는 <em>변환이 예상되는 비즈니스 논리에 부합하는가?</em> 에 대한 물음에서 파생된 체크포인트인데, 이를 지키기 위해 변환과는 독립적으로 존재하는 semantic, metric 계층이라는 개념이 점점 더 대중화되고 있습니다. </p>
<p>또한, 런타임 시 변환에서 비즈니스 로직을 적용하는 대신, 이러한 정의를 변화 계층 이전에 독립 실행형 단계로 유지하는 추세로 가고있다고 합니다. 이러한 문제를 효과적으로 해결할 수 있는 툴이 바로 <a href="https://docs.getdbt.com/docs/use-dbt-semantic-layer/dbt-sl"><strong>Dbt</strong></a>입니다.</p>
<p>사실, 보통은 <code>semantic</code>, <code>metric</code> 계층을 구축하기 이전에 사용자에 대한 정보를 모으고, 값에 대한 명확한 표현 및 분리를 위해 <code>fact</code>, <code>dimension</code> 계층에 대한 테이블을 구성해놓습니다. </p>
<p>본 글에서는, 실제로 dbt를 통해 간단한 A/B 테스트 과정을 위한 파이프라인을 구축해 볼 예정인데, 이때 주로 사용할 계층인 <code>fact</code> 그리고 <code>dimension</code> 테이블에 대한 설명을 먼저 진행해보겠습니다. 이미 알고계신다면 넘어가셔도 좋습니다 :)</p>
<h3 id="fact-테이블">Fact 테이블</h3>
<p><code>fact</code> 테이블은 분석의 초점이 되는 <strong>양적 정보를 포함하는 중앙 테이블</strong>을 뜻합니다. 쉽게 말하면, <code>fact</code> 테이블은 <strong>값을 나타내는</strong> 테이블, <code>dimension</code> 테이블은 <strong>값을 설명하는</strong> 테이블이라고 생각하면 되고, <code>fact</code> 테이블은 일반적으로 <code>dimension</code> 테이블들과 외래 키로 연결되곤 합니다. (보통 <code>fact</code> 테이블의 크기가 훨씬 큽니다)</p>
<p>일반적으로 <code>fact</code> 테이블에는 <strong>비즈니스 결정</strong>에 사용될 매출 수익, 판매량, 이익과 같은 측정 항목을 포함시킵니다.</p>
<h3 id="dimension-테이블">Dimension 테이블</h3>
<p><code>dimension</code> 테이블은 <code>fact</code> 테이블에 대한 상세정보, 즉 <strong>특정 개체의 속성값을 제공</strong>하는 테이블입니다. (예: <code>고객</code>, <code>제품</code> 과 같은 테이블) 따라서, <code>fact</code> 테이블의 <strong>데이터에 맥락을 제공하여 다양한 방식으로 분석가능</strong>하게 해줍니다. 위에서 언급했다시피, <code>dimension</code> 테이블은 보통 PK를 가지며, <code>fact</code> 테이블에서 참조합니다. </p>
<h2 id="데이터의-결함">데이터의 결함</h2>
<p>두번째로는, 변환에는 데이터의 변형이 수반되므로 사용 중인 <strong>데이터에 결함이 없고 실제 데이터를 정확히 나타내는지 확인</strong>하는 것이 매우 중요하다고 합니다. 저는 이를 일종의 ‘<strong>데이터 정합성</strong>’을 지켜야 한다는 것에 대한 강조라고 생각합니다. 본 글에서는 데이터 정합성에 대한 논의는 중점적으로 진행하지 않을 것이긴 한데, 많은 사람들이 데이터 정합성에 대한 정의를 다른 개념과 혼동하는 것 같아 한 번 정리해보려고 합니다.</p>
<h3 id="데이터-정합성이란">데이터 정합성이란?</h3>
<p>일반적으로 데이터 정합성이라고 하면, data set에 대한 결손값이나, 중복값, 혹은 이상값을 얼마나 잘 검증했는지를 뜻한다고 생각합니다. 엄밀히 개념을 설명하자면, <strong><code>데이터 정합성</code></strong>이란 어떠한 데이터들에 대한 값이 서로 일치한지를 의미합니다.</p>
<h3 id="데이터-정합성-검증-예시">데이터 정합성 검증 예시</h3>
<p>보통, 원천 데이터에서 데이터를 가져올 경우 기본적으로 데이터가 올바르게 들어 왔는지에 대한 확인이 필요합니다. 이를 검증하기 위해서 OLTP → OLAP 형태의 데이터 파이프라인을 기준으로 설명하면 아래와 같은 2가지의 기본적인 검증 로직을 추가할 수 있습니다. </p>
<p>첫번째로, <strong>원천 데이터와 목적지 데이터의 건수가 같은지</strong> 비교합니다. </p>
<p>원천 데이터에 로그 적재 시간이 적재되어 있다고 가정하면, created_date에 특정기간에 대한 조건을 걸어서, 레코드를 카운트하고 이를 비교하여 중복이나 결손값을 확인할 수 있습니다. 이때, 원천 데이터가 인덱스가 지원되는 OLTP 기반의 DB라면 created_date에 인덱스를 설정하면 부하를 줄일수 있습니다. </p>
<p>또한, 이 과정에서 전체 레코드수를 계산하지 않고 특정 기간에 필터링을 거는 이유는 보통 원천데이터 소스는 OLTP 기반의 DB에 저장되어 있기 때문에, 계속해서 데이터가 변하기 때문에 기간별로 필터링하는 것이라고 할 수 있습니다. 추가로, 이러한 모니터링 결과는 데이터의 증감률이나, 최근 데이터의 존재여부를 가시적으로 확인할 수 있다는 장점도 있습니다.</p>
<p>두번째로는, <strong>목적지의 데이터의 유니크 키값</strong>을 이용하여 <strong>중복이 없는지</strong>를 비교합니다.</p>
<p>사실 앞서 검증한 데이터의 건수가 올바르다고 해도, 아래와 같은 SQL문을 통해 중복에 대한 검증은 추가로 진행되어야 합니다.</p>
<pre><code class="language-sql">SELECT 
    id, 
    COUNT(*) as cnt
FROM 
    raw_data.user_event
GROUP BY id
HAVING COUNT(*) &gt; 1</code></pre>
<p>간단하게 데이터 정합성의 개념과 검증 방법에 대해 알아보았는데, 가장 중요한 것은 이러한 데이터를 검증 하기 위한 과정에서 비용이 많이 발생하거나 시스템의 부하를 줄 경우를 고려하며 개발을 진행해야 한다는 것입니다. </p>
<p>이제 본론으로 돌아와, 데이터 품질에 대한 3번째 논의로 넘어가보려고 합니다 :)</p>
<h2 id="데이터-카탈로그">데이터 카탈로그</h2>
<p>마지막으로, <strong>데이터 변환 때문에 데이터 집합이 동일한 경로에서 어떻게 파생</strong>되었는지 알기 어려울 수 있다는 점이 있습니다. 이는 <strong>데이터 카탈로그</strong> 문제로 치환될 수 있는데, 이렇게 데이터의 계보를 유지하고 모니터링할 수 있는 것도 중요합니다. 이를 데이터 엔지니어가 직접 작업하고 운영한다는 것은 시간이 많이 소모되고 고된 일이라는 것을 아실 것입니다. 이러한 문제는 보통 오픈소스인 <a href="https://datahubproject.io/">Datahub</a>를 사용하여 해결합니다. </p>
<p>Dbt를 본격적으로 설명하기 전에, 먼저 짚고 넘어가야 할 <strong>히스토리를 유지하는 것에 대한 중요성</strong>과 5가지 <strong>SCD Type</strong>에 대한 개념을 정리해봅시다.</p>
<h1 id="history를-왜-유지해야-할까">history를 왜 유지해야 할까?</h1>
<p>OLAP 환경인 데이터 웨어하우스나 데이터 레이크에서 테이블들의 히스토리를 유지하는 것이 중요한 이유는 <strong>일부 속성들은 시간을 두고 변하게</strong> 되기 때문입니다. </p>
<p>보통은 <code>created_at</code> (생성시간으로 한번 만들어지면 고정)과 <code>updated_at</code> (마지막 수정 시간을 나타냄)과 같은 timestamp 필드를 생성하여 관리하는 것이 좋은데, <strong>컬럼의 성격에 따라 이를 어떻게 유지할 지에 대한 방법</strong>이 또 달라집니다. 이를 설명하기 위한 개념인 <strong>SCD Type</strong> (Slowly Changing Dimension) 5가지에 대해 설명드리겠습니다.</p>
<h2 id="scd-type-0">SCD Type 0</h2>
<p><code>SCD Type 0</code>는 한번 쓰고 나면 바꿀 이유가 없는 경우들을 뜻합니다. 예를 들어, 유저의 회원등록일이나 제품 첫 구매일과 같이 첫 이벤트 발생 시에 정해지면 갱신되지 않고 고정되는 필드들이 있을 것입니다.</p>
<h2 id="scd-type-1">SCD Type 1</h2>
<p><code>SCD Type 1</code>는 데이터가 새로 생기면, 덮어쓰면 되는 컬럼들에 대한 특성입니다. 또한, 처음 레코드 생성시에는 존재하지 않았지만, 나중에 생기면서 채우는 경우도 이에 해당합니다.</p>
<p>예를들어, 연간 소득 필드의 경우 지속적으로 덮어쓰면 큰 이상이 없을 것이고, 고객이 초기에 이메일을 저장하지 않았을 때, 후에 업데이트하는 경우도 예시로 적합합니다.</p>
<h2 id="scd-type-2">SCD Type 2</h2>
<p><code>SCD Type 2</code>는 특정 entity에 대한 데이터가 새로운 레코드로 추가되어야 하는 경우입니다.
예를 들어, 이커머스 서비스를 사용중인 유저의 등급이 변화했다고 가정해봅시다.</p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/5848ba8a-2e19-4085-9c4e-5ca73d90b274/image.png" alt=""></p>
<p>이때, rank가 update된 사항은 중요한 데이터가 될 수 있으므로 변경시간을 같이 추가하여 데이터 품질을 유지할 수 있을 것입니다. 참고로  <code>SCD Type 2</code> 은 이후 <code>dbt snapshot</code> 기능을 사용하면서 한번 더 언급할 특성입니다 :)</p>
<h2 id="scd-type-3">SCD Type 3</h2>
<p><code>SCD Type 3</code> 는 <code>SCD Type 2</code>의 대안으로, 특정 entity 데이터가 새로운 컬럼으로 추가되는 경우를 뜻합니다. </p>
<p>위의 경우와 동일한 상황이라면, 아래와 같이 새로운 컬럼 (previous_rank)를 생성하여 데이터 품질을 유지할 수 있을 것입니다. 이 경우에도 변경시간 또한 별도 컬럼으로 존재해야 할 것입니다.</p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/9fca4429-41f7-4ab7-912c-95b1ecd32c6b/image.png" alt=""></p>
<h2 id="scd-type-4">SCD Type 4</h2>
<p><code>SCD Type 4</code> 는 특정 entity에 대한 데이터를 새로운 Dimension 테이블에 저장하는 경우로, 일종의 <code>SCD Type 2</code>의 변종입니다. 예를 들어, 위 상황과 동일하다면 아래처럼 별도의 <strong>과거 이력 테이블</strong>을 생성하여, 아예 일반화하여 히스토리를 유지하는 방식입니다.</p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/0c8c0580-4573-4cd1-93e5-bca17af0a58e/image.png" alt=""></p>
<p>지금까지, 데이터 품질의 정의와 히스토리의 중요성, 그리고 <strong>SCD Type</strong> (Slowly Changing Dimension)에 대해 알아보았습니다. 특히, SCD Type에서 논의하는 지점들에 대해서 <strong><em>Dbt는 특정 세팅만 해주면 이와 같은 특성들에 대한 적절한 대처를 큰 스트레스(?)없이 효율적으로 진행할 수 있습니다.</em></strong> </p>
<p>그럼 이제 Dbt가 무엇인지, 어떤 방식으로 데이터 변환 과정에 관여하는지를 본격적으로 알아보겠습니다.</p>
<h1 id="what-is-dbt">What is Dbt?</h1>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/e90033bc-0ab0-48f3-a430-1158a1d8ce94/image.png" alt=""></p>
<p>출처: <a href="https://docs.getdbt.com/docs/introduction">https://docs.getdbt.com/docs/introduction</a></p>
<p>Dbt는 Data Build Tool의 약자로 ELT(Extract, Load, Transform)용 오픈소스 도구로, 데이터 웨어하우스 내에서 데이터 변환을 수행합니다. (ELT와 ETL의 개념에 대해서는 예전에 <a href="https://velog.io/@idle-danie/Data-Data-Team-Data-Engineer">Data와 Data Engineer의 역할</a>이라는 포스팅에서 짧게 언급하였으니 확인해주시면 감사하겠습니다 🙂) </p>
<p>여담이지만, Dbt의 등장으로 <strong>Analytics Engineer</strong>라는 직무 개념이 나왔다고 합니다. </p>
<p>우리가 흔히 알고 있는 BigQuery나 Snowflake와 같은 데이터 웨어하우스 솔루션들은 모두 지원하는 것으로 보입니다. 보통은 아래와 같이 <strong>Airlfow</strong>로 <strong>dbt</strong>를 스케줄링하고, <strong>DW</strong>와 연동된 <strong>dbt</strong>내의 여러 모델을 적절한 구조로 배치하고, 이를 실행한 결과를 통해 원하는 데이터를 확인할 수 있도록 데이터 파이프라인을 구성합니다.</p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/4b8f02af-0c7f-4067-9b4b-eb59e6ee46c6/image.png" alt=""></p>
<p>dbt는 <a href="https://www.getdbt.com/about-us">dbt Labs</a>에서 제공하는 Cloud 버전도 존재합니다. 워낙, 툴 자체가 가볍기 때문에 우리가 늘 고려하는 DW 비용(?)처럼 큰 무리를 주지 않고, 직접 관리하는 리소스 비용을 생각하면 비용이 적절하여 많이 사용한다고 합니다. 본 글에서는, dbt를 직접 설치(dbt core)하여 로컬에서 작업해보려고 합니다. </p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/7eba218c-e167-49cb-a397-ef9d0b41b370/image.png" alt=""></p>
<p>출처: <a href="https://docs.getdbt.com/docs/cloud/about-cloud/architecture">https://docs.getdbt.com/docs/cloud/about-cloud/architecture</a></p>
<h2 id="dbt의-특징">Dbt의 특징</h2>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/b7880f26-3bdb-467e-9dcb-4e9518a769d2/image.png" alt=""></p>
<p>Dbt는 아래와 같은 특징들을 가지고 있어, 데이터 엔지니어의 여러 요구조건을 만족시킬 수 있습니다. </p>
<ol>
<li><strong>데이터 변경 사항을 이해하기 쉽고 rollback 가능</strong>: SQL 기반의 변환 작업을 코드로 관리하여, 변경 사항을 쉽게 추적하고 rollback을 지원</li>
<li><strong>데이터 품질 테스트 및 에러 보고</strong>: 데이터 테스트 기능을 제공하여, 데이터 품질을 보장하고 에러를 사전에 감지 (ex: <code>dbt test</code>)</li>
<li><strong>Fact 테이블의 Incremental Update</strong>: Incremental Update 기능을 지원하여, Fact 테이블의 데이터를 효율적으로 갱신 가능</li>
<li><strong>Dimension 테이블 변경 추적 (history 테이블)</strong>:  SCD Type을 고려한 기능을 통해 Dimension 테이블의 변경 이력을 추적 가능</li>
</ol>
<p>이외에도 dbt에서는, <code>dbt docs generate</code> 명령어를 통해 편리하게 documentation 기능을 활용할 수 있고, <code>dbt docs serve</code> 명령어를 통해 데이터간 리니지를 쉽게 확인할 수 있는 기능도 지원합니다만, 본 글에서는 위의 특징들을 중점적으로 활용하고, 실제 구현하는 방식에 대해 설명해보려고 합니다 😄</p>
<h1 id="dbt로-ab-테스트해보기">Dbt로 A/B 테스트해보기</h1>
<blockquote>
<p>A/B 테스트 분석을 쉽게 하기 위한 ELT 테이블을 만들어보자!</p>
</blockquote>
<h2 id="도입">도입</h2>
<p>먼저, Dbt 파이프라인을 구축하기 위해 DW는 AWS Redshift를 채택하였고, 입력테이블은 아래와 같이 생성하였습니다. (참고로, dummy_data는 미리 random한 데이터를 생성하여 csv형태로 테이블을 만든 뒤, 삽입하였습니다)</p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/887af828-0aab-4b8b-8971-29e3b49f1199/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/bc6148b6-80f3-490f-a6bc-1efd8ad8d8d0/image.png" alt=""></p>
<ul>
<li><code>user_event</code>: 사용자, 날짜, 아이템 별로 impression이 있는 경우에 해당 정보를 기록하고, impression으로 부터 클릭, 구매, 구매 시 금액이 기록되는 데이터<ul>
<li>실제 환경에서는 이러한 aggregate정보를 로그 파일등의 소스로부터 만들어내는 프로세스가 필요할 것입니다.</li>
</ul>
</li>
<li><code>user_variant</code>: 사용자가 소속한 AB test variant를 기록한 데이터입니다. (예: control vs test)<ul>
<li>보통은 experiment와 variant 테이블이 별도로 존재하고, 언제 variant_id로 소속되었는지를 기록하는 timestamp 필드가 존재하는 것이 일반적입니다.</li>
</ul>
</li>
<li><code>user_metadata</code>: 성별, 나이 등의 메타정보를 담은 데이터<ul>
<li>이를 이용하여 여러 각도에서 AB테스트를 진행하여 다양한 인사이트를 도출할 수 있습니다.</li>
</ul>
</li>
</ul>
<p>최종적으로, 저희의 목표인 ELT 테이블 (생성테이블)은 미리 SELECT문으로 표현해보면, 아래와 같은 형태일 것입니다.</p>
<pre><code class="language-sql">SELECT
    variant_id,
    ue.user_id,
    datestamp,
    age,
    gender,
COUNT(DISTINCT item_id) num_of_items, -- 총 impression
COUNT(DISTINCT CASE WHEN clicked THEN item_id END) num_of_clicks, -- 총 purchase
SUM(paidamount) revenue -- 총 revenue
FROM raw_data.user_event ue
JOIN raw_data.user_variant uv ON ue.user_id = uv.user_id 
JOIN raw_data.user_metadata um ON uv.user_id = um.user_id 
GROUP by 1, 2, 3, 4, 5;</code></pre>
<p>생성 테이블: Variant 별 사용자에 대한 daily summary 테이블</p>
<ul>
<li>variant_id, user_id, datestamp, age, gender (5개의 필드에 대해 그룹바이)</li>
<li>총 impression, 총 click, 총 purchase, 총 revenue (sum 분석)</li>
</ul>
<p>추가적으로, <code>raw_data</code> 스키마 이외의 분석을 위한 용도로 <code>danie</code> 라는 스키마를 생성하였습니다. </p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/93ed7bc0-173a-4d42-8b48-143b8849e00d/image.png" alt=""></p>
<h2 id="dbt-설치">dbt 설치</h2>
<p>이제, dbt를 직접 설치해야 하는데 저는 dbt에 대해 조금 더 깊게 알아보기 위해, dbt Cloud 대신 dbt Core를 직접 로컬에 설치하여 진행해보려고 합니다 :) </p>
<p><em><del>(늘 느끼는 것이지만, 공식문서보고 직접 설치 파일을 하나하나 뜯어보는 것이 나한테 가장 빠른 학습 방법인 것 같다ㅎ)</del></em></p>
<p>아래 명령어를 실행하여 <code>dbt-redshift</code> 를 설치하게 되면, dbt Core를 설치함과 동시에 redshift와의 연동을 쉽게 진행할 수 있습니다.</p>
<pre><code class="language-bash">pip3 install dbt-redshift</code></pre>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/e39c83d1-b381-4df0-ab15-a52757fc0d2c/image.png" alt=""></p>
<p>참고로, python은 3.12x 버전, dbt는 1.8x를 사용하였고 각 버전 호환성은 아래 링크에 정리되어 있습니다.</p>
<p><a href="https://docs.getdbt.com/faqs/Core/install-python-compatibility#python-compatibility-matrix">What version of Python can I use? | dbt Developer Hub</a></p>
<h2 id="dbt-프로젝트-생성">dbt 프로젝트 생성</h2>
<p>아래 명령어를 실행하게 되면, dbt 프로젝트를 생성함과 동시에 Redshift connection을 위한 config를 설정할 수 있습니다.</p>
<pre><code class="language-bash">dbt init dbt_user_analysis</code></pre>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/f2c5363b-22da-405e-b477-7cbf8e6c2ddb/image.png" alt=""></p>
<p>아래의 사진처럼, 프로젝트 디렉토리 <code>dbt_user_analysis</code> 는 dbt_project.yml, tests, snapshots, models 등을 포함하고 있습니다. </p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/a502032c-5e90-4521-a6d4-775e7de431a4/image.png" alt=""></p>
<p>이제부터 dbt의 핵심 구성 및 기능이라고 할 수 있는 <code>models</code>, <code>tests</code>, <code>snapshots</code> 를 중심으로 글을 이어나가겠습니다.</p>
<h1 id="dbt-model">dbt model</h1>
<p><code>dbt model</code>은 ELT 테이블을 만들 때 기본이 되는 빌딩 블록이고, <code>Table</code>이나 <code>View</code> 혹은 <code>CTE</code>의 형태로 존재합니다. 또한, model은 일종의 입력, 중간 그리고 최종 테이블을 정의하는 곳이라고 생각하면 되는데, 구체적으로 설명하면 아래와 같습니다.</p>
<blockquote>
<p>dbt model은 raw, staging, core와 같은 일종의 계층에 대한 티어 개념이 존재하는데, raw → staging (src) → core의 순서로 이해하면 됩니다.</p>
</blockquote>
<h2 id="input">Input</h2>
<p>입력(raw)과 중간(staging, src) 데이터 정의</p>
<ul>
<li><code>raw</code>는 <code>CTE</code>로 정의</li>
<li><code>staging</code>은 <code>View</code>로 정의</li>
</ul>
<h2 id="output">Output</h2>
<p>최종 (core) 데이터 정의</p>
<ul>
<li><code>core</code>는 <code>Table</code>로 정의</li>
</ul>
<p>최종적으로, 위와 같은 데이터 정의들은 models 디렉토리 아래에 SQL파일로 존재합니다. </p>
<p>이제 A/B 테스트를 위한 최종 ELT 테이블을 위해 raw에서부터 core까지 이어지는 과정을 총 3가지 단계를 통해 실제 구현해보겠습니다. </p>
<h2 id="1단계">1단계</h2>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/b1bce943-8f97-4aa5-94f0-ed75e879d113/image.png" alt=""></p>
<p>DW에 존재하는 raw_data를 기반으로 데이터 품질을 검증하여 Staging단계에 올리는 과정입니다. 데이터 용량이 매우 클 경우에는, incremental_update 타입으로 append시켜 유지하면 됩니다. </p>
<p>src (base) 테이블은 아래와 같이 정의하였습니다. </p>
<p><code>src_user_event.sql</code></p>
<pre><code class="language-sql">WITH src_user_event AS (
    SELECT * FROM raw_data.user_event
) 
SELECT
    user_id, 
    datestamp, 
    item_id, 
    clicked, 
    purchased, 
    paidamount
FROM src_user_event</code></pre>
<p><code>src_user_variant.sql</code></p>
<pre><code class="language-sql">WITH src_user_variant AS (
    SELECT * FROM raw_data.user_variant
) 
SELECT
    user_id,
    variant_id 
FROM
    src_user_variant</code></pre>
<p><code>src_user_metadata.sql</code></p>
<pre><code class="language-sql">WITH src_user_metadata AS (
    SELECT * FROM raw_data.user_metadata
) 
SELECT
    user_id, 
    age, 
    gender, 
    updated_at
FROM 
    src_user_metadata </code></pre>
<p>초기 dbt 프로젝트를 생성하면 기본적으로 models 디렉토리에 example이 주어지는데, 이를 삭제하고 위의 src모델들을 추가하였고, <code>dbt run</code>을 실행해보면 아래와 같은 결과를 볼 수 있습니다. </p>
<p><code>dbt debug</code>
<img src="https://velog.velcdn.com/images/idle-danie/post/ba2846de-febf-461e-9031-0d993d39da78/image.png" alt=""></p>
<p><code>dbt run</code>
<img src="https://velog.velcdn.com/images/idle-danie/post/fc52df6a-e20b-463b-bbc1-35984bf2a40b/image.png" alt=""></p>
<p><code>Redshift</code></p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/892ad685-c498-4acc-89ef-769d31218c9c/image.png" alt=""></p>
<h2 id="2단계">2단계</h2>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/13fa3ef8-930d-459a-85d2-93edc0ac7830/image.png" alt=""></p>
<p>위 사진과 같은 과정을 진행하기 전에, dbt에서 중요한 개념인 <code>Materialization</code>에 대해서 알아볼 필요가 있습니다.</p>
<h3 id="what-is-materialization">What is Materialization?</h3>
<p><code>Materialization</code> 은 입력 데이터들을 연결해서 새로운 데이터를 생성하는 것 (방식)을 뜻하는데, 보통 여기서 추가 transformation이나 data cleanup을 수행합니다. </p>
<p>dbt는 4가지의 내장 materialization을 제공하는데, 이는 아래와 같습니다.</p>
<p> <code>View</code></p>
<ul>
<li>데이터를 자주 사용하지 않는 경우</li>
</ul>
<p><code>Table</code></p>
<ul>
<li>데이터를 반복해서 자주 사용하는 경우</li>
</ul>
<p><code>Incremental (Table Appends)</code></p>
<ul>
<li>Fact 테이블</li>
<li>과거 레코드를 수정할 필요가 없는 경우</li>
<li>Upsert 지원</li>
</ul>
<p><code>Ephemeral (CTE)</code></p>
<ul>
<li>한 SELECT에서 자주 사용되는 데이터를 모듈화하는데 사용</li>
</ul>
<p><code>materialized</code> format을 config를 통해서 테이블마다 정해줄 수도 있지만 dbt_project.yml 파일을 수정하여 진행해도 됩니다. 저는 dbt_project.yml의 models 부분을 아래와 같이 수정하여, 프로젝트의 테이블들은 기본적으로 view로 빌드되지만, dim 디렉토리에 있는 테이블들은 모두 table로 빌드되는 구조를 유지했습니다.</p>
<pre><code class="language-yaml">models:
  dbt_user_analysis:
    +materialized: view
    dim:
      +materialized: table</code></pre>
<h3 id="jinja-템플릿">Jinja 템플릿</h3>
<p>이제부터 model을 생성하는 SQL문에서는 <strong>Jinja 템플릿</strong>을 활용해 볼 것입니다.</p>
<p>dbt에서는 Jinja 템플릿을 기반으로 <code>ref 태그</code>와 <code>config</code>를 활용하여 dbt 작업의 효율성을 더할 수 있습니다. </p>
<p>아래의 SQL문에서는 <code>ref 태그</code>를 통해 dbt내의 다른 테이블들에 엑세스를 진행할 것이고, <code>config</code>문을 사용하여 <code>materialized</code> 종류와 입력을 할 때 스키마가 변경되었을 경우에 대응 전략을 정할 수 있는 <code>on_schema_change</code> 파라미터를 사용할 것입니다. </p>
<p><code>on_schema_change</code>는 <strong>fail, sync_all_columns, ignore, append_new_columns</strong> 등이 있는데, 나머지 방식을 사용한다고 해도 성공보장이 없기 때문에, fail 처리가 가장 안정적인 방식일 것입니다.</p>
<p><code>fact_user_event.sql</code></p>
<blockquote>
<p>그럼에도, 중복데이터가 생길 수 있기때문에 새로 생긴데이터만 incremental하게 업데이트 하려면 아래와 같이 별도의 where절을 사용하여 처리하면 된다.</p>
</blockquote>
<pre><code class="language-sql">{{ 
  config(
    materialized = &#39;incremental&#39;,
    on_schema_change=&#39;fail&#39; 
  )
}}
WITH src_user_event AS (
    SELECT * FROM {{ ref(&quot;src_user_event&quot;) }}
) 
SELECT
    user_id, 
    datestamp, 
    item_id, 
    clicked, 
    purchased, 
    paidamount
FROM 
    src_user_event

WHERE datestamp is not NULL 
{% if is_incremental() %}
    AND datestamp &gt; (SELECT max(datestamp) FROM {{ this }}) 
{% endif %}</code></pre>
<p><code>dim_user_metadata.sql</code></p>
<pre><code class="language-sql">WITH src_user_metadata AS (
    SELECT * FROM {{ ref(&#39;src_user_metadata&#39;) }}
) 
SELECT
    user_id, 
    age, 
    gender, 
    updated_at
FROM 
    src_user_metadata</code></pre>
<p><code>dim_user_variant.sql</code></p>
<pre><code class="language-sql">WITH src_user_variant AS (
    SELECT * FROM {{ ref(&#39;src_user_variant&#39;) }}
) 
SELECT
    user_id,
    variant_id 
FROM
    src_user_variant
</code></pre>
<p>추가로, config에서 <code>incremental_strategy</code> 파라미터도 설정할 수 있는데, 아래의 값들을 사용할 수 있습니다.</p>
<ul>
<li>append</li>
<li>merge</li>
<li>insert_overwrite</li>
</ul>
<p>상황에 따라, unique_key와 merge_update_columns필드를 사용하기도 하므로, 이러한 사항들을 고려하여<code>incremental_strategy</code> 를 적절하게 사용하면 좋을 것 같다.</p>
<p>이제 <code>dbt run</code> 을 통해 실행해보면 아래와 같이 테이블이 생성된 것을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/67d8a1c4-3c11-4514-8c43-59b86cad62e7/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/db90e55e-aa42-4333-a3d9-248acbed5003/image.png" alt=""></p>
<blockquote>
<p>참고로 <code>dbt compile</code>은 SQL code 까지만 생성하고 실행하지는 않는다. 여기서 말하는 SQL code는 target디렉토리에 존재한다.</p>
</blockquote>
<h2 id="3단계">3단계</h2>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/221ba076-f343-4639-ad57-e9fa4aa8ce26/image.png" alt=""></p>
<p>최종적으로 A/B 테스트의 결과를 도출하기 위한 작업을 진행해보겠습니다.</p>
<p>어느 variant에 속해있는지, 성별과 연령은 어떻게 분포하는지를 보기 위해 먼저, <code>dim_user_variant</code>와 <code>dim_user_metadata</code>를 JOIN하여 <code>dim_user</code> 테이블을 구성하려고 합니다. </p>
<p><code>dim_user.sql</code></p>
<pre><code class="language-sql">WITH um AS (
    SELECT * FROM {{ ref(&quot;dim_user_metadata&quot;) }}
), uv AS (
    SELECT * FROM {{ ref(&quot;dim_user_variant&quot;) }}
) 
SELECT
    uv.user_id, 
    uv.variant_id, 
    um.age, 
    um.gender
FROM 
    uv
LEFT JOIN um ON uv.user_id = um.user_id</code></pre>
<p>마지막으로, impressions, clicks, 구매 등에 대한 통계를 살펴보기 위해 <code>dim_user</code>와 <code>fact_user_event</code>를 조인하여 새로 생성한 <code>analytics</code> 디렉토리 밑에 <code>analytics_variant_user_daily</code> 테이블을 구성합니다.</p>
<p><code>analytics_variant_user_daily.sql</code></p>
<pre><code class="language-sql">WITH u AS (
    SELECT * FROM {{ ref(&quot;dim_user&quot;) }}
), ue AS (
    SELECT * FROM {{ ref(&quot;fact_user_event&quot;) }}
) 
SELECT
    variant_id,
    ue.user_id,
    datestamp,
    age,
    gender,
COUNT(DISTINCT item_id) num_of_items, 
COUNT(DISTINCT CASE WHEN clicked THEN item_id END) num_of_clicks, 
SUM(purchased) num_of_purchases, 
SUM(paidamount) revenue 
FROM 
    ue 
LEFT JOIN u ON ue.user_id = u.user_id GROUP by 1, 2, 3, 4, 5</code></pre>
<p>이제 <code>dbt run</code> 을 통해 실행해보면 A/B 테스트를 위한 최종적인 테이블이 생성된 것을 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/17db3560-5b30-4b60-8897-375d62b243a1/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/8e559383-5a2f-4001-aaa3-abc09ca41c6b/image.png" alt=""></p>
<h1 id="dbt-snapshots">dbt snapshots</h1>
<p><code>dimension</code> 테이블은 성격에 따라 데이터 변경이 자주 생길 수 있으므로 히스토리를 유지하는 것이 중요합니다. </p>
<p>dbt에서 <code>snapshot</code>은 테이블의 변화를 계속적으로 기록함으로써 과거 어느 시점이건 다시 돌아가서 테이블의 내용을 볼 수 있는 기능을 이야기 합니다. 이를 통해, 테이블에 문제가 있을경우 과거데이터로 rollback이 가능하고, 다양한 데이터 관련 문제에 대한 효율적인 디버깅 과정이 가능하게 합니다. </p>
<p>스냅샷을 사용하면 글의 서두에서 언급했던 <code>SCD Type 2</code> 와 같은 특성에 대해 히스토리를 유지하며 데이터 품질을 보장할 수 있습니다.</p>
<p> 기존 <code>Dimension</code> 테이블에서 특정 entity에 대한 데이터가 변경되는 경우 새로운 <code>Dimension</code> 테이블을 생성하여 히스토리를 유지하는데, 구체적인 과정은 아래와 같습니다.</p>
<p>기본 구조는 PK를 기준으로 변경시간이 현재 DW에 있는 시간보다 미래인 경우를 변경 감지 기준으로 삼고, updated_at을 기준으로 새로운 데이터가 업데이트되면 히스토리 테이블에 append하게 됩니다. </p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/f5ebccfb-4da7-4b8a-867b-3105c21ba97f/image.png" alt=""></p>
<p>snapshots 디렉토리에 아래와 같은 <code>scd_user_metadata.sql</code>을 작성하고, <code>dbt snapshot</code> 명령어를 실행하면 히스토리 테이블이 생성된 것을 확인할 수 있습니다. </p>
<p><code>scd_user_metadata.sql</code></p>
<pre><code class="language-sql">{% snapshot scd_user_metadata %}

{{ 
  config(
    target_schema=&#39;danie&#39;,
    unique_key=&#39;user_id&#39;, 
    strategy=&#39;timestamp&#39;, 
    updated_at=&#39;updated_at&#39;, 
    invalidate_hard_deletes=True
  ) 
}}
SELECT * FROM raw_data.user_metadata

{% endsnapshot %}</code></pre>
<p><code>dbt snapshot</code></p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/61fae803-9181-421b-b085-a188f3777738/image.png" alt=""></p>
<p><code>Redshift</code></p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/72580057-705b-4f10-b398-211683ad116d/image.png" alt=""></p>
<h1 id="dbt-tests">dbt tests</h1>
<p>여기서 말하는 테스트는 일반적으로 소프트웨어에서 말하는 테스트가 아닌 일종의 데이터 품질을 테스트하는 방법을 뜻합니다.</p>
<p><code>dbt test</code>의 종류를 나누자면, 아래와 같이 2가지로 나눌 수 있다.</p>
<h2 id="generics-test--내장-테스트"><strong>Generics Test:  내장 테스트</strong></h2>
<p><code>Generics test</code>는 Airflow operator처럼 꺼내쓸 수 있는 일종의 dbt 내장 테스트인데, <code>unique</code>, <code>not_null</code>, <code>accepted_values</code>, <code>relationships</code> 등의 테스트를 지원합니다. </p>
<p>models 디렉토리에 yaml 형태로 테스트 파일을 생성하면 되는데, 저는 아래와 같이 구성하였습니다.</p>
<p><code>schema.yml</code></p>
<pre><code class="language-yaml">version: 2
models:
- name: dim_user_metadata
columns:
- name: user_id
tests:
- unique - not_null</code></pre>
<p>이를 테스트로 활용하기 위해서는 <code>dbt test</code> 명령어를 실행하여 아래와 같은 결과를 확인할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/18fc8e97-88cc-4382-a2c5-bf4ca5abd484/image.png" alt=""></p>
<h2 id="singular-test-커스텀-테스트"><strong>Singular Test: 커스텀 테스트</strong></h2>
<p>기본적으로 SELECT로 간단하게 실행할 수 있고, 결과가 리턴되면 “실패”로 간주되는 테스트입니다.</p>
<p>tests 디렉토리에 생성하고, 있으면 안될 것들이 있는지 확인하는 정도로 활용합니다.</p>
<p>예를들어, Primary Key Uniqueness 테스트 (물론, <code>generic test</code>로 쉽게 검증 가능하지만 ^^)를 진행한다고 하면, tests 디렉토리에 아래와 같이 <code>dim_user_metadata.sql</code>파일을 작성하면 됩니다.</p>
<p><code>dim_user_metadata.sql</code></p>
<pre><code class="language-sql">SELECT *
FROM ( 
    SELECT
        user_id, 
        COUNT(1) cnt 
    FROM
        danie.dim_user_metadata
    GROUP BY 1
    ORDER BY 2 DESC
    LIMIT 1
)
WHERE cnt &gt; 1</code></pre>
<p>이제 위의 <code>generic test</code>에서 진행한 것과 동일하게 <code>dbt test</code> 명령어를 수행하면 되지만, 이렇게 되면 방금 진행했던 test들도 포함되어 실행되니, 아래처럼 특정 테이블을 지정하면 관련 테이블들에 대한 검증만 진행할 수 있습니다.</p>
<p><code>dbt test --select dim_user_metadata</code></p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/22283333-2ab1-4d65-8fb1-adcd7d1df4e0/image.png" alt=""></p>
<p>본 글을 작성하며 개발한 사항은 아래 <a href="https://github.com/idle-danie/dbt_session">github public repo</a>에 업로드하였으니 참고바랍니다 :)</p>
<h1 id="참고문헌">참고문헌</h1>
<ul>
<li>견고한 데이터 엔지니어링 (written by Joe Reis &amp; Matt Housley)</li>
<li><a href="https://docs.getdbt.com/">dbt Developer Hub</a></li>
<li><a href="https://burning-dba.tistory.com/130">RDBMS 데이터 적재 시 데이터 정합성 체크</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터베이스 아키텍처 분석 (feat.  MySQL, MongoDB, BigQuery)]]></title>
            <link>https://velog.io/@idle-danie/DB-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98%EB%B6%84%EC%84%9D</link>
            <guid>https://velog.io/@idle-danie/DB-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98%EB%B6%84%EC%84%9D</guid>
            <pubDate>Tue, 02 Jul 2024 16:20:20 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>MySQL, MongoDB, BigQuery 등 많은 종류의 DB를 사용해보고 수많은 쿼리를 작성해보았지만, 정확히 아킥텍쳐와 실행원리를 뜯어본 적은 없는 것 같아 각각의 DB (MySQL, MongoDB, BigQuery에 대하여 알아보고 비교해보려고 합니다. 본 글은 RealMySQL 8.0, 그리고 말미에 언급되는 레퍼런스를 참고하여 작성되었습니다 :)</p>
</blockquote>
<h1 id="mysql">MySQL</h1>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/13be7cd9-9a14-464f-a69d-3e277e82e2ad/image.png" alt=""></p>
<p>MySQL은 가장 대표적인 <strong>RDBMS</strong> (관계형 데이터베이스) 시스템이라고 할 수 있다.
먼저 MySQL 공식문서에서 제공하는 대략적인 아키텍쳐는 아래 그림과 같다.</p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/fea561a2-3238-45b8-ab79-7f5bf8df95b9/image.png" alt=""></p>
<p>크게 MySQL 엔진(두뇌 역할)과 스토리지 엔진 (팔, 다리 역할)으로 분류할 수 있다. 
MySQL 엔진에는 클라이언트의 요청을 처리하는 커넥션 핸들러와 쿼리파서, 전처리기, 옵티마이저 등이 있고, 스토리지 엔진에는 InnoDB (default) 엔진과 MyISAM 엔진이 있다.</p>
<h1 id="쿼리-실행-구조">쿼리 실행 구조</h1>
<blockquote>
<p>일단 우리가 파이썬에서 커넥션을 위한 라이브러리를 사용하든, Shell에 바로 접근하든 MySQL서버에서 쿼리를 실행할때의 과정은 아래와 같을 것이다.</p>
</blockquote>
<ol>
<li><strong>쿼리파서</strong>: 쿼리 파서는 클라이언트 요청으로 들어온 쿼리를 토큰으로 분리하여 트리 형태의 구조로 만들어낸다. 기본적인 문접오류는 이 과정에서 발견된다.</li>
<li><strong>전처리기</strong>: 위 과정에서 만들어진 파서 트리를 기반으로 쿼리 문장에 구조적인 문제점이 있는지 확인한다. 실제 존재하지 않거나 권한 상 사용할 수 없는 개체의 토큰은 해당 단계에서 걸러진다.</li>
<li><strong>옵티마이져</strong>: 요청으로 들어온 쿼리 문장을 저렴한 비용으로 가장 빠르게 처리할지를 결정한다.</li>
<li><strong>실행 엔진</strong>: 옵티마이저가 회사의 경영진이라면, 실행엔진은 중간관리자, 핸들러는 각 업무의 실무자로 비유할 수 있다. </li>
<li><strong>핸들러 (스토리지 엔진)</strong>: 핸들러는 MySQL 실행 엔진의 요청에 따라 데이터를 디스크를 저장하고 디스크로부터 읽어 오는 역할을 담당한다. </li>
</ol>
<p>아래는 책에서 소개한 옵티마이저가 GROUP BY를 처리할 때, 임시테이블을 사용했다고 가정한 예시입니다.</p>
<blockquote>
<ol>
<li>실행 엔진이 핸들러에게 임시테이블을 만들라고 요청</li>
<li>다시 실행 엔진은 WHERE 절에 일치하는 레코드를 읽어오라고 핸들러에게 요청</li>
<li>읽어온 레코드들은 1번에서 준비한 임시 테이블로 저장하라고 다시 핸들러에게 요청</li>
<li>데이터가 준비된 임시 테이블에서 필요한 방식으로 (예: GROUP BY) 데이터를 읽어 오라고 핸들러에게 다시 요청</li>
<li>최종적으로 실행 엔진은 결과를 사용자나 다른 모듈로 넘김</li>
</ol>
</blockquote>
<h1 id="mysql-스레딩-구조">MySQL 스레딩 구조</h1>
<blockquote>
<p>MySQL 서버는 스레드 기반으로 작동하며, 크게 포그라운드 스레드와 백그라운드 스레드로 구분된다. 
실행 중인 스레드의 목록은 performance_schema 데이터베이스의 threads 테이블에서 확인할 수 있다. </p>
</blockquote>
<h2 id="포그라운드-스레드">포그라운드 스레드</h2>
<blockquote>
<p>포그라운드 스레드는 최소한 MySQL 접속된 클라이언트의 수만큼 존재하며, 주로 각 클라이언트 사용자가 요청하는 쿼리 문장을 처리한다. 그래서 포그라운드 스레드를 클라이언트 스레드라고도 부른다. </p>
</blockquote>
<p>클라이언트 사용자가 작업을 마치고 커넥션을 종료하면 해당 커넥션을 담당하던 스레드는 스레드 캐시로 되돌아간다. 이미 스레드 캐시에 일정 개수 이상의 대기 중인 스레드가 있으면 스레드 캐시에 넣지 않고 스레드를 종료시켜 일정 개수의 스레드만 스레드 캐시에 존재하게 한다. 이때 스레드 캐시에 유지할 수 있는 최대 스레드 개수는 thread_cache_size 시스템 변수로 설정한다.</p>
<p>포그라운드 스레드는 데이터를 MySQL의 데이터 버퍼나 캐시로부터 가져오며, 버퍼나 캐시에 없는 경우에는 직접 디스크의 데이터나 인덱스 파일로부터 데이터를 읽어와서 작업을 처리한다. MyISAM 테이블은 디스크 쓰기 작업까지 포그라운드 스레드가 처리하지만, InnoDB 테이블은 데이터 버퍼나 캐시까지만 포그라운드 스레드가 처리하고, 나머지 버퍼로부터 디스크까지 기록하는 작업은 백그라운드 스레드가 처리한다.</p>
<h2 id="백그라운드-스레드">백그라운드 스레드</h2>
<p>앞서 언급한 것과 같이 백그라운드 스레드는 MyISAM과는 연관이 없는 사항이지만, InnoDB는 다음과 같이 여러 가지 작업이 백그라운드로 처리된다.</p>
<blockquote>
<ul>
<li>인서트 버퍼를 병합하는 스레드</li>
</ul>
</blockquote>
<ul>
<li>로그를 디스크로 기록하는 스레드</li>
<li>InnoDB 버퍼 풀의 데이터를 디스크에 기록하는 스레드</li>
<li>데이터를 버퍼로 읽어오는 스레드</li>
<li>잠금이나 데드락을 모니터링하는 스레드</li>
</ul>
<p>위의 작업 중 가장 중요한 것은 <strong>로그 스레드와 버퍼의 데이터를 디스크로 내렸는 작업을 처리하는 쓰기 스레드</strong>이다. MySQL 5.5 버전부터 데이터 쓰기 및 읽기 스레드의 개수를 2개 이상 지정할 수 있게하여, innodb_write_io_threads와 innodb_read_io_threads 시스템 변수로 스레드의 개수를 설정할 수 있다. InnoDB에서도 데이터를 읽는 작업은 주로 포그라운드 스레드에서 처리되기 떄문에 읽기 스레드는 많이 설정할 필요가 없지만 쓰기 스레드는 아주 많은 작업을 처리하기 때문에 일반적인 내장 디스크를 사용할 때는 2~4 정도, DAS나 SAN과 같은 스토리지를 사용할 때는 디스크를 최적으로 사용할 수 있을 만큼 풍분히 설정하는 것이 좋다고 한다. </p>
<p>사용자의 요청을 처리하는 도중 쓰기 작업은 버퍼링되어 처리될 수 있지만, 읽기 작업은 절대 지연될 수 없다. 책에서는 <strong><em>사용자가 SELECT 쿼리를 실행했는데, 10분 뒤에 결과를 돌려주겠다</em></strong>하는 데이터베이스는 없다는 예시를 들어 설명한다. </p>
<p>그래서 일반적인 DBMS에는 대부분 쓰기 작업을 버퍼링해서 일괄 처리하는 기능이 탑재돼 있으며, InnoDB 또한 이러한 방식으로 처리한다. 
InnoDB에서는 INSERT, UPDATE, DELETE 쿼리로 데이터가 변경되는 경우 데이터가 디스크의 데이터로 완전히 저장될 때까지 기다리지 않아도 된다. 
그러나, 앞서 포그라운드 스레드에서 설명했다시피 MyISAM 엔진은 포그라운드 스레드가 쓰기 작업까지 함께 하고, 일반적인 쿼리는 쓰기 버퍼링 기능을 사용할 수 없다.</p>
<h1 id="mongodb">MongoDB</h1>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/faeb38fe-0ee0-485d-93e7-6f2280a9f35c/image.png" alt=""></p>
<p>MongoDB는 대표적인 <strong>NoSQL</strong>중 하나이다. 나또한 토론 채팅 서비스를 구축하면서, 또는 업무에서 빠르게 비정형 데이터 위주로 구성된 DB 구축하고 싶을 떄 사용하였던 데이터베이스이다. 아무래도 objectid를 사용하여 어플리케이션 서버에서 1대1로 매칭할 수 있으므로 빠른 개발이 가능한 것 같다.</p>
<p>일단, MongoDB의 기본적인 구성은 아래와 같습니다. 
<img src="https://velog.velcdn.com/images/idle-danie/post/d920f387-28fa-4795-aa4f-e371e3b2e82b/image.png" alt=""></p>
<figcaption style="text-align:center; font-size:15px; color:#808080; margin-top:40px">출처: https://infohub.delltechnologies.com/en-us/l/mongodb-on-dell-powerflex-with-nvme-over-tcp/mongodb-architecture-1/</figcaption>

<ul>
<li><strong>Config 서버</strong>: 중개자 계층, 샤딩을 위한 메타 데이터를 저장한다. (데이터들의 위치 정보를 저장)</li>
<li><strong>Mongos 서버</strong>: MongoDB의 중개자 역할, Config 서버의 메타 데이터를 이용해 각 MongoDB에 데이터 접근을 도와준다.(라우터와 같은 역할)</li>
<li><strong>Mongod 서버</strong>: MongoDB의 데이터 서버로써, 서버 장애에 대비해 MongoDB 서버 안에 여러 개의 리플리카 셋 구조로 구성되어 있다.</li>
</ul>
<h2 id="what-is-documentdb">What is DocumentDB?</h2>
<p>일단 MongoDB는 도큐먼트 데이터베이스라고 보통 불린다. 표면적으로는, JSON 형식으로 데이터를 관리하고, 도큐먼트 단위로 데이터를 저장하기 때문일 것이다.
보통, 도큐먼트는 관계를 가지는 데이터를 중첩 도큐먼트와 배열을 사용하여 1개의 도큐먼트로 표현한다.</p>
<pre><code class="language-json">{
  &quot;name&quot;: &quot;John Doe&quot;,
  &quot;age&quot;: 30,
  &quot;isStudent&quot;: false,
  &quot;courses&quot;: [&quot;Math&quot;, &quot;Science&quot;, &quot;History&quot;],
  &quot;address&quot;: {
    &quot;street&quot;: &quot;123 Main St&quot;,
    &quot;city&quot;: &quot;Anytown&quot;,
    &quot;zipcode&quot;: &quot;12345&quot;
  }
}</code></pre>
<h2 id="bson-형식">BSON 형식</h2>
<p>JSON은 구문 분석 속도, 타입 명확성 부족, 공간효율성 등의 면에서 단점을 가지고 있다. 
따라서, 위와 같은 단점을 보완하기 위해 MongoDB는 BSON (Binary JSON) 형식을 도입하였다고 한다. 
그 결과, MongoDB에서 우리가 눈으로 데이터를 확인할 때는 JSON으로 보이지만, 나머지 상황에서는 모두 BSON 형태로 저장하고 전송한다고 한다. (MongoDB 초기에는 모두 JSON으로 관리하였다고 한다)</p>
<h3 id="bson의-장점"><strong>BSON의 장점</strong></h3>
<p>BSON(Binary JSON)은 JSON의 단점을 보완하기 위해 고안된 바이너리 형식의 데이터 포맷이다. BSON은 다음과 같은 장점을 가지고 있다.</p>
<p><strong>1. 빠른 구문 분석 속도</strong></p>
<ul>
<li>BSON은 이진 포맷이기 때문에, 컴퓨터가 데이터를 읽고 해석하는 데 더 효율적입니다. 이진 데이터를 직접 읽고 필요한 위치로 이동할 수 있기 때문에 구문 분석 속도가 빠르다.</li>
<li>BSON은 데이터를 타입과 함께 저장하므로, 파싱 시 데이터 타입을 명확히 알 수 있어 추가적인 변환 과정이 필요 없다.</li>
</ul>
<p><strong>2. 공간 효율성</strong></p>
<ul>
<li>BSON은 데이터 타입 정보를 포함하여 저장하므로, 숫자, 날짜, 바이너리 데이터 등을 효율적으로 저장할 수 있다.</li>
<li>키 이름을 길게 반복하는 대신, BSON은 짧은 형식으로 데이터를 저장하여 공간을 절약할 수 있다.</li>
<li>BSON은 정수, 부동 소수점 등의 숫자 데이터를 효율적인 이진 형식으로 저장한다. 예를 들어, 정수 1234는 4바이트로 저장되며, 이는 텍스트 형식보다 공간 효율적이다.</li>
</ul>
<blockquote>
<p>아래는 카카오 개발 컨퍼런스에서 MongoDB에 대하여 발표한 내용을 중심으로 정리한 글입니다. MongoDB 개념을 이해하고, 실제 카카오에서는 어떤 방식으로 MongoDB를 사용하는지 소개하는 유익한 영상이므로 MongoDB 입문자라면 한번씩 보시는걸 추천드립니다 :)
<a href="https://tv.kakao.com/channel/3693125/cliplink/414072595">https://tv.kakao.com/channel/3693125/cliplink/414072595</a></p>
</blockquote>
<h2 id="mongodb의-특징">MongoDB의 특징</h2>
<blockquote>
<p>MongoDB의 4가지 특징 (신뢰성, 확장성, 유연성, 인덱싱 지원)을 가지고 있습니다. </p>
</blockquote>
<h3 id="reliability-서버-장애에도-서비스는-계속-동작">Reliability: 서버 장애에도 서비스는 계속 동작</h3>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/a15cbfe4-c735-4a6b-85e2-fcfb20785f35/image.png" alt=""></p>
<figcaption style="text-align:center; font-size:15px; color:#808080; margin-top:40px">출처: https://tv.kakao.com/channel/3693125/cliplink/414072595</figcaption>

<p>일반적으로 MongoDB는 1개의 primary와 2개의 secondary로 구성된 <strong>레프리카셋 구조</strong>를 가지고 있어 데이터 복제와 고가용성을 구현하기 때문에 장애로부터 안정된 상태를 유지한다. (primary, secondary는 master나 slave라는 용어로도 쓰이기도 한다)</p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/1f1d48aa-5f3f-4b59-b0b1-91b2f8ce9996/image.png" alt=""></p>
<figcaption style="text-align:center; font-size:15px; color:#808080; margin-top:40px">출처: https://www.mongodb.com/resources/products/capabilities/replication
</figcaption>
primary는 데이터 읽기 및 쓰기 요청을 처리하고, secondary는 변경된 데이터를 복제하는 과정을 가지고 있다.

<p>만약 primary에서 장애가 발생하면 secondary가 primary가 된다. 따라서, 어느 한 서버에서 장애가 발생해도 데이터 유실을 막을 수 있고 application 서버는 별도로 이에 대한 처리를 하지 않아도 된다!</p>
<h3 id="scalability-데이터와-트래픽-증가에-따라-수평확장-가능">Scalability: 데이터와 트래픽 증가에 따라 수평확장 가능</h3>
<p>MongoDB는 데이터 증가로 더이상 하나의 레플리카셋에 못담을 상황일 때, <strong>데이터를 샤딩하여 분산</strong> 시켜준다. 또한, 이러한 과정이 서비스 중단없이 온라인으로 진행된다.
사실 MongoDB에서 <strong>auto-sharding</strong>을 지원한다고 했을 때 구체적으로 어떤 것을 의미하는지 몰랐는데, 이러한 점을 뜻하는 것으로 보인다)</p>
<p>위와 같은 과정을 용어로는 <strong>밸런싱</strong> 기능이라고 하는데, 이는 특정샤드에 데이터가 몰리면 다른 샤드로 데이터를 옮겨 전반적으로 모든 샤드가 균등하게 데이터를 저장할수 있게 하는 것을 뜻한다.</p>
<p>또한, 온라인상에서 데이터를 밸런싱하기 때문에 단일 레플리카셋에서 샤드로의 온라인 전환이 가능하며, 샤드의 확장 축소 모두 온라인에서 진행할 수 있다.</p>
<p>위에서 MongoDB의 구성에 대하여 언급하였는데, 구체적으로 어떤 과정으로 샤드클러스터에서 데이터를 다루는지 살펴보자.</p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/2cd7fbb3-598b-45f4-bf50-609b052d3370/image.png" alt=""></p>
<figcaption style="text-align:center; font-size:15px; color:#808080; margin-top:40px">출처: https://tv.kakao.com/channel/3693125/cliplink/414072595</figcaption>

<blockquote>
<ol start="0">
<li>샤드 클러스터에 저장되는 실제 데이터는 각 샤드 1, 2, 3에 나누어 저장</li>
</ol>
</blockquote>
<ol>
<li>어떤 데이터가 어떤 샤드에 있는지는 <strong>config</strong> 서버에 저장</li>
<li><strong>application</strong> 서버는 <strong>mongos</strong> 서버를 통해 샤드 클러스터에 접근</li>
<li><strong>mongos</strong> 서버는 <strong>config</strong> 서버와 통신하여 요청받은 데이터가 어느 샤드에 있는지 확인하고 해당 샤드에서 데이터를 조회하여 <strong>application</strong>에 전달</li>
</ol>
<p>application단에서는 샤드 클러스터 내부 동작을 알 필요없이 위와 같은 일련의 과정을 mongos 서버에서 알아서 해주기 때문에, application에서의 접근을 쉽게 만들어 준다.</p>
<h3 id="flexibility-여러가지-형태의-데이터를-손쉽게-저장">Flexibility: 여러가지 형태의 데이터를 손쉽게 저장</h3>
<p>MySQL과 같은 RDBMS의 경우, 새로운 특성을 추가하려면 컬럼을 별도로 추가해야 한다.
그러나, MongoDB는 스키마를 제공하지 않으므로 데이터 변경에도 유용하게 대처 가능하다.
예를 들어, 고객의 핸드폰 번호를 담는 테이블이 있을 때 기존에 없던 기기 OS 정보를 추가하고 싶을 때 혹은 핸드폰이 여러개일 때, 테이블을 따로 추가하지 않고 배열로 그냥 담아버리면 된다.</p>
<p>참고로 RDMBS에서의 데이터 단위와 MongoDB에서의 데이터 단위에 대한 대응관계는 아래와 같다.</p>
<table>
<thead>
<tr>
<th align="left"><center>RDBMS</center></th>
<th align="right"><center>MongoDB</center></th>
</tr>
</thead>
<tbody><tr>
<td align="left"><center>Database</center></td>
<td align="right"><center>Database</center></td>
</tr>
<tr>
<td align="left"><center>Tables</center></td>
<td align="right"><center>Collections</center></td>
</tr>
<tr>
<td align="left"><center>Rows</center></td>
<td align="right"><center>Documents</center></td>
</tr>
<tr>
<td align="left"><center>Columns</center></td>
<td align="right"><center>Fields</center></td>
</tr>
</tbody></table>
<p>이와 같은 특성으로, MongoDB는 데이터 구조를 한눈에 볼수 있고 application에서 다루는 객체와 1대1대응 관계로 이루어져 있어 개발자는 쉽게 데이터를 이해하고 빠르게 개발할 수 있다.</p>
<h3 id="index-support-다양한-조건으로-빠른-데이터-검색">Index Support: 다양한 조건으로 빠른 데이터 검색</h3>
<p>보통의 NoSQL에서는 데이터를 찾고 분산할 목적으로 키(PK)를 한개만 제공한다.</p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/63cf8280-ac51-41c8-950f-711f0aa7b5c4/image.png" alt=""></p>
<figcaption style="text-align:center; font-size:15px; color:#808080; margin-top:40px">출처: https://tv.kakao.com/channel/3693125/cliplink/414072595</figcaption>

<p>nosql에 customer_id를 PK로 지정한다고 가정하면, 데이터는 여러 서버에 customer_id를 기준으로 나누어서 저장할 것이다. 때문에, 특정 id값으로 검색하면 데이터가 저장된 서버를 알 수 있고 바로 조회가 가능하다. 그러나, 이름을 기준으로 검색할 상황이 있을 때는 해당 데이터 (정확히는 documents)가 어느 서버에 위치하는지는 모르기 때문에 모든 서버를 검색해야 하고, 대용량 데이터일 경우 비용은 매우 클 것이다.</p>
<p>MongoDB의 다양한 인덱스 제공 기능은 위와 같은 이슈를 방지할 수 있다.
필요한 필드에 필요한 만큼 인덱스를 생성할 수 있으므로, 위 예시의 경우에는 이름 field에도 인덱스를 생성하여 데이터를 빠르게 찾을 수 있다.</p>
<p>MongoDB가 제공하는 다양한 형태의 인덱스는 아래와 같다.</p>
<ul>
<li>Hashed 인덱스: 샤드 클러스터에서 데이터를 균등하게 분산하고자 할 때 사용하는 인덱스</li>
<li>TTL 인덱스: 제한시간을 설정하여 오래됀 데이터를 자동으로 지워주는 인덱스; 보관 기간이 정해진 데이터는 어플리케이션에서 굳이 관리 하지 않아도 된다.</li>
<li>Geospatial 인덱스: 공간 내의 거리나 범위를 다루기 위해 사용하는 일종의 공간 인덱스; 카카오 모빌리티 서비스에서 사용한다.</li>
<li>Multikey 인덱스, Partial 인덱스...etc</li>
</ul>
<h2 id="사용사례">사용사례</h2>
<blockquote>
<p>해당 발표에서 MongoDB의 여러 사용사례를 보여주었는데, 대용량 로그 저장 및 조회를 위해MongoDB를 도입한 사례와 MySQL에서 MongoDB로 이전한 사례가 MongoDB의 이해에 매우 도움될 것 같아 본 글에서 소개드리려고 합니다 :)</p>
</blockquote>
<h3 id="대용량-로그-저장-및-조회를-위한-mongodb를-도입">대용량 로그 저장 및 조회를 위한 MongoDB를 도입</h3>
<p>보통, 사용자의 요청은 로그로 저장되며 통계를 분석하여 서비스 개선에 활용하는데, 통계를 계산할 시에 특정 기간에 대한 전체 데이터를 읽는 용도로 HBase가 많이 사용됩니다.</p>
<p>하지만, 통계가 아니라 이름이나 물품명으로 등으로 검색해야 할 시에, HBase는 PK만 지원하므로 키가 걸리지 않은 데이터라면 전체 데이터를 읽어야 하고, 이는 높은 비용을 발생시킬 가능성이 농후합니다.</p>
<p>위와 같은 상황에서, MongoDB를 도입하여 <strong>필요한 필드에 필요한 만큼 인덱스를 부여</strong>할 수 있기 때문에 대용량 데이터가 있어도 빠르게 데이터를 찾을 수 있습니다.</p>
<p>따라서, application에서 로그를 저장할 때 MongoDB에 저장하거나, HBase의 로그데이터를 주기적으로 MongoDB에 업데이트하면 관리자가 다양한 조건으로 원하는 데이터를 빠르게 조회할 수 있습니다. </p>
<h3 id="db-이전-사례-mysql-→-mongodb">DB 이전 사례 (MySQL → MongoDB)</h3>
<p>사내 서비스 중 서비스가 확장되면서 기존의 MySQL로 유지하던 서비스의 한계점이 아래와 같이 나타났다고 합니다.</p>
<ul>
<li>상품데이터와 로그데이터의 혼재  </li>
<li>수TB의 디스크 -&gt; Scale-Up 한계</li>
<li>테이블 당 수백 GB -&gt; 스키마 변경시마다 10시간 이상 소요</li>
</ul>
<p>아무래도 RDBMS 특성 상 마지막 한계점은 극복하기 어려우므로 NoSQL로의 이전은 불가피 했을 것으로 보인다. 따라서 MongoDB로 이전하였고, 아래와 같은 성과를 보였다고 합니다.</p>
<ul>
<li>로그데이터 이관 -&gt; 63% 압축률  </li>
<li>스키마 변경 부담 제거</li>
<li>샤드 클러스터 구성 -&gt; 서비스의 확장에 따라 유연하게 Scale-Up</li>
</ul>
<p>아마 압축 방식은 default로 방식을 사용하였다고 했는데, 아마도 <strong>snappy</strong> 방식 일 것이다. 아래는 MongoDB에서 지원하는 여러 압축 방식인데, MongoDB는 snappy 방식이 기본값으로 설정되어있다.</p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/581f6ca0-2ac6-4296-90e5-dfb044ade2f1/image.png" alt=""></p>
<figcaption style="text-align:center; font-size:15px; color:#808080; margin-top:40px">출처: https://stackoverflow.com/questions/37614410/comparison-between-lz4-vs-lz4-hc-vs-blosc-vs-snappy-vs-fastlz</figcaption>

<h1 id="bigquery">BigQuery</h1>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/0d6f49bd-4ef6-4d47-9823-545cd859f430/image.png" alt="">
BigQuery는 RedShift, Snowflake와 같은 데이터웨어하우스 솔루션 중 하나로, 그 중 현재 가장 많이 쓰이는 시스템이라고 볼 수 있다. 
데이터웨어하우스는 대용량 데이터를 처리하기 위한 빅데이터 기반 데이터베이스 이므로, 굳이 데이터의 양이 크지 않다면 사용하지 않아도 된다. (오히여, 성능 저하가 올 수 도 있음)</p>
<p>BigQuery의 표면적인 특징은 아래와 같습니다. </p>
<ul>
<li>SQL로 데이터 처리 가능 (Nested fields, Repeated fields)</li>
<li>CSV, JSON, Avro, Parquet 등과 같은 다양한 데이터 포맷을 지원 </li>
<li>구글 클라우드 내의 다른 서비스들과 연동이 쉬움</li>
<li>배치 데이터 중심이지만 실시간 데이터 처리 지원</li>
<li><a href="https://cloud.google.com/bigquery/docs/nested-repeated?hl=ko">스키마 변경도 유연하게 대처 가능</a></li>
</ul>
<p>사실 대부분의 클라우드 기반의 데이터웨어하우스 솔루션들은 위 특징들을 만족한다. 특히 3번째 특징인 동일한 클라우드 내에서 다른 서비스들과 연동들이 쉬운 부분이 개발 생산성에 큰 기여를 한다. 예를 들어, AWS내에서 <strong>AWS Athena + S3 + RedShift</strong> 조합이나, GCP내에서 <strong>Cloud Storage + BigQuery + Cloud Scheduler</strong>  조합을 사용하여 파이프라인을 구축한다면 리소스 소모가 매우 줄어들 수 있다. 
물론, 필요성과 성능 및 비용을 고려하여 파이프라인 구성을 제작하는 것이 선결과제 임을 잊으면 안된다 :)</p>
<p>다음 step으로 넘어가기 전에, BigQuery를 사용할 때 알고 있으면 좋은 2가지를 설명해보려고 합니다.</p>
<blockquote>
<ul>
<li><strong>PK 제공 X</strong>: 보통의 데이터웨어하우스 솔루션들은 동일한 사항인데, RDBMS와 같이 인덱스나 PK와 같은 키는 제공되지 않는다. 애초에 대용량 처리를 위한 데이터웨어하우스 설계 목적에 반하기 때문이라고 생각된다. 따라서, 보통 Full scan으로 진행됩니다.</li>
</ul>
</blockquote>
<ul>
<li><strong>Delete 불가</strong>: BigQuery는 Delete문을 지원하지 않고, append 하는 것만 지원한다. 따라서, 한번 입력된 데이터는 변경되거나 삭제될 수 없습니다. 나 또한 실제로 BigQuery를 사용하며 필드를 잘못 추가하여 데이터가 몇개 유입되는 바람에 테이블을 지우고 다시 생성한 적이 있다ㅠ 근데, BigQuery는 Update문도 지원하지 않는다고 들었는데 나는 얼핏 단발성으로 사용했던 기억이 있는 것 같은데...? 추후에 관련 경험을 공유해보겠다 :)</li>
</ul>
<p>그럼 왜 기존의 데이터베이스를 쓰지 않고, 데이터 웨어하우스를 꼭 사용해야 하는걸까?
<img src="https://velog.velcdn.com/images/idle-danie/post/8ea385c7-3348-4cc2-b5d5-b43d35a8c4ee/image.png" alt="">
기존 데이터베이스는 읽기/쓰기 작업과 분석 작업에서 리소스 충돌이 발생할 수 있지만, BigQuery는 컴퓨팅 레이어와 스토리지 레이어를 분리하여 각 레이어가 독립적으로 성능과 가용성을 유지하며 동적으로 리소스를 할당할 수 있기 때문이다.
또한, BigQuery 스토리지는 고가용성을 위해 여러 위치 간에 자동으로 복제됩니다. 물론, 이 부분은 MongoDB도 유사한 특징을 가지고 있다.</p>
<p>그렇다면, 이제부터 구체적으로 왜 BigQuery가 대용량 데이터에 적합한 솔루션인지 3가지 특징을 통해 알아보자. 아마, 클라우드 기반의 데이터웨어하우스 특성에 대한 전체적인 이야기가 될 수 있다. </p>
<h2 id="열-기반-스토리지-columnar-storage">열 기반 스토리지 (Columnar Storage)</h2>
<p>이해하기 쉽게 RDBMS를 사용하는 사례를 하나 가정해보겠습니다. 
만약, MySQL에서 아래의 쿼리를 실행한다고 생각해보자.</p>
<pre><code class="language-sql">select product_id, client_id 
from payment_table 
where ~~~~
</code></pre>
<p>아무리 특정 컬럼을 select하고 where에 조건을 걸었다 하더라도, 일단 SSD에서 테이블 전체를 읽어와서 메모리에 올린 다음에 attribute를 필터링한다는 것을 알고 있을 것이다. 이때 발생하는 I/O 부담을 줄이려고, 우리는 보통 <strong>partitioning</strong>을 사용한다. 어쨋든, 이러한 이슈가 발생하는 이유는 일반적으로 RDBMS가 Row 기반으로 설계되었기 때문일 것이다. (필요한 컬럼만 똑딱떼어올 수 없다는 뜻이다)</p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/d1416146-0eb8-4afc-8fbf-ae389e68e826/image.png" alt=""></p>
<figcaption style="text-align:center; font-size:15px; color:#808080; margin-top:40px">출처 논문: Dremel: Interactive Analysis of Web-Scale Datasets (Google paper)</figcaption>

<p>하지만, BigQuery는 레코드 별로 저장하는 것이 아니라 컬럼 별로 저장하는 <strong>열 기반 스토리지 형식</strong>을 갖추고 있기 때문에 위와 같은 상황에서는 유리한 위치를 점한다. </p>
<p>아래의 그래프는 관련 논문에서 single-field에 대한 접근을 기준으로 실험한 결과이다. 참고로, Dremel은 Google의 BigQuery 서비스 에서 사용되는 쿼리 엔진이다.</p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/09af853b-a1e4-4c96-bb5f-970e4a9ae352/image.png" alt=""></p>
<figcaption style="text-align:center; font-size:15px; color:#808080; margin-top:40px">출처 논문: Dremel: Interactive Analysis of Web-Scale Datasets (Google paper)</figcaption>

<p>추가로, 컬럼 별로 같은 데이터 타입들로 이루어진 데이터들이 모아져있어, 컬럼별 압축률도 우수하며 컬럼을 추가하거나 삭제하는 것도 매우 빠릅니다. MongoDB 설명 말미에 각 압축 방식에 따른 Compression ratio에 대한 그림을 확인해보면 가장 높은 압출률이 10을 넘지를 못하지만, BigQuery의 경우 상회할 수 있다고 한다.</p>
<h2 id="스키마-구조-feat-repetition-level-definition-level">스키마 구조 (feat. Repetition Level, Definition Level)</h2>
<p>서두에 BigQuery의 표면적인 특징을 나열하면서, 스키마 변경도 유연하게 대처가 가능하다고 언급하였다. 아마 이런 의문이 들 수 있다.
<em><strong>&quot;중첩된 데이터에 대해 MongoDB는 Document-based DB니까 대처가 쉽게 가능할 것 같긴 한데, 열 지향 기반으로 설계된 BigQuery가 어떻게...?&quot;</strong></em></p>
<p>BigQuery는 중첩된 필드(Nested fields)와 반복된 필드(Repeated fields)를 효율적으로 저장하고 쿼리하기 위해 <strong>Repetition Level</strong>과 <strong>Definition Level</strong>을 사용한다. 이 두 개념을 통해 중첩된 데이터 구조를 열 지향 방식으로 평탄화하여 저장하고, 이를 효율적으로 쿼리할 수 있다. </p>
<p>일단 BigQuery가 이러한 구조를 어떻게 처리하는지 이해하기 위해, 먼저 논문에서 제공하는 두 개의 샘플 레코드(r1과 r2)와 Nested fields와 Repeated fields의 개념을 정리해보자.</p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/d4296a22-ec0b-4a5a-9bc7-7bcaf08b5852/image.png" alt=""></p>
<figcaption style="text-align:center; font-size:15px; color:#808080; margin-top:40px">출처 논문: Dremel: Interactive Analysis of Web-Scale Datasets (Google paper)</figcaption>

<h3 id="what-is-nested-fields--repeated-fields">What is Nested fields &amp; Repeated fields?</h3>
<ol>
<li><p><strong>Nested fields</strong>: 중첩된 필드는 한 레코드 내에 또 다른 레코드를 포함하는 구조를 말한다. 예를 들어, <code>Name</code> 필드 안에 <code>Language</code> 필드가 중첩된 구조를 가질 수 있다.</p>
</li>
<li><p><strong>Repeated fields</strong>: 반복된 필드는 한 필드 내에 여러 값을 가질 수 있는 구조를 말한다. 예를 들어, <code>Links</code> 필드 안에 여러 개의 <code>Forward</code> 링크를 포함할 수 있다.</p>
</li>
</ol>
<h3 id="repetition-level과-definition-level">Repetition Level과 Definition Level</h3>
<p>먼저, Repetition Level과 Definition Level의 개념은 아래와 같다.</p>
<ol>
<li><strong>Repetition Level (r level)</strong>: Repetition Level은 반복된 필드가 몇 번째 반복인지를 나타낸다. 중첩된 반복 구조에서 각 레벨의 반복 횟수를 표현한다.</li>
<li><strong>Definition Level (d level)</strong>: Definition Level은 특정 값이 정의되었는지 여부를 나타낸다. 중첩된 구조에서 각 필드가 정의된 깊이를 표현한다.</li>
</ol>
<p>이제, 아래의 사진에서 Repetition Level과 Definition Level의 역할을 정리해보자.</p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/30d15d47-28a1-4c94-b14f-a350864a70d2/image.png" alt=""></p>
<figcaption style="text-align:center; font-size:15px; color:#808080; margin-top:40px">출처 논문: Dremel: Interactive Analysis of Web-Scale Datasets (Google paper)</figcaption>

<p><strong>Repetition Level</strong>는 중첩된 필드나 반복된 필드가 여러 번 반복될 때 각 반복의 순서를 나타낸다. 예를 들어, <code>Links.Forward</code> 필드가 세 번 반복될 때 각 값의 반복 수준은 0, 1, 2가 된다.</p>
<p><strong>Definition Level</strong>는 필드가 정의되었는지 여부와 정의된 깊이를 나타낸다. 예를 들어, <code>Name.Language.Country</code> 필드가 정의되지 않은 경우, 해당 값은 NULL로 표시되며 Definition Level은 정의되지 않은 깊이를 나타낸다.</p>
<p>이와 같은 변환 과정을 통해, BigQuery는 중첩된 데이터를 효율적으로 쿼리할 수 있게 된다. 
그 결과, BigQuery는 열 지향 기반 데이터베이스임에도 불구하고 복잡한 계층적 데이터를 효과적으로 처리할 수 있다.</p>
<h2 id="트리-기반-분산-처리-tree-architecture-distribution">트리 기반 분산 처리 (Tree Architecture Distribution)</h2>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/a69d5539-d5b4-416e-8b31-a5567cee97bf/image.png" alt=""></p>
<figcaption style="text-align:center; font-size:15px; color:#808080; margin-top:40px">출처 논문: Dremel: Interactive Analysis of Web-Scale Datasets (Google paper)</figcaption>

<p>Dremel 논문 기준으로 설명하면 전체적인 과정은 아래와 같을 것입니다.
(root 서버는 mixer 0, intermediate 서버는 mixer 1이라는 용어로 쓰이기도 하니, 같은 개념이라고 이해해도 무관합니다)</p>
<blockquote>
<ol>
<li>쿼리 입력: root 서버에 SQL 쿼리를 입력</li>
<li>쿼리 분할: root 서버는 입력된 SQL 쿼리를 더 작은 SQL 문으로 분할하여 intermediate 서버로 전달</li>
<li>중간 서버 처리: intermediate 서버는 root 서버로부터 받은 쿼리를 다시 더 작은 단위로 쪼개어 leaf 서버로 전달</li>
<li>leaf 서버 처리: leaf 서버는 실제 데이터를 저장하고 있는 파일 시스템에서 데이터를 읽어와 쿼리 연산을 수행</li>
<li>결과 집계: leaf 서버는 연산 결과를 부모 노드(intermediate 서버)로 전달 -&gt; intermediate 서버는 받은 결과를 집계하여 루트 서버로 전달</li>
<li>최종 결과 반환: root 서버는 모든 결과를 취합하여 최종 쿼리 결과를 반환</li>
</ol>
</blockquote>
<p>실제 SQL 쿼리를 예시를 통해, 위 과정을 적용해보자.</p>
<pre><code class="language-sql">SELECT
  customer_id,
  SUM(purchase_amount) AS total_purchase
FROM
  ecommerce_data_transactions
WHERE
  purchase_date BETWEEN &#39;2020-01-01&#39; AND &#39;2022-12-31&#39;
GROUP BY
  customer_id
ORDER BY
  total_purchase DESC
LIMIT
  5</code></pre>
<blockquote>
<ol>
<li>디스크에서 customer_id, purchase_date, purchase_amount 컬럼만을 읽어들입니다.</li>
<li>leaf 서버: 각 leaf 노드에서 읽어들인 데이터를 가지고 2020~2022년 기간의 데이터를 고객 ID 단위로 그룹화하고, 해당 고객의 총 구매 금액을 계산합니다.</li>
<li>intermediate 서버: LEAF 노드에서 계산된 고객별 총 구매 금액을 합칩니다.</li>
<li>root 서버: intermediate 서버에서 올라온 모든 값을 합치면서 총 구매 금액을 기준으로 소팅합니다. 소팅이 끝난 후에, 상위 5명의 레코드를 반환합니다.</li>
</ol>
</blockquote>
<h1 id="마무리">마무리</h1>
<p>지금까지 MySQL, MongoDB, BigQuery의 아키텍처와 구동원리를 알아보았습니다. 모두 동일한 관점에서 글을 작성하지는 않았지만, 글을 통해 각 DB/DW가 가지고 있는 특징을 이해한다면  훨씬 효과적인 DB 선택과 그에 따른 효율적인 아키텍쳐를 구성할 수 있을 것입니다. </p>
<h1 id="참고문헌">참고문헌</h1>
<ul>
<li>Real MySQL 8.0 written by 백은빈, 이성욱</li>
<li><a href="https://meetup.nhncloud.com/posts/275">mongoDB Story 2: mongoDB 특징과 구성요소 : NHN Cloud Meetup</a></li>
<li><a href="https://tv.kakao.com/v/414072595">카카오와 MongoDB</a></li>
<li><a href="https://elky84.github.io/2018/09/26/mongodb_architecture/">MongoDB 서버 구축 및 아키텍쳐 - 엘키의 주절 주절</a></li>
<li><a href="https://blog.toktokhan.dev/mongodb-%EB%B6%80%EC%88%98%EA%B8%B0-27a0812f6bd6">MongoDB 부수기</a></li>
<li><a href="https://cloud.google.com/bigquery/docs/introduction?hl=ko">BigQuery 개요  |  Google Cloud</a></li>
<li>Dremel: Interactive Analysis of Web-Scale Datasets (Google paper)</li>
<li><a href="https://yeomko.tistory.com/27">갈아먹는 BigQuery [1] 빅쿼리 소개</a></li>
<li><a href="https://yeomko.tistory.com/28">갈아먹는 BigQuery [2] 빅쿼리 스키마 및 데이터 모델</a></li>
<li><a href="https://bcho.tistory.com/1117">구글 빅데이타 플랫폼 빅쿼리 아키텍쳐 소개</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[스트리밍 데이터처리에 대한 이해 (feat. EDA, CDC, Kafka)]]></title>
            <link>https://velog.io/@idle-danie/%EC%8B%A4%EC%8B%9C%EA%B0%84%EC%8A%A4%ED%8A%B8%EB%A6%AC%EB%B0%8D%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@idle-danie/%EC%8B%A4%EC%8B%9C%EA%B0%84%EC%8A%A4%ED%8A%B8%EB%A6%AC%EB%B0%8D%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Thu, 14 Mar 2024 17:05:02 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/idle-danie/post/a3dbbdc0-6e95-4b75-9a1b-54edcfa480ae/image.png" alt=""></p>
<h1 id="데이터-스트리밍의-중요성">데이터 스트리밍의 중요성</h1>
<p>데이터 스트리밍은 데이터가 생성되는 즉시 <strong>지속적으로 데이터를 수집, 처리, 분석하는 기술</strong>을 말합니다. 
해당 방법은 전통적인 배치 처리 방식과 대비되며, 실시간으로 정보를 파악하고 <strong>즉각적인 피드백 루프</strong>를 생성할 수 있다는 점이 가장 큰 장점입니다. 아래는 스트리밍 데이터 처리가 쓰이고 있는 예시입니다.</p>
<blockquote>
<p>예를 들어, 금융 서비스 업계에서는 주식 시장의 변동을 실시간으로 감지하고 자동으로 거래를 실행하는 알고리즘 거래 시스템이 필요합니다. 또한, 전자상거래 플랫폼은 사용자의 클릭스트림 데이터를 실시간으로 분석하여 개인화된 상품 추천을 제공해야 합니다.</p>
</blockquote>
<p>이처럼 최신에 들어서는, 웬만한 기업은 서비스의 원활한 운영과 실시간 데이터 분석을 위해 스트리밍 데이터를 처리하려고 노력하고,  <strong><code>Kafka</code></strong>나 <strong><code>Flink</code></strong>와 같은 프레임워크를 하나 이상 채용하여 사용하고 있습니다. </p>
<p>본 글의 말미에는 가장 인기있는 스트리밍 데이터 처리 프레임워크인 <strong><code>Kafka</code></strong> 를 소개할 예정입니다. 
그 전에, 스트리밍 데이터에 대한 이해를 위해 필요한 주요 개념들 (ex: <strong><code>EDA</code></strong>, <strong><code>Topic</code></strong>, <strong><code>Micro-batch</code></strong>, etc)을 먼저 설명하려고 합니다.</p>
<p>가장 먼저 스트리밍 데이터 플랫폼을 알아보기 위해 필요한 개념은  <strong><code>이벤트 기반 아키텍처</code></strong> (Event-Driven-Architecture) 입니다.</p>
<h1 id="이벤트-기반-아키텍처-event-driven-architecture">이벤트 기반 아키텍처 (Event-Driven-Architecture)</h1>
<p>비즈니스는 수도 없이 많은 동적인 사건들의 발생으로 이루어져 있습니다. 유저의 회원가입부터 로그인, 장바구니 담기, 상품 구매, 상품 재구매 까지 모두 이벤트의 일종이라고 볼 수 있습니다. </p>
<p><strong><code>이벤트 기반 워크플로우</code></strong>에서는 데이터 엔지니어링 수명 주기의 다양한 부분에서 이벤트를 생성, 라우팅, 소비의 프로세스가 진행됩니다. </p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/dabffdd3-9d20-4e29-be43-92400ad916ea/image.png" alt=""></p>
<p><strong><code>이벤트 기반 아키텍처</code></strong>에서는 위에서 설명한 워크플로우를 기반으로 다양한 서비스간 통신을 진행합니다. </p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/8374a3c6-b913-4ae7-83c5-d91117b07662/image.png" alt=""></p>
<p>출처: <a href="https://akasai.space/architecture/about_event_driven_architecture/">https://akasai.space/architecture/about_event_driven_architecture/</a></p>
<p>해당 아키텍처의 장점은 이벤트의 상태를 여러 서비스에 분산시키기 때문에, 오프라인 상태가 되거나, 분산 시스템에서 노드에 장애가 발생하거나, 여러 소비자 또는 서비스가 동일한 이벤트에 접근하도록 할 때 유용하다는 점입니다. 보통, 서비스가 느슨하게 결합된 경우에는 항상 이벤트 중심 아키텍처를 포함합니다. </p>
<p><strong><code>이벤트 기반 아키텍처</code></strong>는 아래와 같은 이유로 인기가 더욱 높아지고 있습니다.</p>
<blockquote>
<ol>
<li>이벤트 기반 아키텍처의 핵심 계층인 메시지 큐와 이벤트 스트리밍 플랫폼은 클라우드 환경에서 더 쉽게 설정하고 관리 가능</li>
<li>실시간 분석을 직접 통합하는 어플리케이션인 데이터 앱의 증가</li>
</ol>
</blockquote>
<p>핵심적으로, <strong><code>이벤트 기반 아키텍처</code></strong>는 이벤트가 어플리케이션 작업을 트리거하고 실시간에 가까운 분석을 제공할 수 있습니다. </p>
<p>또한, 데이터 수집과 변환 단계에서도 원천 시스템에서 메시지 전달을 위해 사용했던 것과 같은 이벤트 스트리밍 플랫폼을 사용하여 실시간 분석을 위한 데이터를 처리할 수 있다는 점이 주요합니다. </p>
<h1 id="메시지-큐와-스트리밍-플랫폼">메시지 큐와 스트리밍 플랫폼</h1>
<p>이벤트 기반 아키텍처와 관련하여 <strong><code>메시지 큐</code></strong>와 <strong><code>스트리밍 플랫폼</code></strong>이라는 용어가 있는데, 혼용되는 경우가 많습니다. </p>
<h2 id="메시지--메시지-큐">메시지 &amp; 메시지 큐</h2>
<p>먼저, <strong><code>메시지</code></strong>는 이벤트 기반 시스템에서 불연속적이고 단일한 신호의 일종으로, 둘 이상의 시스템 간에 전달되는 원시 데이터입니다. </p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/32d3982c-5720-4a33-9808-f813f10e4ae0/image.png" alt=""></p>
<p><strong><code>메시지 큐</code></strong>는 게시 및 구독 모델을 사용하여 개별 시스템 간에 데이터를 비동기적으로 전송하는 메커니즘입니다.</p>
<p>기본적으로, 데이터는 <strong><code>메시지 큐</code></strong>에 게시되어 1명 이상의 구독자에게 전달되고, 메시지가 수신되면 큐에서 삭제됩니다. 
<strong><code>메시지 큐</code></strong>를 사용하면 어플리케이션과 시스템을 서로 분리할 수 있어, 보통 MSA 환경에서 많이 사용됩니다. </p>
<p><strong><code>메시지 큐</code></strong>는 메시지를 버퍼링해 일시적인 부하 급증을 처리하고, 복제 기능을 갖춘 분산 아키텍처를 통해 메시지를 내구성 있게 보존합니다. 메시지 큐에서는 메시지 순서 지정, 전달 빈도에 대한 개념이 중요합니다.</p>
<h3 id="메시지-순서-지정"><strong>메시지 순서 지정</strong></h3>
<p>메시지가 생성, 전송, 수신되는 순서는 다운스트림 사용자에게 큰 영향을 미칠 수 있는데, 사실 분산 메시지 큐의 순서는 까다로운 문제입니다. </p>
<p><strong><code>메시지 큐</code></strong>는 종종 모호한 순서와 선입선출 (FIFO) 개념을 적용합니다. 당연히 엄격하게 FIFO가 적용되면 먼저 들어온 메시지가 후에 들어온 메시지보다 먼저 수신될테지만, 고도로 분산된 시스템에서 잘못된 순서로 게시되거나 수신될 수 있습니다.</p>
<p>당연한 말이겠지만, 위 문제는<strong><code>메시지 큐 기술</code></strong>이 순서를 보증해줘야만 해결해 줄 수 있다. (ex: <strong><code>AWS SQS</code></strong> 표준 큐의 오버헤드 관리)</p>
<h3 id="전달-빈도"><strong>전달 빈도</strong></h3>
<p>메시지는 정확히 한번 발송되면 사용자가 메시지를 확인한 뒤 메시지는 사라지며 다시 전달되지 않고, 적어도 한번 송신된 메시지는 여러 명의 유저 또는 같은 유저가 2회 이상 소비할 수 있습니다. </p>
<p>따라서, 사용자가 메시지를 완전히 처리했지만, 처리를 확인하기 전에 실패할 때에 대한 대응을 적절히 할 수 있을 것입니다.</p>
<p>이상적으로는 시스템이 <strong><code>멱등성</code></strong> 상태여야 하고, 그러한 상태라면 메시지를 여러 번 처리한 결과와 한번 처리한 결과는 같을 것입니다. </p>
<h2 id="스트림--스트리밍-플랫폼">스트림 &amp; 스트리밍 플랫폼</h2>
<blockquote>
<p>이벤트는 ‘일반적으로 어떤 상태의 변화와 같은 무언가가 발생한 것’이고 단일 이벤트는 <code>key</code>, <code>value</code>, <code>timestamp</code> 와 같은 특성을 포함합니다.</p>
</blockquote>
<p><strong><code>스트림</code></strong>은 <strong>이벤트 레코드의 추가 전용 로그</strong>입니다. 이벤트가 발생하면 순서대로 누적되며, <strong><code>timestamp</code></strong> 또는 id로 이벤트 순서를 정렬할 수 있고, 여러 이벤트에 걸쳐 무슨 일이 일어났는지를 살펴볼 때 <strong><code>스트림</code></strong>을 사용할 수 있습니다. </p>
<p><strong><code>메시지</code></strong>와 <strong><code>스트림</code></strong>은 pub → sub구조로 메시지를 전달한다는 점에서 유사하지만, 가장 큰 차이는 메시지 큐가 주로 특정 전달을 보장하는 <strong><code>메시지 라우팅</code></strong>에 사용된다는 것이다. 이벤트 스트리밍 플랫폼은 정렬된 레코드 로그에서 데이터를 수집하고 처리하는데 사용됩니다.</p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/22d13891-bfae-4868-9b69-cd7824f8356b/image.png" alt=""></p>
<p><strong><code>스트림</code></strong>의 <strong>추가 전용 특성</strong>으로 인해 레코드는 장기적인 보존 기간에 걸쳐 유지되므로, 여러 레코드의 집계 또는 스트림 내 특정 시점으로 <code>rollback</code> 기능과 같은 레코드의 복잡한 프로세스를 실행할 수 있습니다.</p>
<p><strong><code>스트림</code></strong>을 처리하는 시스템은 <strong><code>메시지</code></strong>를 처리할 수 있으며, <strong><code>스트리밍 플랫폼</code></strong>은 메시지 전달에 자주 사용됩니다.
또한, 위에서 언급했다시피 메시지 분석을 수행할 때는 <strong><code>메시지</code></strong>를 <strong><code>스트림</code></strong>에 축적하고, 나중에 특정 값에 대한 추세와 통계를 확인할 수 있습니다.</p>
<p>이제부터는, <strong><code>이벤트 스트리밍 플랫폼</code></strong>에서 몇 가지 중요한 특성에 대해 설명하려고 합니다. <strong><code>Kafka</code></strong>를 포함해서 거의 모든 실시간 처리 프레임워크에서도 동일한 특성과 개념이 통용됩니다.</p>
<h3 id="토픽-topic">토픽 (Topic)</h3>
<p><strong><code>스트리밍 플랫폼</code></strong>에서 생산자는 관련 이벤트 모음인 <strong><code>토픽</code></strong>에 이벤트를 스트리밍합니다.</p>
<p>사실, 토픽을 여러 개 두거나 하나의 토픽에 여러 생산자를 할당하거나 하는 제한은 없고 보통 개발자가 환경에 맞게 설정하면 됩니다. 나의 경우로는, 암호화폐 거래소 별로 비트코인의 시장가를 포함한 여러 정보를 Kafka를 이용하여 실시간 처리하는 프로젝트를 진행하였는데 (엄밀히 말하면 마이크로 배치ㅎ), 거래소 별로 토픽을 할당하였다.</p>
<p><em>‘견고한 데이터 엔지니어링’</em>  책에서는 토픽 개념을 설명하기 위해서 일종의 <strong><code>주문처리 시스템</code></strong>을 가정하고, <strong><code>web orders</code></strong>라는 <strong><code>토픽</code></strong>, <strong><code>marketing</code></strong>과 <strong><code>fulfillment(주문처리)</code></strong>라는 생산자를 설정합니다.</p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/9a0a7d43-9104-471c-a96a-9b34be808137/image.png" alt=""></p>
<p><strong><code>fulfillment</code></strong> 구독자 (sub)은 이벤트를 사용해 주문 처리 프로세스를 트리거하고, <strong><code>marketing</code></strong>은 실시간 분석을 실행하거나 마케팅 캠페인을 조정하기 위해 ML 모델을 학습하고 실행할 것입니다. 글 말미에 추가로 설명하겠지만, 현업에서 위와 같은 상황이라면 보통 <strong><code>web orders</code></strong> 토픽 생산자를 <strong><code>Kafka</code></strong>, <strong><code>marketing</code></strong>을 processing하는 역할로 <strong><code>Spark Streaming</code></strong>로 처리하는 사례가 많은 것 같다.  </p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/8e9ff7c8-db62-4690-b652-da10881eedf8/image.png" alt=""></p>
<h3 id="스트림-파티션-stream-partition">스트림 파티션 (Stream Partition)</h3>
<p><strong><code>스트림 파티션</code></strong>은 스트림을 여러 스트림으로 <strong><code>분할(partition)</code></strong>한 것입니다. </p>
<p>메시지는 <strong><code>파티션 키</code></strong>에 따라 파티션 간에 분산되고, <strong><code>파티션 키</code></strong>가 같은 메시지는 항상 같은 파티션에 저장됩니다.
일종의 <strong><code>MongoDB의 해쉬 인덱스</code></strong>나 <strong><code>해쉬 테이블</code></strong>처럼, 함께 처리해야 할 메시지끼리 동일한 <strong><code>파티션 키</code></strong>를 설정하여 분산시킨다고 생각하면 됩니다. </p>
<p>그러나, 우리는 이때 자연스럽게 특정 파티션에 메시지가 몰리는 상황이 아마 떠오를 것입니다. 이것을 <strong><code>hotspotting</code></strong>현상 이라고 합니다. <strong>파티션 하나에 전달되는 메시지의 수가 불균형한 현상</strong>인데, 파티션 키를 적절하게 조절하며 해당 현상을 방지해야할 것이다.</p>
<h3 id="내결함성과-복원성">내결함성과 복원성</h3>
<p><strong><code>이벤트 스트리밍 플랫폼</code></strong>은 일반적으로 다양한 노드에 스트림이 저장되는 <strong>분산형 시스템</strong>입니다. 따라서, <strong>노드가 다운되면 다른 노드가 해당 노드를 대체해 스트림에 계속 접근</strong> 할 수 있는데, <em>‘견고한 데이터 엔지니어링’</em>  책에서는 이러한 특성을 <strong>내결함성과 복원성</strong>이라고 칭합니다. 이러한 특성 때문에, <strong><code>이벤트 스트리밍 플랫폼</code></strong>은 이벤트 데이터를 안정적으로 생성, 저장 및 수집할 수 있는 시스템이 필요할 때 좋은 선택이 될 수 있습니다.</p>
<h1 id="스트리밍-스토리지">스트리밍 스토리지</h1>
<blockquote>
<p>최초의 실시간 쿼리 엔진은 어떻게 보면 OLTP기반의 트랜잭션 데이터베이스라고 볼 수 있다. 하지만, 대량에 데이터에 걸쳐 실행되는 분석 쿼리의 경우 확장 및 잠금의 제한으로 대용량에 적합한 쿼리를 실행시키기 어렵습니다.</p>
</blockquote>
<p>기본적으로 스트리밍 데이터는 배치성 데이터와 스토리지 요구 사항이 다릅니다.
(배치 데이터에 대한 스토리지 개념은 <strong><code>DW/DL</code></strong>, <strong><code>HDFS</code></strong>, <strong><code>Spark</code></strong>, <strong><code>iceberg</code></strong>에 대한 내용과 결합하여 다른 포스팅을 통해 소개드리려고 합니다 🙂)</p>
<p><strong><code>메시지 큐</code></strong>의 경우, 저장된 데이터는 일시적이며 일정기간이 지나면 사라질 것으로 예상되지만, <strong><code>Kafka</code></strong>와 같이 분산되고 확장 가능한 <strong>스트리밍 프레임워크는 매우 오랜 기간 동안 스트리밍 데이터를 보존할 수 있습니다.</strong></p>
<p><strong><code>Kafka</code></strong>는 자주 접근하지 않는 오래된 메시지를 객체 스토리지에 푸쉬해 무기한 데이터 보존을 지원하는데, 이러한 기능은 <strong><code>AWS Kinesis</code></strong>나 <strong><code>GCP Pub/Sub</code></strong>도 지원한다. </p>
<p>위와 같은 데이터 스토리지 특성은 <strong><code>리플레이</code></strong> 개념과 연관되어 있습니다. </p>
<h2 id="리플레이">리플레이</h2>
<p><strong><code>리플레이</code></strong>는 스트리밍 스토리지 시스템의 표준 데이터 검색 메커니즘입니다.</p>
<p><strong><code>리플레이</code></strong>를 사용하면 스트리밍 시스템에 저장된 과거 데이터의 범위를 반환할 수 있기 때문에, 시간범위에 걸쳐 배치 쿼리를 실행하거나 스트리밍 파이프라인에서 데이터를 재처리하는 데 사용할 수 있습니다.</p>
<p><strong><code>Kafka</code></strong>와 <strong><code>AWS Kinesis</code></strong>나 <strong><code>GCP Pub/Sub</code></strong>은 모두 이벤트 보존 및 리플레이를 지원하지만, <strong><code>RabbitMQ</code></strong> 같은 경우에는 일반적으로 모든 사용자가 메시지를 소비한 후 메시지를 삭제합니다.</p>
<h2 id="stream-to-batch-스토리지-아키텍처">Stream-to-Batch 스토리지 아키텍처</h2>
<p><strong><code>Stream-to-Batch 스토리지 아키텍처</code></strong>는 <a href="https://www.databricks.com/kr/glossary/lambda-architecture">람다 아키텍처</a>와 유사점이 있지만, 해당 아키텍처는 기본적으로 스트리밍 스토리지 시스템의 토픽을 통과하는 데이터는 여러 소비자에게 기록되는 형태입니다.</p>
<p>이러한 소비자 중 일부는 스트림에 대한 통계를 생성하는 실시간 처리 시스템일 것입니다. </p>
<p>배치 데이터의 흐름은 아래와 같을 것입니다.
<strong>배치 스토리지 사용자</strong>는 장기 보전 및 배치 쿼리를 위해 데이터를 쓸 것이고 <strong>배치 소비자</strong>는 시간이나 배치의 크기에 대한 설정 가능한 트리거에 근거해 S3 객체를 생성할 수 있는 <strong><code>AWS Kinesis Firehose</code></strong>와 같은 시스템이 될 수 있습니다.</p>
<p><strong><code>BigQuery</code></strong>와 같은 시스템은 <strong>스트리밍 데이터를 스트리밍 버퍼로 수집합니다</strong>. 이 스트리밍 버퍼는 자동으로 컬럼형 객체 스토리지로 다시 초기화 될 것이고, 쿼리 엔진은 스트리밍 버퍼와 객체 데이터 모두에 대한 원활한 쿼리를 지원해 사용자에게 거의 실시간에 가까운 최신 테이블 뷰를 제공할 수 있습니다.</p>
<h1 id="스트리밍-데이터-처리-시-고려해야-할-사항">스트리밍 데이터 처리 시 고려해야 할 사항</h1>
<h2 id="메시지와-스트림은-늘-유동적">메시지와 스트림은 늘 유동적</h2>
<p>스트리밍 데이터에 대한 수집은 <strong>데이터의 게시, 소비, 재게시, 재소비와 함께 비선형적일 수 있습니다.</strong> 따라서, <strong>data flow를 충분히 고려하여 실시간 데이터 파이프라인을 구성</strong>해야합니다.</p>
<p>또한, 대부분의 데이터의 형태가 <strong>JSON과 같은 반정형 구조</strong>일 확률이 높으므로, <strong>페이로드의 스키마가 즉흥적으로 변경</strong>될 수 있습니다. 생산자 쪽에서 새로운 필드를 도입한 구조의 데이터를 송신하면, 대상이 되는 DW나 처리 파이프라인이 인식하지 못하는 상황은 좋지 않기 때문에 <strong>유연한 스키마를 유지하는 것이 중요합니다.</strong></p>
<p><em>추가로, 후에 설명할  <strong><code>CDC 시스템</code></strong>의 경우에도 필드를 다른 타입으로 (예: 국제표준화기구의 날짜/시간 datetime형식 대신 문자열로) 리캐스팅하는 이슈가 존재할 수 있다.</em></p>
<h2 id="처리량에-대한-모니터링">처리량에 대한 모니터링</h2>
<p>메시지와 이벤트는 가능한 짧은 <strong><code>지연시간 (latency)</code></strong>를 통해 흐르게 해야 합니다. 즉, 적절한 <strong><code>파티션</code></strong>이나 <strong><code>샤드</code></strong>에 대한 <strong>대폭과 처리량을 프로비저닝 할 줄 알아야 합니다.</strong> </p>
<p>그렇기에, 이벤트 처리에 <strong>충분한 메모리, 디스크 및 CPU 자원을 제공</strong>하고, 실시간 파이프라인을 관리할 때는 자동 계산 기능을 사용해 <strong>트래픽의 급상승에 대처하고 부하 감소에 따른 비용도 절감</strong>해야 합니다. 때문에 스트리밍 플랫폼 관리에는 배치 플랫폼보다 오버헤드가 발생할 여지가 많습니다.</p>
<h1 id="마이크로-배치-vs-진짜-real-time">마이크로 배치 vs 진짜 Real-time</h1>
<blockquote>
<p>요즘말로 하면 ‘성능이 좋은 서버에서 실행되는 REST API는 소켓 통신과 구별할 수 없다’ 이런 느낌일까?</p>
</blockquote>
<p>사실 우리가 <strong>‘실시간’</strong> 이라고 칭했던 것들이 대부분 <strong>‘마이크로 배치’</strong> 형태였을 가능성이 높습니다.</p>
<p><strong><code>마이크로 배치</code></strong>는 배치 지향 프레임워크를 스트리밍 상황에 적용하는 방법으로, 2분 간격에서 1초 간격까지 실행할 수 있습니다. <strong><code>Spark Streaming</code></strong>과 같은 것이 대표적인 마이크로 배치 프레임워크라고 볼 수 있는데, 높은 배치 빈도로 자원을 적절히 할당하면 실시간과 유사한 성능을 발휘할 수 있습니다.</p>
<p>아래 사진은 DataFrame을 기반으로 스트리밍 처리하는 Structured Streaming의 예시입니다.
<img src="https://velog.velcdn.com/images/idle-danie/post/254a514d-011a-41c5-a0b2-07eb0ff65fab/image.png" alt=""></p>
<p>진정한 Real-time을 구현하는 스트리밍 시스템인 <strong><code>Flink Streaming</code></strong>은 하나의 이벤트를 처리하도록 설계되었습니다. (물론 exactly once 전달은 <strong><code>Spark Streaming</code></strong>도 동일하게 적용되기는 한다) 
그렇기에, 오베헤드가 상당할 것으로 쉽게 예상할 수 있습니다.</p>
<p>그러나, <strong><code>Flink Streaming</code></strong>의 경우에도 개별 이벤트에 데이터를 추가하는 기본적인 강화 프로세스에서는 지연시간이 짧은 이벤트를 한 번에 하나씩 전달할 수 있기 때문에 역설적이게도 여전히 많은 배치 프로세스가 발생합니다. </p>
<p>그래서 사실 당연히 정답이 없을 뿐더러, 개인적인 생각으로는 두 기술을 엄격하게 구별시키는 것도 큰 의미는 없을 것 같습니다. 
_(여담으로, 마이크로 배치라는 용어 자체가 경쟁기술을 배제하기 위해 사용되기도 하였다고 한다.)
_
결론적으로, 각자의 환경에 맞게 도메인 지식이 어느정도 받쳐주는지를 고려하면서 시스템의 요구사항을 적절하게 해소하면 된다고 생각한다 :)</p>
<h1 id="변경-데이터-캡처-changed-data-capture">변경 데이터 캡처 (Changed Data Capture)</h1>
<p>사실, 스트림에는 <strong><code>이벤트 스트림</code></strong>과 <strong><code>CDC</code></strong>라는 두 가지 주요 유형이 존재한다. </p>
<p><strong><code>CDC</code></strong>는 데이터베이스에서 발생하는 각 변경 이벤트(예: <strong><code>CRUD</code></strong>)를 추출하는 방법으로, DB 간에 거의 실시간으로 복제하거나 다운스트림 처리를 위한 이벤트 스트림을 생성하는 데 자주 사용됩니다. </p>
<p>흔히, 요즘 데이터엔지니어링 분야에서 많이 언급되는 트렌드인 <strong><code>Zero-ETL</code></strong>을 구축한다고 하면, <strong><code>CDC</code></strong>가 기술적 배경이 될 확률이 높습니다.</p>
<p><strong><code>CDC</code></strong>는 DB의 종류에 따라 다르게 처리됩니다.</p>
<blockquote>
<p><strong><code>RDBMS</code></strong>의 경우는 스트림을 생성하기 위해 처리될 수 있는 이벤트 로그를 종종 생성해 DB 서버에 직접 저장하고, 많은 클라우드 <strong><code>NoSQL</code></strong>의 경우는 로그 또는 이벤트 스트림을 목표로 하는 스토리지 위치로 전송할 수 있습니다.</p>
</blockquote>
<p>많은 <strong><code>CDC</code></strong>의 형태가 있지만, 스트리밍 데이터의 관점에서 볼 때는 <strong><code>연속 CDC</code></strong> 개념을 살펴보아야 합니다.</p>
<h2 id="연속-cdc">연속 CDC</h2>
<p><strong><code>연속 CDC</code></strong>는 모든 테이블 이력을 캡처해 실시간 데이터베이스 복제 또는 실시간 스트리밍 분석을 위한 거의 실시간 데이터 수집을 지원합니다. 
일반적으로 <strong><code>연속 CDC</code></strong>는 정기적인 쿼리를 실행해 테이블 변경사항을 일괄적으로 가져오는 것이 아니라, 데이터베이스에 대한 각 쓰기를 이벤트로 처리합니다.</p>
<p>OLTP기반의 RDBMS를 대상으로 <strong>연속 CDC 이벤트 스트림을 캡쳐하기 위해 사용하는 것은 보통 로그 기반 CDC 입니다.</strong> </p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/886094b0-3e1a-476f-bc7a-34595af195c9/image.png" alt=""></p>
<p>출처: <a href="https://www.striim.com/blog/log-based-change-data-capture/">https://www.striim.com/blog/log-based-change-data-capture/</a></p>
<p><strong><code>데이터베이스 이진 로그</code></strong>는 데이터베이스의 모든 변경 사항을 순차적으로 기록하기 때문에, <strong><code>CDC</code></strong> 도구는 이 로그를 읽고 이벤트 형식으로 <strong><code>Debezium</code></strong>과 같은 플랫폼을 타깃으로 전송하게 됩니다. (여기서 말하는 <strong><code>이진 로그</code></strong>는 우리가 MySQL에서 replication을 진행할 때 사용하는 <strong><code>바이너리 로그 기반 복제의 이진로그</code></strong>와 동일한 개념입니다)</p>
<p>이제 기본적인 스트리밍 데이터에 대한 이해가 끝났으니, 이벤트 스트리밍 플랫폼으로 가장 많은 사랑을 받고 있는 <strong><code>Apache Kafka</code></strong>에 대해 본격적으로 알아보자. </p>
<h1 id="why-kafka">Why Kafka?</h1>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/135f1369-2d5c-46a1-85a6-a5bc2f8827a2/image.png" alt=""></p>
<p>보통 파이프라인의 성능은 두 가지로 결정됩니다. 바로 <strong><code>latency</code></strong>와 <strong><code>처리량</code></strong> 입니다.
아무래도 스트리밍 데이터를 처리하는 프레임워크는 그 성격 상 <strong><code>latency</code></strong>에 치중될 수 밖에 없습니다. 
그러나, <strong><code>Kafka</code></strong>는 <strong>디스크 기반의 로깅 메커니즘</strong>을 통해 처리량 또한 준수한 수준을 보입니다.</p>
<p>특히, <strong><code>Kafka</code></strong> 는 <strong>강력한 데이터 복제를 ** ** <code>브로커</code></strong>를 통해 지원합니다.</p>
<p><strong><code>브로커</code></strong>는 Kafka 클러스터를 구성하는 기본 단위입니다. <strong><code>Kafka</code></strong> 에서는 <strong>데이터를 여러 브로커에 복제하여 저장하여</strong>, 어떤 브로커에 <strong>문제가 발생해도 데이터 손실 없이 처리를 계속</strong>할 수 있음을 의미하며, 실시간 시스템에서는 이러한 내구성이 매우 중요한 요소로 작용합니다.</p>
<p>추가로,  <strong><code>분산 시스템</code></strong>으로 설계되어 있어 데이터를 여러 서버(브로커)에 걸쳐 저장하고 처리할 수 있기에, 단일 노드의 성능 한계를 넘어서는 확장성과 높은 처리량을 가능하게 합니다.</p>
<p>이외에도, <strong><code>Kafka</code></strong>는 다양한 프로그래밍 언어 및 프레임워크와의 호환성, 그리고 이미 형성되어 있는 대규모 커뮤니티 지원을 통해 많은 기업들이 <strong><code>Kafka</code></strong>를 채용하여 비즈니스 데이터를 효율적으로 관리하고 있습니다.</p>
<h1 id="kafka-architecture">Kafka Architecture</h1>
<blockquote>
<p>Kafka config: Topic, Partition, Replica, Leader, Follower…. etc</p>
</blockquote>
<p>토픽과 파티션은 위에서 이미 언급했지만, 조금 더 상세한 이해를 위해 구체적으로 한번 더 짚고 넘어가겠다. </p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/d4a51c63-fe3f-414b-bc6d-ea51437ec04b/image.png" alt=""></p>
<h2 id="토픽-topic-1"><strong>토픽 (Topic)</strong></h2>
<p><strong><code>토픽</code></strong>은 <strong>데이터의 카테고리나 분류를 나타내는 단위</strong>입니다.
예를 들어, &quot;user_signups&quot; 또는 &quot;order_transactions&quot;와 같은 토픽을 만들 수 있습니다. 각 토픽은 하나 이상의 파티션을 가질 수 있습니다.</p>
<h2 id="파티션-partition"><strong>파티션 (Partition)</strong></h2>
<p><strong><code>파티션</code></strong>은 <strong>토픽의 데이터를 분할하는 단위</strong>입니다. 
토픽의 데이터가 여러 파티션에 나누어 저장되어 사용되기 때문에, <strong>병렬 처리를 통해 높은 처리량을 달성</strong>할 수 있습니다. 처음, 해당 개념을 접했을 때 혼자 아래와 같은 예시를 만들어 이해하려고 애썼던 기억이 있다.</p>
<p>예를 들어, &quot;user_signups&quot;라는 토픽이 3개의 파티션으로 구성되어 있다면:</p>
<blockquote>
<ul>
<li>파티션 1: 사용자 가입 데이터의 1/3</li>
</ul>
</blockquote>
<ul>
<li>파티션 2: 사용자 가입 데이터의 1/3</li>
<li>파티션 3: 사용자 가입 데이터의 1/3</li>
</ul>
<p>이렇게 각 파티션은 토픽의 전체 데이터 중 일부를 독립적으로 저장합니다.</p>
<p><strong><code>프로듀서</code></strong>가 <strong><code>토픽</code></strong>에 메시지를 쓸 때, Kafka는 해당 메시지를 특정 파티션에 할당합니다. 이 할당은 여러 가지 방법(예: 라운드 로빈, 키 해싱 등)으로 수행될 수 있습니다. 따라서, 기본적으로 토픽의 모든 데이터를 조회하려면 모든 파티션에서 데이터를 읽어와야 합니다. 
단, 각 파티션 내에서의 메시지 순서는 유지되지만, 토픽 전체의 파티션 간 메시지 순서는 보장되지 않을 수 있습니다. </p>
<h2 id="복제본-replica"><strong>복제본 (Replica)</strong></h2>
<p><strong><code>복제본</code></strong>은 <strong>파티션의 복사본</strong>으로, 데이터의 내구성과 고가용성을 보장하기 위해 사용됩니다. </p>
<p>위에서 언급했다시피, 만약 한 <strong><code>브로커</code></strong>가 실패하더라도 해당 <strong><code>브로커</code></strong>에 저장된 <strong><code>파티션의 복제본</code></strong>이 다른 <strong><code>브로커</code></strong>에 존재하기 때문에 데이터 손실을 방지할 수 있습니다.</p>
<h2 id="segment">Segment</h2>
<p><strong><code>Segment</code></strong>는 <strong><code>브로커</code></strong>의 로컬 스토리지에 저장되는 파티션의 물리적인 저장 단위입니다.</p>
<blockquote>
</blockquote>
<ol>
<li><strong>파일 기반 저장</strong>: 각 세그먼트는 실제로 두 가지 주요 파일로 구성됩니다. 하나는 실제 메시지를 저장하는 <strong><code>.log</code></strong> 파일, 그리고 메시지의 위치를 빠르게 찾기 위한 <strong><code>.index</code></strong> 파일입니다.</li>
<li><strong>Rolling</strong>: 세그먼트는 설정된 크기나 기간에 도달하면 &quot;roll&quot; 되며, 새로운 세그먼트 파일이 생성됩니다. 이것은 오래된 데이터를 효율적으로 삭제하거나 관리할 수 있게 해주는 장점이 있습니다.</li>
<li><strong>데이터 삭제 및 보존</strong>: 오래된 데이터를 삭제할 때, <strong><code>Kafka</code></strong>는 전체 세그먼트 파일을 삭제함으로써 효율성을 유지합니다. 데이터 보존 정책(retention policy)에 따라, 메시지가 보존되는 시간이나 세그먼트의 크기에 도달하면 해당 세그먼트는 삭제될 수 있습니다.</li>
<li><strong>효율적인 읽기 및 쓰기</strong>: 세그먼트 구조는 <strong><code>Kafka</code></strong>가 대량의 데이터를 빠르게 읽고 쓰는 데 있어 효율적입니다. 특히, 순차적인 디스크 I/O 작업은 랜덤 액세스보다 훨씬 빠르기 때문에 세그먼트는 이러한 순차적인 접근의 이점을 활용합니다.</li>
</ol>
<h2 id="각-파티션은-리더leader와-하나-이상의-팔로워follower로-구성">각 파티션은 리더(leader)와 하나 이상의 팔로워(follower)로 구성</h2>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/0f39977e-faf9-43d6-a9b6-0c676f9b75e9/image.png" alt=""></p>
<blockquote>
<ol>
<li><strong>쓰기 작업</strong>: 프로듀서가 데이터를 쓰려고 할 때, 이 작업은 해당 파티션의 리더에게 전달됩니다. 오직 리더만이 해당 파티션에 데이터를 쓸 수 있습니다.</li>
<li><strong>팔로워 동기화</strong>: 리더가 데이터를 받고 기록한 후, 이 데이터는 팔로워들에게 복제됩니다. 팔로워는 리더로부터 데이터를 주기적으로 가져와서 자신의 로그에 동기화합니다. 이렇게 해서, 만약 리더가 실패하면, 팔로워 중 하나가 새로운 리더로 승격될 수 있으며, 데이터 손실 없이 서비스를 계속 제공할 수 있습니다.</li>
<li><strong>읽기 작업</strong>: 컨슈머가 데이터를 읽을 때 기본적으로 리더에서 데이터를 읽습니다. 그러나 <strong><code>Kafka</code></strong> 설정에 따라, 컨슈머가 팔로워 브로커로부터 직접 데이터를 읽는 것도 가능하며, 이를 통해 읽기 처리량을 분산시킬 수 있습니다.</li>
</ol>
</blockquote>
<h1 id="참고문헌">참고문헌</h1>
<blockquote>
</blockquote>
<ul>
<li><a href="https://www.confluent.io/ko-kr/learn/data-streaming/">데이터 스트리밍: 장점, 예시 및 사용 사례 | KR</a></li>
<li>견고한 데이터 엔지니어링 (written by Joe Reis &amp; Matt Housley)</li>
<li><a href="https://kafka.apache.org/documentation/">Apache Kafka</a> 공식문서</li>
<li>데이터 엔지니어링 데브코스 lecture note (created by 한기용)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Git으로 개발 생산성 올리기 0편 (feat. Commit의 진짜 의미, 충돌이 나는 이유)]]></title>
            <link>https://velog.io/@idle-danie/Git%EC%9C%BC%EB%A1%9C-%EA%B0%9C%EB%B0%9C-%EC%83%9D%EC%82%B0%EC%84%B1-%EC%98%AC%EB%A6%AC%EA%B8%B0-0%ED%8E%B8-feat.-Git%EC%9D%98-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC-Git-pull%EC%9D%80-%EB%82%98%EC%81%98%EB%8B%A4</link>
            <guid>https://velog.io/@idle-danie/Git%EC%9C%BC%EB%A1%9C-%EA%B0%9C%EB%B0%9C-%EC%83%9D%EC%82%B0%EC%84%B1-%EC%98%AC%EB%A6%AC%EA%B8%B0-0%ED%8E%B8-feat.-Git%EC%9D%98-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC-Git-pull%EC%9D%80-%EB%82%98%EC%81%98%EB%8B%A4</guid>
            <pubDate>Thu, 14 Mar 2024 15:41:48 GMT</pubDate>
            <description><![CDATA[<h1 id="0-들어가기">0. 들어가기</h1>
<p>본 <strong>Git으로 개발 생산성 올리기 시리즈</strong>는 현업에서 Git을 사용하며 가장 유용하게 사용했던 명령어 및 개발 생산성을 올릴 수 있었던 경험을 공유하기 위해 제작되었다. 
0편에서는 본격적인 시리즈에 들어가기 전, 기본적인 Git에 대한 간단한 소개 및 동작원리, 그리고 Git에 관한 흥미로운 글을 소개해보려고 한다. 
Git을 사용한 경험이 한번이라도 있다면, 충분히 이해하실 것으로 기대합니다 :) </p>
<h1 id="1-git이란">1. Git이란?</h1>
<p>Git은 <strong>분산 버전 관리 시스템(DVCS)</strong>으로, 소프트웨어 개발 프로젝트에서 소스 코드의 변경 사항을 추적하고 여러 사용자 간의 작업을 조율하는 데 사용되고 있다. 분산 버전 관리 시스템인 이유는, 원본 코드는 원격 레포지토리에 공유되어 있고, 각자의 로컬 환경에 로컬 코드가 <strong>분산</strong>되어 있기 때문이다.
참고로, Git은 Linux를 만든 리누스 토르발스에 의해 2005년에 개발되었다.</p>
<h2 id="git을-왜-써야할까">Git을 왜 써야할까?</h2>
<p>Git을 사용하는 주된 이유는 팀 내 또는 개인 프로젝트에서의 소스 코드 관리를 용이하게 하기 위함이다. Git은 변경 사항을 효율적으로 추적하고, <strong>버전을 관리</strong>함으로써 여러 버전의 문서를 안전하게 보관하고 필요시 원하는 버전으로 쉽게 되돌아갈 수 있게 한다.</p>
<h2 id="svn과의-차이점">SVN과의 차이점</h2>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/47117a4b-6941-4706-9deb-7f5feb46fe5e/image.png" alt="">
SVN은 중앙집중식 버전 관리 시스템(CVCS)이며, 모든 파일의 버전 관리 정보가 중앙 서버에 저장됩니다. 위에서 언급한 바와 같이 Git은 <strong>분산 버전 관리 시스템</strong>으로, 각 사용자가 전체 저장소의 복사본을 로컬에 보유하며, 이로 인해 네트워크 상태에 구애받지 않고 작업을 진행할 수 있다. 브랜칭이나 병합과 같은 유연성도 우수하다.</p>
<h1 id="2-git의-기본적인-동작-원리">2. Git의 기본적인 동작 원리</h1>
<blockquote>
</blockquote>
<ul>
<li>구성</li>
<li>동작원리<ul>
<li>Commit은 어떻게 이루어질까?</li>
<li>충돌이 왜 이러날까?</li>
</ul>
</li>
</ul>
<h2 id="구성">구성</h2>
<ul>
<li><p>Working Directory: 실제 파일들이 위치한 디렉토리로, 사용자가 현재 작업하고 있는 공간이다.</p>
</li>
<li><p>Staging Area: 커밋하기 전의 준비 영역으로, Git에서 변경사항을 임시로 저장하는 곳이다. 
ex) git add를 사용하여 파일 변화를 스테이지 위에 올린다</p>
</li>
<li><p>Local Repository: 사용자의 PC에 위치한 저장소로, 프로젝트의 모든 버전 정보를 포함합니다.
ex) git commit을 하여 스테이지 위의 파일 변화들을 로컬 레포지토리에 기록한다</p>
</li>
<li><p>HEAD: 현재 체크아웃된 커밋을 가리키는 포인터로, 가장 최근의 작업 상태를 나타낸다. 보통 커밋 기록을 볼 때, <strong>_HEAD 0XEF56 _</strong> 와 같은 로그를 볼 수 있는데, 현재 0XEF56 라는 커밋을 HEAD가 가리키고 있고, 이가 최근 커밋이라는 것라고 생각하면 된다. 후에, cherry-pick이나 해당 커밋으로 돌아가기 위해서는 해당 값(0XEF56)을 기반으로 진행되게 된다.</p>
</li>
<li><p>Remote Repository: 로컬과 분리되어 있는 Git원격 저장소
ex) git push를 하여 원격저장소로 커밋 결과들을 올린다
<img src="https://velog.velcdn.com/images/idle-danie/post/67426b3c-0a5f-4c61-b33a-cfa0896eb8be/image.png" alt=""></p>
</li>
</ul>
<h2 id="동작-원리">동작 원리</h2>
<blockquote>
<p>사실 Git을 처음 사용하기 시작했을 때, 많은 고민을 하지 않았었다. add, commit, push, pull만 잘 동작하면, 크게 무리가 없을 것 같다고 생각했고 실제로도 필요가(?) 없었다. 큰 장벽을 만나기 전까지.... 
그래서 한번 쯤은, 하루 이틀 정도 Git의 작동원리나 사용사례에 대한 자료들을 찾아보며 깊게 이해해 보시길 추천드린다.</p>
</blockquote>
<p>사실 기본적인 동작원리는 위 구성 파트에서 설명이 다 이루어졌다. 
따라서, 동작원리와 별개로 Git을 이해하며 개인적으로 중요하게 생각했던 개념 몇 가지를 설명하려고 한다.</p>
<h3 id="commit은-어떻게-이루어질까">Commit은 어떻게 이루어질까?</h3>
<p>위에서는 하루 이틀이라고 칭했지만, 나는 사실 꽤 오랜기간 Git에 관하여 제대로 이해하지 못했다. 지금도 완벽하다고 생각하진 않고, 누가 설명을 요청하면 아직도 헷갈리는 개념이 많아 쉽지 않다. 하지만, 기본적인 이해로 넘어가는 허들을 극복하는 순간은 <strong>Commit에 대한 이해</strong>를 했을 때였다. </p>
<p>기본적으로, 버젼관리에서의 핵심은 파일 변화이다. 수정, 삭제, 추가 등의 행위를 말하는 것이다. Git에서는 이것을 측정 및 감지하는 단위가 <strong>Commit</strong>인 것이다.</p>
<p>앞서 말한 SVN은 이전 파일과의 차이를 추적하며 동작이 되는 구조인데, Git은 차이 뿐만 아니라 전체 코드에 대한 기록도 유지된다. 이는 용량적인 문제가 발생하는 것처럼 보이지만, SVN은 처음 작업이 시작할 때부터의 변경사항을 모두 추적해야 하므로 불필요한 연산이 발생한다. Git의 커밋 개념으로 비교하자면, 처음 커밋부터 모두 체크해야한다는 것이다. 허나, Git은 알다시피 <strong>직전 커밋과의 비교</strong>만 진행된다. 그리고, 파일 변화가 이루어지지 않은 작업물들은 일종의 심볼릭 링크 형태로 저장되기 때문에, 용량적인 문제를 야기하지 않는다. </p>
<p>정리하자면 Git의 버전관리는 이전 커밋과의 차이점만 비교하며 이루어지고, 일일이 코드의 차이점을 연산하지 않고 <strong>스냅샷(=커밋)</strong>을 찍어 비교한다. </p>
<p>무식(?)하지만 효율적인 방식으로, 오직 커밋 단위로 Git은 동작하기 때문에 이에 대한 이해가 필수적이라고 생각한다. </p>
<h3 id="충돌이-왜-이러날까">충돌이 왜 이러날까?</h3>
<p>아마 Git이 우리의 머리를 아프게 하는 순간은 병합을 시도했을 때 <strong>충돌이 일어났을 때</strong>일 것이다.
충돌이 일어나는 이유는 간단하지만, 해결법은 다양할 것이다. 예를 들어, 커밋을 되돌리거나, 충돌난 부분을 직접 수정하거나, 극단적(?)으로 force 명령어를 사용하는 것이다. 개인적으로, force 명령어는 개인만이 코드를 관리하지 않는 이상 추천하지 않고 직접 수정하는 것을 추천한다 :)</p>
<p>*<em>일단 충돌이 일어나는 이유는, 쉽게 말하면 같은 코드를 수정했기 때문이다. 
*</em></p>
<p>일반적으로, 병합을 시도할 때 3가지 경우의 수가 존재한다. 병합을 시도하는 브랜치를 A와 B로 설명하려고 한다.</p>
<ol>
<li>Fast-Forward: A (변경사항 a) + B (변경사항 X) = A<ul>
<li>B는 변경사항이 없기 때문에, 그냥 A로 돌려감기 하면 됌</li>
</ul>
</li>
<li>Merge-commit: A (변경사항 a) + B (변경사항 b) = A + B<ul>
<li>변경사항이 없기 때문에 병합이 정상적으로 진행되고, 병합되었다는 커밋이 기록 됌</li>
</ul>
</li>
</ol>
<p>충돌이 일어나는 경우는 3번째 경우이다.
*<em>A (변경사항 a) + B (변경사항 a&#39;) = 변경사항에 대한 코드가 겹치기 때문에 병합 못함
*</em></p>
<p>이런 생각을 할 수 있다. <em>코드 출처가 같긴 하지만, 서로의 코드에 영향이 가지 않고 기존 코드 수정 없이 코드가 추가 된 것인데도 충돌이 왜 발생해?</em>
앞에서 설명했던 것과 같이 Git은 커밋을 단위로 변화를 감지하고 이는 스냅샷을 비교하는 것이다. 개행을 했든 주석을 추가했든, 변화는 변화일 뿐이다. 
그러니, 침착하게(?) 충돌난 부분을 체크하고 직접 수정하는 것이 가장 빠른 해결책으로 생각된다 ㅎ
당연히, 충돌이 해결되면 2번의 경우와 같아지므로 Merge-commit이 발생한다.</p>
<p>아래는 Azure 기술 블로그에 병합 관련하여 포스팅된 글이다. 추가적으로 Git에 대한 이해를 더할 수 있는 좋은 글들이 많으니 참고 바랍니다 :)
<a href="https://learn.microsoft.com/ko-kr/azure/devops/repos/git/merging?view=azure-devops&amp;tabs=visual-studio-2022">https://learn.microsoft.com/ko-kr/azure/devops/repos/git/merging?view=azure-devops&amp;tabs=visual-studio-2022</a></p>
<h3 id="git-pull은-나쁘다">Git pull은 나쁘다?</h3>
<p>초기 Git 개발에 참여했던 Felipe Contreras가 <a href="https://felipec.wordpress.com/2021/07/13/why-is-git-pull-broken/">Why is git pull broken?</a>라는 자극적인 제목으로 작성한 글을 우연히 보게되었는데 인상깊어 소개합니다ㅎ</p>
<p>해당 글의 주된 내용은 git push와 git pull은 git 작동원리에서는 반대의 의미를 가지고 있지 않고, git pull의 지나친 사용을 지양해야 하는 이유를 상세하게 설명합니다. git pull을 습관적으로 사용하고 (바로 나..ㅎ) rebase 작동원리를 이해하고 싶은 분께 추천드립니다 :)</p>
<blockquote>
<p>이전에 관련 세션을 준비했다가 삑난(?) 경험이 있어서 준비했던 내용에 대해 시간이 된다면 아래 포스팅을 추가로 올릴 예정입니다 :)</p>
</blockquote>
<ol>
<li>Git으로 개발 생산성 올리기 1편 (feat. git stash를 알고 삶의 질이 올라갔다?)</li>
<li>Git으로 개발 생산성 올리기 2편 (feat. 코딩 스타일 관리 자동화를 위한 Git Hook 도입기)</li>
<li>Git으로 개발 생산성 올리기 3편 (feat. Github Action with Terraform)</li>
</ol>
<p>참고로, 관련해서 발표 준비를 위한 테스트 용도로 만들었던 git repository link는 다음과 같습니다. 후에 포스팅을 이어나간다면, repo는 cleanup해서 새로 생성해보겠습니다 ㅎ 
<a href="https://github.com/idle-danie/git_session">https://github.com/idle-danie/git_session</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[OpenAI를 활용한 챗봇 설계 및 실험 (feat. fine-tuning & parameter 정밀 분석)]]></title>
            <link>https://velog.io/@idle-danie/OpenAI%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%B1%97%EB%B4%87-%EC%84%A4%EA%B3%84-%EB%B0%8F-%EC%8B%A4%ED%97%98-fine-tuning-parameter-%EC%A0%95%EB%B0%80-%EB%B6%84%EC%84%9D</link>
            <guid>https://velog.io/@idle-danie/OpenAI%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%B1%97%EB%B4%87-%EC%84%A4%EA%B3%84-%EB%B0%8F-%EC%8B%A4%ED%97%98-fine-tuning-parameter-%EC%A0%95%EB%B0%80-%EB%B6%84%EC%84%9D</guid>
            <pubDate>Wed, 13 Mar 2024 14:29:08 GMT</pubDate>
            <description><![CDATA[<blockquote>
</blockquote>
<p>이 글에서는 기본적으로 OpenAI API(ChatGPT)를 활용하여 chatbot을 개발할 때, 중요한 API 2가지를 parameter 중심으로 풀어보고, 경험에서 얻은 Tip과 개인적인 견해를 소개해보려고 합니다. 
참고로 각 API 제목에 공식문서 링크가 첨부되어 있습니다 :)</p>
<h1 id="서론">서론</h1>
<p>개인적은 생각으로는, <a href="https://arxiv.org/pdf/1706.03762.pdf"><strong><em>Attention is all you need</em></strong></a> 논문이 나온 뒤, LLM 모델에 대한 연구와 관심도가 폭발적으로 늘어났던 것 같다.
<del>최근 몇년 간, 아카이브(인공지능 한정)는 90프로정도가 중국 연구원분들의 논문으로 채워져 있는 것 같다…</del>
현업에서는 독자적인 LLM 모델을 만들거나 LLAMA와 같은 오픈소스를 활용하여 챗봇을 개발하려는 시도가 많아졌던 것 같다.</p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/04ced7b3-9f89-4d65-a5b7-5d1c3c79be72/image.png" alt=""></p>
<p>*<em>OPENAI에서 ChatGPT4가 나오기 전까지는….
*</em></p>
<p>ChatGPT가 워낙 general하기도 하고, 답변 생성 성능이 타 모델에 비해 좋기도 해서 이제는 <em>&quot;OpenAI를 잘 활용하자&quot;_로 많은 분들의 생각이 변한 것 같다. 물론, 연구비 지원이 가능한 기업들은 독자적인 모델을 지속적으로 개발 중이신 것 같긴하다. ~~_언젠가는 나도 기여하는 날이 왔으면...ㅎ</em>~~
다만, ChatGPT와 같이 general한 모델 보다는 특정 도메인에 customized된 챗봇을 개발하는 방향으로 산업이 변화하는 것으로 보인다. 
이제 어떻게 OpenAI의 API를 사용하여, 나만의 customized chatbot을 제작할 수 있는지 알아보자.</p>
<p><strong>추가로, 본 글에서 설명하는 API는 모두 <a href="https://platform.openai.com/playground/chat">OpenAI playground</a>에서 미리 체험해볼 수 있으니 꼭 먼저 사용해보길 추천드립니다 :)</strong></p>
<h1 id="1-text-generation-api">1. <a href="https://platform.openai.com/docs/api-reference/chat/create">Text generation API</a></h1>
<blockquote>
<p>우리가 흔히 사용하는 웹 ChatGPT를 API로 만들었다고 생각하면 됩니다 :)</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/c6c6f7b2-efca-4751-a4ca-ef372c8b9d82/image.png" alt=""></p>
<h2 id="code">Code</h2>
<p>공식문서에서 제공한 코드 예시는 아래와 같다. </p>
<pre><code class="language-python">MODEL = &quot;gpt-3.5-turbo&quot;
response = client.chat.completions.create(
    model=MODEL,
    messages=[
        {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: &quot;You are a helpful assistant.&quot;},
        {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;Explain asynchronous programming in the style of the pirate Blackbeard.&quot;},
    ],
    &quot;&quot;&quot;
    parameters
    ex) temperature=1,top_p=1, frequency_penalty=0, presence_penalty=0
    &quot;&quot;&quot;
)
print(response.choices[0].message.content)
</code></pre>
<h2 id="parameter">Parameter</h2>
<blockquote>
<p>message에 해당하는 prompt구성법은 굳이 언급하지 않겠다. 또한, prompt를 구성하는 절대적인 해답 또한 없다. 나중에 prompt-engineering은 따로 포스팅 하겠다. </p>
</blockquote>
<p>API parameter가 여러가지 있지만, 아래 5개 정도가 개략적인 답변의 형태를 결정한다. </p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/8b402fe9-87e2-46e8-ae33-ac9c2b145aa9/image.png" alt=""></p>
<figcaption style="text-align:center; font-size:15px; color:#808080; margin-top:40px">
    출처: https://platform.openai.com/playground/chat
  </figcaption>

<ul>
<li><strong>N</strong>: 생성하는 답변 개수 (default = 1)</li>
<li><strong>Temperature</strong>: 값이 높을수록 창의성 &amp; 무작위성 증가 (default = 1)</li>
<li><strong>TopP</strong>: 값이 높을수록 다양한 답변을 생성 (default = 1)</li>
<li><strong>Frequency Penalty</strong>: 값이 높을수록 반복을 더 많이 피하려고 함 (default = 0)</li>
<li><strong>Presence Penalty</strong>: 값이 높을수록 이미 언급된 내용을 피하려고 함 (default = 0)</li>
</ul>
<p>모두 optional한 것이라 굳이 사용하지 않아도, OpenAI 에서 default로 설정한 값이 사용되도 큰 무리는 없을 것이다. (아마, 우리가 웹 ChatGPT에서 사용하는 값이 default값이 아닐까 생각한다)</p>
<p>*<em>다만, 우리가 특정 도메인을 타겟하여 customize한다면 파라미터 운영을 해야하는 순간이 온다. *</em></p>
<p>예를들어, 어떤 도메인은 <strong>general하지 않은 즉 창의성이 돋보이는 답변</strong>이 생성되어야 할 수도 있고, 어떤 도메인은 <strong>outlier가 절대 나타나면 안되고 보편적인 답변</strong>이 우선시되어야 할 수 있다는 말이다. 따라서 해당 파라미터를 컨트롤하며, 특정 도메인에 맞는 최적해를 여러 실험을 통해 도출해야 한다. 막상 생각은 안나지만 이해를 돕기위해 도메인 예를 굳이 들어보자면, 전자는 <em>&#39;막장 드라마 대본 생성기&#39;</em>,  후자는 _&#39;공공기관 고객 응대 챗봇&#39;_라고 생각할 수 있겠다.</p>
<h3 id="n">N</h3>
<blockquote>
<p><strong>N (응답 개수)</strong>: 이 파라미터는 챗봇이 한 번에 생성하는 답변의 개수를 결정합니다. 높은 값은 더 많은 수의 다양한 답변을 생성하며, 낮은 값은 적은 수의 답변을 생성합니다.</p>
</blockquote>
<p>위 코드에서 아래 line의 인덱스를 보고 의문을 가진다면, 아래의 N parameter 설명을 읽게 되면 이해하게 될 것이다.</p>
<pre><code class="language-python">print(response.choices[0].message.content)</code></pre>
<p>우리가 웹에서 ChatGPT를 사용할 때, 우리는 답변을 1개 받는 이유는 <strong>N이 default값 1로 설정</strong>되어 있기 때문이다!
가끔, 우리가 답변 2개를 제시받고 그 중 더 선호하는 답변을 선택하라고 할 때가 있는데 그때는 N=2인 상태로 API response를 받는 것이다. 아마, 내부 로직 상에서 해당 답변을 선호된 답변으로 저장하고 아래 2가지 방향으로 develop되지 않을까? 라는 개인적인 생각이다.</p>
<ol>
<li>해당 chat session에 한하여, 선호된 답변의 방향성으로 prompt를 생성하도록 유도</li>
<li>후에, 해당 모델을 선호된 답변을 바탕으로 학습모델 fine-tuning</li>
</ol>
<p>*<em>N 파라미터를 활용하는 방법은 2가지로 축약된다.
*</em></p>
<ol>
<li><strong>실험에 대한 검증</strong>: N=1로 설정했을 때, 실험 진행 중 생성된 답변에 대한 신뢰성이 보장할 수 없을 때 N값을 증가시켜 생성되는 답변의 양상을 지켜볼 수 있다.</li>
<li><strong>다양한 답변을 도출</strong>: 1번의 의도와는 다르게, 애초에 여러 prompt를 생성하고 싶을 수 있다. 다만, 최적 파라미터와 input prompt설정에 대한 실험이 완료되었을 때 추천한다 :) 
그 이유는 N=n (n&gt;1)일 때 경험 상, 전체적인 답변의 질이 낮아지는 점, 실험 결과를 객관적으로 분석하기 힘든 점 등이 있어 실험 단계에서는 위 의도를 관철시키는 것이 어렵기 때문이다. </li>
</ol>
<p><em>*<em>결국 우리의 목표는, N=1인 상태에서 최적의 답변이 생성되어야 한다는 것이다. 
*</em></em></p>
<h3 id="temperature--topp">Temperature &amp; TopP</h3>
<blockquote>
<p><strong>Temperature</strong>: 이 값은 챗봇의 응답이 얼마나 예측가능한지 또는 창의적인지를 조절합니다. 낮은 온도(예: 0.2)는 더 예측가능하고 일관된 답변을 생성합니다. 높은 온도(예: 2.0)는 더 창의적이고 예상치 못한 답변을 생성합니다. (range: 0 &lt;= Temperature &lt;=2)</p>
</blockquote>
<blockquote>
<p><strong>TopP</strong>: 이 값은 챗봇이 고려하는 답변의 다양성을 조절합니다. 낮은 TopP 값은 챗봇이 더 일반적이고 보편적인 답변을 선택하게 하며, 높은 TopP 값은 더 다양하고 예측 불가능한 답변을 생성합니다.</p>
</blockquote>
<p><a href="https://platform.openai.com/docs/api-reference/chat/create#chat-create-top_p">공식문서에서의 TopP 설명을 번역하면 아래와 같다.
</a><em>이 설정은 모델이 토큰을 선택할 때, 확률이 높은 상위 일정 비율의 토큰들만 고려하도록 합니다. 예를 들어, top_p가 0.1(10%)로 설정되어 있다면, 모델은 가능한 토큰들 중 확률 합이 상위 10%에 해당하는 토큰들만을 고려하여 다음 토큰을 선택하게 됩니다.
이 방법은 텍스트 생성에서 더 다양성을 부여하거나, 너무 예측 가능한 결과를 피하고자 할 때 유용합니다. 일반적으로 top_p 설정이나 temperature 설정 중 하나를 조정하는 것을 권장하지만, <strong>두 설정을 동시에 조정하는 것은 권장하지 않습니다.</strong> 이는 두 설정 모두 생성 텍스트의 다양성과 예측 가능성을 조절하는 역할을 하기 때문입니다.</em></p>
<p>여기서 중요한 점은, <strong>두 파라미터를 동시에 조작변인으로 설정하지 말아야 한다는 점이다!</strong>
물론, 정확하게 파라미터가 프롬프트에 작용하는 방식은 다르지만 중요한 것은 공식문서에서도 명시된 것처럼, 실제 실험을 진행할 시에 두 파라미터를 조작변인을 설정한다면, 답변의 창의성에 대한 명확한 수치를 평가하기 어려울 것이다. 
그래서 추천하는 방식은, 아예 전체적인 파라미터 운영에서 <strong>두 파라미터 중 한 가지를 제외시키는 것이다.</strong> 
이외에는 아래와 같은 2가지 방식으로 활용할 수 있을 것 같다.</p>
<ol>
<li><strong>Temperature vs TopP:</strong> 명확하게 아래와 같이 변인을 통제하여 둘 중 어떤 파라미터를 선택할지 선택
실험 A: Temperature=0.5, TopP=1.0 
실험 B: Temperature=1.0, TopP=0.5 </li>
<li>여러 실험을 거쳐 (Temperature, TopP)에 대한 최적 조합 찾기</li>
</ol>
<p>물론, 3번과 같이 여러 실험을 거쳐 조합에 대한 최적해를 찾는 것이 이상적이긴 합니다만 현실적으로 모델링을 직접 한 것이 아니기 때문에, 해당 파라미터가 실제로 어떤 작용을 하는지는 모른다는 점이 있습니다. </p>
<p>*<em>결론: 하나는 default값 쓰고, 나머지를 조작하며 최적해 찾기
*</em></p>
<h3 id="frequency-penalty--presence-penalty">Frequency Penalty &amp; Presence Penalty</h3>
<blockquote>
<p><strong>Frequency Penalty (빈도 패널티)</strong>: 이 설정은 챗봇이 반복되는 단어나 구문을 얼마나 피할지 결정합니다. 높은 값은 챗봇이 반복을 피하도록 하며, 낮은 값은 반복이 더 자주 발생하게 합니다.
(range: -2.0 &lt;= fp &lt;= 2.0)</p>
</blockquote>
<blockquote>
<p><strong>Presence Penalty (출현 패널티)</strong>: 이 값은 챗봇이 이전에 사용한 내용을 얼마나 피할지 조절합니다. 높은 값은 챗봇이 같은 내용을 반복하지 않도록 하며, 낮은 값은 챗봇이 이미 언급한 내용을 다시 사용할 가능성이 높아집니다.
(range: -2.0 &lt;= pp &lt;= 2.0)</p>
</blockquote>
<p><em>두 parameter 모두 <strong>단어에 대한 중복 &amp; 반복</strong>에 관한 논의이지만 구체적으로 어떤 차이점이 있는지에 대한 이해가 쉽지 않을 것 같아서 아래 코드와 예시를 통해 설명해보려 한다.</em></p>
<blockquote>
<pre><code class="language-python">mu[j] -&gt; mu[j] - c[j] * alpha_frequency - float(c[j] &gt; 0) * alpha_presence</code></pre>
</blockquote>
<pre><code>- Logits (mu[j]): Logit은 모델이 특정 단어를 선택할 확률을 나타내는 로그 확률입니다. 모델은 이러한 로그 확률을 사용하여 다음에 올 단어를 결정합니다.
- c[j]: 이는 모델이 현재 위치 이전에 특정 단어를 선택한 횟수를 나타냅니다.
- alpha_frequency (빈도 패널티 계수): 이 계수는 특정 단어가 반복될 때마다 그 단어의 로그 확률을 감소시키는 데 사용됩니다. 즉, 단어가 더 자주 나타날수록, 그 단어가 다시 선택될 가능성이 낮아집니다.
- alpha_presence (존재 패널티 계수): 이 계수는 단어가 한 번이라도 나타난 경우, 그 단어의 로그 확률을 일정량 감소시키는 데 사용됩니다. 즉, 단어가 이미 한 번 사용되었다면, 다시 나타날 가능성이 줄어듭니다.

예를들어 &#39;고양이&#39;라는 단어를 이미 한 번 사용했다면, &#39;존재 패널티&#39;는 &#39;고양이&#39;라는 단어가 다시 사용될 확률을 감소시키기 위해, 그 단어의 로그 확률에 직접적인 감소를 적용합니다. 만약 &#39;고양이&#39;가 여러 번 반복되었다면, &#39;빈도 패널티&#39;는 그 단어가 선택될 때마다 로그 확률을 더욱 감소시켜, 모델이 같은 단어를 계속해서 반복하는 것을 방지합니다.

결론적으로, 빈도 패널티는 **특정 단어가 반복될수록 점점 더 큰 패널티를 적용**하는 반면, 존재 패널티는 단어가 **문장에서 한 번이라도 나타나면 일정량의 패널티를 적용**합니다.

_위 설명이 어렵다면 아래 2가지 사항만 알고 넘어가자!
_
&gt; 1. 빈도 패널티는 **특정 단어의 과도한 반복을 방지**하는 데 유용하며, 존재 패널티는 **다양한 단어와 구가 생성된 텍스트에 등장하도록 유도**하는 데 더 적합합니다.
2. 공식문서에서도 명시되어 있지만, 두 값 모두 1.0 이상으로 parameter 설정 시에는 답변의 퀄리티가 확실히 떨어지는 것으로 보입니다.

# 2. [Fine-tuning API](https://platform.openai.com/docs/guides/fine-tuning)

## Fine-tuning의 필요성
![](https://velog.velcdn.com/images/idle-danie/post/d3bb2b08-d9c2-4269-8752-ded5ce9ff838/image.png)

우리가 웹에서 사용하는 ChatGPT은 굉장히 general한 모델을 사용하고 있습니다. 
그렇기에, 어떤 질문에도 평균 이상의 답변을 생성해냅니다. 그러나, 각자 아래와 같은 경험이 있을 것입니다. 

특정 채팅 세션에서, 특정 보고서 형식에 맞게 질문 몇 가지를 던져주고 원하는 응답을 이끈다. 그리고, 일주일 뒤 다시 돌아와 해당 채팅을 다시 활용합니다. 나만의 모델을 제작한 셈입니다. 아래에서 말하는 개념과는 살짝 다르지만, **학습이 별도로 필요**하단 말을 하려고 하는 것입니다.

결론적으로, 특정 도메인에 맞춰 커스텀하기 위해서는 별도의 학습, **즉 fine-tuning이 필요합니다. **

fine-tuning은 머신러닝에서 **특정 작업에 최적화된 성능을 달성하기 위해 이미 훈련된 모델을 추가적으로 조정하는 과정**을 말합니다. 이 과정은 특히 딥러닝에서 널리 사용되며, 큰 데이터셋으로 사전에 훈련된 모델을 새로운, 종종 더 작은 데이터셋에 적용할 때 일반적으로 수행됩니다.

fine-tuning을 진행할 때는 기존 모델의 구조를 유지하면서 일부 매개변수만을 조정합니다. 이를 통해 모델은 새로운 데이터에 특화되어 성능이 개선되는 것을 목표로 합니다. 
예를 들어, 대규모 이미지 데이터셋으로 사전 훈련된 모델을 특정 동물을 분류하는 작업에 맞게 파인튜닝할 수 있습니다. 이 과정은 학습 시간을 크게 단축시키고, 필요한 데이터의 양을 줄이며, 전반적인 효율성을 높이는 효과가 있습니다.

일단 먼저 OpenAI에서 제공하는 **fine-tuning API를 사용하는 방법**부터 공식문서의 코드를 통해 알아봅시다.

## Code
### 1. Training data 제작하기

```python
from openai import OpenAI
client = OpenAI()

client.files.create(
  file=open(&quot;mydata.jsonl&quot;, &quot;rb&quot;),
  purpose=&quot;fine-tune&quot;
)</code></pre><p>Code에서 확인할 수 있듯이, 먼저 jsonl파일 형식으로 dataset을 준비해야 한다.
아래는 공식문서에서 제공하는 prompt 형식이다.</p>
<pre><code class="language-python">{&quot;messages&quot;: [{&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: &quot;Marv is a factual chatbot that is also sarcastic.&quot;}, {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;What&#39;s the capital of France?&quot;}, {&quot;role&quot;: &quot;assistant&quot;, &quot;content&quot;: &quot;Paris, as if everyone doesn&#39;t know that already.&quot;}]}
{&quot;messages&quot;: [{&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: &quot;Marv is a factual chatbot that is also sarcastic.&quot;}, {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;Who wrote &#39;Romeo and Juliet&#39;?&quot;}, {&quot;role&quot;: &quot;assistant&quot;, &quot;content&quot;: &quot;Oh, just some guy named William Shakespeare. Ever heard of him?&quot;}]}
{&quot;messages&quot;: [{&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: &quot;Marv is a factual chatbot that is also sarcastic.&quot;}, {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;How far is the Moon from Earth?&quot;}, {&quot;role&quot;: &quot;assistant&quot;, &quot;content&quot;: &quot;Around 384,400 kilometers. Give or take a few, like that really matters.&quot;}]}</code></pre>
<blockquote>
<p>참고로, fine-tuning API에 활용될 수 있는 OpenAI 모델은 아래와 같습니다.
<strong>gpt-3.5-turbo</strong>, babbage-002, davinci-002
모델마다 각각 지원하는 prompt형식이 다를 수 있습니다. 
본 글은 가장 최신 모델인 <strong>gpt-3.5-turbo</strong>를 기준으로 설명하겠습니다.</p>
</blockquote>
<p>gpt-4.0-turbo model이 작년 11월 <a href="https://devday.openai.com/">DevDay 컨퍼런스</a>를 통해 공개되고 이전 모델인 gpt-4.0을 fine-tuning 가능한 모델로 일부 파트너에게만 공개했습니다만 그 이후 업데이트 소식은 없습니다... <del>아마 ChatGPT웹에서 나만의 모델을 제작할 수 있는 gpt-store가 출시되고 이제 fine-tuning API는 관심밖으로...?</del></p>
<h3 id="2-fine-tuning-모델-제작">2. Fine-tuning 모델 제작</h3>
<blockquote>
<p>모델 제작시에 Parameter managing이 가능하다. 이 부분은 <strong>아래 Parameter section</strong>에서 제대로 설명하겠다. 일단 아래는 별다른 parameter를 넣지 않고 fine-tuning 작업을 생성하는 코드이다. </p>
</blockquote>
<pre><code class="language-python">from openai import OpenAI
client = OpenAI()

client.fine_tuning.jobs.create(
  training_file=&quot;file-abc123&quot;, 
  model=&quot;gpt-3.5-turbo&quot;
)</code></pre>
<p>1번 과정에서 만든 <strong>training_file의 이름</strong>과 사용할 <strong>fine-tuning에 사용될 gpt 모델명</strong>을 명시해야 합니다. 
소요시간은 추가로 학습시키는 데이터의 양에 따라 달라지고, 경험 상으로 prompt 10개 정도를 학습할 경우 5~10분 정도 소요됩니다.</p>
<blockquote>
<p>참고로, 이미 fine-tuning으로 학습한 모델도 다시 fine-tuning을 진행할 수 있습니다!
이 경우, 기존에 제작한 모델명을 명시해주면 됩니다 :)
ex) 별다른 네이밍 작업을 하지 않으면 <strong>ft:gpt-3.5-turbo:my-org:custom_suffix:id</strong>와 같은 스타일로 모델명이 할당됩니다 </p>
</blockquote>
<h3 id="3-fine-tuning-진행상황-추적">3. Fine-tuning 진행상황 추적</h3>
<blockquote>
<p>같은 OpenAI key(token)을 사용하며, 여러명이 작업을 할 경우 작업이 겹칠 수 있다.
같은 시간에 여러 작업이 병렬처리 되는 것이 아니라, <strong>큐 형태로 먼저 시작된 작업부터 한번에 하나의 작업만 처리하게 되기 때문이다.</strong> 이 경우, 각자 상황에 맞게 해결하면 되지만 예상하다시피 아래 명령어들이 분명 필요해질 것 입니다 :) <del><em>현재는 처리 방식이 달라졌을 수 있습니다ㅎ</em></del></p>
</blockquote>
<h4 id="현재-진행중인-fine-tuning-job-list">현재 진행중인 fine-tuning job list</h4>
<pre><code class="language-python">client.fine_tuning.jobs.list(limit=10)</code></pre>
<h4 id="현재-fine-tuning-job-상태-호출">현재 fine-tuning job 상태 호출</h4>
<pre><code class="language-python">client.fine_tuning.jobs.retrieve(&quot;ftjob-abc123&quot;)</code></pre>
<h4 id="진행중인-fine-tuning-job-취소">진행중인 fine-tuning job 취소</h4>
<pre><code class="language-python">client.fine_tuning.jobs.cancel(&quot;ftjob-abc123&quot;)</code></pre>
<h4 id="fine-tuning-model-제거">fine-tuning model 제거</h4>
<pre><code class="language-python">client.models.delete(&quot;ft:gpt-3.5-turbo:acemeco:suffix:abc123&quot;)
</code></pre>
<h3 id="4-fine-tuning-모델-사용">4. Fine-tuning 모델 사용</h3>
<blockquote>
<p>fine-tuning job이 끝나면 모델 제작이 완료된 것이니....바로 사용하면 됩니다ㅎ
위에서 설명했다시피 <strong>Text-generation API</strong>를 사용할 때, 우리는 사용할 모델명을 명시한다. 
해당 변수에 우리가 제작한 fine-tuning model 명을 넣어주면 된다. </p>
</blockquote>
<pre><code class="language-python">from openai import OpenAI
client = OpenAI()

completion = client.chat.completions.create(
  model=&quot;ft:gpt-3.5-turbo:my-org:custom_suffix:id&quot;,
  messages=[
    {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: &quot;You are a helpful assistant.&quot;},
    {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;Hello!&quot;}
  ]
)
print(completion.choices[0].message)</code></pre>
<h2 id="parameter-1">Parameter</h2>
<blockquote>
<p>아래의 코드에서와 같이 fine-tuning job을 생성할 때, hyperparameter 설정을 하면 됩니다 :)</p>
</blockquote>
<pre><code class="language-python">client.fine_tuning.jobs.create(
  training_file=&quot;file-abc123&quot;,
  model=&quot;gpt-3.5-turbo&quot;,
  hyperparameters={
    &quot;n_epochs&quot;:2,
    &quot;batch_size&quot;:1,
    &quot;learning_rate_multiplier&quot;:1
  }
)
</code></pre>
<p>fine-tuning API의 경우 아래 사진에서 보여지는 3가지 parameter에 대해 설명할 것입니다 :)</p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/84cfb56a-e0a5-4630-ad6f-2be111bcfb40/image.png" alt=""></p>
<figcaption style="text-align:center; font-size:15px; color:#808080; margin-top:40px">출처: https://platform.openai.com/finetune</figcaption>

<h3 id="batch_size">batch_size</h3>
<blockquote>
<p><strong>batch_size</strong>: 배치 크기는 <strong>한 번의 학습(iteration)에 사용되는 데이터 샘플의 수</strong>를 의미합니다. </p>
</blockquote>
<p><strong>배치 크기가 크면 모델 파라미터가 업데이트되는 빈도가 줄어들지만</strong>, 업데이트할 때의 변동성이 낮아집니다. 큰 배치 크기는 계산 효율성을 높일 수 있지만, 때로는 <strong>과적합을 피하기 위해 작은 배치 크기가 더 적합</strong>할 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/47edb63d-c1fd-4ac4-91fe-4da88a1f211f/image.png" alt=""></p>
<figcaption style="text-align:center; font-size:15px; color:#808080; margin-top:40px">
    출처: https://blog.paperspace.com/how-to-maximize-gpu-utilization-by-finding-the-right-batch-size/#resources
  </figcaption>

<p>배치 크기에 대한 직관적인 이해를 위해, 훈련 데이터셋에 1000개의 샘플이 있다고 가정해 보겠습니다. </p>
<p>이 경우 <strong>배치 크기가 10이라면, 데이터셋은 100개의 배치로 나뉘며</strong>, 각 배치에는 10개의 샘플이 포함됩니다. 모델은 각 배치를 처리할 때마다 가중치를 업데이트하고, 이러한 과정을 전체 데이터셋에 대해 반복합니다.</p>
<p><strong>배치 크기가 1이라면</strong> (확률적 경사 하강법), 각 샘플을 독립적으로 처리하고, 각 샘플마다 가중치를 업데이트합니다. 이는 높은 변동성을 가지지만 더 빠른 수렴을 가져올 수 있습니다. 흔히, 배치 사이즈가 작을 때, <strong>underfitting(과소적합)</strong>의 위험성이 있다고 합니다.</p>
<p><strong>배치 크기가 1000이라면</strong> (전체 배치 학습법), 전체 데이터셋을 한 번에 처리하고, 한 번의 큰 가중치 업데이트를 합니다. 이는 계산적으로 안정적이지만, 메모리 사용이 많고, 때로는 지역 최소값에 갇힐 위험이 있습니다. 흔히, 배치 사이즈가 클 때, <strong>overfitting(과적합)</strong>의 위험성이 있다고 합니다.</p>
<p>이처럼 배치 크기는 <strong>메모리 사용, 학습 속도, 모델의 성능에 영향</strong>을 주기 때문에 신중하게 운영할 필요가 있습니다.</p>
<h3 id="learning_rate_multiplier">learning_rate_multiplier</h3>
<blockquote>
<p><strong>learning_rate_multiplier</strong>: 학습률은 모델이 학습하는 속도나 정도를 조절하는 값입니다. </p>
</blockquote>
<p>기본적으로는 모델이 학습할 때 각 파라미터를 얼마나 조정할지 결정합니다. </p>
<p>학습률 배수는 일종의 기본 학습률에 적용되는 <strong>스케일링 인자</strong>입니다. 이 값을 조정하여 모델의 학습률을 늘리거나 줄일 수 있습니다. 
<img src="https://velog.velcdn.com/images/idle-danie/post/0efed600-613c-473f-a489-6450b53b10be/image.png" alt=""></p>
<figcaption style="text-align:center; font-size:13px; color:#808080; margin-top:40px">
    출처:   https://www.deeplearningwizard.com/deep_learning/boosting_models_pytorch/lr_scheduling/#learning-intuition-recap
  </figcaption>


<p><strong>학습률이 너무 높으면</strong> 모델이 <strong>최적의 해를 지나쳐 버리는 현상</strong>이 발생할 수 있어, 결국에는 학습 과정에서 불안정해지고, 모델의 성능이 안좋아집니다. </p>
<p>반면, <strong>너무 낮은 학습률</strong>은 모델이 데이터의 핵심 특징을 충분히 학습하기까지 <strong>매우 오랜 시간</strong>이 걸릴 수 있습니다. 이는 효율적이지 않으며, 오히려 충분한 학습이 이루어지지 않을 경우 예상치 못한 결과를 초래할 수 있습니다.</p>
<h3 id="n_epochs">n_epochs</h3>
<blockquote>
<p><strong>n_epochs</strong>: 전체 훈련 데이터가 학습에 한 번 사용되는 주기를 의미합니다. </p>
</blockquote>
<p>즉, 에포크 수는 머신 러닝 모델이 전체 훈련 데이터 세트를 한 번 완전히 통과하는 횟수를 의미합니다. </p>
<p>모델이 한 에포크 동안 훈련 데이터 전체를 한 번 통과하므로, <strong>에포크 수를 늘릴수록 모델은 데이터를 더 많이 볼 수 있으며</strong>, 이는 복잡한 문제를 더 잘 학습하는 데 도움이 될 수 있습니다. </p>
<p>하지만 에포크 수가 지나치게 높으면 훈련 시간이 길어지고, 모델이 <strong>훈련 데이터에 과도하게 적합되어 과적합을 초래</strong>할 수 있습니다.</p>
<h1 id="tip">Tip</h1>
<h2 id="토큰-관리는-필수">토큰 관리는 필수!</h2>
<blockquote>
<p>기본적으로 토큰은 곧 돈입니다. 따라서, 무작정 사용하다가는 돈쭐날 수 있을 위험이 있으므로 아래 몇 가지 추천드리는 가이드라인을 참고해주세요. </p>
</blockquote>
<h3 id="1-max_token-운영으로-사용되는-토큰의-양-제한">1. max_token 운영으로 사용되는 토큰의 양 제한</h3>
<p>max_tokens 변수를 설정하면, 사용할 토큰의 양을 제한할 수 있다. max_token의 값은 입력과 답변의 목적에 따라 달라지겠지만, 이러한 부분도 실험을 통해 조절하길 권장한다.</p>
<h3 id="2-한글-사용으로-인한-지나친-토큰-소모-지양">2. 한글 사용으로 인한 지나친 토큰 소모 지양</h3>
<p>기본적으로, 한국어 문자에 사용되는 토큰 개수는 영어 문자에 사용되는 토큰 개수보다 훨씬 많다. 상황에 따라 다르기 때문에, 구체적인 수치를 제시하기 어렵지만 적어도 2배 이상은 소요된다고 예상된다. 굳이 한국어에 대한 입출력이 필요하지 않은 모델이라면, 아래의 사항을 고려해보길 바란다.</p>
<ul>
<li>유료 번역 API 사용: google-translate api, deep-l api (추천), papago api</li>
<li>웹 번역기 크롤링: 동적 크롤링을 활용하여, 웹 번역기에 사용할 한국어 텍스트를 입력하고 결과값을 다시 크롤링하여 사용</li>
</ul>
<h2 id="명확한-실험기준과-평가-파이프라인-구축-중요">명확한 실험기준과 평가 파이프라인 구축 중요!</h2>
<blockquote>
<p>본 글을 요약하자면, OpenAI API를 사용하여 Chat-completion을 하거나 fine-tuning을 할 때 중요한 두가지는 아래와 같다.</p>
</blockquote>
<ul>
<li>parameter 운영</li>
<li>prompt-engineering</li>
</ul>
<p>전자에 대해서는 본 글에서 설명한 개념을 이해하고, 이를 바탕으로 실험기준을 세워야 합니다.
당연히, 파라미터 값은 context와 적용할 도메인의 성격에 따라 달라질 수 있기에 더더욱 명확한 실험기준으로 빠르게 최적해를 찾는 것이 중요합니다. </p>
<p><del>그러나, 결국 중요한 것은 신뢰성 있는 <strong>training_data에 대한 확보 + 도메인 전문가의 평가</strong> 라고 생각이 듭니다. 뻔한 소리지만, data의 퀄리티는 늘 중요한 것이겠지요ㅎ</del></p>
<p>후자에 대해서는, 본 글에서 따로 다루고 있지 않지만 prompt를 잘 구성하는 것도 LLM 성능에 지대한 영향을 끼칩니다. 그러니, 기본적인 prompt 구성법과 이에 대한 실험 기준을 명확하게 하는 것도 중요하다.</p>
<blockquote>
<p>최근 mlflow에서는 LLM prompt 운영에 용이한 template도 제공하고 있고, 당연히 모델에 대한 evaluate 파이프라인도 구축할 때 활용할 수 있으므로 mlflow도 적극 활용하길 추천합니다.
<a href="https://mlflow.org/docs/latest/llms/llm-evaluate/index.html">https://mlflow.org/docs/latest/llms/llm-evaluate/index.html</a></p>
</blockquote>
<h2 id="첨언">첨언</h2>
<ul>
<li>혹시, 머신러닝에 대한 이해가 부족한 것 같다면 제가 이전에 올린 <a href="https://velog.io/@idle-danie/%EC%9A%A9%EC%96%B4-%EC%A0%95%EB%A6%AC">ML/DL 용어 정리</a> 포스팅을 참고해주시면 좋을 것 같습니다ㅎ</li>
<li>꼭, API를 사용하기 전에 <a href="https://platform.openai.com/playground/chat">OpenAI playground</a>에서 체험해보세요!</li>
</ul>
<blockquote>
<h2 id="참고문헌">참고문헌</h2>
</blockquote>
<ul>
<li><a href="https://platform.openai.com/docs/api-reference">https://platform.openai.com/docs/api-reference</a></li>
<li><a href="https://platform.openai.com/playground/chat">https://platform.openai.com/playground/chat</a></li>
<li><a href="https://www.trustedreviews.com/versus/chat-gpt-4-vs-chat-gpt-3-4309130">https://www.trustedreviews.com/versus/chat-gpt-4-vs-chat-gpt-3-4309130</a></li>
<li><a href="https://www.researchgate.net/figure/Schematic-drawing-of-the-fine-tuned-pipeline-left-branch-and-in-the-zero-shot-pipeline_fig1_374642679">https://www.researchgate.net/figure/Schematic-drawing-of-the-fine-tuned-pipeline-left-branch-and-in-the-zero-shot-pipeline_fig1_374642679</a></li>
<li><a href="https://blog.paperspace.com/how-to-maximize-gpu-utilization-by-finding-the-right-batch-size/#resources">https://blog.paperspace.com/how-to-maximize-gpu-utilization-by-finding-the-right-batch-size/#resources</a></li>
<li><a href="https://etd.ohiolink.edu/apexprod/rws_etd/send_file/send?accession=osu1587693436870594&amp;ref=blog.paperspace.com">https://etd.ohiolink.edu/apexprod/rws_etd/send_file/send?accession=osu1587693436870594&amp;ref=blog.paperspace.com</a></li>
<li><a href="https://www.deeplearningwizard.com/deep_learning/boosting_models_pytorch/lr_scheduling/#step-wise-decay-every-epoch">https://www.deeplearningwizard.com/deep_learning/boosting_models_pytorch/lr_scheduling/#step-wise-decay-every-epoch</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[OOP practice by ATM (feat. 파이썬, Airflow Dag)]]></title>
            <link>https://velog.io/@idle-danie/OOP</link>
            <guid>https://velog.io/@idle-danie/OOP</guid>
            <pubDate>Thu, 27 Jul 2023 23:09:49 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Airflow Dag를 작성하다 보면 객체 지향 프로그래밍에 대한 중요성을 느낀다.
오랜만에 문법도 익힐 겸 ATM도 구현해보고 이전에 공부했던 전공 책을 꺼내어 OOP 개념과 python class 문법을 복습해보았다. </p>
</blockquote>
<h2 id="python-class-구현-방법">python class 구현 방법</h2>
<p>파이썬에서 클래스를 구현하는 기본 방법을 간단한 계산기 구현 예제로 살펴보겠습니다.</p>
<pre><code class="language-python"> class Calculator:
     def __init__(self):
         self.result = 0

     def add(self, num):
         self.result += num
         return self.result

 cal1 = Calculator()
 cal2 = Calculator()

 class FourCal:
     def __init__(self, first, second):
         self.first = first
         self.second = second
     def setdata(self, first, second):
         self.first = first
         self.second = second
     def add(self):
         result = self.first + self.second
         return result

 class MoreFourCal(FourCal):
     pass

 a = MoreFourCal(4,2)
 print(a.add())</code></pre>
<p>이 예제에서는 기본적인 클래스 구현과 상속에 대해 보여줍니다. Calculator 클래스는 기본적인 덧셈 기능을 제공하며, FourCal 클래스는 두 숫자를 더하는 메서드를 포함합니다. MoreFourCal 클래스는 FourCal 클래스를 상속받아 추가 기능 없이 그대로 사용합니다. 이제 이를 기반으로 ATM기를 구현하여 객체 지향 프로그래밍(OOP)의 원칙을 다시 익혀보겠습니다.</p>
<h2 id="atm기를-구현하며-oop를-다시-익히다">ATM기를 구현하며 OOP를 다시 익히다</h2>
<blockquote>
<p>OOP를 처음 접하는 분들에게는 어떠한 것도 참고하지 마시고 최소 기능만 구상하시고 ATM 구현해보시는 것을 추천합니다.</p>
</blockquote>
<h3 id="atm-atm-controller-class-구현">ATM, ATM controller Class 구현</h3>
<pre><code class="language-python"># Import Bank class from the bank module

from bank import Bank

# Define the class ATM which represents the ATM machine

class ATM:

    # Constructor for ATM class. It takes a Bank object as argument

    def __init__(self, bank: Bank):
        self.bank = bank
        self.card_number = None
        self.account_number = None

    # Method for inserting card. It validates the card_number with the bank

    def insert_card(self, card_number: str) -&gt; None:
        if self.bank.validate_card(card_number):
            self.card_number = card_number
        else:
            raise ValueError(&quot;Invalid card&quot;)

    # Method for entering PIN. It validates the PIN with the bank

    def enter_pin(self, pin: str) -&gt; None:
        if not self.bank.validate_pin(self.card_number, pin):
            raise ValueError(&quot;Invalid PIN&quot;)

    # Method for getting a list of accounts linked to the card

    def get_accounts_list(self) -&gt; list:
        return self.bank.get_accounts(self.card_number)

    # Method for selecting an account. It validates if the account_name exists in the bank

    def select_account(self, account_name: str) -&gt; None:
        if account_name not in self.bank.get_accounts(self.card_number):
            raise ValueError(&quot;Account does not exist&quot;)
        self.account_number = account_name

    # Method for checking the balance of the selected account

    def check_balance(self) -&gt; int:
        return self.bank.get_balance(self.card_number, self.account_number)

    # Method for depositing money into the selected account

    def deposit_money(self, amount: int) -&gt; None:
        self.bank.deposit(self.card_number, self.account_number, amount)

    # Method for withdrawing money from the selected account

    def withdraw_money(self, amount: int) -&gt; None:
        self.bank.withdraw(self.card_number, self.account_number, amount)

# Define the class ATMController which controls the ATM machine

class ATMController:
    def __init__(self, atm: ATM):
        self.atm = atm

    def insert_card(self, card_number: str) -&gt; str:
        try:
            self.atm.insert_card(card_number)
            return &quot;Your card has been successfully inserted&quot;
        except ValueError as e:
            return str(e)

    def enter_pin(self, pin: str) -&gt; str:
        try:
            self.atm.enter_pin(pin)
            return &quot;PIN number entered successfully&quot;
        except ValueError as e:
            return str(e)

    def get_accounts_list(self) -&gt; list:
        try:
            return self.atm.get_accounts_list()
        except ValueError as e:
            return str(e)

    def select_account(self, account_name: str) -&gt; str:
        try:
            self.atm.select_account(account_name)
            return &quot;Account has been successfully selected&quot;
        except ValueError as e:
            return str(e)

    def check_balance(self) -&gt; int:
        try:
            return self.atm.check_balance()
        except ValueError as e:
            return str(e)

    def deposit_money(self, amount: int) -&gt; str:
        try:
            self.atm.deposit_money(amount)
            return &quot;Deposit has been processed&quot;
        except ValueError as e:
            return str(e)

    def withdraw_money(self, amount: int) -&gt; str:
        try:
            self.atm.withdraw_money(amount)
            return &quot;Withdrawal has been processed&quot;
        except ValueError as e:
            return str(e)</code></pre>
<h3 id="atm-class-workflow">ATM Class workflow</h3>
<blockquote>
</blockquote>
<ul>
<li>ATM 클래스는 Bank 클래스의 객체를 받아 초기화됩니다.</li>
<li>insert_card, enter_pin, get_accounts_list, select_account, check_balance, deposit_money, withdraw_money 메서드를 통해 ATM의 기본 기능을 수행합니다.</li>
<li>각 메서드는 Bank 클래스의 메서드를 호출하여 필요한 작업을 수행합니다.</li>
</ul>
<h3 id="atm-controller-class-workflow">ATM Controller Class workflow</h3>
<blockquote>
<ul>
<li>ATMController 클래스는 ATM 객체를 받아 초기화됩니다.</li>
</ul>
</blockquote>
<ul>
<li>각 메서드는 ATM 클래스의 메서드를 호출하여 결과를 반환하거나 예외를 처리합니다.</li>
<li>insert_card, enter_pin, get_accounts_list, select_account, check_balance, deposit_money, withdraw_money 메서드를 통해 ATM의 기능을 제어합니다.</li>
</ul>
<h3 id="bank-class-구현">Bank Class 구현</h3>
<pre><code class="language-python">from abc import ABC, abstractmethod

# This class represents the blueprint for any bank

class Bank(ABC):

    # Method to check if a card is valid

    @abstractmethod
    def validate_card(self, card_number: str) -&gt; bool:
        pass

    # Method to verify the entered PIN

    @abstractmethod
    def validate_pin(self, card_number: str, pin: str) -&gt; bool:
        pass

    # Method to retrieve all accounts associated with a card

    @abstractmethod
    def get_accounts(self, card_number: str) -&gt; list:
        pass

    # Method to check balance of an account

    @abstractmethod
    def get_balance(self, card_number: str, account_number: str) -&gt; int:
        pass

    # Method to deposit money into an account

    @abstractmethod
    def deposit(self, card_number, account_number: str, amount: int) -&gt; None:
        pass

    # Method to withdraw money from an account

    @abstractmethod
    def withdraw(self, card_number, account_number: str, amount: int) -&gt; None:
        pass

# MockBank is a basic bank for testing purposes

class MockBank(Bank):

    # MockBank has some predefined accounts for testing

    def __init__(self):
        self.accounts = {
            &quot;123456&quot;: {&quot;pin&quot;: 1234, &quot;accounts&quot;: {&quot;account1&quot;: 5000, &quot;account2&quot;: 10000}},
            &quot;654321&quot;: {&quot;pin&quot;: 4321, &quot;accounts&quot;: {&quot;account1&quot;: 3000, &quot;account2&quot;: 20000}}
        }

    def validate_card(self, card_number: str) -&gt; bool:
        return card_number in self.accounts

    def validate_pin(self, card_number: str, pin: int) -&gt; bool:
        return self.accounts[card_number][&quot;pin&quot;] == pin

    def get_accounts(self, card_number: str) -&gt; list:
        return list(self.accounts[card_number][&quot;accounts&quot;].keys())

    def get_balance(self, card_number: str, account_name: str) -&gt; int:
        return self.accounts[card_number][&quot;accounts&quot;][account_name]

    def deposit(self, card_number: str, account_name: str, amount: int) -&gt; None:
        self.accounts[card_number][&quot;accounts&quot;][account_name] += amount

    def withdraw(self, card_number: str, account_name: str, amount: int) -&gt; None:
        if self.accounts[card_number][&quot;accounts&quot;][account_name] &lt; amount:
            raise ValueError(&quot;balance is insufficient&quot;)
        self.accounts[card_number][&quot;accounts&quot;][account_name] -= amount</code></pre>
<h3 id="bank-class-workflow">Bank Class workflow</h3>
<blockquote>
<ul>
<li>Bank 클래스는 추상 클래스(ABC)로 정의되어 있으며, 여러 추상 메서드를 포함합니다.</li>
</ul>
</blockquote>
<ul>
<li>MockBank 클래스는 Bank 클래스를 상속받아 실제 구현을 제공합니다.</li>
<li>validate_card, validate_pin, get_accounts, get_balance, deposit, withdraw 메서드를 통해 카드 유효성 검사, PIN 검사, 계좌 조회, 잔액 조회, 입금 및 출금 기능을 제공합니다.</li>
</ul>
<h3 id="컨트롤러-테스트-by-unittest">컨트롤러 테스트 by unittest</h3>
<pre><code class="language-python">from bank import MockBank
from atm import ATM, ATMController
import unittest

class ATMControllerTest(unittest.TestCase):
    def setUp(self):
        self.bank = MockBank()
        self.atm = ATM(self.bank)
        self.atm_controller = ATMController(self.atm)

    def test_insert_card(self):
        result = self.atm_controller.insert_card(&quot;123456&quot;)
        self.assertEqual(result, &quot;Your card has been successfully inserted&quot;)

    def test_enter_pin(self):
        self.atm_controller.insert_card(&quot;123456&quot;)
        result = self.atm_controller.enter_pin(1234)
        self.assertEqual(result, &quot;PIN number entered successfully&quot;)

    def test_get_accounts_list(self):
        self.atm_controller.insert_card(&quot;123456&quot;)
        result = self.atm_controller.get_accounts_list()
        self.assertEqual(result, [&quot;account1&quot;, &quot;account2&quot;])

    def test_select_account(self):
        self.atm_controller.insert_card(&quot;123456&quot;)
        self.atm_controller.get_accounts_list()
        result = self.atm_controller.select_account(&quot;account1&quot;)
        self.assertEqual(result, &quot;Account has been successfully selected&quot;)

    def test_check_balance(self):
        self.atm_controller.insert_card(&quot;123456&quot;)
        self.atm_controller.get_accounts_list()
        self.atm_controller.select_account(&quot;account1&quot;)
        result = self.atm_controller.check_balance()
        self.assertEqual(result, 5000)

    def test_deposit_money(self):
        self.atm_controller.insert_card(&quot;123456&quot;)
        self.atm_controller.get_accounts_list()
        self.atm_controller.select_account(&quot;account1&quot;)
        result = self.atm_controller.deposit_money(1000)
        self.assertEqual(result, &quot;Deposit has been processed&quot;)

    def test_withdraw_money(self):
        self.atm_controller.insert_card(&quot;123456&quot;)
        self.atm_controller.get_accounts_list()
        self.atm_controller.select_account(&quot;account1&quot;)
        result = self.atm_controller.withdraw_money(3000)
        self.assertEqual(result, &quot;Withdrawal has been processed&quot;)

if __name__ == &#39;__main__&#39;:
    unittest.main()

# if hard to understand unittest, use this
# def test_ATMController():

#     bank = MockBank()
#     atm = ATM(bank)
#     atm_controller = ATMController(atm)

#     result = atm_controller.insert_card(&quot;654321&quot;)
#     assert result == &quot;Your card has been successfully inserted&quot;

#     result = atm_controller.enter_pin(4321)
#     assert result == &quot;PIN number entered successfully&quot;

#     result = atm_controller.get_accounts_list()
#     assert result == [&quot;account1&quot;, &quot;account2&quot;]

#     result = atm_controller.select_account(&quot;account1&quot;)
#     assert result == &quot;Account has been successfully selected&quot;

#     result = atm_controller.check_balance()
#     assert result == 3000

#     result = atm_controller.deposit_money(1000)
#     assert result == &quot;Deposit has been processed&quot;

#     result = atm_controller.withdraw_money(3000)
#     assert result == &quot;Withdrawal has been processed&quot;

# test_ATMController()</code></pre>
<h3 id="test-class-workflow">Test Class workflow</h3>
<blockquote>
<ul>
<li>ATMControllerTest 클래스는 unittest.TestCase를 상속받아 ATM의 다양한 기능을 테스트합니다.</li>
</ul>
</blockquote>
<ul>
<li>setUp 메서드를 통해 테스트 환경을 초기화합니다.</li>
<li>각 테스트 메서드는 ATMController 클래스의 메서드를 호출하여 기능을 검증합니다.</li>
</ul>
<p>해당 예제를 통해 기본적인 클래스 구현, 상속, 추상 클래스 사용 및 유닛 테스트 작성 방법을 알아보았습니다.</p>
<h2 id="airflow-dag-작성에서-객체-지향-프로그래밍oop의-중요성">Airflow DAG 작성에서 객체 지향 프로그래밍(OOP)의 중요성</h2>
<p>개인적인 생각으로, Airflow DAG(DAG: Directed Acyclic Graph)을 작성할 때 객체 지향 프로그래밍(OOP)이 중요한 이유는 특정 작업(task)을 클래스로 정의하고, 이를 여러 DAG에서 재사용하는 것이 Airflow 아키텍처의 지향점에 부합하다고 생각해서이다.</p>
<h3 id="코드-예시-oop를-활용한-airflow-dag-작성">코드 예시: OOP를 활용한 Airflow DAG 작성</h3>
<h4 id="step-1-공통-작업-클래스를-정의">Step 1: 공통 작업 클래스를 정의</h4>
<pre><code class="language-python">from airflow.models import BaseOperator
from airflow.utils.decorators import apply_defaults

class MyTaskOperator(BaseOperator):
    @apply_defaults
    def __init__(self, param1, param2, *args, **kwargs):
        super(MyTaskOperator, self).__init__(*args, **kwargs)
        self.param1 = param1
        self.param2 = param2

    def execute(self, context):
        # 작업 실행 로직
        print(f&quot;Executing task with {self.param1} and {self.param2}&quot;)</code></pre>
<h4 id="step-2-dag-작성-시-작업-클래스를-활용">Step 2: DAG 작성 시 작업 클래스를 활용</h4>
<pre><code class="language-python">from airflow import DAG
from airflow.operators.dummy_operator import DummyOperator
from datetime import datetime

# DAG 기본 설정
default_args = {
    &#39;owner&#39;: &#39;airflow&#39;,
    &#39;start_date&#39;: datetime(2023, 1, 1),
    &#39;retries&#39;: 1,
}

# DAG 정의
with DAG(dag_id=&#39;my_dag&#39;, default_args=default_args, schedule_interval=&#39;@daily&#39;) as dag:
    start = DummyOperator(task_id=&#39;start&#39;)

    # MyTaskOperator를 사용하여 작업 생성
    task1 = MyTaskOperator(task_id=&#39;task1&#39;, param1=&#39;value1&#39;, param2=&#39;value2&#39;)
    task2 = MyTaskOperator(task_id=&#39;task2&#39;, param1=&#39;value3&#39;, param2=&#39;value4&#39;)

    end = DummyOperator(task_id=&#39;end&#39;)

    # 작업 의존성 설정
    start &gt;&gt; task1 &gt;&gt; task2 &gt;&gt; end</code></pre>
<h3 id="코드-설명">코드 설명</h3>
<ol>
<li><p><strong>MyTaskOperator 클래스 정의</strong>:</p>
<ul>
<li><code>MyTaskOperator</code>는 <code>BaseOperator</code>를 상속받아 구현한 클래스입니다.</li>
<li><code>__init__</code> 메서드에서 필요한 매개변수를 초기화합니다.</li>
<li><code>execute</code> 메서드에서 실제 작업을 수행하는 로직을 작성합니다.</li>
</ul>
</li>
<li><p><strong>DAG 정의</strong>:</p>
<ul>
<li>DAG를 정의할 때 <code>MyTaskOperator</code>를 사용하여 작업을 생성합니다.</li>
<li><code>start</code>, <code>task1</code>, <code>task2</code>, <code>end</code> 작업을 정의하고, 작업 간의 의존성을 설정합니다.</li>
</ul>
</li>
</ol>
<h3 id="결론">결론</h3>
<p>Airflow DAG를 작성할 때 객체 지향 프로그래밍(OOP)을 활용하면 코드의 재사용성, 유지보수성, 확장성, 가독성을 크게 향상시킬 수 있기에, 예시처럼 작업을 클래스로 정의하고 이를 여러 DAG에서 재사용하면, 복잡한 워크플로우를 보다 효율적으로 관리할 수 있습니다 :)</p>
<blockquote>
</blockquote>
<p>ATM 코드에 대한 실행방법과 추가로 현금함을 구현하기 위한 가이드라인은 아래 github 링크의 README.md에 포함되어 있습니다 :)
소스코드: <a href="https://github.com/idle-danie/OOP_atm">https://github.com/idle-danie/OOP_atm</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ML/DL 용어정리]]></title>
            <link>https://velog.io/@idle-danie/%EC%9A%A9%EC%96%B4-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@idle-danie/%EC%9A%A9%EC%96%B4-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Thu, 27 Jul 2023 22:38:39 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>많이 사용되지만 혼동되는 ML/DL 관련 용어들과 개념들에 대해 정리해보았습니다 :)</p>
</blockquote>
<h1 id="ai-vs-ml-vs-dl">AI vs ML vs DL</h1>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/a3389327-9420-4042-8101-318115c979cf/image.png" alt=""></p>
<h1 id="components-of-deep-learning">Components of Deep Learning</h1>
<ul>
<li><p><strong><code>Data</code></strong>: 이 문제를 풀기 위해 어떤 데이터를 사용해야할까?</p>
</li>
<li><p><strong><code>Model</code></strong>: 어떤 모델이 원하는 결과를 최대한 잘 도출할까?
ex) GPT-3, LSTM, GAN, ResNet</p>
</li>
<li><p><strong><code>Loss function</code></strong> : 모델을 어떻게 학습시킬까?
ex) 
Regression type -&gt; MAE, MSE, RMSE
Classification type -&gt; Binary cross-entropy, Categorial cross-entropy</p>
</li>
</ul>
<ul>
<li><strong><code>Optimization Algorithm</code></strong>: 네트워크를 어떻게 줄일까?</li>
</ul>
<h1 id="overview-of-deep-learning-history-posted-by-denny-britz">Overview of Deep Learning history posted by Denny Britz</h1>
<blockquote>
<p>시간에 따른 딥러닝 모델의 발전과정 overview에 대한 글인데, 흥미롭게 읽어서 가져와보았습니다 :) 
<a href="https://dennybritz.com/posts/deep-learning-ideas-that-stood-the-test-of-time/">https://dennybritz.com/posts/deep-learning-ideas-that-stood-the-test-of-time/</a></p>
</blockquote>
<ul>
<li><p><strong><code>AlexNet (2012)</code></strong>
<img src="https://velog.velcdn.com/images/idle-danie/post/17568d2d-2980-4fc4-93a3-296263484364/image.png" alt=""></p>
</li>
<li><p><strong><code>DQN (2013)</code></strong>
<img src="https://velog.velcdn.com/images/idle-danie/post/fd0302ab-2b7b-46bc-b38e-b45cd6931a8a/image.png" alt=""></p>
</li>
<li><p><strong><code>Seq2Seq by Attention (2014)</code></strong>
<img src="https://velog.velcdn.com/images/idle-danie/post/9ac23196-d3fe-4382-81af-ae6d740d9481/image.png" alt=""></p>
</li>
<li><p><strong><code>Adam Optimizer (2014)</code></strong></p>
</li>
<li><p><strong><code>GAN (2014, 2015)</code></strong></p>
</li>
<li><p><strong><code>ResNet (2015)</code></strong>
<img src="https://velog.velcdn.com/images/idle-danie/post/0082fc68-d735-4304-8be7-553f17ed72dc/image.png" alt=""></p>
</li>
<li><p><strong><code>Transformer (2017)</code></strong>
<img src="https://velog.velcdn.com/images/idle-danie/post/9a6e9bb6-54d5-4c85-b3c6-79d05e6770a1/image.png" alt=""></p>
</li>
<li><p><strong><code>BERT and fine tuned models (2018)</code></strong>
<img src="https://velog.velcdn.com/images/idle-danie/post/a59834c6-0b24-4228-b4f1-b09d5294a990/image.png" alt=""></p>
</li>
<li><p><strong><code>Large Language models like GPT-3 (2019~)</code></strong>
<img src="https://velog.velcdn.com/images/idle-danie/post/344e9fda-21a3-4e58-8f73-60ed7a151c3b/image.png" alt=""></p>
</li>
</ul>
<h1 id="optimization">Optimization</h1>
<ul>
<li><p><strong><code>Generalization</code></strong>: 학습된 모델이 unseen data에서도 work well?</p>
<ul>
<li><strong><code>Generalization performance</code></strong> =&gt; Generalization gap = (Test error - Training error)</li>
</ul>
</li>
<li><p><strong><code>Overfitting</code></strong>: Training data에서는 well work, Test data에서는 not well work</p>
</li>
<li><p><strong><code>Underfitting</code></strong>: 네트워크가 간단하거나 train이 부족해서 Training data에서도 not well work</p>
</li>
<li><p><strong><code>parameter</code></strong>: 최적해에서 찾고 싶은 값 (ex: weight, bias)</p>
</li>
<li><p><strong><code>hyperparameter</code></strong>: output을 결정하는 변수 (ex: learning rate; 어떤 loss function을 사용할 것인지?)</p>
</li>
<li><p><strong><code>Cross-validation</code></strong> : Training data를 partition하여 Train data, Validation data에 적용 (Training data -&gt; Training data + Validation data) </p>
<ul>
<li>최적의 hyperparameter set을 찾고 고정한 상태에서 학습시킬 때는 모든 데이터 활용
(test data x)</li>
</ul>
</li>
<li><p><strong><code>Bias</code></strong>: 얼마나 목표 타겟에 가깝나</p>
<ul>
<li>low bias: 타겟에 가깝다</li>
<li>high bias: 타겟에서 멀다</li>
</ul>
</li>
<li><p><strong><code>Variance</code></strong>: 얼마나 모여있는지 </p>
<ul>
<li>low variance -&gt; 잘 모여있다</li>
<li>high variance : overfitting 가능성 큼</li>
</ul>
</li>
<li><p><strong><code>Bias and Variance Tradeoff</code></strong> : bias와 variance를 동시에 줄이기는 쉽지 않다</p>
</li>
<li><p><strong><code>Bootstrapping</code></strong>: dataset에서 무작위로 표본을 추출하여 여러 예측 모델 생성 (any test or metric that uses random sampling)</p>
</li>
<li><p><strong><code>Bagging (Boostrapping aggregating) 앙상블</code></strong> : 독립적으로 고정된 학습데이터로 모델 여러개를 훈련 (averaging or voting)</p>
<ul>
<li>예) 10만개의 학습데이터로 하나의 모델을 학습하지 않고, 80%로 n개의 모델을 돌리고 값의 평균 또는 voting 출력값을 사용 </li>
</ul>
</li>
</ul>
<h1 id="참고문헌">참고문헌</h1>
<blockquote>
<ol>
<li><a href="https://sungwookkang.com/1409">https://sungwookkang.com/1409</a></li>
<li><a href="https://brunch.co.kr/@mnc/9">https://brunch.co.kr/@mnc/9</a></li>
<li><a href="https://dennybritz.com/posts/deep-learning-ideas-that-stood-the-test-of-time/">https://dennybritz.com/posts/deep-learning-ideas-that-stood-the-test-of-time/</a></li>
</ol>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터 수집 실행시간 단축기: 결국은 병렬처리?]]></title>
            <link>https://velog.io/@idle-danie/%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%88%98%EC%A7%91-%EC%8B%A4%ED%96%89%EC%8B%9C%EA%B0%84-%EB%8B%A8%EC%B6%95%EA%B8%B0</link>
            <guid>https://velog.io/@idle-danie/%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%88%98%EC%A7%91-%EC%8B%A4%ED%96%89%EC%8B%9C%EA%B0%84-%EB%8B%A8%EC%B6%95%EA%B8%B0</guid>
            <pubDate>Tue, 27 Jun 2023 17:01:30 GMT</pubDate>
            <description><![CDATA[<h2 id="도입">도입</h2>
<blockquote>
<p>본 글은 <a href="https://www.acmicpc.net/">Baekjoon</a>에서 제공하는 알고리즘 문제 및 유저 데이터를 분석하는 [solved.ac] (<a href="https://solved.ac/">https://solved.ac/</a>) 서비스 및 API를 사용하여 관련 데이터를 추출한 뒤, DW/DM을 생성하고 ETL을 구축하는 <a href="https://github.com/devcourse-team1-2-baekjoon/project_baekjoon">프로젝트</a>의 일부 과정에 해당하는 내용입니다 :)</p>
</blockquote>
<p>본 내용에서 설명하는 구체적인 상황과 문제점은 아래와 같습니다.</p>
<h3 id="상황">상황</h3>
<blockquote>
<p>solved.ac API를 호출하여, 알고리즘 문제 관련 데이터를 추출하여 csv에 저장해야 한다.</p>
</blockquote>
<p>아무런 성능 개선 코드가 적용되지 않은 기본 API호출 코드는 아래와 같을 것이다. </p>
<pre><code class="language-python">import requests

url = &quot;https://~~~&quot;
headers = {&quot;Accept&quot;: &quot;application/json&quot;}

page = 1
while True:
    querystring = {&quot;query&quot;: &#39;&#39;, &quot;page&quot;: str(page)}
    response = requests.get(url, headers=headers, params=querystring)
    data = response.json()

    for item in data[&#39;items&#39;]:
        if item[&#39;tier&#39;] &lt; 27:  # If the &#39;tier&#39; is less than 7
            break
    page += 1</code></pre>
<h3 id="문제점">문제점</h3>
<p>일단 프로젝트 여건 상 <strong>로컬에서 파이프라인 실행</strong>해야 하기 때문에, 데이터가 커질수록, 메모리나 디스크 i/o와 같은 성능 이슈 발생한다. 또한, 위와 같은 이유일 수도 있겠지만 기본적으로 API를 호출하여 데이터를 추출하여 DW/DM에 적재하는 <strong>총 파이프라인 실행시간이 매우 길다</strong>. </p>
<p>문제가 발생한 이유를 <strong>pandas를 이용하여 데이터를 csv에 저장하는 방식에서 병목</strong>이 일어났다고 판단하여 디스크 i/o와 Memory를 활용하여 csv에 데이터를 저장하는 방식을 비교해보려고 한다. 또한, 전체적인 파이프라인 실행속도를 높이기 위해 우선 데이터를 추출하는 과정에서 파이썬 비동기 (async)를 도입한다.</p>
<h2 id="디스크-io-vs-memory">디스크 i/o vs Memory</h2>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/602f5e2e-ba83-421b-812f-3abd94af31b8/image.png" alt="">
일반적인 상식으로 생각해보면, 메모리를 활용한 방식이 더 빠를 것이고 코드의 가독성도 좋다.</p>
<p>아무리 좋은 SSD를 구비하여 i/o속도를 높여도, 메모리 용량을 업그레이드하는 것보다 컴퓨터 성능 개선에 효과적이지 않다는 것을 들어 보았을 것이다. </p>
<p>pandas 공식 문서에서도 메모리를 활용한 방식을 채택한다. 
<a href="https://pandas.pydata.org/docs/user_guide/scale.html">https://pandas.pydata.org/docs/user_guide/scale.html</a></p>
<p>그래도 일단 비교를 위해 두 방식을 모두 실행해보자.</p>
<h2 id="디스크-io를-활용한-추출">디스크 i/o를 활용한 추출</h2>
<blockquote>
<p>한번에 모든 데이터를 csv를 적재하는 것이 아닌 일정 bunch단위를 미리 정해놓고, 데이터가 정해놓은 데이터 단위 기준에 다다르면 그때마다 csv를 적재하는 방식이다.</p>
</blockquote>
<h3 id="디스크-io를-활용한-code">디스크 i/o를 활용한 code</h3>
<pre><code class="language-python">async def collect_data_and_save_to_csv():
    url = &quot;https://~~~&quot;

    async with aiohttp.ClientSession() as session:
        count = await fetch_page(session, url, 0, ua.random)
        max_page = count[&#39;count&#39;] // 50 + 1
        csv_file = os.path.join(os.getcwd(), &#39;async_get_user.csv&#39;)
        start_time = time.time()
        user_agent = ua.random

        try:
            page = 1
            while page &lt;= max_page:
                if page % 100 == 0:
                    user_agent = ua.random

                data = await fetch_page(session, url, page, user_agent)
                if data is None:
                    continue
                items = data[&#39;items&#39;]

                filtered_items = []
                for item in items:
                    if item[&#39;tier&#39;] == 6:
                        print(&quot;Tier 6 reached. Stopping data collection.&quot;)
                        return

                    filtered_item = {
                        &#39;handle&#39;: item[&#39;handle&#39;],
                        &#39;solvedCount&#39;: item[&#39;solvedCount&#39;],
                        &#39;tier&#39;: item[&#39;tier&#39;],
                        &#39;rating&#39;: item[&#39;rating&#39;],
                        &#39;ratingByProblemsSum&#39;: item[&#39;ratingByProblemsSum&#39;],
                        &#39;ratingByClass&#39;: item[&#39;ratingByClass&#39;],
                        &#39;ratingBySolvedCount&#39;: item[&#39;ratingBySolvedCount&#39;],
                        &#39;ratingByVoteCount&#39;: item[&#39;ratingByVoteCount&#39;],
                        &#39;class&#39;: item[&#39;class&#39;],
                        &#39;maxStreak&#39;: item[&#39;maxStreak&#39;],
                        &#39;joinedAt&#39;: item[&#39;joinedAt&#39;],
                        &#39;rank&#39;: item[&#39;rank&#39;]
                    }
                    filtered_items.append(filtered_item)

                df = pd.DataFrame(filtered_items)
                if page == 1:
                    df.to_csv(csv_file, index=False, encoding=&#39;utf-8&#39;)
                else:
                    df.to_csv(csv_file, mode=&#39;a&#39;, header=False, index=False, encoding=&#39;utf-8&#39;)

                page += 1
                print(f&quot;현재 페이지: {page}&quot;)

        except Exception as e:
            print(f&quot;An error occurred: {e}&quot;)
            return

        end_time = time.time()
        execution_time = end_time - start_time
        print(f&quot;Task 실행 시간: {execution_time}초&quot;)
        print(&quot;Data collection and saving completed successfully.&quot;)
</code></pre>
<h3 id="실행시간">실행시간</h3>
<blockquote>
<p>Tier 6 reached. Stopping data collection.
총 소요 시간: 512.5091943740845</p>
</blockquote>
<h2 id="메모리를-활용한-추출">메모리를 활용한 추출</h2>
<blockquote>
<p>위와 달리, 데이터를 호출하고 메모리에 담은 뒤 한번에 csv형태로 저장하는 방식이다.</p>
</blockquote>
<pre><code class="language-python">async def collect_data():
    url = &quot;https://~~~&quot;

    async with aiohttp.ClientSession() as session:
        count = await fetch_page(session, url, 0, ua.random)
        max_page = count[&#39;count&#39;]//50+1
        data_list = []

        try:
            page = 1
            user_agent = ua.random
            while page &lt;= max_page:
                if page % 100 == 0:
                    user_agent = ua.random

                data = await fetch_page(session, url, page, user_agent)
                if data is None:
                    continue
                items = data[&#39;items&#39;]

                filtered_items = []
                for item in items:
                    if item[&#39;tier&#39;] == 6:
                        print(&quot;Tier 6 reached. Stopping data collection.&quot;)
                        return data_list

                    filtered_item = {
                        &#39;handle&#39;: item[&#39;handle&#39;],
                        &#39;solvedCount&#39;: item[&#39;solvedCount&#39;],
                        &#39;tier&#39;: item[&#39;tier&#39;],
                        &#39;rating&#39;: item[&#39;rating&#39;],
                        &#39;ratingByProblemsSum&#39;: item[&#39;ratingByProblemsSum&#39;],
                        &#39;ratingByClass&#39;: item[&#39;ratingByClass&#39;],
                        &#39;ratingBySolvedCount&#39;: item[&#39;ratingBySolvedCount&#39;],
                        &#39;ratingByVoteCount&#39;: item[&#39;ratingByVoteCount&#39;],
                        &#39;class&#39;: item[&#39;class&#39;],
                        &#39;maxStreak&#39;: item[&#39;maxStreak&#39;],
                        &#39;joinedAt&#39;: item[&#39;joinedAt&#39;],
                        &#39;rank&#39;: item[&#39;rank&#39;]
                    }
                    filtered_items.append(filtered_item)

                data_list.extend(filtered_items)
                print(f&quot;현재 페이지: {page}&quot;)
                page += 1

        except Exception as e:
            print(f&quot;오류 발생: {e}&quot;)
            return

        return data_list
</code></pre>
<h3 id="실행시간-1">실행시간</h3>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/bc10bcfb-d793-4498-afb4-95b342240b75/image.png" alt="">
아래와 같이 특정 상황에서 멈추는 상황이 발생하였다. 여기서 주목할 점은, 멈추는 페이지의 지점이 실행 시마다 비슷하다는 점이다. 따라서, 디스크 i/o를 활용하는 방식을 채택하지 않는다면, 메모리 제한 및 gc 관리가 필요할 것으로 예측할 수 있다.</p>
<h2 id="problem">Problem</h2>
<p>사실 현재 측정하는 실행 속도는 로컬 환경의 요소로만 결정되지는 않는다. 
외부적 요소인 <strong>네트워크 상태와 request수 제한</strong>, 그리고 내부적 요소인 <strong>python garbage collection</strong>와 <strong>pandas 메모리 할당</strong> 등의 여러 변수가 있기 때문에 총 소요시간을 통해 명확한 결론을 낼 수는 없다.
단지, 이러한 모든 가능성을 인지하고 상황에 따른 엔지니어링을 해야한다는 것이 중요하다.</p>
<p>일단, 문제점을 정리해보면 아래와 같을 것이다.</p>
<h3 id="외부적-요인-네트워크-api-request-제한">외부적 요인: 네트워크, API request 제한</h3>
<blockquote>
<p><strong>주의</strong>
크롤링을 할 때는 robots.txt, API를 호출할 때는 어디가에 숨어있을 API가이드라인을 통해 명확히 request수 제한과 같은 제한사항을 꼭 체크하자. 이미 IP block이 이루어졌다면, 제한이 풀리길 기다리거나 VPN을 사용하는 방법을 사용해야 하니, 미리미리 인지하고 따르자.</p>
</blockquote>
<p>어떠한 내부적인 요소를 fix하여 퍼포먼스를 개선한다 해도, 네트워크 환경이 작업을 제한하면 의미가 없다.</p>
<p>제한을 지키며 극한의 퍼포먼스를 내기 위해서는 최대한 API 가이드라인에 따라 호출 시간을 <strong>sleep()</strong>을 활용하여 지연시키거나, <strong>asyncio에서 await</strong>을 사용하여 API 서버에 피해를 주지 않아야 한다. </p>
<p>위 사항으로 만족(?)되지 않는다면, 극한의 최적화를 위한 다음과 같은 방법들이 있을 수 있다. (권장 X)</p>
<ol>
<li><code>User agent 변경</code>
Python에서는 fake user agent 라이브러리를 활용하여 매 호출, 혹은 특정 횟수마다 user-agent를 변경해주면 다른 유저로 인식하여 request수 제한 같은 요소를 피할 수 있다. 
하지만, 이러한 이상한(?)행위도 봇으로 쉽게 식별될 수 있다는 점을 인지하자.</li>
<li><code>semaphore 활용</code> 
멀테쓰레딩을 사용하고 있다면, semaphore로 쓰레드 수를 제한하여 실행 속도를 조절할 수 있다.</li>
</ol>
<h3 id="내부적-요인-메모리-부하">내부적 요인: 메모리 부하</h3>
<p>한번에 모든 데이터를 메모리에 저장하고, 이를 변환하는 행위는 메모리의 부하를 발생시킬 수 있다. 
해결책은 뻔하겠지만, 나눠서 변환하면 된다! 또한, gc를 사용하여 적절히 메모리 관리를 개선할 수 있다. 
아래 코드는 10페이지마다 한 번씩 gc.collect()를 호출하여 메모리 관리를 개선한 code이다.</p>
<pre><code class="language-python">import gc
import aiohttp
import asyncio
from fake_useragent import UserAgent

ua = UserAgent()

async def fetch_page(session, url, page, user_agent):
    headers = {&#39;User-Agent&#39;: user_agent}
    params = {&#39;page&#39;: page}
    async with session.get(url, headers=headers, params=params) as response:
        if response.status == 200:
            return await response.json()
        return None

async def collect_data():
    url = &quot;https://~~~&quot;

    async with aiohttp.ClientSession() as session:
        count = await fetch_page(session, url, 0, ua.random)
        max_page = count[&#39;count&#39;] // 50 + 1
        data_list = []

        try:
            page = 1
            user_agent = ua.random
            while page &lt;= max_page:
                if page % 100 == 0:
                    user_agent = ua.random

                data = await fetch_page(session, url, page, user_agent)
                if data is None:
                    page += 1
                    continue
                items = data[&#39;items&#39;]

                filtered_items = []
                for item in items:
                    if item[&#39;tier&#39;] == 6:
                        print(&quot;Tier 6 reached. Stopping data collection.&quot;)
                        return data_list

                    filtered_item = {
                        &#39;handle&#39;: item[&#39;handle&#39;],
                        &#39;solvedCount&#39;: item[&#39;solvedCount&#39;],
                        &#39;tier&#39;: item[&#39;tier&#39;],
                        &#39;rating&#39;: item[&#39;rating&#39;],
                        &#39;ratingByProblemsSum&#39;: item[&#39;ratingByProblemsSum&#39;],
                        &#39;ratingByClass&#39;: item[&#39;ratingByClass&#39;],
                        &#39;ratingBySolvedCount&#39;: item[&#39;ratingBySolvedCount&#39;],
                        &#39;ratingByVoteCount&#39;: item[&#39;ratingByVoteCount&#39;],
                        &#39;class&#39;: item[&#39;class&#39;],
                        &#39;maxStreak&#39;: item[&#39;maxStreak&#39;],
                        &#39;joinedAt&#39;: item[&#39;joinedAt&#39;],
                        &#39;rank&#39;: item[&#39;rank&#39;]
                    }
                    filtered_items.append(filtered_item)

                data_list.extend(filtered_items)
                print(f&quot;현재 페이지: {page}&quot;)
                page += 1

                # 특정 간격으로 가비지 컬렉션 실행
                if page % 10 == 0:
                    gc.collect()

        except Exception as e:
            print(f&quot;오류 발생: {e}&quot;)
            return

        return data_list
</code></pre>
<p>위와 같이 코드 개선은 기존에 비해 20배 이상의 실행속도 개선을 가져왔다.</p>
<h3 id="실행시간-개선">실행시간 (개선)</h3>
<blockquote>
<p>Tier 6 reached. Stopping data collection.
총 소요 시간: 25.1141256242419</p>
</blockquote>
<h2 id="결국은-multi-process">결국은 Multi-Process?</h2>
<p>모든 개선 과정을 끝내고 문득 이러한 생각이 들었다. <strong><em>그냥 멀티 프로세싱을 쓰면 안되나?</em></strong>
이러한 흐름으로 처음 멀티 프로세싱을 접하게 되었다. </p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/bdab0546-5197-40af-8c8c-f0510d55e22b/image.png" alt=""></p>
<p>일반적으로, 실행속도 개선을 위해서 <strong>멀티 쓰레드, 비동기 호출, 멀티 프로세싱</strong>을 활용할 수 있다.
각각의 대한 정의를 간단하게 정리해보면 아래와 같을 것이다.  </p>
<p>*<em><code>멀티프로세싱 (Multi-processing)</code>
*</em>
멀티프로세싱은 여러 프로세스를 생성하여 병렬로 작업을 수행하는 방식으로, 각 프로세스는 별도의 메모리 공간을 사용하여 CPU 집약적인 작업에서 성능을 향상시킨다.</p>
<p>*<em><code>비동기 (Asynchronous)</code>
*</em>
비동기 프로그래밍은 작업이 완료될 때까지 기다리지 않고 다른 작업을 계속 수행하는 방식으로, 주로 io 바운드 작업에서 효율성을 높이고 응답성을 개선한다.</p>
<p>*<em><code>멀티쓰레딩 (Multit-hreading)</code>
*</em>
멀티쓰레딩은 단일 프로세스 내에서 여러 쓰레드를 생성하여 병렬로 작업을 수행하는 방식으로, 메모리를 공유하며 CPU 및 io 바운드 작업의 성능을 향상시킨다.</p>
<p>하지만 python은 GIL때문에 멀티 쓰레딩에 제한적일 수 밖에 없고 데이터의 매우 커질 경우 극한의 퍼포먼스를 위해서는 병렬처리를 선택할 수 밖에 없다고 생각한다. 멀티 프로세싱을 적용하여 최종 코드를 개선하면 아래와 같을 것이다. </p>
<pre><code class="language-python">import gc
import aiohttp
import asyncio
from fake_useragent import UserAgent
from concurrent.futures import ProcessPoolExecutor

ua = UserAgent()

async def fetch_page(session, url, page, user_agent):
    headers = {&#39;User-Agent&#39;: user_agent}
    params = {&#39;page&#39;: page}
    async with session.get(url, headers=headers, params=params) as response:
        if response.status == 200:
            return await response.json()
        return None

async def process_page(session, url, page):
    user_agent = ua.random
    data = await fetch_page(session, url, page, user_agent)
    if data is None:
        return []

    items = data[&#39;items&#39;]
    filtered_items = []
    for item in items:
        if item[&#39;tier&#39;] == 6:
            print(&quot;Tier 6 reached. Stopping data collection.&quot;)
            return filtered_items

        filtered_item = {
            &#39;handle&#39;: item[&#39;handle&#39;],
            &#39;solvedCount&#39;: item[&#39;solvedCount&#39;],
            &#39;tier&#39;: item[&#39;tier&#39;],
            &#39;rating&#39;: item[&#39;rating&#39;],
            &#39;ratingByProblemsSum&#39;: item[&#39;ratingByProblemsSum&#39;],
            &#39;ratingByClass&#39;: item[&#39;ratingByClass&#39;],
            &#39;ratingBySolvedCount&#39;: item[&#39;ratingBySolvedCount&#39;],
            &#39;ratingByVoteCount&#39;: item[&#39;ratingByVoteCount&#39;],
            &#39;class&#39;: item[&#39;class&#39;],
            &#39;maxStreak&#39;: item[&#39;maxStreak&#39;],
            &#39;joinedAt&#39;: item[&#39;joinedAt&#39;],
            &#39;rank&#39;: item[&#39;rank&#39;]
        }
        filtered_items.append(filtered_item)

    return filtered_items

async def collect_data():
    url = &quot;https://~~~&quot;

    async with aiohttp.ClientSession() as session:
        count = await fetch_page(session, url, 0, ua.random)
        max_page = count[&#39;count&#39;] // 50 + 1

        loop = asyncio.get_event_loop()
        tasks = []
        with ProcessPoolExecutor() as executor:
            for page in range(1, max_page + 1):
                tasks.append(loop.run_in_executor(executor, process_page, session, url, page))

            data_list = await asyncio.gather(*tasks)

        # Flatten the list of lists
        data_list = [item for sublist in data_list for item in sublist]

        # 특정 간격으로 가비지 컬렉션 실행
        gc.collect()

        return data_list</code></pre>
<p>하지만, 로컬과 같이 제한된 상황에서 core 수를 늘릴 수 있는 상황이 아니면, 사실상 멀티프로세싱을 통한 개선은 의미가 없다. </p>
<p>결국, 앞서 i/o vs Memory에서의 논점과 같이 문제가 발생하는 상황에 따라 적절한 대응방식을 상이하기 때문에, 단순 비교를 하는 것은 불가능하다. </p>
<h2 id="고찰">고찰</h2>
<p>결국 개발자의 실력은 <strong>기술적 이해도를 바탕으로 상황에 맞는 최적의 방법을 도출</strong>하는 수준에 비례하는 것 같다. 그래서, 경험과 지식을 기반으로 나름의 기준을 미리 설정하는 습관이 중요하다고 생각한다.</p>
<p>나는 고민의 시간을 줄이기 위해 나름의 3가지 고려사항 및 기준을 세웠다.</p>
<ol>
<li>간단한 태스크인가?
개인적으로 가지고 있는 기준은 일단 간단한 태스크라면 15분 안에 실행이 끝나야 한다.
15분의 기준은 AWS Lambda와 같은 severless 환경에서 요구하는 최대 실행 시간이다.
말 그대로, severless에서 task를 돌릴만큼의 심플한 태스크이어야 한다.
<em>(Airflow의 과부하를 줄이고 간단한 task는 Lambda에서 진행 -&gt; Lambda task call Dag)</em></li>
<li>쓰레드 간의 연관성이 결과에 주요한 영향을 미치지 않아야 한다. 
예를 들어, 크롤링 작업이라면 페이지 순서가 고려하지 않아도 된다는 점이다.</li>
<li>하나의 쓰레드가 실패한다면?
하나의 쓰레드가 실패하면 아래의 사진처럼 전체 프로세스에 영향을 미치게 된다. 따라서 하나의 쓰레드가 하는 일을 정확하게 파악하는 것이 중요하다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/d8a15ed0-b4d6-4dc1-9d23-4af1f6fd3f4e/image.png" alt=""></p>
<blockquote>
<h3 id="참고문헌">참고문헌</h3>
<p><a href="https://eunjinii.tistory.com/41">https://eunjinii.tistory.com/41</a></p>
</blockquote>
<p>ps. 2024년에서 이 글을 다시 보니, 약간 민망하다. 하지만, 당시에는 해당 인사이트를 얻기까지 엄청난 고뇌의 시간이 들었던 것으로 기억한다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Data와 Data Engineer의 역할 (feat. OLAP)]]></title>
            <link>https://velog.io/@idle-danie/Data-Data-Team-Data-Engineer</link>
            <guid>https://velog.io/@idle-danie/Data-Data-Team-Data-Engineer</guid>
            <pubDate>Tue, 23 May 2023 13:37:44 GMT</pubDate>
            <description><![CDATA[<h2 id="데이터로-할-수-있는-것">데이터로 할 수 있는 것?</h2>
<blockquote>
<p>데이터를 통해 <strong>사용자의 서비스 경험을 개선</strong>하고 <strong>운영비용을 감소</strong>시켜 결과적으로 <strong>회사의 의사결정</strong>을 도울 수 있다.
하지만 전적으로 데이터만 믿고 따라가기만 한다고 성공을 보장할 수 없다.
따라서, 아래의 data decision을 대표하는 두 정의를 살펴보고 &#39;데이터에 의한 결정은 무조건 옳다&#39;라는 편협한 사고를 없애야 한다. 결과적으로, 회사의 크기와 상황에 맞는 결정론을 기반으로 의사결정을 진행해야 한다는 것이 중요하다.</p>
</blockquote>
<h3 id="data-driven-decision-vs-data-informed-decision">Data driven decision vs Data informed decision</h3>
<p><strong><code>Data driven decision</code></strong> : 결정의 주체와 수단이 모두 데이터인 data-decison</p>
<p>기본적으로 데이터를 기반으로 의사결정을 진행합니다. 개인의 주관을 배제하고, 어떤 선택이 나은지 결정할 수 있는 가설을 테스트 및 검증하여 결과를 의사결정에 반영하는 것입니다.</p>
<p><strong><code>Data informed decision</code></strong> : 위와 같이 결정의 주체는 사람이지만, 수단에는 데이터를 포함한 다른 결정 요소들이 개입하는 data-decison</p>
<p>데이터를 보고 주관적인 의견을 생성하여 결론을 도출하는 것이라고 이해하면 될 것 같다. 따라서, 해당 결정을 진행하기 위해서는 기본적으로 Data Team의 인원들과 의사결정 주체가 주요 지표와 도메인 지식에 대한 이해도가 필수적으로 요구될 것이다.</p>
<h2 id="data-team-구성">Data Team 구성</h2>
<blockquote>
<p>모든 데이터 팀이 아래와 같이 이루어져 있다고 말할 수 없지만, 성숙한 데이터 팀의 경우 보통 아래 사진과 같은 구성을 보인다.
<del>(그림에 재능은 없으니 이해해 주시길...ㅎ)</del> 보통 Data Team의 구성원들은 아래와 같은 직군으로 분류된다.
🔼 = Data Scientist
⏹️ = Data Analyst
⏺️ = Data Engineer
<img src="https://velog.velcdn.com/images/idle-danie/post/7f8534e1-6ad2-4117-93a7-174a2e9aea6b/image.jpg" alt=""></p>
</blockquote>
<p><strong><code>데이터 분석가</code></strong></p>
<ul>
<li>데이터 웨어하우스의 데이터를 기반으로 지표를 만들고 대시보드를 통한 시각화</li>
<li>내부 직원들의 데이터 관련 질문 응답</li>
</ul>
<p><strong><code>데이터 사이언티스트</code></strong></p>
<ul>
<li>인공지능 모델을 개발하여 서비스 개선 (개인화, 자동화, 최적화)</li>
</ul>
<p><strong><code>데이터 엔지니어</code></strong>는 아래에서 따로 설명하려고 한다 :)</p>
<h2 id="data-engineers-role">Data Engineer&#39;s Role</h2>
<blockquote>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/d2ee80c0-65a4-4171-b7c3-4923099f4120/image.jpg" alt=""></p>
</blockquote>
<p><strong><code>소프트웨어 엔지니어링</code></strong></p>
<ul>
<li>기본적으로 벡엔드 프레임워크를 사용하여 API를 개발할 수 있어야 한다고 생각한다. 여러가지 상황이 있겠지만 데이터를 처리하는 전반적인 과정일 수 있고, 머신러닝 엔지니어의 역할을 한다면 프로덕션 단계에서 ML/DL 모델이 API로써 역할을 할 수 있게 만들어 줘야 할 것이다. 또한 데이터 분석가에게도 필요한 API나 데이터를 공급해야 한다.</li>
</ul>
<p><strong><code>데이터 웨어하우스 (DW) 구축</code></strong></p>
<ul>
<li>주로 클라우드 서비스로 관리하는 추세이다 (ex: <strong><code>BigQuery</code></strong>, <strong><code>Redshift</code></strong>, <strong><code>Snowflake</code></strong>)</li>
</ul>
<p><strong><code>데이터 파이프라인 구축 (ETL)</code></strong></p>
<ul>
<li><p><strong>ETL</strong>: 기존의 데이터베이스에서 원하는 정보를 데이터 웨어하우스에 적재하는 것 ⇒ 추출(Extract), 변환(Transform), 로드(Load)의 과정을 의미한다. 이러한 과정에 필요한 스케줄링은 보통 <strong>Airflow</strong>로 많이 관리한다.</p>
</li>
<li><p><strong>ELT</strong>: 데이터 웨어하우스에 적재된 데이터를 새로운 정보 혹은 요약된 정보로 제공하는 것이다. 주로, <a href="https://www.getdbt.com/product/what-is-dbt"><strong>DBT</strong></a>가 해당 역할을 포함하여 늘 데이터 엔지니어를 피곤한게 하는 data transform 과정을 효율적으로 처리하게 해주고 있어, 많은 회사들이 해당 기술을 채용하고 있다. </p>
</li>
<li><p>어느정도 성숙한 Data team에서는 Amazon S3와 같은 서비스를 이용하여 데이터 레이크를 구축하는데, 위 사진의 초록 화살표의 과정에서는 <strong>Spark</strong>와 같은 빅데이터 처리 시스템이 많이 쓰인다.</p>
</li>
</ul>
<h2 id="why-olap">Why OLAP?</h2>
<blockquote>
<p>아예 데이터 엔지니어링 분야를 모르거나, 개발을 처음 접하는 분들은 문득 이런 생각이 들 수 있다.
<strong>_&#39;그냥 Backend Engineer가 서비스 DB에 적재된 데이터를 기반으로 위의 역할들을 잘 소화하면 되는 것 아닌가?&#39;
_</strong></p>
</blockquote>
<p>아마, 이러한 물음은 <em><strong>&#39;서비스를 하고 있는 OLTP 시스템 내에서 그냥 분석하면 되는 것 아니야?&#39;</strong></em> 라는 물음으로 치환되어 발생하는 것으로 생각된다. 
하지만, <strong>OLTP (Online Transaction Processing)</strong> 에서 직접 분석을 실행하기에는 한계점이 매우 많다. </p>
<p>물론, 일회성 분석이나 소규모 기업에서는 OLTP에서 직접 분석을 실행하는 경우도 있지만, 단기적으로는 효과적일 수 있어도 궁극적으로 확장성이 매우 떨어진다. 어떤 시점에서 OLTP의 구조적 제한이나 경쟁 트랜잭션 워크로드와의 리소스 경합 때문에 성능 문제가 발생할 수 있다.</p>
<p>따라서, 트랜잭션을 처리하기 위해 설계 된 시스템이 아닌, 대규모 분석 쿼리를 실행하도록 구축되어 있는 <strong>OLAP (Online Analytical Processing)</strong> 시스템이 필요하다.</p>
<p>보통의 OLAP 시스템은 열 기반으로 설계되어 있어, 대량의 데이터를 스캔하도록 최적화 되어 있습니다. 우리가 위에서 언급한 DW 솔루션들은 모두 같은 특징을 가지고 있다. 참고로, OLAP 시스템에서는 인덱스나 PK와 같은 OLTP에서 우리가 빠르게 데이터를 조회하기 위해서 사용하는 것들은 사용되지 않는다. 모든 쿼리에는 일반적으로 100MB 이상인 최소 데이터 블록을 스캔하는데, 이러한 시스템에서 초당 수천 개의 개별 항목을 조회하려고 하면 그 사용 사례에 맞게 설계된 캐싱 계층과 결합되지 않는 한 시스템이 중단될 것이다.</p>
<p>참고로, 초기 데이터 웨어하우스는 보통 트랜잭션 어플리케이션에 사용되는 것과 같은 RDBMS를 기반으로 구축되었었는데, MPP (Massively Parallel Processing) 시스템의 인기가 높아지면서 대용량 데이터에 걸쳐 검색 성능을 크게 개선할 수 있는 병렬 프로세싱으로 전환되었다고 한다. </p>
<p>구체적으로, OLTP 시스템에서 가장 많이 쓰이는 MySQL과 MongoDB, 그리고 OLAP 시스템 내에서 가장 인기있는 데이터 웨어하우스 솔루션인 BigQuery에 대한 <a href="https://velog.io/@idle-danie/DB-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98%EB%B6%84%EC%84%9D">심층적인 분석을 진행한 포스팅</a>을 현재 게시했으니 관심이 있으시면 확인해주세요 :)</p>
<h2 id="data-discovery">Data Discovery</h2>
<p>사실, 데이터 엔지니어의 역할은 위에서 언급한 역할들에만 국한되지는 않습니다.</p>
<blockquote>
<p>회사와 Data Team이 성장해가며 아래와 같은 시나리오가 그려질 수 있다.</p>
</blockquote>
<h3 id="problem">Problem</h3>
<blockquote>
<ol>
<li>데이터가 커지면 테이블과 대시보드의 수가 증가 → 정보과잉의 문제</li>
<li>데이터 분석 시 어느 테이블이나 대시보드를 봐야하는지 혼란 → 효율성의 문제</li>
<li>데이터 변환 때문에 데이터 집합이 동일한 경로에서 어떻게 파생되었는지 알기 어려움 </li>
</ol>
</blockquote>
<p>위와 같은 문제들은, Data Catalog 혹은 Data Discovery 문제로 치환될 수 있기 때문에, 데이터의 계보를 유지하고 모니터링할 수 있는 것도 성장된 데이터 팀에서의 데이터 엔지니어의 역할 중 하나입니다. 보통 아래와 같은 솔루션을 통해 해당 문제를 해결합니다. </p>
<h3 id="solution">Solution</h3>
<blockquote>
</blockquote>
<ol>
<li>데이터 조회가 적은 사항에 대해 데이터 분석가와 협의하거나 자체적으로 사용 빈도를 추적하여, 주기적으로 테이블과 대시보드를 클린업 진행</li>
<li>Datahub나 Amundsen과 같이 Data Discovery에서 주로 발생하는 문제를 해결해주는 서비스를 이용</li>
</ol>
<h2 id="참고문헌">참고문헌</h2>
<blockquote>
</blockquote>
<ul>
<li>견고한 데이터 엔지니어링 (written by Joe Reis &amp; Matt Housley)</li>
<li><a href="https://dovetail.com/product-development/data-driven-vs-data-informed/">https://dovetail.com/product-development/data-driven-vs-data-informed/</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[IP에 대하여 (동적IP, IP관리 in AWS, DNS)]]></title>
            <link>https://velog.io/@idle-danie/IP%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC-%EB%8F%99%EC%A0%81IP-AWS-EC2%EC%97%90%EC%84%9C%EC%9D%98-IP-DNS</link>
            <guid>https://velog.io/@idle-danie/IP%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC-%EB%8F%99%EC%A0%81IP-AWS-EC2%EC%97%90%EC%84%9C%EC%9D%98-IP-DNS</guid>
            <pubDate>Mon, 15 May 2023 06:40:48 GMT</pubDate>
            <description><![CDATA[<h2 id="서론">서론</h2>
<blockquote>
<p>AWS EC2 관련 스터디를 하던 중 IP에 대한 개념이 올바르게 정립되지 않은 것 같아 2학년 때 수강했던 컴퓨터 네트워크 전공책, 그리고 내가 알고 있는 개념을 정리하여 글을 적게 되었다. 일반적인(?) 상황에서 떠오르는 의문점을 해결하는 형식으로 글을 구성했다보니 IP에 대한 이해가 전무해도 흐름을 따라가다 보면 개념을 이해할 수 있을 것이다. </p>
</blockquote>
<h2 id="ip">IP?</h2>
<blockquote>
<p>IP = Internet Protocol</p>
</blockquote>
<p>인터넷에서 컴퓨터와 다른 장치가 통신할 때 사용되는 규약(프로토콜)이다. 또한 TCP/IP 프로토콜 스택에서 중요한 역할을 수행한다. </p>
<p>이해가 어렵다면 아래의 글을 통해 일단 IP를 인식하자. </p>
<h4 id="네트워크-상에서-두-개의-컴퓨터끼리-통신을-한다고-가정한다면-일단-그-둘을-식별하는-id가-필요할-것이다-그것이-바로-ip"><em>네트워크 상에서 두 개의 컴퓨터끼리 통신을 한다고 가정한다면, 일단 그 둘을 식별하는 ID가 필요할 것이다. 그것이 바로 IP!</em></h4>
<p>IP는 패킷 스위칭 네트워크에서 데이터를 전달하는 데 사용됩니다. 이때, IP는 송신자와 수신자 간의 주소를 지정하고 데이터를 작은 패킷으로 나누어서 전송한다. 이렇게 작게 분할된 패킷들은 최적의 경로로 전달되며, 도착지에서는 이를 재조합하여 완전한 데이터로 복원된다.</p>
<p>IPv4와 IPv6는 IP의 버전을 나타내며, 현재 대부분의 인터넷에서는 IPv4가 사용되고 있다. IPv4는 32비트 주소 체계를 사용하며, 이는 대략 42억개의 주소를 지원한다. 그러나 인터넷이 확장됨에 따라 IPv4 주소의 부족 문제가 발생하게 되어, 더 많은 주소를 지원하는 IPv6로의 전환도 이루어지고 있다.</p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/c64c2c97-8ff5-4333-8da4-94d83b1ce6e8/image.png" alt=""></p>
<p>직관적으로 본인이 이용하고 있는 네트워크에 대한 컴퓨터가 할당받은 IP를 보고 싶다면 명령 프롬프트에서 ipconfig를 실행하면 된다. (기본적인 설정에서도 확인가능)</p>
<p>Windows </p>
<pre><code>ipconfig</code></pre><p>mac OS </p>
<pre><code>ip addr show</code></pre><p><img src="https://velog.velcdn.com/images/idle-danie/post/8a3f3e18-650d-430c-aad4-2ea99e29d590/image.jpg" alt=""></p>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/aeff0ef0-f8ff-471a-841a-24912771aad2/image.jpg" alt=""></p>
<h2 id="ip는-항상-하나로-고정">IP는 항상 하나로 고정?</h2>
<blockquote>
<p>IP는 고정IP와 동적IP의 두가지 형태로 존재한다. </p>
</blockquote>
<p>본인이 장치에 대한 권한이 있거나 서버의 관리자라면 선택할 수 있다. </p>
<p>고정IP는 말 그대로 관리자가 수동으로 컴퓨터나 장치에 IP를 부여하는 방식이다. </p>
<p>쉽게 말하면 우리가 어떠한 서비스에 회원가입할 때 하나의 고정 ID를 생성하는 것과 같다. 그렇기에 장치가 off되더라도 이전에 설정했던 IP는 유지된다. 이러한 특성으로 보통 서버나 네트워크 장비에 이용한다. </p>
<p>동적IP는 DHCP서버가 IP주소를 컴퓨터나 장치에 자동으로 부여하는 방식이다. </p>
<p>쉽게 말하면 우리가 가입한 어떠한 서비스의 ID가 1주일에 한번씩 바뀐다는 것이다. (물론 자동으로 로그인이 될 것이다^^)</p>
<p>이처럼 네트워크 상에 연결될 때마다 DHCP서버에서 IP주소를 자동으로 할당하고 보통 스마트폰이나 개인용 컴퓨터에서 이용한다. </p>
<p><em>‘어떤 방식이 좋다!’ 와 같은 이해 방식 보다 어떤 네트워크에 연결되느냐, 본인의 장치가 어떤 목적과 특성을 가지고 있느냐에 초점을 맞추고 그에 따른 방식을 선택하는 것이 맞는 이해인듯 하다.</em></p>
<h2 id="그렇다면-같은-네트워크를-써도-ip는-다르겠네">그렇다면 같은 네트워크를 써도 IP는 다르겠네?</h2>
<blockquote>
<p>그럴 확률이 높다. </p>
</blockquote>
<p>물론 같은 네트워크에 연결된 장치들, 쉽게 말하면 가정용 Wifi 네트워크를 공유하고 있는 스마트폰, 테블릿이 고정IP 형식으로 동일한 IP주소를 할당받을 수 있다. 하지만 IP주소 충돌 (IP conflict)이슈가 발생한다. </p>
<p>통신 과정에서 IP주소가 같다면 충돌이 발생하고 서로의 패킷을 인식하지 못하는 이슈가 일어나기 때문에 DHCP서버에서 주소를 할당하는 동적IP방식을 채택하거나, 서로 다른 고정IP를 부여하는 방식으로 문제를 해결해야 한다. </p>
<h2 id="정확히-ip를-부여하는-주체가-무엇이야">정확히 IP를 부여하는 주체가 무엇이야?</h2>
<blockquote>
<p>무엇(DHCP 서버)일 수도 있고 누구(네트워크 관리자)일 수도 있다.  </p>
</blockquote>
<p><strong>하지만 아마 이러한 질문이 나온다면 동적IP에 대한 물음일 것이라고 예상한다.</strong></p>
<p>일반적으로 네트워크 장치에서 (Wifi라우터나 스위치)에서 부여한다. 주체를 정확히 하자면 앞서 언급한 DHCP(Dynamic Host Configuration Protocol) 서버이다. </p>
<p>DHCP 서버는 일련의 IP 주소 대역을 관리하며, 네트워크에 연결된 장치들이 DHCP 서버로부터 IP 주소를 요청하면, DHCP 서버는 이에 대해 유효한 IP 주소를 할당한다. 일반적으로 DHCP 서버는 할당된 IP 주소의 유효 기간(TTL, Time To Live)을 지정하여 일정 시간이 지나면 해당 IP 주소를 해제하고 다른 장치들에게 할당할 수 있도록 한다. </p>
<p>당연히 IP주소를 재할당하는 주기는 관리자가 설정할 수 있다. </p>
<p>또한, DHCP 서버는 일정한 규칙에 따라 IP 주소를 할당한다. 예를 들어, DHCP 서버에서는 할당 가능한 IP 주소 범위를 미리 설정하고, 클라이언트가 IP 주소를 요청할 때마다 사용 가능한 IP 주소 중 하나를 선택하여 할당한다. 이러한 알고리즘은 네트워크 관리자가 DHCP 서버를 구성할 때 설정할 수 있으며, 일반적으로 다양한 알고리즘이 지원된다.</p>
<h2 id="동적ip-개념이-왜-나오게-된거야">동적IP 개념이 왜 나오게 된거야?</h2>
<blockquote>
<p>예상했겠지만 초기 인터넷 개발 환경에서는 동적IP개념이 없었지만 IP주소의 부족, 주소 관리의 효율성 등의 문제가 발생하여 나오게 된 개념이다.</p>
</blockquote>
<p>이 밖에도 동적 IP방식을 사용하면 IP주소를 필요할 때만 할당하여 자원 효율성을 지킬 수 있고, 보안성, 비용 절감 등의 문제가 해결될 수 있다.</p>
<h2 id="problem-서버에는-무조건-고정ip">Problem: 서버에는 무조건 고정IP?</h2>
<p>예전에는 서버를 구축한다고 하면 서버용 하드웨어를 직접 구매하여, 네트워크 카드를 설치하고 고정IP를 할당하곤 했다. </p>
<blockquote>
<p>하지만 현재는 AWS 혹은 GCP에서 제공하는 클라우드 서비스를 이용하여 이러한 불필요한 문제를 해결할 수 있다. </p>
</blockquote>
<p>어찌되었든 지금까지의 글을 이해했다면 당연히 서버는 고정IP 방식을 사용해야 한다. 서버가 꺼지던 관리자가 보수 작업을 하던,  클라이언트가 쉽게 다시 서버에 접속하기 위해서는 IP주소가 변하지 않아야 하기 때문이다. </p>
<p>AWS EC2 인스턴스에는 동적IP주소가 기본적으로 할당된다.</p>
<p>그렇다면 IP주소가 변경될 때마다 클라이언트들에게 변경된 IP주소를 알려줘야 하는 이슈가 발생하는데 이러한 문제를 어떻게 해결해야 할까?</p>
<h2 id="solution">Solution</h2>
<ol>
<li>AWS Elastic IP를 기능을 사용하면 EC2 인스턴스에 고정IP주소를 할당할 수 있다. 
기본적으로 region당 5개까지 할당이 가능하고 추가로 필요하면 AWS 콘솔의 Support Center에서 Service limit increase를 진행해야 한다. 
<img src="https://velog.velcdn.com/images/idle-danie/post/455bed3e-0348-47c7-a8e3-78600098ccca/image.png" alt=""></li>
</ol>
<ol start="2">
<li><p>DNS (Domain Name System), AWS Route 53: DNS를 이용하여 인스턴스의 IP주소와 도메인 이름을 매핑하는 방식이다. 전통적으로 계속 사용하는 방식 중 하나인데, 클라이언트는 도메인 이름 (예: 웹사이트 주소)를 통해 접속하고, DNS서버는 이에 매핑되는 IP주소를 반환해 주는 방식이다. 따라서 고정되지 않는 IP문제를 해결할 수 있고 AWS같은 경우 AWS Route53 서비스가 DNS기능을 제공한다.
<img src="https://velog.velcdn.com/images/idle-danie/post/99539c08-8f6e-4b30-acd7-c1c2fe2a7b92/image.gif" alt=""></p>
</li>
<li><p>AWS ELB(Elastic Load Balancer): 다수의 인스턴스에 접속하는 클라이언트의 트래픽을 분산시킨다. 인스턴스의 IP주소가 변경되더라도 클라이언트는 ELB의 END-POINT에 접속하게 하며 문제를 해결한다. 
<img src="https://velog.velcdn.com/images/idle-danie/post/d731cbb8-30f5-4173-ba5e-bb161bd61a3a/image.png" alt=""></p>
</li>
</ol>
<p>서버의 관리자는 각각의 상황(비용, 인프라)에 맞게 방식을 채택해야 할 것이다. </p>
<h2 id="dns-analysis-in-wireshark">DNS analysis in Wireshark</h2>
<blockquote>
<p>전공 수업 중 본인이 진행한 프로젝트 (Wireshark를 이용한 네트워크 분석) 자료이다. 당시 네이버 쇼핑 도메인을 이용하였다.
DNS의 표준 포트 번호는 53번이다.  </p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/7eb78d32-872a-45c7-98fe-3886d26603a6/image.png" alt=""></p>
<p>본인의 IPv4 주소 → 168.126.63.1(server) ## DNS request 진행</p>
<p>168.126.63.1(server)→ 본인의 IPv4 주소 ## DNS response 진행</p>
<h2 id="고찰">고찰</h2>
<p>_당연히 이러한 동적IP가 야기하는 문제점과 DNS와 같은 해결방식이 클라우드 서비스 때문에 발생한것이 아닌 것을 알고 있을 것이다. _</p>
<p>어떻게 보면 클라우드 서비스가 새로운 인터넷의 개념을 재정의 하거나 창조한 것이 아닐 수는 있다. 쉽게 말해서 Amazon이나 Google이 우리가 개별적으로 구축해야 할 서버용 하드웨어를 대신 대용량으로 구매하여 엄청난 크기의 데이터 센터를 구축한 뒤 우리에게 인프라를 제공하는 것이기 때문이다. </p>
<p>하지만 우리는 클라우드 서비스 덕분에 서버 구축에 필요한 초기 비용을 절감하고, 유연하게 자원을 할당할 수 있고, 계속해서 개발되는 매우 많은 서비스를 통해 부가 가치를 창출할 수 있다는 점에서 클라우드 서버 구축 환경은 엄청난 혁신이라고 생각한다. </p>
<blockquote>
<h2 id="참고문헌">참고문헌</h2>
</blockquote>
<ol>
<li>컴퓨터 네트워킹: 하향식 접근 (By James Kurose, Keith Ross) </li>
<li><a href="https://aws.amazon.com/ko/what-is/computer-networking/">https://aws.amazon.com/ko/what-is/computer-networking/</a></li>
<li><a href="https://ko.wikipedia.org/wiki/%EC%9D%B8%ED%84%B0%EB%84%B7_%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C">https://ko.wikipedia.org/wiki/인터넷_프로토콜</a></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Django framework란?  (feat. MTV pattern, 실제 tutorial)]]></title>
            <link>https://velog.io/@idle-danie/django-framework-API-%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@idle-danie/django-framework-API-%EC%84%9C%EB%B2%84-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0</guid>
            <pubDate>Fri, 12 May 2023 14:48:34 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>본 글은 Django framework 공식문서에서 소개하는 설치 과정과 제시하는 MTV패턴을 중심으로 tutorial의 핵심 요소를 정리한 글입니다.</p>
</blockquote>
<h2 id="what-is-django">What is Django?</h2>
<p><strong><code>Django</code></strong>는 파이썬으로 작성된 고수준 웹 프레임워크로, 신속한 개발과 깔끔한 디자인을 가능하게 합니다. <strong><code>Django</code></strong>는 <strong>&quot;batteries-included&quot;</strong> 철학을 따르며, 많은 기능을 내장하여 웹 개발에 필요한 거의 모든 것을 제공합니다.</p>
<p>Django는 <strong><code>MTV 패턴</code></strong>을 따릅니다. <strong><code>MTV</code></strong>는 Model-Template-View의 약자로, 흔히 우리가 아는 <strong><code>MVC(Model-View-Controller) 패턴</code></strong>의 변형입니다. Django에서는 View가 실제로 비즈니스 로직을 처리하는 것이 아니라, 템플릿 시스템을 사용하여 데이터를 표시하는 역할을 합니다.
<img src="https://velog.velcdn.com/images/idle-danie/post/225f7ba8-5c06-46d0-bc05-bd50373e8ce7/image.png" alt=""></p>
<blockquote>
</blockquote>
<ul>
<li><strong><code>Model</code></strong>: 데이터베이스 구조를 정의합니다. 데이터의 필드와 동작을 정의하며, 데이터베이스와의 상호 작용을 관리합니다.</li>
<li><strong><code>Template</code></strong>: 사용자에게 표시될 화면을 정의합니다. HTML과 Django 템플릿 언어를 사용하여 데이터를 렌더링합니다.</li>
<li><strong><code>View</code></strong>: 요청을 처리하고 적절한 템플릿을 선택하여 응답을 반환합니다. 비즈니스 로직을 구현하고, 모델과 템플릿을 연결합니다.</li>
</ul>
<p>이제 공식문서에서 제시하는 가이드라인에 따라 Django를 설치해보고, 간단하게 구성 요소를 살펴봅시다.</p>
<h2 id="초기-세팅">초기 세팅</h2>
<h3 id="가상환경">가상환경</h3>
<blockquote>
<p>기존 환경에서 실행해도 상관은 없지만 여러 프로젝트의 버젼 충돌로 인한 에러를 방지하기 위하여 virtual environment를 구축하는것이 바람직하다</p>
</blockquote>
<h3 id="가상환경-생성">가상환경 생성</h3>
<pre><code>py -m venv project-name</code></pre><h3 id="가상환경-활성화">가상환경 활성화</h3>
<pre><code>project-name\Scripts\activate.bat</code></pre><h3 id="가상환경-비활성화">가상환경 비활성화</h3>
<pre><code>deactivate</code></pre><h3 id="django-설치">django 설치</h3>
<pre><code>py -m pip install Django</code></pre><h3 id="장고-버젼-확인">장고 버젼 확인</h3>
<pre><code>django-admin –version</code></pre><h3 id="장고-프로젝트-생성">장고 프로젝트 생성</h3>
<pre><code>$ django-admin startproject mysite</code></pre><h3 id="장고-서버-실행">장고 서버 실행</h3>
<pre><code>$ python manage.py runserver</code></pre><h3 id="앱-생성">앱 생성</h3>
<pre><code>$ python manage.py startapp polls ## polls -&gt; App 이름</code></pre><blockquote>
<p>생성된 앱을 서버에서 구축하기 위해서는 urls path와 settings를 관리해야 합니다.</p>
</blockquote>
<p>참고로, 아래에서 언급되는 <code>urls.py</code>파일은 쉽게 설명하면 API request path를 관리하는 파일이라고 보면 됩니다. (ex: Node.js 프로젝트에서 관리하는 index.ts 파일)</p>
<h3 id="프로젝트-디렉토리의-urlspy">프로젝트 디렉토리의 urls.py</h3>
<pre><code class="language-python">from django.contrib import admin
from django.urls import path, include
urlpatterns = [
    path(&quot;admin/&quot;, admin.site.urls),
    path(&quot;polls/&quot;, include(&#39;polls.urls&#39;))
]</code></pre>
<h3 id="특정-앱-디렉토리-ex-polls의-urlspy">특정 앱 디렉토리 (ex: polls)의 urls.py</h3>
<pre><code class="language-python">from django.urls import path
from . import views
urlpatterns = [
    path(&#39;&#39;,views.index, name=&#39;index&#39;)
    path(&#39;some_url&#39;,views.some_url)
]</code></pre>
<h3 id="프로젝트-디렉토리의-settingspy">프로젝트 디렉토리의 settings.py</h3>
<pre><code class="language-python">INSTALLED_APPS = [
    &#39;django.contrib.admin&#39;,
    &#39;django.contrib.auth&#39;,
    &#39;django.contrib.contenttypes&#39;,
    &#39;django.contrib.sessions&#39;,
    &#39;django.contrib.messages&#39;,
    &#39;django.contrib.staticfiles&#39;,
    &#39;polls.apps.PollsConfig&#39;,
]</code></pre>
<h2 id="models">Models</h2>
<blockquote>
<p>장고의 model은 RDB의 개념이라고 생각하면 쉽다.
따라서 SQL과 같이 schema를 생성하고 attribute을 추가하고 이에 대한 filtering이 가능하다.</p>
</blockquote>
<h3 id="앱-디렉토리의-modelspy">앱 디렉토리의 models.py</h3>
<pre><code class="language-python">from django.db import models

class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField(&#39;date published&#39;)

class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)
</code></pre>
<h2 id="templates">Templates</h2>
<blockquote>
<p>화면단 구현에 필요한 각각의 html 파일은 template 디렉토리에서 보통 관리한다. </p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/bb268742-6103-4750-ab29-c81fc43043b0/image.png" alt=""></p>
<p><strong><code>index.html</code></strong> 예시</p>
<pre><code class="language-html">{% if questions %}
&lt;ul&gt;
    {% for question in questions %}
        &lt;li&gt;{{question}}&lt;/li&gt;
    {% endfor %}
&lt;/ul&gt;
{% else %}
&lt;p&gt;no questions&lt;/p&gt;
{% endif %}</code></pre>
<h2 id="views">Views</h2>
<h3 id="viewspy">views.py</h3>
<blockquote>
<p>각각의 html파일을 함수 단위로 manage한다.</p>
</blockquote>
<pre><code class="language-python">from .models import *
from django.shortcuts import render

def index(request):
    latest_question_list = Question.objects.order_by(&#39;-pub_date&#39;)[:5]
    context = {&#39;questions&#39;: latest_question_list}
    return render(request, &#39;polls/index.html&#39;, context)

def detail(request, question_id):
    question = Question.objects.get(pk=question_id)
    return render(request, &#39;polls/detail.html&#39;, {&#39;question&#39;: question})</code></pre>
<p>참고로, render는 간단하게 말하면 html파일을 화면에 보이게 만들어 주는 것이라고 이해하면 된다.</p>
<h3 id="urlspy">urls.py</h3>
<blockquote>
<p>추가한 view의 결과를 보기 위해서는 path를 추가해야한다</p>
</blockquote>
<pre><code class="language-python">from django.urls import path 
from . import views  

app_name = &#39;polls&#39;

urlpatterns = [     
    path(&#39;&#39;, views.index, name=&#39;index&#39;),
    path(&#39;some_url&#39;, views.some_url), 
    path(&#39;&lt;int:question_id&gt;/&#39;, views.detail, name=&#39;detail&#39;),     
]</code></pre>
<h2 id="error-처리">Error 처리</h2>
<blockquote>
<p>404에러: http에서 요청한 페이지를 찾을 수 없을 때 생기는 에러</p>
</blockquote>
<h3 id="404-에러-방지">404 에러 방지</h3>
<pre><code class="language-python">def detail(request, question_id):

    try:
        question = Question.objects.get(pk=question_id)
    except Question.DoesNotExist:
        raise Http404(&quot;Question does not exist&quot;)

    question = get_object_or_404(Question, pk=question_id)
    return render(request, &#39;polls/detail.html&#39;, {&#39;question&#39;: question})</code></pre>
<h3 id="서버에서-요청을-동시에-처리할-때-발생할-수-있는-에러-방지">서버에서 요청을 동시에 처리할 때 발생할 수 있는 에러 방지</h3>
<blockquote>
<p>Solve: 두개의 서버가 아닌 하나의 DB에서 처리</p>
</blockquote>
<pre><code class="language-python">def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = question.choice_set.get(pk=request.POST[&#39;choice&#39;])
    except (KeyError, Choice.DoesNotExist):
        return render(request, &#39;polls/detail.html&#39;, {&#39;question&#39;: question, &#39;error_message&#39;: f&quot;선택이 없습니다. id={request.POST[&#39;choice&#39;]}&quot;})
    else:
        selected_choice.votes = F(&#39;votes&#39;) + 1
        selected_choice.save()
        return HttpResponseRedirect(reverse(&#39;polls:index&#39;))</code></pre>
<h2 id="admin">Admin</h2>
<blockquote>
<ul>
<li>django에는 관리자 페이지 및 관리자 기능을 할 수 있는 admin.py를 제공한다</li>
</ul>
</blockquote>
<ul>
<li>관리자 페이지에서 보여질 모델의 필드, 필터링 기준, 검색 기준 등을 설정할 수 있으며, 모델에 대한 CRUD(CREATE, READ, UPDATE, DELETE) 작업을 수행하는 메소드도 정의할 수 있다.</li>
<li>쉽게 말하면 Admin을 활용하여 DB의 내용 관리(CRUD)를 손쉽게 할 수 있다는 것이다.</li>
</ul>
<h3 id="crud를-위한-admin-page-커스터마이즈">CRUD를 위한 Admin page 커스터마이즈</h3>
<pre><code class="language-python">from django.contrib import admin
from .models import Choice, Question

admin.site.register(Choice)

class ChoiceInline(admin.TabularInline):
    model = Choice
    extra = 3


class QuestionAdmin(admin.ModelAdmin):
    fieldsets = [
        (&#39;질문 섹션&#39;, {&#39;fields&#39;: [&#39;question_text&#39;]}),
        (&#39;생성일&#39;, {&#39;fields&#39;: [&#39;pub_date&#39;], &#39;classes&#39;: [&#39;collapse&#39;]}),        
    ]
    readonly_fields = [&#39;pub_date&#39;]
    inlines = [ChoiceInline]
    list_filter = [&#39;pub_date&#39;]
    search_fields = [&#39;question_text&#39;, &#39;choice__choice_text&#39;]

admin.site.register(Question, QuestionAdmin)</code></pre>
<p>지금까지 공식문서를 따라, <strong><code>Django</code></strong> 아키텍처 내에서 <strong><code>MTV패턴</code></strong>이 대략적으로 어떤 구조로 구현되고, 에러에 대한 관리와 admin 기능을 어떻게 코드로 관리하는지에 대해 알아보았다.</p>
<p>이후, tutorial에서 제시하는 가이드라인을 전체 구현한 코드는 아래 github repository에 업로드 해놓았습니다 :)
<a href="https://github.com/idle-danie/django_study/tree/main">GitHub - idle-danie/django_study: django_study_tutorial</a></p>
<h2 id="참고문헌">참고문헌</h2>
<blockquote>
<p><a href="https://www.djangoproject.com/start/">https://www.djangoproject.com/start/</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[디지털 마케팅과 데이터 ]]></title>
            <link>https://velog.io/@idle-danie/%EB%94%94%EC%A7%80%ED%84%B8-%EB%A7%88%EC%BC%80%ED%8C%85%EA%B3%BC-%EB%8D%B0%EC%9D%B4%ED%84%B0</link>
            <guid>https://velog.io/@idle-danie/%EB%94%94%EC%A7%80%ED%84%B8-%EB%A7%88%EC%BC%80%ED%8C%85%EA%B3%BC-%EB%8D%B0%EC%9D%B4%ED%84%B0</guid>
            <pubDate>Fri, 12 May 2023 09:17:21 GMT</pubDate>
            <description><![CDATA[<h1 id="데이터-기반-마케팅">데이터 기반 마케팅</h1>
<blockquote>
<p><em>디지털 마케팅은 데이터 기반 마케팅을 뜻한다</em> </p>
</blockquote>
<h2 id="접점--채널--광고미디어">접점 = 채널 = 광고미디어</h2>
<blockquote>
<p>접점 데이터 수집 및 저장하는 것은 매우 중요한 요소이다.</p>
</blockquote>
<ul>
<li>고객은 다양한 경로를 통해 방문, 구매를 진행</li>
<li>가격이 상대적으로 비싼 물건이나 서비스일수록 시간을 두고 여러경로를 통해 같은 사이트를 여러 번 방문하면서 리서치</li>
<li>방문에 기여한 채널들을 기록하는 것이 중요</li>
</ul>
<h2 id="최종-전환-기록">최종 전환 기록</h2>
<p>이는 구매나 회원가입과 같은 마케팅의 목표를 위해 필요한 기록이다.</p>
<h2 id="보조-전환-기록">보조 전환 기록</h2>
<ul>
<li>보조전환은 최종전환의 징조</li>
<li>사용자의 방문 정보들이 계속 추출되어야 함</li>
</ul>
<blockquote>
<p><strong>_위와 같은 수집활동 데이터가 저장되는 데이터베이스가 바로 디지털 마케팅 데이터 인프라의 핵심!
_</strong></p>
</blockquote>
<h2 id="분석-방법">분석 방법</h2>
<blockquote>
<ol>
<li>라스트 터치</li>
<li>퍼스트 터치</li>
<li>멀티 터치 </li>
</ol>
</blockquote>
<h3 id="last-touch-model">Last touch model</h3>
<ul>
<li>보통 싼 물건 </li>
<li>최종 구매전의 마지막 채널에게 모든 성과</li>
</ul>
<h3 id="last-non-direct-touch-model">Last non-direct touch model</h3>
<ul>
<li>마지막 채널이지만, 직접방문이 아니느 그 전 채널에게 성과를 부여</li>
</ul>
<h3 id="first">First</h3>
<ul>
<li>처음 방문 채널</li>
</ul>
<h3 id="linear-멀티-터치-모델">Linear (멀티 터치 모델)</h3>
<ul>
<li>모든 채널에게 동일하게 나눠주는 모델</li>
</ul>
<h3 id="time-decay-멀티-터치-모델">Time decay (멀티 터치 모델)</h3>
<ul>
<li>가장 최근모델들에게 가중치를 더 주는 모델</li>
</ul>
<h2 id="고객의-평생-가치-life-time-balue">고객의 평생 가치 (Life time balue)</h2>
<ul>
<li>사용자의 초기행동을 보고 미래에 가져다줄 수 있는 가치 예측</li>
<li>처음에는 간단하게 휴리스틱 기반 예측 가능</li>
</ul>
<h2 id="고객-이탈률-customer-churn-예측--리텐션과-반대">고객 이탈률 (customer churn) 예측  (리텐션과 반대)</h2>
<ul>
<li>리타게팅 광고: 쿠키를 사용하여 유저를 따라다니며 광고 제시</li>
<li>쿠키: 점점 사용할 수 없는 방향으로 법안 적용 -&gt; 정교한 머신러닝 모델로 대체</li>
</ul>
<h1 id="마케팅-핵심-지표">마케팅 핵심 지표</h1>
<blockquote>
<p>개인적으로 마케팅을 진행할 때, 데이터 엔지니어라도 필수적으로 이해하고 있어야 한다고 생각하는 지표 14가지의 정의와 계산 방법을 정리해보았다.</p>
</blockquote>
<ol>
<li><strong>Cost</strong>:<ul>
<li><strong>설명</strong>: 광고 캠페인에 소요된 총 비용.<ul>
<li><strong>계산 방법</strong>: 플랫폼에서 제공하는 광고 비용 데이터의 합계.</li>
</ul>
</li>
</ul>
</li>
<li><strong>Impressions</strong>:<ul>
<li><strong>설명</strong>: 광고가 사용자에게 노출된 총 횟수.</li>
<li><strong>계산 방법</strong>: 플랫폼에서 제공하는 노출 수 데이터의 합계.</li>
</ul>
</li>
<li><strong>Click</strong>:<ul>
<li><strong>설명</strong>: 광고가 클릭된 총 횟수.</li>
<li><strong>계산 방법</strong>: 플랫폼에서 제공하는 클릭 수 데이터의 합계.</li>
</ul>
</li>
<li><strong>Install</strong>:<ul>
<li><strong>설명</strong>: 광고를 통해 앱이 설치된 총 횟수.</li>
<li><strong>계산 방법</strong>: 플랫폼에서 제공하는 설치 수 데이터의 합계.</li>
</ul>
</li>
<li><strong>Sign-up</strong>:<ul>
<li><strong>설명</strong>: 광고를 통해 사용자가 가입한 총 횟수.</li>
<li><strong>계산 방법</strong>: 플랫폼에서 제공하는 가입 수 데이터의 합계.</li>
</ul>
</li>
<li><strong>Purchase</strong>:<ul>
<li><strong>설명</strong>: 광고를 통해 구매가 이루어진 총 횟수.</li>
<li><strong>계산 방법</strong>: 플랫폼에서 제공하는 구매 수 데이터의 합계.</li>
</ul>
</li>
<li><strong>Revenue</strong>:<ul>
<li><strong>설명</strong>: 광고를 통해 발생한 총 수익.</li>
<li><strong>계산 방법</strong>: 플랫폼에서 제공하는 수익 데이터의 합계.</li>
</ul>
</li>
<li><strong>CTR (Click-Through Rate)</strong>:<ul>
<li><strong>설명</strong>: 광고 노출 수 대비 클릭 수 비율.</li>
<li><strong>계산 방법</strong>: <code>(클릭 수 / 노출 수) * 100</code></li>
</ul>
</li>
<li><strong>CPC (Cost Per Click)</strong>:<ul>
<li><strong>설명</strong>: 클릭당 비용.</li>
<li><strong>계산 방법</strong>: <code>광고 비용 / 클릭 수</code></li>
</ul>
</li>
<li><strong>CPI (Cost Per Install)</strong>:<ul>
<li><strong>설명</strong>: 설치당 비용.</li>
<li><strong>계산 방법</strong>: <code>광고 비용 / 설치 수</code></li>
</ul>
</li>
<li><strong>CPA (Cost Per Action) - Sign-up</strong>:<ul>
<li><strong>설명</strong>: 가입당 비용.</li>
<li><strong>계산 방법</strong>: <code>광고 비용 / 가입 수</code></li>
</ul>
</li>
<li><strong>CAC (Customer Acquisition Cost) - Sign-up</strong>:<ul>
<li><strong>설명</strong>: 고객 한 명을 획득하는 데 드는 비용.</li>
<li><strong>계산 방법</strong>: <code>광고 비용 / 고객 획득 수</code> (여기서는 가입 수)</li>
</ul>
</li>
<li><strong>CPA (Cost Per Action) - Purchase</strong>:<ul>
<li><strong>설명</strong>: 구매당 비용.</li>
<li><strong>계산 방법</strong>: <code>광고 비용 / 구매 수</code></li>
</ul>
</li>
<li><strong>ROAS (Return On Ad Spend)</strong>:<ul>
<li><strong>설명</strong>: 광고 비용 대비 수익 비율.</li>
<li><strong>계산 방법</strong>: <code>(광고 수익 / 광고 비용) * 100</code></li>
</ul>
</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[SQL basic concept]]></title>
            <link>https://velog.io/@idle-danie/SQL-Basic-Concept</link>
            <guid>https://velog.io/@idle-danie/SQL-Basic-Concept</guid>
            <pubDate>Tue, 09 May 2023 10:32:33 GMT</pubDate>
            <description><![CDATA[<h1 id="sql-basic-concept">SQL basic concept</h1>
<blockquote>
<p>SQLD를 공부하면서, SQL의 basic concept와 주의하거나 알면 좋은 지점들에 대해서 정리해보았습니다.</p>
</blockquote>
<h2 id="what-is-sql">What is SQL?</h2>
<p>SQL(Structured Query Language)은 관계형 데이터베이스 관리 시스템(RDBMS)에서 데이터를 관리하고 조작하기 위해 사용되는 표준 언어입니다. SQL을 사용하면 데이터베이스에 저장된 데이터를 삽입, 수정, 삭제 및 조회할 수 있습니다. SQL은 강력한 데이터 조작 기능과 함께 데이터베이스 스키마 생성 및 변경을 위한 명령도 제공합니다. SQL의 주요 구성 요소로는 <code>DDL(데이터 정의 언어)</code>, <code>DML(데이터 조작 언어)</code>, <code>DQL(데이터 질의 언어)</code> 등이 있습니다.</p>
<h2 id="sql-입문자를-위한-tip">SQL 입문자를 위한 Tip</h2>
<ul>
<li>다수의 SQL문은 세미콜론으로 분리합니다.</li>
<li>SQL 주석<ul>
<li><code>--</code>: 인라인 한 줄짜리 주석</li>
<li><code>/* ... */</code>: 여러 줄에 걸쳐 사용 가능한 주석</li>
</ul>
</li>
<li>SQL 키워드는 나름대로의 포맷팅이 필요합니다(팀끼리 상의하여 컨벤션을 정하는 것이 중요)<ul>
<li>테이블 및 필드 이름 명명 규칙 정하기<ul>
<li>단수형 vs 복수형 예) <code>User</code> vs <code>Users</code></li>
<li><code>_</code> vs CamelCasing 예) <code>user_time</code> vs <code>UserTime</code></li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="sql-vs-pandas">SQL vs Pandas</h2>
<blockquote>
<p>Pandas가 등장할 당시에는, &#39;전반적인 데이터 처리 트렌드가 SQL에서 Pandas로 넘어오겠다&#39;라고 생각하는 개발자들도 존재했다고 한다. 하지만, SQL과 달리 코드 관계자가 아닌 인원이 코드를 보고 한눈에 어떤 의미인지를 모른다는 치명적인 단점이 존재하여, 여전히 SQL은 데이터 조작 언어로서 입지를 공공히 하고 있다. </p>
</blockquote>
<p>물론 설계 목적 자체가 다르기 때문에 비교하는 것이 무의미할 수 있지만, 데이터를 다룬다는 큰 범주안에서 유사한 역할을 담당한다고 볼 수 있기에 같이 설명을 드리려고 합니다.</p>
<ul>
<li><strong>SQL</strong>: 관계형 데이터베이스에 저장된 데이터를 효율적으로 관리하고 질의할 수 있도록 설계된 언어입니다.</li>
<li><strong>Pandas</strong>: Python 환경에서 데이터 분석 및 조작을 위해 설계된 라이브러리로, 데이터프레임을 사용하여 다양한 데이터 조작을 간편하게 수행할 수 있습니다.</li>
</ul>
<p>보통 <strong>SQL</strong>은 디스크 기반의 대규모 데이터베이스에서 효율적으로 작동하고, 데이터베이스 서버에서 실행됩니다. 반면에 <strong>Pandas</strong>는 데이터프레임 형태로 메모리에 데이터를 로드하여 빠르고 유연한 데이터 조작을 가능하게 합니다. 사실, Pandas의 메모리 할당 방식은 대규모 데이터를 다루기 어렵기 때문에, 이 때 유사한 용도로 <strong>PySpark</strong>가 많이 쓰입니다. 대규모 데이터에 대한 명확한 기준은 없지만, 경험 상 10GB가 넘어가는 데이터를 처리할 때는 PySpark를 사용하는 것이 유리한 것 같습니다.</p>
<h2 id="ddl">DDL</h2>
<h3 id="create-table">CREATE TABLE</h3>
<ul>
<li>primary key 속성을 지정할 수 있으나 무시됩니다.</li>
<li>CTAS:<ul>
<li><code>CREATE TABLE ... AS SELECT ...</code>을 통해 서머리 테이블을 생성할 수 있습니다.</li>
<li>ELT의 역할을 할 수 있지만, 테스트할 수 없고 테이블 컬럼에 대한 디테일한 관리가 어렵습니다.</li>
<li>초기 설정한 데이터 타입이 최종 타입입니다.</li>
<li><code>CREATE TABLE</code> → <code>INSERT</code> → <code>SELECT</code></li>
<li>보통 DBT(Data Build Tool)를 사용합니다.</li>
</ul>
</li>
</ul>
<h3 id="drop-table">DROP TABLE</h3>
<ul>
<li><code>DROP TABLE ...;</code>:<ul>
<li>없는 테이블을 지우려고 하면 에러가 발생합니다.</li>
</ul>
</li>
<li><code>DROP TABLE IF EXISTS ...;</code> 사용을 권장합니다.</li>
<li><strong>VS DELETE FROM</strong>:<ul>
<li><code>DELETE FROM</code>은 조건에 맞는 레코드들을 삭제합니다(테이블 자체는 유지).</li>
</ul>
</li>
</ul>
<h3 id="alter-table">ALTER TABLE</h3>
<ul>
<li>새로운 컬럼 추가:<ul>
<li><code>ALTER TABLE 테이블이름 ADD COLUMN 필드이름 필드타입;</code></li>
</ul>
</li>
<li>기존 컬럼 이름 변경:<ul>
<li><code>ALTER TABLE 테이블이름 RENAME 현재필드이름 TO 새필드이름;</code></li>
</ul>
</li>
<li>기존 컬럼 제거:<ul>
<li><code>ALTER TABLE 테이블이름 DROP COLUMN 필드이름;</code></li>
</ul>
</li>
<li>테이블 이름 변경:<ul>
<li><code>ALTER TABLE 현재테이블이름 RENAME TO 새테이블이름;</code></li>
</ul>
</li>
</ul>
<h2 id="dml">DML</h2>
<blockquote>
<p>SELECT, FROM, WHERE 절은 너무 기초적인 문법이기에 설명에서 제외</p>
</blockquote>
<h3 id="in">IN</h3>
<ul>
<li><code>WHERE channel IN (&#39;Google&#39;, &#39;Youtube&#39;)</code>:<ul>
<li><code>WHERE channel = &#39;Google&#39; OR channel = &#39;Youtube&#39;</code></li>
</ul>
</li>
</ul>
<h3 id="like구별-and-ilike-대소문자-구별-안-함">LIKE(구별) and ILIKE (대소문자 구별 안 함)</h3>
<ul>
<li><code>WHERE channel LIKE &#39;G%&#39;</code> → <code>&#39;G*&#39;</code></li>
<li><code>WHERE channel LIKE &#39;%o%&#39;</code> → <code>&#39;*o*&#39;</code></li>
<li><code>NOT LIKE</code> 또는 <code>NOT ILIKE</code><ul>
<li><strong>mysql</strong>은 대소문자 구별을 하지 않음</li>
<li><strong>PostgreSQL/Redshift</strong>는 구별함</li>
</ul>
</li>
</ul>
<h3 id="between">BETWEEN</h3>
<ul>
<li>DATE RANGE MATCHING</li>
</ul>
<h3 id="string-functions">String functions</h3>
<ul>
<li><code>LEFT(str, N)</code></li>
<li><code>REPLACE(str, exp1, exp2)</code></li>
<li><code>UPPER(str)</code></li>
<li><code>LOWER(str)</code></li>
<li><code>LEN(str)</code></li>
<li><code>LPAD</code>, <code>RPAD</code></li>
<li><code>SUBSTRING</code></li>
</ul>
<h3 id="insert-into-vs-copy">INSERT INTO VS .COPY</h3>
<ul>
<li>일반적으로 INSERT가 더 느립니다. (<a href="https://www.youtube.com/watch?v=L9K0l65wMbQ">배치 삽입 메커니즘</a> 이해 필요)</li>
<li><code>INSERT INTO table SELECT * FROM ...</code>:<ul>
<li>필드의 타입을 제어하려면 <code>CREATE TABLE table AS SELECT</code>보다 낫습니다.</li>
<li>그러나 varchar 길이를 맞추는 것은 쉽지 않습니다.</li>
<li>Snowflake와 BigQuery는 string 타입을 지원합니다.</li>
</ul>
</li>
</ul>
<h2 id="group-by">GROUP BY</h2>
<ul>
<li>DAU, WAU, MAU 계산할 때 GROUP BY가 필요합니다.<pre><code class="language-sql">--- mau 계산 sql예시
SELECT TO_CHAR(A.TS, &#39;YYYY-MM&#39;) AS month,
       COUNT(DISTINCT B.userid) AS mau
FROM raw_data.session_timestamp A
JOIN raw_data.user_session_channel B ON A.sessionid = B.sessionid
GROUP BY 1
ORDER BY 1 DESC;
</code></pre>
</li>
</ul>
<h2 id="order-by">ORDER BY</h2>
<ul>
<li><strong>NULL value ordering</strong> (NULL이 가장 큰 값?)<ul>
<li>In Redshift, NULL은 최대값으로 간주됩니다.<ul>
<li><code>ORDER BY 1 DESC;</code> → NULL이 가장 앞에 위치</li>
<li><code>ORDER BY 1 DESC NULLS LAST;</code> → NULL이 맨 뒤로 이동</li>
</ul>
</li>
</ul>
</li>
<li><strong>ORDER BY와 GROUP BY</strong> → 포지션 번호 vs 필드 이름<ul>
<li><code>GROUP BY 1</code> == <code>GROUP BY month</code> == <code>GROUP BY TO_CHAR(A.ts, &#39;YYYY-MM&#39;)</code></li>
</ul>
</li>
</ul>
<h2 id="type-cast-and-conversion">Type Cast and Conversion</h2>
<ul>
<li><strong>Type casting</strong><ul>
<li><code>cast</code> 또는 <code>::</code> 연산자 사용<ul>
<li><code>channel::int</code> (본 예시는 PostgreSQL기반의 Redshift에 해당, SQL마다 다를 수 있음)</li>
<li><code>cast(channel as int)</code></li>
</ul>
</li>
</ul>
</li>
<li><strong>Conversion</strong><ul>
<li><strong>Date conversion</strong><ul>
<li><code>convert_timezone</code><ul>
<li><code>convert_timezone(&#39;America/Los_Angeles&#39;, ts)</code></li>
<li><code>SELECT pg_timezone_names()</code></li>
</ul>
</li>
<li><code>date</code>, <code>truncate</code></li>
<li><code>date_trunc</code><ul>
<li>첫 번째 인자가 어떤 값을 추출하는지 지정</li>
</ul>
</li>
<li><code>extract</code> 또는 <code>date_part</code>: 날짜, 시간에서 특정 부분의 값을 추출</li>
<li><code>datediff</code>, <code>dateadd</code>, <code>get_current...</code></li>
</ul>
</li>
<li><strong><code>TO_CHAR(A.TS, &#39;YYYY-MM&#39;) AS month</code></strong>은 아래와 같이 사용해도 같은 output을 도출<ul>
<li><code>LEFT(A.ts, 7)</code></li>
<li><code>DATE_TRUNC(&#39;month&#39;, A.ts)</code></li>
<li><code>SUBSTRING(A.TS, 1, 7)</code></li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="null">NULL</h2>
<blockquote>
<p>값이 존재하지 않음을 의미하고, 0과 비어있는 문자열과는 다르다는 점을 인지해야 한다.</p>
</blockquote>
<ul>
<li><code>IS NULL</code>, <code>IS NOT NULL</code> 형식으로 사용<ul>
<li>Boolean 타입의 필드도 <code>IS TRUE</code>, <code>IS FALSE</code> 형식으로 비교</li>
</ul>
</li>
<li>LEFT JOIN 시 매칭되는 것이 있는지 확인할 때 유용</li>
<li>NULL 값을 다른 값으로 변환하고 싶다면?<ul>
<li><code>COALESCE</code></li>
<li><code>NULLIF</code></li>
</ul>
</li>
<li>특정 값을 NULL이나 0으로 나누면?<ul>
<li>NULL로 나누면 결과는 NULL<pre><code class="language-sql">SELECT 10 / NULL; -- 결과: NULL</code></pre>
</li>
<li>0으로 나누면 오류(Division by zero error)가 발생<pre><code class="language-sql">SELECT 10 / 0; -- 결과: 오류 (Division by zero)
## Count
| … | … | value |
| --- | --- | --- |
| … | … | NULL |
| … | … | 1 |
| … | … | 1 |
| … | … | 0 |
| … | … | 0 |
| … | … | 4 |
| … | … | 3 |
</code></pre>
</li>
</ul>
</li>
</ul>
<pre><code class="language-sql">COUNT(0) FROM Table = 7 
COUNT(value) FROM Table = 6 
COUNT(DISTINCT value) FROM Table = 4</code></pre>
<h1 id="sql-실행순서">SQL 실행순서</h1>
<blockquote>
<p>세부적으로 들어가면 사실 최종적인 실행순서는 보통 쿼리 옵티마이져에 의해서 결정된다. 다만, 기본적으로 아래 사진과 같이 이해하고 있다면 SQL문을 작성할 때 크게 도움이 된다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/a91fb8bf-a1a1-45c1-ae25-602d8cc2e7f1/image.png" alt=""></p>
<h1 id="참고문헌">참고문헌</h1>
<blockquote>
<p><a href="https://jaehoney.tistory.com/191">https://jaehoney.tistory.com/191</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[SqlAlchemy error troubleshoot]]></title>
            <link>https://velog.io/@idle-danie/TIL-Day-4</link>
            <guid>https://velog.io/@idle-danie/TIL-Day-4</guid>
            <pubDate>Thu, 13 Apr 2023 08:12:15 GMT</pubDate>
            <description><![CDATA[<h2 id="trouble">Trouble</h2>
<blockquote>
<p>Colab을 통해 Redshift로의 connection 과정에서 문제가 발생했다.
1주일 전까지는 이러한 문제가 발생되지 않았기에 내가 관리하지 않은 웹상 개발환경인 colab에서의 최근에 일어난 update과정이 문제가 되었다고 추론했다. </p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/6f784e5f-ddc1-4993-97c0-485e16432b70/image.png" alt=""></p>
<h2 id="solve">Solve</h2>
<blockquote>
<p>python ORM인 SqlAlchemy가 새로운 버전으로 업데이트 되는 과정에서 충돌이 발생한 것으로 보인다. </p>
</blockquote>
<p><a href="https://www.sqlalchemy.org/download.html">https://www.sqlalchemy.org/download.html</a>
따라서 이전 버전인 1.4.47을 설치하였고 정상적으로 작동하는 것을 확인했다.
<img src="https://velog.velcdn.com/images/idle-danie/post/68894a3d-0ebd-4102-9012-86de43b90fe3/image.png" alt=""></p>
<p>파이썬 노트북 환경에서는 이러한 일이 비일비재하고 큰 변화없이 모듈이 실행이 안된다면 모듈의 업데이트 현황을 확인하는 것이 습관이 되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Linked list 직접 구현해보기 by Python]]></title>
            <link>https://velog.io/@idle-danie/Linkedlist</link>
            <guid>https://velog.io/@idle-danie/Linkedlist</guid>
            <pubDate>Tue, 11 Apr 2023 08:10:09 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>오래 전 자료구조 시간에 배운 Linked List에 대한 개념이 머릿속에서 모호해져서, 파이썬으로 직접 구현하며 개념을 상기해보려 한다.</p>
</blockquote>
<h1 id="linked-list">Linked list</h1>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/c76aebc8-d0fa-479a-8168-af83048265f9/image.png" alt=""></p>
<h2 id="what-is-linked-list">What is Linked list?</h2>
<p>Linked list(연결 리스트)는 데이터를 노드(Node)라는 단위로 저장하는 자료구조다. 각 노드는 데이터 필드와 다음 노드를 가리키는 포인터로 구성되어 있으며, 이러한 노드들이 일렬로 연결되어 있는 형태를 띄고 있다. Linked list의 주요 특징은 삽입과 삭제가 용이하다는 점이다. 배열과 달리 미리 할당된 메모리 공간이 필요 없으며, 필요한 만큼 메모리를 할당받을 수 있다.</p>
<p>Linked list는 주로 다음과 같은 연산을 지원한다:</p>
<ul>
<li>삽입(Insertion): 리스트의 앞, 중간, 끝에 새로운 노드를 삽입할 수 있음</li>
<li>삭제(Deletion): 특정 노드를 리스트에서 제거할 수 있음</li>
<li>탐색(Search): 리스트를 순회하며 특정 값을 가진 노드를 찾을 수 있음</li>
</ul>
<h2 id="code-implementation">Code implementation</h2>
<blockquote>
<p>linked list를 파이썬으로 직접 구현하면 아래와 같을 것입니다 ㅎ</p>
</blockquote>
<pre><code class="language-python">class Node:
    def __init__(self, val):
        self.val = val
        self.next = None


class SingleLinkedList:
    def __init__(self):
        self.head = None

    def insertAtHead(self, val): 
        node = ListNode(val)
        node.next = self.head
        self.head = node

    def insertBack(self, val): 
        node = ListNode(val)
        crnt_node = self.head
        while crnt_node.next:
            crnt_node = crnt_node.next
        crnt_node.next = node

    def findNode(self, val): 
        crnt_node = self.head
        while crnt_node is not None:
            if crnt_node.val == val:
                return crnt_node
            crnt_node = crnt_node.next
        raise RuntimeError(&#39;LinkedList: empty&#39;)

    def insertAfter(self, node, val): 
        new_node = ListNode(val)
        new_node.next = node.next
        node.next = new_node

    def popAfter(self, prev_node): 
        if prev_node.next is not None:
            prev_node.next = prev_node.next.next
</code></pre>
<p>간단하게 코드에 대한 설명을 하자면 아래와 같습니다. </p>
<blockquote>
</blockquote>
<ul>
<li>Node 클래스는 연결 리스트의 각 노드를 나타냅니다. 각 노드는 값(<code>val</code>)과 다음 노드를 가리키는 포인터(<code>next</code>)를 가집니다.</li>
<li><code>SingleLinkedList</code> 클래스는 단일 연결 리스트를 나타냅니다.</li>
<li><code>insertAtHead</code> 메서드는 리스트의 맨 앞에 새로운 노드를 삽입합니다.</li>
<li><code>insertBack</code> 메서드는 리스트의 맨 뒤에 새로운 노드를 삽입합니다.</li>
<li><code>findNode</code> 메서드는 리스트를 순회하며 특정 값을 가진 노드를 찾습니다.</li>
<li><code>insertAfter</code> 메서드는 특정 노드 뒤에 새로운 노드를 삽입합니다.</li>
<li><code>popAfter</code> 메서드는 특정 노드 뒤의 노드를 삭제합니다.</li>
</ul>
<h1 id="double-linked-list">Double Linked list</h1>
<p><img src="https://velog.velcdn.com/images/idle-danie/post/3d10d09b-762a-4954-a7f9-bb552ddb5bc4/image.png" alt=""></p>
<h2 id="what-is-double-linked-list">What is Double Linked list?</h2>
<p>Double Linked list(이중 연결 리스트)는 각 노드가 두 개의 포인터를 가지는 자료구조다. 하나는 다음 노드를 가리키고 다른 하나는 이전 노드를 가리킨다. 이러한 구조 덕분에 양방향으로 리스트를 순회할 수 있어 단일 연결 리스트에 비해 노드의 삽입과 삭제가 더 효율적이다.</p>
<p>Double Linked list의 주요 특징은 다음과 같다.</p>
<ul>
<li>양방향 순회: 리스트의 앞과 뒤 어느 방향으로든 순회가 가능</li>
<li>더 빠른 삽입과 삭제: 특정 노드의 앞이나 뒤에 노드를 삽입하거나 삭제할 때, 추가적인 포인터 조작이 필요 없음</li>
</ul>
<h2 id="code-implementation-1">Code implementation</h2>
<blockquote>
<p>double linked list를 파이썬으로 직접 구현하면 아래와 같을 것입니다 ㅎ</p>
</blockquote>
<pre><code class="language-python">class Node:

    def __init__(self, item):
        self.data = item
        self.prev = None
        self.next = None


class DoublyLinkedList:

    def __init__(self):
        self.nodeCount = 0
        self.head = Node(None)
        self.tail = Node(None)
        self.head.prev = None
        self.head.next = self.tail
        self.tail.prev = self.head
        self.tail.next = None


    def __repr__(self):
        if self.nodeCount == 0:
            return &#39;LinkedList: empty&#39;

        s = &#39;&#39;
        curr = self.head
        while curr.next.next:
            curr = curr.next
            s += repr(curr.data)
            if curr.next.next is not None:
                s += &#39; -&gt; &#39;
        return s


    def getLength(self):
        return self.nodeCount


    def traverse(self):
        result = []
        curr = self.head
        while curr.next.next:
            curr = curr.next
            result.append(curr.data)
        return result


    def reverse(self):
        result = []
        curr = self.tail
        while curr.prev.prev:
            curr = curr.prev
            result.append(curr.data)
        return result


    def getAt(self, pos):
        if pos &lt; 0 or pos &gt; self.nodeCount:
            return None

        if pos &gt; self.nodeCount // 2:
            i = 0
            curr = self.tail
            while i &lt; self.nodeCount - pos + 1:
                curr = curr.prev
                i += 1
        else:
            i = 0
            curr = self.head
            while i &lt; pos:
                curr = curr.next
                i += 1

        return curr


    def insertAfter(self, prev, newNode):
        next = prev.next
        newNode.prev = prev
        newNode.next = next
        prev.next = newNode
        next.prev = newNode
        self.nodeCount += 1
        return True


    def insertAt(self, pos, newNode):
        if pos &lt; 1 or pos &gt; self.nodeCount + 1:
            return False

        prev = self.getAt(pos - 1)
        return self.insertAfter(prev, newNode)


    def popAfter(self, prev):
        curr = prev.next
        next = curr.next
        prev.next = next
        next.prev = prev
        self.nodeCount -= 1
        return curr.data


    def popAt(self, pos):
        if pos &lt; 1 or pos &gt; self.nodeCount:
            raise IndexError(&#39;Index out of range&#39;)

        prev = self.getAt(pos - 1)
        return self.popAfter(prev)


    def concat(self, L):
        self.tail.prev.next = L.head.next
        L.head.next.prev = self.tail.prev
        self.tail = L.tail

        self.nodeCount += L.nodeCount
</code></pre>
<p>이번에도 간단하게 코드에 대한 설명을 하자면 아래와 같습니다. </p>
<blockquote>
</blockquote>
<ul>
<li>Node 클래스는 이중 연결 리스트의 각 노드를 나타냅니다. 각 노드는 값(<code>data</code>), 이전 노드를 가리키는 포인터(<code>prev</code>), 다음 노드를 가리키는 포인터(<code>next</code>)를 가집니다.</li>
<li><code>DoublyLinkedList</code> 클래스는 이중 연결 리스트를 나타냅니다.</li>
<li><code>__repr__</code> 메서드는 리스트의 모든 노드를 문자열로 반환합니다.</li>
<li><code>getLength</code> 메서드는 리스트의 길이를 반환합니다.</li>
<li><code>traverse</code> 메서드는 리스트를 순회하며 모든 노드의 값을 리스트로 반환합니다.</li>
<li><code>reverse</code> 메서드는 리스트를 역순으로 순회하며 모든 노드의 값을 리스트로 반환합니다.</li>
<li><code>getAt</code> 메서드는 특정 위치에 있는 노드를 반환합니다.</li>
<li><code>insertAfter</code> 메서드는 특정 노드 뒤에 새로운 노드를 삽입합니다.</li>
<li><code>insertAt</code> 메서드는 특정 위치에 새로운 노드를 삽입합니다.</li>
<li><code>popAfter</code> 메서드는 특정 노드 뒤의 노드를 삭제합니다.</li>
<li><code>popAt</code> 메서드는 특정 위치의 노드를 삭제합니다.</li>
<li><code>concat</code> 메서드는 두 리스트를 연결합니다.</li>
</ul>
<h1 id="마무리">마무리</h1>
<blockquote>
<p>실제로 해당 개념들이 어떤 상황에서 활용되면 좋은지 알아보자 :)</p>
</blockquote>
<h3 id="linked-list-활용">Linked List 활용</h3>
<ol>
<li><p><strong>동적 메모리 할당</strong>: 배열과 달리 미리 크기를 정해놓지 않아도 되기 때문에, 동적으로 크기가 변하는 데이터를 처리할 때 유용하다. 예를 들어, 메모리 관리, 객체 풀(pool) 등에서 사용된다.</p>
</li>
<li><p><strong>데이터 삽입/삭제가 빈번한 경우</strong>: 특정 위치에 데이터를 삽입하거나 삭제할 때 배열보다 효율적이다. 삽입/삭제 시에 모든 요소를 이동시킬 필요 없이 포인터만 조작하면 되기 때문이다.</p>
</li>
<li><p><strong>스택과 큐 구현</strong>: Linked List는 스택과 큐 같은 자료구조를 구현하는 데 자주 사용된다. 스택에서는 후입선출(LIFO), 큐에서는 선입선출(FIFO) 방식으로 데이터를 처리할 수 있다.</p>
</li>
<li><p><strong>그래프 및 트리 구현</strong>: 그래프와 트리 같은 복잡한 자료구조도 Linked List를 사용해서 구현할 수 있다. 특히, 각 노드가 여러 자식 노드를 가질 수 있는 상황에서 유용하다.</p>
</li>
</ol>
<h3 id="double-linked-list-활용">Double Linked List 활용</h3>
<ol>
<li><p><strong>양방향 순회</strong>: 이중 연결 리스트는 앞뒤로 자유롭게 순회할 수 있어서, 양방향 탐색이 필요한 경우에 적합하다. 예를 들어, 뒤로 가기/앞으로 가기 기능이 있는 웹 브라우저의 히스토리 관리에 사용된다.</p>
</li>
<li><p><strong>이중 연결 리스트 기반 자료구조</strong>: Deque(양쪽 끝에서 삽입과 삭제가 가능한 큐), LRU(Least Recently Used) 캐시 등에서 사용된다. LRU 캐시는 최근에 사용된 적이 없는 데이터를 제거하는 방식의 캐시 알고리즘인데, 이중 연결 리스트를 사용하면 효율적으로 구현할 수 있다.</p>
</li>
<li><p><strong>텍스트 편집기</strong>: 텍스트 편집기에서 커서의 이동, 삽입, 삭제 같은 연산을 빠르게 처리하기 위해 이중 연결 리스트를 사용한다. 커서가 문장의 중간에 있을 때도 효율적으로 삽입/삭제가 가능하기 때문이다.</p>
</li>
</ol>
<p>이처럼 Linked List와 Double Linked List는 각각의 장점을 활용하면, 다양한 상황에서 효율적인 데이터 처리를 가능하게 할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[3rd party]]></title>
            <link>https://velog.io/@idle-danie/3rd-party</link>
            <guid>https://velog.io/@idle-danie/3rd-party</guid>
            <pubDate>Thu, 30 Mar 2023 18:50:55 GMT</pubDate>
            <description><![CDATA[<p>흔히 데이터 엔지니어링, 혹은 데이터 엔지니어의 역할을 이야기 할 때, <strong><code>3rd party</code></strong>라는 용어가 등장합니다. 정확하게 어떠한 뜻을 가지고 있는지 모호하게 인지하고 있는 것 같아, 간단하게 정리해보았습니다. </p>
<blockquote>
<p>KEYWORD: <em>하드웨어 개발자와 소프트웨어 개발자와의 관계</em></p>
</blockquote>
<ul>
<li><strong><code>1st party</code></strong> -&gt; 하드웨어 개발자, 원천기술 보유자 ex) Apple, MS</li>
<li><strong><code>2nd party</code></strong> -&gt; 하드웨어 개발자와의 직접적인 관계를 통해 소프트웨어를 개발하는 자 ex) 1st party와 하청관계를 가지고 개발하는 자 </li>
<li><strong><code>3rd party</code></strong> -&gt; 하드웨어 개발자와의 직접적인 관계 없이 소프트웨어를 개발하는 자, 원천기술을 활용하여 개발하는 자, 1st party와 user를 연결해주는 자
ex) Apple Appstore에서 앱 출시하는 앱 개발자, <strong>플러그인 &amp; 라이브러리 &amp; 프레임워크 개발자</strong></li>
</ul>
<blockquote>
<p><em>비즈니스적인 측면에서도 3rd party는 1st party의 프로덕트를 이용하게 하기 때문에 공생 관계라고 볼 수 있다</em></p>
</blockquote>
<p>구체적으로 공생 관계에 대한 예시는 아래와 같을 것입니다.</p>
<ol>
<li>앱스토어 생태계: Apple의 Appstore는 3rd party 개발자들이 만든 수많은 앱들로 가득 차 있습니다. 이들은 Apple의 iOS 플랫폼을 활용하여 혁신적인 앱을 개발하고, 사용자들에게 다양한 경험을 제공합니다. 이는 iOS 플랫폼의 가치를 높이고, 사용자들이 Apple 제품을 계속 사용하게 만드는 요인 중 하나입니다.</li>
<li>플러그인 및 라이브러리: 소프트웨어 개발에서 3rd party가 개발한 플러그인과 라이브러리는 개발자들이 보다 쉽게 기능을 추가하고, 효율적으로 작업할 수 있도록 돕습니다. 예를 들어, Python의 Pandas 라이브러리는 데이터 엔지니어들이 데이터 처리 작업을 더욱 간편하게 수행할 수 있도록 해줍니다.</li>
</ol>
<blockquote>
<p>참고문헌
<a href="https://ko.m.wikipedia.org/wiki/%EC%84%9C%EB%93%9C_%ED%8C%8C%ED%8B%B0_%EA%B0%9C%EB%B0%9C%EC%9E%90">https://ko.m.wikipedia.org/wiki/%EC%84%9C%EB%93%9C_%ED%8C%8C%ED%8B%B0_%EA%B0%9C%EB%B0%9C%EC%9E%90</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[유용한 Python 내장함수 (3) ]]></title>
            <link>https://velog.io/@idle-danie/%EC%9C%A0%EC%9A%A9%ED%95%9C-Python-%EB%82%B4%EC%9E%A5%ED%95%A8%EC%88%98-3</link>
            <guid>https://velog.io/@idle-danie/%EC%9C%A0%EC%9A%A9%ED%95%9C-Python-%EB%82%B4%EC%9E%A5%ED%95%A8%EC%88%98-3</guid>
            <pubDate>Fri, 24 Mar 2023 09:41:28 GMT</pubDate>
            <description><![CDATA[<p>Python 내장함수, 자료형, 정규표현식, 모듈...etc</p>
<hr>
<blockquote>
<p><strong>enumerate(list): 원소와 인덱스 동시에 얻을 수 있는 함수</strong></p>
</blockquote>
<pre><code>for i, element in enumerate(a, start = 1):
    print(i, element)

1 daniel
2 john
3 alex</code></pre><blockquote>
<p><strong>zip(): iterator 객체 사용하여 병렬처리 가능</strong></p>
</blockquote>
<pre><code>&gt;&gt;&gt; name = [&quot;Daniel&quot;, &quot;Alex&quot;, &quot;Steve&quot;]
&gt;&gt;&gt; st_num = [101, 102, 103]
&gt;&gt;&gt; st_score = [100, 90, 34]
&gt;&gt;&gt; st_score.append(12)
exam_score = list(zip(name, st_num, st_score))
&gt;&gt;&gt; exam_score
[(&#39;Daniel&#39;, 101, 100), (&#39;Alex&#39;, 102, 90), (&#39;Steve&#39;, 103, 34)]

&gt;&gt;&gt; a, b, c = zip(*exam_score)
&gt;&gt;&gt; a, b, c
((&#39;Daniel&#39;, &#39;Alex&#39;, &#39;Steve&#39;), (101, 102, 103), (100, 90, 34))
&gt;&gt;&gt; exam_score
[(&#39;Daniel&#39;, 101, 100), (&#39;Alex&#39;, 102, 90), (&#39;Steve&#39;, 103, 34)]

&gt;&gt;&gt; dict(zip(name, st_num))
{&#39;Daniel&#39;: 101, &#39;Alex&#39;: 102, &#39;Steve&#39;: 103}

&gt;&gt;&gt; for i in zip(name, st_num):
    print(i[0], i[1])

Daniel 101
Alex 102
Steve 103
</code></pre><blockquote>
<p><strong>from collections import Counter: 딕셔너리 형의 객체를 반환하여 요소의 개수를 value로 확인할 수 있다. most_common()은 tuple이 포함된 리스트를 반환한다.</strong></p>
</blockquote>
<pre><code>&gt;&gt;&gt; from collections import Counter
&gt;&gt;&gt; counter = Counter() # 빈 카운터 생성

&gt;&gt;&gt; Counter(name)
Counter({&#39;Daniel&#39;: 1, &#39;Alex&#39;: 1, &#39;Steve&#39;: 1})
&gt;&gt;&gt; Counter(&quot;sdifjodvmef&quot;)
Counter({&#39;d&#39;: 2, &#39;f&#39;: 2, &#39;s&#39;: 1, &#39;i&#39;: 1, &#39;j&#39;: 1, &#39;o&#39;: 1, &#39;v&#39;: 1, &#39;m&#39;: 1, &#39;e&#39;: 1})

&gt;&gt;&gt; a = Counter(name)
&gt;&gt;&gt; a[&quot;Daniel&quot;]
1

&gt;&gt;&gt; Counter(&quot;doijfoivmkd&quot;).most_common()
[(&#39;d&#39;, 2), (&#39;o&#39;, 2), (&#39;i&#39;, 2), (&#39;j&#39;, 1), (&#39;f&#39;, 1), (&#39;v&#39;, 1), (&#39;m&#39;, 1), (&#39;k&#39;, 1)]
&gt;&gt;&gt; Counter(&quot;udsinjckwdckomvl&quot;).most_common(3)
[(&#39;d&#39;, 2), (&#39;c&#39;, 2), (&#39;k&#39;, 2)]

&gt;&gt;&gt; b = Counter([1,1,1,1,1,1,1,1,2,2,2,12,31])
&gt;&gt;&gt; a+b
Counter({1: 13, 2: 8, 21: 1, 12: 1, 31: 1})
&gt;&gt;&gt; a-b
Counter({2: 2, 21: 1})
&gt;&gt;&gt; a=b.total()
Traceback (most recent call last):
  File &quot;&lt;pyshell#41&gt;&quot;, line 1, in &lt;module&gt;
    a=b.total()
AttributeError: &#39;Counter&#39; object has no attribute &#39;total&#39;
파이썬 3.1이후에 total()이 추가되었다고 하는데 지금은 호출되지 않는다. 굳이 구하려 한다면 아래와 같이 구하면 될 것이다. 
&gt;&gt;&gt; sum(a.values())
11

&gt;&gt;&gt; a &amp; b
Counter({1: 5, 2: 3})
&gt;&gt;&gt; a | b
Counter({1: 8, 2: 5, 21: 1, 12: 1, 31: 1})
&gt;&gt;&gt; a.items()
dict_items([(1, 5), (2, 5), (21, 1)])
&gt;&gt;&gt; a.values()
dict_values([5, 5, 1])

&gt;&gt;&gt; list(a.elements())
[1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 21]

&gt;&gt;&gt; a.update([1,1,1,1,1,1,1])
&gt;&gt;&gt; a
Counter({1: 12, 2: 5, 21: 1})

&gt;&gt;&gt; a.subtract([2,2,2,2])
&gt;&gt;&gt; 
&gt;&gt;&gt; a
Counter({1: 12, 2: 1, 21: 1})</code></pre><blockquote>
<p>*<em>sorted의 key활용: 다중조건에서 오름차순을 사용할 때 str같은 경우 -를 이용할 때 bad operand type으로 취급된다. *</em></p>
</blockquote>
<pre><code>&gt;&gt;&gt; sorted(array, key = lambda x:(x[1], x[0]))
[(&#39;b&#39;, 2), (&#39;g&#39;, 3), (&#39;a&#39;, 4), (&#39;k&#39;, 4), (&#39;f&#39;, 5)]
&gt;&gt;&gt; sorted(array, key = lambda x:(-x[1], x[0]))
[(&#39;f&#39;, 5), (&#39;a&#39;, 4), (&#39;k&#39;, 4), (&#39;g&#39;, 3), (&#39;b&#39;, 2)]</code></pre><blockquote>
<p><strong>dic 정렬: lamda식을 이용하여 key, value를 기준으로 정렬</strong></p>
</blockquote>
<p><strong><em>- key 정렬</em></strong></p>
<pre><code>&gt;&gt;&gt; sorted(dic)
[&#39;a&#39;, &#39;b&#39;, &#39;f&#39;, &#39;g&#39;, &#39;k&#39;]
&gt;&gt;&gt; sorted(dic.items())
[(&#39;a&#39;, 4), (&#39;b&#39;, 2), (&#39;f&#39;, 5), (&#39;g&#39;, 3), (&#39;k&#39;, 4)]
&gt;&gt;&gt; dict(sorted(dic.items()))
{&#39;a&#39;: 4, &#39;b&#39;: 2, &#39;f&#39;: 5, &#39;g&#39;: 3, &#39;k&#39;: 4}</code></pre><p><strong><em>- value 정렬</em></strong></p>
<pre><code>&gt;&gt;&gt; sorted(dic,key=lambda x:dic[x])
[&#39;b&#39;, &#39;g&#39;, &#39;a&#39;, &#39;k&#39;, &#39;f&#39;]
&gt;&gt;&gt; sorted(dic.items(), key=lambda x:x[1])
[(&#39;b&#39;, 2), (&#39;g&#39;, 3), (&#39;a&#39;, 4), (&#39;k&#39;, 4), (&#39;f&#39;, 5)]
&gt;&gt;&gt; dict(sorted(dic.items(), key=lambda x:x[1]))
{&#39;b&#39;: 2, &#39;g&#39;: 3, &#39;a&#39;: 4, &#39;k&#39;: 4, &#39;f&#39;: 5}</code></pre><blockquote>
<p><strong>startswith(): 특정 문자열로 시작되는지 확인하는 함수</strong></p>
</blockquote>
<pre><code>&gt;&gt;&gt; string = &quot;helloworld&quot;
&gt;&gt;&gt; string.startswith(&quot;hello&quot;)
True
&gt;&gt;&gt; string.startswith(&quot;hello &quot;)
False
&gt;&gt;&gt; string = &quot;Hello, my name is Daniel&quot;

&gt;&gt;&gt; for voc in string.lower().split():
    if voc.startswith(&quot;daniel&quot;):
        print(&quot;there is Daniel!&quot;)

there is Daniel!</code></pre><blockquote>
<p><strong>lower(), upper(), islower(), isupper(), swapcase(), capitalize()</strong></p>
</blockquote>
<pre><code>&gt;&gt;&gt; string
&#39;Hello, my name is Daniel&#39;

&gt;&gt;&gt; string.swapcase()
&#39;hELLO, MY NAME IS dANIEL&#39;

&gt;&gt;&gt; &quot;dddd&quot;.capitalize()
&#39;Dddd&#39;

&gt;&gt;&gt; string.upper()
&#39;HELLO, MY NAME IS DANIEL&#39;

&gt;&gt;&gt; string.lower()
&#39;hello, my name is daniel&#39;

&gt;&gt;&gt; string.isupper()
False
&gt;&gt;&gt; string.islower()
False
&gt;&gt;&gt; string.lower().islower()
True

&gt;&gt;&gt; string[10:].upper()
&#39;NAME IS DANIEL&#39;
&gt;&gt;&gt; string = string[0:10] + string[10:].upper()
&gt;&gt;&gt; string
&#39;Hello, my NAME IS DANIEL&#39;
&gt;&gt;&gt; </code></pre><blockquote>
<p><strong>set() 연산</strong></p>
</blockquote>
<ul>
<li>집합 선언<pre><code>&gt;&gt;&gt; s1 = {1,2,3,4,5}
&gt;&gt;&gt; s2 = {3,4,5,6,7}
&gt;&gt;&gt; a = [1,2]
&gt;&gt;&gt; s3 = set(a)
&gt;&gt;&gt; s3
{1, 2}</code></pre></li>
<li>합집합 (s.union())<pre><code>&gt;&gt;&gt; s4 = s1 | s2
&gt;&gt;&gt; s4
{1, 2, 3, 4, 5, 6, 7}</code></pre></li>
<li>교집합 (s.intersection())<pre><code>&gt;&gt;&gt; s5 = s1 &amp; s2
&gt;&gt;&gt; s5
{3, 4, 5}</code></pre></li>
<li>차집합 (s.difference())<pre><code>&gt;&gt;&gt; s2-s1
{6, 7}</code></pre></li>
<li>집합간의 비교, 교집합이 공집합인지 여부는 s.isdisjoint()<pre><code>&gt;&gt;&gt; s1 == s2
False
&gt;&gt;&gt; s1 != s2
True
&gt;&gt;&gt; s2
{3, 4, 5, 6, 7}
&gt;&gt;&gt; s1 = {3}
&gt;&gt;&gt; s1.isdisjoint(s2)
False</code></pre></li>
<li>add(), update()<pre><code>&gt;&gt;&gt; s1.add(9)
&gt;&gt;&gt; s1
{1, 2, 3, 4, 5, 9}
&gt;&gt;&gt; s1.update({1,2,4,5})
&gt;&gt;&gt; s1
{1, 2, 3, 4, 5, 9}</code></pre></li>
<li>remove(), discard(), pop(): discard()는 제거할 원소가 없어도 에러X<pre><code>&gt;&gt;&gt; s1.remove(1)
&gt;&gt;&gt; s1
{2, 3, 4, 5, 9}
&gt;&gt;&gt; s1.discard(10)
&gt;&gt;&gt; s1.discard(3)
&gt;&gt;&gt; s1
{2, 4, 5, 9}</code></pre></li>
<li>pop(), clear()<pre><code>&gt;&gt;&gt; s1.pop()
2
&gt;&gt;&gt; s1
{4, 5, 9}
&gt;&gt;&gt; s1.clear()
&gt;&gt;&gt; s1
set()</code></pre></li>
</ul>
<blockquote>
<p><strong>참고문헌</strong></p>
</blockquote>
<ol>
<li><a href="https://docs.python.org/3/library/collections.html#collections.Counter">https://docs.python.org/3/library/collections.html#collections.Counter</a></li>
<li><a href="https://docs.python.org/3/tutorial/index.html">https://docs.python.org/3/tutorial/index.html</a></li>
<li><a href="https://blockdmask.tistory.com/451">https://blockdmask.tistory.com/451</a></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[유용한 Python 내장함수 (2)]]></title>
            <link>https://velog.io/@idle-danie/%EC%9C%A0%EC%9A%A9%ED%95%9C-Python-%EB%82%B4%EC%9E%A5%ED%95%A8%EC%88%98-2</link>
            <guid>https://velog.io/@idle-danie/%EC%9C%A0%EC%9A%A9%ED%95%9C-Python-%EB%82%B4%EC%9E%A5%ED%95%A8%EC%88%98-2</guid>
            <pubDate>Tue, 07 Feb 2023 16:23:50 GMT</pubDate>
            <description><![CDATA[<p>Python 내장함수, 자료형, 정규표현식, 모듈...etc</p>
<hr>
<blockquote>
<p>*<em>sorted() vs list.sort() *</em></p>
</blockquote>
<pre><code>&gt;&gt;&gt; a = [234,35,1]
&gt;&gt;&gt; sorted(a)
[1, 35, 234]
&gt;&gt;&gt; a
[234, 35, 1]
&gt;&gt;&gt; a.sort()
&gt;&gt;&gt; a
[1, 35, 234]</code></pre><blockquote>
<p><strong>Dictionary (key, value 형식)</strong></p>
</blockquote>
<pre><code>&gt;&gt;&gt; a = {}
&gt;&gt;&gt; a[3] = [1,2,3]
&gt;&gt;&gt; a[3][1]
2
&gt;&gt;&gt; a[3][0]
1
&gt;&gt;&gt; a[2] = {1: &#39;a&#39;}
&gt;&gt;&gt; a
{3: [1, 2, 3], 2: {1: &#39;a&#39;}}
&gt;&gt;&gt; a[2][1]
&#39;a&#39;
&gt;&gt;&gt; del a[3]
&gt;&gt;&gt; a
{2: {1: &#39;a&#39;}}
</code></pre><blockquote>
<p><strong>Dictionary 내장함수 (keys(), values(), get(), clear())</strong></p>
</blockquote>
<pre><code>&gt;&gt;&gt; test = {1: 123, 2 : 2345, 3: 32435}
&gt;&gt;&gt; test
{1: 123, 2: 2345, 3: 32435}
&gt;&gt;&gt; test.keys()
dict_keys([1, 2, 3])
&gt;&gt;&gt; len(test)
3
&gt;&gt;&gt; for i in test.keys():
    print(i)
1
2
3
&gt;&gt;&gt; a = list(test.keys())
&gt;&gt;&gt; a
[1, 2, 3]</code></pre><pre><code>&gt;&gt;&gt; test.values()
dict_values([123, 2345, 32435])
&gt;&gt;&gt; for i in test.values():
    print(i)


123
2345
32435</code></pre><pre><code>&gt;&gt;&gt; for i in test.items():
    print(i)
    type(i)
    i[0]
    i[1]


(1, 123)
&lt;class &#39;tuple&#39;&gt;
1
123
(2, 2345)
&lt;class &#39;tuple&#39;&gt;
2
2345
(3, 32435)
&lt;class &#39;tuple&#39;&gt;
3
32435

&gt;&gt;&gt; test.get(1)
123</code></pre><blockquote>
<p><strong>Dictionary 값 추가 &amp; 삭제 (update(), pop())</strong></p>
</blockquote>
<pre><code>&gt;&gt;&gt; test.update(age = 10, height = 100)
&gt;&gt;&gt; test
{&#39;age&#39;: 10, &#39;height&#39;: 100} # key 문자열로 인식 

&gt;&gt;&gt; test.update({2018: 2022})
</code></pre><pre><code>&gt;&gt;&gt; test.pop(2018)
2022
&gt;&gt;&gt; test
{&#39;age&#39;: 10, &#39;height&#39;: 100}</code></pre><blockquote>
<p>*<em>lamda 표현식 *</em></p>
</blockquote>
<pre><code>&gt;&gt;&gt; a = lamda x:x+3
&gt;&gt;&gt; a(1)
4</code></pre><blockquote>
<p><strong>map(함수, list) and list comprehension</strong></p>
</blockquote>
<pre><code>&gt;&gt;&gt; a = [2,4,6,8,10]
&gt;&gt;&gt; c = list(map((lambda x:x//2), a))
&gt;&gt;&gt; c
[1, 2, 3, 4, 5]

&gt;&gt;&gt; list(map(lambda x,y: x+y, a, c))
[140, 188, 236]

&gt;&gt;&gt; c = [x//2 for x in a]
&gt;&gt;&gt; c
[1, 2, 3, 4, 5]

#### 가독성이 떨어지지만 아래와 같이 표현도 가능하다
&gt;&gt;&gt; d = list(map(lambda x:print(x) if x%2==0 else x+1, a))
2
4
6
8
10</code></pre><ul>
<li>filter()<pre><code>&gt;&gt;&gt; c = list(filter(lambda x:x&gt;3, a))
&gt;&gt;&gt; c
[4, 6, 8, 10]
</code></pre></li>
</ul>
<blockquote>
<blockquote>
<blockquote>
<p>c = [x*23 for x in a if x&gt;4]
c<img src="https://velog.velcdn.com/images/idle-danie/post/eeb67bd4-dbae-4606-9fa7-a5f4f471ebf2/image.jpg" alt=""></p>
</blockquote>
</blockquote>
</blockquote>
<p>[138, 184, 230]</p>
<blockquote>
<blockquote>
<blockquote>
</blockquote>
</blockquote>
</blockquote>
<pre><code>- reduce()</code></pre><blockquote>
<blockquote>
<blockquote>
<p>from functools import reduce
reduce(lambda x,y:x<em>y, a)
3840
reduce(lambda x,y:x</em>y, a, 2)
7680
reduce(lambda x,y:x*y, a, 0)
0</p>
</blockquote>
</blockquote>
</blockquote>
<pre><code>- list comprehension</code></pre><blockquote>
<blockquote>
<blockquote>
<p>c = [x<em>2 if x&gt;4 else x</em>5 for x in range(1,10)]
c
[5, 10, 15, 20, 10, 12, 14, 16, 18]</p>
</blockquote>
</blockquote>
</blockquote>
<pre><code>


</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[유용한 Python 내장함수 (1) ]]></title>
            <link>https://velog.io/@idle-danie/%EC%9C%A0%EC%9A%A9%ED%95%9C-Python-%EB%82%B4%EC%9E%A5%ED%95%A8%EC%88%98</link>
            <guid>https://velog.io/@idle-danie/%EC%9C%A0%EC%9A%A9%ED%95%9C-Python-%EB%82%B4%EC%9E%A5%ED%95%A8%EC%88%98</guid>
            <pubDate>Wed, 25 Jan 2023 15:38:56 GMT</pubDate>
            <description><![CDATA[<p>** Python 내장함수, 자료형, 정규표현식, 모듈...etc**</p>
<hr>
<blockquote>
<h3 id="문자열-공간-채우기-zfill-rjust">문자열 공간 채우기 (zfill(), rjust())</h3>
</blockquote>
<ul>
<li><p>zfill(): 특정 width만큼 0채우기 (단, 이미 width가 채워져 있다면 변화 X)</p>
<pre><code>&gt;&gt;&gt; a = &quot;0b113&quot;
&gt;&gt;&gt; a[2:].zfill(5)
&#39;00113&#39;</code></pre></li>
<li><p>rjust(): 0이아닌 특정 문자 지정 가능</p>
<pre><code>&gt;&gt;&gt; a[2:].rjust(5, &quot;a&quot;)
&#39;aa113&#39;</code></pre><blockquote>
<h3 id="특정-원소-인덱스-찾기-index-index">특정 원소 인덱스 찾기 (index(), index())</h3>
</blockquote>
</li>
<li><p>index()</p>
<pre><code>&gt;&gt;&gt; a = [&quot;Kim&quot;, &quot;Lee&quot;]
&gt;&gt;&gt; a.index(&quot;Lee&quot;)
1
</code></pre></li>
</ul>
<blockquote>
<blockquote>
<blockquote>
<p>a.index(&quot;Lim&quot;)
Traceback (most recent call last):
  File &quot;&lt;pyshell#2&gt;&quot;, line 1, in <module>
    a.index(&quot;Lim&quot;)
ValueError: &#39;Lim&#39; is not in list</p>
</blockquote>
</blockquote>
</blockquote>
<pre><code>- find()</code></pre><blockquote>
<blockquote>
<blockquote>
<p>a.find(&quot;Kim&quot;)
Traceback (most recent call last):
  File &quot;&lt;pyshell#3&gt;&quot;, line 1, in <module>
    a.find(&quot;Kim&quot;)
AttributeError: &#39;list&#39; object has no attribute &#39;find&#39;</p>
</blockquote>
</blockquote>
</blockquote>
<blockquote>
<blockquote>
<blockquote>
<p>b = &quot;Kim&quot;
b.find(&quot;i&quot;)
1</p>
</blockquote>
</blockquote>
</blockquote>
<blockquote>
<blockquote>
<blockquote>
<p>b.find(&quot;h&quot;)
-1</p>
</blockquote>
</blockquote>
</blockquote>
<pre><code>&gt; ### Binary 계산

- int(): n진수 -&gt; 10진수</code></pre><blockquote>
<blockquote>
<blockquote>
<p>int(&quot;1111&quot;,2)
15
int(&quot;1111&quot;,3)
40</p>
</blockquote>
</blockquote>
</blockquote>
<pre><code>- bin(), 논리연산</code></pre><blockquote>
<blockquote>
<blockquote>
<p>bin(20)
&#39;0b10100&#39;</p>
</blockquote>
</blockquote>
</blockquote>
<blockquote>
<blockquote>
<blockquote>
<p>39 | 22 # OR 연산
55
38 &amp; 33 # AND 연산
32
39 ^ 32 # XOR 연산
7
~39 # NOT 연산
-40</p>
</blockquote>
</blockquote>
</blockquote>
<pre><code>&gt; ### 순열, 조합 
</code></pre><p>from itertools import permutations, combinations, product, combinations_with_replacement</p>
<p>data = [&#39;A&#39;, &#39;B&#39;, &quot;C&quot;]</p>
<p>result = list(permutations(data, 2))
print(result) # 순열</p>
<p>result1 = list(combinations(data,2))
print(result1) # 조합</p>
<p>result2 = list(product(data, repeat=2))
print(result2) # 중복순열</p>
<p>result3 = list(combinations_with_replacement(data, 2))
print(result3) #중복조합</p>
<p>[(&#39;A&#39;, &#39;B&#39;), (&#39;A&#39;, &#39;C&#39;), (&#39;B&#39;, &#39;A&#39;), (&#39;B&#39;, &#39;C&#39;), (&#39;C&#39;, &#39;A&#39;), (&#39;C&#39;, &#39;B&#39;)]
[(&#39;A&#39;, &#39;B&#39;), (&#39;A&#39;, &#39;C&#39;), (&#39;B&#39;, &#39;C&#39;)]
[(&#39;A&#39;, &#39;A&#39;), (&#39;A&#39;, &#39;B&#39;), (&#39;A&#39;, &#39;C&#39;), (&#39;B&#39;, &#39;A&#39;), (&#39;B&#39;, &#39;B&#39;), (&#39;B&#39;, &#39;C&#39;), (&#39;C&#39;, &#39;A&#39;), (&#39;C&#39;, &#39;B&#39;), (&#39;C&#39;, &#39;C&#39;)]
[(&#39;A&#39;, &#39;A&#39;), (&#39;A&#39;, &#39;B&#39;), (&#39;A&#39;, &#39;C&#39;), (&#39;B&#39;, &#39;B&#39;), (&#39;B&#39;, &#39;C&#39;), (&#39;C&#39;, &#39;C&#39;)]</p>
<pre><code>&gt; ### 문자열에서 특정 문자 교체

- replace()
</code></pre><blockquote>
<blockquote>
<blockquote>
<p>a
&#39;hellohelloworld&#39;
a = a.replace(&quot;hello&quot;,&quot;stop&quot;)
a
&#39;stopstopworld&#39;
a = a.replace(&quot;stop&quot;, &quot;&quot;,1)
a
&#39;stopworld&#39;
a = a.replace(&quot;stop&quot;, &quot;hello&quot;)
a
&#39;helloworld&#39;
a.replace(&quot;hello&quot;,&quot;j&quot;).replace(&quot;w&quot;,&quot;k&quot;)
&#39;jkorld&#39;
a = &#39;helloworld&#39;
a = a[:2].replace(&quot;he&quot;, &quot;hello&quot;)
a
&#39;hello&#39;</p>
</blockquote>
</blockquote>
</blockquote>
<pre><code>&gt; ### find(), join(), split()을 이용한 문자열 삽입
</code></pre><blockquote>
<blockquote>
<blockquote>
<p>myIntro = &quot;{} is {} years old&quot;.format(myName, myAge)
myIntro = &quot;Daniel is 25 years old&quot;
idx = myIntro.find(&quot;25&quot;)
idx
10
myProfile = myIntro[:idx] + &quot;good at soccer and &quot; + myIntro[idx:]
myProfile
&#39;Daniel is good at soccer and 25 years old&#39;</p>
</blockquote>
</blockquote>
</blockquote>
<pre><code></code></pre><blockquote>
<blockquote>
<blockquote>
<p>stringList = myProfile.split()
stringList.insert(5, &quot;and polite &quot;)
myProfile = &#39; &#39;.join(stringList)
myProfile
&#39;Daniel is good at soccer and polite  and 25 years old&#39;</p>
</blockquote>
</blockquote>
</blockquote>
<pre><code>&gt; ### 리스트 요소 삭제

- del list[ ]</code></pre><blockquote>
<blockquote>
<blockquote>
<p>a = [1,2,3,4,5]
del a[1]
a
[1, 3, 4, 5]</p>
</blockquote>
</blockquote>
</blockquote>
<pre><code>- remove()</code></pre><blockquote>
<blockquote>
<blockquote>
<p>b = [1,1,1,1,66,88,99]
b.remove(1)
b
[1, 1, 1, 66, 88, 99]
for i in b:
    b.remove(3)
    print(b)</p>
</blockquote>
</blockquote>
</blockquote>
<p>Traceback (most recent call last):
  File &quot;&lt;pyshell#129&gt;&quot;, line 2, in <module>
    b.remove(3)
ValueError: list.remove(x): x not in list</p>
<h2 id="리스트-내에-제거할-원소가-있어야-한다">리스트 내에 제거할 원소가 있어야 한다</h2>
<blockquote>
<blockquote>
<blockquote>
<p>for i in b:
    b.remove(1)
    print(b)</p>
</blockquote>
</blockquote>
</blockquote>
<p>[1, 1, 66, 88, 99]
[1, 66, 88, 99]
[66, 88, 99]</p>
<pre><code></code></pre>]]></description>
        </item>
    </channel>
</rss>