<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Infrastructure on kenji.blog</title><link>http://kenji.blog/ko/categories/infrastructure/</link><description>Recent content in Infrastructure on kenji.blog</description><generator>Hugo -- gohugo.io</generator><language>ko</language><copyright>kenjinote</copyright><lastBuildDate>Sun, 13 Sep 2026 07:00:00 +0900</lastBuildDate><atom:link href="http://kenji.blog/ko/categories/infrastructure/index.xml" rel="self" type="application/rss+xml"/><item><title>Hyper-V vs WSL2: Windows의 가상화 기술 비교</title><link>http://kenji.blog/ko/p/hyper-v-vs-wsl2-windows-virtualization/</link><pubDate>Sun, 13 Sep 2026 07:00:00 +0900</pubDate><guid>http://kenji.blog/ko/p/hyper-v-vs-wsl2-windows-virtualization/</guid><description>&lt;img src="http://kenji.blog/p/hyper-v-vs-wsl2-windows-virtualization/img/eyecatch.jpg" alt="Featured image of post Hyper-V vs WSL2: Windows의 가상화 기술 비교" />&lt;h2 id="1-소개-windows에서의-가상화-진화">1. 소개: Windows에서의 가상화 진화
&lt;/h2>&lt;p>Windows 플랫폼에서의 가상화 기술은 최근 수십 년 동안 극적인 진화를 이루었습니다. 과거에는 서드파티 Type 2 하이퍼바이저(VMware Workstation이나 VirtualBox 등)가 주류였으나, Microsoft가 Windows Server 2008에서 &amp;lsquo;Hyper-V&amp;rsquo;를 도입한 이후 Type 1 하이퍼바이저가 데스크톱 OS인 Windows 10/11에도 내장되기 시작했습니다.&lt;/p>
&lt;p>그리고 최근 개발자들 사이에서 가장 주목받고 있는 것이 &amp;lsquo;WSL2 (Windows Subsystem for Linux 2)&amp;lsquo;입니다. WSL1이 시스템 호출 변환(Translation)에 의존했던 반면, WSL2는 Hyper-V 기술을 응용한 &amp;lsquo;경량 유틸리티 VM (Lightweight Utility VM)&amp;lsquo;을 채택하여 완벽한 Linux 호환성과 비약적인 성능 향상을 실현했습니다.&lt;/p>
&lt;p>본 기사에서는 이 두 가지 강력한 가상화 기술, 즉 모든 기능을 갖춘 &amp;lsquo;Hyper-V&amp;rsquo;와 개발자 경험에 특화된 &amp;lsquo;WSL2&amp;rsquo;의 아키텍처, 성능(CPU, 메모리, 디스크 I/O), 네트워크 구성, 그리고 최적의 사용 사례에 대해 깊이 있는 기술적 세부 사항과 함께 철저하게 비교 및 해설합니다.&lt;/p>
&lt;hr>
&lt;h2 id="2-하이퍼바이저의-기초-이론과-아키텍처-비교">2. 하이퍼바이저의 기초 이론과 아키텍처 비교
&lt;/h2>&lt;p>가상화 기술을 이해하는 데 있어 필수적인 것이 하이퍼바이저(가상 머신 모니터: VMM)의 유형 분류입니다.&lt;/p>
&lt;h3 id="21-type-1과-type-2-하이퍼바이저의-차이">2.1. Type 1과 Type 2 하이퍼바이저의 차이
&lt;/h3>&lt;p>하이퍼바이저는 하드웨어에 대한 접근을 추상화하여 여러 OS(게스트 OS)를 단일 물리적 머신 위에서 동시에 실행하게 해주는 소프트웨어 계층입니다.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Type 1(베어메탈형)&lt;/strong>: 하드웨어 위에서 직접 실행됩니다. 호스트 OS라는 개념이 존재하지 않으며(엄밀히 말해 특권을 가진 관리 OS가 존재할 수 있습니다), 오버헤드가 매우 낮고 높은 성능과 보안을 제공합니다. 예: Hyper-V, VMware ESXi, Xen.&lt;/li>
&lt;li>&lt;strong>Type 2(호스트형)&lt;/strong>: 호스트 OS(Windows나 macOS 등) 위에서 애플리케이션으로 실행됩니다. 모든 하드웨어 접근은 호스트 OS를 거치기 때문에 오버헤드가 큽니다. 예: VMware Workstation, Oracle VirtualBox.&lt;/li>
&lt;/ul>
&lt;p>Windows의 Hyper-V는 순수한 &lt;strong>Type 1 하이퍼바이저&lt;/strong>입니다. Hyper-V를 활성화하면, 사실 평소 사용자가 조작하는 Windows OS 자체도 &amp;lsquo;루트 파티션(Root Partition)&amp;lsquo;이라 불리는 특수한 가상 머신 안에서 동작하게 됩니다.&lt;/p>
&lt;h3 id="22-hyper-v-아키텍처-상세">2.2. Hyper-V 아키텍처 상세
&lt;/h3>&lt;p>Hyper-V 아키텍처는 마이크로커널 설계를 채택하고 있으며, 파티션(Partition)이라는 논리적 분리 단위를 기반으로 합니다.&lt;/p>
&lt;pre class="mermaid">
graph TD
A[&amp;#34;하드웨어 (CPU, RAM, Disk, NIC)&amp;#34;] --&amp;gt; B[&amp;#34;Windows 하이퍼바이저 (Ring -1)&amp;#34;]
B --&amp;gt; C[&amp;#34;루트 파티션 (Windows OS)&amp;#34;]
B --&amp;gt; D[&amp;#34;자식 파티션 1 (Windows VM)&amp;#34;]
B --&amp;gt; E[&amp;#34;자식 파티션 2 (Linux VM)&amp;#34;]
C --&amp;gt; F[&amp;#34;VMBus (가상 머신 버스)&amp;#34;]
D --&amp;gt; F
E --&amp;gt; F
C --&amp;gt; G[&amp;#34;VID (가상화 인프라 드라이버)&amp;#34;]
C --&amp;gt; H[&amp;#34;VMWP.exe (워커 프로세스)&amp;#34;]
&lt;/pre>
&lt;ul>
&lt;li>&lt;strong>Windows 하이퍼바이저&lt;/strong>: CPU의 가장 높은 특권 수준(Ring -1 또는 VMX Root Mode)에서 동작하며, 메모리 할당과 CPU 스케줄링만을 담당합니다. 디바이스 드라이버는 포함되어 있지 않습니다.&lt;/li>
&lt;li>&lt;strong>루트 파티션&lt;/strong>: 호스트 Windows OS가 동작하는 파티션입니다. 모든 디바이스 드라이버를 가지며, 하드웨어를 직접 제어합니다. 또한, 자식 파티션의 관리 기능(WMI 프로바이더나 VMWP.exe 등)을 제공합니다.&lt;/li>
&lt;li>&lt;strong>자식 파티션&lt;/strong>: 게스트 OS가 동작하는 파티션입니다. 하드웨어에 대한 직접 접근은 허용되지 않으며, &amp;lsquo;VMBus&amp;rsquo;라는 논리적 메모리 공유 버스를 통해 루트 파티션으로 I/O 요청을 전송(Synthetic I/O)합니다.&lt;/li>
&lt;/ul>
&lt;h3 id="23-wsl2와-lightweight-utility-vm의-작동-원리">2.3. WSL2와 Lightweight Utility VM의 작동 원리
&lt;/h3>&lt;p>WSL2는 Hyper-V와 동일한 Type 1 하이퍼바이저 기반 기술을 사용하지만, 모든 기능을 갖춘 Hyper-V 가상 머신과는 다른 &amp;lsquo;가상 머신 플랫폼(Virtual Machine Platform: VMP)&amp;lsquo;이라는 서브셋 기능을 이용합니다.&lt;/p>
&lt;p>WSL2에 채택된 &amp;lsquo;경량 유틸리티 VM (Lightweight Utility VM)&amp;lsquo;은 기존 VM이 갖는 레거시 하드웨어 에뮬레이션(가상 BIOS나 가상 마더보드 등)을 완전히 배제했습니다.&lt;/p>
&lt;pre class="mermaid">
graph TD
A[&amp;#34;Windows 호스트 OS (유저 스페이스)&amp;#34;]
B[&amp;#34;NTFS 파일 시스템&amp;#34;]
C[&amp;#34;9P 프로토콜 서버 (Plan 9)&amp;#34;]
D[&amp;#34;경량 유틸리티 VM (Linux 커널)&amp;#34;]
E[&amp;#34;ext4.vhdx (가상 디스크)&amp;#34;]
F[&amp;#34;Linux 유저 스페이스 (WSL2 배포판)&amp;#34;]
A --&amp;gt; C
C --&amp;gt;| 크로스 OS 파일 공유 | D
D --&amp;gt; E
D --&amp;gt; F
&lt;/pre>
&lt;p>WSL2의 가장 큰 특징은 &lt;strong>빠른 시작 속도&lt;/strong>와 &lt;strong>호스트 OS와의 매끄러운 통합&lt;/strong>입니다. 수 초 내에 Linux 커널이 부팅되며, Windows 측의 파일 시스템(NTFS)에는 Plan 9의 &lt;code>9P&lt;/code> 네트워크 파일 시스템 프로토콜을 통해 접근합니다.&lt;/p>
&lt;hr>
&lt;h2 id="3-성능-철저-분석-컴퓨팅-리소스와-io">3. 성능 철저 분석: 컴퓨팅 리소스와 I/O
&lt;/h2>&lt;p>가상 머신의 성능은 CPU, 메모리, 그리고 디스크 I/O의 각 구성 요소에서 발생하는 오버헤드의 총합으로 나타납니다.&lt;/p>
&lt;h3 id="31-cpu와-컨텍스트-스위치-오버헤드">3.1. CPU와 컨텍스트 스위치 오버헤드
&lt;/h3>&lt;p>Hyper-V와 WSL2는 모두 하드웨어 지원 가상화(Intel VT-x / AMD-V)를 사용합니다. CPU 명령어는 기본적으로 네이티브 속도로 실행되지만, 특권 명령어를 실행하거나 I/O를 처리할 때는 &amp;lsquo;VM Exit&amp;rsquo;라 불리는 인터럽트가 발생하여 하이퍼바이저로 컨텍스트 스위치가 일어납니다.&lt;/p>
&lt;p>이때의 CPU 오버헤드 $T_{overhead}$는 다음과 같은 수학적 모델로 표현할 수 있습니다.&lt;/p>
$$ T_{overhead} = \sum_{i=1}^{N} (t_{vm\_exit} + t_{hypercall\_process} + t_{vm\_entry}) $$&lt;p>여기서:&lt;/p>
&lt;ul>
&lt;li>$N$: 단위 시간당 VM Exit 발생 횟수&lt;/li>
&lt;li>$t_{vm\_exit}$: 게스트에서 하이퍼바이저로의 전환 시간&lt;/li>
&lt;li>$t_{hypercall\_process}$: VMBus를 통한 I/O 처리 및 인터럽트 처리 시간&lt;/li>
&lt;li>$t_{vm\_entry}$: 하이퍼바이저에서 게스트로의 복귀 시간&lt;/li>
&lt;/ul>
&lt;p>WSL2는 레거시 에뮬레이션이 없기 때문에 $t_{hypercall\_process}$가 극히 작게 최적화되어 있습니다. 따라서 순수한 CPU 연산(예: 커널 컴파일이나 머신 러닝 모델 추론)에서는 베어메탈 환경과 비교해도 수 퍼센트 이내의 성능 저하에 그칩니다.&lt;/p>
&lt;h3 id="32-메모리-할당-메커니즘">3.2. 메모리 할당 메커니즘
&lt;/h3>&lt;p>메모리 관리 기법에 있어서 두 기술은 명확한 설계 철학의 차이를 보입니다.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Hyper-V (동적 메모리)&lt;/strong>: 게스트 VM의 메모리 수요에 따라 루트 파티션이 동적으로 메모리를 할당하고 회수합니다. 하지만 게스트 OS 내에서 페이지 캐시로 확보된 메모리는 시스템에 여유가 없는 이상 쉽게 해제되지 않는 경향이 있습니다.&lt;/li>
&lt;li>&lt;strong>WSL2 (동적 메모리 회수)&lt;/strong>: WSL2는 독자적인 방식을 가지고 있어, Linux VM 내에서 불필요해진 메모리(캐시 포함)를 정기적으로 Windows 호스트에 반환(Reclaim)합니다. 초기 WSL2에서는 Linux의 페이지 캐시가 Windows의 메모리를 모두 소진하는 문제(Vmmem 프로세스 비대화)가 있었으나, 현재는 커널 패치를 통해 개선되었습니다.&lt;/li>
&lt;/ul>
&lt;h3 id="33-디스크-io-특성-vhdx-vs-ext4vhdx">3.3. 디스크 I/O 특성 (VHDX vs ext4.vhdx)
&lt;/h3>&lt;p>가상 머신 성능에서 가장 병목 현상이 발생하기 쉬운 곳이 디스크 I/O입니다.&lt;/p>
&lt;p>I/O 레이턴시 $L_{total}$은 다음과 같이 계산됩니다.&lt;/p>
$$ L_{total} = L_{guest\_fs} + L_{vmbus} + L_{host\_fs} + L_{physical\_disk} $$&lt;p>&lt;strong>Hyper-V의 경우&lt;/strong>:
일반적인 Hyper-V 게스트는 &lt;code>VHDX&lt;/code> 포맷의 가상 디스크를 사용합니다. 게스트 OS 내의 파일 시스템(ext4 또는 NTFS)에서 발생한 I/O 요청은 VMBus의 블록 디바이스 스토리지 드라이버(storvsc)를 통과하여 Windows 측 NTFS 상의 VHDX 파일에 대한 접근으로 처리됩니다.&lt;/p>
&lt;p>&lt;strong>WSL2의 경우&lt;/strong>:
WSL2의 Linux 배포판은 전용 &lt;code>ext4.vhdx&lt;/code> 파일 내에 구축된 네이티브 ext4 파일 시스템 위에서 동작합니다. Linux 내에서의 파일 조작(&lt;code>~&lt;/code> 디렉터리 등)은 위에서 언급한 Hyper-V와 동등한 네이티브 성능을 발휘합니다.
하지만, &lt;strong>WSL2의 Linux에서 Windows 측의 파일(&lt;code>/mnt/c/&lt;/code> 등)에 접근할 경우&lt;/strong>, 혹은 그 반대의 경우, 처리 방식이 크게 달라집니다. 이러한 크로스 OS 접근에는 &lt;code>9P (Plan 9 File System Protocol)&lt;/code>가 사용됩니다.&lt;/p>
$$ L_{cross\_os} = L_{9p\_client} + L_{socket\_transfer} + L_{9p\_server} + L_{ntfs} $$&lt;p>이 9P 프로토콜을 경유한 접근은 직렬화(Serialize) 처리의 오버헤드가 커서, 작은 파일을 대량으로 읽고 쓰는 용도(예: Windows 측 디렉터리에 위치한 Node.js 프로젝트에서의 &lt;code>npm install&lt;/code>이나 Git 조작)에서는 성능이 현저히 떨어집니다(때로는 10배 이상의 지연 발생).
그러므로, &lt;strong>WSL2를 사용할 때는 반드시 프로젝트 파일을 Linux의 네이티브 파일 시스템(&lt;code>~/&lt;/code> 산하)에 배치하는 것이 철칙&lt;/strong>입니다.&lt;/p>
&lt;hr>
&lt;h2 id="4-네트워크-구조-nat-default-switch-bridged">4. 네트워크 구조: NAT, Default Switch, Bridged
&lt;/h2>&lt;p>네트워크 기능의 유연성은 Hyper-V와 WSL2의 큰 차이 중 하나입니다.&lt;/p>
&lt;h3 id="41-wsl2의-네트워크-nat-기반">4.1. WSL2의 네트워크 (NAT 기반)
&lt;/h3>&lt;p>WSL2의 네트워크는 기본적으로 Hyper-V의 가상 스위치 기술을 사용한 &amp;lsquo;NAT (Network Address Translation)&amp;rsquo; 구성으로 되어 있습니다.
Linux VM에는 Windows 호스트와는 다른 사설 IP 주소(예: &lt;code>172.20.x.x&lt;/code>)가 자동으로 할당됩니다. Windows 호스트에서는 &lt;code>localhost&lt;/code>를 통해 WSL2 내에서 실행된 서비스(포트)로 포워딩되는 구조가 내장되어 있어, 개발자는 네트워크를 의식하지 않고도 웹 서버 등을 테스트할 수 있습니다.&lt;/p>
&lt;p>최근 WSL2에는 &amp;lsquo;Mirrored 모드&amp;rsquo;라는 새로운 네트워크 모드가 프리뷰 버전으로 도입되었습니다. 이를 통해 IPv6 지원 및 VPN 연결 호환성이 향상되었습니다(&lt;code>.wslconfig&lt;/code>에서 설정 가능).&lt;/p>
&lt;h3 id="42-hyper-v의-가상-스위치-virtual-switch">4.2. Hyper-V의 가상 스위치 (Virtual Switch)
&lt;/h3>&lt;p>Hyper-V는 엔터프라이즈 수준의 고도화된 네트워크 구축이 가능합니다. &amp;lsquo;가상 스위치 관리자&amp;rsquo;를 통해 주로 세 가지 모드를 제공합니다.&lt;/p>
&lt;ol>
&lt;li>&lt;strong>외부 (External)&lt;/strong>: 호스트 머신의 물리적 NIC를 가상 스위치에 바인딩하여, 게스트 VM을 물리적 네트워크에 직접 참여시킵니다(브리지 연결). VM은 DHCP 서버로부터 물리 네트워크와 동일한 서브넷의 IP를 얻습니다.&lt;/li>
&lt;li>&lt;strong>내부 (Internal)&lt;/strong>: 호스트 OS와 VM 간, 그리고 VM 간의 통신만을 허용합니다. 외부 네트워크로 직접 나갈 수는 없습니다.&lt;/li>
&lt;li>&lt;strong>프라이빗 (Private)&lt;/strong>: VM 간의 통신만을 허용하며, 호스트 OS와의 통신도 차단합니다. 격리된 검증 환경을 구축할 때 사용됩니다.&lt;/li>
&lt;/ol>
&lt;h3 id="43-powershell을-이용한-고급-hyper-v-네트워크-구축">4.3. PowerShell을 이용한 고급 Hyper-V 네트워크 구축
&lt;/h3>&lt;p>개발이나 테스트 환경에서 VM용으로 사용자 정의된 NAT 네트워크를 구축하고 싶을 경우, PowerShell을 사용하면 세밀한 제어가 가능합니다. 다음은 내부 가상 스위치를 생성하고, 여기에 NAT를 구성하여 VM에 인터넷 액세스를 제공하는 스크립트 예시입니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-powershell" data-lang="powershell">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 1. 내부 가상 스위치 생성&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">$SwitchName&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;HyperV-NatSwitch&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">New-VMSwitch&lt;/span> &lt;span class="n">-SwitchName&lt;/span> &lt;span class="nv">$SwitchName&lt;/span> &lt;span class="n">-SwitchType&lt;/span> &lt;span class="n">Internal&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 2. 호스트 측 가상 NIC에 IP 주소 설정 (게이트웨이가 될 IP)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">$GatewayIP&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;192.168.100.1&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">$NetPrefix&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="mf">24&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">$InterfaceAlias&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;vEthernet (&lt;/span>&lt;span class="nv">$SwitchName&lt;/span>&lt;span class="s2">)&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">New-NetIPAddress&lt;/span> &lt;span class="n">-IPAddress&lt;/span> &lt;span class="nv">$GatewayIP&lt;/span> &lt;span class="n">-PrefixLength&lt;/span> &lt;span class="nv">$NetPrefix&lt;/span> &lt;span class="n">-InterfaceAlias&lt;/span> &lt;span class="nv">$InterfaceAlias&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 3. NAT 네트워크 구성&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">$NatName&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;HyperV-NatNetwork&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">$NatSubnet&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="s2">&amp;#34;192.168.100.0/24&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">New-NetNat&lt;/span> &lt;span class="n">-Name&lt;/span> &lt;span class="nv">$NatName&lt;/span> &lt;span class="n">-InternalIPInterfaceAddressPrefix&lt;/span> &lt;span class="nv">$NatSubnet&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 확인용 명령어&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">Get-NetNat&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>이 구성을 통해 지정한 Hyper-V 게스트에 수동으로 &lt;code>192.168.100.x&lt;/code>의 IP와 게이트웨이 &lt;code>192.168.100.1&lt;/code>을 설정함으로써, 호스트를 경유해 외부와 통신 가능한 독자적인 NAT 세그먼트를 구축할 수 있습니다.&lt;/p>
&lt;hr>
&lt;h2 id="5-사용-사례와-실전-선택-가이드">5. 사용 사례와 실전 선택 가이드
&lt;/h2>&lt;p>지금까지의 아키텍처와 성능 차이를 바탕으로, 어떤 상황에서 어떤 기술을 채택해야 할지 정의합니다.&lt;/p>
&lt;h3 id="51-wsl2를-선택해야-하는-시나리오">5.1. WSL2를 선택해야 하는 시나리오
&lt;/h3>&lt;p>WSL2는 &amp;lsquo;개발자의 생산성 향상&amp;rsquo;에 특화되어 설계되었습니다. 다음과 같은 용도에 최적입니다.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>웹 개발 및 클라우드 네이티브 개발&lt;/strong>: Docker Desktop(WSL2 백엔드)이나 Podman을 사용한 컨테이너 개발.&lt;/li>
&lt;li>&lt;strong>Linux 전용 도구 사용&lt;/strong>: bash, grep, awk, sed 또는 Linux용 GCC나 Clang 컴파일러를 일상적으로 사용하는 경우.&lt;/li>
&lt;li>&lt;strong>GUI 애플리케이션 (WSLg)&lt;/strong>: Linux의 X11/Wayland 애플리케이션을 Windows 데스크톱 위에서 매끄럽게 실행하고 싶은 경우.&lt;/li>
&lt;li>&lt;strong>머신 러닝 및 AI 개발&lt;/strong>: GPU 패스스루 기능(NVIDIA CUDA on WSL)을 이용한 TensorFlow나 PyTorch의 고속 학습.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>주의점&lt;/strong>: 커널을 세밀하게 커스터마이즈하고 싶거나, systemd에 강하게 의존하는 복잡한 서비스(현재 systemd는 지원되지만, 기본적으로 비활성화되어 있거나 제한이 있음)를 구축할 경우에는 제약이 따를 수 있습니다.&lt;/p>
&lt;h3 id="52-hyper-v를-선택해야-하는-시나리오">5.2. Hyper-V를 선택해야 하는 시나리오
&lt;/h3>&lt;p>Hyper-V는 &amp;lsquo;인프라스트럭처의 가상화와 완전한 격리&amp;rsquo;를 목적으로 합니다. 다음과 같은 용도에서는 필수가 됩니다.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Windows VM 실행&lt;/strong>: 다른 버전의 Windows(Windows Server나 오래된 Windows 10 등)를 테스트 환경으로 실행하는 경우.&lt;/li>
&lt;li>&lt;strong>중첩 가상화 (Nested Virtualization)&lt;/strong>: 가상 머신 안에서 또 다른 가상 머신(Hyper-V나 KVM)을 실행하고 싶은 경우. 인프라 엔지니어의 검증 환경에 필수적입니다.&lt;/li>
&lt;li>&lt;strong>고급 네트워크 요구사항&lt;/strong>: 외부 브리지 연결(동일 LAN 참여), VLAN 태깅, 다중 NIC 할당 등 네트워크 구성을 엄격하게 제어해야 하는 경우.&lt;/li>
&lt;li>&lt;strong>스냅샷 (체크포인트)&lt;/strong>: VM의 특정 시점 상태를 저장하고 언제든 즉시 롤백할 수 있는 기능. 소프트웨어의 파괴적인 테스트나 악성코드 분석 등에 매우 유용합니다.&lt;/li>
&lt;li>&lt;strong>고정 리소스 할당&lt;/strong>: CPU 코어 수나 메모리 양을 엄격하게 고정하여 호스트 OS에 미치는 영향을 최소화하고 싶은 경우.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="6-수학적-모델을-통한-io-처리량-고찰-부록">6. 수학적 모델을 통한 I/O 처리량 고찰 (부록)
&lt;/h2>&lt;p>시스템 엔지니어로서 두 기술의 I/O 성능 한계를 파악할 때, 처리량(Throughput) $S$와 블록 크기 $B$의 관계를 이론적으로 이해하는 것은 중요합니다.&lt;/p>
&lt;p>데이터 전송의 처리량 $S$는 단위 시간당 데이터 전송량이며, 다음과 같이 모델링됩니다.&lt;/p>
$$ S(B) = \frac{B}{L_{setup} + \frac{B}{R_{max}}} $$&lt;ul>
&lt;li>$B$: 블록 크기 (Bytes)&lt;/li>
&lt;li>$L_{setup}$: I/O 요청 설정 및 컨텍스트 스위치에 수반되는 고정 레이턴시&lt;/li>
&lt;li>$R_{max}$: 복사나 디바이스 전송 시 하드웨어의 최대 대역폭&lt;/li>
&lt;/ul>
&lt;p>WSL2의 9P 프로토콜을 통한 파일 접근에서는 이 $L_{setup}$이 매우 큽니다(소켓 통신과 프로토콜의 직렬화/역직렬화 때문). 따라서 블록 크기 $B$가 작을 경우(수 KB 정도의 작은 파일에 대한 대량의 읽기/쓰기), 분모에서 $L_{setup}$의 영향이 지배적이게 되어 처리량 $S$는 극적으로 떨어집니다.
반대로 Hyper-V의 VMBus를 경유하는 VHDX 접근에서는 $L_{setup}$이 하드웨어 인터럽트에 가까운 수준까지 최적화되어 있으므로, 작은 크기의 블록에서도 높은 IOPS를 유지할 수 있습니다.&lt;/p>
&lt;p>이러한 수학적 현실이 &amp;ldquo;WSL2에서는 프로젝트 파일을 Windows 측에 두면 안 된다&amp;quot;는 모범 사례의 논리적 근거가 됩니다.&lt;/p>
&lt;hr>
&lt;h2 id="7-결론-공존하는-두-가지-가상화-기술">7. 결론: 공존하는 두 가지 가상화 기술
&lt;/h2>&lt;p>Hyper-V와 WSL2는 어느 한쪽이 우수하다는 것이 아니라, **&amp;lsquo;목적이 다른 두 가지 솔루션&amp;rsquo;**입니다.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>WSL2&lt;/strong>는 Windows라는 OS의 틀을 깨고, Linux 생태계를 매끄럽고 빠르게 Windows 사용자에게 전달하기 위한 &amp;lsquo;최고의 통합 도구&amp;rsquo;입니다. 개발자를 위한 궁극의 CLI 환경이라 해도 과언이 아닙니다.&lt;/li>
&lt;li>&lt;strong>Hyper-V&lt;/strong>는 엔터프라이즈 데이터 센터에서 축적된 강력한 격리성과 관리 기능을 데스크톱으로 가져온 &amp;lsquo;본격적인 하이퍼바이저&amp;rsquo;입니다. 네트워크 구축, Windows OS 테스트, 인프라 환경 시뮬레이션에 있어서 타의 추종을 불허합니다.&lt;/li>
&lt;/ul>
&lt;p>현대의 Windows 환경에서 이 두 기술은 대등하게 경쟁하는 것이 아니라, 동일한 VM 플랫폼 위에서 아름답게 공존합니다. 목적에 맞게 적재적소에 활용함으로써, Windows는 세계에서 가장 강력하고 유연한 엔지니어링 워크스테이션이 될 것입니다.&lt;/p></description></item><item><title>Docker를 사용한 재현 가능한 로컬 개발 환경 구축 과정</title><link>http://kenji.blog/ko/p/docker-reproducible-local-dev-environment/</link><pubDate>Sun, 13 Sep 2026 01:00:00 +0900</pubDate><guid>http://kenji.blog/ko/p/docker-reproducible-local-dev-environment/</guid><description>&lt;img src="http://kenji.blog/p/docker-reproducible-local-dev-environment/img/eyecatch.jpg" alt="Featured image of post Docker를 사용한 재현 가능한 로컬 개발 환경 구축 과정" />&lt;h2 id="1-머리말-내-환경에서는-되는데에서-벗어나기">1. 머리말: &amp;ldquo;내 환경에서는 되는데&amp;quot;에서 벗어나기
&lt;/h2>&lt;p>소프트웨어 개발 현장에서 개발자 간의 환경 차이로 인해 발생하는 &amp;ldquo;내 환경에서는 되는데(It works on my machine)&amp;ldquo;라는 문제는 오랫동안 많은 프로젝트에서 시간을 낭비하게 만드는 요인이었습니다. OS의 차이, 설치된 언어의 버전, 라이브러리의 의존성, 전역으로 설치된 도구의 충돌 등 로컬 환경은 항상 &amp;lsquo;상태의 불확실성&amp;rsquo;에 노출되어 있습니다.&lt;/p>
&lt;p>이러한 과제를 근본적으로 해결하는 것이 &lt;strong>Docker&lt;/strong>를 비롯한 컨테이너 기술과 &lt;strong>Infrastructure as Code (IaC)&lt;/strong> 패러다임입니다. 로컬 개발 환경을 컨테이너화함으로써 OS 수준에서의 격리를 실현하고, 코드베이스와 함께 환경 자체를 버전 관리하는 것이 가능해집니다.&lt;/p>
&lt;p>본 문서에서는 Docker, Docker Compose, 그리고 VSCode DevContainers를 활용하여 **&amp;ldquo;누가, 언제, 어떤 머신에서 실행하더라도 한 치의 오차 없이 동일한 상태가 되는 재현 가능한 로컬 개발 환경&amp;rdquo;**을 구축하기 위한 단계와 그 이면에 있는 깊은 기술적 메커니즘에 대해 수리적 관점도 곁들여 철저하게 해설합니다.&lt;/p>
&lt;hr>
&lt;h2 id="2-infrastructure-as-code-iac-와-컨테이너-기술의-친화성">2. Infrastructure as Code (IaC) 와 컨테이너 기술의 친화성
&lt;/h2>&lt;h3 id="iac의-원칙과-로컬-환경에의-적용">IaC의 원칙과 로컬 환경에의 적용
&lt;/h3>&lt;p>Infrastructure as Code (IaC) 란 인프라스트럭처의 설정이나 프로비저닝을 수동 프로세스가 아닌 기계가 읽을 수 있는 정의 파일을 통해 관리하는 접근 방식입니다. IaC의 핵심이 되는 원칙에는 다음 요소들이 포함됩니다.&lt;/p>
&lt;ol>
&lt;li>&lt;strong>선언적 접근 (Declarative Approach)&lt;/strong>: &amp;lsquo;어떻게 상태를 변경할 것인가&amp;rsquo;가 아니라 &amp;lsquo;최종적으로 어떤 상태여야 하는가&amp;rsquo;를 정의합니다.&lt;/li>
&lt;li>&lt;strong>멱등성 (Idempotency)&lt;/strong>: 스크립트를 몇 번 실행하더라도 항상 동일한 결과(상태)가 보장됩니다.&lt;/li>
&lt;li>&lt;strong>버전 관리 (Version Control)&lt;/strong>: 인프라의 상태가 코드로서 Git 등의 VCS에 저장되어, 변경 이력 추적이나 피어 리뷰가 가능해집니다.&lt;/li>
&lt;/ol>
&lt;p>로컬 개발 환경에서 IaC를 실천한다는 것은 &lt;code>Dockerfile&lt;/code>이나 &lt;code>docker-compose.yml&lt;/code>, &lt;code>devcontainer.json&lt;/code>을 사용하여 개발 환경의 &amp;lsquo;이상적인 모습&amp;rsquo;을 코드화하는 것을 의미합니다. 이를 통해 새로 팀에 합류한 멤버도 리포지토리를 클론하고 명령어 하나를 실행하는 것만으로 즉시 개발을 시작할 수 있는 온보딩 경험을 실현할 수 있습니다.&lt;/p>
&lt;h3 id="컨테이너-기술을-지탱하는-커널-기능">컨테이너 기술을 지탱하는 커널 기능
&lt;/h3>&lt;p>컨테이너 기술은 가상 머신(VM)과 같은 하이퍼바이저형 가상화와는 달리, 호스트 OS의 커널을 공유하면서 프로세스를 격리(아이솔레이션)하는 경량 가상화 기술입니다. 이를 실현하기 위해 주로 Linux 커널의 다음과 같은 기능이 이용됩니다.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Namespaces&lt;/strong>: 프로세스마다 시스템 리소스(PID, 네트워크, 마운트 포인트, 사용자 등)의 독립적인 뷰를 제공합니다.&lt;/li>
&lt;li>&lt;strong>Cgroups (Control Groups)&lt;/strong>: 프로세스가 사용할 수 있는 물리 리소스(CPU, 메모리, 디스크 I/O 등)의 제한과 할당을 수행합니다.&lt;/li>
&lt;li>&lt;strong>UnionFS (Union File System)&lt;/strong>: 여러 디렉토리 트리(레이어)를 투명하게 겹쳐서 하나의 파일 시스템으로 보이게 하는 기술입니다. Docker의 이미지 레이어는 이 기술에 의존하고 있습니다.&lt;/li>
&lt;/ul>
&lt;p>리소스 제한의 수리 모델을 생각해 봅시다. 호스트 머신의 총 메모리 용량을 $M_{\text{total}}$로 하고, 호스트 상에서 동작하는 $n$개의 컨테이너 메모리 제한을 $m_i$라고 합니다. 시스템이 안정적으로 가동되기 위한 필요 조건은, 호스트 OS나 기타 프로세스가 소비하는 기본 메모리 $M_{\text{os}}$를 고려할 때 다음과 같은 부등식으로 나타낼 수 있습니다.&lt;/p>
$$ \sum_{i=1}^{n} m_i \le M_{\text{total}} - M_{\text{os}} $$&lt;p>Cgroups를 이용하여 컨테이너별로 $m_i$를 엄격하게 정의함으로써 특정 컨테이너가 메모리 누수를 일으켰을 때에도 OOM (Out Of Memory) Killer에 의해 다른 컨테이너나 호스트 시스템 전체가 다운되는 것을 방지할 수 있습니다.&lt;/p>
&lt;hr>
&lt;h2 id="3-효율적인-dockerfile-설계-멀티-스테이지-빌드-마스터하기">3. 효율적인 Dockerfile 설계: 멀티 스테이지 빌드 마스터하기
&lt;/h2>&lt;p>재현 가능한 환경의 첫걸음은 애플리케이션의 실행 환경을 정의하는 &lt;code>Dockerfile&lt;/code>의 설계입니다. 여기서는 Python (FastAPI) 을 예로 들어 &lt;strong>멀티 스테이지 빌드&lt;/strong>를 활용한 안전하고 가벼운 Dockerfile의 모범 사례를 해설합니다.&lt;/p>
&lt;p>멀티 스테이지 빌드는 하나의 &lt;code>Dockerfile&lt;/code> 안에서 여러 개의 &lt;code>FROM&lt;/code> 명령을 사용하여 빌드 환경(컴파일러나 개발 도구가 포함된 무거운 환경)과 실행 환경(필요한 결과물만 가지는 가벼운 환경)을 분리하는 기법입니다.&lt;/p>
&lt;h3 id="실전적인-python-fastapi용-dockerfile">실전적인 Python FastAPI용 Dockerfile
&lt;/h3>&lt;p>아래 코드는 Poetry를 사용한 의존성 관리와 멀티 스테이지 빌드를 결합한 고도화된 &lt;code>Dockerfile&lt;/code> 예제입니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-dockerfile" data-lang="dockerfile">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># ---------------------------------------------------------&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># Stage 1: Builder (빌드 환경)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># ---------------------------------------------------------&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="s"> python:3.11-slim AS builder&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 필요한 환경변수 설정&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">ENV&lt;/span> &lt;span class="nv">PYTHONUNBUFFERED&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="nv">PYTHONDONTWRITEBYTECODE&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="nv">POETRY_VERSION&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span>.6.1 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="nv">POETRY_HOME&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/opt/poetry&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="nv">POETRY_VIRTUALENVS_IN_PROJECT&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="nv">POETRY_NO_INTERACTION&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 의존 패키지 설치&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> apt-get update &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> apt-get install -y --no-install-recommends &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> curl build-essential &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> curl -sSL https://install.python-poetry.org &lt;span class="p">|&lt;/span> python3 - &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> apt-get clean &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> rm -rf /var/lib/apt/lists/*&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">ENV&lt;/span> &lt;span class="nv">PATH&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$POETRY_HOME&lt;/span>&lt;span class="s2">/bin:&lt;/span>&lt;span class="nv">$PATH&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">WORKDIR&lt;/span>&lt;span class="s"> /app&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 의존성 파일 복사 및 설치&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> pyproject.toml poetry.lock ./&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> poetry install --no-root --only main&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># ---------------------------------------------------------&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># Stage 2: Runtime (실행 환경)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># ---------------------------------------------------------&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="s"> python:3.11-slim AS runtime&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">ENV&lt;/span> &lt;span class="nv">PYTHONUNBUFFERED&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="nv">PYTHONDONTWRITEBYTECODE&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">1&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="nv">PATH&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;/app/.venv/bin:&lt;/span>&lt;span class="nv">$PATH&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 최소한의 비특권 사용자 생성&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> groupadd -r appuser &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> useradd -r -g appuser appuser&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">WORKDIR&lt;/span>&lt;span class="s"> /app&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 빌더로부터 가상 환경(의존성)만 복사&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> --from&lt;span class="o">=&lt;/span>builder --chown&lt;span class="o">=&lt;/span>appuser:appuser /app/.venv /app/.venv&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 애플리케이션 코드 복사&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> --chown&lt;span class="o">=&lt;/span>appuser:appuser ./src /app/src&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 비특권 사용자로 전환&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">USER&lt;/span>&lt;span class="s"> appuser&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="c"># 컨테이너 시작 시 기본 명령어&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">ENTRYPOINT&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;uvicorn&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;src.main:app&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;--host&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;0.0.0.0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;--port&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;8000&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="멀티-스테이지-빌드에-의한-이미지-크기의-수학적-평가">멀티 스테이지 빌드에 의한 이미지 크기의 수학적 평가
&lt;/h3>&lt;p>단일 스테이지에서 빌드했을 때의 이미지 크기를 $S_{\text{single}}$, 멀티 스테이지 빌드를 적용했을 때의 이미지 크기를 $S_{\text{multi}}$라고 합니다. 크기 감소율 $R$은 다음과 같이 계산됩니다.&lt;/p>
$$ R = \left( 1 - \frac{S_{\text{multi}}}{S_{\text{single}}} \right) \times 100 \ (\%) $$&lt;p>예를 들어 $S_{\text{single}}$에는 OS의 기본 이미지(약 110MB), 개발용 패키지(gcc 등 약 150MB), Poetry 본체(약 40MB), 프로젝트 의존 라이브러리(약 80MB), 소스 코드(약 5MB)가 포함되어 총 385MB가 되었다고 가정합시다.
반면 $S_{\text{multi}}$에서는 기본 이미지(110MB)에 의존 라이브러리(80MB)와 소스 코드(5MB)만 복사되므로 총 195MB가 됩니다.&lt;/p>
$$ R = \left( 1 - \frac{195}{385} \right) \times 100 \approx 49.35\% $$&lt;p>이처럼 멀티 스테이지 빌드를 도입함으로써 이미지 크기를 절반 정도로 줄일 수 있습니다. 이미지 크기 감소는 레지스트리로부터의 Pull 시간 단축, 디스크 용량 절약, 그리고 공격 표면(Attack Surface) 축소를 통한 보안 향상으로 직결됩니다.&lt;/p>
&lt;hr>
&lt;h2 id="4-docker-compose를-통한-여러-컨테이너-오케스트레이션">4. Docker Compose를 통한 여러 컨테이너 오케스트레이션
&lt;/h2>&lt;p>최신 웹 애플리케이션 개발에서는 웹 서버, 데이터베이스, 캐시 서버 등 여러 구성 요소가 연동하는 마이크로서비스 아키텍처가 일반적입니다. 로컬 환경에서 이들을 일원화하여 관리하기 위해 &lt;code>docker-compose.yml&lt;/code>을 사용합니다.&lt;/p>
&lt;p>이번에는 &amp;ldquo;Web (FastAPI)&amp;rdquo;, &amp;ldquo;Database (PostgreSQL)&amp;rdquo;, &amp;ldquo;Cache (Redis)&amp;ldquo;의 3계층 구조 시스템을 로컬에 구축합니다.&lt;/p>
&lt;h3 id="아키텍처-다이어그램-mermaid">아키텍처 다이어그램 (Mermaid)
&lt;/h3>&lt;p>다음 다이어그램은 로컬 머신에서의 각 컨테이너, 네트워크, 그리고 볼륨의 관계성을 나타낸 블록 다이어그램입니다.&lt;/p>
&lt;pre class="mermaid">
graph TD
User[&amp;#34;호스트 머신 (브라우저/curl)&amp;#34;] --&amp;gt;|Localhost:8000| Web[&amp;#34;FastAPI 웹 컨테이너&amp;#34;]
subgraph &amp;#34;Docker 브리지 네트워크 (app-network)&amp;#34;
Web --&amp;gt;|포트 5432| DB[&amp;#34;PostgreSQL 컨테이너&amp;#34;]
Web --&amp;gt;|포트 6379| Redis[&amp;#34;Redis 컨테이너&amp;#34;]
end
DB --&amp;gt; Volume1[&amp;#34;명명된 볼륨 (postgres_data)&amp;#34;]
Redis --&amp;gt; Volume2[&amp;#34;명명된 볼륨 (redis_data)&amp;#34;]
HostDir[&amp;#34;호스트 소스 코드 (./src)&amp;#34;] -.-&amp;gt;|바인드 마운트| Web
&lt;/pre>
&lt;h3 id="docker-composeyml-의-구현-및-상세-해설">docker-compose.yml 의 구현 및 상세 해설
&lt;/h3>&lt;p>다음은 실전적인 환경 구축에 견딜 수 있는 견고한 &lt;code>docker-compose.yml&lt;/code> 예제입니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;span class="lnt">25
&lt;/span>&lt;span class="lnt">26
&lt;/span>&lt;span class="lnt">27
&lt;/span>&lt;span class="lnt">28
&lt;/span>&lt;span class="lnt">29
&lt;/span>&lt;span class="lnt">30
&lt;/span>&lt;span class="lnt">31
&lt;/span>&lt;span class="lnt">32
&lt;/span>&lt;span class="lnt">33
&lt;/span>&lt;span class="lnt">34
&lt;/span>&lt;span class="lnt">35
&lt;/span>&lt;span class="lnt">36
&lt;/span>&lt;span class="lnt">37
&lt;/span>&lt;span class="lnt">38
&lt;/span>&lt;span class="lnt">39
&lt;/span>&lt;span class="lnt">40
&lt;/span>&lt;span class="lnt">41
&lt;/span>&lt;span class="lnt">42
&lt;/span>&lt;span class="lnt">43
&lt;/span>&lt;span class="lnt">44
&lt;/span>&lt;span class="lnt">45
&lt;/span>&lt;span class="lnt">46
&lt;/span>&lt;span class="lnt">47
&lt;/span>&lt;span class="lnt">48
&lt;/span>&lt;span class="lnt">49
&lt;/span>&lt;span class="lnt">50
&lt;/span>&lt;span class="lnt">51
&lt;/span>&lt;span class="lnt">52
&lt;/span>&lt;span class="lnt">53
&lt;/span>&lt;span class="lnt">54
&lt;/span>&lt;span class="lnt">55
&lt;/span>&lt;span class="lnt">56
&lt;/span>&lt;span class="lnt">57
&lt;/span>&lt;span class="lnt">58
&lt;/span>&lt;span class="lnt">59
&lt;/span>&lt;span class="lnt">60
&lt;/span>&lt;span class="lnt">61
&lt;/span>&lt;span class="lnt">62
&lt;/span>&lt;span class="lnt">63
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;3.8&amp;#39;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">services&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">web&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">build&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">context&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">.&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">target&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">runtime&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">container_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dev_web&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;8000:8000&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">./src:/app/src:ro &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 호스트의 코드를 읽기 전용으로 마운트(핫 리로드 용도)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">REDIS_URL=redis://redis:6379/0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">env_file&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">.env&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">depends_on&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">db&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">condition&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">service_healthy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">redis&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">condition&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">service_started&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">networks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">app-network&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;uvicorn&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;src.main:app&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;--host&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;0.0.0.0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;--port&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;8000&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;--reload&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">db&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">postgres:15-alpine&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">container_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dev_db&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;5432:5432&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">environment&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">POSTGRES_USER&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">postgres&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">POSTGRES_PASSWORD&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${POSTGRES_PASSWORD}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">POSTGRES_DB&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">${POSTGRES_DB}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">postgres_data:/var/lib/postgresql/data&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">networks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">app-network&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">healthcheck&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">test&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;CMD-SHELL&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;pg_isready -U postgres -d ${POSTGRES_DB}&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">interval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">5s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">timeout&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">5s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">retries&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">redis&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">redis:7-alpine&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">container_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dev_redis&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">ports&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="s2">&amp;#34;6379:6379&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">redis_data:/data&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">networks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">app-network&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;redis-server&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;--appendonly&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;yes&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">volumes&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">postgres_data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">redis_data&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">networks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">app-network&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">driver&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">bridge&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;h3 id="볼륨-volumes-과-데이터-영속화">볼륨 (Volumes) 과 데이터 영속화
&lt;/h3>&lt;p>컨테이너는 원칙적으로 &amp;lsquo;상태를 가지지 않는(스테이트리스)&amp;rsquo; 동시에 &amp;lsquo;단명(에페머럴)&amp;lsquo;하는 존재입니다. 컨테이너를 파기하면 내부 데이터도 함께 사라집니다. 데이터베이스의 데이터나 캐시를 유지하기 위해서는 호스트 머신의 파일 시스템 영역을 컨테이너에 마운트해야 합니다.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>바인드 마운트 (Bind Mount)&lt;/strong>: 위의 &lt;code>web&lt;/code> 서비스에서의 &lt;code>./src:/app/src:ro&lt;/code>가 이에 해당합니다. 호스트의 특정 디렉토리를 컨테이너 내부에 직접 매핑합니다. 로컬에서의 코드 편집을 즉각 컨테이너(핫 리로드)에 반영하기 위해 사용합니다. 보안 관점에서 &lt;code>:ro&lt;/code> (Read-Only) 옵션을 부여하여 컨테이너 측에서 호스트의 소스 코드를 변경할 수 없게 하는 것이 모범 사례입니다.&lt;/li>
&lt;li>&lt;strong>명명된 볼륨 (Named Volume)&lt;/strong>: &lt;code>postgres_data&lt;/code>나 &lt;code>redis_data&lt;/code>가 해당합니다. Docker가 내부적으로(&lt;code>/var/lib/docker/volumes/&lt;/code> 등) 관리하는 영역으로 바인드 마운트보다 I/O 성능이 뛰어나며 OS 간의 파일 시스템 차이를 흡수해 줍니다. 데이터베이스의 영속화에는 반드시 이것을 사용합니다.&lt;/li>
&lt;/ul>
&lt;h3 id="네트워크-networking-와-서비스-디스커버리">네트워크 (Networking) 와 서비스 디스커버리
&lt;/h3>&lt;p>Docker Compose는 기본적으로 프로젝트마다 고유한 브리지 네트워크를 생성합니다. 위의 &lt;code>app-network&lt;/code>가 그것입니다.
동일한 네트워크에 속하는 컨테이너끼리는 IP 주소가 아니라 &amp;lsquo;서비스 이름(예: &lt;code>db&lt;/code>, &lt;code>redis&lt;/code>)&amp;lsquo;을 호스트명으로 사용하여 이름 확인(DNS 확인)을 할 수 있습니다.
예를 들어 Web 컨테이너에서는 &lt;code>postgresql://postgres:password@db:5432/mydb&lt;/code>라는 URL로 데이터베이스에 접근할 수 있습니다. 이를 통해 로컬 환경이든 프로덕션 환경이든 환경 변수를 통해 접속처를 투명하게 전환할 수 있게 됩니다.&lt;/p>
&lt;h3 id="상태-확인헬스-체크과-실행-순서-제어">상태 확인(헬스 체크)과 실행 순서 제어
&lt;/h3>&lt;p>&lt;code>depends_on&lt;/code> 지시어는 컨테이너의 시작 순서를 제어하지만, 단순히 &lt;code>depends_on&lt;/code>만 지정하면 &amp;lsquo;DB 컨테이너가 시작된&amp;rsquo; 단계에서 Web 컨테이너가 시작되어 버립니다. 실제로는 DB의 초기화 프로세스(PostgreSQL의 프로세스 시작이나 테이블 준비)가 완료되기까지 몇 초가 걸리기 때문에 Web 컨테이너에서 DB로의 접속이 오류가 나는 경우가 있습니다.
이를 방지하기 위해 &lt;code>healthcheck&lt;/code>를 정의하고 &lt;code>condition: service_healthy&lt;/code>를 지정하여 &amp;lsquo;DB가 연결 요청을 받아들일 수 있는 상태가 되었음&amp;rsquo;을 확인한 후에 Web 컨테이너를 시작하게 할 수 있습니다.&lt;/p>
&lt;hr>
&lt;h2 id="5-환경-변수-관리와-보안-env">5. 환경 변수 관리와 보안 (.env)
&lt;/h2>&lt;p>데이터베이스의 비밀번호나 API 키 등 민감한 정보를 &lt;code>docker-compose.yml&lt;/code>에 하드코딩하는 것은 반드시 피해야 할 안티패턴입니다. 대신 환경 변수 파일 &lt;code>.env&lt;/code>를 사용하여 이 값들을 주입합니다.&lt;/p>
&lt;p>프로젝트 루트에 &lt;code>.env&lt;/code> 파일을 생성합니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-ini" data-lang="ini">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># .env 파일 (Git 관리 대상에서 제외하기 위해 .gitignore에 추가할 것)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">POSTGRES_PASSWORD&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">supersecretpassword&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">POSTGRES_DB&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">devdb&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="na">API_SECRET_KEY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">dev_secret_key_12345&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>Docker Compose는 기본적으로 실행 디렉토리에 있는 &lt;code>.env&lt;/code> 파일을 읽어 YAML 파일 안의 &lt;code>${VAR_NAME}&lt;/code>이라는 자리표시자를 전개합니다. 이 방식을 통해 로컬, 스테이징, 프로덕션 등 각 환경마다 다른 설정 값을 인프라 코드의 변경 없이 안전하게 관리할 수 있게 됩니다.&lt;/p>
&lt;hr>
&lt;h2 id="6-vscode-devcontainers에-의한-궁극의-개발-경험">6. VSCode DevContainers에 의한 궁극의 개발 경험
&lt;/h2>&lt;p>여기까지 Docker를 사용한 견고한 백엔드 환경을 구축했습니다. 하지만 여기서 한 걸음 더 나아갈 수 있습니다. &lt;strong>VSCode DevContainers (Remote - Containers)&lt;/strong> 기능을 사용하면 에디터(VSCode) 자체의 백엔드를 컨테이너 내부에서 실행하는 것이 가능해집니다.&lt;/p>
&lt;p>이를 통해 로컬 머신에는 Python이나 Node.js조차 설치할 필요가 없어지며, Linter(flake8/eslint)나 포매터(black/prettier), IDE의 확장 기능에 이르기까지 모든 것을 코드베이스 내에 정의하여 팀 전원과 공유할 수 있습니다.&lt;/p>
&lt;h3 id="devcontainerjson-설정">devcontainer.json 설정
&lt;/h3>&lt;p>프로젝트 루트에 &lt;code>.devcontainer&lt;/code> 디렉토리를 생성하고 그 안에 구성 파일을 배치합니다.&lt;/p>
&lt;p>&lt;code>.devcontainer/devcontainer.json&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt"> 1
&lt;/span>&lt;span class="lnt"> 2
&lt;/span>&lt;span class="lnt"> 3
&lt;/span>&lt;span class="lnt"> 4
&lt;/span>&lt;span class="lnt"> 5
&lt;/span>&lt;span class="lnt"> 6
&lt;/span>&lt;span class="lnt"> 7
&lt;/span>&lt;span class="lnt"> 8
&lt;/span>&lt;span class="lnt"> 9
&lt;/span>&lt;span class="lnt">10
&lt;/span>&lt;span class="lnt">11
&lt;/span>&lt;span class="lnt">12
&lt;/span>&lt;span class="lnt">13
&lt;/span>&lt;span class="lnt">14
&lt;/span>&lt;span class="lnt">15
&lt;/span>&lt;span class="lnt">16
&lt;/span>&lt;span class="lnt">17
&lt;/span>&lt;span class="lnt">18
&lt;/span>&lt;span class="lnt">19
&lt;/span>&lt;span class="lnt">20
&lt;/span>&lt;span class="lnt">21
&lt;/span>&lt;span class="lnt">22
&lt;/span>&lt;span class="lnt">23
&lt;/span>&lt;span class="lnt">24
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Python FastAPI Dev Environment&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;dockerComposeFile&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;../docker-compose.yml&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;service&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;web&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;workspaceFolder&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;/app&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;customizations&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;vscode&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;settings&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;python.defaultInterpreterPath&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;/app/.venv/bin/python&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;python.formatting.provider&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;black&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;editor.formatOnSave&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;extensions&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;ms-python.python&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;ms-python.vscode-pylance&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;ms-python.black-formatter&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="s2">&amp;#34;tamasfe.even-better-toml&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;forwardPorts&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="mi">8000&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">5432&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">6379&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;remoteUser&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;appuser&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;postCreateCommand&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;poetry install&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>이 파일을 리포지토리에 포함시켜 두면 VSCode에서 프로젝트를 여는 순간 &amp;ldquo;Reopen in Container&amp;quot;라는 프롬프트가 표시되고, 클릭하는 것만으로 필요한 모든 컨테이너가 켜지고 확장 기능이 설치되어 즉시 코딩을 시작할 수 있는 상태가 됩니다. 그야말로 마법 같은 경험입니다.&lt;/p>
&lt;hr>
&lt;h2 id="7-요청-처리-시퀀스와-성능-모델링">7. 요청 처리 시퀀스와 성능 모델링
&lt;/h2>&lt;p>구축한 로컬 개발 환경에서의 웹 애플리케이션 요청 처리 라이프사이클을 시퀀스 다이어그램으로 확인하고, 그 성능의 수리 모델을 고찰합니다.&lt;/p>
&lt;h3 id="시퀀스-다이어그램-요청-흐름">시퀀스 다이어그램 (요청 흐름)
&lt;/h3>&lt;pre class="mermaid">
sequenceDiagram
participant Client as &amp;#34;브라우저 / VSCode&amp;#34;
participant Web as &amp;#34;FastAPI (웹)&amp;#34;
participant Redis as &amp;#34;Redis 캐시&amp;#34;
participant DB as &amp;#34;PostgreSQL&amp;#34;
Client-&amp;gt;&amp;gt;Web: &amp;#34;GET /api/users/123&amp;#34;
activate Web
Web-&amp;gt;&amp;gt;Redis: &amp;#34;user:123에 대한 캐시 확인&amp;#34;
activate Redis
alt &amp;#34;캐시 적중 (데이터 있음)&amp;#34;
Redis--&amp;gt;&amp;gt;Web: &amp;#34;캐시된 사용자 데이터 반환&amp;#34;
Web--&amp;gt;&amp;gt;Client: &amp;#34;200 OK (빠른 응답)&amp;#34;
else &amp;#34;캐시 미스 (데이터 없음)&amp;#34;
Redis--&amp;gt;&amp;gt;Web: &amp;#34;Null (찾을 수 없음)&amp;#34;
deactivate Redis
Web-&amp;gt;&amp;gt;DB: &amp;#34;SELECT * FROM users WHERE id = 123&amp;#34;
activate DB
DB--&amp;gt;&amp;gt;Web: &amp;#34;데이터베이스 행 반환&amp;#34;
deactivate DB
Web-&amp;gt;&amp;gt;Redis: &amp;#34;user:123 데이터 설정 (TTL: 60s)&amp;#34;
activate Redis
Redis--&amp;gt;&amp;gt;Web: &amp;#34;OK&amp;#34;
deactivate Redis
Web--&amp;gt;&amp;gt;Client: &amp;#34;200 OK (표준 응답)&amp;#34;
end
deactivate Web
&lt;/pre>
&lt;h3 id="처리-지연레이턴시-수리-모델">처리 지연(레이턴시) 수리 모델
&lt;/h3>&lt;p>위 시스템에서의 평균 요청 처리 시간 $T_{\text{total}}$을 수리적으로 모델화합니다.
각 처리의 레이턴시를 다음과 같이 정의합니다.&lt;/p>
&lt;ul>
&lt;li>$T_{\text{net}}$: 클라이언트와 웹 컨테이너 간의 네트워크 레이턴시&lt;/li>
&lt;li>$T_{\text{app}}$: 애플리케이션 측의 순수한 처리 시간 (직렬화 등)&lt;/li>
&lt;li>$T_{\text{cache}}$: Redis로부터의 읽기/쓰기에 걸리는 시간&lt;/li>
&lt;li>$T_{\text{db}}$: PostgreSQL로의 쿼리 실행에 걸리는 시간&lt;/li>
&lt;li>$p_{\text{miss}}$: 캐시 미스 비율 ($0 \le p_{\text{miss}} \le 1$)&lt;/li>
&lt;/ul>
&lt;p>이 때, 평균적인 응답 시간은 다음의 기댓값 계산식으로 표현됩니다.&lt;/p>
$$ T_{\text{total}} = T_{\text{net}} + T_{\text{app}} + T_{\text{cache}} + p_{\text{miss}} \times (T_{\text{db}} + T_{\text{cache\_write}}) $$&lt;p>로컬 개발 환경(Docker 내부)에서는 $T_{\text{net}}$은 거의 0에 가까워지지만, 주목해야 할 것은 &lt;strong>바인드 마운트 시의 I/O 성능&lt;/strong>입니다. 특히 Windows/macOS 상에서 Docker Desktop을 사용하는 경우, 호스트 OS와 VM(컨테이너) 간의 파일 공유 오버헤드로 인해 $T_{\text{app}}$(코드 읽어들이는 시간 등)이 비대해지는 경향이 있습니다. 이 병목 현상을 해소하기 위해 앞서 언급한 DevContainers를 이용하여 소스 코드 전체를 명명된 볼륨 내에 배치하거나, WSL2 (Windows Subsystem for Linux 2) 환경 네이티브에서 Docker 엔진을 동작시키는 아키텍처가 강력히 권장됩니다.&lt;/p>
&lt;hr>
&lt;h2 id="8-docker-빌드-성능-최적화-레이어-캐시-전략">8. Docker 빌드 성능 최적화: 레이어 캐시 전략
&lt;/h2>&lt;p>Dockerfile을 작성할 때 &amp;lsquo;레이어 캐시&amp;rsquo;의 원리를 이해하고 있는지 여부에 따라 빌드 시간은 극적으로 달라집니다.
Docker는 Dockerfile의 각 명령(&lt;code>FROM&lt;/code>, &lt;code>RUN&lt;/code>, &lt;code>COPY&lt;/code> 등)마다 파일 시스템의 차분(레이어)을 생성하고 캐시로 유지합니다. 재빌드 시에는 변경이 없는 레이어의 캐시가 재사용됩니다.&lt;/p>
&lt;p>중요한 원칙은 **&amp;ldquo;변경 빈도가 낮은 것부터 순서대로 작성한다&amp;rdquo;**는 것입니다.&lt;/p>
&lt;p>소스 코드 변경이 빌드 시간에 미치는 영향의 모델화를 생각해 봅시다. 총 빌드 시간을 $T_{\text{build}}$, 각 단계의 실행 시간을 $T_{\text{layer}_i}$, 캐시 적중 여부를 불리언 값 $c_i \in \{0, 1\}$ (캐시 적중 시 1)이라고 합니다.&lt;/p>
$$ T_{\text{build}} = T_{\text{init}} + \sum_{i=1}^{n} (1 - c_i) \times T_{\text{layer}_i} $$&lt;p>한 번 레이어 $k$에서 캐시 미스($c_k = 0$)가 발생하면 그 이후의 모든 레이어 $j > k$에서 캐시가 무효화($c_j = 0$)됩니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-dockerfile" data-lang="dockerfile">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 나쁜 예 (소스 코드를 먼저 복사하고 있음)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> ./src /app/src&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> pyproject.toml poetry.lock ./&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> poetry install&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>위의 경우 코드를 한 줄만 수정해도 첫 번째 &lt;code>COPY&lt;/code>가 캐시 미스가 되어 시간이 오래 걸리는 &lt;code>RUN poetry install&lt;/code>이 매번 실행되고 맙니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;span class="lnt">2
&lt;/span>&lt;span class="lnt">3
&lt;/span>&lt;span class="lnt">4
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-dockerfile" data-lang="dockerfile">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># 좋은 예 (의존성 해결을 먼저 수행)&lt;/span>&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> pyproject.toml poetry.lock ./&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">RUN&lt;/span> poetry install&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="err">&lt;/span>&lt;span class="k">COPY&lt;/span> ./src /app/src&lt;span class="err">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;p>이렇게 작성하면 소스 코드를 변경해도 &lt;code>poetry install&lt;/code>의 레이어 캐시($c_i = 1$)가 작동하므로 빌드 시간은 수 분에서 수 초로 극적으로 단축됩니다.&lt;/p>
&lt;hr>
&lt;h2 id="9-트러블슈팅과-팁">9. 트러블슈팅과 팁
&lt;/h2>&lt;p>로컬 환경 운용 시 자주 마주치는 문제와 해결책을 꼽아보았습니다.&lt;/p>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>포트 충돌 오류&lt;/strong>
&lt;code>Bind for 0.0.0.0:8000 failed: port is already allocated&lt;/code>와 같은 오류가 난 경우, 로컬 머신의 다른 프로세스가 그 포트를 사용하고 있는 것입니다. 호스트 측의 포트 번호를 &lt;code>ports: - &amp;quot;8080:8000&amp;quot;&lt;/code>과 같이 변경하여 피할 수 있습니다.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>디스크 용량 고갈&lt;/strong>
장기간 Docker를 사용하다 보면 사용하지 않는 이미지나 볼륨(Dangling Images / Volumes)이 축적되어 수십 GB의 디스크 영역을 압박하는 경우가 있습니다. 정기적으로 다음 명령어를 통해 시스템을 정리하는 것을 권장합니다.&lt;/p>
&lt;div class="highlight">&lt;div class="chroma">
&lt;table class="lntable">&lt;tr>&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code>&lt;span class="lnt">1
&lt;/span>&lt;/code>&lt;/pre>&lt;/td>
&lt;td class="lntd">
&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">docker system prune -a --volumes
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/td>&lt;/tr>&lt;/table>
&lt;/div>
&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>파일 권한 문제&lt;/strong>
Linux 환경에서 바인드 마운트를 사용할 경우, 컨테이너 내부에서 생성된 파일의 소유자가 &lt;code>root&lt;/code>가 되어 호스트 측에서 편집할 수 없게 되는 경우가 있습니다. Dockerfile 내에서 비특권 사용자를 생성하고, 호스트 OS 자신의 UID/GID (예: 1000:1000)와 일치시킴으로써 이 문제를 해결할 수 있습니다.&lt;/p>
&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="10-맺음말-재현-가능성이-가져다주는-개발-속도-향상">10. 맺음말: 재현 가능성이 가져다주는 개발 속도 향상
&lt;/h2>&lt;p>Docker, Docker Compose, 그리고 VSCode DevContainers를 결합함으로써 &amp;ldquo;누가 환경을 띄워도 완전히 같은 상태가 되는&amp;rdquo; 견고한 로컬 개발 환경이 실현됩니다.&lt;/p>
&lt;p>IaC의 패러다임을 로컬 환경에 도입하는 것은 단순히 초기 설정 시간을 단축하는 것에 그치지 않습니다. 인프라 설정 변경에 대한 불안을 해소하고 새로운 기술 스택의 실험을 용이하게 하며 CI/CD 파이프라인으로의 원활한 전환을 가능하게 하는 등, 개발 사이클 전체의 속도와 품질을 비약적으로 향상시킵니다.&lt;/p>
&lt;p>본 문서에서 해설한 멀티 스테이지 빌드에 의한 이미지 크기 최적화나, 상태 확인을 사용한 의존성 제어, 레이어 캐시를 의식한 Dockerfile 작성 등의 모범 사례를 활용하여, 꼭 여러분의 프로젝트에도 최고의 개발 경험(DX: Developer Experience)을 도입해 보시기 바랍니다.&lt;/p></description></item></channel></rss>