<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>jina_ham.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Sun, 13 Sep 2026 06:33:13 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>jina_ham.log</title>
            <url>https://velog.velcdn.com/images/jina_ham/profile/c4782f07-8e36-4de4-970f-d83e61a3cf82/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. jina_ham.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/jina_ham" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[채널톡 면접관이 직접 알려주는 CS 면접 대비 - Java 편]]></title>
            <link>https://velog.io/@jina_ham/%EC%B1%84%EB%84%90%ED%86%A1-%EB%A9%B4%EC%A0%91%EA%B4%80%EC%9D%B4-%EC%A7%81%EC%A0%91-%EC%95%8C%EB%A0%A4%EC%A3%BC%EB%8A%94-CS-%EB%A9%B4%EC%A0%91-%EB%8C%80%EB%B9%84-Java-%ED%8E%B8</link>
            <guid>https://velog.io/@jina_ham/%EC%B1%84%EB%84%90%ED%86%A1-%EB%A9%B4%EC%A0%91%EA%B4%80%EC%9D%B4-%EC%A7%81%EC%A0%91-%EC%95%8C%EB%A0%A4%EC%A3%BC%EB%8A%94-CS-%EB%A9%B4%EC%A0%91-%EB%8C%80%EB%B9%84-Java-%ED%8E%B8</guid>
            <pubDate>Sun, 13 Sep 2026 06:33:13 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/jina_ham/post/7aeaf48b-8057-44fd-8947-536729a70d57/image.png" alt=""></p>
<p><a href="https://inf.run/f8Bys">강의링크</a></p>
<h1 id="면접-준비가-막막했던-취준생의-jscode-제이온님-java-면접-대비-강의-후기">면접 준비가 막막했던 취준생의 JSCODE 제이온님 Java 면접 대비 강의 후기</h1>
<h2 id="강의를-듣게-된-계기">강의를 듣게 된 계기</h2>
<p>현재 2026 하반기 공채를 준비하고 있는 취준생이다.
막상 본격적으로 취준을 시작하려고 하니 할게 정말 정말 많았다.</p>
<p>현재 기본적으로 이력서와 포트폴리오를 작성해둔 상태이지만, 채용 공고를 보다 보면
기업에서 원하는 기술 부분에서 내가 부족한 점이 많다고 생각하여 관련 기술 공부도 하고 포폴도 틈틈히 디벨롭 하고 있다.
개발자다 보니 코테도 준비해야 하고 면접 준비도 해야 한다.
하나를 잡으면 다른 하나가 밀리는 느낌이라 매일 우선순위를 다시 짜는 것도 일이었다.</p>
<p>그 중에서도 제일 막막했던게 면접 준비였던 것 같다.
CS로 공부할 내용이 정말 많았다.
Java, Spring, DB, 운영체제, 컴퓨터 구조, 네트워크 등... 이 cs를 책 한권을 사서 온전히 공부한 후 기술 면접을 준비하기에는 거의 몇 년이 걸릴 것 같았다.
게다가 책을 처음부터 끝까지 다 읽는다고 해서 그 내용이 그대로 면접 답변이 되는 것도 아니다.
아는 것과 말로 설명할 수 있는 것은 완전히 다른 문제니까.
실제로 혼자 예상 질문을 뽑아서 답변해보려고 하면 &quot;어... 그건 그러니까...&quot; 하다가 끝나는 경우가 많았다.
내가 이해한 게 맞는지, 이 정도 깊이로 대답해도 되는건지 확인해줄 사람이 없다는 게 제일 답답했다.</p>
<p>이런 막막함을 가지고 있을 때 JSCODE 제이온님의 면접 대비 강의를 발견하게 되었다.
나의 막막했던 면접 준비를 조금 수월하게 만들어줄 수 있는 강의였다.</p>
<h2 id="강의는-이렇게-구성되어-있다">강의는 이렇게 구성되어 있다</h2>
<p>해당 강의는 실제 면접 질문에서 많이 나오는 질문들에 대해 어떻게 대답을 하면 좋은지
질문 별로 Bronze, Silver, Gold 답변 예시를 보여준다.
답변에 어떤 키워드가 들어가면 좋은지, 해당 답변에 이은 꼬리 질문에는 어떤 것들이 나올 수 있는지...</p>
<p>이 Bronze / Silver / Gold 구성이 개인적으로 제일 좋았다.
보통 &quot;이 질문엔 이렇게 답하세요&quot; 하고 모범 답안 하나만 주는 자료가 많은데,
이 강의는 부족한 답변부터 잘한 답변까지 단계별로 보여주니까
&quot;내 답변은 지금 어느 단계에 있는지&quot;를 스스로 채점할 수 있다.</p>
<p>나는 실제 강의를 들으면서 속으로 답변을 해보면 최대 silver에서 머물렀다.
하지만 Gold 답변을 보면서 내 답변에서 어디가 부족한지 되짚으면서 들을 수 있었다.
대부분 개념 자체를 몰라서가 아니라,
개념은 알지만 &quot;그래서 그게 왜 중요한지&quot;, &quot;실무에서 어떤 문제로 이어지는지&quot;까지 연결하지 못해서 Silver에 머무는 경우였다.
이걸 알게 된 것만으로도 앞으로 CS를 공부하는 방향이 꽤 명확해졌다.</p>
<p>꼬리 질문 파트도 실전에서 진짜 도움이 될 것 같다.
면접에서 제일 무서운 건 첫 질문이 아니라 그 뒤에 따라오는 &quot;그럼 왜 그렇게 동작하나요?&quot; 같은 질문이니까.
미리 어떤 꼬리 질문이 나올 수 있는지 알고 있으면, 답변을 할 때 그 방향까지 염두에 두고 말하게 된다.</p>
<h2 id="강의-내용">강의 내용</h2>
<p>내가 이번에 들은 강의는 Java편이었다.
강의는 아래와 같은 섹션으로 이루어져있다.</p>
<ul>
<li><strong>JVM과 실행 원리</strong> — 자바 코드가 실제로 어떤 과정을 거쳐 실행되는지. 면접에서 거의 빠지지 않고 나오는 단골 주제다.</li>
<li><strong>GC</strong> — 가비지 컬렉션의 동작 방식과 관련 개념들. 혼자 공부할 때 제일 정리가 안 되던 파트였는데 답변 기준이 생기니 훨씬 잡기 쉬웠다.</li>
<li><strong>동시성 이슈</strong> — 멀티스레드 환경에서 생기는 문제와 그걸 다루는 방법. 백엔드 면접이라면 반드시 짚고 넘어가야 하는 부분.</li>
<li><strong>객체지향 프로그래밍</strong> — 알고 있다고 생각했지만 막상 말로 설명하려니 제일 애매했던 주제.</li>
<li><strong>람다, 스트림</strong> — 실무에서 자주 쓰면서도 &quot;왜 쓰는지&quot;를 설명하라고 하면 막히는 부분.</li>
</ul>
<p>그리고 특별 부록으로 <strong>빈출 질문 PDF</strong>도 추가로 제공된다!!
강의를 다 들은 후에 이 PDF로 혼자 셀프 모의면접을 해볼 수 있어서 복습용으로 아주 좋다.</p>
<h2 id="이런-분들께-추천">이런 분들께 추천</h2>
<p>해당 강의는 Java에 대한 기본 개념이 있다는 전제하에 강의를 진행한다.
하지만 해당 개념 공부를 해본 적 없는 사람이더라도 질문 주제에 대한 개념을 스스로 정리해본 후 강의를 수강하는 것 정도는 괜찮다고 생각하고,
CS 면접 대비를 가장 빠르고 완벽하게 해낼 수 있는 방법이라고 생각한다!!</p>
<p>정리하자면,</p>
<ul>
<li>이력서/포폴은 어느정도 준비했는데 면접 준비는 어디서부터 손대야 할지 모르겠는 취준생</li>
<li>CS 개념은 공부했는데 막상 말로 설명하려니 막히는 사람</li>
<li>내 답변이 면접관 기준에서 몇 점짜리인지 확인받고 싶은 사람</li>
</ul>
<p>이런 분들이라면 한 번 들어볼 만한 강의다.
나도 Java편을 먼저 들었으니, 남은 CS 영역들도 이런 식으로 하나씩 채워나갈 생각이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Remote-SSH 설치]]></title>
            <link>https://velog.io/@jina_ham/Remote-SSH-%EC%84%A4%EC%B9%98</link>
            <guid>https://velog.io/@jina_ham/Remote-SSH-%EC%84%A4%EC%B9%98</guid>
            <pubDate>Sun, 13 Sep 2026 03:51:40 GMT</pubDate>
            <description><![CDATA[<h2 id="1-vscode에서-remote-ssh-설치">1. VSCode에서 Remote-SSH 설치</h2>
<p> <img src="https://velog.velcdn.com/images/jina_ham/post/9d724e84-2f16-47b0-a034-00f9d10ae5a3/image.png" alt=""></p>
<ul>
<li>Remote-SSH는 VSCode의 확장 프로그램으로 만들어져 있다.</li>
</ul>
<h2 id="2-key-pair-권한-수정하기">2. key pair 권한 수정하기</h2>
<ul>
<li>key pair는 AWS EC2를 생성할 때 발급받은 키페어이다.</li>
<li>해당 Key pair를 ssh라는 빈 폴더를 만들고 그 안에 key pair를 복사해 넣어둔다.
<img src="https://velog.velcdn.com/images/jina_ham/post/d5d84a32-c84d-4de2-b239-c226894d83c9/image.png" alt=""></li>
<li>위 경로에서 터미널로 파일 권한을 수정해준다. <pre><code>// Mac/Linux
chmod 400 ~/.ssh/your-key.pem
</code></pre></li>
</ul>
<p>// Windows
icacls &quot;C:\Users\사용자명.ssh\your-key.pem&quot; /inheritance:r /grant:r &quot;$env:USERNAME:R&quot;</p>
<pre><code>
## 3. Remote-SSH: Connect to Host
![](https://velog.velcdn.com/images/jina_ham/post/06f8f7fe-8671-42a5-9b17-38d861cd6006/image.png)
![](https://velog.velcdn.com/images/jina_ham/post/00dba315-2259-4952-ab68-e565f586fde4/image.png)
![](https://velog.velcdn.com/images/jina_ham/post/fd61798a-29ac-4b71-98e3-de0a845210a7/image.png)
![](https://velog.velcdn.com/images/jina_ham/post/87780620-92bf-4f7b-9ea2-e516461c1c50/image.png)

- config파일이 열리면 아래 내용을 입력해준다.</code></pre><h1 id="read-more-about-ssh-config-files-httpslinuxdienetman5ssh_config">Read more about SSH config files: <a href="https://linux.die.net/man/5/ssh_config">https://linux.die.net/man/5/ssh_config</a></h1>
<p>Host myec2
  HostName [EC2 인스턴스의 IP 주소]
  User ec2-user
  IdentityFile ~/.ssh/ec2_key.pem // 실제 키 파일의 위치 
  StrictHostKeyChecking accept-new</p>
<p>```</p>
<ul>
<li>아래와 같이 myec2가 생성된 것을 볼 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jina_ham/post/05e0a0d8-af33-497a-b190-007a5b25be2b/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jina_ham/post/fbfee998-9add-4f7c-b1cc-787a2ee03fc1/image.png" alt=""></p>
<ul>
<li>새로 window가 열리면서 ec2에 원격으로 연결된 것을 볼 수 있다. </li>
<li>terminal을 통해 ec2에 원격으로 명령어를 입력할 수 있고, 폴더를 관리하기 쉬워진다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kubernetes Controller]]></title>
            <link>https://velog.io/@jina_ham/Kubernetes-Controller</link>
            <guid>https://velog.io/@jina_ham/Kubernetes-Controller</guid>
            <pubDate>Sun, 13 Sep 2026 03:09:48 GMT</pubDate>
            <description><![CDATA[<h1 id="3-controller">3. Controller</h1>
<h2 id="controller란-무엇인가">Controller란 무엇인가</h2>
<p>Pod를 직접 만들면 관리는 사람의 몫이 된다. Pod가 죽어도 아무도 다시 만들어주지 않고, 트래픽이 늘어도 알아서 늘어나지 않으며, 버전을 올리려면 일일이 지우고 다시 만들어야 한다.</p>
<p><strong>Controller는 Pod를 대신 관리해 주는 오브젝트</strong>다. 사용자가 원하는 상태를 선언해 두면, Controller가 현재 상태를 계속 감시하면서 그 상태에 맞추는 작업을 수행한다.</p>
<pre><code>   [ Pod만 직접 만들 때 ]              [ Controller를 쓸 때 ]

   Pod가 죽음                          Pod가 죽음
        │                                   │
        ▼                                   ▼
   사람이 인지                         Controller가 즉시 인지
        │                                   │
        ▼                                   ▼
   사람이 다시 생성                    자동으로 새 Pod 생성</code></pre><p>Controller가 제공하는 기능은 네 가지다.</p>
<table>
<thead>
<tr>
<th>기능</th>
<th>설명</th>
<th>사용하는 Controller</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Auto Healing</strong></td>
<td>죽은 Pod를 감지해 새로 만든다</td>
<td>ReplicationController, ReplicaSet, DaemonSet</td>
</tr>
<tr>
<td><strong>Auto Scaling</strong></td>
<td>부하에 따라 Pod 수를 늘리고 줄인다</td>
<td>HPA</td>
</tr>
<tr>
<td><strong>Software Update</strong></td>
<td>여러 Pod의 버전을 일괄 업그레이드한다</td>
<td>Deployment</td>
</tr>
<tr>
<td><strong>Job</strong></td>
<td>필요한 순간에만 Pod를 만들어 작업을 수행한다</td>
<td>Job, CronJob</td>
</tr>
</tbody></table>
<hr>
<h2 id="3-1-controller의-네-가지-기능">3-1. Controller의 네 가지 기능</h2>
<h3 id="auto-healing">Auto Healing</h3>
<p>Pod가 죽거나 Pod가 올라가 있는 Node 자체에 장애가 발생하면, Controller가 <strong>즉각적으로 인지해서 다른 Node에 Pod를 새로 만든다.</strong></p>
<pre><code>   ┌──────────────────┐
   │   Controller     │  ← 상태를 계속 감시
   └────┬────────┬────┘
        │        │
   장애 인지     새로 생성
        │        │
        ▼        ▼
  ┌──────────┐  ┌──────────┐
  │  Node1   │  │  Node2   │
  │ ┌──────┐ │  │ ┌──────┐ │
  │ │ Pod1 │ │  │ │ Pod1 │ │  ← 새 Node에 다시 생성
  │ │  ✗   │ │  │ │  ✓   │ │
  │ └──────┘ │  │ └──────┘ │
  │  ⚠ 장애  │  │          │
  └──────────┘  └──────────┘</code></pre><p>사람이 개입하기 전에 복구가 끝나므로, 엔지니어는 복구 이력만 확인하면 된다.</p>
<blockquote>
<p>담당 Controller: <strong>ReplicationController</strong>, <strong>ReplicaSet</strong>, <strong>DaemonSet</strong></p>
</blockquote>
<h3 id="auto-scaling">Auto Scaling</h3>
<p>Pod의 리소스 사용량이 <strong>limit에 도달한 상태</strong>를 Controller가 인지하면, Pod를 하나 더 만들어서 <strong>부하를 분산</strong>시킨다.</p>
<pre><code>   ┌──────────────────┐
   │   Controller     │  ← 리소스 사용량 감시
   └────────┬─────────┘
            │  Pod1이 limit 도달
            ▼
   ┌──────┐        ┌──────────┐
   │ Pod1 │ ─┬───▶ │  Pod1    │  부하 50%
   │ ███  │  │     │  ██      │
   │ 한계 │  │     └──────────┘
   └──────┘  │     ┌──────────┐
             └───▶ │  Pod2    │  부하 50%
                   │  ██      │  ← 새로 생성
                   └──────────┘</code></pre><p>Pod를 늘려 부하를 나누므로 서비스가 느려지거나 멈추는 상황을 막을 수 있다.</p>
<blockquote>
<p>담당 Controller: <strong>HPA (Horizontal Pod Autoscaler)</strong></p>
</blockquote>
<h3 id="software-update">Software Update</h3>
<p>여러 개의 Pod를 <strong>한 번에 새로운 버전으로 업그레이드</strong>한다. 업데이트 중 문제가 생기면 이전 버전으로 되돌리는 것도 가능하다.</p>
<pre><code>   ┌──────────────────┐
   │   Controller     │
   └────────┬─────────┘
            │
   ┌────────┴─────────────────────┐
   │                              │
   ▼                              ▼
 v1 v1 v1                     v2 v2 v2
 ┌──┐┌──┐┌──┐    ──────▶     ┌──┐┌──┐┌──┐
 │P1││P2││P3│                │P1││P2││P3│
 └──┘└──┘└──┘                └──┘└──┘└──┘
 v1 v1 v1                     v2 v2 v2
 ┌──┐┌──┐┌──┐                ┌──┐┌──┐┌──┐
 │P4││P5││P6│                │P4││P5││P6│
 └──┘└──┘└──┘                └──┘└──┘└──┘</code></pre><blockquote>
<p>담당 Controller: <strong>Deployment</strong></p>
</blockquote>
<h3 id="job">Job</h3>
<p>일시적인 작업을 해야 하는 경우, Controller가 <strong>필요한 순간에만 Pod를 만든다.</strong> 작업이 끝나면 Pod가 사용하던 자원을 반납한다.</p>
<pre><code>   ┌──────────────────┐
   │   Controller     │
   └───┬──────────┬───┘
       │          │
       ▼          ▼
  ┌─────────┐ ┌─────────┐ ┌─────────┐
  │  Pod    │ │  Pod    │ │  Pod    │
  │ (없음)  │→│ (실행)  │→│ (종료)  │
  └─────────┘ └─────────┘ └─────────┘
  Resource    Resource     Resource
  ┌─┐┌─┐┌─┐   ┌─┐┌ ┐┌ ┐    ┌─┐┌─┐┌─┐
  └─┘└─┘└─┘   └─┘└ ┘└ ┘    └─┘└─┘└─┘
  자원 여유    자원 사용중   자원 반납</code></pre><p>Pod를 계속 띄워두지 않으므로 <strong>자원을 효율적으로 사용</strong>할 수 있다.</p>
<blockquote>
<p>담당 Controller: <strong>Job</strong>, <strong>CronJob</strong></p>
</blockquote>
<hr>
<h2 id="3-2-replicationcontroller와-replicaset">3-2. ReplicationController와 ReplicaSet</h2>
<h3 id="두-controller의-관계">두 Controller의 관계</h3>
<table>
<thead>
<tr>
<th>Controller</th>
<th>상태</th>
</tr>
</thead>
<tbody><tr>
<td><strong>ReplicationController</strong></td>
<td><strong>Deprecated</strong> (더 이상 사용하지 않음)</td>
</tr>
<tr>
<td><strong>ReplicaSet</strong></td>
<td>ReplicationController를 <strong>대체(Replaced)</strong> 한 Controller</td>
</tr>
</tbody></table>
<p>역할은 같지만 ReplicaSet이 더 발전된 selector 기능을 갖추면서 ReplicationController를 대체했다. 현재는 ReplicaSet을 사용한다.</p>
<p>두 Controller는 세 가지 요소로 구성된다.</p>
<table>
<thead>
<tr>
<th>구성 요소</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Template</strong></td>
<td>새로 만들 Pod의 내용을 정의한다</td>
</tr>
<tr>
<td><strong>Replicas</strong></td>
<td>Pod를 몇 개로 유지할지 정한다</td>
</tr>
<tr>
<td><strong>Selector</strong></td>
<td>어떤 Pod를 관리 대상으로 삼을지 고른다</td>
</tr>
</tbody></table>
<hr>
<h3 id="template">Template</h3>
<p>Controller를 만들 때 <strong>Pod의 내용을 템플릿으로 넣어둔다.</strong> Controller가 Pod를 새로 만들어야 할 때 이 템플릿을 그대로 사용한다.</p>
<pre><code>   ┌──────────────────────────────┐
   │       Replication            │
   │                              │        ┌──────────┐
   │  selector    template        │        │ Pod:v2   │
   │  ┌────────┐  ┌──────────┐    │        │          │
   │  │type:web│  │  Pod:v2  │◀───┼────────│ Template │
   │  └────────┘  └──────────┘    │        │  Update  │
   └───────┬──────────────────────┘        └──────────┘
           │
     Re-create
           │
   ┌───────┴──────┬──────────────┐
   ▼              ▼              ▼
┌────────┐   ┌────────┐    ┌──────────┐
│  Pod   │   │  Pod   │    │  Pod:v2  │
│type:web│   │type:web│    │ type:web │
│  ⚠     │   │   ✗    │    │    ✓     │
└────────┘   └────────┘    └──────────┘
  기존 Pod    삭제하면       템플릿대로
              새로 생성됨     v2로 생성</code></pre><p>여기서 중요한 동작이 있다. <strong>템플릿을 v2로 수정해도 이미 실행 중인 Pod는 그대로 남는다.</strong> 기존 Pod를 삭제해야 그때 템플릿을 보고 v2 Pod가 새로 만들어진다.</p>
<p>이 성질을 이용하면 Pod를 하나씩 지우면서 버전을 올리는 것이 가능하다. 다만 수작업이므로, 실제 버전 업그레이드는 뒤에 나올 Deployment가 담당한다.</p>
<p><strong>Pod를 직접 만드는 경우</strong></p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: pod-1
  labels:
    type: web
spec:
  containers:
    - name: container
      image: tmkube/app:v1</code></pre>
<p><strong>ReplicationController로 만드는 경우</strong></p>
<pre><code class="language-yaml">apiVersion: v1
kind: ReplicationController
metadata:
  name: replication-1
spec:
  replicas: 1
  selector:
    type: web
  template:
    metadata:
      name: pod-1
      labels:
        type: web
    spec:
      containers:
        - name: container
          image: tmkube/app:v2</code></pre>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>replicas</code></td>
<td>유지할 Pod의 개수</td>
</tr>
<tr>
<td><code>selector</code></td>
<td>관리 대상 Pod를 고르는 조건</td>
</tr>
<tr>
<td><code>template</code></td>
<td>새 Pod를 만들 때 사용할 내용. Pod YAML의 <code>metadata</code>와 <code>spec</code>이 그대로 들어간다</td>
</tr>
</tbody></table>
<p><code>template</code> 안의 내용이 앞의 Pod YAML과 동일한 구조라는 점을 보면 이해하기 쉽다. <strong>Pod 정의를 Controller 안에 그대로 넣어둔 것</strong>이다.</p>
<h3 id="replicas">Replicas</h3>
<p><strong>지정한 개수만큼 Pod가 유지되도록 관리한다.</strong></p>
<pre><code>   ┌──────────────────┐          ┌──────────────────┐
   │   Replication    │          │   Replication    │
   │                  │          │                  │
   │   replicas       │          │  replicas  template
   │   ┌────┐         │          │  ┌────┐  ┌──────┐│
   │   │ 3  │         │          │  │ 2  │  │ Pod  ││
   │   └────┘         │          │  └────┘  └──────┘│
   └──┬────┬────┬─────┘          └───┬──────────┬───┘
      │    │    │                    │    ⚠     │
      │    │    │  Scale Out         │  Pod 삭제 │
      ▼    ▼    ▼                    ▼          ▼
   ┌───┐┌───┐┌───┐                ┌───┐      ┌───┐
   │Pod││Pod││Pod│                │Pod│      │Pod│
   └───┘└───┘└───┘                └───┘      └───┘
                                        새로 생성됨</code></pre><table>
<thead>
<tr>
<th>상황</th>
<th>Controller의 동작</th>
</tr>
</thead>
<tbody><tr>
<td><code>replicas</code>를 1에서 3으로 변경</td>
<td>Pod를 2개 더 만든다 (<strong>Scale Out</strong>)</td>
</tr>
<tr>
<td>Pod 하나가 삭제되거나 죽음</td>
<td>개수를 채우기 위해 새 Pod를 만든다 (<strong>Auto Healing</strong>)</td>
</tr>
<tr>
<td><code>replicas</code>를 3에서 1로 변경</td>
<td>Pod 2개를 삭제한다 (<strong>Scale In</strong>)</td>
</tr>
</tbody></table>
<p>여기서 알 수 있는 것은 <strong>Auto Healing과 Auto Scaling이 사실 같은 동작</strong>이라는 점이다. Controller는 &quot;지정된 개수를 유지한다&quot;는 하나의 원칙만 수행하며, 그 결과가 상황에 따라 복구로도 보이고 확장으로도 보이는 것이다.</p>
<p>또한 죽은 Pod를 되살리는 것이 아니라 <strong>항상 새 Pod를 만든다.</strong> 따라서 Pod 안에 저장한 데이터는 복구되지 않는다.</p>
<h3 id="selector">Selector</h3>
<p><strong>어떤 Pod를 관리 대상으로 삼을지</strong> 고르는 조건이다. Service의 selector가 트래픽을 보낼 Pod를 고르는 것과 같은 방식이다.</p>
<p>ReplicationController와 ReplicaSet의 가장 큰 차이가 여기에 있다.</p>
<pre><code>   ReplicationController          ReplicaSet
   ─────────────────────          ──────────────────────────
   ┌──────────────┐               ┌──────────────────────────┐
   │  type: web   │               │ matchLabels  matchExpressions
   │              │               │ ┌────────┐  ┌───────────┐│
   │  (값이 정확히│               │ │type:web│  │key: ver   ││
   │   같아야 함) │               │ └────────┘  │operator:  ││
   └──────────────┘               │             │  Exists   ││
                                  │             └───────────┘│
                                  └──────────────────────────┘</code></pre><table>
<thead>
<tr>
<th>구분</th>
<th>ReplicationController</th>
<th>ReplicaSet</th>
</tr>
</thead>
<tbody><tr>
<td>지정 방식</td>
<td>값이 <strong>정확히 일치</strong>하는 조건만 가능</td>
<td><code>matchLabels</code> + <code>matchExpressions</code></td>
</tr>
<tr>
<td>표현력</td>
<td>단순</td>
<td>key의 <strong>존재 여부, 포함, 제외</strong>까지 표현 가능</td>
</tr>
</tbody></table>
<p><strong>선택 결과 예시</strong></p>
<p>세 개의 Pod가 있다고 하자.</p>
<pre><code>  ┌────────────┐  ┌────────────┐  ┌────────────┐
  │   Pod1     │  │   Pod2     │  │   Pod3     │
  │ type: web  │  │ type: web  │  │ type: db   │
  │ ver : v1   │  │ ver : alpha│  │ ver : beta │
  └────────────┘  └────────────┘  └────────────┘</code></pre><table>
<thead>
<tr>
<th>Controller</th>
<th>조건</th>
<th>선택되는 Pod</th>
</tr>
</thead>
<tbody><tr>
<td>ReplicationController</td>
<td><code>type: web</code></td>
<td>Pod1, Pod2</td>
</tr>
<tr>
<td>ReplicaSet</td>
<td><code>matchLabels: type: web</code> + <code>matchExpressions: {key: ver, operator: Exists}</code></td>
<td>Pod1, Pod2</td>
</tr>
</tbody></table>
<p>ReplicaSet에서 두 조건이 모두 지정되면 <strong>둘 다 만족하는 Pod만</strong> 선택된다.</p>
<h3 id="matchexpressions의-operator">matchExpressions의 operator</h3>
<p><code>matchExpressions</code>는 네 가지 연산자를 제공한다.</p>
<pre><code>        Exists                          DoesNotExist
  ┌──────────────────┐            ┌──────────────────┐
  │   ReplicaSet     │            │   ReplicaSet     │
  │   Key: A         │            │   Key: A         │
  └──────────────────┘            └──────────────────┘
   ┌────┐┌────┐┌────┐              ┌────┐┌────┐┌────┐
   │Pod1││Pod2││Pod3│              │Pod1││Pod2││Pod3│
   │A:1 ││A:2 ││B:2 │              │A:1 ││A:2 ││B:2 │
   └────┘└────┘└────┘              └────┘└────┘└────┘
     ✓     ✓     ✗                   ✗     ✗     ✓
   A라는 key가 있으면 선택          A라는 key가 없으면 선택


          In                              NotIn
  ┌──────────────────┐            ┌──────────────────┐
  │   ReplicaSet     │            │   ReplicaSet     │
  │   Key: A         │            │   Key: A         │
  │   Values: 2,3    │            │   Values: 2,3    │
  └──────────────────┘            └──────────────────┘
   ┌────┐┌────┐┌────┐              ┌────┐┌────┐┌────┐
   │Pod1││Pod2││Pod3│              │Pod1││Pod2││Pod3│
   │A:1 ││A:2 ││A:3 │              │A:1 ││A:2 ││A:3 │
   └────┘└────┘└────┘              └────┘└────┘└────┘
     ✗     ✓     ✓                   ✓     ✗     ✗
   값이 2 또는 3이면 선택           값이 2, 3이 아니면 선택</code></pre><table>
<thead>
<tr>
<th>operator</th>
<th>조건</th>
<th>필요한 항목</th>
</tr>
</thead>
<tbody><tr>
<td><code>Exists</code></td>
<td>해당 key가 <strong>존재하면</strong> 선택</td>
<td>key</td>
</tr>
<tr>
<td><code>DoesNotExist</code></td>
<td>해당 key가 <strong>없으면</strong> 선택</td>
<td>key</td>
</tr>
<tr>
<td><code>In</code></td>
<td>key의 값이 values <strong>안에 있으면</strong> 선택</td>
<td>key, values</td>
</tr>
<tr>
<td><code>NotIn</code></td>
<td>key의 값이 values <strong>안에 없으면</strong> 선택</td>
<td>key, values</td>
</tr>
</tbody></table>
<p>값을 정확히 알지 못해도 &quot;버전 정보가 붙어 있는 Pod 전부&quot;처럼 유연한 조건을 만들 수 있다는 것이 ReplicaSet의 장점이다.</p>
<h3 id="replicaset-yaml">ReplicaSet YAML</h3>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: replica-1
spec:
  replicas: 3
  selector:
    matchLabels:
      type: web
    matchExpressions:
      - {key: ver, operator: Exists}
  template:
    metadata:
      name: pod
      labels:
        type: web
        ver: v1
    spec:
      containers:
        - name: container
          image: tmkube/app</code></pre>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>apiVersion: apps/v1</code></td>
<td>ReplicaSet은 <code>v1</code>이 아니라 <strong><code>apps/v1</code></strong> 을 사용한다</td>
</tr>
<tr>
<td><code>replicas: 3</code></td>
<td>Pod 3개를 유지한다</td>
</tr>
<tr>
<td><code>matchLabels</code></td>
<td>값이 정확히 일치해야 하는 조건</td>
</tr>
<tr>
<td><code>matchExpressions</code></td>
<td>key의 존재 여부나 포함 관계로 지정하는 조건</td>
</tr>
<tr>
<td><code>template</code></td>
<td>새 Pod를 만들 때 사용할 정의</td>
</tr>
</tbody></table>
<h3 id="주의할-점--template의-label은-selector를-만족해야-한다">주의할 점 — template의 label은 selector를 만족해야 한다</h3>
<p><code>selector</code> 조건과 <code>template</code>의 <code>labels</code>가 맞지 않으면 문제가 생긴다.</p>
<pre><code>   selector: type: web        template의 labels: type: db
        │                              │
        │  조건에 맞는 Pod를 찾음      │  이 Pod를 만듦
        ▼                              ▼
   조건에 맞는 Pod가 없음  ──────▶  계속 새 Pod 생성
                                       │
                                   무한 반복</code></pre><p>Controller가 만든 Pod를 자기 자신이 인식하지 못하므로, 개수를 채우려고 Pod를 계속 만들어내게 된다. 쿠버네티스는 이런 설정을 아예 거부하여 ReplicaSet 생성을 막는다.</p>
<h1 id="3-3-deployment">3-3. Deployment</h1>
<h2 id="deployment란">Deployment란</h2>
<p>Deployment는 <strong>Software Update를 담당하는 Controller</strong>다. 앞에서 본 ReplicaSet은 Pod의 개수를 유지할 뿐, 버전을 올리는 기능은 없었다.</p>
<p>Deployment는 ReplicaSet을 직접 관리하면서 버전 업그레이드를 처리한다.</p>
<pre><code>   Deployment  ──관리──▶  ReplicaSet  ──관리──▶  Pod
   (버전 관리)            (개수 유지)          (실행)</code></pre><p>사용자는 Deployment만 만들면 된다. ReplicaSet은 Deployment가 알아서 생성하고 관리한다.</p>
<h2 id="배포-방식의-종류">배포 방식의 종류</h2>
<p>Deployment는 두 가지 배포 방식을 직접 지원하고, 나머지 두 가지는 Service와 조합해 구현한다.</p>
<table>
<thead>
<tr>
<th>배포 방식</th>
<th>Deployment 지원</th>
<th>다운타임</th>
<th>추가 자원</th>
</tr>
</thead>
<tbody><tr>
<td><strong>ReCreate</strong></td>
<td>지원 (<code>strategy: Recreate</code>)</td>
<td>발생</td>
<td>없음</td>
</tr>
<tr>
<td><strong>Rolling Update</strong></td>
<td>지원 (<code>strategy: RollingUpdate</code>, 기본값)</td>
<td>없음</td>
<td>배포 중 일부 증가</td>
</tr>
<tr>
<td><strong>Blue/Green</strong></td>
<td>미지원 (Controller 2개 + Service 활용)</td>
<td>없음</td>
<td>2배</td>
</tr>
<tr>
<td><strong>Canary</strong></td>
<td>미지원 (Service + Ingress 활용)</td>
<td>없음</td>
<td>일부 증가</td>
</tr>
</tbody></table>
<hr>
<h2 id="3-3-1-recreate">3-3-1. ReCreate</h2>
<h3 id="개념">개념</h3>
<p>기존 Pod를 <strong>전부 삭제한 뒤</strong> 새 버전의 Pod를 만드는 방식이다. 가장 단순하지만 <strong>다운타임이 발생</strong>한다.</p>
<h3 id="동작-과정">동작 과정</h3>
<p>v1 Pod 2개를 v2로 올리는 상황이다.</p>
<p><strong>① 초기 상태</strong></p>
<pre><code>              ┌──────────────┐
              │   Service    │
              └──┬────────┬──┘
                 │        │
          ┌──────▼──┐ ┌───▼─────┐
          │ Pod v1  │ │ Pod v1  │
          └─────────┘ └─────────┘

   자원 사용량:  ██ ██        (2개)
   서비스 상태:  정상</code></pre><p><strong>② v1 Pod를 모두 삭제</strong></p>
<pre><code>              ┌──────────────┐
              │   Service    │
              └──────────────┘
                 연결할 Pod 없음

          ┌─────────┐ ┌─────────┐
          │ Pod v1  │ │ Pod v1  │
          │    ✗    │ │    ✗    │
          └─────────┘ └─────────┘

   자원 사용량:  □□ □□        (0개, 자원도 함께 반납됨)
   서비스 상태:  ⚠ Downtime 발생</code></pre><p>이 시점에 사용자가 접속하면 <strong>연결할 Pod가 없어 서비스가 중단된다.</strong></p>
<p><strong>③ v2 Pod 2개 생성</strong></p>
<pre><code>              ┌──────────────┐
              │   Service    │
              └──┬────────┬──┘
                 │        │
          ┌──────▼──┐ ┌───▼─────┐
          │ Pod v2  │ │ Pod v2  │
          └─────────┘ └─────────┘

   자원 사용량:  ██ ██        (2개)
   서비스 상태:  정상 복구</code></pre><h3 id="자원-사용량의-변화">자원 사용량의 변화</h3>
<pre><code>   사용량
     │
   2 │ ██ ██              ██ ██
     │
   0 │        □□ □□
     └────────────────────────────▶ 시간
       초기    삭제 후     생성 후
              (Downtime)</code></pre><p>배포 중 자원 사용량이 <strong>0으로 떨어진다.</strong> 추가 자원이 필요 없다는 것이 유일한 장점이다.</p>
<h3 id="yaml">YAML</h3>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment-1
spec:
  selector:
    matchLabels:
      type: app
  replicas: 2
  strategy:
    type: Recreate
  revisionHistoryLimit: 1
  template:
    metadata:
      labels:
        type: app
    spec:
      containers:
        - name: container
          image: tmkube/app:v1</code></pre>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>strategy.type: Recreate</code></td>
<td>전부 삭제 후 재생성하는 방식을 사용한다</td>
</tr>
<tr>
<td><code>revisionHistoryLimit</code></td>
<td>롤백을 위해 보관할 이전 ReplicaSet의 개수. 기본값은 10</td>
</tr>
</tbody></table>
<h3 id="내부에서-일어나는-일">내부에서 일어나는 일</h3>
<p>Deployment는 ReplicaSet의 <code>replicas</code> 수를 조절하는 방식으로 동작한다.</p>
<pre><code>   ┌────────────────────────────────────────────┐
   │              Deployment                     │
   │   selector   replicas   template            │
   │   type:app      2       Pod v2  ◀── 템플릿 변경
   └───────┬────────────────────┬───────────────┘
           │                    │
    ┌──────▼──────┐      ┌──────▼──────┐
    │ ReplicaSet  │      │ ReplicaSet  │
    │  (v1용)     │      │  (v2용)     │
    │ replicas 0  │      │ replicas 2  │
    └─────────────┘      └──────┬──────┘
      Pod 없음                  │
                         ┌──────┴──────┐
                         ▼             ▼
                   ┌─────────┐   ┌─────────┐
                   │ Pod v2  │   │ Pod v2  │
                   └─────────┘   └─────────┘</code></pre><p>기존 ReplicaSet의 <code>replicas</code>를 <strong>2에서 0으로</strong> 내리고, 새 ReplicaSet의 <code>replicas</code>를 <strong>0에서 2로</strong> 올린다. 두 작업이 순차적으로 일어나므로 그 사이에 Pod가 하나도 없는 구간이 생긴다.</p>
<p>기존 ReplicaSet은 삭제되지 않고 <code>replicas: 0</code> 상태로 남는다. <strong>롤백할 때 다시 이 값을 올리기 위해서</strong>다.</p>
<h3 id="사용하는-경우">사용하는 경우</h3>
<ul>
<li>개발 환경처럼 다운타임이 허용되는 경우</li>
<li>v1과 v2가 동시에 떠 있으면 안 되는 경우 (같은 DB 스키마를 공유하는데 호환되지 않는 경우 등)</li>
</ul>
<hr>
<h2 id="3-3-2-rolling-update">3-3-2. Rolling Update</h2>
<h3 id="개념-1">개념</h3>
<p>Pod를 <strong>하나씩 교체</strong>하는 방식이다. Deployment의 <strong>기본값</strong>이며, 다운타임이 없다.</p>
<h3 id="동작-과정-1">동작 과정</h3>
<p>replicas: 2, 기본값인 <code>maxSurge: 1</code>, <code>maxUnavailable: 0</code> 기준이다. Pod 개수는 <strong>2 → 3 → 2 → 3 → 2</strong>로, 3에서 머무르지 않고 매번 2로 돌아온 뒤 다시 늘어난다.</p>
<p><strong>① 초기 상태 (2개)</strong></p>
<pre><code>              ┌──────────────┐
              │   Service    │
              └──┬────────┬──┘
                 │        │
          ┌──────▼──┐ ┌───▼─────┐
          │ Pod v1  │ │ Pod v1  │
          └─────────┘ └─────────┘

   자원 사용량:  ██ ██        (2개)</code></pre><p><strong>② v2 Pod 하나 생성 → Ready 대기 (3개)</strong></p>
<pre><code>              ┌──────────────────┐
              │     Service      │
              └──┬───────┬────┬──┘
                 │       │    │
          ┌──────▼──┐ ┌──▼───┐ ┌─▼──────┐
          │ Pod v1  │ │Pod v1│ │Pod v2  │  ← 새로 생성, Ready 대기
          └─────────┘ └──────┘ └────────┘

   자원 사용량:  ██ ██ ██     (3개, 증가)
   서비스 상태:  v1과 v2가 동시에 서비스 중</code></pre><p>여기가 Rolling Update의 특징이다. <strong>이 시점에 접속하는 사용자는 v1에 연결될 수도 있고 v2에 연결될 수도 있다.</strong> Service가 두 버전의 Pod에 모두 트래픽을 분배하기 때문이다.</p>
<p><strong>③ v2가 Ready 되면, v1 하나 삭제 (2개로 감소)</strong></p>
<pre><code>              ┌──────────────┐
              │   Service    │
              └──┬────────┬──┘
                 │        │
          ┌──────▼──┐ ┌───▼─────┐
          │ Pod v1  │ │ Pod v2  │
          └─────────┘ └─────────┘

   자원 사용량:  ██ ██        (2개, 다시 감소)
   서비스 상태:  v1 1개, v2 1개</code></pre><p><code>maxUnavailable: 0</code>이므로 새 Pod가 Ready 상태가 되어야만 기존 Pod를 삭제한다. &quot;삭제&quot;와 &quot;생성&quot;은 동시에 일어나는 것이 아니라 <strong>항상 하나씩 순차적으로</strong> 일어난다.</p>
<p><strong>④ v2 Pod 하나 더 생성 → Ready 대기 (3개로 증가)</strong></p>
<pre><code>              ┌──────────────────┐
              │     Service      │
              └──┬───────┬────┬──┘
                 │       │    │
          ┌──────▼──┐ ┌──▼───┐ ┌─▼──────┐
          │ Pod v1  │ │Pod v2│ │Pod v2  │  ← 새로 생성, Ready 대기
          └─────────┘ └──────┘ └────────┘

   자원 사용량:  ██ ██ ██     (3개, 다시 증가)
   서비스 상태:  v1 1개, v2 2개</code></pre><p><strong>⑤ 남은 v1 삭제 → 완료 (2개)</strong></p>
<pre><code>              ┌──────────────┐
              │   Service    │
              └──┬────────┬──┘
                 │        │
          ┌──────▼──┐ ┌───▼─────┐
          │ Pod v2  │ │ Pod v2  │
          └─────────┘ └─────────┘

   자원 사용량:  ██ ██        (2개, 원상 복귀)
   서비스 상태:  정상</code></pre><h3 id="자원-사용량의-변화-1">자원 사용량의 변화</h3>
<pre><code>   사용량
     │
   3 │      ██        ██
     │     ╱  ╲       ╱ ╲
   2 │ ██ ╱    ██   ╱     ██
     │
     └──────────────────────────▶ 시간
       초기  생성  삭제  생성  삭제
             (v2)  (v1) (v2)  (v1)</code></pre><p>3에서 머무르지 않고 <strong>생성 → 삭제 → 생성 → 삭제</strong>를 한 번에 한 동작씩 순서대로 밟으며 3과 2 사이를 오간다. Pod 개수가 많아질수록 이 오르내림이 그만큼 더 반복된다. 배포 중 <strong>일시적으로 추가 자원을 요구한다</strong>는 것이 ReCreate와의 차이이며, 대신 <strong>다운타임이 없다.</strong></p>
<h3 id="내부에서-일어나는-일-1">내부에서 일어나는 일</h3>
<pre><code>   ┌────────────────────────────────────────────┐
   │              Deployment                     │
   │   selector   replicas   template            │
   │   type:app      2       Pod v2              │
   └───────┬────────────────────┬───────────────┘
           │                    │
    ┌──────▼──────┐      ┌──────▼──────┐
    │ ReplicaSet  │      │ ReplicaSet  │
    │  (v1용)     │      │  (v2용)     │
    │ 2 → 2 → 1 → │      │ 0 → 1 → 1 → │
    │     1 → 0   │      │     2 → 2   │
    └─────────────┘      └─────────────┘
       한 번에 한 칸씩만 움직인다</code></pre><p>한 번에 <strong>한 ReplicaSet의 값만 1씩</strong> 움직이며, 두 ReplicaSet의 합이 <code>replicas + maxSurge</code>(여기선 3)를 넘지 않고 <code>replicas - maxUnavailable</code>(여기선 2) 밑으로 내려가지도 않도록 조절된다.</p>
<p>ReCreate가 <code>2 → 0</code> 후 <code>0 → 2</code>로 한 번에 크게 움직였다면, Rolling Update는 <strong>한 칸씩 번갈아 가며</strong> 조절한다. 그래서 두 버전의 Pod가 잠시 공존한다.</p>
<h3 id="yaml-1">YAML</h3>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: deployment-2
spec:
  selector:
    matchLabels:
      type: app
  replicas: 2
  strategy:
    type: RollingUpdate
  minReadySeconds: 10
  template:
    metadata:
      labels:
        type: app
    spec:
      containers:
        - name: container
          image: tmkube/app:v1</code></pre>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>strategy.type: RollingUpdate</code></td>
<td>하나씩 교체하는 방식. <strong>생략해도 기본값</strong></td>
</tr>
<tr>
<td><code>minReadySeconds</code></td>
<td>Pod가 Ready 상태가 된 후 <strong>다음 Pod로 넘어가기까지 대기할 시간(초)</strong></td>
</tr>
</tbody></table>
<p><code>minReadySeconds</code>가 필요한 이유가 있다. Pod가 Ready 상태가 되었더라도 애플리케이션이 완전히 안정화되지 않았을 수 있다. 이 값을 두면 각 단계마다 잠시 기다리므로, 문제가 있는 버전이 한꺼번에 배포되는 것을 막을 수 있다.</p>
<h3 id="주의할-점">주의할 점</h3>
<p>v1과 v2가 동시에 서비스되는 구간이 있으므로, <strong>두 버전이 공존해도 문제가 없어야 한다.</strong></p>
<ul>
<li>API 응답 형식이 바뀌면 클라이언트가 혼란을 겪을 수 있다</li>
<li>DB 스키마가 바뀌면 한쪽 버전에서 오류가 발생할 수 있다</li>
</ul>
<p>이런 경우에는 ReCreate나 Blue/Green을 고려해야 한다.</p>
<h2 id="3-3-3-bluegreen">3-3-3. Blue/Green</h2>
<h3 id="개념-2">개념</h3>
<p><strong>Controller를 하나 더 만들어서</strong> 새 버전을 통째로 준비해 두고, Service의 연결 대상만 바꾸는 방식이다.</p>
<p>Deployment가 직접 지원하는 기능이 아니라, <strong>Controller 두 개와 Service의 Label을 이용해 직접 구현</strong>한다.</p>
<h3 id="동작-과정-2">동작 과정</h3>
<p><strong>① v1 Controller와 Pod만 존재 (Blue)</strong></p>
<pre><code>   ┌──────────────┐
   │  Controller  │
   │    (v1)      │
   └──┬────────┬──┘
      │        │
 ┌────▼────┐ ┌─▼───────┐
 │ Pod     │ │ Pod     │
 │ ver: v1 │ │ ver: v1 │
 └────┬────┘ └────┬────┘
      │           │
      └─────┬─────┘
            │
      ┌─────▼──────┐
      │  Service   │
      │  ver: v1   │  ← selector가 v1을 가리킴
      └────────────┘

   자원 사용량:  ██ ██        (2개)</code></pre><p><strong>② v2 Controller를 새로 생성 (Green)</strong></p>
<pre><code>   ┌──────────────┐        ┌──────────────┐
   │  Controller  │        │  Controller  │
   │    (v1)      │        │    (v2)      │  ← 새로 생성
   └──┬────────┬──┘        └──┬────────┬──┘
      │        │              │        │
 ┌────▼────┐ ┌─▼───────┐ ┌────▼────┐ ┌─▼───────┐
 │ Pod     │ │ Pod     │ │ Pod     │ │ Pod     │
 │ ver: v1 │ │ ver: v1 │ │ ver: v2 │ │ ver: v2 │
 └────┬────┘ └────┬────┘ └─────────┘ └─────────┘
      │           │
      └─────┬─────┘         트래픽 없음 (대기 상태)
            │
      ┌─────▼──────┐
      │  Service   │
      │  ver: v1   │
      └────────────┘

   자원 사용량:  ██ ██ ██ ██   (4개, 2배)</code></pre><p>이 시점에서 v2 Pod는 떠 있지만 Service가 연결하지 않으므로 <strong>트래픽을 받지 않는다.</strong> 이 상태에서 v2를 충분히 검증할 수 있다.</p>
<p><strong>③ Service의 Label만 v2로 변경</strong></p>
<pre><code>   ┌──────────────┐        ┌──────────────┐
   │  Controller  │        │  Controller  │
   │    (v1)      │        │    (v2)      │
   └──┬────────┬──┘        └──┬────────┬──┘
      │        │              │        │
 ┌────▼────┐ ┌─▼───────┐ ┌────▼────┐ ┌─▼───────┐
 │ Pod     │ │ Pod     │ │ Pod     │ │ Pod     │
 │ ver: v1 │ │ ver: v1 │ │ ver: v2 │ │ ver: v2 │
 └─────────┘ └─────────┘ └────┬────┘ └────┬────┘
                              │           │
   트래픽 없음                └─────┬─────┘
                                    │
                              ┌─────▼──────┐
                              │  Service   │
                              │  ver: v2   │  ← selector 변경
                              └────────────┘</code></pre><p>Service의 <code>selector</code>를 <code>ver: v1</code>에서 <code>ver: v2</code>로 수정하는 것만으로 <strong>순간적으로 전환</strong>된다. Pod를 만들거나 지우는 과정이 없으므로 <strong>다운타임이 없다.</strong></p>
<p><strong>④ v1 Controller 삭제</strong></p>
<pre><code>                           ┌──────────────┐
                           │  Controller  │
                           │    (v2)      │
                           └──┬────────┬──┘
                              │        │
                         ┌────▼────┐ ┌─▼───────┐
                         │ Pod     │ │ Pod     │
                         │ ver: v2 │ │ ver: v2 │
                         └────┬────┘ └────┬────┘
                              └─────┬─────┘
                              ┌─────▼──────┐
                              │  Service   │
                              │  ver: v2   │
                              └────────────┘

   자원 사용량:  ██ ██        (2개로 복귀)</code></pre><h3 id="자원-사용량의-변화-2">자원 사용량의 변화</h3>
<pre><code>   사용량
     │
   4 │     ████ ████
     │
   2 │ ██ ██        ██ ██
     │
     └───────────────────────────▶ 시간
       초기  배포 중     완료
            (2배 사용)</code></pre><h3 id="장점과-단점">장점과 단점</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td><strong>장점 — 다운타임 없음</strong></td>
<td>Label 변경만으로 전환되므로 순간적으로 바뀐다</td>
</tr>
<tr>
<td><strong>장점 — 롤백이 쉬움</strong></td>
<td>문제가 생기면 Service의 Label을 다시 v1로 되돌리면 끝이다</td>
</tr>
<tr>
<td><strong>장점 — 사전 검증 가능</strong></td>
<td>v2가 트래픽을 받기 전에 충분히 테스트할 수 있다</td>
</tr>
<tr>
<td><strong>단점 — 자원 2배</strong></td>
<td>두 버전의 Pod가 모두 떠 있어야 하므로 자원이 두 배로 필요하다</td>
</tr>
</tbody></table>
<p><strong>롤백의 차이</strong>가 특히 중요하다. Rolling Update는 롤백할 때도 Pod를 하나씩 다시 교체해야 하지만, Blue/Green은 Label 하나만 되돌리면 즉시 복구된다. 그래서 실패했을 때의 영향이 가장 적은 방식이다.</p>
<hr>
<h2 id="3-3-4-canary">3-3-4. Canary</h2>
<h3 id="개념-3">개념</h3>
<p>새 버전을 <strong>일부 트래픽에만 먼저 노출</strong>해서 검증한 뒤, 문제가 없으면 전체로 확대하는 방식이다.</p>
<p>이름은 탄광에서 유독가스를 미리 감지하던 카나리아 새에서 유래했다.</p>
<h3 id="방법-1--pod-개수로-비율-조절">방법 1 — Pod 개수로 비율 조절</h3>
<p>가장 단순한 구현이다. Service 하나가 v1과 v2 Pod를 모두 선택하도록 하고, <strong>Pod 개수로 트래픽 비율을 조절</strong>한다.</p>
<pre><code>   ┌──────────────┐        ┌──────────────┐
   │  Controller  │        │  Controller  │
   │    (v1)      │        │    (v2)      │
   └──┬────────┬──┘        └──────┬───────┘
      │        │                  │
 ┌────▼────┐ ┌─▼───────┐    ┌─────▼─────┐
 │ Pod     │ │ Pod     │    │ Pod       │
 │ ty: app │ │ ty: app │    │ ty: app   │
 │ ver: v1 │ │ ver: v1 │    │ ver: v2   │
 └────┬────┘ └────┬────┘    └─────┬─────┘
      │           │               │
      └───────────┼───────────────┘
                  │
            ┌─────▼──────┐
            │  Service   │
            │  ty: app   │  ← ver를 조건에 넣지 않음
            └────────────┘

   트래픽 비율:  v1 67%  /  v2 33%</code></pre><p>Service의 selector가 <code>ty: app</code>만 보므로 v1, v2 Pod를 모두 선택한다. v2 Pod가 1개, v1 Pod가 2개이므로 <strong>트래픽의 약 1/3이 v2로 간다.</strong></p>
<p>문제가 없으면 v2 Pod를 늘리고 v1 Pod를 줄여가며 비율을 조정한다.</p>
<h3 id="방법-2--ingress-controller로-경로-분기">방법 2 — Ingress Controller로 경로 분기</h3>
<p>URL 경로에 따라 서비스를 나누는 방식이다. <strong>Ingress Controller</strong>를 활용한다.</p>
<pre><code>   ┌──────────────┐        ┌──────────────┐
   │  Controller  │        │  Controller  │
   │    (v1)      │        │    (v2)      │
   └──┬────────┬──┘        └──────┬───────┘
      │        │                  │
 ┌────▼────┐ ┌─▼───────┐    ┌─────▼─────┐
 │ Pod     │ │ Pod     │    │ Pod       │
 │ ver: v1 │ │ ver: v1 │    │ ver: v2   │
 └────┬────┘ └────┬────┘    └─────┬─────┘
      └─────┬─────┘               │
            │                     │
      ┌─────▼──────┐        ┌─────▼──────┐
      │  Service   │        │  Service   │
      │  ver: v1   │        │  ver: v2   │
      └─────┬──────┘        └─────┬──────┘
            │                     │
          /app                 /v2/app
            │                     │
            └──────────┬──────────┘
                       │
           ┌───────────▼────────────┐
           │  Ingress Controller    │
           └────────────────────────┘</code></pre><table>
<thead>
<tr>
<th>요청 경로</th>
<th>연결되는 버전</th>
</tr>
</thead>
<tbody><tr>
<td><code>/app</code></td>
<td>v1</td>
</tr>
<tr>
<td><code>/v2/app</code></td>
<td>v2</td>
</tr>
</tbody></table>
<p>특정 사용자에게만 <code>/v2/app</code> 경로를 안내해 새 버전을 먼저 써보게 할 수 있다. 검증이 끝나면 Ingress 설정을 바꿔 <code>/app</code> 요청이 v2로 가도록 전환한다.</p>
<pre><code>   검증 전:  /app     ──▶  v1
             /v2/app  ──▶  v2

   전환 후:  /app     ──▶  v2</code></pre><h3 id="자원-사용량의-변화-3">자원 사용량의 변화</h3>
<pre><code>   사용량
     │
   3 │      ███ ███ ███
     │
   2 │ ██ ██          ██ ██
     │
     └───────────────────────────▶ 시간
       초기  검증 중     완료</code></pre><p>v2 Pod를 조금만 띄우므로 Blue/Green처럼 2배가 필요하지는 않다.</p>
<h3 id="장점과-단점-1">장점과 단점</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td><strong>장점 — 다운타임 없음</strong></td>
<td>기존 v1이 계속 서비스하는 상태에서 v2를 추가한다</td>
</tr>
<tr>
<td><strong>장점 — 위험 최소화</strong></td>
<td>문제가 생겨도 영향받는 사용자가 일부로 제한된다</td>
</tr>
<tr>
<td><strong>장점 — 실제 환경 검증</strong></td>
<td>테스트 환경이 아닌 실제 트래픽으로 새 버전을 확인할 수 있다</td>
</tr>
<tr>
<td><strong>단점 — 구성이 복잡</strong></td>
<td>Controller, Service, Ingress를 직접 구성하고 관리해야 한다</td>
</tr>
<tr>
<td><strong>단점 — 모니터링 필요</strong></td>
<td>v2에 문제가 있는지 판단할 지표와 관찰 체계가 갖춰져 있어야 의미가 있다</td>
</tr>
</tbody></table>
<hr>
<h2 id="네-가지-배포-방식-비교">네 가지 배포 방식 비교</h2>
<pre><code>   ReCreate           Rolling Update      Blue/Green         Canary
   ────────           ──────────────      ──────────         ──────
   v1 v1              v1 v1               v1 v1  v2 v2       v1 v1 v2
     ↓                  ↓                    ↘  ↙              ↓  ↓
   (없음) ⚠            v1 v2              Service 전환      Service
     ↓                  ↓                    ↓                 ↓
   v2 v2              v2 v2               v2 v2             비율 조정

   Downtime 발생      Zero Downtime       Zero Downtime     Zero Downtime
   자원 추가 없음      자원 일부 증가       자원 2배          자원 일부 증가</code></pre><table>
<thead>
<tr>
<th>구분</th>
<th>ReCreate</th>
<th>Rolling Update</th>
<th>Blue/Green</th>
<th>Canary</th>
</tr>
</thead>
<tbody><tr>
<td>다운타임</td>
<td><strong>발생</strong></td>
<td>없음</td>
<td>없음</td>
<td>없음</td>
</tr>
<tr>
<td>배포 중 자원</td>
<td>감소 (0까지)</td>
<td>일부 증가</td>
<td><strong>2배</strong></td>
<td>일부 증가</td>
</tr>
<tr>
<td>두 버전 공존</td>
<td>없음</td>
<td><strong>있음</strong></td>
<td>없음 (전환 순간만)</td>
<td><strong>있음 (의도적)</strong></td>
</tr>
<tr>
<td>롤백 속도</td>
<td>느림 (재배포)</td>
<td>느림 (재교체)</td>
<td><strong>즉시 (Label 변경)</strong></td>
<td>빠름 (v2 제거)</td>
</tr>
<tr>
<td>Deployment 지원</td>
<td><code>Recreate</code></td>
<td><code>RollingUpdate</code></td>
<td>미지원 (직접 구성)</td>
<td>미지원 (직접 구성)</td>
</tr>
<tr>
<td>구성 난이도</td>
<td>매우 쉬움</td>
<td>쉬움</td>
<td>보통</td>
<td>어려움</td>
</tr>
</tbody></table>
<p><strong>선택 기준</strong></p>
<ul>
<li>다운타임이 허용되고 두 버전이 공존하면 안 되는가 → <strong>ReCreate</strong></li>
<li>무난하게 무중단 배포를 하고 싶은가 → <strong>Rolling Update</strong> (가장 일반적)</li>
<li>자원에 여유가 있고 즉시 롤백이 중요한가 → <strong>Blue/Green</strong></li>
<li>새 버전의 위험이 크고 실제 트래픽으로 검증하고 싶은가 → <strong>Canary</strong></li>
</ul>
<h1 id="3-4-daemonset-job-cronjob">3-4. DaemonSet, Job, CronJob</h1>
<h2 id="3-4-1-daemonset">3-4-1. DaemonSet</h2>
<h3 id="개념-4">개념</h3>
<p>ReplicaSet은 &quot;Pod를 몇 개 유지할지&quot;를 정하고, 스케줄러가 자원 여유를 보고 배치할 Node를 고른다. 그래서 Pod가 특정 Node에 몰리거나, 어떤 Node에는 하나도 배치되지 않을 수 있다.</p>
<p>DaemonSet은 다르다. <strong>Node의 자원 상태와 상관없이 모든 Node에 Pod를 하나씩 생성한다.</strong></p>
<pre><code>   [ ReplicaSet ]                    [ DaemonSet ]
   ──────────────                    ─────────────
   ┌────────────┐                    ┌────────────┐
   │ ReplicaSet │                    │ DaemonSet  │
   └─┬────┬─────┘                    └─┬───┬───┬──┘
     │    │                            │   │   │
   ┌─▼────▼──────┐                   ┌─▼───────────┐
   │   Node1     │                   │   Node1     │
   │  Pod  Pod   │                   │    Pod      │
   │  ▓▓ ▓▓ ▓▓   │  자원 여유 많음   │             │
   └─────────────┘                   └─────────────┘
   ┌─────────────┐                   ┌─────────────┐
   │   Node2     │                   │   Node2     │
   │    Pod      │                   │    Pod      │
   │  ▓▓ ▓▓      │                   │             │
   └─────────────┘                   └─────────────┘
   ┌─────────────┐                   ┌─────────────┐
   │   Node3     │                   │   Node3     │
   │  (없음)     │  자원 부족        │    Pod      │
   │  ▓          │                   │             │
   └─────────────┘                   └─────────────┘

   자원 여유에 따라 배치            Node마다 무조건 1개</code></pre><p>Node가 새로 추가되면 DaemonSet이 <strong>그 Node에도 자동으로 Pod를 하나 생성</strong>한다. Node가 제거되면 그 Pod도 함께 사라진다.</p>
<h3 id="사용-사례">사용 사례</h3>
<p>Node마다 하나씩 떠 있어야 의미가 있는 작업에 사용한다.</p>
<table>
<thead>
<tr>
<th>용도</th>
<th>대표 도구</th>
<th>하는 일</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Performance</strong></td>
<td>Prometheus (Node Exporter)</td>
<td>Node의 CPU, 메모리, 디스크 사용량을 수집</td>
</tr>
<tr>
<td><strong>Logging</strong></td>
<td>Fluentd</td>
<td>Node에 쌓인 로그를 수집해 중앙 서버로 전송</td>
</tr>
<tr>
<td><strong>Storage</strong></td>
<td>GlusterFS</td>
<td>Node의 디스크를 묶어 분산 스토리지로 제공</td>
</tr>
</tbody></table>
<p>공통점은 <strong>Node 하나하나의 정보에 접근해야 한다</strong>는 것이다. 로그를 수집하려면 모든 Node에 수집기가 있어야 하고, 한 Node라도 빠지면 그 Node의 로그는 유실된다.</p>
<p>Volume에서 다룬 <code>hostPath</code>가 DaemonSet과 함께 쓰이는 이유도 여기에 있다. 모든 Node에 Pod를 하나씩 띄우고, 각 Pod가 자기 Node의 <code>/var/log</code>를 마운트해 로그를 읽는 구조다.</p>
<h3 id="nodeselector--특정-node에만-배치">nodeSelector — 특정 Node에만 배치</h3>
<p>모든 Node가 아니라 조건에 맞는 Node에만 배치할 수도 있다.</p>
<pre><code>   ┌──────────────────────────────┐
   │        DaemonSet             │
   │  selector    template        │
   │  type:app    Pod             │──▶ nodeSelector: os: centos
   └───┬──────────┬───────────┬───┘
       │          │           │
       ▼          ▼           ✗
  ┌─────────┐┌─────────┐┌─────────┐
  │  Node1  ││  Node2  ││  Node3  │
  │  Pod    ││  Pod    ││ (없음)  │
  │os:centos││os:centos││os:ubuntu│
  └─────────┘└─────────┘└─────────┘</code></pre><p><code>os: centos</code> Label이 붙은 Node에만 Pod가 생성되고, <code>os: ubuntu</code>인 Node3에는 생성되지 않는다.</p>
<h3 id="hostport--node의-포트로-직접-접근">hostPort — Node의 포트로 직접 접근</h3>
<p>DaemonSet은 Node마다 Pod가 하나씩이므로, <strong>Node의 포트를 Pod에 직접 연결</strong>할 수 있다.</p>
<pre><code>   ┌─────────────────┐  ┌─────────────────┐
   │     Node1       │  │     Node2       │
   │   ┌─────────┐   │  │   ┌─────────┐   │
   │   │  Pod    │   │  │   │  Pod    │   │
   │   │  8080   │   │  │   │  8080   │   │
   │   └────┬────┘   │  │   └────┬────┘   │
   │        │        │  │        │        │
   │   [ 18080 ]     │  │   [ 18080 ]     │
   └────────┬────────┘  └────────┬────────┘
            │                    │
        Node IP : 18080 로 직접 접근</code></pre><table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>containerPort</code></td>
<td>컨테이너가 사용하는 포트</td>
</tr>
<tr>
<td><code>hostPort</code></td>
<td>그 컨테이너를 연결할 <strong>Node의 포트</strong></td>
</tr>
</tbody></table>
<p>NodePort와 비슷해 보이지만 차이가 있다. NodePort는 Service를 거쳐 어느 Node로 들어와도 Pod가 있는 Node로 전달되지만, <code>hostPort</code>는 <strong>그 Node의 Pod에 직접 연결</strong>된다. 각 Node의 상태를 개별적으로 확인해야 하는 모니터링 도구에 적합하다.</p>
<h3 id="yaml-2">YAML</h3>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: daemonset-1
spec:
  selector:
    matchLabels:
      type: app
  template:
    metadata:
      labels:
        type: app
    spec:
      nodeSelector:
        os: centos
      containers:
        - name: container
          image: tmkube/app
          ports:
            - containerPort: 8080
              hostPort: 18080</code></pre>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>replicas</code> 없음</td>
<td><strong>DaemonSet에는 replicas가 없다.</strong> 개수는 Node 수로 결정되기 때문</td>
</tr>
<tr>
<td><code>nodeSelector</code></td>
<td>조건에 맞는 Node에만 Pod를 배치</td>
</tr>
<tr>
<td><code>hostPort</code></td>
<td>Node의 포트를 Pod에 직접 연결</td>
</tr>
</tbody></table>
<p><code>replicas</code>가 없다는 점이 다른 Controller와 가장 뚜렷한 차이다.</p>
<hr>
<h2 id="3-4-2-job">3-4-2. Job</h2>
<h3 id="pod-replicaset-job의-차이">Pod, ReplicaSet, Job의 차이</h3>
<p>세 오브젝트 모두 Pod를 만들지만, <strong>Pod가 종료되었을 때의 동작</strong>이 다르다.</p>
<pre><code>   ┌──────────────┐
   │     Pod      │   Node1 장애 시 →  Pod 사라짐, 복구 없음
   └──────────────┘

   ┌──────────────┐                    ┌─────────────────────┐
   │  ReplicaSet  │   Node1 장애 시 →  │ Node2에 Recreate    │
   └──────────────┘                    │ 계속 Running 유지   │
                                       └─────────────────────┘

   ┌──────────────┐                    ┌─────────────────────┐
   │     Job      │   Node1 장애 시 →  │ Node2에 Recreate    │
   └──────────────┘                    │ 작업 끝나면 Finish  │
                                       └─────────────────────┘</code></pre><table>
<thead>
<tr>
<th>구분</th>
<th>Pod</th>
<th>ReplicaSet</th>
<th>Job</th>
</tr>
</thead>
<tbody><tr>
<td>Node 장애 시</td>
<td>복구되지 않음</td>
<td>다른 Node에 재생성</td>
<td>다른 Node에 재생성</td>
</tr>
<tr>
<td>Pod 종료 후</td>
<td>종료된 상태로 남음</td>
<td><strong>계속 Restart</strong> (서비스 유지)</td>
<td><strong>Finish</strong> (작업 완료로 처리)</td>
</tr>
<tr>
<td>목적</td>
<td>일회성 실행</td>
<td>계속 떠 있어야 하는 서비스</td>
<td><strong>끝이 있는 작업</strong></td>
</tr>
</tbody></table>
<p>핵심은 <strong>ReplicaSet은 Pod가 끝나는 것을 장애로 보고 다시 띄우지만, Job은 정상 종료로 인정한다</strong>는 점이다.</p>
<p>웹 서버가 꺼지면 문제이므로 다시 띄워야 하지만, 백업 작업이 끝난 것은 성공이므로 다시 띄우면 안 된다. 이 차이 때문에 Job이 따로 존재한다.</p>
<h3 id="사용-사례-1">사용 사례</h3>
<table>
<thead>
<tr>
<th>용도</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Backup</strong></td>
<td>데이터베이스를 백업 파일로 내보내는 작업</td>
</tr>
<tr>
<td><strong>Checking</strong></td>
<td>데이터 정합성 검사, 업데이트 확인</td>
</tr>
<tr>
<td><strong>Messaging</strong></td>
<td>대량 메일이나 알림 발송</td>
</tr>
</tbody></table>
<p>작업이 끝나면 Pod가 종료되고 <strong>자원이 반납되므로</strong>, 항상 띄워두는 것보다 자원을 효율적으로 쓸 수 있다.</p>
<h3 id="completions--몇-번-실행할-것인가">completions — 몇 번 실행할 것인가</h3>
<pre><code>   ┌─────────────────────────────────┐
   │            Job                   │
   │       completions : 6            │
   │   ┌────┐ ┌────┐ ┌────┐          │
   │   │Pod │ │Pod │ │Pod │          │
   │   └────┘ └────┘ └────┘          │
   │   ┌────┐ ┌────┐ ┌────┐          │
   │   │Pod │ │Pod │ │Pod │          │
   │   └────┘ └────┘ └────┘          │
   └─────────────────────────────────┘
        Pod 6개가 순차적으로 실행되고 완료</code></pre><p><code>completions: 6</code>은 <strong>성공적으로 완료되어야 하는 Pod의 수</strong>를 뜻한다. 6개가 모두 성공해야 Job이 완료 상태가 된다.</p>
<h3 id="parallelism--몇-개를-동시에-실행할-것인가">parallelism — 몇 개를 동시에 실행할 것인가</h3>
<pre><code>   parallelism: 2

   ┌────┐ ┌────┐     ┌────┐ ┌────┐     ┌────┐ ┌────┐
   │Pod │ │Pod │  →  │Pod │ │Pod │  →  │Pod │ │Pod │
   └────┘ └────┘     └────┘ └────┘     └────┘ └────┘
    1회차 (2개)       2회차 (2개)       3회차 (2개)

        합계 6개 = completions</code></pre><p>기본값은 1이라 하나씩 순차 실행되지만, <code>parallelism: 2</code>를 주면 <strong>2개씩 동시에</strong> 실행된다. 6개를 처리하는 데 걸리는 시간이 줄어든다.</p>
<h3 id="activedeadlineseconds--제한-시간">activeDeadlineSeconds — 제한 시간</h3>
<pre><code>   ┌────┐                              ┌ ─ ─┐
   │Job │ ────────────────────────────▶  Job
   └────┘                              └ ─ ─┘
     시작              30초 경과        강제 종료</code></pre><p>작업이 지정한 시간 안에 끝나지 않으면 <strong>강제로 종료</strong>한다. 무한 루프에 빠지거나 응답 없는 외부 시스템을 기다리며 Pod가 영원히 남는 상황을 막는다.</p>
<h3 id="yaml-3">YAML</h3>
<pre><code class="language-yaml">apiVersion: batch/v1
kind: Job
metadata:
  name: job-1
spec:
  completions: 6
  parallelism: 2
  activeDeadlineSeconds: 30
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: container
          image: tmkube/init</code></pre>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>apiVersion: batch/v1</code></td>
<td>Job과 CronJob은 <code>apps/v1</code>이 아니라 <strong><code>batch/v1</code></strong> 을 사용한다</td>
</tr>
<tr>
<td><code>completions</code></td>
<td>성공해야 하는 Pod의 총 개수</td>
</tr>
<tr>
<td><code>parallelism</code></td>
<td>동시에 실행할 Pod의 개수</td>
</tr>
<tr>
<td><code>activeDeadlineSeconds</code></td>
<td>작업의 제한 시간(초). 초과하면 강제 종료</td>
</tr>
<tr>
<td><code>restartPolicy: Never</code></td>
<td>Pod가 실패해도 <strong>컨테이너를 재시작하지 않는다</strong></td>
</tr>
<tr>
<td><code>selector</code> 없음</td>
<td>Job은 selector를 지정하지 않는다. 쿠버네티스가 자동으로 부여한다</td>
</tr>
</tbody></table>
<p><code>restartPolicy</code>는 Job에서 반드시 지정해야 하는 값이다.</p>
<table>
<thead>
<tr>
<th>값</th>
<th>동작</th>
</tr>
</thead>
<tbody><tr>
<td><code>Never</code></td>
<td>실패 시 컨테이너를 재시작하지 않고 <strong>새 Pod를 만든다</strong></td>
</tr>
<tr>
<td><code>OnFailure</code></td>
<td>실패 시 <strong>같은 Pod 안에서 컨테이너를 재시작</strong>한다</td>
</tr>
</tbody></table>
<p><code>Always</code>는 Job에서 사용할 수 없다. 작업이 끝나도 계속 재시작하게 되어 Job의 목적과 맞지 않기 때문이다.</p>
<hr>
<h2 id="3-4-3-cronjob">3-4-3. CronJob</h2>
<h3 id="개념-5">개념</h3>
<p>CronJob은 <strong>Job을 정해진 일정에 따라 반복 생성</strong>하는 Controller다. Job을 직접 만드는 것이 아니라, <strong>Job을 만드는 Controller</strong>라는 점이 핵심이다.</p>
<pre><code>   ┌─────────────────────────────────────────┐
   │              CronJob                     │
   │   schedule : */1 * * * *                 │
   │   jobTemplate : Job                      │
   └────┬──────────────┬──────────────┬──────┘
        │              │              │
     1 Min          2 Min          3 Min
        │              │              │
        ▼              ▼              ▼
   ┌─────────┐   ┌─────────┐   ┌─────────┐
   │   Job   │   │   Job   │   │   Job   │
   └────┬────┘   └────┬────┘   └────┬────┘
        │             │             │
        ▼             ▼             ▼
   ┌─────────┐   ┌─────────┐   ┌─────────┐
   │   Pod   │   │   Pod   │   │   Pod   │
   └─────────┘   └─────────┘   └─────────┘</code></pre><p>1분마다 Job이 하나씩 생성되고, 각 Job이 Pod를 만들어 작업을 수행한다.</p>
<h3 id="schedule--일정-지정">schedule — 일정 지정</h3>
<p>리눅스의 cron 표현식을 그대로 사용한다.</p>
<pre><code>   * * * * *
   │ │ │ │ │
   │ │ │ │ └── 요일 (0~6, 0=일요일)
   │ │ │ └──── 월 (1~12)
   │ │ └────── 일 (1~31)
   │ └──────── 시 (0~23)
   └────────── 분 (0~59)</code></pre><table>
<thead>
<tr>
<th>표현식</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>*/1 * * * *</code></td>
<td>1분마다</td>
</tr>
<tr>
<td><code>0 * * * *</code></td>
<td>매시 정각</td>
</tr>
<tr>
<td><code>0 2 * * *</code></td>
<td>매일 새벽 2시</td>
</tr>
<tr>
<td><code>0 3 * * 0</code></td>
<td>매주 일요일 새벽 3시</td>
</tr>
</tbody></table>
<h3 id="concurrencypolicy--이전-작업이-안-끝났을-때">concurrencyPolicy — 이전 작업이 안 끝났을 때</h3>
<p>정해진 시간이 되었는데 <strong>이전 Job이 아직 끝나지 않은 경우</strong>를 어떻게 처리할지 정한다.</p>
<pre><code>        Allow                   Forbid                  Replace
   ┌───────────────┐     ┌───────────────┐      ┌───────────────┐
   │1min 2min 3min │     │1min 2min 3min │      │1min 2min 3min │
   │               │     │      skip     │      │               │
   │ Job  Job  Job │     │ Job   ⚠   Job │      │ Job  Job  Job │
   │  │    │    │  │     │  │        │   │      │  │ ✗  │    │  │
   │ Pod  Pod  Pod │     │ Pod ────▶ Pod │      │ Pod  Pod  Pod │
   └───────────────┘     └───────────────┘      └───────────────┘

   이전 Job과 상관없이   이전 Job이 실행 중이면   이전 Job을 삭제하고
   무조건 새로 생성       이번 차례는 건너뜀       새 Job으로 교체</code></pre><table>
<thead>
<tr>
<th>값</th>
<th>동작</th>
<th>사용하는 경우</th>
</tr>
</thead>
<tbody><tr>
<td><code>Allow</code> (기본값)</td>
<td>이전 Job과 무관하게 새 Job을 생성한다. 여러 Job이 동시에 실행될 수 있다</td>
<td>작업들이 서로 영향을 주지 않는 경우</td>
</tr>
<tr>
<td><code>Forbid</code></td>
<td>이전 Job이 실행 중이면 이번 실행을 <strong>건너뛴다</strong></td>
<td>같은 작업이 중복되면 안 되는 경우 (예: DB 백업)</td>
</tr>
<tr>
<td><code>Replace</code></td>
<td>이전 Job을 <strong>삭제하고</strong> 새 Job으로 교체한다</td>
<td>최신 결과만 중요한 경우 (예: 캐시 갱신)</td>
</tr>
</tbody></table>
<h3 id="suspend와-manual-trigger">Suspend와 Manual Trigger</h3>
<pre><code>   ┌─────────────────────────────────┐
   │           CronJob               │◀── Suspend (일시 중지)
   │   schedule : */1 * * * *        │
   │   jobTemplate : Job             │◀── Manual Trigger (수동 실행)
   └─────────────────────────────────┘</code></pre><table>
<thead>
<tr>
<th>기능</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Suspend</strong></td>
<td>스케줄을 일시 중지한다. CronJob은 남아 있지만 Job을 만들지 않는다</td>
</tr>
<tr>
<td><strong>Manual Trigger</strong></td>
<td>일정과 무관하게 지금 즉시 Job을 실행한다. 테스트나 긴급 작업에 사용</td>
</tr>
</tbody></table>
<pre><code class="language-bash"># 일시 중지
kubectl patch cronjob cron-job -p &#39;{&quot;spec&quot;:{&quot;suspend&quot;:true}}&#39;

# 수동 실행
kubectl create job --from=cronjob/cron-job manual-job-1</code></pre>
<h3 id="yaml-4">YAML</h3>
<pre><code class="language-yaml">apiVersion: batch/v1
kind: CronJob
metadata:
  name: cron-job
spec:
  schedule: &quot;*/1 * * * *&quot;
  concurrencyPolicy: Allow
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: Never
          containers:
            - name: container
              image: tmkube/app</code></pre>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>schedule</code></td>
<td>Job을 생성할 일정 (cron 표현식)</td>
</tr>
<tr>
<td><code>concurrencyPolicy</code></td>
<td>이전 Job이 실행 중일 때의 처리 방식</td>
</tr>
<tr>
<td><code>jobTemplate</code></td>
<td>생성할 <strong>Job의 내용</strong></td>
</tr>
</tbody></table>
<p>구조가 한 겹 더 깊다는 점에 주의해야 한다.</p>
<pre><code>   CronJob
     └── jobTemplate        ← Job의 정의
           └── template     ← Pod의 정의
                 └── containers</code></pre><p>CronJob이 Job을 만들고, Job이 Pod를 만들기 때문에 템플릿이 두 번 중첩된다.</p>
<hr>
<h2 id="job과-cronjob의-공통점과-차이점">Job과 CronJob의 공통점과 차이점</h2>
<h3 id="공통점">공통점</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td><strong>목적</strong></td>
<td>끝이 있는 일시적인 작업을 수행한다</td>
</tr>
<tr>
<td><strong>apiVersion</strong></td>
<td>둘 다 <code>batch/v1</code>을 사용한다</td>
</tr>
<tr>
<td><strong>Pod 종료 처리</strong></td>
<td>작업이 끝나면 Pod를 Finish로 처리하고 재시작하지 않는다</td>
</tr>
<tr>
<td><strong>자원 효율</strong></td>
<td>작업이 끝나면 자원을 반납한다</td>
</tr>
<tr>
<td><strong>restartPolicy</strong></td>
<td><code>Never</code> 또는 <code>OnFailure</code>만 사용 가능하다</td>
</tr>
<tr>
<td><strong>사용 사례</strong></td>
<td>Backup, Checking, Messaging</td>
</tr>
</tbody></table>
<h3 id="차이점">차이점</h3>
<pre><code>        Job                          CronJob
   ────────────                 ──────────────────
   ┌──────────┐                 ┌──────────┐
   │   Job    │                 │ CronJob  │
   └────┬─────┘                 └────┬─────┘
        │                            │ 일정마다
        ▼                       ┌────┼────┬────┐
   ┌──────────┐                 ▼    ▼    ▼    ▼
   │   Pod    │              ┌────┐┌────┐┌────┐
   └──────────┘              │Job ││Job ││Job │
                             └─┬──┘└─┬──┘└─┬──┘
   한 번 실행하고 끝            ▼     ▼     ▼
                             Pod   Pod   Pod

                             일정에 따라 반복</code></pre><table>
<thead>
<tr>
<th>구분</th>
<th>Job</th>
<th>CronJob</th>
</tr>
</thead>
<tbody><tr>
<td><strong>실행 시점</strong></td>
<td>생성하는 즉시 1회 실행</td>
<td><strong>일정에 따라 반복 실행</strong></td>
</tr>
<tr>
<td><strong>만드는 대상</strong></td>
<td>Pod를 직접 생성</td>
<td><strong>Job을 생성</strong> (Job이 Pod를 생성)</td>
</tr>
<tr>
<td><strong>반복 여부</strong></td>
<td>반복하지 않음</td>
<td>계속 반복</td>
</tr>
<tr>
<td><strong>고유 설정</strong></td>
<td><code>completions</code>, <code>parallelism</code>, <code>activeDeadlineSeconds</code></td>
<td><code>schedule</code>, <code>concurrencyPolicy</code>, <code>jobTemplate</code></td>
</tr>
<tr>
<td><strong>템플릿 깊이</strong></td>
<td><code>template</code> (Pod)</td>
<td><code>jobTemplate</code> → <code>template</code> (Job → Pod)</td>
</tr>
<tr>
<td><strong>사용 예</strong></td>
<td>일회성 데이터 마이그레이션</td>
<td>매일 새벽 2시 DB 백업</td>
</tr>
</tbody></table>
<p><strong>관계로 정리하면 이렇다.</strong></p>
<pre><code>   CronJob  ──생성──▶  Job  ──생성──▶  Pod
   (일정 관리)        (작업 관리)      (실제 실행)</code></pre><p>CronJob은 Job을 대체하는 것이 아니라 <strong>Job을 반복 실행해 주는 상위 개념</strong>이다. 한 번만 실행할 작업이면 Job을, 주기적으로 실행할 작업이면 CronJob을 사용한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kubernetes Object]]></title>
            <link>https://velog.io/@jina_ham/Kubernetes-Object</link>
            <guid>https://velog.io/@jina_ham/Kubernetes-Object</guid>
            <pubDate>Wed, 09 Sep 2026 11:24:06 GMT</pubDate>
            <description><![CDATA[<h1 id="2-kubernetes-object">2. Kubernetes Object</h1>
<h2 id="2-1-pod">2-1. Pod</h2>
<p>Pod는 쿠버네티스에서 애플리케이션을 배포하는 가장 작은 단위다. Pod를 이해하려면 <strong>Container</strong>, <strong>Label</strong>, <strong>Node Schedule</strong> 세 가지를 알아야 한다.</p>
<hr>
<h2 id="2-1-1-container">2-1-1. Container</h2>
<h3 id="구조">구조</h3>
<pre><code>┌──────────── Pod (IP: 10.244.1.7) ────────────┐
│                                               │
│   ┌────────────────┐   ┌────────────────┐    │
│   │  container1    │   │  container2    │    │
│   │  port 8000     │   │  port 8080     │    │
│   └────────────────┘   └────────────────┘    │
│                                               │
│      하나의 IP를 공유 → 포트는 달라야 한다    │
└───────────────────────────────────────────────┘</code></pre><h3 id="포트">포트</h3>
<p>각 컨테이너는 Service가 연결될 수 있도록 포트를 가진다. Service는 이 포트를 통해 컨테이너에 트래픽을 전달한다.</p>
<p>Pod 안의 컨테이너들은 하나의 IP를 공유하므로 <strong>같은 Pod 안에서 동일한 포트를 사용할 수 없다.</strong> 두 컨테이너가 모두 8080을 사용하면 포트 충돌이 발생한다. 위 구조처럼 8000과 8080으로 다르게 지정해야 한다.</p>
<h3 id="pod의-ip">Pod의 IP</h3>
<table>
<thead>
<tr>
<th>특성</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>할당 방식</td>
<td>Pod 생성 시 자동으로 할당된다</td>
</tr>
<tr>
<td>변경 여부</td>
<td><strong>Pod가 재생성되면 IP가 변경된다</strong></td>
</tr>
<tr>
<td>접근 범위</td>
<td>클러스터 내부에서만 이 IP로 접근할 수 있다</td>
</tr>
<tr>
<td>외부 접근</td>
<td>클러스터 외부에서는 이 IP로 접근할 수 없다</td>
</tr>
</tbody></table>
<p>Pod IP는 고정되지 않으므로 다른 애플리케이션이 Pod IP를 직접 지정해 통신하면 안 된다. 이 문제를 해결하기 위해 뒤에서 다룰 <strong>Service</strong>를 사용한다.</p>
<h3 id="yaml">YAML</h3>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: pod-1
spec:
  containers:
    - name: container1
      image: tmkube/p8000
      ports:
        - containerPort: 8000
    - name: container2
      image: tmkube/p8080
      ports:
        - containerPort: 8080</code></pre>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>kind: Pod</code></td>
<td>생성할 오브젝트의 종류</td>
</tr>
<tr>
<td><code>metadata.name</code></td>
<td>Pod의 이름. 같은 네임스페이스 안에서 중복될 수 없다</td>
</tr>
<tr>
<td><code>spec.containers</code></td>
<td>Pod 안에서 실행할 컨테이너 목록. 여러 개를 정의할 수 있다</td>
</tr>
<tr>
<td><code>image</code></td>
<td>컨테이너가 실행할 이미지</td>
</tr>
<tr>
<td><code>containerPort</code></td>
<td>컨테이너가 사용하는 포트. Pod 안에서 중복될 수 없다</td>
</tr>
</tbody></table>
<h3 id="더-알아두면-좋은-것">더 알아두면 좋은 것</h3>
<p><code>containerPort</code>는 실제로 포트를 열어주는 설정이 아니다. 애플리케이션이 어떤 포트를 사용하는지 <strong>명시하는 문서 역할</strong>에 가깝다. 실제 포트는 컨테이너 내부의 애플리케이션이 연다. 다만 이 값을 적어두면 Service를 연결하거나 다른 사람이 YAML을 볼 때 구조를 파악하기 쉬워지므로 작성하는 것이 좋다.</p>
<hr>
<h2 id="2-1-2-label">2-1-2. Label</h2>
<h3 id="개념">개념</h3>
<p>Label은 오브젝트에 붙이는 <strong>key: value 형태의 이름표</strong>다. Pod뿐 아니라 Service, Node 등 모든 오브젝트에 붙일 수 있다.</p>
<p>쿠버네티스에서 오브젝트가 많아지면 이름만으로는 관리가 어렵다. Label을 붙여두면 <strong>원하는 조건의 오브젝트만 골라내는 것</strong>이 가능해진다. 이때 조건을 지정하는 쪽이 <code>selector</code>다.</p>
<h3 id="예시-상황">예시 상황</h3>
<p>개발 환경과 운영 환경에 각각 web, db, server를 운영한다고 하자. Pod가 총 6개다.</p>
<pre><code>                      Label로 분류된 Pod들

  ┌──────────────────────────┬──────────────────────────┐
  │        lo: dev           │      lo: production      │
  ├──────────────────────────┼──────────────────────────┤
  │  ┌────────────────────┐  │  ┌────────────────────┐  │
  │  │ Pod                │  │  │ Pod                │  │
  │  │ type: web          │  │  │ type: web          │  │
  │  │ lo: dev            │  │  │ lo: production     │  │
  │  └────────────────────┘  │  └────────────────────┘  │
  │  ┌────────────────────┐  │  ┌────────────────────┐  │
  │  │ Pod                │  │  │ Pod                │  │
  │  │ type: db           │  │  │ type: db           │  │
  │  │ lo: dev            │  │  │ lo: production     │  │
  │  └────────────────────┘  │  └────────────────────┘  │
  │  ┌────────────────────┐  │  ┌────────────────────┐  │
  │  │ Pod                │  │  │ Pod                │  │
  │  │ type: server       │  │  │ type: server       │  │
  │  │ lo: dev            │  │  │ lo: production     │  │
  │  └────────────────────┘  │  └────────────────────┘  │
  └──────────────────────────┴──────────────────────────┘</code></pre><p>각 Pod는 두 개의 Label을 갖는다.</p>
<ul>
<li><code>type</code> : 어떤 역할인가 (web / db / server)</li>
<li><code>lo</code> : 어떤 환경인가 (dev / production)</li>
</ul>
<h3 id="selector로-골라내기">Selector로 골라내기</h3>
<p>Service가 <code>selector: type: web</code> 을 지정하면, <strong>type이 web인 Pod에만</strong> 연결된다.</p>
<pre><code>   Service svc-1
   selector:
     type: web
        │
        │  type: web 인 Pod만 선택
        ▼
  ┌─────────────┐   ┌─────────────┐
  │ type: web   │   │ type: web   │      ┌─────────────┐
  │ lo: dev     │   │ lo:         │      │ type: db    │  ← 선택 안 됨
  │             │   │ production  │      │ lo: dev     │
  └─────────────┘   └─────────────┘      └─────────────┘</code></pre><p>여기서 <code>lo</code>는 조건에 없으므로 dev와 production의 web Pod가 모두 선택된다. 개발용 Service를 따로 만들고 싶다면 조건을 두 개로 지정하면 된다.</p>
<pre><code class="language-yaml">selector:
  type: web
  lo: dev</code></pre>
<p>Label이 여러 개 지정되면 <strong>모두 만족하는 Pod만</strong> 선택된다.</p>
<h3 id="label을-붙이는-이유">Label을 붙이는 이유</h3>
<table>
<thead>
<tr>
<th>이유</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>연결 대상 지정</td>
<td>Service가 어떤 Pod에 트래픽을 보낼지 Label로 결정한다</td>
</tr>
<tr>
<td>환경 분리</td>
<td>개발용과 운영용 Pod를 섞이지 않게 관리할 수 있다</td>
</tr>
<tr>
<td>일괄 조회</td>
<td><code>kubectl get pods -l lo=dev</code> 처럼 조건에 맞는 Pod만 조회할 수 있다</td>
</tr>
<tr>
<td>확장성</td>
<td>Pod가 늘어나도 Label만 맞으면 Service가 자동으로 인식한다</td>
</tr>
</tbody></table>
<p>마지막 항목이 특히 중요하다. Service는 특정 Pod를 이름으로 기억하지 않는다. <strong>조건에 맞는 Pod를 그때그때 찾아서</strong> 연결한다. 그래서 Pod가 재생성되어 IP가 바뀌어도, 새 Pod에 같은 Label만 붙어 있으면 Service는 문제없이 동작한다.</p>
<h3 id="yaml-1">YAML</h3>
<p><strong>Service — 조건을 지정하는 쪽</strong></p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: svc-1
spec:
  selector:
    type: web
  ports:
    - port: 8080</code></pre>
<p><strong>Pod — Label을 붙이는 쪽</strong></p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: pod-2
  labels:
    type: web
    lo: dev
spec:
  containers:
    - name: container
      image: tmkube/init</code></pre>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>metadata.labels</code></td>
<td>Pod에 붙는 Label. key: value 형태로 여러 개 지정 가능</td>
</tr>
<tr>
<td><code>spec.selector</code></td>
<td>Service가 연결할 Pod의 조건. 지정한 Label을 모두 가진 Pod가 선택된다</td>
</tr>
<tr>
<td><code>ports.port</code></td>
<td>Service가 사용하는 포트</td>
</tr>
</tbody></table>
<h3 id="더-알아두면-좋은-것-1">더 알아두면 좋은 것</h3>
<p>Label과 비슷하게 생겼지만 목적이 다른 <strong>Annotation</strong>이 있다. Label은 오브젝트를 <strong>선택하기 위한</strong> 정보이고, Annotation은 선택에는 쓰이지 않는 <strong>부가 정보</strong>를 기록하는 용도다. 배포 일시, 담당자, 관련 문서 링크 같은 것을 Annotation에 적는다.</p>
<hr>
<h2 id="2-1-3-node-schedule">2-1-3. Node Schedule</h2>
<h3 id="개념-1">개념</h3>
<p>Pod는 반드시 어느 한 Node 위에서 실행된다. <strong>어떤 Node에서 실행할지 결정하는 과정</strong>을 Node Schedule이라고 한다.</p>
<p>방법은 두 가지다.</p>
<table>
<thead>
<tr>
<th>방법</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>직접 지정</td>
<td>사용자가 특정 Node를 지목한다</td>
</tr>
<tr>
<td>스케줄러 판단</td>
<td>쿠버네티스 스케줄러가 상황을 보고 결정한다</td>
</tr>
</tbody></table>
<h3 id="방법-1--직접-지정-nodeselector">방법 1 — 직접 지정 (nodeSelector)</h3>
<p>Node에도 Label을 붙일 수 있다. Pod에 <code>nodeSelector</code>를 지정하면 해당 Label을 가진 Node에만 배치된다.</p>
<pre><code>   Pod-3
   nodeSelector:
     hostname: node1
        │
        │ hostname: node1 인 Node를 찾는다
        ▼
  ┌──────────────────┐   ┌──────────────────┐
  │ Node1            │   │ Node2            │
  │ hostname: node1  │   │ hostname: node2  │
  │                  │   │                  │
  │   [Pod-3] ← 배치 │   │                  │
  └──────────────────┘   └──────────────────┘</code></pre><pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: pod-3
spec:
  nodeSelector:
    hostname: node1
  containers:
    - name: container
      image: tmkube/init</code></pre>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>nodeSelector</code></td>
<td>Pod를 배치할 Node의 조건. 해당 Label을 가진 Node에만 배치된다</td>
</tr>
</tbody></table>
<p><strong>주의할 점</strong>: 조건에 맞는 Node가 없거나, 그 Node에 자원이 부족하면 Pod는 배치되지 못하고 <strong>Pending 상태로 대기</strong>한다. 다른 Node로 알아서 옮겨가지 않는다.</p>
<p><strong>사용하는 경우</strong>: GPU가 달린 Node에서만 실행해야 하거나, SSD가 장착된 Node에 데이터베이스를 배치해야 하는 경우처럼 특정 Node의 조건이 필요할 때 사용한다.</p>
<h3 id="방법-2--스케줄러-판단-resources">방법 2 — 스케줄러 판단 (resources)</h3>
<p>Node를 지정하지 않으면 스케줄러가 결정한다. 이때 판단 기준이 되는 것이 <code>resources</code>다.</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: pod-4
spec:
  containers:
    - name: container
      image: tmkube/init
      resources:
        requests:
          memory: 2Gi
        limits:
          memory: 3Gi</code></pre>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>requests</code></td>
<td>Pod가 <strong>최소한 필요로 하는</strong> 자원. 스케줄러가 Node를 고를 때 이 값을 기준으로 삼는다</td>
</tr>
<tr>
<td><code>limits</code></td>
<td>Pod가 <strong>최대로 사용할 수 있는</strong> 자원. 실행 중 이 값을 넘지 못하도록 제한한다</td>
</tr>
</tbody></table>
<h3 id="스케줄러의-판단-과정">스케줄러의 판단 과정</h3>
<p>Pod-4가 <code>requests: memory 2Gi</code>를 요구하는 상황이다. 클러스터에 Node가 세 개 있다.</p>
<pre><code>  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐
  │   Node1      │  │   Node2      │  │   Node3      │
  │              │  │              │  │              │
  │ 전체 8Gi     │  │ 전체 8Gi     │  │ 전체 8Gi     │
  │ 사용중 7Gi   │  │ 사용중 2Gi   │  │ 사용중 5Gi   │
  │ 여유  1Gi    │  │ 여유  6Gi    │  │ 여유  3Gi    │
  └──────────────┘  └──────────────┘  └──────────────┘
        ✗                 ✓                 ✓
     2Gi 불가          배치 가능         배치 가능</code></pre><p><strong>1단계 — 필터링 (Filtering)</strong></p>
<p>배치가 <strong>불가능한</strong> Node를 먼저 제외한다.</p>
<ul>
<li>Node1: 여유 메모리가 1Gi뿐이라 2Gi를 확보할 수 없다 → <strong>제외</strong></li>
<li>Node2, Node3: 조건 충족 → 후보로 남는다</li>
</ul>
<p><strong>2단계 — 스코어링 (Scoring)</strong></p>
<p>남은 후보 중 <strong>가장 적합한</strong> Node를 점수로 고른다. 자원 여유가 많은 Node가 높은 점수를 받는다.</p>
<ul>
<li>Node2: 여유 6Gi → 높은 점수</li>
<li>Node3: 여유 3Gi → 낮은 점수</li>
</ul>
<p><strong>3단계 — 배치</strong></p>
<p>점수가 가장 높은 <strong>Node2에 Pod-4를 배치</strong>한다.</p>
<pre><code>  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐
  │   Node1      │  │   Node2      │  │   Node3      │
  │              │  │  [Pod-4]     │  │              │
  │ 여유  1Gi    │  │ 여유  4Gi    │  │ 여유  3Gi    │
  └──────────────┘  └──────────────┘  └──────────────┘
                        ↑ 배치됨</code></pre><p>이렇게 스케줄러는 자원 여유가 부족한 Node를 걸러내고, 남은 Node 중 가장 여유 있는 곳에 Pod를 배치해 클러스터 전체의 자원 사용을 고르게 유지한다.</p>
<h3 id="requests와-limits의-동작-차이">requests와 limits의 동작 차이</h3>
<p>두 값의 역할이 다르므로 구분해서 이해해야 한다.</p>
<table>
<thead>
<tr>
<th></th>
<th>requests</th>
<th>limits</th>
</tr>
</thead>
<tbody><tr>
<td>시점</td>
<td>Pod를 <strong>배치할 때</strong></td>
<td>Pod가 <strong>실행 중일 때</strong></td>
</tr>
<tr>
<td>역할</td>
<td>스케줄러가 Node를 고르는 기준</td>
<td>자원 사용량의 상한</td>
</tr>
<tr>
<td>초과 시</td>
<td>—</td>
<td>메모리 초과 시 컨테이너 종료 후 재시작</td>
</tr>
</tbody></table>
<p>메모리가 <code>limits</code>를 초과하면 컨테이너가 강제 종료된다. 이 상태를 <strong>OOMKilled</strong>(Out Of Memory Killed)라고 한다. CPU는 초과해도 종료되지 않고 사용량이 제한되기만 한다.</p>
<h3 id="더-알아두면-좋은-것-2">더 알아두면 좋은 것</h3>
<p><code>nodeSelector</code>는 조건이 맞지 않으면 Pod가 배치되지 않는 단순하고 엄격한 방식이다. 실무에서는 <strong>&quot;가능하면 이 Node에, 안 되면 다른 Node에라도&quot;</strong> 처럼 유연한 조건이 필요한 경우가 많다. 이런 조건은 <strong>Node Affinity</strong>로 표현하며, 이후 단계에서 다루는 개념이다. 처음에는 nodeSelector로 Node 지정의 개념을 이해하고, 이후에 Affinity로 확장해 나가면 된다.</p>
<h1 id="2-2-service">2-2. Service</h1>
<h2 id="service란-무엇이고-왜-필요한가">Service란 무엇이고 왜 필요한가</h2>
<h3 id="문제-상황--pod-ip는-신뢰할-수-없다">문제 상황 — Pod IP는 신뢰할 수 없다</h3>
<p>Pod는 언제든지 죽을 수 있다. 노드에 장애가 나거나, 새 버전을 배포하거나, 자원이 부족해 재배치되는 경우 Pod는 삭제되고 새로 생성된다.</p>
<p>이때 <strong>Pod가 재생성되면 IP가 바뀐다.</strong></p>
<pre><code>       [ 배포 전 ]                      [ Pod 재생성 후 ]

  ┌────────────────┐               ┌────────────────┐
  │ Pod-1          │               │ Pod-1 (새로)   │
  │ IP 10.244.1.7  │               │ IP 10.244.1.23 │  ← IP 변경
  └────────────────┘               └────────────────┘
         ▲                                  ▲
         │ 10.244.1.7 로 호출                │  연결 끊김 ✗
  ┌────────────────┐               ┌────────────────┐
  │ 다른 애플리케이션│               │ 다른 애플리케이션│
  └────────────────┘               └────────────────┘</code></pre><p>Pod IP를 직접 지정해 통신하면, Pod가 한 번 재생성되는 순간 연결이 끊긴다. 즉 <strong>Pod IP는 신뢰성이 없다.</strong></p>
<h3 id="해결--service가-고정된-주소를-제공한다">해결 — Service가 고정된 주소를 제공한다</h3>
<p>Service는 <strong>사용자가 직접 삭제하지 않는 한 삭제되거나 재생성되지 않는다.</strong> 따라서 Service의 IP는 변하지 않는다.</p>
<pre><code>  ┌─────────────────────────────────────────────┐
  │  Service (IP 172.96.10.17) ← 변하지 않음    │
  └──────────────────┬──────────────────────────┘
                     │ Label로 Pod를 찾아 연결
                     ▼
        ┌────────────────────────────┐
        │ Pod  IP 10.244.1.7         │
        │  ↓ 재생성                   │
        │ Pod  IP 10.244.1.23        │  ← IP가 바뀌어도
        └────────────────────────────┘     Service가 알아서 다시 연결</code></pre><p>Service는 특정 Pod를 IP로 기억하지 않고 <strong>Label 조건으로 그때그때 찾아서</strong> 연결한다. 그래서 Pod가 재생성되어 IP가 바뀌어도 새 Pod에 같은 Label만 붙어 있으면 연결이 유지된다.</p>
<p>정리하면 Service의 역할은 다음과 같다.</p>
<table>
<thead>
<tr>
<th>역할</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>고정 주소 제공</td>
<td>변하지 않는 IP와 이름으로 Pod에 접근할 수 있게 한다</td>
</tr>
<tr>
<td>Pod 추적</td>
<td>Label 조건에 맞는 현재 살아있는 Pod를 계속 추적한다</td>
</tr>
<tr>
<td>트래픽 분배</td>
<td>연결된 Pod가 여러 개면 요청을 나눠서 전달한다</td>
</tr>
</tbody></table>
<hr>
<h2 id="service의-종류">Service의 종류</h2>
<table>
<thead>
<tr>
<th>종류</th>
<th>접근 범위</th>
<th>주요 용도</th>
</tr>
</thead>
<tbody><tr>
<td><strong>ClusterIP</strong></td>
<td>클러스터 내부만</td>
<td>내부 통신, 디버깅, 대시보드</td>
</tr>
<tr>
<td><strong>NodePort</strong></td>
<td>클러스터 외부 가능 (Node IP)</td>
<td>데모, 임시 연결</td>
</tr>
<tr>
<td><strong>LoadBalancer</strong></td>
<td>클러스터 외부 가능 (전용 IP)</td>
<td>실제 외부 서비스 노출</td>
</tr>
</tbody></table>
<p>세 가지는 별개의 기능이 아니라 <strong>누적되는 구조</strong>다. NodePort는 ClusterIP의 기능을 포함하고, LoadBalancer는 NodePort의 기능을 포함한다.</p>
<pre><code>  ┌───────────────────────────────────────────┐
  │  LoadBalancer                             │
  │  ┌─────────────────────────────────────┐  │
  │  │  NodePort                           │  │
  │  │  ┌───────────────────────────────┐  │  │
  │  │  │  ClusterIP                    │  │  │
  │  │  │  (기본 - 내부 접근)           │  │  │
  │  │  └───────────────────────────────┘  │  │
  │  │  + 모든 Node에 포트 개방            │  │
  │  └─────────────────────────────────────┘  │
  │  + 외부 로드밸런서와 전용 IP              │
  └───────────────────────────────────────────┘</code></pre><hr>
<h2 id="2-2-1-clusterip">2-2-1. ClusterIP</h2>
<h3 id="특징">특징</h3>
<p>ClusterIP는 Service의 <strong>기본 타입</strong>이다. <code>type</code>을 지정하지 않으면 자동으로 ClusterIP가 된다.</p>
<table>
<thead>
<tr>
<th>특성</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>IP 할당</td>
<td>Service 생성 시 클러스터 전용 IP가 자동 할당된다</td>
</tr>
<tr>
<td>접근 범위</td>
<td><strong>클러스터 내부에서만</strong> 접근 가능하다</td>
</tr>
<tr>
<td>외부 접근</td>
<td>외부에서는 이 IP로 접근할 수 없다</td>
</tr>
<tr>
<td>수명</td>
<td>사용자가 삭제하지 않는 한 IP가 변하지 않는다</td>
</tr>
</tbody></table>
<h3 id="구조-1">구조</h3>
<pre><code>┌─────────────────── Cluster ───────────────────┐
│                                                │
│   ┌──────────────────────────────────────┐    │
│   │  Service svc-1                       │    │
│   │  IP: 172.96.10.17                    │    │
│   │  port 9000  →  targetPort 8080       │    │
│   │  selector: app: pod                  │    │
│   └───────────────┬──────────────────────┘    │
│                   │  app: pod 인 Pod를 찾음   │
│                   ▼                            │
│      ┌────────────────────────┐               │
│      │ Pod-1                  │               │
│      │ labels: app: pod       │               │
│      │ containerPort: 8080    │               │
│      └────────────────────────┘               │
│                                                │
└────────────────────────────────────────────────┘
              ▲
              │  ✗ 외부에서 172.96.10.17 접근 불가
        ┌───────────┐
        │  External │
        └───────────┘</code></pre><h3 id="포트의-흐름">포트의 흐름</h3>
<p>Service에는 포트가 두 개 등장한다. 처음 배울 때 가장 헷갈리는 부분이므로 흐름으로 이해하는 것이 좋다.</p>
<pre><code>   요청: 172.96.10.17 : 9000
                │
                │  ① port 9000 으로 Service에 도착
                ▼
        ┌───────────────┐
        │   Service     │
        │   port: 9000  │
        └───────┬───────┘
                │  ② targetPort 8080 으로 전달
                ▼
        ┌───────────────┐
        │   Pod         │
        │   8080        │
        └───────────────┘</code></pre><table>
<thead>
<tr>
<th>항목</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>port</code></td>
<td><strong>Service</strong>가 열어두는 포트. 요청을 받는 입구</td>
</tr>
<tr>
<td><code>targetPort</code></td>
<td><strong>Pod의 컨테이너</strong>가 사용하는 포트. 요청을 전달할 목적지</td>
</tr>
</tbody></table>
<h3 id="service-ip로-pod에-접근하기">Service IP로 Pod에 접근하기</h3>
<p>클러스터 내부의 다른 Pod에서 접근하는 방법이다.</p>
<pre><code class="language-bash"># Service의 IP로 접근
curl 172.96.10.17:9000

# Service의 이름으로 접근 (권장)
curl svc-1:9000</code></pre>
<p>IP 대신 <strong>Service 이름</strong>으로도 접근할 수 있다. 쿠버네티스에는 내부 DNS가 있어서 Service 이름을 IP로 변환해 준다. Service를 지우고 다시 만들면 IP는 바뀔 수 있지만 이름은 그대로이므로, 실무에서는 이름으로 접근하는 방식을 사용한다.</p>
<p>클러스터 외부에서 확인해야 할 때는 다음 명령으로 임시 터널을 열 수 있다.</p>
<pre><code class="language-bash">kubectl port-forward svc/svc-1 9000:9000</code></pre>
<h3 id="사용-사례">사용 사례</h3>
<p>ClusterIP는 외부에 노출되지 않기 때문에 안전하다. 다음과 같은 경우에 사용한다.</p>
<ul>
<li><strong>내부 트래픽</strong>: 백엔드 애플리케이션이 데이터베이스나 캐시 서버에 접근할 때</li>
<li><strong>서비스 디버깅</strong>: 개발자가 특정 Pod의 동작을 확인할 때</li>
<li><strong>대시보드, 모니터링</strong>: 내부 관리 도구에 접근할 때</li>
<li><strong>인증된 사용자 연결</strong>: 별도의 인증을 거친 사용자만 내부 경로로 연결할 때</li>
</ul>
<h3 id="yaml-2">YAML</h3>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: svc-1
spec:
  selector:
    app: pod
  ports:
    - port: 9000
      targetPort: 8080
  type: ClusterIP</code></pre>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: pod-1
  labels:
    app: pod
spec:
  containers:
    - name: container
      image: tmkube/app
      ports:
        - containerPort: 8080</code></pre>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>selector: app: pod</code></td>
<td><code>app: pod</code> Label을 가진 Pod를 찾아 연결한다</td>
</tr>
<tr>
<td><code>port: 9000</code></td>
<td>Service가 요청을 받는 포트</td>
</tr>
<tr>
<td><code>targetPort: 8080</code></td>
<td>Pod의 컨테이너로 전달할 포트</td>
</tr>
<tr>
<td><code>type: ClusterIP</code></td>
<td>클러스터 내부에서만 접근 가능. 생략 시 기본값</td>
</tr>
<tr>
<td><code>labels: app: pod</code></td>
<td>Service의 selector와 일치해야 연결된다</td>
</tr>
</tbody></table>
<p><strong>연결의 핵심</strong>은 Pod의 <code>labels</code>와 Service의 <code>selector</code>가 일치하는 것이다. 둘 중 하나라도 다르면 Service는 Pod를 찾지 못한다.</p>
<hr>
<h2 id="2-2-2-nodeport">2-2-2. NodePort</h2>
<h3 id="특징-1">특징</h3>
<p>NodePort는 <strong>ClusterIP의 기능을 그대로 가지면서</strong>, 외부에서 접근할 수 있는 통로를 추가한다.</p>
<table>
<thead>
<tr>
<th>ClusterIP에서 유지되는 것</th>
<th>NodePort에서 추가되는 것</th>
</tr>
</thead>
<tbody><tr>
<td>Service IP가 할당된다</td>
<td><strong>모든 Node에 동일한 포트가 열린다</strong></td>
</tr>
<tr>
<td>클러스터 내부에서 접근 가능하다</td>
<td>Node의 IP와 그 포트로 <strong>외부에서 접근 가능</strong>하다</td>
</tr>
<tr>
<td>Label로 Pod를 찾아 연결한다</td>
<td>포트 범위는 <strong>30000~32767</strong> 로 제한된다</td>
</tr>
</tbody></table>
<h3 id="구조-2">구조</h3>
<pre><code>┌───────────────────── Cluster ─────────────────────┐
│                                                    │
│         ┌──────────────────────────────┐          │
│         │  Service svc-2               │          │
│         │  IP 172.96.10.17 : 9000      │          │
│         └───┬──────────────────────┬───┘          │
│             │                      │              │
│  ┌──────────▼─────────┐  ┌─────────▼──────────┐  │
│  │ Node1              │  │ Node2              │  │
│  │ 192.168.56.31      │  │ 192.168.56.32      │  │
│  │   ┌──────────┐     │  │   ┌──────────┐     │  │
│  │   │   Pod    │     │  │   │   Pod    │     │  │
│  │   └──────────┘     │  │   └──────────┘     │  │
│  │   [ 30001 ]        │  │   [ 30001 ]        │  │
│  └────────┬───────────┘  └─────────┬──────────┘  │
└───────────┼────────────────────────┼─────────────┘
            │                        │
            └────────┬───────────────┘
                     │  ← 모든 Node에 같은 포트가 열린다
              ┌────────────┐
              │  External  │
              └────────────┘

  외부 접근:  192.168.56.31:30001  또는  192.168.56.32:30001
              (어느 Node로 접근해도 동일하게 동작)</code></pre><p>핵심은 <strong>&quot;모든 Node에 Port 할당&quot;</strong> 이다. Pod가 Node1에만 있어도 Node2의 30001로 접근할 수 있다. 요청을 받은 Node가 Pod가 있는 Node로 트래픽을 전달해 주기 때문이다.</p>
<h3 id="요청의-흐름">요청의 흐름</h3>
<pre><code>   외부 요청: 192.168.56.32 : 30001
                    │
                    │  ① Node2의 nodePort로 도착
                    ▼
            ┌───────────────┐
            │  Service      │  ② port 9000
            └───────┬───────┘
                    │  ③ targetPort 8080 으로 전달
                    ▼
            ┌───────────────┐
            │   Pod  8080   │
            └───────────────┘</code></pre><table>
<thead>
<tr>
<th>포트</th>
<th>위치</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><code>nodePort</code></td>
<td>Node</td>
<td>외부에서 접근하는 입구 (30000~32767)</td>
</tr>
<tr>
<td><code>port</code></td>
<td>Service</td>
<td>Service가 받는 포트</td>
</tr>
<tr>
<td><code>targetPort</code></td>
<td>Pod</td>
<td>컨테이너가 사용하는 포트</td>
</tr>
</tbody></table>
<h3 id="yaml-3">YAML</h3>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: svc-2
spec:
  selector:
    app: pod
  ports:
    - port: 9000
      targetPort: 8080
      nodePort: 30000
  type: NodePort</code></pre>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>type: NodePort</code></td>
<td>모든 Node에 포트를 열어 외부 접근을 허용한다</td>
</tr>
<tr>
<td><code>nodePort: 30000</code></td>
<td>열어둘 포트 번호. <strong>30000~32767</strong> 범위만 지정 가능하다. 생략하면 범위 안에서 자동 할당된다</td>
</tr>
</tbody></table>
<h3 id="externaltrafficpolicy">externalTrafficPolicy</h3>
<p>NodePort에는 트래픽 전달 방식을 정하는 옵션이 있다.</p>
<pre><code class="language-yaml">spec:
  externalTrafficPolicy: Local</code></pre>
<table>
<thead>
<tr>
<th>값</th>
<th>동작</th>
</tr>
</thead>
<tbody><tr>
<td><code>Cluster</code> (기본값)</td>
<td>요청을 받은 Node에 Pod가 없으면 <strong>다른 Node의 Pod로 전달</strong>한다</td>
</tr>
<tr>
<td><code>Local</code></td>
<td>요청을 받은 <strong>그 Node의 Pod에만</strong> 전달한다. 없으면 요청이 실패한다</td>
</tr>
</tbody></table>
<pre><code>   [ Cluster ]                        [ Local ]

  Node2로 요청                       Node2로 요청
      │                                   │
      │ Node2에 Pod 없음                  │ Node2에 Pod 없음
      ▼                                   ▼
  Node1의 Pod로 전달 ✓                요청 실패 ✗
  (한 번 더 이동하므로               (이동이 없어 빠르고
   클라이언트 IP가 사라짐)            클라이언트 IP가 유지됨)</code></pre><p><code>Local</code>은 불필요한 Node 간 이동을 없애고 클라이언트의 원래 IP를 알 수 있다는 장점이 있지만, Pod가 없는 Node로 요청이 오면 실패한다는 단점이 있다.</p>
<h3 id="프로덕션에서-사용하지-않는-이유">프로덕션에서 사용하지 않는 이유</h3>
<p>NodePort는 데모나 임시 연결용이며 실제 서비스 환경에는 적합하지 않다.</p>
<table>
<thead>
<tr>
<th>문제</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>포트 번호가 제한된다</td>
<td>30000~32767만 사용 가능하다. 웹 서비스의 기본 포트인 80이나 443을 쓸 수 없다</td>
</tr>
<tr>
<td>주소가 지저분하다</td>
<td>사용자가 <code>192.168.56.31:30001</code> 형태로 접속해야 한다. 도메인을 연결하기 어렵다</td>
</tr>
<tr>
<td>Node IP에 의존한다</td>
<td>Node의 IP를 사용자가 알아야 한다. Node가 교체되거나 추가되면 주소가 달라진다</td>
</tr>
<tr>
<td>장애에 취약하다</td>
<td>사용자가 접속한 Node가 죽으면 접속이 끊긴다. 다른 Node로 자동 전환되지 않는다</td>
</tr>
<tr>
<td>Node가 노출된다</td>
<td>클러스터를 구성하는 서버의 IP를 외부에 알려야 하므로 보안상 바람직하지 않다</td>
</tr>
</tbody></table>
<p>이런 문제를 해결하기 위해 실제 서비스에서는 LoadBalancer를 사용한다.</p>
<hr>
<h2 id="2-2-3-loadbalancer">2-2-3. LoadBalancer</h2>
<h3 id="특징-2">특징</h3>
<p>LoadBalancer는 <strong>NodePort의 기능을 그대로 가지면서</strong>, 외부 로드밸런서를 통해 <strong>하나의 접속 주소</strong>를 제공한다.</p>
<table>
<thead>
<tr>
<th>특성</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>외부 IP</td>
<td>로드밸런서 전용 IP가 할당된다</td>
</tr>
<tr>
<td>포트</td>
<td>원하는 포트를 사용할 수 있다 (80, 443 등)</td>
</tr>
<tr>
<td>장애 대응</td>
<td>로드밸런서가 정상 Node로만 트래픽을 보낸다</td>
</tr>
<tr>
<td>제약</td>
<td><strong>로드밸런서를 제공하는 환경이 필요하다</strong></td>
</tr>
</tbody></table>
<h3 id="구조-3">구조</h3>
<pre><code>              ┌────────────┐
              │  External  │
              └──────┬─────┘
                     │  접속 주소 하나만 알면 된다
                     ▼
        ┌────────────────────────────┐
        │      Load Balancer         │  ← 클라우드가 제공
        │      (외부 IP 할당)        │
        └──────┬──────────────┬──────┘
               │              │
┌──────────────┼──────────────┼─────────────────┐
│  Cluster     │              │                 │
│  ┌───────────▼──────┐ ┌─────▼────────────┐   │
│  │ Node1            │ │ Node2            │   │
│  │  ┌──────────┐    │ │  ┌──────────┐    │   │
│  │  │   Pod    │    │ │  │   Pod    │    │   │
│  │  └──────────┘    │ │  └──────────┘    │   │
│  └──────────────────┘ └──────────────────┘   │
└───────────────────────────────────────────────┘</code></pre><p>NodePort에서는 사용자가 Node의 IP를 직접 알아야 했지만, LoadBalancer는 <strong>로드밸런서 주소 하나만</strong> 알면 된다. 로드밸런서가 정상 동작하는 Node를 골라서 트래픽을 전달하므로, Node 하나가 죽어도 서비스가 계속된다.</p>
<h3 id="yaml-4">YAML</h3>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: svc-3
spec:
  selector:
    app: pod
  ports:
    - port: 9000
      targetPort: 8080
  type: LoadBalancer</code></pre>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>type: LoadBalancer</code></td>
<td>외부 로드밸런서를 생성하고 전용 외부 IP를 할당받는다</td>
</tr>
</tbody></table>
<p><code>nodePort</code>를 적지 않아도 내부적으로 자동 할당된다. LoadBalancer가 NodePort의 기능을 포함하고 있기 때문이다.</p>
<h3 id="사용-시-주의점">사용 시 주의점</h3>
<p>LoadBalancer는 <strong>로드밸런서를 제공해 주는 플랫폼이 있어야</strong> 동작한다. AWS, GCP, Azure 같은 클라우드 환경에서는 Service를 만드는 순간 클라우드가 로드밸런서를 자동으로 생성해 준다.</p>
<p>반면 개인 PC에 설치한 학습용 클러스터(minikube, kubeadm 등)에서는 로드밸런서를 만들어 줄 주체가 없다. 이 경우 Service의 상태를 조회하면 <code>EXTERNAL-IP</code>가 계속 <code>&lt;pending&gt;</code> 으로 남는다.</p>
<pre><code class="language-bash">$ kubectl get svc
NAME    TYPE           EXTERNAL-IP     PORT(S)
svc-3   LoadBalancer   &lt;pending&gt;       9000:31234/TCP</code></pre>
<p>또한 클라우드에서 LoadBalancer 타입 Service를 만들면 로드밸런서가 하나씩 생성되며, 이는 <strong>비용이 발생</strong>한다. Service를 여러 개 만들면 로드밸런서도 그만큼 생성되므로 실무에서는 Ingress를 함께 사용해 로드밸런서 하나로 여러 서비스를 처리하는 구성을 많이 쓴다.</p>
<hr>
<h2 id="세-가지-service-비교">세 가지 Service 비교</h2>
<pre><code>   ClusterIP              NodePort                LoadBalancer
   ─────────              ────────                ────────────
                                                  ┌──────────┐
                                                  │    LB    │
                                                  └────┬─────┘
                          ┌────┐  ┌────┐          ┌────┴─────┐
     ┌──────┐             │30001│ │30001│          │30001│30001│
     │ Svc  │             └──┬─┘  └──┬─┘          └──┬──┘└──┬─┘
     └──┬───┘             ┌──┴────┬──┴──┐          ┌──┴────┬─┴───┐
     ┌──┴───┐             │ Node1 │Node2│          │ Node1 │Node2│
     │ Pod  │             │  Pod  │ Pod │          │  Pod  │ Pod │
     └──────┘             └───────┴─────┘          └───────┴─────┘

   내부만 접근           Node IP로 접근          전용 IP 하나로 접근</code></pre><table>
<thead>
<tr>
<th>구분</th>
<th>ClusterIP</th>
<th>NodePort</th>
<th>LoadBalancer</th>
</tr>
</thead>
<tbody><tr>
<td>외부 접근</td>
<td>불가</td>
<td>가능</td>
<td>가능</td>
</tr>
<tr>
<td>접속 주소</td>
<td>Service IP (내부)</td>
<td>Node IP : 30000~32767</td>
<td>로드밸런서 전용 IP</td>
</tr>
<tr>
<td>포트 제약</td>
<td>없음</td>
<td>30000~32767</td>
<td>없음</td>
</tr>
<tr>
<td>Node 장애 시</td>
<td>—</td>
<td>해당 Node 접속 끊김</td>
<td>다른 Node로 자동 전환</td>
</tr>
<tr>
<td>별도 환경 필요</td>
<td>없음</td>
<td>없음</td>
<td>로드밸런서 제공 환경 필요</td>
</tr>
<tr>
<td>주요 용도</td>
<td>내부 통신, 디버깅, 대시보드</td>
<td>데모, 임시 연결</td>
<td>실제 외부 서비스 노출</td>
</tr>
</tbody></table>
<h1 id="2-3-volume">2-3. Volume</h1>
<h2 id="volume이란-무엇이고-왜-필요한가">Volume이란 무엇이고 왜 필요한가</h2>
<p>컨테이너 안에 저장한 데이터는 <strong>컨테이너가 재시작되면 사라진다.</strong> 컨테이너는 이미지를 기반으로 매번 새로 만들어지기 때문에, 실행 중에 쓴 파일은 남지 않는다.</p>
<pre><code>   ┌──────────────┐          ┌──────────────┐
   │ Container    │          │ Container    │
   │              │  재시작  │  (새로 생성) │
   │ /data/a.txt  │  ──────▶ │              │  ← 파일 사라짐
   └──────────────┘          └──────────────┘</code></pre><p>Volume은 이 문제를 해결하기 위해 <strong>컨테이너 외부에 데이터를 저장하는 공간</strong>을 컨테이너에 연결해 주는 오브젝트다.</p>
<p>Volume에는 세 가지 방식이 있으며, <strong>데이터가 얼마나 오래 살아남는지</strong>가 다르다.</p>
<table>
<thead>
<tr>
<th>방식</th>
<th>데이터가 유지되는 범위</th>
</tr>
</thead>
<tbody><tr>
<td><strong>emptyDir</strong></td>
<td>Pod가 살아있는 동안만</td>
</tr>
<tr>
<td><strong>hostPath</strong></td>
<td>Node가 살아있는 동안</td>
</tr>
<tr>
<td><strong>PVC / PV</strong></td>
<td>클러스터와 무관하게 영구 보존</td>
</tr>
</tbody></table>
<pre><code>   emptyDir          hostPath           PVC / PV
   ────────          ────────           ────────
   Pod 삭제 시       Node 삭제 시       외부 저장소에 보관
   사라짐            사라짐             계속 남음

   [ 짧음 ] ──────────────────────────────▶ [ 김 ]
                    데이터 수명</code></pre><hr>
<h2 id="2-3-1-emptydir">2-3-1. emptyDir</h2>
<h3 id="특징-3">특징</h3>
<p>emptyDir은 <strong>Pod 안에 생성되는 볼륨</strong>이다.</p>
<table>
<thead>
<tr>
<th>특성</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>생성 시점</td>
<td>Pod가 생성될 때 만들어진다</td>
</tr>
<tr>
<td>삭제 시점</td>
<td><strong>Pod가 삭제될 때 함께 사라진다</strong></td>
</tr>
<tr>
<td>초기 상태</td>
<td>이름 그대로 <strong>최초에는 비어 있다</strong></td>
</tr>
<tr>
<td>용도</td>
<td>Pod 안의 컨테이너끼리 데이터를 공유한다</td>
</tr>
</tbody></table>
<p>Pod와 생명주기가 같기 때문에, 영구 보관이 필요 없는 <strong>일시적 목적의 데이터</strong>를 담는다.</p>
<h3 id="구조-4">구조</h3>
<pre><code>┌─────────────────── Pod ───────────────────┐
│                                            │
│                    ┌──────────────────┐   │
│                ┌──▶│  Container1      │   │
│  ┌──────────┐  │   │  mountPath:      │   │
│  │  Volume  │──┤   │   /mount1        │   │
│  │(emptyDir)│  │   └──────────────────┘   │
│  └──────────┘  │   ┌──────────────────┐   │
│                └──▶│  Container2      │   │
│                    │  mountPath:      │   │
│                    │   /mount2        │   │
│                    └──────────────────┘   │
│                                            │
│   Pod 생성 시 만들어지고, 삭제 시 없어짐   │
└────────────────────────────────────────────┘</code></pre><p>같은 Volume을 두 컨테이너가 각각 다른 경로에 마운트했다. Container1이 <code>/mount1</code>에 파일을 쓰면 Container2는 <code>/mount2</code>에서 그 파일을 읽을 수 있다. <strong>경로 이름은 달라도 실체는 같은 저장 공간</strong>이다.</p>
<h3 id="어떤-데이터를-넣는가">어떤 데이터를 넣는가</h3>
<p>Pod가 사라지면 없어져도 괜찮은 데이터를 넣는다.</p>
<table>
<thead>
<tr>
<th>예시</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>로그 파일</td>
<td>애플리케이션이 로그를 쓰고, 사이드카 컨테이너가 읽어서 외부로 전송</td>
</tr>
<tr>
<td>파일 변환 작업</td>
<td>업로드된 이미지를 한 컨테이너가 받아두고, 다른 컨테이너가 썸네일로 변환</td>
</tr>
<tr>
<td>캐시 데이터</td>
<td>다시 계산하면 되는 임시 연산 결과</td>
</tr>
<tr>
<td>컨테이너 간 신호 파일</td>
<td>초기화 완료 여부를 파일로 표시해 다른 컨테이너에 알림</td>
</tr>
</tbody></table>
<p>반대로 <strong>주문 내역, 회원 정보, 업로드된 원본 파일</strong>처럼 사라지면 안 되는 데이터는 절대 emptyDir에 두면 안 된다.</p>
<h3 id="yaml-5">YAML</h3>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: pod-volume-1
spec:
  containers:
    - name: container1
      image: tmkube/init
      volumeMounts:
        - name: empty-dir
          mountPath: /mount1
    - name: container2
      image: tmkube/init
      volumeMounts:
        - name: empty-dir
          mountPath: /mount2
  volumes:
    - name: empty-dir
      emptyDir: {}</code></pre>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>volumes</code></td>
<td>Pod 수준에서 사용할 볼륨을 정의한다</td>
</tr>
<tr>
<td><code>emptyDir: {}</code></td>
<td>빈 볼륨을 만든다. 별도 설정이 필요 없어 <code>{}</code> 로 비워둔다</td>
</tr>
<tr>
<td><code>volumeMounts.name</code></td>
<td>위에서 정의한 볼륨의 이름과 일치해야 한다</td>
</tr>
<tr>
<td><code>mountPath</code></td>
<td>컨테이너 내부에서 이 볼륨이 보일 경로</td>
</tr>
</tbody></table>
<p><code>volumes</code>에서 볼륨을 <strong>정의</strong>하고, <code>volumeMounts</code>에서 각 컨테이너에 <strong>연결</strong>하는 두 단계 구조다.</p>
<hr>
<h2 id="2-3-2-hostpath">2-3-2. hostPath</h2>
<h3 id="특징-4">특징</h3>
<p>hostPath는 <strong>Pod가 올라가 있는 Node의 경로</strong>를 볼륨으로 사용한다.</p>
<table>
<thead>
<tr>
<th>특성</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>저장 위치</td>
<td>Node의 파일시스템</td>
</tr>
<tr>
<td>데이터 유지</td>
<td><strong>Pod가 죽어도 Node의 데이터는 사라지지 않는다</strong></td>
</tr>
<tr>
<td>공유 범위</td>
<td>같은 Node에 있는 Pod들이 같은 데이터를 본다</td>
</tr>
<tr>
<td>제약</td>
<td>Node마다 별도의 저장 공간이므로 <strong>Node가 바뀌면 데이터를 잃는다</strong></td>
</tr>
</tbody></table>
<h3 id="구조-5">구조</h3>
<pre><code>┌─────────────── Node1 ────────────────┐
│                                       │
│   ┌──────────────┐    ┌───────────┐  │
│   │   Volume     │───▶│   Pod1    │  │
│   │  /node-v1    │    │  /data    │  │
│   │              │    └───────────┘  │
│   │  (Node의     │    ┌───────────┐  │
│   │   실제 경로) │───▶│   Pod2    │  │
│   └──────────────┘    │  /data    │  │
│                       └───────────┘  │
└───────────────────────────────────────┘
        │
        │  Pod2가 죽고 Node2에 재생성되면?
        ▼
┌─────────────── Node2 ────────────────┐
│                                       │
│   ┌──────────────┐    ┌───────────┐  │
│   │   Volume     │───▶│   Pod2    │  │
│   │  /node-v1    │    │  /data    │  │
│   │              │    └───────────┘  │
│   │  ← 비어 있음 │                   │
│   └──────────────┘                   │
└───────────────────────────────────────┘
       Node1의 데이터는 볼 수 없다</code></pre><p><strong>이것이 hostPath의 가장 큰 함정이다.</strong> Pod가 재생성될 때 다른 Node에 배치되면, 그 Node의 경로에는 이전 데이터가 없다. 같은 경로 이름을 썼더라도 물리적으로 다른 디스크이기 때문이다.</p>
<p>또한 Node를 추가할 때마다 <strong>해당 경로를 직접 만들고 마운트를 걸어줘야 한다.</strong> Node가 늘어날수록 관리 부담이 커진다.</p>
<h3 id="어떤-데이터를-넣는가-1">어떤 데이터를 넣는가</h3>
<p>hostPath는 애플리케이션 데이터 저장용이 아니라, <strong>Node 자체의 정보에 접근</strong>하기 위해 사용한다.</p>
<table>
<thead>
<tr>
<th>예시</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>Node 로그 수집</td>
<td><code>/var/log</code> 를 마운트해 Node의 시스템 로그를 읽음</td>
</tr>
<tr>
<td>Node 모니터링</td>
<td><code>/proc</code>, <code>/sys</code> 를 마운트해 CPU·메모리 사용량 수집</td>
</tr>
<tr>
<td>컨테이너 런타임 접근</td>
<td><code>/var/run/docker.sock</code> 등에 접근</td>
</tr>
<tr>
<td>Node 설정 파일 참조</td>
<td>Node에 있는 인증서나 설정 파일을 읽음</td>
</tr>
</tbody></table>
<p>이런 작업은 <strong>Node마다 하나씩 Pod를 띄우는 방식(DaemonSet)</strong> 과 함께 쓰인다. 모든 Node에 로그 수집기를 하나씩 배치하는 구성이 대표적이다.</p>
<h3 id="yaml-6">YAML</h3>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: pod-volume-2
spec:
  containers:
    - name: container
      image: tmkube/init
      volumeMounts:
        - name: host-path
          mountPath: /mount1
  volumes:
    - name: host-path
      hostPath:
        path: /node-v
        type: Directory</code></pre>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>hostPath.path</code></td>
<td>볼륨으로 사용할 <strong>Node의 실제 경로</strong></td>
</tr>
<tr>
<td><code>type: Directory</code></td>
<td>해당 경로가 디렉터리로 <strong>이미 존재해야 한다</strong>는 의미</td>
</tr>
</tbody></table>
<p><code>type: Directory</code>로 지정하면 사전에 그 경로가 Node에 없을 경우 Pod가 생성되지 않는다. 경로가 없을 때 자동으로 만들려면 <code>DirectoryOrCreate</code>를 사용한다.</p>
<hr>
<h2 id="2-3-3-pvc--pv">2-3-3. PVC / PV</h2>
<h3 id="왜-필요한가">왜 필요한가</h3>
<p>emptyDir은 Pod와 함께 사라지고, hostPath는 Node에 묶여 있다. 데이터베이스 데이터처럼 <strong>어떤 상황에도 사라지면 안 되는 데이터</strong>는 두 방식 모두 적합하지 않다.</p>
<p>PV와 PVC는 클러스터 외부의 저장소(NFS, AWS EBS, iSCSI 등)를 연결해 <strong>영속성 있는 볼륨</strong>을 제공한다.</p>
<pre><code>   emptyDir              hostPath              PV / PVC
   ────────              ────────              ────────
   Pod 안에 저장          Node에 저장           외부 저장소에 저장
        │                     │                      │
   Pod 삭제 → 소멸       Node 변경 → 소멸       Pod·Node와 무관하게 보존</code></pre><h3 id="두-오브젝트의-역할-분리">두 오브젝트의 역할 분리</h3>
<p>PV와 PVC가 나뉘어 있는 이유는 <strong>관리자와 사용자의 역할을 분리</strong>하기 위해서다.</p>
<pre><code>        User (개발자)                    Admin (인프라 관리자)
   ───────────────────────         ────────────────────────────
   &quot;1Gi 저장 공간이 필요해요&quot;      &quot;실제 저장소를 준비해뒀습니다&quot;
            │                                   │
           PVC                                 PV
     (요청서 / 신청서)               (실제 저장소 연결 정보)</code></pre><table>
<thead>
<tr>
<th>오브젝트</th>
<th>담당</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><strong>PV</strong> (PersistentVolume)</td>
<td>Admin</td>
<td>실제 저장소를 클러스터에 등록한다. 용량, 접근 방식, 저장소 위치를 정의</td>
</tr>
<tr>
<td><strong>PVC</strong> (PersistentVolumeClaim)</td>
<td>User</td>
<td>필요한 용량과 조건을 요청한다. 실제 저장소가 무엇인지 몰라도 된다</td>
</tr>
</tbody></table>
<p>개발자는 저장소가 NFS인지 AWS EBS인지 알 필요 없이 &quot;1Gi가 필요하다&quot;고 요청만 하면 된다. 저장소 종류가 바뀌어도 PVC는 수정할 필요가 없다.</p>
<h3 id="생성-과정">생성 과정</h3>
<pre><code>┌────────────── User ──────────────┐   ┌───────────── Admin ─────────────┐
│                                   │   │                                  │
│                                   │   │  ┌────────────────────┐         │
│                                   │   │  │ PersistentVolume   │  ┌─────┐│
│                                   │   │  │ pv-01              │──│ 실제││
│                                   │   │  │ 용량 1Gi           │  │저장소││
│                                   │   │  └─────────┬──────────┘  │ NFS ││
│                                   │   │            │             │ AWS ││
│         ┌──────────────────┐      │   │            │             │iSCSI││
│         │ PersistentVolume │◀─────┼───┼────────────┘             └─────┘│
│         │ Claim  pvc-01    │  ③   │   │  ┌────────────────────┐         │
│         │ 요청 1Gi         │      │   │  │ PersistentVolume   │         │
│         └────────┬─────────┘      │   │  │ pv-02              │         │
│                  │  ②             │   │  └────────────────────┘         │
│         ┌────────▼─────────┐      │   │            ①                    │
│         │      Pod         │      │   │                                  │
│         │  PVC 마운트  ④   │      │   │                                  │
│         └──────────────────┘      │   │                                  │
└───────────────────────────────────┘   └──────────────────────────────────┘</code></pre><p><strong>① PV 정의 생성 (Admin)</strong></p>
<p>관리자가 실제 저장소를 PV로 등록한다. &quot;1Gi 짜리 저장 공간이 이 위치에 있다&quot;는 정보다.</p>
<p><strong>② PVC 생성 (User)</strong></p>
<p>개발자가 &quot;1Gi가 필요하다&quot;는 요청서를 만든다.</p>
<p><strong>③ PV 연결 (자동)</strong></p>
<p>쿠버네티스가 조건에 맞는 PV를 찾아 PVC와 연결한다. 이 연결을 <strong>바인딩(Binding)</strong> 이라고 한다. 조건에 맞는 PV가 없으면 PVC는 <code>Pending</code> 상태로 대기한다.</p>
<p><strong>④ Pod 생성 시 PVC 마운트 (User)</strong></p>
<p>Pod에서 PVC를 볼륨으로 지정하면, 연결된 PV의 실제 저장소를 사용하게 된다.</p>
<p>Pod는 PV를 직접 참조하지 않고 항상 <strong>PVC를 통해서만</strong> 접근한다.</p>
<h3 id="어떤-데이터를-넣는가-2">어떤 데이터를 넣는가</h3>
<p>사라지면 안 되는 모든 데이터를 넣는다.</p>
<table>
<thead>
<tr>
<th>예시</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>데이터베이스 데이터</td>
<td>MySQL, PostgreSQL의 실제 데이터 파일</td>
</tr>
<tr>
<td>사용자 업로드 파일</td>
<td>프로필 이미지, 첨부 문서</td>
</tr>
<tr>
<td>메시지 큐 데이터</td>
<td>Kafka, RabbitMQ의 저장 데이터</td>
</tr>
<tr>
<td>애플리케이션 영구 로그</td>
<td>감사(audit) 목적으로 보존해야 하는 기록</td>
</tr>
</tbody></table>
<h3 id="yaml-7">YAML</h3>
<p><strong>PV — 관리자가 저장소를 등록</strong></p>
<pre><code class="language-yaml">apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-01
spec:
  capacity:
    storage: 1G
  accessModes:
    - ReadWriteOnce
  local:
    path: /node-v
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - {key: kubernetes.io/hostname, operator: In, values: [node1]}</code></pre>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>capacity.storage</code></td>
<td>이 PV가 제공하는 용량</td>
</tr>
<tr>
<td><code>accessModes</code></td>
<td>접근 방식 (아래 표 참고)</td>
</tr>
<tr>
<td><code>local.path</code></td>
<td>저장소의 실제 경로</td>
</tr>
<tr>
<td><code>nodeAffinity</code></td>
<td>이 PV를 사용하는 <strong>Pod가 해당 Node에 만들어지도록</strong> 지정</td>
</tr>
</tbody></table>
<p><code>local</code> 타입은 특정 Node의 디스크를 사용하므로, 그 Node에서만 접근 가능하다. 그래서 <code>nodeAffinity</code>로 Pod가 반드시 node1에 배치되도록 강제한다.</p>
<p><strong>PVC — 사용자가 저장 공간을 요청</strong></p>
<pre><code class="language-yaml">apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-01
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1G
  storageClassName: &quot;&quot;</code></pre>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>resources.requests.storage</code></td>
<td>요청하는 용량</td>
</tr>
<tr>
<td><code>accessModes</code></td>
<td>요청하는 접근 방식. <strong>PV와 일치해야 연결된다</strong></td>
</tr>
<tr>
<td><code>storageClassName: &quot;&quot;</code></td>
<td>자동 생성을 사용하지 않고, 이미 만들어진 PV와 연결한다는 의미</td>
</tr>
</tbody></table>
<p><strong>Pod — PVC를 볼륨으로 사용</strong></p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: pod-volume-3
spec:
  containers:
    - name: container
      image: tmkube/init
      volumeMounts:
        - name: pvc-pv
          mountPath: /volume
  volumes:
    - name: pvc-pv
      persistentVolumeClaim:
        claimName: pvc-01</code></pre>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>persistentVolumeClaim.claimName</code></td>
<td>사용할 PVC의 이름</td>
</tr>
</tbody></table>
<h3 id="accessmodes">accessModes</h3>
<p>PV와 PVC를 연결할 때 반드시 일치해야 하는 값이다.</p>
<table>
<thead>
<tr>
<th>값</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>ReadWriteOnce</code> (RWO)</td>
<td><strong>하나의 Node</strong>에서 읽기·쓰기 가능</td>
</tr>
<tr>
<td><code>ReadOnlyMany</code> (ROX)</td>
<td>여러 Node에서 읽기만 가능</td>
</tr>
<tr>
<td><code>ReadWriteMany</code> (RWX)</td>
<td>여러 Node에서 읽기·쓰기 가능</td>
</tr>
</tbody></table>
<p><code>ReadWriteOnce</code>가 가장 일반적이며, 데이터베이스처럼 하나의 Pod만 데이터를 쓰는 경우에 사용한다. 여러 Pod가 동시에 같은 파일에 써야 한다면 <code>ReadWriteMany</code>를 지원하는 저장소(NFS 등)가 필요하다.</p>
<hr>
<h2 id="세-가지-volume-비교">세 가지 Volume 비교</h2>
<pre><code>   emptyDir                hostPath                 PVC / PV
   ────────                ────────                 ────────
  ┌──────────┐          ┌──────────────┐        ┌──────────────┐
  │   Pod    │          │    Node      │        │    Pod       │
  │ ┌──────┐ │          │  ┌────────┐  │        │      │       │
  │ │Volume│ │          │  │  Pod   │  │        │     PVC      │
  │ └──────┘ │          │  └───┬────┘  │        │      │       │
  └──────────┘          │  ┌───▼────┐  │        │      PV      │
                        │  │ /path  │  │        └──────┼───────┘
                        │  └────────┘  │               ▼
                        └──────────────┘        ┌──────────────┐
                                                │ 외부 저장소  │
                                                │ NFS / AWS    │
                                                └──────────────┘</code></pre><table>
<thead>
<tr>
<th>구분</th>
<th>emptyDir</th>
<th>hostPath</th>
<th>PVC / PV</th>
</tr>
</thead>
<tbody><tr>
<td>저장 위치</td>
<td>Pod 내부</td>
<td>Node의 파일시스템</td>
<td>외부 저장소</td>
</tr>
<tr>
<td>Pod 삭제 시</td>
<td>데이터 소멸</td>
<td>데이터 유지</td>
<td>데이터 유지</td>
</tr>
<tr>
<td>Node 변경 시</td>
<td>—</td>
<td><strong>데이터 접근 불가</strong></td>
<td>데이터 유지</td>
</tr>
<tr>
<td>설정 난이도</td>
<td>매우 간단</td>
<td>간단</td>
<td>PV·PVC를 각각 정의해야 함</td>
</tr>
<tr>
<td>관리 주체</td>
<td>개발자</td>
<td>개발자</td>
<td>Admin(PV) + User(PVC)</td>
</tr>
<tr>
<td>주요 용도</td>
<td>컨테이너 간 임시 데이터 공유</td>
<td>Node 로그·모니터링 정보 접근</td>
<td>DB, 업로드 파일 등 영구 데이터</td>
</tr>
</tbody></table>
<p><strong>선택 기준</strong>은 다음과 같이 정리할 수 있다.</p>
<ul>
<li>데이터가 Pod와 함께 사라져도 되는가 → <strong>emptyDir</strong></li>
<li>Node 자체의 파일에 접근해야 하는가 → <strong>hostPath</strong></li>
<li>데이터가 절대 사라지면 안 되는가 → <strong>PVC / PV</strong></li>
</ul>
<h1 id="2-4-configmap과-secret">2-4. ConfigMap과 Secret</h1>
<h2 id="왜-필요한가-1">왜 필요한가</h2>
<h3 id="문제-상황--환경마다-값이-다르다">문제 상황 — 환경마다 값이 다르다</h3>
<p>같은 애플리케이션이라도 개발 환경과 운영 환경에서 설정 값이 달라야 하는 경우가 많다.</p>
<table>
<thead>
<tr>
<th>설정 항목</th>
<th>Dev</th>
<th>Production</th>
</tr>
</thead>
<tbody><tr>
<td>SSH</td>
<td>False</td>
<td>True</td>
</tr>
<tr>
<td>User</td>
<td>Dev</td>
<td>Prod</td>
</tr>
<tr>
<td>Key</td>
<td>LS0tLs..</td>
<td>MII3Ld..</td>
</tr>
</tbody></table>
<p>이 값들을 컨테이너 이미지 안에 넣어두면 어떻게 될까.</p>
<pre><code>   ┌──────────────────────┐    ┌──────────────────────┐
   │ Container for Dev    │    │ Container for Prod   │
   │  ┌────────────────┐  │    │  ┌────────────────┐  │
   │  │  A Service     │  │    │  │  A Service     │  │
   │  │  SSH  : False  │  │    │  │  SSH  : True   │  │
   │  │  User : Dev    │  │    │  │  User : Prod   │  │
   │  │  Key  : LS0tLs.│  │    │  │  Key  : MII3Ld.│  │
   │  └────────────────┘  │    │  └────────────────┘  │
   └──────────────────────┘    └──────────────────────┘

        환경마다 이미지를 따로 만들어야 한다</code></pre><p>이 방식에는 두 가지 문제가 있다.</p>
<ul>
<li><strong>이미지가 환경 수만큼 늘어난다.</strong> 개발·스테이징·운영이 있다면 이미지가 3개다. 코드는 완전히 같은데 설정만 다른 이미지를 각각 관리해야 한다.</li>
<li><strong>개발 환경에서 검증한 이미지가 운영에 그대로 가지 않는다.</strong> 운영용 이미지는 별도로 빌드된 것이므로, 테스트한 것과 다른 이미지가 배포된다.</li>
</ul>
<h3 id="해결--설정을-이미지-밖으로-분리한다">해결 — 설정을 이미지 밖으로 분리한다</h3>
<p>이미지는 <strong>값을 비워둔 채 하나만</strong> 만들고, 실제 값은 배포 시점에 외부에서 주입한다.</p>
<pre><code>   ┌──────────────────────┐
   │  Container (하나)    │
   │  ┌────────────────┐  │
   │  │  A Service     │  │
   │  │  SSH  : [    ] │  │  ← 값이 비어 있음
   │  │  User : [    ] │  │
   │  │  Key  : [    ] │  │
   │  └────────────────┘  │
   └──────────────────────┘
              │
      ┌───────┴────────┐
      ▼                ▼
   [ Dev 값 주입 ]  [ Prod 값 주입 ]</code></pre><p>이때 값을 담아두는 오브젝트가 <strong>ConfigMap</strong>과 <strong>Secret</strong>이다.</p>
<h3 id="configmap과-secret의-구분">ConfigMap과 Secret의 구분</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>ConfigMap</th>
<th>Secret</th>
</tr>
</thead>
<tbody><tr>
<td>담는 값</td>
<td>일반 상수</td>
<td>보안 관리가 필요한 값</td>
</tr>
<tr>
<td>예시</td>
<td>SSH 여부, 사용자명, 포트, 로그 레벨, API 주소</td>
<td>비밀번호, 인증서, API 키, DB 접속 정보</td>
</tr>
<tr>
<td>저장 형식</td>
<td>평문</td>
<td><strong>Base64 인코딩</strong></td>
</tr>
<tr>
<td>저장 위치</td>
<td>일반 저장</td>
<td><strong>메모리(tmpfs)에 저장</strong></td>
</tr>
</tbody></table>
<h3 id="전체-구조">전체 구조</h3>
<pre><code>┌──────────── Dev ────────────┐   ┌──────── Production ─────────┐
│                              │   │                              │
│  ┌────────────┐              │   │  ┌────────────┐              │
│  │ ConfigMap  │              │   │  │ ConfigMap  │              │
│  │ SSH : False│──┐           │   │  │ SSH : True │──┐           │
│  │ User: Dev  │  │           │   │  │ User: Prod │  │           │
│  └────────────┘  │  ┌──────┐ │   │  └────────────┘  │  ┌──────┐ │
│                  ├─▶│ Pod  │ │   │                  ├─▶│ Pod  │ │
│  ┌────────────┐  │  │ Env  │ │   │  ┌────────────┐  │  │ Env  │ │
│  │  Secret    │  │  └──────┘ │   │  │  Secret    │  │  └──────┘ │
│  │ Key:LS0tLs.│──┘           │   │  │ Key:MII3Ld.│──┘           │
│  └────────────┘              │   │  └────────────┘              │
└──────────────────────────────┘   └──────────────────────────────┘

        같은 이미지 + 다른 ConfigMap/Secret = 다른 동작</code></pre><p>Pod를 생성할 때 ConfigMap과 Secret의 값을 가져와 <strong>컨테이너의 환경변수에 세팅</strong>한다. 이미지는 하나지만 어떤 ConfigMap을 붙이느냐에 따라 Dev용 Pod가 되기도 하고 Production용 Pod가 되기도 한다.</p>
<hr>
<h2 id="secret이-메모리에-저장되는-이유">Secret이 메모리에 저장되는 이유</h2>
<p>Secret은 Node의 디스크가 아니라 <strong>메모리(tmpfs)</strong> 에 저장된다. 보안상의 이유 때문이다.</p>
<pre><code>   [ 디스크에 저장한다면 ]              [ 메모리에 저장하면 ]

   ┌──────────────────┐               ┌──────────────────┐
   │  Node 디스크     │               │  Node 메모리     │
   │  password=1234   │               │  password=1234   │
   └──────────────────┘               └──────────────────┘
            │                                   │
   Node 종료 후에도 파일이 남음          Node 종료 시 함께 사라짐
   디스크를 확보하면 값을 볼 수 있음     흔적이 남지 않음</code></pre><table>
<thead>
<tr>
<th>이유</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>흔적이 남지 않는다</td>
<td>디스크에 쓰면 파일 삭제 후에도 복구가 가능하다. 메모리는 전원이 꺼지면 내용이 사라진다</td>
</tr>
<tr>
<td>물리적 유출에 대비한다</td>
<td>디스크를 물리적으로 확보당해도 Secret 값을 읽을 수 없다</td>
</tr>
<tr>
<td>백업에 포함되지 않는다</td>
<td>디스크 백업이나 스냅샷을 뜰 때 Secret 값이 함께 복사되지 않는다</td>
</tr>
<tr>
<td>사용 범위를 최소화한다</td>
<td>Secret은 그 값을 필요로 하는 Pod가 있는 Node에만 전달되고, Pod가 삭제되면 즉시 제거된다</td>
</tr>
</tbody></table>
<p>Secret에는 크기 제한이 있다. <strong>하나의 Secret은 1MByte를 넘을 수 없다.</strong> 메모리에 저장되기 때문에, 큰 데이터를 담으면 Node의 메모리를 잠식할 수 있어서다.</p>
<h3 id="base64-인코딩">Base64 인코딩</h3>
<p>Secret에 값을 넣을 때는 <strong>Base64로 인코딩해서</strong> 넣는다.</p>
<pre><code>   원본 값              Base64 인코딩         Secret에 저장
   &quot;1234&quot;      ──────▶   &quot;MTIzNA==&quot;    ──────▶  data.Key
                                                     │
                                                     │ Pod에 주입 시
                                                     │ 자동 디코딩
                                                     ▼
                                              컨테이너 환경변수
                                                  PW = 1234</code></pre><p>컨테이너에 전달될 때는 쿠버네티스가 <strong>자동으로 디코딩</strong>하므로, 애플리케이션은 원본 값을 그대로 받는다.</p>
<p>여기서 반드시 알아야 할 점이 있다. <strong>Base64는 암호화가 아니다.</strong> 누구나 되돌릴 수 있는 단순 변환이다.</p>
<pre><code class="language-bash">$ echo &quot;MTIzNA==&quot; | base64 --decode
1234</code></pre>
<p>Base64를 쓰는 이유는 보안이 아니라, 인증서처럼 줄바꿈이나 특수문자가 섞인 값을 YAML에 안전하게 담기 위해서다. 따라서 <strong>Secret이 담긴 YAML 파일을 Git에 그대로 올리면 안 된다.</strong> 실무에서는 별도의 비밀 관리 도구(Sealed Secrets, Vault 등)를 함께 사용한다.</p>
<hr>
<h2 id="값을-주입하는-세-가지-방법">값을 주입하는 세 가지 방법</h2>
<p>ConfigMap과 Secret의 값을 컨테이너에 전달하는 방법은 세 가지다.</p>
<pre><code>   ① Env (Literal)        ② Env (File)         ③ Volume Mount (File)
   ───────────────        ────────────         ─────────────────────
   상수 값을              파일 내용을           파일을 파일 그대로
   환경변수로             환경변수로            컨테이너에 마운트

   SSH = False            file = (파일 내용)    /mount/file.txt</code></pre><hr>
<h2 id="2-4-1-env-literal--상수를-환경변수로">2-4-1. Env (Literal) — 상수를 환경변수로</h2>
<h3 id="개념-2">개념</h3>
<p>가장 기본적인 방식이다. ConfigMap과 Secret에 <strong>key: value 형태의 상수</strong>를 넣고, 이를 컨테이너의 환경변수로 주입한다.</p>
<h3 id="구조-6">구조</h3>
<pre><code>  ┌─────────────────┐        ┌─────────────────┐
  │   ConfigMap     │        │     Secret      │
  │   cm-dev        │        │     sec-dev     │
  │                 │        │                 │
  │  Key   : Value  │        │  Key   : Value  │
  │  SSH   : False  │        │  PW    : Base64 │  ← 메모리 1Mbyte
  │  user  : dev    │        │                 │
  └────────┬────────┘        └────────┬────────┘
           │                          │
           │                          │  Decoding
           ▼                          ▼
  ┌──────────────────────────────────────────┐
  │                  Pod                      │
  │  ┌────────────────────────────────────┐  │
  │  │            Container               │  │
  │  │  env                               │  │
  │  │   SSH  : False                     │  │
  │  │   user : dev                       │  │
  │  │   PW   : 1234   ← 자동 디코딩됨    │  │
  │  └────────────────────────────────────┘  │
  └──────────────────────────────────────────┘</code></pre><p>ConfigMap의 값은 그대로 전달되고, Secret의 값은 Base64에서 <strong>원본으로 디코딩되어</strong> 전달된다.</p>
<h3 id="yaml-8">YAML</h3>
<p><strong>ConfigMap</strong></p>
<pre><code class="language-yaml">apiVersion: v1
kind: ConfigMap
metadata:
  name: cm-dev
data:
  SSH: &quot;false&quot;
  User: dev</code></pre>
<p><strong>Secret</strong></p>
<pre><code class="language-yaml">apiVersion: v1
kind: Secret
metadata:
  name: sec-dev
data:
  Key: MTIzNA==</code></pre>
<p><strong>Pod</strong></p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: pod-1
spec:
  containers:
    - name: container
      image: tmkube/init
      envFrom:
        - configMapRef:
            name: cm-dev
        - secretRef:
            name: sec-dev</code></pre>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>data</code></td>
<td>key: value 형태의 값 목록</td>
</tr>
<tr>
<td><code>envFrom</code></td>
<td>ConfigMap이나 Secret의 <strong>모든 key를 한 번에</strong> 환경변수로 가져온다</td>
</tr>
<tr>
<td><code>configMapRef.name</code></td>
<td>가져올 ConfigMap의 이름</td>
</tr>
<tr>
<td><code>secretRef.name</code></td>
<td>가져올 Secret의 이름</td>
</tr>
</tbody></table>
<p><code>envFrom</code>을 쓰면 안에 있는 key가 전부 환경변수가 된다. 특정 key만 골라서 가져오고 싶다면 <code>env</code>를 사용한다.</p>
<h3 id="사용-예시">사용 예시</h3>
<p>애플리케이션에서 다음과 같이 값을 읽는다.</p>
<pre><code class="language-java">// Spring Boot
@Value(&quot;${SSH}&quot;)
private boolean sshEnabled;</code></pre>
<p>애플리케이션 코드는 이 값이 ConfigMap에서 왔는지 Secret에서 왔는지 알 필요가 없다. 그냥 환경변수를 읽을 뿐이다.</p>
<hr>
<h2 id="2-4-2-env-file--파일-내용을-환경변수로">2-4-2. Env (File) — 파일 내용을 환경변수로</h2>
<h3 id="개념-3">개념</h3>
<p>설정 값이 길거나 이미 파일로 존재하는 경우, 파일 자체를 ConfigMap에 넣을 수 있다. 이때 <strong>파일 이름이 key가 되고, 파일 안의 내용이 value가 된다.</strong></p>
<pre><code>        file.txt
   ┌──────────────────┐
   │  Content         │      ConfigMap에 넣으면
   │  (파일 내용)     │  ──────────────────────▶  key   : file.txt
   └──────────────────┘                           value : Content
        ▲        ▲
     파일 이름  파일 내용
        │        │
       key     value</code></pre><h3 id="구조-7">구조</h3>
<pre><code>              file.txt
       ┌──────────────────┐
       │     Content      │
       └────┬────────┬────┘
            │        │
            ▼        ▼
  ┌─────────────┐  ┌─────────────┐
  │  ConfigMap  │  │   Secret    │
  │             │  │             │
  │ Key : Value │  │ Key : Value │
  │ file.txt :  │  │ file.txt :  │
  │   Content   │  │   Base64    │
  └──────┬──────┘  └──────┬──────┘
         │                │  Decoding
         ▼                ▼
  ┌──────────────────────────────┐
  │            Pod                │
  │  ┌────────────────────────┐  │
  │  │       Container        │  │
  │  │  env                   │  │
  │  │   file : Content       │  │
  │  └────────────────────────┘  │
  └───────────────────────────────┘</code></pre><h3 id="생성-방법">생성 방법</h3>
<p>이 방식은 YAML로 직접 작성하지 않고 <strong>명령어로 생성</strong>한다. 파일 내용을 사람이 옮겨 적을 필요가 없다.</p>
<pre><code class="language-bash"># ConfigMap 생성
kubectl create configmap cm-file --from-file=./file.txt

# Secret 생성 (Base64 인코딩은 자동으로 처리됨)
kubectl create secret generic sec-file --from-file=./file.txt</code></pre>
<p><code>--from-file</code> 옵션을 쓰면 파일 이름이 key가 되고 내용이 value가 된다. Secret은 인코딩까지 자동으로 해준다.</p>
<h3 id="yaml-9">YAML</h3>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: file
spec:
  containers:
    - name: container
      image: tmkube/init
      env:
        - name: file
          valueFrom:
            configMapKeyRef:
              name: cm-file
              key: file.txt</code></pre>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>env.name</code></td>
<td>컨테이너에서 사용할 <strong>환경변수 이름</strong></td>
</tr>
<tr>
<td><code>valueFrom</code></td>
<td>값을 다른 오브젝트에서 가져온다는 의미</td>
</tr>
<tr>
<td><code>configMapKeyRef.name</code></td>
<td>가져올 ConfigMap의 이름</td>
</tr>
<tr>
<td><code>configMapKeyRef.key</code></td>
<td>ConfigMap 안에서 가져올 key. 여기서는 파일 이름</td>
</tr>
</tbody></table>
<p>앞의 <code>envFrom</code>이 모든 key를 가져왔다면, <code>env</code> + <code>valueFrom</code>은 <strong>특정 key 하나만</strong> 골라서 원하는 이름의 환경변수로 만든다. 위 예시에서는 <code>file.txt</code>라는 key의 값을 <code>file</code>이라는 환경변수로 받는다.</p>
<h3 id="한계">한계</h3>
<p>환경변수는 <strong>컨테이너가 시작될 때 한 번 주입되고 끝난다.</strong> ConfigMap의 값을 나중에 수정해도 이미 실행 중인 컨테이너의 환경변수는 바뀌지 않는다. 새 값을 반영하려면 Pod를 재생성해야 한다.</p>
<hr>
<h2 id="2-4-3-volume-mount-file--파일을-파일로-마운트">2-4-3. Volume Mount (File) — 파일을 파일로 마운트</h2>
<h3 id="개념-4">개념</h3>
<p>ConfigMap의 내용을 환경변수가 아니라 <strong>파일 형태 그대로</strong> 컨테이너 안에 넣는 방식이다. Volume에서 배운 마운트 방식을 그대로 사용한다.</p>
<h3 id="구조-8">구조</h3>
<pre><code>              file.txt
       ┌──────────────────┐
       │     Content      │
       └────┬────────┬────┘
            │        │
            ▼        ▼
  ┌─────────────┐  ┌─────────────┐
  │  ConfigMap  │  │   Secret    │
  │ file.txt :  │  │ file.txt :  │
  │   Content   │  │   Base64    │
  └──────┬──────┘  └──────┬──────┘
         │                │  Decoding
         ▼                ▼
  ┌──────────────────────────────────┐
  │              Pod                  │
  │  ┌────────────────────────────┐  │
  │  │        Container           │  │
  │  │                            │  │
  │  │   /mount                   │  │  ← mountPath
  │  │     └── file.txt           │  │  ← key가 파일명이 된다
  │  │           Content          │  │  ← value가 파일 내용이 된다
  │  └────────────────────────────┘  │
  └───────────────────────────────────┘</code></pre><p>마운트한 경로 아래에 <strong>key 이름의 파일이 생성</strong>되고, 그 파일 안에 value가 들어간다.</p>
<h3 id="yaml-10">YAML</h3>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: mount
spec:
  containers:
    - name: container
      image: tmkube/init
      volumeMounts:
        - name: file-volume
          mountPath: /mount
  volumes:
    - name: file-volume
      configMap:
        name: cm-file</code></pre>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>volumes.configMap.name</code></td>
<td>볼륨으로 사용할 ConfigMap의 이름</td>
</tr>
<tr>
<td><code>mountPath</code></td>
<td>컨테이너 안에서 파일이 놓일 경로</td>
</tr>
</tbody></table>
<p>Volume에서 <code>emptyDir</code>이나 <code>hostPath</code>를 쓰던 자리에 <code>configMap</code>을 넣는 구조다. Secret을 마운트할 때는 <code>secret: secretName: sec-file</code> 형태로 작성한다.</p>
<p>컨테이너 안에서 확인하면 다음과 같다.</p>
<pre><code class="language-bash">$ ls /mount
file.txt

$ cat /mount/file.txt
Content</code></pre>
<h3 id="어떤-경우에-사용하는가">어떤 경우에 사용하는가</h3>
<table>
<thead>
<tr>
<th>예시</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>애플리케이션 설정 파일</td>
<td><code>application.yml</code>, <code>nginx.conf</code> 처럼 파일로 읽어야 하는 설정</td>
</tr>
<tr>
<td>인증서</td>
<td>TLS 인증서와 키 파일</td>
</tr>
<tr>
<td>초기화 스크립트</td>
<td>컨테이너 시작 시 실행할 스크립트 파일</td>
</tr>
</tbody></table>
<p>환경변수로는 여러 줄로 된 설정 파일을 다루기 어렵다. 이런 경우 Volume Mount 방식이 적합하다.</p>
<h3 id="환경변수-방식과의-결정적-차이">환경변수 방식과의 결정적 차이</h3>
<p>Volume Mount 방식은 <strong>ConfigMap을 수정하면 마운트된 파일도 자동으로 갱신된다.</strong> Pod를 재생성하지 않아도 된다.</p>
<pre><code>   [ Env 방식 ]                     [ Volume Mount 방식 ]

   ConfigMap 수정                    ConfigMap 수정
        │                                 │
        ▼                                 ▼
   컨테이너 환경변수 그대로          마운트된 파일 자동 갱신
        │                                 │
   Pod 재생성 필요                   Pod 유지 가능</code></pre><p>다만 파일 내용이 바뀌어도 애플리케이션이 자동으로 다시 읽어들이지는 않는다. 애플리케이션이 파일 변경을 감지해 재로딩하는 기능을 가지고 있어야 실제로 반영된다.</p>
<hr>
<h2 id="세-가지-방식-비교">세 가지 방식 비교</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>Env (Literal)</th>
<th>Env (File)</th>
<th>Volume Mount (File)</th>
</tr>
</thead>
<tbody><tr>
<td>전달 형태</td>
<td>환경변수</td>
<td>환경변수</td>
<td>파일</td>
</tr>
<tr>
<td>값의 출처</td>
<td>YAML에 직접 작성한 상수</td>
<td>파일 이름(key) + 파일 내용(value)</td>
<td>파일 이름(key) + 파일 내용(value)</td>
</tr>
<tr>
<td>생성 방법</td>
<td>YAML 작성</td>
<td><code>kubectl create --from-file</code></td>
<td><code>kubectl create --from-file</code></td>
</tr>
<tr>
<td>값 수정 시</td>
<td>Pod 재생성 필요</td>
<td>Pod 재생성 필요</td>
<td><strong>파일 자동 갱신</strong></td>
</tr>
<tr>
<td>적합한 경우</td>
<td>짧은 설정 값</td>
<td>파일 내용을 변수로 쓸 때</td>
<td>설정 파일, 인증서</td>
</tr>
</tbody></table>
<p><strong>선택 기준</strong></p>
<ul>
<li>값이 짧고 단순한 상수인가 → <strong>Env (Literal)</strong></li>
<li>파일에 있는 값을 환경변수로 받고 싶은가 → <strong>Env (File)</strong></li>
<li>애플리케이션이 파일 형태로 읽어야 하는가 → <strong>Volume Mount (File)</strong></li>
</ul>
<h1 id="2-5-namespace-resourcequota-limitrange">2-5. Namespace, ResourceQuota, LimitRange</h1>
<h2 id="왜-필요한가-2">왜 필요한가</h2>
<h3 id="문제-상황--자원-독점">문제 상황 — 자원 독점</h3>
<p>쿠버네티스 클러스터가 사용할 수 있는 자원(Memory, CPU)은 정해져 있다. 그런데 아무런 제약이 없다면 한 팀이 자원을 전부 가져가는 상황이 발생할 수 있다.</p>
<pre><code>┌──────────────── Kubernetes Cluster ────────────────┐
│                                                     │
│   Memory  ┌─┐┌─┐┌─┐┌─┐┌─┐                          │
│   CPU     └─┘└─┘└─┘└─┘└─┘   ← 클러스터 전체 자원   │
│              │                                      │
│              │ A팀이 전부 사용                      │
│              ▼                                      │
│   ┌──────────────────┐      ┌──────────────────┐  │
│   │  A팀 Pod         │      │  B팀 Pod         │  │
│   │  전체 자원 점유  │      │  자원 부족 ✗     │  │
│   └──────────────────┘      └──────────────────┘  │
│                                                     │
└─────────────────────────────────────────────────────┘</code></pre><p>한쪽에서 자원을 독점하면 다른 쪽은 Pod를 아예 생성하지 못한다. 개발 환경의 테스트 Pod가 메모리를 다 써버려서 운영 서비스가 배포되지 못하는 상황이 벌어질 수 있다.</p>
<p>이를 방지하기 위해 세 가지 오브젝트를 사용한다.</p>
<table>
<thead>
<tr>
<th>오브젝트</th>
<th>역할</th>
<th>비유하자면</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Namespace</strong></td>
<td>클러스터를 논리적으로 나눈다</td>
<td>자원을 나눠 쓸 <strong>구역</strong>을 만든다</td>
</tr>
<tr>
<td><strong>ResourceQuota</strong></td>
<td>Namespace가 쓸 수 있는 <strong>총량</strong>을 제한한다</td>
<td>구역 전체의 <strong>한도</strong>를 정한다</td>
</tr>
<tr>
<td><strong>LimitRange</strong></td>
<td>Namespace에 들어오는 <strong>Pod 하나의 크기</strong>를 제한한다</td>
<td>구역에 들어올 수 있는 <strong>개별 크기</strong>를 정한다</td>
</tr>
</tbody></table>
<h3 id="세-가지의-관계">세 가지의 관계</h3>
<pre><code>┌─────────────────── Kubernetes Cluster ───────────────────┐
│                                                           │
│  Memory ┌─┐┌─┐┌─┐┌─┐┌─┐                                  │
│  CPU    └─┘└─┘└─┘└─┘└─┘                                  │
│      │        │         │                                 │
│      ▼        ▼         ▼                                 │
│  ┌────────┐ ┌────────┐ ┌────────────────────────────┐    │
│  │Resource│ │Resource│ │ ResourceQuota  LimitRange  │    │
│  │ Quota  │ │ Quota  │ │                            │    │
│  │┌──────┐│ │┌──────┐│ │ ┌────────────┐             │    │
│  ││ Pod  ││ ││ Pod  ││ │ │            │   ┌─────┐   │    │
│  ││ Pod  ││ ││ Pod  ││ │ │    Pod     │◀──│ Pod │   │    │
│  │└──────┘│ │└──────┘│ │ │  (큰 Pod)  │   └─────┘   │    │
│  │Namespace│ │Namespace│ │ └────────────┘  ┌─────┐   │    │
│  └────────┘ └────────┘ │      크기 초과 ✗ │ Pod │   │    │
│                        │                  └─────┘   │    │
│                        │      Namespace             │    │
│                        └────────────────────────────┘    │
└───────────────────────────────────────────────────────────┘</code></pre><p>ResourceQuota는 Namespace가 쓸 수 있는 <strong>총합</strong>을 막고, LimitRange는 Namespace에 들어오려는 <strong>Pod 하나하나의 크기</strong>를 막는다.</p>
<p>두 오브젝트 모두 <strong>Namespace에 설정하는 오브젝트</strong>다. 클러스터 전체에 한 번에 적용하는 방법은 없으며, 자원을 통제하려는 Namespace마다 각각 만들어야 한다.</p>
<hr>
<h2 id="2-5-1-namespace">2-5-1. Namespace</h2>
<h3 id="개념-5">개념</h3>
<p>Namespace는 하나의 클러스터를 <strong>여러 개의 논리적인 공간으로 나누는</strong> 오브젝트다. 물리적으로 서버를 나누는 것이 아니라, 같은 클러스터 안에서 오브젝트를 그룹으로 구분하는 것이다.</p>
<table>
<thead>
<tr>
<th>용도</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td>환경 분리</td>
<td><code>dev</code>, <code>staging</code>, <code>production</code></td>
</tr>
<tr>
<td>팀 분리</td>
<td><code>team-a</code>, <code>team-b</code></td>
</tr>
<tr>
<td>서비스 분리</td>
<td><code>frontend</code>, <code>backend</code>, <code>monitoring</code></td>
</tr>
</tbody></table>
<h3 id="특성-1--이름의-중복-허용">특성 1 — 이름의 중복 허용</h3>
<p><strong>같은 Namespace 안에서는 오브젝트 이름이 유일해야 한다.</strong> 반대로 말하면, Namespace가 다르면 같은 이름을 써도 된다.</p>
<pre><code>┌──── Namespace-1 ────┐      ┌──── Namespace-2 ────┐
│                      │      │                      │
│   ┌──────────────┐   │      │   ┌──────────────┐   │
│   │   Pod1       │   │      │   │   Pod1       │   │
│   │   nm: pod1   │   │      │   │   nm: pod1   │   │
│   └──────────────┘   │      │   └──────────────┘   │
│                      │      │                      │
│   같은 이름을 또 만들면 ✗  │      │  다른 Namespace라 가능 ✓ │
└──────────────────────┘      └──────────────────────┘</code></pre><p><code>dev</code> 네임스페이스와 <code>production</code> 네임스페이스에 각각 <code>api-server</code>라는 이름의 Pod를 둘 수 있다. 이름을 <code>dev-api-server</code>, <code>prod-api-server</code>처럼 구분해서 지을 필요가 없다.</p>
<h3 id="특성-2--다른-namespace의-자원과-연결되지-않는다">특성 2 — 다른 Namespace의 자원과 연결되지 않는다</h3>
<p>Service의 <code>selector</code>는 <strong>같은 Namespace 안의 Pod만</strong> 선택한다. Label이 완전히 같아도 Namespace가 다르면 연결되지 않는다.</p>
<pre><code>┌──── Namespace-1 ────┐      ┌──── Namespace-2 ────┐
│                      │      │                      │
│   ┌──────────────┐   │      │   ┌──────────────┐   │
│   │   Pod1       │   │      │   │  Service1    │   │
│   │   nm: pod1   │◀──┼──✗───┼───│ selector:    │   │
│   │              │   │      │   │   nm: pod1   │   │
│   └──────────────┘   │      │   └──────────────┘   │
│                      │      │                      │
└──────────────────────┘      └──────────────────────┘

     Label이 같아도 Namespace가 다르면 선택되지 않는다</code></pre><p>다만 이것은 <strong>오브젝트 연결이 안 된다는 의미이지, 네트워크가 차단된다는 의미는 아니다.</strong> Pod의 IP를 직접 알고 있다면 다른 Namespace의 Pod에도 접근할 수 있다.</p>
<pre><code>   Namespace-2의 Pod  ──── 10.2.3.12 로 직접 호출 ────▶  Namespace-1의 Pod
                                                              접근 가능 ✓</code></pre><p>네트워크까지 차단하려면 <strong>NetworkPolicy</strong>라는 별도의 오브젝트가 필요하다.</p>
<h3 id="특성-3--namespace에-속하지-않는-오브젝트가-있다">특성 3 — Namespace에 속하지 않는 오브젝트가 있다</h3>
<p>모든 오브젝트가 Namespace에 속하는 것은 아니다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>오브젝트</th>
</tr>
</thead>
<tbody><tr>
<td>Namespace에 속함</td>
<td>Pod, Service, ConfigMap, Secret, PVC, Deployment</td>
</tr>
<tr>
<td><strong>클러스터 전체에 속함</strong></td>
<td><strong>Node, PersistentVolume(PV)</strong></td>
</tr>
</tbody></table>
<pre><code>        ┌──────────────┐   ┌──────────────┐
        │      PV      │   │     Node     │  ← Namespace에 속하지 않음
        └──────┬───────┘   └──────┬───────┘
               │                  │
     ┌─────────┴──────────────────┴─────────┐
     │                                       │
┌────▼─────────────┐              ┌──────────▼───────┐
│  Namespace-1     │              │  Namespace-2     │
│    Pod, Service  │              │    Pod, Service  │
└──────────────────┘              └──────────────────┘</code></pre><p>Node는 물리적인 서버이고 PV는 실제 저장소이므로, 논리적 구분인 Namespace에 종속될 수 없다. 여러 Namespace의 Pod가 같은 Node 위에서 실행된다.</p>
<h3 id="특성-4--namespace를-삭제하면-안의-오브젝트가-모두-삭제된다">특성 4 — Namespace를 삭제하면 안의 오브젝트가 모두 삭제된다</h3>
<p>Namespace를 지우면 그 안에 있던 Pod, Service, ConfigMap이 <strong>전부 함께 삭제된다.</strong> 정리할 때는 편리하지만 실수하면 되돌릴 수 없으므로 주의해야 한다.</p>
<h3 id="yaml-11">YAML</h3>
<p><strong>Namespace 생성</strong></p>
<pre><code class="language-yaml">apiVersion: v1
kind: Namespace
metadata:
  name: nm-1</code></pre>
<p><strong>Pod를 특정 Namespace에 생성</strong></p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: pod-1
  namespace: nm-1
  labels:
    nm: pod1
spec:
  containers:
    - name: container
      image: tmkube/init</code></pre>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>metadata.namespace</code></td>
<td>이 오브젝트가 속할 Namespace. 생략하면 <code>default</code>에 생성된다</td>
</tr>
</tbody></table>
<p><strong>다른 Namespace의 Service</strong></p>
<pre><code class="language-yaml">apiVersion: v1
kind: Service
metadata:
  name: svc-1
  namespace: nm-2
spec:
  selector:
    nm: pod1
  ports:
    - port: 8080</code></pre>
<p>이 Service는 <code>nm-2</code>에 있으므로 <code>nm-1</code>에 있는 Pod를 선택하지 못한다.</p>
<h3 id="자주-쓰는-명령어">자주 쓰는 명령어</h3>
<pre><code class="language-bash"># Namespace 목록 조회
kubectl get namespace

# 특정 Namespace의 Pod 조회
kubectl get pods -n nm-1

# 모든 Namespace의 Pod 조회
kubectl get pods --all-namespaces</code></pre>
<p><code>-n</code> 옵션을 생략하면 <code>default</code> Namespace를 조회한다. Pod를 만들었는데 목록에 안 보인다면 Namespace를 지정하지 않아서인 경우가 많다.</p>
<hr>
<h2 id="2-5-2-resourcequota">2-5-2. ResourceQuota</h2>
<h3 id="개념-6">개념</h3>
<p>ResourceQuota는 <strong>하나의 Namespace가 사용할 수 있는 자원의 총량</strong>을 제한한다.</p>
<pre><code>┌────────────────── Namespace (nm-1) ──────────────────┐
│                                                       │
│   ResourceQuota                                       │
│   requests.memory : 3Gi   ← 이 Namespace의 총 한도   │
│   limits.memory   : 6Gi                               │
│                                                       │
│   ┌──────────────┐  ┌──────────────┐                 │
│   │ Pod1         │  │ Pod3         │                 │
│   │ requests 2Gi │  │ requests 2Gi │  ← 합계 4Gi     │
│   │ limits   4Gi │  │ limits   2Gi │     한도 3Gi 초과│
│   └──────────────┘  └──────┬───────┘                 │
│                            │                          │
│                            ✗ 생성 거부                │
└───────────────────────────────────────────────────────┘</code></pre><p>Pod1이 이미 2Gi를 요청한 상태에서 Pod3가 2Gi를 추가로 요청하면 합계가 4Gi가 되어 한도인 3Gi를 넘는다. 이 경우 <strong>Pod3는 생성되지 않는다.</strong></p>
<h3 id="제한할-수-있는-항목">제한할 수 있는 항목</h3>
<p>ResourceQuota는 자원의 양뿐 아니라 오브젝트의 개수도 제한할 수 있다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>항목</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Compute Resource</strong></td>
<td>cpu, memory, storage</td>
</tr>
<tr>
<td><strong>Objects Count</strong></td>
<td>Pod, Service, ConfigMap, PVC, Secret 등의 개수</td>
</tr>
</tbody></table>
<p>Pod 개수를 10개로 제한해두면 그 Namespace에서는 11번째 Pod를 만들 수 없다.</p>
<h3 id="중요한-제약--pod에-반드시-resources를-명시해야-한다">중요한 제약 — Pod에 반드시 resources를 명시해야 한다</h3>
<p>ResourceQuota가 설정된 Namespace에서는 <strong>모든 Pod가 requests와 limits를 반드시 작성해야 한다.</strong></p>
<pre><code>   ┌──────────────┐
   │ Pod2         │
   │              │  ← resources 항목이 없음
   │ (설정 없음)  │
   └──────┬───────┘
          │
          ✗ 생성 거부</code></pre><p>이유는 단순하다. ResourceQuota는 &quot;현재 사용량의 합계&quot;를 계산해서 한도와 비교하는데, Pod가 자기 사용량을 밝히지 않으면 계산이 불가능하기 때문이다.</p>
<p>이 제약 때문에 실무에서는 ResourceQuota를 걸어두면 개발자가 <code>resources</code>를 깜빡해 Pod 생성이 실패하는 일이 자주 발생한다. 뒤에 나오는 LimitRange의 <code>default</code> 설정으로 이 문제를 해결할 수 있다.</p>
<h3 id="yaml-12">YAML</h3>
<p><strong>ResourceQuota</strong></p>
<pre><code class="language-yaml">apiVersion: v1
kind: ResourceQuota
metadata:
  name: rq-1
  namespace: nm-1
spec:
  hard:
    requests.memory: 3Gi
    limits.memory: 6Gi</code></pre>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>namespace</code></td>
<td>이 제한을 적용할 Namespace</td>
</tr>
<tr>
<td><code>hard</code></td>
<td>넘을 수 없는 한도. Namespace 전체의 <strong>합계</strong> 기준</td>
</tr>
<tr>
<td><code>requests.memory</code></td>
<td>Namespace 안 모든 Pod의 requests 합계 한도</td>
</tr>
<tr>
<td><code>limits.memory</code></td>
<td>Namespace 안 모든 Pod의 limits 합계 한도</td>
</tr>
</tbody></table>
<p><strong>Pod</strong></p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: pod-2
  namespace: nm-1
spec:
  containers:
    - name: container
      image: tmkube/app
      resources:
        requests:
          memory: 2Gi
        limits:
          memory: 2Gi</code></pre>
<h3 id="사용-예시-1">사용 예시</h3>
<pre><code class="language-yaml">spec:
  hard:
    requests.cpu: &quot;4&quot;
    requests.memory: 8Gi
    limits.cpu: &quot;8&quot;
    limits.memory: 16Gi
    pods: &quot;20&quot;
    services: &quot;5&quot;
    persistentvolumeclaims: &quot;3&quot;</code></pre>
<p>개발 팀에 이런 Quota를 설정하면, 그 팀은 CPU 4코어와 메모리 8Gi 안에서 최대 20개의 Pod를 운영할 수 있다. 아무리 많이 배포해도 다른 팀의 자원을 침범할 수 없다.</p>
<hr>
<h2 id="2-5-3-limitrange">2-5-3. LimitRange</h2>
<h3 id="개념-7">개념</h3>
<p>ResourceQuota가 Namespace <strong>전체 합계</strong>를 막는다면, LimitRange는 <strong>Pod 하나하나의 크기</strong>를 검사한다.</p>
<pre><code>   ┌──────────────┐                    LimitRange
   │ Pod1         │                    ┌──────────────────────┐
   │ limits 5Gi   │────✗──▶ Namespace  │ min : 1Gi            │
   └──────────────┘                    │ max : 4Gi            │
                                       │ maxLimitRequestRatio │
   ┌──────────────┐                    │       : 3            │
   │ Pod2         │                    └──────────────────────┘
   │ requests 1Gi │────✗──▶ Namespace
   │ limits   4Gi │
   └──────────────┘

   ┌──────────────┐
   │ Pod3         │────✓──▶ Namespace
   │ requests 1Gi │           들어감
   │ limits   2Gi │
   └──────────────┘</code></pre><p>Namespace에 들어오려는 Pod를 문 앞에서 검사해, 조건에 맞지 않으면 들여보내지 않는 방식이다.</p>
<h3 id="검사-항목">검사 항목</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>min</code></td>
<td>Pod가 요청할 수 있는 <strong>최소</strong> 크기</td>
</tr>
<tr>
<td><code>max</code></td>
<td>Pod가 요청할 수 있는 <strong>최대</strong> 크기</td>
</tr>
<tr>
<td><code>maxLimitRequestRatio</code></td>
<td>limits ÷ requests 의 <strong>최대 비율</strong></td>
</tr>
</tbody></table>
<h3 id="각-pod가-거부되는-이유">각 Pod가 거부되는 이유</h3>
<p><strong>Pod1 — max 초과</strong></p>
<pre><code>   limits: 5Gi  &gt;  max: 4Gi        ✗ 거부</code></pre><p>한 Pod가 너무 큰 자원을 가져가는 것을 막는다.</p>
<p><strong>Pod2 — 비율 초과</strong></p>
<pre><code>   limits 4Gi ÷ requests 1Gi = 4  &gt;  maxLimitRequestRatio: 3   ✗ 거부</code></pre><p>이 항목은 처음 보면 이해하기 어려우므로 의미를 짚고 넘어가는 것이 좋다.</p>
<p><code>requests</code>는 스케줄러가 Node를 고를 때 쓰는 값이고, <code>limits</code>는 실제로 쓸 수 있는 상한이다. 둘의 차이가 너무 크면 문제가 생긴다.</p>
<pre><code>   requests 1Gi 로 Node에 배치됨
        │
        │  스케줄러는 이 Pod가 1Gi만 쓸 것으로 예상
        ▼
   실제로는 limits 4Gi 까지 사용
        │
        ▼
   Node의 메모리가 예상보다 빨리 고갈됨
   → 같은 Node의 다른 Pod가 영향을 받음</code></pre><p><code>maxLimitRequestRatio: 3</code>은 &quot;실제 사용량이 신고한 값의 3배를 넘지 못하게 하라&quot;는 뜻이다. 신고한 값과 실제 사용량의 격차를 줄여 Node의 자원 예측을 정확하게 만든다.</p>
<p><strong>Pod3 — 통과</strong></p>
<pre><code>   min 1Gi ≤ requests 1Gi ≤ max 4Gi        ✓
   min 1Gi ≤ limits   2Gi ≤ max 4Gi        ✓
   limits 2Gi ÷ requests 1Gi = 2  ≤  3     ✓</code></pre><h3 id="default와-defaultrequest--값을-자동으로-채워준다">default와 defaultRequest — 값을 자동으로 채워준다</h3>
<p>LimitRange에는 검사 외에 <strong>값을 대신 넣어주는</strong> 기능도 있다.</p>
<pre><code>   ┌──────────────┐                    LimitRange
   │ Pod3         │                    ┌──────────────────────┐
   │ (resources   │────▶ Namespace ────│ defaultRequest       │
   │  미작성)     │                    │   memory : 1Gi       │
   └──────────────┘                    │ default              │
                                       │   memory : 2Gi       │
          │                            └──────────────────────┘
          ▼
   ┌──────────────┐
   │ Pod3         │
   │ requests 1Gi │  ← 자동으로 채워짐
   │ limits   2Gi │
   └──────────────┘</code></pre><table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>defaultRequest</code></td>
<td>Pod가 requests를 작성하지 않았을 때 자동으로 넣어줄 값</td>
</tr>
<tr>
<td><code>default</code></td>
<td>Pod가 limits를 작성하지 않았을 때 자동으로 넣어줄 값</td>
</tr>
</tbody></table>
<p>앞서 ResourceQuota를 걸면 모든 Pod가 <code>resources</code>를 반드시 작성해야 한다고 했다. LimitRange의 이 설정을 함께 걸어두면, 개발자가 작성하지 않아도 기본값이 자동으로 들어가므로 Pod 생성이 실패하지 않는다. <strong>두 오브젝트를 함께 사용하는 이유가 여기에 있다.</strong></p>
<h3 id="yaml-13">YAML</h3>
<pre><code class="language-yaml">apiVersion: v1
kind: LimitRange
metadata:
  name: lr-1
  namespace: nm-1
spec:
  limits:
    - type: Container
      min:
        memory: 1Gi
      max:
        memory: 4Gi
      defaultRequest:
        memory: 1Gi
      default:
        memory: 2Gi
      maxLimitRequestRatio:
        memory: 3</code></pre>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>type: Container</code></td>
<td>검사 대상. Container 단위로 검사한다</td>
</tr>
<tr>
<td><code>min</code> / <code>max</code></td>
<td>허용되는 크기의 하한과 상한</td>
</tr>
<tr>
<td><code>defaultRequest</code> / <code>default</code></td>
<td>값이 없을 때 자동으로 넣어줄 requests와 limits</td>
</tr>
<tr>
<td><code>maxLimitRequestRatio</code></td>
<td>limits ÷ requests 의 최대 비율</td>
</tr>
</tbody></table>
<p><code>type</code>에는 <code>Container</code> 외에 <code>Pod</code>(Pod 안 컨테이너들의 합계 기준)와 <code>PersistentVolumeClaim</code>(요청 가능한 저장 용량 기준)도 지정할 수 있다.</p>
<hr>
<h2 id="세-오브젝트-비교">세 오브젝트 비교</h2>
<pre><code>   Namespace              ResourceQuota           LimitRange
   ─────────              ─────────────           ──────────
   ┌──────────┐          ┌──────────────┐        ┌──────────────┐
   │          │          │  총합 3Gi    │        │ Pod 하나당   │
   │  구역을  │          │  ┌────┐┌────┐│        │ 1Gi ~ 4Gi    │
   │  나눈다  │          │  │Pod ││Pod ││        │              │
   │          │          │  └────┘└────┘│   ✗◀──│ [ 5Gi Pod ]  │
   └──────────┘          │  합계 초과 ✗ │        └──────────────┘
                         └──────────────┘

   논리적 분리            총량 제한               개별 크기 제한</code></pre><table>
<thead>
<tr>
<th>구분</th>
<th>Namespace</th>
<th>ResourceQuota</th>
<th>LimitRange</th>
</tr>
</thead>
<tbody><tr>
<td>역할</td>
<td>클러스터를 논리적으로 분리</td>
<td>Namespace의 자원 <strong>총량</strong> 제한</td>
<td>Pod 하나의 <strong>크기</strong> 제한</td>
</tr>
<tr>
<td>검사 기준</td>
<td>—</td>
<td>Namespace 안 모든 Pod의 합계</td>
<td>Pod(Container) 개별 값</td>
</tr>
<tr>
<td>적용 범위</td>
<td>클러스터</td>
<td>Namespace</td>
<td>Namespace</td>
</tr>
<tr>
<td>제한 대상</td>
<td>—</td>
<td>cpu, memory, storage, 오브젝트 개수</td>
<td>min, max, 비율</td>
</tr>
<tr>
<td>값 자동 설정</td>
<td>—</td>
<td>없음</td>
<td><code>default</code>, <code>defaultRequest</code> 제공</td>
</tr>
<tr>
<td>위반 시</td>
<td>—</td>
<td>Pod 생성 거부</td>
<td>Pod 생성 거부</td>
</tr>
</tbody></table>
<p><strong>함께 사용하는 이유</strong></p>
<p>세 오브젝트는 하나씩 쓰기보다 조합해서 쓸 때 효과가 있다.</p>
<ul>
<li><strong>Namespace</strong>로 팀이나 환경별 구역을 나눈다</li>
<li><strong>ResourceQuota</strong>로 각 구역이 클러스터 자원을 독점하지 못하게 막는다</li>
<li><strong>LimitRange</strong>로 구역 안에서 하나의 Pod가 지나치게 커지는 것을 막고, 값을 빠뜨린 Pod에는 기본값을 채워준다</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kubernetes 기초]]></title>
            <link>https://velog.io/@jina_ham/Kubernetes-%EA%B8%B0%EC%B4%88</link>
            <guid>https://velog.io/@jina_ham/Kubernetes-%EA%B8%B0%EC%B4%88</guid>
            <pubDate>Wed, 09 Sep 2026 08:20:41 GMT</pubDate>
            <description><![CDATA[<h2 id="1-kubernetes-k8s-소개">1. Kubernetes (K8s) 소개</h2>
<h3 id="1-0-이름의-유래">1-0. 이름의 유래</h3>
<p><strong>Kubernetes</strong>는 고대 그리스어로 <strong>배의 조타수(Helmsman)</strong> 를 의미한다. 컨테이너라는 화물을 실은 배를 조종하는 역할에서 따온 이름이다.</p>
<h3 id="1-0-0-쿠버네티스란">1-0-0. 쿠버네티스란</h3>
<table>
<thead>
<tr>
<th>정의</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>플랫폼을 만들기 위한 플랫폼</td>
<td>원시적인 기능들만 제공하고, 사용자가 필요한 구성을 결정해 <strong>자신만의 배포 플랫폼을 제작</strong>한다</td>
</tr>
<tr>
<td>오픈소스 플랫폼</td>
<td>컨테이너화된 애플리케이션의 <strong>자동 배포, 확장 및 관리</strong>를 해주는 오픈소스 플랫폼이다</td>
</tr>
</tbody></table>
<hr>
<h3 id="1-0-1-쿠버네티스가-해결하는-문제">1-0-1. 쿠버네티스가 해결하는 문제</h3>
<h4 id="서버의-확장-관리는-어떻게-하는가">서버의 확장 관리는 어떻게 하는가</h4>
<p>코드 한 줄을 바꾸거나 명령어 하나를 입력하면 몇 초 만에 똑같은 파드가 수십 개로 늘어난다. CPU 사용량이 올라가면 알아서 자동으로 늘려준다.</p>
<blockquote>
<p><strong>HPA (Horizontal Pod Autoscaler), Auto-scaling</strong></p>
</blockquote>
<h4 id="문제가-생긴-서버의-재시작은">문제가 생긴 서버의 재시작은</h4>
<p>쿠버네티스는 의심이 많고 부지런한 감시자다. 파드가 응답이 없거나 죽으면 사람이 개입하기 전에 기존 파드를 버리고 새 파드를 만들어 교체해 둔다. 엔지니어는 아침에 출근해서 복구 이력 로그만 확인하면 된다.</p>
<blockquote>
<p><strong>Self-Healing</strong></p>
</blockquote>
<h4 id="매번-패치할-때마다-야근과-주말-근무를-해야-하는가">매번 패치할 때마다 야근과 주말 근무를 해야 하는가</h4>
<p>무중단 배포 기능 덕분에 대낮에 서비스가 정상적으로 돌아가는 중에도 패치를 진행할 수 있다.</p>
<blockquote>
<p><strong>Rolling Update</strong></p>
</blockquote>
<h4 id="기능-하나-때문에-서버-전체가-죽는-상황">기능 하나 때문에 서버 전체가 죽는 상황</h4>
<p>기능별로 컨테이너와 파드를 쪼개 놓았기 때문에, 결제 파드가 터지더라도 로그인이나 장바구니 파드는 아무런 영향을 받지 않게 구성할 수 있다.</p>
<blockquote>
<p><strong>MSA 구조</strong></p>
</blockquote>
<h4 id="새로운-환경으로-이전하려면">새로운 환경으로 이전하려면</h4>
<p>애플리케이션과 환경을 통째로 박스(컨테이너 이미지)에 담아 코드로 정의해 두었기 때문에, 밑바닥이 AWS든 네이버 클라우드든 상관없이 <strong>K8s 클러스터만 준비되어 있다면 그 코드를 그대로 가져다 실행</strong>할 수 있다.</p>
<hr>
<h4 id="핵심-기능-정리">핵심 기능 정리</h4>
<table>
<thead>
<tr>
<th>문제</th>
<th>쿠버네티스의 해결</th>
<th>기능 이름</th>
</tr>
</thead>
<tbody><tr>
<td>서버 확장 관리</td>
<td>명령 한 번으로 파드 증설, CPU 사용량에 따라 자동 확장</td>
<td>HPA / Auto-scaling</td>
</tr>
<tr>
<td>장애 서버 재시작</td>
<td>죽은 파드를 감지해 자동으로 교체</td>
<td>Self-Healing</td>
</tr>
<tr>
<td>배포 시 서비스 중단</td>
<td>서비스 운영 중에도 무중단으로 패치</td>
<td>Rolling Update</td>
</tr>
<tr>
<td>장애의 전체 확산</td>
<td>기능별로 파드를 분리해 영향 범위를 격리</td>
<td>MSA 구조</td>
</tr>
<tr>
<td>환경 이전의 어려움</td>
<td>애플리케이션과 환경을 이미지로 코드화해 어디서든 실행</td>
<td>이식성(Portability)</td>
</tr>
</tbody></table>
<h3 id="1-1-왜-쿠버네티스가-필요한가">1-1. 왜 쿠버네티스가 필요한가</h3>
<p><strong>AS-IS: 쿠버네티스 도입 이전</strong></p>
<p>컨테이너(또는 서버)를 운영자가 직접 관리하는 방식이었다. 트래픽이 몰려도 수동으로 서버를 늘려야 했고, 프로세스가 죽으면 사람이 알아채고 재시작해야 했으며, 배포도 수동 스크립트나 순차 작업에 의존했다.</p>
<p>이 방식의 한계:</p>
<ul>
<li>트래픽 급증 시 즉각적인 대응이 어려움 (스케일 아웃에 사람 손이 필요)</li>
<li>장애 발생 시 감지와 복구까지의 시간(MTTR)이 길어짐</li>
<li>배포 중 서비스 중단이 발생하기 쉬움</li>
<li>운영 리소스가 반복 작업(재시작, 스케일링)에 소모됨</li>
</ul>
<p><strong>TO-BE: 쿠버네티스 도입 이후</strong></p>
<p>쿠버네티스는 이런 반복적인 운영 작업을 선언적(Declarative)으로 자동화한다. &quot;몇 개의 파드를 어떤 상태로 유지할지&quot;만 정의하면, 나머지는 쿠버네티스 컨트롤 플레인이 지속적으로 감시하고 스스로 맞춰준다.</p>
<ul>
<li><strong>Deployment</strong>: 애플리케이션의 배포와 버전 관리를 선언적으로 처리한다. 원하는 상태(replica 수, 이미지 버전 등)를 명세하면 쿠버네티스가 실제 상태를 그 상태로 맞춰주고, 롤링 업데이트를 통해 무중단 배포가 가능해진다. AS-IS에서 수동으로 순차 배포하던 것과 달리, 배포 전략(RollingUpdate, Recreate 등)을 코드로 관리할 수 있다.</li>
<li><strong>Auto Scaling</strong>: HPA(Horizontal Pod Autoscaler) 등을 통해 CPU/메모리 사용률이나 커스텀 메트릭 기준으로 파드 수를 자동으로 늘리고 줄인다. AS-IS에서는 트래픽 예측과 수동 증설이 필요했지만, 쿠버네티스는 실시간 지표를 기반으로 즉각 대응한다.</li>
<li><strong>Auto Healing</strong>: 컨테이너나 파드가 비정상 상태(Liveness/Readiness Probe 실패, 크래시 등)가 되면 쿠버네티스가 자동으로 감지해 재시작하거나 새 파드로 교체한다. 사람이 장애를 인지하고 조치하기 전에 시스템이 스스로 복구하므로 장애 대응 시간이 크게 줄어든다.</li>
</ul>
<p><strong>핵심 차이점 요약</strong></p>
<table>
<thead>
<tr>
<th>구분</th>
<th>AS-IS (쿠버네티스 이전)</th>
<th>TO-BE (쿠버네티스 도입 후)</th>
</tr>
</thead>
<tbody><tr>
<td>배포</td>
<td>수동/순차 배포, 중단 위험 존재</td>
<td>Deployment로 선언적 관리, 무중단 롤링 업데이트</td>
</tr>
<tr>
<td>확장</td>
<td>트래픽 대응이 수동, 반응 속도 느림</td>
<td>Auto Scaling으로 지표 기반 자동 확장/축소</td>
</tr>
<tr>
<td>장애 대응</td>
<td>사람이 감지 후 수동 복구</td>
<td>Auto Healing으로 자동 감지 및 자가 복구</td>
</tr>
<tr>
<td>운영 부담</td>
<td>반복 작업에 인력 소모</td>
<td>선언적 설정으로 운영 자동화, 사람은 정책 설계에 집중</td>
</tr>
</tbody></table>
<h3 id="1-2-클라우드-자동화-도구-전반과-비교한-쿠버네티스의-핵심-장점">1-2. 클라우드 자동화 도구 전반과 비교한 쿠버네티스의 핵심 장점</h3>
<ul>
<li><strong>관리 단위</strong>: 클라우드의 Auto Scaling(예: 인스턴스 그룹, 관리형 컨테이너 서비스 등)은 인스턴스·태스크 단위 관리에 머무르지만, 쿠버네티스는 배포·네트워크·스토리지·설정까지 애플리케이션 전체를 하나의 API로 관리</li>
<li><strong>벤더 종속성</strong>: 각 클라우드의 자동화 도구는 해당 클라우드 생태계에 종속되지만, 쿠버네티스는 오픈소스 표준이라 어떤 클라우드·온프레미스에서도 동일한 방식으로 동작</li>
<li><strong>선언적 API 범용성</strong>: 클라우드 관리형 서비스는 제공사가 정한 정책 범위 안에서만 동작하지만, 쿠버네티스는 Controller/CRD로 사실상 어떤 리소스든 같은 방식으로 정의하고 관리 가능</li>
<li><strong>스케줄링 정교함</strong>: 대부분의 자동화 도구는 단순 규칙 기반 배치에 그치지만, 쿠버네티스는 어피니티·taint·토폴로지 분산 등 세밀한 조건으로 워크로드 배치를 제어</li>
<li><strong>생태계</strong>: 각 벤더의 도구는 자사 생태계 안에서만 확장 가능하지만, 쿠버네티스는 Helm·Istio·Prometheus·ArgoCD 등 벤더 중립적인 표준 생태계를 보유</li>
</ul>
<h2 id="1-3-vm과-container-비교">1-3. VM과 Container 비교</h2>
<h3 id="구조-비교">구조 비교</h3>
<pre><code>        [ VM 방식 ]                          [ Container 방식 ]

┌──────┬──────┬──────┐              ┌──────┬──────┬──────┐
│ App  │ App  │ App  │              │ App  │ App  │ App  │
│ Bin  │ Bin  │ Bin  │              │ Bin  │ Bin  │ Bin  │
│ /Lib │ /Lib │ /Lib │              │ /Lib │ /Lib │ /Lib │
├──────┼──────┼──────┤              └──────┴──────┴──────┘
│Guest │Guest │Guest │                  ← 컨테이너 이미지
│  OS  │  OS  │  OS  │              ┌─────────────────────┐
├──────┴──────┴──────┤              │  Container Engine   │
│    Hypervisor      │              ├─────────────────────┤
├────────────────────┤              │      Host OS        │
│      Host OS       │              │  namespace + cgroups│
├────────────────────┤              ├─────────────────────┤
│    Host Server     │              │    Host Server      │
└────────────────────┘              └─────────────────────┘

       커널 N개                            커널 1개</code></pre><h3 id="vm의-구조">VM의 구조</h3>
<p><strong>계층 구성</strong></p>
<table>
<thead>
<tr>
<th>계층</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>Host Server</td>
<td>물리 서버. CPU, 메모리, 디스크 등 실제 하드웨어</td>
</tr>
<tr>
<td>Host OS</td>
<td>물리 서버에서 동작하는 운영체제</td>
</tr>
<tr>
<td>Hypervisor</td>
<td>가상화 담당. 물리 자원을 논리적으로 분할해 각 VM에 할당</td>
</tr>
<tr>
<td>Guest OS</td>
<td>VM마다 독립적으로 부팅되는 완전한 운영체제</td>
</tr>
</tbody></table>
<p><strong>특징</strong></p>
<ul>
<li><strong>독립성</strong>: Guest OS마다 커널이 분리되어 있어 서로 영향을 주지 않는다</li>
<li><strong>환경 자유도</strong>: Guest OS마다 다른 언어 런타임과 도구를 설치할 수 있다<ul>
<li>예: 동일 물리 서버에서 Guest OS A는 Java 17 + Gradle, Guest OS B는 Python 3.11 + pip</li>
</ul>
</li>
</ul>
<p><strong>비용</strong></p>
<ul>
<li><strong>자원 중복</strong>: Guest OS 개수만큼 운영체제가 중복 실행된다</li>
<li><strong>기동 시간</strong>: 커널 부팅부터 수행하므로 수십 초 ~ 수 분 소요</li>
<li><strong>이미지 크기</strong>: OS 전체가 포함되어 수 GB 단위</li>
</ul>
<h3 id="container의-구조">Container의 구조</h3>
<p><strong>계층 구성</strong></p>
<table>
<thead>
<tr>
<th>계층</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>Host Server</td>
<td>물리 서버</td>
</tr>
<tr>
<td>Host OS</td>
<td>운영체제. <strong>커널을 모든 컨테이너가 공유</strong></td>
</tr>
<tr>
<td>Container Engine</td>
<td>컨테이너 이미지를 실행 (예: Docker)</td>
</tr>
<tr>
<td>Container Image</td>
<td>애플리케이션(서비스) + 라이브러리 + 실행 바이너리를 패키징</td>
</tr>
</tbody></table>
<p><strong>특징</strong></p>
<ul>
<li><strong>Guest OS 없음</strong>: Hypervisor와 Guest OS 계층이 존재하지 않는다</li>
<li><strong>커널 공유</strong>: 별도 OS를 부팅하지 않고 Host OS의 커널을 공유한다</li>
<li><strong>프로세스 단위 실행</strong>: 커널 입장에서 컨테이너는 하나의 프로세스다</li>
</ul>
<p><strong>격리 방식</strong></p>
<p>컨테이너는 리눅스 커널의 두 기능으로 격리와 자원 제어를 수행한다.</p>
<table>
<thead>
<tr>
<th>기능</th>
<th>담당</th>
</tr>
</thead>
<tbody><tr>
<td>namespace</td>
<td>커널 자원의 격리</td>
</tr>
<tr>
<td>cgroups</td>
<td>자원 사용량의 제어</td>
</tr>
</tbody></table>
<h3 id="namespace--커널-자원의-격리">namespace — 커널 자원의 격리</h3>
<p><strong>정의</strong></p>
<p>커널이 관리하는 시스템 자원을 프로세스 그룹 단위로 분리하는 커널 기능이다. 특정 namespace에 속한 프로세스는 해당 namespace에 할당된 자원만 인식하고 접근할 수 있다.</p>
<p><strong>종류</strong></p>
<table>
<thead>
<tr>
<th>namespace</th>
<th>격리 대상</th>
</tr>
</thead>
<tbody><tr>
<td><strong>mnt</strong></td>
<td>파일시스템 마운트 포인트</td>
</tr>
<tr>
<td><strong>pid</strong></td>
<td>프로세스 ID 공간</td>
</tr>
<tr>
<td><strong>net</strong></td>
<td>네트워크 인터페이스, IP 주소, 포트, 라우팅 테이블</td>
</tr>
<tr>
<td><strong>ipc</strong></td>
<td>프로세스 간 통신 자원 (공유 메모리, 세마포어, 메시지 큐)</td>
</tr>
<tr>
<td><strong>uts</strong></td>
<td>호스트명, 도메인명</td>
</tr>
<tr>
<td><strong>user</strong></td>
<td>사용자 ID(UID)와 그룹 ID(GID) 매핑</td>
</tr>
</tbody></table>
<p><strong>namespace의 역할 — 자원 격리</strong></p>
<p>컨테이너는 Host OS의 커널을 공유한다. 별도 조치가 없다면 모든 컨테이너가 동일한 파일시스템, 동일한 포트, 동일한 프로세스 공간을 사용하게 되어 서로 충돌한다.</p>
<p>namespace는 커널이 관리하는 자원을 프로세스 그룹별로 분리해, 각 컨테이너가 자신에게 할당된 자원만 인식하도록 만든다. 그 결과 하나의 커널 위에서 여러 컨테이너가 독립된 환경처럼 동작할 수 있다.</p>
<h3 id="cgroups--자원-사용량의-제어">cgroups — 자원 사용량의 제어</h3>
<p><strong>정의</strong></p>
<p>프로세스 그룹이 사용할 수 있는 시스템 자원의 양을 할당하고 제한하는 커널 기능이다. namespace가 자원의 <strong>접근 범위</strong>를 결정한다면, cgroups는 자원의 <strong>사용량 상한</strong>을 결정한다.</p>
<p><strong>제어 자원</strong></p>
<table>
<thead>
<tr>
<th>자원</th>
<th>제어 내용</th>
</tr>
</thead>
<tbody><tr>
<td><strong>CPU</strong></td>
<td>CPU 사용 시간의 비율 및 상한</td>
</tr>
<tr>
<td><strong>Memory</strong></td>
<td>메모리 사용량 상한 (초과 시 프로세스 종료)</td>
</tr>
<tr>
<td><strong>I/O</strong></td>
<td>블록 디바이스 읽기/쓰기 대역폭</td>
</tr>
<tr>
<td><strong>Network</strong></td>
<td>네트워크 트래픽 분류 및 우선순위</td>
</tr>
</tbody></table>
<p><strong>적용 효과</strong></p>
<ul>
<li>컨테이너에 메모리 512MB 제한을 설정하면 cgroups가 이를 강제한다</li>
<li>제한을 초과하면 해당 프로세스가 종료된다</li>
<li>컨테이너 단위로 적용되므로 하나의 컨테이너가 호스트 전체 자원을 점유하는 상황을 방지한다</li>
</ul>
<h3 id="핵심-차이-정리">핵심 차이 정리</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>VM</th>
<th>Container</th>
</tr>
</thead>
<tbody><tr>
<td>가상화 대상</td>
<td>하드웨어 (Guest OS 전체를 실행)</td>
<td>프로세스 (namespace + cgroups로 격리)</td>
</tr>
<tr>
<td>커널</td>
<td>Guest OS마다 독립된 커널</td>
<td>Host OS의 커널을 공유</td>
</tr>
<tr>
<td>기동 시간</td>
<td>수십 초 ~ 수 분</td>
<td>수백 ms ~ 수 초</td>
</tr>
<tr>
<td>이미지 크기</td>
<td>수 GB</td>
<td>수십 MB ~ 수백 MB</td>
</tr>
<tr>
<td>자원 오버헤드</td>
<td>큼 (OS가 중복 실행됨)</td>
<td>작음 (프로세스 수준)</td>
</tr>
<tr>
<td>서버당 밀도</td>
<td>수십 대</td>
<td>수백 개</td>
</tr>
<tr>
<td>격리 수준</td>
<td>커널까지 완전 분리</td>
<td>커널 공유 (상대적으로 낮음)</td>
</tr>
<tr>
<td>OS 제약</td>
<td>Host와 다른 OS 실행 가능</td>
<td>Host 커널과 동일 계열만 실행 가능</td>
</tr>
</tbody></table>
<hr>
<h2 id="1-4-pod와-container--쿠버네티스에서의-역할">1-4. Pod와 Container — 쿠버네티스에서의 역할</h2>
<h3 id="pod의-정의">Pod의 정의</h3>
<p><strong>개념</strong></p>
<ul>
<li>쿠버네티스가 생성하고 관리하는 <strong>가장 작은 배포 단위</strong></li>
<li>하나 이상의 컨테이너로 구성된다</li>
<li>쿠버네티스는 개별 컨테이너가 아닌 Pod 단위로 스케줄링, 배포, 확장, 삭제를 수행한다</li>
</ul>
<p><strong>Pod 내 컨테이너의 제약</strong></p>
<ul>
<li>항상 동일한 노드에 배치된다</li>
<li>동일한 생명주기를 갖는다 (함께 생성되고 함께 종료된다)</li>
</ul>
<p><strong>Pod 단위로 묶는 이유</strong></p>
<p>애플리케이션과 그 애플리케이션에 종속된 보조 프로세스를 하나의 논리적 단위로 다루기 위해서다.</p>
<h3 id="pod와-container의-역할">Pod와 Container의 역할</h3>
<table>
<thead>
<tr>
<th></th>
<th>Pod</th>
<th>Container</th>
</tr>
</thead>
<tbody><tr>
<td>역할</td>
<td>배포·스케줄링의 단위, 네트워크와 스토리지의 경계</td>
<td>애플리케이션 프로세스의 실행 단위</td>
</tr>
<tr>
<td>관리 주체</td>
<td>쿠버네티스 스케줄러</td>
<td>컨테이너 런타임</td>
</tr>
<tr>
<td>IP 주소</td>
<td>Pod마다 하나씩 할당</td>
<td>Pod의 IP를 공유</td>
</tr>
<tr>
<td>배치</td>
<td>하나의 노드에 배치됨</td>
<td>Pod 내부에 1개 이상 존재</td>
</tr>
<tr>
<td>생명주기</td>
<td>생성과 삭제의 단위</td>
<td>Pod 내부에서 개별 재시작 가능</td>
</tr>
</tbody></table>
<h3 id="pod-내부의-공유-범위">Pod 내부의 공유 범위</h3>
<pre><code>┌─────────── Pod (IP: 10.244.1.7) ───────────┐
│                                             │
│  ┌──────────────┐      ┌──────────────┐    │
│  │ Container A  │      │ Container B  │    │
│  │              │      │              │    │
│  │ 개별 소유:   │      │ 개별 소유:   │    │
│  │  파일시스템  │      │  파일시스템  │    │
│  │  프로세스    │      │  프로세스    │    │
│  └──────┬───────┘      └──────┬───────┘    │
│         │                     │             │
│    ─────┴─────────────────────┴─────        │
│      공유: net namespace (동일 IP)          │
│            ipc namespace                    │
│            Volume                           │
└─────────────────────────────────────────────┘</code></pre><p><strong>공유되는 자원</strong></p>
<table>
<thead>
<tr>
<th>자원</th>
<th>내용</th>
<th>제약</th>
</tr>
</thead>
<tbody><tr>
<td>net namespace</td>
<td>동일한 IP 주소를 가지며 <code>localhost</code>로 상호 호출 가능</td>
<td>같은 포트 번호를 동시에 사용할 수 없음</td>
</tr>
<tr>
<td>ipc namespace</td>
<td>공유 메모리를 통한 통신 가능</td>
<td>—</td>
</tr>
<tr>
<td>Volume</td>
<td>Pod 수준에서 정의하고 각 컨테이너가 마운트</td>
<td>마운트를 선언해야 접근 가능</td>
</tr>
</tbody></table>
<p><strong>공유되지 않는 자원</strong></p>
<table>
<thead>
<tr>
<th>자원</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>mnt namespace</td>
<td>컨테이너별로 자신의 이미지에 포함된 파일시스템을 사용</td>
</tr>
<tr>
<td>pid namespace</td>
<td>컨테이너별로 프로세스 공간이 분리됨</td>
</tr>
</tbody></table>
<h3 id="컨테이너-구성-예시">컨테이너 구성 예시</h3>
<p><strong>컨테이너 1개로 구성된 Pod</strong></p>
<p>가장 일반적인 형태로, 하나의 애플리케이션을 하나의 Pod로 배포한다.</p>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: forday-api
spec:
  containers:
    - name: app
      image: forday-api:1.2.0
      ports:
        - containerPort: 8080</code></pre>
<p><strong>사이드카 — 메인 컨테이너에 보조 기능을 추가</strong></p>
<ul>
<li>구성: 애플리케이션 컨테이너 + 로그 수집 컨테이너</li>
<li>동작: 애플리케이션이 볼륨에 로그를 기록하면, 로그 수집 컨테이너가 같은 볼륨을 읽어 외부로 전송</li>
<li>하나의 Pod로 묶는 이유: 두 컨테이너가 같은 노드에서 같은 볼륨을 바라봐야 하기 때문</li>
</ul>
<pre><code class="language-yaml">apiVersion: v1
kind: Pod
metadata:
  name: forday-api
spec:
  volumes:
    - name: log-volume
      emptyDir: {}
  containers:
    - name: app
      image: forday-api:1.2.0
      ports:
        - containerPort: 8080
      volumeMounts:
        - name: log-volume
          mountPath: /var/log/app
    - name: log-collector
      image: fluent-bit:2.1
      volumeMounts:
        - name: log-volume
          mountPath: /var/log/app</code></pre>
<p><strong>Init Container — 메인 컨테이너보다 먼저 실행</strong></p>
<ul>
<li>동작: 메인 컨테이너보다 먼저 실행되고, 정상 종료되어야만 메인 컨테이너가 시작된다</li>
<li>용도: 데이터베이스 스키마 마이그레이션 등 애플리케이션 기동 전에 반드시 완료되어야 하는 작업</li>
</ul>
<pre><code class="language-yaml">spec:
  initContainers:
    - name: db-migration
      image: forday-migration:1.2.0
  containers:
    - name: app
      image: forday-api:1.2.0</code></pre>
<p><strong>구성 시 판단 기준</strong></p>
<ul>
<li>여러 컨테이너를 하나의 Pod에 넣는 경우: 보조 컨테이너가 메인 컨테이너와 생명주기를 완전히 공유해야 할 때</li>
<li>분리해야 하는 경우: 독립적으로 확장되고 독립적으로 재시작되어야 할 때<ul>
<li>예: 애플리케이션 서버와 데이터베이스는 서로 다른 Pod로 구성한다</li>
</ul>
</li>
</ul>
<h3 id="배포-시-pod의-동작">배포 시 Pod의 동작</h3>
<p><strong>전제 — Pod는 수정하지 않고 교체한다</strong></p>
<p>Pod는 생성된 이후 내용을 변경하지 않는 것을 원칙으로 한다. 새로운 버전의 배포는 다음을 의미한다.</p>
<ul>
<li>기존 Pod의 이미지를 교체하는 것이 <strong>아니다</strong></li>
<li>새로운 Pod를 생성하고 기존 Pod를 삭제하는 것이다</li>
</ul>
<p><strong>동작 흐름</strong> (<code>forday-api:1.2.0</code> → <code>1.3.0</code>)</p>
<pre><code>[1] Deployment의 이미지 버전을 1.3.0으로 변경
        ↓
[2] 새 버전의 Pod 생성
      스케줄러가 배치할 노드를 선택
      → 이미지 다운로드 → 컨테이너 실행
        ↓
[3] 새 Pod가 정상 상태(Ready)가 될 때까지 대기
      Ready가 되어야 Service를 통해 트래픽을 전달받음
        ↓
[4] 기존 버전의 Pod 삭제
      Service에서 먼저 제외한 뒤 종료
        ↓
[5] Pod 개수만큼 [2]~[4]를 반복</code></pre><p><strong>롤링 업데이트(Rolling Update)</strong></p>
<ul>
<li>새 Pod가 정상 동작을 확인받은 뒤에 기존 Pod를 제거한다</li>
<li>배포 중에도 서비스 가능한 Pod가 항상 유지된다</li>
<li>이것이 쿠버네티스가 무중단 배포를 제공하는 기본 원리다</li>
</ul>
<p><strong>Pod IP 변경과 Service</strong></p>
<table>
<thead>
<tr>
<th>문제</th>
<th>해결</th>
</tr>
</thead>
<tbody><tr>
<td>Pod가 새로 생성될 때마다 IP 주소가 변경된다</td>
<td>쿠버네티스는 <strong>Service</strong> 리소스를 제공한다</td>
</tr>
<tr>
<td>Pod IP를 직접 참조하면 배포 이후 통신이 끊긴다</td>
<td>Service가 고정된 접근 지점을 제공하고, 현재 정상 상태인 Pod들에게 요청을 분배한다</td>
</tr>
</tbody></table>
<p>따라서 애플리케이션 간 통신은 항상 Service를 통해 이루어져야 한다.</p>
<h2 id="1-5-클러스터-구조와-pod-관리">1-5. 클러스터 구조와 Pod 관리</h2>
<h3 id="master-node와-worker-node">Master Node와 Worker Node</h3>
<pre><code>┌──────────────── Master Node (Control Plane) ────────────────┐
│  API Server    스케줄러    컨트롤러                          │
│  = 명령을 받고, 배치를 결정하고, 상태를 감시               │
└─────────────────────────┬───────────────────────────────────┘
                          │ 지시
        ┌─────────────────┼─────────────────┐
        ▼                 ▼                 ▼
┌──────────────┐  ┌──────────────┐  ┌──────────────┐
│ Worker Node1 │  │ Worker Node2 │  │ Worker Node3 │
│  Pod  Pod    │  │  Pod         │  │  Pod  Pod    │
└──────────────┘  └──────────────┘  └──────────────┘
        = 실제로 Pod(컨테이너)가 실행되는 서버</code></pre><table>
<thead>
<tr>
<th>구분</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Master Node</strong></td>
<td>클러스터의 두뇌. 사용자 명령을 받고, Pod를 어느 노드에 배치할지 결정하고, 현재 상태가 원하는 상태와 같은지 계속 감시한다</td>
</tr>
<tr>
<td><strong>Worker Node</strong></td>
<td>실제 작업 서버. Master의 지시를 받아 Pod를 실행하고, 자신의 상태를 Master에 보고한다</td>
</tr>
</tbody></table>
<p>사용자는 Worker Node를 직접 조작하지 않는다. Master에게 &quot;이렇게 해달라&quot;고 선언하면 Master가 Worker에 배분한다.</p>
<h3 id="replicaset">ReplicaSet</h3>
<p><strong>역할: Pod의 개수를 지정한 수만큼 유지한다</strong></p>
<pre><code>ReplicaSet (replicas: 3)
        │
        ├── Pod A  (Running)
        ├── Pod B  (Running)
        └── Pod C  (Running)

  Pod C 가 죽으면 ↓

        ├── Pod A  (Running)
        ├── Pod B  (Running)
        └── Pod D  (새로 생성)   ← ReplicaSet이 자동 생성</code></pre><ul>
<li>Pod가 죽거나 노드에 장애가 나면 새 Pod를 만들어 개수를 채운다 → <strong>Auto Healing</strong></li>
<li>Pod 개수를 늘리라고 지시하면 그만큼 새로 생성한다 → <strong>Auto Scaling</strong></li>
<li>죽은 Pod를 되살리는 것이 아니라, 항상 <strong>새 Pod를 생성</strong>한다</li>
</ul>
<h3 id="계층-관계">계층 관계</h3>
<pre><code>Deployment  →  ReplicaSet  →  Pod  →  Container
   배포/버전 관리    개수 유지     배포 단위    실행 단위</code></pre><p>사용자는 보통 ReplicaSet을 직접 만들지 않고 Deployment를 만든다. Deployment가 ReplicaSet을 생성하고, ReplicaSet이 Pod를 관리한다. 새 버전을 배포하면 Deployment가 새 ReplicaSet을 만들어 Pod를 교체한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[단일 EC2 + Nginx로 구현하는 Blue-Green 무중단 배포]]></title>
            <link>https://velog.io/@jina_ham/%EB%8B%A8%EC%9D%BC-EC2-Nginx%EB%A1%9C-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-Blue-Green-%EB%AC%B4%EC%A4%91%EB%8B%A8-%EB%B0%B0%ED%8F%AC</link>
            <guid>https://velog.io/@jina_ham/%EB%8B%A8%EC%9D%BC-EC2-Nginx%EB%A1%9C-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94-Blue-Green-%EB%AC%B4%EC%A4%91%EB%8B%A8-%EB%B0%B0%ED%8F%AC</guid>
            <pubDate>Tue, 08 Sep 2026 10:17:08 GMT</pubDate>
            <description><![CDATA[<p>배포할 때마다 서비스가 몇 초라도 끊긴다면, 그 몇 초 사이에 발생하는 요청은 전부 실패한다. 이 글은 별도의 로드밸런서나 여러 대의 서버 없이, <strong>EC2 한 대 + Docker + Nginx</strong> 조합만으로 무중단 배포(Blue-Green Deployment)를 구현한 과정을 정리한 것이다.</p>
<h2 id="목차">목차</h2>
<ol>
<li>Blue-Green 배포란?</li>
<li>단일 EC2에서 Blue-Green을 구성하는 이유</li>
<li>전체 아키텍처</li>
<li>트래픽 전환 흐름</li>
<li>구현 과정</li>
<li>마무리</li>
</ol>
<hr>
<h2 id="1-blue-green-배포란">1. Blue-Green 배포란?</h2>
<p>Blue-Green 배포는 <strong>동일한 서비스를 두 개의 환경(Blue / Green)으로 나눠 운영</strong>하다가, 배포 시점에 트래픽을 한쪽에서 다른 쪽으로 전환하는 방식이다.</p>
<ul>
<li>평소에는 두 환경 중 하나(예: Blue)만 실제 트래픽을 받는다.</li>
<li>새 버전을 배포할 때는 반대편 환경(Green)에 새 버전을 띄운다.</li>
<li>Green이 정상 동작하는지 헬스체크로 확인한다.</li>
<li>문제가 없으면 트래픽을 Green으로 전환한다.</li>
<li>이전 환경(Blue)은 종료하거나 다음 배포를 위한 대기 상태로 남긴다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jina_ham/post/8e084396-e4cb-4932-b482-1b72b79da817/image.png" alt=""></p>
<p>핵심은 <strong>&quot;새 버전을 미리 켜두고, 준비가 끝난 뒤에 트래픽만 옮긴다&quot;</strong>는 점이다. 기존 버전을 내리고 새 버전을 올리는 방식(Rolling 없는 단순 재배포)과 달리 서비스 중단 구간이 없다.</p>
<h2 id="2-단일-ec2에서-blue-green을-구성하는-이유">2. 단일 EC2에서 Blue-Green을 구성하는 이유</h2>
<p>Blue-Green 배포는 보통 ALB(Application Load Balancer)와 여러 대의 EC2, 또는 ECS/Kubernetes 같은 오케스트레이션 도구를 이용해 구성한다. 하지만 이런 구성은 비용과 운영 복잡도가 올라간다.</p>
<p>이번 구조는 <strong>EC2 한 대 안에서 Docker 컨테이너 두 개(Blue/Green)를 서로 다른 포트로 띄우고, 그 앞단의 Nginx가 리버스 프록시 역할을 하면서 트래픽을 전환</strong>하는 방식이다. 서버 한 대만으로도 무중단 배포가 가능하다는 점에서, 초기 단계의 서비스나 비용에 민감한 프로젝트에 적합하다.</p>
<p>물론 트레이드오프도 있다. EC2 한 대에 의존하기 때문에 해당 인스턴스 자체가 죽으면 서비스 전체가 중단되고, 두 컨테이너가 동시에 떠 있는 시점에는 CPU/메모리 자원을 동시에 소모한다. 인스턴스 사양을 여유 있게 잡아야 하는 이유다.</p>
<h2 id="3-전체-아키텍처">3. 전체 아키텍처</h2>
<p><img src="https://velog.velcdn.com/images/jina_ham/post/9ced9f0f-7e1d-437a-9836-e78dd74f65be/image.png" alt=""></p>
<p>포트 구조를 정리하면 다음과 같다.</p>
<table>
<thead>
<tr>
<th>구성 요소</th>
<th>포트</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>Nginx</td>
<td>80</td>
<td>외부 요청을 받아 Blue/Green으로 라우팅</td>
</tr>
<tr>
<td>Blue 컨테이너</td>
<td>8080 (Host) → 8080 (Container)</td>
<td>운영 트래픽을 받는 환경</td>
</tr>
<tr>
<td>Green 컨테이너</td>
<td>8081 (Host) → 8080 (Container)</td>
<td>대기 또는 신규 배포 환경</td>
</tr>
</tbody></table>
<p>Docker의 포트 매핑 문법은 <code>&quot;호스트포트:컨테이너포트&quot;</code>이다. Spring Boot는 컨테이너 내부에서 항상 8080번을 리슨하고, 이 포트가 호스트(EC2)의 8080 또는 8081번으로 노출되는 구조다. Nginx는 컨테이너 내부 포트를 알 필요가 없고, EC2 호스트 기준 포트(<code>localhost:8080</code>, <code>localhost:8081</code>)만 바라본다.</p>
<pre><code>Spring Boot (컨테이너 내부 8080)
   ↑
Docker 포트 매핑 (8080:8080 또는 8081:8080)
   ↑
EC2 Host Port (8080 / 8081)
   ↑
Nginx (upstream이 localhost:8080 또는 localhost:8081 참조)</code></pre><h2 id="4-트래픽-전환-흐름">4. 트래픽 전환 흐름</h2>
<p>배포 스크립트가 실행됐을 때 내부적으로 일어나는 순서는 다음과 같다.</p>
<p><img src="https://velog.velcdn.com/images/jina_ham/post/acb726a2-cf26-4d2d-a5b9-366007e5b52d/image.png" alt=""></p>
<p>여기서 핵심은 <strong>Nginx 설정 자체를 바꾸는 게 아니라, <code>service-env.inc</code>라는 변수 파일 하나만 갈아끼운다는 것</strong>이다. 이 파일에는 <code>$service_url</code> 변수 값(blue 또는 green)만 들어 있고, <code>nginx -s reload</code>는 워커 프로세스를 무중단으로 재시작하기 때문에 서비스가 끊기지 않는다.</p>
<h2 id="5-구현-과정">5. 구현 과정</h2>
<h3 id="51-ec2-초기-설정---jdk-설치">5.1 EC2 초기 설정 - JDK 설치</h3>
<pre><code class="language-bash"># 우분투 버전
sudo apt update

# jdk 17 설치
sudo apt install openjdk-17-jdk -y

# 설치 확인
java -version</code></pre>
<p>Spring Boot 애플리케이션을 EC2에서 직접 빌드/실행할 일은 없지만(빌드는 GitHub Actions에서, 실행은 Docker 컨테이너로), Docker 이미지 빌드나 로컬 디버깅을 대비해 JDK를 설치해둔다.</p>
<h3 id="52-docker--docker-compose-설치">5.2 Docker / Docker Compose 설치</h3>
<pre><code class="language-bash">sudo apt-get update &amp;&amp; \
    sudo apt-get install -y apt-transport-https ca-certificates curl software-properties-common &amp;&amp; \
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - &amp;&amp; \
    sudo apt-key fingerprint 0EBFCD88 &amp;&amp; \
    sudo add-apt-repository &quot;deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable&quot; &amp;&amp; \
    sudo apt-get update &amp;&amp; \
    sudo apt-get install -y docker-ce &amp;&amp; \
    sudo usermod -aG docker ubuntu &amp;&amp; \
    sudo curl -L &quot;https://github.com/docker/compose/releases/download/1.23.2/docker-compose-$(uname -s)-$(uname -m)&quot; -o /usr/local/bin/docker-compose &amp;&amp; \
    sudo chmod +x /usr/local/bin/docker-compose &amp;&amp; \
    sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose

# 설치 확인
docker -v
docker compose version

# 데몬 상태 확인
systemctl status docker</code></pre>
<p>Docker 저장소를 apt에 등록하고 <code>docker-ce</code>를 설치한 뒤, <code>ubuntu</code> 유저를 <code>docker</code> 그룹에 추가해 매번 <code>sudo</code> 없이 명령어를 쓸 수 있게 한다. 이 스크립트는 EC2를 새로 띄울 때마다 반복하기보다, <strong>User Data(EC2 초기 실행 스크립트)에 등록해두면 인스턴스 생성과 동시에 자동으로 설치</strong>되도록 할 수 있다.</p>
<h3 id="53-nginx-설치">5.3 Nginx 설치</h3>
<pre><code class="language-bash"># 패키지 목록 최신화
sudo apt update

# nginx 설치에 필요한 라이브러리
sudo apt install curl gnupg2 ca-certificates lsb-release ubuntu-keyring

# nginx 공식 서명 키 등록
curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor \
    | sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg &gt;/dev/null

# 저장소 등록
echo &quot;deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \
http://nginx.org/packages/ubuntu $(lsb_release -cs) nginx&quot; \
    | sudo tee /etc/apt/sources.list.d/nginx.list

# nginx 설치
sudo apt update
sudo apt install nginx</code></pre>
<p>nginx.org의 공식 패키지를 쓰기 위해 서명 키를 등록하고 저장소를 추가하는 과정이다. Ubuntu 기본 저장소의 nginx보다 최신 버전을 안전하게 받을 수 있다.</p>
<pre><code class="language-bash">sudo systemctl start nginx
sudo systemctl status nginx</code></pre>
<p>설치 후 <code>systemctl</code>로 서비스를 시작하고 상태를 확인한다.</p>
<h3 id="54-nginx-설정---bluegreen-업스트림-구성">5.4 Nginx 설정 - Blue/Green 업스트림 구성</h3>
<p><code>/etc/nginx/conf.d/default.conf</code>를 아래와 같이 수정한다.</p>
<pre><code class="language-nginx"># =========================
# Blue / Green Upstream
# =========================

# 운영 중인 서버 (localhost:8080)
upstream blue {
    server localhost:8080;
}

# 대기 또는 신규 배포 서버 (localhost:8081)
upstream green {
    server localhost:8081;
}

# =========================
# Nginx Server
# =========================

server {
    listen 80;
    server_name localhost;

    # 현재 트래픽을 어디로 보낼지는 이 파일에서 결정한다.
    # nginx 재시작 없이 이 include 파일만 바꾸면 Blue/Green 전환이 가능하다.
    include /etc/nginx/conf.d/service-env.inc;

    location / {
        # 핵심: $service_url 값에 따라 blue 또는 green으로 프록시
        proxy_pass http://$service_url;

        # 클라이언트 실제 IP 전달
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # 원본 Host 유지 (OAuth, CORS 대응에 필요)
        proxy_set_header Host $http_host;

        # 타임아웃 (Spring Boot 권장값)
        proxy_connect_timeout 5s;
        proxy_send_timeout 30s;
        proxy_read_timeout 30s;
    }

    error_page 500 502 503 504 /50x.html;
    location = /50x.html {
        root /usr/share/nginx/html;
    }
}</code></pre>
<p>이 설정의 포인트는 <code>proxy_pass http://$service_url;</code>이다. <code>$service_url</code>이 <code>blue</code>면 <code>upstream blue</code>(즉 <code>localhost:8080</code>)로, <code>green</code>이면 <code>upstream green</code>(<code>localhost:8081</code>)으로 요청이 전달된다. 어느 쪽으로 보낼지는 아래 <code>service-env.inc</code> 파일 하나가 결정한다.</p>
<h3 id="55-트래픽-스위치-변수-파일">5.5 트래픽 스위치 변수 파일</h3>
<p><code>/etc/nginx/conf.d/service-env.inc</code></p>
<pre><code class="language-nginx"># 최초 기동 시에는 blue 환경으로 시작
set $service_url blue;</code></pre>
<p>배포 스크립트가 이 한 줄을 <code>set $service_url green;</code>으로 바꾸고 <code>nginx -s reload</code>만 실행하면, Nginx 프로세스를 새로 켜지 않고도 트래픽 대상이 바뀐다.</p>
<pre><code class="language-bash"># 문법 검사
sudo nginx -t

# 무중단 반영
sudo nginx -s reload</code></pre>
<p><code>nginx -s reload</code>는 기존 연결을 끊지 않고 워커 프로세스만 교체하기 때문에, 이 시점에 들어오는 요청도 유실되지 않는다.</p>
<blockquote>
<p><strong>참고</strong>: 로드밸런서(ALB)를 앞단에 둔다면, ALB의 리스너 포트(80)와 대상 그룹 포트(80)를 맞추고, EC2 보안 그룹에서 ALB 보안 그룹으로부터의 80번 포트 인바운드를 허용해야 한다.</p>
</blockquote>
<h3 id="56-헬스체크-엔드포인트">5.6 헬스체크 엔드포인트</h3>
<p>배포 스크립트가 새로 띄운 컨테이너(Green)가 정상 기동됐는지 확인하는 용도로 헬스체크 API를 둔다.</p>
<pre><code class="language-java">@Slf4j
@RestController
public class TestController {

    @Value(&quot;${server.port}&quot;)
    private int serverPort;

    @Value(&quot;${server.env}&quot;)
    private String serverEnv;

    @Value(&quot;${serverName}&quot;)
    private String serverName;

    @GetMapping(&quot;/health_check&quot;)
    public TestResponseDto healthCheck() {
        return new TestResponseDto(&quot;테스트에 성공하였습니다.&quot;, serverPort, serverEnv, serverName);
    }

    @GetMapping(&quot;/env&quot;)
    public String env() {
        return serverEnv;
    }
}</code></pre>
<p><code>server.env</code>, <code>serverName</code> 같은 값은 Spring 프로필(blue/green)에 따라 다르게 주입되며, 지금 응답하는 컨테이너가 blue인지 green인지 확인하는 용도로 쓴다. <code>/health_check</code>는 배포 스크립트가 새 컨테이너의 기동 완료 여부를 폴링하는 엔드포인트로 사용된다.</p>
<h3 id="57-spring-boot-설정---프로필-분리">5.7 Spring Boot 설정 - 프로필 분리</h3>
<p><code>application.yml</code>에서 blue/green/common 세 프로필로 나눠 관리한다. 아래 값 중 자격 증명, 엔드포인트 등 민감한 정보는 실제 값 대신 환경변수/플레이스홀더로 표시했다.</p>
<pre><code class="language-yaml">spring:
  profiles:
    group:
      blue: blue, common
      green: green, common
  application:
    name: ForDay

---
# Blue
spring:
  config:
    activate:
      on-profile: blue

server:
  port: 8080   # 컨테이너 내부 포트 (blue/green 모두 동일)
  env: blue
serverName: blue_server

---
# Green
spring:
  config:
    activate:
      on-profile: green

server:
  port: 8080
  env: green
serverName: green_server

---
# 공통 설정
spring:
  config:
    activate:
      on-profile: common

  datasource:
    url: jdbc:mysql://${DB_HOST}:3306/${DB_NAME}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}

  jpa:
    database: mysql
    database-platform: org.hibernate.dialect.MySQLDialect
    generate-ddl: false
    hibernate:
      ddl-auto: update
    show-sql: false

  data:
    redis:
      host: ${REDIS_HOST}
      port: 6379

  jwt:
    secret: ${JWT_SECRET}
    access-expiration: 180
    refresh-expiration: 14

server:
  address: 0.0.0.0

management:
  endpoints:
    web:
      exposure:
        include: health</code></pre>
<p><code>blue</code>/<code>green</code> 프로필은 <code>server.env</code>와 <code>serverName</code>만 다르고, 둘 다 컨테이너 내부에서는 동일하게 8080 포트를 리슨한다(외부 노출 포트만 Docker 실행 시 8080/8081로 갈린다). DB, Redis, JWT 같은 민감한 값은 코드/설정 파일에 하드코딩하지 않고 환경변수나 GitHub Actions Secrets로 주입하는 것을 권장한다.</p>
<h3 id="58-dockerfile">5.8 Dockerfile</h3>
<pre><code class="language-dockerfile">FROM eclipse-temurin:17-jdk

WORKDIR /app

COPY build/libs/*SNAPSHOT.jar app.jar

EXPOSE 8080

ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;app.jar&quot;]</code></pre>
<p>Gradle로 빌드된 jar를 컨테이너에 복사하고 8080 포트로 실행하는 단순한 구조다. blue/green 이미지는 완전히 동일하며, 차이는 실행 시 <code>SPRING_PROFILES_ACTIVE</code> 환경변수와 호스트 포트 매핑에서만 발생한다.</p>
<h3 id="59-docker-compose-파일-참고용">5.9 docker-compose 파일 (참고용)</h3>
<p>로컬 테스트나 수동 배포 시 참고할 수 있는 compose 파일이다. (실제 CI/CD 배포는 5.10의 <code>deploy.yml</code>에서 <code>docker run</code>으로 직접 처리한다.)</p>
<pre><code class="language-yaml"># docker-compose-blue.yml
version: &#39;3.8&#39;

services:
  blue:
    image: &lt;ECR_REPOSITORY_URI&gt;:latest
    container_name: blue
    ports:
      - &quot;8080:8080&quot;   # EC2 외부 노출 : 컨테이너 내부(Spring Boot)
    environment:
      - SPRING_PROFILES_ACTIVE=blue
      - ENV=blue
    restart: always</code></pre>
<pre><code class="language-yaml"># docker-compose-green.yml
version: &#39;3.8&#39;

services:
  green:
    image: &lt;ECR_REPOSITORY_URI&gt;:latest
    container_name: green
    ports:
      - &quot;8081:8080&quot;
    environment:
      - SPRING_PROFILES_ACTIVE=green
      - ENV=green
    restart: always</code></pre>
<p>두 파일의 차이는 컨테이너 이름, 호스트 포트(8080/8081), <code>SPRING_PROFILES_ACTIVE</code> 값뿐이다.</p>
<h3 id="510-github-actions로-cicd-자동화">5.10 GitHub Actions로 CI/CD 자동화</h3>
<p><code>main</code> 브랜치에 푸시되면 빌드 → ECR 푸시 → EC2 SSH 접속 → Blue/Green 전환까지 자동으로 처리한다.</p>
<p><strong>Build Job</strong></p>
<pre><code class="language-yaml">name: Deploy To EC2 (Blue-Green)

on:
  push:
    branches: [ &quot;main&quot; ]

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 17

      - name: Create application.yml
        run: echo &quot;${{ secrets.APPLICATION_PROPERTIES }}&quot; &gt; ./src/main/resources/application.yml

      - name: Build Spring Boot
        run: |
          chmod +x ./gradlew
          ./gradlew clean build -x test

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-region: ${{ secrets.AWS_REGION }}
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

      - name: Login to ECR
        run: |
          aws ecr get-login-password --region ${{ secrets.AWS_REGION }} \
          | docker login --username AWS --password-stdin ${{ secrets.ECR_REGISTRY }}

      - name: Build &amp; Push Docker Image
        run: |
          docker build -t forday .
          docker tag forday:latest ${{ secrets.ECR_REGISTRY }}/forday:latest
          docker push ${{ secrets.ECR_REGISTRY }}/forday:latest</code></pre>
<p><code>application.yml</code>처럼 민감 정보가 담긴 설정 파일은 리포지토리에 커밋하지 않고, GitHub Actions Secrets(<code>APPLICATION_PROPERTIES</code>)에 전체 내용을 저장해뒀다가 빌드 시점에 파일로 복원한다. AWS 계정 ID, ECR 리전 등도 코드에 노출하지 않고 Secrets(<code>ECR_REGISTRY</code>, <code>AWS_REGION</code>)로 관리한다.</p>
<p><strong>Deploy Job</strong></p>
<pre><code class="language-yaml">  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Configure SSH
        run: |
          mkdir -p ~/.ssh
          echo &quot;${{ secrets.SSH_PRIVATE_KEY }}&quot; &gt; ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
          cat &lt;&lt;EOF &gt;&gt; ~/.ssh/config
          Host ec2-server
            HostName ${{ secrets.EC2_PUBLIC_IP }}
            User ubuntu
            IdentityFile ~/.ssh/id_rsa
            StrictHostKeyChecking no
          EOF

      - name: Blue-Green Deploy via SSH
        run: |
          ssh ec2-server &lt;&lt; &#39;EOF&#39;
            set -e

            aws ecr get-login-password --region ${{ secrets.AWS_REGION }} \
              | docker login --username AWS --password-stdin ${{ secrets.ECR_REGISTRY }}

            # 1) service-env.inc 파일이 없으면 blue로 초기화
            if [ ! -f /etc/nginx/conf.d/service-env.inc ]; then
              echo &quot;set \$service_url blue;&quot; | sudo tee /etc/nginx/conf.d/service-env.inc
            fi

            # 2) 현재 트래픽이 blue/green 중 어디로 가고 있는지 확인
            CURRENT_VAL=$(grep -oP &#39;(?&lt;=set \$service_url ).*(?=;)&#39; /etc/nginx/conf.d/service-env.inc || echo &quot;blue&quot;)

            # 3) 반대편을 배포 대상으로 지정
            if [ &quot;$CURRENT_VAL&quot; = &quot;blue&quot; ]; then
              TARGET=&quot;green&quot;; TARGET_PORT=8081; OLD_TARGET=&quot;blue&quot;
            else
              TARGET=&quot;blue&quot;; TARGET_PORT=8080; OLD_TARGET=&quot;green&quot;
            fi

            echo &quot;배포 대상: $TARGET (포트: $TARGET_PORT)&quot;
            docker pull ${{ secrets.ECR_REGISTRY }}/forday:latest

            # 4) 기존에 같은 이름의 컨테이너가 있다면 정리 후 새로 기동
            docker stop $TARGET || true
            docker rm $TARGET || true

            docker run -d \
              --name $TARGET \
              --restart=always \
              --network forday-net \
              -v /etc/localtime:/etc/localtime:ro \
              -e TZ=Asia/Seoul \
              -e SPRING_PROFILES_ACTIVE=$TARGET \
              -p $TARGET_PORT:8080 \
              ${{ secrets.ECR_REGISTRY }}/forday:latest

            # 5) 헬스체크 - 최대 20회, 5초 간격으로 재시도
            for i in {1..20}; do
              if curl -sf http://localhost:$TARGET_PORT/health_check; then
                HEALTH_OK=true
                break
              fi
              echo &quot;대기 중... ($i/20)&quot;
              sleep 5
            done

            if [ &quot;$HEALTH_OK&quot; != &quot;true&quot; ]; then
              echo &quot;헬스 체크 실패&quot;
              docker logs $TARGET
              exit 1
            fi

            # 6) 헬스체크 통과 시에만 Nginx 트래픽 전환
            echo &quot;set \$service_url $TARGET;&quot; | sudo tee /etc/nginx/conf.d/service-env.inc
            sudo nginx -t &amp;&amp; sudo nginx -s reload

            # 7) 이전 컨테이너 정리
            docker stop $OLD_TARGET || true
            docker rm $OLD_TARGET || true
            docker image prune -af
          EOF</code></pre>
<p>이 스크립트에서 배포 실패를 막는 안전장치는 두 가지다.</p>
<ul>
<li><strong>헬스체크를 통과하기 전에는 절대 Nginx 설정을 바꾸지 않는다.</strong> <code>HEALTH_OK</code> 플래그가 <code>true</code>가 되지 않으면 <code>exit 1</code>로 종료되고, 기존 컨테이너(트래픽을 받고 있던 쪽)는 그대로 유지된다.</li>
<li><strong>이전 컨테이너는 전환이 끝난 뒤에 종료한다.</strong> 전환 도중 문제가 생겨도 롤백할 대상이 남아있는 구조다.</li>
</ul>
<p>민감 정보(AWS 자격 증명, ECR 레지스트리 주소, EC2 IP, SSH 키 등)는 모두 GitHub Actions Secrets로 분리해 워크플로 코드에는 값이 노출되지 않는다.</p>
<h2 id="6-마무리">6. 마무리</h2>
<p>이 구조를 한 줄로 요약하면, <strong>&quot;Nginx 앞단은 그대로 두고, 뒷단의 upstream 대상만 변수 파일 하나로 바꿔치기한다&quot;</strong>는 것이다. 배포 순서는 항상 아래와 같이 진행된다.</p>
<ol>
<li>반대편(대기) 컨테이너에 새 버전을 띄운다.</li>
<li>헬스체크로 정상 기동을 확인한다.</li>
<li>통과하면 Nginx의 <code>$service_url</code>만 바꾸고 리로드한다.</li>
<li>이전 컨테이너를 종료한다.</li>
</ol>
<p>서버 한 대로 구현할 수 있어 비용 부담이 적고 구조도 단순하지만, 인스턴스 자체의 장애에는 취약하다는 한계가 있다. 트래픽과 가용성 요구 수준이 올라가면 ALB + 다중 EC2, 혹은 ECS/EKS 기반의 Blue-Green/Canary 배포로 확장하는 것이 다음 단계가 될 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Https 적용후 Post 요청 connection timeout 발생]]></title>
            <link>https://velog.io/@jina_ham/Https-%EC%A0%81%EC%9A%A9%ED%9B%84-Post-%EC%9A%94%EC%B2%AD-connection-timeout-%EB%B0%9C%EC%83%9D</link>
            <guid>https://velog.io/@jina_ham/Https-%EC%A0%81%EC%9A%A9%ED%9B%84-Post-%EC%9A%94%EC%B2%AD-connection-timeout-%EB%B0%9C%EC%83%9D</guid>
            <pubDate>Sat, 24 Jan 2026 14:12:24 GMT</pubDate>
            <description><![CDATA[<h3 id="✅-상황">✅ 상황</h3>
<p>원래 서버에서 http를 사용하고 있다가 https로 배포한 프론트와의 통신을 위해 
가비아 도메인을 ACM을 통해 인증서를 발급받아 새로 https를 적용하였다!</p>
<p>GET 요청으로 health_check를 보냈을 때는 응답이 잘 왔으나 </p>
<h3 id="✅-문제">✅ 문제</h3>
<p>POST 요청을 보내니 connection timeout이 발생하는 것이다.
<img src="https://velog.velcdn.com/images/jina_ham/post/9f71dbeb-88fd-4cfe-ab84-69767ec6fcb4/image.png" alt=""></p>
<p>나는 서버에서 nginx를 활용한 무중단 배포 blue-green을 사용하고 있었다. </p>
<h3 id="✅-원인">✅ 원인</h3>
<ul>
<li>검색해보니</li>
<li>HTTPS 암호화로 인해 패킷이 무거워지거나, nginx의 기본 타임아웃 설정이 데이터가 포함된 POST 요청을 처리하기에 너무 짧은 것이었다.</li>
</ul>
<h3 id="✅-해결">✅ 해결</h3>
<ul>
<li>Nginx 설정 파일 수정하기 (경로 /etc/nginx/conf.d/default.conf)</li>
</ul>
<pre><code>location / {
    proxy_connect_timeout 300;
    proxy_send_timeout 300;
    proxy_read_timeout 300;
    send_timeout 300;
    client_max_body_size 20M;
    ...
}</code></pre><p><img src="https://velog.velcdn.com/images/jina_ham/post/12b0ed6c-0d0c-4708-b3fb-17c0c00e7324/image.png" alt=""></p>
<ul>
<li>위 내용 작성 후 </li>
</ul>
<pre><code>sudo systemctl daemon-reload (서비스 설정 변경 인식)

sudo systemctl reload nginx (Nginx 엔진에 설정 반영)</code></pre><p>다시 postman으로 요청을 보내면 응답이 정상적으로 온다!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[비전공자도 이해할 수 있는 Docker 입문/실전 (인프런-박재성)]]></title>
            <link>https://velog.io/@jina_ham/%EB%B9%84%EC%A0%84%EA%B3%B5%EC%9E%90%EB%8F%84-%EC%9D%B4%ED%95%B4%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-Docker-%EC%9E%85%EB%AC%B8%EC%8B%A4%EC%A0%84-%EC%9D%B8%ED%94%84%EB%9F%B0-%EB%B0%95%EC%9E%AC%EC%84%B1</link>
            <guid>https://velog.io/@jina_ham/%EB%B9%84%EC%A0%84%EA%B3%B5%EC%9E%90%EB%8F%84-%EC%9D%B4%ED%95%B4%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-Docker-%EC%9E%85%EB%AC%B8%EC%8B%A4%EC%A0%84-%EC%9D%B8%ED%94%84%EB%9F%B0-%EB%B0%95%EC%9E%AC%EC%84%B1</guid>
            <pubDate>Sun, 09 Nov 2025 05:14:14 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.inflearn.com/course/%EB%B9%84%EC%A0%84%EA%B3%B5%EC%9E%90-docker-%EC%9E%85%EB%AC%B8-%EC%8B%A4%EC%A0%84/dashboard">https://www.inflearn.com/course/%EB%B9%84%EC%A0%84%EA%B3%B5%EC%9E%90-docker-%EC%9E%85%EB%AC%B8-%EC%8B%A4%EC%A0%84/dashboard</a></p>
<p>초기에 개발에 입문 했을 때 사람들이 Docker Docker 하길래 도대체 무슨 기술인지 궁금했다. 백엔드 개발을 시작하면서 다양한 기술들을 배우고 싶었고 인프런 강의를 통해 원하는 기술들을 공부하는데 많은 도움이 되었다. </p>
<p>특히 박재성님 강의 도움을 정말 많이 받았다. 
지금 포스팅은 Docker 강의 후기이지만 정말 많은 강의들을 들었다. 
말그대로 전반적인 강의들은 필요한 액기스만 압축해놓은 거의 하루종일 시간만 투자하면 해당 기술을 배울 수 있다. </p>
<p>무엇보다 박재성님은 수업 자료를 항상 너무나도 꼼꼼하게 노션, pdf 형식으로 모두 제공해주시는게 정말 좋았다. 그만큼 수업에 더 깊이 집중할 수 있었다. </p>
<p>강의는 다음과 같았다.</p>
<ul>
<li>Docker 기본 개념</li>
<li>현업에서 자주 사용하는 Docker CLI 익히기</li>
<li>도커 볼륨을 활용해 데이터 유실 방지하기</li>
<li>Dockerfile 활용해 이미지 직접 만들기 </li>
<li>Docker Compose를 활용해 컨테이너 관리하기</li>
<li>Docker Compose를 활용해 2개 이상의 컨테이너 관리하기</li>
<li>AWS EC2에 서버 배포해보기 </li>
<li>AWS EC2에서 Docker를 활용해 배포해보기</li>
</ul>
<p>박재성님의 다른 강의들은 더 깊숙한 개념을 알고 싶다면 추가적인 학습이 필요한 것들도 있지만 
Docker강의는 이 강의만으로도 Docker를 정복할 수 있을 정도로 강의 구성이 너무 좋았다.</p>
<p>그래서 Docker 강의 덕분에 서버 배포도 이제 수월하게 할 수 있게 되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로그래머스 42576번: 완주하지 못한 선수(Java, 해시)]]></title>
            <link>https://velog.io/@jina_ham/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-42576%EB%B2%88-%EC%99%84%EC%A3%BC%ED%95%98%EC%A7%80-%EB%AA%BB%ED%95%9C-%EC%84%A0%EC%88%98Java-%ED%95%B4%EC%8B%9C</link>
            <guid>https://velog.io/@jina_ham/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-42576%EB%B2%88-%EC%99%84%EC%A3%BC%ED%95%98%EC%A7%80-%EB%AA%BB%ED%95%9C-%EC%84%A0%EC%88%98Java-%ED%95%B4%EC%8B%9C</guid>
            <pubDate>Wed, 08 Oct 2025 06:18:17 GMT</pubDate>
            <description><![CDATA[<h2 id="☑️-문제">☑️ 문제</h2>
<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/42576">https://school.programmers.co.kr/learn/courses/30/lessons/42576</a></p>
<p><img src="https://velog.velcdn.com/images/jina_ham/post/f57526f9-2985-4379-8dbc-9c3d4661c0ec/image.png" alt=""></p>
<h2 id="✔️관련-알고리즘-개념">✔️관련 알고리즘 개념</h2>
<p><a href="https://www.notion.so/228f769b85668073a96ac0cc87e18ffb?pvs=21">해시</a> </p>
<h2 id="☑️-문제-분석">☑️ 문제 분석</h2>
<ul>
<li><p>해당 문제는 HashMap의 key와 value쌍을 이용하여 문제를 해결하는 것이다. 여기서 중요한 것은 예제 3과 같이</p>
<blockquote>
<p>&quot;mislav&quot;는 참여자 명단에는 두 명이 있지만, 완주자 명단에는 한 명밖에 없기 때문에 한명은 완주하지 못했습니다.</p>
</blockquote>
<p>  동명이인이 있는 경우를 처리하는 것이다.</p>
</li>
<li><p>예제 3에 대해 분석해보자</p>
<ul>
<li>completion 정보를 hashMap에 저장해둔다.</li>
<li>이후 participant를 반복문으로 돌면서 hashMap에 저장된 참가자 정보가 없다면 해당 참가자가 완주하지 못한 것으로 판단하여 해당 참가자의 이름을 반환한다.</li>
</ul>
<ol>
<li>completion 정보 저장<ol>
<li>stanko 삽입 (여기서 value값은 key에 해당하는 이름을 가진 사람수이다.)</li>
</ol>
</li>
</ol>
</li>
</ul>
<pre><code>        | key | value |
        | --- | --- |
        | stanko  | 1 |
    2. ana 삽입


        | key | value |
        | --- | --- |
        | stanko  | 1 |
        | ana  | 1 |
    3. mislav 삽입 .


        | key | value |
        | --- | --- |
        | stanko  | 1 |
        | ana  | 1 |
        | mislav  | 1 |
2. participant 정보다 hashMap에 포함된건지 확인


    | key | value |
    | --- | --- |
    | stanko  | 1 |
    | ana  | 1 |
    | mislav  | 1 |
    1. mislav 확인
        1. 현재 hashMap에는 mislav가 존재한다. 해당 참가자를 확인했으므로 hashMap에서 지워준다.


            | key | value |
            | --- | --- |
            | stanko  | 1 |
            | ana  | 1 |
    2. stanko확인
        1. 현재 hashMap에는 stanko가 존재한다. 해당 참가자를 확인했으므로 hashMap에서 지워준다.


            | key | value |
            | --- | --- |
            | ana  | 1 |
    3. mislav 확인
        1. 현재 hashMap에는 mislav이 존재하지 않는다. 즉 완주자 명단에 존재하지 않는 것이므로 해당 참가자의 이름을 반환한다. </code></pre><h2 id="☑️-코드">☑️ 코드</h2>
<pre><code class="language-jsx">import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;

class Solution {
    public String solution(String[] participant, String[] completion) {
        HashMap&lt;String, Integer&gt; map = new HashMap&lt;&gt;();
        for (String s : completion) {
            if(map.containsKey(s)) {
                Integer value = map.get(s);
                map.put(s, value+1);
            }else {
                map.put(s, 1);
            }
        }

        String answer = &quot;&quot;;
        for (String s : participant) {
            if(map.containsKey(s)) {
                Integer value = map.get(s);
                if(value.equals(1)) map.remove(s);
                else map.put(s, value-1);
            } else {
                answer = s;
                break;
            }
        }

        return answer;
    }

    public static void main(String[] args) {
        String[] participant = {&quot;mislav&quot;, &quot;stanko&quot;, &quot;mislav&quot;, &quot;ana&quot;};
        String[] completion = {&quot;stanko&quot;, &quot;ana&quot;, &quot;mislav&quot;};
        System.out.println(new Solution().solution(participant, completion));
    }
}</code></pre>
<h2 id="☑️-채점-결과---맞음">☑️ 채점 결과 :  맞음</h2>
<h2 id="☑️-어려웠던-점">☑️ 어려웠던 점</h2>
<ul>
<li>동명이인을 처리하기 위한 로직이 어려웠다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 10868번: 최솟값(Java, 트리, 세그먼트 트리)]]></title>
            <link>https://velog.io/@jina_ham/%EB%B0%B1%EC%A4%80-10868%EB%B2%88-%EC%B5%9C%EC%86%9F%EA%B0%92Java-%ED%8A%B8%EB%A6%AC-%EC%84%B8%EA%B7%B8%EB%A8%BC%ED%8A%B8-%ED%8A%B8%EB%A6%AC</link>
            <guid>https://velog.io/@jina_ham/%EB%B0%B1%EC%A4%80-10868%EB%B2%88-%EC%B5%9C%EC%86%9F%EA%B0%92Java-%ED%8A%B8%EB%A6%AC-%EC%84%B8%EA%B7%B8%EB%A8%BC%ED%8A%B8-%ED%8A%B8%EB%A6%AC</guid>
            <pubDate>Mon, 15 Sep 2025 04:56:29 GMT</pubDate>
            <description><![CDATA[<h2 id="☑️-문제">☑️ 문제</h2>
<p><a href="https://www.acmicpc.net/problem/10868">https://www.acmicpc.net/problem/10868</a></p>
<p><img src="https://velog.velcdn.com/images/jina_ham/post/280e2c04-e358-4394-a583-7b7f99b1bd31/image.png" alt=""></p>
<h2 id="✔️관련-알고리즘-개념">✔️관련 알고리즘 개념</h2>
<p><a href="https://www.notion.so/228f769b856681f98956c7d72adb5c84?pvs=21">세그먼트 트리</a> </p>
<h2 id="☑️-문제-분석">☑️ 문제 분석</h2>
<ul>
<li><p>이 문제는 구간에 해당하는 최솟값을 구하는 문제이다.</p>
</li>
<li><p>M개의 구간에 대해 최솟값을 각각 구해야 하므로 빠르게 연산을 수행할 수 있는 세그먼트 자료구조를 이용하면 된다.</p>
</li>
<li><p>예제입력 1</p>
</li>
</ul>
<pre><code>10 4
75
30
100
38
50
51
52
20
81
5
1 10
3 5
6 9
8 10</code></pre><ul>
<li><p><strong>1단계, 트리 초기화하기 - 리프노드에 원본 데이터 입력</strong></p>
<ul>
<li><p>2*i ≥ 10 (N : 데이터 개수)를 만족하는 최솟값 I는 4이다.</p>
</li>
<li><p>배열 size = 2*i *2 = 32이다.</p>
</li>
<li><p>시작 리프노트 = 2*i = 16이므로 인덱스 16부터 N개의 데이터를 채운다.</p>
<p><img src="https://velog.velcdn.com/images/jina_ham/post/589628c9-0104-4dd5-96ac-224a33662879/image.png" alt=""></p>
</li>
</ul>
</li>
<li><p><strong>2단계, 질의 값 구하기</strong></p>
<ul>
<li><p><strong>세그먼트 트리 index = 주어진 질의 index + 2ⁱ -1를 이용하여 세그먼트 트리용 index로 전환한다.</strong></p>
<p>① start_index % 2 == 1(짝수)일 때 해당 노드를 선택한다.</p>
<p>② end_index % 2 == 0(홀수)일 때 해당 노드를 선택한다. </p>
<p>③ start_index depth 변경 → start_index = (start_index + 1)  / 2 연산을 실행한다.</p>
<p>④ end_index depth 변경 → end_index = (end_index - 1)  / 2 연산을 실행한다. </p>
<p>⑤ ①~④를 반복하다가 end_index &lt; start_index가 되면 종료한다.</p>
</li>
<li><p>예를 들어, 예시입력1에서 3 5는 3번째 데이터에서 5번째 데이터 중 최솟값을 구하는 것이다.</p>
</li>
<li><p>위의 과정을 통해 범위에 속하는 최솟값을 구하면 아래와 같다.</p>
</li>
<li><p>범위 시작 인덱스 = 3 + 2^4 - 1 = 18</p>
</li>
<li><p>범위 끝 인덱스 = 5 + 2^4 - 1 = 20</p>
<aside>
📰

<p>s % 2 == 1 ⇒ 18 % 2 ≠ 1 → 노드 미선택</p>
<p>e % 2 == 0 ⇒ 20 % 2 == 0 → <strong>20번째 노드 선택</strong></p>
<p>s = (s + 1) / 2 ⇒ s = 9</p>
<p>e = (e - 1) / 2 ⇒ e = 9;</p>
<p>s % 2 == 1 ⇒ 9 % 2 ==1 → <strong>9번째 노드 선택</strong></p>
<p>e % 2 == 0 ⇒ 9 % 2 ≠ 0 → 노드 미선택</p>
</aside>

<p>→ 최종적으로 선택된 노드는 20번째 노드와 9번째 노드이다. 여기서 9번째 노드는 18번째와 19번째 노드의 최솟값을 지니고 있으므로 20번째와 9번째 노드를 선택한 것은 18~20번째 노드를 잘 선택한 것과 같다. </p>
</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jina_ham/post/b301cb80-f2f1-43b1-a211-f4c541fcfb98/image.png" alt=""></p>
<h2 id="☑️-코드">☑️ 코드</h2>
<pre><code class="language-jsx">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;

public class Main {
    static int[] tree;
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int N = Integer.parseInt(st.nextToken());
        int M = Integer.parseInt(st.nextToken());

        int temp = N; // N 보존을 위해 복사
        int treeHeight = 0;
        while(temp != 0) {
            temp /= 2;
            treeHeight++;
        }

        int treeSize = (int) Math.pow(2, treeHeight+1);
        tree = new int[treeSize+1];

        int start_leaf_node_index = (int) Math.pow(2, treeHeight);
        for (int i = start_leaf_node_index; i &lt; start_leaf_node_index + N; i++) {
            tree[i] = Integer.parseInt(br.readLine());
        }

        // 트리 초기화
        setTree(treeHeight);

        for (int i = 0; i &lt; M; i++) {
            st = new StringTokenizer(br.readLine());
            int a = Integer.parseInt(st.nextToken());
            int b = Integer.parseInt(st.nextToken());

            a += (int) Math.pow(2, treeHeight) -1;
            b += (int) Math.pow(2, treeHeight) -1;
            System.out.println(findMin(a, b));
        }

    }

    private static int findMin(int s, int e) {
        // a에서 b까지의 범위 중 최솟값 구하기
        int minValue = Integer.MAX_VALUE;

        while(s &lt;= e) {
            if(s % 2 == 1) {
                minValue = Math.min(minValue, tree[s]);
                s++;
            }
            if(e % 2 == 0) {
                minValue = Math.min(minValue, tree[e]);
                e--;
            }
            s /= 2;
            e /= 2;
        }
        return minValue;
    }

    private static void setTree(int treeHeight) {
        int index = (int) Math.pow(2, treeHeight) - 1;
        for (int i = index; i &gt; 0; i--) {
            tree[i] = Math.min(tree[2*i], tree[2*i+1]);
        }
    }
}</code></pre>
<h2 id="☑️-채점-결과---맞음">☑️ 채점 결과 :  맞음</h2>
<h2 id="☑️-어려웠던-점">☑️ 어려웠던 점</h2>
<ul>
<li>없음</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[
백준 5052번: 전화번호 목록(Java, 트리, 트라이)]]></title>
            <link>https://velog.io/@jina_ham/%EB%B0%B1%EC%A4%80-5052%EB%B2%88-%EC%A0%84%ED%99%94%EB%B2%88%ED%98%B8-%EB%AA%A9%EB%A1%9DJava-%ED%8A%B8%EB%A6%AC-%ED%8A%B8%EB%9D%BC%EC%9D%B4</link>
            <guid>https://velog.io/@jina_ham/%EB%B0%B1%EC%A4%80-5052%EB%B2%88-%EC%A0%84%ED%99%94%EB%B2%88%ED%98%B8-%EB%AA%A9%EB%A1%9DJava-%ED%8A%B8%EB%A6%AC-%ED%8A%B8%EB%9D%BC%EC%9D%B4</guid>
            <pubDate>Sat, 13 Sep 2025 11:00:46 GMT</pubDate>
            <description><![CDATA[<h2 id="☑️-문제">☑️ 문제</h2>
<p><a href="https://www.acmicpc.net/problem/5052">https://www.acmicpc.net/problem/5052</a></p>
<p><img src="https://velog.velcdn.com/images/jina_ham/post/77a0e2de-0317-4d66-8ed1-75147a081aaa/image.png" alt=""></p>
<h2 id="✔️관련-알고리즘-개념">✔️관련 알고리즘 개념</h2>
<p><a href="https://www.notion.so/228f769b8566811f9c89fb1f32bbd1bc?pvs=21">트라이</a> </p>
<h2 id="☑️-문제-분석">☑️ 문제 분석</h2>
<ul>
<li>해당 문제는 트라이 자료구조를 사용하면 된다.</li>
<li>우선 일관성이 깨지는 경우는 다음과 같이 두가지이다.</li>
</ul>
<h3 id="✍️-첫-번째-911의-prefix-91이-나중에-나오는-경우">✍️ 첫 번째, 911의 prefix 91이 나중에 나오는 경우</h3>
<pre><code class="language-bash">1 // t
2 // n
911
91</code></pre>
<p><img src="https://velog.velcdn.com/images/jina_ham/post/a34f60dc-98a9-4671-b1ca-ee1537f13732/image.png" alt=""></p>
<ul>
<li><p>트라이 자료구조에 911이 있는 상태에서 91을 삽입하는 경우</p>
<ul>
<li><p>91에서 1이 삽입되고 isEnd를 true라고 값을 표시한 후</p>
</li>
<li><p>현재 노드에 대해 자식노드가 존재하는지 탐색한다. 만약 자식이 존재하면 일관성이 벗어나는 것이다.</p>
</li>
<li><p>관련 코드</p>
<pre><code class="language-bash">   if(k == phoneNumber.length()-1) {
                          now.isEnd = true;
                          // 자식이 있으면 일관성 위배
                          for (tNode child : now.next) {
                              if (child != null) {
                                  consistency = false;
                                  break;
                              }
                          }
                      }</code></pre>
</li>
</ul>
</li>
</ul>
<h3 id="✍️-두-번째-prefix-911이-먼저-나오고-9112546이-나중에-나오는-경우">✍️ 두 번째, prefix 911이 먼저 나오고 9112546이 나중에 나오는 경우</h3>
<pre><code class="language-bash">2 // t
3 // n
911
97625999
91125426</code></pre>
<p><img src="https://velog.velcdn.com/images/jina_ham/post/34addbf0-d4aa-4e14-b7e8-ee059e19a62d/image.png" alt=""></p>
<ul>
<li><p>트라이 자료구조에서 911은 이미 삽입되어 있고 91125426을 삽입하고 있는 경우</p>
<ul>
<li><p>91125426에서 세번째 숫자인 1을 삽입했는데 해당 숫자를 다 삽입하지 않았는데도 isEnd가 true인 상태이다. 이 경우는 이미 다른 숫자 값이 존재하여 해당 숫자를 prefix로 갖는 경우이므로 일관성에 벗어나는 것이다.</p>
</li>
<li><p>관련 코드</p>
<pre><code class="language-bash">    if(now.isEnd) {
                          consistency = false;
                          break;
                      }</code></pre>
</li>
</ul>
</li>
</ul>
<h2 id="☑️-코드">☑️ 코드</h2>
<pre><code class="language-jsx">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int t = Integer.parseInt(br.readLine()); // 테스트 케이스 개수

        for (int i = 0; i &lt; t; i++) {
            boolean consistency = true;
            int n = Integer.parseInt(br.readLine()); // 전화번호 수

            tNode root = new tNode(); // 루트노드는 공백으로
            for (int j = 0; j &lt; n; j++) {
                String phoneNumber = br.readLine();
                tNode now = root;
                for (int k = 0; k &lt; phoneNumber.length(); k++) {
                    char ch = phoneNumber.charAt(k);
                    if(now.next[ch - &#39;0&#39;] == null) {
                        now.next[ch - &#39;0&#39;] = new tNode();
                    }
                    if(now.isEnd) {
                        consistency = false;
                        break;
                    }
                    now = now.next[ch - &#39;0&#39;];

                    if(k == phoneNumber.length()-1) {
                        now.isEnd = true;
                        // 자식이 있으면 일관성 위배
                        for (tNode child : now.next) {
                            if (child != null) {
                                consistency = false;
                                break;
                            }
                        }
                    }
                }
            }
            System.out.println(consistency ? &quot;YES&quot; : &quot;NO&quot;);
        }
    }

    static class tNode {
        tNode[] next = new tNode[10];
        boolean isEnd;
    }
}
</code></pre>
<h2 id="☑️-채점-결과---틀림-→-맞음">☑️ 채점 결과 :  틀림 → 맞음</h2>
<h2 id="☑️-어려웠던-점">☑️ 어려웠던 점</h2>
<ul>
<li>아래의 경우를 생각하지 못함</li>
</ul>
<pre><code class="language-bash">1
2
911
91</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 1012번: 유기농 양배추(Java, 그래프, DFS, BFS)]]></title>
            <link>https://velog.io/@jina_ham/%EB%B0%B1%EC%A4%80-1012%EB%B2%88-%EC%9C%A0%EA%B8%B0%EB%86%8D-%EC%96%91%EB%B0%B0%EC%B6%94Java-%EA%B7%B8%EB%9E%98%ED%94%84-DFS-BFS</link>
            <guid>https://velog.io/@jina_ham/%EB%B0%B1%EC%A4%80-1012%EB%B2%88-%EC%9C%A0%EA%B8%B0%EB%86%8D-%EC%96%91%EB%B0%B0%EC%B6%94Java-%EA%B7%B8%EB%9E%98%ED%94%84-DFS-BFS</guid>
            <pubDate>Thu, 11 Sep 2025 01:49:12 GMT</pubDate>
            <description><![CDATA[<h2 id="☑️-문제">☑️ 문제</h2>
<p><a href="https://www.acmicpc.net/problem/1012">https://www.acmicpc.net/problem/1012</a>
<img src="https://velog.velcdn.com/images/jina_ham/post/10ba5391-3743-4ec6-a957-640bbb8d0a50/image.png" alt=""></p>
<h2 id="✔️관련-알고리즘-개념">✔️관련 알고리즘 개념</h2>
<p><a href="https://www.notion.so/228f769b856681caa2e8e5f644438e56?pvs=21">깊이 우선 탐색</a> </p>
<p><a href="https://www.notion.so/268f769b85668049b606fcebdbeb144e?pvs=21">너비 우선 탐색 </a> </p>
<h2 id="☑️-문제-분석">☑️ 문제 분석</h2>
<ul>
<li>해당 문제는 BFS 혹은 DFS 탐색을 진행하면서 탐색이 진행되는 그룹수를 구하면 된다.</li>
</ul>
<h2 id="☑️-코드">☑️ 코드</h2>
<pre><code class="language-jsx">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;

public class Main {
    static int[][] visited;
    static Queue&lt;Cabbage&gt; queue = new LinkedList&lt;&gt;();
    static int T, M, N, K;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        T = Integer.parseInt(br.readLine()); // 테스트 케이스 개수

        for (int i = 0; i &lt; T; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            M = Integer.parseInt(st.nextToken()); // 가로 길이 (열)
            N = Integer.parseInt(st.nextToken()); // 세로 길이 (행)
            K = Integer.parseInt(st.nextToken()); // 배추 위치 개수

            visited = new int[N][M];

            for (int j = 0; j &lt; K; j++) {
                st = new StringTokenizer(br.readLine());
                int x = Integer.parseInt(st.nextToken()); // 가로
                int y = Integer.parseInt(st.nextToken()); // 세로
                visited[y][x] = 1; // 좌표 저장
            }

            queue.clear();
            int count = 0;
            for (int r = 0; r &lt; N; r++) {
                for (int c = 0; c &lt; M; c++) {
                    if (visited[r][c] == 1) {
                        BFS(r, c);
                        count++;
                    }
                }
            }
            System.out.println(count);
        }
    }

    static void BFS(int r, int c) {
        visited[r][c] = 0;
        queue.add(new Cabbage(r, c));

        while (!queue.isEmpty()) {
            Cabbage cur = queue.poll();

            // 상
            if (cur.r - 1 &gt;= 0 &amp;&amp; visited[cur.r - 1][cur.c] == 1) {
                visited[cur.r - 1][cur.c] = 0;
                queue.add(new Cabbage(cur.r - 1, cur.c));
            }
            // 하
            if (cur.r + 1 &lt; N &amp;&amp; visited[cur.r + 1][cur.c] == 1) {
                visited[cur.r + 1][cur.c] = 0;
                queue.add(new Cabbage(cur.r + 1, cur.c));
            }
            // 좌
            if (cur.c - 1 &gt;= 0 &amp;&amp; visited[cur.r][cur.c - 1] == 1) {
                visited[cur.r][cur.c - 1] = 0;
                queue.add(new Cabbage(cur.r, cur.c - 1));
            }
            // 우
            if (cur.c + 1 &lt; M &amp;&amp; visited[cur.r][cur.c + 1] == 1) {
                visited[cur.r][cur.c + 1] = 0;
                queue.add(new Cabbage(cur.r, cur.c + 1));
            }
        }
    }

    static class Cabbage {
        int r, c;
        Cabbage(int r, int c) {
            this.r = r;
            this.c = c;
        }
    }
}
</code></pre>
<h2 id="☑️-채점-결과---맞음">☑️ 채점 결과 :  맞음</h2>
<h2 id="☑️-어려웠던-점">☑️ 어려웠던 점</h2>
<ul>
<li>여기서 <code>M</code>은 가로(열), <code>N</code>은 세로(행)</li>
<li>Java에서 배열은 <code>arr[행][열]</code> 구조라서 <code>new int[N][M]</code> 로 만들어야 한다</li>
<li>이전 코드에서는 x=가로, y=세로를 그대로 <code>[x][y]</code>에 넣고 있어서 index가 꼬인다.</li>
</ul>
<p>👉 <code>visited = new int[N][M];</code></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 1647번: 도시 분할 계획(Java, 그래프, 최소신장트리)]]></title>
            <link>https://velog.io/@jina_ham/%EB%B0%B1%EC%A4%80-1647%EB%B2%88-%EB%8F%84%EC%8B%9C-%EB%B6%84%ED%95%A0-%EA%B3%84%ED%9A%8DJava-%EA%B7%B8%EB%9E%98%ED%94%84-%EC%B5%9C%EC%86%8C%EC%8B%A0%EC%9E%A5%ED%8A%B8%EB%A6%AC</link>
            <guid>https://velog.io/@jina_ham/%EB%B0%B1%EC%A4%80-1647%EB%B2%88-%EB%8F%84%EC%8B%9C-%EB%B6%84%ED%95%A0-%EA%B3%84%ED%9A%8DJava-%EA%B7%B8%EB%9E%98%ED%94%84-%EC%B5%9C%EC%86%8C%EC%8B%A0%EC%9E%A5%ED%8A%B8%EB%A6%AC</guid>
            <pubDate>Sun, 31 Aug 2025 07:04:54 GMT</pubDate>
            <description><![CDATA[<h2 id="☑️-문제">☑️ 문제</h2>
<p><a href="https://www.acmicpc.net/problem/1647">https://www.acmicpc.net/problem/1647</a>
<img src="https://velog.velcdn.com/images/jina_ham/post/51912706-5667-4e86-b7b3-648691eeb925/image.png" alt=""></p>
<h2 id="✔️관련-알고리즘-개념">✔️관련 알고리즘 개념</h2>
<p><a href="https://www.notion.so/228f769b856681698e89fe758fa4ee22?pvs=21">최소 신장 트리</a> </p>
<h2 id="☑️-문제-분석">☑️ 문제 분석</h2>
<ul>
<li><p>이 문제는 집(노드)를 길(에지)로 연결했을 때 비용(가중치)가 최소가 되도록 해야 하므로 최소 신장 트리의 개념을 이용하여 문제에 접근해야 한다.</p>
</li>
<li><p>문제에서는 모든 집을 연결하는 것이 아니라 집들을 길로 연결하여 두 개의 마을을 만들어야 한다. 만약 모든 집들을 연결하려면 N-1개의 에지를 사용하면 된다. 하지만 우리는 문제 요구 조건에 따라 두 개의 마을로 나눠야 하므로 N-2개의 에지만 사용하면 된다.</p>
</li>
<li><p>예시 입력 1을 분석해보자</p>
<ul>
<li><p>집의 개수는 7, 길의 개수는 12이고</p>
</li>
<li><p>그 아래 12개의 줄에 집의 번호들과 비용이 작성되어 있다.</p>
</li>
<li><p>최소 신장 트리를 사용하기 위해서는 우선순위 큐를 이용하여 비용이 낮은 순으로 정렬하고</p>
</li>
<li><p>사이클 여부를 판단하기 위해 유니온 파인드를 수행해야 한다.</p>
<pre><code class="language-bash">7 12
1 2 3
1 3 2
3 2 1
2 5 2
3 4 4
7 3 6
5 1 5
1 6 2
6 4 1
6 5 3
4 5 3
6 7 4</code></pre>
</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jina_ham/post/6ab38387-7c25-4ca1-81df-ff66c982a003/image.png" alt="">
<img src="https://velog.velcdn.com/images/jina_ham/post/9bf02cc9-47c3-4f6a-9935-9aa535f7c890/image.png" alt="">
<img src="https://velog.velcdn.com/images/jina_ham/post/87ae9e59-1f12-4847-8ab6-aac53d8d330c/image.png" alt=""></p>
<h2 id="☑️-코드">☑️ 코드</h2>
<pre><code class="language-jsx">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;

public class Main {
    static int N, M; // 집의 개수, 길의 개수
    static int[] parent;
    static PriorityQueue&lt;Edge&gt; queue = new PriorityQueue&lt;&gt;();

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        N = Integer.parseInt(st.nextToken());
        M = Integer.parseInt(st.nextToken());

        // 베열 초기화
        parent = new int[N+1];
        for (int i = 1; i &lt;= N; i++) {
            parent[i] = i;
        }

        for (int i = 0; i &lt; M; i++) {
            st = new StringTokenizer(br.readLine());
            int A = Integer.parseInt(st.nextToken());
            int B = Integer.parseInt(st.nextToken());
            int C = Integer.parseInt(st.nextToken());
            queue.add(new Edge(A, B, C));
        }

        int used_Edge = 0;
        int result = 0;
        while (used_Edge &lt; N-2) {
            Edge poll = queue.poll();
            if(find(poll.A) != find(poll.B)) {
                union(poll.A, poll.B);
                used_Edge++;
                result += poll.C;
            }
        }

        System.out.println(result);

    }

    static int find(int a) {
        if(parent[a] == a) return a;
        else return parent[a] = find(parent[a]);
    }

    static void union(int a, int b) {
        a = find(a);
        b = find(b);

        if(a != b) parent[b] = a;
    }

    static class Edge implements Comparable&lt;Edge&gt;{
        int A;
        int B;
        int C;

        Edge(int A, int B, int C) {
            this.A = A;
            this.B = B;
            this.C = C;
        }

        @Override
        public int compareTo(Edge o) {
            return this.C - o.C;
        }
    }

}
</code></pre>
<h2 id="☑️-채점-결과---맞음">☑️ 채점 결과 :  맞음</h2>
<h2 id="☑️-어려웠던-점">☑️ 어려웠던 점</h2>
<ul>
<li>없음</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 14938번: 서강그라운드(Java, 그래프, 플로이드-워셜)]]></title>
            <link>https://velog.io/@jina_ham/%EB%B0%B1%EC%A4%80-14938%EB%B2%88-%EC%84%9C%EA%B0%95%EA%B7%B8%EB%9D%BC%EC%9A%B4%EB%93%9CJava-%EA%B7%B8%EB%9E%98%ED%94%84-%ED%94%8C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%9B%8C%EC%85%9C</link>
            <guid>https://velog.io/@jina_ham/%EB%B0%B1%EC%A4%80-14938%EB%B2%88-%EC%84%9C%EA%B0%95%EA%B7%B8%EB%9D%BC%EC%9A%B4%EB%93%9CJava-%EA%B7%B8%EB%9E%98%ED%94%84-%ED%94%8C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%9B%8C%EC%85%9C</guid>
            <pubDate>Fri, 29 Aug 2025 04:57:16 GMT</pubDate>
            <description><![CDATA[<h2 id="☑️-문제">☑️ 문제</h2>
<p><a href="https://www.acmicpc.net/problem/14938">https://www.acmicpc.net/problem/14938</a>
<img src="https://velog.velcdn.com/images/jina_ham/post/b0caee6c-6668-4266-b6dd-24440d8dccbb/image.png" alt=""></p>
<h2 id="✔️관련-알고리즘-개념">✔️관련 알고리즘 개념</h2>
<p><a href="https://www.notion.so/241f769b856680de9fd0cac663c6066d?pvs=21">플로이드-워셜⭐</a> </p>
<h2 id="☑️-문제-분석">☑️ 문제 분석</h2>
<ul>
<li><p>해당 문제는 모든 노드간의 최단거리를 계산을 해야하는 문제이므로 플로이드-워션 개념을 사용하여 문제를 풀면된다.</p>
</li>
<li><p>예시입력1을 분석해보면</p>
<pre><code class="language-bash">  5 5 4
  5 7 8 2 3
  1 4 5
  5 2 4
  3 2 3
  1 2 3</code></pre>
<ul>
<li>첫 째줄에는 지역개수(n), 수색범위(m), 길의 개수(r)가 주어진다.</li>
<li>둘째줄에는 1번에서 n번 지역까지 아이템 개수가 주어진다. 이는 별도의 items배열을 생성하여 지역별 아이템 개수를 저장해두고 최대 아이템 개수를 계산할 때 이용하면 된다.</li>
</ul>
</li>
</ul>
<pre><code>    | 1 | 2 | 3 | 4 | 5 |
    | --- | --- | --- | --- | --- |
    | 5 | 7 | 8 | 2 | 3 |
- 이후 r개의 줄에는 길 정보가 주어져있으므로 인접행렬에 저장한다.
    - 단, 양방향으로 지역간 이동이 가능하므로 꼭 양방향으로 거리 정보를 저장한다.</code></pre><ul>
<li><p>최종 출력</p>
<ul>
<li><p>이제 최종 출력은 예은이가 1번~n번 지역에 떨어졌을 때 각각 경우에서 얻을 수 있는 아이템 개수를 계산하고 각 경우에서 얻을 수 있는 아이템 개수가 제일 큰 값을 출력하면 된다.</p>
<pre><code class="language-bash">int max_item = Integer.MIN_VALUE; // 최대 아이템 개수
      for (int i = 1; i &lt;= n; i++) {
          int sum = 0;
          for (int j = 1; j &lt;= n; j++) {
              if(distance[i][j] &lt;= m) {
                  sum += items[j];
              }
          }
          if(max_item &lt; sum) max_item = sum;
      }</code></pre>
</li>
</ul>
</li>
</ul>
<h2 id="☑️-코드">☑️ 코드</h2>
<pre><code class="language-jsx">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;

public class Main {
    static int[][] distance; // 거리 배열
    static int[] items; // 지역별 아이템 개수

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int n = Integer.parseInt(st.nextToken()); // 지역 개수
        int m = Integer.parseInt(st.nextToken()); // 수색 범위
        int r = Integer.parseInt(st.nextToken()); // 길의 개수

        // 지역별 아이템 개수 저장
        items = new int[n+1];
        st = new StringTokenizer(br.readLine());
        for (int i = 1; i &lt;= n; i++) {
            items[i] = Integer.parseInt(st.nextToken());
        }

        // 인접 행렬 초기화, i와 j의 값이 같은 곳은 0으로 초기화 나머지는 무한대
        distance = new int[n+1][n+1];
        for (int i = 1; i &lt;= n; i++) {
            for (int j = 1; j &lt;= n; j++) {
                if(i == j) distance[i][j] = 0;
                else distance[i][j] = Integer.MAX_VALUE;
            }
        }

        // 길 정보 입력받기
        for (int i = 0; i &lt; r; i++) {
            st = new StringTokenizer(br.readLine());
            int a = Integer.parseInt(st.nextToken());
            int b = Integer.parseInt(st.nextToken());
            int l = Integer.parseInt(st.nextToken());
            if(distance[a][b] &gt; l) {
                distance[a][b] = l;
                distance[b][a] = l;
            }
        }

        // 플로이드-워셜을 적용하여 노드간 최단거리 계산하기
        for (int k = 1; k &lt;= n; k++) {
            for (int i = 1; i &lt;= n; i++) {
                for (int j = 1; j &lt;= n; j++) {
                    if(distance[i][k] != Integer.MAX_VALUE &amp;&amp; distance[k][j] != Integer.MAX_VALUE &amp;&amp; distance[i][j] &gt; distance[i][k] + distance[k][j])
                        distance[i][j] = distance[i][k] + distance[k][j];
                }
            }
        }

        int max_item = Integer.MIN_VALUE; // 최대 아이템 개수
        for (int i = 1; i &lt;= n; i++) {
            int sum = 0;
            for (int j = 1; j &lt;= n; j++) {
                if(distance[i][j] &lt;= m) {
                    sum += items[j];
                }
            }
            if(max_item &lt; sum) max_item = sum;
        }

        System.out.println(max_item);
    }

}
</code></pre>
<h2 id="☑️-채점-결과---틀림-→-맞음">☑️ 채점 결과 :  틀림 → 맞음</h2>
<h2 id="☑️-어려웠던-점">☑️ 어려웠던 점</h2>
<ul>
<li>양방향 그래프이므로 거리 정보를 양방향으로 저장했어야 했다.</li>
</ul>
<pre><code class="language-bash">// 처음엔 단방향 저장
if(distance[a][b] &gt; l) {
    distance[a][b] = l;
}

--------&gt;
// 양방향 거리 
if(distance[a][b] &gt; l) {
    distance[a][b] = l;
    distance[b][a] = l;
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 7040번: 밥 먹기(Java, 그래프, 벨만-포드)]]></title>
            <link>https://velog.io/@jina_ham/%EB%B0%B1%EC%A4%80-7040%EB%B2%88-%EB%B0%A5-%EB%A8%B9%EA%B8%B0Java-%EA%B7%B8%EB%9E%98%ED%94%84-%EB%B2%A8%EB%A7%8C-%ED%8F%AC%EB%93%9C</link>
            <guid>https://velog.io/@jina_ham/%EB%B0%B1%EC%A4%80-7040%EB%B2%88-%EB%B0%A5-%EB%A8%B9%EA%B8%B0Java-%EA%B7%B8%EB%9E%98%ED%94%84-%EB%B2%A8%EB%A7%8C-%ED%8F%AC%EB%93%9C</guid>
            <pubDate>Thu, 28 Aug 2025 04:18:57 GMT</pubDate>
            <description><![CDATA[<h2 id="☑️-문제">☑️ 문제</h2>
<p><a href="https://www.acmicpc.net/problem/7040">https://www.acmicpc.net/problem/7040</a>
<img src="https://velog.velcdn.com/images/jina_ham/post/b5a5e0e7-afbf-4a83-a626-3c9fdd8d5819/image.png" alt=""></p>
<h2 id="✔️관련-알고리즘-개념">✔️관련 알고리즘 개념</h2>
<p><a href="https://www.notion.so/241f769b85668001bf01c08b02c9c424?pvs=21">벨만-포드</a> </p>
<h2 id="☑️-문제-분석">☑️ 문제 분석</h2>
<ul>
<li>우선 문제 조건에서 1번부터 N번까지의 소들이 번호순대로 줄을 설 때 1번소와 N번 소의 최대 거리를 구해야 한다.</li>
<li>예시 입력 1을 분석해보자</li>
</ul>
<pre><code class="language-bash">4 2 1
1 3 10
2 4 20
2 3 3</code></pre>
<ul>
<li>소는 1번부터 4번까지 4마리가 있고 서로를 좋아하는 소의 쌍은 2쌍, 서로를 싫어하는 소의 쌍은 1쌍이 있다.
1번, 3번 소는 서로를 좋아하므로 최대 10까지만 떨어져 있을 수 있고
2번, 4번 소도 서로를 좋아하므로 최대 20까지만 떨어져 있을 수 있다.
그리고
2번, 3번 소는 서로를 싫어하므로 최소 3만큼 떨어져 있어야 한다.</li>
<li>최종적으로 1번과 4번의 최대 거리를 구해야 하므로 최대한 소들이 서로 떨어져 있게끔 한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jina_ham/post/332b3e37-4080-44a7-803b-efdaf4be6fd4/image.png" alt=""></p>
<ul>
<li>위 그림을 보면 2와 4가 최대 20만큼 떨어져 있다. 이제 1과 2가 최대한 멀리 떨어져 있을 수록 1과 4사이의 거리가 최대일 것이다.
또한 1과 3이 최대 2만큼 떨어져 있다. 이제 3과 4가 최대한 멀리 떨어져 있을 수록 1과 4사이의 거리가 최대일 것이다. 예시 입력 1에서는
2와 3의 최소 거리가 나와 있으므로 전자를 이용하여 문제를 풀면된다.</li>
<li>따라서 1과 2사이 최대 거리는 7, 2와 4사이 최대 거리는 20이므로 1과 4사이의 최대거리는 27이다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jina_ham/post/60c4b82e-63b1-49cd-8939-1505f95e2da3/image.png" alt=""></p>
<p>위의 과정을 벨만-포드를 적용하여 이해해보자</p>
<p><strong>에지리스트 – (c1, c2, d)순</strong></p>
<p><strong>(1, 3, 10)</strong></p>
<p><strong>(2, 4, 20)</strong></p>
<p><strong>(2, 3, 3)</strong></p>
<p><strong>1번째 업데이트</strong></p>
<p>(1)(1, 3, 10)</p>
<ul>
<li>1번소의 distance[1] != ∞ =&gt; <strong>distance[3] = 10으로 업데이트</strong></li>
</ul>
<p>(2) (2, 4, 20)</p>
<ul>
<li>2번 소의 distance[2] = ∞ =&gt; 업데이트 불가</li>
</ul>
<p>(3) (2, 3, 3)은 최소 거리이고 우리는 3번 소의 distance[3] = 10 (!= ∞)</p>
<p>라는 것을 이용해서 <strong>distance[2] = distance[3] – 10 = 7로 업데이트</strong></p>
<p><strong>여기서</strong></p>
<p><strong>MD배열에서 distance[c2] != ∞이면 distance[c1] = distance[c2] – d로 계산할 수 있다.</strong></p>
<p><strong>ML배열은 일반적인 벨만포드와 같이 계산하면 된다.</strong></p>
<p><strong>2번째 업데이트</strong></p>
<p>(1)(1, 3, 10)</p>
<ul>
<li>업데이트 진행할 것 없음</li>
</ul>
<p>(2) (2, 4, 20)</p>
<ul>
<li>distance[2] != ∞ <strong>=&gt; distance[4] = distance[2] +20 = 27로 업데이트</strong></li>
</ul>
<p>(3) (2, 3, 3)</p>
<ul>
<li>업데이트 진행할 것 없음</li>
</ul>
<p>최종적으로 한번 더 에지리스트를 순회하여</p>
<ul>
<li>업데이트 되는 값이 있으면 음수사이클이 존재하는 것과 같으므로 줄을 서는 것이 불가 -&gt; <strong>1을 출력</strong></li>
<li>distance[N] = ∞ -&gt; <strong>2출력</strong></li>
<li>줄서는것이 가능 &amp; distance[N] != ∞ -&gt; d<strong>istance[N] 출력</strong></li>
</ul>
<h2 id="☑️-코드">☑️ 코드</h2>
<pre><code class="language-jsx">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;

public class Main {
    static int[] distance; // 거리 배열
    static List&lt;Edge&gt; edge = new ArrayList&lt;&gt;(); // 에지 리스트
    static int N;
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        N = Integer.parseInt(st.nextToken());
        int ML = Integer.parseInt(st.nextToken());
        int MD = Integer.parseInt(st.nextToken());

        distance = new int[N+1];
        Arrays.fill(distance, Integer.MAX_VALUE);

        // ML 입력받기
        for (int i = 0; i &lt; ML; i++) {
            st = new StringTokenizer(br.readLine());
            int c1 = Integer.parseInt(st.nextToken());
            int c2 = Integer.parseInt(st.nextToken());
            int d = Integer.parseInt(st.nextToken());
            edge.add(new Edge(&quot;ML&quot;, c1, c2, d));
        }

        // MD 입력받기
        for (int i = 0; i &lt; MD; i++) {
            st = new StringTokenizer(br.readLine());
            int c1 = Integer.parseInt(st.nextToken());
            int c2 = Integer.parseInt(st.nextToken());
            int d = Integer.parseInt(st.nextToken());
            edge.add(new Edge(&quot;MD&quot;, c1, c2, d));
        }

         // 시작 노드의 거리 배열 값은 0이다.
        distance[1] = 0;
         // N-1번 벨만-포드를 반복한다.
        for (int i = 0; i &lt; N - 1; i++) {
            for (Edge e : edge) {
                if(e.mode.equals(&quot;ML&quot;)) {
                    if(distance[e.c1] != Integer.MAX_VALUE &amp;&amp; distance[e.c2] &gt; distance[e.c1] + e.d) {
                        distance[e.c2] = distance[e.c1] + e.d;
                    }
                } else if(e.mode.equals(&quot;MD&quot;)) {
                    if(distance[e.c2] != Integer.MAX_VALUE &amp;&amp; distance[e.c1] &gt; distance[e.c2] - e.d) {
                        distance[e.c1] = distance[e.c2] - e.d;
                    }
                }
            }
        }

        boolean imPossible = false;
        for (Edge e : edge) {
            if(e.mode.equals(&quot;ML&quot;)) {
                if(distance[e.c1] != Integer.MAX_VALUE &amp;&amp; distance[e.c2] &gt; distance[e.c1] + e.d) {
                    imPossible = true;
                }
            } else if(e.mode.equals(&quot;MD&quot;)) {
                if(distance[e.c2] != Integer.MAX_VALUE &amp;&amp; distance[e.c1] &gt; distance[e.c2] - e.d) {
                    imPossible = true;
                }
            }
        }

        if(imPossible) System.out.println(-1);
        else if(distance[N] == Integer.MAX_VALUE) System.out.println(-2);
        else System.out.println(distance[N]);
    }

    static class Edge {
         String mode;
         int c1;
         int c2;
         int d;

         Edge(String mode, int c1, int c2, int d) {
             this.mode = mode;
             this.c1 = c1;
             this.c2 = c2;
             this.d = d;
         }
    }

}
</code></pre>
<h2 id="☑️-채점-결과---틀림-→-맞음">☑️ 채점 결과 :  틀림 → 맞음</h2>
<ul>
<li>처음에는 벨만-포드는 최단거리 문제인데 여기는 최대 거리를 구하라고 해서 Integer.MAX_VALUE가 아닌 Integer,MIN_VALUE를 사용해서 틀렸다.</li>
</ul>
<aside>
📰

<p>백준 7040번은 &quot;최대 거리&quot;를 직접 구하는 문제가 <strong>아닙니다</strong>.</p>
<p>실제로는 <strong>제약조건을 만족하는 최소 거리</strong>를 구하는 문제예요.</p>
<h3 id="제약-조건">제약 조건</h3>
<ol>
<li><p><strong>ML 도로 (일반 도로)</strong></p>
<ul>
<li><p><code>c2 - c1 ≤ d</code></p>
<p>  즉, <code>c2</code>는 <code>c1</code>보다 최대 d만큼 멀다.</p>
<p>  👉 최단거리 그래프의 &quot;정방향 간선&quot; 제약처럼 동작.</p>
</li>
</ul>
</li>
<li><p><strong>MD 도로 (검문 도로)</strong></p>
<ul>
<li><p><code>c2 - c1 ≥ d</code></p>
<p>  즉, <code>c1</code>은 <code>c2</code>보다 최소 d만큼 가깝다.</p>
<p>  👉 <code>c1 ≤ c2 - d</code>라는 불평등식.</p>
<p>  이 역시 &quot;최단거리 제약&quot; 형태.</p>
</li>
</ul>
</li>
</ol>
<p>즉, 이 문제는</p>
<p><strong>“모든 ML/MD 조건을 만족하는 가장 작은 거리들”</strong>을 찾아야 함 = 최단경로 문제.</p>
</aside>

<h2 id="☑️-어려웠던-점">☑️ 어려웠던 점</h2>
<ul>
<li>처음에 문제를 이해하는 것이 어려웠다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 1916번: 최소비용 구하기(Java, 그래프, 다익스트라)]]></title>
            <link>https://velog.io/@jina_ham/%EB%B0%B1%EC%A4%80-1916%EB%B2%88-%EC%B5%9C%EC%86%8C%EB%B9%84%EC%9A%A9-%EA%B5%AC%ED%95%98%EA%B8%B0Java-%EA%B7%B8%EB%9E%98%ED%94%84-%EB%8B%A4%EC%9D%B5%EC%8A%A4%ED%8A%B8%EB%9D%BC</link>
            <guid>https://velog.io/@jina_ham/%EB%B0%B1%EC%A4%80-1916%EB%B2%88-%EC%B5%9C%EC%86%8C%EB%B9%84%EC%9A%A9-%EA%B5%AC%ED%95%98%EA%B8%B0Java-%EA%B7%B8%EB%9E%98%ED%94%84-%EB%8B%A4%EC%9D%B5%EC%8A%A4%ED%8A%B8%EB%9D%BC</guid>
            <pubDate>Tue, 26 Aug 2025 03:40:05 GMT</pubDate>
            <description><![CDATA[<h2 id="☑️-문제">☑️ 문제</h2>
<p><a href="https://www.acmicpc.net/problem/1916">https://www.acmicpc.net/problem/1916</a>
<img src="https://velog.velcdn.com/images/jina_ham/post/7609a2a2-0524-4c79-9936-fd47b90f1c42/image.png" alt=""></p>
<h2 id="✔️관련-알고리즘-개념">✔️관련 알고리즘 개념</h2>
<p><a href="https://www.notion.so/228f769b856681bd90f3f0cc091e1687?pvs=21">다익스트라</a> </p>
<h2 id="☑️-문제-분석">☑️ 문제 분석</h2>
<ul>
<li>해당 문제는 최종적으로  출발 도시에서 도착 도시까지 가는데 드는 최소 비용을 출력해야 한다.</li>
<li>최소 비용을 구하기 위해서는 다익스트라를 활용하여 출발 도시에서 모든 노드까지 이르는 최소 거리를 구한 후 마지막 출력에서는 최소 거리 중 도착 도시에 이르는 최소 거리만을 출력하면 된다.
<img src="https://velog.velcdn.com/images/jina_ham/post/1af6d73c-20c9-4d2f-9590-bed98465d204/image.png" alt="">
<img src="https://velog.velcdn.com/images/jina_ham/post/9858cda7-5ac1-4639-9c17-2f4e858920e6/image.png" alt="">
<img src="https://velog.velcdn.com/images/jina_ham/post/f608e5db-1297-4c35-aa3a-55ea310c9ce2/image.png" alt=""></li>
</ul>
<h2 id="☑️-코드">☑️ 코드</h2>
<pre><code class="language-jsx">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;

public class Main {
    public static int[] distance; // 최단 거리 저장 배열
    public static boolean[] visited; // 노드 방문 여부 표시
    public static ArrayList&lt;Edge&gt;[] list; // 인접리스트
    public static PriorityQueue&lt;Edge&gt; q = new PriorityQueue&lt;&gt;();

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int N = Integer.parseInt(br.readLine()); // 도시 개수
        int M = Integer.parseInt(br.readLine()); // 버스 개수

        distance = new int[N+1];
        visited = new boolean[N+1];
        list = new ArrayList[N+1];

        // 인접리스트 초기화
        for (int i = 1; i &lt;= N; i++) {
            list[i] = new ArrayList&lt;&gt;();
        }

        Arrays.fill(distance, Integer.MAX_VALUE);

        StringTokenizer st;
        // 버스 노선 입력받기
        for (int i = 0; i &lt; M; i++) {
            st = new StringTokenizer(br.readLine());
            int a = Integer.parseInt(st.nextToken()); // 출발 도시
            int b = Integer.parseInt(st.nextToken()); // 도착 도시
            int cost = Integer.parseInt(st.nextToken());
            list[a].add(new Edge(b, cost));
        }

        st = new StringTokenizer(br.readLine());
        int start = Integer.parseInt(st.nextToken());
        int end = Integer.parseInt(st.nextToken());

        distance[start] = 0;
        q.add(new Edge(start, 0));

        while (!q.isEmpty()) {
            Edge current = q.poll();
            int c_v = current.vertex;

            if(visited[c_v]) continue; // 이미 방문한 노드라면 넘어감
            visited[c_v] = true;

            for (Edge edge : list[c_v]) {
                int vertex = edge.vertex;
                int weight = edge.weight;

                if(distance[vertex] &gt; distance[c_v] + weight) {
                    distance[vertex] = distance[c_v] + weight;
                    q.add(new Edge(vertex, distance[vertex]));
                }
            }
        }

        System.out.println(distance[end]);
    }

    static class Edge implements Comparable&lt;Edge&gt; {
        int vertex;
        int weight;

        public Edge(int vertex, int weight) {
            this.vertex = vertex;
            this.weight = weight;
        }
        @Override
        public int compareTo(Edge o) {
            return this.weight - o.weight;
        }
    }

}
</code></pre>
<h2 id="☑️-채점-결과--맞음">☑️ 채점 결과 : 맞음</h2>
<h2 id="☑️-어려웠던-점">☑️ 어려웠던 점</h2>
<ul>
<li>없음</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 3665번: 최종순위(Java, 그래프, 위상정렬)]]></title>
            <link>https://velog.io/@jina_ham/%EB%B0%B1%EC%A4%80-3665%EB%B2%88-%EC%B5%9C%EC%A2%85%EC%88%9C%EC%9C%84Java-%EA%B7%B8%EB%9E%98%ED%94%84-%EC%9C%84%EC%83%81%EC%A0%95%EB%A0%AC</link>
            <guid>https://velog.io/@jina_ham/%EB%B0%B1%EC%A4%80-3665%EB%B2%88-%EC%B5%9C%EC%A2%85%EC%88%9C%EC%9C%84Java-%EA%B7%B8%EB%9E%98%ED%94%84-%EC%9C%84%EC%83%81%EC%A0%95%EB%A0%AC</guid>
            <pubDate>Mon, 25 Aug 2025 05:23:54 GMT</pubDate>
            <description><![CDATA[<h2 id="☑️-문제">☑️ 문제</h2>
<p><a href="https://www.acmicpc.net/problem/3665">https://www.acmicpc.net/problem/3665</a></p>
<p><img src="https://velog.velcdn.com/images/jina_ham/post/75be55c4-c142-4d8d-bc98-eebac37ebffd/image.png" alt=""></p>
<h2 id="✔️관련-알고리즘-개념">✔️관련 알고리즘 개념</h2>
<p><a href="https://www.notion.so/228f769b8566811f84f3c2bc0699f134?pvs=21">위상 정렬</a> </p>
<h2 id="☑️-문제-분석">☑️ 문제 분석</h2>
<ul>
<li>그림을 이용하여 문제를 입력하면 다음과 같다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jina_ham/post/77d65483-48bb-4a77-8cc6-f2bbab923874/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jina_ham/post/e170a860-f536-41cb-9a83-543b1093298e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jina_ham/post/9ca6c145-cbe3-40ad-8eee-20c1b3a77da0/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jina_ham/post/d7a2df25-04f3-4574-b456-125231386297/image.png" alt=""></p>
<ul>
<li>만약 위상 정렬을 적용하여 노드간의 순서가 잘 출력 될 것 같으면 노드간의 순서를 출력한다.<ul>
<li>하지만, 진입차수가 0인 차수가 없는 경우에는 정렬이 불가하므로 ‘IMPOSSIBLE’을 출력하고</li>
<li>반면, 진입차수가 0인 차수가 존재하지만 여러개 있는 경우 정확한 순위를 매길 수 없으므로 ‘?’를 출력한다.</li>
</ul>
</li>
</ul>
<h2 id="☑️-코드">☑️ 코드</h2>
<pre><code class="language-jsx">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;

public class Main {
    static int[] lastRank;
    static ArrayList&lt;ArrayList&lt;Integer&gt;&gt; arrayList; 
    static int[] D;                                 // 진입차수
    static int n;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringBuilder out = new StringBuilder();
        int t = Integer.parseInt(br.readLine()); // 테스트 케이스 개수

        for (int tc = 0; tc &lt; t; tc++) {
            n = Integer.parseInt(br.readLine()); // 팀의 개수

            // 작년 순위
            lastRank = new int[n + 1];
            StringTokenizer st = new StringTokenizer(br.readLine());
            for (int j = 1; j &lt;= n; j++) {
                lastRank[j] = Integer.parseInt(st.nextToken()); j 인덱스에 저장
            }


            arrayList = new ArrayList&lt;&gt;(n + 1);
            for (int j = 0; j &lt;= n; j++) arrayList.add(new ArrayList&lt;&gt;());
            D = new int[n + 1];

            // 작년 순위를 인접리스트에 저장 
            for (int j = 1; j &lt;= n; j++) {
                for (int k = j + 1; k &lt;= n; k++) {
                    int u = lastRank[j]; 
                    int v = lastRank[k]; 
                    arrayList.get(u).add(v);
                    D[v]++;
                }
            }

            // 상대적인 등수가 바뀐 쌍의 수
            int m = Integer.parseInt(br.readLine());
            for (int j = 0; j &lt; m; j++) {
                st = new StringTokenizer(br.readLine());
                int a = Integer.parseInt(st.nextToken());
                int b = Integer.parseInt(st.nextToken());
                changeRank(a, b); 
            }

            // 최종 위상 정렬 적용하여 올해 순위 출력하기
            out.append(topologicalSort()).append(&#39;\n&#39;);
        }

        System.out.print(out.toString());
    }

    private static String topologicalSort() {
        Queue&lt;Integer&gt; queue = new ArrayDeque&lt;&gt;();
        for (int i = 1; i &lt;= n; i++) if (D[i] == 0) queue.offer(i);

        List&lt;Integer&gt; order = new ArrayList&lt;&gt;(n);
        boolean ambiguous = false;

        for (int step = 0; step &lt; n; step++) {
            if (queue.isEmpty()) return &quot;IMPOSSIBLE&quot;;  

            if (queue.size() &gt; 1) ambiguous = true;     // 같은 시점의 후보가 2개 이상 → 모호

            int now = queue.poll();
            order.add(now);

            for (int next : arrayList.get(now)) {
                D[next]--;
                if (D[next] == 0) queue.offer(next);
            }
        }

        if (ambiguous) return &quot;?&quot;;

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i &lt; n; i++) {
            if (i &gt; 0) sb.append(&#39; &#39;);
            sb.append(order.get(i));
        }
        return sb.toString();
    }

    // a와 b팀의 상대적 순위 바꾸기 
    private static void changeRank(int a, int b) {
        // a -&gt; b 가 있으면 b -&gt; a 로 뒤집기
        if (arrayList.get(a).contains(b)) {
            arrayList.get(a).remove((Integer) b);
            D[b]--;
            arrayList.get(b).add(a);
            D[a]++;
        }
        // 없으면 b -&gt; a가 있다고 보고 a -&gt; b로 뒤집기
        else if (arrayList.get(b).contains(a)) {
            arrayList.get(b).remove((Integer) a);
            D[a]--;
            arrayList.get(a).add(b);
            D[b]++;
        }
    }
}
</code></pre>
<h2 id="☑️-채점-결과---맞음">☑️ 채점 결과 :  맞음</h2>
<h2 id="☑️-어려웠던-점">☑️ 어려웠던 점</h2>
<ul>
<li>다양한 예외 상황을 고려하여 코드를 작성하는 것이 어려웠다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 16562번: 친구비(Java, 그래프, 유니온 파인드)]]></title>
            <link>https://velog.io/@jina_ham/%EB%B0%B1%EC%A4%80-16562%EB%B2%88-%EC%B9%9C%EA%B5%AC%EB%B9%84Java-%EA%B7%B8%EB%9E%98%ED%94%84-%EC%9C%A0%EB%8B%88%EC%98%A8-%ED%8C%8C%EC%9D%B8%EB%93%9C</link>
            <guid>https://velog.io/@jina_ham/%EB%B0%B1%EC%A4%80-16562%EB%B2%88-%EC%B9%9C%EA%B5%AC%EB%B9%84Java-%EA%B7%B8%EB%9E%98%ED%94%84-%EC%9C%A0%EB%8B%88%EC%98%A8-%ED%8C%8C%EC%9D%B8%EB%93%9C</guid>
            <pubDate>Mon, 18 Aug 2025 05:12:47 GMT</pubDate>
            <description><![CDATA[<h2 id="☑️-문제">☑️ 문제</h2>
<p><a href="https://www.acmicpc.net/problem/16562">https://www.acmicpc.net/problem/16562</a></p>
<p><img src="https://velog.velcdn.com/images/jina_ham/post/26c441d6-c746-42e3-831c-2123d145cdaf/image.png" alt=""></p>
<h2 id="✔️관련-알고리즘-개념">✔️관련 알고리즘 개념</h2>
<p><a href="https://www.notion.so/228f769b856681dfaf2fea52083dbfdb?pvs=21">유니온 파인드</a> </p>
<h2 id="☑️-문제-분석">☑️ 문제 분석</h2>
<ul>
<li>우선 예시입력 1, 2를 직접 확인해보면서 풀이를 파악한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jina_ham/post/08febaeb-91ea-4e5a-9f8f-ad4121c1eb8d/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jina_ham/post/db90491e-c5e4-4a12-93e3-11096ce8077a/image.png" alt=""></p>
<ul>
<li>그런데 위의 과정을 구현하고 나면 우리는 parent배열을 통해 집합 관계를 확인할 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jina_ham/post/3621f36d-143e-4044-ae39-e44f24f8cf6d/image.png" alt=""></p>
<ul>
<li>위 정보를 통해 최소 비용을 계산해야 한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jina_ham/post/e793239f-a11d-4809-b4a6-b6b9e4b662fa/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jina_ham/post/8142e852-5a86-4561-b8cd-577e3f8f17a4/image.png" alt=""></p>
<ul>
<li>즉, 배열을 돌면서 각 친구들의 친구비와 대표 노드의 최소 비용을 비교하면서 최소 비용을 업데이트해가면 된다.</li>
</ul>
<h2 id="☑️-코드">☑️ 코드</h2>
<pre><code class="language-jsx">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.StringTokenizer;

public class Main {
    static int[] parent;
    static int[] friendCost;
    static int[] minCost;
    static  int k;
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int N = Integer.parseInt(st.nextToken()); // 학생 수
        int M = Integer.parseInt(st.nextToken()); // 친구 관계수
        k = Integer.parseInt(st.nextToken()); // 가지고 있는 돈

        // 친구 비 저장 배열
        friendCost = new int[N+1];
        st = new StringTokenizer(br.readLine());
        for (int i = 1; i &lt;= N; i++) {
            friendCost[i] = Integer.parseInt(st.nextToken());
        }

        // 대표 노드 저장 배열
        parent = new int[N+1];
        minCost = new int[N+1];

        // 대표 노드 저장 배열 초기화
        for (int i = 1; i &lt;= N; i++) {
            parent[i] = i;
        }

        // 친구 관계수를 입력받고 해당 친구 번호들에 대해 union 연산을 한다.
        for (int i = 0; i &lt; M; i++) {
            st = new StringTokenizer(br.readLine());
            int f1 = Integer.parseInt(st.nextToken());
            int f2 = Integer.parseInt(st.nextToken());

            if(f2 &lt; f1) union(f2, f1);
            else union(f1, f2);
        }

        // 루트별 최소비 계산
        final int INF = 1_000_000_000;
        int[] minCost = new int[N + 1];
        Arrays.fill(minCost, INF);

        for (int i = 1; i &lt;= N; i++) {
            int r = find(i); // 반드시 find로 루트 보정
            minCost[r] = Math.min(minCost[r], friendCost[i]);
        }

        int sum = 0;
        for (int i = 1; i &lt;= N; i++) {
            if (i == find(i) &amp;&amp; minCost[i] != INF) sum += minCost[i]; // 대표만 합산
        }

        if (sum &lt;= k) System.out.println(sum);
        else System.out.println(&quot;Oh no&quot;);

    }

    public static void union(int a, int b) {
        a = find(a);
        b = find(b);

        if(a != b) {
            parent[b] = a;
        }
    }

    public static int find(int a) {
        // 대표 노드 반환 함수
        if(parent[a] == a) return a;
        else return parent[a] = find(parent[a]);
    }
}
</code></pre>
<h2 id="☑️-채점-결과---틀림-→-맞음">☑️ 채점 결과 :  틀림 → 맞음</h2>
<ul>
<li>집합 관계를 토대로 최소 비용을 계산하는 부분이 틀렸었다.</li>
</ul>
<h2 id="☑️-어려웠던-점">☑️ 어려웠던 점</h2>
<ul>
<li>최소 비용 계산이 어려웠다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 2166번: 다각형의 면적(Java, 기하)]]></title>
            <link>https://velog.io/@jina_ham/%EB%B0%B1%EC%A4%80-2166%EB%B2%88-%EB%8B%A4%EA%B0%81%ED%98%95%EC%9D%98-%EB%A9%B4%EC%A0%81Java-%EA%B8%B0%ED%95%98</link>
            <guid>https://velog.io/@jina_ham/%EB%B0%B1%EC%A4%80-2166%EB%B2%88-%EB%8B%A4%EA%B0%81%ED%98%95%EC%9D%98-%EB%A9%B4%EC%A0%81Java-%EA%B8%B0%ED%95%98</guid>
            <pubDate>Thu, 31 Jul 2025 06:50:53 GMT</pubDate>
            <description><![CDATA[<h2 id="☑️-문제">☑️ 문제</h2>
<p><a href="https://www.acmicpc.net/problem/2166">https://www.acmicpc.net/problem/2166</a></p>
<p><img src="https://velog.velcdn.com/images/jina_ham/post/4879ba48-86ec-4961-8515-3a08c06d3706/image.png" alt=""></p>
<h2 id="✔️관련-알고리즘-개념">✔️관련 알고리즘 개념</h2>
<h3 id="shoelace-공식shoelace-formula"><strong>Shoelace 공식(Shoelace formula)</strong></h3>
<ul>
<li><strong>2차원 평면에서 다각형의 면적을 구하는 공식</strong>으로, 점들이 <strong>순서대로(시계 혹은 반시계 방향)</strong> 주어졌을 때 사용할 수 있다.</li>
</ul>
<hr>
<h2 id="🥾-shoelace-공식-신발끈-공식">🥾 Shoelace 공식 (신발끈 공식)</h2>
<p>다각형의 꼭짓점이 다음과 같이 순서대로 주어졌다.</p>
<p>$(x1,y1), (x2,y2), (x3,y3), …, (xn,yn)$</p>
<p>이때, 면적 A는 다음과 같은 <strong>신발끈 공식</strong>으로 계산된다.</p>
<p><img src="https://velog.velcdn.com/images/jina_ham/post/386b3d5c-380c-4876-9c06-b7112d9ec9e6/image.png" alt=""></p>
<p>단, 여기에 <strong>끝 점의 다음 점</strong>으로 <strong>시작점을 다시 사용해야 하므로</strong>:</p>
<ul>
<li>$xn+1=x1$</li>
<li>$yn+1=y1$이 된다.</li>
</ul>
<h2 id="☑️-문제-분석">☑️ 문제 분석</h2>
<ul>
<li><p>해당 문제는 N각형을 나타내는 N개의 좌표가 주어졌을 때 해당 다각형의 면적을 구하는 문제이다.</p>
</li>
<li><p>처음에는 N각형을 쪼개서 삼각형을 만든 다음 각 삼각형의 넓이를 |CCW|/2를 이용하여 구한 다음 다 더해서 N각형의 넓이를 구하려고 했는데 이는 좌표가 시계방향 혹은 반시계 순서대로 주어져야 사용할 수 있다.</p>
</li>
<li><p>N각형의 넓이를 삼각형으로 쪼개지 않고 바로 구할 수 있는 방법은 신발끈 공식을 이용하여 문제를 푼다.</p>
<p>  <img src="https://velog.velcdn.com/images/jina_ham/post/79f75a7a-63c2-4993-99f7-141b3d38ea14/image.png" alt=""></p>
</li>
</ul>
<ul>
<li>단 여기서 주의해야 할 것은 i값이 n일 때 i+1값은 존재하지 않으므로 x1, y1값으로 계산해야 한다는 점이다.</li>
</ul>
<h2 id="☑️-코드">☑️ 코드</h2>
<pre><code class="language-jsx">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int N = Integer.parseInt(br.readLine());

        Coordinates[] p = new Coordinates[N];
        for (int i = 0; i &lt; N; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            int a = Integer.parseInt(st.nextToken());
            int b = Integer.parseInt(st.nextToken());
            p[i] = new Coordinates(a, b);
        }

        double result = 0;
        for (int i = 0; i &lt; N; i++) {
            int j = (i + 1) % N; // 다음 점, 마지막 → 첫 점으로 순환
            result += (long)p[i].x * p[j].y - (long)p[j].x * p[i].y;
        }

        System.out.printf(&quot;%.1f\n&quot;, Math.abs(result) / 2.0);
    }

    public static class Coordinates {
        int x, y;
        Coordinates(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }
}
</code></pre>
<h2 id="☑️-채점-결과---틀림-→-맞음">☑️ 채점 결과 :  틀림 → 맞음</h2>
<h2 id="☑️-어려웠던-점">☑️ 어려웠던 점</h2>
<ul>
<li>신발끈 공식을 떠올리지 못했다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 9095번: 1, 2, 3 더하기 (Java, 동적 계획법)]]></title>
            <link>https://velog.io/@jina_ham/%EB%B0%B1%EC%A4%80-9095%EB%B2%88-1-2-3-%EB%8D%94%ED%95%98%EA%B8%B0-Java-%EB%8F%99%EC%A0%81-%EA%B3%84%ED%9A%8D%EB%B2%95</link>
            <guid>https://velog.io/@jina_ham/%EB%B0%B1%EC%A4%80-9095%EB%B2%88-1-2-3-%EB%8D%94%ED%95%98%EA%B8%B0-Java-%EB%8F%99%EC%A0%81-%EA%B3%84%ED%9A%8D%EB%B2%95</guid>
            <pubDate>Thu, 31 Jul 2025 04:17:16 GMT</pubDate>
            <description><![CDATA[<h2 id="☑️-문제">☑️ 문제</h2>
<p><a href="https://www.acmicpc.net/problem/9095">https://www.acmicpc.net/problem/9095</a></p>
<p><img src="https://velog.velcdn.com/images/jina_ham/post/2609f8a3-2dee-4664-b1c0-66d38b26e5e2/image.png" alt=""></p>
<h2 id="✔️관련-알고리즘-개념">✔️관련 알고리즘 개념</h2>
<p><a href="https://www.notion.so/228f769b8566813dbd46c605b068a3ce?pvs=21">동적 계획법</a> </p>
<h2 id="☑️-문제-분석">☑️ 문제 분석</h2>
<ul>
<li>여기서 n의 값은 11보다 작은 양수이다.</li>
<li>n에 대한 경우의 수는 앞의 세 개의 n값들의 합이다.<ul>
<li>왜 앞의 3개의 합이냐면 합을 1, 2, 3의 합으로만 나타낼 수 있기 때문이다.</li>
</ul>
</li>
<li>아래 그림을 보면<ul>
<li>4를 1, 2, 3의 합으로 나타내는 경우의 수는 총 7가지이다.<ul>
<li>1을 나타내는 모든 식에 각각 3을 더해주면 4를 나타내는 식이 된다.</li>
<li>2를 나타내는 모든 식에 각각 2를 더해주면 4를 나타내는 식이 된다.</li>
<li>3을 나타내는 모든 식에 각각 1을 더해주면 4를 나타내는 식이 된다.</li>
</ul>
</li>
<li>따라서 여기서 점화식은 p[i] = p[i-1] + p[i-2] + p[i-3] (i &gt; 3) 이 나온다.</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/jina_ham/post/b3e0b97b-dac8-40da-92c0-8a0d1cf41a58/image.png" alt=""></p>
<h2 id="☑️-코드">☑️ 코드</h2>
<pre><code class="language-jsx">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {
    static int[] dp = new int[11]; // n은 11보다 작다고 했으니 0~10까지

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        // DP 초기화
        dp[1] = 1;
        dp[2] = 2;
        dp[3] = 4;
        for (int i = 4; i &lt;= 10; i++) {
            dp[i] = dp[i - 1] + dp[i - 2] + dp[i - 3];
        }

        int T = Integer.parseInt(br.readLine());
        for (int i = 0; i &lt; T; i++) {
            int n = Integer.parseInt(br.readLine());
            System.out.println(dp[n]);
        }
    }
}</code></pre>
<h2 id="☑️-채점-결과---맞음">☑️ 채점 결과 :  맞음</h2>
<h2 id="☑️-어려웠던-점">☑️ 어려웠던 점</h2>
<ul>
<li>없음</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>