<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>rudin_.log</title>
        <link>https://velog.io/</link>
        <description>성장하기 위한 기록</description>
        <lastBuildDate>Wed, 13 May 2026 03:16:38 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>rudin_.log</title>
            <url>https://velog.velcdn.com/images/rudin_/profile/98ac20e7-c040-43db-90ed-a1e9818e55b1/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. rudin_.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/rudin_" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[MicrosoftDataSchool] 87일차 - AzureDB Server RBAC, Defender, 사용량 경고]]></title>
            <link>https://velog.io/@rudin_/MicrosoftDataSchool-87%EC%9D%BC%EC%B0%A8-AzureDB-ServER-RBAC</link>
            <guid>https://velog.io/@rudin_/MicrosoftDataSchool-87%EC%9D%BC%EC%B0%A8-AzureDB-ServER-RBAC</guid>
            <pubDate>Wed, 13 May 2026 03:16:38 GMT</pubDate>
            <description><![CDATA[<h1 id="azure-sql-database-hands-on---데이터베이스-인증-및-권한-부여-구성">Azure SQL Database Hands-on - 데이터베이스 인증 및 권한 부여 구성</h1>
<blockquote>
<p>Microsoft DP-300 기반 Azure SQL Database 인증 및 권한 관리 실습 정리
Azure SQL Database에서 Microsoft Entra ID 기반 인증과 사용자·역할·권한 관리를 실습한다.</p>
</blockquote>
<hr>
<h1 id="1-실습-개요">1. 실습 개요</h1>
<h2 id="시나리오">시나리오</h2>
<p>AdventureWorks 환경의 보안을 담당하는 DBA 역할로, Azure SQL Database에 Microsoft Entra ID 기반 인증을 구성하고 최소 권한 원칙(Least Privilege)을 적용한다.</p>
<ul>
<li>Azure SQL Database에 Entra Admin 지정</li>
<li>Microsoft Entra MFA 인증으로 SSMS 접속</li>
<li>Contained User 생성</li>
<li>사용자 정의 Role 생성</li>
<li>Stored Procedure 실행 권한 부여</li>
<li><code>EXECUTE AS USER</code>를 이용한 권한 테스트</li>
</ul>
<hr>
<h1 id="2-핵심-개념-정리">2. 핵심 개념 정리</h1>
<h2 id="microsoft-entra-id">Microsoft Entra ID</h2>
<p>기존 Azure AD(Azure Active Directory)의 새로운 이름이다.
Azure SQL Database의 인증 백엔드로 사용할 수 있으며 MFA, 조건부 액세스 등을 적용할 수 있다.</p>
<h2 id="entra-admin">Entra Admin</h2>
<p>Azure SQL Logical Server 단위로 지정되는 관리자 계정이다.
해당 사용자는 서버 내 데이터베이스에 대한 최고 수준 권한을 가진다.</p>
<h2 id="contained-user">Contained User</h2>
<p>Master DB 로그인 없이 특정 데이터베이스 내부에서만 인증되는 사용자이다.</p>
<pre><code class="language-sql">CREATE USER [username] WITH PASSWORD = &#39;password&#39;;</code></pre>
<p>이 방식은 데이터베이스 이동성을 높인다.</p>
<h2 id="database-role">Database Role</h2>
<p>권한을 묶어서 관리하기 위한 역할(Role)이다.</p>
<pre><code class="language-sql">CREATE ROLE RoleName;
ALTER ROLE RoleName ADD MEMBER UserName;</code></pre>
<h2 id="execute-as-user">EXECUTE AS USER</h2>
<p>현재 세션의 실행 컨텍스트를 특정 사용자로 변경하여 권한을 테스트한다.</p>
<pre><code class="language-sql">EXECUTE AS USER = &#39;UserName&#39;</code></pre>
<p>원래 권한으로 돌아갈 때는 다음을 사용한다.</p>
<pre><code class="language-sql">REVERT;</code></pre>
<hr>
<h1 id="3-실습-환경-준비">3. 실습 환경 준비</h1>
<h2 id="준비물">준비물</h2>
<ul>
<li>Azure SQL Logical Server</li>
<li>AdventureWorksLT 샘플 데이터베이스</li>
<li>SSMS 또는 Azure Data Studio</li>
<li>Microsoft Entra 계정</li>
</ul>
<p>AdventureWorksLT 샘플 DB 생성 시:</p>
<ul>
<li>워크로드 환경: 개발</li>
<li>샘플 데이터 사용: <code>AdventureWorksLT</code></li>
</ul>
<p>선택하여 생성한다.</p>
<hr>
<h1 id="4-microsoft-entra-admin-구성">4. Microsoft Entra Admin 구성</h1>
<h2 id="azure-portal에서-관리자-지정">Azure Portal에서 관리자 지정</h2>
<h3 id="절차">절차</h3>
<ol>
<li>Azure Portal 접속</li>
<li>All resources 선택</li>
<li>SQL Server 선택</li>
<li><code>구성되지 않음</code> 클릭</li>
<li>Microsoft Entra ID 사용자 검색</li>
<li>사용자 선택 후 Save</li>
</ol>
<p>이 과정을 통해 해당 계정이 Azure SQL Server의 Entra Admin이 된다.</p>
<hr>
<h1 id="5-ssms에서-microsoft-entra-mfa-인증-연결">5. SSMS에서 Microsoft Entra MFA 인증 연결</h1>
<h2 id="서버-이름-복사">서버 이름 복사</h2>
<p>Azure Portal → SQL Server → Overview 에서 서버 이름 복사</p>
<p>예시:</p>
<pre><code class="language-text">myserver.database.windows.net</code></pre>
<hr>
<h2 id="ssms-연결">SSMS 연결</h2>
<h3 id="인증-방식-설정">인증 방식 설정</h3>
<p>SSMS에서:</p>
<ul>
<li><p>Connect → Database Engine</p>
</li>
<li><p>Server name 입력</p>
</li>
<li><p>Authentication:</p>
<ul>
<li><code>Microsoft Entra MFA</code></li>
<li>또는 <code>Azure Active Directory - Universal with MFA</code></li>
</ul>
</li>
</ul>
<p>선택</p>
<hr>
<h2 id="방화벽-이슈">방화벽 이슈</h2>
<p>처음 접속 시 클라이언트 IP를 방화벽에 추가해야 할 수 있다.</p>
<p>SSMS가 자동으로 추가 기능을 제공하기도 한다.</p>
<hr>
<h1 id="6-데이터베이스-사용자-생성">6. 데이터베이스 사용자 생성</h1>
<p>AdventureWorksLT 데이터베이스에서 새 쿼리를 생성한다.</p>
<h2 id="사용자-생성">사용자 생성</h2>
<pre><code class="language-sql">CREATE USER [DP300User1] WITH PASSWORD = &#39;Azur3Pa$$&#39;;
GO

CREATE USER [DP300User2] WITH PASSWORD = &#39;Azur3Pa$$&#39;;
GO</code></pre>
<p>이 사용자들은 AdventureWorksLT 데이터베이스 범위(scope) 안에서만 동작한다.</p>
<hr>
<h1 id="7-사용자-정의-role-생성">7. 사용자 정의 Role 생성</h1>
<h2 id="role-생성">Role 생성</h2>
<pre><code class="language-sql">CREATE ROLE [SalesReader];
GO</code></pre>
<h2 id="사용자-추가">사용자 추가</h2>
<pre><code class="language-sql">ALTER ROLE [SalesReader] ADD MEMBER [DP300User1];
GO

ALTER ROLE [SalesReader] ADD MEMBER [DP300User2];
GO</code></pre>
<hr>
<h1 id="8-stored-procedure-생성">8. Stored Procedure 생성</h1>
<h2 id="demoproc-생성">DemoProc 생성</h2>
<pre><code class="language-sql">CREATE OR ALTER PROCEDURE SalesLT.DemoProc
AS
SELECT
    P.Name,
    SUM(SOD.LineTotal) AS TotalSales,
    SOH.OrderDate
FROM SalesLT.Product P
INNER JOIN SalesLT.SalesOrderDetail SOD
    ON SOD.ProductID = P.ProductID
INNER JOIN SalesLT.SalesOrderHeader SOH
    ON SOH.SalesOrderID = SOD.SalesOrderID
GROUP BY P.Name, SOH.OrderDate
ORDER BY TotalSales DESC
GO</code></pre>
<p>이 프로시저는 상품별 매출 데이터를 조회한다.</p>
<hr>
<h1 id="9-권한-테스트">9. 권한 테스트</h1>
<h2 id="execute-as-user-실행">EXECUTE AS USER 실행</h2>
<pre><code class="language-sql">EXECUTE AS USER = &#39;DP300User1&#39;
EXECUTE SalesLT.DemoProc</code></pre>
<p>실행 시 권한 오류가 발생한다.</p>
<pre><code class="language-text">The EXECUTE permission was denied on the object &#39;DemoProc&#39;</code></pre>
<p>왜냐하면 <code>SalesReader</code> 역할에는 아직 아무 권한도 없기 때문이다.</p>
<hr>
<h1 id="10-execute-권한-부여">10. EXECUTE 권한 부여</h1>
<h2 id="권한-추가">권한 추가</h2>
<pre><code class="language-sql">REVERT;

GRANT EXECUTE ON SCHEMA::SalesLT TO [SalesReader];
GO</code></pre>
<ul>
<li><p><code>REVERT</code></p>
<ul>
<li>원래 권한 컨텍스트로 복귀</li>
</ul>
</li>
<li><p><code>GRANT EXECUTE</code></p>
<ul>
<li>SalesLT 스키마 내 프로시저 실행 권한 부여</li>
</ul>
</li>
</ul>
<hr>
<h1 id="11-다시-실행">11. 다시 실행</h1>
<pre><code class="language-sql">EXECUTE AS USER = &#39;DP300User1&#39;
EXECUTE SalesLT.DemoProc</code></pre>
<p>이번에는 정상적으로 결과가 반환된다.</p>
<hr>
<h1 id="12-핵심-심화-개념---ownership-chain">12. 핵심 심화 개념 - Ownership Chain</h1>
<p>이번 실습에서 가장 중요한 개념이다.</p>
<h2 id="왜-execute-권한만-줬는데-select가-되었을까">왜 EXECUTE 권한만 줬는데 SELECT가 되었을까?</h2>
<p><code>DP300User1</code>에게는 다음 테이블에 대한 SELECT 권한이 없다.</p>
<ul>
<li>SalesLT.Product</li>
<li>SalesLT.SalesOrderDetail</li>
<li>SalesLT.SalesOrderHeader</li>
</ul>
<p>그런데도 Stored Procedure는 정상 동작했다.</p>
<p>이유는 SQL Server의 <strong>Ownership Chain(소유권 체인)</strong> 때문이다.</p>
<hr>
<h2 id="ownership-chain-원리">Ownership Chain 원리</h2>
<p>SQL Server는 Stored Procedure 실행 시:</p>
<ol>
<li>프로시저 자체의 EXECUTE 권한만 검사</li>
<li>내부 객체들이 동일한 owner이면</li>
<li>내부 SELECT 권한 검사를 생략</li>
</ol>
<p>즉:</p>
<pre><code class="language-text">사용자
→ 프로시저 실행 권한 검사
→ 내부 테이블 권한 검사 생략
→ 실행 성공</code></pre>
<hr>
<h2 id="이번-실습에서-체인이-유지된-이유">이번 실습에서 체인이 유지된 이유</h2>
<p>다음 객체들의 owner가 모두 <code>dbo</code>이다.</p>
<ul>
<li>SalesLT.DemoProc</li>
<li>SalesLT.Product</li>
<li>SalesLT.SalesOrderDetail</li>
<li>SalesLT.SalesOrderHeader</li>
</ul>
<p>따라서 Ownership Chain이 끊기지 않는다.</p>
<hr>
<h1 id="13-운영-관점에서-중요한-점">13. 운영 관점에서 중요한 점</h1>
<p>Ownership Chain은 운영 환경에서 매우 많이 사용된다.</p>
<p>대표 패턴:</p>
<pre><code class="language-text">사용자에게 테이블 직접 권한은 주지 않음
↓
Stored Procedure 실행만 허용
↓
프로시저 내부에서 데이터 접근</code></pre>
<p>이 방식의 장점:</p>
<ul>
<li>최소 권한 원칙 적용 가능</li>
<li>테이블 직접 접근 차단 가능</li>
<li>API 형태의 DB 접근 구조 구현 가능</li>
</ul>
<hr>
<h2 id="하지만-주의할-점">하지만 주의할 점</h2>
<p>권한이 강력한 만큼 위험성도 존재한다.</p>
<p>특히:</p>
<ul>
<li>프로시저 내부에서 민감 컬럼 노출 가능</li>
<li>동적 SQL 사용 시 Ownership Chain 깨질 수 있음</li>
</ul>
<p>운영 환경 권장 사항:</p>
<ul>
<li>스키마 owner 표준화</li>
<li><code>WITH EXECUTE AS OWNER</code> 사용</li>
<li>정기 권한 감사 수행</li>
</ul>
<hr>
<h1 id="14-실습-검증-체크리스트">14. 실습 검증 체크리스트</h1>
<p>다음 항목이 모두 성공하면 실습 완료이다.</p>
<ul>
<li>Entra ID 관리자가 서버에 지정됨</li>
<li>DP300User1 / DP300User2 생성 완료</li>
<li>SalesReader Role 생성 완료</li>
<li>권한 부여 전 EXECUTE 실패 확인</li>
<li>GRANT EXECUTE 후 실행 성공 확인</li>
</ul>
<hr>
<h1 id="15-정리clean-up">15. 정리(Clean-up)</h1>
<p>실습 종료 후 삭제 가능:</p>
<pre><code class="language-sql">DROP USER DP300User1;
DROP USER DP300User2;
DROP ROLE SalesReader;</code></pre>
<p>운영 환경에서는 Entra Admin을 개인 계정보다는 보안 그룹 기반으로 관리하는 것이 일반적이다.</p>
<hr>
<h1 id="마무리">마무리</h1>
<ul>
<li>Microsoft Entra ID 인증</li>
<li>MFA 기반 SSMS 연결</li>
<li>Contained User 생성</li>
<li>Database Role 기반 권한 관리</li>
<li>Stored Procedure 실행 권한</li>
<li>EXECUTE AS USER 권한 테스트</li>
<li>Ownership Chain 동작 원리</li>
</ul>
<hr>
<h1 id="azure-sql-database-hands-on---microsoft-defender-for-sql-활성화-및-데이터-분류">Azure SQL Database Hands-on - Microsoft Defender for SQL 활성화 및 데이터 분류</h1>
<blockquote>
<p>Azure SQL Database에서 Microsoft Defender for SQL을 활성화하고, 데이터 분류(Data Discovery &amp; Classification), 취약성 평가(VA), Threat Detection, Auditing, Dynamic Data Masking까지 실습한다.</p>
</blockquote>
<hr>
<h1 id="1-실습-개요-1">1. 실습 개요</h1>
<h2 id="시나리오-1">시나리오</h2>
<ul>
<li>Microsoft Defender for SQL 활성화</li>
<li>민감 정보 자동 분류</li>
<li>Vulnerability Assessment(VA)</li>
<li>Threat Detection 알림</li>
<li>SQL Auditing</li>
<li>Dynamic Data Masking(DDM)</li>
</ul>
<pre><code class="language-text">발견 → 경고 → 추적 → 보호</code></pre>
<hr>
<h1 id="2-핵심-개념-정리-1">2. 핵심 개념 정리</h1>
<h2 id="microsoft-defender-for-sql">Microsoft Defender for SQL</h2>
<p>Azure SQL Database의 보안 기능이다.</p>
<p>주요 기능:</p>
<ul>
<li>SQL Injection 탐지</li>
<li>이상 로그인 탐지</li>
<li>Brute-force 탐지</li>
<li>Vulnerability Assessment(VA)</li>
<li>Threat Detection</li>
</ul>
<p>등을 제공한다.</p>
<hr>
<h2 id="data-discovery--classification">Data Discovery &amp; Classification</h2>
<p>DB 내부 컬럼을 스캔하여:</p>
<ul>
<li>이메일</li>
<li>전화번호</li>
<li>개인정보</li>
<li>금융정보</li>
</ul>
<p>같은 민감 데이터를 자동 식별하고 분류(Label)를 부여한다.</p>
<hr>
<h2 id="vulnerability-assessmentva">Vulnerability Assessment(VA)</h2>
<p>보안 취약점을 자동 분석하는 기능이다.</p>
<p>예시:</p>
<ul>
<li>과도한 권한</li>
<li>위험한 설정</li>
<li>오래된 구성</li>
<li>감사 미설정</li>
</ul>
<p>등을 탐지한다.</p>
<hr>
<h2 id="sql-auditing">SQL Auditing</h2>
<p>데이터베이스 내부 이벤트를 기록하는 기능이다.</p>
<p>저장 위치:</p>
<ul>
<li>Storage Account</li>
<li>Log Analytics</li>
<li>Event Hub</li>
</ul>
<p>등으로 전송 가능하다.</p>
<hr>
<h2 id="dynamic-data-maskingddm">Dynamic Data Masking(DDM)</h2>
<p>실제 데이터를 변경하지 않고:</p>
<pre><code class="language-text">조회 결과만 가려서 표시</code></pre>
<p>하는 기능이다.</p>
<p>예시:</p>
<pre><code class="language-text">test@example.com
↓
tXXX@XXXX.com</code></pre>
<hr>
<h1 id="3-microsoft-defender-for-sql-활성화">3. Microsoft Defender for SQL 활성화</h1>
<h2 id="azure-portal-접속">Azure Portal 접속</h2>
<ol>
<li>Azure Portal 접속</li>
<li>SQL servers 검색</li>
<li>대상 SQL Server 선택</li>
</ol>
<hr>
<h2 id="defender-활성화">Defender 활성화</h2>
<p>경로:</p>
<pre><code class="language-text">SQL Server
→ Security
→ Microsoft Defender for Cloud</code></pre>
<p>이후:</p>
<pre><code class="language-text">사용(Enable)</code></pre>
<p>클릭</p>
<hr>
<h2 id="토글-확인">토글 확인</h2>
<p><code>Configure</code> 진입 후:</p>
<pre><code class="language-text">MICROSOFT DEFENDER FOR SQL = ON</code></pre>
<p>상태인지 확인한다.</p>
<hr>
<h1 id="4-data-discovery--classification">4. Data Discovery &amp; Classification</h1>
<h2 id="데이터-분류-기능-진입">데이터 분류 기능 진입</h2>
<p>경로:</p>
<pre><code class="language-text">AdventureWorksLT
→ Security
→ Data Discovery &amp; Classification</code></pre>
<hr>
<h2 id="자동-분류-추천-수락">자동 분류 추천 수락</h2>
<p>포털에서 다음 메시지가 표시된다.</p>
<pre><code class="language-text">15개의 열에서 민감 정보 발견</code></pre>
<p>이후:</p>
<ul>
<li>Select All</li>
<li>Accept selected recommendations</li>
<li>Save</li>
</ul>
<p>를 실행한다.</p>
<hr>
<h2 id="결과">결과</h2>
<p>총:</p>
<ul>
<li>5개 테이블</li>
<li>15개 컬럼</li>
</ul>
<p>이 자동 분류된다.</p>
<hr>
<h1 id="5-vulnerability-assessmentva">5. Vulnerability Assessment(VA)</h1>
<h2 id="목적">목적</h2>
<p>보안 취약점을 자동 스캔한다.</p>
<p>예시:</p>
<ul>
<li>약한 설정</li>
<li>과도 권한</li>
<li>감사 미설정</li>
<li>공개 접근</li>
</ul>
<p>등을 탐지한다.</p>
<hr>
<h2 id="실습-흐름">실습 흐름</h2>
<ol>
<li>Defender for SQL 활성화</li>
<li>VA 실행</li>
<li>Findings 확인</li>
<li>일부 Remediation 수행</li>
<li>Passed 상태 확인</li>
</ol>
<hr>
<h1 id="6-threat-detection-알림-시뮬레이션">6. Threat Detection 알림 시뮬레이션</h1>
<h2 id="목적-1">목적</h2>
<p>실제 보안 알림 흐름을 테스트한다.</p>
<hr>
<h2 id="이메일-알림-설정">이메일 알림 설정</h2>
<p>Defender for Cloud 설정에서:</p>
<ul>
<li>이메일 주소 등록</li>
<li>알림 유형 선택</li>
</ul>
<p>후 저장한다.</p>
<hr>
<h2 id="brute-force-시뮬레이션">Brute-force 시뮬레이션</h2>
<p>SSMS에서:</p>
<pre><code class="language-text">잘못된 비밀번호로 4~5회 로그인</code></pre>
<p>시도</p>
<hr>
<h2 id="sql-injection-패턴-시뮬레이션">SQL Injection 패턴 시뮬레이션</h2>
<pre><code class="language-sql">EXEC sp_executesql
N&#39;SELECT * FROM sys.databases WHERE name = &#39;&#39;anything&#39;&#39; OR 1=1&#39;;
GO</code></pre>
<p>또는:</p>
<pre><code class="language-sql">DECLARE @sql NVARCHAR(MAX) =
N&#39;SELECT * FROM sys.tables WHERE name LIKE &#39;&#39;%&#39;&#39; OR 1=1&#39;;

EXEC (@sql);</code></pre>
<p>이후 Defender for Cloud의:</p>
<pre><code class="language-text">Security alerts</code></pre>
<p>에서 알림을 확인한다.</p>
<p>예시:</p>
<ul>
<li>Brute force attack</li>
<li>Potential SQL injection</li>
</ul>
<hr>
<h1 id="7-민감도-분류-메타데이터-확인">7. 민감도 분류 메타데이터 확인</h1>
<h2 id="시스템-카탈로그-조회">시스템 카탈로그 조회</h2>
<p>분류 정보는:</p>
<pre><code class="language-text">sys.sensitivity_classifications</code></pre>
<p>에 저장된다.</p>
<hr>
<h2 id="조회-쿼리">조회 쿼리</h2>
<pre><code class="language-sql">SELECT 
    SCHEMA_NAME(o.schema_id) AS [schema],
    o.name AS [table],
    c.name AS [column],
    sc.label,
    sc.information_type,
    sc.rank_desc
FROM sys.sensitivity_classifications sc
INNER JOIN sys.objects o
    ON sc.major_id = o.object_id
INNER JOIN sys.columns c
    ON sc.major_id = c.object_id
   AND sc.minor_id = c.column_id
ORDER BY [schema], [table], [column];</code></pre>
<hr>
<h2 id="수동-분류-추가">수동 분류 추가</h2>
<pre><code class="language-sql">ADD SENSITIVITY CLASSIFICATION TO
    SalesLT.Customer.MiddleName
WITH (
    LABEL = &#39;Confidential&#39;,
    LABEL_ID = &#39;332211aa-bbcc-ddee-ff00-112233445566&#39;,
    INFORMATION_TYPE = &#39;Name&#39;,
    INFORMATION_TYPE_ID = &#39;5b56518b-5a91-490b-9300-983344497a82&#39;,
    RANK = MEDIUM
);</code></pre>
<hr>
<h2 id="결과-확인">결과 확인</h2>
<p>기존:</p>
<pre><code class="language-text">15개 컬럼</code></pre>
<p>↓</p>
<p>추가 후:</p>
<pre><code class="language-text">16개 컬럼</code></pre>
<p>으로 증가한다.</p>
<hr>
<h1 id="8-sql-auditing--log-analytics">8. SQL Auditing + Log Analytics</h1>
<h2 id="목적-2">목적</h2>
<p>민감 데이터 접근 이력을 추적한다.</p>
<hr>
<h2 id="auditing-활성화">Auditing 활성화</h2>
<p>경로:</p>
<pre><code class="language-text">AdventureWorksLT
→ Auditing
→ Enable Azure SQL Auditing</code></pre>
<p>이후:</p>
<ul>
<li>Log Analytics 연결</li>
<li>Diagnostic Settings 추가</li>
</ul>
<p>를 수행한다.</p>
<hr>
<h2 id="감사-이벤트-발생">감사 이벤트 발생</h2>
<pre><code class="language-sql">SELECT TOP 10
    EmailAddress,
    FirstName,
    LastName
FROM SalesLT.Customer;
GO</code></pre>
<hr>
<h2 id="kql-조회">KQL 조회</h2>
<pre><code class="language-sql">AzureDiagnostics
| where Category == &quot;SQLSecurityAuditEvents&quot;
| take 10</code></pre>
<hr>
<h2 id="핵심-포인트">핵심 포인트</h2>
<p>감사 로그 내부에:</p>
<pre><code class="language-text">data_sensitivity_information_s</code></pre>
<p>필드가 자동 포함된다.</p>
<p>즉:</p>
<pre><code class="language-text">누가 민감 데이터를 조회했는가</code></pre>
<p>를 추적할 수 있다.</p>
<hr>
<h1 id="9-dynamic-data-maskingddm">9. Dynamic Data Masking(DDM)</h1>
<h2 id="목적-3">목적</h2>
<p>민감 데이터를 일반 사용자에게 숨긴다.</p>
<hr>
<h1 id="마스킹-규칙-추가">마스킹 규칙 추가</h1>
<h2 id="email-마스킹">Email 마스킹</h2>
<pre><code class="language-sql">ALTER TABLE SalesLT.Customer
    ALTER COLUMN EmailAddress
    ADD MASKED WITH (FUNCTION = &#39;email()&#39;);
GO</code></pre>
<hr>
<h2 id="phone-마스킹">Phone 마스킹</h2>
<pre><code class="language-sql">ALTER TABLE SalesLT.Customer
    ALTER COLUMN Phone
    ADD MASKED WITH (
        FUNCTION = &#39;partial(0,&quot;XXX-XXX-&quot;,4)&#39;
    );
GO</code></pre>
<hr>
<h1 id="10-일반-사용자-관점-테스트">10. 일반 사용자 관점 테스트</h1>
<h2 id="execute-as-user-1">EXECUTE AS USER</h2>
<pre><code class="language-sql">EXECUTE AS USER = &#39;DP300User1&#39;;

SELECT TOP 5
    FirstName,
    LastName,
    EmailAddress,
    Phone
FROM SalesLT.Customer;

REVERT;</code></pre>
<hr>
<h2 id="결과-1">결과</h2>
<p>일반 사용자는:</p>
<pre><code class="language-text">aXXX@XXXX.com
XXX-XXX-1234</code></pre>
<p>형태로 보인다.</p>
<hr>
<h1 id="11-unmask-권한-부여">11. UNMASK 권한 부여</h1>
<h2 id="권한-추가-1">권한 추가</h2>
<pre><code class="language-sql">GRANT UNMASK TO DP300User1;</code></pre>
<hr>
<h2 id="다시-조회">다시 조회</h2>
<pre><code class="language-sql">EXECUTE AS USER = &#39;DP300User1&#39;;

SELECT TOP 5
    EmailAddress,
    Phone
FROM SalesLT.Customer;

REVERT;</code></pre>
<p>이번에는 실제 원본 데이터가 보인다.</p>
<hr>
<h2 id="원상-복구">원상 복구</h2>
<pre><code class="language-sql">REVOKE UNMASK FROM DP300User1;

ALTER TABLE SalesLT.Customer
    ALTER COLUMN EmailAddress DROP MASKED;

ALTER TABLE SalesLT.Customer
    ALTER COLUMN Phone DROP MASKED;</code></pre>
<hr>
<h1 id="12-실무적으로-중요한-포인트">12. 실무적으로 중요한 포인트</h1>
<h2 id="ddm의-한계">DDM의 한계</h2>
<p>DDM은:</p>
<pre><code class="language-text">표시값만 가리는 기능</code></pre>
<p>이다.</p>
<p>즉:</p>
<ul>
<li>실제 데이터는 그대로 존재</li>
<li>관리자 권한자는 원본 조회 가능</li>
</ul>
<p>하다.</p>
<hr>
<h2 id="실제-운영에서는">실제 운영에서는</h2>
<p>다음 조합으로 다층 방어를 구성한다.</p>
<pre><code class="language-text">Classification
→ DDM
→ TDE / Always Encrypted
→ RLS(Row-Level Security)</code></pre>
<hr>
<h1 id="13-실습-검증-체크리스트">13. 실습 검증 체크리스트</h1>
<p>다음 항목이 모두 성공하면 실습 완료이다.</p>
<ul>
<li>Defender for SQL 활성화</li>
<li>15개 컬럼 자동 분류</li>
<li>VA Findings 확인</li>
<li>Threat Detection 알림 확인</li>
<li>sys.sensitivity_classifications 증가 확인</li>
<li>SQLSecurityAuditEvents 수집 확인</li>
<li>DDM 마스킹 동작 확인</li>
<li>UNMASK 후 원본 노출 확인</li>
</ul>
<hr>
<h1 id="14-정리clean-up">14. 정리(Clean-up)</h1>
<p>실습 종료 후:</p>
<ul>
<li>DROP MASKED</li>
<li>REVOKE UNMASK</li>
<li>추가 분류 제거</li>
<li>Log Analytics 삭제</li>
<li>VA Storage 삭제</li>
</ul>
<p>등을 수행할 수 있다.</p>
<hr>
<h1 id="마무리-1">마무리</h1>
<p>이번 실습에서는 Azure SQL Database 보안 기능 전반을 실습했다.</p>
<pre><code class="language-text">Defender 활성화
→ 민감 데이터 발견
→ Threat Detection
→ 감사 로그 수집
→ Dynamic Data Masking 보호</code></pre>
<pre><code class="language-text">발견 → 탐지 → 감사 → 보호</code></pre>
<hr>
<h1 id="azure-sql-database-hands-on---cpu-사용률-식별-및-경고">Azure SQL Database Hands-on - CPU 사용률 식별 및 경고</h1>
<blockquote>
<p>Azure Monitor를 활용하여 Azure SQL Database의 CPU 사용률을 모니터링하고, 평균 CPU 사용량이 80%를 초과할 경우 이메일 알림을 전송하는 Alert Rule을 구성한다.</p>
</blockquote>
<hr>
<h1 id="1-실습-개요-2">1. 실습 개요</h1>
<h2 id="시나리오-2">시나리오</h2>
<p>AdventureWorksLT 데이터베이스의 평균 CPU 사용률이 80%를 초과하면 자동으로 이메일 경고를 전송하도록 Azure Monitor Alert를 구성한다.</p>
<p>이번 실습에서는:</p>
<ul>
<li>CPU percentage 메트릭 기반 경고 생성</li>
<li>Alert Rule 구성</li>
<li>Action Group 생성</li>
<li>이메일 알림 설정</li>
<li>실제 CPU 부하 테스트</li>
</ul>
<p>까지 수행한다.</p>
<hr>
<h1 id="2-핵심-개념-정리-2">2. 핵심 개념 정리</h1>
<h2 id="azure-monitor-alert-rule">Azure Monitor Alert Rule</h2>
<p>Azure 리소스의 메트릭 또는 로그를 주기적으로 평가하여:</p>
<pre><code class="language-text">조건 충족 시 자동 작업(Action) 실행</code></pre>
<p>하는 기능이다.</p>
<p>구성 요소:</p>
<ul>
<li>Signal</li>
<li>Condition</li>
<li>Action Group</li>
</ul>
<p>으로 이루어진다.</p>
<hr>
<h2 id="cpu-percentage">CPU percentage</h2>
<p>Azure SQL Database의 평균 CPU 사용률 메트릭이다.</p>
<p>기본적으로:</p>
<pre><code class="language-text">1분 단위 집계</code></pre>
<p>가 사용된다.</p>
<hr>
<h2 id="aggregation-type">Aggregation Type</h2>
<p>메트릭 집계 방식이다.</p>
<p>대표 유형:</p>
<ul>
<li>Average</li>
<li>Minimum</li>
<li>Maximum</li>
<li>Total</li>
</ul>
<p>이번 실습에서는:</p>
<pre><code class="language-text">Average</code></pre>
<p>를 사용한다.</p>
<hr>
<h2 id="action-group">Action Group</h2>
<p>경고 발생 시 수행할 작업 묶음이다.</p>
<p>예시:</p>
<ul>
<li>Email</li>
<li>SMS</li>
<li>Push</li>
<li>Voice</li>
<li>Webhook</li>
<li>Azure Function</li>
</ul>
<p>등을 연결할 수 있다.</p>
<hr>
<h1 id="3-실습-환경-준비-1">3. 실습 환경 준비</h1>
<h2 id="준비물-1">준비물</h2>
<ul>
<li>AdventureWorksLT 데이터베이스</li>
<li>Azure Portal 접근 권한</li>
<li>이메일 수신 가능 계정</li>
</ul>
<hr>
<h1 id="4-azure-monitor-alert-생성">4. Azure Monitor Alert 생성</h1>
<h2 id="alerts-메뉴-진입">Alerts 메뉴 진입</h2>
<p>경로:</p>
<pre><code class="language-text">AdventureWorksLT
→ Monitoring
→ Alerts</code></pre>
<p>이후:</p>
<pre><code class="language-text">+ Create alert rule</code></pre>
<p>선택</p>
<hr>
<h1 id="5-cpu-signal-설정">5. CPU Signal 설정</h1>
<h2 id="signal-선택">Signal 선택</h2>
<pre><code class="language-text">CPU percentage</code></pre>
<p>선택</p>
<hr>
<h2 id="조건-설정">조건 설정</h2>
<p>다음과 같이 설정한다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>값</th>
</tr>
</thead>
<tbody><tr>
<td>Threshold Type</td>
<td>Static</td>
</tr>
<tr>
<td>Aggregation Type</td>
<td>Average</td>
</tr>
<tr>
<td>Operator</td>
<td>Greater than</td>
</tr>
<tr>
<td>Threshold Value</td>
<td>80</td>
</tr>
</tbody></table>
<p>즉:</p>
<pre><code class="language-text">평균 CPU 사용률 &gt; 80%</code></pre>
<p>조건이 되면 경고가 발생한다.</p>
<hr>
<h1 id="6-action-group-생성">6. Action Group 생성</h1>
<h2 id="actions-탭-이동">Actions 탭 이동</h2>
<p>Alert Rule 생성 화면에서:</p>
<pre><code class="language-text">Actions
→ Create action group</code></pre>
<p>선택</p>
<hr>
<h2 id="기본-정보-입력">기본 정보 입력</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>값</th>
</tr>
</thead>
<tbody><tr>
<td>Action group name</td>
<td>emailgroup</td>
</tr>
<tr>
<td>Display name</td>
<td>emailgroup</td>
</tr>
</tbody></table>
<p>입력 후:</p>
<pre><code class="language-text">Next: Notifications</code></pre>
<p>선택</p>
<hr>
<h1 id="7-이메일-알림-설정">7. 이메일 알림 설정</h1>
<h2 id="notifications-설정">Notifications 설정</h2>
<p>다음과 같이 입력한다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>값</th>
</tr>
</thead>
<tbody><tr>
<td>Notification type</td>
<td>Email/SMS message/Push/Voice</td>
</tr>
<tr>
<td>Name</td>
<td>DemoLab</td>
</tr>
</tbody></table>
<p>이후 이메일 주소 입력</p>
<hr>
<h2 id="생성-완료">생성 완료</h2>
<pre><code class="language-text">Review + create
→ Create</code></pre>
<p>를 눌러 Alert Rule과 Action Group을 생성한다.</p>
<hr>
<h1 id="8-이메일-알림-확인">8. 이메일 알림 확인</h1>
<p>구성이 완료되면:</p>
<pre><code class="language-text">You&#39;ve been added to an Azure Monitor action group</code></pre>
<p>형태의 이메일을 수신할 수 있다.</p>
<p>이후 실제 CPU 사용량이 80%를 초과하면 Azure Monitor Alert 메일이 전송된다.</p>
<hr>
<h1 id="9-실제-cpu-부하-발생-테스트">9. 실제 CPU 부하 발생 테스트</h1>
<p>실습 문서에서는 CPU 부하를 강제로 발생시키기 위한 쿼리를 제공한다.</p>
<hr>
<h1 id="대량-카테시안-곱-쿼리">대량 카테시안 곱 쿼리</h1>
<pre><code class="language-sql">SELECT
    COUNT(*) AS TotalCount,
    SUM(CAST(ABS(CHECKSUM(NEWID())) AS FLOAT)) AS RandomSum
FROM SalesLT.SalesOrderDetail a
CROSS JOIN SalesLT.SalesOrderDetail b
CROSS JOIN SalesLT.SalesOrderDetail c
WHERE
    SQRT(POWER(a.UnitPrice, 2) + POWER(b.UnitPrice, 2)) &gt; 100
    AND a.OrderQty * b.OrderQty * c.OrderQty &gt; 0;</code></pre>
<hr>
<h2 id="왜-cpu-사용률이-높아질까">왜 CPU 사용률이 높아질까?</h2>
<p>이 쿼리는:</p>
<ul>
<li>CROSS JOIN</li>
<li>수학 연산</li>
<li>문자열/랜덤 함수</li>
</ul>
<p>를 동시에 수행한다.</p>
<p>특히:</p>
<pre><code class="language-text">N × N × N</code></pre>
<p>형태의 카테시안 곱이 발생하므로 CPU 부하가 매우 커진다.</p>
<hr>
<h1 id="10-cpu-지속-사용-루프">10. CPU 지속 사용 루프</h1>
<h2 id="루프-기반-cpu-사용">루프 기반 CPU 사용</h2>
<pre><code class="language-sql">DECLARE @StartTime DATETIME = GETDATE();
DECLARE @EndTime DATETIME = DATEADD(SECOND, 60, @StartTime);
DECLARE @Dummy FLOAT;

WHILE GETDATE() &lt; @EndTime
BEGIN
    SET @Dummy =
        SQRT(PI() * RAND()) * POWER(RAND(), 2);
END;</code></pre>
<hr>
<h2 id="특징">특징</h2>
<p>약 1분 동안 지속적으로 CPU를 사용한다.</p>
<p>특징:</p>
<ul>
<li>반복 수학 연산</li>
<li>랜덤 함수 호출</li>
<li>지속 루프 실행</li>
</ul>
<p>으로 인해 CPU 사용률을 강제로 상승시킨다.</p>
<hr>
<h1 id="11-실습-검증">11. 실습 검증</h1>
<p>다음 항목이 모두 성공하면 실습 완료이다.</p>
<ul>
<li>AdventureWorksLT &gt; Alerts에 새 규칙 생성</li>
<li>Action Group(emailgroup) 생성</li>
<li>Notification(DemoLab) 생성</li>
<li>이메일 수신 확인</li>
</ul>
<hr>
<h1 id="12-운영-관점에서-중요한-포인트">12. 운영 관점에서 중요한 포인트</h1>
<h2 id="evaluation-frequency">Evaluation Frequency</h2>
<p>경고 평가 주기이다.</p>
<p>예시:</p>
<ul>
<li>1분</li>
<li>5분</li>
</ul>
<p>주기를 사용할 수 있다.</p>
<p>짧을수록 민감하지만 비용과 false positive가 증가한다.</p>
<hr>
<h2 id="static-vs-dynamic-threshold">Static vs Dynamic Threshold</h2>
<h3 id="static">Static</h3>
<pre><code class="language-text">CPU &gt; 80%</code></pre>
<p>처럼 고정값 사용</p>
<hr>
<h3 id="dynamic">Dynamic</h3>
<p>Azure가 과거 패턴을 학습하여 자동 임계값을 계산한다.</p>
<p>트래픽 변동성이 큰 시스템에서는 Dynamic Threshold가 false positive를 줄여준다.</p>
<hr>
<h1 id="13-실무에서-자주-사용하는-패턴">13. 실무에서 자주 사용하는 패턴</h1>
<p>운영 환경에서는 보통:</p>
<table>
<thead>
<tr>
<th>Severity</th>
<th>용도</th>
</tr>
</thead>
<tbody><tr>
<td>Sev0</td>
<td>장애 수준</td>
</tr>
<tr>
<td>Sev1</td>
<td>긴급</td>
</tr>
<tr>
<td>Sev2</td>
<td>일반 경고</td>
</tr>
</tbody></table>
<p>형태로 Action Group을 분리해 관리한다.</p>
<p>예시:</p>
<pre><code class="language-text">cpu-sev0-email
cpu-sev1-teams
cpu-sev2-monitoring</code></pre>
<p>같은 형태로 표준화한다.</p>
<hr>
<h1 id="14-정리clean-up-1">14. 정리(Clean-up)</h1>
<p>실습 종료 후 비용 절약을 위해:</p>
<ul>
<li>Alert Rule 삭제</li>
<li>Action Group 삭제</li>
</ul>
<p>를 수행할 수 있다.</p>
<hr>
<h1 id="마무리-2">마무리</h1>
<p>이번 실습에서는 Azure SQL Database의 CPU 사용률을 기준으로 Azure Monitor Alert를 구성했다.</p>
<p>핵심 흐름은 다음과 같다.</p>
<pre><code class="language-text">CPU percentage 메트릭 선택
→ 임계값 설정
→ Action Group 생성
→ 이메일 알림 연결
→ 실제 CPU 부하 테스트</code></pre>
<p>Azure Monitor Alert는 단순 이메일 기능이 아니라:</p>
<ul>
<li>운영 자동화</li>
<li>장애 탐지</li>
<li>비용 감시</li>
<li>성능 이상 탐지</li>
</ul>
<p>등 Azure 운영 전반의 핵심 기능으로 활용된다.</p>
<hr>
<h1 id="azure-architecture-seminar-정리">Azure Architecture Seminar 정리</h1>
<blockquote>
<p>Azure 아키텍처를 단순 서비스 나열이 아니라 프레임워크 기반으로 설계하는 방법을 다룬 세미나 자료 정리
핵심 주제는 CAF(Cloud Adoption Framework), WAF(Well-Architected Framework), 그리고 Azure Reference Architecture이다.</p>
</blockquote>
<hr>
<h1 id="1-세미나-핵심-목표">1. 세미나 핵심 목표</h1>
<pre><code class="language-text">프레임워크 기반으로 아키텍처를 평가하고 설계하는 사고방식</code></pre>
<ol>
<li>WAF·CAF 기반으로 설계 평가</li>
<li>안티패턴 ↔ 대응패턴 구분</li>
<li>실제 Azure Reference Architecture 분석</li>
</ol>
<hr>
<h1 id="2-온프렘과-클라우드의-결정적-차이">2. 온프렘과 클라우드의 결정적 차이</h1>
<p>세미나에서는 클라우드 아키텍처 사고방식이 온프렘과 근본적으로 다르다고 설명한다.</p>
<hr>
<h2 id="1-장애를-가정하고-설계">1) 장애를 가정하고 설계</h2>
<h3 id="온프렘">온프렘</h3>
<pre><code class="language-text">장애가 안 나도록 비싸게 설계</code></pre>
<h3 id="azure">Azure</h3>
<pre><code class="language-text">장애는 반드시 발생한다고 가정</code></pre>
<p>따라서:</p>
<ul>
<li>Availability Zone</li>
<li>Region 분산</li>
<li>Backup</li>
<li>Site Recovery</li>
</ul>
<p>기반으로 설계한다.</p>
<hr>
<h2 id="2-비용은-운영-행위">2) 비용은 운영 행위</h2>
<p>온프렘은:</p>
<pre><code class="language-text">CapEx</code></pre>
<p>중심</p>
<p>클라우드는:</p>
<pre><code class="language-text">OpEx</code></pre>
<p>중심이다.</p>
<p>즉:</p>
<ul>
<li>어떤 SKU를 쓰는가</li>
<li>언제 끄는가</li>
<li>얼마나 자동화하는가</li>
</ul>
<p>자체가 비용 설계가 된다.</p>
<hr>
<h2 id="3-보안은-경계가-아니라-신원">3) 보안은 경계가 아니라 신원</h2>
<p>온프렘:</p>
<pre><code class="language-text">방화벽 중심</code></pre>
<p>Azure:</p>
<pre><code class="language-text">Zero Trust</code></pre>
<p>즉:</p>
<ul>
<li>누가</li>
<li>어떤 신원으로</li>
<li>어디서 접근했는가</li>
</ul>
<p>가 핵심 기준이 된다.</p>
<hr>
<h1 id="3-caf-vs-waf">3. CAF vs WAF</h1>
<p>이번 세미나의 핵심이다.</p>
<hr>
<h1 id="caf-cloud-adoption-framework">CAF (Cloud Adoption Framework)</h1>
<h2 id="조직-차원의-프레임워크">조직 차원의 프레임워크</h2>
<p>CAF는:</p>
<pre><code class="language-text">회사가 클라우드를 어떻게 도입하는가</code></pre>
<p>를 다룬다.</p>
<p>구성 단계:</p>
<ol>
<li>전략</li>
<li>계획</li>
<li>준비</li>
<li>도입</li>
<li>거버넌스</li>
<li>관리</li>
</ol>
<p>즉:</p>
<pre><code class="language-text">회사 전체의 클라우드 여정</code></pre>
<p>을 정의한다.</p>
<hr>
<h1 id="waf-well-architected-framework">WAF (Well-Architected Framework)</h1>
<h2 id="워크로드-차원의-프레임워크">워크로드 차원의 프레임워크</h2>
<p>WAF는:</p>
<pre><code class="language-text">이 시스템 하나를 어떻게 잘 만들 것인가</code></pre>
<p>를 평가한다.</p>
<p>즉:</p>
<ul>
<li>웹앱</li>
<li>데이터 파이프라인</li>
<li>AI 서비스</li>
</ul>
<p>각 워크로드마다 적용된다.</p>
<hr>
<h1 id="4-waf-51-기둥">4. WAF 5+1 기둥</h1>
<p>Azure WAF는 총 6개 관점으로 시스템을 평가한다.</p>
<table>
<thead>
<tr>
<th>기둥</th>
<th>핵심 질문</th>
</tr>
</thead>
<tbody><tr>
<td>Reliability</td>
<td>장애가 나도 돌아가는가</td>
</tr>
<tr>
<td>Security</td>
<td>신원·데이터를 어떻게 보호하는가</td>
</tr>
<tr>
<td>Cost Optimization</td>
<td>필요한 만큼만 쓰는가</td>
</tr>
<tr>
<td>Operational Excellence</td>
<td>변경·관측이 자동화되어 있는가</td>
</tr>
<tr>
<td>Performance Efficiency</td>
<td>부하 변화에 맞게 확장되는가</td>
</tr>
<tr>
<td>Sustainability</td>
<td>자원·탄소를 줄이고 있는가</td>
</tr>
</tbody></table>
<hr>
<h1 id="5-왜-프레임워크가-필요한가">5. 왜 프레임워크가 필요한가</h1>
<p>프레임워크 없이 설계하면:</p>
<ul>
<li>사람마다 기준이 다름</li>
<li>비용·보안·성능 트레이드오프 추적 불가</li>
<li>시간이 지나면 왜 그렇게 설계했는지 잊어버림</li>
</ul>
<p>반대로 프레임워크를 사용하면:</p>
<ul>
<li>공통 언어 생성</li>
<li>체크리스트 기반 리뷰 가능</li>
<li>트레이드오프 기록 가능</li>
<li>자기 진단 가능</li>
</ul>
<p>해진다.</p>
<hr>
<h1 id="6-waf-기둥별-핵심-패턴">6. WAF 기둥별 핵심 패턴</h1>
<hr>
<h1 id="reliability-신뢰성">Reliability (신뢰성)</h1>
<h2 id="안티패턴">안티패턴</h2>
<ul>
<li>단일 리전</li>
<li>단일 VM</li>
<li>복구 테스트 없음</li>
<li>단일 Load Balancer</li>
</ul>
<hr>
<h2 id="대응-패턴">대응 패턴</h2>
<ul>
<li>Availability Zone 분산</li>
<li>Geo-redundant</li>
<li>Backup + ASR</li>
<li>Front Door 멀티리전</li>
</ul>
<hr>
<h2 id="관련-azure-서비스">관련 Azure 서비스</h2>
<ul>
<li>Azure Backup</li>
<li>Site Recovery</li>
<li>Front Door</li>
<li>SQL Geo-replication</li>
</ul>
<hr>
<h1 id="security-보안">Security (보안)</h1>
<h2 id="안티패턴-1">안티패턴</h2>
<ul>
<li>IP 기반 신뢰</li>
<li>Secret 하드코딩</li>
<li>Owner 권한 남발</li>
</ul>
<hr>
<h2 id="대응-패턴-1">대응 패턴</h2>
<ul>
<li>Zero Trust</li>
<li>Managed Identity</li>
<li>Key Vault</li>
<li>RBAC 최소 권한</li>
<li>PIM</li>
</ul>
<hr>
<h2 id="관련-azure-서비스-1">관련 Azure 서비스</h2>
<ul>
<li>Entra ID</li>
<li>Managed Identity</li>
<li>Key Vault</li>
<li>Defender for Cloud</li>
<li>Private Endpoint</li>
</ul>
<hr>
<h1 id="cost-optimization-비용-최적화">Cost Optimization (비용 최적화)</h1>
<h2 id="안티패턴-2">안티패턴</h2>
<ul>
<li>모든 리소스 Pay-as-you-go</li>
<li>Dev/Test 24시간 켜둠</li>
<li>과도한 SKU</li>
</ul>
<hr>
<h2 id="대응-패턴-2">대응 패턴</h2>
<ul>
<li>Reservation</li>
<li>Savings Plan</li>
<li>Spot VM</li>
<li>Auto-shutdown</li>
<li>Cost Alert</li>
</ul>
<hr>
<h1 id="operational-excellence-운영-우수성">Operational Excellence (운영 우수성)</h1>
<h2 id="안티패턴-3">안티패턴</h2>
<ul>
<li>포털 클릭 기반 운영</li>
<li>수동 배포</li>
<li>로그 분산</li>
</ul>
<hr>
<h2 id="대응-패턴-3">대응 패턴</h2>
<ul>
<li>Terraform / Bicep IaC</li>
<li>GitHub Actions</li>
<li>Canary / Blue-Green 배포</li>
<li>Log Analytics 중앙화</li>
</ul>
<hr>
<h1 id="performance-efficiency-성능-효율성">Performance Efficiency (성능 효율성)</h1>
<h2 id="안티패턴-4">안티패턴</h2>
<ul>
<li>모든 요청이 DB로 직행</li>
<li>수직 확장만 사용</li>
<li>앱 서버가 정적 자산 직접 제공</li>
</ul>
<hr>
<h2 id="대응-패턴-4">대응 패턴</h2>
<ul>
<li>Redis Cache</li>
<li>Read Replica</li>
<li>Autoscale</li>
<li>CDN</li>
<li>Front Door</li>
</ul>
<hr>
<h1 id="sustainability-지속가능성">Sustainability (지속가능성)</h1>
<h2 id="안티패턴-5">안티패턴</h2>
<ul>
<li>탄소 효율 고려 없는 리전 선택</li>
<li>낮은 사용률 VM 유지</li>
<li>피크 시간대 배치</li>
</ul>
<hr>
<h2 id="대응-패턴-5">대응 패턴</h2>
<ul>
<li>Right-sizing</li>
<li>유휴 리소스 자동 정리</li>
<li>탄소 인지 스케줄링</li>
</ul>
<hr>
<h1 id="7-가장-중요한-개념--트레이드오프">7. 가장 중요한 개념 — 트레이드오프</h1>
<p>세미나에서 가장 강조하는 부분이다.</p>
<pre><code class="language-text">모든 기둥을 동시에 완벽하게 만족하는 아키텍처는 없다</code></pre>
<p>예시:</p>
<table>
<thead>
<tr>
<th>충돌</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td>신뢰성 ↔ 비용</td>
<td>멀티리전은 비쌈</td>
</tr>
<tr>
<td>보안 ↔ 성능</td>
<td>Private Endpoint는 홉 증가</td>
</tr>
<tr>
<td>성능 ↔ 비용</td>
<td>Premium SKU 비용 증가</td>
</tr>
<tr>
<td>운영 ↔ 성능</td>
<td>관측 시스템 자체 부하</td>
</tr>
</tbody></table>
<p>즉:</p>
<pre><code class="language-text">좋은 아키텍처란
어떤 기둥을 왜 양보했는지 설명할 수 있는 아키텍처</code></pre>
<p>이다.</p>
<hr>
<h1 id="8-레퍼런스-아키텍처-1--표준-웹-애플리케이션">8. 레퍼런스 아키텍처 1 — 표준 웹 애플리케이션</h1>
<h2 id="구조">구조</h2>
<pre><code class="language-text">Front Door
→ App Service
→ Azure SQL</code></pre>
<p>보조 구성:</p>
<ul>
<li>Key Vault</li>
<li>Managed Identity</li>
<li>App Insights</li>
<li>Blob + CDN</li>
</ul>
<hr>
<h2 id="핵심-의사결정">핵심 의사결정</h2>
<h3 id="app-service-vs-aks">App Service vs AKS</h3>
<ul>
<li>단순 웹앱 → App Service</li>
<li>복잡한 컨테이너 운영 → AKS</li>
</ul>
<hr>
<h3 id="active-active-vs-active-passive">Active-Active vs Active-Passive</h3>
<ul>
<li>낮은 RTO/RPO → Active-Active</li>
<li>비용 절감 → Active-Passive</li>
</ul>
<hr>
<h3 id="sql-vs-cosmos-db">SQL vs Cosmos DB</h3>
<ul>
<li>트랜잭션 중심 → Azure SQL</li>
<li>글로벌 분산 중심 → Cosmos DB</li>
</ul>
<hr>
<h1 id="9-레퍼런스-아키텍처-2--데이터-파이프라인">9. 레퍼런스 아키텍처 2 — 데이터 파이프라인</h1>
<h2 id="medallion-architecture">Medallion Architecture</h2>
<p>구조:</p>
<pre><code class="language-text">Bronze
→ Silver
→ Gold</code></pre>
<p>흐름:</p>
<pre><code class="language-text">Event Hubs
→ ADLS Gen2
→ Databricks
→ Synapse
→ Power BI</code></pre>
<hr>
<h2 id="핵심-개념">핵심 개념</h2>
<h3 id="bronze">Bronze</h3>
<p>원본 보존</p>
<h3 id="silver">Silver</h3>
<p>정제·검증</p>
<h3 id="gold">Gold</h3>
<p>비즈니스 집계</p>
<hr>
<h2 id="왜-medallion을-쓰는가">왜 Medallion을 쓰는가</h2>
<ul>
<li>원본 보존 가능</li>
<li>재처리 용이</li>
<li>데이터 품질 추적 가능</li>
<li>계층별 책임 분리</li>
</ul>
<hr>
<h1 id="10-레퍼런스-아키텍처-3--ai-추론--rag">10. 레퍼런스 아키텍처 3 — AI 추론 / RAG</h1>
<h2 id="구조-1">구조</h2>
<pre><code class="language-text">Container Apps
→ Azure OpenAI
→ AI Search
→ Cosmos DB</code></pre>
<p>보조 서비스:</p>
<ul>
<li>Front Door</li>
<li>Content Safety</li>
<li>Blob Storage</li>
<li>App Insights</li>
</ul>
<hr>
<h2 id="rag-흐름">RAG 흐름</h2>
<ol>
<li>질문 임베딩</li>
<li>AI Search 벡터 검색</li>
<li>관련 문서 검색</li>
<li>GPT 응답 생성</li>
<li>Content Safety 필터링</li>
<li>대화 저장</li>
</ol>
<hr>
<h1 id="11-ai-아키텍처-핵심-의사결정">11. AI 아키텍처 핵심 의사결정</h1>
<h2 id="ptu-vs-payg">PTU vs PAYG</h2>
<h3 id="ptu">PTU</h3>
<ul>
<li>일정한 트래픽</li>
<li>낮은 지연 요구</li>
</ul>
<h3 id="payg">PAYG</h3>
<ul>
<li>초기 구축</li>
<li>변동 트래픽</li>
</ul>
<hr>
<h2 id="container-apps-vs-aks">Container Apps vs AKS</h2>
<h3 id="container-apps">Container Apps</h3>
<ul>
<li>서버리스</li>
<li>빠른 자동 스케일</li>
</ul>
<h3 id="aks">AKS</h3>
<ul>
<li>복잡한 운영</li>
<li>세밀한 네트워크 제어</li>
</ul>
<hr>
<h2 id="ai-search-vs-cosmos-vs-postgresql">AI Search vs Cosmos vs PostgreSQL</h2>
<table>
<thead>
<tr>
<th>서비스</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>AI Search</td>
<td>하이브리드 검색</td>
</tr>
<tr>
<td>Cosmos DB</td>
<td>글로벌 분산</td>
</tr>
<tr>
<td>PostgreSQL + pgvector</td>
<td>비용 효율</td>
</tr>
</tbody></table>
<hr>
<h1 id="12-세-가지-아키텍처-비교">12. 세 가지 아키텍처 비교</h1>
<table>
<thead>
<tr>
<th>워크로드</th>
<th>주요 우선순위</th>
</tr>
</thead>
<tbody><tr>
<td>표준 웹앱</td>
<td>신뢰성·보안</td>
</tr>
<tr>
<td>데이터 파이프라인</td>
<td>비용·성능</td>
</tr>
<tr>
<td>AI 추론(RAG)</td>
<td>보안·운영</td>
</tr>
</tbody></table>
<hr>
<h2 id="가장-큰-위험-요소">가장 큰 위험 요소</h2>
<table>
<thead>
<tr>
<th>워크로드</th>
<th>위험</th>
</tr>
</thead>
<tbody><tr>
<td>웹앱</td>
<td>DR 미검증</td>
</tr>
<tr>
<td>데이터</td>
<td>데이터 품질</td>
</tr>
<tr>
<td>AI</td>
<td>프롬프트 주입·비용 폭주</td>
</tr>
</tbody></table>
<hr>
<h1 id="13-waf-자체-평가-체크리스트">13. WAF 자체 평가 체크리스트</h1>
<p>세미나 마지막에는 자신의 시스템을 WAF 기준으로 평가한다.</p>
<table>
<thead>
<tr>
<th>기둥</th>
<th>체크 항목</th>
</tr>
</thead>
<tbody><tr>
<td>신뢰성</td>
<td>DR / AZ / RPO / RTO</td>
</tr>
<tr>
<td>보안</td>
<td>Managed Identity / Key Vault</td>
</tr>
<tr>
<td>비용</td>
<td>Reservation / 자동 종료</td>
</tr>
<tr>
<td>운영</td>
<td>IaC / 중앙 로그</td>
</tr>
<tr>
<td>성능</td>
<td>캐시 / Autoscale</td>
</tr>
<tr>
<td>지속가능성</td>
<td>Right-sizing</td>
</tr>
</tbody></table>
<hr>
<h1 id="14-핵심-결론">14. 핵심 결론</h1>
<p>이번 세미나의 핵심 메시지는 다음 한 문장으로 정리된다.</p>
<pre><code class="language-text">좋은 아키텍처는
완벽한 아키텍처가 아니라,
어떤 기둥을 왜 양보했는지 설명할 수 있는 아키텍처</code></pre>
<p>그리고 WAF는:</p>
<pre><code class="language-text">서비스를 외우기 위한 프레임워크가 아니라,
좋은 질문을 하기 위한 프레임워크</code></pre>
<p>이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MicrosoftDataSchool] 86일차 - AzureVM에 Spark(2)]]></title>
            <link>https://velog.io/@rudin_/MicrosoftDataSchool-86%EC%9D%BC%EC%B0%A8-AzureVM%EC%97%90-Spark2</link>
            <guid>https://velog.io/@rudin_/MicrosoftDataSchool-86%EC%9D%BC%EC%B0%A8-AzureVM%EC%97%90-Spark2</guid>
            <pubDate>Tue, 12 May 2026 06:21:33 GMT</pubDate>
            <description><![CDATA[<h1 id="실시간-데이터-파이프라인--kafka--postgresql--spark-structured-streaming--배치-ml-추론">실시간 데이터 파이프라인 — Kafka + PostgreSQL + Spark Structured Streaming + 배치 ML 추론</h1>
<h2 id="2-postgresql-16-설치-및-초기화">2. PostgreSQL 16 설치 및 초기화</h2>
<blockquote>
<ul>
<li>Ubuntu 24.04 기본 저장소에서 PostgreSQL 16을 apt로 설치하고 systemd로 관리한다.</li>
</ul>
</blockquote>
<ul>
<li>운영용 사용자(<code>handson</code>)와 데이터베이스(<code>onestore</code>)를 생성하고, <code>.pgpass</code>로 비밀번호 입력을 자동화한다.</li>
<li>보안 기본값을 적용한다 (외부 네트워크 차단, scram-sha-256 인증).</li>
<li>B2ms 8GB 환경에 맞는 메모리 튜닝을 적용한다.</li>
</ul>
<table>
<thead>
<tr>
<th>용어</th>
<th>정의</th>
</tr>
</thead>
<tbody><tr>
<td><strong>PostgreSQL Cluster</strong></td>
<td>한 서버 인스턴스 안의 모든 DB·role을 묶는 단위. 보통 1 서버 = 1 cluster.</td>
</tr>
<tr>
<td><strong>Role</strong></td>
<td>사용자 + 그룹 통합 개념. <code>CREATE USER</code>는 사실 role 생성.</td>
</tr>
<tr>
<td><strong>peer 인증</strong></td>
<td>Unix 소켓 접속 시 Linux 사용자명과 PG role이 일치하면 비번 없이 통과.</td>
</tr>
<tr>
<td><strong>scram-sha-256</strong></td>
<td>PG14+ 기본 비번 인증 방식. TCP 접속(127.0.0.1 포함)에 적용.</td>
</tr>
<tr>
<td><strong>pg_hba.conf</strong></td>
<td>&quot;누가, 어디서, 어떻게 접속 가능한가&quot;를 정의하는 인증 규칙 파일.</td>
</tr>
<tr>
<td><strong>postgresql.conf</strong></td>
<td>서버 동작 설정 (메모리·로그·복제 등).</td>
</tr>
<tr>
<td><strong>shared_buffers</strong></td>
<td>PG가 페이지 캐시처럼 쓰는 공유 메모리. B2ms에선 PG 할당분(1.2GB)의 절반 수준으로.</td>
</tr>
<tr>
<td><strong>work_mem</strong></td>
<td>정렬·해시 조인 한 단위가 쓸 메모리. 너무 크면 OOM.</td>
</tr>
<tr>
<td><strong>.pgpass</strong></td>
<td>홈 디렉터리의 권한 600 파일에 비번을 저장해 매번 입력 안 하게 함.</td>
</tr>
</tbody></table>
<h4 id="개념-설명-cluster--database--role">개념 설명: cluster / database / role</h4>
<p>┌──────────────────────────────────────────────────────────┐
│  Linux user &quot;azureuser&quot; (나)                              │
│                                                          │
│   ① Unix 소켓 접속:  sudo -u postgres psql               │
│      → peer 인증 (Linux user == PG role 매칭)            │
│      → 슈퍼유저 postgres로 진입, 관리용                   │
│                                                          │
│   ② TCP 접속 127.0.0.1:5432:                            │
│      psql -h 127.0.0.1 -U handson -d onestore           │
│      → scram-sha-256 (비밀번호)                          │
│      → 운영 DB 작업용                                     │
└──────────────────────────────────────────────────────────┘</p>
<h4 id="개념-설명-메모리-튜닝-룰-오브-썸">개념 설명: 메모리 튜닝 룰 오브 썸</h4>
<table>
<thead>
<tr>
<th>파라미터</th>
<th>의미</th>
<th>B2ms 설정값</th>
<th>근거</th>
</tr>
</thead>
<tbody><tr>
<td><code>shared_buffers</code></td>
<td>PG 공유 메모리 캐시</td>
<td><code>768MB</code></td>
<td>PG 할당 1.2GB의 약 60%</td>
</tr>
<tr>
<td><code>work_mem</code></td>
<td>쿼리당 정렬·해시 메모리</td>
<td><code>16MB</code></td>
<td>동시 쿼리 5개 × 16MB = 80MB로 안전</td>
</tr>
<tr>
<td><code>maintenance_work_mem</code></td>
<td>VACUUM·CREATE INDEX용</td>
<td><code>128MB</code></td>
<td>학습 환경 충분</td>
</tr>
<tr>
<td><code>effective_cache_size</code></td>
<td>OS 페이지 캐시 추정치 (플래너 힌트)</td>
<td><code>2GB</code></td>
<td>실제 메모리 점유 X</td>
</tr>
<tr>
<td><code>max_connections</code></td>
<td>동시 연결 한도</td>
<td><code>20</code></td>
<td>학습 환경. 한 연결당 ~10MB 점유</td>
</tr>
</tbody></table>
<h3 id="postgresql-16-설치">PostgreSQL 16 설치</h3>
<pre><code>sudo apt update
sudo apt install -y postgresql-16 postgresql-client-16 postgresql-contrib-16

psql --version
sudo systemctl status postgresql --no-pager</code></pre><h3 id="슈퍼유저-진입-및-운영-사용자db-생성">슈퍼유저 진입 및 운영 사용자/DB 생성</h3>
<pre><code>-- 운영 사용자 생성 (비밀번호는 학습용. 운영 시엔 강력한 값으로)
CREATE ROLE handson WITH LOGIN PASSWORD &#39;비밀번호&#39;;

-- 운영 DB 생성 (소유자 handson)
CREATE DATABASE onestore OWNER handson ENCODING &#39;UTF8&#39;;

-- 추가 권한 (DB 안 모든 객체에 대한 풀권한)
GRANT ALL PRIVILEGES ON DATABASE onestore TO handson;

\l onestore
\du handson
\q</code></pre><h3 id="tcp-접속-테스트">TCP 접속 테스트</h3>
<pre><code>psql -h 127.0.0.1 -U handson -d onestore -c &quot;SELECT current_user, current_database(), version();&quot;
# 비밀번호 프롬프트 → handson_pw_2026</code></pre><h3 id="pgpass로-비밀번호-자동화">.pgpass로 비밀번호 자동화</h3>
<pre><code>cat &gt; ~/.pgpass &lt;&lt; &#39;EOF&#39;
127.0.0.1:5432:onestore:handson:비밀번호
127.0.0.1:5432:*:handson:비밀번호
EOF

chmod 600 ~/.pgpass
ls -l ~/.pgpass</code></pre><pre><code># 비번 입력 없이 접속되는지 확인
psql -h 127.0.0.1 -U handson -d onestore -c &quot;SELECT &#39;pgpass works&#39; AS status;&quot;</code></pre><h3 id="환경변수로-접속-정보-설정">환경변수로 접속 정보 설정</h3>
<pre><code>grep -q &quot;PGUSER=handson&quot; ~/.bashrc || cat &gt;&gt; ~/.bashrc &lt;&lt; &#39;EOF&#39;

# === PostgreSQL 접속 (Part 3) ===
export PGHOST=127.0.0.1
export PGPORT=5432
export PGDATABASE=onestore
export PGUSER=handson
# 비밀번호는 .pgpass가 처리 (PGPASSWORD 환경변수 사용 X — ps에 노출됨)
EOF

source ~/.bashrc

# 환경변수 만으로 psql 접속되는지 확인
psql -c &quot;SELECT current_user, current_database();&quot;</code></pre><h3 id="메모리-튜닝postgresqlconf">메모리 튜닝(postgresql.conf)</h3>
<pre><code># 백업 (필수)
sudo cp /etc/postgresql/16/main/postgresql.conf \
        /etc/postgresql/16/main/postgresql.conf.original

# 사용자 정의 설정을 별도 파일로
sudo tee /etc/postgresql/16/main/conf.d/99-handson.conf &gt; /dev/null &lt;&lt; &#39;EOF&#39;
# === Handson Part 3 메모리 튜닝 (B2ms 8GB) ===
# PG 할당 예산 ~1.2GB

shared_buffers = 768MB
work_mem = 16MB
maintenance_work_mem = 128MB
effective_cache_size = 2GB
max_connections = 20

# WAL / 체크포인트 (학습 환경 안정성)
wal_buffers = 16MB
checkpoint_completion_target = 0.9
min_wal_size = 80MB
max_wal_size = 1GB

# SSD 가정 (Azure VM은 SSD)
random_page_cost = 1.1

# 로그 (트러블슈팅 편의)
log_min_duration_statement = 1000   # 1초 이상 쿼리 로깅
log_line_prefix = &#39;%t [%p]: user=%u,db=%d,app=%a &#39;
EOF

# include_dir 확인
grep -A1 &quot;^include_dir&quot; /etc/postgresql/16/main/postgresql.conf

# 서비스 재시작
sudo systemctl restart postgresql@16-main

# 적용 확인
psql -c &quot;SHOW shared_buffers; SHOW work_mem; SHOW max_connections;&quot;</code></pre><h3 id="psql-기본-사용법">psql 기본 사용법</h3>
<pre><code>\?              -- psql 명령어 도움말 전체
\h SELECT       -- SELECT SQL 도움말
\l              -- 데이터베이스 목록
\dt             -- 현재 DB의 테이블 목록 (Section 3 후 채워짐)
\d 테이블명      -- 테이블 스키마 상세
\du             -- role 목록
\timing         -- 쿼리 실행 시간 표시 토글
\x              -- 결과 가로/세로 출력 토글
\!              -- 셸 명령 실행 (예: \! ls)
\i 파일.sql      -- SQL 파일 실행
\q              -- 종료</code></pre><h2 id="3-스키마-설계-및-테이블-생성">3. 스키마 설계 및 테이블 생성</h2>
<blockquote>
<ul>
<li>데이터 모델링 결과를 SQL 스크립트로 작성하고 <code>psql -f</code>로 일괄 실행한다.</li>
</ul>
</blockquote>
<ul>
<li>PRIMARY KEY / FOREIGN KEY / CHECK / INDEX의 의미와 선택 근거를 이해한다.</li>
<li><code>\d 테이블명</code>으로 스키마를 검증한다.</li>
</ul>
<table>
<thead>
<tr>
<th>용어</th>
<th>정의</th>
</tr>
</thead>
<tbody><tr>
<td><strong>dim / fact</strong></td>
<td>데이터 웨어하우스의 별 모양 스키마 명명. dim = 차원, fact = 사실.</td>
</tr>
<tr>
<td><strong>PRIMARY KEY</strong></td>
<td>행을 유일하게 식별. <code>NOT NULL + UNIQUE</code> 자동 부여.</td>
</tr>
<tr>
<td><strong>FOREIGN KEY</strong></td>
<td>다른 테이블의 PK를 참조. 무결성 보장.</td>
</tr>
<tr>
<td><strong>CHECK 제약</strong></td>
<td>행 단위 조건식. 위반 시 INSERT/UPDATE 거부.</td>
</tr>
<tr>
<td><strong>NUMERIC(p, s)</strong></td>
<td>정확한 십진수. 금액 표현엔 FLOAT 대신 NUMERIC.</td>
</tr>
<tr>
<td><strong>INDEX</strong></td>
<td>검색 가속용 보조 자료구조. INSERT 비용 ↑ / SELECT 비용 ↓.</td>
</tr>
</tbody></table>
<h3 id="float이-아닌-numeric을-쓰는-이유">Float이 아닌 Numeric을 쓰는 이유</h3>
<pre><code>-- FLOAT 함정
SELECT 0.1::float8 + 0.2::float8;
-- 결과: 0.30000000000000004 (반올림 오차)

-- NUMERIC은 정확
SELECT 0.1::numeric + 0.2::numeric;
-- 결과: 0.3</code></pre><h4 id="개념-설명-인덱스-선택-근거">개념 설명: 인덱스 선택 근거</h4>
<p>쿼리 패턴:</p>
<pre><code class="language-sql">-- 1) 특정 고객의 거래 조회
SELECT * FROM transactions_fact WHERE customer_id = &#39;C001234&#39;;
-- 2) 최근 N분간 전체 거래
SELECT * FROM transactions_fact WHERE ts &gt; NOW() - INTERVAL &#39;5 min&#39;;
-- 3) 고객별 거래 수 집계
SELECT customer_id, COUNT(*) FROM transactions_fact GROUP BY customer_id;</code></pre>
<ul>
<li><code>customer_id</code>에 인덱스: 1번·3번 가속</li>
<li><code>ts</code>에 인덱스: 2번 가속 (시계열 슬라이싱)</li>
<li><code>transaction_id</code>는 PK라 자동 인덱스</li>
</ul>
<h3 id="ddl">DDL</h3>
<pre><code>cat &gt; ~/spark-handson/sql/init.sql &lt;&lt; &#39;EOF&#39;
-- =====================================================================
-- OneStore Hands-on Part 3 — 초기 스키마
-- 실행: psql -f ~/spark-handson/sql/init.sql
-- 멱등: IF NOT EXISTS 절 사용. 재실행해도 안전.
-- =====================================================================

\set ON_ERROR_STOP on

BEGIN;

-- ----------------------------
-- 1) customers_dim — Part 2 정제본 적재 대상
-- ----------------------------
CREATE TABLE IF NOT EXISTS customers_dim (
    customer_id  VARCHAR(20) PRIMARY KEY,
    age          INT,
    gender       VARCHAR(10),
    country      VARCHAR(10),
    plan_type    VARCHAR(20),
    signup_date  DATE,
    loaded_at    TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

COMMENT ON TABLE customers_dim IS &#39;Part 2 customers_clean 정제본 적재 (배치 1회)&#39;;

-- ----------------------------
-- 2) transactions_fact — 스트리밍 적재 대상
-- ----------------------------
CREATE TABLE IF NOT EXISTS transactions_fact (
    transaction_id  VARCHAR(30) PRIMARY KEY,
    customer_id     VARCHAR(20) NOT NULL
                    REFERENCES customers_dim(customer_id),
    ts              TIMESTAMP NOT NULL,
    amount          NUMERIC(10, 2) CHECK (amount &gt;= 0),
    category        VARCHAR(20),
    status          VARCHAR(20)
                    CHECK (status IN (&#39;COMPLETED&#39;,&#39;PENDING&#39;,&#39;FAILED&#39;,&#39;REFUNDED&#39;)),
    ingested_at     TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_tx_customer ON transactions_fact(customer_id);
CREATE INDEX IF NOT EXISTS idx_tx_ts       ON transactions_fact(ts);

COMMENT ON TABLE transactions_fact IS &#39;Spark Structured Streaming이 Kafka에서 적재&#39;;

-- ----------------------------
-- 3) ml_predictions — 배치 추론 결과
-- ----------------------------
CREATE TABLE IF NOT EXISTS ml_predictions (
    customer_id     VARCHAR(20) PRIMARY KEY
                    REFERENCES customers_dim(customer_id),
    predicted_at    TIMESTAMP NOT NULL,
    churn_proba     NUMERIC(5, 4) CHECK (churn_proba BETWEEN 0 AND 1),
    churn_label     INT CHECK (churn_label IN (0, 1)),
    model_version   VARCHAR(50)
);

CREATE INDEX IF NOT EXISTS idx_pred_at ON ml_predictions(predicted_at);

COMMENT ON TABLE ml_predictions IS &#39;cron 배치 추론 결과 (UPSERT, 고객당 1행)&#39;;

COMMIT;

-- 확인용
\echo &#39;&#39;
\echo &#39;=== 생성된 테이블 ===&#39;
\dt
EOF

ls -l ~/spark-handson/sql/init.sql</code></pre><pre><code>#실행
psql -f ~/spark-handson/sql/init.sql</code></pre><h2 id="pyspark에서-postgresql-연결-jdbc">PySpark에서 PostgreSQL 연결 (JDBC)</h2>
<blockquote>
<ul>
<li>PostgreSQL JDBC 드라이버를 PySpark에 통합 (<code>--packages</code> 자동 다운로드 / 수동 jar).</li>
</ul>
</blockquote>
<ul>
<li>안전한 비밀번호 처리 패턴(<code>.pgpass</code> 파싱)을 적용.</li>
<li>Part 2의 <code>customers_clean</code> Parquet을 <code>customers_dim</code> 테이블에 적재.</li>
<li>PySpark에서 PostgreSQL 데이터를 다시 읽어 검증하는 양방향 패턴.</li>
</ul>
<table>
<thead>
<tr>
<th>용어</th>
<th>정의</th>
</tr>
</thead>
<tbody><tr>
<td><strong>JDBC</strong></td>
<td>Java Database Connectivity. JVM이 RDBMS와 통신하는 표준 API. PySpark는 내부 JVM이 사용.</td>
</tr>
<tr>
<td><strong>JDBC URL</strong></td>
<td>DB 위치 + 옵션. 예: <code>jdbc:postgresql://127.0.0.1:5432/onestore</code></td>
</tr>
<tr>
<td><strong>Maven Coordinates</strong></td>
<td><code>groupId:artifactId:version</code>. PostgreSQL JDBC는 <code>org.postgresql:postgresql:42.7.4</code>.</td>
</tr>
<tr>
<td><strong><code>--packages</code></strong></td>
<td>Maven Central에서 자동 다운로드 + <code>~/.ivy2/cache</code> 저장.</td>
</tr>
<tr>
<td><strong><code>--jars</code></strong></td>
<td>받아둔 jar 파일 경로 직접 지정. 오프라인용.</td>
</tr>
<tr>
<td><strong>Write Mode</strong></td>
<td><code>append</code> / <code>overwrite</code> / <code>error</code>(기본) / <code>ignore</code>. RDBMS 적재엔 <code>append</code> + 멱등성 코드.</td>
</tr>
<tr>
<td><strong><code>batchsize</code></strong></td>
<td>JDBC INSERT 한 번에 묶는 행 수 (기본 1000).</td>
</tr>
</tbody></table>
<h4 id="--packages-vs---jars">--packages vs --jars</h4>
<pre><code>┌─────────────────────────────────────────────────────────────┐
│  ① --packages org.postgresql:postgresql:42.7.4              │
│     → Maven Central → ~/.ivy2/cache → 자동 클래스패스        │
│   (장점: 의존성 transitively 해결, 첫 실행 후 캐시됨)          │
│   (단점: 외부 네트워크 필요, 첫 실행 1~3분)                   │
│                                                             │
│  ② --jars /path/to/postgresql-42.7.4.jar                    │
│   (장점: 오프라인 가능, 즉시 시작)                            │
│   (단점: 의존성 수동 관리)                                    │
└─────────────────────────────────────────────────────────────┘</code></pre><h4 id="개념-설명-spark-jdbc-적재-동작">개념 설명: Spark JDBC 적재 동작</h4>
<pre><code>PySpark DataFrame (10,000 rows)
        │
        │ .write.format(&quot;jdbc&quot;).mode(&quot;append&quot;)
        │   .option(&quot;batchsize&quot;, 1000)
        ▼
JDBC Driver (postgresql-42.7.4.jar)
        │ INSERT INTO customers_dim VALUES (...) -- 1000행 묶음 × 10
        ▼
PostgreSQL</code></pre><blockquote>
<p><strong>왜 <code>mode(&quot;overwrite&quot;)</code>가 위험한가?</strong>
Spark JDBC <code>overwrite</code>는 기본 <code>DROP TABLE + CREATE TABLE</code>. 우리 customers_dim엔 PK·CHECK·FK 메타데이터가 붙어 DROP하면 다 사라진다. <strong>그래서 <code>append</code> + 멱등성 패턴</strong>.</p>
</blockquote>
<h3 id="jdbc-드라이버-사전-캐싱">JDBC 드라이버 사전 캐싱</h3>
<pre><code>cat &gt; /tmp/cache_jdbc.py &lt;&lt; &#39;PYEOF&#39;
from pyspark.sql import SparkSession
spark = SparkSession.builder.appName(&quot;cache_jdbc&quot;).getOrCreate()
print(&quot;PostgreSQL JDBC 드라이버 다운로드 완료&quot;)
spark.stop()
PYEOF

spark-submit \
  --packages org.postgresql:postgresql:42.7.4 \
  --conf spark.driver.memory=1g \
  /tmp/cache_jdbc.py 2&gt;&amp;1 | grep -E &quot;(SUCCESSFUL|downloaded|완료|ERROR)&quot;

# 캐시 확인
ls -la ~/.ivy2/jars/ | grep postgresql</code></pre><h3 id="적재">적재</h3>
<pre><code>cat &gt; ~/spark-handson/jobs/load_customers_dim.py &lt;&lt; &#39;PYEOF&#39;
#!/usr/bin/env python3
&quot;&quot;&quot;
load_customers_dim.py
Part 2의 customers_clean Parquet을 PostgreSQL customers_dim 테이블에 적재.
&quot;&quot;&quot;
import os
import sys
from pathlib import Path

from pyspark.sql import SparkSession
from pyspark.sql.functions import col


def get_pgpass(host: str, port: str, db: str, user: str) -&gt; str:
    pgpass = Path.home() / &quot;.pgpass&quot;
    if not pgpass.exists():
        sys.exit(&quot;ERROR: ~/.pgpass not found. See Section 2 Step 2-4.&quot;)
    if oct(pgpass.stat().st_mode)[-3:] != &quot;600&quot;:
        sys.exit(&quot;ERROR: ~/.pgpass permission must be 600.&quot;)
    for line in pgpass.read_text().splitlines():
        if not line or line.startswith(&quot;#&quot;):
            continue
        parts = line.split(&quot;:&quot;)
        if len(parts) != 5:
            continue
        h, p, d, u, pw = parts
        if h in (host, &quot;*&quot;) and p in (port, &quot;*&quot;) \
           and d in (db, &quot;*&quot;) and u in (user, &quot;*&quot;):
            return pw
    sys.exit(f&quot;ERROR: no matching .pgpass entry for {user}@{host}:{port}/{db}&quot;)


PG_HOST = os.environ.get(&quot;PGHOST&quot;, &quot;127.0.0.1&quot;)
PG_PORT = os.environ.get(&quot;PGPORT&quot;, &quot;5432&quot;)
PG_DB   = os.environ.get(&quot;PGDATABASE&quot;, &quot;onestore&quot;)
PG_USER = os.environ.get(&quot;PGUSER&quot;, &quot;handson&quot;)
PG_PW   = get_pgpass(PG_HOST, PG_PORT, PG_DB, PG_USER)

JDBC_URL = f&quot;jdbc:postgresql://{PG_HOST}:{PG_PORT}/{PG_DB}&quot;
JDBC_PROPS = {
    &quot;user&quot;: PG_USER,
    &quot;password&quot;: PG_PW,
    &quot;driver&quot;: &quot;org.postgresql.Driver&quot;,
}

CUSTOMERS_PARQUET = os.path.expanduser(&quot;~/spark-handson/data/customers_clean&quot;)


def main():
    spark = (
        SparkSession.builder
        .appName(&quot;load_customers_dim&quot;)
        .getOrCreate()
    )
    spark.sparkContext.setLogLevel(&quot;ERROR&quot;)

    print(f&quot;[INFO] reading parquet: {CUSTOMERS_PARQUET}&quot;)
    df = spark.read.parquet(CUSTOMERS_PARQUET)

    df_to_load = df.select(
        col(&quot;customer_id&quot;),
        col(&quot;age&quot;).cast(&quot;int&quot;),
        col(&quot;gender&quot;),
        col(&quot;country&quot;),
        col(&quot;plan_type&quot;),
        col(&quot;signup_date&quot;).cast(&quot;date&quot;),
    )

    src_count = df_to_load.count()
    print(f&quot;[INFO] source rows: {src_count}&quot;)

    existing = (
        spark.read.jdbc(url=JDBC_URL, table=&quot;customers_dim&quot;,
                        properties=JDBC_PROPS).count()
    )
    print(f&quot;[INFO] customers_dim existing rows: {existing}&quot;)

    if existing &gt; 0:
        print(&quot;[WARN] customers_dim is not empty. Skipping load.&quot;)
        print(&quot;       To reload, run reset SQL first:&quot;)
        print(&quot;       psql -c &#39;TRUNCATE customers_dim, transactions_fact, ml_predictions CASCADE;&#39;&quot;)
        spark.stop()
        return

    print(&quot;[INFO] loading into customers_dim ...&quot;)
    (
        df_to_load.write
        .format(&quot;jdbc&quot;)
        .option(&quot;url&quot;, JDBC_URL)
        .option(&quot;dbtable&quot;, &quot;customers_dim&quot;)
        .option(&quot;user&quot;, PG_USER)
        .option(&quot;password&quot;, PG_PW)
        .option(&quot;driver&quot;, &quot;org.postgresql.Driver&quot;)
        .option(&quot;batchsize&quot;, 1000)
        .mode(&quot;append&quot;)
        .save()
    )

    loaded = (
        spark.read.jdbc(url=JDBC_URL, table=&quot;customers_dim&quot;,
                        properties=JDBC_PROPS).count()
    )
    print(f&quot;[OK] loaded rows: {loaded} (expected: {src_count})&quot;)

    spark.stop()


if __name__ == &quot;__main__&quot;:
    main()
PYEOF

chmod +x ~/spark-handson/jobs/load_customers_dim.py
ls -l ~/spark-handson/jobs/load_customers_dim.py</code></pre><h3 id="실행">실행</h3>
<pre><code>spark-submit \
  --packages org.postgresql:postgresql:42.7.4 \
  --conf spark.driver.memory=2g \
  ~/spark-handson/jobs/load_customers_dim.py \
  2&gt;&amp;1 | tee ~/spark-handson/logs/load_customers_dim.log | grep -E &quot;INFO|WARN|OK|ERROR&quot;</code></pre><h3 id="선택-재적재">(선택) 재적재</h3>
<pre><code># 1) 자식 테이블 + customers_dim 모두 비우기 (CASCADE)
psql -c &quot;TRUNCATE customers_dim, transactions_fact, ml_predictions CASCADE;&quot;

# 2) 다시 적재
spark-submit \
  --packages org.postgresql:postgresql:42.7.4 \
  ~/spark-handson/jobs/load_customers_dim.py</code></pre><h2 id="kafka-37-단일-노드-설치kraft-모드">Kafka 3.7 단일 노드 설치(KRaft 모드)</h2>
<blockquote>
<ul>
<li>Apache Kafka 3.7을 tarball로 설치하고 <code>/opt/kafka</code>에 배치.</li>
</ul>
</blockquote>
<ul>
<li>KRaft(Kafka Raft) 모드로 클러스터 초기화 (ZooKeeper 미사용).</li>
<li>B2ms 8GB 환경에 맞게 JVM 힙을 1GB로 제한.</li>
<li>systemd unit으로 등록.</li>
<li><code>transactions-raw</code> 토픽 생성 + 콘솔 producer/consumer 검증.</li>
</ul>
<table>
<thead>
<tr>
<th>용어</th>
<th>정의</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Broker</strong></td>
<td>메시지를 저장·전송하는 Kafka 서버 노드.</td>
</tr>
<tr>
<td><strong>Controller</strong></td>
<td>클러스터 메타데이터(토픽·파티션·리더 선출) 관리. KRaft에선 broker와 통합 가능.</td>
</tr>
<tr>
<td><strong>Topic</strong></td>
<td>메시지가 발행되는 논리적 스트림 이름.</td>
</tr>
<tr>
<td><strong>Partition</strong></td>
<td>토픽을 물리적으로 쪼갠 단위. 파티션 수 = 병렬 consume 가능 수.</td>
</tr>
<tr>
<td><strong>Offset</strong></td>
<td>한 파티션 안에서 메시지의 순차 ID.</td>
</tr>
<tr>
<td><strong>Consumer Group</strong></td>
<td>같은 그룹 consumer들이 토픽 파티션을 분담. Spark Streaming은 자체 그룹.</td>
</tr>
<tr>
<td><strong>KRaft</strong></td>
<td>&quot;Kafka Raft&quot; 메타데이터 합의 프로토콜. ZooKeeper 대체. Kafka 3.3+ production-ready.</td>
</tr>
<tr>
<td><strong><code>process.roles</code></strong></td>
<td>한 프로세스가 어떤 역할인지. 단일 노드는 <code>broker,controller</code> 둘 다.</td>
</tr>
<tr>
<td><strong><code>log.dirs</code></strong></td>
<td>Kafka가 메시지를 디스크에 저장하는 디렉터리.</td>
</tr>
<tr>
<td><strong><code>retention.ms</code></strong></td>
<td>메시지 디스크 유지 시간. 우리는 1시간(3,600,000ms).</td>
</tr>
</tbody></table>
<h4 id="kraft-vs-zookeeper">KRaft vs ZooKeeper</h4>
<pre><code>┌─── 기존 (ZooKeeper 모드) ───┐    ┌─── KRaft 모드 ───────────┐
│   ┌─────────────┐            │    │   ┌────────────────────┐  │
│   │ ZooKeeper   │  메타데이터  │    │   │ Kafka 프로세스      │  │
│   │ (별도 클러스터) │            │    │   │  ├─ broker        │  │
│   └──────┬──────┘            │    │   │  └─ controller     │  │
│          │                   │    │   │  (메타데이터 = Raft) │  │
│   ┌──────▼──────┐            │    │   └────────────────────┘  │
│   │ Kafka brokers│            │    │                           │
│   └─────────────┘            │    │   프로세스 1개로 끝         │
│   프로세스 2종류 운영 부담       │    │   메모리 절약 + 단순성      │
└──────────────────────────────┘    └───────────────────────────┘</code></pre><blockquote>
<p><strong>B2ms 8GB에 KRaft가 적합한 이유</strong>: ZooKeeper 모드는 별도 JVM(~512MB) 추가. KRaft는 통합 → 메모리·복잡도 절약.</p>
</blockquote>
<h4 id="topicpartitionoffset">Topic/Partition/Offset</h4>
<pre><code>Topic: transactions-raw   (partitions=3)
┌───────────────────────────────────────────────────────────┐
│ Partition 0:  [m0][m1][m2][m3][m4][m5]...                 │
│ Partition 1:  [m0][m1][m2]...                              │
│ Partition 2:  [m0][m1][m2][m3]...                          │
└───────────────────────────────────────────────────────────┘

Producer가 메시지 발행:
  - key 없음 → 라운드로빈/스티키
  - key 있음 → hash(key) % partitions로 같은 key는 같은 파티션 (순서 보장)

Consumer가 메시지 소비:
  - 같은 Consumer Group 안의 consumer들이 파티션 분담
  - Spark Streaming도 내부적으로 Consumer Group 사용</code></pre><h4 id="kraft-단일-노드의-listeners">KRaft 단일 노드의 listeners</h4>
<table>
<thead>
<tr>
<th>listener</th>
<th>포트</th>
<th>용도</th>
</tr>
</thead>
<tbody><tr>
<td><code>PLAINTEXT</code></td>
<td>9092</td>
<td>클라이언트(Producer·Consumer) ↔ broker</td>
</tr>
<tr>
<td><code>CONTROLLER</code></td>
<td>9093</td>
<td>controller ↔ controller (메타데이터 합의용 Raft)</td>
</tr>
</tbody></table>
<p>단일 노드라도 controller listener 필수.</p>
<h4 id="메모리-예산">메모리 예산</h4>
<table>
<thead>
<tr>
<th>컴포넌트</th>
<th>설정값</th>
</tr>
</thead>
<tbody><tr>
<td>JVM Heap (<code>-Xmx1g</code>)</td>
<td>1GB 고정</td>
</tr>
<tr>
<td>Page Cache</td>
<td>동적 (남은 메모리)</td>
</tr>
<tr>
<td>Direct Memory</td>
<td>~256MB 추가</td>
</tr>
</tbody></table>
<p>Kafka는 <strong>JVM 힙을 작게 유지하고 OS page cache에 의존</strong>. <code>-Xmx1g</code>로도 충분.</p>
<h3 id="tarball-다운로드">tarball 다운로드</h3>
<pre><code>mkdir -p ~/dl &amp;&amp; cd ~/dl

KAFKA_VER=3.7.0
KAFKA_FILE=kafka_2.13-${KAFKA_VER}.tgz
KAFKA_URL_PRIMARY=&quot;https://archive.apache.org/dist/kafka/${KAFKA_VER}/${KAFKA_FILE}&quot;
KAFKA_URL_FALLBACK=&quot;https://dlcdn.apache.org/kafka/${KAFKA_VER}/${KAFKA_FILE}&quot;

curl -fLO &quot;$KAFKA_URL_PRIMARY&quot; || curl -fLO &quot;$KAFKA_URL_FALLBACK&quot;

ls -lh ${KAFKA_FILE}
file ${KAFKA_FILE}</code></pre><h3 id="압축-해제--optkafka-배치">압축 해제 + /opt/kafka 배치</h3>
<pre><code>cd ~/dl
sudo tar -xzf kafka_2.13-3.7.0.tgz -C /opt/
sudo mv /opt/kafka_2.13-3.7.0 /opt/kafka-3.7.0
sudo ln -sfn /opt/kafka-3.7.0 /opt/kafka
ls -la /opt/ | grep -E &quot;kafka|-&gt;&quot;</code></pre><h3 id="환경변수-등록">환경변수 등록</h3>
<pre><code>grep -q &quot;KAFKA_HOME&quot; ~/.bashrc || cat &gt;&gt; ~/.bashrc &lt;&lt; &#39;EOF&#39;

# === Kafka (Part 3) ===
export KAFKA_HOME=/opt/kafka
export PATH=$PATH:$KAFKA_HOME/bin
EOF

source ~/.bashrc
echo &quot;KAFKA_HOME=$KAFKA_HOME&quot;
which kafka-topics.sh</code></pre><h3 id="데이터-로그-디렉터리-준비">데이터, 로그 디렉터리 준비</h3>
<pre><code>mkdir -p ~/spark-handson/kafka-data
mkdir -p ~/spark-handson/logs/kafka
ls -ld ~/spark-handson/kafka-data ~/spark-handson/logs/kafka</code></pre><h3 id="serverproperties-작성">server.properties 작성</h3>
<pre><code>sudo cp /opt/kafka/config/kraft/server.properties \
        /opt/kafka/config/kraft/server.properties.original

sudo tee /opt/kafka/config/kraft/server.properties &gt; /dev/null &lt;&lt; &#39;EOF&#39;
# ===========================================================================
# Kafka 3.7 KRaft Single-Node — Hands-on Part 3
# ===========================================================================

# ---- KRaft Roles ---------------------------------------------------------
process.roles=broker,controller
node.id=1
controller.quorum.voters=1@localhost:9093

# ---- Listeners -----------------------------------------------------------
listeners=PLAINTEXT://:9092,CONTROLLER://:9093
inter.broker.listener.name=PLAINTEXT
controller.listener.names=CONTROLLER
listener.security.protocol.map=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
advertised.listeners=PLAINTEXT://localhost:9092

# ---- Storage -------------------------------------------------------------
log.dirs=/home/azureuser/spark-handson/kafka-data

# ---- Topic Defaults (단일 노드용 축소) -----------------------------------
num.partitions=3
default.replication.factor=1
offsets.topic.replication.factor=1
transaction.state.log.replication.factor=1
transaction.state.log.min.isr=1
min.insync.replicas=1

# ---- Retention (학습용 기본 1h, 토픽별 재정의 가능) ----------------------
log.retention.hours=1
log.segment.bytes=104857600
log.retention.check.interval.ms=300000

# ---- Performance (단일 노드 학습 환경) ------------------------------------
num.network.threads=3
num.io.threads=4
socket.send.buffer.bytes=102400
socket.receive.buffer.bytes=102400
socket.request.max.bytes=104857600

# ---- Auto Topic Creation (학습 편의, 운영에선 false 권장) ----------------
auto.create.topics.enable=false
EOF

grep -E &quot;^(process.roles|node.id|listeners|log.dirs|num.partitions|log.retention)&quot; \
     /opt/kafka/config/kraft/server.properties</code></pre><h3 id="kafka_heap_opts-정착">KAFKA_HEAP_OPTS 정착</h3>
<pre><code class="language-bash">sudo tee /etc/default/kafka &gt; /dev/null &lt;&lt; &#39;EOF&#39;
KAFKA_HEAP_OPTS=&quot;-Xmx1g -Xms1g&quot;
LOG_DIR=/home/azureuser/spark-handson/logs/kafka
EOF

cat /etc/default/kafka</code></pre>
<h3 id="cluster-id-생성--storage-format">Cluster ID 생성 + Storage Format</h3>
<pre><code class="language-bash">KAFKA_CLUSTER_ID=$(/opt/kafka/bin/kafka-storage.sh random-uuid)
echo &quot;Cluster ID: $KAFKA_CLUSTER_ID&quot;

echo &quot;$KAFKA_CLUSTER_ID&quot; &gt; ~/spark-handson/kafka-data/CLUSTER_ID.txt

/opt/kafka/bin/kafka-storage.sh format \
  -t &quot;$KAFKA_CLUSTER_ID&quot; \
  -c /opt/kafka/config/kraft/server.properties

ls ~/spark-handson/kafka-data/
cat ~/spark-handson/kafka-data/meta.properties</code></pre>
<h3 id="systemd-unit-작성">systemd unit 작성</h3>
<pre><code class="language-bash">sudo tee /etc/systemd/system/kafka.service &gt; /dev/null &lt;&lt; &#39;EOF&#39;
[Unit]
Description=Apache Kafka (KRaft single-node)
Documentation=https://kafka.apache.org/documentation/
After=network.target

[Service]
Type=simple
User=azureuser
Group=azureuser
EnvironmentFile=/etc/default/kafka
Environment=JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64

ExecStart=/opt/kafka/bin/kafka-server-start.sh /opt/kafka/config/kraft/server.properties
ExecStop=/opt/kafka/bin/kafka-server-stop.sh

Restart=on-failure
RestartSec=5s
LimitNOFILE=100000
TimeoutStopSec=60s

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable kafka
sudo systemd-analyze verify /etc/systemd/system/kafka.service</code></pre>
<h3 id="kafka-기동">Kafka 기동</h3>
<pre><code class="language-bash">sudo systemctl start kafka
sleep 5
sudo systemctl status kafka --no-pager | head -15</code></pre>
<pre><code class="language-bash"># 포트 LISTEN 확인 (9092 + 9093)
ss -tlnp 2&gt;/dev/null | grep -E &#39;:9092 |:9093 &#39;</code></pre>
<pre><code class="language-bash"># Kafka 자체 로그
tail -20 ~/spark-handson/logs/kafka/server.log 2&gt;/dev/null \
  || tail -20 /opt/kafka/logs/server.log</code></pre>
<blockquote>
<p>로그 마지막에 <code>Kafka Server started</code> 확인.
실패시 다음과 같이 fix.</p>
<h1 id="1-환경-파일을-올바른-변수명으로-다시-작성">1) 환경 파일을 올바른 변수명으로 다시 작성</h1>
</blockquote>
<pre><code class="language-bash">sudo tee /etc/default/kafka &gt; /dev/null &lt;&lt; &#39;EOF&#39;
KAFKA_HEAP_OPTS=&quot;-Xmx1g -Xms1g&quot;
LOG_DIR=/home/azureuser/spark-handson/logs/kafka
EOF

# 2) 재시작 카운터 초기화 (41번 실패한 거 리셋)
sudo systemctl reset-failed kafka

# 3) 시작
sudo systemctl restart kafka
sleep 8
sudo systemctl status kafka --no-pager | head -8</code></pre>
<p>다시 확인</p>
<pre><code class="language-bash"># Kafka 자체 로그
tail -20 ~/spark-handson/logs/kafka/server.log 2&gt;/dev/null \
  || tail -20 /opt/kafka/logs/server.log</code></pre>
<h3 id="토픽-생성">토픽 생성</h3>
<pre><code class="language-bash">kafka-topics.sh --bootstrap-server localhost:9092 \
  --create --topic transactions-raw \
  --partitions 3 \
  --replication-factor 1 \
  --config retention.ms=3600000 \
  --config segment.ms=600000

kafka-topics.sh --bootstrap-server localhost:9092 --list
kafka-topics.sh --bootstrap-server localhost:9092 \
  --describe --topic transactions-raw</code></pre>
<h3 id="콘솔-producerconsumer-검증">콘솔 Producer/Consumer 검증</h3>
<p> 터미널:</p>
<pre><code class="language-bash">printf &#39;msg-1\nmsg-2\nmsg-3\n&#39; | \
  kafka-console-producer.sh --bootstrap-server localhost:9092 \
    --topic transactions-raw

kafka-console-consumer.sh --bootstrap-server localhost:9092 \
  --topic transactions-raw \
  --from-beginning \
  --max-messages 3 \
  --timeout-ms 10000</code></pre>
<h2 id="python-producer">Python Producer</h2>
<blockquote>
<ul>
<li><code>kafka-python</code> 클라이언트로 Producer 구현.</li>
</ul>
</blockquote>
<ul>
<li><code>customers_dim</code>에서 추출한 실제 <code>customer_id</code>로 외래키 정합성 유지.</li>
<li>발행률(rate), 실행 시간(duration), 이상치 비율(dirty rate) 인자로 제어.</li>
<li>의도적으로 1% 비율로 깨진 데이터를 섞어 Section 7 정제 로직 검증 재료를 만든다.</li>
</ul>
<table>
<thead>
<tr>
<th>용어</th>
<th>정의</th>
</tr>
</thead>
<tbody><tr>
<td><strong><code>KafkaProducer</code></strong></td>
<td>kafka-python의 Producer 클라이언트. <code>send()</code> 비동기, <code>flush()</code>로 강제 전송.</td>
</tr>
<tr>
<td><strong><code>send(topic, key, value)</code></strong></td>
<td>메시지 발행. 백그라운드 스레드가 배치로 broker에 전송.</td>
</tr>
<tr>
<td><strong><code>key</code></strong></td>
<td>같은 key는 같은 파티션. 고객별 순서 보장에 사용.</td>
</tr>
<tr>
<td><strong><code>value_serializer</code></strong></td>
<td>dict → bytes 변환 함수. <code>json.dumps().encode()</code>.</td>
</tr>
<tr>
<td><strong><code>acks</code></strong></td>
<td>broker ack 요구 수준. <code>all</code> / <code>1</code> / <code>0</code>. 학습은 <code>all</code>.</td>
</tr>
<tr>
<td><strong><code>linger_ms</code></strong></td>
<td>배치 전송 대기 시간. 10ms가 throughput/latency 균형점.</td>
</tr>
<tr>
<td><strong>Dirty Data</strong></td>
<td>일부러 위반시킨 데이터. 다운스트림 정제 검증용.</td>
</tr>
</tbody></table>
<pre><code>Application 스레드           kafka-python 내부            Kafka Broker
─────────────────            ─────────────────            ─────────────
producer.send(...)  ──→  [accumulator 큐]
producer.send(...)  ──→  [accumulator 큐]
                              │
                              │                 linger_ms 또는 batch.size 도달
                              ▼    
                         [Sender 스레드] ──→     TCP ─→         [9092]
                              │
                              │                 broker ack 대기 (acks=all)
                              ▼
                         [retry 또는 done]

producer.flush()    ──→  큐 비울 때까지 블록
producer.close()    ──→  flush + 연결 종료</code></pre><p><strong><code>flush()</code>를 안 부르면?</strong> 스크립트 종료 시 큐에 남은 메시지 손실 가능. 본 가이드는 <code>try/finally</code>에서 호출.</p>
<h4 id="key-사용-전략">key 사용 전략</h4>
<pre><code>key=None 또는 매번 다른 key:
  Partition 0: [t1][t4][t7]...    ← 라운드로빈
  Partition 1: [t2][t5]...
  Partition 2: [t3][t6]...

key=customer_id (&quot;C001234&quot;):
  hash(&quot;C001234&quot;) % 3 = 1
  → 이 고객의 모든 거래는 항상 Partition 1
  → 순서 보장</code></pre><p><code>key=customer_id</code>. 이유:</p>
<ol>
<li>같은 고객의 거래 순서 보장 (이상 거래 탐지 시 유용)</li>
<li>파티션 분포가 customer_id 분포에 따름 → 자연스러운 부하 분산</li>
</ol>
<h4 id="4종-dirty-패턴">4종 dirty 패턴</h4>
<table>
<thead>
<tr>
<th>패턴</th>
<th>위반 종류</th>
<th>Section 7 정제에서 어떻게 걸러지나</th>
</tr>
</thead>
<tbody><tr>
<td><code>negative_amount</code></td>
<td>도메인 (amount ≥ 0)</td>
<td><code>WHERE amount &gt;= 0</code> 또는 PG CHECK</td>
</tr>
<tr>
<td><code>invalid_status</code></td>
<td>열거형</td>
<td><code>WHERE status IN (...)</code> 또는 PG CHECK</td>
</tr>
<tr>
<td><code>unknown_customer</code></td>
<td>외래키</td>
<td>PG FK 거부 → 7-2의 화이트리스트로 사전 차단</td>
</tr>
<tr>
<td><code>missing_field</code></td>
<td>스키마</td>
<td>Spark <code>from_json</code> 시 NULL → 필터</td>
</tr>
</tbody></table>
<h3 id="kafka-python-설치"><code>kafka-python</code> 설치</h3>
<pre><code class="language-bash">echo $VIRTUAL_ENV
# /home/azureuser/sparkenv 가 나와야 함

pip install --quiet &quot;kafka-python&gt;=2.0.0,&lt;3.0.0&quot;

python -c &quot;import kafka; print(&#39;kafka-python&#39;, kafka.__version__)&quot;</code></pre>
<h4 id="customer_id-샘플-파일-생성">customer_id 샘플 파일 생성</h4>
<p>Producer가 매번 PG에 쿼리하면 비싸다. 시작 시 한 번 추출.</p>
<pre><code class="language-bash">psql -tAc &quot;SELECT customer_id FROM customers_dim ORDER BY customer_id&quot; \
  &gt; ~/spark-handson/streaming/customer_ids.txt

wc -l ~/spark-handson/streaming/customer_ids.txt
head -3 ~/spark-handson/streaming/customer_ids.txt
tail -3 ~/spark-handson/streaming/customer_ids.txt</code></pre>
<pre><code class="language-text"># 기대 출력:
10000 ~/spark-handson/streaming/customer_ids.txt
C000001
C000002
...
C009998
C009999</code></pre>
<h4 id="gen_eventspy-작성"><code>gen_events.py</code> 작성</h4>
<pre><code class="language-bash">cat &gt; ~/spark-handson/streaming/gen_events.py &lt;&lt; &#39;PYEOF&#39;
#!/usr/bin/env python3
&quot;&quot;&quot;
gen_events.py — OneStore 가짜 거래 이벤트 생성기 (Kafka Producer)

실행 예시:
  # 기본: 초당 5건, 무한, dirty 1%
  python ~/spark-handson/streaming/gen_events.py

  # 60초간 초당 10건
  python ~/spark-handson/streaming/gen_events.py --rate 10 --duration 60

  # systemd가 호출할 때 (로그 파일 분리)
  python ~/spark-handson/streaming/gen_events.py \
    --rate 5 --log-file ~/spark-handson/logs/gen_events.log
&quot;&quot;&quot;
import argparse
import json
import logging
import os
import random
import signal
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import List

from kafka import KafkaProducer
from kafka.errors import KafkaError

# ── 도메인 정의 ────────────────────────────────────────────────────────
CATEGORIES = [
    (&quot;GROCERY&quot;,      35),
    (&quot;FASHION&quot;,      20),
    (&quot;ELECTRONICS&quot;,  15),
    (&quot;BOOKS&quot;,        10),
    (&quot;SPORTS&quot;,       10),
    (&quot;BEAUTY&quot;,        5),
    (&quot;FOOD_DELIVERY&quot;, 5),
]
STATUS = [
    (&quot;COMPLETED&quot;, 85),
    (&quot;PENDING&quot;,   10),
    (&quot;FAILED&quot;,     3),
    (&quot;REFUNDED&quot;,   2),
]
DIRTY_PATTERNS = [
    &quot;negative_amount&quot;,
    &quot;invalid_status&quot;,
    &quot;unknown_customer&quot;,
    &quot;missing_field&quot;,
]

logger = logging.getLogger(&quot;gen_events&quot;)


# ── 유틸 ────────────────────────────────────────────────────────────────
def load_customer_ids(path: str) -&gt; List[str]:
    p = Path(os.path.expanduser(path))
    if not p.exists():
        sys.exit(f&quot;ERROR: customer sample file not found: {p}&quot;)
    ids = [ln.strip() for ln in p.read_text().splitlines() if ln.strip()]
    if len(ids) &lt; 100:
        sys.exit(f&quot;ERROR: too few customer ids ({len(ids)}). Expected 100+.&quot;)
    logger.info(f&quot;loaded {len(ids)} customer ids&quot;)
    return ids


def make_tx_id(seq: int) -&gt; str:
    &quot;&quot;&quot;T + YYYYMMDDHHMMSS + 3-digit sequence (max 30 chars).&quot;&quot;&quot;
    now = datetime.now(timezone.utc)
    return f&quot;T{now.strftime(&#39;%Y%m%d%H%M%S&#39;)}{seq:03d}&quot;


def amount_for_category(cat: str) -&gt; float:
    if cat == &quot;ELECTRONICS&quot;:
        return round(random.uniform(50_000, 2_000_000), 2)
    if cat == &quot;GROCERY&quot;:
        return round(random.uniform(3_000, 80_000), 2)
    if cat == &quot;FASHION&quot;:
        return round(random.uniform(15_000, 300_000), 2)
    return round(random.uniform(5_000, 150_000), 2)


def gen_clean(customer_ids: List[str], seq: int) -&gt; dict:
    cat = random.choices(
        [c for c, _ in CATEGORIES],
        weights=[w for _, w in CATEGORIES], k=1
    )[0]
    st = random.choices(
        [s for s, _ in STATUS],
        weights=[w for _, w in STATUS], k=1
    )[0]
    return {
        &quot;transaction_id&quot;: make_tx_id(seq),
        &quot;customer_id&quot;: random.choice(customer_ids),
        &quot;timestamp&quot;: datetime.now(timezone.utc).isoformat(),
        &quot;amount&quot;: amount_for_category(cat),
        &quot;category&quot;: cat,
        &quot;status&quot;: st,
    }


def gen_dirty(customer_ids: List[str], seq: int) -&gt; dict:
    ev = gen_clean(customer_ids, seq)
    pat = random.choice(DIRTY_PATTERNS)
    if pat == &quot;negative_amount&quot;:
        ev[&quot;amount&quot;] = -abs(ev[&quot;amount&quot;])
    elif pat == &quot;invalid_status&quot;:
        ev[&quot;status&quot;] = &quot;ZZZ_INVALID&quot;
    elif pat == &quot;unknown_customer&quot;:
        ev[&quot;customer_id&quot;] = &quot;C999999&quot;          # customers_dim에 없는 ID
    elif pat == &quot;missing_field&quot;:
        ev.pop(&quot;category&quot;, None)
    ev[&quot;_dirty&quot;] = pat                          # _ 시작 필드는 다운스트림 무시
    return ev


# ── Producer ────────────────────────────────────────────────────────────
def make_producer(bootstrap: str) -&gt; KafkaProducer:
    return KafkaProducer(
        bootstrap_servers=bootstrap,
        value_serializer=lambda v: json.dumps(v, default=str).encode(&quot;utf-8&quot;),
        key_serializer=lambda k: (k or &quot;&quot;).encode(&quot;utf-8&quot;),
        acks=&quot;all&quot;,
        linger_ms=10,
        retries=3,
        max_in_flight_requests_per_connection=1,  # 순서 보장
    )


# ── 메인 루프 ───────────────────────────────────────────────────────────
def main():
    ap = argparse.ArgumentParser()
    ap.add_argument(&quot;--bootstrap&quot;,   default=&quot;localhost:9092&quot;)
    ap.add_argument(&quot;--topic&quot;,       default=&quot;transactions-raw&quot;)
    ap.add_argument(&quot;--sample-file&quot;, default=&quot;~/spark-handson/streaming/customer_ids.txt&quot;)
    ap.add_argument(&quot;--rate&quot;,        type=float, default=5.0,
                    help=&quot;events per second&quot;)
    ap.add_argument(&quot;--duration&quot;,    type=int,   default=0,
                    help=&quot;seconds; 0 = infinite&quot;)
    ap.add_argument(&quot;--dirty-rate&quot;,  type=float, default=0.01)
    ap.add_argument(&quot;--log-file&quot;,    default=None)
    args = ap.parse_args()

    # 로깅
    handlers = [logging.StreamHandler()]
    if args.log_file:
        Path(args.log_file).parent.mkdir(parents=True, exist_ok=True)
        handlers.append(logging.FileHandler(args.log_file))
    logging.basicConfig(
        level=logging.INFO,
        format=&quot;%(asctime)s %(levelname)s %(message)s&quot;,
        handlers=handlers,
    )

    customer_ids = load_customer_ids(args.sample_file)
    producer = make_producer(args.bootstrap)
    logger.info(f&quot;connected to {args.bootstrap}, topic={args.topic}, &quot;
                f&quot;rate={args.rate}/s, dirty={args.dirty_rate*100:.1f}%&quot;)

    # 그레이스풀 종료
    stop = {&quot;flag&quot;: False}
    def handle(sig, _frame):
        logger.info(f&quot;signal {sig} received, stopping...&quot;)
        stop[&quot;flag&quot;] = True
    signal.signal(signal.SIGINT, handle)
    signal.signal(signal.SIGTERM, handle)

    interval = 1.0 / args.rate if args.rate &gt; 0 else 0.0
    seq = 0
    sent_clean = sent_dirty = sent_err = 0
    start = time.time()

    try:
        while not stop[&quot;flag&quot;]:
            if args.duration and (time.time() - start) &gt;= args.duration:
                break

            seq = (seq + 1) % 1000
            is_dirty = random.random() &lt; args.dirty_rate
            ev = (gen_dirty if is_dirty else gen_clean)(customer_ids, seq)

            try:
                producer.send(args.topic,
                              key=ev.get(&quot;customer_id&quot;),
                              value=ev)
                if is_dirty:
                    sent_dirty += 1
                else:
                    sent_clean += 1
            except KafkaError as e:
                sent_err += 1
                logger.warning(f&quot;send error: {e}&quot;)

            total = sent_clean + sent_dirty
            if total and total % 100 == 0:
                el = time.time() - start
                logger.info(
                    f&quot;sent={total} clean={sent_clean} dirty={sent_dirty} &quot;
                    f&quot;err={sent_err} elapsed={el:.1f}s rate={total/el:.1f}/s&quot;
                )

            if interval &gt; 0:
                time.sleep(interval)
    finally:
        logger.info(&quot;flushing remaining messages...&quot;)
        producer.flush(timeout=10)
        producer.close(timeout=10)
        el = time.time() - start
        total = sent_clean + sent_dirty
        logger.info(
            f&quot;DONE total={total} clean={sent_clean} dirty={sent_dirty} &quot;
            f&quot;err={sent_err} elapsed={el:.1f}s avg_rate={total/max(el,0.01):.1f}/s&quot;
        )


if __name__ == &quot;__main__&quot;:
    main()
PYEOF

chmod +x ~/spark-handson/streaming/gen_events.py
ls -l ~/spark-handson/streaming/gen_events.py
wc -l ~/spark-handson/streaming/gen_events.py</code></pre>
<h4 id="짧은-실행-10초간-초당-5건">짧은 실행 (10초간 초당 5건)</h4>
<p><strong>터미널 1 (Producer)</strong>:</p>
<pre><code class="language-bash">python ~/spark-handson/streaming/gen_events.py \
  --rate 5 --duration 10 --dirty-rate 0.10
# (학습용으로 dirty-rate 10%로 올려 dirty 패턴 가시성 ↑)</code></pre>
<h4 id="메시지-수신-확인-consumer">메시지 수신 확인 (Consumer)</h4>
<pre><code class="language-bash">kafka-console-consumer.sh --bootstrap-server localhost:9092 \
  --topic transactions-raw \
  --from-beginning \
  --max-messages 50 \
  --timeout-ms 5000 \
  | head -5</code></pre>
<h4 id="dirty-데이터-패턴-분포-확인">dirty 데이터 패턴 분포 확인</h4>
<pre><code class="language-bash">kafka-console-consumer.sh --bootstrap-server localhost:9092 \
  --topic transactions-raw --from-beginning \
  --max-messages 50 --timeout-ms 5000 2&gt;/dev/null \
  | grep &#39;^{&#39; \
  | jq -r &#39;._dirty // &quot;clean&quot;&#39; \
  | sort | uniq -c</code></pre>
<h2 id="spark-structured-streaming--kafka-consume--정제">Spark Structured Streaming — Kafka Consume + 정제</h2>
<blockquote>
<ul>
<li>Spark Structured Streaming의 micro-batch 모델 이해.</li>
</ul>
</blockquote>
<ul>
<li>Kafka source(<code>format=&quot;kafka&quot;</code>)에서 binary value를 JSON으로 파싱.</li>
<li>명시적 스키마(StructType)로 타입 안전성 확보.</li>
<li>4종 dirty 패턴을 정제 로직으로 걸러낸다.</li>
<li>콘솔 sink로 적재 전 결과를 시각적으로 검증.</li>
</ul>
<table>
<thead>
<tr>
<th>용어</th>
<th>정의</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Structured Streaming</strong></td>
<td>DataFrame/Dataset API로 스트리밍 처리. 내부는 micro-batch.</td>
</tr>
<tr>
<td><strong>Micro-batch</strong></td>
<td>일정 간격(trigger)으로 누적된 데이터를 한 번에 배치 처리.</td>
</tr>
<tr>
<td><strong>Trigger</strong></td>
<td>&quot;언제 다음 배치를 실행할지&quot;. <code>processingTime=&#39;5 seconds&#39;</code> / <code>available now</code> 등.</td>
</tr>
<tr>
<td><strong>Source / Sink</strong></td>
<td>입력/출력. 우리는 Kafka → 최종 PG (7-2) / 검증용 console (7-1).</td>
</tr>
<tr>
<td><strong>Schema-on-read</strong></td>
<td>Kafka는 binary만 저장. consumer가 읽을 때 명시.</td>
</tr>
<tr>
<td><strong><code>from_json</code></strong></td>
<td>JSON 문자열을 StructType 스키마에 따라 컬럼으로 펼침.</td>
</tr>
<tr>
<td><strong><code>foreachBatch</code></strong></td>
<td>각 micro-batch DataFrame에 임의 로직 적용 (7-2에서 사용).</td>
</tr>
<tr>
<td><strong><code>checkpointLocation</code></strong></td>
<td>offset·메타데이터 저장 디렉터리. 재시작 시 정확히 끊긴 지점부터 재개.</td>
</tr>
</tbody></table>
<h4 id="micro-batch-모델">Micro-batch 모델</h4>
<pre><code>시간축 →

  Trigger 1 (t=5s)        Trigger 2 (t=10s)        Trigger 3 (t=15s)
       │                       │                       │
       ▼                       ▼                       ▼
   ┌────────┐             ┌────────┐             ┌────────┐
   │ batch  │            │ batch  │             │ batch  │
   │  ID=0  │            │  ID=1  │             │  ID=2  │
   │ 25 msg │            │ 27 msg │             │ 24 msg │
   └────┬───┘             └────┬───┘             └────┬───┘
        │ DataFrame           │ DataFrame            │ DataFrame
        ▼                      ▼                      ▼
    [정제 로직]            [정제 로직]            [정제 로직]
        │                      │                      │
        ▼                      ▼                      ▼
    [Sink]                  [Sink]                  [Sink]
        │                      │                      │
        ▼                      ▼                      ▼
   checkpoint 갱신       checkpoint 갱신        checkpoint 갱신</code></pre><p><strong>핵심</strong>: 매 trigger마다 받은 메시지가 <strong>정적 DataFrame</strong>. 이후 처리는 일반 Spark 연산과 동일.</p>
<h4 id="kafka-source의-raw-스키마">Kafka source의 raw 스키마</h4>
<p><code>spark.readStream.format(&quot;kafka&quot;)</code>가 반환하는 DataFrame은 항상 다음 7개 컬럼.</p>
<table>
<thead>
<tr>
<th>컬럼</th>
<th>타입</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>key</code></td>
<td>binary</td>
<td>Producer가 보낸 key (우리는 customer_id)</td>
</tr>
<tr>
<td><code>value</code></td>
<td>binary</td>
<td>Producer가 보낸 value (우리는 JSON bytes)</td>
</tr>
<tr>
<td><code>topic</code></td>
<td>string</td>
<td>토픽 이름</td>
</tr>
<tr>
<td><code>partition</code></td>
<td>int</td>
<td>파티션 번호</td>
</tr>
<tr>
<td><code>offset</code></td>
<td>long</td>
<td>메시지의 offset</td>
</tr>
<tr>
<td><code>timestamp</code></td>
<td>timestamp</td>
<td>broker가 메시지를 받은 시각</td>
</tr>
<tr>
<td><code>timestampType</code></td>
<td>int</td>
<td>timestamp의 의미</td>
</tr>
</tbody></table>
<h4 id="4종-dirty-패턴-정제-매핑">4종 dirty 패턴 정제 매핑</h4>
<pre><code>원본 메시지 (JSON)
     │ CAST(value AS STRING) → from_json(payload, SCHEMA)
     ▼
struct&lt;transaction_id, customer_id, timestamp, amount, category, status&gt;
     │ ① missing_field (category null) → IS NOT NULL 필터
     │ ② negative_amount → amount &gt;= 0 필터
     │ ③ invalid_status → status IN (...) 필터
     │ ④ unknown_customer → 7-2에서 화이트리스트로 차단
     ▼
정제된 DataFrame
     │
     ▼
sink (7-1: 콘솔, 7-2: PostgreSQL)</code></pre><h4 id="kafka-패키지-사전-캐싱">Kafka 패키지 사전 캐싱</h4>
<pre><code class="language-bash">
cat &gt; /tmp/cache_kafka_pg.py &lt;&lt; &#39;PYEOF&#39;
from pyspark.sql import SparkSession
spark = SparkSession.builder.appName(&quot;cache_kafka_pg&quot;).getOrCreate()
print(&quot;Kafka + PostgreSQL packages loaded&quot;)
spark.stop()
PYEOF

spark-submit \
  --packages org.apache.spark:spark-sql-kafka-0-10_2.12:3.5.8,org.postgresql:postgresql:42.7.4 \
  --conf spark.driver.memory=1g \
  /tmp/cache_kafka_pg.py 2&gt;&amp;1 | grep -E &quot;(SUCCESSFUL|downloaded|loaded|ERROR)&quot;

# 캐시 확인 — Step 4-1과 같은 경로 패턴 (~/.ivy2/jars/, 평탄 구조)
ls -la ~/.ivy2/jars/ | grep -E &quot;spark-sql-kafka|spark-token|kafka-clients|postgresql&quot;</code></pre>
<h4 id="정제-검증용-스크립트-콘솔-sink">정제 검증용 스크립트 (콘솔 sink)</h4>
<pre><code class="language-bash">cat &gt; ~/spark-handson/streaming/streaming_console.py &lt;&lt; &#39;PYEOF&#39;
#!/usr/bin/env python3
&quot;&quot;&quot;
streaming_console.py — Section 7-1 검증용
Kafka transactions-raw 구독 → 정제 → 콘솔 출력.
&quot;&quot;&quot;
import os
from pyspark.sql import SparkSession
from pyspark.sql.functions import col, from_json, to_timestamp
from pyspark.sql.types import StructType, StringType, DoubleType

KAFKA_BOOTSTRAP = &quot;localhost:9092&quot;
KAFKA_TOPIC     = &quot;transactions-raw&quot;
VALID_STATUS    = [&quot;COMPLETED&quot;, &quot;PENDING&quot;, &quot;FAILED&quot;, &quot;REFUNDED&quot;]


PAYLOAD_SCHEMA = (
    StructType()
    .add(&quot;transaction_id&quot;, StringType())
    .add(&quot;customer_id&quot;,    StringType())
    .add(&quot;timestamp&quot;,      StringType())
    .add(&quot;amount&quot;,         DoubleType())
    .add(&quot;category&quot;,       StringType())
    .add(&quot;status&quot;,         StringType())
)


def main():
    spark = (
        SparkSession.builder
        .appName(&quot;streaming_console&quot;)
        .getOrCreate()
    )
    spark.sparkContext.setLogLevel(&quot;ERROR&quot;)

    # 1) Kafka 소스 구독
    raw = (
        spark.readStream
        .format(&quot;kafka&quot;)
        .option(&quot;kafka.bootstrap.servers&quot;, KAFKA_BOOTSTRAP)
        .option(&quot;subscribe&quot;, KAFKA_TOPIC)
        .option(&quot;startingOffsets&quot;, &quot;earliest&quot;)  # 운영은 latest
        .option(&quot;failOnDataLoss&quot;, &quot;false&quot;)
        .load()
    )

    # 2) JSON 파싱
    parsed = (
        raw
        .select(
            col(&quot;partition&quot;).alias(&quot;kafka_partition&quot;),
            col(&quot;offset&quot;).alias(&quot;kafka_offset&quot;),
            col(&quot;timestamp&quot;).alias(&quot;kafka_ts&quot;),
            from_json(col(&quot;value&quot;).cast(&quot;string&quot;), PAYLOAD_SCHEMA).alias(&quot;p&quot;),
        )
        .select(&quot;kafka_partition&quot;, &quot;kafka_offset&quot;, &quot;kafka_ts&quot;, &quot;p.*&quot;)
    )

    # 3) 타입 변환 + 정제
    cleaned = (
        parsed
        .withColumn(&quot;ts&quot;, to_timestamp(col(&quot;timestamp&quot;)))
        .where(col(&quot;transaction_id&quot;).isNotNull())
        .where(col(&quot;customer_id&quot;).isNotNull())
        .where(col(&quot;ts&quot;).isNotNull())
        .where(col(&quot;amount&quot;).isNotNull() &amp; (col(&quot;amount&quot;) &gt;= 0))
        .where(col(&quot;category&quot;).isNotNull())
        .where(col(&quot;status&quot;).isin(VALID_STATUS))
        .select(
            &quot;transaction_id&quot;, &quot;customer_id&quot;, &quot;ts&quot;,
            &quot;amount&quot;, &quot;category&quot;, &quot;status&quot;,
            &quot;kafka_partition&quot;, &quot;kafka_offset&quot;,
        )
    )

    # 4) 콘솔 sink
    query = (
        cleaned.writeStream
        .format(&quot;console&quot;)
        .outputMode(&quot;append&quot;)
        .option(&quot;truncate&quot;, &quot;false&quot;)
        .option(&quot;numRows&quot;, 10)
        .trigger(processingTime=&quot;5 seconds&quot;)
        .queryName(&quot;console_q&quot;)
        .start()
    )

    print(f&quot;[INFO] streaming started. trigger=5s, topic={KAFKA_TOPIC}&quot;)
    print(f&quot;[INFO] press Ctrl-C to stop&quot;)
    query.awaitTermination()


if __name__ == &quot;__main__&quot;:
    main()
PYEOF

chmod +x ~/spark-handson/streaming/streaming_console.py</code></pre>
<h4 id="두-터미널-실행-패턴">두 터미널 실행 패턴</h4>
<p><strong>터미널 A</strong>: Producer</p>
<pre><code class="language-bash">python ~/spark-handson/streaming/gen_events.py \
  --rate 5 --duration 120 --dirty-rate 0.20</code></pre>
<p><strong>터미널 B</strong>: Streaming consumer</p>
<pre><code class="language-bash">spark-submit \
  --packages org.apache.spark:spark-sql-kafka-0-10_2.12:3.5.8 \
  --conf spark.driver.memory=2g \
  ~/spark-handson/streaming/streaming_console.py</code></pre>
<blockquote>
<p><strong>dirty 메시지가 사라진 게 보이는가?</strong> Producer는 dirty 20%를 보냈지만 콘솔엔 정상만. 즉 dirty는 정제 단계에서 모두 제거.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/rudin_/post/69cf32b7-bc95-4616-aeb2-92dc76069c4a/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/rudin_/post/7170abc9-f194-4694-b966-180e22477f5b/image.png" alt=""></p>
<h4 id="종료">종료</h4>
<p><strong>터미널 B</strong>에서 <code>Ctrl-C</code>. graceful shutdown.</p>
<h2 id="spark-structured-streaming--foreachbatch--postgresql-적재--체크포인트">Spark Structured Streaming — foreachBatch + PostgreSQL 적재 + 체크포인트</h2>
<blockquote>
<ul>
<li><code>foreachBatch</code>로 micro-batch DataFrame을 JDBC로 적재.</li>
</ul>
</blockquote>
<ul>
<li><code>customers_dim</code> 화이트리스트로 <code>unknown_customer</code> 사전 차단.</li>
<li><code>ON CONFLICT DO NOTHING</code> 패턴으로 멱등성 확보 (재처리 시 중복 PK 무시).</li>
<li><code>checkpointLocation</code>으로 정확히 끊긴 지점부터 재개 (exactly-once 효과).</li>
<li>운영 환경 권장 옵션(<code>maxOffsetsPerTrigger</code>, retry 정책) 적용.</li>
</ul>
<table>
<thead>
<tr>
<th>용어</th>
<th>정의</th>
</tr>
</thead>
<tbody><tr>
<td><strong><code>foreachBatch(func)</code></strong></td>
<td>각 micro-batch의 DataFrame과 batchId를 인자로 사용자 함수 실행.</td>
</tr>
<tr>
<td><strong>psycopg2</strong></td>
<td>Python의 PostgreSQL 클라이언트. UPSERT 쓸 때 사용.</td>
</tr>
<tr>
<td><strong>UPSERT</strong></td>
<td>INSERT ... ON CONFLICT (...) DO ... PostgreSQL 9.5+ 문법.</td>
</tr>
<tr>
<td><strong>Checkpoint</strong></td>
<td>offset, state, batch metadata 저장. 재시작 시 복구.</td>
</tr>
<tr>
<td><strong>Exactly-once</strong></td>
<td>메시지가 sink에 정확히 1번 반영. (멱등 sink + checkpoint 조합)</td>
</tr>
<tr>
<td><strong>Whitelist</strong></td>
<td>허용 대상만 통과. customers_dim에 있는 customer_id만.</td>
</tr>
</tbody></table>
<h4 id="개념-설명-화이트리스트-사전-차단-vs-fk-거부">개념 설명: 화이트리스트 사전 차단 vs FK 거부</h4>
<pre><code>방어선 1: Spark에서 화이트리스트 (선제 차단)
   df.join(customers_broadcast, &quot;customer_id&quot;, &quot;inner&quot;)
   → unknown_customer 메시지 사전 제거

방어선 2: PostgreSQL FK (최종 차단)
   FOREIGN KEY (customer_id) REFERENCES customers_dim(customer_id)
   → 만에 하나 통과해도 INSERT 거부</code></pre><p>방어선 1만으로 거의 끝. 2는 안전망.</p>
<blockquote>
<p><strong>왜 broadcast?</strong> customers_dim 10,000건 (~1MB)은 작아서 모든 executor에 broadcast 가능. shuffle 없이 빠른 join.</p>
</blockquote>
<h4 id="개념-설명-on-conflict-do-nothing의-멱등성">개념 설명: ON CONFLICT DO NOTHING의 멱등성</h4>
<pre><code class="language-sql">INSERT INTO transactions_fact (transaction_id, customer_id, ts, amount, category, status)
VALUES (...)
ON CONFLICT (transaction_id) DO NOTHING;</code></pre>
<ul>
<li>같은 <code>transaction_id</code> 다시 들어오면 무시.</li>
<li>Streaming 재시작 후 일부 메시지가 재처리되어도 중복 안 됨.</li>
<li><strong>이것이 Streaming의 &quot;exactly-once 효과&quot;의 핵심</strong>.</li>
</ul>
<h4 id="개념-설명-checkpoint-동작">개념 설명: Checkpoint 동작</h4>
<pre><code>첫 실행:
  Kafka offset 0 → batch 0 → PG 적재 → checkpoint 갱신 (offset=25)
  Kafka offset 25 → batch 1 → PG 적재 → checkpoint 갱신 (offset=52)
  ...

재시작 (마지막 checkpoint=52):
  Kafka offset 52 부터 재개
  → batch 마지막에 미완료 적재가 있어도 ON CONFLICT가 멱등성 보장</code></pre><p>Checkpoint 구조:</p>
<pre><code>~/spark-handson/checkpoints/streaming_to_pg/
├── offsets/         ← 매 batch의 시작 offset
├── commits/         ← batch 완료 표시
├── sources/         ← Kafka source 메타
└── state/           ← (stateful 연산 시 사용; 본 가이드는 stateless)</code></pre><blockquote>
<p><strong>체크포인트 디렉터리는 절대 수동 삭제 X</strong> (의도적 reset 외엔). 삭제 시 처음부터 다시 처리 → 중복 가능.</p>
</blockquote>
<h4 id="psycopg2-binary-설치">psycopg2-binary 설치</h4>
<pre><code class="language-bash">pip install --quiet &quot;psycopg2-binary&gt;=2.9.0,&lt;3.0.0&quot;
python -c &quot;import psycopg2; print(&#39;psycopg2&#39;, psycopg2.__version__)&quot;</code></pre>
<h4 id="streaming_to_pgpy-작성"><code>streaming_to_pg.py</code> 작성</h4>
<pre><code class="language-bash">cat &gt; ~/spark-handson/streaming/streaming_to_pg.py &lt;&lt; &#39;PYEOF&#39;
#!/usr/bin/env python3
&quot;&quot;&quot;
streaming_to_pg.py — Section 7-2 본 적재 스크립트
Kafka → 정제 → customers_dim 화이트리스트 → PostgreSQL UPSERT.
checkpoint로 정확한 재시작 보장.
&quot;&quot;&quot;
import os
import sys
from pathlib import Path

import psycopg2
from psycopg2.extras import execute_values

from pyspark.sql import SparkSession
from pyspark.sql.functions import col, from_json, to_timestamp, broadcast
from pyspark.sql.types import StructType, StringType, DoubleType


# ── 설정 ────────────────────────────────────────────────────────────────
KAFKA_BOOTSTRAP = &quot;localhost:9092&quot;
KAFKA_TOPIC     = &quot;transactions-raw&quot;
VALID_STATUS    = [&quot;COMPLETED&quot;, &quot;PENDING&quot;, &quot;FAILED&quot;, &quot;REFUNDED&quot;]
CHECKPOINT_DIR  = os.path.expanduser(&quot;~/spark-handson/checkpoints/streaming_to_pg&quot;)

PAYLOAD_SCHEMA = (
    StructType()
    .add(&quot;transaction_id&quot;, StringType())
    .add(&quot;customer_id&quot;,    StringType())
    .add(&quot;timestamp&quot;,      StringType())
    .add(&quot;amount&quot;,         DoubleType())
    .add(&quot;category&quot;,       StringType())
    .add(&quot;status&quot;,         StringType())
)


# ── PG 접속 정보 (.pgpass 파싱) ─────────────────────────────────────────
def get_pg_password() -&gt; str:
    pgpass = Path.home() / &quot;.pgpass&quot;
    if oct(pgpass.stat().st_mode)[-3:] != &quot;600&quot;:
        sys.exit(&quot;ERROR: ~/.pgpass permission must be 600&quot;)
    for ln in pgpass.read_text().splitlines():
        parts = ln.split(&quot;:&quot;)
        if len(parts) == 5 and parts[3] == &quot;handson&quot;:
            return parts[4]
    sys.exit(&quot;ERROR: handson entry missing in ~/.pgpass&quot;)


PG_HOST, PG_PORT = &quot;127.0.0.1&quot;, 5432
PG_DB,   PG_USER = &quot;onestore&quot;, &quot;handson&quot;
PG_PASS = get_pg_password()


# ── customers_dim 화이트리스트 로드 ─────────────────────────────────────
def load_customer_whitelist(spark: SparkSession):
    jdbc_url = f&quot;jdbc:postgresql://{PG_HOST}:{PG_PORT}/{PG_DB}&quot;
    props = {&quot;user&quot;: PG_USER, &quot;password&quot;: PG_PASS,
             &quot;driver&quot;: &quot;org.postgresql.Driver&quot;}
    df = (
        spark.read.jdbc(jdbc_url,
                        &quot;(SELECT customer_id FROM customers_dim) AS sub&quot;,
                        properties=props)
    )
    cnt = df.count()
    print(f&quot;[INFO] customers_dim whitelist loaded: {cnt} rows&quot;)
    if cnt == 0:
        sys.exit(&quot;ERROR: customers_dim is empty. Run Section 4 first.&quot;)
    return df


# ── foreachBatch 함수 ───────────────────────────────────────────────────
def write_batch_to_pg(batch_df, batch_id):
    &quot;&quot;&quot;매 micro-batch마다 호출.&quot;&quot;&quot;
    if batch_df.rdd.isEmpty():
        print(f&quot;[batch {batch_id}] empty, skip&quot;)
        return

    # Spark Driver로 행 수집 (batch당 보통 수십~수백건)
    rows = batch_df.collect()
    print(f&quot;[batch {batch_id}] received {len(rows)} valid rows&quot;)

    if not rows:
        return

    # psycopg2로 UPSERT
    conn = None
    try:
        conn = psycopg2.connect(
            host=PG_HOST, port=PG_PORT,
            dbname=PG_DB, user=PG_USER, password=PG_PASS,
            connect_timeout=5,
        )
        conn.autocommit = False
        with conn.cursor() as cur:
            data = [
                (r.transaction_id, r.customer_id, r.ts,
                 float(r.amount), r.category, r.status)
                for r in rows
            ]
            sql = &quot;&quot;&quot;
                INSERT INTO transactions_fact
                  (transaction_id, customer_id, ts, amount, category, status)
                VALUES %s
                ON CONFLICT (transaction_id) DO NOTHING
            &quot;&quot;&quot;
            execute_values(cur, sql, data, page_size=500)
            inserted = cur.rowcount
            conn.commit()
            print(f&quot;[batch {batch_id}] inserted={inserted} (skip on conflict)&quot;)
    except Exception as e:
        if conn:
            conn.rollback()
        # 에러 raise → Spark가 batch를 retry. 너무 자주면 max retry 후 query 실패.
        print(f&quot;[batch {batch_id}] ERROR: {e}&quot;)
        raise
    finally:
        if conn:
            conn.close()


def main():
    spark = (
        SparkSession.builder
        .appName(&quot;streaming_to_pg&quot;)
        .getOrCreate()
    )
    spark.sparkContext.setLogLevel(&quot;ERROR&quot;)

    # 1) 화이트리스트 (broadcast로 변환)
    whitelist = broadcast(load_customer_whitelist(spark))

    # 2) Kafka 소스
    raw = (
        spark.readStream
        .format(&quot;kafka&quot;)
        .option(&quot;kafka.bootstrap.servers&quot;, KAFKA_BOOTSTRAP)
        .option(&quot;subscribe&quot;, KAFKA_TOPIC)
        .option(&quot;startingOffsets&quot;, &quot;latest&quot;)           # 운영: latest
        .option(&quot;failOnDataLoss&quot;, &quot;false&quot;)
        .option(&quot;maxOffsetsPerTrigger&quot;, 1000)          # 백프레셔 보호
        .load()
    )

    # 3) JSON 파싱 + 정제
    parsed = (
        raw.select(
            from_json(col(&quot;value&quot;).cast(&quot;string&quot;), PAYLOAD_SCHEMA).alias(&quot;p&quot;)
        )
        .select(&quot;p.*&quot;)
        .withColumn(&quot;ts&quot;, to_timestamp(col(&quot;timestamp&quot;)))
        .where(col(&quot;transaction_id&quot;).isNotNull())
        .where(col(&quot;customer_id&quot;).isNotNull())
        .where(col(&quot;ts&quot;).isNotNull())
        .where(col(&quot;amount&quot;).isNotNull() &amp; (col(&quot;amount&quot;) &gt;= 0))
        .where(col(&quot;category&quot;).isNotNull())
        .where(col(&quot;status&quot;).isin(VALID_STATUS))
    )

    # 4) 화이트리스트 join (unknown_customer 차단)
    cleaned = parsed.join(whitelist, &quot;customer_id&quot;, &quot;inner&quot;) \
                    .select(&quot;transaction_id&quot;, &quot;customer_id&quot;, &quot;ts&quot;,
                            &quot;amount&quot;, &quot;category&quot;, &quot;status&quot;)

    # 5) foreachBatch sink
    query = (
        cleaned.writeStream
        .foreachBatch(write_batch_to_pg)
        .option(&quot;checkpointLocation&quot;, CHECKPOINT_DIR)
        .trigger(processingTime=&quot;10 seconds&quot;)
        .queryName(&quot;kafka_to_pg&quot;)
        .start()
    )

    print(f&quot;[INFO] streaming started&quot;)
    print(f&quot;[INFO] checkpoint: {CHECKPOINT_DIR}&quot;)
    print(f&quot;[INFO] press Ctrl-C to stop gracefully&quot;)

    query.awaitTermination()


if __name__ == &quot;__main__&quot;:
    main()
PYEOF

chmod +x ~/spark-handson/streaming/streaming_to_pg.py
ls -l ~/spark-handson/streaming/streaming_to_pg.py</code></pre>
<h4 id="producer">Producer</h4>
<p><img src="https://velog.velcdn.com/images/rudin_/post/d0fefe38-2df6-4926-9c25-f173d78c6584/image.png" alt=""></p>
<h4 id="streaming-→-pg">Streaming → PG</h4>
<p><img src="https://velog.velcdn.com/images/rudin_/post/0ea3c9c0-db48-4d19-8809-72afb13ad979/image.png" alt=""></p>
<h4 id="pg에서-실시간-적재-확인">PG에서 실시간 적재 확인</h4>
<p><img src="https://velog.velcdn.com/images/rudin_/post/e63297c4-55dc-49af-b893-98d54fd2029b/image.png" alt=""></p>
<h4 id="checkpoint-디렉터리-확인">checkpoint 디렉터리 확인</h4>
<p><img src="https://velog.velcdn.com/images/rudin_/post/6666b436-fef9-4633-a2ef-38372863315e/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/066c7f62-29b0-48df-912f-755011779312/image.png" alt=""></p>
<h4 id="재시작-검증-exactly-once-효과">재시작 검증 (exactly-once 효과)</h4>
<p><strong>터미널 B</strong>에서 <code>Ctrl-C</code> → 30초 대기 → 같은 명령으로 재실행.</p>
<pre><code class="language-bash"># 재실행 전 PG의 전체 행 수
COUNT_BEFORE=$(psql -tAc &quot;SELECT COUNT(*) FROM transactions_fact&quot;)
echo &quot;before restart: $COUNT_BEFORE&quot;

# 30초간 더 실행 후 종료, 다시 행 수 확인
# (재시작 시 [INFO] starting from checkpoint ... 로그 보임)</code></pre>
<blockquote>
<p><strong>예상</strong>: checkpoint 덕분에 끊긴 지점에서 재개. 약간의 중복은 ON CONFLICT가 흡수.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/rudin_/post/bf2dd1ff-18cc-4018-88fc-4ea7f87894c7/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/a48bea01-6a89-4936-9a0b-261de2807140/image.png" alt=""></p>
<h4 id="적재-데이터-검증-sql">적재 데이터 검증 SQL</h4>
<pre><code class="language-bash">psql &lt;&lt; &#39;SQL&#39;
\echo &#39;== 전체 행 수 ==&#39;
SELECT COUNT(*) FROM transactions_fact;

\echo &#39;== status별 분포 ==&#39;
SELECT status, COUNT(*) FROM transactions_fact GROUP BY status ORDER BY 2 DESC;

\echo &#39;== 가장 활발한 고객 5명 ==&#39;
SELECT customer_id, COUNT(*) AS tx_count, SUM(amount) AS total_spent
FROM transactions_fact
GROUP BY customer_id
ORDER BY tx_count DESC
LIMIT 5;

\echo &#39;== category별 평균 금액 ==&#39;
SELECT category, COUNT(*) AS n, ROUND(AVG(amount)::numeric, 0) AS avg_amount
FROM transactions_fact
GROUP BY category
ORDER BY avg_amount DESC;

\echo &#39;== 최근 1분간 처리량 ==&#39;
SELECT COUNT(*) AS recent_count
FROM transactions_fact
WHERE ingested_at &gt;= NOW() - INTERVAL &#39;1 minute&#39;;
SQL</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/ee177717-5489-4fee-9ea7-1c00107ceac1/image.png" alt=""></p>
<h2 id="cron">cron</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>cron</th>
<th>Airflow</th>
</tr>
</thead>
<tbody><tr>
<td><strong>설치</strong></td>
<td>OS에 기본 내장</td>
<td>별도 설치 (DB·webserver·scheduler·worker)</td>
</tr>
<tr>
<td><strong>메모리</strong></td>
<td>&lt; 10MB</td>
<td>1.5~3GB 이상</td>
</tr>
<tr>
<td><strong>잡 정의</strong></td>
<td>1줄 (<code>m h dom mon dow command</code>)</td>
<td>Python DAG 파일 (수십~수백 줄)</td>
</tr>
<tr>
<td><strong>재시도</strong></td>
<td>직접 구현 (wrapper에 retry 로직)</td>
<td>빌트인 (<code>retries</code>, <code>retry_delay</code>, exponential backoff)</td>
</tr>
<tr>
<td><strong>잡 의존성</strong></td>
<td>없음 (시간으로 순서 강제)</td>
<td>DAG 그래프로 명시 (A → B → C)</td>
</tr>
<tr>
<td><strong>백필 (backfill)</strong></td>
<td>없음</td>
<td><code>airflow dags backfill</code> 한 줄</td>
</tr>
<tr>
<td><strong>부분 실패 복구</strong></td>
<td>잡 전체 재실행</td>
<td>실패한 task만 재실행</td>
</tr>
<tr>
<td><strong>모니터링 UI</strong></td>
<td>없음 (로그 파일 직접)</td>
<td>웹 UI, 시각화, SLA 알림</td>
</tr>
<tr>
<td><strong>알림</strong></td>
<td>직접 구현 (mail, webhook)</td>
<td>EmailOperator, SlackOperator 등 빌트인</td>
</tr>
<tr>
<td><strong>분산 실행</strong></td>
<td>없음 (단일 호스트)</td>
<td>CeleryExecutor·KubernetesExecutor</td>
</tr>
<tr>
<td><strong>시각화</strong></td>
<td>없음</td>
<td>Gantt, Graph, Calendar 뷰</td>
</tr>
<tr>
<td><strong>학습 곡선</strong></td>
<td>30분</td>
<td>며칠~몇 주</td>
</tr>
<tr>
<td><strong>운영 비용</strong></td>
<td>거의 0</td>
<td>별도 클러스터 운영</td>
</tr>
</tbody></table>
<h3 id="cron으로-충분한-경우">cron으로 충분한 경우</h3>
<p>다음 <strong>모두</strong> 해당하면 cron 유지.</p>
<ul>
<li><input disabled="" type="checkbox"> 잡 수 &lt; 10개</li>
<li><input disabled="" type="checkbox"> 잡 의존성 없음 (또는 시간 순서로 강제 가능)</li>
<li><input disabled="" type="checkbox"> 모든 잡의 실행 시간 &lt; 다음 호출 간격 (락으로 보강)</li>
<li><input disabled="" type="checkbox"> 실패 시 다음 회차 재실행으로 충분 (즉시 복구 불요)</li>
<li><input disabled="" type="checkbox"> 단일 호스트에서 처리 가능</li>
<li><input disabled="" type="checkbox"> 한국 시간 기준 일정한 시각으로 충분 (윈도우·캘린더 복잡도 없음)</li>
</ul>
<h3 id="airflow가-필요한-시점">Airflow가 필요한 시점</h3>
<p>다음 신호가 <strong>하나라도</strong> 등장하면 Airflow(또는 Dagster, Prefect) 도입 고려.</p>
<table>
<thead>
<tr>
<th>신호</th>
<th>예시</th>
<th>Airflow 해결책</th>
</tr>
</thead>
<tbody><tr>
<td><strong>DAG 의존성</strong></td>
<td>&quot;추출 끝나면 변환, 변환 끝나면 적재, 적재 끝나면 알림&quot;</td>
<td>Operator chaining (<code>A &gt;&gt; B &gt;&gt; C</code>)</td>
</tr>
<tr>
<td><strong>잡 수 증가</strong></td>
<td>매일 50개 이상의 잡, 일부는 매시간·일부는 매일</td>
<td>DAG 단위 관리, 폴더 구조</td>
</tr>
<tr>
<td><strong>부분 실패</strong></td>
<td>5단계 ETL 중 3단계 실패 시 1·2를 재실행하기 싫음</td>
<td>task 단위 재실행</td>
</tr>
<tr>
<td><strong>백필</strong></td>
<td>&quot;지난 30일치 다시 돌려야 함&quot;</td>
<td><code>backfill --start-date ...</code></td>
</tr>
<tr>
<td><strong>동적 분기</strong></td>
<td>입력 크기에 따라 다른 처리 경로</td>
<td>Branch Operator</td>
</tr>
<tr>
<td><strong>다른 시스템 트리거</strong></td>
<td>&quot;Kafka 메시지 수가 임계 초과 시 잡 실행&quot;</td>
<td>Sensor (Kafka·HTTP·File)</td>
</tr>
<tr>
<td><strong>알림 통합</strong></td>
<td>실패 시 Slack·PagerDuty</td>
<td>EmailOperator·SlackWebhook</td>
</tr>
<tr>
<td><strong>SLA 추적</strong></td>
<td>&quot;이 잡은 30분 안에 완료해야 함&quot;</td>
<td>sla 파라미터 + 알림</td>
</tr>
<tr>
<td><strong>분산 실행</strong></td>
<td>한 호스트로 부족</td>
<td>Celery·Kubernetes Executor</td>
</tr>
</tbody></table>
<pre><code>Phase 1 :
  cron + bash wrapper (락·메모리 가드·로그)
  → Part 3 수준에 적합

Phase 2:
  cron + Python orchestrator
  - 더 복잡한 wrapper (DB로 잡 상태 추적)
  - Slack 알림
  → 잡 수 5~15개

Phase 3:
  Airflow (LocalExecutor, single host)
  - DAG, retry, UI
  → 잡 수 15~50개, 의존성 등장

Phase 4:
  Airflow (CeleryExecutor 또는 KubernetesExecutor)
  - 분산 워커
  → 50개 이상, 멀티 팀</code></pre><h3 id="kafka-다중-브로커">kafka 다중 브로커</h3>
<pre><code>현재 (단일):
  ┌─────────┐
  │ broker1 │ partitions: P0, P1, P2 (모두 leader)
  └─────────┘ replication: 1

확장 (3 broker, RF=3):
  ┌─────────┐  ┌─────────┐  ┌─────────┐
  │ broker1 │  │ broker2 │  │ broker3 │
  │ P0 (L)  │  │ P1 (L)  │  │ P2 (L)  │
  │ P2 (F)  │  │ P0 (F)  │  │ P0 (F)  │
  │ P1 (F)  │  │ P2 (F)  │  │ P1 (F)  │
  └─────────┘  └─────────┘  └─────────┘
  L=Leader, F=Follower (replica)</code></pre><p>도입 시 필요한 작업:</p>
<ul>
<li>추가 VM 2대 또는 컨테이너</li>
<li>모든 broker가 같은 controller quorum 공유 (<code>controller.quorum.voters=1@host1:9093,2@host2:9093,3@host3:9093</code>)</li>
<li>토픽 재생성 또는 <code>kafka-reassign-partitions.sh</code>로 replication 추가</li>
<li><code>min.insync.replicas=2</code> (3 broker, RF=3 권장)</li>
<li>Producer <code>acks=all</code> (이미 본 가이드 적용)</li>
</ul>
<h3 id="spark-분산-모드">Spark 분산 모드</h3>
<pre><code>현재 (local):
  spark-submit --master local[*]
  → driver = executor = 한 JVM
  → 메모리 = VM RAM

확장 옵션:
  ① Spark Standalone Cluster
     spark-submit --master spark://master:7077 \
                  --deploy-mode cluster \
                  --executor-memory 4g --executor-cores 2 \
                  --num-executors 5
     → 별도 master + worker 노드 (Spark 자체 클러스터 매니저)

  ② Kubernetes
     spark-submit --master k8s://https://... \
                  --deploy-mode cluster \
                  --conf spark.kubernetes.container.image=...
     → Pod로 동적 executor 생성·소멸

  ③ YARN (Hadoop 환경)
     spark-submit --master yarn --deploy-mode cluster
     → 기존 Hadoop 클러스터 자원 활용</code></pre><h3 id="delta-lake-도입">Delta Lake 도입</h3>
<p>Parquet의 한계 → Delta Lake로 ACID·time travel.</p>
<table>
<thead>
<tr>
<th>기능</th>
<th>Parquet (현재)</th>
<th>Delta Lake</th>
</tr>
</thead>
<tbody><tr>
<td><strong>ACID 트랜잭션</strong></td>
<td>없음 (실패 시 부분 파일)</td>
<td>있음 (<code>_delta_log</code>로 commit)</td>
</tr>
<tr>
<td><strong>Time Travel</strong></td>
<td>없음</td>
<td><code>VERSION AS OF</code> / <code>TIMESTAMP AS OF</code></td>
</tr>
<tr>
<td><strong>Schema Evolution</strong></td>
<td>수동 (파일 재생성)</td>
<td><code>MERGE SCHEMA</code> 자동</td>
</tr>
<tr>
<td><strong>Upsert (MERGE)</strong></td>
<td>없음</td>
<td><code>MERGE INTO ... WHEN MATCHED ...</code></td>
</tr>
<tr>
<td><strong>Optimize·Z-order</strong></td>
<td>없음</td>
<td>빌트인</td>
</tr>
<tr>
<td><strong>Streaming + Batch 통합</strong></td>
<td>어려움</td>
<td>&quot;Lambda 통합&quot;</td>
</tr>
</tbody></table>
<h4 id="전환-예시">전환 예시</h4>
<pre><code># Streaming sink를 Delta로 변경
parsed.writeStream \
  .format(&quot;delta&quot;) \
  .option(&quot;checkpointLocation&quot;, &quot;/.../checkpoints/delta&quot;) \
  .start(&quot;/.../delta/transactions_fact&quot;)

# UPSERT (배치 추론에서 사용 가능)
from delta.tables import DeltaTable
delta_pred = DeltaTable.forPath(spark, &quot;/.../delta/ml_predictions&quot;)
delta_pred.alias(&quot;p&quot;).merge(
    new_pred.alias(&quot;n&quot;),
    &quot;p.customer_id = n.customer_id&quot;
).whenMatchedUpdate(set={...}).whenNotMatchedInsert(values={...}).execute()</code></pre><p>도입 비용: <code>--packages io.delta:delta-spark_2.12:3.0.0</code>. PostgreSQL은 그대로 두고, 분석·아카이브 레이어를 Delta로 분리하는 패턴이 일반적.</p>
<h4 id="schema-registry">Schema Registry</h4>
<p>현재 Producer가 임의 JSON. → 스키마 레지스트리로 contract 강제.</p>
<pre><code>Producer        Schema Registry         Consumer
   │                 ▲                     │
   │ schema_id 조회  │  ┌─schemas:─┐       │
   ├────────────────▶│  │ v1: ... │       │
   │                 │  │ v2: ... │       │
   │                 │  └─────────┘       │
   │  msg = schema_id + binary payload     │
   ├──────────────────────────────────────▶│
                                           │
                       schema_id로 deserialize</code></pre><p>대표 옵션:</p>
<ul>
<li><strong>Confluent Schema Registry</strong>: Avro 표준</li>
<li><strong>Apicurio</strong>: Avro/Protobuf/JSON Schema 다 지원, Apache 2.0</li>
<li><strong>Karapace</strong>: Apache 2.0, Confluent 호환</li>
</ul>
<p>장점:</p>
<ul>
<li>호환성 검증 (BACKWARD/FORWARD/FULL)</li>
<li>Producer/Consumer 독립 진화</li>
<li>메시지 크기 절감 (스키마 미포함, ID만)</li>
</ul>
<h4 id="dead-letter-topic">Dead-Letter Topic</h4>
<p>현재 정제 실패 메시지는 <code>where(...)</code> 필터로 폐기. → 별도 토픽에 보관해 분석.</p>
<pre><code>              ┌───── valid → transactions_fact
              │
[정제 분기]   ┤
              │
              └───── invalid → transactions-dlq (dead-letter)</code></pre><p>구현 패턴:</p>
<pre><code class="language-python"># foreachBatch 내부
valid = batch_df.filter(...).cache()
invalid = batch_df.exceptAll(valid).cache()

# valid → PG
write_to_pg(valid)

# invalid → Kafka dead-letter 토픽 (또는 PG 별도 테이블)
invalid.selectExpr(&quot;CAST(transaction_id AS STRING) AS key&quot;,
                   &quot;to_json(struct(*)) AS value&quot;) \
       .write.format(&quot;kafka&quot;) \
       .option(&quot;topic&quot;, &quot;transactions-dlq&quot;) \
       .save()</code></pre>
<p>dead-letter 분석으로 Producer 버그·외부 시스템 변경 조기 탐지.</p>
<h4 id="모니터링-스택-prometheus--grafana">모니터링 스택 (Prometheus + Grafana)</h4>
<p>현재 사람이 SQL·CLI로 조회 → 자동 메트릭 수집·시각화·알림.</p>
<pre><code>┌────────────────────────────────────────────────────────┐
│ ① 메트릭 노출 (각 컴포넌트 → /metrics endpoint)         │
│    - PG       : postgres_exporter                       │
│    - Kafka    : JMX exporter                            │
│    - Spark    : spark.metrics.conf                      │
│    - Node     : node_exporter (메모리·디스크·CPU)       │
│    - 사용자정의 : pushgateway                           │
└────────────────────────────────────────────────────────┘
                          │
                          ▼
┌────────────────────────────────────────────────────────┐
│ ② Prometheus (시계열 DB) - 주기적 scrape, 저장         │
└────────────────────────────────────────────────────────┘
                          │
                          ▼
┌────────────────────────────────────────────────────────┐
│ ③ Grafana (시각화) - 대시보드, 알림 룰                 │
└────────────────────────────────────────────────────────┘
                          │
                          ▼
┌────────────────────────────────────────────────────────┐
│ ④ Alertmanager - Slack·PagerDuty·Email                │
└────────────────────────────────────────────────────────┘</code></pre><h4 id="확장-우선순위-현재-→-운영으로-가려면">확장 우선순위 (현재 → 운영으로 가려면)</h4>
<p>다음 순서를 권장:</p>
<ol>
<li><strong>Schema Registry</strong> — 데이터 contract 강제</li>
<li><strong>Dead-Letter</strong> — 실패 분석·신뢰도 향상</li>
<li><strong>Prometheus + Grafana</strong> — 정량 모니터링</li>
<li><strong>PG replica + 백업 자동화</strong> — 데이터 안전성</li>
<li><strong>Kafka 3-broker</strong> — Kafka 장애 내성</li>
<li><strong>Spark 분산</strong> — 처리량이 한계 도달 시</li>
<li><strong>Delta Lake</strong> — 분석 레이어 분리 시</li>
</ol>
<hr>
<h1 id="쇼핑몰-인프라-핵심-기술-정리">쇼핑몰 인프라 핵심 기술 정리</h1>
<h2 id="elasticsearch">Elasticsearch</h2>
<h3 id="왜-사용하는가">왜 사용하는가?</h3>
<p>RDBMS는 일반적인 CRUD에는 강하지만 검색 엔진 역할에는 한계가 있다.</p>
<p>예를 들어:</p>
<pre><code class="language-sql">LIKE &#39;%맥북%&#39;</code></pre>
<p>같은 쿼리는 인덱스를 제대로 활용하지 못해 대규모 데이터에서 매우 느려진다.</p>
<p>특히 쇼핑몰에서는:</p>
<ul>
<li>상품 10억 건 규모</li>
<li>한국어 형태소 분석</li>
<li>오타 보정</li>
<li>카테고리 + 가격 + 태그 조합 검색</li>
<li>검색어 적합도 순 정렬</li>
</ul>
<p>등이 필요하기 때문에 Elasticsearch를 별도로 둔다. </p>
<hr>
<h2 id="elasticsearch의-핵심-특징">Elasticsearch의 핵심 특징</h2>
<h3 id="1-역색인inverted-index">1. 역색인(Inverted Index)</h3>
<p>일반 DB:</p>
<pre><code class="language-text">문서 → 단어</code></pre>
<p>Elasticsearch:</p>
<pre><code class="language-text">단어 → 문서</code></pre>
<p>형태로 저장한다.</p>
<p>즉:</p>
<pre><code class="language-text">&quot;맥북&quot;이라는 단어가 들어간 문서 목록</code></pre>
<p>을 미리 만들어두기 때문에 검색이 매우 빠르다.</p>
<hr>
<h3 id="2-형태소-분석">2. 형태소 분석</h3>
<p>한국어는 띄어쓰기·조사·어미 변화가 많다.</p>
<p>예:</p>
<pre><code class="language-text">노트북을
노트북이
노트북용</code></pre>
<p>Elasticsearch는:</p>
<ul>
<li>Nori</li>
<li>Mecab-ko</li>
</ul>
<p>같은 분석기를 통해 단어를 분리하고 정규화할 수 있다. </p>
<hr>
<h3 id="3-relevance-score">3. Relevance Score</h3>
<p>단순 포함 여부가 아니라:</p>
<pre><code class="language-text">검색어와 얼마나 관련 있는가</code></pre>
<p>를 계산해 정렬한다.</p>
<p>예:</p>
<ul>
<li>제목에 포함 → 점수 높음</li>
<li>설명에만 포함 → 점수 낮음</li>
</ul>
<hr>
<h2 id="cdc-change-data-capture">CDC (Change Data Capture)</h2>
<p>문제:</p>
<pre><code class="language-text">상품 DB와 Elasticsearch 데이터를 어떻게 동기화할 것인가?</code></pre>
<p>직접 애플리케이션 코드에서:</p>
<ul>
<li>DB 저장</li>
<li>ES 저장</li>
</ul>
<p>둘 다 처리하면 결합도가 커진다.</p>
<hr>
<h2 id="debezium--kafka-구조">Debezium + Kafka 구조</h2>
<pre><code class="language-text">MySQL binlog
    ↓
Debezium
    ↓
Kafka
    ↓
Elasticsearch</code></pre>
<hr>
<h3 id="debezium-역할">Debezium 역할</h3>
<p>DB binlog를 읽어:</p>
<ul>
<li>INSERT</li>
<li>UPDATE</li>
<li>DELETE</li>
</ul>
<p>변경 이벤트를 추출한다.</p>
<p>즉:</p>
<pre><code class="language-text">DB 변경사항을 이벤트로 변환</code></pre>
<p>하는 역할이다.</p>
<hr>
<h3 id="kafka-역할">Kafka 역할</h3>
<p>변경 이벤트를 버퍼링·전달한다.</p>
<p>장점:</p>
<ul>
<li>대량 이벤트 처리 가능</li>
<li>Consumer 분리 가능</li>
<li>재처리 가능</li>
</ul>
<hr>
<h1 id="redis">Redis</h1>
<h2 id="redis를-사용하는-이유">Redis를 사용하는 이유</h2>
<p>Redis는 메모리 기반 저장소이다.</p>
<p>특징:</p>
<ul>
<li>매우 빠름</li>
<li>읽기 TPS 높음</li>
<li>단순 조회에 강함</li>
</ul>
<p>쇼핑몰처럼:</p>
<ul>
<li>조회량은 많고</li>
<li>데이터 변경은 상대적으로 적은</li>
</ul>
<p>환경에 매우 적합하다.</p>
<hr>
<h1 id="redis-캐시-전략">Redis 캐시 전략</h1>
<p>이 자료에서는:</p>
<pre><code class="language-text">모든 데이터를 동일하게 캐싱하지 않는다</code></pre>
<p>는 점이 중요하다. </p>
<p>데이터 성격에 따라 전략을 다르게 가져간다.</p>
<hr>
<h2 id="1-메인-페이지-상품">1. 메인 페이지 상품</h2>
<p>특징:</p>
<ul>
<li>모든 사용자 동일 데이터</li>
<li>1시간마다 갱신</li>
</ul>
<p>전략:</p>
<pre><code class="language-text">Write-Through
TTL 1시간</code></pre>
<hr>
<h3 id="write-through">Write-Through</h3>
<p>DB 업데이트 시:</p>
<ul>
<li>Redis도 같이 갱신</li>
</ul>
<p>즉:</p>
<pre><code class="language-text">데이터 생성 시 캐시도 함께 생성</code></pre>
<p>하는 방식.</p>
<hr>
<h3 id="ttltime-to-live">TTL(Time To Live)</h3>
<p>캐시에 만료 시간을 둔다.</p>
<pre><code class="language-text">1시간 후 자동 삭제</code></pre>
<p>이 자료에서는:</p>
<ul>
<li>매시간 배치 실행</li>
<li>Redis 갱신</li>
<li>TTL 1시간</li>
</ul>
<p>전략을 사용한다. </p>
<hr>
<h2 id="2-카테고리-트리">2. 카테고리 트리</h2>
<p>특징:</p>
<ul>
<li>거의 안 바뀜</li>
<li>변경 시 즉시 반영 필요</li>
</ul>
<p>전략:</p>
<pre><code class="language-text">Read-Through
+ Explicit Invalidate</code></pre>
<hr>
<h3 id="read-through">Read-Through</h3>
<p>조회 시:</p>
<ol>
<li>Redis 확인</li>
<li>없으면 DB 조회</li>
<li>Redis 저장</li>
</ol>
<hr>
<h3 id="explicit-invalidate">Explicit Invalidate</h3>
<p>관리자가 카테고리를 수정하면:</p>
<pre><code class="language-text">캐시를 직접 삭제</code></pre>
<p>한다.</p>
<p>즉 TTL만 믿지 않고:</p>
<ul>
<li>변경 이벤트 기반으로 캐시 제거</li>
</ul>
<p>전략을 사용한다. </p>
<hr>
<h2 id="3-평점·리뷰-수">3. 평점·리뷰 수</h2>
<p>문제:</p>
<pre><code class="language-text">리뷰 작성마다 평균 계산</code></pre>
<p>을 하면:</p>
<ul>
<li>락 경합</li>
<li>DB 부하</li>
</ul>
<p>문제가 생긴다.</p>
<hr>
<h2 id="해결-전략">해결 전략</h2>
<pre><code class="language-text">30분 배치 집계
→ Redis 저장</code></pre>
<p>즉:</p>
<ul>
<li>실시간 정확성보다</li>
<li>안정성과 성능</li>
</ul>
<p>을 우선한다. </p>
<hr>
<h1 id="메시지-큐message-queue">메시지 큐(Message Queue)</h1>
<h2 id="왜-필요한가">왜 필요한가?</h2>
<p>주문 API는 빠르게 응답해야 한다.</p>
<p>하지만:</p>
<ul>
<li>이메일 발송</li>
<li>판매자 알림</li>
</ul>
<p>은 느릴 수 있다.</p>
<p>이걸 동기로 처리하면:</p>
<ul>
<li>응답 지연</li>
<li>타임아웃</li>
</ul>
<p>이 발생한다. </p>
<hr>
<h1 id="비동기-처리-구조">비동기 처리 구조</h1>
<pre><code class="language-text">주문 API
  ↓
Message Queue
  ↓
Worker</code></pre>
<p>핵심 아이디어:</p>
<pre><code class="language-text">&quot;일단 큐에 넣고 응답 먼저&quot;</code></pre>
<p>이다.</p>
<hr>
<h2 id="queue의-장점">Queue의 장점</h2>
<h3 id="1-트래픽-평탄화traffic-smoothing">1. 트래픽 평탄화(Traffic Smoothing)</h3>
<p>순간 TPS 급증을 큐가 흡수한다.</p>
<hr>
<h3 id="2-시스템-분리">2. 시스템 분리</h3>
<p>주문 시스템과 알림 시스템이 독립된다.</p>
<hr>
<h3 id="3-재시도-가능">3. 재시도 가능</h3>
<p>실패 메시지는:</p>
<ul>
<li>재시도</li>
<li>DLQ(Dead Letter Queue)</li>
</ul>
<p>로 관리 가능하다.</p>
<hr>
<h1 id="sqs-vs-rabbitmq-vs-kafka">SQS vs RabbitMQ vs Kafka</h1>
<h2 id="sqs">SQS</h2>
<p>특징:</p>
<ul>
<li>AWS 관리형</li>
<li>운영 부담 적음</li>
<li>DLQ 기본 제공</li>
</ul>
<p>이 자료에서는:</p>
<ul>
<li>이메일</li>
<li>판매자 알림</li>
</ul>
<p>같은 단순 비동기에 적합하다고 평가했다. </p>
<hr>
<h2 id="rabbitmq">RabbitMQ</h2>
<p>특징:</p>
<ul>
<li>복잡한 라우팅 가능</li>
<li>낮은 지연시간</li>
</ul>
<p>단점:</p>
<ul>
<li>직접 운영 필요</li>
</ul>
<hr>
<h2 id="kafka">Kafka</h2>
<p>특징:</p>
<ul>
<li>초고성능 이벤트 스트리밍</li>
<li>이벤트 재처리 가능</li>
<li>CDC와 매우 잘 맞음</li>
</ul>
<p>하지만:</p>
<ul>
<li>운영 복잡도 높음</li>
<li>단순 큐 용도로는 과함</li>
</ul>
<p>이라는 특징이 있다. </p>
<hr>
<h1 id="hot--warm--cold-아카이빙">Hot / Warm / Cold 아카이빙</h1>
<h2 id="핵심-아이디어">핵심 아이디어</h2>
<p>모든 데이터를 비싼 DB에 둘 필요는 없다.</p>
<p>대부분 오래된 데이터는 거의 조회되지 않는다. </p>
<hr>
<h1 id="계층-분리">계층 분리</h1>
<h2 id="hot">HOT</h2>
<pre><code class="language-text">최근 30일</code></pre>
<ul>
<li>RDS</li>
<li>빠른 조회</li>
<li>고비용</li>
</ul>
<hr>
<h2 id="warm">WARM</h2>
<pre><code class="language-text">30일 ~ 2년</code></pre>
<ul>
<li>샤딩 DB</li>
<li>일부 인덱스 유지</li>
</ul>
<hr>
<h2 id="cold">COLD</h2>
<pre><code class="language-text">2년 ~ 5년</code></pre>
<ul>
<li>S3 Parquet</li>
<li>Athena 조회</li>
</ul>
<p>특징:</p>
<ul>
<li>저비용</li>
<li>느린 조회 허용</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MicrosoftDataSchool] 85일차 - AzureVM에 Spark]]></title>
            <link>https://velog.io/@rudin_/MicrosoftDataSchool-85%EC%9D%BC%EC%B0%A8-AzureVM%EC%97%90-Spark</link>
            <guid>https://velog.io/@rudin_/MicrosoftDataSchool-85%EC%9D%BC%EC%B0%A8-AzureVM%EC%97%90-Spark</guid>
            <pubDate>Mon, 11 May 2026 03:39:35 GMT</pubDate>
            <description><![CDATA[<h1 id="사용하게되는-배경">사용하게되는 배경</h1>
<ol>
<li>데이터가 너무 커진 경우 → 배치 처리</li>
<li>sklearn으로는 학습이 안끝나 분산 ML이 필요한 경우 → ML 파이프라인</li>
<li>실시간(1분내)로 보고받고 싶은 경우 → 스트리밍</li>
</ol>
<p><img src="https://velog.velcdn.com/images/rudin_/post/974bd398-5989-4136-998f-09cf215d229b/image.png" alt=""></p>
<h1 id="1-배치처리">1. 배치처리</h1>
<h2 id="분산-처리">분산 처리</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>단일 서버</th>
<th>분산 처리</th>
</tr>
</thead>
<tbody><tr>
<td>구조</td>
<td>고사양 서버 1대 사용</td>
<td>여러 대의 서버를 묶어 처리</td>
</tr>
<tr>
<td>데이터 처리</td>
<td>메모리 부족 시 전체 데이터 적재 어려움</td>
<td>데이터를 여러 노드에 분산 저장</td>
</tr>
<tr>
<td>확장 방식</td>
<td>수직 확장 (CPU/메모리 증설)</td>
<td>수평 확장 (노드 추가)</td>
</tr>
<tr>
<td>장애 대응</td>
<td>서버 1대 장애 시 전체 중단 위험</td>
<td>일부 노드 장애 시 다른 노드가 대체</td>
</tr>
<tr>
<td>비용 구조</td>
<td>고사양 서버 비용 급증</td>
<td>일반 서버 여러 대로 비용 효율</td>
</tr>
<tr>
<td>재시작 비용</td>
<td>처음부터 다시 읽어야 함</td>
<td>캐시 및 분산 저장 활용 가능</td>
</tr>
<tr>
<td>예시</td>
<td>16코어 / 64GB 서버 1대로 100GB 처리</td>
<td>8코어 서버 10대로 데이터 분산 처리</td>
</tr>
</tbody></table>
<h2 id="spark-아키텍처">Spark 아키텍처</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/707f2a2e-e754-447b-b676-cd349ae97c0d/image.png" alt="">
Driver은 1개, Worker Node는 여러개. 작업이 끝나면 Worker Node는 자원을 반납</p>
<h3 id="핵심-추상화">핵심 추상화</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/bbbcc204-153e-4897-a05d-f87f262413a6/image.png" alt=""></p>
<h3 id="pandas-df-sql">Pandas, DF, SQL</h3>
<table>
<thead>
<tr>
<th>비교 항목</th>
<th>Pandas</th>
<th>Spark DataFrame</th>
<th>Spark SQL</th>
</tr>
</thead>
<tbody><tr>
<td>실행 환경</td>
<td>단일 서버 메모리</td>
<td>여러 노드 분산</td>
<td>여러 노드 분산</td>
</tr>
<tr>
<td>처리 한계</td>
<td>수 GB</td>
<td>수십 ~ 수백 TB</td>
<td>수십 ~ 수백 TB</td>
</tr>
<tr>
<td>문법 친밀도</td>
<td>매우 높음 (Python)</td>
<td>Pandas와 유사</td>
<td>SQL 기반, 분석 친화</td>
</tr>
<tr>
<td>최적화</td>
<td>수동</td>
<td>자동 (Catalyst)</td>
<td>자동 (Catalyst)</td>
</tr>
<tr>
<td>대화형 분석</td>
<td>Jupyter 최적</td>
<td>Notebook 가능</td>
<td>BI 도구와 직접 연결</td>
</tr>
</tbody></table>
<h3 id="지연-실행lazy-evaluation">지연 실행(Lazy Evaluation)</h3>
<p>Spark에서는 <code>Transformation</code> 연산을 수행한다고 해서 즉시 데이터 처리가 실행되지 않는다.
<code>filter()</code>, <code>select()</code>, <code>groupBy()</code>, <code>join()</code>, <code>orderBy()</code> 와 같은 연산은 실제 계산을 수행하는 것이 아니라, 어떤 작업을 수행할지에 대한 실행 계획만 생성한다.</p>
<pre><code class="language-python">df = spark.read.parquet(&#39;orders/&#39;)
df2 = df.filter(df.amount &gt; 100)
df3 = df2.groupBy(&#39;city&#39;).count()
df4 = df3.orderBy(&#39;count&#39;, desc=True)</code></pre>
<p>이 과정에서는 실제로 데이터 파일을 읽거나 계산하지 않는다.
Spark는 단지 “데이터를 읽고 → 필터링하고 → 그룹화하고 → 정렬한다” 라는 작업 흐름(DAG, Directed Acyclic Graph)만 내부적으로 구성한다.</p>
<p>즉, 위 코드는 실행 계획만 수립한 상태이며, 아직 클러스터 자원도 거의 사용하지 않는다.</p>
<p>실제 실행은 <code>Action</code> 연산이 호출되는 순간 발생한다.</p>
<pre><code class="language-python">df4.show(20)</code></pre>
<p><code>show()</code>가 실행되는 시점에 Spark는 지금까지 쌓아둔 Transformation 작업들을 하나의 최적화된 실행 계획으로 구성한 뒤 한 번에 수행한다.
이때 Spark 내부에서는 다음과 같은 작업이 실제로 실행된다.</p>
<ul>
<li>데이터 읽기(read)</li>
<li>filter 수행</li>
<li>groupBy 집계</li>
<li>정렬(sort)</li>
<li>limit 처리</li>
</ul>
<p>이처럼 Spark는 필요한 순간까지 실행을 미루었다가(Action 호출 시점) 최적화된 형태로 한 번에 처리하는데, 이를 <strong>지연 실행(Lazy Evaluation)</strong> 이라고 한다.</p>
<h4 id="대표적인-action-연산">대표적인 Action 연산</h4>
<ul>
<li><code>show()</code></li>
<li><code>collect()</code></li>
<li><code>count()</code></li>
<li><code>first()</code></li>
<li><code>take()</code></li>
<li><code>foreach()</code></li>
<li><code>toPandas()</code></li>
<li><code>write.parquet()</code></li>
<li><code>write.format()</code></li>
</ul>
<h3 id="spark-ui-읽는-법">Spark UI 읽는 법</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/2237f40b-0ad5-4568-8aff-7c8cf251a84a/image.png" alt=""></p>
<h2 id="실습-순서">실습 순서</h2>
<ol>
<li>Parquet 읽고 스키마 확인</li>
</ol>
<p>transactions.parquet 5M 행 읽기, printSchema(), describe() </p>
<hr>
<ol start="2">
<li>집계와 groupBy</li>
</ol>
<p>고객별 거래 횟수, 지역별 매출 합계 — DataFrame API와 SQL을 같은 결과로 비교</p>
<hr>
<ol start="3">
<li>조인과 윈도우 함수</li>
</ol>
<p>customers ⋈ transactions, 고객별 누적 매출 랭킹 — Window.partitionBy() </p>
<hr>
<ol start="4">
<li>Spark UI 분석</li>
</ol>
<p>방금 돌린 쿼리의 Job/Stage/Task 확인, 셔플 크기, 쿼리 플랜 읽기</p>
<h2 id="자주-발생하는-문제">자주 발생하는 문제</h2>
<h3 id="작은-파일-문제">작은 파일 문제</h3>
<p>Kafka 적재 결과를 그대로 Parquet으로 저장하면 수만 개의 작은 파일이 생깁니다. 다음 배치 작업의 메타데이터 로딩이 데이터 처리보다 오래 걸리는 사태가 발생합니다.</p>
<p>→ 해법: <code>coalesce(N)</code> 또는 <code>OPTIMIZE/compaction</code></p>
<hr>
<h3 id="셔플shuffle-과다">셔플(shuffle) 과다</h3>
<p><code>groupBy</code>, <code>join</code>, <code>repartition</code>은 네트워크로 데이터를 재분배합니다. 1TB 셔플 = 1TB 네트워크 트래픽 + 디스크 IO. 가장 비싼 연산입니다.</p>
<p>→ 해법: 셔플 전 filter로 데이터 줄이기, 적절한 partition 수</p>
<hr>
<h3 id="broadcast-join을-안-쓸-때">broadcast join을 안 쓸 때</h3>
<p>작은 테이블(&lt; 100MB)과 큰 테이블을 join할 때 broadcast hint를 안 주면 양쪽 모두 셔플됩니다.</p>
<p>→ 해법: <code>broadcast(small_df)</code> 명시 또는 <code>spark.sql.autoBroadcastJoinThreshold</code> 조정</p>
<hr>


<h1 id="ml-파이프라인">ML 파이프라인</h1>
<p><img src="https://velog.velcdn.com/images/rudin_/post/0afa01d6-12ba-4c80-9bbb-599f536212c3/image.png" alt=""></p>
<h2 id="sklearn이-아닌-spark-mllib를--쓰는-이유">sklearn이 아닌 Spark MLlib를  쓰는 이유</h2>
<p>안되는 순간이 온다.
<img src="https://velog.velcdn.com/images/rudin_/post/d376d1cf-0dd1-4a0a-95e0-9324459d12d8/image.png" alt=""></p>
<h2 id="pipelinemodel">PipelineModel</h2>
<p>Transformer는 변환, Estimator는 학습. 둘을 합쳐서 PipelineModel
<img src="https://velog.velcdn.com/images/rudin_/post/bb048fbe-22cc-4ee4-baef-eba1d435a412/image.png" alt=""></p>
<h2 id="범주형-처리---stringindexer--onehotencoder">범주형 처리 - StringIndexer + OneHotEncoder</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/c1d76d68-d80e-42b9-8260-0a59a2ede6cf/image.png" alt=""></p>
<ul>
<li>StringIndexer만 쓰면 <code>서울=0, 부산=1, 대구=2</code>가 되어 모델이 대구 &gt; 서울이라고 잘못 학습</li>
</ul>
<h2 id="handelinvalidkeep">handelInvalid=&#39;keep&#39;</h2>
<p>학습에 없던 카테고리가 운영에서 들어와도 모델이 죽지 않도록 하는 옵션
<img src="https://velog.velcdn.com/images/rudin_/post/732f3e7c-4c55-407b-8578-0b4a08d182a3/image.png" alt=""></p>
<h2 id="모델-평가-지표">모델 평가 지표</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/7def516a-1f02-49e5-87c7-3d538a6324a7/image.png" alt=""></p>
<h2 id="모델-저장과-로드">모델 저장과 로드</h2>
<blockquote>
<p>노트북에서 학습한 모델을 다른 곳에서 쓰는 방법</p>
</blockquote>
<hr>
<h3 id="학습-환경-노트북gpu-클러스터">학습 환경 (노트북/GPU 클러스터)</h3>
<pre><code class="language-python"># 학습 후 저장
pipeline = Pipeline(stages=[...])
model = pipeline.fit(trainDF)

# 디스크/스토리지에 저장
model.write().overwrite() \
  .save(&#39;s3://models/churn/v1&#39;)

# 또는 MLflow 레지스트리
mlflow.spark.log_model(model)
</code></pre>
<ul>
<li>모든 Transformer + Estimator 상태 저장</li>
<li>학습한 인덱싱 사전, 가중치, 분기 규칙 모두</li>
<li>MLflow 사용시 버전 관리 자동</li>
</ul>
<hr>
<h3 id="서빙-환경-실시간-추론-클러스터">서빙 환경 (실시간 추론 클러스터)</h3>
<pre><code class="language-python"># 운영 환경에서 로드
from pyspark.ml import PipelineModel

model = PipelineModel.load(
  &#39;s3://models/churn/v1&#39;
)

# 새 데이터에 그대로 적용
predictions = model.transform(newCustomers)
predictions.select(&#39;id&#39;, &#39;prediction&#39;).show()
</code></pre>
<ul>
<li>학습 코드의 전처리 그대로 재현됨</li>
<li>환경만 다르면 됨 — 코드는 동일</li>
<li><strong>Part 3 스트리밍에서도 그대로 사용</strong></li>
</ul>
<h1 id="실시간-스트리밍---kafka--structured-streaming">실시간 스트리밍 - Kafka + Structured Streaming</h1>
<p><img src="https://velog.velcdn.com/images/rudin_/post/ffd37f5c-f9ae-47b8-97c4-2557b4a4f9af/image.png" alt=""></p>
<h2 id="배치-batch">배치 Batch</h2>
<p><strong>특징</strong></p>
<ul>
<li>데이터를 모아서 한 번에 처리</li>
<li>처리량 지연: 분~시간 단위</li>
<li>처리량이 크고 비용 효율적</li>
<li>데이터 경계가 명확 (시작/끝)</li>
</ul>
<p><strong>언제 쓰나</strong></p>
<ul>
<li>$\rightarrow$ 일배치 리포트, 월말 정산, ML 학습</li>
<li>$\rightarrow$ &quot;어제까지의 데이터로 OK&quot;인 경우</li>
<li>$\rightarrow$ DataFrame · spark.read.parquet()</li>
</ul>
<hr>
<h2 id="스트리밍-streaming">스트리밍 Streaming</h2>
<p><strong>특징</strong></p>
<ul>
<li>데이터가 도착하는 즉시 처리</li>
<li>지연: 초~분 단위</li>
<li>24시간 무중단, 운영 부담 $\uparrow$</li>
<li>데이터 경계가 모호 (스트림은 끝이 없음)</li>
</ul>
<p><strong>언제 쓰나</strong></p>
<ul>
<li>$\rightarrow$ 실시간 추천, 이상 탐지, 알림</li>
<li>$\rightarrow$ &quot;지금 막 일어난 일에 반응&quot; 필요</li>
<li>$\rightarrow$ spark.readStream.format(&quot;kafka&quot;)</li>
</ul>
<h2 id="lambda-아키텍처">Lambda 아키텍처</h2>
<p>배치의 정확성 + 스트리밍 즉시성</p>
<p><img src="https://velog.velcdn.com/images/rudin_/post/2d5a0ab4-b2f3-441b-837a-234ba7f2f530/image.png" alt=""></p>
<h2 id="kafka">Kafka</h2>
<blockquote>
<p>분산 메시지 큐의 표준</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/rudin_/post/187fcdd0-2a5b-48a3-a5c0-b1d6ef84752f/image.png" alt=""></p>
<h3 id="필요한-이유">필요한 이유</h3>
<p>DB에 직접 쓰면 안되는 이유
제공해주신 이미지 속에 적힌 텍스트를 그대로 추출하여 정리해 드립니다.</p>
<hr>
<h3 id="1-트래픽-흡수">1. 트래픽 흡수</h3>
<p><strong>Peak 시점 보호</strong>
광고 폭주, 이벤트로 초당 트래픽이 10배 뛰어도 Kafka가 흡수.
Consumer는 자기 속도로 처리.
$\rightarrow$ DB가 죽지 않음</p>
<hr>
<h3 id="2-데이터-내구성">2. 데이터 내구성</h3>
<p><strong>재처리 가능</strong>
메시지는 디스크에 저장 $\rightarrow$ Consumer 장애 시 처음부터 재처리.
일주일 전 데이터도 다시 읽기 가능.
$\rightarrow$ 데이터 손실 없음</p>
<hr>
<h3 id="3-다중-소비자">3. 다중 소비자</h3>
<p><strong>Pub/Sub</strong>
한 번 적재한 데이터를 Spark Streaming, 알림 시스템, 분석 DB가 각자 독립 소비.
Producer는 누가 읽는지 몰라도 됨.
$\rightarrow$ 소비자 추가 자유</p>
<hr>
<h3 id="4-비동기-분리">4. 비동기 분리</h3>
<p><strong>느슨한 결합</strong>
Producer와 Consumer가 같은 시간에 살 필요 없음.
Consumer가 잠시 죽어도 Producer는 계속 발행.
$\rightarrow$ 시스템 독립성</p>
<h2 id="structured-streaming">Structured Streaming</h2>
<blockquote>
<p>실시간 스트림 = 무한히 자라는 테이블. 
그 테이블에 대한 SQL은 짧은 주기로 다시 실행됩니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/rudin_/post/6617e03e-d850-469b-b5d5-b08a03b85fd5/image.png" alt=""></p>
<h2 id="체크포인트와-멱등성">체크포인트와 멱등성</h2>
<h3 id="checkpoint">Checkpoint</h3>
<p><strong>Spark가 처리 상태를 디스크에 주기 저장</strong></p>
<pre><code class="language-python">query = df.writeStream \
  .option(
    &#39;checkpointLocation&#39;,
    &#39;s3://checkpoints/job1/&#39;
  ) \
  .start()
</code></pre>
<p><strong>저장되는 것</strong></p>
<ul>
<li>어디까지 처리했는가 (Kafka offset)</li>
<li>진행 중이던 집계 상태</li>
<li>메타데이터 로그</li>
<li><em>$\rightarrow$ 재시작 시 정확히 그 지점부터 이어서 처리*</em></li>
</ul>
<hr>
<h3 id="멱등성-idempotency">멱등성 (Idempotency)</h3>
<p><strong>같은 메시지가 두 번 와도 결과 같게 만들기</strong></p>
<p><strong>왜 필요한가</strong>
Kafka는 &quot;at-least-once&quot; 보장. 장애 복구 시 같은 메시지가 두 번 처리될 수 있음.</p>
<p><strong>멱등 적재 패턴</strong></p>
<ul>
<li>Primary Key 기반 upsert (MERGE)</li>
<li>Delta Lake MERGE INTO 활용</li>
<li>메시지 ID로 중복 제거</li>
</ul>
<blockquote>
<p><strong>Checkpoint + 멱등 = effectively exactly-once</strong></p>
</blockquote>
<hr>

<h1 id="실습">실습</h1>
<p><img src="https://velog.velcdn.com/images/rudin_/post/7e2a8383-71da-44e7-89e5-5808fe15585e/image.png" alt=""></p>
<p>Docker로 Kafka 띄우기 $\rightarrow$ Producer 만들기 $\rightarrow$ Topic 설계 $\rightarrow$ Partition 실험</p>
<p>Structured Streaming 첫 쿼리 $\rightarrow$ JSON 파싱 $\rightarrow$ 윈도우 집계 $\rightarrow$ trigger 비교</p>
<p>Part 2의 PipelineModel 로드 $\rightarrow$ 실시간 추론 $\rightarrow$ PostgreSQL 멱등 적재</p>
<p>프로듀서 강제 종료 $\rightarrow$ 체크포인트 복구 검증 $\rightarrow$ exactly-once 확인</p>
<hr>
<h2 id="환경-준비">환경 준비</h2>
<ol>
<li>azure portal에서 vm 생성</li>
<li>쉘에서 ssh로 접속</li>
<li>가상머신에 라이브러리 설치</li>
</ol>
<ul>
<li><code>APT (Advanced Package Tool)</code>: Debian/Ubuntu 계열 패키지 관리자. apt update로 패키지 목록 갱신, apt upgrade로 실제 업그레이드.</li>
<li><code>OpenJDK 17</code>: 오라클 JDK의 오픈소스 구현. Spark 3.5.x는 Java 8/11/17 공식 지원.</li>
<li><code>JDK vs JRE</code>: JDK = JRE + 컴파일러/디버거. 개발용은 JDK, 실행만 한다면 JRE. Spark도 PySpark 호출 시 내부적으로 JVM 실행이 필요하므로 JDK 권장.</li>
<li><code>PEP 668 (Externally Managed Environment)</code>: Ubuntu 23.04부터 시스템 Python에 pip install을 직접 막는 정책. 시스템 패키지(apt)와 pip 패키지 충돌로 인한 OS 손상 방지가 목적. 해결책은 venv 가상환경 사용 (권장) 또는 --break-system-packages 플래그 (비권장).<pre><code>sudo apt update &amp;&amp; sudo apt upgrade -y
sudo apt install -y openjdk-17-jdk
sudo apt install -y python3-pip python3-venv python3-full wget curl
python3 -m venv ~/sparkenv
source ~/sparkenv/bin/activate</code></pre></li>
</ul>
<h2 id="spark-설치">Spark 설치</h2>
<ul>
<li><code>Apache Spark</code>: 분산 데이터 처리 엔진. 메모리 기반 연산으로 Hadoop MapReduce 대비 빠름. SQL/스트리밍/ML/그래프 통합 API 제공.</li>
<li><code>Spark 3.5.8</code>: 3.5 LTS 라인의 최신 maintenance 릴리스 (2027.11까지 보안 패치). Java 17 정식 지원.</li>
<li><code>Hadoop3 prebuilt</code>: Spark는 Hadoop의 HDFS 클라이언트 라이브러리를 사용해 다양한 스토리지(S3, ADLS 등) 접근. bin-hadoop3 패키지는 Hadoop 3.x 라이브러리가 포함된 사전 빌드 버전 → 별도 빌드 불필요.</li>
<li><code>Standalone 모드</code>: Spark 자체 클러스터 매니저. 본 핸즈온은 단일 노드에서 standalone(또는 local) 모드로 동작.</li>
<li><code>/opt</code>: Linux 전통적으로 third-party 애플리케이션 설치 디렉터리. /usr/local도 가능하나 /opt가 더 격리적.</li>
</ul>
<pre><code>cd ~
wget https://dlcdn.apache.org/spark/spark-3.5.8/spark-3.5.8-bin-hadoop3.tgz

tar -xzf spark-3.5.8-bin-hadoop3.tgz
sudo mv spark-3.5.8-bin-hadoop3 /opt/spark
sudo chown -R azureuser:azureuser /opt/spark
rm spark-3.5.8-bin-hadoop3.tgz</code></pre><hr>
<h3 id="📂-리눅스-주요-디렉토리-구조">📂 리눅스 주요 디렉토리 구조</h3>
<h4 id="1-opt-optional">1. /opt (Optional)</h4>
<ul>
<li><strong>의미:</strong> 추가적인 <strong>독립 소프트웨어 패키지</strong>가 설치되는 곳입니다.</li>
<li><strong>용도:</strong> 시스템 기본 패키지 관리자(apt)가 관리하지 않는, 외부 서드파티 애플리케이션(예: Google Chrome, 전용 데이터베이스, 특정 벤더의 도구 등)이 설치됩니다.</li>
<li><strong>특징:</strong> 보통 한 프로그램이 하나의 하위 디렉토리에 모든 파일(bin, lib 등)을 통째로 가지고 있는 경우가 많습니다.</li>
</ul>
<h4 id="2-var-variable">2. /var (Variable)</h4>
<ul>
<li><strong>의미:</strong> 시스템 운영 중 내용이 <strong>시시각각 변하는 파일들</strong>이 저장되는 곳입니다.</li>
<li><strong>용도:</strong> <em>/var/log: 시스템 및 애플리케이션의 *</em>로그 파일** (가장 자주 확인하게 되는 곳).</li>
<li>/var/lib: 데이터베이스나 패키지 상태 정보.</li>
<li>/var/spool: 메일이나 인쇄 대기열.</li>
</ul>
<ul>
<li><strong>특징:</strong> 용량이 계속 늘어날 수 있는 데이터가 많아, 서버 구축 시 별도의 파티션으로 분리하기도 합니다.</li>
</ul>
<h4 id="3-etc-et-cetera--editable-text-configuration">3. /etc (Et cetera / Editable Text Configuration)</h4>
<ul>
<li><strong>의미:</strong> 시스템 전체의 설정 파일(Configuration files)이 들어있는 곳입니다.</li>
<li><strong>용도:</strong> 네트워크 설정, 사용자 비밀번호 파일, 설치된 프로그램의 설정값(.conf) 등이 위치합니다.</li>
<li><strong>특징:</strong> 텍스트 파일로 되어 있어 관리자가 직접 수정할 수 있습니다.</li>
</ul>
<h4 id="4-usr-unix-system-resources">4. /usr (Unix System Resources)</h4>
<ul>
<li><strong>의미:</strong> 사용자가 실행하는 대부분의 <strong>프로그램과 읽기 전용 데이터</strong>가 들어있습니다.</li>
<li><strong>용도:</strong> */usr/bin: 일반 사용자가 실행하는 실행 파일.</li>
<li>/usr/local: 사용자가 소스 코드로 직접 빌드해서 설치한 프로그램이 위치하는 곳 (/opt와 유사하지만 더 전통적인 방식).</li>
</ul>
<hr>
<table>
<thead>
<tr>
<th>폴더</th>
<th>주요 내용물</th>
<th>비유</th>
</tr>
</thead>
<tbody><tr>
<td>/bin</td>
<td>기본적인 필수 실행 명령 (ls, cp, mv)</td>
<td>생존을 위한 필수 도구</td>
</tr>
<tr>
<td>/etc</td>
<td>시스템 설정 파일</td>
<td>기기 환경 설정 메뉴</td>
</tr>
<tr>
<td>/home</td>
<td>일반 사용자들의 개인 폴더</td>
<td>각자의 개인 사물함</td>
</tr>
<tr>
<td>/opt</td>
<td>서드파티 전용 소프트웨어</td>
<td>별도로 설치한 전문 장비</td>
</tr>
<tr>
<td>/root</td>
<td>최고 관리자(root) 전용 홈 폴더</td>
<td>관리자의 개인실</td>
</tr>
<tr>
<td>/tmp</td>
<td>임시 파일</td>
<td>쓰고 버리는 메모지</td>
</tr>
<tr>
<td>/var</td>
<td>로그, 캐시 등 가변 데이터</td>
<td>계속 기록되는 일기장/장부</td>
</tr>
</tbody></table>
<hr>
<h3 id="windows와-linux-경로-비교">Windows와 Linux 경로 비교</h3>
<table>
<thead>
<tr>
<th>Windows 항목</th>
<th>Linux 경로</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>C:\Windows</td>
<td>/boot, /lib, /bin</td>
<td>운영체제 핵심 파일들이 분산 저장됨</td>
</tr>
<tr>
<td>C:\Program Files</td>
<td>/usr/bin, /opt</td>
<td>프로그램 실행 파일 및 외부 설치 앱</td>
</tr>
<tr>
<td>C:\Users\Jay</td>
<td>/home/jay</td>
<td>사용자의 개인 파일, 설정, 바탕화면 등</td>
</tr>
<tr>
<td>C:\Windows\System32\config</td>
<td>/etc</td>
<td>시스템 전체 설정 (레지스트리 대신 텍스트 파일 사용)</td>
</tr>
<tr>
<td>AppData\Local\Temp</td>
<td>/tmp</td>
<td>임시 파일 저장소 (재부팅 시 보통 삭제됨)</td>
</tr>
</tbody></table>
<h3 id="환경변수-설정">환경변수 설정</h3>
<pre><code>nano ~/.bashrc</code></pre><p>파일 맨 아래에 추가</p>
<pre><code># ===== Apache Spark =====
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
export SPARK_HOME=/opt/spark
export PYSPARK_PYTHON=python3

# Python venv 자동 활성화
source ~/sparkenv/bin/activate
# Python venv 활성화 후 path 추가
export PATH=$PATH:$SPARK_HOME/bin:$SPARK_HOME/sbin</code></pre><pre><code>#적용
source ~/.bashrc</code></pre><h2 id="spark-shell-실행">Spark Shell 실행</h2>
<pre><code>spark-shell --master &quot;local[2]&quot; --driver-memory 2g</code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/a6b7b238-c5ef-4f5d-88bb-b47baa408f3e/image.png" alt=""></p>
<h3 id="pyspark">PySpark</h3>
<pre><code>pyspark --master &quot;local[2]&quot; --driver-memory 2g</code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/b785404d-ec4f-4149-8f02-1a688f030253/image.png" alt=""></p>
<h3 id="sparkpi">SparkPi</h3>
<pre><code>spark-submit \
  --master &quot;local[2]&quot; \
  --driver-memory 2g \
  --class org.apache.spark.examples.SparkPi \
  $SPARK_HOME/examples/jars/spark-examples_2.12-3.5.8.jar \
  100</code></pre><h2 id="jupyter-lab-연동">jupyter lab 연동</h2>
<ul>
<li><code>Jupyter Lab</code>: 노트북 기반 인터랙티브 개발 환경. 코드/마크다운/시각화를 셀 단위로 실행.</li>
<li><code>포트 8888</code>: Jupyter Lab 기본 포트. Azure NSG에서 별도 허용 필요.</li>
<li><code>SSH 터널링 (권장)</code>: Jupyter 포트를 외부에 직접 노출하지 않고, SSH 채널을 통해 로컬 PC의 포트로 포워딩. 보안적으로 안전.</li>
</ul>
<h3 id="설치">설치</h3>
<pre><code>pip install --upgrade pip
pip install jupyterlab pyspark==3.5.8 pandas matplotlib</code></pre><p>참고: pyspark==3.5.8 은 /opt/spark 설치본과 별도로 Python에서 import 가능하도록 PyPI 패키지 설치. 두 버전이 일치해야 충돌 없음.</p>
<h3 id="실행8888-터널링">실행(8888 터널링)</h3>
<pre><code>jupyter lab --no-browser --ip=0.0.0.0 --port=8888</code></pre><p>이후 token 부분 복사</p>
<p>새 로컬 쉘에서 실행</p>
<pre><code>ssh -L 8888:localhost:8888 azureuser@&lt;VM_PUBLIC_IP&gt;</code></pre><p>이후 <code>http://localhost:8888/lab?token=&lt;위에서_복사한_토큰&gt;</code> 접속
<img src="https://velog.velcdn.com/images/rudin_/post/415d22fd-357e-4c95-bb38-743c33835ca7/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/68c04ca8-73e2-4c05-be4f-c55358504843/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/b4b63978-163e-4d09-997d-8e28a87ed21e/image.png" alt=""></p>
<h2 id="메모리-설정">메모리 설정</h2>
<ul>
<li><code>spark.driver.memory</code>: 드라이버 JVM 힙 크기. SparkContext가 사용. collect()로 큰 결과를 가져올 때 이 값이 부족하면 OOM.</li>
<li><code>spark.executor.memory</code>: 익스큐터 JVM 힙 크기. 실제 task 실행 메모리. 단일 노드 local 모드에선 driver와 executor가 같은 JVM이라 driver memory만 의미 있음. standalone/YARN/K8s 클러스터에선 별개.</li>
<li><code>spark.driver.maxResultSize</code>: collect() 결과 최대 크기. 기본 1g. 초과 시 abort.</li>
<li><code>JVM 오버헤드</code>: 힙 크기 외에 메타스페이스, 코드 캐시, GC 등 추가 메모리 (보통 힙의 10~15%).</li>
<li><code>OS 점유</code>: Ubuntu + 시스템 데몬 = 약 600MB~1GB.</li>
</ul>
<pre><code>cp $SPARK_HOME/conf/spark-defaults.conf.template $SPARK_HOME/conf/spark-defaults.conf
nano $SPARK_HOME/conf/spark-defaults.conf</code></pre><p>밑에 추가</p>
<pre><code>spark.driver.memory              3g
spark.driver.maxResultSize       1g
spark.sql.shuffle.partitions     4</code></pre><hr>

<h1 id="데이터-정제와-머신러닝">데이터 정제와 머신러닝</h1>
<blockquote>
<p>합성 데이터 생성: Faker + Spark로 현실적인 더티 데이터 만들기
EDA &amp; 정제: 결측/중복/이상치/일관성 문제를 PySpark로 처리
피처 엔지니어링: 두 테이블 join, 집계, 타겟 변수 생성
분류 ML: Pipeline API로 고객 이탈 예측 모델 구축 및 평가
회귀 ML (보너스): 거래액 예측 모델</p>
</blockquote>
<ul>
<li><code>Faker</code>: Python의 가짜 데이터 생성 라이브러리. 이름·주소·이메일·날짜 등 현실적인 더미 데이터 생성. 학습/테스트용 데이터 제작에 표준.</li>
<li><code>합성 데이터 (Synthetic Data)</code>: 실제 데이터를 모방해 생성한 인공 데이터. 개인정보 우려 없이 ML 학습/테스트 가능.</li>
<li><code>Parquet</code>: 컬럼 지향(Columnar) 압축 포맷. CSV 대비 1) 압축률 5~10배, 2) 스키마 보존, 3) 컬럼 단위 읽기 가능 → Spark 표준 포맷.</li>
<li><code>dirty data 주입</code>: 학습용 데이터에 의도적으로 결측·중복·이상치를 섞는 기법. 정제 실습에 필수.</li>
</ul>
<h2 id="추가-라이브러리-설치">추가 라이브러리 설치</h2>
<pre><code>source ~/sparkenv/bin/activate
pip install faker</code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/1a328bbf-63d0-4866-a03b-81b1bb9df54e/image.png" alt=""></p>
<h2 id="합성데이터-생성">합성데이터 생성</h2>
<ul>
<li><code>SparkSession.createDataFrame()</code>: Python 리스트/Pandas DataFrame을 Spark DataFrame으로 변환. driver 메모리 사용하므로 대용량은 부적합.</li>
<li><code>schema 명시</code>: StructType으로 스키마 명시 → 자동 추론보다 빠르고 정확.</li>
<li><code>Spark 데이터 타입</code>: IntegerType, LongType, StringType, TimestampType, DoubleType, BooleanType 등.</li>
<li><code>결측 표현</code>: Spark는 None (Python) → null (Spark)로 변환. NumPy의 NaN과 다름 주의.</li>
<li><code>.write.mode(&quot;overwrite&quot;).parquet()</code>: 기존 디렉터리 덮어쓰기. mode(&quot;append&quot;)는 추가, mode(&quot;error&quot;)는 기본값.</li>
</ul>
<h3 id="sparksession-생성">SparkSession 생성</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/8ac39e0a-4e73-475e-83f9-cd24427f3097/image.png" alt=""></p>
<h3 id="데이터-생성">데이터 생성</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/885f0250-4f81-473d-89d9-364a5efb7ce6/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/c11c6b71-79c3-4d25-aaec-727f756d0599/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/35e911ce-65ab-4680-8943-8f070a58c152/image.png" alt="">
경고가 뜬다면, 이유: </p>
<ul>
<li>Spark는 driver의 데이터를 task로 보낼 때 직렬화해서 워커에 전송 </li>
<li>50,000개 transaction을 Python 리스트로 만들어 createDataFrame에 넘기면 driver가 통째로 직렬화 </li>
<li>그 직렬화 페이로드가 1131 KiB &gt; Spark 권장값 1000 KiB 
큰 문제 아니나 해결하려면, partition을 명시적으로 나눠서 만들면 됩니다:<pre><code class="language-python"># 기존 방식 (driver가 한 덩어리로 전송)
transactions_df = spark.createDataFrame(transactions_data, schema=transactions_schema)</code></pre>
<pre><code># 개선 방식 (4개 partition으로 분할 전송)
transactions_rdd = spark.sparkContext.parallelize(transactions_data, numSlices=4)
transactions_df = spark.createDataFrame(transactions_rdd, schema=transactions_schema)</code></pre>numSlices=4로 데이터를 4등분해서 보내므로 각 task ~283 KiB로 줄어듭니다.</li>
</ul>
<h3 id="parquet로-저장">parquet로 저장</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/1f216260-9cec-45b8-9395-45041c11f510/image.png" alt=""></p>
<h2 id="데이터-탐색-eda">데이터 탐색 (EDA)</h2>
<ul>
<li><code>EDA (Exploratory Data Analysis)</code>: 모델링 전 데이터의 특성/품질을 파악하는 단계. 통계 요약, 분포, 결측 패턴, 상관관계 등.</li>
<li><code>describe() vs summary()</code>: 둘 다 통계 요약. summary()가 더 풍부 (count, mean, stddev, min, percentiles, max).</li>
<li><code>dtypes / printSchema()</code>: 컬럼 타입 확인. 정제 전 반드시 검토.</li>
<li><code>PySpark → Pandas 변환 주의</code>: .toPandas()는 driver 메모리에 전체를 수집(collect). 큰 데이터에선 OOM 위험. 시각화 직전 작은 집계 후 변환할 것.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/rudin_/post/05a2bc09-41f8-44c4-a660-638adebb0c6d/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/dac20553-8dc6-4a2f-97dc-ac9a58d9635b/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/86a5d8ef-8934-42b7-8f0d-062a2b714e18/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/8583e8e9-7427-4300-98c2-4d6e0fc4465b/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/3be3d3ae-002c-409f-b0c6-99011d9f7b8b/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/bcc847f3-b749-4acd-92ae-05459337351e/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/788fc23c-c3b9-46b7-9a64-ccd2b5caed92/image.png" alt=""></p>
<h2 id="데이터-정제">데이터 정제</h2>
<ul>
<li><code>결측치 처리 전략</code>: 1) 행 제거(dropna), 2) 평균/중앙값 대체(fillna), 3) 그룹별 대체, 4) 별도 카테고리(&quot;UNKNOWN&quot;) 부여. 어느 것이 좋은지는 도메인과 결측 비율에 따름.</li>
<li><code>when().otherwise()</code>: SQL의 CASE WHEN. 조건부 값 변환에 사용.</li>
<li><code>F.trim(), F.upper(), F.lower()</code>: 문자열 정규화 함수.</li>
<li><code>이상치 처리</code>: 1) 제거(필터), 2) winsorize(상하한 자르기), 3) 변환(log). 도메인 지식이 핵심.</li>
<li><code>dropDuplicates()</code>: 모든 컬럼 또는 지정 컬럼 기준 중복 제거. 첫 번째 row를 유지.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/rudin_/post/37f55480-48bb-45b8-beb4-67e364dc97ed/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/7e1b1977-3b04-4a8d-bdb9-c658f201fc32/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/be7e0bdf-ef15-4b6f-a9b0-7fc811cc9c68/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/941a402b-85b4-41a8-b202-25bbbf224988/image.png" alt=""></p>
<h2 id="피처-엔지니어링">피처 엔지니어링</h2>
<ul>
<li><code>피처 엔지니어링 (Feature Engineering)</code>: 원시 데이터에서 모델 학습에 유용한 변수를 만드는 과정. ML 성능의 70~80%는 피처에서 결정된다는 격언도 있음.</li>
<li><code>RFM 분석</code>:<ul>
<li><code>Recency</code>: 마지막 거래 후 경과일 → 작을수록 활성</li>
<li><code>Frequency</code>: 거래 횟수 → 클수록 충성</li>
<li><code>Monetary</code>: 총 거래액 → 클수록 가치 높음. 고객 세분화·이탈 예측의 고전적 피처</li>
</ul>
</li>
<li><code>Window Function</code>: 행 간 관계 연산. 누적합, 순위, 이전 값 참조 등.</li>
<li><code>groupBy().agg()</code>: SQL의 GROUP BY + 집계함수. F.sum, F.avg, F.count, F.countDistinct, F.max, F.min 등.</li>
<li><code>타겟 변수 (Label)</code>: 지도학습에서 예측할 값. 본 시나리오에서는 &quot;최근 30일 비활동 = 이탈&quot;로 정의.</li>
</ul>
<h3 id="정제-데이터-로드">정제 데이터 로드</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/b2a8e49d-bf88-4e20-bb59-e9bb7006c071/image.png" alt=""></p>
<h3 id="거래-데이터-집계">거래 데이터 집계</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/fc6b5b4d-a8c6-4adb-873f-93a320499f7f/image.png" alt=""></p>
<h3 id="추가-피처-카테고리-선호">추가 피처: 카테고리 선호</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/4cf46f77-8dcd-4a02-845c-86a97c327ead/image.png" alt=""></p>
<h3 id="고객-정보와-join">고객 정보와 join</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/bae0f4e2-3bda-4178-9f57-f2f4874b6af4/image.png" alt=""></p>
<h3 id="타겟-변수-이탈-정의">타겟 변수: 이탈 정의</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/f3b16ebc-1dff-48b1-887e-658734af8e8a/image.png" alt=""></p>
<h3 id="최종-피처셋-저장">최종 피처셋 저장</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/cfc5be5d-dd00-4fef-9cb1-78f64c297f5a/image.png" alt=""></p>
<h2 id="머신러닝-이탈-예측-분류">머신러닝: 이탈 예측 (분류)</h2>
<ul>
<li><code>Spark MLlib</code>: Spark의 분산 ML 라이브러리. 두 API 존재:
  pyspark.ml (DataFrame 기반, 현재 권장)
  pyspark.mllib (RDD 기반, 유지보수만 됨)</li>
<li><code>Pipeline</code>: 전처리 + 모델을 단일 객체로 묶음. 학습/예측 시 동일 변환 보장 → 데이터 누수 방지.</li>
<li><code>StringIndexer</code>: 범주형 문자열을 정수 인덱스로 변환. [“KR”,”US”,”KR”] → [0.0, 1.0, 0.0].</li>
<li><code>OneHotEncoder</code>: 인덱스를 희소 벡터로. 0 → [1,0,0], 1 → [0,1,0]. 트리 모델은 불필요하나 선형 모델엔 필수.</li>
<li><code>VectorAssembler</code>: 여러 피처 컬럼을 하나의 vector 컬럼으로 결합. ML 알고리즘 입력 표준 형식.</li>
<li><code>LogisticRegression</code>: 이진 분류의 베이스라인 모델. 확률 출력, 해석 용이.</li>
<li><code>train/test split</code>: randomSplit([0.8, 0.2])로 학습 80% / 평가 20% 분리.</li>
<li><code>평가 지표</code>:<ul>
<li>`Accuracy : 전체 정답률. 클래스 불균형 시 오해 소지.</li>
<li><code>AUC (ROC)</code>: 임계값 무관 분류 성능. 0.5=무작위, 1.0=완벽, 0.7+=쓸만함.</li>
<li><code>Confusion Matrix</code>: TP/FP/TN/FN 행렬.</li>
<li><code>Precision/Recall/F1</code>: 클래스별 세부 성능.</li>
</ul>
</li>
</ul>
<h3 id="피처-로드-및-분할">피처 로드 및 분할</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/039e5357-928e-495d-953f-e8c3a640bf27/image.png" alt=""></p>
<h3 id="pipeline-구성">Pipeline 구성</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/f6118607-aca5-4218-b578-3b75311b2dea/image.png" alt=""></p>
<h3 id="학습">학습</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/26e95758-07be-4ef7-90fe-3adc23fd102f/image.png" alt=""></p>
<h3 id="평가">평가</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/a42e6477-7ebb-46d0-a8cd-23e6f7e62dc4/image.png" alt=""></p>
<h3 id="confusion-matrix">Confusion Matrix</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/458f26bc-2a5a-4020-9e49-e50483d6b859/image.png" alt=""></p>
<h3 id="피처-중요도-계수">피처 중요도 (계수)</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/6922b59c-43e3-4c6b-b36c-255a6f100a32/image.png" alt=""></p>
<h3 id="모델-저장-및-로드">모델 저장 및 로드</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/d6aac03c-9cf4-447e-84d8-e27248be699f/image.png" alt=""></p>
<hr>

<h2 id="실시간-데이터-파이프라인--kafka--postgresql--spark-structured-streaming--배치-ml-추론">실시간 데이터 파이프라인 — Kafka + PostgreSQL + Spark Structured Streaming + 배치 ML 추론</h2>
<blockquote>
<ol>
<li>PostgreSQL 16을 같은 VM에 설치·운영 (Spark·Kafka와 8GB 안에서 공존)</li>
<li>Kafka 3.x 단일 노드를 KRaft 모드로 설치 (ZooKeeper 없이)</li>
<li>Python 프로듀서로 가짜 거래 이벤트를 Kafka 토픽에 발행</li>
<li>Spark Structured Streaming으로 Kafka 토픽 구독·정제·PostgreSQL 적재</li>
<li>PostgreSQL 데이터를 PySpark로 다시 읽어 분석·조회</li>
<li>Part 2의 학습된 ML 모델을 운영 DB의 신규 데이터에 적용 (배치 추론)</li>
<li>systemd + cron으로 파이프라인 자동화·운영</li>
<li>SQL·Kafka CLI·Spark UI로 파이프라인 모니터링</li>
</ol>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MicrosoftDataSchool] 84일차 - Azure DataWarehouse]]></title>
            <link>https://velog.io/@rudin_/MicrosoftDataSchool-84%EC%9D%BC%EC%B0%A8-Azure-DataWarehouse</link>
            <guid>https://velog.io/@rudin_/MicrosoftDataSchool-84%EC%9D%BC%EC%B0%A8-Azure-DataWarehouse</guid>
            <pubDate>Fri, 08 May 2026 01:46:46 GMT</pubDate>
            <description><![CDATA[<h1 id="azure-data-warehouse">Azure Data Warehouse</h1>
<h2 id="데이터-웨어하우스dw란">데이터 웨어하우스(DW)란</h2>
<p>데이터 웨어하우스(Data Warehouse)는 운영 시스템에 분산된 데이터를 분석 목적에 최적화된 형태로 통합·정제·보관하는 저장소이다.</p>
<p>Bill Inmon은 데이터 웨어하우스를 다음 네 가지 특성으로 정의했다.</p>
<table>
<thead>
<tr>
<th>특성</th>
<th>의미</th>
<th>OLTP와의 차이</th>
</tr>
</thead>
<tbody><tr>
<td>주제 지향(Subject-Oriented)</td>
<td>거래 단위가 아니라 고객, 상품, 이용 같은 분석 주제 중심으로 구성</td>
<td>OLTP는 트랜잭션 중심</td>
</tr>
<tr>
<td>통합(Integrated)</td>
<td>여러 시스템의 코드·단위·정의를 표준화</td>
<td>시스템별 자체 코드 사용</td>
</tr>
<tr>
<td>시간 가변(Time-Variant)</td>
<td>과거 시점 데이터와 이력을 유지</td>
<td>OLTP는 현재값 중심</td>
</tr>
<tr>
<td>비휘발성(Non-Volatile)</td>
<td>적재 후 수정·삭제보다 추가 중심</td>
<td>OLTP는 UPDATE/DELETE 빈번</td>
</tr>
</tbody></table>
<p>OLTP가 “현재 거래를 빠르고 정확하게 처리”하는 시스템이라면,
DW는 “장기간 데이터를 누적해 패턴과 추세를 분석”하는 시스템이다.</p>
<p>따라서:</p>
<ul>
<li>스키마 설계</li>
<li>인덱스 전략</li>
<li>저장 구조</li>
<li>하드웨어 구성</li>
<li>튜닝 방식</li>
</ul>
<p>모두 OLTP와 다르게 접근해야 한다.</p>
<hr>
<h2 id="oltp-vs-olap-워크로드-비교">OLTP vs OLAP 워크로드 비교</h2>
<table>
<thead>
<tr>
<th>관점</th>
<th>OLTP</th>
<th>OLAP / DW</th>
</tr>
</thead>
<tbody><tr>
<td>주 사용자</td>
<td>거래 시스템(POS, 예약, 결제)</td>
<td>분석가, BI, 경영진</td>
</tr>
<tr>
<td>쿼리 특성</td>
<td>단건 조회·갱신</td>
<td>대규모 집계·스캔</td>
</tr>
<tr>
<td>인덱스 전략</td>
<td>B-Tree 중심</td>
<td>Columnstore 중심</td>
</tr>
<tr>
<td>스키마</td>
<td>정규화(3NF)</td>
<td>스타/스노우플레이크</td>
</tr>
<tr>
<td>데이터 신선도</td>
<td>초·밀리초</td>
<td>분·시간·일 단위</td>
</tr>
<tr>
<td>동시성</td>
<td>짧은 트랜잭션 다수</td>
<td>긴 분석 쿼리 소수</td>
</tr>
<tr>
<td>저장 방식</td>
<td>Row Store 중심</td>
<td>Column Store 권장</td>
</tr>
</tbody></table>
<hr>
<h1 id="차원-모델링dimensional-modeling">차원 모델링(Dimensional Modeling)</h1>
<p>차원 모델링은 Ralph Kimball이 정립한 DW 설계 방식이다.</p>
<p>핵심 목표는:</p>
<blockquote>
<p>“사용자가 어떤 측정값을 어떤 관점에서 보고 싶어하는가?”</p>
</blockquote>
<p>를 직관적으로 표현하는 것이다.</p>
<hr>
<h2 id="팩트fact와-디멘션dimension">팩트(Fact)와 디멘션(Dimension)</h2>
<table>
<thead>
<tr>
<th>테이블 종류</th>
<th>역할</th>
<th>따릉이 예시</th>
</tr>
</thead>
<tbody><tr>
<td>팩트(Fact)</td>
<td>측정값 저장. 행 수가 매우 많음</td>
<td>FactRental</td>
</tr>
<tr>
<td>디멘션(Dimension)</td>
<td>분석 관점 제공</td>
<td>DimStation, DimDate</td>
</tr>
<tr>
<td>브릿지/팩트리스 팩트</td>
<td>다대다 관계 표현</td>
<td>본 과정 미사용</td>
</tr>
</tbody></table>
<h3 id="factrental-예시">FactRental 예시</h3>
<ul>
<li>대여 1건 = 1행</li>
<li>이용 시간</li>
<li>이동 거리</li>
<li>탄소 절감량</li>
</ul>
<p>등 숫자형 측정값 중심.</p>
<h3 id="디멘션-예시">디멘션 예시</h3>
<ul>
<li>언제?</li>
<li>어디서?</li>
<li>누가?</li>
<li>어떤 유형?</li>
</ul>
<p>같은 분석 관점을 제공한다.</p>
<hr>
<h2 id="스타-스키마-vs-스노우플레이크">스타 스키마 vs 스노우플레이크</h2>
<h3 id="스타-스키마">스타 스키마</h3>
<p>팩트를 중심에 두고 디멘션이 한 단계로 연결되는 구조.</p>
<pre><code class="language-text">           DimDate
               |
DimUserType - FactRental - DimStation(대여)
               |
          DimStation(반납)
               |
            DimTime</code></pre>
<p>특징:</p>
<ul>
<li>조인 단순</li>
<li>BI 도구 친화적</li>
<li>분석 성능 우수</li>
<li>가장 일반적</li>
</ul>
<hr>
<h3 id="스노우플레이크">스노우플레이크</h3>
<p>디멘션 내부를 다시 정규화한 구조.</p>
<p>예:</p>
<ul>
<li><p>DimStation</p>
<ul>
<li><p>DimDistrict</p>
<ul>
<li>DimCity</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>장점:</p>
<ul>
<li>저장 공간 절약</li>
<li>정규화 수준 높음</li>
</ul>
<p>단점:</p>
<ul>
<li>조인 증가</li>
<li>분석 성능 저하</li>
<li>BI 가독성 저하</li>
</ul>
<p>실무에서는:</p>
<blockquote>
<p>스타 스키마를 기본으로 하고,
디멘션 규모가 매우 클 때만 부분 스노우플레이크를 적용한다.</p>
</blockquote>
<hr>
<h1 id="scdslowly-changing-dimension">SCD(Slowly Changing Dimension)</h1>
<p>디멘션 데이터는 시간이 지나며 변경된다.</p>
<p>예:</p>
<ul>
<li>대여소 이름 변경</li>
<li>자치구 변경</li>
<li>거치대 수 변경</li>
</ul>
<p>이런 변경 이력을 어떻게 관리할지 정의하는 것이 SCD 전략이다.</p>
<hr>
<h2 id="scd-type-비교">SCD Type 비교</h2>
<table>
<thead>
<tr>
<th>타입</th>
<th>동작</th>
<th>특징</th>
<th>본 과정 사용</th>
</tr>
</thead>
<tbody><tr>
<td>Type 0</td>
<td>변경 금지</td>
<td>단순</td>
<td>DimDate</td>
</tr>
<tr>
<td>Type 1</td>
<td>현재값 덮어쓰기</td>
<td>이력 없음</td>
<td>코드 정정</td>
</tr>
<tr>
<td>Type 2</td>
<td>행 추가 + 이력 유지</td>
<td>가장 중요</td>
<td>DimStation</td>
</tr>
<tr>
<td>Type 3</td>
<td>이전값 컬럼 유지</td>
<td>1단계 이력만</td>
<td>미사용</td>
</tr>
<tr>
<td>Type 6</td>
<td>Hybrid</td>
<td>복잡</td>
<td>미사용</td>
</tr>
</tbody></table>
<hr>
<h2 id="scd-type-2">SCD Type 2</h2>
<p>Type 2는 기존 행을 수정하지 않고 새로운 행을 추가한다.</p>
<p>주요 컬럼:</p>
<table>
<thead>
<tr>
<th>컬럼</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>EffectiveStart</td>
<td>시작 시점</td>
</tr>
<tr>
<td>EffectiveEnd</td>
<td>종료 시점</td>
</tr>
<tr>
<td>IsCurrent</td>
<td>현재 유효 여부</td>
</tr>
</tbody></table>
<p>예:</p>
<table>
<thead>
<tr>
<th>StationId</th>
<th>RackCount</th>
<th>EffectiveStart</th>
<th>EffectiveEnd</th>
<th>IsCurrent</th>
</tr>
</thead>
<tbody><tr>
<td>ST-001</td>
<td>10</td>
<td>2025-01-01</td>
<td>2025-09-01</td>
<td>0</td>
</tr>
<tr>
<td>ST-001</td>
<td>15</td>
<td>2025-09-01</td>
<td>NULL</td>
<td>1</td>
</tr>
</tbody></table>
<p>이 방식으로:</p>
<ul>
<li>과거 상태 유지</li>
<li>시점 분석 가능</li>
<li>히스토리 추적 가능</li>
</ul>
<p>해진다.</p>
<hr>
<h2 id="lamda-kappa">Lamda Kappa</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>Lambda</th>
<th>Kappa</th>
</tr>
</thead>
<tbody><tr>
<td>처리 방식</td>
<td>Batch + Stream</td>
<td>Stream Only</td>
</tr>
<tr>
<td>실시간성</td>
<td>좋음</td>
<td>매우 좋음</td>
</tr>
<tr>
<td>정확성</td>
<td>매우 높음</td>
<td>높음</td>
</tr>
<tr>
<td>구조 복잡도</td>
<td>높음</td>
<td>낮음</td>
</tr>
<tr>
<td>개발 난이도</td>
<td>높음</td>
<td>상대적으로 쉬움</td>
</tr>
<tr>
<td>재처리 방식</td>
<td>Batch 재계산</td>
<td>Kafka replay</td>
</tr>
<tr>
<td>대표 기술</td>
<td>Hadoop + Spark</td>
<td>Kafka + Flink</td>
</tr>
</tbody></table>
<h3 id="azure-기준-예시">Azure 기준 예시</h3>
<h4 id="lambda-architecture">Lambda Architecture</h4>
<pre><code>Event Hub
 ├─ Azure Stream Analytics → 실시간 대시보드
 └─ Databricks Batch → 정산/통계</code></pre><p>사용자 구조랑 비슷하게 보면:
KMA/AirKorea
→ Azure Function
→ Event Hub
→ ASA (실시간)
→ PostgreSQL</p>
<ul>
<li>Databricks 배치 분석</li>
</ul>
<h4 id="kappa-architecture">Kappa Architecture</h4>
<pre><code>Kafka/Event Hub
→ Flink/ASA
→ PostgreSQL/Power BI</code></pre><p>배치 없이:</p>
<ul>
<li>스트림만 계속 처리</li>
<li>필요 시 이벤트 재생(replay)</li>
</ul>
<hr>
<h1 id="azure-dw-선택지">Azure DW 선택지</h1>
<p>Azure에서 DW를 구축할 때는 다양한 선택지가 존재한다.</p>
<hr>
<h2 id="azure-sql-database--serverless">Azure SQL Database — Serverless</h2>
<h3 id="특징">특징</h3>
<ul>
<li>자동 일시 중지</li>
<li>사용량 기반 과금</li>
<li>자동 스케일</li>
<li>운영 부담 최소</li>
</ul>
<h3 id="장점">장점</h3>
<ul>
<li>개발/교육 환경 비용 절감</li>
<li>유휴 시간 과금 최소화</li>
<li>PaaS 기반 자동 운영</li>
</ul>
<h3 id="단점">단점</h3>
<ul>
<li>콜드 스타트 발생</li>
<li>vCore 상한 존재</li>
</ul>
<h3 id="본-과정-메인-플랫폼">본 과정 메인 플랫폼</h3>
<hr>
<h2 id="azure-sql-database--provisioned">Azure SQL Database — Provisioned</h2>
<h3 id="특징-1">특징</h3>
<ul>
<li>상시 가동</li>
<li>일정한 응답 성능</li>
<li>Hyperscale 가능</li>
</ul>
<h3 id="장점-1">장점</h3>
<ul>
<li>안정적 응답 시간</li>
<li>대규모 운영 적합</li>
</ul>
<h3 id="단점-1">단점</h3>
<ul>
<li>유휴 시간도 비용 발생</li>
</ul>
<hr>
<h2 id="azure-sql-managed-instance">Azure SQL Managed Instance</h2>
<h3 id="특징-2">특징</h3>
<ul>
<li>SQL Server와 거의 동일</li>
<li>SQL Agent 지원</li>
<li>크로스 DB 쿼리 가능</li>
</ul>
<h3 id="장점-2">장점</h3>
<ul>
<li>기존 SQL Server 이전 용이</li>
<li>높은 호환성</li>
</ul>
<h3 id="단점-2">단점</h3>
<ul>
<li>비용 높음</li>
<li>프로비저닝 느림</li>
</ul>
<hr>
<h2 id="azure-sql-on-vm">Azure SQL on VM</h2>
<h3 id="특징-3">특징</h3>
<ul>
<li>IaaS 기반</li>
<li>OS 직접 관리</li>
<li>SQL Server 전체 기능 사용 가능</li>
</ul>
<h3 id="장점-3">장점</h3>
<ul>
<li>FCI</li>
<li>Replication</li>
<li>Linked Server</li>
<li>CLR</li>
</ul>
<p>등 전체 기능 사용 가능.</p>
<h3 id="단점-3">단점</h3>
<ul>
<li>패치 책임 직접 부담</li>
<li>백업/HA 직접 구성</li>
</ul>
<hr>
<h2 id="microsoft-fabric-warehouse">Microsoft Fabric Warehouse</h2>
<h3 id="특징-4">특징</h3>
<ul>
<li>SaaS 기반</li>
<li>OneLake 통합</li>
<li>Power BI 친화적</li>
</ul>
<h3 id="장점-4">장점</h3>
<ul>
<li>데이터·BI 통합 우수</li>
</ul>
<h3 id="단점-4">단점</h3>
<ul>
<li>Capacity 기반 과금</li>
<li>기능 변화가 빠름</li>
</ul>
<hr>
<h1 id="의사결정-매트릭스">의사결정 매트릭스</h1>
<table>
<thead>
<tr>
<th>조건</th>
<th>추천</th>
</tr>
</thead>
<tbody><tr>
<td>SQL Server 전체 기능 필요</td>
<td>SQL on VM</td>
</tr>
<tr>
<td>온프레미스 거의 그대로 이전</td>
<td>Managed Instance</td>
</tr>
<tr>
<td>비용 최적 + 간헐적 사용</td>
<td>SQL DB Serverless</td>
</tr>
<tr>
<td>일정한 응답 성능 필요</td>
<td>Provisioned</td>
</tr>
<tr>
<td>Power BI 중심 SaaS 환경</td>
<td>Fabric</td>
</tr>
</tbody></table>
<hr>
<h1 id="azure-sql-db-serverless-동작-원리">Azure SQL DB Serverless 동작 원리</h1>
<p>Serverless는 사용한 만큼만 과금되는 컴퓨트 모델이다.</p>
<p>핵심 기능은 다음 세 가지다.</p>
<hr>
<h2 id="자동-일시-중지auto-pause">자동 일시 중지(Auto Pause)</h2>
<p>일정 시간 동안 쿼리나 연결이 없으면:</p>
<ul>
<li>컴퓨트 제거</li>
<li>과금 중단</li>
</ul>
<p>스토리지 비용만 유지된다.</p>
<p>교육/개발 환경에서 매우 유리하다.</p>
<hr>
<h2 id="자동-재개auto-resume">자동 재개(Auto Resume)</h2>
<p>새 연결이 들어오면:</p>
<ul>
<li>자동으로 DB 재개</li>
<li>약 30~60초 콜드 스타트 가능</li>
</ul>
<p>운영 환경에서는:</p>
<ul>
<li>재시도 로직</li>
<li>Keep-alive</li>
<li>워밍 전략</li>
</ul>
<p>등이 필요하다.</p>
<hr>
<h2 id="자동-스케일auto-scale">자동 스케일(Auto Scale)</h2>
<p>최소·최대 vCore 범위를 설정하면:</p>
<ul>
<li>부하에 따라 자동 확장</li>
<li>메모리도 함께 증가</li>
</ul>
<p>한다.</p>
<hr>
<h1 id="storage-→-dw-적재-패턴">Storage → DW 적재 패턴</h1>
<p>DW 적재에서 가장 일반적인 패턴은:</p>
<pre><code class="language-text">CSV → Storage Account → DW</code></pre>
<p>이다.</p>
<p>본 과정에서는 두 가지 표준 패턴을 사용한다.</p>
<hr>
<h2 id="bulk-insert">BULK INSERT</h2>
<p>외부 CSV 파일을 대량 적재하는 전통적 방식.</p>
<h3 id="특징-5">특징</h3>
<ul>
<li>매우 빠름</li>
<li>대량 적재 최적화</li>
<li>staging 적재에 적합</li>
</ul>
<h3 id="흐름">흐름</h3>
<ol>
<li>MASTER KEY 생성</li>
<li>DATABASE SCOPED CREDENTIAL 생성</li>
<li>EXTERNAL DATA SOURCE 생성</li>
<li>BULK INSERT 수행</li>
</ol>
<hr>
<h2 id="openrowsetbulk">OPENROWSET(BULK)</h2>
<p>외부 파일을 가상 테이블처럼 SELECT 하는 방식.</p>
<pre><code class="language-sql">INSERT INTO staging.RentalRaw (rental_id, station_id, started_at, ended_at, duration_min)
SELECT
    JSON_VALUE(c.line, &#39;$.rental_id&#39;),
    CAST(c.station_id AS INT),
    TRY_CONVERT(datetime2, c.started_at),
    TRY_CONVERT(datetime2, c.ended_at),
    c.duration_min
FROM OPENROWSET(
       BULK &#39;2024/2024-01-rental.csv&#39;,
       DATA_SOURCE = &#39;BlobDS&#39;,
       FORMAT = &#39;CSV&#39;,
       FIRSTROW = 2,
       FIELDTERMINATOR = &#39;,&#39;
     ) WITH (
       rental_id     varchar(40),
       station_id    varchar(20),
       started_at    varchar(30),
       ended_at      varchar(30),
       duration_min  int
     ) AS c
WHERE TRY_CONVERT(datetime2, c.started_at) IS NOT NULL;
</code></pre>
<h3 id="특징-6">특징</h3>
<ul>
<li>변환 자유도 높음</li>
<li>INSERT ... SELECT 가능</li>
<li>TRY_CONVERT 활용 가능</li>
</ul>
<h3 id="장점-5">장점</h3>
<ul>
<li>적재 중 정제 가능</li>
<li>필터링 가능</li>
<li>데이터 품질 방어 쉬움</li>
</ul>
<hr>
<h2 id="bulk-insert-vs-openrowset-비교">BULK INSERT vs OPENROWSET 비교</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>BULK INSERT</th>
<th>OPENROWSET</th>
</tr>
</thead>
<tbody><tr>
<td>속도</td>
<td>매우 빠름</td>
<td>상대적으로 느림</td>
</tr>
<tr>
<td>변환</td>
<td>낮음</td>
<td>높음</td>
</tr>
<tr>
<td>에러 처리</td>
<td>제한적</td>
<td>TRY_CONVERT 가능</td>
</tr>
<tr>
<td>권장 용도</td>
<td>staging 적재</td>
<td>정제·변환 적재</td>
</tr>
</tbody></table>
<hr>
<h2 id="책임-기준-분리">책임 기준 분리</h2>
<table>
<thead>
<tr>
<th>레이어</th>
<th>스키마</th>
<th>책임</th>
<th>예시 객체</th>
</tr>
</thead>
<tbody><tr>
<td>Raw</td>
<td>Storage <code>raw/</code></td>
<td>원본 보관, 변환·삭제 금지</td>
<td>2024/2024-01-rental.csv</td>
</tr>
<tr>
<td>Staging (DB)</td>
<td><code>staging</code></td>
<td>원형 그대로 적재된 1차 테이블, 클렌징·중복 제거 단계</td>
<td>staging.RentalRaw</td>
</tr>
<tr>
<td>Warehouse (DB)</td>
<td><code>dw</code></td>
<td>스타 스키마, 팩트·디멘션 정규 모델</td>
<td>dw.FactRental, dw.DimStation</td>
</tr>
<tr>
<td>Mart (DB)</td>
<td><code>mart</code></td>
<td>분석 사용자용 비정규 집계, 뷰·성능 최우선</td>
<td>mart.vw_HourlyDemand</td>
</tr>
</tbody></table>
<hr>
<h1 id="이벤트-기반-적재-패턴">이벤트 기반 적재 패턴</h1>
<p>배치 적재 이후 단계는 자동화이다.</p>
<p>Azure에서는:</p>
<ul>
<li>Event Grid</li>
<li>Azure Functions</li>
<li>Logic Apps</li>
</ul>
<p>를 조합한다.</p>
<hr>
<h2 id="azure-functions">Azure Functions</h2>
<h3 id="특징-7">특징</h3>
<ul>
<li>코드 기반</li>
<li>Python/C#/JS 지원</li>
<li>유연성 높음</li>
</ul>
<h3 id="적합한-경우">적합한 경우</h3>
<ul>
<li>즉시 처리</li>
<li>복잡한 로직</li>
<li>실시간 이벤트</li>
</ul>
<hr>
<h2 id="azure-logic-apps">Azure Logic Apps</h2>
<h3 id="특징-8">특징</h3>
<ul>
<li>GUI 기반</li>
<li>노코드 워크플로우</li>
<li>다양한 SaaS 연결</li>
</ul>
<h3 id="적합한-경우-1">적합한 경우</h3>
<ul>
<li>스케줄 기반 ETL</li>
<li>알림</li>
<li>오케스트레이션</li>
</ul>
<hr>
<h2 id="event-grid">Event Grid</h2>
<h3 id="역할">역할</h3>
<p>Azure 전체 이벤트 라우팅 백본.</p>
<p>예:</p>
<pre><code class="language-text">Blob 업로드
  ↓
Event Grid
  ↓
Function App
  ↓
Stored Procedure 실행</code></pre>
<hr>
<h1 id="이벤트-기반-적재-구조">이벤트 기반 적재 구조</h1>
<pre><code class="language-text">CSV Upload
    ↓
Blob Storage
    ↓ BlobCreated Event
Event Grid
    ↓
Function App
    ↓
EXEC sp_LoadFactFromBlob
    ↓
Azure SQL Database</code></pre>
<p>이 구조의 핵심은:</p>
<blockquote>
<p>“Storage가 진실의 원본(Source of Truth)”</p>
</blockquote>
<p>이라는 점이다.</p>
<hr>
<h1 id="배치-vs-이벤트-적재-선택-기준">배치 vs 이벤트 적재 선택 기준</h1>
<table>
<thead>
<tr>
<th>상황</th>
<th>권장 방식</th>
</tr>
</thead>
<tbody><tr>
<td>하루 1회 대량 적재</td>
<td>배치</td>
</tr>
<tr>
<td>실시간 데이터 도착</td>
<td>이벤트</td>
</tr>
<tr>
<td>대량 + 실시간 혼합</td>
<td>하이브리드</td>
</tr>
</tbody></table>
<p>실무에서는:</p>
<ul>
<li>야간 대량 적재 = 배치</li>
<li>실시간 보정 = 이벤트</li>
</ul>
<p>조합이 가장 흔하다.</p>
<hr>
<h1 id="전체-아키텍처">전체 아키텍처</h1>
<p>이번 실습의 전체 구조는 다음과 같다.</p>
<pre><code class="language-text">서울시 따릉이 CSV
        ↓
Azure Storage Account
(raw / staging / archive)
        ↓
BULK INSERT / OPENROWSET
        ↓
Azure SQL Database Serverless
(staging / dw / mart)
        ↓
분석 쿼리 / Power BI

추가 자동화:
BlobCreated Event
        ↓
Event Grid
        ↓
Azure Function
        ↓
Stored Procedure 실행
        ↓
DW 자동 적재</code></pre>
<p>핵심은 Storage Account를 원본 데이터 저장소로 두고, Azure SQL Database Serverless를 분석용 DW로 사용하는 것이다.</p>
<hr>
<h1 id="lab-01--azure-sql-database-serverless-생성">Lab 01 — Azure SQL Database Serverless 생성</h1>
<h2 id="리소스-그룹과-sql-server-생성">리소스 그룹과 SQL Server 생성</h2>
<p>먼저 실습에서 사용할 변수들을 정의한다.</p>
<pre><code class="language-bash">RG=rg-dwlab-$USER
LOC=koreacentral
SQL_SRV=sql-dwlab-$USER-$RANDOM
SQL_DB=dw_seoulbike
ADMIN_USER=dwadmin
ADMIN_PASS=&#39;Dw!Lab2026Secure&#39;</code></pre>
<p>리소스 그룹과 SQL Server를 생성한다.</p>
<pre><code class="language-bash">az group create -n $RG -l $LOC

az sql server create \
  --name $SQL_SRV \
  --resource-group $RG \
  --location $LOC \
  --admin-user $ADMIN_USER \
  --admin-password $ADMIN_PASS</code></pre>
<p>현재 접속 IP를 방화벽에 등록한다.</p>
<pre><code class="language-bash">MY_IP=$(curl -s https://api.ipify.org)

az sql server firewall-rule create \
  --resource-group $RG \
  --server $SQL_SRV \
  --name allow-me \
  --start-ip-address $MY_IP \
  --end-ip-address $MY_IP</code></pre>
<hr>
<h2 id="serverless-db-생성">Serverless DB 생성</h2>
<pre><code class="language-bash">az sql db create \
  --resource-group $RG \
  --server $SQL_SRV \
  --name $SQL_DB \
  --edition GeneralPurpose \
  --family Gen5 \
  --compute-model Serverless \
  --min-capacity 0.5 \
  --capacity 2 \
  --auto-pause-delay 60 \
  --backup-storage-redundancy Local \
  --collation Korean_Wansung_CI_AS</code></pre>
<p>주요 옵션은 다음과 같다.</p>
<table>
<thead>
<tr>
<th>옵션</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>--compute-model Serverless</code></td>
<td>Serverless 계층 사용</td>
</tr>
<tr>
<td><code>--min-capacity 0.5</code></td>
<td>최소 0.5 vCore</td>
</tr>
<tr>
<td><code>--capacity 2</code></td>
<td>최대 2 vCore</td>
</tr>
<tr>
<td><code>--auto-pause-delay 60</code></td>
<td>60분 미사용 시 자동 일시 중지</td>
</tr>
<tr>
<td><code>--backup-storage-redundancy Local</code></td>
<td>교육용 비용 절감</td>
</tr>
</tbody></table>
<p>접속 확인:</p>
<pre><code class="language-bash">sqlcmd -S $SQL_SRV.database.windows.net \
  -d $SQL_DB \
  -U $ADMIN_USER \
  -P &quot;$ADMIN_PASS&quot; \
  -Q &quot;SELECT @@VERSION;&quot;</code></pre>
<hr>
<h1 id="lab-02--serverless-동작-확인">Lab 02 — Serverless 동작 확인</h1>
<h2 id="auto-pause-시간-변경">Auto Pause 시간 변경</h2>
<p>교육 환경에서는 자동 일시 중지를 빠르게 확인하기 위해 15분으로 변경한다.</p>
<pre><code class="language-bash">az sql db update \
  -g $RG \
  -s $SQL_SRV \
  -n $SQL_DB \
  --auto-pause-delay 15</code></pre>
<hr>
<h2 id="리소스-사용량-확인">리소스 사용량 확인</h2>
<p>SQL Database에서 다음 DMV를 조회해 CPU, 메모리, IO 사용률을 확인한다.</p>
<pre><code class="language-sql">SELECT TOP 5
    avg_cpu_percent,
    avg_memory_usage_percent,
    avg_data_io_percent,
    end_time
FROM sys.dm_db_resource_stats
ORDER BY end_time DESC;</code></pre>
<p>확인할 내용은 다음과 같다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>확인 내용</th>
</tr>
</thead>
<tbody><tr>
<td>Auto Pause</td>
<td>일정 시간 미사용 시 DB가 Paused 상태가 되는지</td>
</tr>
<tr>
<td>Auto Resume</td>
<td>다시 접속했을 때 자동으로 Online 상태가 되는지</td>
</tr>
<tr>
<td>Cold Start</td>
<td>첫 연결까지 30~120초 정도 지연되는지</td>
</tr>
</tbody></table>
<p>운영 환경에서는 Cold Start가 사용자 경험에 영향을 줄 수 있으므로, 재시도 로직이나 Keep-alive 전략을 고려해야 한다.</p>
<hr>
<h1 id="lab-03--storage-account-생성-및-데이터-업로드">Lab 03 — Storage Account 생성 및 데이터 업로드</h1>
<h2 id="storage-account-생성">Storage Account 생성</h2>
<pre><code class="language-bash">STO_ACC=stodwlab$USER$RANDOM

az storage account create \
  -g $RG \
  -n $STO_ACC \
  -l $LOC \
  --sku Standard_LRS \
  --kind StorageV2 \
  --access-tier Hot \
  --allow-blob-public-access false \
  --min-tls-version TLS1_2</code></pre>
<p>Storage Key를 가져온다.</p>
<pre><code class="language-bash">STO_KEY=$(az storage account keys list \
  -g $RG \
  -n $STO_ACC \
  --query [0].value \
  -o tsv)</code></pre>
<p>컨테이너 3개를 생성한다.</p>
<pre><code class="language-bash">for c in raw staging archive; do
  az storage container create \
    --name $c \
    --account-name $STO_ACC \
    --account-key $STO_KEY
done</code></pre>
<table>
<thead>
<tr>
<th>컨테이너</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><code>raw</code></td>
<td>원본 CSV 보관</td>
</tr>
<tr>
<td><code>staging</code></td>
<td>정제 중간 결과</td>
</tr>
<tr>
<td><code>archive</code></td>
<td>적재 완료 파일 보관</td>
</tr>
</tbody></table>
<hr>
<h2 id="따릉이-샘플-데이터-업로드">따릉이 샘플 데이터 업로드</h2>
<p>생성된 샘플 CSV 파일을 <code>raw/seoul_bike/</code> 경로에 업로드한다.</p>
<pre><code class="language-bash">az storage blob upload-batch \
  --account-name &quot;$STO_ACC&quot; \
  --account-key $STO_KEY \
  --destination raw \
  --destination-path seoul_bike/ \
  --source ./seoul_bike_data \
  --pattern &quot;*.csv&quot; \
  --overwrite</code></pre>
<p>업로드 확인:</p>
<pre><code class="language-bash">az storage blob list \
  --account-name &quot;$STO_ACC&quot; \
  --account-key $STO_KEY \
  --container-name raw \
  --prefix &quot;seoul_bike/&quot; \
  --query &quot;[].{name:name, size:properties.contentLength}&quot; \
  -o table</code></pre>
<hr>
<h2 id="sas-토큰-생성">SAS 토큰 생성</h2>
<p>Azure SQL Database에서 Blob을 읽기 위해 SAS 토큰을 생성한다.</p>
<pre><code class="language-bash">EXPIRY=$(date -u -d &quot;+7 days&quot; &#39;+%Y-%m-%dT%H:%MZ&#39;)

SAS=$(az storage container generate-sas \
  --account-name $STO_ACC \
  --name raw \
  --permissions rl \
  --expiry $EXPIRY \
  --https-only \
  --output tsv)

echo $SAS</code></pre>
<p>SAS는 비밀번호와 같은 민감 정보이므로 외부에 노출되지 않도록 관리해야 한다.</p>
<hr>
<h1 id="lab-04--스타-스키마-생성">Lab 04 — 스타 스키마 생성</h1>
<h2 id="스키마-생성">스키마 생성</h2>
<pre><code class="language-sql">CREATE SCHEMA staging AUTHORIZATION dbo;
GO
CREATE SCHEMA dw AUTHORIZATION dbo;
GO
CREATE SCHEMA mart AUTHORIZATION dbo;
GO</code></pre>
<hr>
<h2 id="디멘션-테이블-생성">디멘션 테이블 생성</h2>
<pre><code class="language-sql">CREATE TABLE dw.DimDate (
  DateKey       INT          NOT NULL PRIMARY KEY,
  [Date]        DATE         NOT NULL,
  [Year]        SMALLINT     NOT NULL,
  Quarter       TINYINT      NOT NULL,
  [Month]       TINYINT      NOT NULL,
  MonthName     NVARCHAR(10) NOT NULL,
  [Day]         TINYINT      NOT NULL,
  DayOfWeek     TINYINT      NOT NULL,
  DayName       NVARCHAR(10) NOT NULL,
  IsWeekend     BIT          NOT NULL,
  IsHoliday     BIT          NOT NULL DEFAULT 0
);

CREATE TABLE dw.DimTime (
  TimeKey       INT          NOT NULL PRIMARY KEY,
  [Hour]        TINYINT      NOT NULL,
  [Minute]      TINYINT      NOT NULL,
  TimeBucket    NVARCHAR(10) NOT NULL
);

CREATE TABLE dw.DimUserType (
  UserTypeKey   INT IDENTITY PRIMARY KEY,
  UserTypeCode  VARCHAR(20)  NOT NULL UNIQUE,
  UserTypeName  NVARCHAR(40) NOT NULL
);</code></pre>
<hr>
<h2 id="팩트-테이블-생성">팩트 테이블 생성</h2>
<pre><code class="language-sql">CREATE TABLE dw.FactRental (
  RentalKey       BIGINT IDENTITY,
  RentalId        VARCHAR(40)  NOT NULL,
  StartDateKey    INT          NOT NULL,
  StartTimeKey    INT          NOT NULL,
  EndDateKey      INT          NULL,
  EndTimeKey      INT          NULL,
  StartStationKey BIGINT       NULL,
  EndStationKey   BIGINT       NULL,
  UserTypeKey     INT          NULL,
  DurationMin     INT          NULL,
  DistanceMeter   INT          NULL,
  CarbonGramSaved DECIMAL(10,2) NULL,
  LoadedAt        DATETIME2    NOT NULL DEFAULT SYSUTCDATETIME(),
  SourceFile      VARCHAR(256) NULL,
  CONSTRAINT PK_FactRental PRIMARY KEY NONCLUSTERED (RentalKey)
);

CREATE CLUSTERED COLUMNSTORE INDEX CCI_FactRental
ON dw.FactRental;

CREATE UNIQUE INDEX UX_FactRental_RentalId
ON dw.FactRental(RentalId);</code></pre>
<p><code>FactRental</code>은 대량 집계가 주 목적이므로 Clustered Columnstore Index를 적용한다.</p>
<hr>
<h2 id="dimdate--dimtime-사전-적재">DimDate / DimTime 사전 적재</h2>
<pre><code class="language-sql">WITH d AS (
  SELECT CAST(&#39;2015-01-01&#39; AS DATE) AS dt
  UNION ALL
  SELECT DATEADD(DAY, 1, dt)
  FROM d
  WHERE dt &lt; &#39;2030-12-31&#39;
)
INSERT dw.DimDate (
  DateKey, [Date], [Year], Quarter, [Month], MonthName,
  [Day], DayOfWeek, DayName, IsWeekend
)
SELECT
  CONVERT(INT, FORMAT(dt,&#39;yyyyMMdd&#39;)),
  dt,
  YEAR(dt),
  DATEPART(QUARTER, dt),
  MONTH(dt),
  DATENAME(MONTH, dt),
  DAY(dt),
  DATEPART(WEEKDAY, dt),
  DATENAME(WEEKDAY, dt),
  CASE WHEN DATEPART(WEEKDAY, dt) IN (1,7) THEN 1 ELSE 0 END
FROM d
OPTION (MAXRECURSION 0);</code></pre>
<pre><code class="language-sql">WITH m AS (
  SELECT 0 AS n
  UNION ALL
  SELECT n + 1
  FROM m
  WHERE n &lt; 1439
)
INSERT dw.DimTime (TimeKey, [Hour], [Minute], TimeBucket)
SELECT
  (n / 60) * 100 + (n % 60),
  n / 60,
  n % 60,
  CASE
    WHEN n/60 BETWEEN 0  AND 5  THEN N&#39;심야&#39;
    WHEN n/60 BETWEEN 6  AND 11 THEN N&#39;오전&#39;
    WHEN n/60 BETWEEN 12 AND 17 THEN N&#39;오후&#39;
    WHEN n/60 BETWEEN 18 AND 22 THEN N&#39;저녁&#39;
    ELSE N&#39;심야&#39;
  END
FROM m
OPTION (MAXRECURSION 0);</code></pre>
<p>사용자 유형도 기본 적재한다.</p>
<pre><code class="language-sql">INSERT dw.DimUserType (UserTypeCode, UserTypeName) VALUES
  (&#39;MEMBER&#39;,     N&#39;정기권 회원&#39;),
  (&#39;NONMEMBER&#39;,  N&#39;일일권 비회원&#39;),
  (&#39;UNKNOWN&#39;,    N&#39;미상&#39;);</code></pre>
<p>검증 기준:</p>
<table>
<thead>
<tr>
<th>테이블</th>
<th>기대값</th>
</tr>
</thead>
<tbody><tr>
<td><code>dw.DimDate</code></td>
<td>5,844행</td>
</tr>
<tr>
<td><code>dw.DimTime</code></td>
<td>1,440행</td>
</tr>
<tr>
<td><code>dw.DimUserType</code></td>
<td>3행</td>
</tr>
<tr>
<td><code>dw.FactRental</code></td>
<td>빈 상태</td>
</tr>
</tbody></table>
<hr>
<h1 id="lab-05--bulk-insert로-첫-적재">Lab 05 — BULK INSERT로 첫 적재</h1>
<h2 id="blob-접근-객체-생성">Blob 접근 객체 생성</h2>
<p>SQL Database에서 Blob Storage에 접근하기 위한 3종 객체를 만든다.</p>
<pre><code class="language-sql">IF NOT EXISTS (
  SELECT 1
  FROM sys.symmetric_keys
  WHERE name = &#39;##MS_DatabaseMasterKey##&#39;
)
  CREATE MASTER KEY ENCRYPTION BY PASSWORD = &#39;Lab!MasterKey2026&#39;;

CREATE DATABASE SCOPED CREDENTIAL StorageCred
  WITH IDENTITY = &#39;SHARED ACCESS SIGNATURE&#39;,
       SECRET   = &#39;&lt;SAS_TOKEN&gt;&#39;;

CREATE EXTERNAL DATA SOURCE BlobRaw
  WITH (
    TYPE       = BLOB_STORAGE,
    LOCATION   = &#39;https://&lt;STO_ACC&gt;.blob.core.windows.net/raw&#39;,
    CREDENTIAL = StorageCred
  );</code></pre>
<p><code>&lt;SAS_TOKEN&gt;</code>에는 앞의 <code>?</code>를 제외한 SAS 본문만 넣는다.</p>
<hr>
<h2 id="staging-테이블-생성">Staging 테이블 생성</h2>
<pre><code class="language-sql">DROP TABLE IF EXISTS staging.RentalRaw;

CREATE TABLE staging.RentalRaw (
  RentalId        VARCHAR(20)  NOT NULL,
  BikeId          VARCHAR(20)  NOT NULL,
  StartTime       DATETIME2(0) NOT NULL,
  EndTime         DATETIME2(0) NOT NULL,
  StartStationId  VARCHAR(20)  NOT NULL,
  EndStationId    VARCHAR(20)  NOT NULL,
  DurationMin     INT          NOT NULL,
  DistanceMeter   INT          NOT NULL,
  UserType        VARCHAR(20)  NOT NULL
);</code></pre>
<hr>
<h2 id="csv-파일-bulk-insert">CSV 파일 BULK INSERT</h2>
<pre><code class="language-sql">TRUNCATE TABLE staging.RentalRaw;

DECLARE @i INT = 1, @sql NVARCHAR(MAX);

WHILE @i &lt;= 7
BEGIN
  SET @sql = N&#39;
    BULK INSERT staging.RentalRaw
    FROM &#39;&#39;seoul_bike/rentals_2025090&#39; + CAST(@i AS VARCHAR(1)) + &#39;.csv&#39;&#39;
    WITH (
      DATA_SOURCE     = &#39;&#39;BlobRaw&#39;&#39;,
      FORMAT          = &#39;&#39;CSV&#39;&#39;,
      FIRSTROW        = 2,
      FIELDTERMINATOR = &#39;&#39;,&#39;&#39;,
      ROWTERMINATOR   = &#39;&#39;0x0d0a&#39;&#39;,
      CODEPAGE        = &#39;&#39;65001&#39;&#39;,
      TABLOCK,
      MAXERRORS       = 100
    );&#39;;

  EXEC sp_executesql @sql;
  SET @i += 1;
END</code></pre>
<p>검증:</p>
<pre><code class="language-sql">SELECT COUNT(*) AS staging_rows
FROM staging.RentalRaw;</code></pre>
<p>기대값은 21,600행이다.</p>
<hr>
<h2 id="staging-→-factrental-변환-적재">staging → FactRental 변환 적재</h2>
<pre><code class="language-sql">TRUNCATE TABLE dw.FactRental;

INSERT INTO dw.FactRental (
  RentalId, StartDateKey, StartTimeKey, EndDateKey, EndTimeKey,
  StartStationKey, EndStationKey, UserTypeKey,
  DurationMin, DistanceMeter, CarbonGramSaved,
  LoadedAt, SourceFile
)
SELECT
  s.RentalId,
  CONVERT(INT, CONVERT(VARCHAR(8), s.StartTime, 112)) AS StartDateKey,
  DATEPART(HOUR, s.StartTime) * 60 + DATEPART(MINUTE, s.StartTime) AS StartTimeKey,
  CONVERT(INT, CONVERT(VARCHAR(8), s.EndTime, 112)) AS EndDateKey,
  DATEPART(HOUR, s.EndTime) * 60 + DATEPART(MINUTE, s.EndTime) AS EndTimeKey,
  CAST(NULL AS BIGINT) AS StartStationKey,
  CAST(NULL AS BIGINT) AS EndStationKey,
  COALESCE(ut.UserTypeKey, ut_unk.UserTypeKey) AS UserTypeKey,
  s.DurationMin,
  s.DistanceMeter,
  CAST(s.DistanceMeter * 0.21 AS DECIMAL(10,2)) AS CarbonGramSaved,
  SYSUTCDATETIME() AS LoadedAt,
  &#39;seoul_bike/rentals_&#39; + CONVERT(VARCHAR(8), s.StartTime, 112) + &#39;.csv&#39; AS SourceFile
FROM staging.RentalRaw AS s
LEFT JOIN dw.DimUserType AS ut
  ON ut.UserTypeCode = s.UserType
LEFT JOIN dw.DimUserType AS ut_unk
  ON ut_unk.UserTypeCode = &#39;UNKNOWN&#39;;</code></pre>
<p>여기서는 아직 <code>StartStationKey</code>, <code>EndStationKey</code>를 채우지 않는다.<br>대여소 디멘션은 Lab 06에서 SCD Type 2로 처리한 뒤 백필한다.</p>
<hr>
<h2 id="적재-검증">적재 검증</h2>
<pre><code class="language-sql">SELECT COUNT(*) AS fact_rows
FROM dw.FactRental;</code></pre>
<pre><code class="language-sql">SELECT StartDateKey, COUNT(*) AS c
FROM dw.FactRental
GROUP BY StartDateKey
ORDER BY StartDateKey;</code></pre>
<pre><code class="language-sql">SELECT COUNT(*) AS unmapped_user_type
FROM dw.FactRental
WHERE UserTypeKey IS NULL;</code></pre>
<p>검증 포인트:</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>기대값</th>
</tr>
</thead>
<tbody><tr>
<td>Fact 행수</td>
<td>21,600</td>
</tr>
<tr>
<td>날짜 분포</td>
<td>7일치</td>
</tr>
<tr>
<td>UserTypeKey NULL</td>
<td>0</td>
</tr>
<tr>
<td>StationKey</td>
<td>아직 NULL</td>
</tr>
<tr>
<td>CarbonGramSaved</td>
<td><code>DistanceMeter * 0.21</code></td>
</tr>
</tbody></table>
<hr>
<h1 id="lab-06--scd-type-2-대여소-디멘션-적재">Lab 06 — SCD Type 2 대여소 디멘션 적재</h1>
<h2 id="dimstation-생성">DimStation 생성</h2>
<pre><code class="language-sql">CREATE TABLE dw.DimStation (
  StationKey   BIGINT IDENTITY(1,1) NOT NULL,
  StationId    VARCHAR(20)   NOT NULL,
  StationName  NVARCHAR(100) NOT NULL,
  Gu           NVARCHAR(50)  NOT NULL,
  Lat          DECIMAL(9,5)  NOT NULL,
  Lng          DECIMAL(9,5)  NOT NULL,
  RackCount    INT           NOT NULL,
  RowHash      BINARY(32)    NOT NULL,
  ValidFrom    DATE          NOT NULL,
  ValidTo      DATE          NULL,
  IsCurrent    BIT           NOT NULL,
  LoadedAt     DATETIME2(0)  NOT NULL DEFAULT SYSUTCDATETIME(),
  CONSTRAINT PK_DimStation PRIMARY KEY NONCLUSTERED (StationKey)
);

CREATE UNIQUE INDEX UX_DimStation_BusinessVersion
ON dw.DimStation (StationId, ValidFrom);

CREATE INDEX IX_DimStation_Current
ON dw.DimStation (StationId)
WHERE IsCurrent = 1;</code></pre>
<hr>
<h2 id="rowhash-함수-생성">RowHash 함수 생성</h2>
<pre><code class="language-sql">CREATE OR ALTER FUNCTION dw.fn_StationRowHash(
  @StationName NVARCHAR(100),
  @Gu NVARCHAR(50),
  @Lat DECIMAL(9,5),
  @Lng DECIMAL(9,5),
  @RackCount INT
) RETURNS BINARY(32)
WITH SCHEMABINDING
AS
BEGIN
  RETURN HASHBYTES(&#39;SHA2_256&#39;,
    CONCAT_WS(N&#39;|&#39;,
      @StationName,
      @Gu,
      CONVERT(NVARCHAR(20), @Lat, 1),
      CONVERT(NVARCHAR(20), @Lng, 1),
      CAST(@RackCount AS NVARCHAR(20))
    )
  );
END</code></pre>
<p>RowHash를 사용하면 여러 컬럼을 각각 비교하지 않고 해시값 하나로 변경 여부를 판단할 수 있다.</p>
<hr>
<h2 id="stationraw-적재">StationRaw 적재</h2>
<pre><code class="language-sql">DROP TABLE IF EXISTS staging.StationRaw;

CREATE TABLE staging.StationRaw (
  StationId    VARCHAR(20)   NOT NULL,
  StationName  NVARCHAR(100) NOT NULL,
  Gu           NVARCHAR(50)  NOT NULL,
  Lat          DECIMAL(9,5)  NOT NULL,
  Lng          DECIMAL(9,5)  NOT NULL,
  RackCount    INT           NOT NULL,
  OpenedDate   DATE          NOT NULL
);

TRUNCATE TABLE staging.StationRaw;

BULK INSERT staging.StationRaw
FROM &#39;seoul_bike/stations.csv&#39;
WITH (
  DATA_SOURCE=&#39;BlobRaw&#39;,
  FORMAT=&#39;CSV&#39;,
  FIRSTROW=2,
  FIELDTERMINATOR=&#39;,&#39;,
  ROWTERMINATOR=&#39;0x0d0a&#39;,
  CODEPAGE=&#39;65001&#39;,
  TABLOCK,
  MAXERRORS=0
);</code></pre>
<hr>
<h2 id="최초-적재">최초 적재</h2>
<pre><code class="language-sql">INSERT INTO dw.DimStation (
  StationId, StationName, Gu, Lat, Lng, RackCount,
  RowHash, ValidFrom, ValidTo, IsCurrent
)
SELECT
  s.StationId,
  s.StationName,
  s.Gu,
  s.Lat,
  s.Lng,
  s.RackCount,
  dw.fn_StationRowHash(s.StationName, s.Gu, s.Lat, s.Lng, s.RackCount),
  s.OpenedDate,
  NULL,
  1
FROM staging.StationRaw s;</code></pre>
<hr>
<h2 id="팩트-외래키-백필">팩트 외래키 백필</h2>
<pre><code class="language-sql">UPDATE f
SET f.StartStationKey = s.StationKey
FROM dw.FactRental f
INNER JOIN staging.RentalRaw r
  ON r.RentalId = f.RentalId
INNER JOIN dw.DimStation s
  ON s.StationId = r.StartStationId
 AND CAST(r.StartTime AS DATE) &gt;= s.ValidFrom
 AND (s.ValidTo IS NULL OR CAST(r.StartTime AS DATE) &lt; s.ValidTo)
WHERE f.StartStationKey IS NULL;

UPDATE f
SET f.EndStationKey = s.StationKey
FROM dw.FactRental f
INNER JOIN staging.RentalRaw r
  ON r.RentalId = f.RentalId
INNER JOIN dw.DimStation s
  ON s.StationId = r.EndStationId
 AND CAST(r.EndTime AS DATE) &gt;= s.ValidFrom
 AND (s.ValidTo IS NULL OR CAST(r.EndTime AS DATE) &lt; s.ValidTo)
WHERE f.EndStationKey IS NULL;</code></pre>
<p>검증:</p>
<pre><code class="language-sql">SELECT
  SUM(CASE WHEN StartStationKey IS NULL THEN 1 ELSE 0 END) AS null_start,
  SUM(CASE WHEN EndStationKey IS NULL THEN 1 ELSE 0 END) AS null_end
FROM dw.FactRental;</code></pre>
<p>기대값은 <code>0 / 0</code>이다.</p>
<hr>
<h1 id="lab-07--분석-쿼리">Lab 07 — 분석 쿼리</h1>
<h2 id="시간대별-평균-이용-패턴">시간대별 평균 이용 패턴</h2>
<pre><code class="language-sql">SELECT
  t.TimeBucket,
  d.DayName,
  COUNT(*) AS rentals,
  AVG(f.DurationMin) AS avg_minutes
FROM dw.FactRental f
JOIN dw.DimTime t
  ON t.TimeKey = f.StartTimeKey
JOIN dw.DimDate d
  ON d.DateKey = f.StartDateKey
GROUP BY t.TimeBucket, d.DayName
ORDER BY rentals DESC;</code></pre>
<p>이 쿼리로 시간대와 요일별 이용 패턴을 확인할 수 있다.</p>
<hr>
<h2 id="자치구별-출발도착-비대칭">자치구별 출발/도착 비대칭</h2>
<pre><code class="language-sql">WITH dep AS (
  SELECT s.Gu, COUNT(*) AS departures
  FROM dw.FactRental f
  JOIN dw.DimStation s
    ON s.StationKey = f.StartStationKey
  GROUP BY s.Gu
),
arr AS (
  SELECT s.Gu, COUNT(*) AS arrivals
  FROM dw.FactRental f
  JOIN dw.DimStation s
    ON s.StationKey = f.EndStationKey
  GROUP BY s.Gu
)
SELECT
  COALESCE(d.Gu, a.Gu) AS Gu,
  d.departures,
  a.arrivals,
  a.arrivals - d.departures AS net_flow
FROM dep d
FULL OUTER JOIN arr a
  ON a.Gu = d.Gu
ORDER BY ABS(a.arrivals - d.departures) DESC;</code></pre>
<p><code>net_flow</code>가 크면 해당 자치구에서 자전거 적체 또는 부족이 발생할 가능성이 높다.</p>
<hr>
<h2 id="top-10-od-페어">Top 10 OD 페어</h2>
<pre><code class="language-sql">SELECT TOP 10
  st.StationName AS start_station,
  en.StationName AS end_station,
  COUNT(*) AS trips,
  AVG(f.DurationMin) AS avg_min
FROM dw.FactRental f
JOIN dw.DimStation st
  ON st.StationKey = f.StartStationKey
JOIN dw.DimStation en
  ON en.StationKey = f.EndStationKey
WHERE f.StartStationKey IS NOT NULL
  AND f.EndStationKey IS NOT NULL
  AND f.StartStationKey &lt;&gt; f.EndStationKey
GROUP BY st.StationName, en.StationName
ORDER BY trips DESC;</code></pre>
<hr>
<h2 id="rollup으로-소계-만들기">ROLLUP으로 소계 만들기</h2>
<pre><code class="language-sql">SELECT
  COALESCE(s.Gu, &#39;&lt;&lt;TOTAL_GU&gt;&gt;&#39;) AS Gu,
  COALESCE(CAST(t.Hour AS VARCHAR(8)), &#39;&lt;&lt;TOTAL_HOUR&gt;&gt;&#39;) AS Hour,
  COUNT(*) AS rides
FROM dw.FactRental f
JOIN dw.DimStation s
  ON s.StationKey = f.StartStationKey
JOIN dw.DimTime t
  ON t.TimeKey = f.StartTimeKey
GROUP BY ROLLUP(s.Gu, t.Hour)
ORDER BY GROUPING(s.Gu), s.Gu, GROUPING(t.Hour), t.Hour;</code></pre>
<hr>
<h1 id="lab-08--function-app--event-grid-자동-적재">Lab 08 — Function App + Event Grid 자동 적재</h1>
<h2 id="function-app-생성">Function App 생성</h2>
<pre><code class="language-bash">FUNC_APP=func-dwlab-$USER-$RANDOM
FUNC_LOC=&quot;koreacentral&quot;

az functionapp create \
  -g $RG \
  -n $FUNC_APP \
  --consumption-plan-location $FUNC_LOC \
  --runtime python \
  --runtime-version 3.11 \
  --functions-version 4 \
  --storage-account $STO_ACC \
  --os-type Linux</code></pre>
<p>Managed Identity를 활성화한다.</p>
<pre><code class="language-bash">az functionapp identity assign \
  -g $RG \
  -n $FUNC_APP</code></pre>
<p>Storage 읽기 권한을 부여한다.</p>
<pre><code class="language-bash">FUNC_PRINCIPAL=$(az functionapp identity show \
  -g $RG \
  -n $FUNC_APP \
  --query principalId \
  -o tsv)

STO_ID=$(az storage account show \
  -g $RG \
  -n $STO_ACC \
  --query id \
  -o tsv)

az role assignment create \
  --assignee &quot;$FUNC_PRINCIPAL&quot; \
  --role &#39;Storage Blob Data Reader&#39; \
  --scope &quot;$STO_ID&quot;</code></pre>
<hr>
<h2 id="sql-db에-managed-identity-사용자-등록">SQL DB에 Managed Identity 사용자 등록</h2>
<pre><code class="language-sql">CREATE USER [func-dwlab-...] FROM EXTERNAL PROVIDER;

ALTER ROLE db_datareader ADD MEMBER [func-dwlab-...];
ALTER ROLE db_datawriter ADD MEMBER [func-dwlab-...];

GRANT EXECUTE ON SCHEMA :: staging TO [func-dwlab-...];
GRANT EXECUTE ON SCHEMA :: dw TO [func-dwlab-...];</code></pre>
<p>운영 환경에서는 Function 코드에 SQL 비밀번호를 넣지 않는 것이 중요하다.<br>Managed Identity를 사용하면 Function App 자체의 신원으로 SQL Database에 접근할 수 있다.</p>
<hr>
<h2 id="function-코드-핵심">Function 코드 핵심</h2>
<pre><code class="language-python">import logging, os, struct
import azure.functions as func
from azure.identity import DefaultAzureCredential
import pyodbc

app = func.FunctionApp()

@app.event_grid_trigger(arg_name=&#39;event&#39;)
def blob_loaded(event: func.EventGridEvent):
    data = event.get_json()
    blob_url = data.get(&#39;url&#39;)
    logging.info(f&#39;BlobCreated: {blob_url}&#39;)

    if &#39;/raw/&#39; not in blob_url or not blob_url.endswith(&#39;.csv&#39;):
        logging.info(&#39;Skip non-target blob&#39;)
        return

    rel = blob_url.split(&#39;/raw/&#39;)[-1]

    cred = DefaultAzureCredential()
    token = cred.get_token(
        &#39;https://database.windows.net/.default&#39;
    ).token.encode(&#39;utf-16-le&#39;)

    token_struct = struct.pack(f&#39;=i{len(token)}s&#39;, len(token), token)
    SQL_COPT_SS_ACCESS_TOKEN = 1256

    conn_str = (
        &#39;Driver={ODBC Driver 18 for SQL Server};&#39;
        f&#39;Server=tcp:{os.environ[&quot;SQL_SERVER&quot;]},1433;&#39;
        f&#39;Database={os.environ[&quot;SQL_DB&quot;]};&#39;
        &#39;Encrypt=yes;TrustServerCertificate=no;&#39;
    )

    with pyodbc.connect(
        conn_str,
        attrs_before={SQL_COPT_SS_ACCESS_TOKEN: token_struct}
    ) as cn:
        cn.cursor().execute(
            &#39;EXEC dw.sp_LoadFactFromBlob @blobPath = ?&#39;,
            rel
        ).commit()

    logging.info(&#39;sp_LoadFactFromBlob OK&#39;)</code></pre>
<hr>
<h2 id="stored-procedure-생성">Stored Procedure 생성</h2>
<pre><code class="language-sql">CREATE OR ALTER PROCEDURE dw.sp_LoadFactFromBlob
  @blobPath NVARCHAR(500)
AS
BEGIN
  SET NOCOUNT ON;

  DECLARE @sql NVARCHAR(MAX);

  TRUNCATE TABLE staging.RentalRaw;

  SET @sql = N&#39;BULK INSERT staging.RentalRaw
    FROM &#39;&#39;&#39; + @blobPath + N&#39;&#39;&#39;
    WITH (
      DATA_SOURCE=&#39;&#39;BlobRaw&#39;&#39;,
      FORMAT=&#39;&#39;CSV&#39;&#39;,
      FIRSTROW=2,
      FIELDTERMINATOR=&#39;&#39;,&#39;&#39;,
      ROWTERMINATOR=&#39;&#39;0x0a&#39;&#39;,
      CODEPAGE=&#39;&#39;65001&#39;&#39;,
      MAXERRORS=100
    )&#39;;

  EXEC sp_executesql @sql;

  EXEC dw.sp_TransformAndLoad @sourceFile = @blobPath;
END</code></pre>
<hr>
<h2 id="event-grid-구독-생성">Event Grid 구독 생성</h2>
<pre><code class="language-bash">FUNC_KEY=$(az functionapp keys list \
  -g $RG \
  -n $FUNC_APP \
  --query systemKeys.eventgrid_extension \
  -o tsv)

ENDPOINT=&quot;https://$FUNC_APP.azurewebsites.net/runtime/webhooks/EventGrid?functionName=blob_loaded&amp;code=$FUNC_KEY&quot;

az eventgrid event-subscription create \
  --name sub-blob-to-func \
  --source-resource-id $STO_ID \
  --endpoint-type webhook \
  --endpoint &quot;$ENDPOINT&quot; \
  --included-event-types Microsoft.Storage.BlobCreated \
  --subject-begins-with /blobServices/default/containers/raw/</code></pre>
<p>이제 <code>raw</code> 컨테이너에 CSV가 업로드되면 Event Grid가 Function을 호출하고, Function이 SQL 저장 프로시저를 실행해 적재한다.</p>
<hr>
<h1 id="lab-09--logic-apps-일일-적재-워크플로우">Lab 09 — Logic Apps 일일 적재 워크플로우</h1>
<p>Logic Apps는 GUI 기반으로 ETL 흐름을 구성할 수 있다.</p>
<p>워크플로우 구조는 다음과 같다.</p>
<pre><code class="language-text">Recurrence: 매일 02:00 KST
        ↓
List blobs in raw
        ↓
Filter array
        ↓
For each
        ↓
Execute stored procedure
        ↓
성공: archive 이동
실패: 이메일 알림</code></pre>
<p>Function이 “파일이 올라오자마자 즉시 처리”에 적합하다면, Logic Apps는 “정해진 시간에 여러 작업을 순서대로 실행”하는 데 적합하다.</p>
<hr>
<h1 id="lab-10--운영-pitr-모니터링-권한-정리">Lab 10 — 운영: PITR, 모니터링, 권한, 정리</h1>
<h2 id="pitr-복원">PITR 복원</h2>
<pre><code class="language-bash">RESTORE_TS=$(date -u -d &quot;-5 min&quot; &#39;+%Y-%m-%dT%H:%M:%S&#39;)

az sql db restore \
  -g $RG \
  -s $SQL_SRV \
  -n dw_seoulbike \
  --dest-name dw_seoulbike_pitr \
  --time $RESTORE_TS \
  --edition GeneralPurpose \
  --family Gen5 \
  --capacity 2</code></pre>
<p>복원된 DB 행수 확인:</p>
<pre><code class="language-bash">sqlcmd -S $SQL_SRV.database.windows.net \
  -d dw_seoulbike_pitr \
  -U $ADMIN_USER \
  -P &quot;$ADMIN_PASS&quot; \
  -Q &quot;SELECT COUNT(*) FROM dw.FactRental;&quot;</code></pre>
<hr>
<h2 id="cpu-알림-규칙-생성">CPU 알림 규칙 생성</h2>
<pre><code class="language-bash">az monitor metrics alert create \
  -g $RG \
  -n alert-dw-cpu-high \
  --scopes $(az sql db show -g $RG -s $SQL_SRV -n $SQL_DB --query id -o tsv) \
  --condition &quot;avg cpu_percent &gt; 80&quot; \
  --window-size 5m \
  --evaluation-frequency 1m \
  --severity 2</code></pre>
<hr>
<h2 id="role-기반-권한-분리">ROLE 기반 권한 분리</h2>
<pre><code class="language-sql">CREATE ROLE dw_analyst;
GRANT SELECT ON SCHEMA :: dw TO dw_analyst;
GRANT SELECT ON SCHEMA :: mart TO dw_analyst;
DENY SELECT ON SCHEMA :: staging TO dw_analyst;

CREATE ROLE dw_loader;
GRANT SELECT, INSERT, UPDATE, DELETE ON SCHEMA :: staging TO dw_loader;
GRANT SELECT, INSERT, UPDATE ON SCHEMA :: dw TO dw_loader;
GRANT EXECUTE ON SCHEMA :: dw TO dw_loader;

CREATE ROLE dw_admin;
GRANT CONTROL ON DATABASE :: dw_seoulbike TO dw_admin;</code></pre>
<table>
<thead>
<tr>
<th>Role</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><code>dw_analyst</code></td>
<td>분석가 읽기 권한</td>
</tr>
<tr>
<td><code>dw_loader</code></td>
<td>ETL 적재 권한</td>
</tr>
<tr>
<td><code>dw_admin</code></td>
<td>운영 관리자 권한</td>
</tr>
</tbody></table>
<hr>
<h2 id="리소스-정리">리소스 정리</h2>
<p>실습 종료 후에는 리소스 그룹을 삭제해 비용 누적을 막는다.</p>
<pre><code class="language-bash">az sql db delete \
  -g $RG \
  -s $SQL_SRV \
  -n dw_seoulbike_pitr \
  --yes 2&gt;/dev/null

az group delete \
  -n $RG \
  --yes \
  --no-wait</code></pre>
<hr>
<h1 id="전체-실습-흐름-정리">전체 실습 흐름 정리</h1>
<table>
<thead>
<tr>
<th>단계</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>Azure SQL Database Serverless 생성</td>
</tr>
<tr>
<td>2</td>
<td>Serverless auto-pause / auto-resume 검증</td>
</tr>
<tr>
<td>3</td>
<td>Storage Account 생성 및 CSV 업로드</td>
</tr>
<tr>
<td>4</td>
<td>스타 스키마 생성</td>
</tr>
<tr>
<td>5</td>
<td>BULK INSERT로 staging 적재</td>
</tr>
<tr>
<td>6</td>
<td>staging → FactRental 변환 적재</td>
</tr>
<tr>
<td>7</td>
<td>SCD Type 2로 대여소 디멘션 관리</td>
</tr>
<tr>
<td>8</td>
<td>시간대·자치구·OD 분석</td>
</tr>
<tr>
<td>9</td>
<td>Event Grid + Function으로 자동 적재</td>
</tr>
<tr>
<td>10</td>
<td>Logic Apps로 일일 적재 워크플로우 구성</td>
</tr>
<tr>
<td>11</td>
<td>PITR, Monitor, Role, 비용 정리</td>
</tr>
</tbody></table>
<hr>
<h1 id="15-핵심-정리">15. 핵심 정리</h1>
<p>이번 실습의 핵심은 다음과 같다.</p>
<ol>
<li>DW는 OLTP와 목적이 다르기 때문에 스키마와 인덱스 전략도 달라야 한다.</li>
<li>분석 중심 모델링에서는 Fact와 Dimension을 분리한다.</li>
<li>대량 분석용 Fact 테이블에는 Columnstore Index가 적합하다.</li>
<li>Storage Account는 원본 데이터 저장소 역할을 한다.</li>
<li><code>BULK INSERT</code>는 빠른 1차 적재에 적합하다.</li>
<li>SCD Type 2는 대여소처럼 속성이 변하는 디멘션의 이력을 보존하는 데 사용한다.</li>
<li>Event Grid + Function을 사용하면 Blob 업로드 기반 자동 적재가 가능하다.</li>
<li>Logic Apps는 일정 기반 ETL 오케스트레이션에 적합하다.</li>
<li>PITR, Monitor, Role, 비용 정리는 DW 운영에서 반드시 필요하다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MicrosoftDataSchool] 83일차 - Azure SQL에서 Graph Database 활용하기 ]]></title>
            <link>https://velog.io/@rudin_/MicrosoftDataSchool-83%EC%9D%BC%EC%B0%A8-Azure-SQL%EC%97%90%EC%84%9C-Graph-Database-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@rudin_/MicrosoftDataSchool-83%EC%9D%BC%EC%B0%A8-Azure-SQL%EC%97%90%EC%84%9C-Graph-Database-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 07 May 2026 03:35:30 GMT</pubDate>
            <description><![CDATA[<h1 id="1-개요">1. 개요</h1>
<p><img src="https://velog.velcdn.com/images/rudin_/post/eb69803b-92b0-426c-8b1f-2180c38f8639/image.png" alt="">
그래프 쪽은 JOIN을 명시적으로 쓰지 않아도 패턴이 곧 쿼리가 됨</p>
<h2 id="11-sql-server-graph-database란">1.1 SQL Server Graph Database란?</h2>
<ul>
<li>SQL Server 2017부터 도입됨 </li>
<li>노드(Node)와 엣지(Edge)를 사용하여 복잡한 관계형 데이터를 자연스럽게 표현하고 쿼리할 수 있게 해줌</li>
<li>Azure SQL Database와 Azure SQL VM 모두에서 이 기능을 완벽하게 지원</li>
</ul>
<p>관계형 모델로도 같은 데이터를 표현할 수 있지만, &quot;친구의 친구의 친구&quot;처럼 여러 단계를 거쳐가는 질의를 JOIN으로 풀려고 하면 SQL이 금세 복잡해집니다. 그래프 모델은 이런 다단계 탐색을 시각적인 패턴 그대로 쿼리로 표현할 수 있게 해 줍니다. </p>
<h2 id="12-graph-database를-사용하는-이유">1.2 Graph Database를 사용하는 이유</h2>
<ul>
<li>복잡한 관계 표현 — 다대다 관계와 계층 구조를 직관적으로 모델링 </li>
<li>경로 탐색 — 친구의 친구, 추천 시스템 등 연결 기반 쿼리에 최적화 </li>
<li>패턴 매칭 — MATCH 절로 복잡한 관계 패턴을 간단하게 표현 </li>
<li>기존 SQL과 통합 — 관계형 테이블과 그래프 테이블을 함께 사용 가능 </li>
</ul>
<h3 id="💡--언제-그래프-db가-빛을-발하나요">💡  언제 그래프 DB가 빛을 발하나요?</h3>
<ul>
<li>조직도 / 권한 위임 / 분류 체계처럼 깊이가 가변적인 계층 구조 </li>
<li>소셜 그래프 (친구·팔로우·차단 등 같은 노드 타입 사이의 다양한 관계) </li>
<li>추천 엔진 (콜드 스타트 우회: &quot;비슷한 사람이 좋아하는 것&quot;) </li>
<li>사기 탐지·자금 흐름 추적 (의심 노드를 시작점으로 N단계 확산 분석) </li>
<li>지식 그래프·Knowledge Base (엔티티 + 관계 위주 질의) </li>
</ul>
<h2 id="13-graph-database-핵심-개념">1.3 Graph Database 핵심 개념</h2>
<table>
<thead>
<tr>
<th>개념</th>
<th>설명 / SQL Server 구현</th>
</tr>
</thead>
<tbody><tr>
<td>Node (노드)</td>
<td>엔티티(사람, 게시물, 상품 등)를 표현하는 점. <code>CREATE TABLE ... AS NODE</code> 로 생성하며 내부적으로 <code>$node_id</code> 컬럼이 자동 생성됨</td>
</tr>
<tr>
<td>Edge (엣지)</td>
<td>두 노드를 잇는 방향성 있는 선. <code>CREATE TABLE ... AS EDGE</code> 로 생성하며 <code>$edge_id</code>, <code>$from_id</code>, <code>$to_id</code> 컬럼이 자동 생성됨</td>
</tr>
<tr>
<td>$node_id</td>
<td>노드 고유 ID. JSON 형식(<code>{&quot;schema&quot;:&quot;...&quot;, &quot;table&quot;:&quot;...&quot;, &quot;id&quot;:&quot;...&quot;}</code>)이며 시스템이 자동 부여</td>
</tr>
<tr>
<td>$edge_id</td>
<td>엣지 고유 ID. <code>$node_id</code>와 동일한 JSON 형식 사용</td>
</tr>
<tr>
<td>$from_id / $to_id</td>
<td>엣지가 연결하는 시작/끝 노드의 <code>$node_id</code> 값. 엣지 방향 정의</td>
</tr>
<tr>
<td>MATCH</td>
<td>WHERE 절에서 그래프 패턴을 명시하는 키워드. 예: <code>MATCH(A-(e)-&gt;B)</code></td>
</tr>
</tbody></table>
<p><img src="https://velog.velcdn.com/images/rudin_/post/6cc4fb28-13ee-4869-9906-225c44ac535a/image.png" alt=""></p>
<h4 id="node_id는-직접-다루면-안됨">$node_id는 직접 다루면 안됨</h4>
<p>$node_id 값은 &quot;{&quot;schema&quot;:&quot;dbo&quot;,&quot;table&quot;:&quot;Person&quot;,&quot;id&quot;:0}&quot; 같은 JSON 문자열입니다. 
직접 INSERT하거나 비교 키로 외부 시스템에 노출하지 마세요. 대신 PersonId 같은 비즈니스 키로 식별하고, $node_id는 시스템 내부 조인용으로만 씁니다. </p>
<h1 id="2-환경-설정-및-샘플-데이터">2. 환경 설정 및 샘플 데이터</h1>
<h2 id="21-시나리오-소셜-네트워크">2.1 시나리오: 소셜 네트워크</h2>
<p>이 실습에서는 작은 소셜 네트워크를 모델링합니다. 사용자(Person) 5명과 게시물(Post) 3개가 있고, 사용자 간에는 팔로우 관계가, 사용자와 게시물 사이에는 작성/좋아요 관계가 형성됩니다. 앞으로 모든 쿼리는 이 한 장의 그래프를 기준으로 다양한 질문을 던지게 됩니다. 
<img src="https://velog.velcdn.com/images/rudin_/post/bda638aa-49f4-4712-82d8-4e53bc6c2138/image.png" alt=""></p>
<p>나같은경우엔 sql vm 생성 시 이미지 선택을 잘못했는지, SSMS가 설치되어있지 않았다.
별도로 설치한 후, 연결은 </p>
<ul>
<li>서버이름: localhost</li>
<li>인증서 신뢰 체크</li>
</ul>
<p>후 진행하였다.
<img src="https://velog.velcdn.com/images/rudin_/post/4b2133fe-1516-48d3-9759-e141a734d7e5/image.png" alt=""></p>
<h2 id="22-node-테이블-생성">2.2 Node 테이블 생성</h2>
<p>핵심 키워드는 끝부분의 AS NODE 입니다. </p>
<pre><code class="language-sql">-- 사용자 노드 테이블 
CREATE TABLE Person ( 
    PersonId  INT PRIMARY KEY, 
    Name      NVARCHAR(100), 
    Email     NVARCHAR(200),
    JoinDate  DATE DEFAULT GETDATE()
) AS NODE; 
-- 게시물 노드 테이블 
CREATE TABLE Post ( 
    PostId     INT PRIMARY KEY, 
    Title      NVARCHAR(200), 
    Content    NVARCHAR(MAX), 
    CreatedAt  DATETIME2 DEFAULT GETDATE() 
) AS NODE; </code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/4609ea5f-3d6b-455e-913c-37a80e48203f/image.png" alt="">
① AS NODE 키워드 </p>
<p>테이블 정의 마지막에 AS NODE를 붙이는 것이 그래프 노드 선언의 전부입니다. 이 한 줄로 SQL Server는 내부적으로 $node_id라는 보이지 않는 컬럼을 추가합니다. </p>
<p>② PRIMARY KEY는 별개 </p>
<p>PersonId는 우리가 부여하는 비즈니스 식별자, $node_id는 시스템이 부여하는 그래프 식별자입니다. 두 개가 공존하며 역할이 다릅니다. </p>
<p>③ 일반 컬럼은 자유 </p>
<p>Name, Email 등 평소 테이블 만들듯이 컬럼을 자유롭게 추가하면 됩니다. 그래프 테이블도 본질은 일반 테이블입니다. </p>
<p>④ Post 테이블도 동일 패턴 </p>
<p>Person과 똑같이 AS NODE로 끝맺기만 하면 됩니다. 노드 타입이 두 종류 이상이어도 패턴은 같습니다. </p>
<blockquote>
<p>Person, Post 두 테이블이 생성되며, 각 테이블에는 우리가 정의한 컬럼 외에 $node_id가 숨겨진 형태로 추가됩니다. 
sys.tables 카탈로그 뷰에서 is_node = 1로 표시되며, sys.columns에서 graph_type 값을 가진 시스템 컬럼들이 함께 보입니다. 
아직 데이터는 비어 있고, 두 테이블 사이에는 어떤 관계도 없습니다 — 관계는 다음 단계의 Edge 테이블이 담당합니다. </p>
</blockquote>
<h2 id="23-edge-테이블-생성">2.3 Edge 테이블 생성</h2>
<pre><code class="language-sql">-- 팔로우 관계 (Person → Person) 
CREATE TABLE follows ( 
    FollowDate DATE DEFAULT GETDATE() 
) AS EDGE; 

-- 좋아요 관계 (Person → Post) 
CREATE TABLE likes ( 
    LikedAt DATETIME2 DEFAULT GETDATE() 
) AS EDGE; 

-- 작성 관계 (Person → Post) 
CREATE TABLE wrote AS EDGE; </code></pre>
<p>① AS EDGE 키워드 </p>
<p>엣지 테이블이 됨을 선언합니다. 자동으로 $edge_id, $from_id, $to_id 세 개의 컬럼이 생성됩니다. </p>
<p>② 엣지 속성 컬럼 </p>
<p>follows의 FollowDate처럼, 관계 자체에 대한 메타데이터(언제 맺어졌는지, 가중치 등)를 컬럼으로 자유롭게 둘 수 있습니다. </p>
<p>③ 컬럼 없는 엣지도 가능 </p>
<p>wrote 테이블처럼 사용자 정의 컬럼이 하나도 없어도 됩니다. AS EDGE만 있으면 시스템 컬럼만으로 동작합니다. </p>
<p>④ 방향성 </p>
<p>엣지는 항상 $from_id → $to_id 방향을 갖습니다. 양방향 관계를 표현하고 싶다면 동일한 엣지를 반대 방향으로 한 번 더 INSERT 하면 됩니다. </p>
<blockquote>
<p>⚠️  엣지 테이블에는 PRIMARY KEY를 두지 않습니다 
$edge_id가 시스템 PK 역할을 자동으로 하므로 굳이 추가 PK를 둘 필요가 없습니다. 추가 PK를 두면 같은 두 노드 사이에 여러 엣지(예: 같은 사람이 같은 게시물을 시점을 달리해 두 번 좋아요)를 만들 수 없게 되어 오히려 모델링이 어색해집니다. PRIMARY KEY가 없다고 해서 취소 여부를 못 아는 게 아니라, 엣지 테이블에 FollowedAt, UnfollowedAt, IsActive 같은 속성을 넣어서 관계 자체를 상태 데이터로 관리하는게 보통입니다.</p>
</blockquote>
<p>혹은 SCD를 사용한다.</p>
<h3 id="scd">SCD</h3>
<p>데이터 웨어하우스에서 “시간이 지나며 바뀌는 데이터”를 어떻게 관리할지에 대한 패턴
| 구분       | Type 1    | Type 2           |
| -------- | --------- | ---------------- |
| 데이터 변경 시 | 기존 데이터 수정 | 새 행 추가           |
| 과거 데이터   | 사라짐       | 유지               |
| 테이블 크기   | 작음        | 커짐               |
| 구현 난이도   | 쉬움        | 복잡               |
| 분석 용도    | 현재 상태 중심  | 이력 분석 가능         |
| 예시       | 최신 프로필    | 팔로우 이력, 주소 변경 이력 |</p>
<h2 id="24-샘플-데이터-삽입">2.4 샘플 데이터 삽입</h2>
<p>노드 데이터를 먼저 삽입한 후, 엣지 데이터를 삽입합니다. 이 순서는 매우 중요합니다 — 엣지가 참조할 노드가 먼저 존재해야 합니다. </p>
<h3 id="①-노드-데이터">① 노드 데이터</h3>
<pre><code class="language-sql">-- 사용자 노드 삽입 
INSERT INTO Person (PersonId, Name, Email) VALUES 
    (1, N&#39;김원일&#39;, &#39;kim@example.com&#39;), 
    (2, N&#39;이두석&#39;, &#39;lee@example.com&#39;), 
    (3, N&#39;박삼현&#39;, &#39;park@example.com&#39;), 
    (4, N&#39;정사람&#39;, &#39;jung@example.com&#39;), 
    (5, N&#39;오동현&#39;, &#39;oh@example.com&#39;);   

-- 게시물 노드 삽입 
INSERT INTO Post (PostId, Title, Content) VALUES 
    (101, N&#39;Azure SQL 시작하기&#39;, N&#39;Azure SQL의 기본 사용법을 알아봅니다…&#39;), 
    (102, N&#39;Graph DB 활용법&#39;,    N&#39;소셜 네트워크 구현 예제입니다…&#39;), 
    (103, N&#39;성능 최적화 팁&#39;,     N&#39;쿼리 성능을 향상시키는 방법…&#39;); </code></pre>
<h3 id="②-엣지-데이터-follows">② 엣지 데이터 (follows)</h3>
<pre><code class="language-sql">-- 팔로우 관계 삽입 (누가 누구를 팔로우하는지) 
INSERT INTO follows ($from_id, $to_id) VALUES 
    ((SELECT $node_id FROM Person WHERE PersonId = 1), 
     (SELECT $node_id FROM Person WHERE PersonId = 2)),     -- 김원일 → 이두석 

    ((SELECT $node_id FROM Person WHERE PersonId = 1), 
     (SELECT $node_id FROM Person WHERE PersonId = 3)),     -- 김원일 → 박삼현 

    ((SELECT $node_id FROM Person WHERE PersonId = 2), 
     (SELECT $node_id FROM Person WHERE PersonId = 3)),     -- 이두석 → 박삼현 

    ((SELECT $node_id FROM Person WHERE PersonId = 3), 
     (SELECT $node_id FROM Person WHERE PersonId = 4)),     -- 박삼현 → 정사람 

    ((SELECT $node_id FROM Person WHERE PersonId = 4), 
     (SELECT $node_id FROM Person WHERE PersonId = 5));     -- 정사람 → 오동현 </code></pre>
<p>① $from_id, $to_id에 직접 INSERT </p>
<p>엣지를 만들 때는 두 시스템 컬럼 $from_id, $to_id에 값을 채워 넣습니다. 이 값들은 노드의 $node_id (JSON)와 동일한 형태여야 합니다. </p>
<p>② 서브쿼리로 $node_id 조회 </p>
<p>JSON 값을 직접 입력하긴 어렵기 때문에, (SELECT $node_id FROM Person WHERE PersonId = 1) 형태로 비즈니스 키 → $node_id 변환을 매번 거칩니다. 이 패턴이 그래프 INSERT의 표준 관용구입니다. </p>
<p>③ 한 INSERT에 여러 엣지 </p>
<p>VALUES 절에 행을 콤마로 나열하면 한 번의 INSERT로 여러 엣지를 만들 수 있어 트랜잭션 비용이 줄어듭니다. </p>
<p>④ 결과 그래프 </p>
<p>실행이 끝나면 그림 2-1의 follows 5개 엣지가 모두 만들어집니다. 5명의 Person 노드를 잇는 사슬과 분기 구조가 형성됩니다. </p>
<h4 id="edge-table에-중복-안넣는-방법">Edge Table에 중복 안넣는 방법</h4>
<p>팔로우 했다가 취소했다가 팔로하면? 삭제하지 않고 그냥 둔다. 따라서 보통은 중복을 허용한다. 굳이 중복방지를 하고싶다면 조건을 걸면 된다. edge table에 굳이 추가 PK를 두지 않는것도 있다.</p>
<h3 id="③-엣지-데이터-wrote-likes">③ 엣지 데이터 (wrote, likes)</h3>
<pre><code class="language-sql">-- 게시물 작성 관계 
INSERT INTO wrote ($from_id, $to_id) VALUES 
    ((SELECT $node_id FROM Person WHERE PersonId = 1), 
     (SELECT $node_id FROM Post   WHERE PostId   = 101)),  -- 김원일 → Post 101 
    ((SELECT $node_id FROM Person WHERE PersonId = 2), 
     (SELECT $node_id FROM Post   WHERE PostId   = 102)),  -- 이두석 → Post 102 
    ((SELECT $node_id FROM Person WHERE PersonId = 3), 
     (SELECT $node_id FROM Post   WHERE PostId   = 103));  -- 박삼현 → Post 103 

-- 좋아요 관계 
INSERT INTO likes ($from_id, $to_id) VALUES 
    ((SELECT $node_id FROM Person WHERE PersonId = 2), 
     (SELECT $node_id FROM Post   WHERE PostId   = 101)), 
    ((SELECT $node_id FROM Person WHERE PersonId = 3), 
     (SELECT $node_id FROM Post   WHERE PostId   = 101)), 
    ((SELECT $node_id FROM Person WHERE PersonId = 1), 
     (SELECT $node_id FROM Post   WHERE PostId   = 102)); </code></pre>
<h2 id="🎯--실습-과제-1">🎯  실습 과제 1</h2>
<h3 id="과제-1-a">과제 1-A.</h3>
<p>새 사용자 &quot;최여섯&quot;(PersonId=6)을 INSERT한 뒤, 김원일이 최여섯을 팔로우하는 엣지를 추가하세요. </p>
<pre><code class="language-sql">INSERT INTO Person (PersonId, Name) VALUES 
(6,N&#39;최여섯&#39;);

INSERT INTO follows ($from_id, $to_id) VALUES 
    ((SELECT $node_id FROM Person WHERE PersonId = 1), 
     (SELECT $node_id FROM Person WHERE PersonId = 6));     -- 김원일 → 최여섯 </code></pre>
<h3 id="과제-1-b">과제 1-B.</h3>
<p>SELECT * FROM Person; 과 SELECT * FROM follows; 를 각각 실행해서 $node_id, $from_id, $to_id 값이 실제로 어떻게 생겼는지 눈으로 확인해 보세요. </p>
<pre><code class="language-sql">SELECT * FROM Person;
SELECT * FROM follows;</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/84497c9e-e3ff-486b-9302-9df2f8d330e3/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/b4e0bc53-beb2-457c-ab39-a3252b7fc41b/image.png" alt=""></p>
<h3 id="과제-1-c">과제 1-C.</h3>
<p>시스템 카탈로그를 사용해 이 데이터베이스의 모든 노드 테이블과 엣지 테이블을 나열해 보세요. 힌트:  SELECT name, is_node, is_edge FROM sys.tables WHERE is_node = 1 OR is_edge = 1; </p>
<p><img src="https://velog.velcdn.com/images/rudin_/post/c014cdd3-c671-49a1-9f09-4b0816da2769/image.png" alt=""></p>
<h1 id="3-match--그래프-패턴-쿼리">3. MATCH — 그래프 패턴 쿼리</h1>
<h2 id="31-이론">3.1 이론</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/b381be65-d60b-4a79-937d-2d7d5659ae43/image.png" alt="">
노드와 엣지를 ASCII 아트처럼 그려서 패턴을 정의하면, SQL Server가 그래프를 따라가며 그 패턴에 맞는 모든 경로를 찾아옵니다. </p>
<p>핵심은 &quot;코드에 그린 그림이 곧 찾고자 하는 패턴&quot;이라는 점입니다. (A)-(e)-&gt;(B) 라고 쓰면 노드 A에서 엣지 e를 타고 노드 B로 가는 모든 쌍을 찾는다는 의미가 됩니다. </p>
<table>
<thead>
<tr>
<th>구문 요소</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td>(node)</td>
<td>노드. FROM 절에 등장한 노드 테이블의 별칭(alias)을 그대로 사용</td>
</tr>
<tr>
<td>-(edge)-&gt;</td>
<td>정방향 엣지. 왼쪽 노드의 <code>$node_id</code>가 엣지의 <code>$from_id</code>와 같고, 오른쪽 노드의 <code>$node_id</code>가 <code>$to_id</code>와 같은 행 탐색</td>
</tr>
<tr>
<td>&lt;-(edge)-</td>
<td>역방향 엣지. <code>from/to</code>가 반대 방향인 형태</td>
</tr>
<tr>
<td>-(edge)-</td>
<td>방향 무관 탐색 (양방향 시도). SQL Server 2019부터 지원</td>
</tr>
<tr>
<td>MATCH(...)</td>
<td>WHERE 절에서 그래프 패턴 전체를 감싸는 표현식. 일반 조건과 <code>AND</code> 결합 가능</td>
</tr>
</tbody></table>
<h2 id="32-기본-패턴-매칭">3.2 기본 패턴 매칭</h2>
<h3 id="직접-연결된-노드-찾기">직접 연결된 노드 찾기</h3>
<pre><code class="language-sql">SELECT 
    Person1.Name AS Follower, 
    Person2.Name AS Following 
FROM Person AS Person1, follows, Person AS Person2 
WHERE MATCH(Person1-(follows)-&gt;Person2) 
  AND Person1.Name = N&#39;김원일&#39;; </code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/bee32df1-6788-4dbd-8af2-97b2b2427df6/image.png" alt=""></p>
<p>① FROM 절에 노드와 엣지 모두 나열 </p>
<p>같은 Person 테이블이지만 시작 노드와 끝 노드 두 역할로 쓰이므로, 별칭 Person1, Person2로 두 번 나타나게 합니다. 가운데 follows는 엣지 테이블 그 자체입니다. </p>
<p>② WHERE MATCH(...) — 패턴 정의 </p>
<p>MATCH 절은 &quot;Person1에서 follows 엣지를 타고 Person2로 가는 경로&quot;를 의미합니다. SQL Server는 이 표현을 &quot;follows.$from_id = Person1.$node_id AND follows.$to_id = Person2.$node_id&quot;라는 조건으로 내부 변환합니다. </p>
<p>③ AND Person1.Name = N&#39;김원일&#39; — 시작점 고정 </p>
<p>MATCH 패턴 자체에 비교 조건을 끼워 넣지 않습니다. 시작 노드를 좁히고 싶을 때는 일반 WHERE 절처럼 AND로 추가합니다. </p>
<p>④ SELECT — 무엇을 가져올지 결정 </p>
<p>Person1.Name(팔로워), Person2.Name(팔로잉 대상)을 컬럼명으로 반환합니다. 같은 테이블에서 두 행을 동시에 다루는 self-join 같은 효과를 MATCH 한 줄로 깔끔하게 만든 셈입니다. </p>
<h3 id="역방향-탐색">역방향 탐색</h3>
<p>이번엔 반대로, &quot;박삼현을 팔로우하는 사람들&quot;을 조회합니다. 두 가지 동등한 표현이 있습니다. </p>
<pre><code class="language-sql">-- 방법 A : 순방향 패턴 + 끝 노드를 박삼현으로 고정 
SELECT Person1.Name AS Follower 
FROM Person AS Person1, follows, Person AS Person2 
WHERE MATCH(Person1-(follows)-&gt;Person2) 
  AND Person2.Name = N&#39;박삼현&#39;; 

-- 방법 B : 역방향 화살표 사용 
SELECT Person1.Name AS Follower 
FROM Person AS Person1, follows, Person AS Person2 
WHERE MATCH(Person2&lt;-(follows)-Person1) 
  AND Person2.Name = N&#39;박삼현&#39;; </code></pre>
<blockquote>
<p>💡  두 표현은 어떻게 다른가요? 
결과는 동일합니다. SQL Server가 동일한 실행 계획으로 평가하기 때문에 성능 차이도 없습니다. 
가독성 차이만 있습니다. &quot;박삼현 입장에서 자기를 팔로우하는 사람&quot;이라는 관점이 자연스러울 때는 방법 B가 읽기 쉽고, &quot;전체 팔로우 관계 중 끝점이 박삼현&quot;이라는 관점이라면 방법 A가 자연스럽습니다. </p>
</blockquote>
<h2 id="33-다중-홉multi-hop-탐색">3.3 다중 홉(Multi-hop) 탐색</h2>
<p>관계형으로는 N홉마다 JOIN이 N-1번 늘어나지만, 그래프에서는 패턴에 화살표를 더 이어붙이기만 하면 됩니다. 
<img src="https://velog.velcdn.com/images/rudin_/post/ba73fe07-f06d-4a81-83f6-b55671938479/image.png" alt=""></p>
<h3 id="친구의-친구-찾기-2-hop">친구의 친구 찾기 (2-hop)</h3>
<pre><code class="language-sql">SELECT DISTINCT 
    Person1.Name AS Person, 
    Person2.Name AS Friend, 
    Person3.Name AS FriendOfFriend 
FROM 
    Person AS Person1, 
    follows AS f1, 
    Person AS Person2, 
    follows AS f2, 
    Person AS Person3 
WHERE MATCH(Person1-(f1)-&gt;Person2-(f2)-&gt;Person3) 
  AND Person1.Name = N&#39;김원일&#39; 
  AND Person1.PersonId &lt;&gt; Person3.PersonId;   -- 자기 자신 제외 </code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/d2389180-7d6e-43ce-acec-9110026d2535/image.png" alt=""></p>
<p>① FROM 절이 길어지는 이유 </p>
<p>Person1, Person2, Person3 — 같은 Person 테이블의 별칭이 3개. 엣지도 f1, f2 두 개로 별도 별칭을 부여합니다. 한 패턴 안에 같은 테이블이 여러 번 등장할 수 있기 때문에 별칭은 필수입니다. </p>
<p>② MATCH 패턴 — 화살표 잇기 </p>
<p>Person1-(f1)-&gt;Person2-(f2)-&gt;Person3. 두 엣지가 가운데 노드 Person2를 공유하면서 자연스럽게 이어집니다. 그래프의 ASCII 그림이 곧 우리가 찾고자 하는 경로의 모양. </p>
<p>③ DISTINCT가 필요한 이유 </p>
<p>같은 사람이 여러 경로로 도달 가능할 수 있습니다. 예를 들어 X가 다른 두 친구를 통해 동시에 친구의 친구가 되는 경우, DISTINCT 없이는 결과가 중복됩니다. </p>
<p>④ 자기 자신 제외 </p>
<p>Person1.PersonId &lt;&gt; Person3.PersonId. 만약 A→B→A라는 상호 팔로우가 있다면 A 자신이 친구의 친구로 잡혀버립니다. 이 조건으로 그런 경우를 제외합니다. </p>
<blockquote>
<p>⚠️  결과 폭발에 주의 
N홉 탐색은 평균 차수(degree)의 N제곱에 비례하는 경로 수를 만들어낼 수 있습니다. 
평균 팔로우 100명이라면 3홉만 해도 100^3 = 100만 경로 후보가 발생합니다. 
실무에서는 보통 2~3홉으로 제한하고, 시작 노드를 명확하게 좁히는 WHERE 조건을 함께 사용합니다. </p>
</blockquote>
<h3 id="3단계-연결-탐색-3-hop">3단계 연결 탐색 (3-hop)</h3>
<pre><code class="language-sql">SELECT DISTINCT 
    P1.Name AS Start, 
    P2.Name AS Hop1, 
    P3.Name AS Hop2, 
    P4.Name AS Hop3 
FROM 
    Person AS P1, follows AS f1, 
    Person AS P2, follows AS f2, 
    Person AS P3, follows AS f3, 
    Person AS P4 
WHERE MATCH(P1-(f1)-&gt;P2-(f2)-&gt;P3-(f3)-&gt;P4) 
  AND P1.Name = N&#39;김원일&#39;; </code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/a10fe6cd-e7a5-4bf6-9849-69a4b29ecd12/image.png" alt=""></p>
<h1 id="4-복합-패턴-쿼리">4. 복합 패턴 쿼리</h1>
<h2 id="41-여러-엣지-타입-결합">4.1 여러 엣지 타입 결합</h2>
<p>한 쿼리 안에서 서로 다른 종류의 엣지를 자유롭게 섞을 수 있습니다. 예를 들어 &quot;내가 팔로우하는 사람이 작성한 게시물&quot;은 follows + wrote 두 엣지 타입을 거치는 패턴입니다. </p>
<h3 id="내가-팔로우하는-사람이-작성한-게시물">내가 팔로우하는 사람이 작성한 게시물</h3>
<pre><code class="language-sql">SELECT 
    Person1.Name  AS Me, 
    Person2.Name  AS Following, 
    Post.Title    AS PostTitle 
FROM 
    Person AS Person1, 
    follows, 
    Person AS Person2, 
    wrote, 
    Post 
WHERE MATCH(Person1-(follows)-&gt;Person2-(wrote)-&gt;Post) 
  AND Person1.Name = N&#39;김원일&#39;; </code></pre>
<p>① 서로 다른 노드 타입의 등장 </p>
<p>이 쿼리는 Person 노드 두 개와 Post 노드 하나가 한 패턴에 등장합니다. 노드 별칭만 다르게 잡으면 한 그래프 안에 다양한 종류의 노드를 자유롭게 섞을 수 있습니다. </p>
<p>② 두 엣지 타입을 한 패턴에 </p>
<p>follows와 wrote는 의미가 전혀 다른 엣지지만, 가운데 Person2 노드를 공유하면서 자연스럽게 이어집니다. 패턴 표현 그대로 &quot;사람을 따라가서 그 사람이 쓴 글까지&quot; 한 줄로 표현되는 셈. </p>
<p>③ 관계형 모델과 비교 </p>
<p>같은 결과를 관계형으로 풀려면 Person ⨝ follows ⨝ Person ⨝ wrote ⨝ Post의 4단계 JOIN이 필요합니다. MATCH 패턴은 이 모든 JOIN 조건을 한 줄로 압축합니다. </p>
<h2 id="42-조건부-필터링과-집계">4.2 조건부 필터링과 집계</h2>
<h3 id="팔로워-수-계산">팔로워 수 계산</h3>
<pre><code class="language-sql">SELECT 
    Person2.Name, 
    COUNT(*) AS FollowerCount 
FROM Person AS Person1, follows, Person AS Person2 
WHERE MATCH(Person1-(follows)-&gt;Person2) 
GROUP BY Person2.Name, Person2.PersonId 
ORDER BY FollowerCount DESC; </code></pre>
<p>▶  실행 흐름 </p>
<p>1) follows 엣지 전체를 훑어 (시작자, 도착자) 쌍을 모두 만든다. 
2) Person2(도착자) 기준으로 그룹화한다. 
3) 각 그룹의 행 수를 세면 그 사람이 받은 팔로우 수가 된다. </p>
<p>샘플 그래프에서는 박삼현이 2명(김원일, 이두석)으로 1위, 그 외는 모두 1명</p>
<h3 id="인기-게시물-찾기-좋아요-수-기준">인기 게시물 찾기 (좋아요 수 기준)</h3>
<pre><code class="language-sql">SELECT 
    Post.Title, 
    COUNT(*) AS LikeCount 
FROM Person, likes, Post 
WHERE MATCH(Person-(likes)-&gt;Post) 
GROUP BY Post.PostId, Post.Title 
HAVING COUNT(*) &gt;= 2 
ORDER BY LikeCount DESC; </code></pre>
<p>① Person 별칭 생략 가능 </p>
<p>같은 테이블이 한 패턴에 한 번만 등장하면 별칭(AS …)을 생략하고 테이블명을 그대로 별칭처럼 사용할 수 있습니다. 가독성을 위해 명시하는 것이 보통이지만 짧은 쿼리에서는 생략도 흔합니다. </p>
<p>② GROUP BY에 PK 포함 </p>
<p>Title이 같다면 PostId 없이 Title만으로 그룹화하면 잘못 합쳐질 수 있습니다. 비즈니스 키를 GROUP BY에 함께 넣는 습관을 들이세요. </p>
<p>③ HAVING으로 사후 필터 </p>
<p>집계 결과에 대한 조건은 WHERE가 아니라 HAVING입니다. &quot;좋아요 2개 이상인 게시물만&quot;이라는 조건이 여기에 해당. </p>
<h1 id="5-실무-시나리오">5. 실무 시나리오</h1>
<h2 id="51-추천-시스템-구현">5.1 추천 시스템 구현</h2>
<h3 id="친구-추천-공통-친구-기반">친구 추천 (공통 친구 기반)</h3>
<p>소셜 네트워크의 클래식한 추천 알고리즘 중 하나는 &quot;공통 친구가 많은 사람&quot;을 우선 추천하는 방식입니다. 나의 1-hop 친구(공통 친구)를 거쳐서 도달 가능한 2-hop의 사람을 모두 모은 뒤, 같은 사람이 여러 경로로 도달될수록 점수가 높다고 보는 것이 핵심입니다. 
<img src="https://velog.velcdn.com/images/rudin_/post/2dbc362b-2245-4bbb-94ba-c5d68ddd8698/image.png" alt=""></p>
<pre><code class="language-sql">
-- 김원일에게 친구 추천: 공통 친구가 많은 사람 
SELECT 
    Recommended.Name AS RecommendedPerson, 
    COUNT(*)         AS CommonFriends 
FROM 
    Person  AS Me, 
    follows AS f1, 
    Person  AS CommonFriend, 
    follows AS f2, 
    Person  AS Recommended 
WHERE MATCH(Me-(f1)-&gt;CommonFriend-(f2)-&gt;Recommended) 
  AND Me.Name = N&#39;김원일&#39; 
  AND Me.PersonId &lt;&gt; Recommended.PersonId 
  -- 이미 팔로우하는 사람 제외 
  AND NOT EXISTS ( 
        SELECT 1 FROM follows AS existing -- 데이터 존재 여부(TRUE) 를 확인하는 성능 최적화용
        WHERE existing.$from_id = Me.$node_id 
          AND existing.$to_id   = Recommended.$node_id 
  ) 
GROUP BY Recommended.PersonId, Recommended.Name 
ORDER BY CommonFriends DESC; </code></pre>
<p>① Me-(f1)-&gt;CommonFriend-(f2)-&gt;Recommended </p>
<p>2-hop 패턴. 가운데 CommonFriend는 &quot;내가 팔로우하는 사람&quot;이자 &quot;Recommended를 팔로우하는 사람&quot; 두 역할을 동시에 수행합니다. </p>
<p>② COUNT(*)의 의미 </p>
<p>같은 Recommended가 서로 다른 CommonFriend를 통해 여러 번 도달되면, 그만큼 (Me, CommonFriend, Recommended) 행이 여러 개 나옵니다. 이걸 GROUP BY Recommended로 묶어 COUNT하면 곧 &quot;공통 친구의 수&quot;가 됩니다. </p>
<p>③ 자기 자신 제외 </p>
<p>A→B→A 형태의 상호 팔로우가 있을 때 자기가 자기에게 추천되는 것을 막습니다. </p>
<p>④ 이미 팔로우 중인 사람 제외 (NOT EXISTS) </p>
<p>Me.$node_id에서 Recommended.$node_id로 가는 follows 엣지가 이미 존재하면 추천 후보에서 빼야 합니다. NOT EXISTS는 결과 1건만 확인되면 빠르게 끝나기 때문에 NOT IN 보다 효율적이고 NULL 안전합니다. </p>
<p>⑤ 결과 해석 </p>
<p>샘플 그래프에서 김원일에게 추천될 수 있는 후보는 정사람뿐(박삼현 → 정사람 경로). 이두석 → 박삼현 경로는 박삼현이 이미 팔로우 중이라 NOT EXISTS에서 걸러집니다. </p>
<h3 id="select-1">select 1</h3>
<ul>
<li>데이터 값 자체는 필요 없고, 조건을 만족하는 행이 존재하는지만 확인하고 싶을 때</li>
<li>DB 연결 테스트</li>
<li>쿼리 동작 확인용</li>
</ul>
<h3 id="콘텐츠-추천-팔로우하는-사람들이-좋아한-게시물">콘텐츠 추천 (팔로우하는 사람들이 좋아한 게시물)</h3>
<pre><code class="language-sql">SELECT 
    Post.Title, 
    COUNT(DISTINCT Following.PersonId) AS LikedByFollowing 
FROM 
    Person  AS Me, 
    follows, 
    Person  AS Following, 
    likes, 
    Post 
WHERE MATCH(Me-(follows)-&gt;Following-(likes)-&gt;Post) 
  AND Me.Name = N&#39;김원일&#39; 
  -- 내가 이미 좋아요한 게시물 제외 
  AND NOT EXISTS ( 
        SELECT 1 FROM likes AS myLikes 
        WHERE myLikes.$from_id = Me.$node_id 
          AND myLikes.$to_id   = Post.$node_id 
  ) 
GROUP BY Post.PostId, Post.Title 
ORDER BY LikedByFollowing DESC; </code></pre>
<h3 id="💡--count-vs-countdistinct--차이">💡  COUNT(*) vs COUNT(DISTINCT …) 차이</h3>
<p>여기서 COUNT(<em>)를 쓰면 &quot;내 팔로우 친구 한 명이 같은 게시물에 두 번 좋아요한 경우&quot;가 잘못 가중되어 계산됩니다. 
COUNT(DISTINCT Following.PersonId)는 &quot;이 게시물을 좋아한 (서로 다른) 친구 수&quot;라는 정확한 지표가 됩니다. 데이터 모델상 한 사람이 같은 게시물을 두 번 좋아요 할 수 없다면(원천적으로 안된다면) COUNT(</em>)도 무방하지만, 안전한 기본값은 DISTINCT. </p>
<h2 id="52-영향력-분석">5.2 영향력 분석</h2>
<h3 id="인플루언서-찾기">인플루언서 찾기</h3>
<p>&quot;받은 팔로워 수&quot;와 &quot;내 글이 받은 좋아요 수&quot;를 합산해 영향력 점수를 계산합니다. CTE(공통 테이블 표현식)로 두 지표를 따로 구하고 마지막에 LEFT JOIN으로 합치는 패턴입니다. </p>
<pre><code class="language-sql">-- 팔로워 수와 게시물 좋아요 수를 합산한 영향력 점수 
WITH FollowerCounts AS ( 
    SELECT 
        Person2.PersonId, 
        COUNT(*) AS Followers 
    FROM Person AS Person1, follows, Person AS Person2 
    WHERE MATCH(Person1-(follows)-&gt;Person2) 
    GROUP BY Person2.PersonId 
), 
LikeCounts AS ( 
    SELECT 
        Author.PersonId, 
        COUNT(*) AS TotalLikes 
    FROM Person AS Liker, likes, Post, wrote, Person AS Author 
    WHERE MATCH(Liker-(likes)-&gt;Post&lt;-(wrote)-Author) 
    GROUP BY Author.PersonId 
) 
SELECT 
    p.Name, 
    ISNULL(f.Followers, 0)   AS Followers, 
    ISNULL(l.TotalLikes, 0)  AS TotalLikes, 
    ISNULL(f.Followers, 0) + ISNULL(l.TotalLikes, 0) AS InfluenceScore 
FROM Person p 
LEFT JOIN FollowerCounts f ON p.PersonId = f.PersonId 
LEFT JOIN LikeCounts     l ON p.PersonId = l.PersonId 
ORDER BY InfluenceScore DESC; </code></pre>
<p>① CTE 1 — FollowerCounts </p>
<p>4.2의 팔로워 수 쿼리를 그대로 가져와 &quot;PersonId별 팔로워 수&quot;를 임시 테이블처럼 다룹니다. CTE는 메인 쿼리에서 한 번만 사용해도 가독성을 크게 높입니다. </p>
<p>② CTE 2 — LikeCounts와 역방향 패턴 </p>
<p>Liker-(likes)-&gt;Post&lt;-(wrote)-Author. 좋아요한 사람으로부터 게시물로 가고, 그 게시물을 누가 썼는지 역방향으로 가져옵니다. 한 패턴 안에서 &quot;정방향 + 역방향&quot; 혼용이 가능합니다. </p>
<p>③ LEFT JOIN으로 모든 사람 보존 </p>
<p>글을 쓴 적 없거나 팔로워가 0인 사람도 결과에 나타나야 합니다. INNER JOIN으로 묶으면 이런 사람이 사라지므로 LEFT JOIN + ISNULL 패턴이 정석. </p>
<p>④ ISNULL로 NULL을 0으로 </p>
<p>집계 결과가 없는 사람은 Followers/TotalLikes가 NULL입니다. ISNULL(…, 0)로 0 처리해야 더하기 연산이 망가지지 않습니다. </p>
<blockquote>
<p>⚠️  실무 단순화 주의 
&quot;팔로워 수 + 좋아요 수&quot; 단순합은 학습용 예시입니다. 
실무에서는 PageRank, HITS, Eigenvector centrality 같은 지표가 훨씬 견고합니다 — &quot;유명한 사람을 많이 팔로우하는 게 단순히 무명인 사람 100명을 팔로우하는 것보다 점수가 높아야&quot; 하기 때문입니다. 
SQL Graph 자체는 PageRank 내장 함수를 제공하지 않으므로, 본격 분석은 외부 그래프 엔진(Neo4j GDS, Spark GraphFrames 등)이나 Python(networkx)으로 옮겨 수행하는 것이 일반적입니다. </p>
</blockquote>
<h1 id="6-shortest_path--최단-경로-탐색">6. SHORTEST_PATH — 최단 경로 탐색</h1>
<p>SQL Server 2019부터는 두 노드 간의 최단 경로를 자동으로 찾아주는 SHORTEST_PATH 키워드가 추가되었습니다. &quot;6단계 분리 이론&quot;을 직접 검증해 볼 수 있는 강력한 기능입니다. (Azure SQL Database도 동일한 호환성 레벨에서 지원합니다.)</p>
<p><img src="https://velog.velcdn.com/images/rudin_/post/ff3d0356-628c-4a95-b939-859fb9fad4db/image.png" alt=""></p>
<h2 id="61-x명까지의-모든-도달-가능-노드">6.1: &quot;X명까지의 모든 도달 가능 노드&quot;</h2>
<pre><code class="language-sql">-- 김원일에서 출발해 follows 경로상 모든 도달 가능한 사람과 거리 
SELECT 
    LAST_VALUE(Person2.Name) WITHIN GROUP (GRAPH PATH) AS Reachable, 
    STRING_AGG(Person2.Name, &#39; → &#39;) WITHIN GROUP (GRAPH PATH) AS Path, 
    COUNT(Person2.PersonId)  WITHIN GROUP (GRAPH PATH) AS Distance 
FROM 
    Person AS Person1, 
    follows FOR PATH AS f, 
    Person  FOR PATH AS Person2 
WHERE MATCH( SHORTEST_PATH( Person1( -(f)-&gt;Person2 )+ ) ) 
  AND Person1.Name = N&#39;김원일&#39; 
ORDER BY Distance; </code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/2e8fa673-e798-4325-be86-e65acb826387/image.png" alt=""></p>
<p>이 쿼리는 김원일이 follows 경로로 도달 가능한 모든 사람과 그 사람까지의 최단 거리, 그리고 거치는 경로를 한꺼번에 반환합니다. 거리 1(직접 팔로우), 거리 2(친구의 친구), 거리 3 형태로 자연스럽게 묶입니다.</p>
<blockquote>
<p>⚠️SHORTEST_PATH 사용상 주의 
SHORTEST_PATH는 &quot;엣지 가중치&quot;를 고려하지 않는 단순 BFS입니다. 가중치가 있는 최단 경로(다익스트라)가 필요하면 SQL Graph로는 직접 구현할 수 없고, 외부 그래프 엔진을 사용해야 합니다. 
하나의 패턴 안에 SHORTEST_PATH는 한 번만 사용 가능합니다. 
큰 그래프에서는 시작 노드를 매우 좁게 좁히지 않으면 (예: WHERE Person1.Name = …) 폭발적으로 느려질 수 있습니다. 
데이터베이스 호환성 레벨이 140 이상(SQL Server 2019, Azure SQL DB 최신)이어야 합니다. </p>
</blockquote>
<h1 id="7-성능-최적화">7. 성능 최적화</h1>
<h2 id="71-인덱스-전략">7.1 인덱스 전략</h2>
<p>그래프 테이블도 결국 일반 테이블이므로, 일반 SQL Server의 인덱스 전략이 그대로 적용됩니다. 다만 자주 검색되는 시작 노드의 비즈니스 키와, 엣지의 $from_id, $to_id 시스템 컬럼에 인덱스가 잘 걸려 있는지가 핵심입니다. </p>
<pre><code class="language-sql">
-- 노드 테이블의 자주 검색되는 컬럼에 인덱스 
CREATE INDEX IX_Person_Name  ON Person(Name); 
CREATE INDEX IX_Post_Title   ON Post(Title);

-- 엣지 테이블의 시스템 컬럼에 인덱스 (★ 가장 중요) 
CREATE INDEX IX_follows_from ON follows($from_id); 
CREATE INDEX IX_follows_to   ON follows($to_id); 
CREATE INDEX IX_likes_from   ON likes($from_id); 
CREATE INDEX IX_likes_to     ON likes($to_id); 
CREATE INDEX IX_wrote_from   ON wrote($from_id); 
CREATE INDEX IX_wrote_to     ON wrote($to_id); </code></pre>
<p>① 시작점 좁히기용 인덱스 </p>
<p>IX_Person_Name 같은 비즈니스 컬럼 인덱스는 &quot;Name = N&#39;김원일&#39;&quot; 같은 조건이 시작 노드를 빠르게 찾도록 도와줍니다. 시작 노드를 좁히지 못하면 그래프 전체를 훑게 되어 성능 저하의 가장 흔한 원인이 됩니다. </p>
<p>② $from_id, $to_id 인덱스 </p>
<p>그래프 탐색은 본질적으로 &quot;엣지의 한쪽 끝 ID를 기준으로 다른 쪽을 찾는&quot; 연산의 반복입니다. 이 두 컬럼이 인덱스 없이 풀 스캔되면 N홉 쿼리가 N제곱으로 느려집니다. 엣지 테이블을 만들면 거의 자동 반사로 두 인덱스를 함께 만드세요. </p>
<p>③ 복합 인덱스 고려 </p>
<p>특정 조건(예: 특정 기간의 follows만)이 자주 함께 들어간다면 ($from_id, FollowDate) 같은 복합 인덱스가 더 유리할 수 있습니다. 단순 단일 컬럼 인덱스만 무한정 만드는 것이 능사는 아닙니다. </p>
<h2 id="72-모범-사례">7.2 모범 사례</h2>
<h3 id="1-명명-규칙">1. 명명 규칙</h3>
<p>Node는 단수 명사 단수형(Person, Post), Edge는 동사 또는 관계명(follows, likes, wrote). 엣지가 동사형이면 패턴이 자연어처럼 읽힙니다. </p>
<h3 id="2-데이터-무결성">2. 데이터 무결성</h3>
<p>엣지 제약 조건을 적극 활용해 잘못된 노드 타입 사이에 엣지가 만들어지지 않도록. </p>
<h3 id="3-성능-모니터링">3. 성능 모니터링</h3>
<p>실행 계획에서 Edge Scan, Filter 비용이 큰 단계를 찾아내고, 인덱스/통계 갱신을 주기적으로 수행. </p>
<h3 id="4-홉-수-제한">4. 홉 수 제한</h3>
<p>가능한 한 명시적인 N-hop 쿼리(2~3홉)로 작성하고, 무제한 SHORTEST_PATH는 시작 노드가 충분히 좁혀진 경우에만. </p>
<h1 id="8-관계형--그래프-통합">8. 관계형 + 그래프 통합</h1>
<h2 id="81-하이브리드-쿼리">8.1 하이브리드 쿼리</h2>
<p>Graph DB의 가장 큰 장점 중 하나는 기존 관계형 테이블과 자유롭게 결합할 수 있다는 점입니다. 그래프 패턴 + 일반 JOIN을 한 쿼리에 섞을 수 있어, 별도 NoSQL 그래프 DB로 ETL할 필요 없이 같은 데이터베이스 안에서 모든 분석을 할 수 있습니다. </p>
<pre><code class="language-sql">-- 가정: 일반 관계형 테이블 UserActivity가 있다 
CREATE TABLE UserActivity ( 
    ActivityId    INT IDENTITY PRIMARY KEY, 
    PersonId      INT, 
    ActivityType  NVARCHAR(50), 
    ActivityDate  DATETIME2 DEFAULT GETDATE() 
); 

-- 그래프 패턴 + 관계형 JOIN을 한 쿼리에 섞기 
SELECT 
    p.Name, 
    COUNT(DISTINCT f2.$to_id)  AS FollowingCount, 
    COUNT(DISTINCT ua.ActivityId) AS RecentActivities 
FROM Person p 
LEFT JOIN follows f2     ON f2.$from_id = p.$node_id 
LEFT JOIN UserActivity ua 
       ON p.PersonId = ua.PersonId 
      AND ua.ActivityDate &gt;= DATEADD(day, -7, GETDATE()) 
GROUP BY p.PersonId, p.Name; </code></pre>
<p>① $node_id로 직접 JOIN </p>
<p>MATCH 절을 쓰지 않고도, follows 엣지의 $from_id와 Person의 $node_id를 직접 ON 조건으로 묶을 수 있습니다. 그래프 패턴 표현이 어색한 상황에서 유용한 우회로. </p>
<p>② 관계형 테이블과 자연스럽게 결합 </p>
<p>UserActivity는 그래프와 무관한 일반 테이블이지만, PersonId라는 비즈니스 키로 자연스럽게 LEFT JOIN됩니다. 같은 데이터베이스 안에 있다는 것의 큰 이점. </p>
<p>③ COUNT(DISTINCT)의 두 용도 </p>
<p>f2.$to_id를 DISTINCT 카운트하면 &quot;팔로잉 인원&quot;을, ua.ActivityId를 DISTINCT 카운트하면 &quot;활동 건수&quot;를 동시에 한 GROUP BY 안에서 산출할 수 있습니다. 두 측면을 각각 별도 쿼리로 만들고 합치는 수고를 덜어줍니다. </p>
<h2 id="82-그래프-데이터를-json으로-내보내기">8.2 그래프 데이터를 JSON으로 내보내기</h2>
<p>그래프를 외부 시스템(D3.js, 시각화 도구 등)에 전달할 때 흔한 형식은 nodes/edges가 분리된 JSON입니다.</p>
<pre><code class="language-sql">-- 노드를 JSON 배열로 
SELECT 
    PersonId   AS id, 
    Name       AS label, 
    &#39;Person&#39;   AS type 
FROM Person 
FOR JSON PATH, ROOT(&#39;nodes&#39;);  

-- 엣지를 JSON 배열로 
SELECT 
    Person1.PersonId   AS source, 
    Person2.PersonId   AS target, 
    &#39;follows&#39;          AS type 
FROM Person AS Person1, follows, Person AS Person2 
WHERE MATCH(Person1-(follows)-&gt;Person2) 
FOR JSON PATH, ROOT(&#39;edges&#39;); </code></pre>
<blockquote>
<p>💡  FOR JSON PATH의 결과 형태 
{ &quot;nodes&quot;: [ {&quot;id&quot;:1,&quot;label&quot;:&quot;김원일&quot;,&quot;type&quot;:&quot;Person&quot;}, … ] } 형태가 한 줄로 반환됩니다. 
필요하면 두 쿼리 결과를 애플리케이션 단에서 합쳐 { nodes:[…], edges:[…] } 형태로 만든 뒤 D3.js force layout 등으로 렌더링하면 됩니다. 
대용량 그래프라면 FOR JSON 결과가 행당 2GB 한도에 부딪힐 수 있으므로 페이지 단위로 잘라 내보내는 패턴이 안전합니다. </p>
</blockquote>
<h1 id="9-자주-하는-실수--트러블슈팅">9. 자주 하는 실수 / 트러블슈팅</h1>
<h2 id="91-노드엣지-정의-실수">9.1 노드/엣지 정의 실수</h2>
<h3 id="⚠️--실수-1-as-node--as-edge-키워드-누락">⚠️  실수 1: AS NODE / AS EDGE 키워드 누락</h3>
<p>증상: 평범한 테이블이 만들어지지만, 이후 MATCH 쿼리에서 &quot;테이블이 그래프 노드/엣지가 아닙니다&quot; 류 에러가 발생. </p>
<p>원인: CREATE TABLE 마지막의 AS NODE 또는 AS EDGE를 빠뜨림. </p>
<p>해결: 테이블을 DROP한 뒤 다시 생성하거나, 새 그래프 테이블을 만들고 데이터를 옮긴 뒤 교체. (일반 테이블 → 그래프 테이블로의 자동 변환은 지원되지 않음.) </p>
<h3 id="⚠️--실수-2-엣지-테이블에-primary-key-추가">⚠️  실수 2: 엣지 테이블에 PRIMARY KEY 추가</h3>
<p>증상: 같은 두 노드 사이에 여러 엣지를 만들 수 없게 됨. 예) &quot;한 사람이 같은 게시물을 두 번 좋아요&quot;가 안 됨. </p>
<p>원인: $edge_id가 시스템 PK 역할을 하는데, 추가로 사용자 PK를 두면 의도치 않은 유일성 제약이 걸림. </p>
<p>해결: 엣지 테이블에는 일반 PK를 두지 않습니다. 유일성이 정말 필요하면 UNIQUE INDEX로 명시적으로 표현. </p>
<h2 id="92-insert-단계의-실수">9.2 INSERT 단계의 실수</h2>
<h3 id="⚠️--실수-3-node_id를-직접-insert하려-함">⚠️  실수 3: $node_id를 직접 INSERT하려 함</h3>
<p>증상: &quot;$node_id 컬럼에 직접 값을 삽입할 수 없습니다&quot; 류 에러. </p>
<p>원인: $node_id는 시스템이 자동 생성하는 컴퓨티드 컬럼. 사용자가 값을 넣을 수 없습니다. </p>
<p>해결: INSERT INTO Person (PersonId, Name, …) VALUES … 형태로, 시스템 컬럼은 빼고 비즈니스 컬럼만 명시. </p>
<h3 id="⚠️--실수-4-엣지-insert-시-null-from_id--to_id">⚠️  실수 4: 엣지 INSERT 시 NULL $from_id / $to_id</h3>
<p>증상: 엣지가 만들어졌지만 MATCH 쿼리에서 잡히지 않음. </p>
<p>원인: 서브쿼리 (SELECT $node_id FROM Person WHERE PersonId = 999)가 매칭 행이 없어 NULL을 반환했고, NULL인 채로 INSERT됨. </p>
<p>해결: 서브쿼리 작성 시 비즈니스 키가 실제 존재하는지 먼저 확인. 또는 INSERT 전에 EXISTS 체크를 추가하거나, 트랜잭션 + RAISERROR 패턴으로 안전망 구성.</p>
<h2 id="93-match-절-작성-실수">9.3 MATCH 절 작성 실수</h2>
<h3 id="⚠️--실수-5-match를-select-절에-적음">⚠️  실수 5: MATCH를 SELECT 절에 적음</h3>
<p>증상: &quot;MATCH 절은 WHERE 절에서만 사용할 수 있습니다&quot; 에러. </p>
<p>원인: 익숙한 SQL 사고방식으로 SELECT나 FROM 위치에 MATCH를 두려고 함. </p>
<p>해결: MATCH는 반드시 WHERE 절 안에 위치. 일반 비교 조건은 AND로 자유롭게 결합. </p>
<h3 id="⚠️--실수-6-from-절에-같은-테이블-별칭-누락">⚠️  실수 6: FROM 절에 같은 테이블 별칭 누락</h3>
<p>증상: &quot;Person이 두 번 사용되었지만 별칭이 없습니다&quot; 류 에러. </p>
<p>원인: 한 패턴에 같은 노드 타입이 여러 번 등장할 때 별칭(AS Person1, AS Person2)을 부여하지 않음. </p>
<p>해결: 같은 테이블이 한 패턴에 두 번 이상 나오면 반드시 다른 별칭을 부여. </p>
<h3 id="⚠️--실수-7-match-패턴-안에-비교-연산자">⚠️  실수 7: MATCH 패턴 안에 비교 연산자</h3>
<p>증상: 컴파일 에러. </p>
<p>원인: MATCH(Person1.Name = N&#39;김원일&#39;-(follows)-&gt;Person2) 처럼 패턴 안에 일반 비교를 넣으려 함. </p>
<p>해결: MATCH 안에는 그래프 패턴만. 일반 비교는 AND로 분리. </p>
<h2 id="94-shortest_path-관련-실수">9.4 SHORTEST_PATH 관련 실수</h2>
<h3 id="⚠️--실수-8-for-path-키워드-누락">⚠️  실수 8: FOR PATH 키워드 누락</h3>
<p>증상: &quot;SHORTEST_PATH 함수의 인수에는 FOR PATH 별칭이 필요합니다&quot; 류 에러. </p>
<p>원인: 가변 길이 패턴 안의 노드/엣지 변수에 FOR PATH 표시가 빠짐. </p>
<p>해결: follows FOR PATH AS f, Person FOR PATH AS Person2 형태로 모든 반복 패턴 변수에 FOR PATH 추가. </p>
<h3 id="⚠️--실수-9-호환성-레벨이-낮음">⚠️  실수 9: 호환성 레벨이 낮음</h3>
<p>증상: SHORTEST_PATH 키워드 자체를 인식하지 못함. </p>
<p>원인: 데이터베이스 호환성 레벨 &lt; 140. </p>
<p>해결: ALTER DATABASE [DBName] SET COMPATIBILITY_LEVEL = 140; (또는 더 높음). Azure SQL DB에서는 보통 자동으로 최신 레벨이지만, 마이그레이션된 DB는 확인 필요. </p>
<h2 id="95-성능-관련-실수">9.5 성능 관련 실수</h2>
<h3 id="⚠️--실수-10-시작-노드를-좁히지-않은-n-hop-쿼리">⚠️  실수 10: 시작 노드를 좁히지 않은 N-hop 쿼리</h3>
<p>증상: 데이터가 조금만 커져도 쿼리가 수십 초 ~ 수 분. </p>
<p>원인: WHERE 절에 시작 노드를 좁히는 조건이 없어, 모든 노드 쌍에 대해 N-hop 패턴을 평가함. </p>
<p>해결: AND Person1.Name = …, Person1.PersonId = … 같은 시작 노드 식별 조건을 반드시 함께 사용. 인덱스(7.1)도 함께 점검. </p>
<h3 id="⚠️--실수-11-from_id--to_id-인덱스-부재">⚠️  실수 11: $from_id / $to_id 인덱스 부재</h3>
<p>증상: 작은 그래프에서는 빠르지만 데이터 증가에 따라 N제곱으로 느려짐. </p>
<p>원인: 엣지의 $from_id, $to_id에 인덱스가 없어, 매 홉마다 풀 스캔. </p>
<p>해결: 7.1의 모든 엣지 테이블에 ($from_id), ($to_id) 인덱스를 만들고, 통계를 최신 상태로 유지. </p>
<h2 id="96-진단용-sql-모음">9.6 진단용 SQL 모음</h2>
<pre><code class="language-sql">-- (1) 모든 그래프 테이블 목록 
SELECT name, is_node, is_edge 
FROM sys.tables 
WHERE is_node = 1 OR is_edge = 1; 

-- (2) 그래프 테이블의 시스템 컬럼 확인 
SELECT t.name AS TableName, c.name AS ColumnName, c.graph_type_desc 
FROM sys.tables t 
JOIN sys.columns c ON t.object_id = c.object_id 
WHERE (t.is_node = 1 OR t.is_edge = 1) AND c.graph_type IS NOT NULL 
ORDER BY t.name, c.column_id;  

-- (3) 엣지 제약 조건 목록 
SELECT OBJECT_NAME(parent_object_id) AS EdgeTable, name AS ConstraintName 
FROM sys.edge_constraints; 

-- (4) 호환성 레벨 확인 
SELECT name, compatibility_level 
FROM sys.databases 
WHERE name = DB_NAME(); </code></pre>
<hr>

<h1 id="neo4j와-azure-기반-금융-사기-탐지-실습">Neo4j와 Azure 기반 금융 사기 탐지 실습</h1>
<h2 id="1장-그래프-데이터베이스-이론">1장. 그래프 데이터베이스 이론</h2>
<h3 id="14-lpg-vs-rdf--두-가지-그래프-모델">1.4 LPG vs RDF — 두 가지 그래프 모델</h3>
<p> LPG(Labeled Property Graph) 는 노드와 엣지에 ‘라벨(타입)’을 붙이고 ‘속성(key=value)’을 자유롭게 달 수 있는 모델로, Neo4j가 대표적입니다. RDF(Resource Description Framework) 는 W3C 표준으로 모든 데이터를 ‘주어-서술어-목적어(triple)’로 표현하며, 시맨틱 웹과 지식 그래프 분야에서 강세입니다.
실무에서는 표현력과 학습 곡선의 균형이 좋은 LPG가 더 널리 채택되고 있습니다.</p>
<h3 id="15-그래프-db를-써야-할-세-가지-신호">1.5 그래프 DB를 써야 할 ‘세 가지 신호’</h3>
<p>모든 데이터를 그래프에 담으려는 것은 망치를 든 사람에게 모든 것이 못으로 보이는 함정입니다. 다음 중 둘 이상이 해당될 때 그래프 DB가 적합합니다.</p>
<ul>
<li>관계의 깊이가 3-hop 이상. SNS의 친구 추천, 사기 탐지의 우회 거래 등.</li>
<li>관계 자체가 분석 대상. ‘무엇이 연결되어 있는가’가 ‘무엇이 있는가’보다 중요한 도메인. 지식 그래프, 추천 시스템, 사회망 분석.</li>
<li>스키마가 자주 변하거나 다양한 관계 타입이 등장. 새로운 관계 타입을 추가하려고 매번 ALTER TABLE을 하지 않아도 됨.<blockquote>
<p>단순 트랜잭션 처리(OLTP), 회계장부형 데이터, 전통적 보고서 생성, 컬럼 단위 집계 분석 등은 RDB나 데이터 웨어하우스가 훨씬 효율적입니다. 그래프 DB를 강제로 도입하면 오히려 복잡도만 늘어납니다.</p>
</blockquote>
</li>
</ul>
<h3 id="16-주요-제품-비교">1.6 주요 제품 비교</h3>
<table>
<thead>
<tr>
<th>제품</th>
<th>모델</th>
<th>특징</th>
<th>운영 형태</th>
</tr>
</thead>
<tbody><tr>
<td>Neo4j</td>
<td>LPG</td>
<td>Cypher 표준, 가장 큰 생태계</td>
<td>셀프호스팅 / AuraDB</td>
</tr>
<tr>
<td>Amazon Neptune</td>
<td>LPG + RDF</td>
<td>AWS 통합, Gremlin/SPARQL 지원</td>
<td>AWS 매니지드</td>
</tr>
<tr>
<td>Azure Cosmos DB (Gremlin API)</td>
<td>LPG</td>
<td>Azure 통합, 글로벌 분산</td>
<td>Azure 매니지드</td>
</tr>
<tr>
<td>TigerGraph</td>
<td>LPG</td>
<td>분산 처리 강점, GSQL</td>
<td>엔터프라이즈</td>
</tr>
<tr>
<td>ArangoDB</td>
<td>다중모델</td>
<td>그래프 + 문서 + KV 동시 지원</td>
<td>오픈소스</td>
</tr>
</tbody></table>
<h2 id="2장-neo4j-핵심-개념">2장. Neo4j 핵심 개념</h2>
<h3 id="21-neo4j-아키텍처-한눈에">2.1 Neo4j 아키텍처 한눈에</h3>
<p>Neo4j는 JVM 위에서 동작하는 단일 프로세스 데이터베이스입니다. 클라이언트는 두 가지 프로토콜로 접속합니다. HTTP(7474)는 웹 브라우저 기반 Neo4j Browser용이고, Bolt(7687)은 애플리케이션 드라이버용 바이너리 프로토콜입니다.
스토리지는 ‘기록 파일(record store)’ 구조로, 각 노드와 관계는 고정 크기 레코드로 저장되어 ID 기반 직접 접근(O(1))이 가능합니다. 이것이 N-hop 트래버설이 빠른 핵심 비결입니다 — JOIN 비용 없이 인접 관계의 메모리 주소를 바로 따라갑니다(‘index-free adjacency’).</p>
<h3 id="22-lpg-데이터-모델">2.2 LPG 데이터 모델</h3>
<pre><code class="language-markdown">| 구성요소 | 설명 | 예시 |
|----------|------|------|
| Node | 개체, 라벨로 타입 구분 | (:Account) |
| Relationship | 노드 사이의 방향성 있는 관계 | [:TRANSFER] |
| Property | 노드/관계에 붙는 key=value 데이터 | {amount: 89500000} |
| Label | 노드의 분류 태그 (다중 부착 가능) | :Account:HighRisk |</code></pre>
<h3 id="23-cypher-쿼리-언어">2.3 Cypher 쿼리 언어</h3>
<p>Cypher는 Neo4j가 만든 그래프 질의 언어로, 2018년 ISO/IEC GQL 표준의 기반이 되었습니다. 핵심 아이디어는 ‘ASCII 아트로 패턴을 그린다’ 입니다.</p>
<pre><code>// &#39;계좌 A가 계좌 B에게 송금했다&#39;를 그림으로
MATCH (a:Account)-[:TRANSFER]-&gt;(b:Account)
RETURN a, b
LIMIT 5;</code></pre><table>
<thead>
<tr>
<th>절</th>
<th>역할</th>
<th>SQL 대응</th>
</tr>
</thead>
<tbody><tr>
<td>MATCH</td>
<td>패턴 매칭 (데이터 찾기)</td>
<td>FROM + JOIN + WHERE</td>
</tr>
<tr>
<td>WHERE</td>
<td>조건 필터</td>
<td>WHERE</td>
</tr>
<tr>
<td>RETURN</td>
<td>결과 반환</td>
<td>SELECT</td>
</tr>
<tr>
<td>CREATE</td>
<td>노드/관계 생성</td>
<td>INSERT</td>
</tr>
<tr>
<td>MERGE</td>
<td>있으면 매칭, 없으면 생성</td>
<td>UPSERT</td>
</tr>
<tr>
<td>WITH</td>
<td>중간 결과 파이프라인</td>
<td>서브쿼리 + AS</td>
</tr>
</tbody></table>
<h3 id="24-인덱스와-제약조건">2.4 인덱스와 제약조건</h3>
<p>인덱스 없는 그래프 DB는 인덱스 없는 RDB보다도 더 빨리 느려집니다. 첫 노드를 어떻게 ‘찾을 것인가(seek)’가 트래버설의 시작점이기 때문입니다. Neo4j 5.x에서는 다음 두 가지를 거의 항상 만들어 둡니다.</p>
<pre><code>// 고유성 제약 (자동으로 인덱스 생성됨)
CREATE CONSTRAINT account_id IF NOT EXISTS
  FOR (a:Account) REQUIRE a.accountId IS UNIQUE;

// 일반 인덱스 (자주 조회하는 속성)
CREATE INDEX account_country IF NOT EXISTS
  FOR (a:Account) ON (a.country);</code></pre><h3 id="25-gds--graph-data-science-라이브러리">2.5 GDS — Graph Data Science 라이브러리</h3>
<p>Neo4j의 진가는 단순 패턴 매칭을 넘어 그래프 알고리즘까지 한 자리에서 실행할 수 있다는 점입니다. GDS 라이브러리는 다음과 같은 알고리즘을 한 줄로 호출할 수 있게 해 줍니다.</p>
<table>
<thead>
<tr>
<th>분류</th>
<th>알고리즘</th>
<th>활용 예</th>
</tr>
</thead>
<tbody><tr>
<td>중심성</td>
<td>PageRank, Betweenness</td>
<td>허브 계좌 탐지, 영향력 분석</td>
</tr>
<tr>
<td>커뮤니티</td>
<td>Louvain, Label Propagation, WCC</td>
<td>공모 집단, 클러스터링</td>
</tr>
<tr>
<td>경로</td>
<td>Shortest Path, A*</td>
<td>최단 자금 흐름</td>
</tr>
<tr>
<td>유사도</td>
<td>Node Similarity, Jaccard</td>
<td>추천 시스템</td>
</tr>
<tr>
<td>임베딩</td>
<td>FastRP, GraphSAGE, Node2Vec</td>
<td>ML 피처 생성</td>
</tr>
</tbody></table>
<blockquote>
<p>📌  GDS의 동작 원리
GDS는 디스크의 그래프를 그대로 쓰지 않고, 분석 대상 부분을 ‘메모리에 투영(project)’한 뒤 알고리즘을 돌립니다. 큰 그래프에서도 빠른 이유이자, 분석이 끝나면 명시적으로 drop하지 않으면 메모리에 남는 이유이기도 합니다.</p>
</blockquote>
<h2 id="3장-azure-환경-구축">3장. Azure 환경 구축</h2>
<p>NSG로 본인 IP만 허용 → 7474, 7687 포트 한정 개방
<img src="https://velog.velcdn.com/images/rudin_/post/b740cd48-7ad9-4319-b10a-cfd29a20c0e7/image.png" alt=""></p>
<p>이후 vm 내에서 neo4j 설치</p>
<ul>
<li>APT 업데이트 + Java 21 설치 — Neo4j 5.x는 OpenJDK 21을 요구합니다.</li>
<li>Neo4j 공식 GPG 키 + APT 저장소 등록 — debian.neo4j.com을 신뢰 저장소로 추가.</li>
<li>apt install neo4j — 5.26.x 안정 버전 설치.</li>
<li>외부 접속 허용 — 기본은 127.0.0.1만 listen하므로 0.0.0.0으로 변경. 보안은 NSG가 담당.</li>
<li>서비스 시작 + 검증 — systemctl로 부팅 시 자동 시작 등록.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/rudin_/post/1ca64735-7345-4d44-bc2e-3344f7b99a8f/image.png" alt=""></p>
<blockquote>
<p>💡  메모리 튜닝의 황금률
Neo4j 성능의 80%는 메모리 설정이 좌우합니다. 일반적으로 &#39;heap size 2~4GB&#39; + &#39;나머지 메모리는 page cache&#39;에 할당합니다. 본 실습은 8GB VM 기준 heap 2GB + page cache 1GB로 설정합니다</p>
</blockquote>
<h2 id="4장-openpay-시나리오">4장. OpenPay 시나리오</h2>
<h3 id="41-도메인-모델링">4.1 도메인 모델링</h3>
<table>
<thead>
<tr>
<th>구성</th>
<th>분류</th>
<th>노드/관계</th>
</tr>
</thead>
<tbody><tr>
<td>계좌</td>
<td>노드</td>
<td>(:Account {accountId, name, country, createdAt})</td>
</tr>
<tr>
<td>디바이스</td>
<td>노드</td>
<td>(:Device {deviceId, type, fingerprint})</td>
</tr>
<tr>
<td>IP 주소</td>
<td>노드</td>
<td>(:IPAddress {ipId, address, country, isProxy})</td>
</tr>
<tr>
<td>계좌가 디바이스를 소유</td>
<td>관계</td>
<td>(:Account)-[:OWNS]-&gt;(:Device)</td>
</tr>
<tr>
<td>계좌가 IP에서 접속</td>
<td>관계</td>
<td>(:Account)-[:USED]-&gt;(:IPAddress)</td>
</tr>
<tr>
<td>계좌가 계좌에 송금</td>
<td>관계</td>
<td>(:Account)-[:TRANSFER {amount, timestamp, channel}]-&gt;(:Account)</td>
</tr>
</tbody></table>
<blockquote>
<p>💡  모델링 원칙
‘속성으로 표현할 수 있는 것은 노드로 만들지 마라.’ 처음 입문하면 거래 채널(MOBILE/WEB/ATM)도 노드로 만들고 싶어지지만, 이는 트래버설을 무겁게 만들 뿐입니다. 채널은 TRANSFER 관계의 속성으로 충분합니다. 반면 ‘디바이스’는 여러 계좌가 ‘공유’할 가능성이 핵심이므로 반드시 노드여야 합니다.</p>
</blockquote>
<h3 id="42-데이터-적재">4.2 데이터 적재</h3>
<p>부속 코드 02-data-generation/generate_data.py는 결정론적 시드(SEED=20260506)로 다음 데이터를 생성</p>
<p><img src="https://velog.velcdn.com/images/rudin_/post/cecf0529-1e2c-46d6-a772-de1df458c639/image.png" alt=""></p>
<pre><code>// CSV 파일을 한 줄씩 읽어 노드 생성
LOAD CSV WITH HEADERS FROM &#39;file:///accounts.csv&#39; AS row
CREATE (a:Account {
  accountId: row.accountId,
  name:      row.name,
  country:   row.country,
  createdAt: datetime(row.createdAt)
});

// 관계는 양 끝 노드를 MATCH로 찾고 CREATE
LOAD CSV WITH HEADERS FROM &#39;file:///transactions.csv&#39; AS row
CALL {
  WITH row
  MATCH (src:Account {accountId: row.fromAccount})
  MATCH (dst:Account {accountId: row.toAccount})
  CREATE (src)-[t:TRANSFER {
    txId:      row.txId,
    amount:    toFloat(row.amount),
    timestamp: datetime(row.timestamp),
    channel:   row.channel
  }]-&gt;(dst)
} IN TRANSACTIONS OF 1000 ROWS;
</code></pre><blockquote>
<p>⚠  Neo4j 5.x의 변화
Neo4j 4.x에서 쓰던 &#39;USING PERIODIC COMMIT&#39;은 5.x에서 deprecated되었습니다. 대신 &#39;CALL { ... } IN TRANSACTIONS OF N ROWS&#39; 패턴을 사용해야 큰 CSV도 메모리 부족 없이 적재됩니다.</p>
</blockquote>
<pre><code>MATCH (a:Account)   RETURN &#39;Account&#39;    AS label, count(a) AS cnt
UNION ALL
MATCH ()-[r:TRANSFER]-&gt;() RETURN &#39;TRANSFER&#39; AS label, count(r) AS cnt;</code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/dfcb329e-5b3c-4b53-92bc-9d1694f7b918/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/aa98f4fa-48c0-466c-931b-f5333583476b/image.png" alt=""></p>
<h2 id="5장-cypher-사기-탐지-실습">5장. Cypher 사기 탐지 실습</h2>
<h3 id="51-org-5-허브-계좌--단순-집계로-시작">5.1 [ORG-5] 허브 계좌 — 단순 집계로 시작</h3>
<p>가장 쉬운 패턴은 ‘비정상적으로 많이 받는 계좌’입니다. RDB의 GROUP BY와 본질적으로 같습니다.</p>
<pre><code>MATCH (sender:Account)-[t:TRANSFER]-&gt;(receiver:Account)
RETURN receiver.accountId AS account,
       count(t)           AS in_count,
       sum(t.amount)      AS total_received
ORDER BY in_count DESC
LIMIT 10;
</code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/e001f05a-c18a-487f-9539-357401642cca/image.png" alt=""></p>
<h3 id="52-org-3-smurfing--where-조건으로-패턴-좁히기">5.2 [ORG-3] SMURFING — WHERE 조건으로 패턴 좁히기</h3>
<p>‘100만원 미만 거래를 같은 수신자에게 10회 이상’ — CTR(고액현금거래보고) 기준선을 회피하려는 분할 송금 패턴입니다. WHERE로 금액 임계값을 걸고 count()로 횟수를 셉니다.</p>
<pre><code>MATCH (s:Account)-[t:TRANSFER]-&gt;(r:Account)
WHERE t.amount &lt; 1000000
WITH s, r, count(t) AS n_tx, sum(t.amount) AS total
WHERE n_tx &gt;= 10
RETURN s.accountId AS sender,
       r.accountId AS receiver,
       n_tx        AS tx_count,
       round(total) AS total_amount
ORDER BY n_tx DESC;
</code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/6a6507dd-eff1-42c0-b508-1a990b24fecd/image.png" alt=""></p>
<h3 id="53-org-2-디바이스-공모--양방향-패턴">5.3 [ORG-2] 디바이스 공모 — 양방향 패턴</h3>
<p>‘한 디바이스를 여러 계좌가 공유한다’는 SQL로 표현하면 GROUP BY device + HAVING count(*) &gt;= N입니다. Cypher에서는 패턴이 거의 그림 그대로 — (a)-[:OWNS]-&gt;(d)&lt;-[:OWNS]-(b) 가 ‘서로 다른 두 계좌가 같은 디바이스를 가리킨다’는 뜻입니다.</p>
<pre><code>MATCH (a:Account)-[:OWNS]-&gt;(d:Device)
WITH d, collect(DISTINCT a.accountId) AS accounts
WHERE size(accounts) &gt;= 8
RETURN d.deviceId AS device,
       size(accounts) AS n_accounts,
       accounts
ORDER BY n_accounts DESC;
</code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/f303205c-65ac-4ad2-af17-f7da7f4932cf/image.png" alt=""></p>
<blockquote>
<p>💡  노이즈는 학습 자료다
결과에 의도 외 계좌가 섞여 있는 것은 버그가 아닙니다. 현실 데이터에는 ‘우연히’ 같은 디바이스를 쓴 가족, 한 디바이스를 두 명이 공유하는 부부 등이 늘 섞여 있습니다. 이런 노이즈와 진짜 사기를 구분하는 능력 자체가 분석가의 몫입니다.</p>
</blockquote>
<h3 id="54-org-4-해외-ip-공모--다중-조건-결합">5.4 [ORG-4] 해외 IP 공모 — 다중 조건 결합</h3>
<p>디바이스 공모와 같은 패턴이지만 IP가 대상이고, 추가로 ‘국가가 한국이 아님’이라는 조건이 붙습니다.</p>
<pre><code>MATCH (a:Account)-[:USED]-&gt;(ip:IPAddress)
WITH ip, collect(DISTINCT a.accountId) AS accounts
WHERE size(accounts) &gt;= 10
  AND ip.country &lt;&gt; &#39;KR&#39;
RETURN ip.ipId, ip.address, ip.country, ip.isProxy,
       size(accounts) AS n_accounts, accounts
ORDER BY n_accounts DESC;
</code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/d6c785d7-4739-428c-8dbf-f9e60bbb696f/image.png" alt=""></p>
<h3 id="55-org-1-자금세탁-순환--그래프-db의-강점">5.5 [ORG-1] 자금세탁 순환 — 그래프 DB의 강점</h3>
<p>여기까지의 패턴은 RDB도 어떻게든 풀 수 있습니다. 하지만 ‘N개 계좌를 거쳐 자금이 출발지로 돌아오는 순환’은 RDB로는 사실상 불가능합니다. 자기 조인 4번을 짜야 하고, 각 단계마다 시간 순서까지 비교해야 하기 때문입니다.
Cypher에서는 단 한 줄. 시작 노드와 끝 노드를 같은 변수 a로 묶기만 하면 됩니다.</p>
<pre><code>MATCH path = (a:Account)-[t1:TRANSFER]-&gt;(b:Account)
                        -[t2:TRANSFER]-&gt;(c:Account)
                        -[t3:TRANSFER]-&gt;(d:Account)
                        -[t4:TRANSFER]-&gt;(a)
WHERE a &lt;&gt; b AND b &lt;&gt; c AND c &lt;&gt; d AND d &lt;&gt; a
  AND t1.timestamp &lt; t2.timestamp
  AND t2.timestamp &lt; t3.timestamp
  AND t3.timestamp &lt; t4.timestamp
  AND t1.amount &gt; 10000000
RETURN a.accountId, b.accountId, c.accountId, d.accountId,
       round(t1.amount) AS amt1,
       round(t4.amount) AS amt4,
       duration.between(t1.timestamp, t4.timestamp).hours AS hours;</code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/3d2c33bb-470f-4e49-8a70-487d76fbf624/image.png" alt=""></p>
<p>의심도 점수 매기기
탐지를 넘어 우선순위를 매기는 것이 분석가의 다음 일입니다. 다음 두 신호가 강할수록 의심도가 높습니다.
(1) 거래 간격이 짧을수록 — 자동화된 자금세탁일 가능성, (2) 금액이 비슷할수록 — 수수료 정도만 빠지는 패턴.</p>
<h2 id="6장-gds-알고리즘-응용">6장. GDS 알고리즘 응용</h2>
<h3 id="gds-설치">gds 설치</h3>
<pre><code>cd /var/lib/neo4j/plugins/
sudo wget https://github.com/neo4j/graph-data-science/releases/download/2.13.2/neo4j-graph-data-science-2.13.2.jar -O neo4j-graph-data-science.jar

sudo systemctl restart neo4j</code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/a3bf66f7-7cf8-4908-8f52-91fbb7bb81cf/image.png" alt=""></p>
<h3 id="61-그래프-프로젝션--gds의-첫-단계">6.1 그래프 프로젝션 — GDS의 첫 단계</h3>
<p>GDS는 디스크 그래프가 아닌 ‘메모리 투영본’에서 알고리즘을 돌립니다. 분석할 부분만 골라 메모리에 올리는 작업이 프로젝션입니다.</p>
<pre><code>CALL gds.graph.project(
  &#39;fraud-graph&#39;,
  &#39;Account&#39;,
  {
    TRANSFER: {
      orientation: &#39;NATURAL&#39;,
      properties: &#39;amount&#39;
    }
  }
);
</code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/4d415a3d-f5d5-49a4-abd8-daaaf5af7c81/image.png" alt=""></p>
<h3 id="62-pagerank--허브-식별의-정교한-버전">6.2 PageRank — 허브 식별의 정교한 버전</h3>
<p>PageRank는 본래 1998년 구글 검색이 등장하면서 ‘웹페이지 중요도’를 매기기 위해 만들어졌습니다. 핵심 직관은 ‘중요한 페이지로부터 링크받은 페이지는 중요하다’ 입니다. 이 직관을 거래 그래프에 적용하면, 단순 ‘수신 건수’를 넘어 ‘중요한 계좌로부터 송금받은 계좌’가 더 높은 점수를 받습니다.</p>
<pre><code>CALL gds.pageRank.stream(&#39;fraud-graph&#39;, {
  maxIterations: 20,
  dampingFactor: 0.85,
  relationshipWeightProperty: &#39;amount&#39;
})
YIELD nodeId, score
WITH gds.util.asNode(nodeId) AS account, score
RETURN account.accountId, round(score * 100) / 100 AS pagerank
ORDER BY pagerank DESC LIMIT 10;
</code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/d93a857b-30ea-4b28-9c1d-56aead31e7bc/image.png" alt=""></p>
<h3 id="63-weakly-connected-components--공모-집단">6.3 Weakly Connected Components — 공모 집단</h3>
<p>WCC는 ‘방향 무시하고 한 덩어리로 연결된 노드들’을 찾는 알고리즘입니다. 디바이스/IP 공유 그래프에서 연결된 컴포넌트는 곧 ‘같은 자원을 통해 연결된 계좌 집단’ — 즉 잠재적 공모 그룹입니다.</p>
<pre><code>CALL gds.graph.project.cypher(
  &#39;shared-resource-graph&#39;,
  &#39;MATCH (a:Account) RETURN id(a) AS id&#39;,
  &#39;
   MATCH (a1:Account)-[:OWNS]-&gt;(:Device)&lt;-[:OWNS]-(a2:Account)
   WHERE id(a1) &lt; id(a2)
   RETURN id(a1) AS source, id(a2) AS target
   UNION
   MATCH (a1:Account)-[:USED]-&gt;(:IPAddress)&lt;-[:USED]-(a2:Account)
   WHERE id(a1) &lt; id(a2)
   RETURN id(a1) AS source, id(a2) AS target
  &#39;
);

CALL gds.wcc.stream(&#39;shared-resource-graph&#39;)
YIELD nodeId, componentId
WITH componentId, collect(gds.util.asNode(nodeId).accountId) AS members
WHERE size(members) &gt;= 6
RETURN componentId, size(members) AS size, members
ORDER BY size DESC;
</code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/ced81c7e-d953-4dc5-b100-d6cd90e26385/image.png" alt=""></p>
<h3 id="64-louvain--거래-흐름-기반-커뮤니티">6.4 Louvain — 거래 흐름 기반 커뮤니티</h3>
<p>Louvain은 모듈러리티 최적화 기반 커뮤니티 탐지 알고리즘입니다. ‘안에서는 빽빽하게 연결되어 있고, 밖으로는 듬성듬성 연결된’ 노드 집단을 찾습니다. 거래 흐름에 적용하면 ‘끼리끼리 거래하는 그룹’이 드러납니다.</p>
<pre><code>CALL gds.louvain.stream(&#39;fraud-graph&#39;, {
  relationshipWeightProperty: &#39;amount&#39;
})
YIELD nodeId, communityId
WITH communityId, collect(gds.util.asNode(nodeId).accountId) AS members
WHERE size(members) &gt;= 3 AND size(members) &lt;=30
RETURN communityId, size(members) AS size, members[0..10] AS sample
ORDER BY size DESC LIMIT 10;
</code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/5d265df3-accd-4754-8de0-0311793c19fc/image.png" alt=""></p>
<blockquote>
<p>📌  알고리즘의 한계 이해하기
Louvain은 거래 흐름을 보지 디바이스/IP는 보지 않습니다. 즉 ORG-2/ORG-4(자원 공모)는 Louvain만으로는 잡히지 않습니다. </p>
</blockquote>
<h3 id="65-메모리-정리">6.5 메모리 정리</h3>
<pre><code>CALL gds.graph.drop(&#39;fraud-graph&#39;) YIELD graphName RETURN graphName;
CALL gds.graph.drop(&#39;shared-resource-graph&#39;) YIELD graphName RETURN graphName;
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[MicrosoftDataSchool] 82일차 - Azure SQL Index]]></title>
            <link>https://velog.io/@rudin_/MicrosoftDataSchool-82%EC%9D%BC%EC%B0%A8-Azure-SQL-Index</link>
            <guid>https://velog.io/@rudin_/MicrosoftDataSchool-82%EC%9D%BC%EC%B0%A8-Azure-SQL-Index</guid>
            <pubDate>Thu, 07 May 2026 00:39:19 GMT</pubDate>
            <description><![CDATA[<h1 id="sql-server-성능-튜닝-실습-정리---인덱스-실행-계획-blocking-query-store">SQL Server 성능 튜닝 실습 정리 - 인덱스, 실행 계획, Blocking, Query Store</h1>
<p>SQL Server 성능 튜닝을 제대로 이해하려면 가장 먼저 알아야 하는 것이 바로 <strong>인덱스(Index)</strong> 이다.</p>
<p>이번 실습에서도:</p>
<ul>
<li>Fragmentation</li>
<li>Key Lookup</li>
<li>Execution Plan</li>
<li>Query Store</li>
<li>Blocking</li>
</ul>
<p>같은 다양한 성능 이슈를 다뤘지만, 결국 대부분의 문제는 인덱스와 연결되어 있었다.</p>
<p>따라서 본격적인 실습 내용을 보기 전에, 먼저 SQL Server 인덱스 개념을 정리하고 시작한다.</p>
<hr>
<h1 id="인덱스index란">인덱스(Index)란?</h1>
<p>인덱스는 데이터를 빠르게 찾기 위한 자료구조이다.</p>
<p>책에서 원하는 내용을 찾을 때:</p>
<ul>
<li>책 전체를 처음부터 끝까지 읽지 않고</li>
<li>맨 뒤의 색인(Index)을 먼저 보는 것과 비슷하다.</li>
</ul>
<p>SQL Server에서도 인덱스가 없다면 원하는 데이터를 찾기 위해 테이블 전체를 읽어야 한다.</p>
<p>이를 <strong>Table Scan</strong>이라고 한다.</p>
<p>데이터 양이 적을 때는 큰 문제가 없지만, 데이터가 수백만 건 이상으로 증가하면 성능 차이가 매우 커진다.</p>
<hr>
<h1 id="인덱스가-필요한-이유">인덱스가 필요한 이유</h1>
<h2 id="1-검색-속도-향상">1. 검색 속도 향상</h2>
<p>인덱스는 원하는 데이터 위치를 빠르게 찾을 수 있게 해준다.</p>
<p>예를 들어:</p>
<pre><code class="language-sql">SELECT *
FROM Users
WHERE Email = &#39;test@test.com&#39;</code></pre>
<p>같은 쿼리에서 Email 컬럼에 인덱스가 존재하면 SQL Server는 전체 데이터를 읽지 않고 필요한 데이터만 바로 찾을 수 있다.</p>
<hr>
<h2 id="2-디스크-io-감소">2. 디스크 I/O 감소</h2>
<p>인덱스가 없으면 SQL Server는 테이블 전체를 읽는다.</p>
<p>즉:</p>
<ul>
<li>더 많은 페이지 읽기</li>
<li>더 많은 디스크 접근</li>
<li>더 많은 메모리 사용</li>
</ul>
<p>이 발생한다.</p>
<p>인덱스를 사용하면 필요한 데이터만 읽기 때문에 I/O가 감소한다.</p>
<hr>
<h2 id="3-대규모-시스템-필수-요소">3. 대규모 시스템 필수 요소</h2>
<p>실제 운영 환경에서는:</p>
<ul>
<li>수많은 사용자</li>
<li>동시 요청</li>
<li>대량 데이터</li>
</ul>
<p>를 처리해야 한다.</p>
<p>인덱스 없이 운영하면:</p>
<ul>
<li>응답 속도 저하</li>
<li>CPU 사용량 증가</li>
<li>서버 부하 증가</li>
</ul>
<p>가 발생한다.</p>
<p>따라서 인덱스는 단순 최적화가 아니라 필수 요소에 가깝다.</p>
<hr>
<h1 id="sql-server의-인덱스-구조">SQL Server의 인덱스 구조</h1>
<p>SQL Server는 대부분 B-Tree(B+Tree) 구조를 사용한다.</p>
<p>구조는 다음과 같다.</p>
<table>
<thead>
<tr>
<th>구조</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>Root Node</td>
<td>검색 시작점</td>
</tr>
<tr>
<td>Intermediate Node</td>
<td>중간 탐색</td>
</tr>
<tr>
<td>Leaf Node</td>
<td>실제 데이터 또는 데이터 위치 저장</td>
</tr>
</tbody></table>
<hr>
<h1 id="b-tree-구조-동작-방식">B-Tree 구조 동작 방식</h1>
<p>예를 들어 다음과 같은 데이터가 있다고 가정한다.</p>
<pre><code class="language-text">1, 5, 10, 20, 50, 100</code></pre>
<p>SQL Server는 이 값을 트리 형태로 정렬하여 저장한다.</p>
<p>검색 시에는:</p>
<ol>
<li>Root Node 탐색</li>
<li>중간 노드 이동</li>
<li>Leaf Node 도달</li>
</ol>
<p>과정을 거친다.</p>
<p>즉:</p>
<blockquote>
<p>전체 데이터를 순차 탐색하지 않아도 된다.</p>
</blockquote>
<p>이것이 인덱스가 빠른 이유이다.</p>
<hr>
<h1 id="clustered-index">Clustered Index</h1>
<p>Clustered Index는 실제 데이터 자체가 정렬된다.</p>
<p>즉:</p>
<ul>
<li>데이터 저장 순서</li>
<li>인덱스 순서</li>
</ul>
<p>가 동일하다.</p>
<hr>
<h1 id="특징">특징</h1>
<table>
<thead>
<tr>
<th>특징</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>테이블당 1개만 가능</td>
<td>실제 데이터 순서는 하나만 존재 가능</td>
</tr>
<tr>
<td>범위 검색에 강함</td>
<td>BETWEEN, ORDER BY 최적화</td>
</tr>
<tr>
<td>Primary Key에 자주 사용</td>
<td>기본 키 생성 시 자동 생성되는 경우 많음</td>
</tr>
</tbody></table>
<hr>
<h1 id="clustered-index-예시">Clustered Index 예시</h1>
<pre><code class="language-sql">CREATE CLUSTERED INDEX IX_Users_Id
ON Users(UserId);</code></pre>
<p>이 경우 Users 테이블 데이터 자체가 UserId 기준으로 정렬된다.</p>
<hr>
<h1 id="nonclustered-index">Nonclustered Index</h1>
<p>Nonclustered Index는 실제 데이터와 별도 구조로 존재한다.</p>
<p>인덱스에는:</p>
<ul>
<li>키 값</li>
<li>데이터 위치 포인터</li>
</ul>
<p>만 저장된다.</p>
<hr>
<h1 id="구조-특징">구조 특징</h1>
<table>
<thead>
<tr>
<th>구조</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>별도 인덱스 구조 존재</td>
<td>데이터와 분리</td>
</tr>
<tr>
<td>Leaf Node</td>
<td>실제 데이터 위치 저장</td>
</tr>
<tr>
<td>여러 개 생성 가능</td>
<td>최대 999개</td>
</tr>
</tbody></table>
<hr>
<h1 id="nonclustered-index-예시">Nonclustered Index 예시</h1>
<pre><code class="language-sql">CREATE NONCLUSTERED INDEX IX_Users_Email
ON Users(Email);</code></pre>
<p>이 경우 Email 기반 검색이 빨라진다.</p>
<hr>
<h1 id="index-seek-vs-table-scan">Index Seek vs Table Scan</h1>
<p>실행 계획에서 가장 중요하게 보는 것 중 하나가:</p>
<ul>
<li>Index Seek</li>
<li>Table Scan</li>
</ul>
<p>이다.</p>
<hr>
<h1 id="table-scan">Table Scan</h1>
<p>Table Scan은 테이블 전체를 읽는다.</p>
<p>즉:</p>
<pre><code class="language-text">1행부터 끝까지 전부 읽음</code></pre>
<p>데이터가 많을수록 매우 느려진다.</p>
<hr>
<h1 id="index-seek">Index Seek</h1>
<p>Index Seek는 필요한 데이터만 찾는다.</p>
<p>즉:</p>
<pre><code class="language-text">원하는 위치만 바로 접근</code></pre>
<p>따라서 훨씬 빠르다.</p>
<hr>
<h1 id="실행-계획에서-확인-가능">실행 계획에서 확인 가능</h1>
<p>실행 계획(Execution Plan)에서:</p>
<ul>
<li>Table Scan 발생 여부</li>
<li>Index Seek 사용 여부</li>
</ul>
<p>를 확인할 수 있다.</p>
<p>튜닝에서 가장 기본적으로 보는 부분이다.</p>
<hr>
<h1 id="covering-index">Covering Index</h1>
<p>Covering Index는 쿼리에 필요한 모든 컬럼이 인덱스에 포함된 경우를 말한다.</p>
<p>즉:</p>
<ul>
<li>추가 테이블 접근 없이</li>
<li>인덱스만 읽어서</li>
<li>결과 반환 가능</li>
</ul>
<p>하다.</p>
<hr>
<h1 id="include-사용">INCLUDE 사용</h1>
<pre><code class="language-sql">CREATE INDEX IX_Orders_CustomerId
ON Orders(CustomerId)
INCLUDE (OrderDate, TotalAmount);</code></pre>
<hr>
<h1 id="장점">장점</h1>
<table>
<thead>
<tr>
<th>장점</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Key Lookup 제거</td>
<td>추가 테이블 접근 감소</td>
</tr>
<tr>
<td>Logical Read 감소</td>
<td>성능 향상</td>
</tr>
<tr>
<td>실행 계획 단순화</td>
<td>I/O 절약</td>
</tr>
</tbody></table>
<hr>
<h1 id="key-lookup">Key Lookup</h1>
<p>Key Lookup은 SQL Server가:</p>
<ol>
<li>인덱스로 위치 찾고</li>
<li>원본 데이터 다시 접근</li>
</ol>
<p>하는 작업이다.</p>
<p>즉:</p>
<pre><code class="language-text">인덱스만으로 필요한 컬럼이 부족함</code></pre>
<p>을 의미한다.</p>
<hr>
<h1 id="왜-문제인가">왜 문제인가?</h1>
<p>Key Lookup은:</p>
<ul>
<li>랜덤 I/O 증가</li>
<li>페이지 접근 증가</li>
<li>성능 저하</li>
</ul>
<p>를 유발한다.</p>
<p>특히 결과 건수가 많을수록 심각해진다.</p>
<hr>
<h1 id="fragmentation조각화">Fragmentation(조각화)</h1>
<p>데이터가 지속적으로 변경되면 인덱스 페이지 순서가 깨진다.</p>
<p>이를 Fragmentation이라고 한다.</p>
<hr>
<h1 id="조각화-발생-원인">조각화 발생 원인</h1>
<table>
<thead>
<tr>
<th>작업</th>
<th>영향</th>
</tr>
</thead>
<tbody><tr>
<td>INSERT</td>
<td>페이지 분할 발생</td>
</tr>
<tr>
<td>UPDATE</td>
<td>데이터 이동 발생</td>
</tr>
<tr>
<td>DELETE</td>
<td>빈 공간 증가</td>
</tr>
</tbody></table>
<hr>
<h1 id="조각화-문제점">조각화 문제점</h1>
<table>
<thead>
<tr>
<th>문제</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>디스크 I/O 증가</td>
<td>페이지 순서 깨짐</td>
</tr>
<tr>
<td>Logical Read 증가</td>
<td>더 많은 페이지 읽기</td>
</tr>
<tr>
<td>성능 저하</td>
<td>응답 속도 감소</td>
</tr>
</tbody></table>
<hr>
<h1 id="fragmentation-확인">Fragmentation 확인</h1>
<p>SQL Server에서는 다음 DMV로 확인 가능하다.</p>
<pre><code class="language-sql">sys.dm_db_index_physical_stats</code></pre>
<hr>
<h1 id="reorganize-vs-rebuild">REORGANIZE vs REBUILD</h1>
<p>조각화 해결 방법은 크게 2가지이다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>REORGANIZE</th>
<th>REBUILD</th>
</tr>
</thead>
<tbody><tr>
<td>방식</td>
<td>페이지 재정렬</td>
<td>인덱스 새 생성</td>
</tr>
<tr>
<td>부하</td>
<td>낮음</td>
<td>높음</td>
</tr>
<tr>
<td>통계 갱신</td>
<td>X</td>
<td>O</td>
</tr>
<tr>
<td>추천 구간</td>
<td>10~30%</td>
<td>30% 이상</td>
</tr>
</tbody></table>
<hr>
<h1 id="실행-계획execution-plan">실행 계획(Execution Plan)</h1>
<p>SQL Server는 쿼리를 실행하기 전에:</p>
<blockquote>
<p>“어떻게 실행하는 것이 가장 효율적인가”</p>
</blockquote>
<p>를 계산한다.</p>
<p>이 결과가 실행 계획이다.</p>
<hr>
<h1 id="실행-계획에서-보는-주요-요소">실행 계획에서 보는 주요 요소</h1>
<table>
<thead>
<tr>
<th>요소</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td>Index Seek</td>
<td>효율적 검색</td>
</tr>
<tr>
<td>Index Scan</td>
<td>인덱스 전체 탐색</td>
</tr>
<tr>
<td>Table Scan</td>
<td>테이블 전체 탐색</td>
</tr>
<tr>
<td>Key Lookup</td>
<td>원본 데이터 재접근</td>
</tr>
<tr>
<td>Hash Match</td>
<td>해시 기반 조인</td>
</tr>
<tr>
<td>Sort</td>
<td>정렬 작업</td>
</tr>
</tbody></table>
<hr>
<h1 id="query-optimizer">Query Optimizer</h1>
<p>실행 계획은 Query Optimizer가 생성한다.</p>
<p>Optimizer는:</p>
<ul>
<li>인덱스</li>
<li>통계 정보</li>
<li>데이터 분포</li>
<li>비용 계산</li>
</ul>
<p>을 기반으로 가장 효율적이라고 판단한 플랜을 선택한다.</p>
<p>하지만 항상 완벽하지는 않다.</p>
<p>따라서:</p>
<ul>
<li>잘못된 실행 계획</li>
<li>비효율 플랜</li>
<li>Parameter Sniffing</li>
</ul>
<p>같은 문제도 발생한다.</p>
<hr>
<h1 id="query-store">Query Store</h1>
<p>SQL Server의 Query Store는:</p>
<ul>
<li>실행 계획 저장</li>
<li>느린 쿼리 분석</li>
<li>성능 Regression 추적</li>
</ul>
<p>기능을 제공한다.</p>
<p>특히:</p>
<ul>
<li>이전보다 느려진 쿼리</li>
<li>플랜 변경</li>
<li>강제 플랜 적용(Force Plan)</li>
</ul>
<p>분석에 매우 유용하다.</p>
<hr>
<h1 id="blocking">Blocking</h1>
<p>Blocking은 트랜잭션이 서로 Lock을 기다리는 현상이다.</p>
<p>예를 들어:</p>
<ol>
<li>세션 A가 UPDATE 수행</li>
<li>COMMIT 안 함</li>
<li>세션 B가 같은 데이터 읽기 시도</li>
</ol>
<p>하면 세션 B는 대기 상태가 된다.</p>
<hr>
<h1 id="snapshot-isolation">Snapshot Isolation</h1>
<p>Blocking 완화를 위해 SQL Server는 Snapshot 기반 격리 수준을 제공한다.</p>
<p>대표적으로:</p>
<pre><code class="language-sql">READ_COMMITTED_SNAPSHOT</code></pre>
<p>이 있다.</p>
<hr>
<h1 id="핵심-개념">핵심 개념</h1>
<p>기존 Read Committed:</p>
<ul>
<li>Lock 기다림</li>
</ul>
<p>Snapshot 기반:</p>
<ul>
<li>이전 버전(Row Version) 읽음</li>
</ul>
<p>즉:</p>
<blockquote>
<p>Reader와 Writer 충돌 감소</p>
</blockquote>
<p>효과가 있다.</p>
<hr>
<h1 id="이번-실습에서-중요했던-핵심-포인트">이번 실습에서 중요했던 핵심 포인트</h1>
<p>이번 실습의 핵심은 단순 SQL 문법이 아니었다.</p>
<p>진짜 중요한 것은:</p>
<ul>
<li>왜 실행 계획이 그렇게 나오는가</li>
<li>왜 인덱스가 필요한가</li>
<li>왜 Key Lookup이 성능 병목이 되는가</li>
<li>왜 Fragmentation이 Logical Read를 증가시키는가</li>
<li>왜 Blocking이 발생하는가</li>
</ul>
<h2 id="를-sql-server-내부-동작-관점에서-이해하는-것이었다">를 SQL Server 내부 동작 관점에서 이해하는 것이었다.</h2>
<h1 id="lab07---fragmentation-실습">Lab07 - Fragmentation 실습</h1>
<p>이번 실습에서는 조각화를 의도적으로 발생시켰다.</p>
<h2 id="1-조각화-상태-확인">1. 조각화 상태 확인</h2>
<pre><code class="language-sql">USE AdventureWorks2017
GO

SELECT i.name Index_Name
, avg_fragmentation_in_percent
, db_name(database_id)
, i.object_id
, i.index_id
, index_type_desc
FROM sys.dm_db_index_physical_stats(
    db_id(&#39;AdventureWorks2017&#39;),
    object_id(&#39;person.address&#39;),
    NULL,
    NULL,
    &#39;DETAILED&#39;
) ps
INNER JOIN sys.indexes i
ON ps.object_id = i.object_id
AND ps.index_id = i.index_id
WHERE avg_fragmentation_in_percent &gt; 50</code></pre>
<p>처음에는 조각화가 거의 없기 때문에 결과가 나오지 않는다.</p>
<hr>
<h2 id="2-데이터-대량-insert">2. 데이터 대량 INSERT</h2>
<pre><code class="language-sql">USE AdventureWorks2017
GO

INSERT INTO [Person].[Address]
(
    [AddressLine1],
    [AddressLine2],
    [City],
    [StateProvinceID],
    [PostalCode],
    [SpatialLocation],
    [rowguid],
    [ModifiedDate]
)
SELECT
    AddressLine1,
    AddressLine2,
    &#39;Amsterdam&#39;,
    StateProvinceID,
    PostalCode,
    SpatialLocation,
    newid(),
    getdate()
FROM Person.Address;
GO</code></pre>
<p>이 작업으로 Person.Address 테이블 크기가 증가하며 페이지 분할(Page Split)이 발생한다.</p>
<p>결과적으로 인덱스 조각화율이 크게 증가한다.</p>
<hr>
<h2 id="3-논리적-읽기logical-read-측정">3. 논리적 읽기(Logical Read) 측정</h2>
<pre><code class="language-sql">SET STATISTICS IO,TIME ON
GO

USE AdventureWorks2017
GO

SELECT DISTINCT (StateProvinceID)
,count(StateProvinceID) AS CustomerCount
FROM person.Address
GROUP BY StateProvinceID
ORDER BY count(StateProvinceID) DESC;
GO</code></pre>
<p>메시지 탭에서 Logical Read 값을 확인할 수 있다.</p>
<p>조각화가 심해질수록 더 많은 페이지를 읽게 된다.</p>
<hr>
<h1 id="인덱스-rebuild">인덱스 REBUILD</h1>
<p>조각화 문제 해결을 위해 REBUILD를 수행한다.</p>
<pre><code class="language-sql">USE AdventureWorks2017
GO

ALTER INDEX [IX_Address_StateProvinceID]
ON [Person].[Address]
REBUILD PARTITION = ALL</code></pre>
<p>REBUILD는 인덱스를 새로 만드는 작업이다.</p>
<p>결과:</p>
<ul>
<li>Fragmentation 감소</li>
<li>Logical Read 감소</li>
<li>쿼리 성능 향상</li>
</ul>
<hr>
<h1 id="reorganize-vs-rebuild-1">REORGANIZE vs REBUILD</h1>
<table>
<thead>
<tr>
<th>구분</th>
<th>REORGANIZE</th>
<th>REBUILD</th>
</tr>
</thead>
<tbody><tr>
<td>방식</td>
<td>페이지 재정렬</td>
<td>인덱스 새 생성</td>
</tr>
<tr>
<td>부하</td>
<td>낮음</td>
<td>높음</td>
</tr>
<tr>
<td>통계 갱신</td>
<td>X</td>
<td>O</td>
</tr>
<tr>
<td>권장 구간</td>
<td>10~30%</td>
<td>30% 이상</td>
</tr>
</tbody></table>
<hr>
<h1 id="실행-계획execution-plan-1">실행 계획(Execution Plan)</h1>
<p>SQL Server는 쿼리를 실행하기 전에 “어떻게 실행할지” 계획을 만든다.</p>
<p>이것이 Execution Plan이다.</p>
<p>실행 계획을 통해:</p>
<ul>
<li>Index Seek</li>
<li>Table Scan</li>
<li>Key Lookup</li>
<li>Hash Match</li>
</ul>
<p>등이 어떻게 발생하는지 확인할 수 있다.</p>
<hr>
<h1 id="lab09---암시적-형-변환implicit-conversion">Lab09 - 암시적 형 변환(Implicit Conversion)</h1>
<p>다음 쿼리를 실행했다.</p>
<pre><code class="language-sql">SELECT BusinessEntityID,
       NationalIDNumber,
       LoginID,
       HireDate,
       JobTitle
FROM HumanResources.Employee
WHERE NationalIDNumber = 14417807;</code></pre>
<p>실행 계획에서 경고가 발생했다.</p>
<p>원인은 Implicit Conversion이다.</p>
<hr>
<h1 id="왜-발생했는가">왜 발생했는가?</h1>
<p>NationalIDNumber 컬럼 타입은:</p>
<pre><code class="language-sql">nvarchar(15)</code></pre>
<p>이다.</p>
<p>하지만 비교값은 숫자(INT)이다.</p>
<p>따라서 SQL Server가 내부적으로:</p>
<pre><code class="language-sql">CONVERT_IMPLICIT(...)</code></pre>
<p>를 수행한다.</p>
<hr>
<h1 id="문제점">문제점</h1>
<p>Implicit Conversion은 다음 문제를 만든다.</p>
<table>
<thead>
<tr>
<th>문제</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>CPU 증가</td>
<td>형 변환 연산 발생</td>
</tr>
<tr>
<td>인덱스 비효율</td>
<td>Seek 최적화 방해</td>
</tr>
<tr>
<td>실행 계획 악화</td>
<td>Optimizer가 비효율 플랜 선택</td>
</tr>
</tbody></table>
<hr>
<h1 id="해결-방법-1---쿼리-수정">해결 방법 1 - 쿼리 수정</h1>
<pre><code class="language-sql">WHERE NationalIDNumber = &#39;14417807&#39;;</code></pre>
<p>문자열로 맞춰주면 된다.</p>
<p>이렇게 하면:</p>
<ul>
<li>경고 제거</li>
<li>실행 계획 개선</li>
<li>인덱스 활용 가능</li>
</ul>
<p>해진다.</p>
<hr>
<h1 id="해결-방법-2---컬럼-타입-변경">해결 방법 2 - 컬럼 타입 변경</h1>
<pre><code class="language-sql">ALTER TABLE [HumanResources].[Employee]
ALTER COLUMN [NationalIDNumber] INT NOT NULL;</code></pre>
<p>하지만 여기서 중요한 문제가 발생한다.</p>
<hr>
<h1 id="인덱스-문제">인덱스 문제</h1>
<p>NationalIDNumber는 기존 인덱스에서 사용 중이었다.</p>
<p>따라서:</p>
<ol>
<li>인덱스 DROP</li>
<li>ALTER COLUMN</li>
<li>인덱스 재생성</li>
</ol>
<p>순서가 필요하다.</p>
<p>즉 운영 환경에서는 다운타임 문제가 발생할 수 있다.</p>
<hr>
<h1 id="key-lookup-문제">Key Lookup 문제</h1>
<p>Lab10에서는 Key Lookup 문제를 분석했다.</p>
<p>실행 계획에서:</p>
<ul>
<li>Index Seek</li>
<li>Key Lookup(cost 99%)</li>
</ul>
<p>이 발생했다.</p>
<hr>
<h1 id="왜-key-lookup이-발생하는가">왜 Key Lookup이 발생하는가?</h1>
<p>현재 인덱스는:</p>
<pre><code class="language-sql">(ProductID)</code></pre>
<p>만 포함한다.</p>
<p>하지만 SELECT에서 필요한 컬럼은 더 많다.</p>
<p>따라서 SQL Server는:</p>
<ol>
<li>인덱스로 위치 찾기</li>
<li>원본 데이터 다시 접근</li>
</ol>
<p>을 수행한다.</p>
<p>이 추가 접근이 Key Lookup이다.</p>
<hr>
<h1 id="covering-index-1">Covering Index</h1>
<p>해결 방법은 Covering Index이다.</p>
<pre><code class="language-sql">CREATE NONCLUSTERED INDEX
[IX_SalesOrderDetail_ProductID]

ON [Sales].[SalesOrderDetail]
([ProductID],[ModifiedDate])

INCLUDE (
    [CarrierTrackingNumber],
    [OrderQty],
    [UnitPrice]
)

WITH (DROP_EXISTING = on);</code></pre>
<p>핵심:</p>
<ul>
<li>필요한 컬럼을 INCLUDE로 포함</li>
<li>인덱스만 읽어서 쿼리 해결 가능</li>
</ul>
<p>결과:</p>
<ul>
<li>Key Lookup 제거</li>
<li>Logical Read 감소</li>
<li>성능 향상</li>
</ul>
<hr>
<h1 id="query-store-1">Query Store</h1>
<p>Query Store는 SQL Server의 성능 분석 기능이다.</p>
<p>활성화:</p>
<pre><code class="language-sql">ALTER DATABASE [AdventureWorks2017]
SET QUERY_STORE = ON;</code></pre>
<hr>
<h1 id="query-store로-할-수-있는-것">Query Store로 할 수 있는 것</h1>
<ul>
<li>느린 쿼리 찾기</li>
<li>실행 계획 비교</li>
<li>성능 Regression 추적</li>
<li>좋은 실행 계획 강제 적용</li>
</ul>
<hr>
<h1 id="force-plan">Force Plan</h1>
<p>실행 계획 중 더 좋은 플랜을 강제로 사용 가능하다.</p>
<p>실습에서는:</p>
<ul>
<li>빠른 Plan</li>
<li>느린 Plan</li>
</ul>
<p>두 개를 비교 후:</p>
<pre><code class="language-text">Force Plan</code></pre>
<p>기능으로 좋은 플랜을 강제 적용했다.</p>
<hr>
<h1 id="blocking-1">Blocking</h1>
<p>Blocking은 트랜잭션이 서로 잠금을 기다리는 현상이다.</p>
<hr>
<h1 id="blocking-실습">Blocking 실습</h1>
<h2 id="세션-1">세션 1</h2>
<pre><code class="language-sql">BEGIN TRANSACTION

UPDATE Person.Person
SET LastName = LastName;</code></pre>
<p>트랜잭션 종료 안 함.</p>
<hr>
<h2 id="세션-2">세션 2</h2>
<pre><code class="language-sql">SELECT TOP (1000)
    [LastName],
    [FirstName],
    [Title]
FROM Person.Person
WHERE FirstName = &#39;David&#39;</code></pre>
<p>결과:</p>
<ul>
<li>무한 대기</li>
<li>Blocking 발생</li>
</ul>
<hr>
<h1 id="왜-발생했는가-1">왜 발생했는가?</h1>
<p>첫 번째 세션이 Lock을 유지 중이기 때문이다.</p>
<p>두 번째 세션은 같은 데이터 접근 시도 중이라 대기 상태가 된다.</p>
<hr>
<h1 id="extended-events">Extended Events</h1>
<p>Blocking 추적을 위해 Extended Events를 사용했다.</p>
<pre><code class="language-sql">blocked_process_report</code></pre>
<p>를 통해:</p>
<ul>
<li>누가 막는지</li>
<li>어떤 쿼리가 원인인지</li>
<li>어떤 세션인지</li>
</ul>
<p>확인 가능하다.</p>
<hr>
<h1 id="read_committed_snapshot">READ_COMMITTED_SNAPSHOT</h1>
<p>Blocking 완화를 위해 Snapshot Isolation 기반 설정을 적용했다.</p>
<pre><code class="language-sql">ALTER DATABASE AdventureWorks2017
SET READ_COMMITTED_SNAPSHOT ON
WITH ROLLBACK IMMEDIATE;</code></pre>
<hr>
<h1 id="핵심-개념-1">핵심 개념</h1>
<p>기존 Read Committed:</p>
<ul>
<li>수정 중 데이터 접근 시 대기</li>
</ul>
<p>READ_COMMITTED_SNAPSHOT:</p>
<ul>
<li>이전 버전(Row Version) 읽음</li>
<li>Lock 기다리지 않음</li>
</ul>
<p>즉:</p>
<blockquote>
<p>Reader와 Writer 충돌 감소</p>
</blockquote>
<hr>
<h1 id="최종-정리">최종 정리</h1>
<p>이번 실습에서는 단순 SQL 작성이 아니라 실제 운영 환경 수준의 SQL Server 성능 튜닝 과정을 경험했다.</p>
<p>특히 다음 내용을 실제로 확인할 수 있었다.</p>
<ul>
<li>인덱스 구조와 동작 방식</li>
<li>Fragmentation이 성능에 미치는 영향</li>
<li>실행 계획 분석</li>
<li>Implicit Conversion 문제</li>
<li>Key Lookup 제거 방법</li>
<li>Covering Index 설계</li>
<li>Query Store 기반 분석</li>
<li>Blocking 및 Snapshot Isolation</li>
</ul>
<p>결국 SQL 성능 튜닝의 핵심은:</p>
<blockquote>
<p>“왜 SQL Server가 그런 실행 계획을 선택했는가”</p>
</blockquote>
<p>를 이해하는 것이라는 점을 확인할 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MicrosoftDataSchool] 81일차 - SQL Server VM → Azure SQL Database 마이그레이션, 데이터베이스 조각화]]></title>
            <link>https://velog.io/@rudin_/MicrosoftDataSchool-81%EC%9D%BC%EC%B0%A8-SQL-Server-VM-Azure-SQL-Database-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98</link>
            <guid>https://velog.io/@rudin_/MicrosoftDataSchool-81%EC%9D%BC%EC%B0%A8-SQL-Server-VM-Azure-SQL-Database-%EB%A7%88%EC%9D%B4%EA%B7%B8%EB%A0%88%EC%9D%B4%EC%85%98</guid>
            <pubDate>Mon, 04 May 2026 01:02:51 GMT</pubDate>
            <description><![CDATA[<h1 id="carmarket-중고차-mvp--iaas에서-paas로">CarMarket 중고차 MVP — IaaS에서 PaaS로</h1>
<h2 id="아키텍처-개요">아키텍처 개요</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/91467739-63a8-40c1-951a-5d6f78679851/image.png" alt=""></p>
<pre><code class="language-shell">#!/usr/bin/env bash
# ============================================================
# 01-setup-vm-sqlserver.sh
# Azure VM 생성 → SQL Server 2025 설치 → 외부 SSMS 접근 → 시딩
#
# 로컬 PC (Mac/Linux/WSL) 에서 실행
#
# 사용법:
#   bash 01-setup-vm-sqlserver.sh
#
# 환경변수 사전 설정 가능:
#   export RG=rg-carmarket-lab LOC=koreacentral SA_PASSWORD=&#39;YourP@ssw0rd!&#39;
#   bash 01-setup-vm-sqlserver.sh
# ============================================================

set -euo pipefail

# =============================================================
# 기본값
# =============================================================
RG=&quot;${RG:-rg-carmarket-lab}&quot;
LOC=&quot;${LOC:-koreacentral}&quot;
VM=&quot;${VM:-vm-carmarket-$(date +%m%d)}&quot;
VM_SIZE=&quot;${VM_SIZE:-Standard_B2s}&quot;
USER_NAME=&quot;${USER_NAME:-azureuser}&quot;
SSH_KEY=&quot;${SSH_KEY:-$HOME/.ssh/id_rsa.pub}&quot;
SA_PASSWORD=&quot;${SA_PASSWORD:-}&quot;
REPO_URL=&quot;https://github.com/jhjwlee/sqlvm_usedcar.git&quot;

# 색상
G=&#39;\033[0;32m&#39;; Y=&#39;\033[0;33m&#39;; R=&#39;\033[0;31m&#39;; B=&#39;\033[0;34m&#39;; NC=&#39;\033[0m&#39;
banner() { echo &quot;&quot;; echo -e &quot;${B}═══════════════════════════════════════════════════${NC}&quot;; echo -e &quot;${B}  $1${NC}&quot;; echo -e &quot;${B}═══════════════════════════════════════════════════${NC}&quot;; }
step()   { echo &quot;&quot;; echo -e &quot;${G}▶ [$1/$TOTAL_STEPS]${NC} $2&quot;; }
ok()     { echo -e &quot;${G}  ✓${NC} $1&quot;; }
warn()   { echo -e &quot;${Y}  ⚠${NC} $1&quot;; }
fail()   { echo -e &quot;${R}  ✗${NC} $1&quot;; }
abort()  { echo -e &quot;${R}❌ $1${NC}&quot;; exit 1; }

TOTAL_STEPS=8
LOG_FILE=&quot;/tmp/carmarket-setup-$(date +%Y%m%d-%H%M%S).log&quot;
exec &gt; &gt;(tee -a &quot;$LOG_FILE&quot;) 2&gt;&amp;1

banner &quot;CarMarket Lab — VM + SQL Server + SSMS 접근 자동 설정&quot;
echo &quot;  리소스 그룹: $RG&quot;
echo &quot;  위치:       $LOC&quot;
echo &quot;  VM 이름:    $VM&quot;
echo &quot;  VM 크기:    $VM_SIZE&quot;
echo &quot;  로그 파일:  $LOG_FILE&quot;

# =============================================================
# Step 1: 사전 점검
# =============================================================
step 1 &quot;사전 점검 (Azure CLI · SSH 키 · 로그인)&quot;

command -v az &gt;/dev/null 2&gt;&amp;1 || abort &quot;Azure CLI 미설치. https://aka.ms/azcli&quot;
ok &quot;Azure CLI: $(az version --query &#39;\&quot;azure-cli\&quot;&#39; -o tsv 2&gt;/dev/null || echo &#39;unknown&#39;)&quot;

if ! az account show &gt;/dev/null 2&gt;&amp;1; then
  warn &quot;Azure 로그인 필요&quot;
  az login
fi
ok &quot;구독: $(az account show --query name -o tsv)&quot;

if [ ! -f &quot;$SSH_KEY&quot; ]; then
  warn &quot;SSH 키 없음 → 자동 생성&quot;
  ssh-keygen -t rsa -b 4096 -N &quot;&quot; -f &quot;${SSH_KEY%.pub}&quot; -q
fi
ok &quot;SSH 키: $SSH_KEY&quot;

# =============================================================
# Step 2: SA 비밀번호 입력
# =============================================================
step 2 &quot;SA 비밀번호 설정&quot;

if [ -z &quot;$SA_PASSWORD&quot; ]; then
  echo &quot;  SQL Server SA 비밀번호를 입력하세요.&quot;
  echo &quot;  요구사항: 8자+ / 대·소문자·숫자·특수문자 중 3종 이상&quot;
  echo &quot;  예) CarMarket@2026&quot;
  while true; do
    read -s -p &quot;  SA Password: &quot; SA_PASSWORD; echo &quot;&quot;
    read -s -p &quot;  Confirm:     &quot; SA_CONFIRM;  echo &quot;&quot;
    if [ &quot;$SA_PASSWORD&quot; = &quot;$SA_CONFIRM&quot; ] &amp;&amp; [ ${#SA_PASSWORD} -ge 8 ]; then
      break
    fi
    echo -e &quot;${R}  비밀번호 불일치 또는 8자 미만. 재입력.${NC}&quot;
  done
fi
ok &quot;SA 비밀번호 설정 완료 (${#SA_PASSWORD}자)&quot;

# 비용 안내
echo &quot;&quot;
echo &quot;  예상 비용: VM(B2s) ≈ \$0.5/일&quot;
echo &quot;  예상 시간: 약 10~15분&quot;
read -p &quot;  진행? (y/N): &quot; ok_proceed
[[ &quot;$ok_proceed&quot; =~ ^[Yy]$ ]] || abort &quot;취소됨&quot;

# =============================================================
# Step 3: Resource Group + VM 생성
# =============================================================
step 3 &quot;Azure 리소스 생성 (RG + VM)&quot;

if az group show -n &quot;$RG&quot; &gt;/dev/null 2&gt;&amp;1; then
  ok &quot;RG &#39;$RG&#39; 이미 존재 (재사용)&quot;
else
  az group create -n &quot;$RG&quot; -l &quot;$LOC&quot; --output none
  ok &quot;RG &#39;$RG&#39; 생성&quot;
fi

if az vm show -g &quot;$RG&quot; -n &quot;$VM&quot; &gt;/dev/null 2&gt;&amp;1; then
  warn &quot;VM &#39;$VM&#39; 이미 존재 → 재사용&quot;
else
  echo &quot;  → VM 생성 중 (3~5분)...&quot;
  az vm create \
    --resource-group &quot;$RG&quot; \
    --name &quot;$VM&quot; \
    --image Ubuntu2404 \
    --size &quot;$VM_SIZE&quot; \
    --admin-username &quot;$USER_NAME&quot; \
    --ssh-key-values &quot;$SSH_KEY&quot; \
    --public-ip-sku Standard \
    --storage-sku Premium_LRS \
    --os-disk-size-gb 32 \
    --output none
  ok &quot;VM &#39;$VM&#39; 생성 완료&quot;
fi

PUBIP=$(az vm show -d -g &quot;$RG&quot; -n &quot;$VM&quot; --query publicIps -o tsv)
ok &quot;Public IP: $PUBIP&quot;

# =============================================================
# Step 4: NSG 포트 오픈 (22, 1433, 5000)
# =============================================================
step 4 &quot;NSG 포트 오픈 — SSH(22) + SQL(1433) + Flask(5000)&quot;

# NSG 이름 자동 탐색
NSG_NAME=$(az network nsg list -g &quot;$RG&quot; --query &quot;[0].name&quot; -o tsv 2&gt;/dev/null || echo &quot;${VM}NSG&quot;)

open_port() {
  local PORT=$1 PRIORITY=$2 NAME=$3
  if az network nsg rule show -g &quot;$RG&quot; --nsg-name &quot;$NSG_NAME&quot; -n &quot;$NAME&quot; &gt;/dev/null 2&gt;&amp;1; then
    ok &quot;$NAME ($PORT) 이미 존재&quot;
  else
    az vm open-port -g &quot;$RG&quot; -n &quot;$VM&quot; --port &quot;$PORT&quot; --priority &quot;$PRIORITY&quot; --output none 2&gt;/dev/null || true
    ok &quot;$NAME ($PORT) 오픈&quot;
  fi
}

open_port 1433 1010 &quot;allow_sql_1433&quot;
open_port 5000 1020 &quot;allow_flask_5000&quot;
ok &quot;NSG 규칙 적용 완료&quot;

# =============================================================
# Step 5: SSH 대기 + 접속
# =============================================================
step 5 &quot;SSH 연결 대기&quot;

echo &quot;  → SSH 준비 대기 (최대 90초)...&quot;
for i in $(seq 1 45); do
  if ssh -o StrictHostKeyChecking=no -o ConnectTimeout=3 -o BatchMode=yes \
     &quot;$USER_NAME@$PUBIP&quot; &quot;echo ready&quot; &gt;/dev/null 2&gt;&amp;1; then
    ok &quot;SSH 연결 가능&quot;
    break
  fi
  sleep 2
  [ $i -eq 45 ] &amp;&amp; abort &quot;SSH 연결 90초 타임아웃&quot;
done

# =============================================================
# Step 6: VM 내부 — SQL Server 설치 (0.0.0.0 바인딩)
# =============================================================
step 6 &quot;VM 내부: SQL Server 2025 설치 + 0.0.0.0 바인딩&quot;

ssh -o StrictHostKeyChecking=accept-new &quot;$USER_NAME@$PUBIP&quot; bash -s &quot;$SA_PASSWORD&quot; &lt;&lt; &#39;REMOTE_SCRIPT&#39;
#!/usr/bin/env bash
set -euo pipefail

SA_PASSWORD=&quot;$1&quot;
G=&#39;\033[0;32m&#39;; NC=&#39;\033[0m&#39;
ok() { echo -e &quot;${G}  ✓${NC} $1&quot;; }

echo &quot;=== [VM] 패키지 업데이트 ===&quot;
sudo apt update -qq
sudo apt install -y -qq curl wget gnupg2 software-properties-common \
  apt-transport-https ca-certificates lsb-release git unzip jq &gt; /dev/null
ok &quot;기본 패키지&quot;

# Swap (RAM &lt; 6GB)
RAM_GB=$(free -g | awk &#39;NR==2{print $2}&#39;)
if [ &quot;$RAM_GB&quot; -lt 6 ] &amp;&amp; ! swapon --show | grep -q swapfile; then
  sudo fallocate -l 2G /swapfile &amp;&amp; sudo chmod 600 /swapfile
  sudo mkswap /swapfile -q &amp;&amp; sudo swapon /swapfile
  grep -q &quot;/swapfile&quot; /etc/fstab || echo &#39;/swapfile none swap sw 0 0&#39; | sudo tee -a /etc/fstab &gt; /dev/null
  ok &quot;swap 2GB 활성화&quot;
fi

echo &quot;=== [VM] Microsoft GPG 키 + 저장소 ===&quot;
sudo rm -f /etc/apt/sources.list.d/mssql-server-2022.list /etc/apt/sources.list.d/mssql-server-preview.list
if [ ! -f /usr/share/keyrings/microsoft-prod.gpg ]; then
  curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | \
    sudo gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg
fi
curl -fsSL https://packages.microsoft.com/config/ubuntu/24.04/mssql-server-2025.list | \
  sudo tee /etc/apt/sources.list.d/mssql-server-2025.list &gt; /dev/null
curl -fsSL https://packages.microsoft.com/config/ubuntu/24.04/prod.list | \
  sudo tee /etc/apt/sources.list.d/mssql-release.list &gt; /dev/null
sudo apt update -qq
ok &quot;저장소 등록&quot;

echo &quot;=== [VM] SQL Server 설치 ===&quot;
if ! dpkg -l | grep -q &quot;^ii.*mssql-server &quot;; then
  sudo apt install -y -qq mssql-server &gt; /dev/null
  ok &quot;mssql-server 패키지 설치&quot;
else
  ok &quot;mssql-server 이미 설치됨&quot;
fi

if ! sudo systemctl is-active --quiet mssql-server; then
  sudo MSSQL_PID=Developer ACCEPT_EULA=Y MSSQL_SA_PASSWORD=&quot;$SA_PASSWORD&quot; \
    /opt/mssql/bin/mssql-conf -n setup &gt; /dev/null
  ok &quot;SQL Server setup (Developer Edition)&quot;
fi

# ★ 핵심: 0.0.0.0 바인딩 (외부 SSMS 접근 허용)
sudo /opt/mssql/bin/mssql-conf set network.ipaddress 0.0.0.0 &gt; /dev/null
sudo systemctl restart mssql-server

echo &quot;=== [VM] SQL Server 시작 대기 ===&quot;
for i in $(seq 1 30); do
  if sudo ss -tlnp 2&gt;/dev/null | grep -q &quot;:1433&quot;; then
    ok &quot;0.0.0.0:1433 listen 확인&quot;
    break
  fi
  sleep 2
  [ $i -eq 30 ] &amp;&amp; { echo &quot;❌ 60초 내 시작 안됨&quot;; exit 1; }
done

echo &quot;=== [VM] mssql-tools18 + ODBC ===&quot;
sudo ACCEPT_EULA=Y apt install -y -qq mssql-tools18 unixodbc-dev msodbcsql18 &gt; /dev/null
grep -q &quot;mssql-tools18/bin&quot; &quot;$HOME/.bashrc&quot; || \
  echo &#39;export PATH=&quot;$PATH:/opt/mssql-tools18/bin&quot;&#39; &gt;&gt; &quot;$HOME/.bashrc&quot;
export PATH=&quot;$PATH:/opt/mssql-tools18/bin&quot;
ok &quot;mssql-tools18 + ODBC Driver 18&quot;

# 연결 검증
if /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P &quot;$SA_PASSWORD&quot; -C -Q &quot;SELECT 1&quot; -h -1 -W 2&gt;/dev/null | grep -q &quot;^1$&quot;; then
  ok &quot;sqlcmd 로컬 연결 성공&quot;
else
  echo &quot;❌ sqlcmd 연결 실패&quot;; exit 1
fi

echo &quot;=== [VM] 완료 ===&quot;
REMOTE_SCRIPT

ok &quot;SQL Server 2025 설치 + 0.0.0.0 바인딩 완료&quot;

# =============================================================
# Step 7: DB 스키마 + 시드 데이터
# =============================================================
step 7 &quot;DB 스키마 + 시드 데이터 적용&quot;

ssh &quot;$USER_NAME@$PUBIP&quot; bash -s &quot;$SA_PASSWORD&quot; &quot;$REPO_URL&quot; &lt;&lt; &#39;SEED_SCRIPT&#39;
#!/usr/bin/env bash
set -euo pipefail
SA_PASSWORD=&quot;$1&quot;
REPO_URL=&quot;$2&quot;
INSTALL_DIR=&quot;$HOME/sqlvm_usedcar&quot;
export PATH=&quot;$PATH:/opt/mssql-tools18/bin&quot;

G=&#39;\033[0;32m&#39;; NC=&#39;\033[0m&#39;
ok() { echo -e &quot;${G}  ✓${NC} $1&quot;; }

# Repo clone
if [ -d &quot;$INSTALL_DIR/.git&quot; ]; then
  cd &quot;$INSTALL_DIR&quot; &amp;&amp; git pull --rebase --quiet
else
  [ -d &quot;$INSTALL_DIR&quot; ] &amp;&amp; mv &quot;$INSTALL_DIR&quot; &quot;${INSTALL_DIR}.bak.$(date +%s)&quot;
  git clone -q &quot;$REPO_URL&quot; &quot;$INSTALL_DIR&quot;
  cd &quot;$INSTALL_DIR&quot;
fi
ok &quot;Repo clone: $(git rev-parse --short HEAD)&quot;

# Schema
sqlcmd -S localhost -U sa -P &quot;$SA_PASSWORD&quot; -C -i sql/schema.sql &gt; /dev/null
ok &quot;schema.sql 적용 (Users, Cars, Inquiries + 3 indexes)&quot;

# Seed
sqlcmd -S localhost -U sa -P &quot;$SA_PASSWORD&quot; -C -i sql/seed.sql &gt; /dev/null

# 검증
SEED_COUNT=$(sqlcmd -S localhost -U sa -P &quot;$SA_PASSWORD&quot; -C -d CarMarket \
  -Q &quot;SELECT COUNT(*) FROM Cars&quot; -h -1 -W 2&gt;/dev/null | head -1 | tr -d &#39; \r&#39;)
if [ &quot;$SEED_COUNT&quot; = &quot;5&quot; ]; then
  ok &quot;seed.sql 적용 (Users 5건, Cars 5건)&quot;
else
  echo &quot;⚠ Cars 행 수: $SEED_COUNT (예상 5)&quot;
fi
SEED_SCRIPT

ok &quot;CarMarket DB 시딩 완료&quot;

# =============================================================
# Step 8: Flask 앱 + systemd + 검증
# =============================================================
step 8 &quot;Flask 앱 배포 + 헬스체크&quot;

ssh &quot;$USER_NAME@$PUBIP&quot; bash -s &quot;$SA_PASSWORD&quot; &lt;&lt; &#39;APP_SCRIPT&#39;
#!/usr/bin/env bash
set -euo pipefail
SA_PASSWORD=&quot;$1&quot;
INSTALL_DIR=&quot;$HOME/sqlvm_usedcar&quot;

G=&#39;\033[0;32m&#39;; NC=&#39;\033[0m&#39;
ok() { echo -e &quot;${G}  ✓${NC} $1&quot;; }

sudo apt install -y -qq python3 python3-pip python3-venv python3-dev &gt; /dev/null

cd &quot;$INSTALL_DIR/app&quot;
[ ! -f venv/bin/activate ] &amp;&amp; python3 -m venv venv
source venv/bin/activate
pip install --quiet --upgrade pip
pip install --quiet -r requirements.txt
deactivate

cat &gt; .env &lt;&lt;EOF
SA_PASSWORD=$SA_PASSWORD
DB_SERVER=localhost
DB_NAME=CarMarket
FLASK_PORT=5000
EOF
chmod 600 .env
ok &quot;.env 생성&quot;

cd &quot;$INSTALL_DIR&quot;
sudo cp systemd/carmarket.service /etc/systemd/system/carmarket.service
sudo sed -i &quot;s|/home/azureuser|$HOME|g&quot; /etc/systemd/system/carmarket.service
sudo sed -i &quot;s|User=azureuser|User=$(whoami)|&quot; /etc/systemd/system/carmarket.service
sudo systemctl daemon-reload
sudo systemctl enable carmarket --quiet
sudo systemctl restart carmarket
sleep 3

if sudo systemctl is-active --quiet carmarket; then
  ok &quot;carmarket.service 실행 중&quot;
else
  echo &quot;❌ Flask 서비스 실행 실패&quot;
  sudo journalctl -u carmarket -n 20 --no-pager
  exit 1
fi

HEALTH=$(curl -s --max-time 5 http://localhost:5000/health || echo &#39;{}&#39;)
if echo &quot;$HEALTH&quot; | grep -q &#39;&quot;status&quot;:&quot;ok&quot;&#39;; then
  ok &quot;Health OK: $HEALTH&quot;
fi
APP_SCRIPT

ok &quot;Flask 앱 배포 완료&quot;

# =============================================================
# 외부 검증
# =============================================================
echo &quot;&quot;
sleep 3
echo &quot;  → 외부 헬스체크...&quot;
if curl -fsS --max-time 10 &quot;http://$PUBIP:5000/health&quot; 2&gt;/dev/null | grep -q &#39;&quot;status&quot;:&quot;ok&quot;&#39;; then
  ok &quot;외부 Flask 접근 확인: http://$PUBIP:5000/&quot;
else
  warn &quot;Flask 외부 접근 실패 — NSG/서비스 확인 필요&quot;
fi

# =============================================================
# 완료 안내
# =============================================================
banner &quot;설치 완료!&quot;

# 환경변수 파일 저장
cat &gt; &quot;$HOME/.carmarket-env&quot; &lt;&lt;EOF
export RG=$RG
export LOC=$LOC
export VM=$VM
export PUBIP=$PUBIP
export USER_NAME=$USER_NAME
export SA_PASSWORD=&#39;$SA_PASSWORD&#39;
EOF
chmod 600 &quot;$HOME/.carmarket-env&quot;

cat &lt;&lt;EOF

┌─────────────────────────────────────────────────────────────┐
│  SQL Server 2025 (Developer)  — 외부 SSMS 접근 가능        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  SSMS 연결 정보:                                            │
│    서버:    $PUBIP,1433                                     │
│    인증:    SQL Server 인증                                  │
│    로그인:  sa                                              │
│    암호:    (설정한 SA_PASSWORD)                             │
│    ★ 연결 속성 → &quot;서버 인증서 신뢰&quot; 체크                     │
│                                                             │
│  웹 앱: http://$PUBIP:5000/                                 │
│  SSH:   ssh $USER_NAME@$PUBIP                               │
│                                                             │
│  비용 차단:  az vm deallocate -g $RG -n $VM                 │
│  완전 삭제:  az group delete -n $RG --yes --no-wait         │
│                                                             │
│  환경변수:   source ~/.carmarket-env                         │
└─────────────────────────────────────────────────────────────┘

EOF
</code></pre>
<ol>
<li>Azure CLI 로그인 확인 + SSH 키 검증</li>
<li>Resource Group + Ubuntu 24.04 VM (B2s) 생성</li>
<li>NSG에서 22(SSH) + 1433(SQL) + 5000(Flask) 포트 오픈</li>
<li>SQL Server 2025 Developer Edition 설치</li>
<li>0.0.0.0 바인딩 (외부 SSMS 접근 허용)</li>
<li>CarMarket DB 스키마 생성 + 시드 데이터 5건</li>
<li>Flask 앱 배포 + systemd 서비스 등록</li>
</ol>
<h2 id="ssms-연결">SSMS 연결</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/76ad8d8a-bf61-4487-a0ad-9db37c663a62/image.png" alt="">
또는 VSCode에서 SQL Server(mssql) extension을 통해 접속 가능</p>
<h3 id="데이터-확인">데이터 확인</h3>
<pre><code>USE CarMarket;
GO
-- 테이블 목록 확인
SELECT TABLE_NAME, TABLE_TYPE
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_TYPE = &#39;BASE TABLE&#39;;
-- 차량 매물 확인
SELECT c.Brand, c.Model, c.Year,
FORMAT(c.Price, &#39;N0&#39;) AS Price,
FORMAT(c.Mileage, &#39;N0&#39;) AS Mileage,
u.Name AS Seller
FROM Cars c
JOIN Users u ON c.SellerId = u.UserId
ORDER BY c.Price DESC;
-- 인덱스 확인
SELECT i.name AS IndexName,
t.name AS TableName,
COL_NAME(ic.object_id, ic.column_id) AS ColumnName
FROM sys.indexes i
JOIN sys.tables t ON i.object_id = t.object_id
JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
WHERE i.name LIKE &#39;IX_%&#39;;</code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/329935fb-ae94-4c2c-bdf2-71ce814921d9/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/07470af1-2433-47ca-ad72-9540bd030577/image.png" alt=""></p>
<p>이 실습에서는 학습 편의를 위해 1433을 외부에 직접 오픈합니다. 프로덕션 환경에서는 절대 하지 마세요. 실무에서는 다음 방법을 사용합니다:</p>
<ul>
<li>SSH 터널: <code>ssh -L 1433:localhost:1433 azureuser@&lt;PUBIP&gt;</code> 후 SSMS에서 localhost 접속</li>
<li>VPN Gateway 또는 Azure Bastion</li>
<li>Private Endpoint</li>
</ul>
<h2 id="azure-sql-database-마이그레이션">Azure SQL Database 마이그레이션</h2>
<pre><code class="language-shell">#!/usr/bin/env bash
# ============================================================
# 02-migrate-to-azure-sql.sh
# VM SQL Server → Azure SQL Database 마이그레이션 (Azure DMS)
#
# 사전 조건:
#   - 01-setup-vm-sqlserver.sh 완료
#   - source ~/.carmarket-env (환경변수 로드)
#
# 사용법:
#   source ~/.carmarket-env
#   bash 02-migrate-to-azure-sql.sh
# ============================================================

set -euo pipefail

# =============================================================
# 환경변수 확인 + 기본값
# =============================================================
RG=&quot;${RG:-rg-carmarket-lab}&quot;
LOC=&quot;${LOC:-koreacentral}&quot;
VM=&quot;${VM:-}&quot;
PUBIP=&quot;${PUBIP:-}&quot;
USER_NAME=&quot;${USER_NAME:-azureuser}&quot;
SA_PASSWORD=&quot;${SA_PASSWORD:-}&quot;

# Azure SQL 관련 변수
SQL_SERVER_NAME=&quot;${SQL_SERVER_NAME:-sql-carmarket-$(date +%m%d)-$RANDOM}&quot;
SQL_DB_NAME=&quot;${SQL_DB_NAME:-CarMarket}&quot;
SQL_ADMIN=&quot;${SQL_ADMIN:-sqladmin}&quot;
SQL_ADMIN_PASSWORD=&quot;${SQL_ADMIN_PASSWORD:-}&quot;
DMS_NAME=&quot;${DMS_NAME:-dms-carmarket-$(date +%m%d)}&quot;

# 색상
G=&#39;\033[0;32m&#39;; Y=&#39;\033[0;33m&#39;; R=&#39;\033[0;31m&#39;; B=&#39;\033[0;34m&#39;; NC=&#39;\033[0m&#39;
banner() { echo &quot;&quot;; echo -e &quot;${B}═══════════════════════════════════════════════════${NC}&quot;; echo -e &quot;${B}  $1${NC}&quot;; echo -e &quot;${B}═══════════════════════════════════════════════════${NC}&quot;; }
step()   { echo &quot;&quot;; echo -e &quot;${G}▶ [$1/$TOTAL_STEPS]${NC} $2&quot;; }
ok()     { echo -e &quot;${G}  ✓${NC} $1&quot;; }
warn()   { echo -e &quot;${Y}  ⚠${NC} $1&quot;; }
abort()  { echo -e &quot;${R}❌ $1${NC}&quot;; exit 1; }

TOTAL_STEPS=7
LOG_FILE=&quot;/tmp/carmarket-migrate-$(date +%Y%m%d-%H%M%S).log&quot;
exec &gt; &gt;(tee -a &quot;$LOG_FILE&quot;) 2&gt;&amp;1

banner &quot;CarMarket Lab — VM → Azure SQL Database 마이그레이션&quot;

# =============================================================
# Step 1: 사전 점검
# =============================================================
step 1 &quot;사전 점검&quot;

command -v az &gt;/dev/null 2&gt;&amp;1 || abort &quot;Azure CLI 미설치&quot;
az account show &gt;/dev/null 2&gt;&amp;1 || { warn &quot;로그인 필요&quot;; az login; }
ok &quot;Azure CLI 로그인 확인&quot;

# PUBIP가 비어있으면 VM에서 가져오기
if [ -z &quot;$PUBIP&quot; ] &amp;&amp; [ -n &quot;$VM&quot; ]; then
  PUBIP=$(az vm show -d -g &quot;$RG&quot; -n &quot;$VM&quot; --query publicIps -o tsv 2&gt;/dev/null || echo &quot;&quot;)
fi
[ -z &quot;$PUBIP&quot; ] &amp;&amp; abort &quot;PUBIP를 확인할 수 없습니다. source ~/.carmarket-env 후 재시도&quot;
ok &quot;소스 VM: $PUBIP&quot;

# SA 비밀번호 확인
if [ -z &quot;$SA_PASSWORD&quot; ]; then
  read -s -p &quot;  소스 VM SA 비밀번호: &quot; SA_PASSWORD; echo &quot;&quot;
fi
ok &quot;소스 SA 비밀번호 확인&quot;

# Azure SQL 관리자 비밀번호
if [ -z &quot;$SQL_ADMIN_PASSWORD&quot; ]; then
  echo &quot;  Azure SQL Database 관리자 비밀번호를 입력하세요.&quot;
  echo &quot;  (소스와 같은 비밀번호 사용 가능)&quot;
  while true; do
    read -s -p &quot;  SQL Admin Password: &quot; SQL_ADMIN_PASSWORD; echo &quot;&quot;
    read -s -p &quot;  Confirm:            &quot; CONFIRM; echo &quot;&quot;
    [ &quot;$SQL_ADMIN_PASSWORD&quot; = &quot;$CONFIRM&quot; ] &amp;&amp; [ ${#SQL_ADMIN_PASSWORD} -ge 8 ] &amp;&amp; break
    echo -e &quot;${R}  불일치 또는 8자 미만${NC}&quot;
  done
fi
ok &quot;Azure SQL 관리자 비밀번호 설정&quot;

echo &quot;&quot;
echo &quot;  Azure SQL Server: $SQL_SERVER_NAME.database.windows.net&quot;
echo &quot;  Database:         $SQL_DB_NAME&quot;
echo &quot;  Admin:            $SQL_ADMIN&quot;
echo &quot;  예상 비용: DTU 기반 S0 ≈ \$0.49/일&quot;
read -p &quot;  진행? (y/N): &quot; ok_proceed
[[ &quot;$ok_proceed&quot; =~ ^[Yy]$ ]] || abort &quot;취소됨&quot;

# =============================================================
# Step 2: Azure SQL Server + Database 생성
# =============================================================
step 2 &quot;Azure SQL Server + Database 생성&quot;

# SQL Server (논리 서버)
if az sql server show -g &quot;$RG&quot; -n &quot;$SQL_SERVER_NAME&quot; &gt;/dev/null 2&gt;&amp;1; then
  ok &quot;SQL Server &#39;$SQL_SERVER_NAME&#39; 이미 존재&quot;
else
  echo &quot;  → 논리 서버 생성 중...&quot;
  az sql server create \
    --resource-group &quot;$RG&quot; \
    --name &quot;$SQL_SERVER_NAME&quot; \
    --location &quot;$LOC&quot; \
    --admin-user &quot;$SQL_ADMIN&quot; \
    --admin-password &quot;$SQL_ADMIN_PASSWORD&quot; \
    --output none
  ok &quot;SQL Server &#39;$SQL_SERVER_NAME&#39; 생성&quot;
fi

# 방화벽: VM Public IP 허용
echo &quot;  → 방화벽 규칙 추가...&quot;
az sql server firewall-rule create \
  --resource-group &quot;$RG&quot; \
  --server &quot;$SQL_SERVER_NAME&quot; \
  --name &quot;AllowSourceVM&quot; \
  --start-ip-address &quot;$PUBIP&quot; \
  --end-ip-address &quot;$PUBIP&quot; \
  --output none 2&gt;/dev/null || true

# 방화벽: 내 로컬 IP 허용
MY_IP=$(curl -s https://api.ipify.org 2&gt;/dev/null || echo &quot;&quot;)
if [ -n &quot;$MY_IP&quot; ]; then
  az sql server firewall-rule create \
    --resource-group &quot;$RG&quot; \
    --server &quot;$SQL_SERVER_NAME&quot; \
    --name &quot;AllowMyIP&quot; \
    --start-ip-address &quot;$MY_IP&quot; \
    --end-ip-address &quot;$MY_IP&quot; \
    --output none 2&gt;/dev/null || true
  ok &quot;방화벽: VM($PUBIP) + 로컬($MY_IP) 허용&quot;
else
  ok &quot;방화벽: VM($PUBIP) 허용&quot;
fi

# Azure 서비스 접근 허용
az sql server firewall-rule create \
  --resource-group &quot;$RG&quot; \
  --server &quot;$SQL_SERVER_NAME&quot; \
  --name &quot;AllowAzureServices&quot; \
  --start-ip-address 0.0.0.0 \
  --end-ip-address 0.0.0.0 \
  --output none 2&gt;/dev/null || true
ok &quot;Azure 서비스 접근 허용&quot;

# Database 생성 (S0 = 10 DTU, 실습에 충분)
if az sql db show -g &quot;$RG&quot; -s &quot;$SQL_SERVER_NAME&quot; -n &quot;$SQL_DB_NAME&quot; &gt;/dev/null 2&gt;&amp;1; then
  ok &quot;Database &#39;$SQL_DB_NAME&#39; 이미 존재&quot;
else
  echo &quot;  → Database 생성 중 (1~2분)...&quot;
  az sql db create \
    --resource-group &quot;$RG&quot; \
    --server &quot;$SQL_SERVER_NAME&quot; \
    --name &quot;$SQL_DB_NAME&quot; \
    --service-objective S0 \
    --output none
  ok &quot;Database &#39;$SQL_DB_NAME&#39; 생성 (S0 / 10 DTU)&quot;
fi

SQL_FQDN=&quot;${SQL_SERVER_NAME}.database.windows.net&quot;
ok &quot;Azure SQL: $SQL_FQDN / $SQL_DB_NAME&quot;

# =============================================================
# Step 3: 소스 VM에서 bacpac 내보내기 준비
# =============================================================
step 3 &quot;소스 DB에서 스키마·데이터 SQL 스크립트 생성&quot;

# DMS 대신 sqlcmd를 통한 직접 마이그레이션 (소규모 DB에 적합)
# 대규모에서는 DMS를 사용하지만, 이 실습은 교육용이므로 두 방식 모두 제공

echo &quot;  → VM에서 스키마 + 데이터 추출...&quot;
ssh &quot;$USER_NAME@$PUBIP&quot; bash -s &quot;$SA_PASSWORD&quot; &lt;&lt; &#39;EXPORT_SCRIPT&#39;
#!/usr/bin/env bash
set -euo pipefail
SA_PASSWORD=&quot;$1&quot;
export PATH=&quot;$PATH:/opt/mssql-tools18/bin&quot;
EXPORT_DIR=&quot;$HOME/migration_export&quot;
mkdir -p &quot;$EXPORT_DIR&quot;

G=&#39;\033[0;32m&#39;; NC=&#39;\033[0m&#39;
ok() { echo -e &quot;${G}  ✓${NC} $1&quot;; }

# Azure SQL 호환 스키마 생성 (IDENTITY 유지, Azure SQL 미지원 구문 제거)
cat &gt; &quot;$EXPORT_DIR/schema-azure.sql&quot; &lt;&lt; &#39;AZSCHEMA&#39;
-- Azure SQL Database용 스키마 (CarMarket)
-- Azure SQL은 CREATE DATABASE를 별도로 실행하므로 DB 생성 구문 제외

-- 기존 테이블 정리 (멱등성)
IF OBJECT_ID(&#39;Inquiries&#39;, &#39;U&#39;) IS NOT NULL DROP TABLE Inquiries;
IF OBJECT_ID(&#39;Cars&#39;,      &#39;U&#39;) IS NOT NULL DROP TABLE Cars;
IF OBJECT_ID(&#39;Users&#39;,     &#39;U&#39;) IS NOT NULL DROP TABLE Users;
GO

CREATE TABLE Users (
    UserId    INT IDENTITY(1,1) PRIMARY KEY,
    Name      NVARCHAR(100)  NOT NULL,
    Email     NVARCHAR(200)  NOT NULL UNIQUE,
    Phone     NVARCHAR(20),
    UserType  NVARCHAR(10)   NOT NULL DEFAULT &#39;both&#39;
              CHECK (UserType IN (&#39;seller&#39;, &#39;buyer&#39;, &#39;both&#39;)),
    CreatedAt DATETIME2      DEFAULT SYSUTCDATETIME()
);
GO

CREATE TABLE Cars (
    CarId       INT IDENTITY(1,1) PRIMARY KEY,
    SellerId    INT            NOT NULL FOREIGN KEY REFERENCES Users(UserId),
    Brand       NVARCHAR(50)   NOT NULL,
    Model       NVARCHAR(100)  NOT NULL,
    Year        INT            NOT NULL,
    Price       DECIMAL(12, 0) NOT NULL,
    Mileage     INT            NOT NULL,
    FuelType    NVARCHAR(20),
    Description NVARCHAR(MAX),
    Status      NVARCHAR(20)   NOT NULL DEFAULT &#39;available&#39;
                CHECK (Status IN (&#39;available&#39;, &#39;reserved&#39;, &#39;sold&#39;)),
    CreatedAt   DATETIME2      DEFAULT SYSUTCDATETIME()
);
GO

CREATE TABLE Inquiries (
    InquiryId INT IDENTITY(1,1) PRIMARY KEY,
    CarId     INT            NOT NULL FOREIGN KEY REFERENCES Cars(CarId),
    BuyerId   INT            NOT NULL FOREIGN KEY REFERENCES Users(UserId),
    Message   NVARCHAR(1000) NOT NULL,
    CreatedAt DATETIME2      DEFAULT SYSUTCDATETIME()
);
GO

CREATE INDEX IX_Cars_Brand     ON Cars(Brand);
CREATE INDEX IX_Cars_Status    ON Cars(Status);
CREATE INDEX IX_Cars_CreatedAt ON Cars(CreatedAt DESC);
GO
AZSCHEMA
ok &quot;Azure SQL 호환 스키마 생성&quot;

# 데이터 추출 (INSERT 문으로)
sqlcmd -S localhost -U sa -P &quot;$SA_PASSWORD&quot; -C -d CarMarket -h -1 -W -Q &quot;
SET NOCOUNT ON;

-- Users
SELECT &#39;SET IDENTITY_INSERT Users ON;&#39;
UNION ALL
SELECT &#39;INSERT INTO Users (UserId, Name, Email, Phone, UserType) VALUES (&#39;
  + CAST(UserId AS NVARCHAR) + &#39;, N&#39;&#39;&#39; + REPLACE(Name, &#39;&#39;&#39;&#39;, &#39;&#39;&#39;&#39;&#39;&#39;) + &#39;&#39;&#39;, &#39;&#39;&#39;
  + Email + &#39;&#39;&#39;, &#39;&#39;&#39; + ISNULL(Phone, &#39;&#39;) + &#39;&#39;&#39;, &#39;&#39;&#39; + UserType + &#39;&#39;&#39;);&#39;
FROM Users
UNION ALL
SELECT &#39;SET IDENTITY_INSERT Users OFF;&#39;
UNION ALL
SELECT &#39;&#39;
UNION ALL
-- Cars
SELECT &#39;SET IDENTITY_INSERT Cars ON;&#39;
UNION ALL
SELECT &#39;INSERT INTO Cars (CarId, SellerId, Brand, Model, Year, Price, Mileage, FuelType, Description, Status) VALUES (&#39;
  + CAST(CarId AS NVARCHAR) + &#39;, &#39; + CAST(SellerId AS NVARCHAR) + &#39;, N&#39;&#39;&#39;
  + REPLACE(Brand, &#39;&#39;&#39;&#39;, &#39;&#39;&#39;&#39;&#39;&#39;) + &#39;&#39;&#39;, N&#39;&#39;&#39; + REPLACE(Model, &#39;&#39;&#39;&#39;, &#39;&#39;&#39;&#39;&#39;&#39;) + &#39;&#39;&#39;, &#39;
  + CAST(Year AS NVARCHAR) + &#39;, &#39; + CAST(Price AS NVARCHAR) + &#39;, &#39;
  + CAST(Mileage AS NVARCHAR) + &#39;, N&#39;&#39;&#39; + ISNULL(FuelType, &#39;&#39;) + &#39;&#39;&#39;, N&#39;&#39;&#39;
  + ISNULL(REPLACE(Description, &#39;&#39;&#39;&#39;, &#39;&#39;&#39;&#39;&#39;&#39;), &#39;&#39;) + &#39;&#39;&#39;, &#39;&#39;&#39; + Status + &#39;&#39;&#39;);&#39;
FROM Cars
UNION ALL
SELECT &#39;SET IDENTITY_INSERT Cars OFF;&#39;
UNION ALL
SELECT &#39;GO&#39;;
&quot; &gt; &quot;$EXPORT_DIR/seed-azure.sql&quot; 2&gt;/dev/null

# 빈 줄/공백 정리
sed -i &#39;/^$/d&#39; &quot;$EXPORT_DIR/seed-azure.sql&quot;
ok &quot;데이터 INSERT 스크립트 생성&quot;

# 행 수 검증
USER_CNT=$(sqlcmd -S localhost -U sa -P &quot;$SA_PASSWORD&quot; -C -d CarMarket \
  -Q &quot;SET NOCOUNT ON; SELECT COUNT(*) FROM Users&quot; -h -1 -W | head -1 | tr -d &#39; \r&#39;)
CAR_CNT=$(sqlcmd -S localhost -U sa -P &quot;$SA_PASSWORD&quot; -C -d CarMarket \
  -Q &quot;SET NOCOUNT ON; SELECT COUNT(*) FROM Cars&quot; -h -1 -W | head -1 | tr -d &#39; \r&#39;)
ok &quot;소스 DB: Users=${USER_CNT}건, Cars=${CAR_CNT}건&quot;

echo &quot;$USER_CNT $CAR_CNT&quot; &gt; &quot;$EXPORT_DIR/source_counts.txt&quot;
EXPORT_SCRIPT

ok &quot;마이그레이션 데이터 준비 완료&quot;

# =============================================================
# Step 4: Azure SQL에 스키마 적용
# =============================================================
step 4 &quot;Azure SQL Database에 스키마 적용&quot;

# VM에서 Azure SQL로 직접 sqlcmd 실행
ssh &quot;$USER_NAME@$PUBIP&quot; bash -s &quot;$SQL_FQDN&quot; &quot;$SQL_ADMIN&quot; &quot;$SQL_ADMIN_PASSWORD&quot; &quot;$SQL_DB_NAME&quot; &lt;&lt; &#39;APPLY_SCHEMA&#39;
#!/usr/bin/env bash
set -euo pipefail
SQL_FQDN=&quot;$1&quot;; SQL_ADMIN=&quot;$2&quot;; SQL_ADMIN_PASSWORD=&quot;$3&quot;; SQL_DB_NAME=&quot;$4&quot;
export PATH=&quot;$PATH:/opt/mssql-tools18/bin&quot;
EXPORT_DIR=&quot;$HOME/migration_export&quot;

G=&#39;\033[0;32m&#39;; NC=&#39;\033[0m&#39;
ok() { echo -e &quot;${G}  ✓${NC} $1&quot;; }

echo &quot;  → Azure SQL에 스키마 적용...&quot;
sqlcmd -S &quot;$SQL_FQDN&quot; -U &quot;$SQL_ADMIN&quot; -P &quot;$SQL_ADMIN_PASSWORD&quot; -d &quot;$SQL_DB_NAME&quot; \
  -i &quot;$EXPORT_DIR/schema-azure.sql&quot; &gt; /dev/null
ok &quot;스키마 적용 완료&quot;

echo &quot;  → Azure SQL에 시드 데이터 적용...&quot;
sqlcmd -S &quot;$SQL_FQDN&quot; -U &quot;$SQL_ADMIN&quot; -P &quot;$SQL_ADMIN_PASSWORD&quot; -d &quot;$SQL_DB_NAME&quot; \
  -i &quot;$EXPORT_DIR/seed-azure.sql&quot; &gt; /dev/null 2&gt;&amp;1 || true
ok &quot;시드 데이터 적용&quot;
APPLY_SCHEMA

ok &quot;Azure SQL 스키마 + 시드 적용 완료&quot;

# =============================================================
# Step 5: Azure DMS를 통한 온라인 마이그레이션 (선택)
# =============================================================
step 5 &quot;Azure DMS 리소스 생성 (추가 마이그레이션 도구)&quot;

echo &quot;  ℹ️  소규모 DB는 Step 4의 직접 sqlcmd 방식으로 충분합니다.&quot;
echo &quot;  ℹ️  대규모·프로덕션에서는 Azure DMS를 사용합니다.&quot;
echo &quot;&quot;

read -p &quot;  DMS 리소스도 생성하시겠습니까? (y/N): &quot; create_dms
if [[ &quot;$create_dms&quot; =~ ^[Yy]$ ]]; then
  # DMS 확장 설치
  az extension add --name dms 2&gt;/dev/null || true

  echo &quot;  → DMS 인스턴스 생성 중 (5~10분)...&quot;
  az dms create \
    --resource-group &quot;$RG&quot; \
    --name &quot;$DMS_NAME&quot; \
    --location &quot;$LOC&quot; \
    --sku-name Standard_1vCores \
    --output none 2&gt;/dev/null || warn &quot;DMS 생성 실패 (수동 생성 필요할 수 있음)&quot;

  ok &quot;DMS &#39;$DMS_NAME&#39; 생성&quot;
  echo &quot;&quot;
  echo &quot;  DMS는 Azure Portal에서 마이그레이션 프로젝트를 생성하여 사용합니다.&quot;
  echo &quot;  Portal → Database Migration Service → 새 마이그레이션 프로젝트&quot;
  echo &quot;    소스: SQL Server ($PUBIP:1433)&quot;
  echo &quot;    대상: Azure SQL Database ($SQL_FQDN)&quot;
else
  ok &quot;DMS 생성 건너뜀 (sqlcmd 직접 방식 사용)&quot;
fi

# =============================================================
# Step 6: 마이그레이션 검증
# =============================================================
step 6 &quot;마이그레이션 검증&quot;

echo &quot;  → Azure SQL 데이터 검증...&quot;
VERIFY_RESULT=$(ssh &quot;$USER_NAME@$PUBIP&quot; bash -s &quot;$SQL_FQDN&quot; &quot;$SQL_ADMIN&quot; &quot;$SQL_ADMIN_PASSWORD&quot; &quot;$SQL_DB_NAME&quot; &lt;&lt; &#39;VERIFY&#39;
export PATH=&quot;$PATH:/opt/mssql-tools18/bin&quot;
SQL_FQDN=&quot;$1&quot;; SQL_ADMIN=&quot;$2&quot;; SQL_ADMIN_PASSWORD=&quot;$3&quot;; SQL_DB_NAME=&quot;$4&quot;

# 테이블 수
TABLE_COUNT=$(sqlcmd -S &quot;$SQL_FQDN&quot; -U &quot;$SQL_ADMIN&quot; -P &quot;$SQL_ADMIN_PASSWORD&quot; -d &quot;$SQL_DB_NAME&quot; \
  -Q &quot;SET NOCOUNT ON; SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE=&#39;BASE TABLE&#39;&quot; \
  -h -1 -W 2&gt;/dev/null | head -1 | tr -d &#39; \r&#39;)

# Users 수
USER_COUNT=$(sqlcmd -S &quot;$SQL_FQDN&quot; -U &quot;$SQL_ADMIN&quot; -P &quot;$SQL_ADMIN_PASSWORD&quot; -d &quot;$SQL_DB_NAME&quot; \
  -Q &quot;SET NOCOUNT ON; SELECT COUNT(*) FROM Users&quot; -h -1 -W 2&gt;/dev/null | head -1 | tr -d &#39; \r&#39;)

# Cars 수
CAR_COUNT=$(sqlcmd -S &quot;$SQL_FQDN&quot; -U &quot;$SQL_ADMIN&quot; -P &quot;$SQL_ADMIN_PASSWORD&quot; -d &quot;$SQL_DB_NAME&quot; \
  -Q &quot;SET NOCOUNT ON; SELECT COUNT(*) FROM Cars&quot; -h -1 -W 2&gt;/dev/null | head -1 | tr -d &#39; \r&#39;)

# 인덱스 수
IDX_COUNT=$(sqlcmd -S &quot;$SQL_FQDN&quot; -U &quot;$SQL_ADMIN&quot; -P &quot;$SQL_ADMIN_PASSWORD&quot; -d &quot;$SQL_DB_NAME&quot; \
  -Q &quot;SET NOCOUNT ON; SELECT COUNT(*) FROM sys.indexes WHERE name LIKE &#39;IX_%&#39;&quot; \
  -h -1 -W 2&gt;/dev/null | head -1 | tr -d &#39; \r&#39;)

echo &quot;$TABLE_COUNT $USER_COUNT $CAR_COUNT $IDX_COUNT&quot;
VERIFY
)

read T_CNT U_CNT C_CNT I_CNT &lt;&lt;&lt; &quot;$VERIFY_RESULT&quot;
ok &quot;Azure SQL 테이블: ${T_CNT}개&quot;
ok &quot;Azure SQL Users:  ${U_CNT}건&quot;
ok &quot;Azure SQL Cars:   ${C_CNT}건&quot;
ok &quot;Azure SQL 인덱스: ${I_CNT}개&quot;

# 소스와 비교
SOURCE_COUNTS=$(ssh &quot;$USER_NAME@$PUBIP&quot; &quot;cat ~/migration_export/source_counts.txt&quot; 2&gt;/dev/null || echo &quot;5 5&quot;)
read S_U S_C &lt;&lt;&lt; &quot;$SOURCE_COUNTS&quot;

if [ &quot;$U_CNT&quot; = &quot;$S_U&quot; ] &amp;&amp; [ &quot;$C_CNT&quot; = &quot;$S_C&quot; ]; then
  ok &quot;✅ 소스 ↔ 대상 데이터 일치 (Users: $S_U, Cars: $S_C)&quot;
else
  warn &quot;데이터 불일치: 소스(U:$S_U, C:$S_C) ↔ 대상(U:$U_CNT, C:$C_CNT)&quot;
fi

# =============================================================
# Step 7: 연결 정보 저장
# =============================================================
step 7 &quot;연결 정보 저장&quot;

# 환경변수 파일 업데이트
cat &gt;&gt; &quot;$HOME/.carmarket-env&quot; &lt;&lt;EOF

# Azure SQL Database
export SQL_SERVER_NAME=$SQL_SERVER_NAME
export SQL_FQDN=$SQL_FQDN
export SQL_DB_NAME=$SQL_DB_NAME
export SQL_ADMIN=$SQL_ADMIN
export SQL_ADMIN_PASSWORD=&#39;$SQL_ADMIN_PASSWORD&#39;
EOF
chmod 600 &quot;$HOME/.carmarket-env&quot;
ok &quot;~/.carmarket-env 업데이트&quot;

# =============================================================
# 완료 안내
# =============================================================
banner &quot;마이그레이션 완료!&quot;

cat &lt;&lt;EOF

┌─────────────────────────────────────────────────────────────┐
│  Azure SQL Database 마이그레이션 결과                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  소스 (VM SQL Server):                                      │
│    $PUBIP:1433 / sa                                         │
│                                                             │
│  대상 (Azure SQL Database):                                 │
│    서버: $SQL_FQDN                                          │
│    DB:   $SQL_DB_NAME                                       │
│    관리자: $SQL_ADMIN                                       │
│                                                             │
│  SSMS 연결 (Azure SQL):                                     │
│    서버: $SQL_FQDN                                          │
│    인증: SQL Server 인증                                     │
│    로그인: $SQL_ADMIN                                       │
│    암호: (설정한 SQL_ADMIN_PASSWORD)                         │
│                                                             │
│  검증: 테이블 ${T_CNT}개, Users ${U_CNT}건, Cars ${C_CNT}건   │
│                                                             │
│  다음 단계:                                                  │
│    bash 03-switch-app-to-azure-sql.sh                       │
│    (Flask 앱을 Azure SQL로 전환)                             │
│                                                             │
│  환경변수: source ~/.carmarket-env                           │
└─────────────────────────────────────────────────────────────┘

EOF
</code></pre>
<ol>
<li>Azure SQL Server (논리 서버) + Database (S0) 생성</li>
<li>방화벽 규칙 설정 (VM IP + 로컬 IP + Azure 서비스)</li>
<li>VM SQL Server에서 스키마·데이터를 SQL 스크립트로 추출</li>
<li>Azure SQL Database에 스키마·시드 데이터 적용</li>
<li>Azure DMS 리소스 생성 (선택)</li>
<li>소스 ↔ 대상 데이터 일치 검증</li>
</ol>
<p><img src="https://velog.velcdn.com/images/rudin_/post/845fbfee-b531-41b5-b94a-aea4ece017b6/image.png" alt=""></p>
<h3 id="검증">검증</h3>
<p>마이그레이션 검증 (SSMS에서)</p>
<pre><code>-- 테이블 구조 비교
SELECT TABLE_NAME,
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS c
WHERE c.TABLE_NAME = t.TABLE_NAME) AS ColumnCount
FROM INFORMATION_SCHEMA.TABLES t
WHERE TABLE_TYPE = &#39;BASE TABLE&#39;
ORDER BY TABLE_NAME;
-- 행 수 비교 (소스와 동일해야 함)
SELECT &#39;Users&#39; AS TableName, COUNT(*) AS [RowCount] FROM Users
UNION ALL
SELECT &#39;Cars&#39;, COUNT(*) FROM Cars
UNION ALL
SELECT &#39;Inquiries&#39;, COUNT(*) FROM Inquiries;
-- 데이터 내용 확인
SELECT c.Brand, c.Model, c.Year,
FORMAT(c.Price, &#39;N0&#39;) AS Price,
u.Name AS Seller
FROM Cars c
JOIN Users u ON c.SellerId = u.UserId
ORDER BY c.Price DESC;
-- Azure SQL 특유 정보 확인
SELECT
@@VERSION AS SQLVersion,
DB_NAME() AS DatabaseName,
DATABASEPROPERTYEX(DB_NAME(), &#39;Edition&#39;) AS Edition,
DATABASEPROPERTYEX(DB_NAME(), &#39;ServiceObjective&#39;) AS ServiceTier;</code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/09f7b91e-8207-4f51-9311-374e1946bad4/image.png" alt=""></p>
<h3 id="vm-sql-server-vs-azure-sql-database-비교">VM SQL Server vs Azure SQL Database 비교</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/5db8dae0-7c4b-4e01-a5ab-9cb851a42496/image.png" alt=""></p>
<h2 id="flask-앱-연결-전환">Flask 앱 연결 전환</h2>
<pre><code class="language-shell">#!/usr/bin/env bash
# ============================================================
# 03-switch-app-to-azure-sql.sh
# Flask 앱 연결 대상을 VM SQL Server → Azure SQL Database로 전환
#
# 사전 조건:
#   - 01, 02 스크립트 완료
#   - source ~/.carmarket-env
#
# 사용법:
#   source ~/.carmarket-env
#   bash 03-switch-app-to-azure-sql.sh
# ============================================================

set -euo pipefail

# 환경변수
RG=&quot;${RG:-rg-carmarket-lab}&quot;
VM=&quot;${VM:-}&quot;
PUBIP=&quot;${PUBIP:-}&quot;
USER_NAME=&quot;${USER_NAME:-azureuser}&quot;
SQL_FQDN=&quot;${SQL_FQDN:-}&quot;
SQL_DB_NAME=&quot;${SQL_DB_NAME:-CarMarket}&quot;
SQL_ADMIN=&quot;${SQL_ADMIN:-sqladmin}&quot;
SQL_ADMIN_PASSWORD=&quot;${SQL_ADMIN_PASSWORD:-}&quot;

# 색상
G=&#39;\033[0;32m&#39;; Y=&#39;\033[0;33m&#39;; R=&#39;\033[0;31m&#39;; B=&#39;\033[0;34m&#39;; NC=&#39;\033[0m&#39;
banner() { echo &quot;&quot;; echo -e &quot;${B}═══════════════════════════════════════════════════${NC}&quot;; echo -e &quot;${B}  $1${NC}&quot;; echo -e &quot;${B}═══════════════════════════════════════════════════${NC}&quot;; }
ok()    { echo -e &quot;${G}  ✓${NC} $1&quot;; }
warn()  { echo -e &quot;${Y}  ⚠${NC} $1&quot;; }
abort() { echo -e &quot;${R}❌ $1${NC}&quot;; exit 1; }

banner &quot;Flask 앱 → Azure SQL Database 전환&quot;

# 검증
[ -z &quot;$SQL_FQDN&quot; ] &amp;&amp; abort &quot;SQL_FQDN 환경변수 없음. source ~/.carmarket-env&quot;
[ -z &quot;$PUBIP&quot; ] &amp;&amp; abort &quot;PUBIP 환경변수 없음. source ~/.carmarket-env&quot;
[ -z &quot;$SQL_ADMIN_PASSWORD&quot; ] &amp;&amp; { read -s -p &quot;Azure SQL Admin 비밀번호: &quot; SQL_ADMIN_PASSWORD; echo &quot;&quot;; }

echo &quot;&quot;
echo &quot;  현재:  localhost (VM SQL Server)&quot;
echo &quot;  전환:  $SQL_FQDN (Azure SQL Database)&quot;
echo &quot;&quot;
read -p &quot;  Flask 앱 연결 대상을 Azure SQL로 전환? (y/N): &quot; ok_proceed
[[ &quot;$ok_proceed&quot; =~ ^[Yy]$ ]] || abort &quot;취소됨&quot;

# =============================================================
# VM에서 .env 수정 + 재시작
# =============================================================
echo &quot;&quot;
echo &quot;  → .env 백업 + 수정...&quot;

ssh &quot;$USER_NAME@$PUBIP&quot; bash -s &quot;$SQL_FQDN&quot; &quot;$SQL_DB_NAME&quot; &quot;$SQL_ADMIN&quot; &quot;$SQL_ADMIN_PASSWORD&quot; &lt;&lt; &#39;SWITCH&#39;
#!/usr/bin/env bash
set -euo pipefail
SQL_FQDN=&quot;$1&quot;; SQL_DB_NAME=&quot;$2&quot;; SQL_ADMIN=&quot;$3&quot;; SQL_ADMIN_PASSWORD=&quot;$4&quot;
APP_DIR=&quot;$HOME/sqlvm_usedcar/app&quot;

G=&#39;\033[0;32m&#39;; NC=&#39;\033[0m&#39;
ok() { echo -e &quot;${G}  ✓${NC} $1&quot;; }

# 백업
cp &quot;$APP_DIR/.env&quot; &quot;$APP_DIR/.env.vm-backup&quot;
ok &quot;기존 .env 백업 → .env.vm-backup&quot;

# Azure SQL 용 .env 생성
cat &gt; &quot;$APP_DIR/.env&quot; &lt;&lt;EOF
# Azure SQL Database 연결
SA_PASSWORD=$SQL_ADMIN_PASSWORD
DB_SERVER=$SQL_FQDN
DB_NAME=$SQL_DB_NAME
DB_USER=$SQL_ADMIN
FLASK_PORT=5000
EOF
chmod 600 &quot;$APP_DIR/.env&quot;
ok &quot;.env 업데이트 → $SQL_FQDN&quot;

# app.py에서 DB_USER 환경변수 지원하도록 패치 (필요시)
if ! grep -q &#39;DB_USER&#39; &quot;$APP_DIR/app.py&quot;; then
  # DB_USER = &quot;sa&quot; → 환경변수에서 읽도록 변경
  sed -i &#39;s/DB_USER = &quot;sa&quot;/DB_USER = os.environ.get(&quot;DB_USER&quot;, &quot;sa&quot;)/&#39; &quot;$APP_DIR/app.py&quot;
  ok &quot;app.py: DB_USER 환경변수 지원 패치&quot;
fi

# 재시작
sudo systemctl restart carmarket
sleep 3

if sudo systemctl is-active --quiet carmarket; then
  ok &quot;carmarket.service 재시작 완료&quot;
else
  echo &quot;❌ 서비스 재시작 실패&quot;
  sudo journalctl -u carmarket -n 20 --no-pager
  exit 1
fi

# 헬스체크
HEALTH=$(curl -s --max-time 10 http://localhost:5000/health || echo &#39;{}&#39;)
if echo &quot;$HEALTH&quot; | grep -q &#39;&quot;status&quot;:&quot;ok&quot;&#39;; then
  ok &quot;Health OK (Azure SQL 연결): $HEALTH&quot;
else
  echo &quot;⚠ Health 실패: $HEALTH&quot;
  echo &quot;  → VM SQL Server로 롤백하려면:&quot;
  echo &quot;     cp $APP_DIR/.env.vm-backup $APP_DIR/.env&quot;
  echo &quot;     sudo systemctl restart carmarket&quot;
fi
SWITCH

# 외부 검증
echo &quot;&quot;
echo &quot;  → 외부 헬스체크...&quot;
sleep 2
HEALTH=$(curl -s --max-time 10 &quot;http://$PUBIP:5000/health&quot; || echo &#39;{}&#39;)
if echo &quot;$HEALTH&quot; | grep -q &#39;&quot;status&quot;:&quot;ok&quot;&#39;; then
  ok &quot;외부에서 Azure SQL 통해 앱 정상 작동 확인&quot;
else
  warn &quot;외부 헬스체크 실패: $HEALTH&quot;
fi

# API 검증
echo &quot;  → API 테스트 (차량 목록)...&quot;
CARS=$(curl -s --max-time 10 &quot;http://$PUBIP:5000/api/cars&quot; || echo &#39;[]&#39;)
CAR_COUNT=$(echo &quot;$CARS&quot; | python3 -c &quot;import sys,json; print(len(json.load(sys.stdin)))&quot; 2&gt;/dev/null || echo &quot;0&quot;)
ok &quot;API 응답: Cars ${CAR_COUNT}건&quot;

banner &quot;전환 완료!&quot;

cat &lt;&lt;EOF

┌─────────────────────────────────────────────────────────────┐
│  Flask 앱이 Azure SQL Database를 사용 중입니다               │
│                                                             │
│  웹 앱:  http://$PUBIP:5000/                                │
│  DB:     $SQL_FQDN / $SQL_DB_NAME                           │
│                                                             │
│  롤백 방법 (VM SQL Server로 복귀):                           │
│    ssh $USER_NAME@$PUBIP                                    │
│    cp ~/sqlvm_usedcar/app/.env.vm-backup ~/sqlvm_usedcar/app/.env │
│    sudo systemctl restart carmarket                         │
└─────────────────────────────────────────────────────────────┘

EOF
</code></pre>
<ol>
<li>기존 .env 백업 ( .env.vm-backup )</li>
<li>DB 연결 대상을 Azure SQL Database로 변경</li>
<li>app.py 에 DB_USER 환경변수 지원 패치</li>
<li>carmarket 서비스 재시작</li>
<li>헬스체크 + API 테스트</li>
</ol>
<p>이후 ssh로 vm 접속</p>
<pre><code>cd ~/sqlvm_usedcar/app
# 1. 현재 DB_USER 확인
grep &#39;DB_USER&#39; app.py
# 2. app.py 수정 (DB_USER를 환경변수에서 읽도록)
sed -i &#39;s/DB_USER = &quot;sa&quot;/DB_USER = os.environ.get(&quot;DB_USER&quot;, &quot;sa&quot;)/&#39; app.py
# 3. .env에 DB_USER가 있는지 확인
cat .env
# 4. DB_USER가 없으면 추가
grep -q &#39;DB_USER&#39; .env || echo &#39;DB_USER=sqladmin&#39; &gt;&gt; .env
# 5. 서비스 재시작
sudo systemctl restart carmarket
# 6. 확인
curl -s http://localhost:5000/health</code></pre><h3 id="전환-후-검증">전환 후 검증</h3>
<pre><code># 헬스체크 — db: connected 확인
curl http://$PUBIP:5000/health
# 차량 목록 — Azure SQL에서 조회
curl http://$PUBIP:5000/api/cars
# 매물 등록 테스트 — Azure SQL에 INSERT
curl -X POST http://$PUBIP:5000/api/cars \
-H &quot;Content-Type: application/json&quot; \
-d &#39;{&quot;seller_id&quot;:1,&quot;brand&quot;:&quot;기아&quot;,&quot;model&quot;:&quot;카니발&quot;,&quot;year&quot;:2023,&quot;price&quot;:38000000,&quot;mileage&quot;:10000}&#39;</code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/830ba0fb-224b-453b-9918-4e21d57e3c19/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/7acafcab-7e58-407b-9329-a38d22ac23ba/image.png" alt=""></p>
<h2 id="롤백-vm-sql-server로-복귀">롤백 (VM SQL Server로 복귀)</h2>
<pre><code>ssh azureuser@$PUBIP
cp ~/sqlvm_usedcar/app/.env.vm-backup ~/sqlvm_usedcar/app/.env
sudo systemctl restart carmarket</code></pre><h2 id="리소스-정리">리소스 정리</h2>
<pre><code>source ~/.carmarket-env
# 방법 1: VM만 중지 (데이터 유지, 비용 중단)
bash scripts/99-cleanup.sh
# 방법 2: 전체 삭제 (되돌릴 수 없음)
bash scripts/99-cleanup.sh --delete</code></pre><hr>

<h1 id="sql-server-on-azure-virtual-machines">SQL Server on Azure Virtual Machines</h1>
<p><img src="https://velog.velcdn.com/images/rudin_/post/cf9c4921-3576-4bf9-a083-50fee5993f36/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/b1b352b3-c936-47d0-9d45-832f62239912/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/ef486cf5-a3b2-44d0-bba5-8b49e0d19aeb/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/7ec1b03c-4dc8-43dc-9006-7596bd46bdb0/image.png" alt=""></p>
<h2 id="cidr">CIDR</h2>
<p>사이더
CIDR(Classless Inter-Domain Routing, 클래스 없는 도메인 간 라우팅)은 1993년 도입된 IP 주소 할당 및 라우팅 효율화 방식입니다. 고정된 클래스 기반 체계(A, B, C)를 대체하여 IP 주소 낭비를 줄이고, 접두어(Prefix)를 사용하여 유연하게 네트워크 영역을 나누어 라우팅 테이블 크기를 줄인다.</p>
<h1 id="sql-server-인덱스-조각화-문제-감지-및-수정-실습">SQL Server 인덱스 조각화 문제 감지 및 수정 실습</h1>
<h2 id="실습-개요">실습 개요</h2>
<p>이 실습은 SQL Server에서 <strong>인덱스 조각화(Index Fragmentation)</strong> 문제를 감지하고, 조각난 인덱스를 다시 작성하여 쿼리 성능 변화를 확인하는 과정이다.</p>
<p>AdventureWorks 데이터베이스를 복원한 뒤, <code>Person.Address</code> 테이블에 데이터를 추가하여 인덱스 조각화를 인위적으로 발생시킨다. 이후 DMV를 사용해 조각화 수준을 확인하고, <code>ALTER INDEX ... REBUILD</code>로 인덱스를 다시 작성한다. 마지막으로 <code>SET STATISTICS IO, TIME ON</code>을 사용해 논리적 읽기 수가 줄어드는지 비교한다.</p>
<hr>
<h2 id="실습-배경">실습 배경</h2>
<p>AdventureWorks는 10년 넘게 자전거와 자전거 부품을 소비자와 유통업체에 직접 판매해 온 회사이다. 최근 고객 요청을 처리하는 데 사용되는 제품의 성능 저하가 발견되었다.</p>
<p>데이터베이스 관리자는 SQL 도구를 사용하여 성능 문제를 식별하고, 발견된 문제를 해결할 수 있는 실행 가능한 솔루션을 제공해야 한다.</p>
<p>이 실습에서는 다음을 수행한다.</p>
<ul>
<li>AdventureWorks2017 데이터베이스 복원</li>
<li>인덱스 조각화 상태 확인</li>
<li>대량 데이터 삽입으로 조각화 유발</li>
<li>조각화된 인덱스 확인</li>
<li>논리적 읽기 수 측정</li>
<li>인덱스 다시 작성</li>
<li>조각화 감소 및 논리적 읽기 감소 확인</li>
</ul>
<hr>
<h2 id="참고-ssms에서-라인-번호-표시하기">참고: SSMS에서 라인 번호 표시하기</h2>
<p>T-SQL 코드를 복사하여 실행할 때 디버깅을 쉽게 하기 위해 SSMS 편집기에 라인 번호를 표시할 수 있다.</p>
<p>설정 경로:</p>
<pre><code class="language-text">Tools → Options → Text Editor → Transact-SQL → General → Line numbers 체크</code></pre>
<hr>
<h1 id="데이터베이스-복원">데이터베이스 복원</h1>
<h2 id="1-adventureworks2017-백업-파일-다운로드">1. AdventureWorks2017 백업 파일 다운로드</h2>
<p>랩 가상 머신에서 아래 경로의 데이터베이스 백업 파일을 다운로드한다.</p>
<pre><code class="language-text">https://github.com/MicrosoftLearning/dp-300-database-administrator/blob/master/Instructions/Templates/AdventureWorks2017.bak</code></pre>
<p>다운로드한 파일은 아래 폴더에 저장한다.</p>
<pre><code class="language-text">C:\LabFiles\Monitor and optimize</code></pre>
<p>해당 폴더가 없다면 직접 생성한다.</p>
<hr>
<h2 id="2-ssms-실행">2. SSMS 실행</h2>
<p>Windows 시작 버튼을 선택하고 <code>SSMS</code>를 입력한다.</p>
<p>목록에서 <strong>Microsoft SQL Server Management Studio 18</strong>을 선택한다.</p>
<hr>
<h2 id="3-sql-server-연결">3. SQL Server 연결</h2>
<p>SSMS가 열리면 <strong>Connect to Server</strong> 대화 상자가 표시된다.</p>
<p>기본 인스턴스 이름이 미리 채워져 있으면 그대로 <strong>Connect</strong>를 선택한다.</p>
<p>서버가 보이지 않는 경우에는 다음을 선택해 서버를 찾을 수 있다.</p>
<pre><code class="language-text">&lt;Browse for more&gt;</code></pre>
<hr>
<h2 id="4-new-query-선택">4. New Query 선택</h2>
<p>Object Explorer에서 <strong>Databases</strong> 폴더를 선택한 뒤, 상단의 <strong>New Query</strong>를 선택한다.</p>
<hr>
<h2 id="5-데이터베이스-복원-쿼리-실행">5. 데이터베이스 복원 쿼리 실행</h2>
<p>New Query 창에 아래 T-SQL을 복사하여 붙여넣고 실행한다.</p>
<pre><code class="language-sql">RESTORE DATABASE AdventureWorks2017
FROM DISK = &#39;C:\LabFiles\Monitor and optimize\AdventureWorks2017.bak&#39;
WITH RECOVERY,
 MOVE &#39;AdventureWorks2017&#39;
 TO &#39;C:\LabFiles\Monitor and optimize\AdventureWorks2017.mdf&#39;,
 MOVE &#39;AdventureWorks2017_log&#39;
 TO &#39;C:\LabFiles\Monitor and optimize\AdventureWorks2017_log.ldf&#39;;</code></pre>
<blockquote>
<p>백업 파일 이름과 경로는 실제 다운로드한 파일 위치와 일치해야 한다. 경로가 다르면 복원 명령이 실패한다.</p>
</blockquote>
<hr>
<h2 id="6-복원-성공-확인">6. 복원 성공 확인</h2>
<p>복원이 완료되면 메시지 창에 성공 메시지가 표시된다.</p>
<p>예시:</p>
<pre><code class="language-text">RESTORE DATABASE successfully processed ... pages ...</code></pre>
<hr>
<h1 id="인덱스-조각화-조사">인덱스 조각화 조사</h1>
<h2 id="1-현재-조각화-상태-확인">1. 현재 조각화 상태 확인</h2>
<p>New Query를 선택한 뒤 아래 T-SQL 코드를 실행한다.</p>
<pre><code class="language-sql">USE AdventureWorks2017
GO
SELECT i.name Index_Name
, avg_fragmentation_in_percent
, db_name(database_id)
, i.object_id
, i.index_id
, index_type_desc
FROM
sys.dm_db_index_physical_stats(db_id(&#39;AdventureWorks2017&#39;),object_id(&#39;person.address&#39;),NULL,NULL,&#39;DETAILED&#39;) ps
INNER JOIN sys.indexes i ON ps.object_id = i.object_id 
AND ps.index_id = i.index_id
WHERE avg_fragmentation_in_percent &gt; 50
-- find indexes where fragmentation is greater than 50%</code></pre>
<p>이 쿼리는 <code>Person.Address</code> 테이블에서 조각화가 50%를 초과하는 인덱스를 조회한다.</p>
<p>처음 실행하면 반환되는 결과가 없다. 즉, 현재는 50%를 초과하는 조각화된 인덱스가 없는 상태이다.</p>
<hr>
<h2 id="2-데이터-삽입으로-조각화-유발">2. 데이터 삽입으로 조각화 유발</h2>
<p>다음 T-SQL을 실행하여 <code>Person.Address</code> 테이블에 많은 수의 새 레코드를 삽입한다.</p>
<pre><code class="language-sql">USE AdventureWorks2017
GO

INSERT INTO [Person].[Address]
 ([AddressLine1]
 ,[AddressLine2]
 ,[City]
 ,[StateProvinceID]
 ,[PostalCode]
 ,[SpatialLocation]
 ,[rowguid]
 ,[ModifiedDate])

SELECT AddressLine1,
 AddressLine2, 
 &#39;Amsterdam&#39;,
 StateProvinceID, 
 PostalCode, 
 SpatialLocation, 
 newid(), 
 getdate()
FROM Person.Address;
GO</code></pre>
<p>이 쿼리는 기존 <code>Person.Address</code> 데이터를 다시 읽어 같은 테이블에 추가 삽입한다.</p>
<p>특히 <code>City</code> 값을 <code>&#39;Amsterdam&#39;</code>으로 고정하여 삽입한다. 결과적으로 행 개수가 약 2배로 늘어나고, <code>Person.Address</code> 테이블과 관련 인덱스의 조각화 수준이 증가한다.</p>
<hr>
<h2 id="3-조각화-상태-다시-확인">3. 조각화 상태 다시 확인</h2>
<p>처음 실행했던 조각화 확인 쿼리를 다시 실행한다.</p>
<pre><code class="language-sql">USE AdventureWorks2017
GO
SELECT i.name Index_Name
, avg_fragmentation_in_percent
, db_name(database_id)
, i.object_id
, i.index_id
, index_type_desc
FROM
sys.dm_db_index_physical_stats(db_id(&#39;AdventureWorks2017&#39;),object_id(&#39;person.address&#39;),NULL,NULL,&#39;DETAILED&#39;) ps
INNER JOIN sys.indexes i ON ps.object_id = i.object_id 
AND ps.index_id = i.index_id
WHERE avg_fragmentation_in_percent &gt; 50
-- find indexes where fragmentation is greater than 50%</code></pre>
<p>이제 고도로 조각난 인덱스 4개를 확인할 수 있다.</p>
<p>예시 결과에서는 다음과 같은 인덱스들이 50% 이상의 조각화를 보인다.</p>
<table>
<thead>
<tr>
<th>Index_Name</th>
<th align="right">avg_fragmentation_in_percent</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>AK_Address_rowguid</td>
<td align="right">약 98%</td>
<td>rowguid 관련 인덱스</td>
</tr>
<tr>
<td>IX_Address_AddressLine1_AddressLine2_City_StateProvinceID_PostalCode</td>
<td align="right">약 98%</td>
<td>주소 검색 관련 인덱스</td>
</tr>
<tr>
<td>IX_Address_AddressLine1_AddressLine2_City_StateProvinceID_PostalCode</td>
<td align="right">약 90%</td>
<td>주소 검색 관련 인덱스</td>
</tr>
<tr>
<td>IX_Address_StateProvinceID</td>
<td align="right">약 80~81%</td>
<td>StateProvinceID 관련 인덱스</td>
</tr>
</tbody></table>
<hr>
<h1 id="논리적-읽기-수-측정">논리적 읽기 수 측정</h1>
<h2 id="1-statistics-io-time-활성화-후-쿼리-실행">1. STATISTICS IO, TIME 활성화 후 쿼리 실행</h2>
<p>다음 쿼리를 실행한다.</p>
<pre><code class="language-sql">SET STATISTICS IO,TIME ON
GO

USE AdventureWorks2017
GO

SELECT DISTINCT (StateProvinceID)
 ,count(StateProvinceID) AS CustomerCount
FROM person.Address
GROUP BY StateProvinceID
ORDER BY count(StateProvinceID) DESC;
GO</code></pre>
<p>이 쿼리는 <code>Person.Address</code> 테이블에서 <code>StateProvinceID</code>별 건수를 집계하고, 건수가 많은 순서대로 정렬한다.</p>
<hr>
<h2 id="2-messages-탭에서-logical-reads-확인">2. Messages 탭에서 logical reads 확인</h2>
<p>SQL Server Management Studio의 결과 창에서 <strong>Messages</strong> 탭을 클릭한다.</p>
<p>여기에서 쿼리에 의해 수행된 논리적 읽기 수를 확인한다.</p>
<p>실습 자료 기준으로 조각화된 상태에서의 논리적 읽기 수는 다음과 같다.</p>
<pre><code class="language-text">logical reads = 94</code></pre>
<p>논리적 읽기(logical reads)는 SQL Server가 버퍼 캐시에서 읽은 데이터 페이지 수를 의미한다.</p>
<p>조각화가 심하면 쿼리가 필요한 데이터를 찾기 위해 더 많은 페이지를 읽게 되고, 이로 인해 성능 저하가 발생할 수 있다.</p>
<hr>
<h1 id="조각난-인덱스-다시-작성">조각난 인덱스 다시 작성</h1>
<h2 id="1-ix_address_stateprovinceid-인덱스-rebuild">1. IX_Address_StateProvinceID 인덱스 REBUILD</h2>
<p>다음 T-SQL을 실행하여 <code>IX_Address_StateProvinceID</code> 인덱스를 다시 작성한다.</p>
<pre><code class="language-sql">USE AdventureWorks2017
GO
ALTER INDEX [IX_Address_StateProvinceID] ON [Person].[Address] REBUILD PARTITION = ALL
WITH (PAD_INDEX = OFF, 
 STATISTICS_NORECOMPUTE = OFF, 
 SORT_IN_TEMPDB = OFF, 
 IGNORE_DUP_KEY = OFF, 
 ONLINE = OFF, 
 ALLOW_ROW_LOCKS = ON, 
 ALLOW_PAGE_LOCKS = ON)</code></pre>
<p><code>ALTER INDEX ... REBUILD</code>는 인덱스를 새로 다시 만드는 작업이다.</p>
<p>이를 통해 인덱스 페이지가 정리되고, 논리적 순서와 물리적 순서가 더 잘 맞춰지며, 페이지 내부의 빈 공간도 정리된다.</p>
<hr>
<h2 id="2-인덱스-조각화-감소-확인">2. 인덱스 조각화 감소 확인</h2>
<p>아래 쿼리를 실행하여 <code>IX_Address_StateProvinceID</code> 인덱스의 조각화가 더 이상 50%를 초과하지 않는지 확인한다.</p>
<pre><code class="language-sql">USE AdventureWorks2017
GO

SELECT DISTINCT i.name Index_Name
 , avg_fragmentation_in_percent
 , db_name(database_id)
 , i.object_id
 , i.index_id
 , index_type_desc
FROM
sys.dm_db_index_physical_stats(db_id(&#39;AdventureWorks2017&#39;),object_id(&#39;person.address&#39;),NULL,NULL,&#39;DETAILED&#39;) ps
 INNER JOIN sys.indexes i ON (ps.object_id = i.object_id AND ps.index_id = i.index_id)
WHERE i.name = &#39;IX_Address_StateProvinceID&#39;</code></pre>
<p>결과를 비교하면 <code>IX_Address_StateProvinceID</code> 인덱스의 조각화가 약 81%에서 0%로 감소한 것을 확인할 수 있다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th align="right">REBUILD 전</th>
<th align="right">REBUILD 후</th>
</tr>
</thead>
<tbody><tr>
<td>IX_Address_StateProvinceID 조각화율</td>
<td align="right">약 81%</td>
<td align="right">0%</td>
</tr>
</tbody></table>
<hr>
<h1 id="인덱스-재작성-후-논리적-읽기-비교">인덱스 재작성 후 논리적 읽기 비교</h1>
<h2 id="1-동일한-select-쿼리-재실행">1. 동일한 SELECT 쿼리 재실행</h2>
<p>이전 섹션에서 실행했던 집계 쿼리를 다시 실행한다.</p>
<pre><code class="language-sql">SET STATISTICS IO,TIME ON
GO

USE AdventureWorks2017
GO

SELECT DISTINCT (StateProvinceID)
 ,count(StateProvinceID) AS CustomerCount
FROM person.Address
GROUP BY StateProvinceID
ORDER BY count(StateProvinceID) DESC;

GO</code></pre>
<hr>
<h2 id="2-messages-탭에서-logical-reads-재확인">2. Messages 탭에서 logical reads 재확인</h2>
<p>인덱스를 다시 작성했기 때문에 이전보다 효율적으로 데이터를 읽을 수 있다.</p>
<p>실습 자료 기준으로 인덱스 재작성 후 논리적 읽기는 다음과 같이 감소한다.</p>
<pre><code class="language-text">logical reads = 70</code></pre>
<table>
<thead>
<tr>
<th>상태</th>
<th align="right">logical reads</th>
</tr>
</thead>
<tbody><tr>
<td>인덱스 REBUILD 전</td>
<td align="right">94</td>
</tr>
<tr>
<td>인덱스 REBUILD 후</td>
<td align="right">70</td>
</tr>
</tbody></table>
<p>즉, 인덱스 유지 관리가 쿼리 성능에 영향을 줄 수 있다는 것을 확인할 수 있다.</p>
<hr>
<h1 id="실습-결과-정리">실습 결과 정리</h1>
<p>이번 실습에서는 인덱스를 다시 작성하고 논리적 읽기를 분석하여 쿼리 성능을 높이는 방법을 확인했다.</p>
<hr>
<h2 id="1-인덱스-조각화의-이해-및-영향-확인">1. 인덱스 조각화의 이해 및 영향 확인</h2>
<p>인덱스 조각화(Index Fragmentation)는 데이터베이스에서 데이터가 삽입, 업데이트, 삭제되는 과정에서 발생한다.</p>
<p>인덱스의 논리적 순서와 실제 디스크상의 물리적 순서가 달라지거나, 데이터 페이지 내부에 빈 공간이 생기는 현상을 말한다.</p>
<p>이 실습에서는 대량의 데이터를 삽입하여 인덱스 조각화를 인위적으로 발생시켰다. 이를 통해 조각화가 실제로 어떻게 발생하는지 간접적으로 경험할 수 있었다.</p>
<p>가장 중요한 점은 조각화가 심해지면 SQL Server가 데이터를 읽을 때 더 많은 페이지를 읽어야 한다는 것이다.</p>
<p>그래서 이 실습에서는 <code>SET STATISTICS IO ON</code>을 사용해 논리적 읽기(Logical Reads) 횟수를 측정했다. 조각화된 인덱스를 사용하는 쿼리는 불필요하게 많은 페이지를 읽게 되어 성능 저하를 유발할 수 있다.</p>
<hr>
<h2 id="2-조각화-진단-방법-학습">2. 조각화 진단 방법 학습</h2>
<p>조각화 상태는 <code>sys.dm_db_index_physical_stats</code> 동적 관리 뷰(DMV)를 사용해 확인한다.</p>
<p>이 DMV를 통해 특정 테이블이나 특정 인덱스의 조각화 수준을 퍼센트로 확인할 수 있다.</p>
<p>DBA가 시스템 상태를 진단할 때 사용하는 핵심 도구 중 하나이다.</p>
<hr>
<h2 id="3-조각화-해결-방법-학습-및-효과-검증">3. 조각화 해결 방법 학습 및 효과 검증</h2>
<p>심하게 조각화된 인덱스는 <code>ALTER INDEX REBUILD</code> 명령어를 사용하여 다시 작성할 수 있다.</p>
<p>인덱스 재구축은 인덱스 페이지를 새로 만들고, 물리적 순서를 논리적 순서에 가깝게 정리하며, 페이지 내부의 빈 공간을 제거한다.</p>
<p>참고로 <code>ALTER INDEX REORGANIZE</code>는 온라인으로 조각화를 일부 정리하는 다른 방법이다.</p>
<p>실습에서는 인덱스를 재구축한 후 동일한 쿼리를 다시 실행했다. 그 결과 논리적 읽기 수가 감소하는 것을 확인했다.</p>
<p>이는 조각화 해결이 실제 쿼리 성능 향상으로 이어질 수 있음을 보여준다.</p>
<hr>
<h2 id="4-데이터베이스-유지-관리의-중요성">4. 데이터베이스 유지 관리의 중요성</h2>
<p>인덱스 조각화는 시간이 지나면서 자연스럽게 발생한다.</p>
<p>따라서 데이터베이스 성능을 최적으로 유지하려면 정기적인 인덱스 유지 관리가 필요하다.</p>
<p>DBA는 주기적으로 조각화 수준을 모니터링하고, 필요에 따라 인덱스를 재구성하거나 다시 작성해야 한다.</p>
<hr>
<h1 id="핵심-요약">핵심 요약</h1>
<table>
<thead>
<tr>
<th>구분</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>문제</td>
<td>인덱스 조각화로 인해 불필요한 페이지 읽기 증가</td>
</tr>
<tr>
<td>진단 도구</td>
<td><code>sys.dm_db_index_physical_stats</code></td>
</tr>
<tr>
<td>측정 지표</td>
<td><code>avg_fragmentation_in_percent</code>, <code>logical reads</code></td>
</tr>
<tr>
<td>조각화 유발</td>
<td><code>Person.Address</code>에 대량 INSERT</td>
</tr>
<tr>
<td>해결 방법</td>
<td><code>ALTER INDEX ... REBUILD</code></td>
</tr>
<tr>
<td>효과</td>
<td>조각화율 약 81% → 0%, logical reads 94 → 70</td>
</tr>
<tr>
<td>의미</td>
<td>인덱스 유지 관리가 쿼리 성능에 영향을 미침</td>
</tr>
</tbody></table>
<hr>
<h1 id="사용한-주요-t-sql-모음">사용한 주요 T-SQL 모음</h1>
<h2 id="데이터베이스-복원-1">데이터베이스 복원</h2>
<pre><code class="language-sql">RESTORE DATABASE AdventureWorks2017
FROM DISK = &#39;C:\LabFiles\Monitor and optimize\AdventureWorks2017.bak&#39;
WITH RECOVERY,
 MOVE &#39;AdventureWorks2017&#39;
 TO &#39;C:\LabFiles\Monitor and optimize\AdventureWorks2017.mdf&#39;,
 MOVE &#39;AdventureWorks2017_log&#39;
 TO &#39;C:\LabFiles\Monitor and optimize\AdventureWorks2017_log.ldf&#39;;</code></pre>
<h2 id="조각화-확인">조각화 확인</h2>
<pre><code class="language-sql">USE AdventureWorks2017
GO
SELECT i.name Index_Name
, avg_fragmentation_in_percent
, db_name(database_id)
, i.object_id
, i.index_id
, index_type_desc
FROM
sys.dm_db_index_physical_stats(db_id(&#39;AdventureWorks2017&#39;),object_id(&#39;person.address&#39;),NULL,NULL,&#39;DETAILED&#39;) ps
INNER JOIN sys.indexes i ON ps.object_id = i.object_id 
AND ps.index_id = i.index_id
WHERE avg_fragmentation_in_percent &gt; 50
-- find indexes where fragmentation is greater than 50%</code></pre>
<h2 id="데이터-삽입으로-조각화-유발">데이터 삽입으로 조각화 유발</h2>
<pre><code class="language-sql">USE AdventureWorks2017
GO

INSERT INTO [Person].[Address]
 ([AddressLine1]
 ,[AddressLine2]
 ,[City]
 ,[StateProvinceID]
 ,[PostalCode]
 ,[SpatialLocation]
 ,[rowguid]
 ,[ModifiedDate])

SELECT AddressLine1,
 AddressLine2, 
 &#39;Amsterdam&#39;,
 StateProvinceID, 
 PostalCode, 
 SpatialLocation, 
 newid(), 
 getdate()
FROM Person.Address;
GO</code></pre>
<h2 id="논리적-읽기-측정">논리적 읽기 측정</h2>
<pre><code class="language-sql">SET STATISTICS IO,TIME ON
GO

USE AdventureWorks2017
GO

SELECT DISTINCT (StateProvinceID)
 ,count(StateProvinceID) AS CustomerCount
FROM person.Address
GROUP BY StateProvinceID
ORDER BY count(StateProvinceID) DESC;
GO</code></pre>
<h2 id="인덱스-다시-작성">인덱스 다시 작성</h2>
<pre><code class="language-sql">USE AdventureWorks2017
GO
ALTER INDEX [IX_Address_StateProvinceID] ON [Person].[Address] REBUILD PARTITION = ALL
WITH (PAD_INDEX = OFF, 
 STATISTICS_NORECOMPUTE = OFF, 
 SORT_IN_TEMPDB = OFF, 
 IGNORE_DUP_KEY = OFF, 
 ONLINE = OFF, 
 ALLOW_ROW_LOCKS = ON, 
 ALLOW_PAGE_LOCKS = ON)</code></pre>
<h2 id="특정-인덱스-조각화-확인">특정 인덱스 조각화 확인</h2>
<pre><code class="language-sql">USE AdventureWorks2017
GO

SELECT DISTINCT i.name Index_Name
 , avg_fragmentation_in_percent
 , db_name(database_id)
 , i.object_id
 , i.index_id
 , index_type_desc
FROM
sys.dm_db_index_physical_stats(db_id(&#39;AdventureWorks2017&#39;),object_id(&#39;person.address&#39;),NULL,NULL,&#39;DETAILED&#39;) ps
 INNER JOIN sys.indexes i ON (ps.object_id = i.object_id AND ps.index_id = i.index_id)
WHERE i.name = &#39;IX_Address_StateProvinceID&#39;</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MicrosoftDataSchool] 80일차 - Azure VM, SQL Server 실습]]></title>
            <link>https://velog.io/@rudin_/MicrosoftDataSchool-80%EC%9D%BC%EC%B0%A8-Azure-VM-SQL-Server-%EC%8B%A4%EC%8A%B5</link>
            <guid>https://velog.io/@rudin_/MicrosoftDataSchool-80%EC%9D%BC%EC%B0%A8-Azure-VM-SQL-Server-%EC%8B%A4%EC%8A%B5</guid>
            <pubDate>Thu, 30 Apr 2026 03:42:02 GMT</pubDate>
            <description><![CDATA[<h1 id="중고차-커뮤니티-mvp-실습">중고차 커뮤니티 MVP 실습</h1>
<h1 id="vm-생성">VM 생성</h1>
<p><img src="https://velog.velcdn.com/images/rudin_/post/ed41a451-6a54-4783-88f3-263a7bd49b86/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/749f92bc-2e02-4310-9543-64da13747f92/image.png" alt=""></p>
<h2 id="디스크">디스크</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/ca1eb900-4d0c-4d2e-98ff-e30d3f91dcdb/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/b0d56c9d-f7bd-4c47-bf76-cdc3cf5023e3/image.png" alt=""></p>
<p>여기서는 안쓰지만, 디스크를 사용하여 애플리케이션 등 저장이 가능하다.</p>
<h2 id="자동-종료">자동 종료</h2>
<p>관리탭
<img src="https://velog.velcdn.com/images/rudin_/post/c38a00b4-55c2-4037-afdf-7e21bead1ca3/image.png" alt=""></p>
<p>이후 검토 + 만들기 하고 <code>.pem</code> 확장자의 ssh 키를 다운받는다.</p>
<h1 id="vm-연결">VM 연결</h1>
<p><img src="https://velog.velcdn.com/images/rudin_/post/1788c521-16c6-40ed-ade7-e3f38772e080/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/9c8f232b-deb2-4fdc-9915-cb2db6e32cd3/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/09dd1cb1-0e09-4c11-a9cc-69017d4fb1c9/image.png" alt="">
SSH 명령 탭의 경로 입력하기</p>
<h2 id="powershell-연결">powershell 연결</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/080512b5-ca02-409f-80dc-6a5c6f511481/image.png" alt=""></p>
<pre><code>ssh -i &quot;키경로&quot; azureuser@ip주소</code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/502c2974-6535-47e3-8b4b-2707e433cb16/image.png" alt=""></p>
<h2 id="os-정보-확인">OS 정보 확인</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/483c2695-79e3-46f1-9f27-974641088a3b/image.png" alt=""></p>
<h2 id="패키지-업데이트">패키지 업데이트</h2>
<pre><code>sudo apt update
sudo apt upgrade -y
sudo apt install -y curl wget gnupg2 software-properties-common apt-transport-https ca-certificates</code></pre><h2 id="swap-파일-생성sql-server-안정성">Swap 파일 생성(SQL Server 안정성)</h2>
<p>B2s의 4GB RAM은 SQL Server + Python + OS 동시 구동에 빠듯합니다. 2GB swap을 추가하여 OOM Killer 발동을 예방합니다.</p>
<pre><code>sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo &#39;/swapfile none swap sw 0 0&#39; | sudo tee -a /etc/fstab
free -h</code></pre><p>free -h 출력의 Swap 행에 2.0Gi 표시되면 OK.
<img src="https://velog.velcdn.com/images/rudin_/post/f244ac78-ce6e-463e-b368-56f63b76d414/image.png" alt="">
Swap(스왑) 파일은 물리적 메모리(RAM)가 부족할 때 디스크(HDD/SSD)의 일부를 메모리처럼 사용하는 가상 메모리 공간입니다.RAM이 가득 찼을 때 스왑 공간이 없으면 리눅스 커널은 OOM(Out of Memory) Killer를 동작시켜 중요 프로세스를 강제로 종료합니다. 스왑 파일은 이러한 갑작스러운 시스템 멈춤이나 응용 프로그램 종료를 막아줍니다. </p>
<h2 id="시간대-설정">시간대 설정</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/3ad0a020-5226-43ab-833c-a247018e3cab/image.png" alt=""></p>
<h1 id="sql-server2025-설치">SQL Server2025 설치</h1>
<h2 id="microsoft-공식-저장소-등록">Microsoft 공식 저장소 등록</h2>
<pre><code># Microsoft GPG 키 등록 (Ubuntu 24.04 권장 방식)
curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | \
  sudo gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg

# SQL Server 2025 저장소 등록 (Ubuntu 24.04 공식)
curl -fsSL https://packages.microsoft.com/config/ubuntu/24.04/mssql-server-2025.list | \
  sudo tee /etc/apt/sources.list.d/mssql-server-2025.list

sudo apt update
</code></pre><h2 id="sql-server-패키지-설치">SQL Server 패키지 설치</h2>
<pre><code>sudo apt install -y mssql-server</code></pre><h2 id="mssql-conf-setup--edition-·-비밀번호-설정">mssql-conf setup — Edition · 비밀번호 설정</h2>
<pre><code>sudo /opt/mssql/bin/mssql-conf setup</code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/fdb57430-8ff7-47fd-98e2-962872d81f36/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/fe4bd180-676b-4b6d-92fd-7af71b8c6a84/image.png" alt=""></p>
<h2 id="서비스-상태-확인">서비스 상태 확인</h2>
<pre><code>systemctl status mssql-server --no-pager</code></pre><p>Active: active (running) 표시되면 OK.
LISTEN 0.0.0.0:1433 으로 바인딩 — 다음 단계에서 localhost-only로 변경합니다.
<img src="https://velog.velcdn.com/images/rudin_/post/56a87108-c686-44ae-8e13-9347a21e4cc6/image.png" alt=""></p>
<h2 id="sql-server를-localhost-전용으로-바인딩-보안-핵심">SQL Server를 localhost 전용으로 바인딩 (보안 핵심)</h2>
<p>SQL Server를 외부에 노출하면 Brute force 공격의 1순위 대상이 됩니다. NSG뿐 아니라 SQL Server 자체에서도 localhost만 바인딩하도록 이중 차단합니다.</p>
<pre><code># /var/opt/mssql/mssql.conf 에 IP 바인딩 설정 추가
sudo /opt/mssql/bin/mssql-conf set network.ipaddress 127.0.0.1
sudo systemctl restart mssql-server
sudo ss -tlnp | grep 1433</code></pre><p>ss 출력이 127.0.0.1:1433 으로만 표시되면 OK. 0.0.0.0:1433 또는 *:1433 이면 실패.</p>
<p><img src="https://velog.velcdn.com/images/rudin_/post/9e32016a-6d22-4d27-af1f-3bc92ece5adc/image.png" alt=""></p>
<h3 id="설정-제거-방법">설정 제거 방법</h3>
<pre><code>sudo /opt/mssql/bin/mssql-conf unset network.ipaddress
sudo systemctl restart mssql-server</code></pre><p>0.0.0.0:1433 나오면 외부 접속 가능 상태
<img src="https://velog.velcdn.com/images/rudin_/post/d73fcc13-404a-4909-84ff-02cbde83f082/image.png" alt=""></p>
<h1 id="mssql-tools-odbc-driver-python">mssql-tools, ODBC Driver, Python</h1>
<pre><code># prod.list 저장소 등록 (Ubuntu 24.04용)
curl -fsSL https://packages.microsoft.com/config/ubuntu/24.04/prod.list | \
  sudo tee /etc/apt/sources.list.d/mssql-release.list

sudo apt update
sudo ACCEPT_EULA=Y apt install -y mssql-tools18 unixodbc-dev msodbcsql18

# PATH에 sqlcmd 추가
echo &#39;export PATH=&quot;$PATH:/opt/mssql-tools18/bin&quot;&#39; &gt;&gt; ~/.bashrc
source ~/.bashrc</code></pre><h2 id="sqlcmd-접속">sqlcmd 접속</h2>
<pre><code>sqlcmd -S localhost -U sa -P &#39;&lt;SA_비밀번호&gt;&#39; -C -Q &quot;SELECT @@VERSION&quot;</code></pre><p>Microsoft SQL Server 2025 (RTM-CU...) 또는 17.x 버전 정보 출력되면 OK.
<img src="https://velog.velcdn.com/images/rudin_/post/76d44ce1-4e0e-4fcd-ab64-160690d8ceab/image.png" alt=""></p>
<ul>
<li>-C 옵션: TrustServerCertificate (자체 서명 인증서 신뢰). 실습용.</li>
<li>비밀번호에 특수문자가 있을 때는 작은따옴표 &#39; &#39; 로 감쌀 것.</li>
<li>명령 히스토리에 비밀번호가 남는 것을 피하려면 -P 생략 후 프롬프트 입력 권장.</li>
</ul>
<h2 id="python3--가상환경--필수-패키지-설치">Python3 + 가상환경 + 필수 패키지 설치</h2>
<pre><code>sudo apt install -y python3 python3-pip python3-venv

# 작업 디렉토리
mkdir -p ~/carmarket &amp;&amp; cd ~/carmarket

# 가상환경 생성
python3 -m venv venv
source venv/bin/activate

# 패키지 설치
pip install --upgrade pip
pip install flask pyodbc python-dotenv gunicorn</code></pre><p>pip list | grep -E &#39;Flask|pyodbc|python-dotenv&#39; 로 3개 패키지 출력 확인.
<img src="https://velog.velcdn.com/images/rudin_/post/5a54a2b8-3847-4a2a-9176-7948892fef87/image.png" alt=""></p>
<h3 id="pyodbc-동작-검증">pyodbc 동작 검증</h3>
<pre><code>python3 -c &quot;import pyodbc; print(pyodbc.drivers())&quot;</code></pre><p>출력에 [&#39;ODBC Driver 18 for SQL Server&#39;] 가 포함되면 OK.
<img src="https://velog.velcdn.com/images/rudin_/post/3ecab966-28a0-4151-83b3-475e3a976f94/image.png" alt=""></p>
<h1 id="데이터베이스-스키마-시드-데이터">데이터베이스, 스키마, 시드 데이터</h1>
<p>CarMarket 데이터베이스를 만들고 Users, Cars, Inquiries 3-table 모델을 구축, 시드 데이터를 적재</p>
<h2 id="스키마-sql-파일-작성">스키마 SQL 파일 작성</h2>
<pre><code>cd ~/carmarket
nano schema.sql
</code></pre><p>schema.sql</p>
<pre><code>CREATE DATABASE CarMarket;
GO
USE CarMarket;
GO

CREATE TABLE Users (
    UserId    INT IDENTITY(1,1) PRIMARY KEY,
    Name      NVARCHAR(100) NOT NULL,
    Email     NVARCHAR(200) NOT NULL UNIQUE,
    Phone     NVARCHAR(20),
    UserType  NVARCHAR(10) NOT NULL DEFAULT &#39;both&#39;
              CHECK (UserType IN (&#39;seller&#39;,&#39;buyer&#39;,&#39;both&#39;)),
    CreatedAt DATETIME2 DEFAULT SYSUTCDATETIME()
);

CREATE TABLE Cars (
    CarId       INT IDENTITY(1,1) PRIMARY KEY,
    SellerId    INT NOT NULL FOREIGN KEY REFERENCES Users(UserId),
    Brand       NVARCHAR(50) NOT NULL,
    Model       NVARCHAR(100) NOT NULL,
    Year        INT NOT NULL,
    Price       DECIMAL(12,0) NOT NULL,
    Mileage     INT NOT NULL,
    FuelType    NVARCHAR(20),
    Description NVARCHAR(MAX),
    Status      NVARCHAR(20) NOT NULL DEFAULT &#39;available&#39;
                CHECK (Status IN (&#39;available&#39;,&#39;reserved&#39;,&#39;sold&#39;)),
    CreatedAt   DATETIME2 DEFAULT SYSUTCDATETIME()
);

CREATE TABLE Inquiries (
    InquiryId INT IDENTITY(1,1) PRIMARY KEY,
    CarId     INT NOT NULL FOREIGN KEY REFERENCES Cars(CarId),
    BuyerId   INT NOT NULL FOREIGN KEY REFERENCES Users(UserId),
    Message   NVARCHAR(1000) NOT NULL,
    CreatedAt DATETIME2 DEFAULT SYSUTCDATETIME()
);

CREATE INDEX IX_Cars_Brand     ON Cars(Brand);
CREATE INDEX IX_Cars_Status    ON Cars(Status);
CREATE INDEX IX_Cars_CreatedAt ON Cars(CreatedAt DESC);
GO</code></pre><h3 id="시드-데이터-작성">시드 데이터 작성</h3>
<pre><code>nano seed.sql</code></pre><pre><code>USE CarMarket;
GO

INSERT INTO Users (Name, Email, Phone, UserType) VALUES
(N&#39;김판매&#39;, &#39;seller1@test.com&#39;, &#39;010-1111-1111&#39;, &#39;seller&#39;),
(N&#39;이판매&#39;, &#39;seller2@test.com&#39;, &#39;010-2222-2222&#39;, &#39;seller&#39;),
(N&#39;박판매&#39;, &#39;seller3@test.com&#39;, &#39;010-3333-3333&#39;, &#39;seller&#39;),
(N&#39;최구매&#39;, &#39;buyer1@test.com&#39;,  &#39;010-4444-4444&#39;, &#39;buyer&#39;),
(N&#39;정구매&#39;, &#39;buyer2@test.com&#39;,  &#39;010-5555-5555&#39;, &#39;buyer&#39;);

INSERT INTO Cars (SellerId, Brand, Model, Year, Price, Mileage, FuelType, Description) VALUES
(1, N&#39;현대&#39;,   N&#39;쏘나타 DN8&#39;,     2021, 18500000, 45000, N&#39;가솔린&#39;, N&#39;무사고, 1인 소유, 정기점검 완료&#39;),
(1, N&#39;기아&#39;,   N&#39;K5 3세대&#39;,        2020, 16000000, 62000, N&#39;가솔린&#39;, N&#39;썬루프, 어라운드뷰 옵션&#39;),
(2, N&#39;BMW&#39;,   N&#39;520d (G30)&#39;,      2019, 28000000, 78000, N&#39;디젤&#39;,   N&#39;풀옵션, 가죽시트, 무사고&#39;),
(2, N&#39;벤츠&#39;,   N&#39;E300 (W213)&#39;,     2020, 38000000, 55000, N&#39;가솔린&#39;, N&#39;AMG 패키지, 1인 소유&#39;),
(3, N&#39;제네시스&#39;, N&#39;G80 (RG3)&#39;,      2022, 45000000, 28000, N&#39;가솔린&#39;, N&#39;신차급, 출고 1년&#39;);
GO
</code></pre><h3 id="sql-파일-실행">sql 파일 실행</h3>
<pre><code>read -s -p &quot;SA Password: &quot; SA_PWD
export SA_PWD

sqlcmd -S localhost -U sa -P &quot;$SA_PWD&quot; -C -i schema.sql
sqlcmd -S localhost -U sa -P &quot;$SA_PWD&quot; -C -i seed.sql</code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/556ccf0e-eda9-4210-b164-97547dc14742/image.png" alt=""></p>
<h3 id="데이터-검증">데이터 검증</h3>
<pre><code>sqlcmd -S localhost -U sa -P &quot;$SA_PWD&quot; -C -d CarMarket -Q \
  &quot;SELECT c.Brand, c.Model, c.Year, c.Price, u.Name AS Seller
   FROM Cars c JOIN Users u ON c.SellerId = u.UserId
   ORDER BY c.Price DESC&quot;</code></pre><h1 id="flask-백엔드-구현rest-api">Flask 백엔드 구현(REST API)</h1>
<p>Python Flask로 5개의 REST 엔드포인트(/health, GET·POST /cars, POST /inquiries, GET /users)를 구현하고 SQL Server와 연결</p>
<h2 id="env파일로-비밀번호-분리">.env파일로 비밀번호 분리</h2>
<pre><code>nano .env

SA_PASSWORD=YourStrongP@ssw0rd
DB_SERVER=localhost
DB_NAME=CarMarket
FLASK_PORT=5000

# 권한 600 — 본인만 읽기·쓰기
chmod 600 .env
ls -la .env</code></pre><h2 id="apppy">app.py</h2>
<pre><code>nano app.py</code></pre><pre><code>import os
from contextlib import contextmanager
import pyodbc
from flask import Flask, request, jsonify, render_template_string
from dotenv import load_dotenv

load_dotenv()

app = Flask(__name__)

DB_SERVER   = os.environ.get(&quot;DB_SERVER&quot;, &quot;localhost&quot;)
DB_NAME     = os.environ.get(&quot;DB_NAME&quot;, &quot;CarMarket&quot;)
DB_USER     = &quot;sa&quot;
DB_PASSWORD = os.environ.get(&quot;SA_PASSWORD&quot;)
FLASK_PORT  = int(os.environ.get(&quot;FLASK_PORT&quot;, 5000))

CONN_STR = (
    &quot;DRIVER={ODBC Driver 18 for SQL Server};&quot;
    f&quot;SERVER={DB_SERVER};DATABASE={DB_NAME};&quot;
    f&quot;UID={DB_USER};PWD={DB_PASSWORD};&quot;
    &quot;TrustServerCertificate=yes;Encrypt=yes;&quot;
)

@contextmanager
def db():
    conn = pyodbc.connect(CONN_STR, autocommit=False)
    try:
        yield conn
        conn.commit()
    except Exception:
        conn.rollback()
        raise
    finally:
        conn.close()

# ====== Health check ======
@app.route(&quot;/health&quot;)
def health():
    try:
        with db() as conn:
            cur = conn.cursor()
            cur.execute(&quot;SELECT 1&quot;)
            cur.fetchone()
        return jsonify({&quot;status&quot;: &quot;ok&quot;, &quot;db&quot;: &quot;connected&quot;}), 200
    except Exception as e:
        return jsonify({&quot;status&quot;: &quot;error&quot;, &quot;db&quot;: str(e)}), 500

# ====== Users ======
@app.route(&quot;/api/users&quot;, methods=[&quot;GET&quot;])
def list_users():
    with db() as conn:
        cur = conn.cursor()
        cur.execute(&quot;SELECT UserId, Name, Email, Phone, UserType FROM Users ORDER BY UserId&quot;)
        rows = cur.fetchall()
    return jsonify([
        {&quot;id&quot;: r[0], &quot;name&quot;: r[1], &quot;email&quot;: r[2], &quot;phone&quot;: r[3], &quot;type&quot;: r[4]}
        for r in rows
    ])

# ====== Cars: 목록 + 검색 ======
@app.route(&quot;/api/cars&quot;, methods=[&quot;GET&quot;])
def list_cars():
    brand = request.args.get(&quot;brand&quot;)
    max_price = request.args.get(&quot;max_price&quot;, type=int)

    sql = &quot;&quot;&quot;
        SELECT c.CarId, u.Name, c.Brand, c.Model, c.Year, c.Price,
               c.Mileage, c.FuelType, c.Description, c.Status, c.CreatedAt
          FROM Cars c
          JOIN Users u ON c.SellerId = u.UserId
         WHERE c.Status = &#39;available&#39;
    &quot;&quot;&quot;
    params = []
    if brand:
        sql += &quot; AND c.Brand = ?&quot;
        params.append(brand)
    if max_price:
        sql += &quot; AND c.Price &lt;= ?&quot;
        params.append(max_price)
    sql += &quot; ORDER BY c.CreatedAt DESC&quot;

    with db() as conn:
        cur = conn.cursor()
        cur.execute(sql, params)
        rows = cur.fetchall()

    return jsonify([
        {
            &quot;id&quot;: r[0], &quot;seller&quot;: r[1], &quot;brand&quot;: r[2], &quot;model&quot;: r[3],
            &quot;year&quot;: r[4], &quot;price&quot;: int(r[5]), &quot;mileage&quot;: r[6],
            &quot;fuel&quot;: r[7], &quot;desc&quot;: r[8], &quot;status&quot;: r[9],
            &quot;created_at&quot;: r[10].isoformat() if r[10] else None
        } for r in rows
    ])

# ====== Cars: 등록 ======
@app.route(&quot;/api/cars&quot;, methods=[&quot;POST&quot;])
def create_car():
    data = request.get_json(silent=True) or {}
    required = [&quot;seller_id&quot;, &quot;brand&quot;, &quot;model&quot;, &quot;year&quot;, &quot;price&quot;, &quot;mileage&quot;]
    missing = [k for k in required if k not in data]
    if missing:
        return jsonify({&quot;error&quot;: f&quot;missing fields: {missing}&quot;}), 400

    with db() as conn:
        cur = conn.cursor()
        cur.execute(&quot;&quot;&quot;
            INSERT INTO Cars (SellerId, Brand, Model, Year, Price, Mileage, FuelType, Description)
            OUTPUT INSERTED.CarId
            VALUES (?, ?, ?, ?, ?, ?, ?, ?)
        &quot;&quot;&quot;, data[&quot;seller_id&quot;], data[&quot;brand&quot;], data[&quot;model&quot;], int(data[&quot;year&quot;]),
             int(data[&quot;price&quot;]), int(data[&quot;mileage&quot;]),
             data.get(&quot;fuel&quot;, &quot;&quot;), data.get(&quot;desc&quot;, &quot;&quot;))
        new_id = cur.fetchone()[0]
    return jsonify({&quot;car_id&quot;: new_id}), 201

# ====== Inquiries ======
@app.route(&quot;/api/inquiries&quot;, methods=[&quot;POST&quot;])
def create_inquiry():
    data = request.get_json(silent=True) or {}
    for k in (&quot;car_id&quot;, &quot;buyer_id&quot;, &quot;message&quot;):
        if k not in data:
            return jsonify({&quot;error&quot;: f&quot;missing {k}&quot;}), 400

    with db() as conn:
        cur = conn.cursor()
        cur.execute(&quot;&quot;&quot;
            INSERT INTO Inquiries (CarId, BuyerId, Message)
            OUTPUT INSERTED.InquiryId
            VALUES (?, ?, ?)
        &quot;&quot;&quot;, int(data[&quot;car_id&quot;]), int(data[&quot;buyer_id&quot;]), data[&quot;message&quot;])
        new_id = cur.fetchone()[0]
    return jsonify({&quot;inquiry_id&quot;: new_id}), 201

# ====== UI: 단일 페이지 (Step 8에서 추가) ======
INDEX_HTML = &quot;&quot;  # Step 8에서 채움

@app.route(&quot;/&quot;)
def index():
    return render_template_string(INDEX_HTML)

if __name__ == &quot;__main__&quot;:
    app.run(host=&quot;0.0.0.0&quot;, port=FLASK_PORT, debug=False)</code></pre><h3 id="로컬-테스트">로컬 테스트</h3>
<pre><code>source venv/bin/activate
python app.py</code></pre><pre><code># 다른 터미널 또는 같은 세션에서:
curl -s http://localhost:5000/health | python3 -m json.tool</code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/289a2e91-6422-4375-bed6-a5b35e28ca0d/image.png" alt=""></p>
<h3 id="api-동작-테스트">API 동작 테스트</h3>
<pre><code>curl -s http://localhost:5000/api/cars | python3 -m json.tool</code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/5959faa4-5047-4e9e-9875-005b692ec204/image.png" alt=""></p>
<pre><code># 브랜드 필터
curl -s &quot;http://localhost:5000/api/cars?brand=BMW&quot; | python3 -m json.tool

# 차량 등록
curl -s -X POST http://localhost:5000/api/cars \
  -H &quot;Content-Type: application/json&quot; \
  -d &#39;{&quot;seller_id&quot;:1,&quot;brand&quot;:&quot;기아&quot;,&quot;model&quot;:&quot;카니발&quot;,&quot;year&quot;:2022,&quot;price&quot;:35000000,&quot;mileage&quot;:15000,&quot;fuel&quot;:&quot;디젤&quot;,&quot;desc&quot;:&quot;하이리무진 풀옵&quot;}&#39; \
  | python3 -m json.tool

# 문의 등록
curl -s -X POST http://localhost:5000/api/inquiries \
  -H &quot;Content-Type: application/json&quot; \
  -d &#39;{&quot;car_id&quot;:1,&quot;buyer_id&quot;:4,&quot;message&quot;:&quot;실차 확인 가능한가요?&quot;}&#39; \
  | python3 -m json.tool
</code></pre><h3 id="백그라운드-프로세스-종료">백그라운드 프로세스 종료</h3>
<pre><code>ps aux | grep &quot;[p]ython app.py&quot;
kill &lt;PID&gt;</code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/5f236cff-c5ab-4bc9-9c07-106bc558697d/image.png" alt=""></p>
<h1 id="프론트엔드">프론트엔드</h1>
<p>Bootstrap 5 CDN으로 차량 목록·등록·문의 UI를 구현. 단일 HTML 문자열을 Flask render_template_string으로 제공</p>
<p>app.py의 INDEX_HTML = &quot;&quot; 라인을 아래 내용으로 교체</p>
<pre><code>INDEX_HTML = &quot;&quot;&quot;
&lt;!doctype html&gt;
&lt;html lang=&quot;ko&quot;&gt;
&lt;head&gt;
  &lt;meta charset=&quot;utf-8&quot;&gt;
  &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width,initial-scale=1&quot;&gt;
  &lt;title&gt;중고차 마켓 MVP&lt;/title&gt;
  &lt;link href=&quot;https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css&quot; rel=&quot;stylesheet&quot;&gt;
  &lt;style&gt;
    body { background:#f4f8fa; }
    .navbar { background:#21295C !important; }
    .card-car { transition: transform .15s; }
    .card-car:hover { transform: translateY(-3px); box-shadow:0 6px 20px rgba(0,0,0,.08);}
    .price { color:#065A82; font-weight:700; }
    .badge-status { font-size: .75rem; }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;nav class=&quot;navbar navbar-dark px-4&quot;&gt;
  &lt;span class=&quot;navbar-brand mb-0 h4&quot;&gt;🚗 중고차 마켓 MVP&lt;/span&gt;
  &lt;div&gt;
    &lt;button class=&quot;btn btn-light btn-sm me-2&quot; onclick=&quot;showRegister()&quot;&gt;+ 매물 등록&lt;/button&gt;
    &lt;span id=&quot;health&quot; class=&quot;badge bg-secondary&quot;&gt;checking…&lt;/span&gt;
  &lt;/div&gt;
&lt;/nav&gt;

&lt;div class=&quot;container my-4&quot;&gt;
  &lt;div class=&quot;row g-2 mb-3 align-items-end&quot;&gt;
    &lt;div class=&quot;col-md-3&quot;&gt;
      &lt;label class=&quot;form-label small&quot;&gt;브랜드&lt;/label&gt;
      &lt;select id=&quot;filterBrand&quot; class=&quot;form-select form-select-sm&quot;&gt;
        &lt;option value=&quot;&quot;&gt;전체&lt;/option&gt;
        &lt;option&gt;현대&lt;/option&gt;&lt;option&gt;기아&lt;/option&gt;&lt;option&gt;제네시스&lt;/option&gt;
        &lt;option&gt;BMW&lt;/option&gt;&lt;option&gt;벤츠&lt;/option&gt;
      &lt;/select&gt;
    &lt;/div&gt;
    &lt;div class=&quot;col-md-3&quot;&gt;
      &lt;label class=&quot;form-label small&quot;&gt;최대 가격(원)&lt;/label&gt;
      &lt;input id=&quot;filterPrice&quot; type=&quot;number&quot; class=&quot;form-control form-control-sm&quot; placeholder=&quot;예: 30000000&quot;&gt;
    &lt;/div&gt;
    &lt;div class=&quot;col-md-2&quot;&gt;
      &lt;button class=&quot;btn btn-primary btn-sm w-100&quot; onclick=&quot;loadCars()&quot;&gt;검색&lt;/button&gt;
    &lt;/div&gt;
    &lt;div class=&quot;col-md-4 text-end&quot;&gt;
      &lt;small class=&quot;text-muted&quot; id=&quot;count&quot;&gt;&lt;/small&gt;
    &lt;/div&gt;
  &lt;/div&gt;

  &lt;div id=&quot;cars&quot; class=&quot;row g-3&quot;&gt;&lt;/div&gt;
&lt;/div&gt;

&lt;!-- 등록 모달 --&gt;
&lt;div class=&quot;modal fade&quot; id=&quot;regModal&quot; tabindex=&quot;-1&quot;&gt;
  &lt;div class=&quot;modal-dialog&quot;&gt;
    &lt;div class=&quot;modal-content&quot;&gt;
      &lt;div class=&quot;modal-header bg-primary text-white&quot;&gt;&lt;h5 class=&quot;modal-title&quot;&gt;매물 등록&lt;/h5&gt;&lt;/div&gt;
      &lt;div class=&quot;modal-body&quot;&gt;
        &lt;div class=&quot;row g-2&quot;&gt;
          &lt;div class=&quot;col-6&quot;&gt;&lt;label class=&quot;form-label&quot;&gt;판매자&lt;/label&gt;
            &lt;select id=&quot;r_seller&quot; class=&quot;form-select&quot;&gt;&lt;/select&gt;&lt;/div&gt;
          &lt;div class=&quot;col-6&quot;&gt;&lt;label class=&quot;form-label&quot;&gt;브랜드&lt;/label&gt;
            &lt;input id=&quot;r_brand&quot; class=&quot;form-control&quot; placeholder=&quot;현대&quot;&gt;&lt;/div&gt;
          &lt;div class=&quot;col-6&quot;&gt;&lt;label class=&quot;form-label&quot;&gt;모델&lt;/label&gt;
            &lt;input id=&quot;r_model&quot; class=&quot;form-control&quot;&gt;&lt;/div&gt;
          &lt;div class=&quot;col-3&quot;&gt;&lt;label class=&quot;form-label&quot;&gt;연식&lt;/label&gt;
            &lt;input id=&quot;r_year&quot; type=&quot;number&quot; class=&quot;form-control&quot; value=&quot;2022&quot;&gt;&lt;/div&gt;
          &lt;div class=&quot;col-3&quot;&gt;&lt;label class=&quot;form-label&quot;&gt;연료&lt;/label&gt;
            &lt;input id=&quot;r_fuel&quot; class=&quot;form-control&quot; placeholder=&quot;가솔린&quot;&gt;&lt;/div&gt;
          &lt;div class=&quot;col-6&quot;&gt;&lt;label class=&quot;form-label&quot;&gt;가격(원)&lt;/label&gt;
            &lt;input id=&quot;r_price&quot; type=&quot;number&quot; class=&quot;form-control&quot;&gt;&lt;/div&gt;
          &lt;div class=&quot;col-6&quot;&gt;&lt;label class=&quot;form-label&quot;&gt;주행(km)&lt;/label&gt;
            &lt;input id=&quot;r_mile&quot; type=&quot;number&quot; class=&quot;form-control&quot;&gt;&lt;/div&gt;
          &lt;div class=&quot;col-12&quot;&gt;&lt;label class=&quot;form-label&quot;&gt;설명&lt;/label&gt;
            &lt;textarea id=&quot;r_desc&quot; rows=&quot;2&quot; class=&quot;form-control&quot;&gt;&lt;/textarea&gt;&lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;div class=&quot;modal-footer&quot;&gt;
        &lt;button class=&quot;btn btn-secondary&quot; data-bs-dismiss=&quot;modal&quot;&gt;취소&lt;/button&gt;
        &lt;button class=&quot;btn btn-primary&quot; onclick=&quot;submitCar()&quot;&gt;등록&lt;/button&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;!-- 문의 모달 --&gt;
&lt;div class=&quot;modal fade&quot; id=&quot;inqModal&quot; tabindex=&quot;-1&quot;&gt;
  &lt;div class=&quot;modal-dialog&quot;&gt;
    &lt;div class=&quot;modal-content&quot;&gt;
      &lt;div class=&quot;modal-header bg-info text-white&quot;&gt;&lt;h5 class=&quot;modal-title&quot;&gt;문의하기&lt;/h5&gt;&lt;/div&gt;
      &lt;div class=&quot;modal-body&quot;&gt;
        &lt;p class=&quot;text-muted small mb-2&quot;&gt;차량 ID: &lt;span id=&quot;inq_car_id&quot;&gt;&lt;/span&gt;&lt;/p&gt;
        &lt;label class=&quot;form-label&quot;&gt;구매자&lt;/label&gt;
        &lt;select id=&quot;inq_buyer&quot; class=&quot;form-select mb-2&quot;&gt;&lt;/select&gt;
        &lt;label class=&quot;form-label&quot;&gt;메시지&lt;/label&gt;
        &lt;textarea id=&quot;inq_msg&quot; class=&quot;form-control&quot; rows=&quot;3&quot;&gt;&lt;/textarea&gt;
      &lt;/div&gt;
      &lt;div class=&quot;modal-footer&quot;&gt;
        &lt;button class=&quot;btn btn-secondary&quot; data-bs-dismiss=&quot;modal&quot;&gt;취소&lt;/button&gt;
        &lt;button class=&quot;btn btn-info text-white&quot; onclick=&quot;submitInquiry()&quot;&gt;전송&lt;/button&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;script src=&quot;https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js&quot;&gt;&lt;/script&gt;
&lt;script&gt;
let users = [];

async function checkHealth() {
  try {
    const r = await fetch(&#39;/health&#39;);
    const j = await r.json();
    document.getElementById(&#39;health&#39;).className = &#39;badge &#39; + (j.status===&#39;ok&#39; ? &#39;bg-success&#39; : &#39;bg-danger&#39;);
    document.getElementById(&#39;health&#39;).textContent = &#39;DB &#39; + j.status;
  } catch (e) {
    document.getElementById(&#39;health&#39;).className = &#39;badge bg-danger&#39;;
    document.getElementById(&#39;health&#39;).textContent = &#39;DB error&#39;;
  }
}

async function loadUsers() {
  const r = await fetch(&#39;/api/users&#39;);
  users = await r.json();
  const sellerSel = document.getElementById(&#39;r_seller&#39;);
  const buyerSel  = document.getElementById(&#39;inq_buyer&#39;);
  sellerSel.innerHTML = users.filter(u =&gt; u.type !== &#39;buyer&#39;)
    .map(u =&gt; &#39;&lt;option value=&quot;&#39; + u.id + &#39;&quot;&gt;&#39; + u.name + &#39; (&#39; + u.email + &#39;)&lt;/option&gt;&#39;).join(&#39;&#39;);
  buyerSel.innerHTML  = users.filter(u =&gt; u.type !== &#39;seller&#39;)
    .map(u =&gt; &#39;&lt;option value=&quot;&#39; + u.id + &#39;&quot;&gt;&#39; + u.name + &#39;&lt;/option&gt;&#39;).join(&#39;&#39;);
}

async function loadCars() {
  const brand = document.getElementById(&#39;filterBrand&#39;).value;
  const price = document.getElementById(&#39;filterPrice&#39;).value;
  const params = new URLSearchParams();
  if (brand) params.append(&#39;brand&#39;, brand);
  if (price) params.append(&#39;max_price&#39;, price);
  const r = await fetch(&#39;/api/cars?&#39; + params);
  const cars = await r.json();
  document.getElementById(&#39;count&#39;).textContent = &#39;검색 결과: &#39; + cars.length + &#39;건&#39;;
  document.getElementById(&#39;cars&#39;).innerHTML = cars.map(function(c) {
    return &#39;&#39; +
      &#39;&lt;div class=&quot;col-md-6 col-lg-4&quot;&gt;&#39; +
        &#39;&lt;div class=&quot;card card-car h-100&quot;&gt;&lt;div class=&quot;card-body&quot;&gt;&#39; +
          &#39;&lt;div class=&quot;d-flex justify-content-between&quot;&gt;&#39; +
            &#39;&lt;h5 class=&quot;card-title mb-0&quot;&gt;&#39; + c.brand + &#39; &#39; + c.model + &#39;&lt;/h5&gt;&#39; +
            &#39;&lt;span class=&quot;badge bg-success badge-status&quot;&gt;&#39; + c.status + &#39;&lt;/span&gt;&#39; +
          &#39;&lt;/div&gt;&#39; +
          &#39;&lt;p class=&quot;text-muted small mt-1&quot;&gt;&#39; + c.year + &#39;년식 · &#39; + c.mileage.toLocaleString() + &#39;km · &#39; + (c.fuel||&#39;-&#39;) + &#39;&lt;/p&gt;&#39; +
          &#39;&lt;p class=&quot;price h5 mb-2&quot;&gt;&#39; + c.price.toLocaleString() + &#39; 원&lt;/p&gt;&#39; +
          &#39;&lt;p class=&quot;small mb-2&quot;&gt;&#39; + (c.desc||&#39;&#39;) + &#39;&lt;/p&gt;&#39; +
          &#39;&lt;p class=&quot;small text-muted mb-2&quot;&gt;판매자: &#39; + c.seller + &#39;&lt;/p&gt;&#39; +
          &#39;&lt;button class=&quot;btn btn-sm btn-outline-info&quot; onclick=&quot;openInquiry(&#39; + c.id + &#39;)&quot;&gt;문의하기&lt;/button&gt;&#39; +
        &#39;&lt;/div&gt;&lt;/div&gt;&#39; +
      &#39;&lt;/div&gt;&#39;;
  }).join(&#39;&#39;);
}

function showRegister() { new bootstrap.Modal(document.getElementById(&#39;regModal&#39;)).show(); }

async function submitCar() {
  const body = {
    seller_id: parseInt(document.getElementById(&#39;r_seller&#39;).value),
    brand:   document.getElementById(&#39;r_brand&#39;).value.trim(),
    model:   document.getElementById(&#39;r_model&#39;).value.trim(),
    year:    parseInt(document.getElementById(&#39;r_year&#39;).value),
    price:   parseInt(document.getElementById(&#39;r_price&#39;).value),
    mileage: parseInt(document.getElementById(&#39;r_mile&#39;).value),
    fuel:    document.getElementById(&#39;r_fuel&#39;).value.trim(),
    desc:    document.getElementById(&#39;r_desc&#39;).value.trim(),
  };
  const r = await fetch(&#39;/api/cars&#39;, {
    method: &#39;POST&#39;,
    headers: {&#39;Content-Type&#39;: &#39;application/json&#39;},
    body: JSON.stringify(body)
  });
  if (r.ok) {
    alert(&#39;등록 완료!&#39;);
    bootstrap.Modal.getInstance(document.getElementById(&#39;regModal&#39;)).hide();
    loadCars();
  } else {
    alert(&#39;실패: &#39; + (await r.text()));
  }
}

function openInquiry(carId) {
  document.getElementById(&#39;inq_car_id&#39;).textContent = carId;
  document.getElementById(&#39;inq_msg&#39;).value = &#39;&#39;;
  new bootstrap.Modal(document.getElementById(&#39;inqModal&#39;)).show();
}

async function submitInquiry() {
  const body = {
    car_id:   parseInt(document.getElementById(&#39;inq_car_id&#39;).textContent),
    buyer_id: parseInt(document.getElementById(&#39;inq_buyer&#39;).value),
    message:  document.getElementById(&#39;inq_msg&#39;).value.trim(),
  };
  if (!body.message) { alert(&#39;메시지를 입력하세요&#39;); return; }
  const r = await fetch(&#39;/api/inquiries&#39;, {
    method: &#39;POST&#39;,
    headers: {&#39;Content-Type&#39;: &#39;application/json&#39;},
    body: JSON.stringify(body)
  });
  if (r.ok) {
    alert(&#39;문의 전송 완료!&#39;);
    bootstrap.Modal.getInstance(document.getElementById(&#39;inqModal&#39;)).hide();
  } else {
    alert(&#39;실패: &#39; + (await r.text()));
  }
}

checkHealth();
loadUsers();
loadCars();
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
&quot;&quot;&quot;
</code></pre><h2 id="로컬-테스트-1">로컬 테스트</h2>
<pre><code>source venv/bin/activate
python app.py &amp;

curl -s http://localhost:5000/ | head -20</code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/aad4fdeb-0faf-4eda-9f51-074f9034a100/image.png" alt=""></p>
<h1 id="외부-접근-설정nsg--systemd-서비스화">외부 접근 설정(NSG) + systemd 서비스화</h1>
<p>NSG에 5000 포트를 열어 외부 브라우저에서 접근 가능하게 하고, Flask 앱을 systemd 서비스로 등록해 SSH 종료 후에도 동작하게 설정</p>
<h2 id="nsg-규칙-추가---flask-5000-포트">NSG 규칙 추가 - Flask 5000 포트</h2>
<p>로컬 개발 머신(VM이 아닌, 로컬 PC)에서 실행</p>
<pre><code># 5000 포트 외부 노출
az vm open-port \
  --resource-group $RG \
  --name $VM \
  --port 5000 \
  --priority 1010

# 현재 NSG 규칙 확인
az network nsg rule list \
  --resource-group $RG \
  --nsg-name ${VM}NSG \
  --output table</code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/e0ea58b7-9db6-46e7-9fa1-11653ce32fd1/image.png" alt=""></p>
<ul>
<li>1433 포트는 절대 열지 마세요. SQL Server는 Step 4에서 127.0.0.1 바인딩했지만 NSG도 이중 차단입니다.</li>
<li>우선순위(priority) 1010은 SSH 1000과 충돌하지 않도록 100 이상 차이를 둡니다.</li>
<li>끝나면 5000도 다시 닫는것 필수</li>
</ul>
<h2 id="외부-차단-검증">외부 차단 검증</h2>
<pre><code># 외부에서 SQL Server 접속 시도 — 반드시 timeout 발생해야 정상
sqlcmd -S $PUBIP,1433 -U sa -P &#39;dummy&#39; -C -l 5
# 출력 예: Login timeout expired ... (10 ~ 60초 후)

# 또는 nc(netcat) / Test-NetConnection
# Linux/Mac:
nc -zv $PUBIP 1433
# 결과: &quot;Connection refused&quot; 또는 &quot;timed out&quot; → OK

# Windows PowerShell:
Test-NetConnection -ComputerName $env:PUBIP -Port 1433
# TcpTestSucceeded : False → OK</code></pre><p>1433 포트가 외부에서 timeout 또는 refused — 정상. 만약 connect 성공하면 즉시 NSG와 SQL Server bind 설정 재점검.</p>
<h2 id="flask앱-systemd-서비스-등록">Flask앱 systemd 서비스 등록</h2>
<pre><code>sudo nano /etc/systemd/system/carmarket.service

[Unit]
Description=CarMarket Flask App
After=network.target mssql-server.service
Requires=mssql-server.service

[Service]
Type=simple
User=azureuser
WorkingDirectory=/home/azureuser/carmarket
EnvironmentFile=/home/azureuser/carmarket/.env
ExecStart=/home/azureuser/carmarket/venv/bin/gunicorn \
  --bind 0.0.0.0:5000 \
  --workers 2 \
  --access-logfile - \
  app:app
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

# 서비스 활성화 및 시작
sudo systemctl daemon-reload
sudo systemctl enable carmarket
sudo systemctl start carmarket
sudo systemctl status carmarket --no-pager
</code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/91bd6627-ba0b-449c-a5e6-4091b1ab104a/image.png" alt=""></p>
<p>Active: active (running) 표시되면 OK. 실패 시 journalctl -u carmarket -n 50 으로 로그 확인.</p>
<h2 id="외부-브라우저로-접속">외부 브라우저로 접속</h2>
<p>로컬 PC 브라우저에서 <code>http://&lt;PUBIP&gt;:5000/</code> 로 접근
(azure portal의 vm 리소스에 기본 NIC 공용 IP 사용)
<img src="https://velog.velcdn.com/images/rudin_/post/84a61638-c1c3-4a9c-b539-5634097dab0d/image.png" alt=""></p>
<h1 id="정리">정리</h1>
<p>리소스 삭제하거나, vm의 인바운드 포트 규칙의 5000 삭제</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MicrosoftDataSchool] 79일차 - DB 역사]]></title>
            <link>https://velog.io/@rudin_/MicrosoftDataSchool-79%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@rudin_/MicrosoftDataSchool-79%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Wed, 29 Apr 2026 01:09:14 GMT</pubDate>
            <description><![CDATA[<p>supabase, postgresql, mysql, mariadb, firebase 등으로 시작하고 후에 확장해서 로드밸런서 붙이거나 하는 것
MVP: Minimum Viable Product</p>
<p>초기에 죄다 azure function(서버리스)로 처리하면 의존성이 증가하여 유지보수가 힘들다 → NOSQL로도 만들수 있지만, RDBMS를 함께 써야 확장성이 좋다.</p>
<p>MySQL을 쓰더라도 온프레미스로 vm(월 100$)에 설치해서 직접 관리하냐 vs 돈 좀 내고 Paas 서비스(Azure Database for MySQL 월 200$) 쓸거냐</p>
<hr>

<h1 id="azure-database-engineer-bootcamp-정리">Azure Database Engineer Bootcamp 정리</h1>
<h1 id="지형도--환경-셋업">지형도 &amp; 환경 셋업</h1>
<p>Azure 데이터 서비스의 전체 그림을 이해하고, 실습 환경을 직접 구축하는 과정이다.
예상 비용은 <strong>1인당 약 $0.5</strong>로 안내되어 있다.</p>
<hr>
<h1 id="학습-목표">학습 목표</h1>
<p>이번 과정의 학습 목표는 다음과 같다.</p>
<table>
<thead>
<tr>
<th>학습 목표</th>
</tr>
</thead>
<tbody><tr>
<td>DB 진화 과정(계층형 → RDBMS → NoSQL → NewSQL) 설명 가능</td>
</tr>
<tr>
<td>CAP / BASE 차이를 실무 사례로 비교 가능</td>
</tr>
<tr>
<td>Azure 데이터 서비스 전체 지형도 + 의사결정 트리 활용</td>
</tr>
<tr>
<td>구독 · 리소스 그룹 · VNet · NSG 직접 생성</td>
</tr>
<tr>
<td>Budget Alert로 비용 사전 통제</td>
</tr>
<tr>
<td>리소스 정리 스크립트 작성 및 실행</td>
</tr>
</tbody></table>
<hr>
<h1 id="선수-지식-확인">선수 지식 확인</h1>
<table>
<thead>
<tr>
<th>스킬</th>
<th>수준</th>
<th>구분</th>
</tr>
</thead>
<tbody><tr>
<td>SQL</td>
<td>SELECT / JOIN 기본 쿼리 작성 · 조인 이해</td>
<td>필수</td>
</tr>
<tr>
<td>Linux CLI</td>
<td><code>ls</code>, <code>cd</code>, <code>ssh</code>, <code>vi</code> 기본 조작</td>
<td>필수</td>
</tr>
<tr>
<td>Azure Portal</td>
<td>리소스 검색 · 생성 화면 탐색</td>
<td>필수</td>
</tr>
<tr>
<td>네트워킹 기초</td>
<td>IP, 서브넷, 방화벽 개념</td>
<td>권장</td>
</tr>
<tr>
<td>Git 기초</td>
<td>clone, commit 수준</td>
<td>권장</td>
</tr>
</tbody></table>
<hr>
<h1 id="db-역사와-이론">DB 역사와 이론</h1>
<h2 id="왜-db-역사를-배우는가">왜 DB 역사를 배우는가?</h2>
<p>DB 역사를 배우는 이유는 현재 기술의 장단점이 과거 문제 해결 과정에서 탄생했기 때문이다.</p>
<ul>
<li>현재 기술의 장단점은 과거 문제 해결 과정에서 탄생</li>
<li>NoSQL은 RDBMS를 “대체”하는 것이 아님</li>
<li>워크로드에 맞는 DB 선택 = DBA 핵심 역량</li>
<li>Azure에 데이터 서비스가 10가지 넘는 이유를 이해하기 위함</li>
</ul>
<hr>
<h2 id="계층형-db-→-관계형-db">계층형 DB → 관계형 DB</h2>
<h3 id="1960s-계층형-db">1960s 계층형 DB</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>대표 시스템</td>
<td>IBM IMS (1966)</td>
</tr>
<tr>
<td>구조</td>
<td>트리 구조: 부모 → 자식</td>
</tr>
<tr>
<td>장점</td>
<td>빠른 읽기</td>
</tr>
<tr>
<td>한계</td>
<td>유연성 부족</td>
</tr>
</tbody></table>
<h3 id="1970s-네트워크-db">1970s 네트워크 DB</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>대표 모델</td>
<td>CODASYL</td>
</tr>
<tr>
<td>특징</td>
<td>다대다 관계 지원</td>
</tr>
</tbody></table>
<h3 id="197080s-관계형-db">1970~80s 관계형 DB</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>핵심 인물</td>
<td>E.F. Codd (1970)</td>
</tr>
<tr>
<td>핵심 개념</td>
<td>데이터를 테이블로 추상화</td>
</tr>
<tr>
<td>쿼리 언어</td>
<td>SQL: 선언적 쿼리 언어</td>
</tr>
<tr>
<td>대표 DB</td>
<td>Oracle (1979), SQL Server (1989), PostgreSQL (1996)</td>
</tr>
</tbody></table>
<hr>
<h2 id="계층형·네트워크-db의-한계">계층형·네트워크 DB의 한계</h2>
<p>RDBMS가 혁명적이었던 이유는 기존 계층형·네트워크 DB의 한계를 해결했기 때문이다.</p>
<ul>
<li>트리/그래프 구조에서는 데이터 접근 경로를 프로그래머가 직접 코딩해야 했다.</li>
<li>스키마 변경 시 애플리케이션 전체 수정이 필요했다.</li>
<li>데이터 독립성이 부족했다.</li>
<li>물리적 저장 구조와 논리적 구조가 결합되어 있었다.</li>
<li>다대다 관계 표현이 복잡했다.</li>
<li>이러한 문제들이 Codd의 관계 모델 탄생 배경이 되었다.</li>
</ul>
<hr>
<h2 id="sql의-탄생과-표준화">SQL의 탄생과 표준화</h2>
<h3 id="sql의-진화">SQL의 진화</h3>
<table>
<thead>
<tr>
<th>연도</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>1970</td>
<td>Codd 관계 모델 논문</td>
</tr>
<tr>
<td>1974</td>
<td>SEQUEL (IBM)</td>
</tr>
<tr>
<td>1979</td>
<td>Oracle V2, 최초 상용</td>
</tr>
<tr>
<td>1986</td>
<td>SQL-86, ANSI 표준</td>
</tr>
<tr>
<td>1992</td>
<td>SQL-92</td>
</tr>
<tr>
<td>1999</td>
<td>SQL:1999, CTE / 윈도우 함수</td>
</tr>
<tr>
<td>2016</td>
<td>SQL:2016, JSON</td>
</tr>
</tbody></table>
<hr>
<h2 id="sql의-선언적-혁신">SQL의 선언적 혁신</h2>
<p>SQL은 “어떻게 가져올지”가 아니라 “무엇을 가져올지”를 기술한다.</p>
<table>
<thead>
<tr>
<th>방식</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>절차적</td>
<td>HOW를 기술, 어떻게 데이터를 가져올지 작성</td>
</tr>
<tr>
<td>선언적</td>
<td>WHAT을 기술, 무엇을 가져올지 작성</td>
</tr>
</tbody></table>
<p>예시:</p>
<pre><code class="language-sql">SELECT name FROM users
WHERE age &gt; 30;</code></pre>
<p>이 경우 사용자는 원하는 결과만 선언하고, 실제 최적 경로는 DB 엔진이 결정한다.</p>
<hr>
<h2 id="rdbms-30년-지배">RDBMS 30년 지배</h2>
<p>1980년대부터 2000년대까지 RDBMS는 데이터베이스 시장을 지배했다.</p>
<ul>
<li>1980~2000: Oracle, SQL Server, MySQL, PostgreSQL 시장 장악</li>
<li>ACID 트랜잭션: 금융 · ERP · 재고 시스템의 기반</li>
<li>SQL 표준화: 어떤 RDBMS든 비슷한 쿼리 사용 가능</li>
<li>정규화 이론: 중복 제거, 무결성 보장<ul>
<li>예)중복되는 부분을 테이블 분리 후 ID로 관리</li>
</ul>
</li>
<li>그러나 2000년대 웹 스케일 문제가 RDBMS의 한계를 드러냄</li>
</ul>
<hr>
<h1 id="nosql">NoSQL</h1>
<p>Not Only SQL</p>
<h2 id="nosql-4가지-데이터-모델">NoSQL 4가지 데이터 모델</h2>
<table>
<thead>
<tr>
<th>모델</th>
<th>대표 기술</th>
<th>특징</th>
<th>사용 예</th>
</tr>
</thead>
<tbody><tr>
<td>Key-Value</td>
<td>Redis, DynamoDB</td>
<td>최고 성능</td>
<td>캐시, 세션 관리</td>
</tr>
<tr>
<td>Document</td>
<td>MongoDB, Cosmos DB</td>
<td>JSON / BSON, 유연한 스키마</td>
<td>문서형 데이터</td>
</tr>
<tr>
<td>Column-Family</td>
<td>Cassandra, HBase</td>
<td>대규모 쓰기</td>
<td>시계열, 로그</td>
</tr>
<tr>
<td>Graph</td>
<td>Neo4j, Gremlin</td>
<td>관계 탐색</td>
<td>소셜, 추천</td>
</tr>
</tbody></table>
<hr>
<h2 id="nosql이-등장한-배경">NoSQL이 등장한 배경</h2>
<p>2000년대 Google과 Amazon이 NoSQL의 길을 열었다.</p>
<ul>
<li><p>2004: Google BigTable 논문 → Column-Family 모델에 영감</p>
</li>
<li><p>2007: Amazon Dynamo 논문 → Key-Value + Eventual Consistency</p>
</li>
<li><p>2009: MongoDB, Cassandra, Redis 등 폭발적 등장</p>
</li>
<li><p>핵심 동인: 웹 스케일</p>
<ul>
<li>수십억 사용자</li>
<li>페타바이트 데이터</li>
</ul>
</li>
<li><p>RDBMS의 수직 확장 한계</p>
<ul>
<li>단일 서버 성능에 의존</li>
</ul>
</li>
<li><p>수평 확장 필요</p>
<ul>
<li>Scale-Out</li>
<li>분산 시스템</li>
<li>CAP 트레이드오프 발생</li>
</ul>
</li>
</ul>
<p>실시간으로 기록해야하는 부분(그날 게임점수 신기록) 이런건 NoSQL로 처리하는게 이득</p>
<hr>
<h1 id="데이터베이스의-종류와-역사">데이터베이스의 종류와 역사</h1>
<h2 id="데이터베이스란">데이터베이스란?</h2>
<p>데이터베이스는 구조화된 정보나 데이터의 조직화된 모음이다. 일반적으로 컴퓨터 시스템에 전자적으로 저장된다.</p>
<p>DBMS(Database Management System)는 사용자와 애플리케이션이 데이터베이스와 상호 작용할 수 있게 해주는 소프트웨어이다.</p>
<h3 id="주요-역할">주요 역할</h3>
<table>
<thead>
<tr>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>데이터의 일관성 유지</td>
</tr>
<tr>
<td>보안 및 접근 제어 관리</td>
</tr>
<tr>
<td>중복 데이터 제거 및 저장 공간 최적화</td>
</tr>
<tr>
<td>다중 사용자 접근 제어</td>
</tr>
</tbody></table>
<hr>
<h2 id="데이터와-정보의-차이">데이터와 정보의 차이</h2>
<h3 id="데이터data">데이터(Data)</h3>
<p>데이터는 가공되지 않은 원시 사실이나 관찰 결과를 의미한다. 그 자체로는 특별한 의미가 없는 단순한 숫자, 문자, 이미지 등의 모음이다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>예시</td>
<td><code>36.5</code>, <code>&quot;홍길동&quot;</code>, <code>2023-05-15</code></td>
</tr>
<tr>
<td>특징</td>
<td>컴퓨터가 처리할 수 있는 형태로 표현된 사실</td>
</tr>
<tr>
<td>상태</td>
<td>해석되지 않은 원시 상태</td>
</tr>
</tbody></table>
<h3 id="정보information">정보(Information)</h3>
<p>정보는 데이터를 가공·처리하여 의미를 부여한 결과물이다. 특정 목적을 위해 데이터를 해석하고 조직화한 형태이다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>예시</td>
<td>“체온은 정상(36.5℃)입니다”</td>
</tr>
<tr>
<td>특징</td>
<td>의사결정에 활용 가능한 가치 있는 결과물</td>
</tr>
<tr>
<td>상태</td>
<td>맥락과 관계성이 부여된 상태</td>
</tr>
</tbody></table>
<hr>
<h2 id="데이터베이스의-역사-개요">데이터베이스의 역사 개요</h2>
<p>데이터베이스 기술은 1960년대부터 시작되어 컴퓨팅 기술의 발전과 함께 계속 진화해왔다.</p>
<p>초기에는 단순한 파일 시스템으로 데이터를 관리했지만, 시간이 지나면서 계층형 DB, 네트워크형 DB, 관계형 DB, 객체지향 DB, NoSQL, NewSQL, 클라우드 DB까지 발전했다.</p>
<p>이러한 변화는 다음 요인에 의해 가속화되었다.</p>
<table>
<thead>
<tr>
<th>발전 요인</th>
</tr>
</thead>
<tbody><tr>
<td>하드웨어 성능 향상</td>
</tr>
<tr>
<td>네트워크 기술 발전</td>
</tr>
<tr>
<td>비즈니스 요구사항 변화</td>
</tr>
</tbody></table>
<hr>
<h1 id="파일-시스템-시대">파일 시스템 시대</h1>
<h2 id="파일-시스템-시대-1세대">파일 시스템 시대 (1세대)</h2>
<p>파일 시스템은 초기 데이터 관리 방식이다. 데이터를 데이터베이스가 아니라 파일 단위로 저장하고, 애플리케이션이 직접 파일을 읽고 쓰는 방식이었다.</p>
<h3 id="파일-시스템의-특징">파일 시스템의 특징</h3>
<table>
<thead>
<tr>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>데이터를 단순 텍스트 파일이나 바이너리 파일로 저장</td>
</tr>
<tr>
<td>각 애플리케이션마다 독립적인 파일 구조 사용</td>
</tr>
<tr>
<td>메인프레임 컴퓨터에서 주로 사용</td>
</tr>
</tbody></table>
<h3 id="파일-시스템의-한계">파일 시스템의 한계</h3>
<table>
<thead>
<tr>
<th>한계</th>
</tr>
</thead>
<tbody><tr>
<td>데이터 중복 발생 불가피</td>
</tr>
<tr>
<td>일관성 유지 어려움</td>
</tr>
<tr>
<td>복잡한 쿼리나 검색 기능 부재</td>
</tr>
<tr>
<td>데이터 보안 취약</td>
</tr>
<tr>
<td>동시 접근 제어 불가능</td>
</tr>
</tbody></table>
<p>이 시기에는 프로그래머가 직접 파일 관리 로직을 구현해야 했다. 따라서 데이터 접근과 관리에 많은 시간과 자원이 소모되었다.</p>
<hr>
<h1 id="계층형-데이터베이스">계층형 데이터베이스</h1>
<h2 id="계층형-데이터베이스-2세대">계층형 데이터베이스 (2세대)</h2>
<p>계층형 데이터베이스는 데이터를 트리 구조로 구성한다. 부모-자식 관계를 중심으로 데이터를 표현하며, 하향식으로 데이터를 탐색한다.</p>
<h3 id="주요-특징">주요 특징</h3>
<table>
<thead>
<tr>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>트리 구조로 데이터 구성</td>
</tr>
<tr>
<td>부모-자식 관계(1:N) 표현</td>
</tr>
<tr>
<td>하향식 데이터 탐색 방식</td>
</tr>
<tr>
<td>IBM IMS(Information Management System, 1966년) 등장</td>
</tr>
</tbody></table>
<h3 id="한계점">한계점</h3>
<table>
<thead>
<tr>
<th>한계</th>
</tr>
</thead>
<tbody><tr>
<td>복잡한 관계 표현 어려움</td>
</tr>
<tr>
<td>데이터 접근 경로 사전 정의 필요</td>
</tr>
<tr>
<td>구조 변경 시 전체 시스템에 영향</td>
</tr>
</tbody></table>
<p>계층형 데이터베이스는 파일 시스템의 한계를 극복하고 구조화된 데이터 관리를 가능하게 했지만, 복잡한 다대다 관계를 표현하기에는 제한적이었다.</p>
<hr>
<h1 id="네트워크형-데이터베이스">네트워크형 데이터베이스</h1>
<h2 id="네트워크형-데이터베이스-1">네트워크형 데이터베이스</h2>
<p>네트워크형 데이터베이스는 계층형 모델보다 복잡한 관계를 표현하기 위해 등장했다. 그래프 구조를 사용하며 레코드 간 다대다 관계를 표현할 수 있다.</p>
<table>
<thead>
<tr>
<th>요소</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>그래프 구조</td>
<td>레코드 간 다대다(N:M) 관계 표현 가능</td>
</tr>
<tr>
<td>CODASYL</td>
<td>1970년대 네트워크 DB 표준화 모델 개발</td>
</tr>
<tr>
<td>포인터 시스템</td>
<td>레코드 간 직접 연결 포인터 사용</td>
</tr>
<tr>
<td>유연한 쿼리</td>
<td>계층형보다 향상된 데이터 검색 기능</td>
</tr>
</tbody></table>
<p>네트워크형 데이터베이스는 계층형 모델의 제한을 극복했지만, 여전히 데이터 구조와 쿼리가 복잡하다는 한계가 있었다.</p>
<hr>
<h1 id="관계형-데이터베이스-도입">관계형 데이터베이스 도입</h1>
<h2 id="ef-codd의-혁신">E.F. Codd의 혁신</h2>
<p>1970년 IBM 연구원 에드거 F. 코드(E.F. Codd)는 <strong>“A Relational Model of Data for Large Shared Data Banks”</strong>라는 논문을 발표했다. 이 논문은 관계형 데이터베이스의 이론적 기반을 마련했다.</p>
<h3 id="주요-혁신점">주요 혁신점</h3>
<table>
<thead>
<tr>
<th>혁신점</th>
</tr>
</thead>
<tbody><tr>
<td>테이블(릴레이션) 구조 도입</td>
</tr>
<tr>
<td>수학적 집합 이론 기반</td>
</tr>
<tr>
<td>데이터와 물리적 저장 구조 분리</td>
</tr>
<tr>
<td>선언적 쿼리 언어 개념 제시</td>
</tr>
</tbody></table>
<p>코드의 관계형 모델은 이전 데이터베이스 패러다임의 복잡성을 극복하고, 직관적이고 유연한 데이터 구조를 제공했다. 이 개념은 현대 데이터베이스 발전의 토대가 되었다.</p>
<hr>
<h2 id="관계형-dbms의-발전-3세대">관계형 DBMS의 발전 (3세대)</h2>
<table>
<thead>
<tr>
<th>시기</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>1974</td>
<td>IBM System R 프로젝트에서 SQL(Structured Query Language) 개발</td>
</tr>
<tr>
<td>1979</td>
<td>Oracle V2 출시, 최초의 상업용 RDBMS</td>
</tr>
<tr>
<td>1980년대</td>
<td>IBM DB2, Informix, Sybase 등장</td>
</tr>
<tr>
<td>1989</td>
<td>ANSI 및 ISO에서 SQL 표준 제정</td>
</tr>
<tr>
<td>1990년대</td>
<td>Microsoft SQL Server, MySQL 등장으로 RDBMS 대중화</td>
</tr>
</tbody></table>
<p>관계형 데이터베이스는 표준화된 SQL의 등장과 함께 산업 표준으로 자리 잡았다. 비즈니스 애플리케이션과 공공 시스템에서 널리 채택되었고, 이 시기에 데이터 무결성, 트랜잭션 처리, 백업/복구 등의 핵심 기능이 발전했다.</p>
<hr>
<h1 id="객체지향-dbms">객체지향 DBMS</h1>
<h2 id="객체지향-dbms-4세대">객체지향 DBMS (4세대)</h2>
<p>1980년대 후반 객체지향 프로그래밍이 인기를 얻으면서 복잡한 데이터 구조를 더 잘 표현할 수 있는 데이터베이스의 필요성이 커졌다.</p>
<h3 id="객체지향-db의-등장-배경">객체지향 DB의 등장 배경</h3>
<table>
<thead>
<tr>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>객체지향 프로그래밍의 확산</td>
</tr>
<tr>
<td>복잡한 데이터 구조 표현 필요</td>
</tr>
<tr>
<td>객체와 데이터베이스 간 표현 차이 해소 필요</td>
</tr>
</tbody></table>
<h3 id="주요-특징-1">주요 특징</h3>
<table>
<thead>
<tr>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>객체와 클래스 개념을 데이터베이스에 적용</td>
</tr>
<tr>
<td>상속, 다형성, 캡슐화 지원</td>
</tr>
<tr>
<td>복잡한 데이터 타입과 관계 표현 가능</td>
</tr>
<tr>
<td>객체 참조를 통한 관계 표현</td>
</tr>
</tbody></table>
<h3 id="대표-시스템">대표 시스템</h3>
<table>
<thead>
<tr>
<th>대표 객체지향 DBMS</th>
</tr>
</thead>
<tbody><tr>
<td>GemStone</td>
</tr>
<tr>
<td>ObjectStore</td>
</tr>
<tr>
<td>Versant</td>
</tr>
</tbody></table>
<p>객체지향 DBMS는 복잡한 데이터 표현에 강점이 있었지만, 관계형 데이터베이스의 강력한 점유율과 표준화된 SQL의 부재로 인해 주류 시장에서는 제한적인 성공을 거두었다.</p>
<hr>
<h1 id="객체-관계형-dbms">객체-관계형 DBMS</h1>
<h2 id="객체-관계형-dbms-5세대">객체-관계형 DBMS (5세대)</h2>
<p>객체-관계형 DBMS는 관계형 데이터베이스의 테이블 구조와 객체지향 데이터베이스의 유연성을 결합한 하이브리드 모델이다.</p>
<h3 id="하이브리드-접근법">하이브리드 접근법</h3>
<p>관계형 데이터베이스의 테이블 구조와 객체지향 데이터베이스의 유연성을 결합한다.</p>
<h3 id="확장된-데이터-타입">확장된 데이터 타입</h3>
<p>복잡한 사용자 정의 데이터 타입, 배열, XML, JSON 등 다양한 형식의 데이터를 직접 저장하고 처리할 수 있다.</p>
<h3 id="sql-확장">SQL 확장</h3>
<p>객체 조작을 위한 확장된 SQL 구문을 제공하여 복잡한 데이터 구조에 대한 쿼리를 쉽게 한다.</p>
<h3 id="대표-시스템-1">대표 시스템</h3>
<table>
<thead>
<tr>
<th>대표 객체-관계형 DBMS</th>
</tr>
</thead>
<tbody><tr>
<td>PostgreSQL</td>
</tr>
<tr>
<td>Oracle Database</td>
</tr>
<tr>
<td>IBM DB2</td>
</tr>
<tr>
<td>Microsoft SQL Server</td>
</tr>
</tbody></table>
<p>현대적인 관계형 데이터베이스 대부분은 객체-관계형 기능을 통합하고 있다.</p>
<hr>
<h1 id="dbms-역사-총정리">DBMS 역사 총정리</h1>
<table>
<thead>
<tr>
<th>세대</th>
<th>유형</th>
<th>시기</th>
<th>주요 특징</th>
<th>대표 시스템</th>
</tr>
</thead>
<tbody><tr>
<td>1세대</td>
<td>파일 시스템</td>
<td>1960년대 초</td>
<td>단순 파일 기반 데이터 저장</td>
<td>ISAM, VSAM</td>
</tr>
<tr>
<td>2세대</td>
<td>계층형</td>
<td>1960년대 중반</td>
<td>트리 구조, 부모-자식 관계</td>
<td>IBM IMS</td>
</tr>
<tr>
<td>3세대</td>
<td>네트워크형</td>
<td>1970년대 초</td>
<td>그래프 구조, 복잡한 관계</td>
<td>IDMS, CODASYL</td>
</tr>
<tr>
<td>4세대</td>
<td>관계형</td>
<td>1970~80년대</td>
<td>테이블 구조, SQL</td>
<td>Oracle, DB2, SQL Server</td>
</tr>
<tr>
<td>5세대</td>
<td>객체지향 / 객체관계형</td>
<td>1990년대</td>
<td>객체 모델, 복잡한 데이터 처리</td>
<td>PostgreSQL, ObjectStore</td>
</tr>
</tbody></table>
<p>데이터베이스 시스템은 단순 파일 저장에서 시작해 복잡한 데이터 관계와 구조를 표현할 수 있는 형태로 발전했다. 각 세대는 이전 세대의 한계를 극복하고 새로운 요구사항을 충족하기 위해 등장했다.</p>
<hr>
<h1 id="대표적-관계형-dbms">대표적 관계형 DBMS</h1>
<h2 id="oracle-database">Oracle Database</h2>
<p>1979년 출시된 엔터프라이즈급 RDBMS이다. 대규모 트랜잭션 처리와 안정성에 강점이 있다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>출시</td>
<td>1979년</td>
</tr>
<tr>
<td>특징</td>
<td>대규모 트랜잭션 처리, 안정성</td>
</tr>
<tr>
<td>주요 사용처</td>
<td>금융, 통신, 제조 등 대형 기업</td>
</tr>
</tbody></table>
<h2 id="mysql">MySQL</h2>
<p>1995년 출시된 오픈소스 RDBMS이다. 웹 애플리케이션과의 뛰어난 호환성과 속도가 특징이다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>출시</td>
<td>1995년</td>
</tr>
<tr>
<td>특징</td>
<td>오픈소스, 웹 애플리케이션 친화적, 빠른 속도</td>
</tr>
<tr>
<td>주요 사용처</td>
<td>WordPress, Facebook 등 웹 서비스 기반</td>
</tr>
</tbody></table>
<h2 id="microsoft-sql-server">Microsoft SQL Server</h2>
<p>1989년 출시된 Microsoft의 RDBMS이다. Windows 환경과의 통합성이 뛰어나다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>출시</td>
<td>1989년</td>
</tr>
<tr>
<td>특징</td>
<td>Windows 환경과 뛰어난 통합성</td>
</tr>
<tr>
<td>주요 사용처</td>
<td>중소기업부터 대기업까지 다양한 비즈니스</td>
</tr>
</tbody></table>
<h2 id="postgresql">PostgreSQL</h2>
<p>1996년 출시된 고급 오픈소스 RDBMS이다. 확장성과 표준 준수에 강점이 있다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>출시</td>
<td>1996년</td>
</tr>
<tr>
<td>특징</td>
<td>확장성, SQL 표준 준수, 복잡한 쿼리 처리</td>
</tr>
<tr>
<td>주요 사용처</td>
<td>대규모 데이터베이스, 복잡한 분석 시스템</td>
</tr>
</tbody></table>
<hr>
<h1 id="mysql과-postgresql-비교">MySQL과 PostgreSQL 비교</h1>
<h2 id="mysql의-특징">MySQL의 특징</h2>
<table>
<thead>
<tr>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>읽기 작업에 최적화된 성능</td>
</tr>
<tr>
<td>단순한 설치와 구성</td>
</tr>
<tr>
<td>웹 애플리케이션과의 호환성</td>
</tr>
<tr>
<td>다양한 스토리지 엔진 지원</td>
</tr>
<tr>
<td>빠른 처리 속도 중심</td>
</tr>
</tbody></table>
<p>MySQL은 주로 콘텐츠 관리 시스템, 블로그, 웹 애플리케이션에 적합하다.</p>
<h2 id="postgresql의-특징">PostgreSQL의 특징</h2>
<table>
<thead>
<tr>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>SQL 표준 준수 중시</td>
</tr>
<tr>
<td>복잡한 쿼리 최적화</td>
</tr>
<tr>
<td>고급 데이터 타입과 함수 지원</td>
</tr>
<tr>
<td>지리 정보 시스템(GIS) 기능</td>
</tr>
<tr>
<td>동시성과 안정성 중심</td>
</tr>
</tbody></table>
<p>PostgreSQL은 금융 시스템, 과학 연구, 복잡한 데이터 분석에 적합하다.</p>
<hr>
<h1 id="오픈소스-db의-부상">오픈소스 DB의 부상</h1>
<table>
<thead>
<tr>
<th>시기</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>1995년</td>
<td>MySQL 출시. Michael Widenius와 David Axmark이 개발. 무료 관계형 DB로 웹 개발에 혁신을 가져옴</td>
</tr>
<tr>
<td>1996년</td>
<td>PostgreSQL 출시. 버클리 대학 POSTGRES 프로젝트에서 발전한 고급 오픈소스 RDBMS</td>
</tr>
<tr>
<td>2000년대 초</td>
<td>닷컴 붐과 저비용 웹 솔루션 수요 증가로 MySQL 등 오픈소스 DB 채택 급증</td>
</tr>
<tr>
<td>2008년</td>
<td>Sun Microsystems가 MySQL을 10억 달러에 인수. 오픈소스 DB의 상업적 가치 입증</td>
</tr>
<tr>
<td>2010년 이후</td>
<td>오픈소스 DB가 엔터프라이즈 영역에서도 주류화. MariaDB, MongoDB 등 다양한 솔루션 등장</td>
</tr>
</tbody></table>
<hr>
<h1 id="엔터프라이즈-dbms">엔터프라이즈 DBMS</h1>
<h2 id="oracle-database-1">Oracle Database</h2>
<p>글로벌 금융 기관과 대기업에서 많이 사용되는 엔터프라이즈 DBMS이다.</p>
<table>
<thead>
<tr>
<th>주요 특징</th>
</tr>
</thead>
<tbody><tr>
<td>고성능 트랜잭션 처리(OLTP) 지원</td>
</tr>
<tr>
<td>실시간 애플리케이션 클러스터(RAC)</td>
</tr>
<tr>
<td>고급 보안 및 암호화 기능</td>
</tr>
<tr>
<td>다양한 데이터 유형 지원: 공간 데이터, 멀티미디어 등</td>
</tr>
</tbody></table>
<h2 id="ibm-db2">IBM Db2</h2>
<p>대형 메인프레임 환경에서 강점을 가진 엔터프라이즈 데이터베이스이다.</p>
<table>
<thead>
<tr>
<th>주요 특징</th>
</tr>
</thead>
<tbody><tr>
<td>고성능 분석 처리</td>
</tr>
<tr>
<td>AI 기반 쿼리 최적화</td>
</tr>
<tr>
<td>하이브리드 트랜잭션/분석 처리(HTAP)</td>
</tr>
<tr>
<td>메인프레임 환경과의 통합 지원</td>
</tr>
</tbody></table>
<h2 id="microsoft-sql-server-1">Microsoft SQL Server</h2>
<p>Windows 환경에서 강력한 통합 기능을 제공하는 엔터프라이즈 솔루션이다.</p>
<table>
<thead>
<tr>
<th>주요 특징</th>
</tr>
</thead>
<tbody><tr>
<td>비즈니스 인텔리전스 도구 통합</td>
</tr>
<tr>
<td>인메모리 OLTP 엔진</td>
</tr>
<tr>
<td>Microsoft 생태계와 높은 통합성</td>
</tr>
<tr>
<td>고급 보안 및 감사 기능</td>
</tr>
</tbody></table>
<hr>
<h1 id="nosql의-등장">NoSQL의 등장</h1>
<h2 id="nosql의-의미">NoSQL의 의미</h2>
<p>NoSQL은 <strong>Not Only SQL</strong>을 의미한다. 전통적인 관계형 데이터베이스의 한계를 넘어서기 위해 등장한 새로운 데이터베이스 패러다임이다.</p>
<h2 id="등장-배경">등장 배경</h2>
<table>
<thead>
<tr>
<th>배경</th>
</tr>
</thead>
<tbody><tr>
<td>웹 2.0과 소셜 미디어의 폭발적 성장</td>
</tr>
<tr>
<td>빅데이터와 실시간 분석 필요성</td>
</tr>
<tr>
<td>수평적 확장성(Scale-out) 요구</td>
</tr>
<tr>
<td>유연한 스키마와 다양한 데이터 형식 필요</td>
</tr>
</tbody></table>
<p>2000년대 후반 Google의 BigTable과 Amazon의 Dynamo 논문이 발표되면서 NoSQL 움직임이 본격화되었다. 이후 MongoDB, Cassandra, Redis 등 다양한 NoSQL 데이터베이스가 등장하며 새로운 데이터 저장 패러다임을 형성했다.</p>
<hr>
<h1 id="nosql-배경">NoSQL 배경</h1>
<table>
<thead>
<tr>
<th>수치</th>
<th>의미</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>2.5EB</td>
<td>일일 생성 데이터</td>
<td>2020년 기준 전 세계에서 하루에 생성되는 데이터 양. 이 중 80% 이상이 비정형 데이터</td>
</tr>
<tr>
<td>1B+</td>
<td>소셜 미디어 사용자</td>
<td>소셜 미디어 플랫폼 사용자는 매일 수십억 건의 상호작용 데이터를 생성</td>
</tr>
<tr>
<td>40%</td>
<td>연간 데이터 증가율</td>
<td>기업 데이터는 연평균 40% 이상 증가</td>
</tr>
<tr>
<td>1000x</td>
<td>처리 속도 향상</td>
<td>일부 NoSQL 시스템은 특정 워크로드에서 관계형 DB보다 최대 1000배 빠른 처리 속도 제공</td>
</tr>
</tbody></table>
<hr>
<h1 id="nosql-주요-유형-개관">NoSQL 주요 유형 개관</h1>
<table>
<thead>
<tr>
<th>유형</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Key-Value</td>
<td>단순한 키와 값의 쌍으로 데이터 저장. 고속 읽기/쓰기에 최적화</td>
</tr>
<tr>
<td>Document</td>
<td>JSON/BSON 형식의 문서로 데이터 저장. 유연한 스키마</td>
</tr>
<tr>
<td>Column</td>
<td>컬럼 패밀리 단위로 데이터 저장. 대규모 분석에 적합</td>
</tr>
<tr>
<td>Graph</td>
<td>노드와 관계로 데이터 저장. 복잡한 연결 분석에 최적화</td>
</tr>
</tbody></table>
<p>각 NoSQL 유형은 특정 사용 사례와 데이터 모델에 최적화되어 있다. 관계형 데이터베이스로 해결하기 어려운 특정 문제를 효율적으로 해결할 수 있다. 오늘날 많은 기업은 다양한 워크로드를 위해 여러 유형의 데이터베이스를 함께 사용하는 멀티 모델 접근 방식을 채택하고 있다.</p>
<hr>
<h1 id="key-value-db">Key-Value DB</h1>
<h2 id="작동-원리">작동 원리</h2>
<p>Key-Value DB는 단순한 키(key)와 값(value)의 쌍으로 데이터를 저장하는 가장 기본적인 NoSQL 데이터베이스이다.</p>
<h2 id="주요-특징-2">주요 특징</h2>
<table>
<thead>
<tr>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>초고속 읽기/쓰기 성능</td>
</tr>
<tr>
<td>수평적 확장성(Scale-out) 용이</td>
</tr>
<tr>
<td>단순한 API: GET, PUT, DELETE</td>
</tr>
<tr>
<td>스키마 제약 없음</td>
</tr>
</tbody></table>
<h2 id="대표적-시스템">대표적 시스템</h2>
<table>
<thead>
<tr>
<th>시스템</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Redis</td>
<td>인메모리 기반, 다양한 데이터 구조 지원</td>
</tr>
<tr>
<td>Amazon DynamoDB</td>
<td>완전 관리형 서비스</td>
</tr>
<tr>
<td>Riak</td>
<td>고가용성, 분산 아키텍처</td>
</tr>
<tr>
<td>Memcached</td>
<td>분산 캐싱 시스템</td>
</tr>
</tbody></table>
<h2 id="주요-활용-분야">주요 활용 분야</h2>
<table>
<thead>
<tr>
<th>활용 분야</th>
</tr>
</thead>
<tbody><tr>
<td>세션 관리 및 사용자 프로필</td>
</tr>
<tr>
<td>실시간 추천 엔진</td>
</tr>
<tr>
<td>쇼핑 카트 및 캐싱 시스템</td>
</tr>
<tr>
<td>IoT 데이터 저장</td>
</tr>
</tbody></table>
<hr>
<h1 id="document-db">Document DB</h1>
<h2 id="작동-원리-1">작동 원리</h2>
<p>Document DB는 JSON, BSON 또는 XML과 같은 반구조화된 형식의 문서로 데이터를 저장한다. 각 문서는 자체적으로 완결된 정보를 포함하며, 다양한 필드와 중첩 구조를 가질 수 있다.</p>
<h3 id="예시-문서">예시 문서</h3>
<pre><code class="language-json">{
  &quot;id&quot;: &quot;user123&quot;,
  &quot;name&quot;: &quot;홍길동&quot;,
  &quot;email&quot;: &quot;hong@example.com&quot;,
  &quot;orders&quot;: [
    { &quot;id&quot;: &quot;ord1&quot;, &quot;date&quot;: &quot;2023-01-15&quot; },
    { &quot;id&quot;: &quot;ord2&quot;, &quot;date&quot;: &quot;2023-02-20&quot; }
  ]
}</code></pre>
<h2 id="주요-특징-3">주요 특징</h2>
<table>
<thead>
<tr>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>유연한 스키마: 문서마다 다른 구조 가능</td>
</tr>
<tr>
<td>복잡한 데이터 구조의 자연스러운 표현</td>
</tr>
<tr>
<td>개발자 친화적인 데이터 모델</td>
</tr>
<tr>
<td>수평적 확장성 및 복제 지원</td>
</tr>
<tr>
<td>강력한 쿼리 기능: 인덱싱, 집계 등</td>
</tr>
</tbody></table>
<h2 id="대표-시스템-및-활용">대표 시스템 및 활용</h2>
<table>
<thead>
<tr>
<th>대표 Document DB</th>
</tr>
</thead>
<tbody><tr>
<td>MongoDB</td>
</tr>
<tr>
<td>Couchbase</td>
</tr>
<tr>
<td>Firebase Firestore</td>
</tr>
<tr>
<td>Amazon DocumentDB</td>
</tr>
</tbody></table>
<p>주요 활용 분야는 웹 애플리케이션, 콘텐츠 관리 시스템, 카탈로그, 사용자 프로필 관리 등이다.</p>
<hr>
<h1 id="컬럼형-데이터베이스">컬럼형 데이터베이스</h1>
<h2 id="작동-원리-2">작동 원리</h2>
<p>컬럼형 데이터베이스는 데이터를 행이 아닌 컬럼 단위로 저장한다. 관련 컬럼들은 <strong>컬럼 패밀리</strong>로 그룹화된다. 분석 쿼리에서 필요한 컬럼만 효율적으로 읽을 수 있다는 장점이 있다.</p>
<h2 id="주요-특징-4">주요 특징</h2>
<table>
<thead>
<tr>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>대규모 데이터 분석에 최적화</td>
</tr>
<tr>
<td>고도의 확장성: 페타바이트 규모</td>
</tr>
<tr>
<td>희소 매트릭스 효율적 처리</td>
</tr>
<tr>
<td>데이터 압축률이 높음</td>
</tr>
<tr>
<td>분산 아키텍처 기본 지원</td>
</tr>
</tbody></table>
<h2 id="대표-시스템-2">대표 시스템</h2>
<table>
<thead>
<tr>
<th>시스템</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Apache Cassandra</td>
<td>높은 가용성, 선형적 확장성</td>
</tr>
<tr>
<td>HBase</td>
<td>Hadoop 에코시스템 기반</td>
</tr>
<tr>
<td>Google Bigtable</td>
<td>구글의 대규모 데이터 처리 시스템</td>
</tr>
<tr>
<td>ScyllaDB</td>
<td>고성능 Cassandra 호환 시스템</td>
</tr>
</tbody></table>
<h2 id="주요-활용-분야-1">주요 활용 분야</h2>
<table>
<thead>
<tr>
<th>활용 분야</th>
</tr>
</thead>
<tbody><tr>
<td>시계열 데이터</td>
</tr>
<tr>
<td>센서 데이터</td>
</tr>
<tr>
<td>로그 분석</td>
</tr>
<tr>
<td>대규모 분석 시스템</td>
</tr>
</tbody></table>
<hr>
<h1 id="그래프-db">그래프 DB</h1>
<p>그래프 DB는 실제 세계의 연결 구조를 노드와 관계로 모델링한다.</p>
<table>
<thead>
<tr>
<th>요소</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>노드와 관계</td>
<td>실제 세계의 연결 구조를 노드(개체)와 엣지(관계)로 자연스럽게 모델링</td>
</tr>
<tr>
<td>관계 탐색</td>
<td>복잡한 관계를 효율적으로 탐색하는 쿼리 성능 우수</td>
</tr>
<tr>
<td>속성 그래프</td>
<td>노드와 관계 모두에 속성을 부여할 수 있는 유연한 모델</td>
</tr>
<tr>
<td>그래프 쿼리</td>
<td>Cypher, Gremlin 등 그래프 전용 쿼리 언어 지원</td>
</tr>
</tbody></table>
<h2 id="대표-시스템-3">대표 시스템</h2>
<table>
<thead>
<tr>
<th>대표 그래프 DB</th>
</tr>
</thead>
<tbody><tr>
<td>Neo4j</td>
</tr>
<tr>
<td>Amazon Neptune</td>
</tr>
<tr>
<td>JanusGraph</td>
</tr>
<tr>
<td>ArangoDB</td>
</tr>
</tbody></table>
<h2 id="주요-활용-분야-2">주요 활용 분야</h2>
<table>
<thead>
<tr>
<th>활용 분야</th>
</tr>
</thead>
<tbody><tr>
<td>소셜 네트워크 분석</td>
</tr>
<tr>
<td>추천 엔진</td>
</tr>
<tr>
<td>사기 탐지</td>
</tr>
<tr>
<td>지식 그래프</td>
</tr>
<tr>
<td>네트워크 및 IT 운영 분석</td>
</tr>
</tbody></table>
<hr>
<h1 id="newsql의-출현">NewSQL의 출현</h1>
<p>NewSQL은 전통적인 관계형 데이터베이스의 강점과 NoSQL의 확장성을 결합한 데이터베이스이다.</p>
<table>
<thead>
<tr>
<th>핵심 요소</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>관계형 모델</td>
<td>SQL 및 ACID 트랜잭션 지원</td>
</tr>
<tr>
<td>수평적 확장성</td>
<td>NoSQL 수준의 분산 아키텍처</td>
</tr>
<tr>
<td>고성능 처리</td>
<td>트랜잭션과 분석 워크로드 동시 최적화</td>
</tr>
</tbody></table>
<h2 id="주요-특징-5">주요 특징</h2>
<table>
<thead>
<tr>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>분산 SQL 쿼리 처리</td>
</tr>
<tr>
<td>자동 샤딩(Sharding) 지원</td>
</tr>
<tr>
<td>실시간 분석 기능</td>
</tr>
<tr>
<td>클라우드 네이티브 설계</td>
</tr>
</tbody></table>
<h2 id="대표-시스템-4">대표 시스템</h2>
<table>
<thead>
<tr>
<th>시스템</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Google Spanner</td>
<td>글로벌 분산 트랜잭션</td>
</tr>
<tr>
<td>CockroachDB</td>
<td>Spanner에서 영감을 받은 오픈소스 DB</td>
</tr>
<tr>
<td>VoltDB</td>
<td>인메모리 트랜잭션 처리</td>
</tr>
<tr>
<td>TiDB</td>
<td>MySQL 호환 분산 데이터베이스</td>
</tr>
</tbody></table>
<p>NewSQL은 미션 크리티컬한 트랜잭션 처리와 대규모 데이터 처리를 동시에 요구하는 현대적 애플리케이션에 적합하다.</p>
<hr>
<h1 id="인메모리-dbms">인메모리 DBMS</h1>
<h2 id="기본-개념">기본 개념</h2>
<p>인메모리 DBMS는 주로 디스크가 아닌 메인 메모리(RAM)에 데이터를 저장하고 처리하는 데이터베이스 시스템이다. 디스크 I/O 병목을 제거하여 매우 빠른 처리 속도를 제공한다.</p>
<h2 id="주요-특징-6">주요 특징</h2>
<table>
<thead>
<tr>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>초고속 데이터 접근: 디스크 대비 수십~수백 배</td>
</tr>
<tr>
<td>실시간 데이터 처리 및 분석</td>
</tr>
<tr>
<td>복잡한 쿼리의 빠른 실행</td>
</tr>
<tr>
<td>낮은 지연 시간(latency)</td>
</tr>
</tbody></table>
<h2 id="대표-시스템-5">대표 시스템</h2>
<table>
<thead>
<tr>
<th>시스템</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Redis</td>
<td>오픈소스 인메모리 키-값 저장소</td>
</tr>
<tr>
<td>SAP HANA</td>
<td>기업용 인메모리 분석 플랫폼</td>
</tr>
<tr>
<td>MemSQL(SingleStore)</td>
<td>SQL 기반 분산 인메모리 DB</td>
</tr>
<tr>
<td>VoltDB</td>
<td>고성능 트랜잭션 처리</td>
</tr>
</tbody></table>
<h2 id="주요-활용-분야-3">주요 활용 분야</h2>
<table>
<thead>
<tr>
<th>활용 분야</th>
</tr>
</thead>
<tbody><tr>
<td>실시간 분석</td>
</tr>
<tr>
<td>금융 거래</td>
</tr>
<tr>
<td>게임</td>
</tr>
<tr>
<td>IoT</td>
</tr>
<tr>
<td>실시간 대시보드</td>
</tr>
</tbody></table>
<hr>
<h1 id="데이터베이스의-활용-변화">데이터베이스의 활용 변화</h1>
<p>데이터베이스의 활용 영역은 단순 트랜잭션 처리에서 실시간 분석, AI 기반 의사결정, 엣지 컴퓨팅까지 확장되고 있다.</p>
<table>
<thead>
<tr>
<th>활용 영역</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>웹 애플리케이션</td>
<td>초기 인터넷과 웹사이트를 위한 단순 데이터 저장</td>
</tr>
<tr>
<td>소셜 미디어</td>
<td>대규모 사용자 관계와 상호작용 데이터 처리</td>
</tr>
<tr>
<td>빅데이터</td>
<td>페타바이트 규모의 구조화/비구조화 데이터 분석</td>
</tr>
<tr>
<td>IoT 센서 데이터</td>
<td>수백만 디바이스의 실시간 데이터 수집과 처리</td>
</tr>
<tr>
<td>AI 및 머신러닝</td>
<td>모델 훈련 및 추론을 위한 대규모 데이터 저장소</td>
</tr>
</tbody></table>
<p>현대 애플리케이션은 다양한 데이터 유형과 워크로드를 처리할 수 있는 다기능 데이터 플랫폼을 요구한다.</p>
<hr>
<h1 id="데이터베이스와-클라우드">데이터베이스와 클라우드</h1>
<h2 id="클라우드-전환의-배경">클라우드 전환의 배경</h2>
<p>기업들은 자체 데이터센터에서 데이터베이스를 운영·유지보수하는 대신, 클라우드 제공업체가 관리하는 서비스형 데이터베이스(DBaaS)로 전환하고 있다.</p>
<h2 id="주요-이점">주요 이점</h2>
<table>
<thead>
<tr>
<th>이점</th>
</tr>
</thead>
<tbody><tr>
<td>초기 투자 비용(CapEx) 감소</td>
</tr>
<tr>
<td>자동화된 확장성 및 고가용성</td>
</tr>
<tr>
<td>관리 부담 및 운영 비용 감소</td>
</tr>
<tr>
<td>신속한 배포 및 테스트 환경 구성</td>
</tr>
<tr>
<td>최신 기술로의 지속적 업그레이드</td>
</tr>
</tbody></table>
<h2 id="주요-클라우드-제공업체">주요 클라우드 제공업체</h2>
<table>
<thead>
<tr>
<th>클라우드 제공업체</th>
</tr>
</thead>
<tbody><tr>
<td>Amazon Web Services(AWS)</td>
</tr>
<tr>
<td>Microsoft Azure</td>
</tr>
<tr>
<td>Google Cloud Platform(GCP)</td>
</tr>
<tr>
<td>IBM Cloud</td>
</tr>
<tr>
<td>Oracle Cloud</td>
</tr>
</tbody></table>
<p>2023년 기준 새로 배포되는 데이터베이스의 75% 이상이 클라우드 환경에서 구축되고 있으며, 이 비율은 계속 증가할 전망이라고 설명한다.</p>
<hr>
<h1 id="클라우드-dbms-개요">클라우드 DBMS 개요</h1>
<h2 id="관리형-서비스">관리형 서비스</h2>
<p>패치, 백업, 확장, 고가용성 설정 등 데이터베이스 관리 작업을 클라우드 제공업체가 자동으로 처리한다.</p>
<h2 id="탄력적-확장성">탄력적 확장성</h2>
<p>필요에 따라 컴퓨팅 및 스토리지 리소스를 자동으로 확장하거나 축소하여 비용을 최적화할 수 있다.</p>
<h2 id="서버리스-옵션">서버리스 옵션</h2>
<p>사용한 만큼만 비용을 지불하는 서버리스 데이터베이스 옵션을 통해 인프라 관리 부담을 제거한다.</p>
<h2 id="멀티-모델-지원">멀티 모델 지원</h2>
<p>하나의 서비스에서 관계형, 문서, 그래프 등 다양한 데이터 모델을 지원하여 애플리케이션 개발을 단순화한다.</p>
<h2 id="글로벌-분산">글로벌 분산</h2>
<p>전 세계 데이터 센터에 데이터를 자동으로 복제하여 지연 시간을 줄이고 데이터 주권 준수를 지원한다.</p>
<h2 id="서비스-통합">서비스 통합</h2>
<p>분석, 머신러닝, IoT 등 다른 클라우드 서비스와 통합하여 데이터 활용 가치를 높인다.</p>
<hr>
<h1 id="aws의-주요-db-서비스">AWS의 주요 DB 서비스</h1>
<h2 id="amazon-rds">Amazon RDS</h2>
<p>관리형 관계형 데이터베이스 서비스이다. MySQL, PostgreSQL, Oracle, SQL Server, MariaDB 등 다양한 엔진을 지원한다.</p>
<table>
<thead>
<tr>
<th>주요 기능</th>
</tr>
</thead>
<tbody><tr>
<td>자동 백업 및 패치 적용</td>
</tr>
<tr>
<td>다중 AZ 배포를 통한 고가용성</td>
</tr>
<tr>
<td>읽기 전용 복제본 지원</td>
</tr>
</tbody></table>
<h2 id="amazon-dynamodb">Amazon DynamoDB</h2>
<p>완전 관리형 NoSQL 데이터베이스 서비스이다. 무제한 확장성과 밀리초 단위 성능을 제공한다.</p>
<table>
<thead>
<tr>
<th>주요 기능</th>
</tr>
</thead>
<tbody><tr>
<td>서버리스 아키텍처</td>
</tr>
<tr>
<td>자동 다중 리전 복제</td>
</tr>
<tr>
<td>온디맨드 용량 모드</td>
</tr>
</tbody></table>
<h2 id="amazon-aurora">Amazon Aurora</h2>
<p>MySQL 및 PostgreSQL과 호환되는 클라우드 네이티브 관계형 데이터베이스이다. 기존 엔진보다 최대 5배 빠른 성능을 제공한다고 설명한다.</p>
<table>
<thead>
<tr>
<th>주요 기능</th>
</tr>
</thead>
<tbody><tr>
<td>분산 스토리지 아키텍처</td>
</tr>
<tr>
<td>자동 복구 기능</td>
</tr>
<tr>
<td>글로벌 데이터베이스 지원</td>
</tr>
</tbody></table>
<h2 id="기타-aws-db-서비스">기타 AWS DB 서비스</h2>
<table>
<thead>
<tr>
<th>서비스</th>
<th>용도</th>
</tr>
</thead>
<tbody><tr>
<td>Amazon Redshift</td>
<td>데이터 웨어하우스</td>
</tr>
<tr>
<td>ElastiCache</td>
<td>인메모리 캐싱</td>
</tr>
<tr>
<td>Neptune</td>
<td>그래프 DB</td>
</tr>
<tr>
<td>DocumentDB</td>
<td>MongoDB 호환</td>
</tr>
<tr>
<td>Timestream</td>
<td>시계열 DB</td>
</tr>
</tbody></table>
<hr>
<h1 id="google-cloud-db-서비스">Google Cloud DB 서비스</h1>
<h2 id="cloud-spanner">Cloud Spanner</h2>
<p>글로벌 분산 트랜잭션을 지원하는 수평적 확장 관계형 데이터베이스이다. 강력한 일관성과 99.999% 가용성을 제공한다.</p>
<table>
<thead>
<tr>
<th>주요 기능</th>
</tr>
</thead>
<tbody><tr>
<td>글로벌 트랜잭션 일관성</td>
</tr>
<tr>
<td>자동 샤딩 및 복제</td>
</tr>
<tr>
<td>SQL 인터페이스</td>
</tr>
</tbody></table>
<h2 id="cloud-bigtable">Cloud Bigtable</h2>
<p>대규모 분석 및 운영 워크로드를 위한 완전 관리형 NoSQL 데이터베이스 서비스이다.</p>
<table>
<thead>
<tr>
<th>주요 기능</th>
</tr>
</thead>
<tbody><tr>
<td>선형적 확장성</td>
</tr>
<tr>
<td>HBase API 호환성</td>
</tr>
<tr>
<td>빅데이터 워크로드 최적화</td>
</tr>
</tbody></table>
<h2 id="cloud-sql">Cloud SQL</h2>
<p>MySQL, PostgreSQL, SQL Server를 위한 완전 관리형 관계형 데이터베이스 서비스이다.</p>
<table>
<thead>
<tr>
<th>주요 기능</th>
</tr>
</thead>
<tbody><tr>
<td>자동 백업 및 복제</td>
</tr>
<tr>
<td>고가용성 구성</td>
</tr>
<tr>
<td>암호화 및 VPC 지원</td>
</tr>
</tbody></table>
<h2 id="bigquery">BigQuery</h2>
<p>서버리스 엔터프라이즈 데이터 웨어하우스이다. 페타바이트 규모의 데이터를 실시간으로 분석할 수 있다.</p>
<table>
<thead>
<tr>
<th>주요 기능</th>
</tr>
</thead>
<tbody><tr>
<td>SQL 기반 분석</td>
</tr>
<tr>
<td>머신러닝 통합</td>
</tr>
<tr>
<td>실시간 스트리밍 분석</td>
</tr>
</tbody></table>
<h2 id="기타-google-cloud-db-서비스">기타 Google Cloud DB 서비스</h2>
<table>
<thead>
<tr>
<th>서비스</th>
<th>용도</th>
</tr>
</thead>
<tbody><tr>
<td>Firestore</td>
<td>문서형 DB</td>
</tr>
<tr>
<td>Memorystore</td>
<td>인메모리 DB</td>
</tr>
<tr>
<td>Firebase Realtime Database</td>
<td>실시간 동기화 DB</td>
</tr>
</tbody></table>
<hr>
<h1 id="microsoft-azure-db-서비스">Microsoft Azure DB 서비스</h1>
<h2 id="azure-sql-database">Azure SQL Database</h2>
<p>Microsoft SQL Server 기반의 완전 관리형 관계형 데이터베이스 서비스이다.</p>
<table>
<thead>
<tr>
<th>주요 기능</th>
</tr>
</thead>
<tbody><tr>
<td>지능형 성능 최적화</td>
</tr>
<tr>
<td>자동 확장 및 백업</td>
</tr>
<tr>
<td>고급 보안 기능</td>
</tr>
<tr>
<td>서버리스 컴퓨팅 옵션</td>
</tr>
</tbody></table>
<h2 id="azure-cosmos-db">Azure Cosmos DB</h2>
<p>글로벌 분산 멀티 모델 데이터베이스 서비스이다. 다양한 데이터 모델과 API를 지원한다.</p>
<table>
<thead>
<tr>
<th>주요 기능</th>
</tr>
</thead>
<tbody><tr>
<td>SQL, MongoDB, Cassandra, Gremlin, Table API 지원</td>
</tr>
<tr>
<td>글로벌 분산 및 다중 지역 쓰기</td>
</tr>
<tr>
<td>밀리초 단위 응답 시간 SLA</td>
</tr>
<tr>
<td>자동 인덱싱 및 확장</td>
</tr>
</tbody></table>
<h2 id="azure-database-for-mysqlpostgresql">Azure Database for MySQL/PostgreSQL</h2>
<p>오픈소스 데이터베이스를 위한 완전 관리형 서비스이다.</p>
<table>
<thead>
<tr>
<th>주요 기능</th>
</tr>
</thead>
<tbody><tr>
<td>자동 패치 및 백업</td>
</tr>
<tr>
<td>고가용성 구성</td>
</tr>
<tr>
<td>확장 가능한 스토리지</td>
</tr>
<tr>
<td>고급 보안 기능</td>
</tr>
</tbody></table>
<h2 id="기타-azure-db-서비스">기타 Azure DB 서비스</h2>
<table>
<thead>
<tr>
<th>서비스</th>
<th>용도</th>
</tr>
</thead>
<tbody><tr>
<td>Azure Synapse Analytics</td>
<td>데이터 웨어하우스</td>
</tr>
<tr>
<td>Azure Cache for Redis</td>
<td>인메모리 DB</td>
</tr>
<tr>
<td>Azure Database for MariaDB</td>
<td>관리형 MariaDB</td>
</tr>
</tbody></table>
<hr>
<h1 id="클라우드-dbms-특징">클라우드 DBMS 특징</h1>
<h2 id="무중단-백업-및-복구">무중단 백업 및 복구</h2>
<p>자동화된 백업 시스템과 시점 복구(Point-in-Time Recovery)를 통해 데이터 손실 위험을 최소화한다. 백업 작업이 성능에 영향을 미치지 않으며 재해 복구 계획도 쉽게 구현할 수 있다.</p>
<h2 id="자동-확장-및-성능-최적화">자동 확장 및 성능 최적화</h2>
<p>워크로드 증가에 따라 리소스를 자동 확장하고, AI 기반 성능 모니터링과 최적화 도구로 데이터베이스 성능을 지속적으로 개선한다.</p>
<p>제공 기능:</p>
<table>
<thead>
<tr>
<th>기능</th>
</tr>
</thead>
<tbody><tr>
<td>쿼리 분석</td>
</tr>
<tr>
<td>인덱스 추천</td>
</tr>
<tr>
<td>자동 튜닝</td>
</tr>
</tbody></table>
<h2 id="향상된-보안-및-규정-준수">향상된 보안 및 규정 준수</h2>
<p>저장 및 전송 중 암호화, 세밀한 접근 제어, 위협 감지 등 다양한 보안 기능이 기본 제공된다.</p>
<p>지원하는 규정 준수 예시:</p>
<table>
<thead>
<tr>
<th>규정</th>
</tr>
</thead>
<tbody><tr>
<td>GDPR</td>
</tr>
<tr>
<td>HIPAA</td>
</tr>
<tr>
<td>PCI DSS</td>
</tr>
</tbody></table>
<h2 id="고가용성-및-재해-복구">고가용성 및 재해 복구</h2>
<p>다중 가용 영역 및 지역 복제, 자동 장애 조치(failover), 상시 가동 아키텍처 등을 통해 최대 99.999%의 가용성을 보장한다. 자연재해나 지역 장애에도 서비스 연속성을 유지할 수 있다.</p>
<hr>
<h1 id="온프레미스-db-vs-클라우드-db">온프레미스 DB vs 클라우드 DB</h1>
<h2 id="온프레미스-데이터베이스">온프레미스 데이터베이스</h2>
<h3 id="장점">장점</h3>
<table>
<thead>
<tr>
<th>장점</th>
</tr>
</thead>
<tbody><tr>
<td>데이터에 대한 완전한 제어권</td>
</tr>
<tr>
<td>네트워크 지연 시간 최소화</td>
</tr>
<tr>
<td>라이선스 기반 일회성 비용 구조</td>
</tr>
<tr>
<td>클라우드 의존성 없음</td>
</tr>
</tbody></table>
<h3 id="단점">단점</h3>
<table>
<thead>
<tr>
<th>단점</th>
</tr>
</thead>
<tbody><tr>
<td>높은 초기 투자 비용</td>
</tr>
<tr>
<td>확장성 제한 및 복잡성</td>
</tr>
<tr>
<td>인력 및 유지보수 부담</td>
</tr>
<tr>
<td>재해 복구 구현 어려움</td>
</tr>
</tbody></table>
<h2 id="클라우드-데이터베이스">클라우드 데이터베이스</h2>
<h3 id="장점-1">장점</h3>
<table>
<thead>
<tr>
<th>장점</th>
</tr>
</thead>
<tbody><tr>
<td>빠른 배포 및 확장성</td>
</tr>
<tr>
<td>사용량 기반 비용 구조</td>
</tr>
<tr>
<td>관리 부담 최소화</td>
</tr>
<tr>
<td>내장된 고가용성 및 재해 복구</td>
</tr>
</tbody></table>
<h3 id="단점-1">단점</h3>
<table>
<thead>
<tr>
<th>단점</th>
</tr>
</thead>
<tbody><tr>
<td>데이터 주권 및 규제 이슈</td>
</tr>
<tr>
<td>네트워크 의존성 및 지연 가능성</td>
</tr>
<tr>
<td>장기적인 비용 증가 가능성</td>
</tr>
<tr>
<td>벤더 종속성(Lock-in) 위험</td>
</tr>
</tbody></table>
<p>많은 기업은 두 접근 방식의 장점을 결합한 하이브리드 방식을 채택하고 있다. 워크로드 특성과 비즈니스 요구사항에 따라 최적의 배포 모델을 선택한다.</p>
<hr>
<h1 id="멀티-클라우드·하이브리드-db">멀티 클라우드·하이브리드 DB</h1>
<h2 id="멀티-클라우드-전략">멀티 클라우드 전략</h2>
<p>멀티 클라우드는 여러 클라우드 제공업체의 데이터베이스 서비스를 동시에 활용하는 접근 방식이다.</p>
<h3 id="장점-2">장점</h3>
<table>
<thead>
<tr>
<th>장점</th>
</tr>
</thead>
<tbody><tr>
<td>벤더 종속성 감소</td>
</tr>
<tr>
<td>각 제공업체의 강점 활용</td>
</tr>
<tr>
<td>지역별 최적 서비스 선택</td>
</tr>
<tr>
<td>협상력 및 위험 분산</td>
</tr>
</tbody></table>
<h3 id="단점-2">단점</h3>
<table>
<thead>
<tr>
<th>단점</th>
</tr>
</thead>
<tbody><tr>
<td>일관된 관리의 복잡성 증가</td>
</tr>
<tr>
<td>데이터 동기화 복잡성 증가</td>
</tr>
</tbody></table>
<h2 id="하이브리드-데이터베이스-환경">하이브리드 데이터베이스 환경</h2>
<p>하이브리드 데이터베이스 환경은 온프레미스와 클라우드 데이터베이스를 함께 운영하는 방식이다.</p>
<h3 id="장점-3">장점</h3>
<table>
<thead>
<tr>
<th>장점</th>
</tr>
</thead>
<tbody><tr>
<td>민감한 데이터는 온프레미스에 보관</td>
</tr>
<tr>
<td>탄력적 워크로드는 클라우드로 이동</td>
</tr>
<tr>
<td>점진적 클라우드 마이그레이션</td>
</tr>
<tr>
<td>기존 투자 활용과 혁신 균형</td>
</tr>
</tbody></table>
<h3 id="주요-과제">주요 과제</h3>
<table>
<thead>
<tr>
<th>과제</th>
</tr>
</thead>
<tbody><tr>
<td>데이터 일관성 유지</td>
</tr>
<tr>
<td>복잡한 네트워크 구성</td>
</tr>
</tbody></table>
<p>최근에는 Kubernetes 기반 데이터베이스 운영과 같은 컨테이너화된 접근 방식이 등장하여 환경 간 이식성을 높이고 있다. 데이터베이스 가상화 및 추상화 레이어를 통해 복잡성을 관리하는 솔루션도 발전하고 있다.</p>
<hr>
<h1 id="dbms-발전의-핵심-트렌드">DBMS 발전의 핵심 트렌드</h1>
<h2 id="오픈소스-확산">오픈소스 확산</h2>
<p>엔터프라이즈 영역에서도 오픈소스 데이터베이스 채택이 급증하고 있다.</p>
<table>
<thead>
<tr>
<th>주요 흐름</th>
</tr>
</thead>
<tbody><tr>
<td>PostgreSQL의 기업용 워크로드 확대</td>
</tr>
<tr>
<td>MongoDB, Redis 등 NoSQL 솔루션의 성숙</td>
</tr>
<tr>
<td>클라우드 제공업체의 오픈소스 호환 서비스</td>
</tr>
<tr>
<td>개발자 커뮤니티 중심 혁신 가속화</td>
</tr>
</tbody></table>
<h2 id="클라우드-전환-가속">클라우드 전환 가속</h2>
<p>기업들이 자체 데이터센터에서 클라우드 환경으로 데이터베이스를 이전하는 추세가 가속화되고 있다.</p>
<table>
<thead>
<tr>
<th>주요 흐름</th>
</tr>
</thead>
<tbody><tr>
<td>DBaaS(Database as a Service) 모델 확산</td>
</tr>
<tr>
<td>서버리스 데이터베이스 도입 증가</td>
</tr>
<tr>
<td>멀티 클라우드 및 하이브리드 전략 채택</td>
</tr>
<tr>
<td>마이그레이션 도구 및 서비스 발전</td>
</tr>
</tbody></table>
<h2 id="다양한-데이터-포맷-지원">다양한 데이터 포맷 지원</h2>
<p>정형 데이터 외에도 다양한 비정형/반정형 데이터를 처리할 수 있는 능력이 중요해지고 있다.</p>
<table>
<thead>
<tr>
<th>주요 흐름</th>
</tr>
</thead>
<tbody><tr>
<td>JSON, XML, 지리공간 데이터 네이티브 지원</td>
</tr>
<tr>
<td>텍스트, 이미지, 오디오 분석 기능 통합</td>
</tr>
<tr>
<td>그래프 데이터 및 관계 분석 강화</td>
</tr>
<tr>
<td>멀티 모델 데이터베이스 증가</td>
</tr>
</tbody></table>
<hr>
<h1 id="데이터베이스-선택-기준">데이터베이스 선택 기준</h1>
<h2 id="용도-파악">용도 파악</h2>
<p>트랜잭션 처리(OLTP), 분석(OLAP), 혼합(HTAP) 중 어떤 워크로드인지 파악해야 한다.</p>
<h2 id="데이터-특성">데이터 특성</h2>
<p>정형 데이터인지 비정형 데이터인지, 데이터 크기와 성장률은 어느 정도인지, 관계 복잡성은 어떤지 고려해야 한다.</p>
<h2 id="확장성-요구">확장성 요구</h2>
<p>예상 사용자 수, 트래픽 패턴, 수직/수평 확장 필요성을 고려해야 한다.</p>
<h2 id="일관성-vs-가용성-요구사항">일관성 vs 가용성 요구사항</h2>
<p>CAP 이론에 따라 강한 일관성(CP)이 중요한지, 가용성과 파티션 허용(AP)이 중요한지 고려해야 한다.</p>
<p>예를 들어 금융 거래는 강한 일관성이 필요하지만, 소셜 미디어는 일시적 불일치를 허용할 수 있다.</p>
<h2 id="쿼리-패턴-및-성능-요구사항">쿼리 패턴 및 성능 요구사항</h2>
<p>복잡한 조인이 필요한지, 단순 키-값 검색이 중심인지, 실시간 응답이 필요한지에 따라 적합한 DB가 달라진다.</p>
<p>명확히 정의해야 할 성능 지표:</p>
<table>
<thead>
<tr>
<th>성능 지표</th>
</tr>
</thead>
<tbody><tr>
<td>지연 시간</td>
</tr>
<tr>
<td>처리량</td>
</tr>
<tr>
<td>동시 사용자 수</td>
</tr>
</tbody></table>
<h2 id="기술-생태계-및-개발자-역량">기술 생태계 및 개발자 역량</h2>
<p>팀의 기존 기술 스택과 호환되는지, 개발자가 얼마나 익숙한지, 커뮤니티 지원과 도구 생태계가 충분한지 고려해야 한다.</p>
<p>좋은 데이터베이스라도 팀이 효과적으로 활용할 수 없다면 가치가 제한된다.</p>
<hr>
<h1 id="주요-dbms-비교-표">주요 DBMS 비교 표</h1>
<table>
<thead>
<tr>
<th>유형</th>
<th>대표 시스템</th>
<th>강점</th>
<th>약점</th>
<th>주요 사용 사례</th>
</tr>
</thead>
<tbody><tr>
<td>관계형</td>
<td>Oracle, MySQL, PostgreSQL</td>
<td>트랜잭션 처리, 데이터 일관성, SQL 표준</td>
<td>수평적 확장성 제한, 스키마 변경 어려움</td>
<td>금융, ERP, CRM 시스템</td>
</tr>
<tr>
<td>문서형</td>
<td>MongoDB, Couchbase</td>
<td>유연한 스키마, 개발 생산성, JSON 지원</td>
<td>복잡한 조인, 트랜잭션 처리 제한</td>
<td>콘텐츠 관리, 모바일 앱, 카탈로그</td>
</tr>
<tr>
<td>키-값</td>
<td>Redis, DynamoDB</td>
<td>초고속 응답, 단순성, 확장성</td>
<td>복잡한 쿼리 제한, 데이터 관계 표현 어려움</td>
<td>캐싱, 세션 관리, 실시간 분석</td>
</tr>
<tr>
<td>컬럼형</td>
<td>Cassandra, HBase</td>
<td>대규모 쓰기/분석, 수평적 확장성</td>
<td>실시간 읽기 성능, 복잡한 구성</td>
<td>IoT 데이터, 로그 분석, 시계열 데이터</td>
</tr>
<tr>
<td>그래프</td>
<td>Neo4j, Neptune</td>
<td>관계 탐색, 연결 데이터 분석</td>
<td>대규모 확장성, 학습 곡선</td>
<td>소셜 네트워크, 추천 엔진, 사기 탐지</td>
</tr>
<tr>
<td>인메모리</td>
<td>Redis, SAP HANA</td>
<td>초고속 성능, 실시간 처리</td>
<td>비용, 메모리 제한, 지속성 관리</td>
<td>실시간 분석, 캐싱, 게임 리더보드</td>
</tr>
<tr>
<td>NewSQL</td>
<td>Google Spanner, CockroachDB</td>
<td>확장성 + SQL + 트랜잭션</td>
<td>성숙도, 복잡성, 비용</td>
<td>글로벌 금융 시스템, 고확장성 앱</td>
</tr>
</tbody></table>
<p>각 데이터베이스 유형은 고유한 강점과 약점을 가지고 있으며, 특정 워크로드와 사용 사례에 최적화되어 있다.</p>
<p>많은 현대 애플리케이션은 다양한 유형의 데이터베이스를 함께 사용하는 <strong>폴리글랏 퍼시스턴스(Polyglot Persistence)</strong> 접근 방식을 채택하고 있다.</p>
<hr>
<h1 id="미래의-데이터베이스-전망">미래의 데이터베이스 전망</h1>
<h2 id="ai-기반-자율-데이터베이스">AI 기반 자율 데이터베이스</h2>
<p>인공지능이 자동으로 데이터베이스를 튜닝, 최적화, 관리하는 시스템이 확산되고 있다.</p>
<p>자동화되는 영역:</p>
<table>
<thead>
<tr>
<th>영역</th>
</tr>
</thead>
<tbody><tr>
<td>쿼리 최적화</td>
</tr>
<tr>
<td>인덱스 생성</td>
</tr>
<tr>
<td>리소스 할당</td>
</tr>
<tr>
<td>보안 위협 감지</td>
</tr>
</tbody></table>
<p>DBA의 역할도 단순 운영보다 전략적 방향으로 진화하고 있다.</p>
<h2 id="서버리스-데이터베이스의-확산">서버리스 데이터베이스의 확산</h2>
<p>인프라 관리 없이 필요한 만큼만 사용하고 비용을 지불하는 서버리스 DB가 주류화될 전망이다. 개발자는 데이터베이스 운영보다 비즈니스 로직과 애플리케이션 개발에 집중할 수 있다.</p>
<h2 id="엣지-컴퓨팅과-분산-데이터베이스">엣지 컴퓨팅과 분산 데이터베이스</h2>
<p>IoT 장치와 5G 네트워크 확산으로 데이터 생성 지점에 가까운 엣지 위치에서 데이터를 처리하는 분산 데이터베이스 시스템이 중요해질 것이다.</p>
<p>핵심 과제는 중앙 클라우드와 엣지 노드 간의 효율적인 데이터 동기화이다.</p>
<h2 id="데이터베이스와-ai의-통합">데이터베이스와 AI의 통합</h2>
<p>데이터베이스 시스템 내에서 직접 머신러닝 모델을 실행하고 학습하는 기능이 강화될 것이다.</p>
<p>데이터 이동 없이 데이터베이스 내부에서 분석과 예측을 수행하는 <strong>AI in DB</strong> 개념이 발전할 것이다.</p>
<h2 id="블록체인-기반-분산-데이터베이스">블록체인 기반 분산 데이터베이스</h2>
<p>높은 투명성과 변조 방지가 필요한 애플리케이션을 위한 블록체인 기반 데이터베이스 시스템이 발전할 것이다.</p>
<p>중요성이 증가할 산업:</p>
<table>
<thead>
<tr>
<th>산업</th>
</tr>
</thead>
<tbody><tr>
<td>공급망</td>
</tr>
<tr>
<td>금융</td>
</tr>
<tr>
<td>의료</td>
</tr>
</tbody></table>
<hr>
<h1 id="결론-데이터베이스의-진화-방향">결론: 데이터베이스의 진화 방향</h1>
<h2 id="다종-복합-db-환경-도래">다종 복합 DB 환경 도래</h2>
<p>단일 데이터베이스 시스템으로 모든 요구사항을 충족하는 시대는 지나고 있다. 현대적인 데이터 아키텍처는 다양한 유형의 데이터베이스를 목적에 맞게 조합하는 방향으로 진화하고 있다.</p>
<table>
<thead>
<tr>
<th>변화</th>
</tr>
</thead>
<tbody><tr>
<td>워크로드별 최적화된 데이터 저장소 활용</td>
</tr>
<tr>
<td>마이크로서비스와 연계된 분산 데이터 관리</td>
</tr>
<tr>
<td>데이터 통합 및 거버넌스의 중요성 증가</td>
</tr>
</tbody></table>
<h2 id="데이터-활용-방식의-변화">데이터 활용 방식의 변화</h2>
<p>데이터베이스는 단순한 저장소를 넘어 비즈니스 가치 창출의 핵심 도구로 진화하고 있다.</p>
<table>
<thead>
<tr>
<th>변화</th>
</tr>
</thead>
<tbody><tr>
<td>실시간 인사이트 생성과 의사결정 지원</td>
</tr>
<tr>
<td>AI/ML과 결합한 예측 분석 기능 강화</td>
</tr>
<tr>
<td>데이터 중심 조직으로의 변화 가속화</td>
</tr>
<tr>
<td>데이터 민주화와 셀프 서비스 분석 확산</td>
</tr>
</tbody></table>
<p>데이터베이스 기술은 60년이 넘는 역사 동안 계속 진화해왔다. 앞으로도 클라우드, AI, IoT 등의 기술과 융합하며 발전할 것이다.</p>
<p>이러한 변화에 맞춰 데이터 전략을 수립하고 적응하는 조직이 디지털 시대의 경쟁에서 우위를 점할 수 있다.</p>
<hr>
<h1 id="참고-자료">참고 자료</h1>
<h2 id="dbms-선택-실전-사례">DBMS 선택 실전 사례</h2>
<p>다양한 산업 분야에서 비즈니스 요구사항에 따라 데이터베이스를 선택한 실제 사례를 살펴볼 수 있다.</p>
<table>
<thead>
<tr>
<th>사례</th>
</tr>
</thead>
<tbody><tr>
<td>전자상거래 플랫폼의 멀티 모델 DB 전략</td>
</tr>
<tr>
<td>금융 기관의 하이브리드 클라우드 구현</td>
</tr>
<tr>
<td>의료 기관의 데이터 보안 및 규정 준수 접근법</td>
</tr>
</tbody></table>
<hr>
<h1 id="newsql과-vector-db">NewSQL과 Vector DB</h1>
<h2 id="newsql">NewSQL</h2>
<p>NewSQL은 ACID와 수평 확장을 동시에 목표로 한다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>핵심</td>
<td>ACID + 수평 확장</td>
</tr>
<tr>
<td>대표 기술</td>
<td>CockroachDB, Google Spanner, TiDB</td>
</tr>
</tbody></table>
<hr>
<h2 id="vector-db">Vector DB</h2>
<p>Vector DB는 AI/ML 임베딩 검색에 사용된다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>핵심</td>
<td>AI/ML 임베딩 검색</td>
</tr>
<tr>
<td>대표 기술</td>
<td>Pinecone, Weaviate, Azure AI Search</td>
</tr>
<tr>
<td>주요 활용</td>
<td>RAG 패턴 핵심</td>
</tr>
</tbody></table>
<hr>
<h1 id="acid-트랜잭션">ACID 트랜잭션</h1>
<p>ACID는 관계형 DB의 핵심 보장이다.</p>
<table>
<thead>
<tr>
<th>요소</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td>Atomicity</td>
<td>전부 성공 또는 전부 실패</td>
</tr>
<tr>
<td>Consistency</td>
<td>전후 무결성 유지</td>
</tr>
<tr>
<td>Isolation</td>
<td>동시 트랜잭션 비간섭</td>
</tr>
<tr>
<td>Durability</td>
<td>커밋 후 장애에도 보존</td>
</tr>
</tbody></table>
<hr>
<h1 id="cap-정리">CAP 정리</h1>
<p>CAP는 분산 시스템에서 중요한 세 가지 특성이다.</p>
<table>
<thead>
<tr>
<th>요소</th>
<th>의미</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>C</td>
<td>Consistency</td>
<td>모든 노드 동일 데이터, 강한 일관성</td>
</tr>
<tr>
<td>A</td>
<td>Availability</td>
<td>모든 요청에 응답, 다운타임 없음</td>
</tr>
<tr>
<td>P</td>
<td>Partition Tolerance</td>
<td>네트워크 분할 시에도 동작, 분산 시스템 필수</td>
</tr>
</tbody></table>
<p>세가지를 모두 동시에 만족할 순 없다.(동시에 두개까지만 가능)</p>
<hr>
<h2 id="cap-실무-적용--서비스별-선택">CAP 실무 적용 — 서비스별 선택</h2>
<table>
<thead>
<tr>
<th>서비스</th>
<th>CAP 선택</th>
<th>일관성</th>
<th>가용성</th>
</tr>
</thead>
<tbody><tr>
<td>SQL Server</td>
<td>CP</td>
<td>강한 일관성</td>
<td>장애 시 다운타임</td>
</tr>
<tr>
<td>Cosmos DB (Strong)</td>
<td>CP</td>
<td>강한 일관성</td>
<td>쓰기 지연</td>
</tr>
<tr>
<td>Cosmos DB (Session)</td>
<td>AP (실질)</td>
<td>세션 내 일관</td>
<td>항상 응답</td>
</tr>
<tr>
<td>Cassandra</td>
<td>AP</td>
<td>최종 일관성</td>
<td>항상 응답</td>
</tr>
<tr>
<td>MongoDB (기본)</td>
<td>CP</td>
<td>강한 일관성</td>
<td>Primary 장애 시 선출 대기</td>
</tr>
</tbody></table>
<hr>
<h1 id="base-특성">BASE 특성</h1>
<p>BASE는 NoSQL에서 많이 사용하는 유연한 일관성 모델이다.</p>
<table>
<thead>
<tr>
<th>요소</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td>Basically Available</td>
<td>항상 응답, 오래된 데이터라도 응답</td>
</tr>
<tr>
<td>Soft State</td>
<td>시스템 상태가 시간에 따라 변함</td>
</tr>
<tr>
<td>Eventually Consistent</td>
<td>충분한 시간이 지나면 일관성에 도달</td>
</tr>
</tbody></table>
<hr>
<h2 id="acid-vs-base-비교">ACID vs BASE 비교</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>ACID (RDBMS)</th>
<th>BASE (NoSQL)</th>
</tr>
</thead>
<tbody><tr>
<td>일관성</td>
<td>강한 일관성 (Strong)</td>
<td>최종 일관성 (Eventual)</td>
</tr>
<tr>
<td>확장</td>
<td>수직 확장 (Scale-Up)</td>
<td>수평 확장 (Scale-Out)</td>
</tr>
<tr>
<td>스키마</td>
<td>고정 (DDL)</td>
<td>유연 (Schema-less)</td>
</tr>
<tr>
<td>트랜잭션</td>
<td>복잡한 조인 최적</td>
<td>대량 읽기/쓰기 최적</td>
</tr>
<tr>
<td>사용 사례</td>
<td>금융 · ERP · 재고</td>
<td>소셜 · IoT · 실시간 분석</td>
</tr>
</tbody></table>
<hr>
<h1 id="db-진화-요약">DB 진화 요약</h1>
<p>60년의 DB 진화를 한 장으로 요약하면 다음과 같다.</p>
<table>
<thead>
<tr>
<th>시대</th>
<th>변화</th>
<th>핵심 의미</th>
</tr>
</thead>
<tbody><tr>
<td>1960s → 1970s</td>
<td>계층형 → 관계형</td>
<td>데이터 독립성 혁명</td>
</tr>
<tr>
<td>2000s</td>
<td>NoSQL</td>
<td>스케일과 유연성 혁명</td>
</tr>
<tr>
<td>2010s</td>
<td>NewSQL</td>
<td>ACID + Scale-Out 결합 시도</td>
</tr>
<tr>
<td>2020s</td>
<td>Vector DB</td>
<td>AI/ML 시대의 검색 인프라</td>
</tr>
</tbody></table>
<p>핵심 메시지는 다음과 같다.</p>
<blockquote>
<p>정답은 없다. 워크로드에 맞는 선택이 최선이다.</p>
</blockquote>
<p>이 원칙이 이후 전체 과정의 판단 기준이 된다.</p>
<hr>
<h1 id="azure-데이터-서비스-지형도">Azure 데이터 서비스 지형도</h1>
<h2 id="azure-데이터-서비스-전체-맵">Azure 데이터 서비스 전체 맵</h2>
<p>Azure에서 선택할 수 있는 데이터 서비스는 크게 네 영역으로 나눌 수 있다.</p>
<table>
<thead>
<tr>
<th>분류</th>
<th>서비스</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>IaaS — VM 기반</td>
<td>SQL Server on VM, PG/MySQL on VM</td>
<td>100% 제어</td>
</tr>
<tr>
<td>PaaS — 관계형</td>
<td>SQL Database, SQL MI, Flexible Server</td>
<td>관리형 관계형 DB</td>
</tr>
<tr>
<td>PaaS — NoSQL</td>
<td>Cosmos DB</td>
<td>5 API, 글로벌 분산</td>
</tr>
<tr>
<td>분석 · 시계열</td>
<td>Synapse, ADX, Fabric</td>
<td>분석 및 시계열 처리</td>
</tr>
</tbody></table>
<hr>
<h2 id="sql-vm-vs-sql-db-vs-sql-mi">SQL VM vs SQL DB vs SQL MI</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>SQL VM</th>
<th>SQL DB</th>
<th>SQL MI</th>
</tr>
</thead>
<tbody><tr>
<td>관리</td>
<td>OS 직접 관리</td>
<td>완전 관리</td>
<td>거의 완전 관리</td>
</tr>
<tr>
<td>호환성</td>
<td>100%</td>
<td>일부 제한</td>
<td>99%</td>
</tr>
<tr>
<td>HA</td>
<td>AG 직접 구성</td>
<td>내장 자동</td>
<td>내장 자동</td>
</tr>
<tr>
<td>비용</td>
<td>VM + 라이선스</td>
<td>DTU / vCore</td>
<td>vCore</td>
</tr>
<tr>
<td>프로비저닝</td>
<td>수 분</td>
<td>수 분</td>
<td>약 4시간</td>
</tr>
<tr>
<td>부트캠프</td>
<td>Day 2-3</td>
<td>Day 4-5</td>
<td>이론만 (D-2)</td>
</tr>
</tbody></table>
<hr>
<h1 id="cosmos-db-개요">Cosmos DB 개요</h1>
<p>Cosmos DB는 글로벌 분산 NoSQL 서비스이다.</p>
<ul>
<li>글로벌 분산 NoSQL</li>
<li>60개 이상 Azure 리전 지원</li>
<li>10ms 미만 읽기/쓰기</li>
<li>5가지 일관성 수준</li>
<li>5가지 API</li>
<li>상세 실습은 Day 6에서 진행 예정</li>
</ul>
<hr>
<h1 id="oss-paas--분석-서비스">OSS PaaS + 분석 서비스</h1>
<table>
<thead>
<tr>
<th>서비스</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>PG Flexible Server</td>
<td>Day 5 마이그레이션 타깃</td>
</tr>
<tr>
<td>MySQL Flexible Server</td>
<td>MySQL 관리형 서비스</td>
</tr>
<tr>
<td>Synapse Analytics</td>
<td>데이터 웨어하우스</td>
</tr>
<tr>
<td>ADX</td>
<td>시계열 분석, Day 7 실습, Free Cluster</td>
</tr>
<tr>
<td>Microsoft Fabric</td>
<td>통합 분석 플랫폼</td>
</tr>
</tbody></table>
<hr>
<h1 id="의사결정-트리--5개-질문">의사결정 트리 — 5개 질문</h1>
<p>Azure 데이터 서비스를 선택할 때 다음 질문을 기준으로 판단한다.</p>
<table>
<thead>
<tr>
<th>질문</th>
<th>선택</th>
</tr>
</thead>
<tbody><tr>
<td>SQL Server 100% 필요?</td>
<td>SQL VM</td>
</tr>
<tr>
<td>완전 관리형 + SQL 호환?</td>
<td>SQL MI (이론)</td>
</tr>
<tr>
<td>관계형 + 비용 최적?</td>
<td>SQL DB Serverless</td>
</tr>
<tr>
<td>PG/MySQL 워크로드?</td>
<td>Flexible Server</td>
</tr>
<tr>
<td>글로벌 분산 + 다중 모델?</td>
<td>Cosmos DB</td>
</tr>
</tbody></table>
<hr>
<h1 id="워크샵-시나리오별-서비스-선택">워크샵: 시나리오별 서비스 선택</h1>
<table>
<thead>
<tr>
<th>시나리오</th>
<th>선택</th>
</tr>
</thead>
<tbody><tr>
<td>온프레미스 SQL 2016 이관</td>
<td>MI가 이상적이지만 프로비저닝 4시간 → SQL DB 검토</td>
</tr>
<tr>
<td>글로벌 게임 프로필</td>
<td>Cosmos DB NoSQL API</td>
</tr>
<tr>
<td>IoT 센서 실시간</td>
<td>ADX + KQL</td>
</tr>
</tbody></table>
<hr>
<h1 id="환경-셋업-실습">환경 셋업 실습</h1>
<p>Azure 구독부터 Budget Alert까지 직접 구축한다.</p>
<hr>
<h2 id="워크샵-가이드-의사결정-트리-실습">워크샵 가이드: 의사결정 트리 실습</h2>
<p>목표는 3개 시나리오에 대해 2~3인 조별 토론 후 서비스 선택 결과를 발표하는 것이다.</p>
<table>
<thead>
<tr>
<th>순서</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>강사가 시나리오 3개를 화면에 표시</td>
</tr>
<tr>
<td>2</td>
<td>2~3인 조 구성</td>
</tr>
<tr>
<td>3</td>
<td>조별 5분 토론: 각 시나리오에 적합한 Azure 서비스 선택</td>
</tr>
<tr>
<td>4</td>
<td>각 조 1분 발표: 선택한 서비스와 이유</td>
</tr>
<tr>
<td>5</td>
<td>강사가 정답과 추가 고려사항 설명</td>
</tr>
<tr>
<td>6</td>
<td>의사결정 트리와 비교하며 피드백</td>
</tr>
</tbody></table>
<hr>
<h1 id="실습-1-구독-·-리소스-그룹-생성">실습 1: 구독 · 리소스 그룹 생성</h1>
<p>부트캠프 전용 리소스 그룹을 생성한다.</p>
<pre><code class="language-bash"># Cloud Shell: Portal 상단 &gt;_ 아이콘

# 구독 확인
az account show --output table

# 리소스 그룹 생성
az group create \
--name rg-bootcamp-day1 \
--location koreacentral \
--tags &quot;project=bootcamp&quot; &quot;owner=&lt;이름&gt;&quot;

# 확인
az group show --name rg-bootcamp-day1 -o table</code></pre>
<hr>
<h1 id="실습-2-vnet-·-nsg">실습 2: VNet · NSG</h1>
<p>부트캠프 전 과정에서 사용할 VNet과 NSG를 생성한다.</p>
<pre><code class="language-bash"># VNet 생성
az network vnet create \
-g rg-bootcamp-day1 \
--name vnet-bootcamp \
--address-prefix 10.0.0.0/16 \
--subnet-name snet-default \
--subnet-prefix 10.0.1.0/24

# NSG 생성 + SSH 규칙
az network nsg create \
-g rg-bootcamp-day1 -n nsg-bootcamp

az network nsg rule create \
-g rg-bootcamp-day1 --nsg-name nsg-bootcamp \
-n AllowSSH --priority 1000 \
--destination-port-ranges 22 \
--access Allow --protocol Tcp --direction Inbound</code></pre>
<hr>
<h1 id="vnet과-nsg-기본-개념">VNet과 NSG 기본 개념</h1>
<h2 id="vnet-가상-네트워크">VNet (가상 네트워크)</h2>
<p>VNet은 Azure 내 프라이빗 네트워크이다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>주소 공간</td>
<td>10.0.0.0/16</td>
</tr>
<tr>
<td>서브넷</td>
<td>10.0.1.0/24</td>
</tr>
<tr>
<td>역할</td>
<td>VM, DB, 서비스가 통신하는 기반</td>
</tr>
<tr>
<td>확장</td>
<td>온프레미스 VPN 연결 가능</td>
</tr>
<tr>
<td>부트캠프 사용</td>
<td><code>vnet-bootcamp</code> (Day 1~9)</td>
</tr>
</tbody></table>
<hr>
<h2 id="nsg-네트워크-보안-그룹">NSG (네트워크 보안 그룹)</h2>
<p>NSG는 방화벽 규칙 집합이다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>역할</td>
<td>인바운드 / 아웃바운드 제어</td>
</tr>
<tr>
<td>기본 정책</td>
<td>모든 인바운드 차단</td>
</tr>
<tr>
<td>AllowSSH (22)</td>
<td>Day 2 VM용</td>
</tr>
<tr>
<td>AllowRDP (3389)</td>
<td>Day 3 SQL VM</td>
</tr>
<tr>
<td>원칙</td>
<td>최소 필요 포트만 개방</td>
</tr>
</tbody></table>
<hr>
<h1 id="실습-3-cloud-shell-환경">실습 3: Cloud Shell 환경</h1>
<p>목표는 CLI 환경을 확인하고 기본 명령을 연습하는 것이다.</p>
<table>
<thead>
<tr>
<th>순서</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>Portal → Cloud Shell (<code>&gt;_</code>) → Bash</td>
</tr>
<tr>
<td>2</td>
<td><code>az version</code>으로 CLI 버전 확인</td>
</tr>
<tr>
<td>3</td>
<td><code>az account show</code>로 구독 확인</td>
</tr>
<tr>
<td>4</td>
<td><code>az group list</code>로 리소스 그룹 목록 확인</td>
</tr>
<tr>
<td>5</td>
<td><code>az resource list</code>로 리소스 확인</td>
</tr>
<tr>
<td>6</td>
<td>로컬 CLI 설치는 선택사항</td>
</tr>
</tbody></table>
<hr>
<h1 id="실습-4-budget-alert-설정">실습 4: Budget Alert 설정</h1>
<p>목표는 비용 한도 초과를 사전에 감지하는 것이다.</p>
<table>
<thead>
<tr>
<th>순서</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>Portal → Cost Management → Budgets</td>
</tr>
<tr>
<td>2</td>
<td><code>+ Add</code> → <code>bootcamp-budget</code></td>
</tr>
<tr>
<td>3</td>
<td>Amount: ₩30,000 ~ ₩50,000</td>
</tr>
<tr>
<td>4</td>
<td>Alert: 80%, 100% 이메일</td>
</tr>
<tr>
<td>5</td>
<td>Action Group: 본인 이메일</td>
</tr>
<tr>
<td>6</td>
<td>Create</td>
</tr>
</tbody></table>
<hr>
<h1 id="리소스-정리-스크립트">리소스 정리 스크립트</h1>
<p>Day 1은 VM이 없으므로 모든 리소스를 유지한다.</p>
<p>유지 리소스는 다음과 같다.</p>
<table>
<thead>
<tr>
<th>유지 리소스</th>
</tr>
</thead>
<tbody><tr>
<td><code>rg-bootcamp-day1</code></td>
</tr>
<tr>
<td><code>vnet-bootcamp</code></td>
</tr>
<tr>
<td><code>nsg-bootcamp</code></td>
</tr>
<tr>
<td><code>bootcamp-budget</code></td>
</tr>
</tbody></table>
<p>확인 명령:</p>
<pre><code class="language-bash"># Day 1은 VM 없음 — 모두 유지

# 유지 리소스:
# rg-bootcamp-day1
# vnet-bootcamp
# nsg-bootcamp
# bootcamp-budget

# 확인만:
az resource list -g rg-bootcamp-day1 -o table

# Cost Analysis에서 비용 확인: $0.5 이하</code></pre>
<hr>
<h1 id="day-1-리소스-정리--체크리스트">Day 1 리소스 정리 + 체크리스트</h1>
<pre><code class="language-bash"># Day 1 체크리스트
az resource list -g rg-bootcamp-day1 -o table</code></pre>
<p>정리 체크리스트는 다음과 같다.</p>
<table>
<thead>
<tr>
<th>체크리스트</th>
</tr>
</thead>
<tbody><tr>
<td>VNet / NSG / Budget 유지 확인</td>
</tr>
<tr>
<td>Cost Analysis: $0.5 이하</td>
</tr>
<tr>
<td>불필요 리소스 없음</td>
</tr>
</tbody></table>
<p>Day 1 예상 비용: <code>$0.5</code>
한도: <code>$10/일</code></p>
<hr>
<h1 id="비용-관리--9일-전체-전략">비용 관리 — 9일 전체 전략</h1>
<p>이 습관이 9일간의 비용을 결정한다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>일일 1인당 한도</td>
<td>$10</td>
</tr>
<tr>
<td>대부분 사용 비용</td>
<td>$0.5 ~ $1.0</td>
</tr>
<tr>
<td>9일 총 예상</td>
<td>$10.8</td>
</tr>
<tr>
<td>한도 대비</td>
<td>6~8배 버퍼</td>
</tr>
<tr>
<td>Budget Alert</td>
<td>80%, 100% 도달 시 이메일</td>
</tr>
<tr>
<td>Cost Analysis</td>
<td>매일 실습 종료 시 직접 확인</td>
</tr>
<tr>
<td>가장 중요한 절약 수단</td>
<td>VM deallocate</td>
</tr>
<tr>
<td>주의</td>
<td>Stop ≠ Deallocate, Stop은 계속 과금됨</td>
</tr>
</tbody></table>
<hr>
<h1 id="qa">Q&amp;A</h1>
<table>
<thead>
<tr>
<th>질문</th>
<th>답변</th>
</tr>
</thead>
<tbody><tr>
<td>SQL MI를 왜 실습 안 하나?</td>
<td>프로비저닝 4시간이 걸리므로 이론 + 매트릭스로 대체</td>
</tr>
<tr>
<td>Vector DB는 Azure에서?</td>
<td>AI Search 벡터 검색, Cosmos MongoDB vCore</td>
</tr>
<tr>
<td>Budget Alert 안 와요</td>
<td>최대 24시간 지연 가능, Cost Analysis 직접 확인</td>
</tr>
<tr>
<td>Free Trial로 9일 가능?</td>
<td>$200 크레딧, 예상 $10.8로 충분</td>
</tr>
</tbody></table>
<hr>
<h1 id="정리">정리</h1>
<table>
<thead>
<tr>
<th>모듈</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>DB 역사와 이론</td>
<td>계층형 → RDBMS → NoSQL → NewSQL</td>
</tr>
<tr>
<td>DB 이론</td>
<td>ACID vs BASE, CAP</td>
</tr>
<tr>
<td>Azure 서비스 지형도</td>
<td>SQL VM / DB / MI, Cosmos DB</td>
</tr>
<tr>
<td>의사결정</td>
<td>의사결정 트리 워크샵</td>
</tr>
<tr>
<td>환경 셋업 실습</td>
<td>구독 · RG · VNet · NSG</td>
</tr>
<tr>
<td>비용 관리</td>
<td>Budget Alert 설정</td>
</tr>
<tr>
<td>다음 과정</td>
<td>OSS DB on VM: PostgreSQL · MySQL · MongoDB</td>
</tr>
</tbody></table>
<hr>
<h1 id="핵심-요약">핵심 요약</h1>
<p>이번 과정은 단순히 Azure 리소스를 만드는 실습이 아니라,
데이터베이스가 왜 지금과 같은 형태로 발전했는지 이해하고,
워크로드에 따라 적절한 Azure 데이터 서비스를 선택하기 위한 기준을 세우는 과정이다.</p>
<p>핵심은 다음과 같다.</p>
<ul>
<li>RDBMS는 데이터 독립성과 ACID를 기반으로 발전했다.</li>
<li>NoSQL은 웹 스케일과 수평 확장 문제를 해결하기 위해 등장했다.</li>
<li>NewSQL은 ACID와 Scale-Out을 동시에 추구한다.</li>
<li>Vector DB는 AI/ML 시대의 검색 인프라로 중요해졌다.</li>
<li>Azure에서는 SQL VM, SQL DB, SQL MI, Cosmos DB, Flexible Server, ADX, Fabric 등 다양한 선택지가 있다.</li>
<li>정답은 하나가 아니라, 워크로드에 맞는 선택이 최선이다.</li>
<li>실습 환경은 RG, VNet, NSG, Budget Alert를 기준으로 구성한다.</li>
<li>비용 관리는 매일 Cost Analysis 확인과 VM deallocate가 핵심이다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MicrosoftDataSchool] 78일차 - Azure SQL Database 데이터 복구 실습]]></title>
            <link>https://velog.io/@rudin_/MicrosoftDataSchool-78%EC%9D%BC%EC%B0%A8-Azure-SQL-Database</link>
            <guid>https://velog.io/@rudin_/MicrosoftDataSchool-78%EC%9D%BC%EC%B0%A8-Azure-SQL-Database</guid>
            <pubDate>Tue, 28 Apr 2026 00:54:20 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Azure의 대표 컨셉은 Microsoft Entra ID이다(AAD)</p>
</blockquote>
<h1 id="azure-sql-database-실습">Azure SQL Database 실습</h1>
<table>
<thead>
<tr>
<th>항목</th>
<th>Azure SQL Database</th>
<th>Managed Instance</th>
</tr>
</thead>
<tbody><tr>
<td>단위</td>
<td>DB</td>
<td>인스턴스</td>
</tr>
<tr>
<td>파일 시스템</td>
<td>없음</td>
<td>있음</td>
</tr>
<tr>
<td>SQL Server 엔진</td>
<td>추상화됨</td>
<td>거의 그대로</td>
</tr>
<tr>
<td>OS 접근</td>
<td>불가</td>
<td>일부 가능</td>
</tr>
<tr>
<td>복원 방식</td>
<td>.bacpac</td>
<td>.bak</td>
</tr>
<tr>
<td>물리 구조 접근</td>
<td>불가</td>
<td>가능</td>
</tr>
<tr>
<td>보안 구조</td>
<td>다중 테넌트</td>
<td>격리된 환경</td>
</tr>
</tbody></table>
<h2 id="lab-1--lab-환경-구축">Lab 1 – Lab 환경 구축</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/e3c547de-c810-4ec6-98fe-0ef86f7decea/image.png" alt="">
Bastion은 가상 네트워크 생성시 선택 가능</p>
<hr>
<h2 id="portal에서-azure-sql-찾기">Portal에서 Azure SQL 찾기</h2>
<p>Azure Portal 상단 검색창에서 <strong>Azure SQL</strong>을 검색하면
Azure SQL과 관련된 서비스 목록이 표시된다. </p>
<p>이 화면에서는 다음과 같은 리소스를 확인할 수 있다.</p>
<ul>
<li>Azure SQL Database</li>
<li>Azure SQL Managed Instance</li>
<li>SQL Server on Azure VM</li>
</ul>
<hr>
<h2 id="azure-sql-선택을-위한-질문">Azure SQL 선택을 위한 질문</h2>
<p>Azure SQL 선택 화면에서는 사용자의 요구사항을 기반으로
적절한 서비스를 추천하기 위해 질문을 제공한다. </p>
<p>이 질문들은 다음과 같은 기준을 포함한다.</p>
<ul>
<li>어떤 유형의 워크로드인가</li>
<li>기존 시스템을 마이그레이션하는가</li>
<li>새로운 애플리케이션인가</li>
</ul>
<hr>
<h2 id="azure-sql-선택-의사-결정-트리">Azure SQL 선택 의사 결정 트리</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/83e45e98-33eb-422d-b554-cee19d14bf9f/image.png" alt=""></p>
<hr>
<h2 id="azure로-database-마이그레이션">Azure로 Database 마이그레이션</h2>
<p>Azure에서는 데이터베이스 마이그레이션을 쉽게 하기 위한 도구를 제공한다. </p>
<p>마이그레이션 시작 화면에서 다음 옵션을 확인할 수 있다.</p>
<ul>
<li>Azure Database Migration Service</li>
<li>Azure Arc enabled SQL Server</li>
<li>Azure Migrate</li>
</ul>
<hr>
<h2 id="데이터베이스를-azure로-이전하기-위한-방법">데이터베이스를 Azure로 이전하기 위한 방법</h2>
<p>Azure에서 제공하는 마이그레이션 방법은 다음과 같다.</p>
<ul>
<li><p>다양한 DB → Azure 이전
→ Azure Database Migration Service</p>
</li>
<li><p>SQL Server → Azure 이전
→ Azure Arc enabled SQL Server</p>
</li>
<li><p>앱 + DB 전체 이전
→ Azure Migrate </p>
</li>
</ul>
<hr>
<h1 id="azure-sql-database-만들기">Azure SQL Database 만들기</h1>
<p><a href="https://learn.microsoft.com/ko-kr/azure/private-link/tutorial-private-endpoint-sql-portal">https://learn.microsoft.com/ko-kr/azure/private-link/tutorial-private-endpoint-sql-portal</a></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>Azure SQL Database</th>
<th>Managed Instance</th>
</tr>
</thead>
<tbody><tr>
<td>단위</td>
<td>DB</td>
<td>인스턴스</td>
</tr>
<tr>
<td>파일 시스템</td>
<td>없음</td>
<td>있음</td>
</tr>
<tr>
<td>SQL Server 엔진</td>
<td>추상화됨</td>
<td>거의 그대로</td>
</tr>
<tr>
<td>OS 접근</td>
<td>불가</td>
<td>일부 가능</td>
</tr>
<tr>
<td>복원 방식</td>
<td>.bacpac</td>
<td>.bak</td>
</tr>
<tr>
<td>물리 구조 접근</td>
<td>불가</td>
<td>가능</td>
</tr>
<tr>
<td>보안 구조</td>
<td>다중 테넌트</td>
<td>격리된 환경</td>
</tr>
</tbody></table>
<h2 id="워크로드-환경-및-백업-스토리지-설정">워크로드 환경 및 백업 스토리지 설정</h2>
<p>다음과 같이 설정한다. </p>
<ul>
<li>SQL Elastic Pool: 사용 안 함</li>
<li>워크로드 환경: 개발</li>
<li>백업 스토리지: 로컬 중복</li>
</ul>
<hr>
<h2 id="컴퓨팅-요소-설정">컴퓨팅 요소 설정</h2>
<p>컴퓨팅 계층 및 하드웨어를 설정한다. </p>
<ul>
<li>서비스 계층: 범용</li>
<li>컴퓨팅 계층: 프로비전됨</li>
<li>하드웨어: Gen5</li>
</ul>
<p>👉 이후 Serverless와 비교 목적</p>
<hr>
<h2 id="네트워크-설정">네트워크 설정</h2>
<p>데이터베이스 접근을 위한 네트워크 설정을 수행한다. </p>
<ul>
<li>연결 방법: 퍼블릭 엔드포인트</li>
<li>방화벽: 현재 IP 추가</li>
<li>연결 정책: 기본값</li>
<li>TLS: 1.2</li>
</ul>
<hr>
<h2 id="보안-설정">보안 설정</h2>
<p>기본 제공 옵션을 그대로 유지한다. </p>
<hr>
<h2 id="추가-설정">추가 설정</h2>
<p>기존 데이터 사용에서
👉 샘플 선택 (AdventureWorks) </p>
<hr>
<h2 id="생성">생성</h2>
<p>“만들기”를 선택하면 배포가 진행된다. </p>
<ul>
<li>소요 시간: 약 1~2분</li>
</ul>
<hr>
<h1 id="db-확인-및-쿼리-실행">DB 확인 및 쿼리 실행</h1>
<hr>
<h2 id="db-확인">DB 확인</h2>
<p>쿼리 편집기를 통해 생성된 DB를 확인한다. </p>
<ul>
<li>SQL Server 인증 방식 사용</li>
</ul>
<hr>
<h2 id="쿼리-실행">쿼리 실행</h2>
<pre><code class="language-sql">SELECT TOP 20 pc.Name as CategoryName, p.name as ProductName
FROM SalesLT.ProductCategory pc
JOIN SalesLT.Product p
ON pc.productcategoryid = p.productcategoryid;</code></pre>
<p>쿼리 실행 결과를 통해 데이터 확인 가능 </p>
<hr>
<h1 id="ssms-설치">SSMS 설치</h1>
<p>SSMS 설치 가이드를 통해 설치 진행 
연결시 Microsoft Entra 인증으로 생성했다면 Microsoft Entra 암호 입력 후 
인증서 신뢰를 체크하면 연결됨</p>
<hr>
<h1 id="데이터베이스-복구">데이터베이스 복구</h1>
<hr>
<h2 id="삭제-유형별-비교">삭제 유형별 비교</h2>
<table>
<thead>
<tr>
<th>선택</th>
<th>결과</th>
<th>복구 가능성</th>
</tr>
</thead>
<tbody><tr>
<td>데이터베이스만 삭제</td>
<td>서버 유지, DB 삭제</td>
<td>일정 기간 복구 가능</td>
</tr>
<tr>
<td>서버 삭제</td>
<td>서버 + DB 삭제</td>
<td>서버 복구 불가, DB만 일부 복구</td>
</tr>
<tr>
<td>둘 다 삭제</td>
<td>전부 삭제</td>
<td>DB만 복구 가능</td>
</tr>
</tbody></table>
<hr>
<h1 id="방화벽-확인">방화벽 확인</h1>
<p>Azure SQL Database에서
현재 IP 확인 후 방화벽 등록 </p>
<hr>
<h1 id="virtual-network-생성">Virtual Network 생성</h1>
<p><img src="https://velog.velcdn.com/images/rudin_/post/af3d9cda-2cc6-4055-84b7-f526deaacacc/image.png" alt=""></p>
<h2 id="bastion">Bastion</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/4ec11b48-f874-4717-aa9e-70dc04f467f5/image.png" alt=""></p>
<h2 id="virtual-machine">virtual machine</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/500d8a2f-cfec-4236-abee-206c8f2b6cc5/image.png" alt="">
bastion을 통해야하므로 공용 인바운드 포트 X</p>
<h2 id="sql-db-프라이빗-엔드포인트">sql db 프라이빗 엔드포인트</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/c6e9f36e-0e24-4f47-87c7-1e04bd068539/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/8c25015b-b0e3-4646-8af2-6189b8a90c55/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/38d91dc6-587f-4be9-996e-10b3871af406/image.png" alt=""></p>
<h2 id="연결">연결</h2>
<p><code>vm</code>-<code>bastion으로 연결</code>-<code>ssh 로컬 파일로 접속(azureuser)</code>
<img src="https://velog.velcdn.com/images/rudin_/post/b015d153-cf09-4972-b3ce-985cdd7cd0fb/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/ec64a779-5ca1-4c38-8080-5f25ed435fdb/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/359d41c7-9a78-4208-af5f-c2bb3112d6d7/image.png" alt=""></p>
<h2 id="sql-server-도구-설치">SQL Server 도구 설치</h2>
<p><a href="https://learn.microsoft.com/ko-kr/sql/linux/sql-server-linux-setup-tools?view=sql-server-ver17&amp;tabs=redhat-install%2Codbc-ubuntu-1804">https://learn.microsoft.com/ko-kr/sql/linux/sql-server-linux-setup-tools?view=sql-server-ver17&amp;tabs=redhat-install%2Codbc-ubuntu-1804</a></p>
<pre><code>sudo su
curl -sSL -O https://packages.microsoft.com/config/ubuntu/24.04/packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
exit

sudo apt-get update
sudo apt-get install mssql-tools18 unixodbc-dev

sudo apt-get update
sudo apt-get install mssql-tools18

echo &#39;export PATH=&quot;$PATH:/opt/mssql-tools18/bin&quot;&#39; &gt;&gt; ~/.bash_profile
source ~/.bash_profile

echo &#39;export PATH=&quot;$PATH:/opt/mssql-tools18/bin&quot;&#39; &gt;&gt; ~/.bashrc
source ~/.bashrc</code></pre><hr>
<h1 id="managed-instance-생성">Managed Instance 생성</h1>
<p><img src="https://velog.velcdn.com/images/rudin_/post/fc610b04-86a3-4bfe-af7b-b80c0dea4a5f/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/a78c08e8-a581-43cf-b2f5-6cc633eed946/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/eedf7d16-1268-42d4-b045-9a0d862a4328/image.png" alt=""></p>
<hr>
<h2 id="설정">설정</h2>
<ul>
<li>Public endpoint 사용</li>
<li>포트 3342 오픈</li>
<li>NSG에서 IP 허용 </li>
</ul>
<hr>
<h2 id="ssms-연결">SSMS 연결</h2>
<ul>
<li>Endpoint 복사</li>
<li>Server name 입력</li>
<li>Connect</li>
</ul>
<p><img src="https://velog.velcdn.com/images/rudin_/post/2cd098ba-0596-4be1-8a1e-3a8a16c73823/image.png" alt=""></p>
<hr>
<h1 id="lab-2--데이터-복원-및-관리">Lab 2 – 데이터 복원 및 관리</h1>
<hr>
<h2 id="데이터-적재">데이터 적재</h2>
<p>AdventureWorks2022.bak 다운로드 후
Blob Storage에 업로드 </p>
<hr>
<h2 id="blob-storage-구성">Blob Storage 구성</h2>
<ul>
<li>Storage Account 생성</li>
<li>Container 생성</li>
<li>파일 업로드</li>
</ul>
<hr>
<h2 id="sas-생성">SAS 생성</h2>
<p>SAS는 다음 역할 수행 </p>
<ul>
<li>제한된 시간 동안 접근 허용</li>
<li>권한 제어 가능</li>
<li>URL 기반 인증</li>
</ul>
<hr>
<h2 id="managed-instance에서-복원">Managed Instance에서 복원</h2>
<ul>
<li>SSMS → Restore Database</li>
<li>Blob URL + SAS 사용
<img src="https://velog.velcdn.com/images/rudin_/post/62f36555-3525-4504-8da0-0ded7c9954d7/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/05391056-3ba1-4229-9dba-b5637f914284/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/7bae32ac-be65-460c-aa3d-32ffdc0888fa/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/c306532b-60f7-474d-a0e1-8de4c6bd87df/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/0fa45de3-dec4-44a7-82a2-95d3799e70de/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/31993acf-136e-4969-9cf5-a149166f2cab/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/5a9e8e86-310a-4f69-ae9b-98af013e824a/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/7759a09b-02ee-4151-92b4-442f6ebc09a5/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/2451a3bb-f81b-4f0b-910e-b0f865b669f7/image.png" alt=""></li>
</ul>
<hr>
<h1 id="sql-database-vs-managed-instance">SQL Database vs Managed Instance</h1>
<hr>
<h2 id="구조-차이">구조 차이</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>SQL Database</th>
<th>Managed Instance</th>
</tr>
</thead>
<tbody><tr>
<td>구조</td>
<td>논리 DB</td>
<td>인스턴스</td>
</tr>
<tr>
<td>파일 시스템</td>
<td>없음</td>
<td>있음</td>
</tr>
<tr>
<td>복원 방식</td>
<td>.bacpac</td>
<td>.bak</td>
</tr>
</tbody></table>
<p>MI는 Lift &amp; Shift에 적합</p>
<hr>
<h1 id="bacpac-vs-bak-비교">.bacpac vs .bak 비교</h1>
<p><code>.bak</code>은 <strong>완벽한 시점 복원용 백업/복구 파일</strong>이고, <code>.bacpac</code>은 <strong>클라우드 이전 및 버전 업/다운그레이드용 마이그레이션 파일</strong>이다. </p>
<table>
<thead>
<tr>
<th>구분</th>
<th><code>.bak</code></th>
<th><code>.bacpac</code></th>
</tr>
</thead>
<tbody><tr>
<td>성격</td>
<td>물리적 백업</td>
<td>논리적 아카이브</td>
</tr>
<tr>
<td>목적</td>
<td>특정 시점의 DB를 그대로 복원</td>
<td>DB 구조와 데이터를 다른 환경으로 이전</td>
</tr>
<tr>
<td>포함 내용</td>
<td>데이터 파일 구조, 트랜잭션 로그 상태 등</td>
<td>테이블 구조(스키마) + 데이터</td>
</tr>
<tr>
<td>일관성</td>
<td>트랜잭션 로그 포함으로 특정 시점 일관성 보장</td>
<td>데이터와 구조를 추출해서 이동</td>
</tr>
<tr>
<td>속도</td>
<td>빠름</td>
<td>상대적으로 느릴 수 있음</td>
</tr>
<tr>
<td>용량</td>
<td>큼</td>
<td>작음</td>
</tr>
<tr>
<td>대용량 DB</td>
<td>적합</td>
<td>대용량에는 비효율적일 수 있음</td>
</tr>
<tr>
<td>버전 호환성</td>
<td>낮음. 상위 버전에서 만든 <code>.bak</code>은 하위 버전 복원 불가</td>
<td>높음. 하위 버전으로도 이동 가능</td>
</tr>
<tr>
<td>Azure SQL Database</td>
<td>직접 가져오기 불가</td>
<td>가져오기 가능</td>
</tr>
<tr>
<td>Azure SQL Managed Instance</td>
<td>복원 가능</td>
<td>가져오기 가능하지만 <code>.bak</code>이 더 적합</td>
</tr>
</tbody></table>
<hr>
<h2 id="bak의-장점"><code>.bak</code>의 장점</h2>
<h3 id="1-완벽한-데이터-일관성">1. 완벽한 데이터 일관성</h3>
<p><code>.bak</code> 파일은 특정 시점의 데이터베이스를 그대로 저장한 <strong>Point-in-time 스냅샷</strong>이다.
백업 도중 데이터가 변경되더라도 트랜잭션 로그를 함께 포함하기 때문에 데이터 무결성이 보장된다. </p>
<p>즉, 운영 중인 DB를 백업하더라도
“백업 시점 기준으로 데이터가 깨지지 않은 상태”를 유지할 수 있다.</p>
<hr>
<h3 id="2-압도적인-속도와-성능">2. 압도적인 속도와 성능</h3>
<p><code>.bak</code>은 데이터베이스의 물리적 페이지 단위 블록을 통째로 백업한다.
그래서 대용량 데이터베이스에서는 <code>.bacpac</code>보다 백업/복원 속도가 훨씬 유리하다. </p>
<p>특히 수백 GB~TB 단위 DB에서는 <code>.bak</code> 방식이 사실상 필수에 가깝다.</p>
<hr>
<h3 id="3-시스템-부하가-비교적-작음">3. 시스템 부하가 비교적 작음</h3>
<p><code>.bak</code> 백업은 SQL Server 엔진의 기본 백업 기능을 사용한다.
그래서 운영 중인 서버에서 백업을 수행해도 비교적 안정적으로 동작하고, 시스템 부하도 상대적으로 적다. </p>
<hr>
<h2 id="bacpac의-장점"><code>.bacpac</code>의 장점</h2>
<h3 id="1-버전-호환성과-이식성">1. 버전 호환성과 이식성</h3>
<p><code>.bacpac</code>은 데이터베이스의 물리 구조를 그대로 복사하는 것이 아니라,
테이블 구조와 데이터를 논리적으로 추출한 파일이다.</p>
<p>그래서 SQL Server 버전에 덜 종속된다.
예를 들어 SQL Server 2022에서 만든 <code>.bak</code>은 SQL Server 2019로 복원할 수 없지만, <code>.bacpac</code>은 구조와 데이터를 추출한 형태이기 때문에 하위 버전으로 이동할 수 있다. </p>
<hr>
<h3 id="2-azure-sql-database-이전에-적합">2. Azure SQL Database 이전에 적합</h3>
<p>Azure SQL Database는 사용자에게 파일 시스템 접근 권한을 제공하지 않는다.
그래서 SQL Server의 물리 백업 파일인 <code>.bak</code>을 직접 복원할 수 없다.</p>
<p>반면 <code>.bacpac</code>은 스키마와 데이터만 담은 논리적 아카이브이기 때문에 Azure SQL Database로 가져올 수 있다. </p>
<p>즉,</p>
<ul>
<li><strong>Azure SQL Managed Instance</strong> → <code>.bak</code> 복원 가능</li>
<li><strong>Azure SQL Database</strong> → <code>.bacpac</code> 가져오기 사용</li>
</ul>
<hr>
<h3 id="3-파일-크기가-작음">3. 파일 크기가 작음</h3>
<p><code>.bacpac</code>은 트랜잭션 로그나 여유 디스크 공간을 포함하지 않는다.
순수하게 구조와 데이터만 추출해서 압축하기 때문에 <code>.bak</code>보다 파일 크기가 훨씬 작아질 수 있다.</p>
<p>자료 예시에서는 <strong>4.3GB <code>.bak</code> 파일이 197MB <code>.bacpac</code> 파일로 줄어들 수 있음</strong>을 설명한다. </p>
<hr>
<h2 id="정리">정리</h2>
<table>
<thead>
<tr>
<th>상황</th>
<th>적합한 파일</th>
</tr>
</thead>
<tbody><tr>
<td>SQL Server 전체를 특정 시점으로 복구</td>
<td><code>.bak</code></td>
</tr>
<tr>
<td>대용량 DB 백업/복원</td>
<td><code>.bak</code></td>
</tr>
<tr>
<td>Managed Instance로 복원</td>
<td><code>.bak</code></td>
</tr>
<tr>
<td>Azure SQL Database로 이전</td>
<td><code>.bacpac</code></td>
</tr>
<tr>
<td>버전 차이가 있는 환경으로 이동</td>
<td><code>.bacpac</code></td>
</tr>
<tr>
<td>파일 크기를 줄여 이관</td>
<td><code>.bacpac</code></td>
</tr>
</tbody></table>
<p>한 줄로 정리하면,
<strong><code>.bak</code>은 “복구용”, <code>.bacpac</code>은 “이전용”에 가깝다.</strong></p>
<hr>
<h1 id="database-watcher">Database Watcher</h1>
<p><img src="https://velog.velcdn.com/images/rudin_/post/a0b64c4e-3a6b-4708-b632-e7cd78574cb5/image.png" alt=""></p>
<hr>
<h1 id="클라우드-비교">클라우드 비교</h1>
<table>
<thead>
<tr>
<th>서비스 분야</th>
<th>AWS (Amazon Web Services)</th>
<th>Azure (Microsoft Azure)</th>
<th>GCP (Google Cloud Platform)</th>
</tr>
</thead>
<tbody><tr>
<td>컴퓨팅 (가상 서버)</td>
<td>EC2 (Elastic Compute Cloud)<br>가장 다양한 인스턴스 타입 제공</td>
<td>Azure Compute<br>Windows 서버와의 호환성 우수</td>
<td>Compute Engine<br>빠른 부팅 속도와 자동 할인 기능</td>
</tr>
<tr>
<td>컨테이너 (컨테이너 관리)</td>
<td>ECS / EKS<br>Kubernetes 및 자체 서비스 지원</td>
<td>Azure Kubernetes Service (AKS)<br>Kubernetes 관리 및 통합 강점</td>
<td>Google Kubernetes Engine (GKE)<br>Kubernetes 원조, 안정적 관리</td>
</tr>
<tr>
<td>스토리지 (객체 스토리지)</td>
<td>S3<br>클라우드 스토리지 사실상 표준</td>
<td>Blob Storage<br>온프레미스 연동 용이</td>
<td>Cloud Storage<br>다양한 클래스, 비용 효율</td>
</tr>
<tr>
<td>데이터베이스 (관계형 DB)</td>
<td>RDS<br>MySQL, PostgreSQL 등 다양한 엔진 지원</td>
<td>Azure SQL Database<br>SQL Server와 완벽 호환</td>
<td>Cloud SQL<br>간편한 관리와 성능</td>
</tr>
<tr>
<td>서버리스 (코드 실행)</td>
<td>Lambda<br>다양한 언어 및 서비스 연동</td>
<td>Azure Functions<br>.NET 환경과 자연스러운 통합</td>
<td>Cloud Functions<br>사용량 기반 단순 과금</td>
</tr>
<tr>
<td>빅데이터 (분석, DW)</td>
<td>Redshift / EMR<br>DW + 빅데이터 플랫폼</td>
<td>Synapse Analytics<br>데이터 통합 및 분석 강점</td>
<td>BigQuery<br>압도적 속도와 확장성</td>
</tr>
<tr>
<td>AI/ML (머신러닝 플랫폼)</td>
<td>SageMaker<br>모델 개발~배포 End-to-End</td>
<td>Azure Machine Learning<br>MS AI 기술과 통합</td>
<td>Vertex AI<br>TensorFlow 기반 최신 AI 기술</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MicrosoftDataSchool] 77일차 - Azure SQL Database 이론]]></title>
            <link>https://velog.io/@rudin_/MicrosoftDataSchool-77%EC%9D%BC%EC%B0%A8-Azure-SQL-Database</link>
            <guid>https://velog.io/@rudin_/MicrosoftDataSchool-77%EC%9D%BC%EC%B0%A8-Azure-SQL-Database</guid>
            <pubDate>Mon, 27 Apr 2026 02:13:45 GMT</pubDate>
            <description><![CDATA[<h1 id="azure-sql-database-정리">Azure SQL Database 정리</h1>
<h2 id="azure-sql-개요">Azure SQL 개요</h2>
<p>Azure SQL은 Azure 클라우드에서 SQL Server 데이터베이스 엔진을 사용하는 관리형, 보안 및 인텔리전트 제품군이다. SQL Server 엔진 기반이기 때문에 기존 애플리케이션을 비교적 쉽게 마이그레이션할 수 있고, 익숙한 도구와 언어, 리소스를 계속 사용할 수 있다.</p>
<p>Azure SQL 제품군은 크게 세 가지로 나뉜다.</p>
<table>
<thead>
<tr>
<th>제품</th>
<th>설명</th>
<th>적합한 상황</th>
</tr>
</thead>
<tbody><tr>
<td>Azure SQL Database</td>
<td>서버리스 컴퓨팅을 포함하는 인텔리전트 관리형 데이터베이스 서비스</td>
<td>클라우드에서 새로운 앱을 구축하는 경우</td>
</tr>
<tr>
<td>Azure SQL Managed Instance</td>
<td>SQL Server 데이터베이스 엔진과 거의 100% 동일한 기능을 제공하는 완전 관리형 인스턴스</td>
<td>기존 SQL Server 애플리케이션을 대규모로 현대화하거나 마이그레이션하는 경우</td>
</tr>
<tr>
<td>Azure VM 위의 SQL Server</td>
<td>SQL Server 워크로드를 Azure VM으로 리프트 앤 시프트하며 SQL Server 호환성과 OS 수준 액세스를 유지</td>
<td>OS 수준 제어와 완전한 호환성이 필요한 경우</td>
</tr>
</tbody></table>
<p>리프트 앤 시프트는 주로 IaaS 환경에서 많이 한다.</p>
<hr>
<h2 id="azure-sql-포트폴리오-비교">Azure SQL 포트폴리오 비교</h2>
<p>Azure SQL은 SQL Server 엔진을 기반으로 구축된 통합 SQL 포트폴리오이다. 서비스 선택은 관리 책임, 호환성, 제어 수준에 따라 달라진다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>Azure Virtual Machines 위의 SQL Server</th>
<th>Azure SQL Managed Instance</th>
<th>Azure SQL Database</th>
</tr>
</thead>
<tbody><tr>
<td>서비스 유형</td>
<td>IaaS</td>
<td>PaaS</td>
<td>PaaS</td>
</tr>
<tr>
<td>가장 적합한 앱</td>
<td>리호스팅 및 OS 수준 액세스/제어가 필요한 앱</td>
<td>기존 앱 현대화</td>
<td>클라우드 신규 앱 구축</td>
</tr>
<tr>
<td>주요 특징</td>
<td>자동화된 관리 기능 및 OS 수준 액세스</td>
<td>SQL Server와의 높은 호환성, 기본 VNet 지원</td>
<td>사전 프로비저닝 또는 서버리스 컴퓨팅, 하이퍼스케일 스토리지</td>
</tr>
<tr>
<td>제어 수준</td>
<td>가장 높음</td>
<td>중간</td>
<td>가장 낮음</td>
</tr>
<tr>
<td>관리 부담</td>
<td>가장 큼</td>
<td>중간</td>
<td>가장 작음</td>
</tr>
</tbody></table>
<p>리호스팅은 IT 시스템 마이그레이션에서 대표적으로 사용되는 방식으로, 기존 시스템을 큰 변경 없이 다른 환경으로 이전하는 방법이다.(센터 이전도 가능)</p>
<p>신기능은 Azure Virtual Machines 외의 2종류 정도에만 잘 들어간다.</p>
<hr>
<h2 id="versionless-database-엔진과-호환성">Versionless Database 엔진과 호환성</h2>
<p>Azure VM 위의 SQL은 선택한 특정 SQL Server 버전에 묶여 있다. 반면 Azure SQL Database와 Azure SQL Managed Instance는 PaaS 특성상 특정 버전에 종속되지 않는다. 특히 Always-up-to-date 업데이트 정책을 사용하면 최신 클라우드 기능을 빠르게 반영할 수 있다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>SQL Server 2025 업데이트 정책</th>
<th>Always-up-to-date 업데이트 정책</th>
</tr>
</thead>
<tbody><tr>
<td>특징</td>
<td>SQL Server 2025 버전과의 호환성 유지</td>
<td>최신 클라우드 기능 즉시 반영</td>
</tr>
<tr>
<td>장점</td>
<td>온프레미스 SQL 2025로 복원 및 링크 가능</td>
<td>최신 엔진 성능 및 보안 업데이트 자동 적용</td>
</tr>
<tr>
<td>제한</td>
<td>최신 클라우드 전용 엔진 기능 사용 불가</td>
<td>이전 버전 정책으로 복구 불가, 하향 불가</td>
</tr>
</tbody></table>
<p>핵심은 SQL Server, Azure SQL Database, Managed Instance가 하나의 공통 코드베이스를 기반으로 최신 엔진 기능을 제공한다는 점이다. SQL Server 버전은 몇 년 단위로 출시되지만, PaaS 서비스는 지속적으로 엔진이 업데이트된다. 이 구조 덕분에 OS 및 SQL Server 패치 부담이 줄어든다.</p>
<hr>
<h2 id="azure-sql-서비스-비교">Azure SQL 서비스 비교</h2>
<p>Azure SQL 서비스는 Bare Metal부터 PaaS까지 다양한 형태로 SQL Server를 사용할 수 있게 한다.
(Bare Metal은 가상화 없이 사용하는 것)
Private Cloud는 자체적으로 구축한 클라우드 환경</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>Azure SQL Database</th>
<th>Azure SQL Managed Instance</th>
<th>Azure VM의 SQL Server</th>
</tr>
</thead>
<tbody><tr>
<td>지원 기능</td>
<td>대부분의 SQL DB 기능 지원, 일부 제약</td>
<td>거의 모든 온프레미스 인스턴스 수준</td>
<td>모든 온프레미스 기능 지원</td>
</tr>
<tr>
<td>가용성/확장성</td>
<td>99.995% SLA, 단일/풀링 지원</td>
<td>99.99% SLA, 단일/풀링 가능</td>
<td>99.99% SLA, VM 크기/구성에 따름</td>
</tr>
<tr>
<td>유지 관리/패치</td>
<td>자동 패치·백업</td>
<td>자동 패치·백업</td>
<td>직접 관리 필요, 일부 자동 기능 지원</td>
</tr>
<tr>
<td>네트워크 접근</td>
<td>Azure Private Link 등 지원</td>
<td>ExpressRoute, VPN Gateway 등 지원</td>
<td>Azure Virtual Network 내에 배치</td>
</tr>
<tr>
<td>최대 저장 공간</td>
<td>128TB</td>
<td>16TB</td>
<td>256TB 이상, 스토리지 추가 가능</td>
</tr>
<tr>
<td>마이그레이션 용이성</td>
<td>일부 SQL Server 기능 제한적 호환</td>
<td>더 높은 호환성, 마이그레이션 쉬움</td>
<td>온프레미스와 동일, 완벽 호환</td>
</tr>
<tr>
<td>운영 책임</td>
<td>대부분 Azure에서 관리</td>
<td>일부 Azure와 공동 책임 모델</td>
<td>사용자 직접 관리</td>
</tr>
<tr>
<td>온프레미스 연계</td>
<td>기본 제공, 제한적</td>
<td>네트워크/도메인 연계 지원</td>
<td>도메인/애플리케이션 등 완벽 연동</td>
</tr>
<tr>
<td>사용 사례</td>
<td>SaaS 앱, 단일/풀링 DB</td>
<td>리프트 앤 시프트, 복잡한 SQL 앱</td>
<td>레거시 이전, 사용자화 앱, 고도의 통제 필요</td>
</tr>
</tbody></table>
<hr>
<h2 id="azure-sql-결정-트리">Azure SQL 결정 트리</h2>
<p>Azure SQL 결정 트리는 신규 앱인지, 기존 DB 마이그레이션인지, OS 접근이 필요한지, SQL Server 호환성이 어느 정도 필요한지에 따라 서비스를 선택하도록 돕는다.</p>
<ul>
<li>신규 클라우드 앱이면 Azure SQL Database가 우선 고려된다.</li>
<li>기존 SQL Server 앱을 마이그레이션하고 호환성이 중요하면 Managed Instance가 적합하다.</li>
<li>OS 수준 접근, 특정 SQL Server 기능, 완전한 제어가 필요하면 SQL Server on Azure VM이 적합하다.</li>
<li>대규모 확장, 서버리스, 하이퍼스케일 요구가 있으면 Azure SQL Database의 Hyperscale 또는 Serverless 옵션을 고려한다.</li>
</ul>
<hr>
<h2 id="azure-sql-db-내부-구조-control-ring-vs-data-ring">Azure SQL DB 내부 구조: Control Ring vs Data Ring</h2>
<p>Azure SQL은 단일 서버가 아니라 Control Ring과 Data Ring으로 나뉜 분산 시스템이다. Control Ring은 라우팅을 담당하고, Data Ring은 실제 연산을 수행한다. 백엔드 노드에 장애가 발생해도 Control Ring이 정상 노드로 트래픽을 우회하여 연결 단절을 최소화한다.
<img src="https://velog.velcdn.com/images/rudin_/post/c792eff0-db64-46eb-bba8-2039babc5cc8/image.png" alt=""></p>
<table>
<thead>
<tr>
<th>구성 요소</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>클라이언트 App</td>
<td>포트 1433으로 쿼리 요청</td>
</tr>
<tr>
<td>Control Ring / Gateway Layer</td>
<td>TDS 프로토콜의 앞문 역할, 인증 처리, 방화벽 검사, DB 위치 메타데이터 확인</td>
</tr>
<tr>
<td>Data Ring / Database Compute Layer</td>
<td>SQL Server 프로세스가 동작하는 컨테이너/VM 풀, 쿼리 파싱·컴파일·실행</td>
</tr>
<tr>
<td>Storage Layer</td>
<td>Azure Blob Storage 기반 데이터 및 로그 파일 저장</td>
</tr>
</tbody></table>
<hr>
<h2 id="연결-토폴로지-proxy-vs-redirect">연결 토폴로지: Proxy vs Redirect</h2>
<p>Azure SQL은 성능과 네트워크 보안 요구에 따라 Proxy와 Redirect 방식의 연결 토폴로지를 제공한다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>Proxy 모드</th>
<th>Redirect 모드</th>
</tr>
</thead>
<tbody><tr>
<td>작동 방식</td>
<td>모든 통신이 Gateway를 경유하여 Data Node로 전달</td>
<td>최초 연결 시 Gateway에 노드 위치를 질의한 뒤, 이후 Data Node에 직접 연결</td>
</tr>
<tr>
<td>포트 요구사항</td>
<td>아웃바운드 TCP 1433만 개방</td>
<td>TCP 1433 + 11000~11999 범위 개방 필요</td>
</tr>
<tr>
<td>장점</td>
<td>보안 설정이 단순함</td>
<td>지연 시간 최소화, 처리량 극대화</td>
</tr>
<tr>
<td>사용 환경</td>
<td>인터넷을 통한 외부 연결 시 기본값</td>
<td>Azure 내부망, VNet, VM 연결 시 기본값 또는 성능 권장 방식</td>
</tr>
</tbody></table>
<hr>
<h2 id="azure-sql-주요-기능">Azure SQL 주요 기능</h2>
<p>Azure SQL은 안전하고 안정적인 운영을 위해 다양한 기능을 제공한다.</p>
<table>
<thead>
<tr>
<th>기능</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Business continuity</td>
<td>비즈니스 연속성 보장</td>
</tr>
<tr>
<td>High Availability</td>
<td>고가용성 구성</td>
</tr>
<tr>
<td>Automated Backups</td>
<td>자동 백업</td>
</tr>
<tr>
<td>Geo-replication</td>
<td>지리적 복제</td>
</tr>
<tr>
<td>Scalability</td>
<td>확장성</td>
</tr>
<tr>
<td>Automated patching</td>
<td>자동 패치</td>
</tr>
<tr>
<td>Security</td>
<td>보안</td>
</tr>
<tr>
<td>Automatic tuning</td>
<td>자동 튜닝</td>
</tr>
<tr>
<td>Built-in monitoring and intelligence</td>
<td>내장 모니터링 및 지능형 분석</td>
</tr>
<tr>
<td>Migrating to Azure</td>
<td>Azure 마이그레이션 지원</td>
</tr>
</tbody></table>
<hr>
<h1 id="구매-모델-및-스케일링-전략">구매 모델 및 스케일링 전략</h1>
<h2 id="azure-sql-배포-옵션">Azure SQL 배포 옵션</h2>
<p>Azure SQL은 다양한 워크로드 요구 사항에 맞게 여러 배포 옵션을 제공한다.</p>
<table>
<thead>
<tr>
<th>배포 옵션</th>
<th>설명</th>
<th>대표 형태</th>
</tr>
</thead>
<tbody><tr>
<td>SQL virtual machines</td>
<td>OS 수준 접근이 필요한 마이그레이션 및 애플리케이션에 적합</td>
<td>SQL virtual machine</td>
</tr>
<tr>
<td>Managed instances</td>
<td>리프트 앤 시프트 마이그레이션에 적합</td>
<td>Single instance, Instance pool</td>
</tr>
<tr>
<td>Databases</td>
<td>최신 클라우드 애플리케이션에 적합</td>
<td>Single database, Elastic pool</td>
</tr>
</tbody></table>
<p>SQL virtual machines는 SQL Server와 OS를 직접 접근하고 관리할 수 있다. Managed Instance는 SQL Server surface area 대부분을 지원하면서도 완전 관리형 서비스이다. Database는 Hyperscale, Serverless, Elastic Pool 등을 통해 최신 앱에 적합한 운영 방식을 제공한다.
Elastic이라는 단어가 들어갔다면 확장성에 집중한 모델임을 알 수 있다.</p>
<hr>
<h2 id="dtu-vs-vcore-모델">DTU vs vCore 모델</h2>
<p>Azure SQL Database는 DTU 기반 구매 모델과 vCore 기반 구매 모델을 제공한다. vCore 기반 모델이 권장된다.</p>
<table>
<thead>
<tr>
<th>구매 모델</th>
<th>설명</th>
<th>적합한 대상</th>
</tr>
</thead>
<tbody><tr>
<td>DTU 기반</td>
<td>컴퓨팅, 스토리지, IO 리소스를 번들로 묶은 측정값 기반. 단일 DB는 DTU, 탄력적 풀은 eDTU로 표시</td>
<td>간단하고 미리 구성된 리소스 옵션을 원하는 고객</td>
</tr>
<tr>
<td>vCore 기반</td>
<td>컴퓨팅과 스토리지 리소스를 독립적으로 선택 가능. Azure 하이브리드 혜택으로 비용 절감 가능</td>
<td>유연성, 제어, 투명성을 중요시하는 고객</td>
</tr>
</tbody></table>
<p>DTU는 단순하고 미리 구성된 리소스 옵션을 제공하는 반면, vCore는 CPU와 스토리지 등 리소스를 더 투명하게 선택할 수 있다.</p>
<hr>
<h2 id="provisioned-vs-serverless-설계-및-auto-pause">Provisioned vs Serverless 설계 및 Auto-pause</h2>
<p>워크로드 패턴에 따라 고정 리소스 방식과 자동 스케일링 방식을 선택할 수 있다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>Provisioned</th>
<th>Serverless</th>
</tr>
</thead>
<tbody><tr>
<td>동작 방식</td>
<td>24시간 내내 지정된 vCore와 메모리 항상 할당</td>
<td>최소~최대 vCore 범위 안에서 수요에 따라 자동 스케일링</td>
</tr>
<tr>
<td>과금</td>
<td>시간당 고정 과금</td>
<td>초 단위 과금</td>
</tr>
<tr>
<td>장점</td>
<td>성능이 일정하고 쿼리 응답 지연이 없음</td>
<td>사용하지 않을 때 비용 절감 가능</td>
</tr>
<tr>
<td>Auto-pause</td>
<td>없음</td>
<td>지정 시간 동안 쿼리가 없으면 DB 일시 중지, 스토리지 비용만 청구</td>
</tr>
<tr>
<td>Auto-resume</td>
<td>없음</td>
<td>새 연결 발생 시 자동 재개, 첫 연결 시 1~2초 지연 가능</td>
</tr>
<tr>
<td>적용 대상</td>
<td>트래픽이 꾸준하거나 리소스 사용량을 예측 가능한 Production 워크로드</td>
<td>간헐적·예측 불가능한 워크로드, 야간 트래픽 없는 시스템, Dev/Test 환경</td>
</tr>
</tbody></table>
<hr>
<h2 id="scalability">Scalability</h2>
<p>Azure SQL 서비스 유형별 확장 방식과 특징은 다르다.</p>
<table>
<thead>
<tr>
<th>서비스 유형</th>
<th>확장 방식</th>
<th>주요 특징 및 옵션</th>
<th>유의할 점</th>
</tr>
</thead>
<tbody><tr>
<td>Azure SQL Database (PaaS)</td>
<td>포털에서 CPU/메모리/스토리지 슬라이더로 즉시 상향/하향 조정, Elastic Pool/서버리스 등 리소스 풀링·자동 확장</td>
<td>다운타임 없이 수분 내 리소스 증감, Elastic Pool로 여러 DB 간 자원 공유, 서버리스 부하 기반 자동 확장/축소, Premium·Hyperscale·DTU/vCore 옵션</td>
<td>자동 확장/축소는 서버리스 전용, Elastic/Hyperscale은 별도 과금 및 일부 제한</td>
</tr>
<tr>
<td>Azure SQL Managed Instance</td>
<td>인스턴스 단위 CPU/메모리/스토리지 증감, Instance Pool로 여러 인스턴스 자원 풀링</td>
<td>Portal/CLI에서 인스턴스 리사이즈, Instance Pool 활용 가능</td>
<td>자동화 옵션 없음, 수동 확장, 증설 시 엔진 재구동 등 단기 중단 가능</td>
</tr>
<tr>
<td>SQL Server on Azure VM (IaaS)</td>
<td>VM 크기 변경, 디스크 추가/변경, AG 등으로 Scale-Out</td>
<td>Azure VM 관리화면에서 VM 스펙 교체, 스토리지 추가, AG로 수평 분산 가능</td>
<td>VM 리사이즈나 스토리지 증설 시 OS/DB 재시작 필요, 직접 관리 필요</td>
</tr>
</tbody></table>
<hr>
<h2 id="elastic-database-풀">Elastic Database 풀</h2>
<p>Elastic Database Pool은 여러 데이터베이스 또는 인스턴스 간에 리소스를 공유하고 비용을 최적화하는 기능이다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>SQL Database Elastic Pool</th>
<th>SQL Managed Instance Pool</th>
</tr>
</thead>
<tbody><tr>
<td>개념</td>
<td>하나의 프로비저닝된 SQL Database 리소스 세트 내에서 여러 DB 호스트</td>
<td>여러 Managed Instance를 호스트하고 리소스를 공유</td>
</tr>
<tr>
<td>장점</td>
<td>여러 DB 성능을 하나의 간소화된 방식으로 관리·모니터링</td>
<td>컴퓨팅 리소스를 사전 프로비저닝하여 배포 시간 단축, 더 작은 MI 구성 가능</td>
</tr>
<tr>
<td>적합 사례</td>
<td>SaaS 애플리케이션 또는 공급자</td>
<td>대규모 Managed Instance 마이그레이션 및 통합</td>
</tr>
<tr>
<td>상태</td>
<td>일반적으로 사용</td>
<td>공개 미리 보기 상태로 언급됨</td>
</tr>
</tbody></table>
<p>Paychex 사례에서는 여러 고객의 시간 및 결제 관리를 개별 DB로 운영하면서도 비용 절감을 위해 SQL Database Elastic Pool을 선택했다.</p>
<hr>
<h2 id="하이퍼스케일-모델-지역-중복-가용성">하이퍼스케일 모델: 지역 중복 가용성</h2>
<p>하이퍼스케일 모델은 기존 로컬/공유 스토리지 모델과 달리 컴퓨트와 스토리지 계층을 완전히 분리한다. 이로 인해 대용량 데이터, 빠른 확장성, 효율적 장애 복구를 제공한다.</p>
<table>
<thead>
<tr>
<th>구성 요소</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>컴퓨팅 노드</td>
<td>읽기/쓰기 노드와 읽기 전용 노드 등으로 구성 가능</td>
</tr>
<tr>
<td>페이지 서버</td>
<td>데이터 페이지를 관리하는 분산 스토리지 계층</td>
</tr>
<tr>
<td>로그 서비스</td>
<td>로그 처리를 담당하는 고가용성 구성 요소</td>
</tr>
<tr>
<td>영구 스토리지</td>
<td>Azure Storage 기반, 네이티브 고가용성 및 중복 기능 제공</td>
</tr>
<tr>
<td>Azure Service Fabric</td>
<td>구성 요소 상태를 제어하고 장애 시 정상 노드로 장애 조치 수행</td>
</tr>
</tbody></table>
<p>하이퍼스케일 모델의 특징은 다음과 같다.</p>
<ul>
<li>컴퓨트와 스토리지가 완전히 분리된다.</li>
<li>여러 컴퓨트와 여러 스토리지 구성 요소가 독립적으로 확장된다.</li>
<li>노드 장애 시 다른 컴퓨트 노드에서 서비스를 재개할 수 있다.</li>
<li>수십 TB급 대용량 데이터베이스에 적합하다.</li>
<li>새 복제본 또는 스냅샷을 빠르게 생성할 수 있다.</li>
</ul>
<hr>
<h2 id="하이퍼스케일-모델-영역간-중복-가용성">하이퍼스케일 모델: 영역간 중복 가용성</h2>
<p>영역 중복성을 선택하면 하이퍼스케일 계층 전체에 대해 가용성 영역 간 복제가 적용되어 영역 수준 복원력이 보장된다.</p>
<ul>
<li>애플리케이션 로직 변경 없이 엔터프라이즈급 내결함성 구성 가능</li>
<li>한 가용성 영역 장애 시 실시간 자동 failover 지원</li>
<li>데이터 손실 없는 고가용성 보장</li>
<li>서비스 중단 없이 패치 및 업그레이드 가능</li>
<li>대용량 확장성과 빠른 복구 제공</li>
</ul>
<hr>
<h1 id="네트워크-보안">네트워크 보안</h1>
<h2 id="방화벽-규칙-서버-수준-vs-db-수준">방화벽 규칙: 서버 수준 vs DB 수준</h2>
<p>Azure SQL 방화벽은 서버 수준과 데이터베이스 수준 두 겹으로 동작한다. 최소 권한 원칙에 따라 특정 DB에만 접근을 허용하는 데이터베이스 수준 방화벽 규칙이 권장된다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>서버 수준 방화벽</th>
<th>데이터베이스 수준 방화벽</th>
</tr>
</thead>
<tbody><tr>
<td>적용 범위</td>
<td>논리적 SQL 서버에 속한 모든 DB 접근 허용</td>
<td>규칙이 생성된 특정 단일 DB에만 접근 허용</td>
</tr>
<tr>
<td>설정 방법</td>
<td>Azure Portal, PowerShell, Azure CLI, REST API</td>
<td>포털 설정 불가, T-SQL <code>sp_set_database_firewall_rule</code> 사용</td>
</tr>
<tr>
<td>보안성</td>
<td>범위가 넓어 상대적으로 낮음</td>
<td>특정 DB만 허용하므로 높음</td>
</tr>
<tr>
<td>주의사항</td>
<td>“Azure 서비스 및 리소스에서 이 서버에 액세스하도록 허용” 옵션은 전 세계 Azure 서비스 IP를 허용하므로 위험</td>
<td>규칙 관리가 T-SQL 기반</td>
</tr>
</tbody></table>
<p>규칙 평가 순서는 데이터베이스 수준 규칙이 먼저이며, 매칭되면 해당 DB만 접속된다. 데이터베이스 수준 규칙이 없으면 서버 수준 규칙을 확인하고, 둘 다 없으면 접속이 차단된다.</p>
<hr>
<h2 id="vnet-service-endpoint-vs-private-link">VNet Service Endpoint vs Private Link</h2>
<p>PaaS 데이터베이스를 퍼블릭 인터넷에서 격리하고 회사 내부망과 연동하는 방식은 서비스에 따라 다르다.
온프레미스라면 VNet Injection이 적합(다만 전용인 VNet Injection이 더 비쌈)</p>
<p>각각 공용, 전용
| 구분 | Azure SQL Database: Private Link | SQL Managed Instance: VNet Injection |
|---|---|---|
| 방식 | 프라이빗 엔드포인트 사용 | 가상 네트워크 주입 방식 |
| 구조 | 기존 VNet 변경 불필요, 개별 DB 단위 사설 IP 매핑 | MI 전용 서브넷 필수 |
| 인프라 | 외부 PaaS 인프라 유지 | 내 VNet 안에 서버를 통째로 넣는 구조 |
| 장점 | 개별 DB 단위로 안전하게 사설 접근 가능 | 완벽한 양방향 통신, 온프레미스 VPN 연동, 물리적 격리에 가까운 수준 |</p>
<hr>
<h1 id="데이터베이스-보안-및-관리">데이터베이스 보안 및 관리</h1>
<h2 id="데이터-암호화-및-접근-제어">데이터 암호화 및 접근 제어</h2>
<p>Azure SQL은 TDE, Always Encrypted, RLS, DDM 등 다계층 데이터 보호를 제공한다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>기능</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>물리적 암호화</td>
<td>TDE</td>
<td>디스크에 저장되는 데이터와 백업 파일을 실시간 암호화. 기본값 ON. 디스크 탈취 시에도 데이터 보호</td>
</tr>
<tr>
<td>물리적 암호화</td>
<td>Always Encrypted</td>
<td>클라이언트 애플리케이션에서 데이터를 암호화한 뒤 DB 엔진으로 전송. DB 메모리에서도 암호화 상태 유지</td>
</tr>
<tr>
<td>논리적 필터링</td>
<td>RLS</td>
<td>로그인 사용자 권한에 따라 보이는 행을 필터링. 예: 서울 지점 직원은 서울 지점 매출만 조회</td>
</tr>
<tr>
<td>논리적 필터링</td>
<td>DDM</td>
<td>주민등록번호, 신용카드 번호 등 민감한 열을 마스킹해 반환. 실제 데이터는 변경되지 않음</td>
</tr>
</tbody></table>
<hr>
<h2 id="microsoft-entra-id-및-비밀번호-없는-연결">Microsoft Entra ID 및 비밀번호 없는 연결</h2>
<p>Azure SQL은 SQL Server 인증 대신 Microsoft Entra ID 기반 토큰 인증을 지원한다. 이를 통해 소스코드에서 비밀번호를 제거할 수 있다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>중앙 집중식 통제</td>
<td>퇴사자 발생 시 Entra ID에서 계정만 비활성화하면 DB 접근 차단</td>
</tr>
<tr>
<td>MFA 지원</td>
<td>로그인 시 스마트폰 앱 승인 등 추가 인증 강제 가능</td>
</tr>
<tr>
<td>온프레미스 AD 연동</td>
<td>기존 사내 Active Directory와 동기화하여 SSO 가능</td>
</tr>
<tr>
<td>Managed Identity</td>
<td>Azure 리소스에 고유 ID를 부여하고, Connection String에 <code>Authentication=Active Directory Managed Identity</code>를 사용</td>
</tr>
<tr>
<td>Entra-only authentication</td>
<td>SQL sa 계정 로그인을 원천 차단하고 토큰 기반 접근만 허용</td>
</tr>
</tbody></table>
<hr>
<h2 id="azure-sql-ledger">Azure SQL Ledger</h2>
<p>Azure SQL Ledger는 블록체인의 SHA-256 해시 기술을 관계형 데이터베이스 엔진에 탑재하여 데이터 위변조를 탐지할 수 있도록 하는 기능이다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>작동 원리</td>
<td>INSERT/UPDATE/DELETE 발생 시 트랜잭션 내용을 SHA-256 해시로 암호화하여 블록 생성</td>
</tr>
<tr>
<td>해시 체인</td>
<td>이전 트랜잭션 해시를 다음 트랜잭션이 참조하여 중간 데이터 조작 시 전체 해시값이 깨짐</td>
</tr>
<tr>
<td>Updatable Ledger</td>
<td>일반 테이블처럼 UPDATE/DELETE 가능. 변경 이력은 History Table에 영구 보존</td>
</tr>
<tr>
<td>Append-Only Ledger</td>
<td>INSERT만 가능. UPDATE/DELETE는 엔진 레벨에서 거부</td>
</tr>
<tr>
<td>사용 사례</td>
<td>SIEM, 보안 감사 로그, 금융 거래, 결제 내역, 외부 감사용 데이터 무결성 증명</td>
</tr>
</tbody></table>
<hr>
<h1 id="성능-모니터링-및-최적화">성능 모니터링 및 최적화</h1>
<h2 id="built-in-monitoring-and-intelligence">Built-in monitoring and intelligence</h2>
<p>Azure SQL은 여러 도구를 통해 모니터링과 성능 분석을 제공한다.</p>
<table>
<thead>
<tr>
<th>기능/도구</th>
<th>설명</th>
<th>Azure SQL Database</th>
<th>Managed Instance</th>
<th>SQL on Azure VM</th>
</tr>
</thead>
<tbody><tr>
<td>Azure Monitor</td>
<td>CPU, 메모리, 저장소, 연결 등 리소스 실시간 관찰, 알림 및 진단 로그 집계</td>
<td>지원</td>
<td>지원</td>
<td>지원</td>
</tr>
<tr>
<td>Database Watcher</td>
<td>DB 성능, 건강 상태, 트랜잭션 등 심층 모니터링 및 대시보드</td>
<td>지원</td>
<td>지원</td>
<td>미지원</td>
</tr>
<tr>
<td>Query Performance Insights</td>
<td>상위 리소스 소모/비효율 쿼리 현황 시각화, 실행 성능 분석</td>
<td>지원</td>
<td>일부 지원</td>
<td>SSMS에서 지원</td>
</tr>
<tr>
<td>Intelligent Insights</td>
<td>AI 기반 장애/성능 저하 원인 자동 감지 및 해결 가이드 제공</td>
<td>지원</td>
<td>지원</td>
<td>미지원</td>
</tr>
<tr>
<td>Alert &amp; 대시보드</td>
<td>포털 기반 임계치 알림, 상태/로그 대시보드 제공</td>
<td>지원</td>
<td>지원</td>
<td>Portal/Log Analytics 지원</td>
</tr>
<tr>
<td>Deep Query Analytics</td>
<td>Query Store 등으로 쿼리 실행 이력, 실행 계획, 상세 워크로드 분석</td>
<td>지원</td>
<td>지원</td>
<td>직접 또는 외부 도구 활용</td>
</tr>
<tr>
<td>Best Practice 검사</td>
<td>보안, 아키텍처, 성능 등 운영 모범 실천 기준 자동 점검 및 경고</td>
<td>지원</td>
<td>지원</td>
<td>IaaS Agent 필요</td>
</tr>
</tbody></table>
<hr>
<h2 id="대기-통계-wait-stats-분석-방법">대기 통계 Wait Stats 분석 방법</h2>
<p>Wait Stats는 쿼리가 실행되는 동안 어떤 자원을 기다리느라 시간이 소요되었는지 알려주는 성능 트러블슈팅 핵심 지표이다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Wait Stats 의미</td>
<td>SQL 엔진이 필요한 자원을 기다린 시간과 이유를 기록한 데이터</td>
</tr>
<tr>
<td>분석 가치</td>
<td>“쿼리가 느리다”를 “디스크 읽기를 기다리느라 70% 시간을 썼다”처럼 구체화 가능</td>
</tr>
<tr>
<td>DMV</td>
<td><code>sys.dm_db_wait_stats</code>를 통해 DB 레벨 누적 대기 통계 확인</td>
</tr>
<tr>
<td>Query Store</td>
<td>특정 쿼리별, 시간대별 대기 통계를 과거 이력까지 추적</td>
</tr>
</tbody></table>
<table>
<thead>
<tr>
<th>주요 Wait Type</th>
<th>의미</th>
<th>해결 방향</th>
</tr>
</thead>
<tbody><tr>
<td>PAGEIOLATCH_*</td>
<td>디스크에서 데이터 페이지를 메모리로 읽어오기를 기다림</td>
<td>인덱스 튜닝, 메모리 증설</td>
</tr>
<tr>
<td>LCK_M_*</td>
<td>다른 쿼리가 테이블/행 잠금을 잡고 있어 해제를 기다림</td>
<td>트랜잭션 최적화</td>
</tr>
<tr>
<td>CXPACKET</td>
<td>병렬 쿼리 처리 중 스레드 간 속도 차이로 인한 대기</td>
<td>MAXDOP 설정 조정</td>
</tr>
</tbody></table>
<hr>
<h2 id="automatic-tuning-및-지능형-인사이트">Automatic tuning 및 지능형 인사이트</h2>
<p>PaaS 서비스인 Azure SQL Database와 Managed Instance는 AI 기반 자동/추천 성능 최적화 기능을 제공한다. VM 기반 SQL은 운영자가 직접 튜닝해야 한다.</p>
<table>
<thead>
<tr>
<th>서비스 유형</th>
<th>지원 여부</th>
<th>주요 기능/특징</th>
</tr>
</thead>
<tbody><tr>
<td>Azure SQL Database (PaaS)</td>
<td>기본 제공, 자동/수동 설정</td>
<td>자동 인덱스 관리, 인덱스 자동 생성/삭제, 실행 계획 비효율 발견 시 자동 롤백, 지속적 성능 분석, 튜닝 이력 제공</td>
</tr>
<tr>
<td>Azure SQL Managed Instance</td>
<td>기본 제공, 동일</td>
<td>Azure SQL Database와 동일</td>
</tr>
<tr>
<td>SQL Server on Azure VM (IaaS)</td>
<td>미지원, 직접 관리</td>
<td>수동 튜닝 필수, 자동 제안/적용 기능 없음</td>
</tr>
</tbody></table>
<hr>
<h1 id="고가용성-아키텍처-및-재해-복구">고가용성 아키텍처 및 재해 복구</h1>
<h2 id="high-availability">High Availability</h2>
<p>Azure SQL의 고가용성 내부 구현은 서비스 계층에 따라 다르다.</p>
<table>
<thead>
<tr>
<th>특성</th>
<th>General Purpose 계층</th>
<th>Business Critical 계층</th>
<th>Hyperscale 계층</th>
</tr>
</thead>
<tbody><tr>
<td>적용 서비스</td>
<td>Azure SQL Database &amp; Managed Instance</td>
<td>Azure SQL Database &amp; Managed Instance</td>
<td>Azure SQL Database 전용</td>
</tr>
<tr>
<td>HA 설계 원칙</td>
<td>컴퓨팅/스토리지 분리</td>
<td>Always On 가용성 그룹</td>
<td>분산 함수 모델, 컴퓨팅·스토리지·로그 분리</td>
</tr>
<tr>
<td>아키텍처 구성</td>
<td>스테이트리스 컴퓨팅 노드 클러스터, Azure Premium Storage, 3중 복제 스토리지</td>
<td>1개 주 복제본(RW), 3개 보조 복제본(RO), 모든 복제본 로컬 SSD 사용</td>
<td>1개 주 복제본(RW), 0~4개 HA 보조 복제본(RO), 분산 페이지 서버, 고가용성 로그 서비스</td>
</tr>
<tr>
<td>데이터 복제 방식</td>
<td>스토리지 계층에서 3중 복제(LRS/ZRS)</td>
<td>동기식 복제</td>
<td>로그 서비스 및 페이지 서버를 통한 비동기 복제</td>
</tr>
<tr>
<td>장애 조치 메커니즘</td>
<td>컴퓨팅 노드 장애 시 다른 정상 노드로 연결 자동 전환</td>
<td>주 복제본 장애 시 보조 복제본 중 하나로 자동 승격</td>
<td>주 컴퓨팅 복제본 장애 시 HA 복제본 중 하나로 초고속 승격</td>
</tr>
<tr>
<td>RTO</td>
<td>수십 초</td>
<td>일반적으로 10초 이내</td>
<td>수 초</td>
</tr>
<tr>
<td>RPO</td>
<td>0, 커밋된 데이터 손실 없음</td>
<td>0, 데이터 손실 없음</td>
<td>0, 데이터 손실 없음</td>
</tr>
<tr>
<td>읽기 스케일 아웃</td>
<td>제한적</td>
<td>보조 복제본을 통한 읽기 스케일 아웃 가능</td>
<td>HA 보조 복제본을 통한 읽기 스케일 아웃 가능</td>
</tr>
<tr>
<td>스토리지 유형</td>
<td>원격 Azure Premium Storage</td>
<td>로컬 SSD</td>
<td>분산 페이지 서버 기반 관리형 스토리지</td>
</tr>
<tr>
<td>주요 장점</td>
<td>비용 효율성, 컴퓨팅/스토리지 독립 확장성</td>
<td>높은 성능, 낮은 RTO/RPO, 미션 크리티컬 워크로드 적합</td>
<td>극대화된 확장성, 초고속 복구, 대규모 워크로드 적합</td>
</tr>
</tbody></table>
<hr>
<h2 id="availability-architectural-models">Availability Architectural Models</h2>
<p>Azure SQL Database와 SQL Managed Instance는 각각 고유한 고가용성 아키텍처 모델을 제공한다.</p>
<table>
<thead>
<tr>
<th>서비스</th>
<th>고가용성 아키텍처 모델</th>
</tr>
</thead>
<tbody><tr>
<td>Azure SQL Database</td>
<td>General Purpose: 원격/로컬 저장소 분리, Business Critical: Always On/로컬 스토리지/복제, Hyperscale: 분산 스토리지·컴퓨트 계층 구조</td>
</tr>
<tr>
<td>SQL Managed Instance</td>
<td>General Purpose: Standard Availability, Business Critical: Always On 기반 고가용성 클러스터</td>
</tr>
</tbody></table>
<hr>
<h2 id="azure-sql-database-가용성-모델">Azure SQL Database 가용성 모델</h2>
<table>
<thead>
<tr>
<th>서비스 티어</th>
<th>고가용성 모드</th>
<th>지역 중복 가용성</th>
<th>영역간 중복 가용성</th>
</tr>
</thead>
<tbody><tr>
<td>General Purpose (vCore)</td>
<td>원격 스토리지</td>
<td>예</td>
<td>예</td>
</tr>
<tr>
<td>Business Critical (vCore)</td>
<td>로컬 스토리지</td>
<td>예</td>
<td>예</td>
</tr>
<tr>
<td>Hyperscale (vCore)</td>
<td>하이퍼스케일</td>
<td>예</td>
<td>예</td>
</tr>
<tr>
<td>Basic (DTU)</td>
<td>원격 스토리지</td>
<td>예</td>
<td>아니오</td>
</tr>
<tr>
<td>Standard (DTU)</td>
<td>원격 스토리지</td>
<td>예</td>
<td>아니오</td>
</tr>
<tr>
<td>Premium (DTU)</td>
<td>로컬 스토리지</td>
<td>예</td>
<td>예</td>
</tr>
</tbody></table>
<hr>
<h2 id="azure-sql-managed-instance-가용성-모델">Azure SQL Managed Instance 가용성 모델</h2>
<table>
<thead>
<tr>
<th>서비스 티어</th>
<th>고가용성 모드</th>
<th>지역 중복 가용성</th>
<th>영역간 중복 가용성</th>
</tr>
</thead>
<tbody><tr>
<td>General Purpose (vCore)</td>
<td>원격 스토리지</td>
<td>예</td>
<td>예</td>
</tr>
<tr>
<td>Next-gen General Purpose (vCore) - preview</td>
<td>원격 스토리지</td>
<td>예</td>
<td>예</td>
</tr>
<tr>
<td>Business Critical (vCore)</td>
<td>로컬 스토리지</td>
<td>예</td>
<td>예</td>
</tr>
</tbody></table>
<hr>
<h2 id="원격-스토리지-모델-지역-중복-가용성">원격 스토리지 모델: 지역 중복 가용성</h2>
<p>DTU 기반 Basic/Standard 계층과 vCore 기반 General Purpose 계층은 Remote Storage 가용성 모델을 사용한다. 컴퓨팅 레이어와 저장소 계층이 분리되어 있다.</p>
<p>Stateful한건 늘리거나 하지 않고, Stateless에 computing 가능한 부분을 두어 늘릴 수 있게 함.
예를 들자면, 홈쇼핑 데이터베이스라면 주 기능에 관한건 Stateful한 부분에, 그리고 접속이나 계산 등 computing하고 가변적으로 늘려야 하는 부분은 Stateless 사용</p>
<table>
<thead>
<tr>
<th>계층</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Stateless compute layer</td>
<td>데이터베이스 엔진 프로세스를 실행하는 컴퓨팅 계층. 장애 시 다른 노드로 전환 가능</td>
</tr>
<tr>
<td>Stateful data layer</td>
<td>Azure Blob/Premium Storage에 데이터 파일과 로그 파일 저장. 원격 스토리지 기반 복제 제공</td>
</tr>
</tbody></table>
<hr>
<h2 id="로컬-스토리지-모델-지역-중복-가용성">로컬 스토리지 모델: 지역 중복 가용성</h2>
<p>DTU Premium 계층과 vCore Business Critical 계층은 컴퓨팅 리소스와 로컬 SSD 스토리지를 단일 노드에 통합하는 로컬 스토리지 모델을 사용한다.</p>
<ul>
<li>각 컴퓨트 노드에는 데이터베이스 엔진과 로컬 SSD가 결합되어 있다.</li>
<li>주요 데이터 파일과 로그 파일이 각 노드의 로컬 SSD에 저장된다.</li>
<li>Always On Availability Group을 통해 노드 간 동기화 복제를 수행한다.</li>
<li>로컬 SSD 직접 접근으로 IO 지연이 낮고 성능이 높다.</li>
<li>장애 발생 시 동기화된 Secondary 노드가 Primary로 승격된다.</li>
</ul>
<p>로컬이 더 비싸고 빠름</p>
<hr>
<h2 id="원격-스토리지-모델-영역간-중복-가용성">원격 스토리지 모델: 영역간 중복 가용성</h2>
<p>Zone-redundant 옵션을 사용하면 하나의 Region 내 서로 다른 가용성 영역에 컴퓨트 노드가 분산 배치된다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Remote storage model</td>
<td>컴퓨트와 스토리지가 분리. 컴퓨트 노드는 stateless, 영구 데이터는 Azure Premium Storage 등에 저장</td>
</tr>
<tr>
<td>Zone redundant availability</td>
<td>여러 Azure Zone에 Control Ring, 노드, 스토리지 계층이 존재. 데이터와 로그는 ZRS 등 zone-redundant storage에 보관</td>
</tr>
<tr>
<td>장애 대응</td>
<td>Azure Traffic Manager를 통해 정상 Zone 노드로 자동 우회</td>
</tr>
</tbody></table>
<hr>
<h2 id="로컬-스토리지-모델-영역간-중복-가용성">로컬 스토리지 모델: 영역간 중복 가용성</h2>
<p>Premium 또는 Business Critical 계층에서 영역 중복을 사용하면 복제본이 동일 지역의 여러 가용성 영역에 배치된다.</p>
<ul>
<li>SPOF 제거를 위해 Control Ring이 여러 영역에 걸쳐 복제된다.</li>
<li>게이트웨이 링 라우팅은 Azure Traffic Manager가 제어한다.</li>
<li>기존 복제본을 다양한 가용성 영역에 배치하므로 추가 비용 없이 사용할 수 있다.</li>
<li>데이터센터 중단 같은 큰 장애에도 탄력적으로 복구 가능하다.</li>
<li>기존 Premium/Business Critical DB 또는 Elastic Pool을 영역 중복 구성으로 변환할 수 있다.</li>
</ul>
<hr>
<h2 id="automated-backups">Automated Backups</h2>
<p>Azure SQL 서비스별 자동 백업 방식은 다르다.</p>
<table>
<thead>
<tr>
<th>유형</th>
<th>자동 백업 기본 제공</th>
<th>백업 종류/주기</th>
<th>보존 기간</th>
<th>백업 저장 위치</th>
<th>추가 설정/특징</th>
</tr>
</thead>
<tbody><tr>
<td>Azure SQL Database</td>
<td>O, 자동</td>
<td>주 1회 전체, 12시간마다 증분, 약 10분마다 로그</td>
<td>7~35일 기본, 최대 10년 LTR 옵션</td>
<td>RA-GRS</td>
<td>PITR, LTR 지원</td>
</tr>
<tr>
<td>SQL Managed Instance</td>
<td>O, 자동</td>
<td>전체, 차등, 로그</td>
<td>7~35일 기본, 최대 10년 LTR</td>
<td>RA-GRS</td>
<td>PaaS 서비스의 통합 자동 백업 관리</td>
</tr>
<tr>
<td>SQL on Azure VM</td>
<td>X, 별도 설정 필요</td>
<td>사용자 지정</td>
<td>사용자 지정</td>
<td>Azure Storage, Recovery Services Vault, 외부 저장소</td>
<td>SQL IaaS Agent, Azure Backup 등 설정 필요. 관리자가 주기·보존·저장소 직접 결정</td>
</tr>
</tbody></table>
<p>LTR: Long Term Retention</p>
<hr>
<h2 id="automated-patching">Automated Patching</h2>
<p>Azure SQL Database와 Azure SQL Managed Instance는 PaaS 서비스이므로 OS와 SQL 데이터베이스 엔진의 최신 보안 업데이트 및 성능 개선 패치가 자동 적용된다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>Azure SQL Database / Managed Instance</td>
<td>OS 및 SQL 엔진 패치 자동 적용, 관리자가 별도 패치하지 않아도 최신 버전과 보안 수준 유지</td>
</tr>
<tr>
<td>Managed Instance 유지 관리 기간</td>
<td>자동 패치가 적용될 주간 시간대를 선택해 단기 가용성 영향을 제어 가능</td>
</tr>
<tr>
<td>Managed Instance 업데이트 정책</td>
<td>Always Up-to-date 정책 또는 지연 정책 선택 가능</td>
</tr>
<tr>
<td>SQL Server on Azure VM</td>
<td>SQL IaaS Agent Extension 등록 및 별도 설정 필요. Windows Update, 보안 패치, SQL Patch 스케줄을 관리자가 직접 구성 가능</td>
</tr>
</tbody></table>
<hr>
<h2 id="business-continuity-참고">Business Continuity 참고</h2>
<p>Business Continuity는 기업의 핵심 자산, 서비스, 수익에 대한 위협을 식별하고, 주요 비즈니스 기능이 재난이나 장애 상황에서도 계속 운영될 수 있도록 하는 전략이다.</p>
<p>Azure SQL Database에서는 내장된 고가용성, 지역 중복, 장애 복구 기능을 통해 서비스 지속성을 보장한다. SPOF는 시스템, 네트워크, 소프트웨어 등 특정 지점 장애가 전체 시스템 중단으로 이어지는 단일 실패 지점을 의미한다.</p>
<hr>
<h2 id="azure-site-recovery-참고">Azure Site Recovery 참고</h2>
<p>Azure Site Recovery는 Microsoft Azure에서 제공하는 재해 복구 서비스이다. 주요 IT 중단 발생 시에도 비즈니스 애플리케이션과 워크로드를 계속 실행하여 Business Continuity를 보장하도록 돕는다.</p>
<hr>
<h2 id="geo-replication">Geo-replication</h2>
<p>Geo-replication은 Azure SQL Database, Managed Instance, SQL Server on Azure VM에 따라 지원 방식이 다르다.</p>
<table>
<thead>
<tr>
<th>서비스 계층</th>
<th>주요 Geo-replication 옵션</th>
<th>복제 방향/방식</th>
<th>페일오버/관리 특징</th>
<th>지원 및 제한 사항</th>
</tr>
</thead>
<tbody><tr>
<td>Azure SQL Database (PaaS)</td>
<td>Active Geo-replication: 최대 4개 세컨더리 지원, Failover groups: 그룹 단위 페일오버/엔드포인트 자동 전환</td>
<td>단일 DB, Elastic Pool, 그룹 단위. 단방향/읽기 전용 세컨더리</td>
<td>자동 복제, 수동/자동 페일오버 선택. Failover group으로 일괄 관리, 연결 엔드포인트 자동 생성</td>
<td>PaaS라 설정/운영 간편. 서버리스, Hyperscale 등은 제한적 지원</td>
</tr>
<tr>
<td>Azure SQL Managed Instance</td>
<td>Failover groups: 전체 인스턴스 단위 DR, Geo-replication 일부</td>
<td>인스턴스 전체/그룹 단위, 읽기/쓰기 세컨더리</td>
<td>Failover group 자동/수동, 엔드포인트 리디렉션, 정책에 따른 자동 복구</td>
<td>Managed Instance 전용. 일부 설정/기능 변동 가능</td>
</tr>
<tr>
<td>SQL Server on Azure VM (IaaS)</td>
<td>Always On Availability Groups, Distributed Availability Group</td>
<td>VM 간 멀티 리전, 양방향 AG, 복수 세컨더리</td>
<td>쿼럼/AG 정책에 따른 페일오버. 복수 세컨더리로 로드 분산 가능. 수동/자동 다양</td>
<td>IaaS 직접 구성. Windows Failover Cluster 필요, 네트워크/쿼럼 설정 직접 관리, 비용/관리 책임</td>
</tr>
</tbody></table>
<hr>
<h1 id="migration--innovation">Migration &amp; Innovation</h1>
<h2 id="migrating-to-azure">Migrating to Azure</h2>
<p>Azure는 온프레미스 SQL Server, 다른 클라우드, 타 DB 등에서 Azure SQL 제품으로 안전하게 이전하기 위한 절차와 도구를 제공한다.</p>
<table>
<thead>
<tr>
<th>도구/방식</th>
<th>대상 플랫폼</th>
<th>주요 방식/설명</th>
<th>지원 대상/특이사항</th>
</tr>
</thead>
<tbody><tr>
<td>Azure Database Migration Service (DMS)</td>
<td>온프레미스 SQL Server, 타 클라우드 DB, Oracle, MySQL 등</td>
<td>온라인/오프라인 마이그레이션, 다운타임 최소화, 스키마+데이터+보안+연결 전환 지원</td>
<td>Azure SQL Database, Managed Instance, SQL Server on VM</td>
</tr>
<tr>
<td>Dacpac / BACPAC</td>
<td>모든 SQL Server/DB</td>
<td>스키마 및 데이터 옵션을 dacpac/bacpac 파일로 추출 후 신규 DB에 업로드 및 Import</td>
<td>소형 또는 부분 데이터 이전에 적합</td>
</tr>
<tr>
<td>Log Replay Service</td>
<td>온프레미스 SQL → Managed Instance</td>
<td>트랜잭션 로그 전송, 실시간 또는 순차적 로그 누적 적용</td>
<td>Managed Instance 전용</td>
</tr>
<tr>
<td>Managed Instance Link</td>
<td>온프레미스/VM SQL Server → Managed Instance</td>
<td>Always On AG 기술 활용, 실시간 데이터 싱크</td>
<td>Hybrid DR, 긴밀한 실시간 연동에 적합</td>
</tr>
<tr>
<td>Native backup/restore</td>
<td>온프레미스/VM SQL Server → Managed Instance</td>
<td>백업 파일을 Azure Storage로 업로드</td>
<td>대용량, 장기 보관, 이관, 복구에 적합</td>
</tr>
<tr>
<td>Distributed Availability Groups</td>
<td>온프레미스 ↔ Azure VM 상 SQL Server</td>
<td>AG 기반 장애 복구, 저지연 클러스터, VM 간 이중화 복제, 직접적 고가용성 구성</td>
<td>IaaS VM 기반 SQL 전용</td>
</tr>
<tr>
<td>Azure Migrate/Arc</td>
<td>대규모, 복합 자원, 하이브리드 등 다양한 환경</td>
<td>전체 인프라/워크로드 평가, 추천, 예측, DB 포함 전체 인프라 및 서비스 분석·상품 제안</td>
<td>대형·엔터프라이즈·하이브리드 환경에 적합</td>
</tr>
</tbody></table>
<hr>
<h2 id="클라우드-마이그레이션-비즈니스-드라이버-및-전략">클라우드 마이그레이션: 비즈니스 드라이버 및 전략</h2>
<p>성공적인 클라우드 마이그레이션은 기술 이동만이 아니라 명확한 비즈니스 이유를 기반으로 전략을 선택하는 과정이다.</p>
<table>
<thead>
<tr>
<th>단계</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>비즈니스 목표 정의</td>
<td>클라우드 채택으로 달성하려는 상위 수준 성과 정의. 예: AI 도입, 민첩성 향상, 비용 절감, 혁신 가속화</td>
</tr>
<tr>
<td>격차 식별</td>
<td>현재 상태와 목표 수준 간 차이 분석. 성능, 확장성, 규정 준수, 아키텍처 제한 식별</td>
</tr>
<tr>
<td>비즈니스 드라이버 결정</td>
<td>파악된 격차를 메워야 하는 구체적이고 실행 가능한 이유 확정. 최적의 8R 전략 선택 기준</td>
</tr>
</tbody></table>
<table>
<thead>
<tr>
<th>비즈니스 드라이버</th>
<th>마이그레이션 전략</th>
</tr>
</thead>
<tbody><tr>
<td>중복되거나 가치가 낮은 워크로드를 용도 폐기</td>
<td>Retire</td>
</tr>
<tr>
<td>비즈니스 중단 최소화, 가까운 시일 내 현대화 계획 없음</td>
<td>Rehost</td>
</tr>
<tr>
<td>관리 부담을 줄이고 신뢰성을 높이기 위해 PaaS 솔루션과 최소한의 코드 수정 필요</td>
<td>Replatform</td>
</tr>
<tr>
<td>기술 부채를 줄이거나 클라우드 최적화를 위해 코드 수정 필요</td>
<td>Refactor</td>
</tr>
<tr>
<td>클라우드 네이티브 기능 활용을 위해 아키텍처 변경 필요</td>
<td>Rearchitect</td>
</tr>
<tr>
<td>운영 단순화를 위해 SaaS/AI 솔루션 필요</td>
<td>Replace</td>
</tr>
<tr>
<td>요구사항 충족을 위해 새로운 클라우드 네이티브 솔루션 필요</td>
<td>Rebuild</td>
</tr>
<tr>
<td>안정성이 필요하고 변경 사항이 없어야 함</td>
<td>Retain</td>
</tr>
</tbody></table>
<hr>
<h2 id="azure-cloud-migration-strategy-8r">Azure Cloud Migration Strategy: 8R</h2>
<table>
<thead>
<tr>
<th>전략</th>
<th>Business driver</th>
<th>주요 지표</th>
</tr>
</thead>
<tbody><tr>
<td>Retire</td>
<td>중복되거나 가치가 낮은 워크로드를 용도 폐기</td>
<td>현재 또는 미래 비즈니스 가치 제한, 마이그레이션/현대화 비용이 이점보다 큼</td>
</tr>
<tr>
<td>Rehost</td>
<td>비즈니스 중단 최소화, 현대화 계획 없음</td>
<td>워크로드 안정적, Azure 호환, 마이그레이션 리스크 낮음, 단기 클라우드 도입 목표, 현대화 급하지 않음, 자본 지출 절감, 데이터센터 공간 확보, Azure 경험 부족</td>
</tr>
<tr>
<td>Replatform</td>
<td>관리 부담 감소, 신뢰성 향상, PaaS와 최소 코드 수정 필요</td>
<td>안정성과 재해 복구 단순화, OS 및 라이선스 관리 부담 감소, 적절한 투자로 전환 시간 단축, 애플리케이션 컨테이너화</td>
</tr>
<tr>
<td>Refactor</td>
<td>기술 부채 감소 또는 클라우드 최적화를 위한 코드 수정</td>
<td>유지보수 비용 감소, 기술 부채 감소, Azure SDK 사용, 코드 성능 개선, 코드 비용 최적화, 클라우드 디자인 패턴 적용, 모니터링용 코드 계측 적용</td>
</tr>
<tr>
<td>Rearchitect</td>
<td>클라우드 네이티브 기능 활용을 위한 아키텍처 변경</td>
<td>애플리케이션 모듈화/서비스 분해 필요, 구성 요소별 확장 요구 다름, 미래 혁신 지원 필요, 기술 스택 혼재</td>
</tr>
<tr>
<td>Replace</td>
<td>운영 단순화를 위해 SaaS/AI 솔루션 필요</td>
<td>운영 단순화, 내부 개발 리소스를 다른 곳에 활용, 커스터마이징 필요성 적음</td>
</tr>
<tr>
<td>Rebuild</td>
<td>새로운 클라우드 네이티브 솔루션 필요</td>
<td>레거시 시스템이 낡거나 유연하지 않음, 더 빠른 개발/출시 필요, 운영 비용 절감, 최신 프레임워크와 도구 필요</td>
</tr>
<tr>
<td>Retain</td>
<td>안정성이 필요하고 변경 사항이 없어야 함</td>
<td>워크로드 안정적, 규정 준수, 비즈니스 요구 충족, 단기 이동 동인 없음, ROI 낮음</td>
</tr>
</tbody></table>
<p>참고로 Gartner의 5R 이후 AWS에서는 Repurchase를 추가한 6R 및 7R이 많이 활용되며, Azure에서는 8R로 분류해 설명한다.</p>
<hr>
<h2 id="aws-cloud-migration-strategy-참고">AWS Cloud Migration Strategy 참고</h2>
<p>AWS 7R은 다음과 같이 시각적으로 정리된다.</p>
<table>
<thead>
<tr>
<th>그룹</th>
<th>전략</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Sustain</td>
<td>Rehost</td>
<td>Lift and shift, Amazon EC2에 호스트</td>
</tr>
<tr>
<td>Sustain</td>
<td>Relocate</td>
<td>Hypervisor-level lift and shift, 인프라를 클라우드로 이동</td>
</tr>
<tr>
<td>Optimize</td>
<td>Replatform</td>
<td>Lift and reshape, 일부 클라우드 기능 활용</td>
</tr>
<tr>
<td>Optimize</td>
<td>Repurchase</td>
<td>Drop and shop, 일반적으로 SaaS 제품으로 전환</td>
</tr>
<tr>
<td>Grow</td>
<td>Refactor</td>
<td>Re-architect, Amazon Aurora나 DynamoDB 등 목적 기반 DB 활용</td>
</tr>
<tr>
<td>별도 판단</td>
<td>Retain</td>
<td>유지</td>
</tr>
<tr>
<td>별도 판단</td>
<td>Retire</td>
<td>폐기</td>
</tr>
</tbody></table>
<hr>
<h1 id="최신-기능">최신 기능</h1>
<h2 id="vector-데이터-지원-및-rag-아키텍처">Vector 데이터 지원 및 RAG 아키텍처</h2>
<p>Azure SQL Database는 VECTOR 데이터 형식과 VECTOR_DISTANCE 함수를 지원하여 별도 벡터 DB 없이 기존 관계형 데이터와 함께 RAG 아키텍처를 구현할 수 있다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>VECTOR 데이터 타입</td>
<td>OpenAI 등 AI 모델이 생성한 다차원 임베딩 배열을 테이블 컬럼에 직접 저장 가능</td>
</tr>
<tr>
<td>VECTOR_DISTANCE 함수</td>
<td>코사인 유사도 등을 이용해 사용자 질문과 의미가 비슷한 데이터를 SQL 쿼리로 검색</td>
</tr>
<tr>
<td>기존 문제</td>
<td>RDBMS 데이터와 벡터 DB가 분리되어 데이터 동기화 및 조인 분석이 어려움</td>
</tr>
<tr>
<td>해결 방식</td>
<td>Azure SQL 하나에서 일반 데이터 필터링과 의미론적 벡터 검색을 동시에 수행. RLS 정책도 유지 가능</td>
</tr>
</tbody></table>
<hr>
<h2 id="json-native-support">JSON Native Support</h2>
<p>기존에는 JSON을 NVARCHAR 문자열 컬럼에 담아 처리했지만, JSON 타입으로 처리하면서 성능 개선이 가능해졌다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>개선 내용</th>
</tr>
</thead>
<tbody><tr>
<td>Total Storage Footprint</td>
<td>약 82% 감소</td>
</tr>
<tr>
<td>Data I/O</td>
<td>약 80% 감소</td>
</tr>
<tr>
<td>Query Execution</td>
<td>약 2.5~4배 빠름</td>
</tr>
<tr>
<td>Throughput</td>
<td>약 20~40배 증가</td>
</tr>
<tr>
<td>CPU Usage</td>
<td>약 27% 감소</td>
</tr>
<tr>
<td>Logical Reads</td>
<td>쿼리 실행당 약 80% 감소</td>
</tr>
<tr>
<td>예시 저장 공간</td>
<td>사용량이 5.94GB 수준에서 1.06GB 수준으로 감소한 비교 화면 제시</td>
</tr>
</tbody></table>
<hr>
<h2 id="fabric-mirrored-databases-연동">Fabric Mirrored Databases 연동</h2>
<p>Azure SQL은 Microsoft Fabric의 Mirrored Database와 연결된다. 포털에서 미러링을 켜면 Azure SQL 데이터가 Fabric OneLake에 실시간에 가깝게 Delta 포맷으로 복제된다.</p>
<table>
<thead>
<tr>
<th>특징</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Zero-ETL</td>
<td>복잡한 파이프라인 개발 없이 버튼 클릭만으로 연동</td>
</tr>
<tr>
<td>Near Real-time</td>
<td>Insert, Update, Delete 변경분을 실시간에 가깝게 증분 복제</td>
</tr>
<tr>
<td>Delta Parquet</td>
<td>분석에 최적화된 개방형 포맷으로 자동 변환 저장</td>
</tr>
<tr>
<td>성능 격리</td>
<td>원본 운영 DB 성능 저하 없이 OLAP 분석 가능</td>
</tr>
</tbody></table>
<hr>
<h1 id="사례">사례</h1>
<h2 id="azure-virtual-machines의-sql-server-사례-allscripts">Azure Virtual Machines의 SQL Server 사례: Allscripts</h2>
<p>Allscripts는 의료 서비스 소프트웨어 제조업체이다. 애플리케이션을 안전하고 안정적으로 호스트하기 위해 Azure로 빠르게 이동하려 했고, Azure Site Recovery를 사용해 약 1,000개의 VM에서 실행 중이던 애플리케이션 수십 개를 3주 만에 Azure로 마이그레이션했다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>고객 과제</td>
<td>애플리케이션을 자주 변환하고 안정적으로 호스트해야 함</td>
</tr>
<tr>
<td>선택 서비스</td>
<td>Azure Virtual Machines의 SQL Server</td>
</tr>
<tr>
<td>주요 도구</td>
<td>Azure Site Recovery</td>
</tr>
<tr>
<td>결과</td>
<td>약 1,000개 VM 기반 애플리케이션을 빠르게 Azure로 이동</td>
</tr>
</tbody></table>
<hr>
<h2 id="azure-sql-managed-instance-사례-komatsu">Azure SQL Managed Instance 사례: Komatsu</h2>
<p>Komatsu는 건설용 중장비 제조 회사로, 여러 메인프레임 애플리케이션의 다양한 데이터를 통합적으로 파악하고 오버헤드를 줄이고자 했다. SQL Server 기능 호환성이 중요했기 때문에 Azure SQL Managed Instance를 선택했다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>고객 과제</td>
<td>여러 메인프레임 애플리케이션 통합, 관리 오버헤드 절감</td>
</tr>
<tr>
<td>선택 서비스</td>
<td>Azure SQL Managed Instance</td>
</tr>
<tr>
<td>이전 데이터</td>
<td>약 1.5TB</td>
</tr>
<tr>
<td>주요 혜택</td>
<td>자동 패치, 버전 업데이트, 자동 백업, 고가용성, 관리 오버헤드 절감</td>
</tr>
<tr>
<td>결과</td>
<td>약 49% 비용 절감, 약 25~30% 성능 향상</td>
</tr>
</tbody></table>
<hr>
<h2 id="azure-sql-database-사례-accuweather">Azure SQL Database 사례: AccuWeather</h2>
<p>AccuWeather는 날씨 분석 및 예측 기업으로, 빅데이터, 머신러닝, AI 기능을 활용하기 위해 Azure를 선택했다. 데이터베이스 관리보다 모델과 애플리케이션 구축에 집중하고자 SQL Database를 Azure Data Factory, Azure Machine Learning 등과 함께 사용했다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>고객 과제</td>
<td>기상 분석 및 예측 기능을 강화하고 빅데이터/AI 기능 활용 필요</td>
</tr>
<tr>
<td>선택 서비스</td>
<td>Azure SQL Database</td>
</tr>
<tr>
<td>연계 서비스</td>
<td>Azure Data Factory, Azure Machine Learning</td>
</tr>
<tr>
<td>목적</td>
<td>매출 및 고객 예측을 위한 내부 애플리케이션 빠른 배포</td>
</tr>
<tr>
<td>주요 장점</td>
<td>관리 부담 감소, 확장성, 최신 클라우드 서비스와의 연계</td>
</tr>
</tbody></table>
<hr>
<h1 id="전체-정리">전체 정리</h1>
<p>Azure SQL은 SQL Server 엔진을 기반으로 한 Azure의 통합 데이터베이스 제품군이다. Azure SQL Database, Azure SQL Managed Instance, SQL Server on Azure VM은 각각 관리 수준, 호환성, 제어 범위가 다르며, 워크로드 특성에 따라 선택해야 한다.</p>
<p>PaaS 기반의 Azure SQL Database와 Managed Instance는 자동 패치, 자동 백업, 고가용성, 보안, 성능 모니터링, 자동 튜닝을 제공한다. 특히 Serverless, Elastic Pool, Hyperscale, Private Link, Entra ID 인증, Ledger, Vector, Fabric Mirroring 등 최신 기능을 통해 운영 부담을 줄이면서 확장성과 보안성을 강화할 수 있다.</p>
<p>마이그레이션 관점에서는 DMS, BACPAC, Log Replay Service, Managed Instance Link, Native backup/restore, Azure Migrate/Arc 등 다양한 도구를 제공한다. 클라우드 이전 전략은 Retire, Rehost, Replatform, Refactor, Rearchitect, Replace, Rebuild, Retain의 8R 관점에서 비즈니스 목표와 워크로드 특성에 맞게 선택해야 한다.</p>
<h2 id="한-줄-요약">한 줄 요약</h2>
<p>Azure SQL은 SQL Server 기반 워크로드를 Azure에서 운영하기 위한 통합 데이터베이스 플랫폼이며, 서비스 유형별로 관리 책임과 호환성, 확장성, 보안 기능이 다르므로 워크로드 특성에 맞는 선택이 중요하다.</p>
<hr>

<h1 id="실습">실습</h1>
<h2 id="azure-sql-생성">Azure SQL 생성</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/a5dd552b-b5bd-4417-8d31-2646873bf966/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/4332b036-0a9a-4157-b73b-27513bc7ab80/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/46a8cf2a-c811-46f4-8992-aef1736cc76e/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/c556d7af-16e2-481c-a861-f1de339c802c/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/e3c396c6-ba87-45a1-bf9b-60eac2cc7003/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/1aacb894-3cf3-47c9-8945-5203ae1e41c8/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/ebef80d2-3751-4607-86c4-75a5f5a07986/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/fda10f43-4124-4305-b9b5-c1ae7e48226d/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/31bc14b9-dea0-40b6-87a5-2fb20f5c35d2/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MicrosoftDataSchool] 75일차 - Azure DevOps]]></title>
            <link>https://velog.io/@rudin_/MicrosoftDataSchool-75%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@rudin_/MicrosoftDataSchool-75%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Thu, 23 Apr 2026 08:51:59 GMT</pubDate>
            <description><![CDATA[<h1 id="azure-devops">Azure DevOps</h1>
<p>소프트웨어 개발 프로젝트의 계획, 개발, 테스트 및 배포 전 과정을 지원하는 통합 플랫폼</p>
<table>
<thead>
<tr>
<th>주요서비스명</th>
<th>역할 및 기능</th>
<th>개발 단계</th>
</tr>
</thead>
<tbody><tr>
<td>Azure Boards</td>
<td>애자일 계획, 작업 추적, 백로그 관리, 칸반 보드 및 스크럼 지원</td>
<td>계획 &amp; 추적</td>
</tr>
<tr>
<td>Azure Repos</td>
<td>Git 기반의 무제한 프라이빗 저장소 제공, 코드 버전 관리 및 협업</td>
<td>코드 관리</td>
</tr>
<tr>
<td>Azure Pipelines</td>
<td>CI/CD 파이프라인 자동화, 코드 커밋 시 자동 빌드, 테스트, 배포 실행</td>
<td>빌드 &amp; 배포</td>
</tr>
<tr>
<td>Azure Test Plans</td>
<td>수동 및 탐색적 테스트 도구, 테스트 케이스 관리 및 실행 결과 추적</td>
<td>테스트 관리</td>
</tr>
<tr>
<td>Azure Artifacts</td>
<td>Maven, npm, NuGet 등 패키지 종속성 저장 및 공유 피드 관리</td>
<td>패키지 관리</td>
</tr>
</tbody></table>
<p><img src="https://velog.velcdn.com/images/rudin_/post/067a901c-470f-4acf-acf7-2308f3cb33f1/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/4cb563a0-1743-4fe3-931a-efee965fb16e/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/20f34848-d45d-4781-b6db-cb1ccba94057/image.png" alt=""></p>
<h1 id="graphql-기반-api-배포">GraphQL 기반 API 배포</h1>
<p>Fabric에서 레이크하우스 생성
<img src="https://velog.velcdn.com/images/rudin_/post/5a00d4af-4364-4c1f-846a-ee5612c9923e/image.png" alt="">
graphQL 생성
<img src="https://velog.velcdn.com/images/rudin_/post/7e245c89-d3a6-49b7-94c5-917a2fec0ebc/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/ea348329-09e5-4d57-bb3f-d03ed10b690d/image.png" alt="">
데이터 업로드 후 graphQL 연결
<img src="https://velog.velcdn.com/images/rudin_/post/e011b76c-d9dc-46ad-bece-8bab01fef528/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/8314f411-0d03-41ba-a9a2-c96edc0ac723/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/d4706f92-92f7-4ea0-aa0c-4b1a32d52390/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/77c3ddb1-d2c5-4870-a823-d17a0fd54a13/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/a00d84e8-669d-45bb-8b8e-0d901d0b5396/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/a1fdbf86-7e9a-4118-8688-11818909e105/image.png" alt="">
엔드포인트 복사로 확인
<img src="https://velog.velcdn.com/images/rudin_/post/99d7d999-f656-490a-b0fe-1663e953f700/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/983d7859-cc71-47d2-902f-8ce39baee757/image.png" alt="">
network를 확인해서 http request 내역 확인 가능
<code>엔드포인트</code>, <code>메소드</code>, <code>헤더</code>, <code>바디</code></p>
<h2 id="postman으로-확인">Postman으로 확인</h2>
<p>Authorization Token은 개발자도구로 확인해야하는데, safari를 사용한다면 환경설정에서 웹 개발자 도구 보기를 선택해야한다.
<img src="https://velog.velcdn.com/images/rudin_/post/98497710-80d6-4d99-abbb-4b41dbee3f5e/image.png" alt="">
<code>opt</code>+<code>cmd</code>+<code>i</code>를 누르고, 개발자도구탭에서 네트워크를 선택한다
<img src="https://velog.velcdn.com/images/rudin_/post/f28d55a6-0d81-4b70-a920-31a543c470b0/image.png" alt="">
graphql을 찾고, 우클릭해서 curl로 복사 선택
<img src="https://velog.velcdn.com/images/rudin_/post/bdac6ac8-840f-4aec-aeea-8a3d9529ac0e/image.png" alt="">
이후 postman에서 import로 추가해준다.
<img src="https://velog.velcdn.com/images/rudin_/post/bdf51464-2f6c-43b1-9adc-ff9b384cb959/image.png" alt="">
자동으로 authorization token이 들어가고
<img src="https://velog.velcdn.com/images/rudin_/post/f2b6aaf8-984f-4ed4-849f-4dc0156cbaf2/image.png" alt="">
정상적으로 200 OK 가 떨어진다.
<img src="https://velog.velcdn.com/images/rudin_/post/a7742a73-c7fc-45e4-9242-ec3bbfcedc31/image.png" alt="">
참고로 체크는 GraphQL로 해야하는데, 로그인 안하면 지원 안하는듯. 로그인하니 뜬다.
<img src="https://velog.velcdn.com/images/rudin_/post/95aebe9a-83fc-45ea-ae8f-4297d31ef167/image.png" alt=""></p>
<h2 id="가상환경-생성">가상환경 생성</h2>
<pre><code># 1. &#39;fabric-lab&#39;이라는 이름의 새로운 폴더(디렉토리)를 생성합니다.
mkdir fabric-lab

# 2. 생성한 &#39;fabric-lab&#39; 폴더 안으로 이동합니다.
cd fabric-lab

# 3. &#39;fabric&#39;이라는 이름의 콘다 가상 환경을 생성합니다.
#    -c conda-forge: 패키지를 가져올 채널(저장소) 지정
#    nodejs=24: 최신 버전인 Node.js 24 버전을 함께 설치합니다.
conda create -n fabric -c conda-forge nodejs=24

# 4. 방금 만든 &#39;fabric3&#39; 가상 환경을 활성화합니다. 
# (주의: 위에서 &#39;fabric&#39;으로 만드셨다면 &#39;conda activate fabric&#39;이 맞습니다.)
conda activate fabric3

# 5. 서버 코드를 담을 &#39;server&#39; 폴더를 생성합니다.
mkdir server

# 6. &#39;server&#39; 폴더 안으로 이동합니다.
cd server

# 7. Node.js 프로젝트를 초기화합니다. (-y는 모든 설정을 기본값으로 자동 승인함)
# 이 명령어를 치면 package.json 파일이 생성됩니다.
npm init -y

# 8. 서버 운영에 필요한 핵심 라이브러리들을 설치합니다.
# express: 웹 서버 프레임워크
# cors: 교차 출처 리소스 공유(보안 정책) 해결용
# @azure/identity: Azure 서비스 인증용 (패브릭 연결 시 필요)
# node-fetch@2: API 요청을 보내기 위한 라이브러리 (버전 2)
npm install express cors @azure/identity node-fetch@2
</code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/277a3b61-ac90-4c02-a8f1-8f8413d457ce/image.png" alt=""></p>
<h2 id="샘플-코드-실행">샘플 코드 실행</h2>
<p>fabric-lab/server 에서 실행</p>
<pre><code>npm init -y
npm install express cors @azure/identity node-fetch@2</code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/9f24797e-ab40-4265-ac84-f6808ce1efe7/image.png" alt=""></p>
<p>fabric의 graphQL에서 코드 생성(js)
<img src="https://velog.velcdn.com/images/rudin_/post/56951edd-54c2-4aab-94ec-b23c18644afe/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/0c0ff989-3ac1-46e6-adf4-2b42eb47a421/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/f4b24ff0-e1dc-4f10-9c91-2ac1f62227cb/image.png" alt="">
commonjs 를 module로 바꿔줘야한다
<img src="https://velog.velcdn.com/images/rudin_/post/c4db75f6-eac7-4e81-aebb-b80c543712fc/image.png" alt=""></p>
<p>샘플 코드 저장 후 node {파일명}.js로 실행하면 브라우저 인증 후 데이터 조회 가능 (graphql.js 의 경로가 package.json과 동일한 경로여야 함)
이 경우 authentication을 브라우저에서 열리면서 해야한다.
<img src="https://velog.velcdn.com/images/rudin_/post/c4dfdd02-5b82-4039-ae03-065818f35af0/image.png" alt=""></p>
<p>클라이언트 secret을 통해서 인증하는방식으로 해야 앱 개발 가능(매번 브라우저 인증은 불가능)</p>
<h2 id="앱등록-후-액세스-관리">앱등록 후 액세스 관리</h2>
<p>Entra Id 화면- 관리- 앱등록에서 Fabric 전체를 관리할 어플리케이션 등록 가능
등록 후 ClientId와 Secretkey 발급 가능
이후 fabric에서 액세스관리에서 기여자로 추가
<code>tenant ID</code> <code>client ID</code> <code>client Secret</code> 필요</p>
<h2 id="클라이언트로-인증-처리">클라이언트로 인증 처리</h2>
<pre><code class="language-js">import { ClientSecretCredential, InteractiveBrowserCredential } from &quot;@azure/identity&quot;;

// Acquire a token
// DO NOT USE IN PRODUCTION.
// Below code to acquire token is for development purpose only to test the GraphQL endpoint
// For production, always register an application in a Microsoft Entra ID tenant and use the appropriate client_id and scopes
// https://learn.microsoft.com/en-us/fabric/data-engineering/connect-apps-api-graphql#create-a-microsoft-entra-app

const TENANT_ID = &quot;&quot;;
const CLIENT_ID = &quot;&quot;;
const CLIENT_SECRET = &quot;&quot;;

let app = new ClientSecretCredential(TENANT_ID, CLIENT_ID, CLIENT_SECRET);
let tokenPromise = app.getToken(&#39;https://analysis.windows.net/powerbi/api/.default&#39;);
let accessToken = await tokenPromise;

const endpoint = &#39;&#39;;
const query = `
query {
  namhae_travels(first: 10) {
     items {
        no
        name
        address
     }
  }
}
`;

const variables = 
  {

  }
  ;

const headers = {
    &#39;Content-Type&#39;: &#39;application/json&#39;,
    &#39;Authorization&#39;: `Bearer ${accessToken.token}`
};

async function fetchData()     {
    try {
        const response = await fetch(endpoint, {
            method: &#39;POST&#39;,
            headers: headers,
            body: JSON.stringify({ query, variables }),
        });

        const result = await response.json();
        console.log(JSON.stringify(result));
    } catch (error) {
        console.log(&#39;Error fetching data:&#39;, error);
    }
}

fetchData();
</code></pre>
<h2 id="devops에-레포지토리-업로드">DevOps에 레포지토리 업로드</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/22658406-906b-4f32-be2a-b8823ee08def/image.png" alt="">
devops의 repos-files의 링크를 local에서 add remote에 사용</p>
<p>vscode상에서는 차례대로 링크복붙-<code>origin</code>입력-git credentials 복붙(password)</p>
<p><img src="https://velog.velcdn.com/images/rudin_/post/5eb9521e-8093-4cc0-bb41-4844761921c0/image.png" alt=""></p>
<h2 id="ci-파이프라인">CI 파이프라인</h2>
<p>Settings-Agent Pool-Default
<img src="https://velog.velcdn.com/images/rudin_/post/ab9e4695-b06f-4fdf-a8ae-1aeacff5ee61/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/dfe5e44f-af78-447d-a9eb-6458446f4370/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/28c1546e-47a9-4e04-a2d9-53884a1ef6f6/image.png" alt=""></p>
<p>이후 mac에서는 엄청나게 비밀번호 입력을 많이 해야한다고 하셔서 안내해주신대로 비밀번호 입력을 한 번만 하도록 설정했다.(권한변경)</p>
<pre><code>sudo xattr -rd com.apple.quarantine &lt;agent파일 위치&gt;</code></pre><p>이후</p>
<pre><code>./config.sh</code></pre><p>하는 도중에 해당 폴더가 documents 하위에 있어 루트권한으로도 접근이 막히는 문제가 발생했다.
따라서 agent폴더 자체를 루트디렉토리로 옮겨서 해결했다.</p>
<p>PAT 토큰 발급은 우상단 user settings로 하면 된다.
<img src="https://velog.velcdn.com/images/rudin_/post/cdeb638c-0728-4f2e-9d4b-0600e95a362b/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/rudin_/post/a25f6801-74c9-4b6c-8bce-063587adf9b1/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/b7aad394-119a-41bf-8ccd-c9f457a699dc/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/rudin_/post/3b51b561-2254-41c8-8a86-c2f0b46f5f2b/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/eaabd040-57f5-4f02-b24c-ca952a6d6463/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/ddd2257b-ab65-4d95-8095-2e1134b75ace/image.png" alt=""></p>
<pre><code class="language-yaml">trigger:
  paths:
    include:
      - server/*

pool:
  name: &#39;Default&#39;

steps:
  - checkout: self
    clean: true       # 기존에 남은 찌꺼기 파일들을 깨끗이 지우고 시작
    fetchDepth: 1     # 최신 커밋 1개만 빠르게 가져옴
    displayName: &#39;Get Sources (Manual)&#39;
  - task: NodeTool@0
    inputs:
      versionSpec: &#39;24.x&#39;
    displayName: &#39;Install Node.js&#39;

  - script: |
      cd server
      npm install
      # NestJS나 TypeScript를 사용한다면 build 스크립트가 필수입니다.
      # 일반 Express라면 생략 가능하지만, 관례상 포함하는 경우가 많습니다.
      npm run build --if-present
    displayName: &#39;Install and Build&#39;

  - task: ArchiveFiles@2
    inputs:
      # 중요: &#39;server&#39; 폴더 전체를 압축하되,
      # 보통 node_modules를 포함해야 App Service에서 바로 실행됩니다.
      rootFolderOrFile: &#39;server&#39;
      includeRootFolder: false
      archiveType: &#39;zip&#39;
      archiveFile: &#39;$(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip&#39;
    displayName: &#39;Archive files&#39;

  - task: PublishBuildArtifacts@1
    inputs:
      PathtoPublish: &#39;$(Build.ArtifactStagingDirectory)&#39;
      ArtifactName: &#39;drop&#39;
    displayName: &#39;Publish Artifact&#39;
</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/5aecf973-5b18-4a5a-9f48-dc32e4ea44b7/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/b7ac5ae2-3657-4681-a570-3fcbc2d1ea01/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/9fdf6355-bd6f-436b-890e-d7463de3e6a5/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/rudin_/post/338222f6-483e-4507-822e-3849ef162049/image.png" alt=""></p>
<pre><code class="language-python">import { ClientSecretCredential } from &quot;@azure/identity&quot;;
import express from &#39;express&#39;;
import cors from &#39;cors&#39;;
import fetch from &#39;node-fetch&#39;;

const TENANT_ID = &quot;&quot;;
const CLIENT_ID = &quot;&quot;;
const CLIENT_SECRET = &quot;&quot;;

let credential = new ClientSecretCredential(TENANT_ID, CLIENT_ID, CLIENT_SECRET);
let tokenPromise = credential.getToken(&#39;https://analysis.windows.net/powerbi/api/.default&#39;);
let accessToken = await tokenPromise;

const app = express();
app.use(cors()); //이부분 괄호 안쳤다가 접속 안됨
app.use(express.json());

const config = {

}

app.get(&#39;/&#39;, (req, res) =&gt; {
    res.json({
        status: &quot;OK&quot;,
        message: &quot;조회에 성공하였습니다.&quot;,
        token: accessToken
    })
});

app.listen(3000, () =&gt; {
    console.log(&quot;Server is running on port 3000&quot;);
});</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/169afd7e-4d97-4e58-9943-4d0189d06e20/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/99411786-5b53-441e-8232-030d2901f553/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MicrosoftDataSchool] 74일차 - Fabric T-SQL, Data Warehouse, Direct Lake, KQL]]></title>
            <link>https://velog.io/@rudin_/MicrosoftDataSchool-74%EC%9D%BC%EC%B0%A8-Fabric-T-SQL-Data-Warehouse-Direct-Lake-KQL</link>
            <guid>https://velog.io/@rudin_/MicrosoftDataSchool-74%EC%9D%BC%EC%B0%A8-Fabric-T-SQL-Data-Warehouse-Direct-Lake-KQL</guid>
            <pubDate>Wed, 22 Apr 2026 08:35:09 GMT</pubDate>
            <description><![CDATA[<h1 id="microsoft-fabric-data-warehouse를-활용한-고성능-t-sql-분석-및-데이터-통합">Microsoft Fabric Data Warehouse를 활용한 고성능 T-SQL 분석 및 데이터 통합</h1>
<h2 id="sql-endpoint-vs-dedicated-warehouse">SQL Endpoint vs Dedicated Warehouse</h2>
<p>Microsoft Fabric는 전통적인 Data Warehouse 외에도 Lakehouse에 정제된 데이터를 두고 SQL Endpoint로 읽는 방법도 제공</p>
<table>
<thead>
<tr>
<th>특징</th>
<th>SQL Endpoint (Lakehouse)</th>
<th>Dedicated Warehouse</th>
<th>선택 기준</th>
</tr>
</thead>
<tbody><tr>
<td>용도</td>
<td>읽기 전용 분석 (Silver/Gold)</td>
<td>읽기/쓰기 모두 가능</td>
<td>데이터를 쓰냐? → DW, 읽기만? → Endpoint</td>
</tr>
<tr>
<td>데이터 위치</td>
<td>Lakehouse의 Delta Table 직접 액세스</td>
<td>독립적 저장소</td>
<td>이미 Lakehouse 있다? → Endpoint</td>
</tr>
<tr>
<td>성능</td>
<td>V-Order 최적화 활용</td>
<td>전용 리소스</td>
<td>대용량 DW 작업 → DW, 분석 → Endpoint</td>
</tr>
<tr>
<td>비용</td>
<td>저렴 (공유 리소스)</td>
<td>높음 (전용 리소스)</td>
<td>예산 조건 확인</td>
</tr>
<tr>
<td>Power BI 연결</td>
<td>Direct Lake 최적</td>
<td>Direct Query / Import</td>
<td>Direct Lake 필요 → Endpoint</td>
</tr>
<tr>
<td>T-SQL 지원</td>
<td>완전 지원</td>
<td>완전 지원</td>
<td>문법 동일</td>
</tr>
</tbody></table>
<h2 id="spark-sql-vs-t-sql">Spark SQL vs T-SQL</h2>
<table>
<thead>
<tr>
<th>기능 영역</th>
<th>Spark SQL (Databricks/Fabric Notebook)</th>
<th>T-SQL (Fabric Warehouse/Endpoint)</th>
</tr>
</thead>
<tbody><tr>
<td>주 목적</td>
<td>대용량 데이터 처리 및 ETL 변환</td>
<td>정형 데이터 조회, 보고, 비즈니스 로직 구현</td>
</tr>
<tr>
<td>로직 구현</td>
<td>UDF (Python/Scala 결합)</td>
<td>Stored Procedure (SQL 전용)</td>
</tr>
<tr>
<td>트랜잭션</td>
<td>파일 기반 (ACID), 암시적</td>
<td>세션 기반, 명시적 (BEGIN TRAN)</td>
</tr>
<tr>
<td>제어 흐름</td>
<td>외부 코드 (Python/Scala)에 의존</td>
<td>SQL 자체 지원 (IF, WHILE, 변수)</td>
</tr>
<tr>
<td>사용자층</td>
<td>데이터 엔지니어, 데이터 사이언티스트</td>
<td>데이터 분석가, BI 개발자, DBA</td>
</tr>
<tr>
<td>문법 특징</td>
<td>LIMIT, current_date(), approx_distinct()</td>
<td>TOP, GETDATE(), COUNT(DISTINCT)</td>
</tr>
</tbody></table>
<p><em>T-SQL</em>: Microsoft SQL Server와 Azure SQL Database에서 사용하는 표준 SQL 언어의 확장판으로서 데이터 정의, 데이터 조작, 데이터 제어 뿐만 아니라, 절차적 프로그래밍 기능을 추가하여 복잡한 비즈니스 로직을 서버 측에서 구현</p>
<h3 id="t-sql을-쓰는-이유">T-SQL을 쓰는 이유</h3>
<ul>
<li><strong>Universal Connectivity</strong>: Excel, 3rd Party BI도구와 높은 호환성</li>
<li><strong>Standard Governance</strong>: GRATN/DENY 기반의 명확한 오브젝트 레벨 권한 관리</li>
<li><strong>Logic Encapsulation</strong>: Stored Procedure를 통해 비즈니스 로직을 DB 내부에 안전하게 격리</li>
</ul>
<h3 id="cross-database-query">Cross-Database Query</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/5f04340e-6cdb-46d5-adaf-948c22aa506c/image.png" alt=""></p>
<p>다른 Database, 다른 Lakehouse에 있는 테이블을 JOIN 하기 위해서 데이터를 이동할 필요가 없음</p>
<h3 id="power-bi-direct-lake-mode">Power BI Direct Lake Mode</h3>
<ul>
<li>Import는 메모리 한계가 있고 Direct Query는 느림</li>
<li>이럴때 Direct Lake를 사용하면 빠른 속도에 대용량 파일도 로드 가능</li>
</ul>
<table>
<thead>
<tr>
<th>Feature</th>
<th>Import</th>
<th>Direct Query</th>
<th>Direct Lake</th>
</tr>
</thead>
<tbody><tr>
<td>Speed</td>
<td>Very Fast</td>
<td>Slow</td>
<td>Very Fast</td>
</tr>
<tr>
<td>Data Copy</td>
<td>Yes (Duplication)</td>
<td>No</td>
<td>No (Zero Copy)</td>
</tr>
<tr>
<td>Freshness</td>
<td>Schedule Refresh</td>
<td>Real-time</td>
<td>Real-time</td>
</tr>
<tr>
<td>Limit</td>
<td>Memory Limit</td>
<td>DB Load</td>
<td>Large Scale Support</td>
</tr>
<tr>
<td>### Semantic Models</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Direct Lake모드가 제공하는 빠른 속도 덕분에 기본 모델 생성을 해서 BI 리포트 생성을 좀 더 편하게 해주는 default semantic model 기능이 제공되었으나, 자동 생성의 부작용으로 필요 없는 모델들이 생기고 워크스페이스가 복잡해짐에 따라서 해당 기능은 중단되었고, 대신 사용자가 명시적으로 생성하도록 변경</td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody></table>
<hr>

<h1 id="실습-microsoft-fabric에서-data-warehouse-쿼리">실습: Microsoft Fabric에서 Data Warehouse 쿼리</h1>
<p><img src="https://velog.velcdn.com/images/rudin_/post/40432b5e-abcf-464e-a68c-a5da4e9ce9f3/image.png" alt=""></p>
<h2 id="data-warehouse-쿼리">Data Warehouse 쿼리</h2>
<p>SQL 쿼리 편집기는 IntelliSense, 코드 완성, 구문 강조 표시, 클라이언트 측 구문 분석(parsing) 및 유효성 검사를 지원합니다. Data Definition Language (DDL), Data Manipulation Language (DML) 및 Data Control Language (DCL) 문을 실행할 수 있다.</p>
<pre><code class="language-sql">SELECT
    D.MonthName,
    COUNT(*) AS TotalTrips,
    SUM(T.TotalAmount) AS TotalRevenue
FROM dbo.Trip AS T
JOIN dbo.[Date] AS D
    ON T.[DateID]=D.[DateID]
GROUP BY D.MonthName;</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/6c1ed0fe-2621-4821-800d-eaad7380af08/image.png" alt=""></p>
<p>이 쿼리를 통해 각 월별 전체 이동 횟수와 총 수익을 확인할 수 있다.</p>
<hr>
<p>다음으로 요일별 평균 이동 시간과 평균 이동 거리를 분석한다.</p>
<pre><code class="language-sql">SELECT
    D.DayName,
    AVG(T.TripDurationSeconds) AS AvgDuration,
    AVG(T.TripDistanceMiles) AS AvgDistance
FROM dbo.Trip AS T
JOIN dbo.[Date] AS D
    ON T.[DateID]=D.[DateID]
GROUP BY D.DayName;</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/39c048a9-15ef-42da-9fe4-74c0d90a0258/image.png" alt=""></p>
<p>이를 통해 요일별 이동 패턴을 파악할 수 있다.</p>
<hr>
<p>또한, 이동이 많이 발생한 도시를 확인하기 위해
도시별 이동 수 상위 10개를 조회한다.</p>
<pre><code class="language-sql">SELECT TOP 10
    G.City,
    COUNT(*) AS TotalTrips
FROM dbo.Trip AS T
JOIN dbo.Geography AS G
    ON T.DropoffGeographyID=G.GeographyID
GROUP BY G.City
ORDER BY TotalTrips DESC;</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/a3c2bfad-8786-4f20-aac5-5fba16617849/image.png" alt=""></p>
<p>이 쿼리는 특정 도시에서의 이동량이 얼마나 집중되는지 확인하는 데 유용하다.</p>
<hr>
<h2 id="데이터-일관성-확인">데이터 일관성 확인</h2>
<p>분석 결과의 신뢰성을 확보하기 위해 데이터의 일관성을 확인한다.</p>
<p>먼저 비정상적으로 긴 이동 시간이 존재하는지 확인한다.
(24시간 = 86400초 기준)</p>
<pre><code class="language-sql">-- Check for trips with unusually long duration
SELECT COUNT(*) FROM dbo.Trip WHERE TripDurationSeconds &gt; 86400; -- 24 hours</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/20cf4f90-f478-4390-8720-58a4c81c09e3/image.png" alt=""></p>
<p>이 값이 존재한다면 데이터 오류 가능성을 의심할 수 있다.</p>
<hr>
<p>다음으로 음수 이동 시간이 존재하는지 확인한다.</p>
<pre><code class="language-sql">-- Check for trips with negative trip duration
SELECT COUNT(*) FROM dbo.Trip WHERE TripDurationSeconds &lt; 0;</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/40f90eda-eeb1-43eb-93f5-5d9c31c059b7/image.png" alt=""></p>
<p>음수 이동 시간은 명백한 데이터 오류이므로 반드시 제거해야 한다.
확인된 데이터 오류를 제거하여 데이터 품질을 개선한다.</p>
<pre><code class="language-sql">-- Remove trips with negative trip duration
DELETE FROM dbo.Trip WHERE TripDurationSeconds &lt; 0;</code></pre>
<p>이 과정을 통해 분석에 사용되는 데이터의 정확도를 높일 수 있다.</p>
<hr>
<h2 id="뷰로-저장">뷰로 저장</h2>
<p>이제 자주 사용하는 분석 쿼리를 View로 저장하여 재사용할 수 있도록 한다.</p>
<p>먼저 기본 집계 쿼리를 작성한다.</p>
<pre><code class="language-sql">SELECT
    D.DayName,
    AVG(T.TripDurationSeconds) AS AvgDuration,
    AVG(T.TripDistanceMiles) AS AvgDistance
FROM dbo.Trip AS T
JOIN dbo.[Date] AS D
    ON T.[DateID]=D.[DateID]
GROUP BY D.DayName;</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/b975a479-747f-434d-9cb9-403c246d3462/image.png" alt=""></p>
<hr>
<p>이후 특정 조건(예: 1월 데이터)으로 필터링한다.</p>
<pre><code class="language-sql">SELECT
    D.DayName,
    AVG(T.TripDurationSeconds) AS AvgDuration,
    AVG(T.TripDistanceMiles) AS AvgDistance
FROM dbo.Trip AS T
JOIN dbo.[Date] AS D
    ON T.[DateID]=D.[DateID]
WHERE D.Month = 1
GROUP BY D.DayName</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/c27c2100-fbb6-4eb4-9a8d-d22f026ee065/image.png" alt=""></p>
<hr>
<p>해당 쿼리를 선택한 후 <strong>Save as view</strong> 기능을 사용하여
<code>vw_JanTrip</code>이라는 이름으로 저장한다.
<img src="https://velog.velcdn.com/images/rudin_/post/0de3519e-0975-49aa-a04d-27510145eb08/image.png" alt=""></p>
<p>이렇게 생성된 View는 이후 반복적인 분석이나 BI 도구에서 재사용할 수 있다.</p>
<hr>

<h1 id="t-sql을-사용하여-data-warehouse에-데이터-로드">T-SQL을 사용하여 Data Warehouse에 데이터 로드</h1>
<h2 id="lakehouse-생성-및-데이터-준비">Lakehouse 생성 및 데이터 준비</h2>
<p>먼저 Microsoft Fabric에서 Workspace를 생성한 뒤, Lakehouse를 생성한다.</p>
<p>이후 제공된 <code>sales.csv</code> 파일을 Lakehouse의 Files 영역에 업로드한다.
업로드한 파일을 기반으로 <strong>테이블을 생성(Create table)</strong> 하여 <code>staging_sales</code> 테이블을 만든다.</p>
<p>이 테이블은 이후 Data Warehouse로 데이터를 적재하기 위한 <strong>Staging 영역</strong> 역할을 한다.</p>
<hr>
<h2 id="data-warehouse-생성">Data Warehouse 생성</h2>
<p>다음으로 Data Warehouse를 생성한다.</p>
<p>이 Warehouse는 Lakehouse의 데이터를 기반으로
분석용 Fact/Dimension 테이블을 구성하는 공간이다.</p>
<p>Warehouse에서 안하고 Lakehouse에서 하면 create table 할때 denied되니 명심하자</p>
<hr>
<h2 id="fact-및-dimension-테이블-생성">Fact 및 Dimension 테이블 생성</h2>
<p>Warehouse에서 SQL Query를 열고,
Fact 테이블과 Dimension 테이블을 생성한다.</p>
<pre><code class="language-sql">CREATE SCHEMA Sales;
GO

CREATE TABLE Sales.Fact_Sales
(
    SalesOrderNumber NVARCHAR(20) NOT NULL,
    SalesOrderLineNumber INT NOT NULL,
    OrderDate DATE NOT NULL,
    CustomerKey INT NOT NULL,
    ItemKey INT NOT NULL,
    Quantity INT,
    UnitPrice FLOAT,
    Tax FLOAT
);
GO

CREATE TABLE Sales.Dim_Customer
(
    CustomerKey INT IDENTITY(1,1) NOT NULL,
    CustomerName NVARCHAR(100),
    Email NVARCHAR(100)
);
GO

CREATE TABLE Sales.Dim_Item
(
    ItemKey INT IDENTITY(1,1) NOT NULL,
    ItemName NVARCHAR(100)
);
GO</code></pre>
<ul>
<li><code>Fact_Sales</code> → 판매 데이터 저장 (Fact Table)</li>
<li><code>Dim_Customer</code>, <code>Dim_Item</code> → 차원 테이블</li>
</ul>
<hr>
<h2 id="staging-데이터-연결-view-생성">Staging 데이터 연결 (View 생성)</h2>
<p>Lakehouse의 <code>staging_sales</code> 테이블을 Warehouse에서 참조하기 위해 View를 생성한다.</p>
<pre><code class="language-sql">CREATE VIEW Sales.Staging_Sales
AS
SELECT *
FROM staging_sales;</code></pre>
<p>이 View를 통해 Lakehouse 데이터를 Warehouse에서 직접 조회할 수 있다.</p>
<hr>
<h2 id="데이터-로드를-위한-저장-프로시저-생성">데이터 로드를 위한 저장 프로시저 생성</h2>
<p>Staging 데이터를 Fact/Dimension 테이블로 적재하기 위해
저장 프로시저를 생성한다.</p>
<pre><code class="language-sql">CREATE PROCEDURE Sales.LoadDataFromStaging (@OrderYear INT)
AS
BEGIN

-- Load customers
INSERT INTO Sales.Dim_Customer (CustomerName, Email)
SELECT DISTINCT CustomerName, Email
FROM Sales.Staging_Sales
WHERE YEAR(OrderDate) = @OrderYear
AND CustomerName NOT IN (SELECT CustomerName FROM Sales.Dim_Customer);

-- Load items
INSERT INTO Sales.Dim_Item (ItemName)
SELECT DISTINCT Item
FROM Sales.Staging_Sales
WHERE YEAR(OrderDate) = @OrderYear
AND Item NOT IN (SELECT ItemName FROM Sales.Dim_Item);

-- Load fact table
INSERT INTO Sales.Fact_Sales
SELECT
    s.SalesOrderNumber,
    s.SalesOrderLineNumber,
    s.OrderDate,
    c.CustomerKey,
    i.ItemKey,
    s.Quantity,
    s.UnitPrice,
    s.Tax
FROM Sales.Staging_Sales s
JOIN Sales.Dim_Customer c
    ON s.CustomerName = c.CustomerName
JOIN Sales.Dim_Item i
    ON s.Item = i.ItemName
WHERE YEAR(s.OrderDate) = @OrderYear;

END;</code></pre>
<p>이 프로시저는 다음 작업을 수행한다.</p>
<ul>
<li>고객 데이터 적재</li>
<li>상품 데이터 적재</li>
<li>Fact 테이블 적재</li>
</ul>
<hr>
<h2 id="데이터-로드-실행">데이터 로드 실행</h2>
<p>2021년 데이터를 Warehouse로 로드한다.</p>
<pre><code class="language-sql">EXEC Sales.LoadDataFromStaging 2021;</code></pre>
<hr>
<h2 id="데이터-분석">데이터 분석</h2>
<p>데이터가 정상적으로 로드되었는지 확인하기 위해
분석 쿼리를 실행한다.</p>
<hr>
<h3 id="고객별-총-판매액">고객별 총 판매액</h3>
<pre><code class="language-sql">SELECT
    c.CustomerName,
    SUM(f.Quantity * (f.UnitPrice + f.Tax)) AS TotalSales
FROM Sales.Fact_Sales f
JOIN Sales.Dim_Customer c
    ON f.CustomerKey = c.CustomerKey
GROUP BY c.CustomerName
ORDER BY TotalSales DESC;</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/83feb154-6d02-4a16-a390-f6576c25b04e/image.png" alt=""></p>
<hr>
<h3 id="상품별-총-판매액">상품별 총 판매액</h3>
<pre><code class="language-sql">SELECT
    i.ItemName,
    SUM(f.Quantity * (f.UnitPrice + f.Tax)) AS TotalSales
FROM Sales.Fact_Sales f
JOIN Sales.Dim_Item i
    ON f.ItemKey = i.ItemKey
GROUP BY i.ItemName
ORDER BY TotalSales DESC;</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/edaf37eb-8f48-4b8b-91df-b3648b099d7a/image.png" alt=""></p>
<hr>
<h3 id="카테고리별-상위-고객-분석">카테고리별 상위 고객 분석</h3>
<pre><code class="language-sql">WITH SalesCTE AS
(
    SELECT
        c.CustomerName,
        i.ItemName,
        SUM(f.Quantity * (f.UnitPrice + f.Tax)) AS TotalSales,
        CASE
            WHEN i.ItemName LIKE &#39;%Bike%&#39; THEN &#39;Bike&#39;
            ELSE &#39;Other&#39;
        END AS Category
    FROM Sales.Fact_Sales f
    JOIN Sales.Dim_Customer c
        ON f.CustomerKey = c.CustomerKey
    JOIN Sales.Dim_Item i
        ON f.ItemKey = i.ItemKey
    GROUP BY c.CustomerName, i.ItemName
)
SELECT *
FROM
(
    SELECT *,
           ROW_NUMBER() OVER (PARTITION BY Category ORDER BY TotalSales DESC) AS rn
    FROM SalesCTE
) t
WHERE rn &lt;= 5;</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/95b81ff4-3360-4557-a203-518e7cbf05c2/image.png" alt=""></p>
<hr>

<h1 id="자습서-direct-lake-의미-체계-모델-및-power-bi-보고서-만들기">자습서: Direct Lake 의미 체계 모델 및 Power BI 보고서 만들기</h1>
<h2 id="데이터-가져오기">데이터 가져오기</h2>
<p>Warehouse에서 데이터 가져오기 선택
샘플 데이터의 Retail~ 선택
<img src="https://velog.velcdn.com/images/rudin_/post/c99a56cc-b97f-42a3-bb5e-2a2562ac1cbc/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/afd4f202-29bf-43fc-80bc-ce681290f23d/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/4c45c54f-9132-49b0-bbf6-39fcf09ca23f/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/17b0a7f2-0913-419d-8d68-2bacd14e3574/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/bc38a62e-aa20-4c64-86d4-c566301ee9c8/image.png" alt=""></p>
<h2 id="의미체계-모델-만들기">의미체계 모델 만들기</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/db4323e2-98e7-42c0-b4a8-9419cb899080/image.png" alt="">
만들어진 의미체계 모델은 작업 영역에서 확인 가능
<img src="https://velog.velcdn.com/images/rudin_/post/547cc0ba-f8b5-4160-b0e6-2c2c77acd109/image.png" alt=""></p>
<h3 id="관계-관리">관계 관리</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/2ceb6a43-035b-491d-aadd-49ded7513565/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/9eaf0e5f-5141-4361-9519-8127637b111f/image.png" alt=""></p>
<h2 id="보고서-만들기">보고서 만들기</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/a1a742b7-2e87-43f1-9e6d-560cc4974d7a/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/df5d28bd-30a4-4542-acac-0550ec3e0b44/image.png" alt=""></p>
<hr>

<h1 id="fabric을-활용한-실시간-분석">Fabric을 활용한 실시간 분석</h1>
<h2 id="security-architecture--hierarchy">Security Architecture &amp; Hierarchy</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/db8a2a04-41ca-41a3-8829-ceda804b93bc/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/336481d3-fe26-437f-906d-ea2869684d14/image.png" alt=""></p>
<h2 id="workspace-roles--permissions">Workspace Roles &amp; Permissions</h2>
<p>단위입니다. 팀 리더, 데이터 엔지니어, 분석가, 보고서 사용자 등
역할에 따라 Admin, Member, Contributor, Viewer 중 하나를 부여하여 필요한 권한만 제공
더 세밀한 제어가 필요하면 &#39;Share&#39;로 특정 아이템만 공유</p>
<p><img src="https://velog.velcdn.com/images/rudin_/post/c24e0f51-9e8f-4928-9560-f0141ab38a97/image.png" alt=""></p>
<h2 id="onelake-security">OneLake Security</h2>
<p>데이터 자체(테이블, 폴더)에 대한 접근을 제어.</p>
<p><img src="https://velog.velcdn.com/images/rudin_/post/bc0744f8-cedf-427b-8ca1-bd585ce66933/image.png" alt=""></p>
<h2 id="row-level-securityrls">Row-Level Security(RLS)</h2>
<p>사용자의 권한(User Context)에 따라 테이블의 특정 행(Row)만 조회되도록 필터링하는 보안 기능
<img src="https://velog.velcdn.com/images/rudin_/post/f970aead-2c2c-4e34-8dfa-d49f38090fc1/image.png" alt=""></p>
<h2 id="implementing-rls-with-t-sql">Implementing RLS with T-SQL</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/9b2f1c68-59d0-4aae-99fc-7b9a64e175d1/image.png" alt=""></p>
<h3 id="1-보안-함수-생성">1. 보안 함수 생성</h3>
<p>USER_NAME() 함수를 사용하여 현재 접속한 사용자 식별</p>
<h3 id="2-보안-정책-적용">2. 보안 정책 적용</h3>
<p>위에서 만든 함수를 실제 테이블에 결합
STATE = ON 으로 설정하는 즉시 모든 쿼리에 필터가 적용</p>
<h2 id="sql-permissions-grant-deny--column-level-security">SQL Permissions: GRANT, Deny &amp; Column-Level Security</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/9e648418-6c0a-4f6b-9436-9a60fcdd5641/image.png" alt=""></p>
<h2 id="governance-purview-lineage--endorsement">Governance: Purview, Lineage &amp; Endorsement</h2>
<h3 id="purview-hub-데이터-보호">Purview Hub (데이터 보호)</h3>
<p>• MIP Label: 데이터에 &#39;기밀(Confidential)’ tag를 붙이면, 엑셀로 다운로드해도 암호화가 유지
• 인사이트: &quot;우리 회사에 민감 정보가 얼마나 있지?&quot;를 대시보드로 표시</p>
<h3 id="data-lineage-데이터-족보">Data Lineage (데이터 족보)</h3>
<p>• 자동 시각화: 원본 데이터가 어떤 파이프라인을 거쳐 어떤 리포트가 되었는지 자동 생성
• 영향도 분석: &quot;이 테이블 고치면 어떤 리포트가 깨질까?“ 미리 알 수 있음</p>
<h3 id="endorsement-신뢰-마크">Endorsement (신뢰 마크)</h3>
<p>• Certified (인증됨): IT 부서가 &quot;이건 믿고 써도 됨&quot;이라고 보증한 데이터.
• Promoted (홍보됨): 팀 리더가 &quot;우리 팀 데이터 공유할게&quot;라고 내놓은 데이터.</p>
<h2 id="real-time-intelligence-kql-database--eventstream">Real-Time Intelligence: KQL Database &amp; Eventstream</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/edd59c7b-8e42-452e-825f-264f5d1ad883/image.png" alt=""></p>
<h3 id="kql-database-저장소">KQL Database (저장소)</h3>
<p>• 고성능 로그 분석: 기존 SQL DB는 로그 쌓이면 느려지지만, KQL DB는 페타바이트급 로그도 순식간에 검색
• 비정형 데이터: JSON, 텍스트 로그 등 구조가 일정하지 않은 데이터도 그대로 넣고 바로 쿼리</p>
<h3 id="eventstream-연결-통로">Eventstream (연결 통로)</h3>
<p>• No-Code 연결: IoT 센서, 앱 로그, Kafka 등을 코딩 없이 클릭만으로 연결
• 실시간 처리: 데이터가 들어오는 즉시 필터링하거나 변환해서 KQL DB나 Lakehouse로 전송</p>
<h2 id="kql-kusto-query-language">KQL (Kusto Query Language)</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/39f22a68-9fe5-498a-9d6a-89f6b1de61d5/image.png" alt=""></p>
<p>데이터를 탐색하고 패턴을 발견하고, 변칙과 이상값을 식별하고, 통계 모델링을 만드는 등의 작업을 수행할 수 있는 강력한 도구로서 Microsoft에서 만들었으며 Azure Data Explorer, Azure Monitor, Microsoft Fabric 등에 사용
대소문자를 구분함</p>
<h2 id="fabric-activator-formely-reflex">Fabric Activator (formely Reflex)</h2>
<p>데이터 소스에서 특정 패턴이나 조건이 감지될 때 자동으로 작업을 실행하는
코드 없는 저지연 이벤트 감지 엔진</p>
<ul>
<li>1초 미만의 대기 시간으로 데이터 원본을 지속적으로 모니터링</li>
<li>임계값이 충족되거나 특정 패턴이 검색되면 작업 (예: 전자메일 또는 Teams 알림 보내기, Power Automate 흐름 시작, 타사 시스템 통합 등)을 시작</li>
<li>데이터가 지속적으로 흐르는 반응형 이벤트 기반 아키텍처에 적합하며, 이벤트 데이터의 상태 저장 평가에 따라 거의 실시간으로 결정</li>
</ul>
<hr>

<h1 id="실습-microsoft-fabric-데이터-웨어하우스-보안-설정">실습: Microsoft Fabric 데이터 웨어하우스 보안 설정</h1>
<h2 id="테이블의-column에-동적-데이터-마스킹-규칙-적용">테이블의 Column에 동적 데이터 마스킹 규칙 적용</h2>
<p>동적 데이터 마스킹 규칙은 테이블 수준의 개별 Column에 적용되므로 모든 Query가 마스킹의 영향을 받습니다. 기밀 데이터를 볼 명시적인 권한이 없는 사용자는 Query 결과에서 마스킹된 값을 보게 되며, 데이터를 볼 명시적인 권한이 있는 사용자는 마스킹되지 않은 데이터를 봅니다. 마스크에는 기본(default), 이메일(email), 무작위(random), 사용자 지정 문자열(custom string)의 네 가지 유형이 있습니다. </p>
<ol>
<li>Warehouse에서 T-SQL 타일을 선택하고, 다음 T-SQL 문을 사용하여 테이블을 생성하고 데이터를 삽입하고 조회합니다.<pre><code class="language-python">CREATE TABLE dbo.Customers
(   
 CustomerID INT NOT NULL,   
 FirstName varchar(50) MASKED WITH (FUNCTION = &#39;partial(1,&quot;XXXXXXX&quot;,0)&#39;) NULL,     
 LastName varchar(50) NOT NULL,     
 Phone varchar(20) MASKED WITH (FUNCTION = &#39;default()&#39;) NULL,     
 Email varchar(50) MASKED WITH (FUNCTION = &#39;email()&#39;) NULL   
);
</code></pre>
</li>
</ol>
<p>INSERT dbo.Customers (CustomerID, FirstName, LastName, Phone, Email) VALUES
(29485,&#39;Catherine&#39;,&#39;Abel&#39;,&#39;555-555-5555&#39;,&#39;catherine0@adventure-works.com&#39;),
(29486,&#39;Kim&#39;,&#39;Abercrombie&#39;,&#39;444-444-4444&#39;,&#39;kim2@adventure-works.com&#39;),
(29489,&#39;Frances&#39;,&#39;Adams&#39;,&#39;333-333-3333&#39;,&#39;frances0@adventure-works.com&#39;);</p>
<p>SELECT * FROM dbo.Customers;</p>
<pre><code>마스크 해제된 데이터를 볼 수 없는 사용자가 테이블을 Query할 때, FirstName Column은 문자열의 첫 글자와 XXXXXXX를 표시하고 마지막 문자는 표시하지 않습니다. Phone Column은 xxxx를 표시합니다. Email Column은 이메일 주소의 첫 글자 다음에 XXX@XXX.com을 표시합니다. 이 접근 방식은 민감한 데이터가 기밀을 유지하도록 보장하면서도 제한된 사용자가 테이블을 Query할 수 있도록 합니다.
![](https://velog.velcdn.com/images/rudin_/post/8f5fd4e8-123a-4eda-adc9-78cd9d61ca4d/image.png)

2. ▷ 실행 버튼을 사용하여 SQL 스크립트를 실행합니다. 이 스크립트는 Data Warehouse의 dbo Schema에 Customers라는 새 테이블을 생성합니다.
3. 그런 다음, 탐색기 창에서 Schemas &gt; dbo &gt; Tables를 확장하고 Customers 테이블이 생성되었는지 확인합니다. Workspace 생성자로서 마스크 해제된 데이터를 볼 수 있는 Workspace Admin 역할의 멤버이므로, SELECT 문은 마스크 해제된 데이터를 반환합니다.

## 행 수준 보안(Row-level security) 적용
```python
CREATE TABLE dbo.Sales  
(  
    OrderID INT,  
    SalesRep VARCHAR(60),  
    Product VARCHAR(10),  
    Quantity INT  
);

--Populate the table with 6 rows of data, showing 3 orders for each test user. 
INSERT dbo.Sales (OrderID, SalesRep, Product, Quantity) VALUES
(1, &#39;&lt;username1&gt;@&lt;your_domain&gt;.com&#39;, &#39;Valve&#39;, 5),   
(2, &#39;&lt;username1&gt;@&lt;your_domain&gt;.com&#39;, &#39;Wheel&#39;, 2),   
(3, &#39;&lt;username1&gt;@&lt;your_domain&gt;.com&#39;, &#39;Valve&#39;, 4),  
(4, &#39;&lt;username2&gt;@&lt;your_domain&gt;.com&#39;, &#39;Bracket&#39;, 2),   
(5, &#39;&lt;username2&gt;@&lt;your_domain&gt;.com&#39;, &#39;Wheel&#39;, 5),   
(6, &#39;&lt;username2&gt;@&lt;your_domain&gt;.com&#39;, &#39;Seat&#39;, 5);  

SELECT * FROM dbo.Sales;  </code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/0c34671a-6014-4aa7-858f-2325d3ea5a81/image.png" alt=""></p>
<p>새 Schema, Function으로 정의된 보안 Predicate, 그리고 보안 정책을 생성</p>
<pre><code class="language-python">--Create a separate schema to hold the row-level security objects (the predicate function and the security policy)
CREATE SCHEMA rls;
GO

/*Create the security predicate defined as an inline table-valued function.
A predicate evaluates to true (1) or false (0). This security predicate returns 1,
meaning a row is accessible, when a row in the SalesRep column is the same as the user
executing the query.*/   
--Create a function to evaluate who is querying the table
CREATE FUNCTION rls.fn_securitypredicate(@SalesRep AS VARCHAR(60)) 
    RETURNS TABLE  
WITH SCHEMABINDING  
AS  
    RETURN SELECT 1 AS fn_securitypredicate_result   
WHERE @SalesRep = USER_NAME();
GO   
/*Create a security policy to invoke and enforce the function each time a query is run on the Sales table.
The security policy has a filter predicate that silently filters the rows available to 
read operations (SELECT, UPDATE, and DELETE). */
CREATE SECURITY POLICY SalesFilter  
ADD FILTER PREDICATE rls.fn_securitypredicate(SalesRep)   
ON dbo.Sales  
WITH (STATE = ON);
GO</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/8cbe9155-ca96-4c66-becc-6c9d2760eaf3/image.png" alt=""></p>
<h2 id="열-수준-보안column-level-security-구현">열 수준 보안(Column-level security) 구현</h2>
<p>열 수준 보안은 어떤 사용자가 테이블의 특정 Column에 접근할 수 있는지 지정할 수 있도록 합니다. 이는 Column 목록과 Column을 읽을 수 있거나 없는 사용자 또는 역할을 지정하여 테이블에 GRANT 또는 DENY 문을 발행함으로써 구현됩니다. 접근 관리를 간소화하기 위해 개별 사용자 대신 역할에 권한을 할당합니다. 이 실습에서는 테이블을 생성하고, 테이블의 Column 하위 집합에 접근 권한을 부여하며, 제한된 Column이 본인 외의 사용자에게는 보이지 않는지 테스트합니다.</p>
<pre><code class="language-python">CREATE TABLE dbo.Orders
(   
    OrderID INT,   
    CustomerID INT,  
    CreditCard VARCHAR(20)      
);   
INSERT dbo.Orders (OrderID, CustomerID, CreditCard) VALUES
(1234, 5678, &#39;111111111111111&#39;),
(2341, 6785, &#39;222222222222222&#39;),
(3412, 7856, &#39;333333333333333&#39;);   
SELECT * FROM dbo.Orders;</code></pre>
<pre><code class="language-python">DENY SELECT ON dbo.Orders (CreditCard) TO [&lt;username1&gt;@&lt;your_domain&gt;.com];</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/1f650da1-8964-47e1-9959-22725df7f124/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/11f31fe4-8809-4993-b801-8df3e7f00ff8/image.png" alt=""></p>
<h2 id="t-sql을-사용하여-sql-세분화된-권한-구성">T-SQL을 사용하여 SQL 세분화된 권한 구성</h2>
<p>Fabric은 Workspace 수준 및 항목 수준에서 데이터 접근을 제어할 수 있는 권한 모델을 가지고 있습니다. Fabric Warehouse의 보안 개체(securables)를 사용자들이 무엇을 할 수 있는지 더 세밀하게 제어해야 할 때, 표준 SQL 데이터 제어 언어(DCL) 명령어인 GRANT, DENY, REVOKE를 사용할 수 있습니다. 이 실습에서는 객체(objects)를 생성하고, GRANT 및 DENY를 사용하여 객체를 보호한 다음, Query를 실행하여 세분화된 권한 적용의 효과를 확인합니다.</p>
<pre><code class="language-python">CREATE PROCEDURE dbo.sp_PrintMessage
AS
PRINT &#39;Hello World.&#39;;
GO   
CREATE TABLE dbo.Parts
(
    PartID INT,
    PartName VARCHAR(25)
);

INSERT dbo.Parts (PartID, PartName) VALUES
(1234, &#39;Wheel&#39;),
(5678, &#39;Seat&#39;);
 GO

/*Execute the stored procedure and select from the table and note the results you get
as a member of the Workspace Admin role. Look for output from the stored procedure on 
the &#39;Messages&#39; tab.*/
EXEC dbo.sp_PrintMessage;
GO   
SELECT * FROM dbo.Parts</code></pre>
<p>다음으로, Workspace Viewer 역할의 멤버인 사용자에게 테이블에 대한 DENY SELECT 권한을 부여하고, 동일한 사용자에게 프로시저에 대한 GRANT EXECUTE 권한을 부여합니다. <code>&lt;username1&gt;@&lt;your_domain&gt;.com</code>을 Workspace에 Viewer 권한을 가진 사용자의 사용자 이름으로 대체합니다.</p>
<pre><code class="language-python">DENY SELECT on dbo.Parts to [&lt;username1&gt;@&lt;your_domain&gt;.com];

GRANT EXECUTE on dbo.sp_PrintMessage to [&lt;username1&gt;@&lt;your_domain&gt;.com];</code></pre>
<hr>

<h1 id="microsoft-fabric에서-eventstream을-사용하여-실시간-데이터-수집">Microsoft Fabric에서 Eventstream을 사용하여 실시간 데이터 수집</h1>
<p>Eventstream은 Microsoft Fabric의 기능으로, 실시간 이벤트를 캡처, 변환 및 다양한 대상으로 라우팅합니다. Eventstream에 이벤트 데이터 원본, 대상 및 변환을 추가할 수 있습니다.</p>
<h2 id="eventhouse-만들기">Eventhouse 만들기</h2>
<p>작업 영역에서 + 새 항목을 선택
<img src="https://velog.velcdn.com/images/rudin_/post/f7d5416f-03df-4cbd-bdb5-b6a37bce0450/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/8b33f9b1-9327-4180-85d6-4d218bee1186/image.png" alt=""></p>
<h2 id="eventstream-만들기">Eventstream 만들기</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/440ddbce-a20a-4aca-85d6-10ddab18f098/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/3f3d6188-150e-487f-a4ce-a7e9c56200b2/image.png" alt="">
샘플 데이터 사용
<img src="https://velog.velcdn.com/images/rudin_/post/5f14496d-07bd-4253-b911-c79d81b029e2/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/bc326108-10fb-4824-a3e0-1f98ea0b096e/image.png" alt=""></p>
<h2 id="원본-추가">원본 추가</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/6debace3-8c1d-405a-88c2-3e4445731e18/image.png" alt=""></p>
<h2 id="대상-추가">대상 추가</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/e3f4d900-0595-419c-a1e8-ccd554f38790/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/f89172f7-94c6-4f8f-9167-86b108f49244/image.png" alt="">
<em>watermark delay</em>: 데이터가 늦게 도착해도 윈도우(결과 집계)를 닫지 않고 얼마나 더 기다려줄 것인가</p>
<h2 id="캡처된-데이터-쿼리">캡처된 데이터 쿼리</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/05e14776-fdce-45fe-9489-20b911db048a/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/1386f129-e79f-4d64-b91a-7bd8c9462fa9/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/e82f63a5-762c-4206-853a-49f9e893b70c/image.png" alt=""></p>
<h2 id="이벤트-데이터-변환">이벤트 데이터 변환</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/74809ba5-e704-486c-bdf2-9a62f6fa5271/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/37a6072e-f7b2-4eb4-8891-3be7ecccb88c/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/0d9dc182-49e6-45c8-85b7-0e5bf13a7ba8/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/ff3ab149-aa82-4420-b57a-3be1ab1331ed/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/cfd16e66-2240-475f-8618-5399afd4538b/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/035f0287-bcc1-44fd-be9b-9ca5621c16a7/image.png" alt=""></p>
<h2 id="변환된-데이터-쿼리">변환된 데이터 쿼리</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/ac238a0f-2f18-4567-9680-62baeb016af8/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/a18a5691-fcdc-4263-b5b1-62c84ada1c5f/image.png" alt=""></p>
<hr>

<h1 id="microsoft-fabric-eventhouse에서-데이터-작업">Microsoft Fabric Eventhouse에서 데이터 작업</h1>
<h2 id="kql을-사용하여-데이터-쿼리">KQL을 사용하여 데이터 쿼리</h2>
<pre><code class="language-python">// Use &#39;project&#39; and &#39;take&#39; to view a sample number of records in the table and check the data.
Bikestream
| project Street, No_Bikes
| take 10</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/822b95f4-1f0f-4443-975a-4e86a6a3c7dc/image.png" alt=""></p>
<pre><code class="language-python">Bikestream
| project Street, [&quot;Number of Empty Docks&quot;] = No_Empty_Docks
| take 10</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/05d83584-a628-4ebd-b4c6-108f4f9aecda/image.png" alt=""></p>
<pre><code class="language-python">
Bikestream
| summarize [&quot;Total Number of Bikes&quot;] = sum(No_Bikes)</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/f4c6ac7b-33b9-4b61-8b25-a34c882494f5/image.png" alt=""></p>
<pre><code class="language-python">Bikestream
| summarize [&quot;Total Number of Bikes&quot;] = sum(No_Bikes) by Neighbourhood
| project Neighbourhood, [&quot;Total Number of Bikes&quot;]</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/023968bf-3641-4cea-ac1e-446af65bf39a/image.png" alt=""></p>
<pre><code class="language-python">Bikestream
| summarize [&quot;Total Number of Bikes&quot;] = sum(No_Bikes) by Neighbourhood
| project Neighbourhood = case(isempty(Neighbourhood) or isnull(Neighbourhood), &quot;Unidentified&quot;, Neighbourhood), [&quot;Total Number of Bikes&quot;]</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/89a7a4fe-c75f-473e-a3b2-7ad94634e142/image.png" alt=""></p>
<pre><code class="language-python">Bikestream
| summarize [&quot;Total Number of Bikes&quot;] = sum(No_Bikes) by Neighbourhood
| project Neighbourhood = case(isempty(Neighbourhood) or isnull(Neighbourhood), &quot;Unidentified&quot;, Neighbourhood), [&quot;Total Number of Bikes&quot;]
| sort by Neighbourhood asc</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/54a2f601-5482-4805-8a5b-7145e451fb9a/image.png" alt=""></p>
<pre><code class="language-python">Bikestream
| summarize [&quot;Total Number of Bikes&quot;] = sum(No_Bikes) by Neighbourhood
| project Neighbourhood = case(isempty(Neighbourhood) or isnull(Neighbourhood), &quot;Unidentified&quot;, Neighbourhood), [&quot;Total Number of Bikes&quot;]
| order by Neighbourhood asc</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/4e02af2a-51a6-4055-9215-36c9f7eeb4b5/image.png" alt=""></p>
<pre><code class="language-python">Bikestream
| where Neighbourhood == &quot;Chelsea&quot;
| summarize [&quot;Total Number of Bikes&quot;] = sum(No_Bikes) by Neighbourhood
| project Neighbourhood = case(isempty(Neighbourhood) or isnull(Neighbourhood), &quot;Unidentified&quot;, Neighbourhood), [&quot;Total Number of Bikes&quot;]
| sort by Neighbourhood asc</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/867cf1d8-0261-417e-bf89-41a0e090877c/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MicrosoftDataSchool] 73일차 - Fabric에서 Apache Spark 사용하기, 델타 테이블 사용, 레이크하우스에서 메달리온 아키텍처 생성, Data Wrangler]]></title>
            <link>https://velog.io/@rudin_/MicrosoftDataSchool-73%EC%9D%BC%EC%B0%A8-Fabric%EC%97%90%EC%84%9C-Apache-Spark-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-%EB%8D%B8%ED%83%80-%ED%85%8C%EC%9D%B4%EB%B8%94-%EC%82%AC%EC%9A%A9-%EB%A0%88%EC%9D%B4%ED%81%AC%ED%95%98%EC%9A%B0%EC%8A%A4%EC%97%90%EC%84%9C-%EB%A9%94%EB%8B%AC%EB%A6%AC%EC%98%A8-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%83%9D%EC%84%B1-Data-Wrangler</link>
            <guid>https://velog.io/@rudin_/MicrosoftDataSchool-73%EC%9D%BC%EC%B0%A8-Fabric%EC%97%90%EC%84%9C-Apache-Spark-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-%EB%8D%B8%ED%83%80-%ED%85%8C%EC%9D%B4%EB%B8%94-%EC%82%AC%EC%9A%A9-%EB%A0%88%EC%9D%B4%ED%81%AC%ED%95%98%EC%9A%B0%EC%8A%A4%EC%97%90%EC%84%9C-%EB%A9%94%EB%8B%AC%EB%A6%AC%EC%98%A8-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98-%EC%83%9D%EC%84%B1-Data-Wrangler</guid>
            <pubDate>Tue, 21 Apr 2026 08:41:01 GMT</pubDate>
            <description><![CDATA[<h1 id="microsoft-fabric-apache-spark-데이터-분석">Microsoft Fabric: Apache Spark 데이터 분석</h1>
<p>이 가이드는 Fabric Lakehouse로 데이터를 수집하고, <strong>PySpark</strong>와 <strong>Spark SQL</strong>을 사용하여 데이터를 읽고, 분석하고, 시각화하는 전체 과정을 다룹니다.</p>
<h2 id="1-환경-설정-및-데이터-준비">1. 환경 설정 및 데이터 준비</h2>
<h3 id="작업-영역-및-lakehouse-생성">작업 영역 및 Lakehouse 생성</h3>
<ol>
<li><strong>작업 영역 만들기</strong>: <a href="https://app.fabric.microsoft.com/home?experience=fabric-developer">Fabric 홈 페이지</a>에서 <strong>Workspaces</strong>를 선택하고, <strong>Advanced</strong> 섹션에서 Fabric Capacity 라이선스 모드를 선택하여 새 작업 영역을 만듭니다.</li>
<li><strong>Lakehouse 생성</strong>: <strong>Create</strong> 메뉴에서 <strong>Lakehouse</strong>를 선택하고 이름을 지정하여 생성합니다.</li>
<li><strong>데이터 업로드</strong>:<ul>
<li><a href="https://github.com/MicrosoftLearning/dp-data/raw/main/orders.zip">orders.zip</a> 파일을 다운로드하고 압축을 풉니다.</li>
<li>Lakehouse Explorer의 <strong>Files</strong> 폴더에서 <strong>Upload &gt; Upload folder</strong>를 선택하여 <code>orders</code> 폴더(2019.csv, 2020.csv, 2021.csv 포함)를 업로드합니다.</li>
</ul>
</li>
</ol>
<h3 id="notebook-생성">Notebook 생성</h3>
<ul>
<li><strong>Create</strong> 페이지에서 <strong>Notebook</strong>을 생성합니다.</li>
</ul>
<h2 id="2-데이터-로드-및-스키마-정의">2. 데이터 로드 및 스키마 정의</h2>
<h3 id="기본-데이터-로드-csv">기본 데이터 로드 (CSV)</h3>
<p><code>2019.csv</code> 파일을 로드하는 가장 기본적인 코드입니다.</p>
<pre><code class="language-python"># 2019.csv 파일 로드 (헤더 포함)
df = spark.read.format(&quot;csv&quot;).option(&quot;header&quot;,&quot;true&quot;).load(&quot;Files/orders/2019.csv&quot;)
display(df)</code></pre>
<h3 id="스키마-정의-및-모든-파일-로드">스키마 정의 및 모든 파일 로드</h3>
<p>데이터 형식을 명시적으로 지정하고, 와일드카드(<code>*</code>)를 사용하여 <code>orders</code> 폴더 내의 모든 CSV 파일을 로드합니다.</p>
<pre><code class="language-python">from pyspark.sql.types import *

# 스키마 정의
orderSchema = StructType([
    StructField(&quot;SalesOrderNumber&quot;, StringType()),
    StructField(&quot;SalesOrderLineNumber&quot;, IntegerType()),
    StructField(&quot;OrderDate&quot;, DateType()),
    StructField(&quot;CustomerName&quot;, StringType()),
    StructField(&quot;Email&quot;, StringType()),
    StructField(&quot;Item&quot;, StringType()),
    StructField(&quot;Quantity&quot;, IntegerType()),
    StructField(&quot;UnitPrice&quot;, FloatType()),
    StructField(&quot;Tax&quot;, FloatType())
])

# 모든 연도의 CSV 파일 로드
df = spark.read.format(&quot;csv&quot;).schema(orderSchema).load(&quot;Files/orders/*.csv&quot;)
display(df)</code></pre>
<h2 id="3-데이터-탐색-및-집계">3. 데이터 탐색 및 집계</h2>
<h3 id="데이터-필터링">데이터 필터링</h3>
<p>특정 열을 선택하거나 조건을 적용하여 데이터를 필터링합니다.</p>
<pre><code class="language-python"># 특정 제품을 구매한 고유 고객 리스트 추출
customers = df.select(&quot;CustomerName&quot;, &quot;Email&quot;).where(df[&#39;Item&#39;]==&#39;Road-250 Red, 52&#39;)
print(f&quot;Total records: {customers.count()}&quot;)
print(f&quot;Distinct customers: {customers.distinct().count()}&quot;)
display(customers.distinct())</code></pre>
<h3 id="데이터-집계-및-그룹화">데이터 집계 및 그룹화</h3>
<p>제품별 수량 합계 및 연도별 주문 수를 계산합니다.</p>
<pre><code class="language-python"># 제품별 주문 수량 합계
productSales = df.select(&quot;Item&quot;, &quot;Quantity&quot;).groupBy(&quot;Item&quot;).sum()
display(productSales)

# 연도별 판매 주문 수 계산
from pyspark.sql.functions import *
yearlySales = df.select(year(col(&quot;OrderDate&quot;)).alias(&quot;Year&quot;)).groupBy(&quot;Year&quot;).count().orderBy(&quot;Year&quot;)
display(yearlySales)</code></pre>
<h2 id="4-데이터-변환-및-저장">4. 데이터 변환 및 저장</h2>
<h3 id="데이터프레임-변환-열-추가-및-재정렬">데이터프레임 변환 (열 추가 및 재정렬)</h3>
<p>연/월 추출, 이름 분리 등 복합적인 변환을 수행합니다.</p>
<pre><code class="language-python">from pyspark.sql.functions import *

# 연, 월 열 추가 및 FirstName, LastName 분리
transformed_df = df.withColumn(&quot;Year&quot;, year(col(&quot;OrderDate&quot;))).withColumn(&quot;Month&quot;, month(col(&quot;OrderDate&quot;)))
transformed_df = transformed_df.withColumn(&quot;FirstName&quot;, split(col(&quot;CustomerName&quot;), &quot; &quot;).getItem(0)).withColumn(&quot;LastName&quot;, split(col(&quot;CustomerName&quot;), &quot; &quot;).getItem(1))

# 열 필터링 및 순서 재정렬
transformed_df = transformed_df[&quot;SalesOrderNumber&quot;, &quot;SalesOrderLineNumber&quot;, &quot;OrderDate&quot;, &quot;Year&quot;, &quot;Month&quot;, &quot;FirstName&quot;, &quot;LastName&quot;, &quot;Email&quot;, &quot;Item&quot;, &quot;Quantity&quot;, &quot;UnitPrice&quot;, &quot;Tax&quot;]

# 결과 확인
display(transformed_df.limit(5))</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/de03c04f-8ae5-42b8-a843-b0f3f9331a1d/image.png" alt=""></p>
<h3 id="변환된-데이터-저장-parquet">변환된 데이터 저장 (Parquet)</h3>
<p>데이터를 Parquet 형식으로 저장하고 다시 로드합니다.</p>
<pre><code class="language-python"># Parquet 형식으로 저장
transformed_df.write.mode(&quot;overwrite&quot;).parquet(&#39;Files/transformed_data/orders&#39;)
print(&quot;Transformed data saved!&quot;)

# 저장된 Parquet 파일 로드
orders_df = spark.read.format(&quot;parquet&quot;).load(&quot;Files/transformed_data/orders&quot;)
display(orders_df)</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/8524aad7-5a6d-4f8c-8d3b-5c8936e9f8f2/image.png" alt=""></p>
<h3 id="데이터-분할partitioning-저장">데이터 분할(Partitioning) 저장</h3>
<p>성능 향상을 위해 연도 및 월별로 데이터를 분할하여 저장합니다.</p>
<pre><code class="language-python"># Year 및 Month별로 분할 저장
orders_df.write.partitionBy(&quot;Year&quot;,&quot;Month&quot;).mode(&quot;overwrite&quot;).parquet(&quot;Files/partitioned_data&quot;)
print(&quot;Transformed data saved!&quot;)

# 특정 파티션(2021년 전체) 데이터만 로드
orders_2021_df = spark.read.format(&quot;parquet&quot;).load(&quot;Files/partitioned_data/Year=2021/Month=*&quot;)
display(orders_2021_df)</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/3ce51505-1eb8-45ae-8453-b997cb0aa8b0/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/0aef51be-5538-44d0-a9d3-7d10856c5e86/image.png" alt=""></p>
<h2 id="5-테이블-및-sql-작업">5. 테이블 및 SQL 작업</h2>
<h3 id="delta-테이블-생성-및-쿼리">Delta 테이블 생성 및 쿼리</h3>
<p>관계를 정의하고 SQL로 쿼리할 수 있도록 Delta 테이블로 저장합니다.</p>
<pre><code class="language-python"># Delta 테이블로 저장
df.write.format(&quot;delta&quot;).saveAsTable(&quot;salesorders&quot;)

# 테이블 정보 확인
spark.sql(&quot;DESCRIBE EXTENDED salesorders&quot;).show(truncate=False)</code></pre>
<p>salesorders 테이블의 ..메뉴에서 Load data&gt; Spakr를 선택하여 새 코드 셀 추가
<img src="https://velog.velcdn.com/images/rudin_/post/fb901710-170a-4ba6-819b-464cabcc7d89/image.png" alt=""></p>
<pre><code class="language-python"># PySpark 내에서 SQL 쿼리 실행
df = spark.sql(&quot;SELECT * FROM day1_lakehouse2.salesorders LIMIT 1000&quot;)
display(df)</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/6dd3ec7d-381a-4775-a4a3-dc0eea8fa312/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/75ba55ae-cac5-4c40-aa26-2330a3a756ac/image.png" alt=""></p>
<h3 id="sql-매직-커맨드-사용">SQL 매직 커맨드 사용</h3>
<p>셀 상단에 <code>%%sql</code>을 사용하여 직접 SQL 문을 실행합니다.</p>
<pre><code class="language-sql">%%sql
SELECT YEAR(OrderDate) AS OrderYear, 
       SUM((UnitPrice * Quantity) + Tax) AS GrossRevenue
FROM salesorders
GROUP BY YEAR(OrderDate)
ORDER BY OrderYear;</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/0c7bd3dd-4414-45a4-baeb-42ea6218c8fd/image.png" alt=""></p>
<h2 id="6-데이터-시각화">6. 데이터 시각화</h2>
<h3 id="spark로-데이터-시각화">Spark로 데이터 시각화</h3>
<pre><code class="language-python">%%sql
SELECT * FROM salesorders</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/05353232-f4a5-49a4-b8dc-b68c7da4451b/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/4e781ccb-c944-4d0f-8f1f-6097b8507899/image.png" alt=""></p>
<h3 id="matplotlib를-사용한-시각화">Matplotlib를 사용한 시각화</h3>
<p>Spark 데이터프레임을 Pandas로 변환하여 막대형 차트를 생성합니다.</p>
<pre><code class="language-python">import pandas as pd
from matplotlib import pyplot as plt

# SQL 쿼리 결과를 Spark DF로 가져오기
sqlQuery = &quot;SELECT CAST(YEAR(OrderDate) AS CHAR(4)) AS OrderYear, \
            SUM((UnitPrice * Quantity) + Tax) AS GrossRevenue, \
            COUNT(DISTINCT SalesOrderNumber) AS YearlyCounts \
            FROM salesorders \
            GROUP BY CAST(YEAR(OrderDate) AS CHAR(4)) \
            ORDER BY OrderYear&quot;
df_spark = spark.sql(sqlQuery)

# Matplotlib를 위해 Pandas DF로 변환
df_sales = df_spark.toPandas()

# 차트 사용자 지정 및 출력
plt.clf()
fig = plt.figure(figsize=(8,3))
plt.bar(x=df_sales[&#39;OrderYear&#39;], height=df_sales[&#39;GrossRevenue&#39;], color=&#39;orange&#39;)
plt.title(&#39;Revenue by Year&#39;)
plt.xlabel(&#39;Year&#39;)
plt.ylabel(&#39;Revenue&#39;)
plt.grid(color=&#39;#95a5a6&#39;, linestyle=&#39;--&#39;, linewidth=2, axis=&#39;y&#39;, alpha=0.7)
plt.xticks(rotation=45)
plt.show()

# 서브플롯 생성 (막대 차트 + 파이 차트)
plt.clf()
fig, ax = plt.subplots(1, 2, figsize = (10,4))
ax.bar(x=df_sales[&#39;OrderYear&#39;], height=df_sales[&#39;GrossRevenue&#39;], color=&#39;orange&#39;)
ax.set_title(&#39;Revenue by Year&#39;)
ax.pie(df_sales[&#39;YearlyCounts&#39;])
ax.set_title(&#39;Orders per Year&#39;)
ax.legend(df_sales[&#39;OrderYear&#39;])
fig.suptitle(&#39;Sales Data&#39;)
plt.show()</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/264904b7-3539-4b39-a697-7da4c6a50f79/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/42418d39-0c1f-40c4-b883-b4fd4e491300/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/9a302f34-7c52-4945-9684-f2b485f19f5f/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/c1484db0-5a2b-4a34-954d-639562ddf694/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/3b99098f-698b-417f-acf2-f5fb9a4bf724/image.png" alt=""></p>
<h3 id="seaborn을-사용한-시각화">Seaborn을 사용한 시각화</h3>
<p>더 간결한 코드로 세련된 테마의 차트를 생성합니다.</p>
<pre><code class="language-python">import seaborn as sns

# 막대 차트 (Whitegrid 테마)
plt.clf()
sns.set_theme(style=&quot;whitegrid&quot;)
ax = sns.barplot(x=&quot;OrderYear&quot;, y=&quot;GrossRevenue&quot;, data=df_sales)
plt.show()

# 선형 차트
plt.clf()
ax = sns.lineplot(x=&quot;OrderYear&quot;, y=&quot;GrossRevenue&quot;, data=df_sales)
plt.show()</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/79ad7064-cb27-40b2-a102-0306eec3bef8/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/f0953121-1377-4c24-b365-678008e67611/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/4eb4e9a4-7610-44e6-ae5f-7cec81e00387/image.png" alt=""></p>
<h2 id="7-리소스-정리">7. 리소스 정리</h2>
<p>실습이 종료되면 세션을 중단하고 작업 영역을 삭제합니다.</p>
<ol>
<li>Notebook 상단에서 <strong>Stop session</strong>을 클릭합니다.</li>
<li><strong>Workspace settings</strong>에서 <strong>Remove this workspace</strong>를 선택하여 삭제합니다.</li>
</ol>
<hr>

<h2 id="스트리밍-데이터에-delta-table-사용">스트리밍 데이터에 Delta Table 사용</h2>
<p>Delta Lake는 스트리밍 데이터를 지원합니다. Delta Table은 Spark Structured Streaming API를 사용하여 생성된 데이터 스트림의 Sink 또는 Source가 될 수 있습니다. 이 예시에서는 시뮬레이션된 IoT(Internet of Things) 시나리오에서 일부 스트리밍 데이터의 Sink로 Delta Table을 사용합니다.</p>
<pre><code class="language-python"> from notebookutils import mssparkutils
 from pyspark.sql.types import *
 from pyspark.sql.functions import *

 # Create a folder
 inputPath = &#39;Files/data/&#39;
 mssparkutils.fs.mkdirs(inputPath)

 # Create a stream that reads data from the folder, using a JSON schema
 jsonSchema = StructType([
 StructField(&quot;device&quot;, StringType(), False),
 StructField(&quot;status&quot;, StringType(), False)
 ])
 iotstream = spark.readStream.schema(jsonSchema).option(&quot;maxFilesPerTrigger&quot;, 1).json(inputPath)

 # Write some event data to the folder
 device_data = &#39;&#39;&#39;{&quot;device&quot;:&quot;Dev1&quot;,&quot;status&quot;:&quot;ok&quot;}
 {&quot;device&quot;:&quot;Dev1&quot;,&quot;status&quot;:&quot;ok&quot;}
 {&quot;device&quot;:&quot;Dev1&quot;,&quot;status&quot;:&quot;ok&quot;}
 {&quot;device&quot;:&quot;Dev2&quot;,&quot;status&quot;:&quot;error&quot;}
 {&quot;device&quot;:&quot;Dev1&quot;,&quot;status&quot;:&quot;ok&quot;}
 {&quot;device&quot;:&quot;Dev1&quot;,&quot;status&quot;:&quot;error&quot;}
 {&quot;device&quot;:&quot;Dev2&quot;,&quot;status&quot;:&quot;ok&quot;}
 {&quot;device&quot;:&quot;Dev2&quot;,&quot;status&quot;:&quot;error&quot;}
 {&quot;device&quot;:&quot;Dev1&quot;,&quot;status&quot;:&quot;ok&quot;}&#39;&#39;&#39;

 mssparkutils.fs.put(inputPath + &quot;data.txt&quot;, device_data, True)

 print(&quot;Source stream created...&quot;)</code></pre>
<p>방금 실행한 코드는 가상의 IoT 디바이스에서 읽은 데이터를 나타내는, 일부 데이터가 저장된 폴더를 기반으로 스트리밍 데이터 Source를 생성했습니다.</p>
<pre><code class="language-python"># Write the stream to a delta table
delta_stream_table_path = &#39;Tables/iotdevicedata&#39;
checkpointpath = &#39;Files/delta/checkpoint&#39;
deltastream = iotstream.writeStream.format(&quot;delta&quot;).option(&quot;checkpointLocation&quot;, checkpointpath).start(delta_stream_table_path)
print(&quot;Streaming to delta sink...&quot;)</code></pre>
<p>이 코드는 스트리밍 디바이스 데이터를 Delta 형식으로 iotdevicedata라는 폴더에 씁니다. Tables 폴더에 있는 폴더 위치의 경로 때문에 해당 폴더에 Table이 자동으로 생성됩니다.</p>
<pre><code class="language-python">%%sql
SELECT * FROM IotDeviceData;</code></pre>
<pre><code class="language-python"># Add more data to the source stream
more_data = &#39;&#39;&#39;{&quot;device&quot;:&quot;Dev1&quot;,&quot;status&quot;:&quot;ok&quot;}
{&quot;device&quot;:&quot;Dev1&quot;,&quot;status&quot;:&quot;ok&quot;}
{&quot;device&quot;:&quot;Dev1&quot;,&quot;status&quot;:&quot;ok&quot;}
{&quot;device&quot;:&quot;Dev1&quot;,&quot;status&quot;:&quot;ok&quot;}
{&quot;device&quot;:&quot;Dev1&quot;,&quot;status&quot;:&quot;error&quot;}
{&quot;device&quot;:&quot;Dev2&quot;,&quot;status&quot;:&quot;error&quot;}
{&quot;device&quot;:&quot;Dev1&quot;,&quot;status&quot;:&quot;ok&quot;}&#39;&#39;&#39;

mssparkutils.fs.put(inputPath + &quot;more-data.txt&quot;, more_data, True)</code></pre>
<pre><code class="language-python">%%sql
SELECT * FROM IotDeviceData;</code></pre>
<pre><code class="language-python">deltastream.stop()</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/4a2fdbfe-6849-4149-a28e-841f9d362c3a/image.png" alt="">
코드에서 <code>Files/data</code> 안에 있는 데이터들로 하도록 설정해서, 기존의 sales 데이터까지 읽어들여 NULL값이 입력되는 문제가 있었다. sales 데이터를 삭제하고 진행하면 정상적으로 진행된다.</p>
<p>또한 동일한 데이터(동일한 셀)을 넣는걸 반복하더라도 중복으로 적재되지 않는다.</p>
<hr>


<h1 id="pyspark-기반-메달리온-아키텍처-구축과-data-wrangler를-활용한-효율적-데이터-전처리">PySpark 기반 메달리온 아키텍처 구축과 Data Wrangler를 활용한 효율적 데이터 전처리</h1>
<h2 id="starter-pool">Starter Pool</h2>
<p>Fabric에서는 Live Pool(Warm) 방식으로 클러스터를 운영 → 대기 시간 없이 거의 바로 세션을 시작 가능</p>
<ul>
<li>항상 켜져 있음</li>
<li>5~10초안에 세션 시작</li>
<li>대기 비용 X</li>
</ul>
<h2 id="onelake--notebook">OneLake &amp; Notebook</h2>
<ul>
<li>Fabric Notebook에서는 Lakehouse Explorer(OneLake에 저장된 테이블 및 파일 탐색)가 내장</li>
<li>데이터 탐색을 위한 별도 탭 이동 없이 Notebook 내에서 데이터 탐색, 코드 작성, 시각화를 진행</li>
</ul>
<table>
<thead>
<tr>
<th>구분</th>
<th>Azure Databricks</th>
<th>Microsoft Fabric</th>
</tr>
</thead>
<tbody><tr>
<td>외부 데이터 접근 방식</td>
<td>mount 필요 (Storage 연결 설정)</td>
<td>바로 접근 가능 (Lakehouse 기반)</td>
</tr>
<tr>
<td>설정 과정</td>
<td>App Registration → RBAC 권한 → Key Vault → Secret Scope → mount 코드 작성</td>
<td>별도 설정 없음</td>
</tr>
<tr>
<td>보안 처리</td>
<td>Key Vault + Secret 관리 필요</td>
<td>플랫폼에서 자동 관리</td>
</tr>
<tr>
<td>사용 편의성</td>
<td>초기 설정 복잡, 매번 mount 필요</td>
<td>매우 간단 (즉시 사용)</td>
</tr>
<tr>
<td>코드 예시</td>
<td>dbutils.fs.mount(...) 설정 필요</td>
<td>spark.read.csv(&quot;Files/...&quot;) 바로 사용</td>
</tr>
<tr>
<td>플랫폼 특성</td>
<td>IaaS/PaaS 기반 구성형 환경</td>
<td>완전 관리형 SaaS 통합 플랫폼</td>
</tr>
<tr>
<td>Lakehouse 연동</td>
<td>직접 연결 및 설정 필요</td>
<td>기본 내장 및 긴밀한 통합</td>
</tr>
</tbody></table>
<h2 id="spark-on-databricks-vs-spark-on-fabric">Spark on Databricks vs Spark on Fabric</h2>
<table>
<thead>
<tr>
<th>비교 항목</th>
<th>Spark on Databricks</th>
<th>Spark on Fabric</th>
</tr>
</thead>
<tbody><tr>
<td>클러스터 시작</td>
<td>Cold Start (3~5분 소요)<br>직접 생성 및 관리 필요</td>
<td>Live Start (5~10초 소요)<br>Starter Pool 자동 할당</td>
</tr>
<tr>
<td>저장소 연결</td>
<td>Mount 방식 (dbutils.fs.mount)<br>별도 권한 / Key Vault 설정 필수</td>
<td>Direct Access (Files/, Tables/)<br>OneLake 자동 통합, 설정 불필요</td>
</tr>
<tr>
<td>파일 최적화</td>
<td>Z-Order<br>사용자가 수동으로 실행 (OPTIMIZE)</td>
<td>V-Order Write 시 자동 적용 (기본값)</td>
</tr>
<tr>
<td>BI 연동</td>
<td>Power BI Import / DirectQuery<br>데이터 이동 또는 성능 제약 있음</td>
<td>Direct Lake<br>데이터 이동 없이 실시간급 조회</td>
</tr>
<tr>
<td>비용 모델</td>
<td>VM + DBU (이중 과금 구조)<br>복잡한 비용 예측</td>
<td>Capacity 단위 (통합 과금)<br>단일 Capacity로 모든 워크로드 사용</td>
</tr>
</tbody></table>
<br>

<table>
<thead>
<tr>
<th>구분</th>
<th>Databricks</th>
<th>Microsoft Fabric</th>
</tr>
</thead>
<tbody><tr>
<td>파일 경로</td>
<td>dbfs:/mnt/my_mount/data.csv</td>
<td>Files/data.csv (Relative)<br>Tables/my_table<br>abfss://... (Full Path)</td>
</tr>
<tr>
<td>파일 읽기</td>
<td>spark.read.csv(&quot;dbfs:/mnt/...&quot;)</td>
<td>spark.read.csv(&quot;Files/data/raw.csv&quot;)</td>
</tr>
<tr>
<td>테이블 저장</td>
<td>df.write.saveAsTable(&quot;hive_metastore...&quot;)</td>
<td>df.write.format(&quot;delta&quot;).save(&quot;Tables/sales&quot;)</td>
</tr>
<tr>
<td>파일 목록 조회</td>
<td>dbutils.fs.ls(&quot;/mnt/...&quot;)</td>
<td>mssparkutils.fs.ls(&quot;Files/...&quot;)</td>
</tr>
<tr>
<td>데이터 레이크</td>
<td>별도 구성 필요</td>
<td>OneLake 기본 통합</td>
</tr>
<tr>
<td>Delta Lake</td>
<td>필요 시 설치</td>
<td>기본 탑재 (설치 불필요)</td>
</tr>
</tbody></table>
<h2 id="medallion-architecture">Medallion Architecture</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>Bronze Layer (Raw Zone)</th>
<th>Silver Layer (Trusted Zone)</th>
<th>Gold Layer (Curated Zone)</th>
</tr>
</thead>
<tbody><tr>
<td>목표</td>
<td>원본 데이터 그대로 저장 (History 보존)</td>
<td>깨끗하고 신뢰할 수 있는 데이터 (분석 준비 완료)</td>
<td>비즈니스 리포팅 및 AI 모델링용 데이터</td>
</tr>
<tr>
<td>주요 작업</td>
<td>read (csv, json, parquet)<br>메타데이터 컬럼 추가 (ingestion_date, source_system)</td>
<td>NULL 처리 (dropna / fillna)<br>중복 제거 (dropDuplicates)<br>타입 변환 (cast)<br>스키마 강제</td>
<td>집계 (groupBy, sum, avg)<br>조인 (Fact + Dimension, Star Schema)<br>파생 변수 생성 (withColumn)</td>
</tr>
</tbody></table>
<h2 id="data-quality-startegy">Data Quality Startegy</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/c2d8318d-2bcd-467c-9e8c-6d4073769138/image.png" alt=""></p>
<h3 id="level-3-advanced-validation-고급">Level 3: Advanced Validation (고급)</h3>
<ul>
<li>What: 복잡한 비즈니스 규칙 검증</li>
<li>How: Great Expectations (GX) 라이브러리 활용 (Fabric Notebook에 설치 가능)</li>
<li>Note: Fabric은 Python 라이브러리를 자유롭게 지원하므로, 기존에 쓰던 GX 같은 도구를 그대로 사용 가능</li>
</ul>
<h3 id="level-2-constraint-checks-중급">Level 2: Constraint Checks (중급)</h3>
<ul>
<li>What: 값의 유효성 검사 (NULL 불가, 양수만 허용 등)</li>
<li>How: Delta Lake의 CHECK 제약 조건 사용</li>
<li>Code:<code>ALTER TABLE sales ADD CONSTRAINT valid_amount
CHECK (amount &gt; 0);</code></li>
</ul>
<h3 id="level-1-schema-enforcement-기본">Level 1: Schema Enforcement (기본)</h3>
<ul>
<li>What: 데이터 타입과 컬럼명 강제</li>
<li>How: Delta Table의 Schema Enforcement 기능 활용</li>
<li>Code: <code>df.write.option(&quot;mergeSchema&quot;, &quot;false&quot;).save(...)</code>
(스키마 변경 시 에러 발생시켜 보호)</li>
</ul>
<h2 id="delta-lake-in-fabric">Delta Lake in Fabric</h2>
<h4 id="native-integration">Native Integration</h4>
<ul>
<li>Spark, SQL Engine, Power BI 모두 Delta Lake를 기본 포맷으로 인식</li>
<li>복잡한 변환 과정이 없음<ul>
<li>Databricks에서는 Parquet도 써야 하는데, Fabric은 Delta만 쓰면 됨<h4 id="v-order-optimization">V-Order Optimization</h4>
</li>
</ul>
</li>
<li>Spark로 저장하는 순간 자동 최적화</li>
<li>열 순서 재정렬, 압축 레벨 조정</li>
<li>별도의 Z-Ordering 설정 불필요<h4 id="one-copy-principle">One Copy Principle</h4>
</li>
<li>Power BI가 Direct Lake 방식으로 직접 읽음 (복제 없음)</li>
<li>Bronze/Silver/Gold 데이터를 바로BI에 연결 가능</li>
<li>저장 공간 절감 + 최신성 보장<h2 id="data-wrangler">Data Wrangler</h2>
전처리 과정을 visualization해서 보여줌</li>
<li>예비 데이터 분석을 위한 몰입형 인터페이스를 제공하는 Notebook 기반 도구</li>
<li>격자 형태의 데이터 표시, 동적 요약 통계, 기본 제공 시각화 및 일반적인 데이터 정리 작업에 대한 라이브러리를 결합</li>
</ul>
<hr>

<h1 id="실습-microsoft-fabric-레이크하우스에-메달리온-아키텍처-구축하기">실습: Microsoft Fabric 레이크하우스에 메달리온 아키텍처 구축하기</h1>
<p>맞다. 전에 내가 <strong>페이지에 있는 코드 셀 전부를 다 안 넣었다</strong>.
이번엔 페이지 원문을 다시 확인해서, <strong>실습 흐름에 나온 코드 셀들을 순서대로 빠짐없이 재구성</strong>해서 준다. 이 실습은 Silver용 Notebook, SQL 쿼리 2개, Gold용 Notebook으로 구성된다. ([YSSuperS2000][1])</p>
<p>아래 내용은 <strong>Velog에 올리기 좋게 정리한 완전판</strong>이다.</p>
<hr>
<h1 id="microsoft-fabric-medallion-lakehouse-실습-정리">Microsoft Fabric Medallion Lakehouse 실습 정리</h1>
<h2 id="개요">개요</h2>
<p>이번 실습에서는 Microsoft Fabric Lakehouse에서 <strong>Bronze → Silver → Gold</strong> 구조의 메달리온 아키텍처를 구축한다.
실습 흐름은 다음과 같다.</p>
<ul>
<li>Bronze 폴더에 원본 CSV 업로드</li>
<li>Notebook으로 Silver Delta 테이블 생성 및 적재</li>
<li>SQL endpoint로 Silver 데이터 탐색</li>
<li>Notebook으로 Gold 차원/팩트 테이블 생성</li>
<li>필요 시 Semantic Model 생성 ([YSSuperS2000][1])</li>
</ul>
<hr>
<h2 id="1-bronze-layer">1. Bronze Layer</h2>
<p>Lakehouse의 <code>Files/bronze/</code> 경로에 아래 3개 파일을 업로드한다. ([YSSuperS2000][1])</p>
<ul>
<li><code>2019.csv</code></li>
<li><code>2020.csv</code></li>
<li><code>2021.csv</code></li>
</ul>
<hr>
<h2 id="2-silver-layer-notebook">2. Silver Layer Notebook</h2>
<h3 id="2-1-원본-csv-로드--스키마-정의">2-1. 원본 CSV 로드 + 스키마 정의</h3>
<pre><code class="language-python">from pyspark.sql.types import *

orderSchema = StructType([
    StructField(&quot;SalesOrderNumber&quot;, StringType()),
    StructField(&quot;SalesOrderLineNumber&quot;, IntegerType()),
    StructField(&quot;OrderDate&quot;, DateType()),
    StructField(&quot;CustomerName&quot;, StringType()),
    StructField(&quot;Email&quot;, StringType()),
    StructField(&quot;Item&quot;, StringType()),
    StructField(&quot;Quantity&quot;, IntegerType()),
    StructField(&quot;UnitPrice&quot;, FloatType()),
    StructField(&quot;Tax&quot;, FloatType())
])

df = spark.read.format(&quot;csv&quot;).option(&quot;header&quot;, &quot;false&quot;).schema(orderSchema).load(&quot;Files/bronze/*.csv&quot;)

display(df.head(10))</code></pre>
<h3 id="2-2-컬럼-추가-및-데이터-정제">2-2. 컬럼 추가 및 데이터 정제</h3>
<p>원본 파일명, 플래그 여부, 생성/수정 시각을 추가하고, <code>CustomerName</code>이 비어 있거나 null이면 <code>&quot;Unknown&quot;</code>으로 치환한다. ([YSSuperS2000][1])</p>
<pre><code class="language-python">from pyspark.sql.functions import when, lit, col, current_timestamp, input_file_name

df = df.withColumn(&quot;FileName&quot;, input_file_name()) \
    .withColumn(&quot;IsFlagged&quot;, when(col(&quot;OrderDate&quot;) &lt; &#39;2019-08-01&#39;, True).otherwise(False)) \
    .withColumn(&quot;CreatedTS&quot;, current_timestamp()) \
    .withColumn(&quot;ModifiedTS&quot;, current_timestamp())

df = df.withColumn(
    &quot;CustomerName&quot;,
    when((col(&quot;CustomerName&quot;).isNull() | (col(&quot;CustomerName&quot;) == &quot;&quot;)), lit(&quot;Unknown&quot;))
    .otherwise(col(&quot;CustomerName&quot;))
)</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/10dd0b29-3cd0-4c09-824c-200c6de5f0d5/image.png" alt=""></p>
<h3 id="2-3-salessales_silver-테이블-생성">2-3. <code>sales.sales_silver</code> 테이블 생성</h3>
<p>실습 페이지에서는 Delta Lake 형식으로 Silver 테이블 스키마를 먼저 정의한다. 
만약 스키마를 사용하지 않는다고 lakehouse 생성 시 설정한 경우 <code>sales.sales_silver</code> 가 아닌, <code>sales_silver</code>로 테이블을 설정해서 dbo에 저장하면 된다.</p>
<pre><code class="language-python">from pyspark.sql.types import *
from delta.tables import *

DeltaTable.createIfNotExists(spark) \
    .tableName(&quot;sales.sales_silver&quot;) \
    .addColumn(&quot;SalesOrderNumber&quot;, StringType()) \
    .addColumn(&quot;SalesOrderLineNumber&quot;, IntegerType()) \
    .addColumn(&quot;OrderDate&quot;, DateType()) \
    .addColumn(&quot;CustomerName&quot;, StringType()) \
    .addColumn(&quot;Email&quot;, StringType()) \
    .addColumn(&quot;Item&quot;, StringType()) \
    .addColumn(&quot;Quantity&quot;, IntegerType()) \
    .addColumn(&quot;UnitPrice&quot;, FloatType()) \
    .addColumn(&quot;Tax&quot;, FloatType()) \
    .addColumn(&quot;FileName&quot;, StringType()) \
    .addColumn(&quot;IsFlagged&quot;, BooleanType()) \
    .addColumn(&quot;CreatedTS&quot;, DateType()) \
    .addColumn(&quot;ModifiedTS&quot;, DateType()) \
    .execute()</code></pre>
<h3 id="2-4-silver-테이블-upsert">2-4. Silver 테이블 Upsert</h3>
<p><code>SalesOrderNumber</code>, <code>OrderDate</code>, <code>CustomerName</code>, <code>Item</code> 기준으로 merge를 수행하고, 일치하지 않으면 insert한다. 실습 페이지의 <code>whenMatchedUpdate</code>는 비어 있는 형태로 제시되어 있다. ([YSSuperS2000][1])</p>
<pre><code class="language-python">from delta.tables import *

deltaTable = DeltaTable.forPath(spark, &#39;Tables/sales_silver&#39;)

dfUpdates = df

deltaTable.alias(&#39;silver&#39;) \
  .merge(
    dfUpdates.alias(&#39;updates&#39;),
    &#39;silver.SalesOrderNumber = updates.SalesOrderNumber and silver.OrderDate = updates.OrderDate and silver.CustomerName = updates.CustomerName and silver.Item = updates.Item&#39;
  ) \
  .whenMatchedUpdate(set=
    {
    }
  ) \
  .whenNotMatchedInsert(values=
    {
      &quot;SalesOrderNumber&quot;: &quot;updates.SalesOrderNumber&quot;,
      &quot;SalesOrderLineNumber&quot;: &quot;updates.SalesOrderLineNumber&quot;,
      &quot;OrderDate&quot;: &quot;updates.OrderDate&quot;,
      &quot;CustomerName&quot;: &quot;updates.CustomerName&quot;,
      &quot;Email&quot;: &quot;updates.Email&quot;,
      &quot;Item&quot;: &quot;updates.Item&quot;,
      &quot;Quantity&quot;: &quot;updates.Quantity&quot;,
      &quot;UnitPrice&quot;: &quot;updates.UnitPrice&quot;,
      &quot;Tax&quot;: &quot;updates.Tax&quot;,
      &quot;FileName&quot;: &quot;updates.FileName&quot;,
      &quot;IsFlagged&quot;: &quot;updates.IsFlagged&quot;,
      &quot;CreatedTS&quot;: &quot;updates.CreatedTS&quot;,
      &quot;ModifiedTS&quot;: &quot;updates.ModifiedTS&quot;
    }
  ) \
  .execute()</code></pre>
<hr>
<h2 id="3-sql-endpoint에서-silver-데이터-탐색">3. SQL Endpoint에서 Silver 데이터 탐색</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/c30834fc-6e6b-4355-9e06-7d1f3020173a/image.png" alt=""></p>
<h3 id="3-1-연도별-총매출">3-1. 연도별 총매출</h3>
<pre><code class="language-sql">SELECT YEAR(OrderDate) AS Year
, CAST (SUM(Quantity * (UnitPrice + Tax)) AS DECIMAL(12, 2)) AS TotalSales
FROM sales_silver
GROUP BY YEAR(OrderDate)
ORDER BY YEAR(OrderDate)</code></pre>
<h3 id="3-2-구매-수량-상위-고객-10명">3-2. 구매 수량 상위 고객 10명</h3>
<pre><code class="language-sql">SELECT TOP 10 CustomerName, SUM(Quantity) AS TotalQuantity
FROM sales_silver
GROUP BY CustomerName
ORDER BY TotalQuantity DESC</code></pre>
<hr>
<h2 id="4-gold-layer-notebook">4. Gold Layer Notebook</h2>
<p>Notebook 이름: <code>Transform data for Gold</code>
이 Notebook에서는 <code>sales_silver</code>를 기반으로 날짜 차원, 고객 차원, 제품 차원, 판매 팩트 테이블을 만든다. ([YSSuperS2000][1])</p>
<h3 id="4-1-silver-데이터-로드">4-1. Silver 데이터 로드</h3>
<pre><code class="language-python">df = spark.read.table(&quot;Sales.sales_silver&quot;)</code></pre>
<hr>
<h3 id="4-2-날짜-차원-테이블-생성">4-2. 날짜 차원 테이블 생성</h3>
<h3 id="4-2-1-salesdimdate_gold-테이블-생성">4-2-1. <code>sales.dimdate_gold</code> 테이블 생성</h3>
<pre><code class="language-python">from pyspark.sql.types import *
from delta.tables import *

DeltaTable.createIfNotExists(spark) \
    .tableName(&quot;sales.dimdate_gold&quot;) \
    .addColumn(&quot;OrderDate&quot;, DateType()) \
    .addColumn(&quot;Day&quot;, IntegerType()) \
    .addColumn(&quot;Month&quot;, IntegerType()) \
    .addColumn(&quot;Year&quot;, IntegerType()) \
    .addColumn(&quot;mmmyyyy&quot;, StringType()) \
    .addColumn(&quot;yyyymm&quot;, StringType()) \
    .execute()</code></pre>
<h3 id="4-2-2-날짜-차원-dataframe-생성">4-2-2. 날짜 차원 DataFrame 생성</h3>
<pre><code class="language-python">from pyspark.sql.functions import col, dayofmonth, month, year, date_format

dfdimDate_gold = df.dropDuplicates([&quot;OrderDate&quot;]).select(
        col(&quot;OrderDate&quot;),
        dayofmonth(&quot;OrderDate&quot;).alias(&quot;Day&quot;),
        month(&quot;OrderDate&quot;).alias(&quot;Month&quot;),
        year(&quot;OrderDate&quot;).alias(&quot;Year&quot;),
        date_format(col(&quot;OrderDate&quot;), &quot;MMM-yyyy&quot;).alias(&quot;mmmyyyy&quot;),
        date_format(col(&quot;OrderDate&quot;), &quot;yyyyMM&quot;).alias(&quot;yyyymm&quot;),
    ).orderBy(&quot;OrderDate&quot;)

display(dfdimDate_gold.head(10))</code></pre>
<h3 id="4-2-3-날짜-차원-upsert">4-2-3. 날짜 차원 Upsert</h3>
<pre><code class="language-python">from delta.tables import *

deltaTable = DeltaTable.forPath(spark, &#39;Tables/dimdate_gold&#39;)

dfUpdates = dfdimDate_gold

deltaTable.alias(&#39;gold&#39;) \
  .merge(
    dfUpdates.alias(&#39;updates&#39;),
    &#39;gold.OrderDate = updates.OrderDate&#39;
  ) \
  .whenMatchedUpdate(set=
    {
    }
  ) \
  .whenNotMatchedInsert(values=
    {
      &quot;OrderDate&quot;: &quot;updates.OrderDate&quot;,
      &quot;Day&quot;: &quot;updates.Day&quot;,
      &quot;Month&quot;: &quot;updates.Month&quot;,
      &quot;Year&quot;: &quot;updates.Year&quot;,
      &quot;mmmyyyy&quot;: &quot;updates.mmmyyyy&quot;,
      &quot;yyyymm&quot;: &quot;updates.yyyymm&quot;
    }
  ) \
  .execute()</code></pre>
<hr>
<h3 id="4-3-고객-차원-테이블-생성">4-3. 고객 차원 테이블 생성</h3>
<h3 id="4-3-1-salesdimcustomer_gold-테이블-생성">4-3-1. <code>sales.dimcustomer_gold</code> 테이블 생성</h3>
<pre><code class="language-python">from pyspark.sql.types import *
from delta.tables import *

DeltaTable.createIfNotExists(spark) \
    .tableName(&quot;sales.dimcustomer_gold&quot;) \
    .addColumn(&quot;CustomerName&quot;, StringType()) \
    .addColumn(&quot;Email&quot;, StringType()) \
    .addColumn(&quot;First&quot;, StringType()) \
    .addColumn(&quot;Last&quot;, StringType()) \
    .addColumn(&quot;CustomerID&quot;, LongType()) \
    .execute()</code></pre>
<h3 id="4-3-2-고객-차원용-silver-dataframe-생성">4-3-2. 고객 차원용 Silver DataFrame 생성</h3>
<pre><code class="language-python">from pyspark.sql.functions import col, split

dfdimCustomer_silver = df.dropDuplicates([&quot;CustomerName&quot;, &quot;Email&quot;]).select(
    col(&quot;CustomerName&quot;),
    col(&quot;Email&quot;)
).withColumn(
    &quot;First&quot;, split(col(&quot;CustomerName&quot;), &quot; &quot;).getItem(0)
).withColumn(
    &quot;Last&quot;, split(col(&quot;CustomerName&quot;), &quot; &quot;).getItem(1)
)

display(dfdimCustomer_silver.head(10))</code></pre>
<h3 id="4-3-3-customerid-생성">4-3-3. CustomerID 생성</h3>
<p>기존 <code>dimCustomer_gold</code>와 비교해서 신규 고객만 남긴 뒤 <code>monotonically_increasing_id()</code>로 ID를 만든다. ([YSSuperS2000][1])</p>
<pre><code class="language-python">from pyspark.sql.functions import monotonically_increasing_id, col, when, coalesce, max, lit

dfdimCustomer_temp = spark.read.table(&quot;Sales.dimCustomer_gold&quot;)
MAXCustomerID = dfdimCustomer_temp.select(
    coalesce(max(col(&quot;CustomerID&quot;)), lit(0)).alias(&quot;MAXCustomerID&quot;)
).first()[0]

dfdimCustomer_gold = dfdimCustomer_silver.join(
    dfdimCustomer_temp,
    (dfdimCustomer_silver.CustomerName == dfdimCustomer_temp.CustomerName) &amp;
    (dfdimCustomer_silver.Email == dfdimCustomer_temp.Email),
    &quot;left_anti&quot;
)

dfdimCustomer_gold = dfdimCustomer_gold.withColumn(
    &quot;CustomerID&quot;,
    monotonically_increasing_id() + MAXCustomerID + 1
)

display(dfdimCustomer_gold.head(10))</code></pre>
<h3 id="4-3-4-고객-차원-upsert">4-3-4. 고객 차원 Upsert</h3>
<pre><code class="language-python">from delta.tables import *

deltaTable = DeltaTable.forPath(spark, &#39;Tables/dimcustomer_gold&#39;)

dfUpdates = dfdimCustomer_gold

deltaTable.alias(&#39;gold&#39;) \
  .merge(
    dfUpdates.alias(&#39;updates&#39;),
    &#39;gold.CustomerName = updates.CustomerName AND gold.Email = updates.Email&#39;
  ) \
  .whenMatchedUpdate(set=
    {
    }
  ) \
  .whenNotMatchedInsert(values=
    {
      &quot;CustomerName&quot;: &quot;updates.CustomerName&quot;,
      &quot;Email&quot;: &quot;updates.Email&quot;,
      &quot;First&quot;: &quot;updates.First&quot;,
      &quot;Last&quot;: &quot;updates.Last&quot;,
      &quot;CustomerID&quot;: &quot;updates.CustomerID&quot;
    }
  ) \
  .execute()</code></pre>
<hr>
<h3 id="4-4-제품-차원-테이블-생성">4-4. 제품 차원 테이블 생성</h3>
<h3 id="4-4-1-salesdimproduct_gold-테이블-생성">4-4-1. <code>sales.dimproduct_gold</code> 테이블 생성</h3>
<pre><code class="language-python">from pyspark.sql.types import *
from delta.tables import *

DeltaTable.createIfNotExists(spark) \
    .tableName(&quot;sales.dimproduct_gold&quot;) \
    .addColumn(&quot;ItemName&quot;, StringType()) \
    .addColumn(&quot;ItemID&quot;, LongType()) \
    .addColumn(&quot;ItemInfo&quot;, StringType()) \
    .execute()</code></pre>
<h3 id="4-4-2-제품-차원용-silver-dataframe-생성">4-4-2. 제품 차원용 Silver DataFrame 생성</h3>
<p><code>Item</code> 컬럼을 <code>ItemName</code>, <code>ItemInfo</code>로 분리한다. ([YSSuperS2000][1])</p>
<pre><code class="language-python">from pyspark.sql.functions import col, split, lit, when

dfdimProduct_silver = df.dropDuplicates([&quot;Item&quot;]).select(col(&quot;Item&quot;)) \
    .withColumn(&quot;ItemName&quot;, split(col(&quot;Item&quot;), &quot;, &quot;).getItem(0)) \
    .withColumn(
        &quot;ItemInfo&quot;,
        when(
            (split(col(&quot;Item&quot;), &quot;, &quot;).getItem(1).isNull() |
             (split(col(&quot;Item&quot;), &quot;, &quot;).getItem(1) == &quot;&quot;)),
            lit(&quot;&quot;)
        ).otherwise(split(col(&quot;Item&quot;), &quot;, &quot;).getItem(1))
    )

display(dfdimProduct_silver.head(10))</code></pre>
<h3 id="4-4-3-itemid-생성">4-4-3. ItemID 생성</h3>
<pre><code class="language-python">from pyspark.sql.functions import monotonically_increasing_id, col, lit, max, coalesce

dfdimProduct_temp = spark.read.table(&quot;Sales.dimProduct_gold&quot;)

MAXProductID = dfdimProduct_temp.select(
    coalesce(max(col(&quot;ItemID&quot;)), lit(0)).alias(&quot;MAXItemID&quot;)
).first()[0]

dfdimProduct_gold = dfdimProduct_silver.join(
    dfdimProduct_temp,
    (dfdimProduct_silver.ItemName == dfdimProduct_temp.ItemName) &amp;
    (dfdimProduct_silver.ItemInfo == dfdimProduct_temp.ItemInfo),
    &quot;left_anti&quot;
)

dfdimProduct_gold = dfdimProduct_gold.withColumn(
    &quot;ItemID&quot;,
    monotonically_increasing_id() + MAXProductID + 1
)

display(dfdimProduct_gold.head(10))</code></pre>
<h3 id="4-4-4-제품-차원-upsert">4-4-4. 제품 차원 Upsert</h3>
<pre><code class="language-python">from delta.tables import *

deltaTable = DeltaTable.forPath(spark, &#39;Tables/dimproduct_gold&#39;)

dfUpdates = dfdimProduct_gold

deltaTable.alias(&#39;gold&#39;) \
  .merge(
    dfUpdates.alias(&#39;updates&#39;),
    &#39;gold.ItemName = updates.ItemName AND gold.ItemInfo = updates.ItemInfo&#39;
  ) \
  .whenMatchedUpdate(set=
    {
    }
  ) \
  .whenNotMatchedInsert(values=
    {
      &quot;ItemName&quot;: &quot;updates.ItemName&quot;,
      &quot;ItemInfo&quot;: &quot;updates.ItemInfo&quot;,
      &quot;ItemID&quot;: &quot;updates.ItemID&quot;
    }
  ) \
  .execute()</code></pre>
<hr>
<h3 id="4-5-판매-팩트-테이블-생성">4-5. 판매 팩트 테이블 생성</h3>
<h3 id="4-5-1-salesfactsales_gold-테이블-생성">4-5-1. <code>sales.factsales_gold</code> 테이블 생성</h3>
<pre><code class="language-python">from pyspark.sql.types import *
from delta.tables import *

DeltaTable.createIfNotExists(spark) \
    .tableName(&quot;sales.factsales_gold&quot;) \
    .addColumn(&quot;CustomerID&quot;, LongType()) \
    .addColumn(&quot;ItemID&quot;, LongType()) \
    .addColumn(&quot;OrderDate&quot;, DateType()) \
    .addColumn(&quot;Quantity&quot;, IntegerType()) \
    .addColumn(&quot;UnitPrice&quot;, FloatType()) \
    .addColumn(&quot;Tax&quot;, FloatType()) \
    .execute()</code></pre>
<h3 id="4-5-2-팩트-dataframe-생성">4-5-2. 팩트 DataFrame 생성</h3>
<p>고객 차원, 제품 차원과 조인해서 <code>CustomerID</code>, <code>ItemID</code>를 붙인다. ([YSSuperS2000][1])</p>
<pre><code class="language-python">from pyspark.sql.functions import col, split, lit, when

dfdimCustomer_temp = spark.read.table(&quot;Sales.dimCustomer_gold&quot;)
dfdimProduct_temp = spark.read.table(&quot;Sales.dimProduct_gold&quot;)

df = df.withColumn(&quot;ItemName&quot;, split(col(&quot;Item&quot;), &quot;, &quot;).getItem(0)) \
    .withColumn(
        &quot;ItemInfo&quot;,
        when(
            (split(col(&quot;Item&quot;), &quot;, &quot;).getItem(1).isNull() |
             (split(col(&quot;Item&quot;), &quot;, &quot;).getItem(1) == &quot;&quot;)),
            lit(&quot;&quot;)
        ).otherwise(split(col(&quot;Item&quot;), &quot;, &quot;).getItem(1))
    )

dffactSales_gold = df.alias(&quot;df1&quot;) \
    .join(
        dfdimCustomer_temp.alias(&quot;df2&quot;),
        (df.CustomerName == dfdimCustomer_temp.CustomerName) &amp;
        (df.Email == dfdimCustomer_temp.Email),
        &quot;left&quot;
    ) \
    .join(
        dfdimProduct_temp.alias(&quot;df3&quot;),
        (df.ItemName == dfdimProduct_temp.ItemName) &amp;
        (df.ItemInfo == dfdimProduct_temp.ItemInfo),
        &quot;left&quot;
    ) \
    .select(
        col(&quot;df2.CustomerID&quot;),
        col(&quot;df3.ItemID&quot;),
        col(&quot;df1.OrderDate&quot;),
        col(&quot;df1.Quantity&quot;),
        col(&quot;df1.UnitPrice&quot;),
        col(&quot;df1.Tax&quot;)
    ) \
    .orderBy(col(&quot;df1.OrderDate&quot;), col(&quot;df2.CustomerID&quot;), col(&quot;df3.ItemID&quot;))

display(dffactSales_gold.head(10))</code></pre>
<h3 id="4-5-3-팩트-테이블-upsert">4-5-3. 팩트 테이블 Upsert</h3>
<pre><code class="language-python">from delta.tables import *

deltaTable = DeltaTable.forPath(spark, &#39;Tables/factsales_gold&#39;)

dfUpdates = dffactSales_gold

deltaTable.alias(&#39;gold&#39;) \
  .merge(
    dfUpdates.alias(&#39;updates&#39;),
    &#39;gold.OrderDate = updates.OrderDate AND gold.CustomerID = updates.CustomerID AND gold.ItemID = updates.ItemID&#39;
  ) \
  .whenMatchedUpdate(set=
    {
    }
  ) \
  .whenNotMatchedInsert(values=
    {
      &quot;CustomerID&quot;: &quot;updates.CustomerID&quot;,
      &quot;ItemID&quot;: &quot;updates.ItemID&quot;,
      &quot;OrderDate&quot;: &quot;updates.OrderDate&quot;,
      &quot;Quantity&quot;: &quot;updates.Quantity&quot;,
      &quot;UnitPrice&quot;: &quot;updates.UnitPrice&quot;,
      &quot;Tax&quot;: &quot;updates.Tax&quot;
    }
  ) \
  .execute()</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/4c911958-fe89-4fc9-8783-792230e60b0e/image.png" alt=""></p>
<hr>

<h1 id="microsoft-fabric에서-data-wrangler를-사용하여-데이터-전처리">Microsoft Fabric에서 Data Wrangler를 사용하여 데이터 전처리</h1>
<h2 id="dataframe에-데이터-로드">Dataframe에 데이터 로드</h2>
<pre><code class="language-python"># Azure storage access info for open dataset diabetes
blob_account_name = &quot;azureopendatastorage&quot;
blob_container_name = &quot;ojsales-simulatedcontainer&quot;
blob_relative_path = &quot;oj_sales_data&quot;
blob_sas_token = r&quot;&quot; # Blank since container is Anonymous access

# Set Spark config to access  blob storage
wasbs_path = f&quot;wasbs://%s@%s.blob.core.windows.net/%s&quot; % (blob_container_name, blob_account_name, blob_relative_path)
spark.conf.set(&quot;fs.azure.sas.%s.%s.blob.core.windows.net&quot; % (blob_container_name, blob_account_name), blob_sas_token)
print(&quot;Remote blob path: &quot; + wasbs_path)

# Spark reads csv
df = spark.read.csv(wasbs_path, header=True)</code></pre>
<pre><code class="language-python">import pandas as pd

df = df.toPandas()
df = df.sample(n=500, random_state=1)

df[&#39;WeekStarting&#39;] = pd.to_datetime(df[&#39;WeekStarting&#39;])
df[&#39;Quantity&#39;] = df[&#39;Quantity&#39;].astype(&#39;int&#39;)
df[&#39;Advert&#39;] = df[&#39;Advert&#39;].astype(&#39;int&#39;)
df[&#39;Price&#39;] = df[&#39;Price&#39;].astype(&#39;float&#39;)
df[&#39;Revenue&#39;] = df[&#39;Revenue&#39;].astype(&#39;float&#39;)

df = df.reset_index(drop=True)
df.head(4)</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/e84ca673-6e3b-4252-bb79-d39ae283fa69/image.png" alt=""></p>
<h2 id="텍스트-데이터-형식-지정">텍스트 데이터 형식 지정</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/5a0361c2-70c3-4215-a3da-3d4b77021853/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/702634dd-d831-4036-9d64-4a5cbbc535a5/image.png" alt=""></p>
<p>1 Data Wrangler 대시보드에서 그리드의 Brand Feature를 선택합니다.
<img src="https://velog.velcdn.com/images/rudin_/post/5f7e090d-81f3-46c4-a19f-473258f0b58b/image.png" alt=""></p>
<p>2 Operations 패널로 이동하여 찾기 및 바꾸기를 확장한 다음, 찾기 및 바꾸기를 선택합니다.
3 찾기 및 바꾸기 패널에서 다음 속성을 변경합니다.</p>
<pre><code>이전 값: “.”
새 값: “” (공백 문자)
작업 결과가 디스플레이 그리드에 자동으로 미리 보기됩니다.</code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/f012c3d5-d7bf-4b0a-b42a-c6aae26386b6/image.png" alt=""></p>
<p>4 적용을 선택합니다.
5 Operations 패널로 돌아가서 형식을 확장합니다.
6 첫 글자 대문자로 변환을 선택합니다. 모든 단어 대문자로 변환 토글을 켜고, 적용을 선택합니다.
<img src="https://velog.velcdn.com/images/rudin_/post/2feb8125-5959-48c9-9597-7a5e4861af11/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/e278f5af-a541-41e0-8f9d-bbb97f36a4e7/image.png" alt=""></p>
<p>7 Notebook에 코드 추가를 선택합니다. 또한, 코드를 복사하여 변환된 데이터 세트를 CSV 파일로 저장할 수도 있습니다.
8 Data Wrangler에서 생성된 코드는 원래 dataframe을 덮어쓰지 않으므로, 10행과 11행을 <code>df = clean_data(df)</code> 코드로 바꿉니다. 최종 코드 블록은 다음과 같아야 합니다.</p>
<pre><code class="language-python">def clean_data(df):
    # Replace all instances of &quot;.&quot; with &quot; &quot; in column: &#39;Brand&#39;
    df[&#39;Brand&#39;] = df[&#39;Brand&#39;].str.replace(&quot;.&quot;, &quot; &quot;, case=False, regex=False)
    # Capitalize the first character in column: &#39;Brand&#39;
    df[&#39;Brand&#39;] = df[&#39;Brand&#39;].str.title()
    return df

df = clean_data(df)</code></pre>
<p>9 코드 셀을 실행하고 Brand 변수를 확인합니다.</p>
<pre><code class="language-python">df[&#39;Brand&#39;].unique()</code></pre>
<h3 id="원-핫-인코딩-변환-적용">원-핫 인코딩 변환 적용</h3>
<p>원-핫 인코딩: 범주형 데이터를 머신러닝 모델이 이해할 수 있도록, 고유한 값에 해당하는 위치에만 1을 부여하고 나머지는 0으로 채워 수치화하는 방식</p>
<p>1 상단 메뉴에서 df dataframe에 대해 Data Wrangler를 시작합니다.
2 그리드의 Brand Feature를 선택합니다.
3 Operations 패널에서 수식을 확장한 다음, 원-핫 인코딩을 선택합니다.
<img src="https://velog.velcdn.com/images/rudin_/post/9773d051-081b-4009-92d3-b940ce521e90/image.png" alt=""></p>
<p>4 원-핫 인코딩 패널에서 적용을 선택합니다.
<img src="https://velog.velcdn.com/images/rudin_/post/56d075d7-c957-4d5e-a9d9-932d5b67cd8a/image.png" alt=""></p>
<p>5 코드를 생성하지 않고 Data Wrangler를 종료합니다.</p>
<h3 id="정렬-및-필터링-작업">정렬 및 필터링 작업</h3>
<p>1 df dataframe에 대해 Data Wrangler를 시작합니다.
2 Operations 패널로 돌아가서 정렬 및 필터링을 확장합니다.
3 필터를 선택합니다.
<img src="https://velog.velcdn.com/images/rudin_/post/fa24074d-87e0-4957-81e0-fdbdc219e5c0/image.png" alt=""></p>
<p>4 필터 패널에서 다음 조건을 추가합니다.</p>
<pre><code>대상 열: Store
작업: 같음
값: 1227
동작: 일치하는 행 유지</code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/1a5680d0-29cf-4733-9e4a-2b9c737f50bc/image.png" alt=""></p>
<p>5 적용을 선택하고 Data Wrangler 디스플레이 그리드의 변경 사항을 확인합니다.
6 Revenue Feature를 선택한 다음, 요약 사이드 패널의 세부 정보를 검토합니다.
7 Operations 패널로 돌아가서 정렬 및 필터링을 확장합니다.
8 값 정렬을 선택합니다.
9 값 정렬 패널에서 다음 속성을 선택합니다.</p>
<pre><code>열 이름: Price
정렬 순서: 내림차순</code></pre><p><img src="https://velog.velcdn.com/images/rudin_/post/0cd70d41-724f-4930-8212-77d76298b43f/image.png" alt=""></p>
<p>10 적용을 선택합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MicrosoftDataSchool] 72일차 - Fabric 시작하기]]></title>
            <link>https://velog.io/@rudin_/MicrosoftDataSchool-72%EC%9D%BC%EC%B0%A8-Fabric-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@rudin_/MicrosoftDataSchool-72%EC%9D%BC%EC%B0%A8-Fabric-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 20 Apr 2026 08:37:13 GMT</pubDate>
            <description><![CDATA[<h1 id="fabric-시작하기">Fabric 시작하기</h1>
<p>Databricks와 같은 서비스를 포함하는 더 큰 서비스</p>
<h2 id="실습-준비">실습 준비</h2>
<h3 id="평가판-활성화">평가판 활성화</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/2071edbc-c09d-4235-8f09-1a797fb74306/image.png" alt=""></p>
<h3 id="작업-영역-생성">작업 영역 생성</h3>
<p>패브릭 평가판을 선택한다.
<img src="https://velog.velcdn.com/images/rudin_/post/32ddf9dc-ea82-44bc-ada3-f7a706ad0359/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/4651e54f-b170-440c-bb6a-5fd5d8db2ac6/image.png" alt=""></p>
<hr>

<h1 id="onelake-기반의-데이터-수집-정제-파이프라인과-웨어하우스-분석-기초">OneLake 기반의 데이터 수집, 정제 파이프라인과 웨어하우스 분석 기초</h1>
<h2 id="fabric의-필요성">Fabric의 필요성</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/6ee49b82-720f-423a-8b43-ee1d724e20a3/image.png" alt=""></p>
<ul>
<li>도구의 파편화에서 비용 추적이 어려우며, 관리 복잡도가 증가함</li>
<li>데이터 사일로 현상 발생(팀 간 데이터 공유 어려움)</li>
<li>각 플랫폼별 학습곡선 존재
→ 통합 솔루션의 등장 필요</li>
</ul>
<h2 id="azure-service-fabric">Azure Service Fabric</h2>
<p>마이크로서비스와 컨테이너를 패키징, 배포, 관리하는 분산 시스템 플랫폼</p>
<h3 id="fabric에-속한-서비스">Fabric에 속한 서비스</h3>
<table>
<thead>
<tr>
<th>Experience</th>
<th>담당자</th>
<th>핵심기능</th>
</tr>
</thead>
<tbody><tr>
<td>Data Factory</td>
<td>데이터 엔지니어</td>
<td>데이터 수집, Mirroring (Zero-ETL), Shortcuts(연결)</td>
</tr>
<tr>
<td>Data Engineering</td>
<td>데이터 엔지니어</td>
<td>Spark 기반 변환, Lakehouse 구조, Medallion Architecture</td>
</tr>
<tr>
<td>Data Science</td>
<td>데이터 과학자</td>
<td>ML 모델 개발, MLflow 통합</td>
</tr>
<tr>
<td>Data Warehousing</td>
<td>데이터 분석가</td>
<td>T-SQL 분석, DirectLake(실시간), 엔터프라이즈 DW</td>
</tr>
<tr>
<td>Real-Time Intelligence</td>
<td>실시간 분석가</td>
<td>KQL 스트림 분석, Data Activator (알림)</td>
</tr>
<tr>
<td>Power BI</td>
<td>BI 개발자</td>
<td>시각화 &amp; 대시보드</td>
</tr>
</tbody></table>
<h3 id="onelake의-계층-구조">OneLake의 계층 구조</h3>
<pre><code>Tenant → Domain → Workspace → Item</code></pre><h3 id="lakehouse-vs-warehouse">Lakehouse vs Warehouse</h3>
<p>Fabric 은 둘 다 지원</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>Data Warehouse</th>
<th>Lakehouse</th>
</tr>
</thead>
<tbody><tr>
<td>데이터 형태</td>
<td>정형 데이터만 (정제된 형식)</td>
<td>모든 형식 (정형 + 반정형 + 비정형)</td>
</tr>
<tr>
<td>스키마</td>
<td>엄격함 (Schema-on-Write, Star Schema)</td>
<td>유연함 (Schema-on-Read)</td>
</tr>
<tr>
<td>데이터 흐름</td>
<td>Lakehouse → 변환 → 저장</td>
<td>수집 → 저장 (원본 그대로)</td>
</tr>
<tr>
<td>계층 구조</td>
<td>Gold (최종, 분석용)</td>
<td>Bronze(원본) → Silver(정제) → Gold</td>
</tr>
<tr>
<td>주요 사용자</td>
<td>비즈니스 분석가 (BI 팀)</td>
<td>데이터 엔지니어</td>
</tr>
<tr>
<td>처리 방식</td>
<td>ETL 중심</td>
<td>ELT 및 스트리밍 포함</td>
</tr>
<tr>
<td>활용 영역</td>
<td>BI, 리포트 중심</td>
<td>BI + 데이터 사이언스 + ML + 실시간 분석</td>
</tr>
</tbody></table>
<h3 id="microsoft-fabric-의사-결정-가이드">Microsoft Fabric 의사 결정 가이드</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/baf3eebb-6476-4a1d-8047-d20c5419aa76/image.png" alt=""></p>
<h3 id="데이터-이동을-위한-전략적-선택-가이드">데이터 이동을 위한 전략적 선택 가이드</h3>
<p>Fabric에서는 가능한 Shortcut이나 Mirroring을 쓰고, 어쩔 수 없을 때만 Copy하도록 권장</p>
<table>
<thead>
<tr>
<th>비교 항목</th>
<th>1. Mirroring</th>
<th>2. Copy Data Pipeline</th>
<th>3. Shortcut</th>
</tr>
</thead>
<tbody><tr>
<td>별명</td>
<td>거울 (실시간)</td>
<td>트럭 (배치)</td>
<td>지름길 (연결)</td>
</tr>
<tr>
<td>데이터 이동</td>
<td>자동 복제 (Zero-ETL)</td>
<td>물리적 복사</td>
<td>이동 없음</td>
</tr>
<tr>
<td>복잡도</td>
<td>매우 쉬움</td>
<td>중간 (설정 필요)</td>
<td>매우 쉬움</td>
</tr>
<tr>
<td>비용</td>
<td>무료 (컴퓨팅만)</td>
<td>유료 (처리량 비례)</td>
<td>무료</td>
</tr>
<tr>
<td>대상</td>
<td>Azure SQL, Snowflake</td>
<td>On-prem DB, File</td>
<td>S3, ADLS Gen2</td>
</tr>
</tbody></table>
<h3 id="shortcut의-동작방식">Shortcut의 동작방식</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/3d4858fc-9a41-4ada-bbf0-90dfef80a814/image.png" alt=""></p>
<p>Spark나 SQL은 OneLake를 바라보지만 실제 I/O는 원본 스토리지에서 직접 발생
Egress비용(Cloud 간 이동)은 발생할 수 있지만 Fabric 내부 저장 비용은 0원</p>
<hr>

<h1 id="실습">실습</h1>
<h2 id="lakehouse-생성-및-파일-업로드">Lakehouse 생성 및 파일 업로드</h2>
<blockquote>
<p><a href="https://microsoftlearning.github.io/mslearn-fabric/Instructions/Labs/01-lakehouse.html">https://microsoftlearning.github.io/mslearn-fabric/Instructions/Labs/01-lakehouse.html</a></p>
</blockquote>
<ul>
<li>Fabric의 저장소인 OneLake와 Lakehouse를 직접 만들어보기</li>
</ul>
<h3 id="생성">생성</h3>
<p>좌상단 <code>+새항목</code> 버튼 클릭
<img src="https://velog.velcdn.com/images/rudin_/post/3bc03457-2efb-4475-9bca-031e5dcc003c/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/7787c87e-2537-40d9-a916-eb3d6728761b/image.png" alt=""></p>
<ul>
<li>레이크하우스 스키마: 스키마 별로 저장 가능. X시 기본 데이터테이블에 저장</li>
</ul>
<h3 id="파일-업로드">파일 업로드</h3>
<p><code>데이터 가져오기</code> - <code>파일 업로드</code>
<img src="https://velog.velcdn.com/images/rudin_/post/d25cb3a1-0339-4740-b8d5-63212513c63e/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/9fff8e05-24af-4e1e-be5b-575795d4b04e/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/df3a10ad-d102-417c-bc4a-086d5fb6f305/image.png" alt=""></p>
<h3 id="테이블에-로드">테이블에 로드</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/a1478b8d-1d4f-4b9f-b193-808b5a70f414/image.png" alt=""></p>
<p>새 테이블 만들기를 시도했으나, 과정의 이전 기수들의 리소스 해제가 안되어있어 Too many Requests 오류가 발생했다.
<img src="https://velog.velcdn.com/images/rudin_/post/e97ac8b0-1b9e-4129-b23a-c63c57e0434a/image.png" alt="">
삭제해주신 후 정상 진행이 되었다.
무료 평가판에서는 테넌트 내의 50명이 정원인 듯 했다.</p>
<h3 id="쿼리-전송">쿼리 전송</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/4f3231d0-14d8-4051-b7af-a548d1f72686/image.png" alt="">
SQL 분석 엔드포인트로 이동
<img src="https://velog.velcdn.com/images/rudin_/post/9c25722a-e750-4a92-9bbd-0fc63b8aa666/image.png" alt="">
<code>새 SQL 쿼리</code> 선택
<img src="https://velog.velcdn.com/images/rudin_/post/a1a7f7b7-a022-4f66-977c-31d0f2c93d66/image.png" alt=""></p>
<h3 id="visual-query">Visual Query</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/e73e35f6-c260-4c41-b6cc-2f2d991daf82/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/e0daf835-4ae7-40f8-a383-b4cb24d343ef/image.png" alt="">
테이블을 드래그 앤 드랍해서 시작
<img src="https://velog.velcdn.com/images/rudin_/post/2ca2be77-bb30-4c6b-b960-0a7995cfcaac/image.png" alt="">
SQL 보기 선택으로 SQL 코드 확인 가능
<img src="https://velog.velcdn.com/images/rudin_/post/cedc69f7-dd1a-4a96-81a0-b2df488a1ef2/image.png" alt="">
뷰로 저장도 가능
<img src="https://velog.velcdn.com/images/rudin_/post/7b4f7664-2f4b-46df-bb48-be69df387084/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/c300f9c8-9ac7-49a3-b7bd-58b1c820ed68/image.png" alt=""></p>
<h2 id="파이프라인으로-데이터-가져오기http">파이프라인으로 데이터 가져오기(HTTP)</h2>
<p><a href="https://microsoftlearning.github.io/mslearn-fabric/Instructions/Labs/04-ingest-pipeline.html">https://microsoftlearning.github.io/mslearn-fabric/Instructions/Labs/04-ingest-pipeline.html</a></p>
<h3 id="파이프라인-생성">파이프라인 생성</h3>
<p>작업영역-좌상단 새항목-파이프라인 선택
<img src="https://velog.velcdn.com/images/rudin_/post/d778fba6-e62d-41fc-b1e7-6df1b294aafc/image.png" alt=""></p>
<p>데이터 복사 도우미 선택
<img src="https://velog.velcdn.com/images/rudin_/post/54f0cdba-94a4-447a-b69a-6291c56bca2e/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/714d984e-442a-41a6-b30b-79225a3794fa/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/50416c4f-fc4d-4e13-8e7b-a33be117f10c/image.png" alt="">
기존 레이크하우스 선택
<img src="https://velog.velcdn.com/images/rudin_/post/49eaf42d-c06a-43b0-b151-19cbc2d22d92/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/7a5605b2-a4f0-4215-985b-265efa394197/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/a2d0ca85-a278-4411-b8b1-76ce7c980361/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/501181be-8492-4b03-8a89-46e1b5dc3292/image.png" alt=""></p>
<p>연결에 파이프라인을 추가해야하는데, 안 뜨는 현상이 발생했다.
그래서 일단 작업영역으로 나와 직접 1회 파이프라인을 실행했다.
<img src="https://velog.velcdn.com/images/rudin_/post/6b5d2d33-7200-44e9-88f3-302124ad7981/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/74bb9107-83cb-4437-8541-ba2344572b14/image.png" alt="">
이렇게 하면 <code>복사작업</code> 권한이 생긴다..고했으나 생기지 않았다.
우상단 점 3개 - 설정 - 연결 및 게이트웨이 관리
<img src="https://velog.velcdn.com/images/rudin_/post/f3fae97e-5672-4c47-abe1-f3a5a09acd27/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/36ae3e9a-6518-4759-9262-a138eebe6396/image.png" alt="">
안뜬다면 자격증명을 새로 만들어야 한다.
<img src="https://velog.velcdn.com/images/rudin_/post/4188fd43-05dc-4518-9ba6-77ced1e400a8/image.png" alt="">
좌상단 신규 후 클라우드 선택
<img src="https://velog.velcdn.com/images/rudin_/post/f39c8ef6-470d-4a3d-b7de-093ae65fc90d/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/3471e31d-a6a3-4c35-9557-787917e84da1/image.png" alt="">
추가하면 이제 표시된다.
<img src="https://velog.velcdn.com/images/rudin_/post/a1ed02e2-12b4-4af1-a71f-a22848818d3b/image.png" alt=""></p>
<h3 id="노트북-생성">노트북 생성</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/65ce1d51-52d6-420d-90a5-00a4e804fdc7/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/49671708-ff96-417b-9870-6397eafecabf/image.png" alt=""></p>
<p>여기서도 표준 세션을 할당할 때 403 에러가 발생했다. 다른 분이 세션을 끊어주셔야만 쓸 수 있었다. 문제가 많다.
<img src="https://velog.velcdn.com/images/rudin_/post/3aad6b16-23ac-4941-919d-c4e1c5d80e8e/image.png" alt="">
또한 세션을 받더라도, Storage Blob Data Contributor 권한이 없어 에러가 발생했다.</p>
<p><img src="https://velog.velcdn.com/images/rudin_/post/ca2d7d91-234f-4a46-a0d2-20a56ebda258/image.png" alt="">
작업 영역 설정에서 
<img src="https://velog.velcdn.com/images/rudin_/post/d2fee995-95f3-46ce-a9a8-8a8aa10a8771/image.png" alt="">
작업 영역 유형 설정이 가능하다.
여기서 원격 컴퓨팅 리소스를 설정할 수 있다.
일단은 강의에선 5개의 컴퓨팅 리소스로 사람들을 분할하여 해결하고자 했다.</p>
<p>그럼에도 400 에러가 발생했다.
<img src="https://velog.velcdn.com/images/rudin_/post/51acf7a4-df09-4943-bfaa-35335b4fbd1c/image.png" alt="">
알고보니 내가 데이터 항목을 추가하지 않아서였다.</p>
<p>이후에도 강사님께서 추가 컴퓨팅 리소스를 마련해주심에도 세션이 시작되지 않아 노트북을 아예 새로 생성했더니 세션 설정이 되었다.
미묘하게 처리 확인이 힘든 에러가 많은듯하다.</p>
<p><img src="https://velog.velcdn.com/images/rudin_/post/1299542b-7701-428a-8d0f-685892fdff0b/image.png" alt=""></p>
<p>참고로 변수처럼 테이블 이름을 바꿀 수 있게 하기 위해서는 toggle parameter cell 을 선택해줘야 한다.
<img src="https://velog.velcdn.com/images/rudin_/post/02bc8cd9-4de9-408d-8b5b-c61216ceac02/image.png" alt=""></p>
<h3 id="파이프라인에-노트북-추가">파이프라인에 노트북 추가</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/1703b8e6-2fec-42d1-b86b-cbd6414b3d4a/image.png" alt="">
우상단 노트북 선택으로 추가
<img src="https://velog.velcdn.com/images/rudin_/post/a6c3106d-056a-4e7d-885f-6ad9261b8d8b/image.png" alt="">
이후 <code>기본 매개변수 추가</code></p>
<p><img src="https://velog.velcdn.com/images/rudin_/post/7599ea99-7c1b-42d0-a07d-90c51ce777c3/image.png" alt="">
데이터 삭제를 추가하여 기존에 만들어둔 csv를 삭제한다.
<img src="https://velog.velcdn.com/images/rudin_/post/49558f4f-b8a1-4973-bdde-1a6f2b6f11f7/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/b092566d-2a4a-48fb-8f75-2996c666dda5/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/rudin_/post/7dacc77b-d9ea-4ea8-9077-d73433b79856/image.png" alt=""></p>
<p>기존의 sales2가 아닌, new_sales2로 테이블이 생성됨을 확인 가능하다
<img src="https://velog.velcdn.com/images/rudin_/post/d85de149-d90a-4f0a-ae49-18fa7a438a04/image.png" alt=""></p>
<hr>

<h2 id="데이터-웨어하우스에서-데이터-분석">데이터 웨어하우스에서 데이터 분석</h2>
<p><a href="https://microsoftlearning.github.io/mslearn-fabric/Instructions/Labs/06-data-warehouse.html">https://microsoftlearning.github.io/mslearn-fabric/Instructions/Labs/06-data-warehouse.html</a></p>
<p><img src="https://velog.velcdn.com/images/rudin_/post/f72f1c7c-667b-4211-b3d9-87bf52cce762/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/c0494440-3bea-4d09-98bd-64898991047c/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/dd5bfa43-d1e2-4a6c-a605-8237ba9c3124/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/0a4fc535-17bc-4612-bb81-ac45046f1662/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/a828cb19-fd27-419c-b6ca-30080ef925f3/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/25d8608b-a29c-4a80-9594-92a6902660bc/image.png" alt="">
이후 <a href="https://raw.githubusercontent.com/MicrosoftLearning/dp-data/main/create-dw.txt">https://raw.githubusercontent.com/MicrosoftLearning/dp-data/main/create-dw.txt</a> 의 쿼리로 데이터 생성</p>
<ul>
<li>DimCustomer</li>
<li>DimDate</li>
<li>DimProduct</li>
<li>FactSalesOrder</li>
</ul>
<h3 id="쿼리">쿼리</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/743f8b97-3466-4bad-8739-6ef6b2a58c0f/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/c5ddc6ff-1ee1-4475-bee3-fa61cf31b3c1/image.png" alt=""></p>
<h3 id="뷰-생성">뷰 생성</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/9b5a57e3-940f-4496-8210-f3c9deca504b/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/c75c38a3-c12f-48a3-bc0e-593843968d7b/image.png" alt=""></p>
<h3 id="visual-query-1">Visual Query</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/8c1bc6c2-dd87-4d8b-8eca-e1880d0aaebb/image.png" alt=""></p>
<hr>

<h2 id="fabric에서-apache-spark를-사용하여-데이터-분석">Fabric에서 Apache Spark를 사용하여 데이터 분석</h2>
<p><a href="https://microsoftlearning.github.io/mslearn-fabric/Instructions/Labs/02-analyze-spark.html">https://microsoftlearning.github.io/mslearn-fabric/Instructions/Labs/02-analyze-spark.html</a></p>
<h3 id="데이터-읽어오기">데이터 읽어오기</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/1d1e286c-dd2a-46d0-90a1-68d5f6875306/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/070a559d-3689-416f-ba36-b6727eba7b96/image.png" alt=""></p>
<p>와일드카드를 이용하여 전체 데이터를 가져올 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/rudin_/post/1aa76132-b560-48b0-9563-7920481cc4e9/image.png" alt=""></p>
<h3 id="dataframe에서-데이터-탐색">DataFrame에서 데이터 탐색</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/51a199bb-dec3-4c22-a933-a3714a332d93/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/9085d33b-37a6-4b4f-b2cf-6f5c53021121/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/d24f88df-899d-4f76-bc4e-5f36a32abd90/image.png" alt=""></p>
<h3 id="dataframe에서-데이터-집계-및-그룹화">DataFrame에서 데이터 집계 및 그룹화</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/6d0fa693-6624-4757-ad02-168a97d7b4ab/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/fa609591-d2ca-4173-88d6-cfb2916474b8/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MicrosoftDataschool] 71일차 - 오늘의밥..]]></title>
            <link>https://velog.io/@rudin_/MicrosoftDataschool-71%EC%9D%BC%EC%B0%A8-%EC%98%A4%EB%8A%98%EC%9D%98%EB%B0%A5</link>
            <guid>https://velog.io/@rudin_/MicrosoftDataschool-71%EC%9D%BC%EC%B0%A8-%EC%98%A4%EB%8A%98%EC%9D%98%EB%B0%A5</guid>
            <pubDate>Fri, 17 Apr 2026 08:17:41 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/rudin_/post/9093d055-f210-474c-826c-3a765b8be14b/image.jpeg" alt=""></p>
<p>그럭저럭 맛있었다
저번보다는 반찬이 호불호 없는편</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MicrosoftDataSchool] 61~71일차 - 2차 팀 프로젝트 <AutoDetect>: Azure Databricks를 활용한 소스코드 취약점 자동탐지 및 분석 솔루션]]></title>
            <link>https://velog.io/@rudin_/MicrosoftDataSchool-61%EC%9D%BC%EC%B0%A8-2%EC%B0%A8-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-AutoDetect-Azure-Databricks%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%86%8C%EC%8A%A4%EC%BD%94%EB%93%9C-%EC%B7%A8%EC%95%BD%EC%A0%90-%EC%9E%90%EB%8F%99%ED%83%90%EC%A7%80-%EB%B0%8F-%EB%B6%84%EC%84%9D-%EC%86%94%EB%A3%A8%EC%85%98</link>
            <guid>https://velog.io/@rudin_/MicrosoftDataSchool-61%EC%9D%BC%EC%B0%A8-2%EC%B0%A8-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-AutoDetect-Azure-Databricks%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%9C-%EC%86%8C%EC%8A%A4%EC%BD%94%EB%93%9C-%EC%B7%A8%EC%95%BD%EC%A0%90-%EC%9E%90%EB%8F%99%ED%83%90%EC%A7%80-%EB%B0%8F-%EB%B6%84%EC%84%9D-%EC%86%94%EB%A3%A8%EC%85%98</guid>
            <pubDate>Fri, 17 Apr 2026 08:04:20 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/rudin_/post/456b6d98-2b07-4882-a801-164ce2917c96/image.png" alt=""></p>
<blockquote>
<p><a href="https://github.com/data-school-2nd-5th/AutoDetect">Github 링크</a></p>
</blockquote>
<h1 id="cwe-공식-문서에-기반한-js-코드-취약점-탐지-vs-extension">CWE 공식 문서에 기반한 js 코드 취약점 탐지 VS Extension</h1>
<h2 id="1-제안-배경">1. 제안 배경</h2>
<ul>
<li>LLM 및 Vibe Coding을 활용한 코드 단위 개발자 계층의 증대로 <strong>공격 벡터(Attack Vector) 증가</strong></li>
<li>개발 단계에서 발견되지 않은 취약점은 테스트 및 배포과정에서 더 큰 수정 비용과 운영 리스크로 직결</li>
<li>보안 지식이 부족한 전공자 혹은 비전공자 개발자를 위한 <strong>저비용 소스코드 취약점 분석 지침 가이드</strong> 필요</li>
<li>시중 상용 취약점 탐지 및 보수 솔루션의 한계<ul>
<li>개발자가 코딩 작업 중 즉시 활용 가능해야 함</li>
<li>탐지 자체가 아닌 유지 보수에 도움이 되는 설명 필요</li>
<li>공식 보안 기준에 따른 취약점 정보에 대한 설명 필요</li>
</ul>
</li>
</ul>
<h3 id="본-프로젝트의-제안-방향">본 프로젝트의 제안 방향</h3>
<ul>
<li>Hallucination 방지를 위한 비생성형 AI 기반 취약점 탐지와 RAG+LLM 기술을 활용한 설명 기능의 결합</li>
<li>AI 모델 신뢰성 확보를 위한 모델 의사결정 투명성 확보</li>
<li>Azure Cloud Resource 기반으로 확장성 및 범용성 확보</li>
</ul>
<h2 id="2-추진-필요성">2. 추진 필요성</h2>
<ul>
<li>취약점 발견 시점이 늦어짐에 따라 수정 범위 및 비용 증가로 인한 조기 발견 필요</li>
<li>보안 내재화(Security By Design): 보안의 사후 대응이 아닌 소프트웨어 설계 단계에서의 보안 고려<ul>
<li>소프트웨어 개발자 중심의 보안 내재화 필요</li>
</ul>
</li>
<li>설명 가능한 분석 체계<ul>
<li>단순 “취약점 탐지”에 그치지 않는 “모델의 의사결정 투명성”을 보장하는 XAI 기술 도입</li>
<li>실제 현장에서 필요한 정보는 “취약점이 있음”이 아닌 “코드의 어떤 부분이 취약점인지”가 필요</li>
<li>또한, 취약점 분석 시 Exploit 위험도 우선순위에 따른 수정 절차 필요</li>
</ul>
</li>
</ul>
<h2 id="3-제안-개요">3. 제안 개요</h2>
<h3 id="1-목적">(1) 목적</h3>
<ul>
<li>소스코드 내 보안 취약점을 자동으로 탐지</li>
<li>해당 소스코드에서 발견된 취약점에 대한 MITRE 공식 문서 기반의 설명 제공</li>
</ul>
<h3 id="2-구성">(2) 구성</h3>
<ul>
<li>1단계: 비생성형 ML 모델을 활용한 취약점 탐지</li>
<li>2단계: 발견된 취약점 설명 및 대응 가이드</li>
<li>3단계: 개발 환경 연계 (VSCode Extension)</li>
</ul>
<h2 id="4-기대-효과">4. 기대 효과</h2>
<h3 id="1-보안-리스크-사전-예방">(1) 보안 리스크 사전 예방</h3>
<ul>
<li>개발 초기 단계에서 취약점 식별 가능</li>
<li>CI/CD 절차의 이전 단계에서 발생할 수 있는 보안 대책 강구 가능</li>
<li>조직 차원의 리스크 관리 강화 효과 기대</li>
</ul>
<h3 id="2-수정-비용-절감">(2) 수정 비용 절감</h3>
<ul>
<li>취약점의 조기 발견을 통한 분석·수정·재검증 비용의 절감 가능</li>
<li>보안에 대한 사전 지식이 부족한 사람에게도 공식 문서 기반의 취약점 정보를 제공해 탐색 시간 단축</li>
</ul>
<h3 id="3-개발-생산성-증대">(3) 개발 생산성 증대</h3>
<ul>
<li>현재 개발 중인 코드에 적합한 CVE, CWE에 대한 자료를 탐색하지 않아도 취약점에 대한 지표 제공</li>
<li>코드 리뷰 및 보안 검토 효율 향상</li>
<li>반복 발생 취약점에 대한 대응 표준화 가능</li>
</ul>
<h3 id="4-보안-내재화를-통한-시스템-안전성-향상">(4) 보안 내재화를 통한 시스템 안전성 향상</h3>
<ul>
<li>시스템의 보안 수준을 특정 인력에게 편중시키지 않도록 유도</li>
<li>개발 조직 전체의 보안 인식 제고</li>
<li>장기적인 시스템 보안 유지 기반 마련</li>
</ul>
<h2 id="5-차별성-및-도입-타당성">5. 차별성 및 도입 타당성</h2>
<h3 id="1-시장-조사">(1) 시장 조사</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/30b5a022-cc44-4d1b-a0a2-53489d770a24/image.png" alt=""></p>
<h3 id="2-차별점">(2) 차별점</h3>
<ul>
<li>XAI 기반 탐지 근거 시각화</li>
<li>Azure-based End to End Framework</li>
<li>공식 취약점 문서 기반 RAG 시스템 구축</li>
<li>경량 모델 + 실시간 IDE UX를 통한 개발 도중 즉시 피드백 가능</li>
<li>사용자 피드백 반영을 통한 데이터 재학습 및 정책 최적화까지 확장할 수 있음</li>
</ul>
<h2 id="6-리스크-및-대응-방향">6. 리스크 및 대응 방향</h2>
<h3 id="1-예상-리스크">(1) 예상 리스크</h3>
<ul>
<li>초기 단계에서 모든 언어 및 모든 취약점 유형을 포괄하기 어려움</li>
<li>데이터셋의 범위와 분포의 한계 존재</li>
<li>탐지 모델과 설명 모델의 적용 범위 차이 존재</li>
</ul>
<h3 id="2-대응-방향">(2) 대응 방향</h3>
<ul>
<li>초기는 Vibe Coding에서 특히 많이 사용되는 언어(JavaScript)와 우선순위 높은 취약점으로 한정</li>
<li>취약점 범위 : <a href="https://owasp.org/Top10/2025/A01_2025-Broken_Access_Control/">https://owasp.org/Top10/2025/A01_2025-Broken_Access_Control/</a></li>
<li>한정된 범위 내의 적정선의 성능 확보 후 점진적으로 확대</li>
<li>생성형 AI 단독 판단이 아닌 공식 문서 기반 결합으로 생성형 기반 타 솔루션과 다른 신뢰도 보완</li>
<li>최종 판단은 개발자 및 보안 담당자 검토로 진행 (보조 기구로서의 의의)</li>
<li>사용자 피드백 및 최신 취약점 문서 반영을 통한 지속적인 개선</li>
</ul>
<h3 id="기본-관점">기본 관점</h3>
<ul>
<li>본 솔루션은 완전 자동화 및 판단 도구가 아님</li>
<li>개발자와 보안 담당자의 판단을 빠르고 일관되게 지원하는 보조 지표로서의 의의를 가짐</li>
</ul>
<h2 id="8-차후-발전-방향">8. 차후 발전 방향</h2>
<ul>
<li>취약점 탐지 모델 정확도 향상</li>
<li>취약점 탐지 모델의 Handling Scope 확장<ul>
<li>다언어 지원</li>
<li>학습 데이터베이스 수집 및 구축을 통한 최신 취약점 반영</li>
</ul>
</li>
<li>RAG를 활용한 LLM Hallucination 최소화 및 취약점 데이터베이스 기반 수정 방안 제안 기능 추가</li>
<li>서비스 별 반복 발생 취약점 분석 대시보드 제공</li>
<li>최신 OWASP 상위 10개의 취약점에 기반한 우선순위 자동 정렬화</li>
</ul>
<h2 id="9-결론">9. 결론</h2>
<ul>
<li>개발 과정에서 보안리스크를 조기에 식별 가능</li>
<li>장기적인 보안 사고 대응 역량 강화를 위해 필요한 보안 지원 도구 및 체계의 구축</li>
<li>핵심 구성<ul>
<li>경량화된 비생성형 AI 모델을 활용한 취약점 탐지</li>
<li>설명 가능한 인공지능(XAI) 기술 도입을 통한 모델 투명성 확보</li>
<li>공식 취약점 문서 기반 분석 체계 수립</li>
<li>사용률이 높은 IDE인 VSCode Extension을 통한 UX</li>
</ul>
</li>
<li>기대 효과<ul>
<li>시스템 보안 내재화</li>
<li>개발 생산성 향상</li>
<li>운영 리스크 감소</li>
</ul>
</li>
<li>종합<ul>
<li>개발 보안 수준 향상, 업무 효율 개선을 동시에 기대할 수 있는 전략적 솔루션</li>
</ul>
</li>
</ul>
<hr>

<h2 id="시스템-아키텍처">시스템 아키텍처</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/e9610f29-9432-4f0d-87a6-2fec29206708/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/c4ac2b01-c80d-482b-ae2e-092e2125cc5e/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/36cb952d-afe3-47aa-bfad-32878216c0cc/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/b4e69171-ef16-463b-b450-e9ad1a17a768/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/d42492c8-aae6-4097-ba23-68ee5b1ce9fb/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/36c0c4fc-96d8-41dc-a2b9-39b95bb541f4/image.png" alt=""></p>
<hr>

<h2 id="시연-영상">시연 영상</h2>
<p><a href="https://canva.link/txux14cq2bj3wzf">시연 영상</a></p>
<hr>

<h2 id="트러블슈팅">트러블슈팅</h2>
<h3 id="azure-function-시작-실패-modulenotfounderror-requests-2026-04-06">Azure Function 시작 실패 (ModuleNotFoundError: requests) (2026-04-06)</h3>
<p>Problem: 배포 후 Function Host 기동 실패
Cause: FlexConsumption 배포 경로에서 remote build 미적용으로 런타임 의존성 미설치
Solution: GitHub Actions에 enable-oryx-build=true, remote-build=true 적용 + runtime/databricks 의존성 분리</p>
<h3 id="databricks-run-now-400-실패-2026-04-06">Databricks run-now 400 실패 (2026-04-06)</h3>
<p>Problem: Function에서 Databricks job 제출 자체 실패
Cause: Job이 job_parameters 계약인데 코드가 notebook_params 사용
Solution: 호출 payload를 job_parameters로 전환</p>
<h3 id="databricks-태스크-실패-source_xml_path-is-required-2026-04-0607">Databricks 태스크 실패 (source_xml_path is required) (2026-04-06~07)</h3>
<p>Problem: Function 응답은 성공인데 Databricks task는 내부 실패
Cause: Notebook widget/파라미터 바인딩 누락, base_parameters 미정의
Solution: notebook wrapper에서 파라미터 바인딩 고정 + 전달 경로 정규화</p>
<h3 id="sas-url-읽기-실패-binaryfile-진입-오류-2026-04-07">SAS URL 읽기 실패 (binaryFile 진입 오류) (2026-04-07)</h3>
<p>Problem: HTTP(S) SAS URL인데 Spark binaryFile 경로로 처리되어 AnalysisException
Cause: 경로 문자열 공백/quote 등으로 분기 미스
Solution: source_xml_path 정규화 + URL scheme 판별 강화 + HTTP(S)는 직접 읽기 강제</p>
<h3 id="delta-적재-0건간헐-실패-2026-04-0708">Delta 적재 0건/간헐 실패 (2026-04-07~08)</h3>
<p>Problem: 실행 완료처럼 보이나 테이블 row count가 0 또는 No module named &#39;azure&#39; 재발
Cause: XML namespace 미처리 + package import side-effect가 azure SDK 의존성 유입
Solution: parser namespace 처리/빈 결과 가드 추가 + service/shared lazy import 구조로 전환</p>
<h3 id="ucexternal-location-생성-불가-2026-04-06">UC/External Location 생성 불가 (2026-04-06)</h3>
<p>Problem: Catalog에서 External data 메뉴 미노출 및 External Location 생성 실패
Cause: Account 권한·metastore 할당·HNS·CREATE EXTERNAL LOCATION 권한 부족
Solution: HNS 활성 스토리지로 전환 + Access Connector/Metastore 권한 보강</p>
<h3 id="gcs→adf-파라미터-공백으로-파이프라인-오동작-2026-04-09">GCS→ADF 파라미터 공백으로 파이프라인 오동작 (2026-04-09)</h3>
<p>Problem: bucket_name/object_path가 빈 값으로 전달되어 If Condition/경로 처리 실패
Cause: Form 전송(application/x-www-form-urlencoded) 또는 {&quot;parameters&quot;:{...}} 중첩 payload 사용
Solution: application/json + 최상위 키(bucket_name, object_path, object_generation)로 고정</p>
<h3 id="eventarccloud-function-배포-권한-오류-2026-04-09">Eventarc/Cloud Function 배포 권한 오류 (2026-04-09)</h3>
<p>Problem: Eventarc 관련 permission denied, SA not found로 배포 차단
Cause: 이벤트 수신/Invoker/PubSub 퍼블리셔 권한 및 트리거 SA 설정 불일치
Solution: 역할 재부여 + 전용 SA 재지정 + 버킷 리전 맞춘 재배포</p>
<h3 id="databricks-path_not_found-파일명경로-변환-이슈-2026-04-09">Databricks PATH_NOT_FOUND (파일명/경로 변환 이슈) (2026-04-09)</h3>
<p>Problem: ADLS 저장 경로와 notebook 읽기 경로 불일치
Cause: ADF Dataset/Copy에서 디렉토리·파일명 매핑 분리 미흡
Solution: p_dir/p_file 분리 매핑으로 raw/&lt;원본디렉토리&gt;/&lt;원본파일명&gt; 규칙 고정</p>
<h3 id="ml-대시보드-null이상치-문제-2026-04-14">ML 대시보드 NULL/이상치 문제 (2026-04-14)</h3>
<p>Problem: training_data_count, f1/r2/rmse 등 지표가 NULL 또는 비정상값
Cause: metric key 매핑 협소, source 탐색 제한, Best Run/Trend 집계식 취약
Solution: params/tags/run_metrics_history fallback 확장 + key 탐지 규칙 강화 + Best Run 정렬식(R2 DESC, RMSE ASC) 보정</p>
<h3 id="운영-장애-감지-공백-2026-04-13">운영 장애 감지 공백 (2026-04-13)</h3>
<p>Problem: 실패 발생 시 통합 알림 체계 부재
Cause: 플랫폼별 실패 이벤트가 분산되어 즉시 감지 어려움
Solution: Logic App Webhook 기반 공통 알림 훅 표준화 + 테스트 추가 + GCP/ADF/Databricks 운영 가이드 정리</p>
<hr>

<h2 id="내가-담당한-부분">내가 담당한 부분</h2>
<ul>
<li>Azure Databricks Account 관리 및 대시보드 구현</li>
<li>Azure Factory 구성, GCP Bucket, Cloud Function 생성 및 연결</li>
<li>Azure Function-Blob(ADLS)-AAC-ADB 데이터 파이프라인 구현</li>
<li>Logic App으로 데이터 파이프라인 실패 시 이메일 알림기능 구현</li>
</ul>
<h2 id="추가로-배운-부분azure-비용적-관점">추가로 배운 부분(Azure 비용적 관점)</h2>
<ul>
<li>databricks 컴퓨팅 생성하면 azure 파생 그룹이 발생 → 클라우드 관리자에게 요청하여 해당 그룹으로 포함 요청하여 가시성을 얻어야 토탈 요금 확인 가능(기존 리소스그룹 요금 + databricks전용 리소스그룹 요금)</li>
<li>작업 시작 전 항상 git pull --rebase origin main, 푸시 직전 다시 git fetch 후 필요 시 git pull --rebase origin main, main에서는 merge commit 안 만들고 rebase/fast-forward만 사용하여 브랜치 히스토리를 깔끔하게</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MicrosoftDataSchool] 60일차 - AzureDataFactory 필터, Until, Join]]></title>
            <link>https://velog.io/@rudin_/MicrosoftDataSchool-60%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@rudin_/MicrosoftDataSchool-60%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Wed, 01 Apr 2026 03:42:21 GMT</pubDate>
            <description><![CDATA[<h1 id="azure-data-factory-필터-활동-filter-activity">[Azure Data Factory] 필터 활동 (Filter Activity)</h1>
<h2 id="1-개요">1. 개요</h2>
<p>필터 활동은 필터 변환과 달리, 파이프라인 내에서 배열(Array) 데이터(예: Lookup, Get Metadata, Web Activity 등에서 반환된 값)를 조건에 따라 필터링하는 활동(Activity)입니다.</p>
<ul>
<li><strong>사용 위치</strong>: 파이프라인(컨트롤 플로우)에 배치하여 실행</li>
<li><strong>적용 대상</strong>: 일반적으로 JSON 배열, 객체 배열 등 구조화된 리스트 데이터</li>
<li><strong>주요 예시</strong>:<ul>
<li>Get Metadata로 파일 목록을 받아온 후, 특정 확장자만 필터링</li>
<li>Lookup으로 여러 레코드의 배열을 가져온 후, 특정 조건에 맞는 레코드만 추출</li>
</ul>
</li>
</ul>
<h3 id="필터-활동-vs-필터-변환-비교"><strong>필터 활동 vs 필터 변환 비교</strong></h3>
<table>
<thead>
<tr>
<th align="left">구분</th>
<th align="left">필터 활동</th>
<th align="left">필터 변환</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>적용 위치</strong></td>
<td align="left">파이프라인(Activity)</td>
<td align="left">매핑 데이터 플로우(Transformation)</td>
</tr>
<tr>
<td align="left"><strong>적용 대상</strong></td>
<td align="left">배열 데이터(Array, JSON 등)</td>
<td align="left">테이블 데이터(행/컬럼 기반)</td>
</tr>
<tr>
<td align="left"><strong>조건 작성 방식</strong></td>
<td align="left">파이프라인 식(표현식)</td>
<td align="left">데이터 플로우 내 식(식 편집기)</td>
</tr>
<tr>
<td align="left"><strong>용도</strong></td>
<td align="left">리스트, 메타데이터 등 구조화된 배열</td>
<td align="left">레코드(행) 기반 데이터 처리</td>
</tr>
<tr>
<td align="left"><strong>주요 사용 예시</strong></td>
<td align="left">파일/테이블/객체 리스트 조건 분기</td>
<td align="left">데이터 필터링(컬럼 조건에 따라 행 추출)</td>
</tr>
</tbody></table>
<hr>
<h2 id="2-실습-준비">2. 실습 준비</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/2a89bd78-6625-4e08-9c5e-84ca7141f973/image.png" alt=""></p>
<h3 id="2-1-입력-컨테이너-및-파일-준비"><strong>2-1. 입력 컨테이너 및 파일 준비</strong></h3>
<ul>
<li><strong>Storage Account</strong>: <code>a000storagedemo</code></li>
<li><strong>Container</strong>: <code>baseball-hitter</code></li>
<li><strong>준비 파일 목록</strong>:<ul>
<li><code>2000_2001_hitter.csv</code></li>
<li><code>2000_2001_hitter.xlsx</code></li>
<li><code>2002_2013_hitter.csv</code></li>
<li><code>2002_2013_hitter.xlsx</code></li>
<li><code>2014_hitter.csv</code></li>
<li><code>2014_hitter.xlsx</code></li>
</ul>
</li>
</ul>
<h3 id="2-2-링크드-서비스-확인"><strong>2-2. 링크드 서비스 확인</strong></h3>
<ul>
<li><strong>이름</strong>: <code>BlobStorage1</code></li>
<li><strong>형식</strong>: Azure Blob Storage</li>
<li><strong>통합 런타임</strong>: <code>AutoResolveIntegrationRuntime</code></li>
</ul>
<hr>
<h2 id="3-메인-파이프라인-구성-filtercsvfiles_pl">3. 메인 파이프라인 구성 (FilterCsvFiles_PL)</h2>
<h3 id="3-1-get-metadata-활동-get-metadata---list-files"><strong>3-1. Get Metadata 활동 (<code>Get Metadata - List Files</code>)</strong></h3>
<ul>
<li><strong>데이터 세트</strong>: <code>baseballinput_DS</code> (baseball-hitter 컨테이너 연결)</li>
<li><strong>필드 목록</strong>:<ul>
<li><code>exists</code></li>
<li><code>childItems</code></li>
</ul>
</li>
<li><strong>실행 결과(출력 예시)</strong>:<pre><code class="language-json">{
    &quot;exists&quot;: true,
    &quot;itemName&quot;: &quot;baseball-hitter&quot;,
    &quot;itemType&quot;: &quot;Folder&quot;,
    &quot;childItems&quot;: [
        { &quot;name&quot;: &quot;2000_2001_hitter.csv&quot;, &quot;type&quot;: &quot;File&quot; },
        { &quot;name&quot;: &quot;2000_2001_hitter.xlsx&quot;, &quot;type&quot;: &quot;File&quot; },
        ...
    ]
}</code></pre>
</li>
</ul>
<h3 id="3-2-if-condition-활동-if-exist"><strong>3-2. If Condition 활동 (<code>If Exist</code>)</strong></h3>
<ul>
<li><strong>식(Expression)</strong>:<pre><code>@activity(&#39;Get Metadata - List Files&#39;).output.exists</code></pre></li>
<li><strong>True 작업</strong>: 내부에 필터 및 후속 활동 배치</li>
</ul>
<h3 id="중요-설계-변경-사유"><strong>[중요] 설계 변경 사유</strong></h3>
<ul>
<li><strong>제약 사항</strong>: 중첩된 ForEach 작업은 지원되지 않으며, <strong>ForEach 작업을 If Condition activity 범위 내에서 사용할 수 없습니다.</strong></li>
<li><strong>해결 방법</strong>: If Condition 내에서 <strong>Execute Pipeline</strong> 활동을 사용하여 자식 파이프라인을 호출하는 방식으로 구성합니다.</li>
</ul>
<hr>
<h2 id="4-자식-파이프라인-구성-filterforeachcopy_pl">4. 자식 파이프라인 구성 (FilterForEachCopy_PL)</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/c63075a3-f8fa-4265-8fb5-a15095caee41/image.png" alt=""></p>
<h3 id="4-1-매개변수-설정"><strong>4-1. 매개변수 설정</strong></h3>
<ul>
<li><strong>이름</strong>: <code>fileListToProcess</code></li>
<li><strong>형식</strong>: <code>Array</code></li>
</ul>
<h3 id="4-2-필터-활동-filter-csv"><strong>4-2. 필터 활동 (<code>Filter CSV</code>)</strong></h3>
<ul>
<li><strong>항목(Items)</strong>:<pre><code>@pipeline().parameters.fileListToProcess</code></pre></li>
<li><strong>조건(Condition)</strong>:<pre><code>@and(
    equals(item().type, &#39;File&#39;),
    endswith(item().name, &#39;.csv&#39;)
)</code></pre></li>
</ul>
<h3 id="4-3-foreach-활동-foreach-files"><strong>4-3. ForEach 활동 (<code>ForEach Files</code>)</strong></h3>
<ul>
<li><strong>항목(Items)</strong>:<pre><code>@activity(&#39;Filter CSV&#39;).output.value</code></pre></li>
</ul>
<p><img src="https://velog.velcdn.com/images/rudin_/post/e854dc1b-364f-4455-9c74-28537a4dc906/image.png" alt=""></p>
<h3 id="4-4-복사-활동-copy-files"><strong>4-4. 복사 활동 (<code>Copy Files</code>)</strong></h3>
<ul>
<li><strong>원본 데이터 세트</strong>: <code>baseballCopyInput_DS</code><ul>
<li>매개변수 <code>fileName</code> 사용: <code>@dataset().fileName</code></li>
<li>값 매핑: <code>@item().name</code></li>
</ul>
</li>
<li><strong>싱크 데이터 세트</strong>: <code>baseballCopyOutput_DS</code><ul>
<li>경로: <code>baseball-hitter/csv</code></li>
<li>매개변수 <code>fileName</code> 사용: <code>@dataset().fileName</code></li>
<li>값 매핑: <code>@item().name</code></li>
</ul>
</li>
</ul>
<hr>
<h2 id="5-메인-파이프라인-수정-및-실행">5. 메인 파이프라인 수정 및 실행,</h2>
<h3 id="5-1-execute-pipeline-활동-추가"><strong>5-1. Execute Pipeline 활동 추가</strong></h3>
<ul>
<li>If Condition의 <strong>True</strong> 섹션에 <code>Execute Pipeline</code> 활동을 추가합니다.</li>
<li><strong>호출된 파이프라인</strong>: <code>FilterForEachCopy_PL</code></li>
<li><strong>매개변수(<code>fileListToProcess</code>) 전달</strong>:<pre><code>@activity(&#39;Get Metadata - List Files&#39;).output.childItems</code></pre></li>
</ul>
<h3 id="5-2-실행-결과-확인"><strong>5-2. 실행 결과 확인</strong></h3>
<ol>
<li><strong>Get Metadata</strong>: 성공 (6개 항목 조회)</li>
<li><strong>If Exist</strong>: 성공 (True 분기)</li>
<li><strong>Execute Pipeline</strong>: 성공 (자식 파이프라인 호출)<ul>
<li>입력 데이터 확인: <code>.csv</code>와 <code>.xlsx</code> 파일이 모두 포함된 배열 전달</li>
</ul>
</li>
<li><strong>자식 파이프라인 내 Filter</strong>: 성공 (6개 중 <code>.csv</code> 파일 3개만 필터링)</li>
</ol>
<h3 id="5-3-최종-스토리지-확인"><strong>5-3. 최종 스토리지 확인</strong>,</h3>
<ul>
<li><code>baseball-hitter/csv</code> 폴더 내에 필터링된 파일들이 정상 복사되었는지 확인합니다:<ul>
<li><code>2000_2001_hitter.csv</code></li>
<li><code>2002_2013_hitter.csv</code></li>
<li><code>2014_hitter.csv</code></li>
</ul>
</li>
</ul>
<hr>
<p><strong>마무리</strong>: Filter 활동을 통해 파이프라인 흐름 제어 단계에서 배열 데이터를 정교하게 제어할 수 있으며, If Condition과 결합 시 자식 파이프라인 호출 방식을 활용해야 함을 유의하시기 바랍니다.</p>
<hr>
<hr>
<h1 id="azure-data-factory-파생열-및-조건부-분할-변환-derived-column--conditional-split">[Azure Data Factory] 파생열 및 조건부 분할 변환 (Derived Column &amp; Conditional Split)</h1>
<h2 id="1-개요-1">1. 개요</h2>
<h3 id="파생열-변환-derived-column-transformation"><strong>파생열 변환 (Derived Column Transformation)</strong></h3>
<p>파생열 변환은 기존 데이터 컬럼을 가공하거나 새로운 컬럼을 추가할 때 사용하는 변환 단계입니다. 입력된 데이터의 컬럼 값을 수식/표현식으로 가공하여 파생 컬럼을 생성하거나 기존 컬럼 값을 대체할 수 있습니다.</p>
<ul>
<li><strong>구성 요소</strong>:<ul>
<li><strong>파생 컬럼(Derived Column)</strong>: 새로운 컬럼 추가 또는 기존 컬럼 값 대체</li>
<li><strong>표현식(Expression)</strong>: 문자열, 수치, 날짜, 논리 연산 등 다양한 함수/연산자 지원</li>
<li><strong>미리보기(Data Preview)</strong>: 변환 결과를 즉시 확인할 수 있는 기능</li>
</ul>
</li>
<li><strong>장점</strong>:<ul>
<li>기존 문자열 컬럼에서 특정 패턴 추출하여 파생 정보 제공</li>
<li>데이터 전처리 및 가공의 자동화</li>
<li>별도의 데이터 소스 수정 없이 컬럼 가공 가능</li>
<li>다양한 비즈니스 로직 반영 가능</li>
</ul>
</li>
<li><strong>사용 시나리오</strong>:<ul>
<li>성적 데이터에서 점수에 따라 &quot;합격/불합격&quot; 여부 컬럼 추가</li>
<li>주문 데이터에서 단가 × 수량으로 총액(Total) 컬럼 추가</li>
<li>날짜 데이터를 가공하여 연도, 월 등 새로운 컬럼 추출</li>
</ul>
</li>
</ul>
<h3 id="조건부-분할-변환-conditional-split-transformation"><strong>조건부 분할 변환 (Conditional Split Transformation)</strong></h3>
<p>조건부 분할 변환은 입력 데이터 행(Row)을 지정한 조건(수식/표현식)에 따라 여러 그룹(분기)으로 나누어 주는 변환 단계입니다.</p>
<ul>
<li><strong>구성 요소</strong>:<ul>
<li><strong>분기 조건(Condition)</strong>: 분리 기준이 되는 조건을 작성하며, 하나의 데이터 행에 대해 첫 번째로 참이 되는 조건에 따라 분기함</li>
<li><strong>기본 분기(Default Output)</strong>: 모든 조건을 만족하지 않을 때 데이터를 분리할 기본 분기 지정</li>
<li><strong>미리보기(Data Preview)</strong>: 분할 결과를 즉시 확인할 수 있는 기능</li>
</ul>
</li>
<li><strong>장점</strong>:<ul>
<li>조건별로 데이터 흐름을 분리하여 후처리 용이</li>
<li>복잡한 분기 로직을 시각적으로 설계 가능</li>
<li>다양한 조건별 분석 및 후속 처리 지원</li>
</ul>
</li>
<li><strong>사용 시나리오</strong>:<ul>
<li>점수 90점 이상/80점 이상/기타 등급별로 데이터 분할</li>
<li>주문 상태(배송완료/배송중/취소 등)에 따라 데이터 분할</li>
<li>거래 금액이 임계값 이상/미만인 고객 분리</li>
</ul>
</li>
</ul>
<hr>
<h2 id="2-실습-준비-1">2. 실습 준비</h2>
<h3 id="2-1-실습-데이터-employeecsv"><strong>2-1. 실습 데이터 (<code>employee.csv</code>)</strong></h3>
<pre><code class="language-text">ID, Name, Salary, Address, Location, Email
1, 김철수, 2750000, 서울 강남구, Korea, chulsu.kim@example1.com
2, 이영희,, Irvine CA, US, younghee.lee@example3.com
3, 박민준, 3820000, 인천 연수구, Korea, minjun.park@example1.com
4, 최지영, 4500000, 부산 해운대구, Korea, jiyoung.choi@example2.com
5, 정윤화, 2810000, 대구 수성구, Korea, yoonhwa.chung@example2.com
6, 강서준, 3540000,, China, seojun.kang@example2.com
7, 윤아영,, 제주 제주시, Korea, ayoung.yoon@example3.com</code></pre>
<h3 id="2-2-컨테이너-및-데이터-세트-준비"><strong>2-2. 컨테이너 및 데이터 세트 준비</strong></h3>
<ol>
<li><strong>Storage</strong>: <code>a000storagedemo2</code> 내 <code>employee</code> 컨테이너 생성 및 <code>employee.csv</code> 업로드.</li>
<li><strong>데이터 흐름 디버그 켜기</strong>: Small 컴퓨팅, 1시간 TTL 설정.</li>
<li><strong>소스 데이터 세트 (<code>sourceCsv_DS</code>)</strong>: <code>employee.csv</code> 연결, 첫 번째 행을 머리글로 설정.</li>
</ol>
<hr>
<h2 id="3-실습-1-데이터-정제-및-파생열-생성">3. [실습 1] 데이터 정제 및 파생열 생성</h2>
<h3 id="3-1-주소-누락값-처리-missingvalueaddress"><strong>3-1. 주소 누락값 처리 (<code>missingValueAddress</code>)</strong></h3>
<ul>
<li><strong>열</strong>: <code>Address</code></li>
<li><strong>식</strong>:<pre><code>iif(isNull(Address), &#39;unknown&#39;, Address)</code></pre></li>
</ul>
<h3 id="3-2-급여-누락값-처리-missingvaluesalary"><strong>3-2. 급여 누락값 처리 (<code>missingValueSalary</code>)</strong></h3>
<ul>
<li><strong>열</strong>: <code>Salary</code></li>
<li><strong>식</strong>:<pre><code>iif(isNull(Salary), &#39;2500000&#39;, Salary)</code></pre></li>
</ul>
<h3 id="3-3-국가-컬럼-대문자-변환-upperlocationtocountry"><strong>3-3. 국가 컬럼 대문자 변환 (<code>upperLocationToCountry</code>)</strong></h3>
<ul>
<li><strong>열</strong>: <code>Country</code> (새로 만들기)</li>
<li><strong>식</strong>:<pre><code>upper(Location)</code></pre></li>
</ul>
<h3 id="3-4-싱크-설정-및-파이프라인-실행"><strong>3-4. 싱크 설정 및 파이프라인 실행</strong></h3>
<ul>
<li><strong>싱크(<code>sink1</code>)</strong>: <code>employee_processed.csv</code>로 단일 파일 출력.</li>
<li><strong>파이프라인</strong>: <code>employee1_PL</code> 생성 후 데이터 흐름 실행.</li>
<li><strong>결과</strong>: <code>employee_processed.csv</code> 생성 완료.</li>
</ul>
<hr>
<h2 id="4-실습-2-복합-파생열-및-조건부-분할-advanced">4. [실습 2] 복합 파생열 및 조건부 분할 (Advanced)</h2>
<p>기존 파이프라인에 추가 실습 구성을 연결합니다.</p>
<h3 id="4-1-급여-등급-컬럼-생성-addsalarygrade"><strong>4-1. 급여 등급 컬럼 생성 (<code>addSalaryGrade</code>)</strong></h3>
<ul>
<li><strong>열</strong>: <code>SalaryGrade</code></li>
<li><strong>식</strong>:<pre><code>iif(toInteger(Salary) &lt;= 3000000, &#39;Low&#39;,
iif(toInteger(Salary) &lt;= 4000000, &#39;Mid&#39;, &#39;High&#39;))</code></pre></li>
</ul>
<h3 id="4-2-이메일-도메인-추출-addemaildomain"><strong>4-2. 이메일 도메인 추출 (<code>addEmailDomain</code>)</strong></h3>
<ul>
<li><strong>열</strong>: <code>EmailDomain</code></li>
<li><strong>식</strong>:<pre><code>split(Email,&#39;@&#39;)</code></pre></li>
</ul>
<h3 id="4-3-조건부-분할-splitbylocation"><strong>4-3. 조건부 분할 (<code>SplitByLocation</code>)</strong></h3>
<ul>
<li><strong>스트림 1 (<code>headquarters</code>)</strong>:<ul>
<li>조건: <code>Location == &#39;Korea&#39;</code></li>
</ul>
</li>
<li><strong>스트림 2 (<code>Branch</code>)</strong>:<ul>
<li>조건: (기본 분기 - 조건을 충족하지 않는 행)</li>
</ul>
</li>
</ul>
<h3 id="4-4-다중-싱크-설정"><strong>4-4. 다중 싱크 설정</strong></h3>
<ol>
<li><strong>본사 싱크 (<code>headquaterSink</code>)</strong>:<ul>
<li>파일 이름: <code>headquarters-employee.csv</code></li>
<li>내용: 한국(Korea) 근무자 데이터 5건.</li>
</ul>
</li>
<li><strong>지사 싱크 (<code>branchSink</code>)</strong>:<ul>
<li>파일 이름: <code>branch-employee.csv</code></li>
<li>내용: 해외(US, China) 근무자 데이터 2건.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="5-최종-결과-확인">5. 최종 결과 확인</h2>
<p><strong>headquarters-employee.csv 예시</strong>:</p>
<pre><code class="language-text">ID, Name, Salary, Address, Location, Email, Country, SalaryGrade, EmailDomain
1, 김철수, 2750000, 서울 강남구, Korea, chulsu.kim@example1.com, KOREA, Low, example1.com
3, 박민준, 3820000, 인천 연수구, Korea, minjun.park@example1.com, KOREA, Mid, example1.com
4, 최지영, 4500000, 부산 해운대구, Korea, jiyoung.choi@example2.com, KOREA, High, example2.com
...</code></pre>
<p><strong>branch-employee.csv 예시</strong>:</p>
<pre><code class="language-text">ID, Name, Salary, Address, Location, Email, Country, SalaryGrade, EmailDomain
2, 이영희, 2500000, Irvine CA, US, younghee.lee@example3.com, US, Low, example3.com
6, 강서준, 3540000, unknown, China, seojun.kang@example2.com, CHINA, Mid, example2.com</code></pre>
<hr>
<h2 id="6-실습-마무리">6. 실습 마무리</h2>
<p>실습이 완료된 후에는 불필요한 비용이 발생하지 않도록 <strong>데이터 흐름 디버그 모드</strong>를 반드시 중지하고 게시(Publish)를 확인합니다.</p>
<hr>
<hr>
<h1 id="azure-data-factory-until-활동-및-set-variable-활동">[Azure Data Factory] Until 활동 및 Set Variable 활동</h1>
<h2 id="1-개요-2">1. 개요</h2>
<h3 id="until-활동-until-activity"><strong>Until 활동 (Until Activity)</strong></h3>
<p>Until 활동은 지정한 조건이 만족될 때까지 내부에 정의한 액티비티를 반복 실행하는 파이프라인 제어 단계입니다.</p>
<ul>
<li><strong>구성 요소</strong>:<ul>
<li><strong>반복 조건(Expression)</strong>: 반복을 종료할 시점을 결정하는 논리식 (예: <code>@equals(variables(&#39;fileFound&#39;), true)</code>)</li>
<li><strong>내부 액티비티(Activities)</strong>: 반복 루프 내에서 매회 수행할 액티비티 (예: Get Metadata → If Condition → Copy Data 등)</li>
<li><strong>대기 간격(Timeout/Interval)</strong>: 두 반복 사이에 대기할 시간(초 단위)을 설정하여 과도한 호출 방지</li>
</ul>
</li>
<li><strong>장점</strong>:<ul>
<li>알려지지 않은 반복 횟수를 처리할 수 있어, 파일 도착·상태 변경 등 비동기 이벤트 대기 시 유용</li>
<li>복잡한 루프 로직(분기, 에러 처리)을 시각적으로 설계 가능</li>
<li>반복 중간에 변수 업데이트나 외부 서비스 호출을 결합하여 동적 파이프라인 구현 지원</li>
</ul>
</li>
<li><strong>사용 시나리오</strong>:<ul>
<li>외부 시스템 파일 도착 여부를 확인하여, 파일이 준비될 때까지 반복 폴링</li>
<li>메타데이터 기준으로 데이터 누적이 완료될 때까지 복사/병합 작업 반복</li>
<li>API 호출 응답 상태가 원하는 결과가 나올 때까지 재시도</li>
</ul>
</li>
</ul>
<h3 id="set-variable-활동-set-variable-activity"><strong>Set Variable 활동 (Set Variable Activity)</strong></h3>
<p>Set Variable 활동은 파이프라인 변수(Pipeline Variable)의 값을 동적으로 변경하는 액티비티입니다.</p>
<ul>
<li><strong>구성 요소</strong>:<ul>
<li><strong>변수 이름(Variable Name)</strong>: 미리 선언된 파이프라인 변수 중 업데이트할 변수 선택</li>
<li><strong>값 표현식(Value/Expression)</strong>: 고정 값 또는 동적 콘텐츠 (예: <code>@item().name</code>, <code>@add(variables(&#39;count&#39;),1)</code>)</li>
<li><strong>데이터 형식(Type)</strong>: String, Bool, Array 등 변수 선언 시 지정된 형식</li>
</ul>
</li>
</ul>
<hr>
<h2 id="2-실습-준비-2">2. 실습 준비</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/fcf63b19-5fba-46b6-8a3c-409ae0ce1bae/image.png" alt=""></p>
<h3 id="2-1-실습-데이터-employee_batchcsv"><strong>2-1. 실습 데이터 (<code>employee_batch.csv</code>)</strong></h3>
<pre><code class="language-text">ID, Name, Department, Salary, Location
1, 오준호, IT, 69190, Gwangju
2, 최예린, Finance, 61538, Daegu
3, 강다은, Finance, 55729, Seoul
4, 정우성, Marketing, 46409, Gwangju
5, 오준호, Marketing, 57249, Seoul
6, 서지아, HR, 45784, Incheon
7, 박지훈, HR, 53096, Seoul
8, 오준호, Marketing, 52560, Seoul
9, 강다은, Finance, 57533, Seoul
10, 정우성, IT, 52343, Daegu
11, 최예린, Marketing, 52206, Seoul
12, 강다은, HR, 66980, Gwangju
13, 강다은, Finance, 50801, Incheon
14, 박지훈, Marketing, 64190, Incheon
15, 한수진, Marketing, 61921, Seoul
16, 정우성, Finance, 50986, Daegu
17, 이서연, Marketing, 63225, Daegu
18, 강다은, Finance, 55647, Gwangju
19, 한수진, IT, 53716, Seoul
20, 이서연, Finance, 68355, Incheon
21, 정우성, Finance, 67009, Daegu
22, 김민준, Finance, 64334, Seoul
23, 서지아, Marketing, 69376, Daegu
24, 한수진, Marketing, 57323, Daegu
25, 윤도현, Finance, 49780, Incheon
26, 김민준, Marketing, 47368, Busan
27, 서지아, HR, 57039, Gwangju
28, 박지훈, HR, 51655, Gwangju
29, 오준호, Marketing, 53173, Incheon
30, 최예린, Marketing, 49495, Daegu
31, 윤도현, Finance, 55893, Seoul
32, 박지훈, IT, 67386, Daegu
33, 정우성, Marketing, 67998, Incheon
34, 박지훈, HR, 58403, Gwangju
35, 오준호, Finance, 58121, Daegu
36, 정우성, Marketing, 67303, Gwangju
37, 윤도현, HR, 55966, Seoul
38, 오준호, HR, 45853, Gwangju
39, 이서연, IT, 65530, Gwangju
40, 최예린, Finance, 65153, Busan
41, 윤도현, Finance, 61958, Busan
42, 이서연, IT, 62532, Busan
43, 서지아, IT, 67677, Gwangju</code></pre>
<h3 id="2-2-컨테이너-준비"><strong>2-2. 컨테이너 준비</strong></h3>
<ul>
<li><strong>input</strong>: <code>employee_batch.csv</code> 업로드 완료</li>
<li><strong>output</strong>: 비어있는 상태로 준비</li>
</ul>
<hr>
<h2 id="3-실습-1-레코드-수-조회-data-flow-gettotalcount_df">3. [실습 1] 레코드 수 조회 (Data Flow: <code>GetTotalCount_DF</code>)</h2>
<p>전체 레코드 수를 계산하여 파일로 저장하는 데이터 플로우를 구성합니다,.</p>
<h3 id="3-1-소스-및-집계-설정"><strong>3-1. 소스 및 집계 설정</strong></h3>
<ul>
<li><strong>소스 (<code>employeeBatchData</code>)</strong>: <code>employee_batch.csv</code> 연결.</li>
<li><strong>집계 (<code>aggregateCount</code>)</strong>:<ul>
<li>그룹화 방법: 열별 그룹화 없음.</li>
<li>집계 열: <code>totalRecords</code></li>
<li>식: <code>count(ID)</code>.</li>
</ul>
</li>
</ul>
<h3 id="3-2-싱크-설정-sinktotalcount"><strong>3-2. 싱크 설정 (<code>sinkTotalCount</code>)</strong></h3>
<ul>
<li><strong>싱크 형식</strong>: Delimited Text (인라인).</li>
<li><strong>파일 이름 옵션</strong>: 단일 파일로 출력.</li>
<li><strong>단일 파일로 출력</strong>: <code>total_count.csv</code>.</li>
<li><strong>최적화</strong>: 단일 파티션 설정.</li>
</ul>
<hr>
<h2 id="4-실습-2-배치-및-분기-처리-data-flow-processemployeebatches_df">4. [실습 2] 배치 및 분기 처리 (Data Flow: <code>ProcessEmployeeBatches_DF</code>)</h2>
<p>데이터를 배치 단위로 읽어 부서별로 분기하여 저장합니다,.</p>
<h3 id="4-1-데이터-플로우-매개변수-정의"><strong>4-1. 데이터 플로우 매개변수 정의</strong></h3>
<ul>
<li><code>offset</code> (integer): 시작 지점</li>
<li><code>limit</code> (integer): 읽어올 행 수</li>
</ul>
<h3 id="4-2-소스-설정-employeebatchdata2"><strong>4-2. 소스 설정 (<code>employeeBatchData2</code>)</strong></h3>
<ul>
<li><strong>원본 옵션</strong>:<ul>
<li>건너뛰기 줄 수: <code>$offset</code></li>
<li>행 제한: <code>$limit</code></li>
</ul>
</li>
</ul>
<h3 id="4-3-조건부-분할-splitbydepartment"><strong>4-3. 조건부 분할 (<code>SplitByDepartment</code>)</strong></h3>
<ul>
<li><strong>HR</strong>: <code>Department==&#39;HR&#39;</code></li>
<li><strong>IT</strong>: <code>Department==&#39;IT&#39;</code></li>
<li><strong>OtherEmployees</strong>: (조건을 충족하지 않는 행)</li>
</ul>
<h3 id="4-4-다중-싱크-및-동적-파일명-설정"><strong>4-4. 다중 싱크 및 동적 파일명 설정</strong></h3>
<ul>
<li><strong>SinkHR</strong>: <code>concat(&#39;employee_hr_&#39;,toString($offset),&#39;.csv&#39;)</code></li>
<li><strong>SinkIT</strong>: <code>concat(&#39;employee_it_&#39;,toString($offset),&#39;.csv&#39;)</code></li>
<li><strong>sinkOther</strong>: <code>concat(&#39;employee_others_&#39;,toString($offset),&#39;.csv&#39;)</code></li>
<li><em>모든 싱크는 &#39;단일 파일로 출력&#39; 및 &#39;단일 파티션&#39; 설정을 사용합니다.</em></li>
</ul>
<hr>
<h2 id="5-파이프라인-구성-processemployeebatch_pl">5. 파이프라인 구성 (<code>ProcessEmployeeBatch_PL</code>)</h2>
<h3 id="5-1-매개변수-및-변수-선언"><strong>5-1. 매개변수 및 변수 선언</strong></h3>
<ul>
<li><strong>매개변수</strong>: <code>batchSize</code> (Int, 기본값: 50)</li>
<li><strong>변수</strong>:<ul>
<li><code>batchOffset</code> (Integer, 기본값: 0)</li>
<li><code>totalCount</code> (Integer, 기본값: 0)</li>
<li><code>tempOffset</code> (Integer, 기본값: 0)</li>
</ul>
</li>
</ul>
<h3 id="5-2-전체-레코드-수-조회-및-저장"><strong>5-2. 전체 레코드 수 조회 및 저장</strong></h3>
<ol>
<li><strong>Data Flow 활동 (<code>GetDataFlowTotalCount</code>)</strong>: <code>GetTotalCount_DF</code> 실행.</li>
<li><strong>Set Variable 활동 (<code>SetTotalCount</code>)</strong>: 조회된 행 수를 변수에 저장.<ul>
<li><strong>값 식</strong>:<pre><code>@activity(&#39;GetDataFlowTotalCount&#39;).output.runStatus.metrics.sinkTotalCount.sources.employeeBatchData.rowsRead
```,
</code></pre></li>
</ul>
</li>
</ol>
<h3 id="5-3-until-활동-untilallbatches"><strong>5-3. Until 활동 (<code>UntilAllBatches</code>)</strong></h3>
<ul>
<li><strong>반복 종료 조건</strong>:<pre><code>@greaterOrEquals(variables(&#39;batchOffset&#39;), variables(&#39;totalCount&#39;))
```,
</code></pre></li>
</ul>
<h3 id="5-4-until-내부-액티비티-구성"><strong>5-4. Until 내부 액티비티 구성</strong></h3>
<ol>
<li><strong>Data Flow 활동 (<code>ProcessBatchDataFlow</code>)</strong>: 데이터를 배치 단위로 처리.<ul>
<li><code>offset</code> 파라미터 매핑: <code>variables(&#39;batchOffset&#39;)</code></li>
<li><code>limit</code> 파라미터 매핑: <code>pipeline().parameters.batchSize</code></li>
</ul>
</li>
<li><strong>Set Variable 활동 (<code>SetOffset</code>)</strong>: 현재 오프셋 임시 저장.<ul>
<li><code>tempOffset</code> = <code>variables(&#39;batchOffset&#39;)</code></li>
</ul>
</li>
<li><strong>Set Variable 활동 (<code>SetOffset2</code>)</strong>: 다음 실행을 위한 오프셋 갱신.<ul>
<li><code>batchOffset</code> = <code>@add(variables(&#39;tempOffset&#39;), pipeline().parameters.batchSize)</code></li>
</ul>
</li>
</ol>
<hr>
<h2 id="6-결과-확인-및-마무리">6. 결과 확인 및 마무리,</h2>
<h3 id="6-1-파이프라인-실행-결과-모니터링"><strong>6-1. 파이프라인 실행 결과 모니터링</strong></h3>
<ul>
<li><code>GetDataFlowTotalCount</code>: 성공 (200개 레코드 조회 확인)</li>
<li><code>SetTotalCount</code>: 성공 (totalCount = 200 설정)</li>
<li><code>UntilAllBatches</code>: 성공 (배치 크기 50 기준 총 4회 반복 실행)</li>
<li><code>ProcessBatchDataFlow</code> (각 회차): 성공</li>
</ul>
<h3 id="6-2-최종-생성-파일-목록-output-컨테이너"><strong>6-2. 최종 생성 파일 목록 (<code>output</code> 컨테이너)</strong></h3>
<ul>
<li><code>total_count.csv</code> (200 저장 확인)</li>
<li><code>employee_hr_0.csv</code>, <code>employee_hr_50.csv</code>, <code>employee_hr_100.csv</code>, <code>employee_hr_150.csv</code></li>
<li><code>employee_it_0.csv</code>, <code>employee_it_50.csv</code>, <code>employee_it_100.csv</code>, <code>employee_it_150.csv</code></li>
<li><code>employee_others_0.csv</code>, <code>employee_others_50.csv</code>, <code>employee_others_100.csv</code>, <code>employee_others_150.csv</code></li>
</ul>
<h3 id="6-3-파일-내용-상세-예시"><strong>6-3. 파일 내용 상세 예시</strong>,,</h3>
<ul>
<li><strong>HR 0번 오프셋</strong>: ID 6(서지아), 7(박지훈), 12(강다은) 등 포함.</li>
<li><strong>IT 0번 오프셋</strong>: ID 1(오준호), 10(정우성), 19(한수진) 등 포함.</li>
<li><strong>Others 0번 오프셋</strong>: ID 2(최예린), 3(강다은), 4(정우성) 등 포함.</li>
</ul>
<p>실습 완료 후에는 비용 발생 방지를 위해 <strong>데이터 흐름 디버그 모드</strong>를 반드시 중지하십시오.</p>
<hr>
<hr>
<h1 id="azure-data-factory-join-변환-join-transformation">[Azure Data Factory] Join 변환 (Join Transformation)</h1>
<h2 id="1-join-변환-개요">1. Join 변환 개요</h2>
<p>Join 변환(Join Transformation)은 두 개의 입력 스트림을 지정된 키를 기준으로 병합(Join)하는 데이터 변환 단계입니다.,</p>
<h3 id="구성-요소"><strong>구성 요소</strong></h3>
<ul>
<li><strong>Join 조건(Join Conditions)</strong>: 두 입력 간 조인할 기준 컬럼 설정 (예: ID, Email 등)</li>
<li><strong>Join 유형(Join Type)</strong>: Inner, Left Outer, Right Outer, Full Outer 중 선택</li>
<li><strong>키 충돌 시 처리 방식</strong>: 동일한 이름의 컬럼이 양쪽에 있는 경우 우선순위 설정 가능</li>
</ul>
<h3 id="사용-시나리오"><strong>사용 시나리오</strong></h3>
<ul>
<li>사용자 정보와 주문 정보를 ID 기준으로 병합</li>
<li>로그 정보와 에러 코드 목록을 조인하여 분석</li>
<li>마스터 테이블과 세부 정보 테이블 병합</li>
</ul>
<h3 id="장점"><strong>장점</strong></h3>
<ul>
<li>다양한 조인 방식 제공으로 유연한 병합 구조 설계 가능</li>
<li>조건 기반 병합 처리로 데이터 정합성 확보</li>
<li>하나의 데이터 흐름 안에서 복잡한 관계형 연산 처리 가능</li>
</ul>
<hr>
<h2 id="2-join-유형별-상세-설명">2. Join 유형별 상세 설명</h2>
<h3 id="1-inner-join-id---고객id-기준"><strong>1) Inner Join (ID - 고객ID 기준)</strong></h3>
<p>두 테이블 모두에 조인 키가 존재하는 데이터만 반환합니다.</p>
<table>
<thead>
<tr>
<th align="left">A.ID</th>
<th align="left">A.이름</th>
<th align="left">A.도시</th>
<th align="left">B.주문ID</th>
<th align="left">B.고객ID</th>
<th align="left">B.상품명</th>
</tr>
</thead>
<tbody><tr>
<td align="left">1</td>
<td align="left">김철수</td>
<td align="left">서울</td>
<td align="left">101</td>
<td align="left">1</td>
<td align="left">노트북</td>
</tr>
<tr>
<td align="left">1</td>
<td align="left">김철수</td>
<td align="left">서울</td>
<td align="left">104</td>
<td align="left">1</td>
<td align="left">모니터</td>
</tr>
<tr>
<td align="left">2</td>
<td align="left">이영희</td>
<td align="left">부산</td>
<td align="left">102</td>
<td align="left">2</td>
<td align="left">마우스</td>
</tr>
</tbody></table>
<h3 id="2-full-outer-join"><strong>2) Full Outer Join</strong></h3>
<p>양쪽 테이블의 모든 데이터를 반환하며, 짝이 없는 경우 NULL로 표시됩니다.</p>
<table>
<thead>
<tr>
<th align="left">A.이름</th>
<th align="left">A.ID</th>
<th align="left">A.도시</th>
<th align="left">B.주문ID</th>
<th align="left">B.고객ID</th>
<th align="left">B.상품명</th>
</tr>
</thead>
<tbody><tr>
<td align="left">김철수</td>
<td align="left">1</td>
<td align="left">서울</td>
<td align="left">101</td>
<td align="left">1</td>
<td align="left">노트북</td>
</tr>
<tr>
<td align="left">김철수</td>
<td align="left">1</td>
<td align="left">서울</td>
<td align="left">104</td>
<td align="left">1</td>
<td align="left">모니터</td>
</tr>
<tr>
<td align="left">이영희</td>
<td align="left">2</td>
<td align="left">부산</td>
<td align="left">102</td>
<td align="left">2</td>
<td align="left">마우스</td>
</tr>
<tr>
<td align="left">박민준</td>
<td align="left">3</td>
<td align="left">서울</td>
<td align="left">NULL</td>
<td align="left">NULL</td>
<td align="left">NULL</td>
</tr>
<tr>
<td align="left">최지우</td>
<td align="left">5</td>
<td align="left">인천</td>
<td align="left">NULL</td>
<td align="left">NULL</td>
<td align="left">NULL</td>
</tr>
<tr>
<td align="left">NULL</td>
<td align="left">NULL</td>
<td align="left">NULL</td>
<td align="left">103</td>
<td align="left">4</td>
<td align="left">키보드</td>
</tr>
</tbody></table>
<h3 id="3-left-join"><strong>3) Left Join</strong></h3>
<p>왼쪽 테이블(고객 정보)의 모든 데이터와 오른쪽 테이블(주문 정보)의 매칭되는 데이터를 반환합니다.</p>
<table>
<thead>
<tr>
<th align="left">A.ID</th>
<th align="left">A.이름</th>
<th align="left">A.도시</th>
<th align="left">B.주문ID</th>
<th align="left">B.고객ID</th>
<th align="left">B.상품명</th>
</tr>
</thead>
<tbody><tr>
<td align="left">1</td>
<td align="left">김철수</td>
<td align="left">서울</td>
<td align="left">101</td>
<td align="left">1</td>
<td align="left">노트북</td>
</tr>
<tr>
<td align="left">1</td>
<td align="left">김철수</td>
<td align="left">서울</td>
<td align="left">104</td>
<td align="left">1</td>
<td align="left">모니터</td>
</tr>
<tr>
<td align="left">2</td>
<td align="left">이영희</td>
<td align="left">부산</td>
<td align="left">102</td>
<td align="left">2</td>
<td align="left">마우스</td>
</tr>
<tr>
<td align="left">3</td>
<td align="left">박민준</td>
<td align="left">서울</td>
<td align="left">NULL</td>
<td align="left">NULL</td>
<td align="left">NULL</td>
</tr>
<tr>
<td align="left">5</td>
<td align="left">최지우</td>
<td align="left">인천</td>
<td align="left">NULL</td>
<td align="left">NULL</td>
<td align="left">NULL</td>
</tr>
</tbody></table>
<h3 id="4-right-join"><strong>4) Right Join</strong></h3>
<p>오른쪽 테이블(주문 정보)의 모든 데이터와 왼쪽 테이블(고객 정보)의 매칭되는 데이터를 반환합니다.</p>
<table>
<thead>
<tr>
<th align="left">A.ID</th>
<th align="left">A.이름</th>
<th align="left">A.도시</th>
<th align="left">B.주문ID</th>
<th align="left">B.고객ID</th>
<th align="left">B.상품명</th>
</tr>
</thead>
<tbody><tr>
<td align="left">1</td>
<td align="left">김철수</td>
<td align="left">서울</td>
<td align="left">101</td>
<td align="left">1</td>
<td align="left">노트북</td>
</tr>
<tr>
<td align="left">1</td>
<td align="left">김철수</td>
<td align="left">서울</td>
<td align="left">104</td>
<td align="left">1</td>
<td align="left">모니터</td>
</tr>
<tr>
<td align="left">2</td>
<td align="left">이영희</td>
<td align="left">부산</td>
<td align="left">102</td>
<td align="left">2</td>
<td align="left">마우스</td>
</tr>
<tr>
<td align="left">NULL</td>
<td align="left">NULL</td>
<td align="left">NULL</td>
<td align="left">103</td>
<td align="left">4</td>
<td align="left">키보드</td>
</tr>
</tbody></table>
<hr>
<h2 id="3-실습-준비">3. 실습 준비</h2>
<h3 id="3-1-실습-데이터-준비"><strong>3-1. 실습 데이터 준비</strong></h3>
<ul>
<li><strong>customers.csv</strong><pre><code class="language-text">ID, Name, Address
1, 김철수, 서울
2, 이영희, 부산
3, 박민준, 서울
5, 최지우, 인천</code></pre>
</li>
<li><strong>orders.csv</strong><pre><code class="language-text">OrderID, CustomerID, ProductName
101, 1, 노트북
102, 2, 마우스
103, 4, 키보드
104, 1, 모니터</code></pre>
</li>
</ul>
<h3 id="3-2-컨테이너-준비"><strong>3-2. 컨테이너 준비</strong>,</h3>
<ul>
<li><strong>input</strong>: <code>customers.csv</code>, <code>orders.csv</code> 업로드</li>
<li><strong>output</strong>: 비어있는 상태로 준비</li>
</ul>
<hr>
<h2 id="4-실습-1-기본-join-데이터-플로우-구성">4. [실습 1] 기본 Join 데이터 플로우 구성</h2>
<h3 id="4-1-소스-설정"><strong>4-1. 소스 설정</strong>,</h3>
<ol>
<li><strong>Source 1 (<code>customerData</code>)</strong>: <code>CustomersInput_DS</code> 연결 (customers.csv).</li>
<li><strong>Source 2 (<code>ordersData</code>)</strong>: <code>Orderinput_DS</code> 연결 (orders.csv).<ul>
<li>두 소스 모두 <strong>프로젝션 가져오기</strong>를 통해 ID, OrderID 등을 integer 형식으로 정의합니다.,</li>
</ul>
</li>
</ol>
<h3 id="4-2-join-변환-설정-joincustomerorders"><strong>4-2. Join 변환 설정 (<code>joinCustomerOrders</code>)</strong></h3>
<ul>
<li><strong>왼쪽 스트림</strong>: <code>customerData</code></li>
<li><strong>오른쪽 스트림</strong>: <code>ordersData</code></li>
<li><strong>조인 유형</strong>: <code>내부(Inner)</code></li>
<li><strong>조인 조건</strong>:<ul>
<li>왼쪽: <code>ID</code></li>
<li>오른쪽: <code>CustomerID</code></li>
</ul>
</li>
</ul>
<h3 id="4-3-싱크-설정-sinkjoineddata"><strong>4-3. 싱크 설정 (<code>sinkJoinedData</code>)</strong></h3>
<ul>
<li><strong>데이터 세트</strong>: <code>JoinedOutputCSV_DS</code></li>
<li><strong>설정</strong>: 단일 파티션, <strong>단일 파일로 출력</strong>.</li>
<li><strong>파일명</strong>: <code>joined_customer_orders.csv</code></li>
</ul>
<h3 id="4-4-파이프라인-실행-및-결과-확인"><strong>4-4. 파이프라인 실행 및 결과 확인</strong>,</h3>
<ul>
<li><strong>파이프라인</strong>: <code>JoinCustomerOrders_PL</code></li>
<li><strong>활동</strong>: <code>ExecuteJoin_DF</code> (Join_DF 실행)</li>
<li><strong>결과</strong>: <code>output</code> 컨테이너에 3개의 행이 포함된 파일 생성 확인.,</li>
</ul>
<hr>
<h2 id="5-실습-2-고급-데이터-변환-집계-및-필터">5. [실습 2] 고급 데이터 변환 (집계 및 필터)</h2>
<h3 id="5-1-고객별-주문-건수-집계-aggregatetotalordersbycustomer"><strong>5-1. 고객별 주문 건수 집계 (<code>aggregateTotalOrdersByCustomer</code>)</strong></h3>
<ul>
<li><strong>들어오는 스트림</strong>: <code>joinCustomerOrders</code></li>
<li><strong>그룹화 방법</strong>: <code>ID</code>, <code>Name</code></li>
<li><strong>집계 컬럼</strong>: <code>totalOrders</code></li>
<li><strong>식</strong>:<pre><code class="language-text">count(OrderID)</code></pre>
</li>
<li><strong>결과 싱크</strong>: <code>customer_total_orders.csv</code></li>
</ul>
<h3 id="5-2-서울-주문-데이터-필터링-filterseoulcustomers"><strong>5-2. 서울 주문 데이터 필터링 (<code>filterSeoulCustomers</code>)</strong></h3>
<ul>
<li><strong>들어오는 스트림</strong>: <code>joinCustomerOrders</code></li>
<li><strong>필터 식</strong>:<pre><code class="language-text">Address == &#39;서울&#39;</code></pre>
</li>
<li><strong>결과 싱크</strong>: <code>seoul_customer_orders.csv</code>,</li>
</ul>
<hr>
<h2 id="6-최종-실행-결과-요약">6. 최종 실행 결과 요약</h2>
<ol>
<li><strong>joined_customer_orders.csv</strong>: 조인된 전체 데이터 (3건)</li>
<li><strong>customer_total_orders.csv</strong>:<ul>
<li>이영희: 1건</li>
<li>김철수: 2건</li>
</ul>
</li>
<li><strong>seoul_customer_orders.csv</strong>:<ul>
<li>김철수(서울)의 주문 데이터 2건 (모니터, 노트북)</li>
</ul>
</li>
</ol>
<p><strong>실습 마무리</strong>: 모든 작업이 완료되면 비용 발생 방지를 위해 <strong>데이터 흐름 디버그 모드를 종료</strong>합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MicrosoftDataSchool] 59일차 - Azure Data Factory 실습]]></title>
            <link>https://velog.io/@rudin_/MicrosoftDataSchool-59%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@rudin_/MicrosoftDataSchool-59%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Tue, 31 Mar 2026 08:51:29 GMT</pubDate>
            <description><![CDATA[<p>제공해주신 소스 파일 <strong>&quot;03-30-2.03 데이터플로우_v12.pdf&quot;</strong>의 모든 페이지 내용을 생략 없이, 특히 누락되었던 <strong>적용 예시</strong>와 <strong>수식</strong>들을 원문 그대로 포함하여 정리해 드립니다. 여러 줄로 된 부분은 합치지 않고 소스 형태를 유지했습니다.</p>
<hr>
<h1 id="azure-data-factory-데이터-플로우">[Azure Data Factory] 데이터 플로우</h1>
<h2 id="1-매핑-데이터-플로우-개요">1. 매핑 데이터 플로우 개요</h2>
<p>매핑 데이터 플로우는 데이터 변환을 시각적으로 설계할 수 있는 기능으로, 코딩 없이 GUI 기반으로 데이터 흐름을 정의하고 실행할 수 있도록 지원합니다.</p>
<h3 id="특징"><strong>특징</strong></h3>
<ul>
<li>코드 작성 없이 ETL 작업 가능</li>
<li>병렬 처리 기반 대용량 데이터 변환</li>
<li>Spark 기반 백엔드 자동 실행</li>
<li>ADF 파이프라인 내 액티비티로 실행 가능</li>
</ul>
<h3 id="적용-예시"><strong>적용 예시</strong></h3>
<ul>
<li><strong>Raw 데이터 정제 및 변환 작업</strong></li>
<li><strong>데이터 웨어하우스 적재 전 변환</strong></li>
<li><strong>로그 데이터의 전처리 및 분석 준비</strong></li>
</ul>
<hr>
<h2 id="2-주요-구성요소-및-기능">2. 주요 구성요소 및 기능</h2>
<ul>
<li><strong>Source</strong>: 데이터 가져오기 (Blob, SQL 등)</li>
<li><strong>Transformation</strong>: 변환 로직 적용</li>
<li><strong>Sink</strong>: 결과 저장 (Blob, SQL 등)</li>
</ul>
<h3 id="변환-기능"><strong>변환 기능</strong></h3>
<ul>
<li><strong>Select</strong>: 컬럼 선택 및 이름 변경</li>
<li><strong>Filter</strong>: 조건에 따라 행 필터링</li>
<li><strong>Join</strong>: 다른 스트림과 조인</li>
<li><strong>Aggregate</strong>: 그룹화 및 집계</li>
<li><strong>Derived Column</strong>: 계산 컬럼 생성</li>
<li><strong>Sort</strong>: 정렬 수행</li>
<li><strong>Pivot</strong>: 데이터 형태 변환</li>
</ul>
<h3 id="변환-기능-적용-예시"><strong>변환 기능 적용 예시</strong></h3>
<ul>
<li><strong>시험 점수를 과목별 평균으로 집계</strong></li>
<li><strong>이벤트 로그에서 &#39;오류&#39;만 필터링하여 저장</strong></li>
</ul>
<h3 id="흐름-제어-기능"><strong>흐름 제어 기능</strong></h3>
<ul>
<li><strong>Conditional Split</strong>: 조건에 따라 경로 분기</li>
<li><strong>Exists</strong>: 조건 충족 여부 확인</li>
<li><strong>Lookup</strong>: 외부 값 참조</li>
</ul>
<h3 id="흐름-제어-기능-적용-예시"><strong>흐름 제어 기능 적용 예시</strong></h3>
<ul>
<li><strong>고객 나이에 따라 다른 테이블에 저장</strong></li>
<li><strong>기존 고객 여부 확인 후 신규 등록 여부 결정</strong></li>
</ul>
<hr>
<h2 id="3-pipeline-vs-mdf-및-실행-방식">3. Pipeline vs MDF 및 실행 방식</h2>
<h3 id="역할-구분"><strong>역할 구분</strong></h3>
<table>
<thead>
<tr>
<th align="left">구분</th>
<th align="left">파이프라인</th>
<th align="left">데이터 플로우</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>역할</strong></td>
<td align="left">실행 제어 / 전체 흐름 관리</td>
<td align="left">데이터 변환 수행</td>
</tr>
</tbody></table>
<h3 id="데이터-흐름-디버그"><strong>데이터 흐름 디버그</strong></h3>
<ul>
<li><strong>디버그 모드</strong>: 파이프라인 실행 전, 결과를 미리 확인 가능하며 Sample Data 기반으로 변환 결과를 확인합니다.</li>
<li><strong>예시</strong><ul>
<li><strong>변환 로직 개발 중 오류 확인</strong></li>
<li><strong>실시간으로 컬럼 파생 결과 시각화 확인</strong></li>
</ul>
</li>
</ul>
<h3 id="mdf-실행-방식"><strong>MDF 실행 방식</strong></h3>
<ul>
<li>파이프라인 내 <strong>Data Flow Activity</strong>로 호출</li>
<li><strong>Integration Runtime(IR)</strong>을 통해 Spark 클러스터 생성 (클러스터는 자동 생성 및 종료)</li>
<li><strong>예시</strong><ul>
<li><strong>주기적으로 정해진 시간마다 ETL 실행</strong></li>
<li><strong>조건 만족 시 트리거로 Data Flow 자동 실행</strong></li>
</ul>
</li>
</ul>
<hr>
<h2 id="4-실습-준비-데이터-및-환경-세팅">4. [실습 준비] 데이터 및 환경 세팅</h2>
<h3 id="실습-데이터-student-performance-in-exams"><strong>실습 데이터: Student Performance in Exams</strong></h3>
<ul>
<li>Kaggle 데이터 활용 (StudentsPerformance.csv)</li>
<li><strong>컬럼 구성</strong><ul>
<li>gender, race/ethnicity, parental level of education, lunch, test preparation course, math score, reading score, writing score</li>
</ul>
</li>
</ul>
<h3 id="환경-준비"><strong>환경 준비</strong></h3>
<ol>
<li><strong>실습 컨테이너 준비</strong>: <code>input</code>, <code>output</code> 컨테이너의 기존 데이터 삭제 후 <code>StudentsPerformance.csv</code> 업로드.</li>
<li><strong>링크드 서비스 준비</strong>: <code>BlobStorage1</code> (Azure Blob Storage) 생성 및 연결 테스트 성공 확인.</li>
<li><strong>데이터세트 생성</strong>:<ul>
<li><strong>입력(<code>StudentsInputDS</code>)</strong>: <code>input</code> 컨테이너, <code>StudentsPerformance.csv</code> 참조, 첫 번째 행을 머리글로 설정, 스키마 가져오기(연결/저장소에서).</li>
<li><strong>출력(<code>StudentsOutputDS</code>)</strong>: <code>output</code> 컨테이너 참조, 첫 번째 행을 머리글로 설정.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="5-실습-1-데이터-정제-및-등급-부여">5. [실습 1] 데이터 정제 및 등급 부여</h2>
<h3 id="5-1-매핑-데이터-플로우-생성-및-소스-추가"><strong>5-1. 매핑 데이터 플로우 생성 및 소스 추가</strong></h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/424943bb-fb41-4ab6-9361-24d8b9d734d2/image.png" alt=""></p>
<ul>
<li>이름: <code>StudentsCleanFlow</code></li>
<li><strong>데이터 흐름 디버그</strong> 켜기: AutoResolveIntegrationRuntime, Small, TTL 1시간 설정.
<img src="https://velog.velcdn.com/images/rudin_/post/0a046bcf-b9f5-42c0-9a7b-4344527a2295/image.png" alt=""></li>
<li><strong>소스(<code>Students</code>)</strong>: <code>StudentsInputDS</code> 연결 후 <strong>프로젝션 가져오기</strong>를 통해 데이터 형식 검색.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/rudin_/post/90005cf3-2f09-40c9-a4b8-b9a5ac8da761/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/9bfe2178-11cb-4f37-8b96-646ac5e73aa1/image.png" alt=""></p>
<p>스키마 드리프트: 원본이 아닌 별도의 테이블을 만들어 사용할 때 어느정도의 오류는 ok 하는 기능</p>
<h3 id="5-2-평균-점수-계산-derived-column---averagescore"><strong>5-2. 평균 점수 계산 (Derived Column - <code>AverageScore</code>)</strong></h3>
<p>컴포넌트 옵션에는 항상 출력, 입력 스트림이 존재함
<img src="https://velog.velcdn.com/images/rudin_/post/eb928c48-650b-496c-870f-328c12767796/image.png" alt=""></p>
<ul>
<li><code>+</code> 누른 후 <code>파생 열</code> 추가</li>
<li>출력 스트림 이름: <code>AverageScore</code></li>
<li>열 이름: <code>avg_score</code></li>
<li><strong>식</strong>:<pre><code>(toInteger({math score})+toInteger({reading score})+toInteger({writing score}))/3</code></pre></li>
</ul>
<h3 id="5-3-등급-부여-derived-column---gradelevel"><strong>5-3. 등급 부여 (Derived Column - <code>GradeLevel</code>)</strong></h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/3983c961-8f06-4d96-938d-47fa1ddc7ce0/image.png" alt=""></p>
<ul>
<li>출력 스트림 이름: <code>GradeLevel</code></li>
<li>열 이름: <code>grade</code></li>
<li><strong>식</strong>:<pre><code>iif(avg_score&gt;=90, &#39;A&#39;, 
iif(avg_score&gt;=80, &#39;B&#39;,
iif(avg_score&gt;=70, &#39;C&#39;, 
iif(avg_score&gt;=60, &#39;D&#39;, &#39;F&#39;))))</code></pre></li>
</ul>
<h3 id="5-4-싱크-설정-studentcleansink"><strong>5-4. 싱크 설정 (StudentCleanSink)</strong></h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/53a4855b-a628-49e1-bf7d-437bbdc73bce/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/04c79fd8-256b-4116-899f-0ddfca725dd4/image.png" alt=""></p>
<ul>
<li><p>데이터 세트: <code>StudentsOutputDS</code></p>
</li>
<li><p><strong>설정</strong>: 파티션 설정을 &#39;단일 파티션&#39;으로 변경 후 <strong>단일 파일로 출력</strong> 선택. (기본적으로는 분산 저장을 지원)
<img src="https://velog.velcdn.com/images/rudin_/post/01be62fb-d5df-4bcf-ba13-fa3dc3bf7432/image.png" alt=""></p>
</li>
<li><p><strong>파일명</strong>: <code>Students_clean.csv</code></p>
</li>
</ul>
<h3 id="5-5-파이프라인-실행-및-결과-확인"><strong>5-5. 파이프라인 실행 및 결과 확인</strong></h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/6917f9bd-6b36-46f3-8d41-d071429bb400/image.png" alt=""></p>
<ol>
<li><strong>파이프라인 생성</strong>: <code>StudentsPerformancePipeline</code>에 <code>Data Flow</code> 활동(<code>RunStudentsCleanFlow</code>) 추가.</li>
<li><strong>실행</strong>: <strong>모두 게시</strong> 후 디버그 실행.</li>
<li><strong>결과 확인</strong>: <code>output</code> 컨테이너의 <code>Students_clean.csv</code>에서 평균 점수와 등급(A~F) 확인.</li>
<li><strong>모니터링</strong>: 각 단계별(Source, AverageScore, GradeLevel) 처리 시간 및 기록된 행(1,000행) 진단 정보 확인.
<img src="https://velog.velcdn.com/images/rudin_/post/616326f0-987b-4e7f-b0b6-b7e682af97d4/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/1faa6794-a76a-468f-b0c3-949ebfe67b28/image.png" alt=""></li>
</ol>
<hr>
<h2 id="6-실습-2-그룹별-집계-실습-advanced">6. [실습 2] 그룹별 집계 실습 (Advanced)</h2>
<h3 id="6-1-데이터-플로우-편집-및-분기"><strong>6-1. 데이터 플로우 편집 및 분기</strong></h3>
<ul>
<li><code>AverageScore</code> 단계에서 <strong>새 분기(New Branch)</strong> 추가.
<img src="https://velog.velcdn.com/images/rudin_/post/6b59c4ea-c2ef-4933-a9e6-ac323af03255/image.png" alt=""></li>
</ul>
<h3 id="6-2-데이터-집계-aggregate---averagebygroup"><strong>6-2. 데이터 집계 (Aggregate - <code>AverageByGroup</code>)</strong></h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/834494b8-d7a4-4141-b216-3e16b3c1c2d1/image.png" alt=""></p>
<ul>
<li><p>출력 스트림 이름: <code>AverageByGroup</code></p>
</li>
<li><p><strong>그룹화 방법</strong>: <code>race/ethnicity</code> 열 기준.
<img src="https://velog.velcdn.com/images/rudin_/post/f67e37d9-6771-4238-97a8-10554eefa9ce/image.png" alt=""></p>
</li>
<li><p><strong>집계 컬럼</strong>: <code>avg_score_by_group</code></p>
</li>
<li><p><strong>식</strong>: <strong><code>avg(avg_score)</code></strong>
<img src="https://velog.velcdn.com/images/rudin_/post/7ec39679-9980-4f7c-a323-d48d1fb56783/image.png" alt=""></p>
</li>
</ul>
<h3 id="6-3-새로운-싱크-추가-studentgroupcleansink"><strong>6-3. 새로운 싱크 추가 (StudentGroupCleanSink)</strong></h3>
<ul>
<li>데이터 세트: <code>StudentsOutputDS</code></li>
<li><strong>설정</strong>: 단일 파티션, <strong>단일 파일로 출력</strong>.</li>
<li><strong>파일명</strong>: <code>avg_score_by_group.csv</code></li>
</ul>
<p><img src="https://velog.velcdn.com/images/rudin_/post/7c6f5589-1030-4fa6-9e6c-cda5f4bc520f/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/f8af6ea2-9505-489a-b0e2-6467e0de9e53/image.png" alt=""></p>
<h3 id="6-4-최종-실행-및-확인"><strong>6-4. 최종 실행 및 확인</strong></h3>
<ul>
<li>변경 사항 게시 후 파이프라인 재실행.
<img src="https://velog.velcdn.com/images/rudin_/post/3414771a-6517-43d4-bee8-429762fec9b1/image.png" alt=""></li>
</ul>
<hr>
<h2 id="7-마무리">7. 마무리</h2>
<ul>
<li>실습 완료 후 비용 발생 방지를 위해 <strong>데이터 흐름 디버그 모드</strong>를 반드시 중지합니다.</li>
</ul>
<hr>
<hr>
<h1 id="azure-data-factory-통합-런타임-integration-runtime">[Azure Data Factory] 통합 런타임 (Integration Runtime)</h1>
<h2 id="1-통합-런타임-개요">1. 통합 런타임 개요</h2>
<p>통합 런타임(Integration Runtime)은 Azure Data Factory에서 데이터 이동, 변환, 실행 등의 기능을 수행하기 위한 컴퓨팅 인프라 역할을 수행합니다.</p>
<p><strong>Integration Runtime의 핵심</strong></p>
<ul>
<li>ADF의 모든 데이터 처리 작업은 통합 런타임(IR)을 통해 실행됩니다.</li>
<li>IR은 다양한 네트워크 환경 및 데이터 소스/싱크와의 연결을 지원합니다.</li>
<li>작업 목적과 데이터 위치에 따라 적절한 IR 유형을 선택해야 합니다.</li>
</ul>
<table>
<thead>
<tr>
<th align="left">IR의 수행 기능</th>
<th align="left">IR의 수행 내용</th>
</tr>
</thead>
<tbody><tr>
<td align="left">활동 실행</td>
<td align="left">Copy, 외부 리소스 실행 등 여러 가지 활동의 지원</td>
</tr>
<tr>
<td align="left">데이터 이동</td>
<td align="left">클라우드 ↔ 온프레미스 간 안전한 복사 수행</td>
</tr>
<tr>
<td align="left">데이터 흐름 실행</td>
<td align="left">Mapping Data Flow 등 고급 데이터 변환 지원<br>* Azure IR 전용</td>
</tr>
</tbody></table>
<p><strong>IR은 &quot;ADF의 실행 엔진&quot;</strong>이며, 어떤 네트워크에서 데이터를 가져오고 어디로 보낼 것인지에 따라 적절한 유형을 선택하는 것이 필수적입니다.</p>
<hr>
<h2 id="2-통합-런타임의-유형">2. 통합 런타임의 유형</h2>
<p>데이터 팩토리에서는 다양한 환경에 맞게 세 가지 유형의 통합 런타임을 제공합니다. 각 유형은 사용자의 네트워크 구조, 데이터 위치, 기존 시스템 여부에 따라 선택해야 합니다.</p>
<h3 id="통합-런타임의-세-가지-유형"><strong>통합 런타임의 세 가지 유형</strong></h3>
<table>
<thead>
<tr>
<th align="left">통합 런타임 유형</th>
<th align="left">주요 목적 및 특성</th>
</tr>
</thead>
<tbody><tr>
<td align="left">Azure Integration Runtime</td>
<td align="left">Azure 내부 서비스 간 데이터 이동 및 변환, 완전 관리형, 서버리스 컴퓨팅 환경</td>
</tr>
<tr>
<td align="left">Self-hosted IR (SHIR)</td>
<td align="left">온프레미스 또는 VNet 내 리소스와 연결, 사용자 컴퓨터/VM에 런타임 설치 필요</td>
</tr>
<tr>
<td align="left">Azure-SSIS IR</td>
<td align="left">SSIS 패키지를 Azure에서 실행하기 위한 전용 런타임, SQL Managed Instance 필요</td>
</tr>
</tbody></table>
<h3 id="통합-런타임-유형별-특성"><strong>통합 런타임 유형별 특성</strong></h3>
<table>
<thead>
<tr>
<th align="left">구분</th>
<th align="left">Azure IR</th>
<th align="left">SHIR</th>
<th align="left">Azure-SSIS IR</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>관리 주체</strong></td>
<td align="left">Microsoft</td>
<td align="left">사용자 직접 관리</td>
<td align="left">Microsoft</td>
</tr>
<tr>
<td align="left"><strong>설치 필요 여부</strong></td>
<td align="left">없음</td>
<td align="left">설치 필요</td>
<td align="left">설치 필요</td>
</tr>
<tr>
<td align="left"><strong>데이터 흐름 지원 여부</strong></td>
<td align="left">지원</td>
<td align="left">미지원</td>
<td align="left">미지원</td>
</tr>
<tr>
<td align="left"><strong>사용 위치</strong></td>
<td align="left">Azure 간 서비스</td>
<td align="left">온프레미스, VNet 리소스</td>
<td align="left">SSIS 기반 데이터 마이그레이션</td>
</tr>
</tbody></table>
<p>IR 선택은 데이터의 출발지/도착지, 네트워크 형태(공용/사설), 기존 시스템 여부(SSIS 등) 등을 고려하여 결정합니다.</p>
<hr>
<h2 id="3-self-hosted-통합-런타임-shir">3. Self-Hosted 통합 런타임 (SHIR)</h2>
<p>Self-Hosted 통합 런타임은 Azure Data Factory에서 사설 네트워크나 온프레미스 환경의 데이터에 접근할 수 있도록 해주는 소프트웨어 컴포넌트입니다.</p>
<h3 id="shir의-필요성"><strong>SHIR의 필요성</strong></h3>
<ul>
<li>가상 사설망(VNet) 내의 데이터 및 리소스 접근 필요 시</li>
<li>온프레미스의 데이터베이스와의 연동 시</li>
<li>전용 드라이버 및 커넥터를 사용해야 하는 특수 데이터 환경</li>
</ul>
<h3 id="네트워크별-ir-지원-현황"><strong>네트워크별 IR 지원 현황</strong></h3>
<table>
<thead>
<tr>
<th align="left">유형</th>
<th align="left">Azure Cloud</th>
<th align="left">Private Network</th>
</tr>
</thead>
<tbody><tr>
<td align="left">Azure IR</td>
<td align="left">Activity 실행, 데이터 이동, 데이터 플로우</td>
<td align="left">지원 안함</td>
</tr>
<tr>
<td align="left">Self-hosted IR</td>
<td align="left">Activity 실행, 데이터 이동</td>
<td align="left">Activity 실행, 데이터 이동</td>
</tr>
<tr>
<td align="left">Azure-SSIS IR</td>
<td align="left">SSIS 패키지 실행 (제한적)</td>
<td align="left">SSIS 패키지 실행</td>
</tr>
</tbody></table>
<ul>
<li>온프레미스 DB(예: Oracle, MySQL, MSSQL 등)와 연동이 필요한 경우에는 SHIR가 반드시 필요합니다.</li>
<li>SHIR은 로컬 머신 또는 VM에 설치되며, Azure Portal에서 연결 상태의 모니터링도 가능합니다. (보통 데이터 이관용이다. 데이터플로우 지원 X)</li>
</ul>
<hr>
<h2 id="4-실습-shir-환경-구축-및-데이터-복사">4. [실습] SHIR 환경 구축 및 데이터 복사</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/c29a54a7-e361-4e71-b75b-47215d9e46b4/image.png" alt=""></p>
<h3 id="step-1-실습-환경-구성-인프라"><strong>Step 1: 실습 환경 구성 (인프라)</strong></h3>
<p>실습을 위해 다음과 같은 리소스를 순차적으로 생성합니다.</p>
<ol>
<li><strong>Azure Virtual Network 생성</strong>: <code>vnet</code> (주소 공간: <code>10.0.0.0/16</code>).
<img src="https://velog.velcdn.com/images/rudin_/post/05334c4c-3c3c-4ea4-9d9e-dafdcd7444dc/image.png" alt=""></li>
<li><strong>Subnet 생성</strong>: <code>subnet</code> (주소 범위: <code>10.0.1.0/24</code>)
<img src="https://velog.velcdn.com/images/rudin_/post/fe952193-b620-4a79-a7d6-211e8dde01d8/image.png" alt=""></li>
<li><strong>SQL Server용 VM 생성</strong>: <code>SQL-vm</code>
<img src="https://velog.velcdn.com/images/rudin_/post/1b011ee5-f9e9-4486-ba4e-fe09ef9ea362/image.png" alt=""></li>
</ol>
<ul>
<li><p>이미지: SQL Server 2019 Developer on Windows Server 2019
<img src="https://velog.velcdn.com/images/rudin_/post/2474508d-eaf6-4a4f-b0fa-ceb92b96a4e9/image.png" alt="">
모든 이미지 보기 선택
<img src="https://velog.velcdn.com/images/rudin_/post/8ba3025a-e67e-4a28-bc48-496b64eef670/image.png" alt="">
만들기 후 2세대 선택 </p>
</li>
<li><p>크기: Standard_B2ms (2 vcpu, 8 GiB 메모리)</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/rudin_/post/14ab300e-0098-4838-ac43-e8ca8525e34f/image.png" alt=""></p>
<ul>
<li>서브넷 설정</li>
<li>인바운드 포트: RDP(3389) 허용</li>
</ul>
<p><img src="https://velog.velcdn.com/images/rudin_/post/7477fb0b-9f3e-4269-bb85-7b0124ed1eba/image.png" alt=""></p>
<ul>
<li>SQL 연결: 프라이빗(가상 네트워크 내), 포트 1433
<img src="https://velog.velcdn.com/images/rudin_/post/3e92ca5f-7a9a-4f31-9b53-8b516a21acd2/image.png" alt=""></li>
</ul>
<ol start="4">
<li><strong>SHIR용 VM 생성</strong>: <code>SHIR-vm</code></li>
</ol>
<ul>
<li><p>이미지: Windows Server 2019 Datacenter
<img src="https://velog.velcdn.com/images/rudin_/post/c0a858d6-3a67-45b6-9faa-e8d24d049212/image.png" alt=""></p>
</li>
<li><p>디스크 표준으로 설정
<img src="https://velog.velcdn.com/images/rudin_/post/f58e6e31-f778-42f7-b1c1-10fe4e21a0d1/image.png" alt=""></p>
</li>
<li><p>네트워크: <code>vnet</code> / <code>subnet</code> 연결
<img src="https://velog.velcdn.com/images/rudin_/post/02d12443-684a-49bb-9544-f9d00fd26ebe/image.png" alt=""></p>
</li>
</ul>
<h3 id="step-2-shir-설치-및-등록"><strong>Step 2: SHIR 설치 및 등록</strong></h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/a49eb2d8-9ba6-4839-8475-a848cd243d21/image.png" alt="">
들어가서 <code>RDP 파일 다운로드</code> </p>
<ol>
<li><p><strong>SHIR-vm 접속</strong>: RDP를 통해 가상 머신에 접속합니다.</p>
</li>
<li><p><strong>런타임 다운로드</strong>: VM 내부 브라우저에서 &#39;Microsoft Integration Runtime&#39;을 검색하여 설치 파일을 다운로드합니다.
<img src="https://velog.velcdn.com/images/rudin_/post/0ff9de55-e4b7-4bad-991a-65c95f20b29f/image.png" alt=""></p>
</li>
<li><p><strong>ADF에서 SHIR 생성</strong>: ADF Studio의 [관리] &gt; [통합 런타임]에서 &#39;자체 호스팅&#39; 유형으로 <code>shir</code>를 생성하고 <strong>인증 키</strong>를 복사합니다.
<img src="https://velog.velcdn.com/images/rudin_/post/28e8c5b0-f8e3-4459-8f34-58b5f6c2aad2/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/287b2850-0a82-4cbd-ba17-1950e78bf676/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/0fe910b3-55d7-45b8-82cc-75e49085234d/image.png" alt=""></p>
</li>
<li><p><strong>노드 등록</strong>: VM에 설치된 Configuration Manager를 실행하고 복사한 인증 키를 입력하여 등록을 완료합니다.
<img src="https://velog.velcdn.com/images/rudin_/post/471dd67b-9e79-40eb-8242-186e8999ea9f/image.png" alt="">
ADF 관리탭에서도 표시된다.
<img src="https://velog.velcdn.com/images/rudin_/post/a6dc7c98-b493-43e6-bcf9-9d8321ecb3fb/image.png" alt=""></p>
</li>
</ol>
<h3 id="step-3-데이터-준비-원본-및-싱크"><strong>Step 3: 데이터 준비 (원본 및 싱크)</strong></h3>
<ol>
<li><strong>원본 데이터</strong>: Blob Storage의 <code>input</code> 컨테이너에 <code>StudentsPerformance.csv</code> 업로드.</li>
<li><strong>싱크 테이블 생성</strong>: <code>SQL-vm</code> 내 SSMS를 실행하여 데이터베이스와 테이블을 생성합니다.(동일하게 SQL-vm의 rdp 파일 설치 후 실행)
<img src="https://velog.velcdn.com/images/rudin_/post/647cd328-dd00-4409-893b-0586208c0bb3/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/7be1786b-d061-4eaa-b28e-31167f028d39/image.png" alt="">
Trust server certificate 체크
<img src="https://velog.velcdn.com/images/rudin_/post/617bef81-33c2-4906-9ae5-e5918389edb1/image.png" alt=""></li>
</ol>
<pre><code class="language-sql">    CREATE DATABASE StudentsDB;
    GO
    USE StudentsDB;

    CREATE TABLE StudentsPerformance (
        gender NVARCHAR(10),
        race_ethnicity NVARCHAR(20),
        parental_level_of_education NVARCHAR(50),
        lunch NVARCHAR(20),
        test_preparation_course NVARCHAR(20),
        math_score INT,
        reading_score INT,
        writing_score INT
    );</code></pre>
<h3 id="step-4-링크드-서비스-및-데이터세트-구성"><strong>Step 4: 링크드 서비스 및 데이터세트 구성</strong></h3>
<ol>
<li><p><strong>링크드 서비스 (원본)</strong>: <code>BlobStorage1</code> (AutoResolveIntegrationRuntime 사용).
<img src="https://velog.velcdn.com/images/rudin_/post/7f978c6a-4f4a-4754-bd59-26ee2a6e25aa/image.png" alt=""></p>
</li>
<li><p><strong>링크드 서비스 (싱크)</strong>: <code>vnetSqlServer1</code></p>
<ul>
<li>통합 런타임: <code>shir</code> 선택</li>
<li>서버 이름: SQL-vm의 프라이빗 IP (<code>10.0.1.4</code>)</li>
<li>데이터베이스: <code>StudentsDB</code></li>
</ul>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/rudin_/post/52b93b63-7087-49f9-a41d-e95ef2edae2b/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/74458228-205f-41a7-896f-e049080f3223/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/16c0760b-375b-434c-9fad-c73f13e36ad2/image.png" alt=""></p>
<ol start="3">
<li><strong>데이터세트</strong>:<ul>
<li>원본: <code>StudentsInputDS</code> (DelimitedText)</li>
<li>싱크: <code>vnetStudentsDS1</code> (SQL Server 테이블)
<img src="https://velog.velcdn.com/images/rudin_/post/873b5e3e-0e9a-469e-a253-576fc5438631/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/7d4e9445-1cbb-484d-a1c4-5721fa101212/image.png" alt=""></li>
</ul>
</li>
</ol>
<h3 id="step-5-파이프라인-생성-및-실행"><strong>Step 5: 파이프라인 생성 및 실행</strong></h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/bdc5ebaa-aea8-45d9-b7e6-06d751988a29/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/2bfb9a5d-e595-4ef7-8b8c-f916e4f9c95e/image.png" alt=""></p>
<ol>
<li><strong>활동</strong>: <strong>Copy Data</strong> 활동(<code>Copy Blob to SQL</code>) 추가.</li>
<li><strong>매핑(Mapping)</strong>: 원본 CSV 컬럼과 SQL 테이블 컬럼을 매핑합니다 (gender, math_score 등). <code>스키마 가져오기</code> 후 매핑</li>
</ol>
<p><img src="https://velog.velcdn.com/images/rudin_/post/40917c50-739f-436c-bccb-b9c9ef257bbb/image.png" alt=""></p>
<ol start="3">
<li><strong>실행</strong>: 파이프라인을 <strong>게시</strong>한 후 <strong>디버그</strong>를 실행하여 &#39;성공&#39; 상태를 확인합니다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/rudin_/post/d0561eac-11e1-438f-8074-1da29850207c/image.png" alt=""></p>
<ol start="4">
<li><strong>결과 확인</strong>: SQL-vm에서 <code>SELECT TOP 50 * FROM StudentsPerformance;</code> 쿼리를 통해 데이터 복사 여부를 검증합니다.
<img src="https://velog.velcdn.com/images/rudin_/post/9e0c44fc-6bbd-4265-a03a-dae90a9ed5b5/image.png" alt=""></li>
</ol>
<hr>
<h2 id="5-실습-마무리-및-리소스-정리">5. 실습 마무리 및 리소스 정리</h2>
<p>실습 완료 후 비용 발생을 방지하기 위해 다음 리소스들을 반드시 삭제해야 합니다.</p>
<ul>
<li><strong>가상 머신</strong>: <code>SHIR-vm</code>, <code>SQL-vm</code></li>
<li><strong>관련 리소스</strong>: 공용 IP 주소, 네트워크 인터페이스(NIC), 디스크, 네트워크 보안 그룹(NSG)</li>
<li><strong>네트워크</strong>: <code>vnet-demo</code> (가상 네트워크)</li>
</ul>
<p><strong>주의</strong>: SHIR 노드가 설치된 VM을 삭제하면 ADF Studio에서 해당 통합 런타임 상태가 &#39;사용할 수 없음&#39;으로 표시됩니다. 필요하지 않은 경우 ADF 내의 통합 런타임과 연결된 서비스도 함께 삭제하여 정리합니다.</p>
<ol>
<li>SQL-VM</li>
<li>VM 2개</li>
<li>NIC</li>
<li>IP, NSG, Disk</li>
<li>V-NET 
순으로 삭제</li>
</ol>
<p>이후 연결된 서비스에서 vnetSqlServer1의 통합 런타임 연결을 AutoResolveIntegrationRuntime으로 변경하여 적용 후 데이터세트의 vnetStudentsDS1, 파이프라인의 CopyStudentPerformance 삭제, 게시 후 연결된서비스 마저 삭제</p>
<hr>
<hr>
<h1 id="azure-data-factory-lookup-및-foreach-활동을-활용한-동적-데이터-처리">[Azure Data Factory] Lookup 및 ForEach 활동을 활용한 동적 데이터 처리</h1>
<h2 id="1-개요">1. 개요</h2>
<h3 id="lookup-activity"><strong>Lookup Activity</strong></h3>
<p>Lookup 활동을 통해 다양한 데이터 소스에서 목록이나 데이터를 조회하여, 파이프라인의 동적인 데이터 처리 흐름을 설계할 수 있습니다. Lookup 활동은 반복 작업의 시작점이 되는 정보를 제공합니다.</p>
<ul>
<li><strong>Lookup 활동 개요</strong><ul>
<li>외부 데이터 소스(파일, 테이블 등)에서 데이터를 읽어오는 활동</li>
<li>파이프라인 내에서 동적으로 사용할 데이터 목록을 조회</li>
<li>다양한 데이터 소스와 호환됨</li>
</ul>
</li>
<li><strong>Lookup 활동의 주요 기능</strong><ul>
<li>파일 또는 테이블의 내용을 읽어와 결과를 파이프라인 변수로 저장</li>
<li>주로 목록(리스트) 형태의 데이터를 반환</li>
<li>결과를 다음 활동(예: ForEach)에 전달하여 반복 작업의 입력값으로 활용</li>
</ul>
</li>
<li><strong>활용 예시</strong><ul>
<li>데이터베이스 테이블에서 처리 대상 파일 목록 조회</li>
<li>Blob Storage 내 폴더의 파일 리스트 추출</li>
<li>JSON/CSV 파일에서 데이터 로드</li>
</ul>
</li>
</ul>
<h3 id="foreach-activity"><strong>ForEach Activity</strong></h3>
<p>ForEach 활동을 사용하면, 조회된 데이터 목록을 활용하여 각 항목별로 동일하거나 다양한 작업을 반복 실행할 수 있습니다. ForEach 활동을 통해 대량 데이터의 일괄 처리와 자동화가 가능합니다.</p>
<ul>
<li><strong>ForEach 활동 개요</strong><ul>
<li>반복 작업(Loop)을 수행하는 활동</li>
<li>입력받은 목록(배열, 리스트)의 각 항목에 대해 지정된 하위 활동 집합을 실행</li>
<li>Lookup 등 다른 활동의 결과를 받아 반복 처리에 활용</li>
</ul>
</li>
<li><strong>ForEach 활동의 주요 기능</strong><ul>
<li>배열(리스트) 형태의 입력 데이터에 대해 작업 반복</li>
<li>각 항목마다 복사, 변환, 로깅 등 다양한 하위 활동 실행 가능</li>
<li>병렬 또는 순차적(직렬) 실행 방식 선택 가능</li>
</ul>
</li>
<li><strong>활용 예시</strong><ul>
<li>여러 파일을 반복적으로 복사(Copy)</li>
<li>여러 테이블에 데이터 일괄 적재</li>
<li>개별 레코드/오브젝트마다 별도 처리 로직 실행</li>
</ul>
</li>
</ul>
<hr>
<h2 id="2-lookup---foreach-절차-및-예시">2. Lookup - ForEach 절차 및 예시</h2>
<h3 id="실행-절차"><strong>실행 절차</strong></h3>
<ol>
<li><strong>Lookup 활동 실행</strong>: 외부 데이터 소스(예: 데이터베이스, Blob Storage 등)에서 처리 대상 목록을 읽어옴</li>
<li><strong>결과값 전달</strong>: Lookup의 결과(리스트)를 ForEach 활동에 입력</li>
<li><strong>ForEach 반복 실행</strong>: 리스트의 각 항목마다 Copy, 변환, 알림 등 지정된 하위 작업을 순차적/병렬로 실행</li>
<li><strong>각 항목별 결과 처리</strong>: 성공/실패 로깅, 후속 작업 연결 등</li>
</ol>
<h3 id="활용-예시"><strong>활용 예시</strong></h3>
<ul>
<li>여러 파일을 일괄 데이터베이스에 적재</li>
<li>테이블 행별로 데이터 처리 반복</li>
<li>여러 시스템에 동일 처리 반복</li>
</ul>
<hr>
<h2 id="3-실습-준비">3. 실습 준비</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/c40a5c30-0614-42c3-b10d-4129c9c4b285/image.png" alt=""></p>
<h3 id="3-1-리소스-구성"><strong>3-1. 리소스 구성</strong></h3>
<ul>
<li>리소스 그룹</li>
<li>데이터 팩터리(V2)</li>
<li>Logic app</li>
<li>스토리지 계정</li>
<li>SQL 데이터베이스</li>
<li>SQL Server</li>
</ul>
<h3 id="3-2-실습-데이터"><strong>3-2. 실습 데이터</strong></h3>
<ol>
<li><strong>Red Wine Quality</strong>: <code>winequality-red.csv</code> (또는 <code>wine.csv</code>)</li>
<li><strong>Titanic Dataset</strong>: <code>titanic.csv</code></li>
<li><strong>Adult Census Income</strong>: <code>adult.csv</code></li>
</ol>
<h3 id="3-3-목적지sink-컨테이너-준비"><strong>3-3. 목적지(Sink) 컨테이너 준비</strong></h3>
<ul>
<li>스토리지 계정 내에 <code>output</code> 컨테이너를 준비합니다.</li>
</ul>
<h3 id="3-4-소스-데이터베이스-테이블-생성"><strong>3-4. 소스 데이터베이스 테이블 생성</strong></h3>
<p>SQL 데이터베이스에서 다음 쿼리를 실행하여 테이블을 생성합니다.</p>
<p><strong>wine 테이블 생성</strong></p>
<pre><code class="language-sql">CREATE TABLE wine (
    fixed_acidity FLOAT,
    volatile_acidity FLOAT,
    citric_acid FLOAT,
    residual_sugar FLOAT,
    chlorides FLOAT,
    free_sulfur_dioxide FLOAT,
    total_sulfur_dioxide FLOAT,
    density FLOAT,
    pH FLOAT,
    sulphates FLOAT,
    alcohol FLOAT,
    quality INT
);</code></pre>
<p><strong>titanic 테이블 생성</strong></p>
<pre><code class="language-sql">CREATE TABLE titanic (
    PassengerId INT,
    Survived INT,
    Pclass INT,
    Name NVARCHAR(100),
    Sex NVARCHAR(10),
    Age FLOAT,
    SibSp INT,
    Parch INT,
    Ticket NVARCHAR(20),
    Fare FLOAT,
    Cabin NVARCHAR(20),
    Embarked NVARCHAR(5)
);</code></pre>
<p><strong>adult 테이블 생성</strong></p>
<pre><code class="language-sql">CREATE TABLE adult (
    age INT,
    workclass NVARCHAR(20),
    fnlwgt INT,
    education NVARCHAR(20),
    education_num INT,
    marital_status NVARCHAR(30),
    occupation NVARCHAR(20),
    relationship NVARCHAR(20),
    race NVARCHAR(20),
    sex NVARCHAR(10),
    capital_gain INT,
    capital_loss INT,
    hours_per_week INT,
    native_country NVARCHAR(30),
    income NVARCHAR(10)
);</code></pre>
<p>이후 컨테이너에 csv파일을 올리고, 이전 매개변수 실습에서 사용했던 파이프라인을 이용하여 해당 테이블에 csv의 데이터들을 넣고 input 스토리지에서 파일 삭제</p>
<hr>
<h2 id="4-실습-단계">4. 실습 단계</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/e5e3caa1-17cf-4cb6-9972-4e1da5ce4ac5/image.png" alt=""></p>
<h3 id="4-1-링크드-서비스-및-데이터세트-설정"><strong>4-1. 링크드 서비스 및 데이터세트 설정</strong></h3>
<p><strong>연결된 서비스(Linked Service)</strong></p>
<ul>
<li><strong>BlobStorage1</strong>: Azure Blob Storage 연결</li>
<li><strong>outputSQL</strong>: Azure SQL Database 연결</li>
</ul>
<p><strong>데이터세트(Dataset)</strong></p>
<ul>
<li><strong>TableListDS</strong>: SQL DB의 테이블 목록 조회용
<img src="https://velog.velcdn.com/images/rudin_/post/403ffd27-3c67-4e80-a198-f001c6d6fb48/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/19f22431-cc62-4c08-ac07-196ec68eb4f3/image.png" alt=""></li>
</ul>
<ul>
<li><p><strong>SourceTableDS</strong>: 매개변수(<code>schemaName</code>, <code>tableName</code>)를 사용하여 동적으로 테이블을 지정함</p>
<ul>
<li><code>schemaName</code> 식: <code>@dataset().schemaName</code></li>
<li><code>tableName</code> 식: <code>@dataset().tableName</code></li>
</ul>
<p><img src="https://velog.velcdn.com/images/rudin_/post/46714c85-001a-4ef1-acc5-30d4f282eb69/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/13c14cbf-da3b-4cc3-b34d-8caac7bd4b8d/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/fd59d369-c244-4ee6-aa2a-7da1a8281804/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/6a822919-faf3-4eb2-830b-7d6c1eaefc06/image.png" alt=""></p>
</li>
<li><p><strong>SinkCsvDS</strong>: 매개변수(<code>fileName</code>)를 사용하여 동적으로 출력 파일명을 지정함</p>
<ul>
<li><code>fileName</code> 식: <code>@dataset().fileName</code>
<img src="https://velog.velcdn.com/images/rudin_/post/134c2ede-8dc9-450e-ad14-a8fc885c2792/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/e97ceecd-2833-4d6c-a162-daabc3d2d6ab/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/60f6b19a-b224-4900-9b6a-7746e5abe96f/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/b21dae19-9083-4b0f-8420-d456556bf0eb/image.png" alt=""></li>
</ul>
</li>
</ul>
<h3 id="4-2-파이프라인-구성-lookup-활동"><strong>4-2. 파이프라인 구성: Lookup 활동</strong></h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/fe6dc1f4-48b9-465d-be16-4da921874370/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/cf280a8a-3316-45f5-b10c-20ba1d54e5a0/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/cfedd5c4-79eb-42a0-8cba-59499204ab19/image.png" alt=""></p>
<ul>
<li>활동 이름: <code>ListTables</code></li>
<li>원본 데이터세트: <code>TableListDS</code></li>
<li><strong>쿼리 실행</strong>:<pre><code class="language-sql">SELECT TABLE_SCHEMA, TABLE_NAME
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_TYPE = &#39;BASE TABLE&#39;
AND TABLE_SCHEMA = &#39;dbo&#39;</code></pre>
</li>
</ul>
<h3 id="4-3-파이프라인-구성-foreach-활동"><strong>4-3. 파이프라인 구성: ForEach 활동</strong></h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/e15fc362-47e8-47b1-9dfa-65b8608160e2/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/d11d4a98-b2f0-4aa3-8a06-3c58ef42d663/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/7c1820e3-3cb9-4da0-923e-6acfff0e1a34/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/e003a099-53c7-4de9-b9f1-3caf8f3532ab/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/6a886ae7-aa84-4abb-aa80-0b0158bbd2da/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/36d2b8e7-9994-4d88-8e12-5b7d0f7605ef/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/81a5a94f-a9de-403d-b80f-4762bb99cb6e/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/d6cd14dc-f038-45a2-913a-c2d340fe2aed/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/da7522b9-3f26-496d-b2e9-7c40561399ef/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/f4e401fb-372b-47bc-accd-c6b08179fe30/image.png" alt=""></p>
<ul>
<li>활동 이름: <code>ForEachTable</code></li>
<li><strong>항목(Items) 설정</strong>:<ul>
<li>식: <code>@activity(&#39;ListTables&#39;).output.value</code></li>
</ul>
</li>
<li><strong>하위 활동(Copy Data)</strong>:<ul>
<li>이름: <code>ExportTable</code></li>
<li><strong>원본(Source) 설정</strong>: <code>SourceTableDS</code><ul>
<li><code>tableName</code>: <code>@item().TABLE_NAME</code></li>
<li><code>schemaName</code>: <code>@item().TABLE_SCHEMA</code></li>
</ul>
</li>
<li><strong>싱크(Sink) 설정</strong>: <code>SinkCsvDS</code><ul>
<li><code>fileName</code>: <code>@concat(item().TABLE_NAME, &#39;.csv&#39;)</code></li>
</ul>
</li>
</ul>
</li>
</ul>
<hr>
<h2 id="5-결과-확인">5. 결과 확인</h2>
<ol>
<li><p><strong>파이프라인 실행</strong>: &#39;모두 게시&#39; 후 파이프라인을 실행합니다.
<img src="https://velog.velcdn.com/images/rudin_/post/b6b91470-682d-449e-8c14-8a95b06c2db9/image.png" alt=""></p>
</li>
<li><p><strong>모니터링</strong>: <code>ListTables</code> 성공 후 <code>ForEachTable</code> 내에서 각 테이블(<code>adult</code>, <code>titanic</code>, <code>wine</code>)에 대한 <code>ExportTable</code> 활동이 성공했는지 확인합니다.</p>
</li>
<li><p><strong>출력 확인</strong>: 스토리지의 <code>output</code> 컨테이너에 <code>adult.csv</code>, <code>titanic.csv</code>, <code>wine.csv</code> 파일이 생성되었는지 확인합니다.
<img src="https://velog.velcdn.com/images/rudin_/post/486d701f-8336-4bf1-8e81-a2d62554fcb3/image.png" alt=""></p>
</li>
</ol>
<hr>
<hr>
<h1 id="azure-data-factory-이메일-알림-email-notification">[Azure Data Factory] 이메일 알림 (Email Notification)</h1>
<h2 id="1-azure-data-factory-이메일-알림-개요">1. Azure Data Factory 이메일 알림 개요</h2>
<p>다양한 상황에서 Azure Data Factory의 데이터 처리 상태를 사용자에게 알릴 필요가 있습니다. 이메일 알림은 이런 요구를 충족시키기 위한 유용한 수단입니다.</p>
<h3 id="알림의-필요성"><strong>알림의 필요성</strong></h3>
<ul>
<li>데이터 파이프라인이 자동으로 수행되기 때문에, 처리 상태에 대한 가시성이 부족할 수 있음.</li>
<li>운영자가 실시간으로 상태를 확인하기 어렵기 때문에, 자동 알림을 통해 시스템 신뢰도를 높일 수 있음.</li>
<li>알림을 통해 문제 발생 시 즉각적인 조치가 가능하며, 운영 효율성 향상에 기여.</li>
</ul>
<h3 id="알림-사용-사례"><strong>알림 사용 사례</strong></h3>
<table>
<thead>
<tr>
<th align="left">분류</th>
<th align="left">사례</th>
<th align="left">설명</th>
</tr>
</thead>
<tbody><tr>
<td align="left">개발/운영</td>
<td align="left">ETL 오류 또는 성능 지연 발생</td>
<td align="left">운영팀에 오류 발생 사실 실시간 전달</td>
</tr>
<tr>
<td align="left">보안/감사</td>
<td align="left">민감한 데이터 이동 완료 시</td>
<td align="left">감사 로그 목적의 알림 전송</td>
</tr>
<tr>
<td align="left">데이터 분석</td>
<td align="left">외부 시스템에서 데이터 수집 시작 시</td>
<td align="left">자동화된 분석 시작 시점 감지 가능</td>
</tr>
<tr>
<td align="left">보고서 갱신</td>
<td align="left">데이터 변환 후 Power BI 리프레시 완료</td>
<td align="left">사용자에게 최신 리포트 반영 시점 안내</td>
</tr>
</tbody></table>
<hr>
<h2 id="2-이메일-알림-구현-방식">2. 이메일 알림 구현 방식</h2>
<p>Azure Data Factory는 이메일 알림을 위한 두 가지 구현 방식을 고려할 수 있습니다. 각 방식은 사용자의 목적에 따라 선택 가능합니다.</p>
<table>
<thead>
<tr>
<th align="left">옵션</th>
<th align="left">설명</th>
<th align="left">장점</th>
<th align="left">권장 시나리오</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>Azure Monitor Alerts</strong></td>
<td align="left">Azure에서 기본 제공하는 모니터링 및 경보 기능을 통해 이메일 알림을 전송</td>
<td align="left">설정 간편, 추가 비용 없음</td>
<td align="left">단순 성공/실패 모니터링, 장애 감지 시 알림</td>
</tr>
<tr>
<td align="left"><strong>Web Activity + Logic Apps</strong></td>
<td align="left">ADF의 Web Activity에서 Logic Apps 호출 후, HTTP 트리거 기반의 이메일 전송</td>
<td align="left">유연한 구성, 세부 커스터마이징 가능, HTML 이메일 가능</td>
<td align="left">알림 내용 및 대상 사용자 지정, 포맷 설정 필요 시</td>
</tr>
</tbody></table>
<ul>
<li><strong>Azure Monitor Alerts</strong>는 사전 정의된 메트릭 조건 기반으로 동작하며, 주로 파이프라인 실패나 시간 초과 같은 이벤트 감지에 적합합니다.</li>
<li><strong>Logic Apps</strong>는 REST API 호출을 기반으로 하므로, 이메일 제목, 수신자, 본문 내용을 동적으로 설정할 수 있습니다. 특히 Power BI 리포트 생성 완료 후 알림, 맞춤형 템플릿 발송 등 세밀한 알림 제어가 필요할 경우 매우 유용합니다.</li>
</ul>
<hr>
<h2 id="3-실습-1---azure-monitor를-이용한-알림">3. 실습 1 - Azure Monitor를 이용한 알림</h2>
<h3 id="3-1-azure-monitor-개요"><strong>3-1. Azure Monitor 개요</strong></h3>
<p>Azure Monitor는 Azure에서 기본 제공되는 종합 모니터링 및 알림 솔루션으로, 다양한 리소스 상태를 감시하고 이벤트 발생 시 알림을 보냅니다.</p>
<ul>
<li><strong>주요 기능</strong>: 클라우드 및 온프레미스 리소스 모니터링 가능, 응용/VM/DB/API 지원, Metric 기반 조건 설정 및 Alerts 트리거, Event Hub 및 Logic Apps 연동 가능,.</li>
<li><strong>구성 요소</strong>: 특정 Activity/Pipeline 상태 감지 → 메트릭 조건 충족 시 알림 생성 → 이메일, SMS, Logic Apps 채널로 전달.</li>
</ul>
<h3 id="3-2-azure-monitor-알림의-단점"><strong>3-2. Azure Monitor 알림의 단점</strong></h3>
<ul>
<li>이메일 포맷 변경 등은 제한적임.</li>
<li>고급 설정은 복잡함.</li>
<li>알림 전달까지 시간 지연이 있을 수 있음.</li>
<li>이메일의 가독성이 떨어질 수 있음.</li>
</ul>
<h3 id="3-3-실습-단계"><strong>3-3. 실습 단계</strong></h3>
<ol>
<li><p><strong>파이프라인 준비</strong>: <code>Lab - Email Alert</code> 내에 <code>AzureMonitorAlert</code> 파이프라인을 생성하고 실패를 유도하는 복사 활동을 구성합니다</p>
</li>
<li><p><strong>경고 규칙 생성</strong>: ADF 모니터링 탭의 [경고 및 메트릭]에서 [새로운 경고 규칙]을 클릭합니다.<img src="https://velog.velcdn.com/images/rudin_/post/fb288782-055c-4ba4-95c9-412cbd4ea7f5/image.png" alt=""></p>
</li>
<li><p><strong>조건 구성</strong>:</p>
<ul>
<li>메트릭: <code>Failed pipeline runs metrics</code>.</li>
<li>경고 논리 조건: &#39;보다 큼&#39;, 임계값 개수 &#39;0&#39;.</li>
<li>차원: <code>FailureType</code> (UserError, SystemError, BadGateway 선택).</li>
<li>평가 기준: 기간 &#39;지난 1분 동안&#39;, 빈도 &#39;1분마다&#39;.</li>
</ul>
</li>
<li><p><strong>알림 및 작업 그룹 구성</strong>:</p>
<ul>
<li>작업 그룹 이름: <code>Test group</code>.</li>
<li>알림 유형: &#39;이메일&#39; 선택 후 수신 메일 주소 입력</li>
</ul>
</li>
<li><p><strong>테스트 및 확인</strong>: 파이프라인 실행 후 실패가 발생하면 설정한 메일로 알림이 오는지 확인합니다. (디버그하면 안됨, 트리거 사용)</p>
</li>
</ol>
<p><strong>[수신 이메일 예시 - Activated]</strong></p>
<blockquote>
<p>Your Azure Monitor alert was triggered
Rule: copy-pipeline failure alert
Metric: PipelineFailedRuns
Value: 1
,</p>
</blockquote>
<p><strong>[수신 이메일 예시 - Deactivated]</strong></p>
<blockquote>
<p>Your Azure Monitor alert was resolved
Alert deactivated because one of the following conditions is no longer true.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/rudin_/post/7756de97-4a7a-49ba-9e95-48937b5d51a1/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/4007ddcd-e4d8-443d-96c9-762f377a2c08/image.png" alt=""></p>
<hr>
<h2 id="4-실습-2---web-activity와-logic-apps를-이용한-알림">4. 실습 2 - Web Activity와 Logic Apps를 이용한 알림</h2>
<h3 id="4-1-절차-및-장점"><strong>4-1. 절차 및 장점</strong></h3>
<ul>
<li><strong>절차</strong>: ADF 내 Web Activity에서 Logic Apps HTTP 트리거 호출 → Logic Apps에서 이메일 전송 및 응답 처리.</li>
<li><strong>장점</strong>: 내용 및 포맷 자유 구성 가능, 조건에 따른 분기 처리, HTML 이메일 구현 가능.</li>
</ul>
<h3 id="4-2-실습-준비"><strong>4-2. 실습 준비</strong></h3>
<ul>
<li><strong>복사 파이프라인</strong>: <code>Copy iris</code>, <code>Copy penguins</code> 활동을 포함하는 <code>LogicAppAlert</code> 파이프라인을 생성합니다,.</li>
<li><strong>데이터</strong>: <code>iris.csv</code>, <code>penguins.csv</code> 파일을 스토리지에 준비합니다,.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/rudin_/post/7339f4dc-594a-45c9-8ca8-587b837504e2/image.png" alt=""></p>
<h3 id="4-3-logic-app-생성-및-트리거-구성"><strong>4-3. Logic App 생성 및 트리거 구성</strong></h3>
<ol>
<li><strong>리소스 생성</strong>: Azure Portal에서 &#39;논리 앱(Logic App)&#39;을 검색하여 &#39;소비(Consumption)&#39; 플랜으로 생성합니다,,.</li>
<li><strong>HTTP 트리거 추가</strong>: 논리 앱 디자이너에서 &#39;When an HTTP request is received&#39; 트리거를 추가합니다.</li>
<li><strong>JSON 스키마 생성</strong>: 아래 샘플 페이로드를 사용하여 스키마를 생성합니다<pre><code>{
&quot;type&quot;: &quot;object&quot;,
&quot;properties&quot;: {
&quot;title&quot;: {
  &quot;type&quot;: &quot;string&quot;
},
&quot;message&quot;: {
  &quot;type&quot;: &quot;string&quot;
},
&quot;AdfName&quot;: {
  &quot;type&quot;: &quot;string&quot;
},
&quot;pipelineName&quot;: {
  &quot;type&quot;: &quot;string&quot;
},
&quot;pipelineRunID&quot;: {
  &quot;type&quot;: &quot;string&quot;
},
&quot;time&quot;: {
  &quot;type&quot;: &quot;string&quot;
}
}
}</code></pre><img src="https://velog.velcdn.com/images/rudin_/post/68508830-a418-4487-8ccd-8bbab0c08528/image.png" alt=""></li>
</ol>
<h3 id="4-4-adf-web-activity-설정"><strong>4-4. ADF Web Activity 설정</strong></h3>
<ol>
<li><strong>활동 추가</strong>: <code>Send OK Email</code> 이름의 Web 활동을 추가합니다.</li>
<li><strong>설정</strong>:<ul>
<li>URL: Logic App에서 생성된 HTTP POST URL.</li>
<li>메서드: <code>POST</code>.</li>
<li>본문(Body): 아래 동적 콘텐츠 식을 입력합니다<pre><code class="language-json">{
&quot;title&quot;: &quot;파이프라인 실행 완료 알림&quot;,
&quot;message&quot;: &quot;데이터 복사 파이프라인이 성공적으로 완료되었습니다.&quot;,
&quot;AdfName&quot;: &quot;@{pipeline().DataFactory}&quot;,
&quot;pipelineName&quot;: &quot;@{pipeline().Pipeline}&quot;,
&quot;pipelineRunID&quot;: &quot;@{pipeline().RunId}&quot;,
&quot;time&quot;: &quot;@{utcNow()}&quot;
}</code></pre>
<img src="https://velog.velcdn.com/images/rudin_/post/39c3a5ac-6ef4-4a58-ab2b-06f7fba44966/image.png" alt=""></li>
</ul>
</li>
</ol>
<p><img src="https://velog.velcdn.com/images/rudin_/post/077a1bf4-5621-4a58-b8e4-04a1e10c7fad/image.png" alt=""></p>
<h3 id="4-5-logic-app-이메일-동작-추가"><strong>4-5. Logic App 이메일 동작 추가</strong></h3>
<ol>
<li><strong>동작 추가</strong>: &#39;Outlook.com&#39; 커넥터의 &#39;메일 보내기(V2)&#39;를 선택합니다,.</li>
<li><strong>본문 구성</strong>: HTTP 트리거에서 받은 동적 콘텐츠를 사용하여 HTML 포맷으로 본문을 작성합니다.<ul>
<li>제목: <code>title</code></li>
<li>본문 예시:
```text</li>
<li>데이터 팩토리 이름: AdfName</li>
<li>파이프라인 이름: pipelineName</li>
<li>파이프라인 Run ID: pipelineRunID</li>
<li>실행 완료 시각: time<pre><code>
</code></pre></li>
</ul>
</li>
</ol>
<h3 id="4-6-결과-확인"><strong>4-6. 결과 확인</strong></h3>
<ul>
<li>성공 시 이메일 제목: <code>파이프라인 실행 완료 알림</code>.</li>
<li>본문 내용에 실제 ADF 리소스 이름과 실행 시각 등이 포함되어 전송됩니다.</li>
</ul>
<hr>
<h2 id="5-파이프라인-실패-알림-추가-심화">5. 파이프라인 실패 알림 추가 (심화)</h2>
<p>성공 알림뿐만 아니라 실패 시에도 알림을 보내기 위해 파이프라인을 확장합니다.</p>
<ol>
<li><strong>실패용 활동 추가</strong>: <code>send NOK Email</code> 이름의 Web 활동을 생성하고 실패 경로(빨간색 선)로 연결합니다.</li>
<li><strong>본문 설정</strong>:<pre><code class="language-json">{
  &quot;title&quot;: &quot;파이프라인 실행 실패 알림&quot;,
  &quot;message&quot;: &quot;데이터 복사 파이프라인의 실행이 실패하였습니다.&quot;,
  &quot;AdfName&quot;: &quot;@{pipeline().DataFactory}&quot;,
  &quot;pipelineName&quot;: &quot;@{pipeline().Pipeline}&quot;,
  &quot;pipelineRunID&quot;: &quot;@{pipeline().RunId}&quot;,
  &quot;time&quot;: &quot;@{utcNow()}&quot;
}</code></pre>
</li>
</ol>
<h3 id="master-파이프라인-활용"><strong>Master 파이프라인 활용</strong></h3>
<p>실제 운영 시에는 개별 파이프라인에 매번 알림을 넣기보다 <strong>MasterAlertPipeline</strong>을 구축하여 호출된 파이프라인의 에러 메시지를 전달하는 방식이 효율적입니다,.
<img src="https://velog.velcdn.com/images/rudin_/post/298c1338-db3f-4706-93ac-2b5718a67e67/image.png" alt=""></p>
<ul>
<li><strong>실패 메시지 전달 식</strong>:<pre><code class="language-json">{
  &quot;title&quot;: &quot;파이프라인 실행 실패 알림&quot;,
  &quot;message&quot;: &quot;@{activity(&#39;Execute Pipeline1&#39;).error.message}&quot;,
  &quot;AdfName&quot;: &quot;@{pipeline().DataFactory}&quot;,
  &quot;pipelineName&quot;: &quot;@{pipeline().Pipeline}&quot;,
  &quot;pipelineRunID&quot;: &quot;@{pipeline().RunId}&quot;,
  &quot;time&quot;: &quot;@{utcNow()}&quot;
}</code></pre>
<img src="https://velog.velcdn.com/images/rudin_/post/f583d191-7a56-4f80-9d1a-b59b85efae9c/image.png" alt=""></li>
</ul>
<h3 id="최종-테스트-결과-실패-시"><strong>최종 테스트 결과 (실패 시)</strong></h3>
<ul>
<li>원본 파일을 삭제한 후 실행하면 다음과 같은 에러 메시지가 포함된 이메일이 수신됩니다.
<img src="https://velog.velcdn.com/images/rudin_/post/0410f9de-cabb-45e8-a155-b5b12800fc20/image.png" alt=""></li>
</ul>
<hr>
<h2 id="6-실습-마무리">6. 실습 마무리</h2>
<p>실습이 끝나면 불필요한 비용 발생을 방지하기 위해 생성한 모든 리소스(Logic App, API Connection, Action Group 등)와 리소스 그룹을 삭제합니다,.</p>
<hr>
<hr>
<h1 id="azure-data-factory-필터-및-정렬-변환-filter-sort-transformation">[Azure Data Factory] 필터 및 정렬 변환 (Filter-Sort Transformation)</h1>
<h2 id="1-개요-1">1. 개요</h2>
<p>데이터 변환 과정에서 특정 조건에 맞는 데이터를 선별하거나, 분석 효율을 높이기 위해 데이터를 정렬하는 과정은 필수적입니다. Azure Data Factory의 매핑 데이터 플로우(MDF)는 이를 시각적으로 구성할 수 있는 기능을 제공합니다.</p>
<h3 id="필터-변환-filter-transformation"><strong>필터 변환 (Filter Transformation)</strong></h3>
<p>필터 변환은 소스 데이터에서 지정한 조건을 만족하는 행(row)만 선택해 통과시키는 변환 단계입니다.</p>
<ul>
<li><strong>구성 요소</strong>:<ul>
<li><strong>조건식(Condition Expression)</strong>: 특정 컬럼에 대한 비교 연산자(=, &lt;, &gt;, &lt;=, &gt;=, != 등) 및 논리 연산자(and, or)를 사용합니다.<ul>
<li>예) <code>Score &gt; 80</code>, <code>Category == &#39;A&#39; and Region != &#39;Seoul&#39;</code></li>
</ul>
</li>
<li><strong>미리보기(Data Preview)</strong>: 조건식 적용 후 결과를 즉시 확인할 수 있습니다.</li>
</ul>
</li>
<li><strong>장점</strong>:<ul>
<li>불필요한 데이터를 미리 제거하여 후속 처리 성능을 향상시킵니다.</li>
<li>비즈니스 로직에 따른 데이터 선별을 시각적으로 구성 가능합니다.</li>
</ul>
</li>
<li><strong>사용 시나리오</strong>:<ul>
<li>특정 기준 점수 이상인 학생만 추출</li>
<li>거래 상태가 &#39;완료(Completed)&#39;인 주문만 처리</li>
<li>결측치(null)나 이상치(outlier)를 제거</li>
</ul>
</li>
</ul>
<h3 id="정렬-변환-sort-transformation"><strong>정렬 변환 (Sort Transformation)</strong></h3>
<p>정렬 변환은 입력된 데이터 세트를 하나 이상의 컬럼을 기준으로 오름차순 또는 내림차순으로 정렬하는 변환 단계입니다.</p>
<ul>
<li><strong>구성 요소</strong>:<ul>
<li><strong>정렬 키(Sort Key)</strong>: 정렬 기준이 되는 컬럼을 선택합니다. 다중 컬럼 지정 시 우선순위에 따라 차례대로 정렬됩니다.</li>
<li><strong>정렬 순서(Order)</strong>: 오름차순(Ascending) 또는 내림차순(Descending)을 선택합니다.</li>
<li><strong>미리보기(Data Preview)</strong>: 정렬 결과를 즉시 확인할 수 있습니다.</li>
</ul>
</li>
<li><strong>장점</strong>:<ul>
<li>사용자 요구에 따른 출력 순서를 제어합니다.</li>
<li>데이터 집계나 순위 분석 전에 데이터 순서를 명확히 정의합니다.</li>
</ul>
</li>
<li><strong>사용 시나리오</strong>:<ul>
<li>시험 점수를 높은 순서대로 정렬해 상위 10명 출력</li>
<li>거래 일자 기준으로 과거 → 최신 순으로 정렬</li>
<li>고객 등급과 가입 일자를 복합 기준으로 정렬</li>
</ul>
</li>
</ul>
<hr>
<h2 id="2-실습-준비">2. 실습 준비</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/5db2db62-2016-4ba6-8493-ebc43cf1967e/image.png" alt=""></p>
<h3 id="실습-구성"><strong>실습 구성</strong></h3>
<ul>
<li><strong>SQL 데이터베이스</strong>: <code>dbo.adult</code> 테이블 사용 (age, workclass, income 등 포함).</li>
<li><strong>Data Flow</strong>: <ol>
<li><strong>Filter</strong>: <code>income == &quot;&gt;50K&quot;</code> (고소득자 데이터 선별).</li>
<li><strong>Sort</strong>: 나이(age) 기준으로 오름차순 정렬.</li>
</ol>
</li>
<li><strong>스토리지 계정</strong>: <code>adult_over50K_sorted.csv</code> 파일로 저장.</li>
</ul>
<h3 id="데이터-및-테이블-준비"><strong>데이터 및 테이블 준비</strong></h3>
<ol>
<li><strong>실습 데이터</strong>: Adult Census Income (Kaggle 데이터).</li>
<li><strong>테이블 생성 쿼리</strong> (필요 시 실행):<pre><code class="language-sql">CREATE TABLE adult (
    age INT,
    workclass NVARCHAR(20),
    fnlwgt INT,
    education NVARCHAR(20),
    education_num INT,
    marital_status NVARCHAR(30),
    occupation NVARCHAR(20),
    relationship NVARCHAR(20),
    race NVARCHAR(20),
    sex NVARCHAR(10),
    capital_gain INT,
    capital_loss INT,
    hours_per_week INT,
    native_country NVARCHAR(30),
    income NVARCHAR(10)
);</code></pre>
</li>
<li><strong>데이터 확인</strong>: <code>select count(*) from [dbo].[adult]</code> 실행 시 32,561건 확인.</li>
<li><strong>목적지 컨테이너</strong>: 스토리지 계정 내 <code>output</code> 컨테이너 준비.</li>
</ol>
<h3 id="링크드-서비스-및-데이터세트-구성"><strong>링크드 서비스 및 데이터세트 구성</strong></h3>
<ul>
<li><strong>링크드 서비스</strong>: <code>outputSQL</code> (Azure SQL DB), <code>BlobStorage1</code> (Azure Blob Storage).</li>
<li><strong>소스 데이터세트 (<code>AdultSqlInput_DS</code>)</strong>: SQL DB의 <code>dbo.adult</code> 테이블 연결.</li>
<li><strong>싱크 데이터세트 (<code>AdultCsvOutput_DS</code>)</strong>: Blob Storage의 <code>output</code> 컨테이너 연결.</li>
</ul>
<hr>
<h2 id="3-실습-1-필터-및-정렬-변환-구성">3. [실습 1] 필터 및 정렬 변환 구성</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/a63f2054-7b0b-4680-b63e-24c36c237a4d/image.png" alt=""></p>
<h3 id="3-1-데이터-플로우-생성-및-소스-추가"><strong>3-1. 데이터 플로우 생성 및 소스 추가</strong></h3>
<ol>
<li><strong>매핑 데이터 플로우 생성</strong>: 이름 <code>FilterSort_DF</code>.</li>
<li><strong>데이터 흐름 디버그 켜기</strong>: Small 크기, 1시간 TTL 설정.</li>
<li><strong>소스 추가</strong>:<ul>
<li>출력 스트림 이름: <code>adult</code>.</li>
<li>데이터 세트: <code>AdultSqlInput_DS</code>.</li>
<li><strong>프로젝션</strong>: age(integer), workclass(string), income(string) 등 형식 확인.</li>
</ul>
</li>
</ol>
<h3 id="3-2-필터-변환-설정-filterhighincome"><strong>3-2. 필터 변환 설정 (<code>FilterHighIncome</code>)</strong></h3>
<ol>
<li><strong>필터 활동 추가</strong>: 소스 뒤에 <code>Filter</code> 활동 연결.</li>
<li><strong>속성 설정</strong>:<ul>
<li>출력 스트림 이름: <code>FilterHighIncome</code>.</li>
<li>들어오는 스트림: <code>adult</code>.</li>
</ul>
</li>
<li><strong>필터 식 입력</strong> (식 작성기 활용):<pre><code>income == &quot;&gt;50K&quot;</code></pre></li>
<li><strong>미리보기</strong>: 데이터가 <code>&gt;50K</code>인 행만 남는지 확인.</li>
</ol>
<h3 id="3-3-정렬-변환-설정-sortbyage"><strong>3-3. 정렬 변환 설정 (<code>SortByAge</code>)</strong></h3>
<ol>
<li><strong>정렬 활동 추가</strong>: 필터 활동 뒤에 <code>Sort</code> 활동 연결.</li>
<li><strong>속성 설정</strong>:<ul>
<li>출력 스트림 이름: <code>SortByAge</code>.</li>
<li>들어오는 스트림: <code>FilterHighIncome</code>.</li>
</ul>
</li>
<li><strong>정렬 조건</strong>:<ul>
<li>열: <code>age</code>.</li>
<li>순서: <code>오름차순 (Ascending)</code>.</li>
</ul>
</li>
<li><strong>미리보기</strong>: 나이가 적은 순서(22, 23, 24...)로 정렬되는지 확인.</li>
</ol>
<h3 id="3-4-싱크-설정-및-파이프라인-실행"><strong>3-4. 싱크 설정 및 파이프라인 실행</strong></h3>
<ol>
<li><strong>싱크 추가</strong>: <code>AdultHighIncomeSortedSink</code> 추가 및 <code>AdultCsvOutput_DS</code> 연결.</li>
<li><strong>설정</strong>: &#39;단일 파티션&#39; 지정 후 <strong>단일 파일로 출력</strong> 선택, 파일명 <code>adult_highincome_sorted.csv</code> 입력.</li>
<li><strong>파이프라인 생성</strong>: <code>FilterSortAdult_PL</code> 생성 후 데이터 플로우 활동 추가.</li>
<li><strong>실행 및 확인</strong>: 파이프라인 성공 후 <code>output</code> 컨테이너에서 결과 파일 확인.</li>
</ol>
<hr>
<h2 id="4-실습-2-필터-및-정렬-매개변수화-advanced">4. [실습 2] 필터 및 정렬 매개변수화 (Advanced)</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/18e76e7e-9d3a-4737-8035-ad65cc76a348/image.png" alt=""></p>
<p>고정된 값이 아닌, 실행 시점에 입력받은 값으로 필터링하고 정렬할 수 있도록 구성을 변경합니다.</p>
<h3 id="4-1-데이터-플로우-매개변수-정의"><strong>4-1. 데이터 플로우 매개변수 정의</strong></h3>
<p>매핑 데이터 플로우의 [매개 변수] 탭에서 다음 항목을 추가합니다.
<img src="https://velog.velcdn.com/images/rudin_/post/3f9be95e-95d0-4702-9f6b-6ed6b1437833/image.png" alt=""></p>
<ul>
<li><code>filterColumn</code> (string): 필터링할 컬럼 이름.</li>
<li><code>filterValue</code> (string): 필터링할 기준 값.</li>
<li><code>sortColumn</code> (string): 정렬할 컬럼 이름.</li>
</ul>
<h3 id="4-2-매개변수-기반-식-작성"><strong>4-2. 매개변수 기반 식 작성</strong></h3>
<ol>
<li><strong>필터 식 수정 (<code>FilterAdult</code>)</strong>:
컬럼의 데이터 형식을 고려하여 동적으로 비교하는 식을 작성합니다.<pre><code>case(
    type(byName($filterColumn))==&#39;Integer&#39;, toInteger(byName($filterColumn)) == toInteger($filterValue),
    toString(byName($filterColumn)) == $filterValue
)</code></pre></li>
<li><strong>정렬 식 수정 (<code>SortByParam</code>)</strong>:
정렬 조건의 열을 식 작성기에서 매개변수로 지정합니다.<pre><code>byName($sortColumn)</code></pre></li>
</ol>
<h3 id="4-3-파이프라인-매개변수-연결"><strong>4-3. 파이프라인 매개변수 연결</strong></h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/c42ad0a6-f976-4a3b-8cd9-3c88ecc2d770/image.png" alt=""></p>
<ol>
<li><strong>파이프라인 매개변수 생성</strong>: <code>pFilterColumn</code>, <code>pFilterValue</code>, <code>pSortColumn</code> 생성.</li>
<li><strong>매핑</strong>: 파이프라인 활동의 [매개 변수] 탭에서 데이터 플로우 매개변수와 파이프라인 매개변수를 연결합니다.<ul>
<li><code>filterColumn</code> = <code>@pipeline().parameters.pFilterColumn</code></li>
<li><code>filterValue</code> = <code>@pipeline().parameters.pFilterValue</code></li>
<li><code>sortColumn</code> = <code>@pipeline().parameters.pSortColumn</code></li>
</ul>
</li>
</ol>
<h3 id="4-4-실행-및-결과-확인"><strong>4-4. 실행 및 결과 확인</strong></h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/db67b520-3b4f-46d7-9942-4335e3662296/image.png" alt=""></p>
<ul>
<li><strong>지금 트리거</strong> 클릭 후 매개변수 값 입력 예시:<ul>
<li><code>pFilterColumn</code>: <code>age</code></li>
<li><code>pFilterValue</code>: <code>52</code></li>
<li><code>pSortColumn</code>: <code>hours_per_week</code></li>
</ul>
</li>
<li><strong>결과</strong>: 나이가 52세인 데이터들만 추출되어 주당 근무 시간순으로 정렬된 <code>adult_filtered_sorted.csv</code> 파일이 생성됩니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/rudin_/post/283c94f2-661c-4bcb-bcbb-66007cc3a83d/image.png" alt=""></p>
<hr>
<p><strong>실습 마무리</strong>: 모든 작업이 완료되면 비용 발생 방지를 위해 <strong>데이터 흐름 디버그 모드</strong>를 반드시 중지하십시오.</p>
<hr>
<hr>
<h1 id="azure-data-factory-메타데이터-활동-get-metadata-activity">[Azure Data Factory] 메타데이터 활동 (Get Metadata Activity)</h1>
<h2 id="1-메타데이터-활동get-metadata-activity-개요">1. 메타데이터 활동(Get Metadata Activity) 개요</h2>
<p>Get Metadata 활동은 데이터 소스(파일, 폴더, 테이블 등)의 메타데이터(크기, 수정일, 컬럼 목록 등)를 조회하는 데이터팩토리의 처리 단계입니다.</p>
<h3 id="구성-요소"><strong>구성 요소</strong></h3>
<ul>
<li><strong>데이터셋(Dataset)</strong>: 데이터 소스의 연결 정보 및 경로를 지정합니다.</li>
<li><strong>조회할 필드(Field list)</strong>: 다음과 같은 필요한 메타데이터 항목을 선택합니다.<ul>
<li><code>childItems</code>: 폴더 내 하위 항목 목록</li>
<li><code>exists</code>: 데이터 소스 존재 여부</li>
<li><code>lastModified</code>: 마지막 수정 시간</li>
<li><code>size</code>: 파일 크기</li>
<li><code>structure</code>: 데이터 구조(컬럼 목록) 등</li>
</ul>
</li>
<li><strong>필터(Pattern/Field filter)</strong>: 특정 파일 확장자나 경로 패턴에 따라 조회 대상을 제한합니다.</li>
</ul>
<h3 id="장점"><strong>장점</strong></h3>
<ul>
<li><strong>동적 분기 처리</strong>: 메타데이터를 기반으로 조건 분기 및 재시도 로직 구현이 가능합니다.</li>
<li><strong>효율적 파이프라인 설계</strong>: 사전 검증을 통해 불필요한 복사 및 변환 작업을 예방합니다.</li>
<li><strong>재사용성</strong>: 공통 메타데이터 조회 로직을 모듈화하여 여러 파이프라인에서 동일하게 활용할 수 있습니다.</li>
</ul>
<h3 id="사용-시나리오"><strong>사용 시나리오</strong></h3>
<ul>
<li><strong>파일 존재 여부 확인</strong>: 파이프라인 실행 전 대상 파일이 있는지 분기 처리합니다.</li>
<li><strong>폴더 목록 조회</strong>: 폴더 내 파일, 하위 폴더 목록을 동적으로 파이프라인에 전달합니다.</li>
<li><strong>테이블 스키마 조회</strong>: 테이블 컬럼 구조를 미리 확인하여 후속 매핑 데이터 흐름에 활용합니다.</li>
</ul>
<hr>
<h2 id="2-실습-1-기본-메타데이터-조회">2. [실습 1] 기본 메타데이터 조회</h2>
<h3 id="실습-준비"><strong>실습 준비</strong></h3>
<ul>
<li><strong>실습 데이터</strong>: UC Irvine Machine Learning Repository의 <strong>Wine Quality</strong> 데이터셋을 활용합니다.</li>
<li><strong>데이터 특성</strong>: 레드 와인(<code>winequality-red.csv</code>)과 화이트 와인(<code>winequality-white.csv</code>)의 화학적 테스트 결과 데이터입니다.</li>
<li><strong>데이터 형식</strong>: 세미콜론(<code>;</code>)을 구분자로 사용하는 CSV 파일입니다.</li>
<li><strong>스토리지 구성</strong>: <code>wine-quality</code> 컨테이너 내에 위 두 파일을 업로드합니다.</li>
</ul>
<h3 id="파이프라인-및-데이터-세트-생성"><strong>파이프라인 및 데이터 세트 생성</strong></h3>
<ol>
<li><strong>파이프라인 생성</strong>: <code>GetMetadataWine_PL</code> 파이프라인을 생성하고 <strong>메타데이터 가져오기</strong> 활동을 추가합니다.</li>
<li><strong>데이터 세트 설정 (<code>wineContainer_DS</code>)</strong>:<ul>
<li>형식: Delimited Text (Azure Blob Storage).</li>
<li>연결된 서비스: <code>BlobStorage1</code>.</li>
<li>파일 경로: <code>wine-quality</code> 컨테이너 지정.</li>
<li>열 구분 기호: <strong>Semicolon (;)</strong> 설정.</li>
</ul>
</li>
</ol>
<h3 id="필드-목록-설정-및-실행"><strong>필드 목록 설정 및 실행</strong></h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/8dbfdcaa-a920-4e9e-ba19-877ba4e6a7ee/image.png" alt=""></p>
<ul>
<li>활동의 <strong>설정</strong> 탭에서 <strong>필드 목록</strong>을 다음과 같이 추가합니다.<ul>
<li><code>exists</code></li>
<li><code>lastModified</code></li>
<li><code>childItems</code></li>
</ul>
</li>
<li><strong>결과 확인 (JSON 출력)</strong>:
<img src="https://velog.velcdn.com/images/rudin_/post/37b6a901-33b9-4390-b633-20f8d7a77b5b/image.png" alt=""></li>
</ul>
<hr>
<h2 id="3-실습-2-메타데이터를-활용한-동적-파일-복사">3. [실습 2] 메타데이터를 활용한 동적 파일 복사</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/0e24f86e-5826-40f8-9385-3d56a5c5b95e/image.png" alt=""></p>
<p>조회된 메타데이터 목록을 바탕으로 특정 조건(파일명에 &#39;red&#39; 포함)에 맞는 파일만 다른 컨테이너로 복사하는 실습입니다.</p>
<h3 id="전체-흐름"><strong>전체 흐름</strong></h3>
<ol>
<li><strong>Get Metadata</strong>: 컨테이너 내 파일 목록 확인.</li>
<li><strong>ForEach</strong>: 파일 목록을 순회하며 반복.</li>
<li><strong>If Condition</strong>: 파일 이름에 &#39;red&#39;가 포함되어 있는지 확인.</li>
<li><strong>Copy Data</strong>: 조건이 참인 경우 <code>wine-quality-output</code> 컨테이너로 복사.</li>
</ol>
<h3 id="활동별-세부-설정"><strong>활동별 세부 설정</strong></h3>
<h4 id="1-foreach-활동-foreachwinefiles"><strong>1) ForEach 활동 (<code>ForEachWineFiles</code>)</strong></h4>
<ul>
<li><strong>항목(Items)</strong> 설정 식:<pre><code>@activity(&#39;WineFiles&#39;).output.childItems</code></pre></li>
</ul>
<h4 id="2-if-condition-활동-if-red"><strong>2) If Condition 활동 (<code>If Red</code>)</strong></h4>
<ul>
<li><strong>식(Expression)</strong> 설정 식:<pre><code>@contains(item().name, &#39;red&#39;)</code></pre></li>
</ul>
<h4 id="3-copy-data-활동-copy-red-wine"><strong>3) Copy Data 활동 (<code>Copy Red Wine</code>)</strong></h4>
<ul>
<li><strong>원본 데이터 세트 (<code>wineContainerInput_DS</code>)</strong>:<ul>
<li>매개 변수: <code>fileName</code> 생성.</li>
<li>연결 설정 식: <code>@dataset().fileName</code>.</li>
<li>활동 내 값 매핑: <code>@item().name</code>.</li>
</ul>
</li>
<li><strong>싱크 데이터 세트 (<code>wineContainerOutput_DS</code>)</strong>:<ul>
<li>파일 경로: <code>wine-quality-output</code>.</li>
<li>매개 변수: <code>fileName</code> 생성.</li>
<li>활동 내 값 매핑: <code>@item().name</code>.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="4-실행-및-결과-검증">4. 실행 및 결과 검증</h2>
<h3 id="실행-결과-모니터링"><strong>실행 결과 모니터링</strong></h3>
<ul>
<li><strong>WineFiles</strong>: 성공 (메타데이터 조회).</li>
<li><strong>ForEachWineFiles</strong>: 성공 (반복 처리).</li>
<li><strong>If Red</strong>: 성공 (조건 판단 - 두 개의 파일에 대해 각각 실행).</li>
<li><strong>Copy Red Wine</strong>: 성공 (조건이 참인 &#39;red&#39; 파일에 대해서만 실행).</li>
</ul>
<h3 id="최종-데이터-확인"><strong>최종 데이터 확인</strong></h3>
<ul>
<li><code>wine-quality-output</code> 컨테이너에 <code>winequality-red.csv</code> 파일이 정상적으로 복사되었음을 확인합니다.</li>
<li>복사된 파일의 내용을 미리 보기 하여 데이터 정합성을 확인합니다.</li>
</ul>
<hr>
<h2 id="5-실습-마무리">5. 실습 마무리</h2>
<ul>
<li>실습이 완료된 후에는 비용 발생 방지를 위해 <strong>데이터 흐름 디버그 모드를 반드시 중지</strong>합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MicrosoftDataSchool] 58일차 - AzureDataFactory, 매개변수, 트리거]]></title>
            <link>https://velog.io/@rudin_/MicrosoftDataSchool-58%EC%9D%BC%EC%B0%A8-AzureDataFactory-%EB%A7%A4%EA%B0%9C%EB%B3%80%EC%88%98-%ED%8A%B8%EB%A6%AC%EA%B1%B0</link>
            <guid>https://velog.io/@rudin_/MicrosoftDataSchool-58%EC%9D%BC%EC%B0%A8-AzureDataFactory-%EB%A7%A4%EA%B0%9C%EB%B3%80%EC%88%98-%ED%8A%B8%EB%A6%AC%EA%B1%B0</guid>
            <pubDate>Mon, 30 Mar 2026 08:29:16 GMT</pubDate>
            <description><![CDATA[<p>ETL: Extract-Transform-Load
데이터의 최종 목적은 sink
Azure Data Factory는 보고서를 작성하는데 기초되는 데이터의 반복적 적재에 사용</p>
<h1 id="azure-data-factory-개요-정리">Azure Data Factory 개요 정리</h1>
<p>Azure Data Factory는 다양한 데이터 소스에서 데이터를 수집하고, 필요한 형태로 이동·변환·적재하는 데이터 통합 서비스이다. 이번 정리는 ADF의 개념, ETL/ELT 배경, 핵심 구성 요소, 그리고 Blob Storage의 CSV 데이터를 SQL Database로 복사하는 기본 실습 흐름까지 한 번에 정리한 내용이다.</p>
<hr>
<h1 id="1-데이터와-ai-시대">1. 데이터와 AI 시대</h1>
<ul>
<li><p>데이터 기반 의사결정 중요성 증가함</p>
</li>
<li><p>기업은 데이터로부터 비즈니스 인사이트 도출 필요함</p>
</li>
<li><p>데이터 활용 목적:</p>
<ul>
<li>고객 성향 분석</li>
<li>사회·경제 변화 분석</li>
<li>비즈니스 전략 수립</li>
</ul>
</li>
</ul>
<p>데이터를 수집하고 저장하는 것만으로는 충분하지 않고, 분석에 적합한 형태로 가공한 뒤 실제 의사결정에 연결해야 가치가 생긴다. 자료에서도 <code>수집/변환/저장 → 데이터 분석 → 비즈니스 인사이트 도출</code> 흐름으로 설명한다. </p>
<hr>
<h1 id="2-데이터-정의-및-유형">2. 데이터 정의 및 유형</h1>
<h2 id="데이터-정의">데이터 정의</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>위키백과</td>
<td>양, 품질, 사실, 통계 등의 형태로 된 의미의 단위</td>
</tr>
<tr>
<td>옥스포드 컴퓨터 용어 사전</td>
<td>프로그램을 운용할 수 있는 형태로 기호화·숫자화한 자료</td>
</tr>
<tr>
<td>네이버 사전</td>
<td>이론을 세우는 데 기초가 되는 사실 또는 바탕 자료</td>
</tr>
<tr>
<td>옥스포드 대사전</td>
<td>추론과 추정의 근거를 이루는 사실</td>
</tr>
</tbody></table>
<h2 id="데이터-유형">데이터 유형</h2>
<table>
<thead>
<tr>
<th>유형</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>정성적 데이터</td>
<td>언어, 문자 등 비정형 데이터</td>
</tr>
<tr>
<td>정량적 데이터</td>
<td>숫자, 도형, 기호 등 정형 데이터</td>
</tr>
<tr>
<td>암묵지</td>
<td>학습, 체험 등으로 개인이 습득한 무형 지식</td>
</tr>
<tr>
<td>형식지</td>
<td>문서화되어 전달·공유가 가능한 지식</td>
</tr>
</tbody></table>
<p>정형 데이터는 저장·검색·분석에 유리하고, 비정형 데이터는 활용 가치가 크지만 전처리와 통합이 더 어렵다. </p>
<hr>
<h1 id="3-데이터와-정보-dikw-구조">3. 데이터와 정보: DIKW 구조</h1>
<h2 id="dikw-개념">DIKW 개념</h2>
<table>
<thead>
<tr>
<th>단계</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Data</td>
<td>관찰을 통해 수집된 원시 데이터</td>
</tr>
<tr>
<td>Information</td>
<td>정제·가공되어 의미가 부여된 데이터</td>
</tr>
<tr>
<td>Knowledge</td>
<td>연결된 정보 패턴을 이해하여 내재화한 결과</td>
</tr>
<tr>
<td>Wisdom</td>
<td>근본 원리에 대한 깊은 이해를 바탕으로 한 의사결정</td>
</tr>
</tbody></table>
<h2 id="예시">예시</h2>
<table>
<thead>
<tr>
<th>단계</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td>Data</td>
<td>A마트 식빵 100원, B마트 식빵 200원</td>
</tr>
<tr>
<td>Information</td>
<td>A마트가 B마트보다 식빵이 더 쌈</td>
</tr>
<tr>
<td>Knowledge</td>
<td>식빵은 A마트에서 사는 것이 좋음</td>
</tr>
<tr>
<td>Wisdom</td>
<td>다른 식료품도 A마트가 더 저렴할 가능성이 높음</td>
</tr>
</tbody></table>
<p>즉, 데이터는 그 자체로 끝나지 않고, 가공과 해석을 거쳐 정보·지식·지혜로 발전해야 실제 비즈니스 가치가 된다. </p>
<hr>
<h1 id="4-oltp와-olap">4. OLTP와 OLAP</h1>
<table>
<thead>
<tr>
<th>구분</th>
<th>OLTP</th>
<th>OLAP</th>
</tr>
</thead>
<tbody><tr>
<td>목적</td>
<td>실시간 데이터 처리</td>
<td>데이터 분석 및 의사결정</td>
</tr>
<tr>
<td>데이터 형태</td>
<td>원시 데이터</td>
<td>정제·집계된 데이터</td>
</tr>
<tr>
<td>구조</td>
<td>정규화된 스키마 중심</td>
<td>분석 친화적 구조</td>
</tr>
<tr>
<td>특징</td>
<td>거래 시스템 중심</td>
<td>다차원 분석 및 리포트 중심</td>
</tr>
</tbody></table>
<p>OLTP는 운영계 시스템이고, OLAP는 분석계 시스템이다. ADF는 주로 운영계의 데이터를 분석계 저장소로 이동시키는 역할과 맞닿아 있다. </p>
<hr>
<h1 id="5-adf-aml-bi의-역할">5. ADF, AML, BI의 역할</h1>
<table>
<thead>
<tr>
<th>단계</th>
<th>도구</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>수집·정제·결합</td>
<td>Azure Data Factory</td>
<td>ETL/ELT, 데이터 파이프라인 구축</td>
</tr>
<tr>
<td>분석·모델링</td>
<td>Azure Machine Learning</td>
<td>EDA, Feature Engineering, 모델 학습·예측</td>
</tr>
<tr>
<td>시각화·의사결정</td>
<td>Power BI</td>
<td>리포트, 대시보드, 결과 공유</td>
</tr>
</tbody></table>
<p>ADF는 데이터를 준비하는 계층이고, AML은 패턴을 학습하는 계층이며, Power BI는 결과를 보여주는 계층이라고 보면 이해가 쉽다. </p>
<hr>
<h1 id="6-데이터-수집·저장-시-고려사항">6. 데이터 수집·저장 시 고려사항</h1>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>파일 포맷</td>
<td>형식 변환이 필요한지 확인해야 함</td>
</tr>
<tr>
<td>질의 처리</td>
<td>쿼리 성능 및 실행 계획 확인 필요함</td>
</tr>
<tr>
<td>JSON 구조</td>
<td>스키마 변경 필요 여부 점검해야 함</td>
</tr>
<tr>
<td>결측치</td>
<td>누락 데이터 처리 기준 필요함</td>
</tr>
<tr>
<td>보안</td>
<td>민감 데이터 보호 방안 필요함</td>
</tr>
<tr>
<td>중복 데이터</td>
<td>여러 소스 통합 시 중복 제거 필요함</td>
</tr>
<tr>
<td>비용·인력</td>
<td>운영·이관에 드는 비용 고려해야 함</td>
</tr>
</tbody></table>
<p>자료에서는 이 과정을 복잡성, 정합성, 무결성, 보안성 문제로 정리한다. </p>
<hr>
<h1 id="7-데이터로부터-가치를-얻는-데-장애가-되는-요인">7. 데이터로부터 가치를 얻는 데 장애가 되는 요인</h1>
<ul>
<li>데이터 사일로: 부서, 시스템별로 데이터가 분리되어 있어 통합·분석이 어려움</li>
<li>이기종 데이터 형식: 정형·비정형 데이터를 모두 다뤄야 해 관리 복잡성 증가함</li>
<li>솔루션 복잡성: 여러 도구를 병행 운영하면 유지보수 부담 커짐</li>
<li>멀티 클라우드 환경: 클라우드별 API와 접근 방식이 달라 관리 비용 증가함</li>
<li>급증하는 운영 비용: 인프라, 도구, 인력 비용이 누적되어 전체 TCO 상승함</li>
</ul>
<p>핵심은 “데이터를 한곳에 통합하고, 권한 있는 사용자가 쉽게 활용할 수 있어야 한다”는 점이다. </p>
<hr>
<h1 id="8-가치-창출을-위한-데이터-환경-구축-요건">8. 가치 창출을 위한 데이터 환경 구축 요건</h1>
<table>
<thead>
<tr>
<th>요소</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>통합 데이터 허브</td>
<td>모든 데이터를 한 곳에 통합하고 다양한 형식을 지원해야 함</td>
</tr>
<tr>
<td>데이터 통합</td>
<td>원천 데이터를 ETL/ELT 방식으로 추출·변환·적재해야 함</td>
</tr>
<tr>
<td>Self-Service Access</td>
<td>사용자가 필요한 데이터를 손쉽게 조회·활용할 수 있어야 함</td>
</tr>
<tr>
<td>Right &amp; Responsibility</td>
<td>데이터 품질 책임과 활용 책임을 분리해 관리해야 함</td>
</tr>
</tbody></table>
<p>ADF는 이 중에서도 특히 <strong>데이터 통합</strong>을 담당하는 대표 도구로 볼 수 있다. </p>
<hr>
<h1 id="9-etl과-elt">9. ETL과 ELT</h1>
<h2 id="etl">ETL</h2>
<ul>
<li>Extract: 원천 시스템에서 데이터 추출함</li>
<li>Transform: 중간 단계에서 정제, 표준화, 집계 수행함</li>
<li>Load: 대상 시스템에 적재함</li>
</ul>
<h2 id="elt">ELT</h2>
<ul>
<li>Extract: 원천 시스템에서 데이터 추출함</li>
<li>Load: 우선 대상 시스템에 원시 데이터 적재함</li>
<li>Transform: 대상 시스템 내부에서 SQL, Spark 등으로 변환함</li>
</ul>
<h2 id="etl-vs-elt-비교">ETL vs ELT 비교</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>ETL</th>
<th>ELT</th>
</tr>
</thead>
<tbody><tr>
<td>처리 순서</td>
<td>추출 → 변환 → 적재</td>
<td>추출 → 적재 → 변환</td>
</tr>
<tr>
<td>변환 위치</td>
<td>외부 시스템</td>
<td>대상 시스템 내부</td>
</tr>
<tr>
<td>실행 시간</td>
<td>잦은 데이터 이동으로 상대적으로 느림</td>
<td>병렬 처리 활용 가능해 빠름</td>
</tr>
<tr>
<td>장점</td>
<td>정제된 상태로 적재 가능</td>
<td>대용량 처리와 클라우드 환경에 유리</td>
</tr>
<tr>
<td>유연성</td>
<td>정해진 파이프라인 중심</td>
<td>SQL/Spark로 유연하게 가공 가능</td>
</tr>
<tr>
<td>적합 환경</td>
<td>전통적인 DWH</td>
<td>고성능 DWH, 레이크하우스</td>
</tr>
<tr>
<td>활용 예시</td>
<td>금융기관 정기 보고서</td>
<td>로그, 센서, ML 분석용 데이터</td>
</tr>
</tbody></table>
<p>최근에는 대용량 비정형·반정형 데이터가 늘어나면서 ELT 방식이 더 자주 활용된다고 설명한다. </p>
<hr>
<h1 id="10-cdcchange-data-capture">10. CDC(Change Data Capture)</h1>
<h2 id="cdc-개념">CDC 개념</h2>
<p>CDC는 데이터 소스에서 발생한 변경 사항만 감지해 추출하고 반영하는 방식이다.</p>
<h2 id="cdc-처리-흐름">CDC 처리 흐름</h2>
<table>
<thead>
<tr>
<th>단계</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Detect</td>
<td>Insert, Update, Delete 같은 변경 이벤트 감지</td>
</tr>
<tr>
<td>Capture</td>
<td>변경 내용을 추출해 전달 가능한 형태로 준비</td>
</tr>
<tr>
<td>Apply</td>
<td>변경분만 대상 시스템에 반영해 동기화 유지</td>
</tr>
</tbody></table>
<h2 id="cdc-특징">CDC 특징</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>주요 목적</td>
<td>전체 재처리 없이 최신 상태 유지</td>
</tr>
<tr>
<td>처리 방식</td>
<td>실시간 또는 Near Real-Time</td>
</tr>
<tr>
<td>주요 기술</td>
<td>DB 로그, 트리거, 타임스탬프 비교, Debezium 등</td>
</tr>
<tr>
<td>장점</td>
<td>대용량 효율 처리, 실시간 분석 가능</td>
</tr>
<tr>
<td>활용 예시</td>
<td>실시간 대시보드, 복제 시스템, 이벤트 기반 아키텍처</td>
</tr>
</tbody></table>
<p>즉, CDC는 ETL/ELT의 배치 처리 한계를 보완하는 실시간 데이터 처리 방식이다. </p>
<hr>
<h1 id="11-데이터-파이프라인">11. 데이터 파이프라인</h1>
<h2 id="데이터-파이프라인-정의">데이터 파이프라인 정의</h2>
<p>데이터 파이프라인은 원천 시스템에서 분석·활용 시스템까지 이어지는 전체 데이터 흐름을 자동화하는 구조다. 수집, 처리, 저장, 전달 전 단계를 연결한다. </p>
<h2 id="데이터-파이프라인-단계">데이터 파이프라인 단계</h2>
<table>
<thead>
<tr>
<th>단계</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Ingest</td>
<td>파일, DB, API, IoT 등에서 데이터 수집</td>
</tr>
<tr>
<td>Process</td>
<td>정제, 필터링, 변환, 결측치 처리, 집계</td>
</tr>
<tr>
<td>Store</td>
<td>데이터 웨어하우스, 데이터 레이크 등에 저장</td>
</tr>
<tr>
<td>Deliver</td>
<td>대시보드, 분석 시스템, 모델링 시스템 등에 전달</td>
</tr>
</tbody></table>
<h2 id="데이터-파이프라인-특징">데이터 파이프라인 특징</h2>
<ul>
<li>자동화: 반복 작업을 자동 실행함</li>
<li>연속성: 흐름이 단계별로 끊기지 않음</li>
<li>확장성: 병렬 처리 및 클라우드 인프라 활용 가능함</li>
<li>신뢰성: 재처리, 오류 감지, 모니터링 가능함</li>
<li>실시간성: 스트리밍 처리도 가능함</li>
</ul>
<hr>
<h1 id="12-데이터-파이프라인-구성-요소">12. 데이터 파이프라인 구성 요소</h1>
<table>
<thead>
<tr>
<th>구성 요소</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Data Source</td>
<td>데이터베이스, 파일 시스템, API, 로그 등 원천 위치</td>
</tr>
<tr>
<td>Extract</td>
<td>소스에서 데이터를 읽어오는 단계</td>
</tr>
<tr>
<td>Transform</td>
<td>필터링, 조인, 포맷 변경, 집계 등 가공 단계</td>
</tr>
<tr>
<td>Load</td>
<td>대상(데이터 웨어하우스, 데이터레이크, NoSQL DB) 시스템에 저장하는 단계</td>
</tr>
<tr>
<td>Orchestration</td>
<td>전체 흐름 제어, 조건 분기, 재시도, 트리거 관리</td>
</tr>
<tr>
<td>Monitoring &amp; Alert</td>
<td>성공/실패 감시, 알림, 로깅, 성능 분석</td>
</tr>
<tr>
<td>Execution Environment</td>
<td>정의된 파이프라인을 실제 실행하는 컴퓨팅 환경</td>
</tr>
</tbody></table>
<p>ADF에서는 이 Execution Environment를 <strong>Integration Runtime</strong>이라고 부른다. </p>
<hr>
<h1 id="13-오케스트레이션과-트랜스포메이션">13. 오케스트레이션과 트랜스포메이션</h1>
<table>
  <thead>
    <tr>
      <th>구분</th>
      <th>구성 요소</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td rowspan="4">오케스트레이션</td>
      <td>워크플로우</td>
      <td>작업 순서와 흐름 정의</td>
    </tr>
    <tr>
      <td>트리거</td>
      <td>일정·이벤트·수동 실행 조건 설정</td>
    </tr>
    <tr>
      <td>조건 분기 및 반복</td>
      <td>조건에 따른 분기와 루프 제어</td>
    </tr>
    <tr>
      <td>에러 처리 및 알림</td>
      <td>실패 시 재시도, 로그, 알림 수행</td>
    </tr>
    <tr>
      <td rowspan="4">트랜스포메이션</td>
      <td>Extract</td>
      <td>데이터 추출</td>
    </tr>
    <tr>
      <td>Transform</td>
      <td>데이터 가공</td>
    </tr>
    <tr>
      <td>Load</td>
      <td>데이터 적재</td>
    </tr>
    <tr>
      <td>사용자 정의 로직</td>
      <td>커스텀 처리 코드 실행</td>
    </tr>
  </tbody>
</table>

<p>ADF는 특히 오케스트레이션에 강점이 있고, 복잡한 변환은 외부 컴퓨팅 서비스와 함께 사용하는 구조가 자주 등장한다. </p>
<hr>
<h1 id="14-execution-environment">14. Execution Environment</h1>
<table>
<thead>
<tr>
<th>도구/플랫폼</th>
<th>실행 환경 명칭</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Azure Data Factory</td>
<td>Integration Runtime</td>
<td>컴퓨팅/실행 엔진(Azure/Self-Hosted/SSIS)</td>
</tr>
<tr>
<td>AWS Glue</td>
<td>Job Worker / Spark Environment</td>
<td>Spark 기반 실행 환경</td>
</tr>
<tr>
<td>Apache Airflow</td>
<td>Worker / Executor</td>
<td>DAG를 실제로 실행하는 프로세스</td>
</tr>
<tr>
<td>Google Dataflow</td>
<td>Worker / Runner</td>
<td>파이프라인을 실행하는 관리형 워커 노드</td>
</tr>
<tr>
<td>Talend</td>
<td>Job Server</td>
<td>Talend Job 실행 환경</td>
</tr>
</tbody></table>
<p>파이프라인이 “무엇을 할지”를 정의한다면, Execution Environment는 “어디서 어떻게 실행할지”를 담당한다. </p>
<ul>
<li>Azure Integration Runtime: 클라우드상</li>
<li>Self-Hosted: 로컬환경상</li>
</ul>
<hr>
<h1 id="15-azure-data-factory-구성-요소">15. Azure Data Factory 구성 요소</h1>
<p><img src="https://velog.velcdn.com/images/rudin_/post/eaefee6b-3604-4111-ae09-e86bea8594fd/image.png" alt=""></p>
<p>자료에서는 ADF를 배송 시스템에 비유한다. 출발지 서류보관함에서 서류를 집하해 처리한 뒤 목적지 서류보관함에 전달하는 흐름으로 설명한다. 이 비유에서 파이프라인은 전체 배송 계획, 액티비티는 개별 운송 작업, 데이터셋은 다루는 서류 묶음, 링크드 서비스는 출발지·도착지 정보, Integration Runtime은 실제 배송을 수행하는 엔진에 해당한다. </p>
<h2 id="adf-핵심-구성-요소">ADF 핵심 구성 요소</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/90e83411-397b-454c-a5a5-b935d1000fc5/image.png" alt=""></p>
<ul>
<li>Linked Service는 저장의 추상화 <h2 id="pipeline과-activity-관계">Pipeline과 Activity 관계</h2>
<img src="https://velog.velcdn.com/images/rudin_/post/aa7d9fc7-2c48-4fc1-a86e-537ee0f0a37b/image.png" alt=""></li>
<li>그림에서는 Activity 중 Copy를 예시로 듬</li>
</ul>
<p>자료의 그림에서는 하나의 Pipeline 아래에 여러 Copy Activity가 들어갈 수 있고, 각 Activity는 입력 Dataset과 출력 Dataset을 가진다. 즉, 파이프라인은 큰 흐름이고 액티비티는 그 안에서 수행되는 세부 작업이다. 
(원본 → 싱크)</p>
<h2 id="pipeline과-linked-service-관계">Pipeline과 Linked Service 관계</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/9d72c636-e80a-4fc3-9d09-7d74cf88f337/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/180d03d3-bb42-46b2-b45b-8a893973db6c/image.png" alt=""></p>
<hr>
<h1 id="16-adf-실습-구성">16. ADF 실습 구성</h1>
<p>기본 실습의 전체 구조는 다음과 같다. 자료의 아키텍처 그림에서 <strong>Blob Storage의 CSV 파일을 SQL Database 테이블로 복사하는 구조</strong>를 보여준다. 중간에는 ADF Pipeline, Integration Runtime, Linked Service, Dataset, Copy Activity가 위치한다.</p>
<pre><code class="language-text">Blob Storage (CSV)
  ↓
Linked Service
  ↓
Source Dataset
  ↓
Copy Activity
  ↓
Sink Dataset
  ↓
Linked Service
  ↓
SQL Database (Table)</code></pre>
<h2 id="실습-아키텍처-구성-요소">실습 아키텍처 구성 요소</h2>
<table>
<thead>
<tr>
<th>구간</th>
<th>구성</th>
</tr>
</thead>
<tbody><tr>
<td>원본</td>
<td>Blob Storage의 CSV 파일</td>
</tr>
<tr>
<td>연결 정보</td>
<td>Blob Linked Service</td>
</tr>
<tr>
<td>원본 정의</td>
<td>Source Dataset</td>
</tr>
<tr>
<td>복사 작업</td>
<td>Copy Activity</td>
</tr>
<tr>
<td>목적지 정의</td>
<td>Sink Dataset</td>
</tr>
<tr>
<td>연결 정보</td>
<td>SQL Linked Service</td>
</tr>
<tr>
<td>목적지</td>
<td>SQL Database Table</td>
</tr>
<tr>
<td>실행 엔진</td>
<td>Integration Runtime</td>
</tr>
<tr>
<td>전체 제어</td>
<td>Pipeline</td>
</tr>
</tbody></table>
<hr>
<h1 id="17-실습">17. 실습</h1>
<h2 id="17-1-실습-데이터-다운로드">17-1. 실습 데이터 다운로드</h2>
<p>실습 데이터는 <code>iris.csv</code>와 <code>iris-columns.sql</code> 파일로 구성된다. <code>iris.csv</code>에는 SepalLength, SepalWidth, PetalLength, PetalWidth, Species 컬럼이 포함된 붓꽃 데이터가 들어 있다. </p>
<h2 id="17-2-리소스-그룹-생성">17-2. 리소스 그룹 생성</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/1948d126-0d9e-45eb-849a-1d1f30fb8655/image.png" alt=""></p>
<h2 id="17-3-adf-리소스-생성">17-3. ADF 리소스 생성</h2>
<p>ADF 생성 시 설정한 주요 항목은 다음과 같다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>설정 예시</th>
</tr>
</thead>
<tbody><tr>
<td>이름</td>
<td>영문자, 숫자, 하이픈 조합</td>
</tr>
<tr>
<td>지역</td>
<td>자유롭게 지정</td>
</tr>
<tr>
<td>버전</td>
<td>V2</td>
</tr>
<tr>
<td>리소스 그룹</td>
<td>방금 만든 그룹 선택</td>
</tr>
</tbody></table>
<p>배포가 완료되면 Data Factory 리소스의 첫 화면에서 Studio를 시작할 수 있다. </p>
<h2 id="17-4-sql-database-생성">17-4. SQL Database 생성</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/fb8393cf-02e3-4439-862d-2aaf444419d2/image.png" alt=""></p>
<p>SQL Database 생성 과정에서는 논리 서버도 함께 만든다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>설정 예시</th>
</tr>
</thead>
<tbody><tr>
<td>리소스 그룹</td>
<td>실습용 리소스 그룹</td>
</tr>
<tr>
<td>데이터베이스 이름</td>
<td>식별 가능한 이름</td>
</tr>
<tr>
<td>서버</td>
<td>새로 만들기</td>
</tr>
<tr>
<td>인증 방식</td>
<td>SQL 인증 사용</td>
</tr>
<tr>
<td>워크로드</td>
<td>개발</td>
</tr>
<tr>
<td>컴퓨팅 계층</td>
<td>DTU Basic</td>
</tr>
<tr>
<td>최대 크기</td>
<td>2GB</td>
</tr>
<tr>
<td>연결 방법</td>
<td>Public Endpoint</td>
</tr>
<tr>
<td>방화벽</td>
<td>Azure 서비스 허용, 현재 클라이언트 IP 허용</td>
</tr>
</tbody></table>
<h3 id="서버-만들기">서버 만들기</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/2070841d-d3c3-4305-9b68-c87cf6bbec87/image.png" alt=""></p>
<h3 id="컴퓨팅-구성">컴퓨팅 구성</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/7007d952-b6d7-45a0-99de-bdeb21adeb8c/image.png" alt=""></p>
<h3 id="네트워크-구성방화벽">네트워크 구성(방화벽)</h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/9bdc04d2-e3f5-4133-9317-95518468bda8/image.png" alt=""></p>
<ul>
<li>TLS 버전은 항상 최신으로 사용하는 것이 좋다<h2 id="17-5-storage-account-생성">17-5. Storage Account 생성</h2>
<img src="https://velog.velcdn.com/images/rudin_/post/452a3f90-4b90-4b8c-9a4e-8b63913bb7c6/image.png" alt=""></li>
</ul>
<p>Storage Account 생성 시 설정 항목은 다음과 같다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>설정 예시</th>
</tr>
</thead>
<tbody><tr>
<td>리소스 그룹</td>
<td>실습용 리소스 그룹</td>
</tr>
<tr>
<td>스토리지 계정 이름</td>
<td>기억하기 쉬운 이름</td>
</tr>
<tr>
<td>지역</td>
<td>ADF와 동일 지역</td>
</tr>
<tr>
<td>기본 스토리지 유형</td>
<td>Azure Blob Storage 또는 Azure Data Lake Storage Gen2</td>
</tr>
<tr>
<td>워크로드</td>
<td>기타</td>
</tr>
<tr>
<td>성능</td>
<td>표준</td>
</tr>
<tr>
<td>중복도</td>
<td>GRS</td>
</tr>
</tbody></table>
<p>배포가 끝나면 Blob 서비스와 컨테이너를 생성해 원본 파일을 올릴 수 있다. </p>
<hr>
<h1 id="18-원본-데이터-준비">18. 원본 데이터 준비</h1>
<h2 id="18-1-컨테이너-생성">18-1. 컨테이너 생성</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/1cecdeb3-3761-4287-bf36-f75746801710/image.png" alt=""></p>
<p>Storage Account에서 Blob service로 이동한 뒤 컨테이너를 생성한다. 자료 예시에서는 <code>inputstorage</code>라는 이름을 사용한다. 컨테이너는 비공개 상태로 생성된다. </p>
<h2 id="18-2-csv-파일-업로드">18-2. CSV 파일 업로드</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/e084e453-168f-4c81-8fe6-7bcce5877463/image.png" alt=""></p>
<p>생성한 컨테이너에 <code>iris.csv</code> 파일을 업로드한다. 업로드 후 파일을 클릭해 개요와 편집 화면을 확인할 수 있다. </p>
<h2 id="18-3-blob-안에서-csv가-보이는-형태">18-3. Blob 안에서 CSV가 보이는 형태</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/24418c7d-ed91-4e65-a340-152a2649374b/image.png" alt=""></p>
<p>자료의 편집 화면 예시에서는 다음과 같은 구조로 보인다.</p>
<ul>
<li>첫 행: 헤더</li>
<li>이후 행: 데이터 샘플</li>
<li>구분자: 쉼표(,)</li>
</ul>
<h2 id="csv-컬럼-구조">CSV 컬럼 구조</h2>
<table>
<thead>
<tr>
<th>컬럼</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td>SepalLength</td>
<td>꽃받침 길이</td>
</tr>
<tr>
<td>SepalWidth</td>
<td>꽃받침 너비</td>
</tr>
<tr>
<td>PetalLength</td>
<td>꽃잎 길이</td>
</tr>
<tr>
<td>PetalWidth</td>
<td>꽃잎 너비</td>
</tr>
<tr>
<td>Species</td>
<td>품종</td>
</tr>
</tbody></table>
<h1 id="참고-대시보드-고정-기능">참고) 대시보드 고정 기능</h1>
<p>자료에서는 실습 중 여러 리소스를 자주 오가야 하므로, 리소스 그룹·ADF·SQL Database·SQL Server·Storage Account를 대시보드에 고정하는 방식을 소개한다. 핀 아이콘으로 메뉴를 고정하고, 새 대시보드를 만들어 자주 쓰는 리소스를 한눈에 모아두면 이동이 편해진다. </p>
<h2 id="대시보드-고정-대상-예시">대시보드 고정 대상 예시</h2>
<table>
<thead>
<tr>
<th>리소스</th>
<th>용도</th>
</tr>
</thead>
<tbody><tr>
<td>Data Factory</td>
<td>파이프라인 편집 및 실행</td>
</tr>
<tr>
<td>SQL Database</td>
<td>목적지 테이블 관리</td>
</tr>
<tr>
<td>SQL Server</td>
<td>방화벽 및 서버 설정</td>
</tr>
<tr>
<td>Storage Account</td>
<td>원본 CSV 업로드</td>
</tr>
<tr>
<td>Resource Group</td>
<td>전체 리소스 관리</td>
</tr>
</tbody></table>
<hr>
<p><img src="https://velog.velcdn.com/images/rudin_/post/8d3c4ef5-c011-44d6-b78c-c1d03c22ec27/image.png" alt="">
좌상단 핀버튼 누르고 추가 가능
대시보드 접근은 좌상단 三 버튼 누르기
<img src="https://velog.velcdn.com/images/rudin_/post/2e78f804-0b8e-470f-ade3-fc3617be0334/image.png" alt=""></p>
<hr>
<h1 id="19-목적지-테이블-준비">19. 목적지 테이블 준비</h1>
<p><img src="https://velog.velcdn.com/images/rudin_/post/94d0f666-b254-4602-af39-0ced131eca12/image.png" alt=""></p>
<p>SQL Database 리소스로 이동한 뒤 쿼리 편집기에서 SQL 인증으로 로그인한다. 이후 <code>iris-columns.sql</code>의 내용을 복사해 실행하여 목적지 테이블을 만든다. 자료 예시에서는 <code>Iris</code> 테이블을 생성한다. 실행 후 Explorer에서 테이블과 컬럼이 보이고, Messages 영역에 <code>Query executed successfully</code>가 표시된다. </p>
<h2 id="생성-sql">생성 SQL</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/bcdd9c83-444d-4547-972d-1de61cef7340/image.png" alt=""></p>
<pre><code class="language-sql">CREATE TABLE Iris (
    SepalLength decimal(5,2),
    SepalWidth decimal(5,2),
    PetalLength decimal(5,2),
    PetalWidth decimal(5,2),
    Species nvarchar(100)
);</code></pre>
<p><img src="https://velog.velcdn.com/images/rudin_/post/5216e8d1-31d0-4354-8e2d-f8e66e9baf6c/image.png" alt=""></p>
<h2 id="생성-테이블-구조">생성 테이블 구조</h2>
<table>
<thead>
<tr>
<th>컬럼</th>
<th>타입</th>
</tr>
</thead>
<tbody><tr>
<td>SepalLength</td>
<td>decimal(5,2)</td>
</tr>
<tr>
<td>SepalWidth</td>
<td>decimal(5,2)</td>
</tr>
<tr>
<td>PetalLength</td>
<td>decimal(5,2)</td>
</tr>
<tr>
<td>PetalWidth</td>
<td>decimal(5,2)</td>
</tr>
<tr>
<td>Species</td>
<td>nvarchar(100)</td>
</tr>
</tbody></table>
<hr>
<h1 id="20-data-factory-studio-진입">20. Data Factory Studio 진입</h1>
<p>ADF 리소스에서 <code>Studio 시작하기</code>를 클릭한 뒤, 왼쪽의 연필 아이콘인 <strong>Author</strong> 메뉴로 이동한다. 여기서 파이프라인, 데이터셋, 연결된 서비스 등을 만들 수 있다.
<img src="https://velog.velcdn.com/images/rudin_/post/e0a5265c-ef9b-46d6-ac9d-fe1643edaf52/image.png" alt=""></p>
<hr>
<h1 id="21-파이프라인-생성">21. 파이프라인 생성</h1>
<p><img src="https://velog.velcdn.com/images/rudin_/post/684b938a-4fa2-4f93-b081-59da275944f2/image.png" alt=""></p>
<p>자료의 화면 예시에서는 파이프라인 목록 오른쪽 메뉴에서 <code>새 파이프라인</code>을 클릭하고, 편집창이 열리면 우측 속성의 일반 메뉴에서 이름을 지정한다. 예시 이름은 <code>Blob to SQL</code>이다. </p>
<h2 id="파이프라인-설정">파이프라인 설정</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/eedd17da-f802-460d-a6de-7126bbdb39b8/image.png" alt=""></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td>파이프라인 이름</td>
<td>Blob to SQL</td>
</tr>
<tr>
<td>역할</td>
<td>전체 데이터 복사 흐름 제어</td>
</tr>
</tbody></table>
<p>파이프라인은 가장 상위의 작업 흐름 단위이며, 이후 여기에 Linked Service, Dataset, Copy Activity가 연결된다.</p>
<hr>
<h1 id="22-원본-linked-service-생성">22. 원본 Linked Service 생성</h1>
<p><img src="https://velog.velcdn.com/images/rudin_/post/451b9ae6-0b8f-4d40-aded-2aa5ba4aa980/image.png" alt=""></p>
<p>관리 메뉴에서 <code>연결된 서비스</code>를 선택하고 새로 만들기를 눌러 Blob Storage 연결 정보를 생성한다. 이 연결은 원본 CSV 파일이 있는 Storage Account를 가리킨다. </p>
<p><img src="https://velog.velcdn.com/images/rudin_/post/906ac697-8844-4986-8921-c9795236d141/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/7bc2255a-b171-4662-9791-01e16fdda485/image.png" alt=""></p>
<p>로컬에 있는걸 연결하고 싶으면 통합 런타임이 아닌 다른 런타임을 사용해야 한다.
생성 전 연결 테스트는 항상 해보자.</p>
<h1 id="22-2-원본-dataset-생성">22-2. 원본 Dataset 생성</h1>
<p><img src="https://velog.velcdn.com/images/rudin_/post/90878e97-8046-44b6-9274-d65ac3a482aa/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/rudin_/post/455e8cf9-839c-41e8-b308-ce53f241de0f/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/f0b8e1da-a565-4ce0-8e31-761c3cf28a9f/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/176f0db9-3564-4edf-b3b9-e70d9e3d5ebc/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/f8159f7a-5e50-4f99-b045-4c8421bb28ad/image.png" alt=""></p>
<ul>
<li>형식: CSV</li>
<li>연결: Blob Linked Service</li>
<li>대상: 업로드한 <code>iris.csv</code></li>
<li>특징: 첫 행을 헤더로 사용함</li>
</ul>
<p>데이터세트는 하나의 함수로 이해하면 됨</p>
<h2 id="데이터-미리보기">데이터 미리보기</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/bb210912-f217-4552-9637-5dfa0e0d4fc4/image.png" alt="">
미리보기로 연결이 정상인지 확인</p>
<hr>
<h1 id="23-싱크-linked-service-생성">23. 싱크 Linked Service 생성</h1>
<p><img src="https://velog.velcdn.com/images/rudin_/post/cf8b8337-aae6-4191-ad56-6d829c1fbd59/image.png" alt="">
Azure SQL Database 선택
<img src="https://velog.velcdn.com/images/rudin_/post/db9c24d8-d968-4b4f-812d-3ef33857b5e3/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/3b7b9398-bcfb-4c5b-b05a-17176b0ee5e9/image.png" alt="">
만들기가 비활성화됐다면 취소했다가 다시 생성하면 된다. 
혹은 db, adf 네트워킹 설정을 다시 확인해보자.</p>
<p>같은 방식으로 SQL Database용 Linked Service를 생성한다. 이 연결은 SQL 서버 주소, 데이터베이스, 인증 정보 등을 사용해 목적지에 접속한다. </p>
<h2 id="linked-service-정리">Linked Service 정리</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/50495a9c-1b5f-4139-9704-de0c57db6a33/image.png" alt=""></p>
<table>
<thead>
<tr>
<th>구분</th>
<th>연결 대상</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>원본 Linked Service</td>
<td>Blob Storage</td>
<td>CSV 원본 연결</td>
</tr>
<tr>
<td>싱크 Linked Service</td>
<td>SQL Database</td>
<td>대상 테이블 연결</td>
</tr>
</tbody></table>
<hr>
<h1 id="23-2-싱크-dataset-생성">23-2. 싱크 Dataset 생성</h1>
<p><img src="https://velog.velcdn.com/images/rudin_/post/a04e1cb4-fdec-435e-a906-b5f43e4b81ec/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/b818b296-5d16-436f-8007-abfb29933ea4/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/a436d314-d442-4ed6-ba79-b7e035c96d8a/image.png" alt=""></p>
<h2 id="dataset-정리">Dataset 정리</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/a08c2279-ad51-4b38-85af-e09146a1c8bd/image.png" alt=""></p>
<table>
<thead>
<tr>
<th>구분</th>
<th>형식</th>
<th>연결</th>
<th>대상</th>
</tr>
</thead>
<tbody><tr>
<td>Source Dataset</td>
<td>CSV</td>
<td>Blob Linked Service</td>
<td>iris.csv</td>
</tr>
<tr>
<td>Sink Dataset</td>
<td>SQL Table</td>
<td>SQL Linked Service</td>
<td>Iris Table</td>
</tr>
</tbody></table>
<hr>
<h1 id="24-copy-activity">24. Copy Activity</h1>
<p><img src="https://velog.velcdn.com/images/rudin_/post/9ecbb66d-14bc-4a7f-a342-35058633a97d/image.png" alt=""></p>
<p>Copy Activity는 실습의 핵심이다. 원본 Dataset에서 데이터를 읽어 싱크 Dataset으로 복사한다. 자료의 실습 구성도에서는 Blob과 SQL 사이 중앙에 Copy Activity가 배치되고, 이후 강조 표시된 그림에서는 Dataset → Copy Activity → Dataset 구간이 하나의 핵심 처리 블록으로 묶여 있다.</p>
<p>좌측 데이터 복사를 드래그 앤 드랍
<img src="https://velog.velcdn.com/images/rudin_/post/de16614e-eaa8-49af-818a-ccf9ced8790e/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/7c1dd38c-83ec-4f17-9da4-98b70dc431b4/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/60087525-45a2-4a8d-bd9c-0e14199de787/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/58a1c94b-5f88-4997-aa87-52ae52c9ffaa/image.png" alt="">
스키마 가져오기 선택
<img src="https://velog.velcdn.com/images/rudin_/post/2cafaa8b-748c-462d-a3ab-36d6e0ca8432/image.png" alt=""></p>
<h2 id="디버그">디버그</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/f0596131-0972-4db6-9688-ccf8d848c46b/image.png" alt="">
실무에서는 트리거를, 실습에서는 디버그를 사용(단일 테스트)
<img src="https://velog.velcdn.com/images/rudin_/post/632121db-efbe-490a-950a-ff0a8000daa3/image.png" alt=""></p>
<p>db의 쿼리편집기에서 확인
<img src="https://velog.velcdn.com/images/rudin_/post/4b9a43d0-a5a3-4d66-bcae-02f36d611de0/image.png" alt=""></p>
<h2 id="copy-activity-역할">Copy Activity 역할</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>입력</td>
<td>Source Dataset</td>
</tr>
<tr>
<td>출력</td>
<td>Sink Dataset</td>
</tr>
<tr>
<td>기능</td>
<td>데이터 복사 및 기본 매핑 수행</td>
</tr>
<tr>
<td>위치</td>
<td>Pipeline 내부</td>
</tr>
</tbody></table>
<hr>
<h1 id="25-게시">25. 게시</h1>
<p><img src="https://velog.velcdn.com/images/rudin_/post/aaa29a59-e076-48a0-857c-a0b9101c5839/image.png" alt="">
게시하지 않으면 저장이 안되므로 주의하자</p>
<hr>
<h1 id="26-파이프라인-실행">26. 파이프라인 실행</h1>
<h2 id="26-2-파이프라인-트리거">26-2. 파이프라인 트리거</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/5fc8d19b-05b9-479e-b71e-cbdf61a720c6/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/a0911925-729e-4c65-b8a9-4dccfb76d183/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/73877590-b3e7-48d8-ab3f-55751f8e54be/image.png" alt=""></p>
<h2 id="26-3-결과-확인">26-3. 결과 확인</h2>
<p>모니터에서 확인
<img src="https://velog.velcdn.com/images/rudin_/post/3341d49a-2f08-4187-a2fb-5b22a424e128/image.png" alt=""></p>
<p>쿼리 편집기에서 확인
<img src="https://velog.velcdn.com/images/rudin_/post/dbf3c7bf-c87d-4bca-ab36-6341fc8d8d5c/image.png" alt="">
동일한 실행을 두 번하여 중복 발생으로 2배 count됨</p>
<hr>
<h1 id="27-파이프라인-수정">27. 파이프라인 수정</h1>
<p><img src="https://velog.velcdn.com/images/rudin_/post/94029763-0398-447d-ad88-c5369036a2ae/image.png" alt="">
매번 실행마다 delete 후 실행하도록 처리
<img src="https://velog.velcdn.com/images/rudin_/post/e566b029-8b61-4c1b-9451-16c9bcba0314/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/8e610f02-f9ab-4bfb-9d0a-0128c39a96ba/image.png" alt=""></p>
<hr>
<h1 id="28-adf-구성-요소-관계-정리">28. ADF 구성 요소 관계 정리</h1>
<h2 id="28-1-pipeline과-activity">28-1. Pipeline과 Activity</h2>
<p>Pipeline 아래에 여러 Activity가 들어갈 수 있다. 하나의 파이프라인 안에서 원본과 목적지가 다른 복사 작업을 여러 개 넣을 수도 있다. </p>
<h2 id="28-2-pipeline과-linked-service">28-2. Pipeline과 Linked Service</h2>
<p>여러 Activity가 같은 원본 Storage 또는 같은 SQL Database를 사용할 경우, 연결 정보는 하나의 Linked Service를 재사용한다. 즉, 연결을 중복 생성하지 않고 중앙에서 관리할 수 있다.</p>
<h2 id="28-3-pipeline과-dataset">28-3. Pipeline과 Dataset</h2>
<p>Copy Activity는 각각 원본 Dataset과 싱크 Dataset을 참조한다. Dataset은 실제 데이터 파일이나 테이블의 위치와 형식을 정의하므로, Activity가 데이터를 해석하는 기준이 된다.</p>
<hr>
<h1 id="29-폴더로-정리">29. 폴더로 정리</h1>
<p><img src="https://velog.velcdn.com/images/rudin_/post/d0e0ce5b-a41a-48e4-87c8-b199685f4a63/image.png" alt=""></p>
<hr>
<h1 id="30-실습-흐름-한-번에-정리">30. 실습 흐름 한 번에 정리</h1>
<h2 id="리소스-준비-단계">리소스 준비 단계</h2>
<ol>
<li>실습 데이터 다운로드</li>
<li>리소스 그룹 생성</li>
<li>Data Factory 생성</li>
<li>SQL Database 및 SQL Server 생성</li>
<li>Storage Account 생성</li>
<li>Blob 컨테이너 생성</li>
<li>CSV 업로드</li>
<li>SQL 테이블 생성</li>
</ol>
<h2 id="adf-작업-단계">ADF 작업 단계</h2>
<ol>
<li>Data Factory Studio 진입</li>
<li>Author 메뉴 이동</li>
<li>새 파이프라인 생성</li>
<li>Blob Linked Service 생성</li>
<li>SQL Linked Service 생성</li>
<li>Source Dataset 생성</li>
<li>Sink Dataset 생성</li>
<li>Copy Activity 추가</li>
<li>Source Dataset 연결</li>
<li>Sink Dataset 연결</li>
<li>복사 실행</li>
</ol>
<p>이 전체 흐름의 목표는 <strong>Blob Storage의 CSV 데이터를 SQL Database 테이블로 복사하는 것</strong>이다.</p>
<hr>
<h1 id="31-핵심-개념-요약">31. 핵심 개념 요약</h1>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>데이터 문제</td>
<td>데이터 사일로, 이기종 데이터, 높은 운영 복잡성</td>
</tr>
<tr>
<td>해결 방향</td>
<td>데이터를 한곳에 통합하고 자동화된 파이프라인 구축</td>
</tr>
<tr>
<td>핵심 방식</td>
<td>ETL, ELT, CDC</td>
</tr>
<tr>
<td>ADF 역할</td>
<td>데이터 이동·오케스트레이션</td>
</tr>
<tr>
<td>실행 엔진</td>
<td>Integration Runtime</td>
</tr>
<tr>
<td>실습 구조</td>
<td>Blob CSV → Copy Activity → SQL Table</td>
</tr>
</tbody></table>
<hr>
<h1 id="32-마무리">32. 마무리</h1>
<p>Azure Data Factory는 데이터를 직접 분석하는 도구라기보다는, <strong>분석 가능한 형태로 데이터를 연결하고 이동시키는 데이터 파이프라인 도구</strong>에 가깝다. 따라서 ADF를 이해할 때는 단순히 “복사 도구”로 보기보다, <strong>원본 시스템과 분석 시스템 사이를 이어주는 오케스트레이션 계층</strong>으로 보는 것이 중요하다. 이번 실습도 결국 Blob Storage의 파일과 SQL Database의 테이블을 연결하면서, Pipeline·Activity·Dataset·Linked Service·Integration Runtime이 어떻게 협력하는지 익히는 과정이라고 볼 수 있다.</p>
<hr>


<p>제공해주신 소스(03-30-2.01 매개변수화_v12.pdf)의 <strong>개요</strong> 부분 내용을 요약 없이 마크다운 형식으로 정리해 드립니다.</p>
<hr>
<h1 id="매개변수화parameterization-개요">매개변수화(Parameterization) 개요</h1>
<h1 id="1-매개변수화parameterization-정의">1. 매개변수화(Parameterization) 정의</h1>
<p>매개변수(Parameter)는 데이터 팩토리의 작업 수행 시 입력값으로 사용되는 값이며, 각 액티비티, 파이프라인, 데이터셋(Datasets) 등에서 사전에 정의된 값 또는 사용자 정의 값을 입력받을 수 있습니다. 매개변수화는 특히 프로덕션 환경에서 재사용성과 유지보수성 향상에 큰 효과가 있습니다.</p>
<h1 id="2-매개변수화를-지원하는-구성요소">2. 매개변수화를 지원하는 구성요소</h1>
<ul>
<li><strong>Parameters (매개변수)</strong>: 파이프라인 실행 시 외부에서 값을 입력받아 유연한 구성 가능.</li>
<li><strong>Variables (변수)</strong>: 파이프라인 내에서 상태값을 유지하거나 중간 결과를 저장.</li>
<li><strong>Expressions (표현식)</strong>: 동적 값을 계산하거나 조건문 등을 구성할 수 있는 함수 기반 표현식.</li>
</ul>
<h1 id="3-매개변수화의-이점">3. 매개변수화의 이점</h1>
<ul>
<li><strong>흐름 제어</strong>: 다양한 조건에 따라 실행 경로를 제어 가능.</li>
<li><strong>시간 절약</strong>: 동일한 파이프라인을 여러 시나리오에 재사용.</li>
<li><strong>유연한 설계</strong>: 솔루션을 일반화하고 유지보수 용이.</li>
</ul>
<hr>
<h1 id="4-parameters-매개변수-상세">4. Parameters (매개변수) 상세</h1>
<p>파이프라인, 데이터세트 등에서 정의하는 외부 입력값으로, 실행 시 값을 주입받아 유연한 동작을 지원합니다.</p>
<table>
<thead>
<tr>
<th align="left">항목</th>
<th align="left">내용</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>주요 특성</strong></td>
<td align="left">• 사전에 정의된 값 혹은 사용자 정의 가능<br>• 주로 실행 시점에 결정되는 정적인 값<br>• 런타임 시점에 값 전달<br>• 데이터세트, 파이프라인 등 다양한 요소에 적용</td>
</tr>
<tr>
<td align="left"><strong>활용 목적</strong></td>
<td align="left">• 동적 처리: 날짜별/부서별/환경별 분기<br>• 재사용성: 동일 파이프라인을 다양한 값으로 실행<br>• 유연성: 실행 시점에 변경 가능한 구성</td>
</tr>
<tr>
<td align="left"><strong>활용 예시</strong></td>
<td align="left">• 날짜별 파이프라인 실행: 특정 일자 데이터만 추출<br>• 환경 분기: Dev./Prod. 연결 서비스 자동 전환<br>• 부서별 로직: SQL 쿼리의 동적 적용</td>
</tr>
<tr>
<td align="left"><strong>구문 예시</strong></td>
<td align="left"><code>@pipeline().parameters.inputDate</code></td>
</tr>
</tbody></table>
<hr>
<h1 id="5-variables-변수-상세">5. Variables (변수) 상세</h1>
<p>파이프라인 실행 중에 값을 저장, 조회, 업데이트할 수 있는 내부 런타임 변수입니다.</p>
<table>
<thead>
<tr>
<th align="left">항목</th>
<th align="left">내용</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>주요 특성</strong></td>
<td align="left">• 범위(scope): 파이프라인 단위(자식 파이프라인에 자동 전달되지 않음)<br>• 런타임 수정 가능: Set Variable 액티비티로 값 변경<br>• 선언 시 초기값 지정 가능<br>• 지원 데이터 타입: String, Boolean, Int, Array</td>
</tr>
<tr>
<td align="left"><strong>활용 예시</strong></td>
<td align="left">• 중간 연산값 관리: 복잡한 표현식 결과를 변수에 담아 재사용<br>• 재시도 카운터: 오류 발생 시 retryCount를 1씩 증가시켜 제어<br>• 루프 인덱스 누적: ForEach 반복 횟수 누적 혹은 조건부 루프 제어<br>• 상태 메시지: 각 단계 완료 후 상세 로그 저장</td>
</tr>
<tr>
<td align="left"><strong>선언 구문</strong></td>
<td align="left"><code>&quot;variables&quot;: { &quot;retryCount&quot;: { &quot;type&quot;: &quot;Int&quot;, &quot;defaultValue&quot;: 0 } }</code></td>
</tr>
<tr>
<td align="left"><strong>참조/할당</strong></td>
<td align="left">참조: <code>@variables(&#39;retryCount&#39;)</code><br>할당: <code>@add(variables(&#39;retryCount&#39;), 1)</code></td>
</tr>
</tbody></table>
<hr>
<h1 id="6-expressions-표현식-상세">6. Expressions (표현식) 상세</h1>
<p>런타임에 동적으로 값의 연산, 변환, 판단을 수행하기 위해 다양한 함수를 포함하는 표현식을 활용합니다.</p>
<ul>
<li><strong>주요 특징</strong>:<ul>
<li><strong>런타임 평가</strong>: 실행 시점에 해석 및 실행되어 동적 경로 생성.</li>
<li><strong>풍부한 함수 라이브러리</strong>: 문자열, 수치, 날짜, 논리, 배열 등 지원.</li>
<li><strong>동적 참조</strong>: 파라미터, 변수, 액티비티 출력값 통합 사용.</li>
<li><strong>중첩 가능</strong>: 함수 안에 함수를 삽입하여 복합 연산 지원.</li>
</ul>
</li>
<li><strong>활용 예시</strong>:<ul>
<li><code>@concat(&#39;landing/&#39;, pipeline().parameters.region, &#39;/&#39;, formatDateTime(utcNow(), &#39;yyyyMMdd&#39;))</code></li>
<li><code>@formatDateTime(addDays(utcNow(), -1), &#39;yyyy-MM-dd&#39;)</code></li>
<li><code>@if(greater(activity(&#39;Lookup&#39;).output.count, 0), &#39;HasData&#39;, &#39;NoData&#39;)</code></li>
</ul>
</li>
<li><strong>자주 쓰이는 함수</strong>: <code>concat</code>, <code>formatDateTime</code>, <code>addDays</code>, <code>if</code>, <code>length</code>, <code>json</code>.</li>
</ul>
<hr>
<h1 id="7-parameters-vs-variables-비교-요약">7. Parameters vs Variables 비교 요약</h1>
<table>
<thead>
<tr>
<th align="left">구분</th>
<th align="left">Parameter</th>
<th align="left">Variable</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>정의</strong></td>
<td align="left">파이프라인 실행 시 외부에서 주입받는 입력 값</td>
<td align="left">파이프라인 실행 중 내부에서 생성, 조회, 업데이트 가능한 런타임 변수</td>
</tr>
<tr>
<td align="left"><strong>적용 범위</strong></td>
<td align="left">파이프라인, 데이터세트, 연결 서비스 등 선언한 레벨에 한정됨</td>
<td align="left">파이프라인 단위(자식 파이프라인에 전달되지 않음)</td>
</tr>
<tr>
<td align="left"><strong>런타임 변경</strong></td>
<td align="left">불가능 (정적 값)</td>
<td align="left">Set Variable 액티비티로 언제든 변경 가능</td>
</tr>
<tr>
<td align="left"><strong>참조 구문</strong></td>
<td align="left"><code>@pipeline().parameters.&lt;이름&gt;</code></td>
<td align="left"><code>@variables(&#39;&lt;이름&gt;&#39;)</code></td>
</tr>
<tr>
<td align="left"><strong>주요 활용 예</strong></td>
<td align="left">날짜 필터링, 환경 분기(environment)</td>
<td align="left">재시도 카운터 증가, 상태 메시지 저장</td>
</tr>
</tbody></table>
<hr>
<h1 id="8-실습-시나리오---파이프라인-매개변수화">8. 실습 시나리오 - 파이프라인 매개변수화</h1>
<h2 id="매개변수화를-위한-시나리오-확장">매개변수화를 위한 시나리오 확장</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/fb647b88-3e06-4fc2-95a1-58676717fcf0/image.png" alt=""></p>
<h2 id="데이터세트의-매개변수화">데이터세트의 매개변수화</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/111c67d1-058c-4eb1-91c7-be02ea0af521/image.png" alt=""></p>
<hr>
<h1 id="9-실습">9. 실습</h1>
<h2 id="9-1-azure-container에-원본-데이터-업로드">9-1. Azure Container에 원본 데이터 업로드</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/014e58e5-1742-4048-b8ce-5e43ea37041b/image.png" alt=""></p>
<h2 id="9-2-목적지-데이터-생성">9-2. 목적지 데이터 생성</h2>
<p>SQL Server에서 SQL 데이터베이스로 접속 후 쿼리편집기에서 추가
<img src="https://velog.velcdn.com/images/rudin_/post/7a199128-b9d1-4ea9-9647-cd29664a4633/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/8a388320-bb70-4d77-a896-5f9e27aed2f0/image.png" alt=""></p>
<h2 id="9-3-연결된-서비스linked-service-준비">9-3. 연결된 서비스(Linked Service) 준비</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/e1947f0e-14e0-476c-b4f3-bfe2dfe06ec5/image.png" alt=""></p>
<h2 id="9-4-소스-데이터세트-생성">9-4. 소스 데이터세트 생성</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/de0c89ce-f4d1-44ed-918d-bca54603b5d0/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/a71e4345-e341-4871-8676-f98939722492/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/88878dac-35b4-4887-b458-0a48598928d0/image.png" alt=""></p>
<h2 id="9-5-목적지-데이터세트-생성">9-5. 목적지 데이터세트 생성</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/e435463b-07eb-4c66-894f-8b4161f3d238/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/427a0d73-8254-4942-baf4-d6969a065e36/image.png" alt=""></p>
<h2 id="9-6-파이프라인-생성">9-6. 파이프라인 생성</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/1dc5482d-430b-46e1-88d3-4246418b829e/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/d9e1b02a-4223-45d5-b5f9-4dec4c8eadd8/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/4c0895d6-8bfd-4a84-bbd0-013d9ac40591/image.png" alt="">
디버그
<img src="https://velog.velcdn.com/images/rudin_/post/0edfa995-c961-4d31-8a9d-3179dc2c53c0/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/3bf391a1-2bd8-4a0e-ac3d-be367907f89d/image.png" alt=""></p>
<h2 id="9-7-데이터세트-매개변수화원본">9-7. 데이터세트 매개변수화(원본)</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/ea02d42c-b62c-40ea-abbe-564d85eb3cb8/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/af4b9b37-8393-438d-aa81-51a6f68829c7/image.png" alt="">
연결탭의 파일 경로에서 파일 이름 삭제
<img src="https://velog.velcdn.com/images/rudin_/post/74d13016-85bd-4aeb-888b-8e79ea9d7ed7/image.png" alt="">
동적 콘텐츠 추가 - 하단의 매개 변수 선택
<img src="https://velog.velcdn.com/images/rudin_/post/fea39ae8-dafe-4607-9fc2-bedfc5897405/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/9d723d01-af4b-4a00-bfe4-b36a8deda08f/image.png" alt=""></p>
<h2 id="9-8-데이터세트-매개변수화싱크">9-8. 데이터세트 매개변수화(싱크)</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/3e381de9-1279-4f92-a2f9-e384f429ada5/image.png" alt="">
수동으로 입력 체크
<img src="https://velog.velcdn.com/images/rudin_/post/377277ad-a1bf-44fb-809a-18110897c59d/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/0ed9dd3e-006c-4327-bdb5-e5cac22a6afe/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/aa69d3a3-fe79-443e-9ec2-e5bc2727abeb/image.png" alt=""></p>
<h2 id="9-9-데이터세트-매개변수화-테스트">9-9. 데이터세트 매개변수화 테스트</h2>
<p>원본 설정
<img src="https://velog.velcdn.com/images/rudin_/post/ffdeec63-560b-40b5-a7fa-aeec5c1a8428/image.png" alt="">
싱크 설정
<img src="https://velog.velcdn.com/images/rudin_/post/a523d2cf-49ac-4f5b-a4dc-a36cd30f7e0b/image.png" alt="">
디버그 실행
<img src="https://velog.velcdn.com/images/rudin_/post/b4a5f549-bd55-4f83-bd8d-4dd048cf5afc/image.png" alt=""></p>
<h2 id="9-10-데이터세트-매개변수화-적용">9-10. 데이터세트 매개변수화 적용</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/e1ebda67-3752-46f0-b2c6-db1115c03883/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/rudin_/post/f33e8efc-71cb-46f4-a0a0-1ddc3bc0f7b9/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/e54b63af-3abe-4107-b9f8-9d937c81b0d0/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/823c9f29-af3f-46b8-a803-3cf6b041a74b/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/7fa1d821-ddfb-4c0b-b682-a5ffe7d1b1d8/image.png" alt=""></p>
<h2 id="9-11-데이터세트-매개변수화-활용-데이터-백업-추가">9-11. 데이터세트 매개변수화 활용: 데이터 백업 추가</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/a4b067d7-7d62-47e5-98ce-de950a435da1/image.png" alt=""></p>
<p>데이터복사 추가 
<img src="https://velog.velcdn.com/images/rudin_/post/f882cfb3-1008-4e27-8d7d-0552dc7ed593/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/4bdbb1ad-14fe-408c-b45a-9f2fb3e00d74/image.png" alt="">
위에서부터 성공, 실패, 항상처리 시 다음 처리 연결 노드</p>
<p>백업이므로 output(원본) → input(싱크)
<img src="https://velog.velcdn.com/images/rudin_/post/416807eb-ca49-4753-9323-04bf8cd766ea/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/81e9810f-9d6f-42c1-b0e9-6615e5c63818/image.png" alt=""></p>
<p>디버그
<img src="https://velog.velcdn.com/images/rudin_/post/4042ad77-706c-4045-a187-29e38071dcd1/image.png" alt="">
추가된것을 확인 가능
<img src="https://velog.velcdn.com/images/rudin_/post/b1af25af-ccad-4e6b-99a0-3731d6a70935/image.png" alt=""></p>
<hr>
<h2 id="9-12-데이터세트-매개변수화-활용2-데이터-백업-날짜-추가">9-12. 데이터세트 매개변수화 활용2: 데이터 백업 날짜 추가</h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/a101f553-0a36-48df-b633-f35a36bad5c4/image.png" alt=""></p>
<p><code>싱크</code> - <code>값</code>에 동적 콘텐츠 추가 후 식 선택</p>
<ol>
<li>String - concat
<img src="https://velog.velcdn.com/images/rudin_/post/c6445752-a659-4dd4-b741-0e485156b721/image.png" alt=""></li>
<li>Date-utcNow
<img src="https://velog.velcdn.com/images/rudin_/post/ea0e8f3b-d22d-47b8-9553-4cfbcf95327d/image.png" alt=""></li>
<li>쉼표 입력 후 <code>.csv</code> 입력 
<img src="https://velog.velcdn.com/images/rudin_/post/a7f722fe-68f0-4855-8c26-44201d7717d9/image.png" alt=""></li>
</ol>
<p>디버그 확인
<img src="https://velog.velcdn.com/images/rudin_/post/5117f079-45d5-4fa5-8187-dcafebd87d98/image.png" alt=""></p>
<hr>
<h1 id="10-adf의-parameters">10. ADF의 parameters</h1>
<h4 id="linked-service-매개변수">Linked Service 매개변수</h4>
<p>• 예시 : SQL Server에서 사용되는 데이터베이스를 매개변수화</p>
<h4 id="dataset-매개변수">Dataset 매개변수</h4>
<p>• 예시 : 파일 이름, Blob 컨테이너 등을 매개변수화</p>
<h4 id="pipeline-매개변수">Pipeline 매개변수</h4>
<p>• pipeline 내에서 특정 값을 전달할 수 있도록 매개변수 사용
• 예시 : pipeline에서 특정 원본 파일을 특정 싱크 파일에 복사하도록 매개변수 지정</p>
<h4 id="global-매개변수">Global 매개변수</h4>
<p>• Data Factory 수준에서 사용되는 매개변수
• 원하는 곳에서 참조 가능</p>
<hr>
<h1 id="11-파이프라인-매개변수화">11. 파이프라인 매개변수화</h1>
<p><img src="https://velog.velcdn.com/images/rudin_/post/a974e9dd-8745-4abc-9165-3793dcd73a41/image.png" alt=""></p>
<p>파이프라인 매개변수 설정
<img src="https://velog.velcdn.com/images/rudin_/post/cfea01f8-9257-48f1-a13d-f7beacd25415/image.png" alt=""></p>
<p>파이프라인식 작성기에서 파라미터 추가
<img src="https://velog.velcdn.com/images/rudin_/post/c307cfa3-e3ce-4d67-ba1d-84fd1ef043a1/image.png" alt=""></p>
<p>원본에도 동적 콘텐츠 추가
<img src="https://velog.velcdn.com/images/rudin_/post/5cd3cfbf-ed77-491b-b5f2-d40ffbc2f016/image.png" alt=""></p>
<p>Copy data1쪽도 동일하게 처리
원본-동적콘텐츠추가
<img src="https://velog.velcdn.com/images/rudin_/post/d9339511-9657-4eae-8408-2eeabd5ba08b/image.png" alt=""></p>
<p>싱크-동적콘텐츠추가
<img src="https://velog.velcdn.com/images/rudin_/post/7c54deaa-a778-4911-bd22-87e2f00278e4/image.png" alt=""></p>
<p>디버그
<img src="https://velog.velcdn.com/images/rudin_/post/36e9919b-6770-4e34-bc8f-f659d729cbe7/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/871a76f1-183c-40b6-a4aa-7bd500f8b7b5/image.png" alt=""></p>
<p>이후 게시</p>
<hr>
<h1 id="트리거">트리거</h1>
<h2 id="1-트리거trigger-개요-및-방식"><strong>1. 트리거(Trigger) 개요 및 방식</strong></h2>
<ul>
<li><strong>개요</strong>: 데이터 수집/변환 워크플로우 설계 시 작업 실행 시점과 방식을 설계하는 것은 매우 중요하며, 트리거는 워크플로우 자동화의 핵심 요소입니다.</li>
<li><strong>제공되는 트리거 방식</strong>:<ul>
<li><strong>Schedule 트리거</strong>: 지정한 일정에 따라 정기적으로 파이프라인을 실행합니다.</li>
<li><strong>Tumbling Window 트리거</strong>: 고정된 시간 간격(윈도우)을 기준으로 데이터를 수집하고 처리하며, 각 윈도우는 겹치지 않습니다.</li>
<li><strong>Storage Event 트리거</strong>: Azure Blob Storage에서 파일이 생성되거나 변경되는 이벤트에 반응합니다.</li>
<li><strong>Custom Event 트리거</strong>: Event Grid, Event Hub 등을 연동하여 사용자 정의 이벤트를 수신하고 파이프라인을 실행합니다.</li>
<li><strong>Manual 트리거</strong>: ADF Studio UI에서 직접 실행하거나 REST API 호출을 통해 수동으로 실행합니다.</li>
</ul>
</li>
</ul>
<h2 id="2-schedule-트리거-상세"><strong>2. Schedule 트리거 상세</strong></h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/1d98dd05-50d2-483a-910f-9e0dc0fa453d/image.png" alt=""></p>
<ul>
<li><strong>정의</strong>: 시작일, 종료일, 주기, 특정 요일 등을 기반으로 파이프라인 실행 일정을 구성하는 방식입니다.</li>
<li><strong>설정 항목</strong>:<ul>
<li><strong>Start Date</strong>: 트리거 시작일 지정.</li>
<li><strong>Time</strong>: 트리거 시작 시각 지정.</li>
<li><strong>Time Zone</strong>: 시간대 설정 (예: Seoul UTC+9).</li>
<li><strong>Recurrence</strong>: 반복 실행 주기 (분, 시간, 일, 주, 월).</li>
<li><strong>End Date</strong>: 트리거 반복 종료 시점 (선택 사항).</li>
</ul>
</li>
<li><strong>적합한 작업</strong>: &quot;하루 한 번&quot;, &quot;매주 월요일 오전 9시&quot;, &quot;매달 1일 오전 2시&quot; 등 정기적인 반복 작업.</li>
<li><strong>관계</strong>: <strong>Many-to-many</strong> 관계로, 하나의 트리거가 여러 파이프라인을 실행할 수 있고 하나의 파이프라인이 여러 트리거에 연결될 수 있습니다.</li>
<li><strong>적용 방식</strong>:<ul>
<li><strong>주기 기반</strong>: 매 1시간, 매일 등 정해진 간격 반복 (실행 시 겹치지 않도록 주의).</li>
<li><strong>특정 시간 지정</strong>: 매일 오전 10시 30분 등 세밀한 시간 지정.</li>
<li><strong>요일 지정</strong>: 매주 월, 수, 금요일 등 요일 기준 실행.</li>
<li><strong>날짜 지정</strong>: 매월 1일, 15일 등 월마다 반복되는 이벤트 처리.</li>
</ul>
</li>
</ul>
<h2 id="3-tumbling-windows-트리거-상세"><strong>3. Tumbling Windows 트리거 상세</strong></h2>
<ul>
<li><strong>정의</strong>: 고정된 크기의 시간 간격으로 구간을 나누고, 각 구간(윈도우)에 대해 하나의 파이프라인 실행을 트리거합니다. 윈도우 간 중첩이 없고 독립적인 실행 단위로 관리됩니다.</li>
<li><strong>주요 특징</strong>:<ul>
<li><strong>간격(Interval)</strong>: 일정 주기 지정.</li>
<li><strong>윈도우 간 관계</strong>: 중첩 없이 실행되며 윈도우 단위별 독립 실행.</li>
<li><strong>재시도 정책(Retry)</strong>: 파이프라인 수준에서 자동 재시도 가능.</li>
<li><strong>상태 관리(Concurrency)</strong>: 이전 실행 결과를 고려할 수 있도록 동시성 설정 지원. 빡빡하게 관리할거면 1로 설정</li>
<li><strong>관계</strong>: <strong>One-to-one</strong> 관계로, 각 트리거는 특정 파이프라인에만 연결됩니다.</li>
<li><strong>시간대 기준</strong>: UTC.</li>
</ul>
</li>
<li><strong>적용 예시</strong>: 시간 구간별 안정적 수행이 필요한 업무, 센서 데이터/로그 등 시간 단위 데이터 처리, 이전 실행 상태에 따른 다음 처리 여부 결정, 실행 실패 시 자동 재시도가 필요한 경우, 상태 기반 병렬 처리 설정 시 활용됩니다.</li>
<li><strong>Schedule 트리거와 비교</strong>:
<img src="https://velog.velcdn.com/images/rudin_/post/b2553788-eaea-4873-9212-ae4257bff319/image.png" alt=""></li>
</ul>
<table>
<thead>
<tr>
<th align="left">구분</th>
<th align="left">Schedule Trigger</th>
<th align="left">Tumbling Windows Trigger</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>주기 유형</strong></td>
<td align="left">고정 주기</td>
<td align="left">고정 간격 시간 구간</td>
</tr>
<tr>
<td align="left"><strong>재시도 정책</strong></td>
<td align="left">없음</td>
<td align="left">파이프라인 단위 재시도 지원</td>
</tr>
<tr>
<td align="left"><strong>실행 관계</strong></td>
<td align="left">Many-to-many</td>
<td align="left">One-to-one</td>
</tr>
<tr>
<td align="left"><strong>상태 관리</strong></td>
<td align="left">이전 수행 상태와 무관</td>
<td align="left">이전 파이프라인 상태 고려</td>
</tr>
<tr>
<td align="left"><strong>실행 의존성</strong></td>
<td align="left">없음</td>
<td align="left">다른 트리거에 의존 가능</td>
</tr>
<tr>
<td align="left"><strong>적용 예</strong></td>
<td align="left">단순 정기 실행</td>
<td align="left">시간 단위 안정적 처리</td>
</tr>
</tbody></table>
<ul>
<li><strong>동시성(Concurrency)</strong>: 1일 경우 이전 파이프라인 종료 시까지 대기하며, 2 이상일 경우 주기가 짧을 때 여러 파이프라인이 겹쳐서 실행될 수 있습니다. 처리되지 못한 윈도우가 쌓였을 때 이를 빠르게 소진하기 위해 2 이상으로 설정하기도 합니다.</li>
<li>작업에 따라 적절한 동시성을 설정해야 합니다. 동시성 설정에 따라, 파이프라인 실행이 무한 대기에 빠질
수도 있으며, 겹쳐서 수행되는 파이프라인으로 인해 문제가 발생할 수 있습니다.
<img src="https://velog.velcdn.com/images/rudin_/post/437ae464-1f27-42b8-8fa5-c67c6bd5ce80/image.png" alt=""></li>
</ul>
<h2 id="4-storage-event-트리거-상세"><strong>4. Storage Event 트리거 상세</strong></h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/95870438-1996-46aa-8598-bdd3599d6ea4/image.png" alt=""></p>
<ul>
<li><strong>정의</strong>: Blob Storage에서 이벤트 발생 시 즉시 자동으로 파이프라인을 실행합니다.</li>
<li><strong>주요 특징</strong>: 실시간성, 자동화(수동 모니터링 불필요), Event Grid 기반(이벤트 감지 및 자동 호출), 다대다 관계 지원.</li>
<li><strong>이벤트 흐름</strong>: 컨텐트 자체가 아닌 <strong>이벤트 정보(파일 이름, 파일 경로 등)</strong>를 전달하며, 데이터 컨텐트는 전달받은 정보를 바탕으로 직접 가져와야 합니다.</li>
<li><strong>설정 및 동작</strong>:<ul>
<li><strong>Blob path ends with</strong>: 설정한 값(예: .csv)으로 끝나는 파일에 대해 적용.</li>
<li><strong>이벤트 종류</strong>: Blob created(생성) 또는 Blob deleted(삭제).</li>
<li><strong>Ignore empty blobs</strong>: 비어 있는 블롭에 대한 처리 여부 설정.</li>
<li><strong>제한 사항</strong>: 파이프라인 실패 시 재시도 정책이 없으며, 동시성 정책이 없어 이벤트 발생 시마다 겹쳐서 수행될 수 있습니다.</li>
</ul>
</li>
</ul>
<h2 id="5-manual-트리거-상세"><strong>5. Manual 트리거 상세</strong></h2>
<p><img src="https://velog.velcdn.com/images/rudin_/post/347e1d9b-6342-44c1-83ab-75ab8d271055/image.png" alt=""></p>
<ul>
<li><strong>정의</strong>: ADF UI 내 메뉴를 통하거나 REST API를 사용하여 수동으로 파이프라인을 트리거합니다.</li>
<li><strong>방법</strong>: ADF UI 직접 트리거 또는 Azure Logic Apps와 연동하여 외부 요청에 따라 파이프라인을 실행합니다.</li>
</ul>
<hr>
<h2 id="6-실습-트리거-구성-과정"><strong>6. [실습] 트리거 구성 과정</strong></h2>
<h3 id="6-1-실습-준비-컨테이너-및-링크드-서비스"><strong>6-1. 실습 준비 (컨테이너 및 링크드 서비스)</strong></h3>
<ul>
<li><p><strong>컨테이너</strong>: <code>input</code>, <code>output</code> 컨테이너를 생성하고 <code>input</code>에 <code>iris.csv</code>를 업로드합니다.<img src="https://velog.velcdn.com/images/rudin_/post/c436f5a3-c126-48a4-ae5f-c801af782c48/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/9446d7e5-ac89-4afb-b31d-63e1122c1086/image.png" alt=""></p>
</li>
<li><p><strong>링크드 서비스</strong>: <code>BlobStorage1</code> (Azure Blob Storage) 생성.
input output을 구분하지 않고 쓸 수 있도록 새로 생성
<img src="https://velog.velcdn.com/images/rudin_/post/b6f475c8-394c-4957-a48a-895925c324ba/image.png" alt=""></p>
</li>
<li><p><strong>데이터세트</strong>: <code>inputCSV1</code>(input 컨테이너), <code>outputCSV1</code>(output 컨테이너) 생성. <code>inputCSV1</code>은 첫 번째 행을 머리글로 설정합니다.
<img src="https://velog.velcdn.com/images/rudin_/post/9398aba5-9324-43b5-865f-fcf77618ec77/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/a7825ed4-78b8-490b-ab2e-33f3266cd10a/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/d4863da9-0ee4-4623-ae31-5a46b237d638/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/f5d02ab0-e4f7-45c2-9a57-009f3629c755/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/b8ad5618-fd1b-4a5b-b13a-660a69f15a75/image.png" alt=""></p>
</li>
<li><p><strong>파이프라인</strong>: <code>pipeline1</code> 생성 후 <strong>Copy Data</strong> 활동을 배치하고 원본과 싱크를 연결합니다.
<img src="https://velog.velcdn.com/images/rudin_/post/770ac6a4-fefa-4b6e-8749-4782579c22f6/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/45e9b36e-f1e1-4f54-a1ec-ff04fbc74dc0/image.png" alt=""></p>
</li>
<li><p><strong>매개변수화</strong>: 데이터세트에 <code>fileName</code> 매개변수를 생성하고 파일 경로에 <code>@dataset().fileName</code> 동적 콘텐츠를 추가합니다. 파이프라인 테스트 시 <code>iris.csv</code>를 입력하여 성공 여부를 확인합니다.
<img src="https://velog.velcdn.com/images/rudin_/post/e610b786-e8b1-4399-8e2b-fcb43673bc68/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/c5498229-126e-4f13-acf9-132fe682121b/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/933117e3-eda6-4880-8b3b-f5e66e832bbb/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/d577e4fe-fa09-46db-82a5-113fdad2be49/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/15314407-28a3-40f5-8707-b2998b3427eb/image.png" alt=""></p>
</li>
<li><p><strong>디버깅</strong>: 파이프라인의 원본, 싱크에 값 입력
<img src="https://velog.velcdn.com/images/rudin_/post/8c23f100-7ab6-4220-82e4-b86c54f3f29e/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/5c89c844-45ef-404b-b5e5-24b50697f7d0/image.png" alt=""></p>
</li>
</ul>
<h3 id="6-2-schedule-트리거-실습"><strong>6-2. Schedule 트리거 실습</strong></h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/5cff2d40-6b4e-4d92-bfcd-176dbb5a41cc/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/d61fab51-3145-4426-a7e7-50e66a481290/image.png" alt=""></p>
<ul>
<li><p><strong>트리거 생성</strong>: <code>scheduleTrigger1</code> (형식: 일정, 주기: 15분) 생성 및 게시.</p>
</li>
<li><p><strong>검증</strong>: 모니터링 탭에서 성공 상태와 생성된 파일을 확인합니다.
<img src="https://velog.velcdn.com/images/rudin_/post/f2990e32-af28-452f-ab45-09075ba3310a/image.png" alt="">
트리거 편집에서 주 단위 고급 되풀이 옵션 확인 가능
<img src="https://velog.velcdn.com/images/rudin_/post/b33f58fa-e03c-44ee-b0b3-8297abca4bcd/image.png" alt=""></p>
</li>
<li><p><strong>One-to-Many 실습</strong>: <code>pipeline2</code>(Wait 활동 포함)를 생성하고 기존 <code>scheduleTrigger1</code>에 연결하여 두 파이프라인이 동시 실행되는 것을 확인합니다.
<img src="https://velog.velcdn.com/images/rudin_/post/089ef26a-fa67-49fc-8a52-7a5856b8bcfc/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/92dc2088-aaa9-4b6c-8e3a-ee13ab764665/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/df872bfe-3fa9-4818-a0b2-073084deaac2/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/19de17a8-93cd-4628-9769-ece8f80b94b3/image.png" alt="">
파이프라인은 여러개지만 트리거는 동일한 트리거
<img src="https://velog.velcdn.com/images/rudin_/post/bc8db634-6a74-420f-be0b-3697c7e37229/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/b00cd9a6-f5a5-4ae3-9458-7a146cfad3bd/image.png" alt="">
<code>관리</code>-<code>트리거</code>에서 중지 후 삭제 가능
<img src="https://velog.velcdn.com/images/rudin_/post/f77e197b-a1b8-4fc4-9828-0b6a4c589d0d/image.png" alt="">
중괄호 아이콘 선택 시 트리거 코드 확인 가능
<img src="https://velog.velcdn.com/images/rudin_/post/b21ee5c8-59d1-4dcf-b260-9d211343cbb4/image.png" alt=""></p>
</li>
</ul>
<h3 id="6-3-tumbling-window-트리거-실습"><strong>6-3. Tumbling Window 트리거 실습</strong></h3>
<p><img src="https://velog.velcdn.com/images/rudin_/post/150e190f-6ea9-4dfa-96fa-c6533f109463/image.png" alt="">
Tumbling Window는 오프셋 오류를 방지하기 위해 00초로 설정하는게 바람직
<img src="https://velog.velcdn.com/images/rudin_/post/5a5f0075-be9e-4970-8fef-059a7cb1cd10/image.png" alt="">
재시도 정책을 2로 하면 실패시 재시도를 최대 2번까지 함
오프셋은 보통 이전걸 참조해야하니 음수값을 많이 준다
창크기는 고정된 시간 기준으로 이전 몇개까지 진행됐던 걸 볼거냐를 설정 가능케 함</p>
<ul>
<li><p><strong>파이프라인 준비</strong>: <code>LoadData</code>(Wait 3초)와 <code>ProcessData</code>(Wait 5초) 파이프라인을 준비합니다.</p>
</li>
<li><p><strong>트리거 구성</strong>: <code>TW_LoadData1</code> 생성 후, <code>TW_ProcessData1</code> 생성 시 <strong>종속성 추가</strong>를 통해 <code>TW_LoadData1</code>이 성공한 후에만 실행되도록 설정합니다.
<img src="https://velog.velcdn.com/images/rudin_/post/04e47b7a-0c5a-46ae-a64a-2f5700201fe7/image.png" alt=""></p>
</li>
<li><p>만약 현재 시각이 08:00:00인 경우, 창 크기가 5분인 경우의 모든 종속성을 검토하고자 한다면 최소한 현재보다 5분 전으로 시작시간을 설정해야 함</p>
</li>
<li><p><strong>확인</strong>: 모니터링 화면에서 &#39;종속성 대기&#39; 및 &#39;성공&#39; 상태를 확인하고 Gantt 차트로 업스트림 관계를 검토합니다.
<img src="https://velog.velcdn.com/images/rudin_/post/7c5ce8d7-0900-46f3-b126-e91ccde0a3ec/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/984c488d-523a-4248-b268-af7320efe0c1/image.png" alt="">
<img src="https://velog.velcdn.com/images/rudin_/post/22ccdc33-3134-43d3-9eaf-8da893e7efa8/image.png" alt=""></p>
</li>
</ul>
<p>매번 변경사항마다 게시를 눌러야 적용됨을 잊지 말자 </p>
<h3 id="6-4-storage-event-트리거-실습"><strong>6-4. Storage Event 트리거 실습</strong></h3>
<ul>
<li><p><strong>준비</strong>: <code>fileName</code> 매개변수를 사용하는 <code>Copy CVS</code> 파이프라인을 생성합니다.(입출력 전부 fileName 매개변수 사용, input output 구분만 주의)</p>
</li>
<li><p><strong>트리거 생성</strong>: <code>SE_NewCSV</code> (이벤트: 생성됨, 끝 문자: .CSV) 생성 및 파이프라인 매개변수에 <code>@triggerBody().fileName</code>을 매핑합니다.
<img src="https://velog.velcdn.com/images/rudin_/post/7f7b0f1f-4944-4b2d-bfa4-e3180d32ec80/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/da171cab-0880-4a7e-ac63-61fcbcfeb009/image.png" alt=""></p>
</li>
<li><p><strong>테스트</strong>: <code>input</code> 컨테이너에 <code>penguins.csv</code>를 업로드하여 파이프라인이 자동 실행되는지 확인합니다.</p>
</li>
<li><p><strong>파일 삭제 실습</strong>: <strong>Delete</strong> 활동을 사용하는 파이프라인과 <code>SE_DeleteCSV</code>(이벤트: 삭제됨) 트리거를 생성하여 파일 삭제 시 로깅이 발생하는지 테스트합니다.
<img src="https://velog.velcdn.com/images/rudin_/post/2125edb1-6254-4dca-912b-844ce013d16a/image.png" alt="">
Delete시에는 항상 로깅을 기본으로 해야 함
<img src="https://velog.velcdn.com/images/rudin_/post/e1dab73e-830b-4893-834c-ac424f0d79a0/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/c5e2c418-8e40-4277-9df5-473ca48b92ea/image.png" alt="">
추가 및 실행 후 input에서 파일을 삭제 시 output에서도 삭제되는지 확인 </p>
</li>
</ul>
<h3 id="6-5-수동-트리거-logic-apps-실습"><strong>6-5. 수동 트리거 (Logic Apps) 실습</strong></h3>
<ul>
<li><p><strong>준비</strong>: <code>Copy CSV to CSV</code> 파이프라인과 관련 데이터세트(<code>inputCSV3</code>, <code>outputCSV3</code>)를 준비합니다.</p>
</li>
<li><p><strong>Logic Apps 구성</strong>: <code>A000-manual-trigger</code> 로직 앱을 생성하고, <strong>Recurrence</strong> 트리거와 <strong>Azure Data Factory - 파이프라인 실행 만들기</strong> 동작을 추가합니다.
<img src="https://velog.velcdn.com/images/rudin_/post/b19dfafb-61db-4cd8-8356-4dda3b868a34/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/e33485c8-35e3-4c8b-8e80-27365eed1f03/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/9aa98c17-c252-4d42-92ac-18c0da6e3937/image.png" alt=""></p>
</li>
<li><p><strong>매개변수 주입</strong>: 로직 앱에서 <code>{&quot;fileName&quot;:&quot;penguins.csv&quot;}</code> JSON 데이터를 전달하도록 설정합니다.</p>
</li>
<li><p><strong>확인</strong>: 로직 앱 실행 기록(Succeeded)과 ADF 파이프라인 모니터링(수동 트리거 항목)을 통해 최종 결과를 확인합니다.
<img src="https://velog.velcdn.com/images/rudin_/post/c427c8ed-248e-43a9-bec9-561115ff3790/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/361c5b6a-6ac9-46e8-a9c5-572884f2e56c/image.png" alt=""><img src="https://velog.velcdn.com/images/rudin_/post/a7755f7a-dc33-4b48-8ed5-df0333064f40/image.png" alt=""></p>
</li>
</ul>
<hr>
<h1 id="혹시몰라서-매개변수-등록-순서-정리">(혹시몰라서) 매개변수 등록 순서 정리</h1>
<p>Azure Data Factory(ADF)에서 매개변수를 설정하는 과정은 크게 <strong>데이터세트 수준의 설정</strong>, <strong>파이프라인 수준의 설정</strong>, 그리고 이 둘을 <strong>연결(매핑)하는 과정</strong>으로 나뉩니다. 전체적인 스텝을 순서대로 정리해 드립니다.</p>
<h3 id="1-데이터세트-매개변수-정의"><strong>1. 데이터세트 매개변수 정의</strong></h3>
<p>먼저 데이터를 동적으로 처리할 수 있도록 데이터세트 자체에 매개변수를 생성합니다.</p>
<ol>
<li><strong>데이터세트 열기</strong>: 수정할 데이터세트를 선택합니다.</li>
<li><strong>매개변수 탭 이동</strong>: 하단의 <strong>[매개변수]</strong> 탭을 클릭한 후 <strong>[+새로 만들기]</strong>를 통해 사용할 이름(예: <code>fileName</code>, <code>tableName</code>)과 형식을 지정합니다.</li>
<li><strong>동적 콘텐츠 적용</strong>: <strong>[연결]</strong> 탭으로 돌아가 동적으로 바뀔 항목(파일명, 테이블명 등)을 클릭하고 하단에 나타나는 <strong>[동적 콘텐츠 추가]</strong>를 선택합니다.</li>
<li><strong>식 작성</strong>: 파이프라인 식 작성기에서 앞서 만든 매개변수를 선택하여 <code>@dataset().매개변수명</code> 형태의 식이 입력되도록 합니다.</li>
</ol>
<h3 id="2-파이프라인-매개변수-정의"><strong>2. 파이프라인 매개변수 정의</strong></h3>
<p>파이프라인 실행 시 외부에서 값을 입력받을 수 있도록 설정합니다.</p>
<ol>
<li><strong>파이프라인 캔버스 클릭</strong>: 파이프라인 내 빈 공간을 클릭하여 하단 속성창을 활성화합니다.</li>
<li><strong>매개변수 추가</strong>: 하단의 <strong>[매개변수]</strong> 탭에서 <strong>[+새로 만들기]</strong>를 눌러 외부에서 주입받을 매개변수 이름과 형식을 정의합니다.</li>
</ol>
<h3 id="3-활동activity에서-매개변수-매핑"><strong>3. 활동(Activity)에서 매개변수 매핑</strong></h3>
<p>파이프라인 매개변수를 데이터세트 매개변수로 전달하는 과정입니다.</p>
<ol>
<li><strong>활동 선택</strong>: 파이프라인 내의 활동(예: 복사 활동)을 클릭합니다.</li>
<li><strong>원본/싱크 설정</strong>: 활동 속성의 <strong>[원본]</strong> 또는 <strong>[싱크]</strong> 탭으로 이동합니다.</li>
<li><strong>데이터세트 속성 입력</strong>: 해당 탭 하단의 <strong>데이터세트 속성</strong> 섹션에 이전에 정의한 데이터세트 매개변수들이 나열됩니다.</li>
<li><strong>파이프라인 매개변수 연결</strong>: 각 속성의 값 필드를 클릭하고 <strong>[동적 콘텐츠 추가]</strong>를 눌러 파이프라인 매개변수를 선택합니다. 식은 <code>@pipeline().parameters.매개변수명</code> 형태로 구성됩니다.</li>
</ol>
<p>이렇게 설정이 완료되면 파이프라인을 <strong>디버그</strong>할 때 팝업창을 통해 매개변수 값을 직접 입력하여 테스트할 수 있습니다.</p>
]]></description>
        </item>
    </channel>
</rss>