<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[My dev journey ]]></title><description><![CDATA[Hi there, I'm a full time software engineering student, and full time mum based in Brisbane, QLD, Australia. 

Korean is my native language and English is my second, but I love learning software in English. 

My posts are a mix of both Korean and English, so there's something for everyone! 

All my posts come straight from my lecture notes, so follow along my study journey with me <3]]></description><link>https://heesu.tech</link><generator>RSS for Node</generator><lastBuildDate>Wed, 14 Oct 2026 01:24:14 GMT</lastBuildDate><atom:link href="https://heesu.tech/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Storage Device Management in Operating Systems]]></title><description><![CDATA[Storage Device Management Overview(저장장치 관리 개요)
이번 주차에서는 저장장치 관리(Storage Device Management)에 대해 학습한다. 프로그램 실행 과정에서 생성된 데이터는 전원이 꺼져도 유지되어야 하므로 보조기억장치(Secondary Storage)에 저장된다.
운영체제(Operating System)는 이러]]></description><link>https://heesu.tech/storage-device-management-in-operating-systems</link><guid isPermaLink="true">https://heesu.tech/storage-device-management-in-operating-systems</guid><dc:creator><![CDATA[Heesu Noh]]></dc:creator><pubDate>Thu, 11 Jun 2026 05:53:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/bfb4bbf9-1bc0-4ee0-8979-e4d5b90acccd.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<hr />
<h3>Storage Device Management Overview(저장장치 관리 개요)</h3>
<p>이번 주차에서는 <code>저장장치 관리(Storage Device Management)</code>에 대해 학습한다. 프로그램 실행 과정에서 생성된 데이터는 전원이 꺼져도 유지되어야 하므로 <code>보조기억장치(Secondary Storage)</code>에 저장된다.</p>
<p>운영체제(Operating System)는 이러한 데이터를 디스크(Disk) 구조에 맞게 효율적으로 저장하고, 필요할 때 빠르게 접근할 수 있도록 관리한다.</p>
<p>이번 시간에는 디스크의 구조(Disk Structure), 데이터 접근 방식(Data Access Method), 저장장치 관리의 전체 흐름을 중심으로 살펴본다.</p>
<p>이번 주차의 학습 목표는 다음과 같다. 먼저 디스크의 구조와 접근 시간(Access Time)의 구성 요소를 설명할 수 있어야 한다. 또한 디스크 접근 비용(Disk Access Cost)을 줄이기 위한 기법을 이해하고, RAID와 같은 저장 방식(Storage Method)의 특징도 설명할 수 있어야 한다.</p>
<p>마지막으로 실습을 통해 디스크 접근 특성(Disk Access Characteristics)과 성능 차이(Performance Difference)를 분석하는 것이 이번 주차의 핵심이다.</p>
<h3>Why Storage Device Management Is Needed</h3>
<p>개념 로딩(Concept Loading)을 통해 저장장치 관리(Storage Device Management)의 필요성을 생각해보자.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/979b31e2-8ce8-48bc-8966-ddaee8281166.png" alt="" style="display:block;margin:0 auto" />

<p>첫 번째 질문이 있다. 컴퓨터에서 파일을 복사하거나 프로그램을 실행할 때를 떠올려보자. 데이터의 크기가 비슷한데도 어떤 경우는 빠르게 처리되고, 어떤 경우는 유독 느리게 느껴지는 경험이 있다. 그렇다면 데이터의 크기가 비슷한데도 속도가 달라지는 이유는 무엇일까?</p>
<p>디스크(Disk)는 데이터를 어디에 저장하고, 어떤 순서로 접근하느냐에 따라 성능 차이가 발생한다. 즉, 단순히 데이터를 저장하는 것만으로는 충분하지 않고, <code>효율적인 접근(Efficient Access)을 위한 관리 방식</code>이 필요하다.</p>
<p>그래서 운영체제(Operating System)는 저장장치 관리(Storage Device Management)를 통해 디스크 접근(Disk Access)을 최적화하고, 전체 시스템 성능(System Performance)을 향상시킨다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/b80b7ae6-28eb-4374-9236-416c3db82b9c.png" alt="" style="display:block;margin:0 auto" />

<p>두 번째 질문이 있다. 여러 프로그램(Program)이 동시에 디스크에 접근 요청을 보내는 상황을 생각해보자. 이때 요청이 들어온 순서대로 처리하는 것이 항상 효율적일까? 아니면 처리 순서에 따라 성능이 달라질까?</p>
<p>만약 비효율적인 순서로 요청을 처리하면 디스크의 이동이 많아지고, 전체 처리 시간(Total Processing Time)이 증가할 수 있다. 이처럼 디스크에서는 요청을 어떤 순서로 처리하느냐에 따라 성능 차이가 발생한다.</p>
<p>따라서 운영체제는 디스크 스케줄링(Disk Scheduling)을 통해 요청 처리 순서(Request Processing Order)를 조정하고, 더 효율적인 디스크 접근이 이루어지도록 관리한다. 이번 시간에는 이러한 저장장치 관리의 필요성과 디스크 접근 최적화 과정을 중심으로 살펴본다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>디스크 스케줄링(Disk Scheduling)은 특히 하드 디스크(HDD, Hard Disk Drive)에서 중요하다. HDD는 데이터를 읽기 위해 디스크 헤드(Disk Head)가 이동해야 하므로, 요청 처리 순서에 따라 탐색 시간(Seek Time)이 크게 달라질 수 있다.</p>
<p>반면 SSD(Solid State Drive)는 물리적인 디스크 헤드가 없기 때문에 HDD보다 탐색 시간의 영향은 작지만, 운영체제는 여전히 입출력 요청(I/O Request)을 효율적으로 관리해야 한다.</p>
<hr />
<h3>Disk Structure and Access Process(디스크 구조와 접근 과정)</h3>
<p>저장장치(Storage Device)의 동작을 이해하기 위해 먼저 디스크(Disk)의 기본 구조와 데이터가 저장되는 물리적인 단위를 살펴본다.</p>
<p>디스크는 데이터를 일정한 <code>물리적 단위(Physical Unit)</code>로 저장하며, 운영체제(Operating System)는 이러한 구조를 바탕으로 데이터를 읽고 쓴다. 따라서 저장장치 관리를 이해하려면 디스크가 어떤 구조로 이루어져 있고, 데이터가 어떤 단위로 저장되는지 알아야 한다.</p>
<p>또한 디스크에 접근할 때는 단순히 데이터를 바로 읽는 것이 아니라, 여러 단계의 접근 과정이 발생한다. 대표적으로 <code>디스크 헤드(Disk Head)</code>가 원하는 위치로 이동하는 <code>탐색 시간(Seek Time),</code> 원하는 <code>섹터(Sector)</code>가 헤드 아래로 올 때까지 기다리는 <code>회전 지연 시간(Rotational Latency)</code>, 실제 데이터를 읽거나 쓰는 <code>전송 시간(Transfer Time)</code>이 포함된다.</p>
<p>이번 내용에서는 디스크의 기본 구조와 데이터 저장 단위, 그리고 탐색 시간(Seek Time), 회전 지연(Rotational Latency), 전송 시간(Transfer Time)을 중심으로 디스크 접근 과정(Disk Access Process)을 살펴본다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>디스크 접근 시간(Disk Access Time)은 보통 탐색 시간(Seek Time), 회전 지연 시간(Rotational Latency), 전송 시간(Transfer Time)을 합쳐서 이해한다. 특히 HDD(Hard Disk Drive)는 물리적으로 헤드가 움직이고 원판이 회전하기 때문에 이 요소들이 성능에 큰 영향을 준다.</p>
<hr />
<h3>Physical Disk Structure and Data Organization</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/170dd111-6c3c-447a-80ce-2dadd6f23581.png" alt="" style="display:block;margin:0 auto" />

<p><code>디스크(Disk)</code>는 여러 장의 <code>플래터(Platter)</code>로 이루어진 저장장치(Storage Device)이다. 플래터는 원형의 형태로 회전하면서 데이터를 저장한다.</p>
<p>각 플래터의 표면에는 동심원 형태의 <code>트랙(Track)</code>이 존재한다. 이 트랙은 다시 더 작은 단위인 <code>섹터(Sector)</code>로 나뉘며, 실제 데이터는 섹터 단위로 저장된다.</p>
<p>또한 여러 플래터에서 같은 위치에 있는 트랙들을 모으면 <code>실린더(Cylinder)</code>를 구성한다. 디스크는 이러한 플래터, 트랙, 섹터, 실린더 구조를 기반으로 데이터를 읽고 쓴다.</p>
<p>하지만 운영체제(Operating System)는 이러한 물리적 구조를 그대로 직접 사용하지 않는다. 대신 <code>논리 블록(Logical Block)</code> 단위를 기준으로 데이터를 관리한다.</p>
<p>즉, 디스크의 물리적인 구조 위에 논리적인 관리 단위를 두어 데이터를 효율적으로 저장하고 접근할 수 있도록 구성되어 있다고 이해할 수 있다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p><code>섹터(Sector)</code>는 디스크에서 실제 데이터가 저장되는 물리적 단위이고, <code>블록(Block)</code>은 파일 시스템(File System)이 데이터를 관리하는 논리적 단위이다.</p>
<p>즉, 운영체제는 사용자가 복잡한 플래터(Platter), 트랙(Track), 섹터(Sector) 구조를 직접 신경 쓰지 않아도 되도록 블록 단위로 추상화(Abstraction)해서 관리한다.</p>
<hr />
<h3>Physical Disk Structure Diagram(디스크의 물리 구조 그림 설명)</h3>
<p>그림은 디스크(Disk)의 물리 구조와 함께 <code>고정 헤드 디스크(Fixed-Head Disk)</code>와 <code>이동 헤드 디스크(Movable-Head Disk)</code>의 차이를 보여준다.</p>
<p>먼저 오른쪽의 <code>이동 헤드 디스크(Movable-Head Disk)</code>를 보면, 여러 장으로 겹쳐 있는 원판이 <code>플래터(Platter)</code>이다. 플래터는 실제 데이터가 저장되는 원판으로, 디스크의 저장 공간을 이루는 핵심 요소이다.</p>
<p>각 플래터 표면에는 동심원 형태의 <code>트랙(Track)</code>이 존재한다. 트랙은 데이터가 저장되는 원형 경로이며, 각 트랙은 다시 더 작은 단위인 <code>섹터(Sector)</code>로 나뉜다. 섹터는 실제 데이터가 기록되는 물리적인 저장 구역이다.</p>
<p>또한 여러 플래터에서 같은 반지름 위치에 있는 트랙들을 세로로 묶으면 <code>실린더(Cylinder)</code>가 된다. 실린더는 헤드(Head)가 같은 위치에 고정된 상태에서 접근할 수 있는 트랙들의 집합이라고 볼 수 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/b7373057-1871-4b22-92a3-8761e5b93f41.png" alt="" style="display:block;margin:0 auto" />

<p>플래터 옆으로 길게 뻗어 있는 구조는 <code>암(Arm)</code>이다. 암은 읽기/쓰기 헤드(Read/Write Head)를 원하는 트랙 위치까지 이동시키는 역할을 한다. 암 끝에 붙어 있는 작은 장치가 바로 읽기/쓰기 헤드이며, 이 헤드가 플래터 표면의 데이터를 실제로 읽거나 기록한다.</p>
<p>암 바깥쪽의 구동 장치는 <code>액추에이터(Actuator)</code>로 볼 수 있다. 이 장치는 암을 안쪽이나 바깥쪽으로 움직여, 헤드가 원하는 트랙으로 이동할 수 있도록 한다.</p>
<p>중앙에 세로로 연결된 부분은 <code>축(Spindle)</code>이다. 축은 플래터를 회전시키는 역할을 하며, 디스크는 이 회전을 통해 원하는 섹터가 헤드 아래에 오도록 해서 데이터를 읽고 쓴다.</p>
<p>이제 왼쪽의 고정 헤드 디스크(Fixed-Head Disk)를 보면, 각 트랙마다 고정된 읽기/쓰기 헤드가 하나씩 배치되어 있다. 즉 트랙마다 전용 헤드가 따로 존재하기 때문에 헤드를 이동시키지 않고도 해당 트랙에 바로 접근할 수 있다. 그래서 접근 속도가 빠르다는 특징이 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/61f0157f-78fd-48cd-a474-24bcb6c38293.png" alt="" style="display:block;margin:0 auto" />

<p>반면 <code>이동 헤드 디스크(Movable-Head Disk)</code>는 하나의 읽기/쓰기 헤드가 여러 트랙 사이를 이동하면서 접근하는 구조이다. 따라서 필요한 트랙으로 헤드를 옮기는 과정이 필요하고, 이로 인해 탐색 시간(Seek Time)이 발생한다. 이러한 헤드 이동 시간은 디스크 접근 시간(Access Time)에 영향을 준다.</p>
<p>정리하면, 고정 헤드 디스크는 각 트랙마다 헤드가 따로 있어 빠르게 접근할 수 있고, 이동 헤드 디스크는 하나의 헤드가 여러 트랙을 이동하면서 접근하기 때문에 구조는 비교적 단순하지만 접근 시간의 영향을 더 크게 받는다. 즉 이 그림은 디스크를 구성하는 요소들의 위치와 역할, 그리고 고정 헤드 방식과 이동 헤드 방식의 차이를 함께 보여주는 구조라고 할 수 있다.</p>
<p>이동 헤드 디스크(Movable-Head Disk)가 많이 사용되는 이유는 큰 저장 용량을 더 효율적이고 경제적으로 구현할 수 있기 때문이다. 고정 헤드 디스크(Fixed-Head Disk)는 각 트랙마다 별도의 헤드가 필요하기 때문에 트랙 수가 많아질수록 구조가 복잡해지고 비용이 증가한다. 반면 이동 헤드 디스크는 하나의 헤드가 여러 트랙을 이동하며 접근하므로 탐색 시간(Seek Time)은 발생하지만, 더 적은 장치로 많은 데이터를 저장할 수 있다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>실제 현대 하드디스크(HDD, Hard Disk Drive)는 대부분 이동 헤드 디스크(Movable-Head Disk) 구조를 사용한다. 고정 헤드 디스크(Fixed-Head Disk)는 개념 설명용으로 자주 등장하지만, 현재는 일반적으로 널리 사용되지는 않는다.</p>
<p>또한 SSD(Solid State Drive)는 플래터, 트랙, 섹터, 헤드 같은 기계적 구조가 없기 때문에 HDD와는 다른 방식으로 데이터에 접근한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/031cc0de-d04d-413b-ad81-a3481b3c329a.png" alt="" style="display:block;margin:0 auto" />

<h3>Logical Blocks and Physical Sectors(논리 블록과 물리 섹터)</h3>
<p>디스크(Disk)의 물리 구조 위에서 데이터가 실제로 어떤 단위로 관리되는지 살펴보자.</p>
<p>디스크에서 데이터를 저장하는 가장 작은 물리적 단위는 <code>섹터(Sector)</code>이다. 하지만 운영체제(Operating System)는 일반적으로 이 섹터를 직접 하나씩 제어하지 않는다. 섹터 단위의 실제 읽기와 쓰기는 <code>디스크 컨트롤러(Disk Controller)</code>와 같은 하드웨어 계층에서 처리된다.</p>
<p>섹터 하나의 크기는 작기 때문에 운영체제가 이를 그대로 관리하면 비효율적일 수 있다. 그래서 운영체제는 여러 개의 섹터를 묶어 <code>논리 블록(Logical Block)</code> 단위로 관리한다.</p>
<p>즉, 파일(File)은 물리적인 섹터 단위가 아니라 논리적인 블록 단위로 디스크에 저장되고 접근된다. 이 과정에서 사용자가 보는 파일의 위치와 실제 디스크에 저장된 물리적 위치는 서로 다를 수 있다.</p>
<p>따라서 운영체제는 복잡한 물리적 저장 구조를 사용자에게 직접 노출하지 않고, 논리 블록 단위를 통해 더 효율적인 저장과 접근이 가능하도록 관리한다고 이해할 수 있다.</p>
<hr />
<h3>Logical Blocks and Physical Sectors Diagram</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/5ec0125f-e0da-40ff-98d2-8936b4e0f069.png" alt="" style="display:block;margin:0 auto" />

<p>이 그림은 디스크(Disk)에서 데이터가 저장되는 <code>물리적 구조</code>를 보여준다.</p>
<p>오른쪽의 원형 그림을 보면 여러 개의 작은 구역으로 나뉘어 있는 부분이 보이는데, 각각의 구역이 <code>섹터(Sector)</code>이다. 섹터는 실제 데이터가 저장되는 가장 작은 물리적 단위이다.</p>
<p>이 섹터들은 원형의 경로를 따라 배열되어 있는데, 이 원형의 경로를 <code>트랙(Track)</code>이라고 한다. 즉 하나의 트랙은 여러 개의 섹터로 구성된다.</p>
<p>왼쪽 그림을 보면 여러 개의 <code>플래터(Platter)</code>가 쌓여 있는 구조를 확인할 수 있다. 이때 여러 플래터에서 같은 반지름 위치에 있는 트랙들을 세로로 연결해서 보면 이를 <code>실린더(Cylinder)</code>라고 한다.</p>
<p>즉 이 그림은 디스크에서 데이터가 트랙과 섹터 단위로 나뉘어 저장되고, 여러 플래터를 기준으로는 실린더 구조로 묶여 관리된다는 점을 보여준다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>운영체제(Operating System)는 이러한 물리적 구조를 직접 사용자에게 보여주지 않는다. 대신 여러 섹터를 묶은 논리 블록(Logical Block) 단위로 데이터를 관리한다.</p>
<p>즉, 물리적으로는 섹터(Sector)와 트랙(Track) 구조를 가지지만, 논리적으로는 블록(Block) 단위로 저장하고 접근한다고 이해하면 된다.</p>
<hr />
<h3>Components of Disk Access Time(디스크 접근 시간의 구성)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/642fffa0-5046-47bb-986b-4b2c87b47687.png" alt="" style="display:block;margin:0 auto" />

<p>디스크(Disk)에서 데이터를 읽고 쓰는 과정은 메모리(Memory)처럼 주소를 바로 읽는 방식과 다르다. 디스크는 <code>기계적인 동작(Mechanical Operation)</code>이 함께 이루어지기 때문에 데이터 요청이 즉시 처리되지 않는다.</p>
<p>먼저 <code>디스크 헤드(Disk Head)</code>가 원하는 <code>트랙(Track)</code> 위치로 이동해야 한다. 그다음 <code>플래터(Platter)</code>가 회전하면서 필요한 <code>섹터(Sector)</code>가 헤드 아래로 도달해야 한다. 이후에야 실제 데이터의 읽기(Read), 쓰기(Write), 전송(Transfer)이 이루어진다.</p>
<p>따라서 <code>디스크 접근 시간(Disk Access Time)</code>은 하나의 요소로만 결정되지 않는다. <code>탐색 시간(Seek Time), 회전 지연 시간(Rotational Latency), 전송 시간(Transfer Time)</code>과 같은 여러 단계가 순차적으로 진행되면서 전체 접근 시간이 만들어진다.</p>
<p>각 단계에서 소요되는 시간이 누적되면 전체 디스크 성능(Disk Performance)과 입출력 성능(I/O Performance)에 영향을 준다. 결국 디스크 접근 시간은 저장장치 성능(Storage Device Performance)을 결정하는 중요한 요소라고 할 수 있다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>디스크 접근 시간(Disk Access Time)은 일반적으로 다음과 같이 이해할 수 있다.</p>
<p><code>Disk Access Time = Seek Time + Rotational Latency + Transfer Time</code></p>
<p>이 개념은 주로 하드 디스크(HDD, Hard Disk Drive)에 해당한다. SSD(Solid State Drive)는 기계적으로 움직이는 헤드나 회전하는 플래터가 없기 때문에 탐색 시간과 회전 지연의 영향이 거의 없다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/be9bc0f4-c047-497f-b94f-70f579ed6ce4.png" alt="" style="display:block;margin:0 auto" />

<h3>Components of Disk Access Time Diagram</h3>
<p>이 그림은 <code>디스크 접근 시간(Disk Access Time)</code>이 어떤 요소들로 구성되는지를 보여준다.</p>
<p>먼저 이동 헤드 디스크(Movable-Head Disk)에서는 읽기/쓰기 헤드(Read/Write Head)가 데이터가 저장된 트랙(Track) 위치까지 이동해야 한다. 이때 헤드가 원하는 트랙으로 이동하는 시간을 <code>탐색 시간(Seek Time)</code>이라고 한다.</p>
<p>그다음 헤드가 원하는 트랙에 도착하더라도, 필요한 데이터가 바로 헤드 아래에 있는 것은 아니다. 디스크가 회전하면서 원하는 섹터(Sector)가 헤드 아래로 올 때까지 기다려야 하는데, 이 시간을 <code>회전 지연 시간(Rotational Latency)</code>이라고 한다.</p>
<p>이후 원하는 섹터가 헤드 아래에 도착하면 실제로 데이터를 읽거나 쓰고, 그 데이터를 전송하는 과정이 이루어진다. 이때 소요되는 시간을 <code>전송 시간(Transfer Time)</code>이라고 한다.</p>
<p>즉, 디스크 접근 시간은 탐색 시간(Seek Time), 회전 지연 시간(Rotational Latency), 전송 시간(Transfer Time)이 합쳐져서 결정된다.</p>
<p>이 세 가지 요소 중 어느 하나라도 길어지면 전체 디스크 성능(Disk Performance)과 입출력 성능(I/O Performance)에 영향을 주게 된다. 따라서 디스크 접근 시간은 저장장치 성능(Storage Device Performance)을 결정하는 중요한 요소라고 할 수 있다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>전송 시간(Transfer Time)은 단순히 기다리는 시간이 아니라, 원하는 섹터가 도착한 뒤 실제 데이터를 읽거나 쓰고 전달하는 데 걸리는 시간이다.</p>
<p>보통 디스크 접근 시간은 다음과 같이 정리한다.</p>
<p><code>Disk Access Time = Seek Time + Rotational Latency + Transfer Time</code></p>
<p>이 개념은 주로 HDD(Hard Disk Drive)에 해당하며, SSD(Solid State Drive)는 기계적인 헤드 이동과 회전이 없기 때문에 탐색 시간과 회전 지연의 영향이 거의 없다.</p>
<hr />
<h3>Seek Time, Rotational Latency, and Transfer Time</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/895c3c55-c7cc-4264-8376-2c67ac6ce8e0.png" alt="" style="display:block;margin:0 auto" />

<p>앞에서 디스크 접근 시간(Disk Access Time)을 구성하는 세 가지 요소를 살펴보았다. 이번에는 <code>탐색 시간(Seek Time), 회전 지연 시간(Rotational Latency), 전송 시간(Transfer Time)</code>을 조금 더 자세히 정리해보자.</p>
<p>먼저 <code>탐색 시간(Seek Time)</code>은 디스크 헤드(Disk Head)가 현재 위치에서 요청된 데이터가 저장된 트랙(Track)까지 이동하는 데 걸리는 시간이다. 즉, 데이터를 읽거나 쓰기 전에 헤드가 먼저 원하는 위치로 이동해야 한다는 의미이다.</p>
<p><code>탐색 시간</code>은 디스크 접근 시간 중 큰 비중을 차지하는 요소이다. 헤드가 이동해야 하는 거리가 멀수록 더 많은 시간이 필요하다. 따라서 데이터가 디스크의 어느 위치에 저장되어 있는지에 따라 전체 처리 속도와 성능 차이가 발생할 수 있다. 이후에 살펴볼 디스크 스케줄링(Disk Scheduling) 기법도 이러한 헤드 이동을 줄이는 방향으로 활용된다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/14675820-9efc-4bef-adfc-42ed9a9f1038.png" alt="" style="display:block;margin:0 auto" />

<p>다음으로 <code>회전 지연 시간(Rotational Latency)</code>은 헤드가 원하는 트랙에 도착한 이후, 목표 섹터(Sector)가 헤드 아래에 올 때까지 기다리는 시간을 의미한다. 즉, 헤드가 올바른 트랙에 도착했더라도 필요한 데이터가 바로 아래에 없다면 디스크가 회전할 때까지 기다려야 한다.</p>
<p>디스크는 일정한 속도로 계속 회전하기 때문에 이 과정에서 추가적인 대기 시간이 발생한다. 평균적으로는 한 바퀴 회전 시간의 절반 정도가 회전 지연 시간으로 발생한다고 볼 수 있다. 따라서 같은 트랙 안에 데이터가 있더라도 섹터의 위치에 따라 실제 접근 시간은 달라질 수 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/d02cc909-d9b1-42e8-af31-0fe715cc2396.png" alt="" style="display:block;margin:0 auto" />

<p>마지막으로 <code>전송 시간(Transfer Time)</code>은 원하는 위치를 찾은 이후, 데이터를 실제로 읽거나 쓰고 메모리(Memory)로 전달하는 데 걸리는 시간이다. 탐색 시간과 회전 지연 시간이 데이터 접근을 위한 준비 과정이라면, 전송 시간은 실제 데이터 처리가 이루어지는 단계라고 볼 수 있다.</p>
<p>전송 시간은 디스크와 메모리 사이의 전송 속도(Transfer Rate)와 데이터 크기(Data Size)에 따라 달라진다. 전송해야 할 데이터의 양이 많을수록 더 많은 시간이 필요하고, 데이터가 적다면 전송 시간도 짧아진다. 다만 일반적으로 탐색 시간과 회전 지연 시간에 비해서는 상대적으로 짧은 편이다.</p>
<p>정리하면, 디스크 접근 시간은 탐색 시간, 회전 지연 시간, 전송 시간이 합쳐져 결정된다. 이 중 특히 탐색 시간과 회전 지연 시간은 HDD(Hard Disk Drive)의 기계적인 구조 때문에 발생하는 중요한 성능 요소이다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>평균 회전 지연 시간(Average Rotational Latency)은 보통 디스크가 한 바퀴 도는 시간의 절반으로 계산한다. 예를 들어 디스크가 한 바퀴 도는 데 10ms가 걸린다면, 평균 회전 지연 시간은 약 5ms로 볼 수 있다.</p>
<p>또한 이 개념은 주로 HDD(Hard Disk Drive)에 해당한다. SSD(Solid State Drive)는 회전하는 플래터(Platter)나 이동하는 헤드(Head)가 없기 때문에 탐색 시간과 회전 지연 시간이 거의 발생하지 않는다.</p>
<hr />
<h3>Why Seek Time Is Important(탐색 시간이 중요한 이유)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/7f5e001d-98b8-4730-8f8a-f7cdb44ad8ce.png" alt="" style="display:block;margin:0 auto" />

<p>디스크 접근 시간(Disk Access Time)을 구성하는 세 가지 요소 중 일반적으로 <code>탐색 시간(Seek Time)</code>이 전체 접근 시간에서 큰 비중을 차지하는 경우가 많다.</p>
<p>가장 큰 이유는 디스크 헤드(Disk Head)가 원하는 트랙(Track)까지 실제로 물리적으로 이동해야 하기 때문이다. 즉, 단순한 전기적 신호 처리만으로 끝나는 것이 아니라 기계적인 움직임(Mechanical Movement)이 동반되기 때문에 시간이 더 많이 필요하다.</p>
<p>또한 헤드가 이동해야 하는 거리에 따라 소요 시간이 크게 달라진다. 가까운 트랙으로 이동할 때와 먼 트랙으로 이동할 때의 차이가 크기 때문에, 탐색 시간은 회전 지연 시간(Rotational Latency)보다 변동 폭이 크게 나타날 수 있다.</p>
<p>따라서 디스크 접근 시간에서 탐색 시간은 중요한 성능 요소로 다루어진다. 이후에 살펴볼 <code>디스크 스케줄링(Disk Scheduling)</code>도 이러한 헤드 이동 거리를 줄여 전체 접근 시간을 줄이는 방향으로 동작한다.</p>
<hr />
<h3>Total Disk Access Time(전체 디스크 접근 시간)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/ff648c3b-3915-4887-bfe7-710cafd6fab9.png" alt="" style="display:block;margin:0 auto" />

<p>정리하면 <code>디스크 접근 시간(Disk Access Time)</code>은 <code>탐색 시간(Seek Time), 회전 지연 시간(Rotational Latency), 전송 시간(Transfer Time)</code>이 합쳐져서 결정된다. 일반적으로는 이 중 탐색 시간이 전체 성능에 가장 큰 영향을 주는 경우가 많다.</p>
<p>요청을 어떤 순서로 처리하느냐에 따라 <code>디스크 헤드(Disk Head)</code>의 이동 거리가 달라지고, 그에 따라 전체 접근 시간도 달라진다. 따라서 디스크 성능(Disk Performance)을 높이기 위해서는 요청 처리 순서(Request Processing Order)를 효율적으로 관리하는 것이 중요하다.</p>
<p>탐색 시간이 실제 성능에 얼마나 큰 영향을 주는지 수치로 계산해보자. 주어진 조건은 다음과 같다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/5143bdde-77df-4b6f-908d-ea4d0df85c4c.png" alt="" style="display:block;margin:0 auto" />

<p>탐색 시간(Seek Time)은 <code>50ms</code>, 회전 지연 시간(Rotational Latency)은 <code>16.8ms</code>이다. 전송 시간(Transfer Time)은 바이트(Byte)당 <code>0.00094ms</code>이다.</p>
<p>예제에서는 1KB를 1024바이트(Byte)로 계산한다. 따라서 1KB 데이터를 전송하는 시간은 다음과 같다.</p>
<p><code>0.00094ms × 1024 = 0.96256ms</code></p>
<p>이 값을 이용해 디스크 종류에 따른 전체 접근 시간을 비교할 수 있다.</p>
<p>먼저 이동 헤드 디스크(Movable-Head Disk)는 헤드가 원하는 트랙으로 이동해야 하므로 탐색 시간, 회전 지연 시간, 전송 시간을 모두 더해야 한다.</p>
<p><code>50 + 16.8 + 0.96256 = 67.76256ms</code></p>
<p>따라서 이동 헤드 디스크의 전체 접근 시간은 약 67.76ms이다.</p>
<p>반면 고정 헤드 디스크(Fixed-Head Disk)는 각 트랙마다 헤드가 존재하므로 헤드 이동 과정이 필요하지 않다. 따라서 탐색 시간을 제외하고 회전 지연 시간과 전송 시간만 더하면 된다.</p>
<p><code>16.8 + 0.96256 = 17.76256ms</code></p>
<p>따라서 고정 헤드 디스크의 전체 접근 시간은 약 17.76ms이다.</p>
<p>결국 같은 조건이라도 디스크 구조(Disk Structure)에 따라 전체 접근 시간은 크게 달라질 수 있다. 특히 탐색 시간(Seek Time)이 전체 디스크 성능에 큰 영향을 준다는 점을 확인할 수 있다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>또한 고정 헤드 디스크(Fixed-Head Disk)는 탐색 시간이 거의 없다는 장점이 있지만, 각 트랙마다 헤드가 필요하므로 구조가 복잡하고 비용이 높다. 그래서 실제 현대 HDD(Hard Disk Drive)에서는 대부분 이동 헤드 디스크(Movable-Head Disk) 구조가 사용된다.</p>
<hr />
<h3>Factors Affecting Disk Performance (디스크 성능 결정 요소)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a18ebdc2-aeec-4924-97fa-9f8e9e4e96c3.png" alt="" style="display:block;margin:0 auto" />

<p>디스크 성능(Disk Performance)은 단순히 데이터 크기(Data Size)만으로 결정되지 않는다. 오히려 데이터를 처리하기 위해 디스크 헤드(Disk Head)가 얼마나 이동해야 하는지가 더 큰 영향을 주는 경우가 많다.</p>
<p>헤드의 이동 거리가 길어질수록 탐색 시간(Seek Time)이 증가하고, 그만큼 전체 디스크 접근 시간(Disk Access Time)도 함께 늘어난다.</p>
<p>또한 데이터가 디스크의 여러 위치에 분산되어 저장되어 있다면, 헤드는 여러 위치로 이동해야 한다. 이 경우 디스크 접근이 비효율적으로 이루어져 성능이 낮아질 수 있다.</p>
<p>요청이 처리되는 순서(Request Processing Order)도 중요하다. 같은 요청들이 들어오더라도 어떤 순서로 처리하느냐에 따라 헤드의 전체 이동 경로가 크게 달라질 수 있다.</p>
<p>즉, 디스크 성능은 데이터의 크기보다 헤드 이동 거리(Head Movement Distance), 데이터 배치(Data Placement), 요청 처리 순서(Request Processing Order)에 더 큰 영향을 받을 수 있다고 이해할 수 있다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>이 내용은 이후에 살펴볼 디스크 스케줄링(Disk Scheduling)과 연결된다. 디스크 스케줄링은 여러 디스크 요청의 처리 순서를 조정하여 헤드 이동 거리를 줄이고, 전체 접근 시간을 줄이기 위한 기법이다.</p>
<hr />
<h3>Disk Access Order and Performance(디스크 접근 순서와 성능)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/ec57f14b-df80-438b-8da5-973228a930b2.png" alt="" style="display:block;margin:0 auto" />

<p>디스크 접근 순서(Disk Access Order)에 따라 성능이 어떻게 달라지는지 살펴보자.</p>
<p>디스크에 들어오는 요청 집합(Request Set)이 같더라도, 어떤 순서로 처리하느냐에 따라 전체 성능은 달라질 수 있다. 그 이유는 요청 처리 순서(Request Processing Order)에 따라 디스크 헤드(Disk Head)가 이동하는 경로와 총 이동 거리가 달라지기 때문이다.</p>
<p>만약 요청을 비효율적인 순서로 처리하면 헤드의 이동 거리가 길어지고, 그만큼 탐색 시간(Seek Time)이 증가한다. 탐색 시간이 증가하면 전체 디스크 접근 시간(Disk Access Time)도 함께 늘어나게 된다.</p>
<p>반대로 불필요한 헤드 이동을 줄이는 순서로 요청을 처리하면 더 짧은 시간 안에 더 많은 요청을 처리할 수 있다. 즉, 효율적인 요청 처리 순서를 결정하는 것이 디스크 성능(Disk Performance)을 높이는 데 중요하다.</p>
<p>정리하면, 디스크 성능은 요청의 개수만으로 결정되는 것이 아니라 요청을 어떤 순서로 처리하는지에 따라 크게 달라질 수 있다. 비효율적인 처리 순서는 성능 저하로 이어지고, 효율적인 처리 순서를 결정하는 방식이 바로 디스크 스케줄링(Disk Scheduling)의 핵심이다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>디스크 스케줄링(Disk Scheduling)은 여러 디스크 입출력 요청(I/O Request)의 처리 순서를 조정하여 헤드 이동 거리(Head Movement Distance)를 줄이는 기법이다.</p>
<p>대표적인 방식으로는 <code>FCFS(First-Come, First-Served), SSTF(Shortest Seek Time First), SCAN, C-SCAN</code> 등이 있다. 이 방식들은 이후에 디스크 요청을 어떤 기준으로 처리할지 비교할 때 중요하게 다루어진다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/01cb232a-77d6-4556-b804-0c96b702b4c1.png" alt="" style="display:block;margin:0 auto" />

<h3>SSD and HDD Comparison (SSD와 HDD 비교)</h3>
<p>이러한 접근 특성(Access Characteristics)은 모든 저장장치(Storage Device)에서 동일하게 나타나는 것은 아니다. 저장장치의 종류가 달라지면 데이터에 접근하는 방식과 성능 특성(Performance Characteristics)도 달라진다.</p>
<p>대표적으로 <code>SSD(Solid State Drive)와 HDD(Hard Disk Drive)</code>를 비교하면 그 차이를 쉽게 확인할 수 있다.</p>
<p>먼저 SSD는 기계적으로 움직이는 헤드(Head)나 플래터(Platter)가 없다. 따라서 탐색 시간(Seek Time)과 회전 지연 시간(Rotational Latency)이 거의 발생하지 않으며, 빠른 접근 속도(Access Speed)를 제공한다.</p>
<p>또한 SSD는 데이터의 위치에 따른 성능 차이가 HDD보다 작다. 하지만 내부 병렬 구조(Internal Parallelism), 컨트롤러(Controller), 플래시 메모리(Flash Memory) 관리 방식에 따라 성능 차이가 발생할 수 있다.</p>
<p>반면 HDD는 헤드 이동(Head Movement)과 디스크 회전(Disk Rotation)이 필요한 구조이다. 따라서 데이터가 어디에 저장되어 있는지에 따라 접근 시간이 크게 달라질 수 있다. 가까운 위치의 데이터는 빠르게 접근할 수 있지만, 멀리 떨어진 데이터에 접근하려면 더 많은 시간이 필요하다.</p>
<p>다만 SSD도 모든 접근에서 성능 차이가 완전히 사라지는 것은 아니다. SSD는 헤드 이동은 없지만, 연속된 데이터를 처리하는 순차 접근(Sequential Access)과 여러 위치에 분산된 데이터를 처리하는 랜덤 접근(Random Access) 사이에서 성능 차이가 발생할 수 있다. 이는 SSD 내부 컨트롤러의 동작 방식과 데이터 관리 방식의 영향을 받기 때문이다.</p>
<p>즉, 저장장치의 종류에 따라 데이터 접근 방식과 성능 구조가 다르다. 지금까지는 저장장치의 구조, 데이터 접근 방식, 그리고 디스크 성능에 영향을 주는 요소들을 살펴보았다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>HDD에서는 탐색 시간(Seek Time)과 회전 지연 시간(Rotational Latency)이 성능에 큰 영향을 준다. 반면 SSD는 기계적인 이동이 없기 때문에 이러한 요소의 영향은 거의 없고, 대신 컨트롤러(Controller), 캐시(Cache), 플래시 메모리 구조(Flash Memory Structure), 쓰기 방식(Write Operation) 등이 성능에 영향을 줄 수 있다.</p>
<p>따라서 디스크 스케줄링(Disk Scheduling)은 HDD에서 특히 중요하며, SSD에서는 HDD와 같은 헤드 이동 최적화보다 내부 병렬 처리, 쓰기 최적화, 수명 관리 같은 요소가 더 중요해진다.</p>
<hr />
<h3>Disk Scheduling and RAID (디스크 스케줄링과 RAID)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/948b62ba-c5e6-4077-828a-c22ab1724db5.png" alt="" style="display:block;margin:0 auto" />

<p>이번에는 디스크 요청 처리 순서를 결정하는 디스크 스케줄링(Disk Scheduling)과 여러 디스크를 활용해 성능과 안정성을 높이는 <code>RAID(Redundant Array of Independent Disks)</code>에 대해 살펴본다.</p>
<p>앞 차시에서 살펴본 것처럼, 헤드 이동(Head Movement)과 회전 동작(Rotation)이 필요한 디스크에서는 요청 처리 방식에 따라 성능 차이가 크게 나타날 수 있다. 디스크 성능(Disk Performance)을 높이기 위해서는 전체 접근 시간(Disk Access Time)을 줄이는 것이 중요하다.</p>
<p>이때 중요한 기준이 되는 것이 요청을 어떤 순서로 처리할 것인가이다. 요청 순서를 효율적으로 조정하면 디스크 헤드(Disk Head)의 이동 거리를 줄일 수 있고, 그 결과 전체 접근 시간도 감소시킬 수 있다.</p>
<p>운영체제(Operating System)는 이러한 디스크 요청(Disk Request)을 효율적으로 관리하기 위해 여러 가지 디스크 스케줄링 기법(Disk Scheduling Algorithm)을 사용한다.</p>
<p>즉, 디스크 접근 순서(Disk Access Order)를 최적화하는 것은 저장장치 성능뿐만 아니라 전체 시스템 성능(System Performance)을 향상시키는 중요한 요소라고 할 수 있다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>디스크 스케줄링(Disk Scheduling)은 특히 HDD(Hard Disk Drive)에서 중요하다. HDD는 물리적으로 헤드가 이동하기 때문에 요청 순서에 따라 탐색 시간(Seek Time)이 크게 달라질 수 있다.</p>
<p>RAID(Redundant Array of Independent Disks)는 여러 개의 디스크를 하나처럼 묶어 사용하는 방식이다. 목적에 따라 데이터 접근 속도를 높이거나, 일부 디스크가 고장 나도 데이터를 복구할 수 있도록 안정성(Reliability)을 높이는 데 사용된다.</p>
<hr />
<h3>Overview of Disk Scheduling(디스크 스케줄링 개요)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/79a75d0b-7802-400b-a088-ee47c7f36dfb.png" alt="" style="display:block;margin:0 auto" />

<p><code>디스크 스케줄링(Disk Scheduling)</code>은 디스크 요청(Disk Request)의 처리 순서를 결정하는 기법이다.</p>
<p>디스크 요청은 여러 프로그램(Program)이나 프로세스(Process)로부터 발생하며, 일반적으로 <code>큐(Queue)</code> 형태로 순차적으로 도착한다. 하지만 요청이 들어온 순서대로 처리하는 것이 항상 가장 효율적인 것은 아니다.</p>
<p>같은 요청 집합(Request Set)이라도 어떤 순서로 처리하느냐에 따라 디스크 성능(Disk Performance)이 달라질 수 있다. 그 이유는 처리 순서에 따라 디스크 헤드(Disk Head)의 이동 경로와 총 이동 거리(Total Head Movement)가 달라지기 때문이다.</p>
<p>요청 처리 순서(Request Processing Order)를 적절히 조정하면 불필요한 헤드 이동을 줄일 수 있고, 그 결과 디스크 접근 시간(Disk Access Time)을 감소시킬 수 있다.</p>
<p>이처럼 디스크 요청의 처리 순서를 결정하는 기법을 디스크 스케줄링(Disk Scheduling)이라고 한다. 이를 위해 여러 가지 스케줄링 알고리즘(Scheduling Algorithm)이 사용되며, 각 알고리즘은 서로 다른 성능 특성(Performance Characteristics)을 가진다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>디스크 스케줄링(Disk Scheduling)의 핵심 목표는 디스크 헤드의 이동 거리를 줄여 탐색 시간(Seek Time)을 줄이는 것이다.</p>
<p>대표적인 디스크 스케줄링 알고리즘에는 FCFS(First-Come, First-Served), SSTF(Shortest Seek Time First), SCAN, C-SCAN 등이 있다.</p>
<hr />
<h3>Disk Scheduling Algorithms (디스크 스케줄링 알고리즘)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/2ef5c1f2-02d8-4faa-a7ee-f53fc95704d3.png" alt="" style="display:block;margin:0 auto" />

<p>디스크 스케줄링 알고리즘(Disk Scheduling Algorithm)은 디스크 요청(Disk Request)을 어떤 순서로 처리할 것인지 결정하는 방식이다. 요청 처리 순서에 따라 디스크 헤드(Disk Head)의 이동 거리와 접근 시간(Access Time)이 달라지기 때문에, 알고리즘에 따라 성능 차이가 발생한다.</p>
<p>대표적인 방식으로는 먼저 들어온 요청을 먼저 처리하는 <code>FCFS(First-Come, First-Served)</code>가 있다. 이 방식은 구조가 단순하지만, 요청 순서에 따라 헤드 이동 거리가 길어질 수 있다.</p>
<p><code>SSTF(Shortest Seek Time First)</code>는 현재 헤드 위치에서 가장 가까운 요청을 우선 처리하는 방식이다. 헤드 이동 거리를 줄일 수 있지만, 멀리 있는 요청이 오래 기다릴 수 있다는 특징이 있다.</p>
<p><code>SCAN</code>은 헤드가 한 방향으로 이동하면서 지나가는 요청을 처리하고, 끝까지 이동한 뒤 반대 방향으로 다시 요청을 처리하는 방식이다. 엘리베이터가 한 방향으로 이동하면서 층을 처리하는 방식과 비슷하게 이해할 수 있다.</p>
<p><code>C-SCAN(Circular SCAN)</code>은 헤드가 한 방향으로만 요청을 처리하고, 끝까지 이동한 뒤 다시 처음 방향으로 돌아와 같은 방식으로 처리하는 알고리즘이다. 요청 처리의 균형을 맞추는 데 유리하다.</p>
<p><code>LOOK</code>과 <code>C-LOOK은</code> SCAN, C-SCAN과 비슷하지만 디스크의 끝까지 무조건 이동하지 않고, 실제 요청이 존재하는 구간까지만 이동한다. 따라서 불필요한 헤드 이동을 줄일 수 있다.</p>
<p>정리하면, 각 디스크 스케줄링 알고리즘은 헤드 이동 방식이 서로 다르며, 그에 따라 접근 시간과 성능 특성도 달라진다. 이후에는 각 알고리즘의 동작 방식과 장단점을 하나씩 살펴본다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>FCFS는 가장 단순하지만 성능 최적화가 부족할 수 있고, SSTF는 가까운 요청을 우선 처리해 평균 탐색 시간을 줄일 수 있지만 기아 현상(Starvation)이 발생할 수 있다.</p>
<p>SCAN, C-SCAN, LOOK, C-LOOK은 헤드 이동 방향을 기준으로 요청을 처리하여 전체적인 헤드 이동을 줄이기 위한 방식이다. 특히 LOOK과 C-LOOK은 실제 요청이 있는 범위까지만 이동하므로 SCAN 계열보다 불필요한 이동을 줄일 수 있다.</p>
<hr />
<h3>Common Track Request Sequence(공통 트랙 요청 순서)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/40242417-814d-4523-b907-888d5dc2b857.png" alt="" style="display:block;margin:0 auto" />

<p>스케줄링 알고리즘(Scheduling Algorithm)을 살펴보기 전에, 같은 조건에서 비교하기 위한 예제를 먼저 확인한다.</p>
<p>표에 제시된 값들은 디스크(Disk)에 순서대로 들어온 요청들의 트랙 번호(Track Number)를 의미한다. 즉 15번 트랙 요청이 먼저 들어오고, 그다음 <code>8, 17, 11, 3, 23, 19, 14, 20</code> 순서로 요청이 도착한 상황이라고 가정한다.</p>
<p>이후에는 이 동일한 요청 집합(Request Set)을 가지고 각 디스크 스케줄링 알고리즘(Disk Scheduling Algorithm)이 어떤 순서로 요청을 처리하는지 비교한다.</p>
<p>중요한 점은 요청 데이터 자체는 같지만, 처리 순서(Request Processing Order)가 달라지면 디스크 헤드(Disk Head)의 이동 거리와 전체 접근 시간(Disk Access Time)이 달라질 수 있다는 것이다.</p>
<p>따라서 이 표는 각 알고리즘의 성능 차이(Performance Difference)를 비교하기 위한 기준 자료라고 할 수 있다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>디스크 스케줄링 알고리즘을 비교할 때는 보통 초기 헤드 위치(Initial Head Position)와 요청 트랙 순서(Request Track Sequence)가 함께 필요하다.</p>
<p>요청 순서가 같더라도 초기 헤드 위치가 달라지면 총 헤드 이동 거리(Total Head Movement)가 달라질 수 있기 때문이다.</p>
<hr />
<h3>FCFS Disk Scheduling(FCFS 디스크 스케줄링)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/499c381b-ba9a-45e5-8121-840fbbc4f568.png" alt="" style="display:block;margin:0 auto" />

<p><code>FCFS(First-Come, First-Served)</code> 스케줄링은 먼저 도착한 디스크 요청(Disk Request)을 먼저 처리하는 가장 단순한 방식이다. <code>큐(Queue)</code>에 들어온 순서를 그대로 유지하기 때문에 구현이 쉽고, 요청이 들어온 순서대로 처리된다는 점에서 공정성(Fairness)이 높다는 장점이 있다.</p>
<p>하지만 FCFS는 요청이 위치한 트랙(Track)의 위치를 고려하지 않는다. 따라서 처리 순서에 따라 디스크 헤드(Disk Head)의 이동 경로가 비효율적으로 길어질 수 있다. 특히 멀리 떨어진 요청이 앞에 있으면, 뒤에 있는 요청들이 오래 기다리는 문제가 발생할 수 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/115c73da-1ff4-4c05-ad26-91d855fb09e1.png" alt="" style="display:block;margin:0 auto" />

<p>예를 들어 요청 순서가 <code>15 → 8 → 17 → 11 → 3 → 23 → 19 → 14 → 20</code>이라고 하자. FCFS는 도착한 순서를 그대로 처리하므로 헤드는 이 순서대로 이동한다.</p>
<p>15에서 8로 이동하면 7만큼 이동하고, 8에서 17로 이동하면 9만큼 이동한다. 이후 17에서 11은 6, 11에서 3은 8, 3에서 23은 20, 23에서 19는 4, 19에서 14는 5, 14에서 20은 6만큼 이동한다.</p>
<p>이를 모두 더하면 총 헤드 이동 거리(Total Head Movement)는 다음과 같다.</p>
<p><code>7 + 9 + 6 + 8 + 20 + 4 + 5 + 6 = 65</code></p>
<p>즉, FCFS는 요청 순서를 그대로 따르기 때문에 헤드가 좌우로 여러 번 이동할 수 있다. 이로 인해 불필요한 이동 거리가 증가하고, 전체 접근 시간(Disk Access Time)이 길어질 수 있다.</p>
<p>정리하면 FCFS는 구조가 단순하고 공정하지만, 디스크 헤드의 이동 거리를 줄이는 데에는 효율적이지 않을 수 있는 방식이다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>이 계산에서는 주어진 요청들 사이의 이동 거리만 더한 것이다. 만약 초기 헤드 위치(Initial Head Position)가 따로 주어진다면, 초기 위치에서 첫 번째 요청인 15번 트랙까지 이동하는 거리도 함께 계산해야 한다.</p>
<hr />
<h3>SSTF Disk Scheduling(SSTF 디스크 스케줄링)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/91602dc1-9117-4732-810b-1556609fca3d.png" alt="" style="display:block;margin:0 auto" />

<p><code>SSTF(Shortest Seek Time First)</code> 스케줄링은 현재 디스크 헤드(Disk Head) 위치를 기준으로 가장 가까운 요청을 먼저 처리하는 방식이다. 즉, 매 단계마다 이동 거리가 가장 짧은 요청을 선택하여 탐색 시간(Seek Time)을 줄이도록 동작한다.</p>
<p>이 방식은 FCFS(First-Come, First-Served)보다 평균 탐색 시간(Average Seek Time)을 줄일 수 있어 처리 효율이 향상된다는 장점이 있다. 하지만 가까운 요청만 계속 선택하게 되면, 멀리 있는 요청은 오랫동안 처리되지 못할 수 있다. 이처럼 특정 요청이 계속 지연되는 문제를 <code>기아 현상(Starvation)</code>이라고 한다.</p>
<p>그림은 SSTF 방식으로 요청을 처리할 때 헤드가 실제로 이동하는 경로를 보여준다. 현재 헤드 위치가 15라고 하면, 가장 가까운 요청을 우선 선택한다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/cc1b98a7-990f-49f8-ad16-42e2eb64829e.png" alt="" style="display:block;margin:0 auto" />

<p><code>15 → 8 → 17 → 11 → 3 → 23 → 19 → 14 → 20</code> 트랙번호이지만 가까운 위치를 계속 선택하기 때문에</p>
<p><code>15 → 14 → 17 → 19 → 20 → 23 → 11 → 8 → 3</code> 로 이동 된 것이다.</p>
<p>먼저 15에서 14로 이동하면 1만큼 이동한다. 그다음 14에서 17로 3만큼, 17에서 19로 2만큼, 19에서 20으로 1만큼 이동한다. 이후 20에서 23으로 3만큼 이동한다.</p>
<p>그다음 남아 있는 요청 중 가장 가까운 위치를 계속 선택하여 11, 8, 3 순서로 처리한다. 이때 23에서 11은 12, 11에서 8은 3, 8에서 3은 5만큼 이동한다.</p>
<p>따라서 총 헤드 이동 거리(Total Head Movement)는 다음과 같다.</p>
<p><code>1 + 3 + 2 + 1 + 3 + 12 + 3 + 5 = 30</code></p>
<p>즉, SSTF는 가까운 요청을 우선 처리하기 때문에 헤드가 좌우로 크게 왕복하는 이동을 줄일 수 있다. 그 결과 총 이동 거리가 30으로 계산되며, 앞서 살펴본 FCFS의 65보다 훨씬 효율적인 결과를 보인다.</p>
<p>정리하면 SSTF는 평균 탐색 시간을 줄이는 데 효과적이지만, 멀리 떨어진 요청이 계속 밀릴 수 있다는 점을 함께 고려해야 하는 방식이다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>SSTF(Shortest Seek Time First)는 현재 위치에서 가장 가까운 요청을 선택하기 때문에 평균 성능은 좋아질 수 있다. 하지만 항상 전체 최적의 이동 경로를 보장하는 것은 아니다.</p>
<p>또한 가까운 요청이 계속 새로 들어오면 먼 위치의 요청은 계속 뒤로 밀릴 수 있으므로, 기아 현상(Starvation)이 발생할 가능성이 있다는 점이 중요하다.</p>
<hr />
<h3>SCAN Disk Scheduling(SCAN 디스크 스케줄링)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a96fe7c6-f81f-4602-979f-6f953b8bbe00.png" alt="" style="display:block;margin:0 auto" />

<p><code>SCAN 스케줄링(SCAN Scheduling)</code>은 디스크 헤드(Disk Head)가 한 방향으로 이동하면서 이동 경로에 있는 요청들을 순차적으로 처리하는 방식이다. 한쪽 끝까지 이동한 뒤에는 방향을 반대로 바꾸어 다시 요청을 처리한다.</p>
<p>이 방식은 엘리베이터가 위아래로 이동하면서 지나가는 층의 요청을 처리하는 방식과 비슷하기 때문에 엘리베이터 알고리즘(Elevator Algorithm)이라고도 불린다.</p>
<p>SCAN은 헤드 이동 경로가 비교적 일정하기 때문에 예측 가능성(Predictability)이 높고, 안정적인 처리가 가능하다. 또한 SSTF(Shortest Seek Time First)처럼 가까운 요청만 계속 선택하는 방식이 아니기 때문에 특정 요청이 계속 밀리는 기아 현상(Starvation)이 발생할 가능성이 상대적으로 낮다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/ccdcedf1-04e5-49c7-9472-cd89c5c54488.png" alt="" style="display:block;margin:0 auto" />

<p><code>15 → 8 → 17 → 11 → 3 → 23 → 19 → 14 → 20</code> 트랙 번호</p>
<p><code>15 → 14 → 11 → 8 → 3 → 0 → 17 → 19 → 20 → 23</code> SCAN 스케줄링의 이동순서</p>
<p>그림에서는 현재 헤드가 15번 트랙에서 시작해 한쪽 방향으로 이동한다. 먼저 14번 트랙을 처리하고, 이어서 11, 8, 3 순서로 요청을 처리한다. 이후 디스크의 끝 방향인 0번 위치까지 이동한 뒤 방향을 바꾸어 반대쪽으로 이동한다.</p>
<p>반대 방향으로 이동하면서 17, 19, 20, 23 순서로 요청을 처리한다. 즉, 중간에 방향을 바꾸지 않고 한 방향으로 끝까지 이동한 뒤 다시 돌아오면서 요청을 처리하는 방식이다.</p>
<p>이 예제에서 총 헤드 이동 거리(Total Head Movement)는 약 38로 계산된다. 따라서 SCAN은 FCFS(First-Come, First-Served)보다 더 효율적이며, 동시에 공정성(Fairness)도 함께 고려한 방식이라고 할 수 있다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>SCAN은 한 방향으로 이동하면서 요청을 처리하기 때문에 헤드 이동이 비교적 규칙적이다. 그래서 FCFS보다 불필요한 왕복 이동을 줄일 수 있고, SSTF보다 특정 요청이 오래 밀릴 가능성도 낮다.</p>
<p>다만 계산에서 디스크의 끝을 어디까지 포함하느냐에 따라 총 이동 거리가 달라질 수 있다. 요청이 있는 마지막 트랙인 23까지만 계산하면 <code>15 → 0 → 23</code>이므로 총 이동 거리는 38이다. 만약 실제 디스크 끝인 24까지 이동한다고 계산하면 총 이동 거리는 39가 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/0491ac3a-21f0-4ba7-89e7-006c71a8517a.png" alt="" style="display:block;margin:0 auto" />

<h3>C-SCAN Disk Scheduling(C-SCAN 디스크 스케줄링)</h3>
<p><code>C-SCAN(Circular SCAN)</code> 스케줄링은 디스크 헤드(Disk Head)가 한 방향으로만 이동하면서 경로상에 있는 요청들을 순차적으로 처리하는 방식이다.</p>
<p>SCAN과 마찬가지로 이동 중에 만나는 요청을 처리하지만, 끝 지점에 도달했을 때 반대 방향으로 요청을 처리하지 않는다. 대신 처음 위치로 다시 이동한 뒤, 다시 같은 방향으로 요청을 처리한다.</p>
<p>즉, C-SCAN은 디스크를 원형 구조처럼 보고 한 방향으로 순환하면서 요청을 처리하는 방식이라고 할 수 있다.</p>
<p>이 방식은 모든 요청이 비슷한 방식으로 서비스를 받기 때문에 대기 시간(Waiting Time)의 편차가 비교적 작다. 따라서 요청을 균등하게 처리할 수 있고, 특정 구간의 요청만 반복적으로 유리해지는 현상을 줄일 수 있다. 이런 점에서 공정성(Fairness)이 높은 스케줄링 방식이라고 볼 수 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/5ffbf956-f726-461e-89ec-6d7f4c529ad7.png" alt="" style="display:block;margin:0 auto" />

<p>예제에서 현재 헤드가 15번 트랙에서 시작한다고 하자. C-SCAN은 한쪽 방향으로 먼저 이동하면서 14, 11, 8, 3 순서로 요청을 처리한다. 이후 디스크의 끝 방향인 0번 위치까지 이동한다.</p>
<p>그다음에는 반대 방향으로 요청을 처리하지 않고, 바로 반대쪽 끝인 24번 위치로 이동한다. 이후 다시 같은 방향으로 이동하면서 23, 20, 19, 17 순서로 요청을 처리한다.</p>
<p>즉, C-SCAN은 한 방향의 처리 흐름만 유지하고, 끝에 도달하면 시작 지점으로 순환해서 다시 같은 방향으로 처리하는 방식이다.</p>
<p>이 예제에서 총 헤드 이동 거리(Total Head Movement)는 46으로 계산된다. SCAN보다 이동 거리는 늘어날 수 있지만, 대기 시간의 편차를 줄이고 요청을 더 균등하게 처리할 수 있는 방식이라고 이해할 수 있다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>C-SCAN은 SCAN보다 공정한 대기 시간 분포를 제공하는 것이 목적이다. SCAN은 방향을 바꾼 뒤 바로 근처 요청을 다시 처리할 수 있기 때문에 특정 위치의 요청이 상대적으로 유리할 수 있다.</p>
<p>반면 C-SCAN은 한 방향으로만 서비스를 제공하므로, 요청들이 좀 더 일정한 주기로 처리된다. 다만 끝에서 반대쪽 끝으로 돌아가는 이동은 실제 요청을 처리하지 않는 이동이므로 총 헤드 이동 거리는 증가할 수 있다.</p>
<hr />
<h3>LOOK Disk Scheduling(LOOK 디스크 스케줄링)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/fe4b6ea9-b0be-46da-b925-6a39bf878736.png" alt="" style="display:block;margin:0 auto" />

<p><code>LOOK 스케줄링(LOOK Scheduling)</code>은 기본 동작 방식이 SCAN스케줄링(SCAN Scheduling)과 유사하지만, 디스크의 끝까지 이동하지 않는 방식이다.</p>
<p>SCAN은 한 방향으로 이동하면서 요청을 처리한 뒤 디스크의 끝까지 이동했다. LOOK은 현재 이동 방향에 요청이 존재하는 마지막 위치까지만 이동한다. 그 지점에서 방향을 바꾸어 다시 반대 방향의 요청을 처리한다.</p>
<p>즉, 요청이 없는 구간까지 불필요하게 이동하지 않기 때문에 디스크 헤드(Disk Head)의 이동 거리를 줄일 수 있다. 그 결과 접근 시간(Access Time)이 개선되고, 전체 처리 효율도 높아질 수 있다.</p>
<p>LOOK 방식은 SCAN의 장점을 유지하면서 불필요한 이동을 줄인 개선형 방식이라고 할 수 있다. 이러한 이유로 실제 환경에서는 SCAN보다 더 효율적으로 사용될 수 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/9cdadcbf-30a9-4cdd-9b67-0468df07d226.png" alt="" style="display:block;margin:0 auto" />

<p><code>15 → 8 → 17 → 11 → 3 → 23 → 19 → 14 → 20</code> 트랙번호</p>
<p><code>15 → 14 → 11 → 8 → 3 → 17 → 19 → 20 → 23</code> LOOK스케줄링 이동순서</p>
<p>그림을 기준으로 보면, 헤드는 15번 트랙에서 시작해 한쪽 방향으로 이동한다. 이동 과정에서 14, 11, 8, 3 순서로 요청을 처리한다. SCAN과 달리 요청이 없는 0번 끝까지 이동하지 않고, 마지막 요청 위치인 3번에서 방향을 바꾼다.</p>
<p>이후 반대 방향으로 이동하면서 17, 19, 20, 23 순서로 요청을 처리한다. 즉, 요청이 존재하는 범위까지만 이동하고, 불필요한 끝 구간 이동은 생략하는 방식이다.</p>
<p>따라서 LOOK 알고리즘은 SCAN보다 헤드 이동 거리를 줄일 수 있으며, 요청 처리의 효율성을 높일 수 있는 방식이다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>또한 주어진 요청 집합을 기준으로 계산하면, LOOK의 이동 경로는 <code>15 → 14 → 11 → 8 → 3 → 17 → 19 → 20 → 23</code>이다. 이 경우 총 이동 거리는 다음과 같다.</p>
<p><code>1 + 3 + 3 + 5 + 14 + 2 + 1 + 3 = 32</code></p>
<p>따라서 이 요청 기준에서는 LOOK의 총 이동 거리는 32로 계산하는 것이 더 정확하다. 다만 강의 자료에서 시작점이나 끝점 포함 방식이 다르게 설정되어 있다면 계산값이 달라질 수 있다.</p>
<hr />
<h3>C-LOOK Disk Scheduling(C-LOOK 디스크 스케줄링)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/6d535e30-f5d3-487c-b77f-278b0c844556.png" alt="" style="display:block;margin:0 auto" />

<p><code>C-LOOK 스케줄링(C-LOOK Scheduling)</code>은 기본적으로 LOOK 스케줄링(LOOK Scheduling)과 유사하지만, 한 방향으로만 요청을 처리하는 방식이다.</p>
<p>현재 방향으로 이동하면서 요청을 처리하다가 마지막 요청 위치에 도달하면 방향을 바꾸지 않는다. 대신 반대편에 있는 첫 번째 요청 위치로 바로 이동한 뒤, 다시 같은 방향으로 요청을 처리한다.</p>
<p>즉, C-LOOK은 LOOK의 장점처럼 요청이 없는 디스크 끝 지점까지 불필요하게 이동하지 않는다. 동시에 C-SCAN(Circular SCAN)처럼 한 방향으로 순환 처리하기 때문에 비교적 균등한 요청 처리가 가능하다.</p>
<p>따라서 C-LOOK은 헤드 이동 거리(Head Movement Distance)를 줄이면서도 공정성(Fairness)을 함께 고려한 효율적인 디스크 스케줄링 방식이라고 할 수 있다. C-SCAN과 비교하면 실제 요청이 없는 끝 지점까지 이동하지 않기 때문에 더 효율적인 순환 처리가 가능하다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/5fc9acb4-ca1c-4921-8e21-0015d624909e.png" alt="" style="display:block;margin:0 auto" />

<p><code>15 → 8 → 17 → 11 → 3 → 23 → 19 → 14 → 20</code> 트랙번호</p>
<p><code>15 → 14 → 11 → 8 → 3 → 23 → 20 → 19 → 17</code> C-LOOK 이동순서</p>
<p>그림을 기준으로 보면, 현재 헤드(Disk Head)는 15번 트랙에서 시작해 한쪽 방향으로 이동한다. 그 과정에서 14, 11, 8, 3 순서로 요청을 처리한다.</p>
<p>이후 LOOK처럼 반대 방향으로 되돌아가며 처리하지 않고, 마지막 요청인 3번에서 반대편의 첫 번째 요청 위치인 23번으로 바로 이동한다. 그다음 같은 방향으로 다시 이동하면서 20, 19, 17 순서로 요청을 처리한다.</p>
<p>즉, C-LOOK은 요청이 존재하는 구간만 순환하면서 처리하고, 요청이 없는 끝 지점 이동은 생략하는 방식이다.</p>
<p>이 예제에서 총 헤드 이동 거리(Total Head Movement)는 38로 계산된다. C-SCAN보다 이동 거리를 줄이면서도 순환 처리의 공정성을 유지할 수 있는 방식이다.</p>
<p>정리하면 C-LOOK은 LOOK과 C-SCAN의 장점을 결합한 개선형 순환 스케줄링 방식이라고 이해할 수 있다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>C-LOOK은 C-SCAN과 달리 디스크의 실제 끝까지 이동하지 않고, 요청이 있는 마지막 위치까지만 이동한다. 그래서 불필요한 헤드 이동을 줄일 수 있다.</p>
<p>이 예제의 이동 경로는 <code>15 → 14 → 11 → 8 → 3 → 23 → 20 → 19 → 17</code>로 볼 수 있다. 총 이동 거리는 다음과 같다.</p>
<p><code>1 + 3 + 3 + 5 + 20 + 3 + 1 + 2 = 38</code></p>
<p>따라서 강의에서 제시한 총 이동 거리 38은 이 경로를 기준으로 하면 맞는 계산이다.</p>
<hr />
<h3>Comparison of Disk Scheduling Algorithms</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/b5fa487f-1d89-4a12-a789-8bc26d7f1655.png" alt="" style="display:block;margin:0 auto" />

<p>지금까지 여러 디스크 스케줄링 알고리즘(Disk Scheduling Algorithm)을 살펴보았다. 각 알고리즘에 따라 디스크 헤드(Disk Head)의 이동 경로와 총 이동 거리(Total Head Movement)가 달라진다는 것을 확인할 수 있었다.</p>
<p>헤드의 이동 거리가 달라지면 탐색 시간(Seek Time)도 달라지고, 그 결과 전체 디스크 접근 시간(Disk Access Time)과 성능(Disk Performance)에도 차이가 발생한다.</p>
<p>일반적으로 헤드의 평균 이동 거리(Average Head Movement)가 짧을수록 탐색 시간이 감소하므로 성능 향상에 유리하다. 예를 들어 SSTF(Shortest Seek Time First)는 현재 위치에서 가장 가까운 요청을 먼저 처리하기 때문에 이동 거리를 줄이는 데 효과적이다.</p>
<p>하지만 SSTF는 가까운 요청만 계속 선택할 경우, 멀리 있는 요청이 오랫동안 처리되지 못하는 기아 현상(Starvation)이 발생할 수 있다.</p>
<p>반면 SCAN 계열 알고리즘(SCAN-Based Algorithm)은 헤드 이동 경로가 비교적 일정하고, 요청을 균형 있게 처리할 수 있다. 따라서 성능(Performance)과 공정성(Fairness) 사이의 균형을 제공하는 방식이라고 할 수 있다.</p>
<p>결국 어떤 디스크 스케줄링 알고리즘이 항상 가장 좋다고 보기는 어렵다. 시스템 환경(System Environment), 요청 패턴(Request Pattern), 공정성 요구(Fairness Requirement), 성능 목표(Performance Goal)에 따라 적절한 방식을 선택하는 것이 중요하다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>디스크 스케줄링(Disk Scheduling)은 주로 HDD(Hard Disk Drive)처럼 헤드 이동이 필요한 저장장치에서 중요한 의미를 가진다. SSD(Solid State Drive)는 물리적인 헤드 이동이 없기 때문에 HDD와 같은 방식의 탐색 시간 최적화 효과는 상대적으로 작다.</p>
<p>정리하면, FCFS는 단순하고 공정하지만 비효율적일 수 있고, SSTF는 이동 거리를 줄이지만 기아 현상이 생길 수 있다. SCAN, C-SCAN, LOOK, C-LOOK은 헤드 이동 방향을 기준으로 요청을 처리하여 성능과 공정성의 균형을 맞추려는 방식이다.</p>
<hr />
<h3>Auxiliary Disk Optimization Methods(디스크 성능 향상을 위한 보조 최적화 방법)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/e3200276-7b97-4674-b7f3-842d7d7a91dc.png" alt="" style="display:block;margin:0 auto" />

<p>지금까지 디스크 스케줄링(Disk Scheduling)을 통해 요청 처리 순서(Request Processing Order)를 조정하는 방법을 살펴보았다. 하지만 디스크 성능 향상(Disk Performance Improvement)은 스케줄링만으로 이루어지는 것은 아니다.</p>
<p>이를 보완하기 위해 다양한 보조 최적화 방법(Auxiliary Optimization Method)이 함께 사용된다.</p>
<p>대표적으로 <code>디스크 요청 재정렬(Request Reordering)</code>을 통해 헤드 이동 거리(Head Movement Distance)를 줄일 수 있다. 요청을 그대로 처리하는 것이 아니라, 더 효율적인 순서로 다시 정렬하면 탐색 시간(Seek Time)을 줄이는 데 도움이 된다.</p>
<p>또한 Read-Ahead 기법은 앞으로 필요할 가능성이 높은 데이터를 미리 읽어두는 방식이다. 특히 연속 접근(Sequential Access)이 많은 경우, 다음에 사용할 데이터를 미리 준비해두면 접근 성능을 높일 수 있다.</p>
<p><code>Write Buffering</code>은 여러 쓰기 요청(Write Request)을 바로 처리하지 않고 버퍼(Buffer)에 모아두었다가 한 번에 처리하는 방식이다. 이를 통해 디스크 접근 횟수(Disk Access Count)를 줄이고, 쓰기 성능을 향상시킬 수 있다.</p>
<p>여기에 운영체제 캐시(OS Cache)와 저장장치 내부 캐시(Device Cache)를 함께 활용하면 전체 접근 속도(Access Speed)를 더 향상시킬 수 있다.</p>
<p>즉, 디스크 성능을 높이기 위해서는 스케줄링뿐만 아니라 요청 재정렬, Read-Ahead, Write Buffering, Cache와 같은 보조 최적화 방법을 함께 사용하는 것이 중요하다. 이후에는 이러한 방법들을 하나씩 살펴본다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>Read-Ahead는 순차적으로 데이터를 읽을 때 효과적이지만, 임의 접근(Random Access)이 많을 경우에는 미리 읽은 데이터가 실제로 사용되지 않아 효율이 떨어질 수 있다.</p>
<p>Write Buffering은 성능을 높이는 데 도움이 되지만, 데이터가 실제 디스크에 기록되기 전에 전원이 꺼지면 데이터 손실 위험이 있을 수 있다. 그래서 운영체제는 동기화(Synchronization)나 플러시(Flush) 같은 방식으로 안정성을 함께 관리한다.</p>
<hr />
<h3>Request Reordering(요청 재정렬)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/89e2165f-4425-47ec-99c3-74ba5efe4eb1.png" alt="" style="display:block;margin:0 auto" />

<p><code>요청 재정렬(Request Reordering)</code>은 디스크 요청(Disk Request)의 처리 순서를 다시 구성하여 더 효율적으로 처리하는 방법이다.</p>
<p>디스크 요청을 도착한 순서 그대로 처리하는 것이 아니라, 서로 가까운 위치에 있는 요청들을 묶어서 처리하면 디스크 헤드(Disk Head)의 이동 거리를 줄일 수 있다.</p>
<p>이렇게 하면 헤드가 불필요하게 멀리 이동했다가 다시 돌아오는 동작을 줄일 수 있고, 그 결과 탐색 시간(Seek Time)과 전체 접근 시간(Disk Access Time)을 감소시키는 효과가 있다.</p>
<p>요청 재정렬은 앞에서 살펴본 디스크 스케줄링 알고리즘(Disk Scheduling Algorithm)과 함께 사용되며, 전체 디스크 처리 효율(Disk Processing Efficiency)을 높이는 데 기여한다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>요청 재정렬은 성능 향상에는 도움이 되지만, 특정 요청이 계속 뒤로 밀리지 않도록 공정성(Fairness)도 함께 고려해야 한다.</p>
<p>즉, 가까운 요청만 계속 우선 처리하면 일부 요청이 오래 기다리는 기아 현상(Starvation)이 발생할 수 있기 때문에, 성능과 공정성 사이의 균형이 중요하다.</p>
<hr />
<h3>Read-Ahead(미리 읽기)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/dcfb652f-1d7c-4195-9e66-c35bd07f77c2.png" alt="" style="display:block;margin:0 auto" />

<p>Read-Ahead는 앞으로 필요할 것으로 예상되는 데이터를 미리 읽어 두는 방식이다. 즉, 아직 요청되지 않은 데이터라도 곧 필요할 가능성이 높다고 판단되면 미리 읽어서 캐시(Cache)에 저장해둔다.</p>
<p>이 방식은 특히 파일을 순차적으로 읽는 순차 접근(Sequential Access) 상황에서 효과적이다. 현재 데이터를 읽고 있다면, 그다음 데이터도 연속해서 필요할 가능성이 높기 때문이다.</p>
<p>미리 읽어둔 데이터가 실제로 요청되면, 다시 디스크(Disk)에 접근하지 않고 캐시에서 바로 전달할 수 있다. 이를 통해 디스크 접근 횟수(Disk Access Count)를 줄이고, 전체 읽기 성능(Read Performance)을 향상시킬 수 있다.</p>
<p>하지만 예측이 빗나가면 문제가 될 수 있다. 미리 읽어둔 데이터가 실제로 사용되지 않는다면 불필요한 입출력(I/O)이 발생하고, 캐시 공간(Cache Space)도 낭비될 수 있다.</p>
<p>따라서 Read-Ahead는 앞으로 사용할 가능성이 높은 데이터를 미리 준비하여 접근 속도를 높이는 최적화 기법이라고 이해할 수 있다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>Read-Ahead는 순차 접근(Sequential Access)에서는 효과적이지만, 랜덤 접근(Random Access)이 많은 경우에는 효율이 떨어질 수 있다.</p>
<p>즉, 다음에 어떤 데이터를 읽을지 예측하기 쉬운 상황에서는 성능 향상에 도움이 되지만, 접근 위치가 계속 바뀌는 경우에는 불필요한 데이터까지 읽게 될 수 있다.</p>
<hr />
<h3>Write Buffering(쓰기 버퍼링)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/c1b25c54-05fc-4977-a151-feb6d6f3aa5a.png" alt="" style="display:block;margin:0 auto" />

<p>Write Buffering은 쓰기 성능(Write Performance)을 높이기 위한 최적화 방법이다. 데이터를 즉시 디스크(Disk)에 기록하지 않고, 먼저 메모리 버퍼(Memory Buffer)에 저장한 뒤 나중에 한꺼번에 처리하는 방식이다.</p>
<p>이 방식은 여러 개의 작은 쓰기 요청(Write Request)을 모아서 처리할 수 있기 때문에 디스크 접근 횟수(Disk Access Count)를 줄이는 데 도움이 된다. 디스크에 매번 접근하는 대신 일정량의 데이터를 모아 한 번에 기록하면 전체 입출력 성능(I/O Performance)을 향상시킬 수 있다.</p>
<p>특히 순차적인 쓰기 작업(Sequential Write)이 많은 환경에서는 Write Buffering이 효율적으로 동작할 수 있다. 작은 쓰기 요청들을 모아 연속적으로 처리하면 디스크 접근이 더 효율적으로 이루어지기 때문이다.</p>
<p>하지만 버퍼에만 저장된 상태에서 시스템 장애(System Failure)나 전원 문제(Power Failure)가 발생하면, 아직 디스크에 기록되지 않은 일부 데이터가 손실될 가능성이 있다.</p>
<p>따라서 Write Buffering은 쓰기 성능을 높이는 데 효과적이지만, 데이터 안정성(Data Reliability)을 함께 고려해야 하는 방식이다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>Write Buffering은 성능 향상에는 도움이 되지만, 데이터가 실제 디스크에 기록되기 전까지는 손실 위험이 있다. 그래서 운영체제(Operating System)는 플러시(Flush)나 동기화(Synchronization)를 통해 버퍼에 있는 데이터를 디스크에 반영한다.</p>
<p>예를 들어 <code>sync</code> 같은 동작은 메모리 버퍼에 있는 변경 내용을 실제 저장장치에 기록하도록 하는 역할을 한다.</p>
<hr />
<h3>Improving Access Speed with Cache</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/d03b6c80-8af5-48ea-bf17-416543194b01.png" alt="" style="display:block;margin:0 auto" />

<p>읽기(Read)와 쓰기(Write) 성능을 높이는 방법 외에도, 자주 사용하는 데이터를 빠르게 제공하기 위해 캐시(Cache) 방식이 함께 사용된다.</p>
<p>캐시는 자주 접근하는 데이터(Frequently Accessed Data)를 메모리(Memory)에 임시로 저장해두는 구조이다. 이후 같은 데이터가 다시 요청되면 디스크(Disk)에 직접 접근하지 않고, 메모리에서 빠르게 제공할 수 있다.</p>
<p>이 방식은 같은 데이터를 반복해서 사용하는 경우에 효과적이다. 디스크 접근 횟수(Disk Access Count)를 줄일 수 있기 때문에 입출력 대기 시간(I/O Waiting Time)을 크게 줄이는 데 도움이 된다.</p>
<p>또한 캐시는 Read-Ahead와 함께 사용될 수 있다. 앞으로 필요할 가능성이 높은 데이터를 미리 읽어 캐시에 저장해두면, 실제 요청이 들어왔을 때 더 빠르게 데이터를 전달할 수 있다.</p>
<p>캐시에는 운영체제(Operating System)가 관리하는 운영체제 캐시(OS Cache)도 있고, 디스크 장치 내부에 포함된 하드웨어 캐시(Hardware Cache)도 있다. 두 캐시가 함께 사용되면 전체 저장장치 접근 속도(Storage Access Speed)를 더 향상시킬 수 있다.</p>
<p>다만 캐시의 효율은 어떤 데이터를 오래 유지하고, 어떤 데이터를 교체할 것인지에 따라 달라진다. 따라서 캐시 교체 정책(Cache Replacement Policy)도 저장장치 성능을 결정하는 중요한 요소라고 할 수 있다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>캐시는 자주 사용되는 데이터를 빠르게 제공할 수 있지만, 캐시 공간(Cache Space)은 제한되어 있다. 그래서 운영체제는 LRU(Least Recently Used)와 같은 캐시 교체 정책(Cache Replacement Policy)을 사용해 어떤 데이터를 유지하고 제거할지 결정한다.</p>
<p>또한 쓰기 캐시(Write Cache)의 경우, 데이터가 실제 디스크에 기록되기 전에 장애가 발생하면 데이터 손실 위험이 있을 수 있으므로 안정성 관리도 함께 필요하다.</p>
<hr />
<h3>RAID Storage Configuration(RAID 저장장치 구성 방식)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a542a620-cc56-4f29-8b45-718ccf01e527.png" alt="" style="display:block;margin:0 auto" />

<p>지금까지는 하나의 디스크(Disk)의 접근 성능(Access Performance)을 높이는 방법을 살펴보았다. 이번에는 여러 개의 디스크를 함께 사용하여 성능과 안정성을 높이는 RAID 구조(RAID Structure)에 대해 알아본다.</p>
<p><code>RAID(Redundant Array of Independent Disks)</code>는 여러 개의 물리적 디스크(Physical Disk)를 묶어서 하나의 논리적인 저장장치(Logical Storage Device)처럼 사용하는 방식이다.</p>
<p>RAID를 사용하면 데이터 입출력(Input/Output)을 여러 디스크에 나누어 처리할 수 있다. 이를 통해 하나의 디스크만 사용할 때보다 성능 향상(Performance Improvement)을 기대할 수 있다.</p>
<p>또한 데이터를 여러 디스크에 분산 저장(Distributed Storage)하거나 중복 저장(Redundant Storage)함으로써, 일부 디스크에 장애(Failure)가 발생하더라도 데이터 안정성(Data Reliability)을 높일 수 있다.</p>
<p>즉, RAID는 저장장치의 성능 향상과 데이터 보호(Data Protection)를 함께 고려한 저장장치 구성 방식이라고 이해할 수 있다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>RAID는 목적에 따라 성능을 높이는 방식과 안정성을 높이는 방식이 다르다. 예를 들어 RAID 0은 데이터를 여러 디스크에 나누어 저장해 성능을 높이지만, 중복 저장이 없어 디스크 하나가 고장 나면 데이터 손실 위험이 크다.</p>
<p>반면 RAID 1은 같은 데이터를 두 개 이상의 디스크에 복제하여 안정성을 높이는 방식이다. 이후 RAID 0, RAID 1, RAID 5처럼 RAID 수준(RAID Level)에 따라 특징이 달라진다고 이해하면 좋다.</p>
<hr />
<h3>RAID Levels(RAID 수준)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/3cc3b6dd-be55-4888-9f53-f1de55d82c20.png" alt="" style="display:block;margin:0 auto" />

<p><code>RAID(Redundant Array of Independent Disks)</code>는 하나의 방식만 존재하는 것이 아니라, 디스크를 구성하는 방법에 따라 여러 수준(Level)으로 나뉜다.</p>
<p>대표적인 RAID 수준에는 RAID 0, RAID 1, RAID 2, RAID 3, RAID 4, RAID 5, RAID 6이 있다. 각 수준마다 데이터 저장 방식(Data Storage Method), 성능 향상 방식(Performance Improvement Method), 장애 대응 방식(Fault Tolerance Method)이 다르게 구성된다.</p>
<p>또한 RAID 0+1, RAID 1+0처럼 두 가지 RAID 방식을 결합한 조합 구조(Nested RAID)도 존재한다. 이러한 방식은 성능과 안정성을 함께 고려하기 위해 사용된다.</p>
<p>다만 이번 내용에서는 모든 RAID 수준을 세부적으로 다루기보다는, RAID의 기본 원리를 이해하는 데 핵심이 되는 RAID 0, RAID 1, RAID 5를 중심으로 살펴본다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>RAID 0은 성능 향상에 초점을 둔 방식이고, RAID 1은 데이터 복제(Mirroring)를 통한 안정성 향상에 초점을 둔 방식이다.</p>
<p>RAID 5는 데이터와 패리티(Parity)를 여러 디스크에 분산 저장하여 성능과 안정성을 함께 고려하는 방식이다. 따라서 RAID의 기본 개념을 이해할 때는 RAID 0, RAID 1, RAID 5를 중심으로 비교하면 좋다.</p>
<hr />
<h3>RAID 0: Striping(RAID 0: 스트라이핑)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/73d46bd8-dfa4-420d-a260-b74027071c21.png" alt="" style="display:block;margin:0 auto" />

<p>RAID 0은 데이터를 여러 디스크(Disk)에 나누어 저장하는 방식으로, <code>분산 저장(Distributed Storage)</code> 또는 <code>스트라이핑(Striping)</code> 방식이라고도 한다.</p>
<p>그림을 보면 데이터 A, B, C, D가 각각 다른 디스크에 나누어 저장되고, 그다음 데이터 E, F, G, H도 각 디스크에 분산되어 기록되는 구조를 확인할 수 있다.</p>
<p>이처럼 데이터를 일정한 단위로 나누어 여러 디스크에 동시에 저장하는 방식을 스트라이핑(Striping)이라고 한다. 여러 디스크가 동시에 읽기(Read)와 쓰기(Write)에 참여할 수 있기 때문에 입출력 성능(I/O Performance)을 높일 수 있다.</p>
<p>또한 모든 디스크의 저장 공간을 데이터 저장에 사용할 수 있으므로 저장 공간 활용(Storage Utilization)도 효율적이다.</p>
<p>하지만 RAID 0은 중복 저장(Redundancy)이나 오류 복구 정보(Recovery Information)를 제공하지 않는다. 따라서 디스크 하나만 고장 나더라도 전체 데이터 복구가 어려워질 수 있다.</p>
<p>즉, RAID 0은 데이터 안정성(Data Reliability)보다는 성능(Performance)을 우선하는 환경에 적합한 방식이라고 이해할 수 있다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>RAID 0은 이름에 RAID가 들어가지만, 실제로는 중복성(Redundancy)이 없다. 따라서 중요한 데이터를 안전하게 보관하기 위한 방식이라기보다는, 빠른 읽기와 쓰기 성능이 필요한 경우에 사용하는 방식으로 이해하는 것이 좋다.</p>
<hr />
<h3>RAID 1: Mirroring (RAID 1: 미러링)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/1b5db56b-caf4-49b4-ab2e-b4370cb8dcf5.png" alt="" style="display:block;margin:0 auto" />

<p>RAID 1은 동일한 데이터를 두 개 이상의 디스크(Disk)에 동시에 복사해서 저장하는 방식이다. 이 방식을 미러링(Mirroring)이라고도 한다.</p>
<p>하나의 디스크에 기록된 내용이 다른 디스크에도 그대로 저장되기 때문에, 같은 데이터가 여러 곳에 존재하게 된다. 따라서 디스크 하나에 장애(Failure)가 발생하더라도 다른 디스크에 남아 있는 데이터를 이용해 서비스를 계속할 수 있다.</p>
<p>RAID 1은 읽기(Read) 작업을 여러 디스크에 나누어 처리할 수 있기 때문에 읽기 성능(Read Performance)이 향상될 수 있다. 하지만 같은 데이터를 중복 저장(Redundant Storage)하기 때문에 실제 사용 가능한 저장 공간(Storage Capacity)의 효율은 낮아진다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/9f54e7e0-806b-4e2b-9aaf-78b525c1a6af.png" alt="" style="display:block;margin:0 auto" />

<p>그림으로 보면, 왼쪽 디스크에 <code>ABCD</code>가 저장되면 바로 옆 디스크에도 동일한 <code>ABCD</code>가 그대로 복사되어 저장된다. 마찬가지로 <code>EFGH</code>, <code>IJKL</code>, <code>MNOP</code>도 각각 한 쌍의 디스크에 동일하게 복제된다.</p>
<p>즉, 두 개의 디스크가 하나의 데이터 집합(Data Set)을 함께 보관하는 구조라고 볼 수 있다. 이처럼 동일한 데이터를 동시에 저장해두기 때문에 한쪽 디스크에 문제가 생겨도 다른 디스크를 이용해 데이터를 계속 사용할 수 있다.</p>
<p>정리하면 RAID 1은 저장 공간 효율(Storage Efficiency)은 낮지만, 데이터 안정성(Data Reliability)이 매우 중요한 환경에 적합한 방식이다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>RAID 1은 데이터 보호(Data Protection)에 강하지만, 백업(Backup)을 완전히 대체하는 것은 아니다. 예를 들어 사용자가 실수로 파일을 삭제하거나, 악성코드로 데이터가 손상되면 복제된 디스크에도 같은 변경이 반영될 수 있다.</p>
<p>따라서 RAID 1은 디스크 고장에 대비하는 방식이고, 중요한 데이터는 별도의 백업도 함께 필요하다고 이해하면 좋다.</p>
<hr />
<h3>RAID 5: Distributed Parity(RAID 5: 분산 패리티)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/35f7b751-01e6-4345-ae98-ab0c01ffa3d5.png" alt="" style="display:block;margin:0 auto" />

<p>RAID 5는 <code>패리티(Parity)</code> 기반의 저장 방식이다. 데이터 블록(Data Block)과 패리티 정보를 여러 디스크(Disk)에 분산해서 저장하는 구조이다.</p>
<p>여기서 패리티(Parity)는 디스크 장애가 발생했을 때 손실된 데이터를 복구하기 위한 계산 정보라고 이해할 수 있다. 여러 디스크 중 하나에 장애(Failure)가 발생하더라도, 남아 있는 데이터와 패리티 정보를 이용해 손실된 데이터를 복구할 수 있다.</p>
<p>RAID 5는 읽기 작업(Read Operation)이 여러 디스크에 분산되어 수행되기 때문에 읽기 성능(Read Performance)을 높일 수 있다. 또한 패리티를 통해 장애 복구가 가능하므로 성능(Performance)과 안정성(Reliability) 사이에서 비교적 균형 잡힌 구조를 제공한다.</p>
<p>다만 데이터를 기록할 때는 패리티를 함께 계산하고 저장해야 한다. 따라서 쓰기 작업(Write Operation)에서는 추가적인 패리티 계산 과정이 필요하며, 이로 인해 쓰기 성능(Write Performance)이 저하될 수 있다.</p>
<p>RAID 5는 최소 3개의 디스크가 필요하며, 저장 공간 효율(Storage Efficiency), 읽기 성능, 장애 복구 기능을 함께 고려할 수 있어 일반적인 서버 환경(Server Environment)에서 널리 사용되는 방식이다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/668eff47-6974-4119-91db-e5e6087ca990.png" alt="" style="display:block;margin:0 auto" />

<p>그림으로 보면 각 디스크에는 데이터 블록과 패리티 블록(Parity Block)이 함께 저장된다. 이때 패리티는 특정 디스크 하나에 고정되지 않고 여러 디스크에 분산되어 배치된다.</p>
<p>예를 들어 첫 번째 줄에서는 마지막 디스크에 <code>Parity 0</code>이 저장되고, 다음 줄에서는 다른 디스크에 <code>Parity 1</code>이 저장되는 방식으로 패리티 위치가 바뀐다. 이렇게 패리티를 분산하면 특정 디스크 하나에 패리티 처리 부담이 집중되는 것을 줄일 수 있다.</p>
<p>만약 디스크 하나에 장애가 발생하더라도, 같은 줄에 남아 있는 데이터 블록들과 패리티 정보를 이용해 손실된 블록을 다시 계산할 수 있다.</p>
<p>정리하면 RAID 5는 데이터를 분산 저장하면서 패리티를 함께 관리하여, 저장 공간 효율, 읽기 성능, 장애 복구 기능을 모두 고려한 실용적인 RAID 방식이라고 이해할 수 있다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>RAID 5는 디스크 1개 장애까지는 복구할 수 있지만, 동시에 2개 이상의 디스크가 고장 나면 데이터 복구가 어려울 수 있다. 2개 디스크 장애까지 대비하려면 RAID 6처럼 이중 패리티(Double Parity)를 사용하는 방식이 필요하다.</p>
<p>또한 RAID 5는 패리티 계산 때문에 작은 쓰기 작업(Random Write)이 많은 환경에서는 성능이 떨어질 수 있다. 따라서 읽기 작업이 많고 저장 공간 효율과 안정성을 함께 고려해야 하는 환경에 적합하다.</p>
<hr />
<h3>Comparison of RAID Performance and Reliability(RAID 성능과 안정성 비교)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f8f57fbd-cc83-4192-abd3-3de2ec63337a.png" alt="" style="display:block;margin:0 auto" />

<p>RAID 수준(RAID Level)에 따라 성능(Performance), 안정성(Reliability), 저장 공간 효율(Storage Efficiency)이 달라진다.</p>
<p>RAID 0은 데이터를 여러 디스크(Disk)에 분산 저장(Striping)하기 때문에 입출력 성능(I/O Performance)이 높다는 장점이 있다. 여러 디스크가 동시에 읽기와 쓰기에 참여할 수 있기 때문이다. 하지만 중복 저장(Redundancy)이나 복구 정보(Recovery Information)가 없기 때문에, 디스크 하나에 장애가 발생하면 데이터 보호(Data Protection)를 제공하기 어렵다.</p>
<p>RAID 1은 동일한 데이터를 여러 디스크에 복제 저장(Mirroring)하는 방식이다. 따라서 디스크 하나에 문제가 발생하더라도 다른 디스크에 남아 있는 데이터를 이용할 수 있어 안정성이 매우 높다. 하지만 같은 데이터를 중복해서 저장해야 하므로 실제 사용할 수 있는 저장 공간의 효율은 낮아진다.</p>
<p>RAID 5는 데이터(Data)와 패리티(Parity)를 여러 디스크에 분산 저장하는 방식이다. 이를 통해 읽기 성능(Read Performance)과 장애 복구 기능(Fault Tolerance)을 함께 제공할 수 있다. 따라서 RAID 5는 성능과 안정성 사이에서 비교적 균형 잡힌 방식이라고 할 수 있다.</p>
<p>결국 어떤 RAID 수준이 가장 좋다고 단정하기는 어렵다. 성능이 중요한지, 데이터 안정성이 중요한지, 저장 공간 효율이 중요한지에 따라 적합한 RAID 수준이 달라진다. 따라서 사용 목적(Purpose)과 운영 환경(Operating Environment)에 맞게 적절한 RAID 수준을 선택하는 것이 중요하다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>RAID 0은 성능은 좋지만 안정성이 낮고, RAID 1은 안정성은 높지만 저장 공간 효율이 낮다. RAID 5는 성능, 안정성, 저장 공간 효율을 균형 있게 고려한 방식이다.</p>
<p>다만 RAID는 디스크 고장에 대비하는 기술이지, 백업(Backup)을 완전히 대체하는 기술은 아니다. 실수로 파일을 삭제하거나 데이터가 손상되면 RAID 구성 안에서도 문제가 그대로 반영될 수 있으므로 중요한 데이터는 별도 백업이 필요하다.</p>
<hr />
<h3>Storage Device Practice Overview(저장장치 실습 개요)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/9ffc4e50-1620-4f6f-95bc-b796869011ba.png" alt="" style="display:block;margin:0 auto" />

<p>앞서 디스크(Disk)의 구조, 접근 시간(Access Time), 성능 향상 기법(Performance Optimization Method)을 살펴보았다. 이제 이러한 개념들이 실제 시스템(System)에서 어떻게 나타나는지 확인하는 실습을 진행한다.</p>
<p>이번 실습에서는 <code>순차 접근(Sequential Access)</code>과 <code>랜덤 접근(Random Access)</code>의 차이를 확인하고, FCFS(First-Come, First-Served)와 SSTF(Shortest Seek Time First) 스케줄링을 비교한다. 또한 버퍼링(Buffering)과 캐시(Cache)가 성능에 어떤 영향을 주는지도 함께 살펴본다.</p>
<p>첫 번째 실습은 순차 접근과 랜덤 접근의 차이를 확인하는 내용이다. 디스크는 데이터가 저장된 위치와 접근 방식에 따라 접근 시간이 달라질 수 있다.</p>
<p>연속된 위치의 데이터를 차례대로 읽는 순차 접근은 헤드 이동(Head Movement)이나 위치 변경 비용이 적기 때문에 비교적 효율적으로 처리된다.</p>
<p>반면 떨어진 위치의 데이터를 반복해서 읽는 랜덤 접근은 헤드 이동이나 위치 변경 비용이 증가할 수 있다. 이로 인해 순차 접근보다 더 많은 시간이 걸릴 수 있다.</p>
<p>따라서 이번 실습에서는 접근 방식에 따라 왜 성능 차이가 발생하는지 직접 확인해본다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>순차 접근(Sequential Access)과 랜덤 접근(Random Access)의 성능 차이는 특히 HDD(Hard Disk Drive)에서 크게 나타난다. HDD는 헤드 이동과 회전 지연이 있기 때문이다.</p>
<p>반면 SSD(Solid State Drive)는 물리적인 헤드 이동이 없기 때문에 HDD보다 랜덤 접근 성능이 좋지만, 내부 컨트롤러(Controller)와 플래시 메모리(Flash Memory) 구조에 따라 순차 접근과 랜덤 접근 사이의 성능 차이가 완전히 사라지는 것은 아니다.</p>
<hr />
<h3>Sequential Access and Random Access Comparison</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/6b2eb373-83b7-4ce7-9eff-8fb57b74429a.png" alt="" style="display:block;margin:0 auto" />

<p>순차 접근(Sequential Access)은 현재 위치 다음의 데이터를 연속해서 계속 읽어나가는 방식이다. 반면 랜덤 접근(Random Access)은 필요한 위치로 직접 이동한 뒤 해당 데이터를 읽는 방식이다.</p>
<p>파일 안에서 읽기 위치를 직접 변경할 때는 <code>lseek()</code> 함수를 사용한다. 이번 실습에서는 <code>lseek()</code>를 이용해 접근 위치를 계속 바꾸면서, 위치 변경이 많아질수록 처리 시간이 증가할 수 있다는 점을 확인한다.</p>
<p>그림의 핵심 코드를 보면 먼저 순차 접근은 다음과 같이 이루어진다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/763ed378-d7af-41f3-a5c4-6ddb1e55a949.png" alt="" style="display:block;margin:0 auto" />

<pre><code class="language-c">lseek(fd, 0, SEEK_SET);
read(fd, buf, BLOCK_SIZE);
</code></pre>
<p>여기서 <code>fd</code>는 열린 파일을 가리키는 파일 디스크립터(File Descriptor)이다. <code>0</code>은 이동할 위치를 의미하고, <code>SEEK_SET</code>은 파일의 시작 지점(Start Point)을 기준으로 위치를 정하겠다는 의미이다.</p>
<p>이후 <code>read()</code>를 실행하면 현재 오프셋(Offset) 위치에서 <code>BLOCK_SIZE</code>만큼 데이터를 읽어 <code>buf</code>에 저장한다. 한 번 읽고 나면 파일 오프셋은 자동으로 다음 위치로 이동하므로, 반복해서 읽으면 파일을 앞에서부터 차례대로 읽는 순차 접근이 된다.</p>
<p>반면 랜덤 접근은 다음과 같은 방식으로 이루어진다.</p>
<pre><code class="language-c">offset = (rand() % 1024) * BLOCK_SIZE;
lseek(fd, offset, SEEK_SET);
read(fd, buf, BLOCK_SIZE);
</code></pre>
<p>이 코드는 0부터 1023 사이의 임의의 블록 번호를 선택하고, 여기에 <code>BLOCK_SIZE</code>를 곱해 실제 파일 안에서 이동할 위치를 결정한다. 이후 <code>lseek()</code>를 통해 파일 오프셋을 해당 위치로 직접 이동시키고, <code>read()</code>를 통해 그 위치의 데이터를 읽는다.</p>
<p>즉, 순차 접근은 현재 위치에서 다음 데이터를 계속 읽는 방식이고, 랜덤 접근은 <code>lseek()</code>를 이용해 매번 다른 위치로 이동한 뒤 읽는 방식이다.</p>
<p>두 방식을 같은 횟수만큼 반복해서 실행 시간을 비교하면, 접근 위치를 계속 바꾸는 랜덤 접근이 순차 접근보다 더 많은 처리 비용을 가질 수 있다는 점을 확인할 수 있다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>순차 접근은 파일의 다음 위치를 계속 읽기 때문에 운영체제(Operating System)의 Read-Ahead나 캐시(Cache) 효과를 받기 쉽다.</p>
<p>반면 랜덤 접근은 매번 다른 위치로 이동하기 때문에 HDD(Hard Disk Drive)에서는 헤드 이동(Head Movement)과 탐색 시간(Seek Time)의 영향이 커질 수 있다. SSD(Solid State Drive)는 물리적인 헤드 이동이 없기 때문에 랜덤 접근의 부담이 HDD보다 작지만, 순차 접근과 랜덤 접근의 성능 차이가 완전히 사라지는 것은 아니다.</p>
<hr />
<h3>Sequential and Random Access Example Code</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/e58efb32-d679-4053-b5da-e04580ef711c.png" alt="" style="display:block;margin:0 auto" />

<p>이 예제는 순차 접근(Sequential Access)과 랜덤 접근(Random Access)의 실행 시간을 직접 측정하여 성능 차이를 확인하기 위한 코드이다.</p>
<p>먼저 <code>make_test_file()</code> 함수에서는 실험용 파일(Test File)을 생성한다. <code>open()</code> 함수에서 <code>O_CREAT</code>, <code>O_TRUNC</code>, <code>O_WRONLY</code> 옵션을 사용하여 파일이 없으면 새로 만들고, 기존 파일이 있으면 내용을 비운 뒤 쓰기 전용으로 연다.</p>
<p>그다음 여러 개의 블록 데이터(Block Data)를 반복해서 기록한다. 즉, 순차 접근과 랜덤 접근 실험에 사용할 충분한 크기의 파일을 미리 만들어두는 과정이다.</p>
<p>메인 함수(Main Function)에서는 이 파일을 읽기 전용(Read Only)으로 연 뒤, 순차 접근 시간과 랜덤 접근 시간을 각각 측정한다.</p>
<p>순차 접근은 <code>lseek()</code>를 사용해 파일의 시작 위치로 이동한 뒤, 반복문에서 <code>read()</code>만 수행하여 연속된 블록을 차례대로 읽는다. 이 경우 한 번 읽을 때마다 오프셋(Offset)이 자동으로 다음 위치로 이동하므로 파일을 앞에서부터 순서대로 읽게 된다.</p>
<p>반면 랜덤 접근은 <code>rand()</code>를 사용해 임의의 블록 위치를 정하고, <code>lseek()</code>로 매번 다른 위치로 이동한 뒤 <code>read()</code>를 수행한다. 즉, 파일 안의 여러 위치를 반복적으로 이동하면서 데이터를 읽는 방식이다.</p>
<p>마지막으로 <code>clock()</code> 함수를 이용해 순차 접근과 랜덤 접근의 실행 시간을 측정하고 출력한다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/75817cf1-356d-4b11-8eb5-b580f836f983.png" alt="" style="display:block;margin:0 auto" />

<p>실행 결과를 보면 순차 접근 시간은 <code>0.001248초</code>, 랜덤 접근 시간은 <code>0.004913초</code>로 측정되었다. 같은 양의 데이터를 읽더라도 순차 접근이 랜덤 접근보다 더 빠르게 나타난 것을 확인할 수 있다.</p>
<p>그 이유는 순차 접근은 연속된 위치를 읽기 때문에 위치 변경 비용이 거의 발생하지 않지만, 랜덤 접근은 매번 다른 위치로 이동해야 하므로 추가적인 처리 지연과 탐색 비용(Seek Cost)이 발생할 수 있기 때문이다.</p>
<p>따라서 이 예제는 저장장치(Storage Device)가 연속된 위치를 읽는 접근 방식에 더 유리하며, 디스크 접근 순서(Disk Access Order)가 성능에 영향을 준다는 점을 확인할 수 있는 실습이다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>실습 결과는 실행 환경에 따라 달라질 수 있다. 운영체제 캐시(OS Cache)가 작동하면 두 번째 실행부터는 디스크에서 직접 읽는 것이 아니라 메모리 캐시에서 읽을 수 있기 때문에 시간이 더 짧게 측정될 수 있다.</p>
<hr />
<h3>Disk Scheduling Simulation(디스크 스케줄링 시뮬레이션)</h3>
<p>디스크(Disk)는 같은 요청 집합(Request Set)이 들어오더라도, 어떤 순서로 처리하느냐에 따라 접근 비용(Access Cost)이 달라질 수 있다.</p>
<p>즉, 요청 처리 순서(Request Processing Order)에 따라 디스크 헤드(Disk Head)의 이동 거리와 전체 처리 시간(Total Processing Time)이 달라진다. 따라서 디스크 스케줄링 알고리즘(Disk Scheduling Algorithm)은 효율적인 요청 처리 순서를 결정하기 위해 필요하다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/c1f49888-b877-4fac-bf95-f8395e5275b5.png" alt="" style="display:block;margin:0 auto" />

<p>이번 실습에서는 대표적인 디스크 스케줄링 방식인 FCFS(First-Come, First-Served)와 SSTF(Shortest Seek Time First)를 비교하여 성능 차이를 직접 확인한다.</p>
<p>디스크 요청(Disk Request)은 일반적으로 큐(Queue) 형태로 순차적으로 도착한다. 운영체제(Operating System)는 이 요청들을 어떤 순서로 처리할지 결정하며, 이 과정이 디스크 스케줄링(Disk Scheduling)이다.</p>
<p>특히 HDD(Hard Disk Drive)에서는 헤드가 이동한 거리가 길어질수록 탐색 시간(Seek Time)이 증가하고, 전체 접근 시간(Disk Access Time)에도 큰 영향을 준다.</p>
<p>따라서 가능한 한 헤드 이동 거리(Head Movement Distance)를 줄이는 방향으로 요청을 처리하면 전체 디스크 성능(Disk Performance)을 개선할 수 있다.</p>
<p>이번 시뮬레이션에서는 FCFS와 SSTF가 같은 요청 집합을 어떻게 다르게 처리하는지 살펴보고, 그 결과 총 헤드 이동 거리(Total Head Movement)가 어떻게 달라지는지 비교한다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>FCFS는 요청이 도착한 순서대로 처리하기 때문에 구현이 단순하고 공정하지만, 헤드 이동 거리가 길어질 수 있다.</p>
<p>SSTF는 현재 헤드 위치에서 가장 가까운 요청을 먼저 처리하므로 총 이동 거리를 줄이는 데 유리하지만, 멀리 있는 요청이 오래 기다리는 기아 현상(Starvation)이 발생할 수 있다.</p>
<hr />
<h3>Disk Scheduling Simulation(디스크 스케줄링 시뮬레이션)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/d5da4f48-8801-4924-b5ee-0d719d952247.png" alt="" style="display:block;margin:0 auto" />

<p>디스크(Disk)는 같은 요청 집합(Request Set)이 들어오더라도, 어떤 순서로 처리하느냐에 따라 접근 비용(Access Cost)이 달라질 수 있다.</p>
<p>즉, 요청 처리 순서(Request Processing Order)에 따라 디스크 헤드(Disk Head)의 이동 거리와 전체 처리 시간(Total Processing Time)이 달라진다. 따라서 디스크 스케줄링 알고리즘(Disk Scheduling Algorithm)은 효율적인 요청 처리 순서를 결정하기 위해 필요하다.</p>
<p>이번 실습에서는 대표적인 디스크 스케줄링 방식인 FCFS(First-Come, First-Served)와 SSTF(Shortest Seek Time First)를 비교하여 성능 차이를 직접 확인한다.</p>
<p>디스크 요청(Disk Request)은 일반적으로 큐(Queue) 형태로 순차적으로 도착한다. 운영체제(Operating System)는 이 요청들을 어떤 순서로 처리할지 결정하며, 이 과정이 디스크 스케줄링(Disk Scheduling)이다.</p>
<p>특히 HDD(Hard Disk Drive)에서는 헤드가 이동한 거리가 길어질수록 탐색 시간(Seek Time)이 증가하고, 전체 접근 시간(Disk Access Time)에도 큰 영향을 준다.</p>
<p>따라서 가능한 한 헤드 이동 거리(Head Movement Distance)를 줄이는 방향으로 요청을 처리하면 전체 디스크 성능(Disk Performance)을 개선할 수 있다.</p>
<p>이번 시뮬레이션에서는 FCFS와 SSTF가 같은 요청 집합을 어떻게 다르게 처리하는지 살펴보고, 그 결과 총 헤드 이동 거리(Total Head Movement)가 어떻게 달라지는지 비교한다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>FCFS는 요청이 도착한 순서대로 처리하기 때문에 구현이 단순하고 공정하지만, 헤드 이동 거리가 길어질 수 있다.</p>
<p>SSTF는 현재 헤드 위치에서 가장 가까운 요청을 먼저 처리하므로 총 이동 거리를 줄이는 데 유리하지만, 멀리 있는 요청이 오래 기다리는 기아 현상(Starvation)이 발생할 수 있다.</p>
<hr />
<h3>FCFS and SSTF Simulation Code</h3>
<p>이 코드는 FCFS(First-Come, First-Served)와 SSTF(Shortest Seek Time First) 스케줄링 방식의 <code>총 헤드 이동 거리(Total Head Movement)</code>를 계산하는 핵심 부분이다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/0d6297ae-60af-4bf7-a1de-7d6dab08d519.png" alt="" style="display:block;margin:0 auto" />

<p>먼저 FCFS 코드를 살펴보면, 반복문을 통해 요청 배열(Request Array)을 앞에서부터 차례대로 처리한다. 여기서 <code>abs(current - request[i])</code>는 현재 헤드 위치(Current Head Position)에서 다음 요청 위치(Request Position)까지의 이동 거리를 의미한다.</p>
<p>이 값을 <code>total</code>에 계속 누적하고, 이후 <code>current = request[i]</code>를 통해 현재 헤드 위치를 방금 처리한 요청 위치로 변경한다. 즉, FCFS는 요청이 도착한 순서를 그대로 따라가면서 이동 거리를 차례대로 누적 계산하는 방식이다.</p>
<p>반면 SSTF 코드는 아직 처리하지 않은 요청들 가운데 현재 헤드 위치에서 가장 가까운 요청을 찾는 구조이다. 내부 반복문에서 각 요청과 현재 위치 사이의 거리를 계산하고, 그중 가장 작은 거리인 <code>min</code>을 가진 요청의 인덱스(Index)를 <code>idx</code>로 선택한다.</p>
<p>그다음 선택된 요청을 처리 완료 상태로 표시하고, 해당 이동 거리인 <code>min</code>을 <code>total</code>에 더한다. 이후 현재 위치도 선택된 요청 위치로 변경한다.</p>
<p>즉, SSTF는 매 단계마다 가장 가까운 요청을 선택하여 총 헤드 이동 거리를 줄이도록 동작하는 방식이다.</p>
<p>이 두 코드를 실행해서 결과를 비교하면, 같은 요청 집합(Request Set)이라도 처리 순서(Request Processing Order)에 따라 전체 이동 비용(Total Movement Cost)이 어떻게 달라지는지 확인할 수 있다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>또한 SSTF는 총 이동 거리를 줄이는 데 효과적이지만, 가까운 요청만 계속 선택하면 멀리 있는 요청이 오래 기다리는 기아 현상(Starvation)이 발생할 수 있다.</p>
<hr />
<h3>FCFS and SSTF Simulation Example Code</h3>
<p>이 코드는 같은 요청 집합(Request Set)에서 FCFS(First-Come, First-Served)와 SSTF(Shortest Seek Time First) 방식의 <code>총 헤드 이동 거리(Total Head Movement)</code>를 비교하는 예제이다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/7561e27a-486e-447d-9635-8bc5f186bf73.png" alt="" style="display:block;margin:0 auto" />

<p>먼저 요청 배열(Request Array)에는 다음과 같은 디스크 요청들이 저장되어 있다.</p>
<pre><code class="language-c">int request[N] = {98, 183, 37, 122, 14, 124, 65, 67};
</code></pre>
<p>초기 헤드 위치(Initial Head Position)는 다음과 같이 53으로 설정되어 있다.</p>
<pre><code class="language-c">int head = 53;
</code></pre>
<p>FCFS 부분에서는 요청 배열을 앞에서부터 순서대로 처리한다. 현재 헤드 위치(Current Head Position)와 다음 요청 위치(Request Position)의 차이를 절댓값으로 계산하고, 그 값을 <code>total_fcfs</code>에 누적한다.</p>
<p>즉, FCFS는 요청이 도착한 순서를 그대로 따라가면서 헤드가 얼마나 이동하는지를 계산하는 방식이다.</p>
<p>반면 SSTF 부분에서는 아직 처리되지 않은 요청들 가운데 현재 헤드 위치에서 가장 가까운 요청을 찾는다. 내부 반복문에서 모든 후보 요청과의 거리를 비교하고, 가장 짧은 거리의 요청을 선택한다.</p>
<p>이 과정을 모든 요청이 처리될 때까지 반복하면서 총 이동 거리를 <code>total_sstf</code>에 누적한다. 마지막으로 FCFS와 SSTF의 결과를 출력하여 알고리즘에 따라 이동 비용(Movement Cost)이 어떻게 달라지는지 확인한다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/6d5697af-2535-47b4-a9cc-2e32e073ce08.png" alt="" style="display:block;margin:0 auto" />

<p>실행 결과를 보면 FCFS의 총 이동 거리는 <code>640</code>, SSTF의 총 이동 거리는 <code>246</code>으로 나타난다. 즉, SSTF가 FCFS보다 헤드 이동 거리를 크게 줄일 수 있다는 것을 확인할 수 있다.</p>
<p>그 이유는 FCFS는 요청이 도착한 순서를 그대로 처리하지만, SSTF는 현재 위치에서 가장 가까운 요청을 우선 처리하기 때문이다.</p>
<p>따라서 요청 처리 순서(Request Processing Order)는 디스크 성능(Disk Performance)에 직접적인 영향을 준다. 이 실습은 디스크 스케줄링(Disk Scheduling)이 왜 필요한지 확인할 수 있는 예제라고 할 수 있다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>또한 SSTF는 총 헤드 이동 거리를 줄이는 데 효과적이지만, 가까운 요청이 계속 들어오면 멀리 있는 요청이 오래 기다리는 기아 현상(Starvation)이 발생할 수 있다. 따라서 성능과 공정성(Fairness)을 함께 고려해야 한다.</p>
<hr />
<h3>Buffering Effect Practice(버퍼링 효과 확인)</h3>
<p>이번 실습은 <code>버퍼링(Buffering)</code>이 디스크 성능(Disk Performance)에 어떤 영향을 주는지 확인하는 내용이다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/af5f9234-fa59-47c7-bec8-427e882ce225.png" alt="" style="display:block;margin:0 auto" />

<p>디스크 쓰기 작업(Write Operation)은 프로그램에서 <code>write()</code> 함수를 호출했다고 해서 항상 즉시 디스크(Disk)에 기록되는 것은 아니다. 운영체제(Operating System)는 성능 향상을 위해 데이터를 먼저 메모리 버퍼(Memory Buffer)에 저장한 뒤, 나중에 한꺼번에 디스크에 기록할 수 있다.</p>
<p>즉, 프로그램이 <code>write()</code>를 호출하는 시점과 실제 데이터가 디스크에 기록되는 시점은 서로 다를 수 있다.</p>
<p>이러한 버퍼링 방식은 여러 개의 작은 쓰기 요청(Write Request)을 모아서 처리할 수 있기 때문에 디스크 접근 횟수(Disk Access Count)를 줄이는 데 도움이 된다. 디스크에 매번 접근하지 않고 한 번에 처리하면 전체 입출력 성능(I/O Performance)을 향상시킬 수 있다.</p>
<p>이번 실습에서는 버퍼링이 디스크 접근 횟수와 전체 성능에 어떤 영향을 주는지 확인한다. 이를 통해 운영체제가 왜 데이터를 즉시 디스크에 기록하지 않고, 메모리 버퍼를 활용해 쓰기 작업을 최적화하는지 이해할 수 있다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>버퍼링(Buffering)은 성능 향상에는 도움이 되지만, 데이터가 아직 디스크에 기록되기 전에 시스템 장애(System Failure)나 전원 문제(Power Failure)가 발생하면 데이터 손실 위험이 있을 수 있다.</p>
<p>따라서 운영체제는 필요할 때 플러시(Flush)나 동기화(Synchronization)를 통해 버퍼에 있는 데이터를 실제 디스크에 반영한다.</p>
<hr />
<h3>Buffering Effect Practice(버퍼링 효과 확인)</h3>
<p>이번 실습은 버퍼링(Buffering)이 디스크 성능(Disk Performance)에 어떤 영향을 주는지 확인하는 내용이다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/432133c6-e938-4b73-aed8-bf0c4dc00168.png" alt="" style="display:block;margin:0 auto" />

<p>디스크 쓰기 작업(Write Operation)은 프로그램에서 <code>write()</code> 함수를 호출했다고 해서 항상 즉시 디스크(Disk)에 기록되는 것은 아니다. 운영체제(Operating System)는 성능 향상을 위해 데이터를 먼저 메모리 버퍼(Memory Buffer)에 저장한 뒤, 나중에 한꺼번에 디스크에 기록할 수 있다.</p>
<p>즉, 프로그램이 <code>write()</code>를 호출하는 시점과 실제 데이터가 디스크에 기록되는 시점은 서로 다를 수 있다.</p>
<p>이러한 버퍼링 방식은 여러 개의 작은 쓰기 요청(Write Request)을 모아서 처리할 수 있기 때문에 디스크 접근 횟수(Disk Access Count)를 줄이는 데 도움이 된다. 디스크에 매번 접근하지 않고 한 번에 처리하면 전체 입출력 성능(I/O Performance)을 향상시킬 수 있다.</p>
<p>이번 실습에서는 버퍼링이 디스크 접근 횟수와 전체 성능에 어떤 영향을 주는지 확인한다. 이를 통해 운영체제가 왜 데이터를 즉시 디스크에 기록하지 않고, 메모리 버퍼를 활용해 쓰기 작업을 최적화하는지 이해할 수 있다.</p>
<p>추가로 알아두면 좋은 점</p>
<p>버퍼링(Buffering)은 성능 향상에는 도움이 되지만, 데이터가 아직 디스크에 기록되기 전에 시스템 장애(System Failure)나 전원 문제(Power Failure)가 발생하면 데이터 손실 위험이 있을 수 있다.</p>
<p>따라서 운영체제는 필요할 때 플러시(Flush)나 동기화(Synchronization)를 통해 버퍼에 있는 데이터를 실제 디스크에 반영한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/8745accc-a635-434d-a5e7-8080aa86e372.png" alt="" style="display:block;margin:0 auto" />

<h3>Buffering and <code>fsync()</code></h3>
<p>버퍼링(Buffering)은 디스크 쓰기 성능(Write Performance)을 높이기 위한 방식이다. <code>write()</code> 함수는 데이터를 즉시 디스크(Disk)에 기록하는 것이 아니라, 우선 커널 버퍼(Kernel Buffer)에 저장한다.</p>
<p>이후 운영체제(Operating System)는 일정 시점에 버퍼에 모인 데이터를 실제 디스크에 반영한다. 이렇게 여러 번의 작은 쓰기 요청(Write Request)을 모아서 처리하면 디스크 접근 횟수(Disk Access Count)를 줄일 수 있고, 전체 입출력 효율(I/O Efficiency)을 높일 수 있다.</p>
<p>반대로 <code>fsync()</code>는 버퍼에 있는 데이터를 즉시 디스크에 강제로 기록하도록 요청하는 함수이다. 즉, <code>write()</code>만 호출한 경우에는 데이터가 커널 버퍼에 머물 수 있지만, <code>fsync()</code>를 호출하면 해당 파일과 관련된 변경 내용을 실제 디스크에 반영하게 된다.</p>
<p>사진에 포함된 핵심 코드는 버퍼링 동작과 <code>fsync()</code>의 차이를 확인하기 위한 부분이다.</p>
<pre><code class="language-c">write(fd, buf, SIZE);

// 강제 디스크 반영
fsync(fd);
</code></pre>
<p>먼저 <code>write(fd, buf, SIZE)</code>는 파일에 데이터를 기록하라는 요청을 운영체제에 전달한다. 이 호출은 비교적 빠르게 종료될 수 있으며, 실제 디스크 기록 작업은 운영체제가 나중에 처리할 수 있다.</p>
<p>그다음 <code>fsync(fd)</code>를 호출하면 해당 파일의 버퍼 내용을 즉시 디스크에 반영하도록 요청한다. 따라서 <code>write()</code>만 사용한 경우와 <code>write()</code> 뒤에 <code>fsync()</code>까지 수행한 경우를 비교하면, 강제로 디스크에 저장하는 과정에서 실행 시간이 어떻게 달라지는지 확인할 수 있다.</p>
<p>정리하면, 버퍼링은 쓰기 요청을 모아 처리하여 성능을 높이는 방식이고, <code>fsync()</code>는 데이터 안정성을 위해 버퍼 내용을 실제 디스크에 강제로 기록하는 방식이다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p><code>write()</code>가 성공했다고 해서 데이터가 반드시 즉시 물리 디스크에 저장되었다는 뜻은 아니다. 운영체제의 버퍼에 기록된 상태일 수 있다.</p>
<p>반면 <code>fsync()</code>는 데이터 손실 위험을 줄이는 데 도움이 되지만, 실제 디스크 기록을 기다려야 하므로 실행 시간이 더 길어질 수 있다. 즉, 버퍼링은 성능을 높이고, <code>fsync()</code>는 안정성을 높이는 방향의 기능이라고 이해하면 좋다.</p>
<hr />
<h3>Buffering Effect Example Code</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/00fd22b9-6b3f-4798-894f-c11642741249.png" alt="" style="display:block;margin:0 auto" />

<p>이 예제는 버퍼링(Buffering)만 사용할 때와 매번 <code>fsync()</code>를 사용할 때의 실행 시간을 비교하는 코드이다.</p>
<p>먼저 버퍼링만 사용하는 경우에는 파일을 연 뒤, 반복문 안에서 <code>write()</code>를 여러 번 수행한다. 이때 각 쓰기 요청(Write Request)은 운영체제(Operating System)의 커널 버퍼(Kernel Buffer)를 통해 처리된다. 따라서 <code>write()</code>를 호출할 때마다 실제 디스크(Disk)에 바로 기록되지 않을 수 있다.</p>
<p>반면 <code>fsync()</code>를 사용하는 경우에는 같은 방식으로 <code>write()</code>를 수행한 뒤, 반복적으로 <code>fsync()</code> 함수를 호출한다. 이는 데이터를 쓸 때마다 버퍼에 있는 내용을 실제 디스크에 강제로 반영하도록 하는 구조이다.</p>
<p>두 경우 모두 <code>clock()</code> 함수를 이용해 전체 수행 시간을 측정하고, 마지막에는 <code>close()</code>를 통해 파일을 닫는다. 이를 통해 버퍼링만 사용할 때와 <code>fsync()</code>를 매번 사용할 때 성능 차이가 어떻게 나타나는지 확인할 수 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/cb343bb8-66f2-4328-847a-3b0704ab6208.png" alt="" style="display:block;margin:0 auto" />

<p>실행 결과를 보면 버퍼링만 사용하는 경우는 <code>0.002341초</code>, <code>fsync()</code>를 사용하는 경우는 <code>0.128942초</code>로 측정된다. 즉, <code>fsync()</code>를 매번 호출하면 수행 시간이 크게 증가하는 것을 확인할 수 있다.</p>
<p>그 이유는 버퍼링 방식은 여러 쓰기 요청을 모아서 처리할 수 있지만, <code>fsync()</code>는 매번 실제 디스크 기록이 완료될 때까지 기다려야 하기 때문이다.</p>
<p>따라서 버퍼링은 디스크 접근 횟수(Disk Access Count)를 줄여 성능 향상에 도움이 되지만, 즉시 기록을 보장하려면 그만큼 추가 비용이 발생한다.</p>
<p>즉, 이 실습은 쓰기 성능(Write Performance)과 데이터 안전성(Data Safety) 사이에 트레이드 오프(Trade-off)가 존재한다는 점을 보여준다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p><code>write()</code>는 데이터를 커널 버퍼에 기록한 뒤 빠르게 반환될 수 있지만, <code>fsync()</code>는 버퍼 내용을 실제 저장장치에 반영하도록 강제한다.</p>
<p>그래서 <code>fsync()</code>는 데이터 손실 위험을 줄이는 데 도움이 되지만, 성능 측면에서는 비용이 크다. 중요한 데이터는 안정성이 필요하므로 <code>fsync()</code>가 유용할 수 있고, 일반적인 대량 쓰기에서는 버퍼링을 활용하는 것이 더 효율적일 수 있다.</p>
<hr />
<h3>Cache Effect Practice(캐시 효과 확인)</h3>
<p>이번 실습은 <code>캐시(Cache)</code>가 디스크 접근 성능(Disk Access Performance)에 어떤 영향을 주는지 확인하는 내용이다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/aadc1dd9-4dfe-438c-a0b7-4c60e81340b0.png" alt="" style="display:block;margin:0 auto" />

<p>같은 파일(File)을 반복해서 읽을 때, 매번 수행 시간이 동일하지 않을 수 있다. 그 이유는 운영체제(Operating System)가 디스크(Disk)에서 읽은 데이터를 메모리(Memory)에 캐시 형태로 저장해둘 수 있기 때문이다.</p>
<p>첫 번째 접근에서는 실제 디스크에서 데이터를 읽어야 하므로 시간이 더 걸릴 수 있다. 하지만 이후 같은 데이터를 다시 요청하면, 운영체제가 디스크에 다시 접근하지 않고 메모리에 저장된 캐시 데이터를 사용할 수 있다.</p>
<p>이렇게 캐시에서 데이터를 제공하면 디스크 접근 횟수(Disk Access Count)가 줄어들고, 전체 읽기 성능(Read Performance)이 향상될 수 있다.</p>
<p>따라서 이번 실습은 같은 파일을 반복해서 읽을 때 캐시가 성능에 어떤 영향을 주는지 확인하는 예제이다. 이를 통해 디스크 접근은 항상 실제 디스크에서만 이루어지는 것이 아니라, 운영체제의 캐시 구조를 통해 더 빠르게 처리될 수 있다는 점을 이해할 수 있다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>운영체제 캐시(OS Cache)는 한 번 읽은 데이터를 메모리에 보관해두었다가 같은 데이터가 다시 요청될 때 빠르게 제공한다.</p>
<p>다만 실습 결과는 실행 환경에 따라 달라질 수 있다. 이미 파일이 캐시에 올라와 있다면 첫 번째 실행부터 빠르게 측정될 수 있고, 시스템의 메모리 상태나 다른 작업에 따라 결과가 달라질 수 있다.</p>
<hr />
<h3>Cache Effect Practice(캐시 효과 확인)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a598a045-7cc4-439e-a47e-e65819a7d8cf.png" alt="" style="display:block;margin:0 auto" />

<p>이번 실습은 캐시(Cache)가 디스크 접근 성능(Disk Access Performance)에 어떤 영향을 주는지 확인하는 내용이다.</p>
<p>같은 파일(File)을 반복해서 읽을 때, 매번 수행 시간이 동일하지 않을 수 있다. 그 이유는 운영체제(Operating System)가 디스크(Disk)에서 읽은 데이터를 메모리(Memory)에 캐시 형태로 저장해둘 수 있기 때문이다.</p>
<p>첫 번째 접근에서는 실제 디스크에서 데이터를 읽어야 하므로 시간이 더 걸릴 수 있다. 하지만 이후 같은 데이터를 다시 요청하면, 운영체제가 디스크에 다시 접근하지 않고 메모리에 저장된 캐시 데이터를 사용할 수 있다.</p>
<p>이렇게 캐시에서 데이터를 제공하면 디스크 접근 횟수(Disk Access Count)가 줄어들고, 전체 읽기 성능(Read Performance)이 향상될 수 있다.</p>
<p>따라서 이번 실습은 같은 파일을 반복해서 읽을 때 캐시가 성능에 어떤 영향을 주는지 확인하는 예제이다. 이를 통해 디스크 접근은 항상 실제 디스크에서만 이루어지는 것이 아니라, 운영체제의 캐시 구조를 통해 더 빠르게 처리될 수 있다는 점을 이해할 수 있다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>운영체제 캐시(OS Cache)는 한 번 읽은 데이터를 메모리에 보관해두었다가 같은 데이터가 다시 요청될 때 빠르게 제공한다.</p>
<p>다만 실습 결과는 실행 환경에 따라 달라질 수 있다. 이미 파일이 캐시에 올라와 있다면 첫 번째 실행부터 빠르게 측정될 수 있고, 시스템의 메모리 상태나 다른 작업에 따라 결과가 달라질 수 있다.</p>
<hr />
<h3>Cache Effect Code(캐시 효과 확인 코드)</h3>
<p>캐시(Cache)는 디스크에서 읽은 데이터를 운영체제(Operating System)가 메모리(Memory)에 임시로 저장해두는 방식이다. 이후 동일한 데이터를 다시 읽으면 디스크(Disk)에 다시 접근하지 않고, 메모리 캐시에서 바로 가져올 수 있다.</p>
<p>즉, 느린 저장장치(Storage Device) 대신 빠른 메모리를 활용하여 처리 시간을 줄이는 방식이다. 같은 데이터에 대한 반복 접근이 많을수록 캐시 사용 여부에 따라 성능 차이가 크게 나타날 수 있다.</p>
<p>사진에 포함된 코드는 동일한 파일(File)을 두 번 읽으면서 캐시 효과를 확인하기 위한 핵심 코드이다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a7b064bb-7737-426c-81c2-f0f385b63c1b.png" alt="" style="display:block;margin:0 auto" />

<p>먼저 <code>make_test_file(filename)</code>을 통해 실험에 사용할 테스트 파일(Test File)을 생성한다. 충분한 크기의 파일을 만들어 실제 읽기 동작을 측정할 수 있도록 준비하는 과정이다.</p>
<p>그다음 첫 번째 <code>read(fd, buf, SIZE)</code>를 수행하면 파일의 데이터를 읽는다. 이때 데이터는 실제 디스크에서 읽혀올 수 있으며, 운영체제는 읽은 데이터를 메모리 캐시에 함께 저장할 수 있다.</p>
<p>이후 <code>lseek(fd, 0, SEEK_SET)</code>을 실행하면 파일 오프셋(Offset)을 다시 처음 위치로 이동시킨다. 즉, 같은 데이터를 다시 읽기 위해 시작 위치를 재설정하는 과정이다.</p>
<p>마지막으로 두 번째 <code>read(fd, buf, SIZE)</code>를 수행하면 동일한 데이터를 다시 읽는다. 이때 이미 데이터가 메모리 캐시에 존재한다면, 디스크에 다시 접근하지 않고 더 빠르게 처리될 수 있다.</p>
<p>따라서 첫 번째 읽기 시간과 두 번째 읽기 시간을 비교하면, 캐시가 디스크 접근 성능(Disk Access Performance)에 어떤 영향을 주는지 확인할 수 있다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>캐시 효과는 실행 환경에 따라 다르게 나타날 수 있다. 이미 파일이 캐시에 올라와 있으면 첫 번째 읽기부터 빠르게 측정될 수 있고, 시스템 메모리 상태에 따라 결과가 달라질 수 있다.</p>
<hr />
<h3>Cache Effect Example Code(캐시 효과 확인 예제 코드)</h3>
<p>위 코드는 동일한 파일(File)을 두 번 읽어서 첫 번째 읽기 시간과 두 번째 읽기 시간을 비교하는 예제이다. 이를 통해 운영체제 캐시(OS Cache)가 파일 읽기 성능에 어떤 영향을 주는지 확인할 수 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/05b69548-1b5b-4de8-91da-8fb6ebe3747e.png" alt="" style="display:block;margin:0 auto" />

<p>먼저 상수 정의 부분에서는 읽기 크기(Size), 반복 횟수(Repeat Count), 파일 크기(File Size)가 설정된다. 이후 <code>make_test_file()</code> 함수를 이용해 실험에 사용할 테스트 파일(Test File)을 생성한다.</p>
<p><code>make_test_file()</code>에서는 <code>open()</code>으로 파일을 열고, <code>write()</code>를 이용해 약 2MB 크기의 데이터를 파일에 기록한다. 즉, 캐시 효과(Cache Effect)를 측정할 수 있도록 실험용 데이터를 미리 준비하는 과정이다.</p>
<p>그다음 <code>main()</code> 함수에서는 생성된 파일을 읽기 전용(Read Only)으로 연다. 이후 첫 번째 읽기 시간을 측정한다. 반복문 안에서 <code>read()</code>를 여러 번 수행하면서 파일 내용을 순차적으로 읽어오는데, 이 첫 번째 읽기에는 실제 디스크 접근(Disk Access)이 포함될 수 있기 때문에 상대적으로 시간이 더 걸릴 수 있다.</p>
<p>첫 번째 읽기가 끝난 뒤에는 <code>lseek(fd, 0, SEEK_SET)</code>을 사용해 파일 위치를 다시 처음으로 이동한다. 이는 동일한 파일을 처음부터 다시 읽을 수 있도록 오프셋(Offset)을 재설정하는 과정이다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/1b368054-8150-4633-9333-8afd448b51a4.png" alt="" style="display:block;margin:0 auto" />

<p>그다음 두 번째 읽기에서도 같은 방식으로 <code>read()</code>를 반복 수행하고 시간을 다시 측정한다. 이때 첫 번째 읽기에서 사용된 데이터가 운영체제의 메모리 캐시(Memory Cache)에 남아 있다면, 두 번째 읽기는 디스크에 다시 접근하지 않고 더 빠르게 처리될 수 있다.</p>
<p>마지막으로 첫 번째 읽기 시간(First Read Time)과 두 번째 읽기 시간(Second Read Time)을 출력하여 캐시 효과를 비교한다.</p>
<p>정리하면, 이 코드는 같은 파일을 반복해서 읽을 때 첫 번째 읽기보다 두 번째 읽기가 더 빨라질 수 있음을 확인하는 예제이다. 이는 운영체제가 한 번 읽은 데이터를 메모리에 캐시해두고, 이후 같은 데이터 요청이 들어오면 디스크 대신 캐시에서 빠르게 제공할 수 있기 때문이다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>이 실습 결과는 실행 환경에 따라 달라질 수 있다. 파일이 이미 운영체제 캐시에 올라와 있다면 첫 번째 읽기도 빠르게 측정될 수 있고, 메모리 상태나 백그라운드 작업에 따라 시간 차이가 작게 나타날 수도 있다.</p>
<p>또한 <code>clock()</code>은 프로그램이 사용한 CPU 시간(CPU Time)을 측정하는 함수이기 때문에, 실제 디스크 입출력 시간(Wall-Clock Time)을 정확히 비교하려면 환경에 따라 <code>clock_gettime()</code> 같은 함수를 사용하는 것이 더 적절할 수 있다.</p>
<hr />
<h3>Cache Effect Execution Result(캐시 효과 실행 결과)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/97da060b-f268-4ecb-96a6-31b3aa1b0581.png" alt="" style="display:block;margin:0 auto" />

<p>실행 결과를 보면 첫 번째 읽기(First Read)는 <code>0.004821초</code>, 두 번째 읽기(Second Read)는 <code>0.001237초</code>로 측정되었다. 즉, 두 번째 읽기가 첫 번째 읽기보다 더 빠르게 수행된 것을 확인할 수 있다.</p>
<p>그 이유는 첫 번째 읽기 과정에서 사용된 데이터가 운영체제(Operating System)의 메모리 캐시(Memory Cache)에 저장되었기 때문이다. 이후 같은 데이터를 다시 읽을 때는 디스크(Disk)에 직접 접근하지 않고, 메모리에 저장된 캐시(Cache)를 통해 더 빠르게 데이터를 가져올 수 있다.</p>
<p>따라서 운영체제는 반복적으로 사용되는 데이터에 대해 디스크 접근 횟수(Disk Access Count)를 줄이고, 전체 입출력 성능(I/O Performance)을 개선할 수 있다.</p>
<p>다만 캐시 상태(Cache State), 시스템 상황(System Condition), 백그라운드 작업(Background Process)에 따라 실행 시간은 달라질 수 있다. 따라서 중요한 것은 정확한 시간 값 자체보다, 첫 번째 읽기와 두 번째 읽기 사이에서 캐시 효과가 나타날 수 있다는 점을 이해하는 것이다.</p>
<p>이번 실습에서는 순차 접근(Sequential Access)과 랜덤 접근(Random Access)의 차이, 디스크 스케줄링(Disk Scheduling), 버퍼링(Buffering), 캐시(Cache)의 효과가 실제 시스템에서 어떻게 나타나는지 확인하는 것이 핵심이다.</p>
<h3>추가로 알아두면 좋은 점</h3>
<p>캐시 효과는 항상 같은 크기로 나타나는 것은 아니다. 이미 파일이 캐시에 올라와 있거나, 시스템 메모리 상태가 달라지면 첫 번째 읽기와 두 번째 읽기의 시간 차이가 작게 나타날 수도 있다.</p>
<hr />
<h3>실습결과</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/5e195333-0575-4c91-beee-fb812021efdd.png" alt="" style="display:block;margin:0 auto" />

<p>이 실습을 통해 순차 접근 시간이 랜덤 접근 시간보다 더 빠르게 측정되는 것을 확인하였다. 이는 순차 접근이 연속된 데이터를 차례대로 읽는 방식이기 때문에, 랜덤 접근보다 디스크 접근 비용이 상대적으로 적기 때문이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/d987689b-0dd7-41e0-b965-3ff10fbf8dbf.png" alt="" style="display:block;margin:0 auto" />

<p>디스크 스케줄링 시뮬레이션(Disk Scheduling Simulation)은 같은 디스크 요청 목록(Request Queue)을 FCFS, SSTF 같은 알고리즘으로 처리했을 때 총 헤드 이동 거리(Total Head Movement)가 얼마나 달라지는지 확인하기 위한 코드였다.</p>
<p>실습을 통해 순서대로 처리하는 FCFS 보다는 가까운 작업을 먼저 처리하는 SSTF의 이동거리가 더 짧았다. 이로 인해 디스크 스케줄링이 탐색 시간(Seek Time)을 줄이는 데 어떤 영향을 주는지 볼 수 있었다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/0caf8f1e-30b3-4b27-b802-96f6ea0b92ff.png" alt="" style="display:block;margin:0 auto" />

<p>버퍼링 코드(Buffering Code)는 <code>write()</code>만 사용할 때와 매번 <code>fsync()</code>를 사용할 때의 시간을 비교해서, 버퍼링(Buffering)이 쓰기 성능(Write Performance)에 어떤 영향을 주는지 확인하는 코드였다. 즉, 디스크 성능 전체라기보다는 디스크 쓰기 성능과 안정성 사이의 트레이드오프(Trade-off)를 확인하는 실습이었다.</p>
<p>여러 쓰기 요청을 메모리에 모아두었다가 나중에 처리할 수 있기 때문에 버퍼링을 사용한 코드가 더 빨랐다. fsync 사용이 더 느린이유는 매번 실제 디스크 기록을 강제하기 때문에 대기 시간이 증가하기 때문이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/b405bcef-ba93-4220-967d-b6a76ae90eb8.png" alt="" style="display:block;margin:0 auto" />

<p>캐시 코드(Cache Code)는 같은 파일을 두 번 읽었을 때 첫 번째 읽기와 두 번째 읽기 시간이 어떻게 달라지는지 비교해서, 운영체제 캐시(OS Cache)가 읽기 성능(Read Performance)에 어떤 영향을 주는지 확인하는 코드였다. 즉, 반복 읽기에서 디스크 접근 횟수(Disk Access Count)를 줄여 성능이 좋아질 수 있음을 확인하는 실습이다.</p>
<p>이 실습을 통해 동일한 파일을 반복해서 읽을 경우, 두 번째 읽기가 더 빠르게 수행될 수 있음을 확인하였다. 이는 운영체제가 첫 번째 읽기 데이터를 메모리 캐시에 저장해두고, 이후 같은 데이터를 다시 요청할 때 디스크 접근을 줄일 수 있기 때문이다.</p>
<hr />
]]></content:encoded></item><item><title><![CDATA[Software Testing End-of-Semester Review Notes: Quality, V-Model, Test Process, Test Techniques, and AI Testing]]></title><description><![CDATA[Software Testing Course Review
이번 강의는 새로운 소프트웨어 테스트(Software Testing) 내용을 배우는 시간이 아니라, 한 학기 동안 학습한 핵심 내용을 키워드 중심으로 정리하고 점검하는 시간이다. 지난 시간에는 AI 소프트웨어 테스트(AI Software Testing)의 정의와 필요성을 살펴보았다. 특히 일반 소프트웨]]></description><link>https://heesu.tech/software-testing-end-of-semester-review-notes-quality-v-model-test-process-test-techniques-and-ai-testing</link><guid isPermaLink="true">https://heesu.tech/software-testing-end-of-semester-review-notes-quality-v-model-test-process-test-techniques-and-ai-testing</guid><dc:creator><![CDATA[Heesu Noh]]></dc:creator><pubDate>Tue, 09 Jun 2026 01:16:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/00ddace1-59a3-4480-aae0-3e4c6864a1a3.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Software Testing Course Review</h2>
<p>이번 강의는 새로운 소프트웨어 테스트(Software Testing) 내용을 배우는 시간이 아니라, 한 학기 동안 학습한 핵심 내용을 키워드 중심으로 정리하고 점검하는 시간이다. 지난 시간에는 AI 소프트웨어 테스트(AI Software Testing)의 정의와 필요성을 살펴보았다. 특히 일반 소프트웨어와 달리 AI 소프트웨어(AI Software)는 데이터(Data)와 모델(Model)에 크게 의존하기 때문에, AI 소프트웨어의 특성을 반영한 별도의 테스트 기법(Test Techniques)이 필요하다는 점을 학습했다.</p>
<p>AI 소프트웨어 테스트에서는 기존 소프트웨어 테스트와 마찬가지로 <code>블랙박스 테스트(Black-box Testing)</code>와 <code>화이트박스 테스트(White-box Testing)</code>의 개념이 적용된다. 블랙박스 테스트는 AI 모델 내부 구조를 직접 보지 않고 <code>입력값(Input)</code>과 <code>출력값(Output)</code>의 관계를 중심으로 검증하는 방식이다. 반면 화이트박스 테스트는 AI 모델의 내부 구조, 특히 <code>신경망(Neural Network)</code>이나 <code>뉴런 커버리지(Neuron Coverage)</code>와 같은 요소를 고려하여 테스트하는 방식이다.</p>
<p>또한 AI 소프트웨어를 안전하게 테스트하기 위해 <code>가상 기반 테스트 환경(Virtual-based Testing Environment)</code>과 <code>디지털 트윈(Digital Twin)</code>을 활용하는 방법도 학습했다. 현실 환경에서는 위험한 상황이나 예외적인 상황을 직접 재현하기 어렵기 때문에, <code>시뮬레이션 환경(Simulation Environment)</code>을 통해 다양한 시나리오(Scenario)를 안전하게 반복 테스트할 수 있다. 특히 자율주행(Autonomous Driving), 드론(Drone), 물류 시스템(Logistics System)처럼 실제 테스트 비용과 위험이 큰 분야에서 이러한 방식이 중요하게 사용된다.</p>
<p>이번 정리 강의의 목표는 전체 수업의 목표와도 연결된다.</p>
<p>첫 번째 목표는 소프트웨어 품질 확보(Software Quality Assurance)의 중요성을 인식하는 것이다. 소프트웨어 테스트는 단순히 오류를 찾는 활동이 아니라, 소프트웨어가 요구사항(Requirements)을 충족하고 안정적으로 동작하는지 확인함으로써 품질을 확보하는 중요한 활동이다. 따라서 테스트는 개발의 마지막 단계에서만 수행되는 부가적인 작업이 아니라, 소프트웨어 품질을 보장하기 위한 핵심 프로세스라고 볼 수 있다.</p>
<p>두 번째 목표는 소프트웨어 테스트 프로세스(Software Testing Process)를 설명할 수 있는 것이다. 소프트웨어 테스트는 단순히 테스트를 실행하는 것만을 의미하지 않는다. 테스트 계획(Test Planning), 테스트 설계(Test Design), 테스트 수행(Test Execution), 테스트 평가(Test Evaluation), 테스트 개선(Test Improvement)과 같은 단계가 포함된다. 이러한 과정은 PDCA(Plan-Do-Check-Act) 관점에서도 이해할 수 있다. 즉, 테스트를 계획하고 실행한 뒤 결과를 확인하고, 문제점을 개선하는 반복적인 품질 관리 활동이다.</p>
<p>세 번째 목표는 다양한 소프트웨어 테스트 기법(Software Test Techniques)을 설명할 수 있는 것이다. 수업에서는 블랙박스 테스트와 화이트박스 테스트를 중심으로 여러 테스트 방법을 학습했다. 블랙박스 테스트에는 동등 분할 테스트(Equivalence Partitioning), 경계값 분석(Boundary Value Analysis), 조합 테스트(Combinatorial Testing), 변성 테스트(Metamorphic Testing), 탐색적 테스트(Exploratory Testing) 등이 포함될 수 있다. 화이트박스 테스트에서는 코드 구조나 내부 로직을 기반으로 문장 커버리지(Statement Coverage), 분기 커버리지(Branch Coverage), 그리고 AI 모델의 경우 뉴런 커버리지(Neuron Coverage)와 같은 개념을 활용할 수 있다.</p>
<p>정리하면, 이번 강의는 한 학기 동안 배운 소프트웨어 품질(Software Quality), V-모델(V-Model), 테스트 프로세스(Test Process), 테스트 기법(Test Techniques), AI 소프트웨어 테스트(AI Software Testing)를 전체적으로 복습하는 시간이다. 이 내용을 통해 소프트웨어 테스트가 품질 확보를 위한 핵심 활동이며, 테스트는 계획부터 설계, 수행, 평가, 개선까지 이어지는 체계적인 프로세스라는 점을 다시 확인할 수 있게 될 것이다.</p>
<hr />
<h3>Software Quality and V-Model</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f2cffe70-c2bc-4882-bd40-134a6fa45396.png" alt="" style="display:block;margin:0 auto" />

<p>소프트웨어 품질(Software Quality)과 V-모델(V-Model)에서 가장 중요한 출발점은 소프트웨어(Software)의 정의를 이해하는 것이다. 소프트웨어는 단순히 프로그램 코드만을 의미하는 것이 아니라, 사용자의 요구사항(Requirements)을 만족시키기 위해 만들어진 프로그램, 데이터, 문서, 절차 등을 포함하는 개념이다. 이러한 소프트웨어는 하드웨어처럼 눈에 보이는 물리적 제품은 아니지만, 자동차, 항공기, 의료기기, 금융 시스템 등 다양한 실제 제품과 서비스의 동작에 큰 영향을 준다.</p>
<p>소프트웨어는 변경이 쉽고 복잡도가 높으며, 개발 과정에서 사람이 직접 논리와 구조를 설계한다는 특징을 가진다. 이러한 특징 때문에 요구사항이 잘못 정의되거나, 설계가 부정확하거나, 구현 과정에서 실수가 발생하면 소프트웨어 결함(Software Defect)이 생길 수 있다. 결함은 단순한 프로그램 오류로 끝나는 것이 아니라, 실제 제품의 오작동이나 서비스 장애로 이어질 수 있다. 특히 자동차나 항공기처럼 안전이 중요한 분야에서는 소프트웨어 결함이 큰 사고로 연결될 수 있기 때문에 품질 확보가 매우 중요하다.</p>
<p>이러한 문제를 줄이기 위해 수업에서는 제품 품질(Product Quality)뿐만 아니라 프로세스 품질(Process Quality)의 중요성을 강조했다. 제품 품질은 최종적으로 만들어진 소프트웨어가 요구사항을 만족하고 안정적으로 동작하는지를 의미한다. 반면 프로세스 품질은 소프트웨어를 개발하고 유지보수하는 과정이 체계적이고 적절하게 수행되었는지를 의미한다. 좋은 개발 프로세스를 갖추면 결함을 줄이고, 결과적으로 더 나은 제품 품질을 확보할 수 있다.</p>
<p>이때 중요한 개발 및 테스트 관점으로 V-모델(V-Model)이 등장한다. V-모델은 요구사항 분석(Requirements Analysis), 설계(Design), 구현(Implementation), 테스트(Test)의 관계를 체계적으로 보여주는 모델이다. 개발 단계에서 정의한 요구사항과 설계 내용은 이후 테스트 단계에서 검증되어야 한다. 즉, 요구사항을 기준으로 인수 테스트(Acceptance Testing)를 수행하고, 설계를 기준으로 시스템 테스트(System Testing), 통합 테스트(Integration Testing), 단위 테스트(Unit Testing)를 수행하는 방식으로 연결된다.</p>
<p>정리하면, 소프트웨어 품질과 V-모델의 핵심 흐름은 하나의 시나리오로 이해할 수 있다. 먼저 소프트웨어의 정의와 특징을 이해하고, 이러한 특징 때문에 품질 문제가 발생할 수 있음을 파악한다. 품질 문제는 소프트웨어 결함으로 이어지고, 결함은 실제 제품이나 서비스에 영향을 줄 수 있다. 따라서 소프트웨어 품질을 확보하기 위해 제품 품질과 프로세스 품질을 함께 관리해야 하며, 이를 체계적으로 수행하기 위한 방법으로 V-모델을 적용할 수 있다.</p>
<hr />
<h3>Software의 정의</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f4c976fc-0adb-45fa-a544-d6ba7f045a7e.png" alt="" style="display:block;margin:0 auto" />

<p>소프트웨어(Software)는 단순히 하드웨어(Hardware)를 제어하는 프로그래밍 명령어(Programming Instructions)나 소스 코드(Source Code)의 집합만을 의미하지 않는다. 물론 소스 프로그램(Source Program)이나 실행 가능한 프로그램(Executable Program)은 소프트웨어의 중요한 구성 요소이다. 하지만 소프트웨어를 더 넓게 보면, 개발 과정에서 만들어지는 다양한 산출물(Artifacts)까지 포함하는 개념이다.</p>
<p>소프트웨어는 고객 요구사항 분석(Requirements Analysis), 설계(Design), 구현(Implementation), 테스트(Testing), 배포(Deployment)와 같은 일련의 소프트웨어 개발 프로세스(Software Development Process)를 통해 만들어진다. 이 과정에서 단순히 코드만 생성되는 것이 아니라, 요구사항 명세서(Requirements Specification), 아키텍처 설계서(Architecture Design Document), 상세 설계서(Detailed Design Document), 테스트 결과서(Test Result Report), 배포 문서(Deployment Document), 데이터(Data) 등 다양한 결과물이 함께 만들어진다.</p>
<p>따라서 소프트웨어는 프로그램 코드만을 가리키는 좁은 개념이 아니라, 프로그램을 포함하여 소프트웨어 개발과 유지보수 과정에서 생성되는 모든 관련 산출물의 집합이라고 이해할 수 있다. 즉, 소프트웨어는 소스 코드(Source Code), 문서(Document), 데이터(Data), 테스트 결과(Test Results), 프로세스(Process)와 같은 요소들을 포함하는 종합적인 개념이다.</p>
<p>이러한 정의가 중요한 이유는 소프트웨어 품질(Software Quality)을 평가할 때 코드만 보면 충분하지 않기 때문이다. 요구사항이 잘못 작성되었거나, 설계 문서가 부정확하거나, 테스트 결과가 제대로 관리되지 않으면 최종 프로그램이 정상적으로 동작하더라도 품질 문제가 발생할 수 있다. 따라서 소프트웨어를 이해할 때는 구현 단계에서 나온 코드뿐만 아니라, 개발 전 과정에서 만들어지는 산출물까지 함께 고려해야 한다.</p>
<p>정리하면, 소프트웨어는 하드웨어를 제어하는 명령어 집합만이 아니라, 소스 프로그램을 포함하여 요구사항 분석, 설계, 구현, 테스트, 배포 과정에서 생성되는 문서와 데이터, 테스트 결과 등 모든 산출물을 포함하는 개념이 된다.</p>
<hr />
<h3>Software의 특징</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/af3dba16-7f53-4f55-9f1f-680418897b7b.png" alt="" style="display:block;margin:0 auto" />

<p>소프트웨어(Software)는 대표적으로 네 가지 특징을 가진다. 이러한 특징들은 소프트웨어 품질 문제(Software Quality Issue)와 결함(Defect)이 발생하는 원인이 될 수 있으며, 최종적으로는 소프트웨어가 탑재된 자동차, 항공기, 의료기기와 같은 실제 제품에도 영향을 줄 수 있다.</p>
<p>첫 번째 특징은 <code>비가시성(Invisibility)</code>이다. 소프트웨어는 하드웨어처럼 눈으로 구조를 직접 확인하기 어렵다. 작은 프로그램은 코드의 흐름을 비교적 쉽게 파악할 수 있지만, 수천 줄, 수만 줄, 수십만 줄 이상의 대형 소프트웨어가 되면 내부 구성 요소와 전체 구조를 한눈에 이해하기 어렵다. 이 때문에 소프트웨어의 구조적 문제나 결함을 발견하기가 쉽지 않다.</p>
<p>두 번째 특징은 <code>비선형적이고 복잡한 구조(Nonlinear and Complex Structure)</code>이다. 소프트웨어는 여러 모듈(Module)로 구성되며, 각 모듈은 독립적으로 존재하면서도 서로 데이터를 주고받고 기능적으로 연결된다. 이러한 관계가 많아질수록 소프트웨어는 단순한 선형 구조로 이해하기 어려워지고, 하나의 변경이 다른 부분에 예상하지 못한 영향을 줄 수 있다.</p>
<p>세 번째 특징은 <code>닳아 없어지지 않지만 계속 변경된다는 점(Does Not Wear Out but Changes)</code>이다. 하드웨어(Hardware)는 시간이 지나면 물리적으로 마모되거나 고장날 수 있지만, 소프트웨어 자체는 물리적으로 닳지 않는다. 대신 사용자의 요구 변화, 운영 환경 변화, 기능 추가, 결함 수정 등에 따라 계속 변경된다. 이러한 변경은 소프트웨어가 탑재된 제품이나 서비스가 사용되는 동안 지속될 수 있으며, 변경 과정에서 새로운 결함이 발생할 가능성도 있다.</p>
<p>네 번째 특징은 <code>사람 중심의 작업(Human-centered Work)</code>이라는 점이다. 소프트웨어 개발은 요구사항 분석(Requirements Analysis), 설계(Design), 구현(Implementation), 테스트(Testing) 등 많은 단계에서 사람의 판단과 작업에 의존한다. 따라서 개발 과정에는 항상 휴먼 에러(Human Error)가 포함될 가능성이 있다. 이러한 오류가 소프트웨어 결함으로 이어지고, 결함이 실제 시스템의 고장(Failure)이나 사고(Accident)로 연결될 수 있다.</p>
<p>결국 소프트웨어의 비가시성, 복잡성, 지속적인 변경, 사람 중심의 개발 과정은 모두 소프트웨어 품질을 저해할 수 있는 요인이 된다. 그래서 소프트웨어 개발이 포함된 IT 프로젝트(IT Project)의 성공률을 높이기 위해서는 단순히 프로그래밍(Programming)만 잘하는 것이 아니라, 요구사항 분석, 설계, 구현, 테스트를 체계적으로 수행하는 프로세스(Process)가 필요하다. 즉, 소프트웨어 품질을 확보하기 위해서는 개발 프로세스를 체계적으로 적용하고, 각 단계에서 품질을 점검하는 노력이 중요하다.</p>
<hr />
<h3>Impact of Software Defects</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/36096663-9831-41e7-9a86-195540eeb324.png" alt="" style="display:block;margin:0 auto" />

<p>소프트웨어(Software)는 사람이 직접 분석하고 설계하며 구현하는 결과물이기 때문에, 개발 과정에서 휴먼 에러(Human Error)가 발생할 수 있다. 요구사항을 잘못 이해하거나, 설계 단계에서 논리적 오류가 생기거나, 구현 과정에서 잘못된 코드를 작성하는 것이 모두 휴먼 에러의 예가 될 수 있다.</p>
<p>이러한 휴먼 에러는 단순한 실수로 끝나지 않고, 소프트웨어 내부의 결함(Fault)으로 남을 수 있다. 이후 결함이 실제 실행 과정에서 드러나면 고장(Failure)으로 이어질 수 있다. 즉, 소프트웨어 문제는 <code>error → fault → failure</code>의 흐름으로 전파될 수 있다.</p>
<p>여기서 에러(Error)는 사람이 저지른 실수나 잘못된 판단을 의미하고, 결함(Fault)은 그 실수가 소프트웨어 산출물 안에 남은 상태를 의미한다. 고장(Failure)은 소프트웨어가 실행되었을 때 요구된 기능을 제대로 수행하지 못하는 실제 현상을 의미한다. 작은 실수나 결함이라도 시스템 안에서 전파되면 예상보다 큰 문제로 발전할 수 있다.</p>
<p>특히 소프트웨어가 자동차, 항공기, 의료기기, 금융 시스템, 원자력 시스템과 같은 안전 중요 시스템(Safety-critical System)에 사용될 경우, 결함의 영향은 매우 심각해질 수 있다. 단순한 프로그램 오류가 사람의 생명, 경제적 손실, 환경 피해와 연결될 수 있기 때문이다. 따라서 소프트웨어 결함은 단순히 개발자의 실수나 프로그램 오류로만 볼 것이 아니라, 실제 사회적 피해로 이어질 수 있는 중요한 품질 문제로 이해해야 한다.</p>
<p>결국 소프트웨어 개발에서는 작은 에러와 결함을 가볍게 넘기지 않는 태도가 필요하다. 결함이 상위 단계로 전파되어 큰 사고를 일으키지 않도록, 요구사항 분석(Requirements Analysis), 설계(Design), 구현(Implementation), 테스트(Testing), 유지보수(Maintenance) 전 과정에서 체계적인 프로세스(Process)를 적용해야 한다. 철저한 소프트웨어 개발 프로세스와 테스트 활동은 결함을 조기에 발견하고, 고장과 사고로 이어지는 위험을 줄이기 위한 핵심 방법이라고 할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/74dfe882-984d-47d7-b1d7-56345a1d9ee9.png" alt="" style="display:block;margin:0 auto" />

<h3>Process Quality &amp; Product Quality</h3>
<p>소프트웨어 품질(Software Quality)은 크게 <code>프로세스 품질(Process Quality)</code>과 <code>제품 품질(Product Quality)</code>로 나누어 볼 수 있다. 최종적으로 우리가 확보하고자 하는 것은 사용자가 실제로 사용하는 소프트웨어 제품(Software Product)이나 시스템(System)의 품질이다. 즉, 제품이 요구사항(Requirements)을 만족하고, 안정적으로 동작하며, 사용 목적에 맞게 기능을 수행해야 한다.</p>
<p>하지만 제품 품질을 높이기 위해서는 단순히 프로그램을 잘 구현하는 것만으로는 부족하다. 좋은 소프트웨어를 만들기 위해서는 소프트웨어를 개발하는 과정, 즉 프로세스(Process)가 체계적으로 관리되어야 한다. 요구사항 분석(Requirements Analysis), 설계(Design), 구현(Implementation), 테스트(Testing), 배포(Deployment), 유지보수(Maintenance) 과정이 명확하게 정의되고, 그 절차에 따라 수행되어야 최종 제품의 품질도 높아질 수 있다.</p>
<p>이때 프로세스 품질은 <code>PDCA Cycle</code>을 기반으로 이해할 수 있다. 먼저 Plan 단계에서는 프로세스의 목표를 달성하기 위한 계획과 절차가 정의되어야 한다. Do 단계에서는 정의된 프로세스에 따라 실제 개발과 테스트 활동을 수행한다. Check 단계에서는 계획대로 잘 진행되고 있는지 확인하고 통제(Control)한다. 마지막으로 Act 단계에서는 문제가 있거나 부족한 부분을 지속적으로 개선(Improvement)한다.</p>
<p>즉, 소프트웨어 품질 확보는 한 번의 테스트 수행으로 끝나는 것이 아니라, 계획하고 수행하고 점검하고 개선하는 반복적인 활동이다. 테스트 활동(Test Activities) 역시 PDCA 관점에서 이루어져야 한다. 테스트를 계획(Test Planning)하고, 테스트 케이스(Test Case)를 설계(Test Design)하고, 테스트를 수행(Test Execution)한 뒤, 결과를 평가(Test Evaluation)하고 개선(Test Improvement)하는 과정이 필요하다.</p>
<p>결국 프로세스 품질과 제품 품질은 서로 연결되어 있다. 체계적인 프로세스가 잘 운영되면 결함(Defect)을 조기에 발견하고 줄일 수 있으며, 이는 최종 제품의 품질 향상으로 이어진다. 따라서 소프트웨어 테스트(Software Testing)는 단순히 오류를 찾는 활동이 아니라, PDCA 기반의 품질 관리 활동(Quality Management Activity)으로 이해해야 한다.</p>
<hr />
<h3>V-Model 생명주기 프로세스</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/8982368a-4e07-4f7d-a14e-4886cea5a8e3.png" alt="" style="display:block;margin:0 auto" />

<p>V-Model은 소프트웨어 개발 활동(Development Activities)과 테스트 활동(Test Activities)을 서로 대응시켜 품질을 확보하는 생명주기 모델(Life Cycle Model)이다. V-Model의 왼쪽은 요구사항 분석(Requirements Analysis), 아키텍처 설계(Architecture Design), 상세 설계(Detailed Design), 구현(Implementation)과 같은 개발 영역으로 구성된다. 오른쪽은 단위 테스트(Unit Testing), 통합 테스트(Integration Testing), 시스템 테스트(System Testing), 인수 테스트(Acceptance Testing)와 같은 테스트 영역으로 구성된다.</p>
<p>V-Model의 핵심은 개발 단계와 테스트 단계가 서로 연결되어 있다는 점이다. 고객 또는 사용자 요구사항(Customer/User Requirements)은 인수 테스트(Acceptance Testing)와 대응된다. 사용자가 요구한 기능과 조건을 최종 시스템이 만족하는지 확인하는 단계이기 때문이다. 요구사항 분석(Requirements Analysis)은 시스템 테스트(System Testing)와 대응된다. 시스템 전체가 분석된 요구사항을 충족하는지 검증하는 단계이다.</p>
<p>아키텍처 설계(Architecture Design)는 통합 테스트(Integration Testing)와 대응된다. 아키텍처 설계에서는 시스템을 구성하는 주요 컴포넌트(Component)와 모듈(Module) 간의 관계를 정의하므로, 통합 테스트에서는 이 구성요소들이 서로 올바르게 연동되는지 확인한다. 상세 설계(Detailed Design)는 단위 테스트(Unit Testing)와 대응된다. 상세 설계에서 정의한 개별 모듈이나 함수가 구현 후 올바르게 동작하는지 확인하는 단계가 단위 테스트이다.</p>
<p>V-Model에서 중요한 것은 단순히 개발이 끝난 뒤 테스트를 수행한다는 의미가 아니다. 왼쪽 개발 단계에서 만들어지는 산출물(Artifact)의 품질이 오른쪽 테스트 단계의 품질에 직접적인 영향을 준다는 점이다. 예를 들어 요구사항 분석서(Requirements Specification)의 품질이 낮거나 모호하면, 이를 기반으로 작성되는 시스템 테스트 케이스(System Test Case)의 품질도 낮아질 수밖에 없다. 따라서 테스트 품질을 높이기 위해서는 개발 산출물의 품질도 함께 높여야 한다.</p>
<p>이를 위해 V-Model에서는 검증(Verification)과 확인(Validation)을 강조한다. 검증은 개발 산출물이 정해진 기준과 명세에 맞게 만들어졌는지 확인하는 활동이고, 확인은 최종 제품이 사용자 요구와 실제 사용 목적을 만족하는지 확인하는 활동이다. 즉, V-Model은 개발 과정에서 만들어진 산출물을 계속 점검하고, 최종적으로 소프트웨어가 올바르게 만들어졌는지 확인하는 구조를 가진다.</p>
<p>또한 품질 확보를 위해서는 개발자가 자신의 산출물만 스스로 확인하는 것보다, 제3자의 관점에서 리뷰(Review)하고 점검하는 것이 중요하다. 예를 들어 요구사항 분석 단계에서 나온 분석서는 시스템 테스트 담당자나 품질 담당자가 함께 검토할 수 있다. 이렇게 테스트 담당자가 개발 초기 단계부터 참여하면, 요구사항이 테스트 가능한 형태로 작성되었는지 확인할 수 있고, 이후 테스트 케이스를 더 정확하게 설계할 수 있다.</p>
<p>결국 V-Model은 개발 영역과 테스트 영역을 분리해서 보는 모델이 아니라, 서로 병행하며 품질을 높여가는 모델이다. 왼쪽 개발 산출물의 품질이 좋아야 오른쪽 테스트 산출물의 품질도 좋아지고, 테스트 활동 역시 개발 초기 단계부터 함께 고려되어야 한다. 궁극적으로 V-Model의 목적은 개발과 테스트를 체계적으로 연결하여 소프트웨어 품질(Software Quality)을 확보하는 것이라고 할 수 있다.</p>
<hr />
<h3>Software Testing Process</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/66a44eed-2098-4465-9e4d-94f1b8c7a84b.png" alt="" style="display:block;margin:0 auto" />

<p>두 번째 챕터에서는 소프트웨어 테스트 프로세스(Software Testing Process)에 대해 학습한다. 이 챕터의 핵심은 테스트를 단순히 수행하는 활동으로만 보는 것이 아니라, 체계적인 프로세스(Process)로 이해하는 것이다.</p>
<p>먼저 중요한 키워드는 테스트의 정의(Definition of Testing)이다. 테스트는 소프트웨어가 요구사항(Requirements)을 만족하는지 확인하고, 결함(Defect)을 발견하기 위한 활동이다.</p>
<p>테스트를 체계적으로 수행하기 위해 PDCA 관점의 테스트 프로세스(PDCA-based Test Process)를 살펴보았다. 테스트는 계획(Plan), 수행(Do), 확인(Check), 개선(Act)의 흐름에 따라 반복적으로 관리되어야 한다.</p>
<p>또한 ISO/IEC/IEEE 29119의 테스트 프로세스(Test Process)도 중요한 내용으로 등장한다. 이는 소프트웨어 테스트를 체계적으로 수행하기 위한 표준 프로세스이다.</p>
<p>테스트를 제대로 수행하기 위해서는 먼저 테스트 계획(Test Planning)이 필요하다. 테스트 계획을 세울 때는 어떤 부분을 우선적으로 테스트할지 판단해야 하며, 이를 위해 위험 기반 테스트(Risk-based Testing)가 강조된다. 위험이 큰 부분을 먼저 고려하여 테스트 계획을 수립하는 것이 중요하다.</p>
<p>마지막으로 테스트 프로세스의 자동화(Test Process Automation)도 주요 키워드이다. 테스트 자동화는 테스트 활동을 더 효율적으로 수행하기 위한 방법이다.</p>
<p>정리하면, 이 챕터의 주요 키워드는 테스트의 정의(Definition of Testing), PDCA 기반 테스트 프로세스(PDCA-based Test Process), ISO/IEC/IEEE 29119 테스트 프로세스(Test Process), 테스트 계획(Test Planning), 위험 기반 테스트(Risk-based Testing), 테스트 프로세스 자동화(Test Process Automation)이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/3514f7ec-ee6a-400a-a8ff-cd380917efc4.png" alt="" style="display:block;margin:0 auto" />

<h3>Definition of Testing</h3>
<p>테스트(Testing)의 근본적인 목적은 소프트웨어에 결함이 없음을 증명하는 것이 아니라, 소프트웨어 안에 존재하는 결함(Defect)을 발견하는 것이다. 즉, 테스트를 잘 수행했다는 것은 단순히 프로그램이 실행되는 것을 확인했다는 뜻이 아니라, 숨어 있는 결함을 효과적으로 찾아냈다는 의미이다.</p>
<p>소프트웨어는 개발자가 직접 만들기 때문에, 만든 사람이 스스로 테스트할 경우 문제를 놓칠 가능성이 있다. 따라서 품질을 더 객관적으로 확인하기 위해서는 제3자 관점(Third-party Perspective)에서 테스트하는 것이 중요하다. 조직 내부에서도 개발자와는 다른 관점의 테스터(Tester)가 소프트웨어를 평가하면, 사용자가 실제로 겪을 수 있는 문제를 더 잘 발견할 수 있다.</p>
<p>또한 테스터는 소프트웨어를 실제 사용하는 사용자(User)의 관점도 고려해야 한다. 소프트웨어가 개발자의 PC에서는 잘 동작하더라도, 실제 사용자 환경(User Environment)에서도 항상 잘 동작한다고 보장할 수는 없다. 운영체제(Operating System), 하드웨어(Hardware), 네트워크(Network), 사용 방식 등 다양한 환경에 따라 문제가 발생할 수 있기 때문이다.</p>
<p>따라서 테스트는 다양한 사용자 환경을 구성하고, 체계적인 방법으로 수행되어야 한다. 결론적으로 테스트의 핵심 목적은 결함을 발견하는 것이며, 이를 위해 제3자 관점에서 사용자의 실제 환경을 고려하여 소프트웨어를 평가하는 것이 중요하다.</p>
<hr />
<h3>PDCA-based Test Process</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/21e02938-4a33-4ab0-968a-a53a192a932e.png" alt="" style="display:block;margin:0 auto" />

<p>PDCA 관점의 테스트 프로세스(PDCA-based Test Process)는 테스트를 단순히 입력값을 넣고, 실행하고, 결과를 확인하는 활동으로만 보지 않는다. 테스트를 제대로 수행하기 위해서는 테스트 수행 이전에 <code>Plan, Do, Check, Act</code>의 과정이 체계적으로 진행되어야 한다.</p>
<p>이 개념은 V-Model과도 연결된다. V-Model의 왼쪽 영역에서 요구사항 분석(Requirements Analysis), 설계(Design)와 같은 개발 활동이 진행될 때, 오른쪽 테스트 활동도 함께 계획되고 설계되어야 한다. 즉, 개발이 모두 끝난 뒤에 테스트를 생각하는 것이 아니라, 개발 초기 단계부터 어떤 테스트를 수행할지 함께 고려해야 한다. 이렇게 개발 담당자와 테스트 담당자가 함께 소통하면 산출물(Artifacts)의 품질도 자연스럽게 높아지게 된다.</p>
<p><code>테스트 계획 단계(Test Planning)</code>에서는 전체 테스트 계획을 수립한다. 이때 어떤 기능(Feature)을 테스트할지, 테스트 범위는 어디까지인지, 어떤 방식으로 테스트할지 등을 정한다. 테스트 설계 단계(Test Design)에서는 테스트 케이스(Test Case)를 개발하고, 테스트 절차(Test Procedure)를 정의하며, 테스트 환경(Test Environment)을 준비한다. 이러한 활동들은 실제 테스트를 수행하기 전에 먼저 진행되어야 한다.</p>
<p>그 다음 실제 <code>테스트 수행(Test Execution)</code>이 이루어진다. 테스트는 사람이 직접 수행할 수도 있고, 자동화 도구(Test Automation Tool)를 활용하여 수행할 수도 있다. 테스트 수행 후에는 결과를 확인하고 평가해야 한다. 발견된 문제가 실제 결함(Defect)인지, 테스트 프로세스(Process)의 문제인지, 또는 자동화 도구가 잘못 판단한 것인지 분석해야 한다.</p>
<p>이후에는 평가 결과를 바탕으로 <code>개선(Improvement)</code>을 수행한다. 테스트 과정에서 부족했던 부분이나 잘못된 절차가 있다면 수정하고, 더 나은 테스트 프로세스로 개선해 나가야 한다. 즉, 테스트는 한 번 수행하고 끝나는 것이 아니라, Plan, Do, Check, Act의 흐름에 따라 반복적으로 관리되어야 한다.</p>
<p>정리하면, PDCA 기반 테스트 프로세스는 테스트 활동을 체계적으로 계획하고, 수행하고, 평가하고, 개선하는 과정이다. 이를 통해 테스트 프로세스의 품질(Process Quality)을 높일 수 있고, 결과적으로 소프트웨어 제품의 품질(Product Quality)도 향상될 수 있게 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/c8aa12cb-9be1-4d40-9da1-f8644c8b5a8c.png" alt="" style="display:block;margin:0 auto" />

<h3>ISO/IEC/IEEE 29119 Test Process</h3>
<p><code>ISO/IEC/IEEE 29119</code>는 소프트웨어 테스트 프로세스(Software Test Process)를 국제 표준으로 정의한 문서이다. 이 표준은 수업에서 여러 번 다루어진 중요한 내용이며, 테스트를 체계적으로 수행하기 위한 기준을 제시한다.</p>
<p>ISO/IEC/IEEE 29119의 테스트 프로세스는 크게 세 가지로 나눌 수 있다. 첫 번째는 조직 차원의 테스트 프로세스(Organizational Test Process), 두 번째는 테스트 관리 프로세스(Test Management Process), 세 번째는 동적 테스트 프로세스(Dynamic Test Process)이다.</p>
<p>조직 차원의 테스트 프로세스는 조직 전체에서 테스트를 어떻게 수행할 것인지에 대한 정책(Test Policy)과 전략(Test Strategy)을 수립하는 단계이다. 테스트가 개인이나 프로젝트마다 제각각 수행되는 것이 아니라, 조직 차원에서 일관된 기준과 방향을 가지고 수행될 수 있도록 기반을 마련하는 역할을 한다.</p>
<p>테스트 관리 프로세스는 실제 테스트가 제대로 수행될 수 있도록 계획하고 관리하는 과정이다. 테스트 계획(Test Planning), 테스트 모니터링(Test Monitoring), 테스트 통제(Test Control), 테스트 완료 활동(Test Completion) 등이 여기에 포함될 수 있다. 즉, 동적 테스트를 수행하기 전에 무엇을 어떻게 테스트할지 관리하는 역할을 한다.</p>
<p>동적 테스트 프로세스는 실제로 소프트웨어를 실행하여 테스트하는 과정이다. 테스트 케이스(Test Case)를 설계하고, 테스트 절차(Test Procedure)를 만들고, 테스트 환경(Test Environment)을 준비한 뒤 실제 테스트를 수행한다. 이후 나온 결과를 확인하고 결함(Defect)을 보고하는 활동도 포함된다.</p>
<p>이 세 가지 프로세스는 서로 분리되어 있는 것이 아니라 연결되어 있다. 조직 차원의 테스트 프로세스에서 수립된 정책과 기준은 테스트 관리 프로세스로 전달되고, 테스트 관리 프로세스는 다시 동적 테스트 프로세스가 제대로 수행될 수 있도록 방향을 제공한다. 반대로 동적 테스트 프로세스에서 나온 결과는 테스트 관리 프로세스로 전달되어 평가되고, 다시 조직 차원의 테스트 프로세스 개선에도 활용된다.</p>
<p>즉, ISO/IEC/IEEE 29119 테스트 프로세스에는 PDCA(Plan-Do-Check-Act)의 개념이 반영되어 있다. 테스트를 계획하고 수행한 뒤, 결과를 평가하고 개선하는 흐름이 조직 차원, 관리 차원, 실제 테스트 수행 차원에서 반복적으로 이루어진다. 이를 통해 테스트 프로세스의 품질을 높이고, 궁극적으로 소프트웨어 제품 품질(Product Quality)을 향상시키는 것이 목적이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/7eed56b9-1ac2-4ebe-a5ed-4e309b5f12a8.png" alt="" style="display:block;margin:0 auto" />

<h3>Flow of ISO/IEC/IEEE 29119 Test Process</h3>
<p>ISO/IEC/IEEE 29119 테스트 프로세스(Test Process)는 테스트를 조직 관점, 프로젝트 관점, 실제 수행 관점으로 나누어 체계적으로 관리하는 구조이다. 이 프로세스 안에는 <code>PDCA(Plan-Do-Check-Act)</code>의 흐름이 반영되어 있다.</p>
<p>먼저 <code>조직 차원의 테스트 프로세스(Organizational Test Process)</code>에서는 조직 전체의 테스트 방향을 정한다. 테스트 조직(Test Organization)을 어떻게 구성할 것인지, 테스트 환경(Test Environment)을 어떻게 마련할 것인지, 어떤 기준과 원칙으로 테스트를 수행할 것인지와 같은 <code>테스트 정책(Test Policy)</code>을 수립한다. 이를 바탕으로 조직 차원의 <code>테스트 전략(Test Strategy)</code>이 만들어지고, 이 전략은 프로젝트 차원의 테스트 관리 프로세스(Test Management Process)로 전달된다.</p>
<p>테스트 관리 프로세스에서는 <code>프로젝트 차원에서 테스트 계획(Test Planning)</code>이 시작된다. 이는 PDCA의 Plan에 해당한다. 이 단계에서는 테스트 대상(Test Object), 테스트 목표(Test Objective), 테스트 범위(Test Scope) 등을 정한다. 즉, 무엇을 테스트할 것인지, 어떤 목적을 가지고 테스트할 것인지 계획하는 단계이다.</p>
<p>이후 테스트 계획은 <code>동적 테스트 프로세스(Dynamic Test Process)</code>로 전달된다. 동적 테스트 프로세스에서는 단위 테스트(Unit Testing), 통합 테스트(Integration Testing), 시스템 테스트(System Testing), 인수 테스트(Acceptance Testing) 또는 성능 테스트(Performance Testing), 보안 테스트(Security Testing) 등에 대한 테스트 설계(Test Design)와 구현(Test Implementation)이 이루어진다. 또한 실제 테스트를 수행하기 위한 테스트 환경(Test Environment)도 준비한다. 이 과정 역시 실제 테스트 실행 전에 수행되어야 하는 Plan 단계로 볼 수 있다.</p>
<p>그 다음에는 실제 <code>테스트 수행(Test Execution)</code>이 이루어진다. 테스트 명세(Test Specification)가 준비되고 테스트 환경이 구성된 상태에서, 입력값(Input)을 넣고 출력 결과(Output)가 기대한 대로 나오는지 확인한다. 이 단계는 PDCA의 Do에 해당한다.</p>
<p>테스트 수행 후에는 결과를 확인하고 <code>평가(Test Evaluation)</code>한다. 테스트 결과가 요구사항을 만족하는지, 결함(Defect)이 발견되었는지, 테스트 과정에 문제가 있었는지 확인한다. 이후 테스트 프로세스도 완벽하지 않기 때문에, 더 효율적으로 수행하기 위해 <code>개선(Test Improvement)</code>이 필요하다. 이 과정은 PDCA의 Check와 Act에 해당한다.</p>
<p>정리하면, ISO/IEC/IEEE 29119는 테스트를 조직 차원의 테스트 프로세스, 테스트 관리 프로세스, 동적 테스트 프로세스로 구분한다. 조직 차원의 정책과 전략이 프로젝트 테스트 계획으로 전달되고, 다시 실제 동적 테스트 설계와 수행으로 이어진다. 그리고 테스트 결과는 다시 평가와 개선으로 연결된다. 즉, 테스트 프로세스 전체가 PDCA 사이클을 기반으로 반복적으로 개선되는 구조이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/6021b441-a9d0-4233-9795-2fbf196baa4f.png" alt="" style="display:block;margin:0 auto" />

<h3>Test Planning and Risk-based Test Strategy</h3>
<p><code>테스트 계획(Test Planning)</code>은 테스트 프로세스에서 가장 중요한 부분 중 하나이다. 테스트 계획이란 테스트하고자 하는 대상(Test Object)을 이해하고, 테스트 범위(Test Scope)를 정하며, 어떤 방식으로 테스트할지, 누가 수행할지, 어떤 도구(Test Tool)를 사용할지, 어떤 절차(Test Procedure)를 따를지를 미리 정하는 과정이다.</p>
<p>이때 특히 중요한 것은 <code>위험 기반 테스트 계획(Risk-based Test Planning)</code>을 수립하는 것이다. 위험(Risk)이란 아직 발생하지 않았지만 앞으로 문제가 될 수 있는 잠재적인 요소를 의미한다. 테스트 계획 단계에서는 아직 테스트를 수행하기 전이므로, 테스트 관점에서 어떤 문제가 발생할 수 있는지를 미리 생각해야 한다.</p>
<p>예를 들어 고객이 요구사항(Requirements)을 명확하게 제공하지 않았다면, 이를 기반으로 작성되는 테스트 케이스(Test Case)의 품질도 낮아질 수 있다. 또한 여러 요구사항 중에서 고객(Customer), 사용자(User), 이해관계자(Stakeholder) 입장에서 가장 중요한 요구사항이 무엇인지 파악하는 것도 필요하다. 이러한 과정을 통해 테스트 관점의 위험을 분석할 수 있다.</p>
<p>위험 기반 테스트 전략(Risk-based Test Strategy)이 필요한 이유는 프로젝트의 자원이 항상 제한되어 있기 때문이다. 일정(Schedule), 비용(Cost), 인력, 장소, 도구와 같은 자원은 무한하지 않다. 따라서 모든 요구사항을 동일한 수준으로 동시에 테스트하기는 어렵다. 이때 위험 분석(Risk Analysis)을 통해 중요한 요구사항과 위험도가 높은 부분을 우선적으로 테스트해야 한다.</p>
<p>즉, 위험 기반 테스트 전략은 고객, 사용자, 이해관계자 관점에서 중요한 요구사항을 찾아내고, 그 요구사항에서 발생할 수 있는 위험을 완화(Mitigation)하거나 해결할 수 있는 방법을 테스트 계획에 반영하는 것이다. 테스트 계획 단계에서는 반드시 이러한 위험 기반 관점을 고려해야 하며, 이를 통해 제한된 자원을 효율적으로 사용하고 테스트의 효과를 높일 수 있게된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/02dae042-99ad-418a-a17c-7df9cfb6401a.png" alt="" style="display:block;margin:0 auto" />

<h3>Test Process Automation</h3>
<p>테스트 프로세스 자동화(Test Process Automation)는 PDCA(Plan-Do-Check-Act)에 기반한 테스트 프로세스의 전체 또는 일부를 도구(Tool)를 활용해 수행하는 개념이다. 즉, 테스트 계획(Test Planning), 테스트 설계(Test Design), 테스트 수행(Test Execution), 결과 확인과 개선(Test Evaluation and Improvement) 중 필요한 부분을 자동화하여 테스트를 더 효율적으로 수행하려는 것이다.</p>
<p>하지만 테스트 프로세스 전체를 무조건 자동화하는 것이 항상 좋은 것은 아니다. 각 단계에 맞는 도구를 구축하고 운영하기 위해서는 많은 비용(Cost), 시간(Time), 인력(Resource)이 필요하다. 따라서 자동화는 모든 영역에 적용하기보다, 실제로 효과가 크고 반복적으로 수행되는 필요한 부분에 적용하는 것이 중요하다.</p>
<p>수업에서는 대표적으로 테스트 설계 자동화(Test Design Automation)와 테스트 수행 자동화(Test Execution Automation)를 살펴보았다. 테스트 설계 자동화는 테스트 케이스(Test Case)를 만들거나 조합하는 과정을 도구로 지원하는 것이고, 테스트 수행 자동화는 사람이 반복적으로 실행해야 하는 테스트를 도구를 통해 자동으로 수행하는 것이다.</p>
<p>정리하면, 테스트 프로세스 자동화는 테스트 활동을 더 효율적으로 수행하기 위한 방법이다. 다만 자동화에는 비용과 운영 부담이 따르기 때문에, 꼭 필요한 부분을 선택하여 적용해야 한다. 이번 챕터에서는 PDCA 관점의 테스트 프로세스와 ISO/IEC/IEEE 29119 국제표준을 함께 살펴보았으며, 테스트 자동화는 이러한 프로세스를 효율적으로 운영하기 위한 수단으로 이해할 수 있다.</p>
<p>📌Test Design Automation은 “어떤 테스트 케이스를 만들까?”를 도와주는 도구이다.<br />예: PICT로 입력값 조합을 자동 생성</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/082edc6a-85b3-48b1-89d0-168bf54a7d70.png" alt="" style="display:block;margin:0 auto" />

<p>📌Test Execution Automation은 “그 테스트를 자동으로 실행하자”에 가깝다.<br />예: Selenium이나 Katalon으로 웹사이트 동작을 자동 테스트</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/091d0209-2218-4028-9008-0d918b1d1dcc.png" alt="" style="display:block;margin:0 auto" />

<h3>Software Test Techniques and AI Testing</h3>
<p>세 번째 챕터에서는 소프트웨어 테스트 기법(Software Test Techniques)과 AI 테스트(AI Testing)에 대해 복습한다. 이 챕터의 핵심은 테스트 설계 기법(Test Design Techniques)을 어떻게 분류하고, 일반 소프트웨어와 AI 소프트웨어에 각각 어떤 방식으로 적용할 수 있는지 이해하는 것이다.</p>
<p>먼저 테스트 설계 기법 분류(Classification of Test Design Techniques)를 살펴본다. 테스트 기법은 크게 <code>블랙박스 테스트(Black-box Testing)</code>와 <code>화이트박스 테스트(White-box Testing)</code>로 나눌 수 있다. 블랙박스 테스트는 내부 구조를 보지 않고 요구사항(Requirements)이나 명세(Specification)를 기반으로 입력과 출력의 관계를 확인하는 방식이다. 반면 화이트박스 테스트는 소프트웨어 내부 구조(Internal Structure), 코드 로직(Code Logic), 제어 흐름(Control Flow) 등을 기반으로 테스트하는 방식이다.</p>
<p>이후에는 블랙박스 테스트 설계 기법(Black-box Test Design Techniques)의 종류를 살펴본다. 블랙박스 테스트에서는 <code>동등 분할(Equivalence Partitioning), 경계값 분석(Boundary Value Analysis), 결정 테이블 테스트(Decision Table Testing), 상태 전이 테스트(State Transition Testing)</code> 등 요구사항과 입력 조건을 기반으로 테스트 케이스(Test Case)를 설계하는 방법들이 포함된다.</p>
<p>다음으로 화이트박스 테스트 설계 기법(White-box Test Design Techniques)의 종류를 살펴본다. <code>화이트박스 테스트에서는 소스 코드(Source Code)의 구조를 기준으로 문장 커버리지(Statement Coverage), 분기 커버리지(Branch Coverage), 조건 커버리지(Condition Coverage) 등을 확인한다.</code> 즉, 코드 내부가 얼마나 충분히 테스트되었는지를 평가하는 데 초점을 둔다.</p>
<p>또한 일반 소프트웨어 테스트 기법뿐만 아니라 AI 소프트웨어 테스트(AI Software Testing)의 품질 특성과 기법도 함께 복습한다. AI 소프트웨어는 데이터(Data)와 모델(Model)에 의존하며, 정확도(Accuracy), 편향성(Bias), 강건성(Robustness), 데이터 품질(Data Quality)과 같은 요소가 중요하다. 따라서 기존 테스트 기법을 그대로 적용하는 것에는 한계가 있을 수 있고, AI 소프트웨어의 특성을 반영한 테스트 기법이 필요하다.</p>
<p>정리하면, 이번 챕터의 주요 키워드는 테스트 설계 기법 분류(Classification of Test Design Techniques), 블랙박스 테스트(Black-box Testing), 화이트박스 테스트(White-box Testing), 일반 소프트웨어 테스트 기법(General Software Test Techniques), AI 소프트웨어 품질과 테스트(AI Software Quality and Testing)이 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/2559f92e-4d9d-4837-8560-c432284d28aa.png" alt="" style="display:block;margin:0 auto" />

<h3>Test Design Techniques Classification</h3>
<p>테스트 설계 기법(Test Design Techniques)은 크게 <code>블랙박스 테스트(Black-box Testing)와 화이트박스 테스트(White-box Testing)</code>로 분류할 수 있다. 두 기법의 차이는 테스트 대상 소프트웨어(Software)의 내부 로직을 보는지, 보지 않는지에 있다.</p>
<p>블랙박스 테스트(Black-box Testing)는 이름 그대로 소프트웨어 내부를 ‘검은 상자’처럼 보고, 내부 로직(Internal Logic)이나 소스 코드(Source Code)는 고려하지 않는 테스트 방법이다. 테스트하는 사람은 소프트웨어 안에서 어떤 코드가 실행되는지 직접 확인하지 않고, 입력값(Input)을 넣었을 때 출력값(Output)이 요구사항에 맞게 나오는지를 확인한다.</p>
<p>따라서 블랙박스 테스트에서는 테스트 케이스(Test Case)를 만들 때 요구사항 명세서(Requirements Specification), 설계서(Design Document)와 같은 스펙(Specification)을 기반으로 한다. 그래서 블랙박스 테스트는 명세 기반 테스트(Specification-based Testing) 또는 스펙 기반 테스트라고도 부른다.</p>
<p>반면 화이트박스 테스트(White-box Testing)는 소프트웨어 내부의 소스 코드를 직접 들여다보고 테스트하는 방법이다. 즉, 코드의 구조(Code Structure), 로직(Logic), 조건문, 반복문, 실행 경로(Path)를 고려하여 테스트 케이스를 설계한다. 목표는 소스 코드 안의 가능한 독립적인 경로(Independent Path)를 최대한 많이 테스트하는 것이다.</p>
<p>화이트박스 테스트에서는 테스트 케이스를 만들기 위해 소스 코드를 분석해야 한다. 전체 소스 코드의 구성 요소와 구조를 파악한 뒤, 어떤 부분이 실행되었고 어떤 경로가 테스트되었는지를 확인한다. 그래서 화이트박스 테스트는 구조 기반 테스트(Structure-based Testing)라고도 부른다.</p>
<p>정리하면, 블랙박스 테스트는 내부 코드를 보지 않고 요구사항과 출력 결과를 중심으로 테스트하는 방법이고, 화이트박스 테스트는 소스 코드의 내부 구조와 실행 경로를 분석하여 테스트하는 방법이다. 이 차이가 두 테스트 설계 기법을 구분하는 가장 중요한 기준이다.</p>
<p>📌위에서 배운 PICT는 페어와이즈 테스팅또는 조합 테스팅을 위한 도구로 블랙박스 테스트 설계 자동화(Test Design Automation) 도구와 관련이 있다.</p>
<p>예를 들어 로그인 기능을 테스트할 때:</p>
<p>브라우저: Chrome, Edge, Firefox<br />운영체제: Windows, macOS<br />계정 상태: 정상, 잠김, 만료</p>
<p>이런 입력 조건들을 조합해서 테스트 케이스를 자동으로 만들어주는 것이 PICT이라고 할 수 있다. 이 과정에서 소스 코드는 보지 않기 때문이다.</p>
<p>📌Selenium은 웹 애플리케이션을 자동으로 실행하고 테스트하는 도구이다. 예를 들어 사용자가 버튼을 클릭하고, 로그인하고, 페이지가 이동하고, 결과 메시지가 나타나는지를 자동으로 확인할 수 있다. 이것도 보통은 소스 코드 내부 구조를 직접 분석하지 않고, 사용자의 관점에서 입력과 출력, 화면 동작을 확인한다. 그래서 Selenium은 주로 블랙박스 테스트 수행 자동화(Test Execution Automation) 도구로 볼 수 있다.</p>
<p>예를 들어:</p>
<p>로그인 페이지에 아이디와 비밀번호 입력<br />로그인 버튼 클릭<br />메인 페이지로 이동하는지 확인<br />오류 메시지가 제대로 나오는지 확인</p>
<p>이런 테스트는 내부 코드가 아니라 사용자 화면과 결과를 기준으로 하므로 블랙박스 테스트에 가깝다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/105eb89f-2012-4add-aa8b-b60a4211ab05.png" alt="" style="display:block;margin:0 auto" />

<h3>Black-box Test Design Techniques</h3>
<p><code>블랙박스 테스트(Black-box Testing)</code>는 소프트웨어 내부의 소스 코드(Source Code)나 로직(Logic)을 직접 보지 않고, 요구사항(Requirements)이나 명세(Specification)를 기반으로 테스트 케이스(Test Case)를 설계하는 방법이다. 수업에서는 블랙박스 테스트 설계 기법으로 총 7가지를 학습했다.</p>
<p>첫 번째는 신택스 테스팅(Syntax Testing)이다. 입력값(Input)이 정해진 형식이나 규칙에 맞는지 확인하는 테스트 방식이다. 입력이 올바른 경우와 올바르지 않은 경우를 나누어, 시스템이 각각의 입력을 제대로 처리하는지 확인한다.</p>
<p>두 번째는 동등 분할(Equivalence Partitioning)이다. 입력값이 일정한 범위(Range)를 가질 때, 그 범위를 여러 구간으로 나누고 각 구간의 대표값을 선택하여 테스트하는 방법이다. 예를 들어 성적 관리 시스템에서 점수 구간을 나누고, 각 구간의 대표 점수를 입력하여 결과가 올바르게 나오는지 확인할 수 있다.</p>
<p>세 번째는 경계값 분석(Boundary Value Analysis)이다. 동등 분할에서 나눈 각 구간의 경계 부분을 중심으로 테스트하는 방법이다. 결함은 입력 범위의 중간보다 경계 부분에서 발생할 가능성이 높기 때문에, 구간의 시작값과 끝값 주변을 집중적으로 테스트한다.</p>
<p>네 번째는 결정 테이블 테스팅(Decision Table Testing)이다. 입력 조건(Input Condition)과 출력 결과(Output Result)가 true 또는 false처럼 나누어질 때, 그 조건들의 조합을 표로 정리하여 테스트하는 방법이다. 여러 조건이 함께 작용하여 결과가 달라지는 경우에 유용하다.</p>
<p>다섯 번째는 상태 전이 테스팅(State Transition Testing)이다. 시스템이 여러 상태(State)를 가지고 있고, 이벤트(Event)에 따라 다른 상태로 전이되는 경우에 사용하는 테스트 기법이다. 예를 들어 대기 상태(Waiting State), 실행 상태(Running State), 정지 상태(Stopped State)처럼 시스템의 상태를 정의하고, 이벤트 발생에 따라 상태가 올바르게 변하는지 확인한다. 이때 상태 머신(State Machine)이나 상태 전이 다이어그램(State Transition Diagram)을 활용할 수 있다.</p>
<p>여섯 번째는 시나리오 테스팅(Scenario Testing)이다. 사용자가 실제로 시스템을 사용하는 흐름을 시나리오(Scenario)로 정리하고, 그 흐름에 따라 테스트하는 방법이다. 수업에서는 ATM 현금 인출기(ATM Cash Withdrawal)를 예로 들었다. 정상적으로 현금을 인출하는 시나리오뿐만 아니라, 잘못된 입력, 오류 발생, 다른 기능 사용과 같은 다양한 상황도 함께 테스트할 수 있다.</p>
<p>일곱 번째는 페어와이즈 테스팅(Pairwise Testing)이다. 여러 입력값의 모든 조합을 테스트하기 어려울 때, 두 개의 입력값 쌍(Pair)을 중심으로 조합하여 테스트하는 방법이다. 예를 들어 A, B, C라는 입력 조건이 있을 때 A-B, A-C, B-C처럼 두 조건씩 조합하여 테스트한다. 수업에서는 이 부분을 도구(Tool) PICT를 사용하여 실습하기도 했다.</p>
<p>정리하면, 블랙박스 테스트 설계 기법에는 신택스 테스팅, 동등 분할, 경계값 분석, 결정 테이블 테스팅, 상태 전이 테스팅, 시나리오 테스팅, 페어와이즈 테스팅이 있다. 이 기법들의 공통점은 소스 코드 내부를 직접 분석하지 않고, 요구사항 명세서나 설계서와 같은 스펙을 기반으로 테스트 케이스를 만든다는 점이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/ccdcec69-bbeb-4c17-9603-f1880dbfd47a.png" alt="" style="display:block;margin:0 auto" />

<h3>White-box Test Design Techniques</h3>
<p>화이트박스 테스트(White-box Testing)는 소스 코드(Source Code)의 내부 구조와 실행 경로를 분석하여 테스트 케이스(Test Case)를 설계하는 방법이다. 블랙박스 테스트가 요구사항이나 명세를 기반으로 테스트한다면, 화이트박스 테스트는 실제 소스 코드의 구조를 직접 확인하면서 테스트한다는 점이 특징이다.</p>
<p>수업에서는 화이트박스 테스트 설계 기법으로 크게 7가지를 살펴보았다. 여기서 가장 중요한 개념은 테스트 커버리지(Test Coverage)이다. 테스트 커버리지는 소스 코드의 전체 구조, 문장, 분기, 조건, 경로 중에서 얼마나 많은 부분이 테스트되었는지를 나타내는 기준이다. 예를 들어 문장 테스팅(Statement Testing)을 수행하면 문장 커버리지(Statement Coverage)를 확인하고, 분기 테스팅(Branch Testing)을 수행하면 분기 커버리지(Branch Coverage)를 확인한다.</p>
<p>첫 번째는 문장 테스팅(Statement Testing)이다. 문장 테스팅은 소스 코드 안의 실행 가능한 문장(Statement)이 얼마나 테스트되었는지를 확인하는 방법이다. 일반적으로 세미콜론(Semicolon)으로 끝나는 명령문 단위가 테스트 대상이 될 수 있으며, 전체 문장 중 몇 개가 실행되었는지를 기준으로 문장 커버리지를 계산한다.</p>
<p>두 번째는 분기 테스팅(Branch Testing)이다. 분기 테스팅은 if문과 같은 분기 구조에서 Yes/No 또는 True/False 경로가 모두 테스트되었는지를 확인하는 방법이다. 즉, 조건식 전체의 결과가 참인 경우와 거짓인 경우를 모두 확인하여 분기 커버리지를 높이는 것이 목적이다.</p>
<p>세 번째는 조건 테스팅(Condition Testing)이다. 조건 테스팅은 하나의 분기문 안에 여러 개의 개별 조건(Condition)이 있을 때, 각 조건이 참과 거짓을 모두 가질 수 있도록 테스트하는 방법이다. 분기 테스팅이 전체 분기 결과를 본다면, 조건 테스팅은 분기 안에 포함된 각각의 조건을 개별적으로 본다는 차이가 있다.</p>
<p>네 번째는 분기/조건 테스팅(Branch/Condition Testing)이다. 이는 분기 테스팅과 조건 테스팅을 함께 고려하는 방법이다. 전체 분기 결과도 확인하고, 동시에 분기 안에 있는 개별 조건들도 테스트한다.</p>
<p>다섯 번째는 변경 조건/결정 테스팅(Modified Condition/Decision Testing, MC/DC)이다. MC/DC는 분기/조건 테스팅을 더 강화한 방식으로, 각 개별 조건이 전체 결정 결과(Decision)에 독립적으로 영향을 주는지를 확인하는 테스트 기법이다. 조건이 여러 개인 복잡한 분기문에서 테스트 적합성을 높이기 위해 사용된다.</p>
<p>여섯 번째는 다중 조건 테스팅(Multiple Condition Testing)이다. 다중 조건 테스팅은 조건들의 모든 조합을 테스트하는 방법이다. 모든 조건 조합을 확인하기 때문에 커버리지는 높아질 수 있지만, 조건이 많아질수록 테스트 케이스 수가 크게 증가할 수 있다.</p>
<p>일곱 번째는 기본 경로 테스팅(Basis Path Testing)이다. 기본 경로 테스팅은 소스 코드의 독립적인 실행 경로(Independent Path)를 찾아 테스트하는 방법이다. 이때 순환 복잡도(Cyclomatic Complexity)를 활용하여 프로그램의 복잡도를 계산하고, 테스트해야 할 독립 경로의 수를 파악한다. 수업에서는 이 순환 복잡도를 구하는 방법도 함께 학습했다.</p>
<p>이러한 기법들은 문장 테스팅, 분기 테스팅, 조건 테스팅처럼 비교적 낮은 커버리지에서 시작하여, 분기/조건 테스팅, MC/DC, 다중 조건 테스팅으로 갈수록 더 높은 커버리지를 목표로 한다. 하지만 실제 프로젝트에서는 비용(Cost), 일정(Schedule), 자원(Resource)이 제한되어 있기 때문에 무조건 가장 높은 커버리지를 목표로 하기보다, 상황에 맞는 적절한 테스트 기법을 선택해야 한다.</p>
<p>정리하면, 화이트박스 테스트 설계 기법의 핵심은 소스 코드를 직접 분석하여 테스트 케이스를 만들고, 테스트 커버리지를 높이는 것이다. 문장, 분기, 조건, 경로가 얼마나 테스트되었는지를 확인하는 커버리지 개념을 잘 이해하는 것이 중요하다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/94f7a8b6-cb6b-4138-9872-f666f1ef41b3.png" alt="" style="display:block;margin:0 auto" />

<h3>AI Software Quality Characteristics</h3>
<p>AI 소프트웨어(AI Software)는 일반 소프트웨어가 가지는 품질 특성(Quality Characteristics)을 함께 가진다. 예를 들어 기능 적합성(Functional Suitability), 사용성(Usability), 호환성(Compatibility), 성능 효율성(Performance Efficiency)과 같은 특성은 일반 소프트웨어와 AI 소프트웨어 모두에서 중요하다.</p>
<p>하지만 AI 소프트웨어는 일반 소프트웨어와 구분되는 고유한 특성도 가진다. 대표적으로 <code>적응성(Adaptability), 자율성(Autonomy), 진화(Evolution), 유연성(Flexibility), 편향성(Bias), 성능 메트릭(Performance Metrics), 투명성(Transparency), 복잡성(Complexity), 비결정성(Non-determinism)</code> 등이 있다.</p>
<p>이러한 특성은 AI 소프트웨어가 데이터(Data)를 기반으로 동작하기 때문에 나타난다. AI 모델(AI Model)은 훈련 데이터(Training Data)를 학습하고, 그 학습 결과를 바탕으로 새로운 입력에 대한 예측(Prediction)이나 판단(Decision)을 수행한다. 따라서 더 많은 데이터나 새로운 데이터를 학습하면서 성능이 달라지거나, 환경 변화에 적응하는 모습을 보일 수 있다.</p>
<p>또한 AI 소프트웨어는 일정 수준의 자율성(Autonomy)을 가질 수 있다. 사람이 모든 규칙을 직접 코드로 작성하는 것이 아니라, 모델이 데이터에서 패턴(Pattern)을 학습하고 그에 따라 판단하기 때문이다. 이 과정에서 AI 모델은 유연하게 동작할 수 있지만, 동시에 훈련 데이터에 포함된 편향(Bias)을 함께 학습할 위험도 있다.</p>
<p>AI 소프트웨어의 또 다른 특징은 복잡성과 비결정성이다. AI 모델, 특히 신경망(Neural Network) 기반 모델은 내부 구조가 복잡하여 결과가 어떻게 도출되었는지 이해하기 어려울 수 있다. 또한 학습 방식, 실행 환경, 확률적 처리 방식 등에 따라 같은 입력에 대해서도 결과가 달라질 수 있다. 이러한 특성 때문에 AI 소프트웨어는 일반 소프트웨어보다 테스트하기 어려운 부분이 존재한다.</p>
<p>정리하면, AI 소프트웨어는 일반 소프트웨어의 품질 특성을 가지면서도 데이터 기반 동작으로 인해 적응성, 자율성, 진화, 편향성, 투명성, 복잡성, 비결정성과 같은 고유한 품질 특성을 가진다. 이러한 특성 때문에 AI 소프트웨어만을 위한 테스트 기법(AI Software Test Techniques)이 연구되고 적용되고 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/49ed2846-c6da-469e-bf8b-c5ae1c817a44.png" alt="" style="display:block;margin:0 auto" />

<h3>AI Software Test Technique Classification</h3>
<p>AI 소프트웨어 테스트 기법(AI Software Test Techniques)은 크게 블랙박스 테스트(Black-box Testing)와 화이트박스 테스트(White-box Testing)로 나눌 수 있다. 일반 소프트웨어 테스트에서도 블랙박스와 화이트박스로 나누어 테스트 기법을 분류했듯이, AI 소프트웨어에서도 같은 기준을 적용할 수 있다.</p>
<p>블랙박스 테스트(Black-box Testing)는 AI 모델(AI Model)의 내부 구조나 동작 원리를 직접 고려하지 않고, 입력(Input)에 대한 출력(Output)이 적절한지만 확인하는 방법이다. 일반 소프트웨어에서는 내부 소스 코드 로직(Source Code Logic)을 보지 않고 테스트했다면, AI 소프트웨어에서는 AI 모델 내부를 보지 않고 입력 데이터에 대해 올바른 결과가 나오는지를 확인한다. 대표적인 AI 블랙박스 테스트 기법으로는 변성 테스트(Metamorphic Testing), 탐색적 테스트(Exploratory Testing) 등이 있다.</p>
<p>반면 화이트박스 테스트(White-box Testing)는 AI 모델 내부의 신경망 구조(Neural Network Structure)와 동작을 고려하여 테스트하는 방법이다. 즉, AI 모델이 어떤 구조로 구성되어 있고, 입력 데이터가 들어왔을 때 내부 뉴런(Neuron)들이 어떻게 활성화되는지를 확인하면서 테스트 케이스(Test Case)를 설계하고 수행한다. 이 점이 블랙박스 테스트와의 가장 큰 차이이다.</p>
<p>AI 소프트웨어의 화이트박스 테스트에서 중요한 개념은 커버리지(Coverage)이다. 일반 소프트웨어에서 소스 코드가 얼마나 테스트되었는지를 문장 커버리지(Statement Coverage), 분기 커버리지(Branch Coverage) 등으로 확인했던 것처럼, AI 소프트웨어에서는 신경망을 구성하는 뉴런들이 얼마나 많이 테스트되었는지를 확인한다. 수업에서는 이와 관련하여 뉴런 커버리지(Neuron Coverage)를 간단히 살펴보았다.</p>
<p>정리하면, AI 소프트웨어 테스트 기법도 일반 소프트웨어 테스트와 마찬가지로 블랙박스 테스트와 화이트박스 테스트로 나눌 수 있다. 블랙박스 테스트는 AI 모델 내부를 보지 않고 입력과 출력의 관계를 중심으로 테스트하는 방법이고, 화이트박스 테스트는 AI 모델 내부의 신경망 구조와 뉴런 커버리지를 고려하여 테스트하는 방법이다. AI 소프트웨어 테스트는 아직 활발히 연구가 진행 중인 분야이며, AI의 특성을 반영한 다양한 테스트 기법들이 계속 발전하고 있다.</p>
<hr />
]]></content:encoded></item><item><title><![CDATA[Virtualization and Unix Practice with NetBSD]]></title><description><![CDATA[Introduction to Virtualization and Unix Installation (가상화와 Unix 설치 개요)
이번 시간에는 운영체제(Operating System) 수업의 실습 준비 과정으로 가상화(Virtualization)의 개념을 간단히 살펴보고, Unix(유닉스)를 설치하는 과정을 다룬다. 앞으로 실습에서는 실제 컴퓨터에 직접 운영]]></description><link>https://heesu.tech/virtualization-and-unix-practice-with-netbsd</link><guid isPermaLink="true">https://heesu.tech/virtualization-and-unix-practice-with-netbsd</guid><dc:creator><![CDATA[Heesu Noh]]></dc:creator><pubDate>Sun, 07 Jun 2026 03:15:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/fdddfaa2-a9e3-4df7-953d-551e6091c6b0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3>Introduction to Virtualization and Unix Installation (가상화와 Unix 설치 개요)</h3>
<p>이번 시간에는 운영체제(Operating System) 수업의 실습 준비 과정으로 <code>가상화(Virtualization)</code>의 개념을 간단히 살펴보고, <code>Unix(유닉스)</code>를 설치하는 과정을 다룬다. 앞으로 실습에서는 실제 컴퓨터에 직접 운영체제를 설치하는 것이 아니라, 가상화 환경(Virtualized Environment)을 이용해 별도의 운영체제를 실행하게 된다.</p>
<p><code>가상화(Virtualization)</code>란 하나의 실제 컴퓨터 안에서 또 다른 컴퓨터 환경을 만들어 사용하는 기술이다. 예를 들어 현재 사용 중인 Windows나 macOS 안에서 가상 머신(Virtual Machine)을 만들고, 그 안에 Unix나 Linux 같은 다른 운영체제를 설치해 실행할 수 있다. 이렇게 하면 실제 컴퓨터의 설정을 크게 변경하지 않고도 다양한 운영체제 환경을 실습할 수 있다는 장점이 있다.</p>
<p>이번 수업의 목표는 <code>가상화(Virtualization)</code>가 무엇인지 이해하고, 이를 이용해 Unix(유닉스)를 설치해 보는 것이다. 수업이 끝난 후에는 가상화가 어떤 개념인지 간단히 설명할 수 있어야 하며, Unix 환경에서 기본적인 명령어(Command)를 실행할 준비가 되어 있어야 한다.</p>
<p>Unix(유닉스)는 Linux(리눅스)와 완전히 같은 운영체제는 아니지만, 역사적 맥락과 사용 방식이 매우 비슷하다. 특히 터미널(Terminal)에서 사용하는 기본 명령어(Command)는 상당 부분 동일하거나 유사하다. 따라서 Unix를 학습하면 Linux를 이해하는 데에도 도움이 되고, 반대로 Linux에서 배운 명령어 지식도 Unix 환경에서 활용할 수 있다.</p>
<p>이번 실습에서는 Unix를 직접 설치하면서 운영체제 실습 환경을 구성하게 된다. 이후 수업에서 파일 시스템(File System), 프로세스(Process), 명령어(Command), 권한(Permission) 등 운영체제의 핵심 개념을 배우기 위해서는 먼저 Unix 또는 Linux와 같은 실습 환경이 필요하다. 따라서 이번 시간은 본격적인 운영체제 실습을 시작하기 위한 기초 준비 단계라고 볼 수 있다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p>가상 머신(Virtual Machine)은 실제 컴퓨터 안에 만들어진 “가짜 컴퓨터”라고 생각하면 이해하기 쉽다. 이 가상 머신 안에는 별도의 운영체제(Operating System)를 설치할 수 있고, 실제 컴퓨터와는 분리된 환경에서 안전하게 실습할 수 있다.</p>
<p>Unix(유닉스)와 Linux(리눅스)는 같은 것은 아니지만, Linux는 Unix의 철학과 구조를 많이 참고하여 만들어졌다. 그래서 명령어(Command), 파일 구조(File Structure), 터미널 사용 방식(Terminal Usage)이 비슷하다.</p>
<p>운영체제 수업에서 Unix/Linux 환경을 사용하는 이유는 운영체제의 내부 구조를 이해하기 좋기 때문이다. 특히 프로세스(Process), 파일(File), 권한(Permission), 메모리(Memory), 커널(Kernel) 같은 개념을 명령어와 실습을 통해 직접 확인할 수 있다.</p>
<hr />
<h3>Booting, BIOS, and UEFI (부팅, BIOS, UEFI)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/344d4808-fdf1-4d67-ace7-282ed342ffbc.png" alt="" style="display:block;margin:0 auto" />

<h2>Computer Components (컴퓨터 구성 요소)</h2>
<p>이번 내용은 <code>가상화(Virtualization)</code>를 이해하기 전에, 컴퓨터가 기본적으로 어떻게 구성되고 어떻게 <code>부팅(Booting)</code>되는지를 살펴보는 부분이다. 컴퓨터를 사용하려면 먼저 하드웨어(Hardware)가 필요하고, 그 하드웨어 위에서 동작하는 소프트웨어(Software)가 필요하다.</p>
<p>하드웨어(Hardware)는 컴퓨터의 물리적인 장치들을 의미한다. 대표적으로 CPU(Central Processing Unit), 메모리(Memory), 스토리지(Storage), 마더보드(Motherboard), 파워서플라이(Power Supply), 그리고 입출력 장치(I/O Device)가 있다. CPU는 명령을 처리하는 핵심 장치이고, 메모리는 실행 중인 프로그램이 임시로 사용하는 공간이다. 스토리지는 운영체제와 파일이 저장되는 장치이며, 마더보드는 여러 부품을 연결하는 중심 역할을 한다.</p>
<p>반면 소프트웨어(Software)는 하드웨어 위에서 실행되는 프로그램을 의미한다. 대표적으로 운영체제(Operating System), 응용 프로그램(Application Program), 컴파일러(Compiler) 등이 있다. 운영체제는 하드웨어와 응용 프로그램 사이에서 중간 역할을 하며, 사용자가 컴퓨터 자원을 편리하게 사용할 수 있도록 관리한다.</p>
<hr />
<h3>Booting as a Connection Between Hardware and Software (하드웨어와 소프트웨어의 결합으로서의 부팅)</h3>
<p><code>부팅(Booting)</code>이란 컴퓨터가 전원이 켜진 뒤 운영체제(Operating System)를 실행할 준비를 하는 과정이다. 다시 말해, 하드웨어(Hardware)와 소프트웨어(Software)가 연결되는 첫 단계라고 볼 수 있다.</p>
<p>컴퓨터는 전원이 켜지자마자 바로 운영체제를 실행할 수 있는 것이 아니다. 먼저 하드웨어가 정상적으로 작동하는지 확인하고, 어느 저장 장치(Storage)에서 운영체제를 불러올지 찾아야 한다. 이 과정을 담당하는 것이 과거에는 <code>BIOS(Basic Input-Output System)</code>였고, 최근 컴퓨터에서는 주로 <code>UEFI(Unified Extensible Firmware Interface)</code>가 사용된다.</p>
<p>리눅스(Linux)의 부팅 과정을 예로 들면, 크게 BIOS를 사용하는 방식과 UEFI를 사용하는 방식으로 나눌 수 있다. 쉽게 말하면 BIOS는 오래된 컴퓨터에서 많이 사용되던 방식이고, UEFI는 최근 컴퓨터에서 사용하는 방식이라고 이해할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/2e2fbc40-51f0-4715-9941-19e75cfe4572.png" alt="" style="display:block;margin:0 auto" />

<h3>BIOS Booting (BIOS 기반 부팅)</h3>
<p><code>BIOS(Basic Input-Output System)</code>는 오래된 방식의 펌웨어(Firmware)이다. 펌웨어(Firmware)는 하드웨어와 운영체제 사이에서 가장 먼저 실행되는 낮은 수준의 소프트웨어라고 볼 수 있다.</p>
<p>BIOS 방식에서는 컴퓨터가 켜지면 먼저 마더보드(Motherboard)와 기본 하드웨어를 확인한다. 그 후 운영체제를 부팅하기 위해 정해진 위치를 찾는다. 이때 중요한 개념이 <code>MBR(Master Boot Record)</code>이다.</p>
<p><code>MBR(Master Boot Record)</code>은 저장 장치(Storage)의 특정 위치에 존재하는 부팅 정보 영역이다. BIOS는 이 위치를 찾아가고, 그곳에 있는 <code>부트로더(Bootloader)</code>를 실행한다. 부트로더(Bootloader)는 운영체제를 실제로 불러오는 역할을 한다.</p>
<p>예전에는 이 부팅 위치가 손상되면 나머지 데이터가 멀쩡하더라도 컴퓨터가 정상적으로 부팅되지 않는 문제가 있었다. 예를 들어 하드디스크 안의 파일들은 그대로 남아 있어도, 부트 섹터(Boot Sector)가 손상되면 운영체제를 시작할 수 없었다. 그래서 예전 컴퓨터에서는 부팅 관련 영역을 복구하는 작업이 중요한 문제였다.</p>
<hr />
<h3>UEFI Booting (UEFI 기반 부팅)</h3>
<p><code>UEFI(Unified Extensible Firmware Interface)</code>는 BIOS를 대체하기 위해 등장한 새로운 방식의 펌웨어(Firmware)이다. 최근 컴퓨터에서는 대부분 UEFI를 사용한다.</p>
<p>UEFI 방식에서는 <code>GPT(GUID Partition Table)</code>를 사용한다. GPT는 GUID Partition Table의 약자로, 기존의 MBR보다 더 현대적인 파티션 테이블(Partition Table) 방식이다. 여기서 GUID(Globally Unique Identifier)는 전 세계적으로 고유한 식별자를 의미한다.</p>
<p>UEFI에서는 부트로더(Bootloader)가 EFI 부트로더(EFI Bootloader) 형태로 관리된다. 리눅스(Linux) 환경에서는 <code>GRUB</code>(Grand Unified Bootloader)이 자주 사용된다. GRUB은 사용자가 여러 운영체제 중 하나를 선택하거나, 리눅스 커널(Linux Kernel)을 불러오는 역할을 한다.</p>
<p>BIOS가 정해진 부트 섹터(Boot Sector)를 찾아가는 방식에 가깝다면, UEFI는 더 구조화된 방식으로 부팅 정보를 관리한다. 그래서 큰 용량의 저장 장치(Storage)를 다루거나, 다양한 운영체제 환경을 관리하는 데 더 적합하다.</p>
<hr />
<h3>Why UEFI Replaced BIOS (UEFI가 BIOS를 대체하게 된 이유)</h3>
<p>BIOS는 초기 개인용 컴퓨터 시대에 만들어진 방식이다. 당시에는 저장 장치(Storage)의 용량이 지금처럼 크지 않았다. 교수님 설명처럼 예전에는 킬로바이트(KB), 메가바이트(MB) 단위의 저장 용량을 사용하는 경우가 많았지만, 지금은 스마트폰조차 기가바이트(GB) 단위의 저장 용량을 사용한다.</p>
<p>이처럼 컴퓨터의 저장 용량과 하드웨어 구조가 크게 발전하면서, 기존 BIOS 방식만으로는 현대적인 컴퓨터 환경을 충분히 지원하기 어려워졌다. 그래서 더 큰 저장 장치, 더 유연한 부팅 방식, 더 확장성 있는 구조를 지원하기 위해 UEFI가 등장하게 되었다.</p>
<p>즉, BIOS에서 UEFI로 변화한 이유는 단순히 이름이 바뀐 것이 아니라, 컴퓨터 하드웨어의 발전과 저장 장치 용량 증가에 맞춰 부팅 구조 자체가 더 현대적으로 바뀐 것이라고 볼 수 있다.</p>
<hr />
<h3>Additional Notes (추가 설명)</h3>
<p>가상화(Virtualization)를 공부할 때 부팅(Booting)을 먼저 이해하면 도움이 된다. 가상 머신(Virtual Machine)도 결국 하나의 “가짜 컴퓨터”처럼 동작하기 때문에, 그 안에서도 BIOS 또는 UEFI와 비슷한 방식으로 운영체제가 부팅된다.</p>
<p>BIOS와 UEFI의 차이는 처음에는 어렵게 느껴질 수 있지만, 간단히 정리하면 BIOS는 오래된 부팅 방식이고 UEFI는 현대적인 부팅 방식이다. BIOS는 MBR과 관련이 깊고, UEFI는 GPT와 관련이 깊다고 함께 기억하면 좋다.</p>
<p>리눅스(Linux)를 설치할 때 설치 화면에서 BIOS 모드인지 UEFI 모드인지에 따라 파티션 설정 방식이 달라질 수 있다. 특히 UEFI 환경에서는 <code>EFI System Partition(ESP)</code>이라는 별도의 파티션이 사용될 수 있다. 지금 단계에서는 세부 설정을 모두 외우기보다는, 운영체제가 부팅되기 위해서는 펌웨어(Firmware), 부트로더(Bootloader), 저장 장치(Storage)의 부팅 정보가 함께 작동해야 한다는 흐름을 이해하는 것이 중요하다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/5561040f-3fa8-4ce6-a0f3-f64bd88ba5f4.png" alt="" style="display:block;margin:0 auto" />

<h3>Linux Boot Process and Runlevel (리눅스 부팅 과정과 런레벨)</h3>
<h3>What is Booting? (부팅이란 무엇인가)</h3>
<p>부팅(Booting)이란 컴퓨터에 전원이 켜진 뒤, 운영체제(Operating System)를 사용할 수 있는 상태가 되기까지의 과정이다. 컴퓨터는 전원이 들어온다고 바로 사용할 수 있는 것이 아니라, 먼저 하드웨어(Hardware)가 정상인지 확인하고 운영체제를 불러오는 단계를 거친다.</p>
<p>이 과정에서 CPU, RAM, 마더보드(Motherboard), 스토리지(Storage) 등이 제대로 연결되어 있는지 확인된다. 특히 서버(Server)는 메모리 용량이 크기 때문에 부팅 중 메모리 체크(Memory Check)에 시간이 오래 걸릴 수 있다.</p>
<h3>BIOS, UEFI, and GRUB (BIOS, UEFI, GRUB)</h3>
<p>부팅이 시작되면 가장 먼저 BIOS(Basic Input-Output System) 또는 UEFI(Unified Extensible Firmware Interface)가 실행된다. 이들은 하드웨어를 확인하고, 운영체제를 불러올 수 있는 저장 장치(Storage)를 찾는 역할을 한다.</p>
<p>그 다음에는 GRUB(Grand Unified Bootloader)이라는 부트로더(Bootloader)가 실행된다. GRUB은 리눅스에서 사용할 커널(Kernel)을 선택하고 불러오는 역할을 한다. 즉, BIOS나 UEFI가 하드웨어적인 준비를 담당한다면, GRUB은 운영체제의 핵심인 커널을 실행하는 단계로 연결해 준다.</p>
<h3>Kernel and initramfs (커널과 initramfs)</h3>
<p>GRUB이 커널(Kernel)을 불러오면, 커널은 메모리(Memory)에 올라가 실행된다. 커널은 보통 압축된 형태로 저장되어 있기 때문에 부팅 과정에서 압축을 해제한 뒤 실행된다.</p>
<p>이후 initramfs(initial RAM file system) 또는 initrd(initial RAM disk)가 사용된다. 이것은 실제 루트 파일 시스템(Root File System)을 사용하기 전까지 임시로 사용하는 파일 시스템이다. 쉽게 말하면, 리눅스가 본격적으로 시작되기 전 필요한 준비 작업을 하기 위한 임시 공간이다.</p>
<p>커널은 하드웨어를 초기화하고, 필요한 드라이버(Driver)를 불러온 뒤, 실제 루트 파일 시스템(Root File System)을 마운트(Mount)한다. 루트 파일 시스템은 리눅스 전체 파일 구조의 출발점이기 때문에 반드시 정상적으로 연결되어야 한다.</p>
<h3>init Process and Runlevel (init 프로세스와 런레벨)</h3>
<p>커널 초기화가 끝나면 init 프로세스(init process)가 실행된다. init 프로세스는 PID 1(Process ID 1)을 가지며, 사용자 영역(User Space)에서 가장 먼저 실행되는 프로세스이다.</p>
<p>이후 시스템은 정해진 런레벨(Runlevel)에 따라 어떤 상태로 부팅할지 결정한다. 전통적인 리눅스에서는 런레벨이 0부터 6까지 나뉜다.</p>
<p>Runlevel 0은 시스템 종료(Halt), Runlevel 1은 싱글 유저 모드(Single-user Mode), Runlevel 2와 3은 멀티 유저 모드(Multi-user Mode), Runlevel 5는 GUI(Graphical User Interface)가 포함된 멀티 유저 모드, Runlevel 6은 재부팅(Reboot)을 의미한다.</p>
<p>서버(Server)는 보통 그래픽 화면을 사용하지 않기 때문에 Runlevel 3과 비슷한 환경에서 동작한다. 반대로 GUI 환경을 설치하면 Runlevel 5처럼 그래픽 화면으로 부팅될 수 있다.</p>
<h3>Boot Flow Summary (부팅 흐름 요약)</h3>
<p>리눅스 부팅 과정은 간단히 다음 흐름으로 정리할 수 있다.</p>
<p>전원 켜짐 → BIOS/UEFI 실행 → GRUB 실행 → 커널(Kernel) 로딩 → initramfs/initrd 사용 → 루트 파일 시스템(Root File System) 마운트 → init 프로세스 실행 → 런레벨(Runlevel)에 따른 사용자 환경 진입</p>
<h3>Additional Notes (추가 설명)</h3>
<p>전통적인 리눅스에서는 Runlevel(런레벨)을 사용했지만, 최신 Ubuntu, Fedora, CentOS 계열에서는 대부분 <code>systemd</code>를 사용한다. 그래서 실제 명령어에서는 runlevel보다 target이라는 표현을 더 자주 볼 수 있다.</p>
<p>운영체제 수업에서는 부팅 과정을 외우는 것보다 흐름을 이해하는 것이 더 중요하다. 핵심 흐름은 “펌웨어(Firmware) → 부트로더(Bootloader) → 커널(Kernel) → initramfs/initrd → 루트 파일 시스템(Root File System) → init/systemd → 사용자 환경(User Environment)” 순서로 기억하면 좋다.</p>
<hr />
<h2>Real and Virtual Computers (진짜와 가짜 컴퓨터)</h2>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/586ef7b3-0ad3-4083-86df-d98d697f8e64.png" alt="" style="display:block;margin:0 auto" />

<h3>Virtualization Concept (가상화의 개념)</h3>
<p><code>가상화(Virtualization)</code>는 실제 하드웨어(Hardware)를 그대로 사용하는 것이 아니라, 하드웨어처럼 보이는 환경을 소프트웨어적으로 만들어 사용하는 것을 말한다. 예를 들어 우리가 리눅스(Linux)를 실제 컴퓨터에 직접 설치하지 않고, Virtual Machine(가상 머신)에 설치해서 사용하는 것도 가상화의 한 예이다.</p>
<p>컴퓨터를 구성하는 CPU, Memory, Storage, Motherboard 같은 하드웨어 요소를 실제 물리 장치가 아닌 “가짜 환경”으로 만들어 사용하면, 하나의 컴퓨터 안에서 여러 운영체제(Operating System)를 실행하거나 실습 환경을 안전하게 구성할 수 있다.</p>
<h3>Virtual, Emulate, Simulate (Virtual, Emulate, Simulate의 차이)</h3>
<p>가짜로 무언가를 만든다는 의미에서 비슷하게 사용되는 용어로 Virtual, Emulate, Simulate가 있다. 세 단어는 모두 “진짜는 아니지만 진짜처럼 보이거나 동작하게 만든다”는 공통점이 있지만, 의미에는 조금씩 차이가 있다.</p>
<p><code>Virtual(가상)</code>은 실제로 존재하지 않지만, 실제와 같거나 실제와 같은 효과를 얻는 것을 의미한다. 예를 들어 Virtual Machine(가상 머신)은 실제 컴퓨터 한 대가 더 있는 것은 아니지만, 사용자는 마치 별도의 컴퓨터를 사용하는 것처럼 운영체제를 설치하고 실행할 수 있다.</p>
<p><code>Emulate(에뮬레이트)</code>는 어떤 대상을 흉내 내거나 복제해서 원래의 것과 같거나 비슷하게 동작하도록 만드는 것이다. 다만 실제 원본과 완전히 같은 것은 아니며, 원본의 동작 방식을 따라 하도록 만든 것에 가깝다. 예를 들어 특정 게임기나 오래된 컴퓨터 환경을 현대 컴퓨터에서 실행하는 <code>Emulator(에뮬레이터)</code>가 이에 해당한다.</p>
<p><code>Simulate(시뮬레이트)</code>는 실제는 아니지만, 겉으로 보기에 실제와 비슷하게 보이거나 동작하도록 만드는 것이다. 원래 대상의 내부 구조를 완전히 복제한다기보다는, 외부에서 관찰되는 현상이나 동작을 비슷하게 재현하는 데 초점이 있다.</p>
<p>Virtual은 실제와 같은 효과를 제공하는 것, Emulate는 원래 대상을 흉내 내어 동작하게 만드는 것, Simulate는 실제처럼 보이는 상황이나 동작을 재현하는 것에 가깝다. 운영체제 수업에서 사용하는 가상 머신(Virtual Machine)은 이 중 Virtualization의 대표적인 예라고 볼 수 있다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p>간단히 구분하면 Virtualization은 “실제로 있는 것처럼 사용하게 만드는 것”, Emulation은 “다른 시스템을 흉내 내는 것”, Simulation은 “현상이나 상황을 비슷하게 재현하는 것”이라고 이해하면 된다.</p>
<p>운영체제 실습에서는 Virtual Machine(가상 머신)을 통해 Unix나 Linux를 설치하기 때문에, 실제 컴퓨터를 망가뜨릴 걱정 없이 안전하게 명령어와 시스템 설정을 실습할 수 있게 된다.</p>
<hr />
<h2>History of Virtualization (가상화의 역사)</h2>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/cfe5b81e-0817-40e0-955f-8042d383489a.png" alt="" style="display:block;margin:0 auto" />

<h3>Background of Virtualization (가상화의 등장 배경)</h3>
<p><code>가상화(Virtualization)</code>는 갑자기 등장한 기술이 아니라, 1960년대부터 시작된 오래된 개념이다. 초기의 컴퓨터(Computer)는 지금처럼 개인이 쉽게 사용하는 장비가 아니었다. 가격이 매우 비쌌고, 크기도 컸으며, 전문 지식을 가진 사람만 다룰 수 있는 도구였다.</p>
<p>문제는 이렇게 비싼 컴퓨터를 한 번에 한 사람만 사용할 수 있었다는 점이다. 컴퓨터 자체의 비용뿐만 아니라, 컴퓨터를 관리할 전문가(Expert), 설치 공간(Space), 운영 비용도 필요했기 때문에 자원을 효율적으로 사용하는 방법이 중요해졌다.</p>
<h3>Batch Processing (배치 처리)</h3>
<p>초기에는 컴퓨터가 쉬는 시간을 줄이기 위해 <code>Batch Processing(배치 처리)</code> 방식이 등장했다. Batch Processing은 사람이 직접 하나씩 명령을 실행하는 것이 아니라, 여러 작업(Job)을 모아서 한 번에 컴퓨터에 전달하는 방식이다.</p>
<p>이 방식은 컴퓨터의 낭비되는 시간을 줄이는 데 도움이 되었다. 하지만 여전히 여러 사람이 동시에 컴퓨터를 사용하는 것은 어려웠다.</p>
<h3>Time Sharing (시분할)</h3>
<p>여러 사용자가 하나의 컴퓨터를 함께 사용하기 위해 등장한 개념이 <code>Time Sharing(시분할)</code>이다. Time Sharing은 컴퓨터의 사용 시간을 아주 짧은 단위로 나누어 여러 사용자에게 번갈아 제공하는 방식이다.</p>
<p>사용자 입장에서는 자신이 혼자 컴퓨터를 사용하는 것처럼 느끼지만, 실제로는 컴퓨터가 매우 빠르게 여러 사용자의 작업을 번갈아 처리하고 있는 것이다. 이를 통해 한 사용자의 작업이 끝날 때까지 다른 사용자가 기다릴 필요 없이, 여러 사람이 동시에 시스템을 공유할 수 있게 되었다.</p>
<h3>Connection to Virtualization (가상화와의 연결)</h3>
<p><code>가상화(Virtualization)</code>는 이러한 자원 공유(Resource Sharing)의 필요성에서 발전한 개념이다. 실제 하드웨어(Hardware)는 하나이지만, 사용자나 시스템 입장에서는 여러 개의 독립된 환경을 사용하는 것처럼 보이게 만드는 것이다.</p>
<p>즉, 가상화의 핵심은 컴퓨터 자원을 더 효율적으로 사용하기 위한 것이다. 초기 컴퓨터의 비싼 비용과 제한된 사용 방식이 Batch Processing과 Time Sharing을 만들었고, 이러한 흐름이 이후 가상화 기술로 이어졌다고 볼 수 있다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p>Batch Processing(배치 처리)은 “작업을 모아서 한 번에 처리하는 방식”이고, Time Sharing(시분할)은 “여러 사용자가 동시에 쓰는 것처럼 시간을 나눠 사용하는 방식”이다.</p>
<p>가상화(Virtualization)를 이해할 때는 “가짜 컴퓨터를 만든다”는 표현보다, “비싼 하드웨어 자원을 더 효율적으로 나누어 쓰기 위해 발전한 기술”이라고 이해하면 더 정확하다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/697ab0ca-ef80-4e8d-bae1-d6475aa4fed6.png" alt="" style="display:block;margin:0 auto" />

<h3>Evolution of Virtualization (가상화의 발전 과정)</h3>
<h3>1970s: Operating Systems and Virtual Resources (1970년대: 운영체제와 가상 자원)</h3>
<p>1970년대에는 <code>운영체제(Operating System)</code>가 발전하면서 하나의 컴퓨터 안에서 여러 프로그램이 독립적으로 자원을 사용할 수 있게 되었다. 이때부터 운영체제에서 Virtual(가상)이라는 개념이 자주 등장한다.</p>
<p>대표적인 예가 <code>Virtual Memory(가상 메모리)</code>이다. 실제 RAM(Random Access Memory)은 하나이지만, 각 프로그램은 자신만의 독립적인 메모리 공간을 사용하는 것처럼 동작한다. 또한 <code>VFS(Virtual File System)</code>처럼 실제 파일 시스템의 차이를 숨기고, 프로그램이 통일된 방식으로 파일에 접근할 수 있게 해주는 구조도 가상화의 한 형태라고 볼 수 있다.</p>
<p>즉, 하나의 Mainframe Computer(메인프레임 컴퓨터)에서 여러 프로그램이 독립적으로 하드웨어 자원(Hardware Resource)을 사용하는 것처럼 만들어졌고, 이를 통해 여러 프로그램을 동시에 실행할 수 있게 되었다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/af0de1fe-edb8-4968-b743-1b5c996879f0.png" alt="" style="display:block;margin:0 auto" />

<h3>1980s: Personal Computers and Swap (1980년대: 개인용 컴퓨터와 스왑)</h3>
<p>1980년대에는 Personal Computer(개인용 컴퓨터)가 대중화되기 시작했다. 이전에는 회사나 기관에서 주로 사용하던 컴퓨터가 점차 개인 가정에서도 사용되기 시작한 것이다.</p>
<p>이 시기에는 <code>Virtual Memory(가상 메모리)</code> 개념과 함께 <code>Swap(스왑)</code>이 중요해졌다. Swap은 RAM이 부족할 때 Storage(스토리지)의 일부 공간을 마치 메모리처럼 사용하는 방식이다. 예를 들어 RAM 용량이 작아서 더 이상 프로그램을 실행하기 어려운 상황에서, 디스크 공간을 임시 메모리처럼 활용해 하드웨어의 한계를 보완하려는 시도였다.</p>
<p>또한 당시에는 업무용 컴퓨터와 개인용 컴퓨터의 하드웨어 특성이 달랐기 때문에, 같은 프로그램이라도 환경에 맞게 따로 만들어야 하는 문제가 있었다. 서로 다른 컴퓨터 간에 데이터(Data)를 주고받는 것도 지금처럼 자연스럽지 않았기 때문에, 호환성(Compatibility)이 중요한 과제가 되었다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/e8feae6c-d35c-4cdd-9e93-5efa1264db66.png" alt="" style="display:block;margin:0 auto" />

<h3>1990s: Diverse Operating Systems and JVM (1990년대: 다양한 운영체제와 JVM)</h3>
<p>1990년대에는 Windows, Macintosh, OS/2 등 다양한 Operating System(운영체제)이 등장했다. 운영체제와 하드웨어가 다양해지면서, 개발자는 각각의 환경에 맞게 프로그램을 작성하고 <code>Compile(컴파일)</code>해야 하는 어려움을 겪었다.</p>
<p>이 문제를 해결하는 데 중요한 역할을 한 것이 <code>JVM(Java Virtual Machine)</code>이다. JVM은 Java 프로그램이 특정 하드웨어나 운영체제에 직접 의존하지 않고 실행될 수 있도록 해주는 가상 실행 환경이다. 즉, JVM만 설치되어 있다면 같은 Java 프로그램을 여러 운영체제에서 실행할 수 있었다.</p>
<p>이 개념은 당시 매우 혁신적이었고, Java가 인기를 얻는 중요한 이유가 되었다. 또한 Android(안드로이드) 환경에도 가상 머신(Virtual Machine)의 개념이 사용된다.</p>
<p>이후 VMware와 같은 상용 Virtualization Software(가상화 소프트웨어)도 등장했다. 이를 통해 하나의 컴퓨터 안에서 여러 운영체제를 실행하는 환경이 점점 현실화되었다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/5504f383-87de-4d24-919f-4457e495d1d9.png" alt="" style="display:block;margin:0 auto" />

<h3>After the 2000s: Virtual Machines, Containers, and Cloud (2000년대 이후: 가상 머신, 컨테이너, 클라우드)</h3>
<p>2000년대 이후에는 현재 우리가 사용하는 Virtual Machine(가상 머신) 개념이 본격적으로 정착되었다. 예전에는 Multi-booting(멀티 부팅)을 통해 하나의 컴퓨터에 여러 운영체제를 설치할 수는 있었지만, 한 번에 하나의 운영체제만 실행할 수 있었다.</p>
<p>예를 들어 Windows와 Linux를 같은 컴퓨터에 설치해도 동시에 사용할 수는 없었고, 부팅할 때 하나를 선택해야 했다. 또한 각 운영체제의 데이터가 따로 관리되기 때문에 공용으로 사용하려면 별도의 저장 방식이 필요했다.</p>
<p>하지만 Virtual Machine(가상 머신)을 사용하면 하나의 실제 컴퓨터 안에서 여러 운영체제를 동시에 실행할 수 있다. Windows를 사용하면서 Linux를 함께 실행하거나, 실습용 Unix/Linux 환경을 따로 구성하는 것이 가능해진 것이다.</p>
<p>이후 가상화 기술은 <code>Container(컨테이너)</code> 기술로도 발전했다. 컨테이너는 운영체제 전체를 가상화하기보다는, 애플리케이션(Application)이 실행되는 환경을 가볍게 분리하는 방식이다.</p>
<p>이러한 발전은 <code>Cloud Computing(클라우드 컴퓨팅)</code>이 대중화되는 중요한 기반이 되었다. 하나의 물리 서버(Physical Server)를 여러 사용자와 서비스가 나누어 사용할 수 있게 되었고, 필요한 만큼 서버 환경을 빠르게 만들고 관리할 수 있게 되었다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p><code>Virtual Machine(가상 머신)</code>과 <code>Container(컨테이너)</code>는 비슷해 보이지만 차이가 있다. 가상 머신은 운영체제 전체를 포함한 컴퓨터 환경을 가상화하고, 컨테이너는 애플리케이션 실행 환경을 가볍게 분리한다.</p>
<p>간단히 말하면, Virtual Machine은 “가짜 컴퓨터 한 대를 만드는 것”에 가깝고, Container는 “프로그램이 실행될 공간만 따로 분리하는 것”에 가깝다. 클라우드와 서버 개발에서는 두 개념 모두 매우 중요하게 사용된다.</p>
<hr />
<h3>Features, Advantages, and Disadvantages of Virtualization (가상화의 특징, 장점과 단점)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/8745d9e8-3003-4145-a264-b534cd7a2a41.png" alt="" style="display:block;margin:0 auto" />

<h3>Advantages of Virtualization (가상화의 장점)</h3>
<p><code>가상화(Virtualization)</code>의 가장 큰 장점은 자원 사용률(Resource Utilization)을 높일 수 있다는 점이다. 하나의 물리 머신(Physical Machine)에서 여러 개의 가상 머신(Virtual Machine)을 실행할 수 있기 때문에, CPU, Memory, Storage 같은 하드웨어 자원을 쉬는 시간 없이 더 효율적으로 사용할 수 있다.</p>
<p>또한 <code>비용 절감(Cost Reduction)</code>이 가능하다. 예전에는 새로운 컴퓨터 환경이 필요할 때마다 하드웨어를 따로 준비하고, 디스크를 포맷한 뒤 운영체제(Operating System)와 필요한 프로그램을 설치해야 했다. 하지만 가상화 환경에서는 이미 만들어 둔 가상화 이미지(Virtualization Image)를 복제하면 거의 같은 환경을 빠르게 만들 수 있다.</p>
<p>이처럼 가상 머신을 소프트웨어적으로 빠르게 생성할 수 있다는 점은 매우 큰 장점이다. 물리적인 컴퓨터를 새로 구매하거나 설치하지 않아도 되기 때문에 개발 환경, 실습 환경, 테스트 환경을 빠르게 구성할 수 있다.</p>
<p>또 다른 장점은 <code>장애 복구(Failure Recovery)</code>가 쉬워졌다는 점이다. 가상 머신은 이미지나 스냅샷(Snapshot) 형태로 관리할 수 있기 때문에, 문제가 생겼을 때 이전 상태로 되돌리거나 같은 환경을 다시 만드는 과정이 상대적으로 간단하다. 물론 파일 자체가 손상된 경우에는 별도의 복구 과정이 필요하지만, 전체 시스템 환경을 복원하는 작업은 훨씬 쉬워졌다.</p>
<p>이러한 특징들이 모여 <code>클라우드 컴퓨팅(Cloud Computing)</code>을 가능하게 만든 기반이 되었다. 클라우드에서는 하나의 물리 서버(Physical Server)를 여러 가상 환경으로 나누어 사용자에게 제공할 수 있기 때문이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/cd9ef682-1f54-4b54-8584-7d3084caee28.png" alt="" style="display:block;margin:0 auto" />

<h3>Disadvantages of Virtualization (가상화의 단점)</h3>
<p>가상화에는 단점도 있다. 먼저 가상화 소프트웨어(Virtualization Software) 자체가 자원을 사용한다. 실제 하드웨어 위에 가상화 계층(Virtualization Layer)이 하나 더 추가되기 때문에, CPU나 Memory 같은 자원의 일부가 가상화 소프트웨어를 운영하는 데 사용된다.</p>
<p>또한 업무용 환경에서는 <code>추가 비용(Cost)</code>이 발생할 수 있다. 개인 사용자는 무료 가상화 프로그램을 사용할 수 있는 경우가 많지만, 기업 환경에서는 VMware 같은 상용 가상화 솔루션을 사용해야 할 수도 있고, 라이선스 비용이 발생할 수 있다.</p>
<p>마지막으로 가상화 환경을 운영하고 유지관리(Maintenance)하기 위한 전문 인력(Expert Staff)이 필요하다. 가상 머신을 만들고 관리하는 것 자체는 쉬워졌지만, 실제 기업 환경에서는 성능 관리, 장애 대응, 보안 설정, 백업 관리 등이 필요하기 때문에 전문적인 지식이 요구된다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p>가상화의 핵심은 “하드웨어를 더 효율적으로 나누어 쓰는 것”이다. 그래서 서버, 클라우드, 개발 환경에서 매우 중요하게 사용된다.</p>
<p>단점으로는 <code>성능 오버헤드(Overhead)</code>가 있다. Overhead는 어떤 기능을 사용하기 위해 추가로 들어가는 자원이나 비용을 의미한다. 가상화에서는 가상화 소프트웨어가 중간에 들어가기 때문에 약간의 성능 손실이 생길 수 있다. 다만 현대 가상화 기술에서는 이 손실이 많이 줄어들었기 때문에, 장점이 단점보다 큰 경우가 많다.</p>
<hr />
<h2>Basic Terms in Virtualization (가상화 기본 용어)</h2>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/fdf05ddd-94c5-4d9f-853b-34922cc7b6c9.png" alt="" style="display:block;margin:0 auto" />

<h3>Hypervisor / VMM (하이퍼바이저 / 가상 머신 모니터)</h3>
<p><code>Hypervisor(하이퍼바이저)</code> 또는 <code>VMM(Virtual Machine Monitor)</code>은 가상 머신(Virtual Machine)과 실제 하드웨어(Hardware) 사이에서 동작하는 프로그램이다.</p>
<p>가상 머신이 CPU, Memory, Storage 같은 하드웨어 자원을 사용할 수 있도록 관리하며, 가상 머신의 <code>생성(Create), 삭제(Delete), 실행(Run), 중지(Stop)</code> 같은 작업도 담당한다.</p>
<p>즉, Hypervisor는 여러 개의 가상 머신이 하나의 물리 컴퓨터 자원을 나누어 사용할 수 있게 해주는 핵심 소프트웨어이다.</p>
<h3>Virtual Machine, VM (가상 머신)</h3>
<p><code>Virtual Machine(VM, 가상 머신)</code>은 실제 컴퓨터처럼 동작하는 가상의 컴퓨터 환경이다. 실제 하드웨어에 운영체제(Operating System)를 직접 설치한 컴퓨터와 구분하기 위해 VM이라는 용어를 사용한다.</p>
<p>VM은 하나의 물리 컴퓨터 자원을 공유해서 사용하지만, 각 VM은 서로 독립적으로 분리(Isolation)되어 있다. 따라서 일반적으로 한 VM에서 발생한 일이 다른 VM에 직접 영향을 주지 않는다.</p>
<p>각 VM은 별도의 운영체제를 설치하고, 사용자 계정도 따로 만들며, 네트워크에서도 별도의 MAC Address(MAC 주소)를 받을 수 있다. 그래서 사용자는 VM을 마치 독립된 컴퓨터처럼 사용할 수 있다.</p>
<p>물론 설정에 따라 Host와 Guest 사이에 파일 공유나 네트워크 연결을 만들 수 있지만, 기본 개념은 서로 분리된 독립적인 컴퓨터 환경이라고 이해하면 된다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/6eb23a43-551e-4b6e-9d3d-2aaec5c77b9f.png" alt="" style="display:block;margin:0 auto" />

<h3>Host and Guest (호스트와 게스트)</h3>
<p><code>Host(호스트)</code>는 Hypervisor가 설치되어 있는 실제 물리 컴퓨터를 의미한다. 예를 들어 내 노트북에 VirtualBox나 VMware를 설치했다면, 그 노트북이 Host가 된다.</p>
<p><code>Guest(게스트)</code>는 Hypervisor 위에서 실행되는 가상 컴퓨터이다. 즉, VirtualBox 안에 설치한 Linux나 Unix 같은 운영체제가 Guest Operating System(게스트 운영체제)에 해당한다.</p>
<p>Host와 Guest라는 용어를 따로 사용하는 이유는 두 환경의 역할이 다르기 때문이다. Host Operating System(호스트 운영체제)은 실제 컴퓨터에서 실행되는 운영체제이고, Guest Operating System(게스트 운영체제)은 가상 머신 안에서 실행되는 운영체제이다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/826e5f05-2d31-497a-be1c-f5dd91aa3d92.png" alt="" style="display:block;margin:0 auto" />

<h3>Full Virtualization (전가상화)</h3>
<p><code>Full Virtualization(전가상화)</code>은 하드웨어를 거의 완벽하게 흉내 내는 가상화 방식이다. Guest Operating System(게스트 운영체제)은 자신이 가상화 환경에서 실행되고 있다는 사실을 몰라도 된다.</p>
<p>즉, 운영체제를 수정하지 않고 그대로 설치해서 사용할 수 있다. 사용자는 실제 컴퓨터에 운영체제를 설치하는 것과 거의 같은 방식으로 VM에 운영체제를 설치할 수 있다.</p>
<p>이 방식은 사용하기 편하고 이상적인 방식이지만, 하드웨어를 완전히 흉내 내야 하기 때문에 성능 부담(Overhead)이 생길 수 있다.</p>
<h3>Para-virtualization (반가상화)</h3>
<p><code>Para-virtualization(반가상화)</code>은 Guest Operating System이 자신이 가상화 환경에서 실행되고 있다는 사실을 알고 동작하는 방식이다.</p>
<p>이 방식에서는 Guest Operating System의 수정이 필요할 수 있다. 대신 Hypercall(하이퍼콜)을 사용하여 Hypervisor와 더 직접적으로 소통할 수 있기 때문에, 더 좋은 성능을 얻을 수 있다.</p>
<p>Full Virtualization이 “운영체제를 그대로 설치해서 쓰는 방식”이라면, Para-virtualization은 “가상화 환경에 맞게 운영체제가 협력하는 방식”이라고 이해할 수 있다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p>Full Virtualization(전가상화)은 편리하지만 성능 부담이 있을 수 있고, Para-virtualization(반가상화)은 운영체제 수정이 필요할 수 있지만 성능 면에서 유리할 수 있다.</p>
<p>초보 단계에서는 이렇게 기억하면 된다.<br />Full Virtualization은 “그냥 일반 컴퓨터인 것처럼 속여서 실행하는 방식”이고, Para-virtualization은 “가상화 환경임을 알고 협력해서 실행하는 방식”이다.</p>
<hr />
<h2>QEMU Virtualization Environment (QEMU 가상화 환경)</h2>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/6a733b98-04ea-4c74-99ff-c69927289fce.png" alt="" style="display:block;margin:0 auto" />

<h3>What is QEMU? (QEMU란 무엇인가)</h3>
<p>QEMU는 오픈소스(Open Source) 가상화 및 에뮬레이션 도구이다. QEMU는 하드웨어 전체를 가상으로 제공할 수 있으며, 이를 통해 실제 컴퓨터 안에서 또 다른 컴퓨터 환경을 실행할 수 있다.</p>
<p>QEMU는 단순히 운영체제(Operating System)를 실행하는 것뿐만 아니라, 특정 CPU나 하드웨어 환경을 흉내 내는 기능도 제공한다. 그래서 다른 CPU 구조에 맞게 컴파일된 프로그램이나 운영체제를 실행할 수 있다.</p>
<h3>System Emulation (시스템 에뮬레이션)</h3>
<p>System Emulation은 컴퓨터 전체를 가상으로 흉내 내는 방식이다. CPU, Memory, Disk, I/O Device 같은 하드웨어를 가상으로 제공해서, Guest Operating System(게스트 운영체제)이 실제 컴퓨터에 설치된 것처럼 동작하게 만든다.</p>
<p>예를 들어 QEMU가 특정 CPU 모델과 하드웨어 구성을 제공하면, 운영체제는 자신이 실제 그 하드웨어 위에서 실행되고 있다고 인식할 수 있다. 즉, QEMU는 운영체제가 필요로 하는 하드웨어 환경을 소프트웨어적으로 만들어주는 역할을 한다.</p>
<h3>User Mode Emulation (유저 모드 에뮬레이션)</h3>
<p>QEMU는 User Mode Emulation도 지원한다. 이는 운영체제 전체를 실행하는 것이 아니라, 특정 CPU에 맞게 컴파일된 프로그램을 다른 환경에서 실행할 수 있게 해주는 방식이다.</p>
<p>예를 들어 A라는 CPU용으로 만들어진 프로그램이 있다면, QEMU가 A CPU처럼 동작하는 환경을 제공해서 해당 프로그램이 실행될 수 있도록 도와준다. 이때 프로그램은 마치 원래 CPU에서 실행되는 것처럼 동작할 수 있다.</p>
<h3>Example Commands (예시 명령어)</h3>
<p>사진에 나온 명령어는 QEMU에서 가상 머신을 실행하거나 디스크 이미지를 만드는 예시이다.</p>
<p><code>qemu-system-x86_64 --machine pc --cpu max --smp 4 --m 2048</code></p>
<p>이 명령어는 x86_64 아키텍처용 가상 머신을 실행하는 명령어이다. <code>--machine pc</code>는 일반 PC 형태의 가상 머신을 사용한다는 의미이고, <code>--cpu max</code>는 가능한 CPU 기능을 최대한 사용하겠다는 의미이다. <code>--smp 4</code>는 CPU 코어를 4개 사용한다는 뜻이며, <code>--m 2048</code>은 메모리 2048MB, 즉 2GB를 할당한다는 의미이다.</p>
<p><code>qemu-img create -f raw img_disk 10G</code></p>
<p>이 명령어는 10GB 크기의 디스크 이미지(Disk Image)를 만드는 명령어이다. <code>-f raw</code>는 raw 형식의 이미지 파일을 만들겠다는 뜻이고, <code>img_disk</code>는 생성될 이미지 파일의 이름이다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p>QEMU는 단독으로도 사용할 수 있지만, Linux 환경에서는 <code>KVM(Kernel-based Virtual Machine)</code>과 함께 사용하면 성능이 더 좋아질 수 있다. QEMU가 하드웨어를 가상으로 제공하고, KVM이 실제 CPU의 가상화 기능을 활용해 실행 속도를 높여주는 방식이다.</p>
<p>초보 단계에서는 QEMU를 “가상의 컴퓨터 하드웨어를 만들어주는 도구”라고 이해하면 충분하다.</p>
<hr />
<h2>KVM and Virtualization Tools (KVM과 가상화 도구)</h2>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/b380291c-5678-4800-82e6-0805dd58e8f1.png" alt="" style="display:block;margin:0 auto" />

<h3>KVM (Kernel-based Virtual Machine)</h3>
<p><code>KVM(Kernel-based Virtual Machine)</code>은 리눅스 커널(Linux Kernel)에 포함되어 있는 오픈소스(Open Source) 가상화 기술이다. KVM은 별도의 외부 프로그램처럼만 존재하는 것이 아니라, 리눅스 커널 소스 코드 안에 포함되어 함께 배포된다. 따라서 리눅스 커널 소스를 내려받으면 KVM 관련 소스도 함께 포함되어 있다고 볼 수 있다.</p>
<p>KVM은 Intel VT(Intel Virtualization Technology) 또는 AMD-V(AMD Virtualization) 같은 하드웨어 가상화 기능을 지원하는 x86 하드웨어에서 Full Virtualization(전가상화)을 제공한다. 즉, 게스트 운영체제(Guest Operating System)를 특별히 수정하지 않고도 가상 머신(Virtual Machine) 안에 설치해 사용할 수 있다.</p>
<h3>KVM as a Kernel Module (커널 모듈로서의 KVM)</h3>
<p>KVM은 리눅스의 커널 모듈(Kernel Module) 형태로 동작할 수 있다. 커널 모듈은 리눅스 커널에 필요한 기능을 추가로 붙여 사용하는 방식이다.</p>
<p>즉, KVM은 리눅스 커널 수준에서 CPU의 가상화 기능을 활용하여 가상 머신이 더 효율적으로 실행되도록 돕는다. 그래서 단순히 소프트웨어로 모든 하드웨어를 흉내 내는 방식보다 성능 면에서 유리하다.</p>
<h3>virt-install (가상 머신 설치 도구)</h3>
<p>사진에 나온 <code>virt-install</code>은 KVM 자체가 아니라, 가상 머신을 생성하고 설치할 때 사용하는 관리 도구이다. 이 도구를 사용하면 명령어로 가상 머신의 이름, CPU 개수, RAM 용량, 디스크 이미지, 설치 ISO, 네트워크 설정 등을 지정할 수 있다.</p>
<p>예시 명령어에서는 <code>ubuntu24-vm</code>이라는 이름의 가상 머신을 만들고, CPU 2개, RAM 4096MB, 디스크 20GB를 할당한다. 또한 Ubuntu 24.04 ISO 파일을 CD-ROM처럼 연결하여 운영체제를 설치할 수 있게 한다.</p>
<p>여기서 중요한 점은 실제 컴퓨터에 물리적인 CD-ROM이 없어도, ISO 이미지(ISO Image)를 가상 CD-ROM처럼 연결해서 운영체제를 설치할 수 있다는 것이다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p>QEMU와 KVM은 함께 사용되는 경우가 많다. QEMU는 가상 하드웨어를 제공하고, KVM은 리눅스 커널의 가상화 기능을 이용해 실행 성능을 높여준다.</p>
<p>초보 단계에서는 KVM을 “리눅스 커널이 제공하는 빠른 가상화 기능”, <code>virt-install</code>을 “가상 머신을 만드는 명령어 도구”로 이해하면 충분하다.</p>
<p><code>KVM(Kernel-based Virtual Machine)</code>: 리눅스 커널에 포함된 오픈소스 가상화 기술</p>
<p><code>Linux Kernel(리눅스 커널)</code>: 리눅스 운영체제의 핵심 부분</p>
<p><code>Kernel Module(커널 모듈)</code>: 커널에 기능을 추가하기 위해 사용하는 모듈</p>
<p><code>Intel VT(Intel Virtualization Technology)</code>: 인텔 CPU에서 제공하는 하드웨어 가상화 기능</p>
<p><code>AMD-V(AMD Virtualization)</code>: AMD CPU에서 제공하는 하드웨어 가상화 기능</p>
<p><code>Full Virtualization(전가상화)</code>: 게스트 운영체제를 수정하지 않고 그대로 실행할 수 있는 가상화 방식</p>
<p><code>virt-install</code>: 명령어로 가상 머신을 생성하고 운영체제를 설치하는 도구</p>
<p><code>ISO Image(ISO 이미지)</code>: CD/DVD 내용을 파일 하나로 만든 이미지 파일</p>
<p><code>Guest Operating System(게스트 운영체제)</code>: 가상 머신 안에 설치되어 실행되는 운영체제</p>
<hr />
<h2>Containers and Virtualization (컨테이너와 가상화)</h2>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/c325275d-c260-4995-b52f-e69dfb1cd192.png" alt="" style="display:block;margin:0 auto" />

<h3>What is a Container? (컨테이너란 무엇인가)</h3>
<p>컨테이너(Container)는 말 그대로 하나의 “박스”처럼 생각할 수 있다. 프로그램을 실행하는 데 필요한 파일, 라이브러리(Library), 설정(Configuration), 실행 환경(Runtime Environment)을 하나로 묶어 <code>패키징(Packaging)</code>한 것이다.</p>
<p>가상 머신(Virtual Machine)은 보통 운영체제(Operating System) 전체를 포함해서 하나의 컴퓨터처럼 동작한다. 반면 컨테이너(Container)는 운영체제 전체를 새로 설치하기보다는, 응용 프로그램(Application)을 실행하는 데 필요한 최소한의 요소만 담는 방식으로 발전했다.</p>
<h3>Minimal Environment (최소 실행 환경)</h3>
<p>수업에서 리눅스(Linux)를 설치할 때 서버 버전(Server Version)을 minimal로 설치하면, 일반 설치보다 프로그램이 적게 설치된다. 이는 서버에서 굳이 필요하지 않은 그래픽 환경(GUI), 로그인 도구, 여러 응용 프로그램 등을 제외하고 꼭 필요한 기능만 남긴 것이다.</p>
<p>컨테이너(Container)도 이와 비슷한 방향으로 이해할 수 있다. 초기에는 컨테이너 안에 운영체제에 가까운 구성 요소가 많이 포함되기도 했지만, 최근에는 응용 프로그램(Application)과 실행에 필요한 라이브러리(Library), 도구(Tool)만 담는 가벼운 방식으로 많이 사용된다.</p>
<p>즉, 컨테이너는 “프로그램 실행에 필요한 것만 담은 작은 실행 상자”라고 볼 수 있다.</p>
<h3>Difference Between VM and Container (가상 머신과 컨테이너의 차이)</h3>
<p><code>가상 머신(Virtual Machine)</code>은 운영체제 전체를 포함한 독립적인 컴퓨터 환경을 만든다. 그래서 VM 안에는 게스트 운영체제(Guest Operating System)가 설치되고, 그 위에서 프로그램이 실행된다.</p>
<p>반면 <code>컨테이너(Container)</code>는 보통 운영체제 전체를 포함하지 않고, 호스트 운영체제(Host Operating System)의 커널(Kernel)을 공유하면서 실행된다. 대신 각 컨테이너는 서로 분리(Isolation)되어 독립적으로 동작한다.</p>
<p>이 때문에 컨테이너는 가상 머신보다 용량이 작고, 실행 속도가 빠르며, 필요한 환경을 더 쉽게 배포할 수 있다.</p>
<h3>Containers and Microservices (컨테이너와 마이크로서비스)</h3>
<p>컨테이너(Container)는 <code>마이크로서비스(Microservices)</code> 구조에서도 많이 사용된다. 마이크로서비스는 하나의 큰 프로그램을 여러 개의 작은 서비스(Service)로 나누어 독립적으로 관리하는 방식이다.</p>
<p>예를 들어 하나의 컨테이너에는 웹 서버(Web Server)만 넣고, 다른 컨테이너에는 데이터베이스(Database)를 넣고, 또 다른 컨테이너에는 컴파일(Compile)이나 코드 관리 도구를 넣을 수 있다. 이렇게 각 기능을 작은 단위로 나누면 개발, 배포, 유지보수가 더 쉬워진다.</p>
<p>즉, 컨테이너는 각각 독립적으로 실행되는 작은 실행 환경이고, 마이크로서비스는 이런 작은 실행 환경들을 조합해 전체 프로그램을 구성하는 방식이라고 이해할 수 있다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p>가상 머신(Virtual Machine)은 “컴퓨터 한 대를 통째로 가상으로 만드는 것”에 가깝고, 컨테이너(Container)는 “프로그램 실행에 필요한 환경만 가볍게 포장하는 것”에 가깝다.</p>
<p>그래서 운영체제 실습처럼 전체 OS 환경이 필요할 때는 가상 머신이 적합하고, 실제 서비스 배포나 개발 환경 분리에는 컨테이너가 많이 사용된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/42f57a6d-ae42-4b90-a67f-bc83cc1e90b5.png" alt="" style="display:block;margin:0 auto" />

<h2>NetBSD and BSD Family Overview (NetBSD와 BSD 계열 운영체제 개요)</h2>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/9e23b59f-929b-40d9-bcbb-9f032181fe99.png" alt="" style="display:block;margin:0 auto" />

<h3>Origin of NetBSD’s Name (NetBSD 이름의 유래)</h3>
<p>NetBSD는 BSD 계열에서 발전한 <code>Unix-like Operating System(유닉스 계열 운영체제)</code>이다. 쉽게 말하면, Unix의 영향을 받은 BSD를 기반으로 만들어진 무료 오픈소스 운영체제(Open Source Operating System)라고 이해할 수 있다.</p>
<p>NetBSD라는 이름에서 Net은 Internet(인터넷)과 관련이 있다. NetBSD 프로젝트는 인터넷을 통해 협업하고, 이메일과 네트워크 기반 버전 관리 시스템(Version Control System)을 사용해 운영체제를 개발한 초기 오픈소스 프로젝트 중 하나였다. 그래서 Internet이 프로젝트를 가능하게 한 중요한 기반이 되었고, 이를 반영해 이름에 Net이 들어가게 되었다.</p>
<p>BSD는 Berkeley Software Distribution의 약자로, Unix에서 파생된 운영체제 계열을 의미한다. 따라서 NetBSD라는 이름은 Internet을 기반으로 협업한 BSD 계열 운영체제라는 의미를 담고 있다.</p>
<h3>NetBSD and Real-World Usage (NetBSD와 실제 활용)</h3>
<p>NetBSD는 소프트웨어와 하드웨어 제품 개발에 활용될 수 있다. 강의에서는 예시로 Apple의 Darwin, Sony PlayStation, Apple AirPods 등이 언급되었다. 즉, BSD 계열 운영체제나 그 구조는 실제 상용 제품과 <code>임베디드 장치(Embedded Device)</code>에서도 사용될 수 있다.</p>
<p>여기서 중요한 점은 NetBSD가 단순히 학습용 운영체제가 아니라, 다양한 하드웨어 환경에서 동작할 수 있도록 설계된 운영체제라는 것이다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/82eda2fa-4653-4971-b8ee-6ae702ebdf96.png" alt="" style="display:block;margin:0 auto" />

<h3>NetBSD, FreeBSD, and OpenBSD</h3>
<p>BSD 계열에는 대표적으로 <code>NetBSD, FreeBSD, OpenBSD</code>가 있다. 모두 Unix-like Operating System이지만, 중점적으로 추구하는 방향이 조금씩 다르다.</p>
<p>NetBSD는 <code>Portability(이식성)</code>와 <code>Clean Code(깔끔한 코드)</code>를 중요하게 여긴다. 다양한 하드웨어에서 동작할 수 있도록 설계되어 있으며, 임베디드 시스템(Embedded System)이나 특정 장치 환경에서 활용될 수 있다.</p>
<p>FreeBSD는 <code>Performance(성능)</code>와 <code>Scalability(확장성)</code>에 강점이 있다. 서버(Server) 환경에서 안정적으로 동작하는 데 초점이 있으며, 상용 서비스나 서버 운영 환경에 적합하다. 다만 실제 서버 운영에서는 Linux가 더 익숙하거나 편하다면 Linux를 사용해도 큰 문제는 없다.</p>
<p>OpenBSD는 <code>Security(보안)</code>와 <code>Correctness(정확성)</code>에 중점을 둔다. 보안성이 중요한 시스템, 예를 들어 Firewall(방화벽), IPS(Intrusion Prevention System, 침입 방지 시스템) 같은 보안 장비를 만들 때 활용될 수 있다. 직접 운영체제를 새로 개발하는 것은 어렵고 시간이 많이 들기 때문에, 이미 검증된 OpenBSD 같은 운영체제를 기반으로 사용할 수 있다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p>BSD 계열과 Linux는 모두 Unix-like Operating System으로 분류되지만, 역사와 라이선스(License), 개발 방식에는 차이가 있다. 처음 학습할 때는 “Linux와 BSD는 명령어와 사용 방식이 비슷한 Unix 계열 운영체제”라고 이해하면 충분하다.</p>
<p>NetBSD, FreeBSD, OpenBSD를 간단히 구분하면 다음처럼 기억할 수 있다. NetBSD는 다양한 장치에서 동작하는 이식성 중심, FreeBSD는 서버 성능 중심, OpenBSD는 보안 중심이다.</p>
<hr />
<h2>Preparing to Install NetBSD on Linux (Linux에서 NetBSD 설치 준비)</h2>
<h3>Installation Environment (설치 환경)</h3>
<p>이번 실습에서는 Linux(리눅스) 환경 안에서 Unix-like Operating System(유닉스 계열 운영체제)인 NetBSD를 설치하는 준비 과정을 다룬다. 굳이 Linux 안에서 NetBSD를 설치하는 이유는 이 과목이 Linux를 중심으로 진행되는 수업이기 때문이다.</p>
<p>현재 실습 구조를 보면, 실제 하드웨어(Hardware) 위에 Windows 운영체제가 있고, 그 위에 VirtualBox(버추얼박스)를 설치한 뒤 Linux를 실행하고 있다. 그리고 다시 그 Linux 안에서 QEMU를 이용해 NetBSD를 설치하려는 구조이다.</p>
<p>즉, NetBSD는 실제 물리 컴퓨터에 직접 설치되는 것이 아니라, QEMU가 제공하는 가상 하드웨어(Virtual Hardware) 위에서 실행된다. 엄밀히 말하면 Linux는 VirtualBox가 보여주는 가상 하드웨어를 사용하고, NetBSD는 Linux 안에서 QEMU가 보여주는 또 다른 가상 하드웨어를 사용하는 것이다.</p>
<h3>Step 1: Install QEMU (1단계: QEMU 설치)</h3>
<p>첫 번째 준비 단계는 QEMU를 설치하는 것이다. QEMU는 가상 하드웨어를 제공해주는 오픈소스(Open Source) 가상화 및 에뮬레이션 도구이다.</p>
<p>Linux에서 QEMU를 설치하기 위해 다음 명령어를 사용할 수 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/dec28335-f0ec-46b5-931b-16999c4b518c.png" alt="" style="display:block;margin:0 auto" />

<p>여기서 <code>sudo</code>는 관리자 권한으로 명령어를 실행한다는 의미이고, <code>apt install</code>은 Ubuntu 계열 Linux에서 패키지를 설치할 때 사용하는 명령어이다. <code>qemu-system</code>은 시스템 전체를 에뮬레이션하거나 가상 머신을 실행하는 데 필요한 QEMU 패키지이다.</p>
<h3>Step 2: Download NetBSD Image (2단계: NetBSD 이미지 다운로드)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/51c63f02-8a65-4d64-924b-e17b155befd3.png" alt="" style="display:block;margin:0 auto" />

<p>두 번째 단계는 NetBSD 설치 이미지(Image)를 다운로드하는 것이다. NetBSD 공식 웹사이트에 접속해서 자신의 가상 하드웨어 환경에 맞는 이미지를 선택해 다운로드한다.</p>
<p>NetBSD는 다양한 하드웨어에서 동작하는 것을 목표로 하는 운영체제이기 때문에, 다운로드할 때 어떤 CPU 아키텍처(Architecture)나 시스템용 이미지를 받을지 선택해야 한다. 일반적인 PC 환경이라면 x86 또는 amd64 계열 이미지를 선택하는 경우가 많다.</p>
<p>다운로드한 NetBSD 이미지 파일은 이후 QEMU에서 가상 CD-ROM이나 설치 디스크처럼 사용된다. 즉, 실제 CD나 USB 없이도 이미지 파일을 이용해 운영체제를 설치할 수 있다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p>이번 실습 구조는 “가상 머신 안에서 또 다른 가상 머신을 실행하는 구조”에 가깝다. 그래서 실제 컴퓨터에 직접 설치하는 것보다 성능이 느릴 수 있다.</p>
<p>하지만 실습 목적에서는 안전하게 여러 운영체제를 설치하고 삭제해볼 수 있다는 장점이 있다. 운영체제 설치 실습에서는 실제 컴퓨터를 건드리지 않고 가상 환경에서 연습하는 것이 훨씬 안전하다.</p>
<hr />
<h3>Creating and Running a NetBSD Virtual Machine with QEMU (QEMU로 NetBSD 가상 머신 생성 및 실행)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/e1897339-799f-46bd-aa94-c7ac3a742392.png" alt="" style="display:block;margin:0 auto" />

<h3>Creating a Virtual Disk Image (가상 디스크 이미지 만들기)</h3>
<p>이제 NetBSD를 설치하기 위한 실제 단계로 들어간다. 운영체제(Operating System)를 설치하려면 CPU, RAM, Disk 같은 하드웨어 자원이 필요하다. QEMU에서는 실제 하드디스크 대신 디스크 이미지(Disk Image) 파일을 만들어 가상 하드디스크처럼 사용한다.</p>
<p>예시 명령어는 다음과 같다.</p>
<pre><code class="language-plaintext">qemu-img create -f qcow2 netbsd.qcow2 4G
</code></pre>
<p>이 명령어는 <code>netbsd.qcow2</code>라는 이름의 4GB짜리 가상 디스크를 만든다. 여기서 <code>qcow2</code>는 QEMU에서 자주 사용하는 디스크 이미지 형식이다. 지금 단계에서는 qcow2의 내부 구조까지 자세히 알 필요는 없고, “가상 머신이 사용할 하드디스크 파일”이라고 이해하면 충분하다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/66751c2f-9a90-4f47-b3aa-7e4735bbebf5.png" alt="" style="display:block;margin:0 auto" />

<h3>Running the QEMU Virtual Machine (QEMU 가상 머신 실행하기)</h3>
<p>디스크 이미지를 만들었다면, 다음 단계는 QEMU로 가상 머신(Virtual Machine)을 실행하는 것이다.</p>
<p>예시 명령어는 다음과 같다.</p>
<pre><code class="language-plaintext">qemu-system-x86_64 -m 512m -smp 1 -drive file=netbsd.qcow2,format=qcow2 \
-cdrom NetBSD-10.1-amd64.iso -boot d -net nic,model=virtio -net user
</code></pre>
<p>이 명령어는 x86_64 구조의 가상 머신을 실행하고, RAM은 512MB, CPU는 1개로 설정한다. <code>-drive</code> 옵션은 앞에서 만든 <code>netbsd.qcow2</code> 디스크 이미지를 가상 하드디스크로 연결하는 부분이다.</p>
<p><code>-cdrom NetBSD-10.1-amd64.iso</code>는 다운로드한 NetBSD ISO 이미지를 가상 CD-ROM처럼 연결하는 옵션이다. 실제 CD-ROM을 사용하지 않더라도, ISO 파일을 CD처럼 인식시켜 운영체제를 설치할 수 있다.</p>
<p><code>-boot d</code>는 CD-ROM에서 먼저 부팅하겠다는 의미이다. 즉, NetBSD 설치 ISO로 부팅해서 가상 디스크에 NetBSD를 설치하는 흐름이다.</p>
<h2>GUI Display Issue and SSH X Forwarding (GUI 화면 문제와 SSH X 포워딩)</h2>
<p>QEMU를 실행했을 때 <code>gtk initialization failed</code> 같은 메시지가 보일 수 있다. 이는 QEMU가 그래픽 화면(GUI, Graphical User Interface)을 띄우려고 했지만, 현재 환경에서 그래픽 화면을 표시할 수 없을 때 발생할 수 있다.</p>
<p>SSH 접속은 기본적으로 텍스트 기반 CLI(Command Line Interface) 접속이다. 하지만 QEMU처럼 그래픽 창을 띄워야 하는 프로그램은 화면 정보를 내 컴퓨터로 전달해야 한다. 이때 사용하는 것이 <code>X Forwarding(X 포워딩)</code>이다.</p>
<p>원격 Linux에 SSH로 접속해서 QEMU 그래픽 화면을 띄우려면 <code>ssh -Y</code> 또는 <code>ssh -X</code> 옵션을 사용할 수 있다. Windows에서 접속하는 경우에는 Xming 같은 X Server(X 서버)가 필요하고, macOS에서는 XQuartz를 사용할 수 있다.</p>
<p>X Server는 원격에서 전달된 그래픽 화면 정보를 내 컴퓨터에서 표시해주는 역할을 한다. 즉, SSH로 접속한 원격 서버에서 실행한 GUI 프로그램을 내 컴퓨터 화면에 띄울 수 있게 해준다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p>지금 단계에서 중요한 것은 명령어 옵션을 모두 외우는 것이 아니라, QEMU가 “가상의 컴퓨터”를 만들기 위해 CPU, RAM, Disk, CD-ROM 같은 요소를 옵션으로 지정한다는 흐름을 이해하는 것이다.</p>
<p>간단히 정리하면 <code>qemu-img</code>는 가상 디스크를 만드는 명령어이고, <code>qemu-system-x86_64</code>는 실제로 가상 머신을 실행하는 명령어이다.</p>
<hr />
<h1>NetBSD Installation Steps: Boot and Disk Setup</h1>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/41af7ff3-67f1-45b3-b796-2aadf608a473.png" alt="" style="display:block;margin:0 auto" />

<h3>Starting the NetBSD Installer (NetBSD 설치 프로그램 시작)</h3>
<p>앞에서 만든 QEMU 가상 머신(QEMU Virtual Machine) 실행 명령어를 입력하면 NetBSD 설치 화면이 나타난다. 처음 부팅 화면에서는 <code>1. Boot normally</code>를 선택하면 된다. 이는 특별한 복구 모드나 옵션 없이 일반적인 방식으로 NetBSD 설치 환경을 시작한다는 의미이다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/55eaa145-988c-466a-85a2-c1137553a282.png" alt="" style="display:block;margin:0 auto" />

<p>부팅 후 파란색 설치 화면이 나타나면 언어 선택 화면에서 <code>Installation messages in English</code>를 선택한다. 한국어는 선택지에 없는데, 이는 NetBSD 사용자가 상대적으로 많지 않고 한국어 번역 지원이 제공되지 않기 때문이라고 이해할 수 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/44230afa-0c87-4d83-9a48-d7ae871cc76f.png" alt="" style="display:block;margin:0 auto" />

<p>그 다음 Keyboard Type(키보드 종류) 선택 화면에서는 특별히 바꿀 필요가 없다면 <code>unchanged</code>를 선택한다. 만약 본인이 사용하는 키보드 배열이 다르다면 해당하는 옵션을 선택하면 된다.</p>
<h3>Choosing Installation to Hard Disk (하드디스크 설치 선택)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/87e2c1b1-5d10-46c3-b06e-46c1a5c5515a.png" alt="" style="display:block;margin:0 auto" />

<p>다음으로 <code>NetBSD-10.1 Install System</code> 화면이 나오면 NetBSD를 하드디스크(Hard Disk)에 설치하는 옵션을 선택한다. 여기서 말하는 하드디스크는 실제 컴퓨터의 물리 디스크가 아니라, 앞에서 만든 <code>netbsd.qcow2</code> 가상 디스크 이미지(Disk Image)를 의미한다.</p>
<p>QEMU에서는 이 이미지 파일을 가상 하드디스크처럼 보여주기 때문에, NetBSD 설치 프로그램은 마치 실제 디스크에 운영체제를 설치하는 것처럼 인식한다.</p>
<h3>Partition Concept (파티션 개념)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/ab5a0f2a-3f0f-4401-b6e7-996e69156399.png" alt="" style="display:block;margin:0 auto" />

<p>설치 과정에서 디스크를 변경해도 되는지 묻는 화면이 나온다. 운영체제를 설치하려면 디스크에 파일 시스템(File System)을 만들고, 필요한 영역을 나누는 작업이 필요하기 때문이다.</p>
<p>디스크(Disk)는 하나의 저장 공간이지만, 필요에 따라 여러 구역으로 나누어 사용할 수 있다. 이 구역을 Partition(파티션)이라고 한다. 예전에는 디스크 전체를 하나의 운영체제가 모두 사용하는 경우가 많았지만, 저장 장치의 용량이 커지면서 하나의 디스크를 여러 용도로 나누어 사용하는 방식이 중요해졌다.</p>
<p>예를 들어 하나의 디스크 안에서 일부는 NetBSD, 일부는 Linux, 일부는 Windows용으로 나누어 사용할 수 있다. 이렇게 나누어진 각각의 영역이 파티션이다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/14b0890d-ce74-4083-a34e-c389b2ca592a.png" alt="" style="display:block;margin:0 auto" />

<h3>Boot Sector and GPT (부트 섹터와 GPT)</h3>
<p>운영체제를 설치할 때는 단순히 파일만 복사하는 것이 아니라, 컴퓨터가 어디에서 부팅해야 하는지도 정해야 한다. 이와 관련된 개념이 Boot Sector(부트 섹터)와 Partition Table(파티션 테이블)이다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/b3cd109f-5d54-40ce-b333-dcf88c4b258c.png" alt="" style="display:block;margin:0 auto" />

<p>이번 설치에서는 디스크 크기를 앞에서 만든 그대로 4GB로 사용한다. 이후 파티션 방식으로는 기본값인 GPT(Guid Partition Table)를 선택한다. GPT는 현대적인 파티션 테이블 방식으로, UEFI 환경에서 많이 사용된다.</p>
<p>지금 단계에서는 GPT의 세부 구조를 모두 이해할 필요는 없고, “디스크를 어떻게 나누고 부팅 정보를 어떻게 관리할지 정하는 방식”이라고 이해하면 충분하다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p>이번 실습에서 디스크를 변경한다고 해도 실제 Windows나 Linux 디스크가 바뀌는 것은 아니다. QEMU 안에서 사용하는 <code>netbsd.qcow2</code> 파일만 가상 하드디스크로 사용되기 때문에 비교적 안전하게 실습할 수 있다.</p>
<p>다만 설치 과정에서는 항상 “내가 지금 실제 디스크를 건드리는지, 가상 디스크를 건드리는지”를 구분하는 습관이 중요하다. 실제 운영체제 설치에서는 이 부분을 잘못 선택하면 기존 데이터가 삭제될 수 있기 때문이다.</p>
<hr />
<h2>NetBSD Disk Partitioning and Swap</h2>
<h3>Disk Geometry and Partition Setup (디스크 구조와 파티션 설정)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f00cb121-bb75-4b83-b2d1-c0be8f2eec4c.png" alt="" style="display:block;margin:0 auto" />

<p>NetBSD 설치 과정에서 디스크 크기를 4GB로 설정한 뒤에는 <code>This is the correct geometry</code>를 선택하면 된다. 여기서 말하는 geometry는 디스크 구조를 의미하는데, 이번 실습에서는 QEMU에서 만든 가상 디스크(Disk Image)를 사용하므로 기본값을 그대로 사용하면 된다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/765492d0-6cd1-48c1-95cb-38fb71d8185b.png" alt="" style="display:block;margin:0 auto" />

<p>그다음에는 NetBSD에 맞게 디스크 파티션(Partition)을 설정하는 단계가 나온다. 실제 하드디스크라면 기존 파티션을 삭제하거나, 직접 크기를 조정하거나, 다른 운영체제와 나누어 사용하는 등 여러 선택지가 있을 수 있다. 하지만 이번 실습에서는 새로 만든 4GB 가상 디스크를 사용하므로 자동 설정 옵션인 <code>Set sizes of NetBSD partitions</code>를 선택하면 된다.</p>
<p>파티션(Partition)이란 하나의 디스크(Disk)를 여러 구역으로 나누어 사용하는 것이다. 예를 들어 하나의 디스크 안에 일부는 NetBSD, 일부는 Linux, 일부는 Windows용으로 나누어 사용할 수 있다. 운영체제를 설치할 때는 어느 영역에 시스템을 설치할지, 부팅 정보는 어디에 둘지 등을 정해야 한다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/fdb3cd76-5b89-4cf6-a673-4d8c763b7c90.png" alt="" style="display:block;margin:0 auto" />

<h3>Swap Partition (스왑 파티션)</h3>
<p>파티션 설정 화면을 보면 Swap(스왑)이라는 항목이 보인다. Swap은 RAM(Random Access Memory)이 부족할 때 디스크(Storage)의 일부 공간을 임시 메모리처럼 사용하는 기능이다.</p>
<p>예를 들어 가상 머신에 RAM을 512MB로 설정했는데 여러 프로그램이 실행되면서 메모리가 부족해질 수 있다. 이때 당장 자주 사용하지 않는 메모리 내용을 디스크의 Swap 영역으로 옮기고, RAM 공간을 비워 다른 작업을 계속할 수 있게 한다.</p>
<p>물론 디스크는 RAM보다 훨씬 느리기 때문에 Swap을 사용하면 성능은 느려질 수 있다. 하지만 메모리가 부족해서 프로그램을 아예 실행하지 못하는 것보다는, 느리더라도 디스크를 보조 메모리처럼 사용하는 것이 더 나은 선택이 될 수 있다.</p>
<h3>Swap and Modern Systems (스왑과 현대 시스템)</h3>
<p>과거에는 RAM 용량이 작았기 때문에 Swap이 매우 중요했다. 예를 들어 32비트(32-bit) 시스템에서는 주소 지정(Addressing)의 한계 때문에 사용할 수 있는 메모리에 제한이 있었다. 그래서 부족한 메모리를 보완하기 위해 Swap을 자주 사용했다.</p>
<p>하지만 현재는 64비트(64-bit) 시스템이 일반적이고, 컴퓨터에 장착되는 RAM 용량도 매우 커졌다. 그래서 일반적인 데스크톱 환경에서는 예전만큼 Swap을 적극적으로 사용하지 않는 경우도 있다. 그럼에도 서버나 가상 머신 환경에서는 메모리 부족 상황을 대비해 Swap 영역을 설정하는 것이 여전히 의미가 있다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p>이번 실습에서는 실제 디스크가 아니라 QEMU의 가상 디스크를 사용한다. 따라서 파티션을 설정하거나 포맷해도 실제 Windows나 Linux 디스크가 변경되는 것은 아니다.</p>
<p>Swap은 “RAM을 늘려주는 마법 같은 기능”이라기보다는, RAM이 부족할 때 디스크를 임시 대기 공간으로 사용하는 기능이다. 그래서 시스템 안정성에는 도움이 되지만, 성능 자체는 RAM보다 느리다는 점을 함께 기억하면 좋다.</p>
<hr />
<h2>NetBSD Installation Configuration</h2>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/6b62af46-e729-4f62-8ca1-643a0a183454.png" alt="" style="display:block;margin:0 auto" />

<h3>Partition Size Check (파티션 크기 확인)</h3>
<p>NetBSD 설치 과정에서 디스크 설정을 진행하면 <code>free space 2131 MB</code>와 같은 문구가 보일 수 있다. 이는 해당 공간이 아직 사용되지 않은 빈 공간(Free Space)으로 남아 있다는 의미이다. 이번 실습에서는 특별히 수정하지 않고 그대로 진행하면 된다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/632e2572-0e66-47e3-8a4c-6f8faa7c5285.png" alt="" style="display:block;margin:0 auto" />

<p>이후 파티션 크기(Partition Size)를 확인하는 화면에서 <code>a: 3583</code>, <code>b: 512</code>와 같은 값이 보인다. 여기서 <code>a</code>는 NetBSD가 설치될 주요 파일 시스템(File System) 영역이고, <code>b</code>는 스왑(Swap) 영역으로 이해할 수 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/8fa9fe13-8f40-4cca-8499-34ada3c489f4.png" alt="" style="display:block;margin:0 auto" />

<p>설정이 괜찮다면 <code>Partition sizes ok</code>를 선택하고, 이어서 <code>Shall we continue?</code> 질문에는 <code>Yes</code>를 선택한다.</p>
<h3>Console and Installation Type (콘솔과 설치 방식 선택)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/28c6053c-45f3-4b41-87f4-150b50401b45.png" alt="" style="display:block;margin:0 auto" />

<p>다음 단계에서는 콘솔(Console)을 선택한다. 여기서는 <code>Use BIOS console</code>을 선택하면 된다. 이는 설치 화면을 어떤 방식으로 표시할지 정하는 부분이다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/98f3b125-73ce-461a-9af4-781321bf51a4.png" alt="" style="display:block;margin:0 auto" />

<p>그다음 <code>Select your distribution</code> 화면에서는 <code>Full installation</code>을 선택한다. 처음 설치하는 단계에서는 굳이 구성 요소를 제외할 필요가 없기 때문에 전체 설치를 선택하는 것이 좋다. 필요에 따라 가벼운 설치를 원한다면 minimal 설치를 선택할 수도 있다.</p>
<h3>Installation Source (설치 소스 선택)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/6d2787e4-b48e-4d31-9dc8-f72017fef3bf.png" alt="" style="display:block;margin:0 auto" />

<p>설치 파일을 어디에서 가져올지 선택하는 화면에서는 <code>CD-ROM / DVD / install image media</code>를 선택한다. 앞에서 QEMU 실행 명령어를 통해 NetBSD ISO Image(ISO 이미지)를 가상 CD-ROM(Virtual CD-ROM)으로 연결했기 때문이다.</p>
<p>만약 네트워크를 통해 설치하려면 HTTP, FTP, NFS 같은 옵션을 사용할 수도 있다. 하지만 이번 실습에서는 이미 다운로드한 ISO 이미지를 사용하므로 CD-ROM 방식으로 진행한다.</p>
<h3>Password and Device Setup (비밀번호와 장치 설정)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/205cdf90-b157-4aff-903c-79e389511188.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/1cc98c34-6f17-4526-bf9c-5a6c16a7f497.png" alt="" style="display:block;margin:0 auto" />

<p>설치가 진행된 뒤 <code>Hit enter to continue</code> 메시지가 나오면 Enter를 눌러 다음 단계로 넘어간다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/aa73ba2c-0f43-4084-b925-ad02d0fa503e.png" alt="" style="display:block;margin:0 auto" />

<p>이후 Root Password(루트 비밀번호)를 설정하는 화면이 나온다. 루트 계정(root account)은 시스템 관리자 권한을 가진 계정이므로, 설치 후 관리 작업에 사용된다.</p>
<p>그 다음 USB 장치 등을 지금 테스트할 것인지 묻는 화면이 나오면 <code>Not now, continue!</code>를 선택한다. 이는 지금 바로 외부 장치를 연결해 확인하지 않고, 다음 단계로 넘어가겠다는 의미이다.</p>
<h3>Final Configuration and SSHD (최종 설정과 SSHD)</h3>
<p>마지막으로 지금까지 선택한 설정을 확인하는 화면이 나온다. 여기서 파티션 설정, 설치할 프로그램, 시스템 설정 등을 확인할 수 있다.</p>
<p>이 상태로 설치를 마치려면 <code>Finished configuring</code>을 선택하면 된다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/68e94ee0-da6a-47f6-ac11-bedfc7b4566c.png" alt="" style="display:block;margin:0 auto" />

<p>다만 원격 접속(Remote Access)을 사용하려면 <code>Enable sshd</code> 항목을 <code>Yes</code>로 바꾸는 것이 좋다. <code>sshd</code>는 SSH 서버 데몬(SSH Server Daemon)으로, 외부에서 SSH(Secure Shell)를 통해 NetBSD에 접속할 수 있게 해준다.</p>
<p>키보드의 방향키와 Spacebar(스페이스바)를 이용해 항목을 이동하고, Enter를 눌러 <code>Yes/No</code> 값을 바꿀 수 있다. <code>Enable sshd</code>를 <code>Yes</code>로 설정한 뒤 <code>Finished configuring</code>을 선택하면 기본 설정이 완료된다.</p>
<p>이번 단계에서는 NetBSD 설치를 위한 파티션 크기를 확인하고, BIOS Console을 사용하도록 설정한 뒤, Full installation을 선택한다. 설치 소스는 앞에서 연결한 ISO 이미지를 사용하므로 CD-ROM/DVD/install image media를 선택한다.</p>
<p>이후 루트 비밀번호를 설정하고, 최종 설정 화면에서 필요하다면 <code>Enable sshd</code>를 <code>Yes</code>로 변경한다. SSHD를 활성화하면 설치 후 원격에서 NetBSD 시스템에 접속할 수 있게 된다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p>이번 실습에서는 가상 디스크를 사용하므로 파티션 설정을 기본값으로 진행해도 큰 문제가 없다. 실제 컴퓨터에 운영체제를 설치할 때는 기존 데이터가 삭제될 수 있으므로 파티션 선택을 훨씬 더 신중하게 해야 한다.</p>
<p><code>sshd</code>는 서버 실습에서 매우 중요하다. GUI 없이 CLI 환경으로 서버를 관리할 때는 SSH를 통해 원격 접속하는 경우가 많기 때문이다.</p>
<hr />
<h2>Finishing NetBSD Installation and Exiting QEMU</h2>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/01f652a7-4e1d-40bc-8d12-d11d3b1a32cf.png" alt="" style="display:block;margin:0 auto" />

<h3>Reboot After Installation (설치 후 재부팅)</h3>
<p>NetBSD 설치 설정이 끝나면 다시 <code>NetBSD-10.1 Install System</code> 화면이 나타난다. 이때는 다시 설치를 반복하는 것이 아니라, <code>Install NetBSD to hard disk</code>가 아닌 <code>Reboot the computer</code>를 선택한다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/ba4fcc78-56ce-4cc7-b64b-e39cb66dd78e.png" alt="" style="display:block;margin:0 auto" />

<p>하지만 재부팅을 해도 다시 <code>Installation messages in English</code> 화면으로 돌아오고, 다시 설치 메뉴가 나타날 수 있다. 이는 설치가 실패했다는 뜻이라기보다는, QEMU가 여전히 NetBSD 설치 ISO Image(설치 이미지)로 부팅하고 있기 때문이다.</p>
<p>즉, 가상 머신(Virtual Machine)이 하드디스크가 아니라 CD-ROM ISO에서 다시 부팅하고 있어서 설치 화면이 반복되는 것이다.</p>
<h3>Why the Installer Appears Again (설치 화면이 다시 나오는 이유)</h3>
<p>앞에서 QEMU를 실행할 때 <code>-cdrom NetBSD-10.1-amd64.iso</code> 옵션을 사용했다. 이 옵션은 ISO 파일을 가상 CD-ROM(Virtual CD-ROM)처럼 연결한다는 뜻이다. 또한 <code>-boot d</code> 옵션을 사용하면 CD-ROM에서 먼저 부팅하게 된다.</p>
<p>그래서 NetBSD가 가상 디스크(Disk Image)에 설치되었더라도, QEMU를 같은 명령어로 다시 실행하면 계속 설치 ISO로 부팅될 수 있다. 이 때문에 설치 화면이 도돌이표처럼 반복되는 것이다.</p>
<h3>How to Exit QEMU (QEMU 종료 방법)</h3>
<p>이 상황에서 Windows 컴퓨터나 VirtualBox 전체를 끌 필요는 없다. QEMU는 Linux 터미널에서 실행 중인 하나의 프로세스(Process)이기 때문이다.</p>
<p>처음에 <code>qemu-system-x86_64 ...</code> 명령어를 실행했을 때, QEMU가 하나의 프로그램으로 실행되었다. 우리가 보는 파란 설치 화면은 실제 컴퓨터가 아니라, QEMU 프로세스가 보여주는 가상 머신 화면이다.</p>
<p>따라서 설치가 끝난 뒤 QEMU를 종료하려면 Linux 터미널에서 <code>Ctrl + C</code>를 누르면 된다. 그러면 실행 중이던 QEMU 프로세스가 종료되고, 가상 머신 화면도 사라진다.</p>
<h3>After Exiting QEMU (QEMU 종료 후)</h3>
<p>QEMU를 종료했다고 해서 설치한 NetBSD가 사라지는 것은 아니다. NetBSD는 앞에서 만든 <code>netbsd.qcow2</code> 디스크 이미지(Disk Image)에 설치되어 있다.</p>
<p>다음에 NetBSD를 실행할 때는 설치 ISO로 다시 부팅하지 않도록 해야 한다. 즉, CD-ROM 설치 이미지로 부팅하는 것이 아니라, 설치가 완료된 가상 하드디스크(Virtual Hard Disk)에서 부팅해야 한다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p>설치가 끝난 뒤 다시 설치 화면이 나오는 가장 흔한 이유는 Boot Order(부팅 순서)가 여전히 CD-ROM을 먼저 보도록 되어 있기 때문이다.</p>
<p>간단히 말하면, 설치할 때는 ISO에서 부팅하고, 설치가 끝난 뒤에는 가상 디스크에서 부팅해야 한다.</p>
<hr />
<h2>Booting NetBSD from the Installed Disk</h2>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/4e3a725d-ffa8-46db-86b1-be79f8990222.png" alt="" style="display:block;margin:0 auto" />

<h3>Running QEMU Without CD-ROM (CD-ROM 제거 후 QEMU 실행)</h3>
<p>NetBSD 설치가 끝난 뒤에는 다시 Linux Terminal(리눅스 터미널)로 돌아온다. 이때 이전 설치 명령어와 달리 <code>-cdrom</code> 옵션이 빠진 명령어를 사용한다.</p>
<pre><code class="language-plaintext">qemu-system-x86_64 -m 512m -smp 1 -drive file=netbsd.qcow2,format=qcow2 \
-net nic,model=virtio -net user
</code></pre>
<p>이 명령어는 이미 설치가 완료된 <code>netbsd.qcow2</code> 디스크 이미지(Disk Image)에서 NetBSD를 부팅하라는 의미이다. 설치할 때는 ISO Image(ISO 이미지)를 CD-ROM처럼 연결했지만, 이제는 운영체제가 디스크에 설치되어 있으므로 CD-ROM이 필요하지 않다.</p>
<p>여기서 <code>-m 512m</code>은 RAM을 512MB로 설정한다는 뜻이고, <code>-smp 1</code>은 CPU를 1개 사용한다는 뜻이다. <code>-drive</code> 옵션은 NetBSD가 설치된 가상 하드디스크(Virtual Hard Disk)를 연결하는 부분이다.</p>
<h3>Logging into NetBSD (NetBSD 로그인)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/25110b4c-5b2e-4175-962d-1e6713f48661.png" alt="" style="display:block;margin:0 auto" />

<p>QEMU를 실행하면 NetBSD 부팅 화면이 나타난다. 처음 메뉴에서는 <code>1. Boot normally</code>를 선택하면 된다. 이후 부팅 메시지가 화면에 출력되고, 마지막에 커서가 깜빡이는 로그인 화면이 나타난다.</p>
<p>이때 설치 과정에서 설정한 비밀번호를 입력하면 로그인할 수 있다. 로그인 후 프롬프트(Prompt)에 <code>#</code>가 보이면 Root User(루트 사용자)로 로그인한 상태이고, <code>$</code>가 보이면 일반 사용자(Regular User)로 로그인한 상태이다.</p>
<p>Root User는 시스템 관리자 권한을 가진 계정이므로, 시스템 설정이나 프로그램 설치 같은 작업을 수행할 수 있다.</p>
<h3>Starting X Window (X Window 실행)</h3>
<p>NetBSD에 로그인한 뒤 그래픽 환경을 실행하고 싶다면 프롬프트에서 다음 명령어를 입력한다.</p>
<pre><code class="language-plaintext">startx
</code></pre>
<p><code>startx</code>는 X Window System(X 윈도우 시스템)을 시작하는 명령어이다. X Window System은 Unix/Linux 계열 운영체제에서 GUI(Graphical User Interface)를 제공하는 기본적인 그래픽 시스템이다.</p>
<p>즉, 터미널 기반 CLI(Command Line Interface) 환경에서 그래픽 화면을 실행하고 싶을 때 <code>startx</code>를 사용할 수 있다.</p>
<h3>Nested Virtualization Environment (중첩된 가상화 환경)</h3>
<p>이번 실습 환경은 실제 컴퓨터 위에 Windows가 있고, 그 위에서 VirtualBox가 Linux를 실행하며, 다시 Linux 안에서 QEMU가 NetBSD를 실행하는 구조이다. 즉, 여러 단계로 가상화가 겹쳐 있는 Nested Virtualization(중첩 가상화) 환경이라고 볼 수 있다.</p>
<p>이런 구조에서도 NetBSD는 비교적 작은 용량으로 설치되고 실행된다. 실습에서는 RAM 512MB, 디스크 4GB 정도로도 기본적인 운영이 가능하다. 이를 통해 운영체제 자체는 생각보다 적은 하드웨어 자원으로도 동작할 수 있다는 점을 알 수 있다.</p>
<p>반대로 고성능 게임기나 그래픽 중심 장치에서는 운영체제보다 그래픽 처리 장치(Graphics Processing Unit, GPU)의 역할이 더 중요할 수 있다. 운영체제는 기본적인 시스템 관리를 담당하고, 화려한 그래픽 처리는 주로 그래픽 하드웨어의 지원을 받는다.</p>
<h3>Summary (정리)</h3>
<p>NetBSD 설치 후에는 더 이상 ISO 이미지로 부팅하지 않고, <code>netbsd.qcow2</code> 가상 디스크에서 직접 부팅한다. 이를 위해 QEMU 실행 명령어에서 <code>-cdrom</code> 옵션을 제거한다.</p>
<p>부팅 후 <code>Boot normally</code>를 선택하고 로그인하면 NetBSD를 사용할 수 있다. 프롬프트가 <code>#</code>이면 루트 사용자, <code>$</code>이면 일반 사용자이다. 그래픽 환경이 필요하면 <code>startx</code> 명령어로 X Window System을 실행할 수 있다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p>설치할 때는 ISO 이미지에서 부팅하고, 설치가 끝난 뒤에는 가상 디스크에서 부팅해야 한다. 그래서 설치 후 실행 명령어에서는 <code>-cdrom</code>과 <code>-boot d</code> 옵션을 제거하는 것이 중요하다.</p>
<p><code>#</code> 프롬프트는 강력한 관리자 권한을 의미하므로 명령어를 입력할 때 주의해야 한다. 시스템 파일을 수정하거나 삭제할 수 있기 때문이다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/3becf3b7-3ebb-43ff-a4e3-ab0a58bb52af.png" alt="" style="display:block;margin:0 auto" />

<h3>실습 결과</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/ba624ba0-2875-4a54-91d9-dcd4d53db476.png" alt="" style="display:block;margin:0 auto" />

<p>설치 후 ISO 이미지 없이 <code>netbsd.qcow2</code> 가상 디스크에서 부팅하였고, root 계정으로 로그인하여 NetBSD가 정상적으로 실행되는 것을 확인하였다. Minimal installation으로 설치했기 때문에 X Window 실행은 진행하지 않았다</p>
]]></content:encoded></item><item><title><![CDATA[Linux Server Basics: SSH, LAMP, and Apache Web Hosting]]></title><description><![CDATA[Server Hardware and Its Characteristics (서버 하드웨어와 특징)
서버(Server)는 일반 개인용 컴퓨터(Personal Computer)보다 훨씬 높은 성능과 안정성을 요구하는 컴퓨터 시스템이다. 개인용 컴퓨터는 주로 한 명의 사용자가 개인적인 작업을 하기 위해 사용하는 반면, 서버는 여러 사용자가 동시에 접속하여 서비스를]]></description><link>https://heesu.tech/linux-server-basics-ssh-lamp-and-apache-web-hosting</link><guid isPermaLink="true">https://heesu.tech/linux-server-basics-ssh-lamp-and-apache-web-hosting</guid><dc:creator><![CDATA[Heesu Noh]]></dc:creator><pubDate>Thu, 04 Jun 2026 22:05:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f42d4c4c-a698-4a2c-bf2a-1207d149f02f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/0fc98f48-6a37-464a-b00f-d71b0c41ed4c.png" alt="" style="display:block;margin:0 auto" />

<h3>Server Hardware and Its Characteristics (서버 하드웨어와 특징)</h3>
<p><code>서버(Server)</code>는 일반 개인용 컴퓨터(Personal Computer)보다 훨씬 높은 성능과 안정성을 요구하는 컴퓨터 시스템이다. 개인용 컴퓨터는 주로 한 명의 사용자가 개인적인 작업을 하기 위해 사용하는 반면, 서버는 여러 사용자가 동시에 접속하여 서비스를 이용할 수 있도록 항상 동작해야 한다.</p>
<p>서버와 개인용 컴퓨터의 가장 큰 차이점 중 하나는 <code>운영 시간(Uptime)</code>이다. 과거에는 전기를 아끼기 위해 컴퓨터를 사용하지 않을 때 꺼두는 것이 일반적이었다. 하지만 서버는 웹사이트, 금융 서비스, 행정 시스템, 데이터베이스(Database) 등 다양한 서비스를 계속 제공해야 하기 때문에 하루 24시간, 일주일 내내 켜져 있어야 한다. 따라서 서버 하드웨어(Server Hardware)는 장시간 안정적으로 작동할 수 있도록 설계되어야 한다.</p>
<p>또한 서버는 <code>높은 수준의 보안(Security)</code>을 유지해야 한다. 개인용 컴퓨터는 주로 개인이 사용하는 장치이기 때문에 문제가 생기더라도 영향 범위가 비교적 작다. 하지만 서버는 다수의 사용자가 접근하는 시스템이므로 보안 문제가 발생하면 그 피해가 훨씬 커질 수 있다. 예를 들어 금융권 서버(Financial Server)가 마비되거나 행정기관 서버(Government Server)가 중단된다면 많은 사람들이 서비스를 이용하지 못하게 되고, 복구에도 오랜 시간이 걸릴 수 있다. 이러한 이유로 서버는 외부 공격, 무단 접근, 데이터 유출 등을 막기 위한 보안 체계가 중요하다.</p>
<p>서버는 동시에 많은 사용자의 접속을 처리할 수 있어야 한다. 개인용 컴퓨터는 보통 한 명 또는 소수의 사용자가 사용하는 것을 기준으로 만들어졌기 때문에 대량의 접속 요청을 처리하기 어렵다. 많은 사용자가 동시에 접속하면 CPU, 메모리(Memory), 저장장치(Storage), 네트워크 대역폭(Network Bandwidth) 등 여러 자원이 빠르게 부족해질 수 있다. 따라서 서버는 높은 하드웨어 성능(Hardware Performance)과 안정적인 네트워크 처리 능력(Network Performance)을 갖추어야 한다.</p>
<p>결국 서버 하드웨어(Server Hardware)는 높은 성능, 긴 운영 시간, 강력한 보안, 많은 사용자 접속 처리 능력을 모두 감당할 수 있어야 한다. 개인용 컴퓨터와 비교했을 때 서버는 단순히 성능이 좋은 컴퓨터가 아니라, 여러 사용자를 위한 서비스를 안정적으로 제공하기 위해 설계된 시스템이라고 볼 수 있다.</p>
<p>Uptime(운영 시간)은 서버가 중단되지 않고 계속 동작하는 시간을 의미한다. 서버에서는 이 Uptime이 매우 중요하며, 서비스가 멈추는 시간을 <code>Downtime(중단 시간)</code>이라고 한다.</p>
<p><code>Network Bandwidth(네트워크 대역폭)</code>는 일정 시간 동안 네트워크를 통해 전송할 수 있는 데이터의 양을 의미한다. 많은 사용자가 동시에 서버에 접속하면 네트워크 대역폭이 부족해질 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/0416ae5b-6577-4865-90a9-4329bd7a12f0.png" alt="" style="display:block;margin:0 auto" />

<h3>Server Software and Daemon-Based Operation (서버 소프트웨어와 데몬 기반 동작)</h3>
<p>서버(Server)는 단순히 성능이 높은 하드웨어(Hardware)만을 의미하지 않는다. 서버 하드웨어 위에는 반드시 운영체제(Operating System)와 서버 역할을 수행하는 소프트웨어(Software)가 설치되어야 한다. 그래서 “서버를 설치한다”라고 하면 처음에는 서버 전용 하드웨어가 따로 있어야 한다고 생각할 수 있지만, 실제로는 개인용 컴퓨터(Personal Computer)나 가상 머신(Virtual Machine) 위에도 서버 소프트웨어를 설치하여 서버처럼 동작하게 만들 수 있다.</p>
<p>예를 들어 우분투 리눅스(Ubuntu Linux)를 설치할 때 데스크톱 버전(Desktop Version)이 아니라 서버 버전(Server Version)을 설치했다면, 물리적으로는 개인용 컴퓨터 위에서 실행되고 있더라도 기능적으로는 서버 역할을 수행할 수 있다. 즉 서버와 클라이언트(Client)의 구분은 단순히 하드웨어 종류로만 결정되는 것이 아니라, 실제로 어떤 기능을 수행하는지에 따라 달라진다. 클라이언트의 요청(Request)을 받아 처리하고 응답(Response)을 제공한다면 그 시스템은 서버로 볼 수 있다.</p>
<p>서버 소프트웨어(Server Software)는 하드웨어 위에 설치되어 클라이언트의 요청을 처리하는 역할을 한다. 이 소프트웨어는 개인용 컴퓨터에 설치하든, 서버급 하드웨어(Server-Class Hardware)에 설치하든 기본적인 동작 원리는 같다. 다만 실제 서비스 환경에서는 많은 요청을 안정적으로 처리해야 하기 때문에 더 높은 성능과 안정성이 요구된다.</p>
<p>서버 소프트웨어의 중요한 특징 중 하나는 <code>데몬(Daemon)</code> 형태로 동작한다는 점이다. 리눅스(Linux)에서 프로그램 이름 뒤에 <code>&amp;</code>를 붙이면 해당 프로그램을 백그라운드(Background)에서 실행할 수 있다. 백그라운드 실행은 사용자가 직접 화면에서 계속 조작하지 않아도 프로그램이 뒤에서 동작하는 방식이다. 서버 프로그램도 이와 비슷하게 사용자의 직접적인 개입 없이 계속 실행된다.</p>
<p>하지만 <code>데몬(Daemon)</code>은 단순히 백그라운드에서 실행되는 프로그램보다 더 서버 관리에 적합한 형태라고 볼 수 있다. 데몬은 시스템이 시작될 때 자동으로 실행될 수 있고, 사용자가 직접 명령을 입력하지 않아도 지속적으로 동작한다. 또한 서버 운영을 위해 필요한 관리 기능이나 보호 기능도 함께 포함될 수 있다. 따라서 데몬은 백그라운드에서 지속적으로 실행되며, 사용자와 직접 상호작용하지 않고 필요한 서비스를 제공하는 프로그램이라고 정리할 수 있다.</p>
<h3>Linux Server Version and Desktop Version (리눅스 서버 버전과 데스크톱 버전)</h3>
<p><code>리눅스 서버 버전(Linux Server Version)</code>과 <code>데스크톱 버전(Desktop Version)</code>의 가장 큰 차이는 사용 목적과 사용자 상호작용(User Interaction)에 있다. <code>데스크톱 운영체제(Desktop Operating System)</code>는 개인 사용자가 키보드(Keyboard), 마우스(Mouse), 화면(Display)을 통해 직접 조작하는 환경을 기준으로 만들어진다. 따라서 사용자가 입력했을 때 빠르게 반응하는 응답성(Responsiveness)이 중요하다. 우리가 스마트폰(Smartphone)이나 개인용 컴퓨터를 사용할 때 버튼을 누르면 즉시 반응하기를 기대하는 것과 같다.</p>
<p>반면 <code>서버 운영체제(Server Operating System)</code>는 사용자가 직접 화면을 보며 계속 조작하는 환경을 전제로 하지 않는다. 서버는 대부분 혼자 백그라운드에서 동작하면서 클라이언트의 요청을 처리한다. 따라서 키보드나 마우스 입력에 즉각 반응하는 것보다, 네트워크 요청(Network Request), 프로세스 처리(Process Handling), 파일 관리(File Management), 보안(Security), 안정성(Stability) 같은 기능적인 부분이 더 중요하다.</p>
<p>이 때문에 <code>우분투 서버(Ubuntu Server)</code>를 설치하면 일반적인 그래픽 화면(Graphical User Interface, GUI)이 아니라 검은 터미널 화면(Command Line Interface, CLI)이 나타난다. 이는 오류가 아니라 서버용 운영체제가 사용자 편의성을 위한 그래픽 환경보다 기능성과 자원 효율(Resource Efficiency)을 더 중요하게 설계되었기 때문이다. GUI가 없으면 그래픽 화면을 유지하는 데 필요한 CPU, 메모리(Memory), 저장 공간(Storage)을 줄일 수 있고, 그 자원을 서버 기능에 더 집중할 수 있다.</p>
<p>결국 데스크톱 운영체제는 사용자가 직접 조작하기 편한 환경에 초점을 맞추고, 서버 운영체제는 서비스를 안정적으로 제공하는 기능에 초점을 맞춘다. 이 차이는 운영체제 내부의 스케줄러(Scheduler) 관점에서도 이해할 수 있다. 데스크톱 환경에서는 사용자의 입력에 빠르게 반응하는 작업의 우선순위가 중요하지만, 서버 환경에서는 여러 클라이언트 요청을 효율적이고 안정적으로 처리하는 것이 더 중요하다.</p>
<p><code>Daemon(데몬)</code>은 리눅스나 유닉스(Unix) 계열 시스템에서 백그라운드로 계속 실행되며 특정 서비스를 제공하는 프로그램을 의미한다. 예를 들어 웹 서버(Web Server), 데이터베이스 서버(Database Server), SSH 서버(SSH Server) 등이 데몬 형태로 동작할 수 있다.</p>
<p><code>CLI(Command Line Interface)</code>는 명령어를 입력해서 컴퓨터를 조작하는 방식이고, <code>GUI(Graphical User Interface)</code>는 아이콘, 창, 버튼처럼 시각적인 요소를 이용해 조작하는 방식이다. 서버에서는 보통 CLI를 많이 사용하는데, 이는 자원을 적게 사용하고 원격 관리(Remote Administration)에 적합하기 때문이다.</p>
<p>서버(Server)와 클라이언트(Client)의 구분은 물리적인 기계의 종류보다 역할(Role)에 따라 결정된다. 같은 컴퓨터라도 다른 컴퓨터의 요청을 처리하면 서버가 될 수 있고, 다른 서버에 요청을 보내 서비스를 이용하면 클라이언트가 될 수 있다.</p>
<p>백그라운드 실행(Background Execution)과 데몬(Daemon)은 비슷하지만 완전히 같은 개념은 아니다. 백그라운드 실행은 단순히 프로그램을 뒤에서 실행하는 방식이고, 데몬은 시스템 서비스(System Service)로서 자동 실행, 지속 실행, 관리 기능 등을 포함하는 경우가 많다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/8a8134b6-a2bd-4205-96cd-49dc299567c5.png" alt="" style="display:block;margin:0 auto" />

<h3>X11 and the X Window System (X11과 X 윈도우 시스템)</h3>
<p>리눅스 서버(Linux Server)를 설치하면 일반적인 윈도우 창(Window)이 뜨지 않고, 검은색 터미널 화면(Command Line Interface, CLI)만 보이는 경우가 많다. 반면 리눅스 데스크톱 버전(Linux Desktop Version)이나 안드로이드(Android)처럼 리눅스 커널(Linux Kernel)을 기반으로 하는 시스템에서는 화면에 창이 뜨고, 사용자는 마우스(Mouse), 키보드(Keyboard), 화면(Screen)을 통해 <code>그래픽 환경(Graphical User Interface, GUI)</code>을 사용할 수 있다. 그렇다면 리눅스 기반 시스템에서 이런 그래픽 화면은 어떻게 동작하는지 이해할 필요가 있다.</p>
<p>과거 유닉스(Unix) 계열 시스템에서는 그래픽 화면을 처리하기 위해 X Window System, 즉 X11(X Window System)이라는 구조를 사용했다. X11의 중요한 특징은 화면 출력도 클라이언트-서버 구조(Client-Server Architecture)로 동작한다는 점이다. 일반적으로 우리가 생각하는 서버(Server)와 클라이언트(Client)의 관계는 사용자의 컴퓨터가 클라이언트이고, 멀리 있는 데이터센터(Data Center)의 컴퓨터가 서버인 경우가 많다. 사용자가 웹사이트에 접속하거나 정보를 요청하면, 클라이언트가 요청(Request)을 보내고 서버가 응답(Response)을 제공하는 구조이다.</p>
<p>하지만 X11(X Window System)에서는 이 개념이 조금 반대로 보일 수 있다. 사용자의 앞에 있는 컴퓨터, 즉 키보드(Keyboard), 마우스(Mouse), 화면(Screen)을 가진 워크스테이션(Workstation)이 X Server(X 서버)가 된다. 그리고 실제로 실행되는 원격 프로그램(Remote Program)이 X Client(X 클라이언트)가 된다. 처음 들으면 헷갈릴 수 있지만, 여기서 서버와 클라이언트는 물리적인 위치가 아니라 기능적인 역할(Role)에 따라 구분된다.</p>
<h3>X Server and X Client (X 서버와 X 클라이언트)</h3>
<p><code>X Server(X 서버)</code>는 사용자의 화면(Screen), 키보드(Keyboard), 마우스(Mouse) 같은 입출력 장치(Input/Output Devices)를 관리한다. 사용자가 실제로 보는 화면을 그려주고, 사용자의 입력을 받아 처리하는 역할을 한다. 따라서 X11 구조에서는 사용자 앞에 있는 컴퓨터가 X 서버가 된다.</p>
<p>반대로 <code>X Client(X 클라이언트)</code>는 화면에 표시될 프로그램이다. 예를 들어 원격 컴퓨터(Remote Machine)에서 실행 중인 프로그램이 “이 창을 화면에 띄워 주세요”라고 요청하면, 그 요청을 <code>X Server(X 서버)</code>가 받아 사용자의 화면에 창을 표시한다. 즉 X 클라이언트는 실제 응용 프로그램(Application Program)이고, X 서버는 그 응용 프로그램의 화면 출력을 담당하는 시스템이라고 볼 수 있다.  </p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/3ed2fd92-9d80-4947-8c14-a46f05d85049.png" alt="" style="display:block;margin:0 auto" />

<p>첨부된 그림에서도 위쪽의 <code>User’s Workstation(사용자 워크스테이션)</code>에 Keyboard(키보드), Mouse(마우스), Screen(화면)이 있고, 그 안에 X Server(X 서버)가 위치해 있다. 그리고 X Client(X 클라이언트)는 같은 컴퓨터 안에 있을 수도 있고, 네트워크(Network)를 통해 연결된 Remote Machine(원격 컴퓨터)에 있을 수도 있다. 원격 컴퓨터에서 실행되는 X Client(X 클라이언트)는 네트워크를 통해 X Server(X 서버)에 화면 출력을 요청하고, 사용자는 자신의 화면에서 그 결과를 볼 수 있다.</p>
<h3>Why the Server Is on the User Side (왜 사용자의 컴퓨터가 서버인가)</h3>
<p>일반적인 웹 서비스(Web Service)에서는 사용자의 컴퓨터가 클라이언트(Client)이고, 서비스를 제공하는 컴퓨터가 서버(Server)이다. 하지만 X11(X Window System)에서는 사용자의 화면을 제공하는 쪽이 X Server(X 서버)이기 때문에 사용자의 컴퓨터가 서버가 된다.</p>
<p><code>서버(Server)</code>는 어떤 서비스를 제공하는 쪽이다. X11에서 제공되는 서비스는 화면 출력(Display Output), 키보드 입력(Keyboard Input), 마우스 입력(Mouse Input) 관리이다. 따라서 사용자의 컴퓨터가 이 서비스를 제공하므로 X Server(X 서버)라고 부른다. 반면 원격에서 실행되는 프로그램은 화면을 직접 가지고 있지 않고, X Server에게 “내 화면을 표시해 달라”고 요청하므로 X Client(X 클라이언트)가 된다.</p>
<p>이 부분은 서버와 클라이언트의 개념을 기능적 관점(Functional Perspective)에서 이해하는 좋은 예시이다. 서버와 클라이언트는 반드시 “멀리 있는 고성능 컴퓨터 = 서버”, “내 앞의 컴퓨터 = 클라이언트”로 정해지는 것이 아니다. 어떤 기능을 제공하느냐, 어떤 쪽이 요청을 보내느냐에 따라 역할이 달라질 수 있다.</p>
<h3>X11 and Remote Display (X11과 원격 화면 출력)</h3>
<p>X11(X Window System)의 중요한 특징 중 하나는 원격 화면 출력(Remote Display)이 가능하다는 점이다. 원격 컴퓨터(Remote Machine)에서 프로그램이 실행되더라도, 그 프로그램의 창(Window)을 사용자의 화면에 띄울 수 있다. 이때 원격 프로그램은 X Client(X 클라이언트)로 동작하고, 사용자의 컴퓨터는 X Server(X 서버)로 동작한다.</p>
<p>예를 들어 서버에 직접 모니터가 연결되어 있지 않더라도, 원격에서 실행한 프로그램의 그래픽 화면을 사용자의 컴퓨터로 전달할 수 있다. 사용자는 자신의 키보드와 마우스로 프로그램을 조작하고, 그 결과를 자신의 화면에서 확인할 수 있다. 이 개념은 나중에 배우게 될 SSH(Secure Shell)와도 연결된다. SSH는 기본적으로 텍스트 기반 원격 접속(Remote Text Login)을 제공하지만, 설정에 따라 X11 Forwarding(X11 포워딩)을 사용하면 원격 프로그램의 그래픽 화면을 로컬 화면에 띄우는 것도 가능하다.</p>
<h3>Linux Server and GUI (리눅스 서버와 GUI)</h3>
<p>리눅스 서버 버전(Linux Server Version)은 기본적으로 GUI(Graphical User Interface)를 포함하지 않는 경우가 많다. 서버는 사용자가 직접 화면을 보며 마우스로 조작하는 것보다, 네트워크 요청(Network Request)을 처리하고 서비스를 안정적으로 실행하는 것이 더 중요하기 때문이다. 그래서 리눅스 서버를 설치하면 검은 터미널 화면만 보이고, 창이나 아이콘 같은 그래픽 환경이 나타나지 않는다.</p>
<p>하지만 GUI가 없다고 해서 서버에서 그래픽 프로그램을 전혀 사용할 수 없는 것은 아니다. X11(X Window System) 같은 구조를 이용하면, 실제 프로그램은 서버나 원격 컴퓨터에서 실행되고 화면은 사용자의 컴퓨터에 표시할 수 있다. 즉 리눅스 서버는 기본적으로 텍스트 기반으로 관리되지만, 필요하다면 원격 화면 출력(Remote Display) 방식으로 그래픽 정보를 확인할 수도 있다.</p>
<p>다만 이번 단계에서는 X11의 세부 동작 원리를 모두 깊게 이해할 필요는 없다. 중요한 것은 리눅스나 유닉스 계열 시스템에서 그래픽 화면도 클라이언트-서버 구조(Client-Server Architecture)로 동작할 수 있으며, X11에서는 사용자의 화면을 관리하는 쪽이 X Server(X 서버)이고, 화면에 표시될 프로그램이 X Client(X 클라이언트)라는 점이다.</p>
<p>X11(X Window System)은 유닉스(Unix)와 리눅스(Linux) 계열 시스템에서 오래 사용된 그래픽 시스템이다. 현대 리눅스 데스크톱에서는 Wayland(웨이랜드)라는 새로운 디스플레이 서버 프로토콜(Display Server Protocol)도 많이 사용되지만, X11 개념은 리눅스 그래픽 환경을 이해하는 데 여전히 중요하다.</p>
<p>X Server(X 서버)는 화면, 키보드, 마우스를 관리하는 쪽이고, X Client(X 클라이언트)는 화면에 표시될 응용 프로그램(Application Program)이다. 이 때문에 X11에서는 사용자의 앞에 있는 컴퓨터가 서버가 될 수 있다.</p>
<p>SSH(Secure Shell)는 원격 서버에 접속할 때 사용하는 대표적인 텍스트 기반 원격 접속 도구이다. 일반적으로는 터미널 명령어(Command)를 입력해 서버를 관리하지만, X11 Forwarding(X11 포워딩)을 사용하면 원격 서버에서 실행한 GUI 프로그램을 내 컴퓨터 화면에 띄울 수도 있다.</p>
<p>이번 내용에서 가장 헷갈리기 쉬운 부분은 “왜 내 앞의 컴퓨터가 서버인가?”이다. 서버(Server)는 물리적으로 멀리 있는 컴퓨터라는 뜻이 아니라, 어떤 기능을 제공하는 프로그램 또는 시스템이라는 뜻이다. X11에서는 내 컴퓨터가 화면 출력 서비스를 제공하므로 X Server(X 서버)가 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/99a19c41-b4b4-4108-8208-0a2bbc01e0e7.png" alt="" style="display:block;margin:0 auto" />

<h3>SSH for Server Configuration and Remote Access (서버 설정과 원격 접속 SSH)</h3>
<p><code>서버(Server)</code>를 관리할 때 가장 중요한 기능 중 하나는 원격 접속(Remote Access)이다. 서버는 항상 사용자의 눈앞에 있는 컴퓨터가 아닐 수 있고, 데이터센터(Data Center), 다른 사무실, 또는 물리적으로 멀리 떨어진 장소에 있을 수 있다. 이런 서버를 직접 모니터(Monitor), 키보드(Keyboard), 마우스(Mouse)에 연결해서 조작하는 것은 매우 불편하기 때문에, 네트워크(Network)를 통해 원격으로 접속하는 방법이 필요하다.</p>
<p>과거에는 원격 접속을 위해 <code>텔넷(Telnet)</code>이라는 프로그램이 많이 사용되었다. <code>텔넷(Telnet)</code>은 멀리 있는 컴퓨터에 접속해서 마치 그 컴퓨터 앞에서 명령어를 입력하는 것처럼 사용할 수 있게 해주는 원격 접속 프로그램(Remote Login Program)이었다. 예전에는 서버와 단말기(Terminal)를 물리적인 전선으로 연결해서 입력과 출력을 주고받는 방식이었기 때문에, 서버에서 멀리 떨어진 장소에서는 사용하기 어려웠다. 하지만 텔넷을 사용하면 집이나 다른 층의 사무실에서도 네트워크를 통해 서버에 접속할 수 있었다.</p>
<p>그러나 <code>텔넷(Telnet)</code>에는 큰 보안 문제(Security Problem)가 있었다. 사용자가 입력하는 아이디, 비밀번호, 명령어 같은 정보가 암호화(Encryption)되지 않은 상태로 전송되었기 때문이다. 즉 네트워크 중간에서 데이터를 볼 수 있는 사람이 있다면, 사용자가 입력한 내용을 그대로 확인할 수 있었다. 서버는 보안이 매우 중요한 시스템이기 때문에 이러한 방식은 위험하다. 이런 이유로 텔넷은 현재 거의 사용되지 않고, <code>SSH(Secure Shell)</code>로 대체되었다.</p>
<h3>SSH and Secure Remote Login (SSH와 안전한 원격 접속)</h3>
<p><code>SSH(Secure Shell)</code>는 서버에 안전하게 원격 접속하기 위한 대표적인 프로그램이다. SSH는 텔넷처럼 원격 서버(Remote Server)에 접속하여 명령어(Command)를 입력하고 결과를 확인할 수 있게 해주지만, 중요한 차이점은 통신 내용을 <code>암호화(Encryption)</code>한다는 점이다. 따라서 사용자가 입력하는 비밀번호(Password), 명령어(Command), 서버에서 돌아오는 결과(Output)가 네트워크 중간에서 쉽게 노출되지 않는다.</p>
<p>서버 관리를 할 때 SSH는 매우 기본적인 도구이다. 리눅스 서버(Linux Server)는 보통 GUI(Graphical User Interface)보다 CLI(Command Line Interface)를 중심으로 관리되기 때문에, SSH를 통해 원격으로 접속한 뒤 터미널(Terminal)에서 명령어를 입력하여 서버를 설정하고 관리한다.</p>
<h3>SSH Service and Restart (SSH 서비스와 재시작)</h3>
<p>리눅스에서 SSH는 하나의 서비스(Service) 형태로 동작한다. 즉 백그라운드(Background)에서 계속 실행되면서 외부에서 들어오는 SSH 접속 요청을 기다린다. 사용자가 SSH 서버 프로그램을 설치하면 기본적인 접속 기능은 사용할 수 있지만, 설정 파일(Configuration File)을 수정한 경우에는 변경 사항이 바로 적용되지 않을 수 있다.</p>
<p>이럴 때는 SSH 서비스를 다시 시작(Restart)해야 한다. 강의에서 언급된 명령어는 다음과 같다.</p>
<pre><code class="language-plaintext">sudo systemctl restart ssh.service
</code></pre>
<p>여기서 <code>sudo</code>는 관리자 권한(Superuser Privilege)으로 명령어를 실행한다는 의미이고, <code>systemctl</code>은 리눅스에서 서비스(Service)를 관리하는 명령어이다. <code>restart</code>는 서비스를 다시 시작하라는 뜻이며, <code>ssh.service</code>는 SSH 서비스(Service)를 의미한다. 즉 이 명령어는 SSH 서비스를 다시 시작해서 변경된 설정이 적용되도록 하는 명령어이다.</p>
<h3>SSH Port and Configuration File (SSH 포트와 설정 파일)</h3>
<p>SSH는 기본적으로 22번 포트(Port 22)를 사용한다. 포트(Port)는 네트워크에서 특정 서비스(Service)를 구분하기 위한 번호라고 이해할 수 있다. 예를 들어 웹페이지 접속에 사용되는 HTTP(HyperText Transfer Protocol)는 일반적으로 80번 포트(Port 80)를 사용하고, SSH는 보통 22번 포트를 사용한다.</p>
<p>하지만 22번 포트는 SSH의 기본 포트(Default Port)로 널리 알려져 있기 때문에, 외부 공격자가 SSH 접속을 시도할 때 가장 먼저 확인하는 포트가 될 수 있다. 그래서 보안 설정의 한 방법으로 SSH 포트를 다른 번호로 변경하기도 한다. 예를 들어 22번 대신 90번, 190번처럼 다른 포트 번호를 사용할 수 있다.</p>
<p>SSH 포트를 변경하려면 다음 설정 파일(Configuration File)을 수정한다.</p>
<pre><code class="language-plaintext">/etc/ssh/sshd_config
</code></pre>
<p>이 파일은 SSH 서버 <code>데몬(SSH Daemon)</code>, 즉 <code>sshd</code>의 설정 파일이다. 여기에서 <code>Port</code>에 해당하는 줄을 찾아 원하는 숫자로 변경하면 SSH 접속 포트를 바꿀 수 있다. 설정 파일을 수정한 뒤에는 앞에서 본 것처럼 SSH 서비스를 재시작해야 변경 사항이 적용된다.</p>
<pre><code class="language-plaintext">sudo systemctl restart ssh.service
</code></pre>
<p>단, 포트를 변경한 후에는 접속할 때도 새 포트 번호를 지정해야 한다. 예를 들어 포트를 190번으로 바꾸었다면 다음과 같은 방식으로 접속해야 한다.</p>
<pre><code class="language-plaintext">ssh -p 190 사용자이름@서버주소
</code></pre>
<p>여기서 <code>-p</code> 옵션은 접속할 포트 번호(Port Number)를 지정하는 옵션이다.</p>
<h3>Telnet vs SSH (텔넷과 SSH의 차이)</h3>
<p>텔넷(Telnet)과 SSH(Secure Shell)는 모두 원격 접속(Remote Access)을 위한 도구라는 공통점이 있다. 하지만 가장 큰 차이는 보안(Security)이다. 텔넷은 데이터를 암호화하지 않고 전송하기 때문에 중간에서 통신 내용을 가로채면 사용자의 입력 정보를 볼 수 있다. 반면 SSH는 통신 내용을 암호화하여 전송하므로 훨씬 안전하다.</p>
<p>따라서 현대 서버 환경에서는 텔넷을 거의 사용하지 않고 SSH를 사용한다. 특히 서버에는 관리자 계정, 시스템 설정, 서비스 운영과 관련된 중요한 정보가 많기 때문에, 원격 접속 도구는 반드시 보안을 고려해야 한다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p>Port(포트)는 네트워크에서 특정 프로그램이나 서비스를 구분하는 번호이다. 같은 서버 안에서도 웹 서버(Web Server), SSH 서버(SSH Server), 데이터베이스 서버(Database Server) 등이 동시에 실행될 수 있는데, 각각 다른 포트를 사용하여 구분된다.</p>
<p>SSH 기본 포트(Default SSH Port)는 22번이다. 하지만 보안상의 이유로 포트를 바꾸는 경우도 있다. 다만 포트를 바꾸는 것만으로 완전한 보안이 되는 것은 아니다. 이것은 공격 시도를 줄이는 보조적인 방법일 뿐이며, 강한 비밀번호(Strong Password), 공개키 인증(Public Key Authentication), 방화벽(Firewall) 설정 등이 함께 필요하다.</p>
<p><code>sshd_config</code>에서 <code>sshd</code>는 SSH Daemon(SSH 데몬)을 의미한다. 데몬(Daemon)은 백그라운드에서 계속 실행되며 특정 서비스를 제공하는 프로그램이다. SSH 데몬은 외부에서 들어오는 SSH 접속 요청을 기다리다가, 사용자가 접속하면 인증(Authentication)을 처리하고 터미널 환경을 제공한다.</p>
<p><code>systemctl</code>은 리눅스에서 서비스(Service)를 관리하는 대표적인 명령어이다. 예를 들어 서비스를 시작(Start), 중지(Stop), 재시작(Restart), 상태 확인(Status)할 때 사용할 수 있다.</p>
<pre><code class="language-plaintext">sudo systemctl status ssh.service
</code></pre>
<p>위 명령어를 사용하면 현재 SSH 서비스가 정상적으로 실행 중인지 확인할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/2a4fa7d9-2fb9-4fab-ba0d-879bccdc8aef.png" alt="" style="display:block;margin:0 auto" />

<h3>SSH Key-Based Login (SSH 키 기반 로그인)</h3>
<p>SSH(Secure Shell)를 사용해 원격 서버(Remote Server)에 접속할 때 보통은 다음과 같은 형태의 명령어를 사용한다.</p>
<p><code>ssh username@server_address</code></p>
<p>여기서 username은 서버에 존재하는 사용자 계정(User Account)이고, server_address는 도메인 이름(Domain Name)이나 IP 주소(IP Address)가 될 수 있다. 이렇게 접속을 시도하면 비밀번호(Password)를 입력하라는 화면이 나타나고, 올바른 비밀번호를 입력하면 원격 서버에 로그인할 수 있다.</p>
<p>하지만 이 방식은 서버에 접속할 때마다 매번 비밀번호를 입력해야 한다는 불편함이 있다. 서버를 자주 관리하거나, 여러 서버에 반복적으로 접속해야 하는 환경에서는 매번 비밀번호를 입력하는 방식이 비효율적일 수 있다. 이런 문제를 해결하기 위해 사용하는 방법이 <code>SSH Key-Based Login(SSH 키 기반 로그인)</code>이다.</p>
<h3>Public Key and Private Key (공개키와 개인키)</h3>
<p>SSH 키 기반 로그인(SSH Key-Based Login)에서는 공개키(Public Key)와 개인키(Private Key)라는 두 개의 키(Key)를 사용한다. 이 두 키는 서로 짝을 이루는 관계이다.</p>
<p><code>개인키(Private Key)</code>는 사용자 본인만 가지고 있어야 하는 중요한 키이다. 말 그대로 개인적으로 보관해야 하는 키이며, 다른 사람에게 노출되면 안 된다. 개인키는 사용자가 “정말 이 키의 주인인지”를 증명하는 데 사용된다.</p>
<p>반면 <code>공개키(Public Key)</code>는 서버에 등록해 둘 수 있는 키이다. 이름 그대로 공개될 수 있는 키이지만, 그렇다고 해서 아무 곳에나 무분별하게 배포해도 된다는 뜻은 아니다. 공개키가 서버의 특정 계정에 등록되면, 그 공개키와 짝이 되는 개인키를 가진 사용자는 비밀번호 없이 해당 계정으로 접속할 수 있기 때문이다.</p>
<p>즉 SSH 키 기반 로그인에서는 클라이언트(Client) 쪽에는 <code>개인키(Private Key)</code>가 있고, 서버(Server) 쪽에는 <code>공개키(Public Key)</code>가 저장된다. 사용자가 SSH로 접속을 시도하면 서버는 등록된 공개키와 사용자의 개인키가 서로 짝이 맞는지 검증한다. 검증에 성공하면 비밀번호를 입력하지 않아도 로그인이 가능해진다.</p>
<h3>Creating SSH Keys with ssh-keygen (ssh-keygen으로 SSH 키 생성하기)</h3>
<p>SSH 키를 만들 때는 <code>ssh-keygen</code> 명령어를 사용한다. 강의 자료에서는 다음과 같은 형식이 제시되었다.</p>
<p><code>ssh-keygen -t 알고리즘</code></p>
<p>예를 들어 RSA 알고리즘(RSA Algorithm)을 사용하고, 키 길이를 4096비트(Bit)로 지정하려면 다음과 같이 입력할 수 있다.</p>
<p><code>ssh-keygen -t rsa -b 4096</code></p>
<p>여기서 -t 옵션은 사용할 암호화 알고리즘(Algorithm)을 지정하는 옵션이고, -b 옵션은 <code>키의 길이(Key Length)</code>를 지정하는 옵션이다. 알고리즘에는 RSA, Ed25519 등 여러 종류가 있으며, 어떤 암호화 방식을 사용할지 선택할 수 있다.</p>
<p>명령어를 실행하면 사용자의 홈 디렉토리(Home Directory) 아래에 있는 .ssh 디렉토리(Directory)에 키 파일이 생성된다. 예를 들어 RSA 알고리즘을 사용하면 보통 다음과 같은 파일이 만들어진다.</p>
<p><code>~/.ssh/id_rsa</code><br /><code>~/.ssh/id_rsa.pub</code></p>
<p>id_rsa는 <code>개인키(Private Key)</code>이고, id_rsa.pub는 <code>공개키(Public Key)</code>이다. 파일 이름 끝에 .pub가 붙어 있으면 공개키라고 이해하면 된다.</p>
<h3>Copying the Public Key to the Server (공개키를 서버에 복사하기)</h3>
<p>키를 생성한 뒤에는 <code>공개키(Public Key)</code>를 로그인하려는 서버 계정에 복사해야 한다. 이때 사용하는 대표적인 명령어가 <code>ssh-copy-id</code>이다.</p>
<p><code>ssh-copy-id username@server_address</code></p>
<p>예를 들어 서버의 계정 이름이 koreacu이고 서버 주소가 192.168.0.10이라면 다음과 같이 사용할 수 있다.</p>
<p><code>ssh-copy-id koreacu@192.168.0.10</code></p>
<p>이 명령어를 실행하면 내 컴퓨터에 있는 공개키가 원격 서버의 해당 계정으로 복사된다. 그리고 서버의 홈 디렉토리 아래 <code>.ssh/authorized_keys</code> 파일에 공개키 내용이 저장된다.</p>
<p><code>~/.ssh/authorized_keys</code></p>
<p>authorized_keys 파일은 “이 계정으로 접속을 허용할 공개키 목록”이라고 이해하면 된다. 이 파일 안에 등록된 공개키와 짝이 맞는 개인키를 가진 사용자는 해당 계정으로 SSH 접속을 할 수 있다.</p>
<p>만약 ssh-copy-id 명령어를 사용할 수 없는 상황이라면, 공개키 파일의 내용을 직접 복사해서 서버의 .ssh/authorized_keys 파일에 붙여 넣을 수도 있다. 즉 공개키 자체는 텍스트 형태의 긴 문자열이므로, 이 내용을 서버의 해당 파일에 저장하면 된다.</p>
<h3>File Permissions for authorized_keys (authorized_keys 파일 권한 설정)</h3>
<p>SSH 키 기반 로그인에서 파일 권한(File Permission)은 매우 중요하다. 특히 <code>.ssh/authorized_keys</code> 파일은 아무나 수정할 수 있으면 안 된다. 만약 다른 사용자나 그룹(Group, Others)이 이 파일에 마음대로 쓰기(Write)를 할 수 있다면, 누군가 자신의 공개키를 추가해서 서버에 접속할 수 있는 위험이 생긴다.</p>
<p>그래서 강의 자료에서는 다음 명령어가 제시되었다.</p>
<p><code>chmod go-w .ssh/authorized_keys</code></p>
<p>여기서 <code>chmod</code>는 파일 권한을 변경하는 명령어이고, go-w는 group과 others에서 <code>write 권한을 제거</code>하라는 의미이다. 조금 더 풀어서 보면 다음과 같다.</p>
<p>g는 group을 의미한다. o는 others를 의미한다. -w는 <code>write permission</code>, 즉 쓰기 권한을 제거한다는 의미이다.</p>
<p>따라서 이 명령어는 .ssh/authorized_keys 파일에 대해 그룹 사용자와 다른 사용자들이 쓰기 권한을 갖지 못하도록 설정하는 명령어이다. 이렇게 해야 다른 사람이 마음대로 공개키를 추가하는 일을 막을 수 있다.</p>
<h3>Why SSH Keys Are Useful (SSH 키 기반 로그인이 유용한 이유)</h3>
<p>SSH 키 기반 로그인(SSH Key-Based Login)을 사용하면 매번 비밀번호를 입력하지 않아도 서버에 접속할 수 있다. 특히 서버나 클러스터(Cluster)를 관리할 때 매우 편리하다. 여러 대의 서버에 반복적으로 접속해야 하거나, 특정 계정으로 자동 접속해야 하는 경우 SSH 키 방식이 자주 사용된다.</p>
<p>예를 들어 클라우드(Cloud) 환경에서 서버 인스턴스(Instance)를 만들거나, 규모가 큰 클러스터링 프로그램(Clustering Program)을 설치할 때 SSH 키를 이용한 접속 설정을 요구하는 경우가 많다. 이는 서버 간 접속이나 자동화된 관리 작업에서 비밀번호 입력 방식보다 훨씬 효율적이기 때문이다.</p>
<p>또한 SSH 키 방식은 비밀번호 기반 로그인(Password-Based Login)보다 보안적으로 더 강하게 설정할 수 있다. 개인키(Private Key)를 안전하게 관리하고, 필요하다면 개인키에 암호(Passphrase)를 추가하면 더 안전하게 사용할 수 있다.</p>
<h3>Security Considerations for SSH Keys (SSH 키 사용 시 보안 주의사항)</h3>
<p>SSH 키를 사용할 때 가장 중요한 것은 개인키(Private Key)를 안전하게 보관하는 것이다. 개인키는 사용자의 신분을 증명하는 핵심 정보이기 때문에 절대 다른 사람에게 공유하면 안 된다. 개인키가 유출되면, 그 키와 짝이 되는 공개키가 등록된 서버에 다른 사람이 접속할 수 있는 위험이 생긴다.</p>
<p>공개키(Public Key)는 개인키보다 덜 민감하지만, 그래도 관리가 필요하다. 공개키가 서버의 authorized_keys 파일에 등록되면 그 키와 짝이 맞는 개인키를 가진 사용자는 접속 권한을 갖게 된다. 따라서 서버 관리자는 authorized_keys 파일에 어떤 공개키가 등록되어 있는지 확인하고, 더 이상 필요 없는 키는 제거해야 한다.</p>
<p>암호화 알고리즘(Encryption Algorithm)과 키 길이(Key Length)도 중요하다. 과거에는 128비트(Bit) 암호화도 매우 안전하다고 여겨졌지만, 컴퓨터 성능이 발전하면서 더 강한 암호화 방식이 필요해졌다. 강의에서 언급된 것처럼 양자컴퓨터(Quantum Computer)가 발전하면 기존 암호화 방식 중 일부는 더 쉽게 공격받을 가능성도 있다. 따라서 실제 환경에서는 현재 권장되는 알고리즘과 충분한 키 길이를 사용하는 것이 중요하다.</p>
<p>전체적으로 보면 SSH 키 기반 로그인은 “클라이언트에는 개인키를 보관하고, 서버에는 공개키를 등록해 두는 방식”이다. 그리고 접속할 때 두 키가 서로 짝이 맞는지 검증하여, 비밀번호 없이 안전하게 로그인할 수 있도록 해준다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p>ssh-keygen -t rsa -b 4096은 RSA 방식으로 4096비트 키를 만드는 예시이다. 최근에는 다음과 같이 Ed25519 알고리즘(Ed25519 Algorithm)을 사용하는 경우도 많다.</p>
<p><code>ssh-keygen -t ed25519</code></p>
<p>개인키(Private Key)는 보통 ~/.ssh/id_rsa 또는 ~/.ssh/id_ed25519처럼 .pub가 붙지 않은 파일이다. 이 파일은 절대 공유하면 안 된다.</p>
<p>공개키(Public Key)는 보통 ~/.ssh/id_rsa.pub 또는 ~/.ssh/id_ed25519.pub처럼 .pub가 붙은 파일이다. 이 파일의 내용을 서버의 ~/.ssh/authorized_keys에 등록한다.</p>
<p>SSH 키 기반 로그인을 설정한 뒤에는 다음처럼 접속할 수 있다.</p>
<p><code>ssh username@server_address</code></p>
<p>설정이 제대로 되어 있다면 비밀번호 입력 없이 바로 로그인된다. 단, 개인키에 Passphrase(개인키 암호)를 설정했다면 접속 시 해당 암호를 입력해야 할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/eaef176d-1af7-4dbf-9dd4-818bc4fe1e80.png" alt="" style="display:block;margin:0 auto" />

<h3>SSHFS and Remote File Access (SSHFS와 원격 파일 접근)</h3>
<p><code>SSHFS(SSH File System)</code>는 <code>SSH(Secure Shell)</code> 프로토콜을 사용해서 원격 서버(Remote Server)에 있는 파일이나 디렉터리(Directory)를 내 로컬 컴퓨터(Local Computer)에서 직접 접근할 수 있게 해주는 방식이다. 일반적인 SSH 접속은 터미널(Terminal)을 통해 명령어를 입력하고 결과를 확인하는 방식이지만, SSHFS를 사용하면 원격 서버의 파일을 내 컴퓨터의 파일 탐색기(File Explorer)나 Finder처럼 열어볼 수 있다.</p>
<p>예를 들어 실습 환경에서 가상 머신(Virtual Machine)에 우분투 서버(Ubuntu Server)를 설치했다고 하자. 보통은 윈도우(Windows), 맥(Mac), 리눅스(Linux) 같은 로컬 컴퓨터에서 SSH로 접속한 뒤 터미널에서 명령어를 입력하여 파일을 확인하거나 설정 파일(Configuration File)을 수정한다. 이 방식은 서버 관리에는 적합하지만, 이미지 파일(Image File)이나 여러 문서 파일을 직접 확인해야 할 때는 불편할 수 있다.</p>
<p>기존 방식에서는 원격 서버에 있는 이미지 파일을 보고 싶다면 <code>SCP(Secure Copy)</code>를 사용해서 파일을 로컬 컴퓨터로 복사한 뒤, <code>이미지 뷰어(Image Viewer)</code>로 열어보는 과정을 거쳐야 한다. 하지만 SSHFS를 사용하면 원격 서버의 디렉터리를 내 컴퓨터의 특정 폴더에 연결해 둘 수 있다. 그러면 파일 탐색기에서 원격 파일 목록이 보이고, 파일을 더블클릭하면 로컬 파일처럼 바로 열 수 있다.</p>
<h3>Installing SSHFS (SSHFS 설치)</h3>
<p>SSHFS를 사용하려면 먼저 클라이언트(Client) 쪽에 SSHFS 프로그램을 설치해야 한다. 우분투(Ubuntu)에서는 다음 명령어로 설치할 수 있다.</p>
<p><code>sudo apt install sshfs</code></p>
<p>여기서 sudo는 관리자 권한(Superuser Privilege)으로 명령어를 실행한다는 뜻이고, apt는 우분투에서 패키지(Package)를 설치하거나 관리할 때 사용하는 패키지 관리자(Package Manager)이다. sshfs는 SSH를 이용해 원격 파일 시스템(Remote File System)을 마운트(Mount)할 수 있게 해주는 프로그램이다.</p>
<h3>Mounting Remote Files with SSHFS (SSHFS로 원격 파일 마운트하기)</h3>
<p>SSHFS를 사용하면 원격 서버의 특정 디렉터리를 내 로컬 컴퓨터의 디렉터리에 연결할 수 있다. 강의 자료에서는 다음과 같은 형태의 명령어가 제시되었다.</p>
<p><code>sshfs -o idmap=user id@remote_host:/remote/path ~/remote_files</code></p>
<p>이 명령어는 원격 서버(Remote Host)에 있는 /remote/path 디렉터리를 내 컴퓨터의 ~/remote_files 디렉터리에 연결하는 명령이다. 연결이 완료되면 ~/remote_files 폴더 안에서 원격 서버의 파일들을 로컬 파일처럼 볼 수 있다.</p>
<p>여기서 id는 원격 서버에 로그인할 <code>사용자 계정(User Account)</code>이고, remote_host는 원격 서버의 IP 주소(IP Address)나 도메인 이름(Domain Name)이다. /remote/path는 원격 서버에서 접근하려는 경로(Path)이고, ~/remote_files는 내 컴퓨터에서 원격 파일을 보여줄 마운트 지점(Mount Point)이다.</p>
<p>예를 들어 원격 서버의 사용자 이름이 koreacu, 서버 주소가 192.168.0.10, 원격 디렉터리가 /home/koreacu/project, 로컬 연결 폴더가 ~/remote_files라면 다음과 같이 사용할 수 있다.</p>
<p><code>sshfs -o idmap=user koreacu@192.168.0.10:/home/koreacu/project ~/remote_files</code></p>
<p>이후 내 컴퓨터에서 ~/remote_files 폴더를 열면 원격 서버의 /home/koreacu/project 안에 있는 파일들이 보이게 된다.</p>
<h3>Unmounting SSHFS (SSHFS 연결 해제하기)</h3>
<p>SSHFS로 연결한 원격 디렉터리를 더 이상 사용하지 않을 때는 마운트 해제(Unmount)를 해야 한다. 강의 자료에서는 다음 명령어가 제시되었다.</p>
<p><code>fusermount -u ~/remote_files</code></p>
<p>여기서 fusermount는 <code>FUSE(File System in Userspace)</code> 기반 파일 시스템을 마운트하거나 해제할 때 사용하는 명령어이고, -u 옵션은 unmount, 즉 연결 해제를 의미한다. 이 명령어를 실행하면 <code>~/remote_files</code>에 연결되어 있던 원격 디렉터리 연결이 해제된다.</p>
<h3>SSHFS vs NFS (SSHFS와 NFS의 차이)</h3>
<p>강의에서 <code>NFS(Network File System)</code>도 함께 언급되었다. NFS는 네트워크를 통해 원격 디렉터리를 공유하는 대표적인 방식이다. 하지만 SSHFS와 NFS는 동작 방식과 사용 목적에 차이가 있다.</p>
<p><code>NFS(Network File System)</code>는 보통 시스템 차원(System Level)에서 디렉터리를 공유한다. 서버 관리자가 특정 디렉터리를 export하고, 다른 컴퓨터들이 그 디렉터리를 네트워크를 통해 마운트해서 사용하는 방식이다. 여러 사용자나 여러 시스템이 공통으로 접근해야 하는 공유 디렉터리를 만들 때 자주 사용된다.</p>
<p>반면 SSHFS(SSH File System)는 SSH 로그인 정보를 기반으로 사용자 대 사용자(User-to-User), 계정 대 계정(Account-to-Account) 형태로 연결된다. 즉 내가 SSH로 로그인할 수 있는 계정의 권한 범위 안에서 원격 파일에 접근하는 방식이다. 그래서 별도의 시스템 차원 공유 설정 없이도, SSH 접속 권한만 있으면 비교적 쉽게 원격 파일을 로컬처럼 사용할 수 있다.</p>
<p>이 차이를 간단히 정리하면, NFS는 서버가 디렉터리를 시스템 차원에서 공유하는 방식이고, SSHFS는 사용자가 SSH 계정을 통해 자신의 권한으로 원격 디렉터리에 접근하는 방식이다.</p>
<h3>Why SSHFS Is Convenient (SSHFS가 편리한 이유)</h3>
<p>SSHFS의 가장 큰 장점은 원격 파일을 로컬 파일처럼 다룰 수 있다는 점이다. SSH만 사용할 경우에는 터미널에서 ls, cd, cat, nano, vim 같은 명령어를 사용해 파일을 확인하고 수정해야 한다. 물론 서버 관리에는 이 방식이 중요하지만, 파일이 많거나 이미지, 문서, 코드 파일을 자주 열어봐야 한다면 불편할 수 있다.</p>
<p>예를 들어 원격 서버의 특정 디렉터리 안에 이미지 파일이 있다고 하자. SSH만 사용한다면 파일 이름을 확인하고, 필요하면 SCP로 파일을 복사한 뒤 로컬에서 열어봐야 한다. 하지만 SSHFS를 사용하면 해당 디렉터리가 내 컴퓨터의 폴더처럼 보이기 때문에 파일을 더블클릭해서 바로 이미지 뷰어로 확인할 수 있다.</p>
<p>이미지 파일뿐만 아니라 텍스트 파일(Text File), 설정 파일(Configuration File), 소스 코드(Source Code), 문서 파일(Document File) 등도 로컬 프로그램으로 바로 열어볼 수 있다. 따라서 SSHFS는 서버의 파일을 더 편리하게 확인하고 수정하기 위한 도구라고 볼 수 있다.</p>
<p>다만 SSHFS는 편리성을 위한 도구이지, 모든 서버 작업을 대체하는 것은 아니다. 서버 설정 변경, 프로그램 설치, 서비스 재시작 같은 작업은 여전히 SSH 터미널을 통해 명령어로 수행하는 경우가 많다. SSHFS는 특히 파일을 확인하거나 편집하는 작업에서 편리함을 제공한다.</p>
<p>Additional Notes (추가 설명)</p>
<p>Mount(마운트)는 어떤 저장장치나 원격 파일 시스템을 내 컴퓨터의 특정 디렉터리에 연결해서 사용할 수 있게 만드는 것을 의미한다. SSHFS에서는 원격 서버의 디렉터리를 내 컴퓨터의 폴더처럼 연결한다.</p>
<p>SCP(Secure Copy)는 SSH를 이용해 원격 서버와 로컬 컴퓨터 사이에서 파일을 복사하는 명령어이다. 파일을 한 번 복사해서 가져오는 방식이라면 SCP가 적합하고, 원격 디렉터리를 계속 로컬처럼 열어보고 싶다면 SSHFS가 더 편리하다.</p>
<p>FUSE(File System in Userspace)는 일반 사용자가 커널(Kernel) 내부를 직접 수정하지 않고도 파일 시스템(File System)을 사용할 수 있게 해주는 구조이다. SSHFS는 FUSE를 기반으로 동작한다.</p>
<p>SSHFS를 사용하려면 원격 서버에 SSH 접속이 가능해야 한다. 즉 SSH 서버(SSH Server)가 실행 중이어야 하고, 사용자가 해당 계정으로 로그인할 권한을 가지고 있어야 한다.</p>
<p>SSHFS를 사용할 때 연결할 로컬 디렉터리, 예를 들어 ~/remote_files는 미리 만들어 두는 것이 좋다.</p>
<p><code>mkdir ~/remote_files</code></p>
<p>그 다음 sshfs 명령어를 실행하면 원격 디렉터리를 이 폴더에 연결할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/801bd7d1-b9f3-46e8-816c-daa6c76dc411.png" alt="" style="display:block;margin:0 auto" />

<h3>LAMP Stack and Apache Web Server</h3>
<p>LAMP Stack(LAMP 스택)은 예전부터 웹 서비스(Web Service)를 구축할 때 많이 사용되던 대표적인 서버 소프트웨어 구성이다. LAMP는 Linux, Apache, MySQL, PHP의 앞 글자를 모은 이름이다. 여기서 Linux(리눅스)는 운영체제(Operating System), Apache(아파치)는 웹 서버(Web Server), MySQL은 데이터베이스(Database), PHP는 웹 페이지를 동적으로 처리하기 위한 서버 사이드 언어(Server-side Language)를 의미한다.</p>
<p>웹 서비스를 제공하려면 단순히 서버 하드웨어(Server Hardware)만 있는 것으로는 부족하다. 서버 하드웨어 위에 운영체제(Operating System)를 설치하고, 그 위에 웹 요청을 처리할 서버 프로그램(Server Program)을 설치해야 한다. LAMP는 이런 웹 서비스 환경을 구성하는 대표적인 조합이라고 볼 수 있다.</p>
<h3>Apache Web Server (아파치 웹 서버)</h3>
<p>Apache(Apache HTTP Server)는 클라이언트(Client), 즉 사용자의 웹 브라우저(Web Browser)로부터 들어오는 요청(Request)을 받아 웹 페이지(Web Page)를 응답(Response)으로 돌려주는 웹 서버 프로그램(Web Server Program)이다.</p>
<p>예를 들어 사용자가 브라우저 주소창에 서버의 IP 주소(IP Address)를 입력하면, 브라우저는 해당 서버에 웹 페이지를 요청한다. 이때 서버에 Apache가 설치되어 있고 정상적으로 실행 중이라면, Apache가 요청을 받아 기본 페이지(Default Page)를 사용자에게 보여준다.</p>
<p>우분투(Ubuntu)에서는 Apache를 다음과 같이 설치할 수 있다.</p>
<pre><code class="language-plaintext">sudo apt install apache2
</code></pre>
<p>여기서 <code>apache2</code>는 우분투에서 Apache 웹 서버를 설치할 때 사용하는 패키지(Package) 이름이다. 설치가 완료된 뒤 Apache가 정상적으로 실행되고 있다면, 웹 브라우저에서 해당 컴퓨터의 IP 주소로 접속했을 때 Apache 기본 페이지가 나타난다. 이 기본 페이지가 보인다는 것은 웹 서버(Web Server)가 정상적으로 설치되고 실행되고 있다는 의미이다.</p>
<h3>Apache Service Restart (아파치 서비스 재시작)</h3>
<p>Apache도 SSH처럼 리눅스에서 서비스(Service) 형태로 동작한다. 즉 백그라운드(Background)에서 계속 실행되면서 외부에서 들어오는 웹 요청(Web Request)을 기다린다.</p>
<p>Apache 설정 파일(Configuration File)을 수정한 경우에는 변경 사항을 적용하기 위해 서비스를 다시 시작(Restart)해야 한다. 강의에서 언급된 명령어는 다음과 같다.</p>
<pre><code class="language-plaintext">sudo systemctl restart apache2.service
</code></pre>
<p>이 명령어는 <code>apache2.service</code>를 재시작하여 변경된 설정이 적용되도록 한다. 만약 설정 파일을 수정했는데 웹 페이지가 제대로 보이지 않는다면, 설정값을 잘못 입력했거나 문법 오류(Configuration Error)가 있을 가능성이 있다. 이 경우에는 설정 파일을 다시 확인하고 수정한 뒤 Apache를 다시 재시작해야 한다.</p>
<h3>Apache Configuration Files (아파치 설정 파일)</h3>
<p>Apache의 주요 설정 파일(Configuration Files)은 보통 <code>/etc/apache2</code> 디렉터리 아래에 위치한다.</p>
<pre><code class="language-plaintext">/etc/apache2
</code></pre>
<p>이 디렉터리 안에는 Apache 동작을 제어하는 여러 설정 파일들이 있다. 강의에서 언급된 대표적인 파일은 다음과 같다.</p>
<pre><code class="language-plaintext">/etc/apache2/apache2.conf
/etc/apache2/ports.conf
/etc/apache2/magic
</code></pre>
<p><code>apache2.conf</code>는 Apache의 전체적인 기본 설정을 담당하는 주요 설정 파일이다. Apache가 어떻게 동작할지, 어떤 설정 파일들을 포함할지 등의 기본적인 내용이 들어 있다.</p>
<p><code>ports.conf</code>는 Apache가 어떤 포트(Port)에서 요청을 받을지 설정하는 파일이다. 웹 서버는 보통 HTTP(HyperText Transfer Protocol)의 기본 포트인 80번 포트(Port 80)를 사용한다. 사용자가 브라우저에서 서버 IP 주소로 접속했을 때 Apache가 응답하려면, Apache가 해당 포트에서 요청을 기다리고 있어야 한다.</p>
<p><code>magic</code> 파일은 파일의 형식(File Type)을 판단하는 데 사용될 수 있는 설정과 관련이 있다. 강의에서는 세부적인 내용보다는 Apache 설정 파일들이 <code>/etc/apache2</code> 아래에 모여 있다는 점을 중심으로 이해하면 된다.</p>
<h3>Default Page and Testing Apache</h3>
<p>Apache를 처음 설치한 뒤 웹 브라우저에서 Apache가 설치된 컴퓨터의 IP 주소로 접속하면 기본 페이지(Default Page)가 나타난다. 이 페이지는 Apache가 정상적으로 설치되었고, 웹 서버가 클라이언트의 요청을 받을 준비가 되어 있다는 것을 확인하는 용도이다.</p>
<p>예를 들어 가상 머신(Virtual Machine)에 Ubuntu Server를 설치하고 그 안에 Apache를 설치했다면, 같은 네트워크에서 해당 가상 머신의 IP 주소로 접속하여 Apache 기본 페이지가 보이는지 확인할 수 있다. 기본 페이지가 보인다면 웹 서버 설치는 성공한 것이다.</p>
<p>만약 설정을 변경한 뒤 페이지가 보이지 않는다면 다음과 같은 가능성을 생각해볼 수 있다. Apache 서비스가 실행 중이 아닐 수 있고, 설정 파일에 오류가 있을 수 있으며, 포트 설정이 잘못되었거나 네트워크 연결 문제가 있을 수도 있다. 이때는 설정을 다시 확인하고 Apache 서비스를 재시작해야 한다.</p>
<pre><code class="language-plaintext">sudo systemctl restart apache2.service
</code></pre>
<p>결국 Apache 실습의 기본 흐름은 Apache 설치, IP 주소로 접속하여 기본 페이지 확인, 설정 파일 수정, 서비스 재시작, 다시 접속해서 결과 확인의 순서로 이해할 수 있다.</p>
<h3>Role of LAMP in Web Services (웹 서비스에서 LAMP의 역할)</h3>
<p><code>LAMP Stack(LAMP 스택)</code>은 웹 서비스가 동작하기 위해 필요한 기본 구성 요소를 하나의 조합으로 묶어 이해하는 방식이다. Linux는 서버의 운영체제 역할을 하고, Apache는 웹 브라우저의 요청을 받아 웹 페이지를 제공한다. MySQL은 회원 정보, 게시글, 상품 정보 같은 데이터를 저장하는 데이터베이스 역할을 한다. PHP는 사용자의 요청에 따라 동적인 웹 페이지(Dynamic Web Page)를 생성하는 역할을 한다.</p>
<p>예를 들어 사용자가 웹사이트에 접속하면 Apache가 요청을 받는다. 만약 단순한 HTML 파일(HTML File)이라면 Apache가 바로 파일을 응답으로 보낼 수 있다. 하지만 로그인, 게시판, 검색처럼 데이터가 필요한 기능이라면 PHP가 요청을 처리하고, 필요한 데이터를 MySQL에서 가져온 뒤 결과 페이지를 만들어 Apache를 통해 사용자에게 전달한다.</p>
<p>따라서 LAMP는 웹 서버(Web Server), 데이터베이스(Database), 서버 사이드 프로그래밍(Server-side Programming)이 함께 동작하는 전형적인 웹 서비스 구조라고 볼 수 있다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p><code>Apache(Apache HTTP Server)</code>는 웹 서버(Web Server) 프로그램이다. 사용자의 브라우저가 보낸 HTTP 요청(HTTP Request)을 받아 HTML, 이미지, CSS, JavaScript 같은 웹 리소스(Web Resource)를 응답으로 보내는 역할을 한다.</p>
<p>LAMP Stack(LAMP 스택)의 구성은 다음과 같이 정리할 수 있다.</p>
<pre><code class="language-plaintext">Linux  : 운영체제(Operating System)
Apache : 웹 서버(Web Server)
MySQL  : 데이터베이스(Database)
PHP    : 서버 사이드 언어(Server-side Language)
</code></pre>
<p>Apache 설정을 수정한 뒤 바로 결과가 적용되지 않는 경우가 많기 때문에, 설정 변경 후에는 서비스를 재시작(Restart)해야 한다.</p>
<pre><code class="language-plaintext">sudo systemctl restart apache2.service
</code></pre>
<p>Apache가 정상 실행 중인지 확인하려면 다음 명령어도 사용할 수 있다.</p>
<pre><code class="language-plaintext">sudo systemctl status apache2.service
</code></pre>
<p>웹 브라우저에서 서버 IP 주소로 접속했을 때 Apache 기본 페이지(Default Page)가 보이면, Apache 웹 서버가 정상적으로 설치되고 실행 중이라고 이해하면 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/afc09026-87e9-4178-bfd1-27810e00471f.png" alt="" style="display:block;margin:0 auto" />

<h3>Apache Directory Structure and Enable System (Apache 디렉토리 구조와 활성화 방식)</h3>
<p>Apache2(Apache HTTP Server)를 설치하면 설정 파일(Configuration File)들이 <code>/etc/apache2/</code> 디렉토리 아래에 정리되어 있다. 이 디렉토리에는 <code>conf-available</code>, <code>conf-enabled</code>, <code>mods-available</code>, <code>mods-enabled</code>, <code>sites-available</code>, <code>sites-enabled</code>와 같은 하위 디렉토리들이 존재한다.</p>
<p>이 구조에서 핵심은 <code>available과 enabled의 차이를 이해</code>하는 것이다. available은 “사용 가능한 설정”이라는 의미이고, enabled는 “현재 실제로 활성화되어 적용 중인 설정”이라는 의미이다. 즉 어떤 설정 파일이 available 디렉토리에 있다고 해서 Apache에 바로 적용되는 것은 아니다. 그 설정을 실제로 사용하려면 enabled 상태로 만들어야 한다.</p>
<h3>Apache Available and Enabled Directories</h3>
<p><code>conf-available</code> 디렉토리에는 Apache에서 사용할 수 있는 설정 파일(Configuration Files)들이 들어 있다. 하지만 이 파일들은 아직 적용된 상태가 아니라, 필요할 때 선택해서 활성화할 수 있는 후보라고 볼 수 있다.</p>
<p>반대로 <code>conf-enabled</code> 디렉토리에는 실제로 Apache에 적용되는 설정들이 들어 있다. 보통 <code>conf-enabled</code> 안의 파일들은 <code>conf-available</code>에 있는 파일을 직접 복사한 것이 아니라, <code>심볼릭 링크(Symbolic Link)</code> 형태로 연결되어 있다. Apache는 재시작(Restart)될 때 enabled 디렉토리에 있는 설정들을 읽고 적용한다.</p>
<p>이와 같은 구조는 모듈(Module)과 사이트(Site) 설정에서도 동일하게 사용된다.</p>
<p><code>mods-available</code>에는 사용 가능한 Apache 모듈(Module)과 관련 설정 파일들이 들어 있다.<br /><code>mods-enabled</code>에는 실제로 활성화된 모듈들이 들어 있다.<br /><code>sites-available</code>에는 사용 가능한 가상 호스트(Virtual Host) 설정 파일들이 들어 있다.<br /><code>sites-enabled</code>에는 실제로 활성화되어 Apache에 적용되는 사이트 설정들이 들어 있다.</p>
<h3>Available vs Enabled (사용 가능 상태와 활성화 상태)</h3>
<p>available과 enabled의 관계는 “설치되어 있지만 아직 사용하지 않는 상태”와 “실제로 사용 중인 상태”의 차이로 이해할 수 있다.</p>
<p>예를 들어 어떤 Apache 모듈(Module)이 <code>mods-available</code>에 존재한다면, 그 모듈은 시스템에 준비되어 있는 상태이다. 하지만 아직 Apache가 그 모듈을 사용하고 있는 것은 아니다. 사용자가 해당 모듈을 활성화하면 <code>mods-enabled</code>에 심볼릭 링크(Symbolic Link)가 만들어지고, Apache를 재시작한 뒤 실제로 적용된다.</p>
<p>사이트 설정(Site Configuration)도 마찬가지이다. <code>sites-available</code>에는 여러 개의 웹사이트 설정을 준비해 둘 수 있다. 그중 실제로 서비스할 사이트만 <code>sites-enabled</code>로 활성화하면 Apache가 해당 사이트 설정을 읽어 웹 서비스를 제공한다.</p>
<h3>Apache Helper Commands (Apache 도구 명령어)</h3>
<p>Apache에서는 사용자가 직접 파일을 복사하거나 링크를 만드는 대신, 전용 도구 명령어(Helper Commands)를 사용해 설정을 활성화하거나 비활성화할 수 있다. 강의 자료에 나온 대표적인 명령어는 다음과 같다.</p>
<pre><code class="language-plaintext">a2enconf
a2disconf
a2enmod
a2dismod
a2ensite
a2dissite
</code></pre>
<p><code>a2enconf</code>는 Apache 설정(Configuration)을 활성화(enable)할 때 사용한다.  </p>
<p><code>a2disconf</code>는 활성화된 설정을 비활성화(disable)할 때 사용한다.  </p>
<p><code>a2enmod</code>는 Apache 모듈(Module)을 활성화할 때 사용한다.  </p>
<p><code>a2dismod</code>는 활성화된 모듈을 비활성화할 때 사용한다.  </p>
<p><code>a2ensite</code>는 사이트 설정(Site Configuration)을 활성화할 때 사용한다.  </p>
<p><code>a2dissite</code>는 활성화된 사이트 설정을 비활성화할 때 사용한다.</p>
<p>예를 들어 특정 사이트 설정을 활성화하려면 다음과 같은 방식으로 사용할 수 있다.</p>
<pre><code class="language-plaintext">sudo a2ensite 사이트설정파일이름
</code></pre>
<p>모듈을 활성화하려면 다음과 같이 사용할 수 있다.</p>
<pre><code class="language-plaintext">sudo a2enmod 모듈이름
</code></pre>
<p>이 명령어들은 내부적으로 available 디렉토리에 있는 파일을 enabled 디렉토리로 연결해 주는 역할을 한다. 사용자가 직접 파일을 복사하는 것이 아니라, Apache가 정해둔 방식에 따라 설정을 활성화하는 것이다.</p>
<h3>Applying Apache Settings (Apache 설정 적용)</h3>
<p>Apache 설정을 available에서 enabled 상태로 바꾸었다고 해서 항상 즉시 적용되는 것은 아니다. Apache는 설정 파일을 읽고 동작하는 서비스(Service)이기 때문에, 설정을 변경한 뒤에는 보통 Apache를 다시 시작(Restart)해야 한다.</p>
<pre><code class="language-plaintext">sudo systemctl restart apache2.service
</code></pre>
<p>Apache가 재시작되면 enabled 디렉토리에 연결된 설정 파일들을 읽고 실제 서비스에 반영한다. 따라서 설정 파일을 수정하거나, 모듈을 활성화하거나, 사이트 설정을 변경한 뒤에는 재시작을 통해 변경 사항을 적용해야 한다.</p>
<p>이 구조를 이해하면 Apache 설정 관리가 조금 더 명확해진다. <code>/etc/apache2/</code> 아래에는 사용 가능한 설정들이 모여 있고, 실제로 적용되는 설정들은 enabled 디렉토리에 연결된다. 그리고 Apache가 다시 시작될 때 enabled된 설정들이 적용되어 웹 서버(Web Server)가 동작하게 된다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p><code>심볼릭 링크(Symbolic Link)</code>는 원본 파일을 직접 복사하지 않고, 다른 위치에서 그 파일을 가리키도록 만든 연결 파일이다. Apache의 <code>enabled</code> 디렉토리에는 보통 실제 설정 파일 복사본이 아니라, <code>available</code> 디렉토리에 있는 파일을 가리키는 심볼릭 링크가 들어간다.</p>
<p><code>available</code>은 “설치되어 있고 선택 가능하지만 아직 적용되지 않은 상태”이고, <code>enabled</code>는 “현재 Apache가 읽어서 적용하는 상태”라고 기억하면 쉽다.</p>
<p>Apache 설정을 변경한 뒤 웹페이지가 제대로 보이지 않는다면 설정 파일의 문법 오류(Configuration Syntax Error), 잘못된 포트 설정(Port Configuration), 사이트 설정 오류(Site Configuration Error) 등을 의심할 수 있다. 이때는 설정을 다시 확인한 뒤 Apache를 재시작해야 한다.</p>
<p>실습에서는 직접 <code>a2ensite</code>, <code>a2enmod</code> 같은 명령어를 사용해 보면 available에서 enabled로 연결되는 구조를 더 쉽게 이해할 수 있다. 특히 나중에 여러 웹사이트를 한 서버에서 운영하거나, PHP 모듈(PHP Module), SSL 모듈(SSL Module) 등을 활성화할 때 이 구조가 자주 사용된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/9821ec52-acb3-423b-9bbe-eba86b26019d.png" alt="" style="display:block;margin:0 auto" />

<h3>MySQL Server in LAMP (LAMP의 MySQL 서버)</h3>
<p>MySQL(MySQL Server)은 LAMP(Linux, Apache, MySQL, PHP) 구성에서 데이터베이스(Database)를 담당하는 서버 프로그램이다. 웹 서비스에서 회원 정보, 게시글, 상품 정보 같은 데이터를 저장하고 관리할 때 사용된다.</p>
<p>MySQL은 Ubuntu에서 다음 명령어로 설치할 수 있다.</p>
<pre><code class="language-plaintext">sudo apt install mysql-server
</code></pre>
<p>설치 후 MySQL 서비스를 재시작할 때는 두 가지 방식이 있다.</p>
<pre><code class="language-plaintext">sudo service mysql restart
sudo systemctl restart mysql.service
</code></pre>
<p><code>service</code> 명령어는 예전 방식이고, <code>systemctl</code>은 최근 Linux(Ubuntu)에서 주로 사용하는 방식이다. 둘 다 MySQL 서비스를 재시작하는 명령어이지만, 요즘은 보통 <code>systemctl</code>을 더 많이 사용한다. MySQL이 실행될 때 발생하는 메시지나 오류는 <code>journalctl</code>로 확인할 수 있다.</p>
<pre><code class="language-plaintext">sudo journalctl -u mysql
</code></pre>
<p>여기서 <code>journalctl</code>은 서비스 로그(Service Log)를 확인하는 명령어이다. MySQL이 정상적으로 실행되지 않거나 문제가 생겼을 때 원인을 확인하는 데 사용한다.</p>
<p>MySQL의 주요 설정 파일은 <code>/etc/mysql/</code> 아래에 있다. 특히 다음 파일에서 네트워크 접속 관련 설정을 변경할 수 있습니다.</p>
<pre><code class="language-plaintext">/etc/mysql/mysql.conf.d/mysqld.cnf
</code></pre>
<p>이 파일 안의 <code>bind-address</code> 설정은 MySQL 서버가 어떤 IP 주소에서 접속을 받을지 결정하는 중요한 설정이다.</p>
<p>마지막으로 MySQL을 실제로 사용하려면 다음 명령어를 입력한다.</p>
<pre><code class="language-plaintext">sudo mysql
</code></pre>
<p>사진에서는 “시작”이라고 되어 있지만, 여기서는 MySQL 프로그램을 실행해서 사용하는 의미에 가깝다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p><code>mysql-server</code>는 MySQL 데이터베이스 서버(Database Server)를 설치하는 패키지이다.</p>
<p><code>systemctl restart mysql.service</code>는 MySQL 서버를 재시작해서 설정 변경을 적용할 때 사용한다.</p>
<p><code>journalctl -u mysql</code>은 MySQL 서비스의 로그(Log)를 확인하는 명령어이다. 오류가 났을 때 가장 먼저 확인해볼 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/8d723966-24ea-4dbe-b813-95acf8a58755.png" alt="" style="display:block;margin:0 auto" />

<h3>PHP Script in LAMP (LAMP에서 PHP 스크립트)</h3>
<p><code>PHP(PHP Script)</code>는 LAMP(Linux, Apache, MySQL, PHP) 구성에서 동적인 웹 페이지(Dynamic Web Page)를 처리하는 역할을 한다. PHP는 단독으로 사용하기보다는 웹 서버(Web Server)인 Apache와 DB 서버(Database Server)인 MySQL이 함께 설치되어 있어야 PHP가 웹 서비스 안에서 의미 있게 사용될 수 있다.</p>
<p>사진에 나온 설치 명령어는 다음과 같다.</p>
<pre><code class="language-plaintext">sudo apt install php libapache2-mod-php php-cli php-cgi php-mysql
</code></pre>
<p>여기서 <code>libapache2-mod-php</code>는 Apache가 PHP 코드를 실행할 수 있게 해주는 모듈(Module)이고, <code>php-mysql</code>은 PHP가 MySQL과 연결할 수 있게 해주는 패키지이다. 즉 PHP는 단독 프로그램이라기보다 Apache와 MySQL 사이에서 웹 요청을 처리하고 데이터베이스와 연결해 주는 역할을 한다.</p>
<p>PHP 설정을 변경한 뒤에는 Apache를 재시작한다.</p>
<pre><code class="language-plaintext">sudo systemctl restart apache2.service
</code></pre>
<p>이는 PHP가 Apache 웹 서버 안에서 동작하기 때문이다. PHP 설정을 바꿨는데 Apache를 재시작하지 않으면 변경 내용이 바로 적용되지 않을 수 있다.</p>
<p>주요 설정 파일은 다음과 같다.</p>
<pre><code class="language-plaintext">/etc/php/8.3/cli/php.ini
/etc/php/8.3/apache2/php.ini
</code></pre>
<p>여기서 <code>8.3</code>은 PHP version(PHP 버전)이므로 사용자 환경에 따라 다를 수 있다. Ubuntu(우분투)를 기준으로 한 경로이기 때문에, 다른 Linux distribution(리눅스 배포판)에서는 설치 명령어나 설정 파일 위치가 달라질 수 있다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p><code>php-cli</code>는 터미널(Command Line)에서 PHP를 실행할 때 사용한다.</p>
<p><code>php-cgi</code>는 웹 서버와 PHP가 CGI 방식으로 연동될 때 사용될 수 있다.</p>
<p>Linux kernel(리눅스 커널)의 기본은 같지만, Ubuntu, CentOS, Fedora 같은 distribution(배포판)에 따라 패키지 관리자, 명령어, 설정 파일 위치가 달라질 수 있으므로 실습 환경이 Ubuntu인지 항상 확인하는 것이 좋다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/ae725af5-4aeb-4a5d-9a9f-6d21b5451c7b.png" alt="" style="display:block;margin:0 auto" />

<h3>Viewing Image Files in Linux (리눅스에서 그림 파일 보기)</h3>
<p>리눅스 서버(Linux Server)는 보통 GUI(Graphical User Interface)가 없는 최소 환경으로 설치되기 때문에, 그림 파일(Image File)을 바로 창으로 열어보기 어렵다. 서버는 사용자의 화면 반응성보다 서비스 실행과 관리에 초점을 두기 때문에, Windows처럼 익숙한 화면 환경이 기본으로 제공되지 않는 경우가 많다.</p>
<p>그림 파일을 확인하는 방법은 여러 가지가 있다. 가장 기본적인 방법은 터미널(Terminal)에서 파일 목록을 확인하는 것이다. 하지만 이미지 내용을 직접 보려면 다른 방법이 필요하다.</p>
<p>첫 번째 방법은 Window Manager(윈도우 매니저)를 설치하는 것이다. Window Manager는 X Window System(X 윈도우 시스템)에서 창을 띄우고 관리하는 프로그램이다. 이것을 사용하면 원격 Linux 환경에서도 GUI처럼 그림 파일을 볼 수 있다. 다만 서버 환경에서는 꼭 필요한 경우가 아니면 잘 사용하지 않는다.</p>
<p>두 번째 방법은 sshFS(SSH File System)를 사용하는 것이다. 원격 서버의 디렉토리를 내 컴퓨터에 연결하면, 서버에 있는 이미지 파일을 내 컴퓨터의 파일 탐색기(File Explorer)에서 보는 것처럼 열 수 있다.</p>
<p>세 번째 방법은 Python SimpleHTTPServer를 사용하는 것이다. 서버에 Python이 설치되어 있다면 Apache(Apache Web Server)를 따로 설치하지 않아도 간단한 웹 서버(Web Server)를 실행할 수 있다.</p>
<pre><code class="language-plaintext">python -m SimpleHTTPServer 포트번호
</code></pre>
<p>또는 Python 3에서는 보통 다음 명령어를 사용한다.</p>
<pre><code class="language-plaintext">python3 -m http.server 포트번호
</code></pre>
<p>이렇게 실행하면 웹 브라우저(Web Browser)를 통해 해당 디렉토리의 파일 목록을 볼 수 있고, 이미지 파일도 클릭해서 확인할 수 있다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p>Window Manager(윈도우 매니저)는 GUI 창을 띄우고 관리하는 프로그램이다.</p>
<p>sshFS는 원격 서버의 파일을 내 컴퓨터 폴더처럼 연결해서 보는 방식이다.</p>
<p>Python SimpleHTTPServer는 간단히 임시 웹 서버를 열어 파일을 확인할 때 유용하다.</p>
<p>핵심은 Linux 서버에서는 그림 파일을 바로 보는 환경이 기본이 아니기 때문에, Window Manager, sshFS, Python 웹 서버 같은 방법을 이용해 확인할 수 있다는 점이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f7920cb3-6f32-4b12-a950-9baed178c998.png" alt="" style="display:block;margin:0 auto" />

<h3>Apache Home Directory (Apache 홈 디렉토리)</h3>
<p>Apache(Apache Web Server)를 설치하면 웹 페이지 파일들이 저장되는 기본 홈 디렉토리(Home Directory)가 정해져 있다. Ubuntu 기준으로 Apache의 기본 웹 문서 디렉토리는 다음 위치이다.</p>
<pre><code class="language-plaintext">/var/www/html
</code></pre>
<p>이 디렉토리 안에 있는 파일들이 웹 브라우저를 통해 사용자에게 보여진다. 예를 들어 기본 페이지는 보통 다음 파일이다.</p>
<pre><code class="language-plaintext">/var/www/html/index.html
</code></pre>
<p>사용자가 새로 <code>my.html</code> 같은 파일을 만들면 다음과 같은 경로에 위치할 수 있다.</p>
<pre><code class="language-plaintext">/var/www/html/my.html
</code></pre>
<p>그리고 웹 브라우저에서 서버의 IP 주소나 도메인으로 접속하면 Apache가 이 디렉토리 안의 파일을 찾아서 보여준다.</p>
<h3>Apache Default Site Configuration (Apache 기본 사이트 설정)</h3>
<p>사진에 나온 설정 파일은 Apache의 기본 사이트 설정 파일이다.</p>
<pre><code class="language-plaintext">/etc/apache2/sites-enabled/000-default.conf
</code></pre>
<p>이 파일 안에는 Apache가 어떤 디렉토리를 웹 페이지의 기준 위치로 사용할지 설정되어 있다. 사진에서 중요한 부분은 다음 줄이다.</p>
<pre><code class="language-plaintext">DocumentRoot /var/www/html
</code></pre>
<p><code>DocumentRoot</code>는 웹 서버가 사용자에게 보여줄 파일들이 있는 최상위 디렉토리(Root Directory)를 의미한다. 즉 현재 설정에서는 <code>/var/www/html</code> 안에 있는 파일들이 웹 서비스의 기본 파일로 사용된다.</p>
<h3>Web Hosting and Multiple Sites (웹 호스팅과 여러 사이트 운영)</h3>
<p>기본적으로 Apache를 처음 설치하면 하나의 기본 사이트가 <code>/var/www/html</code>을 기준으로 동작한다. 그래서 처음에는 “서버 한 대당 홈페이지 하나만 운영할 수 있나?”라고 생각할 수 있다.</p>
<p>하지만 실제 웹 호스팅(Web Hosting) 회사들은 서버 한 대에서 여러 개의 홈페이지를 운영하기도 한다. 이때는 Apache의 Virtual Host(가상 호스트) 설정을 사용해서 사이트마다 다른 디렉토리와 도메인을 연결한다. 예를 들어 하나의 서버 안에서도 A 사이트는 <code>/var/www/siteA</code>, B 사이트는 <code>/var/www/siteB</code>처럼 나누어 운영할 수 있다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p><code>/var/www/html</code>은 Apache의 기본 웹 페이지 파일 위치이다.</p>
<p><code>DocumentRoot</code>는 웹 서버가 실제로 파일을 찾아서 보여주는 기준 디렉토리이다.</p>
<p><code>000-default.conf</code>는 Apache의 기본 사이트 설정 파일이다.</p>
<p>Linux에서 서버를 관리하려면 이런 경로를 직접 찾아가고, 설정 파일을 확인하고, 필요한 파일을 수정하는 연습이 중요하다.</p>
<h3>실습화면</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/0af7547b-8294-45da-a6fa-f47b6491cf55.png" alt="" style="display:block;margin:0 auto" />

<p>가상머신안에 Apache2를 설치하였다. Apache를 설치하면 보통 자동으로 <code>/var/www/html</code> 디렉토리가 생성된다. <code>index.html</code>은 Apache 웹 서버(Apache Web Server)가 기본으로 보여주는 첫 번째 웹페이지 파일이다. 즉 브라우저에서 서버 IP 주소로 접속했을 때:</p>
<pre><code class="language-plaintext">http://서버IP주소
</code></pre>
<p>Apache는 자동으로 <code>/var/www/html/index.html</code> 파일을 찾아서 보여줍니다.</p>
<p>예를 들어 <code>index.html</code> 안에 이런 내용이 있으면:</p>
<pre><code class="language-plaintext">Hello Apache
</code></pre>
<p>브라우저에서 서버 IP로 접속했을 때 <code>Hello Apache</code>가 보이게 됩니다.</p>
<p>정리하면:</p>
<pre><code class="language-plaintext">/var/www/html        → 웹사이트 파일들이 들어가는 기본 폴더
index.html           → 웹사이트 첫 화면 파일
DocumentRoot         → Apache가 웹 파일을 찾는 기준 위치
</code></pre>
<p>그래서 <code>index.html</code>은 “홈페이지의 메인 페이지”라고 이해하면 된다. 블로그로 비유하면, 사용자가 주소만 입력했을 때 가장 먼저 보이는 첫 화면이 된다. <code>cat index.html</code>을 했을 때 <code>&lt;html&gt;</code>, <code>&lt;head&gt;</code>, <code>&lt;body&gt;</code>, <code>&lt;div&gt;</code> 같은 <code>&lt;&gt;</code> 태그가 많이 보이는 이유는 <code>index.html</code>이 HTML 파일이기 때문이다.</p>
<h3>HTML File (HTML 파일)</h3>
<p><code>index.html</code>은 웹 브라우저가 읽는 문서 파일이다. 사람이 보기에는 태그가 잔뜩 있는 코드처럼 보이지만, 브라우저는 이 태그들을 해석해서 우리가 보는 웹페이지 화면으로 보여준다.</p>
<p>예를 들어 파일 안에 이런 내용이 있으면:</p>
<pre><code class="language-plaintext">&lt;h1&gt;Hello Apache&lt;/h1&gt;
&lt;p&gt;This is my first web page.&lt;/p&gt;
</code></pre>
<p>터미널에서 <code>cat index.html</code>을 하면 태그까지 그대로 보인다.</p>
<pre><code class="language-plaintext">&lt;h1&gt;Hello Apache&lt;/h1&gt;
&lt;p&gt;This is my first web page.&lt;/p&gt;
</code></pre>
<p>하지만 브라우저에서 접속하면 태그는 보이지 않고 이렇게 보인다.</p>
<pre><code class="language-plaintext">Hello Apache
This is my first web page.
</code></pre>
<h3>Apache Default Page (Apache 기본 페이지)</h3>
<p>Apache를 설치하면 기본으로 만들어지는 <code>index.html</code>에는 “Apache2 Ubuntu Default Page” 같은 안내 페이지 코드가 들어 있다. 그래서 <code>cat index.html</code>을 하면 태그가 아주 많이 보이는 것이 정상이다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/bfc2bb87-4ecb-4176-862a-b88a4421907b.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/3b646f81-1393-456b-9383-f64ae70ed3d3.png" alt="" style="display:block;margin:0 auto" />

<p>위의 연습은 DocumentRoot /var/www/html2 를 이용해서 html2 를 따로 만들어서 Hello html2 문구를 넣어보았다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/e650a0df-e9eb-4dac-a245-51c0083c7ca5.png" alt="" style="display:block;margin:0 auto" />

<h3>User-Specific Home Directory in Apache (Apache 사용자별 홈 디렉토리)</h3>
<p>Apache의 <code>userdir</code> 기능은 “각 사용자 계정마다 자기 홈페이지 폴더를 하나씩 가질 수 있게 해주는 기능”이다. <code>Apache(Apache Web Server)</code>는 기본적으로 <code>/var/www/html</code>을 홈 디렉토리(DocumentRoot)로 사용한다. 이 위치에 <code>index.html</code> 같은 파일을 두면 웹 브라우저에서 서버 주소로 접속했을 때 해당 파일이 보인다.</p>
<pre><code class="language-plaintext">/var/www/html
</code></pre>
<p>하지만 아래처럼 서버에 여러 사용자 계정(User Account)이 있을 경우를 생각해보자</p>
<pre><code class="language-plaintext">/home/heesunoh
/home/mis
/home/student1
</code></pre>
<p>각 사용자마다 자기 홈페이지를 만들게 하고 싶다면, 모든 사람이 <code>/var/www/html</code>을 같이 쓰는 것보다 자기 홈 디렉토리 안에 홈페이지 폴더를 따로 갖는 것이 편하다. 그래서 Apache는 이렇게 정한다.</p>
<pre><code class="language-plaintext">각 사용자 홈 디렉토리 안의 public_html 폴더를 홈페이지 폴더로 쓰자.
</code></pre>
<p>예를 들어 사용자 이름이 <code>heesunoh</code>라면 다음 위치가 사용자 홈페이지 디렉토리가 될 수 있다. 이 폴더가 <code>heesunoh</code> 사용자의 개인 홈페이지 공간이 되는것이다.</p>
<pre><code class="language-plaintext">/home/heesunoh/public_html
</code></pre>
<p>이 안에 <code>index.html</code> 파일을 아래처럼 만들면 웹에서 접속할 수 있다.</p>
<pre><code class="language-plaintext">/home/heesunoh/public_html/index.html
</code></pre>
<p>그리고 웹에서는 아래처럼 볼 수 있게 하는 기능이다.</p>
<pre><code class="language-plaintext">http://서버IP/~heesunoh/index.html
</code></pre>
<p>그런데 Apache는 처음부터 이 기능을 켜두지 않는다. 그래서 <code>userdir</code> 모듈을 활성화해야 한다. 이때 사용하는 것이 <code>userdir</code> 모듈(Module)이다. <code>userdir</code>을 사용하면 각 사용자의 홈 디렉토리(Home Directory) 안에 있는 <code>public_html</code> 디렉토리를 웹 페이지 공간으로 사용할 수 있다. 관련 설정 파일은 다음 위치에 있다.</p>
<pre><code class="language-plaintext">/etc/apache2/mods-available/userdir.conf
</code></pre>
<p>사진 속 설정의 의미는 다음과 같다. 이는 각 사용자의 홈 디렉토리 아래 <code>public_html</code>을 웹 디렉토리로 사용하겠다는 의미이다.</p>
<pre><code class="language-plaintext">UserDir public_html
</code></pre>
<p>이 기능을 사용하려면 <code>userdir</code> 모듈을 활성화해야 한다.</p>
<pre><code class="language-plaintext">/etc/apache2/sites-enabled/000-default.conf
→ 서버의 기본 홈페이지 설정
→ DocumentRoot /var/www/html

/etc/apache2/mods-available/userdir.conf
→ 사용자별 홈페이지 설정
→ /home/사용자/public_html
</code></pre>
<p>root 계정은 이 기능을 사용하지 못하게 막겠다는 뜻이다. 보안상 root의 개인 홈페이지를 열어두지 않는 것이다.</p>
<pre><code class="language-plaintext">&lt;Directory /home/*/public_html&gt;
</code></pre>
<p>/home/사용자이름/public_html 형태의 모든 사용자 디렉토리에 적용되는 설정이다. 여기서 *는 모든 사용자 이름을 의미한다.</p>
<pre><code class="language-plaintext">sudo a2enmod userdir
sudo systemctl restart apache2.service
</code></pre>
<h3>Additional Notes (추가 설명)</h3>
<p><code>/var/www/html</code>은 서버 전체의 기본 홈페이지 디렉토리이다.</p>
<p><code>/home/사용자이름/public_html</code>은 사용자별 홈페이지 디렉토리이다.</p>
<p><code>userdir</code> 모듈을 활성화하면 여러 사용자가 각자 자신의 홈 디렉토리 안에 홈페이지 파일을 둘 수 있다.</p>
<p>정리하면, 기본 Apache 홈페이지는 <code>/var/www/html</code>에 있고, 사용자별 홈페이지는 <code>userdir</code> 설정을 통해 <code>/home/사용자/public_html</code>에서 제공할 수 있다.ㅇ</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/de2bf4e1-b278-41a6-b67b-7f87e4c0174a.png" alt="" style="display:block;margin:0 auto" />

<p>실습결과 1; <code>/etc/apache2/mods-available/userdir.conf</code> 파일의 실제 내용을 확인했다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/8610f5cc-fc5c-4abc-8967-0fceee6b86b1.png" alt="" style="display:block;margin:0 auto" />

<p>실습결과 2; Apache가 현재 기본 DocumentRoot로 설정된 html2안의 index.html파일을 잘 읽어서 응답함을 확인했다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/494574b1-a860-4396-af23-fb4fcef447d1.png" alt="" style="display:block;margin:0 auto" />

<h3>User-Specific Home Directory with userdir (userdir를 이용한 사용자별 홈 디렉토리)</h3>
<p>이전 실습에서는 Apache의 기본 홈페이지 위치를 배웠다면 지금 실습하려는 부분은 방금 배운 내용을 “실제로 켜고 테스트하는 단계”이다.</p>
<p>Apache(Apache Web Server)는 기본적으로 /var/www/html을 웹 서비스의 기본 홈 디렉토리(DocumentRoot)로 사용한다. 하지만 서버에 여러 사용자 계정(User Account)이 있을 경우, 각 사용자마다 개인 홈페이지를 제공할 수도 있다. 이때 사용하는 기능이 userdir 모듈(Module)이다. 그런데 이건 아직 “기능이 준비되어 있다”는 뜻이지, 실제로 작동한다는 뜻은 아니다. 그래서 이번 실습에서는 이 기능을 실제로 활성화하고, 내 계정 안에 <code>public_html</code> 폴더를 만들어서 웹페이지가 보이는지 확인하는 단계이다. userdir을 활성화하려면 다음 명령어를 사용한다.</p>
<pre><code class="language-plaintext">sudo a2enmod userdir
sudo systemctl restart apache2
</code></pre>
<p>이 명령어는 /etc/apache2/mods-available/userdir.conf에 있는 사용 가능한 모듈 설정을 /etc/apache2/mods-enabled/ 쪽으로 활성화하는 역할을 한다. Apache에서 <code>userdir</code> 기능을 켜는 명령어이고 즉 <code>~사용자이름</code> 주소로 사용자별 홈페이지에 접근할 수 있게 한다. 명령어 실행 후에는 Apache가 친절하게 재시작이 필요하다고 알려준다. 여기까지 Apache에서 사용자별 홈페이지 기능(userdir)을 켠 상태가 되었다.</p>
<p>userdir 설정이 활성화되면 각 사용자의 홈 디렉토리(Home Directory) 안에 있는 public_html 디렉토리가 개인 홈페이지 공간으로 사용된다. 예를 들어 사용자 계정이 heesunoh라면 다음 위치에 홈페이지 파일을 만들 수 있다.</p>
<p><code>/home/heesunoh/public_html/index.html</code></p>
<p>그리고 웹에서 접속할 때는 보통 다음과 같이 사용자 이름 앞에 물결표시(~)를 붙인다.</p>
<p><code>http://서버IP주소/~heesunoh/</code></p>
<p>사진 속 설정 파일에서도 중요한 부분은 다음 내용이다.</p>
<p><code>UserDir public_html &lt;Directory /home/*/public_html&gt;</code></p>
<p><code>UserDir public_html</code>은 각 사용자의 홈 디렉토리 아래 <code>public_html</code> 폴더를 웹 디렉토리로 사용하겠다는 뜻이다. /home/*/public_html에서 *는 여러 사용자 계정을 의미한다. 즉 /home/user1/public_html, /home/user2/public_html처럼 사용자마다 다른 홈페이지 디렉토리를 가질 수 있다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p><code>/var/www/html</code>은 서버 전체의 기본 홈페이지 위치이고, <code>/home/사용자명/public_html</code>은 사용자별 홈페이지 위치이다.</p>
<p>가끔 오래된 홈페이지 주소에서 ~username 형태를 볼 수 있는데, 이는 Apache의 userdir 기능을 사용한 사용자별 홈페이지 방식일 가능성이 높다.</p>
<p>실습할 때는 public_html 디렉토리를 직접 만들어야 한다.</p>
<pre><code class="language-plaintext">mkdir ~/public_html
echo "&lt;h1&gt;My Homepage&lt;/h1&gt;" &gt; ~/public_html/index.html
</code></pre>
<h3>실습결과</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/8620e906-25c8-4311-8d52-023a04d8c2c7.png" alt="" style="display:block;margin:0 auto" />

<p>주소는 맞게 찾았는데, Apache가 그 파일/폴더에 접근할 권한이 없다”는 뜻이 확인되었다. 권한 설정이 아직 부족한 상태로 다음 슬라이드에서 권한 설정을 세팅할것이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/84e919c0-22b0-451b-869e-defe3578d34f.png" alt="" style="display:block;margin:0 auto" />

<h3>User-Specific Directory Permissions (사용자별 디렉토리 권한)</h3>
<p>Apache의 userdir 기능을 사용하면 각 사용자의 홈 디렉토리(Home Directory) 안에 <code>public_html</code> 디렉토리를 만들어 개인 홈페이지를 제공할 수 있다. 예를 들어 사용자 계정이 <code>mis</code>라면 웹 파일은 다음 위치에 둘 수 있다.</p>
<pre><code class="language-plaintext">/home/mis/public_html/index.html
</code></pre>
<p>이때 중요한 것은 permission(권한)이다. Apache가 웹 페이지 파일에 도착하려면 중간 경로인 <code>/home/mis</code>와 <code>~/public_html</code> 디렉토리를 통과할 수 있어야 한다. 그래서 디렉토리에는 execution permission(실행 권한)이 필요하다.</p>
<pre><code class="language-plaintext">chmod 711 /home/$USER
mkdir ~/public_html
chmod 711 ~/public_html
</code></pre>
<p>여기서 <code>711</code>은 소유자(owner)는 <code>rwx</code>, 그룹(group)과 기타 사용자(others)는 <code>x</code> 권한만 가진다는 뜻이다. 디렉토리에서 <code>x</code> 권한은 “실행”이라기보다 그 디렉토리를 통과하거나 접근할 수 있는 권한으로 이해하면 된다. 즉 다른 사용자가 내 홈 디렉토리의 파일 목록을 읽을 수는 없지만, 정확한 경로를 알고 있으면 <code>public_html</code> 안의 웹 파일까지 접근할 수 있게 된다.</p>
<p>웹 페이지 파일 자체는 읽기 권한(read permission)이 있어야 한다. 그래서 <code>index.html</code>에는 다음처럼 읽기 권한을 준다.</p>
<pre><code class="language-plaintext">chmod 744 ~/public_html/index.html
</code></pre>
<p><code>744</code>는 소유자는 읽기, 쓰기, 실행이 가능하고, 그룹과 기타 사용자는 읽기만 가능하다는 뜻이다. 웹 브라우저로 접속하는 외부 사용자는 이 파일을 수정할 필요는 없고 읽기만 하면 되므로 read permission(읽기 권한)이 필요하다.</p>
<p>접속 주소는 보통 다음과 같은 형태가 된다.</p>
<pre><code class="language-plaintext">http://서버IP주소/~사용자이름/index.html
</code></pre>
<p>예를 들어 사용자 이름이 <code>mis</code>라면 다음처럼 접속할 수 있다.</p>
<pre><code class="language-plaintext">http://10.211.55.9/~mis/index.html
</code></pre>
<h3>Additional Notes (추가 설명)</h3>
<p>디렉토리의 <code>x</code> 권한은 해당 디렉토리를 “통과”할 수 있는 권한이다. 파일의 <code>x</code> 권한처럼 프로그램 실행만 의미하는 것이 아니다.</p>
<p><code>711</code>은 다른 사용자가 디렉토리 목록을 볼 수는 없지만, 정확한 파일 경로를 알면 접근할 수 있게 하는 설정이다.</p>
<p><code>744</code>는 웹 페이지 파일을 외부에서 읽을 수 있게 하기 위한 권한 설정이다. Apache가 <code>index.html</code>을 읽을 수 있어야 웹페이지가 정상적으로 보인다.</p>
<h3>실습결과</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/801b98f7-40f9-4193-9ba9-6d4e013dbc1d.png" alt="" style="display:block;margin:0 auto" />

<p>권한을 부여했고 정상적으로 확인할 수 있었다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/9418c85c-6ba8-43eb-a605-a530de8196bc.png" alt="" style="display:block;margin:0 auto" />

<h3>User Directory Without Tilde Using mod_rewrite (mod_rewrite를 이용한 물결표 없는 사용자 디렉토리 접속)</h3>
<p>Apache(Apache Web Server)의 userdir 기능을 사용하면 사용자별 홈페이지에 보통 다음과 같이 접속한다.</p>
<p><code>http://서버IP/~사용자이름/</code></p>
<p>여기서 ~사용자이름 형태는 오래된 방식의 개인 홈페이지 주소에서 자주 볼 수 있다. 하지만 이 방식은 서버에 어떤 사용자 계정(User Account)이 존재하는지 외부에 드러낼 수 있다는 단점이 있다. 사용자 이름이 노출되면, 해당 계정을 대상으로 비밀번호 추측 공격이나 피싱(Phishing) 시도가 이루어질 가능성이 있다.</p>
<p>이를 줄이기 위해 mod_rewrite 모듈(Module)을 사용할 수 있다. mod_rewrite는 사용자가 입력한 URL을 Apache 내부에서 다른 경로로 바꿔서 처리하게 해주는 기능이다. 먼저 다음 명령어로 rewrite 모듈을 활성화한다.</p>
<p><code>sudo a2enmod rewrite</code></p>
<p>활성화 후에는 Apache를 재시작해야 적용된다.</p>
<p><code>sudo systemctl restart apache2</code></p>
<p>그다음 Apache 설정 파일에 rewrite 규칙을 추가한다.</p>
<p>RewriteEngine On</p>
<p>RewriteCond %{REQUEST_URI} ^/([^/]+)<br />RewriteCond /home/%1/public_html -d RewriteRule ^/([^/]+)(.*) /home/\(1/public_html\)2 [L]</p>
<p>이 설정을 적용하면 기존처럼 ~를 붙이지 않고도 사용자 홈페이지에 접속할 수 있다.</p>
<p>기존 방식: http://서버IP/~mis/index.html</p>
<p>변경 후: http://서버IP/mis/index.html</p>
<p>즉 사용자가 /mis/index.html로 접속하면 Apache가 내부적으로 /home/mis/public_html/index.html을 찾아서 보여주는 방식이다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p>mod_rewrite는 URL Rewrite(URL 재작성)를 담당하는 Apache 모듈이다.</p>
<p>RewriteEngine On은 rewrite 기능을 켜는 설정이다.</p>
<p>RewriteRule은 사용자가 입력한 주소를 실제 서버 내부 경로로 바꿔주는 규칙이다.</p>
<p>핵심은 ~username 형태를 직접 보여주지 않고도, 사용자별 public_html 디렉토리에 있는 홈페이지를 접속할 수 있게 만드는 것이다.</p>
<h3>실습 결과</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/5f44859c-59ca-4628-bc43-ef91e951ef78.png" alt="" style="display:block;margin:0 auto" />

<p>Apache 설정안에 Rewrite를 설정하였다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/9a736afe-6d4f-45c8-bc86-1438644485f2.png" alt="" style="display:block;margin:0 auto" />

<p>수정된 내용을 업데이트하고 curl을 이용해 제대로 적용되었는지 확인하였다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/0030686e-a351-44b2-b8a7-dfc23f214cdd.png" alt="" style="display:block;margin:0 auto" />

<h3>User Directory Access Without Tilde (물결표 없이 사용자 디렉토리 접속하기)</h3>
<p>Apache(Apache Web Server)의 userdir 기능을 사용하면 사용자별 홈페이지에 보통 다음과 같이 접속한다.</p>
<pre><code class="language-plaintext">http://서버IP/~사용자이름/index.html
</code></pre>
<p>예를 들어 사진의 첫 번째 화면처럼 다음 주소로 접속하면 사용자 <code>mis</code>의 <code>public_html</code> 디렉토리에 있는 <code>index.html</code> 파일을 볼 수 있다.</p>
<pre><code class="language-plaintext">http://10.211.55.9/~mis/index.html
</code></pre>
<p>그런데 앞에서 설정한 <code>mod_rewrite</code>를 적용하면 <code>~</code> 없이도 같은 사용자 홈페이지에 접속할 수 있다.</p>
<pre><code class="language-plaintext">http://10.211.55.9/mis/index.html
</code></pre>
<p>사진의 두 번째 화면이 바로 이 결과이다. 주소에는 <code>~</code>가 없지만, 실제로는 Apache가 내부적으로 <code>/home/mis/public_html/index.html</code> 파일을 찾아서 보여준다.</p>
<p>즉 두 주소는 겉으로 보이는 URL(URL 주소)은 다르지만, 최종적으로 접근하는 파일은 같은 파일이다.</p>
<pre><code class="language-plaintext">~ 있는 방식:
http://10.211.55.9/~mis/index.html

~ 없는 방식:
http://10.211.55.9/mis/index.html
</code></pre>
<p>실습에서 <code>no ~</code>라는 문구를 추가한 이유는 rewrite 설정이 제대로 적용되었는지 확인하기 위해서이다. 원래 같은 <code>index.html</code> 파일을 보여주지만, 파일 내용을 조금 바꿔서 새 주소 방식도 정상적으로 적용되었는지 확인한 것이다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p><code>~username</code> 방식은 Apache의 userdir 기능에서 사용하는 오래된 사용자별 홈페이지 주소 방식이다.</p>
<p><code>mod_rewrite</code>를 사용하면 사용자가 입력한 주소를 서버 내부의 실제 파일 경로로 바꿔서 처리할 수 있다.</p>
<p>핵심은 <code>~mis/index.html</code>과 <code>/mis/index.html</code>이 겉으로는 다른 주소처럼 보이지만, 설정 후에는 같은 사용자 홈페이지 파일을 보여줄 수 있다는 점이다.</p>
<h3>실습결과</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/046a0cee-2c10-47ca-9dbd-233f5dd4e28b.png" alt="" style="display:block;margin:0 auto" />

<p>물결 표시가 있게/ 없게 가능한지 확인하였고 정상적으로 작동하였다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f89f610d-1955-4116-a998-dd674144a4f6.png" alt="" style="display:block;margin:0 auto" />

<h3>Name-Based Virtual Hosting (이름 기반 가상 호스팅)</h3>
<p>Apache(Apache Web Server)에서는 하나의 서버(Host Server) 안에서 여러 개의 웹 사이트(Web Site)를 운영할 수 있다. 이를 Virtual Host(가상 호스트)라고 한다. 사진에서는 Apache 공식 문서의 Virtual Host 예제를 참고하라고 안내하고 있다.</p>
<p>여기서 중요한 개념은 domain name(도메인 이름)이다. 예를 들어 하나의 서버가 <code>1.1.1.1</code>이라는 IP 주소(IP Address)를 가지고 있고, <a href="http://koreacu.ac.kr"><code>koreacu.ac.kr</code></a>과 다른 옛날 홈페이지 주소가 모두 같은 내용을 보여줘도 된다면, 두 domain name이 같은 IP를 가리키게 하면 된다. 이 경우 웹 서버 입장에서는 같은 서버로 접속하는 것이므로 비교적 단순하다.</p>
<p>하지만 domain name마다 서로 다른 내용을 보여줘야 한다면 설정이 달라진다. 예를 들어 <a href="http://siteA.com"><code>siteA.com</code></a>은 A 홈페이지 파일을 보여주고, <a href="http://siteB.com"><code>siteB.com</code></a>은 B 홈페이지 파일을 보여줘야 한다면 Apache에서 Virtual Host(가상 호스트)를 설정해야 한다. 그러면 하나의 Apache 서버 안에서도 domain name에 따라 서로 다른 DocumentRoot(문서 루트)를 사용할 수 있다.</p>
<p>예를 들면 다음과 같은 방식이다.</p>
<pre><code class="language-plaintext">siteA.com → /var/www/siteA
siteB.com → /var/www/siteB
</code></pre>
<p>즉 서버는 하나지만, 접속한 이름(domain name)에 따라 다른 웹 페이지 디렉토리를 보여줄 수 있다. 웹호스팅(Web Hosting) 회사들이 하나의 서버에서 여러 홈페이지를 운영할 수 있는 이유도 이런 방식과 관련이 있다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p>domain name(도메인 이름)은 사람이 기억하기 쉬운 웹사이트 주소이고, 실제 접속은 IP Address(IP 주소)를 통해 이루어진다.</p>
<p>같은 내용을 보여줄 경우에는 여러 domain name이 같은 IP를 가리키게 하면 된다.</p>
<p>서로 다른 내용을 보여줄 경우에는 Apache의 Virtual Host(가상 호스트) 설정을 사용해 domain name별로 다른 DocumentRoot를 지정할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/770e6d93-516f-476b-8c28-fe0ee37c6051.png" alt="" style="display:block;margin:0 auto" />

<h3>IP-Based Virtual Hosting (IP 주소 기반 가상 호스팅)</h3>
<p>Apache(Apache Web Server)에서는 하나의 서버에서 여러 웹 사이트(Web Site)를 운영할 수 있다. 앞에서는 domain name(도메인 이름)을 기준으로 서로 다른 사이트를 구분하는 방법을 배웠고, 이번에는 IP address(IP 주소)를 기준으로 구분하는 방법이다.</p>
<p>예를 들어 하나의 서버에서 다음처럼 서로 다른 IP 주소로 접속했을 때 다른 웹사이트를 보여줄 수 있다.</p>
<pre><code class="language-plaintext">10.211.55.9   → 첫 번째 웹사이트
10.211.55.100 → 두 번째 웹사이트
</code></pre>
<p>처음에는 “컴퓨터 한 대에는 IP 주소가 하나만 있는 것 아닌가?”라고 생각할 수 있다. 과거에는 물리적인 컴퓨터를 여러 대 준비해야 하는 경우가 많았지만, 현재는 하나의 머신(Machine) 안에서도 여러 IP 주소를 설정하거나, VM(Virtual Machine), 네트워크 설정, 가상화 기술을 통해 여러 주소를 서비스할 수 있다.</p>
<p>즉 IP-Based Virtual Hosting(IP 주소 기반 가상 호스팅)은 하나의 Apache 서버가 여러 IP 주소를 받아들이고, 접속한 IP 주소에 따라 다른 DocumentRoot(문서 루트)를 보여주는 방식이다.</p>
<p>예를 들면 다음과 같이 설정할 수 있다.</p>
<pre><code class="language-plaintext">10.211.55.9   → /var/www/site1
10.211.55.100 → /var/www/site2
</code></pre>
<p>이렇게 하면 같은 Apache 서버를 사용하더라도 접속한 IP 주소에 따라 서로 다른 웹 페이지를 제공할 수 있다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p>Virtual Host(가상 호스트)는 하나의 서버에서 여러 웹사이트를 운영하기 위한 Apache 기능이다.</p>
<p>Name-Based Virtual Host(이름 기반 가상 호스트)는 domain name(도메인 이름)으로 사이트를 구분한다.</p>
<p>IP-Based Virtual Host(IP 기반 가상 호스트)는 IP address(IP 주소)로 사이트를 구분한다.</p>
<p>실제 설정 방법은 Apache 공식 문서의 Virtual Host 예제를 참고하면 된다. 강의에서는 “하나의 서버에서도 여러 IP 주소나 여러 사이트를 서비스할 수 있다”는 개념을 이해하는 것이 핵심이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/1d552129-2606-4332-abee-4ad214fcfaaa.png" alt="" style="display:block;margin:0 auto" />

<h3>Port-Based Virtual Hosting (포트 기반 가상 호스팅)</h3>
<p>Apache(Apache Web Server)는 하나의 서버에서 여러 웹사이트를 운영할 수 있다. 앞에서는 domain name(도메인 이름)이나 IP address(IP 주소)를 기준으로 웹사이트를 나누는 방법을 배웠고, 이번에는 port number(포트 번호)를 기준으로 나누는 방법이다.</p>
<p>기본적으로 웹 서버는 HTTP 요청을 받을 때 80번 포트(Port 80)를 사용한다. 그래서 사용자가 아래처럼 접속하면 사실은 80번 포트로 접속하는 것이다.</p>
<pre><code class="language-plaintext">http://10.211.55.9
</code></pre>
<p>이는 실제로는 다음과 같은 의미이다.</p>
<pre><code class="language-plaintext">http://10.211.55.9:80
</code></pre>
<p>하지만 다른 포트를 사용하면 같은 IP 주소에서도 다른 웹사이트를 운영할 수 있다. 예를 들어 9999번 포트를 사용하면 다음처럼 접속할 수 있다.</p>
<pre><code class="language-plaintext">http://10.211.55.9:9999
</code></pre>
<p>이 경우 기존의 <code>/var/www/html</code>은 80번 포트용 홈 디렉토리(DocumentRoot)로 사용하고, 새로 만든 <code>/var/www/port9999</code>는 9999번 포트용 홈 디렉토리로 사용할 수 있다.</p>
<pre><code class="language-plaintext">sudo mkdir /var/www/port9999/
</code></pre>
<p>그리고 이 디렉토리의 소유자를 현재 사용자로 바꾸면 매번 root 권한 없이 파일을 만들고 수정할 수 있다.</p>
<pre><code class="language-plaintext">sudo chown \(USER:\)USER /var/www/port9999/
</code></pre>
<p>새로운 포트용 설정 파일(Configuration File)은 처음부터 만들기보다 기존 Apache 기본 설정 파일을 복사해서 사용하는 것이 편리하다.</p>
<pre><code class="language-plaintext">sudo cp /etc/apache2/sites-available/000-default.conf /etc/apache2/sites-available/port9999.conf
</code></pre>
<p>이후 <code>port9999.conf</code>에서 <code>VirtualHost</code>의 포트 번호와 <code>DocumentRoot</code>를 9999번 포트용으로 수정하면 된다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p>port(포트)는 같은 IP 주소 안에서 서비스를 구분하는 번호이다.</p>
<p>80번 포트는 기본 웹 서비스(HTTP)에 사용된다.</p>
<p>9999번처럼 다른 포트를 사용하면 같은 서버와 같은 IP 주소에서도 다른 웹사이트를 운영할 수 있다.</p>
<p>핵심은 IP 주소나 도메인을 새로 사지 않아도, 포트 번호를 다르게 해서 별도의 웹 서비스 공간을 만들 수 있다는 점이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/b044fe22-5661-42c6-af72-0280531f2629.png" alt="" style="display:block;margin:0 auto" />

<h3>Port-Based VirtualHost Configuration (포트 기반 VirtualHost 설정)</h3>
<p>Apache(Apache Web Server)에서 다른 포트 번호(Port Number)를 사용하려면, 기존 설정 파일을 복사해서 새로운 설정 파일을 만들 수 있다. 예를 들어 9999번 포트용 웹사이트를 만들려면 다음처럼 새 디렉토리를 만든다.</p>
<pre><code class="language-plaintext">sudo mkdir /var/www/port9999/
</code></pre>
<p>그리고 해당 디렉토리의 소유자를 현재 사용자로 바꾸면, 매번 <code>sudo</code>를 쓰지 않고도 파일을 만들거나 수정할 수 있다.</p>
<pre><code class="language-plaintext">sudo chown \(USER:\)USER /var/www/port9999/
</code></pre>
<p>그다음 기존 Apache 기본 설정 파일을 복사해서 9999번 포트용 설정 파일을 만든다.</p>
<pre><code class="language-plaintext">sudo cp /etc/apache2/sites-available/000-default.conf /etc/apache2/sites-available/port9999.conf
</code></pre>
<p>복사한 <code>port9999.conf</code> 파일에서는 두 부분을 수정한다. 첫 번째는 <code>VirtualHost</code>의 포트 번호이고, 두 번째는 <code>DocumentRoot</code>이다.</p>
<pre><code class="language-plaintext">&lt;VirtualHost *:9999&gt;
    DocumentRoot /var/www/port9999
&lt;/VirtualHost&gt;
</code></pre>
<p><code>&lt;VirtualHost *:9999&gt;</code>는 9999번 포트로 들어오는 요청을 이 설정에서 처리하겠다는 뜻이다. <code>DocumentRoot /var/www/port9999</code>는 9999번 포트로 접속했을 때 보여줄 웹 파일의 위치를 의미한다.</p>
<h3>Additional Notes (추가 설명)</h3>
<h3>기본 웹사이트는 보통 80번 포트와 <code>/var/www/html</code>을 사용한다.</h3>
<pre><code class="language-plaintext">http://서버IP
→ /var/www/html
</code></pre>
<p>9999번 포트를 추가하면 같은 서버에서도 다른 웹사이트처럼 사용할 수 있다.</p>
<pre><code class="language-plaintext">http://서버IP:9999
→ /var/www/port9999
</code></pre>
<p>즉 포트 번호(Port Number)를 다르게 하면 하나의 서버에서 여러 웹사이트를 구분해서 운영할 수 있다.</p>
<h3>실습결과</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/45bb3e3e-9183-491f-a274-815a476d650a.png" alt="" style="display:block;margin-left:auto" />

<p>기존 기본 사이트 설정 파일을 복사해서 <code>port9998.conf</code>를 만들고, VirtualHost 포트를 9998로 바꿨다. 그리고 DocumentRoot를 <code>/var/www/port999</code>8로 지정하였다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/dd2eb788-b097-4554-ab3a-ffb650e75059.png" alt="" style="display:block;margin:0 auto" />

<h3>Apache Listen Port Configuration (Apache 포트 수신 설정)</h3>
<p>Port-Based Virtual Hosting(포트 기반 가상 호스팅)을 사용하려면 VirtualHost 설정만 바꾸는 것으로 끝나지 않는다. Apache가 해당 포트로 들어오는 요청을 받을 수 있도록 ports.conf에도 포트를 추가해야 한다.</p>
<p>Apache의 포트 설정 파일은 다음 위치에 있다.</p>
<pre><code class="language-shell">sudo vi /etc/apache2/ports.conf
</code></pre>
<p>기본적으로 Apache는 80번 포트(Port 80)를 사용한다.</p>
<pre><code class="language-plaintext">Listen 80
</code></pre>
<p>여기에 9999번 포트로도 웹 요청을 받게 하려면 다음 줄을 추가한다.</p>
<pre><code class="language-plaintext">Listen 9999
</code></pre>
<p>즉 설정 파일에는 다음과 같이 들어갈 수 있다.</p>
<pre><code class="language-plaintext">Listen 80
Listen 9999
</code></pre>
<p>이렇게 해야 Apache가 80번 포트뿐만 아니라 9999번 포트에서도 요청을 기다리게 된다. 그리고 앞에서 만든 port9999.conf에서는 9999번 포트로 들어온 요청이 /var/www/port9999 디렉토리를 보도록 설정한다.</p>
<pre><code class="language-plaintext">&lt;VirtualHost *:9999&gt; 
    DocumentRoot /var/www/port9999
&lt;/VirtualHost&gt;
</code></pre>
<p>설정을 바꾼 뒤에는 Apache를 재시작해야 적용된다.</p>
<pre><code class="language-plaintext">sudo systemctl restart apache2
</code></pre>
<h3>Additional Notes (추가 설명)</h3>
<p>ports.conf의 Listen 9999는 “Apache가 9999번 포트에서 요청을 받을 준비를 하겠다”는 뜻이다.</p>
<p>port9999.conf의 &lt;VirtualHost *:9999&gt;는 “9999번 포트로 들어온 요청을 어떤 웹사이트 설정으로 처리할지” 정하는 부분이다.</p>
<p>즉 9999번 포트 웹사이트를 만들려면 두 가지가 필요하다.</p>
<ol>
<li><p>ports.conf에 Listen 9999 추가</p>
</li>
<li><p>port9999.conf에 &lt;VirtualHost *:9999&gt; 설정</p>
</li>
</ol>
<h3>실습결과</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/4c2bc6a0-88fd-4440-875d-42f04f118e6e.png" alt="" style="display:block;margin:0 auto" />

<p>Apache가 9999번 포트 요청을 받을 수 있도록 <code>ports.conf</code>에 <code>Listen 9999</code>를 추가하였다. 마지막으로 새 사이트 설정을 활성화하고 Apache를 재시작하였다. <code>curl</code> <a href="http://localhost:9999%EB%A1%9C"><code>http://localhost:999</code></a>8 확인하면 9998번 포트용 웹페이지가 출력된것을 확인했다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/73149e70-ee7f-4568-a5d3-cc87c299063a.png" alt="" style="display:block;margin:0 auto" />

<h3>Port-Based Site Enable and Restart (포트 기반 사이트 활성화와 재시작)</h3>
<p>Apache(Apache Web Server)에서 9999번 포트용 웹사이트를 만들려면, 앞에서 만든 설정 파일 <code>port9999.conf</code>를 활성화해야 한다. 설정 파일을 만들기만 하면 Apache가 바로 사용하는 것이 아니라, sites-enabled에 연결되어야 실제 적용된다. 사이트 활성화 명령어는 다음과 같다.</p>
<pre><code class="language-plaintext">sudo a2ensite port9999.conf
</code></pre>
<p>이 명령어를 실행하면 Apache가 port9999.conf 사이트 설정을 사용할 수 있게 된다. 실행 결과로 “새 설정을 적용하려면 Apache를 reload 또는 restart 하라”는 안내가 나온다.</p>
<p>그다음 Apache 서비스를 다시 시작한다.</p>
<pre><code class="language-plaintext">sudo systemctl restart apache2
</code></pre>
<p>이제 웹 브라우저에서 다음 주소로 접속하면 9999번 포트용 웹페이지가 보인다.</p>
<pre><code class="language-plaintext">http://10.211.55.9:9999
</code></pre>
<p>기본 웹사이트는 80번 포트를 사용하므로 다음처럼 접속한다.</p>
<pre><code class="language-plaintext">http://10.211.55.9
</code></pre>
<p>반면 9999번 포트 웹사이트는 주소 뒤에 :9999를 붙여야 한다.</p>
<pre><code class="language-plaintext">http://10.211.55.9:9999
</code></pre>
<p>즉 같은 IP 주소(IP Address)를 사용하더라도 포트 번호(Port Number)를 다르게 하면 서로 다른 웹사이트처럼 운영할 수 있다.</p>
<h3>Additional Notes (추가 설명)</h3>
<p>a2ensite는 Apache 사이트 설정(Site Configuration)을 활성화하는 명령어이다.</p>
<p>port9999.conf는 9999번 포트로 들어오는 요청을 처리하기 위한 설정 파일이다.</p>
<p>9999번 포트를 사용하려면 다음 세 가지가 모두 필요하다.</p>
<ol>
<li><p>/etc/apache2/ports.conf 에 Listen 9999 추가</p>
</li>
<li><p>/etc/apache2/sites-available/port9999.conf 설정</p>
</li>
<li><p>sudo a2ensite port9999.conf 후 Apache 재시작</p>
</li>
</ol>
<hr />
]]></content:encoded></item><item><title><![CDATA[Operating System File Management: Directory, Inode, Block, and File Descriptors]]></title><description><![CDATA[파일 시스템(File System)이 필요한 이유


첫 번째 질문이 있다. 컴퓨터에 파일을 저장하는 상황을 떠올려보자. 과제 파일, 사진, 동영상처럼 다양한 파일을 계속 저장하다 보면 파일의 수가 많아질수록 어떤 문제가 발생할까?
파일의 수가 많아지면 필요한 파일을 찾는 데 오랜 시간이 걸릴 수 있다. 또한 파일이 어디에 저장되어 있는지 헷갈릴 수도 있다]]></description><link>https://heesu.tech/operating-system-file-management-directory-inode-block-and-file-descriptors</link><guid isPermaLink="true">https://heesu.tech/operating-system-file-management-directory-inode-block-and-file-descriptors</guid><dc:creator><![CDATA[Heesu Noh]]></dc:creator><pubDate>Sun, 31 May 2026 06:42:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f1f14c97-948c-4ab2-a9e1-92825c0cd602.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3>파일 시스템(File System)이 필요한 이유</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/c35c0239-c05c-480a-943c-db0e488d0ddd.png" alt="" style="display:block;margin:0 auto" />

<p>첫 번째 질문이 있다. 컴퓨터에 파일을 저장하는 상황을 떠올려보자. 과제 파일, 사진, 동영상처럼 다양한 파일을 계속 저장하다 보면 파일의 수가 많아질수록 어떤 문제가 발생할까?</p>
<p>파일의 수가 많아지면 필요한 파일을 찾는 데 오랜 시간이 걸릴 수 있다. 또한 파일이 어디에 저장되어 있는지 헷갈릴 수도 있다. 아무 기준 없이 파일이 저장되어 있다면 원하는 파일을 빠르게 찾는 것은 더욱 어려워진다.</p>
<p>이처럼 데이터를 단순히 저장하는 것만으로는 부족하다. 파일을 효율적으로 사용하기 위해서는 체계적으로 관리하는 구조가 필요하다. 운영체제(Operating System)는 파일 시스템(File System)을 통해 파일을 구조적으로 관리하고, 사용자가 원하는 파일에 효율적으로 접근할 수 있도록 지원한다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/4c74f81c-f20e-4aa3-8e11-db864a68cd58.png" alt="" style="display:block;margin:0 auto" />

<p>두 번째 질문이 있다. 파일을 저장할 때 우리는 디스크(Disk)의 실제 위치를 직접 지정하지 않는다. 보통 파일 이름(File Name)만 정해서 저장한다. 그렇다면 저장된 파일은 실제로 어디에 저장되는 것일까? 또한 나중에 파일을 사용할 때는 어떤 기준으로 찾아오게 될까?</p>
<p>파일의 이름과 실제 저장 위치(Storage Location)는 분리되어 있다. 실제 파일 데이터는 보조기억장치(Secondary Storage)의 특정 위치에 저장되며, 운영체제가 이 위치 정보를 체계적으로 관리한다.</p>
<p>만약 이러한 위치 정보가 제대로 관리되지 않는다면 파일을 찾는 데 시간이 오래 걸리거나, 원하는 파일을 찾지 못하는 상황이 발생할 수 있다. 따라서 이번 내용에서는 파일의 저장 위치를 운영체제가 어떻게 관리하는지, 그리고 파일을 효율적으로 찾기 위해 어떤 구조를 사용하는지 살펴본다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/278be2b4-2c8e-4672-9564-ab030520088d.png" alt="" style="display:block;margin:0 auto" />

<h3>파일 시스템(File System) 학습 개요</h3>
<p>이번 주차는 파일 시스템(File System)에 대해 학습한다. 프로그램이 실행되면서 생성되는 데이터를 지속적으로 보관하기 위해서는 보조기억장치(Secondary Storage)에 저장해야 한다.</p>
<p>이때 운영체제(Operating System)는 데이터를 단순히 저장하는 것이 아니라, 파일(File)이라는 논리적인 단위로 관리한다. 이를 통해 사용자가 데이터를 효율적으로 저장하고 접근할 수 있도록 지원한다.</p>
<p>이번 시간에는 파일의 구성 방식과 관리 방법, 그리고 파일이 디스크(Disk)에 저장되고 접근되는 전체적인 흐름을 살펴본다. 특히 파일과 디렉토리(Directory)의 구성, 아이노드(Inode)를 통한 파일 접근 과정, 디스크 블록(Block) 단위의 저장 방식을 학습한다.</p>
<p>또한 파일이 디스크에 저장될 때 사용되는 연속 할당(Contiguous Allocation), 연결 할당(Linked Allocation), 인덱스 할당(Indexed Allocation) 방식의 특징을 이해하고, 각 방식의 접근 성능과 저장 공간 효율을 비교한다.</p>
<p>마지막으로 파일 입출력(File Input/Output) 실습을 통해 파일 디스크립터(File Descriptor)와 오프셋(Offset)의 의미를 이해한다. 또한 파일이 여러 프로세스(Process) 간에 공유될 때 어떤 구조로 동작하는지도 함께 살펴본다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/b3a063d5-fd6c-45fd-83e7-235397936429.png" alt="" style="display:block;margin:0 auto" />

<h3>파일 시스템(File System)의 개념과 역할</h3>
<p>먼저 파일 시스템(File System)의 개념과 역할에 대해 살펴보자. 파일 시스템이란 파일을 생성(Create), 저장(Store), 검색(Search), 수정(Update)하는 전체 과정을 관리하는 운영체제(Operating System)의 중요한 구성 요소이다.</p>
<p>파일 시스템은 디스크(Disk)의 물리적인 저장 구조를 사용자에게 직접 보여주지 않는다. 대신 파일(File)이라는 논리적인 단위를 통해 사용자가 데이터를 쉽게 저장하고 접근할 수 있도록 도와준다.</p>
<p>또한 파일 시스템은 개별 파일만 관리하는 것이 아니라, 디렉토리(Directory) 구조와 메타데이터(Metadata)를 포함해 전체 저장 공간을 관리한다. 여기서 메타데이터란 파일의 실제 내용이 아니라, 파일의 크기(File Size), 생성 시간(Creation Time), 접근 권한(Access Permission), 저장 위치(Storage Location)처럼 파일을 관리하기 위한 정보를 의미한다.</p>
<p>결국 파일 시스템은 사용자가 복잡한 저장 구조를 직접 신경 쓰지 않아도 데이터를 효율적으로 저장하고 찾을 수 있도록 해주는 핵심적인 관리 구조라고 이해할 수 있다.</p>
<hr />
<h3>파일 시스템(File System)의 역할</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/b28c38ef-b5bd-4596-9d47-a3c16ce370af.png" alt="" style="display:block;margin:0 auto" />

<p>파일 시스템(File System)이 실제로 어떤 역할을 수행하는지 살펴보자.</p>
<p>먼저 파일 시스템은 파일을 생성(Create), 삭제(Delete), 수정(Update)하는 기본적인 작업을 처리한다. 또한 파일마다 접근 권한(Access Permission)을 관리하여 허용된 사용자만 파일을 읽거나 수정할 수 있도록 보호 기능(Protection)을 수행한다.</p>
<p>또한 파일 시스템은 디스크 공간(Disk Space)을 효율적으로 관리한다. 각 파일이 디스크의 어느 위치에 저장되어 있는지 추적하고, 사용자가 파일 이름(File Name)만으로도 해당 파일에 접근할 수 있도록 지원한다.</p>
<p>파일 시스템은 사용자와 저장장치(Storage Device) 사이에서 인터페이스(Interface) 역할도 수행한다. 사용자는 복잡한 디스크 구조를 직접 알 필요 없이 파일과 디렉토리(Directory)를 통해 데이터를 사용할 수 있다.</p>
<p>마지막으로 파일 시스템은 데이터의 일관성(Consistency)과 무결성(Integrity)을 유지한다. 여러 작업이 동시에 이루어지는 상황에서도 데이터가 손상되지 않고 올바른 상태로 유지되도록 관리하는 역할을 한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/120728ba-c747-44a7-8df3-997241d59e2e.png" alt="" style="display:block;margin:0 auto" />

<h3>파일 시스템(File System)의 내부 구조</h3>
<p>파일 시스템(File System)의 내부 구조는 사용자의 요청이 실제 디스크(Disk)에 접근하기까지의 흐름으로 이해할 수 있다.</p>
<p>먼저 <code>사용자</code>는 파일 생성(Create), 읽기(Read), 쓰기(Write)와 같은 작업을 요청한다. 파일 시스템은 이 요청을 받아 처리하며, 이 과정에서 <code>디렉토리 관리(Directory Management)</code>와 <code>파일 조작(File Operation)</code>이 이루어진다.</p>
<p>그다음 단계에서는 파일이 <code>레코드(Record)</code> 단위로 나뉘고, 논리적인 파일 구조(Logical File Structure)가 실제 디스크의 <code>블록 구조(Block Structure)</code>에 맞게 변환된다.</p>
<p>이후에는 <code>블록 캐시(Block Cache)</code>를 통해 디스크 접근 횟수를 줄인다. 자주 사용하는 데이터를 메모리(Memory)에 유지하면 매번 디스크에 접근하지 않아도 되므로 파일 접근 속도를 높일 수 있다.</p>
<p>오른쪽으로 갈수록 실제 디스크와 가까운 영역이다. 이 단계에서는 <code>파일 할당(File Allocation)</code>, <code>빈 공간 관리(Free Space Management)</code>, <code>디스크 스케줄링(Disk Scheduling)</code>과 같은 작업이 수행된다.</p>
<p>즉, 이 구조는 사용자의 파일 요청이 파일 시스템을 거쳐 최종적으로 디스크에 접근하는 전체 흐름을 보여준다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/b2727029-737f-4cba-9343-51d1fccc277b.png" alt="" style="display:block;margin:0 auto" />

<h3>파일(File)의 개념</h3>
<p>파일(File)은 이름을 가진 하나의 <code>논리적인 데이터 집합</code>이다. 파일은 여러 데이터로 구성되어 있지만, 사용자 입장에서는 하나의 단위로 다룰 수 있다.</p>
<p>실제로 파일은 디스크(Disk)에 여러 블록(Block)으로 나뉘어 저장될 수 있다. 하지만 사용자는 파일을 여러 조각으로 인식하지 않고, 하나의 연속된 데이터처럼 사용한다.</p>
<p>즉, 파일의 실제 저장 위치나 내부 구조는 사용자에게 직접 보이지 않는다. 이러한 정보는 운영체제(Operating System)가 내부적으로 관리한다.</p>
<p>따라서 파일은 사용자가 데이터를 쉽게 다룰 수 있도록 제공되는 논리적인 데이터 단위(Logical Data Unit)이며, 실제 저장 구조와 관리는 운영체제가 담당한다고 이해할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/69a880f6-1ca4-4d2e-a866-fbab1170f024.png" alt="" style="display:block;margin:0 auto" />

<h3>파일(File)의 구성 요소</h3>
<p>파일(File)은 여러 단계의 구성 요소로 이루어진다.</p>
<p>먼저 가장 작은 단위로 <code>필드(Field)</code>가 있다. 필드는 이름, 학번, 점수처럼 개별적인 정보를 의미한다.</p>
<p>이러한 필드들이 모이면 하나의 <code>레코드(Record)</code>가 된다. 레코드는 의미 있는 데이터의 묶음으로, 예를 들어 한 학생에 대한 전체 정보를 하나의 레코드라고 볼 수 있다.</p>
<p>여러 개의 레코드는 <code>블록(Block)</code> 단위로 저장된다. 블록은 디스크(Disk)에서 데이터를 읽고 쓰는 기본 단위이기 때문에, 운영체제(Operating System)는 실제로 블록 단위를 기준으로 데이터를 처리한다.</p>
<p>마지막으로 여러 블록이 모여 하나의 파일(File)을 구성한다. 즉, 파일은 필드(Field), 레코드(Record), 블록(Block)이라는 계층 구조를 통해 구성된다고 할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/98a56126-9e01-4364-bb52-40cbcfb4f1c1.png" alt="" style="display:block;margin:0 auto" />

<h3>파일(File)의 속성</h3>
<p>파일(File)은 실제 데이터뿐만 아니라, 파일을 관리하기 위한 다양한 정보도 함께 가진다.</p>
<p>파일의 속성(File Attribute)에는 파일 이름(File Name), 파일 크기(File Size), 저장 위치(Storage Location)와 같은 기본 정보가 포함된다. 운영체제(Operating System)는 이러한 정보를 통해 파일을 식별하고 접근한다.</p>
<p>또한 파일의 생성 시간(Creation Time), 수정 시간(Modification Time)과 같은 시간 정보도 관리된다. 접근 권한(Access Permission)과 같은 보호 정보도 포함되어 있어 파일의 읽기(Read), 쓰기(Write), 수정(Update) 권한을 제어할 수 있다.</p>
<p>이러한 속성들은 파일의 실제 내용을 직접 담고 있지는 않지만, 파일을 효율적으로 관리하고 보호하기 위해 반드시 필요한 정보라고 할 수 있다.</p>
<hr />
<h3>파일(File)의 속성과 데이터 연결 구조</h3>
<p>파일(File)의 속성과 데이터가 어떻게 연결되는지 그림 구조를 통해 살펴보자.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/80b63d24-1749-4806-9d5e-69cb73f018da.png" alt="" style="display:block;margin:0 auto" />

<p>먼저 <code>디렉토리(Directory)</code>에는 파일 이름(File Name)과 해당 파일을 가리키는 <code>인덱스(Index)</code> 정보가 저장되어 있다. 사용자가 파일 이름으로 접근하면, 운영체제(Operating System)는 디렉토리를 통해 해당 파일을 찾는다.</p>
<p>그다음 인덱스(Index)를 통해 <code>파일 헤더(File Header)</code>로 연결된다. 파일 헤더에는 파일 크기(File Size), 생성 시간(Creation Time), 접근 권한(Access Permission)과 같은 <code>파일 속성(File Attribute)</code>이 저장되어 있다.</p>
<p>또한 파일 헤더에는 실제 데이터가 저장된 위치를 가리키는 <code>주소 정보(Address Information)</code>도 포함된다. 운영체제는 이 주소를 따라가 실제 <code>데이터 블록(Data Block)</code> 또는 <code>인덱스 블록(Index Block)</code>에 접근한다.</p>
<p>정리하면, 파일은 디렉토리(Directory), 파일 헤더(File Header), 데이터 블록(Data Block)의 구조를 통해 접근된다. 사용자는 파일 이름만 사용하지만, 내부적으로는 운영체제가 파일 속성과 저장 위치 정보를 이용해 실제 데이터를 찾아간다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/1744cea1-dcc5-4d7b-b2fc-3848d8f4dc0d.png" alt="" style="display:block;margin:0 auto" />

<h3>File Operations(파일 연산)</h3>
<p>이번에는 파일 연산(File Operation)에 대해 살펴본다. 파일 연산은 파일을 열고, 데이터를 읽거나 쓰고, 사용이 끝나면 닫고, 필요하면 삭제하는 일련의 과정으로 이해할 수 있다.</p>
<p>먼저 <code>open</code>은 파일을 식별하고 접근을 준비하는 단계이다. 파일 이름(File Name)을 기준으로 디렉토리(Directory)를 탐색하고, 해당 파일의 위치(Location)와 속성(Attribute) 정보를 찾아온다.</p>
<p><code>read/write</code>는 실제 데이터를 처리하는 단계이다. 이때 현재 파일 내 위치를 <code>오프셋(Offset)</code>이라고 하며, 운영체제(Operating System)는 이 오프셋을 기준으로 데이터를 읽거나 쓴다. 데이터를 처리하면 <code>파일 포인터(File Pointer)</code>의 위치도 함께 이동한다.</p>
<p><code>close</code>는 파일 사용이 끝났을 때 수행되는 단계이다. 파일과 관련된 자원(Resource)을 반환하고, 파일 접근 상태를 종료한다.</p>
<p>마지막으로 <code>delete</code>는 파일을 제거하는 작업이다. 파일을 삭제하고, 해당 파일이 사용하던 디스크 공간(Disk Space)을 해제한다.</p>
<p>즉, 파일 연산은 파일을 열고(open), 사용하고(read/write), 닫고(close), 필요하면 삭제하는(delete) 흐름으로 이루어진다고 이해할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/9ab59a12-da5e-4dab-8ba0-ae4539aac5e6.png" alt="" style="display:block;margin:0 auto" />

<h3>Directory(디렉토리)</h3>
<p>디렉토리(Directory)는 파일 이름(File Name)과 실제 저장 위치를 가리키는 <code>아이노드(Inode)</code>를 연결하는 구조이다.</p>
<p>사용자는 파일 이름으로 파일에 접근하지만, 운영체제(Operating System)는 디렉토리를 통해 해당 파일의 아이노드를 찾고, 이를 바탕으로 실제 저장 위치와 연결한다.</p>
<p>또한 디렉토리는 여러 파일을 그룹화(Grouping)하는 역할을 한다. 관련된 파일을 함께 관리할 수 있게 해주며, 파일 탐색(File Search)을 더 효율적으로 만들어준다.</p>
<p>디렉토리는 계층 구조(Hierarchical Structure)를 통해 파일을 체계적으로 관리한다. 사용자는 폴더(Folder)처럼 보이는 디렉토리 구조를 이용해 파일을 분류하고 쉽게 찾을 수 있다.</p>
<p>📌<code>아이노드(Inode)</code>는 파일의 실제 데이터 자체를 저장하는 공간이 아니라, 파일의 메타데이터(Metadata)와 데이터 블록(Data Block)의 위치 정보를 저장하는 구조이다.</p>
<p>즉, 디렉토리는 보통 “파일 이름 → 아이노드 번호(Inode Number)”를 연결하고, 아이노드는 “파일 정보 → 실제 데이터 블록 위치”를 연결한다고 이해하면 좋다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/1a4e10f7-bfbb-478b-877b-a35eccd6b13c.png" alt="" style="display:block;margin:0 auto" />

<h3>Directory Structure(디렉토리 구조)</h3>
<p>디렉토리(Directory)는 계층 구조(Hierarchical Structure)를 가진다. 즉, 하나의 디렉토리 안에 또 다른 디렉토리가 포함될 수 있으며, 이를 통해 전체 파일을 체계적으로 관리할 수 있다.</p>
<p>일반적으로 디렉토리 구조는 <code>루트 디렉토리(Root Directory)</code>를 기준으로 여러 개의 디렉토리와 파일(File)이 트리 구조(Tree Structure) 형태로 구성된다.</p>
<p>이러한 디렉토리 계층 구조를 사용하면 파일을 논리적으로 구분할 수 있고, 사용자는 원하는 파일을 더 효율적으로 탐색할 수 있다.</p>
<p>📌루트 디렉토리(Root Directory)는 디렉토리 구조의 가장 위에 있는 시작점이다. 리눅스/유닉스(Linux/Unix)에서는 <code>/</code>로 표현하고, 윈도우(Windows)에서는 보통 <code>C:\</code>와 같은 드라이브 단위로 표현된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/956d4481-6504-4fe9-8d74-3750bf2a0666.png" alt="" style="display:block;margin:0 auto" />

<h3>Directory Organization(디렉토리 구조의 조직 방식)</h3>
<p>앞서 디렉토리(Directory)의 역할을 살펴보았다면, 이번에는 디렉토리 구조(Directory Structure)가 어떻게 조직되는지 살펴본다.</p>
<p>초기의 파일 시스템(File System)은 모든 파일을 하나의 공간에 저장하는 <code>단일 디렉토리 구조(Single-Level Directory Structure)</code>로 시작하였다. 하지만 파일의 수가 증가하면서 파일을 구분하고 관리하는 것이 점점 어려워졌다.</p>
<p>이를 해결하기 위해 <code>계층적 디렉토리 구조(Hierarchical Directory Structure)</code>가 사용되기 시작했다. 트리 구조(Tree Structure)가 도입되면서 하나의 디렉토리 안에 하위 디렉토리(Subdirectory)를 포함할 수 있게 되었고, 파일을 주제나 목적에 따라 체계적으로 분류하고 관리할 수 있게 되었다.</p>
<p>또한 <code>그래프 구조(Graph Structure)</code>에서는 하나의 파일을 여러 위치에서 참조할 수 있다. 이를 통해 파일 공유(File Sharing)가 가능하지만, 여러 경로에서 같은 파일을 참조하기 때문에 관리가 더 복잡해질 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/2947bd84-033a-49b9-aa8d-6a26beb0f158.png" alt="" style="display:block;margin:0 auto" />

<h3>Directory Structure 2(디렉토리 구조 2)</h3>
<p>디렉토리가 어떻게 구성되고 연결되는지 살펴보자. 디렉토리 구조는 <code>루트 디렉토리(Root Directory)</code>에서 시작되며, 그 아래에 여러 하위 디렉토리(Subdirectory)와 파일(File)이 연결되는 형태로 구성된다.</p>
<p>디렉토리(Directory)는 단순히 이름만 저장하는 것이 아니라, 파일 이름(File Name)이나 하위 디렉토리 이름과 함께 해당 대상을 가리키는 <mark class="bg-yellow-200 dark:bg-yellow-500/30">아이노드 번호(Inode Number)</mark>를 함께 저장한다.</p>
<p>운영체제(Operating System)는 이 아이노드 번호를 따라가면서 파일의 실제 정보와 저장 위치를 확인하고, 최종적으로 데이터 블록(Data Block)에 접근한다. 여기서 아이노드 번호(Inode Number)는 파일의 정보와 위치를 찾기 위해 사용하는 고유한 식별자이다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/9c6fe869-50e9-4ffe-84fc-dbf714272f52.png" alt="" style="display:block;margin:0 auto" />

<p>예를 들어 디렉토리 안에 <code>p3</code>라는 항목이 있고, 그 옆에 <code>5067</code>과 같은 값이 저장되어 있다면, 이 값은 해당 파일이나 디렉토리를 가리키는 아이노드 번호이다. 운영체제는 이 번호를 이용해 해당 파일의 메타데이터(Metadata)와 데이터 위치를 찾아간다.</p>
<p>또한 데이터 블록(Data Block)은 실제 파일 내용이 저장되어 있는 디스크 공간을 의미한다. 디렉토리는 파일 이름과 아이노드 번호를 연결하고, 아이노드는 실제 데이터 블록의 위치를 관리한다고 이해할 수 있다.</p>
<p>각 디렉토리에는 <code>.</code>과 <code>..</code> 항목도 존재한다. <code>.</code>은 현재 디렉토리(Current Directory)를 의미하고, <code>..</code>은 부모 디렉토리(Parent Directory)를 의미한다. 이를 통해 사용자는 계층 구조 안에서 현재 위치를 기준으로 이동할 수 있다.</p>
<p>정리하면, 디렉토리는 파일 이름과 실제 저장 위치를 연결하고, 계층적인 파일 탐색을 가능하게 하는 구조이다.</p>
<p>📌디렉토리(Directory)는 보통 “파일 이름 → 아이노드 번호(Inode Number)”를 저장한다. 반면 아이노드(Inode)는 파일 크기, 권한, 시간 정보 같은 메타데이터(Metadata)와 실제 데이터 블록(Data Block)의 위치 정보를 저장한다.</p>
<p>즉, 파일 접근 과정은 보통 <code>파일 이름 → 디렉토리 → 아이노드 → 데이터 블록</code>의 흐름으로 이해하면 좋다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/943ec1f6-352a-4a7d-abcd-3cfc4426add1.png" alt="" style="display:block;margin:0 auto" />

<h3>Tree-Structured Directory(트리 구조 디렉토리)</h3>
<p><code>트리 구조 디렉토리(Tree-Structured Directory)</code>는 루트 디렉토리(Root Directory)를 기준으로 하위 디렉토리(Subdirectory)가 계층적으로 확장되는 구조이다.</p>
<p>하나의 디렉토리(Directory) 아래에는 여러 개의 하위 디렉토리와 파일(File)이 포함될 수 있다. 이러한 구조가 반복되면서 전체 파일 시스템(File System)이 구성된다.</p>
<p>트리 구조 디렉토리는 파일을 목적별, 사용자별, 기능별로 구분하여 체계적으로 관리할 수 있게 해준다. 따라서 파일의 탐색(File Search)과 관리 효율이 높아 일반적으로 많이 사용되는 디렉토리 구조이다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/9c549bba-883d-4838-8a1c-daa97609bbfd.png" alt="" style="display:block;margin:0 auto" />

<h3>Tree-Structured Directory Diagram(트리 구조 디렉토리 그림 설명)</h3>
<p>그림을 보면 루트 디렉토리(Root Directory)를 기준으로 여러 사용자 디렉토리(User Directory)가 나뉘어 있는 것을 확인할 수 있다.</p>
<p>각 사용자 디렉토리 안에는 다시 하위 디렉토리(Subdirectory)와 파일(File)이 포함되어 있다. 이러한 구조가 반복되면서 전체 파일 시스템(File System)이 트리 구조(Tree Structure) 형태로 구성된다.</p>
<p>트리 구조 디렉토리(Tree-Structured Directory)는 경로(Path)를 따라 파일을 탐색할 수 있게 해준다. 또한 파일을 사용자별, 목적별, 종류별로 체계적으로 분류할 수 있기 때문에 파일 관리가 효율적이다.</p>
<p>따라서 트리 구조는 현재 가장 일반적으로 사용되는 디렉토리 구조라고 할 수 있다.</p>
<p>트리 구조에서는 파일의 위치를 경로(Path)로 표현한다. 예를 들어 <code>/user1/report.txt</code>와 같은 경로는 루트 디렉토리(Root Directory)에서 시작해 <code>user1</code> 디렉토리 안의 <code>report.txt</code> 파일에 접근한다는 의미이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/96b11f52-fbf3-48c4-afd3-b2d2859cc062.png" alt="" style="display:block;margin:0 auto" />

<h3>Graph-Structured Directory(그래프 구조 디렉토리)</h3>
<p>그래프 구조 디렉토리(Graph-Structured Directory)는 하나의 파일(File)이나 디렉토리(Directory)를 여러 경로(Path)에서 참조할 수 있는 구조이다.</p>
<p>즉, 동일한 파일을 여러 위치에서 공유(File Sharing)할 수 있다는 특징이 있다. 이를 통해 파일의 중복 저장을 줄이고, 데이터를 더 효율적으로 활용할 수 있다.</p>
<p>하지만 그래프 구조는 트리 구조(Tree Structure)보다 관리가 복잡하다. 특히 순환 구조(Cycle)가 발생하면 파일 탐색(File Search) 과정에서 같은 경로를 반복해서 따라가게 되어 무한 반복(Infinite Loop) 문제가 생길 수 있다.</p>
<p>따라서 실제 시스템에서는 순환이 발생하지 않도록 제한한 비순환 그래프(Acyclic Graph) 형태로 사용하는 경우가 많다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/913f3a83-3160-4ec2-90e1-c17267ef80cb.png" alt="" style="display:block;margin:0 auto" />

<h3>Graph-Structured Directory Diagram(그래프 구조 디렉토리 그림 설명)</h3>
<p>그래프 구조 디렉토리(Graph-Structured Directory)는 하나의 파일(File)이나 디렉토리(Directory)가 여러 경로(Path)를 통해 연결될 수 있는 구조이다. 즉, 하나의 파일이 두 개 이상의 디렉토리에서 참조되면서 동일한 데이터를 여러 위치에서 사용할 수 있다.</p>
<p>그림에서 실선은 기본적인 디렉토리 계층 구조(Hierarchical Structure)를 나타내고, 점선은 추가적인 참조(Reference)나 공유 관계(Sharing Relationship)를 의미한다.</p>
<p>예를 들어 <code>order</code> 아래의 <code>mail</code>, <code>expo</code>는 일반적인 하위 디렉토리 관계로 실선으로 연결되어 있다. 반면 <code>copy</code>는 다른 방식으로 연결되어 있으며, <code>copy</code> 아래의 항목은 실선뿐만 아니라 <code>program</code>, <code>expo</code>와도 점선으로 연결되어 있다.</p>
<p>이처럼 그래프 구조에서는 하나의 대상이 여러 위치에서 참조될 수 있고, 경우에 따라 순환 관계(Cycle)가 발생할 수도 있다. 순환 관계가 생기면 파일 탐색(File Search) 과정에서 반복이 발생하거나, 디렉토리 구조 관리가 복잡해질 수 있다.</p>
<p>따라서 그래프 구조 디렉토리는 파일 공유(File Sharing)에는 유리하지만, 탐색과 관리 측면에서는 추가적인 고려가 필요한 구조라고 이해할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/289ca5c8-bf36-4510-896e-8d23f6704425.png" alt="" style="display:block;margin:0 auto" />

<h3>UNIX File System Structure(UNIX 파일 시스템 구조)</h3>
<p>이제 파일 시스템(File System)이 디스크(Disk) 내부에서 어떻게 구성되는지 살펴본다.</p>
<p>UNIX 파일 시스템(UNIX File System)은 하나의 공간으로 구성되는 것이 아니라, 여러 영역으로 나뉘어 관리된다. 대표적으로 부트 블록(Boot Block), 슈퍼블록(Superblock), 아이노드 영역(Inode Area), 데이터 블록(Data Block)으로 구성되며, 각 영역은 서로 다른 역할을 수행한다.</p>
<p>이처럼 기능별로 영역을 분리해서 관리하면 파일 시스템의 효율성(Efficiency)과 안정성(Stability)을 높일 수 있다.</p>
<p>이 구조에서 가장 중요한 개념은 <code>아이노드(Inode)</code>이다. 아이노드는 파일의 실제 데이터 자체를 저장하는 공간이 아니라, 파일의 메타데이터(Metadata)와 데이터 위치 정보(Location Information)를 저장하는 구조이다.</p>
<p>예를 들어 아이노드에는 파일 크기(File Size), 접근 권한(Access Permission), 소유자 정보(Owner Information) 등이 포함된다. 실제 파일 데이터는 아이노드가 가진 포인터(Pointer)를 통해 데이터 블록(Data Block)에 접근하여 찾을 수 있다.</p>
<p>즉, 파일 이름(File Name)은 디렉토리(Directory)가 관리하고, 아이노드(Inode)는 파일의 정보와 실제 데이터 위치를 관리한다고 이해하면 된다.</p>
<p>UNIX/Linux 계열에서는 일반적으로 디렉토리(Directory)가 “파일 이름(File Name) → 아이노드 번호(Inode Number)”를 연결하고, 아이노드(Inode)가 “파일 정보(Metadata) → 데이터 블록(Data Block) 위치”를 연결한다.</p>
<p>따라서 파일 접근 흐름은 보통 다음과 같이 이해할 수 있다.</p>
<p><code>파일 이름(File Name) → 디렉토리(Directory) → 아이노드(Inode) → 데이터 블록(Data Block)</code></p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/31dfca53-5828-4182-88dc-f9fef3068e57.png" alt="" style="display:block;margin:0 auto" />

<h3>Inode Internal Structure(아이노드 내부 구조)</h3>
<p><code>아이노드(Inode)</code> 내부에는 여러 개의 포인터(Pointer)가 포함되어 있다. 이 포인터들은 실제 파일 데이터가 저장된 데이터 블록(Data Block)을 가리키는 역할을 한다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/df286d52-222d-4c20-ac6b-b5db4afee41c.png" alt="" style="display:block;margin:0 auto" />

<p>먼저 <code>직접 블록 포인터(Direct Block Pointer)</code>는 데이터 블록을 바로 가리키는 구조이다. 파일 크기가 작은 경우에는 직접 블록 포인터만으로도 충분히 데이터를 찾을 수 있다.</p>
<p>하지만 파일 크기가 커지면 직접 포인터만으로는 모든 데이터 블록을 가리키기 어렵다. 이때 <code>간접 포인터(Indirect Pointer)</code>가 사용된다.</p>
<p>일중 간접 포인터(Single Indirect Pointer)는 인덱스 블록(Index Block)을 한 번 거쳐 데이터 블록에 접근한다. <code>이중 간접 포인터(Double Indirect Pointer)와 삼중 간접 포인터(Triple Indirect Pointer)</code>는 인덱스 블록을 여러 단계 거쳐 더 많은 데이터 블록을 연결할 수 있다.</p>
<p>즉, 아이노드는 파일 크기에 따라 직접 포인터(Direct Pointer), 일중 간접 포인터(Single Indirect Pointer), 이중 간접 포인터(Double Indirect Pointer), 삼중 간접 포인터(Triple Indirect Pointer)를 사용해 확장된다. 이를 통해 작은 파일은 빠르게 접근하고, 큰 파일은 효율적으로 저장할 수 있다.</p>
<p>직접 포인터(Direct Pointer)는 데이터 블록을 바로 가리키기 때문에 접근 속도가 빠르다. 반면 간접 포인터(Indirect Pointer)는 중간에 인덱스 블록(Index Block)을 거치므로 접근 단계가 늘어나지만, 훨씬 큰 파일을 저장할 수 있게 해준다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/dfc8e47c-389c-445b-bb8f-b0cbf61c7545.png" alt="" style="display:block;margin:0 auto" />

<h3>Actual File Access Process(파일 접근의 실제 동작)</h3>
<p>실제 파일이 어떤 방식으로 접근되는지 살펴보자. 사용자는 파일 이름(File Name)으로 파일에 접근하지만, 운영체제(Operating System) 내부에서는 다른 방식으로 처리된다.</p>
<p>파일 이름은 주로 디렉토리(Directory)에서 사용된다. 운영체제는 디렉토리에서 파일 이름을 찾고, 해당 파일과 연결된 아이노드(Inode)를 확인한다. 실제 파일의 정보와 저장 위치는 이 아이노드를 통해 결정된다.</p>
<p>또한 프로그램(Process)은 파일 이름을 계속 사용하는 것이 아니라, 파일을 열 때 얻은 파일 디스크립터(File Descriptor, FD)를 기준으로 파일에 접근한다. 파일 디스크립터는 열린 파일을 식별하기 위해 운영체제가 제공하는 내부 번호이다.</p>
<p>즉, 사용자는 파일 이름으로 접근한다고 생각하지만, 운영체제 내부에서는 디렉토리(Directory), 아이노드(Inode), 파일 디스크립터(File Descriptor)를 통해 파일 접근이 이루어진다. 사용자가 보는 방식과 운영체제가 처리하는 방식이 다르다는 점이 핵심이다.</p>
<p>일반적으로 파일 접근 흐름은 다음과 같이 이해할 수 있다.</p>
<p><code>파일 이름(File Name) → 디렉토리(Directory) → 아이노드(Inode) → 파일 디스크립터(File Descriptor) → 데이터 블록(Data Block)</code></p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/5c244fe5-9d2e-49d7-a02c-d1b5c2d1974f.png" alt="" style="display:block;margin:0 auto" />

<h3>File Access Flow(파일 접근 흐름)</h3>
<p>실제 파일 접근 흐름은 먼저 파일 이름(File Name)을 기준으로 디렉토리(Directory)를 탐색하는 것에서 시작된다. 운영체제(Operating System)는 디렉토리에서 해당 파일 이름과 연결된 <code>아이노드 번호(Inode Number)</code>를 찾는다.</p>
<p>그다음 아이노드(Inode)를 통해 파일의 정보와 실제 데이터 위치를 확인한다. 아이노드에는 파일의 메타데이터(Metadata)와 <code>데이터 블록(Data Block)을 가리키는 위치 정보</code>가 저장되어 있다.</p>
<p>마지막으로 운영체제는 해당 <code>데이터 블록(Data Block)에 접근</code>하여 읽기(Read) 또는 쓰기(Write) 작업을 수행한다.</p>
<p>더 정확히는 파일 이름과 아이노드가 직접 연결되는 것이 아니라, 디렉토리(Directory)가 중간에서 파일 이름과 아이노드 번호(Inode Number)를 연결한다.</p>
<p>따라서 실제 흐름은 <code>파일 이름(File Name) → 디렉토리(Directory) → 아이노드(Inode) → 데이터 블록(Data Block)</code>으로 이해하면 더 정확하다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/70abe8c4-38a5-45fc-a825-9262af93283d.png" alt="" style="display:block;margin:0 auto" />

<h3>File Protection and Access Control(파일 보호와 접근 제어)</h3>
<p>여러 사용자가 하나의 파일을 함께 사용할 경우, 누구나 자유롭게 수정할 수 있다면 데이터가 덮어씌워지거나 삭제되는 문제가 발생할 수 있다. 그래서 운영체제(Operating System)는 파일을 보호하고, 사용자별로 접근을 제어하는 기능을 제공한다.</p>
<p>파일 보호(File Protection)는 허가되지 않은 접근을 막아 데이터를 안전하게 유지하는 기능이다. 특히 여러 사용자가 동시에 파일을 사용하는 환경에서는 파일 보호 기능이 필수적이다.</p>
<p>접근 제어(Access Control)는 사용자별로 읽기(Read), 쓰기(Write), 실행(Execute)과 같은 작업 권한을 구분하는 역할을 한다. 예를 들어 A 사용자는 파일을 읽기만 가능하고, B 사용자는 파일을 수정할 수 있도록 다르게 설정할 수 있다.</p>
<p>접근 제어 방식에는 ACL(Access Control List) 방식과 그룹 기반 방식(Group-Based Access Control)이 있다. ACL 방식은 사용자별로 세밀한 권한 설정이 가능하지만, 관리가 복잡해질 수 있다. 반면 그룹 기반 방식은 여러 사용자를 그룹으로 묶어 권한을 관리하기 때문에 상대적으로 단순하고 효율적이다.</p>
<p>리눅스/유닉스(Linux/Unix)에서는 파일 권한을 보통 소유자(User), 그룹(Group), 기타 사용자(Others)로 나누어 관리한다. 각 대상에 대해 읽기(Read), 쓰기(Write), 실행(Execute) 권한을 설정할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/48645f17-6971-4f77-b67a-094af60688ed.png" alt="" style="display:block;margin:0 auto" />

<h3>UNIX Permission Structure(UNIX 권한 구조)</h3>
<p>유닉스(UNIX)에서는 파일 접근 권한(File Permission)을 사용자 종류에 따라 구분해서 관리한다. 사용자는 크게 소유자(User/Owner), 그룹(Group), 기타 사용자(Others)로 나뉜다.</p>
<p>권한은 보통 <code>-rwx rwx rwx</code>와 같은 형태로 표현된다. 맨 앞의 문자는 파일 종류(File Type)를 의미한다. 일반 파일이면 <code>-</code>, 디렉토리(Directory)이면 <code>d</code>로 표시된다.</p>
<p>그 뒤의 권한은 세 묶음으로 나뉜다. 첫 번째 묶음은 소유자(User/Owner), 두 번째 묶음은 그룹(Group), 세 번째 묶음은 기타 사용자(Others)에 대한 권한을 나타낸다.</p>
<p>각 권한에서 <code>r</code>은 읽기(Read), <code>w</code>는 쓰기(Write), <code>x</code>는 실행(Execute)을 의미한다. 예를 들어 <code>rwx r-x r--</code>라고 표시되어 있다면, 소유자는 읽기, 쓰기, 실행이 모두 가능하다. 그룹 사용자는 읽기와 실행만 가능하고, 기타 사용자는 읽기만 가능하다는 뜻이다.</p>
<p>이처럼 유닉스는 사용자 그룹을 구분하고, 읽기(Read), 쓰기(Write), 실행(Execute) 권한을 조합하여 파일 접근을 제어한다. 지금까지는 파일 시스템(File System)의 구조, 파일 접근 방식, 그리고 접근 권한까지의 전체 흐름을 살펴보았다.</p>
<p>디렉토리(Directory)에서 실행 권한(Execute Permission)은 파일 실행과는 의미가 다르다. 디렉토리에 <code>x</code> 권한이 있어야 그 디렉토리 안으로 들어가거나 내부 파일에 접근할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/4301a6fe-0385-42ad-a34f-0b2057c6d542.png" alt="" style="display:block;margin:0 auto" />

<h3>Need for File Storage Structure(파일 저장 구조가 필요한 이유)</h3>
<p>파일 저장 구조(File Storage Structure)가 필요한 이유는 디스크(Disk)가 항상 연속된 공간을 보장하지 않기 때문이다. 메모리(Memory)처럼 파일을 하나의 연속된 덩어리로 저장하기 어려운 경우가 많다.</p>
<p>그래서 파일(File)은 여러 개의 블록(Block)으로 나뉘어 디스크의 서로 다른 위치에 저장될 수 있다. 또한 파일이 생성(Create), 삭제(Delete), 확장(Extend)되는 과정에서 블록의 배치도 계속 달라질 수 있다.</p>
<p>이처럼 파일의 저장 위치가 계속 변하기 때문에, 운영체제(Operating System)가 이를 관리하지 않으면 파일 위치를 추적하거나 접근하는 것이 어려워진다.</p>
<p>따라서 운영체제는 파일이 어떤 블록에 저장되어 있는지 관리하기 위해 파일 저장 구조(File Storage Structure)를 사용한다.</p>
<p>파일 저장 구조는 파일 할당 방식(File Allocation Method)과 연결된다. 대표적인 방식으로는 연속 할당(Contiguous Allocation), 연결 할당(Linked Allocation), 인덱스 할당(Indexed Allocation)이 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/72117c8b-4070-4921-a3c8-888d275ffd3a.png" alt="" style="display:block;margin:0 auto" />

<h3>File Storage Structure Diagram(파일 저장 구조 그림 설명)</h3>
<p>각 파일이 어떤 블록(Block)에 저장되어 있는지는 파일 테이블(File Table) 형태로 관리할 수 있다.</p>
<p>예를 들어 파일 A(File A)의 블록 번호가 <code>1, 3, 9</code>라면, 파일 A의 데이터가 디스크(Disk)의 1번, 3번, 9번 블록에 나누어 저장되어 있다는 의미이다.</p>
<p>저장장치(Storage Device)를 보면 이러한 블록들이 서로 떨어진 위치에 분산되어 저장될 수 있다. 따라서 파일을 읽을 때는 각 블록의 위치를 따라가며 데이터를 가져와야 한다.</p>
<p>운영체제(Operating System)는 이러한 블록 위치 정보(Block Location Information)를 관리한다. 그리고 분산되어 저장된 데이터를 하나의 연속된 파일(File)처럼 연결해주는 역할을 한다.</p>
<p>즉, 실제 저장은 여러 블록에 나뉘어 분산되어 있지만, 사용자는 하나의 파일처럼 사용할 수 있다. 이것이 파일 저장 구조(File Storage Structure)가 필요한 이유이다.</p>
<p>📌 파일이 여러 블록에 분산되어 저장되는 현상을 단편화(Fragmentation)와 연결해서 이해할 수 있다. 파일 시스템(File System)은 블록 위치를 관리하여 사용자가 단편화된 저장 구조를 직접 신경 쓰지 않아도 되게 한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/355af758-1934-4647-9fc3-af2219c9f1ae.png" alt="" style="display:block;margin:0 auto" />

<h3>File and Disk Storage Relationship(파일과 디스크 저장의 관계)</h3>
<p>이제 파일의 블록(Block)이 실제 디스크(Disk)에서 어떻게 관리되는지 살펴본다.</p>
<p>파일(File)은 사용자 입장에서 하나의 논리적인 순서(Logical Order)를 가진다. 예를 들어 파일은 <code>0, 1, 2, 3</code>과 같은 순서의 블록들로 구성되며, 이를 <code>논리 블록 번호(VBN, Virtual Block Number)</code>라고 한다.</p>
<p>하지만 실제 디스크에서는 이 데이터가 반드시 연속적으로 저장되는 것이 아니다. 파일의 각 블록은 디스크의 여러 물리적인 위치에 나누어 저장될 수 있으며, 이때 실제 디스크 위치를 나타내는 번호를 <code>물리 블록 번호(LBN, Logical Block Number)</code>라고 한다.</p>
<p>즉, 파일 내부에서는 논리 블록 번호(VBN)를 기준으로 순서가 정해지지만, 실제 디스크에서는 물리 블록 번호(LBN)를 기준으로 저장 위치가 결정된다.</p>
<p>파일 시스템(File System)은 이 두 구조를 연결하는 <code>매핑 정보(Mapping Information)</code>를 관리한다. 이를 통해 실제 블록들이 디스크에 흩어져 있어도 사용자는 파일을 논리적으로 연속된 데이터처럼 사용할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/b954cf18-5427-48e3-aa62-50bc2f7df2c6.png" alt="" style="display:block;margin:0 auto" />

<h3>File and Disk Storage Relationship 2 (파일과 디스크 저장의 관계 2)</h3>
<p>그림을 보면 왼쪽에는 <code>논리적인 파일 구조(Logical File Structure)</code>가 있고, 각 블록은 <code>논리 블록 번호(VBN, Virtual Block Number)</code>를 기준으로 순서가 정해져 있다.</p>
<p>반면 오른쪽의 디스크(Disk)에서는 데이터가 서로 떨어진 <code>물리 블록(Physical Block)</code>에 저장되어 있는 것을 확인할 수 있다. 즉, 파일 안에서는 <code>0, 1, 2, 3</code>처럼 순서가 있어도 실제 디스크에서는 연속된 위치에 저장되지 않을 수 있다.</p>
<p>이때 VBN에서 LBN으로 향하는 화살표는 운영체제(Operating System)가 논리 블록(Logical Block)을 물리 블록(Physical Block)으로 변환하는 <code>매핑(Mapping)</code> 과정을 의미한다.</p>
<p>예를 들어 논리 블록 0번이 디스크의 558번 위치에 저장될 수 있다. 이처럼 논리적인 순서와 실제 저장 위치는 서로 다르게 연결될 수 있다.</p>
<p>따라서 파일을 읽을 때는 단순히 디스크의 연속된 위치를 따라가는 것이 아니라, 운영체제가 매핑 정보(Mapping Information)를 이용해 각 물리 블록을 순서대로 찾아가며 데이터를 읽는다.</p>
<p>VBN(Virtual Block Number)은 파일 안에서의 논리적인 블록 순서를 의미하고, LBN(Logical Block Number)은 실제 저장장치에서 블록이 위치한 주소를 의미한다.</p>
<p>핵심은 사용자는 파일을 연속된 데이터처럼 보지만, 실제 디스크에서는 여러 위치에 나뉘어 저장될 수 있으며 운영체제가 이를 연결해준다는 점이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f0226a3b-9d3d-4371-b8df-af00d701996c.png" alt="" style="display:block;margin:0 auto" />

<h3>Physical Structure of Disk(디스크의 물리적 구조)</h3>
<p>파일이 저장되는 디스크(Disk)의 물리적 구조를 살펴보자. 디스크는 여러 개의 원판(Platter)으로 구성되어 있으며, 각 원판은 <code>트랙(Track)과 섹터(Sector)</code>로 나뉘어 데이터를 저장한다.</p>
<p>실제 데이터는 <code>섹터(Sector)</code> 단위로 저장된다. 디스크에 접근할 때는 원하는 위치로 헤드(Head)가 이동하는 시간과, 원판이 회전하면서 원하는 섹터가 헤드 아래로 올 때까지 기다리는 회전 지연 시간(Rotational Latency)이 발생한다.</p>
<p>즉, 디스크 접근에는 헤드 이동 시간(Seek Time)과 회전 지연 시간(Rotational Latency)이라는 비용이 포함된다. 그래서 디스크에서 데이터를 읽고 쓰는 속도는 단순히 데이터 크기뿐만 아니라, 데이터가 어디에 저장되어 있는지에도 영향을 받는다.</p>
<p>하지만 이러한 물리적인 구조는 사용자에게 직접 보이지 않는다. 파일 시스템(File System)이 디스크 구조를 추상화(Abstraction)해서 관리하기 때문에, 사용자는 복잡한 디스크 구조를 몰라도 파일(File) 단위로 데이터를 쉽게 사용할 수 있다.</p>
<p>이 설명은 전통적인 하드 디스크(HDD, Hard Disk Drive)에 해당한다. SSD(Solid State Drive)는 회전하는 원판이나 헤드가 없기 때문에 헤드 이동 시간(Seek Time)이나 회전 지연 시간(Rotational Latency)이 거의 없다. 다만 운영체제는 HDD든 SSD든 파일 시스템(File System)을 통해 파일을 논리적인 단위로 관리한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/8e65ceb2-0871-4036-9686-f373e1618c10.png" alt="" style="display:block;margin:0 auto" />

<h3>Disk Block and File Storage Structure(디스크 블록과 파일 저장 구조)</h3>
<p>디스크(Disk)는 실제로 <code>섹터(Sector)</code> 단위로 데이터를 저장하지만, 파일 시스템(File System)은 여러 섹터를 묶어 <code>블록(Block)</code> 단위로 관리한다. 이 블록은 파일 저장(File Storage)과 입출력(Input/Output)의 기본 단위가 된다.</p>
<p>운영체제(Operating System)는 데이터를 한 바이트(Byte)씩 처리하기보다는 블록 단위로 읽고 쓴다. 디스크 특성상 작은 단위로 여러 번 접근하는 것보다, 더 큰 단위로 한 번에 읽고 쓰는 것이 효율적이기 때문이다.</p>
<p>블록 안에는 <code>파일 데이터(File Data)</code>가 저장된다. 필요한 경우에는 다음 블록의 위치 정보(Location Information)가 <code>포인터(Pointer)</code> 형태로 포함될 수도 있다. 이는 파일의 여러 블록을 서로 연결해 관리하기 위한 방식이다.</p>
<p>블록 크기(Block Size)는 시스템에서 미리 정해진 값으로 사용되며, 보통 1KB, 2KB, 4KB처럼 2의 거듭제곱 형태로 설정된다. 즉, 파일 시스템은 디스크의 물리적인 섹터를 그대로 사용하는 것이 아니라, 여러 섹터를 묶은 블록 단위로 파일을 저장하고 관리한다.</p>
<p>블록 크기(Block Size)가 너무 작으면 관리해야 할 블록 수가 많아져 오버헤드(Overhead)가 커질 수 있다. 반대로 블록 크기가 너무 크면 작은 파일을 저장할 때 남는 공간이 생겨 내부 단편화(Internal Fragmentation)가 발생할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/e88babf9-eb3f-4b85-93cb-da0321766ffc.png" alt="" style="display:block;margin:0 auto" />

<h3>File and Block Relationship(파일과 블록의 관계)</h3>
<p>정리하면 하나의 파일(File)은 여러 개의 블록(Block)으로 구성된다. 이러한 블록들은 디스크(Disk)상에서 항상 연속적으로 저장되는 것이 아니라, 서로 떨어진 위치에 저장될 수도 있다.</p>
<p>파일의 크기(File Size)에 따라 필요한 블록의 개수가 결정된다. 파일이 작으면 적은 수의 블록만 필요하고, 파일이 커질수록 더 많은 블록이 필요하다.</p>
<p>파일의 읽기(Read)와 쓰기(Write) 작업도 모두 블록 단위로 이루어진다. 운영체제(Operating System)는 파일을 바이트(Byte) 단위로 하나씩 처리하기보다는, 블록 단위로 데이터를 읽고 쓰는 방식으로 관리한다.</p>
<p>이때 블록의 배치 방식(Block Allocation Method)에 따라 파일 접근 성능(File Access Performance)이 달라질 수 있다. 예를 들어 블록이 연속적으로 배치되어 있으면 빠르게 접근할 수 있지만, 여러 위치에 흩어져 있으면 접근 속도가 느려질 수 있다.</p>
<p>접근 속도가 느려지는 이유는 디스크 접근 횟수가 늘어날 수 있기 때문이다. 특히 HDD(Hard Disk Drive)에서는 헤드 이동 시간(Seek Time)이 발생하기 때문에 성능에 영향을 줄 수 있다. 반면 SSD(Solid State Drive)는 물리적인 헤드 이동이 없어서 흩어진 블록에 접근할 때의 영향이 HDD보다 상대적으로 작다.</p>
<p>따라서 이후에 살펴볼 파일 할당 방식(File Allocation Method)에서는 블록을 어떻게 배치하고 관리할 것인지가 중요한 요소가 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/615c086e-4caa-4dcc-baa8-94d0c2ce1853.png" alt="" style="display:block;margin:0 auto" />

<h3>Characteristics of Block-Based Storage(블록 기반 저장의 특징)</h3>
<p>디스크(Disk)는 데이터를 블록(Block) 단위로 저장하고 관리한다. 이러한 블록 기반 저장 방식에는 몇 가지 특징이 있다.</p>
<p>먼저 파일의 크기(File Size)가 블록 크기(Block Size)보다 작은 경우, 블록 내부에 사용되지 않는 공간이 발생할 수 있다. 이를 <code>내부 단편화(Internal Fragmentation)</code>라고 한다.</p>
<p>내부 단편화(Internal Fragmentation)는 할당된 블록 안에서 남는 공간이 생기는 문제이다. 예를 들어 블록 크기가 4KB인데 파일 크기가 1KB라면, 나머지 3KB는 사용되지 않는 공간으로 남을 수 있다.</p>
<p>하지만 블록 단위로 데이터를 처리하면 디스크 입출력(Disk I/O)을 한 번에 수행할 수 있기 때문에, 전체적인 입출력 성능은 효율적으로 향상될 수 있다.</p>
<p>또한 파일 시스템(File System)은 각 블록의 위치 정보를 메타데이터(Metadata) 형태로 관리한다. 따라서 블록이 어떻게 배치되어 있는지에 따라 파일 접근 속도(File Access Speed)가 달라질 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/8a2b9746-55bd-41f6-93f1-145e123f2836.png" alt="" style="display:block;margin:0 auto" />

<h3>Key Design Issues in File Storage Structure(저장 구조 설계의 핵심 문제)</h3>
<p>파일을 여러 블록(Block)으로 나누어 저장할 때는 이 블록들을 어떤 방식으로 배치할 것인지 결정해야 한다. 이것이 <code>파일 저장 구조(File Storage Structure) 설계의 핵심 문제</code>이다.</p>
<p>블록을 연속적으로 배치하면 파일 접근 속도(File Access Speed)는 빨라질 수 있다. 하지만 연속된 빈 공간을 확보해야 하므로 저장 공간 효율(Storage Efficiency)이 떨어질 수 있다.</p>
<p>반대로 블록을 흩어진 위치에 저장하면 디스크 공간을 더 유연하게 사용할 수 있지만, 파일을 읽을 때 여러 위치를 따라가야 하므로 접근 속도가 느려질 수 있다.</p>
<p>즉, 파일 저장 구조에는 성능(Performance)과 저장 효율(Efficiency) 사이의 트레이드 오프(Trade-off)가 존재한다.</p>
<p>또한 파일 시스템(File System)은 내부 단편화(Internal Fragmentation)를 줄이고, 파일 크기 변화에도 유연하게 대응할 수 있어야 한다. 결국 파일 시스템 설계에서는 성능, 효율, 유연성(Flexibility) 사이의 균형을 맞추는 것이 중요하다.</p>
<p>이러한 설계 문제를 해결하기 위해 파일 시스템은 연속 할당(Contiguous Allocation), 연결 할당(Linked Allocation), 인덱스 할당(Indexed Allocation)과 같은 파일 할당 방식(File Allocation Method)을 사용한다. 더 자세한 내용은 앞서 배워볼 예정이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/b1a2f8b5-39ec-48d3-ab49-ab2778ffdbde.png" alt="" style="display:block;margin:0 auto" />

<h3>Overview of File Allocation Methods(파일 할당 방식의 개요)</h3>
<p>파일 할당 방식(File Allocation Method)에서 중요한 것은 파일을 구성하는 블록(Block)을 디스크(Disk)에 어떻게 배치할 것인가이다.</p>
<p>즉, 파일 할당 방식은 파일의 여러 블록을 디스크의 어느 위치에 저장하고, 이 블록들을 어떻게 연결해서 관리할지를 결정하는 방법이다.</p>
<p>할당 방식에 따라 파일 접근 속도(File Access Speed)와 디스크 공간 활용 방식(Disk Space Utilization)이 달라진다. 따라서 파일 할당 방식은 파일 시스템(File System)의 성능을 결정하는 중요한 개념이다.</p>
<p>대표적인 파일 할당 방식(File Allocation Method)에는 연속 할당(Contiguous Allocation), 연결 할당(Linked Allocation), 인덱스 할당(Indexed Allocation)이 있다. 각각 접근 속도, 저장 효율, 파일 크기 변화 대응 방식에서 차이가 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/0c2d9f0d-4dd9-4bb4-b422-da3e7c364e80.png" alt="" style="display:block;margin:0 auto" />

<h3>Contiguous Allocation(연속 할당)</h3>
<p><code>연속 할당(Contiguous Allocation)</code>은 파일을 디스크(Disk)의 하나의 연속된 구간에 저장하는 방식이다. 가장 직관적이고 단순한 파일 할당 방식(File Allocation Method)이라고 할 수 있다.</p>
<p>이 방식에서는 파일의 시작 위치(Start Block)와 길이(Length)만 알면 전체 데이터를 쉽게 찾을 수 있다. 따라서 파일의 저장 구조가 단순하고 관리하기 쉽다.</p>
<p>또한 데이터 블록(Block)이 연속적으로 배치되기 때문에 파일 접근 속도(File Access Speed)가 빠르다는 장점이 있다. 특히 파일을 순서대로 읽는 순차 접근(Sequential Access)에 효율적이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/0a2bffea-788d-4466-af42-ad8e163165d0.png" alt="" style="display:block;margin:0 auto" />

<h3>Contiguous Allocation Structure(연속 할당의 구조)</h3>
<p><code>연속 할당(Contiguous Allocation)</code>의 구조를 그림으로 살펴보면, 디렉토리(Directory)에 파일 이름(File Name), 시작 블록(Start Block), 길이(Length)가 함께 저장되어 있는 것을 확인할 수 있다.</p>
<p>예를 들어 <code>CTR.doc</code> 파일의 시작 블록이 <code>1</code>이고 길이가 <code>4</code>라면, 디스크(Disk)의 1번 블록부터 4개의 연속된 블록을 이 파일이 사용한다는 의미이다.</p>
<p>오른쪽 디스크 구조를 보면 파일이 중간에 끊기지 않고 연속된 공간을 차지하고 있다. 이처럼 연속 할당은 파일을 시작 위치와 길이로 관리하며, 하나의 연속된 구간에 저장하는 방식이다.</p>
<p>연속 할당은 시작 블록과 길이만 알면 원하는 위치를 계산하기 쉽기 때문에 직접 접근(Direct Access)에 유리하다. 예를 들어 파일의 세 번째 블록에 접근하려면 <code>시작 블록 + 2</code>처럼 계산할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a1d41225-ba3e-464d-b0f6-a71e8fe994e5.png" alt="" style="display:block;margin:0 auto" />

<h3>Characteristics of Contiguous Allocation(연속 할당의 특징)</h3>
<p><code>연속 할당(Contiguous Allocation)</code>은 데이터 블록(Block)이 디스크(Disk)에 연속적으로 저장되는 방식이다. 따라서 디스크 헤드 이동(Disk Head Movement)이 최소화되어 높은 입출력 성능(I/O Performance)을 보일 수 있다.</p>
<p>이 방식은 파일을 처음부터 순서대로 읽는 순차 접근(Sequential Access)에도 효율적이고, 특정 위치를 바로 찾아가는 직접 접근(Direct Access)에도 효율적이다.</p>
<p>하지만 단점도 존재한다. 파일을 삭제하거나 이동하는 과정에서 디스크 중간중간에 빈 공간이 생기면서 단편화(Fragmentation) 문제가 발생할 수 있다. 이로 인해 큰 파일을 저장할 때 필요한 연속된 공간을 확보하기 어려워질 수 있다.</p>
<p>또한 파일 크기(File Size)가 증가했을 때 뒤쪽에 연속된 빈 공간이 없다면, 파일 전체를 다른 위치로 이동해야 하는 문제가 생길 수 있다.</p>
<p>그래서 실제 파일 시스템(File System)에서는 연속 할당을 그대로 사용하기보다는 익스텐트(Extent) 기반 방식으로 변형해서 사용하는 경우가 많다. 이 부분은 이후에 다시 살펴보자</p>
<p>📌<code>익스텐트(Extent)</code>는 여러 개의 연속된 블록을 하나의 묶음으로 관리하는 방식이다. 즉, 파일 전체가 하나의 연속 구간에 있어야 하는 것은 아니지만, 가능한 한 연속된 블록 묶음 단위로 저장하여 성능과 유연성을 함께 확보하려는 방식이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/3869dcd9-661c-482e-93dc-a7a436e22772.png" alt="" style="display:block;margin:0 auto" />

<h3>Linked Allocation(연결 할당)</h3>
<p><code>연결 할당(Linked Allocation)</code>은 연속 할당(Contiguous Allocation)의 단점을 보완하기 위한 방식이다. 이 방식은 파일을 여러 블록(Block)으로 나누어 저장하고, 각 블록을 <code>포인터(Pointer)</code>로 연결하여 하나의 파일로 구성한다.</p>
<p>각 블록에는 다음 블록의 위치 정보(Location Information)가 포함되어 있다. 운영체제(Operating System)는 이 포인터를 따라가면서 파일 데이터를 순서대로 읽는다.</p>
<p>연결 할당은 파일을 반드시 연속된 공간에 저장할 필요가 없다. 따라서 디스크(Disk)의 흩어진 빈 공간을 활용할 수 있고, 파일 크기(File Size)가 증가할 때도 비교적 유연하게 확장할 수 있다.</p>
<p>연결 할당은 순차 접근(Sequential Access)에는 적합하지만, 특정 위치로 바로 이동하는 직접 접근(Direct Access)에는 불리하다. 중간 블록에 접근하려면 첫 블록부터 포인터를 따라가야 하기 때문이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/2190c304-9911-4658-b378-c5d8072f0930.png" alt="" style="display:block;margin:0 auto" />

<h3>Linked Allocation Diagram(연결 할당 그림 설명)</h3>
<p>그림을 보면 하나의 파일(File)이 연속된 공간이 아니라 여러 디스크 블록(Disk Block)에 나누어 저장되어 있는 것을 확인할 수 있다.</p>
<p>예를 들어 <code>COT.doc</code> 파일의 시작 블록(Start Block)이 18번이라고 하면, 18번 블록에는 다음 블록인 19번의 위치 정보가 저장되어 있다. 19번 블록으로 이동하면 다시 2번 블록으로 연결된다.</p>
<p>2번 블록에는 더 이상 다음 블록을 가리키는 연결 정보가 없으므로, 이 지점이 파일의 끝(End of File)을 의미한다. 즉, 이 파일은 <code>18 → 19 → 2</code> 순서로 구성되어 있다고 볼 수 있다.</p>
<p>이처럼 연결 할당(Linked Allocation)은 각 블록이 다음 블록의 위치를 <code>포인터(Pointer)</code>로 가지고 있기 때문에, 처음 블록만 알면 나머지 블록들을 순서대로 따라가며 파일을 읽을 수 있다.</p>
<p>따라서 파일은 물리적으로는 디스크 여러 위치에 흩어져 저장되어 있지만, 논리적으로는 하나의 파일처럼 동작한다.</p>
<p>연결 할당(Linked Allocation)은 연속된 빈 공간이 필요 없기 때문에 저장 공간 활용에는 유리하다. 하지만 중간 블록에 바로 접근하려면 앞에서부터 포인터를 따라가야 하므로 직접 접근(Direct Access) 성능은 떨어질 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/7af439f1-eb51-4fe8-8b43-12670abd82e2.png" alt="" style="display:block;margin:0 auto" />

<h3>Characteristics of Linked Allocation(연결 할당의 특징)</h3>
<p><code>연결 할당(Linked Allocation)</code>은 파일을 앞에서부터 순서대로 읽는 순차 접근(Sequential Access)에서는 큰 문제가 없다. 각 블록(Block)의 포인터(Pointer)를 따라가며 차례대로 읽으면 되기 때문이다.</p>
<p>하지만 파일의 중간 위치에 바로 접근하려면 문제가 생긴다. 원하는 블록으로 바로 이동할 수 없고, 처음 블록부터 순서대로 포인터를 따라가야 한다. 이러한 방식을 <code>포인터 체이싱(Pointer Chasing)</code>이라고 한다.</p>
<p>또한 블록들이 디스크(Disk) 전체에 흩어져 저장될 수 있기 때문에, 파일을 읽을 때마다 디스크 헤드 이동(Disk Head Movement)이 자주 발생할 수 있다. 이로 인해 <code>탐색 비용(Seek Cost)</code>이 증가하고 접근 속도가 느려질 수 있다.</p>
<p>중간 블록이 손상될 경우에도 문제가 발생한다. 해당 블록 이후의 연결 정보가 끊어지면 파일의 일부를 읽지 못할 수 있다.</p>
<p>이러한 단점을 보완하기 위해 포인터 정보를 각 블록 안에 저장하지 않고, 한곳에 모아서 관리하는 <code>FAT(File Allocation Table)</code> 방식이 사용되기도 한다.</p>
<p><code>FAT(File Allocation Table)</code>는 파일의 각 블록이 다음에 어떤 블록으로 이어지는지를 별도의 테이블에 저장하는 방식이다. 연결 할당의 기본 개념은 유지하되, 블록 연결 정보를 한곳에서 관리한다는 점이 특징이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/c6262565-a648-4567-98d7-4fdeb53f0a67.png" alt="" style="display:block;margin:0 auto" />

<h3>Indexed Allocation(인덱스 할당)</h3>
<p>앞서 <code>연속 할당(Contiguous Allocation)</code>과 <code>연결 할당(Linked Allocation)</code>을 살펴보았다. 연속 할당은 접근 속도가 빠르지만 연속된 공간을 확보하기 어렵고, 연결 할당은 파일 크기 변화에 유연하지만 직접 접근 속도가 느리다는 문제가 있다.</p>
<p>이러한 단점을 보완하기 위해 사용하는 방식이 <code>인덱스 할당(Indexed Allocation)</code>이다.</p>
<p>인덱스 할당은 데이터 블록(Data Block)을 서로 직접 연결하지 않고, 별도의 인덱스 블록(Index Block)에 각 데이터 블록의 위치 정보를 모아서 저장하는 방식이다.</p>
<p>파일을 읽을 때는 블록을 처음부터 순차적으로 따라가는 것이 아니라, 인덱스 블록을 통해 원하는 데이터 블록의 위치를 바로 확인할 수 있다.</p>
<p>따라서 인덱스 할당은 연결 할당보다 직접 접근(Direct Access)에 유리하고, 파일이 디스크의 여러 위치에 나뉘어 저장되어도 관리가 비교적 효율적이다.</p>
<p>인덱스 할당(Indexed Allocation)은 인덱스 블록(Index Block)을 추가로 사용하기 때문에, 파일마다 인덱스 정보를 저장하기 위한 별도의 공간이 필요하다. 즉, 접근 성능은 좋아지지만 인덱스 블록을 위한 저장 공간 오버헤드(Overhead)가 발생할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/5582398b-e86a-400e-be35-ee555a3351bf.png" alt="" style="display:block;margin:0 auto" />

<h3>Indexed Allocation Structure(인덱스 할당 구조)</h3>
<p>그림을 보면 <code>디렉토리(Directory)</code>에 <code>파일 이름(File Name)</code>과 함께 <code>인덱스 블록(Index Block)</code>의 위치가 저장되어 있다.</p>
<p>예를 들어 <code>test</code> 파일의 인덱스 블록이 8번이라면, 8번 블록에는 <code>18, 20, 5, 7, 14, 9</code>와 같이 실제 데이터 블록(Data Block)의 위치가 목록 형태로 저장되어 있다.</p>
<p>마지막 값이 <code>-1</code>인 경우에는 더 이상 연결된 블록이 없다는 의미이며, 파일의 끝(End of File)을 나타내는 표시로 이해할 수 있다.</p>
<p>이처럼 인덱스 블록(Index Block)은 파일을 구성하는 데이터 블록의 위치를 한곳에 모아서 관리한다. 따라서 실제 데이터가 디스크(Disk)에 흩어져 있어도 필요한 블록의 위치를 바로 확인할 수 있다.</p>
<p>즉, 인덱스 할당(Indexed Allocation)은 연결 할당(Linked Allocation)처럼 블록을 순차적으로 따라갈 필요 없이, 인덱스 블록을 통해 원하는 데이터 블록에 직접 접근할 수 있는 방식이다.</p>
<p>인덱스 할당(Indexed Allocation)은 직접 접근(Direct Access)에 유리하지만, 인덱스 블록(Index Block)을 따로 저장해야 하므로 추가 저장 공간이 필요하다. 또한 파일이 매우 커지면 하나의 인덱스 블록만으로 모든 데이터 블록 위치를 저장하기 어려울 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/70110ca2-c001-47d2-b117-8b15ab2edca7.png" alt="" style="display:block;margin:0 auto" />

<h3>Characteristics of Indexed Allocation(인덱스 할당 방식의 특징)</h3>
<p>인덱스 할당(Indexed Allocation)은 인덱스 블록(Index Block)을 통해 데이터 블록(Data Block)의 위치를 한 번에 확인할 수 있는 방식이다.</p>
<p>이 방식은 원하는 블록의 위치를 바로 찾을 수 있기 때문에 직접 접근(Direct Access)이 가능하다. 따라서 처음 블록부터 순서대로 따라가야 하는 연결 할당(Linked Allocation)보다 접근 성능이 더 빠르다.</p>
<p>또한 데이터 블록들이 디스크(Disk)에 흩어져 있어도 인덱스 블록에 전체 위치 정보가 정리되어 있기 때문에 관리가 비교적 쉽다.</p>
<p>하지만 하나의 인덱스 블록에 저장할 수 있는 주소(Address)의 수에는 제한이 있다. 따라서 큰 파일을 저장할 때는 하나의 인덱스 블록만으로 모든 데이터 블록을 가리키기 어려워질 수 있다.</p>
<p>이를 해결하기 위해 <code>다단계 인덱스 구조(Multi-Level Index Structure)</code>나 유닉스(UNIX)의 아이노드(Inode)처럼 <code>직접 포인터(Direct Pointer)</code>와 <code>간접 포인터(Indirect Pointer)</code>를 함께 사용하는 방식이 사용된다.</p>
<p>다만 간접 포인터를 사용하는 경우에는 중간의 간접 블록(Indirect Block)을 거쳐야 하므로 추가적인 접근 비용(Access Cost)이 발생할 수 있다.</p>
<p>인덱스 할당(Indexed Allocation)은 연속 할당(Contiguous Allocation)과 연결 할당(Linked Allocation)의 단점을 보완한 방식이다. 직접 접근이 가능하면서도 파일이 반드시 연속된 공간에 저장될 필요가 없기 때문에, 성능과 유연성의 균형을 맞추는 방식이라고 이해하면 좋다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/5089372c-67e5-49ae-8023-6af360a6f1ae.png" alt="" style="display:block;margin:0 auto" />

<h3>Comparison of File Allocation Methods(파일 할당 방식 비교)</h3>
<p>지금까지 세 가지 파일 할당 방식(File Allocation Method)을 살펴보았다.</p>
<p>연속 할당(Contiguous Allocation)은 구조가 단순하고 파일 접근 속도(File Access Speed)가 빠르다. 하지만 파일을 저장하기 위해 연속된 빈 공간이 필요하기 때문에 디스크 공간 활용(Disk Space Utilization)에 제약이 생길 수 있다.</p>
<p>연결 할당(Linked Allocation)은 파일 블록(Block)을 흩어진 공간에 저장할 수 있어 공간 활용에는 유리하다. 하지만 원하는 블록에 접근하려면 처음부터 포인터(Pointer)를 따라가야 하므로 접근 효율이 떨어질 수 있다.</p>
<p>인덱스 할당(Indexed Allocation)은 데이터 블록의 위치 정보를 인덱스 블록(Index Block)에 모아서 관리한다. 따라서 직접 접근(Direct Access)이 가능하고 구조적으로 균형이 잡혀 있다. 다만 별도의 인덱스 공간(Index Space)이 필요하다는 특징이 있다.</p>
<p>정리하면 연속 할당은 빠르지만 공간 확보가 어렵고, 연결 할당은 유연하지만 접근 속도가 느릴 수 있으며, 인덱스 할당은 접근성과 유연성의 균형을 제공하지만 추가 공간이 필요하다.</p>
<p>파일 할당 방식은 파일 시스템(File System)의 성능과 저장 효율을 결정하는 중요한 요소이다. 실제 파일 시스템에서는 하나의 방식만 단순히 사용하기보다, 익스텐트(Extent), 아이노드(Inode), 인덱스 구조(Index Structure) 등을 조합하여 성능과 효율을 함께 고려하는 경우가 많다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/2b6ec228-3128-47eb-a8af-d59b585f7264.png" alt="" style="display:block;margin:0 auto" />

<h3>Design Considerations for File Systems(파일 시스템 설계 고려 요소)</h3>
<p>결국 파일 시스템(File System)을 설계할 때는 하나의 방식만 고려하는 것이 아니라 여러 요소를 함께 살펴봐야 한다.</p>
<p>먼저 파일 접근 패턴(File Access Pattern)에 따라 적합한 저장 방식이 달라진다. 파일을 처음부터 순서대로 읽는 순차 접근(Sequential Access)의 비중이 높은지, 특정 위치에 바로 접근하는 직접 접근(Direct Access) 또는 임의 접근(Random Access)의 비중이 높은지에 따라 효율적인 할당 방식이 달라질 수 있다.</p>
<p>또한 디스크 접근 비용(Disk Access Cost)도 중요한 요소이다. 특히 HDD(Hard Disk Drive)에서는 헤드 이동 시간(Seek Time)과 회전 지연 시간(Rotational Latency)이 발생하기 때문에, 블록(Block)이 어떻게 배치되어 있는지가 성능에 영향을 준다.</p>
<p>저장 효율(Storage Efficiency)과 접근 성능(Access Performance) 사이에는 항상 트레이드 오프(Trade-off)가 존재한다. 따라서 실제 파일 시스템에서는 하나의 방식만 사용하는 것이 아니라 여러 방식을 결합한 하이브리드 구조(Hybrid Structure)를 사용하는 경우가 많다.</p>
<p>예를 들어 ext4와 같은 파일 시스템(File System)에서는 연속 할당(Contiguous Allocation)을 확장한 익스텐트(Extent) 방식과 인덱스 구조(Index Structure)를 함께 활용한다. 이를 통해 연속적인 저장의 성능 장점과 큰 파일을 관리하기 위한 유연성을 함께 확보할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/2d9df1d4-9c87-4527-8d84-d2bf6fb82659.png" alt="" style="display:block;margin:0 auto" />

<h3>File System Operation in Practice(파일 시스템의 실제 동작)</h3>
<p>이제 앞에서 살펴본 개념들이 실제 시스템(System)에서 어떻게 동작하는지 살펴본다.</p>
<p>프로그램이 실행되면 파일이 생성(Create)되거나, 기존 파일에 접근(Access)하고, 데이터를 읽고 쓰는 작업(Read/Write)이 이루어진다. 사용자는 파일 이름(File Name)을 기준으로 파일을 사용하지만, 운영체제(Operating System) 내부에서는 디렉토리(Directory), 아이노드(Inode), 파일 디스크립터(File Descriptor), 오프셋(Offset) 등을 통해 파일 접근이 처리된다.</p>
<p>이번 내용에서는 프로그램 실행 과정에서 파일이 어떻게 생성되고 접근되는지, 그리고 이러한 작업이 시스템 내부에서 어떤 구조로 처리되는지를 중심으로 살펴본다.</p>
<p>파일을 <code>open</code>하면 운영체제는 파일 이름을 찾아 아이노드(Inode) 정보를 확인하고, 열린 파일을 관리하기 위해 파일 디스크립터(File Descriptor)를 반환한다. 이후 프로그램은 파일 이름이 아니라 이 파일 디스크립터를 사용해 <code>read</code>, <code>write</code>, <code>close</code> 같은 작업을 수행한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/bd784436-d9e8-4739-98d0-dbb9deed16dd.png" alt="" style="display:block;margin:0 auto" />

<h3>File Descriptor(fd)(파일 디스크립터)</h3>
<p>실습에서는 먼저 <code>파일 디스크립터(File Descriptor, fd)</code>에 대해 살펴본다.</p>
<p>시스템 내부에서 파일은 파일 이름(File Name)으로 계속 접근되는 것이 아니라, 파일 디스크립터(fd)라는 <code>정수값(Integer Value)</code>을 통해 접근된다.</p>
<p>프로그램이 <code>open</code> 함수를 호출하면 운영체제(Operating System)는 해당 파일을 열고, 그 파일을 식별하기 위한 파일 디스크립터를 반환한다.</p>
<p>이후 <code>read</code>나 <code>write</code> 같은 파일 입출력(File I/O) 연산은 파일 이름이 아니라 fd를 기준으로 수행된다. 즉, 사용자는 파일 이름으로 파일을 구분하지만, 운영체제는 열린 파일을 문자열이 아닌 정수값인 파일 디스크립터로 관리한다고 이해할 수 있다.</p>
<p>파일 디스크립터(fd)는 프로세스(Process)마다 따로 관리된다. 같은 숫자의 fd라도 프로세스가 다르면 서로 다른 파일을 가리킬 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/62709e8d-7134-41ae-b9b8-345ba66b2020.png" alt="" style="display:block;margin:0 auto" />

<h3>Core Code: open() and write()</h3>
<p>핵심 코드를 통해 파일 디스크립터(File Descriptor, fd)가 어떻게 사용되는지 살펴보자.</p>
<p><code>int fd = open("lab1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);</code></p>
<p>open() 함수는 파일을 열고, 그 결과로 파일 디스크립터(fd)를 반환한다. 이때 옵션(Option)을 통해 파일을 여는 방식을 결정한다.</p>
<p><code>O_CREAT</code>는 파일이 없으면 새로 생성하겠다는 의미이다. <code>O_TRUNC</code>는 파일이 이미 존재할 경우 기존 내용을 지우겠다는 의미이다. <code>O_WRONLY</code>는 파일을 쓰기 전용(Write Only)으로 열겠다는 의미이다.</p>
<p>따라서 이 코드는 lab1.txt 파일이 없으면 새로 만들고, 이미 존재한다면 기존 내용을 지운 뒤 쓰기 전용으로 여는 코드이다.</p>
<p>뒤에 있는 0644는 <code>파일 권한(File Permission)</code>을 의미한다. 소유자(Owner)는 읽기(Read)와 쓰기(Write)가 가능하고, 그룹(Group)과 기타 사용자(Others)는 읽기만 가능한 권한이다.</p>
<p>다음으로 write() 함수는 파일 이름이 아니라 fd를 기준으로 데이터를 기록한다.</p>
<p><code>write(fd, "ABC\n", 4);</code></p>
<p>여기서 "ABC\n"은 기록할 문자열이고, 4는 총 4바이트(Byte)를 기록한다는 의미이다. 즉 A, B, C 세 글자와 줄바꿈 문자(Newline)까지 포함해 총 4바이트가 파일에 저장된다.</p>
<p>fd는 커널(Kernel)의 <code>열린 파일 테이블(Open File Table)</code>과 연결되어 있기 때문에, 파일의 상태와 현재 위치 정보도 함께 관리된다. 또한 동일한 파일이라도 open()을 다시 호출하면 새로운 fd가 생성된다. 이 부분은 파일 디스크립터의 동작을 이해하는 데 중요한 특징이므로 반드시 기억해야 한다.</p>
<p>📌 O_TRUNC는 “기존 파일을 삭제”하는 것이 아니라, 파일 자체는 유지하되 기존 내용을 비워서 파일 크기를 0으로 만드는 옵션이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/3a97be9c-2358-4497-acd9-d97f8b88513b.png" alt="" style="display:block;margin:0 auto" />

<h3>File Descriptor(fd)(파일 디스크립터)</h3>
<p><code>open()</code> 함수는 파일(File)을 생성하거나 열면서 접근 권한(Access Permission)과 접근 방식(Access Mode)을 함께 설정한다.</p>
<p>예를 들어 <code>O_WRONLY | O_CREAT | O_TRUNC</code>와 같은 옵션(Option)을 사용하면 파일을 쓰기 전용으로 열지, 파일이 없을 때 새로 만들지, 기존 내용을 비울지를 결정할 수 있다.</p>
<p>파일을 열면 운영체제(Operating System)는 해당 파일을 식별하기 위한 파일 디스크립터(File Descriptor, fd)를 생성한다. 이후 프로그램은 파일 이름(File Name)이 아니라 이 fd를 기준으로 파일에 접근한다.</p>
<p><code>write()</code> 함수는 메모리(Memory)에 있는 데이터를 파일에 기록하는 역할을 한다. 이 과정에서도 파일 이름을 직접 사용하는 것이 아니라, <code>open()</code>을 통해 얻은 파일 디스크립터(fd)를 사용한다.</p>
<p>즉, 파일은 사용자 입장에서는 이름으로 구분되지만, 프로그램 실행 중에는 파일 디스크립터(fd)를 통해 관리되고 접근된다고 이해할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/6601767a-6c73-4d95-ba3f-8961d1927592.png" alt="" style="display:block;margin:0 auto" />

<h3>File Descriptor Example(파일 디스크립터 실제 예제)</h3>
<p>파일 디스크립터(File Descriptor, fd)의 실제 예제를 살펴보자.</p>
<p><code>open()</code> 함수는 <code>lab1.txt</code> 파일을 열도록 되어 있다. 이때 <code>O_CREAT</code> 옵션이 있으면 파일이 존재하지 않을 경우 새로 생성하고, <code>O_TRUNC</code> 옵션이 있으면 기존 파일의 내용을 비워 초기화한다.</p>
<p><code>open()</code> 함수가 실행되면 결과로 파일을 식별하는 정수값(Integer Value)인 fd가 반환된다. 이 fd는 열린 <code>lab1.txt</code> 파일을 가리키는 역할을 한다.</p>
<p>이후 <code>write()</code> 함수는 파일 이름이 아니라 fd를 기준으로 데이터를 기록한다. 즉, <code>lab1.txt</code>라는 이름을 직접 사용하는 것이 아니라, <code>open()</code>을 통해 얻은 fd를 이용해 해당 파일에 데이터를 쓰는 방식이다.</p>
<p>마지막으로 <code>close()</code> 함수를 호출하면 파일 사용이 끝났음을 운영체제(Operating System)에 알리고, 파일과 관련된 자원(Resource)을 반환한다.</p>
<p>정리하면 파일 접근 흐름은 <code>open() → fd 반환 → write(fd) → close(fd)</code> 순서로 이루어진다.</p>
<p><code>close()</code>를 호출하지 않으면 열린 파일과 관련된 자원이 계속 유지될 수 있다. 따라서 파일 사용이 끝나면 <code>close(fd)</code>를 통해 파일을 닫는 것이 중요하다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/6b9f76d2-2398-4991-94ed-3195c168c3b6.png" alt="" style="display:block;margin:0 auto" />

<h3>File Descriptor Execution Result(파일 디스크립터 실행 결과)</h3>
<p>실행 결과를 보면 fd 값이 <code>3</code>으로 출력된 것을 확인할 수 있다. 이 값은 운영체제(Operating System)가 해당 파일에 할당한 파일 디스크립터 번호(File Descriptor Number)이다.</p>
<p>일반적으로 파일 디스크립터 <code>0</code>, <code>1</code>, <code>2</code>는 미리 정해진 용도로 사용된다. <code>0</code>은 표준 입력(Standard Input), <code>1</code>은 표준 출력(Standard Output), <code>2</code>는 표준 오류(Standard Error)를 의미한다.</p>
<p>따라서 프로그램에서 새 파일을 열면 보통 그다음 번호인 <code>3</code>부터 파일 디스크립터가 할당된다고 이해할 수 있다.</p>
<p>이후 <code>write()</code> 함수를 통해 두 개의 문자열이 <code>lab1.txt</code> 파일에 기록된다. 마지막으로 <code>close()</code>가 호출되면서 파일이 정상적으로 닫히고, 관련 자원(Resource)이 반환된다.</p>
<p>생성된 <code>lab1.txt</code> 파일을 열어보면 두 문자열이 순서대로 저장되어 있는 것을 확인할 수 있다. 즉, 프로그램은 파일 이름이 아니라 fd 값을 기준으로 파일에 데이터를 쓰고 닫는 과정을 수행한 것이다.</p>
<p>fd 값이 항상 반드시 <code>3</code>으로 고정되는 것은 아니다. 이미 다른 파일이나 자원이 열려 있다면 <code>4</code>, <code>5</code>처럼 다른 번호가 할당될 수 있다. 중요한 점은 <code>0</code>, <code>1</code>, <code>2</code>가 표준 입출력용으로 예약되어 있고, 새로 여는 파일은 사용 가능한 가장 작은 fd 번호를 받는다는 것이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/33da321a-4e97-4173-b4f1-5eaf7b48f924.png" alt="" style="display:block;margin:0 auto" />

<h3>Offset-Based File Access(오프셋 기반 파일 접근)</h3>
<p>두 번째 실습에서는 파일의 현재 위치(Current Position), 즉 오프셋(Offset)을 기준으로 파일 접근이 이루어지는 과정을 살펴본다.</p>
<p><code>write()</code>를 수행하면 현재 오프셋 위치에 데이터가 기록되고, 기록이 끝난 뒤 오프셋은 자동으로 다음 위치로 이동한다. 따라서 별도의 위치를 지정하지 않으면 데이터는 기존 내용 뒤에 계속 이어서 기록된다.</p>
<p>하지만 특정 위치에 데이터를 기록하거나, 파일 중간에 있는 내용을 읽고 싶은 경우에는 <code>lseek()</code> 함수를 사용한다. <code>lseek()</code>는 파일의 오프셋을 원하는 위치로 직접 이동시키는 역할을 한다.</p>
<p>결국 파일의 읽기(Read)와 쓰기(Write) 결과는 현재 오프셋이 어디에 위치해 있는지에 따라 결정된다고 이해할 수 있다.</p>
<p>오프셋(Offset)은 열린 파일 안에서 “다음에 읽거나 쓸 위치”를 의미한다. 같은 파일이라도 파일 디스크립터(File Descriptor, fd)를 어떻게 열었는지에 따라 오프셋이 따로 관리될 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/69b66ac3-c9c1-4db5-a9f4-11e31b7420aa.png" alt="" style="display:block;margin:0 auto" />

<h3>File Offset and <code>lseek()</code>(파일 오프셋과 <code>lseek()</code>)</h3>
<p>핵심 코드에서 먼저 다음과 같이 문자열을 파일에 기록한다.</p>
<pre><code class="language-c">write(fd, "ABCDEFGH\n", 9);
</code></pre>
<p>이 코드는 <code>ABCDEFGH</code>와 줄바꿈 문자(Newline)를 포함해 총 9바이트(Byte)를 파일에 저장한다. 이 상태에서 파일에는 문자열이 순서대로 기록된다.</p>
<p>그다음 <code>lseek()</code> 함수가 사용된다.</p>
<pre><code class="language-c">lseek(fd, 2, SEEK_SET);
</code></pre>
<p>여기서 <code>SEEK_SET</code>은 파일의 시작 위치(Start)를 기준으로 한다는 의미이다. 따라서 이 코드는 파일의 처음부터 2바이트 이동한 위치로 파일 오프셋(Offset)을 옮기는 동작이다.</p>
<p>즉, 파일의 처음으로 돌아가는 것이 아니라 파일 중간 위치로 이동하는 것이다.</p>
<p>이후 다음 코드가 실행된다.</p>
<pre><code class="language-c">write(fd, "XXX", 3);
</code></pre>
<p>이 상태에서 <code>"XXX"</code>를 기록하면 기존 데이터가 뒤로 밀리는 것이 아니라, 현재 오프셋 위치부터 기존 내용을 덮어쓰기(Overwrite) 한다.</p>
<p>처음 파일 내용이 다음과 같았다면,</p>
<pre><code class="language-plaintext">ABCDEFGH
</code></pre>
<p>인덱스 기준으로 <code>A</code>는 0번, <code>B</code>는 1번, <code>C</code>는 2번 위치에 있다. <code>lseek(fd, 2, SEEK_SET)</code>은 2번 위치인 <code>C</code>로 이동한다는 의미이다.</p>
<p>따라서 <code>"XXX"</code>를 쓰면 기존의 <code>CDE</code>가 <code>XXX</code>로 덮어쓰기 되어 최종 결과는 다음과 같이 된다.</p>
<pre><code class="language-plaintext">ABXXXFGH
</code></pre>
<p>즉, <code>lseek()</code>는 파일의 현재 위치를 이동시키고, 이후의 <code>write()</code>는 그 위치부터 데이터를 덮어쓴다고 이해하면 된다.</p>
<p><code>lseek(fd, 2, SEEK_SET)</code>에서 <code>2</code>는 “두 번째 글자”가 아니라 “파일 시작 지점에서 2바이트 떨어진 위치”를 의미한다. 문자열 인덱스가 0부터 시작하기 때문에 실제로는 세 번째 문자 위치인 <code>C</code>에 해당한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/5cdee627-5c8e-4b2a-ac46-0a5c0754feee.png" alt="" style="display:block;margin:0 auto" />

<h3>File Offset and <code>lseek()</code> Concept</h3>
<p>(파일 오프셋과 <code>lseek()</code>의 개념)</p>
<p>파일(File)에는 항상 현재 접근 위치(Current Position)인 오프셋(Offset)이 존재한다. <code>read()</code>와 <code>write()</code> 연산은 이 오프셋을 기준으로 수행된다.</p>
<p>파일을 읽거나 쓰면 오프셋은 작업이 끝난 위치 다음으로 자동 이동한다. 따라서 별도로 위치를 지정하지 않으면 파일은 현재 위치부터 계속 이어서 처리된다.</p>
<p>원하는 위치에서 파일을 읽거나 쓰고 싶다면 <code>lseek()</code> 함수를 사용해 오프셋을 직접 이동시킬 수 있다.</p>
<p><code>lseek()</code>에서 기준이 되는 값에는 <code>SEEK_SET</code>, <code>SEEK_CUR</code>, <code>SEEK_END</code>가 있다. <code>SEEK_SET</code>은 파일의 시작 위치, <code>SEEK_CUR</code>은 현재 위치, <code>SEEK_END</code>는 파일의 끝을 기준으로 오프셋을 이동한다.</p>
<p>중요한 점은 <code>lseek()</code> 자체가 파일 내용을 수정하는 함수가 아니라는 것이다. <code>lseek()</code>는 파일의 접근 위치만 바꾸며, 실제 데이터 변경은 이후에 수행되는 <code>read()</code>나 <code>write()</code>에 의해 이루어진다.</p>
<p><code>lseek()</code>는 파일 포인터(File Pointer)의 위치를 이동시키는 함수라고 이해하면 쉽다. 다만 여기서 말하는 포인터는 메모리 주소 포인터가 아니라, 파일 안에서 다음 작업이 이루어질 위치를 의미한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/790017ff-6340-4e2f-aa1f-1ce124d51e9b.png" alt="" style="display:block;margin:0 auto" />

<h3>Overwriting Data with <code>lseek()</code>(특정 위치 이동 후 데이터 덮어쓰기)</h3>
<p>이 예제는 파일의 특정 위치로 이동한 뒤 데이터를 덮어쓰는 과정을 확인하는 코드이다.</p>
<p>먼저 <code>open()</code>을 통해 <code>lab2.txt</code> 파일을 연다. 이후 첫 번째 <code>write()</code>를 수행하면 문자열이 파일에 순서대로 기록된다.</p>
<p>그다음 <code>lseek()</code>를 사용해 파일의 시작 위치(Start)를 기준으로 2번 위치로 이동한다. 이때 오프셋(Offset)이 파일의 중간 위치로 변경된다.</p>
<p>이 상태에서 두 번째 <code>write()</code>를 수행하면 기존 데이터를 뒤로 밀어 넣는 것이 아니라, 현재 오프셋 위치인 2번 위치부터 덮어쓰기(Overwrite)가 이루어진다.</p>
<p>마지막으로 다시 <code>lseek()</code>를 사용해 파일의 처음으로 이동한 뒤, <code>read()</code>를 통해 변경된 파일 내용을 확인한다.</p>
<p>즉, 이 예제는 <code>lseek()</code>로 파일의 접근 위치를 이동시키고, 그 위치에서 <code>write()</code>를 수행하면 기존 데이터가 덮어쓰기 된다는 점을 보여준다.</p>
<p><code>write()</code>는 삽입(Insert)이 아니라 덮어쓰기(Overwrite) 방식으로 동작한다. 따라서 중간 위치에서 데이터를 쓰면 기존 내용이 뒤로 밀리는 것이 아니라, 해당 위치의 데이터가 새 데이터로 바뀐다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/3e1131af-4463-4de6-9f77-8fb9157fd2ce.png" alt="" style="display:block;margin:0 auto" />

<h3><code>lseek()</code> Execution Result(<code>lseek()</code> 실행 결과)</h3>
<p>파일의 실행 결과를 보면 기존 내용이 <code>ABCDEFGH</code>에서 <code>ABXXFGH</code>로 변경된 것을 확인할 수 있다.</p>
<p>처음에는 파일에 <code>ABCDEFGH</code>가 순서대로 저장되어 있었다. 이후 <code>lseek()</code>를 통해 파일의 시작을 기준으로 2번 위치로 이동한 뒤, <code>XX</code>를 기록하면서 기존의 <code>CD</code> 부분이 덮어쓰기 된 것이다.</p>
<p>여기서 중요한 점은 <code>lseek()</code>가 기존 데이터를 뒤로 밀어내는 기능이 아니라는 것이다. <code>lseek()</code>는 파일의 현재 접근 위치(Offset)를 이동시키는 역할만 한다. 이후 <code>write()</code>가 실행되면 현재 오프셋 위치부터 기존 데이터가 덮어쓰기(Overwrite) 된다.</p>
<p>따라서 파일은 단순히 끝에만 이어서 기록되는 구조가 아니라, 현재 오프셋(Offset)을 기준으로 읽기(Read)와 쓰기(Write)가 수행되는 구조라고 이해할 수 있다.</p>
<p>결과가 <code>ABXXFGH</code>라면 실제로는 2바이트인 <code>XX</code>가 기록된 경우에 해당한다. 만약 2번 위치에서 <code>XXX</code>처럼 3바이트를 기록했다면 기존의 <code>CDE</code>가 덮어쓰기 되어 결과는 <code>ABXXXFGH</code>가 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a8ee903f-1a02-4f2b-8f86-0cbac6e90266.png" alt="" style="display:block;margin:0 auto" />

<h3>Descriptor Sharing with <code>dup()</code>(디스크립터 공유)</h3>
<p>이번 실습은 파일 디스크립터(File Descriptor, fd)를 어떻게 생성하느냐에 따라 파일 접근 방식이 달라지는 것을 확인하는 내용이다.</p>
<p>먼저 <code>open()</code>을 두 번 호출하면 서로 다른 fd가 생성된다. 이 경우 각 fd는 독립적인 파일 상태(File State)와 오프셋(Offset)을 가진다. 따라서 한 fd에서 파일 위치가 이동하더라도 다른 fd에는 영향을 주지 않는다.</p>
<p>반면 <code>dup()</code>을 사용하면 기존 fd를 복제하지만, 내부적으로는 같은 열린 파일 상태(Open File Description)를 공유한다. 이 경우 파일 위치를 나타내는 오프셋(Offset)도 함께 공유된다.</p>
<p>따라서 한쪽 fd에서 <code>read()</code>, <code>write()</code>, <code>lseek()</code> 등을 수행해 오프셋이 변경되면, 다른 fd에도 그 변화가 영향을 미친다.</p>
<p>결국 중요한 것은 fd 값 자체가 아니라, 두 fd가 내부적으로 같은 파일 상태를 공유하는지 여부이다.</p>
<p><code>open()</code>을 두 번 호출하면 같은 파일을 열더라도 서로 독립적인 열린 파일 상태(Open File Description)가 만들어진다.</p>
<p>하지만 <code>dup()</code>은 fd 번호만 새로 만들고, 기존 fd가 가리키던 열린 파일 상태를 함께 사용한다. 이 차이는 파일 오프셋(Offset) 공유 여부를 이해하는 데 중요한 부분이므로 기억해두는 것이 좋다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/d21dbed5-39b1-4233-8645-1bb499fe9cc8.png" alt="" style="display:block;margin:0 auto" />

<h3><code>dup()</code> Descriptor Sharing Example</h3>
<p>핵심 코드를 보면 다음과 같이 <code>dup()</code>을 사용한다.</p>
<pre><code class="language-c">fd3 = dup(fd1);
</code></pre>
<p><code>dup(fd1)</code>은 기존 파일 디스크립터(File Descriptor)인 <code>fd1</code>을 복제하여 새로운 파일 디스크립터 <code>fd3</code>을 생성한다. 이때 <code>fd1</code>과 <code>fd3</code>은 번호는 다르지만, 내부적으로는 같은 열린 파일 상태(Open File Description)를 공유한다.</p>
<p>따라서 두 fd는 같은 파일 오프셋(Offset)도 공유한다.</p>
<p>예를 들어 다음 코드가 실행된다고 하자.</p>
<pre><code class="language-plaintext">write(fd1, "A", 1);
</code></pre>
<p>처음 오프셋이 0인 상태에서 <code>fd1</code>을 통해 <code>"A"</code>를 1바이트 기록하면, 0번 위치에 <code>A</code>가 저장되고 오프셋은 1번 위치로 이동한다.</p>
<p>이후 <code>fd3</code>을 통해 다음 코드가 실행된다.</p>
<pre><code class="language-plaintext">write(fd3, "B", 1);
</code></pre>
<p><code>fd3</code>도 <code>fd1</code>과 같은 오프셋을 공유하기 때문에, 현재 오프셋인 1번 위치에 <code>B</code>가 기록된다.</p>
<p>즉, 서로 다른 fd를 사용하더라도 <code>dup()</code>으로 생성된 fd들은 같은 파일 상태와 오프셋을 공유하므로, 하나의 파일에 이어서 기록되는 것처럼 동작한다.</p>
<p>중요한 점은 <code>fd1</code>과 <code>fd3</code>의 숫자값이 같은 것이 아니라, 두 fd가 같은 열린 파일 상태(Open File Description)를 가리킨다는 것이다.</p>
<p>그래서 <code>dup()</code>으로 복제된 fd 사이에서는 오프셋 변화가 서로 영향을 준다. 이 부분은 파일 디스크립터 공유를 이해하는 데 중요한 특징이므로 기억해두는 것이 좋다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/ee4b35f0-2442-4264-948e-b69caff8d4cd.png" alt="" style="display:block;margin:0 auto" />

<h3>Descriptor Sharing with <code>dup()</code></h3>
<p><code>dup()</code>은 기존 파일 디스크립터(File Descriptor)를 복제해서 새로운 fd를 생성하는 함수이다.</p>
<p>이때 단순히 fd 번호만 복사되는 것이 아니라, 동일한 열린 파일 상태(Open File Description)를 함께 공유한다. 따라서 파일의 현재 위치를 나타내는 오프셋(Offset)도 함께 공유된다.</p>
<p>예를 들어 한쪽 fd에서 <code>write()</code>를 수행하면 오프셋이 이동하고, 다른 fd에서도 그 이동된 오프셋을 기준으로 이어서 기록된다. 즉, 서로 다른 fd를 사용하더라도 같은 파일 상태를 공유하기 때문에 하나의 흐름처럼 동작한다.</p>
<p>반면 <code>open()</code>을 두 번 호출하면 같은 파일을 열더라도 각각 독립적인 파일 상태를 가진다. 이 경우 오프셋도 따로 관리되므로, 한쪽 fd의 위치 변화가 다른 fd에 영향을 주지 않는다.</p>
<p>결국 중요한 것은 fd 값 자체가 아니라, 내부적으로 파일 상태를 공유하는지 여부이다. 이 부분은 <code>dup()</code>과 <code>open()</code>의 차이를 이해하는 데 중요한 특징이므로 기억해두는 것이 좋다.</p>
<p><code>dup()</code>으로 생성된 fd는 기존 fd와 같은 열린 파일 상태(Open File Description)를 가리킨다. 그래서 오프셋(Offset)뿐만 아니라 파일 상태 플래그(File Status Flags)도 공유될 수 있다. 다만 fd 번호 자체와 fd별 설정은 서로 다를 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/de7169c7-61d1-4b5c-a534-11870c8af891.png" alt="" style="display:block;margin:0 auto" />

<h3>File Descriptor and Offset Example</h3>
<p>이 예제는 파일 디스크립터(File Descriptor)에 따라 오프셋(Offset)이 어떻게 달라지는지 확인하는 실습이다.</p>
<p>먼저 <code>fd1</code>은 <code>open()</code> 함수를 통해 <code>lab3.txt</code> 파일을 한 번 연다. 그리고 <code>fd2</code>도 마찬가지로 <code>open()</code> 함수를 통해 같은 <code>lab3.txt</code> 파일을 다시 연다.</p>
<p>즉, <code>open()</code>을 두 번 호출하면 같은 파일이라도 서로 독립적인 열린 파일 상태(Open File Description)를 가지게 된다. 따라서 <code>fd1</code>과 <code>fd2</code>의 오프셋(Offset)은 각각 따로 관리된다.</p>
<p>반면 <code>fd3</code>은 <code>dup()</code> 함수를 통해 <code>fd1</code>을 복제해서 만들어진다. 이 경우 <code>fd1</code>과 <code>fd3</code>은 fd 번호는 다르지만, 동일한 열린 파일 상태를 공유한다. 따라서 두 fd는 같은 오프셋을 함께 사용한다.</p>
<p>그다음 코드에서는 <code>write()</code>를 수행한 뒤, <code>lseek()</code>를 통해 <code>off1</code>, <code>off2</code>, <code>off3</code> 값을 확인한다. 이 값들은 각각 <code>fd1</code>, <code>fd2</code>, <code>fd3</code>의 현재 위치, 즉 오프셋을 나타낸다.</p>
<p>정리하면 <code>fd1</code>과 <code>fd3</code>은 <code>dup()</code>으로 연결되어 같은 오프셋을 공유하고, <code>fd2</code>는 같은 파일을 열었더라도 별도로 <code>open()</code>된 것이기 때문에 독립적인 오프셋을 가진다.</p>
<p><code>lseek(fd, 0, SEEK_CUR)</code>을 사용하면 현재 오프셋(Current Offset)을 확인할 수 있다. 여기서 <code>SEEK_CUR</code>은 현재 위치를 기준으로 한다는 뜻이고, 이동 값이 <code>0</code>이므로 위치를 바꾸지 않고 현재 위치만 확인하는 방식이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/6aed4346-c441-4d39-a778-da913e3a8047.png" alt="" style="display:block;margin:0 auto" />

<h3><code>dup()</code> Execution Result</h3>
<p>실행 결과를 보면 먼저 <code>fd1</code>을 통해 <code>A</code>를 기록했을 때, <code>fd1</code>의 오프셋(Offset)이 1로 이동한다.</p>
<p>이때 <code>fd3</code>도 함께 1로 이동한 것을 확인할 수 있다. 이는 <code>fd3</code>가 <code>dup(fd1)</code>을 통해 생성되었기 때문에, <code>fd1</code>과 동일한 열린 파일 상태(Open File Description)를 공유하기 때문이다.</p>
<p>반면 <code>fd2</code>는 같은 <code>lab3.txt</code> 파일을 가리키더라도 <code>open()</code>을 통해 별도로 열린 파일 디스크립터(File Descriptor)이므로, 오프셋이 0으로 유지된다.</p>
<p>즉, 실행 결과를 통해 <code>fd1</code>과 <code>fd3</code>은 같은 오프셋을 공유하고, <code>fd2</code>는 독립적으로 동작한다는 것을 확인할 수 있다.</p>
<p><code>dup()</code>으로 생성된 fd는 기존 fd와 같은 오프셋(Offset)을 공유한다. 반면 같은 파일이라도 <code>open()</code>을 다시 호출해서 얻은 fd는 독립적인 오프셋을 가진다. 이 차이는 파일 디스크립터 공유를 이해하는 데 중요한 부분이므로 기억해두는 것이 좋다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/789adb04-23d4-42f1-b3d4-78227dda742b.png" alt="" style="display:block;margin:0 auto" />

<h3><code>dup()</code> Code Explanation 2</h3>
<p>그다음 코드에서 <code>fd2</code>를 기준으로 다음 쓰기 작업을 수행한다.</p>
<pre><code class="language-c">write(fd2, "B", 1);
</code></pre>
<p><code>fd2</code>는 <code>open()</code>을 통해 별도로 생성된 파일 디스크립터(File Descriptor)이기 때문에, <code>fd1</code>, <code>fd3</code>와는 독립적인 오프셋(Offset)을 가진다. 따라서 이 작업을 수행하면 <code>fd2</code>의 오프셋만 1로 이동하고, <code>fd1</code>과 <code>fd3</code>의 오프셋은 그대로 유지된다.</p>
<p>마지막으로 다음 코드가 실행된다.</p>
<pre><code class="language-c">write(fd3, "C", 1);
</code></pre>
<p><code>fd3</code>는 <code>dup(fd1)</code>을 통해 생성되었기 때문에 <code>fd1</code>과 같은 열린 파일 상태(Open File Description)를 공유한다. 따라서 <code>fd3</code>를 통해 쓰기 작업을 수행하면 <code>fd3</code>의 오프셋뿐만 아니라 <code>fd1</code>의 오프셋도 함께 이동한다.</p>
<p>결국 이 예제는 <code>open()</code>으로 생성된 <code>fd2</code>는 독립적으로 동작하고, <code>dup()</code>으로 연결된 <code>fd1</code>과 <code>fd3</code>는 오프셋을 포함한 파일 상태(File State)를 공유한다는 점을 보여준다.</p>
<p>같은 파일을 가리키더라도 <code>open()</code>으로 따로 열었는지, <code>dup()</code>으로 복제했는지에 따라 오프셋 공유 여부가 달라진다. 이 부분은 파일 디스크립터(File Descriptor)의 동작을 이해하는 데 중요한 내용이므로 기억해두는 것이 좋다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/cc03734e-ebaa-4020-a57e-004f825fd0aa.png" alt="" style="display:block;margin:0 auto" />

<h3>File State Sharing with <code>fork()</code>(<code>fork()</code>에서의 파일 상태 공유)</h3>
<p>이번에는 서로 다른 프로세스(Process)에서 파일 상태(File State)가 어떻게 동작하는지 살펴보는 예제이다.</p>
<p>먼저 <code>fork()</code>가 수행되면 자식 프로세스(Child Process)는 부모 프로세스(Parent Process)의 파일 디스크립터(File Descriptor)를 그대로 상속받는다.</p>
<p>이때 단순히 fd 값만 복사되는 것이 아니라, 동일한 열린 파일 상태(Open File Description)를 공유한다. 따라서 한쪽 프로세스에서 <code>write()</code>를 수행하면 오프셋(Offset)이 변경되고, 이 변화는 다른 프로세스에도 반영된다.</p>
<p>즉, 서로 다른 프로세스라 하더라도 같은 열린 파일 상태를 공유한다면 하나의 파일에 이어서 접근하는 것처럼 동작한다.</p>
<p>결국 파일 상태는 각 프로세스가 따로 관리하는 것이 아니라, 커널(Kernel) 내부에서 관리된다. 여러 프로세스는 이 커널의 파일 상태를 함께 공유할 수 있다.</p>
<p><code>fork()</code> 후 부모와 자식 프로세스는 같은 열린 파일 상태(Open File Description)를 공유하므로 오프셋도 공유된다. 이 부분은 <code>dup()</code>과 비슷하게 이해할 수 있으며, 파일 공유 동작을 이해하는 데 중요한 내용이므로 기억해두는 것이 좋다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/86807bc5-af02-472e-89c8-79647b8267b0.png" alt="" style="display:block;margin:0 auto" />

<h3>File Sharing with <code>fork()</code>(<code>fork()</code>에서의 파일 공유)</h3>
<p>이번 예제는 서로 다른 프로세스(Process)에서 파일 상태(File State)가 어떻게 공유되는지 확인하는 실습이다.</p>
<p><code>fork()</code>는 이전 주차에서도 사용했기 때문에 기본 동작은 익숙할 수 있다. 하지만 여기서의 핵심은 프로세스를 나누는 동작 자체가 아니라, <code>fork()</code> 이후 부모 프로세스(Parent Process)와 자식 프로세스(Child Process)가 파일 자원(File Resource)을 어떻게 공유하는지 확인하는 것이다.</p>
<p>코드를 보면 <code>fork()</code> 이후 자식 프로세스는 <code>"CHILD\n"</code>를 파일에 기록하고, 부모 프로세스는 <code>"PARENT\n"</code>를 파일에 기록한다.</p>
<p>이때 자식 프로세스는 부모 프로세스의 파일 디스크립터(File Descriptor)를 그대로 상속받는다. 단순히 fd 값만 복사되는 것이 아니라, 동일한 열린 파일 상태(Open File Description)를 공유한다.</p>
<p>따라서 두 프로세스의 <code>write()</code>는 서로 완전히 독립적으로 수행되는 것이 아니라, 하나의 파일에 이어서 기록되는 형태로 동작한다. 한쪽 프로세스에서 <code>write()</code>를 수행하면 오프셋(Offset)이 이동하고, 이 변경된 오프셋은 다른 프로세스에도 반영된다.</p>
<p>즉, 이전에는 <code>fork()</code>의 프로세스 생성에 초점을 두었다면, 이번 실습에서는 <code>fork()</code> 이후 파일 상태가 어떻게 공유되는지를 확인하는 것이 핵심이다.</p>
<p><code>fork()</code> 이후 부모와 자식은 같은 열린 파일 상태(Open File Description)를 공유하기 때문에 오프셋(Offset)도 공유한다. 이 부분은 <code>dup()</code>으로 생성된 fd가 오프셋을 공유하는 것과 비슷하게 이해할 수 있다.</p>
<p>다만 부모와 자식 중 어느 쪽이 먼저 실행될지는 스케줄링(Scheduling)에 따라 달라질 수 있다. 그래서 파일에는 두 문자열이 순차적으로 기록되지만, 실행 순서에 따라 <code>CHILD</code>가 먼저 기록될 수도 있고 <code>PARENT</code>가 먼저 기록될 수도 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/99a249f8-904e-4ae4-848b-b3d62acd3d23.png" alt="" style="display:block;margin:0 auto" />

<h3>File Sharing with <code>fork()</code> Code Example</h3>
<p>이 코드는 <code>fork()</code> 이후 부모 프로세스(Parent Process)와 자식 프로세스(Child Process)가 파일 상태(File State)를 어떻게 공유하는지 확인하는 예제이다.</p>
<p>먼저 <code>open()</code>을 통해 <code>lab4.txt</code> 파일을 연다. 이때 파일 디스크립터(File Descriptor, fd)가 생성되고, 이후 <code>fork()</code>가 수행되면서 부모와 자식 프로세스로 나뉜다.</p>
<p><code>fork()</code> 이후 자식 프로세스는 <code>write(fd, "CHILD\n", 6)</code>을 수행하여 파일에 <code>CHILD</code>를 기록한다. 부모 프로세스는 <code>wait(NULL)</code>을 통해 자식 프로세스가 먼저 실행되도록 기다린 뒤, <code>write(fd, "PARENT\n", 7)</code>을 수행한다.</p>
<p>이때 자식 프로세스는 부모 프로세스의 fd를 상속받기 때문에, 두 프로세스는 동일한 열린 파일 상태(Open File Description)와 오프셋(Offset)을 공유한다.</p>
<p>따라서 자식이 먼저 <code>CHILD</code>를 기록하면 오프셋이 그 다음 위치로 이동하고, 이후 부모가 같은 오프셋을 기준으로 <code>PARENT</code>를 이어서 기록한다. 즉, 서로 다른 프로세스라도 같은 파일 상태를 공유하면 하나의 파일에 순서대로 이어 쓰는 것처럼 동작한다.</p>
<p>정리하면 이 예제의 핵심은 <code>fork()</code> 이후 부모와 자식 프로세스가 파일 디스크립터를 상속받고, 동일한 열린 파일 상태와 오프셋을 공유한다는 점이다.</p>
<p>첨부된 코드에서는 <code>open()</code>이 먼저 실행된 뒤 <code>fork()</code>가 수행된다. 그래서 자식 프로세스가 부모의 fd를 상속받을 수 있다.</p>
<p>또한 부모 프로세스에서 <code>wait(NULL)</code>을 사용했기 때문에 자식이 먼저 기록하고, 그다음 부모가 기록된다. 만약 <code>wait()</code>가 없다면 부모와 자식 중 누가 먼저 실행될지는 스케줄링(Scheduling)에 따라 달라지게 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/272613f3-4523-4ea1-8d11-53057fa5a1fc.png" alt="" style="display:block;margin:0 auto" />

<h3><code>fork()</code> File Sharing Execution Result</h3>
<p>프로세스와 파일 공유(<code>fork</code>) 실행 결과를 보면, 화면에는 자식 프로세스(Child Process)와 부모 프로세스(Parent Process)의 완료 메시지가 출력된다.</p>
<p><code>lab4.txt</code> 파일의 내용을 확인하면 자식 프로세스가 먼저 <code>CHILD</code>를 기록하고, 그다음 부모 프로세스가 <code>PARENT</code>를 이어서 기록한 것을 볼 수 있다.</p>
<p>이때 두 프로세스는 같은 파일 디스크립터(File Descriptor)를 상속받았기 때문에 동일한 열린 파일 상태(Open File Description)를 공유한다. 따라서 오프셋(Offset)도 함께 공유된다.</p>
<p>자식 프로세스가 먼저 파일에 데이터를 기록하면 오프셋이 그 다음 위치로 이동하고, 부모 프로세스는 이동된 오프셋을 기준으로 데이터를 이어서 기록한다.</p>
<p>즉, 부모와 자식 프로세스가 각각 파일을 따로 쓰는 것이 아니라, 하나의 파일에 순차적으로 기록되는 구조로 이해할 수 있다.</p>
<p>이 예제에서는 부모 프로세스가 <code>wait()</code>를 사용하기 때문에 자식 프로세스가 먼저 실행된 뒤 부모 프로세스가 기록한다. 만약 <code>wait()</code>가 없다면 실행 순서는 운영체제의 스케줄링(Scheduling)에 따라 달라지게 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/ea635f7f-ec43-4db3-a152-188ee0d12e3f.png" alt="" style="display:block;margin:0 auto" />

<h3>Summary of File Access Structure(파일 접근 구조 정리)</h3>
<p>파일 접근(File Access)은 파일 이름(File Name)이 아니라 파일 디스크립터(File Descriptor, fd)를 통해 이루어진다. 프로그램은 <code>open()</code>을 통해 파일을 열고 fd를 반환받은 뒤, 이후의 <code>read()</code>, <code>write()</code>, <code>close()</code> 작업을 fd 기준으로 수행한다.</p>
<p>또한 파일 내부에서 읽고 쓰는 현재 위치는 오프셋(Offset)으로 관리된다. <code>write()</code>나 <code>read()</code>가 수행되면 오프셋은 자동으로 이동하며, <code>lseek()</code>를 사용하면 원하는 위치로 오프셋을 직접 이동시킬 수 있다.</p>
<p>이번 실습에서는 <code>dup()</code>과 <code>fork()</code>를 통해 파일 디스크립터와 오프셋이 여러 주체 간에 공유될 수 있다는 점도 확인했다. <code>dup()</code>으로 생성된 fd는 기존 fd와 동일한 열린 파일 상태(Open File Description)를 공유하고, <code>fork()</code> 이후 부모와 자식 프로세스도 같은 열린 파일 상태를 공유할 수 있다.</p>
<p>따라서 파일은 단순히 데이터가 저장된 공간이 아니라, 파일 디스크립터(fd), 오프셋(Offset), 커널 내부의 열린 파일 상태(Open File Description)까지 함께 포함된 하나의 상태 객체(State Object)로 이해할 수 있다.</p>
<p>파일 이름(File Name)은 파일을 처음 찾을 때 사용되고, 파일을 연 뒤에는 fd를 통해 접근한다. 중요한 점은 fd 번호 자체보다, 그 fd가 어떤 열린 파일 상태(Open File Description)를 가리키고 있는지이다. 이 부분은 <code>open()</code>, <code>dup()</code>, <code>fork()</code>의 차이를 이해하는 핵심이므로 기억해두는 것이 좋다.</p>
<hr />
<h1>Ubuntu Practice Results</h1>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a30c8258-6bdd-4efe-a962-4b8ae6345e16.png" alt="" style="display:block;margin:0 auto" />

<h3>File Descriptor Practice Result (파일 디스크립터 실습 결과)</h3>
<p>실습 결과를 보면 <code>fd = 3</code>으로 출력된 것을 확인할 수 있다. 이 값은 <code>open()</code>을 통해 <code>lab1.txt</code> 파일을 열었을 때 운영체제(Operating System)가 반환한 파일 디스크립터(File Descriptor) 번호이다.</p>
<p>버추얼머신(Virtual Machine)의 <code>sf_shared</code> 폴더를 확인하면 <code>lab1.txt</code> 파일이 생성되어 있는 것을 볼 수 있다. 파일 안에는 다음과 같이 두 개의 문자열이 순서대로 기록되어 있다.</p>
<pre><code class="language-plaintext">Operating Systems
File Descriptor Test
</code></pre>
<p>겉으로 보기에는 단순히 파일에 문자열을 쓰는 작업처럼 보인다. 하지만 실제 내부에서는 파일 이름(File Name)이 아니라 파일 디스크립터(fd)를 기준으로 파일에 접근하고 데이터를 기록한다.</p>
<p>즉, <code>open()</code>을 통해 파일이 생성되고, <code>write()</code>를 통해 데이터가 기록되며, 마지막으로 <code>close()</code>를 통해 파일 사용이 종료되고 관련 자원(Resource)이 반환된다.</p>
<p>이 실습을 통해 파일은 실행 중에 이름이 아니라 fd를 기준으로 관리되며, 파일 접근은 시스템 호출(System Call)을 통해 이루어진다는 것을 확인할 수 있었다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/fe025919-e573-4cae-b7e6-6529baf8e387.png" alt="" style="display:block;margin:0 auto" />

<h3><code>lseek()</code> Practice Result (<code>lseek()</code> 실습 결과)</h3>
<p>두 번째 실습은 파일의 특정 위치로 이동한 뒤 데이터를 덮어쓰는 과정을 확인하는 실습이다.</p>
<p>실행 결과를 보면 처음에는 <code>ABCDEFGH</code>가 파일에 저장되어 있었지만, <code>lseek()</code>를 통해 2번 위치로 이동한 뒤 <code>XXX</code>를 기록하면서 기존의 일부 데이터가 덮어쓰기 된 것을 확인할 수 있었다.</p>
<p>이후 <code>lab2.txt</code> 파일이 생성되었는지 <code>sf_shared</code> 폴더에서 확인할 수 있었고, 파일 안의 문자열이 중간 위치부터 변경된 것도 확인할 수 있었다.</p>
<p>겉으로 보기에는 문자열이 단순히 수정된 것처럼 보이지만, 실제로는 오프셋(Offset)을 이동한 뒤 해당 위치에서 <code>write()</code>가 수행된 결과이다.</p>
<p>중요한 점은 데이터가 뒤로 밀리는 것이 아니라, 현재 오프셋 위치부터 그대로 덮어쓰기(Overwrite) 된다는 것이다. 이 실습을 통해 파일 접근은 단순한 순차 구조가 아니라 오프셋을 기준으로 이루어지며, <code>lseek()</code>를 통해 파일 내 위치를 직접 제어할 수 있다는 것을 확인할 수 있었다.</p>
<p><code>lseek()</code>는 파일 내용을 직접 바꾸는 함수가 아니라, 파일의 현재 접근 위치(Current Offset)를 이동시키는 함수이다. 실제 데이터 변경은 그 이후에 실행되는 <code>write()</code>에 의해 이루어진다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/fc3d8a03-b1e8-47c7-9f61-8034f397ec6d.png" alt="" style="display:block;margin:0 auto" />

<h3><code>dup()</code> Practice Result (<code>dup()</code> 실습 결과)</h3>
<p>세 번째 실습은 파일 디스크립터(File Descriptor)의 생성 방식에 따라 오프셋(Offset)이 어떻게 달라지는지 확인하는 예제였다.</p>
<p>실행 결과를 보면 <code>A</code> 기록 후에는 <code>fd1</code>과 <code>fd3</code>의 오프셋이 함께 1로 이동하고, <code>fd2</code>는 0으로 유지된다. 이는 <code>fd3</code>가 <code>dup(fd1)</code>으로 생성되어 <code>fd1</code>과 동일한 열린 파일 상태(Open File Description)를 공유하기 때문이다.</p>
<p>그다음 <code>fd2</code>에서 <code>B</code>를 기록하면 <code>fd2</code>의 오프셋만 1로 이동한다. <code>fd2</code>는 같은 <code>lab3.txt</code> 파일을 열었지만, <code>open()</code>을 통해 별도로 생성되었기 때문에 독립적인 오프셋을 가진다.</p>
<p>마지막으로 <code>fd3</code>에서 <code>C</code>를 기록하면 <code>fd3</code>와 파일 상태를 공유하는 <code>fd1</code>의 오프셋도 함께 2로 이동한다. 반면 <code>fd2</code>는 독립적으로 동작한다.</p>
<p>실제로 <code>lab3.txt</code> 파일의 내용이 <code>BC</code>로 나타나는 것은, <code>fd2</code>가 독립적인 오프셋 0번 위치에서 <code>B</code>를 기록하면서 앞부분을 덮어쓰고, 이후 <code>fd3</code>가 공유된 오프셋 위치에서 <code>C</code>를 이어서 기록했기 때문이다.</p>
<p>이 실습을 통해 파일 디스크립터는 단순한 정수값이 아니라, 내부적으로 어떤 열린 파일 상태를 공유하는지에 따라 동작이 달라진다는 것을 확인할 수 있었다.</p>
<p><code>open()</code>을 다시 호출해 생성한 fd는 같은 파일을 가리키더라도 독립적인 오프셋을 가진다. 반면 <code>dup()</code>으로 생성한 fd는 기존 fd와 열린 파일 상태(Open File Description)를 공유하므로 오프셋도 함께 이동한다. 이 차이는 중요한 부분이므로 기억해두는 것이 좋다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/540f1a4b-fb5f-4e97-857d-e69405a93a23.png" alt="" style="display:block;margin:0 auto" />

<h3>File Sharing with <code>fork()</code> Practice Result</h3>
<p>이 예제는 <code>fork()</code> 이후 부모 프로세스(Parent Process)와 자식 프로세스(Child Process)가 파일을 어떻게 공유하는지 확인하는 실습이었다.</p>
<p>실행 결과를 보면 화면에는 <code>child: write complete</code>, <code>parent: write complete</code> 메시지가 출력된다. 또한 <code>lab4.txt</code> 파일이 생성되었고, 파일 내용을 확인하면 먼저 <code>CHILD</code>가 기록되고 그다음 <code>PARENT</code>가 이어서 기록된 것을 볼 수 있다.</p>
<p>겉으로는 부모 프로세스와 자식 프로세스가 각각 파일에 쓰는 것처럼 보인다. 하지만 실제로는 두 프로세스가 같은 파일 디스크립터(File Descriptor)를 상속받고, 동일한 열린 파일 상태(Open File Description)와 오프셋(Offset)을 공유하면서 하나의 파일에 순차적으로 기록하는 구조이다.</p>
<p>이 실습에서는 <code>wait()</code>를 사용했기 때문에 자식 프로세스가 먼저 실행되어 <code>CHILD</code>를 기록하고, 이후 부모 프로세스가 <code>PARENT</code>를 이어서 기록되었다.</p>
<p>이 실습을 통해 <code>fork()</code> 이후에도 파일 상태(File State)는 공유될 수 있으며, 여러 프로세스(Process)가 하나의 파일을 함께 사용할 수 있다는 점을 확인할 수 있었다.</p>
<p><code>fork()</code> 이후 부모와 자식은 같은 열린 파일 상태(Open File Description)를 공유하므로 오프셋(Offset)도 함께 공유된다. 따라서 한쪽 프로세스가 파일에 쓰면 오프셋이 이동하고, 다른 프로세스는 이동된 위치부터 이어서 작업하게 된다.</p>
<p>다만 <code>wait()</code>가 없다면 부모와 자식 중 누가 먼저 실행될지는 스케줄링(Scheduling)에 따라 달라질 수 있다.</p>
<hr />
]]></content:encoded></item><item><title><![CDATA[AI Software Testing Lecture Notes: Black-box, White-box, and Simulation-based Testing]]></title><description><![CDATA[1️⃣ Overview of AI Software Testing2️⃣ AI Software Testing Techniques3️⃣ Virtual-based AI software testing environment

AI 소프트웨어 테스트 개요 (Overview of AI Software Testing)
이번 강의에서는 AI 소프트웨어 테스트(AI Softw]]></description><link>https://heesu.tech/ai-software-testing-lecture-notes-black-box-white-box-and-simulation-based-testing</link><guid isPermaLink="true">https://heesu.tech/ai-software-testing-lecture-notes-black-box-white-box-and-simulation-based-testing</guid><dc:creator><![CDATA[Heesu Noh]]></dc:creator><pubDate>Thu, 28 May 2026 14:51:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/8fcecc0f-24ec-4912-b268-aa32e4b19a38.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>1️⃣ Overview of AI Software Testing<br />2️⃣ AI Software Testing Techniques<br />3️⃣ Virtual-based AI software testing environment</p>
<hr />
<h3>AI 소프트웨어 테스트 개요 (Overview of AI Software Testing)</h3>
<p>이번 강의에서는 AI 소프트웨어 테스트(AI Software Testing)와 관련하여 세 가지 핵심 내용을 간단히 정리한다. 첫 번째는 AI 소프트웨어 테스트가 무엇인지, 그리고 왜 필요한지를 이해하는 것이다. AI 소프트웨어는 일반 소프트웨어와 달리 데이터(Data)와 모델(Model)을 기반으로 동작하는 경우가 많기 때문에, 입력 조건이나 학습 데이터에 따라 결과가 달라질 수 있다. 따라서 AI 소프트웨어가 기대한 대로 동작하는지 확인하고, 예측 결과의 정확성(Accuracy), 신뢰성(Reliability), 안정성(Safety)을 검증하는 테스트 과정이 필요하다.</p>
<p>두 번째는 AI 소프트웨어에도 적용할 수 있는 테스트 기법(Test Techniques)이 있다는 점이다. 일반 소프트웨어 테스트에서 사용하는 기능 테스트(Functional Testing), 성능 테스트(Performance Testing), 회귀 테스트(Regression Testing)와 같은 개념은 AI 소프트웨어에도 활용될 수 있다. 다만 AI 소프트웨어는 결과가 항상 고정적이지 않을 수 있기 때문에, 테스트할 때 데이터 품질(Data Quality), 모델의 예측 성능(Prediction Performance), 편향성(Bias), 예외 상황 처리(Exception Handling) 등을 함께 고려해야 한다.</p>
<p>세 번째는 테스트를 더 효율적으로 수행하기 위한 가상 기반 소프트웨어 테스트 환경(Virtual Software Testing Environment)에 대한 이해이다. 실제 환경에서 AI 소프트웨어를 테스트하기 어렵거나 비용이 많이 드는 경우, 시뮬레이션(Simulation)이나 가상 환경(Virtual Environment)을 활용할 수 있다. 이러한 환경은 다양한 조건을 반복적으로 실험할 수 있게 해주며, 특히 자율주행(Autonomous Driving), 로봇(Robotics), 스마트 시스템(Smart Systems)처럼 실제 테스트가 위험하거나 복잡한 분야에서 유용하다.</p>
<p>정리하면, 이번 강의의 목표는 AI 소프트웨어 테스트의 필요성, AI 소프트웨어에 적용 가능한 테스트 기법, 그리고 가상 기반 테스트 환경의 개념을 이해하는 것이다. AI 소프트웨어는 데이터와 모델에 크게 의존하므로, 단순히 프로그램이 실행되는지만 확인하는 것이 아니라 결과의 품질과 신뢰성을 함께 검증하는 것이 중요하다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/10e960e0-ad47-46d4-bfe5-7bb7c4c4bf3a.png" alt="" style="display:block;margin:0 auto" />

<h3>AI 소프트웨어 테스트의 정의 (Definition of AI Software Testing)</h3>
<p>AI 소프트웨어 테스트(AI Software Testing)의 개념은 기존에 학습한 소프트웨어 테스트(Software Testing)의 기본 목적에서 출발할 수 있다. 일반적인 소프트웨어 테스트의 목적은 개발된 소프트웨어가 요구사항(Requirements), 아키텍처(Architecture), 설계(Design)를 만족하는지 확인하는 것이다. 만약 소프트웨어가 정해진 요구사항이나 설계를 만족하지 못한다면, 이를 결함(Defect)이라고 부른다. 따라서 테스트의 핵심 목적은 다양한 관점과 환경에서 소프트웨어를 실행하고 검증하여, 존재할 수 있는 결함을 최대한 많이 찾아내는 데 있다.</p>
<p>AI 소프트웨어 테스트도 이 기본 개념에서 크게 벗어나지 않는다. 기존의 소프트웨어 테스트 앞에 AI(Artificial Intelligence)라는 개념이 추가된 것으로 이해할 수 있다. 인공지능 기술(AI Technology)을 적용하여 학습이 완료된 모델(Trained Model)은 하나의 AI 소프트웨어(AI Software)로 볼 수 있다. 따라서 AI 소프트웨어 테스트란, 이 훈련 완료 모델이 요구사항을 충족하는지, 의도한 기능을 정상적으로 수행하는지, 그리고 실제 사용 환경에서 신뢰할 수 있는 결과를 내는지를 검증(Verification)하고 확인(Validation)하는 프로세스라고 할 수 있다.</p>
<p>즉, 기존 소프트웨어 테스트의 대상이 일반 프로그램에서 AI 모델(AI Model) 또는 AI 기반 소프트웨어(AI-based Software)로 바뀐 것이다. 이 테스트 프로세스를 통해 AI 모델의 정확도(Accuracy), 정밀성(Precision), 편향성(Bias) 등을 평가할 수 있다. AI 소프트웨어는 단순히 코드가 오류 없이 실행되는지만 보는 것이 아니라, 모델이 얼마나 올바른 판단을 내리는지, 특정 데이터나 사용자 집단에 대해 편향된 결과를 내지 않는지, 그리고 다양한 입력 상황에서도 안정적으로 동작하는지를 함께 확인해야 한다.</p>
<p>AI 소프트웨어 테스트 프로세스(AI Software Testing Process)는 기존 소프트웨어 개발에서 사용되는 V-Model의 흐름과도 연결된다. AI 소프트웨어 역시 요구사항 분석(Requirements Analysis), 설계(Design), 구현(Implementation), 테스트(Test)의 단계를 거치며, AI 특성상 훈련 데이터 준비(Training Data Preparation), 모델 학습(Model Training), 모델 평가(Model Evaluation)와 같은 과정이 중요하게 포함된다. V-Model의 왼쪽 단계에서 정의한 요구사항과 데이터 준비 과정이 적절했는지를, 오른쪽 테스트 단계에서 검증하는 구조로 이해할 수 있다.</p>
<p>AI 소프트웨어는 일반 소프트웨어의 품질 특성(Quality Characteristics)도 가지고 있다. 예를 들어 기능 적합성(Functional Suitability), 성능 효율성(Performance Efficiency), 호환성(Compatibility), 사용성(Usability) 등이 이에 해당한다. AI 소프트웨어도 결국 사용자가 요구한 기능을 제공해야 하며, 효율적으로 동작하고, 다른 시스템과 잘 연동되며, 사용하기 쉬워야 한다.</p>
<p>하지만 AI 소프트웨어에는 일반 소프트웨어와 구분되는 고유한 품질 특성도 존재한다. 대표적으로 적응성(Adaptability), 자율성(Autonomy), 진화(Evolution), 유연성(Flexibility), 편향성(Bias) 등이 있다. AI 모델은 데이터와 환경에 따라 결과가 달라질 수 있고, 일부 시스템은 스스로 판단하거나 변화하는 상황에 적응해야 한다. 따라서 AI 소프트웨어 테스트는 이러한 고유한 특성이 실제로 적절하게 구현되어 있는지, 그리고 그 과정에서 위험하거나 부정확한 결과가 발생하지 않는지를 확인하는 것을 목표로 한다.</p>
<p>정리하면, AI 소프트웨어 테스트는 인공지능 기술이 적용된 모델이나 시스템이 요구사항을 만족하고 정상적으로 동작하는지 확인하는 테스트 프로세스이다. 기존 소프트웨어 테스트가 결함을 찾아내는 활동이었다면, AI 소프트웨어 테스트는 여기에 더해 모델의 정확도, 정밀성, 편향성, 적응성, 자율성 등 AI 고유의 품질 특성까지 함께 평가한다는 점에서 차이가 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/24360c3a-ef9c-487e-b46a-6c416012cec0.png" alt="" style="display:block;margin:0 auto" />

<h3>AI 소프트웨어 특성을 반영한 테스트 기법의 필요성</h3>
<h3>(Need for Test Techniques Reflecting AI Software Characteristics)</h3>
<p>AI 소프트웨어(AI Software)는 일반 소프트웨어와 달리 모델(Model)이 훈련 데이터(Training Data)를 기반으로 학습하고, 새로운 입력에 대해 예측(Prediction)이나 판단(Decision)을 수행한다. 따라서 AI 소프트웨어를 테스트할 때는 단순히 프로그램이 실행되는지, 기능이 동작하는지만 확인하는 것으로는 충분하지 않다. 모델이 얼마나 정확하게 예측하는지, 결과가 얼마나 정밀한지, 특정 데이터나 사용자 집단에 대해 편향된 결과를 내지 않는지도 함께 확인해야 한다.</p>
<p>AI 모델의 성능은 훈련 데이터에 크게 의존한다. 훈련 데이터의 양이 부족하거나, 데이터가 편향되어 있거나, 실제 사용 환경을 충분히 반영하지 못하면 모델의 예측 결과도 부정확해질 수 있다. 이 때문에 AI 소프트웨어 테스트에서는 코드의 결함뿐만 아니라 데이터 품질(Data Quality), 데이터 편향(Data Bias), 모델 성능(Model Performance)도 중요한 평가 대상이 된다.</p>
<p>또한 AI 소프트웨어는 추론 과정(Inference Process)이 복잡하고, 경우에 따라 비결정적 동작(Non-deterministic Behavior)을 보일 수 있다. 비결정적 동작이란 같은 입력을 넣었을 때 항상 완전히 같은 결과가 나오지 않을 수 있다는 뜻이다. AI 모델은 학습 데이터, 학습 방식, 확률적 처리 방식, 실행 환경 등에 따라 결과가 달라질 수 있기 때문이다. 따라서 AI 소프트웨어 테스트에서는 하나의 입력에 대해 하나의 정답만 확인하는 방식보다, 허용 가능한 결과 범위(Acceptable Range)나 성능 기준(Performance Criteria)을 정해 평가하는 방식이 필요하다.</p>
<p>결국 AI 소프트웨어에는 기존 소프트웨어 테스트 기법만으로는 충분히 검증하기 어려운 부분이 있다. 기능 테스트(Functional Testing), 성능 테스트(Performance Testing), 회귀 테스트(Regression Testing)와 같은 기존 기법도 여전히 필요하지만, 여기에 더해 정확도 평가(Accuracy Evaluation), 정밀성 평가(Precision Evaluation), 편향성 테스트(Bias Testing), 데이터 품질 테스트(Data Quality Testing), 강건성 테스트(Robustness Testing)처럼 AI의 특성을 반영한 테스트 기법이 함께 사용되어야 한다.</p>
<p>정리하면, AI 소프트웨어 테스트는 일반 소프트웨어 테스트의 개념을 바탕으로 하지만, AI 모델의 데이터 의존성(Data Dependency), 예측 성능(Prediction Performance), 복잡한 추론 과정, 비결정적 동작 가능성 등을 추가로 고려해야 한다. 이러한 이유로 AI 소프트웨어의 특징을 반영한 별도의 테스트 기법이 필요하다.</p>
<hr />
<h3>성적관리 AI 소프트웨어에서 기존 테스트 기법 적용의 한계</h3>
<h3>(Limitations of Traditional Test Techniques in AI Grade Management Software)</h3>
<p>기존의 성적관리 소프트웨어(Grade Management Software)는 요구사항(Requirements)이 비교적 명확하게 정해져 있다. 예를 들어 “0점 이상 70점 미만은 F”, “70점 이상 80점 미만은 C”, “80점 이상 90점 미만은 B”, “90점 이상 100점 이하는 A”와 같이 점수 구간과 등급 기준을 명확하게 정의할 수 있다. 이런 경우에는 요구사항을 기준으로 설계하고 구현한 뒤, 동등 분할 테스트(Equivalence Partitioning)나 경계값 분석(Boundary Value Analysis)을 적용하여 테스트 케이스(Test Case)를 만들 수 있다.</p>
<p>예를 들어 70점 이상 80점 미만 구간이 하나의 동등 분할 영역이라면, 이 구간 안의 대표값을 선택하여 테스트할 수 있다. 또한 69점, 70점, 79점, 80점처럼 등급이 바뀌는 경계값을 중심으로 테스트하면 요구사항이 제대로 구현되었는지 확인할 수 있다. 즉, 일반 소프트웨어에서는 요구사항이 명확하기 때문에 이를 기준으로 입력값을 나누고, 각 구간을 대표하는 테스트 데이터를 선정하는 방식이 효과적으로 사용될 수 있다.</p>
<p>하지만 성적관리 AI 소프트웨어(AI Grade Management Software)의 경우에는 상황이 달라질 수 있다. AI 소프트웨어는 사람이 직접 모든 규칙을 코드로 작성하는 것이 아니라, 주어진 훈련 데이터(Training Data)를 바탕으로 모델(Model)이 학습한다. 예를 들어 모델이 70점 이상 80점 미만, 80점 이상 90점 미만의 데이터만 학습한 상태라면, 모델은 이 두 점수 구간에 대해서는 비교적 잘 예측할 수 있을 수 있다. 그러나 50점대 점수나 90점 이상 100점 이하의 점수처럼 훈련 데이터에 충분히 포함되지 않은 입력이 들어오면, 모델이 정확한 결과를 예측하지 못할 가능성이 있다.</p>
<p>이것은 AI 모델이 훈련 데이터에 크게 의존하기 때문이다. 모델이 다양한 학생 그룹(Student Group), 다양한 점수 구간, 다양한 성적 패턴을 학습하지 못했다면, 새로운 학생 그룹이나 학습하지 않은 점수 범위에 대해서는 신뢰하기 어려운 결과를 낼 수 있다. 따라서 AI 소프트웨어 테스트에서는 단순히 입력값을 몇 개의 구간으로 나누어 테스트하는 것만으로는 충분하지 않을 수 있다. 훈련 데이터가 전체 상황을 충분히 대표하고 있는지, 특정 구간이나 집단에 치우쳐 있지 않은지, 새로운 입력에 대해서도 안정적인 예측을 할 수 있는지를 함께 확인해야 한다.</p>
<p>또한 AI 소프트웨어는 요구사항이 훈련 데이터에 따라 변하거나 불명확해질 수 있다는 특징이 있다. 일반 소프트웨어에서는 요구사항이 명확히 정의되어 있어야 동등 분할이나 경계값 분석을 적용하기 쉽다. 하지만 AI 소프트웨어는 모델이 데이터로부터 규칙을 학습하기 때문에, 사람이 사전에 모든 판단 기준을 명확하게 정의하기 어려운 경우가 많다. 이처럼 요구사항 자체가 데이터와 학습 결과에 따라 달라질 수 있다면, 기존의 동등 분할 테스트나 경계값 분석만으로는 충분한 테스트가 어렵다.</p>
<p>정리하면, 동등 분할 테스트(Equivalence Partitioning)와 경계값 분석(Boundary Value Analysis)은 요구사항이 명확한 일반 소프트웨어에서는 효과적인 테스트 기법이다. 그러나 AI 소프트웨어는 훈련 데이터에 따라 모델의 판단 기준이 형성되고, 학습하지 않은 데이터에 대해서는 예측 성능이 떨어질 수 있다. 따라서 AI 소프트웨어를 테스트할 때는 기존 테스트 기법뿐만 아니라 데이터 품질(Data Quality), 데이터 분포(Data Distribution), 편향성(Bias), 일반화 성능(Generalization Performance)을 함께 검증해야 한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/4c04ad87-42bb-4e98-bc7d-bc316d3db8ef.png" alt="" style="display:block;margin:0 auto" />

<h3>비결정적 동작을 고려한 AI 소프트웨어 테스트 기법의 필요성</h3>
<h3>(Need for AI Testing Techniques Considering Non-deterministic Behavior)</h3>
<p>AI 소프트웨어(AI Software)는 입력에 따라 결과가 달라질 수 있는 <code>비결정적 동작(Non-deterministic Behavior)</code>을 보일 수 있다. 일반 소프트웨어는 요구사항이 명확하게 정의되어 있으면, 같은 입력에 대해 항상 같은 결과가 나오는 것을 기대할 수 있다. 예를 들어 성적관리 소프트웨어에서 90점 이상 100점 이하의 점수를 입력하면 A가 나와야 한다는 요구사항이 있다면, 동등 분할 테스트(Equivalence Partitioning)를 통해 해당 구간의 대표값을 넣고 결과가 요구사항과 일치하는지 확인할 수 있다.</p>
<p>하지만 AI 소프트웨어는 같은 입력이나 비슷한 입력에 대해 항상 완전히 일관된 결과를 보장하기 어려울 수 있다. 모델(Model)이 훈련 데이터(Training Data)를 기반으로 확률적 예측(Probabilistic Prediction)을 수행하거나, 실행 환경과 학습 방식에 따라 결과가 달라질 수 있기 때문이다. 따라서 동등 클래스(Equivalence Class)의 대표값을 넣었을 때 항상 예상한 결과가 나오는지만 확인하는 기존 방식에는 한계가 있다. AI 소프트웨어에서는 결과가 요구사항에 맞는지만 보는 것이 아니라, 다양한 입력 상황에서도 모델이 안정적이고 신뢰할 수 있는 예측을 하는지 확인해야 한다.</p>
<p>예를 들어 이미지 분류 모델(Image Classification Model)을 테스트한다고 가정할 수 있다. 이 모델이 특정 이미지에만 의존하지 않고 다양한 이미지에 대해 골고루 예측할 수 있는지 확인하기 위해, 무작위로 생성된 이미지(Randomly Generated Images)를 입력하여 분류 정확도(Classification Accuracy)를 테스트할 수 있다. 이를 통해 모델이 훈련 데이터와 비슷한 이미지에만 잘 반응하는 것이 아니라, 새로운 입력 데이터에 대해서도 어느 정도 일반화(Generalization)할 수 있는지 확인할 수 있다.</p>
<p>또 다른 방법은 이미지에 노이즈(Noise)를 추가하여 테스트하는 것이다. 예를 들어 이미지의 픽셀값(Pixel Value)을 의도적으로 변경하거나, 이미지의 좌우 위치를 바꾸거나, 밝기와 대비를 조정할 수 있다. 이렇게 변형된 이미지를 입력했을 때 모델이 잘못된 분류를 하는지 확인하면, 모델의 강건성(Robustness)을 평가할 수 있다. 즉, 약간의 변화나 방해 요소가 있어도 모델이 안정적으로 올바른 결과를 내는지 확인하는 것이다.</p>
<p>자율주행 자동차 시스템(Autonomous Driving System)에서도 AI 특성을 반영한 테스트가 필요하다. 실제 도로에서는 매우 다양한 상황이 발생할 수 있기 때문에, 단순한 정상 주행 상황만 테스트하는 것으로는 충분하지 않다. 예를 들어 터널을 빠져나오자마자 앞에 고장 난 차량이 있고, 반대편 차선에서는 차량이 다가오는 상황처럼 위험도가 높은 최악의 상황(Worst-case Scenario)을 설정할 수 있다. 이러한 시나리오 기반 테스트(Scenario-based Testing)를 통해 다양한 교통 상황을 시뮬레이션(Simulation)하고, AI 시스템이 안전하게 판단하고 주행할 수 있는지 검증해야 한다.</p>
<p>정리하면, AI 소프트웨어는 비결정적 동작 가능성과 데이터 의존성을 가지고 있기 때문에 기존의 동등 분할 테스트만으로는 충분하지 않을 수 있다고 할 수 있겠다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/8f089b17-a2cc-42fe-a6a3-f7b8175c936b.png" alt="" style="display:block;margin:0 auto" />

<h3>데이터 품질과 양을 고려한 AI 소프트웨어 테스트 기법의 필요성</h3>
<h3>(Need for AI Testing Techniques Considering Data Quality and Quantity)</h3>
<p>AI 소프트웨어(AI Software)의 성능은 훈련에 사용된 데이터(Training Data)의 품질(Quality)과 양(Quantity)에 크게 의존한다. 일반 소프트웨어는 개발자가 작성한 명확한 규칙과 요구사항에 따라 동작하지만, AI 소프트웨어는 데이터를 기반으로 패턴(Pattern)을 학습하고 그 결과를 바탕으로 예측(Prediction)이나 분류(Classification)를 수행한다. 따라서 데이터가 다양하고 품질이 좋을수록 AI 모델(Model)의 성능도 향상될 가능성이 높다.</p>
<p>하지만 훈련 데이터에 특정 패턴이나 편향(Bias)이 존재하면, AI 모델 역시 그 편향을 학습할 수 있다. 예를 들어 동양인 얼굴 데이터를 중심으로 학습한 얼굴 인식 AI 모델(Face Recognition AI Model)에 서양인 얼굴 데이터가 입력된다면, 모델이 충분히 학습하지 못한 데이터이기 때문에 정확도가 낮아질 수 있다. 이는 모델이 다양한 집단의 데이터를 균형 있게 학습하지 못했을 때 발생할 수 있는 데이터 편향(Data Bias)의 예시이다.</p>
<p>이러한 특성 때문에 AI 소프트웨어에는 기존의 동등 분할 테스트(Equivalence Partitioning)를 그대로 적용하는 데 한계가 있다. 동등 분할은 입력값을 일정한 구간이나 그룹으로 나누고, 각 구간의 대표값을 테스트하는 방식이다. 그러나 AI 모델은 단순히 입력 구간만으로 결과가 결정되는 것이 아니라, 훈련 데이터의 분포(Data Distribution), 데이터의 다양성(Data Diversity), 품질, 편향 여부에 따라 결과가 달라질 수 있다. 따라서 구간을 나누어 대표값만 테스트하는 방식으로는 AI 모델의 실제 성능을 충분히 확인하기 어렵다.</p>
<p>예를 들어 이미지 분류 모델(Image Classification Model)을 테스트할 때, 훈련 데이터의 양이 증가함에 따라 모델의 성능이 실제로 향상되는지 확인할 수 있다. 데이터 양을 단계적으로 늘려가며 정확도(Accuracy), 정밀도(Precision), 재현율(Recall) 등의 성능 지표가 어떻게 변화하는지 평가하는 것이다. 이를 통해 모델이 더 많은 데이터를 학습했을 때 일반화 성능(Generalization Performance)이 좋아지는지 확인할 수 있다.</p>
<p>또한 특정 클래스(Class)가 훈련 데이터에 과대표적으로 포함되어 있는지 확인하는 방식으로 데이터 편향성을 테스트할 수 있다. 예를 들어 이미지 분류 모델에서 특정 동물, 특정 인종, 특정 사물 이미지가 지나치게 많이 포함되어 있다면, 모델은 그 클래스에 유리한 방향으로 학습될 수 있다. 따라서 다양한 클래스가 균형 있게 포함된 데이터셋(Dataset)을 준비하고, 모델이 특정 클래스에 치우친 예측을 하지 않는지 검증해야 한다.</p>
<p>마지막으로 데이터 품질의 다양성도 테스트해야 한다. 이미지 분류 모델에 고품질 이미지(High-quality Image)뿐만 아니라 저품질 이미지(Low-quality Image), 흐릿한 이미지(Blurred Image), 어두운 이미지(Dark Image), 노이즈가 있는 이미지(Noisy Image) 등을 입력하여 모델의 정확도를 평가할 수 있다. 이를 통해 실제 환경에서 데이터 품질이 일정하지 않더라도 모델이 안정적으로 동작하는지 확인할 수 있다.</p>
<p>정리하면, AI 소프트웨어의 성능은 훈련 데이터의 품질과 양에 크게 영향을 받는다. 따라서 AI 소프트웨어 테스트에서는 단순한 입력값 구간 테스트뿐만 아니라 데이터 양에 따른 성능 변화, 데이터 편향성, 클래스 불균형(Class Imbalance), 데이터 품질 다양성까지 함께 검증해야 한다.</p>
<hr />
<h1>2️⃣ AI Software Testing Techniques</h1>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/176b563c-e90d-46d7-8fef-e66fc8aa34f9.png" alt="" style="display:block;margin:0 auto" />

<h3>ISO/IEC/IEEE 29119의 AI 소프트웨어 테스트 기법 분류</h3>
<h3>(Classification of AI Software Test Techniques in ISO/IEC/IEEE 29119)</h3>
<p>ISO/IEC/IEEE 29119에서는 AI 소프트웨어 테스트 기법을 크게 <code>블랙박스 테스트(Black-box Testing)</code>와 <code>화이트박스 테스트(White-box Testing)</code>로 구분해서 이해할 수 있다. 이는 기존 일반 소프트웨어 테스트에서도 사용했던 분류 방식과 유사하다.</p>
<p>먼저 <code>블랙박스 테스트(Black-box Testing)</code>는 소프트웨어의 내부 구조나 구현 방식을 직접 보지 않고, 요구사항(Requirements)이나 명세(Specification)를 기반으로 테스트 케이스(Test Case)를 설계하는 방법이다. 그래서 명세 기반 테스트(Specification-based Testing)라고도 한다. 일반 소프트웨어에서는 입력값을 넣었을 때 요구사항에 맞는 출력값이 나오는지를 확인했다면, AI 소프트웨어에서도 같은 개념이 적용된다.</p>
<p>AI 소프트웨어의 블랙박스 테스트에서는 AI 모델(AI Model)의 내부 구조는 직접 분석하지 않고, 입력 데이터(Input Data)에 대해 출력값(Output)이 적절하게 나오는지를 확인한다. 예를 들어 이미지 분류 모델(Image Classification Model)에 이미지를 입력했을 때, 그 이미지가 사람인지, 동물인지, 또는 동물이라면 개인지 고양이인지를 올바르게 분류하는지 확인하는 방식이다. 즉, 모델 내부에서 어떤 계산이 이루어졌는지보다는 입력에 따른 결과가 요구사항에 맞는지를 중심으로 테스트한다.</p>
<p>반면 <code>화이트박스 테스트(White-box Testing)</code>는 내부 구조를 고려하여 테스트 케이스를 설계하는 방법이다. 기존 일반 소프트웨어에서는 소스 코드(Source Code)의 내부 로직(Internal Logic), 조건문, 반복문, 분기 구조 등을 기준으로 테스트를 수행했다. 그래서 구조 기반 테스트(Structure-based Testing)라고도 한다.</p>
<p>AI 소프트웨어의 경우, 이 내부 구조는 단순한 소스 코드 로직만이 아니라 훈련 데이터(Training Data)를 통해 만들어진 AI 모델의 구조로 확장해서 이해할 수 있다. 특히 딥러닝 모델(Deep Learning Model)에서는 신경망(Neural Network)의 내부 구조와 동작을 고려하여 테스트를 수행할 수 있다. 예를 들어 특정 입력을 넣었을 때 신경망의 어떤 뉴런(Neuron)이나 계층(Layer)이 활성화되는지 확인하고, 모델 내부가 얼마나 다양한 방식으로 테스트되었는지를 평가할 수 있다.</p>
<p>일반 소프트웨어의 화이트박스 테스트에서는 문장 커버리지(Statement Coverage), 분기 커버리지(Branch Coverage)처럼 코드가 얼마나 테스트되었는지를 측정했다. AI 소프트웨어에서는 이와 유사하게 신경망 커버리지(Neural Network Coverage)나 뉴런 커버리지(Neuron Coverage)와 같은 개념이 중요하게 다루어진다. 이는 테스트 데이터가 모델 내부의 다양한 뉴런이나 경로를 얼마나 활성화했는지를 확인하려는 시도이다.</p>
<p>정리하면, AI 소프트웨어 테스트도 기존 소프트웨어 테스트처럼 <code>블랙박스 테스트</code>와 <code>화이트박스 테스트</code>로 구분할 수 있다. <code>블랙박스 테스트</code>는 AI 모델의 내부 구조를 보지 않고 입력과 출력의 관계를 중심으로 검증하는 방식이고, <code>화이트박스 테스트</code>는 AI 모델의 내부 구조, 특히 신경망의 동작과 활성화 정도를 고려하여 테스트하는 방식이다. 차이점은 기존 소프트웨어에서는 테스트 대상의 내부 구조가 주로 소스 코드였다면, AI 소프트웨어에서는 그 대상이 AI 모델과 신경망 구조로 확장된다는 점이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/66d3ce75-eef3-4aa0-87f2-75c5333d0882.png" alt="" style="display:block;margin:0 auto" />

<h3>AI 소프트웨어의 블랙박스 테스트 설계 기법</h3>
<h3>(Black-box Test Design Techniques for AI Software)</h3>
<p>AI 소프트웨어(AI Software)에 적용할 수 있는 블랙박스 테스트 설계 기법(Black-box Test Design Techniques)에는 <code>조합 테스트(Combinatorial Testing), 백투백 테스트(Back-to-back Testing), 변성 테스트(Metamorphic Testing), 탐색적 테스트(Exploratory Testing)</code> 등이 있다. 블랙박스 테스트는 AI 모델(AI Model)의 내부 구조를 직접 분석하지 않고, 입력 데이터(Input Data)에 대해 어떤 출력(Output)이 나오는지를 중심으로 테스트하는 방식이다.</p>
<p><code>조합 테스트(Combinatorial Testing)</code>는 여러 입력값이나 조건을 조합하여 테스트 케이스(Test Case)를 만드는 기법이다. 이는 AI 소프트웨어뿐만 아니라 일반 소프트웨어에서도 충분히 사용할 수 있는 테스트 방법이다.</p>
<p>하지만 AI 소프트웨어에서는 조합 테스트를 적용하는 데 한계가 있을 수 있다. AI 모델은 입력 특성(Feature), 파라미터(Parameter), 데이터 조건이 매우 많을 수 있기 때문이다. 특히 대규모 AI 모델의 경우 수많은 매개변수와 입력 조건을 모두 조합하여 테스트하는 것은 현실적으로 어렵다. 따라서 모든 조합을 테스트하기보다는 중요한 조합을 선별하거나, 위험도가 높은 입력 조건을 중심으로 테스트하는 방식이 필요하다.</p>
<p><code>백투백 테스트(Back-to-back Testing)</code>는 동일한 입력을 여러 개의 서로 다른 AI 모델에 넣고, 그 결과를 비교하는 테스트 기법이다. AI 소프트웨어에서는 모델이 얼마나 정확하게 예측(Prediction)하는지가 중요하기 때문에, 하나의 모델 결과만 보는 것이 아니라 다른 방식으로 개발된 모델들과 비교하여 신뢰성을 확인할 수 있다.</p>
<p>예를 들어 같은 문제를 해결하기 위해 A 모델, B 모델, C 모델을 각각 다른 알고리즘(Algorithm), 다른 개발자, 또는 다른 학습 방식으로 만든다고 가정할 수 있다. 이후 동일한 입력 데이터를 세 모델에 넣었을 때 결과가 서로 유사하거나 일관되게 나온다면, 해당 예측 결과에 대한 신뢰도를 높일 수 있다. 반대로 모델마다 결과가 크게 다르다면, 어떤 모델이 왜 다른 판단을 했는지 추가 분석이 필요하다.</p>
<p><code>변성 테스트(Metamorphic Testing)</code>와 <code>탐색적 테스트(Exploratory Testing)</code>도 AI 소프트웨어의 블랙박스 테스트 기법에 포함될 수 있다. 변성 테스트는 정답을 명확히 알기 어려운 상황에서 입력을 변형하고, 그에 따른 출력 관계가 예상한 특성을 유지하는지 확인하는 방식이다. 탐색적 테스트는 사전에 모든 테스트 케이스를 정해두기보다, 테스터가 시스템을 사용하고 관찰하면서 새로운 결함이나 이상 동작을 찾아내는 방식이다. 이 두 기법은 다음 내용에서 더 자세히 다룰 수 있다.</p>
<p>정리하면, AI 소프트웨어의 블랙박스 테스트는 모델 내부 구조보다는 입력과 출력의 관계를 중심으로 수행된다. 조합 테스트는 다양한 입력 조건의 조합을 확인하는 데 사용되고, 백투백 테스트는 여러 AI 모델의 결과를 비교하여 예측의 일관성과 신뢰성을 확인하는 데 활용된다. 다만 AI 소프트웨어는 입력 조건과 파라미터가 매우 많기 때문에, 모든 조합을 테스트하기보다는 효율적인 테스트 설계가 중요하다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/00fb7e1a-ab00-455e-a936-e8a1e3aa4bcd.png" alt="" style="display:block;margin:0 auto" />

<h3>변성 테스트 (Metamorphic Testing)</h3>
<p><code>변성 테스트(Metamorphic Testing)</code>는 AI 모델(AI Model)의 복잡성(Complexity)과 비결정성(Non-determinism) 때문에 예상 결과(Expected Result)를 명확하게 정하기 어려운 경우에 사용할 수 있는 테스트 기법이다. 일반적인 소프트웨어 테스트에서는 입력값(Input)에 대해 예상 결과를 미리 정하고, 실제 결과(Actual Result)와 비교하여 테스트 통과 여부를 판단한다. 하지만 AI 소프트웨어(AI Software)는 모델의 구조가 복잡하고, 경우에 따라 같은 입력이나 비슷한 입력에 대해서도 결과가 달라질 수 있어 예상 결과를 단정하기 어려운 경우가 있다.</p>
<p>이때 변성 테스트는 특정 입력에 대한 정확한 출력값 하나를 확인하기보다, 입력이 변화했을 때 출력이 어떤 관계(Relation)를 가져야 하는지를 기준으로 테스트한다. 즉, “입력이 이렇게 바뀌면 출력도 이런 방식으로 유지되거나 변화해야 한다”는 관계를 설정하고, AI 모델이 그 관계를 만족하는지 확인하는 방식이다.</p>
<p>예를 들어 이미지 분류 모델(Image Classification Model)을 생각해볼 수 있다. 원본 이미지가 고양이(Cat)라면, 이 이미지의 밝기(Brightness)를 조금 바꾸어도 본질적으로는 여전히 고양이 이미지이다. 따라서 AI 모델이 잘 만들어졌다면 밝기가 바뀐 이미지도 고양이로 분류해야 한다. 마찬가지로 고양이 이미지를 좌우 반전(Horizontal Flip)하더라도 이미지의 방향만 바뀐 것이므로, 모델은 여전히 고양이로 판단할 수 있어야 한다.</p>
<p>이처럼 변성 테스트에서는 원본 입력(Source Input)을 변형하여 새로운 입력(Follow-up Input)을 만들고, 두 입력에 대한 출력 사이의 관계가 적절한지 확인한다. 원본 고양이 이미지를 고양이로 분류했다면, 밝기만 바꾼 이미지나 좌우 반전한 이미지도 고양이로 분류해야 한다는 식이다. 여기서 중요한 것은 정확한 출력값 하나보다, 입력 변화에 따른 출력의 일관성(Consistency)과 관계성(Relation)을 검증하는 것이다.</p>
<p>자율주행 자동차(Autonomous Vehicle)에서도 변성 테스트를 적용할 수 있다. 예를 들어 자율주행차가 특정 도로를 정방향으로 안전하게 주행했다면, 같은 도로를 반대 방향으로 주행할 때도 주변 환경(Environment)을 인식하고 안전하게 주행할 수 있어야 한다. 물론 주행 방향이 바뀌면 도로 표지판, 차선, 차량 흐름 등 조건도 달라질 수 있지만, 기본적으로 AI 모델은 변화된 입력 상황에서도 적절한 주행 판단을 내려야 한다.</p>
<p>정리하면, 변성 테스트는 AI 소프트웨어에서 예상 결과를 정확히 정하기 어려울 때 유용한 테스트 기법이다. 입력값을 조금씩 변형하고, 그에 따른 출력값이 논리적으로 타당한 관계를 유지하는지 확인한다. 이미지 분류에서는 밝기 변경이나 좌우 반전 후에도 같은 객체로 분류되는지 확인할 수 있고, 자율주행에서는 주행 방향이나 환경 조건이 달라져도 안전한 판단을 하는지 검증할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/bf3d73db-f356-4a53-b227-0bfc28260027.png" alt="" style="display:block;margin:0 auto" />

<h3>탐색적 테스트 (Exploratory Testing)</h3>
<p>탐색적 테스트(Exploratory Testing)는 AI 소프트웨어(AI Software)가 학습하지 못한 알려지지 않은 케이스(Unknown Case)를 찾아내기 위해 반복적으로 테스트를 수행하는 기법이다. AI 모델(AI Model)은 훈련 데이터(Training Data)를 기반으로 학습하지만, 현실의 모든 데이터를 전부 학습할 수는 없다. 따라서 실제 사용 환경에서는 훈련 데이터에 포함되지 않았던 새로운 상황이 나타날 수 있고, 이 경우 모델이 예측하지 못한 동작을 하거나 전혀 엉뚱한 결과를 낼 가능성이 있다.</p>
<p>탐색적 테스트는 이러한 알려지지 않은 케이스를 계속 찾아가는 것을 목표로 한다. 기존 테스트 결과를 활용하고, 입력 데이터(Input Data)에 대해 다양한 추출(Sampling)과 조합(Combination) 기법을 적용하면서 반복적으로 테스트를 수행한다. 즉, 미리 정해진 테스트 케이스만 실행하는 것이 아니라, 테스트 과정에서 발견된 결과를 바탕으로 새로운 테스트 케이스(Test Case)를 계속 만들어 나가는 방식이다.</p>
<p>예를 들어 자율주행 자동차(Autonomous Vehicle)를 테스트한다고 하면, 먼저 입력 영역(Input Domain)을 정의해야 한다. 시골길인지 도심 도로인지, 날씨가 맑은지 비가 오는지, 터널이 있는지, 앞에 고장 난 차량이 있는지, 반대편에서 차량이 오는지와 같은 조건을 정리한다. 이후 이러한 조건을 조합하여 테스트 케이스를 생성하고, 시뮬레이션(Simulation)을 통해 테스트를 수행한다.</p>
<p>AI 소프트웨어의 탐색적 테스트에서는 실제 환경보다 시뮬레이션 기반 테스트(Simulation-based Testing)가 중요하다. 특히 자율주행처럼 안전과 관련된 시스템에서는 알려지지 않은 케이스가 실제 사고로 이어질 수 있기 때문이다. 따라서 위험한 상황이나 예측하기 어려운 상황은 먼저 가상 환경(Virtual Environment)에서 반복적으로 실험하는 것이 안전하다.</p>
<p>테스트를 수행한 뒤에는 결과를 확인하고, 문제가 발견되면 수정 후 재테스트(Retest)를 진행한다. 만약 테스트 과정에서 기존에 알려지지 않았던 새로운 케이스가 발견되면, 이를 신규 테스트 케이스(New Test Case)로 추가하여 다시 테스트한다. 또한 테스트 결과의 위험도(Risk Level)를 분석하여, 위험도가 높은 상황이라면 우선적으로 추가 테스트 케이스를 만들고 검증해야 한다.</p>
<p>정리하면, 탐색적 테스트는 이미 알려진 케이스(Known Case)만 확인하는 것이 아니라, 알려지지 않은 케이스를 하나씩 찾아가며 테스트 범위와 커버리지(Coverage)를 넓히는 방식이다. AI 소프트웨어는 모든 상황을 미리 학습하기 어렵기 때문에, 탐색적 테스트를 통해 새로운 입력 상황과 위험한 사례를 계속 발견하고 보완하는 과정이 필요하다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/8c366053-765b-4df6-b2e6-445e4924f579.png" alt="" style="display:block;margin:0 auto" />

<h3>AI 소프트웨어의 화이트박스 테스트 설계 기법</h3>
<h3>(White-box Test Design Techniques for AI Software)</h3>
<p>AI 소프트웨어(AI Software)에 대한 화이트박스 테스트(White-box Testing)는 AI 모델(AI Model)의 내부 구조를 고려하여 테스트하는 방법이다. 일반 소프트웨어의 화이트박스 테스트가 소스 코드(Source Code)의 내부 로직과 구조를 기반으로 테스트 케이스(Test Case)를 설계하는 것처럼, AI 소프트웨어에서는 모델 내부의 신경망(Neural Network) 구조와 뉴런(Neuron)의 동작을 중심으로 테스트를 수행한다.</p>
<p>AI 모델에서 각각의 뉴런은 일반 소프트웨어의 코드 구성 요소와 유사하게 볼 수 있다. 따라서 화이트박스 테스트에서는 테스트 데이터를 입력했을 때 모델 내부의 뉴런들이 얼마나 많이 활성화(Activation)되는지, 즉 얼마나 넓은 범위의 내부 구조가 테스트되었는지를 확인하는 것이 중요하다. 이는 일반 소프트웨어 테스트에서 문장 커버리지(Statement Coverage)나 분기 커버리지(Branch Coverage)를 높이려는 것과 비슷한 개념이다.</p>
<p>AI 소프트웨어의 화이트박스 테스트 설계 기법은 크게 <code>뉴런 커버리지(Neuron Coverage), 임계점 커버리지(Threshold Coverage), 부호 변경 커버리지(Sign Change Coverage), 값 변경 커버리지(Value Change Coverage)</code> 등으로 나눌 수 있다. 이들은 모두 AI 모델 내부의 뉴런이 어떻게 반응하고 변화하는지를 기준으로 테스트 범위를 넓히기 위한 방법이다.</p>
<p>가장 기본이 되는 개념은 <code>뉴런 커버리지(Neuron Coverage)</code>이다. 뉴런 커버리지는 테스트 입력을 통해 AI 모델 내부의 뉴런들이 얼마나 많이 활성화되었는지를 측정하는 방식이다. 이후 임계점 커버리지, 부호 변경 커버리지, 값 변경 커버리지와 같은 기법들은 뉴런 커버리지 개념을 바탕으로 조금 더 세부적인 변화나 조건을 고려하는 방식으로 확장된다.</p>
<p>정리하면, AI 소프트웨어의 화이트박스 테스트는 AI 모델의 내부 구조를 분석하고, 신경망 안의 뉴런들이 충분히 테스트되었는지를 확인하는 방식이다. 일반 소프트웨어에서 코드 커버리지(Code Coverage)를 높이는 것이 중요하듯이, AI 소프트웨어에서는 뉴런 커버리지와 같은 모델 내부 커버리지(Model Coverage)를 높이는 것이 중요한 테스트 목표가 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/5eed5611-7125-4c0b-b4f2-d69a78e637de.png" alt="" style="display:block;margin:0 auto" />

<h3>신경망 구조 참고 (Neural Network Structure)</h3>
<p>신경망(Neural Network)은 사람의 두뇌에서 뉴런(Neuron)들이 서로 연결되어 정보를 처리하는 방식에서 영감을 받아 만들어진 계산 모델(Computational Model)이다. AI 모델(AI Model)은 여러 개의 노드(Node) 또는 뉴런으로 구성되며, 이러한 뉴런들이 여러 층(Layer)으로 연결되어 입력 데이터를 처리하고 결과를 출력한다.</p>
<p>예를 들어 벚꽃과 배꽃을 구분한다고 생각해볼 수 있다. 사람은 과거의 경험을 바탕으로 꽃의 색, 모양, 크기, 잎의 형태 등을 보고 판단한다. 이와 비슷하게 AI 모델도 입력된 데이터를 바탕으로 특징(Feature)을 학습하고, 이를 이용해 새로운 데이터가 어떤 대상에 해당하는지 예측(Prediction)하거나 분류(Classification)한다.</p>
<p>신경망은 일반적으로 입력 레이어(Input Layer), 은닉 레이어(Hidden Layer), 출력 레이어(Output Layer)로 구성된다. 입력 레이어는 외부에서 데이터를 입력받는 부분이다. 이미지 분류 모델(Image Classification Model)의 경우, 이미지의 픽셀값(Pixel Value)이 입력 데이터가 될 수 있다.</p>
<p>은닉 레이어는 입력 레이어에서 받은 정보를 처리하고 계산하는 부분이다. 이 단계에서 AI 모델은 입력 데이터의 특징을 추출하고, 여러 계산 과정을 거쳐 출력 결과를 만들기 위한 정보를 정리한다. 은닉 레이어가 여러 개로 깊게 구성된 모델을 딥러닝 모델(Deep Learning Model)이라고 볼 수 있다.</p>
<p>출력 레이어는 최종 결과를 외부에 제공하는 역할을 한다. 예를 들어 꽃 이미지를 입력했을 때, 출력 레이어는 해당 이미지가 벚꽃인지, 배꽃인지와 같은 분류 결과를 제공한다. 즉, 신경망은 입력 데이터를 받아 은닉 레이어에서 계산을 수행하고, 출력 레이어를 통해 최종 판단 결과를 내는 구조로 동작한다.</p>
<p>정리하면, 신경망은 사람의 두뇌 구조에서 영감을 받은 AI 모델의 기본 구조이며, 입력 레이어, 은닉 레이어, 출력 레이어로 구성된다. 입력 레이어는 데이터를 받고, 은닉 레이어는 계산과 특징 추출을 수행하며, 출력 레이어는 최종 예측 결과를 제공한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/efc5d8c1-99cc-41e3-adc9-e0611d08f17f.png" alt="" style="display:block;margin:0 auto" />

<h3>뉴런 커버리 (Neuron Coverage)</h3>
<p>뉴런 커버리지(Neuron Coverage)는 신경망(Neural Network) 내부의 뉴런(Neuron)이 테스트 데이터셋(Test Dataset)에 의해 얼마나 활성화되었는지를 측정하는 화이트박스 테스트(White-box Testing) 개념이다. 일반 소프트웨어에서 코드의 문장이나 분기가 얼마나 실행되었는지를 커버리지(Coverage)로 확인하듯이, AI 소프트웨어에서는 모델 내부의 뉴런이 얼마나 많이 활성화되었는지를 기준으로 테스트 범위를 확인할 수 있다.</p>
<p>뉴런은 입력값(Input Value)을 계산하여 활성값(Activation Value)이라는 출력값을 생성한다. 특정 입력 데이터가 들어왔을 때 어떤 뉴런은 활성화(Activation)되고, 어떤 뉴런은 거의 반응하지 않거나 비활성화(Deactivation)될 수 있다. 테스트 데이터셋을 넣었을 때 활성화되는 뉴런이 많을수록 신경망 내부의 다양한 부분이 테스트되었다고 볼 수 있다.</p>
<p>예를 들어 이미지 분류 모델(Image Classification Model)을 테스트할 때, 기존 데이터셋으로는 전체 23개의 뉴런 중 20개만 활성화되었다고 가정할 수 있다. 이 경우 3개의 뉴런은 아직 테스트 데이터에 의해 충분히 사용되지 않은 상태이다. 더 다양한 이미지를 추가하면 기존에 비활성화되었던 뉴런이 활성화될 수 있고, 그 결과 모델 내부의 더 넓은 부분을 테스트할 수 있다.</p>
<p>뉴런 커버리지는 다음과 같이 계산할 수 있다.</p>
<p>뉴런 커버리지(Neuron Coverage) = 활성화된 뉴런 수 / 전체 뉴런 수 × 100%</p>
<p>예를 들어 전체 뉴런이 23개이고, 그중 20개가 활성화되었다면 뉴런 커버리지는 20 / 23 × 100 = 86.96%가 된다. 이후 새로운 이미지를 추가하여 23개의 뉴런이 모두 활성화되었다면 뉴런 커버리지는 23 / 23 × 100 = 100%가 된다.</p>
<p>즉, 뉴런 커버리지는 테스트 데이터가 AI 모델 내부의 뉴런을 얼마나 폭넓게 자극했는지 확인하는 지표이다. 이를 통해 기존 테스트 데이터로는 확인하지 못했던 모델 내부 동작을 더 많이 살펴볼 수 있으며, 비활성화된 뉴런을 활성화할 수 있는 새로운 테스트 데이터를 추가하는 방식으로 테스트 범위를 넓힐 수 있다.</p>
<p>정리하면, AI 소프트웨어 테스트에서는 블랙박스 테스트(Black-box Testing) 관점에서 변성 테스트(Metamorphic Testing), 탐색적 테스트(Exploratory Testing) 등을 사용할 수 있고, 화이트박스 테스트 관점에서는 뉴런 커버리지(Neuron Coverage)와 같은 기법을 사용할 수 있다. 뉴런 커버리지는 AI 모델 내부 구조를 기준으로 테스트 범위를 확인하는 대표적인 방법이며, 이후 임계점 커버리지(Threshold Coverage), 부호 변경 커버리지(Sign Change Coverage), 값 변경 커버리지(Value Change Coverage)와 같은 기법으로 확장될 수 있다.</p>
<hr />
<h3>3️⃣ Virtual-based AI software testing environment</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/b3cbbed6-601c-4b6f-932e-a74cb0c2ae15.png" alt="" style="display:block;margin:0 auto" />

<p>가상 기반 AI 소프트웨어 테스트 환경(Virtual-based AI Software Testing Environment)은 실제 하드웨어(Hardware)나 실제 소프트웨어 환경을 직접 사용하지 않고, 디지털 트윈(Digital Twin)이나 시뮬레이션(Simulation)을 활용하여 AI 소프트웨어(AI Software)를 테스트하는 환경을 말한다. 꼭 AI 소프트웨어에만 적용되는 개념은 아니지만, AI 소프트웨어는 현실 세계의 다양한 상황을 학습하고 판단해야 하는 경우가 많기 때문에 가상 기반 테스트 환경의 필요성이 특히 크다.</p>
<p>디지털 트윈(Digital Twin)은 현실 세계의 물리적 환경을 가상 공간에 동일하거나 유사하게 구현한 것을 의미한다. 예를 들어 실제 도로에 센서(Sensor), 카메라(Camera), 건물, 차량, 보행자 등이 있다면, 이러한 요소들을 가상 환경 안에 재현할 수 있다. 또한 현실에서 교통 상황이나 주변 환경이 변하면, 가상 환경에서도 그 변화를 반영하여 실제와 비슷한 조건을 만들 수 있다. 현실 세계와 가상 세계를 서로 대응시키는 개념이기 때문에 ‘트윈(Twin)’이라는 표현이 사용된다.</p>
<p>이러한 가상 환경에서는 자율주행 자동차(Autonomous Vehicle), 드론(Drone), 물류 시스템(Logistics System), 항만 시스템(Port System), 군사 훈련 시스템(Military Training System) 등 다양한 AI 소프트웨어를 테스트할 수 있다. 실제 도로, 공중, 항만, 물류 창고, 훈련장을 그대로 사용하지 않고도 가상으로 환경을 구성한 뒤, 그 안에서 AI 모델이 어떻게 판단하고 동작하는지 확인할 수 있다.</p>
<p>가상 기반 테스트 환경이 필요한 가장 큰 이유는 현실 세계에서 테스트하기 어려운 제약이 많기 때문이다. 예를 들어 자율주행 자동차의 사고 상황이나 위험한 도로 상황을 실제로 일부러 재현하는 것은 매우 위험하다. 또한 현실에서는 비가 오는 날, 안개가 낀 날, 터널을 빠져나오자마자 장애물이 있는 상황, 갑자기 보행자가 뛰어나오는 상황처럼 다양한 엣지 케이스(Edge Case)를 모두 직접 테스트하기 어렵다.</p>
<p>가상 환경에서는 이러한 위험한 시나리오(Scenario)나 예외적인 상황을 비교적 안전하게 재현할 수 있다. 사고 위험이 큰 상황도 실제 사람이나 장비를 위험에 빠뜨리지 않고 테스트할 수 있으며, 같은 조건을 여러 번 반복해서 실행할 수도 있다. 이를 통해 AI 소프트웨어가 다양한 환경에서도 안정적으로 판단하는지, 위험 상황에서 적절하게 반응하는지 확인할 수 있다.</p>
<p>또한 가상 테스트 환경은 시간과 비용을 줄이는 데도 도움이 된다. 현실에서 여러 환경을 직접 구성하려면 많은 비용과 시간이 필요하지만, 가상 환경을 한 번 구축해두면 다양한 조건을 바꾸어가며 반복적으로 테스트할 수 있다. 여러 테스트를 병렬적으로 동시에 수행할 수도 있기 때문에 전체 테스트 일정과 비용을 줄이는 효과가 있다.</p>
<p>정리하면, 가상 기반 AI 소프트웨어 테스트 환경은 현실 세계를 디지털 트윈이나 시뮬레이션으로 구현하고, 그 안에서 AI 소프트웨어를 안전하고 반복적으로 테스트하는 환경이다. 특히 AI 소프트웨어는 예측하기 어려운 상황, 위험한 시나리오, 다양한 엣지 케이스에 대응해야 하므로, 가상 환경을 활용한 테스트가 매우 중요하다.</p>
]]></content:encoded></item><item><title><![CDATA[AI Software Quality Lecture Note: From Core Concepts to Development Process]]></title><description><![CDATA[1️⃣Overview of AI Software2️⃣Quality Issues and Characteristics of AI Software3️⃣ AI Software Development and Quality

1️⃣Overview of AI Software


AI 소프트웨어 개요(AI Software Overview)
AI 소프트웨어(AI Softwa]]></description><link>https://heesu.tech/ai-software-quality-lecture-note-from-core-concepts-to-development-process</link><guid isPermaLink="true">https://heesu.tech/ai-software-quality-lecture-note-from-core-concepts-to-development-process</guid><dc:creator><![CDATA[Heesu Noh]]></dc:creator><pubDate>Wed, 27 May 2026 13:33:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/bc50275f-bc11-439c-a88d-75e4850ade01.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>1️⃣Overview of AI Software<br />2️⃣Quality Issues and Characteristics of AI Software<br />3️⃣ AI Software Development and Quality</p>
<hr />
<h1>1️⃣Overview of AI Software</h1>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/db99cd2e-f1eb-4277-9fe0-3f269cc7ebf9.png" alt="" style="display:block;margin:0 auto" />

<h3>AI 소프트웨어 개요(AI Software Overview)</h3>
<p>AI 소프트웨어(AI Software)는 인공지능 기술을 활용해 만들어지는 소프트웨어 제품이다. 일반적인 소프트웨어가 정해진 명령과 규칙에 따라 동작한다면, AI 소프트웨어는 데이터를 바탕으로 판단하거나 예측하고, 사람의 지능적인 작업을 모방하도록 만들어진다.</p>
<p>AI 소프트웨어에는 기계학습(Machine Learning), 자연어 처리(Natural Language Processing), 컴퓨터 비전(Computer Vision), 빅데이터(Big Data) 등의 기술이 복합적으로 적용될 수 있다. 이를 통해 사용자의 질문에 답변하거나, 주변 환경을 인식하고, 상황에 맞게 스스로 판단하여 행동할 수 있다.</p>
<p>예를 들어 챗봇(Chatbot)이나 GPT와 같은 대화형 인공지능 모델(Conversational AI Model)은 사람이 대화하는 것처럼 사용자의 요청에 자연스럽게 응답한다. 또한 서빙 로봇이나 물류 로봇은 주변 상황을 인식하고, 멈추거나 이동해야 하는 상황을 판단하여 서비스를 제공한다.</p>
<p>자율주행 자동차(Autonomous Vehicle)도 AI 소프트웨어가 활용되는 대표적인 사례이다. 자율주행 자동차는 도로, 사람, 차량, 장애물 등 다양한 환경을 인식하고, 운전자가 판단하듯이 안전한 주행을 수행하도록 설계된다.</p>
<p>따라서 AI 소프트웨어는 사람이 수행하는 판단, 예측, 인식, 대화와 같은 지능적인 작업을 모방하거나, 스스로 판단하여 작업을 수행할 수 있도록 돕는 소프트웨어라고 정리할 수 있다.</p>
<hr />
<h3>기존 소프트웨어와 AI 소프트웨어의 차이(Difference Between Traditional Software and AI Software)</h3>
<p>기존 소프트웨어(Traditional Software)는 정해진 요구사항(Requirements)을 만족하도록 개발된다. 사용자가 제시한 기능이나 조건에 맞게 동작하면 품질이 좋은 소프트웨어라고 판단할 수 있다. 즉, 요구사항이 비교적 명확하게 정의되어 있고, 그 요구사항대로 동작하는지가 중요한 기준이 된다.</p>
<p>반면 AI 소프트웨어(AI Software)는 모든 요구사항을 명확하게 정의하기 어렵다는 특징이 있다. 사람도 모든 상황을 미리 경험하지 못하지만, 과거의 경험을 바탕으로 새로운 상황을 판단한다. AI 소프트웨어도 이와 비슷하게 제한된 데이터(Data)를 바탕으로 학습하고, 새로운 상황이 주어졌을 때 학습한 내용을 기반으로 판단하거나 예측한다.</p>
<p>따라서 AI 소프트웨어는 기존 소프트웨어처럼 모든 동작을 미리 정해두기보다는, 학습 데이터(Training Data)를 기반으로 상황에 맞는 결과를 만들어내는 방식으로 동작한다. 이 점이 기존 소프트웨어와 AI 소프트웨어의 가장 큰 차이이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a6f8352b-6adb-4288-ab7a-6912c9cec760.png" alt="" style="display:block;margin:0 auto" />

<h3>AI 소프트웨어의 동작 방식(How AI Software Works)</h3>
<p>AI 소프트웨어(AI Software)는 데이터(Data)를 기반으로 학습하고, 학습한 내용을 바탕으로 새로운 상황을 예측(Prediction)하거나 판단(Decision)한다. 입력된 데이터에서 규칙적인 패턴(Pattern)을 찾고, 이를 통해 학습된 모델(Model)을 만든다.</p>
<p>예를 들어 동물을 식별하는 AI는 개와 고양이 이미지 데이터를 학습한다. 이때 각 이미지에는 “개(Dog)” 또는 “고양이(Cat)”처럼 정답을 표시한 레이블(Label)이 붙어 있을 수 있다. AI는 이러한 학습 데이터(Training Data)를 신경망(Neural Network)이나 알고리즘(Algorithm)을 통해 학습하고, 개와 고양이를 구분하는 모델을 만든다.</p>
<p>이후 새로운 이미지 데이터가 들어오면, 학습된 모델은 해당 이미지가 개에 가까운지 고양이에 가까운지 예측한다. 다만 AI의 판단은 항상 100% 정확한 것이 아니라, 학습한 데이터와 패턴을 바탕으로 가장 가능성이 높은 결과를 예측하는 것이다.</p>
<p>따라서 AI 소프트웨어의 품질을 확보하기 위해서는 충분한 양의 데이터뿐만 아니라, 데이터가 정확하고 적절하게 구성되어 있는지도 중요하다. 잘못된 데이터나 부족한 데이터로 학습하면 AI 모델의 예측 결과도 부정확해질 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/980db308-9ae7-451a-9d75-b624bcc32d2a.png" alt="" style="display:block;margin:0 auto" />

<h3>AI 소프트웨어에서 발생할 수 있는 문제점(Problems in AI Software)</h3>
<p>AI 소프트웨어(AI Software)는 데이터(Data)를 기반으로 학습하고 예측하기 때문에, 품질 문제가 발생할 수 있다. 대표적인 문제는 크게 세 가지로 정리할 수 있다.</p>
<p>첫째, 훈련 데이터(Training Data)의 품질이 낮거나 편향(Bias)될 수 있다. 데이터가 부족하거나 잘못되어 있으면 AI 모델(Model)도 잘못된 패턴을 학습하게 된다. 또한 특정 집단이나 상황에 치우친 데이터로 학습하면 실제 결과도 편향될 수 있다.</p>
<p>둘째, 훈련 과정(Training Process)에서 에포크(Epoch) 수나 파라미터(Parameter) 값이 적절하지 않을 수 있다. 학습이 너무 부족하면 모델이 충분히 패턴을 배우지 못하고, 반대로 너무 과도하게 학습하면 훈련 데이터에만 지나치게 맞춰지는 문제가 생길 수 있다.</p>
<p>셋째, 훈련 데이터와 실제 데이터(Real-world Data) 사이에 차이가 생길 수 있다. AI 모델은 학습한 환경과 실제 사용 환경이 다르면 예상보다 낮은 성능을 보일 수 있다. 특히 시간이 지나면서 환경이 변하면 기존 모델의 판단이 부정확해질 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/50deebbe-c5f1-4964-872c-e95d9aec4984.png" alt="" style="display:block;margin:0 auto" />

<h3>훈련 데이터 품질과 편향(Training Data Quality and Bias)</h3>
<p>AI 소프트웨어(AI Software)의 성능은 훈련 데이터(Training Data)의 품질에 큰 영향을 받는다. 훈련 데이터의 품질이 낮으면, 이를 바탕으로 학습한 모델(Model)의 성능도 떨어질 수밖에 없다.</p>
<p>예를 들어 얼굴 인식(Face Recognition) AI를 학습할 때 흐릿한 이미지, 조명이 좋지 않은 이미지, 각도가 불안정한 이미지가 많이 포함되어 있다면 모델이 얼굴을 정확하게 식별하기 어렵다. 즉, 데이터가 부정확하거나 품질이 낮으면 AI의 판단 결과도 부정확해질 수 있다.</p>
<p>또한 훈련 데이터가 특정 인종, 성별(Gender), 연령(Age)에 치우쳐 있으면 편향(Bias) 문제가 발생할 수 있다. 특정 그룹의 데이터가 많으면 그 그룹에 대해서는 성능이 좋을 수 있지만, 데이터가 부족한 그룹에 대해서는 성능이 낮아질 수 있다. 따라서 다양한 집단을 포함한 균형 잡힌 데이터가 필요하다.</p>
<p>마지막으로 훈련 데이터가 특정 인물이나 특정 환경(Environment)에만 한정되어 있으면 일반화(Generalization) 능력이 부족해질 수 있다. 예를 들어 자율주행 자동차(Autonomous Vehicle)가 도시 환경 위주로만 학습했다면, 시골길이나 낯선 도로 환경에서는 안전하게 판단하지 못할 수 있다. 따라서 AI 모델이 실제 환경에서도 잘 동작하려면 다양한 상황과 환경을 반영한 데이터로 학습해야 한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/03ecf4a0-bd44-40d7-bb4c-8b12c1afbe67.png" alt="" style="display:block;margin:0 auto" />

<h3>훈련 과정의 문제: 과적합과 과소적합(Overfitting and Underfitting)</h3>
<p>AI 소프트웨어(AI Software)는 훈련 과정(Training Process)에서 <code>에포크(Epoch)</code> 수나 파라미터(Parameter) 값이 적절하지 않으면 품질 문제가 발생할 수 있다. 대표적인 문제는 <code>과적합(Overfitting)</code>과 <code>과소적합(Underfitting)</code>이다.</p>
<p><code>과적합(Overfitting)</code>은 모델(Model)이 훈련 데이터(Training Data)에 지나치게 맞춰진 상태를 말한다. 에포크 수가 너무 많거나 모델이 너무 복잡하면, 훈련 데이터에서는 좋은 성능을 보이지만 새로운 데이터(Unseen Data)에 대해서는 정확도가 떨어질 수 있다. 예를 들어 훈련 데이터의 대부분이 개 이미지이고 고양이 이미지는 적다면, 모델은 개는 잘 구분하지만 고양이는 제대로 예측하지 못할 수 있다.</p>
<p><code>과소적합(Underfitting)</code>은 모델이 훈련 데이터의 패턴(Pattern)을 충분히 학습하지 못한 상태를 말한다. 에포크 수가 너무 적거나 모델 구조가 너무 단순하면, 훈련 데이터와 새로운 데이터 모두에서 성능이 낮게 나타날 수 있다. 예를 들어 감정 분석 AI가 “좋다”, “나쁘다”처럼 단순한 감정만 학습했다면, 복잡한 감정 상태를 제대로 구분하지 못할 수 있다.</p>
<p>따라서 AI 모델을 개발할 때는 에포크 수, 모델 복잡도(Model Complexity), 파라미터 값을 적절하게 조정하여 훈련 데이터에만 치우치지 않고 새로운 데이터에도 잘 동작하도록 만들어야 한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/4d446a5b-749d-4c4e-90a9-17a00bc6e14c.png" alt="" style="display:block;margin:0 auto" />

<h3>훈련 데이터와 실제 환경의 차이(Training-Serving Data Difference)</h3>
<p>AI 소프트웨어(AI Software)는 훈련 데이터(Training Data)를 기반으로 학습하지만, 실제 사용 환경(Real-world Environment)이 훈련 환경과 다르면 예상대로 동작하지 못할 수 있다.</p>
<p>예를 들어 자율주행 자동차(Autonomous Vehicle)의 AI 모델이 주로 차량이 있는 도로 데이터만 학습했다면, 실제 도로에서 보행자(Pedestrian), 자전거(Bicycle), 공사 구간(Construction Zone) 같은 다양한 요소를 제대로 인식하지 못할 수 있다.</p>
<p>또한 훈련된 환경과 실제 운영 환경이 달라지는 경우도 문제가 된다. 예를 들어 모델이 건조하고 밝은 날씨의 도로만 학습했다면, 비가 오거나 안개가 낀 상황에서는 정확한 판단이 어려울 수 있다. 도로 구조가 바뀌거나 우회도로가 생기는 경우도 모델 성능에 영향을 줄 수 있다.</p>
<p>특히 자율주행처럼 안전(Safety)이 중요한 시스템에서는 이러한 차이가 큰 문제가 될 수 있다. 실제 환경에서도 AI가 안정적으로 판단하고 제어할 수 있도록 다양한 상황을 반영한 데이터와 지속적인 검증이 필요하다.</p>
<hr />
<h3>정리(Summary): AI 소프트웨어와 품질 문제(AI Software and Quality Issues)</h3>
<p>지금까지 AI 소프트웨어(AI Software)의 정의와 품질에 영향을 주는 문제점을 살펴보았다. AI 소프트웨어는 데이터(Data)를 기반으로 학습하고, 학습된 모델(Model)을 통해 판단하거나 예측하는 소프트웨어이다.</p>
<p>AI 소프트웨어의 품질 문제는 주로 데이터와 모델 측면에서 발생한다. 훈련 데이터(Training Data)의 품질이 낮거나 편향되면 모델의 성능이 떨어질 수 있고, 훈련 과정에서 에포크(Epoch)나 파라미터(Parameter)가 적절하지 않으면 과적합(Overfitting) 또는 과소적합(Underfitting)이 발생할 수 있다. 또한 훈련 환경과 실제 사용 환경이 다르면 모델이 예상대로 동작하지 않을 수 있다.</p>
<p>기존 소프트웨어(Traditional Software)는 정해진 요구사항(Requirements)을 얼마나 잘 만족하는지가 품질의 기준이 된다. 하지만 AI 소프트웨어는 모든 상황과 요구사항을 명확히 정의하기 어렵다. 아직 경험하지 못한 데이터와 환경이 많기 때문에, 제한된 학습 데이터를 바탕으로 최대한 적절한 결과를 내야 한다.</p>
<p>따라서 AI 소프트웨어의 품질 확보(Quality Assurance)는 앞으로 계속 해결해야 할 중요한 과제이다. AI가 실제 환경에서도 신뢰성 있게 동작하려면 데이터 품질, 모델 학습 과정, 실제 환경 변화까지 함께 고려해야 한다.</p>
<hr />
<h1>2️⃣Quality Issues and Characteristics of AI Software</h1>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/80b78dcf-38a6-428d-90b9-f45eecb9578b.png" alt="" style="display:block;margin:0 auto" />

<h2>AI 소프트웨어의 품질 문제와 영향(Quality Issues and Impact of AI Software)</h2>
<p>이번 챕터에서는 AI 소프트웨어(AI Software)의 품질 문제와 특성에 대해 살펴본다. AI 소프트웨어는 자율주행 자동차(Autonomous Vehicle), 항공기(Aircraft), 의료기기(Medical Device)와 같은 최종 제품 안에서 시스템(System)이나 서브시스템(Subsystem)을 제어하는 중요한 요소가 될 수 있다.</p>
<p>AI 소프트웨어에 결함(Defect)이 발생하면 단순히 소프트웨어 내부 문제로 끝나지 않는다. 개발자의 실수, 낮은 품질의 데이터(Data), 편향된 데이터(Biased Data), 새로운 환경을 반영하지 못한 학습 문제 등으로 인해 모델(Model)의 성능이 낮아질 수 있다. 예를 들어 개를 고양이로 잘못 판단하는 것처럼 잘못된 예측이나 분류가 발생할 수 있다.</p>
<p>이러한 오류는 상위 <code>서브시스템</code>과 전체 <code>시스템</code>으로 전달되어 <code>최종 제품</code>의 동작에 영향을 줄 수 있다. 특히 자율주행처럼 안전이 중요한 분야에서는 AI 소프트웨어가 경험하지 못한 환경에서 오동작을 일으키고, 그 결과 인명 피해, 상해, 경제적 손실, 환경적 피해로 이어질 수 있다.</p>
<p>따라서 AI 소프트웨어의 품질 문제는 단순한 성능 저하가 아니라 실제 사고와 위해(Harm)로 연결될 수 있는 중요한 문제이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/dc61f8e9-7b26-43c4-9d2b-d0b752b238d0.png" alt="" style="display:block;margin:0 auto" />

<h2>AI 소프트웨어의 위험성(Risks of AI Software)</h2>
<p>AI 소프트웨어(AI Software)는 다양한 분야에서 활용되지만, 결함이나 악용이 발생하면 실제 피해(Harm)로 이어질 수 있다. 대표적인 사례로는 딥페이크(Deepfake), 피싱(Phishing), AI 예술(AI Art), 의료 AI(Medical AI) 문제가 있다.</p>
<p>딥페이크(Deepfake)는 AI를 이용해 특정 인물의 얼굴이나 목소리를 실제처럼 흉내 내는 기술이다. 이를 악용하면 유명인이나 공인을 사칭해 가짜 광고, 투자 사기, 구매 유도 영상 등을 만들 수 있다. 실제로 호주 Scamwatch도 유명인 딥페이크를 이용한 투자 사기가 증가하고 있다고 경고했다.</p>
<p>대화형 인공지능(Conversational AI)도 악용될 수 있다. ChatGPT와 같은 생성형 AI(Generative AI)를 사용하면 자연스러운 문장이나 이메일을 쉽게 만들 수 있기 때문에, 피싱 이메일(Phishing Email)이나 사기 메시지가 더 그럴듯하게 작성될 수 있다. 사용자가 이를 진짜로 착각해 링크를 클릭하면 개인정보 유출이나 경제적 피해가 발생할 수 있다.</p>
<p>AI 예술(AI Art)도 논쟁이 되고 있다. 예를 들어 2022년 미국 콜로라도 주립 박람회(Colorado State Fair)에서 Jason Allen의 AI 생성 이미지가 디지털 아트 부문에서 1위를 차지하면서, AI로 만든 예술을 어떻게 평가하고 보상해야 하는지 논쟁이 커졌다.</p>
<p>의료 분야에서도 AI 소프트웨어의 위험성이 나타날 수 있다. IBM Watson for Oncology는 암 치료 추천에 사용된 AI 시스템이었는데, 2018년 STAT 보도에 따르면 내부 문서에서 안전하지 않거나 부정확한 치료 추천 사례가 지적되었다. 의료 AI가 환자의 상태나 실제 환경을 충분히 반영하지 못하면 잘못된 치료 방향을 제시할 수 있고, 이는 환자에게 큰 위해로 이어질 수 있다.</p>
<p>따라서 AI 소프트웨어의 위험성은 단순한 기술 오류를 넘어 경제적 손실, 사회적 논쟁, 개인정보 피해, 의료 사고와 같은 다양한 형태의 Harm으로 연결될 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/6998a6e8-1f4e-4889-b463-bb7597ef6fb7.png" alt="" style="display:block;margin:0 auto" />

<h3>AI 소프트웨어의 품질 특성(Quality Characteristics of AI Software)</h3>
<p>AI 소프트웨어(AI Software)는 기존 소프트웨어(Traditional Software)처럼 기능적 특성(Functional Characteristics)과 비기능적 특성(Non-functional Characteristics)을 가진다. 하지만 AI 소프트웨어는 데이터와 모델을 기반으로 학습하고 판단하기 때문에, 기존 소프트웨어와 구분되는 추가적인 품질 특성도 함께 고려해야 한다.</p>
<p>AI 소프트웨어의 대표적인 품질 특성에는 적응성(Adaptability), 자율성(Autonomy), 진화성(Evolution), 유연성(Flexibility), 편향성(Bias), 성능 메트릭(Performance Metric), 투명성(Transparency), 복잡성(Complexity), 비결정성(Non-determinism)이 있다.</p>
<p>이러한 특성들은 AI 소프트웨어가 실제 환경에서 얼마나 안정적으로 동작하는지, 새로운 상황에 얼마나 잘 대응하는지, 판단 과정이 얼마나 설명 가능한지 등을 평가하는 데 중요한 기준이 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/af81fb76-c8aa-4cb9-86e8-56a11f8b353d.png" alt="" style="display:block;margin:0 auto" />

<h3>일반 소프트웨어의 품질 특성(ISO/IEC 25010 SQuaRE)</h3>
<p>ISO/IEC 25010 SQuaRE는 시스템과 소프트웨어의 품질 특성(Quality Characteristics)을 정의한 국제 표준이다. 특정 산업에만 한정되지 않고 금융, 자동차, 의료, 공공 시스템 등 다양한 분야의 소프트웨어 품질을 평가하는 기준으로 활용될 수 있다.</p>
<p>이 표준에서는 좋은 품질의 소프트웨어가 갖추어야 할 대표적인 특성으로 기능 적합성(Functional Suitability), 성능 효율성(Performance Efficiency), 호환성(Compatibility), 사용성(Usability), 신뢰성(Reliability), 보안성(Security), 유지보수성(Maintainability), 이식성(Portability)을 제시한다. 즉, 소프트웨어가 원하는 기능을 제대로 수행하고, 성능이 좋으며, 다른 시스템과 잘 호환되고, 사용하기 쉽고, 안정적이며, 보안과 유지보수 측면에서도 문제가 적어야 좋은 품질이라고 볼 수 있다.</p>
<p>AI 소프트웨어(AI Software)도 기본적으로 이러한 일반 소프트웨어 품질 특성을 만족해야 한다. 다만 AI 소프트웨어는 데이터(Data)와 모델(Model)을 기반으로 학습하고 판단하기 때문에, 여기에 더해 적응성(Adaptability), 자율성(Autonomy), 편향성(Bias), 투명성(Transparency), 비결정성(Non-determinism)과 같은 추가적인 품질 특성도 함께 고려해야 한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/4c6466e2-8f50-4b99-9bc8-4605031734d7.png" alt="" style="display:block;margin:0 auto" />

<h3>AI 소프트웨어의 품질 특성: 적응성, 자율성, 진화(Adaptability, Autonomy, Evolution)</h3>
<p>AI 소프트웨어(AI Software)는 기존 소프트웨어와 달리 환경 변화나 새로운 데이터에 따라 동작 방식이 달라질 수 있다. 그중 대표적인 품질 특성으로 적응성(Adaptability), 자율성(Autonomy), 진화(Evolution)가 있다.</p>
<p>적응성(Adaptability)은 환경이나 상황 변화에 맞춰 소프트웨어가 자동으로 조정되는 능력이다. 예를 들어 스마트홈 시스템(Smart Home System)은 사용자의 생활 패턴을 학습하여 에너지 사용을 조절할 수 있다. 사용자가 집에 없는 시간에는 에너지를 절약하고, 수면 시간에는 온도나 습도를 조절하는 방식이 이에 해당한다.</p>
<p>자율성(Autonomy)은 사람의 개입 없이 소프트웨어가 스스로 작업을 수행하고 의사결정(Decision Making)을 내리는 능력이다. 대표적인 예는 자율주행 자동차(Autonomous Vehicle)이다. 자율주행 자동차는 주변 환경을 인식하고, 차선 변경(Lane Change), 속도 조절(Speed Control) 등을 판단하며 안전하게 주행해야 한다.</p>
<p>진화(Evolution)는 시간이 지나면서 새로운 데이터(Data)나 요구사항(Requirements)을 반영해 성능을 향상시키는 능력이다. 예를 들어 대화형 인공지능 모델(Conversational AI Model)은 특정 시점까지의 데이터를 학습해 답변을 생성한다. 이후 새로운 데이터와 요구사항을 반영해 다시 학습하거나 개선되면 더 나은 성능을 제공할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/662d491d-437d-498d-b9ac-f219596ad3b7.png" alt="" style="display:block;margin:0 auto" />

<h3>AI 소프트웨어의 품질 특성: 유연성, 편향성, 성능 메트릭(Flexibility, Bias, Performance Metrics)</h3>
<p>유연성(Flexibility)은 AI 소프트웨어가 다양한 데이터 유형(Data Type)이나 작업 유형(Task Type)에 맞게 조정될 수 있는 능력이다. 예를 들어 음성 인식 소프트웨어(Speech Recognition Software)는 여러 언어, 다양한 사람의 발음, 억양, 사투리까지 인식할 수 있어야 한다. 특정한 형태의 데이터만 처리하는 것이 아니라, 다양한 입력 상황에 대응할 수 있는 능력이 유연성이다.</p>
<p>편향성(Bias)은 AI 모델(Model)이 특정 그룹이나 특성에 대해 치우친 예측을 내리는 경향을 말한다. 훈련 데이터(Training Data)가 특정 인종, 성별, 연령, 환경에 치우쳐 있으면 모델도 그 방향으로 편향될 수 있다. 예를 들어 AI 채용 소프트웨어(AI Recruiting Software)가 특정 성별이나 인종에게 불리한 판단을 내린다면 공정성(Fairness)에 문제가 생긴다. 따라서 AI 소프트웨어는 편향을 줄이고 공정한 판단을 할 수 있도록 관리되어야 한다.</p>
<p>성능 메트릭(Performance Metrics)은 AI 소프트웨어의 성능을 수치로 평가하는 기준이다. 대표적인 지표로 정확도(Accuracy), 정밀도(Precision), 재현율(Recall)이 있다.</p>
<p>정확도(Accuracy)는 전체 예측 중 올바르게 예측한 비율이다. 예를 들어 100개의 이메일 중 90개를 올바르게 분류했다면 정확도는 90%이다.</p>
<p>정밀도(Precision)는 AI가 스팸(Spam)이라고 예측한 것 중 실제로 스팸인 비율이다. 예를 들어 AI가 15개를 스팸으로 예측했고, 그중 실제 스팸이 13개라면 정밀도는 13/15이다.</p>
<p>재현율(Recall)은 실제 스팸 메일 중에서 AI가 스팸으로 올바르게 찾아낸 비율이다. 예를 들어 실제 스팸이 20개인데 AI가 그중 15개를 찾아냈다면 재현율은 15/20이다.</p>
<p>이처럼 AI 소프트웨어의 성능은 정확도, 정밀도, 재현율 같은 지표를 통해 평가할 수 있으며, 이러한 지표를 개선하는 것이 품질 향상과 연결된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/4d102394-a134-45e3-8583-823551434640.png" alt="" style="display:block;margin:0 auto" />

<h3>AI 소프트웨어의 품질 특성: 투명성, 복잡성, 비결정성(Transparency, Complexity, Non-determinism)</h3>
<p>투명성(Transparency)은 AI 소프트웨어가 어떤 근거와 과정을 통해 판단했는지 설명하거나 이해할 수 있게 하는 특성이다. AI 모델은 많은 데이터를 학습하고 복잡한 방식으로 결과를 예측하기 때문에, 내부 판단 과정을 사람이 바로 이해하기 어려울 수 있다. 예를 들어 이미지 분류 모델(Image Classification Model)이 고양이를 인식했다면, 어떤 특징을 보고 고양이라고 판단했는지 시각화하거나 설명할 수 있어야 한다. 이처럼 AI의 판단 과정을 이해 가능하게 만드는 것이 투명성이다.</p>
<p>복잡성(Complexity)은 AI 모델의 구조가 얼마나 복잡한지, 얼마나 많은 파라미터(Parameter)를 가지고 있는지를 의미한다. 심층 신경망(Deep Neural Network)은 매우 많은 파라미터를 사용해 데이터를 학습한다. 일반적으로 모델이 복잡할수록 더 다양한 패턴을 학습할 수 있어 성능이 높아질 수 있지만, 그만큼 내부 구조를 이해하거나 관리하기 어려워진다. 따라서 성능을 높이면서도 복잡성을 줄이려는 연구도 계속 진행되고 있다.</p>
<p>비결정성(Non-determinism)은 AI 모델의 결과가 항상 100% 확정적이지 않고, 확률적 판단에 기반할 수 있다는 특성이다. AI 소프트웨어는 모든 데이터를 학습할 수 없기 때문에 새로운 상황에서 정답에 가까운 결과를 예측한다. 따라서 기존 소프트웨어처럼 정해진 입력에 대해 항상 명확한 결과가 나오는 방식과는 차이가 있다. 이러한 불확실성이 AI 소프트웨어의 비결정성과 연결된다.</p>
<p>정리하면 AI 소프트웨어는 기존 소프트웨어의 품질 특성뿐 아니라 적응성(Adaptability), 자율성(Autonomy), 진화(Evolution), 유연성(Flexibility), 편향성(Bias), 성능 메트릭(Performance Metrics), 투명성(Transparency), 복잡성(Complexity), 비결정성(Non-determinism) 같은 추가적인 품질 특성을 함께 고려해야 한다.</p>
<hr />
<h1>3️⃣ AI Software Development and Quality</h1>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/1e368449-cd42-4ec6-9a6a-09f9adc97b8c.png" alt="" style="display:block;margin:0 auto" />

<h3>AI 소프트웨어 개발 품질(AI Software Development Quality)</h3>
<p>AI 소프트웨어(AI Software)는 하나의 제품(Product)이기 때문에, 품질을 확보하려면 개발과 운영 과정 전체의 품질을 높이는 것이 중요하다. 앞에서 살펴본 것처럼 AI 소프트웨어는 데이터(Data), 모델(Model), 학습 과정(Training Process), 실제 운영 환경(Operational Environment)에 따라 품질 문제가 발생할 수 있다.</p>
<p>따라서 AI 소프트웨어의 품질을 높이기 위해서는 단순히 결과만 확인하는 것이 아니라, 개발 프로세스(Development Process)를 체계적으로 관리해야 한다. 데이터 수집, 데이터 품질 검토, 모델 학습, 평가(Evaluation), 배포(Deployment), 운영 및 모니터링(Monitoring) 과정이 잘 관리되어야 최종 AI 제품의 품질도 높아질 수 있다.</p>
<p>기존 소프트웨어와 마찬가지로 AI 소프트웨어도 개발 프로세스를 따르지만, AI는 데이터와 학습 과정이 핵심이기 때문에 기존 소프트웨어와는 다른 관리 기준이 필요하다. 특히 데이터 품질 기준(Data Quality Criteria), 모델 평가 기준(Model Evaluation Criteria), 운영 중 성능 변화 확인이 중요하다.</p>
<p>결국 AI 소프트웨어의 품질을 확보하기 위해서는 제품 자체만 보는 것이 아니라, 제품을 개발하고 운영하는 프로세스의 품질(Process Quality)을 함께 높여야 한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/04986d1d-bb25-42e4-bb8b-cc15be8d7375.png" alt="" style="display:block;margin:0 auto" />

<h3>V모델(V-Model)과 AI 소프트웨어 개발(AI Software Development)</h3>
<p>AI 소프트웨어(AI Software)도 기존 소프트웨어처럼 V모델(V-Model)을 기반으로 개발할 수 있다. V모델은 개발 단계와 테스트 단계를 서로 연결하여, 개발 초반부터 품질을 관리하는 방식이다.</p>
<p>기존 V모델의 왼쪽에는 고객/사용자 요구사항(Customer/User Requirements), 요구사항 분석(Requirements Analysis), 아키텍처 설계(Architecture Design), 상세 설계(Detailed Design)가 있다. 아래쪽에는 구현(Coding)이 있으며, 오른쪽에는 단위 테스팅(Unit Testing), 통합 테스팅(Integration Testing), 시스템 테스팅(System Testing), 인수 테스팅(Acceptance Testing)이 배치된다.</p>
<p>V모델의 핵심은 각 개발 단계에 대응되는 테스트를 함께 고려하는 것이다. 즉, 개발이 끝난 뒤에만 테스트하는 것이 아니라 요구사항과 설계 단계부터 어떤 기준으로 검증할지 생각해야 한다. 이렇게 앞단계부터 품질을 관리해야 최종 제품의 품질도 높아질 수 있다.</p>
<p>AI 소프트웨어도 V모델을 완전히 벗어나는 것은 아니다. 다만 AI 소프트웨어는 데이터(Data), 모델(Model), 학습(Training), 평가(Evaluation) 같은 요소가 중요하기 때문에 기존 V모델과 용어와 적용 방식에는 차이가 있을 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/73d550d1-01fd-44dd-af19-f6d84f4ef5dd.png" alt="" style="display:block;margin:0 auto" />

<h3>V모델 기반 AI 소프트웨어 시스템 개발(V-Model-based AI Software System Development)</h3>
<p>AI 소프트웨어(AI Software)를 포함한 전체 시스템도 V모델(V-Model)을 기반으로 개발할 수 있다. 먼저 시스템 관점에서 요구사항을 분석하고, 시스템 아키텍처(System Architecture)를 설계한다. 이후 시스템을 구성하는 요소 중 어떤 부분을 일반 소프트웨어(Traditional Software)로 개발할지, 어떤 부분을 AI 소프트웨어로 개발할지 결정한다.</p>
<p>일반 소프트웨어는 기존 V모델처럼 요구사항 분석(Requirements Analysis), 아키텍처/상세 설계(Architecture/Detailed Design), 구현(Coding), 단위/통합 테스팅(Unit/Integration Testing), 시스템 테스팅(System Testing)의 과정을 따른다.</p>
<p>AI 소프트웨어도 V모델의 개념을 적용할 수 있다. 다만 용어와 절차가 조금 다르다. AI 소프트웨어는 먼저 가능한 범위 안에서 요구사항을 정의한다. 예를 들어 “고양이와 강아지를 분류하는 모델을 만든다”와 같은 목표를 설정한다. 이후 훈련 데이터(Training Data)를 준비하고, 데이터를 기반으로 모델을 훈련(Training)한다. 그다음 평가 데이터(Evaluation Data)를 사용해 모델을 테스트하고, 최종적으로 모델이 요구사항을 만족하는지 확인한다.</p>
<p>이후 일반 소프트웨어와 AI 소프트웨어는 하나의 시스템으로 통합되고, 통합 테스팅(Integration Testing), 시스템 테스팅(System Testing), 인수 테스팅(Acceptance Testing)을 거쳐 최종 제품으로 완성된다.</p>
<p>핵심은 AI 소프트웨어도 품질 확보(Quality Assurance)를 위해 체계적인 개발 프로세스를 따라야 한다는 점이다. V모델은 개발 단계와 테스트 단계를 연결하여 초기 단계부터 품질을 관리할 수 있게 해준다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/dcf35d19-d32a-48e2-afdd-cf0501486729.png" alt="" style="display:block;margin:0 auto" />

<h3>AI 소프트웨어 개발의 주요 단계: 요구사항 분석(Requirements Analysis)</h3>
<p>AI 소프트웨어 개발의 첫 단계는 요구사항 분석(Requirements Analysis)이다. 이 단계에서는 사용자(User), 고객(Customer), 이해관계자(Stakeholder)의 요구사항을 이해하고, AI가 해결해야 할 문제를 정의한다.</p>
<p>AI 소프트웨어는 기존 소프트웨어처럼 모든 요구사항을 완벽하게 정의하기 어렵지만, 가능한 범위 안에서 목표와 기준을 정해야 한다. 예를 들어 고양이 판별 AI를 만든다면, 단순히 고양이인지 아닌지만 분류할 것인지, 다른 동물도 함께 구분할 것인지, 고양이의 품종까지 세부적으로 분류할 것인지 등을 정해야 한다.</p>
<p>또한 입력 데이터(Input Data)의 범위도 요구사항에 포함된다. 고양이 얼굴만 판별할 것인지, 전신 이미지를 대상으로 할 것인지, 국내 고양이만 다룰 것인지 전 세계 다양한 고양이를 포함할 것인지도 결정해야 한다.</p>
<p>AI 소프트웨어에서는 성능 메트릭(Performance Metrics)도 중요한 요구사항이 된다. 예를 들어 정확도(Accuracy) 90%, 정밀도(Precision) 85%, 재현율(Recall) 90%와 같이 목표 성능을 정할 수 있다.</p>
<p>결국 AI 소프트웨어의 요구사항 분석은 기능적 요구사항(Functional Requirements)뿐만 아니라, 일반 소프트웨어 품질 특성과 AI 소프트웨어 고유의 품질 특성을 함께 고려하여 정리하는 과정이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/5c18e343-beed-423b-96a3-710bc8e89a42.png" alt="" style="display:block;margin:0 auto" />

<h3>훈련 데이터 준비(Training Data Preparation)</h3>
<p>AI 소프트웨어 개발의 다음 단계는 훈련 데이터 준비(Training Data Preparation)이다. 요구사항 분석을 통해 AI가 어떤 문제를 해결해야 하는지 정했다면, 그 요구사항을 만족할 수 있는 데이터셋(Dataset)을 설계하고 준비해야 한다.</p>
<p>예를 들어 요구사항이 “고양이인지 아닌지만 판별한다”라면, 이미지마다 “고양이(Cat)” 또는 “고양이가 아님(Not Cat)”이라는 레이블(Label)이 붙은 데이터를 준비하면 된다.</p>
<p>반면 고양이의 종류까지 구분해야 한다면, 고양이 품종마다 다른 레이블을 부여한 이미지 데이터가 필요하다. 이 경우 단순한 이진 분류(Binary Classification)가 아니라 여러 종류를 구분하는 다중 분류(Multi-class Classification)가 된다.</p>
<p>또한 같은 종류의 고양이라도 자세, 배경, 밝기, 촬영 각도 등이 다양한 이미지를 준비해야 한다. 그래야 AI 모델(Model)이 특정 조건에만 맞춰지는 것이 아니라, 다양한 상황에서도 고양이를 잘 판별할 수 있다.</p>
<p>즉, 훈련 데이터 준비는 AI 소프트웨어의 요구사항에 맞게 데이터를 수집하고, 레이블을 붙이고, 다양한 상황을 반영하도록 데이터셋을 구성하는 과정이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f94e6603-9251-40b5-8695-f73c47f95bdc.png" alt="" style="display:block;margin:0 auto" />

<h3>모델 훈련과 테스트(Model Training and Testing)</h3>
<p>세 번째 단계는 준비한 훈련 데이터(Training Data)를 이용해 AI 모델(Model)을 훈련하고, 모델이 제대로 학습되었는지 테스트하는 과정이다. 이 단계에서는 적절한 AI 모델을 선택하고, 훈련용 데이터셋(Training Dataset)을 기반으로 학습을 진행한다.</p>
<p>훈련이 끝난 뒤에는 훈련 데이터에 포함되지 않은 새로운 데이터(Unseen Data)나 테스트 데이터셋(Test Dataset)을 사용해 모델 성능을 평가해야 한다. 훈련 데이터에서만 성능이 좋고 새로운 데이터에서 성능이 낮다면, 실제 환경에서는 원하는 결과를 내기 어렵기 때문이다.</p>
<p>평가 결과가 부족하다면 선택한 AI 모델이 적절한지, 파라미터(Parameter)가 잘 설정되었는지, 훈련 데이터의 양이 충분한지, 데이터가 편향(Bias)되어 있지는 않은지 점검해야 한다. 필요하다면 모델을 수정하거나 데이터를 보완한 뒤 다시 훈련과 테스트를 진행한다.</p>
<p>또한 AI 소프트웨어는 한 번 높은 성능이 나왔다고 끝나는 것이 아니다. 실제 환경에서는 새로운 데이터와 상황이 계속 등장하기 때문에 지속적인 테스트(Continuous Testing)가 필요하다. 제품이 사용되는 동안 모델의 성능을 계속 확인하고, 필요한 경우 재학습(Retraining)이나 업데이트(Update)를 통해 품질을 유지해야 한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/c699e8c5-8497-41d8-b8f5-2922f33dcd46.png" alt="" style="display:block;margin:0 auto" />

<h3>일반 소프트웨어와 AI 소프트웨어 개발의 차이(Difference Between Traditional Software and AI Software Development)</h3>
<p>일반 소프트웨어(Traditional Software)와 AI 소프트웨어(AI Software)의 차이는 <code>연역(Deduction)</code>과 <code>귀납(Induction)</code>의 개념으로 이해할 수 있다.</p>
<p>일반 소프트웨어는 <code>연역적(Deductive)</code> 방식에 가깝다. 먼저 요구사항(Requirements)이라는 큰 전제를 정하고, 이를 만족하기 위해 설계(Design), 구현(Implementation), 테스트(Testing)를 구체화한다. 따라서 요구사항을 기준으로 예상 결과(Expected Result)를 정의하고, 실제 결과가 이를 만족하는지 테스트할 수 있다.</p>
<p>반면 AI 소프트웨어는 <code>귀납적(Inductive)</code> 방식에 가깝다. 개별 데이터 사례를 학습하고, 그 사례들로부터 일반적인 패턴이나 규칙을 찾아낸다. 예를 들어 여러 고양이 데이터를 보고 고양이의 특징을 학습한 뒤, 새로운 이미지가 들어왔을 때 고양이인지 판단하는 방식이다. <strong>Example:</strong> <em>Observation 1:</em> Every swan I have ever seen is white. <em>Observation 2:</em> Every swan my friends have seen is white. <em>Conclusion:</em> Therefore, all swans are white.</p>
<p>AI 소프트웨어는 모든 가능한 사례를 데이터로 수집하고 학습할 수 없기 때문에, 요구사항을 기존 소프트웨어처럼 명확하게 정의하기 어렵다. 알려지지 않은 상황(Unknown)이 많고, 실제 환경에서 새로운 데이터가 계속 등장하기 때문이다. 그래서 AI 소프트웨어는 일반 소프트웨어와 다른 개발 및 테스트 기법이 필요하다.</p>
]]></content:encoded></item><item><title><![CDATA[Git Basics: Version Control, Configuration, and Commits]]></title><description><![CDATA[소프트웨어 버전과 버전 관리 (Software Version & Version Control)
소프트웨어에서 버전(Version) 은 특정 소프트웨어나 코드의 상태를 구분하기 위해 붙이는 식별자(Identifier) 이다. 우리가 스마트폰 앱이나 운영체제를 업데이트할 때 “현재 버전”, “최신 버전”, “업데이트 버전”이라는 표현을 자주 접하는데, 이때의 ]]></description><link>https://heesu.tech/git-basics-version-control-configuration-and-commits</link><guid isPermaLink="true">https://heesu.tech/git-basics-version-control-configuration-and-commits</guid><dc:creator><![CDATA[Heesu Noh]]></dc:creator><pubDate>Mon, 25 May 2026 03:53:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f1f5a3f8-d146-4c34-8240-f9205c7b9b19.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/31189f83-378b-4ead-937b-c4b6a0aee15f.png" alt="" style="display:block;margin:0 auto" />

<h3>소프트웨어 버전과 버전 관리 (Software Version &amp; Version Control)</h3>
<p>소프트웨어에서 <strong>버전(Version)</strong> 은 특정 소프트웨어나 코드의 상태를 구분하기 위해 붙이는 <strong>식별자(Identifier)</strong> 이다. 우리가 스마트폰 앱이나 운영체제를 업데이트할 때 “현재 버전”, “최신 버전”, “업데이트 버전”이라는 표현을 자주 접하는데, 이때의 버전은 단순한 숫자가 아니라 해당 소프트웨어가 어느 시점의 상태인지 알려주는 기준이 된다.</p>
<p>예를 들어 어떤 프로그램에 문제가 생겨 고객센터에 문의하면, 보통 “어떤 버전을 사용하고 있나요?”라는 질문을 받는다. 이는 같은 프로그램이라도 버전에 따라 기능, 오류, 동작 방식이 다를 수 있기 때문이다. 따라서 버전은 소프트웨어를 구분하고 문제를 추적하기 위한 중요한 정보가 된다.</p>
<p>버전을 이해하기 위해 먼저 <strong>식별자(Identifier)</strong> 의 개념을 볼 필요가 있다. 식별자는 어떤 대상을 다른 대상과 구분하기 위해 붙이는 이름이나 번호를 말한다. 사람에게는 이름이나 학번, 사물에는 제품 번호나 모델명 등이 식별자가 될 수 있다. 이름만으로도 대상을 구분할 수 있지만, 번호를 사용하면 더 정확하고 세부적으로 구분할 수 있다. 그래서 소프트웨어 버전에서는 보통 숫자를 많이 사용한다. 숫자는 순서를 나타내기 쉽고, 변경 이력을 관리하기에도 편리하기 때문이다.</p>
<p>코드를 작성할 때도 버전의 개념이 필요하다. 아주 짧고 단순한 코드는 수정하거나 변경할 일이 거의 없을 수도 있다. 하지만 실제 소프트웨어 개발에서는 코드가 계속 수정되고 확장된다. 기능이 추가되거나 오류가 고쳐지고, 구조가 바뀌는 일이 반복된다. 이때 변경된 코드들을 모두 같은 것으로 볼 것인지, 아니면 서로 다른 상태로 구분할 것인지가 중요해진다.</p>
<p>컴퓨터 입장에서는 줄바꿈이나 빈칸 같은 변화가 실행 결과에 영향을 주지 않을 수도 있다. 예를 들어 일부 프로그래밍 언어에서는 공백이나 줄바꿈이 프로그램의 동작에 큰 영향을 주지 않는다. 하지만 사람 입장에서는 코드의 가독성, 구조, 이해도에 큰 차이를 만들 수 있다. 따라서 어떤 변경을 버전으로 구분할 것인지, 어떤 변경은 단순한 편집으로 볼 것인지도 버전 관리에서 생각해야 할 부분이다.</p>
<p>버전이라는 개념은 디지털 시대에만 생긴 것은 아니다. 컴퓨터가 널리 사용되기 전에도 문서, 서류, 설계도, 계약서 등은 종이 형태로 관리되었다. 같은 문서라도 작성 날짜가 다르거나 서식이 바뀌거나 내용이 수정되면 서로 다른 문서로 구분해야 했다. 이때도 “완전히 다른 문서로 볼 것인가?”, “같은 문서의 수정본으로 볼 것인가?”와 같은 판단이 필요했다. 이러한 개념이 소프트웨어 개발에서도 이어져, 변경된 내용을 기록하고 이전 상태와 비교하며 필요할 때 재사용할 수 있도록 발전한 것이 <strong>버전 관리(Version Control)</strong> 이다.</p>
<p>소프트웨어에서는 버전을 보통 숫자로 표시한다. 예를 들어 <code>1.0</code>, <code>1.1</code>, <code>2.0</code>과 같은 형태가 대표적이다. 숫자 버전은 소프트웨어의 변경 정도나 출시 순서를 표현하기 쉽다. 그러나 숫자만으로는 사람이 기억하거나 부르기 어려울 때도 있다. 그래서 소프트웨어나 프로젝트에 <strong>코드명(Code Name)</strong> 또는 <strong>프로젝트명(Project Name)</strong> 을 붙이기도 한다.</p>
<p>예를 들어 어떤 소프트웨어의 공식 버전은 숫자로 관리하되, 개발 과정이나 출시 단계에서는 사람이 부르기 쉬운 이름을 함께 사용할 수 있다. 숫자 버전은 정확한 식별에 유리하고, 코드명이나 프로젝트명은 의사소통과 기억에 유리하다. 즉, 버전 번호와 코드명은 서로 다른 목적을 가진다. 버전 번호는 정확한 구분을 위한 것이고, 코드명은 사람이 쉽게 이해하고 부르기 위한 이름에 가깝다.</p>
<p>결국 소프트웨어 버전은 단순히 숫자를 붙이는 작업이 아니라, 소프트웨어의 변화 과정을 기록하고 구분하는 체계이다. 어떤 기능이 추가되었는지, 어떤 오류가 수정되었는지, 어떤 시점의 코드가 사용되었는지를 알기 위해 버전이 필요하다. 이러한 버전 정보가 체계적으로 관리되어야 여러 사람이 함께 개발할 때 혼란을 줄이고, 문제가 발생했을 때 원인을 추적할 수 있다.</p>
<p>따라서 <strong>버전 관리(Version Control)</strong> 는 소프트웨어 개발에서 매우 중요한 개념이다. 버전 관리를 통해 개발자는 변경 이력을 남기고, 이전 상태로 되돌아가거나, 여러 개발자가 동시에 작업한 내용을 비교하고 통합할 수 있다. 소프트웨어가 계속 변화하는 대상이라는 점에서, 버전은 그 변화를 기록하고 관리하기 위한 기본 단위라고 할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/37b2bf83-670b-4177-8fba-7184a8dbcdd1.png" alt="" style="display:block;margin:0 auto" />

<h3>버전 표기 방식 (Version Numbering)</h3>
<p>소프트웨어의 <strong>버전 표기 방식(Version Numbering)</strong> 은 소프트웨어의 변경 정도를 쉽게 구분하기 위해 사용된다. 기본적으로는 숫자를 사용하며, 숫자를 어떻게 배치하느냐에 따라 소프트웨어가 얼마나 크게 바뀌었는지, 어떤 성격의 변경이 있었는지를 파악할 수 있다.</p>
<p>대표적인 방식으로 <strong>시맨틱 버저닝(Semantic Versioning, SemVer)</strong> 이 있다. 시맨틱 버저닝은 보통 <code>MAJOR.MINOR.PATCH</code> 형식으로 버전을 표기한다. 예를 들어 <code>2.1.3</code>이라는 버전이 있다면, <code>2</code>는 <strong>메이저 버전(Major Version)</strong>, <code>1</code>은 <strong>마이너 버전(Minor Version)</strong>, <code>3</code>은 <strong>패치 버전(Patch Version)</strong> 을 의미한다.</p>
<p><strong>메이저 버전(Major Version)</strong> 은 기존 버전과 호환되지 않을 정도로 큰 변화가 있을 때 증가한다. 예를 들어 기존에 사용하던 <strong>API(Application Programming Interface)</strong> 가 크게 바뀌어 이전 방식으로는 더 이상 정상적으로 사용할 수 없다면 메이저 버전이 올라간다.</p>
<p><strong>마이너 버전(Minor Version)</strong> 은 기존 버전과 호환성을 유지하면서 새로운 기능이 추가되었을 때 증가한다. 즉, 기존 사용자는 큰 문제 없이 계속 사용할 수 있지만, 새로운 기능이 더해진 경우이다.</p>
<p><strong>패치 버전(Patch Version)</strong> 은 기존 버전과 호환성을 유지하면서 버그를 수정했을 때 증가한다. 새로운 기능을 추가하기보다는 오류를 고치거나 안정성을 개선하는 변경에 해당한다.</p>
<p>따라서 <code>MAJOR.MINOR.PATCH</code> 형식의 번호만 보아도 소프트웨어가 완전히 크게 바뀐 것인지, 기능이 조금 추가된 것인지, 단순히 오류가 수정된 것인지 어느 정도 파악할 수 있다. 예를 들어 <code>1.2.3</code>에서 <code>1.2.4</code>로 바뀌었다면 보통 작은 버그 수정으로 이해할 수 있고, <code>1.2.3</code>에서 <code>2.0.0</code>으로 바뀌었다면 기존과 호환되지 않는 큰 변화가 있었을 가능성이 높다.</p>
<p>하지만 모든 소프트웨어가 반드시 이 규칙을 따르는 것은 아니다. <strong>시맨틱 버저닝</strong>은 널리 사용되는 규칙이지만, 프로젝트의 성격이나 개발 문화에 따라 버전 번호를 다르게 정하는 경우도 많다. 어떤 프로젝트는 날짜를 버전으로 사용하기도 하고, 어떤 프로젝트는 내부 기준에 따라 메이저 번호를 올리기도 한다.</p>
<p>숫자 뒤에는 문자나 단어가 추가되기도 한다. 대표적으로 <strong>알파(Alpha)</strong>, <strong>베타(Beta)</strong>, <strong>릴리스 후보(Release Candidate, RC)</strong> 가 있다. 시맨틱 버저닝에서는 정식 릴리스 전 버전을 <code>1.0.0-alpha</code>, <code>1.0.0-beta</code>, <code>1.0.0-rc.1</code>과 같은 방식으로 표기할 수 있다. 이러한 표기는 아직 최종 배포 전 단계라는 의미를 가진다.</p>
<p><strong>알파 버전(Alpha Version)</strong> 은 개발 초기 테스트 단계에 가깝다. 프로그램의 주요 기능이 어느 정도 만들어졌더라도 아직 안정성이 부족할 수 있고, 개발자나 내부 테스트 인원이 문제를 찾는 단계라고 볼 수 있다.</p>
<p><strong>베타 버전(Beta Version)</strong> 은 알파보다 더 완성도 있는 테스트 단계이다. 기능이 어느 정도 마무리된 상태에서 실제 사용자 환경에 가깝게 테스트하는 경우가 많다. 프로젝트에 따라 일반 사용자에게 공개되기도 하고, 제한된 사용자에게만 제공되기도 한다.</p>
<p><strong>릴리스 후보(Release Candidate, RC)</strong> 는 정식 배포 직전 단계의 버전이다. 큰 문제가 발견되지 않으면 이 버전이 최종 릴리스로 이어질 수 있다. 즉, “이 정도면 배포해도 될 것 같다”라고 판단되는 후보 버전이라고 이해할 수 있다.</p>
<p>다만 알파, 베타, 릴리스 후보의 의미도 프로젝트마다 조금씩 다를 수 있다. 어떤 프로젝트에서는 베타 단계에서도 기능이 추가될 수 있고, 어떤 프로젝트에서는 베타 이후부터는 새 기능을 넣지 않고 버그 수정만 진행하기도 한다. 따라서 이러한 용어는 일반적인 흐름을 이해하는 기준으로 보면 된다.</p>
<p><strong>리눅스 커널(Linux Kernel)</strong> 도 버전 번호를 사용하지만, 현재의 리눅스 커널 버전 방식은 시맨틱 버저닝을 엄격하게 따르는 방식이라고 보기는 어렵다. 과거 리눅스 커널은 버전 번호에서 안정 버전과 개발 버전을 구분하는 규칙을 사용한 적이 있다. 예를 들어 2.x 시기에는 짝수 마이너 버전을 안정 버전, 홀수 마이너 버전을 개발 버전으로 구분하는 방식이 사용되었다. 그러나 2.6 커널 이후에는 이러한 방식이 더 이상 사용되지 않게 되었다.</p>
<p>최근의 리눅스 커널에서는 메이저 버전이 올라간다고 해서 반드시 API가 크게 깨지거나, 엄청난 기능 변화가 있었다는 뜻은 아니다. 리누스 토르발스(Linus Torvalds)는 리눅스 커널의 큰 버전 번호 변경이 기능 기반이나 안정/불안정 구분을 의미하지 않으며, 큰 숫자를 피하기 위한 성격도 있다고 설명한 바 있다. 따라서 리눅스 커널의 버전 번호는 시맨틱 버저닝의 <code>MAJOR.MINOR.PATCH</code> 의미와 그대로 대응시키기보다는, 리눅스 커널 프로젝트 고유의 릴리스 흐름으로 이해하는 것이 더 정확하다.</p>
<p>결국 버전 표기 방식은 소프트웨어의 변경 이력을 간단하고 명확하게 전달하기 위한 약속이다. <strong>시맨틱 버저닝(Semantic Versioning)</strong> 은 그중 가장 널리 알려진 방식이지만, 모든 프로젝트가 반드시 같은 방식으로 버전을 관리하는 것은 아니다. 따라서 버전 번호를 볼 때는 숫자의 형식뿐만 아니라, 해당 프로젝트가 어떤 버전 정책을 사용하는지도 함께 확인해야 한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/89208459-0497-4f4a-88f7-8fa2f2606261.png" alt="" style="display:block;margin:0 auto" />

<h3>리눅스 라이브러리 버전 표기 방식 (Linux Library Versioning)</h3>
<p>소프트웨어 버전 표기 방식에는 앞서 살펴본 <strong>시맨틱 버저닝(Semantic Versioning)</strong> 외에도 여러 방식이 존재한다. 특히 리눅스/유닉스 계열에서 사용되는 <strong>공유 라이브러리(Shared Library)</strong> 의 버전 표기에는 일반적인 <code>MAJOR.MINOR.PATCH</code> 방식과 다른 기준이 사용되기도 한다.</p>
<p>그 예로 <strong>Libtool 버전 정보(Libtool Version Info)</strong> 에서 사용하는 <code>current:revision:age</code> 형식이 있다. 여기서 <code>current</code>, <code>revision</code>, <code>age</code>는 단순히 “큰 변경, 작은 변경, 버그 수정”을 의미하는 것이 아니라, 라이브러리의 <strong>인터페이스(Interface)</strong> 와 <strong>호환성(Compatibility)</strong> 을 기준으로 해석된다. GNU Libtool 문서에 따르면 <code>current</code>는 해당 라이브러리가 구현하는 가장 최신 인터페이스 번호이고, <code>revision</code>은 현재 인터페이스의 구현 번호이며, <code>age</code>는 최신 인터페이스와 가장 오래된 호환 인터페이스 사이의 차이를 의미한다.</p>
<p>이 방식은 특히 라이브러리의 <strong>ABI(Application Binary Interface)</strong> 호환성을 관리하기 위해 사용된다. 프로그램이 어떤 공유 라이브러리를 사용할 때, 단순히 기능이 추가되었는지만 중요한 것이 아니라 기존 프로그램이 새 라이브러리와 함께 실행되어도 문제가 없는지가 중요하다. 그래서 <code>current:revision:age</code> 방식은 라이브러리가 어떤 인터페이스 범위까지 호환되는지를 표현하기 위한 목적을 가진다.</p>
<p>예를 들어 <code>current</code>는 라이브러리가 제공하는 최신 인터페이스 번호를 나타내고, <code>age</code>는 그 라이브러리가 과거의 몇 단계 인터페이스까지 호환되는지를 나타낸다. 따라서 <code>current - age</code>부터 <code>current</code>까지의 인터페이스를 지원한다고 볼 수 있다. 만약 <code>current</code>와 <code>age</code>가 같은 두 라이브러리가 있다면, 동적 링커(Dynamic Linker)는 보통 더 높은 <code>revision</code> 값을 가진 라이브러리를 선택한다.</p>
<p>이 점에서 <code>current:revision:age</code>는 <strong>시맨틱 버저닝(Semantic Versioning)</strong> 과 구분된다. 시맨틱 버저닝의 <code>MAJOR.MINOR.PATCH</code>는 일반적으로 API 변경 정도를 사람이 이해하기 쉽게 표현하는 방식이다. 반면 <code>current:revision:age</code>는 라이브러리 인터페이스와 바이너리 호환성을 관리하기 위한 도구적 성격이 강하다. 즉, 숫자가 세 부분으로 나뉘어 있다는 점은 비슷하지만, 각 숫자가 의미하는 바는 서로 다르다.</p>
<p>또한 이 방식은 보통 사람이 직접 보는 제품 버전이라기보다는, 빌드 도구나 운영체제의 라이브러리 로딩 과정에서 의미를 가지는 경우가 많다. <strong>Libtool</strong> 은 <code>current:revision:age</code> 정보를 바탕으로 각 플랫폼에 맞는 공유 라이브러리 버전 이름을 생성한다. 따라서 이 숫자들을 일반적인 소프트웨어 출시 버전처럼 해석하면 혼동이 생길 수 있다.</p>
<p>최근에는 모든 프로젝트가 이러한 기준을 엄격하게 따르지는 않는다. 소프트웨어 규모가 커지고 개발 주기가 빨라지면서, 버전 번호를 어떤 기준으로 올릴 것인지가 프로젝트마다 달라졌기 때문이다. 실제로 Libtool 문서에서도 버전 정보를 너무 자주 갱신하면 <code>current</code> 인터페이스 번호가 빠르게 커질 수 있으므로, 공개 릴리스 직전에만 업데이트하라고 설명한다.</p>
<p>예전에는 버전 번호가 한 자리나 두 자리 수준인 경우가 많아 비교적 단순하게 느껴졌지만, 시간이 지나면서 <code>.20</code>, <code>.30</code>을 넘어 <code>.110</code>, <code>.111</code>처럼 세 자리 숫자가 등장하는 경우도 생겼다. 이렇게 숫자가 커지면 사용자가 버전 번호만 보고 변경 의미를 직관적으로 파악하기 어려워질 수 있다. 그래서 어떤 프로젝트는 시맨틱 버저닝을 따르고, 어떤 프로젝트는 날짜 기반 버전(Date-based Versioning)을 사용하며, 또 어떤 프로젝트는 자체적인 릴리스 정책에 따라 버전을 붙인다.</p>
<p>결국 버전 표기 방식은 하나의 고정된 정답이 아니라, 프로젝트의 목적과 관리 방식에 따라 선택되는 약속이다. <strong>시맨틱 버저닝(Semantic Versioning)</strong> 은 사람이 변경 정도를 이해하기 쉽게 만드는 데 유용하고, <strong>Libtool의</strong> <code>current:revision:age</code> <strong>방식</strong> 은 리눅스/유닉스 계열 공유 라이브러리의 인터페이스와 바이너리 호환성을 관리하는 데 유용하다. 따라서 버전 번호를 볼 때는 숫자의 형식만 보고 판단하기보다, 해당 프로젝트가 어떤 버전 정책을 사용하는지 함께 확인해야 한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/b386c05a-3d51-4993-94fd-7c203473b93f.png" alt="" style="display:block;margin:0 auto" />

<h3>버전 관리와 소프트웨어 개발의 핵심 도구 (Version Control &amp; Core Tool for Software Development)</h3>
<p>소프트웨어 개발에서 <strong>버전(Version)</strong> 을 숫자로 표기하는 것도 중요하지만, 실제 개발 과정에서는 그 버전을 어떻게 기록하고 관리할 것인지가 더 중요하다. 단순히 <code>1.0</code>, <code>2.1.3</code>과 같은 숫자를 붙이는 것만으로는 충분하지 않다. 누가 그 버전을 결정했는지, 어떤 파일들이 그 버전에 포함되는지, 이전 버전과 무엇이 달라졌는지를 함께 관리해야 하기 때문이다.</p>
<p>초기에는 별도의 <strong>버전 관리 도구(Version Control Tool)</strong> 없이 파일 안에 직접 버전 정보를 적는 방식이 사용되었다. 예를 들어 코드 파일의 주석(Comment)에 “이 파일은 1.0 버전이다”, “현재 버전은 2.1.3이다”와 같은 내용을 남기는 식이었다. 파일 수가 적고 개발자가 한 명이거나 소규모인 경우에는 이런 방식도 어느 정도 효과가 있었다. 이는 종이 문서를 관리하던 방식과 비슷하다. 종이 문서에서도 작성일, 수정일, 문서 번호, 개정 번호 등을 표시하여 어떤 문서가 최신인지 구분했는데, 이러한 관리 방식이 초기 소프트웨어 개발에도 이어진 것이다.</p>
<p>하지만 소프트웨어의 규모가 커지면서 상황이 달라졌다. 코드 파일이 10개, 100개, 1000개로 늘어나고, 여러 개발자가 동시에 프로젝트에 참여하게 되면서 단순히 파일 안에 주석으로 버전 정보를 적는 방식은 한계가 생겼다. 어떤 파일이 언제 수정되었는지, 누가 수정했는지, 어떤 이유로 바뀌었는지, 이전 상태로 되돌릴 수 있는지 등을 사람이 직접 관리하기 어려워졌기 때문이다.</p>
<p>이러한 문제를 해결하기 위해 등장한 것이 <code>버전 관리 시스템(Version Control System, VCS)</code> 이다. 버전 관리 시스템은 프로젝트의 변경 이력을 추적하고, 파일의 상태를 관리하며, 여러 개발자가 협업할 수 있도록 도와주는 소프트웨어 개발의 핵심 도구이다.</p>
<p>버전 관리 시스템을 사용하면 개발자는 코드의 변경 내용을 기록할 수 있다. 어떤 파일이 수정되었는지, 어떤 줄이 추가되거나 삭제되었는지, 누가 언제 변경했는지 확인할 수 있다. 또한 문제가 발생했을 때 이전 상태로 되돌아가거나, 특정 시점의 코드를 다시 확인할 수도 있다. 이러한 기능은 개인 개발뿐만 아니라 팀 개발에서도 매우 중요하다.</p>
<p>과거에는 소규모 팀이나 개인 프로젝트에서는 이력 추적이 크게 중요하지 않다고 생각할 수도 있었다. 하지만 현대 소프트웨어 개발 환경에서는 코드의 양이 많든 적든 버전 관리 도구를 사용하는 것이 거의 기본이 되었다. 작은 프로젝트라도 시간이 지나면 수정 내용이 누적되고, 실수로 코드를 잘못 변경할 수 있으며, 이전 상태를 확인해야 할 일이 생기기 때문이다. 따라서 오늘날에는 개인 프로젝트에서도 <code>Git</code> 과 같은 버전 관리 도구를 사용하는 것이 일반적이다.</p>
<p>대표적인 버전 관리 도구로는 <code>Git</code> 이 있다. Git은 현재 가장 널리 사용되는 <strong>분산 버전 관리 시스템(Distributed Version Control System)</strong> 중 하나이다. 특히 <strong>리눅스 커널(Linux Kernel)</strong> 소스 코드는 Git을 사용해 관리된다. 리눅스 커널은 매우 큰 규모의 오픈소스 프로젝트이며, 전 세계의 많은 개발자가 참여한다. 이런 프로젝트에서는 변경 이력을 정확하게 추적하고, 여러 개발자의 작업을 조율하는 것이 매우 중요하기 때문에 Git과 같은 도구가 필수적이다.</p>
<p>버전 관리 도구는 특히 <strong>텍스트 파일(Text File)</strong> 로 작성된 소스 코드를 관리하는 데 강점을 가진다. 소스 코드는 대부분 문자로 이루어져 있고, 줄(Line) 단위로 구성되어 있기 때문에 이전 버전과 현재 버전을 비교하기 쉽다. 예를 들어 어떤 줄이 추가되었는지, 어떤 줄이 삭제되었는지, 어떤 함수가 수정되었는지를 비교할 수 있다. 이러한 비교 결과를 <strong>차이 비교(Diff)</strong> 라고 한다.</p>
<p>반면 <strong>바이너리 파일(Binary File)</strong> 은 텍스트 파일처럼 줄 단위로 비교하기 어렵다. 바이너리 파일은 실행 파일, 이미지, 영상, 압축 파일, 오디오 파일처럼 사람이 직접 읽는 문자 구조가 아니라 연속된 바이트(Byte) 데이터로 구성된다. 텍스트 파일은 줄바꿈 문자(Newline Character)인 <code>\n</code> 또는 <code>\r\n</code>을 기준으로 줄을 나눌 수 있지만, 바이너리 파일에는 이런 줄 구조가 명확하지 않다.</p>
<p>따라서 바이너리 파일은 텍스트 소스 코드처럼 “몇 번째 줄이 바뀌었다”라고 비교하기 어렵다. 물론 Git 같은 도구도 바이너리 파일 자체를 저장하고 관리할 수는 있지만, 텍스트 파일처럼 세부 변경 내용을 보기 좋게 비교하는 데에는 한계가 있다. 예를 들어 이미지 파일이 수정되었다면 Git은 “파일이 변경되었다”는 사실은 알 수 있지만, 일반적인 텍스트 코드처럼 “어떤 줄이 추가되었다”라고 보여주기는 어렵다.</p>
<p>결국 버전 관리 시스템은 소프트웨어 개발에서 변경 이력을 체계적으로 관리하기 위한 도구이다. 과거에는 파일 안에 버전 정보를 직접 적는 방식도 사용되었지만, 현대 개발 환경에서는 Git과 같은 전문적인 버전 관리 도구를 사용하는 것이 표준에 가깝다. 특히 여러 사람이 협업하거나 프로젝트가 장기적으로 유지되는 경우, 버전 관리는 선택이 아니라 필수적인 개발 과정이라고 할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/1e03388b-c7bd-4842-8fd9-7655f3c8d2b5.png" alt="" style="display:block;margin:0 auto" />

<h3>버전 관리 유형: 분산 관리와 중앙 관리 (Types of Version Control: Distributed &amp; Centralized)</h3>
<p><code>버전 관리 시스템(Version Control System, VCS)</code> 은 프로젝트의 변경 이력을 관리하는 방식에 따라 크게 분산 버전 관리(Distributed Version Control) 와 중앙 집중식 버전 관리(Centralized Version Control) 로 나눌 수 있다. 이 구분은 우리가 일상에서 접하는 여러 시스템과도 비슷하다. 예를 들어 정부나 조직 운영 방식에서도 권한과 정보가 여러 곳에 나뉘어 있는 분산 구조(Distributed Structure) 가 있고, 한 곳에 모여 있는 중앙집중 구조(Centralized Structure) 가 있다. 버전 관리도 이와 비슷하게, 변경 이력을 어디에 저장하고 어떻게 공유하느냐에 따라 방식이 달라진다.</p>
<ol>
<li><code>분산 버전 관리 (Distributed Version Control)</code></li>
</ol>
<p>분산 버전 관리(Distributed Version Control) 는 말 그대로 프로젝트의 저장소가 여러 곳에 나뉘어 존재하는 방식이다. 각 개발자는 단순히 파일 일부만 내려받는 것이 아니라, 프로젝트의 변경 이력까지 포함한 거의 완전한 저장소 사본을 자신의 컴퓨터에 보관한다.</p>
<p>즉, 개발자 A, B, C가 같은 프로젝트를 작업한다면 각자의 컴퓨터에는 단순한 코드 파일뿐만 아니라, 과거 변경 이력과 커밋 기록까지 포함된 저장소가 존재한다. 그래서 중앙 서버에 항상 접속하지 않아도 자신의 컴퓨터에서 변경 이력을 확인하거나, 새로운 변경 내용을 기록할 수 있다.</p>
<p>이 방식에서는 “원본 저장소가 어디인가?”라는 질문이 생길 수 있다. 분산 방식에서는 기술적으로 각 저장소가 독립적인 이력을 가지고 있기 때문에, 반드시 하나의 원본만 존재해야 하는 것은 아니다. 다만 실제 협업에서는 보통 팀이 기준이 되는 저장소를 정한다. 예를 들어 GitHub, GitLab, Bitbucket 같은 원격 저장소(Remote Repository)를 기준 저장소로 삼고, 개발자들은 자신의 로컬 저장소(Local Repository)에서 작업한 뒤 변경 내용을 주고받는다.</p>
<p>대표적인 분산 버전 관리 도구로는 <code>Git</code> 과 <code>Mercurial</code> 이 있다. 특히 <code>Git</code>은 오늘날 가장 널리 사용되는 버전 관리 도구 중 하나이며, 리눅스 커널(Linux Kernel)을 비롯한 많은 오픈소스 프로젝트와 실무 개발 환경에서 사용된다.</p>
<p>분산 버전 관리의 장점은 중앙 서버에 문제가 생겨도 각 개발자의 로컬 저장소에 이력이 남아 있다는 점이다. 인터넷 연결이 없거나 중앙 서버에 접속할 수 없는 상황에서도 커밋(Commit), 로그 확인(Log), 브랜치 생성(Branch) 같은 작업을 할 수 있다. 또한 여러 저장소끼리 변경 내용을 주고받을 수 있기 때문에 유연한 협업 구조를 만들 수 있다.</p>
<p>하지만 분산 방식도 단점이 있다. 저장소 구조와 브랜치, 병합(Merge), 충돌 해결(Conflict Resolution) 같은 개념을 이해해야 하므로 처음 배우는 입장에서는 복잡하게 느껴질 수 있다. 또한 팀의 기준 저장소를 명확히 정하지 않으면 어떤 버전이 공식 버전인지 혼란이 생길 수 있다.</p>
<ol>
<li><code>중앙 집중식 버전 관리 (Centralized Version Control)</code></li>
</ol>
<p>중앙 집중식 버전 관리(Centralized Version Control) 는 프로젝트의 저장소와 변경 이력이 중앙 서버 한 곳에 모여 있는 방식이다. 개발자들은 중앙 저장소(Central Repository)에 접속하여 파일을 내려받고, 수정한 뒤 다시 중앙 저장소에 반영한다.</p>
<p>이 방식에서는 원본이 비교적 명확하다. 프로젝트의 공식 이력과 파일은 중앙 서버에 있고, 개발자들은 그 중앙 서버를 기준으로 작업한다. 예를 들어 개발자 A, B, C, D가 있든, 천 명이나 만 명의 개발자가 있든 모두 같은 중앙 저장소를 바라보게 된다.</p>
<p>대표적인 중앙 집중식 버전 관리 도구로는 <code>SVN(Subversion)</code> 과 <code>Perforce</code> 가 있다. SVN은 과거부터 많이 사용된 중앙 집중식 버전 관리 시스템이고, Perforce는 대규모 프로젝트나 게임 개발처럼 대용량 파일과 엄격한 파일 잠금(File Locking)이 필요한 환경에서 사용되는 경우가 많다.</p>
<p>중앙 집중식 버전 관리의 장점은 구조가 비교적 단순하다는 점이다. 원본 저장소가 중앙에 있으므로 어떤 저장소가 기준인지 명확하고, 권한 관리나 접근 제어를 중앙에서 통제하기 쉽다. 회사나 조직 입장에서는 누가 어떤 파일에 접근할 수 있는지, 어떤 변경을 반영할 수 있는지 관리하기 편리하다.</p>
<p>하지만 단점도 분명하다. 중앙 서버나 스토리지에 접근할 수 없으면 개발자들이 주요 작업을 진행하기 어렵다. 특히 커밋이나 최신 이력 확인처럼 중앙 저장소와 연결이 필요한 작업이 제한될 수 있다. 또한 중앙 저장소에 장애가 발생하면 전체 팀의 작업 흐름이 영향을 받을 수 있다.</p>
<p>중앙 집중식 버전 관리에서도 개발자 컴퓨터에 파일이 잠시 저장될 수는 있다. 하지만 이것은 보통 <code>캐싱(Caching)</code> 이나 <code>작업 사본(Working Copy)</code>에 가깝다. 프로젝트의 공식 변경 이력과 원본 저장소는 중앙 서버에 보관된다. 이 점이 분산 버전 관리와의 큰 차이이다.</p>
<h3>공유 디스크와 버전 관리 시스템의 차이 (Shared Disk vs Version Control System)</h3>
<p>중앙 집중식 버전 관리를 보면 NFS(Network File System) 같은 공유 디스크를 사용하는 것과 비슷해 보일 수 있다. 예를 들어 하나의 서버에 있는 디렉토리를 개발자 A와 B가 함께 사용한다면, 모두 같은 파일을 보는 구조이므로 중앙 저장소와 비슷하게 느껴질 수 있다.</p>
<p>하지만 공유 디스크와 버전 관리 시스템은 목적과 기능이 다르다. 공유 디스크는 기본적으로 파일을 여러 사람이 함께 접근할 수 있게 해주는 저장 공간이다. 모든 개발자가 같은 디렉토리의 파일을 열고 수정할 수 있다. 그러나 이것만으로는 변경 이력을 체계적으로 관리하기 어렵다.</p>
<p>반면 버전 관리 시스템(Version Control System) 은 단순히 파일을 공유하는 것을 넘어, 누가 언제 어떤 내용을 수정했는지 기록한다. 이전 버전과 현재 버전을 비교할 수 있고, 특정 시점으로 되돌아갈 수도 있으며, 여러 사람이 동시에 작업한 내용을 병합하거나 충돌을 해결할 수도 있다. 또한 커밋 메시지(Commit Message)를 통해 변경 이유를 남길 수 있고, 브랜치(Branch)를 사용해 기능 개발과 안정 버전을 분리할 수도 있다.</p>
<p>따라서 공유 디스크는 “같은 파일을 함께 쓰는 공간”에 가깝고, 버전 관리 시스템은 “변경 이력을 추적하고 협업을 체계적으로 관리하는 도구”에 가깝다. 겉으로 보기에는 둘 다 중앙 서버에 파일이 있다는 점에서 비슷해 보일 수 있지만, 실제 개발 관리 측면에서는 버전 관리 시스템이 훨씬 엄격하고 편리한 기능을 제공한다.</p>
<p>결국 분산 방식과 중앙 집중 방식은 어느 한쪽이 항상 더 좋다고 말하기 어렵다. 분산 버전 관리(Distributed Version Control) 는 각 개발자가 완전한 이력을 가지고 독립적으로 작업할 수 있다는 장점이 있고, 중앙 집중식 버전 관리(Centralized Version Control) 는 기준 저장소가 명확하고 중앙 통제가 쉽다는 장점이 있다. 프로젝트의 규모, 협업 방식, 보안 요구사항, 파일의 종류에 따라 적절한 방식을 선택하는 것이 중요하다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/78fde23e-e20d-4c51-b9e6-e1f3feca0a65.png" alt="" style="display:block;margin:0 auto" />

<h3>버전 관리의 특징 (Features of Version Control)</h3>
<p><code>버전 관리(Version Control)</code> 는 단순히 파일을 공유하는 것에서 끝나지 않는다. 앞에서 NFS(Network File System) 같은 공유 디스크와 비교했을 때, “공유한다”는 관점에서는 비슷해 보일 수 있다. 하지만 버전 관리 시스템은 단순 공유보다 훨씬 더 엄격하고 체계적인 기준과 기능을 제공한다. 핵심은 파일을 저장하는 것뿐만 아니라, 파일의 변경 이력을 관리하고, 이전 상태로 되돌리고, 여러 사람이 함께 작업할 수 있게 해준다는 점이다.</p>
<p>초기에는 별도의 <code>버전 관리 도구(Version Control Tool)</code> 없이 매우 원시적인 방식으로 파일을 관리하기도 했다. 예를 들어 문서 파일이나 코드 파일을 수정할 때 기존 파일과 같은 이름으로 저장하면 이전 내용은 사라진다. 어제 작성한 내용과 오늘 수정한 내용의 차이를 알기 어렵고, 실수로 덮어쓰면 이전 상태로 돌아가기도 어렵다.</p>
<p>그래서 가장 단순한 방법으로 파일 이름을 바꾸어 저장하는 방식이 사용되었다. 예를 들어 원본 파일이 <code>일기장.hwp</code>라면, 수정본을 <code>일기장1.hwp, 일기장2.hwp</code>처럼 저장하는 식이다. 코드 파일도 마찬가지로 <code>main.c, main_old.c, main_v2.c</code>처럼 이름을 바꾸어 여러 개의 파일로 보관할 수 있다. 이렇게 하면 특정 기능이 이전 버전에서는 잘 작동했는데 현재 버전에서는 작동하지 않을 때, 예전 파일을 다시 열어보거나 복사해서 되돌릴 수 있다.</p>
<p>이 방식은 혼자 작업하거나 아주 작은 규모의 프로젝트에서는 어느 정도 사용할 수 있다. 파일 수가 적고 변경 내용이 단순하다면, 파일 이름만으로도 대략적인 버전 구분이 가능하기 때문이다. 하지만 이 방식은 곧 한계에 부딪힌다. 파일이 많아지고, 수정 횟수가 늘어나고, 여러 사람이 동시에 작업하면 어떤 파일이 최신인지, 어떤 파일이 안정적인 버전인지, 누가 무엇을 바꾸었는지 알기 어려워진다.</p>
<p>현업에서도 처음에는 사람 이름이나 아이디를 파일명에 붙이는 방식으로 구분할 수 있다고 생각할 수 있다. 예를 들어 <code>main_heesu.c, main_john.c, report_final_v3.hwp</code>처럼 저장하는 방식이다. 하지만 이런 방식은 체계적인 버전 관리라고 보기 어렵다. 파일 이름은 많아지지만, 변경 이유나 정확한 수정 내용은 알기 어렵고, 여러 사람이 수정한 내용을 하나로 합치는 것도 어렵다.</p>
<p>문서 프로그램의 경우에는 자체적으로 수정 이력을 저장하는 기능이 있기도 하다. 예를 들어 한글 문서나 워드 문서에서는 변경 내용 추적, 자동 저장, 이전 버전 복원 같은 기능을 제공할 수 있다. 하지만 이런 방식은 문서 내부에 수정 이력이 함께 저장되기 때문에 파일 크기가 커질 수 있다. 실제 문서 내용은 짧아도, 수정 이력과 부가 정보가 많이 쌓이면 파일 크기가 커지는 문제가 생길 수 있다. 따라서 소프트웨어 개발처럼 많은 파일과 긴 변경 이력을 다루는 환경에서는 단순 문서 이력 기능만으로는 충분하지 않다.</p>
<p>결국 파일 이름을 바꾸어 보관하는 방식은 기본적인 버전 구분에는 사용할 수 있지만, 실제 개발 환경에서는 한계가 크다. 혼자 작업하거나 몇 명이 간단히 작업할 때는 가능하지만, 인원이 늘어나고 코드의 양이 많아지면 관리가 어려워진다. 이때 필요한 것이 전문적인 <code>버전 관리 시스템(Version Control System, VCS)</code> 이다.</p>
<p>버전 관리 시스템이 제공하는 중요한 특징은 크게 <code>되돌림(Rollback/Revert), 협업(Collaboration), 추적(Tracking), 파생(Branching)</code> 으로 정리할 수 있다.</p>
<p>첫 번째 특징은 <code>되돌림(Rollback/Revert)</code> 이다. 버전 관리 시스템을 사용하면 언제든지 이전 버전의 상태로 돌아갈 수 있다. 새로운 기능을 추가하다가 문제가 생기거나, 수정한 코드가 예상과 다르게 동작할 경우 과거의 안정적인 상태를 확인하거나 복원할 수 있다. 이 기능은 실수를 줄이고, 개발자가 더 안전하게 코드를 수정할 수 있도록 도와준다.</p>
<p>두 번째 특징은 <code>협업(Collaboration)</code> 이다. 여러 개발자가 같은 프로젝트를 동시에 작업할 때, 단순히 같은 폴더를 공유하는 방식만으로는 충돌을 피하기 어렵다. 버전 관리 시스템은 각 개발자의 변경 내용을 기록하고, 서로 다른 작업 내용을 비교하고 병합할 수 있도록 도와준다. 따라서 다수의 개발자가 같은 프로젝트를 작업하더라도 변경 내용을 체계적으로 공유할 수 있다.</p>
<p>세 번째 특징은 <code>추적(Tracking)</code> 이다. 버전 관리 시스템은 언제, 누가, 무엇을, 왜 변경했는지 기록한다. 단순히 파일의 최신 상태만 저장하는 것이 아니라, 변경 이력 전체를 남긴다. 이를 통해 문제가 발생했을 때 어떤 변경이 원인이 되었는지 찾아볼 수 있고, 특정 기능이 언제 추가되었는지도 확인할 수 있다.</p>
<p>네 번째 특징은 <code>파생(Branching)</code> 이다. 버전 관리 시스템에서는 기존 코드에서 별도의 흐름을 만들어 새로운 버전을 개발할 수 있다. 이를 브랜치(Branch) 라고 한다. 예를 들어 안정적인 버전은 그대로 유지하면서, 새로운 기능을 실험하는 별도의 개발 흐름을 만들 수 있다. 이후 기능이 안정되면 다시 원래 흐름에 합칠 수 있는데, 이를 병합(Merge) 이라고 한다.</p>
<p>이러한 기능들은 단순히 특정 버전 관리 프로그램만의 기능이라기보다, 소프트웨어 개발에서 자연스럽게 필요해진 기능들이다. 파일 이름을 바꾸어 저장하는 방식도 작은 규모에서는 일종의 버전 관리처럼 사용할 수 있지만, 되돌림, 협업, 추적, 파생을 체계적으로 제공하지는 못한다. 그래서 현대 소프트웨어 개발에서는 Git, SVN(Subversion), Perforce 같은 버전 관리 도구를 사용하여 변경 이력을 관리한다.</p>
<p>결국 버전 관리의 핵심은 “파일을 여러 개 저장하는 것”이 아니라 “변경의 흐름을 관리하는 것”이다. 어떤 상태로 되돌아갈 수 있어야 하고, 누가 무엇을 바꾸었는지 알 수 있어야 하며, 여러 사람이 동시에 작업할 수 있어야 한다. 또한 필요할 때는 기존 버전에서 새로운 개발 흐름을 만들어갈 수 있어야 한다. 이러한 이유로 버전 관리는 소프트웨어 개발의 핵심 도구가 되었다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/9a41b845-2492-469c-a5d7-0ca3bea35937.png" alt="" style="display:block;margin:0 auto" />

<h3>버전 관리를 위한 처리 단계 (Version Control Workflow)</h3>
<p><strong>버전 관리(Version Control)</strong> 는 단순히 파일을 저장하는 것이 아니라, 원본을 보관하고, 복사본에서 작업하고, 수정 내용을 기록하며, 필요할 때 새로운 흐름으로 분기하고 다시 통합하는 과정을 포함한다. 이러한 과정을 이해하면 Git 같은 버전 관리 도구가 왜 필요한지 더 쉽게 이해할 수 있다.</p>
<p>가장 먼저 생각할 수 있는 것은 <code>저장고(Repository)</code> 이다. 저장고는 파일과 변경 이력이 보관되는 공간이다. 일반적으로 디스크(Disk)나 서버 저장소(Storage)에 위치하며, 프로젝트의 소스 코드, 문서, 설정 파일, 그리고 그동안의 변경 기록이 함께 저장된다. 단순히 현재 파일만 보관하는 것이 아니라, 누가 언제 어떤 내용을 변경했는지에 대한 이력도 함께 관리한다는 점이 중요하다. 그래서 저장고는 단순한 폴더라기보다, 프로젝트의 기록을 담고 있는 데이터베이스(Database)에 가깝다고 볼 수 있다.</p>
<p>다음은 <code>복사본(Working Copy / Working Tree)</code> 이다. 사용자는 보통 저장고의 원본 파일을 직접 수정하지 않고, 자신의 작업 공간에 복사본을 만들어 작업한다. 원본을 직접 수정하면 기존 상태가 훼손될 수 있고, 어떤 내용이 어떻게 바뀌었는지 관리하기 어렵다. 그래서 원본은 저장고에 보관해두고, 사용자는 복사본에서 파일을 수정한다. 이렇게 하면 원본과 현재 작업 중인 파일을 비교하여 어떤 차이가 생겼는지 확인할 수 있다.</p>
<p>예를 들어 저장고에 있는 파일을 기준으로 개발자가 자신의 컴퓨터에 복사본을 가져온 뒤 코드를 수정한다고 생각할 수 있다. 이때 수정한 내용은 아직 저장고에 반영된 것이 아니다. 단지 작업자의 복사본에서만 바뀐 상태이다. 이 단계에서는 변경 내용을 확인하고, 테스트하고, 필요한 경우 다시 수정할 수 있다.</p>
<p>그다음 단계는 <code>수정본(Commit / Revision)</code> 이다. 사용자가 작업을 완료하고 변경 내용을 저장고에 기록하면, 그 내용은 하나의 수정본이 된다. Git에서는 이 과정을 보통 <code>커밋(Commit)</code> 이라고 부른다. 커밋은 단순히 파일을 저장하는 것이 아니라, 특정 시점의 변경 내용을 하나의 기록으로 남기는 것이다. 이 기록에는 변경된 파일, 변경 내용, 작성자, 시간, 설명 메시지 등이 포함될 수 있다.</p>
<p>수정본이 저장고에 기록되면, 그 상태는 이후 작업의 새로운 기준이 될 수 있다. 즉, 이전 원본에서 출발해 새로운 변경 내용이 반영된 또 하나의 기준 상태가 만들어지는 것이다. 그래서 버전 관리는 단순히 “파일을 덮어쓰기” 하는 방식이 아니라, 변경된 상태들을 순서대로 기록하면서 프로젝트의 흐름을 만들어가는 방식이라고 할 수 있다.</p>
<p>이후에는 <code>분기(Branch)</code> 라는 개념이 등장한다. 분기는 기존의 코드 흐름에서 갈라져 나온 새로운 작업 흐름이다. 새로운 기능을 실험하거나, 기존 버전은 안정적으로 유지하면서 다른 방향의 개발을 진행하고 싶을 때 사용한다. 예를 들어 현재 안정 버전은 그대로 두고, 새로운 기능을 추가하는 별도의 브랜치를 만들 수 있다. 이 브랜치에서는 기존 원본을 기준으로 새로운 개발을 이어가지만, 원래 흐름에는 바로 영향을 주지 않는다.</p>
<p>분기는 “새로운 원본”처럼 생각할 수도 있다. 기존 저장고의 특정 시점을 기준으로 새로운 작업 흐름을 만들고, 그 안에서 독립적으로 수정과 테스트를 진행하기 때문이다. 만약 새로운 기능이 실패하면 해당 분기를 버릴 수도 있고, 성공하면 원래 흐름에 합칠 수도 있다. 이처럼 분기는 새로운 기능을 안전하게 점검하거나, 여러 버전을 동시에 관리하기 위한 중요한 기능이다.</p>
<p>마지막으로 <code>통합(Merge / Integration)</code> 이 있다. 통합은 수정본이나 분기된 작업 내용을 하나로 합치는 과정이다. 여러 사람이 각자 작업한 내용을 하나의 코드 흐름으로 합치거나, 새로운 기능 브랜치에서 작업한 내용을 안정 브랜치에 반영할 때 통합이 이루어진다.</p>
<p>예를 들어 새로운 기능을 개발하기 위해 별도의 브랜치를 만들었다고 하자. 그 기능이 충분히 테스트되고 안정적이라고 판단되면, 해당 브랜치의 변경 내용을 기존 메인 브랜치(Main Branch)에 합친다. 이 과정이 병합(Merge)이다. 통합을 통해 여러 갈래로 나뉘어 있던 변경 내용을 하나의 프로젝트 상태로 정리할 수 있다.</p>
<p>하지만 통합 과정이 항상 자동으로 깔끔하게 끝나는 것은 아니다. 서로 다른 작업자가 같은 파일의 같은 부분을 수정했다면 <code>충돌(Conflict)</code> 이 발생할 수 있다. 이 경우 버전 관리 도구는 충돌이 발생한 위치를 알려주고, 개발자가 어떤 내용을 남길지 직접 결정해야 한다. 따라서 통합은 단순히 “파일을 합치는 일”이 아니라, 여러 변경 흐름을 비교하고 조정하는 과정이라고 볼 수 있다.</p>
<p>정리하면, 버전 관리를 위한 기본 처리 단계는 <code>저장고(Repository), 복사본(Working Copy), 수정본(Commit / Revision), 분기(Branch), 통합(Merge / Integration)</code> 으로 이해할 수 있다. 저장고는 원본과 이력을 보관하는 곳이고, 복사본은 사용자가 실제로 작업하는 공간이다. 수정본은 변경 내용을 저장고에 기록한 결과이며, 분기는 새로운 작업 흐름을 만드는 과정이다. 통합은 나뉘어진 변경 내용을 다시 하나로 합치는 단계이다.</p>
<p>이러한 흐름 덕분에 버전 관리 시스템은 단순한 파일 저장소를 넘어서, 소프트웨어 개발의 변경 이력을 체계적으로 관리하고 협업을 가능하게 하는 핵심 도구가 된다.</p>
<p>📌</p>
<p><code>Repository</code> → 프로젝트 이력 전체가 저장되는 공간<br /><code>Working Tree</code> → 사용자가 실제 파일을 수정하는 공간<br /><code>Staging Area</code> → 커밋하기 전에 변경 내용을 잠시 올려두는 공간<br /><code>Commit</code> → 변경 내용을 하나의 기록으로 저장하는 것<br /><code>Branch</code> → 독립적인 작업 흐름<br /><code>Merge</code> → 브랜치의 변경 내용을 합치는 것</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/9e1f5d98-f6e7-47b9-a4f6-9e1579b28174.png" alt="" style="display:block;margin:0 auto" />

<h3>버전 관리 프로그램의 종류: SCCS (Types of Version Control Systems: SCCS)</h3>
<p>버전 관리 프로그램의 종류를 살펴볼 때 가장 초기 사례 중 하나로 <code>SCCS(Source Code Control System)</code> 를 들 수 있다. SCCS는 <strong>Bell Labs</strong> 에서 개발된 초기 버전 관리 시스템으로 알려져 있으며, 1970년대 초반에 등장했다. 당시 컴퓨터와 소프트웨어 개발 환경은 지금처럼 대중화되지 않았지만, 이미 소스 코드의 변경 이력을 관리해야 할 필요성은 존재했다.</p>
<p>SCCS는 1972년 무렵 <strong>Marc Rochkind</strong> 가 Bell Labs에서 개발을 시작한 것으로 알려져 있다. 초기에는 IBM System/370 환경에서 사용되었고, 이후 UNIX 환경에서도 사용되었다. 이는 버전 관리가 최근에 생긴 개념이 아니라, 컴퓨터와 소프트웨어 개발이 본격적으로 발전하던 시기부터 이미 중요한 문제였다는 것을 보여준다.</p>
<p>SCCS의 목적은 소스 코드와 같은 텍스트 파일의 변경 이력을 관리하는 것이었다. 여러 버전의 파일을 따로따로 계속 복사해두는 대신, 하나의 관리 체계 안에서 파일의 여러 버전을 보관하고, 필요할 때 과거 버전을 다시 꺼내 사용할 수 있게 해주었다. IBM 문서에서도 SCCS는 하나의 파일에 여러 버전이 동시에 존재할 수 있도록 하며, 변경 사항을 추적할 수 있게 해주는 시스템으로 설명된다.</p>
<p>SCCS의 특징 중 하나는 각 소스 파일 안에 <strong>SCCS ID 문자열(SCCS ID String)</strong> 을 넣는 관습이다. C 언어 코드에서는 보통 <code>sccsid</code>라는 문자열 변수를 만들어 그 안에 파일 이름, 버전, 날짜, 작성자 또는 설명 같은 정보를 넣었다. 예를 들면 다음과 같은 형태이다.</p>
<pre><code class="language-c">static char sccsid[] = "@(#)example.c 1.0 2026-05-24";
</code></pre>
<p>이 방식은 단순한 주석(Comment)과는 조금 다르다. 주석은 컴파일 과정에서 보통 제거되기 때문에 실행 파일이나 오브젝트 파일(Object File)에 남지 않는다. 반면 <code>sccsid</code>처럼 문자열 변수로 작성하면, 컴파일된 결과물 안에도 해당 문자열이 포함될 수 있다. 그래서 나중에 바이너리 파일(Binary File)이나 오브젝트 파일 안에서 특정 문자열을 검색하여, 어떤 소스 파일의 어떤 버전이 사용되었는지 확인할 수 있다.</p>
<p>정리하면, <code>SCCS(Source Code Control System)</code> 는 초기 버전 관리 시스템 중 하나로, 소스 코드의 변경 이력을 관리하고 과거 버전을 다시 꺼내 사용할 수 있도록 도와주었다. 또한 <code>sccsid</code> 문자열을 통해 컴파일된 프로그램 안에서도 소스 파일의 버전 정보를 확인할 수 있게 했다. 이 방식은 오늘날의 Git과는 다르지만, 소프트웨어 변경 이력을 체계적으로 관리하려는 초기 시도였다는 점에서 중요한 의미를 가진다.</p>
<p>추가적으로 SCCS는 현대적인 Git과 비교하면 오래된 방식이지만, 버전 관리의 기본 문제를 이미 다루고 있었다는 점에서 중요하다. 즉, “어떤 파일이 언제 어떻게 바뀌었는가”, “과거 버전을 다시 가져올 수 있는가”, “컴파일된 결과물이 어떤 소스에서 만들어졌는가”라는 문제는 1970년대에도 이미 중요했다.</p>
<p>또한 <code>sccsid</code> 방식은 오늘날에도 개념적으로는 이해할 가치가 있다. 현대 소프트웨어에서도 실행 파일 안에 빌드 번호(Build Number), 커밋 해시(Commit Hash), 빌드 날짜(Build Date)를 포함시키는 경우가 있기 때문이다. 이는 문제가 발생했을 때 해당 프로그램이 정확히 어떤 소스 코드에서 만들어졌는지 추적하기 위한 목적이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/4ae3aa7d-a37e-4051-8624-1e1f7e96972a.png" alt="" style="display:block;margin:0 auto" />

<h3>버전 관리 프로그램의 종류: RCS (Types of Version Control Systems: RCS)</h3>
<p><code>RCS(Revision Control System)</code> 는 SCCS 이후에 등장한 초기 버전 관리 시스템 중 하나이다. RCS는 주로 텍스트 형식의 파일, 특히 소스 코드(Source Code), 문서(Documentation), 테스트 데이터(Test Data)처럼 자주 수정되는 파일의 변경 이력을 추적하고 관리하기 위해 사용되었다. GNU RCS 문서에서도 RCS는 여러 개의 파일 리비전(Revision)을 저장, 검색, 기록, 식별, 병합할 수 있는 시스템으로 설명된다.</p>
<p>RCS의 중요한 특징은 수정된 전체 파일을 매번 모두 저장하는 것이 아니라, 변경된 부분만 저장하는 <strong>델타 저장 방식(Delta Storage)</strong> 을 사용한다는 점이다. 여기서 <strong>델타(Delta)</strong> 는 이전 버전과 비교했을 때 달라진 부분, 즉 차이점(Difference)을 의미한다. 예를 들어 A라는 파일이 있고, 그다음 버전에서 몇 줄만 수정되었다면 파일 전체를 다시 저장하는 대신 “어떤 줄이 바뀌었는가”만 기록하는 방식이다. RCS 논문에서도 저장 공간을 절약하기 위해 연속된 리비전 사이의 차이, 즉 델타를 저장한다고 설명한다.</p>
<p>RCS의 또 다른 특징은 기본적으로 한 파일에 대해 한 사용자만 수정하도록 하는 <strong>잠금 방식(Locking Mechanism)</strong> 이다. 여러 사용자가 같은 파일을 동시에 수정하면 서로의 변경 내용이 충돌할 수 있다. 그래서 RCS에서는 사용자가 파일을 수정하기 전에 해당 파일을 체크아웃(Check-out)하고 잠금(Lock)을 걸어, 다른 사용자가 동시에 같은 파일을 수정하지 못하도록 관리했다. 이는 “동시에 수정 불가”라는 방식으로 이해할 수 있다.</p>
<p>이 방식은 당시의 개발 환경에서는 매우 자연스러운 선택이었다. 여러 사람이 같은 파일을 동시에 고치면 어느 변경을 살릴 것인지, 어떤 내용이 최신인지, 서로의 작업이 어떻게 섞여야 하는지 판단하기 어렵다. 특히 현대의 Git처럼 강력한 병합(Merge) 기능이 일반화되기 전에는, 충돌을 나중에 해결하기보다는 처음부터 동시에 수정하지 못하도록 막는 방식이 더 안전하게 느껴질 수 있었다.</p>
<p>물론 파일을 동시에 수정하는 것이 기술적으로 완전히 불가능한 것은 아니다. 현대의 버전 관리 시스템에서는 여러 사람이 같은 파일을 각자 수정한 뒤, 나중에 변경 내용을 비교하고 병합할 수 있다. 하지만 같은 부분을 서로 다르게 수정하면 <strong>충돌(Conflict)</strong> 이 발생한다. RCS의 잠금 방식은 이런 충돌 상황을 미리 줄이기 위해 “한 번에 한 사람만 수정”하도록 제한한 구조라고 볼 수 있다.</p>
<p>운영체제 관점에서 보면, 이것은 공유 자원(Shared Resource)을 여러 사용자가 동시에 접근할 때 발생할 수 있는 문제와도 연결된다. 하나의 파일을 여러 사용자가 동시에 수정하면 데이터 일관성(Data Consistency)이 깨질 수 있고, 마지막에 저장한 사람이 이전 사람의 작업을 덮어쓰는 문제가 생길 수 있다. 따라서 RCS의 잠금 방식은 공유 파일을 안전하게 관리하기 위한 초기 버전 관리 시스템의 접근법이라고 할 수 있다.</p>
<p>정리하면, <strong>RCS(Revision Control System)</strong> 는 텍스트 파일의 변경 이력을 관리하기 위한 초기 버전 관리 시스템이다. RCS는 전체 파일을 매번 저장하기보다 변경된 부분만 저장하는 <strong>델타 저장 방식(Delta Storage)</strong> 을 사용하여 저장 공간을 절약했다. 또한 하나의 파일에 대해 한 사용자만 수정할 수 있도록 하는 <strong>잠금 방식(Locking)</strong> 을 통해 협업 중 발생할 수 있는 충돌을 줄이려 했다. 이러한 특징은 이후 버전 관리 시스템이 발전하는 데 중요한 기반이 되었다.</p>
<p>RCS의 방식은 오늘날 Git과 다르게 느껴질 수 있다. Git은 보통 여러 사람이 각자의 브랜치(Branch)에서 자유롭게 수정한 뒤, 나중에 병합(Merge)하면서 충돌을 해결하는 방식에 가깝다.</p>
<p>반면 RCS는 파일 단위 잠금(File Locking)을 통해 애초에 동시에 수정하는 상황을 줄이려는 방식이었다. 그래서 RCS는 “충돌이 생기면 나중에 해결하자”보다는 “충돌이 생기지 않도록 먼저 잠그자”에 가까운 사고방식을 가진 버전 관리 시스템이라고 볼 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f92fe12c-ae28-4dc2-8f8a-c58e31dd813f.png" alt="" style="display:block;margin:0 auto" />

<h3>버전 관리 프로그램의 종류: CVS (Concurrent Versions System)</h3>
<p><code>CVS(Concurrent Versions System)</code> 는 RCS 이후에 널리 사용된 버전 관리 시스템 중 하나이다. 이름에 들어 있는 <strong>Concurrent</strong> 는 “동시에 진행되는”, “병행하는”이라는 의미를 가진다. 즉, CVS는 여러 사용자가 같은 프로젝트를 함께 작업할 수 있도록 만든 버전 관리 시스템이다.</p>
<p>CVS는 기본적으로 <strong>서버-클라이언트 방식(Server-Client Model)</strong> 으로 동작한다. 여기서 <strong>서버(Server)</strong> 는 프로젝트의 원본 저장소(Repository)를 보관하는 중앙 공간이고, <strong>클라이언트(Client)</strong> 는 개발자가 실제로 작업하는 개인 컴퓨터나 작업 환경을 의미한다. 개발자는 서버에 있는 프로젝트를 자신의 컴퓨터로 가져와 수정하고, 수정이 끝나면 다시 서버에 반영한다.</p>
<p>이 점에서 CVS는 이전의 RCS와 차이가 있다. RCS는 기본적으로 개별 파일 단위의 버전 관리에 가깝고, 하나의 파일을 한 사용자가 잠그고 수정하는 방식이 중심이었다. 반면 CVS는 여러 파일로 이루어진 프로젝트 전체를 관리하고, 여러 사용자가 동시에 작업하는 환경을 더 적극적으로 지원했다. 그래서 CVS는 초기의 중앙 집중식 협업 버전 관리 도구로 이해할 수 있다.</p>
<p>CVS에서도 서버에 저장된 프로젝트를 클라이언트가 가져오는 과정을 <strong>체크아웃(Check-out)</strong> 이라고 한다. 말 그대로 서버에 있는 파일들을 사용자의 작업 공간으로 “가져오는” 과정이다. 사용자는 체크아웃한 복사본(Working Copy)을 자신의 컴퓨터에서 수정한다. 이때 서버의 원본 저장소가 바로 바뀌는 것은 아니고, 클라이언트의 작업 복사본만 수정된다.</p>
<p>수정이 끝난 뒤에는 변경 내용을 서버에 반영해야 한다. 이 과정을 <strong>체크인(Check-in)</strong> 또는 CVS에서는 보통 <strong>커밋(Commit)</strong> 이라고 부른다. 사용자가 커밋하면, 클라이언트에서 수정한 내용이 중앙 서버의 저장소에 기록된다. 이때 서버에는 새로운 리비전(Revision)이 만들어지고, 변경 이력도 함께 남는다.</p>
<p>CVS는 내부적으로 RCS와 관련이 깊다. 초기 CVS는 파일별 이력 관리를 위해 RCS 파일 형식을 활용했다. 그래서 CVS도 텍스트 파일의 변경 이력을 추적하고, 어느 부분이 수정되었는지 관리할 수 있었다. 다만 RCS가 파일 하나를 중심으로 관리했다면, CVS는 여러 파일과 디렉토리로 이루어진 프로젝트를 중앙 저장소에서 함께 관리할 수 있도록 확장한 방식이라고 볼 수 있다.</p>
<p>강의에서 언급된 <strong>심볼릭 링크(Symbolic Link)</strong> 부분은 조심해서 이해할 필요가 있다. CVS는 일반 파일과 달리 심볼릭 링크를 완전한 파일 시스템 객체로 깔끔하게 버전 관리하는 데 한계가 있었다. 그래서 심볼릭 링크가 프로젝트 안에 있을 때, 그것을 그대로 보존하거나 추적하는 방식이 현대적인 Git과는 다르게 제한적일 수 있다. 즉, CVS는 텍스트 기반 소스 코드 관리에는 적합했지만, 파일 시스템의 다양한 속성이나 특수 파일을 완벽하게 다루는 도구는 아니었다.</p>
<p>CVS에서 중요한 점은 여러 사용자가 동시에 작업할 수 있다는 것이다. 예를 들어 개발자 A, B, C가 같은 프로젝트를 체크아웃해서 각자 수정할 수 있다. RCS처럼 한 파일을 반드시 한 사람만 잠그고 수정해야 하는 구조와 비교하면, CVS는 더 유연한 협업을 가능하게 했다.</p>
<p>하지만 여러 사람이 동시에 작업할 수 있다는 것은 충돌 가능성도 생긴다는 뜻이다. 예를 들어 세 명의 개발자가 같은 파일을 가져가서 각자 수정했다고 하자. 개발자 A가 먼저 서버에 커밋하면, 서버의 최신 버전은 A의 수정 내용이 반영된 상태가 된다. 이후 개발자 B가 자신의 수정본을 그대로 커밋하려고 하면 문제가 생길 수 있다. 왜냐하면 B가 작업을 시작했을 때의 파일 버전과 서버의 현재 최신 버전이 달라졌기 때문이다.</p>
<p>이때 CVS는 단순히 B의 파일로 서버의 최신 파일을 덮어쓰는 방식으로 처리하지 않는다. 서버는 보통 사용자의 작업 복사본이 최신 버전을 기준으로 하고 있는지 확인한다. 만약 서버에 이미 다른 사용자의 변경 내용이 반영되어 있다면, B는 먼저 <strong>업데이트(Update)</strong> 를 통해 서버의 최신 변경 내용을 자신의 작업 복사본에 가져와야 한다. 그 후 자신의 수정 내용과 서버의 최신 수정 내용을 합친 뒤 다시 커밋해야 한다.</p>
<p>만약 A, B, C가 서로 다른 부분을 수정했다면 CVS가 자동으로 병합(Merge)할 수 있는 경우도 있다. 예를 들어 A는 함수 <code>a</code>를 수정하고, B는 함수 <code>b</code>를 수정하고, C는 함수 <code>c</code>를 수정했다면 서로 수정 위치가 다르기 때문에 비교적 쉽게 합쳐질 수 있다. 하지만 세 사람이 같은 함수의 같은 부분을 수정했다면 <strong>충돌(Conflict)</strong> 이 발생할 수 있다. 이 경우 CVS는 충돌이 발생한 부분을 표시하고, 개발자가 직접 어떤 내용을 남길지 결정해야 한다.</p>
<p>따라서 “서버는 최신 버전의 수정본만 인정한다”는 표현은 “가장 마지막에 커밋한 사람의 내용만 무조건 남는다”는 뜻으로 이해하면 안 된다. 더 정확하게는, CVS는 서버의 최신 리비전을 기준으로 변경 내용을 반영하려고 하며, 사용자의 작업 복사본이 오래된 상태라면 먼저 최신 변경 내용을 가져와 병합하도록 요구한다고 이해하는 것이 좋다.</p>
<p>정리하면, <strong>CVS(Concurrent Versions System)</strong> 는 중앙 서버에 저장소를 두고, 여러 클라이언트가 프로젝트를 체크아웃하여 수정한 뒤 다시 커밋하는 방식의 버전 관리 시스템이다. RCS의 파일 이력 관리 개념을 확장하여 여러 사람이 하나의 프로젝트를 함께 개발할 수 있도록 만든 도구이며, 충돌을 완전히 없애지는 못하지만 업데이트, 병합, 충돌 표시 기능을 통해 협업을 가능하게 했다.</p>
<p>CVS는 오늘날 Git만큼 많이 사용되지는 않지만, RCS에서 SVN, Git으로 이어지는 버전 관리 시스템 발전 과정에서 중요한 위치를 가진 도구이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/4c171ebf-52f2-4f33-8b7c-7f49983b3258.png" alt="" style="display:block;margin:0 auto" />

<h3>버전 관리 프로그램의 종류: Apache Subversion (SVN)</h3>
<p><code>Apache Subversion(SVN)</code> 은 CVS 이후에 등장한 대표적인 <code>중앙 집중식 버전 관리 시스템(Centralized Version Control System)</code> 이다. Subversion은 보통 명령어 이름을 따서 <strong>SVN</strong> 이라고도 부른다. Apache Subversion 공식 사이트에서도 SVN을 오픈소스 버전 관리 시스템이며, 기업 환경에서도 사용할 수 있는 중앙 집중식 버전 관리 도구로 설명하고 있다.</p>
<p>SVN은 <strong>Apache License</strong> 에 따라 배포되는 오픈소스 소프트웨어이다. 여기서 중요한 점은 <strong>오픈소스(Open Source)</strong> 라는 말이 단순히 “코드를 볼 수 있다”는 뜻만은 아니라는 것이다. 오픈소스 소프트웨어는 라이선스 조건에 따라 사용, 수정, 배포가 허용된다. 다만 어떤 조건으로 사용할 수 있는지는 라이선스마다 다르기 때문에, 실제 프로젝트나 상업적 제품에 사용할 때는 반드시 라이선스 내용을 확인해야 한다.</p>
<p>특히 소프트웨어를 이용해 제품을 만들거나 수익을 얻고자 한다면 <strong>라이선스(License)</strong> 를 이해하는 것이 매우 중요하다. 오픈소스라고 해서 모든 조건이 같은 것은 아니다. 어떤 라이선스는 비교적 자유롭게 상업적 사용을 허용하고, 어떤 라이선스는 수정한 소스 코드 공개나 저작권 표시 유지 같은 조건을 요구한다. 따라서 “오픈소스니까 아무렇게나 사용해도 된다”라고 생각하면 위험하다.</p>
<p>다만 <strong>Apache License 2.0</strong> 은 비교적 허용적인 <strong>퍼미시브 라이선스(Permissive License)</strong> 에 속한다. Apache License 2.0은 사용, 복제, 수정, 배포, 2차 저작물 작성 등을 허용하며, 특허 라이선스 조항도 포함한다. Apache License의 원문에서도 저작권 라이선스가 무상, 전 세계적, 비독점적, 영구적으로 부여된다고 설명한다.</p>
<p>따라서 “오픈소스는 누구나 볼 수 있지만, 돈을 벌려면 반드시 원저작자에게 허가를 받거나 돈을 지불해야 한다”라고 일반화하는 것은 정확하지 않다. 라이선스에 따라 다르다. Apache License 2.0처럼 상업적 사용을 허용하는 라이선스도 있고, GPL처럼 배포 시 소스 코드 공개 의무가 강하게 붙는 라이선스도 있다. 핵심은 오픈소스마다 조건이 다르므로, 사용 전에 해당 라이선스를 확인해야 한다는 점이다.</p>
<p>SVN은 기능적으로 <strong>CVS(Concurrent Versions System)</strong> 의 한계를 개선하고 강화하기 위해 만들어졌다. CVS도 중앙 서버에 저장소를 두고 여러 사용자가 작업하는 방식이었지만, 파일 이름 변경, 디렉토리 이동, 원자적 커밋(Atomic Commit) 등에서 한계가 있었다. SVN은 이러한 문제를 개선하기 위해 설계되었다. Subversion은 2000년에 CollabNet에서 시작되었고, CVS와 비슷하게 동작하면서 CVS의 버그와 부족한 기능을 보완하는 것을 목표로 만들어졌다.</p>
<p>SVN의 중요한 특징 중 하나는 <strong>파일 이름 변경(Rename)</strong> 이나 <strong>파일 이동(Move)</strong>, 그리고 디렉토리 구조의 변경을 버전 관리 이력 안에서 다룰 수 있다는 점이다. CVS는 주로 개별 파일 단위의 이력 관리에 초점이 있었기 때문에, 파일 이름을 바꾸거나 위치를 옮겼을 때 이력을 자연스럽게 보존하는 데 어려움이 있었다. 반면 SVN은 파일뿐만 아니라 <strong>디렉토리(Directory)</strong> 자체도 버전 관리 대상으로 다룬다. 그래서 파일이나 디렉토리를 옮기거나 복사할 때도 변경 이력을 추적하기가 더 쉬워졌다.</p>
<p>SVN은 오늘날 Git만큼 주류로 사용되지는 않지만, 여전히 일부 조직이나 오래된 프로젝트에서는 유지보수 목적으로 사용될 수 있다. 특히 오래전에 개발된 시스템은 전체 버전 관리 도구를 Git으로 옮기는 비용이 크거나, 기존 업무 흐름이 SVN에 맞춰져 있는 경우가 있다. 이런 경우에는 치명적인 문제가 없다면 기존 틀을 유지하면서 SVN을 계속 사용하는 경우도 있다.</p>
<p>따라서 SVN은 “지금 새 프로젝트에서 반드시 배워야 하는 최신 도구”라기보다는, 버전 관리 시스템의 발전 과정에서 CVS의 한계를 보완한 중요한 도구로 이해하면 좋다. RCS, CVS, SVN을 거쳐 Git 같은 분산 버전 관리 시스템이 널리 사용되기 시작했으며, 이 흐름을 이해하면 오늘날 Git이 왜 강력한 도구로 자리 잡았는지도 더 잘 이해할 수 있을 것이다.</p>
<p>📌SVN과 CVS의 차이를 간단히 정리하면 다음과 같다.</p>
<p><strong>CVS</strong> 는 파일 단위 관리 성격이 강하고, 디렉토리 이동이나 파일 이름 변경 이력 관리가 불편했다.<br /><strong>SVN</strong> 은 디렉토리까지 버전 관리 대상으로 다루며, 여러 파일 변경을 하나의 리비전으로 묶어 관리할 수 있다.<br /><strong>SVN</strong> 은 원자적 커밋(Atomic Commit)을 지원하여 여러 파일 변경이 모두 반영되거나 모두 실패하도록 처리할 수 있다.<br /><strong>Git</strong> 은 SVN과 달리 분산 버전 관리 시스템(Distributed Version Control System)이므로, 각 개발자가 로컬에 전체 이력을 가진 저장소를 둘 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/555483ea-3ad1-470e-a41c-1f95b43f28f0.png" alt="" style="display:block;margin:0 auto" />

<h3>버전 관리 프로그램의 종류: Git</h3>
<p><strong>Git</strong> 은 현재 가장 대표적인 <strong>분산 버전 관리 시스템(Distributed Version Control System, DVCS)</strong> 이다. Git은 리눅스 커널(Linux Kernel)을 만든 <strong>리누스 토르발스(Linus Torvalds)</strong> 가 2005년에 만들었다. Git이 만들어진 배경에는 리눅스 커널 개발 과정에서 사용하던 기존 버전 관리 도구인 <strong>BitKeeper</strong> 와 관련된 문제가 있었다.</p>
<p>리눅스 커널 개발자들은 한동안 <strong>BitKeeper</strong> 라는 버전 관리 시스템을 사용했다. BitKeeper는 분산 버전 관리 기능을 가진 도구였지만, 독점 소프트웨어(Proprietary Software)였다. 이후 라이선스 문제로 리눅스 커널 개발자들이 BitKeeper를 계속 사용하기 어려워지면서, 리누스 토르발스는 리눅스 커널 개발에 적합한 새로운 버전 관리 도구를 직접 만들게 되었다. 이 도구가 바로 Git이다. GitHub Blog에서도 Git은 2005년 4월 7일 첫 커밋이 만들어졌고, 리눅스 커널 개발자들이 BitKeeper 접근 권한을 잃은 뒤 만들어졌다고 설명한다.</p>
<p>Git의 가장 큰 특징은 <strong>완전한 분산 관리(Distributed Management)</strong> 가 가능하다는 점이다. 중앙 집중식 버전 관리 시스템에서는 중앙 서버가 원본 저장소 역할을 하고, 개발자들은 그 서버에서 파일을 받아 작업한 뒤 다시 서버에 반영한다. 반면 Git에서는 각 개발자가 자신의 컴퓨터에 프로젝트의 전체 변경 이력을 포함한 저장소를 가질 수 있다.</p>
<p>즉, Git에서는 단순히 파일의 현재 상태만 복사하는 것이 아니라, 과거 커밋 이력까지 포함한 저장소 전체를 로컬에 보관한다. 그래서 중앙 서버에 접속하지 않아도 커밋(Commit)을 만들고, 로그(Log)를 확인하고, 브랜치(Branch)를 만들고, 이전 상태로 되돌아가는 작업을 할 수 있다. 중앙 서버는 협업을 위해 자주 사용되지만, Git의 구조상 반드시 하나의 중앙 서버만 원본이어야 하는 것은 아니다.</p>
<p>이 점에서 Git은 기존 도구의 단순한 점진적 개선이라기보다, 버전 관리 방식을 크게 바꾼 도구로 볼 수 있다. CVS나 SVN 같은 중앙 집중식 버전 관리 시스템에서는 중앙 저장소가 기준이 되는 경우가 많았다. 하지만 Git에서는 각 개발자의 저장소가 독립적인 이력을 가질 수 있고, 필요에 따라 서로 변경 내용을 주고받을 수 있다. 그래서 오픈소스 프로젝트처럼 전 세계의 개발자가 참여하는 환경에 매우 잘 맞는다.</p>
<p>Git은 <strong>무료 오픈소스 소프트웨어(Free and Open Source Software)</strong> 이다. Git 공식 사이트에서도 Git을 무료 오픈소스 분산 버전 관리 시스템이라고 설명한다. Git은 빠르고 효율적으로 작은 프로젝트부터 매우 큰 프로젝트까지 처리할 수 있도록 설계되었다.</p>
<p>Git의 또 다른 장점은 브랜치와 병합이 강력하다는 점이다. Git에서는 새로운 기능을 실험하기 위해 브랜치를 만들고, 기능이 안정되면 다시 메인 브랜치(Main Branch)에 병합할 수 있다. 이 과정이 비교적 빠르고 가볍기 때문에, 현대 개발에서는 기능별 브랜치(Feature Branch), 버그 수정 브랜치(Bugfix Branch), 릴리스 브랜치(Release Branch) 같은 방식이 널리 사용된다.</p>
<p>Git은 현재 개인 프로젝트, 기업 개발, 오픈소스 프로젝트에서 매우 널리 사용된다. GitHub, GitLab, Bitbucket 같은 서비스도 Git을 기반으로 협업 기능을 제공한다. 다만 Git과 GitHub는 같은 것이 아니다. Git은 버전 관리 도구이고, GitHub는 Git 저장소를 온라인에서 호스팅하고 협업할 수 있도록 도와주는 플랫폼이다.</p>
<p>정리하면, <strong>Git</strong> 은 리눅스 커널 개발 과정에서 필요에 의해 만들어진 대표적인 분산 버전 관리 시스템이다. 기존에 사용하던 BitKeeper의 라이선스 문제가 계기가 되었고, 리누스 토르발스가 리눅스 커널 개발에 적합한 도구로 만들었다. Git은 각 개발자가 전체 저장소 이력을 로컬에 보관할 수 있기 때문에 중앙 서버에만 의존하지 않고, 완전한 분산 방식으로 프로젝트를 관리할 수 있다. 현재는 현대 소프트웨어 개발에서 가장 널리 사용되는 버전 관리 도구라고 할 수 있다.</p>
<p>📌Git을 이해할 때는 다음 구분이 중요하다.</p>
<p><strong>Git</strong> 은 버전 관리 프로그램이다.<br /><strong>GitHub</strong> 는 Git 저장소를 온라인에서 관리하고 협업할 수 있게 해주는 서비스이다.<br /><strong>GitLab</strong> 과 <strong>Bitbucket</strong> 도 Git 저장소 호스팅과 협업 기능을 제공하는 플랫폼이다.</p>
<p>즉, Git을 배운다는 것은 GitHub 사용법만 배우는 것이 아니라, 커밋(Commit), 브랜치(Branch), 병합(Merge), 충돌 해결(Conflict Resolution), 원격 저장소(Remote Repository) 같은 버전 관리 개념을 배우는 것이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/1d8145ef-28c5-45b6-8b91-57f9fa5adc15.png" alt="" style="display:block;margin:0 auto" />

<p>버전 관리에서 중요한 문제 중 하나는 여러 개발자가 같은 파일을 동시에 수정할 때 발생하는 충돌이다. 같은 파일이라도 서로 다른 함수를 수정하면 병합이 가능할 수 있지만, 같은 함수나 같은 코드 영역을 수정하면 충돌이 발생할 수 있다. 잠금 방식은 충돌을 줄일 수 있지만 작업 효율을 떨어뜨릴 수 있고, 병합 방식은 협업 효율은 높지만 충돌 해결이 필요하다. 따라서 버전 관리는 안전성과 효율성 사이에서 적절한 균형을 찾는 과정이다.</p>
<h3>버전 관리의 고려사항: 파일 동시 변경 (Version Control Consideration: Concurrent File Changes)</h3>
<p>버전 관리를 사용할 때 중요한 고려사항 중 하나는 여러 개발자가 같은 파일을 동시에 변경하는 상황이다. 소프트웨어 개발에서는 한 파일을 한 사람만 수정하는 경우도 있지만, 실제 협업 환경에서는 여러 개발자가 같은 파일을 동시에 수정하는 일이 자주 발생한다. 이때 변경 내용이 서로 어떻게 겹치는지에 따라 문제가 없을 수도 있고, 충돌이 발생할 수도 있다.</p>
<p>컴퓨터 시스템에서 여러 사용자가 같은 자원에 동시에 접근하는 것은 항상 조심해야 할 문제이다. 운영체제에서 여러 프로세스가 같은 데이터에 동시에 접근하면 <strong>동기화(Synchronization)</strong> 문제나 <strong>경쟁 상태(Race Condition)</strong> 가 발생할 수 있는 것처럼, 버전 관리에서도 여러 개발자가 같은 파일을 동시에 수정하면 변경 내용이 서로 충돌할 수 있다.</p>
<p>가장 단순한 해결 방법은 <strong>잠금(Lock)</strong> 을 사용하는 것이다. 한 개발자가 특정 파일을 수정하는 동안 다른 개발자가 그 파일을 수정하지 못하게 막는 방식이다. 이 방식은 충돌을 예방하는 데 효과적이다. 예를 들어 개발자 A가 <code>main.c</code> 파일을 잠그고 수정하고 있다면, 개발자 B는 A가 작업을 끝내기 전까지 같은 파일을 수정할 수 없다. 이렇게 하면 두 사람이 같은 파일을 동시에 수정해서 생기는 충돌을 줄일 수 있다.</p>
<p>하지만 잠금 방식에는 단점도 있다. 파일 전체를 잠그면 다른 개발자의 작업이 불필요하게 막힐 수 있다. 예를 들어 개발자 A는 같은 파일 안의 함수 <code>login()</code>을 수정하고 있고, 개발자 B는 전혀 다른 함수인 <code>logout()</code>을 수정하려고 할 수 있다. 두 작업은 서로 직접적인 충돌이 없을 수도 있지만, 파일 전체가 잠겨 있으면 B는 작업을 진행할 수 없다. 이 경우 협업 효율이 떨어진다.</p>
<p>그래서 현대 버전 관리 시스템에서는 단순히 파일 전체를 잠그는 방식만 사용하지 않고, 변경 내용을 비교하고 병합하는 방식을 많이 사용한다. 여러 개발자가 같은 파일을 수정하더라도 서로 다른 부분을 수정했다면 자동으로 합칠 수 있다. 예를 들어 개발자 A가 파일의 위쪽 함수를 수정하고, 개발자 B가 아래쪽 함수를 수정했다면 버전 관리 시스템은 두 변경 내용을 비교하여 하나의 파일로 병합할 수 있다.</p>
<p>반면 같은 파일 안에서도 같은 함수, 같은 줄, 같은 코드 블록을 서로 다르게 수정하면 <strong>충돌(Conflict)</strong> 이 발생할 가능성이 높다. 예를 들어 개발자 A와 B가 모두 <code>calculateTotal()</code> 함수의 같은 부분을 수정했다면, 버전 관리 시스템은 어떤 변경 내용을 최종적으로 남겨야 할지 자동으로 판단하기 어렵다. 이 경우 개발자가 직접 충돌 부분을 확인하고, 어떤 코드를 남길지 결정해야 한다.</p>
<p>따라서 버전 관리에서 중요한 것은 “같은 파일을 수정했는가”만이 아니라, “같은 파일의 어느 부분을 수정했는가”이다. 같은 파일이라도 서로 다른 함수나 서로 다른 위치를 수정했다면 병합이 가능할 수 있다. 하지만 같은 함수나 같은 코드 영역을 수정했다면 충돌이 발생할 수 있다.</p>
<p>이 문제는 협업 방식과도 연결된다. 파일을 어떻게 나누어 설계하느냐, 함수와 모듈을 얼마나 명확하게 분리하느냐, 누가 어떤 영역을 담당하느냐에 따라 충돌 가능성이 달라진다. 하나의 거대한 파일에 모든 기능이 들어 있다면 여러 개발자가 같은 파일을 자주 수정하게 되고, 충돌 가능성도 높아진다. 반대로 기능별로 파일과 모듈이 잘 나뉘어 있다면 각 개발자가 독립적으로 작업할 수 있어 충돌이 줄어든다.</p>
<p>결국 버전 관리에서 파일 동시 변경은 단순한 도구 문제가 아니라 개발 구조와 협업 방식의 문제이기도 하다. <strong>잠금(Lock)</strong> 을 사용하면 안전성은 높아질 수 있지만 작업 효율은 떨어질 수 있고, <strong>병합(Merge)</strong> 중심으로 관리하면 효율은 높아지지만 충돌을 해결해야 하는 부담이 생길 수 있다. 따라서 프로젝트의 성격, 파일의 종류, 팀의 작업 방식에 따라 적절한 관리 방법을 선택해야 한다.</p>
<p>특히 텍스트 기반 소스 코드에서는 병합 방식이 자주 사용되지만, 이미지, 영상, 실행 파일, 디자인 파일 같은 <strong>바이너리 파일(Binary File)</strong> 은 줄 단위 비교와 병합이 어렵기 때문에 잠금 방식이 더 적합할 수 있다. 예를 들어 게임 개발에서 3D 모델 파일이나 큰 이미지 파일을 여러 사람이 동시에 수정하면 자동 병합이 어렵기 때문에, 파일 잠금을 통해 한 번에 한 사람만 수정하도록 관리하는 경우가 있다.</p>
<p>정리하면, 버전 관리에서 파일 동시 변경을 고려할 때는 다음 두 가지 상황을 구분해야 한다. 여러 개발자가 같은 파일의 서로 다른 함수를 수정하는 경우에는 자동 병합이 가능할 수 있다. 하지만 여러 개발자가 같은 함수나 같은 코드 영역을 수정하는 경우에는 충돌이 발생할 가능성이 높다. 그래서 버전 관리는 단순히 파일을 저장하는 도구가 아니라, 협업 과정에서 변경 내용을 안전하고 효율적으로 관리하기 위한 체계라고 할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/fc1759c0-1c17-4a71-86fb-3b0dbb3fe1d0.png" alt="" style="display:block;margin:0 auto" />

<p>버전 관리에서 충돌은 같은 파일에서만 발생하는 것이 아니다. 서로 다른 파일을 수정했더라도 두 파일이 서로 의존하고 있다면 문제가 생길 수 있다. 예를 들어 한 파일에서 함수의 이름이나 매개변수를 바꾸면, 그 함수를 사용하는 다른 파일도 함께 수정되어야 한다. 따라서 버전 관리에서는 파일의 위치나 이름뿐만 아니라 파일 간 의존성도 함께 고려해야 한다.</p>
<h3>버전 관리의 고려사항: 파일 간 의존성 (Version Control Consideration: File Dependency)</h3>
<p>버전 관리에서 고려해야 할 또 다른 문제는 <strong>파일 간 의존성(File Dependency)</strong> 이다. 앞에서 살펴본 문제는 여러 개발자가 <strong>같은 파일(Same File)</strong> 을 수정할 때 발생하는 상황이었다. 이때 핵심은 같은 파일 안에서 같은 부분을 수정했는지, 아니면 서로 다른 부분을 수정했는지였다.</p>
<p>이번에 다루는 문제는 조금 다르다. 여기서는 개발자들이 서로 <strong>다른 파일(Different Files)</strong> 을 수정하고 있다. 겉으로 보면 같은 파일을 수정하는 것이 아니기 때문에 충돌이 없을 것처럼 보인다. 하지만 실제 소프트웨어 개발에서는 파일들이 서로 독립적으로 존재하지 않는 경우가 많다. 하나의 파일이 다른 파일을 참조하거나, 한 파일의 변경이 다른 파일의 동작에 영향을 줄 수 있다. 이것이 <strong>의존성(Dependency)</strong> 문제이다.</p>
<p>예를 들어 개발자 A가 <code>user.c</code> 파일을 수정하고, 개발자 B가 <code>auth.c</code> 파일을 수정한다고 하자. 두 사람은 서로 다른 파일을 수정하고 있으므로 버전 관리 시스템 입장에서는 직접적인 줄 단위 충돌은 없을 수 있다. 하지만 <code>auth.c</code>가 <code>user.c</code>의 함수나 구조체를 사용하고 있다면 문제가 생길 수 있다. A가 <code>user.c</code> 안의 함수 이름, 매개변수, 반환값을 바꾸면 B가 수정한 <code>auth.c</code> 코드가 더 이상 정상적으로 동작하지 않을 수 있다.</p>
<p>이처럼 서로 다른 파일을 수정했더라도 파일 사이에 <strong>의존 관계(Dependency Relationship)</strong> 가 있다면 충돌과 비슷한 문제가 발생할 수 있다. 버전 관리 시스템은 텍스트의 같은 줄이 동시에 수정되었는지는 비교할 수 있지만, 서로 다른 파일의 논리적 관계까지 항상 완벽하게 판단하지는 못한다. 따라서 Git 같은 도구에서 병합이 성공했다고 해서 프로그램이 반드시 정상적으로 동작한다는 뜻은 아니다.</p>
<p>반대로 파일 간 의존성이 거의 없다면, 서로 다른 파일을 수정하는 작업은 비교적 안전하게 병합될 수 있다. 예를 들어 개발자 A가 로그인 화면의 문구를 수정하고, 개발자 B가 전혀 다른 통계 출력 파일을 수정했다면 두 변경은 서로 영향을 주지 않을 가능성이 높다. 이런 경우에는 버전 관리 시스템이 자동으로 변경 내용을 합치기 쉽고, 실제 프로그램 실행에도 문제가 적을 수 있다.</p>
<p>따라서 버전 관리에서 중요한 점은 “같은 파일을 수정했는가”뿐만 아니라 “서로 다른 파일이지만 기능적으로 연결되어 있는가”이다. 같은 파일의 서로 다른 함수를 수정하는 문제는 텍스트 충돌에 가깝고, 서로 다른 파일의 의존성 문제는 구조적 충돌이나 의미적 충돌에 가깝다.</p>
<p>예를 들어 한 개발자가 <code>payment.h</code> 파일에서 함수 선언을 바꾸고, 다른 개발자가 <code>payment.c</code> 파일에서 기존 함수 정의를 기준으로 코드를 수정했다면 두 파일은 다르지만 서로 강하게 연결되어 있다. 병합 과정에서는 충돌이 나지 않을 수도 있지만, 컴파일 단계에서 오류가 발생할 수 있다. 또는 컴파일은 되더라도 실행 결과가 잘못될 수도 있다.</p>
<p>이런 문제를 줄이기 위해서는 파일 간 의존성을 잘 관리해야 한다. 기능별로 모듈(Module)을 분리하고, 인터페이스(Interface)를 명확히 정의하며, 한 파일의 변경이 다른 파일에 어떤 영향을 주는지 확인해야 한다. 또한 변경 내용을 병합한 뒤에는 반드시 빌드(Build)와 테스트(Test)를 수행해야 한다. 버전 관리 시스템이 변경 이력을 관리해주더라도, 프로그램의 동작까지 자동으로 보장해주는 것은 아니기 때문이다.</p>
<p>현대 개발 환경에서는 이런 문제를 줄이기 위해 <strong>CI(Continuous Integration)</strong> 를 사용하기도 한다. CI는 여러 개발자의 변경 내용을 자주 통합하고, 통합될 때마다 자동으로 빌드와 테스트를 실행하는 방식이다. 이를 통해 서로 다른 파일을 수정했지만 의존성 때문에 발생하는 문제를 빠르게 발견할 수 있다.</p>
<p>정리하면, 버전 관리의 파일 관련 고려사항은 두 단계로 나누어 볼 수 있다. 첫 번째는 같은 파일 안에서 같은 부분을 수정했는지, 다른 부분을 수정했는지의 문제이다. 두 번째는 서로 다른 파일을 수정했더라도 그 파일들이 서로 의존성을 가지고 있는지의 문제이다. 파일이 다르다고 해서 항상 안전한 것은 아니며, 의존성이 있는 파일이라면 한쪽의 변경이 다른 쪽의 오류로 이어질 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/17c3e66e-115b-440a-8b22-f63f706150db.png" alt="" style="display:block;margin:0 auto" />

<h3>Git 기본 설치 패키지 (Basic Git Installation Packages)</h3>
<p>리눅스/유닉스 환경에서 <strong>Git</strong> 을 사용하려면 먼저 Git 패키지를 설치해야 한다. Ubuntu나 Debian 계열 리눅스에서는 보통 <strong>APT(Advanced Package Tool)</strong> 를 사용하여 패키지를 설치한다. 가장 기본적인 설치 명령어는 다음과 같다.</p>
<pre><code class="language-c">sudo apt install git
</code></pre>
<p>이 명령어는 Git을 사용하기 위한 기본 구성 요소를 설치한다. 일반적으로 Git 저장소를 만들고, 파일을 추가하고, 커밋하고, 브랜치를 만들고, 원격 저장소와 연결하는 기본적인 작업은 이 패키지만 설치해도 충분하다. 즉, Git을 처음 배우거나 일반적인 개발 작업을 하기 위해서는 <code>git</code> 패키지만 설치하면 된다.</p>
<p>Debian 패키지 설명에서도 <code>git</code> 패키지는 최소한의 의존성으로 Git의 주요 구성 요소를 제공한다고 설명한다. 그래픽 인터페이스, 다른 버전 관리 시스템과의 연동 도구, 웹 인터페이스 같은 추가 기능은 별도의 <code>git*</code> 패키지로 제공된다.</p>
<p>반면 다음과 같은 명령어도 있다.</p>
<pre><code class="language-c">sudo apt install git-all
</code></pre>
<p><code>git-all</code>은 Git 하나만 설치하는 것이 아니라, Git과 관련된 다양한 부가 패키지들을 함께 설치하는 패키지이다. 예를 들어 <strong>Git GUI</strong>, <strong>Git Web Interface</strong>, <strong>Git SVN</strong>, <strong>Git CVS</strong>, <strong>Git Email</strong> 등 여러 보조 도구가 함께 설치될 수 있다. 그래서 <code>git-all</code>을 설치하면 설치되는 파일과 의존성 패키지의 양이 훨씬 많아질 수 있다.</p>
<p>쉽게 말해 <code>sudo apt install git</code>은 Git의 기본 기능을 설치하는 것이고, <code>sudo apt install git-all</code>은 Git과 관련된 거의 모든 부가 기능까지 함께 설치하는 방식이라고 볼 수 있다. 그래서 단순히 Git 명령어를 사용해 버전 관리를 배우고자 한다면 <code>git</code>만 설치해도 충분하다. 반대로 Git을 웹 인터페이스와 함께 사용하거나, SVN/CVS 같은 다른 버전 관리 시스템과 연동하거나, 이메일 기반 패치 작업처럼 특수한 기능까지 사용하려면 <code>git-all</code>을 고려할 수 있다.</p>
<p>그러나 초보자나 일반적인 개발자에게 <code>git-all</code>은 보통 필요하지 않다. 설치되는 패키지가 많고, 사용하지 않는 기능까지 함께 설치될 수 있기 때문이다. 특히 수업이나 개인 프로젝트에서 Git의 기본 명령어를 배우는 정도라면 <code>git-all</code>보다는 <code>git</code> 패키지를 설치하는 것이 더 간단하고 적절하다.</p>
<p>Git 설치 후에는 다음 명령어로 설치 여부와 버전을 확인할 수 있다.</p>
<pre><code class="language-c">git --version
</code></pre>
<p>이 명령어를 실행했을 때 <code>git version 2.x.x</code>와 같은 형태로 출력되면 Git이 정상적으로 설치된 것이다. 이후에는 사용자 이름과 이메일을 설정해야 한다. Git은 커밋을 만들 때 누가 변경했는지 기록하기 때문에, 처음 설치한 뒤에는 다음과 같이 기본 사용자 정보를 설정한다.</p>
<pre><code class="language-c">git config --global user.name "사용자이름"
git config --global user.email "이메일주소"
</code></pre>
<p>정리하면, Git을 가장 간단하게 사용하려면 <code>sudo apt install git</code>을 실행하면 된다. <code>git-all</code>은 Git 관련 부가 기능을 한꺼번에 설치하는 패키지이므로, 특별히 필요한 기능이 있을 때만 사용하는 것이 좋다. 일반적인 Git 학습과 개발 작업에서는 기본 <code>git</code> 패키지만으로도 충분하다.</p>
<hr />
<p>📌초보자 기준으로는 다음 순서가 가장 적절하다.</p>
<pre><code class="language-c">sudo apt update
sudo apt install git
git --version
</code></pre>
<p>그다음 Git 사용자 정보를 설정한다.</p>
<pre><code class="language-c">git config --global user.name "사용자이름"
git config --global user.email "이메일주소"
</code></pre>
<p><code>git-all</code>은 Git의 모든 관련 기능을 실험해보고 싶은 경우에는 사용할 수 있지만, 처음 배우는 단계에서는 보통 필요하지 않다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/fe7042c5-840d-42a9-856b-194b47ff4d06.png" alt="" style="display:block;margin:0 auto" />

<p>Git에서 커밋은 특정 순간의 프로젝트 전체 모습을 찍은 사진과 같다. 하지만 매번 모든 파일을 새로 복사해서 저장하는 것은 아니다. 바뀐 파일은 새로 저장하고, 바뀌지 않은 파일은 이전에 저장된 객체를 다시 가리킨다. 그래서 Git은 전체 프로젝트의 스냅샷을 관리하면서도 저장 공간을 효율적으로 사용할 수 있다.</p>
<h3>Git 버전의 개념: 스냅샷 방식 (Git Version Concept: Snapshot Model)</h3>
<p>Git에서 하나의 <strong>버전(Version)</strong> 이 결정된다는 것은 단순히 변경된 파일 몇 개만 따로 저장한다는 뜻이 아니다. Git은 특정 시점의 프로젝트 전체 모습을 하나의 <strong>스냅샷(Snapshot)</strong> 처럼 기록한다. 여기서 스냅샷은 사진을 찍는 것과 비슷한 개념이다. 어느 순간의 프로젝트 상태를 하나의 장면처럼 남겨두는 것이다.</p>
<p>그림을 보면 시간이 오른쪽으로 흐르고, <code>Version 1</code>부터 <code>Version 5</code>까지의 버전이 차례로 나타난다. 각 버전 아래에는 <code>File A</code>, <code>File B</code>, <code>File C</code> 또는 <code>A1</code>, <code>B1</code>, <code>C2</code>처럼 파일의 상태가 표시되어 있다. 이 그림은 Git이 각 버전에서 프로젝트 전체 상태를 어떻게 바라보는지 설명하기 위한 예시이다.</p>
<p><code>Version 1</code>에서는 <code>File A</code>, <code>File B</code>, <code>File C</code>가 존재한다. 이후 <code>Version 2</code>에서는 <code>File A</code>가 <code>A1</code>로 바뀌고, <code>File C</code>가 <code>C1</code>로 바뀐다. 반면 <code>File B</code>는 이전과 동일하다. 그림에서 실선으로 표시된 것은 이전 버전과 비교했을 때 변경된 파일을 의미하고, 점선으로 표시된 것은 이전 버전과 내용이 동일한 파일을 의미한다.</p>
<p><code>Version 3</code>을 보면 <code>A1</code>과 <code>B</code>는 점선으로 표시되어 있다. 이는 <code>Version 2</code>와 비교했을 때 <code>A1</code>과 <code>B</code>는 바뀌지 않았다는 뜻이다. 반면 <code>C1</code>은 <code>C2</code>로 바뀌었기 때문에 새로운 파일 상태로 표시된다. 즉, <code>Version 3</code>이라는 버전은 전체 프로젝트의 상태를 나타내지만, 실제로 새롭게 저장해야 하는 내용은 변경된 <code>C2</code>뿐이라고 볼 수 있다.</p>
<p>이때 중요한 점은 Git이 “버전마다 프로젝트 전체를 사진처럼 기억한다”는 관점과 “변경되지 않은 파일을 매번 새로 복사하지 않는다”는 관점을 함께 가진다는 것이다. 사용자가 보는 관점에서는 <code>Version 3</code>도 <code>A1</code>, <code>B</code>, <code>C2</code>를 모두 포함한 완전한 프로젝트 상태이다. 그러나 내부 저장 방식에서는 이미 이전 버전에 존재하던 <code>A1</code>과 <code>B</code>를 굳이 다시 저장하지 않고, 기존 객체를 참조할 수 있다.</p>
<p>예를 들어 <code>Version 2</code>의 <code>B</code>와 <code>Version 3</code>의 <code>B</code>가 완전히 같은 내용이라면, Git은 같은 내용을 가진 파일을 또 하나 복사해서 저장할 필요가 없다. 이미 저장된 파일 내용을 다시 가리키면 된다. 그래서 Git은 변경된 파일은 새롭게 저장하고, 변경되지 않은 파일은 기존에 저장된 객체를 재사용한다. Git 공식 문서에서도 커밋 객체는 특정 시점의 프로젝트 스냅샷을 나타내는 트리 객체를 가리키며, 변경된 파일에 대해서는 새로운 blob 객체를 저장하고 트리와 커밋 객체를 만든다고 설명한다.</p>
<p>이 구조를 조금 더 기술적으로 보면 Git은 주로 <strong>블롭(Blob)</strong>, <strong>트리(Tree)</strong>, <strong>커밋(Commit)</strong> 객체를 사용한다. <strong>블롭 객체(Blob Object)</strong> 는 파일의 실제 내용을 저장한다. <strong>트리 객체(Tree Object)</strong> 는 디렉토리 구조와 파일 이름, 그리고 각 파일 내용이 어떤 블롭 객체를 가리키는지를 저장한다. <strong>커밋 객체(Commit Object)</strong> 는 특정 시점의 최상위 트리와 부모 커밋, 작성자 정보, 커밋 메시지 등을 저장한다. Git 문서에서도 커밋 객체는 특정 시점의 디렉토리 트리 스냅샷을 나타내는 트리 객체를 참조한다고 설명한다.</p>
<p>따라서 Git의 버전은 단순히 “이전 버전에서 무엇이 바뀌었는가”만 저장하는 방식과는 다르다. Git은 각 커밋을 프로젝트 전체 상태의 스냅샷처럼 다룬다. 하지만 저장 공간을 낭비하지 않기 위해, 내용이 같은 파일은 같은 객체를 재사용한다. 즉, <code>Version 5</code>에서 <code>A2</code>가 이전 버전과 같다면 다시 저장하지 않고 기존 <code>A2</code>를 참조할 수 있고, <code>B2</code>나 <code>C3</code>처럼 변경된 파일만 새로운 내용으로 저장하면 된다.</p>
<p>이 방식은 Git을 이해할 때 매우 중요하다. 사용자는 각 커밋을 “그 시점의 전체 프로젝트 상태”로 생각하면 된다. 하지만 Git 내부에서는 변경되지 않은 파일을 매번 통째로 복사하지 않고, 이미 존재하는 파일 내용 객체를 재사용한다. 그래서 Git은 전체 스냅샷을 관리하면서도 저장 공간을 효율적으로 사용할 수 있다.</p>
<p>정리하면, Git에서 하나의 버전은 특정 순간의 프로젝트 전체 모습을 담은 <strong>스냅샷(Snapshot)</strong> 이다. 변경된 파일은 새 객체로 저장되고, 변경되지 않은 파일은 기존 객체를 다시 참조한다. 따라서 그림의 점선은 “이전 버전과 동일하므로 새로 저장하지 않고 기존 것을 참조할 수 있다”는 의미로 이해할 수 있다. Git은 이런 방식으로 버전마다 전체 프로젝트 상태를 관리하면서도, 불필요한 중복 저장을 줄인다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/c8c413fe-4c4d-4c4c-b19b-2372acda6479.png" alt="" style="display:block;margin:0 auto" />

<p>Git은 파일의 상태를 <strong>Modified</strong>, <strong>Staged</strong>, <strong>Committed</strong> 로 구분한다. 파일을 수정하면 Modified 상태가 되고, <code>git add</code>를 실행하면 Staged 상태가 되며, <code>git commit</code>을 실행하면 Committed 상태가 된다. 이 구조를 통해 Git은 어떤 변경을 다음 커밋에 포함할지 선택할 수 있게 해준다.</p>
<h3>Git의 기본: 파일의 세 가지 상태 (Git Basics: Three File States)</h3>
<p>Git은 파일이 변경되었는지, 변경된 파일이 다음 커밋에 포함될 준비가 되었는지, 그리고 변경 내용이 실제로 저장소에 기록되었는지를 구분해서 관리한다. 이때 Git에서 파일은 기본적으로 <strong>수정됨(Modified)</strong>, <strong>스테이지됨(Staged)</strong>, <strong>커밋됨(Committed)</strong> 이라는 세 가지 상태를 가진다.</p>
<p>먼저 <strong>수정됨(Modified)</strong> 은 파일의 내용이 이전에 저장된 상태와 달라졌다는 뜻이다. 예를 들어 <code>a.c</code>라는 파일이 있고, 이전에는 <code>a</code>라는 내용만 있었는데 이후에 <code>b</code>라는 내용이 추가되었다면 Git은 이 파일을 수정된 파일로 인식한다. 파일 이름은 여전히 <code>a.c</code>로 같지만, 이전 버전과 비교했을 때 내용이 달라졌기 때문에 <strong>Modified</strong> 상태가 된다.</p>
<p>이때 Git은 아주 작은 변경도 파일의 변경으로 인식할 수 있다. 코드 한 줄을 추가하거나 삭제하는 것뿐만 아니라, 공백, 줄바꿈, 주석문 변경도 파일 내용이 달라진 것으로 볼 수 있다. 물론 실제로 이 변경이 프로그램의 실행 결과에 영향을 주는지는 별개의 문제이다. Git 입장에서는 파일의 내용이 이전과 달라졌는지가 중요하다.</p>
<p>다음은 <strong>스테이지됨(Staged)</strong> 상태이다. Staged는 수정된 파일을 다음 커밋에 포함하기 위해 준비해 둔 상태를 말한다. 쉽게 말하면, 변경된 파일을 바로 저장소에 확정하는 것이 아니라, 먼저 <strong>스테이징 영역(Staging Area)</strong> 에 올려두는 것이다. Git에서는 보통 <code>git add</code> 명령어를 사용해 수정된 파일을 스테이징 영역에 올린다.</p>
<p>예를 들어 <code>a.c</code> 파일을 수정한 뒤 다음 명령어를 실행하면, 해당 파일은 Staged 상태가 된다.</p>
<pre><code class="language-c">git add a.c
</code></pre>
<p>이 상태는 “이 파일의 변경 내용을 다음 커밋에 포함하겠다”라고 표시해 둔 것과 같다. 즉, Staged 상태는 커밋 직전의 대기 상태라고 볼 수 있다.</p>
<p>마지막은 <strong>커밋됨(Committed)</strong> 상태이다. Committed는 스테이징 영역에 올라간 변경 내용이 Git의 로컬 저장소(Local Repository)에 실제로 기록된 상태를 말한다. Git에서는 보통 <code>git commit</code> 명령어를 사용해 변경 내용을 커밋한다.</p>
<pre><code class="language-c">git commit -m "Update a.c"
</code></pre>
<p>커밋이 완료되면 해당 변경 내용은 Git의 로컬 데이터베이스에 저장된다. 이때 Git은 단순히 파일을 복사해서 저장하는 것이 아니라, 변경된 상태를 하나의 기록으로 남긴다. 이 기록에는 어떤 파일이 변경되었는지, 누가 변경했는지, 언제 변경했는지, 어떤 메시지를 남겼는지 등의 정보가 포함된다.</p>
<p>따라서 Git에서 <strong>커밋(Commit)</strong> 은 단순히 “명령어를 실행한다”는 의미가 아니라, 프로젝트의 특정 시점 상태를 로컬 저장소에 저장하는 작업이다. Git은 이러한 커밋들을 통해 프로젝트의 변경 이력을 관리한다. 그래서 Git을 데이터베이스 관점에서 보면, 각 커밋은 프로젝트의 한 시점에 대한 기록이고, Git 저장소는 이러한 기록들이 쌓여 있는 데이터베이스라고 볼 수 있다.</p>
<p>정리하면 Git에서 파일의 세 가지 상태는 다음과 같다. <strong>Modified</strong> 는 파일이 수정되었지만 아직 다음 커밋에 포함되도록 준비되지 않은 상태이다. <strong>Staged</strong> 는 수정된 파일이 다음 커밋에 포함되도록 스테이징 영역에 올라간 상태이다. <strong>Committed</strong> 는 변경 내용이 로컬 저장소에 저장 완료된 상태이다.</p>
<p>이 세 가지 상태를 이해하면 Git의 기본 흐름도 자연스럽게 이해할 수 있다. 먼저 파일을 수정하면 Modified 상태가 되고, <code>git add</code>로 스테이징 영역에 올리면 Staged 상태가 되며, <code>git commit</code>으로 저장소에 기록하면 Committed 상태가 된다. Git의 기본 작업 흐름은 이 세 단계를 중심으로 움직인다.</p>
<pre><code class="language-c">파일 수정 → git add → git commit
Modified → Staged → Committed
</code></pre>
<p>결국 Git은 파일이 단순히 “있다” 또는 “없다”로만 관리되는 것이 아니라, 변경되었는지, 커밋 준비가 되었는지, 저장소에 기록되었는지를 구분해서 관리한다. 이 구조 덕분에 개발자는 어떤 변경을 커밋에 포함할지 선택할 수 있고, 프로젝트의 변경 이력을 더 세밀하게 관리할 수 있게되었다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/69e8ebfb-7cb1-4fa0-bae4-e486cb0528be.png" alt="" style="display:block;margin:0 auto" />

<p>Git 설정은 시스템(System), 글로벌(Global), 로컬(Local) 범위로 나뉜다. 시스템 설정은 모든 사용자에게 적용되고, 글로벌 설정은 특정 사용자에게 적용되며, 로컬 설정은 특정 저장소에만 적용된다. 같은 설정이 여러 범위에 존재하면 더 구체적인 설정이 우선 적용되므로, 최종적으로는 로컬 설정이 가장 우선적으로 적용된다.</p>
<hr />
<h3>Git 사용 설정 종류 (Git Configuration Levels)</h3>
<p>Git을 사용하기 위해서는 기본적인 <strong>설정(Configuration)</strong> 이 필요하다. Git 설정은 사용자 이름, 이메일, 편집기, 줄바꿈 처리 방식, 별칭(alias) 등 Git이 동작할 때 참고하는 여러 정보를 저장한다. 이 설정은 적용 범위에 따라 크게 <strong>시스템(System)</strong>, <strong>글로벌(Global)</strong>, <strong>로컬(Local)</strong> 세 가지로 나눌 수 있다.</p>
<p>Git 설정에서 중요한 특징은 <strong>오버라이드(Override)</strong> 구조이다. 오버라이드는 기존 설정을 더 구체적인 설정이 덮어쓴다는 뜻이다. 즉, 넓은 범위의 설정이 먼저 적용되더라도, 더 좁은 범위에서 같은 항목을 다시 설정하면 좁은 범위의 설정이 우선 적용된다.</p>
<p>가장 넓은 범위는 <strong>시스템 설정(System Configuration)</strong> 이다. 시스템 설정은 해당 컴퓨터를 사용하는 모든 사용자에게 적용되는 설정이다. 리눅스 환경에서는 보통 다음 경로에 저장된다.</p>
<pre><code class="language-c">/etc/gitconfig
</code></pre>
<p>이 설정은 컴퓨터 전체에 적용되기 때문에, 같은 시스템을 사용하는 모든 사용자가 영향을 받는다. 예를 들어 학교 실습실이나 서버처럼 여러 사용자가 같은 컴퓨터를 사용하는 환경에서는 시스템 설정이 공통 기본값처럼 작동할 수 있다.</p>
<p>두 번째는 <strong>글로벌 설정(Global Configuration)</strong> 이다. 글로벌 설정은 특정 사용자에게 적용되는 설정이다. 즉, 같은 컴퓨터 안에서도 사용자 계정마다 다른 Git 설정을 가질 수 있다. 리눅스에서는 보통 다음 위치에 저장된다.</p>
<pre><code class="language-plaintext">~/.gitconfig
</code></pre>
<p>또는 다음 위치가 사용될 수 있다.</p>
<pre><code class="language-plaintext">~/.config/git/config
</code></pre>
<p>여기서 <code>~</code>는 현재 사용자의 홈 디렉토리(Home Directory)를 의미한다. 예를 들어 사용자 이름이 <code>student</code>라면 <code>~/.gitconfig</code>는 <code>/home/student/.gitconfig</code>와 같은 의미가 될 수 있다. 일반적으로 Git을 처음 설치한 뒤 사용자 이름과 이메일을 설정할 때 이 글로벌 설정을 많이 사용한다.</p>
<p>예를 들어 다음 명령어는 현재 사용자에게 적용되는 이름과 이메일을 설정한다.</p>
<pre><code class="language-c">git config --global user.name "사용자이름"
git config --global user.email "이메일주소"
</code></pre>
<p>세 번째는 <strong>로컬 설정(Local Configuration)</strong> 이다. 로컬 설정은 특정 Git 저장소, 즉 특정 프로젝트에만 적용되는 설정이다. 로컬 설정은 해당 프로젝트 폴더 안의 다음 파일에 저장된다.</p>
<pre><code class="language-c">.git/config
</code></pre>
<p>로컬 설정은 프로젝트 단위로 적용되기 때문에 가장 구체적인 설정이다. 예를 들어 개인 프로젝트에서는 개인 이메일을 사용하고, 회사 프로젝트에서는 회사 이메일을 사용해야 한다면 로컬 설정을 통해 프로젝트마다 다른 이메일을 지정할 수 있다.</p>
<p>Git 설정의 우선순위는 일반적으로 다음과 같다.</p>
<pre><code class="language-plaintext">System → Global → Local
</code></pre>
<p>즉, 시스템 설정이 가장 넓은 기본값이고, 글로벌 설정이 그 위에 적용되며, 로컬 설정이 최종적으로 가장 우선 적용된다. 같은 설정 항목이 여러 위치에 존재한다면, 더 구체적인 로컬 설정이 글로벌 설정을 덮어쓰고, 글로벌 설정은 시스템 설정을 덮어쓴다.</p>
<p>예를 들어 시스템 설정에는 사용자 이름이 <code>Linux User</code>로 되어 있고, 글로벌 설정에는 <code>Student</code>로 되어 있으며, 특정 프로젝트의 로컬 설정에는 <code>Project Developer</code>로 되어 있다면, 해당 프로젝트 안에서는 최종적으로 <code>Project Developer</code>가 적용된다. 이것이 Git 설정에서 말하는 <strong>오버라이드(Override)</strong> 의 의미이다.</p>
<p>이 구조는 Git을 여러 환경에서 유연하게 사용할 수 있게 해준다. 모든 사용자에게 공통으로 적용할 설정은 시스템 범위에 두고, 개인 사용자가 자주 쓰는 기본 설정은 글로벌 범위에 둔다. 그리고 특정 프로젝트에서만 달라져야 하는 설정은 로컬 범위에 둔다.</p>
<p>정리하면, Git 설정은 <strong>시스템(System)</strong>, <strong>글로벌(Global)</strong>, <strong>로컬(Local)</strong> 세 단계로 나뉜다. 시스템 설정은 컴퓨터 전체와 모든 사용자에게 적용되고, 글로벌 설정은 특정 사용자에게 적용되며, 로컬 설정은 특정 저장소나 프로젝트에 적용된다. 같은 설정이 여러 단계에 존재할 경우, 더 구체적인 설정이 더 넓은 설정을 덮어쓰며, 최종적으로는 로컬 설정이 가장 우선적으로 적용된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/e888db7d-356a-494d-b51a-f3ef72177f48.png" alt="" style="display:block;margin:0 auto" />

<h3>Git 사용 설정 확인과 변경 (Git Configuration Check &amp; Set)</h3>
<p>Git을 사용하려면 먼저 기본 설정을 확인하고 필요한 값을 지정해야 한다. Git 설정은 앞에서 본 것처럼 <strong>시스템(System)</strong>, <strong>글로벌(Global)</strong>, <strong>로컬(Local)</strong> 범위로 나뉘며, 같은 설정이 여러 곳에 존재할 경우 더 좁은 범위의 설정이 우선 적용된다. 즉, <strong>System → Global → Local</strong> 순서로 갈수록 우선순위가 높아지고, 최종적으로는 특정 저장소의 <strong>로컬 설정(Local Configuration)</strong> 이 가장 우선적으로 적용된다.</p>
<p>현재 Git에 어떤 설정들이 적용되어 있는지 확인하려면 터미널에서 다음 명령어를 사용할 수 있다.</p>
<pre><code class="language-plaintext">git config --list --show-origin
</code></pre>
<p>여기서 <code>--list</code>는 현재 적용 가능한 Git 설정 목록을 보여주는 옵션이고, <code>--show-origin</code>은 각 설정이 어느 파일에서 온 것인지 함께 보여주는 옵션이다. 이 명령어를 사용하면 설정값뿐만 아니라 해당 설정이 <strong>시스템 설정(System Configuration)</strong> 에서 온 것인지, <strong>글로벌 설정(Global Configuration)</strong> 에서 온 것인지, 아니면 <strong>로컬 설정(Local Configuration)</strong> 에서 온 것인지 확인할 수 있다.</p>
<p>Git 설정을 변경할 때는 적용 범위에 따라 옵션을 다르게 사용한다. 특정 프로젝트, 즉 현재 Git 저장소에만 설정을 적용하려면 <code>--local</code> 옵션을 사용한다.</p>
<pre><code class="language-plaintext">git config --local user.name "korea cyber university"
git config --local user.email "noreply@koreacu.ac.kr"
</code></pre>
<p>이 명령어는 현재 저장소의 <code>.git/config</code> 파일에 사용자 이름과 이메일을 저장한다. 따라서 이 설정은 현재 프로젝트에서만 적용된다. 예를 들어 개인 프로젝트에서는 개인 이메일을 사용하고, 학교 과제 저장소에서는 학교 이메일을 사용하고 싶을 때 로컬 설정을 사용할 수 있다.</p>
<p>특정 사용자 계정 전체에 적용하려면 <code>--global</code> 옵션을 사용한다.</p>
<pre><code class="language-plaintext">git config --global user.name "사용자이름"
git config --global user.email "이메일주소"
</code></pre>
<p>글로벌 설정은 보통 현재 사용자의 홈 디렉토리에 있는 다음 파일에 저장된다.</p>
<pre><code class="language-plaintext">~/.gitconfig
</code></pre>
<p>또는 환경에 따라 다음 경로가 사용될 수도 있다.</p>
<pre><code class="language-plaintext">~/.config/git/config
</code></pre>
<p>여기서 <code>~</code>는 현재 사용자의 홈 디렉토리(Home Directory)를 의미한다. 따라서 <code>--global</code>은 컴퓨터 전체가 아니라, 현재 로그인한 특정 사용자에게 적용되는 설정이라고 이해하면 된다.</p>
<p>컴퓨터의 모든 사용자에게 공통으로 적용되는 설정은 <code>--system</code> 옵션을 사용한다.</p>
<pre><code class="language-plaintext">git config --system user.name "공통사용자이름"
</code></pre>
<p>시스템 설정은 리눅스 환경에서 보통 다음 파일에 저장된다.</p>
<pre><code class="language-plaintext">/etc/gitconfig
</code></pre>
<p>이 파일은 시스템 전체 설정이므로 일반 사용자 권한으로는 수정이 제한될 수 있다. 따라서 실제로 <code>--system</code> 설정을 변경하려면 관리자 권한이 필요할 수 있다.</p>
<pre><code class="language-plaintext">sudo git config --system user.name "공통사용자이름"
</code></pre>
<p>정리하면, <code>--system</code>은 모든 사용자에게 적용되는 설정이고, <code>--global</code>은 현재 사용자에게 적용되는 설정이며, <code>--local</code>은 현재 Git 저장소에만 적용되는 설정이다. 같은 항목이 여러 위치에 설정되어 있으면 더 구체적인 설정이 기존 설정을 <strong>오버라이드(Override)</strong> 한다. 즉, 로컬 설정이 글로벌 설정을 덮어쓰고, 글로벌 설정이 시스템 설정을 덮어쓴다.</p>
<p>명령어를 입력할 때는 따옴표의 종류도 주의해야 한다. 일반적으로 값에 공백이 들어갈 때는 작은따옴표(<code>'</code>)나 큰따옴표(<code>"</code>)를 사용할 수 있다.</p>
<pre><code class="language-plaintext">git config --local user.name "korea cyber university"
git config --local user.name 'korea cyber university'
</code></pre>
<p>두 명령어는 일반적인 문자열 설정에서는 비슷하게 동작한다. 하지만 백틱(<code>`</code>)은 작은따옴표가 아니다. 리눅스 셸에서 백틱은 명령어 실행 결과를 치환하는 용도로 사용될 수 있으므로, 단순 문자열을 감쌀 때 작은따옴표와 혼동하면 안 된다.</p>
<p>예를 들어 아래처럼 백틱을 사용하면 단순 문자열이 아니라 셸이 그 안의 내용을 명령어처럼 해석하려고 할 수 있다.</p>
<pre><code class="language-plaintext">git config --local user.name `korea cyber university`
</code></pre>
<p>따라서 Git 설정 값을 입력할 때는 보통 큰따옴표(<code>" "</code>) 또는 작은따옴표(<code>' '</code>)를 사용하는 것이 안전하다. 특히 사용자 이름처럼 공백이 포함될 수 있는 값은 따옴표로 감싸는 것이 좋다.</p>
<p>결국 Git 설정은 “어디에 적용할 것인가”가 핵심이다. 모든 사용자에게 적용할 설정은 <strong>System</strong>, 현재 사용자에게 적용할 설정은 <strong>Global</strong>, 특정 프로젝트에만 적용할 설정은 <strong>Local</strong> 범위에 저장한다. 그리고 실제 적용 결과를 확인할 때는 <code>git config --list --show-origin</code>을 사용하면 어떤 설정이 어느 파일에서 왔는지 확인할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/3be810af-ce80-4b01-8185-858946270db8.png" alt="" style="display:block;margin:0 auto" />

<h3>Git 사용 설정: 편집기와 기본 브랜치 설정 (Git Configuration: Editor &amp; Default Branch)</h3>
<p>Git을 사용할 때는 사용자 이름과 이메일뿐만 아니라, 기본 편집기와 기본 브랜치 이름도 설정할 수 있다. 이러한 설정은 Git을 사용할 때 반복적으로 적용되는 기본 동작을 정하는 역할을 한다.</p>
<p>먼저 <strong>기본 편집기(Default Editor)</strong> 설정이 있다. Git에서는 커밋 메시지(Commit Message)를 입력해야 하는 경우가 많다. 보통 간단한 커밋 메시지는 다음처럼 명령어에서 바로 작성할 수 있다.</p>
<pre><code class="language-plaintext">git commit -m "커밋 메시지"
</code></pre>
<p>하지만 <code>-m</code> 옵션을 사용하지 않고 <code>git commit</code>만 실행하면, Git은 커밋 메시지를 입력하라고 편집기 창을 연다. 이때 어떤 편집기를 열 것인지 정하는 설정이 <strong>core.editor</strong> 이다.</p>
<p>예제에서는 다음과 같이 Vim을 기본 편집기로 설정하고 있다.</p>
<pre><code class="language-plaintext">git config --global core.editor vim
</code></pre>
<p>이 명령어는 현재 사용자(Global) 범위에서 Git의 기본 편집기를 <strong>Vim</strong> 으로 설정한다. 이후 커밋 메시지를 직접 입력해야 할 때 Git은 Vim을 실행한다.</p>
<p>다만 모든 사용자가 Vim을 편하게 사용하는 것은 아니다. Vim은 리눅스/유닉스 환경에서 오래전부터 많이 사용된 강력한 텍스트 편집기지만, 처음 사용하는 사람에게는 조작 방식이 어렵게 느껴질 수 있다. Vim을 사용하지 않는다면 다른 편집기를 설정해도 된다. 예를 들어 <strong>Nano</strong> 를 사용하고 싶다면 다음처럼 설정할 수 있다.</p>
<pre><code class="language-plaintext">git config --global core.editor nano
</code></pre>
<p>또는 <strong>Emacs</strong> 를 사용한다면 다음과 같이 설정할 수 있다.</p>
<pre><code class="language-plaintext">git config --global core.editor emacs
</code></pre>
<p>즉, Git에서 편집기 설정이 필요한 이유는 Git 자체가 문서를 작성하기 위해서라기보다, 커밋 메시지처럼 사용자가 직접 텍스트를 입력해야 하는 상황에서 어떤 편집기를 사용할지 정하기 위해서이다.</p>
<p>다음으로 <strong>기본 브랜치(Default Branch)</strong> 이름 설정이 있다. Git에서 새 저장소를 만들면 기본 브랜치가 생성된다. 과거에는 기본 브랜치 이름으로 <code>master</code>가 많이 사용되었지만, 최근에는 <code>main</code>을 사용하는 경우가 많다. 그래서 새 Git 저장소를 만들 때 기본 브랜치 이름을 <code>main</code>으로 지정하려면 다음 명령어를 사용할 수 있다.</p>
<pre><code class="language-plaintext">git config --global init.defaultBranch main
</code></pre>
<p>이 설정은 이후 <code>git init</code>으로 새 저장소를 만들 때 기본 브랜치 이름을 <code>main</code>으로 지정하게 한다. 이미 만들어진 저장소의 브랜치 이름을 바꾸는 명령어가 아니라, 앞으로 새로 생성할 저장소의 기본 브랜치 이름을 정하는 설정이라는 점이 중요하다.</p>
<p>정리하면, Git 설정에서는 사용자의 작업 환경에 맞게 기본 편집기와 기본 브랜치 이름을 지정할 수 있다. <code>core.editor</code>는 Git이 커밋 메시지 등을 입력받을 때 사용할 편집기를 정하는 설정이고, <code>init.defaultBranch</code>는 새 저장소를 만들 때 기본으로 생성될 브랜치 이름을 정하는 설정이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/156f0fb5-82e1-42d8-8c0c-22f37f912a0a.png" alt="" style="display:block;margin:0 auto" />

<h3>Git 사용: 저장소 초기화와 <code>.git</code> 디렉토리 (Git Usage: Repository Initialization &amp; <code>.git</code> Directory)</h3>
<p>Git을 사용하려면 먼저 버전 관리를 적용할 디렉토리에서 <strong>저장소 초기화(Repository Initialization)</strong> 를 해야 한다. Git을 설치했다고 해서 모든 디렉토리가 자동으로 Git 저장소가 되는 것은 아니다. 사용자가 원하는 프로젝트 디렉토리로 이동한 뒤, 그 위치에서 Git 저장소를 만들겠다고 지정해야 한다.</p>
<p>이를 위해 사용하는 명령어가 다음과 같다.</p>
<pre><code class="language-plaintext">git init
</code></pre>
<p><code>git init</code> 명령어를 실행하면 현재 디렉토리 안에 숨김 디렉토리인 <code>.git</code> 디렉토리가 생성된다. 리눅스/유닉스 환경에서는 이름이 점(<code>.</code>)으로 시작하는 파일이나 디렉토리는 기본적으로 숨김 파일(Hidden File)로 취급된다. 그래서 일반적인 <code>ls</code> 명령어만으로는 보이지 않을 수 있고, 다음처럼 <code>-a</code> 옵션을 사용해야 확인할 수 있다.</p>
<pre><code class="language-plaintext">ls -a
</code></pre>
<p><code>.git</code> 디렉토리는 Git 저장소의 핵심이다. 이 안에는 버전 관리에 필요한 여러 정보가 저장된다. 예를 들어 커밋 이력(Commit History), 브랜치 정보(Branch Information), 설정 파일(Configuration File), 객체 데이터(Object Data), 인덱스(Index) 등이 <code>.git</code> 디렉토리 안에 보관된다.</p>
<p>즉, 우리가 프로젝트 폴더에서 보는 일반 파일들은 실제 작업 파일이고, <code>.git</code> 디렉토리는 그 파일들의 변경 이력을 관리하는 내부 저장소라고 볼 수 있다. 사용자는 보통 소스 코드 파일을 수정하고 <code>git add</code>, <code>git commit</code> 같은 명령어를 사용하지만, Git은 내부적으로 <code>.git</code> 디렉토리에 필요한 정보를 기록하면서 버전 관리를 수행한다.</p>
<p>중요한 점은 <code>.git</code> 디렉토리를 삭제하면 해당 프로젝트의 Git 이력이 사라진다는 것이다. 예를 들어 프로젝트 폴더 안에 <code>main.c</code>, <a href="http://README.md"><code>README.md</code></a> 같은 파일이 남아 있더라도, <code>.git</code> 디렉토리를 삭제하면 Git은 더 이상 그 폴더를 Git 저장소로 인식하지 못한다. 현재 파일 자체는 남아 있을 수 있지만, 이전 커밋 이력, 브랜치 정보, 로컬 설정 등 버전 관리와 관련된 정보는 사라진다.</p>
<p>따라서 <code>.git</code> 디렉토리는 일반적인 폴더처럼 함부로 삭제하면 안 된다. 특히 Git을 처음 배우는 단계에서는 “<code>.git</code>은 Git이 자동으로 만든 숨김 폴더니까 지워도 되겠지”라고 생각할 수 있지만, 실제로는 Git 저장소의 핵심 데이터가 들어 있는 중요한 디렉토리이다.</p>
<p>다만 <code>.git</code> 디렉토리를 삭제한다고 해서 현재 작업 중인 파일 자체가 반드시 삭제되는 것은 아니다. 예를 들어 프로젝트 폴더 안의 <code>a.c</code>, <code>b.c</code>, <code>index.html</code> 같은 파일은 그대로 남아 있을 수 있다. 하지만 Git이 관리하던 변경 이력과 저장소 정보는 사라진다. 그래서 해당 폴더는 Git 입장에서는 그냥 일반 디렉토리가 된다.</p>
<p>정리하면, Git을 사용하기 위해서는 프로젝트 디렉토리에서 <code>git init</code>을 실행하여 저장소를 초기화한다. 이때 생성되는 <code>.git</code> 디렉토리에는 Git 버전 관리에 필요한 핵심 정보들이 저장된다. 따라서 <code>.git</code> 디렉토리를 삭제하면 현재 파일은 남아 있더라도, Git이 관리하던 이력과 저장소 정보는 사라진다는 점을 반드시 알아두어야 한다.</p>
<p>📌추가 설명 (Additional Notes)</p>
<p><code>.git</code> 디렉토리를 확인하는 기본 흐름은 다음과 같다.</p>
<pre><code class="language-c">mkdir my-project
cd my-project
git init
ls -a
</code></pre>
<p>이때 <code>.git</code>이 보이면 해당 디렉토리가 Git 저장소로 초기화된 것이다.</p>
<p>Git 저장소인지 확인하고 싶다면 다음 명령어를 사용할 수 있다.</p>
<pre><code class="language-plaintext">git status
</code></pre>
<p>Git 저장소 안이라면 현재 브랜치와 파일 상태가 출력된다. 반대로 <code>.git</code> 디렉토리가 없거나 Git 저장소가 아니라면 오류 메시지가 나타난다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a82f4e60-cd8d-4a95-a15a-c9cedd139be8.png" alt="" style="display:block;margin:0 auto" />

<h3>Git 기본 사용법: 새 파일 추가와 Untracked 상태 (Git Basic Usage: Adding a New File &amp; Untracked State)</h3>
<p>Git 저장소를 만든 뒤 새 파일을 생성하면, Git은 그 파일을 자동으로 버전 관리 대상으로 포함하지 않는다. 예를 들어 Git 저장소 안에서 다음과 같이 <code>helloWorld.c</code> 파일을 만들었다고 하자.</p>
<pre><code class="language-plaintext">vi helloWorld.c
</code></pre>
<p>또는 다른 편집기를 사용해서 같은 파일을 만들 수도 있다.</p>
<pre><code class="language-plaintext">nano helloWorld.c
</code></pre>
<p>이렇게 새 파일을 만들고 나서 현재 Git 저장소의 상태를 확인하려면 다음 명령어를 사용한다.</p>
<pre><code class="language-plaintext">git status
</code></pre>
<p><code>git status</code>는 현재 저장소에서 어떤 파일이 변경되었는지, 어떤 파일이 스테이징 영역(Staging Area)에 올라가 있는지, 어떤 파일이 아직 Git의 관리 대상이 아닌지를 보여준다.</p>
<p>새로 만든 <code>helloWorld.c</code> 파일은 처음에는 <strong>Untracked Files</strong> 항목에 표시된다. 여기서 <strong>Untracked</strong> 는 Git이 아직 해당 파일을 추적하지 않는다는 뜻이다. 즉, 파일은 실제 디렉토리 안에 존재하지만, Git은 아직 이 파일의 변경 이력을 관리하지 않는다.</p>
<p>화면에 표시된 메시지를 보면 다음과 같은 내용이 나온다.</p>
<pre><code class="language-c">Untracked files:
  (use "git add &lt;file&gt;..." to include in what will be committed)
        helloWorld.c
</code></pre>
<p>이 문장은 “이 파일을 다음 커밋에 포함하려면 <code>git add &lt;file&gt;</code> 명령어를 사용하라”는 뜻이다. 즉, Git에게 “이 파일을 앞으로 관리 대상으로 삼아라”라고 알려주어야 한다.</p>
<p>이를 위해 다음 명령어를 입력한다.</p>
<pre><code class="language-plaintext">git add helloWorld.c
</code></pre>
<p><code>git add</code>는 파일을 <strong>스테이징 영역(Staging Area)</strong> 에 올리는 명령어이다. 새 파일의 경우에는 이 명령어를 통해 Git이 해당 파일을 추적하기 시작한다. 즉, <code>helloWorld.c</code>는 더 이상 Untracked 상태가 아니라, 다음 커밋에 포함될 준비가 된 <strong>Staged</strong> 상태가 된다.</p>
<p>여기서 중요한 점은 Git이 파일을 자동으로 모두 기록하지 않는다는 것이다. Git 저장소 안에 파일이 존재한다고 해서 무조건 버전 관리 대상이 되는 것은 아니다. 사용자가 <code>git add</code>를 통해 명시적으로 추가해야 Git이 해당 파일을 관리 대상으로 인식한다.</p>
<p>이는 실수를 줄이기 위한 구조이기도 하다. 프로젝트 폴더 안에는 임시 파일, 실행 결과 파일, 로그 파일, 빌드 결과물처럼 버전 관리에 포함하지 않아야 하는 파일도 있을 수 있다. Git은 사용자가 어떤 파일을 기록할지 선택할 수 있도록, 새 파일을 처음부터 자동으로 추적하지 않고 Untracked 상태로 둔다.</p>
<p>정리하면, Git 저장소 안에서 새 파일을 만들면 처음에는 <strong>Untracked</strong> 상태가 된다. 이 상태는 Git이 아직 해당 파일을 감시하거나 기록하지 않는 상태이다. 새 파일을 버전 관리 대상으로 포함하려면 <code>git add 파일명</code> 명령어를 실행해야 한다. 이후 파일은 <strong>Staged</strong> 상태가 되고, 다음 <code>git commit</code>을 실행할 때 저장소 이력에 기록될 수 있다.</p>
<pre><code class="language-plaintext">vi helloWorld.c
git status
git add helloWorld.c
git status
</code></pre>
<p>이 흐름을 통해 새 파일이 Git에서 어떻게 관리 대상으로 들어가는지 확인할 수 있다.</p>
<p>📌추가 설명 (Additional Notes)</p>
<p>Git에서 새 파일을 추가하는 기본 흐름은 다음과 같다.</p>
<pre><code class="language-c">echo "hello world" &gt; helloWorld.c
git status
git add helloWorld.c
git status
git commit -m "Add helloWorld.c"
</code></pre>
<p>상태 변화는 다음처럼 이해할 수 있다.</p>
<pre><code class="language-plaintext">Untracked → Staged → Committed
</code></pre>
<p>즉, 새 파일은 처음에는 Git이 모르는 파일이고, <code>git add</code>를 실행하면 다음 커밋에 포함될 준비가 되며, <code>git commit</code>을 실행하면 Git 이력에 저장된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/619a0be1-795a-4c5e-b91f-6c48fc8d4f5c.png" alt="" style="display:block;margin:0 auto" />

<p>새 파일을 만들면 Git은 처음에 그 파일을 <strong>Untracked</strong> 상태로 표시한다. <code>git add</code>를 실행하면 파일은 <strong>Staged</strong> 상태가 되어 다음 커밋에 포함될 준비가 된다. 하지만 아직 커밋된 것은 아니므로, 실제 이력에 저장하려면 <code>git commit</code>을 실행해야 한다.</p>
<h3>Git 기본 사용법: <code>git add</code>와 Staged 상태 (Git Basic Usage: <code>git add</code> &amp; Staged State)</h3>
<p>Git 저장소 안에서 새로운 파일을 만들면, 처음에는 Git이 그 파일을 자동으로 관리하지 않는다. 예를 들어 <code>helloWorld.c</code>라는 파일을 새로 만들고 <code>git status</code>를 실행하면, Git은 이 파일을 <strong>추적되지 않은 파일(Untracked File)</strong> 로 표시한다. 이는 파일이 실제 디렉토리 안에는 존재하지만, 아직 Git의 버전 관리 대상에는 포함되지 않았다는 뜻이다.</p>
<p>새 파일을 Git의 관리 대상으로 포함하려면 <code>git add</code> 명령어를 사용한다.</p>
<pre><code class="language-plaintext">git add helloWorld.c
</code></pre>
<p>이 명령어를 실행하면 <code>helloWorld.c</code> 파일은 Git의 <strong>스테이징 영역(Staging Area)</strong> 에 올라간다. 스테이징 영역은 다음 <strong>커밋(Commit)</strong> 에 포함할 변경 내용을 임시로 모아두는 공간이다. 즉, <code>git add</code>는 단순히 “파일을 감시 대상으로 등록한다”는 의미도 있지만, 더 정확하게는 “이 파일의 현재 변경 내용을 다음 커밋에 포함하도록 준비한다”는 뜻이다.</p>
<p><code>git add helloWorld.c</code>를 실행한 뒤 다시 <code>git status</code>를 확인하면, 화면에는 다음과 비슷한 내용이 나타난다.</p>
<pre><code class="language-plaintext">Changes to be committed:
  new file:   helloWorld.c
</code></pre>
<p>여기서 <strong>Changes to be committed</strong> 는 “커밋될 예정인 변경 사항”이라는 뜻이다. 그리고 <strong>new file: helloWorld.c</strong> 는 새 파일인 <code>helloWorld.c</code>가 다음 커밋에 포함될 준비가 되었다는 의미이다. 이 상태가 바로 <strong>Staged 상태</strong>이다.</p>
<p>이전 단계의 <strong>Untracked</strong> 상태와 비교하면 차이가 분명하다. Untracked 상태에서는 Git이 아직 해당 파일을 관리하지 않는다. 하지만 <code>git add</code>를 실행하면 Git은 그 파일을 다음 커밋에 포함할 대상으로 인식한다. 다만 아직 커밋이 완료된 것은 아니다. 즉, 파일이 Git 이력에 영구적으로 저장된 상태는 아니고, 커밋을 기다리는 상태이다.</p>
<p>화면에 보이는 <code>use "git rm --cached &lt;file&gt;..." to unstage</code>라는 문장은, 스테이징 영역에 올린 파일을 다시 내리고 싶을 때 사용할 수 있는 안내이다. 예를 들어 실수로 <code>helloWorld.c</code>를 <code>git add</code> 했다면 다음 명령어로 스테이징 상태를 취소할 수 있다.</p>
<pre><code class="language-plaintext">git rm --cached helloWorld.c
</code></pre>
<p>이 명령어는 파일 자체를 삭제하는 것이 아니라, Git의 스테이징 영역에서만 제거한다. 즉, <code>helloWorld.c</code> 파일은 작업 디렉토리에 그대로 남아 있지만, 다음 커밋에 포함될 예정에서는 빠지게 된다. 새 파일의 경우 다시 <strong>Untracked</strong> 상태로 돌아간다고 이해하면 된다.</p>
<p>정리하면, 새 파일을 만들면 처음에는 <strong>Untracked</strong> 상태가 된다. 이후 <code>git add helloWorld.c</code>를 실행하면 해당 파일은 <strong>Staged</strong> 상태가 되어 다음 커밋에 포함될 준비가 된다. 하지만 아직 <strong>Committed</strong> 상태는 아니므로, 실제 Git 이력에 저장하려면 이후에 <code>git commit</code>을 실행해야 한다.</p>
<pre><code class="language-c">vi helloWorld.c
git status
git add helloWorld.c
git status
git commit -m "Add helloWorld.c"
</code></pre>
<p>이 흐름에서 상태 변화는 다음과 같이 정리할 수 있다.</p>
<pre><code class="language-plaintext">Untracked → Staged → Committed
</code></pre>
<p>Git의 기본 사용법에서 이 흐름은 매우 중요하다. Git은 새 파일이 생겼다고 해서 바로 기록하지 않고, 사용자가 <code>git add</code>로 명시적으로 다음 커밋에 포함할 변경 내용을 선택하게 한다. 이 구조 덕분에 개발자는 어떤 파일을 커밋할지 직접 결정할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/cbd094ee-f4b5-42dc-bb8a-e390597e9b6c.png" alt="" style="display:block;margin:0 auto" />

<h3>Git 기본 사용법: 커밋으로 현재 상태 저장하기 (Git Basic Usage: Commit)</h3>
<p>Git에서 파일을 새로 만들고 <code>git add</code>로 스테이징 영역에 올렸다면, 다음 단계는 <strong>커밋(Commit)</strong> 이다. 커밋은 현재까지 준비된 변경 내용을 Git 저장소에 영구적인 기록으로 남기는 작업이다. 쉽게 말하면, 지금 상태를 하나의 버전으로 “박제해서 보관하는 것”이라고 이해할 수 있다.</p>
<p>커밋을 실행할 때는 보통 다음과 같이 <code>git commit</code> 명령어를 사용한다.</p>
<pre><code class="language-plaintext">git commit -m "first git commit"
</code></pre>
<p>여기서 <code>-m</code> 옵션은 <strong>커밋 메시지(Commit Message)</strong> 를 명령어 안에서 바로 작성하겠다는 의미이다. 커밋 메시지는 사람이 나중에 변경 내용을 이해할 수 있도록 남기는 설명이다. 따라서 아무 의미 없는 문장보다는, “무엇을 변경했는지”를 알 수 있는 내용을 작성하는 것이 좋다.</p>
<p>예를 들어 새 파일 <code>helloWorld.c</code>를 처음 추가한 상황이라면 다음과 같은 메시지를 사용할 수 있다.</p>
<pre><code class="language-plaintext">git commit -m "Add helloWorld.c"
</code></pre>
<p>커밋을 실행하면 Git은 스테이징 영역에 올라와 있던 변경 내용을 로컬 저장소(Local Repository)에 기록한다. 이때 화면에는 커밋 결과가 출력된다. 예시 화면에서는 다음과 같은 내용이 보인다.</p>
<pre><code class="language-c">[master (root-commit) 1c686a5] first git commit
 1 file changed, 5 insertions(+)
 create mode 100644 helloWorld.c
</code></pre>
<p>여기서 <strong>root-commit</strong> 은 이 저장소에서 처음 만들어진 커밋이라는 뜻이다. 아직 이전 커밋이 없기 때문에, 첫 번째 커밋은 루트 커밋(Root Commit)이 된다.</p>
<p><code>1 file changed</code>는 하나의 파일이 변경되었다는 의미이고, <code>5 insertions(+)</code>는 다섯 줄이 추가되었다는 의미이다. <code>create mode 100644 helloWorld.c</code>는 <code>helloWorld.c</code>라는 새 파일이 생성되었고, 일반 파일 권한 모드로 저장되었다는 뜻이다.</p>
<p>중간에 보이는 <code>1c686a5</code> 같은 알 수 없는 문자와 숫자의 조합은 <strong>커밋 해시(Commit Hash)</strong> 또는 <strong>커밋 ID(Commit ID)</strong> 의 짧은 형태이다. Git은 각 커밋을 고유하게 식별하기 위해 해시 값을 사용한다. 이 값은 커밋마다 다르게 생성되며, 나중에 특정 커밋으로 이동하거나, 특정 시점의 내용을 확인하거나, 이전 상태로 되돌릴 때 사용할 수 있다.</p>
<p>즉, 커밋 해시는 Git에서 각 버전을 구분하는 식별자(Identifier) 역할을 한다. 사람이 보기에는 복잡한 문자처럼 보이지만, Git 입장에서는 “이 커밋이 정확히 어떤 기록인가”를 구분하는 중요한 값이다.</p>
<p>커밋을 완료한 뒤 다시 상태를 확인하려면 다음 명령어를 사용한다.</p>
<pre><code class="language-plaintext">git status
</code></pre>
<p>커밋이 정상적으로 완료되었고 추가로 수정된 파일이 없다면 다음과 같은 메시지가 나타난다.</p>
<pre><code class="language-c">On branch master
nothing to commit, working tree clean
</code></pre>
<p>여기서 <strong>nothing to commit</strong> 은 현재 커밋할 변경 내용이 없다는 뜻이다. <strong>working tree clean</strong> 은 작업 디렉토리(Working Tree)가 깨끗하다는 뜻으로, 마지막 커밋 이후 변경된 파일이 없다는 의미이다.</p>
<p>즉, 파일을 수정하고 <code>git add</code>로 스테이징한 뒤 <code>git commit</code>으로 저장소에 기록했기 때문에, 현재 작업 공간에는 Git이 새로 기록해야 할 변화가 남아 있지 않은 상태이다.</p>
<p>정리하면, <code>git commit</code>은 Git에서 변경 내용을 하나의 버전으로 확정하는 명령어이다. <code>git add</code>가 다음 커밋에 포함할 파일을 준비하는 단계라면, <code>git commit</code>은 그 준비된 변경 내용을 실제 Git 이력에 저장하는 단계이다. 커밋이 만들어지면 Git은 고유한 커밋 해시를 부여하고, 이 값을 통해 나중에 특정 버전을 찾거나 되돌아갈 수 있다.</p>
<p>Git의 기본 흐름은 다음과 같이 정리할 수 있다.</p>
<pre><code class="language-plaintext">파일 생성 또는 수정 → git add → git commit → git status 확인
</code></pre>
<p>상태 변화로 보면 다음과 같다.</p>
<pre><code class="language-plaintext">Untracked 또는 Modified → Staged → Committed
</code></pre>
<p>결국 Git에서 커밋은 단순한 저장이 아니라, 프로젝트의 특정 시점을 기록하는 작업이다. 앞으로 Git을 실습할 때는 파일을 수정하고, 변경 내용을 스테이징하고, 커밋을 만든 뒤, <code>git status</code>로 현재 상태가 깨끗한지 확인하는 흐름을 반복하게 된다.</p>
<p><strong>📌추가 설명 (Additional Notes)</strong></p>
<p>커밋 메시지는 나중에 이력을 볼 때 매우 중요하다. 예를 들어 다음처럼 작성하는 것이 좋다.</p>
<pre><code class="language-c">git commit -m "Add helloWorld.c"
git commit -m "Fix login error"
git commit -m "Update README"
</code></pre>
<p>반대로 다음처럼 너무 모호한 메시지는 좋지 않다.</p>
<pre><code class="language-c">git commit -m "update"
git commit -m "fix"
git commit -m "test"
</code></pre>
<p>커밋 메시지는 “나중에 내가 봐도 이 변경이 무엇인지 알 수 있게” 작성하는 것이 가장 좋다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/13de0c27-757a-4fa6-ac78-b03717705423.png" alt="" style="display:block;margin:0 auto" />

<p>Git에서 새 파일은 처음에 <strong>Untracked</strong> 상태이다. <code>git add</code>를 하면 <strong>Staged</strong> 상태가 되고, <code>git commit</code>을 하면 저장소에 기록되어 <strong>Unmodified</strong> 상태가 된다. 이후 파일을 수정하면 <strong>Modified</strong> 상태가 되며, 다시 <code>git add</code>와 <code>git commit</code>을 거쳐 새로운 버전으로 저장된다. Git은 이 과정을 통해 어떤 변경을 기록할지 사용자가 직접 선택할 수 있게 해준다.</p>
<hr />
<h3>파일의 상태 변화 주기 (File Status Lifecycle in Git)</h3>
<p>Git에서 파일은 작업 과정에 따라 여러 상태를 오가게 된다. 대표적인 상태는 <strong>추적되지 않음(Untracked)</strong>, <strong>수정되지 않음(Unmodified)</strong>, <strong>수정됨(Modified)</strong>, <strong>스테이지됨(Staged)</strong> 이다. 이 상태 변화를 이해하면 Git의 기본 사용 흐름을 더 쉽게 따라갈 수 있다.</p>
<p>먼저 <strong>Untracked</strong> 상태는 Git이 아직 관리하지 않는 파일을 의미한다. 새 파일을 만들었지만 아직 <code>git add</code>를 하지 않았다면, Git은 그 파일을 추적 대상으로 보지 않는다. 즉, 파일은 디렉토리에 존재하지만 Git의 변경 이력에는 포함되지 않은 상태이다.</p>
<p>이 파일을 Git의 관리 대상으로 포함하려면 <code>git add</code> 명령어를 사용한다.</p>
<pre><code class="language-plaintext">git add 파일명
</code></pre>
<p>새 파일에 대해 <code>git add</code>를 실행하면 파일은 <strong>Staged</strong> 상태가 된다. 여기서 Staged는 무대(Stage)에 올려놓았다는 의미처럼 이해할 수 있다. 즉, “이 파일을 다음 커밋(Commit)에 포함하겠다”라고 표시한 상태이다.</p>
<p>그다음 <code>git commit</code>을 실행하면 Staged 상태에 있던 파일이 Git 저장소에 기록된다. 커밋이 완료되면 해당 파일은 Git이 알고 있는 최신 원본 상태가 된다. 이 상태를 <strong>Unmodified</strong> 라고 한다. Unmodified는 마지막 커밋 이후 파일이 수정되지 않았다는 뜻이다.</p>
<pre><code class="language-plaintext">git commit -m "커밋 메시지"
</code></pre>
<p>이후 사용자가 파일 내용을 다시 수정하면, Git은 해당 파일을 <strong>Modified</strong> 상태로 인식한다. Modified는 이미 Git이 추적하고 있는 파일이지만, 마지막 커밋 이후 내용이 바뀐 상태를 의미한다. 예를 들어 <code>helloWorld.c</code> 파일을 커밋한 뒤 다시 내용을 추가하거나 수정하면, 그 파일은 Modified 상태가 된다.</p>
<p>Modified 상태의 파일을 다시 다음 커밋에 포함하려면 <code>git add</code>를 실행해야 한다. 이때 <code>git add</code>는 단순히 새 파일을 추적 대상으로 등록하는 역할만 하는 것이 아니라, 수정된 파일의 현재 상태를 다음 커밋에 포함하도록 <strong>스테이징(Staging)</strong> 하는 역할을 한다.</p>
<pre><code class="language-plaintext">git add helloWorld.c
</code></pre>
<p>이렇게 하면 Modified 상태였던 파일은 다시 Staged 상태가 된다. 그리고 <code>git commit</code>을 실행하면 그 변경 내용이 새로운 커밋으로 저장되고, 파일은 다시 Unmodified 상태가 된다.</p>
<p>따라서 Git에서 기본적인 상태 변화는 다음과 같이 이해할 수 있다.</p>
<pre><code class="language-plaintext">Untracked → Staged → Unmodified → Modified → Staged → Unmodified
</code></pre>
<p>조금 더 실제 작업 흐름으로 표현하면 다음과 같다.</p>
<pre><code class="language-c">파일 생성
git add 파일명
git commit -m "첫 커밋"
파일 수정
git add 파일명
git commit -m "수정 내용 저장"
</code></pre>
<p>여기서 <strong>Remove the file</strong> 은 파일을 더 이상 Git이 추적하지 않게 만드는 상황을 의미할 수 있다. 예를 들어 추적 중인 파일을 삭제하고 그 변경을 커밋하면, Git은 해당 파일이 제거되었다는 사실을 이력에 기록한다. 반대로 새 파일을 <code>git add</code> 했다가 다시 스테이징에서 내리거나 추적 대상에서 제외하면, 새 파일은 다시 Untracked 상태로 돌아갈 수 있다.</p>
<p>중요한 점은 Git이 파일을 자동으로 무조건 저장하지 않는다는 것이다. 파일을 수정하면 Modified 상태가 되고, 그 변경을 커밋에 포함하려면 사용자가 직접 <code>git add</code>로 Staged 상태로 만들어야 한다. 그리고 <code>git commit</code>을 해야 비로소 Git의 이력에 저장된다.</p>
<p>결국 Git의 파일 상태 변화는 “작업한 내용을 바로 저장하는 방식”이 아니라, 사용자가 어떤 변경을 다음 버전으로 남길지 선택하는 구조이다. 이 구조 덕분에 여러 파일을 수정했더라도 필요한 변경만 골라 커밋할 수 있고, 변경 이력을 더 깔끔하게 관리할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/8c7eefae-2cc6-4dba-89fa-ece282b35fa1.png" alt="" style="display:block;margin:0 auto" />

<p>Git 사용자 설정이다. <a href="http://user.name"><code>user.name</code></a>은 커밋 작성자 이름이고, <a href="http://user.email"><code>user.email</code></a>은 커밋 작성자 이메일이다. <code>core.editor vim</code>은 Git이 메시지를 입력받을 때 사용할 기본 편집기를 <code>vim</code>으로 설정한 것이다. <code>init.defaultBranch heesu</code>는 앞으로 <code>git init</code>으로 새 저장소를 만들 때 기본 브랜치 이름을 <code>heesu</code>로 하겠다는 뜻이다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/7bdd80bc-5d0b-47f1-82bf-ff4f5e5f6e97.png" alt="" style="display:block;margin:0 auto" />

<p>Git 저장소 안에서 파일 상태를 확인하고, 새 파일을 Git 관리 대상으로 추가하였다. <a href="http://README.md"><code>README.md</code></a>가 처음에는 <code>Untracked</code> 상태였고, <code>git add</code> <a href="http://README.md"><code>README.md</code></a> 후에는 커밋 준비 상태인 <code>Staged</code> 상태가 된 화면이다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/0f7c5aa7-28a0-4078-aaba-13d08c5eba8b.png" alt="" style="display:block;margin:0 auto" />

<p><code>git commit -a -m 'first commit w12'</code>로 <code>.gitignore</code>와 <a href="http://README.md"><code>README.md</code></a>를 커밋했고, 이후 <code>git status</code>에서 <code>working tree clean</code>이 나와 더 이상 저장할 변경사항이 없다는 화면이다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a04f5647-4378-411f-92f8-4ee4753f9516.png" alt="" style="display:block;margin:0 auto" />

<p>echo 로 second line을 추가하니 modified: README.md가 뜬다. git diff로 바뀐 상태를 확인하는 연습을 했다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/ca300bfb-3487-4855-983d-c74ba7ab7673.png" alt="" style="display:block;margin:0 auto" />

<p>git commit이후에 git log에서 자세한 설명을 확인했다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a9e756b7-a4ca-44a0-9c7f-e8e29c9d363b.png" alt="" style="display:block;margin:0 auto" />

<p>commit 이후엔 second line이 추가된 <a href="http://README.md">README.md</a> 가 원본이 되었다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/102f4cd6-ac9d-4fe1-b257-b8658ea7167d.png" alt="" style="display:block;margin:0 auto" />

<p>checkout으로 입력하니 second line이 사라졌다. 그후 switch로 다시 second line이 돌아온것을 확인할수있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/ed250480-f2f1-42bd-8725-226efab56f94.png" alt="" style="display:block;margin:0 auto" />]]></content:encoded></item><item><title><![CDATA[Virtual Memory: Demand Paging, Page Replacement, and Thrashing]]></title><description><![CDATA[1️⃣ Virtual Memory and Demand Paging2️⃣ Page Replacement Algorithms and Thrashing3️⃣ Virtual Memory Operation Observation Practice

1️⃣ Virtual Memory and Demand Paging
가상메모리(Virtual Memory)
이번 주차에서는 ]]></description><link>https://heesu.tech/virtual-memory-demand-paging-page-replacement-and-thrashing</link><guid isPermaLink="true">https://heesu.tech/virtual-memory-demand-paging-page-replacement-and-thrashing</guid><dc:creator><![CDATA[Heesu Noh]]></dc:creator><pubDate>Sat, 23 May 2026 15:17:05 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/bf16c203-463b-4daf-a2ed-c31820c8800e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>1️⃣ Virtual Memory and Demand Paging<br />2️⃣ Page Replacement Algorithms and Thrashing<br />3️⃣ Virtual Memory Operation Observation Practice</p>
<hr />
<h1>1️⃣ Virtual Memory and Demand Paging</h1>
<h3>가상메모리(Virtual Memory)</h3>
<p>이번 주차에서는 메모리 관리(Memory Management) 중 가상메모리(Virtual Memory)를 학습한다. 프로세스(Process)가 실행되려면 메모리가 필요하지만, 실제 물리 메모리(Physical Memory)는 크기가 제한되어 있다. 따라서 모든 프로그램을 한 번에 메모리에 올려 실행하기는 어렵다.</p>
<p>이를 해결하기 위해 운영체제(Operating System)는 가상메모리를 사용한다. 가상메모리는 프로그램 전체가 아니라, 실행에 필요한 부분만 메모리에 올려 사용하는 방식이다. 대표적인 방법이 <code>요구 페이징(Demand Paging)</code>이다. 필요한 페이지(Page)가 메모리에 없을 때는 <code>페이지 부재(Page Fault)</code>가 발생하고, 운영체제가 해당 페이지를 메모리로 가져온다.</p>
<p>또한 메모리가 부족하면 기존 페이지 중 하나를 교체해야 한다. 이를 위해 <code>페이지 대치 알고리즘(Page Replacement Algorithm)</code>이 사용된다. 대표적인 알고리즘으로는 먼저 들어온 페이지를 교체하는 <code>FIFO(First-In, First-Out)</code>와, 가장 오래 사용되지 않은 페이지를 교체하는 <code>LRU(Least Recently Used)</code>가 있다.</p>
<p>페이지 교체가 너무 자주 발생하면 실제 프로그램 실행보다 페이지를 넣고 빼는 데 시간이 더 많이 걸릴 수 있다. 이런 현상을 <code>스래싱(Thrashing)</code>이라고 하며, 시스템 성능을 크게 떨어뜨린다.</p>
<p>이번 주차의 핵심은 가상메모리의 필요성, 요구 페이징, 페이지 부재, 페이지 대치 알고리즘, 스래싱의 흐름을 연결해서 이해하는 것이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f721f260-857a-4879-95eb-8ae2cecc2a2f.png" alt="" style="display:block;margin:0 auto" />

<h3>가상메모리(Virtual Memory)의 필요성 예제</h3>
<p>가상메모리(Virtual Memory)의 필요성은 책상과 책장의 예시로 쉽게 이해할 수 있다. 집에서 공부할 때 책상 위에는 지금 당장 사용하는 책이나 노트만 올려두고, 나머지 책들은 책장에 보관해두는 경우가 많다.</p>
<p>만약 모든 책을 한꺼번에 책상 위에 올려둔다면 책상이 금방 복잡해지고, 오히려 필요한 책을 찾거나 사용하는 데 불편함이 생길 수 있다. 책상 공간은 제한되어 있기 때문에, 현재 필요한 자료만 올려두는 것이 더 효율적이다.</p>
<p>컴퓨터의 메모리(Memory)도 이와 비슷하다. 물리 메모리(Physical Memory)의 크기는 제한되어 있기 때문에, 프로그램 전체를 한 번에 메모리에 올리는 것은 비효율적일 수 있다. 그래서 운영체제(Operating System)는 실행에 필요한 부분만 메모리에 올리고, 나머지는 보조기억장치(Secondary Storage)에 두었다가 필요할 때 가져온다.</p>
<p>이처럼 <strong>필요한 것만 선택적으로 가져와 사용하는 방식</strong>이 가상메모리의 핵심 개념이다. 가상메모리는 제한된 물리 메모리를 더 효율적으로 사용하게 해주며, 여러 프로그램이 동시에 실행될 수 있도록 돕는다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/0de9614e-dd17-4107-ad4b-6f3c67395685.png" alt="" style="display:block;margin:0 auto" />

<h3>필요한 데이터 가져오기(Demand Paging)</h3>
<p>프로그램은 실행되기 위해 메모리(Memory)에 적재되어야 한다. 하지만 실제 물리 메모리(Physical Memory)보다 더 큰 프로그램을 실행해야 하는 경우도 있다. 이때 프로그램 전체를 한 번에 메모리에 올리는 대신, 실행에 필요한 부분만 선택적으로 올려 사용할 수 있다.</p>
<p>이 방식이 가능하려면, 실행 중 필요한 데이터가 메모리에 있는지 확인해야 한다. 만약 필요한 데이터가 메모리에 올라와 있지 않다면 <code>페이지 부재(Page Fault)</code>가 발생한다. 그러면 운영체제(Operating System)는 해당 데이터를 보조기억장치(Secondary Storage)에서 찾아 메모리로 가져온 뒤, 프로그램 실행을 계속 진행한다.</p>
<p>따라서 이번 내용의 핵심 질문은 두 가지로 정리할 수 있다. 첫 번째는 <strong>왜 모든 내용을 한 번에 메모리에 올리지 않는가</strong>이고, 두 번째는 <strong>필요한 데이터를 어떻게 가져와 사용할 것인가</strong>이다. 이 두 질문을 중심으로 가상메모리(Virtual Memory)의 필요성과 동작 방식을 이해해야 한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f04f96da-f6ba-4762-bcc8-ad8b304ece5b.png" alt="" style="display:block;margin:0 auto" />

<h3>가상메모리(Virtual Memory)의 필요성</h3>
<p>이번 차시에서는 가상메모리(Virtual Memory)의 동작을 이해하기 위해, 프로그램을 필요한 부분만 메모리에 올려 실행하는 방식과 실행 중 필요한 데이터를 가져오는 과정을 살펴본다.</p>
<p>프로그램(Program)이 실행되기 위해서는 먼저 메모리(Memory)에 적재되어야 한다. 하지만 여기서 문제가 발생한다. 프로그램의 크기가 점점 커지면서 실제 물리 메모리(Physical Memory)보다 더 큰 프로그램도 실행해야 하는 상황이 생긴다.</p>
<p>또한 컴퓨터에서는 하나의 프로그램만 실행되는 것이 아니라, 여러 프로그램이 동시에 실행되는 경우가 많다. 즉, 메모리의 크기는 제한되어 있는데 메모리를 필요로 하는 프로그램의 수와 크기는 계속 늘어나는 상황이 된다.</p>
<p>이러한 문제를 해결하기 위해 운영체제(Operating System)는 프로그램 전체를 한 번에 메모리에 올리지 않고, 실행에 필요한 부분만 선택적으로 메모리에 올리는 가상메모리 방식을 사용한다. 가상메모리는 제한된 물리 메모리를 효율적으로 사용하고, 여러 프로그램이 동시에 실행될 수 있도록 돕는 중요한 메모리 관리 기법이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/ba7665e6-3154-49d2-b60f-60ced4a3f2bf.png" alt="" style="display:block;margin:0 auto" />

<h3>기존 메모리 적재 방식의 한계(Limitations of Traditional Memory Loading)</h3>
<p>기존 방식처럼 프로그램(Program) 전체를 메모리(Memory)에 한 번에 올려 실행한다고 생각해보자. 이 경우 실제로 지금 당장 사용하지 않는 부분까지 함께 메모리에 적재되기 때문에, 불필요하게 메모리 공간을 차지하게 된다.</p>
<p>더 큰 문제는 프로그램의 크기가 물리 메모리(Physical Memory)보다 큰 경우이다. 프로그램 중 일부만 사용하면 실행할 수 있는 상황이라도, 전체를 한 번에 메모리에 올려야 한다면 실행 자체가 불가능해질 수 있다.</p>
<p>결국 문제의 핵심은 <strong>필요한 만큼만 사용하면 되는데, 전체를 모두 메모리에 올려야 하는 구조</strong>에 있다. 이러한 방식은 메모리 낭비(Memory Waste)를 발생시키고, 큰 프로그램의 실행을 제한한다.</p>
<p>따라서 운영체제(Operating System)는 프로그램 전체가 아니라 실행에 필요한 부분만 선택적으로 메모리에 올리는 방법이 필요하다. 이 문제를 해결하기 위해 등장한 개념이 <code>가상메모리(Virtual Memory)</code>이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/74e4cba2-4a4b-47fa-be1a-f239730eb2e9.png" alt="" style="display:block;margin:0 auto" />

<h3>해결 방안: 가상메모리(Virtual Memory)</h3>
<p>기존 메모리 적재 방식의 한계를 해결하기 위한 방법이 <code>가상메모리(Virtual Memory)</code>이다. 가상메모리의 핵심은 프로그램(Program) 전체를 한 번에 메모리(Memory)에 올리는 것이 아니라, 실행에 필요한 부분만 선택적으로 메모리에 올려 사용하는 것이다.</p>
<p>이렇게 하면 지금 당장 사용하지 않는 부분은 굳이 물리 메모리(Physical Memory)를 차지하지 않는다. 그 결과 제한된 메모리 공간을 더 효율적으로 사용할 수 있다.</p>
<p>또한 프로그램의 전체 크기가 실제 메모리보다 크더라도, 필요한 부분을 나누어 메모리에 올리면서 실행할 수 있다. 즉, 가상메모리는 메모리 낭비를 줄이고, 더 큰 프로그램이나 여러 프로그램을 동시에 실행할 수 있도록 도와주는 메모리 관리(Memory Management) 방식이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/3f928a3e-ef7f-4d17-bf95-7d057f61f173.png" alt="" style="display:block;margin:0 auto" />

<h3>가상메모리(Virtual Memory)의 동작 구조</h3>
<p>그림을 보면 각 프로세스(Process)가 요구하는 메모리 크기가 상당히 크다는 것을 알 수 있다. 예를 들어 프로세스 A(Process A)는 4GB의 메모리를 요구하고, 다른 프로세스들도 각각 일정한 크기의 메모리를 필요로 한다.</p>
<p>하지만 실제 물리 메모리(Physical Memory)는 이 모든 프로세스를 동시에 그대로 담기에는 부족한 경우가 많다. 따라서 운영체제(Operating System)는 메모리 관리자(Memory Manager) 역할을 하면서, 각 프로세스를 전체 그대로 메모리에 올리지 않는다.</p>
<p>대신 실행에 필요한 부분만 선택적으로 물리 메모리에 올리고, 당장 사용하지 않는 나머지 부분은 저장장치(Storage), 즉 보조기억장치(Secondary Storage)에 두고 관리한다.</p>
<p>즉, 프로세스가 요구하는 전체 메모리 크기와 실제 물리 메모리에 올라가는 크기는 다를 수 있다. 이 차이를 운영체제가 조정해주는 구조가 바로 **가상메모리(Virtual Memory)**이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/c19b6607-2d5a-4953-94a0-9d8a674854f6.png" alt="" style="display:block;margin:0 auto" />

<h3>가상메모리(Virtual Memory)의 개념</h3>
<p>가상메모리(Virtual Memory)의 가장 중요한 특징은 프로그램(Program) 전체가 아니라 일부만 메모리(Memory)에 올라간 상태에서도 실행될 수 있다는 점이다. 즉, 지금 당장 필요한 부분만 물리 메모리(Physical Memory)에 올라가고, 나머지 부분은 메모리에 없는 상태로 존재한다.</p>
<p>여기서 메모리에 없다는 것은 데이터가 사라졌다는 의미가 아니다. 당장 사용하지 않는 부분은 저장장치(Storage), 즉 보조기억장치(Secondary Storage)에 유지되어 있다가 필요한 시점에 다시 메모리로 올라온다.</p>
<p>중요한 것은 프로그램이 바라보는 관점과 실제 시스템 내부의 구조가 다르다는 점이다. 프로그램 입장에서는 전체 메모리 공간이 하나의 연속된 공간처럼 보인다. 즉, 프로그램은 자신이 필요한 메모리 전체를 끊김 없이 사용할 수 있다고 인식한다.</p>
<p>하지만 실제 시스템 내부에서는 물리 메모리에 일부만 올라와 있고, 나머지는 저장장치에 나뉘어 저장되어 있다. 운영체제(Operating System)는 이 과정을 관리하면서 필요한 부분을 메모리에 올리고, 필요하지 않은 부분은 저장장치에 두는 방식으로 동작한다.</p>
<p>결국 가상메모리는 프로그램에게는 연속적인 메모리 공간을 제공하는 것처럼 보이게 하면서, 실제 내부에서는 필요한 부분만 나누어 관리하는 메모리 관리(Memory Management) 방식이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/34c5c560-ce7c-4102-974a-44bfd33f2b82.png" alt="" style="display:block;margin:0 auto" />

<h3>요구 페이징(Demand Paging)의 개념</h3>
<p>앞에서 살펴본 것처럼 가상메모리(Virtual Memory)에서는 프로그램(Program) 전체가 아니라 일부만 메모리(Memory)에 올라간 상태에서도 실행될 수 있다.</p>
<p>그렇다면 메모리에 올라와 있지 않은 나머지 부분은 언제, 어떻게 사용될까?</p>
<p>프로그램은 처음부터 필요한 모든 내용을 메모리에 올리지 않고 실행을 시작한다. 하지만 실행이 진행되다 보면 현재 메모리에 없는 코드(Code)나 데이터(Data)가 필요해질 수 있다. 이때 운영체제(Operating System)는 필요한 부분을 그 시점에 메모리로 가져온다.</p>
<p>이처럼 처음부터 모든 페이지(Page)를 메모리에 올리는 것이 아니라, 실제로 필요해졌을 때 해당 페이지를 메모리에 적재하는 방식을 <code>요구 페이징(Demand Paging)</code>이라고 한다.</p>
<p>즉, 요구 페이징은 “필요할 때 가져온다”는 개념을 바탕으로 한다. 이를 통해 메모리 사용량을 줄이고, 프로그램 실행에 필요한 부분만 효율적으로 관리할 수 있게 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/b16a47d2-441b-4faf-834f-30c1443b8318.png" alt="" style="display:block;margin:0 auto" />

<h3>요구 페이징(Demand Paging)의 핵심 특징</h3>
<p><code>요구 페이징(Demand Paging)</code>은 프로그램(Program)을 시작할 때 모든 내용을 미리 메모리(Memory)에 올려두는 방식이 아니다. 실행 중에 실제로 필요한 순간이 되었을 때, 해당 부분을 메모리로 가져와 사용하는 방식이다.</p>
<p>즉, 어떤 페이지(Page)가 필요해지기 전까지는 물리 메모리(Physical Memory)에 올리지 않는다. 그러다가 프로그램이 해당 페이지에 접근하려는 시점에 운영체제(Operating System)가 그 페이지를 메모리로 가져오고, 실행을 계속 이어가게 한다.</p>
<p>이 방식에서는 실행 과정에 따라 필요한 데이터(Data)나 코드(Code)만 선택적으로 메모리에 올라간다. 따라서 프로그램 전체가 항상 메모리에 존재할 필요는 없다.</p>
<p>하지만 프로그램 입장에서는 자신이 사용하는 전체 메모리 공간이 하나의 연속된 공간처럼 보인다. 실제 시스템 내부에서는 일부만 메모리에 있고 나머지는 저장장치(Storage)에 있는 구조이지만, 프로그램은 이 차이를 직접 인식하지 않고 실행을 계속한다.</p>
<p>결국 요구 페이징은 필요한 부분만 필요할 때 메모리에 올려 메모리를 효율적으로 사용하는 가상메모리(Virtual Memory)의 핵심 동작 방식이라고 볼 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/79955469-9380-4355-9735-8d0da0520789.png" alt="" style="display:block;margin:0 auto" />

<h3>요구 페이징(Demand Paging)의 동작 과정</h3>
<p>요구 페이징(Demand Paging)은 프로그램(Program)이 실행되는 중 특정 주소(Address)에 접근할 때 동작한다. 먼저 운영체제(Operating System)는 해당 주소에 필요한 데이터(Data)가 현재 물리 메모리(Physical Memory)에 올라와 있는지 확인한다.</p>
<p>만약 해당 데이터가 메모리에 존재한다면, 프로그램은 바로 그 데이터를 사용하고 실행을 계속 이어간다. 하지만 필요한 데이터가 메모리에 없다면, 즉시 사용할 수 없기 때문에 운영체제가 저장장치(Storage)에서 해당 데이터를 찾아 물리 메모리로 가져온다.</p>
<p>그 후 운영체제는 원래 실행하려던 명령(Instruction)을 다시 수행하게 하고, 프로그램은 중단되었던 흐름을 이어서 진행한다.</p>
<p>정리하면, 요구 페이징은 프로그램이 특정 주소에 접근했을 때 필요한 데이터가 메모리에 있는지 확인하고, 없을 경우 저장장치에서 가져와 메모리에 적재한 뒤 다시 실행을 이어가는 방식이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f1eb0e73-311c-4c84-b853-a9bb05f97254.png" alt="" style="display:block;margin:0 auto" />

<h3>요구 페이징(Demand Paging)의 동작 관점</h3>
<p>요구 페이징(Demand Paging)은 <code>메인 메모리(Main Memory)</code>와 <code>보조기억장치(Secondary Storage)</code> 사이에서 데이터를 주고받으며 동작한다. 그림에서 왼쪽은 메인 메모리, 오른쪽은 보조기억장치를 나타낸다.</p>
<p>프로그램 A(Program A)와 프로그램 B(Program B)가 실행 중이지만, 두 프로그램의 전체 내용이 모두 메모리에 올라와 있는 것은 아니다. 실제로는 실행에 필요한 일부만 메인 메모리에 올라와 있고, 나머지 부분은 보조기억장치에 저장되어 있다.</p>
<p>프로그램 실행이 진행되다가 현재 메모리에 없는 데이터(Data)가 필요해지면, 운영체제(Operating System)는 보조기억장치에 있는 해당 데이터를 메모리로 가져온다. 이때 메모리에 빈 공간이 부족하면 기존에 메모리에 있던 일부 데이터가 보조기억장치로 나가고, 새로 필요한 데이터가 그 자리를 대신 차지한다.</p>
<p>즉, 요구 페이징은 메모리와 보조기억장치 사이에서 필요한 데이터가 들어오고 나가는 교체 과정을 통해 프로그램 실행을 이어가는 방식이다. 이 그림은 요구 페이징이 실제로 이러한 데이터 교체(Page Replacement) 과정을 통해 동작한다는 점을 보여준다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f6234734-23eb-4f53-afa6-0047578c4b44.png" alt="" style="display:block;margin:0 auto" />

<h3>적재 방식 비교(Loading Method Comparison)</h3>
<p><code>페이지(Page)</code>는 프로그램(Program)을 일정한 크기로 나눈 단위이다. 즉, 프로그램 전체를 하나의 덩어리로 관리하는 것이 아니라 여러 개의 작은 블록(Block)으로 나누어 각각 메모리(Memory)에 올리고 관리하는 구조라고 볼 수 있다.</p>
<p>페이지를 메모리에 올리는 방식은 크게 <code>선적재(Preloading)</code>와 <code>요구 페이징(Demand Paging)</code>으로 비교할 수 있다.</p>
<p><code>선적재(Preloading)</code>는 프로그램을 실행하기 전에 필요한 페이지를 미리 메모리에 올리는 방식이다. 이 방식은 초기에 많은 페이지를 한꺼번에 올려야 하므로 실행 준비 시간이 길어질 수 있다. 또한 실제로 사용하지 않는 페이지까지 메모리에 포함될 수 있어 메모리 낭비가 발생할 수 있다.</p>
<p>반면 <code>요구 페이징(Demand Paging)</code>은 프로그램 실행 중 필요한 페이지가 생겼을 때 그때그때 메모리에 올리는 방식이다. 필요한 페이지만 선택적으로 가져오기 때문에 초기 적재 부담이 줄어들고, 메모리를 더 효율적으로 사용할 수 있다.</p>
<p>따라서 두 방식의 핵심 차이는 페이지를 언제 메모리에 올리느냐에 있다. 선적재는 실행 전에 미리 올리는 방식이고, 요구 페이징은 실행 중 실제로 필요해진 시점에 올리는 방식이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/bc239ddd-86c0-4fed-a3a5-d35ae4491aec.png" alt="" style="display:block;margin:0 auto" />

<h3>페이지 부재(Page Fault)의 개념</h3>
<p><code>요구 페이징(Demand Paging)</code>에서 중요한 개념 중 하나는 <code>페이지 부재(Page Fault)</code>이다. 프로그램(Program)이 실행되는 중 특정 주소(Address)의 페이지(Page)에 접근했을 때, <mark class="bg-yellow-200 dark:bg-yellow-500/30">해당 페이지가 현재 메모리(Memory)에 없는 경우</mark>가 있다. 이때 페이지 부재가 발생한다.</p>
<p>운영체제(Operating System)는 먼저 페이지 테이블(Page Table)을 통해 해당 페이지가 메모리에 있는지 확인한다. 페이지 테이블에는 각 페이지가 현재 메모리에 올라와 있는지를 나타내는 값이 저장되어 있다. 이 값이 1이면 페이지가 메모리에 존재한다는 의미이고, 0이면 아직 메모리에 없다는 의미이다.</p>
<p>페이지가 메모리에 있으면 프로그램은 바로 해당 페이지에 접근할 수 있다. 반대로 페이지가 메모리에 없다면 페이지 부재가 발생하고, 운영체제는 필요한 페이지를 저장장치(Storage)에서 가져와 메모리에 적재한다.</p>
<p>여기서 중요한 점은 페이지 부재를 단순한 오류(Error)로 보면 안 된다는 것이다. 요구 페이징에서는 처음부터 프로그램 전체가 아니라 일부 페이지만 메모리에 올려두기 때문에, 실행 중 메모리에 없는 페이지가 필요해지는 상황은 자연스럽게 발생한다.</p>
<p>따라서 페이지 부재는 문제가 생겼다는 의미라기보다, 이제 해당 페이지가 필요하다는 신호라고 볼 수 있다. 즉, 페이지 부재는 운영체제가 필요한 페이지를 메모리에 가져오도록 만드는 정상적인 동작 과정의 일부이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/51108fda-bdff-432b-9c66-28526411c765.png" alt="" style="display:block;margin:0 auto" />

<h3>페이지 부재(Page Fault)와 타당 비트(Valid Bit)</h3>
<p>페이지 부재(Page Fault)는 프로그램(Program)이 특정 페이지(Page)에 접근하려고 할 때, 해당 페이지가 현재 메모리(Memory)에 없는 경우 발생한다. 이를 확인하기 위해 운영체제(Operating System)는 <code>페이지 테이블(Page Table)</code>을 사용한다.</p>
<p>그림을 보면 논리 주소(Logical Address)에서 시작해 페이지 테이블을 거쳐 물리 메모리(Physical Memory)로 연결되는 구조를 확인할 수 있다. 이때 페이지 테이블에는 각 페이지가 현재 메모리에 올라와 있는지를 표시하는 값이 포함되어 있다. 이 값을 <code>타당 비트(Valid Bit)</code> 또는 <code>비타당 비트(Invalid Bit)</code>라고 한다.</p>
<p>타당 비트가 1이면 해당 페이지가 현재 메모리에 존재한다는 의미이다. 따라서 프로그램은 바로 물리 메모리에 접근하여 실행을 이어갈 수 있다. 반대로 비타당 비트가 0이면 해당 페이지가 아직 메모리에 올라와 있지 않다는 의미이다.</p>
<p>이 경우에는 해당 페이지를 바로 사용할 수 없기 때문에 페이지 부재가 발생한다. 그러면 운영체제는 필요한 페이지를 저장장치(Storage)에서 찾아 메모리로 가져오는 과정을 수행한다.</p>
<p>즉, 타당 비트와 비타당 비트는 각 페이지가 현재 메모리에 존재하는지 판단하고, 프로그램이 해당 페이지에 접근할 수 있는지를 결정하는 기준이라고 볼 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/afdacd44-8cf9-4124-9353-6c95f9ea1c60.png" alt="" style="display:block;margin:0 auto" />

<h3>페이지 부재(Page Fault) 발생 시점</h3>
<p>페이지 부재(Page Fault)는 프로그램(Program)이 실행되는 과정에서 자연스럽게 여러 번 발생할 수 있다. 요구 페이징(Demand Paging)에서는 처음부터 모든 페이지(Page)를 메모리(Memory)에 올려두지 않기 때문이다.</p>
<p>먼저 프로그램을 처음 실행했을 때 페이지 부재가 발생할 수 있다. 아직 필요한 페이지들이 모두 메모리에 올라와 있지 않은 상태이기 때문에, 처음 접근하는 페이지에서 페이지 부재가 발생한다.</p>
<p>또한 실행이 진행되면서 이전에 사용하지 않았던 새로운 코드(Code)나 데이터(Data) 영역에 접근할 때도 페이지 부재가 발생할 수 있다. 프로그램의 실행 흐름이 바뀌거나 다른 기능을 사용하게 되면, 그 순간 새로운 페이지가 필요해질 수 있기 때문이다.</p>
<p>결국 페이지 부재는 한 번만 발생하는 현상이 아니다. 프로그램이 실행되는 동안 필요한 페이지가 메모리에 없을 때마다 반복적으로 발생할 수 있으며, 이는 요구 페이징 구조에서 나타나는 정상적인 동작 과정이라고 볼 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/7313567d-d6d7-4458-9af2-847678797a79.png" alt="" style="display:block;margin:0 auto" />

<h3>페이지 부재(Page Fault) 처리 흐름</h3>
<p>페이지 부재(Page Fault)가 발생하면 운영체제(Operating System)가 개입하여 처리를 시작한다. 먼저 프로그램(Program)이 특정 주소(Address)에 접근하면, 운영체제는 페이지 테이블(Page Table)을 통해 해당 페이지(Page)가 메모리(Memory)에 있는지 확인한다.</p>
<p>이때 해당 페이지가 메모리에 없다면 <code>페이지 부재 처리</code>가 시작된다. 운영체제는 저장장치(Storage)에 있는 해당 페이지를 찾아 읽어오고, 그 페이지를 물리 메모리(Physical Memory)에 적재한다.</p>
<p>페이지가 메모리에 올라오면 운영체제는 중단되었던 명령(Instruction)을 다시 실행한다. 실제 내부 동작에서는 프로그램 실행이 잠시 멈췄다가 다시 이어지지만, 프로그램 입장에서는 중단 없이 연속적으로 실행되는 것처럼 보인다.</p>
<p>정리하면, 페이지 부재 처리 흐름은 페이지 테이블 확인, 운영체제 개입, 저장장치에서 페이지 읽기, 메모리 적재, 중단된 명령 재실행의 과정으로 이루어진다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/856afb77-fa61-41a6-83f1-7359b6b189fb.png" alt="" style="display:block;margin:0 auto" />

<h3>페이지 부재(Page Fault) 처리 흐름 그림</h3>
<p>그림은 페이지 부재(Page Fault)가 발생했을 때 처리되는 과정을 단계별로 보여준다.</p>
<p>먼저 프로세스(Process)가 실행되는 중 1️⃣논리 주소(Logical Address)를 통해 특정 페이지(Page)에 접근을 시도한다. 이때 운영체제(Operating System)는 2️⃣페이지 테이블(Page Table)을 확인하여 해당 페이지가 현재 메모리(Memory)에 있는지 판단한다.</p>
<p>만약 해당 페이지가 메모리에 없다면 3️⃣페이지 부재가 발생한다. 그러면 운영체제가 개입하여 메모리 안에서 4️⃣비어 있는 공간을 찾고, 5️⃣저장장치(Storage)에 있는 해당 페이지를 가져와 그 공간에 적재한다.</p>
<p>6️⃣페이지가 메모리에 올라오면 7️⃣페이지 테이블의 정보도 새롭게 수정된다. 이후 운영체제는 중단되었던 명령(Instruction)을 8️⃣다시 실행하여 프로세스가 계속 진행되도록 한다.</p>
<p>정리하면 페이지 부재 처리 흐름은 논리 주소 접근, 페이지 테이블 확인, 페이지 부재 발생, 운영체제 개입, 페이지 적재, 페이지 테이블 수정, 명령 재실행의 순서로 이해할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/b692bfb8-53cd-4090-af3b-109ab62d4658.png" alt="" style="display:block;margin:0 auto" />

<h3>요구 페이징(Demand Paging)의 동작과 핵심 결과</h3>
<p>요구 페이징(Demand Paging)에서는 프로그램(Program)이 특정 페이지(Page)에 접근할 때마다 해당 페이지가 메모리(Memory)에 있는지 확인한다. 페이지가 이미 메모리에 있다면 프로그램 실행은 바로 이어진다.</p>
<p>반대로 해당 페이지가 메모리에 없다면 <code>페이지 부재(Page Fault)</code>가 발생한다. 이 경우 운영체제(Operating System)는 저장장치(Storage)에서 필요한 페이지를 가져와 메모리에 적재하고, 중단되었던 명령(Instruction)을 다시 실행한다.</p>
<p>이 과정이 반복되면서 실제로 필요한 페이지들만 메모리에 유지된다. 따라서 프로그램 전체를 한 번에 메모리에 올리지 않아도 실행할 수 있고, 메모리 낭비를 줄일 수 있다.</p>
<p>즉, 요구 페이징은 필요한 부분만 메모리에 유지하면서도 프로그램 실행이 계속 이어지도록 해주는 가상메모리(Virtual Memory)의 핵심 동작 방식이다.</p>
<p>지금까지는 가상메모리의 개념, 요구 페이징의 동작 방식, 그리고 페이지 부재가 발생하고 처리되는 과정을 살펴보았다.</p>
<hr />
<h1>2️⃣ Page Replacement Algorithms and Thrashing</h1>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/270d5e75-65cc-4441-ae66-666d0da50556.png" alt="" style="display:block;margin:0 auto" />

<h3>페이지 대치(Page Replacement)의 필요성</h3>
<p>이번에는 메모리에 올라온 페이지(Page)를 어떤 기준으로 교체할지 결정하는 <code>페이지 대치 알고리즘(Page Replacement Algorithm)</code>과 <code>스레싱(Thrashing)</code> 현상에 대해 살펴본다.</p>
<p><code>요구 페이징(Demand Paging)</code>에서는 프로그램 실행 중 필요한 페이지를 계속 메모리(Memory)로 가져온다. 하지만 여기서 문제가 발생한다. 메모리에 올릴 수 있는 페이지의 개수, 즉 프레임(Frame)의 수는 물리 메모리(Physical Memory)의 크기에 의해 결정되기 때문에 무한히 늘릴 수 없다.</p>
<p>따라서 메모리에 항상 여유 공간이 있는 것은 아니다. 이미 대부분의 공간이 사용 중인 상태에서 새로운 페이지를 메모리에 올려야 하는 상황이 자주 발생할 수 있다.</p>
<p>이때 새로운 페이지를 적재하려면, 기존에 메모리에 올라와 있던 페이지 중 하나를 제거해야 한다. 즉, 어떤 페이지를 내보내고 어떤 페이지를 계속 유지할 것인지 선택해야 하는 문제가 생긴다.</p>
<p>이처럼 제한된 메모리 공간에서 새로운 페이지를 적재하기 위해 기존 페이지를 교체해야 하며, 이때 필요한 기준이 바로 <code>페이지 대치 알고리즘</code>이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a76211fc-b052-42d2-bbb0-a4f6bc69bc6e.png" alt="" style="display:block;margin:0 auto" />

<h3>페이지 대치(Page Replacement)의 필요성 그림</h3>
<p>이 그림은 새로운 페이지(Page)를 메모리(Memory)에 올려야 할 때, 왜 <code>페이지 대치(Page Replacement)</code>가 필요한지를 보여준다.</p>
<p>현재 물리 메모리(Physical Memory)에는 여러 페이지가 이미 적재되어 있다. 각 사용자 또는 프로세스(Process)는 자신만의 논리 주소 공간(Logical Address Space)과 페이지 테이블(Page Table)을 가지고 있으며, 페이지 테이블을 통해 어떤 페이지가 메모리에 올라와 있는지 확인할 수 있다.</p>
<p>예를 들어 특정 프로세스가 load M 명령을 실행한다고 하면, 이는 M 페이지에 있는 데이터를 사용하려는 동작이다. 그런데 페이지 테이블을 확인했을 때 M 페이지에 프레임 번호(Frame Number)가 없고 비타당 상태(Invalid State)로 표시되어 있다면, 현재 메모리에 없는 페이지라는 의미가 된다.</p>
<p>이 경우 M 페이지가 필요해졌기 때문에 페이지 부재(Page Fault)가 발생한다. 운영체제(Operating System)는 M 페이지를 보조기억장치(Secondary Storage)에서 가져와 물리 메모리에 적재해야 한다.</p>
<p>하지만 물리 메모리에 빈 프레임(Free Frame)이 없다면, 이미 메모리에 올라와 있는 페이지 중 하나를 제거해야 한다. 그리고 그 자리에 새로 필요한 M 페이지를 올리게 된다.</p>
<p>즉, 이 그림은 각 프로세스가 논리 주소 공간과 페이지 테이블을 따로 가지고 있어도, 실제 물리 메모리는 공유되기 때문에 새로운 페이지를 올리기 위해 기존 페이지를 교체해야 하는 상황이 발생한다는 것을 보여준다. 이때 어떤 페이지를 내보낼지 결정하는 기준이 페이지 <code>대치 알고리즘(Page Replacement Algorithm)</code>이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/d793d6c9-fef4-443c-931a-db5680828e14.png" alt="" style="display:block;margin:0 auto" />

<h3>페이지 대치(Page Replacement)의 개념</h3>
<p>페이지 대치(Page Replacement)는 메모리에 빈 공간이 없을 때, 새로운 페이지(Page)를 올리기 위해 기존 페이지 중 하나를 제거하고 그 자리에 필요한 페이지를 적재하는 과정이다.</p>
<p>새로운 페이지를 메모리(Memory)에 올려야 하는데 빈 프레임(Free Frame)이 없다면, 운영체제(Operating System)는 현재 메모리에 있는 페이지 중 하나를 선택해야 한다. 이때 선택되어 메모리에서 제거되는 페이지를 <code>희생 페이지(Victim Page)</code>라고 한다.</p>
<p>희생 페이지는 메모리에서 제거되고, 필요한 경우 저장장치(Storage)로 이동한다. 그 다음 운영체제는 보조기억장치(Secondary Storage)에서 새로 필요한 페이지를 읽어와 메모리에 적재한다.</p>
<p>마지막으로 페이지 테이블(Page Table)을 갱신하여 새 페이지가 어느 프레임(Frame)에 들어갔는지 반영한다. 즉, 페이지 대치는 기존 페이지를 하나 내보내고, 새로운 페이지를 그 자리에 올린 뒤, 변경된 상태를 페이지 테이블에 기록하는 과정이라고 이해할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/16e9604e-f240-4fc2-87aa-e697f4644ac6.png" alt="" style="display:block;margin:0 auto" />

<h3>페이지 대치(Page Replacement) 개념 그림</h3>
<p>이 그림은 페이지 대치(Page Replacement)가 실제로 어떤 흐름으로 이루어지는지를 보여준다. 메모리(Memory)에 빈 공간이 없는 상태에서 새로운 페이지(Page)를 올려야 한다면, 먼저 현재 메모리에 있는 페이지 중 하나를 <code>희생 페이지(Victim Page)</code>로 선택해야 한다.</p>
<p>1번 과정에서는 기존 페이지 중 하나가 희생 페이지로 선택되어 메모리 밖으로 나간다. 이때 어떤 페이지가 선택되는지는 고정되어 있지 않고, <code>페이지 대치 알고리즘(Page Replacement Algorithm)</code>에 따라 달라진다. 예를 들어 M 페이지가 희생 페이지로 선택되었다면, M 페이지는 메모리에서 제거되어 저장장치(Storage)로 이동한다.</p>
<p>2번 과정에서는 M 페이지가 더 이상 메모리에 없다는 사실을 반영하기 위해 페이지 테이블(Page Table)의 상태가 변경된다. 즉, 해당 페이지의 정보가 비타당 상태(Invalid State)로 수정된다.</p>
<p>3번 과정에서는 현재 필요한 페이지를 저장장치에서 읽어와 메모리로 가져온다. 예를 들어 A 페이지가 필요하다면, 앞에서 비워진 프레임(Frame)에 A 페이지가 적재된다.</p>
<p>마지막 4번 과정에서는 새로 올라온 A 페이지의 위치가 반영되도록 페이지 테이블이 갱신된다. 즉, A 페이지가 어느 프레임에 들어갔는지 기록하고, 해당 페이지를 타당 상태(Valid State)로 변경한다.</p>
<p>결국 페이지 대치는 기존 페이지를 하나 내보내고, 그 자리에 새로운 페이지를 올린 뒤, 변경된 내용을 페이지 테이블에 반영하는 과정이라고 이해할 수 있겠다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f7b18635-74f6-473d-9c5e-b7fc1007212d.png" alt="" style="display:block;margin:0 auto" />

<h3>페이지 대치 알고리즘(Page Replacement Algorithm)의 목표</h3>
<p>페이지 대치(Page Replacement)가 필요한 상황을 이해했다면, 다음으로 중요한 질문은 어떤 페이지(Page)를 선택해서 내보낼 것인가이다. 어떤 페이지를 희생 페이지(Victim Page)로 선택하느냐에 따라 전체 시스템 성능이 크게 달라질 수 있다.</p>
<p><code>페이지 대치 알고리즘(Page Replacement Algorithm)</code>은 기본적으로 페이지 부재(Page Fault)가 발생하는 횟수를 줄이는 것을 목표로 한다. 페이지 부재가 자주 발생하면 운영체제(Operating System)가 저장장치(Storage)에서 페이지를 다시 가져와야 하므로 실행 속도가 느려질 수 있다.</p>
<p>특히 이미 제거했던 페이지가 곧바로 다시 필요해지는 상황이 반복되면 불필요한 페이지 교체가 계속 발생한다. 이런 경우 메모리(Memory)는 계속 페이지를 넣고 빼는 데 사용되고, 실제 프로그램 실행 효율은 떨어지게 된다.</p>
<p>따라서 페이지 대치 알고리즘의 핵심은 가능하면 당장 필요하지 않을 페이지를 선택해 내보내는 것이다. 결국 어떤 기준으로 페이지를 선택하느냐에 따라 알고리즘의 성능 차이가 발생하게 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f4ceb1be-159b-482a-80a6-88c5345bd0e7.png" alt="" style="display:block;margin:0 auto" />

<h3>페이지 대치 알고리즘(Page Replacement Algorithm): 프레임 수(Frame Count)와 페이지 부재(Page Fault)</h3>
<p><code>페이지 대치 알고리즘(Page Replacement Algorithm)</code>을 이해하려면 먼저 <code>프레임(Frame)</code>의 개념을 살펴볼 필요가 있다. 메모리(Memory)의 프레임 수는 <mark class="bg-yellow-200 dark:bg-yellow-500/30">동시에 메모리에 올려둘 수 있는 페이지(Page)의 개수</mark>를 의미한다. 즉, 프레임이 많을수록 프로그램(Program)이 사용하는 데이터를 더 많이 메모리에 유지할 수 있다.</p>
<p>프로그램이 실행될 때 페이지들은 일정한 순서로 계속 참조된다. 이러한 페이지 접근 순서를 <code>참조 문자열(Reference String)</code>이라고 한다. 프로그램은 이 참조 문자열을 따라가며 필요한 페이지를 하나씩 요청하게 된다.</p>
<p>프레임 수가 많아지면 한 번 메모리에 가져온 페이지를 더 오래 유지할 수 있다. 따라서 같은 페이지를 다시 요청했을 때, 그 페이지가 아직 메모리에 남아 있을 가능성이 높아진다. 그 결과 페이지 부재(Page Fault)가 줄어들 수 있다.</p>
<p>반대로 프레임 수가 적으면 메모리에 유지할 수 있는 페이지 수가 제한된다. 이 경우 새로운 페이지를 가져오기 위해 기존 페이지를 더 자주 교체해야 하고, 이미 내보낸 페이지가 다시 필요해지는 상황도 발생할 수 있다.</p>
<p>결국 프레임 수는 페이지 부재의 발생 빈도와 직접적으로 관련이 있다. 프레임이 많을수록 페이지를 더 많이 유지할 수 있어 페이지 부재가 줄어들 가능성이 높고, 프레임이 적을수록 페이지 교체가 자주 발생하게 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/811306df-7f8b-4aff-ad3d-15bb8328bd7d.png" alt="" style="display:block;margin:0 auto" />

<h3>프레임 수(Frame Count)와 페이지 부재(Page Fault)</h3>
<p><code>프레임(Frame)</code>은 메모리(Memory)에 페이지(Page)를 올려둘 수 있는 공간을 의미한다. 따라서 프레임 수가 많다는 것은 동시에 더 많은 페이지를 메모리에 유지할 수 있다는 뜻이다.</p>
<p>그래서 일반적인 경우엔 프레임수가 증가할수록 '페이지부재' 횟수는 감소하는 경향을 보이게 된다. 참조 문자열에따라 실행하는 과정에서 이미 사용했던 페이지가 메모리에 계속 남아있게되면서 다시 가져와야하는 상황이 줄어들기 때문이다.</p>
<p>결국 메모리에 더 많은 페이지를 유지할 수 있을수록 저장장치(Storage)에서 데이터를 다시 읽어오는 횟수가 줄어들고, 그 결과 페이지 부재도 감소한다. 따라서 대부분의 경우 프레임 수가 늘어나면 프로그램 실행 성능이 좋아진다고 볼 수 있다고 이해하자.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/84970dcb-14e3-4858-bea9-60d3461a0f24.png" alt="" style="display:block;margin:0 auto" />

<h3>프레임 수(Frame Count)와 페이지 부재(Page Fault)의 예</h3>
<p>프레임 수(Frame Count)와 페이지 부재(Page Fault)의 관계를 볼 때, 항상 같은 결과가 나타나는 것은 아니라는 점을 알아둘 필요가 있다.</p>
<p>그림의 왼쪽에는 <code>참조 문자열(Reference String)</code>이 제시되어 있다. 참조 문자열은 프로그램(Program)이 실행되는 동안 페이지(Page)에 접근하는 순서를 의미한다. 이 문자열을 보면 같은 페이지가 반복해서 등장하는 것을 확인할 수 있다. 즉, 프로그램은 새로운 페이지만 계속 사용하는 것이 아니라, 이미 사용했던 페이지를 다시 참조하는 패턴을 가진다.</p>
<p>따라서 어떤 페이지를 메모리(Memory)에 계속 유지하느냐가 페이지 부재 횟수에 큰 영향을 준다. 이미 사용했던 페이지가 메모리에 남아 있다면 다시 접근할 때 바로 사용할 수 있지만, 메모리에서 제거된 상태라면 다시 페이지 부재가 발생하게 된다.</p>
<p>오른쪽 그래프는 같은 참조 문자열을 기준으로 프레임 수를 바꿔가며 실행했을 때의 결과를 보여준다. 일반적으로 프레임 수가 많아질수록 페이지 부재 횟수는 감소하는 경향이 있다. 메모리에 더 많은 페이지를 유지할 수 있기 때문에, 이미 사용했던 페이지를 다시 참조할 때 페이지 부재가 발생할 가능성이 줄어들기 때문이다.</p>
<p>다만 모든 경우에 프레임 수가 증가한다고 항상 페이지 부재가 줄어드는 것은 아니다. 특정 <code>페이지 대치 알고리즘(Page Replacement Algorithm)</code>에서는 같은 참조 문자열을 사용하더라도 예상과 다른 결과가 나타날 수 있다. 이 점은 이후 페이지 대치 알고리즘을 이해하는 데 중요한 부분이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/704cd5ee-d152-4c81-abab-4ef03366a2f7.png" alt="" style="display:block;margin:0 auto" />

<h3>선입선출 페이지 대치 알고리즘(FIFO Page Replacement Algorithm)</h3>
<p>가장 기본적인 <code>페이지 대치 알고리즘(Page Replacement Algorithm)</code> 중 하나는 선입선출 방식인 <code>FIFO(First-In, First-Out)</code>이다.</p>
<p>FIFO는 이름 그대로 먼저 메모리(Memory)에 들어온 페이지(Page)를 먼저 제거하는 방식이다. 즉, 페이지가 메모리에 적재된 순서를 기준으로 가장 오래전에 들어온 페이지가 교체 대상이 된다.</p>
<p>이 구조는 자료구조(Data Structure)에서 배우는 큐(Queue)와 비슷하게 이해할 수 있다. 먼저 들어온 데이터가 앞쪽에 위치하고, 새로운 페이지가 들어와야 할 때는 가장 앞에 있는 페이지가 제거된다.</p>
<p>FIFO는 적재된 순서만 관리하면 되기 때문에 구현이 매우 단순하다. 따라서 이해하기 쉽고 구현하기 쉬운 페이지 대치 알고리즘이라고 볼 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/1d38d893-417a-4774-bddc-78aa9d7d3371.png" alt="" style="display:block;margin:0 auto" />

<h3>선입선출 페이지 대치 알고리즘(FIFO Page Replacement Algorithm)의 한계</h3>
<p><code>선입선출 페이지 대치 알고리즘(FIFO Page Replacement Algorithm)</code>은 구조가 단순하다는 장점이 있지만, 한계도 분명하다.</p>
<p>가장 큰 한계는 <code>페이지(Page)</code>가 실제로 사용되고 있는지와 관계없이, 단순히 메모리(Memory)에 들어온 순서만을 기준으로 제거 대상이 결정된다는 점이다. 즉, 가장 오래전에 들어온 페이지가 현재도 계속 사용되고 있는 중요한 페이지일 수 있는데도 교체 대상이 될 수 있다.</p>
<p>또한 FIFO는 페이지가 과거에 언제 들어왔는지만 고려할 뿐, 앞으로 다시 사용될 가능성은 반영하지 못한다. 따라서 실제 프로그램(Program)의 사용 패턴을 충분히 고려하지 못하고, 비효율적인 페이지 교체(Page Replacement)가 발생할 수 있다.</p>
<p>결국 FIFO는 구현이 쉽고 이해하기 쉬운 방식이지만, 페이지의 실제 사용 빈도나 중요도를 반영하지 못한다는 한계를 가진다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/7d09df66-50ef-40d3-9ef2-39c1b7f125af.png" alt="" style="display:block;margin:0 auto" />

<h3>선입선출 페이지 대치 알고리즘(FIFO Page Replacement Algorithm)의 동작 과정</h3>
<p><code>선입선출 페이지 대치 알고리즘(FIFO Page Replacement Algorithm)</code>은 메모리에 먼저 들어온 페이지(Page)를 먼저 제거하는 방식이다. 이 과정은 큐(Queue)의 앞쪽에 있는 페이지를 제거하고, 새로 들어온 페이지를 뒤쪽에 추가하는 흐름으로 동작한다.</p>
<p>1️⃣ 먼저 FIFO <code>큐(Queue)</code>의 <code>헤드(Head)</code>에 있는 페이지가 제거 대상, 즉 <code>희생 페이지(Victim Page)</code>로 선택된다. 이 페이지는 가장 먼저 메모리(Memory)에 들어온 페이지이므로 교체 대상이 된다.</p>
<p>2️⃣ 그다음 선택된 희생 페이지는 메모리에서 제거되어 <code>저장장치(Storage)</code>로 이동한다. 이 과정을 <code>스왑 아웃(Swap Out)</code>이라고 한다. 페이지가 메모리에서 나갔기 때문에, 해당 페이지에 대한 페이지 테이블(Page Table)의 정보도 갱신된다.</p>
<p>3️⃣ 이때 <code>타당 비트(Valid Bit)</code>는 0으로 변경되어 <code>비타당 상태(Invalid State)</code>가 된다.</p>
<p>4️⃣ 이후 새로 필요한 페이지를 저장장치에서 가져와 메모리에 적재한다. 이 과정을 <code>스왑 인(Swap In)</code>이라고 한다.</p>
<p>5️⃣ 새 페이지가 메모리에 올라오면 페이지 테이블도 다시 갱신된다. 새 페이지가 어느 <code>프레임(Frame)</code>에 들어갔는지 기록하고, 타당 비트를 1로 변경하여 <code>타당 상태(Valid State)</code>로 표시한다.</p>
<p>6️⃣ 마지막으로 새로 적재된 페이지는 FIFO 큐의 뒤쪽에 추가된다. 이렇게 하면 메모리에 들어온 순서가 다시 정리되고, 다음에 페이지 교체가 필요할 때는 다시 큐의 앞쪽에 있는 페이지가 제거 대상이 된다.</p>
<p>즉, FIFO 방식은 기존 페이지를 <code>스왑 아웃(Swap Out)</code>하고, 새로운 페이지를 <code>스왑 인(Swap In)</code>한 뒤, 새 페이지를 큐의 뒤쪽에 추가하는 과정을 반복하는 페이지 대치 방식이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/1a004a28-3ddb-4f3d-aab8-2314624fac7f.png" alt="" style="display:block;margin:0 auto" />

<h3>선입선출 페이지 대치 알고리즘(FIFO Page Replacement Algorithm) 예시</h3>
<p>그림의 위쪽에는 하나의 <code>참조 문자열(Reference String)</code>이 주어져 있다. 참조 문자열은 프로그램(Program)이 실행되는 동안 <code>페이지(Page)</code>를 요청하는 순서를 의미한다. 예를 들어 페이지가 <code>1, 2, 3, 2, 1, 5, 2, 1, 6</code>과 같은 순서로 요청되면, 프로그램은 이 순서에 따라 필요한 페이지에 접근하게 된다.</p>
<p>이때 프로그램이 어떤 페이지를 사용하려고 했는데, 해당 페이지가 현재 메모리(Memory)에 없다면 <code>페이지 부재(Page Fault)</code>가 발생한다.</p>
<p>그림에서는 <code>프레임(Frame)</code> 수가 3개일 때 <code>FIFO(First-In, First-Out)</code> 방식으로 페이지가 어떻게 적재되고 교체되는지를 보여준다. 처음 1이 요청되면 메모리가 비어 있으므로 첫 번째 프레임에 적재되고 페이지 부재가 발생한다. 이후 2와 3도 아직 메모리에 없기 때문에 각각 페이지 부재가 발생하며 프레임이 채워진다.</p>
<p>그다음 2와 1처럼 이미 메모리에 있는 페이지가 다시 요청되면 페이지 부재 없이 그대로 사용된다. 하지만 5처럼 메모리에 없는 새로운 페이지가 요청되면 다시 페이지 부재가 발생한다. 이때 빈 프레임이 없으므로 FIFO 규칙에 따라 가장 먼저 들어온 페이지인 1이 제거되고, 그 자리에 5가 적재된다.</p>
<p>이러한 과정을 참조 문자열의 끝까지 반복하면, FIFO 방식에서는 먼저 들어온 페이지를 기준으로 계속 교체가 이루어진다. 따라서 페이지가 실제로 자주 사용되는지와 관계없이, 가장 오래전에 적재된 페이지가 제거될 수 있다.</p>
<p>단, 녹취록에 나온 예시 참조 문자열이 1, 2, 3, 2, 1, 5, 2, 1, 6만을 의미한다면 페이지 요청 횟수가 9번이므로 페이지 부재가 14번 발생할 수는 없다. 14번의 페이지 부재는 그림에 표시된 더 긴 전체 참조 문자열을 기준으로 한 결과로 보는 것이 자연스럽다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/0b8f0b56-89bf-43be-b7af-14d74583b231.png" alt="" style="display:block;margin:0 auto" />

<h3>선입선출 페이지 대치 알고리즘(FIFO Page Replacement Algorithm)의 문제점</h3>
<p><code>선입선출 페이지 대치 알고리즘(FIFO Page Replacement Algorithm)</code>은 단순히 먼저 들어온 페이지(Page)를 먼저 제거하는 방식이다. 하지만 이 방식은 메모리(Memory)를 더 많이 할당해도 항상 성능이 좋아지는 것은 아니라는 한계를 가진다.</p>
<p>그림의 왼쪽에는 하나의 <code>참조 문자열(Reference String)</code>이 주어져 있고, 이를 기준으로 프레임(Frame)을 3개 사용했을 때와 4개 사용했을 때의 결과가 비교되어 있다. 일반적으로는 프레임 수가 늘어나면 더 많은 페이지를 메모리에 유지할 수 있으므로 <code>페이지 부재(Page Fault)</code>가 줄어드는 것이 자연스럽다.</p>
<p>하지만 이 예시에서는 프레임이 3개일 때 페이지 부재가 9번 발생했고, 프레임을 4개로 늘렸을 때는 오히려 페이지 부재가 10번으로 증가하였다. 즉, 메모리를 더 많이 할당했음에도 페이지 부재가 더 많이 발생한 것이다.</p>
<p>이러한 현상을 <code>벨라디의 이상 현상(Belady’s Anomaly)</code>이라고 한다. 이는 FIFO와 같은 일부 페이지 대치 알고리즘에서 나타날 수 있는 문제로, 프레임 수를 늘렸는데도 페이지 부재 횟수가 오히려 증가하는 현상을 의미한다.</p>
<p>결국 FIFO는 구현이 쉽고 단순하지만, 실제 페이지 사용 패턴을 고려하지 않기 때문에 메모리를 더 많이 사용해도 성능이 나빠질 수 있다는 한계를 가진다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/81b4a08f-db33-43d0-928b-031bca143bce.png" alt="" style="display:block;margin:0 auto" />

<h3>최적 페이지 대치 알고리즘(Optimal Page Replacement Algorithm)</h3>
<p><code>최적 페이지 대치 알고리즘(Optimal Page Replacement Algorithm)</code>은 앞으로 가장 오랫동안 사용되지 않을 페이지(Page)를 선택해서 제거하는 방식이다. 즉, 과거에 언제 사용되었는지가 아니라 앞으로 언제 다시 사용될지를 기준으로 희생 페이지(Victim Page)를 결정한다.</p>
<p>이 방식에서는 가장 나중에 다시 사용될 페이지, 또는 앞으로 사용되지 않을 페이지를 제거한다. 이렇게 하면 불필요한 페이지 교체(Page Replacement)를 최소화할 수 있기 때문에 페이지 부재(Page Fault) 횟수를 가장 적게 만들 수 있다.</p>
<p>다만 이 알고리즘은 앞으로의 페이지 참조 순서, 즉 <code>미래의 참조 정보(Future Reference Information)</code>를 알고 있다는 가정이 필요하다. 실제 실행 중에는 미래에 어떤 페이지가 사용될지 정확히 알 수 없기 때문에 현실적으로 구현하기는 어렵다.</p>
<p>따라서 최적 페이지 대치 알고리즘은 실제 운영체제(Operating System)에서 그대로 사용되기보다는, 다른 페이지 대치 알고리즘의 성능을 비교하는 기준으로 사용된다. 즉, 가장 이상적인 성능을 보이는 기준 알고리즘(Benchmark Algorithm)이라고 이해할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/1554bd2c-d3e9-470b-bd79-efa75c40c19f.png" alt="" style="display:block;margin:0 auto" />

<h3>최적 페이지 대치 알고리즘(Optimal Page Replacement Algorithm)의 한계</h3>
<p><code>최적 페이지 대치 알고리즘(Optimal Page Replacement Algorithm)</code>은 앞으로 어떤 페이지(Page)가 언제 다시 사용될지를 미리 알고 있어야 한다. 하지만 실제 시스템에서는 프로그램(Program)의 미래 참조(Future Reference)를 정확히 알 수 없다.</p>
<p>따라서 이 알고리즘을 실제 운영체제(Operating System)에서 그대로 구현하는 것은 불가능하다. 실행 중인 프로그램이 앞으로 어떤 순서로 페이지에 접근할지 미리 예측할 수 없기 때문이다.</p>
<p>그래서 최적 페이지 대치 알고리즘은 실제로 사용하는 방식이라기보다 이론적인 기준(Theoretical Standard)으로 활용된다. 다른 페이지 대치 알고리즘(Page Replacement Algorithm)이 얼마나 효율적으로 동작하는지 평가할 때, 이론적으로 가장 좋은 경우와 비교하는 기준으로 사용된다.</p>
<p>즉, 최적 페이지 대치 알고리즘은 현실적으로 구현하기는 어렵지만, 페이지 부재(Page Fault)를 최소화할 수 있는 이상적인 알고리즘으로서 성능 비교의 기준이 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/adf9b7a2-34d6-4746-81a4-c3bbe21238f5.png" alt="" style="display:block;margin:0 auto" />

<h3>최적 페이지 대치 알고리즘(Optimal Page Replacement Algorithm)의 동작 과정</h3>
<p><code>최적 페이지 대치 알고리즘(Optimal Page Replacement Algorithm)</code>은 앞으로 가장 오랫동안 사용되지 않을 페이지(Page)를 선택해 교체하는 방식이다. 같은 참조 문자열(Reference String)이 주어지고 프레임(Frame)이 3개라고 할 때, 참조 문자열의 흐름을 보면서 어떤 페이지를 제거할지 결정한다.</p>
<p>처음에는 1, 2, 3 페이지가 차례대로 요청되어 프레임이 채워지고, 이 과정에서 페이지 부재(Page Fault)가 발생한다. 이후 새로운 페이지인 5가 필요해지면, 현재 메모리에 있는 1, 2, 3 중 어떤 페이지를 제거할지 선택해야 한다.</p>
<p>이때 최적 알고리즘은 현재 위치 이후의 참조 문자열을 확인한다. 5 다음에 2가 먼저 다시 사용되고, 그다음 1이 다시 사용되며, 3은 그보다 더 나중에 사용된다면 3이 교체 대상이 된다. 즉, 현재 메모리에 있는 페이지 중 앞으로 가장 늦게 다시 사용될 페이지를 제거하는 것이다.</p>
<p>이 방식은 단순히 먼저 들어온 페이지를 제거하는 것이 아니라, 앞으로의 참조 순서를 기준으로 가장 효율적인 선택을 한다. 따라서 불필요한 페이지 교체를 최소화할 수 있고, 전체 페이지 부재 횟수도 줄어든다.</p>
<p>이 예제에서는 최적 페이지 대치 알고리즘을 적용했을 때 총 페이지 부재 횟수가 9회로 나타난다. 이는 앞에서 살펴본 FIFO(First-In, First-Out) 방식보다 더 적은 결과이다. 결국 최적 페이지 대치 알고리즘은 미래의 참조 정보를 알고 있다는 가정하에서 페이지 부재를 최소화하는 이론적으로 가장 효율적인 알고리즘이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/01d76dca-e0ec-4311-ac1a-1d4d4a660aa9.png" alt="" style="display:block;margin:0 auto" />

<h3>LRU 페이지 대치 알고리즘(LRU Page Replacement Algorithm)</h3>
<p><code>LRU(Least Recently Used) 페이지 대치 알고리즘</code>은 가장 오랫동안 사용되지 않은 페이지(Page)를 선택해서 제거하는 방식이다. 앞으로 어떤 페이지가 사용될지 정확히 알 수 없기 때문에, 대신 과거의 사용 이력(Usage History)을 기준으로 판단한다.</p>
<p>이 알고리즘은 최근에 사용된 페이지는 다시 사용될 가능성이 높고, 오랫동안 사용되지 않은 페이지는 앞으로도 사용되지 않을 가능성이 높다고 가정한다. 따라서 메모리(Memory)에 있는 페이지 중 가장 오래 사용되지 않은 페이지를 희생 페이지(Victim Page)로 선택한다.</p>
<p>LRU는 미래의 참조 정보를 알 수 없다는 현실적인 한계를 고려하여, 과거의 참조 기록을 바탕으로 최적 페이지 대치 알고리즘(Optimal Page Replacement Algorithm)에 가까운 선택을 하려는 방식이다.</p>
<p>즉, LRU는 실제 사용 패턴을 어느 정도 반영할 수 있기 때문에 FIFO(First-In, First-Out)보다 효율적인 결과를 기대할 수 있는 대표적인 페이지 대치 알고리즘이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/d4bbea8a-3852-4a5f-a694-792c41c0e34b.png" alt="" style="display:block;margin:0 auto" />

<h3>LRU 페이지 대치 알고리즘(LRU Page Replacement Algorithm)의 특징</h3>
<p><code>LRU(Least Recently Used) 페이지 대치 알고리즘</code>은 최근에 사용된 페이지(Page)는 다시 사용될 가능성이 높다는 가정을 바탕으로 동작한다. 따라서 각 페이지가 마지막으로 사용된 시점을 기준으로, 가장 오랫동안 사용되지 않은 페이지를 교체 대상으로 선택한다.</p>
<p>이 방식은 과거의 사용 이력(Usage History)을 반영하기 때문에 FIFO(First-In, First-Out)보다 더 합리적인 선택이 가능하다. 단순히 먼저 들어온 페이지를 제거하는 것이 아니라, 실제로 최근에 사용되었는지를 고려하기 때문이다.</p>
<blockquote>
<p>하지만 LRU가 동작하려면 각 페이지가 언제 사용되었는지를 기록해야 한다. 예를 들어 페이지 사용 시간(Use Time)을 저장하거나, 스택(Stack)과 같은 구조를 이용해 사용 순서(Use Order)를 관리할 수 있다.</p>
</blockquote>
<p>즉, LRU는 페이지 사용 패턴을 반영할 수 있어 효율적인 페이지 대치가 가능하지만, 이를 구현하기 위해 추가적인 정보 관리 비용(Management Cost)이 필요하다는 특징이 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/ca11ee0f-b618-4fad-85b7-9c64fc4e723c.png" alt="" style="display:block;margin:0 auto" />

<h3>LRU 페이지 대치 알고리즘(LRU Page Replacement Algorithm)의 동작 흐름</h3>
<p><code>LRU(Least Recently Used) 페이지 대치 알고리즘</code>은 가장 오랫동안 사용되지 않은 페이지(Page)를 교체 대상으로 선택하는 방식이다. <code>참조 문자열(Reference String)</code>이 주어지고 프레임(Frame)이 3개일 때, 페이지 요청 순서에 따라 메모리 상태가 계속 갱신된다.</p>
<p>처음 1, 2, 3 페이지가 차례대로 들어올 때는 메모리가 비어 있는 상태이기 때문에 각각 페이지 부재(Page Fault)가 발생하고, 프레임이 하나씩 채워진다.</p>
<p>이후 2가 다시 요청되면 이미 메모리에 있기 때문에 페이지 교체는 발생하지 않는다. 대신 2는 가장 최근에 사용된 페이지로 갱신된다. 그다음 1이 요청될 때도 마찬가지로 교체 없이 사용되며, 이번에는 1이 가장 최근에 사용된 페이지가 된다.</p>
<p>이 상태에서 5가 요청되면 메모리가 이미 가득 차 있기 때문에 기존 페이지 중 하나를 제거해야 한다. LRU에서는 가장 오래 사용되지 않은 페이지를 선택한다. 이때 3은 처음 들어온 이후로 다시 사용되지 않았고, 1과 2는 최근에 사용되었기 때문에 3이 제거된다. 그 자리에 5가 새로 적재된다.</p>
<p>이후에도 페이지가 요청될 때마다 최근 사용 시점을 기준으로 가장 오래 사용되지 않은 페이지를 교체한다. 이 과정을 끝까지 반복하면 예제에서는 총 11회의 페이지 부재가 발생한다.</p>
<p>즉, LRU의 핵심은 단순히 메모리에 들어온 순서가 아니라 실제 사용 이력(Usage History)을 기준으로 교체 대상을 결정한다는 점이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a65d6354-0896-48f3-880e-8f659bad301e.png" alt="" style="display:block;margin:0 auto" />

<h3>전역 대치(Global Replacement), 지역 대치(Local Replacement)</h3>
<p><code>페이지 대치(Page Replacement)</code>에서 중요한 문제 중 하나는 교체 대상을 어느 범위까지 볼 것인가이다. 즉, 특정 프로세스(Process) 내부에서만 희생 페이지(Victim Page)를 선택할 것인지, 아니면 메모리(Memory)에 올라와 있는 모든 프로세스의 페이지를 대상으로 선택할 것인지에 따라 동작 방식이 달라진다.</p>
<p>이 기준에 따라 페이지 대치는 <code>전역 대치(Global Replacement)</code>와 <code>지역 대치(Local Replacement)</code>로 나눌 수 있다.</p>
<p><code>전역 대치(Global Replacement)</code>는 희생 페이지를 선택할 때 메모리에 있는 모든 프로세스의 프레임(Frame)을 대상으로 하는 방식이다. 따라서 한 프로세스가 새로운 페이지(Page)를 필요로 할 때, 다른 프로세스가 사용 중인 프레임을 가져올 수도 있다. 이 방식은 시스템 전체 관점에서는 메모리를 효율적으로 사용할 수 있지만, 특정 프로세스 입장에서는 자신에게 할당된 프레임을 빼앗길 수 있기 때문에 성능이 불안정해질 수 있다.</p>
<p>반면 <code>지역 대치(Local Replacement)</code>는 각 프로세스가 자신에게 할당받은 프레임 안에서만 페이지 교체를 수행하는 방식이다. 다른 프로세스의 프레임에는 영향을 주지 않기 때문에 각 프로세스의 실행은 비교적 안정적으로 유지될 수 있다. 하지만 전체 메모리를 유연하게 활용하기는 어렵기 때문에, 시스템 전체 관점에서는 효율이 떨어질 수 있다.</p>
<p>결국 전역 대치와 지역 대치는 메모리 효율성과 프로세스 안정성 사이의 차이를 보여준다. 전역 대치는 전체 메모리 활용에는 유리하지만 개별 프로세스 성능이 흔들릴 수 있고, 지역 대치는 개별 프로세스의 안정성은 높지만 전체 메모리 활용 효율은 낮아질 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/75cd5af9-e5bf-48d0-9378-9666e8ada300.png" alt="" style="display:block;margin:0 auto" />

<h3>페이지 버퍼링 알고리즘(Page Buffering Algorithm)</h3>
<p>지금까지는 어떤 페이지(Page)를 선택해서 교체할지, 즉 <code>페이지 대치 알고리즘(Page Replacement Algorithm)</code>에 대해 살펴보았다. 하지만 페이지 부재(Page Fault)가 발생했을 때는 어떤 페이지를 교체할지도 중요하지만, 실제 성능에는 <code>디스크 입출력(Disk I/O) 시간</code>이 큰 영향을 준다.</p>
<blockquote>
<p>페이지 부재가 발생하면 필요한 페이지를 보조기억장치(Secondary Storage)에서 메모리(Memory)로 가져와야 한다. 이 과정에서 가장 큰 지연은 디스크에서 데이터를 읽어오는 시간 때문에 발생한다. 따라서 무엇을 교체할 것인가뿐만 아니라, 페이지 부재를 얼마나 빠르게 처리할 것인가도 중요하다.</p>
</blockquote>
<p>이러한 지연을 줄이기 위해 사용하는 기법이 <code>페이지 버퍼링(Page Buffering)</code>이다. 페이지 버퍼링은 페이지 부재가 발생했을 때 바로 사용할 수 있는 여유 프레임(Free Frame)을 미리 확보해두거나, 제거된 페이지를 <code>버퍼(Buffer)</code>에 잠시 유지해두는 방식이다.</p>
<p>이렇게 하면 새로운 페이지가 필요할 때 바로 적재할 수 있는 공간이 준비되어 있기 때문에 처리 지연을 줄일 수 있다. 또한 경우에 따라 제거된 페이지가 다시 필요해지면 디스크에서 다시 읽어오지 않고 버퍼에 남아 있는 페이지를 재사용할 수도 있다.</p>
<p>결국 페이지 버퍼링은 디스크 입출력 지연을 줄이기 위해 미리 공간을 준비하고, 제거된 페이지를 잠시 보관하여 페이지 부재 처리 시간을 줄이는 기법이라고 이해할 수 있겠다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/d0df9129-6883-4b6e-8177-098573335e6d.png" alt="" style="display:block;margin:0 auto" />

<h3>스래싱(Thrashing)</h3>
<p><code>페이지 대치 알고리즘(Page Replacement Algorithm)</code>과 <code>페이지 버퍼링(Page Buffering)</code>을 통해 <code>가상메모리(Virtual Memory)</code>의 성능을 어느 정도 개선할 수 있다. 하지만 메모리(Memory)가 부족하거나 실행 중인 프로세스(Process)가 지나치게 많아지면 이러한 기법만으로는 성능을 유지하기 어렵다.</p>
<p>이처럼 페이지 부재(Page Fault)가 과도하게 발생하는 현상을 <code>스래싱(Thrashing)</code>이라고 한다. 정상적인 경우에는 페이지 교체(Page Replacement)가 발생하더라도 프로그램 실행이 계속 이어진다. 하지만 스래싱 상태에서는 페이지를 메모리로 가져오고 다시 내보내는 작업이 반복되면서, 실제 프로그램 실행은 거의 이루어지지 않는다.</p>
<p>이 경우 <code>CPU(Central Processing Unit)</code>는 대부분의 시간을 작업이 끝나기를 기다리는 데 사용하고, <code>디스크(Disk)</code>는 계속해서 페이지를 읽고 쓰는 작업만 수행하게 된다. 결국 시스템 전체가 실제 실행보다 페이지 교체에 대부분의 시간을 사용하는 비효율적인 상태에 빠지게 된다.</p>
<p>따라서 스래싱은 단순히 페이지 부재가 많이 발생하는 문제가 아니라, <code>메모리 부족</code>으로 인해 시스템 성능 전체가 크게 저하되는 현상이라고 이해할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/1eddce12-b0d7-4b27-961a-fd206545a9b5.png" alt="" style="display:block;margin:0 auto" />

<h3>스레싱(Thrashing) 발생 원인</h3>
<p>스레싱(Thrashing)의 발생 원인은 결국 메모리 부족(Memory Shortage)과 관련이 있다. 프로세스(Process)에 할당된 <code>프레임(Frame)</code> 수가 부족하면, 프로세스가 필요로 하는 페이지(Page)를 메모리에 충분히 유지할 수 없다. 이 경우 필요한 페이지가 메모리에 없어서 페이지 부재(Page Fault)가 계속 발생하게 된다.</p>
<p>또한 동시에 실행되는 프로세스의 수가 너무 많아져도 스레싱이 발생할 수 있다. 실행 중인 프로세스가 많아지면 각 프로세스에 할당될 수 있는 메모리의 양이 줄어들고, 그 결과 각 프로세스가 필요한 페이지를 충분히 유지하지 못하게 된다.</p>
<p>이때 중요한 개념이 <code>워킹 셋(Working Set)</code>이다. 워킹 셋은 프로세스가 일정 시간 동안 실제로 자주 사용하는 페이지들의 집합을 의미한다. 프로세스가 원활하게 실행되려면 이 워킹 셋을 메모리에 유지할 수 있어야 한다.</p>
<p>하지만 워킹 셋을 유지할 만큼의 메모리가 확보되지 않으면, 필요한 페이지를 계속 가져오고 다시 내보내는 상황이 반복된다. 이로 인해 페이지 부재가 과도하게 발생하고, 결국 스레싱 상태에 빠지게 된다.</p>
<p>즉, 스레싱의 핵심 원인은 프로세스가 자주 사용하는 페이지 집합인 워킹 셋을 메모리에 유지할 만큼 충분한 프레임이 확보되지 않는 데 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/3967d8ad-291b-4e2e-8779-6376e33849c4.png" alt="" style="display:block;margin:0 auto" />

<h3>스래싱(Thrashing) 동작 과정</h3>
<p><code>스래싱(Thrashing)</code>이 발생하면 페이지 교체(Page Replacement)가 반복되는 악순환 구조로 이어진다.</p>
<p>먼저 프로그램(Program) 실행 중 <code>페이지 부재(Page Fault)</code>가 계속 증가하기 시작한다. 필요한 페이지(Page)가 메모리(Memory)에 없기 때문에 운영체제(Operating System)는 새로운 페이지를 메모리에 올려야 한다.</p>
<p>그런데 메모리에 여유 공간이 부족하면, 새로운 페이지를 올리기 위해 기존에 있던 페이지 중 하나를 제거해야 한다. 문제는 이렇게 제거된 페이지가 얼마 지나지 않아 다시 필요해질 수 있다는 점이다.</p>
<p>그러면 운영체제는 다시 그 페이지를 가져오기 위해 또 다른 페이지를 제거하게 된다. 이 과정이 반복되면 메모리는 계속 페이지를 가져오고 내보내는 작업에 사용되고, 실제 프로그램 실행은 거의 이루어지지 않게 된다.</p>
<p>즉, 스래싱은 페이지 부재 증가, 기존 페이지 제거, 제거된 페이지의 재요청, 또 다른 페이지 제거가 반복되면서 실행보다 페이지 교체가 더 많이 발생하는 상태라고 이해할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/48db742a-7acf-420d-bb19-f90144e7f956.png" alt="" style="display:block;margin:0 auto" />

<h3>스래싱(Thrashing)과 워킹 셋(Working Set)</h3>
<p><code>스래싱(Thrashing)</code>의 동작 과정을 이해하기 위해서는 <code>워킹 셋(Working Set)</code>의 개념을 함께 살펴볼 필요가 있다. 워킹 셋은 프로세스(Process)가 일정 시간 동안 실제로 자주 사용하는 <code>페이지(Page)들의 집합</code>을 의미한다.</p>
<p>그림에서는 프로그램이 실행되면서 <code>페이지 참조열(Page Reference String)</code>을 따라 여러 페이지를 순서대로 참조하는 모습을 보여준다. 이때 특정 시점을 기준으로 바로 이전의 일정 구간, 즉 델타(Delta)만큼의 범위 안에서 최근에 사용된 페이지들을 모아 워킹 셋을 구한다.</p>
<p>예를 들어 <code>t1</code> 시점에서 왼쪽으로 <code>델타=10</code> 범위를 보면, 이 구간 안에서 참조된 페이지는 <code>2, 1, 5, 7</code>이다. 따라서 이때의 워킹 셋은 <code>[2, 1, 5, 7]</code>이 된다.</p>
<p><code>t2</code> 시점에서도 같은 방식으로 왼쪽의 <code>델타=10</code> 범위를 확인한다. 이 구간에서는 <code>7, 5, 1, 3, 4</code>가 참조되었으므로, t2 시점의 워킹 셋은 <code>[7, 5, 1, 3, 4]</code>가 된다.</p>
<p><code>t3</code> 시점에서는 최근에 참조된 페이지가 <code>3, 4</code>로 줄어든다. 따라서 이때의 워킹 셋은 <code>[3, 4]</code>가 된다.</p>
<p>이 그림이 보여주는 핵심은 워킹 셋이 고정된 하나의 집합이 아니라는 점이다. 프로그램이 실행되는 시점에 따라 최근에 자주 참조되는 페이지의 묶음은 계속 달라진다. 따라서 운영체제(Operating System)는 각 시점의 워킹 셋을 메모리(Memory)에 유지할 수 있도록 관리해야 한다.</p>
<p>만약 워킹 셋을 충분히 메모리에 유지하지 못하면, 방금까지 필요했던 페이지를 계속 내보내고 다시 가져오는 일이 반복된다. 이런 상황이 심해지면 페이지 교체(Page Replacement)가 실제 실행보다 더 많이 발생하게 되고, 결국 스래싱으로 이어질 수 있다.</p>
<p>또한 델타의 크기도 중요하다. 델타가 너무 작으면 실제로 필요한 페이지들을 충분히 포함하지 못할 수 있다. 반대로 델타가 너무 크면 현재는 자주 사용하지 않는 페이지까지 포함하게 되어 메모리를 낭비할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/65c4760b-64be-46e4-8024-1bd6c618e78b.png" alt="" style="display:block;margin:0 auto" />

<h3>워킹 셋(Working Set)과 프레임 수(Frame Count)</h3>
<p>이 그림은 <code>워킹 셋(Working Set)</code>의 크기와 <code>프레임 수(Frame Count)</code>에 따라 <code>페이지 부재 비율(Page Fault Rate)</code>이 어떻게 달라지는지를 보여준다.</p>
<p>가로축은 <code>메모리(Memory)에 할당된 프레임 수</code>를 의미하고, 세로축은 <code>페이지 부재 비율</code>을 의미한다. 프레임 수가 매우 적은 구간에서는 페이지 부재 비율이 매우 높게 나타난다. 이 구간은 프로세스(Process)의 워킹 셋보다 할당된 프레임 수가 적은 상태라고 볼 수 있다.</p>
<p>즉, 프로세스가 자주 사용하는 페이지(Page)들을 메모리에 모두 유지하지 못하기 때문에, 필요한 페이지를 계속 가져오고 다시 내보내는 상황이 반복된다. 그 결과 페이지 부재가 급격히 증가하며, 이 상태가 바로 스래싱(Thrashing)이 발생하는 구간이다.</p>
<p>프레임 수를 점점 늘리면 페이지 부재 비율이 급격히 떨어지는 구간이 나타난다. 이 지점은 메모리가 워킹 셋을 수용할 수 있게 되는 구간이다. 자주 사용하는 페이지들이 메모리에 유지되기 때문에 페이지 부재가 크게 감소한다.</p>
<p>그 이후로 프레임 수를 더 늘리면 페이지 부재 비율은 계속 줄어들 수 있지만, 감소 폭은 점점 작아진다. 이미 대부분의 워킹 셋이 메모리에 유지되고 있기 때문에 추가적인 프레임을 더 제공해도 성능 개선 효과가 크지 않기 때문이다.</p>
<p>따라서 핵심은 각 프로세스의 워킹 셋을 충분히 담을 수 있을 만큼의 프레임이 확보되어야 한다는 점이다. 그래야 페이지 부재를 줄이고 스래싱을 방지할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f060e1bd-3a5c-4102-9cb2-dd8e3b94ab25.png" alt="" style="display:block;margin:0 auto" />

<h3>스래싱(Thrashing)의 영향</h3>
<p>스래싱(Thrashing)이 발생하면 시스템 전체에 여러 가지 영향이 연쇄적으로 나타난다. 가장 먼저 페이지 부재율(Page Fault Rate)이 급격하게 증가한다. 필요한 페이지(Page)가 계속 메모리(Memory)에 없기 때문에, 운영체제(Operating System)는 반복적으로 페이지를 가져와야 한다.</p>
<p>이로 인해 디스크 입출력(Disk I/O)이 크게 증가한다. 페이지를 계속 읽고 쓰는 작업이 반복되면서 디스크(Disk)가 과도하게 사용되는 상태가 된다.</p>
<p>반대로 CPU(Central Processing Unit)는 실제 작업을 제대로 수행하지 못하고, 대부분의 시간을 디스크 작업이 끝나기를 기다리는 데 사용하게 된다. 즉, CPU는 놀고 있고 디스크만 계속 동작하는 비정상적인 상태가 되는 것이다.</p>
<p>이러한 상황이 계속되면 프로그램 실행보다 페이지 교체(Page Replacement)에 더 많은 시간이 사용된다. 결과적으로 시스템 전체 성능(System Performance)이 크게 저하된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/2efef6c7-3d7e-41a5-aebb-f839b0c7ee83.png" alt="" style="display:block;margin:0 auto" />

<h3>CPU 이용률(CPU Utilization)과 다중 프로그래밍 수준(Degree of Multiprogramming)</h3>
<p><code>다중 프로그래밍 수준(Degree of Multiprogramming)</code>이 증가한다는 것은 동시에 실행되는 프로세스(Process)의 수가 많아진다는 의미이다. 초기에는 프로세스 수가 늘어날수록 CPU(Central Processing Unit)가 처리할 작업도 많아지기 때문에 CPU 이용률 (CPU Utilization)이 증가한다.</p>
<p>예를 들어 한 프로세스가 입출력(I/O) 작업으로 기다리는 동안, 다른 프로세스가 CPU를 사용할 수 있다. 따라서 CPU가 쉬는 시간이 줄어들고, 시스템 전체의 처리 효율이 좋아질 수 있다.</p>
<p>하지만 프로세스 수가 일정 수준을 넘어서면 문제가 발생한다. 동시에 실행되는 프로세스가 너무 많아지면 각 프로세스에 할당되는 메모리(Memory)의 양이 줄어든다. 그 결과 각 프로세스가 자신의 워킹 셋(Working Set)을 충분히 메모리에 유지하지 못하게 되고, 페이지 부재(Page Fault)가 증가한다.</p>
<p>페이지 부재가 많아지면 운영체제(Operating System)는 필요한 페이지(Page)를 디스크(Disk)에서 계속 가져와야 한다. 이 과정에서 프로세스들은 실제 실행보다 대기 시간이 길어지고, CPU는 디스크 작업이 끝나기를 기다리게 된다.</p>
<p>결국 다중 프로그래밍 수준이 증가하면 처음에는 CPU 이용률이 높아지지만, 일정 수준을 넘어서면 스래싱(Thrashing)이 발생하면서 CPU 이용률이 급격히 감소한다. 즉, 프로세스 수를 무조건 늘리는 것이 항상 좋은 것은 아니며, 메모리가 감당할 수 있는 수준 안에서 적절하게 조절해야 한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/c40df476-beee-48c8-a6fb-03f26c97440f.png" alt="" style="display:block;margin:0 auto" />

<h3>CPU 이용률(CPU Utilization)과 다중 프로그래밍 수준(Degree of Multiprogramming) 그래프</h3>
<p>이 그래프는 <code>다중 프로그래밍 수준(Degree of Multiprogramming)</code>이 <code>CPU 이용률(CPU Utilization)</code>에 어떤 영향을 주는지를 보여준다.</p>
<p>초기에는 동시에 실행되는 프로세스(Process)의 수가 증가할수록 CPU가 처리할 작업도 많아지기 때문에 CPU 이용률이 증가한다. 한 프로세스가 입출력(I/O)이나 페이지 처리로 기다리는 동안 다른 프로세스가 실행될 수 있기 때문이다.</p>
<p>하지만 일정 수준을 넘어서면 문제가 발생한다. 프로세스 수가 너무 많아지면 각 프로세스에 할당되는 프레임(Frame) 수가 부족해지고, 워킹 셋(Working Set)을 메모리(Memory)에 충분히 유지하지 못하게 된다. 그 결과 페이지 부재(Page Fault)가 증가하고, 스래싱(Thrashing)이 발생한다.</p>
<p>스래싱 구간에서는 프로세스들이 실제 실행보다 페이지를 가져오고 내보내는 작업에 더 많은 시간을 사용한다. 이로 인해 디스크 입출력(Disk I/O)은 증가하지만 CPU는 대기하는 시간이 많아져 CPU 이용률이 급격히 감소한다.</p>
<p>즉, 다중 프로그래밍 수준이 낮을 때는 프로세스 수를 늘리는 것이 CPU 이용률을 높이는 데 도움이 된다. 하지만 너무 많이 늘리면 메모리 부족으로 인해 스래싱이 발생하고, 오히려 CPU 이용률이 떨어진다. 따라서 운영체제(Operating System)는 메모리가 감당할 수 있는 적정 수준에서 프로세스 수를 조절해야 한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/92a7bb5b-d74a-44b3-8f15-5ff56d281c78.png" alt="" style="display:block;margin:0 auto" />

<h3>스래싱(Thrashing) 해결 방법</h3>
<p>스래싱(Thrashing)이 발생하면 시스템은 실제 실행보다 페이지 교체(Page Replacement)에 더 많은 시간을 사용하게 된다. 따라서 해결 방법은 메모리(Memory)에 걸리는 부담을 줄이는 방향으로 접근해야 한다.</p>
<p>대표적인 방법은 동시에 실행되는 프로세스(Process)의 수를 줄이는 것이다. 즉, 다중 프로그래밍 수준(Degree of Multiprogramming)을 낮추어 각 프로세스가 사용할 수 있는 메모리 공간을 늘리는 방식이다.</p>
<p>또 다른 방법은 일부 프로세스를 일시적으로 중단하거나 제거하여, 남아 있는 프로세스들에게 더 많은 프레임(Frame)을 확보해주는 것이다. 이렇게 하면 각 프로세스가 자신의 워킹 셋(Working Set)을 메모리에 유지할 가능성이 높아지고, 페이지 부재(Page Fault)의 발생도 줄어들 수 있다.</p>
<p>결국 스래싱을 해결하려면 너무 많은 프로세스가 제한된 메모리를 나누어 쓰는 상황을 완화해야 한다. 각 프로세스가 필요한 페이지들을 충분히 유지할 수 있도록 프레임을 확보해주는 것이 핵심이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/3fd37965-c7e7-4854-9316-9a17743f0b35.png" alt="" style="display:block;margin:0 auto" />

<h3>스래싱(Thrashing) 방지 기법</h3>
<p><code>스래싱(Thrashing)</code>을 근본적으로 방지하기 위해서는 각 프로세스(Process)가 실행에 필요한 만큼의 메모리(Memory)를 유지할 수 있도록 관리하는 것이 중요하다. 즉, 프로세스가 자주 사용하는 페이지(Page)를 충분히 메모리에 올려둘 수 있어야 한다.</p>
<p>대표적인 방법은 <code>워킹 셋 모델(Working Set Model)</code>이다. 워킹 셋 모델은 각 프로세스가 일정 시간 동안 자주 사용하는 페이지들의 집합, 즉 워킹 셋(Working Set)을 메모리에 유지하도록 관리하는 방식이다. 프로세스의 워킹 셋을 메모리에 유지할 수 있으면 페이지 부재(Page Fault)를 줄이고 스래싱을 예방할 수 있다.</p>
<p>또 다른 방법은 <code>페이지 부재 빈도(Page Fault Frequency, PFF)</code>를 기준으로 프레임(Frame) 수를 조절하는 방식이다. 페이지 부재율(Page Fault Rate)이 높으면 해당 프로세스에 할당된 프레임 수가 부족하다는 의미로 보고 프레임을 늘려준다. 반대로 페이지 부재율이 낮으면 프레임이 과도하게 할당된 상태일 수 있으므로 프레임 수를 줄인다.</p>
<p>즉, 스래싱 방지의 핵심은 프로세스가 필요로 하는 페이지를 충분히 유지할 수 있도록 프레임 수를 조절하는 것이다. 이를 통해 페이지 부재가 과도하게 발생하지 않도록 하고, 시스템 성능 저하를 막을 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/5fa85e86-bcc6-4f9c-9f24-433bc1a2c49d.png" alt="" style="display:block;margin:0 auto" />

<h3>페이지 부재 빈도(Page Fault Frequency, PFF) 기반 스래싱(Thrashing) 방지</h3>
<p><code>페이지 부재 빈도(Page Fault Frequency, PFF) 방식</code>은 <code>페이지 부재율(Page Fault Rate)</code>을 기준으로 <code>프레임 수(Frame Count)</code>를 조절하여 <code>스래싱(Thrashing)</code>을 방지하는 방법이다.</p>
<p>이 그래프는 페이지 부재율이 일정한 범위 안에 유지되도록 프레임 수를 조절하는 흐름을 보여준다. 페이지 부재율이 상한(Upper Bound)을 넘으면, 해당 프로세스(Process)에 할당된 프레임 수가 부족하다는 의미로 볼 수 있다. 따라서 프레임 수를 증가시켜 더 많은 페이지(Page)를 메모리(Memory)에 유지하도록 하고, 페이지 부재율을 낮춘다.</p>
<p>반대로 페이지 부재율이 하한(Lower Bound)보다 낮으면, 해당 프로세스에 프레임이 과도하게 할당되어 있을 가능성이 있다. 이 경우에는 프레임 수를 줄여 다른 프로세스가 메모리를 사용할 수 있도록 한다.</p>
<p>즉, PFF 방식은 페이지 부재율이 너무 높지도, 너무 낮지도 않게 일정 범위 안에서 유지되도록 프레임 수를 조절하는 기법이다. 이를 통해 각 프로세스가 필요한 만큼의 메모리를 확보하게 하고, 페이지 부재가 과도하게 발생하는 스래싱 상태를 예방할 수 있다.</p>
<hr />
<h1>3️⃣ Virtual Memory Operation Observation Practice</h1>
<h3>가상메모리(Virtual Memory) 실습 확인</h3>
<p>지금부터는 가상메모리(Virtual Memory) 환경에서 페이지(Page)가 실제로 어떻게 동작하는지 프로그램 실행을 통해 확인한다.</p>
<p>앞에서 <code>요구 페이징(Demand Paging)</code>과 <code>페이지 부재(Page Fault)</code>의 개념을 살펴보았다. 요구 페이징은 프로그램 실행 중 필요한 페이지를 그때그때 메모리(Memory)로 가져오는 방식이고, 페이지 부재는 필요한 페이지가 현재 메모리에 없을 때 발생하는 현상이다.</p>
<p>이제는 이러한 개념들이 실제 시스템(System)에서 어떻게 나타나는지 직접 확인해볼 차례이다. 프로그램을 실행하면서 메모리 접근 과정에서 페이지가 어떻게 적재되고, 페이지 부재가 어떤 방식으로 발생하는지 살펴볼 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/456d249e-d69a-4cb0-9d38-0bd56fe68856.png" alt="" style="display:block;margin:0 auto" />

<h3>가상메모리(Virtual Memory) 실습 목표</h3>
<p>이번 실습에서는 가상 주소 공간(Virtual Address Space)을 확보하는 시점과 실제 메모리(Memory)에 적재되는 시점의 차이를 확인한다. 또한 페이지(Page)에 처음 접근했을 때와 다시 접근했을 때 어떤 차이가 나타나는지도 비교한다.</p>
<p>추가로 <code>fork</code> 이후에 <code>복사 후 쓰기(Copy-on-Write, COW)</code>가 어떻게 동작하는지도 함께 살펴본다. 이를 위해 단순히 코드를 실행하는 것이 아니라, 특정 기준을 가지고 결과를 관찰하는 것이 중요하다.</p>
<p>이번 실습에서는 <code>mmap, fork, getrusage()</code> 호출을 사용해 메모리 동작을 확인한다. 특히 <code>getrusage()</code>는 프로그램 실행 중 발생한 페이지 부재(Page Fault) 횟수를 확인할 수 있게 해준다.</p>
<p>여기서 중요한 관찰 기준은 페이지 부재이다. 페이지 부재는 크게 <code>마이너 페이지 부재(Minor Page Fault)</code>와 <code>메이저 페이지 부재(Major Page Fault)</code>로 나눌 수 있다.</p>
<p><code>마이너 페이지 부재</code>는 디스크 접근 없이 메모리 할당이나 페이지 연결이 이루어지는 경우를 의미한다. 반면 <code>메이저 페이지 부재</code>는 필요한 데이터를 가져오기 위해 실제 디스크 입출력(Disk I/O)이 발생하는 경우를 의미한다.</p>
<p>따라서 이번 실습에서는 페이지에 처음 접근하거나 쓰기 작업이 발생하는 순간, 마이너 페이지 부재 값이 어떻게 변화하는지를 중심으로 관찰한다. 이를 통해 가상메모리 환경에서 페이지가 실제로 언제 메모리에 적재되고, 요구 페이징(Demand Paging)이 어떻게 나타나는지 확인할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/e228ddf6-0e24-4aff-8ae5-93bcd17131c9.png" alt="" style="display:block;margin:0 auto" />

<h3>요구 페이징(Demand Paging) 관찰</h3>
<p>이번 실습에서는 <code>페이지(Page)</code>에 처음 접근했을 때 <code>페이지 부재(Page Fault)</code>가 발생하는지, 그리고 실제 메모리(Memory)가 언제 할당되는지를 확인한다.</p>
<p><code>요구 페이징(Demand Paging)</code>의 동작 방식에서 <code>mmap()</code> 함수를 사용하면 먼저 가상 주소 공간(Virtual Address Space)만 확보된다. 이 시점에서는 실제 물리 메모리(Physical Memory)가 아직 할당된 상태가 아니다.</p>
<p>이후 프로그램이 해당 메모리에 실제로 접근하는 순간, 비로소 물리 메모리에 적재가 이루어진다. 이 과정에서 각 페이지에 처음 접근할 때마다 페이지 부재가 발생할 수 있다.</p>
<p>즉, <code>mmap()</code>으로 주소 공간을 확보하는 것과 실제 메모리가 할당되는 시점은 다르다. 이번 실습에서는 이 차이를 확인하면서, 요구 페이징이 실제 시스템에서 어떻게 나타나는지 관찰한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/26cc914d-9856-4fba-bf4a-b02fb25a900c.png" alt="" style="display:block;margin:0 auto" />

<h3>핵심 코드(Core Code)</h3>
<p>핵심 코드는 <code>mmap()</code>을 통해 가상 주소 공간(Virtual Address Space)을 확보하고, 이후 각 페이지(Page)에 실제로 접근하면서 페이지 부재(Page Fault)가 발생하는지를 확인하는 부분이다.</p>
<p>먼저 <code>char *buf = mmap(...);</code> 코드는 <code>mmap()</code> 함수를 사용해 가상 주소 공간을 확보하는 역할을 한다. 이 시점에서는 주소 공간만 예약된 상태이고, 실제 물리 메모리(Physical Memory)는 아직 할당되지 않는다.</p>
<p>그다음 <code>buf[i * page_size] = 1;</code> 코드는 각 페이지에 한 번씩 접근하도록 만든다. 이 접근이 일어나는 순간 해당 페이지가 처음으로 사용되고, 그때 실제 물리 메모리에 적재가 이루어진다. 이 과정에서 페이지 부재가 발생한다.</p>
<p>여기서 중요한 점은 각 페이지는 처음 접근할 때만 페이지 부재가 발생한다는 것이다. 같은 페이지에 다시 접근하면 이미 메모리에 올라와 있기 때문에 추가적인 페이지 부재는 발생하지 않는다.</p>
<p>즉, 이 실습에서 페이지 부재는 단순한 접근 횟수보다 서로 다른 페이지를 처음 접근한 횟수와 관련이 있다. <code>mmap()</code>으로 주소 공간을 확보하는 시점과 실제 메모리가 할당되는 시점이 다르다는 것을 이 코드를 통해 확인할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/8cabd1b8-d6c9-41c8-b611-0c2df21fac5d.png" alt="" style="display:block;margin:0 auto" />

<h3>요구 페이징(Demand Paging) 관찰 코드</h3>
<p>이 코드는 페이지 부재(Page Fault)가 언제 발생하는지를 확인하기 위해 세 가지 시점으로 나누어 측정한다.</p>
<p>먼저 <code>long f1 = get_faults();</code>를 통해 프로그램 시작 시점의 페이지 부재 횟수를 저장한다. 그 다음 <code>char *buf = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);</code>를 호출하여 가상 주소 공간(Virtual Address Space)을 확보한다.</p>
<p>이후 <code>long f2 = get_faults();</code>를 통해 <code>mmap()</code> 호출 이후의 페이지 부재 횟수를 확인한다. 이 시점에서는 실제 메모리 접근이 이루어진 것이 아니기 때문에 페이지 부재 횟수는 거의 변하지 않는다. 즉, <code>mmap()</code>은 가상 주소 공간만 확보할 뿐, 실제 물리 메모리(Physical Memory)를 바로 할당하는 것은 아니다.</p>
<p>그 다음 반복문 <code>for (int i = 0; i &lt; pages; i++) buf[i * page_size] = 1;</code>을 통해 각 페이지(Page)에 한 번씩 접근한다. 이 부분이 각 페이지에 처음 접근하는 순간이며, 이때 실제 물리 메모리 적재가 이루어지고 페이지 부재가 발생한다.</p>
<p>마지막으로 <code>f3 = get_faults();</code>를 통해 접근 이후의 페이지 부재 횟수를 확인하면, 접근한 페이지 수만큼 페이지 부재가 증가한 것을 확인할 수 있다.</p>
<p>즉, 이 코드는 <code>mmap()</code> 시점에는 페이지 부재가 거의 발생하지 않지만, 실제로 각 페이지에 접근하는 순간 페이지 부재가 발생한다는 것을 확인하기 위한 코드이다. 이를 통해 <code>요구 페이징(Demand Paging)</code>에서는 주소 공간을 확보하는 시점과 실제 메모리가 할당되는 시점이 다르다는 것을 알 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/905bf2ab-dab4-4797-8c4f-ada7c9d6b36c.png" alt="" style="display:block;margin:0 auto" />

<h3>요구 페이징(Demand Paging) 관찰 결과</h3>
<p>실행 결과를 보면 세 시점에서의 <code>마이너 폴트(Minor Fault)</code> 값을 비교할 수 있다. 이번 실습에서는 특히 마이너 폴트의 변화에 집중한다.</p>
<p>먼저 프로그램 시작 시점에서는 마이너 폴트 값이 <code>minor: 320</code>처럼 일정한 값으로 나타난다. 그다음 <code>mmap()</code> 호출 이후를 보면 마이너 폴트 값이 <code>minor: 322</code> 정도로 거의 변하지 않은 것을 확인할 수 있다. 이는 mmap() 함수가 실제 물리 메모리(Physical Memory)를 바로 할당하는 것이 아니라, 가상 주소 공간(Virtual Address Space)만 확보한다는 것을 보여준다.</p>
<p>반면 실제로 페이지(Page)에 접근한 이후에는 마이너 폴트 값이 <code>minor: 1340</code>처럼 크게 증가한다. 이 증가는 각 페이지에 처음 접근하는 순간 페이지 부재(Page Fault)가 발생했기 때문이다. 즉, 확보된 가상 주소 공간에 실제로 접근할 때 물리 메모리가 연결되면서 마이너 폴트가 증가한 것이다.</p>
<p>따라서 이 실습 결과의 핵심은 <code>mmap()</code> 이후에는 페이지 부재가 거의 증가하지 않지만, 실제 페이지에 접근하는 순간 마이너 폴트가 크게 증가한다는 점이다. 이를 통해 요구 페이징(Demand Paging)에서는 가상 주소 공간을 확보하는 시점과 실제 메모리가 할당되는 시점이 다르다는 것을 확인할 수 있다.</p>
<p>단, 마이너 폴트의 숫자 자체는 실행 환경에 따라 달라질 수 있다. 운영체제(Operating System)의 상태나 기존 실행 이력에 따라 초기값이나 증가량은 다르게 나타날 수 있으므로, 중요한 것은 숫자 자체보다 mmap() 이후와 실제 접근 이후의 변화 흐름이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/e09e04c2-bab1-4dcf-aec3-6604225255f8.png" alt="" style="display:block;margin:0 auto" />

<h3>요구 페이징(Demand Paging) 관찰 결과 해석</h3>
<p>실행 결과를 정리하면, <code>mmap()</code> 함수는 실제 물리 메모리(Physical Memory)를 바로 할당하는 것이 아니라 가상 주소 공간(Virtual Address Space)만 확보한다는 것을 확인할 수 있다.</p>
<p>따라서 <code>mmap()</code>을 호출한 직후에는 페이지 부재(Page Fault)가 거의 증가하지 않는다. 아직 해당 주소 공간에 실제로 접근하지 않았기 때문이다.</p>
<p>반면 확보된 주소 공간의 각 페이지(Page)에 실제로 접근하는 순간, 그때 물리 메모리에 적재가 이루어진다. 이 과정에서 <code>마이너 폴트(Minor Fault)</code>가 증가한다.</p>
<p>즉, 요구 페이징(Demand Paging)에서는 주소 공간을 확보하는 시점과 실제 메모리가 할당되는 시점이 다르다. 실제 메모리는 페이지에 처음 접근하는 순간 할당되며, 이때 페이지 부재가 발생한다고 이해할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/aa96e047-46b0-4b8b-b600-9b6fb4dec06c.png" alt="" style="display:block;margin:0 auto" />

<h3>실행 결과 해석(Result Interpretation)</h3>
<p><code>Fault</code>의 의미를 보면, <code>마이너 폴트(Minor Fault)</code>는 디스크 접근(Disk Access) 없이 메모리 할당(Memory Allocation)이 이루어지는 경우를 의미한다. 반면 <code>메이저 폴트(Major Fault)</code>는 필요한 데이터를 가져오기 위해 디스크 입출력(Disk I/O)이 발생하는 경우를 의미한다.</p>
<p>이번 실습에서는 마이너 폴트가 증가하는 모습을 통해 요구 페이징(Demand Paging)의 동작을 확인하였다. <code>mmap()</code> 함수로 가상 주소 공간(Virtual Address Space)을 확보했을 때는 실제 물리 메모리(Physical Memory)가 바로 할당되지 않았고, 페이지(Page)에 실제로 접근하는 순간 메모리가 사용되면서 마이너 폴트가 증가하였다.</p>
<p>결국 가상메모리(Virtual Memory)의 핵심은 메모리가 할당되는 시점이 단순히 주소 공간을 확보한 시점이 아니라, 실제로 해당 페이지에 접근하는 시점이라는 점이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/54aa9e37-3085-4bd9-9b8b-7b0e6de23652.png" alt="" style="display:block;margin:0 auto" />

<p>실습 결과</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/e5fd1280-8a28-4292-9d10-ecd73d65769d.png" alt="" style="display:block;margin:0 auto" />

<h3>요구 페이징 재접근 특성 관찰 Observation of Re-access Characteristics in Demand Paging</h3>
<p>이번 실습에서는 페이지(Page)에 처음 접근했을 때 페이지 부재(Page Fault)가 발생하는 것에서 한 단계 더 나아가, 이미 접근했던 페이지에 다시 접근했을 때 어떤 변화가 있는지 확인한다.</p>
<p>핵심은 동일한 페이지를 다시 접근할 때 페이지 부재가 다시 발생하는지 관찰하는 것이다. <code>요구 페이징(Demand Paging)</code>에서는 페이지에 처음 접근하는 순간 물리 메모리(Physical Memory)에 적재가 이루어지고, 이때 페이지 부재가 발생한다.</p>
<p>하지만 한 번 메모리에 적재된 페이지는 이후 다시 접근할 때 그대로 재사용될 수 있다. 따라서 같은 페이지에 다시 접근하는 경우에는 일반적으로 추가적인 페이지 부재가 발생하지 않는다.</p>
<p>이번 실습에서는 처음 접근과 재접근을 비교하면서, 페이지가 한 번 물리 메모리에 올라온 이후에는 어떻게 재사용되는지 확인한다. 이를 통해 요구 페이징에서 페이지 부재는 단순한 접근 횟수가 아니라, 처음 접근하는 페이지 수와 관련이 있다는 점을 이해할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/c91323c6-fc88-42e0-af95-61595b0c6e80.png" alt="" style="display:block;margin:0 auto" />

<h3>Observation of Re-access Characteristics in Demand Paging</h3>
<p>핵심 코드는 같은 방식으로 페이지(Page)에 접근하는 반복문이 두 번 실행되는 구조이다. 이를 통해 처음 접근할 때와 다시 접근할 때 페이지 부재(Page Fault)가 어떻게 달라지는지 확인할 수 있다.</p>
<p>첫 번째 루프(First Loop)는 각 페이지에 처음 접근하는 과정이다. 이때 각 페이지는 아직 물리 메모리(Physical Memory)에 올라와 있지 않기 때문에, 페이지마다 페이지 부재가 발생한다. 즉, 처음 접근하는 순간 실제 메모리 적재가 이루어진다.</p>
<p>반면 두 번째 루프(Second Loop)는 이미 한 번 접근했던 동일한 페이지에 다시 접근하는 과정이다. 이 경우 해당 페이지들은 이미 메모리에 올라와 있기 때문에 추가적인 페이지 부재는 거의 발생하지 않는다.</p>
<p>즉, 이 코드는 첫 접근(First Access)과 재접근(Re-access)의 차이를 확인하기 위한 예제이다. 페이지는 한 번 메모리에 올라오면 이후 다시 접근할 때 그대로 재사용될 수 있으며, 같은 페이지에 반복해서 접근한다고 해서 매번 페이지 부재가 발생하는 것은 아니다라는 걸 알수있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/367088ab-0eb3-49f1-94c5-d29ae1ac6792.png" alt="" style="display:block;margin:0 auto" />

<h3>Core Code for Observing Re-access Characteristics in Demand Paging</h3>
<p>이 코드는 같은 페이지(Page)에 대한 접근을 두 번 수행하면서, 각각 페이지 폴트(Page Fault)의 변화를 비교하는 코드이다.</p>
<p>먼저 첫 번째 순회(First Pass)에서는 before 값을 통해 현재 <code>마이너 폴트(Minor Fault)</code> 수를 저장한 뒤, 반복문을 통해 각 페이지에 한 번씩 접근한다. 이때 모든 페이지는 처음 접근되는 상태이기 때문에, 각 페이지마다 페이지 폴트가 발생한다. 따라서 첫 번째 순회 결과에서는 페이지 수만큼 마이너 폴트가 증가하는 것을 확인할 수 있다.</p>
<p>두 번째 순회(Second Pass)에서는 이미 한 번 접근했던 동일한 페이지에 다시 접근한다. 이 경우 해당 페이지들은 이미 물리 메모리(Physical Memory)에 올라와 있는 상태이기 때문에, 추가적인 페이지 폴트는 거의 발생하지 않는다. 따라서 두 번째 순회의 결과는 첫 번째 순회와 비교했을 때 매우 작거나 0에 가까운 값으로 나타난다.</p>
<p>결국 이 코드는 첫 접근(First Access)에서는 페이지 폴트가 발생하지만, 재접근(Re-access) 시에는 같은 페이지가 이미 메모리에 적재되어 있기 때문에 페이지 폴트가 거의 발생하지 않는다는 점을 수치적으로 확인하기 위한 예제라고 할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a9fda3c8-093b-4881-a2e0-e7eaf79667a0.png" alt="" style="display:block;margin:0 auto" />

<h3>Example Execution Result</h3>
<p>실행 결과를 보면 첫 번째 순회(First Pass)에서 <code>마이너 폴트(Minor Fault)가 1024</code>만큼 증가한 것을 확인할 수 있다. 이는 페이지(Page) 수가 1024개이기 때문에, 각 페이지에 처음 접근하면서 모든 페이지에서 페이지 폴트(Page Fault)가 발생했기 때문이다.</p>
<p>반면 두 번째 순회(Second Pass)에서는 <code>마이너 폴트 증가량이 0</code>으로 나타난다. 이는 첫 번째 순회에서 이미 접근했던 페이지들이 모두 물리 메모리(Physical Memory)에 올라와 있기 때문이다. 따라서 같은 페이지에 다시 접근할 때는 추가적인 페이지 폴트가 발생하지 않는다.</p>
<p>이 결과를 통해 페이지는 처음 접근할 때 메모리에 적재되고, 한 번 적재된 이후에는 다시 접근해도 그대로 재사용된다는 점을 확인할 수 있다. 즉, 요구 페이징(Demand Paging)에서는 페이지 폴트가 매번 발생하는 것이 아니라, 해당 페이지에 처음 접근할 때 주로 발생한다고 이해할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/133cf88a-5cf7-4fad-82f5-12c7ab2d766a.png" alt="" style="display:block;margin:0 auto" />

<h3>Result Interpretation</h3>
<p>지금까지의 결과를 정리하면, 페이지(Page)에 처음 접근할 때는 해당 페이지가 아직 메모리(Memory)에 없기 때문에 <code>페이지 폴트(Page Fault)</code>가 발생한다.</p>
<p>하지만 한 번 물리 메모리(Physical Memory)에 적재된 이후에는 같은 페이지에 다시 접근하더라도 추가적인 페이지 폴트는 발생하지 않는다. 이미 해당 페이지가 메모리에 올라와 있기 때문에 그대로 재사용될 수 있기 때문이다.</p>
<p>또한 페이지 폴트의 증가량은 단순한 접근 횟수로 결정되는 것이 아니라, 처음 접근한 서로 다른 페이지 수에 의해 결정된다. 이는 메모리가 <code>페이지 단위(Page Unit)</code>로 관리되기 때문이다.</p>
<p>따라서 이 실습의 핵심은 각 페이지는 처음 접근할 때만 페이지 폴트가 발생하고, 한 번 적재된 페이지는 메모리에 유지되는 동안 재사용된다는 점이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f6f2339c-6040-4c9d-abf3-d4822734ed42.png" alt="" style="display:block;margin:0 auto" />

<p>실습결과</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/0faa3c6c-5937-4be2-b6fa-e04076f6bbac.png" alt="" style="display:block;margin:0 auto" />

<h3>Copy-on-Write 관찰(Observation of Copy-on-Write)</h3>
<p>세 번째 실습에서는 <code>fork()</code> 이후 메모리(Memory)가 어떻게 복사되는지 확인한다. 특히 쓰기 작업(Write)이 발생하는 시점에 <code>Copy-on-Write</code>가 어떻게 동작하는지, 그리고 그 결과 부모 프로세스(Parent Process)와 자식 프로세스(Child Process)의 메모리가 어떻게 분리되는지를 살펴본다.</p>
<p>핵심 개념은 <code>fork()</code>가 호출되었을 때 부모 프로세스와 자식 프로세스가 처음부터 메모리를 완전히 따로 복사해서 가지는 것이 아니라는 점이다. 처음에는 두 프로세스가 같은 메모리 공간을 공유하는 상태로 시작한다.</p>
<p>하지만 이후 부모 프로세스나 자식 프로세스 중 하나가 해당 메모리에 쓰기 작업을 수행하면, 그 순간 실제 복사가 이루어진다. 이렇게 쓰기 시점에 복사가 발생하는 방식을 <code>Copy-on-Write</code>라고 한다.</p>
<p>즉, <code>Copy-on-Write</code>는 처음에는 메모리를 공유하다가, 실제로 내용을 변경하려는 순간에만 복사하여 부모와 자식의 메모리를 분리하는 방식이다. 이를 통해 불필요한 메모리 복사를 줄이고, fork() 이후의 메모리 사용을 더 효율적으로 관리할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/e28f2465-7ec7-431b-b619-597e30c2c271.png" alt="" style="display:block;margin:0 auto" />

<h3>Copy-on-Write Core Code</h3>
<p><code>Copy-on-Write</code> 핵심 코드는 크게 세 단계로 나누어 이해할 수 있다.</p>
<p>먼저 <code>fork()</code> 이전에는 <code>buf[i * page_size] = 1</code> 코드를 통해 부모 프로세스(Parent Process)가 각 페이지(Page)에 먼저 접근한다. 이 과정에서 해당 페이지들은 물리 메모리(Physical Memory)에 적재된다.</p>
<p>그 이후 <code>pid_t pid = fork();</code>가 호출되면 부모 프로세스와 자식 프로세스(Child Process)가 생성된다. 이 시점에서는 메모리 전체가 바로 복사되는 것이 아니라, 두 프로세스가 같은 페이지를 공유하는 상태가 된다.</p>
<p>이후 자식 프로세스가 <code>buf[i * page_size] += 1</code>과 같이 공유 중인 페이지에 쓰기 작업(Write)을 시도하면 <code>Copy-on-Write</code>가 발생한다. 공유된 페이지는 바로 수정될 수 없기 때문에, 쓰기 시점에 <code>페이지 폴트(Page Fault)</code>가 발생한다.</p>
<p>운영체제(Operating System)는 이 페이지 폴트를 계기로 해당 페이지를 복사하고, 자식 프로세스에게 새로운 페이지를 할당한다. 그 결과 자식 프로세스는 복사된 새 페이지를 사용하고, 부모 프로세스는 기존 페이지를 그대로 유지한다.</p>
<p>즉, <code>Copy-on-Write</code>는 <code>fork()</code> 직후에는 메모리를 공유하다가, 실제 쓰기 작업이 발생하는 순간에만 페이지를 복사하여 부모와 자식 프로세스의 메모리를 분리하는 방식이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a8d3599d-6d07-4547-bb49-e6a3a39237b9.png" alt="" style="display:block;margin:0 auto" />

<h3>Copy-on-Write Core Code</h3>
<p>이 코드는 Copy-on-Write가 어떤 시점에서 발생하는지를 확인하기 위한 코드이다.</p>
<p>먼저 fork() 이전에는 부모 프로세스(Parent Process)가 반복문을 통해 각 페이지(Page)에 한 번씩 접근한다. 이 과정에서 페이지가 물리 메모리(Physical Memory)에 적재된다.</p>
<p>그다음 fork()가 호출되면 부모 프로세스와 자식 프로세스(Child Process)가 생성된다. 이 시점에서는 실제 메모리 복사가 일어나지 않고, 두 프로세스가 동일한 페이지를 공유한다.</p>
<p>이후 자식 프로세스에서 <code>buf[i * page_size] += 1</code> 코드가 실행되면 공유된 페이지에 쓰기 작업(Write)이 발생한다. 이때 공유된 페이지는 읽기 전용(Read-only) 상태이기 때문에 페이지 폴트(Page Fault)가 발생한다.</p>
<p>운영체제(Operating System)는 이 페이지 폴트를 계기로 해당 페이지를 복사하고, 자식 프로세스에게 새로운 페이지를 할당한다. 이 과정이 <code>Copy-on-Write</code>이다.</p>
<p>따라서 자식 프로세스에서는 쓰기 작업이 발생한 시점에 마이너 폴트(Minor Fault)가 증가하는 것을 확인할 수 있다. 반면 부모 프로세스는 기존 페이지를 그대로 유지하므로, 자식 프로세스의 쓰기 작업이 부모 프로세스의 메모리 값에는 영향을 주지 않는다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/403a67e7-3bf6-4012-9172-7d9794fc4e2a.png" alt="" style="display:block;margin:0 auto" />

<h3>Copy-on-Write Observation Result</h3>
<p>실행 결과를 보면 자식 프로세스(Child Process)와 부모 프로세스(Parent Process)의 차이를 확인할 수 있다.</p>
<p>먼저 자식 프로세스에서는 마이너 폴트(Minor Fault)가 256만큼 증가한 것을 볼 수 있다. 이는 자식 프로세스가 각 페이지(Page)에 쓰기 작업(Write)을 수행하는 순간마다 페이지 폴트(Page Fault)가 발생했기 때문이다. 즉, 자식 프로세스가 접근한 페이지 수만큼 Copy-on-Write가 발생했다고 볼 수 있다.</p>
<p>또한 자식 프로세스의 첫 번째 페이지 값은 기존 값 1에서 2로 증가하였다. 이는 자식 프로세스가 해당 페이지에 쓰기 작업을 수행했기 때문이다.</p>
<p>반면 부모 프로세스의 첫 번째 페이지 값은 여전히 1로 유지된다. 이는 자식 프로세스가 공유 페이지를 직접 수정한 것이 아니라, 쓰기 시점에 별도의 페이지를 복사해서 사용했기 때문이다.</p>
<p>따라서 Copy-on-Write는 fork() 직후에는 부모와 자식이 같은 페이지를 공유하지만, 쓰기 작업이 발생하는 순간 페이지를 복사하여 두 프로세스의 메모리를 분리한다. 이로 인해 자식 프로세스의 변경은 부모 프로세스에 영향을 주지 않는다는 것을 알 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/8d522f38-af29-45f2-b66a-49c5ffea6f86.png" alt="" style="display:block;margin:0 auto" />

<h3>Copy-on-Write Result Interpretation</h3>
<p>지금까지의 결과를 정리하면, <code>fork()</code> 직후에는 부모 프로세스(Parent Process)와 자식 프로세스(Child Process)가 동일한 페이지(Page)를 공유한다. 이 상태에서는 실제 메모리 복사(Memory Copy)가 바로 이루어지지 않고, 같은 데이터를 함께 사용하는 구조가 된다.</p>
<p>이후 자식 프로세스가 해당 페이지에 쓰기 작업(Write)을 수행하면, 그 시점에서 Copy-on-Write가 발생한다. 즉, 복사는 fork()가 호출되는 순간이 아니라 실제로 write가 발생하는 순간에 이루어진다.</p>
<p>이때 자식 프로세스는 새롭게 복사된 페이지를 사용하고, 부모 프로세스는 기존 페이지를 그대로 유지한다. 따라서 자식 프로세스가 값을 변경하더라도 부모 프로세스의 메모리 값에는 영향을 주지 않는다.</p>
<p>또한 Copy-on-Write는 페이지 폴트(Page Fault)를 이용해 실제 복사 시점을 지연시키는 방식이라고 이해할 수 있다. 처음에는 메모리를 공유하다가, 쓰기 작업이 발생할 때만 필요한 페이지를 복사함으로써 불필요한 메모리 복사를 줄일 수 있다.</p>
<p>이번 실습은 복잡한 구현보다는 코드를 실행하고 결과를 관찰하는 것이 핵심이다. mmap(), fork(), getrusage()를 통해 가상 주소 공간(Virtual Address Space), 요구 페이징(Demand Paging), 페이지 폴트(Page Fault), Copy-on-Write의 동작을 실제 결과로 확인하는 데 목적이 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f14755a8-2ebb-47f9-996d-66ddb30020b4.png" alt="" style="display:block;margin:0 auto" />

<p>실습 결과</p>
<hr />
]]></content:encoded></item><item><title><![CDATA[Test Design and Test Execution Automation]]></title><description><![CDATA[1️⃣ Core Concepts of Test Automation2️⃣ Test Design Automation3️⃣ Test Execution Automation

1️⃣ Core Concepts of Test Automation
테스트 자동화의 학습목표 (Learning Objectives of Test Automation)
이번 강의의 목표는 테스트 ]]></description><link>https://heesu.tech/test-design-and-test-execution-automation</link><guid isPermaLink="true">https://heesu.tech/test-design-and-test-execution-automation</guid><dc:creator><![CDATA[Heesu Noh]]></dc:creator><pubDate>Tue, 19 May 2026 15:45:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/4d0c17ee-7992-428c-ad2a-4c22f8813a0e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>1️⃣ Core Concepts of Test Automation<br />2️⃣ Test Design Automation<br />3️⃣ Test Execution Automation</p>
<hr />
<h1>1️⃣ Core Concepts of Test Automation</h1>
<h3>테스트 자동화의 학습목표 (Learning Objectives of Test Automation)</h3>
<p>이번 강의의 목표는 테스트 자동화(Test Automation)의 개념과 적용 범위를 이해하는 것이다. 테스트 자동화란 사람이 반복적으로 수행하던 테스트 활동을 도구나 프로그램을 이용해 자동으로 처리하거나 지원하는 것을 말한다. 자동화는 테스트 시간을 줄이고, 반복 테스트를 일관되게 수행하며, 테스트 효율을 높이기 위해 필요하다.</p>
<p>테스트 자동화는 테스트 프로세스의 여러 단계에 적용될 수 있다. 그중 대표적인 것이 테스트 설계 자동화(Test Design Automation)이다. 이는 테스트 케이스(Test Case), 테스트 데이터(Test Data), 테스트 조건(Test Condition) 등을 자동으로 생성하거나 설계 과정을 지원하는 것이다.</p>
<p>또 다른 핵심 영역은 테스트 수행 자동화(Test Execution Automation)이다. 이는 작성된 테스트를 자동으로 실행하고, 실제 결과(Actual Result)와 기대 결과(Expected Result)를 비교하며, 테스트 결과를 기록하는 과정을 지원한다.</p>
<p>결국 테스트 자동화는 단순히 테스트를 실행하는 것만이 아니라, 테스트 설계부터 수행까지 테스트 활동 전반의 효율성과 정확성을 높이는 데 목적이 있다.</p>
<hr />
<h2>테스트 자동화 핵심 개념 (Core Concepts of Test Automation)</h2>
<p>테스트 자동화(Test Automation)는 테스트 프로세스의 전체 또는 일부를 도구(Tool)를 활용해 자동으로 수행하거나 지원하는 것을 의미한다. 테스트 프로세스에는 <code>테스트 계획(Test Planning), 테스트 설계(Test Design), 테스트 수행(Test Execution), 테스트 평가(Test Evaluation), 테스트 개선(Test Improvement)</code> 등이 포함되며, 이 중 필요한 부분을 자동화할 수 있다.</p>
<p>일반적으로 <code>자동화(Automation)</code>는 사람이 직접 수행하던 반복적인 절차를 시스템이나 도구가 대신 수행하도록 만드는 것이다. 사람이 직접 모든 단계를 수행하면 입력 실수, 절차 누락, 결과 확인 오류 등이 발생할 수 있고, 시간과 비용도 많이 든다. 반면 자동화를 적용하면 반복 작업에서 발생하는 실수를 줄이고, 사람은 더 창의적이고 중요한 업무에 집중할 수 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/289c881d-1b3e-4d6c-a681-2084b63a16e7.png" alt="" style="display:block;margin:0 auto" />

<p>예를 들어 로그인 기능을 매뉴얼 테스트(Manual Testing)로 수행하는 경우, 테스터는 웹사이트 주소를 입력하고, 아이디와 비밀번호를 입력한 뒤, 로그인 버튼을 클릭하고, Welcome 페이지가 정상적으로 나타나는지 직접 확인해야 한다.  </p>
<h3>Selenium과 JUnit을 활용한 로그인 테스트 자동화 예시</h3>
<h3>(Login Test Automation with Selenium and JUnit)</h3>
<p>문법을 자세히 이해하는 것보다 중요한 것은 각 코드가 어떤 역할을 하는지 이해하는 것이다. 예를 들어 로그인 기능을 자동화하는 테스트 메소드가 <code>shouldVisitWelcomePage()</code>라고 하면, 이 메소드는 사용자가 로그인했을 때 Welcome 페이지로 정상 이동하는지를 확인하는 역할을 한다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/36f3b16e-55ae-4ef8-892d-ab4e6a5e3e14.png" alt="" style="display:block;margin:0 auto" />

<p>먼저 Selenium의 <code>driver.get()</code> 메소드는 테스트할 웹사이트 주소로 이동하는 기능을 한다. 매뉴얼 테스트(Manual Testing)에서는 테스터가 매번 웹사이트 주소를 직접 입력해야 하지만, 자동화 테스트(Automated Testing)에서는 driver.get("웹 사이트 주소")처럼 주소를 코드로 한 번 작성해두면 반복 실행할 때마다 동일한 주소로 접속할 수 있다. 이를 통해 주소 입력 실수를 줄일 수 있다.</p>
<p>다음으로 <code>driver.findElement().sendKeys()</code>는 웹페이지에서 특정 입력란을 찾아 값을 입력하는 역할을 한다. 예를 들어 아이디 입력란을 찾아 사용자 아이디를 입력하고, 비밀번호 입력란을 찾아 비밀번호를 입력할 수 있다. 웹페이지의 id 속성이나 xpath 등을 이용해 원하는 요소(Element)를 찾고, <code>sendKeys()</code>를 통해 값을 넣는 방식이다.</p>
<p>로그인 버튼을 누르는 과정은 <code>click()</code> 메소드를 통해 자동화할 수 있다. 예를 들어 로그인 버튼 요소를 찾은 뒤 <code>click()</code>을 실행하면, 사람이 직접 버튼을 클릭하는 것처럼 자동으로 로그인 버튼이 눌린다. 이렇게 웹사이트 접속, 아이디 입력, 비밀번호 입력, 버튼 클릭 과정을 코드로 작성해두면 같은 테스트를 여러 번 반복해도 동일한 절차로 실행할 수 있다.</p>
<p>마지막으로 JUnit의 <code>assertEquals()</code> 메소드는 기대 결과(Expected Result)와 실제 결과(Actual Result)를 비교하는 역할을 한다. 테스트에서는 예상한 결과와 실제 실행 결과가 같은지 확인하는 과정이 중요하다. 예를 들어 로그인 후 나타나야 하는 “웰컴 메시지”를 기대값으로 두고, 실제 웹페이지에서 getText()로 가져온 메시지와 비교한다. 두 값이 같으면 테스트가 정상적으로 통과한 것이고, 다르면 결함(Defect)이 발생했을 가능성이 있다고 볼 수 있다.</p>
<p>정리하면 Selenium은 웹페이지를 열고, 입력란에 값을 넣고, 버튼을 클릭하는 등 사용자의 행동을 자동화하는 데 사용된다. JUnit은 그 결과가 예상한 값과 같은지 검증하는 데 사용된다. 따라서 Selenium과 JUnit을 함께 사용하면 로그인 기능처럼 반복적으로 확인해야 하는 테스트를 효율적으로 자동화할 수 있다.</p>
<p>이러한 과정을 Selenium이나 JUnit 같은 테스트 도구를 활용해 자동화할 수 있다. Selenium은 웹 애플리케이션(Web Application)의 동작을 자동으로 제어하고 테스트할 수 있는 프레임워크이며, JUnit은 Java 기반의 테스트를 지원하는 도구이다. 예를 들어 Selenium을 이용하면 웹사이트 접속, 아이디 입력, 비밀번호 입력, 로그인 버튼 클릭 과정을 코드로 작성할 수 있다.</p>
<p>또한 JUnit의 <code>assertEquals()</code>와 같은 메소드를 사용하면 기대 결과(Expected Result)와 실제 결과(Actual Result)를 비교할 수 있다. 로그인 후 나타나야 하는 Welcome 메시지를 기대값으로 설정하고, 실제 웹페이지에서 가져온 메시지와 비교하여 두 값이 같으면 테스트가 정상적으로 수행된 것으로 볼 수 있다.</p>
<p>결국 테스트 자동화는 사람이 반복적으로 수행하던 테스트 절차를 코드와 도구로 자동화하여, 테스트 시간을 줄이고 실수를 줄이며 효율성을 높이는 방법이다. 특히 반복 테스트(Repeated Testing), 회귀 테스트(Regression Testing), 입력 데이터가 많은 테스트에서 효과적으로 활용된다.</p>
<hr />
<h3>테스트 자동화의 필요성 (Necessity of Test Automation)</h3>
<p>테스트 자동화(Test Automation)가 필요한 이유는 반복적인 테스트를 효율적으로 수행하고, 사람의 실수를 줄이며, 일관된 방식으로 테스트를 진행하기 위해서이다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/06e778ed-952a-45dc-9dd3-802027fe058a.png" alt="" style="display:block;margin:0 auto" />

<p>첫 번째 필요성은 반복적인 작업을 효율적으로 수행할 수 있다는 점이다. 소프트웨어는 요구사항이 계속 변경될 수 있기 때문에, 기존 기능이 여전히 정상적으로 동작하는지 반복해서 확인해야 한다. 이때 테스트 절차를 코드화해두면, 입력값만 바꿔가며 같은 테스트를 쉽게 반복할 수 있다. 특히 회귀 테스트(Regression Testing)처럼 기존 기능을 계속 다시 확인해야 하는 경우 자동화의 효과가 크다.</p>
<p>또한 부하 테스트(Load Testing)에서도 테스트 자동화가 필요하다. 예를 들어 시스템이 100명의 동시 접속자를 허용해야 한다면, 실제 사용자 100명을 모집해서 동시에 접속시키는 것은 비용과 시간이 많이 든다. 대신 테스트 스크립트(Test Script), 스레드(Thread), 프로세스(Process) 등을 이용해 여러 사용자가 동시에 접속하는 상황을 가상으로 만들 수 있다. 이를 통해 시스템이 많은 부하를 받는 상황에서도 안정적으로 동작하는지 확인할 수 있다.</p>
<p>두 번째 필요성은 실수를 줄이고 일관되게 테스트할 수 있다는 점이다. 사람이 직접 테스트를 수행하면 잘못된 데이터를 입력하거나, 정해진 절차를 빠뜨리는 실수가 발생할 수 있다. 하지만 테스트 자동화를 적용하면 미리 작성된 테스트 코드(Test Code)에 따라 동일한 절차가 반복적으로 실행되기 때문에, 테스트를 더 일관성 있게 수행할 수 있다.</p>
<p>예를 들어 로그인 기능을 테스트할 때 아이디 입력, 비밀번호 입력, 로그인 버튼 클릭, 결과 확인 과정을 코드로 작성해두면 매번 같은 방식으로 테스트할 수 있다. 이를 통해 입력 실수나 절차 누락을 줄일 수 있다.</p>
<p>다만 테스트 자동화가 모든 테스트에 항상 적합한 것은 아니다. 테스트 자동화는 테스트 프로세스 전체에 적용될 수도 있지만, 필요한 일부 과정에만 적용될 수도 있다. 특히 반복적으로 수행되는 부분, 실수가 자주 발생하는 부분, 많은 데이터를 다루는 부분이 자동화 대상이 된다.</p>
<p>반대로 반복이 거의 없거나, 화면 전환이 자연스러운지처럼 사람이 직접 보고 판단하는 것이 더 적절한 테스트도 있다. 예를 들어 e북에서 페이지가 부드럽게 넘어가는지 확인하는 경우에는 자동화보다 사람이 직접 확인하는 것이 더 명확할 수 있다.</p>
<p>결론적으로 테스트 자동화는 무조건 많이 적용하는 것이 아니라, 비용 대비 효과가 큰 부분에 집중해서 적용해야 한다. 반복이 많고 실수 가능성이 높은 테스트를 자동화할 때, 테스트 시간과 비용을 줄이고 테스트 품질을 높일 수 있게 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/3e4b10ee-b3df-469b-a7a1-139703c0c9e7.png" alt="" style="display:block;margin:0 auto" />

<h3>테스트 프로세스에서 자동화 가능한 부분</h3>
<h3>(Automation Areas in the Test Process)</h3>
<p>테스트 자동화(Test Automation)는 테스트 프로세스 전체에 적용될 수도 있고, 필요한 일부 단계에만 적용될 수도 있다. 따라서 <code>테스트 계획(Test Planning), 테스트 설계(Test Design), 테스트 수행(Test Execution), 테스트 평가(Test Evaluation), 테스트 개선(Test Improvement)</code> 등 여러 단계에서 자동화가 가능하다.</p>
<p>먼저 <code>테스트 계획(Test Planning</code>) 단계에서는 테스트 일정, 인력, 범위 등을 관리해야 한다. 이 과정은 사람이 직접 계획할 수도 있지만, 일정 관리 도구나 테스트 관리 도구(Test Management Tool)를 사용하면 일부 작업을 자동화하거나 체계적으로 관리할 수 있다.</p>
<p><code>테스트 설계(Test Design)</code> 단계에서는 테스트 케이스(Test Case), 테스트 데이터(Test Data), 테스트 조건(Test Condition) 등을 만드는 과정에서 자동화가 많이 활용될 수 있다. 특히 반복적으로 작성해야 하는 테스트 케이스나 다양한 입력값을 기반으로 하는 테스트 데이터 생성은 자동화의 효과가 크다.</p>
<p><code>테스트 수행(Test Execution)</code> 단계는 테스트 자동화가 가장 많이 적용되는 부분 중 하나이다. 작성된 테스트 케이스를 자동으로 실행하고, 기대 결과(Expected Result)와 실제 결과(Actual Result)를 비교하며, 테스트 결과를 기록하는 과정이 자동화될 수 있다.</p>
<p><code>테스트 평가(Test Evaluation)</code>와 <code>테스트 개선(Test Improvement)</code> 단계에서도 자동화가 가능하다. 테스트 수행 결과로 나온 데이터를 도구를 통해 분석하면, 결함 발생률, 테스트 성공률, 반복적으로 실패하는 기능 등을 파악할 수 있다. 이러한 데이터는 이후 테스트 프로세스를 개선하는 데 활용될 수 있다.</p>
<p>다만 테스트 자동화에서 특히 집중하는 부분은 <code>테스트 설계(Test Design)와 테스트 수행(Test Execution)</code>이다. 테스트 케이스를 만들고, 이를 실제로 실행하며 결과를 확인하는 과정에서 자동화의 효과가 크기 때문이다.</p>
<p>정리하면, 테스트 자동화는 테스트 프로세스의 여러 단계에 적용될 수 있지만, 모든 단계를 무조건 자동화하는 것은 아니다. 필요성과 효과를 고려하여 자동화할 부분을 선택해야 하며, 특히 테스트 설계와 테스트 수행 단계가 핵심적인 자동화 대상이 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/38f31d68-745e-4a79-a138-f52b37649312.png" alt="" style="display:block;margin:0 auto" />

<h3>테스트 설계 자동화의 개념 (Concept of Test Design Automation)</h3>
<p>두 번째 시간에는 <code>테스트 설계 자동화(Test Design Automation)</code>의 개념과 적용 방법을 간단한 예시를 통해 살펴본다.</p>
<p>테스트 설계 자동화란 <code>테스트 설계 프로세스(Test Design Process)</code>의 전체 또는 일부를 도구(Tool)를 활용해 자동화하는 것을 의미한다. 특히 테스트 케이스(Test Case)를 개발하거나, 테스트 수행을 위한 환경(Test Environment)을 준비하는 과정에서 반복적이고 실수가 발생하기 쉬운 작업이 자동화 대상이 된다.</p>
<p>테스트 설계 단계에서는 테스트할 조건을 정리하고, 그 조건을 바탕으로 테스트 케이스와 테스트 데이터를 만든다. 이 과정이 매번 수작업으로 이루어지면 누락이나 입력 오류가 발생할 수 있다. 따라서 도구를 사용해 테스트 케이스 생성, 테스트 데이터 준비, 테스트 환경 구성 등을 지원하면 더 효율적이고 일관된 테스트 설계가 가능하다.</p>
<p><code>ISO/IEC/IEEE 29119의 동적 테스트 프로세스(Dynamic Test Process</code>)를 기준으로 보면, 테스트 설계 자동화의 주요 대상은 <code>테스트 설계 및 구현(Test Design and Implementation)과 테스트 환경 구성 및 유지(Test Environment Set-up and Maintenance) 단계</code>이다. 즉, 테스트를 실제로 수행하기 전에 필요한 테스트 케이스, 테스트 데이터, 테스트 환경을 준비하는 부분이 테스트 설계 자동화의 핵심 범위라고 볼 수 있다.</p>
<p>정리하면, 테스트 설계 자동화는 테스트를 실행하기 전에 필요한 준비 과정을 도구로 지원하거나 자동화하는 것이다. 이를 통해 반복적인 설계 작업을 줄이고, 실수를 줄이며, 테스트 준비 과정을 더 체계적으로 관리할 수 있다.</p>
<hr />
<h3>테스트 케이스 개발 자동화 (Test Case Development Automation)</h3>
<p><code>테스트 케이스 개발 자동화(Test Case Development Automation)</code>는 테스트 설계 기법(Test Design Technique)을 도구에 적용하여 테스트 케이스(Test Case)를 자동으로 생성하거나 지원하는 것이다.</p>
<p>테스트 케이스는 보통 블랙박스 테스트 설계 기법(Black-box Test Design Technique)이나 화이트박스 테스트 설계 기법(White-box Test Design Technique)을 적용해 만든다. 하지만 사람이 직접 테스트 케이스를 만들면 커버리지(Coverage)를 충분히 만족하는 케이스를 빠짐없이 설계하기 어렵고, 누락이나 실수가 발생할 수 있다. 따라서 테스트 설계 기법이 적용된 도구를 활용하면 더 효율적으로 테스트 케이스를 만들 수 있다.</p>
<p>예를 들어 성적에 따라 학점을 부여하는 요구사항이 있다고 하자. 90점 이상 100점 이하는 A, 80점 이상 90점 미만은 B, 70점 이상 80점 미만은 C를 부여하는 방식이다. 이때 각 점수 구간은 같은 결과를 기대할 수 있는 <code>동치 클래스(Equivalence Class)</code>로 볼 수 있다.</p>
<p><code>동등 분할 테스트(Equivalence Partitioning)</code>는 이러한 입력값의 범위를 의미 있는 구간으로 나누고, 각 구간에서 대표값을 선택해 테스트하는 블랙박스 테스트 기법이다. 예를 들어 90<del>100 구간에서는 95를 대표값으로 선택하고, 80</del>90 구간에서는 85를 대표값으로 선택해 예상한 학점이 나오는지 확인할 수 있다.</p>
<p>이러한 요구사항 명세서(Requirement Specification)를 테스트 설계 도구에 반영하면, 도구가 각 구간의 대표값을 찾아 테스트 케이스를 생성할 수 있다. 더 나아가 생성된 테스트 케이스를 기반으로 실제 테스트 수행까지 연결할 수도 있다.</p>
<p>정리하면, 테스트 케이스 개발 자동화는 테스트 설계 기법을 도구로 지원하여 테스트 케이스를 더 빠르고 일관성 있게 만들기 위한 방법이다. 특히 동등 분할 테스트처럼 일정한 규칙이 있는 테스트 설계에서는 자동화의 효과가 크다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/c47360df-686b-4bef-b020-d8e347974f7a.png" alt="" style="display:block;margin:0 auto" />

<h3>택배 예약 시스템 예시와 동등 분할의 한계 (Parcel Reservation System Example and Limitation of Equivalence Partitioning)</h3>
<p>이번 예시는 테스트 케이스 개발 자동화(Test Case Development Automation)를 이해하기 위한 것이다. 택배 예약 시스템(Parcel Reservation System)에서는 주요 입력 요소(Input Factor)로 택배 무게(Parcel Weight), 배송 지역(Delivery Area), 배송 기간(Delivery Period)이 있다.</p>
<p>동등 분할 테스트(Equivalence Partitioning)를 적용하면 각 입력 요소를 의미 있는 구간이나 값으로 나누고, 각 구간에서 대표값(Representative Value)을 선택한다. 예를 들어 택배 무게는 여러 무게 구간으로 나누고, 배송 지역은 A, B, C, 배송 기간은 일반 배송과 익일 배송으로 나눌 수 있다. 이렇게 나누어진 동치 클래스(Equivalence Class)에서 대표값을 뽑아 테스트 케이스를 만들 수 있다.</p>
<p>이러한 요구사항이 명확하게 정의되어 있다면, 도구를 활용해 테스트 케이스를 자동으로 생성할 수 있다. 택배 무게처럼 범위가 있는 값은 각 구간의 대표값을 선택하고, 배송 지역이나 배송 기간처럼 선택지가 정해진 값은 해당 값을 그대로 테스트 데이터로 사용할 수 있다.</p>
<p>하지만 동등 분할 테스트의 한계는 입력값들의 조합(Combination)을 충분히 고려하지 않는다는 점이다. 동등 분할은 각 입력 요소가 개별적으로 잘 동작하는지를 확인하는 데 초점을 둔다. 예를 들어 택배 무게, 배송 지역, 배송 기간을 각각 테스트할 수는 있지만, 특정 무게와 특정 지역, 특정 배송 기간이 함께 조합되었을 때 발생하는 문제는 놓칠 수 있다.</p>
<p>이 문제를 해결하는 가장 단순한 방법은 모든 조합을 테스트하는 것이다. 예를 들어 택배 무게가 5개 구간, 배송 지역이 3개, 배송 기간이 2개라면 전체 조합은 5 × 3 × 2 = 30개가 된다. 모든 조합을 테스트하면 조합으로 인한 결함을 더 많이 발견할 수 있지만, 테스트 케이스 수가 많아져 시간, 비용, 자원이 많이 필요하다.</p>
<p>따라서 현실적인 대안으로 <code>페어와이즈 테스팅(Pairwise Testing)</code>을 사용할 수 있다. 페어와이즈 테스팅은 모든 입력값의 전체 조합을 테스트하는 대신, 두 입력 요소 사이의 값 조합을 최소 한 번 이상 포함하도록 테스트 케이스를 설계하는 방법이다. 이를 통해 테스트 케이스 수를 줄이면서도 조합으로 인해 발생할 수 있는 결함을 어느 정도 확인할 수 있다.</p>
<p>정리하면, 동등 분할 테스트는 각 입력값의 대표값을 선택해 효율적으로 테스트하는 데 유용하지만, 입력값 간 조합 문제를 충분히 다루기 어렵다. 이 한계를 보완하기 위해 전체 조합 테스트나 페어와이즈 테스팅 같은 방법을 고려할 수 있으며, 이러한 테스트 케이스 생성 과정도 도구를 통해 자동화할 수 있다.</p>
<hr />
<h3>페어와이즈 테스팅 (Pairwise Testing)</h3>
<p>페어와이즈 테스팅(Pairwise Testing)은 모든 입력값의 전체 조합을 테스트하기 어려울 때 사용하는 조합 테스트 기법(Combinatorial Testing Technique)이다. 시간과 비용의 제약 때문에 모든 조합을 테스트할 수 없는 경우, 두 개의 입력 요소(Input Factor) 값들이 최소 한 번 이상 함께 조합되도록 테스트 케이스(Test Case)를 설계한다.</p>
<p>전체 조합 테스트(Exhaustive Testing)는 가능한 모든 입력값 조합을 확인하기 때문에 결함을 찾을 가능성은 높지만, 입력 요소가 많아질수록 테스트 케이스 수가 급격히 증가한다. 반면 페어와이즈 테스팅은 모든 조합을 다 확인하지는 않지만, 두 입력값 간의 주요 조합을 포함하도록 하여 테스트 케이스 수를 줄이면서도 일정 수준의 효과를 얻을 수 있다.</p>
<p>예를 들어 입력 요소가 A, B, C 세 개이고, 각 요소에 두 개의 값이 있다고 하자. A는 A1, A2, B는 B1, B2, C는 C1, C2를 가진다. 모든 조합을 테스트하면 2 × 2 × 2 = 8개의 테스트 케이스가 필요하다.</p>
<p>하지만 페어와이즈 테스팅에서는 세 요소 전체의 모든 조합을 확인하는 대신, 두 요소씩 묶은 조합을 고려한다. 즉 A와 B, A와 C, B와 C의 값 조합이 최소 한 번 이상 포함되도록 테스트 케이스를 구성한다.</p>
<p>예를 들어 다음과 같이 테스트 케이스를 만들 수 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/0530614a-4a28-4e9e-8279-f58ee278e71b.png" alt="" style="display:block;margin:0 auto" />

<p>이렇게 구성하면 전체 8개 조합을 모두 테스트하지 않아도, A-B, A-C, B-C 사이의 가능한 값 조합을 최소 한 번씩 확인할 수 있다. 예를 들어 TC1에서는 A1-B1, A1-C1, B1-C1 조합이 확인되고, TC2에서는 A1-B2, A1-C2, B2-C2 조합이 확인된다.</p>
<p>즉 페어와이즈 테스팅은 전체 조합을 모두 테스트하지 않는다는 한계가 있지만, 테스트 케이스 수를 줄이면서 입력값 간 조합 문제를 효율적으로 확인할 수 있는 방법이다.</p>
<p>다만 입력 요소나 값의 개수가 많아지면 사람이 직접 페어와이즈 테스트 케이스를 설계하기 어려워진다. 예를 들어 A1, A2뿐 아니라 A3, A4처럼 값이 늘어나고 입력 요소도 많아지면 조합이 복잡해진다. 이때는 페어와이즈 테스트 케이스를 자동으로 생성해주는 도구를 활용하는 것이 효과적이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a6506488-bd69-4369-8652-bdfecdb84c5e.png" alt="" style="display:block;margin:0 auto" />

<h3>택배 예약 시스템의 페어와이즈 테스팅 적용</h3>
<h3>(Applying Pairwise Testing to a Parcel Reservation System)</h3>
<p>택배 예약 시스템(Parcel Reservation System)에서는 입력 요소(Input Factor)로 <code>택배 무게(Parcel Weight), 배송 지역(Delivery Area), 배송 기간(Delivery Period)</code>이 있다.</p>
<p>이 예시에서 택배 무게는 5개의 대표값(Representative Value), 배송 지역은 A, B, C의 3개 값, 배송 기간은 일반과 익일의 2개 값으로 나뉜다. 모든 조합 테스트(All Combination Testing)를 수행하면 5 × 3 × 2 = 30개의 테스트 케이스(Test Case)가 필요하다.</p>
<p>모든 조합을 테스트하면 입력값들의 조합을 가장 넓게 확인할 수 있지만, 테스트 케이스 수가 많아져 시간, 비용, 자원이 많이 필요하다. 따라서 현실적으로는 비효율적일 수 있다.</p>
<p>이때 페어와이즈 테스팅(Pairwise Testing)을 적용하면 테스트 케이스 수를 줄일 수 있다. 예시에서는 전체 30개의 테스트 케이스 대신 15개의 페어와이즈 테스트 케이스를 만든다. 이 15개 케이스는 모든 조합을 전부 확인하지는 않지만, 택배 무게와 배송 지역, 택배 무게와 배송 기간, 배송 지역과 배송 기간 사이의 주요 쌍 조합(Pair Combination)을 포함하도록 구성된다.</p>
<p>즉 페어와이즈 테스팅은 전체 조합을 모두 테스트하지 않으면서도, 두 입력 요소 간의 조합을 최대한 효율적으로 확인하는 방법이다. 이를 통해 테스트 케이스 수를 줄이고, 테스트 수행에 필요한 시간과 비용을 줄일 수 있다.</p>
<p>하지만 입력 요소와 값이 많아지면 사람이 직접 페어와이즈 테스트 케이스를 만드는 것은 헷갈리고 어렵다. 그래서 이런 경우에는 페어와이즈 테스트 케이스를 자동으로 생성해주는 도구(Test Case Generation Tool)를 활용할 수 있다. 이처럼 테스트 케이스 수를 줄이면서도 중요한 조합을 확인할 수 있는 부분이 테스트 설계 자동화(Test Design Automation)가 필요한 이유이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/ce2dffd1-e22d-4145-874b-bc97ad16eada.png" alt="" style="display:block;margin:0 auto" />

<h3>PICT를 활용한 테스트 케이스 자동 생성(Automatic Test Case Generation with PICT)</h3>
<p>이번 예시는 페어와이즈 테스팅 도구(Pairwise Testing Tool)인 <code>PICT</code>를 활용해 테스트 케이스(Test Case)를 자동으로 생성하는 방법이다.</p>
<p>예시에서는 스마트폰 제품 명세(Smartphone Product Specification)를 기준으로 테스트 케이스를 만든다. 입력 요소(Input Factor)는 크기(Size), 무게(Weight), 저장 공간(Storage), 메모리(Memory), 배터리(Battery) 총 5가지이다. 각 요소는 2개의 값을 가진다. 예를 들어 크기는 6.1 inch와 6.8 inch, 무게는 220g과 240g, 저장 공간은 256GB와 512GB, 메모리는 8GB와 12GB, 배터리는 16hrs와 18hrs로 나눌 수 있다.</p>
<p>이 모든 값을 전체 조합(All Combination)으로 테스트하면 2 × 2 × 2 × 2 × 2 = 32개의 테스트 케이스가 필요하다. 하지만 모든 조합을 테스트하면 시간과 비용이 많이 들기 때문에, 페어와이즈 테스팅(Pairwise Testing)을 적용해 테스트 케이스 수를 줄일 수 있다.</p>
<p>이 예시에서 중요한 점은 제품의 각 요소가 독립적으로만 문제를 일으키는 것이 아니라, 서로 조합되었을 때 품질 문제(Quality Issue)가 발생할 수 있다는 것이다. 예를 들어 스마트폰의 크기(Size)가 커지면 무게(Weight)가 증가할 수 있고, 무게나 배터리 사용 시간(Battery Life)에 따라 사용자의 불편함이 생길 수도 있다. 따라서 각 요소를 따로 보는 것뿐만 아니라, 요소들 간의 조합도 함께 고려해야 한다.</p>
<p>PICT는 이러한 입력 요소와 값들을 기반으로, 두 요소 간의 조합(Pair Combination)이 포함되도록 테스트 케이스를 자동으로 생성해주는 도구이다. 사람이 직접 조합을 계산하면 복잡하고 실수하기 쉽지만, PICT를 사용하면 더 적은 수의 테스트 케이스로 주요 조합을 확인할 수 있다.</p>
<p>정리하면, PICT를 활용한 테스트 케이스 자동 생성은 전체 조합 테스트보다 테스트 케이스 수를 줄이면서도, 입력 요소 간의 중요한 조합을 확인하기 위한 방법이다. 이는 테스트 설계 자동화(Test Design Automation)의 대표적인 예시라고 볼 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/231055ed-026f-49d0-9fb2-c3c61c4bb09e.png" alt="" style="display:block;margin:0 auto" />

<h3>PICT 도구 적용 과정 (Applying the PICT Tool)</h3>
<p>PICT는 페어와이즈 테스팅(Pairwise Testing)을 적용하여 테스트 케이스(Test Case)를 자동으로 생성해주는 도구이다. 이번 예시에서는 스마트폰 제품 명세(Smartphone Product Specification)를 테스트 데이터(Test Data)로 만들고, 이를 PICT에 입력하여 테스트 케이스를 생성한다.</p>
<p>먼저 11주차 자료에서 PICT 도구(PICT Tool)와 테스트 데이터(Test Data) 파일을 PC로 다운로드한다. 테스트 데이터 파일은 스마트폰의 요구사항이나 제품 명세를 바탕으로 작성된 예시 파일이다. 여기에는 크기(Size), 무게(Weight), 저장 공간(Storage), 메모리(Memory), 배터리(Battery)와 같은 입력 요소와 각 요소의 값들이 정리되어 있다.</p>
<p>다운로드가 끝나면 명령 프롬프트(Command Prompt, cmd)를 실행하고, PICT 도구와 테스트 데이터 파일이 있는 경로로 이동한다. 이후 pict.exe testdata.txt 명령어를 입력하면, testdata.txt에 작성된 입력값을 바탕으로 페어와이즈 테스트 케이스가 자동으로 생성된다.</p>
<p>PICT 내부에는 테스트 케이스를 생성하는 알고리즘이 포함되어 있지만, 이번 강의에서는 알고리즘 자체를 자세히 이해하는 것보다 입력 요소들 사이의 두 값 조합(Pair Combination)을 어떻게 만들어내는지 이해하는 것이 중요하다. 즉, PICT를 활용하면 사람이 직접 복잡한 조합을 계산하지 않아도, 주요 조합을 포함하는 테스트 케이스를 효율적으로 만들 수 있다.</p>
<p>정리하면, PICT 적용 과정은 테스트 데이터를 준비하고, 명령 프롬프트에서 PICT를 실행하여 테스트 케이스를 자동 생성하는 방식이다. 이는 테스트 설계 자동화(Test Design Automation)의 실제 적용 예시라고 볼 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f087c5bb-a01a-4b16-a272-2f7f47dc3c24.png" alt="" style="display:block;margin:0 auto" />

<h3>PICT를 활용한 테스트 케이스 자동 생성 결과</h3>
<h3>(Test Case Generation Result with PICT)</h3>
<p>스마트폰 제품 명세(Smartphone Product Specification)는 테스트 데이터(Test Data) 파일에 정리되어 있다. 이 파일에는 크기(Size), 무게(Weight), 저장 공간(Storage), 메모리(Memory), 배터리(Battery)와 같은 입력 요소와 각 요소의 값이 포함된다.</p>
<p>이 테스트 데이터를 PICT 도구(PICT Tool)에 입력하면, 페어와이즈 테스팅(Pairwise Testing)을 만족하는 테스트 케이스가 자동으로 생성된다. 예시에서는 전체 조합을 모두 테스트하면 32개의 테스트 케이스가 필요하지만, PICT를 적용하면 7개의 테스트 케이스만으로 주요 쌍 조합(Pair Combination)을 확인할 수 있다.</p>
<p>즉 Size와 Weight, Size와 Storage, Size와 Memory, Size와 Battery뿐만 아니라, Weight와 Storage, Memory와 Battery처럼 각 입력 요소들 사이의 두 값 조합을 고려한 테스트가 가능하다. 이를 통해 모든 조합을 테스트하지 않더라도 중요한 조합을 효율적으로 확인할 수 있다.</p>
<p>테스트 설계(Test Design) 단계에서는 테스트 설계 기법(Test Design Technique)을 이용해 직접 테스트 케이스를 만들 수도 있다. 하지만 사람이 직접 만들 경우 불필요한 테스트 케이스가 많아지거나, 반대로 중요한 조합을 빠뜨려 커버리지(Coverage)를 충분히 확보하지 못할 수 있다.</p>
<p>이때 PICT와 같은 도구를 사용하면 커버리지를 일정 수준 유지하면서도 테스트 케이스 수를 줄일 수 있다. 따라서 테스트 케이스를 더 효율적이고 체계적으로 생성할 수 있으며, 이는 테스트 설계 자동화(Test Design Automation)의 대표적인 예시라고 볼 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/b21f20b9-9e85-4854-b00b-207cecf69be5.png" alt="" style="display:block;margin:0 auto" />

<h3>테스트 환경 준비 자동화 (Test Environment Preparation Automation)</h3>
<p>테스트 환경 준비 자동화(Test Environment Preparation Automation)는 테스트를 수행하기 위한 시스템 환경을 수동으로 구성하는 대신, 스크립트(Script)나 도구(Tool)를 사용해 자동으로 구성하는 것을 의미한다.</p>
<p>예를 들어 웹 테스트(Web Testing)를 수행한다면 웹페이지 주소, 포트 번호(Port Number), 설정값(Configuration Value), 필요한 도구와 라이브러리 정보 등을 미리 스크립트에 기록해둘 수 있다. 이후 이 스크립트를 실행하면 테스트에 필요한 환경을 자동으로 구성할 수 있다.</p>
<p>테스트 환경을 수동으로 구성하면 개발 환경(Development Environment)에서는 잘 동작하던 애플리케이션이 테스트 환경(Test Environment)이나 운영 환경(Production Environment)에서는 제대로 동작하지 않는 경우가 생길 수 있다. 이는 각 환경에 설치된 도구, 라이브러리(Library), 버전(Version), 설정값 등이 다르기 때문에 발생할 수 있다.</p>
<p>이러한 문제를 줄이기 위해 테스트 환경도 가능한 한 개발 환경, 테스트 환경, 운영 환경이 동일하거나 유사하게 구성되도록 관리해야 한다. 이를 위해 인프라(Infrastructure)를 코드처럼 관리하는 IaC(Infrastructure as Code) 개념을 활용할 수 있다.</p>
<p>IaC는 서버, 네트워크, 설정값, 라이브러리 등 인프라 구성 정보를 코드로 작성하고 관리하는 방식이다. 이를 사용하면 개발자, 테스터, 운영 담당자가 같은 환경을 기준으로 작업할 수 있고, 환경 차이로 인한 오류를 줄일 수 있다.</p>
<p>정리하면, 테스트 환경 준비 자동화는 테스트 환경을 일관되게 구성하고, 수동 설정에서 발생할 수 있는 실수를 줄이기 위한 방법이다. 특히 여러 환경에서 동일한 조건으로 테스트해야 할 때 효과적이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/372bba1c-156c-4ae1-8021-3500a3a6d717.png" alt="" style="display:block;margin:0 auto" />

<h3>도커 기반 개발 및 운영 환경 자동 구성 예시</h3>
<h3>(Docker-based Environment Automation Example)</h3>
<p>개발 및 운영 환경을 자동으로 구성하는 대표적인 도구 중 하나가 도커(Docker)이다. 도커는 애플리케이션 실행에 필요한 라이브러리(Library), 미들웨어(Middleware), 시스템 도구(System Tool), 런타임 환경(Runtime Environment) 등을 컨테이너(Container)라는 단위로 패키징하여 관리한다.</p>
<p>기존의 가상 머신(Virtual Machine)은 하나의 컴퓨터 안에 또 다른 운영체제(OS)를 설치하고, 그 위에 빌드 도구, 테스트 도구, 애플리케이션 등을 설치하는 방식이다. 이 경우 OS 위에 또 다른 OS가 올라가기 때문에 비교적 무겁고, 많은 컴퓨터 자원이 필요하다.</p>
<p>반면 도커는 호스트 운영체제(Host OS)를 공유하면서 필요한 실행 파일과 설정 파일만 컨테이너 단위로 묶어 실행한다. 따라서 가상 머신보다 가볍고, 자원을 더 효율적으로 사용할 수 있다.</p>
<p>예를 들어 개발 환경에서는 빌드(Build), 테스트(Test), 지속적 통합(Continuous Integration)을 위해 Jenkins, Git, 테스트 도구 등을 사용할 수 있다. 이러한 도구들을 각각 컨테이너로 구성하면 환경을 더 쉽게 관리할 수 있다. 빌드 환경에 문제가 생기면 빌드 컨테이너만 수정하면 되고, 테스트 환경에 문제가 생기면 테스트 컨테이너를 수정하면 된다.</p>
<p>테스트 환경도 도커를 활용해 자동으로 구성할 수 있다. 테스트를 수행하기 위해 필요한 도구와 설정을 컨테이너로 만들어두면, 매번 서버를 직접 설정하지 않아도 동일한 테스트 환경을 빠르게 실행할 수 있다. 이를 통해 개발 환경과 테스트 환경의 차이를 줄이고, 테스트 자동화(Test Automation)를 더 효율적으로 수행할 수 있다.</p>
<p>정리하면, 도커는 컨테이너(Container)를 이용해 개발, 빌드, 테스트, 운영 환경을 일관되게 구성할 수 있도록 돕는 도구이다. 테스트 환경 준비 자동화(Test Environment Preparation Automation)에서도 도커를 활용하면 환경 구성의 실수를 줄이고, 자원을 효율적으로 사용하며, 반복 가능한 테스트 환경을 만들 수 있다.</p>
<hr />
<h3>테스트 수행 자동화의 개념 (Concept of Test Execution Automation)</h3>
<p>이번 챕터에서는 테스트 자동화(Test Automation) 중 실제 테스트를 실행하는 단계인 테스트 수행 자동화(Test Execution Automation)를 살펴본다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/15b430dd-e183-45b1-b61f-a7b091a84d20.png" alt="" style="display:block;margin:0 auto" />

<p>테스트 수행 자동화란 이미 설계된 테스트 케이스(Test Case)를 실행하고, 테스트 결과(Test Result)를 판정하며, 결과를 기록하거나 보고하는 과정에서 반복적이고 실수가 발생하기 쉬운 작업을 도구(Tool)를 활용해 자동화하는 것을 의미한다.</p>
<p>ISO/IEC/IEEE 29119 표준의 동적 테스트 프로세스(Dynamic Test Process)를 기준으로 보면, 테스트 수행 자동화의 주요 대상은 <code>테스트 수행(Test Execution)과 테스트 결과 보고(Test Result Reporting) 단계</code>이다. 즉, 테스트를 실제로 실행하고 그 결과를 확인·기록·보고하는 부분이 자동화 대상이 된다.</p>
<p>다만 테스트 수행 자동화가 제대로 이루어지기 위해서는 그 전에 테스트 설계(Test Design)와 테스트 환경 구성(Test Environment Set-up)이 적절하게 준비되어 있어야 한다. 테스트 케이스가 명확하지 않거나 테스트 환경이 불안정하면, 자동화된 테스트 수행도 신뢰하기 어렵기 때문이다.</p>
<p>정리하면, 테스트 수행 자동화는 준비된 테스트 케이스를 도구를 통해 반복 실행하고, 기대 결과(Expected Result)와 실제 결과(Actual Result)를 비교하며, 테스트 결과를 체계적으로 기록하는 과정이다. 이를 통해 테스트 수행 과정의 시간과 실수를 줄이고, 반복 테스트를 더 일관되게 수행할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/d799b33a-17de-46db-81c5-290055db87e3.png" alt="" style="display:block;margin:0 auto" />

<h3>Selenium IDE를 활용한 회원가입 웹페이지 반복 테스트</h3>
<h3>(Repeated Web Page Testing with Selenium IDE)</h3>
<p>이번 예시는 회원가입 웹페이지(Sign-up Web Page)를 대상으로 Selenium IDE를 적용해 테스트 수행을 자동화하는 과정이다. Selenium IDE는 웹 애플리케이션(Web Application)의 동작을 녹화하고 재실행할 수 있는 테스트 자동화 도구(Test Automation Tool)이다.</p>
<p>테스터가 직접 웹페이지에서 아이디를 입력하고, 도메인을 선택하거나 입력하고, 화면을 클릭하는 과정을 Selenium IDE로 녹화하면, 이후 같은 절차를 반복 실행할 수 있다. 즉 사람이 매번 같은 테스트 절차를 수행하지 않아도, 녹화된 테스트 시나리오(Test Scenario)를 도구가 대신 실행할 수 있다.</p>
<p>예시에서는 위메프 회원가입 화면의 이메일 ID 입력 기능을 테스트 대상으로 한다. 전체 회원가입 기능이 아니라, 이메일 ID와 도메인 입력이 정상적으로 처리되는지만 확인한다.</p>
<p>테스트 케이스명(Test Case Name)은 “이메일 ID 입력 정상”이다. 시나리오는 먼저 이메일 ID 입력 영역을 클릭하고, 이메일 ID에 cuk을 입력한다. 그다음 @ 기호 다음의 도메인 영역을 클릭하면, 아직 도메인이 입력되지 않았기 때문에 “이메일 형식을 다시 확인해 주세요.”라는 빨간 메시지가 표시되어야 한다.</p>
<p>이후 사용자가 도메인 영역을 다시 클릭하고 <a href="http://cuk.edu">cuk.edu</a>를 입력한 뒤, 회원가입 페이지의 빈 영역을 클릭하면 더 이상 이메일 형식 오류 메시지가 표시되지 않아야 한다. 즉 올바른 이메일 형식인 <a href="mailto:cuk@cuk.edu">cuk@cuk.edu</a>가 완성되었기 때문에 오류 메시지가 사라지는 것이 정상적인 예상 결과(Expected Result)이다.</p>
<p>이 테스트의 입력값(Input Value)은 이메일 ID인 cuk과 도메인인 <a href="http://cuk.edu">cuk.edu</a>이다. 예상 결과는 이메일 형식이 정상적으로 입력되었을 때 아무 오류 메시지도 표시되지 않는 것이다.</p>
<p>정리하면, 이 예시는 사람이 직접 반복해야 하는 회원가입 입력 절차를 Selenium IDE로 녹화하고 재실행하여 테스트 수행 자동화(Test Execution Automation)를 적용하는 사례이다. 간단한 이메일 입력 테스트처럼 보이지만, 실제로는 클릭, 입력, 오류 메시지 확인, 재입력, 결과 확인이라는 여러 단계가 필요하기 때문에 자동화의 효과를 확인하기 좋은 예시이다.</p>
<hr />
<h3>📌Selenium IDE 설치 관련 변경 사항</h3>
<h3>(Change in Selenium IDE Installation)</h3>
<p>강의 예시에서는 크롬 브라우저(Chrome Browser)에서 Selenium IDE를 설치하고, 회원가입 웹페이지의 테스트 과정을 녹화하여 반복 실행하는 방식으로 설명한다.</p>
<p>하지만 현재 공지에 따르면 크롬 웹스토어(Chrome Web Store)에서 Selenium IDE가 정상적으로 지원되지 않아, 강의와 동일한 방식으로 직접 설치하고 실행하기는 어렵다. 따라서 해당 실습은 실제 설치보다는 녹화 영상을 참고하여 Selenium IDE의 동작 방식과 테스트 수행 자동화(Test Execution Automation)의 개념을 이해하는 데 초점을 두면 된다.</p>
<p>대신 Selenium IDE와 유사하게 웹 브라우저에서 사용자의 동작을 녹화하고 재생할 수 있는 대체 도구로 Katalon Recorder를 사용할 수 있다. Katalon Recorder도 웹 애플리케이션(Web Application)의 클릭, 입력, 검증 과정을 기록하고 반복 실행하는 데 활용할 수 있으므로, Selenium IDE 실습의 대안으로 적절하다.</p>
<p>정리하면, 이 예시는 특정 도구 설치 자체보다 웹 테스트 과정을 녹화(Record)하고 재생(Playback)하여 반복 테스트를 자동화하는 개념을 이해하는 것이 핵심이다. Selenium IDE를 직접 사용하기 어렵다면 Katalon Recorder와 같은 대체 도구를 통해 비슷한 방식의 테스트 수행 자동화를 경험할 수 있다.</p>
]]></content:encoded></item><item><title><![CDATA[Linux Kernel Compilation and Module Programming]]></title><description><![CDATA[1️⃣ Kernel Compilation2️⃣ Module Program

1️⃣ Kernel Compilation
Kernel Compilation and Module Programming
이번 시간에는 리눅스 커널(Linux Kernel) 과 컴파일(Compilation) 에 대해 학습한다. 리눅스를 공부하다 보면 커널이라는 단어를 자주 접하게 되는데,]]></description><link>https://heesu.tech/linux-kernel-compilation-and-module-programming</link><guid isPermaLink="true">https://heesu.tech/linux-kernel-compilation-and-module-programming</guid><dc:creator><![CDATA[Heesu Noh]]></dc:creator><pubDate>Sat, 16 May 2026 16:03:01 GMT</pubDate><content:encoded><![CDATA[<p>1️⃣ Kernel Compilation<br />2️⃣ Module Program</p>
<hr />
<h1>1️⃣ Kernel Compilation</h1>
<h3>Kernel Compilation and Module Programming</h3>
<p>이번 시간에는 <strong>리눅스 커널(Linux Kernel)</strong> 과 <strong>컴파일(Compilation)</strong> 에 대해 학습한다. 리눅스를 공부하다 보면 커널이라는 단어를 자주 접하게 되는데, 커널은 운영체제의 핵심 부분으로 하드웨어와 소프트웨어 사이에서 자원을 관리하고 시스템이 동작할 수 있도록 도와주는 역할을 한다.</p>
<p>이번 강의에서는 리눅스 커널이 무엇인지뿐만 아니라, 공개되어 있는 <strong>소스 코드(Source Code)</strong> 를 어떻게 수정하고, 수정한 내용을 실제 시스템에 적용하는 방법까지 살펴본다. 이를 위해 먼저 <strong>커널 컴파일(Kernel Compilation)</strong> 의 기본 절차를 이해하고, 이후에는 커널 기능을 보다 유연하게 확장할 수 있는 <strong>모듈 프로그램(Module Program)</strong> 에 대해서도 다룬다.</p>
<p><strong>커널 컴파일(Kernel Compilation)</strong> 은 말 그대로 커널의 소스 코드를 컴퓨터가 실행할 수 있는 형태로 변환하는 작업이다. 컴파일이라는 행위 자체는 단순해 보일 수 있지만, 실제 커널 컴파일에는 여러 준비 과정과 설정 단계가 필요하다. 특히 리눅스 커널은 매우 큰 규모의 프로그램이기 때문에 전체 커널을 컴파일하는 데 시간이 오래 걸릴 수 있다.</p>
<p>과거에도 커널 컴파일은 당시 컴퓨터 성능에 비해 상당히 무거운 작업이었다. 현재는 CPU 성능과 코어 수가 많이 향상되었지만, 리눅스 커널 자체도 계속 확장되고 복잡해졌기 때문에 여전히 컴파일 시간이 적지 않게 걸릴 수 있다. 그래서 모든 기능을 커널에 직접 포함시키는 방식만 사용하는 것이 아니라, 필요한 기능을 별도로 추가하거나 제거할 수 있는 <strong>모듈(Module)</strong> 방식을 함께 사용한다.</p>
<p><strong>모듈(Module)</strong> 은 커널의 기능을 확장하기 위한 프로그램 형태로 이해할 수 있다. 커널 전체를 다시 컴파일하지 않고도 특정 기능을 추가하거나 사용할 수 있게 해 주기 때문에, 커널을 보다 효율적으로 관리하는 데 도움이 된다. 예를 들어 특정 장치 드라이버(Device Driver)나 파일 시스템(File System) 기능 등을 모듈 형태로 사용할 수 있다.</p>
<p>이번 수업의 핵심은 먼저 <strong>리눅스 커널 컴파일(Linux Kernel Compilation)</strong> 의 기본 절차를 이해하는 것이다. 그다음에는 <strong>모듈 프로그램(Module Program)</strong> 을 간단히 작성하고 컴파일하여 사용할 수 있다는 수준까지 익히는 것이 목표이다. 모듈 프로그램은 다양한 분야에서 활용될 수 있지만, 이번 시간에는 복잡한 응용보다는 “커널 기능을 별도의 모듈로 만들고 사용할 수 있다”는 기본 개념을 중심으로 학습한다.</p>
<p>정리하면, 이번 강의는 리눅스 운영체제의 핵심인 <strong>커널(Kernel)</strong> 을 이해하고, 공개된 커널 소스를 수정하여 적용하는 방법, 그리고 커널 전체를 다시 컴파일하지 않고 기능을 확장할 수 있는 <strong>모듈(Module)</strong> 개념을 배우는 시간이다. 이를 통해 리눅스가 단순히 사용하는 운영체제가 아니라, 사용자가 직접 수정하고 확장할 수 있는 구조를 가진 운영체제라는 점을 이해할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/6c79cdf0-fe72-4d8d-8b47-f60b69257032.png" alt="" style="display:block;margin:0 auto" />

<h3>Kernel Release Types and Maintainers</h3>
<p>이번 강의에서는 <strong>커널 컴파일(Kernel Compilation)</strong> 을 본격적으로 다루기 전에, 리눅스 커널 소스에서 자주 보게 되는 릴리스 관련 용어들을 먼저 정리한다. 리눅스 커널은 단순히 한 사람이 만든 코드를 공개해 두고 아무나 사용하는 구조가 아니라, 오랜 시간 동안 많은 개발자와 <strong>메인테이너(Maintainer)</strong> 들이 체계적으로 관리하는 프로젝트이다.</p>
<p>리눅스 커널은 처음에 <strong>리누스 토발즈(Linus Torvalds)</strong> 가 학창 시절 취미 프로젝트로 시작했다. 그는 자신의 운영체제 커널 아이디어를 인터넷 뉴스그룹과 메일을 통해 공유했고, 이에 관심을 가진 사람들이 참여하면서 점차 커다란 오픈소스 프로젝트로 발전했다. 당시의 개발 방식은 지금처럼 실시간 채팅으로 즉각 반응하는 방식이라기보다는, <strong>메일링 리스트(Mailing List)</strong> 를 통해 패치를 보내고 의견을 주고받는 방식에 가까웠다.</p>
<p>리눅스 커널은 무료로 사용할 수 있고 소스 코드도 공개되어 있지만, 아무 체계 없이 관리되는 것은 아니다. 커널 소스에는 저작권 표시와 라이선스 조건이 존재하며, 대표적으로 <strong>GPL(General Public License)</strong> 에 따라 배포된다. 따라서 누구나 소스 코드를 확인하고 수정할 수 있지만, 수정한 코드를 배포할 때는 라이선스 조건을 따라야 한다. 즉, “완전히 아무 조건 없는 공짜 코드”라기보다는, 오픈소스 라이선스 안에서 자유롭게 사용하고 수정할 수 있는 구조라고 이해하는 것이 정확하다.</p>
<p>커널 개발이 계속 진행되면서 여러 기능이 추가되고, 각 기능이나 하위 시스템을 관리하는 사람들이 생겼다. 이들을 <strong>메인테이너(Maintainer)</strong> 라고 한다. 메인테이너는 특정 드라이버, 파일 시스템, 네트워크, 메모리 관리 등 커널의 특정 영역을 관리하며, 외부 개발자들이 보낸 <strong>패치(Patch)</strong> 를 검토한다. 패치는 버그를 수정하거나, 더 나은 알고리즘을 적용하거나, 새로운 기능을 추가하기 위해 기존 코드를 변경한 내용이다.</p>
<p>개발자가 패치를 보내면 메인테이너는 해당 코드가 적절한지 검토하고, 필요한 경우 테스트용 코드 흐름에 포함시킨다. 이후 일정 기간 동안 테스트를 거치며 문제가 없는지 확인한 뒤, 최종적으로 공식 커널 릴리스에 반영될 수 있다. 리눅스 커널 개발에는 많은 메인테이너가 참여하지만, 핵심적인 최종 흐름을 관리하는 사람은 많지 않다. 특히 <strong>메인라인 커널(Mainline Kernel)</strong> 의 최종 통합과 릴리스에는 리누스 토발즈의 역할이 크다.</p>
<p>커널 릴리스에서 먼저 볼 수 있는 용어는 <strong>프리패치(Prepatch)</strong> 또는 <strong>RC(Release Candidate)</strong> 이다. 이는 정식 안정 버전으로 공개되기 전 단계의 커널이다. 새로운 기능이나 수정된 코드가 들어간 상태이며, 개발자와 관심 있는 사용자들이 직접 컴파일하고 테스트해 볼 수 있다. 아직 안정 버전으로 확정된 것은 아니기 때문에, 실제 운영 환경보다는 테스트 목적으로 사용하는 것이 적절하다. 공식 <a href="http://kernel.org">kernel.org</a> 설명에서도 <strong>Prepatch 또는 RC 커널은 mainline 커널의 사전 릴리스이며, 보통 커널 개발자와 리눅스 애호가를 대상으로 한다</strong>고 설명한다.</p>
<p>다음은 <strong>메인라인(Mainline)</strong> 이다. 메인라인 커널은 새로운 기능이 본격적으로 반영되는 중심 개발 흐름이다. 새로운 드라이버, 기능 개선, 내부 구조 변경 등이 이 흐름을 통해 들어온다. 공식 문서에 따르면 리눅스 커널은 느슨한 시간 기반 릴리스 방식을 사용하며, 보통 새로운 주요 커널 릴리스가 2~3개월 간격으로 나온다.</p>
<p><strong>스테이블(Stable)</strong> 은 메인라인 커널이 공개된 이후, 버그 수정과 안정성 보완이 적용되는 릴리스이다. 새로운 기능을 계속 추가하기보다는 이미 공개된 커널에서 발견된 문제를 고치고, 필요한 수정 사항을 반영하는 데 초점이 있다. 안정 커널의 패치는 검토 후 RC 형태로 다시 테스트될 수 있으며, 문제가 없을 때 안정 릴리스에 포함된다.</p>
<p>마지막으로 <strong>롱텀(Longterm)</strong> 또는 <strong>LTS(Long Term Support)</strong> 는 특정 안정 커널 버전을 오랫동안 유지 관리하는 릴리스이다. 어떤 커널 버전은 오래 사용해도 좋다고 판단되면 장기 지원 대상으로 선정되고, 이후 몇 년 동안 필요한 버그 수정과 보안 수정이 계속 제공된다. 서버, 임베디드 시스템, 기업 환경처럼 커널을 자주 바꾸기 어려운 곳에서는 이런 Longterm 커널이 중요하다. <a href="http://kernel.org">kernel.org</a>의 릴리스 페이지에서도 mainline, stable, longterm과 같은 분류를 통해 현재 관리 중인 커널 버전을 구분해 제공한다.</p>
<p>정리하면, <strong>Prepatch/RC</strong> 는 정식 공개 전 테스트용 후보 버전이고, <strong>Mainline</strong> 은 새로운 기능이 들어가는 중심 개발 버전이다. <strong>Stable</strong> 은 메인라인 공개 이후 버그 수정 중심으로 관리되는 버전이며, <strong>Longterm/LTS</strong> 는 특정 안정 버전을 장기간 유지보수하는 버전이다. 따라서 커널을 직접 컴파일하거나 소스를 내려받을 때는 자신이 테스트 목적의 최신 기능을 원하는지, 일반적인 안정성을 원하는지, 장기간 유지보수되는 버전을 원하는지에 따라 적절한 커널 종류를 선택해야 한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/4a822696-8017-4d54-9908-27974f6bb4d8.png" alt="" style="display:block;margin:0 auto" />

<h3>커널 소스 다운로드와 커널 버전 번호</h3>
<h3>Kernel Source Download and Kernel Version Numbering</h3>
<p>리눅스 <strong>커널 소스(Kernel Source)</strong> 는 공식적으로 <a href="http://kernel.org"><code>kernel.org</code></a>에서 다운로드할 수 있다. 강의에서 언급한 주소는 <a href="https://www.kernel.org/pub/%EC%9D%B4%EB%A9%B0"><code>https://www.kernel.org/pub/</code>이며</a>, 이곳에 접속하면 웹 브라우저를 통해 디렉터리 구조를 직접 확인할 수 있다. 커널 소스는 보통 그 안의 <code>linux/kernel</code> 디렉터리 아래에서 버전별로 정리되어 있으며, <code>v4.x</code>, <code>v5.x</code>, <code>v6.x</code>와 같은 형태로 세대별 디렉터리가 나뉘어 있다. <a href="http://kernel.org">kernel.org</a>의 공식 디렉터리에도 <code>v4.x</code>, <code>v5.x</code>, <code>v6.x</code>와 같은 항목이 실제로 구분되어 있다.</p>
<p>커널 소스를 다운로드하는 방법은 여러 가지가 있다. 웹에서 파일을 직접 내려받는 <strong>HTTP Protocol</strong>, 개발 이력을 함께 관리할 수 있는 <strong>Git</strong>, 그리고 서버 간 파일 동기화에 사용되는 <strong>Rsync Protocol</strong> 방식이 있다. 같은 커널 소스라도 어떤 프로토콜을 사용하느냐에 따라 접근 위치가 다를 수 있다. 일반적으로 단순히 커널 소스 압축 파일을 내려받아 컴파일해 보는 목적이라면 HTTP 방식으로도 충분하고, 커널 개발 흐름을 따라가거나 패치를 적용하고 관리하려면 <strong>Git</strong> 을 사용하는 것이 적절하다.</p>
<p>커널 릴리스 페이지에서는 앞에서 배운 <strong>Mainline</strong>, <strong>Stable</strong>, <strong>Longterm</strong> 과 같은 정보도 함께 확인할 수 있다. <strong>Mainline Kernel</strong> 은 새로운 기능이 들어가는 중심 개발 흐름이고, <strong>Stable Kernel</strong> 은 mainline이 공개된 뒤 버그 수정이 반영되는 안정 버전이다. <strong>Longterm Kernel</strong> 은 오래된 커널 계열을 장기간 유지보수하는 버전으로, 중요한 버그 수정이 필요한 경우 이전 커널 트리에 수정 사항을 반영하는 방식으로 관리된다. <a href="http://kernel.org">kernel.org</a>에서도 mainline은 리누스 토발즈가 관리하고, stable과 longterm은 지정된 stable 커널 메인테이너가 관리한다고 설명한다.</p>
<p>강의에서 예로 든 <code>5.10.252</code>와 같은 숫자는 커널의 구체적인 버전 번호이다. 여기서 <code>5.10</code>은 커널 계열을 의미하고, 마지막의 <code>.252</code>는 해당 계열에서 안정화 패치가 누적되어 올라간 패치 버전으로 볼 수 있다. 즉, <code>5.10.252</code>는 5.10 계열 커널이 장기간 유지보수되면서 여러 번의 수정 사항이 반영된 상태라고 이해하면 된다.</p>
<p>이때 Longterm 목록에 5.10 계열까지만 보인다면, 4.x 계열은 현재 <a href="http://kernel.org">kernel.org</a>의 공식 장기 지원 대상에서는 빠졌다고 볼 수 있다. 다만 이것은 4.x 커널 소스 자체가 사라졌다는 뜻은 아니다. 실제 <a href="http://kernel.org">kernel.org</a>의 커널 디렉터리에는 <code>v4.x</code> 디렉터리가 남아 있다. 따라서 구형 시스템이나 임베디드 시스템에서 4.x 계열 커널을 사용하고 있다면, <a href="http://kernel.org">kernel.org</a>에서 제공하는 공식 longterm 유지보수 범위에서는 벗어났을 가능성이 크다고 이해하면 된다.</p>
<p>하지만 사용자가 설치한 리눅스 배포판이나 특정 회사가 4.x 기반 커널을 별도로 관리할 가능성은 있다. 예를 들어 기업용 배포판, 임베디드 장비 제조사, 특정 하드웨어 벤더는 자신들의 제품 안정성을 위해 오래된 커널을 자체적으로 유지보수할 수 있다. <a href="http://kernel.org">kernel.org</a>에서도 배포판들이 자체적인 장기 유지보수 커널을 제공할 수 있으며, 이런 커널은 <a href="http://kernel.org">kernel.org</a>에 호스팅되지 않고 <a href="http://kernel.org">kernel.org</a> 개발자들이 직접 지원하지 않는다고 설명한다. 이 경우 유지보수는 해당 회사나 커뮤니티의 자본력, 기술력, 지원 정책에 따라 달라질 수 있고, 기업용 지원이라면 비용이 발생할 수도 있다.</p>
<p>커널 버전 번호의 의미도 함께 알아둘 필요가 있다. 예전 리눅스 커널에서는 버전 번호의 홀수와 짝수에 의미가 있었다. 예를 들어 첫 번째 점 뒤의 숫자가 홀수이면 개발 버전, 짝수이면 안정 버전처럼 구분하던 시기가 있었다. 하지만 이 방식은 오래전에 사라졌다. <a href="http://kernel.org">kernel.org</a>에서도 이 홀수·짝수 구분 방식은 2.6 릴리스 이후 폐기되었고, 현재는 사전 릴리스 커널을 <code>-rc</code>로 표시한다고 설명한다.</p>
<p>최근의 커널 버전 번호는 과거처럼 엄격한 의미를 갖는다기보다는 순차적으로 올라가는 번호에 가깝게 이해하면 된다. <code>4.x</code>, <code>5.x</code>, <code>6.x</code>처럼 큰 숫자가 바뀐다고 해서 항상 커널 내부 구조가 완전히 달라졌다는 뜻은 아니다. <a href="http://kernel.org">kernel.org</a>의 설명에 따르면 큰 버전 번호는 점 뒤의 숫자가 너무 커 보일 때 증가하며, 그 외에 특별한 이유가 있는 것은 아니라고 한다. 따라서 최근에는 버전 번호 자체의 상징적 의미보다는, 해당 버전이 <strong>Mainline</strong>, <strong>Stable</strong>, <strong>Longterm</strong> 중 어디에 속하는지, 그리고 지원 기간이 어떻게 되는지를 확인하는 것이 더 중요하다.</p>
<p>커널 소스를 받을 수 있는 대표적인 경로로는 <strong>Git</strong> 과 <strong>Rsync</strong> 도 있다. Git 저장소는 <a href="http://git.kernel.org"><code>git.kernel.org</code></a>에서 확인할 수 있고, Rsync 경로는 <code>rsync://rsync.kernel.org/pub/</code> 형태로 제공된다. <strong>Rsync</strong> 는 파일과 디렉터리를 효율적으로 동기화하는 데 사용되는 도구이자 프로토콜이다. 과거에는 백업이나 서버 간 미러링 작업에 많이 사용되었고, 현재도 저장소나 백업 시스템 내부에서는 활용될 수 있다. 다만 리눅스를 처음 사용하는 입장에서는 직접 사용할 일이 많지 않을 수 있으므로, 커널 소스를 다운로드할 수 있는 여러 방식 중 하나로 알아두면 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/332a2871-96fc-4685-8dfc-01e8ade33919.png" alt="" style="display:block;margin:0 auto" />

<h3>웹에서 커널 소스 읽기와 메일링 리스트</h3>
<p>Reading Kernel Source Online and Mailing Lists</p>
<p>커널 소스가 처음이라면, 반드시 처음부터 소스 파일을 직접 다운로드해서 압축을 풀고 하나하나 열어볼 필요는 없다. 물론 전문적으로 커널을 수정하거나 컴파일을 직접 해 보려면 컴퓨터 안에 커널 소스를 내려받아 확인하는 것이 좋다. 하지만 단순히 커널 소스가 어떻게 구성되어 있는지 보고 싶거나, 특정 함수와 파일을 가볍게 살펴보고 싶은 단계라면 웹에서 커널 소스를 볼 수 있는 사이트를 이용하는 방법도 있다.</p>
<p>대표적인 예로 <strong>Bootlin Elixir Cross Referencer</strong> 가 있다. 강의에서 언급한 <a href="https://elixir.bootlin.com/linux/v6.19.8/source"><code>https://elixir.bootlin.com/linux/v6.19.8/source</code></a> 와 같은 주소에 들어가면 리눅스 커널 소스가 웹에서 보기 쉽게 정리되어 있다. <strong>Elixir</strong> 는 리눅스 커널 같은 C/C++ 프로젝트의 여러 릴리스를 색인화하고, 소스 코드 안의 함수나 매크로, 구조체 등을 서로 연결해서 볼 수 있도록 만든 <strong>Source Code Cross-Referencer</strong> 이다. Bootlin의 Elixir GitHub 설명에서도 Elixir는 LXR에서 영감을 받은 소스 코드 교차 참조 도구이며, Linux kernel 같은 C/C++ 프로젝트의 모든 릴리스를 색인화하는 것이 주요 목적이라고 설명한다.</p>
<p>실제로 커널 소스를 컴퓨터에 다운로드해서 압축을 풀면, 웹에서 보는 것과 비슷한 디렉터리 구조가 나타난다. 커널 소스 루트에는 매우 많은 폴더와 파일이 있으며, 각각의 디렉터리는 커널의 특정 기능이나 하위 시스템과 관련되어 있다. 예를 들어 네트워크와 관련된 부분, 장치 드라이버와 관련된 부분, 파일 시스템과 관련된 부분처럼 커널 내부 기능이 디렉터리별로 나뉘어 있다. 웹 브라우저에서 이런 디렉터리를 클릭하면 파일 목록을 확인할 수 있고, 개별 파일을 클릭하면 소스 코드를 바로 읽을 수 있다.</p>
<p>이런 웹 기반 소스 브라우저의 장점은 단순히 파일 내용을 보여주는 데서 끝나지 않는다는 점이다. 코드 안에 있는 함수 이름이나 자료구조, 매크로 등이 링크처럼 연결되어 있어, 클릭하면 관련 정의나 사용 위치를 쉽게 따라갈 수 있다. 처음 커널 소스를 보는 입장에서는 어디서부터 봐야 할지 막막할 수 있는데, 이런 방식은 소스 코드의 연결 관계를 따라가며 이해하는 데 도움이 된다. 즉, 커널 소스가 궁금하지만 아직 직접 다운로드하거나 빌드 환경을 구성하기 어렵다면, 먼저 Elixir 같은 웹 도구를 이용해 구조를 익히는 것도 좋은 방법이다.</p>
<p>반대로 커널을 전문적으로 수정하고 싶거나 실제로 패치를 작성하고 싶다면, 웹에서 보는 것만으로는 부족하다. 이 경우에는 커널 소스를 로컬 컴퓨터에 직접 내려받고, Git으로 버전 관리를 하면서 파일을 수정하고 컴파일하는 방식으로 작업해야 한다. 웹 브라우저는 소스를 읽고 탐색하는 데는 편리하지만, 실제 개발과 테스트는 로컬 환경에서 이루어지는 경우가 많다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/02f1f041-8f74-4e67-bb21-291cffeb6b08.png" alt="" style="display:block;margin:0 auto" />

<p>커널 개발과 관련해서 또 알아둘 것이 <strong>메일링 리스트(Mailing List)</strong> 이다. 강의에서 언급된 <a href="https://kernelnewbies.org/ML"><code>https://kernelnewbies.org/ML</code></a> 은 커널 개발 입문자에게 도움이 되는 메일링 리스트 정보를 정리한 페이지이다. 메일링 리스트는 쉽게 말해 단체 이메일 주소라고 볼 수 있다. 어떤 사람이 메일링 리스트 주소로 메일을 보내면, 그 리스트에 가입된 사람들이 같은 메일을 받아볼 수 있다. KernelNewbies 페이지에서도 여러 유용한 메일링 리스트를 소개하고 있으며, kernelnewbies 메일링 리스트에 가입하려면 별도의 구독 페이지를 이용하라고 안내하고 있다.</p>
<p>리눅스 커널 개발은 오래전부터 메일 중심으로 이루어져 왔다. 앞에서 리누스 토발즈가 자신의 프로젝트를 메일과 뉴스그룹을 통해 알렸던 것처럼, 커널 개발 문화에서는 지금도 메일을 통해 패치를 보내고, 의견을 주고받고, 코드 리뷰를 진행하는 방식이 중요하다. KernelNewbies의 메일링 리스트 설명에 따르면, 이 리스트는 리눅스 커널을 함께 배우기 위한 이메일 리스트이며, 모든 리스트 구성원에게 글을 보내려면 <a href="mailto:kernelnewbies@kernelnewbies.org"><code>kernelnewbies@kernelnewbies.org</code></a> 로 메일을 보내면 된다.</p>
<p>특정 분야에 관심이 있다면 관련 메일링 리스트를 찾아보는 것도 도움이 된다. 예를 들어 메모리 관리와 관련된 내용을 보고 싶다면 메모리 관련 커널 메일링 리스트나 해당 하위 시스템의 논의 내용을 찾아볼 수 있다. 메일링 리스트에 가입하면 해당 주제와 관련된 개발자들의 논의, 패치 검토, 질문과 답변을 받아볼 수 있다. 다만 커널 관련 메일링 리스트는 메일 양이 많을 수 있으므로, 처음에는 KernelNewbies처럼 입문자용 자료를 먼저 참고하거나, 필요한 주제의 아카이브를 검색하는 방식으로 접근하는 것이 부담이 적다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/3ee35c94-a907-49a1-95b4-8ec700ad3252.png" alt="" style="display:block;margin:0 auto" />

<h3>리눅스/유닉스 강의노트: 커널 패치 제출을 위한 준비 과정</h3>
<h3>Preparing to Submit Kernel Patches</h3>
<p>이번에는 리눅스 <strong>커널 패치(Kernel Patch)</strong> 를 제출하기 위해 어떤 준비 과정이 필요한지 살펴본다. 사용자가 커널을 직접 수정하고, 그 수정 사항을 개인적으로만 사용하는 것이 아니라 실제 리눅스 커널 개발 흐름에 반영하고 싶다면 단순히 코드를 고치는 것만으로는 부족하다. 커널 코드를 수정하는 도구, 코드 작성 형식, 이메일 설정, Git 사용법, 커널 소스 다운로드와 컴파일 과정까지 함께 준비해야 한다.</p>
<p>먼저 커널 코드를 수정하려면 <strong>편집기(Editor)</strong> 가 필요하다. 강의에서는 그 예로 <strong>Vim</strong> 설정을 언급한다. Vim은 오래전부터 리눅스와 유닉스 환경에서 널리 사용되어 온 텍스트 편집기이며, 리눅스 커널 개발에 참여해 온 많은 개발자들이 익숙하게 사용해 온 도구이기도 하다. 리눅스 커널은 20세기 후반에 시작된 오래된 프로젝트이기 때문에, 초기부터 개발에 참여했던 사람들 중에는 터미널 기반 편집기와 메일 기반 개발 환경에 익숙한 사람이 많았다. 그래서 커널 개발 문서나 예제에서도 <code>vi</code> 또는 <code>vim</code> 같은 편집기를 자연스럽게 볼 수 있다.</p>
<p>하지만 중요한 것은 반드시 Vim만 사용해야 한다는 뜻은 아니다. 핵심은 어떤 편집기를 쓰더라도 리눅스 커널의 <strong>코딩 스타일(Coding Style)</strong> 에 맞게 코드를 작성해야 한다는 점이다. 개인적으로 혼자 수정해서 사용하는 코드라면 자기에게 편한 방식으로 작성해도 큰 문제가 없을 수 있다. 그러나 수정한 코드를 전체 리눅스 커널에 반영하고 싶다면, 중간에서 코드를 검토하는 <strong>메인테이너(Maintainer)</strong> 들이 보기 편한 형식과 커널 프로젝트에서 정한 규칙을 따라야 한다.</p>
<p>예를 들어 들여쓰기 <strong>인덴테이션(Indentation)</strong> 방식이 중요하다. 일반적인 프로젝트에서는 2칸 또는 4칸 들여쓰기를 사용하는 경우가 많지만, 리눅스 커널의 C 코드 스타일에서는 탭을 사용하며, 탭은 8글자 너비로 간주된다. 공식 리눅스 커널 코딩 스타일 문서에서도 “탭은 8글자이며, 따라서 들여쓰기도 8글자”라고 설명한다. 이런 규칙을 지키지 않으면 코드 내용이 아무리 좋아도 리뷰 과정에서 지적을 받을 수 있고, 형식이 맞지 않아 패치가 받아들여지지 않을 수도 있다.</p>
<p>그래서 커널 개발을 준비할 때는 편집기 설정을 커널 스타일에 맞춰 두는 것이 좋다. Vim을 사용한다면 탭 너비, 자동 들여쓰기, 공백 처리 등을 커널 스타일에 맞게 설정할 수 있다. 또한 커널 소스에는 스타일 문제를 확인할 수 있는 도구도 제공된다. 예를 들어 <strong>checkpatch</strong> 는 패치가 커널 코딩 스타일을 잘 따르는지 검사하는 데 사용된다. 공식 문서에서도 코드 들여쓰기는 스페이스가 아니라 탭을 사용해야 하며, 문서나 일부 설정 파일을 제외하면 일반 코드의 들여쓰기에 스페이스를 사용하지 않는다고 설명한다.</p>
<p>두 번째 준비는 <strong>이메일 설정(Email Setup)</strong> 이다. 리눅스 커널 개발에서는 패치를 이메일로 보내는 문화가 여전히 중요하다. 커널 패치는 보통 메일 본문에 인라인 텍스트 형태로 보내며, 첨부 파일 방식은 리뷰 과정에서 인용하고 토론하기 불편하기 때문에 일반적으로 선호되지 않는다. 공식 문서에서도 리눅스 커널 패치는 이메일로 제출되며, 가능하면 이메일 본문에 인라인 텍스트로 넣는 것이 좋다고 설명한다. 따라서 커널 패치를 제출하려면 일반적인 메일 작성뿐 아니라, 패치 형식이 깨지지 않도록 보내는 설정도 중요하다.</p>
<p>세 번째는 <strong>Git 설정(Git Setup)</strong> 이다. 현재 리눅스 커널은 Git으로 관리된다. Git은 리누스 토발즈가 커널 개발을 위해 만든 분산 버전 관리 시스템이며, 커널 소스를 내려받고, 변경 이력을 관리하고, 패치를 생성하는 데 핵심적으로 사용된다. 커널 개발을 제대로 하려면 단순히 압축 파일을 다운로드하는 것보다 Git 저장소를 이용하는 것이 좋다. 공식 패치 제출 문서에서도 현재 커널 소스 트리가 없다면 Git으로 소스를 받는 것을 안내하며, tarball로 커널 릴리스를 다운로드하는 방법도 가능하지만 커널 개발을 하기에는 어려운 방식이라고 설명한다.</p>
<p>그다음은 <strong>커널 소스 다운로드(Downloading Kernel Source)</strong> 이다. 커널을 수정하려면 먼저 수정할 대상이 되는 커널 소스가 필요하다. 단순 학습이라면 특정 버전의 커널 소스를 다운로드해서 살펴볼 수 있지만, 실제 패치를 제출하려면 어느 트리를 기준으로 작업할지 확인해야 한다. 모든 패치가 무조건 리누스 토발즈의 메인라인 트리만을 기준으로 만들어지는 것은 아니며, 특정 하위 시스템의 메인테이너가 관리하는 별도 트리를 기준으로 패치를 요구할 수도 있다. 공식 문서에서도 대부분의 하위 시스템 메인테이너는 자신들이 관리하는 트리를 기준으로 패치를 준비하길 원할 수 있다고 설명한다.</p>
<p>다섯 번째는 <strong>커널 설정(Kernel Configuration)</strong> 이다. 커널 소스를 받은 뒤에는 어떤 기능을 포함하고 제외할지 설정해야 한다. 커널은 매우 큰 프로그램이기 때문에 모든 기능을 무조건 하나의 형태로 빌드하는 것이 아니라, 시스템에 필요한 기능을 선택하고, 어떤 기능은 커널에 직접 포함하고, 어떤 기능은 모듈로 만들 수 있다. 이 설정 과정이 끝나야 실제 컴파일을 진행할 수 있다.</p>
<p>여섯 번째는 <strong>커널 컴파일(Kernel Compilation)</strong> 이다. 설정이 끝나면 커널 소스를 컴파일하여 실행 가능한 커널 이미지와 필요한 모듈들을 만든다. 커널 컴파일은 일반 프로그램보다 시간이 오래 걸릴 수 있으며, 시스템 성능과 설정 내용에 따라 소요 시간이 달라진다. 따라서 처음부터 전체 커널을 완벽하게 수정하려고 하기보다는, 작은 수정부터 시작해 컴파일과 테스트 과정을 반복하면서 익숙해지는 것이 좋다.</p>
<p>일곱 번째는 <strong>코드 수정과 재컴파일(Code Modification and Recompilation)</strong> 이다. 커널 소스를 수정한 뒤에는 다시 컴파일하여 오류가 없는지 확인해야 한다. 단순히 문법 오류가 없는지만 보는 것이 아니라, 수정한 내용이 실제 커널 동작에 영향을 주는 만큼 빌드와 테스트 과정이 중요하다. 커널 코드는 시스템 전체와 연결되어 있기 때문에 작은 수정도 예상하지 못한 문제를 만들 수 있다.</p>
<p>마지막은 <strong>수정한 코드 제출(Submitting Modified Code)</strong> 이다. 수정한 코드가 준비되면 Git을 이용해 패치를 만들고, 정해진 형식에 맞춰 메일로 제출한다. 패치 메일 제목에는 보통 <code>[PATCH]</code> 를 붙여 다른 논의 메일과 구분한다. 공식 문서에서도 리눅스 커널 메일 트래픽이 많기 때문에 제목에 <code>[PATCH]</code> 를 붙이는 것이 일반적인 관례이며, <code>git send-email</code>을 사용하면 이를 자동으로 처리해 준다고 설명한다.</p>
<p>정리하면, 커널 패치를 제출하기 위한 준비 과정은 단순히 “코드를 고친다”에서 끝나지 않는다. 편집기 설정, 커널 코딩 스타일 준수, 이메일 설정, Git 설정, 커널 소스 다운로드, 커널 설정, 컴파일, 수정, 재컴파일, 패치 제출까지 이어지는 흐름을 이해해야 한다. 특히 리눅스 커널은 오랜 시간 동안 메일 기반 협업 문화와 엄격한 코드 형식을 유지해 온 프로젝트이기 때문에, 전체 커뮤니티에 반영하고 싶은 수정이라면 그 문화와 규칙에 맞춰 작업하는 것이 중요하다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/3eb27caf-6483-4b8f-ae72-e2aa697249b1.png" alt="" style="display:block;margin:0 auto" />

<h3>커널 컴파일 준비와 소스 코드 압축 풀기 Preparing for Kernel Compilation and Extracting Source Code</h3>
<p>이번에는 실제로 <strong>커널 컴파일(Kernel Compilation)</strong> 을 하기 위한 준비 과정을 살펴본다. 커널을 컴파일하려면 먼저 컴파일에 필요한 도구와 라이브러리를 설치해야 하고, 그다음 리눅스 커널 소스 코드를 다운로드한 뒤 압축을 풀고 소스 코드 디렉터리로 이동해야 한다.</p>
<p>먼저 필요한 도구 패키지를 설치한다. 강의에서 제시된 명령어는 다음과 같다.</p>
<pre><code class="language-plaintext">sudo apt-get install libncurses5-dev gcc make git exuberant-ctags bc libssl-dev flex bison
</code></pre>
<p>여기서 <strong>apt-get</strong> 은 Debian 계열 리눅스 배포판에서 패키지를 설치하거나 관리할 때 사용하는 명령어이다. Ubuntu나 Debian 기반 환경에서는 <code>apt-get install</code>을 사용해 필요한 패키지를 설치할 수 있다. 최근에는 사용자 입장에서 더 간단하게 <code>apt install</code>을 사용하는 경우도 많다. 따라서 아래처럼 작성해도 된다.</p>
<pre><code class="language-plaintext">sudo apt install libncurses5-dev gcc make git exuberant-ctags bc libssl-dev flex bison
</code></pre>
<p>다만 <code>apt-get</code>이 완전히 사라진 것은 아니다. Ubuntu 공식 문서에서는 <code>apt</code>는 대화형 사용에 적합하고, <code>apt-get</code>은 스크립트에서 사용하는 것이 좋다고 설명한다. 기본적인 설치 명령에서는 두 명령어의 문법이 거의 같기 때문에, 수업이나 실습에서는 <code>apt install</code>로 이해해도 무리가 없다. (<a href="https://ubuntu.com/server/docs/how-to/software/package-management/?utm_source=chatgpt.com">Ubuntu Documentation</a>)</p>
<p>설치하는 패키지들을 보면 각각 커널 컴파일 과정에서 필요한 역할이 있다. <strong>gcc</strong> 는 C 언어 컴파일러이고, <strong>make</strong> 는 Makefile에 따라 빌드 과정을 실행하는 도구이다. <strong>git</strong> 은 커널 소스 코드를 내려받거나 버전 관리를 할 때 사용한다. <strong>libncurses5-dev</strong> 는 커널 설정 메뉴를 터미널에서 보기 좋게 표시하는 <code>menuconfig</code> 같은 기능에 필요할 수 있다. <strong>bc</strong> 는 커널 빌드 중 계산 작업에 사용될 수 있고, <strong>libssl-dev</strong> 는 인증서나 암호화 관련 빌드 과정에서 필요할 수 있다. <strong>flex</strong> 와 <strong>bison</strong> 은 일부 설정 파일이나 문법 분석 관련 도구를 생성하는 데 사용된다. <strong>exuberant-ctags</strong> 는 소스 코드 안의 함수나 변수 위치를 추적하기 쉽게 해 주는 태그 파일 생성 도구로, 커널 소스 탐색에 도움이 된다.</p>
<p>강의 자료에서는 두 번째 단계로 <strong>리눅스 소스 코드 다운로드(Downloading Linux Source Code)</strong> 를 언급한다. 녹취록에는 두 번째 단계에도 패키지 설치 명령어가 한 번 더 적혀 있는데, 문맥상 이 부분은 커널 소스 코드를 다운로드하는 단계로 보는 것이 자연스럽다. 예를 들어 <a href="http://kernel.org">kernel.org</a>에서 원하는 커널 버전의 압축 파일을 다운로드하거나, Git을 이용해 소스 코드를 받을 수 있다. 강의에서는 이후 <code>linux-6.1.166.tar.xz</code> 파일을 예로 들고 있으므로, 이 파일을 다운로드했다고 가정하고 설명이 이어진다.</p>
<p>그다음 단계는 <strong>압축 풀기(Extracting Archive)</strong> 이다. 예시 명령어는 다음과 같다.</p>
<pre><code class="language-plaintext">tar xJvf linux-6.1.166.tar.xz
</code></pre>
<p>여기서 <code>tar</code>는 여러 파일과 디렉터리를 하나의 아카이브 파일로 묶거나 풀 때 사용하는 명령어이다. 옵션의 의미를 보면 <code>x</code>는 압축을 푼다는 뜻이고, <code>v</code>는 진행 과정을 화면에 자세히 보여준다는 뜻이며, <code>f</code>는 뒤에 파일 이름이 온다는 뜻이다. 가운데의 대문자 <code>J</code>는 <strong>xz 압축(XZ Compression)</strong> 형식의 파일을 처리할 때 사용하는 옵션이다. GNU tar 문서에서도 <code>-J</code> 옵션은 <code>--xz</code>와 같은 의미로, 아카이브를 xz 방식으로 처리한다고 설명한다. (<a href="https://www.gnu.org/software/tar/manual/html_section/All-Options.html?utm_source=chatgpt.com">GNU tar Manual</a>)</p>
<p>따라서 파일 이름이 <code>.tar.xz</code>로 끝난다면 위와 같이 <code>xJvf</code> 옵션을 사용할 수 있다. 만약 파일이 <code>.tar.gz</code> 또는 <code>.tgz</code>로 끝난다면 gzip 형식이므로 보통 <code>xzvf</code>를 사용한다.</p>
<pre><code class="language-plaintext">tar xzvf linux-6.1.166.tar.gz
</code></pre>
<p>파일이 <code>.tar.bz2</code>로 끝난다면 bzip2 형식이므로 보통 <code>xjvf</code>를 사용한다.</p>
<pre><code class="language-plaintext">tar xjvf linux-6.1.166.tar.bz2
</code></pre>
<p>즉, 다운로드한 파일의 확장자가 무엇인지에 따라 <code>tar</code> 옵션이 달라질 수 있다. 강의에서 말한 것처럼 <code>.xz</code>로 끝나는 파일은 <code>J</code> 옵션을 사용하는 예로 이해하면 된다.</p>
<p>압축을 풀고 나면 커널 소스 코드 디렉터리가 생성된다. 예를 들어 <code>linux-6.1.166.tar.xz</code> 파일을 풀면 보통 <code>linux-6.1.166</code>이라는 디렉터리가 만들어진다. 그다음에는 해당 디렉터리로 이동한다.</p>
<pre><code class="language-plaintext">cd linux-6.1.166
</code></pre>
<p>이제 이 디렉터리 안에서 커널 설정과 컴파일 작업을 진행할 수 있다. 즉, 이번 단계의 흐름은 필요한 빌드 도구를 설치하고, 커널 소스 코드를 다운로드한 뒤, 압축을 풀고, 생성된 소스 코드 디렉터리로 이동하는 과정이라고 정리할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/5a7375fa-3bab-41d9-b040-07925bdfbc4c.png" alt="" style="display:block;margin:0 auto" />

<h3>커널 설정 파일과 <code>.config</code></h3>
<h3>Kernel Configuration File and <code>.config</code></h3>
<p><strong>커널 컴파일(Kernel Compilation)</strong> 을 하려면 먼저 커널 설정 파일인 <code>.config</code> 파일을 준비해야 한다. 커널 소스 코드를 다운로드하고 압축을 풀었다고 해서 바로 컴파일할 수 있는 것은 아니다. 리눅스 커널은 매우 많은 기능과 드라이버, 파일 시스템, 네트워크 기능, 보안 기능 등을 포함하고 있기 때문에, 그중 어떤 기능을 커널에 포함할지 먼저 설정해야 한다. 이 설정 내용을 저장하는 파일이 바로 <code>.config</code> 이다.</p>
<p>여기서 파일 이름 앞에 붙은 점 <code>.</code>은 리눅스에서 숨김 파일을 의미한다. 따라서 <code>.config</code> 파일은 일반적인 <code>ls</code> 명령으로는 보이지 않을 수 있고, <code>ls -a</code>처럼 숨김 파일까지 표시하는 옵션을 사용해야 확인할 수 있다. 이 <code>.config</code> 파일은 커널 소스 코드의 최상위 디렉터리, 즉 <strong>Kernel Source Tree</strong> 의 루트 위치에 있어야 한다. 그래야 <code>make</code> 명령으로 커널을 설정하거나 컴파일할 때 해당 설정을 기준으로 빌드가 진행된다.</p>
<p>가장 간단한 방법은 현재 사용 중인 커널의 설정을 복사해서 사용하는 것이다. 여기서 “현재 사용 중인 커널”이란, 지금 시스템을 부팅할 때 실제로 사용된 커널을 의미한다. 리눅스 시스템에는 보통 현재 부팅된 커널의 설정 정보가 <code>/boot</code> 디렉터리 아래에 저장되어 있다. 예를 들어 현재 실행 중인 커널 버전은 <code>uname -r</code> 명령으로 확인할 수 있고, 그 버전에 해당하는 설정 파일을 커널 소스 디렉터리의 <code>.config</code>로 복사해서 사용할 수 있다.</p>
<pre><code class="language-plaintext">cp /boot/config-$(uname -r) .config
</code></pre>
<p>이렇게 하면 현재 사용 중인 환경의 커널 설정을 새로 컴파일할 커널 소스에 가져와서 사용할 수 있다. 이 방식이 처음 커널을 컴파일하는 사람에게 가장 안전한 이유는, 현재 시스템에서 정상적으로 동작하고 있는 드라이버와 기능 설정을 최대한 그대로 가져오기 때문이다. 예를 들어 지금 사용하는 네트워크 카드, 저장장치, 파일 시스템, 그래픽 장치 등에 필요한 설정이 포함되어 있을 가능성이 높다.</p>
<p>물론 사용자가 커널 설정을 직접 하나하나 선택할 수도 있다. 기본 설정을 적용하고 싶다면 다음과 같은 명령을 사용할 수 있다.</p>
<pre><code class="language-plaintext">make defconfig
</code></pre>
<p><code>make defconfig</code> 는 현재 아키텍처에 맞는 기본 커널 설정을 생성하는 명령이다. 이 명령을 사용하면 커널 소스에 정의된 기본값을 바탕으로 <code>.config</code> 파일이 만들어진다. 다만 이 기본 설정이 현재 내 컴퓨터의 하드웨어와 완전히 일치한다고 보장할 수는 없다. 따라서 처음 커널 컴파일을 실습할 때는 현재 사용 중인 커널 설정을 복사해 오는 방식이 더 안전할 수 있다.</p>
<p>설정을 직접 수정하고 싶다면 <code>make menuconfig</code> 또는 <code>make nconfig</code> 를 사용할 수 있다.</p>
<pre><code class="language-plaintext">make menuconfig
</code></pre>
<pre><code class="language-plaintext">make nconfig
</code></pre>
<p><code>make menuconfig</code> 는 터미널 안에서 메뉴 형태로 커널 옵션을 선택할 수 있게 해 주는 설정 도구이다. <code>make nconfig</code> 도 비슷하게 텍스트 기반 메뉴 화면을 제공하지만, <code>menuconfig</code>보다 개선된 형태의 인터페이스를 제공한다. 공식 커널 문서에서도 <code>make menuconfig</code>는 텍스트 기반 컬러 메뉴, 라디오 리스트, 대화상자를 제공하고, <code>make nconfig</code>는 향상된 텍스트 기반 컬러 메뉴라고 설명한다.</p>
<p>이런 설정 화면에 들어가면 커널 소스 트리 안의 수많은 기능에 대해 선택할 수 있다. 어떤 기능은 커널 안에 직접 포함할 수 있고, 어떤 기능은 <strong>모듈(Module)</strong> 로 빌드할 수 있으며, 어떤 기능은 아예 제외할 수 있다. 일반적으로 설정 화면에서는 기능을 선택할 때 <code>Y</code>, <code>M</code>, <code>N</code>과 같은 형태로 표시한다. <code>Y</code>는 해당 기능을 커널에 직접 포함한다는 뜻이고, <code>M</code>은 모듈로 빌드한다는 뜻이며, <code>N</code>은 포함하지 않는다는 뜻으로 이해할 수 있다.</p>
<p>이 과정에서 <strong>의존성(Dependency)</strong> 도 중요하다. 커널의 어떤 기능은 다른 기능이 함께 켜져 있어야 제대로 동작한다. 만약 필요한 옵션이 빠지거나 의존성이 맞지 않으면, 컴파일 자체가 실패할 수도 있고, 컴파일은 성공하더라도 새 커널로 부팅했을 때 원하는 기능이 동작하지 않을 수 있다. 공식 커널 문서에서도 새 커널 버전에서는 새로운 설정 옵션이 추가될 수 있으므로, 설정 단계를 건너뛰면 예상하지 못한 문제가 생길 수 있다고 설명한다.</p>
<p>가장 대표적인 문제가 <strong>드라이버(Driver)</strong> 를 빼먹는 경우이다. 예를 들어 기존 커널로 부팅했을 때는 네트워크가 정상적으로 동작했는데, 새 커널을 컴파일하면서 네트워크 카드 드라이버를 제외해 버렸다면 새 커널로 부팅한 뒤 네트워크가 연결되지 않을 수 있다. 사용자는 컴파일 환경에서는 분명히 네트워크가 됐기 때문에 당황할 수 있지만, 실제로 새로 부팅한 커널 안에는 해당 네트워크 드라이버가 없기 때문에 문제가 발생하는 것이다.</p>
<p>그래서 처음 커널을 컴파일할 때는 현재 사용 중인 커널 설정을 복사해 오는 방식이 실용적이다. 현재 커널 설정을 가져오면 지금 시스템에서 사용 중인 드라이버와 기능들이 설정 파일에 포함되어 있을 가능성이 높다. 물론 커널 버전이 달라지면 새로운 설정 항목이 생길 수 있으므로, 기존 <code>.config</code>를 복사한 뒤에는 <code>make oldconfig</code> 또는 <code>make olddefconfig</code> 같은 명령으로 새 버전에 맞게 설정을 정리하는 것이 일반적이다. 공식 문서에서도 기존 <code>.config</code> 파일을 새 버전으로 가져갈 때 <code>make oldconfig</code>를 사용하면 기존 설정을 바탕으로 하고, 새로 추가된 설정 항목에 대해서만 질문한다고 설명한다.</p>
<p>결국 <code>.config</code> 파일은 커널 컴파일에서 매우 중요한 역할을 한다. 커널 소스 전체가 재료라면, <code>.config</code>는 그 재료 중 무엇을 사용할지 정하는 설계도와 같다. 현재 시스템과 비슷한 커널을 만들고 싶다면 기존 설정을 복사하는 것이 좋고, 특정 기능을 직접 켜거나 끄고 싶다면 <code>make menuconfig</code>나 <code>make nconfig</code>를 통해 세부 옵션을 조정하면 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/c1654504-605a-4009-baf7-df695bc93675.png" alt="" style="display:block;margin:0 auto" />

<h3>커널 컴파일과 병렬 빌드 Kernel Compilation and Parallel Build</h3>
<p>커널 설정 파일인 <code>.config</code> 까지 준비되었다면, 실제 <strong>커널 컴파일(Kernel Compilation)</strong> 과정은 명령어 자체만 보면 간단하다. 커널 소스 디렉터리에서 다음과 같이 <code>make</code> 명령을 실행하면 된다.</p>
<pre><code class="language-plaintext">make
</code></pre>
<p>다만 커널은 규모가 큰 프로그램이기 때문에 단순히 <code>make</code>만 실행하면 시간이 오래 걸릴 수 있다. 그래서 보통은 <strong>병렬 컴파일(Parallel Compilation)</strong> 을 사용한다. 병렬 컴파일은 여러 작업을 동시에 나누어 실행하는 방식이다. 이때 사용하는 옵션이 <code>-j</code>이다.</p>
<pre><code class="language-plaintext">make -j4
</code></pre>
<p>여기서 <code>-j4</code>는 <code>make</code>가 동시에 최대 4개의 작업을 실행하도록 한다는 뜻이다. 숫자를 크게 하면 더 많은 작업을 동시에 처리하려고 하기 때문에 컴파일 시간이 줄어들 수 있다. 하지만 숫자가 무조건 크다고 좋은 것은 아니다. 실제로 일을 처리할 수 있는 <strong>CPU Core</strong> 수와 시스템 자원에 영향을 받기 때문이다.</p>
<p>예를 들어 CPU 코어가 1개뿐인데 <code>make -j100</code>을 입력한다고 해서 실제로 100명이 동시에 일하는 것처럼 빨라지는 것은 아니다. 작업을 많이 만들어도 실제로 처리할 CPU 자원이 부족하면 오히려 문맥 전환이나 메모리 사용량이 늘어나 비효율적일 수 있다. 반대로 CPU 코어가 여러 개 있다면 <code>-j</code> 옵션을 사용해서 여러 컴파일 작업을 동시에 실행할 수 있고, 그만큼 결과를 더 빨리 볼 수 있다. GNU Make 문서에서도 <code>make -j</code> 옵션은 한 번에 여러 레시피를 동시에 실행할 수 있게 하는 병렬 실행 옵션이라고 설명한다.</p>
<p>실습에서는 컴파일 시간이 얼마나 걸리는지 확인하기 위해 앞에 <code>time</code> 명령을 붙일 수 있다.</p>
<pre><code class="language-plaintext">time make -j4
</code></pre>
<p>또는 CPU 코어 수에 맞춰 자동으로 값을 넣고 싶다면 다음과 같이 사용할 수도 있다.</p>
<pre><code class="language-plaintext">time make -j$(nproc)
</code></pre>
<p>여기서 <code>nproc</code>은 현재 시스템에서 사용할 수 있는 처리 장치 수를 출력하는 명령이다. 따라서 <code>make -j$(nproc)</code>은 현재 시스템의 CPU 코어 수에 맞춰 병렬 컴파일을 실행하는 방식으로 이해할 수 있다.</p>
<p><code>time</code> 명령을 붙여 실행하면 컴파일이 끝난 뒤 아래와 비슷한 시간이 출력된다.</p>
<pre><code class="language-plaintext">real    7m10.000s
user    35m20.000s
sys     3m40.000s
</code></pre>
<p>여기서 <strong>real time</strong> 은 실제로 사용자가 체감한 시간이다. 쉽게 말해 시계로 잰 시간이다. 예를 들어 오전 10시에 컴파일을 시작해서 오전 10시 7분에 끝났다면 <code>real</code> 시간은 약 7분으로 표시된다. Red Hat 문서에서도 <code>real</code>은 명령이 시작해서 끝날 때까지 실제 경과한 시간이라고 설명한다.</p>
<p><strong>user time</strong> 은 프로그램이 <strong>User Space</strong> 에서 CPU를 사용한 시간이다. 커널 컴파일에서는 컴파일러가 소스 코드를 분석하고 오브젝트 파일을 만드는 작업 대부분이 사용자 영역에서 실행되므로, <code>user</code> 시간이 크게 나올 수 있다. 특히 <code>make -j</code>로 여러 작업을 동시에 실행하면 여러 CPU 코어가 각각 시간을 소비하기 때문에, <code>user</code> 시간이 <code>real</code> 시간보다 훨씬 크게 나올 수 있다.</p>
<p><strong>sys time</strong> 은 프로그램이 <strong>Kernel Space</strong> 에서 CPU를 사용한 시간이다. 예를 들어 파일을 읽고 쓰거나, 프로세스를 생성하거나, 시스템 호출을 처리하는 데 사용된 시간이 여기에 포함된다. Red Hat 문서도 <code>user</code>는 user mode에서 소비된 CPU 시간이고, <code>sys</code>는 kernel mode에서 소비된 CPU 시간이라고 설명한다.</p>
<p>여기서 처음 헷갈릴 수 있는 부분은 <code>real</code>보다 <code>user</code> 시간이 더 크게 나오는 경우이다. 이것은 오류가 아니다. 병렬 컴파일에서는 여러 작업이 동시에 실행되기 때문에 CPU 사용 시간이 합산되어 표시된다. 예를 들어 4개의 컴파일 작업이 각각 10분씩 CPU를 사용했다면, 실제 시계 시간은 10분에 가까울 수 있지만 <code>user</code> 시간은 합산되어 40분처럼 표시될 수 있다. 강의에서 말한 것처럼 “10분, 10분, 10분, 10분 해서 총 40분을 소비했다고 계산하는 방식”으로 이해하면 된다.</p>
<p>반대로 <code>real</code> 시간은 긴데 <code>user</code>와 <code>sys</code> 시간이 작다면, CPU가 계속 바쁘게 계산한 것이 아니라 중간에 기다리는 시간이 많았다는 뜻일 수 있다. 예를 들어 디스크 입출력을 기다렸거나, 다른 프로세스 때문에 대기했거나, 병렬로 처리할 작업이 충분하지 않았을 수 있다. 따라서 커널 컴파일 시간을 볼 때는 단순히 <code>real</code>만 보는 것이 아니라, <code>user</code>와 <code>sys</code> 시간이 어떻게 나오는지도 함께 보면 시스템이 CPU를 얼마나 적극적으로 사용했는지 어느 정도 파악할 수 있다.</p>
<p>결국 커널 컴파일은 <code>.config</code> 파일을 준비한 뒤 <code>make</code>를 실행하는 구조이지만, 실제 시간을 줄이기 위해서는 <code>make -j</code> 옵션을 적절히 사용하는 것이 중요하다. <code>-j</code> 뒤의 숫자는 클수록 무조건 좋은 것이 아니라, 시스템의 CPU 코어 수와 메모리, 디스크 성능에 맞춰 정해야 한다. 그리고 <code>time</code> 명령을 함께 사용하면 실제 경과 시간과 CPU 사용 시간을 나누어 확인할 수 있어, 컴파일이 얼마나 효율적으로 진행되었는지 이해하는 데 도움이 된다.</p>
<hr />
<h1>2️⃣ Module Program</h1>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/22c6dd86-705c-4215-864c-d777d2415cb7.png" alt="" style="display:block;margin:0 auto" />

<h3>모듈 프로그램의 개념과 장점 Concept and Advantages of Module Programming</h3>
<p>이번에는 <strong>모듈 프로그램(Module Program)</strong> 에 대해 간략하게 살펴본다. 앞에서 다룬 <strong>커널(Kernel)</strong> 은 운영체제의 핵심 전체에 가까운 큰 개념이다. 반면 <strong>모듈(Module)</strong> 은 그보다 작은 단위이기 때문에 처음 공부할 때 커널 전체를 다루는 것보다 조금 더 쉽게 느껴질 수 있다.</p>
<p>모듈의 사전적 의미는 표준화된 독립적인 단위라고 볼 수 있다. 컴퓨터에서도 비슷한 의미로 사용된다. 어떤 기능이 하나의 독립된 단위로 만들어져 있고, 필요할 때 끼워 넣거나 필요 없을 때 빼는 방식으로 사용할 수 있다. 즉, 모듈은 자체적으로 하나의 기능을 담당하는 비교적 독립적인 구성 요소라고 이해할 수 있다.</p>
<p>리눅스에서 말하는 <strong>커널 모듈(Kernel Module)</strong> 은 실행 중인 커널에 동적으로 추가하거나 제거할 수 있는 코드이다. 예를 들어 특정 <strong>장치 드라이버(Device Driver)</strong>, 파일 시스템 기능, 네트워크 관련 기능 등이 모듈 형태로 제공될 수 있다. 커널 설정에서 어떤 기능을 커널 안에 직접 포함할 수도 있고, 모듈로 따로 빌드할 수도 있다. 모듈로 만들어진 기능은 필요할 때 커널에 올려 사용하고, 필요하지 않으면 제거할 수 있다.</p>
<p>커널 모듈을 컴파일해서 사용할 때 가장 큰 장점은 <strong>재부팅(Reboot)</strong> 이 필요하지 않다는 점이다. 일반적으로 커널 자체를 수정하면 수정된 커널을 다시 컴파일하고, 그 커널 이미지로 부팅해야 한다. 예를 들어 현재 <code>1.0.0</code> 커널로 부팅해 사용하고 있는데 커널의 일부 기능을 수정했다면, 수정한 커널을 다시 빌드한 뒤 새 커널로 부팅해야 한다. 결국 시스템을 껐다 켜야 한다는 뜻이다.</p>
<p>물론 커널의 매우 중요한 부분이나 시스템 안정성과 직접 관련된 부분을 수정했다면, 새 커널로 안전하게 부팅하는 과정이 필요할 수 있다. 하지만 단순히 특정 기능이나 특정 드라이버만 바꾸고 싶은 상황이라면 이야기가 달라진다. 예를 들어 특정 장치 드라이버만 수정해야 하는 경우, 전체 커널을 다시 컴파일하고 시스템을 재부팅하는 것은 부담이 크다. 이때 해당 드라이버를 모듈 형태로 컴파일하고, 실행 중인 커널에 적용할 수 있다면 훨씬 효율적이다.</p>
<p>이 점은 실제 서비스를 운영하는 환경에서 매우 중요하다. 서버나 금융 시스템처럼 계속 동작해야 하는 시스템에서는 잠깐의 중단도 사용자에게 큰 불편을 줄 수 있다. 예를 들어 금융기관에서 카드 결제가 중단되는 시간이 공지되었는데, 사용자가 하필 그 시간에 결제를 해야 한다면 큰 불편을 겪게 된다. 컴퓨터 시스템 입장에서도 마찬가지로, 서비스가 내려가는 <strong>다운타임(Downtime)</strong> 이 발생하면 그 시간 동안 사용자는 해당 서비스를 이용할 수 없다.</p>
<p>따라서 리눅스 커널 모듈이 재부팅 없이 동적으로 적용될 수 있다는 점은 큰 장점이 된다. 전체 커널을 새로 빌드하고 부팅하는 것보다, 필요한 기능만 모듈로 올리거나 내릴 수 있다면 시스템 운영 측면에서 훨씬 유연하다. 리눅스 커널 문서에서도 외부 모듈을 빌드하려면 미리 빌드된 커널과 그 빌드에 사용된 설정 및 헤더 파일이 필요하다고 설명한다. 또한 배포판 커널을 사용하는 경우, 현재 실행 중인 커널에 맞는 패키지가 배포판에서 제공된다고 설명한다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/c0b5def6-7160-41b9-b373-af33b0aec6a4.png" alt="" style="display:block;margin:0 auto" />

<p>커널 모듈은 주로 <strong>C Language</strong> 로 작성된다. 리눅스 커널 자체가 대부분 C 언어로 작성되어 있기 때문에, 커널 모듈도 일반적으로 C 언어를 사용한다. 다만 일반 사용자 프로그램을 작성하는 C 코드와는 다르다. 커널 모듈은 커널 내부에서 동작하기 때문에 일반적인 표준 C 라이브러리를 자유롭게 사용할 수 없고, 커널이 제공하는 함수와 자료구조를 사용해야 한다.</p>
<p>또 하나 중요한 점은 커널 모듈을 컴파일할 때 현재 사용 중인 커널의 <strong>커널 헤더(Kernel Header)</strong> 와 설정 정보가 맞아야 한다는 것이다. 모듈은 실행 중인 커널에 붙어서 동작하기 때문에, 다른 커널 버전을 기준으로 컴파일하면 로드되지 않거나 동작 중 문제가 생길 수 있다. 공식 커널 문서에서도 외부 모듈을 빌드하려면 빌드에 사용된 설정과 헤더 파일을 포함한 미리 빌드된 커널이 필요하다고 설명한다.</p>
<p>결국 모듈 프로그램은 커널 전체를 다시 수정하고 재부팅하는 부담을 줄여 주는 방식이다. 커널 전체가 큰 덩어리라면, 모듈은 그 안에 필요할 때 끼워 넣을 수 있는 독립적인 기능 단위라고 볼 수 있다. 그래서 커널 모듈은 드라이버처럼 특정 기능만 교체하거나 추가해야 할 때 유용하고, 서비스 중단을 최소화해야 하는 환경에서 특히 큰 장점을 가진다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/c8a26f8c-7f46-46e8-8e01-bb6f868748ab.png" alt="" style="display:block;margin:0 auto" />

<h3>Hello World 커널 모듈 프로그램 Hello World Kernel Module Program</h3>
<p>이번 예시는 Hello World 커널 모듈(Hello World Kernel Module) 프로그램이다. 겉으로 보면 일반 C 프로그램(C Program) 과 크게 다르지 않아 보이지만, 중요한 차이가 있다. 일반 C 프로그램에서는 출력할 때 <code>printf()</code>를 사용하지만, 커널 모듈에서는 <code>printk()</code> 를 사용한다.</p>
<p><code>printk()</code> 는 커널 내부에서 사용하는 출력 함수이다. 일반 프로그램의 <code>printf()</code>처럼 문자열을 출력하는 역할을 하지만, 화면에 바로 출력하기보다는 <code>커널 로그 버퍼(Kernel Log Buffer)</code> 에 메시지를 남긴다. 이 메시지는 보통 <code>dmesg</code> 명령으로 확인할 수 있다. 예를 들어 다음과 같은 코드가 사용된다.</p>
<p><code>printk(KERN_ALERT "Goodbye, cruel world\n");</code></p>
<p>여기서 <code>KERN_ALERT</code> 는 메시지의 중요도를 나타내는 로그 레벨(Log Level) 이다. 커널 메시지는 단순 출력이 아니라, 긴급도나 중요도에 따라 구분되어 기록된다.</p>
<p>또 다른 차이는 일반 C 프로그램에서 보이는 <code>main()</code> 함수가 없다는 점이다. 일반 C 프로그램은 <code>main()</code>에서 시작하지만, 커널 모듈은 독립적으로 실행되는 프로그램이 아니라 실행 중인 커널에 삽입되는 코드이다. 그래서 <code>main()</code> 대신 다음과 같은 매크로를 사용한다.</p>
<p><code>module_init(hello_init);</code> <code>module_exit(hello_exit);</code></p>
<p><code>module_init()</code> 은 모듈이 커널에 로드될 때 실행할 함수를 지정하고, <code>module_exit()</code> 은 모듈이 제거될 때 실행할 함수를 지정한다. 즉, <code>hello_init</code>은 모듈 시작 시점의 함수이고, <code>hello_exit</code>은 모듈 종료 시점의 함수라고 볼 수 있다. 공식 문서에서도 <code>module_init()</code>은 모듈 삽입 시 실행될 함수를 지정하고, <code>module_exit()</code>은 모듈 제거 시 실행될 함수를 지정한다고 설명한다.</p>
<p>따라서 커널 모듈 프로그램은 일반 C 프로그램처럼 <code>main()</code>을 기준으로 시작하지 않는다. 이미 실행 중인 커널 안에 들어가서 동작하기 때문에, 모듈이 삽입될 때 어떤 함수를 실행할지, 제거될 때 어떤 함수를 실행할지를 따로 등록하는 방식으로 동작한다.</p>
<p>이 지점에서 커널 전체의 시작점도 함께 생각해 볼 수 있다. 커널 소스는 매우 크고 많은 파일로 구성되어 있지만, 코드가 많다고 해서 시작점이 여러 개라고 단순히 말할 수는 없다. 커널은 일반 C 프로그램처럼 단순히 <code>main()</code> 하나에서 시작하는 구조가 아니라, 부트로더, 아키텍처별 초기화 코드, 일부 어셈블리 코드(Assembly Code), 그리고 C 코드 초기화 과정이 이어지면서 실행된다. 그래서 이 부분은 단순 암기보다 “왜 커널 모듈에는 main()이 없는가”를 이해하는 것이 중요하다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f1e986dd-baa4-4593-8271-7b8823dbee0f.png" alt="" style="display:block;margin:0 auto" />

<h3>Hello World 커널 모듈 설명</h3>
<h3>Explanation of Hello World Kernel Module</h3>
<p><strong>Hello World 커널 모듈(Hello World Kernel Module)</strong> 에서는 모듈이 시작될 때와 끝날 때 실행되는 함수가 따로 지정된다. 모듈이 커널에 적재되어 활동을 시작할 때는 <code>module_init(hello_init)</code>에 등록된 <code>hello_init</code> 함수가 실행되고, 모듈이 제거되어 활동이 끝날 때는 <code>module_exit(hello_exit)</code>에 등록된 <code>hello_exit</code> 함수가 실행된다.</p>
<p>모듈에는 <strong>라이선스 정보(Module License Information)</strong> 도 적어야 한다. 예제에서는 보통 다음과 같이 작성한다.</p>
<pre><code class="language-c">MODULE_LICENSE("Dual BSD/GPL");
</code></pre>
<p>이것은 해당 모듈이 <strong>BSD/GPL 이중 라이선스(Dual BSD/GPL License)</strong> 를 따른다는 의미이다. 모듈 라이선스는 단순한 설명처럼 보이지만 실제로 영향을 줄 수 있다. 예를 들어 GPL 호환 라이선스가 아닌 모듈은 일부 커널 심볼을 사용할 수 없거나, 커널이 오염된 상태인 <strong>Tainted Kernel</strong> 로 표시될 수 있다.</p>
<p>출력 함수로는 일반 C 프로그램의 <code>printf()</code>가 아니라 <code>printk()</code> 를 사용한다. <code>printk()</code>는 커널에서 사용하는 출력 함수이며, 메시지는 화면에 바로 출력되는 것이 아니라 <strong>커널 로그 버퍼(Kernel Log Buffer)</strong> 에 기록된다. 이 로그는 보통 <code>dmesg</code> 명령이나 <code>/dev/kmsg</code>를 통해 확인할 수 있다.</p>
<p>즉, <code>printf()</code>는 일반 사용자 프로그램이 <strong>User Space</strong> 에서 출력할 때 사용하는 함수이고, <code>printk()</code>는 <strong>Kernel Space</strong> 에서 동작하는 커널용 출력 함수이다. 사용자 공간과 커널 공간은 분리되어 있기 때문에, 커널 내부에서 일반적인 <code>printf()</code>를 사용하는 것이 아니라 커널이 제공하는 <code>printk()</code>를 사용한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/937fb6a4-5f5c-479b-8d59-46ec3d77992a.png" alt="" style="display:block;margin:0 auto" />

<h3>Hello World 모듈 컴파일 준비 Preparing to Compile the Hello World Module</h3>
<p>Hello World 커널 모듈(Hello World Kernel Module) 을 컴파일하려면 현재 실행 중인 커널에 맞는 <code>커널 헤더(Kernel Headers)</code> 가 필요하다. 모듈은 실행 중인 커널에 적재되어 동작하므로, 컴파일할 때 사용하는 헤더 파일도 현재 커널 버전과 맞아야 한다.</p>
<p>먼저 패키지 목록을 최신 상태로 갱신한다.</p>
<p><code>sudo apt update</code></p>
<p>그다음 현재 사용 중인 커널 버전에 맞는 헤더 패키지를 검색할 수 있다.</p>
<p><code>sudo apt-cache search linux-headers-uname -r</code></p>
<p>여기서 <code>uname -r</code> 은 현재 실행 중인 커널 버전을 출력하는 명령이다. 예를 들어 따로 실행하면 다음처럼 커널 버전을 확인할 수 있다.</p>
<p><code>uname -r</code></p>
<p>명령어 안의 백틱은 명령 치환(Command Substitution) 을 의미한다. 즉, <code>uname -r</code> 부분이 먼저 실행되고, 그 결과값이 명령어 안에 들어간다. Bash 문서에서도 명령 치환은 명령의 출력을 다른 명령의 일부로 사용할 수 있게 해 준다고 설명한다.</p>
<p>그래서 아래 두 방식은 같은 의미로 볼 수 있다.</p>
<p><code>sudo apt install linux-headers-uname -r sudo apt install linux-headers-$(uname -r)</code></p>
<p>최근에는 백틱보다 $(uname -r) 형태를 더 읽기 쉬운 방식으로 많이 사용한다. 따라서 현재 커널에 맞는 헤더를 설치할 때는 다음 명령을 쓰면 된다.</p>
<p><code>sudo apt install linux-headers-$(uname -r)</code></p>
<p>명령어 사이의 세미콜론 ; 은 앞 명령을 실행한 뒤 다음 명령을 이어서 실행하라는 의미이다. 예를 들어 다음 명령은 sudo apt update를 먼저 실행하고, 이어서 헤더 패키지를 검색한다.</p>
<p><code>sudo apt update; sudo apt-cache search linux-headers-$(uname -r)</code></p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/72e59570-d988-44dc-aa73-a82b1ff88c87.png" alt="" style="display:block;margin:0 auto" />

<h3>Hello World 컴파일 2: Makefile 만들기</h3>
<h3>Hello World Compilation 2: Creating a Makefile</h3>
<p><strong>Hello World 커널 모듈(Hello World Kernel Module)</strong> 을 컴파일하려면 <strong>Makefile</strong> 이 필요하다. 커널 모듈은 일반 C 프로그램처럼 <code>gcc hello.c</code>처럼 단순히 컴파일하는 방식이 아니라, 현재 실행 중인 커널의 빌드 시스템을 이용해서 컴파일해야 한다. 그래서 컴파일 명령을 매번 길게 입력하지 않도록 Makefile에 정리해 둔다.</p>
<p>예시 Makefile은 다음과 같은 구조이다.</p>
<pre><code class="language-c">obj-m += hello.o

PWD := $(CURDIR)

all:
	\((MAKE) -C /lib/modules/\)(shell uname -r)/build M=$(PWD) modules

clean:
	\((MAKE) -C /lib/modules/\)(shell uname -r)/build M=$(PWD) clean
</code></pre>
<p>여기서 <code>obj-m += hello.o</code> 는 <code>hello.c</code> 파일을 커널 모듈로 빌드하겠다는 의미이다. 빌드가 성공하면 최종적으로 <code>hello.ko</code>라는 <strong>커널 오브젝트(Kernel Object)</strong> 모듈 파일이 만들어진다. 리눅스 커널 문서에서도 외부 모듈을 빌드할 때 <code>obj-m := &lt;module_name&gt;.o</code> 형태를 사용한다고 설명한다.</p>
<p><code>PWD := \((CURDIR)</code> 는 현재 Makefile이 있는 디렉터리 경로를 <code>PWD</code> 변수에 저장하는 부분이다. 이후 <code>M=\)(PWD)</code>로 이 경로를 커널 빌드 시스템에 넘긴다. 즉, “현재 디렉터리에 있는 모듈 소스를 빌드하라”는 뜻이다.</p>
<p><code>all</code>은 기본 빌드 대상이다. 사용자가 그냥 <code>make</code>를 입력하면 보통 <code>all</code> 항목이 실행된다. 여기서 중요한 부분은 다음 명령이다.</p>
<pre><code class="language-plaintext">\((MAKE) -C /lib/modules/\)(shell uname -r)/build M=$(PWD) modules
</code></pre>
<p><code>/lib/modules/\((shell uname -r)/build</code>는 현재 실행 중인 커널 버전에 맞는 빌드 디렉터리를 가리킨다. 이곳에는 커널 모듈을 컴파일하는 데 필요한 헤더와 빌드 정보가 연결되어 있다. <code>\)(shell uname -r)</code>은 현재 커널 버전을 명령어 결과로 가져오는 부분이다. 커널 문서에서도 외부 모듈을 빌드하려면 현재 커널에 맞는 설정과 헤더가 포함된 빌드된 커널이 필요하다고 설명한다.</p>
<p>마지막으로 <code>clean</code>은 컴파일하면서 생성된 중간 파일들을 정리할 때 사용한다.</p>
<pre><code class="language-c">make clean
</code></pre>
<p>즉, 이 Makefile은 “현재 디렉터리의 <code>hello.c</code>를 현재 실행 중인 커널 버전에 맞춰 모듈로 컴파일하고, 필요하면 생성 파일을 정리하는 역할”을 한다고 이해하면 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/b97b0cee-3a2d-4525-b2bf-6795d757f589.png" alt="" style="display:block;margin:0 auto" />

<h3>Hello World 컴파일 3: 모듈 컴파일 결과 확인</h3>
<h3>Hello World Compilation 3: Checking the Module Build Result</h3>
<p><strong>Makefile</strong> 을 만든 뒤 <code>make</code> 명령을 실행하면, Makefile에 작성된 내용이 실행되면서 화면에 컴파일 과정이 출력된다. 예시 화면에서는 현재 커널 버전에 맞는 빌드 디렉터리인 <code>/lib/modules/$(uname -r)/build</code>로 들어가서, 현재 디렉터리에 있는 <code>hello.c</code> 파일을 커널 모듈로 컴파일하는 과정이 표시된다.</p>
<p>출력 내용을 보면 <code>CC [M] hello.o</code>처럼 C 파일을 오브젝트 파일로 컴파일하는 단계가 나오고, 이후 <code>MODPOST</code>, <code>hello.mod.o</code>, <code>hello.ko</code> 생성 과정이 이어진다. 여기서 최종적으로 중요한 결과물은 <code>hello.ko</code> 이다. <code>.ko</code>는 <strong>Kernel Object</strong> 를 의미하며, 리눅스 커널에 적재할 수 있는 모듈 파일이다.</p>
<p>화면에 나오는 <code>warning: the compiler differs from the one used to build the kernel</code> 메시지는 현재 모듈을 컴파일하는 데 사용한 컴파일러와 커널을 빌드할 때 사용된 컴파일러가 다르다는 뜻이다. 예시에서는 커널은 <code>aarch64-linux-gnu-gcc-13</code>으로 빌드되었고, 현재 모듈은 <code>gcc-13</code>으로 빌드되고 있다. 단순 경고일 수 있지만, 커널 모듈은 실행 중인 커널과 밀접하게 연결되므로 가능하면 커널과 호환되는 환경에서 빌드하는 것이 좋다.</p>
<p>또한 <code>Skipping BTF generation ... due to unavailability of vmlinux</code>라는 메시지도 보인다. 이는 <strong>BTF(BPF Type Format)</strong> 정보를 만들려고 했지만 <code>vmlinux</code> 파일이 없어 생략했다는 의미이다. 일반적인 Hello World 모듈 실습에서는 이 메시지가 나와도 <code>hello.ko</code>가 정상적으로 생성되었다면 모듈 컴파일 자체는 성공한 것으로 볼 수 있다.</p>
<p>즉, 이 단계에서는 <code>make</code>를 실행했을 때 여러 컴파일 과정이 출력되고, 마지막에 에러 없이 끝나며 <code>hello.ko</code> 파일이 생성되었는지를 확인하면 된다. <code>hello.ko</code>가 만들어졌다면 Hello World 커널 모듈 컴파일은 완료된 것이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/c1210594-3af2-4bd0-a919-54ebfe2aed64.png" alt="" style="display:block;margin:0 auto" />

<h3>Hello World 컴파일 결과와 모듈 사용 명령어</h3>
<h3>Hello World Compilation Result and Module Commands</h3>
<p><strong>Hello World 커널 모듈(Hello World Kernel Module)</strong> 을 컴파일하고 나면 여러 개의 중간 파일이 생성된다. 그중 실제로 우리가 사용할 최종 결과물은 <code>hello.ko</code> 파일이다. 여기서 <code>.ko</code>는 <strong>Kernel Object</strong> 를 의미하며, 커널에 적재할 수 있는 모듈 파일이다.</p>
<p>컴파일 과정에서 <code>hello.o</code>, <code>hello.mod.o</code>, <code>Module.symvers</code>, <code>modules.order</code> 같은 파일도 함께 생성될 수 있다. 하지만 이것들은 빌드 과정에서 필요한 중간 결과물이거나 관리용 파일이고, 실제로 커널에 올려 사용할 파일은 최종적으로 생성된 <code>hello.ko</code> 이다.</p>
<p><img src="align=%22center%22" alt="" /></p>
<p>이 모듈을 사용하려면 대표적으로 <code>lsmod</code>, <code>insmod</code>, <code>rmmod</code> 명령을 사용한다. <code>lsmod</code> 는 현재 커널에 적재되어 사용 중인 모듈 목록을 보여준다. <code>insmod</code> 는 커널 모듈을 적재해서 사용할 수 있게 해 주는 명령이고, <code>rmmod</code> 는 적재된 모듈을 제거하는 명령이다.</p>
<p>예를 들어 <code>hello.ko</code> 모듈을 커널에 올리려면 다음과 같이 실행한다.</p>
<pre><code class="language-c">sudo insmod hello.ko
</code></pre>
<p>현재 모듈이 올라갔는지 확인하려면 다음 명령을 사용할 수 있다.</p>
<pre><code class="language-plaintext">lsmod
</code></pre>
<p>사용을 끝낸 뒤 모듈을 제거하려면 다음과 같이 실행한다.</p>
<pre><code class="language-plaintext">sudo rmmod hello
</code></pre>
<p>여기서 주의할 점은 모듈이 사용 중일 때는 함부로 제거할 수 없다는 것이다. USB 메모리를 사용 중일 때 강제로 뽑으면 문제가 생길 수 있어서 “꺼내기(Eject)”를 먼저 하는 것처럼, 커널 모듈도 누군가 사용 중이라면 <code>rmmod</code>가 실패할 수 있다. 이 경우에는 어떤 프로세스나 기능이 해당 모듈을 사용 중인지 확인하고, 사용을 끝낸 뒤 제거해야 한다.</p>
<p>즉, 컴파일의 최종 결과물은 <code>hello.ko</code> 이고, 이 파일을 커널에 올릴 때는 <code>insmod</code>, 현재 적재된 모듈을 확인할 때는 <code>lsmod</code>, 모듈을 제거할 때는 <code>rmmod</code> 를 사용한다고 정리할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/ec041c6f-5365-4846-a09b-ecb88e42e8e4.png" alt="" style="display:block;margin:0 auto" />

<h3>모듈 사용하기: <code>insmod</code>와 <code>dmesg</code></h3>
<h3>Using a Module: <code>insmod</code> and <code>dmesg</code></h3>
<p>컴파일이 끝나서 <code>hello.ko</code> 파일이 생성되었다면, 이제 이 모듈을 커널에 적재해서 사용할 수 있다. 모듈을 적재할 때는 <code>insmod</code> 명령을 사용한다.</p>
<pre><code class="language-plaintext">sudo insmod hello.ko
</code></pre>
<p>이 명령을 실행해도 터미널 화면에는 특별한 출력이 보이지 않을 수 있다. 이유는 Hello World 모듈 안에서 사용한 출력 함수가 일반 C 프로그램의 <code>printf()</code> 가 아니라 커널용 출력 함수인 <code>printk()</code> 이기 때문이다. <code>printk()</code>는 표준 출력(Standard Output)으로 바로 보여주는 것이 아니라 <strong>커널 로그(Kernel Log)</strong> 에 메시지를 남긴다.</p>
<p>따라서 모듈에서 출력한 내용을 확인하려면 <code>dmesg</code> 명령을 사용한다.</p>
<pre><code class="language-c">sudo dmesg
</code></pre>
<p><code>dmesg</code>를 실행하면 커널 로그에 기록된 내용들이 출력된다. 여기에는 시스템이 부팅될 때 출력된 여러 커널 메시지도 포함되어 있고, 방금 <code>hello.ko</code> 모듈을 적재하면서 <code>printk()</code>로 남긴 <strong>hello world</strong> 메시지도 확인할 수 있다.</p>
<p>부팅 과정에서 화면에 여러 메시지가 지나가는 것도 같은 맥락으로 볼 수 있다. 부팅이 완전히 끝나기 전에는 아직 일반적인 <strong>User Mode</strong> 환경으로 제어권이 완전히 넘어온 상태가 아니기 때문에, 커널이 직접 출력하거나 기록하는 메시지들이 나타난다. 반면 이미 부팅이 끝난 뒤 모듈을 적재하면 <code>printk()</code> 메시지는 터미널 표준 출력이 아니라 커널 로그에 남기 때문에 <code>dmesg</code>로 확인해야 한다.</p>
<p>이미지에 보이는 것처럼 <code>hello: loading out-of-tree module taints kernel</code>, <code>module verification failed</code> 같은 메시지도 함께 나올 수 있다. 이는 직접 만든 외부 모듈을 적재했기 때문에 커널이 이를 기록하는 것이다. 중요한 것은 마지막에 <code>hello, world</code>와 같은 메시지가 보인다면 모듈이 정상적으로 적재되고 <code>printk()</code>가 실행되었다고 볼 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/1fd9860c-56c0-4e68-b151-655cacd191c2.png" alt="" style="display:block;margin:0 auto" />

<h3>사용 중인 모듈 확인하기 Checking Loaded Modules</h3>
<p>모듈을 커널에 적재한 뒤에는 <code>lsmod</code> 명령으로 현재 사용 중인 모듈 목록을 확인할 수 있다. 앞에서 만든 모듈 이름이 <code>hello</code>였기 때문에, <code>lsmod</code> 결과에서도 <code>hello</code>라는 이름의 모듈이 표시된다.</p>
<p><code>lsmod</code>를 실행하면 보통 다음과 같은 항목들이 나온다.</p>
<pre><code class="language-plaintext">Module    Size    Used by
hello     12288   0
</code></pre>
<p>여기서 <strong>Module</strong> 은 모듈 이름이고, <strong>Size</strong> 는 해당 모듈이 차지하는 크기이다. <strong>Used by</strong> 는 이 모듈을 현재 몇 개의 다른 모듈이나 기능이 사용하고 있는지를 나타낸다.</p>
<p>예를 들어 <code>hello</code> 모듈의 <code>Used by</code> 값이 <code>0</code>이라면, 현재 이 모듈을 다른 모듈이나 기능이 사용하고 있지 않다는 뜻이다. 반대로 어떤 모듈의 <code>Used by</code> 값이 <code>2</code>라면, 두 곳에서 해당 모듈을 사용 중이라는 의미이고, 오른쪽에 어떤 모듈들이 사용하는지도 함께 표시될 수 있다.</p>
<p>이 정보는 모듈을 제거할 때 중요하다. 어떤 모듈이 다른 모듈이나 장치에서 사용 중이라면 <code>rmmod</code> 로 제거가 되지 않을 수 있다. 따라서 모듈을 제거하기 전에는 <code>lsmod</code>로 현재 사용 여부를 확인하는 것이 좋다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/e7aac77f-2f7a-48bd-aa59-748fd57e1d1f.png" alt="" style="display:block;margin:0 auto" />

<h3>모듈 사용 끝내기 Removing a Kernel Module</h3>
<p>사용 중인 <strong>커널 모듈(Kernel Module)</strong> 을 더 이상 사용하지 않을 때는 <code>rmmod</code> 명령을 사용한다. 앞에서 <code>hello.ko</code> 모듈을 <code>insmod</code>로 적재했다면, 제거할 때는 다음과 같이 입력한다.</p>
<pre><code class="language-c">sudo rmmod hello
</code></pre>
<p>여기서 주의할 점은 <code>rmmod</code>를 사용할 때는 파일 이름인 <code>hello.ko</code>가 아니라, 모듈 이름인 <code>hello</code>를 사용한다는 것이다. 이 명령을 실행하면 모듈이 커널에서 제거되고, 모듈 안에 등록된 <code>module_exit(hello_exit)</code> 함수가 실행된다.</p>
<p>예제에서 제거 후 메시지가 보이는 이유는 <code>rmmod</code> 명령 자체가 메시지를 출력한 것이 아니라, 모듈의 종료 함수 안에 작성된 <code>printk()</code> 가 실행되었기 때문이다. 즉, <code>rmmod hello</code>를 실행하면 <code>hello_exit</code> 함수가 호출되고, 그 안의 <code>printk()</code> 문장이 커널 로그에 메시지를 남긴다.</p>
<p>커널 로그를 확인하려면 다시 <code>dmesg</code> 명령을 사용한다.</p>
<pre><code class="language-plaintext">sudo dmesg
</code></pre>
<p><code>dmesg</code> 결과를 보면 메시지 앞에 대괄호 <code>[]</code> 안의 숫자가 붙어 있다. 이 숫자는 사람이 읽기 쉬운 날짜와 시간이 아니라, 시스템이 부팅된 이후 경과한 시간을 나타내는 값이다. 예를 들어 <code>[30624.71020]</code>처럼 표시된다면, 부팅 후 약 30624초가 지난 시점에 해당 메시지가 기록되었다고 볼 수 있다.</p>
<p>사람에게는 “몇 월 며칠 몇 시 몇 분”처럼 달력 기반 시간이 편하지만, 컴퓨터 입장에서는 부팅 시점을 기준으로 시간이 얼마나 지났는지를 숫자로 기록하는 방식이 더 단순하다. 그래서 커널 로그에서는 부팅 이후 흐른 시간을 기준으로 메시지 발생 시점을 표시한다.</p>
<hr />
]]></content:encoded></item><item><title><![CDATA[Memory Management in Operating Systems: Address Structure and Allocation Methods]]></title><description><![CDATA[1️⃣ Overview of Memory Management and Address Structure2️⃣ Contiguous memory allocation method3️⃣ Non-contiguous memory allocation method

1️⃣ Overview of Memory Management and Address Structure
메모리 관]]></description><link>https://heesu.tech/memory-management-in-operating-systems-address-structure-and-allocation-methods</link><guid isPermaLink="true">https://heesu.tech/memory-management-in-operating-systems-address-structure-and-allocation-methods</guid><dc:creator><![CDATA[Heesu Noh]]></dc:creator><pubDate>Fri, 15 May 2026 12:51:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/5c72b524-f933-4c55-a8b4-d0d5a01a2886.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>1️⃣ Overview of Memory Management and Address Structure<br />2️⃣ Contiguous memory allocation method<br />3️⃣ Non-contiguous memory allocation method</p>
<hr />
<h1>1️⃣ Overview of Memory Management and Address Structure</h1>
<h3>메모리 관리 (Memory Management)</h3>
<p>이번 주차에는 메모리 관리(Memory Management)에 대해 알아본다. 프로세스(Process)가 실행되기 위해서는 반드시 메모리(Memory)가 필요하다. 여러 프로세스가 동시에 실행될 때는 이 메모리를 효율적으로 나누고 보호하는 것이 중요하다. 따라서 이번 시간에는 운영체제(Operating System)가 메모리를 어떻게 관리하는지 공부한다.</p>
<p>이번 주차의 목표는 먼저 메모리 관리의 필요성과 메모리가 수행하는 역할을 이해하는 것이다. 또한 논리 주소(Logical Address)와 물리 주소(Physical Address)의 차이를 설명할 수 있어야 한다.</p>
<p>이어서 연속 메모리 할당 방식(Contiguous Memory Allocation)의 구조를 이해하고, 분할 방식(Partitioning)의 특징을 살펴본다. 또한 메모리 배치 전략(Memory Placement Strategy)과 단편화(Fragmentation) 문제가 무엇인지 설명할 수 있어야 한다.</p>
<p>마지막으로 비연속 메모리 할당 방식(Non-Contiguous Memory Allocation)의 개념을 이해한다. 특히 페이징(Paging)과 세그멘테이션(Segmentation)의 구조, 그리고 주소 변환(Address Translation) 과정이 어떻게 이루어지는지 살펴본다.</p>
<p>즉, 이번 주차에서는 메모리의 기본 개념을 이해하고, 다양한 메모리 할당 방식을 학습한 뒤, 주소 변환이 이루어지는 과정까지 차례대로 살펴볼 예정이다.</p>
<hr />
<h3>❓메모리 관리의 필요성 (Need for Memory Management)</h3>
<p>메모리 관리(Memory Management)의 필요성에 대해 생각해보자. 한 컴퓨터에서 여러 프로그램이 동시에 실행된다고 가정하면, 각 프로그램이 실행되기 위해서는 메모리에 적재되어야 한다.</p>
<p>그런데 운영체제(Operating System)가 메모리를 따로 관리하지 않고 프로그램들이 자유롭게 사용한다면 어떤 일이 발생할까? 서로 다른 프로그램이 같은 메모리 영역(Memory Area)을 사용하게 되면 데이터(Data)가 덮어써지거나 다른 프로그램의 영역을 침범하는 문제가 발생할 수 있다.</p>
<p>이로 인해 프로그램이 비정상적으로 동작하거나, 심한 경우에는 시스템 전체에 오류가 발생할 수 있다. 즉, 이 질문은 메모리를 안전하게 보호하고 관리해야 할 필요성을 이해하게 해주는 질문이다.</p>
<p>❓메모리 주소와 주소 연결 (Memory Address and Address Mapping)</p>
<p>다음 질문을 살펴보자. 프로그램이 실행될 때는 메모리 주소(Memory Address)를 이용해서 데이터를 읽고 저장한다. 그렇다면 프로그램이 사용하는 주소가 실제 메모리의 물리적인 위치와 항상 동일할까?</p>
<p>여러 프로그램이 동시에 실행되는 환경에서는 각 프로그램이 사용하는 주소를 그대로 사용할 수 없다. 따라서 <code>물리 주소(Physical Address)</code>와 <code>논리 주소(Logical Address)</code>를 구분할 필요가 있으며, 이 둘을 연결해주는 과정이 필요하다.</p>
<p>운영체제(Operating System)는 프로그램이 사용하는 주소를 실제 메모리의 위치와 연결해주어야 한다. 즉, 프로그램이 바라보는 주소와 실제 메모리상의 위치가 어떻게 대응되는지를 관리해야 한다.</p>
<p>정리하면, 첫 번째 질문은 “왜 메모리 관리(Memory Management)가 필요한가?”에 대한 질문이다. 두 번째 질문은 “주소를 어떻게 연결할 것인가?”에 대한 문제이다. 오늘은 이 두 가지를 중심으로 공부할 예정이다.</p>
<hr />
<h3>메모리 관리의 복잡성과 이중성 (Complexity and Duality of Memory Management)</h3>
<p>이번 차시에서는 메모리 관리(Memory Management)의 필요성과 기본 개념을 이해하기 위해, 먼저 메모리가 수행하는 역할과 주소 구조(Address Structure)에 대해 살펴본다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/5e1e96cc-e34d-4058-84c6-9ab5b2bb0f6c.png" alt="" style="display:block;margin:0 auto" />

<p>초기 일괄 처리 시스템(Batch Processing System)을 떠올려보면, 당시에는 한 번에 하나의 작업(Job)만 실행되는 구조였다. 따라서 하나의 프로그램(Program)만 메모리에 올라갔고, 다른 프로그램과 메모리 영역이 겹칠 일이 없었다. 이 때문에 메모리 관리는 비교적 어렵지 않았다.</p>
<p>하지만 현재의 시스템에서는 여러 프로그램이 동시에 실행되며, 이 프로그램들이 함께 메모리에 올라오게 된다. 이렇게 되면 하나의 메모리를 여러 프로그램이 공유하게 되고, 서로 간섭하지 않도록 관리해야 하는 문제가 생긴다. 결국 여러 프로그램이 하나의 메모리를 함께 사용하게 되면서 메모리 관리는 점점 더 복잡해진다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/38b7d6fe-20ae-4edb-8a03-29c502aa2cf2.png" alt="" style="display:block;margin:0 auto" />

<p>메모리 관리에는 <code>이중성(Duality)</code>도 존재한다. 각 프로세스(Process)의 입장에서 보면, 자신에게 할당된 메모리를 독립적으로 사용하고 싶어 한다. 즉, 다른 프로세스의 영향을 받지 않고 자기 영역만 안전하게 사용하기를 원한다.</p>
<p>반면 운영체제(Operating System)의 입장에서는 하나의 메모리를 여러 프로세스가 사용할 수 있도록 효율적으로 관리해야 한다. 이 과정에서 각 프로세스는 자신의 메모리가 침범되지 않도록 보호(Protection)를 요구하게 된다.</p>
<p>결국 운영체제는 메모리를 효율적으로 활용해야 하는 동시에, 각 프로세스의 메모리 보호도 함께 보장해야 한다. 메모리 관리는 이러한 두 가지 요구를 동시에 만족시켜야 하는 중요한 역할을 수행한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/de2d15f0-e4bf-4dc0-b8d4-bb276256c051.png" alt="" style="display:block;margin:0 auto" />

<h3>메모리 관리의 이중성 (Duality of Memory Management)</h3>
<p>메모리 관리(Memory Management)의 이중성은 메모리를 바라보는 관점이 서로 다르다는 점에서 이해할 수 있다.</p>
<p>먼저 프로세스(Process) 입장에서의 메모리(Memory)는 자신만의 메모리 공간이 따로 존재하는 것처럼 보인다. 각 프로세스는 다른 프로세스와 메모리를 전혀 공유하지 않고, 독립적으로 사용하는 것처럼 인식한다.</p>
<p>반면 메모리 관리자(Memory Manager) 입장에서의 메모리는 여러 프로세스가 나누어 사용하는 구조이다. 운영체제(Operating System)는 메모리를 구분해서 각 프로세스에 할당하고, 필요에 따라 재배치하며 관리한다.</p>
<p>결국 메모리 관리의 핵심은 프로세스에게는 메모리가 독립적으로 보이게 하면서, 실제로는 하나의 메모리를 여러 프로세스가 공유하도록 만드는 것이다.</p>
<hr />
<h3>메모리 관리 작업 (Memory Management Operations)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/49dbffd9-9222-4c02-8894-4944fb76803e.png" alt="" style="display:block;margin:0 auto" />

<p>메모리 관리(Memory Management)에서 실제로 어떤 작업이 이루어지는지 살펴보자.</p>
<p>먼저 실행할 프로그램과 데이터를 메모리로 가져오는 과정이 필요하다. 그리고 메모리에 올라온 프로세스를 어느 위치에 배치할 것인지 결정해야 한다. 또한 메모리가 부족해지면 어떤 프로세스를 내보낼지도 결정해야 한다.</p>
<p>결국 메모리 관리는 <code>가져오기(Fetch), 배치(Placement), 재배치(Replacement)</code>의 과정으로 이루어지며, 이 세 가지를 중심으로 동작한다고 볼 수 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/1ac6e266-1ec2-4d99-bdc7-caa612a7139a.png" alt="" style="display:block;margin:0 auto" />

<p>그림을 보면 메모리 관리의 흐름을 한눈에 확인할 수 있다. 먼저 실행할 프로세스는 하드 디스크(Hard Disk)와 같은 보조기억장치(Secondary Storage)에 저장되어 있다가, 필요할 때 메모리로 가져오게 된다. 이렇게 메모리로 가져오는 과정을 <code>가져오기(Fetch)</code>라고 한다.</p>
<p>그다음 메모리로 올라온 프로세스를 어느 위치에 둘 것인지 결정하는 과정을 <code>배치(Placement)</code>라고 한다. 만약 메모리가 부족한 경우에는 이미 메모리에 올라와 있는 프로세스 중 일부를 내보내고, 새로운 프로세스를 넣어야 한다. 이러한 과정을 <code>재배치(Replacement)</code>라고 한다.</p>
<p>이처럼 메모리 관리는 <code>가져오기(Fetch), 배치(Placement), 재배치(Replacement)</code>의 흐름 속에서 이루어진다고 볼 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/5ad2f125-ea8e-48f1-bd6e-01ef5a15020e.png" alt="" style="display:block;margin:0 auto" />

<h3>메모리 주소와 주소 공간 (Memory Address and Address Space)</h3>
<p>메모리 주소(Memory Address)와 주소 공간(Address Space)의 개념을 조금 더 구체적으로 살펴보자.</p>
<p>메모리(Memory)는 하나의 연속된 주소 공간으로 이루어져 있다. 각 메모리 위치는 서로 다른 고유한 주소(Address)를 가지며, CPU는 이 주소를 이용해 데이터를 읽거나 명령어(Instruction)를 가져온다.</p>
<p>즉, 프로그램(Program)이 실행되는 과정에서는 특정 위치를 직접 지정하기보다, 주소를 기준으로 메모리에 접근한다고 볼 수 있다.</p>
<p>따라서 메모리 주소는 데이터(Data)가 어디에 있는지 구분하고, 해당 위치에 정확하게 접근하기 위한 기준으로 이해할 수 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f171f6c0-8b98-42de-b133-1b1a059d54c6.png" alt="" style="display:block;margin:0 auto" />

<h3>주소 공간 (Address Space)</h3>
<p>프로그램(Program)이 실행될 때 각 프로그램은 자신이 사용할 수 있는 메모리 범위를 가진다. 이 범위 안에서 명령어(Instruction)와 데이터(Data)를 저장하고 접근하게 된다.</p>
<p>여기서 중요한 점은 각 프로세스(Process)가 서로 독립적인 주소 공간(Address Space)을 가진다는 것이다. 즉, 한 프로세스가 사용하는 주소와 다른 프로세스가 사용하는 주소는 서로 분리되어 있다.</p>
<p>이렇게 주소 공간이 분리되어 있기 때문에 여러 프로그램이 동시에 실행되더라도 서로 간섭하지 않고 동작할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f9f84d54-01f8-4082-afa0-fb45f69c4c95.png" alt="" style="display:block;margin:0 auto" />

<h3>논리 주소와 물리 주소 (Logical Address and Physical Address)</h3>
<p>앞에서 각 프로세스(Process)가 독립적인 주소 공간(Address Space)을 가진다고 했다. 그렇다면 여기서 한 가지 의문이 생긴다. 프로그램(Program)이 사용하는 주소와 실제 메모리(Memory)의 위치는 과연 같을까?</p>
<p>먼저 프로그램이 사용하는 주소를 <code>논리 주소(Logical Address)</code>라고 한다. 이 주소는 프로그램이 실행되는 과정에서 CPU가 생성하며, 프로그램이 직접 사용하는 주소이다. 즉, 프로그램의 입장에서 사용하는 주소라고 이해할 수 있다.</p>
<p>반면 실제 메모리에 데이터(Data)가 저장되는 물리적인 위치도 따로 존재한다. 이를 <code>물리 주소(Physical Address)</code>라고 한다. 물리 주소는 실제 메모리 하드웨어(Memory Hardware)에서 사용하는 주소이며, 운영체제(Operating System)가 관리하는 대상이라고 볼 수 있다.</p>
<p>결국 프로그램이 사용하는 논리 주소와 실제 메모리의 물리 주소는 <strong>서로 다르다</strong>. 따라서 이 두 주소를 구분해서 이해하는 것이 중요하다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/5d43121f-cbd4-4316-9589-964b3afe7a25.png" alt="" style="display:block;margin:0 auto" />

<p>논리 주소와 물리 주소의 차이는 그림을 통해 직관적으로 이해할 수 있다. 왼쪽을 보면 각 프로세스가 자신만의 주소 공간을 독립적으로 가지고 있다. 그래서 각 프로세스는 자신의 주소가 연속적으로 구성되어 있는 것처럼 인식한다.</p>
<p>하지만 실제 물리 메모리(Physical Memory)를 보면, 하나의 메모리 공간 안에 여러 프로세스가 섞여 배치되어 있다. 실제 메모리에서는 각 프로세스의 데이터가 반드시 처음부터 끝까지 연속적으로 존재하지 않을 수도 있다.</p>
<p>이때 중요한 점은 논리 주소와 물리 주소가 반드시 1:1로 대응되는 것이 아니라, 서로 다른 위치로 변환되어 연결된다는 것이다. 결국 프로세스 입장에서는 연속된 주소 공간을 사용하는 것처럼 보이지만, 실제로는 운영체제가 주소를 적절히 변환하여 물리 메모리에 배치하고 있다고 이해하면 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/523325af-3e36-4552-bac8-4954735075db.png" alt="" style="display:block;margin:0 auto" />

<h3>주소 변환의 필요성 (Need for Address Translation)</h3>
<p>앞서 본 그림처럼 프로그램(Program)은 논리 주소(Logical Address)를 기준으로 메모리(Memory)에 접근하지만, 실제 메모리는 물리 주소(Physical Address)를 기준으로 동작한다.</p>
<p>즉, 프로그램이 사용하는 주소와 실제 메모리가 사용하는 주소가 서로 다르기 때문에, <strong>이 둘을 연결해주는 과정이 필요하다</strong>. 이 과정을 주소 변환(Address Translation)이라고 볼 수 있다.</p>
<p>또한 중요한 문제는 이 주소를 언제 실제 물리적 위치와 연결할 것인지이다. 논리 주소와 물리 주소를 연결하는 것뿐만 아니라, 그 연결이 이루어지는 시점도 함께 결정해야 한다.</p>
<p>결국 주소 변환에서 중요한 핵심은 논리 주소와 물리 주소를 연결하는 것, 그리고 그 연결 시점을 결정하는 것이라고 할 수 있다.</p>
<hr />
<h3>주소 바인딩 방식 (Address Binding Methods)</h3>
<p>다음은 주소를 실제 메모리 위치에 언제 연결할 것인지에 따라 주소 바인딩(Address Binding) 방식을 나누어 살펴본다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/75db0d11-7440-4184-93e2-98e8012bf9ea.png" alt="" style="display:block;margin:0 auto" />

<p>주소 바인딩은 크게 세 가지로 구분된다. 첫 번째는 컴파일 시간 바인딩(Compile-Time Binding)이다. 이는 프로그램을 컴파일(Compile)하는 시점에 이미 물리 주소(Physical Address)가 결정되는 방식이다.</p>
<p>두 번째는 적재 시간 바인딩(Load-Time Binding)이다. 이는 프로그램이 메모리(Memory)에 적재될 때 주소가 결정되는 방식이다.</p>
<p>세 번째는 실행 시간 바인딩(Execution-Time Binding)이다. 이는 프로그램이 실행되는 동안 주소 변환(Address Translation)이 이루어지는 방식이다. 이처럼 주소를 연결하는 시점에 따라 각기 다른 바인딩 방식이 존재한다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/edc6d8a8-dbb1-43f4-8430-837516f2acf7.png" alt="" style="display:block;margin:0 auto" />

<p>그림을 보면 주소 바인딩이 실제로 어떻게 이루어지는지 이해할 수 있다. 먼저 컴파일 단계(Compile Stage)에서는 원시 코드(Source Code)가 컴파일러(Compiler)를 거쳐 목적 파일(Object File)로 변환된다. 이 단계에서는 프로그램이 사용할 주소가 어느 정도 결정되는 과정이 이루어진다.</p>
<p>다음 적재 단계(Load Stage)에서는 여러 라이브러리(Library)와 목적 파일(Object File)이 링커(Linker)를 통해 하나로 연결된다. 이후 로더(Loader)에 의해 메모리에 적재되며, 이 과정에서 실제 메모리에 올라갈 위치가 결정된다.</p>
<p>마지막 실행 단계(Execution Stage)에서는 프로그램이 메모리에 올라간 이후에도 필요에 따라 주소 변환이 계속 이루어진다. 즉, 실행 중에도 주소가 동적으로 연결되는 방식이라고 볼 수 있다.</p>
<p>이처럼 주소 바인딩은 컴파일(Compile), 적재(Load), 실행(Execution)의 흐름 속에서 단계적으로 이루어진다고 이해할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/bc8a3007-cf4a-4609-af72-96044a60a9dd.png" alt="" style="display:block;margin:0 auto" />

<h3>실행 시간 주소 바인딩 (Execution-Time Address Binding)</h3>
<p>주소 바인딩(Address Binding)은 컴파일 시점(Compile Time), 적재 시점(Load Time), 실행 시점(Execution Time)으로 나누어 이루어진다. 이 중에서 특히 중요한 것이 실행 시간 바인딩(Execution-Time Binding)이다.</p>
<p>앞에서 프로그램(Program)이 실행되는 동안에도 주소 변환(Address Translation)이 계속 이루어진다고 했다. 실행 시간 바인딩 방식에서는 프로그램이 메모리(Memory)의 고정된 위치에 묶여 있지 않아도 실행될 수 있다. 즉, 메모리의 어느 위치에 올라가더라도 문제없이 동작할 수 있다.</p>
<p>또한 실행 중에도 필요에 따라 프로세스(Process)의 위치를 다른 곳으로 이동시킬 수 있다. 따라서 실행 시간 바인딩은 메모리를 보다 유연하게 사용할 수 있도록 해주는 방식이라고 볼 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/9d851ad3-9d53-4e40-af2b-7feaa7c6a91a.png" alt="" style="display:block;margin:0 auto" />

<h3>MMU (Memory Management Unit)</h3>
<p>지금까지 주소 바인딩(Address Binding)이 이루어지는 과정을 살펴보았다. 이제 이렇게 결정된 주소가 실제 실행 과정에서 어떻게 변환되는지 생각해볼 필요가 있다. 이 역할을 수행하는 장치가 바로 <code>MMU(Memory Management Unit)</code>이다.</p>
<p>MMU는 논리 주소(Logical Address)를 물리 주소(Physical Address)로 변환해주는 하드웨어 장치이다. CPU가 생성한 논리 주소를 입력으로 받아, 실제 메모리(Memory)에서 사용되는 물리 주소로 바꿔준다.</p>
<p>이 장치는 CPU 내부 또는 CPU 패키지(CPU Package)에 포함되어 있으며, 프로그램(Program)이 메모리를 사용할 때마다 주소 변환(Address Translation)을 담당한다.</p>
<p>따라서 MMU는 논리 주소를 물리 주소로 변환해주면서, 각 프로세스(Process)가 독립적인 메모리를 사용하는 것처럼 보이도록 만들어주는 역할을 한다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/3e4ac31d-c4a6-4dd9-9f30-49e72a1dcff0.png" alt="" style="display:block;margin:0 auto" />

<h3>MMU의 역할 (Role of MMU)</h3>
<p>이 그림은 MMU(Memory Management Unit)가 어떤 역할을 하는지 보여준다. 프로세스(Process)에서 생성된 논리 주소(Logical Address)가 MMU로 전달되면, MMU는 이 주소를 변환하여 물리 주소(Physical Address)로 바꿔준다.</p>
<p>그다음 변환된 물리 주소를 기준으로 실제 메모리(Memory)에 대한 접근이 이루어진다. 즉, 프로세스가 사용하는 논리 주소가 바로 메모리에 접근하는 것이 아니라, MMU를 거쳐 물리 주소로 변환된 뒤 실제 메모리에 접근하게 된다.</p>
<p>이 과정에서 MMU는 고정 분할(Fixed Partitioning), 동적 분할(Dynamic Partitioning), 페이징(Paging), 세그멘테이션(Segmentation)과 같은 다양한 메모리 관리 기법(Memory Management Techniques)을 기반으로 주소 변환(Address Translation)을 수행한다.</p>
<p>결국 중요한 점은 이러한 기법들이 서로 다른 방식으로 구현될 뿐, MMU가 수행하는 핵심 역할은 논리 주소(Logical Address)를 물리 주소(Physical Address)로 변환하는 것이라는 점이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/181464c1-15ca-4516-9819-5ba70c79e128.png" alt="" style="display:block;margin:0 auto" />

<h3>메모리 보호의 필요성 (Need for Memory Protection)</h3>
<p>여러 프로세스(Process)가 하나의 물리 메모리(Physical Memory)를 함께 사용하는 환경을 떠올려보자. 이때 잘못된 주소(Address)로 접근하게 되면 다른 프로세스의 데이터(Data)를 침범할 수 있다.</p>
<p>또한 사용자 프로그램(User Program)이 운영체제(Operating System)의 영역까지 접근하게 되면, 시스템(System) 자체가 손상될 위험도 존재한다. 이러한 오류는 단순히 하나의 프로그램에서 끝나는 것이 아니라, 시스템 전체의 오류로 확산될 수도 있다.</p>
<p>따라서 운영체제는 이러한 문제를 방지하기 위해 메모리 보호(Memory Protection) 기능을 반드시 제공해야 한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/4991d0b7-0ec2-401e-9eb9-3ab46b35a9df.png" alt="" style="display:block;margin:0 auto" />

<h3>메모리 보호와 기준 레지스터 (Memory Protection and Base Register)</h3>
<p>메모리 보호(Memory Protection)를 구현하기 위한 방법 중 하나가 기준 레지스터(Base Register)이다. 기준 레지스터는 베이스 레지스터(Base Register)라고도 하며, 프로세스(Process)가 사용할 수 있는 메모리(Memory)의 시작 주소를 저장하는 역할을 한다.</p>
<p>프로그램(Program)이 사용하는 논리 주소(Logical Address)에 기준 레지스터의 값, 즉 베이스 값(Base Value)을 더하면 물리 주소(Physical Address)가 만들어진다. 각 프로세스마다 서로 다른 베이스 값을 가지도록 설정하기 때문에, 각 프로세스는 서로 다른 메모리 영역(Memory Area)을 사용하게 된다.</p>
<p>결국 기준 레지스터는 논리 주소를 물리 주소로 변환할 때 기준이 되는 값을 제공하는 역할을 한다고 볼 수 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/e069669b-ab3c-4a98-bd59-aaf4a677330b.png" alt="" style="display:block;margin:0 auto" />

<p>그림을 통해 기준 레지스터를 이용한 주소 변환(Address Translation) 과정을 확인할 수 있다. 예를 들어 프로세스에서 생성된 논리 주소가 346이고, 기준 레지스터에 저장된 값이 1400이라면 두 값을 더해 1746이라는 물리 주소가 만들어진다.</p>
<p>이렇게 생성된 물리 주소를 기준으로 실제 메모리에 접근하게 된다. 이처럼 논리 주소에 기준값을 더해 물리 주소를 생성하는 방식이 기준 레지스터 방식이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/82086dec-be0b-4068-9577-1ffaeae1b221.png" alt="" style="display:block;margin:0 auto" />

<h3>경계 레지스터를 이용한 메모리 보호 (Memory Protection with Limit Register)</h3>
<p>기준 레지스터(Base Register)와 함께 사용되는 것이 경계 레지스터(Limit Register)이다. 경계 레지스터는 프로세스(Process)가 사용할 수 있는 메모리(Memory)의 크기, 즉 허용된 범위(Limit)를 저장한다.</p>
<p>이때 CPU가 생성한 논리 주소(Logical Address)가 허용된 범위를 초과하는지 MMU(Memory Management Unit)와 같은 하드웨어가 검사한다. 만약 프로세스가 허용된 범위를 벗어난 주소로 접근하려고 하면, 해당 접근은 차단된다.</p>
<p>이렇게 해서 프로세스가 자신에게 할당된 메모리 영역(Memory Area)을 넘어가지 못하도록 보호할 수 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/418ca594-aadb-4752-99ab-8f92b47ec1d4.png" alt="" style="display:block;margin:0 auto" />

<p>그림을 통해 경계 레지스터가 어떻게 메모리를 보호하는지 살펴볼 수 있다. 경계 레지스터에는 프로세스가 접근할 수 있는 메모리의 범위가 저장되어 있다. 그러면 CPU가 생성한 논리 주소가 이 범위를 벗어나는지 MMU가 검사하게 된다.</p>
<p>그림에서 운영체제 영역(Operating System Area)은 상위 주소 영역에 위치하고, 사용자 프로그램 영역(User Program Area)은 아래쪽 영역만 사용하도록 제한되어 있다. 따라서 프로그램이 허용된 범위를 벗어나 운영체제 영역에 접근하려고 하면, 해당 접근은 차단된다.</p>
<p>즉, 경계 레지스터는 사용자 프로그램(User Program)이 운영체제 영역을 침범하지 못하도록 보호하는 역할을 한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/dd156035-627d-47e4-b6a7-800cd9848fd3.png" alt="" style="display:block;margin:0 auto" />

<h3>메모리 보호 동작 과정 (Memory Protection Operation)</h3>
<p>메모리 보호(Memory Protection)가 실제로 동작하는 흐름을 살펴보자.</p>
<p>먼저 CPU가 생성한 논리 주소(Logical Address)가 MMU(Memory Management Unit)로 전달된다. MMU는 이 논리 주소가 허용된 범위, 즉 경계값(Limit Value) 안에 있는지 검사한다.</p>
<p>만약 논리 주소가 허용된 범위 안에 있다면, 기준값(Base Value)을 더해 물리 주소(Physical Address)를 생성한다. 반대로 논리 주소가 허용된 범위를 초과하면 예외(Exception)가 발생하고, 이때 운영체제(Operating System)가 개입하게 된다.</p>
<p>즉, 모든 메모리 접근 과정에서는 하드웨어 수준에서 보호 검사(Protection Check)가 수행된다고 이해할 수 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/e57add8b-ad7b-47a1-a121-c6c14a6cbddf.png" alt="" style="display:block;margin:0 auto" />

<p>그림을 통해 메모리 보호가 실제로 어떻게 이루어지는지 구체적으로 살펴보자. 먼저 각 프로세스(Process)마다 기준 레지스터(Base Register)와 경계 레지스터(Limit Register) 값이 설정되어 있다. 기준 레지스터는 시작 주소(Start Address)를 의미하고, 경계 레지스터는 사용할 수 있는 범위(Limit)를 의미한다.</p>
<p>이를 바탕으로 실제 동작 순서를 보면, 프로세스가 생성한 논리 주소가 먼저 허용된 범위 안에 있는지 검사된다. 즉, 이 주소가 경계 레지스터 값보다 작은지를 MMU가 먼저 확인한다.</p>
<p>이 조건을 만족하는 경우에만 그다음 기준 레지스터 값을 더해 물리 주소를 생성한다. 따라서 메모리 보호 과정은 먼저 범위를 검사하고, 그다음 주소 변환(Address Translation)을 수행하는 순서로 이루어진다고 볼 수 있다.</p>
<p>반대로 논리 주소가 허용된 범위를 벗어나면 트랩(Trap)이 발생한다. 이 경우 주소 지정 오류(Addressing Error)로 처리되며, 운영체제가 개입하게 된다.</p>
<p>이처럼 기준 레지스터와 경계 레지스터를 이용하면, 각 프로세스가 자신에게 허용된 범위 안에서만 메모리를 사용하도록 제어할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/153f3aae-d546-48af-8dd1-c1626b08531c.png" alt="" style="display:block;margin:0 auto" />

<h3>메모리 보호 예제 (Memory Protection Example)</h3>
<p>하나의 예제를 통해 메모리 보호(Memory Protection)가 어떻게 동작하는지 확인해보자.</p>
<p>어떤 프로세스(Process)의 기준값(Base Value)이 1000이고, 경계값(Limit Value)이 500이라고 가정한다. 여기서 중요한 점은 경계값이 마지막 주소값이 아니라, 프로세스가 사용할 수 있는 메모리의 크기(Memory Size)를 의미한다는 것이다.</p>
<p>따라서 해당 프로세스가 실제로 접근할 수 있는 물리 주소(Physical Address)의 범위는 1000부터 1499까지가 된다.</p>
<p>이때 논리 주소(Logical Address)가 200이라면, 이 값은 경계값의 범위 안에 있다. 따라서 기준 레지스터(Base Register)의 값이 더해져 물리 주소 1200으로 변환되고, 정상적으로 메모리에 접근할 수 있다.</p>
<p>반대로 논리 주소가 600이라면, 이 값은 경계값을 초과한다. 따라서 접근이 차단되고 예외(Exception)가 발생한다. 이 경우 프로그램(Program)이 강제로 종료되거나, 운영체제(Operating System)가 개입하여 처리하게 된다.</p>
<p>지금까지 메모리 관리(Memory Management)의 기본 개념과 주소 변환(Address Translation), 그리고 메모리 보호 방법까지 살펴보았다. 다음에는 이러한 개념을 바탕으로 실제 메모리 할당 방식(Memory Allocation Methods)에 대해 살펴볼 예정이다.</p>
<hr />
<h1>2️⃣ Contiguous memory allocation method</h1>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/52099eaf-740b-494a-b7b0-9c24bf5d95ee.png" alt="" style="display:block;margin:0 auto" />

<h3>연속 메모리 할당 방식 (Contiguous Memory Allocation)</h3>
<p>이번에는 메모리(Memory)를 실제로 어떤 방식으로 나누어 사용하는지 살펴보기 위해, 연속 메모리 할당 방식(Contiguous Memory Allocation)에 대해 알아본다.</p>
<p>앞에서 프로세스(Process)가 실행되기 위해서는 반드시 메모리에 적재되어야 한다고 배웠다. 메모리를 할당하는 방식은 크게 두 가지로 나눌 수 있는데, <code>연속 메모리 할당(Contiguous Memory Allocation)</code>과 <code>불연속 메모리 할당(Non-Contiguous Memory Allocation)</code>이다.</p>
<p>먼저 살펴볼 연속 메모리 할당 방식은 프로세스를 하나의 연속된 공간에 적재하는 방식이다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/787cb498-fb2f-4580-aa9d-ec5b38f9e374.png" alt="" style="display:block;margin:0 auto" />

<p>그림을 통해 메모리 할당 방식의 전체 구조를 정리해보자. 메모리 할당 방식은 크게 연속 메모리 할당 방식과 불연속 메모리 할당 방식으로 나뉜다.</p>
<p>연속 메모리 할당 방식은 하나의 프로세스를 메모리의 연속된 공간에 배치하는 방식이다. 반면 불연속 메모리 할당 방식은 하나의 프로세스를 여러 개의 분산된 공간에 나누어 배치하는 방식이다.</p>
<p>이 중에서 지금부터는 연속 메모리 할당 방식에 해당하는 방법들을 살펴본다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/6dba939c-5fd0-4a8b-8109-dd43272ebaea.png" alt="" style="display:block;margin:0 auto" />

<h3>단일 프로그래밍 방식 (Single Programming)</h3>
<p>먼저 가장 단순한 형태인 단일 프로그래밍(Single Programming) 방식에 대해 살펴본다. 단일 프로그래밍 방식은 한 번에 하나의 프로그램(Program)만 메모리(Memory)에 적재해서 실행하는 방식이다. 따라서 메모리는 운영체제(Operating System)와 사용자 프로그램(User Program)이 나누어 사용하는 구조를 가진다.</p>
<p>하지만 이 방식에서는 프로그램이 입출력(I/O) 작업을 수행하는 동안 CPU가 아무 작업도 하지 못하고 대기 상태(Waiting State)가 될 수 있다. 따라서 단일 프로그래밍 구조는 매우 단순하지만, 전체 자원 활용(Resource Utilization) 효율은 낮은 방식이라고 볼 수 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/599b1d4c-9145-44a8-ab84-61b2f8b2de48.png" alt="" style="display:block;margin:0 auto" />

<p>다음으로 단일 프로그램의 메모리 구조(Memory Structure)를 살펴보면, 메모리가 어떻게 나뉘어 사용되는지 확인할 수 있다. 메모리는 크게 <code>운영체제 영역(Operating System Area)</code>과 <code>사용자 영역(User Area)</code>으로 구분된다.</p>
<p>운영체제 영역에는 시스템 코드(System Code)와 핵심 기능이 저장되고, 사용자 영역에는 실제 실행 중인 프로그램이 올라오게 된다. 여기서 중요한 점은 사용자 프로그램이 운영체제 영역에 접근하지 못하도록 보호된다는 것이다.</p>
<p>이러한 구조를 통해 단일 프로그램 환경에서는 하나의 프로그램이 메모리를 독점적으로 사용하면서 안정적으로 실행될 수 있게 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/8ccb4c9d-bdcf-4905-a50a-489fc9a7f63f.png" alt="" style="display:block;margin:0 auto" />

<h3>단일 프로그램의 메모리 적재 과정 (Memory Loading in Single Programming)</h3>
<p>단일 프로그램(Single Program) 환경에서 프로그램(Program)이 메모리(Memory)에 올라오는 과정을 살펴보자.</p>
<p>사용자 프로그램(User Program)은 메모리의 사용자 영역(User Area)에 적재된다. 프로그램은 실행될 때마다 디스크(Disk)와 같은 보조기억장치(Secondary Storage)에서 메모리로 올라오게 된다.</p>
<p>새로운 프로그램을 실행하면 기존에 실행되던 프로그램은 메모리에서 제거되고, 새로운 프로그램이 다시 적재된다. 즉, 단일 프로그램 환경은 한 번에 하나의 프로그램만 메모리를 사용하는 구조라고 볼 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/6fb9ebcf-c41e-44ab-b8c3-d108509c1fb4.png" alt="" style="display:block;margin:0 auto" />

<h3>다중 프로그래밍의 필요성 (Need for Multiprogramming)</h3>
<p>연속 메모리 할당 방식(Contiguous Memory Allocation)에서 다중 프로그래밍(Multiprogramming)이 왜 필요한지 살펴보자.</p>
<p>단일 프로그램 환경(Single Program Environment)에서는 한 번에 하나의 프로그램(Program)만 실행된다. 따라서 프로그램이 입출력(I/O) 작업을 수행하는 동안에는 CPU가 아무 일도 하지 않고 대기하게 된다.</p>
<p>이로 인해 CPU와 메모리(Memory) 자원이 비효율적으로 사용된다. 따라서 여러 프로그램을 동시에 실행하여 **자원 활용률(Resource Utilization)**을 높일 필요가 있다.</p>
<p>이러한 요구를 해결하기 위해 <code>고정 분할 방식(Fixed Partitioning)</code>과 같은 메모리 할당 방식(Memory Allocation Method)이 사용된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/36c21fc0-ea3e-45b0-9b46-68a2fcc489a9.png" alt="" style="display:block;margin:0 auto" />

<h3>고정 분할 방식 (Fixed Partitioning)</h3>
<p>고정 분할 방식(Fixed Partitioning)은 운영체제(Operating System)가 메모리(Memory)를 여러 개의 고정된 크기 영역으로 나누어 관리하는 방식이다.</p>
<p>각 영역에는 하나의 프로그램(Program)만 적재되며, 이를 통해 여러 프로그램이 동시에 실행될 수 있는 구조가 만들어진다. 이때 분할 영역(Partition)의 크기는 미리 고정되어 있기 때문에 실행 중에는 변경되지 않는다.</p>
<p>따라서 고정 분할 방식은 구조가 단순하다는 장점이 있지만, 메모리를 유연하게 활용하기에는 한계가 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/be2bae8c-7ea8-4b44-9f90-23c6a11bd757.png" alt="" style="display:block;margin:0 auto" />

<p>그림을 통해 고정 분할 방식이 어떻게 동작하는지 살펴보자. 운영체제는 메모리를 미리 여러 개의 고정된 영역으로 나누고, 각 영역에 하나의 프로그램을 할당한다.</p>
<p>그림에서 A, B, C 와 같은 각 영역에 프로그램이 하나씩 올라와 있는 것을 볼 수 있다. 각 영역은 독립적으로 사용되기 때문에 여러 프로그램을 동시에 실행할 수 있다.</p>
<p>하지만 각 영역의 크기가 고정되어 있으므로, 프로그램의 크기에 따라 사용되지 않는 남는 공간이 발생할 수 있다는 점도 함께 고려해야 한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/ed269bd1-b9a8-40b4-b06f-3a4b3e81d2d0.png" alt="" style="display:block;margin:0 auto" />

<h3>내부 단편화 (Internal Fragmentation)</h3>
<p>분할 내부에 사용되지 않고 남는 공간을 내부 단편화(Internal Fragmentation)라고 한다. 내부 단편화는 프로세스(Process)의 크기가 분할된 영역(Partition)의 크기보다 작을 때 발생한다.</p>
<p>이때 남은 공간은 다른 프로세스가 사용할 수 없기 때문에, 결과적으로 메모리(Memory) 낭비가 발생하게 된다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/bb0a79f8-d8a3-47f5-9389-76ed60824776.png" alt="" style="display:block;margin:0 auto" />

<p>그림을 통해 내부 단편화가 실제로 어떻게 나타나는지 살펴보자. 오른쪽을 보면 메모리가 모두 20KB 크기의 고정된 영역으로 나뉘어 있다. 각 영역에는 하나의 프로세스가 할당된다.</p>
<p>그런데 왼쪽의 프로세스들을 보면 각각의 크기가 40KB, 18KB, 30KB, 17KB처럼 분할 크기와 정확히 맞지 않는다. 예를 들어 크기가 18KB인 프로세스 B(Process B)는 20KB짜리 하나의 영역에 적재되지만, 2KB의 공간이 남게 된다. 17KB인 프로세스 D(Process D)도 20KB짜리 하나의 영역을 사용하면서 3KB의 공간이 사용되지 않고 남게 된다.</p>
<p>이처럼 고정된 크기의 분할에 맞추어 프로세스를 적재하다 보면, 각 분할 내부에 사용되지 않는 공간이 발생한다. 이 남는 공간을 내부 단편화라고 이해할 수 있다.</p>
<hr />
<h3>가변 분할 방식과 외부 단편화 (Variable Partitioning and External Fragmentation)</h3>
<p>앞서 <code>고정 분할 방식(Fixed Partitioning)</code>에서는 분할 크기(Partition Size)가 고정되어 있기 때문에 <code>내부 단편화(Internal Fragmentation)</code>가 발생한다고 배웠다. 이러한 문제를 해결하기 위한 방식이 <code>가변 분할 방식(Variable Partitioning)</code>이다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/7c952bf9-0235-4429-a4e3-c84e7c9effbd.png" alt="" style="display:block;margin:0 auto" />

<p>가변 분할 방식에서는 프로세스(Process)의 크기에 맞게 메모리(Memory)를 연속된 공간으로 동적으로 할당한다. 즉, 메모리를 미리 고정된 크기로 나누는 것이 아니라, 필요할 때마다 적절한 크기로 나누어 사용한다. 따라서 필요한 만큼만 메모리를 할당할 수 있고, 그만큼 메모리 낭비를 줄일 수 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/4ec7aafb-bf60-496e-b951-ae6f2469aa2b.png" alt="" style="display:block;margin:0 auto" />

<p>가변 분할 방식에서는 프로세스 크기에 맞는 연속된 빈 공간을 찾아 그 위치에 프로세스를 적재한다. 고정된 크기로 나누어놓은 공간을 사용하는 것이 아니라, 필요한 크기만큼 공간을 할당하는 방식이다. 실행 중이던 프로세스가 종료되면 해당 위치에는 다시 빈 공간이 생긴다. 이렇게 메모리 안에 빈 공간이 생성되면, 또 다른 프로세스가 그 공간을 사용할 수 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f4855f65-a320-498f-b6fb-6e12f00f6929.png" alt="" style="display:block;margin:0 auto" />

<p>그림을 통해 가변 분할 방식에서 메모리가 어떻게 변화하는지 살펴보자. 프로세스들은 각자의 크기에 맞게 연속된 공간에 적재된다. 그런데 일부 프로세스가 종료되면 그 위치에는 빈 공간이 생긴다.</p>
<p>이때 빈 공간들이 메모리 중간중간에 끊어진 형태로 남게 된다. 이처럼 연속되지 않는 작은 빈 공간이 여러 개로 나뉘어 발생하는 현상을 <code>외부 단편화(External Fragmentation)</code>라고 한다.</p>
<p>즉, 전체적으로는 여유 공간이 충분하더라도 연속된 공간이 부족하면 새로운 프로세스를 적재하지 못하는 문제가 발생할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/6a06fc08-bcf5-417b-b4d7-122315945831.png" alt="" style="display:block;margin:0 auto" />

<h3>외부 단편화와 메모리 압축 (External Fragmentation and Memory Compaction)</h3>
<p>앞서 그림에서 확인했듯이, 프로세스(Process)의 생성과 종료가 반복되면 빈 공간(Free Space)이 메모리(Memory)의 여러 위치에 분산되어 생기게 된다.</p>
<p>이렇게 연속되지 않는 빈 공간들이 많아지면, 전체 여유 공간은 충분하더라도 연속된 공간이 부족해서 새로운 프로세스를 적재하기 어려워질 수 있다. 이러한 현상을 <code>외부 단편화(External Fragmentation)</code>라고 한다.</p>
<p>결과적으로 외부 단편화가 발생하면 메모리 활용 효율(Memory Utilization)이 저하된다. 이 문제를 해결하기 위해 메모리 재배치(Memory Relocation), 즉 압축(Compaction)이 필요할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/0347a787-e195-444f-98f2-816ce55726f6.png" alt="" style="display:block;margin:0 auto" />

<h3>메모리 압축 (Memory Compaction)</h3>
<p>메모리 압축(Memory Compaction)은 분산되어 있는 빈 공간(Free Space)을 하나의 연속된 공간으로 합치는 방식이다. 이를 위해 프로세스(Process)들을 한쪽으로 이동시켜 흩어져 있는 빈 공간을 하나로 모은다.</p>
<p>이렇게 하면 외부 단편화(External Fragmentation) 문제를 완화할 수 있다. 하지만 프로세스를 이동시키는 과정에서 추가적인 오버헤드(Overhead)가 발생할 수 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a4f1098d-12e1-4c1b-9368-0972aa397ec2.png" alt="" style="display:block;margin:0 auto" />

<p>그림을 통해 메모리 압축이 어떻게 이루어지는지 살펴보자. 왼쪽을 보면 프로세스들 사이에 빈 공간이 여러 군데로 나뉘어 있는 것을 확인할 수 있다. 이 상태에서는 전체 여유 공간이 충분하더라도 연속된 공간이 부족하기 때문에 새로운 프로세스를 적재하기 어려울 수 있다.</p>
<p>이러한 문제를 해결하기 위해 압축을 수행한다. 프로세스들을 한쪽으로 이동시켜 흩어져 있던 빈 공간을 하나의 연속된 공간으로 모으는 것이다.</p>
<p>오른쪽 그림을 보면 압축이 수행된 후 하나의 큰 빈 공간이 만들어진 것을 확인할 수 있다. 이처럼 메모리 압축을 통해 외부 단편화 문제를 해결할 수 있지만, 프로세스를 이동하는 과정에서 오버헤드가 발생할 수 있다는 점도 함께 고려해야 한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/28e9837d-d60b-49c9-9db4-4a192e0dd75c.png" alt="" style="display:block;margin:0 auto" />

<h3>메모리 통합 (Memory Coalescing)</h3>
<p><code>연속 메모리 할당 방식(Contiguous Memory Allocation)</code>에서 <code>외부 단편화(External Fragmentation)</code>를 해결하는 또 다른 방법으로 <code>메모리 통합(Memory Coalescing)</code>이 있다.</p>
<p>메모리 통합은 서로 인접해 있는 빈 공간(Free Space)들을 하나로 합치는 방법이다. 이 방식은 프로세스(Process)를 이동시키지 않고, 빈 공간끼리만 병합(Merge)하기 때문에 압축(Compaction) 방식에 비해 오버헤드(Overhead)가 적다는 특징이 있다.</p>
<p>하지만 메모리 통합은 인접한 빈 공간이 있을 때만 수행될 수 있다. 따라서 외부 단편화를 완전히 해결하기보다는 부분적으로 완화하는 방식이라고 볼 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/c79d0e1d-4696-47d8-9a62-e1b0030aa533.png" alt="" style="display:block;margin:0 auto" />

<h3>메모리 배치 전략의 필요성</h3>
<p>(Need for Memory Placement Strategy)</p>
<p>지금까지 살펴본 것처럼 <code>가변 분할 방식(Variable Partitioning)</code>에서는 프로세스(Process)의 생성과 종료가 반복되면서 빈 공간(Free Space)이 계속 생긴다.</p>
<p>이때 빈 공간을 어떻게 활용하느냐에 따라 메모리 사용 효율(Memory Utilization)이 달라진다. 즉, 프로세스를 어떤 공간에 배치할 것인가에 따라 결과가 달라질 수 있다.</p>
<p>따라서 가변 분할 방식에서는 프로세스를 어떤 기준으로 배치할 것인지가 중요한 문제가 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/46888c0e-b35f-4599-a982-86a1d4bfd70b.png" alt="" style="display:block;margin:0 auto" />

<h3>메모리 배치 전략과 단편화 (Memory Placement Strategy and Fragmentation)</h3>
<p><code>가변 분할 방식(Variable Partitioning)</code>에서는 여러 개의 빈 공간(Free Space) 중에서 프로세스(Process)를 어디에 배치할 것인지 선택해야 한다. 이때 어떤 기준으로 공간을 선택하느냐에 따라 메모리 사용 효율(Memory Utilization)이나 성능(Performance)이 달라질 수 있다.</p>
<p>그래서 다양한 메모리 배치 전략(Memory Placement Strategy)이 존재한다. 대표적인 방식으로는 최초 적합(First Fit), 최적 적합(Best Fit), 최악 적합(Worst Fit)이 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/bcf1f875-92d4-44ce-abd6-c9fdcebe1973.png" alt="" style="display:block;margin:0 auto" />

<p>먼저 최초 적합(First Fit) 방식은 메모리의 앞쪽부터 순차적으로 빈 공간을 탐색하다가, 조건을 만족하는 첫 번째 공간에 프로세스를 바로 배치하는 방식이다. 이 방식은 필요한 공간을 찾는 즉시 할당하기 때문에 탐색을 빠르게 끝낼 수 있고, 할당 속도(Allocation Speed)가 빠르다는 특징이 있다.</p>
<p>다만 뒤쪽에 더 적절한 공간이 있더라도 그 공간은 고려하지 않는다. 앞쪽에서 조건을 만족하는 공간을 발견하면 바로 배치하기 때문에, 메모리 앞부분에 작은 빈 공간들이 남을 수 있다는 단점이 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/35bc6d00-3b8f-4df5-ae7f-5882ca7116d0.png" alt="" style="display:block;margin:0 auto" />

<p>그림을 통해 최초 적합 방식이 어떻게 동작하는지 살펴보자. 현재 13KB 크기의 프로세스가 들어온 상황이라고 가정한다. 이때 메모리의 앞쪽부터 빈 공간을 탐색한다.</p>
<p>가장 먼저 발견된 빈 공간이 16KB라면, 이 공간은 요구 크기인 13KB를 만족한다. 따라서 프로세스는 이 위치에 바로 배치된다. 그 결과 16KB 공간 중 일부만 사용되고, 나머지 공간은 그대로 남게 된다.</p>
<p>이처럼 최초 적합 방식은 조건을 만족하는 첫 번째 공간에 바로 할당하는 방식이다. 따라서 탐색 속도는 빠르지만, 메모리 앞쪽에 작은 빈 공간이 계속 남을 수 있다는 특징을 가진다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/c62f0a74-4a26-4c18-a26b-2bdfa79b906f.png" alt="" style="display:block;margin:0 auto" />

<h3>최적 적합 방식 (Best Fit)</h3>
<p>이번에는 최적 적합(Best Fit) 방식에 대해 살펴본다. 최적 적합 방식은 메모리(Memory)에 있는 모든 빈 공간(Free Space)을 탐색한 뒤, 요구 크기에 가장 근접한 공간을 선택하는 방식이다. 즉, 프로세스(Process)가 들어갈 수 있는 공간 중에서 가장 작은 적합 공간을 선택한다.</p>
<p>이 방식은 불필요하게 큰 공간을 사용하는 것을 줄일 수 있기 때문에, 공간 낭비를 줄일 수 있다는 특징이 있다.</p>
<p>예를 들어 13KB 크기의 프로세스가 들어온다고 가정해보자. 이때 16KB 공간이 있더라도, 더 근접한 14KB 공간이 있다면 14KB 공간이 선택된다.</p>
<p>다만 최적 적합 방식은 모든 빈 공간을 탐색해야 하기 때문에 탐색 비용(Search Cost)이 크다. 또한 결과적으로 작은 크기의 빈 공간들이 많이 남게 되는 문제가 발생할 수 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f9b35577-bdd4-46b1-9646-12a9cd49c985.png" alt="" style="display:block;margin:0 auto" />

<p>그림에서는 현재 13KB 크기의 프로세스가 들어온 상황을 보여준다. 이때 메모리에 존재하는 모든 빈 공간을 확인한다. 그 결과 16KB, 14KB, 5KB, 30KB와 같은 공간들 중에서 요구 크기와 가장 근접한 14KB 공간이 선택된다.</p>
<p>따라서 최초 적합(First Fit) 방식처럼 16KB 공간을 사용하는 것이 아니라, 더 작은 14KB 공간에 배치가 이루어진다.</p>
<p>이처럼 최적 적합 방식은 가장 알맞은 크기의 공간을 선택하기 때문에 공간 낭비를 줄일 수 있다. 하지만 모든 빈 공간을 탐색해야 하므로 탐색 비용이 크고, 작은 크기의 빈 공간이 점점 남게 되는 문제가 발생할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/7e1959d6-5872-4042-84b4-5275c5ecfb33.png" alt="" style="display:block;margin:0 auto" />

<h3>최악 적합 방식 (Worst Fit)</h3>
<p>이번에는 최악 적합(Worst Fit) 방식에 대해 살펴본다. 최악 적합 방식은 메모리(Memory)에 있는 빈 공간(Free Space) 중에서 가장 큰 공간을 선택해 프로세스(Process)를 배치하는 방식이다.</p>
<p>예를 들어 13KB 크기의 프로세스가 들어온다고 가정해보자. 이때 14KB나 16KB 공간이 있더라도 해당 공간을 선택하지 않고, 가장 큰 30KB 공간에 프로세스를 배치한다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f79a0df8-b3d6-494d-bd50-7637728fddb9.png" alt="" style="display:block;margin:0 auto" />

<p>이 방식은 큰 공간을 사용한 뒤에도 비교적 큰 여유 공간을 남길 수 있다는 특징이 있다. 하지만 큰 공간이 계속 분할되면서 결과적으로 작은 빈 공간들이 많이 생성될 수 있다는 문제도 있다.</p>
<p>그림에서는 현재 13KB 크기의 프로세스가 들어온 상황을 보여준다. 이때 메모리에 있는 빈 공간들 중 가장 큰 30KB 공간을 선택한다. 16KB나 14KB처럼 더 적절해 보이는 공간이 있더라도 사용하지 않고, 가장 큰 공간에 배치가 이루어진다.</p>
<p>이처럼 최악 적합 방식은 가장 큰 빈 공간을 먼저 사용하면서 상대적으로 큰 여유 공간을 유지하려는 특징이 있다. 그러나 그 큰 공간이 분할되면서 결과적으로 작은 크기의 빈 공간들이 많이 생성될 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/6b170228-5a75-4476-98a2-13ada5fe5dfa.png" alt="" style="display:block;margin:0 auto" />

<h3>연속 메모리 할당 방식의 한계 (Limitations of Contiguous Memory Allocation)</h3>
<p>지금까지 살펴본 것처럼 연속 메모리 할당 방식(Contiguous Memory Allocation)에서는 메모리(Memory)를 사용하는 과정에서 여러 개의 작은 공간으로 나뉘는 단편화(Fragmentation)가 발생할 수 있다.</p>
<p>이렇게 공간이 나뉘어 있으면 전체적으로는 여유 공간이 충분하더라도, 연속된 공간이 부족해서 메모리를 사용할 수 없는 경우가 생긴다. 그 결과 메모리 활용 효율(Memory Utilization)이 떨어지게 된다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/d28393a3-ed8e-4917-8b30-e11fa2d2ac2e.png" alt="" style="display:block;margin:0 auto" />

<p><code>연속 메모리 할당 방식</code>에서 단편화는 두 가지 형태로 나타난다. 먼저 <code>내부 단편화(Internal Fragmentation)</code>는 할당된 공간 내부에서 사용되지 않고 남는 공간이 발생하는 경우이다. 반면 <code>외부 단편화(External Fragmentation)</code>는 빈 공간이 여러 조각으로 나뉘어 흩어져 존재하는 경우이다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/5c96b469-05db-4dff-87d6-ae3aeb9adc6f.png" alt="" style="display:block;margin:0 auto" />

<p>이처럼 내부 단편화와 외부 단편화가 발생하면, 사용할 수 있는 메모리 공간이 충분하더라도 연속된 공간을 확보하기 어려워질 수 있다. 따라서 경우에 따라서는 메모리를 다시 정리해주는 작업이 필요하다. 하지만 이 과정에서도 추가적인 비용(Cost)이 발생한다.</p>
<p>결국 이러한 구조적인 한계를 해결하기 위해서는 연속된 공간에만 의존하지 않는 방식이 필요하다. 다음 시간에는 비연속 메모리 할당 방식(Non-Contiguous Memory Allocation)에 대해 알아볼 예정이다.</p>
<hr />
<h1>3️⃣ Non-contiguous memory allocation method</h1>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/de0d803d-b500-40a0-bfc7-ef4119ec56df.png" alt="" style="display:block;margin:0 auto" />

<h3>비연속 메모리 할당 방식 (Non-Contiguous Memory Allocation)</h3>
<p>지금부터는 <code>비연속 메모리 할당 방식(Non-Contiguous Memory Allocation)</code>이 어떻게 이루어지는지 살펴본다.</p>
<p>앞서 연속 <code>메모리 할당 방식(Contiguous Memory Allocation)</code>의 구조와 한계를 공부했다면, 이번에는 그 문제를 해결하기 위한 비연속 메모리 할당 방식이 어떻게 작동하는지 알아볼 예정이다.</p>
<p>비연속 메모리 할당 방식은 프로세스(Process)를 하나의 연속된 공간에 배치하는 것이 아니라, 여러 개의 떨어진 공간에 나누어 배치하는 방식이다. 그리고 이렇게 분산된 공간을 하나의 연속된 공간처럼 사용할 수 있도록 관리한다.</p>
<p>결국 비연속 메모리 할당 방식은 연속 메모리 할당 방식에서 발생했던 <code>단편화 문제(Fragmentation Problem)</code>를 해결하기 위해 도입된 방식이라고 볼 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/56820663-207a-49ea-a2be-4a0e4228f038.png" alt="" style="display:block;margin:0 auto" />

<h3>페이징 (Paging)</h3>
<p>그럼 <code>비연속 메모리 할당 방식(Non-Contiguous Memory Allocation)</code>이 어떤 방식으로 구현되는지 살펴보자. 먼저 <code>페이징(Paging)</code>의 개념에 대해 알아본다.</p>
<p>페이징은 프로세스(Process)를 하나의 연속된 공간으로 사용하는 것이 아니라, 동일한 크기의 페이지(Page) 단위로 나누어 관리하는 방식이다. 메모리(Memory) 역시 동일한 크기의 프레임(Frame) 단위로 나누어 사용한다.</p>
<p>이렇게 나누어진 페이지들은 서로 다른 프레임에 자유롭게 배치될 수 있다. 따라서 프로세스가 실제 메모리 안에서 여러 위치에 분산되어 있더라도, 하나의 연속된 공간처럼 사용할 수 있게 된다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/81a845cd-f858-4cd2-81e9-74766db997cd.png" alt="" style="display:block;margin:0 auto" />

<p>그림을 통해 페이징 방식이 어떻게 동작하는지 살펴보자. 프로세스는 페이지 단위로 나뉘고, 각 페이지는 메모리의 서로 다른 위치에 있는 프레임에 적재된다. 즉, 하나의 프로세스가 메모리 안에서 연속된 공간에 존재하는 것이 아니라 여러 위치에 나누어 배치되는 구조이다.</p>
<p>이때 <code>MMU(Memory Management Unit)</code>가 논리 주소(Logical Address)를 물리 주소(Physical Address)로 변환하면서 각 페이지가 어느 프레임에 있는지 연결해준다. 그래서 실제로는 프로세스가 메모리 곳곳에 분산되어 있더라도, 프로그램(Program)의 입장에서는 하나의 연속된 공간처럼 사용할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/4c6d4579-ec27-4b7d-b303-72ccd6eeed0e.png" alt="" style="display:block;margin:0 auto" />

<h3>페이징에서의 주소 변환 (Address Translation in Paging)</h3>
<p>페이징(Paging)에서 주소 변환(Address Translation)이 어떻게 이루어지는지 살펴보자.</p>
<p>논리 주소(Logical Address)는 <code>페이지 번호(Page Number, p)</code>와 변위<code>(Offset, d)</code>로 구성된다. 이때 페이지 번호를 기준으로 <code>페이지 테이블(Page Table)</code>을 확인하고, 해당 페이지가 저장된 <code>프레임 번호(Frame Number, f)</code>를 찾는다.</p>
<p>그다음 찾은 프레임 번호와 기존의 변위를 결합하여 최종적인 물리 주소(Physical Address)를 만든다. 즉, 논리 주소의 페이지 번호와 변위는 물리 주소의 프레임 번호와 변위로 변환되는 구조라고 이해할 수 있다.</p>
<p>이 과정에서 페이지 테이블은 주소 변환의 핵심 역할을 수행한다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/394bcb10-697e-483b-9e87-f3bdfcee1d31.png" alt="" style="display:block;margin:0 auto" />

<p>그림을 보면, 프로세스(Process)가 생성한 논리 주소는 <code>페이지 번호(p)</code>와 <code>변위(d)</code>로 나뉘어 MMU(Memory Management Unit)로 전달된다. MMU는 <code>페이지 번호(p)</code>를 이용해 페이지 테이블에서 해당 페이지가 위치한 <code>프레임 번호(f)</code>를 찾는다.</p>
<p>이후 찾은 <code>프레임 번호(f)</code>에 기존의 <code>변위(d)</code>를 그대로 결합하여 물리 주소를 생성한다. 이렇게 만들어진 물리 주소를 기준으로 실제 메모리(Physical Memory)의 해당 프레임 위치에 접근하게 된다.</p>
<p>즉, 페이지 번호는 프레임 번호로 변환되고, 변위는 그대로 유지된다. 이를 통해 최종적으로 실제 메모리 위치가 결정된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/006527d3-cb31-4c8b-96e4-99e12efab7f7.png" alt="" style="display:block;margin:0 auto" />

<h3>페이지 테이블 구조 (Page Table Structure)</h3>
<p>이제 페이지 테이블(Page Table)이 어떤 구조로 되어 있는지 살펴보자. 페이지 테이블은 각 페이지(Page)가 어느 프레임(Frame)에 저장되어 있는지를 기록해두는 자료구조(Data Structure)이다.</p>
<p>이때 페이지 번호(Page Number)를 인덱스(Index)로 사용하여, 해당 페이지가 위치한 프레임 번호(Frame Number)를 빠르게 찾을 수 있다. 즉, 페이지 테이블의 각 항목은 페이지 번호에 대응하는 프레임 번호로 구성되어 있다고 볼 수 있다.</p>
<p>결국 페이지 테이블은 논리 주소(Logical Address)와 물리 주소(Physical Address)를 연결해주는 핵심적인 역할을 한다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/35e2a8c7-1be3-41cc-87e8-de51068cf0d8.png" alt="" style="display:block;margin:0 auto" />

<p>그림을 통해 페이지 테이블이 실제로 어떻게 <code>매핑(Mapping)</code>되는지 확인해보자. 왼쪽에는 논리 주소 공간(Logical Address Space)이 있고, 각 페이지 번호가 페이지 테이블의 인덱스로 사용된다.</p>
<p>예를 들어 논리 주소의 페이지 번호가 2라고 하면, 페이지 테이블의 2번 인덱스를 조회한다. 그 결과 해당 페이지가 프레임 번호 4에 저장되어 있다는 것을 확인할 수 있다. 따라서 이 데이터는 오른쪽의 물리 메모리(Physical Memory)에서 4번 프레임 위치에 존재하게 된다.</p>
<p>이처럼 페이지 번호를 기준으로 페이지 테이블을 조회하면, 해당 데이터가 저장된 실제 위치를 찾아 접근할 수 있다. 즉, 페이지 번호를 알고 있다면 해당 데이터가 저장된 물리 메모리 위치를 빠르게 찾을 수 있는 구조라고 이해하면 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/de79a712-8afe-4a22-831a-b4aad4bf8bdb.png" alt="" style="display:block;margin:0 auto" />

<h3>페이징의 장점과 한계 (Advantages and Limitations of Paging)</h3>
<p>이제 페이징(Paging)을 왜 사용하는지 생각해보자. 페이징의 가장 큰 특징은 연속된 메모리 공간(Contiguous Memory Space)이 없어도 프로세스(Process)를 실행할 수 있다는 점이다.</p>
<p>즉, 메모리(Memory)의 여러 위치에 흩어져 있는 공간을 묶어서 사용하는 것이 가능하다. 따라서 연속 메모리 할당 방식(Contiguous Memory Allocation)에서 문제가 되었던 외부 단편화(External Fragmentation)는 사실상 발생하지 않는다.</p>
<p>그 결과 메모리를 더 유연하게 사용할 수 있고, 전체적인 메모리 활용 효율(Memory Utilization)도 좋아진다. 또한 메모리를 동일한 크기의 단위로 나누어 관리하기 때문에 구조가 단순해지고 관리도 쉬워진다는 특징이 있다.</p>
<p>지금까지 페이징의 장점을 살펴보았다면, 이번에는 한계를 살펴보자. 페이징 방식에서도 메모리를 페이지(Page) 단위로 나누어 관리하기 때문에, 페이지 크기보다 작은 데이터가 들어가면 일부 공간이 남을 수 있다. 따라서 단편화 문제가 완전히 사라지는 것은 아니며, 내부 단편화(Internal Fragmentation)는 여전히 발생할 수 있다.</p>
<p>또한 페이지 테이블(Page Table)을 따로 관리해야 하므로 추가적인 메모리 공간이 필요하다. 주소 변환(Address Translation) 과정에서도 페이지 테이블을 한 번 더 거쳐야 하기 때문에 추가적인 연산이 발생한다.</p>
<p>그 결과 메모리 접근 속도(Memory Access Speed)가 다소 느려질 수 있다는 한계도 존재한다.</p>
<hr />
<h3>세그멘테이션 (Segmentation)</h3>
<p>이번에는 세그멘테이션(Segmentation) 기법을 살펴본다. 페이징(Paging)이 메모리를 일정한 크기로 나누는 방식이었다면, 세그멘테이션은 프로그램(Program)을 의미 단위로 나누는 방식이다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/89e2277b-414e-466f-83a7-db0a66a424e6.png" alt="" style="display:block;margin:0 auto" />

<p>예를 들어 코드 영역(Code Segment), 데이터 영역(Data Segment), 스택 영역(Stack Segment)처럼 프로그램의 논리적인 구조(Logical Structure)를 기준으로 나눈다. 따라서 각 세그먼트(Segment)는 서로 다른 크기를 가질 수 있다. 이처럼 프로그램의 구조를 그대로 반영한다는 점이 세그멘테이션의 특징이다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/05f28de8-aa21-4d7c-b697-11c842cfe67b.png" alt="" style="display:block;margin:0 auto" />

<p>그림을 보면 세그멘테이션은 페이징과 접근 방식이 다르다는 것을 확인할 수 있다. 페이징은 동일한 크기로 나누어 배치하는 방식이지만, 세그멘테이션에서는 각 영역의 크기가 서로 다르게 유지된 상태로 메모리에 배치된다.</p>
<p>즉, <strong>프로그램을 나누는 기준이 크기(Size)가 아니라 의미(Meaning)라는 점이 핵심이다.</strong> 코드, 데이터, 스택처럼 서로 다른 역할을 하는 영역들이 각각 독립적으로 메모리에 배치된다.</p>
<p>이때도 실제 메모리(Physical Memory)에서는 세그먼트들이 분산되어 저장될 수 있지만, 프로그램 입장에서는 하나의 논리적인 구조로 이어져 있는 것처럼 보이게 된다.</p>
<hr />
<h3>세그멘테이션에서의 주소 변환 (Address Translation in Segmentation)</h3>
<p>이제 세그멘테이션(Segmentation)에서 주소가 어떻게 변환되는지 살펴본다. 세그멘테이션은 페이징(Paging)과 구조는 비슷하지만, 주소를 나누는 기준이 다르다는 점이 중요하다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/0e9b1b84-6fc2-43af-8c4d-eec3eeb25334.png" alt="" style="display:block;margin:0 auto" />

<p>세그멘테이션에서 논리 주소(Logical Address)는 세그먼트 번호(Segment Number)와 변위(Offset)로 구성된다. 먼저 세그먼트 번호를 이용해 세그먼트 테이블(Segment Table)에서 해당 세그먼트의 시작 위치(Base)와 길이(Limit)를 확인한다.</p>
<p>이때 변위가 허용된 범위, 즉 세그먼트의 길이를 넘지 않았는지 검사한다. 문제가 없다면 세그먼트의 시작 주소(Base Address)에 변위(Offset)를 더해 최종적인 물리 주소(Physical Address)를 생성한다.</p>
<p>즉, 페이징이 페이지 번호(Page Number)를 프레임 번호(Frame Number)로 변환하는 것이 중심이었다면, 세그멘테이션은 먼저 범위 검사(Limit Check)를 수행한 뒤 실제 위치를 계산하는 방식이라고 이해할 수 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/500db15c-322d-4372-8c03-92126d377d01.png" alt="" style="display:block;margin:0 auto" />

<p>그림을 보면 세그멘테이션에서 주소 변환이 어떤 순서로 이루어지는지 더 명확하게 확인할 수 있다. 왼쪽 상단의 <code>가상 주소(Virtual Address)</code>, 즉 논리 주소는 <code>세그먼트 번호(Segment)</code>와 <code>변위(Offset)</code>로 구성되어 있다.</p>
<p>MMU(Memory Management Unit)는 세그먼트 번호를 이용해 세그먼트 테이블에서 해당 세그먼트의 정보를 찾는다. 이때 중요한 점은 변위가 허용된 범위 안에 있는지 Yes/No로 검사한다는 것이다.</p>
<p>즉, 변위 값은 경계값(Limit Value)보다 작아야 정상적인 접근이 가능하다. 만약 조건을 만족하지 못하면 트랩(Trap)이 발생하고, 주소 오류(Address Error)로 처리된다.</p>
<p>반대로 조건을 만족하면 세그먼트의 <code>시작 주소(address)</code>에 <code>변위(Offset)</code>를 더해 최종적인 물리 주소를 생성한다. 정리하면, 세그멘테이션에서는 MMU가 먼저 변위가 허용 범위 안에 있는지 검사하고, 그다음 기준 주소(Base Address)에 변위를 더해 물리 주소를 구성한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/3b88c660-cea1-4438-8442-8809fccd3399.png" alt="" style="display:block;margin:0 auto" />

<h3>세그멘테이션의 특징 (Characteristics of Segmentation)</h3>
<p>세그멘테이션(Segmentation)의 특징을 정리해보면, 가장 큰 특징은 프로그램(Program)의 논리 구조(Logical Structure)를 그대로 반영해서 메모리(Memory)를 관리할 수 있다는 점이다.</p>
<p>따라서 코드(Code), 데이터(Data), 스택(Stack)과 같은 단위를 각각 따로 관리할 수 있다. 이를 통해 메모리 보호(Memory Protection)나 공유(Sharing)가 더 유연하게 이루어질 수 있다.</p>
<p>또한 세그먼트(Segment)마다 크기가 다르기 때문에, 프로그램의 구조에 맞게 메모리를 사용할 수 있다.</p>
<p>다만 각 세그먼트는 메모리의 연속된 공간에 배치되어야 한다. 따라서 세그멘테이션 방식에서도 외부 단편화(External Fragmentation)가 발생할 수 있다는 한계가 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/802a5012-1296-4f29-ba92-2886db1464a1.png" alt="" style="display:block;margin:0 auto" />

<h3>페이징과 세그멘테이션 비교 (Paging vs Segmentation)</h3>
<p>이제 페이징(Paging)과 세그멘테이션(Segmentation)을 함께 놓고 보면, 두 방식의 차이가 더 명확하게 보인다.</p>
<p>먼저 페이징은 메모리(Memory)를 일정한 크기로 나누어 단순하게 관리하는 방식이다. 반면 세그멘테이션은 프로그램(Program)의 의미 단위로 나누어, 프로그램의 구조를 그대로 반영하는 방식이다.</p>
<p>페이징은 구조가 단순하고 관리가 쉽다는 장점이 있다. 하지만 페이지(Page) 단위로 메모리를 나누기 때문에, 일부 공간이 사용되지 않고 남는 내부 단편화(Internal Fragmentation)가 발생할 수 있다.</p>
<p>세그멘테이션은 프로그램의 논리 구조(Logical Structure)를 잘 반영하고, 보호(Protection)나 공유(Sharing)를 유연하게 처리할 수 있다는 장점이 있다. 하지만 각 세그먼트(Segment)가 연속된 공간에 배치되어야 하기 때문에 외부 단편화(External Fragmentation)가 발생할 수 있다.</p>
<p>주소 변환(Address Translation) 방식에서도 차이가 있다. 페이징은 페이지 번호(Page Number)를 프레임 번호(Frame Number)로 변환하는 방식이라면, 세그멘테이션은 세그먼트의 시작 위치(Base)에 변위(Offset)를 더해 실제 위치를 계산하는 방식이다.</p>
<p>이처럼 두 방식을 비교해보면, 각각의 장단점이 분명하게 드러난다.</p>
]]></content:encoded></item><item><title><![CDATA[Test Process: Test Execution to Test Evaluation and Improvement ]]></title><description><![CDATA[1️⃣ Core Concepts of Test Execution2️⃣ Test Defect Management3️⃣ Core Concepts for Test Evaluation/Improvement

소프트웨어 테스팅(Software Testing): 테스트 수행, 결함 관리, 평가와 개선
오늘은 테스트를 수행하고, 그 결과를 평가하며, 문제점을 개선하는 ]]></description><link>https://heesu.tech/test-process-test-execution-to-test-evaluation-and-improvement</link><guid isPermaLink="true">https://heesu.tech/test-process-test-execution-to-test-evaluation-and-improvement</guid><dc:creator><![CDATA[Heesu Noh]]></dc:creator><pubDate>Mon, 11 May 2026 13:48:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/eeac4868-7b1f-45c6-8579-cd0a70b1fd05.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>1️⃣ Core Concepts of Test Execution<br />2️⃣ Test Defect Management<br />3️⃣ Core Concepts for Test Evaluation/Improvement</strong></p>
<hr />
<h2>소프트웨어 테스팅(Software Testing): 테스트 수행, 결함 관리, 평가와 개선</h2>
<p>오늘은 테스트를 수행하고, 그 결과를 평가하며, 문제점을 개선하는 과정에 대해 학습한다. 이번 강의에서 이해해야 할 학습 목표는 크게 세 가지이다.</p>
<p>첫 번째는 **테스트 수행(Test Execution)**이 무엇인지 이해하는 것이다. 테스트 수행이란 실제로 테스트를 진행하는 활동을 의미하며, 이 과정에서 어떤 주요 활동들이 이루어지는지 설명할 수 있어야 한다.</p>
<p>두 번째는 **테스트 결함 관리(Test Defect Management)**에 대해 이해하는 것이다. 테스트를 수행하면 결함(Defect)이 발견될 수 있는데, 이러한 결함을 체계적으로 관리하는 과정이 필요하다. 따라서 테스트 결함 관리가 무엇인지, 그리고 왜 필요한지 그 필요성까지 설명할 수 있어야 한다.</p>
<p>마지막으로 테스트 과정에 대한 **평가와 개선(Evaluation and Improvement)**을 이해해야 한다. 테스트가 제대로 수행되었는지는 정기적으로 평가되어야 하며, 평가 결과 발견된 문제점은 개선되어야 한다. 이를 위해 테스트 과정을 평가하는 방법과, 문제점을 개선하는 활동이 무엇인지 알아야 한다.</p>
<p>정리하면, 이번 강의의 목표는 테스트 수행(Test Execution)의 의미와 주요 활동을 이해하고, 테스트 결함 관리(Test Defect Management)의 필요성을 파악하며, 테스트 과정의 평가와 개선(Evaluation and Improvement) 활동을 설명할 수 있도록 하는 것이다.</p>
<hr />
<h1><strong>1️⃣ Core Concepts of Test Execution</strong></h1>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/e8d27b2b-e523-4fc4-94f1-f5e58805bdb9.png" alt="" style="display:block;margin:0 auto" />

<h3>테스트 계획과 설계 기반의 테스트 수행(Test Execution Based on Test Planning and Design)</h3>
<p>테스트(Test)는 실제 수행에 들어가기 전에 반드시 선행되어야 할 과정이 있다. 입력값(Input)에 대해 결과(Output)가 적절한지 확인하기 전에, 먼저 테스트를 계획(Test Planning)하고 설계(Test Design)하는 과정이 필요하다.</p>
<p>테스트를 하나의 프로세스(Process)로 볼 때, 우리는 PDCA 관점에서 테스트를 체계적으로 수행해야 한다고 배웠다. 여기서 D에 해당하는 단계가 바로 테스트 수행(Test Execution)이다. 테스트 수행이란 테스트 케이스(Test Case)를 바탕으로 테스트 절차(Test Procedure)에 따라 실제 테스트 환경(Test Environment)에서 테스트를 실행하는 활동이다. 이때 테스트는 사람이 직접 수동(Manual)으로 수행할 수도 있고, 자동화 도구(Automation Tool)를 사용하여 수행할 수도 있다. 중요한 것은 테스트 케이스를 실행한 뒤, 그 결과가 기대한 결과와 일치하는지를 판단하는 것이다.</p>
<p>하지만 테스트 수행이 제대로 이루어지기 위해서는 그 이전 단계가 탄탄하게 준비되어 있어야 한다. 특히 테스트 계획(Test Plan)과 테스트 설계(Test Design)가 기반이 되어야 한다. 이 부분은 V-Model을 학습할 때도 강조되었다. V-Model에서 왼쪽은 개발 활동(Development Activity), 오른쪽은 테스트 활동(Test Activity)을 의미한다. 오른쪽의 테스트 활동에서는 실제 테스트를 수행하지만, 왼쪽의 개발 활동이 진행되는 동안 이미 테스트 계획과 테스트 설계가 충분히 이루어져 있어야 한다.</p>
<p>그 이유는 명확하다. 테스트 계획 수립(Test Planning), 테스트 케이스 개발(Test Case Development), 테스트 절차 정의(Test Procedure Definition), 테스트 환경 준비(Test Environment Setup)와 같은 사전 과정이 모두 준비되어야 실제 테스트 수행 단계에서 체계적인 테스트가 가능하기 때문이다. 이러한 준비가 되어 있으면 입력값을 넣고 출력 결과가 적절한지 확인할 때 시간에 쫓기지 않고, 충분히 준비된 상태에서 안정적으로 테스트를 진행할 수 있다.</p>
<p>다시 말해 테스트 수행(Test Execution)이란 입력에 대해 결과가 적절한지를 확인하는 활동이다. 그러나 이 수행이 제대로 이루어지기 위해서는 그 이전 과정인 테스트 계획(Test Planning)과 테스트 설계(Test Design)가 제대로 수립되어 있어야 한다. 이것이 체계적인 테스트 수행에서 가장 중요한 부분이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/5804236a-0140-4155-98ee-f729341c87ec.png" alt="" style="display:block;margin:0 auto" />

<h3>ISO/IEC/IEEE 29119의 테스트 수행(Test Execution)</h3>
<p>ISO/IEC/IEEE 29119는 소프트웨어 테스트와 관련된 국제 표준(International Standard)이다. 이 표준에서 테스트 수행(Test Execution)은 동적 테스트 프로세스(Dynamic Test Process) 안에 포함된다.</p>
<p>동적 테스트 프로세스 안에는 여러 세부 활동이 있다. 먼저 테스트 설계 및 구현(Test Design and Implementation), 테스트 환경 구성 및 유지(Test Environment Setup and Maintenance)는 테스트 수행 전에 이루어지는 테스트 설계(Test Design) 관련 활동에 해당한다. 이러한 활동을 통해 테스트에 필요한 준비가 완료되면, 그 결과를 바탕으로 실제 테스트를 수행하고, 수행 결과가 적절한지 판단한 뒤 테스트 결과를 보고(Test Result Reporting)하게 된다. 이 부분이 테스트 수행(Test Execution)에 해당한다.</p>
<p>이 국제 표준에서도 테스트 수행은 사전 준비가 완료된 상태에서 진행된다는 점을 강조한다. 테스트 수행을 위해서는 테스트 명세서(Test Specification)와 테스트 환경(Test Environment)이 이미 준비되어 있어야 한다. 즉, 테스트 수행 단계에서 테스트 케이스(Test Case)를 새로 만드는 것이 아니라, 앞 단계에서 테스트 계획과 설계가 충분히 이루어진 상태에서 테스트를 실행하는 것이다.</p>
<p>따라서 테스트 수행(Test Execution)은 단독으로 이루어지는 활동이 아니라, 테스트 명세서(Test Specification), 테스트 환경(Test Environment), 테스트 설계(Test Design)와 같은 사전 준비를 기반으로 진행된다. 이러한 준비가 충분히 되어 있을 때 테스트 수행은 보다 체계적이고 안정적으로 이루어질 수 있다. 이 점을 잘 기억해야 한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/e018e8cf-51ac-4ec9-9414-938e788010a3.png" alt="" style="display:block;margin:0 auto" />

<h3>테스트 수행과 테스트 결과 보고(Test Execution and Test Result Reporting)</h3>
<p>테스트 수행(Test Execution)이란 테스트 절차(Test Procedure)를 실제로 실행하고, 그 테스트 실행 결과(Test Execution Result)를 기록하는 활동이다. 테스트 수행은 테스트 케이스(Test Case)에 기반하여 정해진 절차대로 진행된다.</p>
<p>예를 들어 로그인 기능(Login Function)을 테스트한다고 하면, 사용자는 먼저 로그인 화면을 표시하고, 아이디(ID)를 입력한 뒤, 탭(Tab) 키를 눌러 비밀번호 입력 칸으로 이동한다. 이후 비밀번호(Password)를 입력하고 엔터(Enter)를 누르는 식으로 일련의 절차를 수행한다. 이러한 과정은 임의로 진행되는 것이 아니라, 미리 작성된 테스트 케이스와 테스트 절차에 따라 실행된다.</p>
<p>테스트를 수행한 뒤에는 그 결과를 테스트 케이스에 기록한다. 실제 결과가 기대한 결과와 일치하는지 확인하고, 해당 테스트가 통과(Pass)했는지 또는 실패(Fail)했는지를 명확히 남긴다.</p>
<p>그 다음 단계는 테스트 결과 보고(Test Result Reporting)이다. 테스트 결과 보고란 테스트 실행 결과를 분석한 내용을 바탕으로 결함(Defect)을 식별하고, 그 결함에 대한 상세 정보를 기록하는 과정이다. 즉, 테스트 수행을 통해 얻은 결과를 정리하고, 발견된 문제를 체계적으로 보고하는 활동이라고 할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/13f9878b-52e8-4be5-9c83-a68f6b4461ca.png" alt="" style="display:block;margin:0 auto" />

<h3>테스트 수행의 주요 활동(Main Activities of Test Execution)</h3>
<p>테스트 수행(Test Execution)의 주요 활동 중 하나는 테스트 케이스 실행(Test Case Execution)이다. 테스트 케이스 실행이란 정의된 테스트 절차(Test Procedure)와 테스트 환경(Test Environment)에 따라 단위 테스트(Unit Test), 통합 테스트(Integration Test), 시스템 테스트(System Test), 인수 테스트(Acceptance Test) 등의 테스트 케이스(Test Case)를 실제로 실행하고, 그 결과를 기록하는 활동이다.</p>
<p>테스트 케이스는 테스트 수행 단계에서 새로 만드는 것이 아니라, 앞선 테스트 설계(Test Design) 단계에서 이미 작성되어 있어야 한다. V-Model 관점에서도 개발 활동이 진행되는 동안 테스트 설계가 함께 이루어지고, 그 과정에서 테스트 케이스가 만들어진다. 따라서 테스트 수행 단계에서는 이미 준비된 테스트 케이스를 바탕으로 실제 실행을 진행한다.</p>
<p>테스트는 사람이 직접 수동(Manual)으로 수행할 수도 있고, 자동화 도구(Automation Tool)를 사용하여 수행할 수도 있다. 중요한 것은 어떤 방식으로 실행하든 테스트 케이스에 정의된 입력값(Input)과 예상 출력값(Expected Output)을 기준으로 실제 실행 결과(Actual Result)를 확인하고 기록하는 것이다.</p>
<p>테스트 케이스 양식을 보면 보통 <code>TC_1</code>, <code>TC_2</code>와 같이 테스트 케이스 ID(Test Case ID)가 구분되어 있고, 입력값(Input), 예상 출력값(Expected Output), 실행 결과(Actual Result), 조치사항(Action Item), 조치결과(Action Result) 등을 기록하는 칸이 있다. 이 중 테스트 케이스 ID, 입력값, 예상 출력값과 같은 항목은 테스트 설계 단계에서 이미 작성되어 있어야 한다. 테스트 케이스는 요구사항 문서(Requirements Document)를 바탕으로 만들 수도 있고, 코드(Code)를 기준으로 만들 수도 있다. 또한 화이트박스 테스트(White-box Test)인지 블랙박스 테스트(Black-box Test)인지에 따라 테스트 케이스를 만드는 방식도 달라질 수 있다.</p>
<p>예를 들어 로그인 기능(Login Function)을 테스트한다고 할 때, 테스트 케이스의 목적이 “로그인 시 아이디와 비밀번호의 대소문자를 구분하여 처리한다”라고 되어 있다고 하자. 이때 <code>TC_1</code>의 입력값이 <code>CUK/korea</code>라면, 예상 출력값은 “아이디 없음” 경고가 되어야 한다. 아이디의 대소문자를 구분해야 하기 때문에, 등록된 아이디와 대소문자가 다르면 정상 로그인이 되면 안 되기 때문이다.</p>
<p>그런데 실제 실행 결과(Actual Result)에 “정상 로그인”이라고 나온다면, 이는 예상 출력값과 다르므로 문제가 있는 것이다. 이 경우 조치사항(Action Item)에는 “디버깅 필요(Debugging Required)”와 같은 내용이 기록될 수 있다. 이후 문제가 수정되면 조치결과(Action Result)에 “아이디 없음 경고 표시”와 같이 해결된 내용을 기록한다.</p>
<p>정리하면, 테스트 수행 단계에서는 이미 설계된 테스트 케이스를 실제로 실행하고 그 결과를 기록한다. 이 과정에서 실행 결과(Actual Result), 조치사항(Action Item), 조치결과(Action Result)는 테스트 수행 중에 작성되지만, 그 외의 테스트 케이스 ID(Test Case ID), 입력값(Input), 예상 출력값(Expected Output), 테스트 목적(Test Objective) 등은 테스트 설계 단계에서 미리 준비되어 있어야 한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/7b540074-6d25-425e-9e83-79c810edad98.png" alt="" style="display:block;margin:0 auto" />

<h3>테스트 수행의 주요 활동: 테스트 결과 판정(Main Activities of Test Execution: Test Result Evaluation)</h3>
<p>테스트 수행(Test Execution)의 주요 활동 중 하나는 테스트 결과 판정(Test Result Evaluation)이다. 테스트 결과 판정이란 테스트 수행 결과를 분석하여 결함(Defect) 여부를 판단하고, 결함으로 확인된 경우 그 상세 내용을 기록하는 활동이다.</p>
<p>테스트 케이스(Test Case)에 맞게 테스트를 정상적으로 수행했더라도, 그 결과가 반드시 결함이라고 단정할 수는 없다. 테스트 결과가 실제 결함일 수도 있지만, 결함이 아닐 수도 있기 때문이다. 따라서 테스트 결과를 다시 확인하고 분석하여, 해당 결과가 결함인지 아닌지를 판단하는 과정이 필요하다. 결함으로 판단될 경우에는 그 내용을 상세하게 기록해야 한다.</p>
<p>결함을 상세하게 기록해야 하는 이유는 커뮤니케이션(Communication) 때문이다. V-Model을 기준으로 보면 왼쪽은 개발 활동(Development Activity), 오른쪽은 테스트 활동(Test Activity)에 해당한다. 테스트는 개발자가 작성한 코드(Code)를 대상으로 수행되지만, 테스터(Tester)가 직접 그 코드를 개발한 것은 아니기 때문에 테스트 결과가 성공인지 실패인지는 알 수 있어도, 내부적으로 왜 그런 결과가 발생했는지까지는 정확히 알기 어려울 수 있다. 따라서 개발자가 결함 내용을 충분히 이해할 수 있도록 테스트 결과를 구체적이고 명확하게 작성해야 한다.</p>
<p>즉, 테스트 결과는 단순히 성공(Pass) 또는 실패(Fail)로만 기록해서는 부족하다. 왜 실패했는지, 어떤 상황에서 문제가 발생했는지, 어떤 부분을 조치해야 하는지를 분석하고 기록해야 한다. 이 과정에서 테스트와 개발 사이의 커뮤니케이션이 매우 중요해진다.</p>
<p>예를 들어 결함 ID(Defect ID)가 <code>Defect 001</code>이고, 대상 기능(Target Function)이 로그인(Login) 기능이라고 하자. 테스터(Tester)는 고려인이며, 테스트 일자(Test Date)도 함께 기록되어 있다. 결함 제목(Defect Title)은 “아이디 대소문자 처리 결함”이다.</p>
<p>앞서 로그인 테스트에서 아이디를 대문자 <code>CUK</code>로 입력했을 때, 예상 결과(Expected Result)는 “아이디 없음 경고”가 표시되는 것이었다. 그러나 실제 결과(Actual Result)는 정상 로그인으로 나타났다. 이 경우 상세 설명(Description)에는 아이디의 대소문자를 구분해야 함에도 불구하고, 대문자로 입력한 아이디가 정상적으로 로그인 처리되었다는 내용을 구체적으로 작성해야 한다.</p>
<p>조치사항(Action Item)에는 <code>login()</code> 함수의 대소문자 처리 로직(Case-sensitive Logic)에 대한 디버깅(Debugging)이 필요하다고 기록할 수 있다. 또한 결함의 심각도(Severity)도 함께 분석하여 기록한다. 이 예시에서는 심각도를 “매우 심각(Critical)”으로 판단할 수 있다. 로그인 기능은 사용자가 시스템을 사용할 때 가장 먼저 접하는 기능이므로, 이 기능에 결함이 있으면 전체 시스템 사용에 미치는 영향이 클 수 있기 때문이다.</p>
<p>정리하면, 테스트 수행(Test Execution)이란 앞서 준비된 테스트 케이스(Test Case)를 실제로 실행하고, 그 결과로 나타난 문제점이나 이상 현상이 결함(Defect)에 해당하는지 판단하는 과정이다. 그리고 결함으로 판단된 경우에는 개발자가 이해하고 조치할 수 있도록 결함의 상세 내용을 명확하게 기록해야 한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/337272d1-950c-4fdd-aed2-2c8b767e8356.png" alt="" style="display:block;margin:0 auto" />

<h3>Java-based Source Code Coverage Tools: JUnit, EclEmma</h3>
<p>테스트를 사람이 직접 수동(Manual)으로 수행하는 데에는 한계가 있을 수밖에 없다. 특히 소스코드(Source Code)를 기반으로 화이트박스 테스트(White-box Test)를 수행할 때 중요한 개념 중 하나가 커버리지(Coverage)이다. 커버리지란 코드가 테스트를 통해 얼마나 실행되었는지를 나타내는 기준이다.</p>
<p>이러한 커버리지 측정을 도와주는 도구들도 존재한다. 대표적으로 Eclipse 환경에서 사용할 수 있는 JUnit과 EclEmma가 있다. 이들은 Java 기반 테스트에서 활용할 수 있는 도구로, 플러그인(Plugin) 형태로 제공된다.</p>
<p>JUnit은 Java 프로그램에서 테스트 코드를 작성하고 실행할 때 사용할 수 있는 도구이다. Eclipse 환경에서 JUnit을 사용하면 클래스(Class)별로 테스트 결과를 확인할 수 있고, Line Coverage와 Branch Coverage를 통해 코드가 얼마나 테스트되었는지 볼 수 있다. Line Coverage는 문장 또는 코드 라인이 얼마나 실행되었는지를 의미하고, Branch Coverage는 조건문이나 분기문이 얼마나 테스트되었는지를 의미한다. 테스트가 완료된 부분은 녹색으로, 테스트가 수행되지 않은 부분은 빨간색으로 표시되어 시각적으로 확인할 수 있다.</p>
<p>EclEmma 역시 Eclipse 환경에서 커버리지 측정(Coverage Measurement)에 사용되는 도구이다. EclEmma를 사용하면 소스코드가 테스트된 정도를 색상으로 확인할 수 있다. 녹색은 테스트가 완료된 부분을 의미하고, 노란색은 일부만 테스트된 부분을 의미하며, 빨간색은 테스트가 수행되지 않은 부분을 의미한다. 이를 통해 개발자나 테스터는 어떤 코드가 충분히 테스트되었고, 어떤 부분이 아직 테스트되지 않았는지 쉽게 파악할 수 있다.</p>
<p>이처럼 JUnit과 EclEmma 같은 도구를 활용하면 사람이 직접 모든 테스트 결과를 확인하는 것보다 훨씬 효율적으로 테스트 수행 결과와 코드 커버리지(Code Coverage)를 확인할 수 있다. Java 프로그램을 테스트할 때 이러한 플러그인을 설치하여 활용해 보는 것도 좋은 방법이다.</p>
<p>지금까지는 테스트 수행(Test Execution)이라는 프로세스(Process)에 대해 살펴보았다.</p>
<hr />
<h1>2️⃣ Test Defect Management</h1>
<p>다음은 테스트 결함관리 부분이다. 결함관리가 무엇인지, 왜 필요한지 위주로 살펴보도록할것이다. 테스트를 하고 나면 결함들이 나오게 된다. 그 결함을 관리하는 과정을 '테스트 결함 관리'라고한다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/0888617d-a2b3-4c8a-801c-452bce21fdae.png" alt="" style="display:block;margin:0 auto" />

<h3>테스트 결함 관리 정의(Test Defect Management)</h3>
<p>앞서 테스트 수행(Test Execution) 과정을 통해 단위 테스트(Unit Test), 통합 테스트(Integration Test), 인수 테스트(Acceptance Test) 등 여러 테스트 활동을 진행하였다. 이러한 테스트 단계에서 발생한 결함(Defect)을 식별하고, 결함을 해결하기 위해 조치하는 일련의 프로세스를 테스트 결함 관리(Test Defect Management) 또는 결함 관리 프로세스(Defect Management Process)라고 한다.</p>
<p>결함 관리는 테스트 과정에서 발견된 결함을 체계적으로 관리하기 위해 사용된다. 테스트를 통해 어떤 결함이 발견되었는지, 결함이 몇 개 존재하는지, 그중 몇 개가 해결되었는지 등을 관리 관점에서 측정할 수 있어야 한다. 이러한 측정 결과를 바탕으로 테스트 활동의 상태를 파악할 수 있으며, 이후 테스트 측정(Test Measurement), 테스트 조정(Test Control), 품질 관리(Quality Management)와 같은 관리 프로세스의 기반이 된다.</p>
<p>결함 관리 프로세스(Defect Management Process)의 흐름을 살펴보면, 먼저 테스트를 수행한 뒤 예상 결과(Expected Result)와 실제 결과(Actual Result)를 비교한다. 만약 두 결과가 일치하면 정상적으로 테스트가 수행된 것이고, 불일치가 발생하면 결함 가능성이 있는 상황으로 본다.</p>
<p>이후 해당 문제를 결함으로 식별(Identification)하고 분석(Analysis)한다. 분석 결과 실제 결함이 아니거나 잘못된 테스트로 판단되면 해당 테스트는 종료된다. 반면 실제 결함으로 판단되면 결함을 등록(Defect Registration)하고, 누가 수정할 것인지 담당자를 지정하여 결함 조치(Defect Action)를 진행한다.</p>
<p>결함 조치가 완료되었다고 해서 바로 끝나는 것은 아니다. 수정된 결함이 실제로 해결되었는지 확인하기 위해 반드시 재테스트(Retest)를 수행해야 한다. 재테스트를 통해 결함이 정상적으로 해결되었는지 다시 확인한 뒤, 문제가 없다고 판단되면 테스트 종료(Test Closure)를 확인하게 된다.</p>
<p>따라서 결함 관리 프로세스는 단순히 테스트를 수행하고 결함을 해결하는 것으로 끝나는 과정이 아니다. 결함을 식별하고, 분석하고, 등록하고, 조치한 뒤, 재테스트를 통해 실제 해결 여부를 확인하는 전체 과정을 의미한다.</p>
<p>이러한 과정을 거치면서 결함의 상태(Defect Status)는 계속 변화한다. 예를 들어 결함이 새롭게 등록된 상태는 오픈(Open), 수정이 진행 중인 상태는 진행 중(In Progress), 조치가 완료된 상태는 해결(Resolved)로 관리될 수 있다. 이렇게 결함 상태를 데이터로 측정하고 관리함으로써, 테스트 과정뿐만 아니라 근본적인 품질 관리(Quality Management)까지 이루어질 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/fc9ab705-675c-4e34-bf02-b04c46669525.png" alt="" style="display:block;margin:0 auto" />

<h3>테스트 결함 관리의 필요성 (Necessity of Test Defect Management)</h3>
<p>일반적으로 관리(Management)란 데이터를 수집하고, 그 데이터를 기반으로 일이 제대로 실행될 수 있도록 모니터링(Monitoring)하고 통제(Control)하는 활동을 의미한다. 결함 관리(Defect Management)도 마찬가지이다. 테스트 과정에서 발견된 결함을 체계적으로 수집하고 관리함으로써 소프트웨어의 품질(Quality)을 확보하는 것이 목적이다.</p>
<p>결함은 결국 품질 관리(Quality Management)와 연결된다. 개발 과정에서 개발자들은 실수로 인해 여러 가지 결함(Defect)을 만들 수 있다. 만약 이러한 결함이 해결되지 않은 채 사용자에게 배포되고 운영된다면, 그때부터는 큰 품질 문제가 될 수 있다. 사용자 클레임(Claim)이 발생할 수 있고, 문제를 해결하기 위해 더 큰 비용을 치러야 할 수도 있다.</p>
<p>따라서 결함 관리는 실제 사용자에게 소프트웨어가 배포되기 전에 개발 영역에서 결함을 찾아내고, 이를 체계적으로 관리하여 개발 단계의 품질을 최대한 확보하기 위한 목적으로 수행된다.</p>
<p>테스트 결함 관리(Test Defect Management)의 필요성은 크게 세 가지로 정리할 수 있다.</p>
<p>첫 번째는 결함 상태(Defect Status)를 기반으로 결함 조치 현황을 모니터링하는 데 활용된다는 점이다. 결함은 처음 식별(Identification)된 뒤 등록(Registration), 분석(Analysis), 평가(Evaluation), 처리(Handling), 해결(Resolution), 재확인(Recheck)과 같은 여러 상태를 거친다. 이러한 상태를 기준으로 현재 결함이 어느 단계에 있는지, 조치가 제대로 이루어지고 있는지를 모니터링할 수 있다.</p>
<p>두 번째는 결함 관련 지표(Defect Metrics)를 기반으로 품질 현황을 정량적으로 관리하는 데 활용된다는 점이다. 단순히 품질이 좋다거나 나쁘다고 판단하는 것이 아니라, 데이터를 지표화하여 정량적으로 관리하는 것이 중요하다. 예를 들어 결함 조치율(Defect Resolution Rate), 테스트 케이스 실패율(Test Case Failure Rate)과 같은 지표를 통해 품질 상태를 객관적으로 파악할 수 있다. 이를 통해 현재 소프트웨어의 품질 수준을 보다 명확하게 관리할 수 있다.</p>
<p>세 번째는 결함 유형(Defect Type)을 파악하고 원인 분석(Cause Analysis)에 활용할 수 있다는 점이다. 한 번 발생한 결함은 이후 다른 유사한 프로젝트에서도 다시 발생할 가능성이 있다. 따라서 같은 문제가 반복되지 않도록 결함의 유형을 파악하고, 그 원인이 사람의 문제인지, 도구(Tool)의 문제인지, 절차(Process)의 문제인지 등을 분석해야 한다. 이러한 분석은 향후 결함을 예방하고 품질을 개선하는 데 중요한 기반이 된다.</p>
<p>정리하면, 테스트 결함 관리(Test Defect Management)는 결함을 단순히 기록하는 활동이 아니라, 결함 상태를 모니터링하고, 품질 현황을 정량적으로 관리하며, 결함의 원인을 분석해 향후 품질 개선(Quality Improvement)에 활용하기 위한 중요한 관리 활동이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/624aa23c-b936-4e11-bf5a-de0c1d9ae61f.png" alt="" style="display:block;margin:0 auto" />

<h3>결함 상태 기반의 결함 조치 현황 모니터링</h3>
<h3>(Monitoring Defect Handling Status Based on Defect Status)</h3>
<p>결함 상태(Defect Status)는 결함이 현재 어떤 단계에 있는지를 나타내는 기준이다. <code>결함이 처음 생성(Create Defect)</code>되면 <code>오픈(Open)</code> 상태가 된다. 이후 담당자가 지정되고, 담당자가 해당 결함을 처리하기 시작하면 <code>진행 중(In Progress)</code> 상태가 된다. 담당자가 결함을 모두 해결하면 <code>해결됨(Resolved)</code> 상태가 된다.</p>
<p>그러나 결함이 해결되었다고 해서 바로 종료되는 것은 아니다. 이후 제3자가 해당 결함이 실제로 해결되었는지 확인하는 과정을 거치게 된다. 이 확인 작업까지 완료되어야 비로소 <code>종료(Closed)</code> 상태가 된다.</p>
<p>결함은 중간에 다시 열릴 수도 있다. 예를 들어 결함이 해결된 뒤 재테스트(Retest)를 수행했을 때 문제가 다시 발견되면 <code>재오픈(Reopened)</code> 상태가 된다. 심지어 몇 달 전에 완전히 해결되었다고 판단한 결함이라도, 이후 유사한 문제가 다시 발생하면서 기존 결함이 완전히 해결된 것이 아니었다고 판단될 수 있다. 이 경우에도 결함은 다시 Reopened 상태가 될 수 있다.</p>
<p>이처럼 결함은 <code>Open, In Progress, Resolved, Closed, Reopened</code>와 같은 상태를 거치며 관리된다. 이러한 흐름을 <code>결함 워크플로우(Defect Workflow)</code>라고 한다.</p>
<p>관리자, 특히 품질 관리자(Quality Manager)는 이러한 결함 상태를 바탕으로 프로젝트의 결함 조치 현황을 모니터링한다. 예를 들어 현재 프로젝트에서 Open 상태의 결함이 10개, In Progress 상태의 결함이 5개, Resolved 상태의 결함이 3개, Closed 상태의 결함이 5개라는 식으로 결함의 상태를 파악할 수 있다. 이를 통해 결함이 제대로 조치되고 있는지, 아직 해결되지 않은 결함이 얼마나 남아 있는지, 최종적으로 종료된 결함은 얼마나 되는지를 확인할 수 있다.</p>
<p>결함 관리의 목표는 발견된 결함이 최종적으로 모두 Closed 상태가 되도록 관리하는 것이다. 하지만 실제 프로젝트에서는 결함이 수십 개, 수백 개씩 발생할 수 있고, 유사한 결함이 반복해서 나타날 수도 있다. 따라서 결함을 효과적으로 관리하기 위해서는 결함의 상태 관리(Defect Status Management)가 반드시 필요하다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/1347b40c-e234-4f3f-a41b-11d72e4897c5.png" alt="" style="display:block;margin:0 auto" />

<h3>결함 관련 지표 기반의 품질 현황 관리</h3>
<h3>(Quality Status Management Based on Defect Metrics)</h3>
<p>테스트를 수행하면 많은 결함(Defect)이 발견될 수 있다. 이때 테스트 결함과 관련된 여러 데이터를 수집하고, 그 데이터를 기반으로 품질(Quality)과 결함에 대한 지표(Metrics)를 만들 수 있다. 이러한 지표를 활용하면 품질 현황을 정량적으로 관리할 수 있다.</p>
<p>가장 쉽게 사용할 수 있는 지표 중 하나는 <code>평균 결함 건수(Average Defect Count)</code>이다. 평균 결함 건수는 결함 건수(Defect Count)를 테스트 대상 모듈 수(Number of Test Target Modules)로 나누어 계산할 수 있다. 예를 들어 10개의 모듈을 테스트했는데 20개의 결함이 발견되었다면, 하나의 모듈에서 평균적으로 2개의 결함이 발생한 것으로 볼 수 있다. 이를 통해 20개 또는 30개의 모듈을 테스트할 경우 어느 정도의 결함이 발생할 수 있는지 예측할 수 있고, 특정 모듈에서 결함이 많이 발생하는지도 파악할 수 있다.</p>
<p>또 다른 지표는 <code>심각도별 결함 건수(Number of Defects by Severity)</code>이다. 결함은 전체 시스템 품질에 미치는 영향에 따라 심각도(Severity)를 나눌 수 있다. 예를 들어 해킹 위험, 보안 위험, 오동작으로 인한 제품 운영 문제처럼 시스템 전체에 큰 영향을 미치는 결함은 높은 심각도로 분류될 수 있다. 반면 영향이 작은 결함은 낮은 심각도로 분류된다.</p>
<p>심각도별 결함 비율은 각 심각도별 결함 건수(Number of Defects by Each Severity)를 전체 결함 건수(Total Defect Count)로 나누어 파악할 수 있다. 예를 들어 전체 결함이 10개일 때, 마이너(Minor) 결함이 2개, 보통(Major) 결함이 5개, 매우 심각(Critical) 결함이 3개라면 각각 2/10, 5/10, 3/10의 비율로 나타낼 수 있다. 이를 통해 매우 심각한 결함이 3개 발생했다는 사실을 확인할 수 있고, 해당 결함들을 우선순위(Priority)에 따라 먼저 조치한 뒤 배포 여부를 결정할 수 있다.</p>
<p><code>결함의 유형별 분포 비율(Defect Distribution by Type)</code>도 중요한 지표이다. 결함에는 여러 종류가 있다. 단순한 문법 오류(Syntax Error)일 수도 있고, 서로 다른 A와 B 두 인터페이스(Interface) 사이에서 발생하는 결함일 수도 있으며, 성능 문제(Performance Issue)와 관련된 결함일 수도 있다. 이러한 결함 유형을 분류하고 비율로 파악하면 어떤 종류의 문제가 자주 발생하는지 확인할 수 있다.</p>
<p>또한 <code>결함 조치율(Defect Resolution Rate)</code>과 같은 지표를 통해 결함이 얼마나 해결되고 있는지도 관리할 수 있다. 결함의 상태(Status)를 비율로 파악하면 현재 결함 처리 현황을 보다 명확하게 확인할 수 있다.</p>
<p><code>테스트 케이스 실패율(Test Case Failure Rate)</code>도 결함 관리에서 활용할 수 있는 지표이다. 테스트 케이스(Test Case)를 수행하면 결과는 성공(Pass) 또는 실패(Fail)로 나뉜다. 여기서 실패란 처음에 예상한 결과(Expected Result)가 나오지 않았다는 의미이다. 테스트 케이스의 관점에서 보면 실패가 반드시 나쁜 것은 아니다. 오히려 실패한 테스트 케이스를 통해 결함을 발견할 수 있기 때문에, 테스트가 제대로 수행되었다고 볼 수도 있다.</p>
<p>테스트 케이스 실패율은 결함으로 식별된 테스트 케이스 수(Number of Test Cases Identified as Defects)를 전체 테스트 케이스 수(Total Number of Test Cases)로 나눈 뒤 100을 곱해 계산할 수 있다. 이를 통해 전체 테스트 케이스 중 어느 정도가 결함과 연결되었는지를 파악할 수 있다.</p>
<p>이 외에도 결함과 관련된 다양한 지표를 활용할 수 있다. 이러한 지표들은 단순히 결함을 기록하는 데 그치지 않고, 품질 현황을 정량적으로 파악하고 관리하는 데 사용된다. 궁극적으로 결함 관련 지표(Defect Metrics)를 활용하면 보다 체계적인 품질 관리(Quality Management)가 가능해진다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/85ddcfa2-4890-497a-bc4c-d20dd2d072d5.png" alt="" style="display:block;margin:0 auto" />

<h3>결함 유형 파악 및 원인 분석에 활용</h3>
<h3>(Defect Type Identification and Cause Analysis)</h3>
<p>결함(Defect)이 한 번 발생했다고 해서 다시 발생하지 않는다는 보장은 없다. 결함은 재발할 수 있기 때문에, 결함 관리(Defect Management)를 통해 현재 결함이 Open 상태인지, Closed 상태인지 지속적으로 파악하고 분석해야 한다.</p>
<p>결함을 관리할 때는 단순히 해결 여부만 확인하는 것이 아니라, 해당 결함이 어떤 유형의 결함이었는지도 함께 파악해야 한다. 결함 유형(Defect Type)에는 <code>문법 문제(Syntax Issue), 논리 문제(Logic Issue), 할당 문제(Assignment Issue), 인터페이스 문제(Interface Issue), 성능 문제(Performance Issue), 검사 문제(Checking Issue), 데이터 문제(Data Issue)</code> 등이 있을 수 있다.</p>
<p>이러한 결함 유형을 분석하면 어떤 결함이 많이 발생하는지 확인할 수 있다. 예를 들어 분석 결과 Syntax 결함이 가장 많이 발생하고, 그다음으로 Logic과 관련된 결함이 많이 발생했다면, 해당 유형의 결함을 중심으로 개선 활동을 진행할 수 있다.</p>
<p>많이 발생하는 결함은 우선적으로 해결해야 하며, 이후 같은 결함이 다시 발생하지 않도록 예방 활동도 함께 이루어져야 한다. 이를 위해 개발자 교육(Developer Training)을 진행하거나, 매뉴얼 및 지침서(Manual and Guideline)를 작성할 수 있다. 또한 정적 분석(Static Analysis)을 통해 개발 단계에서 결함을 사전에 확인할 수 있도록 가이드할 필요가 있다.</p>
<p>결함이 발생하면 수정하고 다시 확인하는 과정이 반복된다. 만약 같은 결함이 계속 발생한다면, 회사 입장에서는 시간과 비용의 낭비가 된다. 따라서 결함 관리는 단순히 결함을 해결하는 데 그치는 것이 아니라, 결함의 원인(Cause)을 분석하고 재발을 방지(Recurrence Prevention)하기 위한 목적으로도 활용된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/929dd4c6-9273-4521-838a-57521d39ed72.png" alt="" style="display:block;margin:0 auto" />

<h3>[참고] 미국 소프트웨어 기업의 결함 조치율 현황</h3>
<h3>(Defect Resolution Rate in U.S. Software Companies)</h3>
<p>미국 소프트웨어 기업의 평균 결함 조치율(Defect Resolution Rate)은 약 75~85% 수준이며, AT&amp;T, IBM과 같은 기업들은 99% 수준의 결함 조치율을 달성하고 있다.</p>
<p>소프트웨어의 규모를 기능 점수(Function Point) 기준으로 판단할 때, 기능 점수당 결함 수(Number of Defects per Function Point)를 활용할 수 있다. 이는 소프트웨어 규모 대비 얼마나 많은 결함이 발생하는지를 나타내는 자료이다.</p>
<p>먼저 미흡한 조직의 경우, 품질(Quality)의 중요성에 대한 인식이 부족하다. 이러한 조직은 품질보다는 비용 절감(Cost Reduction)을 우선으로 하는 경향이 있다. 그 결과 기능 점수당 결함 수가 8<del>10개로 매우 많고, 결함 조치율은 50</del>55% 수준에 머무른다. 즉, 발견된 결함 중 약 40% 이상이 해결되지 않는 상태로 남아 있기 때문에 품질 수준이 매우 낮다고 볼 수 있다.</p>
<p>다음으로 평균적인 기업의 경우, 기능 점수당 결함 수는 약 4<del>6개이며, 결함 조치율은 75</del>85% 수준이다. 이 경우에도 여전히 약 20% 정도의 결함은 해결되지 않은 채 남아 있을 수 있다. 많은 소프트웨어 조직들이 이 평균 수준에 속한다고 볼 수 있다.</p>
<p>반면 최상위 기업들은 기능 점수당 결함 수가 1<del>3개 수준으로 낮고, 결함 조치율은 90</del>99%에 이른다. 이는 개발 단계에서 대부분의 결함이 해결되는 조직이라는 의미이며, 품질 수준이 매우 우수한 조직이라고 할 수 있다.</p>
<p>많은 조직들이 여전히 평균 수준에 머물러 있다. 하지만 오늘날은 소프트웨어의 중요성이 더욱 강조되는 소프트웨어 중심 시대이다. 그럴수록 품질의 중요성을 인식하고, 개발자로서 소프트웨어 품질을 높이기 위해 지속적으로 노력하는 자세가 중요하다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/26ab4cef-e02b-4d4f-994c-6fe3a1beb5d8.png" alt="" style="display:block;margin:0 auto" />

<h3>테스트 평가와 개선(Test Evaluation and Improvement)</h3>
<p>앞서 테스트 프로세스(Test Process)는 테스트 계획(Test Planning), 테스트 설계(Test Design), 테스트 수행(Test Execution), 평가와 개선(Evaluation and Improvement)의 순서로 이루어진다고 배웠다. 또한 이 과정은 PDCA 사이클(PDCA Cycle)에 따라 진행된다.</p>
<p>테스트 평가와 개선(Test Evaluation and Improvement)은 PDCA에서 C와 A, 즉 확인(Check)과 조치(Act)에 해당한다. 앞 단계에서 테스트를 계획하고, 설계하고, 수행했다면, 이후에는 그 테스트가 얼마나 효과적으로 이루어졌는지를 평가해야 한다. 그리고 평가 결과 테스트가 효과적이지 않다고 판단되면, 테스트 프로세스를 개선해야 한다.</p>
<p>예를 들어 몇 시간 동안 수백 개의 테스트를 수행했는데 모든 결과가 성공(Pass)으로 나왔다고 하자. 이 경우 제품의 품질(Quality)이 좋다고 판단할 수도 있다. 하지만 반대로 테스트 케이스(Test Case)가 제대로 만들어지지 않았기 때문에 결함을 발견하지 못한 것이라고 판단할 수도 있다. 즉, 테스트 결과가 모두 성공이라고 해서 반드시 테스트가 효과적이었다고 단정할 수는 없다.</p>
<p>또한 테스트는 정해진 일정(Schedule)과 기간(Duration) 안에서 수행되어야 한다. 그런데 테스트 수행 시간이 지나치게 오래 걸린다면, 테스트 케이스가 효율적으로 설계되었는지, 테스트 도구(Test Tool)나 장비(Equipment)에 문제가 있는지, 또는 인력(Personnel) 측면에서 문제가 있는지를 확인해야 한다. 이처럼 테스트가 적절하게 수행되었는지를 평가하고, 필요한 경우 개선하는 과정이 중요하다.</p>
<p>따라서 테스트 평가와 개선(Test Evaluation and Improvement) 단계에서는 테스트 결과 분석 및 평가(Test Result Analysis and Evaluation)가 이루어진다. 그리고 분석 결과를 바탕으로 대책 수립(Countermeasure Planning)과 시정안 권고(Corrective Action Recommendation)가 진행된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/fcadef65-fa2c-4317-839f-d412c4cf85bf.png" alt="" style="display:block;margin:0 auto" />

<h3>참고: ISO/IEC 33063 Process Assessment Model for Software Testing</h3>
<p>ISO/IEC 33063은 소프트웨어 테스팅(Software Testing) 프로세스를 평가하기 위한 표준이다. 앞서 테스트 프로세스(Test Process)는 여러 계층으로 구성되어 있다고 배웠다. 크게 조직 차원의 테스트 프로세스(Organizational Test Process), 테스트 관리 프로세스(Test Management Process), 동적 테스트 프로세스(Dynamic Test Process)로 나눌 수 있으며, 이러한 구조는 ISO/IEC/IEEE 29119 표준에서 다루는 내용이다.</p>
<p>다만 테스트 평가와 개선(Test Evaluation and Improvement)에 대해서는 ISO/IEC/IEEE 29119가 아니라, ISO/IEC 33063 표준을 참고한다. ISO/IEC 33063은 소프트웨어 테스팅 프로세스가 얼마나 적절하게 수행되고 있는지를 평가하기 위한 기준을 제시한다.</p>
<p>즉, ISO/IEC/IEEE 29119가 테스트 프로세스의 구조와 활동을 설명하는 표준이라면, ISO/IEC 33063은 그 테스트 프로세스를 평가하는 데 활용되는 표준이라고 볼 수 있다. 따라서 ISO/IEC 33063은 앞서 살펴본 ISO/IEC/IEEE 29119의 세 가지 테스트 프로세스를 평가하기 위한 기준으로 활용된다.</p>
<p>이 표준은 테스트 프로세스가 제대로 수행되고 있는지 확인하고, 개선이 필요한 부분을 찾는 데 참고할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/0fdba6dc-4e7e-4d68-974c-960336df0b14.png" alt="" style="display:block;margin:0 auto" />

<h3>참고: ISO/IEC 33063 Process Assessment Model for Software Testing</h3>
<p>ISO/IEC 33063에서는 소프트웨어 테스팅(Software Testing) 프로세스를 평가한 뒤, 해당 프로젝트(Project) 또는 조직(Organization)의 능력과 역량을 등급으로 정의한다. 이 등급은 0부터 5까지의 능력 수준(Capability Level)으로 제시되며, 0이 가장 낮은 수준이고 5가 가장 우수한 수준이다.</p>
<p>먼저 레벨 0(Level 0)은 불완전한 프로세스(Incomplete Process)에 해당한다. 테스트 프로세스(Test Process) 자체가 존재하지 않거나, 프로세스가 있더라도 실제 수행 결과가 거의 없는 상태이다. 체계적인 테스트 설계(Test Design) 없이 테스트 수행(Test Execution)에만 급급하고, 일부 테스트 결과만 남아 있는 매우 부족한 조직이라고 볼 수 있다.</p>
<p>레벨 1(Level 1)은 수행된 프로세스(Performed Process)에 해당한다. 이 수준의 조직은 테스트를 실제로 수행한다. 예를 들어 블랙박스 테스트(Black-box Test)나 화이트박스 테스트(White-box Test) 기법을 적용하여 테스트를 진행하고, 그 결과도 존재한다. 하지만 여전히 많은 조직들이 레벨 0과 레벨 1 사이에 머물러 있다.</p>
<p>우리가 지향해야 할 수준은 적어도 레벨 2(Level 2)이다. 레벨 2는 관리된 프로세스(Managed Process)를 의미한다. 이 수준에서는 PDCA Cycle에 기반하여 테스트 계획(Test Planning), 테스트 설계(Test Design), 테스트 수행(Test Execution), 테스트 평가와 개선(Test Evaluation and Improvement) 과정이 지속적으로 운영된다. 즉, 테스트가 단순히 수행되는 것에 그치지 않고, 관리 관점에서 체계적으로 진행되는 조직이라고 할 수 있다.</p>
<p>레벨 3(Level 3)은 정립된 프로세스(Established Process)에 해당한다. 레벨 2와 레벨 3은 적용 범위에서 차이가 있다. 레벨 2가 개별 프로젝트(Project) 단위에서 테스트 프로세스를 관리하는 수준이라면, 레벨 3은 조직(Organization) 차원에서 표준 테스트 프로세스(Standard Test Process)가 정립되어 있는 수준이다. 즉, 특정 프로젝트에만 적용되는 것이 아니라 조직 전체에서 공통적으로 활용할 수 있는 테스트 프로세스가 마련되어 있는 단계이다.</p>
<p>레벨 4(Level 4)는 예측 가능한 프로세스(Predictable Process)에 해당한다. 이 수준에서는 한두 개의 테스트 케이스(Test Case) 결과만으로 품질을 판단하는 것이 아니라, 여러 유사 프로젝트에서 수집한 정량적 데이터(Quantitative Data)를 기반으로 품질을 예측한다. 예를 들어 20~30개 이상의 프로젝트 결과를 모아 표준화된 프로세스(Standardized Process)를 분석하고, 이를 통해 향후 프로젝트의 품질을 예측하는 방식이다.</p>
<p>레벨 5(Level 5)는 최적화된 프로세스(Optimizing Process)에 해당한다. 레벨 5에서는 레벨 4에서 예측한 결과를 바탕으로 문제점을 분석하고, 테스트 프로세스를 지속적으로 개선(Continuous Improvement)한다. 즉, 정량적 데이터와 예측 결과를 활용하여 조직의 테스트 역량을 계속 발전시키는 단계라고 볼 수 있다.</p>
<p>다만 레벨 4와 레벨 5는 매우 이상적인 수준에 가깝다. 따라서 모든 조직이 반드시 이 단계까지 도달해야 한다고 보기보다는, 실무에서는 우선 레벨 2를 목표로 하는 것이 현실적이다. 즉, 단순히 프로젝트를 수행하는 수준을 넘어 PDCA Cycle에 기반하여 테스트를 계획하고, 설계하고, 수행하고, 평가하고 개선하는 관리된 프로세스(Managed Process)를 갖추는 것이 중요하다.</p>
<p>이후 레벨 2가 안정적으로 이루어진다면, 조직 차원의 표준 테스트 프로세스를 정립하는 레벨 3(Level 3)까지 고려해볼 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/36490010-cbe0-48d3-880a-d5a61e3d682a.png" alt="" style="display:block;margin:0 auto" />

<h3>테스트 평가와 개선의 주요 활동: 테스트 결과 분석 및 평가</h3>
<h3>(Main Activities of Test Evaluation and Improvement: Test Result Analysis and Evaluation)</h3>
<p>테스트 평가와 개선(Test Evaluation and Improvement)의 주요 활동 중 하나는 테스트 결과 분석 및 평가(Test Result Analysis and Evaluation)이다. 이는 수행된 테스트가 얼마나 효과적이었는지, 그리고 그 효과를 달성하기 위해 어느 정도의 비용이 소요되었는지를 평가하는 과정이다.</p>
<p>먼저 테스트 효과성(Test Effectiveness)은 테스트를 통해 얼마나 많은 결함(Defect)을 검출했는지를 기준으로 평가한다. 테스트의 목적은 결함을 찾는 데 있으므로, 좋은 테스트 케이스(Test Case)는 결함을 잘 발견할 수 있어야 한다. 테스트 케이스를 여러 개 작성했을 때, 테스트 결과가 성공(Pass)이라는 것은 예상 결과(Expected Result)와 실제 결과(Actual Result)가 일치했다는 의미이다. 반대로 실패(Fail)는 예상한 결과가 나오지 않았다는 뜻이며, 이 경우 결함을 발견했을 가능성이 있다.</p>
<p>따라서 테스트 케이스 실패율(Test Case Failure Rate)은 테스트 효과성을 평가하는 지표로 활용될 수 있다. 테스트 케이스 실패율은 실패한 테스트 케이스 수(Number of Failed Test Cases)를 전체 테스트 케이스 수(Total Number of Test Cases)로 나눈 뒤 100을 곱해 계산한다.</p>
<p>예를 들어 전체 테스트 케이스가 30개이고, 그중 실패한 테스트 케이스가 15개라면 테스트 케이스 실패율은 다음과 같다.</p>
<p><code>15개 / 30개 × 100% = 50%</code></p>
<p>이는 전체 테스트 케이스 중 50%가 실패했으며, 그만큼 결함 발견과 연결될 수 있음을 의미한다. 즉, 테스트 케이스 실패율은 테스트가 결함을 얼마나 찾아냈는지를 보여주는 효과성 지표로 볼 수 있다.</p>
<p>다음으로 테스트 효율성(Test Efficiency)은 테스트 효과를 달성하는 데 소요된 비용이나 공수(Effort)를 함께 고려하는 지표이다. 같은 효과를 얻더라도 더 적은 시간과 비용으로 수행했다면 더 효율적인 테스트라고 볼 수 있다.</p>
<p>예를 들어 테스트 효과성(Test Effectiveness), 즉 테스트 케이스 실패율이 50%이고, 테스트 소요 비용 또는 공수(Test Effort)가 5시간이라고 하자. 이때 테스트 효율성(Test Efficiency)은 테스트 효과성을 테스트 소요 비용으로 나누어 계산할 수 있다.</p>
<p><code>테스트 효율성(Test Efficiency) = 테스트 효과성(Test Effectiveness) / 테스트 소요 비용(Test Effort)</code></p>
<p>따라서 위 예시는 다음과 같이 계산된다.</p>
<p><code>50% / 5시간 = 시간당 10%</code></p>
<p>이는 테스트를 수행할 때 시간당 약 10% 수준의 결함 발견 효과를 얻을 수 있는 테스트가 수행되었다고 해석할 수 있다.</p>
<p>이처럼 테스트 결과 분석 및 평가(Test Result Analysis and Evaluation)는 테스트 케이스 실패율(Test Case Failure Rate), 테스트 효과성(Test Effectiveness), 테스트 효율성(Test Efficiency)과 같은 지표를 기반으로 테스트 프로세스(Test Process)가 얼마나 적절하게 수행되었는지를 평가하는 과정이다. 지표를 활용하면 테스트 활동의 성과를 정량적으로 파악할 수 있고, 이후 테스트 프로세스를 개선하는 데 필요한 근거로 활용할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/238cbd3b-7983-4416-a2e5-5d967edff3e6.png" alt="" style="display:block;margin:0 auto" />

<h3>테스트 평가와 개선의 주요 활동: 대책 수립 및 시정안 권고</h3>
<h3>(Main Activities of Test Evaluation and Improvement: Countermeasure Planning and Corrective Action Recommendation)</h3>
<p>테스트 평가와 개선(Test Evaluation and Improvement)의 주요 활동에는 대책 수립(Countermeasure Planning)과 시정안 권고(Corrective Action Recommendation)가 포함된다. 이는 테스트 효과성(Test Effectiveness)과 테스트 효율성(Test Efficiency)을 높이기 위해 테스트 계획(Test Planning), 테스트 설계(Test Design), 테스트 수행(Test Execution) 등의 활동에 대한 개선 방안을 마련하고, 그에 따라 조치를 수행하는 과정이다.</p>
<p>먼저 테스트 계획(Test Planning)에 대한 개선 방안을 살펴볼 수 있다. 테스트 효과성 지표(Test Effectiveness Metric)에는 테스트 케이스 실패율(Test Case Failure Rate)이 있다. 테스트 케이스 실패율이 낮다는 것은 결함을 검출하지 못한 테스트 케이스가 많다는 의미로 볼 수 있다. 엄밀히 말하면 테스트 케이스(Test Case)가 결함을 잘 찾지 못하도록 만들어졌다는 뜻이므로, 테스트 케이스의 품질을 다시 검토해야 한다.</p>
<p>이 경우 개선 방안으로 위험 분석(Risk Analysis)을 철저히 수행할 수 있다. 결함이 발생할 가능성이 높은 기능이나 품질에 큰 영향을 미치는 기능을 중심으로 테스트하면 테스트의 효과성을 높일 수 있다. 따라서 기존에 위험 분석을 제대로 수행하지 않았던 조직이라면, 테스트 효과를 높이기 위해 테스트 계획 단계에서 위험 분석을 반드시 진행하도록 권고할 수 있다.</p>
<p>다음으로 테스트 설계(Test Design)에 대한 개선 방안을 살펴볼 수 있다. 테스트 소요 비용(Test Cost)이나 공수(Effort)가 크다는 것은 많은 시간과 인력을 들였음에도 테스트가 충분히 효과적이지 않을 수 있다는 의미이다. 그 원인을 분석해 보니 사람이 반복적인 테스트를 일일이 수동(Manual)으로 수행하고 있었다면, 테스트 자동화(Test Automation)를 개선 방안으로 제시할 수 있다.</p>
<p>테스트 자동화 도구(Test Automation Tool)를 도입하면 사람이 반복적으로 수행하던 부분을 도구 기반으로 실행할 수 있다. 이를 통해 반복 작업에 드는 시간과 비용을 줄이고, 사람은 더 창의적이고 생산적인 업무에 집중할 수 있다.</p>
<p>즉, 대책 수립과 시정안 권고는 테스트 평가 결과를 바탕으로 테스트 계획, 설계, 수행 활동의 문제점을 찾고, 테스트 효과성과 효율성을 높이기 위한 개선 방안을 제시하는 활동이다. 테스트 자동화(Test Automation)는 이러한 개선 방안 중 하나이며, 이후 주차에서 더 자세히 다룰 예정이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/3cf01c76-4109-41ed-8b31-37683b5fb794.png" alt="" style="display:block;margin:0 auto" />

<h3>테스트 프로세스 전체 요약 (Summary of Test Process)</h3>
<p>지금까지 테스트(Test)를 평가하고 개선하는 과정까지 살펴보았다. 전체 내용을 간략히 정리하면 다음과 같다.</p>
<p>테스트 표준(Test Standard)에서는 테스트 프로세스(Test Process)를 크게 세 가지 범주로 나눈다. 첫 번째는 조직 차원의 테스트 프로세스(Organizational Test Process), 두 번째는 테스트 관리 프로세스(Test Management Process), 세 번째는 동적 테스트 프로세스(Dynamic Test Process)이다.</p>
<p>먼저 조직 차원의 테스트 프로세스(Organizational Test Process)는 전체 조직 차원에서 테스트 정책(Test Policy)과 테스트 전략(Test Strategy)을 수립하는 활동이다. 하지만 처음 수립한 정책과 전략이 항상 완전한 것은 아니기 때문에, 실제 테스트를 수행하면서 얻은 피드백(Feedback)을 바탕으로 계속 업데이트하고 개선해야 한다.</p>
<p>다음으로 프로젝트 차원의 테스트 관리 프로세스(Test Management Process)는 테스트를 체계적으로 수행하기 위해 테스트 계획(Test Planning)을 세우는 과정이다. 프로젝트 단위에서 어떤 테스트를 어떻게 수행할 것인지 계획하고, 테스트가 일정과 목표에 맞게 진행될 수 있도록 관리한다.</p>
<p>실제로 테스트를 수행하는 부분은 동적 테스트 프로세스(Dynamic Test Process)에 해당한다. 동적 테스트 프로세스는 테스트 설계 및 구현(Test Design and Implementation), 테스트 환경 구성 및 유지(Test Environment Setup and Maintenance), 테스트 수행(Test Execution), 테스트 결과 보고(Test Result Reporting)로 이루어진다. 이 과정에서 테스트 케이스(Test Case)를 실행하고, 결과를 기록하며, 결함(Defect)을 식별하고 보고한다.</p>
<p>동적 테스트 프로세스에서 나온 결과와 데이터는 다시 테스트 관리 프로세스로 전달된다. 이후 테스트 관리 차원에서는 이 데이터를 측정(Measurement)하고, 모니터링(Monitoring)하며, 필요한 경우 제어(Control)한다. 테스트가 완료되면 그 결과는 다시 조직 차원으로 전달되어, 조직의 테스트 정책과 전략을 지속적으로 개선하는 데 활용된다.</p>
<p>즉, 테스트 프로세스는 단순히 테스트를 수행하고 끝나는 것이 아니라, 조직 차원의 정책과 전략, 프로젝트 차원의 계획과 관리, 실제 테스트 수행과 결과 보고가 서로 연결되어 순환하는 구조이다. 테스트를 통해 얻은 데이터와 피드백은 다시 상위 프로세스로 전달되고, 이를 기반으로 테스트 프로세스 전체가 지속적으로 개선된다.</p>
<p>이 내용은 그동안 여러 주차에 걸쳐 학습한 테스트 프로세스의 세부 내용을 종합적으로 정리한 요약 자료로 이해할 수 있다.</p>
]]></content:encoded></item><item><title><![CDATA[Linux/Unix Lecture Notes: Managing Processes and CPU Resources]]></title><description><![CDATA[1️⃣ 프로세스 개요(Overview of Process)
프로세스(Process)와 프로그램(Program)의 차이
첫 번째로 다루는 개념은 프로세스(Process) 와 프로그램(Program) 의 차이이다.일상적으로는 두 용어를 크게 구분하지 않고 사용하는 경우가 많다. 하지만 전공 수업이나 운영체제(Operating System) 분야에서는 두 개념을]]></description><link>https://heesu.tech/linux-unix-lecture-notes-managing-processes-and-cpu-resources</link><guid isPermaLink="true">https://heesu.tech/linux-unix-lecture-notes-managing-processes-and-cpu-resources</guid><dc:creator><![CDATA[Heesu Noh]]></dc:creator><pubDate>Sun, 10 May 2026 15:28:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/8312f7ae-c7ee-46d1-9247-87c893ad4e92.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>1️⃣ 프로세스 개요(Overview of Process)</h1>
<h3>프로세스(Process)와 프로그램(Program)의 차이</h3>
<p>첫 번째로 다루는 개념은 <strong>프로세스(Process)</strong> 와 <strong>프로그램(Program)</strong> 의 차이이다.<br />일상적으로는 두 용어를 크게 구분하지 않고 사용하는 경우가 많다. 하지만 전공 수업이나 운영체제(Operating System) 분야에서는 두 개념을 구분해서 사용한다.</p>
<p>일반적인 사용에서는 큰 차이가 없어 보일 수 있지만, 시스템을 이해하기 위해서는 프로그램과 프로세스를 나누어 생각해야 한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/439de29e-d400-4826-94a1-c4c73cd68b5e.png" alt="" style="display:block;margin:0 auto" />

<h2>프로그램(Program)</h2>
<p>프로그램은 기본적으로 <strong>스토리지(Storage)</strong> 에 저장되어 있는 상태를 의미한다. 여기서 스토리지는 디스크, SSD, USB, CD와 같은 저장장치를 포함한다.</p>
<p>프로그램은 결국 이러한 저장장치 안에 보관되어 있는 <strong>명령어(Instruction)들의 모임</strong>이다.</p>
<p>즉, 엔지니어가 프로그램을 작성하고 저장하면, 그것은 파일(File)의 형태로 스토리지에 존재하게 된다.</p>
<p>교수님은 프로그램의 특징을 다음과 같이 설명했다.</p>
<ul>
<li><p>스토리지에서 제거되기 전까지 계속 존재한다.</p>
</li>
<li><p>파일 형태로 생성되고 보존된다.</p>
</li>
<li><p>스토리지(Storage)에 저장된 상태이다.</p>
</li>
<li><p>명령어들의 집합이다.</p>
</li>
<li><p>파일(File) 형태로 존재한다.</p>
</li>
<li><p>실행되기 전의 정적인 상태(static state)를 의미한다.</p>
</li>
<li><p>프로세스(Process)와 구분하기 위해 CPU와 RAM을 사용하지 않는다고 설명한다.</p>
</li>
</ul>
<p>여기서 “CPU와 RAM을 사용하지 않는다”는 표현은 절대적인 의미라기보다는, <strong>프로세스(Process)와 구분하기 위한 설명</strong>이라고 강조했다. 실제로는 프로그램을 작성하거나 저장하는 과정 자체가 컴퓨터 위에서 이루어지므로 CPU와 RAM이 완전히 사용되지 않는 것은 아니다. 다만 운영체제 개념에서는, 저장장치에 보관된 정적인 상태(static state)의 대상을 설명하기 위해 이렇게 표현한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/0d98f109-0348-4554-8216-9cbc282f2ab6.png" alt="" style="display:block;margin:0 auto" />

<h2>프로세스(Process)</h2>
<p>그렇다면 프로세스(Process)는 무엇을 의미할까?</p>
<p>일상적으로는 프로그램과 프로세스를 거의 같은 의미처럼 사용하는 경우가 많다. 하지만 운영체제(Operating System)를 배우는 과정에서는 두 개념을 구분해서 이해해야 한다. 프로그램이 단순히 저장되어 있는 상태라면, 프로세스는 실제로 실행되면서 활동하고 있는 상태를 의미한다. 교수님 설명에서 가장 중요하게 강조된 부분은 다음이다.</p>
<blockquote>
<p>프로세스는 CPU와 RAM을 사용하고 있으며, 그 안에서 존재하는 실행 단위이다.</p>
</blockquote>
<p>앞에서 프로그램은 스토리지(Storage)에 저장되어 있는 명령어들의 집합이라고 설명했다. 디스크(Disk), SSD, USB 같은 저장장치 안에 파일(File) 형태로 존재하는 것이 프로그램이다. 이 상태의 프로그램은 정적인 상태(static state)에 가깝다.</p>
<p>반면 프로세스는 저장되어 있던 프로그램이 실제로 실행되면서 CPU와 RAM 자원을 사용하는 상태를 의미한다. 즉, 프로그램이 움직이기 시작한 순간부터 프로세스라는 개념으로 바라보게 되는 것이다.</p>
<p>교수님은 이 부분을 설명하면서 “CPU와 RAM에 존재해야 프로세스라고 말할 수 있다”고 표현했다. 결국 프로세스라는 개념은 단순히 저장된 파일이 아니라, 시스템 자원을 사용하며 실제로 동작하고 있는 상태를 나타내기 위해 등장한 것이다.</p>
<hr />
<p>프로그램이 실행되는 과정도 함께 설명했다.</p>
<p>우리가 작성한 프로그램은 원래 스토리지에 저장되어 있다. 하지만 실행 버튼을 누르는 순간 운영체제(Operating System)는 그 프로그램을 메모리(RAM)로 옮긴다. 교수님은 이 과정을 “로드(load)”라고 설명했다.</p>
<p>여기서 load라는 표현은 단순히 데이터를 읽는다는 의미보다, 실제로 짐을 옮기는 것에 가까운 개념이라고 설명했다. 예를 들어 창고에 있던 물건을 배송 차량으로 옮기는 것처럼, 스토리지에 저장되어 있던 프로그램 코드가 RAM으로 이동하는 것이다.</p>
<p>즉 프로그램 실행 과정은 다음과 같이 이해할 수 있다.</p>
<p>스토리지(Storage)에 저장되어 있던 프로그램이 실행 요청을 받으면, 운영체제가 해당 프로그램을 RAM으로 적재(load)한다. 이후 CPU는 RAM에 올라온 명령어와 데이터를 읽어 연산을 수행한다. 그리고 이 데이터들은 CPU 내부의 Cache나 Register 같은 구성 요소를 오가며 처리된다.</p>
<p>교수님은 특히 Register를 언급하면서, CPU 안에서 매우 빠르게 데이터를 주고받으며 연산이 이루어진다고 설명했다. 결국 이러한 과정이 반복되면서 화면 출력이나 계산 결과처럼 사용자가 직접 확인할 수 있는 결과가 만들어진다.</p>
<p>또한 프로세스는 메모리 안에서 여러 영역으로 나뉘어 동작한다고 설명했다. 대표적으로 다음과 같은 구조를 가진다.</p>
<ul>
<li><p>코드(Code)</p>
</li>
<li><p>데이터(Data)</p>
</li>
<li><p>스택(Stack)</p>
</li>
<li><p>힙(Heap)</p>
</li>
</ul>
<p>즉 프로세스는 단순히 “실행 중이다”라는 의미만 가지는 것이 아니라, 메모리 안에서 여러 영역을 구성하며 체계적으로 동작하는 실행 단위라고 볼 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/08d19083-a678-42ed-9fc4-d40c56fed6d9.png" alt="" style="display:block;margin:0 auto" />

<h3>동시 실행(Concurrent Execution)과 작업(Job)</h3>
<p>앞에서는 프로그램(Program)과 프로세스(Process)의 차이를 구분해서 살펴보았다.<br />이번에는 그 개념에서 이어져, 컴퓨터가 실제로 어떻게 여러 작업을 실행하는지와 Job이라는 표현에 대해 설명한다. 교수님은 먼저 하나의 질문을 던졌다.</p>
<blockquote>
<p>컴퓨터는 한 번에 하나의 프로그램만 실행할까, 아니면 동시에 여러 개를 실행할까?</p>
</blockquote>
<p>결론부터 말하면 컴퓨터는 여러 작업을 동시에 실행하고 있다. 스마트폰 역시 마찬가지이다.</p>
<p>우리가 스마트폰이나 컴퓨터를 사용할 때는 보통 하나의 화면만 집중해서 보고 있기 때문에, 마치 하나의 프로그램만 실행되는 것처럼 느껴질 수 있다. 하지만 실제 내부에서는 브라우저, 메신저, 음악 재생, 네트워크 처리, 운영체제 서비스 등 수많은 프로그램이 동시에 동작하고 있다.</p>
<p>물론 사용자가 직접 인터랙션(interaction)하고 있는 프로그램은 보통 하나처럼 보인다. 예를 들어 키보드를 입력하거나 화면을 터치할 때는 현재 활성화된 프로그램만 눈에 띈다. 그러나 동영상을 띄워두거나 여러 창(window)을 동시에 배치하면, 여러 프로그램이 함께 동작하고 있다는 사실을 더 쉽게 확인할 수 있다.</p>
<h3>Job(작업)의 의미</h3>
<p>이 과정에서 교수님은 <strong>Job(작업)</strong> 이라는 표현을 소개했다.</p>
<p>Job이라는 용어는 초기 컴퓨터 역사에서 유래했다고 설명했다. 과거에는 사람이 펀치카드(Punch Card)를 사용하여 컴퓨터에 작업을 요청했고, 컴퓨터는 이를 순서대로 처리하는 배치 처리(Batch Processing) 방식을 사용했다. 여기서 하나의 작업 단위를 Job이라고 부르게 된 것이다.</p>
<p>교수님은 컴퓨터 용어 중 상당수가 원래 사람이 일하던 환경에서 가져온 표현이라고 설명했다. 컴퓨터는 결국 사람의 일을 대신 처리하는 기계이기 때문에, 실제 인간 사회에서 사용되던 용어들이 컴퓨터 분야로 자연스럽게 넘어왔다는 것이다.</p>
<p>예를 들어 전공자들은 “함수(Function)”라는 말을 들으면 프로그래밍 개념을 먼저 떠올리지만, 다른 분야 사람들은 전혀 다른 의미를 생각할 수도 있다. 이처럼 컴퓨터 분야에서는 특정 단어가 거의 고유명사처럼 사용되지만, 원래는 일반적인 의미를 가진 용어였다는 점을 강조했다.</p>
<h3>하나의 Job과 여러 개의 프로세스</h3>
<p>교수님은 하나의 작업(Job)이 반드시 하나의 프로세스(Process)만 의미하는 것은 아니라고 설명했다. 하나의 작업(Job)은 하나 이상의 프로세스로 구성될 수 있다. 앞에서 프로세스는 CPU와 RAM을 사용하며 실제로 동작하고 있는 살아있는 상태라고 설명했다. 이러한 프로세스 여러 개를 하나로 묶어서 하나의 Job으로 볼 수 있다는 것이다.</p>
<ul>
<li><p>프로세스(Process) → 실제 실행 단위</p>
</li>
<li><p>잡(Job) → 사용자 입장에서 묶어서 바라보는 작업 단위라고 이해할 수 있다.</p>
</li>
</ul>
<h3>커널(Kernel) 관점과 사용자(User) 관점</h3>
<p>교수님은 프로세스와 잡을 구분하는 또 다른 기준으로 “누가 관리하느냐”를 설명했다. 프로세스(Process)는 운영체제의 커널(Kernel) 관점에서 추적하고 관리하는 대상이다. 반면 Job은 사용자(User)나 쉘(Shell) 관점에서 관리하는 작업 단위이다.</p>
<p>즉 운영체제 내부에서는 각각의 프로세스를 개별적으로 관리하지만, 사용자는 여러 프로세스를 하나의 작업처럼 다루게 되는 경우가 있다는 의미이다.</p>
<p>이 때문에 각각 다른 식별자(identifier)를 사용한다.</p>
<ul>
<li><p>프로세스 → PID(Process ID)</p>
</li>
<li><p>작업(Job) → JID(Job ID)</p>
</li>
</ul>
<p>교수님은 여기서 “왜 이렇게 비슷한 개념을 계속 세분화하는가?”라는 의문이 생길 수 있다고 설명했다. 하지만 전공이나 전문 분야로 갈수록 더욱 세밀한 구분이 필요해진다고 말했다.</p>
<p>예를 들어 우리가 일상적으로는 모두 “쌀”이라고 부르지만, 실제로는 품종과 특성이 매우 다양하게 나뉘는 것과 비슷한 개념이라고 설명했다.</p>
<h3>백그라운드(Background) 실행</h3>
<p>마지막으로 교수님은 Job이라는 표현이 보통 백그라운드(background)에서 실행되는 프로그램을 의미하는 경우가 많다고 설명했다. 여기서 백그라운드는 사용자가 직접 화면에서 조작하지 않아도 뒤에서 계속 실행되는 상태를 의미한다.</p>
<p>그리고 이에 대비되는 개념이 바로 포어그라운드(Foreground)이다. Foreground는 사용자가 현재 직접 상호작용(interaction)하고 있는 실행 상태를 의미하며, 이에 대한 내용은 다음 슬라이드에서 이어서 설명한다고 언급했다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/d77ae6b4-7fe9-4f9b-a71e-f3931ec9b032.png" alt="" style="display:block;margin:0 auto" />

<h3>동시 실행과 포어그라운드(Foreground)</h3>
<p>프로그램과 프로세스(Process)의 차이를 이해했다면, 이제 실제 컴퓨터가 어떻게 여러 작업을 처리하는지 자연스럽게 이어서 생각해볼 수 있다. 컴퓨터는 한 번에 하나의 프로그램만 실행하는 것처럼 보이기도 하지만, 실제로는 여러 작업이 동시에 실행되고 있다.</p>
<p>스마트폰만 보더라도 쉽게 알 수 있다. 사용자는 현재 하나의 앱 화면만 보고 터치하고 있을 수 있지만, 그 뒤에서는 음악 재생, 네트워크 통신, 알림 처리, 메신저, 운영체제 서비스 등 수많은 프로그램이 함께 동작하고 있다. 컴퓨터 역시 마찬가지이다.</p>
<p>다만 사용자가 직접 보고 상호작용(interaction)하는 프로그램은 보통 하나처럼 느껴진다. 특히 리눅스(Linux) 터미널(Terminal) 환경에서는 이러한 특징이 더욱 뚜렷하게 보인다.</p>
<p>리눅스 서버에 접속하면 흔히 검은 화면의 문자 기반 환경을 보게 된다. 사용자는 쉘(Shell)에 명령어(command)를 입력하고 엔터(Enter)를 누른다. 그러면 프로그램이 실행되고, 실행이 끝나면 다시 입력을 기다리는 상태로 돌아간다. 이때 화면에는 깜빡이는 커서(cursor)가 나타난다.</p>
<p>보통 우리가 예제로 실행하는 프로그램들은 수행 시간이 매우 짧다. 그래서 명령을 입력하면 곧바로 결과가 출력되고 다시 프롬프트(prompt)가 나타난다. 이러한 흐름에 익숙해지다 보니, 자연스럽게 “현재 실행 중인 작업이 끝나야 다음 입력을 할 수 있다”는 방식으로 터미널 환경을 이해하게 된다.</p>
<p>즉 기본적인 쉘 환경은 다음과 같은 흐름으로 동작한다.</p>
<ol>
<li><p>사용자가 명령어를 입력한다.</p>
</li>
<li><p>프로그램이 실행된다.</p>
</li>
<li><p>실행이 종료된다.</p>
</li>
<li><p>다시 입력 대기 상태로 돌아간다.</p>
</li>
</ol>
<p>이 구조만 보면 하나의 터미널에서는 동시에 여러 작업을 할 수 없는 것처럼 느껴질 수 있다. 하지만 실제로는 그렇지 않다.</p>
<h3>Ctrl + Z 와 작업(Job) 제어</h3>
<p>쉘 환경에서는 실행 중인 프로그램을 일시적으로 멈출 수도 있다. 대표적인 방법이 <code>Ctrl + Z</code> 이다.</p>
<p>실행 중인 프로그램에서 <code>Ctrl + Z</code>를 입력하면 현재 작업이 중단(suspend)된다. 중요한 점은 프로그램이 종료되는 것이 아니라, 실행이 멈춘 상태로 유지된다는 것이다.</p>
<p>중단된 작업들은 <code>jobs</code> 명령어를 통해 확인할 수 있다.</p>
<p>이러한 방식은 GUI(Graphical User Interface)에 익숙한 사용자들에게는 다소 낯설 수 있다. 일반적인 데스크톱 환경에서는 여러 창(window)을 띄워놓고 프로그램을 동시에 실행하기 때문에, foreground나 background를 직접 의식하지 않는 경우가 많다.</p>
<p>하지만 현재 다루는 CLI(Command Line Interface) 환경은 하나의 터미널 창 안에서 여러 작업을 관리해야 하는 구조이다. 그래서 작업을 어떻게 중단하고, 다시 실행하며, 앞에서 실행할지 뒤에서 실행할지를 구분하는 개념이 중요해진다.</p>
<h3>Foreground와 Background</h3>
<p>이 과정에서 등장하는 개념이 바로 Foreground와 Background이다.</p>
<p>Foreground(포어그라운드)는 사용자가 현재 직접 상호작용하고 있는 상태를 의미한다. 키보드 입력을 받고 화면에 결과를 출력하며, 사용자가 “지금 보고 있는 작업”이라고 생각하면 이해하기 쉽다. 반면 Background(백그라운드)는 사용자가 직접 보고 있지는 않지만 뒤에서 계속 실행되고 있는 상태를 의미한다.</p>
<p>예를 들어 현재 터미널에서 직접 실행 중인 프로그램은 foreground 작업이고, 뒤에서 계속 동작하도록 돌려놓은 프로그램은 background 작업이 된다.</p>
<p>이처럼 하나의 작업 공간 안에서도 “앞에서 실행되는 작업”과 “뒤에서 실행되는 작업”이 나뉘어 존재하게 된다.</p>
<h3><code>fg</code> 와 <code>bg</code> 명령어</h3>
<p>중단된 작업은 다시 실행할 수도 있다.</p>
<p><code>fg</code> 명령어를 사용하면 작업을 foreground 상태로 가져와 다시 실행할 수 있고, <code>bg</code> 명령어를 사용하면 background 상태에서 계속 실행되도록 만들 수 있다.</p>
<p>즉 <code>Ctrl + Z</code>로 잠시 멈춘 프로그램을 다시 앞에서 실행할 수도 있고, 사용자가 다른 작업을 계속할 수 있도록 뒤에서 실행시킬 수도 있는 것이다.</p>
<p>이러한 기능은 하나의 터미널 환경 안에서도 여러 작업을 효율적으로 관리할 수 있게 해준다.</p>
<p>Job 번호(Job ID)</p>
<p>쉘(Shell)은 여러 작업(Job)을 동시에 관리할 수 있기 때문에, 각 작업에는 번호가 부여된다. 이를 Job ID(JID)라고 한다.</p>
<p>사용자는 이 번호를 기준으로 어떤 작업이 실행 중인지, 어떤 작업이 중단되었는지, 어떤 작업을 foreground나 background로 전환할 것인지 관리하게 된다.</p>
<p>앞에서 프로세스(Process)가 운영체제 커널(Kernel) 관점에서 관리되는 실행 단위였다면, Job은 사용자와 쉘 관점에서 관리되는 작업 단위라고 볼 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/1b8688c8-5890-4dd8-99ce-e5991275a438.png" alt="" style="display:block;margin:0 auto" />

<h3>동시 실행과 백그라운드(Background)</h3>
<p>백그라운드(Background)는 사용자가 직접 보고 입력하지는 않지만, 뒤에서 계속 실행되고 있는 작업을 의미한다. 즉 백그라운드 프로세스(background process)로 실행되는 프로그램을 백그라운드 작업이라고 부른다.</p>
<p>리눅스(Linux)나 유닉스(UNIX) 쉘(Shell)에서는 명령어 뒤에 <code>&amp;</code>를 붙이면 프로그램을 백그라운드로 실행할 수 있다.</p>
<p>예를 들어 일반적으로 프로그램을 실행하면 해당 작업이 끝날 때까지 터미널을 사용할 수 없다. 하지만:</p>
<pre><code class="language-plaintext">command &amp;
</code></pre>
<p>처럼 실행하면 프로그램은 뒤에서 계속 동작하고, 사용자는 바로 프롬프트(prompt)를 다시 받아 다른 명령을 입력할 수 있게 된다. 즉 터미널의 제어권(control)을 다시 돌려받는 것이다.</p>
<p>하지만 백그라운드 작업도 화면 출력(output)을 할 수 있다. 프로그램이 표준출력(Standard Output)을 사용하면, 뒤에서 실행 중이어도 터미널 화면에 내용이 출력될 수 있다.</p>
<p>이 경우 사용자가 다른 작업을 하고 있는데 갑자기 로그나 메시지가 화면에 출력되어 작업 흐름을 방해할 수 있다.</p>
<p>이를 막기 위해 사용하는 설정이:</p>
<pre><code class="language-plaintext">stty tostop
</code></pre>
<p>이다. 이 설정을 사용하면 백그라운드 작업이 터미널 화면에 출력하려고 할 때 해당 작업을 정지(stop)시킬 수 있다. 즉 백그라운드 프로그램이 터미널을 함부로 사용하는 것을 막는 역할을 한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/ca1e65f6-f941-43b1-9a2b-d06875ee8b4d.png" alt="" style="display:block;margin:0 auto" />

<h3>동시 실행과 데몬(Daemon)</h3>
<p>데몬(Daemon)은 사용자가 직접 상호작용(interaction)하지 않는 상태에서 뒤에서 계속 실행되는 프로그램을 의미한다. 즉 사용자가 키보드로 입력하거나 화면을 직접 조작하지 않아도 시스템 내부에서 지속적으로 동작하는 프로세스(process)이다.</p>
<p>보통 데몬은 백그라운드(background) 프로세스 형태로 실행된다. 사용자는 존재를 크게 의식하지 않지만, 시스템 기능을 유지하거나 특정 서비스를 제공하기 위해 계속 동작하고 있다.</p>
<p>예를 들어 서버(server) 기능이나 네트워크(network) 관련 서비스들도 대부분 이러한 방식으로 동작한다.</p>
<p>데몬 프로그램은 관습적으로 이름 끝에 <code>d</code>가 붙는 경우가 많다. 예를 들어 일반 프로그램이:</p>
<pre><code class="language-plaintext">hello.c
</code></pre>
<p>처럼 작성된다면, 데몬 형태로 동작하는 프로그램은:</p>
<pre><code class="language-plaintext">hellod
</code></pre>
<p>또는 이름 끝에 <code>d</code>를 붙이는 방식으로 표현하는 경우가 많다. 다만 이는 어디까지나 관습(convention)일 뿐이며, 반드시 그렇게 해야 하는 것은 아니다.</p>
<p>또한 데몬은 보통 시스템이 시작될 때부터 함께 실행되며, 오랫동안 지속적으로 동작한다. 이 과정에서 관리자 권한인 root 권한으로 실행되는 경우도 많다.</p>
<p>즉 데몬은:</p>
<ul>
<li><p>사용자가 직접 조작하지 않고</p>
</li>
<li><p>백그라운드에서 실행되며</p>
</li>
<li><p>시스템 시작과 함께 동작하고</p>
</li>
<li><p>장시간 계속 실행되는 프로그램이라고 이해할 수 있다.</p>
</li>
</ul>
<hr />
<h3>📌데몬(Daemon)과 백그라운드(Background)의 차이</h3>
<p>데몬(Daemon)과 백그라운드(Background)는 모두 뒤에서 실행된다는 공통점이 있지만, 완전히 같은 개념은 아니다.</p>
<p>백그라운드 프로세스는 단순히 사용자가 직접 제어하지 않고 뒤에서 실행되고 있는 모든 프로그램을 의미한다. 예를 들어 <code>&amp;</code>를 붙여 실행한 프로그램이나, 터미널에서 직접 보고 있지 않지만 계속 동작 중인 작업들이 모두 백그라운드에 해당할 수 있다.</p>
<p>반면 데몬(Daemon)은 그중에서도 특정한 특징을 가진 프로그램을 따로 부르는 이름이다. 즉 백그라운드 프로세스라는 큰 범주 안에 데몬이라는 개념이 포함된다고 볼 수 있다. 데몬은 단순히 “뒤에서 실행되는 프로그램”이 아니라, 시스템 기능을 위해 계속 동작하는 백그라운드 프로그램이라고 이해할 수 있다.</p>
<p>또 하나 중요한 점은 용어 사용에서 “프로그램”과 “프로세스”가 혼용된다는 것이다.</p>
<p>데몬을 설명할 때도 “백그라운드 프로세스로 실행되는 프로그램”이라는 표현을 사용했는데, 이는 실제로는 같은 대상을 서로 다른 관점에서 부르는 것이다.</p>
<ul>
<li><p>프로그램(Program) → 저장된 실행 파일</p>
</li>
<li><p>프로세스(Process) → 실행 중인 상태</p>
</li>
</ul>
<p>하지만 실제 설명에서는 두 용어가 문맥에 따라 혼용되는 경우가 많다. 데몬 역시 “프로그램”이라고 부르기도 하고 “프로세스”라고 부르기도 한다.</p>
<p>결론적으로 정리하면 다음과 같다.</p>
<ul>
<li><p>백그라운드: 뒤에서 실행되는 모든 작업</p>
</li>
<li><p>데몬: 그중에서도 시스템 서비스처럼 지속적으로 동작하는 특수한 백그라운드 프로세스</p>
</li>
<li><p>프로그램/프로세스: 문맥에 따라 혼용되지만, 엄밀히는 실행 전/실행 중 상태의 차이</p>
</li>
</ul>
<p>이처럼 운영체제(Operating System) 개념에서는 용어가 엄격하게 나뉘어 있지만, 실제 설명이나 시험에서는 문맥에 따라 서로 섞여 사용되기도 한다. 따라서 “같은 의미로도 쓰이지만, 구분하면 다르다”는 관점으로 이해하는 것이 중요하다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/5f50f8c4-df2f-4710-b615-376bada0a468.png" alt="" style="display:block;margin:0 auto" />

<h3>프로세스 정보 확인: ps(process status) (1/3)</h3>
<p><code>ps(process status)</code> 명령어는 현재 실행 중인 프로세스(Process)의 상태를 확인하는 데 사용된다. 즉, 특정 시점에서 시스템 안에서 어떤 프로세스들이 동작하고 있는지를 보여주는 도구이다.</p>
<h3>PID (Process ID)</h3>
<p>각 프로세스는 생성될 때 고유한 ID를 부여받는다. 이를 PID(Process ID)라고 한다. 컴퓨터는 기본적으로 모든 대상을 숫자로 구분한다. 사람은 이름으로 구분하는 것이 익숙하지만, 컴퓨터는 효율적인 처리를 위해 숫자 기반으로 식별한다. 프로세스도 마찬가지로 각각 고유한 숫자 ID를 가진다. 이 PID는 프로세스가 생성되는 순서에 따라 부여되며, 시스템 환경에 따라 표현할 수 있는 범위가 달라진다. 예를 들어 32비트 시스템과 64비트 시스템은 처리 가능한 숫자의 범위와 관리 방식이 다를 수 있다.</p>
<h3>PID 범위와 관리</h3>
<p>리눅스(Linux)에서는 PID의 <strong>최대값</strong>을 확인할 수 있다.</p>
<pre><code class="language-plaintext">cat /proc/sys/kernel/pid_max
</code></pre>
<p>이 값은 시스템 설정에 따라 달라지며, 생성 가능한 PID의 최대 범위를 의미한다.</p>
<p>예를 들어 최대값이 100이라고 가정했을 때, 프로세스가 계속 생성되어 100을 초과하면 단순히 0으로 돌아가는 방식이 아니라, 사용 가능한 다른 번호를 찾아 할당하게 된다.</p>
<p>즉 PID는 순환 구조처럼 보일 수 있지만, 실제로는 시스템이 사용 가능한 번호를 재배치하는 방식으로 관리된다.</p>
<p>PID 0과 1</p>
<p>리눅스 시스템에는 특별한 PID가 존재한다.</p>
<ul>
<li><p>PID 0: swapper / idle task (kernel thread)</p>
</li>
<li><p>PID 1: init (또는 systemd 등 초기 사용자 공간 프로세스)</p>
</li>
</ul>
<p>PID 0은 시스템 내부에서 CPU가 할 일이 없을 때 동작하는 idle 상태와 관련된 커널(kernel) 프로세스이다. 일반적으로 사용자에게 직접 보이는 프로세스는 아니다.</p>
<p>PID 1은 시스템이 부팅될 때 가장 먼저 실행되는 기본 프로세스로, 이후 다른 모든 프로세스의 시작점이 된다.</p>
<h3>부모-자식 구조 (PPID)</h3>
<p>프로세스에는 부모-자식 관계가 존재한다. 이를 나타내는 것이 <code>PPID(Parent Process ID)</code>이다.</p>
<p>즉 새로운 프로세스는 기존 프로세스에 의해 생성되며, 이 관계를 통해 시스템은 프로세스 구조를 트리(tree) 형태로 관리한다.</p>
<p>ps 명령 출력 정보</p>
<p><code>ps</code> 명령을 실행하면 다음과 같은 정보들이 함께 출력된다.</p>
<ul>
<li><p>PID (Process ID)</p>
</li>
<li><p>TTY (Terminal Type)</p>
</li>
<li><p>TIME (CPU 사용 시간)</p>
</li>
<li><p>CMD / COMMAND (실행된 명령어)</p>
</li>
</ul>
<p>이 정보들을 통해 현재 어떤 프로세스가 실행되고 있는지, 어떤 터미널에서 실행되었는지, 어떤 명령으로 시작되었는지를 확인할 수 있다.</p>
<h3>의미 정리</h3>
<p><code>ps</code> 명령은 단순히 프로세스 목록을 보여주는 것이 아니라, 시스템이 현재 어떤 작업들을 어떤 PID로 관리하고 있는지를 확인할 수 있는 도구이다.</p>
<p>즉 컴퓨터 내부에서는 모든 실행 단위가 숫자(PID)로 관리되고 있으며, 이를 통해 프로세스를 추적하고 제어할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a9b2f51d-f58b-4da9-9ac7-d70e9749d025.png" alt="" style="display:block;margin:0 auto" />

<h3>ps 추가 설명 (Additional Explanation of ps (process status)) (2/3)</h3>
<p><code>ps(process status)</code> 명령어를 사용하면 현재 실행 중인 프로세스(Process)의 다양한 정보를 확인할 수 있다. 단순히 어떤 프로세스가 실행 중인지뿐만 아니라, 언제 시작되었고 어떤 상태로 동작하고 있는지도 함께 볼 수 있다.</p>
<p>그중 <code>STIME</code> 또는 <code>START</code> 항목은 프로세스의 시작 시간을 의미한다. 최근에 실행된 프로세스는 시간 형태로 표시되지만, 24시간 이상 지난 프로세스는 시간 대신 날짜 형태로 출력된다.</p>
<p>예를 들어 어떤 프로세스가 아주 오랫동안 실행 중이라고 해서, <code>ps</code> 명령이 “3000시간째 실행 중” 같은 정보를 자세하게 보여주지는 않는다. 애초에 <code>ps</code>는 현재 시점의 상태를 간단하게 확인하기 위한 도구이기 때문이다. 따라서 오래 실행된 프로세스는 “언제 시작되었는가” 정도만 표시해주는 방식으로 동작한다.</p>
<p>또 다른 중요한 항목은 <code>STAT</code>이다. 이는 현재 프로세스의 상태(state)를 나타낸다.</p>
<p>프로세스는 단순히 실행 중인 것만이 아니라, 대기(waiting), 중단(stopped), 실행(running) 등 여러 상태를 가질 수 있다. <code>STAT</code> 항목은 이러한 현재 상태를 문자 형태로 보여준다.</p>
<p>특히 BSD 형식의 <code>ps</code> 출력에서는 추가적인 상태 표시가 함께 붙기도 한다.</p>
<p>예를 들어: <code>&lt;, N, L, s, l, +</code></p>
<p>같은 문자들이 나타날 수 있는데, 이는 프로세스의 우선순위(priority), 세션(session) 여부, 멀티스레드(multithread) 상태 등 다양한 특성을 나타낸다.</p>
<p>즉 <code>STAT</code>는 단순히 “실행 중인가?”만 표시하는 것이 아니라, 현재 프로세스가 어떤 특성을 가진 상태인지까지 표현하는 정보라고 볼 수 있다.</p>
<p>이러한 프로세스 상태 개념은 운영체제(Operating System) 수업에서 더 자세히 다루게 된다. 운영체제 이론에서는 보통 프로세스 상태를 3단계, 5단계 등으로 나누어 설명한다.</p>
<p>예를 들어:</p>
<ul>
<li>실행(Running), 준비(Ready), 대기(Waiting)</li>
</ul>
<p>같은 형태로 구분한다. 하지만 실제 리눅스(Linux) 시스템에서 확인하는 상태와 운영체제 교과서에서 배우는 개념은 완전히 같지는 않을 수 있다.</p>
<p>이는 실제 운영체제가 이론적인 모델을 그대로 사용하는 것이 아니라, 시스템 발전 과정에서 불필요한 기능이 제거되거나 새로운 기능이 추가되고 성능 개선이 이루어졌기 때문이다.</p>
<p>즉 운영체제 수업에서 배우는 상태 모델은 개념 이해를 위한 구조이고, 리눅스는 이를 실제 시스템 환경에 맞게 구현하고 발전시킨 결과라고 이해할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/286414e7-8f93-4461-895e-4267d997160b.png" alt="" style="display:block;margin:0 auto" />

<h3>Showing Current Process Status with ps (3/3)</h3>
<p><code>ps(process status)</code> 명령어는 현재 시점에서 실행 중인 프로세스(Process)의 상태를 보여준다. 이때 출력되는 정보에는 메모리 사용량, CPU 사용률, 프로세스 그룹 정보 등 다양한 항목이 포함된다.</p>
<h3>RSS (Resident Set Size)</h3>
<p><code>RSS(Resident Set Size)</code>는 현재 프로세스가 실제로 사용 중인 물리 메모리(physical memory)의 크기를 의미한다. 보통 1000-byte 단위로 표시된다.</p>
<p>즉 프로세스가 RAM 안에서 실제 점유하고 있는 메모리 양이라고 이해할 수 있다.</p>
<p>VSZ (Virtual Memory Size)</p>
<p><code>VSZ(Virtual Memory Size)</code>는 프로세스가 사용하는 가상 메모리(virtual memory)의 전체 크기를 의미한다. 일반적으로 1024-byte 단위로 표시된다.</p>
<p>이는 실제 물리 메모리만이 아니라, 프로세스가 사용할 수 있도록 할당된 가상 주소 공간까지 포함한 개념이다.</p>
<p>%CPU</p>
<p><code>%CPU</code>는 해당 프로세스가 사용하는 CPU 비율을 의미한다. 이는 CPU time과 실제 경과 시간(real time)의 비율을 기준으로 계산된다. 즉 특정 프로세스가 전체 시간 중 얼마나 CPU를 사용하며 동작했는지를 보여주는 값이다.</p>
<h3>%MEM</h3>
<p><code>%MEM</code>은 시스템의 전체 물리 메모리 대비 현재 프로세스의 RSS 비율을 의미한다. 즉 컴퓨터 전체 메모리 중에서 현재 프로세스가 얼마나 메모리를 사용하고 있는지를 퍼센트(%) 형태로 보여주는 항목이다.</p>
<p>PGID (Process Group ID)</p>
<p><code>PGID(Process Group ID)</code>는 프로세스 그룹(Process Group)을 구분하기 위한 번호이다.</p>
<p>만약 PID와 PGID가 같다면, 해당 프로세스는 프로세스 그룹의 리더(leader)라는 의미가 된다.</p>
<p>즉 하나의 프로세스 그룹 안에서도 중심이 되는 프로세스가 존재할 수 있다는 뜻이다.</p>
<h3>ps 명령의 특징</h3>
<p><code>ps</code> 명령어는 실시간으로 계속 업데이트되는 방식이 아니다. 사용자가 <code>ps</code>를 입력하고 엔터(Enter)를 누르는 순간, 바로 그 시점의 프로세스 상태만 출력한다. 즉 현재 상태를 “순간적으로 캡처(snapshot)”해서 보여주는 방식이다. 그래서 프로세스 상태가 빠르게 변하는 상황에서는:</p>
<blockquote>
<p>“왜 방금 본 상태와 다르지?”</p>
</blockquote>
<p>처럼 느껴질 수도 있다. 하지만 리눅스(Linux)에서는 프로세스 상태를 지속적으로 갱신해서 볼 수 있는 방법들도 함께 제공된다. 즉 <code>ps</code>는 현재 상태를 한 번 확인하는 용도이고, 실시간 모니터링을 위한 별도의 도구들도 존재한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/c0de4e47-388c-46b4-bdaf-773622a76f9a.png" alt="" style="display:block;margin:0 auto" />

<h3>ps 주요 옵션/선택 (Major ps Options)</h3>
<p><code>ps(process status)</code> 명령어는 단순히 <code>ps</code>만 입력해서 사용할 수도 있지만, 실제로는 다양한 옵션(option)을 함께 사용해 원하는 형태의 프로세스(Process) 정보를 확인하게 된다. 리눅스(Linux)나 유닉스(UNIX)에서는 보통 <code>-</code> 뒤에 알파벳 옵션을 붙여 사용한다.</p>
<hr />
<pre><code class="language-plaintext">ps -f
</code></pre>
<p><code>-f</code> 옵션은 full-format 형식으로 프로세스 정보를 출력한다. 기본 출력보다 더 많은 정보를 보여주며, 실행된 명령어의 인수(argument)까지 함께 확인할 수 있다. 즉 단순히 어떤 프로그램이 실행 중인지뿐만 아니라, 어떤 옵션과 함께 실행되었는지도 볼 수 있다.</p>
<pre><code class="language-plaintext">ps -u username
</code></pre>
<p><code>-u</code> 옵션은 특정 사용자의 모든 프로세스를 보여준다. 즉 어떤 사용자가 현재 어떤 작업을 하고 있는지 확인할 수 있다는 의미가 된다. 리눅스와 유닉스는 기본적으로 여러 사용자가 함께 사용하는 환경을 전제로 설계된 시스템이다. 그래서 기본 설정 상태에서는 다른 사용자의 정보에 접근 가능한 경우도 존재한다. 예를 들어 권한(permission)을 적절히 설정하지 않으면, 같은 시스템을 사용하는 다른 사람이 파일이나 작업 내용을 확인할 가능성도 있다. 따라서 시스템 관리자(admin)는 사용자 권한과 개인정보 보호 설정을 중요하게 관리해야 한다.</p>
<pre><code class="language-plaintext">ps -e
</code></pre>
<p><code>-e</code> 옵션은 시스템 전체의 모든 프로세스를 보여준다. 즉 현재 운영체제(Operating System) 안에서 실행 중인 전체 작업 목록을 확인할 수 있다.</p>
<pre><code class="language-plaintext">ps -t pts/0
</code></pre>
<p><code>-t</code> 옵션은 특정 터미널(terminal)에서 실행 중인 프로세스만 보여준다. 처음에는 “특정 터미널만 따로 볼 필요가 있을까?”라는 생각이 들 수 있지만, 실제로는 여러 터미널 창을 동시에 사용하는 경우가 많다. 예를 들어 하나의 컴퓨터에서 여러 터미널 창을 띄우고 각각 로그인하여 작업할 수 있다. 이 경우 각 터미널마다 어떤 프로세스가 실행 중인지 따로 확인해야 하는 상황이 생긴다. 즉 터미널은 단순히 하나의 모니터 화면 개념이 아니라, 여러 개의 독립적인 작업 공간처럼 사용할 수 있는 것이다.</p>
<pre><code class="language-plaintext">ps x
</code></pre>
<p><code>x</code> 옵션은 TTY(제어 터미널)가 없는 프로세스까지 포함해서 보여준다. 일반적인 foreground 작업뿐만 아니라, background 프로세스나 데몬(daemon)처럼 터미널과 직접 연결되지 않은 작업들도 확인할 수 있게 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/ec376a89-1ae3-4cae-94f4-71713d450db9.png" alt="" style="display:block;margin:0 auto" />

<h3>프로세스 정보: kill Process Information, <code>kill</code></h3>
<p><code>kill</code> 명령어는 단순히 프로세스를 “강제로 종료하는 명령”으로 많이 알려져 있지만, 실제 의미는 조금 더 넓다. 리눅스(Linux)나 유닉스(UNIX)에서 <code>kill</code>은 프로세스(Process)에 시그널(signal)을 보내는 명령어이다.</p>
<h3>Signal(시그널)</h3>
<p>Signal은 말 그대로 프로세스에게 전달되는 “신호”이다. 실행 중인 프로세스가 작업을 수행하고 있을 때, 외부에서 특정 신호를 끼워 넣어 동작에 영향을 주는 방식이다. 이러한 개념은 인터럽트(interrupt)와 비슷하게 설명되기도 한다. 즉 프로세스가 계속 실행 중일 때 운영체제나 사용자가 특정 signal을 보내면, 프로세스는 그 신호에 맞게 반응하게 된다.</p>
<h3>kill -L</h3>
<pre><code class="language-plaintext">kill -L
</code></pre>
<p><code>kill -L</code> 명령을 입력하면 시스템에서 사용할 수 있는 signal 목록을 확인할 수 있다. 예시에서는 약 64개의 signal이 보일 수 있지만, 이것이 절대적인 숫자를 의미하는 것은 아니다. 시스템 환경이나 운영체제 버전에 따라 제공되는 signal 종류와 개수는 달라질 수 있다.</p>
<p>kill의 실제 의미</p>
<p>이름 때문에 <code>kill</code>은 단순히 프로세스를 제거(delete)하거나 종료시키는 명령처럼 보일 수 있다. 하지만 실제로는 특정 signal을 프로세스에 전달하는 역할을 한다.</p>
<ul>
<li><p><code>kill</code> → signal을 보냄</p>
</li>
<li><p>프로세스 → signal을 받고 반응함</p>
</li>
</ul>
<p>이라는 구조로 동작한다. 컴퓨터는 signal 종류를 각각 구분할 수 있도록 설계되어 있으며, 프로세스는 전달받은 signal에 따라 서로 다른 동작을 수행한다.</p>
<p>예를 들어: 종료, 일시 중단, 재시작, 무시, 사용자 정의 처리</p>
<p>등 다양한 반응이 가능하다. 즉 <code>kill</code>은 “무조건 종료시키는 명령”이 아니라, 프로세스에게 특정 signal을 전달하는 도구라고 이해하는 것이 더 정확하다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/eb08720a-4c68-4e55-8d8a-1f24fd495de2.png" alt="" style="display:block;margin:0 auto" />

<h3>kill의 사용 형식 (kill Command Usage)</h3>
<p><code>kill</code> 명령어는 프로세스(Process)에 특정 시그널(signal)을 보내기 위해 사용된다.</p>
<p>기본 형식은 다음과 같다.</p>
<pre><code class="language-plaintext">kill [option] &lt;pid&gt;
</code></pre>
<p>여기서 <code>pid</code>는 signal을 전달할 대상 프로세스의 PID(Process ID)를 의미한다.</p>
<p>기본 시그널: TERM(15)</p>
<p><code>kill</code> 명령에서 별도의 signal을 지정하지 않으면 기본적으로 <code>TERM(15)</code> signal이 전달된다.</p>
<pre><code class="language-plaintext">kill 100
</code></pre>
<p>처럼 입력하면 실제로는:</p>
<pre><code class="language-plaintext">kill -TERM 100
</code></pre>
<p>과 같은 의미로 동작한다. 여기서 <code>15</code>는 signal 번호이며, 앞에서 확인했던 여러 signal 목록 중 하나이다.</p>
<h3>시그널 지정 방법</h3>
<p>Signal은 숫자(number)와 이름(name) 둘 다 사용할 수 있다. 예를 들어 다음 표현들은 모두 signal을 지정하는 방식이다.</p>
<pre><code class="language-c">kill -9 PID
kill -SIGKILL PID
kill -KILL PID
</code></pre>
<p>즉 signal은: 숫자 형태, 전체 이름 형태, 축약 이름 형태 모두 사용할 수 있다.</p>
<p>자주 사용하는 Signal</p>
<p>자주 사용되는 signal로는 다음과 같은 것들이 있다.</p>
<ul>
<li><code>HUP, INT, KILL, STOP, CONT, 0</code> 이 중 <code>0</code>은 조금 특별하다.</li>
</ul>
<p>Signal 0</p>
<p><code>0</code>은 실제로 프로세스에 동작을 발생시키는 signal이 아니다. 그래서 null signal이라고 부르기도 한다. 주로 다음과 같은 용도로 사용된다.</p>
<ul>
<li><p>해당 프로세스가 존재하는지 확인</p>
</li>
<li><p>현재 사용자가 그 프로세스에 signal을 보낼 권한(permission)이 있는지 확인</p>
</li>
</ul>
<p>즉 signal 자체로 어떤 동작을 수행시키는 것이 아니라, 프로세스 상태를 검사하기 위한 용도로 사용된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/7c364cf8-bf6d-4025-833d-897c31539627.png" alt="" style="display:block;margin:0 auto" />

<h3>PID -1</h3>
<p>일반적으로 PID는 0 이상의 양수로 생각하기 쉽지만, <code>kill</code>에서는 음수 값도 특별한 의미로 사용된다. 예를 들어, <code>kill -1</code> 은 단순히 PID가 <code>-1</code>인 프로세스를 의미하는 것이 아니다. 이는 현재 프로세스 자신과 <code>init</code> 프로세스(PID 1)를 제외한 모든 프로세스에 signal을 보내라는 의미이다. 즉, 자기 자신 제외, PID 1 제외, 나머지 전체 프로세스 대상 이라는 특별한 동작을 수행하게 된다.</p>
<h3>음수 PID와 PGID</h3>
<p><code>-1</code> 이외의 다른 음수 값은 PGID(Process Group ID)를 의미한다. <code>kill -100</code> 이라고 입력하면, PID 100 프로세스 하나에 signal을 보내는 것이 아니라 PGID가 100인 프로세스 그룹 전체 에게 signal을 보내게 된다.</p>
<p>즉 일반적인 <code>kill PID</code>는 하나의 프로세스를 대상으로 하지만, 음수 값을 사용하면 특정 프로세스 그룹 전체에 signal을 전달할 수 있다. 이러한 프로세스 그룹 정보는 <code>ps j</code> 명령어를 통해 확인할 수 있다.</p>
<p>결국 지금까지의 흐름은:</p>
<ol>
<li><p><code>ps(process status)</code>로 프로세스 상태를 확인하고</p>
</li>
<li><p><code>PID(Process ID)</code>나 <code>PGID(Process Group ID)</code>를 파악한 뒤</p>
</li>
<li><p><code>kill</code>을 사용해 원하는 signal을 전달한다 는 구조로 이해할 수 있다.</p>
</li>
</ol>
<hr />
<h1>2️⃣ Resources</h1>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/28082941-07a2-40f4-a9b0-f93f4cc6fa19.png" alt="" style="display:block;margin:0 auto" />

<h3>리눅스 자원: CPU 정보 확인 (<code>lscpu</code>)</h3>
<p>리눅스에서 현재 사용 중인 <strong>CPU 정보(CPU Information)</strong> 는 <code>lscpu</code> 명령어로 확인할 수 있다. 이 명령어를 실행하면 CPU 코어 수, 제조사 정보, CPU 기능, 소켓 구성, NUMA 정보 등을 볼 수 있다.</p>
<p>예를 들어 <code>lscpu</code> 결과에서 코어가 4개로 표시된다면, 해당 시스템에서 사용할 수 있는 CPU 자원이 4개라는 의미로 볼 수 있다. 다만 실제 물리 코어 수인지, 논리 CPU 수인지는 <code>CPU(s)</code>, <code>Core(s) per socket</code>, <code>Thread(s) per core</code> 항목을 함께 확인해야 한다.</p>
<p><code>Vendor ID</code>는 CPU 제조사를 나타낸다. 일반적으로 Intel이나 AMD가 표시되며, Apple 하드웨어에서 리눅스를 사용하는 경우 Apple 관련 정보가 나타날 수도 있다. Apple 장비에서도 리눅스를 설치하거나 실행하는 것은 가능하다.</p>
<p><code>Flags</code> 항목에는 현재 CPU가 지원하는 기능들이 표시된다. 이 값들은 CPU가 어떤 명령어나 기능을 제공하는지 보여주는 정보다.</p>
<p>또한 <code>NUMA node(s)</code> 항목도 확인할 수 있다. <strong>NUMA(Non-Uniform Memory Access)</strong> 는 CPU와 메모리 접근 구조와 관련된 정보다. 일반적인 개인용 컴퓨터에서는 보통 <code>NUMA node(s): 1</code>로 표시된다. 이는 하나의 NUMA 노드만 있다는 뜻이다.</p>
<p>다만 <code>NUMA node(s): 1</code>이 곧바로 CPU 소켓이 1개라는 뜻은 아니다. CPU 소켓 수는 <code>Socket(s)</code> 항목에서 확인하는 것이 정확하다. 일반적인 컴퓨터는 소켓이 하나인 경우가 많지만, 서버용 메인보드에서는 CPU 소켓이 2개, 4개, 8개처럼 여러 개 들어갈 수도 있다.</p>
<p>정리하면, <code>lscpu</code>는 리눅스에서 CPU 자원을 확인할 때 사용하는 기본 명령어이며, CPU 코어 수, 제조사, 기능, 소켓 수, NUMA 구성 등을 확인할 수 있다.</p>
<hr />
<h3>리눅스 자원: CPU 정보 확인 (Linux Resources: Checking CPU Information) - <code>lstopo</code>, <code>htop</code>, <code>top</code></h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/67e470f3-0b8e-4aa2-aa50-8461f024ccdf.png" alt="" style="display:block;margin:0 auto" />

<p><code>lstopo</code>는 시스템의 <strong>위상 정보(System Topology)</strong> 를 보여주는 명령어이다. 앞에서 살펴본 <code>lscpu</code>가 CPU 코어 수나 기본적인 CPU 정보를 보여준다면, <code>lstopo</code>는 시스템 내부 구조를 더 세부적으로 보여준다. 예를 들어 CPU 코어뿐만 아니라 <strong>Cache</strong> 정보, <strong>L2 Cache</strong>, <strong>PCI Bus</strong> 위치 등도 확인할 수 있다.</p>
<p>컴퓨터를 직접 조립하거나 서버를 구축하지 않더라도, 시스템이 어떤 하드웨어 구조를 가지고 있는지 확인해야 할 때가 있다. 이때 <code>lstopo</code>를 사용하면 컴퓨터를 직접 열어보지 않아도 CPU, Cache, PCI Bus 등 자원이 어떤 구조로 연결되어 있는지 확인할 수 있다. 이렇게 확인한 정보는 시스템 자원 분석이나 성능 최적화에 활용할 수 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/31575a8d-2421-4237-9a4c-fd60cd7ea46f.png" alt="" style="display:block;margin:0 auto" />

<p><code>htop</code>은 현재 시스템 자원 사용 상태를 실시간으로 확인할 수 있는 명령어이다. 앞에서 배운 <code>ps</code> 명령어는 실행한 순간의 프로세스 정보만 보여준다. 반면 <code>htop</code>은 CPU와 Memory 사용량을 계속 갱신하면서 보여주기 때문에 시스템 상태를 모니터링하기에 편리하다.</p>
<p><code>htop</code>은 다음과 같이 설치할 수 있다.</p>
<pre><code class="language-plaintext">sudo apt install htop
</code></pre>
<p>또는 환경에 따라 다음 명령어로 설치할 수도 있다.</p>
<pre><code class="language-plaintext">sudo snap install htop
</code></pre>
<p><code>htop</code>을 실행하면 CPU 사용량을 코어별로 확인할 수 있고, Memory가 얼마나 사용되고 있는지도 볼 수 있다. 비슷한 기능은 <code>top</code> 명령어로도 확인할 수 있다.</p>
<p>화면에는 <code>PID</code>, <code>USER</code>, <code>PRI</code>, <code>NI</code>, <code>VIRT</code> 같은 값들이 표시된다. 이 항목들은 앞에서 배운 <code>ps</code> 명령어에서 확인할 수 있는 프로세스 정보와 관련된 값들이다. 사용자는 이 값들을 보고 현재 시스템이 어떤 상태인지 판단할 수 있다.</p>
<p>마지막 부분의 <code>COMMAND</code> 항목을 보면 해당 프로세스가 어떤 명령어로 실행되었는지 확인할 수 있다. 이를 통해 현재 시스템에서 어떤 프로그램이 실행 중인지, 사용자가 어떤 작업을 하고 있는지 파악할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/9bc19184-70c4-45cb-bcb6-624d0fbf55b3.png" alt="" style="display:block;margin:0 auto" />

<h3>Linux Resources: CPU Affinity (<code>taskset</code>, <code>numactl</code>)</h3>
<p>CPU 자원은 기본적으로 운영체제의 <strong>스케줄러(Scheduler)</strong> 가 알아서 관리한다. 사용자가 직접 개입하지 않아도 프로세스가 어느 CPU 코어에서 실행될지 자동으로 결정되도록 설계되어 있다. 이는 사람의 개입을 최소화하고, 시스템이 자원을 효율적으로 사용할 수 있도록 하기 위한 방식이다.</p>
<p>하지만 특정 프로세스를 특정 CPU 코어에서만 실행되도록 지정하고 싶을 때가 있다. 이때 사용하는 명령어가 <code>taskset</code>이다. <code>taskset</code>은 프로세스의 <strong>CPU Affinity</strong> 를 설정하는 명령어로, 어떤 프로세스를 어느 CPU 코어에서 실행할지 지정할 수 있다.</p>
<p>예를 들어 <code>my_program</code>을 1번 CPU에서 실행하도록 지정하려면 다음과 같이 사용할 수 있다.</p>
<pre><code class="language-plaintext">taskset -c 1 my_program
</code></pre>
<p>여러 CPU 코어를 지정할 수도 있다. 예를 들어 6번부터 9번 CPU까지 사용하게 하려면 다음과 같이 입력한다.</p>
<pre><code class="language-plaintext">taskset -c 6-9 my_program
</code></pre>
<p>또는 3번, 5번, 7번 CPU처럼 특정 코어만 골라 지정할 수도 있다.</p>
<pre><code class="language-plaintext">taskset -c 3,5,7 my_program
</code></pre>
<p>과거의 단순한 컴퓨터 환경에서는 작업이 순서대로 대기하고, CPU가 비면 다음 작업을 실행하는 전통적인 <strong>스케줄링(Scheduling)</strong> 방식이 주로 사용되었다. 하지만 멀티코어 CPU 환경에서는 여러 코어를 활용해야 하므로 더 복잡한 스케줄링 방식이 사용된다. <code>taskset</code>을 사용하면 이런 환경에서도 특정 프로세스가 특정 코어에서만 스케줄링되도록 제한할 수 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/936c5d79-ad15-47a9-94d6-96d79f793584.png" alt="" style="display:block;margin:0 auto" />

<p><code>numactl</code>은 프로세스나 공유 메모리의 <strong>NUMA 정책(NUMA Policy)</strong> 을 제어하는 명령어이다. <code>taskset</code>이 주로 CPU 코어를 지정하는 데 사용된다면, <code>numactl</code>은 프로세스가 어떤 CPU 노드와 어떤 NUMA 메모리를 사용할지 지정할 수 있다.</p>
<p>고성능 시스템에서는 CPU 코어 수가 많아지고, 메모리도 여러 영역으로 나뉘어 CPU와 연결된다. 이때 CPU와 물리적으로 가까운 메모리를 사용하는 것이 성능에 유리할 수 있다. <code>numactl</code>은 이런 구조에서 CPU와 메모리 자원을 좀 더 직접적으로 관리할 수 있게 해준다.</p>
<p>예를 들어 <code>my_program</code>을 0번 NUMA 노드의 CPU와 0번 NUMA 노드의 메모리에 묶으려면 다음과 같이 사용할 수 있다.</p>
<pre><code class="language-plaintext">numactl --cpunodebind=0 --membind=0 my_program
</code></pre>
<p>이 명령은 프로그램이 0번 NUMA 노드의 CPU를 사용하고, 메모리도 0번 NUMA 노드의 메모리를 사용하도록 지정한다.</p>
<p>반면 <code>my_program</code>을 0번 CPU에만 묶고, 메모리 위치는 따로 지정하지 않으려면 다음과 같이 사용할 수 있다.</p>
<pre><code class="language-plaintext">numactl --physcpubind=0 my_program
</code></pre>
<p>이 경우 CPU는 0번 CPU로 지정하지만, 메모리 사용 위치까지 강제하지는 않는다. 즉, CPU만 신경 쓰고 메모리는 운영체제의 기본 정책에 맡기는 방식이다.</p>
<p>정리하면, <code>taskset</code>은 프로세스를 특정 CPU 코어에서 실행되도록 지정하는 명령어이고, <code>numactl</code>은 CPU뿐만 아니라 NUMA 메모리 정책까지 함께 제어할 수 있는 명령어이다. 일반적인 환경에서는 운영체제가 CPU 자원을 자동으로 관리하지만, 고성능 시스템이나 세밀한 성능 조정이 필요한 환경에서는 이런 명령어를 통해 자원을 직접 관리할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/d227e3f4-63c7-41d4-8057-1db3878312fa.png" alt="" style="display:block;margin:0 auto" />

<h3>Linux Resources: Priority Adjustment (<code>nice</code>)</h3>
<p>리눅스에서는 프로세스의 <strong>스케줄링 우선순위(Scheduling Priority)</strong> 를 조정할 수 있다. 이때 사용하는 대표적인 명령어가 <code>nice</code>이다. <code>nice</code>는 새로 실행하는 프로세스의 <strong>Nice 값(Nice Value)</strong> 을 지정해서, 해당 프로세스가 CPU 자원을 얼마나 우선적으로 사용할지에 영향을 준다.</p>
<p>리눅스의 Nice 값은 일반적으로 <code>-20</code>부터 <code>19</code>까지 설정할 수 있다. 숫자가 낮을수록 우선순위가 높고, 숫자가 높을수록 우선순위가 낮다. 즉, <code>-20</code>에 가까울수록 더 먼저 처리될 가능성이 높고, <code>19</code>에 가까울수록 상대적으로 뒤로 밀리게 된다.</p>
<p>다만 모든 사용자가 마음대로 우선순위를 높일 수 있는 것은 아니다. 일반 사용자(User)는 보통 Nice 값을 높여서 우선순위를 낮추는 것은 가능하지만, Nice 값을 음수로 낮춰 우선순위를 높이려면 <strong>root 권한(Superuser Privilege)</strong> 이 필요하다.</p>
<p><code>nice</code> 명령어를 사용하지 않고 일반적으로 실행한 새 프로세스의 기본 Nice 값은 <code>0</code>이다. 하지만 <code>nice</code> 명령어를 사용하면서 <code>-n</code> 옵션으로 값을 지정하지 않으면, 기본적으로 Nice 값이 <code>10</code> 증가한다. 즉, 그냥 실행하면 Nice 값이 <code>0</code>이지만, <code>nice my_program</code>처럼 실행하면 Nice 값이 <code>10</code>이 되어 우선순위가 낮아진다. 이는 “빨리 처리해 주세요”가 아니라 “다른 작업보다 조금 늦게 처리해도 됩니다”라는 의미에 가깝다.</p>
<p>예를 들어 <code>my_program</code>의 Nice 값을 <code>15</code>로 지정해서 실행하려면 다음과 같이 입력한다.</p>
<pre><code class="language-plaintext">nice -n 15 my_program
</code></pre>
<p>현재 실행 중인 프로세스의 Nice 값은 <code>top</code>, <code>htop</code>, <code>ps -l</code> 같은 명령어로 확인할 수 있다. 이러한 도구를 통해 프로세스별 우선순위를 확인하고, 시스템에서 어떤 작업이 더 높은 우선순위로 실행되고 있는지 파악할 수 있다.</p>
<p>정리하면, <code>nice</code>는 새 프로세스의 CPU 스케줄링 우선순위를 조정하는 명령어이다. 기본 Nice 값은 <code>0</code>이며, 값이 낮을수록 우선순위가 높고 값이 높을수록 우선순위가 낮다. 특히 음수 Nice 값을 설정해 우선순위를 높이려면 root 권한이 필요하다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/b2c0151f-261e-4647-96c0-df1e9a9ca18c.png" alt="" style="display:block;margin:0 auto" />

<h2>Linux Resources: Priority Adjustment (<code>renice</code>)</h2>
<p><code>renice</code>는 이미 실행 중인 프로세스의 <strong>스케줄링 우선순위(Scheduling Priority)</strong> 를 변경할 때 사용하는 명령어이다. 앞에서 본 <code>nice</code>가 새로 실행하는 프로세스의 우선순위를 지정하는 명령어라면, <code>renice</code>는 현재 실행 중인 프로세스의 <strong>Nice 값(Nice Value)</strong> 을 바꾸는 데 사용된다.</p>
<p>리눅스에서 Nice 값은 일반적으로 <code>-20</code>부터 <code>19</code>까지 설정할 수 있다. 숫자가 낮을수록 우선순위가 높고, 숫자가 높을수록 우선순위가 낮다. 즉, <code>-20</code>에 가까울수록 CPU 자원을 더 우선적으로 받을 가능성이 높고, <code>19</code>에 가까울수록 상대적으로 뒤로 밀리게 된다.</p>
<p><code>renice</code>는 특정 <strong>프로세스 ID(Process ID, PID)</strong> 를 대상으로 사용할 수 있다. 예를 들어 PID가 <code>1234</code>인 프로세스의 Nice 값을 <code>10</code>으로 변경하려면 다음과 같이 입력한다.</p>
<pre><code class="language-plaintext">renice 10 1234
</code></pre>
<p>이 경우 별도의 옵션을 지정하지 않으면 <code>1234</code>는 기본적으로 프로세스 ID로 해석된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f25851a4-75e4-4bac-8d28-27c430e64f85.png" alt="" style="display:block;margin:0 auto" />

<h3>Keep Running After Logout: Ignoring <code>SIGHUP</code> with <code>nohup</code></h3>
<p>리눅스를 사용하다 보면 터미널을 종료하거나 원격 접속이 끊긴 뒤에도 프로그램을 계속 실행해야 할 때가 있다. 이때 사용할 수 있는 명령어가 <code>nohup</code>이다.</p>
<p>터미널 세션(Session)이 종료되거나 연결이 끊기면 실행 중인 프로세스에 <strong>SIGHUP(Signal Hang Up)</strong> 시그널이 전달될 수 있다. 일반적으로 이 시그널을 받은 프로세스는 종료된다.</p>
<p><code>nohup</code>은 이 <strong>SIGHUP</strong> 시그널을 무시하도록 만들어, 터미널이 종료된 뒤에도 프로세스가 계속 실행될 수 있게 한다.</p>
<p>예를 들어 <code>my_program</code>을 로그아웃 이후에도 계속 실행하고 싶다면 다음과 같이 사용할 수 있다.</p>
<pre><code class="language-plaintext">nohup my_program &amp;
</code></pre>
<p>여기서 <code>&amp;</code>는 프로그램을 <strong>백그라운드(Background)</strong> 에서 실행하겠다는 의미이다. <code>nohup</code>으로 실행한 프로그램의 출력은 기본적으로 <code>nohup.out</code> 파일에 저장된다.</p>
<p>정리하면, <code>nohup</code>은 터미널 종료나 접속 종료 이후에도 프로세스를 계속 실행하고 싶을 때 사용하는 명령어이다. 핵심은 <code>SIGHUP</code> 시그널을 무시하게 만들어, 실행 중인 작업이 중간에 끊기지 않도록 하는 것이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/da331fa3-5aad-4b98-9a06-28402b21d961.png" alt="" style="display:block;margin:0 auto" />

<h2>Keep Running After Logout: <code>screen</code></h2>
<p>리눅스/유닉스에서 로그아웃 후에도 작업을 계속 실행해야 할 때 <code>screen</code> 명령어를 사용할 수 있다. <code>screen</code>은 <strong>VT100/ANSI 가상 터미널(Virtual Terminal)</strong> 을 만들어주는 프로그램이다.</p>
<p>일반적으로 사용자가 로그인하면 하나의 터미널 창에서 작업을 하게 된다. 이때 <code>screen</code> 명령을 실행하면 기존 터미널 안에 새로운 가상 터미널 세션(Session)이 만들어진다. 이 세션 안에서 명령어를 실행하거나 프로그램을 돌릴 수 있고, 필요하면 화면에서 빠져나온 뒤 나중에 다시 해당 세션으로 돌아갈 수 있다.</p>
<p><code>screen</code>의 핵심은 터미널 창을 닫거나 로그아웃하더라도, 그 안에서 실행 중이던 프로세스(Process)가 계속 유지될 수 있다는 점이다. 사용자는 원격 접속을 종료한 뒤에도 나중에 다시 로그인해서 기존 <code>screen</code> 세션에 접속하고, 이전 작업 상태를 이어서 확인할 수 있다.</p>
<p>예를 들어 원격 서버에 접속해서 오래 걸리는 작업을 실행해야 하는 상황을 생각할 수 있다. 단순히 터미널에서 작업을 실행한 뒤 접속을 끊으면 작업이 중단될 수 있다. 하지만 <code>screen</code> 안에서 작업을 실행하면 로그아웃한 뒤에도 작업이 계속 돌아가며, 이후 다른 장소에서 다시 접속해 상태를 확인할 수 있다.</p>
<p>예를 들어 <code>screen</code>을 시작하려면 다음과 같이 입력한다.</p>
<pre><code class="language-plaintext">screen
</code></pre>
<p><code>screen</code> 안에서 작업을 실행한 뒤 세션에서 빠져나오면 원래 터미널로 돌아올 수 있다. 이후 다시 접속해서 기존 세션을 불러오면 작업 중이던 화면을 이어서 볼 수 있다.</p>
<p>정리하면, <code>screen</code>은 로그아웃 후에도 작업을 유지하기 위해 사용하는 가상 터미널 도구이다. 원격 접속 환경에서 오래 걸리는 작업을 실행하거나, 장소를 옮겨가며 같은 작업 상태를 이어서 확인해야 할 때 유용하다.</p>
<hr />
<h2>Keep Running After Logout: <code>tmux</code></h2>
<p>리눅스/유닉스에서 로그아웃 후에도 작업을 계속 유지하고 싶을 때 <code>tmux</code>를 사용할 수 있다. <code>tmux</code>는 터미널 세션을 관리해주는 <strong>터미널 멀티플렉서(Terminal Multiplexer)</strong> 이다.</p>
<p><code>tmux</code>는 앞에서 본 <code>screen</code>과 비슷하게 터미널 로그인이나 로그아웃 상태와 상관없이 작업을 유지할 수 있게 해준다. 사용자는 <code>tmux</code> 세션 안에서 프로그램을 실행하고, 터미널 연결을 끊은 뒤에도 나중에 다시 접속해 같은 작업 상태를 이어서 확인할 수 있다.</p>
<p><code>tmux</code>의 특징은 하나의 터미널 안에서 여러 개의 창(Window)을 만들거나, 화면을 여러 영역으로 나누는 <strong>분할(Pane Split)</strong> 기능을 제공한다는 점이다. 그래서 하나의 SSH 접속 환경에서도 여러 작업을 동시에 관리하기 좋다.</p>
<p>예를 들어 한쪽 화면에서는 로그를 확인하고, 다른 화면에서는 명령어를 실행하거나 서버 상태를 모니터링할 수 있다. 이런 점 때문에 <code>tmux</code>는 <strong>SSH 원격 접속(SSH Remote Access)</strong> 으로 서버 작업을 할 때 유용하게 사용된다.</p>
<p><code>tmux</code>는 다음 명령어로 설치할 수 있다.</p>
<pre><code class="language-plaintext">sudo apt install tmux
</code></pre>
<p>설치 후에는 다음과 같이 실행할 수 있다.</p>
<pre><code class="language-plaintext">tmux
</code></pre>
<p>정리하면, <code>tmux</code>는 로그아웃 후에도 작업을 유지할 수 있게 해주는 터미널 세션 관리 도구이다. <code>screen</code>과 비슷하지만, 창과 화면 분할 기능이 강력해 원격 서버에서 여러 작업을 동시에 수행할 때 특히 유용하다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/e1cc6720-4312-4fd5-b228-168866052e00.png" alt="" style="display:block;margin:0 auto" />

<p>리눅스에 tmux를 치니 창이 새로 뜨는것을 확인할 수 있었다.</p>
]]></content:encoded></item><item><title><![CDATA[Inter-Process Communication (IPC): Signals, Pipes, FIFO, and Shared Memory]]></title><description><![CDATA[1️⃣ Concepts of Inter-process Communication and Signal-based Communication2️⃣ Inter-process communication methods3️⃣ Inter-process Communication Practice

프로세스 간 통신 (Inter-Process Communication, IPC)
]]></description><link>https://heesu.tech/inter-process-communication-ipc-signals-pipes-fifo-and-shared-memory</link><guid isPermaLink="true">https://heesu.tech/inter-process-communication-ipc-signals-pipes-fifo-and-shared-memory</guid><dc:creator><![CDATA[Heesu Noh]]></dc:creator><pubDate>Fri, 08 May 2026 16:09:22 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/9f18b02c-debc-4311-ac52-14842082cef4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>1️⃣ Concepts of Inter-process Communication and Signal-based Communication<br />2️⃣ Inter-process communication methods<br />3️⃣ Inter-process Communication Practice</p>
<hr />
<h3>프로세스 간 통신 (Inter-Process Communication, IPC)</h3>
<p>이번 주차의 주제는 프로세스 간 통신(IPC, Inter-Process Communication)이다. 여러 프로세스가 동시에 실행될 때, 이들이 어떻게 데이터를 주고받는지, 그리고 이러한 통신이 어떤 구조와 방법으로 이루어지는지에 대해 살펴본다.</p>
<p>이번 주차의 목표는 다음과 같다. 먼저, 프로세스 간 통신의 개념과 필요성을 이해한다. 이어서 운영체제에서 제공하는 다양한 통신 방식에 대해 알아본다. 또한 파이프(Pipe), FIFO(Named Pipe), 공유 메모리(Shared Memory)와 같은 통신 방법의 특징과 차이점을 구분할 수 있어야 한다. 마지막으로 관련 시스템 호출(System Call)을 활용하여 간단한 통신 프로그램을 구현하고, 그 결과를 통해 실제 동작 과정이 어떻게 이루어지는지 확인한다.</p>
<p>오늘 학습은 이러한 내용들을 중심으로 진행된다.</p>
<hr />
<h3>프로세스 간 통신의 필요성에 대한 예시</h3>
<p>하나의 상황을 가정해보자. 친구들과 단체 채팅방에서 과제 공지가 공유된다고 할 때, 한 사람이 과제 내용을 채팅방에 올리면 다른 사람들은 그 내용을 확인하고 과제를 준비하게 된다. 그런데 이 메시지가 누군가에게 전달되지 않는다면 어떻게 될까? 또한 각자가 따로 정보를 알고 있다면 전체 과제를 제대로 진행할 수 있을까? 이처럼 여러 사람이 정보를 공유할 때, 정보 전달이 어떻게 이루어지는지 생각해볼 수 있다.</p>
<p>또 다른 상황을 가정해보자. 터미널에서 어떤 프로그램이 실행 중일 때, 사용자가 컨트롤 C를 입력하면 실행 중인 프로그램이 갑자기 종료된다. 이 경우 프로그램 내부에서는 종료 명령을 직접 수행하지 않았음에도 불구하고 왜 종료되는 것일까? 또한 운영체제는 이러한 외부 입력을 프로그램에 어떻게 전달하는 것일까?</p>
<p>이와 같은 상황을 통해 프로그램과 운영체제 간의 통신 방식에 대해 살펴본다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/dbf52cc2-6bed-480a-8cb7-e07490036acb.png" alt="" style="display:block;margin:0 auto" />

<h3>프로세스 간 통신 (IPC) 개념과 필요성</h3>
<p>이번 챕터에서는 프로세스 간 통신의 개념과 시그널 기반의 통신에 대해 알아본다.</p>
<p>IPC는 Inter-Process Communication의 약자로, 여러 프로세스들이 서로 정보를 주고받기 위해 운영체제가 제공하는 기능이다. 여러 사람이 협력하여 일을 수행하기 위해 서로 정보를 전달해야 하듯이, 운영체제에서도 여러 프로세스가 협력 작업을 수행하기 위해서는 정보 교환이 필요하다.</p>
<p>이러한 통신이 필요한 이유는 다음과 같다. 첫째, 프로세스 간에 데이터를 전달하기 위해 필요하다. 둘째, 여러 프로세스가 협력하여 하나의 작업을 수행하기 위해 필요하다. 셋째, 특정 이벤트나 상태 변화를 다른 프로세스에 전달하기 위해 사용된다.</p>
<p>즉, IPC는 단순한 데이터 전달을 넘어서 프로세스 간의 협력과 제어를 가능하게 하는 핵심 기능이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/71086349-d4c3-426f-9933-595583c86b21.png" alt="" style="display:block;margin:0 auto" />

<h3>프로세스 간 통신이 필요한 이유</h3>
<p>프로세스는 각각 독립된 주소 공간(Address Space)을 가지기 때문에 다른 프로세스의 메모리에 직접 접근할 수 없다. 예를 들어, 어떤 프로그램이 데이터를 생성하더라도 다른 프로그램이 해당 메모리를 직접 읽어오는 것은 불가능하다.</p>
<p>이러한 한계를 보완하기 위해 운영체제는 프로세스 간에 정보를 주고받을 수 있는 다양한 통신 방법을 제공한다. 결국 프로세스 간의 정보 전달을 위해 IPC가 필요하게 된다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/da9df87e-070e-4d39-a577-d5ac544848e4.png" alt="" style="display:block;margin:0 auto" />

<p>결국 한 프로세스의 작업 결과를 다른 프로세스가 바로 사용할 수 없고, 실행 중에 발생하는 상태나 이벤트도 즉시 전달되지 않는다. 따라서 운영체제는 이러한 문제를 해결하기 위해 IPC 기능을 제공한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/b77e695e-9924-43c1-88c9-14808ac94787.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a6e31247-821e-4ef1-b346-118ff6d63f24.png" alt="" style="display:block;margin:0 auto" />

<h3>프로세스 간 통신 방법</h3>
<p>이러한 IPC 기능은 여러 가지 방법으로 제공된다. 각 방법은 데이터를 전달하는 방식과 사용 목적에 따라 차이가 있다.</p>
<p>먼저 <strong>pipe</strong>는 한 프로세스에서 다른 프로세스로 데이터를 순서대로 전달하는 방식이다.<br /><strong>message queue</strong>는 데이터를 메시지 단위로 나누어 전달하는 방식이다.<br /><strong>shared memory</strong>는 하나의 메모리 공간을 사용하여 보다 빠르게 데이터를 공유하는 방식이다.</p>
<p>즉, 이들은 데이터 전달과 데이터 공유를 위한 대표적인 IPC 방법들이다.</p>
<p>이어서 <strong>signal</strong>은 어떤 이벤트가 발생했다는 것을 간단하게 알려주는 방식이다. 예를 들어, 사용자가 control + C를 누르면 프로그램이 종료되는데, 이때 signal이 전달되면서 해당 동작이 이루어진다.</p>
<p>마지막으로 <strong>socket</strong>은 네트워크를 기반으로 통신하는 방식이다. 이는 같은 컴퓨터뿐만 아니라 다른 컴퓨터에 있는 프로그램과도 데이터를 주고받을 수 있는 통신 방식이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/521eada4-e2c6-43aa-b0ed-52e71164545e.png" alt="" style="display:block;margin:0 auto" />

<h3>Signal 기반 프로세스 간 통신</h3>
<p><strong>signal</strong>은 프로세스에 특정 이벤트 발생을 알리는 운영체제의 비동기 신호(<strong>asynchronous signal</strong>)라고 볼 수 있다. 이 신호는 주로 예외 상황이나 프로세스를 제어하기 위한 목적으로 사용된다.</p>
<p>여기서 중요한 점은 signal은 데이터를 전달하는 방식이 아니라, 이벤트 발생 자체를 알리는 데 목적이 있다는 것이다. 또한 signal을 받게 되면 프로세스의 흐름이 중간에 바뀔 수 있다.</p>
<p>이러한 신호는 운영체제가 자동으로 해당 프로세스에 전달하며, 프로세스는 해당 signal에 따라 종료되거나 특정 동작을 수행하게 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f2ee9bfb-cbc5-4fcd-8b91-d051714d0375.png" alt="" style="display:block;margin:0 auto" />

<h3>Signal Delivery Mechanism</h3>
<p>이러한 signal은 이벤트를 알려주는 방식이라고 설명할 수 있다. 이 신호가 실제로 어떻게 전달되는지를 전달 구조로 보면 다음과 같다.</p>
<ol>
<li><p>한 프로세스에서 signal이 발생한다.</p>
</li>
<li><p>이 신호는 운영체제를 통해 전달되며, 운영체제가 대상 프로세스에게 signal을 넘겨준다.</p>
</li>
<li><p>signal을 받는 프로세스는 그에 맞는 처리를 수행한다.</p>
</li>
</ol>
<p>이 과정에서 signal 상태는 프로세스의 **PCB(Process Control Block)**에 기록되어 지속적으로 관리된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/2c7ff809-dc4c-49d4-9d0c-331496ab2ca3.png" alt="" style="display:block;margin:0 auto" />

<h3>Signal 처리 과정 (signal handling process)</h3>
<p>앞에서 본 전달 흐름을 조금 더 구체적으로 보면, 프로세스 B에서 signal A가 발생하게 되면 이 신호는 바로 프로세스 A로 전달되는 것이 아니라 운영체제를 통해 처리된다.</p>
<p>운영체제는 해당 신호를 대상 프로세스, 즉 프로세스 A의 PCB(Process Control Block)에 기록하게 된다. 이후 프로세스 A는 자신에게 전달된 signal을 확인한 후, 해당 신호에 대한 처리를 수행하게 된다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/2b4aca3e-3ac9-422e-93d2-74f3a47a4633.png" alt="" style="display:block;margin:0 auto" />

<p>그림으로 표현하면, 오른쪽 상단의 프로세스 B에서 signal A가 발생하면 이 신호는 커널(Kernel)로 전달된다. 이후 커널은 이 signal을 프로세스 A의 PCB에 기록하게 되고, 프로세스 A는 signal을 전달받아 해당 신호에 대한 처리를 수행하는 흐름으로 진행된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/14178705-6112-42e3-8a8a-e4f5d29c830a.png" alt="" style="display:block;margin:0 auto" />

<h3>Signal의 처리 방식</h3>
<p>signal이 전달된 이후의 처리 방식은 하나로 정해져 있는 것이 아니라 상황에 따라 선택할 수 있다. 아무 설정을 하지 않으면 프로세스를 종료하거나, 멈추거나, 다시 실행하는 기본 동작으로 처리된다.</p>
<p>또한 필요에 따라 signal을 무시할 수도 있으며, 특정 signal이 왔을 때 사용자가 직접 정의한 함수로 처리하도록 설정할 수도 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/8f6eef40-4348-4806-8a2b-1c3f2b9de437.png" alt="" style="display:block;margin:0 auto" />

<p>프로세스가 signal을 받게 되면 종료되거나, 일시적으로 멈추거나, 다시 실행되는 상태로 바뀌게 되는데 이러한 것들이 기본 동작의 예이다. 즉, signal은 데이터를 주고받는 것이 아니라 프로세스의 상태를 변경하는 역할을 한다고 볼 수 있다.</p>
<p>따라서 signal의 처리 방식은 프로세스가 미리 설정할 수 있다.</p>
<hr />
<h3>시그널 처리 방식의 대표적인 시그널 예시 (examples of signal handling)</h3>
<p>아래의 Signal들은 실제로 자주 사용되는 대표적인 Signal들이다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/4de9b5a2-3f85-4645-b175-84477558cb6c.png" alt="" style="display:block;margin:0 auto" />

<p>우리가 control + C를 누르면 발생하는 <strong>SIGINT</strong>는 실행을 중단시키는 역할을 한다.<br />다음으로 <strong>SIGTERM</strong>에서 TERM은 terminate를 의미하며, 프로세스에 정상적으로 종료하라는 요청을 보내는 signal이다.<br />그리고 <strong>SIGKILL</strong>은 프로세스를 강제로 즉시 종료시키는 signal이다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/d228fd31-1ece-46a9-98c2-8c84fb454ad9.png" alt="" style="display:block;margin:0 auto" />

<p>또한 실행을 멈추거나 다시 이어가는 signal들도 있다.<br /><strong>SIGSTOP</strong>은 실행 중인 프로세스를 멈추는 역할을 하며,<br /><strong>SIGCONT</strong>는 멈춰 있던 프로세스를 다시 실행시키는 역할을 한다.</p>
<p>결국 Signal은 이처럼 프로세스의 상태를 직접 변경하는 역할을 한다고 볼 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/61c33d33-8f63-440a-8283-cdfb4ce3d488.png" alt="" style="display:block;margin:0 auto" />

<h3>Signal 발생 원인 (causes of signal generation)</h3>
<p>Signal은 여러 상황에서 발생할 수 있다. 사용자가 직접 입력하거나, 다른 프로세스가 신호를 보내는 경우에도 발생할 수 있으며, 프로그램 실행 중 오류가 발생할 때에도 signal이 발생할 수 있다.</p>
<p>예를 들어 어떤 프로그램이 접근하면 안 되는 메모리를 잘못 건드리게 되면 <strong>Segmentation Fault</strong>라는 오류가 발생하게 된다. 즉, 존재하지 않는 주소에 접근하거나 허용되지 않은 메모리 영역을 접근해서 발생하는 오류라고 볼 수 있다. 이때 운영체제는 <strong>SIGSEGV</strong>라는 signal을 보내 해당 프로세스에 문제가 발생했음을 알리게 된다.</p>
<p>또한 이러한 경우 외에도 운영체제 내부 이벤트에 의해 signal이 발생할 수도 있다. 예를 들어 control + C를 입력하면 <strong>SIGINT</strong>가 발생하게 되고, 프로그램 실행 중 잘못된 메모리 접근이 발생하면 <strong>SIGSEGV</strong>가 발생하게 된다.</p>
<p>즉, signal은 사용자의 입력에 의해서도 발생할 수 있고 프로그램의 오류에 의해서도 발생할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/70208c0a-b01c-4dc3-8f88-96fc85ab0c39.png" alt="" style="display:block;margin:0 auto" />

<h3>Signal 전달 명령어 (signal delivery commands)</h3>
<p>Signal은 내부에서 자동으로 발생하는 것뿐만 아니라, 사용자가 터미널에서 직접 명령어를 입력하여 보낼 수도 있다. 여기서 명령어(command)는 운영체제에 특정 작업을 수행하도록 요청하는 입력이라고 볼 수 있다.</p>
<p>예를 들어 <strong>ps</strong> 명령어를 사용하면 현재 실행 중인 프로세스 목록이 출력된다. 각 프로세스에는 PID(Process ID)라는 고유 번호가 함께 표시되는데, 이 PID를 기준으로 특정 프로세스를 선택하여 signal을 보낼 수 있다.</p>
<p>여기서 <strong>kill</strong> 명령어는 특정 프로세스에 signal을 보내는 명령어이다. 이때 kill 뒤에는 어떤 signal을 보낼 것인지 함께 지정할 수 있다.</p>
<p>예를 들어 다음과 같은 명령어를 사용할 수 있다.</p>
<pre><code class="language-plaintext">kill -SIGTERM 1234
</code></pre>
<p>이 경우 PID가 1234인 프로세스에 종료 요청 signal인 SIGTERM을 보내게 된다.</p>
<p>또한 다음과 같은 명령어도 사용할 수 있다.</p>
<pre><code class="language-plaintext">kill -SIGINT 1234
</code></pre>
<p>이 명령은 control + C를 누르는 것과 같은 효과를 가지며, 실행을 중단시키는 signal을 보내게 된다.</p>
<p>즉, ps 명령어로 PID를 확인하고, kill 명령어로 원하는 signal을 지정하여 특정 프로세스를 제어할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f3f3da7b-e3dd-4615-bc11-5d9e3b770cb8.png" alt="" style="display:block;margin:0 auto" />

<h3>control + C와 signal 처리 과정</h3>
<p>사용자가 자주 사용하는 control + C를 누르면 프로그램이 바로 종료되는 것처럼 보이지만, 실제로는 signal이 전달되면서 동작이 이루어진다.</p>
<p>사용자가 터미널에서 control + C를 입력하면 터미널이 <strong>SIGINT</strong> signal을 생성하게 되고, 이 signal이 운영체제를 통해 해당 프로세스에 전달된다. 이후 프로세스에 등록된 <strong>signal handler</strong>가 실행되면서 그에 맞는 처리가 이루어진다.</p>
<p>여기서 signal handler는 signal이 전달될 때 실행되는 사용자가 정의한 함수(user-defined function)를 의미한다. 이러한 handler는 프로그램 코드 안에서 signal 함수를 사용하여 미리 등록하게 된다.</p>
<p>예를 들어 다음과 같이 설정할 수 있다.</p>
<pre><code class="language-plaintext">signal(SIGINT, myHandler);
</code></pre>
<p>이 경우 SIGINT가 발생했을 때 <code>myHandler</code> 함수가 실행되도록 동작하게 된다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/ca0f457c-b791-4de2-8f8a-6207ba259373.png" alt="" style="display:block;margin:0 auto" />

<p>이 과정을 그림의 흐름으로 보면 다음과 같다.</p>
<ol>
<li><p>사용자가 control + C를 입력한다.</p>
</li>
<li><p>터미널에서 SIGINT signal이 생성된다.</p>
</li>
<li><p>해당 signal이 커널(Kernel)로 전달된다.</p>
</li>
<li><p>커널은 signal을 프로세스의 PCB(Process Control Block)에 기록한다.</p>
</li>
<li><p>이후 프로세스에 등록된 signal handler가 실행된다.</p>
</li>
</ol>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/41de2ef5-913f-412e-9d48-860664b4cc8b.png" alt="" style="display:block;margin:0 auto" />

<h3>Signal 처리 코드 예제 (signal handling code example)</h3>
<p>이 코드를 보면 SIGINT가 발생했을 때 어떻게 동작하는지를 확인할 수 있다.</p>
<p>먼저 <code>myHandler</code> 함수는 SIGINT signal을 받았을 때 실행되는 함수이며, 여기서는 <code>"SIGNAL RECEIVED"</code>라는 메시지를 출력하도록 되어 있다.</p>
<p>그다음 <code>main</code> 함수 안에서는 다음과 같이 <code>signal(SIGINT, myHandler)</code>를 사용하여 SIGINT가 발생했을 때 해당 함수가 실행되도록 미리 등록해둔다.</p>
<pre><code class="language-plaintext">signal(SIGINT, myHandler);
</code></pre>
<p>이후 while 문 안에서는 <code>pause()</code>를 계속 호출하면서 signal이 올 때까지 대기 상태를 유지하게 된다.</p>
<p>따라서 실제로 control + C를 입력하게 되면 SIGINT가 발생하고, 이후 등록된 <code>myHandler</code> 함수가 실행되는 흐름으로 동작하게 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/58c7dcaa-bd31-4f2c-8a00-03544e5b1c5e.png" alt="" style="display:block;margin:0 auto" />

<h3>Signal 코드 실행 흐름 (signal code execution flow)</h3>
<p>이 과정을 순서대로 살펴보면, 먼저 <code>signal(SIGINT, myHandler)</code>를 통해 SIGINT 처리 함수를 등록해둔다.</p>
<p>그다음 사용자가 control + C를 입력하게 되면 SIGINT signal이 생성되고, 운영체제가 해당 signal을 프로세스에 전달하게 된다. 이후 미리 등록해둔 <code>myHandler</code> 함수가 실행되면서 signal에 대한 처리가 이루어진다.</p>
<p>이처럼 signal handler는 일반적인 프로그램 실행 흐름 중간에 개입하여 동작을 변경하는 방식이라고 이해할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a5288f1c-efe1-4543-ad14-a5249cab85a4.png" alt="" style="display:block;margin:0 auto" />

<h3>Signal 정리</h3>
<p>지금까지 살펴본 Signal을 정리해보면, Signal은 일반적인 데이터 전달 방식과는 다르게 동작한다. 즉, Signal은 데이터를 주고받는 것이 아니라 이벤트를 알리고 프로세스를 제어하기 위한 방식이다.</p>
<p>운영체제는 이러한 Signal의 전달을 관리하며, 필요한 시점에 프로세스의 실행 흐름을 변경하는 역할을 하게 된다.</p>
<p>또한 이러한 점에서 Signal은 인터럽트(Interrupt)와 비슷한 방식으로 동작한다고 볼 수 있다.</p>
<p>지금까지 Signal을 중심으로 이벤트 전달과 프로세스 제어 방식에 대해 살펴보았다.</p>
<hr />
<h1>2️⃣ Inter-process communication methods</h1>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/4ce8b8ff-fee1-4c43-b50d-443e07425e2f.png" alt="" style="display:block;margin:0 auto" />

<h3>데이터 전달 기반 IPC (Data-oriented IPC)</h3>
<p>방금 배운 Signal이라는 것은 어떤 일이 발생했다는 것을 알려주는 역할이었다면, 실제 프로그램에서는 알림만으로는 부족한 경우가 많다. 예를 들어 한 프로그램이 자신이 만든 데이터를 다른 프로그램이 그대로 받아서 사용해야 하는 상황이 있을 수 있다. 이때는 이벤트가 아니라 데이터 자체를 전달해야 하는 상황이 발생한다.</p>
<p>그래서 프로세스 간에는 데이터를 전달하는 별도의 방법이 필요해진다. 이러한 문제를 해결하기 위해 운영체제는 여러 가지 데이터 전달 방식을 제공한다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/beb91166-49bc-4a62-8acf-48883ea80ed1.png" alt="" style="display:block;margin:0 auto" />

<p>한 프로세스에서 만든 데이터를 다른 프로세스로 순서대로 흘려보내는 방식은 Pipe이다. 서로 관련이 없는 프로세스끼리도 이름을 통해 연결하여 데이터를 주고받는 방식은 FIFO (Named Pipe)이다. 하나의 메모리 공간을 함께 사용하면서 데이터를 직접 공유하는 방식은 Shared Memory (공유 메모리)이다.</p>
<p>이처럼 세 가지 모두 데이터를 전달하기 위한 IPC 방식이지만, 구조와 사용 방법, 목적이 서로 다르기 때문에 상황에 맞게 선택해서 사용하면 된다.</p>
<p>그래서 이러한 방식들은 다음 슬라이드에서부터 하나씩 살펴보자.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/cc9fc22f-5464-4d88-b8ac-c9a988a741c8.png" alt="" style="display:block;margin:0 auto" />

<h3>Pipe의 개념 (Concept of Pipe)</h3>
<p>Pipe (Pipe)는 프로세스 간 통신을 위해 운영체제가 제공하는 방법 중 하나이다.</p>
<p>커널 공간(kernel space) 안에 데이터를 잠시 담아둘 수 있는 통로, 즉 버퍼(buffer)를 만들어두고 한 프로세스가 데이터를 써 놓으면 다른 프로세스가 그 데이터를 꺼내가는 방식이다.</p>
<p>즉, 두 프로세스가 직접 메모리를 공유하는 것이 아니라 운영체제가 가운데에 통신용 공간을 마련해 두고, 그 공간을 통해 데이터를 전달하는 구조이다.</p>
<p>이러한 Pipe는 서로 관련이 있는 프로세스 사이에서 많이 사용되며, 대표적으로 부모 프로세스와 자식 프로세스(parent-child process) 사이의 통신에서 자주 사용된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/3a641d5d-ea1a-4234-9da9-87994c8afed2.png" alt="" style="display:block;margin:0 auto" />

<h3>Pipe의 특징 (Characteristics of Pipe)</h3>
<p>하나의 pipe 안에서는 데이터를 보내는 쪽과 데이터를 받는 쪽이 구분되어 있다. 즉 하나의 pipe에서는 한 프로세스가 데이터를 보내고, 다른 프로세스는 데이터를 받아 사용하는 구조이다.</p>
<p>따라서 두 프로세스가 서로 번갈아 가며 자유롭게 데이터를 주고받는 방식은 아니고, 한 방향으로만 데이터가 흐르는 단방향 구조라고 이해하면 된다.</p>
<p>또한 pipe 안에 들어간 데이터는 먼저 들어간 것이 먼저 나오는 방식으로 전달된다. 데이터를 넣은 순서 그대로 꺼내가는 구조이다.</p>
<p>이 pipe는 기본적으로 하나의 컴퓨터 내부에 있는 프로세스들 사이에서 사용하는 방식이며, 다른 컴퓨터와 통신하는 용도로는 사용되지 않는다.</p>
<p>만약 두 프로세스가 서로 데이터를 주고받고 싶다면 하나의 pipe만으로는 부족하며, 보내는 방향과 받는 방향을 따로 만들어야 한다. 이 경우 pipe를 2개 사용하면 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/50e50a94-48c5-47bc-a217-354257f2bec1.png" alt="" style="display:block;margin:0 auto" />

<h3>Pipe의 동작 구조 (Pipe Operation Structure)</h3>
<p>운영체제가 pipe (Pipe)를 만들면 프로세스에게 두 개의 파일 디스크립터(file descriptor)를 반환하게 된다. 여기서 파일 디스크립터는 운영체제가 열어 둔 입출력 대상(input/output resource)을 가리키기 위해 사용되는 번호라고 이해하면 된다.</p>
<p>pipe도 하나의 입출력 통로처럼 다루기 때문에 이러한 파일 디스크립터를 통해 접근하게 된다.</p>
<p>이때 두 개의 디스크립터는 역할이 구분되어 있으며, 일반적으로 다음과 같이 사용된다.</p>
<ul>
<li><p><code>fd[0]</code> : 데이터를 읽는 쪽 (read end)</p>
</li>
<li><p><code>fd[1]</code> : 데이터를 쓰는 쪽 (write end)</p>
</li>
</ul>
<p>즉 pipe는 데이터를 쓰는 쪽과 읽는 쪽이 나눠진 구조로 이해할 수 있다.</p>
<p>따라서 pipe는 겉으로 보기에는 단순한 통로처럼 보이지만, 실제로는 커널(kernel) 내부에 버퍼(buffer)가 생성되고, 그 버퍼에 접근하기 위한 read(), write() 인터페이스와 연결점이 함께 제공되는 구조이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/07376b46-0936-4f3b-8f0b-c57a369823fb.png" alt="" style="display:block;margin:0 auto" />

<h3>Pipe 사용 흐름 (Pipe Usage Flow)</h3>
<p>다음은 pipe(Pipe)를 사용할 때의 흐름이다.</p>
<p>먼저 <code>pipe()</code>를 호출하여 프로세스 간 데이터를 주고받을 수 있는 통신 통로를 생성한다. 그다음 <code>fork()</code>를 통해 부모 프로세스(parent process)와 자식 프로세스(child process)를 생성하게 된다.</p>
<p>이렇게 되면 부모와 자식 프로세스는 <code>pipe()</code> 호출로 생성된 파일 디스크립터(file descriptor)를 공유하게 되며, 두 프로세스 모두 읽기용과 쓰기용 디스크립터를 모두 가지게 된다.</p>
<p>하지만 실제로는 한쪽은 데이터를 보내는 역할, 다른 한쪽은 데이터를 받는 역할을 해야 하기 때문에 각 프로세스는 자신이 사용하지 않는 방향의 디스크립터를 닫아 읽기(read)와 쓰기(write)의 역할을 명확하게 나누게 된다.</p>
<p>이렇게 역할이 정리되면 한 프로세스는 <code>write()</code>를 통해 데이터를 pipe에 쓰고, 다른 프로세스는 <code>read()</code>를 통해 해당 데이터를 읽는 구조로 동작하게 된다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/db2e1a2f-4001-4f98-a792-88e45ab654f0.png" alt="" style="display:block;margin:0 auto" />

<p>그림의 흐름으로 보면 부모 프로세스와 자식 프로세스가 각각 파일 디스크립터를 가지고 있으며, 한쪽에서는 <code>write()</code>를 통해 데이터를 보내고 다른 한쪽에서는 <code>read()</code>를 통해 데이터를 받아가는 구조로 이루어진다.</p>
<p>이때 데이터는 프로세스 사이를 직접 이동하는 것이 아니라 커널(kernel) 내부에 있는 pipe 버퍼(pipe buffer)를 거쳐 전달된다.</p>
<p>즉, pipe는 프로세스끼리 직접 연결되는 구조가 아니라 커널을 통해 데이터를 주고받는 구조라고 이해할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a4fa7c8f-90bf-4fdb-8b87-d0bd53229627.png" alt="" style="display:block;margin:0 auto" />

<h3>FIFO의 개념 (Concept of FIFO)</h3>
<p>FIFO는 Pipe와 비슷하지만 가장 큰 차이점은 이름(name)을 가지고 있다는 점이다.</p>
<p>Pipe는 부모 프로세스(parent process)와 자식 프로세스(child process)처럼 서로 관계가 있는 프로세스 사이에서 주로 사용된다. 그러나 FIFO는 파일처럼 이름이 존재하기 때문에 서로 관련이 없는 프로세스라도 같은 이름을 알고 있으면 연결하여 사용할 수 있는 구조이다.</p>
<p>즉, 파일 시스템(file system) 안에 특수한 형태의 파일로 만들어두고, 해당 파일을 통해 데이터를 주고받는 방식이라고 볼 수 있다.</p>
<p>다시 말해, 통신 통로를 미리 만들어두고 필요한 프로세스들이 그 통로에 연결하여 데이터를 주고받는 방식이라고 이해할 수 있다.</p>
<p>따라서 FIFO는 Pipe를 확장한 형태이며, 이름을 통해 접근할 수 있는 통신 방식이라고 할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/1bb26f6b-8d29-490e-889b-9e740a9a528c.png" alt="" style="display:block;margin:0 auto" />

<h3>FIFO의 특징 (Characteristics of FIFO)</h3>
<p>FIFO는 겉으로는 일반 파일(file)처럼 보이기 때문에 <code>open()</code>, <code>read()</code>, <code>write()</code>, <code>close()</code>와 같은 파일 입출력(file I/O) 방식을 그대로 사용한다.</p>
<p>하지만 실제로는 파일에 데이터를 저장하는 것이 아니라 커널(kernel) 안에 있는 버퍼(buffer)를 통해 데이터를 전달하는 구조이다. 따라서 응용 프로그램(application program) 입장에서는 파일처럼 보이지만, 실제로는 커널 내부 큐(queue)에 저장되어 메모리 기반으로 동작한다고 볼 수 있다.</p>
<p>또한 중요한 점은 여러 프로세스가 하나의 FIFO를 공유하여 통신할 수 있다는 점이다. 이때 데이터는 FIFO 방식과 마찬가지로 먼저 들어간 것이 먼저 나오는 방식으로 처리된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/b5db562e-99c3-432a-8134-ebc2b3ed58e7.png" alt="" style="display:block;margin:0 auto" />

<h3>FIFO의 동작 구조 (FIFO Operation Structure)</h3>
<p>FIFO를 사용할 때 한쪽은 데이터를 보내는 역할로 연결하며, 이는 쓰기 모드(write mode)이다. 다른 한쪽은 데이터를 받는 역할로 연결하며, 이는 읽기 모드(read mode)이다.</p>
<p>이렇게 연결이 이루어지면 한쪽에서 데이터를 쓰고(write), 다른 한쪽에서는 해당 데이터를 읽는(read) 흐름으로 데이터 전달이 이루어진다.</p>
<p>겉으로 보면 파일을 읽고 쓰는 것처럼 보이지만, 실제로 확인해보면 파일 크기는 0바이트로 보이게 된다. 즉, 데이터가 실제 파일에 저장되는 것이 아니라 커널(kernel) 내부 버퍼(buffer)에 임시로 저장되었다가 전달되는 구조이다.</p>
<p>따라서 FIFO는 데이터를 저장하는 파일이라기보다는 프로세스가 접근하기 위한 통신 통로 역할을 한다고 이해할 수 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/91a1cad1-1997-4ef9-9b94-5e8bc28f221c.png" alt="" style="display:block;margin:0 auto" />

<p>그림의 흐름으로 보면 한 프로세스는 FIFO를 쓰기 모드로 열고(open), 다른 프로세스는 읽기 모드로 열어서 연결하게 된다. 이후 한쪽에서 <code>write()</code>를 통해 데이터를 보내면 커널 내부의 FIFO 버퍼에 데이터가 저장되고, 다른 한쪽에서는 <code>read()</code>를 통해 데이터를 읽어가는 구조로 동작한다.</p>
<p>여기서 중요한 점은 FIFO가 파일 시스템(file system) 안에 존재하기 때문에 <code>open()</code>, <code>read()</code>, <code>write()</code>처럼 파일을 다루는 방식 그대로 접근할 수 있다는 점이다.</p>
<p>즉, 사용하는 입장에서는 일반 파일을 열고 읽고 쓰는 것처럼 보이지만, 실제로는 데이터가 파일에 저장되는 것이 아니라 커널 내부 버퍼를 통해 바로 전달되고 있다는 점을 이해해야 한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/21192106-29f2-432c-a82b-87b5dd64ffca.png" alt="" style="display:block;margin:0 auto" />

<h3>FIFO와 Pipe의 비교 (Comparison of FIFO and Pipe)</h3>
<p>Pipe와 FIFO는 모두 프로세스 간에 데이터를 전달하는 방식이라는 점에서 비슷하며, 커널 버퍼(kernel buffer)를 통해 데이터를 주고받는 구조도 동일하다. 또한 겉으로는 파일 입출력(file I/O) 방식으로 접근한다는 점에서도 유사하게 보일 수 있다.</p>
<p>하지만 실제로는 차이점이 존재하며, 이를 기준으로 Pipe와 FIFO를 구분할 수 있다.</p>
<p>Pipe는 이름(name)이 없기 때문에 일반적으로 <code>pipe()</code>와 <code>fork()</code>를 통해 생성된 부모 프로세스(parent process)와 자식 프로세스(child process) 사이에서 파일 디스크립터(file descriptor)를 공유하여 사용하는 경우가 많다.</p>
<p>반면 FIFO는 파일 시스템(file system)에 이름을 가진 형태로 생성되기 때문에 서로 관련이 없는 프로세스라도 같은 이름의 FIFO를 열어(open) 연결할 수 있다.</p>
<p>즉, Pipe와 FIFO는 동작 방식은 비슷하지만, 이름을 통해 접근할 수 있다는 점에서 FIFO는 Pipe를 확장한 형태라고 정리할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a94c9d13-091f-46bd-a360-c86c67cd50e2.png" alt="" style="display:block;margin:0 auto" />

<h3>IPC 방식 비교 (Comparison of IPC Mechanisms)</h3>
<p>Signal은 데이터를 전달하는 방식이 아니라 이벤트(event)를 알리고 프로세스를 제어(control)하는 데 사용하는 방식이다.</p>
<p>Pipe와 FIFO는 모두 커널 버퍼(kernel buffer)를 통해 데이터를 전달하는 방식이라는 공통점이 있다. 하지만 Pipe는 주로 부모 프로세스(parent process)와 자식 프로세스(child process) 관계에서 사용되며, FIFO는 이름(name)을 통해 서로 관련 없는 프로세스끼리도 사용할 수 있다는 차이가 있다.</p>
<p>그리고 Shared Memory는 다른 방식과 달리 데이터를 복사하여 전달하는 것이 아니라 하나의 메모리 공간(memory space)을 직접 공유하는 방식이다. 따라서 동기화(synchronization)가 반드시 필요하다.</p>
<p>데이터 전달 속도를 비교하면 일반적으로 Shared Memory가 가장 빠르며, 그다음이 Pipe와 FIFO이다. Signal은 데이터를 전달하는 방식이 아니기 때문에 제한적인 형태의 IPC라고 볼 수 있다.</p>
<p>결국 어떤 IPC 방식을 사용할지는 구조와 목적에 따라 달라진다. 데이터를 얼마나 빠르게 주고받아야 하는지, 프로세스 간 관계가 있는지, 그리고 동기화가 필요한 상황인지를 고려하여 적절한 IPC 방식을 선택해야 한다.</p>
<hr />
<h1>3️⃣ Inter-process Communication Practice</h1>
<h3>IPC 실행 예제 소개 (Introduction to IPC Execution Examples)</h3>
<p>지금까지 Signal과 다양한 IPC 구조 및 특징에 대해서 살펴보았다.</p>
<p>이제부터는 실제 실행 예제를 통해 프로세스 간 통신(Process Communication)이 어떻게 이루어지는지 확인해볼 예정이다.</p>
<p>특히 Signal, Pipe, FIFO, Shared Memory가 실제 코드에서 어떻게 동작하는지, 그리고 데이터가 어떤 흐름으로 전달되는지를 함께 살펴보게 된다.</p>
<hr />
<h3>Signal 처리 실습 내용 안내 (Signal Handling Practice)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/fac45d2a-3064-4b76-8445-a21284c12192.png" alt="" style="display:block;margin:0 auto" />

<h4>1) Signal의 역할 (Role of Signal)</h4>
<p><code>signal()</code> 함수는 특정 signal이 발생했을 때 어떤 함수를 실행할지를 미리 등록(register)하는 역할을 한다. 즉 signal이 발생하면 운영체제가 단순히 기본 동작(default action)을 수행하는 것이 아니라, 사용자가 지정한 함수(handler function)가 실행되도록 설정하는 것이다.</p>
<hr />
<h4>2) 함수 형태 (Function Prototype)</h4>
<p><code>signal()</code> 함수의 형태는 다음과 같다.</p>
<pre><code class="language-plaintext">void (*signal(int signum, void (*handler)(int)))(int);
</code></pre>
<p>이 함수는 첫 번째 인자로 signal 번호(signal number)인 <code>signum</code>을 받고, 두 번째 인자로는 해당 signal 발생 시 실행할 함수(handler function)를 받는다.</p>
<p>여기서 <code>void (*handler)(int)</code> 부분은 “정수 하나를 입력으로 받는 함수”를 의미한다.</p>
<p>즉 사용자가 정의하는 handler 함수는 signal 번호를 인자로 받는 함수 형태로 작성해야 한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/6a384bef-3401-4729-87e1-7bb32a54c722.png" alt="" style="display:block;margin:0 auto" />

<h4>3) 주요 매개변수 (Main Parameters)</h4>
<p><code>signum</code>은 어떤 signal이 발생했는지를 나타내는 값이다. <code>handler</code>는 해당 signal이 발생했을 때 실행되는 함수이다.</p>
<p>따라서 <code>signal()</code> 함수는 “특정 signal이 발생하면 지정된 함수를 실행하라”라고 등록하는 역할을 수행한다고 이해할 수 있다.  </p>
<h4>4) <code>signal(SIGINT, myHandler);</code></h4>
<p>다음 코드를 보면:</p>
<pre><code class="language-plaintext">signal(SIGINT, myHandler);
</code></pre>
<p>SIGINT signal이 발생했을 때 <code>myHandler</code> 함수가 실행되도록 연결해두는 의미가 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/911b505f-58e7-4d9f-b38e-4ff8e3c8a9e1.png" alt="" style="display:block;margin:0 auto" />

<h4>5) 핵심 동작 (Core Behavior)</h4>
<p>실제 동작 흐름을 보면 사용자가 control + C를 입력하면 SIGINT signal이 발생하게 된다.</p>
<p>이때 미리 등록해둔 <code>myHandler</code> 함수가 실행된다. 그리고 handler 실행이 끝나면 프로그램은 다시 원래 실행 흐름으로 돌아가 계속 진행된다.</p>
<hr />
<h3><code>signal()</code> 예제 코드 (<code>signal()</code> Example Code)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/6070a1ad-d4d1-424c-83b2-16ca4d0c709a.png" alt="" style="display:block;margin:0 auto" />

<p>예제 코드를 살펴보면, <code>main()</code> 함수 안의 다음 코드가 먼저 실행된다.</p>
<pre><code class="language-plaintext">signal(SIGINT, myHandler);
</code></pre>
<p>이 코드는 SIGINT signal이 발생했을 때 <code>myHandler</code> 함수가 실행되도록 등록(register)하는 역할을 한다. 그 아래에는 <code>while(1)</code> 반복문이 있으며, 프로그램이 계속 실행되면서 다음 문장을 반복 출력한다.</p>
<pre><code class="language-plaintext">printf("프로그램 실행중..\n");
</code></pre>
<p>그리고 <code>sleep(3)</code>을 통해 3초씩 대기하게 된다.</p>
<p>즉 이 프로그램은 종료되지 않고 계속 실행되면서 signal이 들어오기를 기다리는 상태라고 볼 수 있다. 그런데 이 상태에서 control + C를 입력하면 SIGINT signal이 발생하고, 등록해두었던 <code>myHandler</code> 함수가 실행된다.</p>
<pre><code class="language-plaintext">void myHandler(int sig) {
    printf("\nSIGINT received!\n");
}
</code></pre>
<p>실행 결과를 보면 <code>"프로그램 실행중.."</code>이 계속 출력되다가, control + C를 누르는 순간 <code>"SIGINT received!"</code> 문장이 출력된다.</p>
<p>그리고 handler 실행이 끝나면 프로그램이 종료되는 것이 아니라 다시 <code>while</code> 반복문으로 돌아가 <code>"프로그램 실행중.."</code>을 계속 출력하게 된다.</p>
<p>즉 여기서는 control + C가 프로그램을 종료시키는 것이 아니라, 사용자가 정의한 함수만 실행하고 다시 원래 흐름으로 복귀하는 동작을 하게 된다.</p>
<p>따라서 이 프로그램을 완전히 종료하려면 터미널에서 control + <code>\</code>를 눌러 SIGQUIT signal을 보내거나, 다른 터미널에서 <code>kill</code> 명령어를 통해 프로세스를 종료해야 한다.</p>
<p>결국 이 예제는 signal이 발생했을 때 프로그램이 즉시 종료되는 것이 아니라, 등록된 handler 함수가 먼저 실행된 뒤 다시 원래 실행 흐름으로 돌아간다는 점을 보여주는 예제라고 할 수 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/8c83f7cb-1d25-4de0-80a6-8c3ea06c7a7d.png" alt="" style="display:block;margin:0 auto" />

<h3>Signal 처리 실행 결과 (Signal Handling Execution Result)</h3>
<p>코드를 실행하면 프로그램은:</p>
<pre><code class="language-plaintext">프로그램 실행중..
</code></pre>
<p>이라는 메시지를 반복적으로 출력하면서 계속 동작한다. 이 상태에서 사용자가 <code>Control + C</code>를 입력하면 프로그램이 바로 종료되지 않고:</p>
<pre><code class="language-plaintext">SIGINT received!
</code></pre>
<p>라는 메시지가 출력된다.</p>
<p>이 동작은 <code>Control + C</code> 입력으로 인해 <code>SIGINT</code> 시그널(signal)이 발생하고, 미리 등록해두었던 <code>myHandler()</code> 함수가 실행되었기 때문이다. 그 이후 프로그램은 종료되지 않고 다시 반복문(loop)으로 돌아가 계속해서:</p>
<pre><code class="language-plaintext">프로그램 실행중..
</code></pre>
<p>을 출력하게 된다. 즉 여기서는 <code>Control + C</code> 입력이 기본 동작(default action)처럼 프로그램을 종료시키는 것이 아니라, 사용자가 직접 정의한 동작(user-defined handler)으로 변경된 상태라고 이해하면 된다. 따라서 이 상태에서 프로그램을 완전히 종료하려면: <code>Control + \</code> 를 입력하여 <code>SIGQUIT</code> 시그널(signal)을 보내거나 다른 터미널에서 <code>kill</code> 명령어를 사용하여 프로세스를 종료해야 한다.  </p>
<p>결국 이 실습은 동일한 <code>Control + C</code> 입력이라도, 시그널 처리 방식(signal handling)을 어떻게 설정하느냐에 따라 프로그램 동작이 달라질 수 있다는 점을 보여주는 예제라고 볼 수 있다.</p>
<hr />
<h3>Pipe 통신과 <code>pipe()</code> 함수 (Pipe Communication and <code>pipe()</code> Function)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/25a39fc2-4ad9-4b83-9d47-69783360a07a.png" alt="" style="display:block;margin:0 auto" />

<h4>1) 함수의 역할 (Role of the Function)</h4>
<p><code>pipe()</code> 함수는 프로세스 간 데이터를 주고받기 위한 통신 통로(communication channel)를 생성하는 역할을 한다.</p>
<p>**<br />2) 함수의 형태 (Function Prototype)**</p>
<p><code>pipe()</code> 함수의 형태는 다음과 같다.</p>
<pre><code class="language-plaintext">int pipe(int fd[2]);
</code></pre>
<p>이 함수가 실행되면 두 개의 파일 디스크립터(file descriptor)를 배열 형태로 반환하게 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/da4bb2a0-3c88-4bfe-bbb2-bbebfd3f9bfb.png" alt="" style="display:block;margin:0 auto" />

<h4>3) 주요 매개변수 (Main Parameters)</h4>
<p>이때 각 파일 디스크립터의 역할은 다음과 같다.</p>
<ul>
<li><p><code>fd[0]</code> : 데이터를 읽는 쪽 (read end)</p>
</li>
<li><p><code>fd[1]</code> : 데이터를 쓰는 쪽 (write end)</p>
</li>
</ul>
<p>즉 하나의 pipe 안에서 읽기와 쓰기 역할이 구분되어 있다고 이해할 수 있다.</p>
<ol>
<li>핵심 동작 (Core Behavior)</li>
</ol>
<p><code>pipe()</code>를 호출하면 해당 통로는 커널(kernel) 내부에 생성된다. 그리고 읽기용(read end)과 쓰기용(write end)이 구분된 pipe 구조가 만들어지게 된다.</p>
<p>이 pipe는 커널 내부에서 생성되기 때문에 프로세스끼리 직접 데이터를 주고받는 것이 아니라, 커널 내부 버퍼(kernel buffer)를 통해 데이터를 전달하게 된다.</p>
<p>또한 <code>fork()</code> 이후에는 부모 프로세스(parent process)와 자식 프로세스(child process)가 파일 디스크립터를 그대로 공유하게 되므로, 두 프로세스는 같은 pipe를 이용하여 통신할 수 있게 된다. 따라서 하나의 pipe를 통해 두 프로세스가 데이터를 주고받을 수 있게 된다.</p>
<p>또한 pipe는 한 방향으로만 데이터가 흐르기 때문에 읽기와 쓰기가 분리된 단방향 통신(unidirectional communication) 구조라고 볼 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/2fc2f469-b66f-448f-8686-2bf0ef4d8f31.png" alt="" style="display:block;margin:0 auto" />

<h4>5) <code>read()</code>와 <code>write()</code></h4>
<p><code>write()</code> 함수는 <code>fd[1]</code>을 통해 pipe에 데이터를 쓰는 역할을 한다.</p>
<p>반면 <code>read()</code> 함수는 <code>fd[0]</code>을 통해 pipe로부터 데이터를 읽어오는 역할을 한다.</p>
<p>따라서 일반적으로는 부모 프로세스가 데이터를 쓰고(write), 자식 프로세스가 데이터를 읽는(read) 구조로 많이 사용된다.</p>
<p>즉, pipe 통로를 먼저 만들고, 이후 <code>fork()</code>로 프로세스를 분리한 다음, 한쪽은 쓰기(write), 다른 한쪽은 읽기(read)를 수행하는 방식으로 데이터가 전달된다. 결국 pipe는 데이터가 한 방향으로 흐르는 IPC 방식이라고 정리할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/2cb54f84-3e21-4070-b68c-6f1a4d4734d4.png" alt="" style="display:block;margin:0 auto" />

<h3>Pipe 예제 코드 (Pipe Example Code)</h3>
<p>먼저 다음 코드 부분을 보면:</p>
<pre><code class="language-plaintext">if (pipe(fd) == -1) {
    perror("pipe");
    exit(1);
}
</code></pre>
<p><code>pipe(fd)</code>를 호출하여 프로세스 간 데이터를 주고받기 위한 pipe를 생성한다.</p>
<p>이때:</p>
<ul>
<li><p><code>fd[0]</code> : 읽기용(read end) 파일 디스크립터(file descriptor)</p>
</li>
<li><p><code>fd[1]</code> : 쓰기용(write end) 파일 디스크립터 로 설정된다.</p>
</li>
</ul>
<p>그다음에는:</p>
<pre><code class="language-plaintext">pid = fork();
</code></pre>
<p>를 통해 부모 프로세스(parent process)와 자식 프로세스(child process)로 분리된다.</p>
<hr />
<p>자식 프로세스 부분을 보면 다음과 같다.</p>
<pre><code class="language-plaintext">else if (pid == 0) { // 자식: 읽기
    close(fd[1]);

    read(fd[0], buf, sizeof(buf));

    printf("자식 프로세스가 받은 메시지: %s\n", buf);

    close(fd[0]);
}
</code></pre>
<p>여기서 <code>close(fd[1]);</code>은 쓰기용(write end) 디스크립터를 닫는 동작이다. 자식 프로세스는 데이터를 읽기만 하기 때문이다.</p>
<p>그다음 <code>read(fd[0], buf, sizeof(buf));</code>를 통해 pipe로 전달된 데이터를 읽어서 <code>buf</code>에 저장하게 된다.</p>
<p>이후 <code>printf()</code>를 사용하여 읽은 데이터를 출력한다.</p>
<hr />
<p>반대로 부모 프로세스 부분은 다음과 같다.</p>
<pre><code class="language-plaintext">else { // 부모: 쓰기
    close(fd[0]);

    char msg[] = "Hello from parent!";

    write(fd[1], msg, strlen(msg) + 1);

    close(fd[1]);

    wait(NULL);
}
</code></pre>
<p>부모 프로세스는 읽기용(read end) 디스크립터인 <code>fd[0]</code>을 닫고, <code>write()</code>를 통해 <code>"Hello from parent!"</code> 문자열을 pipe에 기록한다.</p>
<p>이렇게 되면 부모 프로세스가 작성한 데이터가 커널(kernel) 내부의 pipe buffer를 통해 전달되고, 자식 프로세스는 해당 데이터를 읽어서 출력하게 된다.</p>
<p>따라서 실행 결과에서는 자식 프로세스가 부모 프로세스가 보낸 메시지를 받아 <code>"Hello from parent!"</code>를 출력하는 것을 확인할 수 있다.</p>
<p>즉 이 예제는 pipe를 이용하여 부모 프로세스가 데이터를 보내고(write), 자식 프로세스가 해당 데이터를 읽는(read) IPC 구조를 보여주는 예제라고 볼 수 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/0a5f2bae-b218-4da8-adfe-55bfcfa10e3e.png" alt="" style="display:block;margin:0 auto" />

<h3>Pipe 예제 코드 실행 결과 (Pipe Example Execution Result)</h3>
<p>다음은 Pipe를 이용한 IPC 예제 코드의 실행 결과이다. 이 예제는 부모 프로세스(parent process)와 자식 프로세스(child process)가 데이터를 주고받는 구조를 확인하기 위한 프로그램이다. 프로그램을 실행하면 부모 프로세스가:</p>
<pre><code class="language-plaintext">Hello from parent!
</code></pre>
<p>라는 메시지를 pipe에 기록(write)하게 되고, 자식 프로세스는 해당 메시지를 pipe로부터 읽어(read) 출력하게 된다. 실행 결과에서는 자식 프로세스가 부모 프로세스로부터 전달받은 메시지를 출력하는 화면을 확인할 수 있다. 겉으로 보면 부모 프로세스와 자식 프로세스가 각각 독립적으로 동작하는 것처럼 보이지만, 실제로는 커널(kernel) 내부의 pipe buffer를 통해 데이터가 전달되고 있다.</p>
<p>또한,</p>
<ul>
<li><p>부모 프로세스(parent process)는 write 역할</p>
</li>
<li><p>자식 프로세스(child process)는 read 역할</p>
</li>
</ul>
<p>로 나뉘어 동작하게 된다.</p>
<p>즉 Pipe는 데이터를 한 방향(one-way direction)으로 전달하는 단방향(unidirectional) 통신 구조라는 것을 확인할 수 있다. 결국 이 실습은 Pipe가 부모-자식 프로세스 간(parent-child process)의 통신에서 사용되며, 커널 내부 버퍼(kernel buffer)를 이용하여 데이터를 전달하는 IPC 방식이라는 점을 보여주는 예제라고 볼 수 있다.</p>
<hr />
<h3>FIFO 통신과 <code>mkfifo()</code> 함수 (FIFO Communication and <code>mkfifo()</code> Function)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/e2cf9b73-539d-4732-b206-81f2a9bb1ff9.png" alt="" style="display:block;margin:0 auto" />

<h4>1) <code>mkfifo()</code>의 역할 (Role of <code>mkfifo()</code>)</h4>
<p><code>mkfifo()</code> 함수는 이름 있는 파이프(named pipe), 즉 FIFO 파일을 생성하는 역할을 한다.</p>
<p>일반적인 Pipe는 프로그램 내부에서 생성되지만, FIFO는 파일 시스템(file system) 안에 실제 파일 형태로 생성된다는 특징이 있다.</p>
<ol>
<li>함수 형태 (Function Prototype)</li>
</ol>
<p><code>mkfifo()</code> 함수의 형태는 다음과 같다.</p>
<pre><code class="language-plaintext">int mkfifo(const char *pathname, mode_t mode);
</code></pre>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/4969fd0c-f695-44a6-86e9-7a9c7eba7dcf.png" alt="" style="display:block;margin:0 auto" />

<h4>3) 주요 매개변수 (Main Parameters)</h4>
<ul>
<li><p><code>pathname</code> : FIFO 파일 이름(path)</p>
</li>
<li><p><code>mode</code> : 접근 권한(permission)을 설정하는 값</p>
</li>
</ul>
<p>을 의미한다. 즉 이 함수를 실행하면 파일 시스템 안에 통신용 FIFO 파일이 생성된다고 볼 수 있다.</p>
<ol>
<li>핵심 동작 (Core Behavior)</li>
</ol>
<p>이렇게 생성된 FIFO는 일반 파일처럼 한 번 생성해두면 여러 프로그램에서 반복해서 사용할 수 있다.</p>
<p>또한 FIFO는 파일 이름(name)을 기준으로 접근하기 때문에 부모 프로세스(parent process)와 자식 프로세스(child process) 관계가 아니더라도 서로 관련 없는 프로세스끼리 통신할 수 있다. 즉 Pipe가 프로세스 관계 기반으로 연결된다면, FIFO는 “이름(name)” 기반으로 연결되는 구조라고 볼 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/c0b5eded-2e4d-4c95-9390-42af65118227.png" alt="" style="display:block;margin:0 auto" />

<h4>5) <code>open()</code>, <code>read()</code>, <code>write()</code></h4>
<p>FIFO를 실제로 사용할 때는 <code>open()</code>을 이용하여 FIFO 파일을 열고 통신을 준비하게 된다.</p>
<ul>
<li><p>송신 프로세스(sender process)는 <code>O_WRONLY</code> 모드로 open</p>
</li>
<li><p>수신 프로세스(receiver process)는 <code>O_RDONLY</code> 모드로 open 하여 역할을 나누게 된다.</p>
</li>
</ul>
<p>그 후 <code>write()</code>를 통해 데이터를 보내고, <code>read()</code>를 통해 데이터를 읽는 방식으로 통신이 이루어진다.</p>
<p>겉으로 보면 일반 파일 입출력(file I/O)처럼 보이지만, 실제로는 프로세스 간 데이터 전달이 이루어지는 IPC 구조라고 이해하면 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/dc188299-5e47-4264-a2bb-fa6652e3c0f9.png" alt="" style="display:block;margin:0 auto" />

<h3>FIFO 송신 예제 코드 (FIFO Sender Example Code)</h3>
<p>FIFO 통신에서는 한쪽은 데이터를 보내는 송신(sender) 역할을 하고, 다른 한쪽은 데이터를 받는 수신(receiver) 역할을 하도록 각각 코드를 작성해야 한다.</p>
<p>먼저 데이터를 보내는 송신 측 코드를 보면 다음과 같다.</p>
<pre><code class="language-c">int main() {
    const char *fifo = "myfifo";

    mkfifo(fifo, 0666);

    int fd = open(fifo, O_WRONLY);

    char msg[] = "Hello FIFO!";

    write(fd, msg, strlen(msg) + 1);

    close(fd);

    return 0;
}
</code></pre>
<p>먼저:</p>
<pre><code class="language-c">mkfifo(fifo, 0666);
</code></pre>
<p>는 <code>"myfifo"</code>라는 이름의 FIFO 파일을 생성하는 코드이다. 여기서 <code>0666</code>은 FIFO 파일의 접근 권한(permission)을 의미한다. FIFO는 파일 시스템(file system)에 생성되기 때문에 다른 프로그램에서도 동일한 이름으로 접근할 수 있다.</p>
<p>실제로 터미널에서 실행 후 <code>ls</code> 명령어로 확인해보면 <code>"myfifo"</code>라는 파일이 생성된 것을 볼 수 있다. 하지만 이 파일은 일반 파일(regular file)이 아니라 FIFO 형태의 특수 파일(special file)이다. 즉 디스크에 데이터를 저장하는 파일이 아니라, 프로세스 간 통신(IPC)을 위한 통로 역할을 하는 파일이라고 이해하면 된다.</p>
<p>따라서 겉으로 보기에는 파일처럼 보이지만 실제로는 커널(kernel) 내부 버퍼(buffer)와 연결된 입출력 통로이다.</p>
<p>그다음:</p>
<pre><code class="language-c">int fd = open(fifo, O_WRONLY);
</code></pre>
<p>를 통해 FIFO를 <code>O_WRONLY</code> 모드(write-only mode)로 열게 된다. 이는 해당 프로세스가 데이터를 보내는 역할을 수행하겠다는 의미이다. 이후:</p>
<pre><code class="language-plaintext">write(fd, msg, strlen(msg) + 1);
</code></pre>
<p>를 통해 <code>"Hello FIFO!"</code> 문자열을 FIFO에 기록하게 된다. 마지막으로:</p>
<pre><code class="language-plaintext">close(fd);
</code></pre>
<p>를 호출하여 FIFO 파일을 닫게 된다. 즉 이 예제는 FIFO를 생성하고, 송신 프로세스가 데이터를 write()를 통해 FIFO에 전달하는 구조를 보여주는 예제라고 볼 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/59261934-7178-476b-ad23-fd43cac71589.png" alt="" style="display:block;margin:0 auto" />

<h3>FIFO 수신 예제 코드 (FIFO Receiver Example Code)</h3>
<p>다음은 데이터를 받는 쪽인 수신(receiver) 프로그램이다. 수신 측에서도 동일하게 <code>"myfifo"</code>라는 이름의 FIFO를 사용하게 된다. 이번에는 FIFO를 읽기 전용(read-only) 모드로 열어 데이터를 읽는 역할을 수행한다.</p>
<p>예를 들면 다음과 같은 형태이다.</p>
<pre><code class="language-c">int main() {
    const char *fifo = "myfifo";

    mkfifo(fifo, 0666);

    int fd = open(fifo, O_RDONLY);

    char buf[100];

    read(fd, buf, sizeof(buf));

    printf("수신한 메시지: %s\n", buf);

    close(fd);

    return 0;
}
</code></pre>
<p>여기서:</p>
<pre><code class="language-plaintext">int fd = open(fifo, O_RDONLY);
</code></pre>
<p>는 FIFO를 <code>O_RDONLY</code> 모드(read-only mode)로 열어 데이터를 읽겠다는 의미이다. 그다음:</p>
<pre><code class="language-plaintext">read(fd, buf, sizeof(buf));
</code></pre>
<p>를 통해 FIFO로 들어온 데이터를 읽어서 <code>buf</code>에 저장하게 된다. 이후:</p>
<pre><code class="language-plaintext">printf("수신한 메시지: %s\n", buf);
</code></pre>
<p>를 통해 송신 측에서 전달한 <code>"Hello FIFO!"</code> 문자열이 출력되는 것을 확인할 수 있다.</p>
<p>이 두 프로그램은 서로 독립된 프로그램처럼 보이지만, 동일한 <code>"myfifo"</code>라는 이름의 FIFO를 사용하기 때문에 서로 연결되어 있는 구조이다.</p>
<p>즉 한쪽에서 <code>write()</code>를 통해 데이터를 쓰면, 해당 데이터는 커널(kernel) 내부 FIFO buffer를 통해 전달되고, 다른 한쪽에서는 <code>read()</code>를 통해 그 데이터를 읽어오게 된다.</p>
<p>따라서 실행 결과에서는 <code>"Hello FIFO!"</code>가 출력되는 것을 확인할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/7fd886b4-4450-48fd-972e-150584c72540.png" alt="" style="display:block;margin:0 auto" />

<h3>FIFO 예제 코드 실행 결과 (FIFO Example Execution Result)</h3>
<p>이 코드는 FIFO(named pipe)를 이용하여 두 프로세스가 데이터를 주고받는 과정을 확인하기 위한 예제이다. 여기서는 송신(sender) 프로그램과 수신(receiver) 프로그램을 함께 실행해야 하므로, 10-3 파일과 10-4 파일을 같이 확인해야 한다. 즉 송신과 수신이 동시에 이루어지는 구조이다.</p>
<p>실습에서는 송신 프로그램과 수신 프로그램을 각각 실행해야 하기 때문에 터미널(terminal)을 두 개 사용하였다. 예를 들어 터미널을 우클릭하여 새 창(new terminal window)을 열고, 각각의 터미널에서 해당 폴더로 이동한 뒤 프로그램을 컴파일하고 실행한다.</p>
<p>송신(sender) 프로그램 10-3 을 실행하면:</p>
<pre><code class="language-plaintext">Hello FIFO!
</code></pre>
<p>메시지를 FIFO 파일에 <code>write()</code> 하게 된다.</p>
<p>반면 수신(receiver) 프로그램에서는 FIFO 파일로 전달된 데이터를 <code>read()</code>로 읽고:</p>
<pre><code class="language-plaintext">printf(...)
</code></pre>
<p>를 통해 <code>"Hello FIFO!"</code> 문자열이 출력되는 것을 확인할 수 있다.</p>
<hr />
<p>여기서 중요한 점은 두 프로그램 중 하나만 실행해서는 정상적으로 동작하지 않는다는 점이다.</p>
<p>예를 들어 송신 프로그램만 실행하면 데이터를 받을 대상(receiver)이 없기 때문에 대기(blocking) 상태가 된다. 수신 프로그램도 마찬가지로 데이터가 들어올 때까지 기다리게 된다.</p>
<p>따라서 이 실습에서는 반드시 두 개의 터미널을 열어서:</p>
<ul>
<li><p>송신(sender) 프로그램</p>
</li>
<li><p>수신(receiver) 프로그램</p>
</li>
</ul>
<p>을 각각 실행해야 한다. 이렇게 두 프로그램이 동시에 실행되면:</p>
<ul>
<li><p>송신 측은 <code>write()</code>를 통해 데이터를 FIFO에 기록하고</p>
</li>
<li><p>커널(kernel) 내부 FIFO buffer를 통해 데이터가 전달되며</p>
</li>
<li><p>수신 측은 <code>read()</code>를 통해 해당 데이터를 읽어오게 된다.</p>
</li>
</ul>
<p>또 하나 확인할 수 있는 점은 <code>ls</code> 명령어를 실행하면 <code>"myfifo"</code>라는 파일이 생성된다는 점이다. 하지만 이 파일은 일반 파일(regular file)이 아니다.</p>
<p>예를 들어 일반 파일처럼 내용을 확인하려고 하면, 실제 데이터가 저장되어 있는 구조가 아니기 때문에 아무 내용도 출력되지 않거나 대기 상태(blocking state)가 될 수 있다.</p>
<p>즉 <code>myfifo</code>는 데이터를 저장하는 파일이라기보다는, 프로세스 간 통신(IPC)을 위한 연결 통로(communication channel) 역할을 하는 특수 파일(special file)이라고 이해하면 된다.</p>
<p>결국 이 실습은 FIFO가 파일 형태(file-like interface)를 사용하지만, 실제로는 커널 버퍼(kernel buffer)를 이용하여 프로세스 간 데이터를 전달하는 IPC 방식이라는 점을 보여주는 예제라고 볼 수 있다.</p>
<hr />
<h3>Shared Memory 통신 (Shared Memory Communication)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/6546cda0-fc9f-4262-af4f-adc47baf8b1a.png" alt="" style="display:block;margin:0 auto" />

<p><strong>1)shm_open()의 역할 (Role of shm_open())</strong></p>
<p>shm_open() 함수는 여러 프로세스가 함께 사용할 공유 메모리 객체(shared memory object)를 생성하거나 여는 역할을 한다.</p>
<p>여기서 중요한 점은 공유 메모리를 변수처럼 바로 사용하는 것이 아니라, 먼저 파일 디스크립터(file descriptor) 형태로 받아온다는 점이다.</p>
<p>즉 shm_open()을 호출하면 공유 메모리에 접근할 수 있는 핸들(handle)을 하나 반환받는 구조라고 이해할 수 있다.</p>
<p><strong>2) 함수 형태 (Function Prototype)</strong></p>
<p>shm_open() 함수의 형태는 다음과 같다. <code>int shm_open(const char *name, int oflag, mode_t mode);</code></p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/21b00699-0a22-44ed-b933-a47473ecc2ba.png" alt="" style="display:block;margin:0 auto" />

<p><strong>3) 주요 매개변수 (Main Parameters)</strong></p>
<p>name : 공유 메모리(shared memory)의 이름</p>
<p>oflag : 생성 여부 및 읽기/쓰기 옵션 설정 값</p>
<p>mode : 접근 권한(permission) 설정 값</p>
<p>즉 FIFO처럼 이름(name)을 기준으로 여러 프로세스가 동일한 자원(resource)에 접근할 수 있는 구조이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/12e10aed-010e-4a04-864f-9fb80ab81130.png" alt="" style="display:block;margin:0 auto" />

<p><strong>4) 핵심 동작 (Core Behavior)</strong></p>
<p>shm_open() 함수로 생성된 공유 메모리는 파일 디스크립터(file descriptor) 형태로 반환된다. 하지만 이것은 실제 디스크 파일(disk file)이 아니라, 공유 메모리 영역(shared memory region)에 접근하기 위한 권한(handle)을 받은 상태라고 이해하면 된다.</p>
<p>따라서 여러 프로세스가 동일한 이름(name)으로 shm_open()을 호출하면 같은 공유 메모리 객체(shared memory object)에 접근할 수 있게 된다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f3a91bb8-3f60-437d-9a9f-edf494c875be.png" alt="" style="display:block;margin:0 auto" />

<p><strong>5) ftruncate()와 mmap()</strong></p>
<p>여기서 중요한 단계가 하나 더 존재한다. shm_open()만으로는 공유 메모리를 바로 사용할 수 있는 상태가 아니다. 먼저:</p>
<p><strong>ftruncate()의 역할 (Role of ftruncate()):</strong> ftruncate() 함수를 사용하여 공유 메모리의 크기(size)를 설정해야 한다.</p>
<p><strong>mmap()의 역할 (Role of mmap()):</strong> 그다음 mmap() 함수를 사용하여 공유 메모리를 프로세스의 주소 공간(address space)에 연결(mapping)하게 된다.</p>
<p>즉 이 과정을 통해 포인터(pointer)로 접근 가능한 상태를 만들게 된다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/4e21da5e-9272-40d3-a880-0f3553ca5ff7.png" alt="" style="display:block;margin:0 auto" />

<p><strong>6) 핵심 동작 (Core Behavior)</strong></p>
<p>이렇게 매핑(mapping)이 완료되면 이제는 read()나 write() 함수를 사용하는 것이 아니라, 포인터(pointer)를 이용하여 메모리에 직접 데이터를 쓰고 읽게 된다.</p>
<p>즉 Pipe나 FIFO처럼 중간에서 데이터를 복사하여 전달하는 구조가 아니라, 동일한 메모리 공간을 여러 프로세스가 직접 공유하는 방식으로 동작한다.</p>
<p>따라서 Shared Memory는 매우 빠른 IPC 방식이라고 할 수 있다.</p>
<hr />
<h3>Shared Memory 쓰기 예제 코드 (Shared Memory Writer Example Code)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/1aaeedac-62e8-4870-807e-236d86952909.png" alt="" style="display:block;margin:0 auto" />

<p>다음 코드는 공유 메모리(shared memory)에 데이터를 쓰는(write) 예제 코드이다. Shared Memory 통신에서는 데이터를 기록하는 쓰기 프로그램(writer)과 데이터를 읽는 읽기 프로그램(reader)이 각각 필요하다.</p>
<p>먼저 쓰기 코드를 보면 다음과 같은 흐름으로 동작한다.</p>
<p><code>#define SHM_NAME "/myshm"</code></p>
<p><code>#define SIZE 128</code></p>
<p>위 코드는: 공유 메모리 이름(shared memory name)을 "/myshm"으로 지정하고 공유 메모리 크기(size)를 128 바이트(byte)로 설정한 것이다. 그다음 main() 함수 안을 보면 다음 코드가 나온다.</p>
<p><code>int shm_fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);</code></p>
<p><code>ftruncate(shm_fd, SIZE);</code></p>
<p>여기서: shm_open()은 "/myshm"이라는 이름의 공유 메모리 객체(shared memory object)를 생성하거나 여는 역할을 한다. <code>O_CREAT | O_RDWR</code>는 공유 메모리를 생성(create)하고 읽기/쓰기(read/write)가 가능하도록 설정하는 옵션이다. <code>0666</code>은 접근 권한(permission)을 의미한다.</p>
<p>그다음: <code>ftruncate(shm_fd, SIZE);</code></p>
<p>를 통해 공유 메모리의 크기를 128 바이트로 설정하게 된다. 이후 다음 코드가 실행된다.</p>
<p><code>char *ptr = mmap(0, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);</code></p>
<p>여기서는 mmap() 함수를 사용하여 공유 메모리를 현재 프로세스의 주소 공간(address space)에 연결(mapping)하게 된다. 따라서 이후에는 ptr 포인터(pointer)를 통해 공유 메모리에 직접 접근할 수 있게 된다.</p>
<p>그다음: <code>strcpy(ptr, "Hello Shared Memory!");</code></p>
<p>를 사용하여 "Hello Shared Memory!" 문자열을 공유 메모리 공간에 직접 기록하게 된다.</p>
<p>즉 Pipe나 FIFO처럼 write()를 사용하는 것이 아니라, 메모리 공간에 직접 문자열을 복사하는 방식으로 동작하는 것이다.</p>
<p>마지막으로:</p>
<p><code>printf("공유메모리 데이터 저장 완료\n");</code></p>
<p>와 같은 출력이 나타난다면, 공유 메모리 객체 생성 메모리 크기 설정 메모리 매핑(mapping) 문자열 기록(write) 과정이 정상적으로 완료되었다는 의미가 된다.</p>
<p>즉 이 출력은 단순한 문자열 출력이 아니라, "Hello Shared Memory!" 데이터가 실제 공유 메모리 영역(shared memory region)에 정상적으로 저장되었음을 확인하는 결과라고 볼 수 있다.</p>
<hr />
<h3>Shared Memory 읽기 예제 코드 (Shared Memory Reader Example Code)</h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/9f55d255-86fe-44d7-8338-fd3cc8617fa8.png" alt="" style="display:block;margin:0 auto" />

<p>다음은 동일한 공유 메모리(shared memory)를 읽는(read) 예제 코드이다.</p>
<p>여기서 중요한 부분은 <code>shm_open()</code>이다. 이 코드는 새로운 공유 메모리를 생성하는 것이 아니라, 이미 존재하는 <code>"/myshm"</code> 이름의 공유 메모리를 열어서 접근하는 코드이다.</p>
<p>즉 앞선 쓰기(writer) 프로그램에서:</p>
<p><code>O_CREAT | O_RDWR</code></p>
<p>옵션으로 생성해두었던 공유 메모리를, 읽기(reader) 프로그램에서는 동일한 이름으로 다시 열어서 사용하는 구조이다. 예를 들면 다음 코드가 사용된다.</p>
<p><code>int shm_fd = shm_open(SHM_NAME, O_RDONLY, 0666);</code></p>
<p>여기서:</p>
<p><code>SHM_NAME</code> : 공유 메모리 이름 <code>O_RDONLY</code> : 읽기 전용(read-only) 모드 <code>0666</code> : 접근 권한(permission) 을 의미한다.</p>
<p>즉 읽기 프로그램은 기존에 생성되어 있는 공유 메모리를 읽기 모드로 열어서 접근하게 된다.</p>
<p>그다음에는 <code>mmap()</code> 함수를 사용하여 해당 공유 메모리를 자신의 주소 공간(address space)에 연결(mapping)하게 된다. 예를 들면 다음과 같은 형태이다.</p>
<p><code>char *ptr = mmap(0, SIZE, PROT_READ, MAP_SHARED, shm_fd, 0);</code></p>
<p>이렇게 되면 ptr 포인터(pointer)를 통해 공유 메모리에 직접 접근할 수 있게 된다.</p>
<p>즉 읽기 프로그램도 쓰기 프로그램이 사용했던 동일한 공유 메모리 공간(shared memory region)을 그대로 바라보는 상태가 된다고 이해할 수 있다.</p>
<p>그다음: <code>printf("읽은 데이터: %s\n", ptr);</code></p>
<p>를 통해 공유 메모리 안에 저장되어 있는 데이터를 출력하게 된다.</p>
<p>실행 결과에서 "Hello Shared Memory!"가 출력되는 이유는 앞서 쓰기 프로그램(writer program)이 동일한 공유 메모리 공간에 해당 문자열을 미리 저장해두었기 때문이다.</p>
<p>즉 읽기 프로그램이 새로운 데이터를 만든 것이 아니라, 공유 메모리에 이미 저장되어 있는 내용을 그대로 읽어오는 구조이다.</p>
<p>정리하면 이 두 프로그램은 서로 다른 독립 프로세스(independent process)이지만, 동일한 이름의 공유 메모리를 사용하기 때문에 서로 연결되어 있다.</p>
<p>따라서:</p>
<p>쓰기 프로그램(writer)은 공유 메모리에 데이터를 저장하고 읽기 프로그램(reader)은 동일한 메모리를 직접 읽게 된다.</p>
<p>즉 Pipe나 FIFO처럼 write()와 read()를 통해 데이터를 전달하는 구조가 아니라, 하나의 메모리 공간(shared memory space)을 여러 프로세스가 동시에 공유하여 접근하는 구조라고 이해하면 된다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/03bf0e96-8302-494a-a124-43cd959a6a5a.png" alt="" style="display:block;margin:0 auto" />

<h3>Shared Memory 예제 코드 실행 결과 (Shared Memory Example Execution Result)</h3>
<p>다음 실습은 Shared Memory를 이용하여 데이터를 저장하고 읽어오는 과정을 확인하기 위한 예제이다. 여기서는:</p>
<ul>
<li><p>10-5.c : 쓰기(writer) 프로그램</p>
</li>
<li><p>10-6.c : 읽기(reader) 프로그램이 사용되었다.</p>
</li>
</ul>
<hr />
<p>먼저 10-5.c 쓰기 프로그램(writer program)을 실행하면 문자열을 공유 메모리(shared memory) 공간에 직접 기록하게 된다.</p>
<p>즉 화면에 데이터를 출력하는 것이 아니라, 눈에 보이지 않는 공유 메모리 영역(shared memory region)에 데이터가 저장되는 구조이다.</p>
<p>실행 결과에서는 예를 들어:</p>
<pre><code class="language-plaintext">공유메모리 데이터 저장 완료
</code></pre>
<p>와 같은 메시지가 출력된다. 겉으로 보기에는 단순한 출력처럼 보이지만, 실제로는 그 이전 단계에서:</p>
<pre><code class="language-plaintext">Hello Shared Memory!
</code></pre>
<p>문자열이 공유 메모리 공간에 이미 저장된 상태가 된다. 즉 여러 프로세스가 접근 가능한 동일한 공유 메모리(shared memory object)에 데이터가 기록된 것이다.</p>
<hr />
<p>그다음 10-6 읽기 프로그램(reader program)은 앞서 저장된 공유 메모리 데이터를 읽어오는 과정을 확인하기 위한 예제이다. 쓰기 프로그램(writer)은 데이터를 저장만 했기 때문에, 실제 저장된 내용을 확인하려면 읽기 프로그램(reader)을 실행해야 한다.</p>
<p>읽기 프로그램을 실행하면 다음과 같은 결과가 출력된다.</p>
<pre><code class="language-plaintext">읽은 데이터: Hello Shared Memory!
</code></pre>
<p>여기서 중요한 점은 이 문자열이 현재 읽기 프로그램(reader)이 새로 만든 데이터가 아니라는 점이다. 이 값은 앞서 쓰기 프로그램(writer)이 공유 메모리(shared memory)에 이미 저장해두었던 데이터를 그대로 읽어온 결과이다.</p>
<p>즉 두 프로세스가 동일한 공유 메모리 공간(shared memory space)을 함께 바라보고 있기 때문에, 별도의 데이터 복사(copy) 과정 없이 데이터를 직접 읽을 수 있게 된다.</p>
<p>결국 이 실습은 Shared Memory가 데이터를 전달(transfer)하는 방식이 아니라, 메모리 공간(memory space) 자체를 여러 프로세스가 공유(shared)하는 IPC 방식이라는 점을 보여주는 예제라고 볼 수 있다.</p>
<hr />
]]></content:encoded></item><item><title><![CDATA[Understanding the Shell]]></title><description><![CDATA[1️⃣Understanding the Shell2️⃣Shell Scripts


Shell의 이해
Shell(쉘)은 하나의 컴퓨터 프로그램(computer program)이다. C, Python, Java처럼 익숙한 프로그래밍 언어들과는 다소 생소하게 느껴질 수 있지만, 쉘 자체도 프로그래밍 언어(programming language)의 일종이며 시스템 전]]></description><link>https://heesu.tech/understanding-the-shell</link><guid isPermaLink="true">https://heesu.tech/understanding-the-shell</guid><dc:creator><![CDATA[Heesu Noh]]></dc:creator><pubDate>Tue, 05 May 2026 13:17:18 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/739d5066-ec27-41b4-ac86-4f48637700ec.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>1️⃣Understanding the Shell<br />2️⃣Shell Scripts</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/55ed2bce-ba20-4541-beb6-9434e7deddfb.png" alt="" style="display:block;margin:0 auto" />

<h3>Shell의 이해</h3>
<p>Shell(쉘)은 하나의 컴퓨터 프로그램(computer program)이다. C, Python, Java처럼 익숙한 프로그래밍 언어들과는 다소 생소하게 느껴질 수 있지만, 쉘 자체도 프로그래밍 언어(programming language)의 일종이며 시스템 전반에서 광범위하게 사용된다.</p>
<p>쉘은 세 가지 측면에서 이해할 수 있다. 첫째로 <strong>macro processor</strong>로서, 문자(character)와 기호(symbol)를 조합해 다양한 표현이 가능하다. 둘째로 command interpreter(명령어 해석기)로서, 사용자가 입력한 명령을 해석하고 실행한다. 셋째로 앞서 언급했듯 <strong>programming language</strong>이기도 하다. C, Java, Python을 다뤄봤다면 이 언어들 사이에 공통된 구조적 특성이 있다는 것을 느꼈을 텐데, 쉘도 그러한 언어적 특성을 공유한다.</p>
<p>실용적인 측면에서 쉘은 개발자가 배포(deployment)나 시스템 관리(system administration)를 수행할 때 매우 유용하게 쓰인다. 동작 방식은 두 가지로, 키보드 입력을 통해 대화형(interactive)으로 실행하거나, 파일이나 문자열(string)을 읽어 실행할 수 있다. 이때 파일로 저장된 형태가 바로 shell script(쉘 스크립트)이며, 문자열을 읽는다는 것은 컴파일된 binary(바이너리) 코드가 아닌 사람이 읽을 수 있는 형태, 즉 human-readable한 형태를 의미한다.</p>
<p>마지막으로 중요한 개념이 하나 있다. 쉘에서는 실행 중인 명령이 완료될 때까지 기다린 후 다음 명령을 실행하는 것이 일반적이다. 그러나 명령이 완료되지 않은 상태에서도 추가 명령을 실행할 수 있다. 프로그래밍은 본질적으로 명령의 순차적 나열(sequential execution)인데, 이 개념은 그 순서를 벗어나는 것이다. 이것이 무엇을 의미하는지 생각하며 계속 쉘에 대해 알아보자</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/628ed767-5f92-4180-947a-2b86392e1763.png" alt="" style="display:block;margin:0 auto" />

<h3>Shell의 역할</h3>
<p>Shell(쉘)은 사용자(user)와 커널(kernel) 사이의 상호작용(interaction)을 중개하는 프로그램이다. 여기서 사용자란 컴퓨터 앞에서 입력과 출력을 주고받는 주체로, end user일 수도 있고 개발자(developer)일 수도 있다. 커널이 운영체제의 핵심 알맹이라면, 쉘은 그것을 감싸는 껍질이다. 즉, 사용자는 껍질인 쉘을 통해 알맹이인 커널에 접근한다고 이해하면 쉽다.</p>
<p>쉘은 이러한 중개 역할 외에도 사용자 편의 기능을 제공한다. 대표적인 예가 tab completion(탭 자동완성)이다. 예를 들어 <code>ABCDEF</code>라는 명령어가 있을 때, <code>AB</code>만 입력하고 Tab 키를 누르면 나머지가 자동으로 완성된다. 단, Linux 서버 버전을 기본 설치한 경우 이 기능이 기본으로 지원되지 않을 수 있으며, 별도로 검색해 추가 설치가 필요할 수 있다.</p>
<p>Shell이라는 용어 자체는 하나의 의미로 고정되지 않는다. 프로그램 그 자체를 가리킬 수도 있고, 역할의 관점에서 설명할 수도 있다. 어느 관점에서 보느냐에 따라 조금씩 다르게 받아들이면 된다.</p>
<p>Shell의 종류도 다양하다. Bourne shell, C shell, Korn shell, Z shell 등이 존재한다. CPU마다 내부에서 처리하는 machine language(기계어)가 다르듯, 쉘도 여러 종류가 있다. 이는 어떤 환경이나 도구든 시대와 요구에 따라 부족한 점이 생기고, 그것을 보완하기 위해 새로운 종류가 계속 등장하기 때문이다.</p>
<p>현재 사용 중인 쉘을 확인하려면 다음 명령어를 입력하면 된다.</p>
<pre><code class="language-shell">echo $SHELL
</code></pre>
<p><code>echo</code>는 뒤에 오는 내용을 화면에 출력하는 명령어이고, <code>$</code>는 뒤에 오는 이름이 변수(variable)임을 나타내며 그 변수에 저장된 값을 출력하라는 의미다. <code>SHELL</code>이 대문자인 이유는 environment variable(환경변수)이기 때문이며, 현재 로그인한 사용자가 사용 중인 shell의 이름을 저장하고 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/319e2cb8-479d-42aa-9d1d-72f9e84f88b5.png" alt="" style="display:block;margin:0 auto" />

<h3>Shell의 시작</h3>
<p>Shell은 로그인(login)과 함께 시작된다. 이를 login shell(로그인 쉘)이라고 한다. 현재 시스템에 어떤 shell이 설정되어 있는지 확인하려면 다음 명령어를 사용한다.</p>
<pre><code class="language-shell">cat /etc/passwd
</code></pre>
<p>출력 결과를 보면 각 계정마다 마지막 필드에 shell 경로가 표시된다. <code>/bin/bash</code>라면 해당 사용자가 bash shell을 사용 중이라는 뜻이고, <code>nologin</code>이라면 login shell이 없다는 뜻이다. 사용자가 어떤 shell을 쓰는지 이 방식으로도 확인할 수 있다.</p>
<p>그렇다면 <code>nologin</code>은 왜 존재할까? Linux/Unix에서는 사람이 직접 사용하는 계정 외에도 서비스 운영을 위한 계정이 존재한다. 슬라이드의 예시에서 <code>sshd</code>는 SSH daemon(데몬)을 위한 계정인데, daemon은 백그라운드(background)에서 자동으로 동작하는 프로그램이므로 사람이 직접 로그인해 관여할 필요가 없다. 이런 계정에 login shell을 부여하지 않는 이유는 보안(security) 때문이다. 사용하지 않는 login shell이 존재하면 오히려 해킹(hacking)의 위험 요소가 될 수 있다. 잘 돌아가는 서비스에 굳이 불필요한 접근 경로를 열어둘 필요가 없다는 개념이다.</p>
<p>shell의 변경도 가능하다. 사용자 계정을 생성할 때 shell을 지정할 수 있으며, 이후에는 <code>chsh</code> 명령어로 현재 사용 중인 shell을 다른 shell로 변경할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/bf44ac67-101c-4b72-b6b6-db7382fc1eac.png" alt="" style="display:block;margin:0 auto" />

<h3>Shell Prompt의 종류</h3>
<p>Shell prompt(쉘 프롬프트)는 단순한 기호가 아니라 현재 어떤 권한(permission)으로 작업 중인지를 나타내는 중요한 지표다.</p>
<p>Bash shell 기준으로 일반 사용자(regular user)는 <code>\(</code>, Z shell 기준으로는 <code>%</code>가 표시된다. 반면 root 사용자, 즉 superuser는 <code>#</code>으로 표시된다. 슬라이드의 예시처럼 <code>sudo su</code> 명령어를 입력하면 일반 사용자 상태인 <code>\)</code>에서 root 상태인 <code>#</code>으로 전환되는 것을 확인할 수 있다.</p>
<p>이 구분이 중요한 이유는 권한 범위가 완전히 다르기 때문이다. <code>#</code> 상태, 즉 root 권한에서는 이미 superuser이므로 <code>sudo</code>를 별도로 사용할 필요가 없다. 반면 <code>$</code>나 <code>%</code> 상태에서 높은 권한이 필요한 작업을 수행하려면 <code>sudo</code>가 필요한 경우가 생긴다.</p>
<p>특히 <code>#</code> 상태에서는 각별한 주의가 필요하다. Root는 파일의 소유(ownership)나 권한(permission)에 관계없이 시스템의 모든 것을 처리할 수 있는, 말 그대로 무적 상태다. 예를 들어 <code>rm</code> 명령어를 무심코 실행하면 의도하지 않은 파일이 삭제될 수 있다. 자신이 일반 사용자라고 착각한 채 root 상태에서 작업하다 시스템에 치명적인 문제를 일으키는 경우가 실제로 발생한다. 따라서 프롬프트 기호를 항상 확인하는 습관이 필요하다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/4ef08234-c9e6-4c30-821d-73ab9fe160b3.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/67fcdfcd-e243-4e78-8136-d3bea42a2df6.png" alt="" style="display:block;margin:0 auto" />

<h3>Shell의 기본 동작</h3>
<p>Shell은 기본적으로 입력되는 내용을 순서대로 처리한다. 단, 두 가지 예외가 있다. 첫 번째 예외는 주석문(comment)이다. Shell prompt에서 <code>#</code>는 root 사용자를 의미한다고 했지만, 명령을 입력하는 쪽에서 <code>#</code>가 등장하면 의미가 달라진다. 이때의 <code>#</code>는 주석문 기호로, 컴퓨터는 이 기호 이후의 내용을 무시한다. 사람이 코드에 설명을 덧붙이기 위해 사용하는 용도다. 기본 동작 과정은 다음 여섯 단계로 이루어진다.</p>
<p>1단계 - 입력(input)은 세 가지 경로로 이루어진다. 사용자가 키보드로 입력하는 terminal(터미널), 파일(file), 그리고 argument(인수)로 전달되는 문자열(string)이다. Terminal이라는 명칭은 초창기 컴퓨팅 환경에서 비롯된 역사적 용어이며, 지금은 키보드로 명령을 입력하는 창이라고 이해하면 된다.</p>
<p>2단계 - 입력의 구성으로, 입력은 word(단어)와 operator(연산자)로 이루어지며 특수문자(special character)에 의해 분리된다. 단어는 공백으로 구분되는 문자들의 나열이고, 연산자는 <code>+, =, *, /</code>와 같이 연산을 수행하는 기호를 말한다. Shell에서 이것이 어떻게 활용되는지는 차차 확인해본다.</p>
<p>3단계 - 명령의 분류로, 명령은 simple command(간단한 명령)와 compound command(복합 명령)로 나뉜다. ls처럼 단독으로 사용되는 것이 simple command이고, 여러 명령어가 결합되거나 괄호 등이 추가되는 형태가 compound command다.</p>
<p>4단계 - 확장(expansion) 처리로, 단순한 입력을 넘어 더 진화된 방식으로 처리하는 단계다. 구체적인 내용은 이후에 다룬다.</p>
<p>5단계 - redirection(방향 전환) 처리로, <code>|, &gt;, &lt;</code>와 같은 redirection operator(방향 전환 연산자)와 그 피연산자(operand)를 처리하고 제거하는 단계다.</p>
<p>6단계 - 명령 수행(execution)으로, 앞선 단계를 모두 거친 후 최종적으로 명령이 실행된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/6b718a2c-19d4-4432-b27f-aafd370311fb.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a788412c-3144-4451-a302-9000e6b4d8eb.png" alt="" style="display:block;margin:0 auto" />

<h3>인용 처리 (Quoting)</h3>
<p>Quoting(인용 처리)이란 shell에서 미리 지정된 특별한 의미를 무시하고, 문자를 있는 그대로 처리하도록 하는 것이다. Shell에서 인용 처리는 세 가지 방식으로 이루어진다.</p>
<p>첫 번째는 escape character(이스케이프 문자) (backslash)다. 백슬래시 바로 뒤에 오는 문자를 특수한 의미 없이 그대로 처리한다. 다른 방식들과 혼용될 수 있으므로 구분해서 이해해야 한다.</p>
<p>두 번째는 single quote '(홑따옴표)다. 홑따옴표로 둘러싸인 개별 문자들을 모두 그대로 처리한다.</p>
<p>세 번째는 double quote "(겹따옴표)다. 겹따옴표로 둘러싸인 모든 문자를 그대로 처리한다. 여기서 주의할 점이 있다. 문서나 보고서에서 흔히 보이는 " 또는 ' 형태의 열림·닫힘 따옴표(curly quote)는 shell에서 인식하지 못한다. Shell에서 유효한 따옴표는 키보드 Enter 키 옆에 있는 '와 ", 즉 straight quote뿐이다. 인터넷이나 문서에서 명령어를 복사해 붙여넣을 때 이 curly quote가 섞여 들어와 오류가 발생하는 경우가 실제로 종종 있으므로 주의해야 한다. Single quote와 double quote의 동작 방식은 원칙적으로 다르며, 요즘은 엄격하게 구분되지 않는 경우도 있지만 기본 원칙은 알아두는 것이 좋다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/c98c38a8-bdd3-42d0-809f-7ef524f50568.png" alt="" style="display:block;margin:0 auto" />

<h3>예약어 (Reserved Words)</h3>
<p>Reserved word(예약어)란 shell 내부적으로 이미 의미가 지정되어 있어, 변수 이름 등 다른 용도로 사용할 수 없는 단어들이다.</p>
<p>Shell의 예약어 목록은 다음과 같다.</p>
<p><code>if</code>, <code>then</code>, <code>elif</code>, <code>else</code>, <code>fi</code>, <code>time</code>, <code>for</code>, <code>in</code>, <code>until</code>, <code>while</code>, <code>do</code>, <code>done</code>, <code>case</code>, <code>esac</code>, <code>coproc</code>, <code>select</code>, <code>function</code>, <code>{ }</code>, <code>[[ ]]</code>, <code>!</code></p>
<p>여기서 눈여겨볼 것이 있다. <code>if</code> 문의 끝을 나타내는 것이 <code>fi</code>인데, 이는 <code>if</code>를 거꾸로 쓴 것이다. 마찬가지로 <code>case</code> 문의 끝을 나타내는 것은 <code>esac</code>으로, <code>case</code>를 거꾸로 쓴 것이다. C언어에서는 중괄호 <code>{ }</code>로 블록의 시작과 끝을 구분하지만, shell은 이처럼 키워드를 거꾸로 쓰는 방식으로 구분한다.</p>
<p><code>for</code>, <code>while</code>, <code>until</code>, <code>select</code> 등은 <code>do</code>로 시작해서 <code>done</code>으로 끝나는 방식으로, 뒤집기 방식이 아니라 별도의 키워드를 사용한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/40b5072d-7fee-4655-90a6-4b56b801de69.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/1e9fbe6d-cc1f-479c-97ad-3ba5ffc1fe45.png" alt="" style="display:block;margin:0 auto" />

<h3>간단한 명령어와 Pipeline</h3>
<h4>간단한 명령어 (Simple Command)</h4>
<p>Simple command(간단한 명령어)는 공백으로 구분되는 단어들이 나열되는 방식으로 사용된다. <code>ls -l</code>처럼 첫 번째 단어는 실행될 명령어로 인식되고, 나머지 단어들은 해당 명령어의 argument(인수)로 사용된다.</p>
<h4>Pipeline</h4>
<p>Pipeline(파이프라인)은 <code>|</code> 기호를 사용한다. 키보드에서 이 기호를 찾을 때 대문자 <code>I</code>, 숫자 <code>1</code>, 소문자 <code>l</code>과 혼동하지 않도록 주의해야 하며, 반드시 실습을 통해 직접 확인하는 것이 좋다.</p>
<p>Pipeline은 하나 또는 그 이상의 명령어가 <code>|</code> 또는 <code>|&amp;</code>로 구분되어 나열되는 방식으로 사용된다. 핵심 개념은 <strong>앞 명령어의 실행 결과가 뒤 명령어의 입력으로 전달</strong>된다는 것이다.</p>
<p><code>|</code>와 <code>|&amp;</code>의 차이는 다음과 같다.</p>
<ul>
<li><p><code>|</code> : 앞 명령어의 standard output(표준 출력)만 다음 명령어의 standard input(표준 입력)으로 전달된다. 즉, 실행 도중 발생하는 오류 메시지는 전달되지 않고 화면에만 출력된다.</p>
</li>
<li><p><code>|&amp;</code> : 앞 명령어의 standard output(표준 출력)과 standard error(표준 오류)를 모두 다음 명령어의 standard input으로 전달한다. <code>2&gt;&amp;1|</code>과 동일한 의미다.</p>
</li>
</ul>
<p><code>|&amp;</code>가 필요한 이유는, <code>|</code>만 사용할 경우 중간에 오류가 발생해도 다음 명령어는 이를 전혀 감지하지 못하기 때문이다. 오류 메시지까지 함께 처리해야 하는 상황에서는 <code>|&amp;</code>를 사용한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/c88cee14-5d72-438a-bca7-c99321572a40.png" alt="" style="display:block;margin:0 auto" />

<h3>명령어 목록 (Command List)</h3>
<p>명령어 목록이란 <code>;</code>, <code>&amp;&amp;</code>, <code>||</code>로 연결되는 명령어들의 묶음이며, <code>;</code>, <code>&amp;</code>, 또는 newline 중 하나로 끝나기도 한다. 즉, <code>A; B; C</code>처럼 한 줄에 여러 명령어를 엔터키를 누르기 전에 나열하는 것이 가능하다.</p>
<p>각 구분자의 동작 방식은 다음과 같다.</p>
<p><code>&amp;</code> (background execution) 은 명령어 실행이 완료되기를 기다리지 않고 다음 순서를 진행한다. 앞서 shell의 기본 동작에서 언급했던 개념이 바로 이것이다. 예를 들어 <code>sleep 100</code>을 실행하면 100초 동안 프롬프트가 돌아오지 않아 사용자는 아무것도 할 수 없다. 그러나 <code>sleep 100 &amp;</code>처럼 <code>&amp;</code>를 붙이면 100초가 지나지 않아도 즉시 프롬프트가 돌아와 다음 작업을 계속할 수 있다. 이것이 background execution(백그라운드 실행)이다.</p>
<p><code>;</code> 는 명령어 실행이 완료될 때까지 기다렸다가 다음 순서를 진행한다. <code>a; b; c</code>처럼 나열하면 <code>a</code>가 끝난 후 <code>b</code>, <code>b</code>가 끝난 후 <code>c</code> 순서로 실행된다.</p>
<p><code>&amp;&amp;</code><strong>와</strong> <code>||</code> 도 마찬가지로 <code>;</code> 대신 사용할 수 있으며, 각각 다른 조건으로 다음 명령어의 실행 여부를 결정한다. 이 부분은 직접 실습을 통해 확인하는 것이 가장 좋다.</p>
<p>한 가지 주의할 점은, 인터넷이나 AI 검색 결과로 나오는 명령어 예시가 실제로 동작하지 않는 경우가 있다는 것이다. 이를 직접 실행해보고 구분하는 과정 자체가 실력을 검증하고 향상시키는 과정이 된다. <code>;</code> 사용 시 앞 명령어가 정상적으로 끝나지 않았을 때 어떻게 동작하는지도 직접 실습으로 확인해보자.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/6f68205a-42e6-4591-af62-9d1995988b90.png" alt="" style="display:block;margin:0 auto" />

<h3>명령어 목록 - <code>&amp;&amp;</code>와 <code>||</code></h3>
<p><code>&amp;</code>가 두 개가 되면 <code>&amp;&amp;</code>, vertical bar가 두 개가 되면 <code>||</code>가 된다. 각각의 동작 방식은 다음과 같다.</p>
<p><code>명령어1 &amp;&amp; 명령어2</code> 는 논리적으로 AND 연산에 해당한다. 명령어1이 성공, 즉 exit code(종료 코드) 0을 반환했을 때만 명령어2가 실행된다. 예를 들어 <code>ls korea</code>를 실행했을 때 <code>korea</code>라는 파일이 존재하면 정상적으로 실행되어 0을 반환하고 다음 명령어로 넘어간다. 반대로 오류가 발생해 0이 아닌 값을 반환하면 뒤의 명령어는 아예 실행되지 않는다.</p>
<p><code>명령어1 || 명령어2</code> 는 논리적으로 OR 연산에 해당한다. 명령어1이 실패, 즉 0이 아닌 값을 반환했을 때만 명령어2가 실행된다. 반대로 명령어1이 성공하면 명령어2는 실행되지 않는다.</p>
<p>왜 이런 복잡한 구분이 필요할까? 바로 조건적 실행(conditional execution)이 필요하기 때문이다. 예를 들어 directory(디렉토리)를 생성한 후 그 안에 파일을 복사하는 작업을 한다고 가정하면, 목적지 directory가 정상적으로 만들어졌을 때만 파일 복사를 시도하는 것이 합리적이다. directory가 존재하지 않는 곳에 파일을 복사하려 하면 어차피 실패하기 때문이다. 이처럼 앞의 명령어가 성공했을 때만 뒤의 명령어를 실행해야 하는 상황에서 <code>&amp;&amp;</code>가 유용하게 쓰인다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a388edb6-ca35-4329-8899-753e2ca0f1a5.png" alt="" style="display:block;margin:0 auto" />

<h3>복합 명령어 (Compound Command)</h3>
<p>복합 명령어는 프로그래밍 언어의 구조적 형태로 사용된다. 앞서 살펴본 예약어(reserved word) 또는 제어 연산자(control operator)로 시작하고, 상응하는 예약어 또는 연산자로 종료된다. <code>if</code>로 시작해 <code>fi</code>로 끝나는 것이 대표적인 예다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/0f4d8cc7-3366-42fd-8078-716f888ae789.png" alt="" style="display:block;margin:0 auto" />

<h3>복합명령어(Compound Command) ; <code>until</code></h3>
<h4><code>until</code>: <code>until</code>의 사용 방법은 다음과 같다.</h4>
<pre><code class="language-bash">until test-commands; do consequent-commands; done
</code></pre>
<p>여기서 <code>;</code>의 위치에 주의해야 한다. <code>;</code>가 없으면 문법 오류(syntax error)가 발생한다.</p>
<p>동작 방식은 다음과 같다. <code>test-commands</code>의 종료 상태값(exit code)이 <strong>0이 아닐 때</strong> <code>consequent-commands</code>를 반복 실행한다. 이는 <code>while</code>과 반대 조건이다. 반환값(return value)의 경우, 실행된 명령이 없으면 0을 반환하고, 그렇지 않으면 <code>consequent-commands</code>의 마지막 명령의 실행 결과를 반환한다.</p>
<p>반환값은 단순한 작업에서는 크게 신경 쓰지 않아도 된다. 그러나 작업이 복잡해질수록 앞선 작업이 제대로 끝났는지 확인해야 하는 상황이 생긴다. <code>&amp;&amp;</code>나 <code>||</code>처럼 앞 명령어의 성공 여부에 따라 다음 명령어를 실행할 때도 이 반환값이 기준이 된다. 따라서 어떤 조건에서 반복이 이루어지는지, 그리고 반환값이 언제 참(0)이 되고 거짓(0이 아닌 값)이 되는지의 개념은 기억해두어야 한다. 구체적인 사용법은 실제로 사용할 때 manual(매뉴얼)을 참고하면 된다.</p>
<p>📌좀 더 쉬운 설명</p>
<pre><code class="language-bash">count=1

until [ $count -gt 5 ]; do
    echo "현재 숫자: $count"
    count=$((count + 1))
done
</code></pre>
<p>실행 결과는 이렇다.</p>
<pre><code class="language-plaintext">현재 숫자: 1
현재 숫자: 2
현재 숫자: 3
현재 숫자: 4
현재 숫자: 5
</code></pre>
<p>동작 방식을 풀어서 설명하면 이렇다.</p>
<ul>
<li><p><code>count</code>가 1부터 시작</p>
</li>
<li><p><code>[ $count -gt 5 ]</code> : count가 5보다 큰가? 를 매번 확인</p>
</li>
<li><p>처음에는 1이니까 5보다 크지 않다 → <strong>실패</strong> → 반복 실행</p>
</li>
<li><p>count가 6이 되는 순간 5보다 크다 → <strong>성공</strong> → 반복 종료</p>
</li>
</ul>
<p>즉 조건이 <strong>실패하는 동안</strong> 계속 <code>echo</code>와 <code>count</code> 증가를 반복하다가, 조건이 <strong>성공하는 순간</strong> 멈추는 것이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/57c215cf-be53-4f2b-80e0-fd7cd239aea4.png" alt="" style="display:block;margin:0 auto" />

<h3>복합명령어(Compound Command) ; <code>while</code></h3>
<p><code>until</code>과 구조는 동일하지만 조건이 반대다.</p>
<ul>
<li><p><code>until</code> : test-commands의 exit code가 <strong>0이 아닐 때</strong> 반복</p>
</li>
<li><p><code>while</code> : test-commands의 exit code가 <strong>0일 때</strong> 반복</p>
</li>
</ul>
<p>즉 조건이 <strong>성공하는 동안</strong> 계속 반복하고, 실패하면 멈춘다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/0984165f-7951-4a26-af0b-e679f21a7f93.png" alt="" style="display:block;margin:0 auto" />

<h3>복합명령어(Compound Command) ; <code>for</code></h3>
<p><code>for</code>는 두 가지 형태로 사용된다.</p>
<p><strong>첫 번째 형태</strong></p>
<pre><code class="language-bash">for name [[in words ...];] do commands; done
</code></pre>
<p>여기서 <code>[ ]</code>는 shell 문법에서 선택사항(optional)을 의미한다. <code>man</code> 명령어로 매뉴얼을 볼 때도 <code>[ ]</code> 안의 내용은 필요할 때만 쓰면 된다는 뜻이다. 따라서 <code>in words</code> 부분은 생략 가능하고, 그 뒤의 <code>;</code>도 <code>in words</code>가 있을 때만 필요하다.</p>
<p>동작 방식은 <code>words</code>를 확장해 목록을 만들고, 목록의 각 word에 대해 <code>commands</code>를 1회씩 실행한다. 실행된 것이 없으면 0을 반환하고, 있으면 <code>commands</code>의 마지막 명령의 실행 결과를 반환한다.</p>
<p>예를 들면 이렇다.</p>
<pre><code class="language-bash">for fruit in apple banana orange; do
    echo $fruit
done
</code></pre>
<pre><code class="language-plaintext">apple
banana
orange
</code></pre>
<p><code>apple</code>, <code>banana</code>, <code>orange</code> 각각에 대해 <code>echo</code>를 1회씩 실행한다. Python의 <code>for</code> 문과 동일한 방식으로 동작한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f7342f66-aa57-4924-8654-f8d673d75155.png" alt="" style="display:block;margin:0 auto" />

<h3>복합명령어(Compound Command) ; <code>for</code></h3>
<p>C언어의 <code>for</code>문과 동일한 개념이다.</p>
<pre><code class="language-c">// C언어
for (i=0; i&lt;10; i++)

// Shell
for ((i=0; i&lt;10; i++))
</code></pre>
<p>동작 순서는 다음과 같다.</p>
<ol>
<li><p><code>expr1</code> 초기값 설정 (예: <code>i=0</code>)</p>
</li>
<li><p><code>expr2</code> 조건 확인 → 0이 아니면(참) <code>commands</code> 실행</p>
</li>
<li><p><code>expr3</code> 처리 (예: <code>i++</code>)</p>
</li>
<li><p>다시 2번으로 돌아가 반복</p>
</li>
<li><p><code>expr2</code>가 0(거짓)이 되면 반복 종료</p>
</li>
</ol>
<p>반환값은 <code>commands</code>의 마지막 명령 실행 결과를 반환한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a7bd2886-ab5d-4a4d-9e69-4fa98ffd5e94.png" alt="" style="display:block;margin:0 auto" />

<h3>복합명령어(Compound Command) ; <code>if</code></h3>
<p>필수 요소와 선택 요소를 구분하면 이렇다.</p>
<ul>
<li><p><strong>필수</strong> : <code>if</code>, <code>then</code>, <code>fi</code>, 그리고 명령 끝의 <code>;</code></p>
</li>
<li><p><strong>선택</strong> : <code>elif</code>, <code>else</code> (<code>[ ]</code>로 표시된 부분)</p>
</li>
</ul>
<p><code>then</code>이 없으면 문법 오류가 발생한다. 동작 방식은 <code>test-commands</code>를 실행해 exit code가 0(성공)이면 <code>consequent-commands</code>를 실행하고, 0이 아니면 <code>elif</code> 또는 <code>else</code>로 넘어간다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/1dfeda49-ddc5-4e69-802d-1b71a2c81384.png" alt="" style="display:block;margin:0 auto" />

<h3>복합명령어(Compound Command) ; <code>case</code></h3>
<p>case를 거꾸로 쓴 것이 esac으로 종료를 나타낸다. 문법에서 눈여겨볼 부분이 두 가지 있다.</p>
<p>첫 번째는 ((여는 괄호)다. [(]로 표시되어 있으므로 선택사항이다. 보통 case문에서 조건 패턴을 쓸 때 ) 닫는 괄호만 쓰는 것이 일반적이지만, 괄호는 열고 닫아야 짝이 맞는다고 생각한다면 (를 써도 된다. 둘 다 허용된다.</p>
<p>두 번째는 ;;(세미콜론 두 개)다. 일반적인 ;와 달리 case문에서는 각 패턴의 command-list 끝에 ;;를 사용한다. 이것이 해당 패턴의 실행이 끝났음을 나타낸다. 그리고 [| pattern]처럼 |를 사용해 패턴을 여러 개 나열할 수 있다. 즉 하나의 조건에 여러 패턴을 지정하는 것이 가능하다. 동작 방식은 word가 pattern과 일치할 때만 해당 command-list를 실행한다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/55909053-33f6-498a-a617-1de61638d2ea.png" alt="" style="display:block;margin:0 auto" />

<h3>복합 명령어(Compound Command) - <code>case</code> 종료자의 종류</h3>
<p><code>case</code>문에서 각 패턴 블록의 끝에 오는 종료자(terminator)는 세 가지가 있으며, 각각 동작 방식이 다르다.</p>
<p><code>;;</code> 는 처음 일치하는 pattern의 command-list만 실행하고 완전히 종료한다. 다른 언어의 <code>case</code>문 또는 <code>switch</code>문에서 <code>break</code>를 사용하는 것과 동일한 개념이다.</p>
<p><code>;&amp;</code> 는 처음 일치하는 pattern의 command-list를 실행하고, 다음 pattern의 일치 여부에 상관없이 그 다음 command-list도 강제로 실행한다. 다른 언어의 <code>switch</code>문에서 <code>break</code> 없이 아래로 흘러내려가는 fall-through와 동일한 개념이다.</p>
<p><code>;;&amp;</code> 는 처음 일치하는 pattern의 command-list를 실행한 후, 다음 pattern과 비교를 계속 진행한다. 일치하면 실행하고, 일치하지 않으면 건너뛰며, 끝까지 비교를 계속한다. 다른 언어의 <code>continue</code>와 유사한 개념이다.</p>
<p>정리하면 이렇다.</p>
<table>
<thead>
<tr>
<th>종료자</th>
<th>동작</th>
</tr>
</thead>
<tbody><tr>
<td><code>;;</code></td>
<td>일치하는 것 하나만 실행하고 종료</td>
</tr>
<tr>
<td><code>;&amp;</code></td>
<td>일치하는 것 실행 후 다음 것 무조건 실행</td>
</tr>
<tr>
<td><code>;;&amp;</code></td>
<td>일치하는 것 실행 후 나머지 pattern 계속 비교</td>
</tr>
</tbody></table>
<hr />
<h3>복합 명령어(Compound Command) - <code>select</code></h3>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/faef64b3-95f4-4ae5-99ed-d4178eb944ea.png" alt="" style="display:block;margin:0 auto" />

<pre><code class="language-bash">select name [in words ...]; do commands; done
</code></pre>
<p><code>for</code>문과 구조가 거의 동일하다. <code>words</code>를 확장해 목록을 만들고, 각 word를 순서대로 <code>name</code>에 지정하며 <code>commands</code>를 실행한다. 앞서 예시로 든 <code>apple</code>, <code>banana</code>, <code>orange</code>라면 한 번은 <code>apple</code>, 한 번은 <code>banana</code> 값을 가지는 방식이다. <code>for</code>와의 차이는 <code>select</code>는 자동으로 번호가 매겨진 메뉴를 화면에 출력하고 사용자의 선택을 기다린다는 점이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/0dd0ac1f-80b4-4a79-8691-187427099d84.png" alt="" style="display:block;margin:0 auto" />

<h3>복합 명령어(Compound Command) - <code>(( ... ))</code>, <code>[[ ... ]]</code></h3>
<h4><code>(( expression ))</code></h4>
<p>수학적인 계산을 실행한다. <code>+</code>, <code>-</code>, <code>*</code>, <code>/</code> 같은 산술 연산(arithmetic operation)을 처리한다.</p>
<pre><code class="language-bash">(( 1 + 2 ))
</code></pre>
<h4><code>[[ expression ]]</code></h4>
<p>조건문 평가를 실행한다. 예를 들어 a가 b보다 큰지, 값이 같은지 등의 조건을 판단한다.</p>
<p>여기서 헷갈리기 쉬운 부분이 있다. Shell에서 참/거짓의 반환값은 일반적인 프로그래밍 언어와 <strong>반대</strong>다.</p>
<table>
<thead>
<tr>
<th></th>
<th>Shell</th>
<th>C, Python 등</th>
</tr>
</thead>
<tbody><tr>
<td>참(true)</td>
<td><strong>0</strong></td>
<td>1</td>
</tr>
<tr>
<td>거짓(false)</td>
<td><strong>0이 아닌 값</strong></td>
<td>0</td>
</tr>
</tbody></table>
<p>Shell에서 0이 성공, 즉 참을 의미하는 이유는 성공은 하나지만 실패의 종류는 여러 가지이기 때문이다. 실패했을 때 그 원인을 숫자로 구분하기 위해 이런 방식을 채택했다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/73597cfa-deef-4fd0-b61f-4441a6c7a289.png" alt="" style="display:block;margin:0 auto" />

<h3>복합 명령어 - <code>(...)</code>, <code>!</code>, <code>&amp;&amp;</code>, <code>||</code></h3>
<p>앞서 명령어 나열에서 설명했던 <code>&amp;&amp;</code>와 <code>||</code>의 동작 방식이 여기서 논리 연산(logical operation)의 관점으로 연결된다. <code>&amp;&amp;</code>는 앞이 참일 때만 뒤를 실행해야 최종적으로 참인지 알 수 있고, <code>||</code>는 앞이 실패했을 때 뒤를 실행해야 하나라도 참인지 확인할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/8f58e1af-a6f6-4437-bdda-8e034662f83d.png" alt="" style="display:block;margin:0 auto" />

<h3>명령어 그룹 만들기</h3>
<p>명령어를 하나의 그룹으로 묶는 방법은 두 가지이며, 가장 큰 차이는 <strong>어디서 실행되느냐</strong>다.</p>
<p><code>(list)</code> 는 subshell(서브 쉘) 환경에서 명령을 실행한다. Subshell에서 사용된 변수는 subshell이 종료되면 사라진다.</p>
<p><code>{ list; }</code> 는 current shell(현재 쉘) 환경에서 명령을 실행한다.</p>
<p>이 차이를 변수의 관점으로 이해하면 쉽다. 변수 <code>A=1</code>을 저장했을 때를 생각해보자.</p>
<ul>
<li><p><strong>Subshell</strong> <code>(...)</code> : 자식이 자신의 세계에서 <code>A=2</code>로 바꿔도 부모 세계의 <code>A</code>는 여전히 1이다. Subshell 안에서 변경된 내용은 밖으로 나오지 않는다.</p>
</li>
<li><p><strong>Current shell</strong> <code>{ }</code> : 같은 세계에서 실행되므로 변경된 내용이 그대로 유지된다.</p>
</li>
</ul>
<p>이는 이후에 다룰 fork(포크) 개념과 연결된다. Fork란 자식 process를 계속 생성하는 것인데, 부모와 자식은 각자의 세계를 가지므로 자식 세계에서의 변경이 부모 세계에 영향을 주지 않는다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a07f5e97-c9f5-44dc-8a7a-cf3d2e6cc211.png" alt="" style="display:block;margin:0 auto" />

<h3>함수 (Function)</h3>
<p>함수는 프로그래밍에서 코드의 재사용(reuse)을 위한 개념이다. 반복적으로 사용되는 코드를 하나의 처리 단위로 묶고 이름을 붙여, 필요할 때마다 그 이름만 호출해서 사용할 수 있도록 하는 것이 함수의 역할이다.</p>
<p>Shell에서 함수를 만드는 방법은 두 가지다.</p>
<pre><code class="language-bash"># 첫 번째 방법
fname () compound-command [redirections]

# 두 번째 방법
function fname [()] compound-command [redirections]
</code></pre>
<p>두 방법의 차이는 <code>function</code> 키워드의 유무이다. <code>function</code>을 앞에 명시하면 이것이 함수임을 더 명확하게 나타낼 수 있다. 기능상의 차이는 없으므로 어느 방식을 사용해도 무방하다.</p>
<p><code>[ ]</code>로 표시된 <code>redirections</code>는 선택사항이며, 함수 실행 결과를 어디로 보낼지 방향을 지정할 때 사용한다. 앞서 배운 redirection 개념이 함수에도 그대로 적용된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f6d439bb-25b0-417f-a888-f550c34cde4d.png" alt="" style="display:block;margin:0 auto" />

<h3>Shell Parameters (쉘 매개변수); 이름</h3>
<pre><code class="language-bash">name=[value]
</code></pre>
<p>변수를 선언하고 값을 저장하는 가장 기본적인 방법이다. <code>[value]</code>가 선택사항인 이유는 값 없이 변수 이름만 선언하는 것도 가능하기 때문이다. 즉 초기값 없이 선언만 해두고 나중에 값을 넣을 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/ccd442f8-35f9-4f43-9fa6-860f8942d2d5.png" alt="" style="display:block;margin:0 auto" />

<h3>Shell Parameters (쉘 매개변수); 위치 (Positional Parameter)</h3>
<pre><code class="language-bash">\({N}  또는  \)N
</code></pre>
<p>위치(position)를 숫자로 가리키는 parameter다. 수직선상에 0, 1, 2처럼 위치가 있다고 생각하면 이해하기 쉽다.</p>
<p>예를 들어 <code>ls /home</code>을 입력했을 때를 보면 이렇다.</p>
<table>
<thead>
<tr>
<th>위치</th>
<th>값</th>
</tr>
</thead>
<tbody><tr>
<td><code>$0</code></td>
<td><code>ls</code> (명령어 자체)</td>
</tr>
<tr>
<td><code>$1</code></td>
<td><code>/home</code> (첫 번째 argument)</td>
</tr>
<tr>
<td><code>$2</code></td>
<td>두 번째 argument</td>
</tr>
</tbody></table>
<p>즉 <code>\(0</code>은 실행된 명령어 이름이고, <code>\)1</code>부터가 사용자가 넘긴 argument다. 이것이 positional parameter(위치 매개변수)가 0이 아닌 위치를 가리킨다고 표현한 이유이며, shell script를 작성할 때 매우 자주 활용된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/34132715-c205-4cb2-8cd2-13274f1565a0.png" alt="" style="display:block;margin:0 auto" />

<h3>Shell Parameters - 특수 문자 <code>*</code>와 <code>@</code></h3>
<p><code>*</code>와 <code>@</code> 모두 1부터 시작하는 positional parameter(위치 매개변수)로 확장된다는 점은 동일하다. 그러나 따옴표로 감쌌을 때 동작 방식이 달라진다.</p>
<h4><code>*</code></h4>
<table>
<thead>
<tr>
<th>형태</th>
<th>동작</th>
</tr>
</thead>
<tbody><tr>
<td><code>$*</code></td>
<td><code>\(1 \)2 $3 ...</code> 으로 확장</td>
</tr>
<tr>
<td><code>"$*"</code></td>
<td><code>"\(1c\)2c$3..."</code> 하나의 문자열로 합쳐짐</td>
</tr>
</tbody></table>
<p>여기서 <code>c</code>는 IFS(Internal Field Separator)에 해당하는 구분 문자다. IFS는 필드를 구분하는 문자로, 기본값은 공백이지만 콤마(<code>,</code>) 등 다른 문자로 지정할 수도 있다. 엑셀에서 CSV(Comma-Separated Values) 파일로 저장할 때 콤마가 구분 문자가 되는 것과 같은 개념이다. <code>"$*"</code>로 감싸면 모든 값이 IFS 구분 문자로 이어진 <strong>하나의 문자열</strong>이 된다.</p>
<h4><code>@</code></h4>
<table>
<thead>
<tr>
<th>형태</th>
<th>동작</th>
</tr>
</thead>
<tbody><tr>
<td><code>$@</code></td>
<td><code>\(1 \)2 $3 ...</code> 으로 확장</td>
</tr>
<tr>
<td><code>"$@"</code></td>
<td><code>"\(1" "\)2" "$3" ...</code> 각각 독립된 문자열로 유지</td>
</tr>
</tbody></table>
<p><code>@</code>에는 <code>c</code>(IFS 구분 문자)가 없다. <code>"$@"</code>로 감싸면 각 값이 따옴표로 개별 보호되어 <strong>독립된 문자열</strong>로 유지된다.</p>
<p>결국 가장 큰 차이는 따옴표로 감쌌을 때다. <code>"\(*"</code>는 전체를 하나로 합치고, <code>"\)@"</code>는 각각을 개별적으로 유지한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/cb498211-1ecf-4118-b250-e13208821766.png" alt="" style="display:block;margin:0 auto" />

<h3>Shell Parameters - 특수 문자 <code>#</code>, <code>?</code>, <code>-</code></h3>
<h4><code>#</code></h4>
<pre><code class="language-bash">$#
</code></pre>
<p>positional parameter의 개수를 십진수(decimal)로 반환한다. 즉 전달된 argument의 개수를 나타낸다. 예를 들면 이렇다.</p>
<table>
<thead>
<tr>
<th>명령어</th>
<th><code>$#</code> 값</th>
</tr>
</thead>
<tbody><tr>
<td><code>ls abc.txt</code></td>
<td>1</td>
</tr>
<tr>
<td><code>cp abc.txt def.txt</code></td>
<td>2</td>
</tr>
</tbody></table>
<p>참고로 <code>#</code>은 context(문맥)에 따라 의미가 달라진다는 점을 기억해두자.</p>
<table>
<thead>
<tr>
<th>위치</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td>Shell prompt</td>
<td>root 사용자</td>
</tr>
<tr>
<td>코드 내부</td>
<td>주석(comment)</td>
</tr>
<tr>
<td><code>$#</code></td>
<td>argument 개수</td>
</tr>
</tbody></table>
<h4><code>?</code></h4>
<pre><code class="language-bash">$?
</code></pre>
<p>가장 최근에 실행된 명령어의 exit code(종료 상태값)로 확장된다. <code>cp abc.txt def.txt</code>가 정상적으로 종료되었다면 <code>$?</code>는 0을 반환한다. 앞서 <code>&amp;&amp;</code>와 <code>||</code>에서 다뤘던 exit code 개념이 여기서 그대로 적용된다.</p>
<h4><code>-</code></h4>
<pre><code class="language-bash">$-
</code></pre>
<p>현재 shell에 설정된 option(옵션)의 상태로 확장된다. 구체적인 활용은 직접 실습을 통해 확인해보자.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/4e683c9e-3ef8-4f82-b32c-53af959d17b8.png" alt="" style="display:block;margin:0 auto" />

<h3>Shell Parameters - 특수 문자 <code>$</code>, <code>!</code>, <code>0</code></h3>
<h4>Process와 Program의 차이</h4>
<p>본격적인 내용에 앞서 process(프로세스)의 개념을 짚고 넘어간다.</p>
<ul>
<li><p><strong>Program(프로그램)</strong> : SSD와 같은 disk에 저장되어 있는 상태. 실행되지 않고 보관만 되어 있는 정적인 존재다.</p>
</li>
<li><p><strong>Process(프로세스)</strong> : program이 실행되어 processor(프로세서) 안에서 활발하게 동작하고 있는 상태. 살아있는 동적인 존재다.</p>
</li>
</ul>
<p>참고로 processor는 process를 처리하는 주체, 즉 CPU를 의미한다. <code>-or</code>이 붙으면 행위의 주체가 되듯이, processor는 process를 처리하는 것이다.</p>
<p>실행 중인 각 process에는 고유한 식별자인 PID(Process ID)가 번호로 부여된다. 현재 실행 중인 process와 PID는 <code>ps</code> 명령어로 확인할 수 있다.</p>
<h4><code>$</code>, <code>!</code>, <code>0</code></h4>
<table>
<thead>
<tr>
<th>특수 문자</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>$$</code></td>
<td>현재 shell의 PID로 확장. subshell에서는 shell을 요청한 process의 PID</td>
</tr>
<tr>
<td><code>$!</code></td>
<td>가장 최근에 background로 실행한 process의 PID로 확장</td>
</tr>
<tr>
<td><code>$0</code></td>
<td>현재 실행 중인 shell 또는 shell script의 이름으로 확장</td>
</tr>
</tbody></table>
<p>앞서 background 실행을 <code>&amp;</code>로 한다고 배웠는데, <code>$!</code>는 그 background process의 PID를 가져올 때 사용한다.</p>
<hr />
<h1>2️⃣Shell Scripts</h1>
<p>Shell은 지금까지 배워온 그 검은 명령창 환경 자체다. Shell은 단순한 프로그램이면서도 kernel과 사용자의 입출력을 연결해주는 역할을 한다. 지금까지 배운 내용을 정리하면 shell에서 명령을 사용하는 방법은 다양하다.</p>
<ul>
<li><p>명령어를 하나씩 입력하고 Enter</p>
</li>
<li><p><code>;</code>으로 여러 명령어를 한 줄에 연결</p>
</li>
<li><p><code>&amp;&amp;</code>, <code>||</code>로 조건부 실행</p>
</li>
<li><p><code>if</code>, <code>fi</code>, <code>for</code>, <code>while</code> 등으로 조건문과 반복문 활용</p>
</li>
</ul>
<p>그렇다면 이것들을 구체적으로 어떻게 활용할까? 바로 shell script(쉘 스크립트)다.</p>
<p>Shell script란 배우의 대본(script)과 같다. 배우가 대본에 적힌 대로 연기를 하듯, shell은 script에 적힌 명령들을 순서대로 읽고 실행한다. 즉 앞서 배운 모든 명령어, 조건문, 반복문을 하나의 파일에 담아 한꺼번에 실행할 수 있도록 만든 것이 shell script다.</p>
<p>모든 사람이 shell script를 작성할 일이 있는 것은 아니다. 그러나 개발자로서 이런 것이 존재한다는 것을 알고, 필요할 때 활용할 수 있다는 것을 인지하는 것만으로도 충분한 가치가 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/fee665e4-4474-4ff7-9f65-5c478c2e4ab0.png" alt="" style="display:block;margin:0 auto" />

<h3>Shell Scripts란?</h3>
<p>Shell script는 shell 명령어들을 포함하고 있는 text file(텍스트 파일)이다. 사람이 읽을 수 있는 형태의 문자들이 나열되어 있는 파일이다.</p>
<h2>Shell Script 실행 방법</h2>
<p><strong>1) slash(</strong><code>/</code><strong>)가 없는 파일 이름의 경우 - 현재 directory에서 먼저 탐색</strong></p>
<p>파일 이름이 단순히 <code>a</code>처럼 <code>/</code>가 없는 경우, shell은 가장 먼저 현재 작업 중인 directory(current working directory)에서 해당 파일을 찾는다. 현재 작업 directory는 <code>pwd</code> 명령어로 확인할 수 있으며, 로그인 직후에는 보통 home directory(<code>/home/linux</code> 또는 <code>~</code>)가 된다.</p>
<p>반면 <code>/a</code>처럼 <code>/</code>가 붙으면 root directory 아래에서, <code>/var/a</code>처럼 경로가 지정되면 해당 경로에서 찾는다.</p>
<p><strong>2) 현재 directory에 없으면 -</strong> <code>$PATH</code><strong>에 지정된 순서로 탐색</strong></p>
<p>현재 directory에서 찾지 못하면 <code>$PATH</code> environment variable(환경변수)에 지정된 경로들을 순서대로 탐색한다. 현재 설정된 PATH는 다음 명령어로 확인할 수 있다.</p>
<pre><code class="language-bash">echo $PATH
</code></pre>
<p>PATH에 등록된 경로들이 순서대로 나열되며, shell은 이 순서대로 파일을 찾는다. 이 순서는 사용자 설정에 따라 달라질 수 있으며 절대적이지 않다.</p>
<p><strong>3) 실행 권한(execute permission)의 유무</strong></p>
<p><code>ls -al</code> 명령어로 파일 권한을 확인했을 때 <code>rwx</code> 중 <code>x</code>가 있느냐 없느냐가 실행 권한을 나타낸다. 권한은 소유자(owner), 소유자 그룹(group), 기타(others) 순서로 표시된다.</p>
<table>
<thead>
<tr>
<th>상황</th>
<th>실행 방법</th>
</tr>
</thead>
<tbody><tr>
<td>실행 권한(<code>x</code>)이 있을 때</td>
<td><code>a</code> 입력 후 Enter</td>
</tr>
<tr>
<td>실행 권한(<code>x</code>)이 없을 때</td>
<td><code>bash a</code> 입력 후 Enter</td>
</tr>
</tbody></table>
<p>실행 권한은 <code>chmod</code> 명령어로 부여할 수 있다. 권한이 없을 때 <code>-</code>로 표기된 자리에 <code>x</code>를 부여하면 파일 이름만으로 실행이 가능해진다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/ed3672e7-4b11-457d-b188-ab9e7892f673.png" alt="" style="display:block;margin:0 auto" />

<h3>Shell Script 첫 줄의 <code>#!</code> (Shebang)</h3>
<p>Shell script 파일의 맨 첫 줄에 <code>#!</code>가 빈칸 없이 붙어 있는 경우, 이 줄은 해당 script를 해석할 interpreter(인터프리터)를 지정하는 역할을 한다. 이것을 shebang(쉬뱅)이라고 부른다.</p>
<pre><code class="language-bash">#!/usr/bin/bash
</code></pre>
<p><code>/usr/bin/bash</code>에 위치한 bash를 interpreter로 사용하겠다는 의미다. 경로를 명확하게 고정 지정하는 방식이다. 만약 이 줄이 없다면 현재 사용 중인 shell이 자동으로 script를 해석한다. 그러나 해당 경로에 bash가 존재하지 않으면 실행이 실패한다.</p>
<pre><code class="language-bash">#!/usr/bin/env bash
</code></pre>
<p><code>env</code>를 사용하면 고정된 경로 대신 <code>$PATH</code>에 등록된 경로들을 순서대로 탐색해 가장 먼저 찾아지는 bash interpreter를 사용한다.</p>
<p>두 방식의 차이를 정리하면 이렇다.</p>
<table>
<thead>
<tr>
<th></th>
<th><code>#!/usr/bin/bash</code></th>
<th><code>#!/usr/bin/env bash</code></th>
</tr>
</thead>
<tbody><tr>
<td>탐색 방식</td>
<td>고정 경로에서만 탐색</td>
<td><code>$PATH</code> 전체에서 탐색</td>
</tr>
<tr>
<td>bash가 해당 경로에 없을 때</td>
<td>실행 실패</td>
<td>다른 경로에서 계속 탐색</td>
</tr>
<tr>
<td>이식성(portability)</td>
<td>낮음</td>
<td>높음</td>
</tr>
</tbody></table>
<p>즉 <code>env</code>를 사용하는 방식이 더 일반적이고 다양한 환경에서 동작할 가능성이 높다. 시스템마다 bash의 설치 경로가 다를 수 있기 때문에 <code>#!/usr/bin/env bash</code>가 더 안전한 선택이 될 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/d1f45ddd-7ca1-4058-bbc7-e458b2c7e5e8.png" alt="" style="display:block;margin:0 auto" />

<h3>Interactive Shell과 Non-Interactive Shell</h3>
<h4>Interactive Shell</h4>
<p>Interactive(인터랙티브)란 말 그대로 사용자와 컴퓨터가 서로 주고받는 환경이다. 사용자가 입력하면 컴퓨터가 반응하고, 그 반응에 따라 사용자가 다시 입력하는 방식이다. 예를 들어 <code>sudo apt install</code>을 입력했을 때 설치 목록이 나오고 <code>Y/N</code>을 물어보는 것이 대표적인 interactive 환경이다.</p>
<p>기술적으로는 입력(input)과 오류 출력(error output) 모두 terminal에 연결되어 있는 상태이며, 사용자의 terminal을 통해 읽기와 쓰기가 진행된다.</p>
<h4>Non-Interactive Shell</h4>
<p>Non-interactive는 사용자가 명령을 실행한 후 추가적인 입력 없이 컴퓨터가 알아서 처리하는 환경이다. 실행 후 사용자가 개입할 일이 없는 상태다.</p>
<p>파일 또는 string(문자열)의 내용을 읽고 명령을 실행하는 방식으로 동작하며, shell script 파일을 실행할 때의 환경이 바로 non-interactive shell에 해당한다.</p>
<p>두 가지를 정리하면 이렇다.</p>
<table>
<thead>
<tr>
<th></th>
<th>Interactive Shell</th>
<th>Non-Interactive Shell</th>
</tr>
</thead>
<tbody><tr>
<td>사용자 개입</td>
<td>필요</td>
<td>불필요</td>
</tr>
<tr>
<td>입출력</td>
<td>terminal에 연결</td>
<td>파일 또는 문자열</td>
</tr>
<tr>
<td>대표적인 예</td>
<td>명령창에서 직접 입력</td>
<td>shell script 실행</td>
</tr>
</tbody></table>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/b54ac342-f671-48a1-a6df-c7f2ed23cf1e.png" alt="" style="display:block;margin:0 auto" />

<h3>환경 파일 (Environment Files)</h3>
<p>Bash shell을 사용할 때 shell의 환경을 설정하는 파일들이 있다. prompt 모양, 현재 사용자 이름, 현재 directory 표시 방식, <code>PATH</code> 설정 등을 이 파일들에서 지정할 수 있다.</p>
<h4>Login Shell 환경 파일 읽기 순서</h4>
<p>Login shell이 시작될 때 다음 순서로 환경 파일을 읽고 실행한다.</p>
<ol>
<li><p><code>/etc/profile</code> - 존재한다면 가장 먼저 읽음 (시스템 전체에 적용)</p>
</li>
<li><p><code>~/.bash_profile</code> - 개인 home directory의 설정 파일</p>
</li>
<li><p><code>~/.bash_login</code></p>
</li>
<li><p><code>~/.profile</code></p>
</li>
</ol>
<p>2, 3, 4번은 순서대로 탐색하며 존재하는 파일을 읽는다. <code>~</code>는 본인의 home directory를 의미한다.</p>
<h4>Logout 시 실행되는 파일</h4>
<p>Interactive login shell이 종료되거나 non-interactive login shell의 실행이 끝나면 다음 파일을 읽고 실행한다.</p>
<pre><code class="language-bash">~/.bash_logout
</code></pre>
<h4>환경 파일 확인 방법</h4>
<pre><code class="language-bash">ls -al ~
</code></pre>
<p>위 명령어를 실행하면 <code>.</code>으로 시작하는 숨김 파일(hidden file)들이 표시된다. 이 중에 위에서 언급한 환경 파일들이 보일 것이다. 파일이 존재하지 않는 경우도 있는데, 이는 설치 버전의 차이가 아니라 계정 생성 방식에 따른 차이다. 없다면 직접 만들어서 사용하면 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/11452a6f-ca4a-4101-8672-7c33f43c8d9e.png" alt="" style="display:block;margin:0 auto" />

<h3>Bash 환경 파일 2</h3>
<h4>1) Interactive Non-Login Shell</h4>
<p>처음에는 "interactive인데 왜 login이 없지?"라는 의문이 생길 수 있다. 이는 이미 login이 된 상태에서 새로운 shell을 열거나 terminal을 추가로 실행하는 경우를 말한다. 이미 인증이 된 상태이므로 login 과정 없이 interactive하게 사용할 수 있는 것이다. 이 경우 <code>~/.bashrc</code> 파일이 존재하면 해당 파일을 읽고 실행한다.</p>
<h4>2) <code>su</code> 명령어와 <code>~/.bashrc</code></h4>
<p>처음 login한 다음 script를 실행하거나, <code>-</code> 옵션 없이 <code>su</code> 명령어를 사용하는 경우 환경 전체를 초기화하지 않고 <code>~/.bashrc</code>만 실행한다.</p>
<table>
<thead>
<tr>
<th>명령어</th>
<th>동작</th>
</tr>
</thead>
<tbody><tr>
<td><code>su -</code></td>
<td>환경 전체를 초기화하고 새로운 login shell 시작</td>
</tr>
<tr>
<td><code>su</code></td>
<td>환경을 초기화하지 않고 <code>~/.bashrc</code>만 실행</td>
</tr>
</tbody></table>
<h4>3) <code>~/.bash_profile</code>에 포함되는 일반적인 내용</h4>
<pre><code class="language-bash">if [ -f ~/.bashrc ]; then . ~/.bashrc; fi
</code></pre>
<p>이 구문의 의미는 "~/.bashrc 파일이 존재하면 실행하라"는 뜻이다. <code>-f</code>는 해당 파일이 존재하는지 확인하는 조건이다. 파일이 없는 상태에서 무조건 실행하라고 하면 오류 메시지가 발생하므로, 존재 여부를 먼저 확인하고 있을 때만 실행하는 것이다.</p>
<p>여기서 <code>[ ]</code>의 의미에 대해 짚고 넘어갈 필요가 있다. 앞서 매뉴얼 구문에서 <code>[ ]</code>는 optional을 의미했지만, 여기서의 <code>[ ]</code>는 전혀 다른 의미다. <code>if</code>문 안에서 조건을 판별하기 위한 expression(표현식)을 담는 조건문 기호다. 같은 기호라도 context(문맥)에 따라 의미가 달라진다는 점을 기억해두자.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/3a007656-5ef5-4f9c-82e1-9618de2381d6.png" alt="" style="display:block;margin:0 auto" />

<h3>Bourne Shell Variables - PS1, PS2</h3>
<h4>PS1 (Prompt String 1)</h4>
<p>PS1은 shell prompt에 표시되는 내용을 지정하는 변수다. 예를 들어 로그인했을 때 보이는 <code>linux@KOREACU:~$</code> 같은 형태가 PS1에 의해 결정된다.</p>
<p>PS1에서 사용할 수 있는 특수 문자들은 다음과 같다.</p>
<table>
<thead>
<tr>
<th>특수 문자</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>\s</code></td>
<td>shell의 이름</td>
</tr>
<tr>
<td><code>\v</code></td>
<td>shell의 버전(version)</td>
</tr>
<tr>
<td><code>\$</code></td>
<td>superuser이면 <code>#</code>, 일반 사용자이면 <code>$</code></td>
</tr>
<tr>
<td><code>\w</code></td>
<td>home directory를 나타내는 <code>~</code>와 <code>pwd</code> 값</td>
</tr>
<tr>
<td><code>\h</code></td>
<td>첫 번째 <code>.</code>까지의 hostname</td>
</tr>
<tr>
<td><code>\u</code></td>
<td>현재 사용자 이름</td>
</tr>
</tbody></table>
<p>이 특수 문자들을 single quote(<code>'</code>) 안에 조합해서 PS1을 설정하면 login 시 prompt에 해당 내용들이 표시된다.</p>
<h4>PS2 (Prompt String 2)</h4>
<p>PS2는 명령어가 여러 줄에 걸쳐 입력될 때 표시되는 secondary prompt다. 기본값은 <code>&gt;</code>다.</p>
<h4>주의사항</h4>
<p>PS1에 너무 많은 정보를 담으면 prompt가 길어져 실제로 명령어를 입력할 수 있는 공간이 줄어든다. 따라서 꼭 필요한 정보만 선택해서 표시하는 것이 좋다.</p>
<hr />
<h3>스크립트 파일 <code>.profile</code> (일부)</h3>
<p><code>ls -al ~</code> 명령어로 home directory의 숨김 파일들을 확인하면 <code>.profile</code> 파일을 볼 수 있다. 이 파일의 내용을 살펴보면 다음과 같은 구조로 되어 있다.</p>
<h4>주석문</h4>
<p><code>#</code>으로 시작하는 줄은 주석문(comment)으로, 컴퓨터는 이 내용을 무시한다. 사람이 읽기 위한 설명을 적어두는 용도다.</p>
<h4>조건문 구조</h4>
<pre><code class="language-bash">if [ -n "$BASH_VERSION" ]; then
    if [ -f "$HOME/.bashrc" ]; then
        . "$HOME/.bashrc"
    fi
fi
</code></pre>
<p>여기서 사용된 조건 flag의 의미는 다음과 같다.</p>
<table>
<thead>
<tr>
<th>Flag</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>-n</code></td>
<td>string의 길이가 0이 아니면 참</td>
</tr>
<tr>
<td><code>-f</code></td>
<td>해당 이름이 존재하고 일반 파일(regular file)이면 참</td>
</tr>
</tbody></table>
<h4>동작 방식</h4>
<p><strong>첫 번째 조건</strong> <code>[ -n "\(BASH_VERSION" ]</code>은 <code>\)BASH_VERSION</code> 변수에 값이 있는지 확인한다. 이 변수에 값이 있다는 것은 현재 bash shell을 사용 중이라는 의미다.</p>
<p><strong>두 번째 조건</strong> <code>[ -f "$HOME/.bashrc" ]</code>는 home directory에 <code>.bashrc</code> 파일이 존재하고 일반 파일인지 확인한다. <code>-f</code>에서 일반 파일(regular file)이라고 명시하는 이유는 같은 이름의 directory가 존재할 수도 있기 때문이다.</p>
<p><strong>두 조건이 모두 참이면</strong> <code>. "$HOME/.bashrc"</code>를 실행한다. 즉 bash shell을 사용 중이고 <code>.bashrc</code> 파일이 존재할 때만 해당 파일을 실행하는 안전한 구조다.</p>
<hr />
<h3><code>.profile</code> - PATH 설정</h3>
<pre><code class="language-bash"># set PATH so it includes user's private bin if it exists
if [ -d "$HOME/bin" ]; then
    PATH="\(HOME/bin:\)PATH"
fi
</code></pre>
<p>첫 줄의 <code>#</code>은 주석문이므로 컴퓨터는 무시한다. 사람이 읽기 위한 설명이다.</p>
<h4>조건문 <code>-d</code></h4>
<p>앞서 <code>-f</code>가 regular file 여부를 확인했다면, <code>-d</code>는 directory 여부를 확인한다.</p>
<table>
<thead>
<tr>
<th>Flag</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>-f</code></td>
<td>일반 파일(regular file)이면 참</td>
</tr>
<tr>
<td><code>-d</code></td>
<td>directory이면 참</td>
</tr>
</tbody></table>
<h4>동작 방식</h4>
<p>home directory 안에 <code>bin</code> directory가 존재하면 <code>PATH="\(HOME/bin:\)PATH"</code>를 실행한다. 보통은 home directory 안에 <code>bin</code>이 없지만, 존재할 경우 PATH에 추가하는 것이다.</p>
<p>여기서 핵심은 <strong>순서</strong>다. <code>\(HOME/bin</code>이 앞에, 기존 <code>\)PATH</code>가 뒤에 오도록 콜론(<code>:</code>)으로 구분해 설정한다. PATH는 앞에서부터 순서대로 탐색하므로, 이렇게 설정하면 <code>$HOME/bin</code>을 기존 PATH보다 <strong>먼저</strong> 탐색하게 된다.</p>
<p>즉 같은 이름의 실행 파일이 여러 경로에 존재할 때, home directory의 <code>bin</code>에 있는 것을 우선적으로 사용하겠다는 의미다.</p>
<hr />
<h3><code>.bashrc</code> (일부)</h3>
<pre><code class="language-bash"># if not running interactively, don't do anything 
case $- in
    *i*) ;;
    *) return;;
esac
</code></pre>
<p>첫 줄의 #은 주석문이다. "interactive로 실행 중이 아니라면 아무것도 하지 말아라"라는 설명이다.</p>
<p><strong>구조 분석 :</strong> 앞서 배운 case문이 실제로 사용된 예시다. esac으로 끝나는 것도 확인할 수 있다. 그리고 패턴 앞에 (를 써도 되고 안 써도 된다고 했는데, 여기서는 생략된 형태로 사용되고 있다. $-는 앞서 배운 현재 shell의 option 상태를 나타내는 특수 변수다. 이 값 안에 i가 포함되어 있으면 interactive shell임을 의미한다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/7eb05c98-9c2c-4593-8c33-bae1997398d6.png" alt="" style="display:block;margin:0 auto" />

<p><strong>순서가 중요한 이유 :</strong> 만약 <code>*i*</code>와 <code>*</code>의 순서를 바꾸면 동작이 달라진다. <code>*</code>는 모든 경우에 해당되므로 먼저 오면 <code>i</code>가 포함된 경우도 모두 <code>return</code>을 실행해버린다. <code>case</code>문은 위에서부터 순서대로 패턴을 비교하므로, 구체적인 조건을 먼저, 포괄적인 조건을 나중에 써야 의도한 대로 동작한다.</p>
<hr />
<h3>.bashrc (일부) - chroot 설정</h3>
<pre><code class="language-bash"># set variable identifying the chroot you work in (used in the prompt below)
if [ -z "${debian_chroot:-}" ] &amp;&amp; [ -r /etc/debian_chroot ]; then
    debian_chroot=$(cat /etc/debian_chroot)
fi
</code></pre>
<p>첫 줄은 <code>#</code> 주석문으로 "현재 작업 중인 chroot를 식별하는 변수를 설정한다"는 설명이다.</p>
<h4>새롭게 등장한 조건 flag</h4>
<p>지금까지 배운 flag들을 정리하면 이렇다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/d9d41035-e5f3-4b88-a729-b93fd7d83e4b.png" alt="" style="display:block;margin:0 auto" />

<p><code>-n</code>과 <code>-z</code>는 서로 반대 관계다. 동작 방식 두 조건을 <code>&amp;&amp;</code>로 연결했으므로 둘 다 참일 때만 실행된다.</p>
<p>첫 번째 조건 <code>[ -z "${debian_chroot:-}" ] : debian_chroot</code> 변수의 값이 비어있으면 참 두 번째 조건 <code>[ -r /etc/debian_chroot ] : /etc/debian_chroot</code> 파일이 존재하고 읽기 권한이 있으면 참</p>
<p>두 조건이 모두 참이면 <code>debian_chroot=$(cat /etc/debian_chroot)</code>를 실행한다. cat 명령어로 <code>/etc/debian_chroot</code> 파일의 내용을 읽어 <code>debian_chroot</code> 변수에 저장하는 것이다.</p>
]]></content:encoded></item><item><title><![CDATA[White-Box Testing: Branch/Condition, MC/DC, and Multiple Condition Coverage]]></title><description><![CDATA[1️⃣ Branch/Condition Testing2️⃣ Modified Condition/Decision Coverage Testing3️⃣ Multi-condition testing

학습 목표
오늘 강의에서는 화이트박스 테스팅 기법 중 커버리지 기반의 세 가지 기법을 다룬다.
첫째, 분기·조건 테스팅 기법 (Branch and Condition Tes]]></description><link>https://heesu.tech/white-box-testing-branch-condition-mc-dc-and-multiple-condition-coverage</link><guid isPermaLink="true">https://heesu.tech/white-box-testing-branch-condition-mc-dc-and-multiple-condition-coverage</guid><dc:creator><![CDATA[Heesu Noh]]></dc:creator><pubDate>Fri, 01 May 2026 15:08:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/cfb6ef5c-348c-425d-99ca-5c7184eb020b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>1️⃣ Branch/Condition Testing<br />2️⃣ Modified Condition/Decision Coverage Testing<br />3️⃣ Multi-condition testing</p>
<hr />
<h3>학습 목표</h3>
<p>오늘 강의에서는 화이트박스 테스팅 기법 중 커버리지 기반의 세 가지 기법을 다룬다.</p>
<p>첫째, <strong>분기·조건 테스팅 기법 (Branch and Condition Testing)</strong> 을 이해하고 설명할 수 있어야 한다. 둘째, <strong>변경조건·결정 테스팅 기법 (Modified Condition/Decision Coverage, MC/DC)</strong> 을 이해하고 설명할 수 있어야 한다. 셋째, <strong>다중조건 테스팅 기법 (Multiple Condition Testing)</strong> 을 이해하고 설명할 수 있어야 한다.</p>
<p>세 기법 모두 현업에서 실제로 존재하는 화이트박스 테스팅 기법이다. 특히 MC/DC는 미국 FAA(연방항공청)의 항공 소프트웨어 안전 표준(DO-178B/C)에서 요구하는 기법으로, 높은 신뢰성이 요구되는 시스템에서 광범위하게 사용된다.</p>
<hr />
<h1>1️⃣ Branch/Condition Testing</h1>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/833bc7fe-b07f-46b6-82dc-ecc96c7512b8.png" alt="" style="display:block;margin:0 auto" />

<h3>분기/조건 테스팅 (Branch Condition Testing)</h3>
<p>분기/조건 테스팅은 앞서 별개로 학습했던 <strong>분기 테스팅 (Branch Testing)</strong> 과 <strong>조건 테스팅 (Condition Testing)</strong> 을 하나로 합친 기법이다. 두 기법을 커버리지 기준으로 살펴보면 서로 겹치는 부분도 있지만, 한쪽이 다른 쪽을 완전히 포함하지 못하는 사례가 존재한다. 이러한 한계를 보완하기 위해 두 기법을 결합한 것이 분기/조건 테스팅이다.</p>
<p><strong>정의</strong>는 다음과 같다. 전체 조건식 (분기문 전체) 이 True 한 번, False 한 번을 갖는 동시에, 각 개별 조건 (Individual Condition) 역시 True 한 번, False 한 번을 갖도록 테스트 케이스를 선정하여 테스트하는 기법이다.</p>
<p>장학생 선정 기능을 예제로 살펴보자. <code>if ((평균 &gt;= 90) &amp;&amp; (과목수 &gt;= 5))</code> 라는 구문에서, 마름모 전체가 하나의 <strong>전체 조건식 (Decision)</strong> 이자 하나의 분기가 된다. 이 분기는 Yes/No, 즉 True/False로 나뉜다. 그리고 if문 안의 <code>평균 &gt;= 90</code>과 <code>과목수 &gt;= 5</code>는 각각 <strong>개별 조건 (Individual Condition)</strong> 이며, 이 역시 각각 True/False를 가진다.</p>
<p>따라서 분기/조건 테스팅이 만족해야 할 항목은 다음과 같다.</p>
<ul>
<li><p>전체 조건식의 True/False → 2가지</p>
</li>
<li><p><code>평균 &gt;= 90</code>의 True/False → 2가지</p>
</li>
<li><p><code>과목수 &gt;= 5</code>의 True/False → 2가지</p>
</li>
</ul>
<p>합계 총 6가지이며, 이 6가지를 모두 충족하는 테스트 케이스를 설계하는 것이 목표다.</p>
<p><strong>분기/조건 커버리지 (Branch/Condition Coverage)</strong> 는 다음과 같이 계산한다.</p>
<blockquote>
<p>테스트한 전체 조건 및 개별 조건의 T/F 수 ÷ 전체 조건 및 개별 조건의 T/F 수 × 100%</p>
</blockquote>
<p>예를 들어 (95점, 6과목), (72점, 3과목) 두 케이스를 테스트하면, 첫 번째 케이스는 전체 조건식이 True이고 두 개별 조건 모두 True가 된다. 두 번째 케이스는 전체 조건식이 False이고 두 개별 조건 모두 False가 된다. 결과적으로 6가지 항목을 모두 커버하므로 <strong>분기/조건 커버리지 = 6/6 = 100%</strong> 가 된다.</p>
<p>📌한 가지 주의할 점이 있다. (72점, 3과목) 케이스에서 전체 조건식은 False이고 두 개별 조건 모두 False가 되어 커버리지 계산상 6/6이 성립하지만, 이는 개별 조건의 True/False가 전체 조건식의 True/False와 항상 독립적으로 관찰되는 것이 아님을 의미한다. 이것이 바로 분기/조건 테스팅의 한계이며, 이를 보완하기 위해 MC/DC가 등장하는 배경이 된다.</p>
<hr />
<h3>분기/조건 테스팅 (Branch Condition Testing); 등장 배경</h3>
<p>분기/조건 테스팅이 왜 등장했는지 이해하려면, 먼저 기존 두 기법의 한계를 살펴야 한다.</p>
<p><strong>분기 테스팅 (Branch Testing)</strong> 의 결과물은 <strong>분기 커버리지 (Branch Coverage)</strong> 이고, <strong>조건 테스팅 (Condition Testing)</strong> 의 결과물은 <strong>조건 커버리지 (Condition Coverage)</strong> 이다. 이 둘은 어느 정도 중복되는 영역이 있지만, 분기 커버리지 100%가 조건 커버리지 100%를 보장하지 못하고, 반대로 조건 커버리지 100%가 분기 커버리지 100%를 보장하지 못하는 경우가 발생한다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/b2663c1d-3f67-41f9-af64-0423e4402ab8.png" alt="" style="display:block;margin:0 auto" />

<p><code>A AND B</code> 라는 전체 조건식을 예시로 살펴보자.</p>
<p><strong>분기 테스팅만 적용한 경우</strong>, 전체 조건식이 True 한 번, False 한 번 나오는 테스트 케이스를 설계한다. 예를 들어 <code>A=True</code>, <code>B=False</code>이면 전체 결과는 <code>False</code>, <code>A=True</code>, <code>B=True</code>이면 전체 결과는 True가 된다. 전체 조건식의 <code>True/False</code>가 모두 충족되어 분기 커버리지는 100%이다. 그러나 이 경우 개별 조건 (Individual Condition) A는 <code>True/True</code>만 존재하고 <code>False</code>가 한 번도 등장하지 않는다. 즉 조건 커버리지를 만족하지 못한다.</p>
<p><strong>조건 테스팅만 적용한 경우</strong>, 개별 조건 A와 B 각각이 <code>True/False</code>를 한 번씩 가져야 한다. <code>A=True</code>, <code>B=False</code> 그리고 <code>A=False</code>, <code>B=True</code>로 설계하면 조건 커버리지는 100% 만족된다. 그러나 전체 조건식의 결과는 <code>True AND False = False</code>, <code>False AND True = False</code>로 모두 <code>False</code>가 된다. 전체 조건식 (분기) 의 <code>True</code>가 한 번도 나오지 않아 분기 커버리지를 만족하지 못한다.</p>
<p>이처럼 두 기법이 각각 서로의 커버리지를 보완하지 못하는 상황을 해결하기 위해 등장한 것이 <strong>분기/조건 테스팅 (Branch Condition Testing)</strong> 이다. 이 기법으로 테스트 케이스를 설계하면, 개별 조건 <code>A=False</code>, <code>B=False</code> 그리고 <code>A=True</code>, <code>B=True</code>로 구성할 수 있다. AND 연산 결과 전체 조건식은 <code>False</code>, <code>True</code>가 되어 분기 커버리지도 100%, 조건 커버리지도 100% 만족하는 단 두 개의 테스트 케이스가 완성된다.</p>
<p>이처럼 테스트 설계 기법들은 <strong>테스트 케이스 수는 줄이면서 커버리지는 높이는</strong> 방향으로 발전해왔다. 테스트에서는 항상 효율성 (Efficiency) 과 효과성 (Effectiveness) 을 함께 고려해야 하며, 단순히 테스트 케이스를 무작정 늘리는 것은 좋은 전략이 아니다.</p>
<hr />
<h3>임의의 프로그램에 대한 분기/조건 테스팅 적용 (1/2)</h3>
<p>앞서 다른 기법에서도 사용했던 동일한 코드를 이번에는 분기/조건 테스팅 관점에서 분석해보자. 입력값은 <code>X, Y, Z</code> 세 가지이며, if문이 두 개 존재하고 각각의 if문 안에 개별 조건식들이 포함되어 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/25005225-b00d-4b5b-9974-d632394d24fc.png" alt="" style="display:block;margin:0 auto" />

<p>이를 <strong>제어 흐름도 (Control Flow Graph)</strong> 로 표현하면 다음과 같이 구성된다.</p>
<p>목표는 <strong>분기/조건 커버리지 (Branch/Condition Coverage) 100%</strong> 를 만족하는 테스트 케이스를 설계하는 것이다. 총 <strong>12가지 항목</strong>을 모두 커버하는 테스트 케이스를 최소한의 수로 설계하는 것이 이번 예시의 핵심이다. 구체적인 테스트 케이스 도출은 다음 슬라이드에서 이어진다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/33bb5691-f168-4388-a4cd-12ca3e734127.png" alt="" style="display:block;margin:0 auto" />

<h4>임의의 프로그램에 대한 분기/조건 테스팅 적용 (2/2)</h4>
<p>목표는 모든 분기를 지나는 경로를 포함하면서 개별 조건의 <code>True/False</code>를 만족하는 <strong>최소한의 테스트 케이스</strong>를 도출하는 것이다. 앞서 확인했듯이 만족해야 할 항목은 총 12가지이다.</p>
<p><strong>테스트 케이스 1:</strong> <code>X=0, Y=0, Z=1</code></p>
<ul>
<li><p>개별 조건1 <code>X &gt; 1</code> → 0 &gt; 1이므로 <strong>False</strong></p>
</li>
<li><p>개별 조건2 <code>Y == 1</code> → 0 == 1이므로 <strong>False</strong></p>
</li>
<li><p>분기1 전체 <code>False AND False</code> → <strong>False</strong></p>
</li>
<li><p>개별 조건3 <code>X == 2</code> → 0 == 2이므로 <strong>False</strong></p>
</li>
<li><p>개별 조건4 <code>Z &gt; 1</code> → 1 &gt; 1이므로 <strong>False</strong></p>
</li>
<li><p>분기2 전체 <code>False OR False</code> → <strong>False</strong></p>
</li>
</ul>
<p><strong>테스트 케이스 2:</strong> <code>X=2, Y=1, Z=6</code></p>
<ul>
<li><p>개별 조건1 <code>X &gt; 1</code> → 2 &gt; 1이므로 <strong>True</strong></p>
</li>
<li><p>개별 조건2 <code>Y == 1</code> → 1 == 1이므로 <strong>True</strong></p>
</li>
<li><p>분기1 전체 <code>True AND True</code> → <strong>True</strong></p>
</li>
<li><p>개별 조건3 <code>X == 2</code> → 2 == 2이므로 <strong>True</strong></p>
</li>
<li><p>개별 조건4 <code>Z &gt; 1</code> → 6 &gt; 1이므로 <strong>True</strong></p>
</li>
<li><p>분기2 전체 <code>True OR True</code> → <strong>True</strong></p>
</li>
</ul>
<p>이 두 개의 테스트 케이스만으로 12가지 항목을 모두 커버하므로 <strong>분기/조건 커버리지 = 12/12 = 100%</strong> 가 달성된다.</p>
<p>이처럼 동일한 커버리지 목표를 달성하더라도 테스트 케이스를 어떻게 설계하느냐에 따라 케이스의 수가 크게 달라질 수 있다. 소프트웨어 테스팅에서는 항상 프로젝트의 시간과 비용 제약이 존재하므로, 테스트 케이스 수는 줄이면서 커버리지는 최대한 높이는 것이 핵심 목표이다.</p>
<p>이번 챕터에서는 분기 테스팅과 조건 테스팅을 결합한 <strong>분기/조건 테스팅 (Branch Condition Testing)</strong> 의 개념과 적용 예시를 살펴보았다. 그러나 이 기법 역시 한계가 존재하며, 관련 내용은 다음 기법인 MC/DC에서 이어진다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/66f9bf61-9a1c-49eb-ae7a-04755bcae8cb.png" alt="" style="display:block;margin:0 auto" />

<h1>2️⃣ Change Condition/Decision Testing</h1>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/c48d8953-c679-4dfa-aff7-3e2853680c19.png" alt="" style="display:block;margin:0 auto" />

<h3>변경조건/결정 테스팅 (Modified Condition/Decision Coverage, MC/DC)</h3>
<p><strong>MC/DC</strong> 라는 이름은 다음과 같이 해석할 수 있다. <strong>Condition</strong> 은 앞서 배운 조건 테스팅의 개별 조건을, <strong>Decision</strong> 은 분기, 즉 전체 조건식을 의미한다. 그리고 <strong>Modified</strong> 는 기존 분기/조건 테스팅에 문제가 있어 이를 변경·보완했다는 의미이다.</p>
<p><strong>기존 분기/조건 테스팅의 문제점</strong>은 다음과 같다. 하나의 분기문 안에는 여러 개별 조건이 존재하는데, 어떤 개별 조건의 True/False를 변경하더라도 전체 조건식의 결과에 아무런 영향을 주지 못하는 경우가 발생할 수 있다. 즉 커버리지 수치는 충족되지만 실질적으로 의미 없는 테스트 케이스가 포함될 수 있다는 것이다.</p>
<p>이를 해결하기 위해 등장한 것이 MC/DC이다. <strong>정의</strong>는 다음과 같다. 각 개별 조건이 다른 조건에 영향을 받지 않고 <strong>독립적</strong>으로 작동하면서, 동시에 해당 개별 조건의 변화가 <strong>전체 조건식의 결과에 영향을 주도록</strong> 테스트 케이스를 선정하여 테스트하는 기법이다.</p>
<p>장학생 선정 예제 <code>if ((평균 &gt;= 90) &amp;&amp; (과목수 &gt;= 5))</code> 를 통해 살펴보자. 가능한 케이스는 총 4가지이다.</p>
<table>
<thead>
<tr>
<th>평균 &gt;= 90</th>
<th>과목수 &gt;= 5</th>
<th>장학생 선정</th>
</tr>
</thead>
<tbody><tr>
<td>True</td>
<td>True</td>
<td>True</td>
</tr>
<tr>
<td>True</td>
<td>False</td>
<td>False</td>
</tr>
<tr>
<td>False</td>
<td>True</td>
<td>False</td>
</tr>
<tr>
<td>False</td>
<td>False</td>
<td>False</td>
</tr>
</tbody></table>
<p>이 4가지를 모두 테스트하는 것은 효율적이지 않다. MC/DC는 이 중 의미 있는 케이스만을 추려낸다.</p>
<p><strong>평균 조건의 독립적 영향 확인:</strong> 과목수를 True(6과목)로 고정한 상태에서 평균만 변경한다. <code>(95점, 6과목) → 장학생 선정 True, (72점, 6과목) → 장학생 선정 False</code>. 평균 조건의 변화가 전체 결과에 독립적으로 영향을 준다.</p>
<p><strong>과목수 조건의 독립적 영향 확인:</strong> 평균을 True(95점)로 고정한 상태에서 과목수만 변경한다. <code>(95점, 6과목) → 장학생 선정 True, (95점, 3과목) → 장학생 선정 False</code>. 과목수 조건의 변화가 전체 결과에 독립적으로 영향을 준다.</p>
<p>결과적으로 선정되는 테스트 케이스는 <strong>(95점, 6과목), (95점, 3과목), (72점, 6과목)</strong> 세 가지이다. <strong>False/False</strong> 케이스가 제외되는 이유는, 해당 상태에서 어느 한 개별 조건을 True로 바꾸더라도 AND 연산에 의해 전체 결과가 여전히 False로 유지되기 때문이다. 전체 조건식의 결과에 영향을 주지 못하므로 MC/DC 조건을 만족하지 못한다.</p>
<p><strong>MC/DC 커버리지</strong> 계산 공식은 다음과 같다.</p>
<blockquote>
<p>MC/DC를 만족하는 개별 조건 수 ÷ 테스트가 실제 수행된 MC/DC 만족 전체 개별 조건 수 × 100%</p>
</blockquote>
<p>위 예시에서는 평균과 과목수 두 개별 조건 모두 MC/DC 조건을 만족하므로 <strong>2/2 = 100%</strong> 가 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/85b056f6-c76a-4387-9ec8-5c665f788fdf.png" alt="" style="display:block;margin:0 auto" />

<h3>분기/조건 테스팅 (Branch Condition Testing)의 문제점</h3>
<p>분기/조건 테스팅이 100% 커버리지를 달성하더라도 개발자의 실수를 잡아내지 못하는 경우가 존재한다.</p>
<p>장학생 선정 기능 <code>if ((평균 &gt;= 90) &amp;&amp; (과목수 &gt;= 5))</code> 을 예로 들어보자. 개발자가 실수로 <code>&amp;&amp;</code> 대신 <code>||</code> 를 입력했다고 가정한다. 분기/조건 테스팅 관점에서 (95점, 6과목), (72점, 3과목) 두 케이스를 테스트하면 전체 조건식이 True/False를 모두 충족하고 개별 조건도 True/False를 모두 충족하여 <strong>분기/조건 커버리지 100%</strong> 가 달성된다. 겉으로 보기에는 아무런 문제가 없는 것처럼 보인다.</p>
<p>그러나 실제 동작을 들여다보면 심각한 문제가 숨어있다. <code>||</code> 연산에서는 첫 번째 조건인 평균이 True가 되는 순간, 두 번째 조건인 과목수는 <strong>아예 평가조차 되지 않고</strong> 전체 결과가 True로 확정된다. 즉 (95점, 2과목) 이든 (95점, 0과목) 이든 관계없이 장학생으로 선정되어버리는 것이다. 평균과 과목수를 <strong>동시에</strong> 충족해야 한다는 본래의 요구사항이 완전히 무시되고 있음에도 불구하고, 분기/조건 테스팅은 이 결함을 발견하지 못한다.</p>
<p>이것이 분기/조건 테스팅의 핵심적인 한계이다. 개별 조건 각각의 True/False와 전체 조건식의 True/False는 확인하지만, <strong>각 개별 조건이 전체 조건식의 결과에 실질적으로 독립적인 영향을 미치는지</strong>는 검증하지 못한다. 이 문제를 해결하기 위한 방법은 다음 슬라이드에서 이어진다.</p>
<p>📌프로그래밍에서 <strong>단락 평가 (Short-circuit Evaluation)</strong> 라고 부르는 현상과도 연관된다. <code>||</code> 연산에서 첫 번째 조건이 True이면 두 번째 조건은 평가되지 않고, <code>&amp;&amp;</code> 연산에서 첫 번째 조건이 False이면 두 번째 조건은 평가되지 않는다. 분기/조건 테스팅이 이러한 결함을 탐지하지 못하는 것은 실제로 잘 알려진 한계이며, 이것이 MC/DC 등장의 핵심 동기이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/3145c84d-40a6-4f0c-90f3-f3b236e57e28.png" alt="" style="display:block;margin:0 auto" />

<h3>분기/조건 테스팅 문제점의 해결 방안 (1) 모든 개별 조건을 테스트 케이스로 선정</h3>
<p>가장 단순하고 직관적인 해결 방안은 <strong>가능한 모든 개별 조건의 조합을 테스트 케이스로 선정</strong>하는 것이다. 개별 조건이 2개인 장학생 선정 예제의 경우 True/False 조합이 총 4가지이므로 현실적으로 수행 가능하다. 이 4가지를 모두 테스트하면 <code>&amp;&amp;</code> 가 <code>||</code> 로 잘못 작성된 개발자의 실수도 탐지할 수 있다. 오른쪽 표에서 볼 수 있듯이 <code>||</code> 연산에서는 (95점, 3과목), (72점, 6과목) 케이스의 전체 조건식 결과가 <code>&amp;&amp;</code> 연산과 달리 True로 나타나 결함이 드러나기 때문이다.</p>
<p>그러나 이 방법은 <strong>현실적이지 않다.</strong> 개별 조건이 2개일 때는 2² = 4가지이지만, 개별 조건이 늘어날수록 테스트 케이스 수는 <strong>기하급수적으로 증가</strong>한다. 예를 들어 개별 조건이 20개라면 2²⁰ = 1,048,576가지의 테스트 케이스가 필요하다. 이는 시간과 비용 측면에서 테스트의 효율성과 적합성에 완전히 부합하지 않는 방안이다. 보다 현실적인 해결 방안은 다음에서 이어진다.</p>
<p>**📌**개별 조건 n개에 대한 모든 조합의 수는 2ⁿ으로, 이를 <strong>전수 테스팅 (Exhaustive Testing)</strong> 이라 부른다. 이것이 현실적으로 불가능하다는 점은 소프트웨어 테스팅의 기본 원칙 중 하나인 <strong>"완전한 테스팅은 불가능하다 (Exhaustive Testing is Impossible)"</strong> 와도 일치한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/09c2ed77-b851-4d12-bf28-d44c533d1f6b.png" alt="" style="display:block;margin:0 auto" />

<h3>분기/조건 테스팅 문제점의 해결 방안 (2) 변경조건/결정 테스팅 (MC/DC) 적용</h3>
<p>앞서 살펴본 전수 테스팅 방식의 한계를 극복하는 현실적인 해결책이 바로 <strong>변경조건/결정 테스팅 (Modified Condition/Decision Coverage, MC/DC)</strong> 이다. 이 기법은 다음 세 가지 조건을 동시에 만족해야 한다.</p>
<ol>
<li><p>각 개별 조건은 다른 조건에 영향을 받지 않아야 한다. (<strong>독립성</strong> Independence)</p>
</li>
<li><p>각 개별 조건의 True/False 변화가 전체 조건식의 결과에 영향을 주어야 한다. (<strong>유효성,</strong> Validity )</p>
</li>
<li><p>테스트 케이스 수를 줄일 수 있어야 한다. (<strong>효율성,</strong> Efficiency)</p>
</li>
</ol>
<p>장학생 선정 예제 <code>if ((평균 &gt;= 90) &amp;&amp; (과목수 &gt;= 5))</code> 를 다시 살펴보자.</p>
<p><strong>평균 조건의 독립적 영향 확인:</strong> 과목수를 6(True)으로 고정하고 평균만 95(True) → 72(False)로 변경하면 전체 조건식이 True → False로 바뀐다. 평균 조건이 독립적으로 전체 결과에 영향을 준다.</p>
<p><strong>과목수 조건의 독립적 영향 확인:</strong> 평균을 95(True)로 고정하고 과목수만 6(True) → 3(False)으로 변경하면 전체 조건식이 True → False로 바뀐다. 과목수 조건 역시 독립적으로 전체 결과에 영향을 준다.</p>
<p>결과적으로 도출되는 테스트 케이스는 <strong>(95점, 6과목), (95점, 3과목), (72점, 6과목)</strong> 세 가지이며, 이것만으로 <strong>MC/DC 커버리지 2/2 = 100%</strong> 를 달성한다. 전수 테스팅의 4가지보다 1개 줄어든 수치이다. 더 나아가 개발자가 실수로 <code>||</code> 를 입력했을 경우, 이 3개의 테스트 케이스만으로도 결함을 탐지할 수 있다. <code>||</code> 연산에서는 (95점, 3과목)과 (72점, 6과목)의 전체 조건식 결과가 <code>&amp;&amp;</code> 연산과 다르게 True로 나타나기 때문이다.</p>
<p>일반적으로 <strong>MC/DC 테스팅의 테스트 케이스 수 = 개별 조건 수 + 1</strong> 로 구할 수 있다. 장학생 선정 예제에서는 개별 조건이 2개이므로 2 + 1 = <strong>3개</strong>의 테스트 케이스가 도출되며, 이는 실제 결과와 일치한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/6c1d3881-819f-4584-8ffb-1cb950e6b4c6.png" alt="" style="display:block;margin:0 auto" />

<h3>MC/DC 적용 예시 (1) AND 연산</h3>
<p><code>if ((X &gt; 1) AND (Y == 1))</code> 을 예시로 MC/DC를 적용해보자. 모든 조건을 고려하면 테스트 케이스는 총 4가지가 나온다. MC/DC는 이를 최소화하는 것이 목표이다.</p>
<p>개별 조건1(Individual Condition) <code>X &gt; 1</code> <strong>의 독립적 영향 확인 (TC1, TC3):</strong> <code>Y == 1</code> 을 True로 고정한 상태에서 X만 변경한다. X &gt; 1이 True → False로 바뀌면 전체 조건식도 True → False로 바뀐다. 유효한 케이스이다.</p>
<p>개별 조건2(Individual Condition) <code>Y == 1</code> <strong>의 독립적 영향 확인 (TC1, TC2):</strong> <code>X &gt; 1</code> 을 True로 고정한 상태에서 Y만 변경한다. Y == 1이 True → False로 바뀌면 전체 조건식도 True → False로 바뀐다. 유효한 케이스이다.</p>
<p>두 과정에서 <strong>TC1이 중복</strong>되므로 하나를 제거하면 최종 테스트 케이스는 <strong>TC1, TC2, TC3 총 3개</strong>로 도출된다. 개별 조건 수 2 + 1 = 3 공식과도 일치한다.</p>
<table>
<thead>
<tr>
<th>TC</th>
<th>X &gt; 1</th>
<th>Y == 1</th>
<th>전체 조건식</th>
</tr>
</thead>
<tbody><tr>
<td>TC1</td>
<td>True</td>
<td>True</td>
<td><strong>True</strong></td>
</tr>
<tr>
<td>TC2</td>
<td>True</td>
<td>False</td>
<td><strong>False</strong></td>
</tr>
<tr>
<td>TC3</td>
<td>False</td>
<td>True</td>
<td><strong>False</strong></td>
</tr>
</tbody></table>
<p>4개에서 3개로 단 1개 줄어든 것이 사소해 보일 수 있다. 그러나 실제 프로그램에서 개별 조건이 3, 4, 5개 혹은 10개 이상으로 늘어날 경우, 전수 테스팅(Exhaustive Testing)의 2ⁿ개와 MC/DC의 N+1개 사이의 차이는 <strong>매우 크게</strong> 벌어진다. 예를 들어 개별 조건이 10개라면 전수 테스팅은 1,024개, MC/DC는 11개로 줄어든다. 이것이 MC/DC의 실질적인 가치이다.</p>
<hr />
<h3>MC/DC 적용 예시; 임의의 프로그램 (1/2)</h3>
<p>앞서 분기/조건 테스팅 (Branch Condition Testing) 에서 사용했던 동일한 프로그램과 제어 흐름도 (Control Flow Graph) 를 그대로 활용한다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/09d0053d-a976-42f6-aad9-48fa5a3db327.png" alt="" style="display:block;margin:0 auto" />

<p>이번 목표는 <strong>MC/DC 커버리지 (Modified Condition/Decision Coverage) 100%</strong> 를 만족하는 최소한의 테스트 케이스를 도출하는 것이다. 이를 위해 만족해야 할 조건은 다음과 같다.</p>
<ul>
<li><p>분기1 (Decision 1) <code>(X &gt; 1) AND (Y == 1)</code> 의 개별 조건 (Individual Condition) 각각이 <strong>독립적 (Independence)</strong> 으로 전체 조건식의 결과에 <strong>유효하게 (Effectiveness)</strong> 영향을 주어야 한다.</p>
</li>
<li><p>분기2 (Decision 2) <code>(X == 2) OR (Z &gt; 1)</code> 의 개별 조건 (Individual Condition) 각각도 동일한 조건을 만족해야 한다.</p>
</li>
<li><p>전체 테스트 케이스 수는 <strong>효율적으로 (Efficiency)</strong> 최소화해야 한다.</p>
</li>
</ul>
<p>구체적인 테스트 케이스 도출은 다음 슬라이드에서 이어진다.</p>
<hr />
<h3>MC/DC 적용 예시; 임의의 프로그램 (2/2)</h3>
<p>MC/DC의 N+1 공식에 따라 분기1 (Decision 1) 과 분기2 (Decision 2) 각각 개별 조건 (Individual Condition) 이 2개씩이므로, 각 분기별로 최소 3개의 테스트 케이스가 필요하다.</p>
<p>도출된 테스트 케이스는 다음과 같다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/14b20788-a098-49ff-abb4-9d207fdf75e7.png" alt="" style="display:block;margin:0 auto" />

<p><strong>분기1의 MC/DC 검증:</strong></p>
<ul>
<li><p>개별 조건1 <code>X &gt; 1</code> 의 독립적 영향 확인 (TC1, TC3): 조건2를 True로 고정한 상태에서 조건1이 True → False로 바뀌면 분기1도 True → False로 바뀐다.</p>
</li>
<li><p>개별 조건2 <code>Y == 1</code> 의 독립적 영향 확인 (TC1, TC2): 조건1을 True로 고정한 상태에서 조건2가 True → False로 바뀌면 분기1도 True → False로 바뀐다.</p>
</li>
</ul>
<p><strong>분기2의 MC/DC 검증:</strong></p>
<ul>
<li><p>개별 조건3 <code>X == 2</code> 의 독립적 영향 확인 (TC1, TC2): 조건4를 True/False 각각으로 보면, TC2에서 조건3이 True이고 조건4가 False일 때 분기2가 True, TC3에서 조건3이 False이고 조건4도 False일 때 분기2가 False로 조건3이 독립적으로 영향을 준다.</p>
</li>
<li><p>개별 조건4 <code>Z &gt; 1</code> 의 독립적 영향 확인 (TC1, TC3): 조건3을 False로 고정한 상태에서 조건4가 True → False로 바뀌면 분기2도 True → False로 바뀐다.</p>
</li>
</ul>
<p>이론적으로는 분기1과 분기2 각각 3개씩 총 6개의 테스트 케이스가 필요하지만, 단 <strong>3개의 테스트 케이스</strong>만으로 분기1과 분기2의 MC/DC 조건을 동시에 만족한다. 이는 매우 효율적 (Efficiency) 으로 설계된 테스트 케이스라 할 수 있다.</p>
<p>결과적으로 <strong>MC/DC 커버리지 = 4/4 = 100%</strong> 가 달성된다.</p>
<p>이로써 분기/조건 테스팅 (Branch Condition Testing) 의 문제점을 개선하기 위해 등장한 <strong>변경조건/결정 테스팅 (MC/DC)</strong> 의 개념과 적용 예시를 모두 살펴보았다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/2752c883-f46d-41e3-a4c9-a287a12e4104.png" alt="" style="display:block;margin:0 auto" />

<h1>3️⃣ Multi-condition testing</h1>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/341daeee-7f25-48c8-9f3b-d0bab51e7183.png" alt="" style="display:block;margin:0 auto" />

<h3>다중조건 테스팅 (Multiple Condition Testing)</h3>
<p><strong>다중조건 테스팅 (Multiple Condition Testing)</strong> 은 분기문 안에 있는 각 개별 조건 (Individual Condition) 의 모든 논리적 조합을 테스트 케이스로 선정하여 테스트하는 기법이다. 즉, 전수 테스팅 (Exhaustive Testing) 과 동일한 방식으로 개별 조건의 True/False 조합을 빠짐없이 모두 테스트한다.</p>
<p>장학생 선정 예제 <code>if ((평균 &gt;= 90) &amp;&amp; (과목수 &gt;= 5))</code> 를 보면, 개별 조건이 2개이므로 가능한 조합은 총 4가지이며 이를 모두 테스트한다.</p>
<table>
<thead>
<tr>
<th>평균 &gt;= 90</th>
<th>과목수 &gt;= 5</th>
<th>전체 조건식</th>
</tr>
</thead>
<tbody><tr>
<td>True</td>
<td>True</td>
<td><strong>True</strong></td>
</tr>
<tr>
<td>True</td>
<td>False</td>
<td><strong>False</strong></td>
</tr>
<tr>
<td>False</td>
<td>True</td>
<td><strong>False</strong></td>
</tr>
<tr>
<td>False</td>
<td>False</td>
<td><strong>False</strong></td>
</tr>
</tbody></table>
<p><strong>다중조건 커버리지 (Multiple Condition Coverage)</strong> 계산 공식은 다음과 같다.</p>
<blockquote>
<p>테스트한 개별 조건의 True/False 조합 수 ÷ 전체 개별 조건의 True/False 조합 수 × 100%</p>
</blockquote>
<p>위 예시에서는 4/4 = <strong>100%</strong> 가 된다.</p>
<p>다중조건 테스팅은 세 가지 기법 중 <strong>커버리지가 가장 높다</strong>는 장점이 있다. 그러나 개별 조건이 늘어날수록 테스트 케이스가 2ⁿ으로 기하급수적으로 증가하기 때문에, 시간·비용·노력이 가장 많이 소요된다는 단점이 있다.</p>
<p>이것이 MC/DC와 같이 커버리지와 효율성 (Efficiency) 을 균형 있게 추구하는 기법이 실무에서 더 널리 사용되는 이유이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/d8265465-7344-41bb-90e3-92240804b5df.png" alt="" style="display:block;margin:0 auto" />

<h3>다중조건 테스팅 (Multiple Condition Testing) 적용 예시; 임의의 프로그램</h3>
<p>앞서 사용했던 동일한 프로그램에 다중조건 테스팅 (Multiple Condition Testing) 을 적용해보자. 목표는 <strong>다중조건 커버리지 (Multiple Condition Coverage) 100%</strong> 를 만족하는 테스트 케이스를 도출하는 것이다.</p>
<p>개별 조건 (Individual Condition) 은 총 4개이다.</p>
<ul>
<li><p>개별 조건1: <code>X &gt; 1</code></p>
</li>
<li><p>개별 조건2: <code>Y == 1</code></p>
</li>
<li><p>개별 조건3: <code>X == 2</code></p>
</li>
<li><p>개별 조건4: <code>Z &gt; 1</code></p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/2f365ea5-cef5-4979-932d-53203d503a52.png" alt="" style="display:block;margin:0 auto" />

<p>4개의 개별 조건에 대해 True/False의 모든 논리적 조합을 테스트해야 하므로 테스트 케이스는 <strong>2⁴ = 16개</strong>가 된다.</p>
<p>예를 들어 <code>TC01</code>은 <code>X=0, Y=0, Z=1</code>을 입력하면 4개의 개별 조건이 모두 False가 되고, <code>TC16</code>은 <code>X=2, Y=1, Z=6</code>을 입력하면 모두 True가 된다.</p>
<p><strong>다중조건 커버리지 = 16/16 = 100%</strong> 가 달성된다. 그러나 이를 위해 16개의 테스트 케이스를 모두 수행해야 하므로 앞서 살펴본 분기/조건 테스팅 (Branch Condition Testing) 의 12개, MC/DC의 3개와 비교했을 때 가장 많은 시간과 비용, 노력이 소요된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/dff73cfa-cffe-4292-9235-a0842b8af98d.png" alt="" style="display:block;margin:0 auto" />

<h3>다중조건 테스팅 (Multiple Condition Testing); 테스트 케이스 수와 커버리지의 관계</h3>
<p>다중조건 테스팅 (Multiple Condition Testing) 은 분기문 안의 각 개별 조건 (Individual Condition) 의 모든 논리적 조합을 테스트 케이스로 선정하는 방법이므로, 커버리지 100% 달성에 필요한 최소 테스트 케이스 수는 <strong>2ⁿ</strong> 이 된다. 여기서 N은 개별 조건의 수이다.</p>
<p>지금까지 학습한 화이트박스 테스팅 (White-box Testing) 의 커버리지 기법들을 커버리지 크기 순으로 정리하면 다음과 같다.</p>
<table>
<thead>
<tr>
<th>기법</th>
<th>커버리지 크기</th>
<th>최소 테스트 케이스 수</th>
</tr>
</thead>
<tbody><tr>
<td>함수 테스팅 (Function Testing)</td>
<td>가장 작음</td>
<td>-</td>
</tr>
<tr>
<td>문장 테스팅 (Statement Testing)</td>
<td>↑</td>
<td>-</td>
</tr>
<tr>
<td>분기/조건 테스팅 (Branch Condition Testing)</td>
<td>↑</td>
<td>-</td>
</tr>
<tr>
<td>변경조건/결정 테스팅 (MC/DC)</td>
<td>↑</td>
<td>N + 1</td>
</tr>
<tr>
<td>다중조건 테스팅 (Multiple Condition Testing)</td>
<td>가장 큼</td>
<td>2ⁿ</td>
</tr>
</tbody></table>
<p>실무에서는 가장 현실적인 균형점으로 <strong>MC/DC</strong> 를 최대 기준으로 활용하는 경우가 많다. 특히 항공, 의료 등 높은 신뢰성이 요구되는 시스템에서는 MC/DC가 표준으로 채택된다. 품질 요구사항이 상대적으로 낮은 시스템에서는 <strong>분기/조건 커버리지 (Branch Condition Coverage)</strong> 수준을 적용하기도 한다. 수업에서는 개별 조건이 2~4개인 작은 예제로 학습했지만, 실무에서는 개별 조건의 수가 훨씬 많아 테스트 케이스의 규모도 그에 비례하여 커진다는 점을 반드시 염두에 두어야 한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/51f6ce66-5462-498d-8685-a5b7b558ac25.png" alt="" style="display:block;margin:0 auto" />

<h3>화이트박스 테스트 설계 기법별 커버리지 비교</h3>
<p>지금까지 학습한 기법들을 동일한 예제 <code>if ((A) AND (B))</code> 를 기준으로 최소 테스트 케이스 수와 커버리지를 비교해보자.</p>
<p><strong>분기 테스팅 (Branch Testing):</strong> 전체 조건식이 True/False를 한 번씩 갖는 케이스를 찾는다. TC3과 TC4를 통해 분기 커버리지 (Branch Coverage) 100% 를 만족한다. 최소 <strong>2개</strong>의 테스트 케이스가 필요하다.</p>
<p><strong>조건 테스팅 (Condition Testing):</strong> 각 개별 조건 (Individual Condition) A와 B가 각각 True/False를 한 번씩 갖는 케이스를 찾는다. TC2와 TC3을 통해 조건 커버리지 (Condition Coverage) 100% 를 만족한다. 최소 <strong>2개</strong>의 테스트 케이스가 필요하다.</p>
<p><strong>분기/조건 테스팅 (Branch Condition Testing):</strong> 개별 조건 A, B 각각의 True/False와 전체 조건식의 True/False를 동시에 만족하는 케이스를 찾는다. TC1과 TC4를 통해 분기/조건 커버리지 (Branch Condition Coverage) 100% 를 만족한다. 최소 <strong>2개</strong>의 테스트 케이스가 필요하다.</p>
<p><strong>변경조건/결정 테스팅 (MC/DC):</strong> 개별 조건 수 N + 1 공식에 따라 TC1, TC2, TC3 총 <strong>3개</strong>의 테스트 케이스로 MC/DC 커버리지 100% 를 만족한다.</p>
<p><strong>다중조건 테스팅 (Multiple Condition Testing):</strong> 모든 논리적 조합을 테스트해야 하므로 TC1, TC2, TC3, TC4 총 <strong>4개 (2²)</strong> 의 테스트 케이스가 필요하며, 이를 통해 다중조건 커버리지 (Multiple Condition Coverage) 100% 를 만족한다.</p>
<p>이를 정리하면 커버리지가 높아질수록 테스트 케이스의 수도 증가하는 것을 확인할 수 있다. 특히 다중조건 테스팅 (Multiple Condition Testing) 은 개별 조건이 늘어날수록 테스트 케이스가 2ⁿ으로 기하급수적으로 증가하기 때문에, 실무에서는 커버리지와 효율성 (Efficiency) 의 균형을 고려한 적절한 기법을 선택하는 것이 중요하다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/67376b1b-8ace-4c68-90ed-7179ef8c45d4.png" alt="" style="display:block;margin:0 auto" />

<h3>기본 경로 테스팅 (Basic Path Testing)</h3>
<p>기본 경로 테스팅 (Basic Path Testing) 은 화이트박스 테스팅 (White-box Testing) 기법 중 하나로, 프로그램의 기본 경로 (Basic Path) 들을 실행할 수 있는 테스트 케이스를 선정하여 테스트하는 방법이다. 이때 기본 경로를 선정하기 위해 <strong>순환 복잡도 (Cyclomatic Complexity, CC)</strong> 를 활용한다. 순환 복잡도는 맥케이브 (McCabe) 라는 학자에 의해 고안된 개념이다.</p>
<p>프로그램을 <strong>흐름 그래프 (Flow Graph)</strong> 형태로 표현하면 다음과 같은 요소들로 구성된다.</p>
<ul>
<li><p><strong>노드 (Node):</strong> 명령문을 나타낸다.</p>
</li>
<li><p><strong>엣지 (Edge):</strong> 노드 간의 흐름을 나타낸다.</p>
</li>
<li><p><strong>단정 노드 (Predicate Node):</strong> 반복문이나 선택문 (if문) 과 같이 분기가 발생하는 노드이다.</p>
</li>
<li><p><strong>영역 (Region):</strong> 엣지와 노드로 닫혀진 영역으로 R1, R2, R3 등으로 표시된다.</p>
</li>
</ul>
<p>슬라이드의 예시를 보면 노드 2, 3이 R1 영역을 형성하며 반복 구조를 이루고 있고, 노드 5에서 선택문에 의해 노드 6 또는 노드 7로 분기되는 구조가 표현되어 있다.</p>
<p>순환 복잡도 (CC) 를 구하는 공식은 세 가지가 있으며, 모두 동일한 결과를 도출한다.</p>
<table>
<thead>
<tr>
<th>공식</th>
<th>계산</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>CC = R (영역 수)</td>
<td>R = 3</td>
<td><strong>3</strong></td>
</tr>
<tr>
<td>CC = E - N + 2 (엣지 수 - 노드 수 + 2)</td>
<td>9 - 8 + 2</td>
<td><strong>3</strong></td>
</tr>
<tr>
<td>CC = P + 1 (단정 노드 수 + 1)</td>
<td>2 + 1</td>
<td><strong>3</strong></td>
</tr>
</tbody></table>
<p>계산 결과 순환 복잡도가 3이므로, <strong>최소 3개 이상의 독립적인 경로 (Independent Path)</strong> 를 테스트하면 프로그램의 기본 경로를 모두 테스트할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/847c4e62-4d52-452a-bbe7-690a7b2a492d.png" alt="" style="display:block;margin:0 auto" />

<h3>기본 경로 테스팅 (Basic Path Testing); 기본 경로와 커버리지</h3>
<p>앞서 순환 복잡도 (Cyclomatic Complexity) CC = 3 을 통해 최소 3개의 독립적인 경로 (Independent Path) 가 필요함을 확인했다. 흐름 그래프 (Flow Graph) 에서 도출되는 기본 경로 (Basic Path) 는 다음 3가지이다.</p>
<ul>
<li><p><strong>경로1:</strong> 1 - 2 - 3 - 2 - 4 - 5 - 6 - 8</p>
</li>
<li><p><strong>경로2:</strong> 1 - 2 - 3 - 2 - 4 - 5 - 7 - 8</p>
</li>
<li><p><strong>경로3:</strong> 1 - 2 - 4 - 5 - 6 - 8</p>
</li>
</ul>
<p><strong>기본 경로 커버리지 (Basic Path Coverage)</strong> 계산 공식은 다음과 같다.</p>
<blockquote>
<p>테스트한 기본 경로 수 ÷ 전체 기본 경로 수 × 100%</p>
</blockquote>
<p>전체 기본 경로가 3개인 상황에서 경로1만 테스트했다면 <strong>1/3 = 약 33.33%</strong> 의 커버리지를 갖게 된다. 3개의 경로를 모두 테스트해야 비로소 <strong>기본 경로 커버리지 100%</strong> 를 달성할 수 있다.</p>
<hr />
]]></content:encoded></item><item><title><![CDATA[Process Synchronization and Deadlock Management]]></title><description><![CDATA[1️⃣Mutexes and Semaphores2️⃣Deadlock3️⃣Practice Implementing Synchronization Techniques

1️⃣Mutexes and Semaphores
동기화 기법과 교착상태
오늘은 운영체제의 핵심 주제 중 하나인 동기화 기법과 교착상태에 대해 학습한다. 여러 프로세스가 동시에 실행되는 환경에서는 자원을]]></description><link>https://heesu.tech/process-synchronization-and-deadlock-management</link><guid isPermaLink="true">https://heesu.tech/process-synchronization-and-deadlock-management</guid><dc:creator><![CDATA[Heesu Noh]]></dc:creator><pubDate>Thu, 30 Apr 2026 13:45:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/4ddfa935-81ea-41d8-abac-09b651e0bc3d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>1️⃣Mutexes and Semaphores<br />2️⃣Deadlock<br />3️⃣Practice Implementing Synchronization Techniques</p>
<hr />
<h1>1️⃣Mutexes and Semaphores</h1>
<h3>동기화 기법과 교착상태</h3>
<p>오늘은 운영체제의 핵심 주제 중 하나인 <strong>동기화 기법</strong>과 <strong>교착상태</strong>에 대해 학습한다. 여러 프로세스가 동시에 실행되는 환경에서는 자원을 안전하게 공유하는 것이 매우 중요하다. 이번 강의에서는 프로세스 간의 실행을 조정하는 동기화 기법의 개념과 함께, 교착상태가 발생하는 원인과 그 해결 방법까지 함께 살펴본다.</p>
<p>이번 강의의 학습 목표는 다음과 같다. 먼저 뮤텍스(Mutex)와 세마포어(Semaphore)의 사용 방법과 두 기법의 차이점을 이해한다. 다음으로 생산자-소비자 문제(Producer-Consumer Problem)를 통해 동기화의 구조적 원리를 파악하고, 교착상태가 발생하는 조건을 이해한다. 마지막으로 뮤텍스와 세마포어를 직접 활용한 동기화 프로그램을 구현해보고, 실행 결과를 통해 실제 동작 방식을 확인해본다.</p>
<hr />
<h3>동기화가 필요한 이유? 헬스장 비유</h3>
<p>동기화 개념을 이해하기 위해 먼저 일상적인 상황을 하나 떠올려보자. 헬스장에 러닝머신이 5대 있다고 가정한다. 회원들은 입장할 때마다 비어 있는 머신을 하나씩 골라 사용하게 된다.</p>
<p>여기서 두 가지 문제 상황을 생각해볼 수 있다. 첫째, 이미 누군가가 사용 중인 러닝머신에 다른 사람이 임의로 끼어들어 함께 사용하려 한다면 어떻게 될까? 당연히 두 사람 모두 제대로 운동을 할 수 없고, 최악의 경우 안전사고로 이어질 수 있다. 둘째, 5대가 모두 사용 중인 상황에서 새로운 회원이 들어왔다면 어떻게 해야 할까? 이 사람은 누군가가 머신을 다 쓰고 자리를 비울 때까지 기다리는 수밖에 없다.</p>
<p>이처럼 여러 사람이 동시에 한정된 자원을 사용하려 할 때, 아무런 규칙 없이 접근을 허용하면 충돌과 혼란이 발생한다. 운영체제에서도 마찬가지로, 여러 프로세스가 동시에 공유 자원에 접근하려 할 때 이를 조율하는 메커니즘이 반드시 필요하다. 이것이 바로 동기화가 필요한 근본적인 이유다.</p>
<hr />
<h3>교착상태란 무엇인가? 프린터와 스캐너 비유</h3>
<p>이번에는 교착상태를 이해하기 위한 또 다른 상황을 살펴보자. 사무실에 프린터 1대와 스캐너 1대가 있다고 가정한다.</p>
<p>직원 A는 현재 프린터를 사용하고 있는 상태에서 스캔 작업도 함께 처리해야 하는 상황이다. 그래서 프린터를 점유한 채로 스캐너가 비워지기를 기다리고 있다. 한편 직원 B는 반대로 스캐너를 사용하고 있는 상태에서 출력 작업도 필요하여, 스캐너를 점유한 채로 프린터가 비워지기를 기다리고 있다.</p>
<p>결과적으로 직원 A는 프린터를 쥔 채 스캐너를 기다리고, 직원 B는 스캐너를 쥔 채 프린터를 기다리는 상황이 된다. 서로가 상대방이 점유한 자원을 기다리고 있기 때문에, 누구도 먼저 자원을 놓아주지 않는 한 두 작업 모두 영원히 진행되지 못한다. 이처럼 <strong>둘 이상의 주체가 서로 상대방의 자원을 기다리며 무한정 멈춰버리는 상태</strong>를 교착상태(Deadlock)라고 한다. 이번 강의에서는 이 교착상태가 왜 발생하는지, 그리고 어떻게 해결할 수 있는지를 중점적으로 다뤄볼 것이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/150f5ecd-1fc5-42f5-ac41-4d33ea53fa77.png" alt="" style="display:block;margin:0 auto" />

<h3>뮤텍스(Mutex)와 임계영역(Critical Section)</h3>
<p>뮤텍스(Mutex)와 임계영역(Critical Section) 앞서 살펴본 동시 접근 문제가 왜 발생하는지, 이제 뮤텍스(Mutex)와 세마포어(Semaphore)를 통해 본격적으로 알아보도록 하자. 여러 실행 흐름이 동시에 접근할 경우 문제가 생길 수 있는 코드 영역을 임계영역(Critical Section)이라고 한다. 이 임계영역을 보호하기 위해 사용하는 동기화 기법이 바로 뮤텍스(Mutex)이다. 뮤텍스는 여러 실행 흐름이 임계영역에 동시에 접근하지 못하도록, 한 번에 하나의 실행 흐름만 진입할 수 있도록 제한하는 역할을 한다. 따라서 여러 스레드(Thread)가 동시에 실행되더라도, 임계영역 안에서는 항상 단 하나의 스레드만 작업을 수행하게 된다. 여기서 중요한 점은, 뮤텍스(Mutex)의 목적이 자원의 개수를 관리하는 것이 아니라 접근 자체를 제어하는 것에 있다는 점이다. 뮤텍스(Mutex)는 잠금(Lock)과 해제(Unlock) 방식으로 동작한다. 잠금(Lock)이 설정되어 있는 동안에는 다른 스레드(Thread)가 해당 임계영역(Critical Section)에 진입할 수 없으며, 잠금(Lock)이 해제(Unlock)되어야만 다음 스레드(Thread)가 임계영역(Critical Section)에 진입할 수 있게 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/6d19b834-4bc1-472c-a10e-f85cd537d991.png" alt="" style="display:block;margin:0 auto" />

<h3>뮤텍스(Mutex)의 활용; 공유 데이터 보호</h3>
<p>뮤텍스(Mutex)는 공유 데이터를 수정하는 구간을 보호할 때 사용된다. 여러 스레드(Thread)가 동일한 데이터를 동시에 변경하려 한다면 <strong>데이터 불일치(Data Inconsistency)</strong> 문제가 발생한다. 예를 들어 전역변수나 인덱스처럼 여러 실행 흐름이 함께 사용하는 데이터에 뮤텍스(Mutex)를 적용하게 된다.</p>
<p>특히 주의해야 할 경우는, 값을 읽고 → 수정하고 → 다시 저장하는 것처럼 하나의 연산이 여러 단계로 이루어지는 경우이다. 이때는 각 단계를 따로 보호하는 것이 아니라, 이 전체 구간을 하나의 묶음으로 보호해야 한다. 만약 이 구간이 보호되지 않으면, 중간에 다른 실행 흐름이 끼어들어 의도하지 않은 결과가 발생할 수 있기 때문이다.</p>
<p>여러 단계로 이루어진 연산이 외부에서 보았을 때 중단 없이 하나의 단위로 실행되어야 하는 성질을 원자성(Atomicity)이라고 하며, 뮤텍스(Mutex)는 이 원자성을 보장하기 위한 대표적인 수단이다. 또한 여러 스레드(Thread)가 동시에 공유 데이터에 접근하면서 발생하는 데이터 불일치(Data Inconsistency) 상황을 경쟁 조건(Race Condition)이라는 용어로도 표현한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/8dce686c-b8c8-49f4-834a-31b57f0e6104.png" alt="" style="display:block;margin:0 auto" />

<h3>뮤텍스(Mutex)의 동작 구조; Lock과 Unlock</h3>
<p>뮤텍스(Mutex)는 Lock과 Unlock을 이용해 임계영역(Critical Section)을 보호하는 방식으로 동작한다.</p>
<p>먼저 Lock을 호출하면 잠금(Lock)을 획득한 경우에만 임계영역(Critical Section)에 접근이 가능하다. 만약 이미 다른 실행 흐름이 잠금(Lock)을 가지고 있는 경우에는 Lock을 호출한 지점에서 대기하게 된다. 임계영역(Critical Section)의 작업이 끝나면 Unlock을 호출하여 잠금을 해제(Unlock)하고, 그다음 실행 흐름이 임계영역(Critical Section)에 진입할 수 있도록 한다. 이처럼 Lock과 Unlock으로 임계영역(Critical Section)을 감싸는 구조를 통해 동시에 하나의 실행 흐름만 접근할 수 있도록 제어하게 된다.</p>
<p>이 구조를 코드로 살펴보면, 반복문 안에서 <code>lock(mutex)</code>를 호출하여 임계영역(Critical Section) 진입을 시도한다. 여기서 뮤텍스(Mutex) 변수란 실제로 잠그고 해제하는 대상이 되는 변수이며, Lock이란 해당 뮤텍스(Mutex)에 대해 잠금을 거는 동작이다. 이미 다른 실행 흐름이 이 뮤텍스(Mutex)를 점유하고 있는 경우에는 Lock을 호출하는 지점에서 대기하게 된다. 이후 <code>shared_data++</code>와 같이 공유 데이터를 수정하는 임계영역(Critical Section) 작업을 수행한 뒤, <code>unlock(mutex)</code>를 호출하여 동일한 뮤텍스(Mutex)에 대한 잠금을 해제(Unlock)하고 다음 실행 흐름이 진입할 수 있도록 한다. 즉 **Lock과 Unlock 사이에 위치한 구간이 임계영역(Critical Section)**이 되며, 이 구조를 통해 공유 데이터 수정 구간을 안전하게 보호하게 된다. Unlock을 호출하지 않고 임계영역(Critical Section)을 빠져나가게 되면 다른 실행 흐름이 영원히 대기 상태에 머무르게 되므로, Lock을 획득한 실행 흐름은 반드시 Unlock을 호출해야 한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/05f18098-8cae-494a-8381-51e5443d496b.png" alt="" style="display:block;margin:0 auto" />

<h3>뮤텍스(Mutex)의 설계 특징</h3>
<p>뮤텍스(Mutex)는 상호 배제(Mutual Exclusion)를 위한 동기화(Synchronization) 기법으로, 여기서 핵심은 <strong>임계영역(Critical Section)의 범위를 어떻게 설정하느냐</strong>이다.</p>
<p>기본적으로 공유 데이터를 실제로 수정하는 구간만 임계영역(Critical Section)으로 설정하면 된다. 임계영역(Critical Section)의 범위를 잘못 설정하게 되면 동작 결과가 달라지기 때문에 이 범위 설정은 매우 중요하다. 임계영역(Critical Section)을 너무 넓게 설정하면 불필요한 구간까지 잠기게 되면서 불필요한 대기와 성능 저하가 발생할 수 있다. 반대로 임계영역(Critical Section)이 너무 좁으면 공유 데이터가 제대로 보호되지 않아 데이터 무결성(Data Integrity)이 깨질 수 있다.</p>
<p>참고로 임계영역(Critical Section)을 넓게 잡을수록 동시에 처리할 수 있는 작업의 양이 줄어드는데, 이를 <strong>병렬성(Parallelism) 저하</strong>라고 표현하기도 한다.</p>
<p>다음으로 중요한 점은 Lock과 Unlock은 반드시 쌍으로 사용해야 한다는 것이다. Lock만 수행하고 Unlock을 하지 않으면 다른 실행 흐름이 임계영역(Critical Section)에 영원히 진입하지 못한 채 계속 대기하게 되는 문제가 발생하기 때문이다. 교착상태(Deadlock)라고 한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/6d322d4a-3c5e-402c-bd3d-816ce936b21c.png" alt="" style="display:block;margin:0 auto" />

<h3>세마포어(Semaphore) 초기값의 의미</h3>
<p>세마포어(Semaphore)의 기본 개념과 값이 의미하는 바를 바탕으로, 이번에는 이 값을 실제로 어떻게 활용하는지 중심으로 살펴본다.</p>
<p>세마포어(Semaphore)는 정수형 변수 S로 표현하며, 이 값은 <strong>동시에 사용할 수 있는 자원의 개수</strong>를 의미한다. 따라서 초기값을 어떻게 설정하느냐에 따라 동작 방식이 달라지므로, 이 점을 이해하는 것이 중요하다.</p>
<p>S = 1인 경우, 하나의 실행 흐름만 자원에 접근할 수 있기 때문에 임계영역(Critical Section) 보호를 위한 이진 세마포어(Binary Semaphore)로 사용된다. S = N인 경우, 동시에 N개의 실행 흐름이 자원을 사용할 수 있기 때문에 여러 개의 자원을 동시에 관리하는 계수 세마포어(Counting Semaphore)로 사용된다. S = 0인 경우, 현재 사용할 수 있는 자원이 없다는 의미이므로 Wait 연산을 수행하는 실행 흐름은 대기 상태로 들어가게 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/d5792e4b-dc0c-4131-a146-f1588d34c100.png" alt="" style="display:block;margin:0 auto" />

<h3>세마포어(Semaphore)의 동작 과정</h3>
<p>여러 프로세스(Process)가 동일한 자원을 동시에 요청하는 상황을 생각해보자. 사용 가능한 자원이 남아 있는 경우, 일부 프로세스(Process)는 자원을 획득하여 바로 실행을 진행하게 된다. 반면 자원이 모두 사용 중인 경우, 나머지 프로세스(Process)들은 자원을 획득하지 못하고 대기 상태로 들어가게 된다.</p>
<p>이때 세마포어(Semaphore) 값 S가 현재 사용 가능한 자원의 개수를 나타내며, 이 S값을 기준으로 실행을 계속할 수 있는 프로세스(Process)와 대기해야 하는 프로세스(Process)가 결정된다. 즉 S값이 0보다 크면 자원을 획득하고 실행을 이어갈 수 있으며, S값이 0이면 사용 가능한 자원이 없다는 의미이므로 해당 프로세스(Process)는 대기 상태로 진입하게 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/28c882fc-5cdd-4e36-a6b1-9c049ed0a0d1.png" alt="" style="display:block;margin:0 auto" />

<h3>이진 세마포어(Binary Semaphore)와 계수 세마포어(Counting Semaphore)의 차이</h3>
<p>이진 세마포어(Binary Semaphore)와 계수 세마포어(Counting Semaphore)는 둘 다 세마포어(Semaphore)이지만, 관리하는 대상과 목적이 서로 다르다.</p>
<p>먼저 이진 세마포어(Binary Semaphore)는 값의 범위가 0 또는 1, 즉 사용 가능하거나 사용 불가능하거나 두 가지 상태만 표현한다. 따라서 한 번에 하나의 실행 흐름만 진입할 수 있도록 제어할 때 적합하다. 임계영역(Critical Section)처럼 동시에 둘 이상이 진입하면 안 되는 구간에서 이진 세마포어(Binary Semaphore)를 사용하여 출입을 하나로 제한할 수 있으며, 이 때문에 뮤텍스(Mutex)와 유사한 상호 배제(Mutual Exclusion) 구조로 이해하면 된다.</p>
<p>반면 계수 세마포어(Counting Semaphore)의 값의 범위는 0 이상의 정수로, 자원이 여러 개 있을 때 현재 몇 개까지 사용 가능한지를 숫자로 관리한다. 예를 들어 동일한 프린터가 3대 있다면 최대 3개의 실행 흐름까지 동시에 자원을 사용할 수 있다는 의미이다. 이 경우 하나의 실행 흐름만 허용하는 것이 아니라, 정해진 개수만큼 동시에 접근할 수 있도록 제어하는 것이 핵심이다.</p>
<p>정리하자면, 이진 세마포어(Binary Semaphore)는 <strong>출입 자체를 하나로 제한하는 것</strong>에 초점이 있고, 계수 세마포어(Counting Semaphore)는 <strong>사용 가능한 자원의 개수를 세면서 관리하는 것</strong>에 초점이 있다. 이름은 비슷하지만 실제 목적과 동작 구조는 분명히 다르다.</p>
<blockquote>
<p><em>앞서 언급했듯 뮤텍스(Mutex)는 잠금을 건 실행 흐름만이 해제(Unlock)할 수 있는 소유권(Ownership) 개념이 존재하는 반면, 이진 세마포어(Binary Semaphore)는 잠금을 걸지 않은 실행 흐름도 Signal 연산을 통해 해제할 수 있다는 점에서 구조적 차이도 있다.</em></p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/6c503a75-a9bb-41a3-966d-1107d04f3c99.png" alt="" style="display:block;margin:0 auto" />

<h3>계수 세마포어(Counting Semaphore)의 동작 구조</h3>
<p>계수 세마포어(Counting Semaphore)는 여러 개의 동일한 자원을 여러 실행 흐름이 나누어 사용하는 상황에서 활용된다. 세마포어(Semaphore) 값 S는 현재 사용 가능한 자원의 개수를 나타내며, 이 값에 따라 실행 흐름이 자원을 획득하거나 대기 상태로 전환하게 된다.</p>
<p>즉 계수 세마포어(Counting Semaphore)는 여러 실행 흐름이 자원을 경쟁하는 상황에서, 자원의 사용을 <strong>순서대로 처리할 수 있도록 제어하는 구조</strong>라고 정리할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/6c40d2d9-a7fc-4c33-bb75-70c587ace163.png" alt="" style="display:block;margin:0 auto" />

<h3>세마포어(Semaphore)를 이용한 실행 순서 제어</h3>
<p>지금까지는 여러 실행 흐름이 동시에 자원에 접근하는 것을 막는 것에 초점을 두었다면, 이번에는 <strong>어떤 실행 흐름이 먼저 실행되어야 하는지를 제어하는 것</strong>이 핵심이다.</p>
<p>예를 들어 프로세스1(Process1)이 먼저 작업을 수행한 뒤 Signal 연산을 호출하면, 프로세스2(Process2)는 Wait 연산에서 대기하다가 Signal이 발생하는 순간 실행을 이어가게 된다. 즉 프로세스2(Process2)는 임의로 실행되는 것이 아니라, 프로세스1(Process1)의 작업이 끝난 이후에 실행되도록 순서가 연결되어 있는 것이다. 이처럼 세마포어(Semaphore)는 Wait와 Signal 연산을 이용하여 프로세스(Process) 간의 실행 순서를 명확하게 제어할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/c78a16c0-b581-4fd7-8cfe-3800c7dab60b.png" alt="" style="display:block;margin:0 auto" />

<h3>조건 동기화(Condition Synchronization)</h3>
<p>지금까지 세마포어(Semaphore)를 이용한 실행 순서 제어를 살펴보았다면, 이번에는 실행 순서가 아닌 <strong>특정 조건이 만족되는 시점에서 실행을 제어하는 방식</strong>, 즉 조건 동기화(Condition Synchronization)를 살펴본다.</p>
<p>조건 동기화(Condition Synchronization)는 실행 흐름이 어떤 작업을 수행하기 전에 현재 자원의 상태나 조건이 만족되는지를 먼저 확인한다. 조건이 만족된 경우에만 실행을 진행하고, 충족되지 않는 경우에는 대기 상태로 들어가게 된다. 이후 상태가 변화하여 조건이 만족되면 그때 비로소 실행이 이어지게 된다.</p>
<p>여기서 중요한 점은, 조건 동기화(Condition Synchronization)란 자원을 보호하는 것이 아니라 <strong>언제 실행할 것인가를 제어하는 것</strong>에 목적이 있다는 점이다. 이는 상호 배제(Mutual Exclusion)와는 별도의 개념으로, 보호가 아닌 <strong>실행 시점 제어</strong>로 이해해야 한다.</p>
<p>예를 들어 버퍼(Buffer)가 비어 있는 경우, 소비자(Consumer)는 바로 실행하지 못하고 대기 상태에 머무른다. 버퍼(Buffer)에 데이터가 채워졌을 때에만 실행이 가능해지는 것, 이것이 바로 조건 동기화(Condition Synchronization)이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/bb748f8b-8a99-483e-a3be-be8f23ab25a6.png" alt="" style="display:block;margin:0 auto" />

<h3>상태 기반 제어 필요성</h3>
<p>그렇다면 실제로 어떤 상황에서 조건 동기화(Condition Synchronization)가 필요한지 살펴보자.</p>
<p>먼저 자원이 가득 찬 상태에서는 더 이상 데이터를 추가할 수 없다. 반대로 자원이 비어 있는 상태에서는 데이터를 꺼낼 수가 없다. 즉 어떤 작업을 수행할 수 있는지는 <strong>현재 자원의 상태에 따라 결정</strong>되며, 실행을 무조건 진행하는 것이 아니라 조건이 만족될 때만 수행하도록 제어해야 한다. 이처럼 자원의 상태를 기준으로 실행 여부를 결정하는 구조가 필요하게 된다. 만약 이러한 제어가 없다면 어떤 문제가 발생하는지는 이후에 구체적으로 살펴볼 것이다.</p>
<blockquote>
<p><strong>📌</strong> 이 상황은 앞서 언급한 생산자-소비자 문제(Producer-Consumer Problem)와 직접적으로 연결된다. 자원이 가득 찬 상태에서 데이터를 추가하려는 생산자(Producer)와, 자원이 비어 있는 상태에서 데이터를 꺼내려는 소비자(Consumer) 모두 조건이 만족될 때까지 대기해야 하며, 이 제어가 없을 경우 데이터 손실이나 잘못된 메모리 접근과 같은 심각한 문제로 이어질 수 있다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/8049c82e-4c1f-4f82-945b-3d63876d61f1.png" alt="" style="display:block;margin:0 auto" />

<h3>바쁜 대기(Busy Waiting)의 문제점</h3>
<p>바쁜 대기(Busy Waiting)의 문제점 이 예제는 생산자(Producer)와 소비자(Consumer)가 자원의 상태를 직접 확인하며 동작하는 구조이다. 코드를 살펴보면, 생산자(Producer) 부분에서는 while (counter == BUFFER_SIZE); 구문을 통해 버퍼(Buffer)가 가득 찬 상태인지를 계속 확인하고 있다. 소비자(Consumer) 부분에서는 while (counter == 0); 구문을 통해 버퍼(Buffer)가 비어 있는지를 계속 반복해서 확인하고 있다. 이처럼 조건이 만족될 때까지 아무런 작업도 하지 않고 계속 반복하며 상태를 확인하는 구조를 **바쁜 대기(Busy Waiting)**라고 한다. 이 방식은 조건이 충족되지 않는 동안에도 CPU가 쉬지 않고 계속 반복문을 실행하기 때문에, CPU 시간을 낭비하는 비효율적인 동작 방식이다.</p>
<blockquote>
<p>📌바쁜 대기(Busy Waiting)는 스핀락(Spinlock)이라고도 불리며, 대기 시간이 매우 짧을 것으로 예상되는 경우에는 문맥 교환(Context Switching) 비용을 피할 수 있어 오히려 유리한 상황도 존재한다. 그러나 일반적인 경우에는 세마포어(Semaphore)나 조건 변수(Condition Variable)를 활용하여 대기 상태의 실행 흐름을 슬립(Sleep) 상태로 전환함으로써 CPU 자원을 보다 효율적으로 사용하는 것이 권장된다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a86899fd-b202-48d0-8227-8701c000983a.png" alt="" style="display:block;margin:0 auto" />

<h3>생산자-소비자 문제(Producer-Consumer Problem)의 구조</h3>
<p>생산자-소비자 문제(Producer-Consumer Problem)는 하나의 공유 버퍼(Buffer)를 중심으로 이루어진다. 이 버퍼(Buffer)의 크기는 제한되어 있기 때문에 무한정 데이터를 넣거나 꺼낼 수 없다.</p>
<p>생산자(Producer)와 소비자(Consumer)는 이 버퍼(Buffer)에 동시에 접근하려 하는데, 이때 버퍼(Buffer)의 상태에 따라 가능한 작업이 달라지게 된다. 버퍼(Buffer)가 가득 찬 상태에서는 생산자(Producer)가 더 이상 데이터를 넣을 수 없고, 버퍼(Buffer)가 비어 있는 상태에서는 소비자(Consumer)가 데이터를 꺼낼 수 없다.</p>
<p>즉 이 문제의 핵심은 단순한 자원 접근이 아니라, <strong>현재 버퍼(Buffer)의 상태에 따라 실행 여부가 결정되는 구조</strong>라는 점이다.</p>
<blockquote>
<p><strong>📌</strong> 생산자-소비자 문제(Producer-Consumer Problem)에서는 버퍼(Buffer)의 상태 제어 외에도 생산자(Producer)와 소비자(Consumer)가 동시에 버퍼(Buffer)에 접근할 때 발생하는 <strong>경쟁 조건(Race Condition)</strong> 역시 함께 해결해야 한다. 따라서 실제 구현에서는 조건 동기화(Condition Synchronization)와 함께 뮤텍스(Mutex) 또는 이진 세마포어(Binary Semaphore)를 통한 상호 배제(Mutual Exclusion)도 동시에 적용해야 한다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/5b2ffb99-3dd3-4c94-bd69-016d12b742a8.png" alt="" style="display:block;margin:0 auto" />

<h3>원형 버퍼(Circular Buffer)와 생산자-소비자 구조</h3>
<p>그림을 보면 하나의 원형 버퍼(Circular Buffer)를 기준으로 생산자(Producer)와 소비자(Consumer)가 동시에 접근하고 있다. 생산자(Producer)는 데이터를 버퍼(Buffer)에 삽입하고, 소비자(Consumer)는 데이터를 버퍼(Buffer)에서 꺼내게 된다. 이때 삽입 위치와 제거 위치를 각각 나타내는 포인터(Pointer)가 원형 구조를 따라 이동하게 된다.</p>
<p>여기서 중요한 점은, 이 두 작업이 독립적으로 보이지만 실제로는 버퍼(Buffer)라는 하나의 자원을 공유하고 있다는 것이다. 따라서 버퍼(Buffer)가 가득 찬 경우에는 생산자(Producer)의 작업이 멈추어야 하고, 버퍼(Buffer)가 비어 있는 경우에는 소비자(Consumer)의 작업이 멈추어야 한다. 결국 이 구조에서는 <strong>동시 접근 제어</strong>와 <strong>상태 기반 제어(State-based Control)</strong> 가 모두 필요하다는 점이 문제의 핵심이다.</p>
<blockquote>
<p><strong>📌</strong> 원형 버퍼(Circular Buffer)에서 삽입 위치를 나타내는 포인터를 <strong>쓰기 포인터(Write Pointer)</strong> 또는 <strong>in 포인터</strong>, 제거 위치를 나타내는 포인터를 <strong>제거 포인터</strong> 또는 <strong>out 포인터</strong>라고 부른다. 두 포인터가 같은 위치를 가리키는 경우 버퍼(Buffer)가 비어 있음을 의미하며, 쓰기 포인터(Write Pointer)가 읽기 포인터(Read Pointer)보다 한 칸 뒤에 위치하는 경우 버퍼(Buffer)가 가득 찬 상태를 의미한다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/e99a26df-25b2-4577-8c9f-8a32c4ed8342.png" alt="" style="display:block;margin:0 auto" />

<h3>상호 배제(Mutual Exclusion)가 없는 경우의 문제점</h3>
<p>조건이 지켜지지 않으면 다음과 같은 문제들이 발생하게 된다.</p>
<p>먼저 생산자(Producer)와 소비자(Consumer)가 동시에 실행되면서 in/out 인덱스(Index)가 서로 충돌할 수 있다. 또한 카운터(Counter) 값을 동시에 수정하게 되면 증가와 감소 과정이 엇갈리면서 잘못된 값이 저장될 수 있다. 더 나아가 동일한 위치에 데이터가 덮어쓰기(Overwrite)되거나, 아직 처리되지 않은 데이터가 사라지는 문제도 발생할 수 있다.</p>
<p>이러한 문제들의 근본적인 원인은 <strong>하나의 연산이 여러 단계로 나누어 실행되는 동안 다른 실행 흐름이 끼어들었기 때문</strong>이다. 이 실행 단계들이 서로 교차되면서 의도하지 않은 결과가 발생하게 된다.</p>
<blockquote>
<p><strong>📌</strong> 여기서 설명하는 상황이 앞서 언급한 **경쟁 조건(Race Condition)**의 전형적인 사례이다. 특히 카운터(Counter) 값의 동시 수정 문제는 <code>counter++</code>와 같이 단순해 보이는 연산도 내부적으로는 읽기(Read) → 수정(Modify) → 쓰기(Write)의 세 단계로 이루어지기 때문에 발생한다. 이 세 단계가 원자적(Atomic)으로 처리되지 않으면, 그 사이에 다른 실행 흐름이 끼어들어 데이터 불일치(Data Inconsistency)가 발생할 수 있다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/188fd49d-b0eb-4ee4-9bb6-9ae060541943.png" alt="" style="display:block;margin:0 auto" />

<h3>상호 배제(Mutual Exclusion)가 없는 경우; 동기화의 필요성</h3>
<p>지난 시간에 살펴보았듯이, <code>counter++</code>나 <code>counter--</code> 연산은 하나의 명령어처럼 보이지만 실제로는 읽기(Read) → 수정(Modify) → 쓰기(Write)의 여러 단계로 나뉘어 실행된다. 그렇기 때문에 실행 흐름이 서로 섞이게 되면 값의 오류가 발생할 수 있다. 이 문제를 해결하기 위해 필요한 것이 바로 **동기화(Synchronization)**이다.</p>
<hr />
<blockquote>
<p><strong>📌</strong> <code>counter++</code>와 같은 연산이 여러 단계로 나뉘어 실행됨에도 하나의 명령어처럼 보이는 이유는 고수준 언어(High-level Language)에서 단일 구문으로 표현되기 때문이다. 그러나 실제 CPU가 처리하는 저수준(Low-level) 명령어 단계에서는 여러 단계로 분리되며, 이 사이에 다른 실행 흐름이 끼어들 수 있다. 이를 해결하는 동기화(Synchronization) 기법으로는 앞서 살펴본 뮤텍스(Mutex)와 세마포어(Semaphore)가 대표적이다. 여기서 저수준(Low-level)은 <strong>C, 어셈블리 같은 언어를 지칭하는 것이 아니라</strong>, CPU가 내부적으로 명령어를 처리하는 방식을 설명하는 표현이니 헷갈리지 말자.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/6eff8d5a-75d7-4f8a-9a65-f6463ddb3997.png" alt="" style="display:block;margin:0 auto" />

<h3>정상 설계 구조; 동기화 설계의 핵심</h3>
<p>그렇다면 동기화를 어떻게 구성해야 할까? 생산자-소비자 문제(Producer-Consumer Problem)에서는 공유 자원에 대한 접근 제어와 실행 조건 제어를 함께 다룰 수 있도록 역할을 나누어 설계해야 한다. 여기서는 다음 세 가지를 분리하여 사용하게 된다.</p>
<p>첫째, 뮤텍스(Mutex)는 버퍼(Buffer)에 대한 접근을 제어하여 동시에 하나의 실행 흐름만 접근할 수 있도록 한다. 둘째, <code>empty</code>는 비어 있는 버퍼(Buffer)의 개수를 나타내며 데이터를 삽입할 수 있는 조건을 제어한다. 셋째, <code>full</code>은 채워진 버퍼(Buffer)의 개수를 나타내며 데이터를 꺼낼 수 있는 조건을 제어한다.</p>
<p>즉 뮤텍스(Mutex)는 동시 접근을 막는 역할을 담당하고, <code>empty</code>와 <code>full</code>은 조건이 맞을 때만 실행되도록 하는 역할을 담당한다. 이처럼 <strong>상호 배제(Mutual Exclusion)와 조건 동기화(Condition Synchronization)를 분리하여 설계하는 것</strong>이 생산자-소비자 문제(Producer-Consumer Problem)에서 동기화 설계의 핵심이다.</p>
<hr />
<blockquote>
<p><strong>📌</strong> <code>empty</code>와 <code>full</code>은 계수 세마포어(Counting Semaphore)로 구현되며, 일반적으로 버퍼(Buffer)의 크기가 N일 때 <code>empty</code>의 초기값은 N, <code>full</code>의 초기값은 0으로 설정한다. 생산자(Producer)가 데이터를 삽입할 때마다 <code>empty</code>는 감소하고 <code>full</code>은 증가하며, 소비자(Consumer)가 데이터를 꺼낼 때마다 그 반대로 동작한다.</p>
</blockquote>
<hr />
<h1>2️⃣ 교착상태(Deadlock)</h1>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/8f80d934-54a5-4134-a431-a4d99f56cb9c.png" alt="" style="display:block;margin:0 auto" />

<h3>교착상태(Deadlock)와 다중 자원 환경(Multi-resource Environment)</h3>
<p>이번에는 여러 자원을 동시에 사용하는 환경에서 실행 흐름이 서로 얽히는 문제, 즉 **교착상태(Deadlock)**에 대해 살펴본다.</p>
<p>운영체제(Operating System)에서는 하나의 자원만 사용하는 것이 아니라 CPU, 메모리(Memory), 파일(File), 입출력 장치(I/O Device) 등 여러 자원이 함께 사용된다. 하나의 프로세스(Process)도 작업을 수행하는 동안 이 자원들을 한 번에 모두 사용하는 것이 아니라, 필요한 시점마다 순차적으로 요청하게 된다.</p>
<p>문제는 여러 프로세스(Process)가 동시에 실행될 때 발생한다. 각 프로세스(Process)가 비슷한 자원을 필요로 하다 보니, 결국 동일한 자원을 두고 서로 경쟁하는 상황이 발생하게 된다. 즉 시스템은 단순히 자원을 사용하는 구조가 아니라, 여러 자원이 서로 얽혀 있고 그 안에서 프로세스(Process)들이 경쟁하는 복잡한 구조를 가지게 된다.</p>
<blockquote>
<p>**📌**프로세스(Process)가 자원을 사용하는 일반적인 순서는 <strong>요청(Request) → 사용(Use) → 해제(Release)</strong> 의 세 단계로 이루어진다. 교착상태(Deadlock)는 이 과정에서 여러 프로세스(Process)가 서로 상대방이 점유한 자원의 해제(Release)를 기다리며 무한정 대기하는 상황을 의미한다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/9a003fe9-1a4f-453a-b5af-19c7c32a8225.png" alt="" style="display:block;margin:0 auto" />

<h3>자원 요청 순서와 교착상태(Deadlock)의 발생 구조</h3>
<p>프로세스(Process)마다 자원을 요청하는 순서는 서로 다를 수 있다. 예를 들어 어떤 프로세스(Process)는 먼저 CPU를 사용한 뒤 파일(File)을 요청하고, 다른 프로세스(Process)는 파일(File)을 먼저 사용한 뒤 CPU를 요청할 수도 있다.</p>
<p>이처럼 요청 순서가 다르면 이미 하나의 자원을 점유한 상태에서 다른 자원을 기다리는 상황이 발생할 수 있다. 문제는 이런 상황이 여러 프로세스(Process)에서 동시에 일어난다면, 자원에 대한 대기 상태가 서로 얽히게 된다는 점이다. 즉 단순한 자원 경쟁을 넘어서 <strong>프로세스(Process)들이 서로를 기다리는 구조</strong>, 다시 말해 교착상태(Deadlock)가 만들어질 수 있다.</p>
<blockquote>
<p>**📌**이처럼 프로세스(Process)들이 서로를 기다리는 순환 구조를 **순환 대기(Circular Wait)**라고 하며, 이는 교착상태(Deadlock)가 발생하기 위한 네 가지 필요 조건 중 하나이다. 나머지 세 가지 조건인 상호 배제(Mutual Exclusion), 점유와 대기(Hold and Wait), 비선점(Non-preemption)과 함께 모두 충족될 때 교착상태(Deadlock)가 발생하게 된다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/318a00db-905b-460b-aaf9-66be8f7de6ab.png" alt="" style="display:block;margin:0 auto" />

<h3>교착상태(Deadlock)의 발생 구조; 프로세스 간 자원 점유와 대기</h3>
<p>그림을 통해 교착상태(Deadlock)의 발생 흐름을 구체적으로 살펴보자.</p>
<p>프로세스A(Process A)는 스캐너(Scanner)를 할당받은 상태에서 CD 레코더(CD Recorder)를 추가로 요청하고 있다. 반대로 프로세스B(Process B)는 CD 레코더(CD Recorder)를 먼저 점유한 상태에서 스캐너(Scanner)를 요청하고 있다. 즉 각각 하나의 자원을 이미 점유한 상태에서 서로가 가지고 있는 다른 자원을 기다리고 있는 구조가 된다.</p>
<p>이처럼 자원을 하나씩 나누어 가진 상태에서 추가 자원을 요청하게 되면 프로세스(Process) 간의 대기 관계가 서로 얽히게 된다. 결국 이런 구조가 만들어지면 각 프로세스(Process)는 상대방이 자원을 놓아줄 때까지 기다리게 되고, <strong>전체 실행이 멈추는 교착상태(Deadlock)</strong> 로 이어지게 된다.</p>
<blockquote>
<p><strong>📌</strong> 이 상황은 교착상태(Deadlock)의 네 가지 필요 조건 중 **점유와 대기(Hold and Wait)**와 **순환 대기(Circular Wait)**가 동시에 나타나는 전형적인 사례이다. 어느 한 프로세스(Process)도 먼저 자원을 자발적으로 놓아주지 않는 한 교착상태(Deadlock)는 스스로 해소되지 않게된다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/54460ede-efd8-4da5-bdcf-0cf54012d518.png" alt="" style="display:block;margin:0 auto" />

<h3>교착상태(Deadlock)의 정의</h3>
<p>여러 프로세스(Process)가 서로의 자원을 기다리면서 더 이상 실행을 진행하지 못하는 상태를 교착상태(Deadlock)라고 한다. 각 프로세스(Process)는 이미 하나의 자원을 확보한 상태에서 추가 자원을 요청하는데, 그 자원이 다른 프로세스(Process)에 의해 점유되어 있으면 자신이 가지고 있는 자원을 놓지 못한 채 대기하게 된다. 결국 서로가 상대방의 자원 반납을 기다리는 순환적인 대기 구조가 만들어지게 되고, 이 상태에서는 누구도 다음 단계로 진행할 수 없는 완전한 교착상태(Deadlock)에 빠지게 된다.</p>
<blockquote>
<p>📌 교착상태(Deadlock)가 발생하기 위해서는 다음 네 가지 조건이 동시에 충족되어야 한다. 첫째 상호 배제(Mutual Exclusion), 둘째 점유와 대기(Hold and Wait), 셋째 비선점(Non-preemption), 넷째 순환 대기(Circular Wait)이다. 이 중 하나라도 충족되지 않으면 교착상태(Deadlock)는 발생하지 않으며, 이 점이 교착상태(Deadlock) 예방 전략의 핵심 원리가 된다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/8dc56ce3-db00-488a-aab9-8948210b888b.png" alt="" style="display:block;margin:0 auto" />

<h3>교착상태(Deadlock)의 전형적인 예제; Wait와 Signal 구조</h3>
<p>다음 예제를 통해 교착상태(Deadlock)가 실제로 어떻게 발생하는지 살펴보자.</p>
<p>프로세스P1(Process P1)은 먼저 <code>wait(S)</code>를 통해 자원 S를 확보한 뒤, 추가로 자원 Q를 요청한다. 프로세스P2(Process P2)는 반대로 <code>wait(Q)</code>를 통해 자원 Q를 먼저 확보한 뒤, 자원 S를 추가로 요청한다.</p>
<p>문제는 이 두 프로세스(Process)가 동시에 실행될 때 발생한다. 프로세스P1(Process P1)은 S를 가진 채 Q를 기다리고, 프로세스P2(Process P2)는 Q를 가진 채 S를 기다리는 상황이 만들어진다. 이 경우 프로세스P1(Process P1)은 Q가 없어 더 이상 진행할 수 없고, 프로세스P2(Process P2)도 S가 없어 더 이상 진행할 수 없는 상태가 된다.</p>
<p>더 나아가 <code>signal(Q)</code>와 <code>signal(S)</code>와 같은 자원 반납 코드는 자신의 작업이 완료된 이후에야 실행될 수 있는데, 현재 두 프로세스(Process) 모두 Wait 상태에서 멈춰 있기 때문에 반납 코드까지 도달하지 못한다. 결국 자원은 반납되지 않고 두 프로세스(Process)는 서로를 기다리기만 하게 되는데, 이 구조가 바로 <strong>교착상태(Deadlock)의 전형적인 사례</strong>이다.</p>
<blockquote>
<p>📌이 예제에서는 교착상태(Deadlock)의 네 가지 필요 조건이 모두 충족되어 있다. 자원 S와 Q는 동시에 둘 이상이 사용할 수 없으므로 <strong>상호 배제(Mutual Exclusion)</strong>, 각 프로세스(Process)가 자원을 점유한 채 추가 자원을 기다리므로 <strong>점유와 대기(Hold and Wait)</strong>, 점유된 자원을 강제로 빼앗을 수 없으므로 <strong>비선점(Non-preemption)</strong>, 그리고 P1→Q→P2→S→P1으로 이어지는 **순환 대기(Circular Wait)**가 모두 성립한다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/293c91c7-9dae-4a14-bf97-d1aac2de4a70.png" alt="" style="display:block;margin:0 auto" />

<h3>교착상태(Deadlock)의 발생 조건</h3>
<p>교착상태(Deadlock)는 아무 상황에서나 발생하는 것이 아니다. 특정 조건들이 동시에 만족될 때만 발생하며, 운영체제(Operating System)에서는 이 조건을 다음 네 가지로 정리한다. 상호 배제(Mutual Exclusion), 점유와 대기(Hold and Wait), 비선점(Non-preemption), 순환 대기(Circular Wait)이다. 여기서 중요한 점은, 이 네 가지 중 일부만 성립한다고 해서 곧바로 교착상태(Deadlock)가 발생하는 것은 아니라는 것이다. 반드시 네 가지 조건이 동시에 모두 성립해야만 프로세스(Process)들이 서로 자원을 기다리며 실행이 멈추는 교착상태(Deadlock)가 만들어질 수 있다. 즉 이 네 가지 조건은 교착상태(Deadlock)가 발생하는 구조를 분석하는 기준으로 이해하면 된다.</p>
<blockquote>
<p>📌이 네 가지 조건은 1971년 E. W. Dijkstra가 아닌 E. G. Coffman이 정리한 것으로, 코프만 조건(Coffman Conditions)이라고도 부른다. 보완하자면, 이 네 가지 조건은 교착상태(Deadlock) 분석의 기준이 될 뿐만 아니라 교착상태 예방(Deadlock Prevention) 전략의 핵심 근거가 되기도 한다. 네 가지 조건 중 하나라도 성립하지 않도록 설계하면 교착상태(Deadlock) 자체를 원천적으로 막을 수 있기 때문이다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/48cc9f4b-71b4-497d-9dd6-6363ffb66058.png" alt="" style="display:block;margin:0 auto" />

<h3>교착상태(Deadlock)의 형성 흐름</h3>
<p>교착상태(Deadlock)는 한순간에 갑자기 발생하는 것이 아니라 일정한 흐름을 거쳐 형성된다. 먼저 여러 프로세스(Process) 간의 <strong>자원 경쟁</strong>이 발생하고, 이후 자원을 점유한 채 추가 자원을 기다리는 상황이 만들어진다. 그 대기 관계가 **순환 구조(Circular Wait)**로 이어질 때, 최종적으로 교착상태(Deadlock)가 발생하게 된다. 각 단계의 구체적인 의미는 이어서 자세히 살펴볼 것이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/8c794b3c-48f0-42d5-a683-f0e2b027355f.png" alt="" style="display:block;margin:0 auto" />

<h3>교착상태(Deadlock) 발생 조건 1; 상호 배제(Mutual Exclusion)</h3>
<p>첫 번째 조건은 **상호 배제(Mutual Exclusion)**이다. 이는 하나의 자원을 동시에 하나의 프로세스(Process)에서만 사용할 수 있는 경우를 의미한다. 즉 어떤 프로세스(Process)가 자원을 사용하고 있다면, 다른 프로세스(Process)는 그 자원이 해제될 때까지 기다려야 한다.</p>
<p>여기서 중요한 점은 모든 자원이 이런 성질을 가지는 것은 아니라는 것이다. 예를 들어 읽기 전용(Read-only) 파일처럼 여러 프로세스(Process)가 함께 사용할 수 있는 자원도 있다. 반면 프린터(Printer), 스캐너(Scanner), 테이프 드라이브(Tape Drive)처럼 한 번에 하나의 프로세스(Process)만 사용해야 하는 장치는 대표적인 상호 배제(Mutual Exclusion) 자원이다.</p>
<p>이런 자원이 존재하면 프로세스(Process)들은 자원을 두고 기다릴 수밖에 없으며, 그 기다림이 교착상태(Deadlock)의 출발점이 된다. 즉 상호 배제(Mutual Exclusion)는 단순히 혼자만 사용한다는 의미가 아니라, <strong>다른 프로세스(Process)를 대기 상태로 만드는 구조를 형성한다</strong>는 점에서 교착상태(Deadlock)의 첫 번째 필요 조건이 된다.</p>
<hr />
<blockquote>
<p>**📌**상호 배제(Mutual Exclusion) 조건은 자원의 특성상 제거하기 어려운 경우가 많다. 프린터(Printer)나 스캐너(Scanner)와 같은 장치는 본질적으로 동시 사용이 불가능하기 때문에, 이 조건을 없애는 방식으로 교착상태(Deadlock)를 예방하는 것은 현실적으로 쉽지 않다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/62b60cbe-085a-4071-bc38-f5443e645875.png" alt="" style="display:block;margin:0 auto" />

<h3>교착상태(Deadlock) 발생 조건 2; 점유와 대기(Hold and Wait)</h3>
<p>두 번째 조건은 **점유와 대기(Hold and Wait)**이다. 이는 프로세스(Process)가 하나 이상의 자원을 확보한 상태에서 추가 자원을 요청하는 상황을 의미한다. 즉 이미 자원을 가지고 있으면서 그 자원을 내려놓지 않은 채 다른 자원을 계속 요청하는 구조이다.</p>
<p>문제는 요청한 자원이 즉시 할당되지 않으면 현재 점유하고 있는 자원을 그대로 유지한 채 대기 상태에 머무르게 된다는 점이다. 이 상태가 여러 프로세스(Process)에서 동시에 발생하게 된다면, 각자가 자원을 쥔 채 서로가 가진 자원을 기다리는 구조가 만들어진다. 이처럼 <strong>자원을 점유한 채 추가 자원을 기다리는 구조</strong>가 교착상태(Deadlock)의 두 번째 필요 조건이 된다.</p>
<blockquote>
<p><strong>📌</strong> 점유와 대기(Hold and Wait) 조건을 예방하는 대표적인 방법으로는 두 가지가 있다. 첫째는 프로세스(Process)가 실행 전에 필요한 모든 자원을 한꺼번에 요청하는 방식이고, 둘째는 추가 자원이 필요할 경우 현재 점유 중인 자원을 모두 반납한 뒤 다시 요청하는 방식이다. 다만 두 방법 모두 자원 활용의 효율성이 떨어질 수 있다는 단점이 있다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/878bb212-89f3-467b-b6d4-22036eab53b0.png" alt="" style="display:block;margin:0 auto" />

<h3>교착상태(Deadlock) 발생 조건 3; 비선점(Non-preemption)</h3>
<p>세 번째 조건은 **비선점(Non-preemption)**이다. 이는 이미 할당된 자원을 외부에서 강제로 빼앗을 수 없다는 의미로, 운영체제(Operating System)는 자원을 사용 중인 프로세스(Process)로부터 임의로 자원을 회수할 수 없다. 따라서 자원을 사용하고 있는 프로세스(Process)가 작업을 끝내고 스스로 반납해야만 다른 프로세스(Process)가 그 자원을 사용할 수 있다.</p>
<p>문제는 해당 프로세스(Process)가 다른 자원을 기다리고 있는 상황에서는 자원을 쥔 채 계속 멈춰 있게 된다는 점이다. 예를 들어 프린터(Printer)를 사용 중인 작업은 중간에 빼앗을 수 없기 때문에 작업이 끝날 때까지 다른 프로세스(Process)들은 기다려야 한다. 이처럼 <strong>자원을 강제로 회수할 수 없다는 점</strong>이 대기 상태를 해소하지 못하게 만드는 조건이 된다.</p>
<blockquote>
<p><strong>📌</strong> 비선점(Non-preemption) 조건을 제거하는 방법으로는 프로세스(Process)가 새로운 자원을 요청할 때 즉시 할당받을 수 없다면 현재 점유 중인 모든 자원을 강제로 반납하게 하는 방식이 있다. 다만 이 방식은 프린터(Printer)처럼 작업이 중단되면 처음부터 다시 시작해야 하는 장치에는 적용하기 어렵다는 현실적인 한계가 있다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/23666fe2-9176-4a2a-8ce9-b4b6c638fc56.png" alt="" style="display:block;margin:0 auto" />

<h3>교착상태(Deadlock) 발생 조건 4; 순환 대기(Circular Wait)</h3>
<p>네 번째 조건은 **순환 대기(Circular Wait)**이다. 이는 프로세스(Process)들이 서로가 점유한 자원을 기다리는 관계가 원형 구조로 연결된 상태를 의미한다. 즉 한 프로세스(Process)는 다른 프로세스(Process)가 가진 자원을 기다리고, 그 프로세스(Process)는 또 다른 프로세스(Process)의 자원을 기다리는 식으로 이 관계가 끊어지지 않고 계속 이어지는 구조이다.</p>
<p>이때 이 순환 구조에서 한 곳이라도 끊기면 문제가 해결되지만, 원형으로 완전히 연결되어 있다면 누구도 진행할 수 없는 상태가 된다. 결국 이러한 순환 구조가 형성되는 순간 교착상태(Deadlock)가 만들어질 수 있다.</p>
<blockquote>
<p><strong>📌</strong> 순환 대기(Circular Wait) 조건을 예방하는 대표적인 방법으로는 <strong>자원에 순서를 부여하는 방식</strong>이 있다. 모든 프로세스(Process)가 자원을 반드시 정해진 순서대로만 요청하도록 강제하면 순환 구조 자체가 형성될 수 없기 때문이다. 이는 앞서 살펴본 네 가지 조건 중 현실적으로 가장 제어하기 쉬운 조건으로 알려져 있다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f8d02e41-a69e-4d04-b072-e798166f741f.png" alt="" style="display:block;margin:0 auto" />

<h3>순환 대기(Circular Wait)의 그래프 표현</h3>
<p>앞서 살펴보았듯이 프로세스(Process)들이 서로 상대방의 자원을 기다리게 되면 대기 관계가 서로 연결되는 구조가 만들어진다. 이 대기 관계를 그래프로 표현하면 원형 구조가 나타나게 되는데, 이처럼 대기 관계가 원형으로 이어지는 구조를 순환 대기(Circular Wait)라고 한다. 이 순환 대기(Circular Wait)는 교착상태(Deadlock)가 발생하기 위한 핵심 조건이 된다.</p>
<blockquote>
<p>📌 이처럼 프로세스(Process)와 자원의 대기 관계를 그래프로 표현한 것을 자원 할당 그래프(Resource Allocation Graph)라고 한다. 이 그래프에서 프로세스(Process)는 원(Circle)으로, 자원(Resource)은 사각형(Rectangle)으로 표현되며, 그래프 내에 사이클(Cycle)이 존재할 경우 교착상태(Deadlock)가 발생했거나 발생 가능성이 있다는 것을 시각적으로 확인할 수 있다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/4f4b2ca1-9019-4db2-84d8-810e10b23e8a.png" alt="" style="display:block;margin:0 auto" />

<h3>자원 할당 그래프(Resource Allocation Graph)의 개념</h3>
<p>앞서 살펴보았듯이 프로세스(Process) 간의 대기 관계가 서로 얽히게 되면 순환 구조가 만들어질 수 있다는 것을 확인하였다. 그렇다면 이런 대기 구조가 실제로 형성되었는지, 또 교착상태(Deadlock)가 발생했는지를 어떻게 확인할 수 있을까? 이때 사용하는 것이 바로 자원 할당 그래프(Resource Allocation Graph)이다. 자원 할당 그래프(Resource Allocation Graph)는 프로세스(Process)와 자원(Resource) 사이의 관계를 그림으로 나타내어, 누가 무엇을 기다리고 있고 이미 무엇을 가지고 있는지를 한눈에 파악하기 위한 도구이다. 특히 이 관계를 통해 대기 관계가 서로 얽혀 있는지, 즉 순환 관계(Circular Wait)가 존재하는지도 분석할 수 있게 된다. 따라서 자원 할당 그래프(Resource Allocation Graph)는 교착상태(Deadlock)의 발생 여부를 판단하기 위한 핵심 도구가 된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/8fa8ee0b-2401-48e9-a889-6294f1c90371.png" alt="" style="display:block;margin:0 auto" />

<h3>자원 할당 그래프(Resource Allocation Graph)의 구성 요소</h3>
<p>자원 할당 그래프(Resource Allocation Graph)의 구성 요소를 살펴보면 다음과 같다. 프로세스(Process)는 원(Circle) 형태로 표현되고, 자원(Resource)은 사각형(Rectangle) 으로 표현된다. 즉 형태만 보아도 실행 주체인지 자원인지 바로 구분이 가능하도록 설계되어 있다. 이 둘을 연결하는 간선(Edge)은 방향에 따라 의미가 달라진다. 프로세스(Process)에서 자원(Resource)으로 향하는 요청 간선(Request Edge)은 해당 프로세스(Process)가 그 자원(Resource)을 원하고 있다는 뜻으로, 아직 자원을 받지 못한 채 기다리거나 요청 중인 상태를 나타낸다. 반대로 자원(Resource)에서 프로세스(Process)로 향하는 할당 간선(Assignment Edge)은 해당 자원(Resource)이 현재 프로세스(Process)에게 배정되어 있다는 뜻으로, 이미 사용 중인 상태임을 나타낸다.</p>
<blockquote>
<p>📌 자원(Resource)을 나타내는 사각형 내부에는 자원의 인스턴스(Instance) 개수만큼 점(Dot)으로 표시하는 것이 일반적이다. 예를 들어 동일한 프린터(Printer)가 2대 있다면 사각형 안에 점이 2개 표시되며, 이를 통해 해당 자원(Resource)이 동시에 몇 개의 프로세스(Process)에 할당될 수 있는지를 한눈에 파악할 수 있게된다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/ec95fe11-290d-48fb-b83e-f9e0ab03f900.png" alt="" style="display:block;margin:0 auto" />

<h3>자원 할당 그래프(Resource Allocation Graph)의 간선 변화</h3>
<p>자원 할당 그래프(Resource Allocation Graph)에서 간선(Edge)이 어떻게 만들어지는지 예제를 통해 살펴보자.</p>
<p>먼저 프로세스(Process)가 자원(Resource)을 요청하게 되면, 프로세스(Process)에서 자원(Resource)으로 향하는 **요청 간선(Request Edge)**이 생성된다. 이 상태는 아직 자원(Resource)을 받은 것이 아니라 해당 자원(Resource)을 기다리고 있는 상태를 의미한다. 그 다음으로 자원(Resource)이 실제로 프로세스(Process)에게 할당되면 간선의 방향이 바뀌어 자원(Resource)에서 프로세스(Process)로 향하는 **할당 간선(Assignment Edge)**이 생성된다.</p>
<p>즉 요청 상태에서는 프로세스(Process) → 자원(Resource) 방향이었다가, 할당이 이루어지면 자원(Resource) → 프로세스(Process) 방향으로 바뀌게 된다. 이처럼 <strong>간선의 방향 변화만으로도 현재 상태가 요청 중인지 이미 사용 중인지를 명확하게 구분</strong>할 수 있기 때문에, 자원 할당 그래프(Resource Allocation Graph)에서 간선의 방향은 상태를 나타내는 핵심 요소가 된다.</p>
<blockquote>
<p><strong>📌</strong> 프로세스(Process)가 자원(Resource) 사용을 마치고 반납하게 되면 할당 간선(Assignment Edge) 자체가 제거된다. 즉 간선의 방향이 다시 바뀌는 것이 아니라 간선이 완전히 사라지게 되며, 이를 통해 해당 자원(Resource)이 다시 사용 가능한 상태가 되었음을 그래프에서 확인할 수 있다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/dd2101c6-5f3d-40d8-8173-abf8541ab00a.png" alt="" style="display:block;margin:0 auto" />

<h3>자원 할당 그래프(Resource Allocation Graph)의 사이클(Cycle)과 교착상태(Deadlock)</h3>
<p>자원 할당 그래프(Resource Allocation Graph)에서 가장 중요한 것은 <strong>사이클(Cycle)</strong>, 즉 순환 구조가 형성되는지 여부이다. 프로세스(Process)와 자원(Resource) 사이의 요청 관계와 할당 관계가 연결되면서 하나의 원형태로 이어지게 되면 사이클(Cycle)이 형성된 상태가 된다.</p>
<p>이 사이클(Cycle) 안에서는 각 프로세스(Process)가 다른 프로세스(Process)가 점유한 자원(Resource)을 기다리는 구조가 만들어지게 되는데, 누구도 먼저 실행을 끝내지 못하고 자원(Resource)을 반납하지 못하는 상태가 된다. 이처럼 서로가 서로를 기다리는 구조가 만들어지게 되면 교착상태(Deadlock)가 발생할 수 있으며, 자원 할당 그래프(Resource Allocation Graph)에서 이런 사이클(Cycle)이 존재하는지 확인하는 것이 <strong>교착상태(Deadlock)를 판단하는 핵심 기준</strong>이 된다.</p>
<blockquote>
<p>**📌**사이클(Cycle)이 존재한다고 해서 반드시 교착상태(Deadlock)가 발생한 것은 아니다. 자원(Resource)의 인스턴스(Instance)가 하나뿐인 경우에는 사이클(Cycle) 존재 자체가 교착상태(Deadlock)를 의미하지만, 자원(Resource)의 인스턴스(Instance)가 여러 개인 경우에는 사이클(Cycle)이 존재하더라도 교착상태(Deadlock)가 발생하지 않을 수 있다. 따라서 사이클(Cycle)은 교착상태(Deadlock)의 <strong>필요 조건</strong>이지 <strong>충분 조건</strong>은 아님을 알고있자.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a29ac38b-1033-4903-9050-41094c11817e.png" alt="" style="display:block;margin:0 auto" />

<h3>자원 할당 그래프(Resource Allocation Graph)에서의 교착상태(Deadlock) 분석</h3>
<p>다음 그림을 기준으로 자원 할당 상태를 하나씩 해석해보자.</p>
<p>먼저 프로세스P1(Process P1)을 보면 자원R2(Resource R2)를 하나 점유한 상태에서 자원R1(Resource R1)을 추가로 요청하고 있다. 이는 하나의 자원을 가진 상태에서 다른 자원을 기다리고 있는 구조이다. 다음으로 프로세스P2(Process P2)를 보면 자원R1(Resource R1)과 자원R2(Resource R2)를 하나씩 점유하고 있으면서 자원R3(Resource R3)를 추가로 요청하고 있다. 이미 여러 자원을 확보한 상황에서 또 다른 자원을 기다리고 있는 상황이다. 프로세스P3(Process P3)의 경우에는 자원R3(Resource R3)를 하나 점유하고 있지만 추가적인 요청은 없는 상황이다.</p>
<p>전체적인 관계를 살펴보면, 프로세스P1(Process P1)은 자원R1(Resource R1)을 기다리고 있고, 자원R1(Resource R1)은 프로세스P2(Process P2)에 할당되어 있다. 프로세스P2(Process P2)는 자원R3(Resource R3)를 기다리고 있고, 자원R3(Resource R3)는 프로세스P3(Process P3)에 할당되어 있는 구조이다. 즉 현재 대기 관계가 이어지고 있긴 하지만 완전한 순환 구조를 이루고 있지는 않은 상태이다. 따라서 이 상태는 교착상태(Deadlock)가 아니라 <strong>교착상태(Deadlock)로 진행될 가능성이 있는 중간 단계</strong>로 볼 수 있다.</p>
<blockquote>
<p><strong>📌</strong> 이처럼 교착상태(Deadlock)는 아니지만 자원 부족으로 인해 특정 프로세스(Process)가 계속해서 자원을 할당받지 못하는 상태를 **기아 상태(Starvation)**라고 한다. 현재 그림의 상태에서 프로세스P3(Process P3)가 자원R3(Resource R3)를 오랫동안 반납하지 않는다면, 프로세스P2(Process P2)가 자원R3(Resource R3)를 계속 기다리게 되는 기아 상태(Starvation)로 이어질 수 있다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/18085abd-8353-4495-aef7-414fe12bffb0.png" alt="" style="display:block;margin:0 auto" />

<h3>사이클(Cycle)이 존재하지만 교착상태(Deadlock)가 아닌 경우</h3>
<p>이 그림은 사이클(Cycle)이 존재하지만 반드시 교착상태(Deadlock)라고는 볼 수 없는 경우이다.</p>
<p>흐름을 살펴보면 프로세스P1(Process P1) → 자원R1(Resource R1) → 자원R3(Resource R3) → 자원R2(Resource R2) → 프로세스P2(Process P2) → 프로세스P1(Process P1)으로 이어지는 사이클(Cycle)을 확인할 수 있다. 겉으로 보면 순환 구조가 존재하기 때문에 교착상태(Deadlock)처럼 보일 수 있다.</p>
<p>하지만 여기서 중요한 차이점은 <strong>자원의 인스턴스(Instance)가 여러 개 존재한다</strong>는 점이다. 예를 들어 자원R1(Resource R1)이나 자원R2(Resource R2)에 여러 개의 인스턴스(Instance)가 있다면, 대기 중인 프로세스(Process) 중 일부는 추가로 자원을 할당받을 수 있다. 따라서 모든 프로세스(Process)가 동시에 막히는 것이 아니라 일부는 계속 진행할 수 있고, 그 과정에서 자원이 반납되면 다른 프로세스(Process)들도 다시 실행될 수 있게 되어 대기 구조가 풀리게 된다.</p>
<p>즉 <strong>사이클(Cycle)의 존재만으로 항상 교착상태(Deadlock)라고 판단할 수는 없으며</strong>, 자원의 인스턴스(Instance) 개수를 함께 고려해야 교착상태(Deadlock) 여부를 정확하게 판단할 수 있다.</p>
<blockquote>
<p><strong>📌 자</strong>원(Resource)의 인스턴스(Instance)가 <strong>하나뿐</strong>인 경우에는 사이클(Cycle) 존재 자체가 교착상태(Deadlock)를 의미하지만, 인스턴스(Instance)가 <strong>여러 개</strong>인 경우에는 사이클(Cycle)이 존재하더라도 교착상태(Deadlock)가 발생하지 않을 수 있다. 따라서 사이클(Cycle)은 교착상태(Deadlock)의 <strong>필요 조건</strong>이지 <strong>충분 조건</strong>은 아니다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/15cd4165-fc26-4c66-9558-8898ddca5c6f.png" alt="" style="display:block;margin:0 auto" />

<h3>교착상태(Deadlock) 처리 방법</h3>
<p>교착상태(Deadlock)가 발생하게 되면 프로세스(Process)들이 서로 자원을 기다리면서 전체 시스템의 실행이 멈출 수 있다. 이런 상황이 계속 진행되면 시스템 자원이 낭비되거나 서비스가 중단되는 문제가 발생할 수 있어, 교착상태(Deadlock)를 예방하거나 발생 이후에도 적절히 처리할 수 있는 방법이 필요하다.</p>
<p>교착상태(Deadlock)는 한 가지 방법으로만 해결하는 것이 아니라 여러 가지 접근 방식으로 다룰 수 있으며, 시스템의 설계에 따라 적용하는 방법도 달라지게 된다. 교착상태(Deadlock)를 처리하는 방법은 크게 네 가지로 나뉜다.</p>
<p>첫째, **예방(Prevention)**은 교착상태(Deadlock)가 아예 발생하지 않도록 조건을 미리 제거하는 방식이다. 둘째, **회피(Avoidance)**는 현재 상태를 고려하면서 교착상태(Deadlock)로 이어지지 않도록 자원을 할당하는 방식이다. 셋째, **탐지(Detection)**는 교착상태(Deadlock) 발생을 허용한 뒤 이를 찾아내는 방식이다. 넷째, **복구(Recovery)**는 탐지된 이후 문제를 해결하는 방식이다.</p>
<p>즉 교착상태(Deadlock)는 발생 전에 막을 수도 있고, 발생 가능성을 피할 수도 있으며, 발생 후에 찾아서 해결할 수도 있는 여러 가지 전략이 존재한다. 각각의 방법에 대한 구체적인 내용은 이어서 살펴볼 것이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/cddcd9f3-d2cd-4f25-8838-fa97367df759.png" alt="" style="display:block;margin:0 auto" />

<h3>교착상태 예방(Deadlock Prevention)</h3>
<p>교착상태 예방(Deadlock Prevention)은 교착상태(Deadlock)가 아예 발생하지 않도록 구조 자체를 바꾸는 방법이다. 앞서 살펴본 네 가지 조건 중 하나라도 성립하지 않도록 한다면 교착상태(Deadlock)는 발생할 수 없다. 따라서 시스템을 설계할 때부터 이 조건들이 동시에 만족되지 않도록 제한을 두게 된다.</p>
<p>예를 들어 프로세스(Process)가 필요한 자원을 모두 한 번에 요청하도록 한다면, 자원을 하나 점유한 채 다른 자원을 기다리는 <strong>점유와 대기(Hold and Wait)</strong> 상황을 막을 수 있다. 또는 자원 요청 순서를 미리 정해놓으면 서로가 서로를 기다리는 <strong>순환 대기(Circular Wait)</strong> 구조 자체가 만들어지지 않도록 할 수도 있다.</p>
<p>즉 교착상태 예방(Deadlock Prevention)은 실행 중에 해결하는 것이 아니라, <strong>애초에 교착상태(Deadlock)가 발생할 수 없도록 설계 단계에서 차단하는 방식</strong>이다.</p>
<blockquote>
<p>**📌**교착상태 예방(Deadlock Prevention)은 근본적인 해결책이 될 수 있지만 현실적인 한계도 존재한다. 모든 자원을 한 번에 요청하는 방식은 실제로 필요하지 않은 자원까지 미리 점유하게 되어 **자원 활용의 효율성(Resource Utilization)**이 크게 떨어질 수 있다. 또한 자원 요청 순서를 미리 고정하는 방식은 프로세스(Process)의 유연성을 제한할 수 있다는 단점이 있다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/c41946eb-a7a5-4327-99c5-b0bbac1d5b74.png" alt="" style="display:block;margin:0 auto" />

<h3>교착상태 회피(Deadlock Avoidance)</h3>
<p>교착상태 회피(Deadlock Avoidance)는 자원을 할당할 때마다 교착상태(Deadlock)로 이어질 가능성이 있는지를 미리 확인하는 방식이다. 즉 요청이 들어왔다고 해서 바로 자원을 할당하는 것이 아니라, 이 자원을 할당했을 때 시스템이 안전한 상태(Safe State)를 유지할 수 있는지를 먼저 판단하는 것이다. 여기서 말하는 안전한 상태(Safe State)란 모든 프로세스(Process)가 결국 작업을 끝낼 수 있는 실행 순서가 존재하는 상태를 의미한다. 만약 어떤 자원 할당이 안전한 상태(Safe State)를 깨뜨릴 가능성이 있다면 그 요청은 일단 보류하게 된다. 대표적인 방법으로는 뱅커스 알고리즘(Banker's Algorithm)이 있다. 뱅커스 알고리즘(Banker's Algorithm)은 항상 시스템이 안전한 상태(Safe State)를 유지하는 경우에만 자원을 할당하도록 설계되어 있다. 즉 교착상태 회피(Deadlock Avoidance)는 교착상태(Deadlock) 발생을 완전히 막는 것은 아니지만, 위험한 상태로 가지 않도록 미리 판단하고 조절하는 방식이라고 이해하면 된다.</p>
<blockquote>
<p>📌 안전한 상태(Safe State)와 반대되는 개념으로 불안전한 상태(Unsafe State)가 있다. 불안전한 상태(Unsafe State)가 반드시 교착상태(Deadlock)를 의미하는 것은 아니지만, 교착상태(Deadlock)로 이어질 가능성이 있는 상태를 의미한다. 또한 뱅커스 알고리즘(Banker's Algorithm)은 1965년 E. W. Dijkstra가 고안한 알고리즘으로, 은행이 대출 가능 금액을 관리하는 방식에서 착안하여 이름이 붙여졌다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/b8d16a47-7d29-4362-8ee5-d4e985c49e5d.png" alt="" style="display:block;margin:0 auto" />

<h3>교착상태 탐지(Deadlock Detection)</h3>
<p>교착상태 탐지(Deadlock Detection)는 교착상태(Deadlock)의 발생 자체를 막는 것이 아니라, 일단 발생을 허용한 뒤 이를 찾아내는 방식이다. 시스템은 자원을 자유롭게 할당하면서 운영되지만, 주기적으로 현재 상태를 검사하여 교착상태(Deadlock)가 발생했는지를 확인하게 된다.</p>
<p>이때 자원 할당 그래프(Resource Allocation Graph)를 이용하여 프로세스(Process) 간의 대기 관계를 분석하고, 특히 그래프에서 사이클(Cycle)이 존재하는지를 확인하여 교착상태(Deadlock) 여부를 판단할 수 있다. 즉 교착상태 탐지(Deadlock Detection)는 예방하거나 회피하는 것이 아니라, <strong>발생 이후에 이를 발견하는 방식</strong>이라고 이해하면 된다.</p>
<blockquote>
<p><strong>📌</strong> 교착상태 탐지(Deadlock Detection)는 자원을 자유롭게 할당하기 때문에 예방(Prevention)이나 회피(Avoidance)에 비해 **자원 활용의 효율성(Resource Utilization)**이 높다는 장점이 있다. 그러나 탐지 자체에도 시스템 자원이 소모되므로 탐지 주기를 너무 짧게 설정하면 오버헤드(Overhead)가 커질 수 있고, 너무 길게 설정하면 교착상태(Deadlock)가 오랫동안 지속될 수 있다는 단점이 있다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/96161ef4-034c-4119-a4b3-48b8f4c7d742.png" alt="" style="display:block;margin:0 auto" />

<h3>교착상태 복구(Deadlock Recovery)</h3>
<p>교착상태 복구(Deadlock Recovery)는 교착상태(Deadlock)가 실제로 발생한 이후에 이를 해소하는 방법이다. 즉 이미 프로세스(Process)들이 서로 기다리며 멈춰 있는 상태를 강제로 풀어야 하는 상황이다.</p>
<p>가장 직접적인 방법은 **프로세스 종료(Process Termination)**이다. 문제가 되는 프로세스(Process)를 하나씩 중단시킴으로써 자원이 해제되면서 대기 구조가 깨지는 방식이다. 또 다른 방법은 **자원 강제 회수(Resource Preemption)**이다. 특정 프로세스(Process)가 사용 중인 자원을 강제로 빼앗아 다른 프로세스(Process)에게 할당함으로써 교착상태(Deadlock)를 해소하는 방법이다. 또는 일부 프로세스(Process)를 선택적으로 중단시켜 전체 대기 구조를 끊는 방법도 사용된다.</p>
<p>즉 교착상태 복구(Deadlock Recovery)는 자연스럽게 해결되기를 기다리는 것이 아니라, <strong>시스템이 직접 개입하여 강제적으로 교착 구조를 깨는 방식</strong>이라고 이해하면 된다.</p>
<hr />
<blockquote>
<p>**📌**프로세스 종료(Process Termination) 방식에는 두 가지 세부 전략이 있다. 교착상태(Deadlock)에 관련된 모든 프로세스(Process)를 한꺼번에 종료하는 방식과, 프로세스(Process)를 하나씩 종료하면서 교착상태(Deadlock)가 해소되었는지 확인하는 방식이다. 또한 자원 강제 회수(Resource Preemption) 시에는 어떤 프로세스(Process)의 자원을 빼앗을지 결정하는 <strong>희생자 선택(Victim Selection)</strong> 문제와, 자원을 빼앗긴 프로세스(Process)가 계속해서 희생자로 선택되는 <strong>기아 상태(Starvation)</strong> 문제도 함께 고려해야 한다.</p>
</blockquote>
<hr />
<h1>3️⃣Practice Implementing Synchronization Techniques</h1>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/01320f1b-2e02-4068-8991-0849f93e7277.png" alt="" style="display:block;margin:0 auto" />

<h3>뮤텍스(Mutex) 기반 동기화 실습; 경쟁 상태(Race Condition) 확인</h3>
<p>본격적인 실습에 들어가기 전에, 지난 시간에 살펴보았던 경쟁 상태(Race Condition)를 실제 코드로 확인해볼 것이다.</p>
<p>여러 스레드(Thread)가 동시에 실행되면서 하나의 공유 자원(Shared Resource)에 접근하게 되면 실행 결과가 예상하는 값과 다르게 나올 수 있다. 이처럼 실행 순서에 따라 결과가 달라지는 현상을 **경쟁 상태(Race Condition)**라고 한다.</p>
<p>이번 실습에서는 먼저 뮤텍스(Mutex)를 사용하지 않은 상태에서 어떤 문제가 발생하는지 확인하고, 이후 뮤텍스(Mutex)를 적용하여 문제가 어떻게 해결되는지 비교해볼 것이다.</p>
<blockquote>
<p>**📌**경쟁 상태(Race Condition)는 실행할 때마다 결과가 달라질 수 있기 때문에 디버깅(Debugging)이 매우 어렵다는 특징이 있다. 특정 환경에서는 문제없이 동작하다가 다른 환경에서는 오류가 발생하는 경우도 있어, 실습을 통해 직접 확인하는 것이 개념을 이해하는 데 효과적이다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/fd4d3a21-6f0e-4ede-8149-9360cebb591e.png" alt="" style="display:block;margin:0 auto" />

<h3>뮤텍스(Mutex) 미적용 코드; 경쟁 상태(Race Condition) 발생 확인</h3>
<p>이 코드는 뮤텍스(Mutex)를 사용하지 않고 두 개의 스레드(Thread)가 전역변수 카운터(Counter)를 동시에 증가시키는 예제이다.</p>
<p>겉보기에는 각 스레드(Thread)가 <code>for(int i = 0; i &lt; 100000; i++)</code> 구문을 통해 10만 번씩 증가시키므로 결과가 20만이 되어야 할 것처럼 보인다. 하지만 실행 결과는 매번 달라질 수 있다. 이는 <code>count++</code> 증가 연산이 원자적(Atomic)으로 수행되지 않아 중간에 다른 스레드(Thread)가 끼어들 수 있기 때문이다. 즉 공유 변수(Shared Variable)에 대한 동시 접근으로 인해 **경쟁 상태(Race Condition)**가 발생한다는 것을 이 코드를 통해 확인할 수 있다.</p>
<blockquote>
<p><strong>📌</strong><code>count++</code> 연산은 내부적으로 읽기(Read) → 수정(Modify) → 쓰기(Write)의 세 단계로 처리되는데, 두 스레드(Thread)가 동시에 같은 값을 읽어 각각 증가시킨 뒤 저장하면 한 번의 증가만 반영되는 <strong>갱신 손실(Lost Update)</strong> 문제가 발생한다. 이것이 최종 결과가 20만보다 작게 나오는 원인이 된다.</p>
</blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/adc71099-af3a-454e-9872-bd0f845ee7a8.png" alt="" style="display:block;margin:0 auto" />

<h3>실습 결과; 경쟁 상태(Race Condition) 확인</h3>
<p>실행 환경에 따라 겉으로는 정상적인 20만이 계속 나오는 경우가 있다. 이는 두 스레드(Thread)의 실행이 크게 겹치지 않고 한 스레드(Thread)가 대부분의 작업을 먼저 실행한 후 다른 스레드(Thread)가 실행되는 경우가 있기 때문이다. 또한 시스템의 부하가 낮거나 반복 횟수가 상대적으로 적은 경우에는 충돌이 눈에 띄게 발생하지 않을 수도 있다.</p>
<p>그러나 이런 경우에도 코드 자체가 항상 올바른 결과를 보장하지는 않는다. 반복 횟수를 계속 늘리면 예상했던 정상값이 아닌 더 낮은 값이 나오게 될 것이다. 이는 공유 변수(Shared Variable)에 대한 접근을 보호하지 않기 때문에 실행 순서에 따라 결과가 달라지는 **경쟁 상태(Race Condition)**가 발생할 수 있다는 점을 이 코드를 통해 확인할 수 있다.  </p>
<blockquote>
<p><strong>📌 이</strong>처럼 경쟁 상태(Race Condition)는 항상 눈에 보이는 오류를 발생시키는 것이 아니라 특정 조건에서만 나타나는 경우가 많아 <strong>비결정적(Non-deterministic)</strong> 버그라고도 불립니다. 이런 특성 때문에 테스트 환경에서는 정상적으로 동작하다가 실제 운영 환경에서 예상치 못한 오류가 발생하는 경우가 생길 수 있어, 처음부터 뮤텍스(Mutex)와 같은 동기화(Synchronization) 기법을 적용하여 설계하는 것이 중요하다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/0be61623-48a2-4e17-99fb-7405fa2303f9.png" alt="" style="display:block;margin:0 auto" />

<h3>뮤텍스(Mutex) 적용; 경쟁 상태(Race Condition) 해결</h3>
<p>앞선 예제에서 확인한 것처럼 여러 스레드(Thread)가 하나의 공유 자원(Shared Resource)에 동시에 접근하게 되면 실행 결과가 일정하지 않다는 것을 확인하였다. 이런 문제를 막기 위해서는 공유 자원(Shared Resource)에 대한 접근을 제어하는 동기화(Synchronization)가 필요하다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a9b54aae-a0dc-4b37-9253-31302afd8adb.png" alt="" style="display:block;margin:0 auto" />

<p>이 문제를 해결하기 위해 뮤텍스(Mutex)를 사용해보도록 하자. 뮤텍스(Mutex)는 한 번에 하나의 실행 흐름만 임계영역(Critical Section)에 들어가도록 제한하는 방식으로, 여러 스레드(Thread)가 동시에 접근하면 문제가 되는 코드 영역을 하나의 스레드(Thread)만 실행하도록 보호하는 역할을 한다. 이때 임계영역(Critical Section)이란 여러 실행 흐름이 동시에 접근하면 문제가 발생하는 코드 구간을 의미한다. 뮤텍스(Mutex)는 Lock과 Unlock을 이용하여 이 임계영역(Critical Section)을 감싸는 방식으로 동작하게 된다.</p>
<blockquote>
<p>**📌**뮤텍스(Mutex)를 적용하면 앞서 발생했던 갱신 손실(Lost Update) 문제가 해결되어 두 스레드(Thread)가 각각 10만 번씩 증가시킨 결과가 정확히 20만으로 나오게 된다. 다만 뮤텍스(Mutex)를 적용하면 한 번에 하나의 스레드(Thread)만 임계영역(Critical Section)에 진입할 수 있기 때문에, 미적용 상태에 비해 실행 시간이 다소 길어질 수 있다는 점도 함께 이해하면 좋다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/c284c063-89e5-402d-94ff-7f7420de095f.png" alt="" style="display:block;margin:0 auto" />

<h3>뮤텍스(Mutex) 구현; pthread 함수</h3>
<p>실제 구현에서는 pthread(POSIX Thread)가 제공하는 뮤텍스(Mutex) 함수를 사용한다. 주요 함수는 다음 세 가지이다. 먼저 pthread_mutex_init()은 뮤텍스(Mutex)를 초기화하는 함수이다. 다음으로 pthread_mutex_lock()은 뮤텍스(Mutex)를 잠그는 함수로, 잠금(Lock)을 획득한 경우에만 임계영역(Critical Section)에 진입할 수 있다. 마지막으로 pthread_mutex_unlock()은 잠금을 해제하는 함수로, 다른 스레드(Thread)가 임계영역(Critical Section)에 들어갈 수 있도록 해준다. 이 세 가지 함수를 이용하여 임계영역(Critical Section)을 보호하게 된다.</p>
<blockquote>
<p>📌 pthread(POSIX Thread)는 주로 C언어 기반의 Unix/Linux 환경에서 사용되는 스레드(Thread) 표준 인터페이스이다. 위 세 가지 함수 외에도 뮤텍스(Mutex) 사용이 끝난 후 자원을 해제하는 pthread_mutex_destroy() 함수도 함께 사용하는 것이 일반적이다. 초기화(Init)와 해제(Destroy)를 쌍으로 사용하는 것은 Lock과 Unlock을 쌍으로 사용하는 것만큼 중요하다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/2a36016f-9c75-4fb5-a8ac-3da9eee97181.png" alt="" style="display:block;margin:0 auto" />

<h3>뮤텍스(Mutex) 적용 코드 분석</h3>
<p>코드를 살펴보면 다음과 같은 구조로 동작한다. 먼저 <code>pthread_mutex_t m;</code>으로 뮤텍스(Mutex) 변수를 선언하고, <code>pthread_mutex_init(&amp;m, NULL);</code>을 통해 초기화를 수행한다. 그 다음 work 함수 안에서 공유 변수인 카운터(Counter)를 증가시키는 부분을 살펴보면, <code>pthread_mutex_lock(&amp;m)</code>;으로 뮤텍스(Mutex) Lock을 호출한 뒤 <code>count++</code> 증가 연산을 수행하고, 곧바로 <code>pthread_mutex_unlock(&amp;m)</code>;으로 뮤텍스(Mutex) Unlock을 호출하고 있다. 즉 카운터(Counter)를 수정하는 구간만을 임계영역(Critical Section)으로 설정하여 보호하고 있는 것이다. 이렇게 하면 한 스레드(Thread)가 카운터(Counter)를 증가시키는 동안에는 다른 스레드(Thread)가 해당 구간에 진입할 수 없게 되어, 실행 순서가 섞이지 않고 항상 카운터(Counter) 값이 정확하게 나오는 것을 확인할 수 있다.</p>
<blockquote>
<p>📌 이 코드에서 임계영역(Critical Section)을 count++ 연산만으로 최소화한 것은 올바른 설계이다. 앞서 배운 것처럼 임계영역(Critical Section)을 필요 이상으로 넓게 설정하면 불필요한 대기와 성능 저하가 발생할 수 있기 때문이다. 또한 실습 이후에는 <code>pthread_mutex_destroy(&amp;m);</code>을 호출하여 뮤텍스(Mutex) 자원을 해제하는 것이 올바른 마무리이다.</p>
</blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/33fff775-087d-441c-996a-0d2c0d709d78.png" alt="" style="display:block;margin:0 auto" />

<h3>뮤텍스(Mutex) 적용 코드 실습 결과</h3>
<p>전역변수 <code>int count = 0;</code>은 두 스레드(Thread)가 함께 사용하는 공유 자원(Shared Resource)이고, <code>pthread_mutex_t m;</code>을 통해 이를 보호할 준비를 한다. main 함수에서는 <code>mutex_init</code>으로 뮤텍스(Mutex)를 초기화한 뒤 <code>pthread_create</code>를 통해 두 개의 스레드(Thread)를 생성하고 있다. 이 코드의 핵심은 <code>work</code> 함수이다. 반복문 안에서 카운터(Counter)를 증가시키기 전에 <code>pthread_mutex_lock(&amp;m);</code>을 호출하고, <code>count++</code> 수행 후에는 <code>pthread_mutex_unlock(&amp;m);</code>을 호출하고 있다. 이처럼 카운터(Counter)를 수정하는 구간을 Lock과 Unlock으로 감싸 임계영역(Critical Section)으로 만들고 있다. 이렇게 하면 한 스레드(Thread)가 임계영역(Critical Section)을 수행하는 동안 다른 스레드(Thread)는 Lock을 얻지 못하고 대기하게 된다. 그 결과 실행 순서에 관계없이 결과가 항상 일정하게 유지되며, 경쟁 상태(Race Condition)가 발생하지 않는다.  </p>
<blockquote>
<p>📌 뮤텍스(Mutex) 적용 전후의 실행 시간을 비교해보면 뮤텍스(Mutex) 적용 후 실행 시간이 다소 길어지는 것을 확인할 수 있다. 이는 Lock과 Unlock 과정에서 발생하는 오버헤드(Overhead)와 대기 중인 스레드(Thread)가 순서를 기다리는 시간이 추가되기 때문이다. 즉 뮤텍스(Mutex)는 정확성을 보장하는 대신 성능의 일부를 희생하는 트레이드오프(Trade-off)가 존재한다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/d101fd4b-5f7d-4284-bca9-7eb56d603f91.png" alt="" style="display:block;margin:0 auto" />

<h3>세마포어(Semaphore) 기반 동기화 실습</h3>
<p>뮤텍스(Mutex)와 달리 세마포어(Semaphore)는 실행 흐름의 순서를 제어하는 데도 사용할 수 있다. 즉 어떤 작업이 먼저 끝난 뒤에 다른 작업이 실행되도록 만들 수 있으며, 조건이 만족되지 않으면 해당 실행 흐름을 대기시키고 조건이 충족되었을 때 실행을 이어갈 수 있다.</p>
<p>지난 시간에는 세마포어(Semaphore)가 어떤 방식으로 동작하는지 개념을 중심으로 학습하였다면, 이번에는 <strong>실제 코드에서 세마포어(Semaphore)가 어떻게 사용되는지</strong>를 중심으로 살펴볼 것이다.  </p>
<blockquote>
<p><strong>📌</strong> 뮤텍스(Mutex)와 세마포어(Semaphore)의 핵심적인 차이를 다시 한번 정리하면, 뮤텍스(Mutex)는 <strong>상호 배제(Mutual Exclusion)</strong>, 즉 공유 자원(Shared Resource)에 대한 동시 접근을 막는 것이 주된 목적인 반면, 세마포어(Semaphore)는 이에 더해 <strong>실행 순서 제어</strong>와 **조건 동기화(Condition Synchronization)**까지 활용할 수 있다는 점에서 보다 넓은 범위의 동기화(Synchronization) 기법이라고 할 수 있다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/b053b1c8-bafd-4718-96f2-f3d45a5bbd71.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a92cde93-bb17-49d8-946d-82176fea7e04.png" alt="" style="display:block;margin:0 auto" />

<h3>세마포어(Semaphore) 구현</h3>
<p>기본 함수 세마포어(Semaphore)는 정수값을 기반으로 실행 흐름의 접근이나 실행 순서를 제어하는 동기화(Synchronization) 기법이다. 실제 구현에서는 Wait/Signal 대신 <code>sem_wait</code>와 <code>sem_post</code> 함수를 사용한다. 세마포어(Semaphore)를 사용할 때는 다음 세 가지 기본 함수를 사용하게 된다. 먼저 <code>sem_init()</code>은 세마포어(Semaphore)를 초기화하는 함수이다. <code>sem_wait()</code>는 세마포어(Semaphore)의 값을 감소시키면서 조건에 맞지 않으면 실행 흐름을 대기 상태로 전환한다. <code>sem_post()</code>는 세마포어(Semaphore)의 값을 증가시키면서 대기 중인 실행 흐름이 있다면 다시 실행되도록 한다. 이 세 가지 함수를 통해 실행 흐름의 순서를 제어할 수 있다.  </p>
<blockquote>
<p>📌뮤텍스(Mutex)의 <code>pthread_mutex_destroy()</code>와 마찬가지로 세마포어(Semaphore) 사용이 끝난 후에는 <code>sem_destroy()</code> 함수를 호출하여 세마포어(Semaphore) 자원을 해제하는 것이 올바른 마무리이다. 또한 <code>sem_wait()</code>는 세마포어(Semaphore) 값이 0인 경우 즉시 대기 상태로 진입하며, <code>sem_post()</code>가 호출되어 값이 증가할 때까지 대기하게 된다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/65c22745-b335-4548-9684-8a9c51a70520.png" alt="" style="display:block;margin:0 auto" />

<h3>세마포어(Semaphore)를 이용한 실행 순서 제어 예제</h3>
<p>이 예제는 세마포어(Semaphore)를 이용해 두 스레드(Thread)의 실행 순서를 제어하는 구조이다. <code>sem_init(&amp;sem, 0, 0);</code>에서 세마포어(Semaphore) 값을 0으로 설정하게 되면, <code>sem_wait(&amp;sem;)</code>을 호출한 스레드(Thread)는 처음에 바로 실행되지 못하고 대기 상태로 진입하게 된다. 이후 스레드1(Thread 1)이 작업을 수행한 뒤 <code>sem_post(&amp;sem;)</code>를 호출하는 순간, 대기하고 있던 스레드2(Thread 2)가 깨어나게 된다. 따라서 스레드2(Thread 2)는 스레드1(Thread 1)의 작업이 끝난 이후에만 실행될 수 있다. 실행 결과를 보면 스레드1(Thread 1)의 작업이 먼저 완료된 다음에 스레드2(Thread 2)가 실행되는 순서가 항상 유지된다. 즉 이 예제의 핵심은 세마포어(Semaphore)를 활용하여 스레드(Thread)의 실행 순서를 명확하게 제어할 수 있고, 그 결과 실행 흐름이 항상 일정하게 유지된다는 점이다.  </p>
<blockquote>
<p>📌 이 예제에서 세마포어(Semaphore)의 초기값을 0으로 설정하는 것이 핵심이다. 초기값이 0이기 때문에 스레드2(Thread 2)가 <code>sem_wait()</code>를 호출하는 순간 즉시 대기 상태로 진입하게 되며, 스레드1(Thread 1)이 <code>sem_post()</code>를 호출하기 전까지는 절대 실행될 수 없다. 만약 초기값을 1 이상으로 설정했다면 스레드2(Thread 2)가 스레드1(Thread 1)보다 먼저 실행될 수도 있어 순서 제어가 이루어지지 않게된다.</p>
</blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/848d853d-ae75-430a-b27d-0c4fe087289d.png" alt="" style="display:block;margin:0 auto" />

<h3>세마포어(Semaphore)를 이용한 동기화 코드 실습</h3>
<p><code>sem_t sem;</code>으로 세마포어(Semaphore) 변수를 선언하고, main 함수에서 <code>sem_init(&amp;sem, 0, 0);</code>을 통해 초기값을 0으로 설정한다. 이 초기값이 여기서 중요한데, 0으로 설정하게 되면 <code>sem_wait</code>를 호출하는 스레드(Thread)는 조건이 만족될 때까지 대기 상태에 들어가게 된다. 이 코드에는 두 개의 스레드(Thread)가 있다. T1을 보면 작업 시작을 출력한 뒤 <code>sleep(1)</code>로 일정 시간 실행되고, 작업이 끝나면 <code>sem_post(&amp;sem);</code>을 호출한다. 반면 T2는 처음에 <code>sem_wait(&amp;sem);</code>을 호출하기 때문에 초기에는 바로 실행되지 못하고 대기 상태에 머무르게 된다. 실행 결과를 보면 <code>Thread1 작업 시작 → 작업 완료 → Thread2</code> 실행 순서로 나타나는 것을 확인할 수 있다. 즉 스레드2(Thread 2)는 먼저 생성되더라도 바로 실행되지 않고, 스레드1(Thread 1)이 <code>sem_post(&amp;sem);</code>를 호출한 이후에만 실행된다. 이처럼 세마포어(Semaphore)를 활용하면 특정 작업이 끝난 뒤에 다른 작업이 실행되도록 실행 순서를 명확하게 제어할 수 있다.  </p>
<blockquote>
<p>📌 이 예제에서 스레드2(Thread 2)가 스레드1(Thread 1)보다 먼저 생성되더라도 <code>sem_wait(&amp;sem;)</code>으로 인해 반드시 스레드1(Thread 1)이 먼저 실행되는 구조가 보장된다. 이처럼 세마포어(Semaphore)의 초기값을 0으로 설정하는 방식은 앞서 배운 시그널링(Signaling) 패턴의 전형적인 사례로, 뮤텍스(Mutex)로는 구현하기 어려운 실행 순서 제어를 세마포어(Semaphore)로 간단하게 구현할 수 있다는 점이 핵심이다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/6c10b915-956a-405c-8e1e-aac4da6f398a.png" alt="" style="display:block;margin:0 auto" />

<h3>교착상태(Deadlock) 실습</h3>
<p>이번 실습에서는 서로 다른 자원을 동시에 요청할 때 어떤 문제가 발생하는지를 확인한다.</p>
<p>각 실행 흐름이 하나의 자원을 점유한 상태에서 다른 자원을 추가로 요청하게 되면, 서로가 서로의 자원을 기다리는 상황이 만들어질 수 있다. 이 상태에서는 어떤 쪽도 먼저 자원을 내려놓지 않기 때문에 프로그램 실행이 더 이상 진행되지 않는, 즉 실행이 멈춰버리는 **교착상태(Deadlock)**가 발생하게 된다.  </p>
<blockquote>
<p>**📌**이번 실습에서 확인하게 될 교착상태(Deadlock)는 앞서 이론에서 살펴본 네 가지 필요 조건인 상호 배제(Mutual Exclusion), 점유와 대기(Hold and Wait), 비선점(Non-preemption), 순환 대기(Circular Wait)가 모두 충족되는 상황임을 코드를 통해 직접 확인할 수 있다. 실습을 통해 교착상태(Deadlock)가 발생하면 프로그램이 아무런 오류 메시지 없이 그냥 멈춰버리는 경우가 많아, 실제 디버깅(Debugging) 시 원인을 찾기 매우 어렵다는 점도 함께 이해하면 좋다.</p>
</blockquote>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/8926b081-05c8-45cc-a486-fa69c6389ba7.png" alt="" style="display:block;margin:0 auto" />

<h3>교착상태(Deadlock) 발생 코드 분석</h3>
<p>이 코드는 두 개의 스레드(Thread)가 서로 다른 뮤텍스(Mutex)를 반대 순서로 획득하면서 교착상태(Deadlock)를 발생시키는 예제이다.</p>
<p>스레드1(Thread 1)을 보면 <code>pthread_mutex_lock(&amp;m1);</code>으로 m1을 먼저 잠근 뒤, <code>sleep(1)</code>로 잠시 대기한 후 <code>pthread_mutex_lock(&amp;m2);</code>으로 m2를 잠그려고 한다. 반면 스레드2(Thread 2)는 <code>pthread_mutex_lock(&amp;m2);</code>으로 m2를 먼저 잠근 뒤, <code>sleep(1)</code>로 잠시 대기한 후 <code>pthread_mutex_lock(&amp;m1);</code>으로 m1을 잠그려고 한다.</p>
<p>이 상태에서 실행 순서를 살펴보면, 스레드1(Thread 1)이 m1을 먼저 점유하고 스레드2(Thread 2)가 m2를 먼저 점유하게 된다. 이후 스레드1(Thread 1)은 m2를 기다리고, 스레드2(Thread 2)는 m1을 기다리게 되는데, 결국 두 스레드(Thread) 모두 서로가 가진 자원을 기다리는 상태가 되어 어느 쪽도 더 이상 진행하지 못하게 된다. 이처럼 <strong>자원 획득 순서가 서로 엇갈리게 되면 교착상태(Deadlock)가 실제로 발생할 수 있다</strong>는 점을 이 코드를 통해 확인할 수 있다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/448765be-be29-4a82-bc14-cd66ba3e57c5.png" alt="" style="display:block;margin:0 auto" />

<blockquote>
<p><strong>📌</strong> 이 코드에서 <code>sleep(1)</code>을 사용하는 이유는 두 스레드(Thread)가 각자 첫 번째 뮤텍스(Mutex)를 획득한 뒤 두 번째 뮤텍스(Mutex)를 요청하는 타이밍을 의도적으로 엇갈리게 만들어 교착상태(Deadlock)가 확실히 발생하도록 유도하기 위함이다. 이 교착상태(Deadlock)를 해결하는 가장 간단한 방법은 앞서 배운 것처럼 <strong>두 스레드(Thread)가 뮤텍스(Mutex)를 항상 같은 순서로 획득하도록 코드를 수정</strong>하는 것이다. 예를 들어 스레드2(Thread 2)도 m1을 먼저 잠그고 m2를 잠그도록 순서를 통일하면 순환 대기(Circular Wait) 조건이 깨져 교착상태(Deadlock)가 발생하지 않게된다.</p>
</blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/e12ed5ba-0bc3-40ae-b422-635a472b57a4.png" alt="" style="display:block;margin:0 auto" />

<h3>교착상태(Deadlock) 발생 코드 실습 결과</h3>
<p>이 코드에는 두 개의 뮤텍스(Mutex) m1과 m2가 있으며, 두 스레드(Thread)가 이 자원들을 서로 다른 순서로 획득하도록 작성되어 있다. T1은 <code>pthread_mutex_lock(&amp;m1); → sleep(1); → pthread_mutex_lock(&amp;m2);</code> 순서로, T2는 <code>pthread_mutex_lock(&amp;m2); → sleep(1); → pthread_mutex_lock(&amp;m1);</code>순서로 자원을 요청한다. 즉 두 스레드(Thread)가 서로 반대 순서로 자원을 요청하는 구조이다. 실행하게 되면 출력 순서는 스레드(Thread)의 실행 순서에 따라 달라지게 된다. 스레드1(Thread 1)의 Lock m1 → 스레드2(Thread 2)의 Lock m2 순으로 출력될 수도 있고, 반대로 스레드2(Thread 2)가 먼저 출력될 수도 있다. 여기서 중요한 점은 각 스레드(Thread)가 서로 다른 뮤텍스(Mutex)를 먼저 점유한 상태에서 다른 자원을 추가로 요청한다는 것이다. 이후 스레드2(Thread 2)의 m1 요청과 스레드1(Thread 1)의 m2 요청 출력까지만 보인 채 커서가 멈춰있는 것을 확인할 수 있는데, 이는 실행이 더 이상 진행되지 않고 멈춰있다는 것을 의미한다. 스레드1(Thread 1)이 m1을 가진 채로 m2가 풀리기를 기다리고, 스레드2(Thread 2)는 m2를 가진 채로 m1이 풀리기를 기다리는 순환 대기(Circular Wait) 구조가 만들어져 교착상태(Deadlock)가 발생한 것이다. 이를 해제하기 위해서는 <code>Ctrl + C</code>를 눌러 강제로 종료해야 한다. 이 코드는 자원 획득 순서가 엇갈리게 되면 프로그램 실행이 실제로 멈출 수 있다는 것을 직접 확인할 수 있는 예제이다.  </p>
<blockquote>
<p>📌Ctrl + C는 운영체제(Operating System)가 프로세스(Process)에 SIGINT 시그널(Signal)을 보내 강제 종료하는 방식으로, 교착상태(Deadlock)가 발생한 프로그램을 외부에서 강제로 종료하는 대표적인 방법이다.</p>
</blockquote>
<hr />
<h3>교착상태(Deadlock) 해결 코드 분석; 자원 획득 순서 통일</h3>
<p>이 코드는 두 스레드(Thread) 모두 동일한 순서로 뮤텍스(Mutex)를 획득하도록 수정된 예제이다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/d8a8341f-5a75-4c16-82cf-cb03c676426e.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/08b1d6f2-f07f-4ae2-8349-9138c28c39e5.png" alt="" style="display:block;margin:0 auto" />

<p>스레드1(Thread 1)에서는 <code>pthread_mutex_lock(&amp;m1);</code>으로 m1을 먼저 잠그고, <code>pthread_mutex_lock(&amp;m2);</code>으로 m2를 잠근다. 스레드2(Thread 2)도 마찬가지로 <code>pthread_mutex_lock(&amp;m1);</code>으로 m1을 먼저 잠그고, <code>pthread_mutex_lock(&amp;m2);</code>으로 m2를 잠근다. 즉 두 스레드(Thread) 모두 같은 순서로 자원을 요청하게 된다.</p>
<p>이렇게 되면 한 스레드(Thread)가 m1을 점유한 상태에서 다른 스레드(Thread)는 m1을 얻기 위해 대기하게 되므로, m1과 m2를 동시에 나누어 점유하는 상황 자체가 발생하지 않는다. 실행 결과를 보면 두 스레드(Thread)가 서로를 기다리며 멈추는 것이 아니라 순서대로 정상적으로 실행되며, 교착상태(Deadlock) 없이 모든 작업이 끝까지 진행된다.</p>
<p>이 실습의 핵심은 <strong>자원 획득 순서를 통일하는 것만으로도 교착상태(Deadlock)를 효과적으로 방지할 수 있다</strong>는 것이다.  </p>
<blockquote>
<p><strong>📌</strong> 이 해결 방법은 앞서 배운 교착상태(Deadlock) 예방(Prevention) 전략 중 <strong>순환 대기(Circular Wait) 조건을 제거</strong>하는 방식에 해당한다. 자원에 순서를 부여하고 모든 스레드(Thread)가 그 순서를 따르도록 강제함으로써 순환 구조 자체가 형성되지 않도록 한 것이다. 이처럼 교착상태(Deadlock) 해결은 복잡한 알고리즘 없이도 설계 단계에서 간단한 규칙을 적용하는 것만으로도 효과적으로 예방할 수 있게된다.</p>
</blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/ae322e16-5da2-445a-a956-4e73bcdf2efb.png" alt="" style="display:block;margin:0 auto" />

<h3>교착상태(Deadlock) 해결 코드 실습 결과</h3>
<p>앞의 코드와 비교했을 때 가장 중요한 차이점은 <strong>두 스레드(Thread)가 자원을 획득하는 순서를 동일하게 맞췄다</strong>는 것이다. T1을 보면 <code>pthread_mutex_lock(&amp;m1);</code>으로 m1을 먼저 잠근 뒤 <code>sleep(1)</code>로 잠시 대기하고, 그 다음 <code>pthread_mutex_lock(&amp;m2);</code>로 m2를 잠근다. 스레드2(Thread 2)도 마찬가지로 <code>pthread_mutex_lock(&amp;m1);</code>으로 m1을 먼저 잠그고, 그 다음 m2를 잠그도록 작성되어 있다. 즉 두 스레드(Thread)는 항상 같은 순서로 자원을 요청하고 있다.</p>
<p>이렇게 하면 한 스레드(Thread)가 m1을 점유하고 있을 때 다른 스레드(Thread)는 처음 단계에서부터 m1을 기다리게 되기 때문에, 서로 다른 자원을 하나씩 나눠 가진 채 상대방의 자원을 기다리는 상황 자체가 만들어지지 않는다. 따라서 <strong>순환 대기(Circular Wait) 구조가 형성되지 않아 교착상태(Deadlock)가 발생하지 않게 된다.</strong></p>
<p>실행 결과를 보면 한 스레드(Thread)가 m1과 m2를 차례대로 획득한 후 작업을 마치고, 그 다음 스레드(Thread)가 같은 순서로 실행되는 것을 확인할 수 있다. 프로그램이 중간에 멈추지 않고 끝까지 정상적으로 실행된다는 점에서 <strong>자원 획득 순서를 통일하는 것만으로도 교착상태(Deadlock)를 효과적으로 방지할 수 있다</strong>는 것을 확인할 수 있는 예제였다.  </p>
<blockquote>
<p><strong>📌</strong> 이 해결 방법은 교착상태(Deadlock) 예방(Prevention) 전략 중 <strong>순환 대기(Circular Wait) 조건을 제거</strong>하는 방식에 해당하며, 실제 현업에서도 가장 널리 사용되는 교착상태(Deadlock) 방지 기법 중 하나이다. 복잡한 알고리즘 없이도 자원 획득 순서를 일관되게 유지하는 것만으로 교착상태(Deadlock)를 원천적으로 차단할 수 있다는 점에서 설계 단계에서부터 반드시 고려해야 할 원칙이다.</p>
</blockquote>
<hr />
]]></content:encoded></item><item><title><![CDATA[Linux Programming Environment Setup]]></title><description><![CDATA[In this session, we'll take a brief look at what needs to be installed and what to keep in mind when programming on Linux.
We'll explore how to use C, C++, Python, and Java on Linux. By the end of thi]]></description><link>https://heesu.tech/linux-programming-environment-setup</link><guid isPermaLink="true">https://heesu.tech/linux-programming-environment-setup</guid><dc:creator><![CDATA[Heesu Noh]]></dc:creator><pubDate>Sun, 26 Apr 2026 20:21:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/01edb9c4-e431-42cc-b545-7219205bfad6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this session, we'll take a brief look at what needs to be installed and what to keep in mind when programming on Linux.</p>
<p>We'll explore how to use C, C++, Python, and Java on Linux. By the end of this lesson, you'll be able to set up a programming environment on Linux and know how to proceed after installation. Finally, we'll write some simple code and verify that it runs correctly on Linux.</p>
<p>리눅스에서 프로그래밍을 할 때 기본적으로 설치해야 할 것들과 무엇을 신경 써야 할지 간략하게 알아본다.</p>
<p>C, C++, Python, Java를 리눅스에서 사용하려면 어떻게 해야 하는지 살펴볼 예정이다. 이 수업을 마치고 나면 리눅스에서 프로그래밍 환경을 구성할 수 있게 될 것이고, 설치 후에는 어떻게 진행해야 할지도 알게 될 것이다. 마지막으로 아주 간단한 코드를 작성해 리눅스에서도 정상적으로 동작하는 것을 직접 확인해 볼 것이다.</p>
<hr />
<p>1️⃣ Programming Environment<br />2️⃣ Programming (C, C++, Python, Java)</p>
<hr />
<h1>1️⃣ Programming Environment</h1>
<p>The program development process generally follows these steps:</p>
<ol>
<li><p><strong>Requirements definition &amp; analysis</strong> — Define and analyze what needs to be built.</p>
</li>
<li><p><strong>Design</strong> — Determine how the solution will be implemented.</p>
</li>
<li><p><strong>Implementation</strong> — Write the actual code based on the design.</p>
</li>
<li><p><strong>Verification</strong> — Test whether the program behaves as intended.</p>
</li>
<li><p><strong>Deployment</strong> — Install and release the completed program in a real environment.</p>
</li>
<li><p><strong>Maintenance</strong> — An ongoing phase where issues are identified and improvements are made through actual use.</p>
</li>
</ol>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/14cd9922-6fe6-4f7b-84c2-edeb2886e509.png" alt="" style="display:block;margin:0 auto" />

<p>기본적으로 프로그램을 개발할 때는 아래와 같은 순서로 이루어진다.</p>
<ol>
<li><p><strong>요구사항 정의 및 분석</strong> — 무엇을 만들어야 할지 요구사항을 정하고 분석한다.</p>
</li>
<li><p><strong>설계</strong> — 어떻게 구현할지 방향을 정하는 단계이다.</p>
</li>
<li><p><strong>구현</strong> — 설계에 맞춰 실제 코드를 작성한다.</p>
</li>
<li><p><strong>검증</strong> — 의도한 대로 동작하는지 확인하는 테스트 단계이다.</p>
</li>
<li><p><strong>배포</strong> — 완성된 프로그램을 실제 환경에 설치하고 배포한다.</p>
</li>
<li><p><strong>유지보수</strong> — 실제로 사용하면서 발생하는 문제를 보완하고 개선해 나가는 지속적인 단계이다.</p>
</li>
</ol>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/d2d83d73-8952-4094-8640-8b5011212571.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>Software Development Tools on Ubuntu</strong></h3>
<p>Linux distributions can be broadly divided into two families: Red Hat-based and Debian-based. Red Hat-based distributions are widely used in enterprise environments due to their commercial polish, while developers tend to prefer Debian-based distributions — Ubuntu in particular. While this is difficult to quantify precisely, it can be observed indirectly by the fact that installation guides for new open-source tools are often written with Ubuntu as the reference platform.</p>
<p>On Ubuntu, packages are installed using the <code>apt</code> package manager. The <code>sudo apt install</code> command makes it straightforward to install any desired package. To set up a complete C/C++ development environment in one step, use the following command:</p>
<pre><code class="language-shell">sudo apt install build-essential
</code></pre>
<p>This meta-package includes everything needed for C/C++ development: the C compiler <code>gcc</code>, the C++ compiler <code>g++</code>, the build automation tool <code>make</code>, the C standard library headers <code>libc6-dev</code>, and the Debian package development tool <code>dpkg-dev</code>.</p>
<p>The reason Python and Java are not included in <code>build-essential</code> comes down to history and philosophy. The Linux kernel itself is written in C, and C has traditionally been the foundation of system-level development. In other words, "essential" is defined with C at its core. Python and Java must be installed separately as needed.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/e6676958-83b5-4df1-a681-b8ef225ce6a6.png" alt="" style="display:block;margin:0 auto" />

<h3>우분투 기반 소프트웨어 개발 도구</h3>
<p>리눅스 배포판은 크게 레드햇 계열과 데비안 계열로 나눌 수 있다. 레드햇 계열은 상업적 완성도가 높아 기업 서비스 환경에서 많이 사용되는 반면, 개발자들 사이에서는 데비안 계열, 그 중에서도 우분투를 선호하는 경향이 있다. 이를 수치로 정확히 확인하기는 어렵지만, 새로운 오픈소스 도구들의 설치 가이드를 보면 우분투를 기준으로 작성된 경우가 많다는 점에서 이런 경향을 간접적으로 확인할 수 있다.</p>
<p>우분투에서 패키지를 설치할 때는 <code>apt</code> 패키지 관리자를 사용하며, <code>sudo apt install</code> 명령어를 통해 원하는 패키지를 간편하게 설치할 수 있다. C/C++ 개발 환경을 한 번에 구성할 때는 <code>sudo apt install build-essential</code> 명령어를 사용한다. 이 메타 패키지는 C 컴파일러인 <code>gcc</code>, C++ 컴파일러인 <code>g++</code>, 빌드 자동화 도구인 <code>make</code>, C 표준 라이브러리 헤더인 <code>libc6-dev</code>, 데비안 패키지 개발 도구인 <code>dpkg-dev</code> 등을 포함하고 있다.</p>
<p><code>build-essential</code>에 Python이나 Java가 포함되지 않는 이유는 리눅스 커널 자체가 C로 작성되어 있고, 전통적으로 시스템 개발의 근간이 C이기 때문이다. 즉, "에센셜"의 기준이 C 중심으로 정의되어 있으며, Python이나 Java는 필요에 따라 별도로 설치해야 한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/05fd5b42-2521-489d-a97e-5b3ceda768ec.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>GDB Debugger</strong></h3>
<p>The use of debuggers has declined significantly compared to the past. In earlier days, hardware performance was limited and development machines were scarce, meaning that simply compiling and running code could take a considerable amount of time. In that environment, debuggers were an essential tool — stepping through code line by line was far more practical than restarting from scratch every time something went wrong. Today, however, hardware performance has improved dramatically, and it is often sufficient to trace issues by inserting print statements at key points in the code. That said, there are still situations where a debugger proves invaluable, so it is worth knowing the basics.</p>
<p>To use GDB, first install it, then compile your code with the <code>-g</code> flag. This option embeds symbol information and debugging data directly into the binary, giving the debugger the context it needs to trace execution. It also means that information irrelevant to the end user ends up inside the file. As debugging data accumulates, file size grows and performance can be affected — so the <code>-g</code> flag should be used strictly for debugging purposes and must be removed before a production release. It is not uncommon for developers to overlook this in a rush, resulting in a release binary that is unnecessarily large and underperforming, so this is a point worth keeping in mind.</p>
<p>Once compilation is complete, pass the resulting binary to GDB as a parameter to launch it. This drops you into a GDB-specific shell environment, where you can step through the code and trace execution flow using GDB commands.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/e1ef10e7-7725-4a94-84d2-dd86d89d7dec.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/6d03e55b-eedf-47cc-ac2b-b7671971b945.png" alt="" style="display:block;margin:0 auto" />

<h3>GDB 디버거</h3>
<p>요즘은 디버거를 사용하는 경우가 예전에 비해 많이 줄었다. 과거에는 하드웨어 성능이 낮고 개발 장비도 충분하지 않았기 때문에, 코드를 컴파일하고 실행하는 데만도 상당한 시간이 소요됐다. 그런 환경에서는 문제가 생겼을 때 처음부터 다시 실행하는 것보다 단계별로 흐름을 추적할 수 있는 디버거가 필수적인 도구였다. 하지만 현재는 하드웨어 성능이 크게 향상되어 코드를 빠르게 실행할 수 있고, 중간중간 출력문을 삽입하는 방식으로도 충분히 문제를 확인할 수 있게 되었다. 그런 이유로 디버거의 사용 빈도는 줄었지만, 상황에 따라 필요한 순간이 올 수 있으므로 기본적인 사용법은 알아두는 것이 좋다.</p>
<p>GDB를 사용하려면 먼저 설치 후, 컴파일 시 <code>-g</code> 옵션을 추가해야 한다. 이 옵션을 붙이면 디버거가 활용할 수 있는 심볼 정보와 디버깅 데이터가 바이너리에 함께 포함되어 컴파일된다. 이는 곧 실제 서비스에서는 불필요한 정보가 파일에 포함된다는 의미이기도 하다. 디버깅 정보가 누적되면 파일 크기가 커지고 성능에도 영향을 미치기 때문에, <code>-g</code> 옵션은 디버깅 목적으로만 사용하고 최종 배포 시에는 반드시 제외해야 한다. 간혹 급하게 작업하다가 이 부분을 놓쳐 배포 버전의 파일이 불필요하게 크고 성능이 저하되는 경우가 드물게 발생하기도 하므로 주의가 필요하다.</p>
<p>컴파일이 완료되면 생성된 바이너리 파일을 GDB의 파라미터로 전달하여 실행한다. 그러면 GDB 전용 셸 환경으로 진입하게 되며, 이후 명령어를 통해 코드를 단계별로 실행하며 흐름을 추적할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/51d639e8-7af6-4fe3-bf35-661dc61e4d90.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>Valgrind</strong></h3>
<p>Like GDB, Valgrind is a tool used for debugging. After installation, it requires a specific compile option to be used effectively. The option used when compiling for Valgrind is <code>-O0</code>. Here, <code>O</code> stands for Optimization, and the number that follows indicates the level of optimization. <code>0</code> means that no optimization will be applied at all.</p>
<p>When optimization is enabled, the compiler restructures the code to improve runtime performance and reduce file size. However, in a debugging context, optimization can actually get in the way. During the optimization process, the execution flow may be reordered and certain variables may be eliminated, creating a gap between the code as written by the developer and the code as it actually runs. For this reason, when debugging with Valgrind, the <code>-O0</code> option is used to disable optimization entirely, allowing the tool to analyze the code exactly as it was written.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/b8f680ff-46ba-4b50-8223-ad3d3e263212.png" alt="" style="display:block;margin:0 auto" />

<h3>Valgrind</h3>
<p>Valgrind는 GDB와 마찬가지로 디버깅에 활용되는 도구로, 설치 후 컴파일 시 별도의 옵션을 지정하여 사용한다. Valgrind에서 사용하는 컴파일 옵션은 <code>-O0</code>이다. 여기서 <code>O</code>는 최적화(Optimization)를 의미하며, 뒤에 붙는 숫자가 최적화 수준을 나타낸다. <code>0</code>은 최적화를 전혀 수행하지 않겠다는 의미이다.</p>
<p>최적화를 적용하면 컴파일러가 코드를 재구성하여 실행 성능이 향상되고 파일 크기도 줄어드는 이점이 있다. 그러나 디버깅 상황에서는 최적화가 오히려 방해가 된다. 최적화 과정에서 코드의 실행 흐름이 변경되거나 일부 변수가 제거될 수 있어, 개발자가 작성한 코드와 실제 실행 흐름 사이에 차이가 생기기 때문이다. 따라서 Valgrind로 디버깅할 때는 <code>-O0</code> 옵션을 통해 최적화를 비활성화한 상태로 컴파일하여 코드 본래의 흐름을 그대로 분석할 수 있도록 하는 것이다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/51d639e8-7af6-4fe3-bf35-661dc61e4d90.png" alt="" style="display:block;margin:0 auto" />

<ul>
<li><p><code>-g</code> — GDB 디버깅 정보 포함</p>
</li>
<li><p><code>-O0</code> — 최적화 비활성화 (Valgrind 분석용)</p>
</li>
<li><p><code>-std=gnu99</code> — C99 표준에 GNU 확장 기능을 포함한 규격으로 컴파일</p>
</li>
</ul>
<p><code>-std=gnu99</code>를 명시하는 이유는 컴파일러가 어떤 C 표준을 기준으로 코드를 해석할지 명확하게 지정하기 위함이다. 지정하지 않으면 컴파일러 버전에 따라 기본 표준이 달라질 수 있어, 환경에 따른 동작 차이가 생길 수 있다. 특히 Valgrind로 정확한 분석을 하려면 이처럼 컴파일 옵션을 명확하게 지정하는 것이 좋다.</p>
<hr />
<h1>2️⃣ Programming (C, C++, Python, Java)</h1>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/5b984a05-e05a-4ebc-a4f9-2436b094106f.png" alt="" style="display:block;margin:0 auto" />

<p>To install C and C++ compilers on Linux, use the following commands:</p>
<pre><code class="language-shell">sudo apt install gcc
sudo apt install g++
</code></pre>
<p><code>g++</code>, the C++ compiler, is installed in the same way. The two compilers share a number of similarities — it is possible to compile C code using <code>g++</code>. However, when using <code>gcc</code> to compile C++ code, the C++ standard library is not linked automatically, which requires additional options to be specified manually. For this reason, <code>g++</code> is the recommended compiler for C++ code.</p>
<p>It is also worth understanding the fundamental difference between a compiler and an interpreter. An interpreter executes code directly, whereas a compiler goes through an intermediate stage. In C, compiling the source code first produces an object file as an intermediate output, which is then linked to produce the final executable binary. While this appears to happen in a single step, there is an underlying process taking place behind the scenes.</p>
<p>Why is this intermediate stage necessary? A program is essentially a sequence of functions, but when writing code, there is no requirement to define functions in the exact order they are called. Organizing code by functionality rather than by execution order is far easier to manage. As a result, a function may be called before its definition appears in the source — the compiler handles this by first taking in the full picture before processing everything together.</p>
<p>The same principle applies to <code>#include</code> and header files in C. A header file simply contains declarations — it states that a function or variable with a given name exists — while the actual implementation lives in a library. In other words, the header confirms what is available, and the library provides the functionality at runtime.</p>
<p>Installation itself is straightforward and can be done with the <code>sudo apt install</code> command.</p>
<h3><strong>컴파일러</strong></h3>
<p>리눅스에서 C 컴파일러를 설치할 때는 아래 명령어를 사용한다.</p>
<pre><code class="language-shell">sudo apt install gcc
sudo apt install g++
</code></pre>
<p>마찬가지로 C++의 컴파일러인 <code>g++</code>도 동일한 방식으로 설치할 수 있다. 두 컴파일러는 서로 비슷한 면이 있는데, <code>g++</code>로 C 코드를 컴파일하는 것은 가능하다. 단, <code>gcc</code>로 C++ 코드를 컴파일하는 경우에는 C++ 표준 라이브러리를 자동으로 링크해 주지 않기 때문에 별도의 옵션이 필요하다. 따라서 C++을 컴파일할 때는 <code>g++</code>를 사용하는 것이 권장된다.</p>
<p>컴파일러와 인터프리터의 차이도 기본적으로 알아두면 좋다. 인터프리터는 코드를 바로 실행하는 반면, 컴파일러는 중간 단계를 거친다. C의 경우 소스 코드를 컴파일하면 오브젝트 파일이라는 중간 단계의 파일이 생성되고, 이를 링킹하여 최종 실행 가능한 바이너리 파일이 만들어진다. 눈에는 한 번에 진행되는 것처럼 보이지만, 내부적으로는 이러한 과정을 거친다.</p>
<p>이 중간 단계가 필요한 이유는 무엇일까? 프로그램은 함수들의 순서적 나열로 이루어져 있지만, 실제 코드를 작성할 때 반드시 실행 순서대로 함수를 정의할 필요는 없다. 시간적 순서보다 기능별로 분류하는 것이 관리하기 훨씬 편리하기 때문이다. 따라서 어떤 함수가 호출되더라도 그 정의가 뒤에 있을 수 있는데, 컴파일러는 이를 일단 무시하고 전체를 파악한 뒤 처리한다.</p>
<p>C에서 <code>#include</code>로 헤더 파일을 가져오는 것도 같은 맥락이다. 헤더 파일은 단순히 "이런 이름의 함수나 변수가 존재한다"는 선언을 담고 있으며, 실제 구현 코드는 라이브러리에 저장되어 있다. 즉, 헤더로 선언을 확인하고 실제 동작은 라이브러리에서 가져와 사용하는 구조이다.</p>
<p>설치 자체는 간단하다. <code>sudo apt install</code> 명령어로 손쉽게 진행할 수 있다.</p>
<hr />
<h3><strong>Editor Settings</strong></h3>
<p>vi is an editor that is well worth learning — it will prove useful down the line. Unlike the typical environment where a mouse and keyboard are used together, vi is a text-based editor where every operation is performed using the keyboard alone.</p>
<p>First impressions of vi are rarely positive. There is no syntax highlighting, cursor movement requires keyboard input at every step, and indentation means pressing the space bar multiple times. Fortunately, all of these inconveniences can be resolved through configuration.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/86071d48-f2e6-4802-89aa-3a61cbcb9307.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/16f1189b-778f-4c40-b385-9fe90496cea7.png" alt="" style="display:block;margin:0 auto" />

<p>Adding the following to <code>~/.vimrc</code> will apply the settings. Here is what each option does.</p>
<ul>
<li><p><code>filetype on</code> — Automatically detects the file type based on its extension, identifying whether it is C, Python, or another language, and prepares to distinguish language-specific keywords and function names accordingly.</p>
</li>
<li><p><code>filetype plugin on</code> — Loads the appropriate plugin for the detected file type by referencing files in the <code>.vim/ftplugin</code> directory, automatically applying the relevant settings on startup.</p>
</li>
<li><p><code>filetype indent on</code> — Applies indentation rules specific to the file type. In C, indentation has no effect on how the code runs, but in Python it is part of the syntax — incorrect indentation will cause the interpreter to throw an error and halt execution.</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/da12154b-a4a1-4821-81b0-a75dfe75d434.png" alt="" style="display:block;margin:0 auto" />

<ul>
<li><p><code>syntax on</code> — Applies color to the code so that syntax elements can be distinguished visually.</p>
</li>
<li><p><code>set title</code> — Displays the name of the file currently being edited in the terminal window's title bar.</p>
</li>
<li><p><code>set tabstop</code> — Specifies how many spaces wide a tab character appears. Since tab width can vary across environments, setting this value ensures a consistent tab size within vi.</p>
</li>
<li><p><code>set softtabstop</code> — Defines the width of whitespace inserted when the tab key is pressed. <code>set shiftwidth</code> sets the width applied during indentation commands such as <code>&gt;&gt;</code> and <code>&lt;&lt;</code>, or during auto-indentation. <code>set noexpandtab</code> ensures that pressing the tab key inserts an actual tab character rather than spaces. For automatic indentation after pressing Enter, the <code>autoindent</code> or <code>smartindent</code> option must also be enabled alongside these settings.</p>
</li>
</ul>
<p>One important point to keep in mind: setting tabs to 4 spaces wide on your machine does not mean they will appear 4 spaces wide in every environment. A tab character is stored as a single character, and its visual width is determined by the tab width setting of whichever environment is rendering it. This means that a file using tab characters may look different depending on where it is opened — and this is precisely why mixing tabs and spaces can cause problems in collaborative development.</p>
<h3><strong>편집기 설정</strong></h3>
<p>vi는 익혀두면 나중에 분명히 도움이 되는 편집기이다. 마우스와 키보드를 함께 사용하는 일반적인 환경과 달리, vi는 키보드 하나만으로 모든 작업을 처리하는 텍스트 기반 편집기이다.</p>
<p>처음 vi를 접하면 불편한 점이 한두 가지가 아니다. 문법 색상 구분도 없고, 커서 이동도 키보드로 일일이 해야 하며, 들여쓰기를 하려면 스페이스바를 여러 번 눌러야 하는 번거로움이 있다. 하지만 이러한 문제들은 환경 설정으로 해결할 수 있다.</p>
<p><code>~/.vimrc</code> 파일 안에 아래 내용을 추가하면 적용된다.</p>
<pre><code class="language-shell">filetype plugin indent on
syntax on
set title
set tabstop=8
set softtabstop=8
set shiftwidth=8
set noexpandtab
</code></pre>
<p>각 항목의 의미는 다음과 같다.</p>
<ul>
<li><p><code>filetype on</code>; 파일의 확장자를 통해 C인지 Python인지 등을 자동으로 인식한다. 이를 통해 언어별 예약어나 함수 이름을 구분할 준비를 한다.</p>
</li>
<li><p><code>filetype plugin on</code>; 파일 타입에 맞는 플러그인을 불러온다. <code>.vim/ftplugin</code> 디렉토리 안에 있는 파일을 참조하여, 시작 시 해당 파일 타입에 맞는 설정을 자동으로 로드한다.</p>
</li>
<li><p><code>filetype indent on</code>; 파일 타입에 맞는 들여쓰기 규칙을 적용한다. C는 들여쓰기가 없어도 동작에는 문제가 없지만, Python은 들여쓰기가 문법의 일부이기 때문에 맞지 않으면 인터프리터가 에러를 발생시키며 실행을 멈춘다.</p>
</li>
<li><p><code>syntax on</code>; 코드에 색상을 입혀 문법 요소를 시각적으로 구분할 수 있게 해준다.</p>
</li>
<li><p><code>set title</code>은 현재 편집 중인 파일 이름을 터미널 창의 제목 표시줄에 보여주는 옵션이다.</p>
</li>
<li><p><code>set tabstop</code>은 탭 키를 눌렀을 때 몇 칸을 띄울지 지정하는 옵션이다. 원래 탭의 폭은 환경마다 다를 수 있는데, 이 값을 설정해두면 vi에서 탭 간격을 일정하게 유지할 수 있다.</p>
</li>
<li><p><code>set softtabstop</code>은 탭 키를 눌렀을 때 삽입되는 공백의 폭을 지정하고, <code>set shiftwidth</code>는 <code>&gt;&gt;</code>, <code>&lt;&lt;</code> 등의 들여쓰기 명령이나 자동 들여쓰기 시 적용되는 폭을 설정한다. <code>set noexpandtab</code>은 탭 키를 눌렀을 때 스페이스 대신 실제 탭 문자가 입력되도록 한다. 엔터 후 자동 들여쓰기는 이 옵션들과 함께 <code>autoindent</code> 또는 <code>smartindent</code> 옵션이 함께 동작해야 적용된다.</p>
</li>
</ul>
<p>여기서 한 가지 주의할 점이 있다. 내가 탭을 4칸으로 설정해두었다고 해서 다른 환경에서도 똑같이 4칸으로 보이는 것은 아니다. 탭 문자는 실제로 "탭"이라는 하나의 문자로 저장되며, 화면에 보이는 폭은 각 환경에서 설정한 탭 폭에 따라 달라진다. 따라서 탭 문자를 사용한 파일은 환경에 따라 들여쓰기가 다르게 보일 수 있다. 이것이 바로 협업 시 탭과 스페이스의 혼용이 문제가 되는 이유이기도 하다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/aa94eea6-6fe1-4293-95fc-0c47fc85da63.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>Default Editor Configuration</strong></h3>
<p>The following command allows you to select which editor the system uses by default.</p>
<pre><code class="language-shell">sudo update-alternatives --config editor
</code></pre>
<p>Running this command brings up a list of available editors, as shown in the example. In this case, five editors are listed from 0 to 4, with <code>vim.basic</code> and <code>vim.tiny</code> among the options. The current default is marked with <code>*</code> at option 0, and entering the corresponding number sets that editor as the default.</p>
<p>The reason this configuration matters is as follows. When an editor is explicitly specified — such as <code>nano filename</code> or <code>vim filename</code> — there is no issue. However, there are situations where the system internally invokes an editor automatically, and in those cases, whichever editor has been set as the default is the one that will be launched. It works on the same principle as a smartphone: even if multiple browsers or media players are installed, the one set as the default app is what opens automatically.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/23fadb80-dd66-4406-8e78-31e31697c175.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>기본 편집기 설정</strong></h3>
<p>아래 명령어를 사용하면 시스템에서 기본으로 사용할 편집기를 직접 선택할 수 있다.</p>
<pre><code class="language-shell">sudo update-alternatives --config editor
</code></pre>
<p>명령을 실행하면 사진과 같이 선택 가능한 편집기 목록이 나타난다. 현재 예시에서는 0번부터 4번까지 총 5가지 편집기가 표시되며, 그 중 <code>vim.basic</code>과 <code>vim.tiny</code>도 확인할 수 있다. 현재 기본값은 <code>*</code> 표시가 된 0번이며, 원하는 번호를 입력하면 해당 편집기가 기본으로 설정된다.</p>
<p>이 설정이 필요한 이유는 다음과 같다. <code>nano 파일명</code>, <code>vim 파일명</code>처럼 편집기를 명시적으로 지정해서 실행하는 경우에는 문제가 없다. 하지만 시스템 내부에서 편집기가 자동으로 호출되는 경우에는 기본 편집기로 지정된 프로그램이 실행된다. 스마트폰에서 여러 브라우저나 미디어 플레이어가 설치되어 있어도 기본 앱으로 지정된 것이 자동으로 실행되는 것과 같은 원리이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/54793c1c-f1c0-40c2-a358-e422c99c8fbe.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/b65b0622-2434-4328-9c20-b859f5d71222.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>Build Automation with make</strong></h3>
<p>As a project grows in size, it becomes increasingly difficult to manage everything within a single file. While a project may start out as a single source file, additional files are introduced and organized by functionality as the codebase expands. At that point, running <code>gcc</code> manually for each file becomes tedious and inefficient. <code>make</code> is a build automation tool designed to solve exactly this problem, allowing multiple files to be managed and built together in a single step.</p>
<h3><strong>Makefile Rule Basics</strong></h3>
<p><code>make</code> operates using rules defined in a file called <code>Makefile</code>. Each rule follows this structure:</p>
<pre><code class="language-plaintext">target ... : prerequisites ...
	recipe
	...
</code></pre>
<ul>
<li><p><code>target</code> The output to be produced, such as an executable or object file.</p>
</li>
<li><p><code>prerequisites</code> The files that must exist before the target can be built.</p>
</li>
<li><p><code>recipe</code> The command to run in order to produce the target.</p>
</li>
</ul>
<p>One critical rule to remember: every recipe line must begin with a <strong>tab character</strong>. Using spaces instead will cause <code>make</code> to fail to recognize the line as a recipe and produce an error.</p>
<p><code>make</code> also only re-runs a recipe when a prerequisite file has been modified more recently than the target. If nothing has changed, the build step is skipped. This is one of the key advantages of <code>make</code> — it avoids unnecessary recompilation and saves time as projects grow larger.</p>
<h3><strong>자동화 도구 make</strong></h3>
<p>프로젝트 규모가 커지면 하나의 파일로 프로그램을 완성하기 어려워진다. 초기에는 단일 파일로 작성하더라도 기능이 늘어날수록 파일을 여러 개로 나누어 관리하게 된다. 이때 파일마다 일일이 <code>gcc</code>를 실행하는 것은 번거롭고 비효율적이다. <code>make</code>는 이러한 문제를 해결해 주는 자동화 도구로, 여러 파일을 한 번에 관리하고 빌드할 수 있게 해준다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/53fb46ea-657b-41e9-9532-b5c3c13e82bc.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>Makefile rule 기초</strong></h3>
<p><code>make</code>는 기본적으로 아래와 같은 형식의 rule을 사용한다.</p>
<pre><code class="language-shell">target ... : prerequisites ...
	recipe
	...
</code></pre>
<ul>
<li><p><code>target</code> 만들고자 하는 결과물(실행 파일 또는 오브젝트 파일 등)이다.</p>
</li>
<li><p><code>prerequisites</code> target을 만들기 위해 필요한 선행 파일들이다.</p>
</li>
<li><p><code>recipe</code> target을 만들기 위해 실행할 명령어이다.</p>
</li>
</ul>
<p>여기서 반드시 주의해야 할 점이 있다. recipe 줄은 반드시 <strong>탭(Tab) 문자</strong>로 시작해야 한다. 스페이스바로 맞춰 쓰면 <code>make</code>가 이를 recipe로 인식하지 못하고 오류가 발생한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/03362726-f7f1-4384-9a52-8d57801a57b9.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>Makefile Example</strong></h3>
<p>To look at a real example, open the following Makefile:</p>
<pre><code class="language-shell">vi /usr/share/vim/vim91/macros/maze/Makefile
</code></pre>
<p>Note that <code>vim91</code> in the path may vary depending on the version of vim installed. If you have installed it yourself, you should be able to find it at a similar path.</p>
<p>The numbers on the left side of the screen are line numbers — the same kind commonly seen in an IDE, displayed to make it easy to identify where in the code you are. Line numbers can be configured to appear automatically in vi's settings.</p>
<p>Looking at the content, line 1 contains <code># It's simple...</code>, which is a comment. Line 3 shows <code>maze: mazeansi.c</code>, and line 4 contains <code>cc -o maze mazeansi.c</code>, indented with a single tab. This tells <code>make</code> to compile <code>mazeansi.c</code> and produce an executable named <code>maze</code>.</p>
<p>There is an important concept to understand here. <code>make</code> only runs the recipe when a file listed in the prerequisites has been modified more recently than the target. In other words, <code>mazeansi.c</code> will only be recompiled if it has changed since the last build — if nothing has been modified, the compilation step is skipped entirely. This is one of the core strengths of <code>make</code>.</p>
<p>It may feel somewhat complex at first, but the more you write and use Makefiles yourself, the more natural and convenient the tool becomes.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/750f4ee6-fa87-43da-b442-d2ba4a38f30d.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>Makefile 예제</strong></h3>
<p>실제 예제를 살펴보기 위해 아래 경로의 Makefile을 열어본다.</p>
<pre><code class="language-shell">vi /usr/share/vim/vim91/macros/maze/Makefile
</code></pre>
<p>경로 중 <code>vim91</code> 부분은 설치된 vim 버전에 따라 다를 수 있다. 직접 설치했다면 비슷한 경로에서 찾을 수 있을 것이다.</p>
<p>예제 화면에서 보이는 좌측의 숫자는 코드의 줄 번호이다. 통합 개발 환경(IDE)에서 흔히 볼 수 있는 것처럼, 코드의 위치를 쉽게 파악하기 위해 표시된다. vi 환경 설정에서 줄 번호가 자동으로 표시되도록 설정할 수 있다.</p>
<p>예제 내용을 살펴보면, 1번 줄의 <code># It's simple...</code>은 주석이다. 3번 줄에는 <code>maze: mazeansi.c</code>가 있고, 4번 줄은 탭 키로 한 번 들여쓴 후 <code>cc -o maze mazeansi.c</code> 명령이 작성되어 있다. 이는 <code>mazeansi.c</code> 파일을 컴파일하여 <code>maze</code>라는 실행 파일을 만들라는 의미이다.</p>
<p>여기서 중요한 개념이 하나 있다. <code>make</code>는 prerequisites에 명시된 파일이 target보다 더 최근에 수정되었을 때만 recipe를 실행한다. 즉, <code>mazeansi.c</code>가 수정된 경우에만 다시 컴파일하고, 변경이 없다면 불필요한 컴파일을 건너뛴다. 이것이 <code>make</code>의 핵심 장점 중 하나이다.</p>
<p>처음에는 다소 복잡하게 느껴질 수 있지만, Makefile을 직접 작성하고 사용해 보다 보면 얼마나 편리한 도구인지 자연스럽게 체감하게 될 것이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/bc6d8582-baae-4fc8-8d08-fbf710578532.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>C Compilation Practice</strong></h3>
<p>The teacher recommended installing Linux in a server environment. With a server installation, the monitor displays nothing but text on a black screen — there are no windows to open side by side, and the kind of zooming and resizing familiar from graphical environments is not available. When vi is launched, it fills the entire screen, which means the typical workflow involves exiting vi, running the compiler from the command line, then returning to vi once the check is done — a repetitive back-and-forth that quickly becomes inconvenient.</p>
<p>There are a few ways to work around this. In a Linux server environment, pressing <code>Alt + number</code> at the login screen after booting switches between virtual terminals such as <code>tty1</code> and <code>tty2</code>. Up to six independent login sessions can be used simultaneously, so this is a handy option when the single-screen constraint feels limiting.</p>
<p>That said, there is an even more convenient approach that allows compilation without ever leaving vi. In vi's command mode, typing <code>:</code> opens an internal command prompt, and adding <code>!</code> before a command executes it directly in the shell without exiting vi.</p>
<p>This leads to the following command:</p>
<pre><code class="language-shell">:!gcc % -o %&lt;
</code></pre>
<p>Here, <code>%</code> represents the full name of the file currently being edited, and <code>%&lt;</code> represents the filename without its extension. So if <code>helloWorld.c</code> is open, the command above is equivalent to:</p>
<pre><code class="language-shell">gcc helloWorld.c -o helloWorld
</code></pre>
<p>If compilation succeeds, only <code>Press ENTER or type command to continue</code> appears with no error messages. Pressing Enter returns to the vi screen.</p>
<p>The resulting executable can then be run immediately with:</p>
<pre><code class="language-shell">:!./helloWorld
</code></pre>
<p>This outputs <code>Hello, World.</code>, followed again by <code>Press ENTER or type command to continue</code>. Pressing Enter brings you back to the vi editing screen.</p>
<p>As a side note, if line numbers are not visible in vi, entering the following in command mode will enable them:</p>
<pre><code class="language-shell">:set number
</code></pre>
<p>Line numbers are purely a visual aid for navigating the code and have no effect on the actual file content.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/c40d0f34-d5d3-46e6-91f8-0093f52c12e7.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/4d5cdd2f-bb63-4b94-bf86-c78b9c433b6f.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>C 컴파일 실습</strong></h3>
<p>교수님은 서버 환경으로 리눅스를 설치하는 것을 권장했다. 서버 버전을 설치하면 모니터에는 검은 화면에 글자만 표시된다. 윈도우처럼 여러 창을 띄워 선택적으로 확대·축소하는 것이 불가능하다. vi를 실행하면 그 내용이 화면 전체를 채우기 때문에, 일반적으로는 vi를 종료한 후 명령창에서 컴파일하고, 확인이 끝나면 다시 vi로 돌아와야 하는 번거로움이 있다.</p>
<p>이에 대한 해결 방법이 몇 가지 있다. 먼저 리눅스 서버 환경에서는 부팅 후 로그인 화면에서 <code>Alt + 숫자</code>를 누르면 <code>tty1</code>, <code>tty2</code>와 같이 가상 터미널이 전환된다. 최대 6개까지 독립적인 로그인 세션을 사용할 수 있으므로, 답답하게 느껴질 때 이 방법을 활용하면 된다.</p>
<p>그러나 하나의 터미널 안에서 vi를 벗어나지 않고 컴파일까지 처리하는 더 편리한 방법이 있다. vi의 명령 모드에서 <code>:</code>을 입력하면 내부 명령을 실행할 수 있는데, 여기서 <code>!</code>를 붙이면 vi 환경을 벗어나 셸 명령을 직접 실행할 수 있다.</p>
<p>이를 활용한 명령어가 바로 아래와 같다.</p>
<pre><code class="language-shell">:!gcc % -o %&lt;
</code></pre>
<p>여기서 <code>%</code>는 현재 편집 중인 파일의 이름 전체를, <code>%&lt;</code>는 확장자를 제외한 파일 이름을 의미한다. 즉, <code>helloWorld.c</code>를 편집 중이라면 위 명령은 다음과 동일하다.</p>
<pre><code class="language-plaintext">gcc helloWorld.c -o helloWorld
</code></pre>
<p>컴파일이 성공하면 <code>Press ENTER or type command to continue</code>만 표시되고 오류 메시지가 없다. 엔터를 누르면 vi 화면으로 돌아온다.</p>
<p>이후 아래 명령으로 실행 파일을 바로 실행할 수 있다.</p>
<pre><code class="language-shell">:!./helloWorld
</code></pre>
<p>실행하면 <code>Hello, World.</code>가 출력되고, 다시 <code>Press ENTER or type command to continue</code>가 나타난다. 엔터를 누르면 vi 편집 화면으로 복귀한다.</p>
<p>참고로 vi에서 줄 번호가 보이지 않는다면 명령 모드에서 아래와 같이 입력하면 줄 번호가 표시된다.</p>
<pre><code class="language-shell">:set number
</code></pre>
<p>줄 번호는 코드 위치를 파악하기 위한 시각적 요소일 뿐, 실제 파일 내용에는 영향을 주지 않는다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f96cd2bd-fec0-484a-8803-f844f0f04a1a.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/7fed8755-c0c7-4fb6-9d78-a111fa7df606.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>Compilation with a Single File</strong></h3>
<p>This section covers a more convenient way to compile when there is only one file involved. Add the following to <code>~/.vimrc</code>:</p>
<pre><code class="language-shell">autocmd FileType c setlocal makeprg=gcc\ -Wall\ -g\ %\ -o\ %
autocmd FileType cpp setlocal makeprg=g++\ -Wall\ -g\ %\ -o\ %
</code></pre>
<p><code>autocmd FileType</code> automatically runs a command based on the file type. Files with a <code>.c</code> extension will use <code>gcc</code>, while <code>.cpp</code> files will use <code>g++</code>. <code>setlocal</code> means the setting applies only to the current file. As covered earlier, <code>%</code> refers to the full filename and <code>%&lt;</code> refers to the filename without its extension.</p>
<p>With this configuration in place, there is no need to type <code>:!gcc % -o %&lt;</code> every time. Compilation can be triggered from within vi using just:</p>
<pre><code class="language-shell">:make
</code></pre>
<p>If compilation succeeds, <code>Press ENTER or type command to continue</code> is displayed, and pressing Enter returns to vi. The resulting executable can then be run with:</p>
<pre><code class="language-plaintext">:!./helloWorld
</code></pre>
<p>This is a practical setup that eliminates the repetitive typing of lengthy compile commands when working with a single file.</p>
<h3><strong>컴파일 2; 파일이 하나일 때</strong></h3>
<p>이번에는 파일이 하나만 있을 때 더 간편하게 컴파일하는 방법이다. <code>~/.vimrc</code>에 아래 내용을 추가한다.</p>
<pre><code class="language-shell">autocmd FileType c setlocal makeprg=gcc\ -Wall\ -g\ %\ -o\ %
autocmd FileType cpp setlocal makeprg=g++\ -Wall\ -g\ %\ -o\ %
</code></pre>
<p><code>autocmd FileType</code>은 파일 타입에 따라 자동으로 명령을 실행하는 설정이다. 확장자가 <code>.c</code>이면 <code>gcc</code>를, <code>.cpp</code>이면 <code>g++</code>를 사용하도록 각각 지정한다. <code>setlocal</code>은 해당 설정을 현재 파일에만 적용한다는 의미이다. <code>%</code>와 <code>%&lt;</code>는 앞서 배운 것처럼 각각 현재 파일 이름과 확장자를 제외한 파일 이름을 의미한다.</p>
<p>이 설정을 해두면 이전처럼 <code>:!gcc % -o %&lt;</code>를 매번 입력할 필요 없이, vi 안에서 아래 명령만으로 컴파일이 가능하다.</p>
<pre><code class="language-shell">:make
</code></pre>
<p>컴파일이 성공하면 <code>Press ENTER or type command to continue</code>가 표시되고, 엔터를 누르면 vi로 돌아온다. 이후 실행은 동일하게 아래 명령으로 할 수 있다.</p>
<pre><code class="language-plaintext">:!./helloWorld
</code></pre>
<p>파일이 하나일 때 매번 긴 컴파일 명령을 입력하는 번거로움을 줄여주는 유용한 설정이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/eb22e141-d9eb-49dd-b487-41a64ed96ff6.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>Compilation Errors</strong></h3>
<p>The following example demonstrates what happens when an error occurs during compilation. In the code shown, line 4 contains <code>std::count</code>, when the correct expression should be <code>std::cout</code>. Running <code>:make</code> produces the error message below:</p>
<p>shell</p>
<pre><code class="language-shell">helloError.cpp:4:10: error: 'count' is not a member of 'std'; did you mean 'cout'?
</code></pre>
<p>When an error occurs, the fix must be made at the location indicated. The following vi commands are useful for navigating between errors</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/d7938788-5df7-4322-a739-e66af34452b8.png" alt="" style="display:block;margin:0 auto" />

<ul>
<li><code>:clist</code> Displays the full list of errors produced during compilation.</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/40cc3dec-522d-485e-b747-0724c6c97943.png" alt="" style="display:block;margin:0 auto" />

<ul>
<li><code>:cfirst</code> Jumps to the location of the first error.</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f5e5cca2-ab42-4957-b9a0-373de3e382b3.png" alt="" style="display:block;margin:0 auto" />

<ul>
<li><code>:clast</code> Jumps to the location of the last error.</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/700f7dee-c14f-4254-b98b-a721b54a489c.png" alt="" style="display:block;margin:0 auto" />

<ul>
<li><code>:cn</code> Moves to the next error in sequence.</li>
</ul>
<p>When multiple errors are present, an efficient approach is to start at the first error with <code>:cfirst</code> and work through them one by one using <code>:cn</code>. The key advantage of this method is that error locations can be navigated and fixed without ever leaving vi.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/4260ea21-734a-48c0-8b33-02545e60e336.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>컴파일 에러</strong></h3>
<p>컴파일 중 에러가 발생했을 때의 예제이다. 첨부된 코드를 보면 4번 줄에 <code>std::count</code>라고 작성되어 있는데, 실제로는 <code>std::cout</code>이 올바른 표현이다. <code>:make</code>를 실행하면 두 번째 사진과 같이 아래 에러 메시지가 출력된다.</p>
<pre><code class="language-shell">helloError.cpp:4:10: error: 'count' is not a member of 'std'; did you mean 'cout'?
</code></pre>
<p>에러가 발생하면 해당 위치로 이동하여 수정해야 한다. 이때 유용하게 사용할 수 있는 vi 명령어들이 있다.</p>
<ul>
<li><p><code>:clist</code> 컴파일 중 발생한 에러 목록 전체를 표시한다.</p>
</li>
<li><p><code>:cfirst</code> 첫 번째 에러 위치로 이동한다.</p>
</li>
<li><p><code>:clast</code> 마지막 에러 위치로 이동한다.</p>
</li>
<li><p><code>:cn</code> 다음 에러 위치로 순서대로 이동한다.</p>
</li>
</ul>
<p>에러가 여러 개일 때 <code>:cfirst</code>로 첫 번째 에러부터 시작하여 <code>:cn</code>으로 하나씩 이동하며 순서대로 수정하는 방식이 효율적이다. vi를 종료하지 않고도 에러 위치를 바로 찾아갈 수 있다는 것이 이 방법의 장점이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/8863c2ec-c5a4-4ef1-ad86-4f2d31fd7cca.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>Changing vim Color Theme</strong></h3>
<p>After launching <code>vi</code>, enter the following in NORMAL mode:</p>
<pre><code class="language-shell">:colorscheme [space] &lt;TAB&gt;
</code></pre>
<p>Pressing the <code>Tab</code> key cycles through available color themes one by one, such as <code>blue</code>, <code>evening</code>, and more. Each press of <code>Tab</code> moves to the next theme, and pressing <code>Enter</code> on the desired theme applies it immediately.</p>
<p>To see all available themes at once, press <code>Ctrl + D</code> instead of <code>Tab</code>. As shown in the screenshot, themes such as <code>blue</code>, <code>darkblue</code>, <code>desert</code>, and <code>evening</code> are listed all at once, allowing you to choose and type the one you want.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/d6cbf069-1820-42c7-aba8-2451cf37a52e.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>vim 색상 변경</strong></h3>
<p>vi를 실행한 후 명령 모드에서 아래와 같이 입력한다.</p>
<pre><code class="language-shell">:colorscheme [스페이스] &lt;TAB&gt;
</code></pre>
<p>탭 키를 누르면 <code>blue</code>, <code>evening</code> 등 사용 가능한 색상 테마가 하나씩 순서대로 나타나며, 계속 누를 때마다 다음 테마로 전환된다. 원하는 테마에서 엔터를 누르면 해당 색상이 즉시 적용된다.</p>
<p>목록을 한눈에 보고 싶다면 탭 대신 <code>Ctrl + D</code>를 누르면 된다. 사진에서 볼 수 있듯이 <code>blue</code>, <code>darkblue</code>, <code>desert</code>, <code>evening</code> 등 다양한 테마가 한 번에 나열되어 원하는 것을 골라 입력할 수 있다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/b8e74d57-2bac-4990-a8c1-cf63f5cc9618.png" alt="" style="display:block;margin:0 auto" />

<h3>Interpreter</h3>
<p>Python is one of the most widely used programming languages and comes pre-installed on Ubuntu by default. The installation status and version can be checked with the following command:</p>
<p>shell</p>
<pre><code class="language-shell">python3 --version
</code></pre>
<p>The lecture example shows <code>Python 3.12.3</code>, but the version may differ depending on the user's environment.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/84209c27-65b4-4909-8b20-4c7268021e08.png" alt="" style="display:block;margin:0 auto" />

<p>mine is the same.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/10f3ee1c-15fa-4130-a16d-ef96527a343b.png" alt="" style="display:block;margin:0 auto" />

<h3>Editor Configuration</h3>
<p>Just as <code>filetype plugin</code> was configured for C and C++ earlier, a separate configuration file can be created for Python as well. If tab settings have been applied globally in <code>~/.vimrc</code>, a separate file can be created at the path below to apply different settings specifically for Python files:</p>
<pre><code class="language-shell">vi ~/.vim/after/ftplugin/python.vim
</code></pre>
<pre><code class="language-shell">setlocal expandtab      " Use spaces instead of tab characters
setlocal tabstop=8      " Size of a tab character in spaces
setlocal shiftwidth=4   " Number of spaces for indent/unindent
setlocal softtabstop=4  " Number of spaces inserted when Tab 
</code></pre>
<p>Here, <code>"</code> is vim's comment symbol. Anything after it on the same line is treated as a human-readable note and is not executed by vim.</p>
<hr />
<h3>Inline Configuration in Source Code</h3>
<p>There is also a way to include settings directly in the source code without creating a separate configuration file. Adding the following line to the first or last line of <a href="http://hello.py"><code>hello.py</code></a> will do the job:</p>
<p>python</p>
<pre><code class="language-python"># vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
</code></pre>
<p>When the file is opened with <code>vi</code>, the specified settings will be applied automatically. Note that this feature requires <code>set modeline</code> to be enabled and only applies to that specific file.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/177ecd30-af7d-4f92-8d04-dc642dcb8380.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/f0753023-0871-4f6b-9b22-338f54ba6b55.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h3>Checking <code>set modeline</code> Status</h3>
<pre><code class="language-shell">:set modeline?
</code></pre>
<p>If the result shows <code>modeline</code>, it is enabled. If it shows <code>nomodeline</code>, it is disabled. To enable it, add the following line to <code>~/.vimrc</code>:</p>
<pre><code class="language-shell">set modeline
</code></pre>
<h3>인터프리터</h3>
<p>Python은 Ubuntu에 기본적으로 설치되어 있을 만큼 널리 사용되는 언어이다. 설치 여부와 버전은 아래 명령어로 확인할 수 있다.</p>
<p><code>python3 --version</code></p>
<p>강의 예제에서는 <code>Python 3.12.3</code>으로 표시되지만, 사용자 환경에 따라 버전이 다를 수 있다.</p>
<h3>편집기 설정</h3>
<p>앞서 C, C++에서 <code>filetype plugin</code>을 설정했던 것처럼, Python에 대해서도 별도의 설정 파일을 만들 수 있다. <code>~/.vimrc</code>에서 전체 파일에 공통으로 적용되는 탭 설정을 했다면, Python 파일에만 다른 설정을 적용하고 싶을 때는 아래 경로에 별도 파일을 생성하면 된다.</p>
<pre><code class="language-shell">vi ~/.vim/after/ftplugin/python.vim 
</code></pre>
<pre><code class="language-shell">setlocal expandtab " 탭 문자 대신 공백 사용 
setlocal tabstop=8 " 탭 문자 공백 크기 
setlocal shiftwidth=4 " 들여쓰기/내어쓰기 공백 개수 
setlocal softtabstop=4 " 탭 키 눌렀을 때 공백 개수 
</code></pre>
<p>여기서 "는 vim의 주석 기호로, 해당 줄에서 그 이후 내용은 사람이 읽기 위한 설명으로만 사용되며 vim이 실행하지 않는다. 별도 설정 파일을 만들지 않아도 소스코드 자체에 설정을 포함시키는 방법도 있다. <code>hello.py</code> 파일의 첫 줄 또는 마지막 줄에 아래와 같이 작성하면 된다.</p>
<pre><code class="language-python"># vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
</code></pre>
<p>이렇게 하면 해당 파일을 vi로 열 때 자동으로 지정된 설정이 적용된다. 단, 이 기능은 set modeline이 활성화되어 있어야 동작하며, 해당 파일에만 적용된다.</p>
<h3><code>set modeline</code> 설정 여부 확인</h3>
<pre><code class="language-shell">:set modeline?
</code></pre>
<p>결과가 <code>modeline</code>이면 활성화된 상태이고, <code>nomodeline</code>이면 비활성화된 상태이다.</p>
<p>비활성화되어 있다면 <code>~/.vimrc</code>에 아래 한 줄을 추가하면 된다.</p>
<pre><code class="language-shell">set modeline
</code></pre>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/2ed68f45-d48f-4aaa-8a32-c9aef302062e.png" alt="" style="display:block;margin:0 auto" />

<h3>Running a Simple Python Script</h3>
<p>A Python source file is written as follows:</p>
<pre><code class="language-python">#!/usr/bin/env python

print("hello, Python")

# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
</code></pre>
<p>The first line <code>#!/usr/bin/env python</code> is called a <strong>shebang</strong>, and it tells the operating system which interpreter to use when the file is executed directly.</p>
<p>Once the file is written, it can be run immediately from vim's NORMAL mode by entering:</p>
<pre><code class="language-plaintext">:!python3 %
</code></pre>
<p>When <code>:</code> is followed by <code>!</code>, it means execute a command in the shell. <code>python3</code> is the command to run the Python interpreter, and <code>%</code> refers to the name of the file currently being edited. In other words, the above command is equivalent to running the following in the shell:</p>
<pre><code class="language-plaintext">python3 hello.py
</code></pre>
<p>After execution, <code>hello, Python</code> will be printed, followed by <code>Press ENTER or type command to continue</code>. Pressing Enter returns to the vim editing screen.</p>
<p>⚠️ Note that the shebang on the first line is written as <code>#!/usr/bin/env python</code>, but in most modern Linux environments, the <code>python</code> command may point to Python 2 or may not exist at all. If you are using a Python 3 environment, it is recommended to write <code>#!/usr/bin/env python3</code> instead.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/bda2d6d6-162e-4555-8027-53a2768ab4e5.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h3><strong>Python 간단한 실행</strong></h3>
<p>Python 소스 파일은 아래와 같이 작성한다.</p>
<p>python</p>
<pre><code class="language-python">#!/usr/bin/env python

print("hello, Python")

# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
</code></pre>
<p>첫 줄의 <code>#!/usr/bin/env python</code>은 셔뱅(shebang)이라고 하며, 이 파일을 직접 실행할 때 어떤 인터프리터를 사용할지 운영체제에 알려주는 역할을 한다.</p>
<p>작성이 끝나면 vi 명령 모드에서 아래와 같이 입력하여 바로 실행할 수 있다.</p>
<pre><code class="language-plaintext">:!python3 %
</code></pre>
<p><code>:</code>에 이어 <code>!</code>가 오면 셸에서 명령을 실행하라는 의미이다. <code>python3</code>는 Python 인터프리터를 실행하는 명령이고, <code>%</code>는 현재 편집 중인 파일 이름을 의미한다. 즉, 위 명령은 셸에서 아래를 실행하는 것과 동일하다.</p>
<pre><code class="language-plaintext">python3 hello.py
</code></pre>
<p>실행하면 <code>hello, Python</code>이 출력되고, <code>Press ENTER or type command to continue</code>가 표시된다. 엔터를 누르면 vi 편집 화면으로 돌아온다.</p>
<p>⚠️ 참고로 첫 줄의 <code>#!/usr/bin/env python</code>은 <code>python</code>으로 작성되어 있는데, 현재 대부분의 리눅스 환경에서는 <code>python</code> 명령이 Python 2를 가리키거나 아예 존재하지 않는 경우가 있다. Python 3를 사용하는 환경이라면 <code>#!/usr/bin/env python3</code>로 작성하는 것이 권장된다고 한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/ec061548-8517-4fab-9a58-5e7c7704e241.png" alt="" style="display:block;margin:0 auto" />

<h3>Useful Tools - Installing Python Packages (pip)</h3>
<p>Just as <code>apt install</code> is used to install general packages on Ubuntu, <code>pip</code> is used to install Python packages. pip is a package management tool dedicated to Python. It can be installed with the following command:</p>
<pre><code class="language-shell">sudo apt install python3-pip
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/70abab8c-c22f-4778-a5c8-c02dff5e2031.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/c9eb556b-bb4a-42fe-a462-8ba9099dbd04.png" alt="" style="display:block;margin:0 auto" />

<p>After installation, Python packages can be installed as follows:</p>
<pre><code class="language-plaintext">pip3 install package-name
</code></pre>
<p>The <code>3</code> in <code>pip3</code> indicates that it is the pip version for Python 3.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/df146628-eee7-4993-9095-ea2790e4882a.png" alt="" style="display:block;margin:0 auto" />

<p><strong>Ubuntu 23.04 이후의 제한</strong> 때문에 발생하는 에러</p>
<hr />
<h3><code>venv</code> Python Virtual Environment</h3>
<p><code>venv</code> is a tool for setting up a Python virtual environment.</p>
<pre><code class="language-plaintext">sudo apt install python3-venv
</code></pre>
<p>Python has many versions, and compatibility issues between versions can arise. This is why code written over the course of a month may not work in a different environment. <code>venv</code> allows you to create an isolated Python environment for each project, separating and managing version and package dependencies independently.</p>
<p>It is also recommended to document which Python version and packages were used in a <code>README</code> file, so that other users can reproduce the same environment.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/e9914820-160d-4e4b-bc65-7234d2e5df88.png" alt="" style="display:block;margin:0 auto" />

<p>⚠️ Starting from Ubuntu 23.04, directly installing packages system-wide using pip may be restricted. In this case, the recommended approach is to create a virtual environment using <code>venv</code> and then use pip within it.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/1b73d3a9-db99-4c0d-839f-504600a02413.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>유용한 도구 - 파이썬 패키지 설치 (pip)</strong></h3>
<p>Ubuntu에서 일반 패키지를 설치할 때 <code>apt install</code>을 사용하듯이, Python 패키지를 설치할 때는 <code>pip</code>를 사용한다. pip는 Python 전용 패키지 관리 도구이다. 아래 명령어로 설치할 수 있다.</p>
<pre><code class="language-shell">sudo apt install python3-pip
</code></pre>
<p>설치 후 Python 패키지는 아래와 같이 설치한다.</p>
<pre><code class="language-plaintext">pip3 install 설치할패키지이름
</code></pre>
<p><code>pip3</code>에서 <code>3</code>은 Python 3 버전용 pip임을 의미한다.</p>
<p>다음으로 <code>venv</code>는 Python 가상 환경을 구성하는 도구이다.</p>
<pre><code class="language-plaintext">sudo apt install python3-venv
</code></pre>
<p>Python은 버전이 매우 다양하고 버전 간 호환성 문제가 발생할 수 있다. 한 달 동안 작성한 코드가 다른 환경에서 동작하지 않는 경우가 생길 수 있는 것도 이 때문이다. <code>venv</code>는 프로젝트별로 독립된 Python 환경을 만들어 버전과 패키지 의존성을 분리하여 관리할 수 있게 해준다. 또한 어떤 Python 버전과 패키지를 사용했는지 <code>README</code> 파일에 명시해두면, 다른 사용자가 동일한 환경을 재현하는 데 도움이 된다.</p>
<p>⚠️Ubuntu 23.04 이후부터는 pip로 시스템 전역에 패키지를 직접 설치하는 것이 제한될 수 있다고 한다. 이 경우 <code>venv</code>로 가상 환경을 생성한 후 그 안에서 pip를 사용하는 것이 권장되는 방법이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/9a1b729d-5679-4596-bfa0-cb3515b82de2.png" alt="" style="display:block;margin:0 auto" />

<h3><strong>파이썬 가상 환경 venv</strong></h3>
<p>venv 사용 순서는 아래와 같다.</p>
<pre><code class="language-shell">sudo apt install python3-venv   # venv 설치
mkdir proj ; cd proj            # 프로젝트 디렉토리 생성 후 이동
python3 -m venv .venv           # 가상 환경 생성
source .venv/bin/activate       # 가상 환경 활성화
</code></pre>
<p>가상 환경이 활성화되면 프롬프트 앞에 <code>(.venv)</code>가 표시된다. 이 상태에서 설치하는 패키지는 시스템 전체가 아닌 해당 가상 환경 안에만 적용된다. 가상 환경을 종료할 때는 아래 명령어를 입력한다.</p>
<pre><code class="language-shell">deactivate
</code></pre>
<p>종료되면 프롬프트에서 <code>(.venv)</code> 표시가 사라지고 일반 환경으로 돌아온다.</p>
<p>venv의 세부적인 활용법은 Python을 전문적으로 학습하거나, 다른 사람이 만든 Python 코드를 사용할 때 더 깊이 살펴보면 도움이 될 것이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/c23ae03d-840a-4b2c-a2be-c9f1ac5615a9.png" alt="" style="display:block;margin:0 auto" />

<h3>Types of Java</h3>
<p>Java has a somewhat more complex installation process compared to C or Python. To understand why, it helps to briefly look at the history of Java.</p>
<p>Java was first developed by Sun Microsystems in the late 20th century and was initially distributed for free with the goal of widespread adoption. However, enterprise-level environments required paid license agreements. After Sun Microsystems was acquired by Oracle, the licensing policy changed multiple times. As a result, open source implementations that could be used freely began to emerge.</p>
<p>The main types of Java currently available are as follows:</p>
<ul>
<li><p><strong>OpenJDK</strong> An open source implementation of Java, most widely used in Linux environments.</p>
</li>
<li><p><strong>Oracle Java</strong> Downloaded and installed directly from the Oracle website.</p>
</li>
<li><p><strong>IBM Java</strong> Downloaded and installed from the IBM website.</p>
</li>
<li><p><strong>GNU Compiler (GCJ)</strong> A Java compiler created by the GNU project, though it is not fully compatible with Java from Sun Microsystems or Oracle.</p>
</li>
</ul>
<p>Each distribution has different installation methods and environment configurations. After installation, the path to the Java compiler (<code>javac</code>) and the path to the JVM (Java Virtual Machine) that runs Java bytecode must be separately configured as environment variables.</p>
<h3><strong>Java 종류</strong></h3>
<p>Java는 C나 Python에 비해 설치 과정이 다소 복잡하다. 그 이유를 이해하려면 Java의 역사를 간략히 살펴볼 필요가 있다.</p>
<p>Java는 20세기 후반 Sun Microsystems에서 처음 개발되었으며, 초기에는 보급을 목적으로 무료로 배포되었다. 그러나 엔터프라이즈급 환경에서는 유료 라이선스 계약이 필요했고, 이후 Sun Microsystems가 Oracle에 인수되면서 라이선스 정책이 여러 차례 변경되었다. 이에 따라 자유롭게 사용할 수 있는 오픈 소스 구현체들이 등장하기 시작했다.</p>
<p>현재 사용할 수 있는 Java의 종류는 크게 아래와 같다.</p>
<ul>
<li><p><strong>OpenJDK</strong> — 오픈 소스로 구현된 Java로, 리눅스 환경에서 가장 널리 사용된다.</p>
</li>
<li><p><strong>Oracle Java</strong> — Oracle 홈페이지에서 직접 다운로드하여 설치한다.</p>
</li>
<li><p><strong>IBM Java</strong> — IBM 홈페이지에서 다운로드하여 설치한다.</p>
</li>
<li><p><strong>GNU Compiler (GCJ)</strong> — GNU 프로젝트에서 만든 Java 컴파일러로, Sun Microsystems 및 Oracle의 Java와 완전히 호환되지는 않는다.</p>
</li>
</ul>
<p>각 배포판마다 설치 방법과 환경 설정이 다르며, 설치 후에는 Java 컴파일러(<code>javac</code>)의 경로와 Java 바이트코드를 실행하는 JVM(Java Virtual Machine)의 경로를 환경 변수에 별도로 설정해 주어야 한다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/225c344c-5752-49cd-a50a-1a54666cd70d.png" alt="" style="display:block;margin:0 auto" />

<h3>Installing Java <code>OpenJDK</code></h3>
<p>The simplest method is to install OpenJDK using <code>apt</code>.</p>
<pre><code class="language-shell">sudo apt install openjdk-25-jdk
</code></pre>
<p>After installation, the versions of the compiler and JVM can be checked with the following commands:</p>
<pre><code class="language-shell">javac --version
java --version
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/ec33b132-6972-4385-9468-2a64aa11ec3c.png" alt="" style="display:block;margin:0 auto" />

<h3>JDK vs JRE</h3>
<p>There is an important distinction to make here. Java comes in two forms: <strong>JDK (Java Development Kit)</strong> and <strong>JRE (Java Runtime Environment)</strong>. The JDK is for development and includes the <code>javac</code> compiler, allowing you to write and compile code. The JRE only supports running (runtime) Java programs and does not include a compiler. Therefore, if you intend to write and compile code yourself, you must install the JDK.</p>
<hr />
<h3>Practice — <a href="http://hello.java">hello.java</a></h3>
<p>Write a Java source file as follows:</p>
<p>java</p>
<pre><code class="language-java">public class hello{
    public static void main(String[] args){
        System.out.println("Hello, KoreaCU!");
    }
}
</code></pre>
<p>After writing the file, compile and run it in the following order:</p>
<pre><code class="language-plaintext">javac hello.java   # Compile — generates hello.class (bytecode)
java hello         # Run the bytecode using the JVM
</code></pre>
<p>Just like the <code>:!gcc % -o %&lt;</code> method learned earlier, the same approach can be applied to Java — compiling and running without leaving the vi editor.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/51bee517-f7ef-42a8-aa5a-6676a50013fa.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h3>Important Note for First-Time Java Users</h3>
<p>One key rule to keep in mind when using Java for the first time is the relationship between the file name and the class name. In Java, the file name and the <code>public class</code> name must always match. For example, if the class name is <code>hello</code>, the file name must also be <a href="http://hello.java"><code>hello.java</code></a>. Just as indentation is part of Python's syntax, matching the file name with the class name is a fundamental rule in Java.</p>
<h3><strong>Java 설치 OpenJDK</strong></h3>
<p>가장 간단한 방법은 OpenJDK를 <code>apt</code>로 설치하는 것이다.</p>
<pre><code class="language-shell">sudo apt install openjdk-25-jdk
</code></pre>
<p>설치 후 아래 명령어로 컴파일러와 JVM의 버전을 각각 확인할 수 있다.</p>
<pre><code class="language-shell">javac --version
java --version
</code></pre>
<p>여기서 한 가지 구분해야 할 점이 있다. Java에는 JDK(Java Development Kit)와 JRE(Java Runtime Environment)가 있다. JDK는 개발(Development)용으로 <code>javac</code> 컴파일러가 포함되어 있어 코드를 작성하고 컴파일할 수 있다. JRE는 실행(Runtime)만 가능하며 컴파일러는 포함되어 있지 않다. 따라서 코드를 직접 작성하고 컴파일하려면 반드시 JDK를 설치해야 한다.</p>
<h3><a href="http://hello.java"><strong>hello.java</strong></a> <strong>실습</strong></h3>
<p>아래와 같이 Java 소스 파일을 작성한다.</p>
<pre><code class="language-java">public class hello{
    public static void main(String[] args){
        System.out.println("Hello, KoreaCU!");
    }
}
</code></pre>
<p>작성 후 아래 순서로 컴파일하고 실행한다.</p>
<pre><code class="language-plaintext">javac hello.java   # 컴파일 — hello.class(바이트코드) 생성
java hello         # JVM으로 바이트코드 실행
</code></pre>
<p>앞서 배운 <code>:!gcc % -o %&lt;</code> 방식처럼, vi 안에서 셸을 벗어나지 않고 컴파일과 실행을 처리하는 방법을 Java에도 동일하게 응용할 수 있다.</p>
<p>Java를 처음 사용할 때 주의해야 할 점은 파일 이름과 클래스 이름의 관계이다. Java에서는 파일 이름과 <code>public class</code>의 이름이 반드시 일치해야 한다. 예를 들어 클래스 이름이 <code>hello</code>라면 파일 이름도 반드시 <a href="http://hello.java"><code>hello.java</code></a>여야 한다. Python에서 들여쓰기가 문법의 일부이듯, Java에서는 이 파일명과 클래스명의 일치 여부가 핵심적인 규칙이다.</p>
<hr />
<h3>Note: Difference Between Compiler and Interpreter</h3>
<p>A compiler converts the entire source code into machine language all at once before executing it. C, C++, and Java fall into this category. An interpreter reads and executes code line by line in real time, with Python being the most representative example.</p>
<p>In terms of actual usage, the differences can be summarized as follows:</p>
<ul>
<li><p><strong>C / C++</strong> — Compile with <code>gcc</code> or <code>g++</code>, then execute</p>
</li>
<li><p><strong>Java</strong> — Compile with <code>javac</code>, then run on the JVM using the <code>java</code> command</p>
</li>
<li><p><strong>Python</strong> — Run directly with the <code>python3</code> command without a separate compilation step</p>
</li>
</ul>
<p>Understanding this difference will make it much easier to set up the execution environment for each language.</p>
<p>참고: 컴파일러와 인터프리터의 차이와 각각의 사용법 정도는 기억해두자.</p>
<p>컴파일러는 소스 코드를 한 번에 기계어로 변환한 후 실행하는 방식으로, C와 C++, Java가 이에 해당한다. 인터프리터는 코드를 한 줄씩 읽으며 바로 실행하는 방식으로, Python이 대표적인 예이다.</p>
<p>실제 사용 측면에서 정리하면, C와 C++는 <code>gcc</code> 또는 <code>g++</code>로 컴파일하고, Java는 <code>javac</code>로 컴파일한 뒤 <code>java</code> 명령으로 JVM에서 실행한다. Python은 별도의 컴파일 없이 <code>python3</code> 명령으로 바로 실행할 수 있다. 이 차이를 이해해두면 각 언어의 실행 환경을 구성할 때 어렵지 않게 적용할 수 있을 것이다.</p>
<hr />
]]></content:encoded></item><item><title><![CDATA[White-Box Testing: Coverage, Control Flow, and Test Design Strategies]]></title><description><![CDATA[1️⃣ White Box Test Preview2️⃣ Statement asnd Branch Testing3️⃣ Conditon Testing

1️⃣ White Box Test Preview
What is White-Box Testing? Core Concepts and Goals
White-box testing is a concept that contr]]></description><link>https://heesu.tech/white-box-testing-coverage-control-flow-and-test-design-strategies</link><guid isPermaLink="true">https://heesu.tech/white-box-testing-coverage-control-flow-and-test-design-strategies</guid><dc:creator><![CDATA[Heesu Noh]]></dc:creator><pubDate>Tue, 21 Apr 2026 14:43:03 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/0351ef61-dd07-4152-966f-752f8e7e746d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>1️⃣ White Box Test Preview<br />2️⃣ Statement asnd Branch Testing<br />3️⃣ Conditon Testing</p>
<hr />
<h1>1️⃣ White Box Test Preview</h1>
<h3>What is White-Box Testing? Core Concepts and Goals</h3>
<p>White-box testing is a concept that contrasts with black-box testing. While black-box testing verifies software behavior solely based on inputs and outputs without looking at its internal structure, white-box testing does the opposite; it tests software by directly examining its internal code and structure.</p>
<p>So what is the goal of white-box testing? Based on its relationship with black-box testing, the core goal is to verify that the internal code and logical flow of the software operates correctly.  </p>
<p>화이트박스가 무엇이고 화이트박스의 주요한 개념에 대해서 살펴본다. 앞서 블랙박스의 상대적인 개념으로 화이트박스 테스트라는 개념을 배웠다. 그 내용을 고려해서 화이트박스 테스트의 목표는 무엇인지 생각해보자.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/d745b986-d929-4257-8936-389cfd45f970.png" alt="" style="display:block;margin:0 auto" />

<h3>The Goal of White-Box Testing and the Emergence of Coverage</h3>
<p>Whether black-box or white-box, testing every possible case is practically impossible in terms of time and cost. The same applies to white-box testing. So what should be tested given these constraints?</p>
<p>White-box testing is defined as testing the paths within source code. The more if statements, selection statements, and loops exist in the source code, the more diverse the paths become — and more diverse paths mean more cases to test. Since it is realistically impossible to test every path in software, what matters most is how "effectively" testing is performed.</p>
<p>Therefore, the goal of white-box testing is to test as much of the software as possible. To achieve this, "appropriate" testing must be performed, and the key lies in selecting the right paths. If choosing a single path allows a large portion of the source code to be covered simultaneously, that constitutes an excellent test.</p>
<p>This is why a suitable standard for determining which paths cover the most ground is absolutely necessary; even a simple program can have a vast number of paths. This is the point at which the concept of <strong>Coverage</strong> emerges. Coverage is defined as the suitable standard for determining which specific paths can cover the most ground simultaneously.  </p>
<h3>화이트박스 테스트의 목표와 커버리지 개념의 등장</h3>
<p>블랙박스 테스트든 화이트박스 테스트든, 모든 케이스를 테스트하는 것은 시간과 비용 측면에서 사실상 불가능하다. 화이트박스 테스트 역시 마찬가지다. 그렇다면 시간과 비용의 제약 속에서 무엇을 대상으로 테스트를 수행해야 할까?</p>
<p>화이트박스 테스트의 정의는 소스코드의 경로를 테스트하는 것이다. 소스코드 안에 if문, 선택문, 반복문 등이 많아질수록 경로는 더욱 다양해지고, 경로가 다양해진다는 것은 곧 테스트해야 할 케이스가 많아진다는 뜻이다. 소프트웨어의 모든 경로를 테스트하기란 현실적으로 불가능하기 때문에, 결국 얼마나 '효과적인' 테스트를 수행하느냐가 중요해진다.</p>
<p>따라서 화이트박스 테스트의 목표는 가능한 한 많은 부분을 테스트하는 것이다. 이를 위해서는 '적합한' 테스트가 수행되어야 하며, 그 핵심은 적절한 경로를 선택하는 데 있다. 하나의 경로를 선택했을 때 소스코드의 많은 부분을 함께 커버할 수 있다면, 그것이 바로 좋은 테스트가 된다.</p>
<p>이처럼 어떤 경로를 선택해야 소스코드의 많은 부분을 커버할 수 있는지를 판단하기 위한 적합한 기준이 반드시 필요하다. 간단한 프로그램이라도 경로는 아주 다양할 수 있기 때문이다. 바로 이 지점에서 <strong>커버리지(Coverage)</strong> 라는 개념이 등장한다. 특정 경로를 통해 많은 부분을 함께 커버할 수 있는 적합한 기준, 그것이 커버리지로 정의된다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/7300e857-631d-43c8-88ef-19345e21612f.png" alt="" style="display:block;margin:0 auto" />

<h3>The Concept of Source Code Coverage and Its Levels</h3>
<p>The target of white-box testing is source code. Covering as much of the source code as possible is therefore essential, and from this need emerges the concept of <strong>source code coverage</strong>. Coverage is a metric that represents how much of the source code was executed by the test cases we created. Naturally, a strategy for designing tests that maximizes this coverage is required.</p>
<p>However, achieving 100% coverage across all programs is realistically difficult due to time and cost constraints.</p>
<p>Coverage is divided into levels. From lowest to highest: <strong>Function Coverage</strong>, <strong>Statement Coverage</strong>, <strong>Branch/Condition Coverage</strong>, <strong>Modified Condition/Decision Coverage (MC/DC)</strong>, and <strong>Multiple Condition Coverage</strong>. Function coverage is the lowest level because if the functions within a program are not properly tested, the program as a whole cannot be considered tested.</p>
<p>A higher coverage level means more test cases are required and more effort is needed to create them. For this reason, it is important in practice to at least satisfy the lower levels of coverage as thoroughly as possible. Ultimately, in white-box testing, the standard and direction of testing varies depending on what is being measured — functions, branches, modified conditions, and so on.  </p>
<h3>소스코드 커버리지의 개념과 레벨</h3>
<p>화이트박스 테스트의 대상은 소스코드다. 그렇기 때문에 소스코드의 많은 부분을 커버하는 것이 핵심이며, 이로부터 <strong>소스코드 커버리지</strong>라는 개념이 등장한다. 커버리지란 우리가 만든 테스트케이스가 소스코드를 얼마만큼 실행했는지를 나타내는 지표다. 당연히 이 커버리지를 높이는 방향으로 테스트를 설계하는 전략이 필요하다.</p>
<p>다만 모든 프로그램에서 커버리지를 100% 달성하는 것은 현실적으로 어렵다. 시간과 비용의 제약이 존재하기 때문이다.</p>
<p>커버리지는 레벨별로 나뉜다. 가장 낮은 수준부터 순서대로 나열하면 <strong>함수 커버리지(Function Coverage)</strong>, <strong>문장 커버리지(Statement Coverage)</strong>, <strong>분기/조건 커버리지(Branch/Condition Coverage)</strong>, <strong>변경조건/결정 커버리지(MC/DC)</strong>, <strong>다중조건 커버리지(Multiple Condition Coverage)</strong> 순으로 높아진다. 함수 커버리지가 가장 낮은 수준인 이유는, 프로그램 내의 함수들이 제대로 테스트되지 않으면 전체 프로그램이 테스트되었다고 볼 수 없기 때문이다.</p>
<p>커버리지 레벨이 높다는 것은 그만큼 테스트케이스도 많아지고, 이를 만드는 데 더 많은 노력이 필요하다는 뜻이다. 그렇기 때문에 현실적으로는 적어도 낮은 수준의 커버리지부터 최대한 만족시키는 것이 중요하다. 결국 화이트박스 테스트에서는 함수, 분기, 변경조건 등 무엇을 대상으로 커버리지를 측정하느냐에 따라 테스트의 기준과 방향이 달라진다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/b8adb179-6210-44dc-a5c0-acf68e20c02f.png" alt="" style="display:block;margin:0 auto" />

<h3>Function Coverage; The Most Basic Coverage Concept and Test Design Strategy</h3>
<p>The most fundamental type of coverage is <strong>Function Coverage</strong>. A program is largely composed of functions, and function coverage is the metric that represents how thoroughly those functions have been tested. As mentioned earlier, it is the lowest level of coverage, and since it targets functions, it is conducted through <strong>unit testing</strong>. Function coverage must achieve 100% — every function in a program must naturally be tested, as this is the most basic requirement.</p>
<blockquote>
<p><strong>Function Coverage = Number of Functions Tested / Total Number of Functions × 100%</strong></p>
</blockquote>
<p>In the example, three functions exist: <code>function_A</code>, <code>function_B</code>, and <code>function_C</code>, where <code>function_C</code> internally calls <code>function_A</code>. Testing only <code>function_A</code> yields 1/3, or 33.333%. Testing <code>function_C</code> covers <code>function_A</code> as well, giving 2/3, or 66.666%. To reach 100%, rather than testing A, B, and C separately, testing <code>function_B</code> and <code>function_C</code> alone covers all three functions; achieving 100% function coverage with just two test cases. This is the essence of <strong>test design strategy</strong>.</p>
<p>While the difference between 2 and 3 test cases may seem trivial in this simple example, real programs can have tens, hundreds, or even thousands of functions. Analyzing the program thoroughly and understanding the relationships between functions; in order to achieve the highest possible coverage with the minimum number of test cases; is both <strong>test suitability</strong> and a critical consideration alongside coverage.</p>
<p>커버리지 중에서도 가장 기본적인 것은 함수 커버리지(Function Coverage)다. 프로그램은 많은 부분이 함수로 구성되어 있으며, 함수 커버리지란 이 함수들을 얼마나 테스트했는지를 나타내는 지표다. 앞서 언급했듯 커버리지 레벨 중 가장 낮은 수준에 해당하며, 함수를 대상으로 하기 때문에 단위 테스트(Unit Test)를 통해 진행된다. 함수 커버리지는 반드시 100% 달성되어야 한다. 프로그램에 존재하는 함수는 당연히 모두 테스트되어야 하며, 이것이 테스트의 가장 기본이기 때문이다. 함수 커버리지는 다음과 같이 계산된다.</p>
<blockquote>
<p><strong>함수 커버리지 = 테스트한 함수 수 / 전체 함수 수 × 100%</strong></p>
</blockquote>
<p>첨부된 예시를 보면, function_A, function_B, function_C 세 개의 함수가 존재하고, function_C는 내부적으로 function_A를 호출하고 있다. function_A만 테스트했을 경우 1/3, 즉 33.333%의 커버리지가 나온다. function_C를 테스트하면 function_A도 함께 호출되므로 2/3, 즉 66.666%가 된다. 그렇다면 100%를 달성하려면 어떻게 해야 할까? function_A, B, C를 각각 따로 테스트하는 것보다, function_C를 테스트하면 function_A까지 함께 커버되므로, function_B와 function_C, 이 두 가지만 테스트해도 전체 함수 3개를 모두 커버하여 100%를 달성할 수 있다. 즉, 테스트케이스 2개만으로 함수 커버리지 100%를 만족하게 되는 것이다. 이것이 바로 테스트 설계 전략이다. 이 예시는 비교적 단순하여 2개와 3개의 차이가 크지 않아 보일 수 있다. 그러나 실제 프로그램은 함수가 수십, 수백, 수천 개에 달한다. 시간과 비용이 충분하다면 모두 개별적으로 테스트할 수 있겠지만, 실제 프로젝트에서는 사실상 불가능하다. 따라서 프로그램을 잘 분석하고 함수 간의 관계를 파악하여, 최소한의 테스트케이스로 최대한 높은 커버리지를 달성하는 노력이 필요하다. 이것이 바로 테스트의 적합성이며, 커버리지 개념과 함께 반드시 고려해야 할 부분이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/9540f98e-b2a3-4503-bf5d-593a6f08a2b6.png" alt="" style="display:block;margin:0 auto" />

<h3>Control Flow Graph (CFG); A Core Tool for White-Box Testing</h3>
<p>One of the most important concepts in white-box testing is the <strong>Control Flow Graph (CFG)</strong>. Rather than reading hundreds or thousands of lines of source code directly, representing the code as a diagram in the form of a flowchart makes it far easier to understand. The CFG serves this purpose — it is a graph that logically represents the procedures, processes, and control structures (yes/no, etc.) of a program's execution. In simple terms, it is equivalent to a flowchart.</p>
<p>A CFG is composed of <strong>nodes</strong> and <strong>edges</strong>. Nodes represent program execution, branching, and control flow, while edges represent the flow between nodes. For example, a statement like <code>INT X = 1;</code> becomes node 1, an <code>IF (X &gt; 0)</code> branch becomes node 2, and the yes/no branches become nodes 3 and 4 respectively.</p>
<p>Any program, regardless of length, can be expressed as a CFG. While short code is easy to understand directly, longer code becomes increasingly difficult to follow. Representing it as a CFG allows the flow to be grasped at a glance and makes deriving test cases significantly easier; which is precisely why CFGs are widely used in white-box testing.  </p>
<h3>제어 흐름도(CFG) 화이트박스 테스트의 핵심 표현 도구</h3>
<p>화이트박스 테스트에서 중요한 개념 중 하나가 바로 제어 흐름도(Control Flow Graph, CFG)다. 소스코드 수백, 수천 줄을 그대로 읽는 것보다 이를 순서도 형태의 다이어그램으로 표현하면 훨씬 이해하기 쉽다. 제어 흐름도는 바로 이런 목적으로, 프로그램이 실행되는 절차와 과정, 그리고 제어 구조(yes/no 등)를 논리적으로 표현한 그래프다. 쉽게 말해 플로우차트, 즉 순서도와 같은 형태라고 볼 수 있다. 제어 흐름도는 노드(Node)와 간선(Edge)으로 구성된다. 노드는 프로그램의 실행, 분기, 제어 흐름 등을 표현하고, 간선은 노드 간의 흐름을 나타낸다. 첨부된 예시를 보면, INT X = 1;과 같은 코드가 노드 1로, IF (X &gt; 0) 분기문이 노드 2로 표현되며, 조건에 따른 yes/no 분기가 각각 노드 3과 4로 나뉘는 방식이다. 어떤 프로그램이든, 천 줄이든 만 줄이든 상관없이 제어 흐름도로 표현이 가능하다. 코드의 양이 적을 때는 코드 자체를 이해하는 것이 어렵지 않지만, 줄 수가 많아질수록 파악이 점점 어려워진다. 제어 흐름도로 표현하면 흐름을 한눈에 파악할 수 있고, 이를 통해 테스트케이스를 도출하는 데도 훨씬 용이해진다. 이것이 화이트박스 테스트에서 제어 흐름도를 활용하는 이유이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/3d7d1d87-e6a0-4dff-9ba0-c11ee7f1cef5.png" alt="" style="display:block;margin:0 auto" />

<h3>Types of Control Flow Graphs; Representing if, while, and switch Statements</h3>
<p>A CFG represents the control structures within a program; such as if statements, while loops, and switch statements - as graphs.</p>
<p>The first type is the <strong>if statement</strong>. Depending on whether condition P (the predicate node) is true or false, the flow diverges to either S1 or S2, before merging again at S3.</p>
<p>The second type is the <strong>while loop</strong>. When condition P is true, S1 is executed repeatedly; when false, the loop exits and proceeds to S2.</p>
<p>The third type is the <strong>switch statement</strong>. Depending on condition P, the flow branches into case 1 through case 4 (including default), corresponding to S1 through S4, and all cases eventually merge at S5.</p>
<p>In this way, the control structure of any source code, expressed through nodes and edges, constitutes a Control Flow Graph.  </p>
<h3>제어 흐름도의 유형; if, while, switch 문의 그래프 표현</h3>
<p>제어 흐름도는 프로그램 내의 제어 구조, 즉 if문, while문, switch문 등을 그래프로 표현한 것이다. 첨부된 예시를 통해 대표적인 유형 세 가지를 살펴볼 수 있다.</p>
<p>첫 번째는 <strong>if문</strong>이다. 조건 P(predicate 노드)가 true인 경우와 false인 경우에 따라 노드의 흐름이 달라지며, S1 혹은 S2로 분기된다. 이후 두 경로는 다시 S3으로 합쳐진다.</p>
<p>두 번째는 <strong>while문</strong>이다. 조건 P가 true인 경우 S1을 반복 실행하고, false인 경우 반복문을 벗어나 S2로 진행하는 흐름을 그래프로 표현한다.</p>
<p>세 번째는 <strong>switch문</strong>이다. 조건 P에 따라 case 1부터 case 4(default 포함)까지 각각 S1, S2, S3, S4로 분기되는 흐름을 표현하며, 모든 케이스는 최종적으로 S5로 합쳐진다.</p>
<p>이처럼 어떤 소스코드든 제어 구조를 노드와 간선으로 이루어진 그래프로 표현한 것이 바로 제어 흐름도(Control Flow Graph)이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/02c04eae-bd5d-46e3-88d4-ab89d0bde4fc.png" alt="" style="display:block;margin:0 auto" />

<h3>Individual Conditions vs. the Overall Condition Expression</h3>
<p>When working with CFGs, it is necessary to distinguish between two concepts: <strong>individual conditions</strong> and the <strong>overall condition expression</strong>.</p>
<p>Consider the if statement <code>IF ((X &gt; 2) AND (Y &lt; 4))</code>. This condition expression results in either true or false overall. Within it, 'X is greater than 2' and 'Y is less than 4' are each small, distinct units separated by operators like AND or OR — these are called <strong>individual conditions</strong>. The combination of all individual conditions that together determine the overall true/false result of the control flow is called the <strong>overall condition expression</strong>.</p>
<p>This distinction matters because it is directly tied to coverage. There are testing methods that target the overall condition expression, and separate methods that target individual conditions. The type and level of coverage varies depending on which unit serves as the testing standard; which is why clearly understanding the difference between these two terms is important.</p>
<p>제어 흐름도를 이해하다 보면 <strong>개별 조건</strong>과 <strong>전체 조건식</strong>이라는 두 가지 개념을 구분할 필요가 있다.</p>
<p>예를 들어 <code>IF ((X &gt; 2) AND (Y &lt; 4))</code>라는 if문이 있다고 하자. 이 조건식은 전체적으로 true 아니면 false, 두 가지로 나뉜다. 이때 'X가 2보다 크다'는 조건과 'Y가 4보다 작다'는 조건처럼, AND나 OR 같은 연산자로 구분되는 각각의 작은 단위 조건을 <strong>개별 조건</strong>이라고 한다. 그리고 이 개별 조건들이 모여 제어 흐름 전체의 true/false를 결정하는 조합 전체를 <strong>전체 조건식</strong>이라고 한다.</p>
<p>이 구분이 중요한 이유는 커버리지와 직결되기 때문이다. 커버리지를 측정할 때 전체 조건식을 대상으로 테스트하는 방법이 있고, 개별 조건 단위를 대상으로 테스트하는 방법이 별도로 존재한다. 어떤 단위를 기준으로 테스트하느냐에 따라 커버리지의 종류와 수준이 달라지기 때문에, 이 두 용어의 차이를 명확히 이해해 두는 것이 중요하다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a9f08f5c-2dd7-4dac-8aa1-d7621a846005.png" alt="" style="display:block;margin:0 auto" />

<h3>Types of White-Box Test Design Techniques; End of Lecture 1</h3>
<p>White-box test design techniques are numerous, with many defined in testing standards. Techniques such as <strong>Statement Testing</strong>, <strong>Branch Testing</strong>, and <strong>Condition Testing</strong> exist among many others. In this session, three key techniques were covered, with more advanced techniques to be explored in the next class.</p>
<p>Today's lecture covered the definition and goals of white-box testing, as well as the Control Flow Graph; a tool for applying white-box testing effectively.</p>
<h2>화이트박스 테스트 설계 기법의 종류; 1차시 마무리</h2>
<p>화이트박스 테스트 설계 기법은 테스트 표준에 정의된 것만 해도 아주 다양하다. 문장 테스팅(Statement Testing), 분기 테스팅(Branch Testing), 조건 테스팅(Condition Testing) 등 여러 종류가 존재하는데, 오늘은 이 중 세 가지를 중심으로 살펴볼 예정이며, 고급 기법에 대해서는 다음 시간에 다루게 된다.</p>
<p>오늘 강의에서는 화이트박스 테스트의 정의와 목표, 그리고 이를 효율적으로 활용하기 위한 도구인 제어 흐름도까지의 내용을 살펴보았다.</p>
<hr />
<h1>2️⃣ Statement asnd Branch Testing</h1>
<h2>Statement Testing and Branch Testing; Chapter 2</h2>
<p>Chapter 2 focuses on two of the most representative white-box testing techniques: <strong>Statement Testing</strong> and <strong>Branch Testing</strong>. Through examples, we will explore how to actually design tests using these methods.</p>
<p>The content of statement testing is somewhat apparent from its name. As for branch testing — what might it involve? Recall the concepts of control flow graphs and coverage covered earlier, and consider what each technique might mean based on its name alone.  </p>
<h3>문장 테스팅과 분기 테스팅; 두 번째 챕터 시작</h3>
<p>두 번째 챕터에서는 화이트박스 테스트의 대표적인 기법인 <strong>문장 테스팅</strong>과 <strong>분기 테스팅</strong>, 이 두 가지를 중심으로 살펴본다. 예제를 통해 실제로 테스트를 어떻게 설계하는지를 함께 알아볼 예정이다.</p>
<p>문장 테스팅은 이름에서 어느 정도 그 내용이 드러난다. 분기 테스팅은 과연 무엇을 의미할까? 앞서 배운 제어 흐름도와 커버리지 개념을 떠올리며, 기법의 이름만으로 어떤 내용일지 먼저 생각해보자.</p>
<hr />
<h3>Statement Testing; The Most Fundamental White-Box Test Technique</h3>
<p><strong>Statement Testing</strong> is also referred to as syntax testing. A program is composed of countless commands — variable declarations, function logic, and so on — and each individual unit separated by a semicolon (;) is called a <strong>statement</strong>. Statement testing is a method in which input values are selected as test cases so that every statement making up the program is executed at least once. Statement coverage is calculated as follows:</p>
<blockquote>
<p><strong>Statement Coverage = Number of Statements Tested / Total Number of Statements × 100%</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/b92d4f73-3843-4450-a923-bccdd36bda8f.png" alt="" style="display:block;margin:0 auto" /></blockquote>
<p>Consider the grade management example. The code contains a total of three statements. When score = 80 is tested, the first statement and <code>printf("Pass")</code> are executed, but <code>printf("Fail")</code> is not. The statement coverage in this case is therefore 2/3, or 66.666%.</p>
<p>To reach 100%, one more test case is needed to cover the third statement. Adding score = 60 causes <code>printf("Fail")</code> to execute as well. With just two test cases — score 60 and score 80 — all three statements are covered, achieving 100% statement coverage.</p>
<p>This is the essence of white-box testing: directly analyzing the source code and understanding its paths, while designing tests that maximize coverage and minimize the number of test cases. Statement testing is the most fundamental approach to this.  </p>
<h3>문장 테스팅(Statement Testing); 가장 기본적인 화이트박스 테스트 기법</h3>
<p>문장 테스팅(Statement Testing)은 구문 테스팅이라고도 불린다. 프로그램은 변수 선언, 함수 로직 구성 등 수많은 명령어로 구성되어 있는데, 이 중 세미콜론(;)으로 구분되는 하나하나의 단위를 **문장(Statement)**이라고 한다. 문장 테스팅이란 이 프로그램을 구성하는 문장들이 최소한 한 번은 실행될 수 있도록 입력값을 테스트케이스로 선정하여 테스트하는 방법이다. 문장 커버리지는 다음과 같이 계산된다.</p>
<blockquote>
<p><strong>문장 커버리지 = 테스트한 문장 수 / 전체 문장 수 × 100%</strong></p>
</blockquote>
<p>첨부된 성적 관리 기능 예제를 통해 살펴보자. 이 코드에는 총 세 개의 문장이 존재한다. score가 80인 경우를 테스트하면, 첫 번째 문장과 <code>printf("합격")</code>은 실행되지만 <code>printf("불합격")</code>은 실행되지 않는다. 따라서 이 경우의 문장 커버리지는 2/3, 즉 66.666%가 된다.</p>
<p>100%를 달성하려면 세 번째 문장까지 커버할 수 있는 테스트케이스가 하나 더 필요하다. score가 60인 경우를 추가하면 <code>printf("불합격")</code>까지 실행되므로, score 60과 80, 단 두 개의 테스트케이스만으로 세 개의 문장을 모두 커버하여 문장 커버리지 100%를 달성할 수 있다.</p>
<p>이것이 화이트박스 테스트의 핵심이다. 소스코드를 직접 분석하고 경로를 파악하면서, 커버리지는 높이되 테스트케이스의 수는 줄이는 방향으로 테스트를 설계하는 것. 문장 테스팅은 그 중에서도 가장 기본적인 방법이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/98a011c9-0167-465d-82b0-fa3b39c9a2e3.png" alt="" style="display:block;margin:0 auto" />

<h3>Statement Coverage and the Use of Control Flow Graphs</h3>
<p>The key concept that emerges from statement testing is <strong>Statement Coverage</strong>; a metric representing how thoroughly the statements within a program have been tested. The goal is to design test cases that ensure each statement is executed at least once, while keeping the number of test cases as small as possible.</p>
<p>In the flow graph example, each node represents a single statement. With nodes A through K totaling 10, executing the path A → B → D → H → K covers 5 out of 10 statements, resulting in a statement coverage of <strong>50%</strong>. Additional test cases must be designed to cover the remaining nodes and reach 100%.</p>
<p>While test cases can be derived by directly analyzing the source code, using a control flow graph makes it far easier to intuitively understand the flow and derive test cases. This is precisely why control flow graphs are widely used in white-box testing.</p>
<h3>문장 커버리지와 제어 흐름 그래프의 활용</h3>
<p>문장 테스팅에서 핵심적으로 등장하는 개념이 문장 커버리지(Statement Coverage)다. 프로그램 안에 있는 문장들을 얼마나 테스트했는지의 정도를 나타내는 지표로, 각 문장이 적어도 한 번 이상 실행될 수 있도록 테스트케이스를 설계하되, 그 테스트케이스의 수는 최대한 줄이는 방향으로 설계하는 것이 핵심이다. 첨부된 흐름 그래프를 살펴보자. 각각의 노드를 하나의 문장으로 생각해보면, 전체 노드는 A부터 K까지 총 10개다. 여기서 A → B → D → H → K 경로를 실행하면 10개의 문장 중 5개를 경유한 것이므로 문장 커버리지는 50%가 된다. 100%를 달성하려면 나머지 노드들을 커버할 수 있는 추가적인 테스트케이스를 설계해야 한다. 소스코드를 직접 분석해서 테스트케이스를 만들 수도 있지만, 제어 흐름 그래프를 활용하면 흐름을 직관적으로 파악하여 테스트케이스를 도출하기가 훨씬 수월하다. 이것이 바로 화이트박스 테스트에서 제어 흐름 그래프가 널리 활용되는 이유이다.</p>
<hr />
<h3>Statement Testing in Practice; Achieving 100% Statement Coverage</h3>
<p>Let's apply statement testing to a concrete program and work through the process of deriving test cases that achieve 100% statement coverage by representing the source code as a control flow graph.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/05c24235-7161-486a-af29-31edde9770c0.png" alt="" style="display:block;margin:0 auto" />

<p>The program takes <code>X</code>, <code>Y</code>, and <code>Z</code> as inputs and consists of two if statements. The first if statement stores <code>Z</code> divided by <code>X</code> back into <code>Z</code> if <code>X</code> is greater than 1 AND <code>Y</code> equals 1. The second if statement adds 1 to <code>Z</code> if <code>X</code> equals 2 OR <code>Z</code> is greater than 1. Represented as a control flow graph, this produces two branch nodes, each with a yes path executing the statements <code>Z = Z / X</code> and <code>Z = Z + 1</code> respectively. The goal is to create the minimum number of test cases that execute both of these statements.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/a79d1527-210d-497f-93da-036b0120d169.png" alt="" style="display:block;margin:0 auto" />

<p>With <code>X=3</code>, <code>Y=0</code>, <code>Z=1</code> as the first test case, the AND condition in the first if statement fails because <code>Y</code> is not 1, so the flow takes the no path. The second if statement also takes the no path. As a result, neither statement is executed, yielding a statement coverage of <strong>0%</strong>. Despite the effort put into creating this test case, it fails to execute even a single target statement — a poorly designed case.</p>
<p>In contrast, with <code>X=2</code>, <code>Y=1</code>, <code>Z=6</code>, the first AND condition is satisfied (X &gt; 1 and Y = 1), so the yes path is taken and <code>Z = Z / X</code> executes. The second condition is also satisfied (X = 2), so <code>Z = Z + 1</code> executes as well. A single test case covers both statements, achieving <strong>100% statement coverage</strong>.</p>
<p>Well-designed test cases can achieve high coverage with fewer cases, while poorly designed ones can yield wasted effort and meaningless results. Test case design requires experience and know-how, and it is important to consistently practice considering both test suitability and coverage together.</p>
<h3>문장 테스팅 실전 예제; 문장 커버리지 100% 달성하기</h3>
<p>이번에는 실제 코드에 문장 테스팅을 적용하여 문장 커버리지 100%를 만족하는 테스트케이스를 직접 만들어보는 과정을 살펴본다. 소스코드를 제어 흐름도로 표현하고, 이를 바탕으로 테스트케이스를 도출하는 흐름을 따라가 보자.</p>
<p>먼저 코드를 살펴보면, 입력값으로 <code>X, Y, Z</code>를 받으며 두 개의 if문으로 구성되어 있다. 첫 번째 if문은 X가 1보다 크고 AND Y가 1이면 Z를 X로 나눠 Z에 저장하는 조건이고, 두 번째 if문은 X가 2이거나 OR Z가 1보다 크면 Z에 1을 더해 다시 Z에 저장하는 조건이다. 이를 제어 흐름도로 표현하면 두 개의 분기문과 각각의 yes 경로에 실행되는 문장 <code>Z = Z / X, Z = Z + 1</code>이 이어지는 형태가 된다. 여기서 테스트의 목표는 이 두 개의 문장을 모두 실행시키는 최소한의 테스트케이스를 만드는 것이다.</p>
<p>첫 번째 케이스로 X=3, Y=0, Z=1을 넣어보자. 첫 번째 if문의 AND 조건에서 Y가 1이 아니므로 no로 빠지고, 두 번째 if문도 마찬가지로 no로 빠진다. 결과적으로 두 문장 중 단 하나도 실행되지 않아 문장 커버리지는 0%가 된다. 열심히 테스트케이스를 만들었지만 정작 테스트하고자 했던 문장을 하나도 실행하지 못한, 잘못 설계된 케이스다.</p>
<p>반면 X=2, Y=1, Z=6을 넣으면 어떨까? 첫 번째 AND 조건에서 X가 1보다 크고 Y가 1이므로 yes로 빠져 Z = Z / X가 실행된다. 이후 두 번째 조건에서도 X가 2이므로 yes로 빠져 Z = Z + 1까지 실행된다.</p>
<p>결과적으로 단 하나의 테스트케이스만으로 두 개의 문장을 모두 커버하여 문장 커버리지 100%를 달성하게 된다. 이처럼 테스트케이스를 잘 설계하면 적은 수의 케이스로도 높은 커버리지를 달성할 수 있다. 반대로 잘못 설계하면 많은 노력을 기울이고도 엉뚱한 결과가 나올 수 있다. 테스트케이스 설계에는 충분한 경험과 노하우가 필요하며, 테스트 적합성과 커버리지를 함께 고려하는 연습을 꾸준히 해나가는 것이 중요하다.</p>
<hr />
<h3>Statement Testing in Practice 2; VIP Membership Discount Rate Management Program</h3>
<p>The second example applies statement testing to a program that manages discount rates based on VIP membership level. The goal is to create test cases that achieve 100% statement coverage for the <code>calDiscountRate()</code> method.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/6541be59-848c-488a-8578-6837bb09fd6f.png" alt="" style="display:block;margin:0 auto" />

<p>The program takes two inputs: <code>String membership</code> and <code>int payMonths</code>. The discount rate is divided into three conditions: 16% for VIP members who have paid for 6 or more months, 20% for 12 or more months, and 25% for 18 or more months. Including the final <code>return discountRate</code>, the program contains a total of <strong>5 statements</strong>. The goal is to cover all five with the minimum number of test cases.</p>
<p>The first test case covers only 3 statements: the first condition (16%), the entry point of the second condition, and <code>return discountRate</code>. That is 3 out of 5, giving a coverage of <strong>60%</strong>. To reach 100%, test cases covering the 20% and 25% conditions must be added. This leads to the conclusion that at least <strong>3 test cases</strong> are required for this example.</p>
<p>If one case is accidentally missed and only two test cases are executed, the statement corresponding to the 25% discount rate would go untested, leaving statement coverage at <strong>80%</strong>. This illustrates how important it is to carefully analyze the source code, represent it as a control flow graph, and design test cases that efficiently cover every statement.</p>
<h3>문장 테스팅 실전 예제 2;VIP 멤버십 할인율 관리 프로그램</h3>
<p>두 번째 예제는 VIP 멤버십 등급에 따른 할인율을 관리하는 프로그램에 문장 테스팅을 적용한 사례다. <code>calDiscountRate()</code> 메서드를 대상으로 문장 커버리지 100%를 만족하는 테스트케이스를 만들어본다. 이 프로그램은 <code>String membership</code>과 <code>int payMonths</code>, 두 개의 입력을 받는다. 할인율은 세 가지 조건으로 나뉜다. membership이 VIP이면서 6개월 이상 납부한 경우 16%, 12개월 이상인 경우 20%, 18개월 이상인 경우 25%가 적용된다. 마지막 return discountRate까지 포함하여 전체 문장은 총 5개다. 이 다섯 개의 문장을 최소한의 테스트케이스로 커버하는 것이 목표다. 첫 번째 테스트케이스를 실행하면 첫 번째 조건(16%), 두 번째 조건 진입부, 그리고 return discountRate, 이렇게 3개의 문장만 커버된다. 5개 중 3개이므로 커버리지는 60%다. 100%를 달성하려면 20% 조건과 25% 조건을 각각 커버하는 테스트케이스가 반드시 추가되어야 한다. 따라서 이 예제는 적어도 3개의 테스트케이스가 필요하다는 결론이 나온다. 만약 실수로 한 케이스를 빠뜨려 두 개의 테스트케이스만 실행되었다면, 25% 할인율에 해당하는 문장이 실행되지 않아 문장 커버리지는 80%에 그치게 된다. 이처럼 소스코드를 꼼꼼히 분석하고 제어 흐름도로 표현하여, 모든 문장을 효율적으로 커버하는 테스트케이스를 설계하는 것이 얼마나 중요한지를 잘 기억해두자.</p>
<hr />
<h3>Branch Testing; Verifying the True/False of the Overall Condition Expression</h3>
<p>The second white-box testing technique is <strong>Branch Testing</strong>. It involves selecting test cases so that each branch in the program; such as an if statement — is executed once when true and once when false. Since a branch produces two directions (true/false, or yes/no), verifying both is the core objective. Because it involves making a decision (yes or no), it is also referred to as <strong>Decision Testing</strong>.</p>
<p>An important distinction: branch testing targets the <strong>overall condition expression</strong>, not individual conditions. Test cases only need to satisfy the two cases where the overall condition expression is true and where it is false.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/4940e8d1-513f-42c4-89d0-1c0963b8edac.png" alt="" style="display:block;margin:0 auto" />

<p>In the scholarship selection example, the condition is <code>if ((average &gt;= 90) &amp;&amp; (subject count &gt;= 5))</code>. Inputting (95, 6) satisfies both conditions, taking the true path and selecting the student as a scholarship recipient. Inputting (72, 3) fails the average condition, taking the false path and ending without selection. These two test cases alone achieve <strong>100% branch coverage</strong>. If only the yes case is tested, branch coverage would be just <strong>50%</strong>.</p>
<p>Just as with statement testing, it is important to carefully analyze branch structures and design test cases that satisfy both yes and no outcomes while minimizing the total number of cases.</p>
<h3>분기 테스팅(Branch Testing);전체 조건식의 true/false를 검증하라</h3>
<p>화이트박스 테스트의 두 번째 방법은 분기 테스팅(Branch Testing)이다. 프로그램 안에 존재하는 if문과 같은 분기문이 true인 경우와 false인 경우를 모두 한 번씩 실행할 수 있도록 테스트케이스를 선정하여 테스트하는 방법이다. 분기문은 true/false, 즉 yes/no로 나뉘어 총 2개의 분기가 생기는데, 이 두 가지를 모두 검증하는 것이 핵심이다. yes/no와 같이 무언가를 결정하는 역할을 한다는 점에서 결정 테스팅(Decision Testing)이라고도 불린다. 여기서 중요한 점은 분기 테스팅이 개별 조건이 아닌 전체 조건식을 대상으로 한다는 것이다. 전체 조건식이 true인 경우와 false인 경우, 이 두 가지를 만족하는 테스트케이스를 만들면 된다. 장학생 선정 기능 예제를 살펴보자. 평균 점수가 90점 이상이고 수강 과목 수가 5개 이상인 경우 장학생으로 선정하는 조건이다. 즉 if ((평균 &gt;= 90) &amp;&amp; (과목수 &gt;= 5))라는 하나의 전체 조건식이 존재한다. 여기에 (95, 6)을 입력하면 두 조건을 모두 만족하므로 true로 빠져 장학생이 선정되고, (72, 3)을 입력하면 평균이 90 미만이므로 false로 빠져 종료된다. 이 두 가지 테스트케이스만으로 분기 커버리지 100%를 달성할 수 있다. 만약 yes 경우만 테스트된다면 분기 커버리지는 50%에 그치게 된다. 분기 테스팅에서도 마찬가지로, 분기문을 잘 분석하여 yes/no를 모두 만족하면서 테스트케이스의 수는 최소화하는 설계가 중요하다는 점을 기억하자.</p>
<hr />
<h3>Branch Coverage; Verifying Every Branch</h3>
<p>The metric applied when performing branch testing is <strong>Branch Coverage</strong>, also known as <strong>Decision Coverage</strong>. The key is to design test cases that ensure every branch in the program is executed at least once in both its true and false directions.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/56f38df2-28b6-4783-bffe-39c63c4b6544.png" alt="" style="display:block;margin:0 auto" />

<p>In the flow graph, there are 10 total nodes (A through K). Identifying the branches: the first branch splits from A into B and C; the second splits from B into D and E; the third splits from C into F and G. With two directions per branch, the total number of branches is <strong>6</strong>. Achieving 100% branch coverage requires test cases that cover all 6.</p>
<p>Testing the path A → B → D → H → K covers only the first and second branches. With just 2 out of 6 branches visited, branch coverage is approximately <strong>33%</strong>. Just as with statement coverage, it is important to use the control flow graph to grasp the branch structure at a glance and design test cases that cover as many branches as possible with the fewest cases.</p>
<h3>분기 커버리지(Branch Coverage); 모든 분기를 검증하라</h3>
<p>분기 테스팅을 수행할 때 적용되는 지표가 바로 분기 커버리지(Branch Coverage)다. 결정 커버리지(Decision Coverage)라고도 불리며, 프로그램 내 존재하는 모든 분기가 true인 경우와 false인 경우를 적어도 한 번씩 실행할 수 있도록 테스트케이스를 설계하는 것이 핵심이다. 첨부된 흐름도를 살펴보자.</p>
<p>전체 노드는 A부터 K까지 총 10개다. 여기서 분기가 몇 개 존재하는지 파악해보면, A에서 B와 C로 나뉘는 첫 번째 분기, B에서 D와 E로 나뉘는 두 번째 분기, C에서 F와 G로 나뉘는 세 번째 분기가 존재한다. 각 분기마다 yes/no 두 방향이 생기므로 전체 분기 수는 6개가 된다. 분기 커버리지 100%를 달성하려면 이 6개의 분기를 모두 커버하는 테스트케이스를 만들어야 한다. A → B → D → H → K 경로를 테스트했다고 가정하면, 이 경로에서는 첫 번째 분기와 두 번째 분기만 커버된다. 결과적으로 6개의 분기 중 2개만 방문한 것이므로 분기 커버리지는 약 33%에 그치게 된다. 이것이 분기 커버리지의 개념이다. 문장 커버리지와 마찬가지로, 흐름도를 통해 분기 구조를 한눈에 파악하고 최소한의 테스트케이스로 최대한 많은 분기를 커버하는 설계가 중요하다.</p>
<hr />
<h3>Branch Testing in Practice; Achieving 100% Branch Coverage</h3>
<p>Applying branch testing to the same program used in the statement testing examples, the goal is to create test cases that achieve 100% branch coverage. Since the program and its control flow graph have already been explained, the focus here is on the branches.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/fae063eb-5199-4feb-af50-9ef57d8a1206.png" alt="" style="display:block;margin:0 auto" />

<p>The program has 2 branch nodes. With two directions per node, the total number of branches is <strong>4</strong>. The goal is to derive the minimum test cases that cover all 4 branches.</p>
<p>With X=3, Y=0, Z=1, the AND condition in branch 1 is not satisfied, flowing to false (no), and branch 2 also takes the false (no) path. With X=2, Y=1, Z=6, both branch 1 and branch 2 take the true (yes) path. Two test cases cover all 4 branches, achieving a branch coverage of <strong>4/4 = 100%</strong>.</p>
<p>One key observation: these two test cases are identical to those used in statement testing. In other words, the same test cases simultaneously satisfy both statement coverage and branch coverage. Well-designed test cases can fulfill multiple coverage criteria at once. Achieving multiple coverages with the minimum number of test cases — this is the essence of test design strategy.</p>
<p>This chapter covered two of the most representative white-box testing techniques: statement testing and branch testing.</p>
<h3>분기 테스팅 실전 예제; 분기 커버리지 100% 달성하기</h3>
<p>앞서 문장 테스팅에서 다뤘던 동일한 프로그램에 이번에는 분기 테스팅을 적용하여 분기 커버리지 100%를 만족하는 테스트케이스를 만들어보자. 프로그램과 제어 흐름도에 대한 설명은 문장 테스팅에서 이미 다루었으므로, 여기서는 분기에 집중한다.</p>
<p>이 프로그램에는 분기 노드가 총 2개 존재한다. 각 분기 노드마다 yes/no 두 방향이 생기므로 전체 분기 수는 <strong>4개</strong>다. 따라서 이 4개의 분기를 모두 커버하는 최소한의 테스트케이스를 도출하는 것이 목표다.</p>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/5318e66c-a3e6-4eb4-80c7-574944abc11f.png" alt="" style="display:block;margin:0 auto" />

<p>첫 번째 테스트케이스로 X=3, Y=0, Z=1을 입력하면, 분기1에서 AND 조건을 만족하지 못해 false(no)로 흘러가고, 분기2도 마찬가지로 false(no)로 빠진다. 두 번째 테스트케이스로 X=2, Y=1, Z=6을 입력하면, 분기1과 분기2 모두 true(yes)로 흘러간다. 결과적으로 두 개의 테스트케이스만으로 4개의 분기를 모두 커버하여 분기 커버리지 4/4 = 100%를 달성하게 된다.</p>
<p>여기서 주목할 점이 있다. 이 두 테스트케이스는 앞서 문장 테스팅에서 사용했던 입력값과 동일하다. 즉, 같은 테스트케이스로 문장 커버리지와 분기 커버리지를 동시에 만족시킨 것이다. 이처럼 잘 설계된 테스트케이스는 여러 커버리지를 한꺼번에 충족시킬 수 있다. 최소한의 테스트케이스로 다양한 커버리지를 동시에 만족하는 것, 이것이 테스트 설계 전략의 핵심임을 꼭 기억해두자.</p>
<p>이번 챕터에서는 화이트박스 테스트의 대표적인 두 가지 기법인 문장 테스팅과 분기 테스팅에 대해 살펴보았다.</p>
<hr />
<h1>3️⃣ Conditon Testing</h1>
<p>이번 챕터에서는 조건테스팅에 대해서 살펴본다. 조건테스팅은 무엇이고 이를 살펴보는 예시에 대해서 살펴본다. 먼저 잠깐 생각해보면 - 조건테스팅은 무엇인지 이름만으로 잠시 생각해보자.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/e7f22782-7a0f-4633-af67-5500e316ce99.png" alt="" style="display:block;margin:0 auto" />

<h3>조건 테스팅(Condition Testing); 개별 조건을 검증하라</h3>
<p>세 번째 기법은 조건 테스팅(Condition Testing)이다. 앞서 배운 분기 테스팅과의 차이점을 명확히 이해하는 것이 중요하다. 분기 테스팅은 분기문의 전체 조건식이 true인 경우와 false인 경우를 대상으로 테스트하는 방법이었다. 반면 조건 테스팅은 전체 조건식의 결과와 관계없이 각 개별 조건이 true 한 번, false 한 번을 갖도록 테스트케이스를 선정하여 테스트하는 방법이다. 조건 커버리지는 다음과 같이 계산된다.</p>
<blockquote>
<p>조건 커버리지 = 테스트한 개별 조건의 T/F 수 / 전체 개별 조건의 T/F 수 × 100%</p>
</blockquote>
<p>장학생 선정 기능 예제를 통해 살펴보자. if ((평균 &gt;= 90) &amp;&amp; (과목수 &gt;= 5))라는 분기문에서 전체 조건식은 (평균 &gt;= 90) &amp;&amp; (과목수 &gt;= 5) 전체이고, 개별 조건은 평균 &gt;= 90과 과목수 &gt;= 5, 이 두 가지다. 분기 테스팅은 이 전체 조건식의 true/false를 기준으로 삼지만, 조건 테스팅은 각각의 개별 조건이 모두 true와 false를 한 번씩 경험하도록 테스트케이스를 설계한다. (95, 6)과 (72, 3)을 테스트하면, 첫 번째 케이스에서 두 개별 조건이 모두 true, 두 번째 케이스에서 모두 false가 되어 총 4가지 경우를 모두 만족하게 된다. 결과적으로 조건 커버리지 100%를 달성하게 된다. 개별 조건 각각을 검증하는 만큼, 분기 테스팅에 비해 테스트케이스의 수가 일반적으로 많아질 수밖에 없다는 점도 함께 기억해두자.</p>
<hr />
<h2>분기와 조건의 차이 — 헷갈리지 않게 정리하기</h2>
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/ea844437-b31d-4343-86b7-66cfc16e2a87.png" alt="" style="display:block;margin:0 auto" />

<p>분기 테스팅과 조건 테스팅의 차이를 명확히 이해하기 위해 구체적인 예시로 다시 한번 정리해보자.</p>
<p>"VIP이고 6개월 이상 자동 결제를 하면 16%의 요금을 할인 받는다"는 조건이 있다고 할 때, 여기서 <strong>분기</strong>는 이 전체 조건식, 즉 할인을 받느냐 받지 않느냐를 결정하는 것으로 <strong>1개</strong>다. 반면 <strong>개별 조건</strong>은 'VIP인가'와 '6개월 이상 자동 결제를 했는가', 이렇게 <strong>2개</strong>가 된다.</p>
<p>분기 테스팅은 이 전체 조건식이 true인 경우와 false인 경우를 대상으로 테스트하고, 조건 테스팅은 각각의 개별 조건인 VIP 여부와 납부 개월 수가 각각 true와 false인 경우를 대상으로 테스트한다는 점이 두 기법의 핵심적인 차이다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/1864f392-7d72-45b4-9895-0fa1bd7c2c8b.png" alt="" style="display:block;margin:0 auto" />

<h2>조건 테스팅 실전 예제; 조건 커버리지 100% 달성하기 (1/2)</h2>
<p>앞서 문장 테스팅과 분기 테스팅에서 다뤘던 동일한 프로그램에 이번에는 조건 테스팅을 적용하여 조건 커버리지 100%를 만족하는 테스트케이스를 작성해보자.</p>
<p>먼저 이 프로그램의 구조를 조건 테스팅의 관점에서 분석해보자. 분기는 총 2개이고, 각 분기 안의 개별 조건을 살펴보면 다음과 같다. 첫 번째 분기 <code>IF ((X &gt; 1) AND (Y == 1))</code>에서 'X가 1보다 큰가'가 개별 조건1, 'Y가 1인가'가 개별 조건2가 된다. 두 번째 분기 <code>IF ((X == 2) OR (Z &gt; 1))</code>에서 'X가 2인가'가 개별 조건3, 'Z가 1보다 큰가'가 개별 조건4가 된다. 결과적으로 개별 조건은 총 <strong>4개</strong>다.</p>
<p>조건 테스팅에서는 이 4개의 개별 조건 각각에 대해 true와 false를 한 번씩 검증해야 한다. 따라서 총 <strong>8가지 경우</strong>를 만족해야 조건 커버리지 100%를 달성할 수 있다. 이 8가지 경우를 최소한의 테스트케이스로 커버하는 설계가 필요하다.</p>
<hr />
<img src="https://cdn.hashnode.com/uploads/covers/649932cadd0a52758cdbdeb5/d96c7c6f-3d3c-4707-a85b-634de45efbf4.png" alt="" style="display:block;margin:0 auto" />

<h2>조건 테스팅 실전 예제; 조건 커버리지 100% 달성하기 (2/2)</h2>
<p>이번에는 실제로 테스트케이스를 어떻게 도출했는지 살펴보자. 목표는 4개의 개별 조건 각각에 대해 true와 false를 모두 만족하는, 즉 총 8개의 경우를 커버하는 최소한의 테스트케이스를 만드는 것이다. 이것이 바로 <strong>테스트 적합성</strong>이다.</p>
<p>첫 번째 테스트케이스로 X=0, Y=0, Z=1을 입력해보자. 조건1에서 X가 1보다 커야 하는데 0이므로 false, 조건2에서 Y가 1이어야 하는데 0이므로 false, 조건3에서 X가 2여야 하는데 0이므로 false, 조건4에서 Z가 1보다 커야 하는데 Z=1이므로 false가 된다. 하나의 테스트케이스로 4개의 개별 조건을 모두 false로 커버하는 아주 좋은 케이스다.</p>
<p>두 번째 테스트케이스로 X=2, Y=1, Z=6을 입력하면, 조건1에서 X가 1보다 크므로 true, 조건2에서 Y가 1이므로 true, 조건3에서 X가 2이므로 true, 조건4에서 Z가 1보다 크므로 true가 된다. 마찬가지로 하나의 케이스로 4개의 개별 조건을 모두 true로 커버한다.</p>
<p>결과적으로 단 <strong>2개의 테스트케이스</strong>만으로 8개의 개별 조건을 모두 만족하여 조건 커버리지 8/8 = 100%를 달성하게 된다.</p>
<p>시간과 비용, 자원이 충분하다면 여러 테스트케이스를 만들어 다양하게 테스트할 수 있겠지만, 실제 프로젝트에서는 항상 제약이 존재한다. 주어진 자원을 효율적으로 활용하는 것이 프로젝트의 목표이며, 이를 위해 테스트 전략을 잘 수립하고 적합성 높은 테스트케이스를 설계하는 능력이 필요하다. 이러한 전문성은 다양한 케이스를 직접 만들어보는 연습을 통해 쌓을 수 있으며, 테스트 설계와 관리 관련 자격증을 취득하는 것도 도움이 될 수 있다.</p>
<p>지금까지 화이트박스 테스트의 주요 기법인 함수 커버리지, 문장 테스팅, 분기 테스팅, 조건 테스팅까지 커버리지를 높이기 위한 각각의 테스팅 기법에 대해 살펴보았다.</p>
]]></content:encoded></item></channel></rss>