<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>민호의 개발 블로그</title>
        <link>https://velog.io/</link>
        <description>개발자를 꿈꾸고 있어요</description>
        <lastBuildDate>Fri, 02 Oct 2026 17:26:23 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>민호의 개발 블로그</title>
            <url>https://velog.velcdn.com/images/minho-git/profile/f2cc2f1a-e520-4001-8a03-92e256a2944c/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 민호의 개발 블로그. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/minho-git" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[HTTPS 동작 원리: RSA 키 교환부터 ECDHE, MITM]]></title>
            <link>https://velog.io/@minho-git/HTTPS-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC-RSA-%ED%82%A4-%EA%B5%90%ED%99%98%EB%B6%80%ED%84%B0-ECDHE-MITM</link>
            <guid>https://velog.io/@minho-git/HTTPS-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC-RSA-%ED%82%A4-%EA%B5%90%ED%99%98%EB%B6%80%ED%84%B0-ECDHE-MITM</guid>
            <pubDate>Fri, 02 Oct 2026 17:26:23 GMT</pubDate>
            <description><![CDATA[<p>&quot;HTTPS는 어떻게 동작할까?&quot;라는 질문을 파고들다 보니 생각보다 엮여 있는 개념이 많았습니다. 이번 글에서는 제가 공부하면서 헷갈렸던 순서를 그대로 따라가 보려고 합니다.</p>
<p>처음에는 &quot;공개키로 암호화하면 되는 것 아닌가?&quot;라고 생각했습니다. 그런데 곧 &quot;그 공개키는 어떻게 믿을 수 있지?&quot;라는 의문이 생겼고, 이어서 &quot;중간에 누군가 끼어들면 어떻게 되지?&quot;라는 질문으로 넘어가게 되었습니다. 이 흐름대로 하나씩 정리해 보겠습니다.</p>
<hr>
<h2 id="1-http는-왜-위험할까">1. HTTP는 왜 위험할까</h2>
<p>HTTP는 데이터를 평문 그대로 전송합니다. 클라이언트와 서버 사이에는 공유기, ISP, 라우터 같은 장비가 수없이 많은데, 그중 어느 지점에서든 패킷을 들여다볼 수 있다면 다음과 같은 문제가 생깁니다.</p>
<table>
<thead>
<tr>
<th>문제</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>도청</td>
<td>비밀번호나 카드번호를 그대로 읽을 수 있습니다</td>
</tr>
<tr>
<td>변조</td>
<td>중간에서 내용을 바꿔치기할 수 있습니다</td>
</tr>
<tr>
<td>사칭</td>
<td>접속한 서버가 진짜인지 확인할 수 없습니다</td>
</tr>
</tbody></table>
<p>이 문제를 해결하기 위해 HTTP를 TLS로 감싼 것이 HTTPS입니다. 참고로 SSL은 TLS의 이전 이름입니다. SSL은 보안 취약점 때문에 더 이상 사용되지 않고, 현재는 TLS 1.2와 TLS 1.3이 주로 쓰이고 있습니다.</p>
<hr>
<h2 id="2-먼저-알아야-할-개념-대칭키와-비대칭키">2. 먼저 알아야 할 개념: 대칭키와 비대칭키</h2>
<h3 id="대칭키">대칭키</h3>
<p>대칭키 방식은 암호화할 때와 복호화할 때 같은 키를 사용합니다. 속도가 빠르다는 장점이 있지만, 이 키를 상대방에게 어떻게 안전하게 전달할 것인가라는 문제가 남습니다.</p>
<h3 id="비대칭키-공개키--개인키">비대칭키 (공개키 / 개인키)</h3>
<p>비대칭키 방식은 키가 한 쌍으로 이루어져 있습니다. 공개키는 누구에게나 공개해도 되고, 개인키는 주인만 보관합니다. 쓰임새는 크게 두 가지입니다.</p>
<ul>
<li>암호화: 공개키로 암호화한 데이터는 개인키로만 복호화할 수 있습니다.</li>
<li>서명: 개인키로 서명한 데이터는 공개키로 검증할 수 있고, 이를 통해 실제 주인이 서명했다는 사실을 확인할 수 있습니다.</li>
</ul>
<p>다만 대칭키에 비해 연산이 느립니다.</p>
<p>그래서 TLS는 두 방식을 함께 사용합니다. 비대칭키로 안전한 연결을 준비하고, 실제 데이터는 빠른 대칭키(세션 키)로 주고받는 구조입니다.</p>
<hr>
<h2 id="3-처음-떠올린-방법-rsa-키-교환-tls-12까지">3. 처음 떠올린 방법: RSA 키 교환 (TLS 1.2까지)</h2>
<p>가장 직관적인 방법은 다음과 같습니다.</p>
<ol>
<li>서버는 통신을 시작하기 전에 공개키와 개인키를 미리 만들어 둡니다. 공개키는 암호화에, 개인키는 복호화에 사용합니다.</li>
<li>클라이언트는 세션 키(대칭키)를 생성합니다. 이 키 하나로 암호화와 복호화를 모두 처리합니다.</li>
<li>클라이언트는 서버로부터 공개키를 받습니다.</li>
<li>받은 공개키로 세션 키를 암호화해서 서버에 보냅니다.</li>
<li>서버는 개인키로 이를 복호화해 세션 키를 얻습니다.</li>
<li>이제 양쪽은 같은 세션 키로 데이터를 암호화해 보내고, 받은 쪽에서 복호화합니다.</li>
</ol>
<pre><code>  Client                                           Server
    |                                                 |  (1) generate key pair
    |  (2) generate session key                       |
    |                                                 |
    |&lt;----------------------- (3) server public key --|
    |                                                 |
    |-- (4) Enc(public key, session key) ------------&gt;|
    |                                                 |  (5) Dec(private key)
    |                                                 |      -&gt; session key
    |                                                 |
    |&lt;======= (6) encrypted with session key ========&gt;|</code></pre><p>누군가 중간에서 엿보더라도 암호화된 세션 키만 보일 뿐이고, 개인키가 없으니 풀 수 없습니다. 언뜻 보면 빈틈이 없어 보입니다.</p>
<p>참고로 실제 TLS에서는 세션 키를 그대로 보내지 않습니다. pre-master secret이라는 비밀값을 보낸 뒤, 양쪽이 이 값을 바탕으로 세션 키를 만들어 냅니다. 하지만 공개키로 암호화해서 보내고 개인키로 푼다는 원리 자체는 동일합니다.</p>
<p>문제는 3번 단계에 있습니다.</p>
<hr>
<h2 id="4-그-공개키는-정말-서버의-것일까">4. 그 공개키는 정말 서버의 것일까</h2>
<p>클라이언트에게는 받은 공개키가 진짜 서버의 것인지 확인할 방법이 없습니다. 바로 이 지점에서 MITM(Man-In-The-Middle), 즉 중간자 공격이 가능해집니다.</p>
<h3 id="mitm-공격-시나리오">MITM 공격 시나리오</h3>
<p>공격자가 공용 와이파이 같은 환경에서 클라이언트와 서버 사이에 끼어들었다고 가정해 보겠습니다.</p>
<pre><code>  Client                    Attacker                  Server
    |                         |                         |
    |                         |&lt;------ server pub ------|
    |&lt;----- attacker pub -----|                         |
    |                         |                         |  (1) swap public key
    |                         |                         |
    |-- Enc(attacker pub, ---&gt;|                         |
    |    session key)         |                         |
    |                         | Dec(attacker priv)      |  (2)(3)
    |                         | -&gt; session key!         |
    |                         |                         |
    |                         |--- Enc(server pub, ----&gt;|
    |                         |    session key)         |  (4)
    |                         |                         |
    |&lt;========== reads &amp; modifies everything ==========&gt;|</code></pre><ol>
<li>공격자는 서버의 공개키를 가로채고, 자신의 공개키를 서버의 것처럼 클라이언트에게 전달합니다.</li>
<li>클라이언트는 이를 모른 채 공격자의 공개키로 세션 키를 암호화합니다.</li>
<li>공격자는 자신의 개인키로 이를 복호화해 세션 키를 손에 넣습니다.</li>
<li>그런 다음 서버의 공개키로 다시 암호화해서 서버에 넘깁니다.</li>
</ol>
<p>클라이언트와 서버는 모두 정상적으로 통신하고 있다고 믿지만, 실제로는 공격자가 대화 내용을 모두 읽을 수 있고 필요하면 바꿀 수도 있습니다.</p>
<p>여기서 주목할 점은 암호화 자체에는 아무 문제가 없었다는 것입니다. 빠져 있던 것은 &quot;지금 누구와 암호화된 통신을 하고 있는가&quot;에 대한 확인이었습니다.</p>
<hr>
<h2 id="5-해결책-ca와-인증서">5. 해결책: CA와 인증서</h2>
<p>그렇다면 신뢰할 수 있는 제3자가 &quot;이 공개키는 naver.com의 것이 맞다&quot;고 보증해 주면 됩니다. 이 역할을 하는 기관이 CA(Certificate Authority, 인증기관)입니다.</p>
<h3 id="5-1-인증서-발급-과정">5-1. 인증서 발급 과정</h3>
<ol>
<li>서버가 키 쌍을 직접 생성합니다. 개인키는 절대 서버 밖으로 나가지 않아야 합니다. CA가 대신 만들어 준다면 CA도 개인키를 알게 되기 때문입니다.</li>
<li>서버는 공개키와 도메인 정보를 담은 요청서(CSR)를 CA에 제출합니다.</li>
<li>CA(정확히는 등록 업무를 담당하는 RA)가 요청자가 실제 도메인 소유자인지 확인합니다.</li>
<li>확인이 끝나면 CA가 인증서를 만듭니다. 인증서 내용(도메인, 서버 공개키, 유효기간, 발급자 등)으로 해시값을 계산하고, 그 해시값에 CA의 개인키로 서명합니다.</li>
<li>인증서 내용과 CA의 서명을 묶어 서버에 발급합니다.</li>
</ol>
<p>저는 처음에 &quot;CA가 서버의 공개키를 암호화한다&quot;고 잘못 이해하고 있었습니다. 실제로 서버의 공개키는 인증서 안에 그대로 노출되어 있습니다. CA가 하는 일은 암호화가 아니라 서명입니다. 내용을 숨기려는 것이 아니라, CA가 이 내용을 보증하며 중간에 변경되지 않았다는 사실을 증명하려는 것입니다.</p>
<h3 id="5-2-인증서의-구성">5-2. 인증서의 구성</h3>
<pre><code>+--------------------------------------------+
|  Certificate (X.509)                       |
+--------------------------------------------+
|  Version       : v3                        |
|  Serial Number : 04:a1:9f:...              |
|  Issuer        : DigiCert ... CA           |
|  Validity      : 2026-01-01 ~ 2027-01-01   |
|  Subject       : www.naver.com             |
|  Public Key    : server public key         |
|  Extensions    : SAN: *.naver.com, ...     |
+--------------------------------------------+
|  Sig. Alg. : sha256WithRSAEncryption       |
|  Signature : Sign(CA private key,          |
|                   hash(contents above))    |
+--------------------------------------------+</code></pre><h3 id="5-3-클라이언트의-인증서-검증">5-3. 클라이언트의 인증서 검증</h3>
<p>운영체제와 브라우저에는 신뢰할 수 있는 CA의 인증서(공개키)가 미리 내장되어 있습니다. 이를 루트 인증서 저장소라고 부릅니다. Microsoft, Apple, Google, Mozilla가 각자 기준을 정해 두고, 그 기준을 통과한 CA만 목록에 포함시킵니다.</p>
<p>검증은 다음 순서로 이루어집니다.</p>
<ol>
<li>인증서 내용으로 해시값을 직접 계산합니다.</li>
<li>인증서에 첨부된 서명을 미리 가지고 있던 CA의 공개키로 검증합니다.</li>
<li>서명이 일치하면 CA가 보증한 내용이며 중간에 변조되지 않았다는 뜻입니다.</li>
<li>추가로 인증서의 도메인이 현재 접속하려는 주소와 같은지, 유효기간이 지나지 않았는지를 확인합니다.</li>
</ol>
<p>실제로는 루트 CA가 서버 인증서에 직접 서명하지 않고, 루트 CA에서 중간 CA를 거쳐 서버 인증서로 서명이 이어집니다. 이를 인증서 체인이라고 하며, 브라우저는 이 체인을 따라 올라가 루트 저장소에 있는 CA까지 도달하는지 확인합니다.</p>
<h3 id="5-4-인증서를-그대로-복사하면-어떻게-될까">5-4. 인증서를 그대로 복사하면 어떻게 될까</h3>
<p>인증서는 공개된 정보이므로 공격자도 naver.com의 실제 인증서를 그대로 복사해 보여줄 수 있습니다. 하지만 공격자에게는 그 인증서에 담긴 공개키와 짝을 이루는 개인키가 없습니다.</p>
<ul>
<li>RSA 키 교환에서는 클라이언트가 인증서 속 공개키로 세션 키를 암호화해 보냅니다. 개인키가 없는 공격자는 이를 복호화할 수 없습니다.</li>
<li>TLS 1.3에서는 서버가 핸드셰이크 내용에 자신의 개인키로 서명해서 보냅니다(<code>CertificateVerify</code>). 개인키가 없는 공격자는 이 서명을 만들어 낼 수 없습니다.</li>
</ul>
<p>결국 인증서는 &quot;이 공개키가 naver.com의 것&quot;임을 확인해 주고, 개인키를 실제로 사용할 수 있는지가 &quot;지금 통신하는 상대가 그 주인&quot;임을 확인해 줍니다. 두 가지가 모두 갖춰져야 신원 확인이 완료됩니다.</p>
<h3 id="5-5-mitm이-막히는-원리">5-5. MITM이 막히는 원리</h3>
<p>공격자가 시도할 수 있는 방법은 크게 두 가지인데, 어느 쪽도 성공하지 못합니다.</p>
<table>
<thead>
<tr>
<th>공격자의 시도</th>
<th>결과</th>
</tr>
</thead>
<tbody><tr>
<td>자신의 공개키로 가짜 인증서를 만들어 제시</td>
<td>신뢰할 수 있는 CA의 서명이 없거나 도메인이 일치하지 않아 브라우저가 경고를 표시합니다</td>
</tr>
<tr>
<td>실제 인증서를 복사해서 제시</td>
<td>개인키가 없으므로 복호화도, 서명도 할 수 없습니다</td>
</tr>
</tbody></table>
<hr>
<h2 id="6-남은-문제-개인키가-나중에-유출된다면">6. 남은 문제: 개인키가 나중에 유출된다면</h2>
<p>RSA 키 교환에는 또 하나의 약점이 있습니다. 서버의 개인키 하나만 있으면 모든 세션 키를 풀 수 있다는 점입니다.</p>
<p>공격자가 오늘 오가는 암호화된 통신을 모두 저장해 두었다가, 몇 년 뒤 서버의 개인키를 탈취했다고 생각해 보겠습니다. 그러면 저장해 둔 세션 키를 복호화할 수 있고, 결과적으로 과거의 대화 내용까지 전부 해독할 수 있게 됩니다.</p>
<p>이 문제를 해결하기 위해 등장한 방식이 ECDHE입니다.</p>
<h3 id="6-1-ecdhe-세션-키를-아예-보내지-않는-방식">6-1. ECDHE: 세션 키를 아예 보내지 않는 방식</h3>
<p>ECDHE의 아이디어는 세션 키를 암호화해서 전송하는 대신, 양쪽이 각자 계산해서 같은 값을 얻도록 하자는 것입니다.</p>
<p>작은 숫자로 직접 계산해 보면 쉽게 이해할 수 있습니다. 아래 예시는 원조 격인 DH 방식이며, ECDHE는 거듭제곱 대신 타원곡선 연산을 사용한다는 차이만 있을 뿐 원리는 같습니다.</p>
<p><strong>① 미리 공개된 숫자</strong></p>
<p>표준으로 정해져 있어 브라우저와 서버 코드에 이미 포함되어 있는 값입니다.</p>
<ul>
<li>g = 5, p = 23 (<code>mod 23</code>은 23으로 나눈 나머지를 의미합니다)</li>
</ul>
<p><strong>② 각자 비밀 숫자 선택 (절대 전송하지 않음)</strong></p>
<ul>
<li>클라이언트: a = 6</li>
<li>서버: b = 15</li>
</ul>
<p><strong>③ 공개값을 계산해서 교환</strong></p>
<ul>
<li>클라이언트: 5⁶ mod 23 = 8 → 서버에 전송</li>
<li>서버: 5¹⁵ mod 23 = 19 → 클라이언트에 전송</li>
</ul>
<p><strong>④ 받은 공개값에 자신의 비밀 숫자로 계산</strong></p>
<ul>
<li>클라이언트: 19⁶ mod 23 = 2</li>
<li>서버: 8¹⁵ mod 23 = 2</li>
</ul>
<p>양쪽 모두 2라는 같은 값을 얻었습니다. 이것이 공유 비밀값입니다.</p>
<p>같은 값이 나오는 이유는 간단합니다. 클라이언트가 계산한 19⁶은 (5¹⁵)⁶, 즉 5^(15×6)이고, 서버가 계산한 8¹⁵는 (5⁶)¹⁵, 즉 5^(6×15)입니다. 곱하는 순서만 다를 뿐 둘 다 5^(a×b)를 계산한 셈입니다.</p>
<p>반면 중간에서 엿본 사람은 5, 23, 8, 19만 알고 있습니다. 공유 비밀값을 구하려면 &quot;5를 몇 번 거듭제곱해야 8이 되는가&quot;를 풀어야 하는데, 실제로는 수백 자리에 달하는 큰 수를 사용하기 때문에 현실적으로 풀 수 없습니다.</p>
<h3 id="6-2-전방-비밀성-forward-secrecy">6-2. 전방 비밀성 (Forward Secrecy)</h3>
<p>ECDHE의 E는 Ephemeral, 즉 &quot;일회용&quot;을 뜻합니다. 비밀 숫자는 연결할 때마다 새로 생성되고, 연결이 끝나면 폐기됩니다.</p>
<p>또한 서버의 개인키는 더 이상 세션 키 계산에 쓰이지 않고 신원 증명(서명)에만 사용됩니다. 따라서 나중에 개인키가 유출되더라도 과거의 대화는 안전하게 보호됩니다. 이 성질을 전방 비밀성이라고 합니다.</p>
<table>
<thead>
<tr>
<th></th>
<th>RSA 키 교환</th>
<th>ECDHE</th>
</tr>
</thead>
<tbody><tr>
<td>세션 키</td>
<td>클라이언트가 생성해 암호화 후 전송</td>
<td>전송하지 않고 양쪽이 각자 계산</td>
</tr>
<tr>
<td>서버 개인키의 역할</td>
<td>세션 키 복호화</td>
<td>신원 증명(서명)만 담당</td>
</tr>
<tr>
<td>개인키 유출 시</td>
<td>과거 대화까지 모두 노출</td>
<td>과거 대화는 안전</td>
</tr>
<tr>
<td>TLS 1.3</td>
<td>제거됨</td>
<td>유일하게 남은 방식</td>
</tr>
</tbody></table>
<h3 id="6-3-ecdhe만으로-mitm을-막을-수-있을까">6-3. ECDHE만으로 MITM을 막을 수 있을까</h3>
<p>그렇지 않습니다. ECDHE만으로는 중간자 공격을 막을 수 없습니다.</p>
<p>공격자가 클라이언트와는 자신의 공개값으로, 서버와는 또 다른 공개값으로 각각 키 교환을 진행하면 두 개의 연결을 따로 맺고 그 사이에서 중계할 수 있기 때문입니다. 4장에서 살펴본 공격과 구조가 똑같습니다.</p>
<p>그래서 TLS 1.3에서는 서버가 ECDHE 공개값이 포함된 핸드셰이크 전체에 개인키로 서명합니다. 공격자가 공개값을 바꿔치기하면 서명이 일치하지 않아 곧바로 드러나게 됩니다.</p>
<p>정리하자면 ECDHE는 도청을 막고, 인증서와 서명은 사칭을 막습니다. 안전한 통신을 위해서는 둘 다 필요합니다.</p>
<hr>
<h2 id="7-tls-13-핸드셰이크-전체-흐름">7. TLS 1.3 핸드셰이크 전체 흐름</h2>
<pre><code>  Client                                           Server
    |                                                 |
    |-- ClientHello ---------------------------------&gt;|
    |  random, key_share (client ECDHE pub)           |
    |                                                 |
    |&lt;--------------------------------- ServerHello --|
    |  random, key_share (server ECDHE pub)           |
    |                                                 |
    |      [ both compute shared secret -&gt; keys ]     |
    |                                                 |
    |&lt;------------------------------- {Certificate} --|
    |&lt;------------------------- {CertificateVerify} --|
    |&lt;---------------------------------- {Finished} --|
    |                                                 |
    |-- {Finished} ----------------------------------&gt;|
    |                                                 |
    |&lt;============== application data ===============&gt;|

  {...} = encrypted</code></pre><table>
<thead>
<tr>
<th>단계</th>
<th>하는 일</th>
</tr>
</thead>
<tbody><tr>
<td>ClientHello / ServerHello</td>
<td>사용할 방식을 협상하고, 랜덤값과 키 교환 재료를 주고받습니다</td>
</tr>
<tr>
<td>세션 키 계산</td>
<td>각자 공유 비밀값을 계산한 뒤 랜덤값과 조합해 세션 키를 만듭니다</td>
</tr>
<tr>
<td>Certificate</td>
<td>서버가 인증서를 보내고, 브라우저가 CA 체인을 따라 검증합니다</td>
</tr>
<tr>
<td>CertificateVerify</td>
<td>서버가 개인키로 서명해 인증서의 실제 주인임을 증명합니다</td>
</tr>
<tr>
<td>Finished</td>
<td>지금까지 주고받은 메시지가 변조되지 않았는지 서로 확인합니다</td>
</tr>
</tbody></table>
<p>TLS 1.2에서는 핸드셰이크에 왕복이 두 번 필요했지만, TLS 1.3은 키 교환 재료를 첫 메시지에 바로 실어 보내면서 왕복 한 번(1-RTT)으로 줄였습니다. 세션 키가 더 일찍 만들어지기 때문에 인증서부터 암호화된 상태로 전송된다는 점도 달라진 부분입니다.</p>
<h3 id="헷갈리기-쉬운-세-종류의-키">헷갈리기 쉬운 세 종류의 키</h3>
<table>
<thead>
<tr>
<th>키</th>
<th>생성 시점</th>
<th>용도</th>
</tr>
</thead>
<tbody><tr>
<td>ECDHE 비밀 숫자 / 공개값</td>
<td>연결마다 새로 생성하고 폐기</td>
<td>공유 비밀값 계산</td>
</tr>
<tr>
<td>서버 개인키 / 공개키</td>
<td>서버가 미리 생성해 장기간 보관</td>
<td>신원 증명(서명)</td>
</tr>
<tr>
<td>세션 키</td>
<td>핸드셰이크 중 양쪽이 계산</td>
<td>실제 데이터 암호화(대칭키)</td>
</tr>
</tbody></table>
<hr>
<h2 id="8-그럼에도-mitm이-성공하는-경우">8. 그럼에도 MITM이 성공하는 경우</h2>
<p>TLS가 중간자 공격을 막아 주기는 하지만, 다음과 같은 상황에서는 뚫릴 수 있습니다.</p>
<p><strong>사용자가 인증서 경고를 무시하는 경우</strong>
&quot;이 연결은 안전하지 않습니다&quot;라는 경고 화면에서 &quot;계속 진행&quot;을 누르면 공격자의 인증서를 신뢰하게 됩니다.</p>
<p><strong>공격자의 루트 인증서가 PC에 설치된 경우</strong>
악성 프로그램이 루트 저장소에 가짜 CA를 등록하면, 그 CA로 서명한 인증서는 모두 검증을 통과합니다. 사실 회사의 보안 프록시가 HTTPS 트래픽을 검사하는 것도 이와 같은 방식입니다.</p>
<p><strong>CA가 해킹당하거나 인증서를 잘못 발급한 경우</strong>
이에 대비해 인증서 폐기 목록(CRL, OCSP)과 발급 기록을 공개하는 Certificate Transparency 같은 장치가 마련되어 있습니다.</p>
<p><strong>처음 접속을 HTTP로 하는 경우 (SSL Stripping)</strong>
사용자가 <code>http://</code>로 접속하는 순간, 공격자가 HTTPS로의 전환을 가로막고 평문으로 중계할 수 있습니다. 서버는 HSTS 헤더를 통해 &quot;앞으로는 반드시 HTTPS로만 접속하라&quot;고 브라우저에 알려 이를 방지합니다.</p>
<hr>
<p>지금까지의 내용을 짧게 정리하면 다음과 같습니다.</p>
<ol>
<li>HTTP는 평문으로 통신하기 때문에 도청, 변조, 사칭에 취약합니다.</li>
<li>공개키로 세션 키를 암호화해 보내면 도청은 막을 수 있지만, 공개키의 진위를 확인하지 못하면 중간자 공격에 노출됩니다.</li>
<li>이를 위해 CA가 서버 공개키에 서명한 인증서로 공개키의 주인을 보증하고, 서버는 개인키 서명으로 자신이 그 주인임을 증명합니다.</li>
<li>RSA 키 교환은 개인키가 유출되면 과거 대화까지 해독될 수 있어, TLS 1.3에서는 세션 키를 전송하지 않는 ECDHE만 남게 되었습니다.</li>
<li>결국 HTTPS는 비대칭키로 신원 확인과 키 합의를 수행하고, 실제 데이터는 대칭키인 세션 키로 빠르게 암호화하는 구조입니다.</li>
</ol>
<p>처음에는 단순히 &quot;암호화해서 보낸다&quot; 정도로만 알고 있었는데, 막상 들여다보니 누구와 통신하는지 확인하는 과정이 훨씬 큰 비중을 차지하고 있었습니다. 같은 부분에서 헷갈리셨던 분들께 이 글이 조금이나마 도움이 되었으면 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[기본 키 전략부터 Spring Data JPA까지]]></title>
            <link>https://velog.io/@minho-git/%EA%B8%B0%EB%B3%B8-%ED%82%A4-%EC%A0%84%EB%9E%B5%EB%B6%80%ED%84%B0-Spring-Data-JPA%EA%B9%8C%EC%A7%80</link>
            <guid>https://velog.io/@minho-git/%EA%B8%B0%EB%B3%B8-%ED%82%A4-%EC%A0%84%EB%9E%B5%EB%B6%80%ED%84%B0-Spring-Data-JPA%EA%B9%8C%EC%A7%80</guid>
            <pubDate>Mon, 01 Jun 2026 16:21:09 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>JPA를 처음 배우면서 헷갈렸던 개념들을 정리했습니다.
기본 키 생성 전략, Flush, 준영속 상태, JPA Auditing, JpaRepository 동작 원리까지 다룹니다.</p>
</blockquote>
<hr>
<h2 id="목차">목차</h2>
<ol>
<li><a href="#1-id%EC%99%80-%EA%B8%B0%EB%B3%B8-%ED%82%A4-%EC%83%9D%EC%84%B1-%EC%A0%84%EB%9E%B5">@Id와 기본 키 생성 전략</a></li>
<li><a href="#2-flush-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8%EC%9D%98-%EB%8F%99%EA%B8%B0%ED%99%94">Flush: 영속성 컨텍스트의 동기화</a></li>
<li><a href="#3-%EC%A4%80%EC%98%81%EC%86%8D-%EC%83%81%ED%83%9C%EC%99%80-merge">준영속 상태와 merge()</a></li>
<li><a href="#4-jpa-auditing">JPA Auditing</a></li>
<li><a href="#5-spring-data-jpa---jparepository">Spring Data JPA - JpaRepository</a></li>
</ol>
<hr>
<h2 id="1-id와-기본-키-생성-전략">1. @Id와 기본 키 생성 전략</h2>
<p>엔티티를 만들 때마다 항상 기본 키 설정이 필요하다.
PK를 할당하는 방법은 크게 2가지다.</p>
<ol>
<li>직접 넣기</li>
<li>DB에게 맡기기: <code>@GeneratedValue</code></li>
</ol>
<p><code>@GeneratedValue</code>에는 4가지 옵션이 존재한다.</p>
<h3 id="identity">IDENTITY</h3>
<p>기본 키 생성을 데이터베이스에 위임하는 전략이다. (MySQL의 <code>AUTO_INCREMENT</code>)</p>
<p><strong>특징: 쓰기 지연이 동작하지 않는다.</strong></p>
<p>영속성 컨텍스트의 1차 캐시는 <code>key-value</code> 구조로 관리되는데, key를 만들기 위해 ID 값이 필요하다.
<code>AUTO_INCREMENT</code>는 DB에 <code>INSERT</code> 쿼리를 날려야 ID가 생성되므로, <code>em.persist()</code> 호출 즉시 <code>INSERT SQL</code>을 DB에 보낸다.</p>
<pre><code class="language-java">em.persist(member); // 즉시 INSERT 실행 → ID 반환 → 1차 캐시에 저장</code></pre>
<h3 id="sequence">SEQUENCE</h3>
<p>DB의 시퀀스 오브젝트를 사용하는 전략이다. (Oracle, PostgreSQL, H2)</p>
<p><strong>특징: 쓰기 지연이 가능하다.</strong></p>
<p><code>persist()</code> 시점에 시퀀스 값만 먼저 조회해서 ID를 확보한 뒤, 실제 <code>INSERT</code>는 커밋 시점에 실행된다.</p>
<pre><code class="language-java">@SequenceGenerator(
    name = &quot;MEMBER_SEQ_GENERATOR&quot;,
    sequenceName = &quot;MEMBER_SEQ&quot;,
    initialValue = 1,
    allocationSize = 50 // 성능 최적화 포인트
)
public class SequenceUser {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = &quot;MEMBER_SEQ_GENERATOR&quot;)
    private Long userId;
}</code></pre>
<p><strong>allocationSize로 성능 최적화</strong></p>
<p><code>allocationSize = 50</code>으로 설정하면, 시퀀스 조회 쿼리(<code>call next value</code>)가 50번 저장에 <strong>단 2번만</strong> 발생한다.</p>
<ul>
<li>1번째 호출: 결과 <code>1</code> → 현재 시퀀스 값, 첫 번째 ID로 사용</li>
<li>2번째 호출: 결과 <code>51</code> → 다음 범위의 시작값 (범위 끝 확인용)</li>
</ul>
<p>Hibernate는 이 두 값을 바탕으로 <code>1~50</code> 범위를 메모리에 캐시해서, 2~50 사이의 ID는 DB 호출 없이 바로 꺼내 쓴다.</p>
<blockquote>
<p>⚠️ JPA(<code>allocationSize = 50</code>)와 DB(<code>INCREMENT BY 1</code>)의 증가값이 다르면 PK 중복 에러가 발생할 수 있다. 반드시 통일시키자.</p>
</blockquote>
<h3 id="table">TABLE</h3>
<p>키 생성 전용 테이블을 만들어서 시퀀스를 흉내 내는 전략이다.
모든 DB에서 사용 가능하지만, <code>SELECT</code> + <code>UPDATE</code> + 락으로 인해 성능이 좋지 않다.
DB 마이그레이션 과도기나 여러 DB를 지원해야 하는 경우에 사용한다.</p>
<h3 id="auto">AUTO</h3>
<p>DB 방언에 따라 <code>IDENTITY</code>, <code>SEQUENCE</code>, <code>TABLE</code> 중 하나를 자동으로 선택한다.
운영 환경에서는 <strong>가급적 명시적인 전략을 지정하는 것이 좋다.</strong></p>
<h3 id="전략-비교">전략 비교</h3>
<table>
<thead>
<tr>
<th>전략</th>
<th>쓰기 지연</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td>직접 할당</td>
<td>가능</td>
<td>개발자가 ID 중복 직접 관리</td>
</tr>
<tr>
<td><code>IDENTITY</code></td>
<td>어려움</td>
<td><code>persist()</code> 즉시 INSERT, Batch 불리</td>
</tr>
<tr>
<td><code>SEQUENCE</code></td>
<td>가능</td>
<td>allocationSize로 성능 최적화 가능</td>
</tr>
<tr>
<td><code>TABLE</code></td>
<td>가능</td>
<td>모든 DB 호환, 성능 나쁨</td>
</tr>
<tr>
<td><code>AUTO</code></td>
<td>전략에 따라 다름</td>
<td>명시적 지정 권장</td>
</tr>
</tbody></table>
<h3 id="실무-표준-identity">실무 표준: IDENTITY</h3>
<p>MySQL이 스타트업·웹 서비스의 사실상 표준 DB이고, MySQL은 <code>SEQUENCE</code>를 지원하지 않는다.
또한 INSERT는 대부분 단건으로 일어나므로 <code>IDENTITY</code>와 <code>SEQUENCE</code>의 성능 차이가 거의 없다.
대용량 삽입이 필요한 경우에만 <code>JdbcTemplate</code>이나 <code>MyBatis</code>를 사용하면 된다.</p>
<hr>
<h2 id="2-flush-영속성-컨텍스트의-동기화">2. Flush: 영속성 컨텍스트의 동기화</h2>
<p>Flush는 영속성 컨텍스트의 변경 내용을 DB에 반영하는 작업이다.
쓰기 지연 저장소에 쌓아둔 SQL들을 DB로 한꺼번에 보내는 과정이다.</p>
<h3 id="flush가-발생하는-시점">Flush가 발생하는 시점</h3>
<p><strong>1. <code>em.flush()</code> 직접 호출 (수동)</strong>
실무에서는 거의 사용하지 않는다.</p>
<p><strong>2. 트랜잭션 커밋 시 자동 호출</strong>
<code>tx.commit()</code>을 호출하면 JPA가 자동으로 <code>flush()</code>를 먼저 실행한 뒤 커밋한다.</p>
<p><strong>3. JPQL 쿼리 실행 시 자동 호출 ⭐</strong>
이 메커니즘이 없으면 데이터 불일치 문제가 발생한다.</p>
<pre><code class="language-java">em.persist(memberA); // 1차 캐시에만 저장 (INSERT 대기 중)
em.persist(memberB);
em.persist(memberC);

// JPQL 실행 직전에 자동 Flush 발동!
// persist한 데이터가 DB에 반영되어야 조회 결과에 포함될 수 있다.
List&lt;Member&gt; result = em.createQuery(&quot;select m from Member m&quot;, Member.class)
        .getResultList();
// → 결과: 3명 조회됨</code></pre>
<h3 id="flush-후-1차-캐시는-비워질까">Flush 후 1차 캐시는 비워질까?</h3>
<p><strong>절대 지워지지 않는다.</strong></p>
<p>Flush 동작 순서는 다음과 같다.</p>
<ol>
<li><strong>Dirty Checking</strong>: 1차 캐시 객체와 스냅샷 비교 → 변경 사항 있으면 UPDATE SQL 생성</li>
<li><strong>SQL 전송</strong>: 쓰기 지연 SQL 저장소의 쿼리들을 DB에 전송</li>
<li><strong>DB 응답</strong>: DB가 SQL을 받아서 실행 (아직 커밋은 안 된 상태)</li>
<li><strong>1차 캐시</strong>: 아무 일도 일어나지 않는다. 객체는 그대로 남아있다.</li>
</ol>
<p>1차 캐시를 비우지 않는 이유는 JPA의 핵심 철학인 <strong>영속성과 동일성</strong> 때문이다.</p>
<ul>
<li><strong>재사용성</strong>: 방금 저장하고 바로 다시 조회하는 경우가 많으므로 캐시를 비우면 비효율적이다.</li>
<li><strong>동일성 보장</strong>: 한 트랜잭션 안에서는 <code>a == b</code>가 항상 참이어야 한다.</li>
</ul>
<h3 id="flush-vs-clear">flush() vs clear()</h3>
<table>
<thead>
<tr>
<th>메서드</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><code>flush()</code></td>
<td>쓰기 지연 저장소 → DB 동기화. 1차 캐시는 유지</td>
</tr>
<tr>
<td><code>clear()</code></td>
<td>영속성 컨텍스트(1차 캐시) 전체 비우기</td>
</tr>
</tbody></table>
<h3 id="flushmode-설정">FlushMode 설정</h3>
<ul>
<li><strong>FlushModeType.AUTO (기본값, 권장)</strong>: 커밋 직전 + JPQL 실행 직전에 자동 flush</li>
<li><strong>FlushModeType.COMMIT</strong>: 오직 커밋할 때만 flush → 쿼리가 꼬일 수 있으므로 주의</li>
</ul>
<p>특수한 상황이 아니라면 <strong>AUTO를 사용하자.</strong></p>
<hr>
<h2 id="3-준영속-상태와-merge">3. 준영속 상태와 merge()</h2>
<h3 id="엔티티-생명주기-4가지-상태">엔티티 생명주기: 4가지 상태</h3>
<p><strong>1. 비영속</strong>
<code>new</code>로 객체를 생성한 상태. 영속성 컨텍스트와 전혀 관계가 없다.</p>
<p><strong>2. 영속</strong>
영속성 컨텍스트에 저장되었거나, DB에서 조회된 상태다.</p>
<pre><code class="language-java">em.persist(member); // 저장
em.find(Member.class, 1L); // 조회</code></pre>
<p>1차 캐시에 들어가고, Dirty Checking의 대상이 된다. 반드시 ID 값을 가지고 있다.</p>
<p><strong>3. 준영속 ⚠️</strong>
한때는 영속 상태였지만, 영속성 컨텍스트에서 분리된 상태다.
데이터는 있는데 수정이 안 되는 <strong>좀비 같은 상태</strong>로, 가장 골치 아픈 상태다.
ID 값은 있지만 JPA가 관리하지 않으므로 값을 바꿔도 DB에 반영되지 않는다.</p>
<pre><code class="language-java">em.detach(member); // 준영속으로 전환</code></pre>
<p><strong>4. 삭제</strong>
삭제 마킹을 해둔 상태. <code>flush()</code> 시점에 DELETE 쿼리가 실행된다.</p>
<pre><code class="language-java">em.remove(member);</code></pre>
<blockquote>
<p><code>em.contains(member)</code>로 영속 상태인지 확인할 수 있다.</p>
</blockquote>
<h3 id="merge-복제-기술-덮어쓰기">merge(): 복제 기술, 덮어쓰기</h3>
<p><code>merge</code>는 죽은 객체를 되살리는 게 아니라, <strong>모든 필드를 덮어쓰는 복제 기술</strong>이다.</p>
<p><strong>동작 원리</strong></p>
<ol>
<li>JPA가 준영속 객체를 받는다.</li>
<li>객체의 ID로 1차 캐시 확인 → 없으면 DB 조회 (<code>SELECT</code>)</li>
<li>조회한 영속 객체에 준영속 객체의 값을 <strong>덮어씌운다.</strong></li>
<li>수정된 영속 객체를 반환한다.</li>
<li>처음 준영속 객체는 여전히 준영속 상태로 버려진다.</li>
</ol>
<pre><code>merge(member) 호출
  → SELECT 실행
  → 1차 캐시에 저장  {&quot;name&quot;: &quot;OriginalName&quot;, &quot;grade&quot;: &quot;VIP&quot;}
  → 스냅샷에 저장    {&quot;name&quot;: &quot;OriginalName&quot;, &quot;grade&quot;: &quot;VIP&quot;}  ← 기준점
  → 1차 캐시를 준영속 객체 값으로 덮어씌움
     1차 캐시: {&quot;name&quot;: &quot;UpdatedName&quot;, &quot;grade&quot;: &quot;VIP&quot;}
     스냅샷:   {&quot;name&quot;: &quot;OriginalName&quot;, &quot;grade&quot;: &quot;VIP&quot;}  ← 그대로

커밋 시점
  → Dirty Checking → 다름!
  → UPDATE 쿼리 생성 &amp; DB 전송</code></pre><p><strong>⚠️ merge의 치명적 함정: null 덮어쓰기</strong></p>
<p><code>merge</code>는 모든 필드를 덮어쓰기 때문에, 비워진 값이 있다면 null이 DB에 그대로 반영된다.</p>
<pre><code>이름만 바꾸려고 객체 생성
  detachedMember.setId(id);
  detachedMember.setName(&quot;UserB&quot;);
  // grade는 null 상태!

→ merge() 수행
→ 1차 캐시: {&quot;name&quot;: &quot;UserB&quot;, &quot;grade&quot;: null}
→ UPDATE name = &#39;UserB&#39;, grade = null
→ ☠️ VIP 등급이 증발!</code></pre><p><strong>결론</strong>: 특수한 상황이 아니라면 <code>merge</code>는 사용하지 말고, <strong>Dirty Checking을 활용</strong>하자.
영속 상태의 객체를 직접 수정하면 변경된 필드만 선택적으로 UPDATE되므로 항상 안전하다.</p>
<hr>
<h2 id="4-jpa-auditing">4. JPA Auditing</h2>
<p>엔티티가 생성/수정될 때 시간을 자동으로 기록해주는 기능이다.
매번 <code>createdAt = LocalDateTime.now()</code>를 직접 쓰지 않아도 자동으로 처리된다.</p>
<h3 id="createddate--lastmodifieddate">@CreatedDate / @LastModifiedDate</h3>
<table>
<thead>
<tr>
<th>어노테이션</th>
<th>동작 시점</th>
<th>이후 변경 여부</th>
</tr>
</thead>
<tbody><tr>
<td><code>@CreatedDate</code></td>
<td>최초 INSERT 시</td>
<td>변경 안 됨</td>
</tr>
<tr>
<td><code>@LastModifiedDate</code></td>
<td>INSERT + UPDATE 시</td>
<td>수정될 때마다 갱신</td>
</tr>
</tbody></table>
<h3 id="동작-원리">동작 원리</h3>
<p>JPA는 엔티티 생명주기마다 이벤트 훅을 제공한다.</p>
<table>
<thead>
<tr>
<th>훅</th>
<th>실행 시점</th>
</tr>
</thead>
<tbody><tr>
<td><code>@PrePersist</code></td>
<td><code>persist()</code> 직전</td>
</tr>
<tr>
<td><code>@PostPersist</code></td>
<td><code>persist()</code> 직후</td>
</tr>
<tr>
<td><code>@PreUpdate</code></td>
<td>update 직전</td>
</tr>
<tr>
<td><code>@PostUpdate</code></td>
<td>update 직후</td>
</tr>
<tr>
<td><code>@PreRemove</code></td>
<td><code>remove()</code> 직전</td>
</tr>
<tr>
<td><code>@PostRemove</code></td>
<td><code>remove()</code> 직후</td>
</tr>
</tbody></table>
<p><code>AuditingEntityListener</code>는 Spring Data JPA가 이미 <code>@PrePersist</code>와 <code>@PreUpdate</code> 훅을 구현해둔 클래스다.
우리는 <code>@EntityListeners(AuditingEntityListener.class)</code>로 등록만 하면 된다.</p>
<pre><code>앱 시작
  → JPA가 엔티티 클래스 스캔
  → @EntityListeners 확인 → AuditingEntityListener 등록
  → @PrePersist, @PreUpdate 붙은 메서드 기억해둠

em.persist(member) 호출
  → @PrePersist 시점에 touchForCreate() 실행
  → @CreatedDate 필드에 현재 시간 자동 주입
  → INSERT 쿼리에 시간 포함되어 DB 저장</code></pre><h3 id="설정-방법">설정 방법</h3>
<p>필수 조건 2가지가 모두 있어야 한다. 하나라도 없으면 시간이 <strong>null로 저장</strong>된다.</p>
<pre><code class="language-java">// ① 엔티티에 리스너 등록
@EntityListeners(AuditingEntityListener.class)

// ② 스프링에 Auditing 활성화
@EnableJpaAuditing
@SpringBootApplication
public class Application { ... }</code></pre>
<h3 id="실무-패턴-baseentity-상속">실무 패턴: BaseEntity 상속</h3>
<pre><code class="language-java">@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass // DB 테이블은 안 만들고 필드만 자식 엔티티에 포함
public abstract class BaseEntity {
    @CreatedDate
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;
}

@Entity
public class Member extends BaseEntity { // 상속만 하면 자동 적용
    ...
}</code></pre>
<h3 id="persistable-인터페이스">Persistable 인터페이스</h3>
<p><code>@GeneratedValue</code> 없이 ID를 직접 할당하면 문제가 생긴다.</p>
<pre><code>ID == null → 새 엔티티 → INSERT
ID != null → 기존 엔티티로 오판 → SELECT 후 merge (불필요한 SELECT 발생!)</code></pre><p><code>Persistable</code> 인터페이스를 구현해서 새 엔티티 판단 기준을 직접 정의한다.</p>
<pre><code class="language-java">@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public abstract class BaseEntity implements Persistable&lt;String&gt; {

    @CreatedDate
    private LocalDateTime createdAt;

    @Override
    public boolean isNew() {
        return createdAt == null; // 저장 전엔 null → 새 엔티티로 판단
    }
}</code></pre>
<p><code>@CreatedDate</code>는 <code>persist()</code> 시점에 자동 세팅되므로, 저장 전에는 항상 <code>null</code>이다.
덕분에 <code>isNew() = true</code> → <code>SELECT</code> 없이 바로 <code>INSERT</code> 실행된다.</p>
<hr>
<h2 id="5-spring-data-jpa---jparepository">5. Spring Data JPA - JpaRepository</h2>
<h3 id="핵심-의문">핵심 의문</h3>
<blockquote>
<p>인터페이스에 <code>JpaRepository</code>만 상속받았을 뿐인데 어떻게 동작하는가?</p>
</blockquote>
<h3 id="기존-방식-vs-spring-data-jpa">기존 방식 vs Spring Data JPA</h3>
<pre><code class="language-java">// 기존: EntityManager 직접 사용
@Repository
public class MemberRepository {
    @PersistenceContext
    private EntityManager em;
    ...
}

// Spring Data JPA: 인터페이스 선언만으로 끝
public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {}</code></pre>
<h3 id="작동-원리">작동 원리</h3>
<p>인터페이스(껍데기)만 만들었는데 누가 구현해줄까? → <strong>Spring Framework</strong></p>
<ol>
<li><code>JpaRepository</code>를 상속한 인터페이스를 작성한다.</li>
<li>Spring이 애플리케이션 시작 시 스캔해서 해당 인터페이스를 찾는다.</li>
<li>런타임에 구현 객체(<code>SimpleJpaRepository</code>)를 자동 생성한다.</li>
<li>생성된 프록시 객체를 <strong>스프링 빈으로 등록</strong>한다.</li>
</ol>
<h3 id="simplejparepository">SimpleJpaRepository</h3>
<p>Spring이 자동으로 만들어주는 구현체 내부를 보면:</p>
<pre><code class="language-java">@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository&lt;T, ID&gt; implements JpaRepositoryImplementation&lt;T, ID&gt; {
    private final EntityManager entityManager; // 내부적으로 EntityManager를 직접 사용
}</code></pre>
<p><code>JpaRepository</code>는 <code>EntityManager</code>를 <strong>편리하게 감싼 껍데기</strong>일 뿐이다.
내부적으로 1차 캐시, 변경 감지 등 JPA의 동작 방식은 그대로 유지된다.
→ 이것이 우리가 <code>EntityManager</code>를 먼저 배운 이유다.</p>
<h3 id="save-메서드의-한계">save() 메서드의 한계</h3>
<pre><code class="language-java">public &lt;S extends T&gt; S save(S entity) {
    if (entityInformation.isNew(entity)) {
        em.persist(entity);  // 새로우면 persist (INSERT)
        return entity;
    } else {
        return em.merge(entity);  // 아니면 merge (SELECT 후 UPDATE 또는 INSERT)
    }
}</code></pre>
<p><code>isNew()</code>는 ID가 <code>null</code>이면 <code>true</code>, 아니면 <code>false</code>를 반환한다.
ID를 직접 할당하면 ID가 null이 아니므로 <code>merge()</code>가 실행되어 <strong>불필요한 SELECT</strong>가 발생한다.</p>
<p>이 문제는 앞서 설명한 <code>Persistable</code> 인터페이스 구현으로 해결한다.</p>
<hr>
<h2 id="마치며">마치며</h2>
<p>JPA를 처음 배울 때 가장 헷갈렸던 부분들을 정리해봤습니다.</p>
<p>핵심을 한 줄씩 요약하면:</p>
<ul>
<li><strong>기본 키 전략</strong>: 실무는 IDENTITY, 성능이 필요하면 SEQUENCE + allocationSize</li>
<li><strong>Flush</strong>: DB 동기화일 뿐, 1차 캐시는 절대 비워지지 않는다</li>
<li><strong>준영속</strong>: 좀비 상태, merge 대신 Dirty Checking을 쓰자</li>
<li><strong>JPA Auditing</strong>: 훅을 직접 안 써도 되는 이유는 Spring이 이미 구현해뒀기 때문</li>
<li><strong>JpaRepository</strong>: EntityManager의 편리한 래퍼, 내부 동작은 동일하다</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[JPA Auditing]]></title>
            <link>https://velog.io/@minho-git/JPA-Auditing</link>
            <guid>https://velog.io/@minho-git/JPA-Auditing</guid>
            <pubDate>Sun, 24 May 2026 07:00:33 GMT</pubDate>
            <description><![CDATA[<p>엔티티가 생성/수정될 때 시간을 자동으로 기록해주는 JPA Auditing 기능을 이해하자.</p>
<p>매번 <code>createdAt = LocalDateTime.now()</code>를 직접 쓰지 않아도 자동으로 처리되는 원리를 익히자.</p>
<hr>
<h2 id="createddate--lastmodifieddate">@CreatedDate / @LastModifiedDate</h2>
<table>
<thead>
<tr>
<th>어노테이션</th>
<th>동작 시점</th>
<th>이후 변경 여부</th>
</tr>
</thead>
<tbody><tr>
<td><code>@CreatedDate</code></td>
<td>최초 INSERT 시</td>
<td>변경 안 됨</td>
</tr>
<tr>
<td><code>@LastModifiedDate</code></td>
<td>INSERT + UPDATE 시</td>
<td>수정될 때마다 갱신</td>
</tr>
</tbody></table>
<hr>
<h2 id="동작-원리">동작 원리</h2>
<h3 id="jpa-엔티티-생명주기-훅">JPA 엔티티 생명주기 훅</h3>
<p>JPA는 엔티티 생명주기마다 전/후로 실행할 수 있는 이벤트 훅을 제공한다.</p>
<table>
<thead>
<tr>
<th>훅</th>
<th>실행 시점</th>
</tr>
</thead>
<tbody><tr>
<td><code>@PrePersist</code></td>
<td><code>persist()</code> 직전</td>
</tr>
<tr>
<td><code>@PostPersist</code></td>
<td><code>persist()</code> 직후</td>
</tr>
<tr>
<td><code>@PreUpdate</code></td>
<td>update 직전</td>
</tr>
<tr>
<td><code>@PostUpdate</code></td>
<td>update 직후</td>
</tr>
<tr>
<td><code>@PreRemove</code></td>
<td><code>remove()</code> 직전</td>
</tr>
<tr>
<td><code>@PostRemove</code></td>
<td><code>remove()</code> 직후</td>
</tr>
</tbody></table>
<p>앱 시작 시 JPA가 리스너를 스캔하면서 <strong>어떤 훅 메서드가 있는지 전부 기억해뒀다가</strong>, 해당 시점이 되면 자동으로 실행한다.</p>
<h3 id="auditingentitylistener">AuditingEntityListener</h3>
<p>우리가 훅을 직접 구현하지 않아도 되는 이유는, <strong>Spring Data JPA가 <code>AuditingEntityListener</code> 안에 이미 구현해뒀기 때문</strong>이다.</p>
<pre><code class="language-java">// AuditingEntityListener 내부 (Spring Data JPA가 만들어둔 것)
public class AuditingEntityListener {

    @PrePersist
    public void touchForCreate(Object target) {
        // @CreatedDate 필드를 찾아서 현재 시간 주입
    }

    @PreUpdate
    public void touchForUpdate(Object target) {
        // @LastModifiedDate 필드를 찾아서 현재 시간 주입
    }
}</code></pre>
<p><code>AuditingEntityListener</code>는 6개의 훅 중 <code>@PrePersist</code>와 <code>@PreUpdate</code>만 사용한다.</p>
<p>우리는 <code>@EntityListeners(AuditingEntityListener.class)</code>로 <strong>&quot;이 리스너 써줘&quot;</strong> 라고 등록만 하면 된다.</p>
<h3 id="전체-동작-흐름">전체 동작 흐름</h3>
<pre><code>앱 시작
  → JPA가 엔티티 클래스 스캔
  → @EntityListeners 확인 → AuditingEntityListener 등록
  → 리스너 안의 @PrePersist, @PreUpdate 붙은 메서드도 기억해둠
  → 이후 해당 시점마다 자동 실행 (매번 확인 X)

em.persist(member) 호출
  → JPA: &quot;PrePersist 시점이다!&quot;
  → 기억해둔 touchForCreate() 실행
  → @CreatedDate 필드에 현재 시간 자동 주입
  → INSERT 쿼리에 시간 포함되어 DB 저장</code></pre><hr>
<h2 id="설정-방법">설정 방법</h2>
<h3 id="필수-조건-2가지">필수 조건 2가지</h3>
<p><strong>① 엔티티에 리스너 등록</strong></p>
<pre><code class="language-java">@EntityListeners(AuditingEntityListener.class)</code></pre>
<p><strong>② 스프링에 Auditing 활성화</strong></p>
<pre><code class="language-java">@EnableJpaAuditing
@SpringBootApplication
public class Application { ... }</code></pre>
<p>둘 중 하나라도 없으면 시간이 <strong>null로 저장</strong>된다.</p>
<ul>
<li><code>@EntityListeners</code> 없으면 → 리스너 자체가 연결 안 됨</li>
<li><code>@EnableJpaAuditing</code> 없으면 → 리스너가 Bean으로 등록 안 돼서 동작 안 함</li>
</ul>
<hr>
<h2 id="실무-패턴-baseentity-상속">실무 패턴: BaseEntity 상속</h2>
<p>엔티티마다 <code>@EntityListeners</code>를 붙이는 건 번거롭기 때문에, <strong>BaseEntity를 하나 만들어서 상속</strong>하는 방식을 사용한다.</p>
<pre><code class="language-java">@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass // DB 테이블은 안 만들고 필드만 자식 엔티티에 포함
public abstract class BaseEntity {

    @CreatedDate
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;
}</code></pre>
<pre><code class="language-java">@Entity
public class Member extends BaseEntity { // 상속만 하면 자동 적용
    ...
}

@Entity
public class Order extends BaseEntity { // 얘도 자동 적용
    ...
}</code></pre>
<p><code>@MappedSuperclass</code>는 DB 테이블을 별도로 만들지 않고, 필드만 자식 엔티티 테이블에 포함시켜 주는 어노테이션이다.</p>
<hr>
<h2 id="persistable-인터페이스">Persistable 인터페이스</h2>
<h3 id="왜-필요한가">왜 필요한가?</h3>
<p><code>@GeneratedValue</code>를 사용하면 JPA가 ID를 자동 생성하기 때문에 새 엔티티인지 판단이 쉽다.</p>
<p>하지만 <strong>ID를 직접 할당</strong>하는 경우 문제가 생긴다.</p>
<pre><code class="language-java">// ID를 직접 세팅하는 경우
Member member = new Member();
member.setId(&quot;user-001&quot;); // ID 직접 할당
em.persist(member);</code></pre>
<p>JPA는 <code>save()</code> 호출 시 새 엔티티인지 판단하기 위해 <strong>ID가 null인지 확인</strong>한다.</p>
<pre><code>ID == null → 새 엔티티 → INSERT
ID != null → 기존 엔티티 → SELECT 후 merge (불필요한 SELECT 발생!)</code></pre><p>ID를 직접 할당하면 ID가 null이 아니므로, JPA가 <strong>기존 엔티티로 오판해서 불필요한 SELECT 쿼리</strong>를 날린다.</p>
<h3 id="해결-방법-persistable-구현">해결 방법: Persistable 구현</h3>
<p><code>Persistable</code> 인터페이스를 구현해서 새 엔티티 판단 기준을 직접 정의한다.</p>
<pre><code class="language-java">@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public abstract class BaseEntity implements Persistable&lt;String&gt; {

    @CreatedDate
    private LocalDateTime createdAt;

    @Override
    public boolean isNew() {
        // createdAt이 null이면 새 엔티티 (아직 한 번도 저장 안 됨)
        return createdAt == null;
    }
}</code></pre>
<h3 id="동작-흐름">동작 흐름</h3>
<pre><code>save(member) 호출
  → isNew() 실행
  → createdAt == null ? → true (아직 저장 안 된 새 엔티티)
  → SELECT 없이 바로 INSERT 실행
  → @PrePersist → createdAt에 현재 시간 주입
  → 이후 save() 호출 시 createdAt != null → 기존 엔티티로 판단</code></pre><h3 id="결론">결론</h3>
<p>ID 직접 할당 전략을 쓴다면 <code>Persistable</code>을 구현해서 <strong>불필요한 SELECT를 방지</strong>하자.</p>
<p><code>@CreatedDate</code>와 함께 사용하면 <code>isNew()</code> 판단 기준으로 딱 맞아 떨어진다.</p>
<hr>
<h2 id="핵심-정리">핵심 정리</h2>
<ul>
<li>JPA 훅(<code>@PrePersist</code> 등)은 앱 시작 시 한 번 스캔해서 기억해뒀다가 해당 시점에 자동 실행된다.</li>
<li><code>AuditingEntityListener</code>는 6개 훅 중 <code>@PrePersist</code>, <code>@PreUpdate</code>만 사용하며, Spring이 이미 구현해뒀다. 우리는 등록만 하면 된다.</li>
<li>ID 자동 생성(<code>@GeneratedValue</code>) → 기본 동작으로 충분하다.</li>
<li>ID 직접 할당 → <code>Persistable</code> 구현해서 불필요한 SELECT를 막자.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Spring] 전역 예외 처리와 내부 동작 원리]]></title>
            <link>https://velog.io/@minho-git/Spring-%EC%A0%84%EC%97%AD-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC%EC%99%80-%EB%82%B4%EB%B6%80-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC</link>
            <guid>https://velog.io/@minho-git/Spring-%EC%A0%84%EC%97%AD-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC%EC%99%80-%EB%82%B4%EB%B6%80-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC</guid>
            <pubDate>Sun, 03 May 2026 08:05:25 GMT</pubDate>
            <description><![CDATA[<h2 id="exception-vs-runtimeexception">Exception vs RuntimeException</h2>
<p>자바의 예외는 크게 <code>Exception</code>과 <code>RuntimeException</code> 두 가지로 나뉜다.</p>
<ul>
<li><strong>Exception:</strong> 컴파일러에 의해 엄격하게 감시된다. 따라서 <code>try-catch</code> 문을 사용하거나 <code>throws</code>로 처리해야 한다.</li>
<li><strong>RuntimeException:</strong> 컴파일러가 확인하지 않으며 프로그램 실행 중에 발생한다. </li>
</ul>
<p>스프링 트랜잭션에서 기본적으로 <code>RuntimeException</code>만 롤백된다. <code>Exception</code>은 컴파일러가 체크하는 예외이므로 롤백시키지 않는다. </p>
<p>프로젝트에서는 기본적으로 런타임 예외를 사용하는 것이 적절하다. 체크 예외를 사용할 경우 잦은 <code>try-catch</code>와 <code>throws</code>로 인해 비즈니스 로직에 집중하기 어렵다. 또한 SQL 에러나 네트워크 문제 등은 어차피 복구할 수 없는 경우가 많으며, 런타임 예외는 추가 옵션 없이도 롤백이 가능하여 간편하다.</p>
<p>단, 특정 로직 실패 시 대안 로직(예: 송금 실패 시 재시도)이 실행되어야 하는 경우에는 <code>Exception</code>을 사용할 수 있다. 이때는 트랜잭션에 <code>@Transactional(rollbackFor = Exception.class)</code> 옵션을 명시해야 한다.</p>
<h2 id="커스텀-예외-custom-exception-구현">커스텀 예외 (Custom Exception) 구현</h2>
<p>문제 상황 발생 시 <code>RuntimeException</code>을 발생시켜 롤백을 수행하면 클라이언트는 500 Internal Server Error를 응답받는다. 항상 500 에러를 반환하는 것은 클라이언트 입장에서 정확한 문제 파악을 불가능하게 만든다.</p>
<p>따라서 <code>RuntimeException</code>을 상속받아 상황에 맞는 커스텀 예외를 만들고, 상태 코드와 상세 메시지를 반환하도록 별도의 예외 클래스를 구현해야 한다.</p>
<pre><code class="language-java">public class EmailDuplicateException extends RuntimeException {
    public EmailDuplicateException(String message) {
        super(message);
    }
}

public class UserNotFoundException extends RuntimeException {
    public UserNotFoundException(String message) {
        super(message);
    }
}</code></pre>
<h2 id="restcontrolleradvice와-표준-응답-양식">@RestControllerAdvice와 표준 응답 양식</h2>
<p>발생한 예외는 <code>@RestControllerAdvice</code>를 통해 전역적으로 처리한다. 이 어노테이션이 붙은 클래스는 모든 RestController를 감시하다가 예외가 발생하면 이를 가로채서 처리한다. 내부에는 <code>@ExceptionHandler(예외명.class)</code>를 통해 각 커스텀 예외별 대응 매뉴얼을 작성한다.</p>
<p>프론트엔드 개발자와 원활하게 협업하기 위해 <code>ErrorResponse</code>라는 표준화된 양식을 객체로 만들어 사용한다. 내부 필드를 JSON으로 변환(직렬화)하기 위해 Getter는 필수적으로 선언해야 한다. </p>
<p><code>ResponseEntity</code>를 활용하면 HTTP 상태 코드(status code), 헤더(header), 바디(body)를 세밀하게 제어하여 응답할 수 있다.</p>
<pre><code class="language-java">public class ErrorResponse {

    private String errorCode;
    private String message;

    public ErrorResponse(String errorCode, String message) {
        this.errorCode = errorCode;
        this.message = message;
    }

    // Getter (JSON 직렬화를 위해 필수)
    public String getErrorCode() { 
        return errorCode; 
    }

    public String getMessage() { 
        return message; 
    }
}</code></pre>
<pre><code class="language-java">@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity&lt;ErrorResponse&gt; handleUserNotFoundException(UserNotFoundException ex) {
        ErrorResponse response = new ErrorResponse(&quot;USER_NOT_FOUND&quot;, ex.getMessage());
        return new ResponseEntity&lt;&gt;(response, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(EmailDuplicateException.class)
    public ResponseEntity&lt;ErrorResponse&gt; emailDuplicateException(EmailDuplicateException ex) {
        ErrorResponse response = new ErrorResponse(&quot;EMAIL_DUPLICATE&quot;, ex.getMessage());
        return new ResponseEntity&lt;&gt;(response, HttpStatus.BAD_REQUEST);
    }
}</code></pre>
<h2 id="spring-전역-예외-처리-동작-흐름">Spring 전역 예외 처리 동작 흐름</h2>
<p>단순한 어노테이션 사용을 넘어, Spring 내부에서 전역 예외 처리가 어떻게 동작하는지 추적해보자.</p>
<p><strong>1. 예외 발생 및 전파 (Service 계층)</strong></p>
<ul>
<li>비즈니스 로직에서 런타임 예외(<code>RuntimeException</code>) 발생.</li>
<li>명시적인 예외 처리가 없으므로 콜 스택 언와인딩(Call Stack Unwinding)이 발생하여 상위 호출자를 거쳐 프레임워크 영역(<code>DispatcherServlet</code>)으로 예외가 던져진다.</li>
</ul>
<p><strong>2. 예외 포착 및 복구 위임 (DispatcherServlet)</strong></p>
<ul>
<li><code>DispatcherServlet.doDispatch()</code> 내부의 <code>catch</code> 블록에서 예외 포착.</li>
<li>예외 처리를 위해 <code>processDispatchResult()</code>를 거쳐 <code>processHandlerException()</code>을 호출한다.</li>
</ul>
<p><strong>3. 담당 resolver 탐색 및 템플릿 메서드 패턴 실행</strong></p>
<ul>
<li><code>handlerExceptionResolvers</code> 목록을 순회하여 <code>@RestControllerAdvice</code> 처리를 담당하는 <code>ExceptionHandlerExceptionResolver</code>를 선택한다.</li>
<li><strong>1계층 (공통 처리):</strong> <code>AbstractHandlerExceptionResolver.resolveException()</code> 실행. 공통 사전 작업을 수행한 뒤 하위 추상 메서드를 호출한다.</li>
<li><strong>2계층 (타입 변환):</strong> <code>AbstractHandlerMethodExceptionResolver.doResolveException()</code> 실행. 파라미터로 넘어온 범용 <code>Object</code>를 에러가 발생한 컨트롤러 정보인 <code>HandlerMethod</code>로 안전하게 다운캐스팅 후 하위 추상 메서드를 호출한다.</li>
<li><strong>3계층 (실질 로직):</strong> <code>ExceptionHandlerExceptionResolver.doResolveHandlerMethodException()</code> 실행.</li>
</ul>
<p><strong>4. 타겟 메서드 동적 탐색 및 리플렉션 가동</strong></p>
<ul>
<li>파라미터로 전달된 예외 객체의 클래스 타입(예: <code>UserNotFoundException.class</code>)을 Key로 사용하여, <strong>서버 구동 시점에 캐싱</strong>해둔 예외-메서드 맵(Map)을 조회한다.</li>
<li>예외를 처리할 정확한 <code>@ExceptionHandler</code> 타겟 메서드를 도출한다.</li>
<li><code>ServletInvocableHandlerMethod</code>가 자바 리플렉션 API(<code>invoke()</code>)를 사용하여 타겟 메서드에 예외 객체를 주입하고 강제로 실행한다.</li>
<li>실행 결과로 개발자가 정의한 표준 예외 응답 객체(<code>ErrorResponse</code>)가 반환된다.</li>
</ul>
<p><strong>5. JSON 직렬화 및 통신 종료 (HttpMessageConverter)</strong></p>
<ul>
<li><code>@RestControllerAdvice</code>에 포함된 <code>@ResponseBody</code> 설정에 의해 <code>RequestResponseBodyMethodProcessor</code>가 개입한다.</li>
<li><code>MappingJackson2HttpMessageConverter</code>가 작동하여 내부 Jackson 라이브러리를 호출한다.</li>
<li>Jackson이 반환된 <code>ErrorResponse</code> 객체의 public Getter를 호출하여 데이터를 읽어낸 뒤 JSON 텍스트로 직렬화한다.</li>
<li>완성된 JSON 데이터를 HTTP 응답 스트림(Body)에 작성하고 클라이언트와의 통신을 종료한다.</li>
</ul>
<h2 id="ocp-관점에서의-설계-분석">OCP 관점에서의 설계 분석</h2>
<p>이러한 전역 예외 처리 아키텍처는 객체지향의 핵심 원칙인 OCP를 완벽하게 준수한다. 설계의 핵심은 <code>DispatcherServlet</code>이 구체적인 구현체에 의존하지 않고, 다형성을 활용하여 리졸버를 처리한다는 것이다.</p>
<ul>
<li><strong>수정에는 닫혀 있다:</strong> <code>DispatcherServlet</code> 내부의 예외 처리 로직은 단순히 <code>HandlerExceptionResolver</code> 인터페이스들의 리스트를 순회하며 처리 가능한 리졸버를 찾는 구조로 되어 있다. 새로운 예외 처리 요구사항이 생겨 응답 방식이 변경되더라도, 메인 파이프라인인 핵심 코드는 단 한 줄도 수정할 필요가 없다.</li>
<li><strong>확장에는 열려 있다:</strong> 에러 발생 시 Slack 등 외부 채널로 알림을 전송하는 완전히 새로운 예외 처리 방식이 필요해진다면, 기존 코드를 수정할 필요 없이 기능 확장이 가능하다. <code>HandlerExceptionResolver</code> 인터페이스를 구현한 새로운 커스텀 리졸버 클래스를 생성하여 스프링 빈(Bean)으로 등록하기만 하면 된다.</li>
</ul>
<pre><code class="language-java">// Spring 프레임워크 내부 코드 구조
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
    // 인터페이스에만 의존하여 다형성 활용
    ModelAndView exMv = resolver.resolveException(request, response, handler, ex);

    // 처리할 수 있는 리졸버를 찾으면 즉시 반환
    if (exMv != null) {
        return exMv;
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Backend] 세션 vs JWT, 그리고 실무형 토큰 ]]></title>
            <link>https://velog.io/@minho-git/Backend-%EC%84%B8%EC%85%98-vs-JWT-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%8B%A4%EB%AC%B4%ED%98%95-%ED%86%A0%ED%81%B0</link>
            <guid>https://velog.io/@minho-git/Backend-%EC%84%B8%EC%85%98-vs-JWT-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%8B%A4%EB%AC%B4%ED%98%95-%ED%86%A0%ED%81%B0</guid>
            <pubDate>Wed, 25 Mar 2026 16:11:00 GMT</pubDate>
            <description><![CDATA[<p>이전 포스팅에서 안전한 로그인 흐름을 구현해 보았다. 그렇다면 성공적으로 로그인한 사용자의 &#39;상태&#39;는 어떻게 유지할 수 있을까? 웹 생태계에서 이를 해결하는 대표적인 두 가지 정답은 <strong>세션(Session) 기반 인증</strong>과 <strong>토큰(Token, JWT) 기반 인증</strong>이다.</p>
<h2 id="1-세션session-기반-인증의-한계-stateful">1. 세션(Session) 기반 인증의 한계 (Stateful)</h2>
<p>세션 기반 인증은 사용자가 로그인에 성공하면 서버가 &#39;세션 ID&#39;를 생성하여 중앙 세션 저장소(메모리 또는 DB)에 기록하고, 이를 클라이언트에게 전달하는 방식이다. 이후 클라이언트는 요청마다 세션 ID를 보내고, 서버는 저장소를 뒤져 유저를 확인한다.</p>
<p>이 방식은 서버가 로그인 상태를 직접 기억하고 통제하는 <strong>상태 유지(Stateful)</strong> 방식이다. 보안 통제(강제 로그아웃 등)가 쉽다는 장점이 있지만, 치명적인 단점이 있다. 사용자가 늘어나 트래픽이 몰릴 때 서버를 여러 대로 확장(Scale-out)하게 되면, 모든 서버가 세션 저장소를 공유하고 매번 조회해야 하므로 성능 병목이 발생하기 쉽다.</p>
<h2 id="2-jwt-기반-인증의-등장-stateless">2. JWT 기반 인증의 등장 (Stateless)</h2>
<p>그렇다면 토큰 기반 인증은 어떨까? 흔히 오해하지만, JWT(JSON Web Token)는 그 자체로 인증 방식이라기보다는 정보를 안전하게 &#39;전달&#39;하는 웹 표준 규격이다. </p>
<p><strong>동작 흐름</strong></p>
<ol>
<li>클라이언트가 이메일 + 비밀번호로 로그인 요청을 보낸다.</li>
<li>서버는 DB에서 사용자를 조회하고 해시된 비밀번호를 검증한다.</li>
<li>검증이 완료되면, 서버는 유저 데이터와 서버만의 &#39;비밀키(Secret)&#39;를 조합해 서명된 <strong>JWT</strong>를 발급하여 돌려준다.</li>
<li>이후 클라이언트는 API 요청 시 헤더(또는 쿠키)에 토큰을 담아 보낸다.</li>
<li>서버는 토큰 안의 페이로드와 자신의 비밀키를 이용해 &#39;서명&#39;을 다시 만들어보고, 클라이언트가 보낸 서명과 일치하는지 <strong>검증만</strong> 수행한다.</li>
</ol>
<p>이 방식은 서버가 로그인 상태를 메모리에 따로 기억할 필요 없이, 들어온 토큰의 위조 여부만 수학적으로 검사하면 되는 <strong>무상태(Stateless)</strong> 방식이다. 따라서 서버 확장이 매우 자유롭고, 로드밸런싱이나 마이크로서비스 아키텍처(MSA)에 완벽하게 부합한다.</p>
<h2 id="3-jwt의-구조와-주의할-점">3. JWT의 구조와 주의할 점</h2>
<p>JWT는 <code>.</code>을 기준으로 세 구획으로 나뉜다.</p>
<ul>
<li><strong>Header (헤더):</strong> 서명에 사용된 암호화 알고리즘과 토큰의 타입 정보</li>
<li><strong>Payload (페이로드):</strong> 사용자 식별 정보(ID)와 토큰 만료 시간 등의 실제 데이터</li>
<li><strong>Signature (서명):</strong> 헤더와 페이로드를 서버의 비밀키로 해싱한 암호화 값</li>
</ul>
<p>여기서 가장 주의할 점은 <strong>페이로드의 데이터는 누구나 Base64로 디코딩해서 볼 수 있다는 것</strong>이다. 서명이 있기 때문에 데이터가 중간에 &#39;변조&#39;되었는지는 알 수 있지만, 암호화되어 가려진 것은 아니다. 따라서 페이로드에는 비밀번호나 개인정보 같은 민감한 데이터를 절대 넣어서는 안 된다.</p>
<h2 id="4-토큰-만료의-딜레마와-투트랙two-track-전략">4. 토큰 만료의 딜레마와 투트랙(Two-Track) 전략</h2>
<p>토큰의 페이로드에는 반드시 &#39;만료 시간(Expiration)&#39;이 포함된다. 만료 시간이 너무 길면 해커에게 토큰이 탈취되었을 때 피해가 걷잡을 수 없이 커진다. 반대로 너무 짧으면 사용자가 수시로 재로그인을 해야 하는 끔찍한 UX를 겪게 된다.</p>
<p>이를 해결하기 위해 실무에서는 두 가지 토큰을 함께 사용하는 투트랙 전략을 취한다.</p>
<ul>
<li><strong>Access Token:</strong> 실질적인 API 인증에 사용된다. 탈취 피해를 최소화하기 위해 수명을 매우 짧게(약 15분~30분) 설정한다.</li>
<li><strong>Refresh Token:</strong> Access Token이 만료되었을 때, 사용자의 재로그인 없이 새로운 토큰을 발급받기 위한 보증수표 역할을 한다. 수명은 넉넉하게(약 2주) 잡는다.</li>
</ul>
<h2 id="5-무상태stateless의-대가와-하이브리드-전략">5. 무상태(Stateless)의 대가와 하이브리드 전략</h2>
<p>완벽해 보이는 JWT도 뼈아픈 대가를 치러야 한다. 만약 해커가 수명이 짧은 Access Token을 탈취했거나, 악성 유저를 즉각적으로 차단해야 하는 상황이 오더라도 서버는 <strong>토큰이 만료될 때까지 이를 중앙에서 제재할 방법이 없다.</strong> 서버가 상태를 기억하지 않기 때문이다.</p>
<p>그래서 실무에서는 <strong>Access Token은 Stateless하게 검증하되, 수명이 긴 Refresh Token은 서버의 DB나 인메모리 저장소에 기록하여 Stateful하게 관리</strong>하는 방식을 사용한다. 사용자가 로그아웃하거나 계정이 정지되면, 서버의 저장소에서 해당 Refresh Token을 지워버려 새로운 Access Token 발급을 원천 차단하는 원리다.</p>
<p>더 나아가, 핵심 API(결제, 비밀번호 변경 등)에서는 토큰 서명만 믿지 않고 <strong>하이브리드 방식</strong>으로 실시간 DB 조회를 거치기도 한다. 이때 발생하는 DB 병목 현상은 보통 인메모리 캐시인 <strong>Redis</strong>를 활용해 속도 문제를 극복한다.</p>
<h2 id="6-토큰-저장-위치의-딜레마-xss-vs-csrf">6. 토큰 저장 위치의 딜레마 (XSS vs CSRF)</h2>
<p>발급받은 이 두 토큰을 클라이언트 어디에 보관할 것인가는 보안의 핵심이다.</p>
<ul>
<li><strong>Local Storage:</strong> 브라우저 저장소에 보관하면 관리가 편하지만, <strong>XSS(교차 사이트 스크립팅)</strong> 공격에 취약하다. 해커가 웹사이트에 악성 자바스크립트를 심으면 저장소의 토큰을 고스란히 털어갈 수 있다.</li>
<li><strong>HttpOnly Cookie:</strong> 자바스크립트로는 절대 접근할 수 없는 보안 쿠키다. XSS 방어에는 탁월하지만, 사용자의 브라우저를 속여 강제로 위조된 요청을 보내게 만드는 <strong>CSRF</strong> 공격에 노출될 수 있다. (단, 이는 쿠키의 <code>SameSite</code> 속성 등 설정으로 상당 부분 방어가 가능하다.)</li>
</ul>
<blockquote>
<p><strong>💡 결론적으로 실무에서는:</strong>
수명이 짧은 <strong>Access Token은 클라이언트의 로컬 메모리(JS 전역 변수 등)에 저장</strong>하여 API 요청 시 헤더에 직접 심어(<code>Authorization: Bearer</code>) 보내고, 수명이 길고 중요한 <strong>Refresh Token은 HttpOnly, Secure 속성이 걸린 쿠키에 저장</strong>하여 해커의 접근을 원천 차단하는 방식을 표준으로 삼는다.</p>
</blockquote>
<h2 id="7-보안의-끝판왕-refresh-token-rotation-rtr">7. 보안의 끝판왕: Refresh Token Rotation (RTR)</h2>
<p>Refresh Token을 쿠키에 안전하게 숨겼더라도, 네트워크 스니핑 등으로 탈취당할 가능성은 여전히 존재한다. 해커가 리프레시 토큰을 얻으면 엑세스 토큰을 무한정 재발급받을 수 있는 치명적인 문제가 생긴다.</p>
<p>이를 방어하기 위해 <strong>Token Rotation (RTR)</strong> 기법을 도입한다. 이 방식은 사용자가 Refresh Token을 사용해 새 Access Token을 발급받을 때, <strong>Refresh Token도 무조건 새것으로 교체(1회용으로 사용)</strong>하는 방식이다. 만약 해커가 이미 사용되어 폐기된 구형 Refresh Token을 들고 서버에 나타난다면, 서버는 즉시 이를 토큰 탈취 상황으로 간주하고 해당 유저의 모든 토큰 장부를 삭제시켜 완전히 강제 로그아웃 처리해 버린다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Backend] 로그인과 해싱]]></title>
            <link>https://velog.io/@minho-git/Backend-%EC%95%88%EC%A0%84%ED%95%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8%EA%B3%BC-%ED%95%B4%EC%8B%B1</link>
            <guid>https://velog.io/@minho-git/Backend-%EC%95%88%EC%A0%84%ED%95%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8%EA%B3%BC-%ED%95%B4%EC%8B%B1</guid>
            <pubDate>Tue, 24 Mar 2026 15:16:35 GMT</pubDate>
            <description><![CDATA[<p>백엔드 개발을 하다 보면 CRUD 다음으로 마주하는 산이 바로 &#39;회원 로그인&#39; 기능이다. 로그인을 만든다는 건 곧 시스템의 보안을 다뤄야 한다는 뜻이다. 이때 다음 문장을 꼭 기억하자.</p>
<blockquote>
<p><strong>&quot;보안은 잘하는 것이 아니라, 보안 사고가 터질 수밖에 없는 구조 자체를 만들지 않는 것이다.&quot;</strong></p>
</blockquote>
<h3 id="암호화가-아닌-단방향-해싱">암호화가 아닌 단방향 해싱</h3>
<p>처음 로그인 기능을 구현할 때 흔히 &quot;비밀번호를 안전하게 암호화해야지&quot;라고 생각하기 쉽다. 하지만 이는 정답이 아니다. <strong>정답은 &#39;단방향 해싱(Hashing)&#39;이다.</strong> 암호화는 복호화(원본으로 되돌림)가 가능하다는 것을 전제로 한다. 만약 DB가 털리면서 암호화 키까지 유출된다면, 모든 유저의 비밀번호 원본이 고스란히 노출되는 치명적인 사고로 이어진다. 반면 해싱은 입력값을 넣으면 항상 같은 결과가 나오지만, 원래 값으로 되돌리는 것(복호화)은 불가능하다. 즉, <strong>비밀번호는 &#39;확인&#39;만 가능해야 하며 시스템 관리자조차 다시는 원본을 꺼낼 수 없어야 한다.</strong></p>
<p><strong>💡 로그인 인증 흐름</strong>
로그인 시에는 이 단방향 원리를 이용한다.</p>
<ol>
<li>사용자가 회원가입(이메일 + 비밀번호)을 한다.</li>
<li>DB에는 비밀번호 원본이 아닌, 해싱된 &#39;해시값&#39;이 저장된다.</li>
<li>사용자가 다음에 로그인할 때 입력한 비밀번호를 똑같이 해싱하여, DB에 저장된 해시값과 비교한다.</li>
<li>두 해시값이 일치하면 인증을 통과시킨다.</li>
</ol>
<hr>
<h3 id="아무-해시-함수나-쓰면-안-되는-이유">아무 해시 함수나 쓰면 안 되는 이유</h3>
<p>단방향이라고 해서 SHA-256 같은 일반적인 해시 함수를 로그인에 써서는 안 된다. 일반 해시 함수는 처리 속도가 너무 빠르다. 해커가 무차별 대입(Brute Force) 공격으로 비밀번호를 순식간에 알아낼 위험이 크고, 미리 계산해 둔 해시값 표(레인보우 테이블)를 이용해 역추적할 수도 있다. </p>
<p>따라서 안전한 패스워드 해시 알고리즘은 다음 3가지 조건을 반드시 갖춰야 한다.</p>
<ol>
<li><strong>속도가 느려야 한다.</strong>
계산 속도를 의도적으로 늦춰야 한다. 1초에 수십억 개의 해시값을 대입할 수 있는 것과 1초에 1개밖에 대입하지 못하는 것은 하늘과 땅 차이다. 공격자의 시간 비용을 기하급수적으로 늘려 해커가 스스로 공격을 포기하게 만들어야 한다.</li>
<li><strong>메모리를 많이 써야 한다.</strong>
해시값을 하나 만드는 데 막대한 자원(메모리)을 소모하게 만들면, 한 번에 생성할 수 있는 해시값의 수가 급격히 줄어든다. 이는 병렬 처리를 통한 대규모 공격을 방어하는 핵심이다.</li>
<li><strong>솔트(Salt)가 포함되어야 한다.</strong>
비밀번호 원본에 임의의 텍스트(솔트)를 덧붙여 해싱하는 방식이다. 이렇게 하면 똑같은 비밀번호를 쓰는 유저라도 DB에는 전혀 다른 해시값이 저장되어, 레인보우 테이블 공격을 완벽히 무력화할 수 있다.</li>
</ol>
<p>이 모든 조건을 만족하는 알고리즘으로 Argon2id, bcrypt, scrypt 등이 있다. 그중에서도 현재 백엔드 실무 표준이자 가장 강력히 권장되는 알고리즘은 <strong><code>Argon2id</code></strong>다. 이걸 사용하자.</p>
<hr>
<h3 id="로그인-로직-구현-시-놓치면-안-되는-보안-디테일-3가지">로그인 로직 구현 시 놓치면 안 되는 보안 디테일 3가지</h3>
<p>알고리즘 선택은 끝났다. 하지만 실제 로그인 로직을 짤 때 다음 3가지 디테일을 놓치면 시스템에 빈틈이 생긴다. 꼭 기억하자.</p>
<h4 id="1-타이밍-공격timing-attack-방어">1. 타이밍 공격(Timing Attack) 방어</h4>
<p>로그인을 시도할 때 DB에 해당 이메일이 없으면 바로 에러를 반환하고, 이메일이 존재하면 패스워드 검증을 하느라 시간이 조금 더 걸린다고 가정해 보자. 해커는 이 미세한 <strong>&#39;응답 시간의 차이&#39;</strong>를 측정해 특정 이메일이 시스템에 가입되어 있는지 알아낼 수 있다. 
이를 막으려면 DB에 이메일이 존재하지 않더라도 <strong>임의의 더미(Dummy) 해시 검증 로직</strong>을 실행하도록 만들어야 한다. 성공하든 실패하든 응답 시간을 동일하게 맞춰서 해커의 눈을 속이는 것이다.</p>
<h4 id="2-계정-열거enumeration-방어">2. 계정 열거(Enumeration) 방어</h4>
<p>에러 메시지를 친절하게 &quot;존재하지 않는 계정입니다&quot;와 &quot;비밀번호가 틀렸습니다&quot;로 나누어주면, 해커는 이를 이용해 유효한 이메일 목록만 싹 수집한 뒤 집중 공격을 퍼부을 수 있다. 
시스템 내부 정보가 노출되지 않도록, 계정이 없든 비밀번호가 틀렸든 응답 메시지는 무조건 <strong>&quot;이메일 또는 비밀번호가 올바르지 않습니다&quot;</strong>로 뭉뚱그려 통일해야 한다.</p>
<h4 id="3-재해싱rehashing">3. 재해싱(Rehashing)</h4>
<p>시간이 흘러 컴퓨팅 파워가 발전하면 보안 정책(해싱 반복 횟수, 메모리 사용량 등)을 더 강력하게 업그레이드해야 한다. 이때 기존 유저들의 구버전 해시값은 어떻게 할까? 
유저가 이전 방식으로 해싱된 비밀번호로 정상 로그인을 성공했을 때, 백그라운드에서 <strong>새로운 보안 정책이 적용된 해시값으로 DB를 조용히 업데이트</strong>해 주면 된다. 이를 재해싱이라 부르며, 유저가 모르는 사이에 시스템 보안을 알아서 최신으로 유지하는 마이그레이션 기법이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Project3: Virtual Memory - Memory Mapped Files]]></title>
            <link>https://velog.io/@minho-git/Project3-Virtual-Memory-Memory-Mapped-Files</link>
            <guid>https://velog.io/@minho-git/Project3-Virtual-Memory-Memory-Mapped-Files</guid>
            <pubDate>Sat, 06 Dec 2025 11:55:23 GMT</pubDate>
            <description><![CDATA[<h2 id="1-memory-mapped-files-개념">1. Memory Mapped Files 개념</h2>
<p>지금까지는 파일 입출력(<code>read</code>, <code>write</code>)을 수행할 때 버퍼를 이용하는 방식을 사용해왔다. <code>mmap</code>은 파일을 프로세스의 가상 메모리 주소 공간에 직접 매핑하여, 마치 메모리를 다루듯이 파일을 다룰 수 있게 해주는 기능이다.</p>
<p>이제 파일 데이터를 페이지 단위로 관리하며 가상 메모리 시스템과 통합해야 한다.</p>
<br>

<h2 id="2-mmap-구현-do_mmap">2. mmap 구현 (<code>do_mmap</code>)</h2>
<p>시스템 콜 <code>mmap</code>은 실제 로직을 수행하는 <code>do_mmap</code>을 호출하는 래퍼(Wrapper) 형태로 구성된다. 핵심은 매핑 요청이 유효한지 검증하는 과정이다.</p>
<h3 id="유효성-검사-루틴">유효성 검사 루틴</h3>
<p><code>addr</code>(매핑 시작 주소)에 대해 다음 조건들을 순차적으로 확인해야 한다. 실패 시 <code>NULL</code>을 반환한다.</p>
<ol>
<li><strong>기본 주소 검사:</strong> <code>addr</code>이 <code>NULL</code>이거나 <code>0</code>이 아니어야 한다.</li>
<li><strong>페이지 정렬:</strong> 주소가 정확히 4KB 단위로 나누어 떨어져야 한다. (<code>pg_ofs(addr) == 0</code>)</li>
<li><strong>영역 중복 검사 (<code>spt_find_page</code>):</strong><ul>
<li>이미 실행 중인 코드/데이터 영역과 겹치지 않아야 한다.</li>
<li>스택 영역과 겹치지 않아야 한다.</li>
<li>이전에 <code>mmap</code>으로 할당된 다른 영역과 겹치지 않아야 한다.</li>
</ul>
</li>
<li><strong>영역 범위 검사 (<code>is_user_vaddr</code>):</strong> 매핑하려는 주소 공간이 커널 영역이 아닌 사용자 영역 내에 존재해야 한다.</li>
</ol>
<h3 id="페이지-할당">페이지 할당</h3>
<p>검증이 완료되면 <code>vm_alloc_page_with_initializer()</code>를 사용하여 페이지를 할당한다. 이때 페이지 타입은 <code>VM_FILE</code>이 된다.</p>
<br>

<h2 id="3-munmap-구현-do_munmap">3. munmap 구현 (<code>do_munmap</code>)</h2>
<p><code>munmap</code>은 <code>addr</code>부터 시작하여 연속된 <code>VM_FILE</code> 페이지들을 해제하는 과정이다.</p>
<h3 id="연속된-페이지-처리-문제">연속된 페이지 처리 문제</h3>
<p>단순히 <code>addr</code>만 주어졌을 때, 몇 개의 페이지를 해제해야 하는지 알 수 없는 문제가 있다.
이를 해결하기 위해 <strong>매핑된 첫 번째 페이지의 구조체에 할당된 총 페이지 개수 정보를 저장</strong>해 두어야 한다.</p>
<p><code>do_munmap</code> 호출 시, 첫 페이지에서 페이지 개수를 확인하고 정확히 그 횟수만큼 루프를 돌며 해제 작업을 수행한다.</p>
<br>

<h2 id="4-데이터-동기화-dirty-bit--write-back">4. 데이터 동기화 (Dirty Bit &amp; Write Back)</h2>
<p>메모리에 매핑된 파일 데이터가 수정되었다면, 언젠가는 디스크의 실제 파일에도 반영해줘야 한다.</p>
<h3 id="동기화-시점의-결정">동기화 시점의 결정</h3>
<p>Dirty Bit(수정 여부)를 확인하고 파일에 쓰는 작업을 어디서 수행해야 할까?</p>
<ol>
<li><strong><code>munmap</code> 시점:</strong> 사용자가 명시적으로 해제를 요청할 때 수행된다.</li>
<li><strong><code>file_backed_destroy</code> 시점:</strong> 페이지가 파괴될 때 수행된다.</li>
</ol>
<p>정답은 <strong><code>file_backed_destroy</code></strong> 내부에서 처리하는 것이다. 프로세스가 정상 종료(<code>process_exit</code>)되거나 강제 종료될 때도 SPT가 정리되면서 <code>destroy</code> 함수가 호출되기 때문이다. 이곳에서 처리해야 모든 상황(명시적 해제 및 프로세스 종료)을 커버할 수 있다.</p>
<br>

<h2 id="5-구조체-수정-및-데이터-흐름">5. 구조체 수정 및 데이터 흐름</h2>
<p>파일에 변경 사항을 쓰기 위해서는 해당 페이지가 &#39;어떤 파일&#39;의 &#39;어느 오프셋&#39;에 연결되어 있는지 알아야 한다.</p>
<h3 id="구조체-설계">구조체 설계</h3>
<p><code>struct page</code> 내부의 <code>union</code>에 정의된 <code>file_page</code> 구조체에 <code>aux</code> 정보가 필요하다.
이 <code>aux</code> 필드에는 파일 객체(<code>struct file *</code>)와 읽은 바이트 수, 오프셋 등의 정보가 담겨야 한다.</p>
<h3 id="정보-전달-흐름">정보 전달 흐름</h3>
<p>페이지가 처음 생성될 때는 <code>VM_UNINIT</code> 상태이다. 이때 <code>lazy_load_info</code> 등을 통해 파일 정보를 가지고 있다.
페이지 폴트가 발생하여 <code>file_backed_initialize</code>가 호출될 때, <code>uninit_page</code>가 가지고 있던 <code>aux</code> 정보를 <code>file_page</code>의 <code>aux</code>로 이관해 주어야 한다.</p>
<br>

<h2 id="6-동기화-구현-요약">6. 동기화 구현 요약</h2>
<ol>
<li><strong>구조체 확장:</strong> <code>file_page</code> 구조체에 파일 정보(aux)를 저장할 필드를 추가한다.</li>
<li><strong>초기화 로직:</strong> <code>file_backed_initialize</code> 함수에서 <code>uninit</code> 상태의 aux 데이터를 <code>file_page</code>로 복사/이동시킨다.</li>
<li><strong>파괴 로직:</strong> <code>file_backed_destroy</code> 함수 구현 시, <code>pml4_is_dirty</code>를 통해 수정 여부를 확인한다. 수정되었다면 <code>file_write_at</code> 등을 사용하여 디스크에 내용을 기록하고 페이지를 해제한다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[Project3: Virtual Memory - Stack Growth]]></title>
            <link>https://velog.io/@minho-git/Project3-Virtual-Memory-Stack-Growth</link>
            <guid>https://velog.io/@minho-git/Project3-Virtual-Memory-Stack-Growth</guid>
            <pubDate>Sat, 06 Dec 2025 11:39:21 GMT</pubDate>
            <description><![CDATA[<h2 id="1-stack-growth-개념">1. Stack Growth 개념</h2>
<p>지금까지 구현한 프로젝트에서 <code>USER_STACK</code>은 단일 페이지로 고정되어 있었다. 이제 프로세스가 실행되면서 스택이 현재 크기를 초과할 경우, 필요에 따라 추가 페이지를 할당하여 스택을 확장해 주는 기능을 구현해야 한다.</p>
<p>핵심은 페이지 폴트가 발생했을 때, 이것이 <strong>유효한 스택 확장 요청인지</strong> 아니면 <strong>잘못된 메모리 접근인지</strong>를 구별하는 것이다.</p>
<h3 id="식별-규칙-heuristic">식별 규칙 (Heuristic)</h3>
<p>일반적으로 OS는 시그널 전달 등을 통해 스택 데이터를 수정할 수 있다. 하지만 우리가 다루는 x86-64 아키텍처의 경우, 스택 포인터를 조정하기 전에 접근 권한을 먼저 확인하는 특성이 있다.</p>
<p>이로 인해 <code>PUSH</code> 명령어 실행 시, 스택 포인터(<code>rsp</code>)가 갱신되기 전에 해당 주소에 접근을 시도하게 되고, 실제 <code>rsp</code>보다 8바이트 아래 위치에서 페이지 폴트가 발생할 수 있다. 따라서 유효한 스택 접근으로 판단하는 기준은 다음과 같다.</p>
<blockquote>
<p><strong>접근 주소(<code>addr</code>) &gt;= <code>rsp</code> - 8</strong></p>
</blockquote>
<p>또한 과제 요구사항에 따라 스택의 최대 크기는 <strong>1MB</strong>로 제한한다.</p>
<br>

<h2 id="2-구현-전제-rsp-값의-추적">2. 구현 전제: RSP 값의 추적</h2>
<p>스택 확장의 유효성을 검사하려면 현재 시점의 사용자 스택 포인터(<code>rsp</code>) 값을 정확히 알고 있어야 한다. 페이지 폴트가 발생한 상황에 따라 <code>rsp</code>를 가져오는 방식이 다르다.</p>
<ul>
<li><strong>User Mode에서 발생:</strong> <code>intr_frame</code>의 <code>rsp</code> 멤버를 통해 스택 포인터를 바로 확인할 수 있다.</li>
<li><strong>Kernel Mode에서 발생:</strong> 시스템 콜 처리 도중 페이지 폴트가 발생하여 커널 모드로 진입한 경우, <code>intr_frame</code>의 <code>rsp</code>는 커널 스택을 가리키게 된다. 따라서 유저의 <code>rsp</code> 값을 알 수 없게 된다.</li>
</ul>
<h3 id="해결-방안">해결 방안</h3>
<p>유저 모드에서 커널 모드로 최초 전환되는 시점(예: <code>syscall_handler</code>)에 유저의 <code>rsp</code> 값을 별도로 저장해 두어야 한다.</p>
<ol>
<li><code>struct thread</code> 구조체에 <code>uintptr_t user_rsp</code> 필드를 추가한다.</li>
<li>인터럽트 핸들러 도입부에서 <code>f-&gt;rsp</code> 값을 <code>thread-&gt;user_rsp</code>에 백업한다.</li>
<li><code>vm_try_handle_fault</code>에서 현재 상태가 유저 모드라면 <code>f-&gt;rsp</code>를, 커널 모드라면 <code>thread-&gt;user_rsp</code>를 사용하여 검증 로직을 수행한다.</li>
</ol>
<br>

<h2 id="3-vm_try_handle_fault-로직-수정">3. vm_try_handle_fault 로직 수정</h2>
<p>기존의 페이지 폴트 핸들러를 수정하여 스택 확장이 필요한 케이스를 식별하고 처리해야 한다.</p>
<p><strong>처리 흐름</strong></p>
<ol>
<li><strong>유효성 검사:</strong> 접근한 주소가 유저 영역(<code>is_user_vaddr</code>)인지 확인한다.</li>
<li><strong>Present 비트 확인:</strong> <code>not_present</code>가 <code>false</code>라면, 이미 페이지가 존재하는데 권한 문제로 접근한 것이므로 즉시 <code>false</code>를 반환한다.</li>
<li><strong>SPT 조회:</strong> <code>spt_find_page</code>를 통해 보조 페이지 테이블에 해당 주소가 있는지 확인한다.<ul>
<li><strong>SPT에 존재하는 경우:</strong> 기존 로직대로 Lazy Loading 등을 수행(<code>vm_do_claim_page</code>)한다.</li>
<li><strong>SPT에 없는 경우:</strong> 이곳이 바로 <strong>Stack Growth</strong>를 검토해야 할 지점이다.</li>
</ul>
</li>
</ol>
<br>

<h2 id="4-stack-growth-상세-구현">4. Stack Growth 상세 구현</h2>
<p>SPT에 페이지가 없는 경우, 다음 세 가지 조건을 모두 만족할 때만 스택 확장을 수행한다.</p>
<h3 id="확장-조건">확장 조건</h3>
<ol>
<li><strong>주소 범위:</strong> 접근한 주소(<code>addr</code>)가 유저 영역 내에 존재해야 한다.</li>
<li><strong>RSP 근접성:</strong> <code>addr</code>이 현재 유저 스택 포인터(<code>rsp</code>)보다 크거나, 혹은 <code>PUSH</code> 명령어를 감안하여 <code>rsp - 8</code> 이상이어야 한다.</li>
<li><strong>크기 제한:</strong> 확장 후 전체 스택의 크기가 1MB를 초과하지 않아야 한다. (<code>USER_STACK</code> 시작점부터 <code>addr</code>까지의 거리 계산)</li>
</ol>
<h3 id="vm_stack_growth-함수">vm_stack_growth 함수</h3>
<p>위 조건이 충족되면 <code>vm_stack_growth</code>를 호출하여 실제 확장을 진행한다.</p>
<ol>
<li><strong>Align:</strong> 접근한 주소를 페이지 단위로 내림(<code>pg_round_down</code>) 처리한다.</li>
<li><strong>Allocation:</strong> <code>vm_alloc_page(VM_ANON | VM_MARKER_0, ...)</code>를 호출하여 새로운 익명 페이지를 할당한다. 이때 스택 영역임을 표시할 수 있다.</li>
<li><strong>Claim:</strong> <code>vm_claim_page</code>를 호출하여 물리 프레임과 매핑하고, 필요하다면 <code>memset</code> 등으로 초기화한다.</li>
</ol>
<p>이 과정을 통해 스택은 필요에 따라 1MB 한도 내에서 동적으로 늘어나게 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Project3: Virtual Memory  - Management + Anonymous Page]]></title>
            <link>https://velog.io/@minho-git/Project3-Virtual-Memory-Management-Anonymous-Page</link>
            <guid>https://velog.io/@minho-git/Project3-Virtual-Memory-Management-Anonymous-Page</guid>
            <pubDate>Tue, 02 Dec 2025 16:36:16 GMT</pubDate>
            <description><![CDATA[<h2 id="1-프로젝트-목표-무한한-메모리의-환상">1. 프로젝트 목표: &quot;무한한 메모리의 환상&quot;</h2>
<ul>
<li><p><strong>핵심:</strong> 물리 메모리의 크기 한계를 극복하기 위해 <strong>디스크(Swap/File)</strong> 를 메모리의 연장선으로 사용한다.</p>
</li>
<li><p><strong>원칙: Lazy Loading (게으른 로딩)</strong></p>
<ul>
<li>당장 필요하지 않으면 메모리에 올리지 않고 장부(SPT)에만 적어둔다.</li>
<li>실제로 필요해서 접근할 때(<strong>Page Fault</strong>) 비로소 메모리에 올린다.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="2-spt-supplemental-page-table">2. SPT (Supplemental Page Table)</h2>
<blockquote>
<p><strong>&quot;프로세스의 모든 메모리 자원을 관리하는 절대 장부&quot;</strong> <em>하드웨어 페이지 테이블(PML4)은 멍청해서 주소 변환밖에 모른다. 페이지 상태(스왑, 파일 등)를 관리할 별도의 장부가 필요하다.</em></p>
</blockquote>
<ul>
<li><p><strong>자료구조:</strong> 검색 속도가 빠른 <strong>Hash Table</strong> 사용 권장.</p>
</li>
<li><p><strong>위치:</strong> 프로세스(<code>struct thread</code>) 내부에 존재.</p>
</li>
<li><p><strong>구성요소:</strong> <code>struct page</code> (가상 페이지 정보를 담은 객체)</p>
<ul>
<li><strong><code>struct hash_elem hash_elem</code></strong>: 해시 테이블에 넣기 위해 반드시 추가해야 함.</li>
<li><strong><code>Union</code></strong>: 메모리 절약을 위해 <code>UNINIT</code>, <code>ANON</code>, <code>FILE</code> 중 하나의 상태만 가지도록 설계됨.</li>
</ul>
</li>
</ul>
<h3 id="spt-메서드-구현">SPT 메서드 구현</h3>
<h4 id="①-초기화-spt_init">① 초기화 (<code>spt_init</code>)</h4>
<ul>
<li><p><code>hash_init</code>을 호출하여 빈 테이블을 만든다.</p>
</li>
<li><p><strong>핵심:</strong> <code>page_hash</code>(해싱 함수)와 <code>page_less</code>(비교 함수)를 인자로 넘겨줘야 한다.</p>
<ul>
<li><strong>Key:</strong> 모든 기준은 <strong>가상 주소(<code>va</code>)</strong> 다.</li>
</ul>
</li>
</ul>
<h4 id="②-검색-spt_find_page">② 검색 (<code>spt_find_page</code>)</h4>
<ul>
<li><p><strong>문제:</strong> <code>hash_find</code>는 <code>hash_elem</code>을 요구하는데, 우린 <code>va</code>밖에 없다.</p>
</li>
<li><p><strong>해결 (Dummy Page):</strong></p>
<ol>
<li><code>dummy_page</code>를 만들고 <code>va</code>를 채운다. (<strong><code>pg_round_down</code> 필수!</strong>)</li>
<li><code>hash_find</code>에 <code>dummy_page.hash_elem</code>을 미끼로 던진다.</li>
<li>해시 함수는 오직 <code>va</code>만 보므로, 진짜 페이지를 찾아준다. (같은 녀석으로 인식)</li>
</ol>
</li>
<li><p><strong>반환:</strong> 찾은 <code>elem</code>을 <code>hash_entry</code>로 복원해서 <code>page</code> 반환.</p>
</li>
</ul>
<h4 id="③-삽입-spt_insert_page">③ 삽입 (<code>spt_insert_page</code>)</h4>
<ul>
<li><p><strong>핵심:</strong> 이미 <code>hash_insert</code>가 중복 검사를 해주므로 별도의 검증 로직 불필요.</p>
</li>
<li><p><code>insert</code> 성공 시 <code>NULL</code> 반환, 실패 시 기존 요소 반환함.</p>
</li>
</ul>
<hr>
<h2 id="3-frame-management-물리-메모리-관리">3. Frame Management (물리 메모리 관리)</h2>
<blockquote>
<p><strong>&quot;실제 땅(RAM)을 관리하고 연결하는 과정&quot;</strong></p>
</blockquote>
<h3 id="구조체-관계도-3단-구조">구조체 관계도 (3단 구조)</h3>
<p><code>[ User Page (UVA) ] &lt;---&gt; [ Frame (KVA) ] &lt;---&gt; [ Physical Memory (PA) ]</code></p>
<ul>
<li><p><strong>UVA:</strong> 사용자가 쓰는 가짜 주소. 권한이 없어서 물리 메모리에 직접 못 감.</p>
</li>
<li><p><strong>KVA:</strong> 커널이 쓰는 마스터키. 물리 메모리와 1:1 직통 연결됨.</p>
</li>
<li><p><strong>Page는 Frame을 가진다:</strong> <code>page</code>는 신청서, <code>frame</code>은 등기부. 둘을 연결해야 메모리 사용 가능.</p>
</li>
</ul>
<h3 id="frame-메서드-구현">Frame 메서드 구현</h3>
<ol>
<li><p><strong><code>vm_get_frame</code> (땅 확보):</strong></p>
<ul>
<li><p><code>palloc_get_page(PAL_USER)</code>: <strong>유저 풀</strong>에서 4KB 땅(KVA)을 받아온다.</p>
</li>
<li><p><code>malloc</code>: <strong>커널 풀</strong>에서 <code>struct frame</code> 서류를 만든다.</p>
</li>
<li><p>둘을 연결(<code>frame-&gt;kva = kva</code>)하고 반환.</p>
</li>
</ul>
</li>
<li><p><strong><code>vm_do_claim_page</code> (연결):</strong></p>
<ul>
<li><p><code>vm_get_frame</code> 호출.</p>
</li>
<li><p><code>page &lt;-&gt; frame</code> 서로 포인터 연결.</p>
</li>
<li><p><strong><code>pml4_set_page</code></strong>: 하드웨어(MMU)에 &quot;이 가상 주소는 저 물리 주소다&quot;라고 등록.</p>
</li>
<li><p><code>swap_in</code>: 데이터 로딩 시작.</p>
</li>
</ul>
</li>
<li><p><strong><code>vm_claim_page</code> (인터페이스):</strong></p>
<ul>
<li><code>va</code>로 페이지 찾아서 <code>vm_do_claim_page</code>에 넘기는 결정자 역할.</li>
</ul>
</li>
</ol>
<hr>
<h2 id="4-lazy-loading의-전체-시나리오-핵심-흐름">4. Lazy Loading의 전체 시나리오 (핵심 흐름)</h2>
<blockquote>
<p><strong>&quot;쪽지(Aux)를 써두고, 나중에 펴보고 심부름시킨다.&quot;</strong></p>
</blockquote>
<h3 id="load-프로그램-시작">Load (프로그램 시작)</h3>
<p>데이터를 메모리에 올리지 않고 <strong>쪽지(aux)</strong>만 써둔다.</p>
<ul>
<li><p><strong>함수:</strong> <code>load_segment</code> -&gt; <code>vm_alloc_page_with_initializer</code></p>
</li>
<li><p><strong>동작:</strong></p>
<ol>
<li><p>파일을 4KB씩 쪼갠다.</p>
</li>
<li><p><strong>쪽지(<code>aux</code>) 작성:</strong> &quot;나중에 이 파일의 이 위치 읽어라.&quot;</p>
</li>
<li><p><strong>페이지 생성:</strong> <code>VM_UNINIT</code> 상태로 만들고, 쪽지와 작업반장(<code>lazy_load_segment</code>)을 넣어둠.</p>
</li>
<li><p><strong>SPT 등록:</strong> 메모리는 비어있지만 장부에는 가득 참.</p>
</li>
</ol>
</li>
</ul>
<h3 id="page-fault-실행-중">Page Fault (실행 중)</h3>
<p>사용자가 주소를 건드려서 에러가 난다.</p>
<ul>
<li><p><strong>함수:</strong> <code>vm_try_handle_fault</code> -&gt; <code>vm_do_claim_page</code> -&gt; <code>swap_in</code></p>
</li>
<li><p><strong>동작:</strong></p>
<ol>
<li><p><code>swap_in</code>이 호출되면 <code>uninit_initialize</code>가 실행된다.</p>
</li>
<li><p><strong>신분 세탁:</strong> <code>anon_initializer</code> 등을 호출해 <code>VM_ANON</code> 등으로 변신.</p>
</li>
<li><p><strong>데이터 로딩:</strong> 아까 넣어둔 작업반장(<code>lazy_load_segment</code>) 호출.</p>
</li>
</ol>
</li>
</ul>
<h3 id="lazy-load-진짜-로딩">Lazy Load (진짜 로딩)</h3>
<ul>
<li><p><strong>함수:</strong> <code>lazy_load_segment</code></p>
</li>
<li><p><strong>동작:</strong></p>
<ol>
<li><p>쪽지(<code>aux</code>)를 펼쳐본다.</p>
</li>
<li><p><code>file_read</code>로 진짜 데이터를 읽어서 <code>kva</code>에 넣는다.</p>
</li>
<li><p>남은 공간은 <code>memset(0)</code>으로 청소한다 (보안!).</p>
</li>
</ol>
</li>
</ul>
<hr>
<h2 id="5-stack--memory-mapping-details">5. Stack &amp; Memory Mapping Details</h2>
<h3 id="stack-스택-초기화-setup_stack">Stack (스택) 초기화 (<code>setup_stack</code>)</h3>
<ul>
<li><p><strong>특징:</strong> 파일 족보가 없다. 그냥 <strong>빈 메모리(<code>VM_ANON</code>)</strong>다.</p>
</li>
<li><p><strong>Eager Loading:</strong> 첫 페이지는 인자 전달(<code>argc</code>, <code>argv</code>)을 위해 당장 써야 하므로 <strong>바로 할당(<code>claim</code>)</strong>한다.</p>
</li>
<li><p><strong>구현:</strong></p>
<ol>
<li><p><code>vm_alloc_page(VM_ANON | VM_MARKER_0, ...)</code> (예약)</p>
</li>
<li><p><code>vm_claim_page(...)</code> (즉시 할당)</p>
</li>
<li><p><code>rsp</code> 설정 (USER_STACK)</p>
</li>
</ol>
</li>
</ul>
<hr>
<h2 id="6-process-lifecycle-copy--kill">6. Process Lifecycle (Copy &amp; Kill)</h2>
<h3 id="fork-spt_copy">Fork (<code>spt_copy</code>)</h3>
<p>부모의 SPT를 <code>hash_iterator</code>로 돌면서 복사한다. <strong>얕은 복사는 절대 금지!</strong></p>
<ul>
<li><p><strong><code>VM_UNINIT</code>:</strong> 아직 로딩 안 됨 -&gt; <strong>계획(Initializer, Aux)만 복사</strong>.</p>
</li>
<li><p><strong><code>VM_ANON</code>:</strong> 이미 로딩 됨 -&gt; <strong>물리 메모리 할당(<code>claim</code>) + 내용 복사(<code>memcpy</code>)</strong>.</p>
<ul>
<li><code>struct page</code>를 복사하는 게 아니라, <strong>내용물(4KB)</strong>을 복사해야 함!</li>
</ul>
</li>
</ul>
<h3 id="exit-spt_kill">Exit (<code>spt_kill</code>)</h3>
<ul>
<li><code>hash_clear</code>로 장부를 비우면서 <code>destroy</code> 호출 -&gt; 자원 해제.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Project2. User Programming - From Boot to User Program]]></title>
            <link>https://velog.io/@minho-git/Project2.-User-Programming-pintos-booting-and-first-process-executing-procedure</link>
            <guid>https://velog.io/@minho-git/Project2.-User-Programming-pintos-booting-and-first-process-executing-procedure</guid>
            <pubDate>Tue, 25 Nov 2025 06:35:21 GMT</pubDate>
            <description><![CDATA[<p><strong>Phase 1: 하드웨어 초기화 (Assembly)</strong></p>
<ol>
<li><strong><code>_start</code> ()</strong><ul>
<li>컴퓨터 전원이 켜지면 가장 먼저 실행되는 코드입니다.</li>
<li>CPU를 64비트 모드로 설정하고 기본적인 메모리 관리(페이징)를 준비합니다.</li>
<li>모든 준비가 끝나면 C언어 세상의 시작점인 <code>main()</code> 함수를 호출합니다.</li>
</ul>
</li>
</ol>
<p><strong>Phase 2: 커널 초기화 및 멀티태스킹 활성화 (C)</strong></p>
<ol start="2">
<li><strong><code>main()</code></strong><ul>
<li><code>thread_init()</code>: 현재 실행 중인 코드(main)를 <strong>첫 번째 커널 스레드</strong>로 등록합니다.</li>
<li><code>(기타 초기화)</code>: 파일 시스템, 타이머 등 다른 모든 커널 모듈을 초기화합니다.</li>
<li><code>thread_start()</code>: <strong>인터럽트와 스케줄러를 활성화</strong>합니다. 이 순간부터 핀토스는 여러 스레드를 전환하며 실행할 수 있는 멀티태스킹 운영체제가 됩니다.</li>
<li><code>run_actions()</code>: 부팅 인자를 분석하여 무엇을 실행할지 결정합니다.</li>
</ul>
</li>
</ol>
<p><strong>Phase 3: 첫 사용자 프로그램 실행</strong></p>
<ol start="3">
<li><p><strong><code>run_actions()</code> → <code>run_task()</code></strong></p>
<ul>
<li>사용자가 <code>run my_program</code>과 같은 명령을 내리면 <code>run_task(&quot;my_program&quot;)</code> 함수가 호출됩니다.</li>
</ul>
</li>
<li><p>*<em><code>run_task()</code> → <code>process_create_initd()</code> *</em></p>
<ul>
<li><code>run_task</code>는 첫 사용자 프로세스를 만들기 위해 <code>process_create_initd(&quot;my_program&quot;)</code>를 호출합니다.</li>
</ul>
</li>
<li><p><strong><code>process_create_initd()</code> → <code>thread_create()</code></strong></p>
<ul>
<li><code>process_create_initd</code>는 <code>thread_create</code>를 호출하여 <strong>새로운 커널 스레드를 생성</strong>합니다.</li>
<li><strong>핵심:</strong> 이때 <code>thread_create</code>에 <strong><code>initd</code> 함수의 주소</strong>를 넘겨주며, &quot;이 스레드가 실행될 차례가 오면 <code>initd</code> 함수부터 시작해라&quot; 라고 알려줍니다.</li>
</ul>
</li>
<li><p>*<em>(스케줄러에 의해) → <code>initd()</code> 실행 *</em></p>
<ul>
<li><code>thread_create</code>로 만들어진 새 스레드의 실행 순서가 되면, 스케줄러는 CPU의 제어권을 이 스레드에 넘깁니다.</li>
<li>스레드는 약속된 대로 <code>initd</code> 함수부터 실행을 시작합니다.</li>
<li><code>initd</code> 함수는 최종적으로 <code>process_exec(&quot;my_program&quot;)</code>을 호출하여, 디스크에서 프로그램을 읽어 메모리에 올리고 실행하는 모든 과정을 담당합니다.</li>
</ul>
</li>
</ol>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[Project2. User Programming - Argument Passing]]></title>
            <link>https://velog.io/@minho-git/Project2.-User-Programming-Argument-Passing</link>
            <guid>https://velog.io/@minho-git/Project2.-User-Programming-Argument-Passing</guid>
            <pubDate>Tue, 25 Nov 2025 04:34:11 GMT</pubDate>
            <description><![CDATA[<p><code>process_exec()</code> 함수가 현재는 프로그램 이름만 받도록 구현되어 있다. 이를 확장하여 명령줄 인자를 처리할 수 있도로 구현해야한다.</p>
<h2 id="1-목표">1. 목표</h2>
<ul>
<li><code>process_exec()</code>이 실행되는 로직 </li>
<li><code>process_exec()</code>가 넘겨 받는 문자열 인자들을 parshing </li>
<li>사용자 프로그램의 스택에 인자를 설정</li>
<li>사용자 프로그램이 실행되기 직전에 <code>argc</code>, <code>argv</code> 인자를 레지스터에 설정</li>
</ul>
<br>

<h3 id="구현된-기존-로직-살펴보기">구현된 기존 로직 살펴보기</h3>
<h4 id="process_exec"><code>process_exec()</code></h4>
<p>현재 프로그램에 덮어씌우는 방식</p>
<ul>
<li><code>fork()</code>+<code>exec()</code> -&gt; 쉘 </li>
<li><code>fork()</code> -&gt; 크롬 탭, 웹 서버 요청</li>
</ul>
<pre><code class="language-c">int process_exec (void *f_name) { // 현재: 프로그램 명만 받도록 구현되어 있다.

    char *file_name = f_name;
    bool success;

    /* We cannot use the intr_frame in the thread structure.
    * This is because when current thread rescheduled,
    * it stores the execution information to the member. */

    // 새로운 사용자 프로세스의 초기 상태를 설정하기 위한 레지스트 상태를 저장하는 구조체
    struct intr_frame _if;
    _if.ds = _if.es = _if.ss = SEL_UDSEG; // 사용자 권한으로 설정
    _if.cs = SEL_UCSEG; // 사용자 권한으로 설정
    _if.eflags = FLAG_IF | FLAG_MBS; // 인터럽트 플래그 On | 필수

    /* We first kill the current context */
    process_cleanup (); //현재 실행 중인 프로세스를 종료하고 자원 정리

    /* And then load the binary */
    success = load (file_name, &amp;_if);

    /* If load failed, quit. */
    palloc_free_page (file_name);
    if (!success)
        return -1;

    /* Start switched process. */
    do_iret (&amp;_if);
    NOT_REACHED ();
}</code></pre>
<h4 id="load"><code>load()</code></h4>
<p>나중에 구현할 때 file_name -&gt; cmd_line으로 변경 후 파싱 진행</p>
<pre><code class="language-c">static bool load (const char *file_name, struct intr_frame *if_) {
    struct thread *t = thread_current ();
    struct ELF ehdr;
    struct file *file = NULL;
    off_t file_ofs;
    bool success = false;
    int i;

    // 새로운 빈 페이지 테이블 생성
    t-&gt;pml4 = pml4_create ();
    if (t-&gt;pml4 == NULL)
        goto done;
    process_activate (thread_current ());  

    // 실행 파일 열기
    file = filesys_open (file_name); // 여기 파싱한 이름으로 변경해야한다.
    if (file == NULL) {
        printf (&quot;load: %s: open failed\n&quot;, file_name);
        goto done;
    }

    // ELF 헤더 검증
    if (file_read (file, &amp;ehdr, sizeof ehdr) != sizeof ehdr
            || memcmp (ehdr.e_ident, &quot;\177ELF\2\1\1&quot;, 7) // ELF 매직넘버 검사
            || ehdr.e_type != 2 // Executable Check
            || ehdr.e_machine != 0x3E // amd64 -&gt; x86-64
            || ehdr.e_version != 1 // ELF version check
            // ELF Program Header Entry size, Program Hdr
            || ehdr.e_phentsize != sizeof (struct Phdr) 
            || ehdr.e_phnum &gt; 1024) { // head count check
        printf (&quot;load: %s: error loading executable\n&quot;, file_name);
        goto done;
    }

    // 프로그램 헤더(세그먼트 정보) 읽기
    file_ofs = ehdr.e_phoff; // 프로그램 헤더 테이블 위치
    for (i = 0; i &lt; ehdr.e_phnum; i++) {
        struct Phdr phdr; // 프로그램 헤더 하나를 담을 변수

        if (file_ofs &lt; 0 || file_ofs &gt; file_length (file))
            goto done;

        file_seek (file, file_ofs); // file의 위치를 file_ofs 위치로 이동

        if (file_read (file, &amp;phdr, sizeof phdr) != sizeof phdr)
            goto done;

        file_ofs += sizeof phdr; // 다음 헤더 시작 위치를 가리키도록 변경

        // 세그먼트 타입 확인: 각 세크먼트 타입은 프로그램 헤더 항목에 있다. 1:1 mapping
        // 세그먼트 타입을 확인하고, 어떻게 처리할지 결정하는 필터 역할    
        switch (phdr.p_type) {
            case PT_NULL: // 비어있거나 사용X
            case PT_NOTE: // 주석이나 정보
            case PT_PHDR: // 이미 읽었으므로 로드X
            case PT_STACK: // setup_stack() 함수로 직접 스택을 만들어야함.
            default:
                /* Ignore this segment. */
                break;

            case PT_DYNAMIC: // 다음 3가지는 동적 링킹과 관련된 세그먼트.
            case PT_INTERP:  // pinto는 동적 링킹 지원X
            case PT_SHLIB:
                goto done;

            case PT_LOAD: // 실제로 메모리에 로드해야 한다.
                if (validate_segment (&amp;phdr, file)) { // 세그먼트 유효한지 검사
                    bool writable = (phdr.p_flags &amp; PF_W) != 0;
                    // 페이지 크기를 4kb로 내림
                    uint64_t file_page = phdr.p_offset &amp; ~PGMASK;
                    // 가상 메모리 주소
                    uint64_t mem_page = phdr.p_vaddr &amp; ~PGMASK;
                    // 가상 주소가 페이지의 시작에서부터 얼마나 떨어져 있는지 계산(offset)
                    uint64_t page_offset = phdr.p_vaddr &amp; PGMASK;

                    // .bss 세그먼트를 처리하기 위해 존재한다.
                    uint32_t read_bytes, zero_bytes;
                    if (phdr.p_filesz &gt; 0) { // .bss + .data
                        /* Normal segment.
                        * Read initial part from disk and zero the rest. */
                        read_bytes = page_offset + phdr.p_filesz;
                        // 0으로 채워할 바이트 수. 4kb로 올림. .bss 처리
                        zero_bytes = (ROUND_UP (page_offset + phdr.p_memsz, 
                        PGSIZE) - read_bytes); 
                    } else { .bss
                        /* Entirely zero.
                        * Don&#39;t read anything from disk. */
                        read_bytes = 0;
                        zero_bytes = ROUND_UP (page_offset + phdr.p_memsz, 
                        PGSIZE);
                    }

                    // 실제 로딩(배치) 수행
                    // 실제 메모리 공간 할당 + 가상 메모리 맵핑
                    if (!load_segment (file, file_page, (void *) mem_page,
                                read_bytes, zero_bytes, writable))
                        goto done;
                } else
                    goto done;
                break;
        }
    }

    /* Set up stack. */
    // 스택 공간 할당 + 스택 포인터 설정
    if (!setup_stack (if_))
        goto done;

    /* Start address. */
    if_-&gt;rip = ehdr.e_entry;

    success = true;

done:
    /* We arrive here whether the load is successful or not. */
    file_close (file);
    return success;
}</code></pre>
<h2 id="2-핵심-아이디어">2. 핵심 아이디어</h2>
<p>사용자로부터 입력받은 <code>command_line</code> (예: <code>&quot;ls -l foo&quot;</code>)을 쪼개서 커널이 이해할 수 있는 데이터 형태로 가공하는 단계입니다.</p>
<h3 id="1-문자열-복사">1. 문자열 복사</h3>
<p>가장 먼저 고려해야 할 점은 <strong>&quot;왜 원본 문자열을 그대로 쓰면 안 되는가?&quot;</strong> 입니다. 여기에는 <strong>방어적 프로그래밍</strong>과 <strong>메모리 보호</strong>라는 두 가지 중요한 이유가 있습니다.</p>
<ul>
<li><strong>이유 1: <code>strtok_r</code>의 동작 방식</strong><ul>
<li><code>strtok_r</code> 함수는 구분자(공백 등)를 찾으면 해당 위치의 문자를 <code>\0</code> (NULL)으로 덮어씌워 문자열을 잘라냅니다. 즉, <strong>원본 데이터를 직접 수정</strong>하는 파괴적인 함수입니다.</li>
</ul>
</li>
<li><strong>이유 2: 원본 데이터의 메모리 위치</strong><ul>
<li><code>process_exec</code>에 넘어온 <code>f_name</code> 포인터가 가리키는 곳이 <strong>수정 불가능한 영역(Read-Only Data Segment, 리터럴 문자열)</strong>일 수 있습니다. 이곳을 수정하려 시도하면 <strong>Page Fault</strong>가 발생하여 커널 전체가 멈출(Panic) 위험이 있습니다.</li>
</ul>
</li>
</ul>
<blockquote>
<p><strong>결론:</strong> <code>palloc_get_page(0)</code> 등을 사용해 커널 메모리에 새로운 페이지를 할당받고, <code>strlcpy</code>로 문자열을 안전하게 복사한 뒤 작업을 수행해야 합니다.</p>
</blockquote>
<br>

<h3 id="2-변수-및-배열-설계">2. 변수 및 배열 설계</h3>
<p>파싱한 데이터를 담아둘 자료구조를 설계합니다. C언어의 표준 <code>main</code> 함수 인자를 생각하면 이해가 쉽습니다.</p>
<ul>
<li><strong><code>argc</code> (Argument Count)</strong><ul>
<li>인자의 개수를 세는 정수 변수입니다.</li>
</ul>
</li>
<li><strong><code>argv</code> (Argument Vector)</strong><ul>
<li>인자들의 문자열 주소(포인터)를 저장할 배열입니다.</li>
<li><strong>크기:</strong> 핀토스 과제 기준으로는 보통 인자 개수에 제한을 둡니다. 넉넉하게 <code>char *argv[64]</code> (혹은 128) 정도로 선언하면 충분합니다.</li>
<li><code>argv[0]</code>에는 항상 프로그램의 이름(예: <code>ls</code>)이 들어갑니다.</li>
</ul>
</li>
</ul>
<br>

<h3 id="3-문자열-분리-및-스택-삽입">3. 문자열 분리 및 스택 삽입</h3>
<p>복사한 커맨드 라인을 파싱 한 후, x86-64 Calling Convention에 맞춰 스택을 빈틈없이 채워야 합니다. 스택은 높은 주소에서 낮은 주소로(High Address → Low Address) 자란다는 점을 항상 기억해야 합니다.</p>
<p>전체 과정은 크게 4단계로 나뉩니다.</p>
<p><strong>1. 인자 데이터(String Data) 삽입</strong></p>
<ul>
<li>가장 먼저 파싱 된 문자열 데이터 자체를 스택에 넣습니다.</li>
<li>strtok_r로 분리된 토큰(문자열)을 하나씩 스택에 복사합니다 (memcpy).</li>
<li>이때, 나중에 포인터 배열(argv)을 만들 때 사용하기 위해, <strong>문자열이 저장된 스택 주소(rsp)</strong>를 별도 배열(argv_list)에 기록해 두어야 합니다.</li>
</ul>
<p><strong>2. Word Align (8바이트 정렬)</strong></p>
<ul>
<li>성능 최적화와 규약을 위해 스택 포인터를 8의 배수로 맞춰야 합니다.</li>
<li>문자열 데이터를 다 넣은 직후의 rsp가 8로 나누어떨어지지 않는다면, 8의 배수가 될 때까지 스택 포인터를 내리고 남는 공간을 0 (NULL)으로 채웁니다.</li>
</ul>
<p><strong>3. 인자 주소 배열(String Pointers) 삽입</strong></p>
<ul>
<li>이제 앞서 기록해 둔 문자열의 주소들을 스택에 넣을 차례입니다. 주의할 점은 역순 삽입입니다.</li>
<li>Sentinel (argv[argc]): C 표준에 따라 인자 배열의 끝은 NULL이어야 합니다. 가장 먼저 0을 넣습니다.</li>
<li>Pointers (argv[argc-1] ... argv[0]): 기록해 둔 주소들을 거꾸로 스택에 넣습니다. 그래야 스택의 위쪽(낮은 주소)에서 볼 때 argv[0], argv[1] 순서대로 정렬됩니다.</li>
</ul>
<p><strong>4. 가짜 반환 주소 (Fake Return Address) &amp; 레지스터 설정</strong></p>
<ul>
<li>Fake Return Address: main 함수도 누군가에게 호출된 것처럼 보이게 하기 위해, 반환 주소 공간(void pointer 크기)을 확보하고 0으로 채웁니다.</li>
<li>레지스터 설정: 이제 사용자 프로그램이 시작될 때 인자를 알 수 있도록 CPU 레지스터를 세팅합니다.</li>
<li>RDI: 인자의 개수 (argc)</li>
<li>RSI: 인자 배열의 시작 주소 (argv 즉, argv[0]을 가리키는 스택 주소)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/minho-git/post/992b6a46-5c94-459c-9cbb-39ca89c051e8/image.png" alt=""></p>
<h2 id="3-구현">3. 구현</h2>
<p>위 다이어그램의 구조(x86-64 Calling Convention)를 코드로 구현합니다. 이 작업은 <code>load()</code> 내부에서 수행됩니다.</p>
<ul>
<li><code>Parsing &amp; Data Push</code>: 문자열을 파싱하여 스택에 문자열 데이터를 밀어 넣습니다.</li>
<li><code>Alignment</code>: 8바이트 단위로 스택 포인터를 정렬합니다.</li>
<li><code>Pointer Push</code>: 문자열들의 주소(argv)를 스택에 넣습니다.</li>
<li><code>Register</code>: 프로그램 시작 시 인자를 받을 수 있도록 레지스터를 세팅합니다.</li>
</ul>
<p>수정된 <code>load()</code> :</p>
<pre><code class="language-c">static bool load (const char *cmd_line, struct intr_frame *if_) {
    struct thread *t = thread_current ();
    struct ELF ehdr;
    struct file *file = NULL;
    off_t file_ofs;
    bool success = false;
    int i;

    char *file_name;
    char *copy_cmd_line;
    char *token;
    int count = 0;
    char *save_ptr;
    char *address[64];

    copy_cmd_line = palloc_get_page (0);
    if (copy_cmd_line == NULL)
        goto done; 
    strlcpy (copy_cmd_line, cmd_line, PGSIZE);

    token = strtok_r(copy_cmd_line, &quot; &quot;, &amp;save_ptr); 
    file_name = token;

    if (file_name == NULL) {
        goto done;
    }

    /* Allocate and activate page directory. */
    t-&gt;pml4 = pml4_create ();
    if (t-&gt;pml4 == NULL)
        goto done;
    process_activate (thread_current ());

    /* Open executable file. */
    file = filesys_open (file_name);
    if (file == NULL) {
        printf (&quot;load: %s: open failed\n&quot;, file_name);
        goto done;
    }

    // rox
    file_deny_write(file);

    /* Read and verify executable header. */
    if (file_read (file, &amp;ehdr, sizeof ehdr) != sizeof ehdr
            || memcmp (ehdr.e_ident, &quot;\177ELF\2\1\1&quot;, 7)
            || ehdr.e_type != 2
            || ehdr.e_machine != 0x3E // amd64
            || ehdr.e_version != 1
            || ehdr.e_phentsize != sizeof (struct Phdr)
            || ehdr.e_phnum &gt; 1024) {
        printf (&quot;load: %s: error loading executable\n&quot;, file_name);
        goto done;
    }

    /* Read program headers. */
    file_ofs = ehdr.e_phoff;
    for (i = 0; i &lt; ehdr.e_phnum; i++) {
        struct Phdr phdr;

        if (file_ofs &lt; 0 || file_ofs &gt; file_length (file))
            goto done;
        file_seek (file, file_ofs);

        if (file_read (file, &amp;phdr, sizeof phdr) != sizeof phdr)
            goto done;
        file_ofs += sizeof phdr;
        switch (phdr.p_type) {
            case PT_NULL:
            case PT_NOTE:
            case PT_PHDR:
            case PT_STACK:
            default:
                /* Ignore this segment. */
                break;
            case PT_DYNAMIC:
            case PT_INTERP:
            case PT_SHLIB:
                goto done;
            case PT_LOAD:
                if (validate_segment (&amp;phdr, file)) {
                    bool writable = (phdr.p_flags &amp; PF_W) != 0;
                    uint64_t file_page = phdr.p_offset &amp; ~PGMASK;
                    uint64_t mem_page = phdr.p_vaddr &amp; ~PGMASK;
                    uint64_t page_offset = phdr.p_vaddr &amp; PGMASK;
                    uint32_t read_bytes, zero_bytes;
                    if (phdr.p_filesz &gt; 0) {
                        /* Normal segment.
                         * Read initial part from disk and zero the rest. */
                        read_bytes = page_offset + phdr.p_filesz;
                        zero_bytes = (ROUND_UP (page_offset + phdr.p_memsz, PGSIZE)
                                - read_bytes);
                    } else {
                        /* Entirely zero.
                         * Don&#39;t read anything from disk. */
                        read_bytes = 0;
                        zero_bytes = ROUND_UP (page_offset + phdr.p_memsz, PGSIZE);
                    }
                    if (!load_segment (file, file_page, (void *) mem_page,
                                read_bytes, zero_bytes, writable))
                        goto done;
                }
                else
                    goto done;
                break;
        }
    }

    /* Set up stack. */
    if (!setup_stack (if_))
        goto done;

    /* Start address. */
    if_-&gt;rip = ehdr.e_entry;

    while (token != NULL &amp;&amp; count &lt; 64) {
        if_-&gt;rsp -= (strlen(token) + 1);
        memcpy(if_-&gt;rsp, token, strlen(token) + 1); 

        address[count++] = if_-&gt;rsp;
        token = strtok_r(NULL, &quot; &quot;, &amp;save_ptr);
    }

    // 배열의 마지막 NULL 넣기.
    address[count] = NULL;

    //8바이트 정렬하기
    if_-&gt;rsp = if_-&gt;rsp &amp; ~0x7;

    // 스택에 배열 넣기 
    for (int i = count; i &gt;= 0; i--) {
        if_-&gt;rsp -= sizeof(address[i]);
        *((char **)(if_-&gt;rsp)) = address[i];
    }

    // 레지스터 설정하기
    if_-&gt;R.rdi = count;
    if_-&gt;R.rsi = if_-&gt;rsp;

    if_-&gt;rsp -= sizeof(char *);
    *((char **)(if_-&gt;rsp)) = 0;

    success = true;

done:
    if (copy_cmd_line != NULL) {
        palloc_free_page(copy_cmd_line);
    }

    /* We arrive here whether the load is successful or not. */
    if (success) {
        t-&gt;hold_file = file;
    } else {
        file_close (file); // 무조건 닫으면 안된다. file의 쓰기 금지 락을 유지해야하기 때문이다.
    }

    return success;
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[멀티 레벨 피드백 큐 (MLFQ)]]></title>
            <link>https://velog.io/@minho-git/%EB%A9%80%ED%8B%B0-%EB%A0%88%EB%B2%A8-%ED%94%BC%EB%93%9C%EB%B0%B1-%ED%81%90-MLFQ</link>
            <guid>https://velog.io/@minho-git/%EB%A9%80%ED%8B%B0-%EB%A0%88%EB%B2%A8-%ED%94%BC%EB%93%9C%EB%B0%B1-%ED%81%90-MLFQ</guid>
            <pubDate>Wed, 12 Nov 2025 13:53:33 GMT</pubDate>
            <description><![CDATA[<h3 id="1-mlfq의-목적">1. MLFQ의 목적</h3>
<p>스케줄러 설계 시 두 가지 상충되는 목표가 있습니다.</p>
<ol>
<li><strong>반환 시간 (Turnaround Time) 최적화:</strong> 짧은 작업을 먼저 실행 (SJF 방식)</li>
<li><strong>응답 시간 (Response Time) 최적화:</strong> 대화형 사용자에게 빠른 응답 (RR 방식)</li>
</ol>
<p>SJF는 작업의 총 실행 시간을 미리 알아야 하는 문제가 있고, RR은 반환 시간에 불리합니다.</p>
<p>MLFQ의 핵심 질문은 이것입니다: <strong>&quot;작업의 실행 시간에 대한 사전 정보 없이, 대화형 작업의 응답 시간을 최소화하고 동시에 반환 시간을 최소화하는 스케줄러를 어떻게 설계할 수 있을까?&quot;</strong></p>
<h3 id="2-mlfq의-기본-동작-규칙">2. MLFQ의 기본 동작 규칙</h3>
<p>MLFQ는 여러 개의 큐로 구성되며, 각 큐는 서로 다른 우선순위를 갖습니다. 스케줄러는 작업의 특성에 따라 <strong>우선순위를 동적으로 조정</strong>합니다.</p>
<ul>
<li><strong>규칙 1:</strong> <code>Priority(A) &gt; Priority(B)</code> 이면, A가 실행됩니다. (B는 실행되지 않음)</li>
<li><strong>규칙 2:</strong> <code>Priority(A) = Priority(B)</code> 이면, A와 B는 라운드 로빈(RR) 방식으로 실행됩니다.</li>
</ul>
<p>작업의 우선순위를 결정하는 규칙은 다음과 같습니다.</p>
<ul>
<li><strong>규칙 3:</strong> 작업이 시스템에 진입하면, 가장 높은 우선순위 큐에 배치됩니다.</li>
<li><strong>규칙 4:</strong> 작업이 주어진 타임 슬라이스를 모두 사용하면, 우선순위가 낮아집니다. (즉, 한 단계 아래 큐로 이동)</li>
<li><strong>규칙 5:</strong> 작업이 타임 슬라이스를 소진하기 전에 CPU를 양도하면 (예: I/O 작업), 같은 우선순위를 유지합니다.</li>
</ul>
<p>이 규칙들은 다음과 같은 효과를 가집니다.</p>
<ul>
<li><strong>SJF 근사:</strong> CPU를 오래 사용하는 작업(CPU-bound)은 타임 슬라이스를 계속 소진하여 낮은 우선순위 큐로 빠르게 강등됩니다. (규칙 4)</li>
<li><strong>응답 시간 최적화:</strong> 입출력 위주의 대화형 작업(I/O-bound)은 CPU를 양도하므로 높은 우선순위를 유지하며 빠른 응답을 받습니다. (규칙 5)</li>
</ul>
<hr>
<h3 id="3-기본-규칙의-문제점">3. 기본 규칙의 문제점</h3>
<p>하지만 위의 기본 규칙만으로는 심각한 결점 세 가지가 발생합니다.</p>
<ol>
<li><p><strong>기아 상태 (Starvation)</strong></p>
<ul>
<li>항상 더 높은 우선순위의 작업들만 시스템에 계속 도착한다면, 낮은 우선순위 큐에 있는 작업들은 CPU 시간을 전혀 할당받지 못할 수 있습니다.</li>
</ul>
</li>
<li><p><strong>얌체 프로세스 (Gaming the Scheduler)</strong></p>
<ul>
<li>악의적인 프로세스가 타임 슬라이스가 끝나기 직전, 고의로 I/O 작업을 요청하여 CPU를 양도할 수 있습니다.</li>
<li>이 경우, 규칙 5에 따라 높은 우선순위를 계속 유지하며 CPU를 불공평하게 독점하게 됩니다.</li>
</ul>
</li>
<li><p><strong>특성 변화 대응 불가</strong></p>
<ul>
<li>처음에는 CPU 위주 작업이라 낮은 큐로 강등되었으나, 나중에 대화형 작업으로 성격이 바뀐다 해도 시스템은 과거 기록만 보고 해당 작업을 계속 낮은 우선순위로 취급합니다.</li>
</ul>
</li>
</ol>
<hr>
<h3 id="4-문제-해결-방안">4. 문제 해결 방안</h3>
<p>이러한 문제들을 해결하기 위해 MLFQ에 두 가지 핵심 규칙이 추가됩니다.</p>
<h4 id="해결책-1-우선순위-상향-조정-priority-boost">해결책 1: 우선순위 상향 조정 (Priority Boost)</h4>
<blockquote>
<p><strong>규칙 6 (수정):</strong> 일정 기간 S가 지나면, 시스템의 모든 작업을 최상위 큐로 이동시킨다.</p>
</blockquote>
<p>이 &#39;우선순위 상향 조정&#39;은 두 가지 문제를 한 번에 해결합니다.</p>
<ul>
<li><strong>기아 상태 해결:</strong> 아무리 낮은 우선순위에 있던 작업이라도, 주기적으로 최상위 큐로 올라가 실행될 기회를 얻습니다.</li>
<li><strong>특성 변화 대응:</strong> CPU 위주 작업에서 대화형 작업으로 성격이 바뀐 프로세스도, 최상위 큐로 이동하여 스케줄러가 자신의 변경된 특성을 파악할 기회를 갖게 됩니다.</li>
</ul>
<h4 id="해결책-2-얌체-프로세스-방지-accounting">해결책 2: 얌체 프로세스 방지 (Accounting)</h4>
<p>규칙 4와 5를 &#39;CPU 양도 횟수&#39;가 아닌 &#39;CPU 총 사용 시간&#39;을 기준으로 변경하여 얌체 프로세스를 방지합니다.</p>
<blockquote>
<p><strong>규칙 4/5 (수정):</strong> 주어진 단계(큐)에서 작업의 CPU 총 사용 시간을 측정한다. 작업이 해당 큐의 시간 할당량(Time Allotment)을 모두 소진하면 (CPU를 한 번에 썼든, 여러 번 나눠 썼든 상관없이), 우선순위는 낮아진다. (아래 큐로 이동)</p>
</blockquote>
<p>이렇게 하면 얌체 프로세스가 I/O를 아무리 짧게 자주 발생시켜도, 결국 해당 큐의 총 CPU 사용 시간을 채우게 되면 아래 큐로 강등됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Project1:  Threads - Priority Donation]]></title>
            <link>https://velog.io/@minho-git/Project1-Threads-Priority-Donation</link>
            <guid>https://velog.io/@minho-git/Project1-Threads-Priority-Donation</guid>
            <pubDate>Wed, 12 Nov 2025 08:10:29 GMT</pubDate>
            <description><![CDATA[<h2 id="priority-donation-구현">Priority Donation 구현</h2>
<h4 id="기부donation가-필요한-순간-우선순위-역전-priority-inversion">기부(Donation)가 필요한 순간: 우선순위 역전 (Priority Inversion)</h4>
<p>다음과 같이 우선순위가 다른 세 스레드가 있다고 가정합니다.</p>
<ul>
<li><strong>H 스레드</strong>(50)</li>
<li><strong>M 스레드</strong>(30)</li>
<li><strong>L 스레드</strong>(10)</li>
</ul>
<p><strong>문제 상황 (우선순위 역전):</strong></p>
<ol>
<li><strong>L</strong> 스레드가 특정 <code>lock</code>을 획득하여 실행 중입니다.</li>
<li><strong>H</strong> 스레드가 실행되다가, <strong>L</strong>이 가진 <code>lock</code>이 필요해 <code>lock_acquire()</code>를 호출하고 대기(<code>BLOCKED</code>) 상태가 됩니다.</li>
<li>이제 <strong>L</strong>이 마저 실행되어 <code>lock</code>을 풀어줘야 하는데, <strong>M</strong> 스레드가 CPU를 선점합니다. (M(30) &gt; L(10))</li>
<li><strong>결과:</strong> 가장 우선순위가 높은 <strong>H</strong>(50)는 <strong>L</strong>(10)이 끝나길 기다리는데, <strong>L</strong>은 <strong>M</strong>(30) 때문에 실행되지 못합니다.<blockquote>
<p>즉, <strong>H</strong> 스레드가 자신보다 훨씬 우선순위가 낮은 <strong>M</strong> 스레드 때문에 실행을 못 하는 <strong>&#39;우선순위 역전&#39;</strong>이 발생합니다.</p>
</blockquote>
</li>
</ol>
<p><strong>해결책 (우선순위 기부):</strong></p>
<ol>
<li><strong>H</strong> 스레드가 <strong>L</strong>이 가진 <code>lock</code>을 기다릴 때, 자신의 높은 우선순위(50)를 <strong>L</strong>에게 <strong>기부(Donation)</strong>합니다.</li>
<li><strong>L</strong> 스레드의 우선순위는 일시적으로 <code>max(10, 50)</code> = <strong>50</strong>이 됩니다.</li>
<li><strong>L</strong>(50)은 <strong>M</strong>(30)보다 우선순위가 높아졌으므로, CPU를 선점하여 <code>lock</code>을 해제하는 작업을 빠르게 완료할 수 있습니다.</li>
<li><strong>L</strong>이 <code>lock</code>을 해제(<code>lock_release</code>)하면, 기부받았던 우선순위를 반납하고 원래 우선순위(10)로 돌아갑니다.</li>
<li><strong>H</strong> 스레드는 <code>lock</code>을 획득하고 정상적으로 실행됩니다.</li>
</ol>
<hr>
<h3 id="1-목표">1. 목표</h3>
<p>우선순위 역전(Priority Inversion)이 일어나지 않도록 우선순위 기부(Priority Donation)를 구현하자.</p>
<hr>
<h3 id="2-핵심-아이디어">2. 핵심 아이디어</h3>
<ul>
<li>우선순위 기부를 위해서 필요한 필드를 <code>thread</code> 구조체에 선언한다.</li>
<li>우선순위 역전이 일어나는 <code>lock_acquire</code> 시점에 기부 로직을 구현한다.</li>
<li>기부가 연쇄적으로(Nested) 발생하는 문제를 해결한다.</li>
<li>락을 반납하는 <code>lock_release</code> 시점에 기부를 철회하고 우선순위를 복원한다.</li>
<li><code>thread_set_priority</code> 함수가 기부 상황에서도 올바르게 동작하도록 수정한다.</li>
</ul>
<hr>
<h3 id="3-구현-과정">3. 구현 과정</h3>
<h4 id="1-thread-구조체-필드-추가">1. <code>thread</code> 구조체 필드 추가</h4>
<p><code>thread.h</code>의 <code>struct thread</code>에 기부에 필요한 필드들을 추가합니다.</p>
<ul>
<li><code>original_priority</code>: 기부받기 전의 고유 우선순위를 저장합니다.</li>
<li><code>waiting_on</code>: 락 획득을 대기할 때, 해당 락을 가리키는 포인터입니다. (연쇄 기부 용)</li>
<li><code>donations</code>: 나에게 우선순위를 기부한 스레드들의 리스트입니다.</li>
<li><code>donation_elem</code>: 나를 다른 스레드의 <code>donations</code> 리스트에 넣을 때 사용할 리스트 요소입니다.</li>
</ul>
<pre><code class="language-c">// thread.h
struct thread
{
    ...

    // 기부 전용 명찰
    struct list_elem donation_elem;

    // 기부자들
    struct list donations;

    // 원래 우선순위
    int original_priority;

    // 어떤 락을 기다리고있는지 처음에는 NULL
    struct lock *waiting_on;

    ...

};</code></pre>
<p>(이후 <code>thread.c</code>의 <code>init_thread</code>에서 <code>original_priority = priority</code>, <code>waiting_on = NULL</code>, <code>list_init(&amp;t-&gt;donations)</code>로 초기화합니다.)</p>
<hr>
<h4 id="2-lock_acquire-수정-기부-및-연쇄-전파">2. <code>lock_acquire</code> 수정 (기부 및 연쇄 전파)</h4>
<p><code>synch.c</code>의 <code>lock_acquire</code> 함수를 수정합니다. 락 소유자(<code>holder</code>)가 이미 존재하여 락 획득에 실패하는 경우( <code>if (lock-&gt;holder != NULL)</code> ), <code>sema_down</code>으로 잠들기 전에 기부 로직을 수행합니다.</p>
<p><code>donations</code> 리스트를 정렬하기 위한 헬퍼 함수 <code>donate_priority_less</code>와, 연쇄 기부를 처리할 헬퍼 함수 <code>update_holder_priority</code>를 먼저 정의합니다.</p>
<p><strong><code>lock_acquire</code> 본체 수정 (in <code>synch.c</code>):</strong></p>
<pre><code class="language-c">void 
lock_acquire (struct lock *lock) {
    ASSERT (lock != NULL);
    ASSERT (!intr_context ());
    ASSERT (!lock_held_by_current_thread (lock));

    struct thread *cur = thread_current();

    if (lock-&gt;holder != NULL) { // 락 소유자가 있어 대기 및 기부 발생

        // 1. 내가 이 락을 기다린다고 기록 (연쇄 기부용)
        cur-&gt;waiting_on = lock;

        // 2. 락 소유자의 기부 리스트에 나를 추가
        list_insert_ordered(&amp;lock-&gt;holder-&gt;donations, &amp;cur-&gt;donation_elem, donate_priority_less, NULL); 

        // 3. 락 소유자의 우선순위를 (필요시 연쇄적으로) 갱신
        update_holder_priority(lock-&gt;holder);

        // 4. 락이 풀릴 때까지 잠들기
        sema_down (&amp;lock-&gt;semaphore);

    } else {
        // 락 소유자가 없으므로 바로 획득
        sema_down (&amp;lock-&gt;semaphore); 
    }

    // 락을 획득했으므로 (깨어났거나, 바로 획득했거나)
    cur-&gt;waiting_on = NULL; // 더 이상 기다리는 락 없음
    lock-&gt;holder = cur;     // 내가 이 락의 소유자임
}</code></pre>
<p><strong>연쇄 갱신 헬퍼 함수 (in <code>synch.c</code>):</strong></p>
<pre><code class="language-c">/* * 락 소유자(holder)의 우선순위를 재계산하고,
 * 만약 우선순위가 갱신되었다면 이 갱신을 연쇄적으로 전파(propagate)합니다.
 */
void update_holder_priority(struct thread *holder) {

    int current_priority = holder-&gt;priority; // 갱신 전 우선순위

    // 1. 기본값은 자신의 원래 우선순위
    int new_priority = holder-&gt;original_priority; 

    // 2. donations 리스트에서 가장 높은 우선순위와 비교
    if (!list_empty(&amp;holder-&gt;donations)) {
        struct thread *max_donor = list_entry(list_front(&amp;holder-&gt;donations), struct thread, donation_elem);

        if (new_priority &lt; max_donor-&gt;priority) {
            new_priority = max_donor-&gt;priority;
        }
    }

    holder-&gt;priority = new_priority; // 3. 최종 우선순위로 갱신

    // 4. [연쇄 전파]
    // 만약 우선순위가 갱신되었고, 이 스레드(holder) 또한 다른 락을 기다리고 있다면
    if (current_priority != holder-&gt;priority) {
        if (holder-&gt;waiting_on != NULL &amp;&amp; holder-&gt;waiting_on-&gt;holder != NULL) {
            // 그 락의 소유자(holder-&gt;waiting_on-&gt;holder)도 재귀적으로 갱신
            update_holder_priority(holder-&gt;waiting_on-&gt;holder);
        }
    }
}</code></pre>
<p><strong>비교 헬퍼 함수 (in <code>synch.c</code>):</strong></p>
<pre><code class="language-c">/* donations 리스트를 우선순위 내림차순으로 정렬하기 위한 비교 함수 */
int donate_priority_less(const struct list_elem *a,
                       const struct list_elem *b, void *aux UNUSED)
{
    struct thread *thread_a = list_entry(a, struct thread, donation_elem);
    struct thread *thread_b = list_entry(b, struct thread, donation_elem);

      return thread_a-&gt;priority &gt; thread_b -&gt;priority;
}</code></pre>
<hr>
<h4 id="3-lock_release-수정-기부-철회">3. <code>lock_release</code> 수정 (기부 철회)</h4>
<p>락을 해제할 때, 락 소유자(현재 스레드)의 <code>donations</code> 리스트를 정리하고 우선순위를 원래대로(혹은 남은 기부 중 최고값으로) 복원합니다.</p>
<pre><code class="language-c">// synch.c
void
lock_release (struct lock *lock) {
    ASSERT (lock != NULL);
    ASSERT (lock_held_by_current_thread (lock));

    struct thread *current = thread_current();

    // 1. &#39;donations&#39; 리스트에서 &quot;이 락(lock)&quot;을 기다리던 기부자들을 제거
    struct list_elem *e = list_begin(&amp;current-&gt;donations);
    while (e != list_end(&amp;current-&gt;donations)) {
        struct thread *t= list_entry(e, struct thread, donation_elem);

        if (t-&gt;waiting_on == lock) {
            e = list_remove(e); // 이 락을 기다렸던 기부자 제거
        } else {
            e = list_next(e);
        }
    }

    // 2. 기부자 정리 후 우선순위 재계산
    if (list_empty(&amp;current-&gt;donations)) {
        // 남은 기부자가 없으면, 원래 우선순위로 복원
        current-&gt;priority = current-&gt;original_priority;

    } else {
        // 남은 기부자(다른 락 대기자)가 있다면
        struct thread *max_donor = list_entry(list_front(&amp;current-&gt;donations), struct thread, donation_elem);

        // original_priority와 남은 기부자 중 최고값으로 갱신
        if (max_donor-&gt;priority &gt; current-&gt;original_priority) {
              current-&gt;priority = max_donor-&gt;priority;
         } else {
              current-&gt;priority = current-&gt;original_priority;
          }
    }

    lock-&gt;holder = NULL; // 3. 락 소유자 반납
    sema_up (&amp;lock-&gt;semaphore); // 4. 대기 중인 다음 스레드(최고 우선순위)를 깨움
}</code></pre>
<hr>
<h4 id="4-thread_set_priority-수정-기부와-선점-연동">4. <code>thread_set_priority</code> 수정 (기부와 선점 연동)</h4>
<p>스레드가 스스로 우선순위를 변경할 때, <code>priority</code>가 아닌 <code>original_priority</code>를 변경하도록 수정합니다. 그 후, 기부 리스트를 포함하여 실제 <code>priority</code>를 갱신하고, 필요시 선점을 수행합니다.</p>
<pre><code class="language-c">// thread.c
void 
thread_set_priority(int new_priority)
{
    struct thread *cur = thread_current();

    // 1. 스레드의 &#39;원래&#39; 우선순위를 갱신
     cur-&gt;original_priority = new_priority;

    // 2. &#39;원래&#39; 우선순위와 &#39;기부받은&#39; 우선순위 중 최고값으로 현재 우선순위(priority)를 갱신
    int max_priority = new_priority;

    if (!list_empty(&amp;cur-&gt;donations)) {
        struct thread *front_thread = list_entry(list_begin(&amp;cur-&gt;donations), struct thread, donation_elem);
        if (front_thread-&gt;priority &gt; max_priority) {
            max_priority = front_thread-&gt;priority;
        }
    }

    cur-&gt;priority = max_priority;

     // 3. [선점 로직]
     // 만약 우선순위가 낮아졌다면, ready_list의 1등과 비교하여 양보
     if (!list_empty(&amp;ready_list)) 
     {
          struct thread *front_thread = list_entry(list_begin(&amp;ready_list), struct thread, elem);

          if (cur-&gt;priority &lt; front_thread-&gt;priority) {
               thread_yield();
          }
     }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Project1:  Threads - Priority Scheduling]]></title>
            <link>https://velog.io/@minho-git/Project1-Threads-Priority-Scheduling</link>
            <guid>https://velog.io/@minho-git/Project1-Threads-Priority-Scheduling</guid>
            <pubDate>Wed, 12 Nov 2025 06:11:09 GMT</pubDate>
            <description><![CDATA[<h2 id="⏰-priority-scheduling-구현">⏰ Priority Scheduling 구현</h2>
<h3 id="1-목표">1. 목표</h3>
<p>선점형 우선순위 스케줄링(Preemptive Priority Scheduling)을 구현합니다.</p>
<hr>
<h3 id="2-핵심-아이디어">2. 핵심 아이디어</h3>
<p>우선순위 스케줄링의 목표는 <strong>&quot;가장 높은 우선순위의 스레드가 항상 즉시 실행되도록 보장&quot;</strong>하는 것입니다. 이를 위해 두 가지 핵심 전략을 사용합니다.</p>
<h4 id="1-ready-list의-정렬-자료구조">1. Ready List의 정렬 (자료구조)</h4>
<ul>
<li><code>ready_list</code>를 단순한 큐(FIFO)가 아닌 <strong>우선순위 큐</strong>로 관리합니다.</li>
<li>스레드를 <code>ready_list</code>에 추가할 때마다(<code>unblock</code> 또는 <code>yield</code>) 항상 우선순위가 높은 순서대로 <strong>정렬하여 삽입</strong>합니다.</li>
<li><strong>결과:</strong> <code>ready_list</code>의 맨 앞(<code>list_begin</code>)에는 <strong>&quot;대기 중인 스레드 중 가장 우선순위가 높은 스레드&quot;</strong>가 항상 위치하게 됩니다.</li>
</ul>
<h4 id="2-즉각적인-선점-스케줄링-로직">2. 즉각적인 선점 (스케줄링 로직)</h4>
<p>현재 실행 중인 스레드보다 더 높은 우선순위의 스레드가 나타나면, 즉시 CPU를 빼앗아(선점)와야 합니다. 이 검사는 2가지 시점에 필요합니다.</p>
<ul>
<li><strong>Case 1: (새로운 경쟁자 등장)</strong><ul>
<li><code>timer_sleep</code>이나 <code>lock</code> 대기에서 깨어난 스레드가 <code>ready_list</code>에 삽입될 때,</li>
<li>이 스레드의 우선순위가 <strong>&#39;현재 실행 중인 스레드&#39;</strong>보다 높다면, 즉시 CPU를 선점합니다.</li>
</ul>
</li>
<li><strong>Case 2: (현재 스레드가 약해짐)</strong><ul>
<li><strong>&#39;현재 실행 중인 스레드&#39;</strong>가 스스로의 우선순위를 낮출 때(<code>thread_set_priority</code>),</li>
<li><code>ready_list</code>의 맨 앞(대기 중인 1등)보다 우선순위가 낮아진다면, 즉시 CPU를 양보(선점)당합니다.</li>
</ul>
</li>
</ul>
<hr>
<h3 id="3-구현-과정">3. 구현 과정</h3>
<h4 id="1-ready_list-정렬-삽입-in-thread_unblock">1. <code>ready_list</code> 정렬 삽입 (in <code>thread_unblock</code>)</h4>
<p><code>BLOCKED</code> 상태의 스레드가 <code>READY</code> 상태가 될 때(e.g., <code>lock_release</code>, <code>timer_sleep</code> 종료), <code>ready_list</code>의 맨 뒤에 추가(<code>list_push_back</code>)하는 대신, 우선순위에 맞게 정렬 삽입(<code>list_insert_ordered</code>)하도록 수정합니다.</p>
<p><strong>비교 함수 (priority_less):</strong></p>
<pre><code class="language-c">/* &#39;b&#39;보다 &#39;a&#39;의 우선순위가 높으면 true(1)를 반환하여 리스트 앞에 위치시킴 */
bool 
priority_less (const struct list_elem *a,
               const struct list_elem *b, void *aux UNUSED) {
  struct thread *t_a = list_entry (a, struct thread, elem);
  struct thread *t_b = list_entry (b, struct thread, elem);

  // t_a의 우선순위가 더 크면 &#39;true&#39;를 반환
  return t_a-&gt;priority &gt; t_b-&gt;priority;
}</code></pre>
<p><strong>thread_unblock 수정:</strong></p>
<pre><code class="language-c">// thread.c
void
thread_unblock (struct thread *t) {
    enum intr_level old_level;

    ASSERT (is_thread (t));

    old_level = intr_disable ();
    ASSERT (t-&gt;status == THREAD_BLOCKED);

    // list_push_back 대신 정렬 삽입 함수 사용
    list_insert_ordered(&amp;ready_list, &amp;t-&gt;elem, priority_less, NULL);

    t-&gt;status = THREAD_READY;

    /* * [선점 로직 1]
     * 새로 READY가 된 스레드(t)의 우선순위가
     * 현재 실행 중인 스레드보다 높다면 CPU를 양보(선점)해야 함.
     */
    if (t-&gt;priority &gt; thread_current()-&gt;priority) {

        // 현재 인터럽트 컨텍스트(e.g., timer_interrupt)에서 호출된 경우
        if (intr_context()) {
            // 인터럽트가 끝나자마자 yield가 호출되도록 플래그만 설정
            intr_yield_on_return();
        } 
        // 일반 컨텍스트(e.g., lock_release)에서 호출된 경우
        else {
            // 즉시 yield 호출
            thread_yield();
        }
    }

    intr_set_level (old_level);
}</code></pre>
<h4 id="2-ready_list-정렬-삽입-in-thread_yield">2. ready_list 정렬 삽입 (in thread_yield)</h4>
<p>스레드가 스스로 CPU를 양보(<code>thread_yield</code>)할 때도 <code>ready_list</code>에 정렬 삽입되도록 수정합니다.</p>
<p><strong><code>thread_yield</code> 수정:</strong></p>
<pre><code class="language-c">// thread.c
void 
thread_yield(void) {
    struct thread *curr = thread_current();
    enum intr_level old_level;

    ASSERT (!intr_context ());

    old_level = intr_disable();
    if (curr != idle_thread) {
        // list_push_back 대신 정렬 삽입 함수 사용
        list_insert_ordered(&amp;ready_list, &amp;curr-&gt;elem, priority_less, NULL);
    }

    /* * [버그 수정]
     * do_schedule()은 &#39;if&#39; 블록 밖에 있어야 함.
     * idle_thread가 yield 할 때도 스케줄링이 일어나야
     * ready_list에 있는 새 스레드가 실행될 수 있음.
     */
    do_schedule(THREAD_READY); 

    intr_set_level(old_level);
}</code></pre>
<h4 id="3-선점-로직-in-thread_set_priority">3. 선점 로직 (in thread_set_priority)</h4>
<p>현재 실행 중인 스레드가 자신의 우선순위(<code>thread_set_priority</code>)를 변경할 때 선점이 발생할 수 있습니다.</p>
<p><strong><code>thread_set_priority</code> 수정:</strong></p>
<pre><code class="language-c">// thread.c
void 
thread_set_priority(int new_priority) {

    thread_current()-&gt;priority = new_priority;

    /*
     * [선점 로직 2]
     * 만약 ready_list가 비어있지 않다면,
     * 우선순위를 낮춘 현재 스레드가
     * ready_list의 대기 중인 최고 우선순위 스레드보다
     * 우선순위가 낮아졌는지 확인해야 함.
     */
    if (!list_empty(&amp;ready_list))
    {
        // ready_list의 맨 앞(최고 우선순위 스레드)을 가져옴
        struct thread *front_thread = list_entry(list_begin(&amp;ready_list), struct thread, elem);

        // 만약 내(current)가 맨 앞의 스레드보다 우선순위가 낮다면
        if (thread_current()-&gt;priority &lt; front_thread-&gt;priority) {
            // 즉시 CPU를 양보
            thread_yield();
        }
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Project1:  Threads - Alarm Clock ]]></title>
            <link>https://velog.io/@minho-git/Project1-Threads-Alarm-Clock</link>
            <guid>https://velog.io/@minho-git/Project1-Threads-Alarm-Clock</guid>
            <pubDate>Wed, 12 Nov 2025 06:00:53 GMT</pubDate>
            <description><![CDATA[<h2 id="⏰-alarm-clock-구현">⏰ Alarm Clock 구현:</h2>
<h3 id="1-목표">1. 목표</h3>
<p><code>timer_sleep()</code> 함수의 기존 <strong>Busy-Waiting(적극적 대기)</strong> 방식을 <strong>Blocking(수동적 대기)</strong> 방식으로 변경하여 CPU 효율성을 향상시킵니다.</p>
<h3 id="2-핵심-아이디어-busy-waiting-vs-blocking">2. 핵심 아이디어: Busy-Waiting vs. Blocking</h3>
<p>기존 <code>timer_sleep()</code>은 스레드가 잠들지 않고, <code>while</code> 루프를 계속 돌며 시간이 다 됐는지 검사했습니다.</p>
<ul>
<li><p><strong>기존 방식 (Busy-Waiting):</strong></p>
<ul>
<li>스레드는 <code>READY</code> 상태를 유지합니다.</li>
<li>시간이 안 됐으면 <code>thread_yield()</code>를 호출하여 &#39;Ready List&#39;의 맨 뒤로 갑니다.</li>
<li>CPU를 받으면 다시 <code>while</code> 루프에서 시간을 검사합니다.</li>
<li>이 과정이 반복되며 <strong>CPU가 불필요하게 낭비</strong>됩니다.</li>
</ul>
</li>
<li><p><strong>개선 방식 (Blocking):</strong></p>
<ul>
<li>스레드의 상태를 <code>BLOCKED</code>로 변경합니다.</li>
<li>&#39;Sleep List&#39;에 &quot;깨어날 시간(<code>awake_tick</code>)&quot;과 함께 스레드를 삽입합니다. (이때 리스트는 <code>awake_tick</code> 기준으로 정렬됩니다.)</li>
<li>스레드는 스케줄링 대상에서 제외되어 <strong>CPU를 전혀 사용하지 않습니다.</strong></li>
<li>이후 <strong>타이머 인터럽트</strong>가 발생할 때마다 &#39;Sleep List&#39;를 검사하여, 시간이 다 된 스레드를 &#39;Ready List&#39;로 옮겨 깨웁니다.</li>
</ul>
</li>
</ul>
<h4 id="📊-방식-비교-요약">📊 방식 비교 요약</h4>
<table>
<thead>
<tr>
<th align="left">특징</th>
<th align="left">1. <code>thread_yield()</code> 방식</th>
<th align="left">2. <code>thread_sleep()</code> 방식</th>
</tr>
</thead>
<tbody><tr>
<td align="left"><strong>대기 방식</strong></td>
<td align="left">적극적 대기 (Busy-Waiting)</td>
<td align="left">수동적 대기 (Blocking / Passive-Waiting)</td>
</tr>
<tr>
<td align="left"><strong>스레드 상태</strong></td>
<td align="left"><code>READY</code> (준비 큐에 계속 남아있음)</td>
<td align="left"><code>BLOCKED</code> (준비 큐에서 제거됨)</td>
</tr>
<tr>
<td align="left"><strong>CPU 사용</strong></td>
<td align="left"><strong>낭비가 심함</strong> (계속 스케줄링됨)</td>
<td align="left"><strong>효율적</strong> (대기 중 CPU 사용 안 함)</td>
</tr>
<tr>
<td align="left"><strong>용도</strong></td>
<td align="left">(비효율적)</td>
<td align="left">Pintos의 알람 시계 (정확한 구현)</td>
</tr>
</tbody></table>
<hr>
<h3 id="3-구현-과정-및-주요-코드">3. 구현 과정 및 주요 코드</h3>
<h4 id="1-sleep-list-선언-및-초기화">1. &#39;Sleep List&#39; 선언 및 초기화</h4>
<p><code>BLOCKED</code> 상태의 스레드 중 &#39;시간 대기&#39; 중인 스레드만 따로 관리할 <code>sleep_list</code>를 선언하고 초기화합니다.</p>
<pre><code class="language-c">// thread.c (전역 변수)
static struct list sleep_list;

// thread.c - thread_init() 내부
void
thread_init (void) {
    ...
    list_init (&amp;sleep_list);
    ...
}</code></pre>
<h4 id="2-thread-구조체에-awake_tick-필드-추가">2. &#39;thread&#39; 구조체에 &#39;awake_tick&#39; 필드 추가</h4>
<p>스레드가 깨어나야 할 절대적인 시각(<code>awake_tick</code>)을 저장할 필드를 추가하고 초기화합니다.</p>
<pre><code class="language-c">// thread.h - struct thread 내부
struct thread {
    ...
    int64_t awake_tick;         /* 깨어날 시간 (ticks) */
    ...
};

// thread.c - init_thread() 내부
static void
init_thread (struct thread *t, const char *name, int priority) {
    ...
    t-&gt;awake_tick = 0;
    ...
}</code></pre>
<h4 id="3-timer_sleep-수정">3. &#39;timer_sleep()&#39; 수정</h4>
<p>기존 <code>thread_yield()</code> 루프를 제거하고, 새로 만들 <code>thread_sleep()</code>함수를 호출하도록 변경합니다.</p>
<p><strong>기존 로직:</strong></p>
<pre><code class="language-c">void timer_sleep (int64_t ticks) {
    int64_t start = timer_ticks ();

    ASSERT (intr_get_level () == INTR_ON);
    while (timer_elapsed (start) &lt; ticks)
        thread_yield ();
}</code></pre>
<p><strong>수정된 로직:</strong></p>
<pre><code class="language-c">// timer.c
void 
timer_sleep (int64_t ticks) {
    // 자야 할 시간이 0 이하면 즉시 리턴
    if (ticks &lt;= 0) {
        return;
    }

    // 현재 시각 + 자야 할 기간 = 깨어날 시각
    int64_t awake_tick = timer_ticks () + ticks;

    // 스레드를 &#39;깨어날 시각&#39;까지 재운다.
    thread_sleep(awake_tick);
}</code></pre>
<h4 id="4-thread_sleep-및-정렬-함수-구현">4. thread_sleep() 및 정렬 함수 구현</h4>
<p>스레드를 <code>BLOCKED</code> 상태로 만들고 <code>sleep_list</code>에 삽입하는 핵심 함수와, 리스트 정렬에 필요한 비교 함수를 구현합니다.</p>
<pre><code class="language-c">// thread.c

/* awake_tick이 더 작은 스레드가 리스트의 앞에 오도록 비교 */
bool 
thread_awake_less(const struct list_elem *a, const struct list_elem *b, void *aux UNUSED) {
    struct thread *t_a = list_entry(a, struct thread, elem);
    struct thread *t_b = list_entry(b, struct thread, elem);

    return t_a-&gt;awake_tick &lt; t_b-&gt;awake_tick;
}

/* 현재 스레드를 &#39;awake_tick&#39;까지 재운다. */
void 
thread_sleep(int64_t awake_tick) {
    struct thread *cur = thread_current();

    // Race Condition 방지를 위해 인터럽트 비활성화
    enum intr_level old_level = intr_disable();

    // idle 스레드는 잠들면 안 됨
    ASSERT(cur != idle_thread);

    // 1. 스레드에 깨어날 시간 저장
    cur-&gt;awake_tick = awake_tick;

    // 2. &#39;Sleep List&#39;에 정렬하여 삽입 (가장 빨리 깰 스레드가 맨 앞)
    list_insert_ordered(&amp;sleep_list, &amp;cur-&gt;elem, thread_awake_less, NULL);

    // 3. 스레드를 BLOCKED 상태로 변경 (잠들기)
    thread_block();

    // 4. (나중에 깨어난 후) 인터럽트 상태 복원
    intr_set_level(old_level);
}</code></pre>
<h4 id="5-timer_interrupt-수정">5. timer_interrupt() 수정</h4>
<p>매 틱마다 <code>ticks</code>를 증가시키고, <code>thread_wake_up()</code> 함수를 호출하여 <code>sleep_list</code>를 검사하도록 수정합니다.</p>
<p><strong>기존 로직:</strong></p>
<pre><code class="language-c">/* 타이머 인터럽트 핸들러 */
static void timer_interrupt (struct intr_frame *args UNUSED) {
    ticks++;
    thread_tick ();
}</code></pre>
<p><strong>수정된 로직:</strong></p>
<pre><code class="language-c">/* 타이머 인터럽트 핸들러 */
static void timer_interrupt (struct intr_frame *args UNUSED) {
    ticks++; // 전역 틱 증가

    // &#39;Sleep List&#39;를 검사하여 깨울 시간이 된 스레드를 깨움
    thread_wake_up(ticks); 

    thread_tick (); // 스케줄링 관련 처리
}</code></pre>
<pre><code class="language-c">void thread_wake_up(int64_t current_ticks)
{
    while (!list_empty(&amp;sleep_list)) {

        struct list_elem *e = list_begin(&amp;sleep_list);
        struct thread *t = list_entry(e, struct thread, elem);

        if (t-&gt;awake_tick &gt; current_ticks)
        {
            break;
        }

        list_remove(e);
        thread_unblock(t);
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[레드 블랙 트리 - 삭제]]></title>
            <link>https://velog.io/@minho-git/%EB%A0%88%EB%93%9C-%EB%B8%94%EB%9E%99-%ED%8A%B8%EB%A6%AC-%EC%82%AD%EC%A0%9C</link>
            <guid>https://velog.io/@minho-git/%EB%A0%88%EB%93%9C-%EB%B8%94%EB%9E%99-%ED%8A%B8%EB%A6%AC-%EC%82%AD%EC%A0%9C</guid>
            <pubDate>Fri, 17 Oct 2025 02:08:04 GMT</pubDate>
            <description><![CDATA[<h2 id="🏁-전체적인-흐름-일단-지우고-나중에-고친다">🏁 전체적인 흐름: &quot;일단 지우고, 나중에 고친다!&quot;</h2>
<p>레드-블랙 트리의 삭제는 복잡해 보이지만, 핵심 전략은 아주 간단합니다.</p>
<blockquote>
<ol>
<li>먼저 <strong>일반적인 이진 탐색 트리(BST)의 방식</strong>으로 노드를 삭제한다.</li>
<li>삭제로 인해 <strong>레드-블랙 트리의 속성이 깨졌는지 확인</strong>한다.</li>
<li>속성이 위반되었다면, <strong>회전(Rotation)과 색상 변경(Recoloring)으로 재조정(Fix-up)</strong>한다.</li>
<li>모든 속성을 다시 만족하는 유효한 레드-블랙 트리로 만든다.</li>
</ol>
</blockquote>
<p>결국 &quot;BST처럼 일단 지우고, RBT 규칙에 맞게 사후 처리한다&quot;가 전부입니다. 여기서 가장 중요한 첫 단추는 바로 <strong>&#39;어떤 색의 노드가 실제로 삭제되었는가?&#39;</strong>를 파악하는 것입니다.</p>
<h2 id="🔑-핵심-분기점-삭제되는-색은-무엇인가">🔑 핵심 분기점: &#39;삭제되는 색&#39;은 무엇인가?</h2>
<p>삭제 후 재조정이 필요한지 아닌지를 결정하는 유일한 기준은 <strong>&#39;실제로 트리에서 제거되는 노드의 색&#39;</strong> 입니다. 이 &#39;삭제되는 색&#39;을 판별하는 기준은 삭제할 노드의 자식 수에 따라 나뉩니다.</p>
<h4 id="1-삭제할-노드의-자식이-0개-또는-1개일-때">1. 삭제할 노드의 자식이 0개 또는 1개일 때</h4>
<p>이 경우는 간단합니다. <strong>삭제되는 색 = 삭제되는 노드 본인의 색</strong>입니다.</p>
<pre><code class="language-c">             35(B)
         /           \
       20(R)           50(R)
      /    \         /      \
    10(B)  30(B)    40(B)    80(B)
           /         /
         25(R)     37(R)

// 25(R) 삭제 -&gt; RED 삭제
// 80(B) 삭제 -&gt; BLACK 삭제
// 40(B) 삭제 -&gt; BLACK 삭제</code></pre>
<h4 id="2-삭제할-노드의-자식이-2개일-때">2. 삭제할 노드의 자식이 2개일 때</h4>
<p>BST 삭제 규칙에 따라, 삭제할 노드의 <strong>직후 원소(In-order Successor)</strong>를 찾아 값을 복사한 뒤, 그 Successor 노드를 대신 삭제합니다. 따라서 이 경우 삭제되는 색 = Successor 노드의 색이 됩니다.</p>
<pre><code>             35(B)
         /           \
       20(R)           50(R)
      /    \         /      \
    10(B)  30(B)    40(B)    80(B)
           /         /
         25(R)     37(R)

// 20(R) 삭제 -&gt; Successor는 25(R) -&gt; RED 삭제
// (20번 노드의 &#39;값&#39;만 25로 바뀌고, 실제로는 25(R) 노드가 제거됨)

// 35(B) 삭제 -&gt; Successor는 37(R) -&gt; RED 삭제
// 50(R) 삭제 -&gt; Successor는 80(B) -&gt; BLACK 삭제</code></pre><hr>
<h2 id="✅-쉬운-길-red가-삭제될-때">✅ 쉬운 길: RED가 삭제될 때</h2>
<p>만약 &#39;삭제되는 색&#39;이 <strong>RED</strong>라면, 우리는 운이 좋은 겁니다! <strong>아무런 추가 조치 없이 삭제 작업이 그대로 종료</strong>됩니다. RED 노드는 Black-Height에 영향을 주지 않기 때문에, 어떤 속성도 위반하지 않습니다.</p>
<ul>
<li><strong>#1 (색상)</strong>: 당연히 만족.</li>
<li><strong>#2 (루트)</strong>: 루트가 아닌 RED를 지웠으니 불변.</li>
<li><strong>#3 (NIL)</strong>: NIL 노드는 불변.</li>
<li><strong>#4 (연속 RED)</strong>: RED를 제거했으므로 연속될 위험 없음.</li>
<li><strong>#5 (Black-Height)</strong>: 경로의 BLACK 노드 수에 영향을 주지 않음.</li>
</ul>
<hr>
<h2 id="⚫️-어려운-길-black이-삭제될-때">⚫️ 어려운 길: BLACK이 삭제될 때</h2>
<p>문제는 지금부터입니다. 만약 &#39;삭제되는 색&#39;이 <strong>BLACK</strong>이라면, 상황이 복잡해집니다. BLACK 노드는 경로의 Black-Height를 지탱하는 기둥과 같아서, 이 기둥이 빠지면 여러 속성이 연쇄적으로 무너질 수 있습니다.</p>
<blockquote>
<p><strong>삭제되는 색이 BLACK이라면 #2(루트), #4(연속 RED), #5(Black-Height) 속성을 위반할 수 있습니다.</strong></p>
</blockquote>
<p>#2번을 위반 했을때 루트 노드를 black으로 바꾸면 된다.</p>
<p>특히 <strong>#5 속성(Black-Height)</strong>은 거의 항상 위반된다고 볼 수 있습니다. 삭제된 BLACK 노드를 지나던 경로는 다른 경로보다 Black-Height가 1만큼 낮아지기 때문이죠.</p>
<p>RBT 삭제의 핵심은 바로 이 Black-Height 불균형 문제를 어떻게 해결하느냐에 있습니다.</p>
<hr>
<h2 id="✨-마법의-도구-extra-black">✨ 마법의 도구: Extra Black</h2>
<p>개발자들은 이 문제를 해결하기 위해 재미있는 개념을 도입했습니다. 바로 <strong>&#39;Extra Black&#39;</strong> 입니다.</p>
<blockquote>
<p>#5 속성을 다시 만족 시키기 위해서 삭제된 BLACK 노드의 자리를 대체한 노드에게 임시로 &#39;추가적인 Black 속성&#39;을 부여하여 Black-Height의 균형을 일단 맞추는 것입니다.</p>
</blockquote>
<p>경로에서 black 수를 카운트 할 때 extra black은 하나의 black으로 카운트 된다.</p>
<p>삭제되는 색이 Black이고 #5 속성 위반일 때  Extra Black을 부여받은 노드는 두 가지 상태가 될 수 있습니다.</p>
<ol>
<li><strong>Red-and-Black</strong>: 원래 RED였던 노드가 Extra Black을 부여받은 상태.</li>
<li><strong>Doubly Black</strong>: 원래 BLACK이었던 노드(NIL 노드 포함)가 Extra Black을 부여받은 상태.</li>
</ol>
<p>이제 이 두 가지 특수 상태를 어떻게 정상으로 되돌리는지 알아보겠습니다.</p>
<h3 id="1-red-and-black-해결하기-가장-간단한-케이스">1. Red-and-Black 해결하기 (가장 간단한 케이스)</h3>
<p>Red-and-Black은 &quot;나는 원래 RED지만, 없어진 부모의 BLACK 역할까지 임시로 맡고 있어!&quot;라는 의미입니다. 해결책은 놀랍도록 간단합니다.</p>
<blockquote>
<p><strong>Red-and-Black 노드의 색을 그냥 BLACK으로 바꾸면 모든 문제가 해결됩니다.</strong>
<img src="https://velog.velcdn.com/images/minho-git/post/d98486ee-7d90-4da1-93c6-dce9888d5711/image.png" alt=""></p>
</blockquote>
<ul>
<li><strong>예시: 30(B) 삭제</strong><ul>
<li><code>30(B)</code>의 자리는 자식인 <code>25(R)</code>가 대체합니다.</li>
<li>Black-Height가 1 부족하므로 <code>25(R)</code>에게 Extra Black을 부여하여 <code>25(RB)</code> 상태가 됩니다.</li>
<li><strong>해결</strong>: <code>25(RB)</code>의 색을 최종적으로 <code>BLACK</code>으로 변경합니다.</li>
<li><strong>결과</strong>: 부족했던 Black-Height 1이 <code>25</code>의 새로운 <code>BLACK</code> 색으로 완벽하게 채워지며, 다른 어떤 속성도 위반하지 않습니다.</li>
</ul>
</li>
</ul>
<h3 id="2-doubly-black-해결하기-진짜-rbt-삭제">2. Doubly Black 해결하기 (진짜 RBT 삭제)</h3>
<p>Doubly Black은 &quot;나는 원래도 BLACK인데, 추가로 BLACK 역할까지 떠안아서 너무 무거워!&quot;라는 의미입니다. 이 문제를 해결하는 것이 RBT 삭제의 하이라이트입니다.</p>
<p>Doubly Black 문제를 해결하는 전략은 <strong>&quot;문제를 나 혼자 해결하지 않고 주변(형제 노드)의 도움을 받는다&quot;</strong> 입니다.</p>
<p>Doubly Black 노드 <code>x</code>의 <strong>형제(sibling) 노드와 형제 노드 자식들의 색상</strong>에 따라 총 4가지의 복잡한 케이스로 나뉘며, 회전(Rotation)과 색상 변경(Recoloring)을 조합하여 Extra Black을 제거해 나갑니다. 이 과정은 다음과 같은 목표를 가집니다.</p>
<ol>
<li>Extra Black을 부모에게 전파시켜 문제를 위로 떠넘기기.</li>
<li>회전을 통해 Black-Height의 균형을 맞춰 문제를 한 번에 해결하기.</li>
</ol>
<p>Doubly Black의 4가지 케이스를 자세히 다루자.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[레드 블랙 트리  개념 및 노드 삽입]]></title>
            <link>https://velog.io/@minho-git/%EB%A0%88%EB%93%9C-%EB%B8%94%EB%9E%99-%ED%8A%B8%EB%A6%AC</link>
            <guid>https://velog.io/@minho-git/%EB%A0%88%EB%93%9C-%EB%B8%94%EB%9E%99-%ED%8A%B8%EB%A6%AC</guid>
            <pubDate>Thu, 16 Oct 2025 14:03:55 GMT</pubDate>
            <description><![CDATA[<h2 id="🧐-레드-블랙-트리란">🧐 레드-블랙 트리란?</h2>
<p>레드-블랙 트리는 다음 두 가지 핵심 특징을 가진 <strong>자가 균형 이진 탐색 트리(Self-Balancing BST)</strong>입니다.</p>
<ol>
<li><strong>이진 탐색 트리의 속성을 모두 가집니다.</strong> (왼쪽 서브트리는 부모보다 작고, 오른쪽 서브트리는 부모보다 큼)</li>
<li><strong>스스로 균형을 맞추어</strong> 트리의 높이를 가능한 낮게 유지합니다.</li>
</ol>
<pre><code>        50
       /
     40             -&gt; 20을 찾기 위해서는 n의 갯수만큼 반복해야한다.
    /
  20</code></pre><p>일반적인 BST는 데이터가 정렬된 순서로 들어올 경우, 한쪽으로 치우쳐진 편향 트리(Skewed Tree)가 되어 탐색, 삽입, 삭제의 시간 복잡도가 최악의 경우 <strong>$O(n)$</strong>이 될 수 있습니다. 레드-블랙 트리는 특정 규칙에 따라 노드의 색(Red/Black)을 지정하고, 삽입/삭제 시 트리의 구조를 재조정하여 항상 <strong>$O(log n)$</strong>의 시간 복잡도를 보장합니다.</p>
<hr>
<h2 id="📜-레드-블랙-트리의-5가지-속성">📜 레드-블랙 트리의 5가지 속성</h2>
<p>레드-블랙 트리가 되기 위해서는 아래의 5가지 속성을 반드시 만족해야 합니다.</p>
<ol>
<li><p>모든 노드는 <strong>RED</strong> 혹은 <strong>BLACK</strong> 색상을 가집니다.</p>
</li>
<li><p><strong>루트(Root) 노드는 반드시 BLACK</strong>입니다.</p>
</li>
<li><p>모든 <strong>리프(Leaf) 노드는 BLACK</strong>입니다.</p>
<blockquote>
<p><strong>잠깐, 여기서 리프(Leaf) 노드란?</strong>
레드-블랙 트리에서는 자식 노드가 없는 경우, <code>nil</code> 노드라는 가상의 노드가 있다고 간주합니다. 이 <code>nil</code> 노드가 바로 리프 노드이며, 항상 BLACK 색상입니다. 데이터가 있는 실제 노드와 동등하게 취급하여 규칙을 적용하는 데 사용됩니다.</p>
</blockquote>
</li>
<li><p><strong>RED</strong> 노드의 자식은 <strong>반드시 BLACK</strong>입니다. (즉, RED 노드가 연속으로 두 개 나타날 수 없습니다.)</p>
</li>
<li><p>임의의 한 노드에서부터 그 노드의 자손인 모든 리프 노드까지 가는 경로에 있는 <strong>BLACK 노드의 수는 모두 동일</strong>합니다. (이때, 자기 자신은 카운트에서 제외합니다.)</p>
<blockquote>
<p><strong>새로운 개념: Black-Height</strong>
속성 5 덕분에 &#39;노드 X의 Black-Height&#39;라는 개념이 성립됩니다. 이는 노드 X에서부터 리프 노드까지의 경로에 있는 BLACK 노드의 수를 의미합니다. 어떤 경로로 가든 BLACK 노드의 수가 같기 때문에 유일한 값으로 정의될 수 있습니다.</p>
</blockquote>
</li>
</ol>
<hr>
<h2 id="✨-삽입insertion-연산의-원리">✨ 삽입(Insertion) 연산의 원리</h2>
<p>레드-블랙 트리는 어떻게 균형을 잡을까요? 바로 삽입/삭제 시 위반될 수 있는 속성 4번과 5번을 해결하는 과정에서 자연스럽게 균형이 맞춰집니다.</p>
<h3 id="🧐-왜-새로운-노드는-항상-red일까">🧐 왜 새로운 노드는 항상 RED일까?</h3>
<p>새로운 노드를 삽입할 때는 <strong>항상 RED 색상으로 삽입</strong>합니다. 그 이유는 가장 까다로운 <strong>속성 5번(Black-Height 동일)을 깨지 않기 위해서</strong>입니다.</p>
<p>기존에 속성 5를 만족하는 트리에서 어느 위치에 RED 노드를 추가하더라도, 특정 경로에 BLACK 노드의 수가 추가되는 것이 아니므로 Black-Height는 변하지 않습니다. 만약 BLACK으로 삽입한다면 해당 경로의 Black-Height가 1 증가하여 속성 5를 위반하게 되고, 이를 해결하는 과정은 훨씬 더 복잡해집니다.</p>
<hr>
<h3 id="🌳-초기-상태-유효한-레드-블랙-트리">🌳 초기 상태: 유효한 레드-블랙 트리</h3>
<p>먼저, 모든 속성을 만족하는 간단한 레드-블랙 트리가 있다고 가정해 봅시다. 이 트리에서 루트(10)로부터 모든 리프(nil) 노드까지의 경로에 있는 <strong>블랙 노드의 수는 1개</strong>로 동일합니다 (Black-Height = 1).</p>
<pre><code>      10(B)
     /     \
   5(B)     20(B)</code></pre><ul>
<li><code>10 -&gt; 5 -&gt; nil</code> 경로: 블랙 노드는 <code>5</code> + <code>nil</code> (2개)</li>
<li><code>10 -&gt; 20 -&gt; nil</code> 경로: 블랙 노드는 <code>20</code> + <code>nil</code> (2개)</li>
<li><strong>결과: 속성 5를 만족합니다.</strong></li>
</ul>
<hr>
<h3 id="✅-새로운-노드-30을-red로-삽입">✅: 새로운 노드 &#39;30&#39;을 RED로 삽입</h3>
<p>여기에 새로운 노드 <code>30</code>을 <strong>RED</strong>로 삽입해 보겠습니다. <code>30</code>은 <code>20</code>의 오른쪽 자식으로 들어갑니다.</p>
<pre><code>      10(B)
     /     \
   5(B)     20(B)
              \
              30(R)  &lt;-- RED로 삽입</code></pre><p>삽입 후 Black-Height를 다시 계산해 볼까요?</p>
<ul>
<li><code>10 -&gt; 5 -&gt; nil</code> 경로: 블랙 노드는 <code>5</code>, <code>nil</code> (2개)</li>
<li><code>10 -&gt; 20 -&gt; (left nil)</code> 경로: 블랙 노드는 <code>20</code>, <code>nil</code>(2개)</li>
<li><code>10 -&gt; 20 -&gt; 30 -&gt; nil</code> 경로: 블랙 노드는 <code>20</code>, <code>nil</code>(2개)</li>
</ul>
<p><strong>결론:</strong> 새로운 RED 노드가 추가되었지만, 어떤 경로든 <strong>Black-Height는 여전히 2로 동일</strong>합니다. 가장 중요한 <strong>속성 5가 깨지지 않았습니다.</strong> (이 경우 다른 속성 위반도 없어서 바로 유효한 트리가 되었습니다.)</p>
<hr>
<h3 id="❌-새로운-노드-30을-black으로-삽입-잘못된-방법">❌: 새로운 노드 &#39;30&#39;을 BLACK으로 삽입 (잘못된 방법)</h3>
<p>만약 같은 위치에 <code>30</code>을 <strong>BLACK</strong>으로 삽입한다면 어떻게 될까요?</p>
<pre><code>      10(B)
     /     \
   5(B)     20(B)
              \
              30(B)  &lt;-- BLACK으로 삽입</code></pre><ul>
<li><code>10 -&gt; 5 -&gt; nil</code> 경로: 블랙 노드는 <code>5</code>,<code>nil</code>(2개)</li>
<li><code>10 -&gt; 20 -&gt; (left nil)</code> 경로: 블랙 노드는 <code>20</code>, <code>nil</code> (2개)</li>
<li><code>10 -&gt; 20 -&gt; 30 -&gt; nil</code> 경로: 블랙 노드는 <code>20</code>, <code>30</code>, <code>nil</code> <strong>(3개)</strong></li>
</ul>
<hr>
<h3 id="삽입-과정">삽입 과정</h3>
<ol>
<li>일반적인 BST처럼 노드를 삽입할 위치를 찾습니다.</li>
<li>해당 위치에 <strong>RED</strong> 색상으로 새로운 노드를 삽입합니다.</li>
<li>삽입 후 레드-블랙 트리의 속성을 위반하는지 확인합니다. (주로 속성 2 또는 4)</li>
<li>속성을 위반했다면, <strong>재조정(Restructuring)</strong>과 <strong>색상 변경(Recoloring)</strong>을 통해 해결합니다.</li>
</ol>
<h2 id="🛠️-삽입-후-재조정-3가지-케이스">🛠️ 삽입 후 재조정: 3가지 케이스</h2>
<p>새로운 RED 노드(N)를 삽입했을 때, 부모 노드(P)가 RED라면 <strong>속성 4(RED는 연속될 수 없다)</strong>를 위반하게 됩니다. 이 &quot;Double Red&quot; 문제를 해결하는 방법은 <strong>삼촌 노드(U, 부모의 형제 노드)의 색상</strong>에 따라 3가지 케이스로 나뉩니다.</p>
<p>(N: 새로 삽입된 노드, P: 부모, G: 조부모, U: 삼촌)</p>
<h3 id="case-1-삼촌u이-red인-경우">CASE 1: 삼촌(U)이 RED인 경우</h3>
<p>가장 간단한 케이스입니다. Double Red가 발생했지만, 넘겨줄 반대편(삼촌)도 RED인 상황입니다. 이때는 <strong>색상 변경(Recoloring)</strong>으로 해결합니다.</p>
<ul>
<li><p><strong>해결 방법</strong>:</p>
<ol>
<li>부모(P)와 삼촌(U)을 <strong>BLACK</strong>으로 변경합니다.</li>
<li>조부모(G)를 <strong>RED</strong>로 변경합니다.</li>
<li>조부모(G)를 새로운 노드(N)로 간주하고, 위반 사항이 있는지 다시 확인합니다. (만약 조부모(G)의 부모도 RED라면 또다시 Double Red 문제가 발생하므로 재귀적으로 해결)</li>
</ol>
</li>
<li><p><strong>예시</strong>: 아래와 같은 트리에 <code>30</code>을 삽입해 봅시다.</p>
<pre><code>        20(B)
       /     \
     10(R)   50(R)</code></pre><p><code>50</code>의 왼쪽에 <code>30(R)</code>을 삽입하면 <code>P(50)</code>와 <code>N(30)</code>이 모두 RED가 됩니다. 이때 <code>U(10)</code>도 RED입니다.</p>
<pre><code>        20(B)
       /     \
     10(R)   50(R)  &lt;-- P
             /
           30(R)    &lt;-- N</code></pre><ol>
<li><code>P(50)</code>와 <code>U(10)</code>를 BLACK으로, <code>G(20)</code>를 RED로 변경합니다.</li>
<li><code>G(20)</code>가 RED가 되었지만 루트이므로 <strong>속성 2(루트는 BLACK)</strong>를 위반합니다.</li>
<li>따라서 <code>20</code>을 다시 BLACK으로 변경하여 모든 속성을 만족시킵니다.</li>
</ol>
<pre><code>        20(B)
       /     \
     10(B)   50(B)
             /
           30(R)</code></pre><h3 id="case-2-삼촌u이-black이고-경로가-직선-형태line인-경우">CASE 2: 삼촌(U)이 BLACK이고, 경로가 직선 형태(Line)인 경우</h3>
</li>
</ul>
<p>삽입된 노드(N) - 부모(P) - 조부모(G)의 경로가 <code>//</code> 또는 <code>\\</code> 모양의 직선입니다. <strong>색상 변경</strong>과 <strong>회전</strong>을 통해 균형을 맞춥니다.</p>
<ul>
<li><p><strong>해결 방법</strong>:</p>
<ol>
<li>부모(P)를 <strong>BLACK</strong>으로, 조부모(G)를 <strong>RED</strong>로 변경합니다.</li>
<li><strong>조부모(G)를 기준</strong>으로 회전합니다. (P가 G의 왼쪽 자식이면 우회전, 오른쪽 자식이면 좌회전)</li>
</ol>
</li>
<li><p><strong>예시</strong>: CASE 2에서 변환된 트리로 계속 진행해 봅시다.</p>
<pre><code>        50(B) &lt;-- G
       /
     40(R) &lt;-- P
    /
  20(R) &lt;-- N (역할이 바뀜)</code></pre><ol>
<li><code>P(40)</code>를 BLACK으로, <code>G(50)</code>를 RED로 변경합니다.</li>
</ol>
<pre><code>        50(R)
       /
     40(B)
    /
  20(R)</code></pre><ol start="2">
<li><code>G(50)</code>를 기준으로 <strong>우회전</strong>합니다.</li>
</ol>
<pre><code>        40(B)
       /     \
     20(R)   50(R)</code></pre></li>
</ul>
<h3 id="case-3-삼촌u이-black이고-경로가-꺾인-형태triangle인-경우">CASE 3: 삼촌(U)이 BLACK이고, 경로가 꺾인 형태(Triangle)인 경우</h3>
<p>삽입된 노드(N) - 부모(P) - 조부모(G)의 경로가 꺾여있는 <code>&lt;</code> 또는 <code>&gt;</code> 모양입니다. 이 경우는 <strong>회전(Rotation)</strong>을 통해 경로를 직선 형태로 만들어 CASE 3으로 전환하여 해결합니다.</p>
<ul>
<li><p><strong>해결 방법</strong>:</p>
<ol>
<li><strong>부모(P)를 기준</strong>으로 회전하여 경로를 직선 형태로 만듭니다. (P가 G의 왼쪽 자식이면 좌회전, 오른쪽 자식이면 우회전)</li>
<li>이제 CASE 3의 상황이 되었으므로, CASE 3의 방법으로 해결합니다.</li>
</ol>
</li>
<li><p><strong>예시</strong>: 아래 트리에 <code>40</code>을 삽입해 봅시다.</p>
<pre><code>        50(B)
       /
     20(R)      &lt;-- G
    /   \
        40(R)   &lt;-- N</code></pre><p><code>N(40)</code>-<code>P(20)</code>-<code>G(50)</code>의 경로가 꺾여있고, <code>U</code>는 <code>nil</code> 노드이므로 BLACK입니다.</p>
<ol>
<li><code>P(20)</code>를 기준으로 <strong>좌회전</strong>합니다.</li>
</ol>
<pre><code>        50(B)
       /
     40(R)      &lt;-- 이제 N이 됨 (CASE 3 형태로 변환)
    /
  20(R)</code></pre><ol start="2">
<li>이제 CASE 3의 형태로 바뀌었으므로 아래 방법으로 해결합니다.</li>
</ol>
</li>
</ul>
<h2 id="결론">결론</h2>
<p>레드-블랙 트리의 삽입 과정은 다소 복잡해 보일 수 있지만, <strong>&#39;삼촌 노드의 색상&#39;</strong>을 기준으로 케이스를 나누고, <strong>색상 변경(Recoloring)</strong>과 <strong>회전(Rotation)</strong>이라는 두 가지 도구를 적절히 사용하여 트리의 균형을 맞춘다는 핵심 원리를 이해하면 충분히 정복할 수 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DP]]></title>
            <link>https://velog.io/@minho-git/DP%EB%8B%A4%EC%9D%B4%EB%82%98%EB%AF%B9-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D</link>
            <guid>https://velog.io/@minho-git/DP%EB%8B%A4%EC%9D%B4%EB%82%98%EB%AF%B9-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D</guid>
            <pubDate>Tue, 30 Sep 2025 02:38:54 GMT</pubDate>
            <description><![CDATA[<p>DP(Dynamic Programming)는 암기 과목이 아니라 <strong>&#39;문제를 쪼개서 생각하는 사고법&#39;</strong>에 가깝습니다. 특정 유형을 외우기보다, 어떤 문제든 DP로 풀 수 있게 만드는 &#39;생각의 틀&#39;을 잡는 것이 중요합니다.</p>
<hr>
<h2 id="dp-정복을-위한-4단계-공부법">DP 정복을 위한 4단계 공부법</h2>
<p>DP를 효과적으로 공부하기 위한 체계적인 접근법입니다.</p>
<h3 id="1단계-핵심-개념-이해하기-💡"><strong>1단계: 핵심 개념 이해하기</strong> 💡</h3>
<p>DP의 두 가지 대표적인 방식, <strong>메모이제이션(Memoization)</strong>과 <strong>타뷸레이션(Tabulation)</strong>의 차이를 이해하는 것부터 시작해라.</p>
<ul>
<li><strong>메모이제이션 (Top-down):</strong> 큰 문제를 재귀 호출로 풀어나가되, 한 번 계산한 결과는 기록해두고 재사용하는 방식입니다. 로직이 직관적이라 처음 접근하기 좋습니다.</li>
<li><strong>타뷸레이션 (Bottom-up):</strong> 가장 작은 문제부터 차례대로 정답을 구해 테이블을 채워나가는 방식입니다. 보통 반복문으로 구현하며 성능이 더 좋은 경우가 많습니다.</li>
</ul>
<p>처음에는 어떤 방식이든 <strong>하나만 정해서</strong> 익숙해지는 것을 추천합니다. 대부분의 코딩 테스트에서는 Bottom-up 방식이 주로 사용됩니다.</p>
<hr>
<h3 id="2단계-생각의-틀-훈련하기-💪"><strong>2단계: &#39;생각의 틀&#39; 훈련하기</strong> 💪</h3>
<p>새로운 DP 문제를 만났을 때, 항상 아래 3가지 질문을 스스로에게 던지는 연습을 하세요.</p>
<ol>
<li><p><strong>DP 상태 정의 (<code>dp[i]</code>는 무엇인가?):</strong> <code>dp</code> 배열의 각 칸이 어떤 의미를 갖는지 명확하게 정의해야 합니다.</p>
<ul>
<li><em>(예: <code>dp[i]</code> = <code>i</code>원을 만드는 동전의 최소 개수)</em></li>
<li><em>(예: <code>dp[i]</code> = <code>i</code>번째 집까지 칠하는 최소 비용)</em></li>
</ul>
</li>
<li><p><strong>점화식 찾기 (<code>dp[i]</code>와 이전 값들의 관계는?):</strong> <code>i</code>번째 문제의 정답(<code>dp[i]</code>)을 그보다 작은 문제들의 정답(<code>dp[i-1]</code>, <code>dp[i-2]</code> 등)을 이용해 어떻게 구할 수 있을지 고민합니다. 여기가 DP의 핵심입니다.</p>
<ul>
<li><em>(예: <code>dp[i]</code>는 <code>dp[i-1]</code>과 <code>dp[i-2]</code>를 더한 값이다.)</em></li>
<li><em>(예: <code>dp[i]</code>는 <code>dp[i-1]</code>, <code>dp[i-2]</code>, <code>dp[i-5]</code> 중 최솟값에 1을 더한 값이다.)</em></li>
</ul>
</li>
<li><p><strong>초기값 설정 (Base Case는 무엇인가?):</strong> 점화식이 시작될 수 있는 가장 작은 문제의 정답(<code>dp[0]</code>, <code>dp[1]</code> 등)을 직접 계산해서 <code>dp</code> 배열에 넣어줍니다. 이 기반이 없으면 점화식이 무너집니다.</p>
</li>
</ol>
<hr>
<h3 id="3단계-유형별-문제-풀기-✅"><strong>3단계: 유형별 문제 풀기</strong> ✅</h3>
<p>무작위로 풀기보다, 클래식한 유형별로 묶어서 풀며 패턴을 익히는 것이 효과적입니다.</p>
<ul>
<li><strong>기본 다지기:</strong> 1차원 DP 배열을 사용하는 가장 기초적인 문제들</li>
<li><strong>응용하기:</strong> 2차원 DP 배열, 배낭(Knapsack), LIS(가장 긴 증가하는 부분 수열) 등 유명한 유형들</li>
</ul>
<hr>
<h3 id="4단계-다른-사람의-코드-보기-👀"><strong>4단계: 다른 사람의 코드 보기</strong> 👀</h3>
<p>문제를 푼 후에 다른 사람들은 어떻게 풀었는지 꼭 확인해라. 같은 문제라도 점화식을 다르게 세우거나, DP 상태를 더 효율적으로 정의하는 방법을 배울 수 있습니다.</p>
<hr>
<h2 id="레벨별-추천-문제-백준">레벨별 추천 문제 (백준)</h2>
<p>아래 문제들을 순서대로 풀어보시면 DP에 대한 감을 잡는 데 큰 도움이 될 겁니다.</p>
<h3 id="lv-1-dp와-친해지기-핵심-원리"><strong>LV 1: DP와 친해지기 (핵심 원리)</strong></h3>
<ul>
<li><a href="https://www.acmicpc.net/problem/1463"><strong>1463번: 1로 만들기</strong></a><ul>
<li>DP의 가장 기본이 되는 문제입니다. &#39;생각의 틀 3단계&#39;를 적용하는 연습을 하기에 완벽합니다.</li>
</ul>
</li>
<li><a href="https://www.acmicpc.net/problem/11726"><strong>11726번: 2×n 타일링</strong></a><ul>
<li>매우 유명한 피보나치 수열의 변형 문제입니다. 간단한 점화식을 세우는 연습을 할 수 있습니다.</li>
</ul>
</li>
<li><a href="https://www.acmicpc.net/problem/9095"><strong>9095번: 1, 2, 3 더하기</strong></a><ul>
<li><code>dp[i]</code>를 정의하고, <code>i-1</code>, <code>i-2</code>, <code>i-3</code>과의 관계를 찾는 연습을 하기에 좋습니다.</li>
</ul>
</li>
<li><a href="https://www.acmicpc.net/problem/2579"><strong>2579번: 계단 오르기</strong></a><ul>
<li>이미 푸셨지만, 지금의 지식으로 다시 한번 점화식을 유도해보세요. 왜 그런 로직이 나왔는지 완벽히 이해하는 것이 중요합니다.</li>
</ul>
</li>
</ul>
<h3 id="lv-2-조금-더-생각하기-1차원-dp-응용"><strong>LV 2: 조금 더 생각하기 (1차원 DP 응용)</strong></h3>
<ul>
<li><a href="https://www.acmicpc.net/problem/11053"><strong>11053번: 가장 긴 증가하는 부분 수열 (LIS)</strong></a><ul>
<li>DP의 대표 유형 중 하나인 LIS의 가장 기초적인 문제입니다.</li>
</ul>
</li>
<li><a href="https://www.acmicpc.net/problem/1912"><strong>1912번: 연속합</strong></a><ul>
<li>이전 값들을 누적하는 방식에 대한 새로운 아이디어를 얻을 수 있습니다.</li>
</ul>
</li>
<li><a href="https://www.acmicpc.net/problem/1149"><strong>1149번: RGB거리</strong></a><ul>
<li><code>dp</code> 배열을 2차원으로 확장(<code>dp[i][색깔]</code>)하는 첫걸음입니다.</li>
</ul>
</li>
</ul>
<h3 id="lv-3-유명-유형-정복하기"><strong>LV 3: 유명 유형 정복하기</strong></h3>
<ul>
<li><a href="https://www.acmicpc.net/problem/12865"><strong>12865번: 평범한 배낭</strong></a><ul>
<li>모든 코딩 테스트의 단골손님, <strong>배낭(Knapsack) 문제</strong>의 가장 표준적인 형태입니다. 이 문제는 풀이법을 거의 외워둘 가치가 있습니다.</li>
</ul>
</li>
<li><a href="https://www.acmicpc.net/problem/9251"><strong>9251번: LCS (최장 공통 부분 수열)</strong></a><ul>
<li>두 문자열을 다루는 대표적인 2차원 DP 문제입니다.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[3.7 프로시저]]></title>
            <link>https://velog.io/@minho-git/3.7-%ED%94%84%EB%A1%9C%EC%8B%9C%EC%A0%80</link>
            <guid>https://velog.io/@minho-git/3.7-%ED%94%84%EB%A1%9C%EC%8B%9C%EC%A0%80</guid>
            <pubDate>Sat, 27 Sep 2025 02:52:49 GMT</pubDate>
            <description><![CDATA[<p>이번 절에서는 어셈블리의 여러 함수 간 상호작용과 함수 관리와 관련된 새로운 명령어를 소개한다. </p>
<h3 id="함수-매개변수">함수 매개변수</h3>
<table>
<thead>
<tr>
<th>매개변수</th>
<th>전달 방식</th>
</tr>
</thead>
<tbody><tr>
<td>매개변수 1</td>
<td><code>%rdi</code></td>
</tr>
<tr>
<td>매개변수 2</td>
<td><code>%rsi</code></td>
</tr>
<tr>
<td>매개변수 3</td>
<td><code>%rdx</code></td>
</tr>
<tr>
<td>매개변수 4</td>
<td><code>%rcx</code></td>
</tr>
<tr>
<td>매개변수 5</td>
<td><code>%r8</code></td>
</tr>
<tr>
<td>매개변수 6</td>
<td><code>%r9</code></td>
</tr>
<tr>
<td>매개변수 7 이상</td>
<td>콜 스택 (Call Stack)</td>
</tr>
</tbody></table>
<p>첫 6개 함수 매개변수는 각 레지스터에 로드되며, 그 후 매개변수는 크기에 따라 32비트 데이터는 4바이트 오프셋, 64비트 데이터는 8바이트 오프셋은 콜 스택에 로드된다.</p>
<p>함수 매개변수가 콜 스택에 전달될 때, 스택은 메모리 정렬을 위해 8바이트 크기의 정해진 &#39;슬롯(slot)&#39; 단위로 공간을 할당합니다. 이 때문에 전달되는 데이터의 실제 크기와 상관없이 모든 매개변수는 이 8바이트 슬롯 하나를 차지하게 됩니다. 즉, 8바이트 크기의 long 타입은 슬롯을 완전히 채우고, 4바이트 int, 2바이트 short, 1바이트 char와 같이 작은 데이터들은 슬롯의 일부만 사용하며 나머지 빈 공간은 의미 없는 값(패딩, Padding)으로 채워집니다.</p>
<hr>
<h3 id="예시-추적">예시 추적</h3>
<p>다음 코드 예시를 한 번 추적해보자.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int assign(void) {
    int y = 40;
    return y;
}

int adder(void) {
    int a;
    return a + 2;
}

int main(void) {
    int x;
    assign();
    x = adder();
    printf(&quot;x is: %d\n&quot;, x);
    return 0;
}</code></pre>
<p>이 코드를 <strong>다음 명령어</strong>로 컴파일하고, 어셈블리어로 변환해보자.</p>
<pre><code># 컴파일
gcc -o prog prog.c

# 어셈블리로 변환
objdump -d</code></pre><p><strong>변환된 어셈블리어</strong></p>
<pre><code>0000000000400526 &lt;assign&gt;:
  400526:       55                      push   %rbp
  400527:       48 89 e5                mov    %rsp,%rbp
  40052a:       c7 45 fc 28 00 00 00    movl   $0x28,-0x4(%rbp)
  400531:       8b 45 fc                mov    -0x4(%rbp),%eax
  400534:       5d                      pop    %rbp
  400535:       c3                      retq

0000000000400536 &lt;adder&gt;:
  400536:       55                      push   %rbp
  400537:       48 89 e5                mov    %rsp,%rbp
  40053a:       8b 45 fc                mov    -0x4(%rbp),%eax
  40053d:       83 c0 02                add    $0x2,%eax
  400540:       5d                      pop    %rbp
  400541:       c3                      retq

0000000000400542 &lt;main&gt;:
  400542:       55                      push   %rbp
  400543:       48 89 e5                mov    %rsp,%rbp
  400546:       48 83 ec 10             sub    $0x10,%rsp
  40054a:       e8 e3 ff ff ff          callq  400526 &lt;assign&gt;
  40054f:       e8 d2 ff ff ff          callq  400536 &lt;adder&gt;
  400554:       89 45 fc                mov    %eax,-0x4(%rbp)
  400557:       8b 45 fc                mov    -0x4(%rbp),%eax
  40055a:       89 c6                   mov    %eax,%esi
  40055c:       bf 04 06 40 00          mov    $0x400604,%edi
  400561:       b8 00 00 00 00          mov    $0x0,%eax
  400566:       e8 95 fe ff ff          callq  400400 &lt;printf@plt&gt;
  40056b:       b8 00 00 00 00          mov    $0x0,%eax
  400570:       c9                      leaveq
  400571:       c3                      retq

</code></pre><p>각 함수는 프로그램에 선언된 이름에 해당하는 심볼릭 라벨로 시작한다. 함수 사벨의 주소는 해당 함수의 첫 번째 명령어이다. </p>
<h3 id="main-추적">main 추적</h3>
<p>아래 그림은 main 실행 직전의 스택을 보여준다.
<img src="https://velog.velcdn.com/images/minho-git/post/24862c9b-9c57-4a1c-8c78-b0a2931613df/image.png" alt="">
%rbp에는 이전 함수의 스택 프레임 주소가 담겨있으며, %rsp는 스택 프레임의 시작주소인 0xd48이 담겨있다. %rip에는 명령어의 시작 주소인 0x542가 담겨있다.</p>
<hr>
<p><img src="https://velog.velcdn.com/images/minho-git/post/691014f7-5d6a-4425-b53f-edd047dc8ef7/image.png" alt="">
첫 번째 명령은 %rbp의 현재 값을 스택에 넣는다. 스택은 낮은 주소 방향으로 커지므로 스택 포인터 %rsp는 0xd40으로 업데이트 된다. %rip는 다음 명령어 주소로 업데이트 된다.</p>
<blockquote>
</blockquote>
<p><strong>push 의 역할</strong></p>
<ol>
<li>스택 포인터(%rsp)를 8만큼 감소시켜 스택에 새로운 공간을 확보합니다. (이것이 &#39;스택 영역 확장&#39;에 해당합니다.)</li>
<li>지정한 레지스터의 값(예: %rbp의 값)을 새로 확보된 공간에 복사합니다.</li>
</ol>
<hr>
<p><img src="https://velog.velcdn.com/images/minho-git/post/f94ea27d-b55f-4dc0-a912-7490c1e9cbc7/image.png" alt="">
다음 mov 명령어는 %rbp의 값이 %rsp와 같아지도록 업데이트한다. 이제 %rbp는 main 함수의 스택 프레임의 시작 위치를 가리킨다. %rip는 순서상 다음 명령으로 이동한다.
0xd48에는 리턴주소가 담긴다.</p>
<hr>
<p><img src="https://velog.velcdn.com/images/minho-git/post/a8a1e801-b911-47a5-bc30-6833bb67fb79/image.png" alt=""></p>
<p>sub 명령은 스택 포인터 주소에서 x10을 뺀다. 이것은 스택이 16바이트 커지게 만든다 -&gt; 스택 한 칸 8바이트, 2칸 위로
%rsp는 0xd30을 가리키게 된다. %rip 값은 다음 명령 주소를 가리킨다.</p>
<hr>
<p><img src="https://velog.velcdn.com/images/minho-git/post/dbf8d718-503c-4319-97a3-be9f1da37904/image.png" alt=""></p>
<p>callq 명령은 %rip의 값, 반환주소를 스택에 넣는다. 반환 주소는 함수의 실행이 끝나고, main으로 돌아왔을때 재개되는 프로그램 주소이다. </p>
<p>다음으로, callq 명령은 assign의 함수의 주소를 %rip에 옮긴다. 따라서 다음 실행은 assign이 실행된다.</p>
<hr>
<p><img src="https://velog.velcdn.com/images/minho-git/post/85782696-9bbd-4d88-a2fa-3131350c49fd/image.png" alt=""></p>
<p>assign 함수의 첫 두 명령은 main과 같다. %rbp에 저장된 main 함수의 스택 프레임을 스택에 저장한다.</p>
<hr>
<p><img src="https://velog.velcdn.com/images/minho-git/post/6a15e585-53e8-4f29-b78f-d138163d1e4e/image.png" alt=""></p>
<p>다음 명령은 스택 프레임을 스택의 맨 위 값으로 업데이트 한다. </p>
<hr>
<p><img src="https://velog.velcdn.com/images/minho-git/post/9d70ce1a-413d-470a-a56d-10954b106a6d/image.png" alt=""></p>
<p>mov 명령어는 %0x28(40)을 스택 주소 -0x4(%rbp)에 넣는다. 이 주소는 프레임 포인터보다 4바이트 더 위에 있다. 여기서 %rsp 값은 바꾸지 않는다.</p>
<hr>
<p><img src="https://velog.velcdn.com/images/minho-git/post/55efd389-5dfb-45b7-a0e5-476774fad791/image.png" alt=""></p>
<p>값 $0x28을 레지스터 %eax에 넣는다. 이 레지스터는 반환값을 가진다. </p>
<hr>
<p><img src="https://velog.velcdn.com/images/minho-git/post/92376a80-ada8-47f6-9ed5-5d92a582cac5/image.png" alt=""></p>
<p>이 시점은 assign 함수의 실행이 거의 완료된다. 다음으로 pop %rbp 명령이 실행된다. 이 명령은 %rbp의 값을 이전 값, 0xd40으로 원복한다. 또한 %rsp 값을 0xd28로 수정한다.(스택 포인터를 8만큼 증가)</p>
<hr>
<p><img src="https://velog.velcdn.com/images/minho-git/post/4e5fd221-1adc-4e6d-878f-f989d0ef5435/image.png" alt=""></p>
<p>assign의 마지막 명령은 retq이다. 이전에 저장했던, 리턴주소를 스택에서 꺼내서 rip를 업데이트하여 다음 명령은 0x55f가 실행되도록한다.</p>
<hr>
<p><img src="https://velog.velcdn.com/images/minho-git/post/c26d4acb-c740-4b1a-af38-ab9fedec405a/image.png" alt=""></p>
<p>main으로 돌아와서 addr 함수를 호출하면 새로운 반환주소 0x554를 스택의 이전 주소로 덮어쓴다. %rip는 addr의 첫 번째 명령(0x536)을 가리킨다.</p>
<hr>
<p><img src="https://velog.velcdn.com/images/minho-git/post/c217128b-b28d-4a2c-901d-f74c60979065/image.png" alt=""></p>
<p>addr 함수의 첫 번째 명령은 호출자의 프레임 포인터(main의 %rbp)를 스택에 저장한다.</p>
<hr>
<p><img src="https://velog.velcdn.com/images/minho-git/post/dcc1b06e-9405-4feb-bf36-7cbd0d9847b8/image.png" alt=""></p>
<p>%rsp의 값(0xd20)으로 %rbp를 업데이트 한다.</p>
<hr>
<h3 id="출처-source"><strong>출처 (Source)</strong></h3>
<p>이 게시물의 내용과 이미지는 Suzanne J. Matthews, Tia Newhall, Kevin C. Webb의 <strong><a href="https.diveintosystems.org/book/">Dive into Systems</a></strong>를 기반으로 하며, <strong><a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License</a></strong>에 따라 사용되었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL - 27일차(2025-09-27)]]></title>
            <link>https://velog.io/@minho-git/TIL-27%EC%9D%BC%EC%B0%A82025-09-27</link>
            <guid>https://velog.io/@minho-git/TIL-27%EC%9D%BC%EC%B0%A82025-09-27</guid>
            <pubDate>Sat, 27 Sep 2025 01:47:54 GMT</pubDate>
            <description><![CDATA[<h2 id="공부-내용">공부 내용</h2>
<h3 id="csapp">csapp</h3>
<ul>
<li>3.7 프로시저</li>
<li>3.8 배열과 행렬</li>
</ul>
<h3 id="기타">기타</h3>
<ul>
<li>정글 9기 나만무 프로젝트 발표 관람 및 질의응답</li>
</ul>
<hr>
<h2 id="코딩-테스트-문제풀이">코딩 테스트 문제풀이</h2>
<ul>
<li>[백준] 14501 - 퇴사(Top_Down, Bottom_up)</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>