<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://indexkim.github.io//feed.xml" rel="self" type="application/atom+xml" /><link href="https://indexkim.github.io//" rel="alternate" type="text/html" /><updated>2026-02-26T23:31:08+09:00</updated><id>https://indexkim.github.io//feed.xml</id><title type="html">indexkim</title><subtitle>An amazing website.</subtitle><author><name>indexkim</name></author><entry><title type="html">대규모 VLM 영상 추출 시스템의 프롬프트 최적화·구조적 한계</title><link href="https://indexkim.github.io//vlm-custom-prompt-optimization/" rel="alternate" type="text/html" title="대규모 VLM 영상 추출 시스템의 프롬프트 최적화·구조적 한계" /><published>2026-02-26T00:00:00+09:00</published><updated>2026-02-26T00:00:00+09:00</updated><id>https://indexkim.github.io//vlm-custom-prompt-optimization</id><content type="html" xml:base="https://indexkim.github.io//vlm-custom-prompt-optimization/"><![CDATA[<p>VLM(Vision Language Model)은 시각 정보와 언어 정보를 함께 처리하는 멀티모달 모델입니다.<br />
이 글에서는 VLM에 영상 프레임을 입력해 장면을 자연어로 서술하는 방식으로 사용했습니다.<br />
단순히 객체를 인식하는 것을 넘어, 누가 무엇을 하고 있는지를 문장으로 표현할 수 있어<br />
영상 검색, 장면 분류, 자동 자막 생성 등에 활용됩니다.</p>

<p>초기에는 Custom Prompt만 잘 작성하면 원하는 장면을 추출할 수 있을 것이라 생각했으나,<br />
실제로 진행해 보니 VLM 서술 방식, 고정 프롬프트, 후속 파이프라인의 동작 방식까지<br />
예상보다 깊이 있게 고려해야 할 영역이 많았습니다.</p>

<p>이 글에서는 대규모 영상 AI 프로젝트에서 VLM Custom Prompt 최적화 과정을 정리합니다.<br />
프롬프트 전략 비교, 테스트 자동화, 정량 평가, 인터페이스 설계 변경까지의 과정을 다룹니다.</p>

<blockquote>
  <p><em>본 포스팅의 프롬프트, 데이터, 수치는<br />
실제 프로젝트 내용을 기반으로 일부 각색되었습니다.</em></p>
</blockquote>

<hr />

<h2 id="1-영상-추출에서의-vlm-활용">1. 영상 추출에서의 VLM 활용</h2>

<p>VLM의 아키텍처와 기본 개념은 <a href="/deploying-air-gapped-ai-solution-services/#vlmvision-language-model이란">폐쇄망 AI Solution 서비스 구성 및 배포</a>에서 다뤘습니다.<br />
이 글에서는 VLM을 영상 장면 추출에 적용할 때의 특성에 집중합니다.</p>

<p>이번 프로젝트에서 VLM은 프레임 묶음(10~20장)을 입력받아<br />
해당 구간에서 무엇이 일어나고 있는지를 자연어로 서술합니다.</p>

<table>
  <thead>
    <tr>
      <th>관점</th>
      <th>영상 서술 기반 추출</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>입력</td>
      <td>프레임 시퀀스(10~20장)</td>
    </tr>
    <tr>
      <td>태스크</td>
      <td>장면을 요약해 텍스트로 표현</td>
    </tr>
    <tr>
      <td>출력</td>
      <td>검색/필터링용 설명 문장</td>
    </tr>
    <tr>
      <td>프롬프트 역할</td>
      <td>무엇을 중점적으로 볼지 지정</td>
    </tr>
    <tr>
      <td>평가</td>
      <td>정답 구간과 비교(IoU, F1 등)</td>
    </tr>
  </tbody>
</table>

<p>영상 서술에서는 프롬프트가 관찰 초점을 결정하므로, 프롬프트 설계가 추출 품질을 좌우합니다.</p>

<hr />

<h2 id="2-프롬프트-엔지니어링-prompt-engineering">2. 프롬프트 엔지니어링 (Prompt Engineering)</h2>

<p>프롬프트 엔지니어링은 AI 모델에 입력하는 지시문(프롬프트)을 설계·최적화하여<br />
원하는 출력을 유도하는 기법입니다.</p>

<h3 id="1-llm-프롬프트-엔지니어링과의-차이">1. LLM 프롬프트 엔지니어링과의 차이</h3>

<p>텍스트 전용 LLM에서의 프롬프트 엔지니어링은 주로 다음에 집중합니다:</p>

<ul>
  <li>역할 지정 (“너는 번역가다”)</li>
  <li>출력 형식 지정 (“JSON으로 응답해”)</li>
  <li>Few-shot 예시 제공</li>
  <li>Chain-of-Thought 유도</li>
</ul>

<p>VLM에서의 프롬프트 엔지니어링은<br />
여기에 시각 정보의 해석 방향이 추가됩니다:</p>

<table>
  <thead>
    <tr>
      <th>관점</th>
      <th>LLM 프롬프트</th>
      <th>VLM 프롬프트</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>입력</td>
      <td>텍스트만</td>
      <td>텍스트 + 이미지/프레임</td>
    </tr>
    <tr>
      <td>프롬프트 역할</td>
      <td>“무엇을 생성할 것인가”</td>
      <td>“이미지에서 무엇을 볼 것인가”</td>
    </tr>
    <tr>
      <td>모호성 원인</td>
      <td>언어적 다의성</td>
      <td>시각적 주관성 (구도, 분위기, 맥락)</td>
    </tr>
    <tr>
      <td>오류 유형</td>
      <td>사실 오류, 형식 불일치</td>
      <td>할루시네이션, 과잉 서술</td>
    </tr>
    <tr>
      <td>제어 난이도</td>
      <td>비교적 예측 가능</td>
      <td>동일 프롬프트도 프레임에 따라 결과 변동</td>
    </tr>
  </tbody>
</table>

<h3 id="2-할루시네이션-hallucination">2. 할루시네이션 (Hallucination)</h3>

<p>할루시네이션은 AI 모델이 입력에 존재하지 않는 내용을 사실인 것처럼 생성하는 현상입니다.<br />
LLM에서는 학습 데이터에 없는 사실을 지어내는 형태로 나타나고,<br />
VLM에서는 이미지에 없는 객체나 행동을 서술하는 형태로 나타납니다.</p>

<table>
  <thead>
    <tr>
      <th>구분</th>
      <th>LLM 할루시네이션</th>
      <th>VLM 할루시네이션</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>형태</td>
      <td>존재하지 않는 사실을 생성</td>
      <td>이미지에 없는 객체/행동을 서술</td>
    </tr>
    <tr>
      <td>예시</td>
      <td>“이 문서에는 보안 정책이 명시되어 있습니다.” (실제로는 없음)</td>
      <td>화면에 사람이 없는데 “사람이 걸어가고 있다”</td>
    </tr>
    <tr>
      <td>원인</td>
      <td>학습 데이터의 패턴에 과적합</td>
      <td>프롬프트의 유도 + 시각 정보 부족</td>
    </tr>
  </tbody>
</table>

<p>VLM 프롬프트는 문장을 생성하라는 지시가 아니라 영상에서 무엇을 볼지 지정하는 지시이므로,<br />
프롬프트의 표현 하나가 다수 구간의 서술 결과에 영향을 줄 수 있습니다.</p>

<h3 id="3-system-prompt-vs-user-prompt">3. System Prompt vs User Prompt</h3>

<p>LLM/VLM에 입력되는 프롬프트는 일반적으로 두 영역으로 나뉩니다.</p>

<table>
  <thead>
    <tr>
      <th>구분</th>
      <th>System Prompt</th>
      <th>User Prompt</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>역할</td>
      <td>모델의 동작 방식, 역할, 제약 조건을 정의</td>
      <td>실제 요청이나 질문을 전달</td>
    </tr>
    <tr>
      <td>설정 주체</td>
      <td>개발자/시스템</td>
      <td>최종 사용자</td>
    </tr>
    <tr>
      <td>변경 빈도</td>
      <td>배포 시 고정, 사용자에게 비공개</td>
      <td>매 요청마다 변경 가능</td>
    </tr>
    <tr>
      <td>예시</td>
      <td>“너는 영상 분석 전문가다. 한국어로 1~2문장으로 서술해라.”</td>
      <td>“건물이 나오는 장면을 찾아줘”</td>
    </tr>
  </tbody>
</table>

<p>System Prompt는 모델이 어떻게 동작할지 결정하고, User Prompt는 무엇을 할지 지시합니다.</p>

<p>ChatGPT 같은 서비스에서는 System Prompt가 사용자에게 보이지 않지만,<br />
API를 직접 호출할 때는 <code class="language-plaintext highlighter-rouge">messages</code>의 <code class="language-plaintext highlighter-rouge">role: "system"</code>과 <code class="language-plaintext highlighter-rouge">role: "user"</code>로 구분하여 입력합니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">messages</span> <span class="o">=</span> <span class="p">[</span>
    <span class="p">{</span><span class="s">"role"</span><span class="p">:</span> <span class="s">"system"</span><span class="p">,</span> <span class="s">"content"</span><span class="p">:</span> <span class="s">"너는 영상 분석 전문가다..."</span><span class="p">},</span>
    <span class="p">{</span><span class="s">"role"</span><span class="p">:</span> <span class="s">"user"</span><span class="p">,</span> <span class="s">"content"</span><span class="p">:</span> <span class="s">"이 영상에서 건물이 보이는 장면을 찾아줘"</span><span class="p">},</span>
<span class="p">]</span>
</code></pre></div></div>

<h3 id="4-custom-prompt의-위치">4. Custom Prompt의 위치</h3>

<p>이번 프로젝트에서 Custom Prompt는 전체 프롬프트의 일부로,<br />
시스템이 고정한 기본 프롬프트 위에 사용자가 추가하는 형태입니다.<br />
앞서 설명한 User Prompt와는 다르며, 사용자가 UI에서 입력하는 가변 영역을 가리킵니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌──────────────────────────────┐
│  시스템 고정 프롬프트 (수정 불가)   │
│  ┌────────────────────────┐  │
│  │ 기본 프롬프트            │  │
│  │ 작업 순서 프롬프트         │  │
│  │ 출력 형식 프롬프트    │  │
│  └────────────────────────┘  │
│                              │
│  ┌────────────────────────┐  │
│  │ Custom Prompt (수정 가능) │  │  ← 유일한 가변 영역
│  └────────────────────────┘  │
└──────────────────────────────┘
</code></pre></div></div>

<p>각 Custom Prompt는 다음 두 필드로 구성됩니다.</p>

<table>
  <thead>
    <tr>
      <th>필드</th>
      <th>역할</th>
      <th>예시</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>프롬프트 내용</td>
      <td>추출 대상 장면을 기술</td>
      <td>“인물이 등장하는 장면”</td>
    </tr>
    <tr>
      <td>디스크립션 생성 가이드 (선택)</td>
      <td>VLM이 서술할 때 중점적으로 포함할 내용을 지정</td>
      <td>“의상 위주로 서술”</td>
    </tr>
  </tbody>
</table>

<p>두 필드는 VLM 프롬프트에 하나의 줄로 결합됩니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>인물이 등장하는 장면(의상 위주로 서술)
</code></pre></div></div>

<p>Custom Prompt는 사용자가 UI에서 직접 입력하는 영역이며,<br />
이 프로젝트에서의 역할은 사용자가 원하는 장면을 효과적으로 추출할 수 있도록 <br />
작성 가이드를 설계하고 추출에 최적화된 인터페이스 구조를 제안하는 것이었습니다.<br />
프롬프트 엔지니어링 대상이 Custom Prompt에 한정된다는 점이 핵심 제약이었습니다.</p>

<h3 id="5-영어-프롬프트-vs-한국어-프롬프트">5. 영어 프롬프트 vs 한국어 프롬프트</h3>

<p>프롬프트 엔지니어링이 확산되던 초기에는 논문·예제·벤치마크 대부분이 영어 기준이었기 때문에<br />
영어 프롬프트가 사실상 기본값처럼 사용되었습니다.</p>

<ul>
  <li>레퍼런스가 영어 중심이라 시행착오가 상대적으로 적었고</li>
  <li>당시에는 한국어 프롬프트에서 표현/출력 일관성이 떨어지는 경우도 종종 관찰되었습니다.</li>
</ul>

<p>이번 프로젝트에서는 모든 Custom Prompt를 한국어로 작성했습니다.<br />
모델 자체가 다국어 사전학습(multilingual pretraining)을 거친 최신 버전이었고,<br />
임베딩 모델 또한 다국어 지원 모델을 사용했으며, 사용자가 한국어 기반 UI를 사용했기 때문에<br />
쿼리-서술-후처리 전 과정을 동일 언어로 통일하는 것이 일관성 측면에서 유리했습니다.</p>

<p>영어 프롬프트는 별도로 비교 실험하지 않았으나,<br />
동일 데이터셋에서 한국어 프롬프트의 출력 안정성과 후처리 일관성이 충분하다고 판단했습니다.</p>

<h3 id="6-vlm은-언어를-타는가">6. VLM은 언어를 타는가?</h3>

<p>많은 VLM은 시각 정보가 언어 모델 출력으로 자연스럽게 이어지도록<br />
시각-언어 정렬(alignment)을 거치며 학습됩니다.<br />
이때 언어 모델이 어떤 언어 데이터로 얼마나 학습되었는지가 출력 안정성에 영향을 줍니다.</p>

<ul>
  <li>영어 중심 데이터로 학습된 모델 → 영어 프롬프트에 더 안정적인 편</li>
  <li>다국어 학습 모델 → 한국어 포함 여러 언어에서 상대적으로 안정적</li>
</ul>

<p>모델이 언어를 가린다기보다는,<br />
학습 데이터의 분포와 토큰 통계에 영향을 받는다고 보는 것이 더 정확합니다.</p>

<p>실무에서 실제로 달라지는 부분은 다음과 같습니다.</p>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>영어</th>
      <th>한국어</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>토큰 분해</td>
      <td>단어 단위 토큰화가 비교적 안정적</td>
      <td>형태소 분해 + 조사 결합으로 토큰 길이 변동</td>
    </tr>
    <tr>
      <td>동사 활용</td>
      <td>run / running / ran</td>
      <td>달리다 / 달리고 있다 / 달린다 / 달렸다 → 키워드 매칭에서 차이 발생</td>
    </tr>
    <tr>
      <td>임베딩 일관성</td>
      <td>다국어 임베딩 모델을 쓰면 언어 간 격차가 줄어드는 편 (모델/도메인 의존)</td>
      <td>동일 언어 내에서는 더 안정적</td>
    </tr>
  </tbody>
</table>

<p>VLM, 임베딩 모델 모두 다국어 지원 모델을 사용했고,<br />
검색 쿼리도 한국어였으므로 전 과정을 한국어로 통일했습니다.</p>

<hr />

<h2 id="3-rag-retrieval-augmented-generation">3. RAG (Retrieval-Augmented Generation)</h2>

<p>RAG는 LLM의 응답에 외부 검색 결과를 결합하는 구조입니다.<br />
LLM은 학습 데이터에 포함되지 않은 정보 (사내 문서, 실시간 데이터, 도메인 지식 등)에 대해<br />
정확한 응답을 생성하기 어려우나, RAG는 이 한계를 보완합니다.</p>

<h3 id="1-동작-원리">1. 동작 원리</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. 사용자 질문 입력
2. 질문을 벡터로 변환 (임베딩)
3. 벡터 DB 또는 검색 엔진에서 관련 문서/데이터 검색
4. 검색 결과를 LLM의 컨텍스트에 삽입
5. LLM이 검색 결과를 참고하여 응답 생성
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌──────────┐     ┌──────────────┐     ┌──────────┐
│ 사용자    │────▶│  검색 엔진    │────▶│   LLM    │
│ 질문      │     │ (벡터 DB 등) │     │          │
└──────────┘     └──────────────┘     └──────────┘
                   │ 관련 문서 반환        │ 검색 결과 + 질문
                   └──────────────────────▶│ → 응답 생성
</code></pre></div></div>

<h3 id="2-llm-단독-vs-rag-적용">2. LLM 단독 vs RAG 적용</h3>

<table>
  <thead>
    <tr>
      <th>구분</th>
      <th>LLM 단독</th>
      <th>RAG 적용</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>지식 범위</td>
      <td>학습 데이터까지</td>
      <td>외부 DB의 최신 데이터까지</td>
    </tr>
    <tr>
      <td>할루시네이션</td>
      <td>모르는 내용을 생성할 위험</td>
      <td>검색된 근거 기반으로 응답</td>
    </tr>
    <tr>
      <td>업데이트</td>
      <td>재학습 필요</td>
      <td>DB만 업데이트하면 됨</td>
    </tr>
    <tr>
      <td>비용</td>
      <td>모델 재학습 비용</td>
      <td>검색 인프라 비용</td>
    </tr>
    <tr>
      <td>대표 사례</td>
      <td>검색 결합 없는 순수 LLM 서비스</td>
      <td>사내 문서 QA, 기술 지원 챗봇</td>
    </tr>
  </tbody>
</table>

<h3 id="3-영상-추출에서의-rag">3. 영상 추출에서의 RAG</h3>

<p>영상 추출 파이프라인에서는 RAG를 답변 생성이 아닌 검색 결과의 re-scoring 용도로 사용합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>일반적인 RAG:    질문 → 문서 검색 → LLM이 답변 생성
영상 추출의 RAG:  쿼리 → 임베딩 검색 → LLM이 각 결과에 관련도 점수(1~10) 산출
</code></pre></div></div>

<p>임베딩 유사도 검색(1차)만으로는 부정문, 동의어, 맥락 차이를 정확히 구분하지 못하므로,<br />
LLM이 텍스트를 직접 읽고 2차 필터링하는 구조입니다.</p>

<table>
  <thead>
    <tr>
      <th>구분</th>
      <th>임베딩 검색 (1차)</th>
      <th>RAG 재점수 (2차)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>방식</td>
      <td>벡터 간 코사인 유사도</td>
      <td>LLM이 텍스트를 읽고 점수 산출</td>
    </tr>
    <tr>
      <td>장점</td>
      <td>빠름, 대량 처리 가능</td>
      <td>부정문/동의어/맥락 이해 가능</td>
    </tr>
    <tr>
      <td>한계</td>
      <td>부정 표현도 유사도가 높게 산출될 수 있음</td>
      <td>느림, LLM 비용 발생</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="4-영상-추출-파이프라인">4. 영상 추출 파이프라인</h2>

<p>영상에서 원하는 장면을 찾는 것은 구조적으로 검색 문제에 가깝습니다.<br />
다만, 영상은 텍스트처럼 직접 검색할 수 없으므로 다음과 같은 변환이 필요합니다:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>영상 → VLM으로 텍스트 변환(인덱싱) → 텍스트 임베딩 저장 → 사용자 쿼리로 검색
</code></pre></div></div>

<p>이 구조에서 VLM의 서술은 검색 인덱스의 역할을 합니다.<br />
검색 엔진에서 문서 품질이 검색 결과를 좌우하듯, <br />
VLM 서술 품질이 장면 추출 품질에 크게 영향을 줍니다.</p>

<p>이번 프로젝트에서 사용한 평가 지표(Precision, Recall, F1)도<br />
정보 검색(Information Retrieval) 분야에서 사용하는 것과 동일합니다.<br />
검색 결과 중 정답 비율(Precision)과 전체 정답 중 찾은 비율(Recall)로 검색 품질을 측정합니다.</p>

<p>전체 시스템은 분석엔진과 추출 서버 두 단계로 구성됩니다.</p>

<h3 id="1-분석엔진-vlm-추론">1. 분석엔진 (VLM 추론)</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>영상 입력 → 프레임 샘플링 (N프레임마다 1장) → 배치 구성 (10~20프레임) → VLM 추론 → 구간별 서술 생성
</code></pre></div></div>

<p>VLM 인터페이스 내부에서 프롬프트는 고정 영역과 가변 영역으로 나뉩니다.</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>Custom Prompt</td>
      <td>유일한 가변 영역</td>
    </tr>
  </tbody>
</table>

<p>각 프롬프트의 내용은 다음과 같습니다.</p>

<p><strong>기본 프롬프트:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>당신은 영상 분석 전문가입니다.
주어진 영상 프레임에 나타난 객체와 인물의 행동에 초점을 맞춰
현재 상황을 서술하세요.
화면에 보이는 시각적 정보만 사용하며,
추측이나 영상에 없는 정보를 만들어내서는 안 됩니다.
</code></pre></div></div>

<p><strong>작업 순서 프롬프트:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>작업 순서:
1) 중점 상황이 실제로 보이는지 확인
2) 보이지 않으면 → 핵심 장면만 사실적으로 요약
3) 보이면 → 해당 장면을 우선으로 1~2문장으로 요약
</code></pre></div></div>

<p><strong>Custom Prompt 안내 문구:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>다음 상황들을 중점적으로 서술하여야 합니다.
해당 상황이 나타나지 않으면 절대 서술하지 않아야 하며,
항목 옆에 ()로 지시문이 있으면 따라 서술하여야 합니다.
</code></pre></div></div>

<p><strong>출력 형식 프롬프트:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- 한국어로 1~2문장 작성
- 나오지 않는 상황은 절대 서술하지 않음
- 중점 상황이 없으면 화면에 나타난 장면을 직관적으로 서술
- 포함되어 있지 않다는 이야기를 절대 하지 않음
</code></pre></div></div>

<p>최종 프롬프트 조합 방식:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{기본 프롬프트}
+
{작업 순서 프롬프트}
+
{안내 문구}
+
1. {custom_prompt_1}
2. {custom_prompt_2}
+
{출력 형식 프롬프트}
</code></pre></div></div>

<h3 id="2-추출-서버-클립-생성">2. 추출 서버 (클립 생성)</h3>

<p>분석엔진의 VLM 서술 결과는 Kafka를 통해 추출 서버로 전달되고,<br />
Elasticsearch에 임베딩 벡터와 함께 저장됩니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>사용자 쿼리 → 쿼리 임베딩 → ES KNN 검색 → RAG 재점수 → 구간 병합 → 클립 추출
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>단계</th>
      <th>동작</th>
      <th>주요 파라미터</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>쿼리 임베딩</td>
      <td>사용자 검색어를 벡터로 변환</td>
      <td>다국어 임베딩 모델</td>
    </tr>
    <tr>
      <td>ES KNN 검색</td>
      <td>VLM 서술 임베딩과 유사도 비교</td>
      <td>유사도 임계값, 후보 수</td>
    </tr>
    <tr>
      <td>RAG 재점수</td>
      <td>LLM이 각 결과를 1~10점으로 재평가</td>
      <td>최소 점수 기준</td>
    </tr>
    <tr>
      <td>구간 병합</td>
      <td>인접 구간 합치기</td>
      <td>병합 간격, 최소 구간 길이</td>
    </tr>
  </tbody>
</table>

<p>RAG 재점수 시 적용되는 규칙은 다음과 같습니다:</p>

<ul>
  <li>동의어 허용</li>
  <li>부정문 제외</li>
  <li>과도한 추론 금지</li>
  <li>일정 점수 이상만 최종 선택</li>
</ul>

<hr />

<h2 id="5-고정-프롬프트의-상충-문제">5. 고정 프롬프트의 상충 문제</h2>

<h3 id="1-고정-프롬프트가-항상-서술하도록-설계된-이유">1. 고정 프롬프트가 항상 서술하도록 설계된 이유</h3>

<p>분석엔진의 VLM은 모든 구간을 자연어로 서술해 검색 인덱스를 생성하는 역할로 설계되었습니다.<br />
서술이 없는 구간은 검색 대상이 될 수 없으므로,<br />
“항상 무언가를 서술하라”는 지시는 검색 커버리지 관점에서 합리적입니다.</p>

<p>Custom Prompt가 추가되어도 VLM은 여전히 모든 구간을 서술합니다.<br />
다만 특정 장면이 있으면 그것을 우선 서술하도록 방향을 유도하는 것이 원래 의도였습니다.</p>

<p>문제는 이 구조를 특정 장면 추출 용도로 사용할 때 발생합니다.<br />
분석엔진의 프롬프트는 추출 파이프라인을 고려하여 설계된 것이 아니므로,<br />
범용 서술용 프롬프트와 Custom Prompt 사이에 상충이 생깁니다.</p>

<p>Custom Prompt 기능이 추가될 때 안내 문구만 삽입되었고,<br />
기존 작업 순서 프롬프트는 수정되지 않았습니다.<br />
그 결과 실제 동작에서는 긍정 지시(서술)가 우선되면서,<br />
‘서술하지 말라’는 부정 지시가 기대만큼 반영되지 않았습니다.</p>

<h3 id="2-상충-지시에-대한-vlm의-동작">2. 상충 지시에 대한 VLM의 동작</h3>

<p>LLM/VLM은 프롬프트 내에서 모순되는 지시를 받으면 한쪽을 우선하여 동작합니다.</p>

<table>
  <thead>
    <tr>
      <th>관찰된 패턴</th>
      <th>설명</th>
      <th>적용</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Recency Bias (최신 우선)</td>
      <td>프롬프트 뒤쪽에 위치한 지시가 우선됨</td>
      <td>관찰상 recency 효과는 제한적이었고, 구체성/긍정 지시가 더 강하게 작동</td>
    </tr>
    <tr>
      <td>구체성 우선</td>
      <td>구체적인 지시가 추상적인 지시보다 우선됨</td>
      <td>“화면에 보이는 핵심 장면을 요약”(구체적 행동 지시)이 “서술하지 않아야 하며”(추상적 금지)보다 우선</td>
    </tr>
    <tr>
      <td>긍정 지시 우선</td>
      <td>“~해라”가 “~하지 마라”보다 실행되기 쉬움</td>
      <td>“장면을 서술해라”가 “서술하지 마라”보다 우선 → 결과적으로 “어쨌든 서술”</td>
    </tr>
  </tbody>
</table>

<p>이 세 가지 패턴이 결합되어 부정 지시가 기대만큼 우선되지 않았고,<br />
대상 장면이 없어도 서술이 생성되는 경우가 잦았습니다.</p>

<p>이 동작의 결과로 발생하는 문제:</p>

<table>
  <thead>
    <tr>
      <th>상황</th>
      <th>VLM 출력</th>
      <th>문제점</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>대상 장면 있음</td>
      <td>“빨간 조끼를 입은 사람이 달리고 있다.”</td>
      <td>정상</td>
    </tr>
    <tr>
      <td>대상 장면 없음</td>
      <td>“빨간 조끼를 입은 사람이 달리는 장면은 나타나지 않았습니다. 두 남성이 대화를…”</td>
      <td>부정문에 “조끼”, “달리” 키워드 포함 → 오탐</td>
    </tr>
  </tbody>
</table>

<p>이 부정문 서술은 후속 파이프라인에서 두 가지 경로로 오탐을 유발합니다:</p>

<ol>
  <li>키워드 매칭: “조끼”, “달리” 등 목표 키워드가 부정문에 포함되어 양성 판정</li>
  <li>임베딩 유사도: 부정문이라도 목표 키워드를 포함하므로<br />
임베딩 벡터 간 유사도가 임계값 이상으로 산출될 수 있음</li>
</ol>

<hr />

<h2 id="6-서술형-vs-감지형-인터페이스">6. 서술형 vs 감지형 인터페이스</h2>

<p>VLM이 영상을 분석할 때 결과를 출력하는 방식은 크게 두 가지로 나눌 수 있습니다.</p>

<table>
  <thead>
    <tr>
      <th>구분</th>
      <th>서술형 (v1)</th>
      <th>감지형 (v2)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>역할</td>
      <td>모든 구간에서 장면을 서술</td>
      <td>특정 장면이 있을 때만 서술</td>
    </tr>
    <tr>
      <td>출력 (대상 있음)</td>
      <td>“빨간 조끼를 입은 사람이 달리고 있다.”</td>
      <td>“빨간 조끼를 입은 사람이 달리고 있다.”</td>
    </tr>
    <tr>
      <td>출력 (대상 없음)</td>
      <td>“해당 장면은 나타나지 않았습니다. 두 남성이 테이블에 앉아…”</td>
      <td>“N/A”</td>
    </tr>
    <tr>
      <td>후처리</td>
      <td>키워드 매칭 필요</td>
      <td>N/A 여부만 확인</td>
    </tr>
    <tr>
      <td>오탐 위험</td>
      <td>부정문에 키워드 포함 → 오탐 발생</td>
      <td>대상 없으면 출력 없음 → 오탐 감소</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>서술형은 항상 무언가를 출력하므로,<br />
후속 파이프라인에서 부정문 서술도 유사도 점수를 받을 수 있습니다.</p>
</blockquote>

<hr />

<h2 id="7-프롬프트-전략-3종-비교">7. 프롬프트 전략 3종 비교</h2>

<p>Custom Prompt 최적화를 위해 3가지 전략을 설계하고 동일 조건에서 비교했습니다.</p>

<table>
  <thead>
    <tr>
      <th>전략</th>
      <th>프롬프트 구조</th>
      <th>감지 방식</th>
      <th>특징</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>v1 (서술형 + 키워드 OR)</td>
      <td>기존 고정 프롬프트 그대로</td>
      <td>키워드 1개 이상 매칭</td>
      <td>기존 시스템 동작 방식</td>
    </tr>
    <tr>
      <td>v2 (감지형 N/A)</td>
      <td>감지형 전용 프롬프트</td>
      <td>N/A가 아니면 감지</td>
      <td>인터페이스 구조 변경</td>
    </tr>
    <tr>
      <td>v3 (서술형 + 지시문 + AND)</td>
      <td>v1 프롬프트 + () 지시문</td>
      <td>AND 키워드 그룹 매칭</td>
      <td>v1 유지하면서 후처리 강화</td>
    </tr>
  </tbody>
</table>

<h3 id="1-v1-서술형--키워드-or">1. v1: 서술형 + 키워드 OR</h3>

<p>기존 인터페이스를 그대로 사용합니다.<br />
VLM 출력에서 목표 키워드가 하나라도 포함되면 감지로 판정합니다.</p>

<h3 id="2-v2-감지형-na">2. v2: 감지형 N/A</h3>

<p>프롬프트 구조를 재설계하여 감지 대상의 모든 조건이 동시에 충족되는 경우에만 장면을 서술하고,<br />
하나라도 충족되지 않으면 <code class="language-plaintext highlighter-rouge">"N/A"</code>만 출력하도록 합니다.<br />
후처리는 N/A 여부만 확인하면 됩니다.</p>

<h3 id="3-v3-서술형---지시문--and-키워드">3. v3: 서술형 + () 지시문 + AND 키워드</h3>

<p>v1의 프롬프트 구조를 유지하면서,<br />
<code class="language-plaintext highlighter-rouge">()</code> 지시문으로 VLM 출력에 복장/색상/동작 정보를 강제 포함시킵니다.<br />
후처리에서 AND 키워드 그룹 매칭과 제외 키워드를 적용합니다.</p>

<p>AND 키워드 매칭 예시:</p>

<table>
  <thead>
    <tr>
      <th>그룹</th>
      <th>키워드</th>
      <th>의미</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>그룹1 (색상)</td>
      <td>“빨간”</td>
      <td>복장 색상</td>
    </tr>
    <tr>
      <td>그룹2 (복장)</td>
      <td>“조끼”, “유니폼”</td>
      <td>복장 종류</td>
    </tr>
    <tr>
      <td>그룹3 (동작)</td>
      <td>“달리”</td>
      <td>행동</td>
    </tr>
    <tr>
      <td>제외</td>
      <td>“자세”, “준비”, “하려는”</td>
      <td>실제 동작이 아닌 표현</td>
    </tr>
  </tbody>
</table>

<p>모든 그룹에서 최소 1개씩 매칭되어야 감지 판정합니다.</p>

<hr />

<h2 id="8-테스트-자동화-환경">8. 테스트 자동화 환경</h2>

<h3 id="1-테스트-파이프라인">1. 테스트 파이프라인</h3>

<p>각 전략의 정량 비교를 위해 다음 파이프라인을 자동화했습니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. 프레임 추출 (ffmpeg, N프레임 간격)
2. VLM 추론 (vLLM API, 배치 병렬 처리)
3. 후처리 (키워드 매칭 / N/A 판정)
4. 구간 병합 (인접 감지 구간 합치기)
5. 정답 비교 (IoU 기반 Precision/Recall/F1)
6. 리포트 생성 (JSON + CSV + 클립 추출)
</code></pre></div></div>

<h3 id="2-테스트-조합">2. 테스트 조합</h3>

<p>복수의 추출 유형 × 3가지 프롬프트 전략 × 복수 영상의 조합을 테스트했습니다.<br />
추출 유형은 행동추출(특정 복장/동작 인물 탐지)과 배경 추출(건물, 자연 등)로 구분했습니다.</p>

<h3 id="3-평가-지표">3. 평가 지표</h3>

<p>정답 비교에는 IoU(Intersection over Union) 기반의 매칭을 사용했습니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>IoU = (예측 구간 ∩ 정답 구간) / (예측 구간 ∪ 정답 구간)
</code></pre></div></div>

<p>예를 들어 IoU 임계값을 0.3으로 설정한 경우,<br />
이 이상인 구간을 매칭으로 판정하고 Precision, Recall, F1을 산출합니다.</p>

<table>
  <thead>
    <tr>
      <th>지표</th>
      <th>수식</th>
      <th>의미</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Precision</td>
      <td>TP / (TP + FP)</td>
      <td>감지한 것 중 정답 비율</td>
    </tr>
    <tr>
      <td>Recall</td>
      <td>TP / (TP + FN)</td>
      <td>정답 중 감지한 비율</td>
    </tr>
    <tr>
      <td>F1</td>
      <td>2 × P × R / (P + R)</td>
      <td>Precision과 Recall의 조화 평균</td>
    </tr>
  </tbody>
</table>

<p>정답 클립의 타임스탬프가 없는 경우, 퍼셉추얼 해싱(aHash/dHash)으로 자동 매칭을 시도했으나,  <br />
정확도가 충분하지 않아 원본 영상을 직접 재생하며 수작업으로 타임스탬프를 매칭했습니다.</p>

<hr />

<h2 id="9-실험-결과">9. 실험 결과</h2>

<p>3가지 전략을 동일 조건에서 비교했습니다.</p>

<h3 id="1-행동추출">1. 행동추출</h3>

<p>특정 복장/동작을 가진 인물을 탐지하는 유형입니다.</p>

<table>
  <thead>
    <tr>
      <th>전략</th>
      <th>결과</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>v1 (서술형)</td>
      <td>대량의 오탐 발생</td>
    </tr>
    <tr>
      <td>v2 (감지형)</td>
      <td>오탐 대폭 감소, 대부분의 정답 탐지</td>
    </tr>
    <tr>
      <td>v3 (AND 매칭)</td>
      <td>오탐 일부 감소, 부정문 키워드는 여전히 통과</td>
    </tr>
  </tbody>
</table>

<p>v1에서 프롬프트를 상세하게 작성할수록 VLM이 부정문에도 목표 키워드를 더 많이 포함시켜<br />
오히려 역효과가 발생했습니다.</p>

<h3 id="2-배경-추출">2. 배경 추출</h3>

<p>건물, 자연 등 소재 장면을 탐지하는 유형으로 v1, v2 모두 유의미한 결과를 내지 못했습니다.<br />
정답 기준이 영상미, 구도, 장면전환 등 주관적 요소에 의존하기 때문입니다.</p>

<h3 id="3-인코딩-영향">3. 인코딩 영향</h3>

<p>해상도가 동일하더라도 비트레이트가 낮아지면 압축 아티팩트가 증가하고<br />
복장 색상, 로고 등 세부 시각 요소가 손실되어 동일한 프롬프트에서도 서술 결과가 달라졌습니다.</p>

<h3 id="4-부정문의-자연-필터링">4. 부정문의 자연 필터링</h3>

<p>테스트에서는 VLM 출력을 직접 키워드 매칭하여 부정문이 대량의 오탐을 유발했습니다.<br />
그러나 추출 서버에 배포된 임베딩 모델로 동일한 결과를 재검증하면,<br />
부정문 서술 중 상당수는 유사도 임계값 미만으로 자동 탈락합니다.<br />
즉, 임베딩 유사도 임계값 단계에서 일부 오탐이 추가로 제외됩니다.</p>

<p>다만 모든 부정문이 걸러지는 것은 아니며,<br />
이 임계값은 코드에 하드코딩되어 있어 프롬프트 작성자가 조절할 수 없습니다.</p>

<hr />

<h2 id="10-실험-중-발견한-이슈">10. 실험 중 발견한 이슈</h2>

<p>실험 과정에서 프롬프트 최적화만으로는 해결할 수 없는 시스템 이슈들을 발견했습니다.</p>

<table>
  <thead>
    <tr>
      <th>이슈</th>
      <th>원인</th>
      <th>영향</th>
      <th>대응</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>고정 프롬프트의 지시 모순</td>
      <td>작업 순서 프롬프트가 추출이 아닌 서술 목적으로 설계됨</td>
      <td>Custom Prompt를 아무리 정교하게 작성해도 부정문 서술 생성을 막을 수 없음</td>
      <td>v2 감지형 인터페이스로 작업 순서 프롬프트 자체를 교체</td>
    </tr>
    <tr>
      <td>상세 프롬프트의 역효과</td>
      <td>프롬프트에 정보를 많이 포함할수록 VLM이 부정문에도 해당 키워드를 서술</td>
      <td>오탐이 오히려 증가</td>
      <td>서술형에서는 프롬프트 상세도를 높이지 않음</td>
    </tr>
    <tr>
      <td>추출 서버의 하드코딩된 임계값</td>
      <td>임베딩 유사도 임계값, RAG 최소 점수 등이 코드 내 상수로 고정</td>
      <td>프롬프트 작성자가 제어할 수 없는 영역</td>
      <td>파라미터 외부화 제안</td>
    </tr>
    <tr>
      <td>인코딩 방식에 따른 결과 차이</td>
      <td>비트레이트 변화로 프레임 품질이 저하</td>
      <td>동일 프롬프트에서도 감지 결과가 달라짐</td>
      <td>입력 영상 품질 기준 사전 정의</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="11-리서치-단계에서-검토한-접근법">11. 리서치 단계에서 검토한 접근법</h2>

<p>프로젝트 초기에 다음 접근법들을 리서치하고 일부를 실제 구현하여 테스트했습니다.</p>

<table>
  <thead>
    <tr>
      <th>접근법</th>
      <th>내용</th>
      <th>구현 여부</th>
      <th>프로덕션 적용</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Dense Caption → Query Prompt</td>
      <td>정답 클립에서 캡션 추출 후 검색용 프롬프트 자동 생성</td>
      <td>구현 완료</td>
      <td>불가 (UI가 자연어 1줄 입력)</td>
    </tr>
    <tr>
      <td>구조화 조건 (JSON 스키마)</td>
      <td>필수/선호/제외 조건을 구조화하여 입력</td>
      <td>구현 완료</td>
      <td>불가 (JSON 입력 미지원)</td>
    </tr>
    <tr>
      <td>Keyframe 요약 + 변화점</td>
      <td>시작/종료 트리거 조건 명시</td>
      <td>일부 구현</td>
      <td>불가 (5초 단위 프레임으로 시간 흐름 인식 제한)</td>
    </tr>
    <tr>
      <td>다중 후보 프롬프트 앙상블</td>
      <td>관점별 쿼리 통합 검색</td>
      <td>구현 완료</td>
      <td>불가 (프롬프트 입력 수 제한, 앙상블 로직 없음)</td>
    </tr>
    <tr>
      <td>Hard Negative Mining</td>
      <td>오답 구간과의 차이로 배제 조건 강화</td>
      <td>분석 완료</td>
      <td>불가 (배제 조건 전달 인터페이스 없음)</td>
    </tr>
    <tr>
      <td>타깃 클립 매칭</td>
      <td>타깃 클립 스키마 → 슬라이딩 윈도우 점수화</td>
      <td>구현 완료</td>
      <td>불가 (프로덕션 파이프라인 구조 상이)</td>
    </tr>
  </tbody>
</table>

<p>이 중 Dense Caption, 구조화 조건, 앙상블 등은 구현까지 완료했으나,<br />
인터페이스가 자연어 Custom Prompt만 받는 구조라 적용할 수 없었습니다.</p>

<h3 id="1-프롬프트-작성자의-제어-가능-범위">1. 프롬프트 작성자의 제어 가능 범위</h3>

<p>Custom Prompt 작성자가 실제로 제어할 수 있는 영역과<br />
없는 영역을 정리하면 다음과 같습니다.</p>

<table>
  <thead>
    <tr>
      <th>파이프라인 단계</th>
      <th>제어 주체</th>
      <th>프롬프트 작성자의 영향</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>VLM 추론</td>
      <td>고정 프롬프트 + Custom Prompt</td>
      <td>Custom Prompt만 제어 가능</td>
    </tr>
    <tr>
      <td>임베딩 유사도</td>
      <td>임베딩 모델 + 임계값</td>
      <td>제어 불가</td>
    </tr>
    <tr>
      <td>RAG 재점수</td>
      <td>RAG 시스템 프롬프트 + 최소 점수</td>
      <td>제어 불가</td>
    </tr>
    <tr>
      <td>구간 병합</td>
      <td>병합 파라미터</td>
      <td>제어 불가</td>
    </tr>
    <tr>
      <td>클립 추출</td>
      <td>영상 처리 로직</td>
      <td>제어 불가</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="12-프롬프트-엔지니어링-인사이트">12. 프롬프트 엔지니어링 인사이트</h2>

<p>실험 과정에서 확인한 프롬프트 작성 시 고려사항입니다.</p>

<table>
  <thead>
    <tr>
      <th>원칙</th>
      <th>비효과적 예시</th>
      <th>효과적 예시</th>
      <th>이유</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>부정 조건 활용</td>
      <td>건물이 보이면 예</td>
      <td>건물이 보이고, 인물 클로즈업이 아니고, 실내가 아니면 예</td>
      <td>긍정 조건만으로는 False Positive 많음</td>
    </tr>
    <tr>
      <td>구체적 시각 요소</td>
      <td>고풍스러운 분위기</td>
      <td>기와지붕, 돌담, 목조 구조물이 보임</td>
      <td>분위기는 모호, 시각 요소는 명확</td>
    </tr>
    <tr>
      <td>카메라 구도 명시</td>
      <td>배경이 잘 보임</td>
      <td>와이드샷 또는 버드아이뷰로 건물 전체가 보임</td>
      <td>구도를 특정해야 VLM이 판별 가능</td>
    </tr>
    <tr>
      <td>인물 허용 기준</td>
      <td>인물이 없어야 함</td>
      <td>인물이 있어도 화면의 1/3 이하를 차지하고, 풍경의 일부로 보임</td>
      <td>절대 기준은 False Negative 유발 (군중 씬 제외)</td>
    </tr>
    <tr>
      <td>출력 형식 고정</td>
      <td>자유 서술</td>
      <td>배경탐지: 예/아니오, 설명: (판정 근거 한 문장)</td>
      <td>형식이 고정되어야 후처리 자동화 가능</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="마무리">마무리</h2>

<p>이 글에서는 VLM 영상 추출 시스템의 프롬프트 최적화와 구조적 한계를 정리했습니다.</p>

<ol>
  <li>서술형(v1), 감지형(v2), AND 후처리(v3) 전략을 동일 조건에서 비교</li>
  <li>자동화 파이프라인을 구축해 IoU, Precision, Recall, F1 기반 정량 평가 수행</li>
  <li>감지형 인터페이스가 행동추출에서 가장 안정적인 결과를 보임</li>
  <li>프롬프트 수정만으로는 고정 프롬프트의 모순과 파이프라인 제약을 해소할 수 없음을 확인</li>
  <li>임베딩·재점수·구간 병합 단계가 실제 성능에 큰 영향을 미침을 검증</li>
</ol>

<p>핵심 제약은 고정 프롬프트를 변경할 수 없다는 점이었습니다.<br />
범용 서술 목적의 프롬프트 위에 특정 장면 추출 요구가 추가되면서 상충 지시가 공존하게 되었고,<br />
이는 Custom Prompt만으로 조정하기 어려운 영역이었습니다.</p>

<p>임베딩 임계값, RAG 재점수 기준, 구간 병합 로직 등<br />
후속 파이프라인의 파라미터 역시 프롬프트의 통제 범위를 벗어나 있습니다.</p>

<p>프롬프트는 시스템의 한 구성 요소입니다.<br />
후처리·검색·인터페이스와 역할을 구분하지 않으면 성능 개선은 한계에 부딪힐 수밖에 없습니다.</p>

<p>프롬프트의 책임과 시스템의 책임을 구분하는 인식이 먼저 필요합니다.</p>]]></content><author><name>indexkim</name></author><category term="vision-ai" /><category term="vlm" /><category term="model" /><category term="data" /><category term="engine" /><summary type="html"><![CDATA[VLM(Vision Language Model)은 시각 정보와 언어 정보를 함께 처리하는 멀티모달 모델입니다. 이 글에서는 VLM에 영상 프레임을 입력해 장면을 자연어로 서술하는 방식으로 사용했습니다. 단순히 객체를 인식하는 것을 넘어, 누가 무엇을 하고 있는지를 문장으로 표현할 수 있어 영상 검색, 장면 분류, 자동 자막 생성 등에 활용됩니다.]]></summary></entry><entry><title type="html">폐쇄망 Docker 이미지 CVE 대응</title><link href="https://indexkim.github.io//common-vulnerabilities-and-exposures/" rel="alternate" type="text/html" title="폐쇄망 Docker 이미지 CVE 대응" /><published>2026-02-19T00:00:00+09:00</published><updated>2026-02-19T00:00:00+09:00</updated><id>https://indexkim.github.io//common-vulnerabilities-and-exposures</id><content type="html" xml:base="https://indexkim.github.io//common-vulnerabilities-and-exposures/"><![CDATA[<p>CVE(Common Vulnerabilities and Exposures)는 소프트웨어·하드웨어에서 발견된 보안 취약점을<br />
고유하게 식별하기 위해 부여되는 표준 ID 체계입니다.<br />
Docker 이미지에 포함된 OS 패키지나 Python 패키지 중 알려진 CVE가 존재하면<br />
보안 검수에서 반려될 수 있으며, 배포 전에 이를 식별하고 제거해야 합니다.</p>

<p>일반적인 프로젝트에서는 Docker 이미지의 CVE를 검수하지 않는 경우가 많지만,<br />
보안 요건이 높은 환경에서는 HIGH/CRITICAL 취약점이 0건이어야 배포가 승인됩니다.</p>

<p>온라인 환경에서는 <code class="language-plaintext highlighter-rouge">apt upgrade</code>나 <code class="language-plaintext highlighter-rouge">pip install --upgrade</code> 한 줄이면 끝나는 작업이지만,<br />
폐쇄망에서는 패키지 저장소에 접근할 수 없어 패치 파일을 직접 준비해야 하고,<br />
스캐너의 취약점 DB조차 수동으로 반입해야 합니다.<br />
실제로 대응을 진행해 보니, 패키지 업데이트 외에도<br />
커널 CVE의 검수 범위 문제나 패치 미제공 상황 등 예외 케이스가 많았습니다.</p>

<p>이 글은 폐쇄망 환경에서 Docker 이미지의 CVE를 스캔하고,<br />
패키지를 패치하고, 예외 상황에 대응하는 전체 과정을 정리한 것입니다.</p>

<blockquote>
  <p>특정 이미지나 프로젝트에 한정되지 않으며<br />
Debian/Ubuntu 기반 Docker 이미지라면 동일한 방식으로 적용할 수 있습니다.</p>
</blockquote>

<hr />

<h2 id="1-cvecommon-vulnerabilities-and-exposures">1. CVE(Common Vulnerabilities and Exposures)</h2>

<h3 id="cve란">CVE란?</h3>

<p>CVE는 MITRE Corporation이 운영하며, <code class="language-plaintext highlighter-rouge">CVE-연도-일련번호</code> 형식으로 표기됩니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CVE-2026-0861
 │    │    │
 │    │    └── 일련번호 (해당 연도 내 순번)
 │    └─────── 취약점이 등록된 연도
 └──────────── CVE 접두어
</code></pre></div></div>

<p>CVE가 등록되면 NVD(National Vulnerability Database)에서 해당 취약점의 심각도를 평가합니다.<br />
심각도는 CVSS(Common Vulnerability Scoring System)라는 점수 체계로 산정되며,<br />
0.0~10.0 범위의 점수가 다음과 같은 등급으로 분류됩니다.</p>

<table>
  <thead>
    <tr>
      <th>CVSS 점수</th>
      <th>등급</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>9.0 ~ 10.0</td>
      <td>Critical</td>
      <td>원격 코드 실행, 인증 우회 등 즉각 대응 필요</td>
    </tr>
    <tr>
      <td>7.0 ~ 8.9</td>
      <td>High</td>
      <td>권한 상승, 정보 유출 등 우선 대응 권장</td>
    </tr>
    <tr>
      <td>4.0 ~ 6.9</td>
      <td>Medium</td>
      <td>제한적 조건에서 악용 가능</td>
    </tr>
    <tr>
      <td>0.1 ~ 3.9</td>
      <td>Low</td>
      <td>악용 가능성이 낮음</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>보안 검수에서는 일반적으로 HIGH(7.0 이상)와 CRITICAL(9.0 이상)이 대상<br />
MEDIUM 이하는 조직 보안 정책에 따라 허용되는 경우가 많음</p>
</blockquote>

<p>Docker 이미지에는 OS 패키지와 Python 패키지가 포함되어 있으며,<br />
이 중 알려진 CVE가 존재하는 패키지가 있으면 보안 검수에서 반려됩니다.</p>

<table>
  <thead>
    <tr>
      <th>구분</th>
      <th>자주 보고되는 패키지 예시</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>OS 레벨 CVE</td>
      <td><code class="language-plaintext highlighter-rouge">linux-libc-dev</code>, <code class="language-plaintext highlighter-rouge">gnupg</code>, <code class="language-plaintext highlighter-rouge">gpgv</code>, <code class="language-plaintext highlighter-rouge">openssl</code>, <code class="language-plaintext highlighter-rouge">libexpat1</code> 등</td>
    </tr>
    <tr>
      <td>Python 레벨 CVE</td>
      <td><code class="language-plaintext highlighter-rouge">requests</code>, <code class="language-plaintext highlighter-rouge">urllib3</code>, <code class="language-plaintext highlighter-rouge">setuptools</code>, <code class="language-plaintext highlighter-rouge">torch</code>, <code class="language-plaintext highlighter-rouge">transformers</code> 등</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="취약점-스캐너">취약점 스캐너</h3>

<p>컨테이너 이미지의 CVE를 식별하려면 취약점 스캐너가 필요합니다.<br />
스캐너는 이미지 내 설치된 패키지 목록을 추출하고,<br />
취약점 DB와 대조하여 알려진 CVE가 포함되어 있는지 확인합니다.</p>

<h4 id="trivy">Trivy</h4>

<p><a href="https://github.com/aquasecurity/trivy">Trivy</a>는 Aqua Security에서 개발한 오픈소스 취약점 스캐너입니다.<br />
컨테이너 이미지, 파일시스템, Git 저장소, Kubernetes 클러스터 등을 스캔할 수 있으며,<br />
취약점 외에도 Misconfiguration과 Secret 노출까지 탐지합니다.</p>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>스캔 대상</td>
      <td>컨테이너 이미지, 파일시스템, Git 저장소, K8s 클러스터</td>
    </tr>
    <tr>
      <td>탐지 범위</td>
      <td>CVE, Misconfiguration, Secret</td>
    </tr>
    <tr>
      <td>바이너리 크기</td>
      <td>단일 바이너리 (~50MB), 별도 설치 불필요</td>
    </tr>
    <tr>
      <td>오프라인 지원</td>
      <td>DB를 사전 다운로드하면 완전 오프라인 스캔 가능</td>
    </tr>
  </tbody>
</table>

<h4 id="trivy-db-업데이트-방식">Trivy DB 업데이트 방식</h4>

<p>Trivy는 취약점 정보를 자체 DB에 저장하며,<br />
이 DB는 6시간 간격으로 빌드됩니다.<br />
Trivy CLI는 로컬 DB가 일정 시간(기본 12시간) 이상 경과하면<br />
스캔 전에 자동으로 최신 DB를 다운로드합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Trivy 스캔 실행
    ↓
DB 최신 여부 확인
    ↓ (오래된 경우)
OCI 레지스트리에서 DB 다운로드
    ↓
스캔 수행
</code></pre></div></div>

<p>DB는 다음 OCI 레지스트리에서 배포됩니다.</p>

<table>
  <thead>
    <tr>
      <th>레지스트리</th>
      <th>주소</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>GitHub Container Registry</td>
      <td><code class="language-plaintext highlighter-rouge">ghcr.io/aquasecurity/trivy-db</code></td>
    </tr>
    <tr>
      <td>Docker Hub</td>
      <td><code class="language-plaintext highlighter-rouge">aquasec/trivy-db</code></td>
    </tr>
    <tr>
      <td>AWS ECR</td>
      <td><code class="language-plaintext highlighter-rouge">public.ecr.aws/aquasecurity/trivy-db</code></td>
    </tr>
  </tbody>
</table>

<p>그러나 폐쇄망에서는 OCI 레지스트리에 접근할 수 없으므로<br />
외부망에서 DB를 미리 다운로드하여 물리 매체로 반입해야 합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 외부망: DB 다운로드</span>
./trivy image <span class="nt">--download-db-only</span>

<span class="c"># 외부망: DB 압축</span>
<span class="nb">cd</span> ~/.cache/trivy
<span class="nb">tar</span> <span class="nt">-czf</span> trivy-db.tar.gz db

<span class="c"># 폐쇄망: DB 복원</span>
<span class="nb">mkdir</span> <span class="nt">-p</span> /root/.cache/trivy
<span class="nb">tar</span> <span class="nt">-xzf</span> trivy-db.tar.gz <span class="nt">-C</span> /root/.cache/trivy

<span class="c"># 폐쇄망: 스캔 시 DB 업데이트 차단</span>
trivy fs <span class="nt">--skip-db-update</span> <span class="nt">--scanners</span> vuln /
</code></pre></div></div>

<blockquote>
  <p>폐쇄망에서의 Trivy DB는 수동 반입·수동 갱신<br />
DB가 오래되면 새로 발견된 CVE를 탐지하지 못하므로<br />
검수 시점에 맞춰 최신 DB를 다운로드하여 반입하는 것을 권장</p>
</blockquote>

<h4 id="다른-스캐너와-비교">다른 스캐너와 비교</h4>

<p>Trivy 외에도 컨테이너 이미지 취약점을 스캔할 수 있는 도구들이 있습니다.</p>

<table>
  <thead>
    <tr>
      <th>도구</th>
      <th>개발사</th>
      <th>오프라인 지원</th>
      <th>특징</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Trivy</td>
      <td>Aqua Security</td>
      <td>O (DB 사전 반입)</td>
      <td>단일 바이너리, 빠른 스캔, 설정 간편</td>
    </tr>
    <tr>
      <td>Grype</td>
      <td>Anchore</td>
      <td>O (DB 사전 반입)</td>
      <td>Syft(SBOM 생성 도구)와 연동, Trivy와 유사한 사용법</td>
    </tr>
    <tr>
      <td>Clair</td>
      <td>Red Hat/CoreOS</td>
      <td>O (복잡한 설정 필요)</td>
      <td>Kubernetes 통합에 강점, 서버 데몬 방식</td>
    </tr>
    <tr>
      <td>Docker Scout</td>
      <td>Docker</td>
      <td>X</td>
      <td>Docker Desktop 내장, 오프라인 미지원</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>Trivy를 선택한 이유는 오프라인 지원이 간편하고 단일 바이너리로 동작하기 때문<br />
Grype도 유사한 방식으로 오프라인 스캔이 가능하지만<br />
Clair는 서버 데몬을 별도로 구성해야 해서 폐쇄망에서는 설정 부담이 큼<br />
Docker Scout는 오프라인을 지원하지 않으므로 폐쇄망에서 사용 불가</p>
</blockquote>

<hr />

<h2 id="2-전체-전략">2. 전체 전략</h2>

<p>CVE 대응은 작업용 이미지와 배포용 이미지를 분리하여 진행합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>원본 이미지
    ↓
작업용 이미지 (bash ENTRYPOINT)
    → OS 패키지 제거
    → Python 패키지 업데이트
    → Trivy 스캔 (HIGH/CRITICAL = 0 확인)
    ↓
배포용 이미지 (원래 ENTRYPOINT 복구)
    → 서비스 실행용 최종 이미지
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>이미지</th>
      <th>ENTRYPOINT</th>
      <th>용도</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>작업용</td>
      <td><code class="language-plaintext highlighter-rouge">/bin/bash</code></td>
      <td>CVE 제거 작업 + Trivy 스캔</td>
    </tr>
    <tr>
      <td>배포용</td>
      <td>원본 ENTRYPOINT 복구</td>
      <td>실제 서비스 실행</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>작업용 이미지에서 CVE를 제거한 뒤 <code class="language-plaintext highlighter-rouge">docker commit</code>으로 고정하고<br />
배포용 이미지에서 ENTRYPOINT만 복구하는 2단계 구조<br />
이렇게 분리하면 스캔 도구와 서비스 실행이 충돌하지 않음</p>
</blockquote>

<hr />

<h2 id="3-외부망-준비-1회">3. 외부망 준비 (1회)</h2>

<p>폐쇄망 반입 전에 외부망에서 다음 파일들을 준비합니다.</p>

<h3 id="1-trivy-바이너리">1. Trivy 바이너리</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wget https://github.com/aquasecurity/trivy/releases/download/v0.69.1/trivy_0.69.1_Linux-64bit.tar.gz
<span class="nb">tar</span> <span class="nt">-xzf</span> trivy_0.69.1_Linux-64bit.tar.gz
<span class="nb">chmod</span> +x trivy
</code></pre></div></div>

<blockquote>
  <p>Trivy 릴리즈 페이지에서 최신 버전을 확인하여 다운로드</p>
</blockquote>

<hr />

<h3 id="2-trivy-취약점-db">2. Trivy 취약점 DB</h3>

<p>1장에서 설명한 대로, 폐쇄망에서는 Trivy DB를 수동으로 반입해야 합니다.<br />
1장의 DB 다운로드 및 압축 명령어를 참고하여 <code class="language-plaintext highlighter-rouge">trivy-db.tar.gz</code> 파일을 준비합니다.</p>

<blockquote>
  <p>DB는 6시간마다 갱신되므로 보안 검수 직전에 최신 DB를 다운로드하는 것을 권장</p>
</blockquote>

<hr />

<h3 id="3-python-whl-패키지">3. Python whl 패키지</h3>

<p>Trivy 스캔 결과에서 CVE가 보고된 Python 패키지의 패치된 버전을 <code class="language-plaintext highlighter-rouge">.whl</code> 파일로 다운로드합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip download &lt;패키지명&gt;<span class="o">==</span>&lt;패치_버전&gt; ...
</code></pre></div></div>

<p>예를 들어 <code class="language-plaintext highlighter-rouge">urllib3</code>에 HIGH 취약점이 보고되었고, 패치된 버전이 2.6.0이라면:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip download <span class="nv">urllib3</span><span class="o">==</span>2.6.0
</code></pre></div></div>

<blockquote>
  <p>패키지 버전은 CVE가 수정된 최소 버전 이상을 지정<br />
의존성 충돌을 방지하기 위해 설치 시 <code class="language-plaintext highlighter-rouge">--no-deps</code> 옵션을 사용할 예정이므로<br />
대상 패키지만 정확히 다운로드</p>
</blockquote>

<hr />

<h3 id="4-반입-대상-정리">4. 반입 대상 정리</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>trivy                  # Trivy 바이너리
trivy-db.tar.gz        # 취약점 DB
*.whl                  # 패치된 Python 패키지
</code></pre></div></div>

<hr />

<h2 id="4-작업용-이미지-생성">4. 작업용 이미지 생성</h2>

<h3 id="1-원본-이미지-태그">1. 원본 이미지 태그</h3>

<p>원본 이미지를 작업용 태그로 복제합니다.<br />
원본을 보존하면서 작업용 이미지를 분리하기 위함입니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker tag &lt;IMAGE&gt;:&lt;TAG&gt; &lt;IMAGE&gt;:&lt;TAG&gt;-work
</code></pre></div></div>

<hr />

<h3 id="2-작업용-컨테이너-실행">2. 작업용 컨테이너 실행</h3>

<p>ENTRYPOINT를 <code class="language-plaintext highlighter-rouge">/bin/bash</code>로 오버라이드하여 인터랙티브 셸로 진입합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker <span class="nb">rm</span> <span class="nt">-f</span> cve-work 2&gt;/dev/null <span class="o">||</span> <span class="nb">true

</span>docker run <span class="nt">-it</span> <span class="se">\</span>
  <span class="nt">--entrypoint</span> /bin/bash <span class="se">\</span>
  <span class="nt">--name</span> cve-work <span class="se">\</span>
  &lt;IMAGE&gt;:&lt;TAG&gt;-work
</code></pre></div></div>

<blockquote>
  <p><code class="language-plaintext highlighter-rouge">--entrypoint /bin/bash</code>로 실행해야 컨테이너 안에서 패키지 제거, 설치, 스캔 작업 가능<br />
원래 ENTRYPOINT로 실행하면 셸 접근이 제한됨</p>
</blockquote>

<hr />

<h2 id="5-컨테이너-내부-cve-제거">5. 컨테이너 내부 CVE 제거</h2>

<h3 id="1-파일-복사-호스트--컨테이너">1. 파일 복사 (호스트 → 컨테이너)</h3>

<p>호스트에서 준비한 파일들을 작업용 컨테이너 내부로 복사합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 별도 터미널에서 실행 (호스트)</span>
docker <span class="nb">cp </span>trivy cve-work:/usr/local/bin/trivy
docker <span class="nb">cp </span>trivy-db.tar.gz cve-work:/opt/trivy-db.tar.gz
docker <span class="nb">cp</span> /path/to/whl cve-work:/opt/whl
</code></pre></div></div>

<hr />

<h3 id="2-trivy-db-복원">2. Trivy DB 복원</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 컨테이너 내부에서 실행</span>
<span class="nb">mkdir</span> <span class="nt">-p</span> /root/.cache/trivy
<span class="nb">cd</span> /root/.cache/trivy
<span class="nb">tar</span> <span class="nt">-xzf</span> /opt/trivy-db.tar.gz
</code></pre></div></div>

<hr />

<h3 id="3-os-레벨-cve-제거">3. OS 레벨 CVE 제거</h3>

<p>Trivy 스캔 결과에서 HIGH/CRITICAL로 보고된 OS 패키지를 제거합니다.<br />
Debian/Ubuntu 기반 이미지에서 자주 보고되는 패키지는 다음과 같습니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apt-get update

<span class="c"># 예시: 자주 보고되는 취약 패키지 제거</span>
apt-get purge <span class="nt">-y</span> gnupg<span class="k">*</span> dirmngr
apt-get purge <span class="nt">-y</span> gpg gpgv gpgconf

<span class="c"># 불필요한 의존성 정리</span>
apt-get autoremove <span class="nt">-y</span>
apt-get clean
</code></pre></div></div>

<h4 id="os-패키지가-제거-가능한-이유">OS 패키지가 제거 가능한 이유</h4>

<p>Docker 이미지에는 빌드 시점에 필요했지만<br />
실제 서비스 실행(런타임)에는 사용되지 않는 패키지가 포함되어 있는 경우가 많습니다.<br />
이러한 패키지는 제거해도 서비스에 영향이 없으며,<br />
CVE 제거와 이미지 경량화를 동시에 달성할 수 있습니다.</p>

<p>gnupg, dirmngr, gpg, gpgv, gpgconf는 GPG(GNU Privacy Guard) 스택을 구성하는 패키지들로,<br />
각각의 역할은 다음과 같습니다.</p>

<table>
  <thead>
    <tr>
      <th>패키지</th>
      <th>역할</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">gnupg</code></td>
      <td>GPG 메타 패키지 (아래 도구들을 묶어서 설치)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">gpg</code></td>
      <td>암호화/서명/검증 수행</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">gpgv</code></td>
      <td>서명 검증 전용 (경량 버전)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">gpgconf</code></td>
      <td>GPG 설정 관리 도구</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">dirmngr</code></td>
      <td>키 서버와의 네트워크 통신 담당</td>
    </tr>
  </tbody>
</table>

<p>이들은 주로 <code class="language-plaintext highlighter-rouge">apt-get update</code> / <code class="language-plaintext highlighter-rouge">apt-get install</code> 시<br />
패키지 저장소의 서명을 검증하는 데 사용됩니다.<br />
이미지 빌드 과정에서 패키지를 설치할 때 필요하지만,<br />
빌드가 완료된 후에는 더 이상 패키지를 설치할 일이 없으므로 제거할 수 있습니다.</p>

<p>특히 폐쇄망 환경에서는 외부 패키지 저장소에 접근할 수 없으므로<br />
GPG 서명 검증 기능 자체가 사용될 가능성이 없습니다.</p>

<table>
  <thead>
    <tr>
      <th>시점</th>
      <th>필요 여부</th>
      <th>이유</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>이미지 빌드 시</td>
      <td>O</td>
      <td>apt 패키지 설치 시 저장소 서명 검증</td>
    </tr>
    <tr>
      <td>런타임 (서비스 실행)</td>
      <td>X</td>
      <td>패키지 추가 설치 없음, 서명 검증 불필요</td>
    </tr>
    <tr>
      <td>폐쇄망 환경</td>
      <td>X</td>
      <td>외부 저장소 접근 자체가 불가</td>
    </tr>
  </tbody>
</table>

<h4 id="제거-전-확인-방법">제거 전 확인 방법</h4>

<p>패키지를 제거하기 전에 해당 패키지에 의존하는 다른 패키지가 있는지 확인합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 역의존성 확인: 이 패키지를 필요로 하는 다른 패키지 목록</span>
apt-cache rdepends <span class="nt">--installed</span> &lt;패키지명&gt;
</code></pre></div></div>

<p>역의존성 목록에 서비스 실행에 필요한 패키지가 포함되어 있다면 제거하면 안 됩니다.</p>

<blockquote>
  <p>위는 자주 보고되는 예시이며, 실제 제거 대상은 Trivy 스캔 결과에 따라 다름<br />
핵심 판단 기준은 “이 패키지가 빌드 시점에만 필요한가, 런타임에도 필요한가”<br />
빌드 전용 패키지는 제거해도 서비스에 영향이 없음</p>
</blockquote>

<hr />

<h3 id="4-python-패키지-업데이트">4. Python 패키지 업데이트</h3>

<p>사전에 준비한 <code class="language-plaintext highlighter-rouge">.whl</code> 파일로 취약 Python 패키지를 업데이트합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install</span> <span class="nt">--no-index</span> <span class="nt">--find-links</span><span class="o">=</span>/opt/whl <span class="nt">--no-deps</span> &lt;패키지명&gt;<span class="o">==</span>&lt;패치_버전&gt;
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>옵션</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">--no-index</code></td>
      <td>PyPI 접근 차단 (오프라인 환경이므로 필수)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">--find-links</code></td>
      <td>로컬 whl 디렉터리 지정</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">--no-deps</code></td>
      <td>의존성 자동 설치 방지 (기존 환경 보존)</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><code class="language-plaintext highlighter-rouge">--no-deps</code>를 사용하는 이유는 의존성 자동 해결 시<br />
기존에 설치된 다른 패키지와 버전 충돌이 발생할 수 있기 때문<br />
대상 패키지만 정확히 교체하는 것이 안전</p>
</blockquote>

<hr />

<h3 id="5-버전-증빙">5. 버전 증빙</h3>

<p>업데이트된 패키지 버전을 기록합니다.<br />
보안 검수 시 증빙 자료로 제출할 수 있습니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip list | egrep <span class="s1">'&lt;패키지1&gt;|&lt;패키지2&gt;|...'</span> <span class="se">\</span>
  | <span class="nb">tee</span> /tmp/python_versions.txt
</code></pre></div></div>

<hr />

<h2 id="6-trivy-오프라인-스캔">6. Trivy 오프라인 스캔</h2>

<h3 id="1-스캔-실행">1. 스캔 실행</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">TRIVY_SKIP_DB_UPDATE</span><span class="o">=</span><span class="nb">true

</span>trivy fs <span class="se">\</span>
  <span class="nt">--skip-db-update</span> <span class="se">\</span>
  <span class="nt">--scanners</span> vuln <span class="se">\</span>
  <span class="nt">--severity</span> HIGH,CRITICAL <span class="se">\</span>
  <span class="nt">--format</span> table <span class="se">\</span>
  / | <span class="nb">tee</span> /tmp/trivy_scan_result.txt
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>옵션</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">--skip-db-update</code></td>
      <td>DB 업데이트 시도 차단 (오프라인 필수)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">--scanners vuln</code></td>
      <td>취약점 스캐너만 실행</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">--severity HIGH,CRITICAL</code></td>
      <td>HIGH/CRITICAL 등급만 필터링</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">--format table</code></td>
      <td>사람이 읽기 쉬운 테이블 형식</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">trivy fs /</code></td>
      <td>컨테이너 내 전체 파일시스템 스캔</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="2-합격-기준">2. 합격 기준</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Total: 0 (HIGH: 0, CRITICAL: 0)
</code></pre></div></div>

<p>HIGH 또는 CRITICAL 등급의 취약점이 0건이면 합격입니다.</p>

<blockquote>
  <p>MEDIUM 이하는 일반적으로 검수에서 허용되지만<br />
조직 보안 정책에 따라 기준이 다를 수 있음</p>
</blockquote>

<hr />

<h3 id="3-불합격-시-대응">3. 불합격 시 대응</h3>

<p>스캔 결과에 취약점이 남아 있으면 다음을 확인합니다.</p>

<table>
  <thead>
    <tr>
      <th>상황</th>
      <th>조치</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>OS 패키지 잔여</td>
      <td>추가 <code class="language-plaintext highlighter-rouge">apt-get purge</code> 실행</td>
    </tr>
    <tr>
      <td>Python 패키지 잔여</td>
      <td>패치 버전 whl 재반입 후 재설치</td>
    </tr>
    <tr>
      <td>패치 버전 미존재</td>
      <td>해당 CVE에 대한 예외 신청 (보안팀 협의)</td>
    </tr>
  </tbody>
</table>

<p>모든 CVE가 패키지 제거나 업데이트로 해결되는 것은 아닙니다.<br />
패치가 존재하지 않거나, 제거하면 서비스가 깨지거나,<br />
컨테이너 레벨에서 대응할 수 없는 경우가 있습니다.<br />
이 경우 리스크 수용 또는 예외 처리를 보안팀에 요청해야 합니다.</p>

<hr />

<h3 id="4-예외-처리가-필요한-경우">4. 예외 처리가 필요한 경우</h3>

<p>실제 CVE 대응 과정에서는 패키지 제거나 업데이트만으로 해결되지 않는 경우가 빈번합니다.<br />
아래는 실무에서 자주 발생하는 예외 패턴과 대응 논리입니다.</p>

<h4 id="1-패치-버전이-배포되지-않은-경우">1. 패치 버전이 배포되지 않은 경우</h4>

<p>배포판(Debian, Ubuntu 등)에서 해당 CVE를 Minor issue(<code class="language-plaintext highlighter-rouge">&lt;no-dsa&gt;</code>)로 분류하고,<br />
보안 업데이트를 별도로 제공하지 않는 경우입니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[상황]
- Trivy에서 HIGH로 보고되었으나
  배포판 보안팀은 해당 CVE를 Minor issue로 분류
- 저장소에 적용 가능한 패치 버전이 존재하지 않음

[대응 논리]
- 취약점의 악용 조건이 매우 제한적 (예: 공격자가 특정 파라미터를 동시에 제어해야 함)
- 일반적인 런타임 환경에서 실질 악용 가능성이 낮음
- 패치 제공 시 즉시 반영을 전제로 리스크 수용 요청
</code></pre></div></div>

<blockquote>
  <p>Trivy의 심각도 판정과 배포판의 판정이 다를 수 있음<br />
NVD에서는 HIGH로 분류하더라도 Debian/Ubuntu 보안팀이 자체 평가 후<br />
<code class="language-plaintext highlighter-rouge">&lt;no-dsa&gt;</code> (Debian Security Advisory 미발행)로 처리하는 경우가 있음</p>
</blockquote>

<hr />

<h4 id="2-호스트-커널-cve가-컨테이너에서-탐지되는-경우">2. 호스트 커널 CVE가 컨테이너에서 탐지되는 경우</h4>

<p>Linux 커널 관련 CVE가 컨테이너 이미지 스캔에서 탐지되는 경우입니다.<br />
컨테이너는 자체 커널을 포함하지 않고 호스트 시스템의 커널을 공유하므로,<br />
컨테이너 이미지 레벨에서는 근본적인 해결이 불가능합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[상황]
- 커널 소스 코드에 대한 CVE가 컨테이너 이미지 스캔에서 탐지
- 컨테이너 내부에서 패키지를 제거하거나 업데이트해도 효과 없음

[대응 논리]
- 컨테이너는 자체 커널을 포함/사용하지 않음 (호스트 커널 공유)
- 취약점의 영향 여부는 호스트 OS 커널 버전과 보안 패치 상태에 따라 결정
- 컨테이너 이미지가 아닌 호스트 OS 커널 패치 적용 여부를 기준으로 리스크 관리
</code></pre></div></div>

<p>대표적인 예가 <code class="language-plaintext highlighter-rouge">linux-libc-dev</code> 패키지입니다.<br />
이 패키지는 Linux 커널 헤더 파일을 제공하는 개발용 패키지로,<br />
이미지 빌드 시 C extension을 컴파일할 때 설치되는 경우가 많습니다.</p>

<p>Trivy는 이 패키지의 버전을 기준으로 커널 CVE를 매핑하여 보고합니다.<br />
문제는 이 패키지 하나에 매핑되는 커널 CVE가 매우 많다는 것입니다.<br />
일반적인 Docker 이미지를 스캔하면<br />
전체 탐지 건수의 절반 이상이 <code class="language-plaintext highlighter-rouge">linux-libc-dev</code> 기인인 경우가 흔합니다.</p>

<p>다만 <code class="language-plaintext highlighter-rouge">linux-libc-dev</code>는 커널 헤더 파일일 뿐, 실제 커널 코드가 컨테이너에 포함된 것이 아닙니다.<br />
컨테이너는 호스트의 커널을 공유하므로 이 패키지에서 보고되는 CVE는<br />
컨테이너 이미지 레벨에서는 대응할 수도, 대응할 필요도 없습니다.</p>

<p>따라서 이 유형의 CVE는 컨테이너 이미지 검수 범위에서 제외하고,<br />
호스트 OS의 커널 패치 상태를 기준으로 별도 관리하는 것이 일반적입니다.</p>

<blockquote>
  <p><code class="language-plaintext highlighter-rouge">linux-libc-dev</code>를 검수 범위에서 제외하면 전체 CVE 건수가 대폭 줄어듦<br />
보안팀과 사전에 “커널 CVE는 호스트 OS 영역”이라는 합의를 하면<br />
이후 검수에서 반복적으로 같은 논의를 할 필요가 없음</p>
</blockquote>

<hr />

<h4 id="3-라이브러리가-존재하지만-런타임에-로드되지-않는-경우">3. 라이브러리가 존재하지만 런타임에 로드되지 않는 경우</h4>

<p>취약 라이브러리가 이미지에 포함되어 있지만,<br />
실제 서비스 실행 시 해당 라이브러리가 메모리에 로드되지 않는 경우입니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[상황]
- 취약 라이브러리(.so)가 이미지에 존재
- 그러나 서비스 프로세스의 메모리 맵(/proc/&lt;pid&gt;/maps)에서 미검출
- 다른 핵심 패키지가 해당 라이브러리에 의존하여 제거 불가

[확인 방법]
# 컨테이너 내부에서 실행
cat /proc/&lt;서비스_PID&gt;/maps | grep &lt;라이브러리명&gt;
# → 출력 없으면 런타임에 로드되지 않음

# 역의존성 확인
apt-cache rdepends --installed &lt;라이브러리_패키지명&gt;
# → 핵심 패키지가 의존하고 있으면 제거 불가

[대응 논리]
- 라이브러리는 이미지에 포함되어 있으나 런타임에 사용(로드)되지 않음
- 의존성 관계로 인해 패키지 삭제 시 서비스 기능에 영향
- 실제 악용 가능성이 낮으므로 패치 버전 적용 가능 시까지 리스크 수용 요청
</code></pre></div></div>

<blockquote>
  <p>공유 라이브러리(.so)는 다른 패키지가 의존하더라도 실제 실행 시 로드되지 않을 수 있음<br />
<code class="language-plaintext highlighter-rouge">/proc/&lt;pid&gt;/maps</code>로 런타임 로드 여부를 확인하면 실질적인 영향 범위를 입증할 수 있음</p>
</blockquote>

<hr />

<h4 id="4-패치-완료되었으나-스캐너가-구버전으로-인식하는-경우">4. 패치 완료되었으나 스캐너가 구버전으로 인식하는 경우</h4>

<p>실제로는 패치된 버전의 파일을 사용하고 있지만,<br />
호환성을 위해 구버전 이름의 심볼릭 링크를 유지해 스캐너가 취약 버전으로 탐지하는 경우입니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[상황]
- 취약 버전의 라이브러리/JAR 파일은 이미 삭제
- 패치된 버전의 파일만 존재
- 그러나 애플리케이션 호환성을 위해 구버전 이름의 심볼릭 링크를 유지
- Trivy가 심볼릭 링크의 파일명(구버전)을 기준으로 취약 판정

[확인 방법]
# 실제 파일 확인
ls -la /path/to/&lt;라이브러리&gt;*
# → 심볼릭 링크가 패치된 버전의 실제 파일을 가리키는지 확인

[대응 논리]
- 실제 파일시스템에는 패치된 버전만 존재
- 취약 버전의 파일은 포함되어 있지 않음
- 심볼릭 링크는 이름만 구버전이며, 참조하는 실제 파일은 패치 완료
</code></pre></div></div>

<hr />

<h4 id="5-구버전-베이스-이미지로-인한-대량-cve">5. 구버전 베이스 이미지로 인한 대량 CVE</h4>

<p>베이스 이미지 자체가 오래된 OS 버전(예: Debian 11)으로 빌드되어<br />
다수의 CVE가 동시에 보고되며, 개별 패치로는 대응이 불가능한 경우입니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[상황]
- 구버전 OS 기반 이미지에서 수십~수백 건의 CVE 탐지
- 대부분의 패키지에 Fixed version이 배포되지 않음
- 개별 패키지 업데이트로는 해결 불가

[대응]
- 최신 OS(Debian 12/13 등)로 빌드된 새 이미지를 반입
- 주요 라이브러리 버전 업데이트에 따른 애플리케이션 호환성 검증 필요
- 이미지 재빌드 후 다시 스캔하여 잔여 CVE 확인
</code></pre></div></div>

<blockquote>
  <p>구버전 이미지는 개별 CVE 패치보다 이미지 재빌드가 효율적<br />
다만 라이브러리 메이저 버전이 변경되면 애플리케이션 코드 수정이 필요할 수 있으므로<br />
개발팀과의 사전 호환성 검증이 필수</p>
</blockquote>

<hr />

<h4 id="예외-요청-시-포함할-항목">예외 요청 시 포함할 항목</h4>

<p>예외 처리를 요청할 때는 다음 항목을 정리하여 보안팀에 전달합니다.</p>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>내용</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>CVE ID</td>
      <td><code class="language-plaintext highlighter-rouge">CVE-XXXX-XXXXX</code></td>
    </tr>
    <tr>
      <td>심각도</td>
      <td>CVSS 점수 및 등급 (HIGH / CRITICAL)</td>
    </tr>
    <tr>
      <td>영향 패키지</td>
      <td>패키지명 및 현재 설치 버전</td>
    </tr>
    <tr>
      <td>배포판 분류</td>
      <td>배포판 보안팀의 자체 분류 (예: <code class="language-plaintext highlighter-rouge">&lt;no-dsa&gt;</code>, Minor issue)</td>
    </tr>
    <tr>
      <td>악용 조건</td>
      <td>취약점이 실제로 악용되려면 필요한 조건</td>
    </tr>
    <tr>
      <td>런타임 영향</td>
      <td>서비스 실행 시 해당 코드 경로의 사용 여부</td>
    </tr>
    <tr>
      <td>제거 불가 사유</td>
      <td>의존성, 호환성 등 패치/제거가 불가능한 이유</td>
    </tr>
    <tr>
      <td>대응 계획</td>
      <td>패치 제공 시 즉시 반영, 이미지 재빌드 예정 등</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>“패치가 없다”로 끝내는 것이 아니라<br />
“왜 현 시점에서 리스크가 낮은지”를 기술적으로 입증하는 것이 핵심<br />
런타임 로드 여부, 악용 조건의 제한성, 배포판의 자체 판단 등을 근거로 제시</p>
</blockquote>

<hr />

<h2 id="7-이미지-확정-및-배포용-빌드">7. 이미지 확정 및 배포용 빌드</h2>

<h3 id="1-작업용-이미지-커밋">1. 작업용 이미지 커밋</h3>

<p>스캔을 통과한 작업용 컨테이너를 이미지로 고정합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 컨테이너에서 exit</span>
<span class="nb">exit</span>

<span class="c"># 이미지 커밋</span>
docker commit cve-work &lt;IMAGE&gt;:&lt;TAG&gt;-hardened
</code></pre></div></div>

<blockquote>
  <p><code class="language-plaintext highlighter-rouge">docker commit</code>은 현재 컨테이너 상태를 그대로 이미지로 저장<br />
이 시점의 이미지에는 CVE가 제거된 상태가 반영되어 있음</p>
</blockquote>

<hr />

<h3 id="2-배포용-이미지-빌드">2. 배포용 이미지 빌드</h3>

<p>작업용 이미지는 ENTRYPOINT가 <code class="language-plaintext highlighter-rouge">/bin/bash</code>로 되어 있으므로<br />
서비스 실행을 위해 원래 ENTRYPOINT를 복구해야 합니다.</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> &lt;IMAGE&gt;:&lt;TAG&gt;-hardened</span>
<span class="k">ENTRYPOINT</span><span class="s"> ["&lt;원본_ENTRYPOINT&gt;"]</span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker build <span class="nt">-t</span> &lt;IMAGE&gt;:&lt;TAG&gt;-hardened-release <span class="nb">.</span>
</code></pre></div></div>

<p>원본 이미지의 ENTRYPOINT는 다음 명령으로 확인할 수 있습니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker inspect <span class="nt">--format</span><span class="o">=</span><span class="s1">''</span> &lt;IMAGE&gt;:&lt;TAG&gt;
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>이미지 태그</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">&lt;TAG&gt;</code></td>
      <td>원본 이미지 (CVE 포함)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">&lt;TAG&gt;-work</code></td>
      <td>작업용 태그</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">&lt;TAG&gt;-hardened</code></td>
      <td>CVE 제거 완료, bash ENTRYPOINT</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">&lt;TAG&gt;-hardened-release</code></td>
      <td>CVE 제거 + 서비스 ENTRYPOINT 복구</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="8-서비스-실행">8. 서비스 실행</h2>

<p>runtime 이미지로 서비스를 실행합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker <span class="nb">rm</span> <span class="nt">-f</span> &lt;CONTAINER_NAME&gt; 2&gt;/dev/null <span class="o">||</span> <span class="nb">true

</span>docker run <span class="nt">-d</span> <span class="se">\</span>
  <span class="nt">--name</span> &lt;CONTAINER_NAME&gt; <span class="se">\</span>
  <span class="nt">--gpus</span> <span class="s1">'"device=0"'</span> <span class="se">\</span>
  <span class="nt">-p</span> &lt;HOST_PORT&gt;:&lt;CONTAINER_PORT&gt; <span class="se">\</span>
  <span class="nt">-v</span> /path/to/data:/data <span class="se">\</span>
  &lt;IMAGE&gt;:&lt;TAG&gt;-hardened-release
</code></pre></div></div>

<blockquote>
  <p>GPU 옵션, 포트, 볼륨 마운트 등은 서비스에 맞게 조정</p>
</blockquote>

<hr />

<h2 id="9-최종-검증">9. 최종 검증</h2>

<h3 id="1-서비스-정상-동작-확인">1. 서비스 정상 동작 확인</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 컨테이너 로그</span>
docker logs <span class="nt">-f</span> &lt;CONTAINER_NAME&gt;

<span class="c"># GPU 할당 확인 (GPU 사용 시)</span>
docker <span class="nb">exec</span> <span class="nt">-it</span> &lt;CONTAINER_NAME&gt; nvidia-smi

<span class="c"># API 응답 확인 (API 서버인 경우)</span>
curl http://localhost:&lt;HOST_PORT&gt;/health
</code></pre></div></div>

<p>CVE 패치 과정에서 Python 패키지 버전이 변경되면<br />
deprecated된 함수나 변경된 API로 인해 기존 코드가 동작하지 않을 수 있습니다.<br />
컨테이너 기동 확인 외에 주요 기능에 대한 호환성 검증도 필요합니다.</p>

<table>
  <thead>
    <tr>
      <th>확인 항목</th>
      <th>방법</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>import 오류</td>
      <td>컨테이너 내부에서 <code class="language-plaintext highlighter-rouge">python -c "import &lt;패키지명&gt;"</code> 실행</td>
    </tr>
    <tr>
      <td>기능 테스트</td>
      <td>핵심 API 엔드포인트 호출 및 응답 확인</td>
    </tr>
    <tr>
      <td>로그 확인</td>
      <td>DeprecationWarning, AttributeError 등 경고/에러 유무</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><code class="language-plaintext highlighter-rouge">--no-deps</code>로 대상 패키지만 교체하므로 마이너 패치에서는 문제가 드물지만<br />
메이저/마이너 버전이 변경된 경우 함수 시그니처나 모듈 구조가 달라질 수 있으므로<br />
패치 전후 changelog를 확인하는 것을 권장</p>
</blockquote>

<hr />

<h3 id="2-cve-스캔-결과-추출">2. CVE 스캔 결과 추출</h3>

<p>보안 검수 제출용 증빙 파일을 추출합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 작업용 컨테이너에서 저장한 파일 추출</span>
docker <span class="nb">cp </span>cve-work:/tmp/trivy_scan_result.txt ./
docker <span class="nb">cp </span>cve-work:/tmp/python_versions.txt ./
</code></pre></div></div>

<p>제출 증빙 목록:</p>

<table>
  <thead>
    <tr>
      <th>파일</th>
      <th>내용</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">trivy_scan_result.txt</code></td>
      <td>Trivy 스캔 결과 (HIGH/CRITICAL = 0)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">python_versions.txt</code></td>
      <td>패치 완료된 Python 패키지 버전 목록</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="마무리">마무리</h2>

<p>이 글에서 다룬 CVE 대응 절차를 정리하면 다음과 같습니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. 외부망: Trivy 바이너리 + DB + Python whl 준비
    ↓
2. 폐쇄망: 작업용 컨테이너 생성 (bash ENTRYPOINT)
    ↓
3. 컨테이너 내부: OS 패키지 제거 + Python 패키지 업데이트
    ↓
4. Trivy 오프라인 스캔 (HIGH/CRITICAL = 0 확인)
    ↓
5. 이미지 커밋 → 배포용 이미지 빌드 (ENTRYPOINT 복구)
    ↓
6. 서비스 실행 및 최종 검증
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>단계</th>
      <th>핵심</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>외부망 준비</td>
      <td>스캐너, DB, 패치 파일을 1회 준비하여 반입</td>
    </tr>
    <tr>
      <td>작업용/배포용 분리</td>
      <td>작업용과 실행용 이미지를 분리하여 ENTRYPOINT 충돌 방지</td>
    </tr>
    <tr>
      <td>오프라인 스캔</td>
      <td>Trivy DB를 수동 반입하여 인터넷 없이 스캔</td>
    </tr>
    <tr>
      <td>증빙 관리</td>
      <td>스캔 결과와 패키지 버전을 파일로 저장하여 검수 대응</td>
    </tr>
  </tbody>
</table>

<p>폐쇄망에서는 CVE 하나를 수정하는 것도<br />
“whl 다운로드 → 물리 매체 반입 → 설치 → 재스캔”의 사이클을 거쳐야 합니다.<br />
외부망에서는 한 줄이면 끝나는 작업이 폐쇄망에서는 하루가 걸릴 수 있습니다.</p>

<p>사전에 어떤 CVE가 보고되는지 파악하고,<br />
필요한 패치 파일을 한 번에 준비하여 반입하는 것이 재작업을 줄이는 핵심입니다.</p>]]></content><author><name>indexkim</name></author><category term="docker" /><category term="infra" /><category term="security" /><summary type="html"><![CDATA[CVE(Common Vulnerabilities and Exposures)는 소프트웨어·하드웨어에서 발견된 보안 취약점을 고유하게 식별하기 위해 부여되는 표준 ID 체계입니다. Docker 이미지에 포함된 OS 패키지나 Python 패키지 중 알려진 CVE가 존재하면 보안 검수에서 반려될 수 있으며, 배포 전에 이를 식별하고 제거해야 합니다.]]></summary></entry><entry><title type="html">폐쇄망 AI Solution 서비스 구성 및 배포</title><link href="https://indexkim.github.io//deploying-air-gapped-ai-solution-services/" rel="alternate" type="text/html" title="폐쇄망 AI Solution 서비스 구성 및 배포" /><published>2026-02-12T00:00:00+09:00</published><updated>2026-02-12T00:00:00+09:00</updated><id>https://indexkim.github.io//deploying-air-gapped-ai-solution-services</id><content type="html" xml:base="https://indexkim.github.io//deploying-air-gapped-ai-solution-services/"><![CDATA[<p>이번 글에서는 폐쇄망 AI Solution 개발환경 구축에 이어,<br />
실제 서비스 구성과 배포 과정에 대해 정리합니다.<br />
RHEL 9을 기준으로 하지만, 다른 Linux 배포판에서도 동일한 방식으로 적용할 수 있습니다.</p>

<blockquote>
  <p>특정 조직의 내부 시스템이나 정책, 네트워크 설정은 포함하지 않으며<br />
모든 내용은 공개된 기술과 일반적인 Linux 환경을 기반으로 구성되었습니다.</p>
</blockquote>

<hr />

<h2 id="15-파인튜닝">15. 파인튜닝</h2>

<p>본 프로젝트에서는 VLM(Vision-Language Model) 기반으로 OCR 파인튜닝을 수행했습니다.<br />
기존 PaddleOCR 기반 파인튜닝은 <a href="/ocr-fine-tuning/">PaddleOCR 기반 도메인 특화 OCR fine-tuning</a>에서 다뤘으며,<br />
이번에는 VLM 기반 OCR 방식을 적용했습니다.</p>

<hr />

<h3 id="vlmvision-language-model이란">VLM(Vision-Language Model)이란?</h3>

<p>VLM은 이미지와 텍스트를 함께 처리할 수 있는 멀티모달 모델입니다.<br />
GPT 계열, Gemini 계열, Qwen-VL과 같은 모델들이 대표적인 VLM에 해당합니다.<br />
다만 GPT 및 Gemini와 같은 상용 멀티모달 모델은<br />
내부 Vision–Language 결합 구조가 공개되어 있지 않습니다.<br />
아래 구조는 주로 오픈소스 VLM에서 일반적으로 사용되는 아키텍처입니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌─────────────────┐     ┌──────────────┐     ┌─────────────────┐
│ Vision Encoder  │────▶│  Projector   │────▶│ Language Model  │
│ (이미지 인코더)  │     │ (모달리티 정렬) │     │  (텍스트 생성)   │
└─────────────────┘     └──────────────┘     └─────────────────┘
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>컴포넌트</th>
      <th>역할</th>
      <th>대표 예시</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Vision Encoder</td>
      <td>이미지를 feature vector로 인코딩</td>
      <td>CLIP ViT, SigLIP, EVA-CLIP</td>
    </tr>
    <tr>
      <td>Projector</td>
      <td>이미지 feature를 LLM embedding space로 변환</td>
      <td>Linear/MLP projection</td>
    </tr>
    <tr>
      <td>Language Model</td>
      <td>이미지 정보와 텍스트를 기반으로 응답 생성</td>
      <td>Qwen2, LLaMA, Mistral</td>
    </tr>
  </tbody>
</table>

<p>VLM에 “이 이미지의 텍스트를 읽어줘”라고 프롬프트를 주면<br />
이미지 속 텍스트를 인식하여 자연어로 출력합니다.<br />
이처럼 텍스트 인식을 LLM 생성 과정으로 수행하는 방식을 VLM OCR이라 합니다.</p>

<hr />

<h3 id="ocr-전용-모델-vs-vlm-ocr">OCR 전용 모델 vs VLM OCR</h3>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>OCR 전용 모델 (PaddleOCR, EasyOCR)</th>
      <th>VLM OCR</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>파이프라인</td>
      <td>Detection → Recognition (2단계)</td>
      <td>이미지 → 텍스트 생성 (End-to-End)</td>
    </tr>
    <tr>
      <td>출력 형태</td>
      <td>좌표(bbox) + 텍스트</td>
      <td>텍스트 (좌표는 별도 설계 필요)</td>
    </tr>
    <tr>
      <td>모델 크기</td>
      <td>수십 MB ~ 수백 MB</td>
      <td>수 GB 이상</td>
    </tr>
    <tr>
      <td>학습 데이터</td>
      <td>Detection/Recognition 각각 별도 라벨 필요</td>
      <td>이미지-텍스트 쌍 기반 학습</td>
    </tr>
    <tr>
      <td>파인튜닝</td>
      <td>모델별 전용 학습 코드 필요</td>
      <td>LoRA 등 LLM 기반 범용 기법 활용</td>
    </tr>
    <tr>
      <td>복잡한 레이아웃</td>
      <td>좌표 기반 정밀 제어에 강점</td>
      <td>문맥 기반 보완 가능</td>
    </tr>
    <tr>
      <td>추론 속도</td>
      <td>빠름</td>
      <td>상대적으로 느림 (생성 기반)</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>OCR 전용 모델은 좌표 기반 정밀 탐지와 빠른 추론에 강점이 있고<br />
VLM OCR은 문맥 이해 기반의 유연한 텍스트 인식에 강점이 있음<br />
두 방식은 대체 관계가 아니라 용도에 따라 선택하는 보완 관계</p>
</blockquote>

<hr />

<h3 id="1-데이터셋-구축-모델-보조-라벨링">1. 데이터셋 구축: 모델 보조 라벨링</h3>

<p>학습 데이터는 영상에서 자막이 표시되는 구간을 캡처하여 구축했습니다.<br />
사전학습된 베이스 VLM의 추론 결과를 초안 라벨로 활용하고,<br />
사람이 오류를 수정한 뒤 해당 데이터로 파인튜닝을 진행했습니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>영상에서 자막 구간 캡처 (이미지 추출)
        ↓
베이스 VLM으로 인퍼런스 (초안 라벨 생성)
        ↓
사람이 오류 수정 (Human-in-the-Loop 검수)
        ↓
수정된 데이터로 파인튜닝 학습
</code></pre></div></div>

<blockquote>
  <p>해당 방식은 Model-Assisted Labeling에 해당하며<br />
모델 예측을 사람이 검수·보정하여 최종 정답을 확정하는 Human-in-the-Loop 기반 전략임<br />
모델 예측을 그대로 학습에 사용하는 Pseudo Labeling과는 구분됨<br />
데이터셋 규모가 작을수록 라벨 품질이 성능에 미치는 영향이 큼</p>
</blockquote>

<hr />

<h3 id="2-lora-파인튜닝">2. LoRA 파인튜닝</h3>

<p>LoRA(Low-Rank Adaptation)는 대규모 모델의 원본 가중치를 고정한 채<br />
저랭크 어댑터 행렬만 추가 학습하는 기법입니다.<br />
전체 파라미터 대비 소수(보통 1% 내외)만 학습하므로 메모리 효율이 높지만,<br />
데이터가 매우 적은 경우에는 여전히 과적합이 발생할 수 있습니다.</p>

<p>VLM 파인튜닝 시에는 Vision Encoder와 Projector를 동결(Freeze)하고<br />
Language Model에만 LoRA를 적용하는 것이 일반적입니다.<br />
다만 도메인 특성이 강한 경우 일부 레이어를 추가 학습하기도 합니다.</p>

<table>
  <thead>
    <tr>
      <th>컴포넌트</th>
      <th>학습 여부</th>
      <th>이유</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Vision Encoder</td>
      <td>Freeze</td>
      <td>사전학습된 이미지 특징 보존</td>
    </tr>
    <tr>
      <td>Projector</td>
      <td>Freeze</td>
      <td>일반적으로 동결, 도메인 갭이 큰 경우만 학습</td>
    </tr>
    <tr>
      <td>Language Model</td>
      <td>LoRA 적용</td>
      <td>태스크 특화 텍스트 생성 학습</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>LoRA는 어댑터 가중치만 저장하므로 용량이 수십 MB 수준으로 매우 작음<br />
데이터가 매우 적은 경우 상위 레이어에만 LoRA를 적용하는 방식도 고려할 수 있음</p>
</blockquote>

<hr />

<h3 id="3-평가">3. 평가</h3>

<p>OCR 성능 평가에는 CER(Character Error Rate, 문자 오류율)을 사용합니다.<br />
CER은 모델이 출력한 텍스트와 정답 텍스트 사이의 편집 거리를 문자 단위로 측정하며,<br />
단어 단위 지표(WER)보다 OCR 태스크에 적합합니다.<br />
평가 시에는 공백 및 특수문자 처리 기준을 사전에 정의하여 일관된 방식으로 CER을 계산했습니다.</p>

<p>파인튜닝 후 베이스 모델 대비 CER이 개선되었으며,<br />
소규모 데이터셋임에도 Model-Assisted Labeling 방식의 효과를 확인할 수 있었습니다.</p>

<hr />

<h3 id="4-가중치-관리">4. 가중치 관리</h3>

<p>학습 완료 시 어댑터 파일(<code class="language-plaintext highlighter-rouge">adapter_config.json</code>, <code class="language-plaintext highlighter-rouge">adapter_model.safetensors</code>) 2개만 저장됩니다.<br />
전체 모델을 다시 가져올 필요 없이 어댑터 파일만 교체하면 됩니다.</p>

<blockquote>
  <p>여러 버전의 어댑터를 관리하면 태스크별 모델 전환이 가능<br />
다만 런타임 전환 가능 여부는 사용하는 서빙 프레임워크에 따라 달라짐</p>
</blockquote>

<hr />

<h2 id="16-모델-서빙">16. 모델 서빙</h2>

<p>모델 서빙은 학습이 완료된 AI 모델을 실시간으로 호출할 수 있는 상태로 배포하는 과정입니다.<br />
학습된 가중치 파일(.pt, .safetensors, .trt 등)을 메모리에 로드하고,<br />
외부 요청에 대해 추론 결과를 반환하는 API 형태로 운영합니다.</p>

<p>모델 서빙은 크게 두 가지 요소로 구성됩니다.</p>

<table>
  <thead>
    <tr>
      <th>구분</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>추론 엔진</td>
      <td>모델을 실행하는 런타임 (TensorRT, vLLM, SGLang, Transformers 등)</td>
    </tr>
    <tr>
      <td>서빙 레이어</td>
      <td>추론 결과를 API로 제공하는 웹 서버 (FastAPI, Flask, Triton 등)</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="1-hugging-face-transformers">1. Hugging Face Transformers</h3>

<p>Transformers는 Hugging Face에서 개발한 오픈소스 라이브러리로,<br />
다양한 pretrained 모델(LLM, VLM, Vision 등)을<br />
로드하고 추론·학습할 수 있는 통합 프레임워크입니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">transformers</span> <span class="kn">import</span> <span class="n">AutoModelForCausalLM</span><span class="p">,</span> <span class="n">AutoProcessor</span>

<span class="n">model</span> <span class="o">=</span> <span class="n">AutoModelForCausalLM</span><span class="p">.</span><span class="n">from_pretrained</span><span class="p">(</span><span class="s">"model_path"</span><span class="p">,</span> <span class="n">trust_remote_code</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="n">processor</span> <span class="o">=</span> <span class="n">AutoProcessor</span><span class="p">.</span><span class="n">from_pretrained</span><span class="p">(</span><span class="s">"model_path"</span><span class="p">,</span> <span class="n">trust_remote_code</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>역할</td>
      <td>모델 로드, 토크나이징, 추론, 학습을 위한 기본 라이브러리</td>
    </tr>
    <tr>
      <td>지원 모델</td>
      <td>Hugging Face Hub에 등록된 수만 개의 모델</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">trust_remote_code</code></td>
      <td>모델 폴더 내 커스텀 Python 코드(*.py)를 직접 실행하여 비표준 아키텍처도 로드 가능</td>
    </tr>
    <tr>
      <td>위치</td>
      <td>추론 엔진(vLLM 등)이나 서빙 프레임워크(FastAPI 등)의 기반 레이어</td>
    </tr>
  </tbody>
</table>

<p>vLLM, SGLang 등은 Hugging Face 모델 포맷과 토크나이저를 호환하여 로드합니다.<br />
다만 Transformers의 기본 추론 API는 대규모 서빙 최적화를 제공하지 않으므로,<br />
고부하 프로덕션 환경에서는 전용 추론 엔진을 사용하는 것이 일반적입니다.</p>

<hr />

<h3 id="2-추론-엔진-비교">2. 추론 엔진 비교</h3>

<h4 id="1-tensorrt">(1) TensorRT</h4>

<p>NVIDIA의 GPU 최적화 추론 엔진입니다.<br />
모델을 <code class="language-plaintext highlighter-rouge">.trt</code> 엔진 파일로 변환하여 Layer Fusion, Kernel Auto-Tune, Quantization 등<br />
하드웨어 수준의 최적화를 적용합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># ONNX → TensorRT 변환 예시</span>
trtexec <span class="nt">--onnx</span><span class="o">=</span>model.onnx <span class="nt">--saveEngine</span><span class="o">=</span>model.trt <span class="nt">--fp16</span>
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>대상 모델</td>
      <td>YOLO, ResNet 등 고정 구조 모델</td>
    </tr>
    <tr>
      <td>장점</td>
      <td>최고 수준의 추론 속도, FP16/INT8 경량화 내장</td>
    </tr>
    <tr>
      <td>단점</td>
      <td>GPU 아키텍처에 종속 (Ampere에서 생성한 엔진은 Blackwell에서 사용 불가)</td>
    </tr>
    <tr>
      <td>VLM 지원</td>
      <td>제한적 (커스텀 아키텍처는 변환 불가능한 경우 많음)</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>TensorRT 엔진은 생성 시점의 GPU 아키텍처에 종속되므로<br />
폐쇄망 반입 시 현장 GPU와 동일한 환경에서 변환해야 함</p>
</blockquote>

<hr />

<h4 id="2-vllm">(2) vLLM</h4>

<p>LLM/VLM 전용 고속 추론 엔진입니다.<br />
PagedAttention, Continuous Batching 등 LLM에 특화된 최적화를 제공하며<br />
OpenAI 호환 API를 기본으로 지원합니다.</p>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>대상 모델</td>
      <td>LLM, VLM (Hugging Face 호환 모델)</td>
    </tr>
    <tr>
      <td>장점</td>
      <td>높은 처리량, Continuous Batching, OpenAI 호환 API 기본 제공</td>
    </tr>
    <tr>
      <td>제약</td>
      <td>공식 지원 모델 목록 외 아키텍처는 추가 구현 필요</td>
    </tr>
    <tr>
      <td>폐쇄망</td>
      <td>CUDA 버전 의존성 있음, GPU 아키텍처별 이미지 구분 필요</td>
    </tr>
  </tbody>
</table>

<hr />

<h4 id="3-sglang">(3) SGLang</h4>

<p>SGLang은 LLM/VLM 추론 및 프롬프트 프로그래밍을 위한 프레임워크입니다.<br />
RadixAttention 기반의 KV Cache 재사용 구조를 사용하여<br />
동일 prefix를 공유하는 반복 프롬프트 처리에 강점이 있습니다.</p>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>대상 모델</td>
      <td>LLM, VLM (Hugging Face 호환 모델)</td>
    </tr>
    <tr>
      <td>장점</td>
      <td>KV Cache 재사용, 구조화된 출력 지원</td>
    </tr>
    <tr>
      <td>제약</td>
      <td>공식 지원 외 아키텍처는 추가 구현 필요</td>
    </tr>
    <tr>
      <td>폐쇄망</td>
      <td>Blackwell에서는 dev 이미지 필요, Ampere/Hopper는 안정 릴리즈 사용 가능</td>
    </tr>
  </tbody>
</table>

<hr />

<h4 id="vllm--sglang-docker-배포">vLLM / SGLang Docker 배포</h4>

<p>vLLM과 SGLang은 공식 Docker 이미지를 사용하여 컨테이너로 실행합니다.</p>

<h5 id="vllm">vLLM</h5>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">-itd</span> <span class="se">\</span>
    <span class="nt">--name</span> vllm_server <span class="se">\</span>
    <span class="nt">--gpus</span> <span class="s1">'"device=0"'</span> <span class="se">\</span>
    <span class="nt">--ipc</span><span class="o">=</span>host <span class="se">\</span>
    <span class="nt">-p</span> &lt;PORT&gt;:&lt;PORT&gt; <span class="se">\</span>
    <span class="nt">-v</span> /path/to/models:/models <span class="se">\</span>
    vllm/vllm-openai:v0.11.0 <span class="se">\</span>
    <span class="nt">--model</span> /models/VLM-7B <span class="se">\</span>
    <span class="nt">--host</span> 0.0.0.0 <span class="se">\</span>
    <span class="nt">--port</span> &lt;PORT&gt; <span class="se">\</span>
    <span class="nt">--tensor-parallel-size</span> 1 <span class="se">\</span>
    <span class="nt">--gpu-memory-utilization</span> 0.7 <span class="se">\</span>
    <span class="nt">--max-model-len</span> 8192
</code></pre></div></div>

<h5 id="sglang">SGLang</h5>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">-itd</span> <span class="se">\</span>
    <span class="nt">--name</span> sglang_server <span class="se">\</span>
    <span class="nt">--gpus</span> <span class="s1">'"device=0"'</span> <span class="se">\</span>
    <span class="nt">--shm-size</span> 32g <span class="se">\</span>
    <span class="nt">-p</span> &lt;PORT&gt;:&lt;PORT&gt; <span class="se">\</span>
    <span class="nt">-v</span> /path/to/models:/models <span class="se">\</span>
    <span class="nt">--ipc</span><span class="o">=</span>host <span class="se">\</span>
    lmsysorg/sglang:v0.5.4 <span class="se">\</span>
    python3 <span class="nt">-m</span> sglang.launch_server <span class="se">\</span>
    <span class="nt">--model-path</span> /models/model_name <span class="se">\</span>
    <span class="nt">--host</span> 0.0.0.0 <span class="nt">--port</span> &lt;PORT&gt; <span class="se">\</span>
    <span class="nt">--mem-fraction-static</span> 0.8 <span class="se">\</span>
    <span class="nt">--context-length</span> 70000
</code></pre></div></div>

<h5 id="정상-실행-확인">정상 실행 확인</h5>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 컨테이너 로그 확인</span>
docker logs <span class="nt">-f</span> vllm_server
<span class="c"># → "Application startup complete" 메시지 확인</span>

<span class="c"># API 응답 확인</span>
curl http://localhost:&lt;PORT&gt;/v1/models
<span class="c"># → 200 OK</span>
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>주요 옵션</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">--tensor-parallel-size</code></td>
      <td>모델을 분할할 GPU 수 (30B 이상 모델은 2+ 권장)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">--gpu-memory-utilization</code></td>
      <td>GPU 메모리 사용 비율 (0.6~0.9, 멀티 모델 시 낮게 설정)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">--max-model-len</code></td>
      <td>최대 컨텍스트 길이 (메모리에 직접 영향)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">--mem-fraction-static</code></td>
      <td>SGLang 전용, KV Cache에 할당할 메모리 비율</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>폐쇄망에서는 Docker 이미지를 tar로 반입해야 하므로<br />
<code class="language-plaintext highlighter-rouge">docker save</code> / <code class="language-plaintext highlighter-rouge">docker load</code>로 이미지를 준비함 (<a href="/building-air-gapped-ai-solution-dev-env-2/#12-docker-설치-및-gpu-연동">폐쇄망 AI Solution 개발환경 구축 (2)</a> 참고)</p>
</blockquote>

<hr />

<h4 id="참고-blackwell-gpu-환경에서의-차이">참고: Blackwell GPU 환경에서의 차이</h4>

<p>Blackwell(B200, B300 등) 환경에서는 CUDA 13.0 이상이 필요하며,<br />
작성 시점 기준으로 안정 릴리즈 대신 nightly/dev 이미지를 사용해야 할 수 있습니다.</p>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>Ampere / Hopper</th>
      <th>Blackwell</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>vLLM 이미지</td>
      <td><code class="language-plaintext highlighter-rouge">v0.11.0</code> (안정)</td>
      <td><code class="language-plaintext highlighter-rouge">cu130-nightly-x86_64</code> (nightly)</td>
    </tr>
    <tr>
      <td>SGLang 이미지</td>
      <td><code class="language-plaintext highlighter-rouge">v0.5.4</code> (안정)</td>
      <td><code class="language-plaintext highlighter-rouge">dev-cu13</code> (dev)</td>
    </tr>
    <tr>
      <td>vLLM 추가 설정</td>
      <td>없음</td>
      <td>CUDA 13.0 ptxas 바이너리 주입 + <code class="language-plaintext highlighter-rouge">TRITON_PTXAS_PATH</code> 필요</td>
    </tr>
    <tr>
      <td>SGLang 추가 설정</td>
      <td>없음</td>
      <td>이미지 변경만으로 동작</td>
    </tr>
  </tbody>
</table>

<p>vLLM은 Blackwell에서 Triton 커널 컴파일 시<br />
<code class="language-plaintext highlighter-rouge">ptxas fatal: Value 'sm_103a' is not defined</code> 오류가 발생합니다.<br />
CUDA 13.0 툴킷의 ptxas 바이너리를 추출하여<br />
<code class="language-plaintext highlighter-rouge">-v ptxas:/opt/ptxas:ro -e TRITON_PTXAS_PATH=/opt/ptxas</code>로 주입하면 해결됩니다.</p>

<blockquote>
  <p>SGLang은 이미지 태그만 변경하면 Blackwell에서 동작<br />
vLLM은 이미지 교체 + ptxas 주입이 모두 필요</p>
</blockquote>

<hr />

<h4 id="4-transformers--fastapi-직접-구현">(4) Transformers + FastAPI (직접 구현)</h4>

<p>Hugging Face Transformers로 모델을 직접 로드하고<br />
FastAPI로 API 서버를 구성하는 방식입니다.<br />
대부분의 Hugging Face 호환 모델에서 사용할 수 있습니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># FastAPI 서버 실행</span>
uvicorn app.main:app <span class="nt">--host</span> 0.0.0.0 <span class="nt">--port</span> &lt;PORT&gt;
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>대상 모델</td>
      <td>모든 Hugging Face 모델 (커스텀 아키텍처 포함)</td>
    </tr>
    <tr>
      <td>장점</td>
      <td>어떤 모델이든 서빙 가능, API 구조를 자유롭게 설계 가능</td>
    </tr>
    <tr>
      <td>단점</td>
      <td>Continuous Batching 미지원, 최적화를 직접 구현해야 함</td>
    </tr>
    <tr>
      <td>폐쇄망</td>
      <td>의존성이 적어 반입이 비교적 간편</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="3-서빙-방식-비교">3. 서빙 방식 비교</h3>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>TensorRT</th>
      <th>vLLM</th>
      <th>SGLang</th>
      <th>Transformers + FastAPI</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>대상</td>
      <td>Vision 모델 (YOLO 등)</td>
      <td>LLM / VLM</td>
      <td>LLM / VLM</td>
      <td>모든 모델</td>
    </tr>
    <tr>
      <td>속도</td>
      <td>최고</td>
      <td>높음</td>
      <td>높음</td>
      <td>보통</td>
    </tr>
    <tr>
      <td>Continuous Batching</td>
      <td>X</td>
      <td>O</td>
      <td>O</td>
      <td>X</td>
    </tr>
    <tr>
      <td>OpenAI 호환 API</td>
      <td>X</td>
      <td>O (기본)</td>
      <td>O (기본)</td>
      <td>직접 구현</td>
    </tr>
    <tr>
      <td>커스텀 아키텍처</td>
      <td>변환 가능 시</td>
      <td>지원 목록 한정</td>
      <td>지원 목록 한정</td>
      <td>모든 모델 가능</td>
    </tr>
    <tr>
      <td>LoRA 어댑터 전환</td>
      <td>X</td>
      <td>O</td>
      <td>O</td>
      <td>직접 구현 가능</td>
    </tr>
    <tr>
      <td>폐쇄망 설치 난이도</td>
      <td>중간</td>
      <td>높음 (의존성 많음)</td>
      <td>높음 (의존성 많음)</td>
      <td>낮음</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>일부 커스텀 아키텍처 기반 VLM은 vLLM, SGLang 등 범용 추론 엔진을 지원하지 않음<br />
이 경우 Transformers + FastAPI 조합이 유일한 서빙 방법이 됨</p>
</blockquote>

<hr />

<h3 id="4-커스텀-아키텍처-vlm의-서빙-제약">4. 커스텀 아키텍처 VLM의 서빙 제약</h3>

<p>일부 VLM은 vLLM/SGLang의 공식 지원 목록에 포함되지 않은 커스텀 아키텍처를 사용합니다.<br />
이번 프로젝트에서 사용한 VLM도 이에 해당했습니다.</p>

<table>
  <thead>
    <tr>
      <th>제약</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>커스텀 Projector</td>
      <td>vLLM/SGLang의 모델 레지스트리가 인식하지 못함</td>
    </tr>
    <tr>
      <td>커스텀 이미지 처리</td>
      <td>vLLM의 멀티모달 파이프라인과 호환되지 않음</td>
    </tr>
    <tr>
      <td>동적 타일링</td>
      <td>입력마다 텐서 shape이 달라져 Continuous Batching과 충돌</td>
    </tr>
  </tbody>
</table>

<p>vLLM/SGLang이 해당 아키텍처를 공식 지원하기 전까지는<br />
Transformers의 <code class="language-plaintext highlighter-rouge">trust_remote_code=True</code>로 직접 로드하는 것이 유일한 방법이었습니다.</p>

<hr />

<h3 id="5-fastapi-기반-vlm-서빙-구성">5. FastAPI 기반 VLM 서빙 구성</h3>

<p>위의 제약으로 인해 Transformers + PEFT + FastAPI 조합으로 직접 서빙 서버를 구축했습니다.<br />
API 형식은 OpenAI 호환으로 구성하여<br />
추후 vLLM/SGLang 전환 시 클라이언트 수정이 불필요하도록 설계했습니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[FastAPI 서버]
├── /health                   # 헬스 체크
├── /v1/chat/completions      # OpenAI 호환 API (단건 / 스트리밍)
├── /v1/batch/ocr             # 배치 OCR API (최대 8건)
└── /ocr                      # 단순 OCR API
</code></pre></div></div>

<p>서빙 시에는 베이스 모델과 LoRA 어댑터를 분리하여 로드하며,<br />
<code class="language-plaintext highlighter-rouge">use_lora</code> 파라미터로 런타임에 베이스/파인튜닝 모델을 전환할 수 있습니다.</p>

<blockquote>
  <p>어댑터 폴더에는 <code class="language-plaintext highlighter-rouge">adapter_config.json</code>과 <code class="language-plaintext highlighter-rouge">adapter_model.safetensors</code> 2개만 있어야 함<br />
베이스 모델의 설정 파일이 함께 있으면 로드 시 충돌 발생</p>
</blockquote>

<h4 id="docker-컨테이너-실행">Docker 컨테이너 실행</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">-d</span> <span class="nt">--gpus</span> <span class="s1">'"device=0"'</span> <span class="se">\</span>
    <span class="nt">--shm-size</span> 32g <span class="se">\</span>
    <span class="nt">-p</span> &lt;PORT&gt;:&lt;PORT&gt; <span class="se">\</span>
    <span class="nt">-v</span> /path/to/model:/workspace/model/VLM <span class="se">\</span>
    <span class="nt">-v</span> /path/to/adapter:/workspace/adapter/best_model_clean <span class="se">\</span>
    <span class="nt">-v</span> /path/to/server:/workspace/server <span class="se">\</span>
    <span class="nt">--name</span> vlm_ocr_server <span class="se">\</span>
    base_image:latest <span class="se">\</span>
    uvicorn app.main:app <span class="nt">--host</span> 0.0.0.0 <span class="nt">--port</span> &lt;PORT&gt;
</code></pre></div></div>

<h4 id="정상-실행-확인-1">정상 실행 확인</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl http://localhost:&lt;PORT&gt;/health
<span class="c"># → {"status": "ok", "model_type": "both (base + finetuned)"}</span>
</code></pre></div></div>

<blockquote>
  <p>모델과 어댑터를 별도 볼륨으로 마운트하면 어댑터만 교체하여 모델 버전 전환 가능<br />
폐쇄망에서는 서버 실행 후 반드시 헬스 체크와 샘플 추론으로 정상 동작을 확인해야 함</p>
</blockquote>

<hr />

<h2 id="17-분석엔진-연동">17. 분석엔진 연동</h2>

<p>분석엔진은 AI 모델의 실행과 외부 시스템 연동을 담당하는 백엔드 서비스입니다.<br />
컨테이너 매니저와 분석 스케줄러로 구성되며,<br />
컨테이너 매니저가 각 AI 모델별 분석 컨테이너를 생성·관리합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>         ┌── 분석엔진 (컨테이너 매니저 + 스케줄러 + 분석 컨테이너들)
         │
웹 서비스 ┤
         │
         └── 검색백엔드 ←── Kafka ←── 분석엔진
</code></pre></div></div>

<hr />

<h3 id="1-시작-및-종료">1. 시작 및 종료</h3>

<p>컨테이너 매니저를 먼저 시작한 후 스케줄러를 시작해야 합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 컨테이너 매니저 시작</span>
<span class="nb">cd</span> ~/engine/container_manager
./run.sh

<span class="c"># 스케줄러 시작</span>
<span class="nb">cd</span> ~/engine/scheduler
./run.sh
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 상태 확인</span>
docker ps <span class="nt">--filter</span> <span class="s2">"name=container_manager"</span> <span class="nt">--filter</span> <span class="s2">"name=scheduler"</span>

<span class="c"># 로그 확인</span>
docker logs <span class="nt">-f</span> container_manager
docker logs <span class="nt">-f</span> scheduler
</code></pre></div></div>

<hr />

<h3 id="2-ip-및-연동-설정">2. IP 및 연동 설정</h3>

<p>스케줄러의 <code class="language-plaintext highlighter-rouge">run.sh</code>에서 각 컴포넌트의 IP와 포트를 환경 변수로 설정합니다.</p>

<table>
  <thead>
    <tr>
      <th>환경 변수</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">AGENT_API_HOST</code> / <code class="language-plaintext highlighter-rouge">PORT</code></td>
      <td>컨테이너 매니저 서버 주소</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">SCHEDULER_HOST</code> / <code class="language-plaintext highlighter-rouge">PORT</code></td>
      <td>스케줄러 자신의 주소</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">VIDEO_SEARCH_API_HOST</code> / <code class="language-plaintext highlighter-rouge">PORT</code></td>
      <td>검색 API 서버 주소</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">KAFKA_BROKER_URI</code></td>
      <td>Kafka 브로커 주소</td>
    </tr>
  </tbody>
</table>

<p>컨테이너 매니저의 <code class="language-plaintext highlighter-rouge">params.cfg</code>에서는<br />
분석 컨테이너들이 사용하는 환경 변수와 볼륨 마운트 경로를 설정합니다.</p>

<blockquote>
  <p>params.cfg 변경 후에는 기존 분석 컨테이너들도 리셋해야 새 설정이 적용됨<br />
<code class="language-plaintext highlighter-rouge">./reset_containers.sh &lt;scheduler_host:port&gt;</code> 명령으로 일괄 리셋 가능</p>
</blockquote>

<hr />

<h3 id="3-분석-컨테이너-관리">3. 분석 컨테이너 관리</h3>

<p>컨테이너 매니저는 분석 요청에 따라 GPU가 연동된 분석 컨테이너를 자동으로 생성합니다.<br />
각 컨테이너는 독립된 모델을 탑재하고 있으며,<br />
분석이 완료되면 결과를 Kafka를 통해 검색백엔드로 전달합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 분석 컨테이너 일괄 리셋</span>
./reset_containers.sh localhost:&lt;SCHEDULER_PORT&gt;

<span class="c"># 실행 중인 분석 컨테이너 확인</span>
docker ps | <span class="nb">grep </span>analysis
</code></pre></div></div>

<blockquote>
  <p>분석 컨테이너 내부에서 GPU를 사용하므로 NVIDIA 드라이버 및 Container Toolkit이 <br />
정상 설치되어 있어야 함 (<a href="/building-air-gapped-ai-solution-dev-env-2/#10-nvidia-드라이버-설치">폐쇄망 AI Solution 개발환경 구축 (2)</a> 참고)</p>
</blockquote>

<hr />

<h2 id="18-검색백엔드-배포">18. 검색백엔드 배포</h2>

<p>검색백엔드는 분석엔진의 결과를 수집·인덱싱하고, 조건 기반 검색 API를 제공하는 서비스입니다.<br />
코드 개발은 별도 담당자가 수행하며,<br />
폐쇄망 환경에서는 솔루션 담당자가 배포, 로그 확인, 장애 대응을 직접 수행합니다.</p>

<h3 id="1-구성-요소">1. 구성 요소</h3>

<table>
  <thead>
    <tr>
      <th>구성 요소</th>
      <th>역할</th>
      <th>비고</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Kafka</td>
      <td>분석엔진과의 데이터 송수신</td>
      <td>분석 전 반드시 실행</td>
    </tr>
    <tr>
      <td>Elasticsearch</td>
      <td>분석 데이터 인덱싱 및 검색</td>
      <td>분석 전 반드시 실행</td>
    </tr>
    <tr>
      <td>검색 API 서버</td>
      <td>웹과 통신하는 검색 API</td>
      <td>docker compose로 실행</td>
    </tr>
    <tr>
      <td>검색 오케스트레이터</td>
      <td>LLM 기반 쿼리 이해 및 검색 수행</td>
      <td>docker compose로 실행</td>
    </tr>
    <tr>
      <td>LLM 서빙 (SGLang)</td>
      <td>검색·요약을 위한 LLM</td>
      <td><a href="#16-모델-서빙">16장</a> 참고</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="2-시작-및-종료">2. 시작 및 종료</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Kafka</span>
<span class="nb">cd</span> ~/infrastructure/kafka
docker compose up <span class="nt">-d</span>

<span class="c"># Elasticsearch</span>
<span class="nb">cd</span> ~/infrastructure/elasticsearch
docker compose up <span class="nt">-d</span>

<span class="c"># 검색 API 서버</span>
<span class="nb">cd</span> ~/search/api/docker
docker compose <span class="nt">-f</span> docker-compose.release.yml up <span class="nt">-d</span>

<span class="c"># 검색 오케스트레이터</span>
<span class="nb">cd</span> ~/search/orchestrator/docker
docker compose <span class="nt">-f</span> docker-compose.release.yml up <span class="nt">-d</span>
</code></pre></div></div>

<hr />

<h3 id="3-ip-설정">3. IP 설정</h3>

<p>각 컴포넌트의 <code class="language-plaintext highlighter-rouge">.env</code> 파일에서 IP와 포트를 설정합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 검색 API 서버 (.env)</span>
<span class="nv">SERVER_EXTERNAL_HOST</span><span class="o">=</span>&lt;검색_API_SERVER_IP&gt;
<span class="nv">ELASTICSEARCH_HOST</span><span class="o">=</span>https://&lt;ES_SERVER_IP&gt;:&lt;ES_PORT&gt;
<span class="nv">KAFKA_HOST</span><span class="o">=</span>&lt;KAFKA_SERVER_IP&gt;
</code></pre></div></div>

<blockquote>
  <p>설정 변경 후 반드시 해당 컴포넌트를 재시작해야 적용됨</p>
</blockquote>

<hr />

<h3 id="4-로그-확인-및-에러-추적">4. 로그 확인 및 에러 추적</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 컨테이너 로그</span>
docker logs <span class="nt">-f</span> search_api
docker logs <span class="nt">-f</span> search_orchestrator

<span class="c"># 파일 로그</span>
<span class="nb">tail</span> <span class="nt">-f</span> ~/search/api/logs/orchestration.log
<span class="nb">tail</span> <span class="nt">-f</span> ~/search/orchestrator/logs/orchestrator.log
</code></pre></div></div>

<hr />

<h3 id="5-폐쇄망에서-직접-해결한-이슈">5. 폐쇄망에서 직접 해결한 이슈</h3>

<h4 id="1-elasticsearch-디렉터리-권한-문제">(1) Elasticsearch 디렉터리 권한 문제</h4>

<p>Elasticsearch 컨테이너는 root가 아닌 전용 사용자(기본 UID 1000)로 실행됩니다.<br />
호스트에 bind mount한 데이터/로그 디렉터리의 UID/GID가 일치하지 않으면<br />
로그 파일 생성 및 rename 단계에서 JVM 초기화가 실패할 수 있습니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gc.log permission error
cannot change name gc.log to gc1.log
</code></pre></div></div>

<p>해당 오류는 GC 로그 파일에 대한 쓰기/rename 권한이 없어 발생한 문제였습니다.</p>

<p>일반적인 환경에서는 <code class="language-plaintext highlighter-rouge">chown</code>/<code class="language-plaintext highlighter-rouge">chgrp</code>를 통해 UID/GID를 일치시키는 것이 권장됩니다.<br />
그러나 본 사례에서는 다음과 같은 제약이 존재했습니다.</p>

<ul>
  <li>sudo 권한 미제공</li>
  <li>호스트 파일 소유권 변경 불가</li>
  <li>컨테이너 생성 권한만 허용</li>
</ul>

<p>이 제약 하에서 컨테이너 내부 root 권한을 활용하여<br />
bind mount된 디렉터리의 접근 권한을 간접적으로 조정하는 방식으로 대응했습니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker run <span class="nt">--rm</span> <span class="nt">-v</span> /path/to/es_data:/data &lt;image&gt; <span class="nb">chmod</span> <span class="nt">-R</span> 777 /data
docker run <span class="nt">--rm</span> <span class="nt">-v</span> /path/to/es_logs:/data &lt;image&gt; <span class="nb">chmod</span> <span class="nt">-R</span> 777 /data
</code></pre></div></div>

<blockquote>
  <p><code class="language-plaintext highlighter-rouge">chmod 777</code>은 보안상 권장되지 않지만, <br />
권한이 제한된 환경에서 서비스 가동을 위한 현실적인 대응 전략임<br />
동일 증상은 SELinux 컨텍스트 미적용에서도 발생할 수 있으며<br />
이 경우 <code class="language-plaintext highlighter-rouge">restorecon</code>으로 해결 가능 (<a href="/building-air-gapped-ai-solution-dev-env-2/#12-1-docker-데이터-저장-경로-변경">폐쇄망 AI Solution 개발환경 구축 (2)</a> 참고)</p>
</blockquote>

<hr />

<h4 id="2-elasticsearch-클러스터-재시작-시-노드-종료">(2) Elasticsearch 클러스터 재시작 시 노드 종료</h4>

<p>Elasticsearch 클러스터를 최초로 부트스트랩한 뒤에는 <code class="language-plaintext highlighter-rouge">docker-compose.yml</code>의 <code class="language-plaintext highlighter-rouge">cluster.initial_master_nodes</code> 설정을 제거하는 것이 원칙입니다.<br />
클러스터가 이미 형성된 상태에서 이 설정을 남겨두면,<br />
재시작 시 불필요한 부트스트랩 시도를 유발하거나<br />
cluster UUID 불일치로 실행 실패가 발생할 수 있습니다.</p>

<hr />

<h4 id="3-elasticsearch-인덱싱-확인">(3) Elasticsearch 인덱싱 확인</h4>

<p>Elasticsearch와 Kibana를 실행한 뒤, Kibana UI를 통해 인덱싱 상태를 확인합니다.</p>

<p>Kibana 인덱스 관리 화면(<code class="language-plaintext highlighter-rouge">/app/elasticsearch/content/search_indices</code>)에서<br />
인덱스 목록과 도큐먼트 수가 정상적으로 표시되고,<br />
분석 이후 도큐먼트 수가 증가하는지 확인합니다.</p>

<p>도큐먼트 수가 증가하지 않으면 다음 순서로 점검합니다.</p>

<ul>
  <li>Kafka 연결 상태 확인</li>
  <li>검색백엔드 컨슈머/인덱서 로그 확인</li>
  <li>Elasticsearch 로그에서 인덱싱 에러(4xx/5xx) 확인</li>
</ul>

<hr />

<h4 id="4-kafka-브로커-연결-실패">(4) Kafka 브로커 연결 실패</h4>

<p>Kafka가 실행되었으나 분석엔진이나 검색 서버에서 연결하지 못하는 경우,<br />
Kafka의 <code class="language-plaintext highlighter-rouge">KAFKA_ADVERTISED_HOST_NAME</code>이 실제 서버 IP와 일치하는지 확인합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Kafka env 파일 확인</span>
<span class="nv">KAFKA_ADVERTISED_HOST_NAME</span><span class="o">=</span>&lt;실제_서버_IP&gt;
</code></pre></div></div>

<hr />

<h2 id="19-웹-서비스-배포">19. 웹 서비스 배포</h2>

<p>웹 서비스는 사용자 대시보드, 분석 요청 및 결과 조회를 담당하는 서비스입니다.<br />
코드 개발은 별도 담당자가 수행하며,<br />
폐쇄망 환경에서는 솔루션 담당자가 배포, 로그 확인, 장애 대응을 직접 수행합니다.</p>

<h3 id="1-이미지-반입-및-실행">1. 이미지 반입 및 실행</h3>

<p>웹 서비스는 프론트엔드, 백엔드, DB(MySQL)로 구성되며,<br />
각각 별도의 Docker 이미지를 tar로 반입하여 배포합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 이미지 로드</span>
docker load <span class="nt">-i</span> web_frontend.tar.gz
docker load <span class="nt">-i</span> web_backend.tar.gz
docker load <span class="nt">-i</span> mysql.tar.gz

<span class="c"># 컨테이너 실행 (예: 프론트엔드)</span>
docker run <span class="nt">-d</span> <span class="se">\</span>
    <span class="nt">--name</span> web_frontend <span class="se">\</span>
    <span class="nt">--network</span><span class="o">=</span><span class="s2">"host"</span> <span class="se">\</span>
    <span class="nt">-e</span> <span class="nv">PORT</span><span class="o">=</span>&lt;PORT&gt; <span class="se">\</span>
    <span class="nt">-e</span> <span class="nv">HOSTNAME</span><span class="o">=</span>0.0.0.0 <span class="se">\</span>
    <span class="nt">--restart</span> unless-stopped <span class="se">\</span>
    web_frontend:v1.0
</code></pre></div></div>

<blockquote>
  <p>백엔드와 DB도 동일한 방식으로 이미지 로드 후 컨테이너를 실행<br />
버전 업데이트 시에는 기존 컨테이너를 중지·삭제한 뒤 새 이미지를 로드하여 재실행</p>
</blockquote>

<hr />

<h3 id="2-db-설정-mysql">2. DB 설정 (MySQL)</h3>

<p>웹 백엔드의 설정은 MySQL DB를 통해 관리됩니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># DB 컨테이너 접속</span>
docker <span class="nb">exec</span> <span class="nt">-it</span> db_container /bin/bash
mysql <span class="nt">-u</span> root <span class="nt">-p</span>
use app_db<span class="p">;</span>
</code></pre></div></div>

<h4 id="ai-모델-등록">AI 모델 등록</h4>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">INSERT</span> <span class="k">INTO</span> <span class="o">&lt;</span><span class="err">모델</span><span class="n">_</span><span class="err">설정</span><span class="n">_</span><span class="err">테이블</span><span class="o">&gt;</span>
<span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">description</span><span class="p">,</span> <span class="n">ip</span><span class="p">,</span> <span class="n">port</span><span class="p">,</span> <span class="n">model_data</span><span class="p">)</span>
<span class="k">VALUES</span>
<span class="p">(</span><span class="s1">'VLM-7B'</span><span class="p">,</span> <span class="s1">'멀티모달 모델'</span><span class="p">,</span> <span class="s1">'&lt;VLM_SERVER_IP&gt;'</span><span class="p">,</span> <span class="s1">'&lt;VLM_PORT&gt;'</span><span class="p">,</span> <span class="s1">'/models/VLM-7B'</span><span class="p">);</span>
</code></pre></div></div>

<h4 id="ip-설정-변경">IP 설정 변경</h4>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">UPDATE</span> <span class="o">&lt;</span><span class="err">시스템</span><span class="n">_</span><span class="err">설정</span><span class="n">_</span><span class="err">테이블</span><span class="o">&gt;</span> <span class="k">SET</span> <span class="n">value</span> <span class="o">=</span> <span class="s1">'http://&lt;HOST&gt;:&lt;PORT&gt;'</span> <span class="k">WHERE</span> <span class="k">type</span> <span class="o">=</span> <span class="s1">'search.base-url'</span><span class="p">;</span>
<span class="k">UPDATE</span> <span class="o">&lt;</span><span class="err">시스템</span><span class="n">_</span><span class="err">설정</span><span class="n">_</span><span class="err">테이블</span><span class="o">&gt;</span> <span class="k">SET</span> <span class="n">value</span> <span class="o">=</span> <span class="s1">'http://&lt;HOST&gt;:&lt;PORT&gt;'</span> <span class="k">WHERE</span> <span class="k">type</span> <span class="o">=</span> <span class="s1">'engine.base-url'</span><span class="p">;</span>
</code></pre></div></div>

<hr />

<h3 id="3-운영-중-발생한-이슈">3. 운영 중 발생한 이슈</h3>

<h4 id="분석-에러-시-웹-무한-대기">분석 에러 시 웹 무한 대기</h4>

<p>분석 도중 에러가 발생하면 웹에서 분석 상태가 “진행 중”으로 멈추는 경우가 있습니다.<br />
이때 DB에서 직접 status 값을 변경하여 강제로 실패 또는 완료 처리합니다.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 분석 상태 강제 변경 (실패 처리)</span>
<span class="k">UPDATE</span> <span class="o">&lt;</span><span class="err">분석</span><span class="n">_</span><span class="err">작업</span><span class="n">_</span><span class="err">테이블</span><span class="o">&gt;</span> <span class="k">SET</span> <span class="n">status</span> <span class="o">=</span> <span class="s1">'FAILED'</span> <span class="k">WHERE</span> <span class="n">task_id</span> <span class="o">=</span> <span class="o">&lt;</span><span class="n">id</span><span class="o">&gt;</span><span class="p">;</span>

<span class="c1">-- 또는 완료 처리</span>
<span class="k">UPDATE</span> <span class="o">&lt;</span><span class="err">분석</span><span class="n">_</span><span class="err">작업</span><span class="n">_</span><span class="err">테이블</span><span class="o">&gt;</span> <span class="k">SET</span> <span class="n">status</span> <span class="o">=</span> <span class="s1">'COMPLETED'</span> <span class="k">WHERE</span> <span class="n">task_id</span> <span class="o">=</span> <span class="o">&lt;</span><span class="n">id</span><span class="o">&gt;</span><span class="p">;</span>
</code></pre></div></div>

<blockquote>
  <p>각 컴포넌트 간 연동은 일직선이 아니라 분석-웹, 분석-검색, 검색-웹 등 다대다 구조<br />
에러 원인이 어느 연결에서 발생했는지 파악하는 것이 핵심</p>
</blockquote>

<h4 id="에러-추적-흐름">에러 추적 흐름</h4>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>                ┌── 웹 (분석 상태 조회)
분석엔진 ──────┤
                └── Kafka → 검색백엔드 (결과 인덱싱)
                                 │
                            웹 (검색 결과 조회)
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>웹에서 무한 대기 발견
    ↓
1. 분석엔진 로그 확인 → 분석 자체가 실패했는지?
2. Kafka 연결 상태 확인 → 결과 전달이 끊겼는지?
3. 웹 백엔드 로그 확인 → 콜백을 수신하지 못했는지?
    ↓
원인에 따라 조치 (재시작 / DB 강제 변경 / 설정 수정)
</code></pre></div></div>

<blockquote>
  <p>폐쇄망에서는 솔루션 담당자가 모든 컴포넌트의 로그를 직접 확인해야 함<br />
코드 수정이 필요한 경우 개발자에게 로그를 전달하고 수정된 이미지를 재반입</p>
</blockquote>

<hr />

<h2 id="20-통합-운영-및-테스트">20. 통합 운영 및 테스트</h2>

<h3 id="1-서비스-실행-순서">1. 서비스 실행 순서</h3>

<p>전체 서비스는 의존성에 따라 순서대로 실행해야 합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1. 인프라 (Kafka, Elasticsearch)
    ↓
2. LLM 서빙 (SGLang / vLLM)
    ↓
3. 분석엔진 (컨테이너 매니저 → 스케줄러)
    ↓
4. 검색백엔드 (검색 API → 검색 오케스트레이터)
    ↓
5. 웹 서비스 (프론트엔드, 백엔드)
</code></pre></div></div>

<blockquote>
  <p>역순으로 종료하는 것을 권장<br />
Kafka/Elasticsearch가 내려간 상태에서 분석이 실행되면 데이터 유실 가능</p>
</blockquote>

<hr />

<h3 id="2-e2e-통합-테스트">2. E2E 통합 테스트</h3>

<p>모든 서비스가 실행된 후 다음 흐름을 순차적으로 검증합니다.</p>

<table>
  <thead>
    <tr>
      <th>단계</th>
      <th>확인 항목</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>웹에서 분석 요청 → 스케줄러가 요청 수신</td>
    </tr>
    <tr>
      <td>2</td>
      <td>컨테이너 매니저가 분석 컨테이너 생성 → GPU 연동 확인</td>
    </tr>
    <tr>
      <td>3</td>
      <td>분석 완료 → Kafka로 결과 전송</td>
    </tr>
    <tr>
      <td>4</td>
      <td>검색백엔드가 결과 수신 → Elasticsearch 인덱싱</td>
    </tr>
    <tr>
      <td>5</td>
      <td>웹에서 검색 → 결과 조회 확인</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="3-운영-모니터링">3. 운영 모니터링</h3>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>확인 방법</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>GPU 사용률</td>
      <td><code class="language-plaintext highlighter-rouge">nvidia-smi</code>, <code class="language-plaintext highlighter-rouge">nvtop</code>, Grafana 등</td>
    </tr>
    <tr>
      <td>컨테이너 상태</td>
      <td><code class="language-plaintext highlighter-rouge">docker ps -a</code></td>
    </tr>
    <tr>
      <td>디스크 사용량</td>
      <td><code class="language-plaintext highlighter-rouge">df -h</code> (로그·분석 결과 누적 확인)</td>
    </tr>
    <tr>
      <td>Kafka 상태</td>
      <td>컨슈머 랙(lag) 확인</td>
    </tr>
    <tr>
      <td>Elasticsearch 상태</td>
      <td><code class="language-plaintext highlighter-rouge">curl -k https://localhost:&lt;ES_PORT&gt;/_cluster/health</code></td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="4-장애-대응">4. 장애 대응</h3>

<h4 id="서비스-재시작">서비스 재시작</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 특정 컨테이너 재시작</span>
docker restart &lt;container_name&gt;

<span class="c"># 분석 컨테이너 일괄 리셋</span>
./reset_containers.sh &lt;scheduler_host:port&gt;

<span class="c"># docker compose 서비스 재시작</span>
<span class="nb">cd</span> ~/search/api/docker
docker compose <span class="nt">-f</span> docker-compose.release.yml down
docker compose <span class="nt">-f</span> docker-compose.release.yml up <span class="nt">-d</span>
</code></pre></div></div>

<h4 id="폐쇄망-특화-트러블슈팅">폐쇄망 특화 트러블슈팅</h4>

<table>
  <thead>
    <tr>
      <th>증상</th>
      <th>원인</th>
      <th>조치</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>컨테이너 실행 실패</td>
      <td>이미지 미반입 또는 손상</td>
      <td><code class="language-plaintext highlighter-rouge">docker images</code>로 확인 후 재반입</td>
    </tr>
    <tr>
      <td>GPU 미인식</td>
      <td>NVIDIA 드라이버 또는 Container Toolkit 이상</td>
      <td><code class="language-plaintext highlighter-rouge">nvidia-smi</code> 확인, <a href="/building-air-gapped-ai-solution-dev-env-2/#10-nvidia-드라이버-설치">폐쇄망 AI Solution 개발환경 구축 (2)</a> 참고</td>
    </tr>
    <tr>
      <td>컴포넌트 간 연결 실패</td>
      <td>IP/포트 설정 불일치</td>
      <td><code class="language-plaintext highlighter-rouge">.env</code> 파일 및 <code class="language-plaintext highlighter-rouge">params.cfg</code> IP 확인</td>
    </tr>
    <tr>
      <td>분석 결과 미수신</td>
      <td>Kafka 연결 끊김</td>
      <td>Kafka 실행 상태 및 <code class="language-plaintext highlighter-rouge">ADVERTISED_HOST_NAME</code> 확인</td>
    </tr>
    <tr>
      <td>검색 결과 없음</td>
      <td>Elasticsearch 인덱싱 실패</td>
      <td>Elasticsearch 클러스터 상태 및 디렉터리 권한 확인</td>
    </tr>
    <tr>
      <td>웹 무한 대기</td>
      <td>콜백 누락</td>
      <td>DB status 강제 변경 (<a href="#19-웹-서비스-배포">19장</a> 참고)</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>폐쇄망에서는 에러 발생 시 외부 지원이 제한되므로<br />
로그 확인 → 원인 추적 → 조치의 흐름을 솔루션 담당자가 직접 수행해야 함</p>
</blockquote>

<hr />

<h2 id="마무리">마무리</h2>

<p>이번 글에서는 폐쇄망 환경에서 AI 서비스를 실제 운영 가능한 형태로 구성하고 배포했습니다.<br />
모델 고도화부터 통합 운영까지 다음 단계를 직접 수행했습니다.</p>

<ol>
  <li>
    <p>VLM 기반 OCR 파인튜닝 전략 수립 및 LoRA 적용</p>
  </li>
  <li>
    <p>추론 엔진 비교와 서빙 아키텍처 설계</p>
  </li>
  <li>
    <p>커스텀 아키텍처 VLM 제약 분석 및 대응</p>
  </li>
  <li>
    <p>분석엔진·검색 백엔드·웹 서비스 통합 구조 정리</p>
  </li>
  <li>
    <p>컨테이너 기반 배포 및 운영 안정화</p>
  </li>
  <li>
    <p>통합 테스트 및 장애 대응 체계 수립</p>
  </li>
</ol>

<p>일반적인 환경에서는 인프라, AI, 백엔드, 운영이 분리되지만,<br />
폐쇄망 환경에서는 모델 학습부터 배포, 서비스 연동, 장애 대응까지<br />
현장에서 통합적으로 판단하고 정리해야 합니다.</p>

<p>이번 프로젝트에서는<br />
OS 설치와 네트워크 구성부터<br />
GPU 드라이버·컨테이너 런타임·원격 개발 환경 구축,<br />
모델 파인튜닝, 추론 엔진 선택 및 서빙 구조 설계,<br />
분석엔진·검색 백엔드·웹 서비스 배포,<br />
서비스 실행 순서 체계화와 현장 이슈 대응까지 맡으며<br />
전체 시스템이 안정적으로 동작하도록 구조를 정리하고 조율하는 역할을 수행했습니다.</p>

<p>특히 범용 추론 엔진이 지원하지 않는 커스텀 아키텍처 VLM을<br />
현장 환경에서 동작하도록 전환하고 검증한 과정은<br />
기술 제약 속에서 현실적인 대안을 도출해야 했던 작업이었습니다.</p>

<p>모델 개발을 넘어 인프라·서빙·서비스 연동·운영 안정화까지 직접 다루며<br />
AI 시스템을 구성 요소 단위가 아닌, 실제 운영되는 전체 시스템 기준으로 판단하게 되었습니다.</p>]]></content><author><name>indexkim</name></author><category term="data" /><category term="deploy" /><category term="docker" /><category term="engine" /><category term="infra" /><category term="model" /><category term="system" /><summary type="html"><![CDATA[이번 글에서는 폐쇄망 AI Solution 개발환경 구축에 이어, 실제 서비스 구성과 배포 과정에 대해 정리합니다. RHEL 9을 기준으로 하지만, 다른 Linux 배포판에서도 동일한 방식으로 적용할 수 있습니다.]]></summary></entry><entry><title type="html">폐쇄망 AI Solution 개발환경 구축 (2)</title><link href="https://indexkim.github.io//building-air-gapped-ai-solution-dev-env-2/" rel="alternate" type="text/html" title="폐쇄망 AI Solution 개발환경 구축 (2)" /><published>2025-11-13T00:00:00+09:00</published><updated>2025-11-13T00:00:00+09:00</updated><id>https://indexkim.github.io//building-air-gapped-ai-solution-dev-env-2</id><content type="html" xml:base="https://indexkim.github.io//building-air-gapped-ai-solution-dev-env-2/"><![CDATA[<p>이 글은 폐쇄망 AI Solution 개발환경 구축의 두 번째 편입니다. <br />
RHEL 9을 기준으로 하지만, 다른 Linux 배포판에서도 동일한 방식으로 적용할 수 있습니다.</p>

<blockquote>
  <p>특정 조직의 내부 시스템이나 정책, 네트워크 설정은 포함하지 않으며<br />
모든 내용은 공개된 기술과 일반적인 Linux 환경을 기반으로 구성되었습니다.</p>
</blockquote>

<hr />

<h2 id="10-nvidia-드라이버-설치">10. NVIDIA 드라이버 설치</h2>

<p>NVIDIA 드라이버는 GPU 하드웨어를 제어하는 커널 모듈입니다.<br />
NVIDIA 드라이버가 없으면 <code class="language-plaintext highlighter-rouge">nvidia-smi</code> 명령이 작동하지 않고, CUDA를 사용할 수 없습니다.<br />
NVIDIA Container Toolkit을 사용하는 경우 호스트에는 NVIDIA 드라이버만 설치합니다.</p>

<h3 id="1-gpu-장치-인식-확인">1. GPU 장치 인식 확인</h3>

<p>시스템에서 NVIDIA GPU가 정상적으로 인식되는지 확인합니다.<br />
결과 미출력 시 BIOS에서 PCI 장치를 활성화하거나 슬롯 연결을 점검합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>lspci | <span class="nb">grep</span> <span class="nt">-i</span> nvidia
</code></pre></div></div>

<hr />

<h3 id="2-기존-드라이버-제거">2. 기존 드라이버 제거</h3>

<p>기존 NVIDIA 관련 드라이버나 <code class="language-plaintext highlighter-rouge">nouveau</code> 모듈이 있다면 제거합니다.<br />
<code class="language-plaintext highlighter-rouge">nouveau</code>는 기본 커널 모듈로, 공식 NVIDIA 드라이버(<code class="language-plaintext highlighter-rouge">nvidia.ko</code>)와 충돌을 일으킬 수 있습니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>dnf remove <span class="nt">-y</span> <span class="s1">'*nvidia*'</span>
<span class="nb">sudo </span>dnf remove <span class="nt">-y</span> xorg-x11-drv-nouveau
</code></pre></div></div>

<hr />

<h3 id="3-nouveau-블랙리스트-설정">3. nouveau 블랙리스트 설정</h3>

<p>부팅 시 <code class="language-plaintext highlighter-rouge">nouveau</code> 모듈이 자동 로드되지 않도록 차단합니다.<br />
<code class="language-plaintext highlighter-rouge">modeset=0</code> 설정은 X11 초기화 시 프레임버퍼 점유를 방지합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>bash <span class="nt">-c</span> <span class="s1">'cat &lt;&lt;EOF &gt; /etc/modprobe.d/blacklist-nouveau.conf
blacklist nouveau
options nouveau modeset=0
EOF'</span>
</code></pre></div></div>

<p>생성되는 파일 경로:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/etc/modprobe.d/blacklist-nouveau.conf
</code></pre></div></div>

<hr />

<h3 id="4-커널-빌드-도구-설치">4. 커널 빌드 도구 설치</h3>

<p>NVIDIA 드라이버는 커널 모듈을 직접 빌드하기 때문에 버전에 맞는 개발 도구가 필요합니다.<br />
<code class="language-plaintext highlighter-rouge">uname -r</code> 명령으로 커널 버전을 확인하고, <code class="language-plaintext highlighter-rouge">kernel-devel</code> 패키지의 버전이 일치해야 합니다.</p>

<p><code class="language-plaintext highlighter-rouge">gcc</code>는 GNU Compiler Collection의 약자로, C/C++ 코드를 컴파일하여 실행 가능한 바이너리로 만드는 컴파일러입니다. 
NVIDIA 드라이버는 내부적으로 C 코드로 작성된 커널 모듈을 빌드하므로, <code class="language-plaintext highlighter-rouge">gcc</code>가 설치되어 있어야 설치 과정이 정상적으로 완료됩니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>dnf <span class="nb">install</span> <span class="nt">-y</span> gcc kernel-devel kernel-headers
<span class="nb">uname</span> <span class="nt">-r</span>
rpm <span class="nt">-q</span> kernel-devel
</code></pre></div></div>

<hr />

<h3 id="5-initramfs-재생성">5. initramfs 재생성</h3>

<p><code class="language-plaintext highlighter-rouge">initramfs</code>는 부팅 시 커널이 로드하는 임시 루트 파일시스템입니다.<br />
이전에 포함된 <code class="language-plaintext highlighter-rouge">nouveau.ko</code>를 제거하기 위해 새로 생성합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>dracut <span class="nt">--force</span>
</code></pre></div></div>

<hr />

<h3 id="6-x11-종료-및-cli-모드-전환">6. X11 종료 및 CLI 모드 전환</h3>

<p>GUI(X11, GDM)가 실행 중이면 드라이버 설치가 실패할 수 있습니다.<br />
CLI 모드로 전환하여 그래픽 세션을 종료합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl set-default multi-user.target
<span class="nb">sudo </span>systemctl isolate multi-user.target
</code></pre></div></div>

<hr />

<h3 id="7-시스템-재부팅">7. 시스템 재부팅</h3>

<p>재부팅 후 다음 명령으로 <code class="language-plaintext highlighter-rouge">nouveau</code>가 비활성화되었는지 확인합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>reboot
</code></pre></div></div>

<p>출력 결과가 없으면 성공적으로 비활성화된 것입니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>lsmod | <span class="nb">grep </span>nouveau
</code></pre></div></div>

<hr />

<h3 id="8-nvidia-드라이버-설치">8. NVIDIA 드라이버 설치</h3>

<h4 id="1-rhel-9-환경용-rpm-드라이버-설치">(1) RHEL 9 환경용 <code class="language-plaintext highlighter-rouge">.rpm</code> 드라이버 설치</h4>

<p>NVIDIA 공식 사이트에서 <code class="language-plaintext highlighter-rouge">nvidia-driver-local-repo-rhel9-580.*.rpm</code> 패키지를 사용해<br />
로컬 리포지토리를 구성한 뒤 설치할 수 있습니다.</p>

<p>이 파일은 폐쇄망 환경에서도 동작하는 오프라인 설치용 패키지입니다.<br />
설치 후 자동으로 <code class="language-plaintext highlighter-rouge">/var/nvidia-driver-local-repo-rhel9-580.*</code> 경로에 드라이버 파일이 풀리고,<br />
<code class="language-plaintext highlighter-rouge">/etc/yum.repos.d/</code>에 로컬 리포지토리가 등록됩니다.</p>

<p>설치 절차는 다음과 같습니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 1. local repo 패키지 설치</span>
<span class="nb">sudo </span>rpm <span class="nt">-ivh</span> nvidia-driver-local-repo-rhel9-580.95.05-1.0-1.x86_64.rpm

<span class="c"># 2. 리포지토리 등록 갱신</span>
<span class="nb">sudo </span>dnf clean all
<span class="nb">sudo </span>dnf repolist

<span class="c"># 3. 드라이버 설치</span>
<span class="nb">sudo </span>dnf <span class="nb">install</span> <span class="nt">-y</span> nvidia-driver
</code></pre></div></div>

<hr />

<h4 id="2-run-파일-방식-설치">(2) <code class="language-plaintext highlighter-rouge">.run</code> 파일 방식 설치</h4>

<p><code class="language-plaintext highlighter-rouge">.run</code> 파일 방식으로도 설치할 수 있습니다.<br />
이 방식은 커널 모듈을 직접 빌드하므로 인터넷 연결이 필요하지 않습니다.</p>

<p>설치 절차는 다음과 같습니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 1. 실행 권한 부여</span>
<span class="nb">chmod</span> +x NVIDIA-Linux-x86_64-580.95.05.run

<span class="c"># 2. 드라이버 설치</span>
<span class="nb">sudo </span>bash NVIDIA-Linux-x86_64-580.95.05.run <span class="nt">--no-cc-version-check</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">--no-cc-version-check</code> 옵션은 GCC 버전 불일치 시에도 설치가 중단되지 않도록 하는 옵션입니다.<br />
이 방식은 로컬 리포지토리 구성 없이 바로 설치할 수 있어 간편합니다.</p>

<hr />

<h4 id="3-설치-확인">(3) 설치 확인</h4>

<p>드라이버 설치가 완료되면 시스템을 재부팅한 뒤,
GPU 인식 여부를 확인합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>reboot
</code></pre></div></div>

<p>재부팅 후 다음 명령을 실행합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nvidia-smi
</code></pre></div></div>

<p>정상적으로 설치되었다면 다음과 같은 출력이 표시됩니다.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 580.95.05              Driver Version: 580.95.05      CUDA Version: 13.0     |
+-----------------------------------------+------------------------+----------------------+
| GPU  Name                 Persistence-M | Bus-Id          Disp.A | Volatile Uncorr. ECC |
| Fan  Temp   Perf          Pwr:Usage/Cap |           Memory-Usage | GPU-Util  Compute M. |
|                                         |                        |               MIG M. |
|=========================================+========================+======================|
|   0  NVIDIA B300 SXM6 AC            On  |   00000000:1A:00.0 Off |                    0 |
| N/A   27C    P0            139W / 1100W |       0MiB / 275040MiB |      0%      Default |
|                                         |                        |             Disabled |
+-----------------------------------------+------------------------+----------------------+
|   1  NVIDIA B300 SXM6 AC            On  |   00000000:3C:00.0 Off |                    0 |
| N/A   27C    P0            139W / 1100W |       0MiB / 275040MiB |      0%      Default |
|                                         |                        |             Disabled |
+-----------------------------------------+------------------------+----------------------+
|   2  NVIDIA B300 SXM6 AC            On  |   00000000:62:00.0 Off |                    0 |
| N/A   27C    P0            139W / 1100W |       0MiB / 275040MiB |      0%      Default |
|                                         |                        |             Disabled |
+-----------------------------------------+------------------------+----------------------+
|   3  NVIDIA B300 SXM6 AC            On  |   00000000:73:00.0 Off |                    0 |
| N/A   27C    P0            139W / 1100W |       0MiB / 275040MiB |      0%      Default |
|                                         |                        |             Disabled |
+-----------------------------------------+------------------------+----------------------+
|   4  NVIDIA B300 SXM6 AC            On  |   00000000:9A:00.0 Off |                    0 |
| N/A   27C    P0            139W / 1100W |       0MiB / 275040MiB |      0%      Default |
|                                         |                        |             Disabled |
+-----------------------------------------+------------------------+----------------------+
|   5  NVIDIA B300 SXM6 AC            On  |   00000000:BC:00.0 Off |                    0 |
| N/A   27C    P0            139W / 1100W |       0MiB / 275040MiB |      0%      Default |
|                                         |                        |             Disabled |
+-----------------------------------------+------------------------+----------------------+
|   6  NVIDIA B300 SXM6 AC            On  |   00000000:DF:00.0 Off |                    0 |
| N/A   27C    P0            139W / 1100W |       0MiB / 275040MiB |      0%      Default |
|                                         |                        |             Disabled |
+-----------------------------------------+------------------------+----------------------+
|   7  NVIDIA B300 SXM6 AC            On  |   00000000:F0:00.0 Off |                    0 |
| N/A   27C    P0            139W / 1100W |       0MiB / 275040MiB |      0%      Default |
|                                         |                        |             Disabled |
+-----------------------------------------+------------------------+----------------------+
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Driver Version</code>과 <code class="language-plaintext highlighter-rouge">CUDA Version</code>이 정상적으로 표시되면
드라이버 설치가 완료된 것입니다.</p>

<hr />

<h3 id="9-그래픽-모드-복원-및-확인">9. 그래픽 모드 복원 및 확인</h3>

<p>CLI 모드에서 설치를 완료한 경우,<br />
GUI 환경이 필요하다면 다음 명령으로 그래픽 모드를 복원합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl set-default graphical.target
<span class="nb">sudo </span>reboot
</code></pre></div></div>

<hr />

<h3 id="run-방식과-rpm-방식-비교">.run 방식과 .rpm 방식 비교</h3>

<table>
  <thead>
    <tr>
      <th>구분</th>
      <th><code class="language-plaintext highlighter-rouge">.run</code> 방식</th>
      <th><code class="language-plaintext highlighter-rouge">.rpm</code> 방식</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>설치 방식</td>
      <td>커널 모듈 직접 빌드</td>
      <td>패키지 관리 기반</td>
    </tr>
    <tr>
      <td>장점</td>
      <td>단일 파일로 간편</td>
      <td>Red Hat 지원, 시스템 통합</td>
    </tr>
    <tr>
      <td>단점</td>
      <td>커널 업데이트 시 재설치 필요</td>
      <td>의존성 파일을 모두 준비해야 함</td>
    </tr>
    <tr>
      <td>권장 환경</td>
      <td>오프라인 설치 환경</td>
      <td>표준화된 기업 환경</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="전체-절차-요약">전체 절차 요약</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 1. 기존 드라이버 및 nouveau 제거</span>
<span class="nb">sudo </span>dnf remove <span class="nt">-y</span> <span class="s1">'*nvidia*'</span>
<span class="nb">sudo </span>dnf remove <span class="nt">-y</span> xorg-x11-drv-nouveau

<span class="c"># 2. 블랙리스트 등록</span>
<span class="nb">sudo </span>bash <span class="nt">-c</span> <span class="s1">'cat &lt;&lt;EOF &gt; /etc/modprobe.d/blacklist-nouveau.conf
blacklist nouveau
options nouveau modeset=0
EOF'</span>

<span class="c"># 3. 커널 빌드 도구 설치</span>
<span class="nb">sudo </span>dnf <span class="nb">install</span> <span class="nt">-y</span> gcc kernel-devel kernel-headers

<span class="c"># 4. initramfs 재생성</span>
<span class="nb">sudo </span>dracut <span class="nt">--force</span>

<span class="c"># 5. CLI 모드 전환 후 재부팅</span>
<span class="nb">sudo </span>systemctl set-default multi-user.target
<span class="nb">sudo </span>reboot

<span class="c"># 6-1. NVIDIA 드라이버 설치 (rpm 방식)</span>
<span class="nb">sudo </span>rpm <span class="nt">-ivh</span> nvidia-driver-local-repo-rhel9-580.95.05-1.0-1.x86_64.rpm
<span class="nb">sudo </span>dnf clean all
<span class="nb">sudo </span>dnf <span class="nb">install</span> <span class="nt">-y</span> nvidia-driver

<span class="c"># 6-2. NVIDIA 드라이버 설치 (run 방식)</span>
<span class="nb">chmod</span> +x NVIDIA-Linux-x86_64-580.95.05.run
<span class="nb">sudo </span>bash NVIDIA-Linux-x86_64-580.95.05.run <span class="nt">--no-cc-version-check</span>

<span class="c"># 7. 그래픽 모드 복귀 및 확인</span>
<span class="nb">sudo </span>systemctl set-default graphical.target
<span class="nb">sudo </span>reboot
</code></pre></div></div>

<blockquote>
  <p><code class="language-plaintext highlighter-rouge">nouveau</code> 비활성화 → <code class="language-plaintext highlighter-rouge">initramfs</code> 재생성 → CLI 전환 → 드라이버 설치 순서를 지켜야 함<br />
6단계에서 <code class="language-plaintext highlighter-rouge">.rpm</code> 방식(6-1) 또는 <code class="language-plaintext highlighter-rouge">.run</code> 방식(6-2) 중 선택</p>
</blockquote>

<hr />

<h2 id="11-nvidia-container-toolkit-설치">11. NVIDIA Container Toolkit 설치</h2>

<p><code class="language-plaintext highlighter-rouge">NVIDIA Container Toolkit</code>은 컨테이너의 GPU 인식을 지원하는 핵심 구성 요소입니다.<br />
미설치시 컨테이너 내부에서 <code class="language-plaintext highlighter-rouge">nvidia-smi</code> 명령이 동작하지 않으며,<br />
AI 모델 학습이나 추론 시 GPU 가속을 사용할 수 없습니다.</p>

<hr />

<h3 id="1-구성-요소">1. 구성 요소</h3>

<p>NVIDIA Container Toolkit은 다음 패키지들로 구성됩니다.</p>

<table>
  <thead>
    <tr>
      <th>구성 요소</th>
      <th>역할</th>
      <th>비고</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">libnvidia-container</code></td>
      <td>GPU 장치 파일(<code class="language-plaintext highlighter-rouge">/dev/nvidia*</code>)과 드라이버 라이브러리를 컨테이너 내부로 전달</td>
      <td>GPU 접근 핵심</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">nvidia-container-runtime</code></td>
      <td>Docker 기본 런타임(<code class="language-plaintext highlighter-rouge">runc</code>)을 확장하여 GPU 지원 추가</td>
      <td><code class="language-plaintext highlighter-rouge">--runtime=nvidia</code> 옵션 제공</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">nvidia-docker2</code></td>
      <td>과거 CLI 호환용 패키지</td>
      <td>현재는 통합됨</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">nvidia-container-toolkit</code></td>
      <td>위 구성 전체를 포함하는 최신 통합 패키지</td>
      <td>RHEL 9 기준 사용</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="2-설치-전제-조건">2. 설치 전제 조건</h3>

<p>NVIDIA 커널 드라이버(<code class="language-plaintext highlighter-rouge">nvidia.ko</code>)가 이미 설치되어 있어야 합니다.<br />
설치하려는 NVIDIA Container Toolkit 버전은 드라이버 버전과 호환되어야 합니다.</p>

<p>드라이버 설치 여부 확인:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nvidia-smi
</code></pre></div></div>

<p>버전 호환성 예시:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">driver 580.95.05</code> → <code class="language-plaintext highlighter-rouge">toolkit 1.18.0</code> 사용 가능</li>
  <li><code class="language-plaintext highlighter-rouge">driver 550.78</code> → <code class="language-plaintext highlighter-rouge">toolkit 1.17.9</code> 사용 가능</li>
</ul>

<hr />

<h3 id="3-설치-절차">3. 설치 절차</h3>

<p>GitHub Release에서 직접 받은 <code class="language-plaintext highlighter-rouge">.rpm</code> 파일은 별도의 리포지토리 등록 없이<br />
<code class="language-plaintext highlighter-rouge">dnf install -y *.rpm</code> 명령어로 설치할 수 있습니다.<br />
의존성 순서가 있기 때문에 위 순서를 지키는 것이 안정적입니다.</p>

<p>NVIDIA Container Toolkit 설치 후에는 재부팅이 필요하지 않으며,<br />
Docker 또는 Podman 런타임에서 GPU를 인식하도록 설정만 추가하면 됩니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 1. GitHub Release에서 패키지 다운로드</span>
<span class="c"># https://github.com/NVIDIA/nvidia-container-toolkit/releases</span>
<span class="c"># (예: 1.18.0 버전 기준)</span>
libnvidia-container1-1.18.0-1.x86_64.rpm
libnvidia-container-tools-1.18.0-1.x86_64.rpm
nvidia-container-toolkit-base-1.18.0-1.x86_64.rpm
nvidia-container-toolkit-1.18.0-1.x86_64.rpm

<span class="c"># 2. 설치 실행</span>
<span class="nb">export </span><span class="nv">NVIDIA_CONTAINER_TOOLKIT_VERSION</span><span class="o">=</span>1.18.0-1
<span class="nb">sudo </span>dnf <span class="nb">install</span> <span class="nt">-y</span> <span class="se">\</span>
  libnvidia-container1-<span class="k">${</span><span class="nv">NVIDIA_CONTAINER_TOOLKIT_VERSION</span><span class="k">}</span>.x86_64.rpm <span class="se">\</span>
  libnvidia-container-tools-<span class="k">${</span><span class="nv">NVIDIA_CONTAINER_TOOLKIT_VERSION</span><span class="k">}</span>.x86_64.rpm <span class="se">\</span>
  nvidia-container-toolkit-base-<span class="k">${</span><span class="nv">NVIDIA_CONTAINER_TOOLKIT_VERSION</span><span class="k">}</span>.x86_64.rpm <span class="se">\</span>
  nvidia-container-toolkit-<span class="k">${</span><span class="nv">NVIDIA_CONTAINER_TOOLKIT_VERSION</span><span class="k">}</span>.x86_64.rpm
</code></pre></div></div>

<hr />

<h3 id="4-설치-확인">4. 설치 확인</h3>

<p>설치된 패키지 목록을 확인할 수 있습니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rpm <span class="nt">-qa</span> | <span class="nb">grep </span>nvidia-container
</code></pre></div></div>

<p>출력 예시:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>libnvidia-container1-1.18.0-1.x86_64
libnvidia-container-tools-1.18.0-1.x86_64
nvidia-container-toolkit-base-1.18.0-1.x86_64
nvidia-container-toolkit-1.18.0-1.x86_64
</code></pre></div></div>

<p>NVIDIA Container Toolkit CLI 버전을 확인합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nvidia-ctk <span class="nt">--version</span>
</code></pre></div></div>

<p>출력 예시:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>NVIDIA Container Toolkit CLI version 1.18.0
</code></pre></div></div>

<p>정상적으로 설치되었다면 <code class="language-plaintext highlighter-rouge">/etc/nvidia-container-runtime/config.toml</code> 파일이 존재하며,<br />
NVIDIA Container Toolkit 설정이 적용된 상태입니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">ls</span> /etc/nvidia-container-runtime/
<span class="nb">cat</span> /etc/nvidia-container-runtime/config.toml | <span class="nb">head</span> <span class="nt">-n</span> 10
</code></pre></div></div>

<hr />

<h3 id="호스트-cuda-설치에서-컨테이너-격리로-cuda-설치가-필요-없어진-이유">호스트 CUDA 설치에서 컨테이너 격리로: CUDA 설치가 필요 없어진 이유</h3>

<p>과거에는 환경 설정이 딥러닝 학습의 최대 진입장벽 중 하나였습니다.</p>

<p>Windows 환경에서는 CUDA Toolkit 설치 단계부터 Visual Studio 버전 제약으로 막히는 경우가 많았고,
OpenCV 같은 복잡한 라이브러리는 빌드 실패와 재시도를 며칠에 걸쳐 반복해야 했습니다.<br />
Ubuntu는 CUDA/cuDNN 설치가 비교적 수월했으나 OpenCV 빌드 시 WITH_QT, WITH_FFMPEG 같은 옵션 조합에서 충돌이 반복되었습니다.</p>

<p>그러나 Docker 기반 워크플로우로 전환된 이후부터는 상황이 크게 달라졌습니다.<br />
호스트에는 드라이버만 설치하면 되고, CUDA/cuDNN은 모두 컨테이너에 포함되기 때문에<br />
환경 구성으로 인한 문제는 사실상 사라졌습니다.</p>

<h4 id="1-호스트-직접-설치-방식-conda">1. 호스트 직접 설치 방식 (conda)</h4>
<ul>
  <li>호스트에 NVIDIA 드라이버 + CUDA Toolkit + cuDNN 설치 필요</li>
  <li>Windows의 경우 Visual Studio, CMake, Ninja 등 추가 개발 도구 필수</li>
  <li>OpenCV 소스 빌드 시 Qt/GTK GUI 옵션과 FFmpeg 의존성 충돌이 빈번</li>
  <li>CUDA/cuDNN/PyTorch 버전 조합을 맞추는 데 많은 시간 소모</li>
  <li>프로젝트마다 다른 CUDA 버전을 쓰면 호스트 환경 충돌 발생</li>
  <li>현재도 Jupyter 기반 로컬 개발, 빠른 프로토타이핑에는 유용함</li>
</ul>

<h4 id="2-컨테이너-방식-docker--nvidia-container-toolkit">2. 컨테이너 방식 (Docker + NVIDIA Container Toolkit)</h4>
<ul>
  <li>호스트에는 NVIDIA 드라이버만 설치하면 충분</li>
  <li>CUDA Toolkit, cuDNN, AI 프레임워크는 컨테이너 이미지에 포함</li>
  <li>호스트는 GPU 하드웨어 제어만 담당</li>
  <li>프로젝트마다 CUDA 버전이 달라도 컨테이너로 격리되어 충돌 없음</li>
  <li>환경 재현성이 높고, 학습·배포·운영까지 동일한 구성 유지 가능</li>
  <li>프로덕션·학습 환경에서는 사실상 표준</li>
</ul>

<blockquote>
  <p>단, 호스트에서 직접 CUDA C++ 컴파일을 수행해야 하는 경우 CUDA Toolkit 설치 필요<br />
→ PyTorch C++ Extension, TensorRT 플러그인, FlashAttention·xFormers 등 고성능 CUDA 커널, NVDEC/NPP 기반 미디어 처리 등</p>
</blockquote>

<hr />

<h2 id="12-docker-설치-및-gpu-연동">12. Docker 설치 및 GPU 연동</h2>

<p>Docker를 설치해 컨테이너 기반 개발 환경을 구성합니다.<br />
NVIDIA Container Toolkit과 연동하여 GPU를 사용할 수 있도록 설정합니다.</p>

<hr />

<h3 id="1-docker-설치">1. Docker 설치</h3>

<p>사전에 다운로드한 <code class="language-plaintext highlighter-rouge">.rpm</code> 파일을 사용해 Docker를 설치합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Docker 설치</span>
<span class="nb">sudo </span>dnf <span class="nb">install</span> <span class="nt">-y</span> ./containerd.io-<span class="k">*</span>.rpm <span class="se">\</span>
  ./docker-ce-<span class="k">*</span>.rpm <span class="se">\</span>
  ./docker-ce-cli-<span class="k">*</span>.rpm <span class="se">\</span>
  ./docker-buildx-plugin-<span class="k">*</span>.rpm <span class="se">\</span>
  ./docker-compose-plugin-<span class="k">*</span>.rpm

<span class="c"># 서비스 등록 및 실행</span>
<span class="nb">sudo </span>systemctl <span class="nb">enable</span> <span class="nt">--now</span> docker
<span class="nb">sudo </span>systemctl status docker
</code></pre></div></div>

<hr />

<h3 id="2-nvidia-container-toolkit-연동">2. NVIDIA Container Toolkit 연동</h3>

<p>NVIDIA Container Toolkit을 Docker 런타임에 연결하면<br />
컨테이너 내부에서도 GPU가 인식되어 CUDA 연산을 사용할 수 있습니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>nvidia-ctk runtime configure <span class="nt">--runtime</span><span class="o">=</span>docker
<span class="nb">sudo </span>systemctl restart docker
</code></pre></div></div>

<hr />

<h3 id="3-사용자-권한-설정">3. 사용자 권한 설정</h3>

<p>현재 사용자를 <code class="language-plaintext highlighter-rouge">docker</code> 그룹에 추가해야 루트 권한 없이 명령을 실행할 수 있습니다.<br />
변경 사항은 새 터미널 세션에서 적용됩니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>usermod <span class="nt">-aG</span> docker <span class="nv">$USER</span>
</code></pre></div></div>

<hr />

<h3 id="4-컨테이너-생성-및-이미지-관리">4. 컨테이너 생성 및 이미지 관리</h3>

<p>폐쇄망 환경에서는 Docker Hub에 직접 접근할 수 없으므로,<br />
외부 인터넷 환경에서 이미지를 준비해 tar 파일 형태로 반입해야 합니다.</p>

<p>이미지를 만드는 방법은 두 가지입니다.</p>

<ol>
  <li>Dockerfile 방식</li>
  <li><code class="language-plaintext highlighter-rouge">docker commit</code> 방식</li>
</ol>

<p>두 방식 모두 최종적으로 <code class="language-plaintext highlighter-rouge">docker save</code>로 tar 파일을 생성해 반입합니다.</p>

<hr />

<h4 id="dockerfile">Dockerfile</h4>

<p>가장 일반적이며 재현성과 협업·버전 관리에 유리합니다.<br />
동일 Dockerfile로 언제든지 같은 환경을 다시 만들 수 있고 Git으로 관리가 가능합니다.</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Dockerfile</span>
<span class="k">FROM</span><span class="s"> pytorch/pytorch:2.4.0-cuda12.4</span>

<span class="c"># 시스템 패키지 설치</span>
<span class="k">RUN </span>apt-get update <span class="o">&amp;&amp;</span> <span class="se">\
</span>    apt-get <span class="nb">install</span> <span class="nt">-y</span> vim git wget <span class="o">&amp;&amp;</span> <span class="se">\
</span>    <span class="nb">rm</span> <span class="nt">-rf</span> /var/lib/apt/lists/<span class="k">*</span>

<span class="c"># Python 패키지 설치</span>
<span class="k">RUN </span>pip <span class="nb">install</span> <span class="nt">--no-cache-dir</span> numpy pandas scikit-learn opencv-python

<span class="c"># 작업 디렉터리 설정</span>
<span class="k">WORKDIR</span><span class="s"> /workspace</span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 이미지 빌드</span>
docker build <span class="nt">-t</span> idxkim_image:v0.1 <span class="nb">.</span>

<span class="c"># 이미지를 tar로 저장</span>
docker save <span class="nt">-o</span> idxkim_image_v0.1.tar idxkim_image:v0.1
</code></pre></div></div>

<p>분석 엔진 개발, 프로덕션 배포, 팀 간 협업 환경에 적합하며<br />
CI/CD 파이프라인 구축에 활용됩니다.</p>

<hr />

<h4 id="docker-commit">docker commit</h4>

<p>컨테이너 내부에서 직접 패키지를 설치하고,
그 실행 상태 그대로 스냅샷을 저장하는 방식입니다.
재현성은 Dockerfile보다 떨어지지만,
파인튜닝·실험·일회성 분석에 빠르게 활용할 수 있습니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 1. 베이스 이미지 다운로드</span>
docker pull pytorch/pytorch:2.4.0-cuda12.4

<span class="c"># 2. 컨테이너 실행 및 필요한 패키지 설치</span>
docker run <span class="nt">-it</span> <span class="nt">--name</span> temp_container pytorch/pytorch:2.4.0-cuda12.4 /bin/bash
<span class="c"># (컨테이너 내부에서)</span>
<span class="c"># apt-get update &amp;&amp; apt-get install -y vim git wget</span>
<span class="c"># pip install numpy pandas scikit-learn opencv-python</span>
<span class="c"># exit</span>

<span class="c"># 3. 커스텀 이미지로 저장</span>
docker commit temp_container idxkim_image:v0.1
docker <span class="nb">rm </span>temp_container

<span class="c"># 4. 이미지를 tar로 저장</span>
docker save <span class="nt">-o</span> idxkim_image_v0.1.tar idxkim_image:v0.1
</code></pre></div></div>

<p>모델 파인튜닝, 데이터 전처리 파이프라인 테스트, 라이브러리 조합 탐색 등<br />
일회성 분석 및 실험 환경에 활용됩니다.</p>

<hr />

<h4 id="두-방식-비교">두 방식 비교</h4>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>Dockerfile</th>
      <th>docker commit</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>재현성</td>
      <td>높음</td>
      <td>낮음 (작업 기록 필요)</td>
    </tr>
    <tr>
      <td>버전 관리</td>
      <td>Git 관리 가능</td>
      <td>어려움</td>
    </tr>
    <tr>
      <td>협업</td>
      <td>용이</td>
      <td>환경 차이 발생</td>
    </tr>
    <tr>
      <td>자동화(CI/CD)</td>
      <td>적합</td>
      <td>부적합</td>
    </tr>
    <tr>
      <td>용도</td>
      <td>엔진·API 서버·배포</td>
      <td>실험·파인튜닝·일회성 작업</td>
    </tr>
  </tbody>
</table>

<hr />

<h4 id="폐쇄망-반입-방법">폐쇄망 반입 방법</h4>

<p>두 방식 중 무엇을 사용하든 tar 파일 형태로 반입합니다.
tar 파일 방식을 사용하는 이유는 다음과 같습니다.</p>

<ul>
  <li>외부에서 검증된 환경을 그대로 가져올 수 있음</li>
  <li>폐쇄망 내부에서 재빌드할 필요 없음</li>
  <li>의존성 충돌 없이 동일한 환경 보장</li>
  <li>물리적 이동만으로 환경 복제 가능</li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 외부 환경에서 이미지 검증 완료 후 저장</span>
docker save <span class="nt">-o</span> idxkim_image_v0.1.tar idxkim_image:v0.1

<span class="c"># 폐쇄망 내부에서 즉시 사용</span>
docker load <span class="nt">-i</span> idxkim_image_v0.1.tar
docker run <span class="nt">--gpus</span> all idxkim_image:v0.1  <span class="c"># 바로 실행 가능</span>
</code></pre></div></div>

<h4 id="commit-이미지에서-dockerfile-복원하기">commit 이미지에서 Dockerfile 복원하기</h4>

<p><code class="language-plaintext highlighter-rouge">docker commit</code>으로 만든 이미지를 Dockerfile로 되돌릴 수 있는지는<br />
원본 Dockerfile의 존재 여부에 따라 결정됩니다.</p>

<hr />

<h5 id="원본-dockerfile이-없는-경우">원본 Dockerfile이 없는 경우</h5>

<p>설치 패키지 정보를 기반으로 유사한 Dockerfile을 작성할 수 있지만 완전한 복원은 불가능합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 이미지에서 컨테이너 실행</span>
docker run <span class="nt">-it</span> idxkim_image:v0.1 /bin/bash

<span class="c"># 시스템 패키지 목록 추출</span>
apt list <span class="nt">--installed</span> <span class="o">&gt;</span> apt_packages.txt
dpkg <span class="nt">-l</span> <span class="o">&gt;</span> dpkg_list.txt

<span class="c"># Python 패키지 정확한 버전 추출</span>
pip freeze <span class="o">&gt;</span> requirements.txt

<span class="c"># 설치된 CUDA 버전 확인</span>
<span class="nb">ls</span> /usr/local/cuda<span class="k">*</span>/bin/
which nvidia-smi
</code></pre></div></div>

<p>추출한 정보로 Dockerfile을 재작성합니다.</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> pytorch/pytorch:2.4.0-cuda12.4</span>

<span class="c"># apt_packages.txt 참고하여 필요한 패키지만 선택</span>
<span class="k">RUN </span>apt-get update <span class="o">&amp;&amp;</span> <span class="se">\
</span>    apt-get <span class="nb">install</span> <span class="nt">-y</span> <span class="se">\
</span>    vim <span class="se">\
</span>    git <span class="se">\
</span>    wget <span class="se">\
</span>    <span class="o">&amp;&amp;</span> <span class="nb">rm</span> <span class="nt">-rf</span> /var/lib/apt/lists/<span class="k">*</span>

<span class="c"># requirements.txt 그대로 사용</span>
<span class="k">COPY</span><span class="s"> requirements.txt .</span>
<span class="k">RUN </span>pip <span class="nb">install</span> <span class="nt">--no-cache-dir</span> <span class="nt">-r</span> requirements.txt

<span class="k">WORKDIR</span><span class="s"> /workspace</span>
</code></pre></div></div>

<p>그러나 다음과 같은 정보는 복원할 수 없습니다.</p>

<ul>
  <li>설치 순서 및 의존성 설치 흐름</li>
  <li>빌드 스크립트의 컴파일 옵션 (FFmpeg/OpenCV 빌드 플래그 등)</li>
  <li>환경 변수 및 커스텀 스크립트</li>
  <li>중간 레이어가 어떤 명령으로 만들어졌는지에 대한 정보</li>
</ul>

<hr />

<h5 id="원본-dockerfile이-있는-경우">원본 Dockerfile이 있는 경우</h5>

<p>원본 Dockerfile과 빌드 스크립트를 보유하고 있다면,<br />
<code class="language-plaintext highlighter-rouge">docker commit</code>으로 만든 이미지든 tar 파일이든 상관없이<br />
언제든지 동일한 환경을 재생성할 수 있습니다.</p>

<p>Dockerfile은 환경 구성의 설계도이므로,<br />
이를 보관하면 필요할 때마다 동일한 환경을 재현할 수 있습니다.</p>

<hr />

<h3 id="5-gpu-테스트">5. GPU 테스트</h3>

<p>커스텀 이미지에 <code class="language-plaintext highlighter-rouge">nvidia-smi</code>와 PyTorch가 모두 포함되어 있으므로,<br />
하나의 이미지로 GPU 드라이버 및 CUDA 연동을 모두 확인할 수 있습니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># nvidia-smi로 GPU 드라이버 확인</span>
docker run <span class="nt">--rm</span> <span class="nt">--gpus</span> all idxkim_image:v0.1 nvidia-smi

<span class="c"># PyTorch로 CUDA 가용성 확인</span>
docker run <span class="nt">--rm</span> <span class="nt">--gpus</span> all idxkim_image:v0.1 <span class="se">\</span>
  python <span class="nt">-c</span> <span class="s2">"import torch; print(f'CUDA available: {torch.cuda.is_available()}'); print(f'GPU count: {torch.cuda.device_count()}')"</span>
</code></pre></div></div>

<p>GPU 정보가 정상적으로 출력되면 Docker와 NVIDIA 런타임 연동이 완료된 것입니다.</p>

<hr />

<h2 id="12-1-docker-데이터-저장-경로-변경">12-1. Docker 데이터 저장 경로 변경</h2>

<p>Docker 설치 후 저장 경로(<code class="language-plaintext highlighter-rouge">/var/lib/docker</code>)를 변경해야 하는 경우에만 수행합니다. <br />
새 설치 시에는 <code class="language-plaintext highlighter-rouge">/etc/docker/daemon.json</code> 파일로 경로를 미리 지정하세요.</p>

<h3 id="주의사항">주의사항</h3>

<ul>
  <li>레이어 손상 위험: 복사 중 중단되면 컨테이너 실행이 실패할 수 있습니다.</li>
  <li>다운타임 발생: 모든 컨테이너를 중지한 상태에서 작업해야 합니다.</li>
  <li>백업 권장: 중요한 컨테이너나 이미지는 사전에 백업해 두는 것이 안전합니다.</li>
  <li>SELinux 컨텍스트: 경로 변경 후 새 디렉터리에 SELinux 컨텍스트를 등록해야 합니다.<br />
등록하지 않으면 이후 볼륨 마운트 시 권한 문제가 발생할 수 있습니다.</li>
</ul>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 새 경로에 SELinux 컨텍스트 등록</span>
<span class="nb">sudo </span>semanage fcontext <span class="nt">-a</span> <span class="nt">-t</span> container_var_lib_t <span class="s2">"/home/docker(/.*)?"</span>
<span class="nb">sudo </span>restorecon <span class="nt">-Rv</span> /home/docker
</code></pre></div></div>
<hr />

<h3 id="1-데이터-디렉터리-이동">1. 데이터 디렉터리 이동</h3>

<p>Docker 서비스를 중지하고 기존 데이터를 새 경로로 복사합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl stop docker
<span class="nb">sudo mkdir</span> <span class="nt">-p</span> /home/docker
<span class="nb">sudo </span>rsync <span class="nt">-aP</span> /var/lib/docker/ /home/docker/
<span class="nb">sudo </span>vi /etc/docker/daemon.json
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">/etc/docker/daemon.json</code> 수정 내용:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"data-root"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/home/docker"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"runtimes"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"nvidia"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nl">"args"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
            </span><span class="nl">"path"</span><span class="p">:</span><span class="w"> </span><span class="s2">"nvidia-container-runtime"</span><span class="w">
        </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<hr />

<h3 id="2-서비스-재시작-및-확인">2. 서비스 재시작 및 확인</h3>

<p>Docker 데몬을 다시 로드하고 경로 변경을 확인합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl daemon-reload
<span class="nb">sudo </span>systemctl start docker
docker info | <span class="nb">grep</span> <span class="s2">"Docker Root Dir"</span>
</code></pre></div></div>

<p>출력 예시:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Docker Root Dir: /home/docker
</code></pre></div></div>

<p>정상적으로 변경되면 기존 디렉터리를 제거합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo rm</span> <span class="nt">-rf</span> /var/lib/docker
</code></pre></div></div>

<hr />

<h2 id="12-2-설치-전-데이터-경로-지정-사전-설정-방식">12-2. 설치 전 데이터 경로 지정 (사전 설정 방식)</h2>

<p>Docker 설치 전에 데이터 경로를 지정하는 방법입니다. 
이후 일반적인 Docker 설치 절차를 진행하면
초기부터 <code class="language-plaintext highlighter-rouge">/home/docker</code> 경로를 사용하게 됩니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo mkdir</span> <span class="nt">-p</span> /home/docker
<span class="nb">sudo mkdir</span> <span class="nt">-p</span> /etc/docker
<span class="nb">sudo </span>bash <span class="nt">-c</span> <span class="s1">'cat &lt;&lt;EOF &gt; /etc/docker/daemon.json
{
  "data-root": "/home/docker"
}
EOF'</span>
</code></pre></div></div>

<hr />

<h2 id="12-3-명령어-자동-완성-설치-선택">12-3. 명령어 자동 완성 설치 (선택)</h2>

<p><code class="language-plaintext highlighter-rouge">docker</code> 및 <code class="language-plaintext highlighter-rouge">podman</code> 명령의 자동 완성이 활성화되어
명령 입력 효율이 향상됩니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>dnf <span class="nb">install</span> <span class="nt">-y</span> bash-completion
</code></pre></div></div>

<hr />

<h2 id="13-podman-설치-및-gpu-연동">13. Podman 설치 및 GPU 연동</h2>

<p>Podman은 데몬(daemon) 없이 동작하는 컨테이너 엔진입니다.<br />
데몬은 백그라운드에서 상시 실행되는 서비스 프로세스로,<br />
Docker는 <code class="language-plaintext highlighter-rouge">dockerd</code>라는 데몬을 통해 컨테이너를 관리하지만 <br />
Podman은 명령 실행 시 즉시 컨테이너를 생성·종료합니다.<br />
따라서 Rootless 실행이 가능하고 보안성이 높습니다.</p>

<p>RHEL 9에서는 Podman이 기본 컨테이너 엔진으로 권장됩니다.</p>

<hr />

<h3 id="docker-vs-podman-주요-차이점">Docker vs Podman 주요 차이점</h3>

<table>
  <thead>
    <tr>
      <th>구분</th>
      <th>Docker</th>
      <th>Podman</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>GPU 옵션</td>
      <td><code class="language-plaintext highlighter-rouge">--gpus all</code></td>
      <td><code class="language-plaintext highlighter-rouge">--device nvidia.com/gpu=all</code></td>
    </tr>
    <tr>
      <td>NVIDIA 연동</td>
      <td><code class="language-plaintext highlighter-rouge">nvidia-ctk runtime configure</code></td>
      <td><code class="language-plaintext highlighter-rouge">nvidia-ctk cdi generate</code></td>
    </tr>
    <tr>
      <td>데이터 경로</td>
      <td><code class="language-plaintext highlighter-rouge">/var/lib/docker</code></td>
      <td><code class="language-plaintext highlighter-rouge">/var/lib/containers</code></td>
    </tr>
    <tr>
      <td>설정 파일</td>
      <td><code class="language-plaintext highlighter-rouge">/etc/docker/daemon.json</code></td>
      <td><code class="language-plaintext highlighter-rouge">/etc/containers/storage.conf</code></td>
    </tr>
    <tr>
      <td>권한</td>
      <td><code class="language-plaintext highlighter-rouge">docker</code> 그룹 필요</td>
      <td>Rootless 지원</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>중요: GPU 사용 시 옵션이 다름<br />
Docker: <code class="language-plaintext highlighter-rouge">docker run --gpus all &lt;image&gt;</code><br />
Podman: <code class="language-plaintext highlighter-rouge">podman run --device nvidia.com/gpu=all &lt;image&gt;</code></p>
</blockquote>

<hr />

<h3 id="1-podman-설치">1. Podman 설치</h3>

<p>Podman은 별도의 데몬이 필요하지 않으며, 설치 직후 바로 사용 가능합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Podman 설치</span>
<span class="nb">sudo </span>dnf <span class="nb">install</span> <span class="nt">-y</span> podman
</code></pre></div></div>

<hr />

<h3 id="2-nvidia-container-toolkit-연동-1">2. NVIDIA Container Toolkit 연동</h3>

<p><code class="language-plaintext highlighter-rouge">cdi generate</code>는 Podman용 GPU 장치 매핑 설정 파일(<code class="language-plaintext highlighter-rouge">/etc/cdi/nvidia.yaml</code>)을 생성합니다.
Docker의 <code class="language-plaintext highlighter-rouge">runtime configure</code> 명령과 동일한 역할을 수행합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>nvidia-ctk cdi generate <span class="nt">--output</span><span class="o">=</span>/etc/cdi/nvidia.yaml
</code></pre></div></div>

<hr />

<h3 id="3-컨테이너-이미지-로드-및-실행">3. 컨테이너 이미지 로드 및 실행</h3>

<p>커스텀 이미지를 로드하고 GPU 연동을 테스트합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 커스텀 이미지 로드</span>
<span class="nb">sudo </span>podman load <span class="nt">-i</span> idxkim_image_v0.1.tar

<span class="c"># nvidia-smi로 GPU 드라이버 확인</span>
<span class="nb">sudo </span>podman run <span class="nt">--rm</span> <span class="nt">-it</span> <span class="se">\</span>
  <span class="nt">--device</span> nvidia.com/gpu<span class="o">=</span>all <span class="se">\</span>
  idxkim_image:v0.1 <span class="se">\</span>
  nvidia-smi

<span class="c"># PyTorch로 CUDA 가용성 확인</span>
<span class="nb">sudo </span>podman run <span class="nt">--rm</span> <span class="nt">-it</span> <span class="se">\</span>
  <span class="nt">--device</span> nvidia.com/gpu<span class="o">=</span>all <span class="se">\</span>
  idxkim_image:v0.1 <span class="se">\</span>
  python <span class="nt">-c</span> <span class="s2">"import torch; print(f'CUDA available: {torch.cuda.is_available()}'); print(f'GPU count: {torch.cuda.device_count()}')"</span>
</code></pre></div></div>

<p>정상적으로 GPU 정보가 출력되면 Podman과 NVIDIA Container Toolkit이 연동된 것입니다.</p>

<hr />

<h3 id="4-작업용-컨테이너-실행-예시">4. 작업용 컨테이너 실행 예시</h3>

<p>실제 개발 작업을 위한 컨테이너를 실행합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>podman run <span class="nt">-it</span> <span class="nt">--rm</span> <span class="se">\</span>
  <span class="nt">--device</span> nvidia.com/gpu<span class="o">=</span>all <span class="se">\</span>
  <span class="nt">--shm-size</span><span class="o">=</span>100g <span class="se">\</span>
  <span class="nt">-v</span> /workspace:/workspace <span class="se">\</span>
  idxkim_image:v0.1
</code></pre></div></div>

<hr />

<h2 id="13-1-podman-데이터-저장-경로-변경">13-1. Podman 데이터 저장 경로 변경</h2>

<p>Podman 설치 후 저장 경로(<code class="language-plaintext highlighter-rouge">/var/lib/containers</code>)를 변경해야 하는 경우에만 수행합니다.<br />
새 설치 시에는 <code class="language-plaintext highlighter-rouge">/etc/containers/storage.conf</code> 파일로 경로를 미리 지정하세요.</p>

<h3 id="주의사항-1">주의사항</h3>

<ul>
  <li>레이어 손상 위험: 복사 중 중단되면 컨테이너 실행이 실패할 수 있습니다.</li>
  <li>다운타임 발생: 모든 컨테이너를 중지한 상태에서 작업해야 합니다.</li>
  <li>백업 권장: 중요한 컨테이너나 이미지는 사전에 백업해 두는 것이 안전합니다.</li>
  <li>Rootless 모드: Rootless 환경은 <code class="language-plaintext highlighter-rouge">~/.local/share/containers</code> 경로도 함께 확인해야 합니다.</li>
</ul>

<hr />

<h3 id="1-데이터-디렉터리-이동-1">1. 데이터 디렉터리 이동</h3>

<p>Podman 컨테이너를 중지하고 기존 데이터를 새 경로로 복사합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>podman stop <span class="nt">--all</span>
<span class="nb">sudo du</span> <span class="nt">-sh</span> /var/lib/containers
<span class="nb">sudo mkdir</span> <span class="nt">-p</span> /home/podman
<span class="nb">sudo </span>rsync <span class="nt">-aP</span> /var/lib/containers/ /home/podman/
<span class="nb">sudo </span>vi /etc/containers/storage.conf
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">/etc/containers/storage.conf</code> 수정 내용:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[storage]</span>
<span class="py">driver</span> <span class="p">=</span> <span class="s">"overlay"</span>
<span class="py">runRoot</span> <span class="p">=</span> <span class="s">"/run/containers/storage"</span>
<span class="py">graphRoot</span> <span class="p">=</span> <span class="s">"/home/podman"</span>

<span class="nn">[storage.options]</span>
<span class="py">additionalimagestores</span> <span class="p">=</span> <span class="s">[]</span>
</code></pre></div></div>

<hr />

<h3 id="2-적용-및-확인">2. 적용 및 확인</h3>

<p>설정을 다시 로드하고 경로 변경을 확인합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl daemon-reexec
<span class="nb">sudo </span>systemctl start podman
podman info | <span class="nb">grep </span>graphRoot
</code></pre></div></div>

<p>출력 예시:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>graphRoot: /home/podman
</code></pre></div></div>

<p>정상적으로 변경되면 기존 디렉터리를 제거합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo rm</span> <span class="nt">-rf</span> /var/lib/containers
</code></pre></div></div>

<hr />

<h2 id="13-2-설치-전-데이터-경로-지정-사전-설정-방식">13-2. 설치 전 데이터 경로 지정 (사전 설정 방식)</h2>

<p>Podman 설치 전에 데이터 저장 경로를 변경하는 방법입니다.<br />
이후 일반적인 Podman 설치 절차를 진행하면
초기부터 <code class="language-plaintext highlighter-rouge">/home/podman</code> 경로를 사용합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo mkdir</span> <span class="nt">-p</span> /home/podman
<span class="nb">sudo mkdir</span> <span class="nt">-p</span> /etc/containers
<span class="nb">sudo </span>bash <span class="nt">-c</span> <span class="s1">'cat &lt;&lt;EOF &gt; /etc/containers/storage.conf
[storage]
driver = "overlay"
runRoot = "/run/containers/storage"
graphRoot = "/home/podman"

[storage.options]
additionalimagestores = []
EOF'</span>
</code></pre></div></div>

<hr />

<h2 id="13-3-명령어-자동-완성-설치-선택">13-3. 명령어 자동 완성 설치 (선택)</h2>

<p><code class="language-plaintext highlighter-rouge">podman</code> 및 <code class="language-plaintext highlighter-rouge">docker</code> 명령 자동 완성이 활성화되어
명령 입력 효율이 향상됩니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>dnf <span class="nb">install</span> <span class="nt">-y</span> bash-completion
</code></pre></div></div>

<hr />

<h2 id="14-vscode-remote-ssh-설치">14. VSCode Remote-SSH 설치</h2>

<p>폐쇄망 환경에서 VSCode를 이용해 원격 개발 환경(SSH)을 구성합니다.<br />
호스트는 RHEL 9, 클라이언트는 Windows 10 기준이며,<br />
2025.11 기준 최신 VSCode 버전 <code class="language-plaintext highlighter-rouge">1.105.1</code> 
(커밋 해시 <code class="language-plaintext highlighter-rouge">7d842fb85a0275a4a8e4d7e040d2625abbf7f084</code>)을 사용합니다.</p>

<h3 id="1-커밋-해시-확인">1. 커밋 해시 확인</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>버전: 1.105.1
커밋: 7d842fb85a0275a4a8e4d7e040d2625abbf7f084
</code></pre></div></div>

<p>VSCode를 실행한 후 도움말 &gt; 정보(About) 에서 버전과 커밋 해시를 확인할 수 있습니다.<br />
클라이언트(Windows)와 호스트(RHEL)의 커밋 해시는 반드시 동일해야 합니다.</p>

<h3 id="2-설치-파일-준비">2. 설치 파일 준비</h3>

<table>
  <thead>
    <tr>
      <th>구분</th>
      <th>파일명</th>
      <th>비고</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>VSCode (RHEL 9)</td>
      <td>code-1.105.1-1760482588.el8.x86_64.rpm</td>
      <td>root 권한으로 설치</td>
    </tr>
    <tr>
      <td>VSCode (Windows 10)</td>
      <td>VSCodeUserSetup-x64-1.105.1.exe</td>
      <td>클라이언트 설치용</td>
    </tr>
    <tr>
      <td>VSCode Server</td>
      <td>vscode-server-linux-x64.tar.gz</td>
      <td>커밋 해시 일치 필수</td>
    </tr>
    <tr>
      <td>VSCode 확장</td>
      <td>*.vsix</td>
      <td>Python, Jupyter, Remote-SSH 등</td>
    </tr>
    <tr>
      <td>Docker Desktop</td>
      <td>Docker Desktop Installer.exe</td>
      <td>Dev Container 실행용</td>
    </tr>
  </tbody>
</table>

<h3 id="3-확장-프로그램-다운로드-vsix-파일">3. 확장 프로그램 다운로드 (<code class="language-plaintext highlighter-rouge">.vsix</code> 파일)</h3>

<p>VSCode에서 확장의 <code class="language-plaintext highlighter-rouge">.vsix</code> 파일을 다운로드합니다.</p>

<ol>
  <li>VSCode를 실행합니다.</li>
  <li>확장 탭에서 필요한 확장을 검색합니다.</li>
  <li>마우스 오른쪽 버튼을 클릭한 후 “<code class="language-plaintext highlighter-rouge">.vsix</code> 파일 다운로드”를 선택합니다.</li>
</ol>

<p>다운로드한 <code class="language-plaintext highlighter-rouge">.vsix</code> 파일은 오프라인 환경에서 확장 설치 시 사용합니다.</p>

<h3 id="4-vscode-server-다운로드">4. VSCode Server 다운로드</h3>

<p>VSCode Server는 클라이언트와 동일한 커밋 해시를 기준으로 다운로드해야 합니다.</p>

<p>다운로드 경로:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://update.code.visualstudio.com/commit/&lt;commit-hash&gt;/server-linux-x64/stable
</code></pre></div></div>

<p>예시:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://update.code.visualstudio.com/commit/7d842fb85a0275a4a8e4d7e040d2625abbf7f084/server-linux-x64/stable
</code></pre></div></div>

<p>다운로드한 파일명은 <code class="language-plaintext highlighter-rouge">vscode-server-linux-x64.tar.gz</code> 입니다.</p>

<hr />

<h3 id="5-호스트-설치-절차-rhel-9">5. 호스트 설치 절차 (RHEL 9)</h3>

<h4 id="1-vscode-설치-root-권한">(1) VSCode 설치 (root 권한)</h4>

<p>사전에 다운로드한 <code class="language-plaintext highlighter-rouge">.rpm</code> 파일을 사용해 VSCode를 설치합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>dnf <span class="nb">install</span> <span class="nt">-y</span> code-1.105.1-1760482588.el8.x86_64.rpm
</code></pre></div></div>

<h4 id="2-사용자별-확장-설치">(2) 사용자별 확장 설치</h4>

<p>사용자 계정에 필요한 확장을 설치합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># .vsix 파일이 위치한 경로로 이동</span>
<span class="nb">cd</span> /path/to/vsix/files

<span class="c"># 확장 설치</span>
code <span class="nt">--install-extension</span> <span class="k">*</span>.vsix

<span class="c"># 설치 확인</span>
code <span class="nt">--list-extensions</span> <span class="nt">--show-versions</span>
</code></pre></div></div>

<h4 id="3-vscode-server-구성">(3) VSCode Server 구성</h4>

<p>폐쇄망에서는 VSCode가 서버 파일을 자동으로 다운로드할 수 없으므로<br />
<code class="language-plaintext highlighter-rouge">vscode-server-linux-x64.tar.gz</code> 파일을 사전에 준비하고 직접 설치해야 합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">COMMIT_HASH</span><span class="o">=</span><span class="s2">"7d842fb85a0275a4a8e4d7e040d2625abbf7f084"</span>
<span class="nb">mkdir</span> <span class="nt">-p</span> ~/.vscode-server/cli/servers/Stable-<span class="k">${</span><span class="nv">COMMIT_HASH</span><span class="k">}</span>/server
<span class="nb">cd</span> ~/.vscode-server/cli/servers/Stable-<span class="k">${</span><span class="nv">COMMIT_HASH</span><span class="k">}</span>/server
<span class="nb">tar</span> <span class="nt">-xzf</span> ~/vscode-server-linux-x64.tar.gz <span class="nt">--strip-components</span><span class="o">=</span>1
<span class="nb">chown</span> <span class="nt">-R</span> <span class="nv">$USER</span>:<span class="nv">$USER</span> ~/.vscode-server
<span class="nb">chmod</span> <span class="nt">-R</span> 755 ~/.vscode-server
</code></pre></div></div>

<p>설치 후 디렉터리 구조 예시:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~/.vscode-server/
 └── cli/
     └── servers/
         └── Stable-7d842fb85a0275a4a8e4d7e040d2625abbf7f084/
             └── server/
                 ├── bin/
                 ├── node
                 ├── extensions/
                 ├── out/
                 ├── package.json
                 └── product.json
</code></pre></div></div>

<blockquote>
  <p>온라인 환경에서 이미 구성된 <code class="language-plaintext highlighter-rouge">~/.vscode-server</code> 디렉터리를 tar로 복사하여 사용 가능<br />
예: <code class="language-plaintext highlighter-rouge">tar -czf vscode-server-copy.tar.gz ~/.vscode-server</code> <br />
→ 오프라인 서버로 옮겨 <code class="language-plaintext highlighter-rouge">tar -xzf vscode-server-copy.tar.gz -C ~</code> 실행</p>
</blockquote>

<hr />

<h3 id="6-클라이언트-설치-절차-windows-10">6. 클라이언트 설치 절차 (Windows 10)</h3>

<h4 id="1-vscode-설치">(1) VSCode 설치</h4>

<p><code class="language-plaintext highlighter-rouge">VSCodeUserSetup-x64-1.105.1.exe</code> 파일을 실행하여 기본 옵션으로 설치합니다.</p>

<h4 id="2-확장-설치">(2) 확장 설치</h4>

<p>다운로드한 <code class="language-plaintext highlighter-rouge">.vsix</code> 파일을 사용해 확장을 설치합니다.</p>

<p>VSCode 실행 → 확장 탭 → 우측 상단 점 3개 메뉴 → “<code class="language-plaintext highlighter-rouge">.vsix</code> 파일에서 설치” 선택 후 파일 지정</p>

<h4 id="3-설정-파일-수정">(3) 설정 파일 수정</h4>

<p>폐쇄망 환경에서 VSCode가 자동 업데이트 및 서버 다운로드를 시도하지 않도록 설정합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Ctrl + Shift + P → Preferences: Open User Settings (JSON)
</code></pre></div></div>

<p>다음 내용을 추가합니다.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"update.mode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"none"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"extensions.autoUpdate"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
  </span><span class="nl">"extensions.autoCheckUpdates"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
  </span><span class="nl">"remote.SSH.useLocalServer"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
  </span><span class="nl">"remote.SSH.localServerDownload"</span><span class="p">:</span><span class="w"> </span><span class="s2">"off"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"remote.SSH.allowLocalServerDownload"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
  </span><span class="nl">"remote.SSH.showLoginTerminal"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
  </span><span class="nl">"http.proxySupport"</span><span class="p">:</span><span class="w"> </span><span class="s2">"off"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<blockquote>
  <p><code class="language-plaintext highlighter-rouge">"allowLocalServerDownload": false</code> 옵션으로 서버 자동 다운로드 차단</p>
</blockquote>

<h4 id="4-ssh-구성-추가">(4) SSH 구성 추가</h4>

<p>SSH 연결 정보를 설정 파일에 추가합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Ctrl + Shift + P → Remote-SSH: Open SSH Configuration File
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">C:\Users\&lt;사용자명&gt;\.ssh\config</code> 파일을 편집합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Host gpu-server
    HostName 192.168.0.50
    User idxkim
    Port 22
</code></pre></div></div>

<h4 id="5-dev-container-연결-폐쇄망-환경-수동-설정">(5) Dev Container 연결 (폐쇄망 환경 수동 설정)</h4>

<p>폐쇄망에서는 VSCode가 Dev Container 서버 파일을 자동으로 다운로드할 수 없습니다.<br />
따라서 한 번 연결 시도 후 생성되는 캐시 경로에 서버 파일을 직접 복사해야 합니다.</p>

<ol>
  <li>Dev Container 연결을 한 번 시도합니다.</li>
  <li>연결이 실패하면 다음 경로가 자동으로 생성됩니다.</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>C:\Users\&lt;username&gt;\AppData\Local\Temp\vsch\serverCache\&lt;commit_id&gt;\
</code></pre></div></div>

<ol>
  <li>위 폴더에 <code class="language-plaintext highlighter-rouge">vscode-server-linux-x64.tar.gz</code> 파일을 복사합니다.</li>
  <li>VSCode에서 재연결 시도 시 해당 파일을 사용해 컨테이너 내부로 서버가 복사됩니다.</li>
</ol>

<blockquote>
  <p>캐시 폴더는 임시 디렉터리이므로 삭제되면 동일한 절차를 다시 수행해야 함</p>
</blockquote>

<hr />

<h5 id="작동-원리-참고">작동 원리 참고</h5>

<ul>
  <li>VSCode는 Dev Container를 연결할 때 <code class="language-plaintext highlighter-rouge">vscode-server-linux-x64.tar.gz</code> 파일을 다운로드하려 시도하며, 그 직전에 <code class="language-plaintext highlighter-rouge">serverCache</code> 폴더를 생성합니다.</li>
  <li>폐쇄망에서는 다운로드가 실패하지만, 폴더는 오류 직전 단계에서 자동으로 만들어집니다.</li>
  <li>이 폴더에 서버 파일을 넣으면 VSCode가 다음 연결 시 해당 파일을 재사용해 <br />
컨테이너 내부에 서버를 설치합니다.</li>
  <li>단, 한 번도 실행을 시도하지 않았다면 <code class="language-plaintext highlighter-rouge">serverCache</code> 폴더는 존재하지 않으며<br />
수동으로 폴더를 만들어도 VSCode가 인식하지 못할 수 있습니다.</li>
</ul>

<hr />

<h2 id="14-1-vscode-비밀번호-자동-입력-ssh-key-설정">14-1. VSCode 비밀번호 자동 입력 (SSH Key 설정)</h2>

<h3 id="1-ssh-키-생성-windows">1. SSH 키 생성 (Windows)</h3>

<p>RSA 키 쌍을 생성합니다.<br />
경로와 패스프레이즈는 기본값을 사용합니다(엔터 3회).</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ssh-keygen</span><span class="w"> </span><span class="nt">-t</span><span class="w"> </span><span class="nx">rsa</span><span class="w"> </span><span class="nt">-b</span><span class="w"> </span><span class="nx">4096</span><span class="w">
</span></code></pre></div></div>

<hr />

<h3 id="2-공개키-확인">2. 공개키 확인</h3>

<p>생성된 공개키를 확인합니다.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-Content</span><span class="w"> </span><span class="nx">~/.ssh/id_rsa.pub</span><span class="w">
</span></code></pre></div></div>

<hr />

<h3 id="3-서버-계정-전환-rhel">3. 서버 계정 전환 (RHEL)</h3>

<p>SSH 키를 등록할 계정으로 전환합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>su - &lt;계정명&gt;
</code></pre></div></div>

<hr />

<h3 id="4-ssh-디렉터리-생성">4. SSH 디렉터리 생성</h3>

<p>SSH 키를 저장할 디렉터리를 생성하고 권한을 설정합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> ~/.ssh
<span class="nb">chmod </span>700 ~/.ssh
</code></pre></div></div>

<hr />

<h3 id="5-공개키-등록">5. 공개키 등록</h3>

<p>Windows에서 확인한 공개키(<code class="language-plaintext highlighter-rouge">ssh-rsa ...</code>)를 <code class="language-plaintext highlighter-rouge">authorized_keys</code> 파일에 추가합니다.<br />
파일 편집 후 <code class="language-plaintext highlighter-rouge">Ctrl+O</code> → <code class="language-plaintext highlighter-rouge">Enter</code>로 저장하고 <code class="language-plaintext highlighter-rouge">Ctrl+X</code>로 종료합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nano ~/.ssh/authorized_keys
</code></pre></div></div>

<hr />

<h3 id="6-파일-권한-설정">6. 파일 권한 설정</h3>

<p>SSH 인증을 위한 파일 권한을 설정합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">chmod </span>600 ~/.ssh/authorized_keys
<span class="nb">chown</span> <span class="nt">-R</span> <span class="nv">$USER</span>:<span class="nv">$USER</span> ~/.ssh
</code></pre></div></div>

<hr />

<h3 id="7-selinux-컨텍스트-복원">7. SELinux 컨텍스트 복원</h3>

<p>SELinux가 활성화된 경우 SSH 디렉터리의 보안 컨텍스트를 복원합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>restorecon <span class="nt">-R</span> <span class="nt">-v</span> ~/.ssh
</code></pre></div></div>

<hr />

<h3 id="8-ssh-설정-파일-열기-windows">8. SSH 설정 파일 열기 (Windows)</h3>

<p>VSCode 명령 팔레트에서 SSH 설정 파일을 엽니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Ctrl + Shift + P → Remote-SSH: Open SSH Configuration File
</code></pre></div></div>

<hr />

<h3 id="9-ssh-호스트-추가">9. SSH 호스트 추가</h3>

<p><code class="language-plaintext highlighter-rouge">C:\Users\&lt;사용자명&gt;\.ssh\config</code> 파일에 서버 정보를 추가합니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Host gpu-server
    HostName 192.168.1.50
    User idxkim
    IdentityFile ~/.ssh/id_rsa
</code></pre></div></div>

<hr />

<h3 id="10-연결-테스트">10. 연결 테스트</h3>

<p>VSCode에서 서버로 연결을 시도합니다.<br />
비밀번호 입력 없이 자동으로 로그인되면 SSH 키 인증이 정상적으로 설정된 것입니다.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Ctrl + Shift + P → Remote-SSH: Connect to Host → gpu-server
</code></pre></div></div>

<hr />

<h2 id="마무리">마무리</h2>

<p>폐쇄망 환경에서 AI Solution 개발환경 구축의 핵심 인프라 구성이 완료되었습니다.<br />
이번 글에서는 GPU 기반 컨테이너 환경과 원격 개발 환경을 중심으로 다음 과정을 진행했습니다.</p>

<ol>
  <li>GPU 드라이버 및 런타임 구성
    <ul>
      <li>NVIDIA 드라이버 설치 및 <code class="language-plaintext highlighter-rouge">nouveau</code> 모듈 비활성화</li>
      <li>NVIDIA Container Toolkit 설치로 GPU 컨테이너 연동 구성</li>
    </ul>
  </li>
  <li>컨테이너 엔진 설치 및 GPU 연동
    <ul>
      <li>Docker 및 Podman 설치</li>
      <li>Docker(runtime configure)와 Podman(CDI generate) 설정을 통한 GPU 접근 구성</li>
      <li>데이터 저장 경로 변경 및 최적화</li>
    </ul>
  </li>
  <li>원격 개발 환경 구성
    <ul>
      <li>VSCode Remote-SSH 오프라인 설치</li>
      <li>Dev Container 수동 연결 및 SSH Key 기반 자동 인증 설정</li>
    </ul>
  </li>
</ol>

<p>이로써 폐쇄망 환경에서도 GPU를 활용한 컨테이너 기반 AI 개발 환경이 구축되었습니다.</p>

<blockquote>
  <p>다음 편에서는 AI Solution 서비스 구성 및 배포로 이어집니다.</p>
</blockquote>]]></content><author><name>indexkim</name></author><category term="docker" /><category term="infra" /><category term="os" /><category term="system" /><summary type="html"><![CDATA[이 글은 폐쇄망 AI Solution 개발환경 구축의 두 번째 편입니다. RHEL 9을 기준으로 하지만, 다른 Linux 배포판에서도 동일한 방식으로 적용할 수 있습니다.]]></summary></entry><entry><title type="html">폐쇄망 AI Solution 개발환경 구축 (1)</title><link href="https://indexkim.github.io//building-air-gapped-ai-solution-dev-env-1/" rel="alternate" type="text/html" title="폐쇄망 AI Solution 개발환경 구축 (1)" /><published>2025-10-30T00:00:00+09:00</published><updated>2025-10-30T00:00:00+09:00</updated><id>https://indexkim.github.io//building-air-gapped-ai-solution-dev-env-1</id><content type="html" xml:base="https://indexkim.github.io//building-air-gapped-ai-solution-dev-env-1/"><![CDATA[<p>폐쇄망(Air-Gapped Network)은 외부 인터넷과 물리적으로 격리된 네트워크 환경을 의미합니다.
주로 금융기관, 공공기관, 연구소 등 민감한 정보를 다루는 환경에서 사용됩니다.</p>

<p>폐쇄망에서는 다음과 같은 제약이 있습니다:</p>

<ul>
  <li>인터넷을 통한 패키지 다운로드 불가 (<code class="language-plaintext highlighter-rouge">apt install</code>, <code class="language-plaintext highlighter-rouge">pip install</code> 등)</li>
  <li>클라우드 서비스 접근 불가 (GitHub, Docker Hub, PyPI 등)</li>
  <li>모든 소프트웨어와 데이터는 USB, 외장 SSD 등 물리적 매체로 전달</li>
</ul>

<p>따라서, 폐쇄망에서 AI Solution 개발환경을 구축하려면<br />
사전에 모든 필요한 소프트웨어, 패키지, 모델 파일을 준비하고,<br />
오프라인 상태에서도 완전히 작동하는 독립적인 환경을 만들어야 합니다.</p>

<p>이 글은 실제 폐쇄망 환경 구축 경험을 바탕으로 작성되었습니다.<br />
RHEL 9을 기준으로 하지만, 다른 Linux 배포판에서도 동일한 방식으로 적용할 수 있습니다.</p>

<blockquote>
  <p>특정 조직의 내부 시스템이나 정책, 네트워크 설정은 포함하지 않으며<br />
모든 내용은 공개된 기술과 일반적인 Linux 환경을 기반으로 구성되었습니다.</p>
</blockquote>

<hr />

<h2 id="1-준비물">1. 준비물</h2>

<h3 id="하드웨어">하드웨어</h3>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>GPU 탑재 워크스테이션</td>
      <td>AI 모델 학습 및 추론 환경 구축용 메인 시스템 (서버 역할 가능)</td>
    </tr>
    <tr>
      <td>클라이언트용 노트북 또는 PC</td>
      <td>워크스테이션 제어 및 환경 테스트용</td>
    </tr>
    <tr>
      <td>랜선 (LAN Cable)</td>
      <td>워크스테이션과 클라이언트 간 유선 연결용</td>
    </tr>
    <tr>
      <td>L2 스위치(Layer 2 Switch)</td>
      <td>폐쇄망 내부 네트워크 구성용 (공유기 대신 사용)</td>
    </tr>
    <tr>
      <td>USB 메모리</td>
      <td>RHEL 또는 Ubuntu 설치용 부팅 USB 제작 (16GB 이상 권장)</td>
    </tr>
    <tr>
      <td>외장 SSD 또는 대용량 USB</td>
      <td>오프라인 패키지, Docker 이미지, 모델 파일 전송용 (256GB 이상 권장)</td>
    </tr>
    <tr>
      <td>모니터 / 키보드 / 마우스</td>
      <td>초기 설치 및 설정용 (KVM 스위치 사용 가능)</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="소프트웨어-사전-다운로드-필요">소프트웨어 (사전 다운로드 필요)</h3>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>설명</th>
      <th>용도</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Linux ISO 파일</td>
      <td>RHEL 9.x DVD ISO (~12GB) 또는 Ubuntu 22.04 LTS ISO (~4GB)</td>
      <td>OS 설치 및 오프라인 리포지토리 구성</td>
    </tr>
    <tr>
      <td>USB 부팅 툴</td>
      <td>Rufus (Windows) 또는 Etcher (Mac/Linux)</td>
      <td>설치 USB 제작</td>
    </tr>
    <tr>
      <td>SFTP 클라이언트</td>
      <td>WinSCP 또는 FileZilla</td>
      <td>클라이언트 ↔ 워크스테이션 간 파일 전송</td>
    </tr>
    <tr>
      <td>NVIDIA 드라이버</td>
      <td>GPU 모델에 맞는 <code class="language-plaintext highlighter-rouge">.run</code> 파일 (예: <code class="language-plaintext highlighter-rouge">NVIDIA-Linux-x86_64-550.54.15.run</code>)</td>
      <td>GPU 드라이버 설치</td>
    </tr>
    <tr>
      <td>NVIDIA Container Toolkit</td>
      <td>오프라인 <code class="language-plaintext highlighter-rouge">.rpm</code> 패키지 모음 (예: <code class="language-plaintext highlighter-rouge">libnvidia-container</code>, <code class="language-plaintext highlighter-rouge">nvidia-container-toolkit</code> 등)</td>
      <td>Docker GPU 연동</td>
    </tr>
    <tr>
      <td>Docker 오프라인 패키지</td>
      <td><code class="language-plaintext highlighter-rouge">docker-ce</code>, <code class="language-plaintext highlighter-rouge">docker-ce-cli</code>, <code class="language-plaintext highlighter-rouge">containerd.io</code> (<code class="language-plaintext highlighter-rouge">.rpm</code> 또는 <code class="language-plaintext highlighter-rouge">.deb</code>)</td>
      <td>컨테이너 런타임 설치</td>
    </tr>
    <tr>
      <td>Docker 이미지</td>
      <td>프로젝트용 Docker 이미지를 <code class="language-plaintext highlighter-rouge">.tar</code>로 저장 (예: <code class="language-plaintext highlighter-rouge">idxkim_image:v0.1</code>)</td>
      <td>AI 개발 환경</td>
    </tr>
    <tr>
      <td>VSCode 관련 패키지</td>
      <td>설치 파일(<code class="language-plaintext highlighter-rouge">.exe</code>/<code class="language-plaintext highlighter-rouge">.rpm</code>), 확장(<code class="language-plaintext highlighter-rouge">.vsix</code>), Server 모듈(<code class="language-plaintext highlighter-rouge">vscode-server-linux-x64.tar.gz</code>)</td>
      <td>코드 편집 및 Remote SSH 환경</td>
    </tr>
    <tr>
      <td>사전학습 모델 (선택)</td>
      <td>Hugging Face 등에서 다운로드한 모델 (<code class="language-plaintext highlighter-rouge">.safetensors</code>, <code class="language-plaintext highlighter-rouge">.bin</code> 등)</td>
      <td>AI 모델 학습 및 추론</td>
    </tr>
    <tr>
      <td>추가 개발 도구 (선택)</td>
      <td><code class="language-plaintext highlighter-rouge">git</code>, <code class="language-plaintext highlighter-rouge">vim</code>, <code class="language-plaintext highlighter-rouge">tmux</code> 등의 오프라인 <code class="language-plaintext highlighter-rouge">.rpm</code>/<code class="language-plaintext highlighter-rouge">.deb</code> 패키지</td>
      <td>개발 편의 도구 설치</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>폐쇄망 반입 전 모든 파일의 무결성 검증(해시 체크) 및 보안 스캔을 권장함</p>
</blockquote>

<hr />

<h2 id="2-설치-미디어-준비">2. 설치 미디어 준비</h2>

<h3 id="iso-다운로드">ISO 다운로드</h3>

<ul>
  <li>경로:
<a href="https://developers.redhat.com/products/rhel/download#getredhatenterpriselinux7163">Red Hat Developers 사이트</a>
(공식 포털이 아닌 developers 사이트에서 무료 회원가입 후 다운로드 가능)</li>
  <li>파일 형태: <code class="language-plaintext highlighter-rouge">rhel-9.x-x86_64-dvd.iso</code></li>
  <li>용량: 약 12GB</li>
  <li>
    <p>버전 확인 및 무결성 검증:</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sha256sum </span>rhel-9.x-x86_64-dvd.iso
</code></pre></div>    </div>

    <p>→ 제공된 해시값과 동일한지 확인</p>
  </li>
</ul>

<blockquote>
  <p>Developers용 ISO는 구독 없이도 다운로드 가능하며, 테스트·폐쇄망 개발용으로 적합</p>
</blockquote>

<hr />

<h2 id="3-설치-usb-제작">3. 설치 USB 제작</h2>

<h3 id="도구-rufus">도구: Rufus</h3>

<ul>
  <li><a href="https://rufus.ie">Rufus 공식 사이트</a>에서 다운로드</li>
  <li>ISO 파일(<code class="language-plaintext highlighter-rouge">rhel-9.x-x86_64-dvd.iso</code>) 선택 후 USB 굽기</li>
  <li>
    <p>설정 권장값:</p>

    <ul>
      <li>파티션 방식: GPT</li>
      <li>대상 시스템: UEFI</li>
      <li>파일 시스템: NTFS (4GB 초과 파일 복사 가능)</li>
      <li>빠른 포맷 체크</li>
    </ul>
  </li>
  <li>USB 용량: 최소 16GB 이상 권장</li>
</ul>

<blockquote>
  <p>복사 후 USB에 <code class="language-plaintext highlighter-rouge">EFI</code>, <code class="language-plaintext highlighter-rouge">BaseOS</code>, <code class="language-plaintext highlighter-rouge">AppStream</code> 폴더가 보이면 성공</p>
</blockquote>

<hr />

<h2 id="4-iso-구조-확인">4. ISO 구조 확인</h2>

<h3 id="iso-내부-주요-폴더">ISO 내부 주요 폴더</h3>

<ul>
  <li><code class="language-plaintext highlighter-rouge">/BaseOS</code> – 운영체제 핵심 패키지 (커널, 라이브러리 등)</li>
  <li><code class="language-plaintext highlighter-rouge">/AppStream</code> – 개발 도구 및 응용 프로그램 모듈</li>
</ul>

<p>설치용 ISO를 마운트하면 이 두 폴더를 확인할 수 있습니다.</p>

<p>ISO 파일을 임시 디렉터리에 마운트하여 내부 구조를 확인합니다:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> /mnt/iso
mount <span class="nt">-o</span> loop rhel-9.x-x86_64-dvd.iso /mnt/iso
</code></pre></div></div>

<p>이후 <code class="language-plaintext highlighter-rouge">/mnt/iso/BaseOS</code>와 <code class="language-plaintext highlighter-rouge">/mnt/iso/AppStream</code>을 로컬 리포지토리로 등록하여 <br />
폐쇄망에서도 <code class="language-plaintext highlighter-rouge">dnf install</code>이 가능하도록 구성합니다.</p>

<hr />

<h2 id="5-bios-부팅-및-설치-진입">5. BIOS 부팅 및 설치 진입</h2>

<h3 id="부팅-절차">부팅 절차</h3>

<ol>
  <li>Rufus로 만든 설치 USB를 서버에 꽂기</li>
  <li>BIOS/UEFI 진입 (<code class="language-plaintext highlighter-rouge">DEL</code>, <code class="language-plaintext highlighter-rouge">F2</code>, <code class="language-plaintext highlighter-rouge">F12</code>, <code class="language-plaintext highlighter-rouge">ESC</code> 등 제조사별 키)</li>
  <li>Boot 메뉴에서 USB 선택
    <ul>
      <li>중요: USB가 두 개로 보이면 UEFI: USB 이름 을 선택</li>
      <li>예시: <code class="language-plaintext highlighter-rouge">UEFI: SanDisk</code> (O) vs <code class="language-plaintext highlighter-rouge">SanDisk</code> (X)</li>
      <li>UEFI 모드를 최우선 순위로 설정</li>
    </ul>
  </li>
  <li>메뉴에서 <code class="language-plaintext highlighter-rouge">Install Red Hat Enterprise Linux 9.x</code> 선택</li>
</ol>

<blockquote>
  <p>UEFI 모드 필수 (Legacy BIOS로 설치 시 부팅 문제 발생 가능)</p>
</blockquote>

<hr />

<h2 id="6-설치-ui-단계">6. 설치 UI 단계</h2>

<h3 id="설정-요약">설정 요약</h3>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>선택/설정</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>언어</td>
      <td>English (US)</td>
      <td>설치 중 한글 깨짐 방지</td>
    </tr>
    <tr>
      <td>파티션</td>
      <td>자동(Auto)</td>
      <td>간편하지만 비효율적 ( <code class="language-plaintext highlighter-rouge">/home</code>, <code class="language-plaintext highlighter-rouge">/var</code> 분리되지 않음 )</td>
    </tr>
    <tr>
      <td>네트워크</td>
      <td>비활성화</td>
      <td>폐쇄망 환경이므로 설정 불필요</td>
    </tr>
    <tr>
      <td>설치 타입</td>
      <td>Workstation</td>
      <td>GUI 포함, 초기 환경 구성에 편리함</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">root</code> 계정</td>
      <td>SSH 허용</td>
      <td>원격 관리 편의성 확보</td>
    </tr>
    <tr>
      <td>사용자 계정</td>
      <td>모든 옵션 체크</td>
      <td>관리자 권한 포함</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>파티션은 <code class="language-plaintext highlighter-rouge">/</code>, <code class="language-plaintext highlighter-rouge">/var</code>, <code class="language-plaintext highlighter-rouge">/home</code>을 분리하여 설정하는 것을 권장<br />
로그 누적 및 사용자 데이터로 인한 루트 파티션 포화 방지를 위해 구조 분리 필요</p>
</blockquote>

<hr />

<h2 id="7-로컬-리포지토리-등록">7. 로컬 리포지토리 등록</h2>

<p>Linux 패키지 관리자(<code class="language-plaintext highlighter-rouge">dnf</code>, <code class="language-plaintext highlighter-rouge">apt</code> 등)는 일반적으로 인터넷의 원격 저장소(repository)에서 패키지를 다운로드합니다.
폐쇄망 환경에서는 인터넷 접근이 불가능하므로, RHEL 설치 ISO의 <code class="language-plaintext highlighter-rouge">BaseOS</code>와 <code class="language-plaintext highlighter-rouge">AppStream</code> 폴더를 워크스테이션 내부 디스크로 복사하여 로컬 리포지토리를 구성합니다.</p>

<h3 id="오프라인-dnf-repo-구성-절차">오프라인 dnf repo 구성 절차</h3>

<h4 id="1-iso-마운트-및-디렉터리-복사">(1) ISO 마운트 및 디렉터리 복사</h4>

<p>설치 USB 또는 ISO를 임시로 마운트합니다:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> /mnt/iso
mount <span class="nt">-o</span> loop /path/to/rhel-9.x-x86_64-dvd.iso /mnt/iso
</code></pre></div></div>

<p>ISO 내부 디렉터리를 로컬 디스크로 복사합니다:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> /opt/repos
<span class="nb">cp</span> <span class="nt">-r</span> /mnt/iso/BaseOS /opt/repos/
<span class="nb">cp</span> <span class="nt">-r</span> /mnt/iso/AppStream /opt/repos/
</code></pre></div></div>

<p>마운트 해제 후 USB를 제거합니다:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>umount /mnt/iso
</code></pre></div></div>

<hr />

<h4 id="2-로컬-repo-설정-파일-생성">(2) 로컬 repo 설정 파일 생성</h4>

<p>복사한 로컬 디렉터리를 리포지토리로 등록하기 위해 설정 파일을 생성합니다:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>vi /etc/yum.repos.d/local.repo
</code></pre></div></div>

<p>입력 내용:</p>

<div class="language-ini highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">[BaseOS]</span>
<span class="py">name</span><span class="p">=</span><span class="s">RHEL-9-BaseOS</span>
<span class="py">baseurl</span><span class="p">=</span><span class="s">file:///opt/repos/BaseOS</span>
<span class="py">enabled</span><span class="p">=</span><span class="s">1</span>
<span class="py">gpgcheck</span><span class="p">=</span><span class="s">0</span>

<span class="nn">[AppStream]</span>
<span class="py">name</span><span class="p">=</span><span class="s">RHEL-9-AppStream</span>
<span class="py">baseurl</span><span class="p">=</span><span class="s">file:///opt/repos/AppStream</span>
<span class="py">enabled</span><span class="p">=</span><span class="s">1</span>
<span class="py">gpgcheck</span><span class="p">=</span><span class="s">0</span>
</code></pre></div></div>

<hr />

<h4 id="3-repo-인덱스-갱신-및-확인">(3) repo 인덱스 갱신 및 확인</h4>

<p>로컬 리포지토리 설정이 완료되면 패키지 캐시를 갱신하고
등록된 리포지토리 목록을 확인합니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>dnf clean all
<span class="nb">sudo </span>dnf repolist
</code></pre></div></div>

<p>출력 예시:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>repo id                        repo name
BaseOS                         RHEL-9-BaseOS
AppStream                      RHEL-9-AppStream
</code></pre></div></div>

<hr />

<h4 id="4-패키지-설치-테스트">(4) 패키지 설치 테스트</h4>

<p>로컬 리포지토리가 정상적으로 동작하는지 확인하기 위해
간단한 패키지를 설치해봅니다.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>dnf <span class="nb">install</span> <span class="nt">-y</span> vim
</code></pre></div></div>

<p>정상 설치되면 로컬 리포지토리 구성이 완료된 것입니다.</p>

<blockquote>
  <p>USB를 제거해도 완전한 오프라인 환경 유지됨<br />
추후 새 버전 ISO로 업데이트할 때는 <code class="language-plaintext highlighter-rouge">/opt/repos</code> 디렉터리 덮어쓰기</p>
</blockquote>

<hr />

<h2 id="8-폐쇄망-네트워크-설정">8. 폐쇄망 네트워크 설정</h2>

<h3 id="유선-네트워크-구성">유선 네트워크 구성</h3>

<p>워크스테이션과 클라이언트(노트북/PC)는 L2 스위치(Layer 2 Switch) 를 통해 유선(LAN)으로 연결합니다.
L2 스위치는 MAC 주소를 기반으로 프레임을 전달하지만,
사용자가 별도로 MAC 주소를 설정할 필요는 없습니다.
각 장비의 네트워크 인터페이스에 기본 내장된 MAC 정보를 스위치가 자동으로 학습해 처리합니다.
공유기(Router)는 외부망 연결용 장비이므로 폐쇄망 환경에서는 사용하지 않습니다.</p>

<p>구성 예시:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[Workstation]──LAN──┐
                    │  (Switch/Hub)
[Client Laptop]──LAN─┘
</code></pre></div></div>

<hr />

<h3 id="rhel-워크스테이션-ip-설정">RHEL 워크스테이션 IP 설정</h3>

<p>RHEL 9의 Settings → Network → Wired → IPv4 → Manual 에서 아래 값 입력:</p>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>값</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>IPv4 Address</td>
      <td>192.168.0.50</td>
    </tr>
    <tr>
      <td>Subnet Mask</td>
      <td>255.255.255.0</td>
    </tr>
    <tr>
      <td>Gateway</td>
      <td>(비움)</td>
    </tr>
    <tr>
      <td>DNS</td>
      <td>(비움)</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>폐쇄망에서는 외부 네트워크(인터넷)와의 연결이 없으므로<br />
기본 게이트웨이와 DNS는 설정하지 않음</p>
</blockquote>

<hr />

<h3 id="클라이언트-ip-설정윈도우-기준">클라이언트 IP 설정(윈도우 기준)</h3>

<h4 id="1-네트워크-설정-진입">(1) 네트워크 설정 진입</h4>

<ol>
  <li>제어판 → 네트워크 및 공유 센터 (Network and Sharing Center) 클릭</li>
  <li>왼쪽 메뉴의 어댑터 설정 변경(Change adapter settings) 클릭</li>
  <li>이더넷(Ethernet) 아이콘 찾기 (Wi-Fi가 아닌 유선 연결 항목)</li>
</ol>

<hr />

<h4 id="2-속성-변경">(2) 속성 변경</h4>

<ol>
  <li>Ethernet 아이콘 우클릭 → 속성(Properties)</li>
  <li>목록에서 Internet Protocol Version 4 (TCP/IPv4) 선택</li>
  <li>속성(Properties) 버튼 클릭</li>
</ol>

<hr />

<h4 id="3-ip-수동-입력">(3) IP 수동 입력</h4>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>값</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>IP 주소</td>
      <td>192.168.0.51</td>
    </tr>
    <tr>
      <td>서브넷 마스크</td>
      <td>255.255.255.0</td>
    </tr>
    <tr>
      <td>기본 게이트웨이</td>
      <td>비워둠</td>
    </tr>
    <tr>
      <td>DNS 서버</td>
      <td>비워둠</td>
    </tr>
  </tbody>
</table>

<p>확인(OK) → 닫기(Close) 선택</p>

<hr />

<h4 id="4-연결-확인">(4) 연결 확인</h4>

<p>명령 프롬프트(<code class="language-plaintext highlighter-rouge">cmd</code>)를 열고 다음을 입력합니다:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ping 192.168.0.50
</code></pre></div></div>

<p>워크스테이션에서 응답이 오면 네트워크 연결이 완료된 것입니다.</p>

<hr />

<h4 id="5-설정-확인">(5) 설정 확인</h4>

<p>네트워크 설정이 올바르게 적용되었는지 확인합니다:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ipconfig
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">IPv4 Address</code> 항목이 <code class="language-plaintext highlighter-rouge">192.168.0.51</code>로 표시되면 정상 설정된 것입니다.</p>

<hr />

<h2 id="9-ubuntu-vs-rhelred-hat-enterprise-linux">9. Ubuntu vs RHEL(Red Hat Enterprise Linux)</h2>

<p>폐쇄망 환경에서 AI Solution 개발환경 구축 시 운영체제 선택은 전체 복잡도에 직접적인 영향을 줍니다.
Ubuntu와 RHEL은 모두 Linux 계열이지만 철학, 관리 방식, 보안 정책, 지원 체계가 다릅니다.</p>

<table>
  <thead>
    <tr>
      <th>구분</th>
      <th>Ubuntu</th>
      <th>RHEL</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>계열</td>
      <td>Debian 계열 (Community 기반)</td>
      <td>Red Hat 계열 (Enterprise 상용 지원)</td>
    </tr>
    <tr>
      <td>목적</td>
      <td>범용 데스크톱 및 연구/개발 환경</td>
      <td>기업용 서버 및 안정성 중시 환경</td>
    </tr>
    <tr>
      <td>패키지 관리자</td>
      <td><code class="language-plaintext highlighter-rouge">apt</code> (Advanced Package Tool)</td>
      <td><code class="language-plaintext highlighter-rouge">dnf</code> (Dandified YUM)</td>
    </tr>
    <tr>
      <td>패키지 포맷</td>
      <td><code class="language-plaintext highlighter-rouge">.deb</code></td>
      <td><code class="language-plaintext highlighter-rouge">.rpm</code></td>
    </tr>
    <tr>
      <td>대표 무료 대안</td>
      <td>Linux Mint, Debian</td>
      <td>Rocky Linux, AlmaLinux</td>
    </tr>
    <tr>
      <td>라이선스 / 비용</td>
      <td>무료 (Canonical Account 선택사항)</td>
      <td>유료 구독 기반 (Red Hat Subscription)</td>
    </tr>
    <tr>
      <td>LTS 정책</td>
      <td>5년 기본 + 5년 ESM(Extended)</td>
      <td>10년 지원 (기업 고객 대상)</td>
    </tr>
    <tr>
      <td>설치 편의성</td>
      <td>GUI 기반 설치 간편, 바로 사용 가능</td>
      <td>초기 설정은 복잡하지만 재현 가능한 표준 절차 제공</td>
    </tr>
    <tr>
      <td>AI 프레임워크 지원성</td>
      <td>공식 CUDA, PyTorch, TensorFlow 대부분 Ubuntu 기준 배포</td>
      <td>대부분 지원되지만 일부 수동 설치 필요</td>
    </tr>
    <tr>
      <td>패키지 최신성</td>
      <td>최신 버전 빠르게 반영 (Rolling-ish)</td>
      <td>검증된 안정 버전 유지 (Conservative release)</td>
    </tr>
    <tr>
      <td>오프라인 repo 구성</td>
      <td><code class="language-plaintext highlighter-rouge">apt-mirror</code> 사용 (의존성 관리 필요)</td>
      <td>ISO 복사만으로 가능 (패키지는 제한적)</td>
    </tr>
    <tr>
      <td>주 사용처</td>
      <td>연구소, 스타트업, 개발 환경</td>
      <td>대기업, 공공기관, 납품 프로젝트</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="차이점">차이점</h3>

<h4 id="1-설치-및-초기-세팅">(1) 설치 및 초기 세팅</h4>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>Ubuntu</th>
      <th>RHEL</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>설치 난이도</td>
      <td>GUI 기반 설치, 클릭 몇 번으로 완료</td>
      <td>Subscription 등록 또는 ISO 기반 수동 Repo 구성 필요</td>
    </tr>
    <tr>
      <td>폐쇄망 구성</td>
      <td><code class="language-plaintext highlighter-rouge">apt-mirror</code>, <code class="language-plaintext highlighter-rouge">apt-offline</code> 등 별도 도구 필요</td>
      <td>ISO 내 BaseOS/AppStream 복사만으로 오프라인 Repo 구성 가능</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>Ubuntu는 인터넷 저장소 의존 구조로 인해 오프라인 설정이 복잡<br />
(패키지 의존성, GPG 키, 미러 구성 등을 수동으로 준비해야 함)<br />
RHEL은 ISO 내부에 저장소가 포함되어 있어 복사와 등록만으로 오프라인 운용 가능</p>
</blockquote>

<hr />

<h4 id="2-패키지-관리-구조">(2) 패키지 관리 구조</h4>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>Ubuntu</th>
      <th>RHEL</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>저장소 관리</td>
      <td><code class="language-plaintext highlighter-rouge">/etc/apt/sources.list</code> 단일 파일</td>
      <td><code class="language-plaintext highlighter-rouge">/etc/yum.repos.d/*.repo</code> 다중 리포지토리 구조</td>
    </tr>
    <tr>
      <td>구조적 특징</td>
      <td>단일 구성 파일로 관리 단순</td>
      <td>리포지토리 분리로 구성 유연</td>
    </tr>
    <tr>
      <td>장점</td>
      <td>설정이 간단하고 유지보수 용이</td>
      <td>버전 충돌 방지 및 일관성 확보</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>Ubuntu는 단일 저장소 구조로 관리가 직관적<br />
RHEL은 리포지토리 단위 관리로 확장성과 안정성이 높음</p>
</blockquote>

<hr />

<h4 id="3-보안-정책-selinux-vs-apparmor">(3) 보안 정책 (SELinux vs AppArmor)</h4>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>Ubuntu (AppArmor)</th>
      <th>RHEL (SELinux)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>기본 보안 모듈</td>
      <td>AppArmor</td>
      <td>SELinux</td>
    </tr>
    <tr>
      <td>접근 제어 방식</td>
      <td>프로파일 기반 (경로 중심)</td>
      <td>라벨 기반 (보안 컨텍스트 중심)</td>
    </tr>
    <tr>
      <td>특징</td>
      <td>설정이 간단하고 유연, 개발 친화적</td>
      <td>세밀한 제어 가능, 강력한 정책 적용</td>
    </tr>
    <tr>
      <td>단점</td>
      <td>세부 제어 어려움, 기업 보안 표준 미흡</td>
      <td>설정 까다로움, 디버깅 난이도 높음</td>
    </tr>
    <tr>
      <td>폐쇄망 운영 시</td>
      <td>Permission 오류 적음</td>
      <td>Permission denied 자주 발생 (context 문제)</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>AppArmor는 유연하고 개발에 친화적<br />
SELinux는 정밀한 제어와 보안 감사 기능이 강력함<br />
폐쇄망에서는 SELinux를 <code class="language-plaintext highlighter-rouge">Permissive</code> 모드로 완화 운영하는 경우가 많음</p>
</blockquote>

<hr />

<h4 id="4-ai-프레임워크-호환성">(4) AI 프레임워크 호환성</h4>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>Ubuntu</th>
      <th>RHEL</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>공식 지원</td>
      <td>PyTorch, TensorFlow, CUDA 등 대부분 공식 지원</td>
      <td>수동 wheel 설치 필요 (특히 폐쇄망)</td>
    </tr>
    <tr>
      <td>업데이트</td>
      <td>최신 버전 즉시 반영</td>
      <td>안정성 위주, 업데이트 느림</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>Ubuntu는 AI 개발 환경 구성이 용이<br />
RHEL은 장기적인 운영 안정성이 높음</p>
</blockquote>

<hr />

<h4 id="5-오프라인-패키지-리포지토리-구성">(5) 오프라인 패키지 리포지토리 구성</h4>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>Ubuntu</th>
      <th>RHEL</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>구성 도구</td>
      <td><code class="language-plaintext highlighter-rouge">apt-mirror</code>, <code class="language-plaintext highlighter-rouge">apt-offline</code></td>
      <td>ISO(BaseOS/AppStream) 복사로 바로 구성</td>
    </tr>
    <tr>
      <td>복잡도</td>
      <td>의존성 관리 복잡</td>
      <td>구성 간단, 제한적 패키지</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>Ubuntu는 최신 패키지와 다양한 버전을 제공<br />
RHEL은 단순한 구조로 오프라인 환경 구성에 용이</p>
</blockquote>

<hr />

<h4 id="6-컨테이너-관리-docker-vs-podman">(6) 컨테이너 관리 (Docker vs Podman)</h4>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>Ubuntu</th>
      <th>RHEL</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>기본 컨테이너 엔진</td>
      <td>Docker</td>
      <td>Podman</td>
    </tr>
    <tr>
      <td>실행 권한</td>
      <td><code class="language-plaintext highlighter-rouge">root</code> 필요</td>
      <td><code class="language-plaintext highlighter-rouge">rootless</code> 지원 (보안성 우수)</td>
    </tr>
    <tr>
      <td>호환성</td>
      <td>풍부한 이미지 및 Compose 지원</td>
      <td>Docker Compose 별도 설치 필요</td>
    </tr>
    <tr>
      <td>보안</td>
      <td>Docker 데몬 권한 위험</td>
      <td>Podman은 프로세스 격리 강화</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>Ubuntu는 개발 및 배포 환경 구성이 편리<br />
RHEL은 컨테이너 보안성과 격리성이 우수</p>
</blockquote>

<hr />

<h3 id="요약-비교-및-선택-가이드">요약 비교 및 선택 가이드</h3>

<table>
  <thead>
    <tr>
      <th>목적</th>
      <th>추천 OS</th>
      <th>이유</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>AI 개발 환경 (일반)</td>
      <td>Ubuntu LTS</td>
      <td>설치 간단, AI 프레임워크 호환성 우수, 최신 패키지 반영 속도 빠름</td>
    </tr>
    <tr>
      <td>오프라인 repo 구성</td>
      <td>RHEL / Rocky Linux</td>
      <td>ISO 복사만으로 로컬 리포지토리 구성 가능 (의존성 안정적)</td>
    </tr>
    <tr>
      <td>보안 감사 필수 환경</td>
      <td>RHEL / Rocky Linux</td>
      <td>SELinux 기본 활성, 엔터프라이즈 보안 정책 및 인증 기준 충족</td>
    </tr>
    <tr>
      <td>폐쇄망 + AI 개발 병행</td>
      <td>Ubuntu LTS</td>
      <td>패키지 다양성, Docker/CUDA 생태계와 높은 호환성, 개발 생산성 우수</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>Ubuntu는 개발 및 테스트 환경에 적합<br />
RHEL은 보안과 안정성을 요구하는 운영 환경에 적합</p>
</blockquote>

<hr />

<h3 id="apt--dnf-명령어-대응표">APT ↔ DNF 명령어 대응표</h3>

<p>Ubuntu와 RHEL 계열의 가장 큰 차이 중 하나는 패키지 관리 도구입니다.<br />
Ubuntu는 <code class="language-plaintext highlighter-rouge">apt</code>, RHEL은 <code class="language-plaintext highlighter-rouge">dnf</code>를 사용하지만, 명령 체계와 기능은 매우 유사합니다.
아래 표는 동일 작업을 수행할 때의 명령어 대응 관계입니다.</p>

<table>
  <thead>
    <tr>
      <th>목적</th>
      <th>Ubuntu (apt)</th>
      <th>RHEL / CentOS / Rocky (dnf)</th>
      <th>비고</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>패키지 목록 갱신</td>
      <td><code class="language-plaintext highlighter-rouge">sudo apt update</code></td>
      <td><code class="language-plaintext highlighter-rouge">sudo dnf clean all</code><br /><code class="language-plaintext highlighter-rouge">sudo dnf repolist</code></td>
      <td><code class="language-plaintext highlighter-rouge">dnf update</code>는 업그레이드용</td>
    </tr>
    <tr>
      <td>패키지 검색</td>
      <td><code class="language-plaintext highlighter-rouge">apt search docker</code></td>
      <td><code class="language-plaintext highlighter-rouge">dnf search podman</code></td>
      <td>결과는 repo별로 구분 표시</td>
    </tr>
    <tr>
      <td>패키지 설치</td>
      <td><code class="language-plaintext highlighter-rouge">sudo apt install -y docker.io</code></td>
      <td><code class="language-plaintext highlighter-rouge">sudo dnf install -y podman</code></td>
      <td><code class="language-plaintext highlighter-rouge">-y</code> 옵션으로 자동 yes</td>
    </tr>
    <tr>
      <td>패키지 제거</td>
      <td><code class="language-plaintext highlighter-rouge">sudo apt remove -y docker.io</code></td>
      <td><code class="language-plaintext highlighter-rouge">sudo dnf remove -y podman</code></td>
      <td>의존성 자동 제거</td>
    </tr>
    <tr>
      <td>필요없는 패키지 제거</td>
      <td><code class="language-plaintext highlighter-rouge">sudo apt autoremove -y</code></td>
      <td><code class="language-plaintext highlighter-rouge">sudo dnf autoremove -y</code></td>
      <td>미사용 패키지 정리</td>
    </tr>
    <tr>
      <td>전체 업그레이드</td>
      <td><code class="language-plaintext highlighter-rouge">sudo apt upgrade -y</code></td>
      <td><code class="language-plaintext highlighter-rouge">sudo dnf upgrade -y</code></td>
      <td>RHEL은 <code class="language-plaintext highlighter-rouge">update</code> 대신 <code class="language-plaintext highlighter-rouge">upgrade</code></td>
    </tr>
    <tr>
      <td>특정 패키지 정보 확인</td>
      <td><code class="language-plaintext highlighter-rouge">apt show podman</code></td>
      <td><code class="language-plaintext highlighter-rouge">dnf info podman</code></td>
      <td>버전, 의존성 확인</td>
    </tr>
    <tr>
      <td>로컬 파일 직접 설치</td>
      <td><code class="language-plaintext highlighter-rouge">sudo dpkg -i file.deb</code></td>
      <td><code class="language-plaintext highlighter-rouge">sudo rpm -ivh file.rpm</code></td>
      <td>포맷 다름 (.deb vs .rpm)</td>
    </tr>
    <tr>
      <td>GPG 키 추가</td>
      <td><code class="language-plaintext highlighter-rouge">apt-key add key.gpg</code></td>
      <td><code class="language-plaintext highlighter-rouge">rpm --import key.gpg</code></td>
      <td>repo 인증 키 등록</td>
    </tr>
    <tr>
      <td>저장소 추가</td>
      <td><code class="language-plaintext highlighter-rouge">add-apt-repository ppa:example/ppa</code></td>
      <td><code class="language-plaintext highlighter-rouge">dnf config-manager --add-repo URL</code></td>
      <td><code class="language-plaintext highlighter-rouge">.repo</code> 파일 생성됨</td>
    </tr>
    <tr>
      <td>저장소 목록</td>
      <td><code class="language-plaintext highlighter-rouge">/etc/apt/sources.list</code></td>
      <td><code class="language-plaintext highlighter-rouge">/etc/yum.repos.d/*.repo</code></td>
      <td>RHEL은 repo 분리 구조</td>
    </tr>
    <tr>
      <td>패키지 캐시 위치</td>
      <td><code class="language-plaintext highlighter-rouge">/var/cache/apt/archives</code></td>
      <td><code class="language-plaintext highlighter-rouge">/var/cache/dnf</code></td>
      <td>캐시 삭제 가능</td>
    </tr>
    <tr>
      <td>의존성 확인</td>
      <td><code class="language-plaintext highlighter-rouge">apt depends python3</code></td>
      <td><code class="language-plaintext highlighter-rouge">dnf repoquery --requires python3</code></td>
      <td>dnf가 더 세부적</td>
    </tr>
    <tr>
      <td>설치된 패키지 목록</td>
      <td><code class="language-plaintext highlighter-rouge">apt list --installed</code></td>
      <td><code class="language-plaintext highlighter-rouge">dnf list installed</code></td>
      <td>동일 기능</td>
    </tr>
    <tr>
      <td>잠금/병렬 설치 구조</td>
      <td>단일 프로세스</td>
      <td>트랜잭션 기반</td>
      <td>dnf가 더 안정적</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>RHEL 8 이후 <code class="language-plaintext highlighter-rouge">yum</code>은 <code class="language-plaintext highlighter-rouge">dnf</code>로 완전히 대체<br />
Ubuntu는 <code class="language-plaintext highlighter-rouge">.deb</code>, RHEL은 <code class="language-plaintext highlighter-rouge">.rpm</code> 포맷을 사용하므로 교차 설치 불가<br />
폐쇄망 환경에서는 <code class="language-plaintext highlighter-rouge">dnf</code>가 ISO 또는 디렉터리 기반 리포지토리(<code class="language-plaintext highlighter-rouge">baseurl=file:///opt/repos/...</code>) 구성에 유리</p>
</blockquote>

<hr />

<h2 id="마무리">마무리</h2>

<p>폐쇄망 환경에서 AI Solution 개발환경을 구축하는 과정은<br />
OS 설치를 넘어 완전한 독립 생태계 구성을 목표로 합니다.</p>

<p>이번 글에서는 폐쇄망 환경 구축의 기초 단계를 다음과 같이 진행했습니다.</p>

<ol>
  <li>준비 및 OS 설치
    <ul>
      <li>하드웨어·소프트웨어 목록 정리 및 RHEL ISO 기반 부팅 USB 제작</li>
      <li>UEFI 모드 부팅 및 Workstation 설치 완료</li>
    </ul>
  </li>
  <li>오프라인 패키지 저장소 구성
    <ul>
      <li>ISO 내부 BaseOS/AppStream을 로컬 디스크로 복사</li>
      <li><code class="language-plaintext highlighter-rouge">/etc/yum.repos.d/local.repo</code> 설정으로 인터넷 없이 <code class="language-plaintext highlighter-rouge">dnf install</code> 가능한 환경 구축</li>
    </ul>
  </li>
  <li>폐쇄망 네트워크 설정
    <ul>
      <li>Switch/Hub 기반 유선 연결 및 고정 IP 할당</li>
      <li>워크스테이션–클라이언트 간 통신 확인</li>
    </ul>
  </li>
  <li>운영체제 비교 (Ubuntu vs RHEL)
    <ul>
      <li>패키지 관리, 보안 정책, AI 프레임워크 호환성 분석</li>
      <li>폐쇄망 환경에 적합한 OS 선택 기준 제시</li>
    </ul>
  </li>
</ol>

<p>이로써 인터넷 연결 없이도 패키지 설치와 네트워크 통신이 가능한<br />
폐쇄망 기본 인프라 환경이 완성되었습니다.</p>

<blockquote>
  <p>다음 편에서는 컨테이너와 GPU 기반 AI 개발 환경 구축 과정으로 이어집니다.</p>
</blockquote>]]></content><author><name>indexkim</name></author><category term="infra" /><category term="os" /><category term="system" /><summary type="html"><![CDATA[폐쇄망(Air-Gapped Network)은 외부 인터넷과 물리적으로 격리된 네트워크 환경을 의미합니다. 주로 금융기관, 공공기관, 연구소 등 민감한 정보를 다루는 환경에서 사용됩니다.]]></summary></entry><entry><title type="html">PaddleOCR 기반 도메인 특화 OCR fine-tuning</title><link href="https://indexkim.github.io//ocr-fine-tuning/" rel="alternate" type="text/html" title="PaddleOCR 기반 도메인 특화 OCR fine-tuning" /><published>2025-08-14T00:00:00+09:00</published><updated>2025-08-14T00:00:00+09:00</updated><id>https://indexkim.github.io//ocr-fine-tuning</id><content type="html" xml:base="https://indexkim.github.io//ocr-fine-tuning/"><![CDATA[<p>OCR(Optical Character Recognition, 광학 문자 인식)은 이미지나 스캔 문서 속의 문자를 식별해 디지털 텍스트로 변환하는 기술입니다.
단순히 글자를 읽는 기능처럼 보이지만, 실제 구현 과정에서는 다양한 글자 모양·배경·촬영 환경에 대응해야 하므로 
Detection(문자 영역 탐지)과 Recognition(문자열 인식)이라는 두 단계로 나누어 처리합니다.</p>

<p>초기에는 OCR이 이미지를 글자로 변환하는 기술이라고 생각했으나,<br />
실제 fine-tuning을 진행해 보니 Detection과 Recognition 구조 차이, 데이터 라벨 구조,<br />
그리고 환경설정의 까다로움까지 예상보다 깊이 있게 다뤄야 할 영역이 많았습니다.</p>

<p>공개된 OCR 데이터셋은 대부분 일상적인 상황과 표준어 중심으로 구성되어 있어,<br />
특정 환경에서 자주 등장하는 표현이나 어휘를 충분히 반영하지 못하는 경우가 많습니다.<br />
이에 맞춰 한국어 확장 커스텀 사전부터 이미지 생성, 라벨링까지 직접 데이터셋을 제작하고<br />
PaddleOCR 기반으로 fine-tuning을 진행했습니다.</p>

<hr />

<h2 id="text-detection-vs-text-recognition">Text Detection vs Text Recognition</h2>

<table>
  <thead>
    <tr>
      <th>구분</th>
      <th>Text Detection (문자 영역 탐지)</th>
      <th>Text Recognition (문자열 인식)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>역할</td>
      <td>이미지/영상에서 문자가 있는 영역을 찾아 Bounding Box로 표시</td>
      <td>Detection이 찾아준 영역 내부의 문자 내용을 해석</td>
    </tr>
    <tr>
      <td>출력</td>
      <td>위치 정보(x, y, w, h 또는 polygon 좌표)</td>
      <td>문자열(예: “PADDLEOCR”)</td>
    </tr>
    <tr>
      <td>난이도</td>
      <td>다양한 각도·배경·조명에서 텍스트 영역 구분</td>
      <td>다양한 폰트·언어·왜곡·노이즈 대응</td>
    </tr>
    <tr>
      <td>예시</td>
      <td>간판 위치 박스</td>
      <td>박스 내부의 “COFFEE”</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>대부분의 OCR 엔진은 Detection → Recognition 순서로 동작합니다.</p>
</blockquote>

<hr />

<h2 id="paddleocr-vs-easyocr">PaddleOCR vs EasyOCR</h2>

<p>OCR 프레임워크 중 유명한 것은 크게 두 가지가 있습니다.<br />
EasyOCR은 설치와 사용이 쉽습니다. <code class="language-plaintext highlighter-rouge">pip install easyocr</code> 한 줄로 바로 시작할 수 있습니다.<br />
다만, 구조가 단일 파이프라인 형태라 Detection·Recognition을 개별적으로 튜닝하거나,<br />
End-to-End 구조를 쓰기에는 제약이 많습니다.</p>

<p>PaddleOCR은 Baidu에서 만든 PaddlePaddle 프레임워크 기반으로,<br />
Detection·Recognition·End-to-End 모델을 모두 제공합니다.<br />
모델 구조와 학습 설정을 세밀하게 조정할 수 있고, TensorRT/ONNX 변환도 지원합니다.<br />
단점은 설치가 복잡하고, 문서가 중국어 중심이라 처음 진입 장벽이 높습니다.</p>

<p>이번 프로젝트의 목적이 특정 도메인에서의 고정밀 fine-tuning이었기 때문에,<br />
구조적 유연성과 End-to-End 지원이 강점인 PaddleOCR을 선택했습니다.</p>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>PaddleOCR</th>
      <th>EasyOCR</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>개발사/배경</td>
      <td>Baidu 주도, PaddlePaddle 프레임워크 기반</td>
      <td>Jaided AI 주도, PyTorch 기반</td>
    </tr>
    <tr>
      <td>구조</td>
      <td>Detection / Recognition / (옵션) Classification 단계별 모듈화</td>
      <td>단일 파이프라인</td>
    </tr>
    <tr>
      <td>언어 지원</td>
      <td>80+ 언어</td>
      <td>약 80여 개 (일부 언어는 인식률 낮음)</td>
    </tr>
    <tr>
      <td>fine-tuning</td>
      <td>공식 문서와 예제 풍부, Detection·Recognition 각각 독립 fine-tuning 가능</td>
      <td>제한적, Recognition 쪽 fine-tuning 위주</td>
    </tr>
    <tr>
      <td>속도/최적화</td>
      <td>경량 모델(PP-OCR Mobile)부터 대형 모델까지 다양, ONNX/TensorRT 변환 용이</td>
      <td>간단 설치 후 바로 사용 가능하지만, 경량화·배포 옵션 적음</td>
    </tr>
    <tr>
      <td>커뮤니티</td>
      <td>중국·아시아권에서 매우 활발, GitHub 스타 수 높음</td>
      <td>초기 진입은 쉬우나 유지보수 속도 느림</td>
    </tr>
    <tr>
      <td>장점</td>
      <td>산업 적용 사례 풍부, 대규모 커스터마이징 가능</td>
      <td>설치 간단, 빠른 프로토타이핑</td>
    </tr>
    <tr>
      <td>단점</td>
      <td>초기 설정과 학습 곡선 있음</td>
      <td>대규모·고성능 튜닝에는 제약</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="paddle-계열-패키지">Paddle 계열 패키지</h2>

<p>Paddle 계열은 이름이 비슷해서 처음 접하면 헷갈리기 쉽습니다.</p>

<table>
  <thead>
    <tr>
      <th>이름</th>
      <th>역할</th>
      <th>설치 방식</th>
      <th>비고</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>PaddlePaddle</td>
      <td>프레임워크 코어 (PyTorch, TensorFlow 같은 엔진)</td>
      <td><code class="language-plaintext highlighter-rouge">pip install paddlepaddle</code> 또는 <code class="language-plaintext highlighter-rouge">pip install paddlepaddle-gpu</code></td>
      <td>GPU 쓸 경우 CUDA 버전 맞춰서 GPU 빌드 설치 필수</td>
    </tr>
    <tr>
      <td>PaddleX</td>
      <td>Paddle 기반 AutoML/GUI 툴킷</td>
      <td><code class="language-plaintext highlighter-rouge">pip install paddlex</code></td>
      <td>OCR 포함 여러 작업 지원하지만, 의존성 충돌 잦음. OCR fine-tuning에 필수 아님</td>
    </tr>
    <tr>
      <td>PaddleOCR</td>
      <td>OCR 전용 라이브러리 (GitHub 레포)</td>
      <td><code class="language-plaintext highlighter-rouge">git clone https://github.com/PaddlePaddle/PaddleOCR.git</code></td>
      <td>학습·추론 전체 파이프라인 포함</td>
    </tr>
    <tr>
      <td>paddleocr</td>
      <td>추론용 CLI/경량 패키지</td>
      <td><code class="language-plaintext highlighter-rouge">pip install paddleocr</code></td>
      <td><code class="language-plaintext highlighter-rouge">paddleocr --image_dir img.jpg</code> 같은 추론만 가능. 학습 코드 없음</td>
    </tr>
    <tr>
      <td>ppocr</td>
      <td>PaddleOCR 레포 안의 파이썬 패키지 네임스페이스</td>
      <td>레포 클론 후 <code class="language-plaintext highlighter-rouge">pip install -e .</code></td>
      <td>fine-tuning시 사용하는 학습/추론 핵심 코드</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="paddleocr-fine-tuning-환경-구성-conda-기반">PaddleOCR fine-tuning 환경 구성 (Conda 기반)</h2>

<p>PaddleOCR은 설치 순서를 잘못 잡으면 의존성 충돌이나 버전 불일치가 발생할 수 있습니다.<br />
아래 순서를 반드시 지키는 것이 안전합니다.</p>

<h3 id="1-conda-가상환경-생성">1. Conda 가상환경 생성</h3>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>conda update <span class="nt">-n</span> base <span class="nt">-c</span> defaults conda
conda create <span class="nt">-n</span> paddle <span class="nv">python</span><span class="o">=</span>3.10
conda activate paddle
python <span class="nt">-m</span> pip <span class="nb">install</span> <span class="nt">--upgrade</span> pip setuptools wheel
</code></pre></div></div>

<h3 id="2-paddleocr-설치-순서-중요">2. PaddleOCR 설치 (순서 중요)</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># GPU 환경 기준 (CUDA 11.8 예시)</span>
pip <span class="nb">install </span>paddlepaddle-gpu<span class="o">==</span>2.6.2 <span class="nt">-f</span> https://www.paddlepaddle.org.cn/whl/mkl/avx/stable.html

<span class="c"># CPU만 사용할 경우</span>
<span class="c"># pip install paddlepaddle</span>

<span class="c"># 추론만 쓸 경우</span>
pip <span class="nb">install </span>paddleocr

<span class="c"># 학습 시에는 반드시 GitHub clone + 개발 모드 설치</span>
git clone https://github.com/PaddlePaddle/PaddleOCR.git
<span class="nb">cd </span>PaddleOCR
pip <span class="nb">install</span> <span class="nt">-r</span> requirements.txt
pip <span class="nb">install</span> <span class="nt">-e</span> <span class="nb">.</span>

<span class="c"># (선택) ONNX 변환</span>
pip <span class="nb">install </span>paddle2onnx
</code></pre></div></div>

<h3 id="3-학습-실행-예시">3. 학습 실행 예시</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># REC 예시</span>
python tools/train.py <span class="nt">-c</span> /workspace/idxkim/cfg/rec_svtrnet.yml

<span class="c"># E2E 예시</span>
python tools/train.py <span class="nt">-c</span> /workspace/idxkim/cfg/e2e_r50_vd_pg.yml
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">-c</code> 옵션에는 PaddleOCR의 config 파일 경로를 넣어야 합니다.</li>
  <li>커스텀 데이터셋을 쓰려면 원본 config를 복사 후 데이터 경로만 수정하여 사용합니다.</li>
</ul>

<hr />

<h2 id="설치-시-자주-발생하는-오류">설치 시 자주 발생하는 오류</h2>

<table>
  <thead>
    <tr>
      <th>오류 상황</th>
      <th>원인</th>
      <th>해결 방법</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ModuleNotFoundError: No module named "ppocr"</code></td>
      <td>pip <code class="language-plaintext highlighter-rouge">paddleocr</code>만 설치 후 학습 시도</td>
      <td>GitHub clone + <code class="language-plaintext highlighter-rouge">pip install -e .</code> 실행</td>
    </tr>
    <tr>
      <td>CUDA 버전 불일치</td>
      <td>PaddlePaddle GPU 버전과 CUDA 환경 불일치</td>
      <td>PaddlePaddle 호환표 확인 후 맞는 버전 설치</td>
    </tr>
    <tr>
      <td>CPU/GPU 빌드 동시 설치</td>
      <td><code class="language-plaintext highlighter-rouge">paddlepaddle</code>와 <code class="language-plaintext highlighter-rouge">paddlepaddle-gpu</code> 둘 다 설치</td>
      <td>하나만 남기고 제거</td>
    </tr>
    <tr>
      <td>의존성 충돌</td>
      <td><code class="language-plaintext highlighter-rouge">paddlex</code> 등 불필요 패키지 설치</td>
      <td>pip uninstall paddlex</td>
    </tr>
  </tbody>
</table>

<h3 id="설치-시-핵심-체크리스트">설치 시 핵심 체크리스트</h3>

<ul>
  <li>설치 순서: PaddlePaddle → PaddleOCR → 기타 유틸 순으로 설치</li>
  <li>학습 목적: GitHub clone + pip install -e . 필수</li>
  <li>추론 목적: pip paddleocr만 설치</li>
  <li>문제가 꼬이면 가상환경을 새로 만드는 게 가장 빠름</li>
  <li>GPU 사용 시 반드시 CUDA 버전 호환 여부 확인 (공식 문서 참고)</li>
</ul>

<hr />

<h2 id="docker-컨테이너-기반-학습-환경-구성">Docker 컨테이너 기반 학습 환경 구성</h2>

<p>실무에서 모델 학습 시 이미 빌드된 분석엔진용 Docker 이미지(A 이미지)를 사용합니다.<br />
배포 환경과 동일하게 구성되어 있어, 추가 빌드 없이 바로 컨테이너를 생성할 수 있습니다.</p>

<p>이 방식은 학습 환경과 배포 환경이 완전히 일치하므로,<br />
모델을 분석엔진에 올릴 때 환경 호환성 문제를 최소화할 수 있습니다.<br />
필요에 따라 학습 전용 이미지(B 이미지)를 따로 사용해 실험한 뒤,<br />
완성된 모델만 분석엔진 환경에 반영하기도 합니다.</p>

<hr />

<h3 id="1-배포-환경-기반에서-학습">1. 배포 환경 기반에서 학습</h3>

<ul>
  <li>A 이미지: 실제 제품 배포 시 사용하는 분석엔진 컨테이너의 베이스 이미지</li>
  <li>이 컨테이너에서 바로 학습·검증까지 진행</li>
  <li>장점:
    <ul>
      <li>학습 환경과 배포 환경이 동일</li>
      <li>Ultralytics, SAM, PaddleOCR 등 다른 Vision AI 프레임워크와 공존 가능</li>
      <li>모델 검증, 최적화, 배포까지 한 컨테이너에서 처리 가능</li>
    </ul>
  </li>
</ul>

<hr />

<h3 id="2-학습-전용-이미지에서-학습">2. 학습 전용 이미지에서 학습</h3>

<ul>
  <li>A 이미지와는 별도의 학습 전용 컨테이너</li>
  <li>학습 완료 후 모델 가중치와 설정 파일만 A 이미지 환경에 반영</li>
  <li>장점:
    <ul>
      <li>환경 충돌 최소화</li>
      <li>실험적인 버전 테스트에 유리</li>
    </ul>
  </li>
</ul>

<hr />

<h3 id="컨테이너별-비교">컨테이너별 비교</h3>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>A 이미지 (배포 환경 기반)</th>
      <th>B 이미지 (학습 전용)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>용도</td>
      <td>학습 + 검증 + 최종 배포</td>
      <td>학습 전용, 실험/대체</td>
    </tr>
    <tr>
      <td>구성</td>
      <td>분석엔진 + PaddleOCR + 기타 AI 프레임워크</td>
      <td>CUDA + PaddleOCR 최소 구성</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>학습 완료 모델을 A 이미지에 반영할 때는<br />
가중치 파일 + character_dict + config.yml 세트를 그대로 옮기는 것이 안전합니다.</p>
</blockquote>

<hr />

<h2 id="ocr-데이터셋-생성-과정">OCR 데이터셋 생성 과정</h2>

<p>이번 프로젝트에서는 사전 제작한 한국어 확장 커스텀 사전을 활용하여 도메인 맞춤형 한글 OCR 데이터셋을 직접 생성했습니다.
실제 서비스 환경에서 발생할 수 있는 색상 대비, 폰트 다양성, 배치, 초성 분포, 회전 요소를 반영하여,
모델이 다양한 상황에서도 안정적으로 동작할 수 있도록 설계했습니다.</p>

<hr />

<h3 id="1-hsv-기반-배경전경-색상-선택">1. HSV 기반 배경/전경 색상 선택</h3>

<p>OCR 모델은 배경과 글자색의 대비가 낮으면 인식률이 떨어집니다.<br />
RGB 색상값을 무작위로 지정하는 방식 대신 HSV 색공간에서 색상을 생성하고,<br />
상대 휘도(luminance) 기반으로 최소 대비 기준을 충족하는 전경색을 선택했습니다.</p>

<ul>
  <li>목적: 글자와 배경이 충분히 구분되도록 하여 가독성을 확보</li>
  <li>구현 방식:
    <ol>
      <li>HSV 범위에서 무작위 색상 생성</li>
      <li>배경 휘도 값에 따라 전경색 후보 범위를 다르게 설정</li>
      <li>대비 비율이 <code class="language-plaintext highlighter-rouge">threshold</code> 이상인 색상만 채택</li>
    </ol>
  </li>
  <li>효과: 색상 다양성은 유지하면서도, 글자가 묻히는 경우를 방지</li>
</ul>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">colorsys</span>
<span class="kn">import</span> <span class="nn">random</span>

<span class="k">def</span> <span class="nf">hsv_rand</span><span class="p">(</span><span class="n">h</span><span class="p">,</span> <span class="n">s</span><span class="p">,</span> <span class="n">v</span><span class="p">):</span>
    <span class="n">hh</span><span class="p">,</span> <span class="n">ss</span><span class="p">,</span> <span class="n">vv</span> <span class="o">=</span> <span class="n">random</span><span class="p">.</span><span class="n">uniform</span><span class="p">(</span><span class="o">*</span><span class="n">h</span><span class="p">),</span> <span class="n">random</span><span class="p">.</span><span class="n">uniform</span><span class="p">(</span><span class="o">*</span><span class="n">s</span><span class="p">),</span> <span class="n">random</span><span class="p">.</span><span class="n">uniform</span><span class="p">(</span><span class="o">*</span><span class="n">v</span><span class="p">)</span>
    <span class="n">r</span><span class="p">,</span> <span class="n">g</span><span class="p">,</span> <span class="n">b</span> <span class="o">=</span> <span class="n">colorsys</span><span class="p">.</span><span class="n">hsv_to_rgb</span><span class="p">(</span><span class="n">hh</span><span class="p">,</span> <span class="n">ss</span><span class="p">,</span> <span class="n">vv</span><span class="p">)</span>
    <span class="k">return</span> <span class="nb">tuple</span><span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="mi">255</span><span class="o">*</span><span class="n">x</span><span class="p">)</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="p">(</span><span class="n">r</span><span class="p">,</span> <span class="n">g</span><span class="p">,</span> <span class="n">b</span><span class="p">))</span>

<span class="k">def</span> <span class="nf">calculate_luminance</span><span class="p">(</span><span class="n">rgb</span><span class="p">):</span>
    <span class="n">r</span><span class="p">,</span> <span class="n">g</span><span class="p">,</span> <span class="n">b</span> <span class="o">=</span> <span class="p">[</span><span class="n">c</span><span class="o">/</span><span class="mi">255</span> <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">rgb</span><span class="p">]</span>
    <span class="k">return</span> <span class="mf">0.2126</span><span class="o">*</span><span class="n">r</span> <span class="o">+</span> <span class="mf">0.7152</span><span class="o">*</span><span class="n">g</span> <span class="o">+</span> <span class="mf">0.0722</span><span class="o">*</span><span class="n">b</span>

<span class="k">def</span> <span class="nf">contrast_ratio</span><span class="p">(</span><span class="n">bg</span><span class="p">,</span> <span class="n">fg</span><span class="p">,</span> <span class="n">thresh</span><span class="o">=</span><span class="mf">2.0</span><span class="p">):</span>
    <span class="n">L1</span><span class="p">,</span> <span class="n">L2</span> <span class="o">=</span> <span class="n">calculate_luminance</span><span class="p">(</span><span class="n">bg</span><span class="p">),</span> <span class="n">calculate_luminance</span><span class="p">(</span><span class="n">fg</span><span class="p">)</span>
    <span class="k">return</span> <span class="p">(</span><span class="nb">max</span><span class="p">(</span><span class="n">L1</span><span class="p">,</span> <span class="n">L2</span><span class="p">)</span><span class="o">+</span><span class="mf">0.05</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="nb">min</span><span class="p">(</span><span class="n">L1</span><span class="p">,</span> <span class="n">L2</span><span class="p">)</span><span class="o">+</span><span class="mf">0.05</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="n">thresh</span>
</code></pre></div></div>

<hr />

<h3 id="2-폰트-크기-외곽선-굵기-랜덤화">2. 폰트 크기, 외곽선, 굵기 랜덤화</h3>

<p>한글 OCR에서는 글자 크기와 스타일 변화에 대응할 수 있는 학습이 중요합니다.
고정된 스타일만 학습하면 실제 환경에서 작은 글자나 얇은 글씨를 잘 인식하지 못합니다.</p>

<ul>
  <li>목적: 다양한 글자 크기·굵기·외곽선 스타일에 대응</li>
  <li>구현 방식:
    <ul>
      <li>폰트 크기: 40~100px 범위에서 랜덤</li>
      <li>외곽선(stroke): 두께(0~3px)와 색상 랜덤</li>
    </ul>
  </li>
  <li>효과: 폰트와 크기가 바뀌어도 인식률 유지</li>
</ul>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">PIL</span> <span class="kn">import</span> <span class="n">ImageFont</span>
<span class="n">font_size</span> <span class="o">=</span> <span class="n">random</span><span class="p">.</span><span class="n">randint</span><span class="p">(</span><span class="mi">40</span><span class="p">,</span> <span class="mi">100</span><span class="p">)</span>
<span class="n">font</span> <span class="o">=</span> <span class="n">ImageFont</span><span class="p">.</span><span class="n">truetype</span><span class="p">(</span><span class="n">FONT_PATH</span><span class="p">,</span> <span class="n">font_size</span><span class="p">)</span>
<span class="n">stroke_w</span> <span class="o">=</span> <span class="n">random</span><span class="p">.</span><span class="n">randint</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span>
</code></pre></div></div>

<hr />

<h3 id="3-초성-기반-stratified-split">3. 초성 기반 Stratified Split</h3>

<p>데이터셋을 학습/검증 세트로 나눌 때, 문자 분포가 불균형하면 성능 검증이 왜곡될 수 있습니다.<br />
특히, 자주 등장하지 않는 초성이 검증 세트에서 전부 빠지면 모델이 그 문자를 학습하지 못합니다.</p>

<ul>
  <li>목적: 모든 초성이 학습·검증에 고르게 분포하도록 유지</li>
  <li>구현 방식:
    <ul>
      <li>각 단어의 초성을 추출하여 그룹화</li>
      <li>그룹별로 무작위 분할</li>
    </ul>
  </li>
  <li>효과: 검증 정확도의 신뢰도 향상</li>
</ul>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">CHO</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="s">"ㄱㄲㄴㄷㄸㄹㅁㅂㅃㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎ"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">get_choseong</span><span class="p">(</span><span class="n">ch</span><span class="p">):</span>
    <span class="k">return</span> <span class="n">CHO</span><span class="p">[(</span><span class="nb">ord</span><span class="p">(</span><span class="n">ch</span><span class="p">)</span><span class="o">-</span><span class="nb">ord</span><span class="p">(</span><span class="s">"가"</span><span class="p">))</span><span class="o">//</span><span class="p">(</span><span class="mi">21</span><span class="o">*</span><span class="mi">28</span><span class="p">)]</span> <span class="k">if</span> <span class="s">"가"</span> <span class="o">&lt;=</span> <span class="n">ch</span> <span class="o">&lt;=</span> <span class="s">"힣"</span> <span class="k">else</span> <span class="s">"기타"</span>
</code></pre></div></div>

<hr />

<h3 id="4-detectionrecognition-데이터-동시-생성">4. Detection/Recognition 데이터 동시 생성</h3>

<p>OCR 파이프라인은 문자 영역 탐지(Detection)와 문자열 인식(Recognition) 두 단계로 구성됩니다.
합성 단계에서 두 작업에 필요한 데이터를 동시에 생성하면 효율성이 높아집니다.</p>

<ul>
  <li>목적: 한 번의 합성으로 두 작업의 학습 데이터를 모두 확보</li>
  <li>구현 방식:
    <ul>
      <li>Detection 라벨: polygon 좌표 + 텍스트</li>
      <li>Recognition 라벨: crop 이미지 + 텍스트</li>
    </ul>
  </li>
  <li>효과: 데이터 생성 효율 향상, 두 태스크 간 일관성 유지</li>
</ul>

<hr />

<h3 id="5-회전--직사각형-crop-방식">5. 회전 + 직사각형 crop 방식</h3>

<p>데이터 생성 시 이미지와 라벨 좌표를 동시에 회전하여 augmentation 효과를 주었습니다.
Recognition crop은 회전된 polygon 그대로 잘라내는 사다리꼴이 아니라,
회전된 polygon을 감싸는 axis-aligned 직사각형을 잘라 저장합니다.</p>

<ul>
  <li>목적: 회전된 데이터에서도 라벨 불일치 없이 Recognition 입력을 일정하게 유지</li>
  <li>구현 방식:
    <ol>
      <li>이미지와 polygon 좌표를 동일 각도로 회전</li>
      <li>polygon의 min/max 좌표를 이용해 직사각형 bbox 계산</li>
      <li>bbox 영역을 crop → 필요 시 배경 여백 포함</li>
    </ol>
  </li>
  <li>효과:
    <ul>
      <li>기울어진 글자라도 모델 입력 형태가 일정</li>
      <li>perspective 변환 없이 구현 단순화</li>
    </ul>
  </li>
</ul>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">crop_rotated_bbox</span><span class="p">(</span><span class="n">rotated_img</span><span class="p">,</span> <span class="n">rotated_points</span><span class="p">):</span>
    <span class="n">xs</span> <span class="o">=</span> <span class="p">[</span><span class="n">p</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">rotated_points</span><span class="p">]</span>
    <span class="n">ys</span> <span class="o">=</span> <span class="p">[</span><span class="n">p</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">rotated_points</span><span class="p">]</span>
    <span class="n">xmin</span><span class="p">,</span> <span class="n">xmax</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="nb">min</span><span class="p">(</span><span class="n">xs</span><span class="p">)),</span> <span class="nb">int</span><span class="p">(</span><span class="nb">max</span><span class="p">(</span><span class="n">xs</span><span class="p">))</span>
    <span class="n">ymin</span><span class="p">,</span> <span class="n">ymax</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="nb">min</span><span class="p">(</span><span class="n">ys</span><span class="p">)),</span> <span class="nb">int</span><span class="p">(</span><span class="nb">max</span><span class="p">(</span><span class="n">ys</span><span class="p">))</span>
    <span class="k">return</span> <span class="n">rotated_img</span><span class="p">.</span><span class="n">crop</span><span class="p">((</span><span class="n">xmin</span><span class="p">,</span> <span class="n">ymin</span><span class="p">,</span> <span class="n">xmax</span><span class="p">,</span> <span class="n">ymax</span><span class="p">))</span>
</code></pre></div></div>

<hr />

<h3 id="6-전체-설계-방향">6. 전체 설계 방향</h3>

<ol>
  <li>색상 대비 최적화 → HSV 기반 색상 생성 + 대비 계산으로 가독성 확보</li>
  <li>다양한 시각 스타일 대응 → 폰트 크기·굵기·외곽선 랜덤화</li>
  <li>문자 분포 균형 유지 → 초성 기반 Stratified Split</li>
  <li>멀티태스크 데이터 동기화 → Detection/Recognition 동시 생성</li>
  <li>좌표·입력 형태 안정성 → 회전은 데이터 생성 단계에서 처리, Recognition은 직사각형 crop</li>
</ol>

<hr />

<h2 id="paddleocr-모델-유형-비교">PaddleOCR 모델 유형 비교</h2>

<table>
  <thead>
    <tr>
      <th>유형</th>
      <th>핵심 구조</th>
      <th>장점</th>
      <th>단점</th>
      <th>주요 사용 케이스</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Recognition (SVTR)</td>
      <td>잘라진 텍스트만 인식</td>
      <td>데이터 준비 용이, 빠른 실험</td>
      <td>Detection 품질 의존</td>
      <td>빠른 프로토타이핑</td>
    </tr>
    <tr>
      <td>Detection (DB)</td>
      <td>텍스트 위치 감지</td>
      <td>Recognition 품질 개선, 복잡 배경 강점</td>
      <td>라벨 품질 중요, 학습 시간 증가</td>
      <td>서비스 배포 전 탐지 성능 강화</td>
    </tr>
    <tr>
      <td>End-to-End (PGNet)</td>
      <td>감지+인식 동시 학습</td>
      <td>상호 최적화, 속도·정확도 모두 확보</td>
      <td>학습 난이도·데이터 품질 요구 높음</td>
      <td>고정밀 OCR 서비스</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="1-recognitionsvtr">1. Recognition(SVTR)</h3>

<p>텍스트 크롭 이미지만을 이용해 SVTR 기반 Recognition 모델을 fine-tuning했습니다.
이 방식은 데이터 준비가 비교적 간단하고, 빠르게 실험을 시작할 수 있다는 장점이 있습니다.
다만 Detection 단계에서 텍스트 위치를 잘못 잡으면, 이후 Recognition 성능이 제한되는 한계가 있습니다.
복잡한 배경이나 다양한 배치 구조에서는 Detection 오류가 누적되어 전체 인식률이 떨어질 수 있습니다.</p>

<ul>
  <li>장점
    <ul>
      <li>데이터 준비와 파이프라인 구성이 단순합니다.</li>
      <li>빠른 성능 검증과 파라미터 튜닝에 유리합니다.</li>
    </ul>
  </li>
  <li>단점
    <ul>
      <li>Detection 품질에 의존도가 높습니다.</li>
      <li>실제 서비스 환경의 복잡한 레이아웃에는 취약합니다.</li>
    </ul>
  </li>
</ul>

<hr />

<h3 id="2-detectiondb">2. Detection(DB)</h3>

<p>텍스트 위치를 더 정확하게 찾기 위해 DB(Differentiable Binarization) 기반 Detection 모델을 fine-tuning했습니다.
DB는 속도와 정확도의 균형이 좋아 PaddleOCR에서 가장 널리 사용되는 Detection 구조이며,
이번 fine-tuning에서는 텍스트 탐지 정확도를 높여 Recognition 입력 품질을 개선하는 데 집중했습니다.
Detection 품질이 향상되면서 Recognition 단계의 오류도 크게 줄어들었습니다.</p>

<ul>
  <li>장점
    <ul>
      <li>Recognition 입력 품질이 눈에 띄게 개선됩니다.</li>
      <li>복잡한 레이아웃이나 다양한 배경 조건에서 강해집니다.</li>
      <li>경량·고정밀 버전 모두 지원해 환경에 맞게 선택할 수 있습니다.</li>
    </ul>
  </li>
  <li>단점
    <ul>
      <li>박스 라벨링 품질이 성능에 직접적인 영향을 미칩니다.</li>
      <li>학습 시간이 늘어날 수 있습니다.</li>
    </ul>
  </li>
</ul>

<hr />

<h3 id="3-end-to-endpgnet">3. End-to-End(PGNet)</h3>

<p>Detection과 Recognition을 동시에 학습하는 PGNet 기반 End-to-End 구조를 적용했습니다.
Backbone과 Neck은 공개 checkpoint를 재사용하고, Head는 분리하여 커스텀 사전에 맞춰 새로 학습했습니다.
이 방식은 두 모듈이 상호 보완적으로 최적화되므로 전체 인식 정확도를 극대화할 수 있습니다.
다만 학습 난이도가 높고, 데이터셋 품질 관리와 하이퍼파라미터 설정에 더 많은 노력이 필요합니다.</p>

<ul>
  <li>장점
    <ul>
      <li>Detection과 Recognition이 상호 최적화됩니다.</li>
      <li>파이프라인 전체의 추론 속도와 일관성이 높아집니다.</li>
      <li>라벨링 일관성이 유지되면 재학습 효율이 좋습니다.</li>
    </ul>
  </li>
  <li>단점
    <ul>
      <li>학습 난이도가 높고, 데이터셋 품질 요구 수준이 높습니다.</li>
      <li>모델 구조 변경이나 Head 재학습 과정에서 오류 가능성이 증가합니다.</li>
    </ul>
  </li>
</ul>

<hr />

<h2 id="head를-분리해야-하는-이유">Head를 분리해야 하는 이유</h2>

<p>PaddleOCR에서 fine-tuning을 진행할 때 안정적인 학습을 위해 Head를 분리합니다.<br />
아래에서는 Head를 분리하는 이유와, 적용 시 함께 고려해야 할 사항을 정리했습니다.</p>

<hr />

<h3 id="1-ultralytics와-paddleocr의-checkpoint-로딩-구조-비교">1. Ultralytics와 PaddleOCR의 checkpoint 로딩 구조 비교</h3>

<p>Ultralytics YOLO 계열은 <code class="language-plaintext highlighter-rouge">pretrained=True</code> 상태에서 클래스 수를 변경하면, Head 부분이 자동으로 재초기화되거나 <code class="language-plaintext highlighter-rouge">strict=False</code> 옵션을 통해 shape가 맞지 않는 키를 건너뛸 수 있습니다.
이 방식은 Backbone과 Neck만 그대로 두고 Head는 새로 구성되므로, 모델 구조 변경 시 비교적 유연하게 대처할 수 있습니다.</p>

<p>반면 PaddleOCR의 PGNet은 기본 loader가 checkpoint 전체를 strict하게 읽습니다.
Backbone과 Neck만 로드하는 별도의 스위치가 제공되지 않기 때문에, Head를 분리하지 않으면 shape mismatch 오류가 발생할 가능성이 있습니다.</p>

<hr />

<h3 id="2-ocr에서-head의-특성">2. OCR에서 Head의 특성</h3>

<p>OCR의 Head는 문자 집합(character_dict)과 시퀀스 디코딩 방식에 종속됩니다.<br />
라벨 구성 차이가 있으면 출력 차원 불일치, 분포 불일치로 인해 학습이 불안정해질 수 있습니다.</p>

<hr />

<h3 id="3-head를-분리해야-하는-이유">3. Head를 분리해야 하는 이유</h3>

<ol>
  <li>라벨 공간 불일치
    <ul>
      <li>공개 checkpoint는 영문/범용 데이터 기준으로 학습된 경우가 많습니다.</li>
      <li>이번처럼 한국어 확장 사전을 쓸 경우 기존 Head는 맞지 않을 수 있습니다.</li>
    </ul>
  </li>
  <li>전이학습 단위 차이
    <ul>
      <li>Backbone/Neck은 시각 특징을 추출하므로 재사용 가치가 높지만,</li>
      <li>Head는 문자 클래스 분포와 언어적 패턴을 반영하므로 데이터셋에 맞춘 재학습이 필요합니다.</li>
    </ul>
  </li>
  <li>수렴 안정성
    <ul>
      <li>불일치 Head를 사용하면 초기 학습에서 불필요한 gradient를 소모하고 불안정해질 수 있습니다.</li>
      <li>출력 차원을 맞춘 Head로 시작하면 적은 데이터로도 빠르게 수렴할 수 있습니다.</li>
    </ul>
  </li>
</ol>

<hr />

<h3 id="4-checkpoint-변환-필요성">4. checkpoint 변환 필요성</h3>

<ul>
  <li>PaddleOCR의 사전학습 checkpoint는 <code class="language-plaintext highlighter-rouge">backbone.</code>, <code class="language-plaintext highlighter-rouge">neck.</code>, <code class="language-plaintext highlighter-rouge">head.</code> 네임스페이스로 파라미터가 구분됩니다.</li>
  <li>Head를 재학습하고 Backbone/Neck만 전이학습하려면 checkpoint 변환이 필요합니다.</li>
  <li>변환 시 Head 파라미터를 제거하고 Backbone/Neck만 남긴 새로운 <code class="language-plaintext highlighter-rouge">.pdparams</code> 파일을 생성해야 합니다.</li>
</ul>

<hr />

<h3 id="head-처리-방식-변경-과정에서-겪은-이슈">Head 처리 방식 변경 과정에서 겪은 이슈</h3>

<p>처음에는 다음과 같은 방법을 시도했습니다.</p>
<ul>
  <li>PaddleOCR 내부 loader를 수정하여 <code class="language-plaintext highlighter-rouge">strict=False</code>를 적용</li>
  <li>학습 직전에 state dict에서 Head 부분만 제거하는 커스텀 로직 삽입</li>
</ul>

<p>그러나 다음과 같은 문제가 반복적으로 발생했습니다.</p>
<ul>
  <li>Head 관련 shape mismatch / missing key / unexpected key 오류 발생</li>
  <li>학습 로그에 Head가 scratch(랜덤 초기화)로 붙었다는 메시지가 출력되며 성능 하락</li>
  <li>분산 학습 환경, 재시작, 버전 차이에 따라 결과가 재현되지 않음</li>
</ul>

<p>결국, pretrained checkpoint를 Backbone/Neck 전용으로 변환해 사용하는 것이 가장 안정적이고 재현성이 높았습니다.</p>

<hr />

<h2 id="해결-방법">해결 방법</h2>

<p>아래 스크립트를 사용하면 Head 네임스페이스를 제거한 새로운 checkpoint를 생성할 수 있습니다.<br />
변환된 파일을 YAML 설정의 <code class="language-plaintext highlighter-rouge">Global.pretrained_model</code> 경로에 지정하여 사용합니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># filter_ckpt.py 
</span><span class="kn">import</span> <span class="nn">sys</span> 
<span class="kn">import</span> <span class="nn">paddle</span>

<span class="k">def</span> <span class="nf">filter_head</span><span class="p">(</span><span class="n">in_path</span><span class="p">,</span> <span class="n">out_path</span><span class="p">,</span> <span class="n">drop_prefixes</span><span class="o">=</span><span class="p">(</span><span class="s">"head."</span><span class="p">,)):</span>
    <span class="c1"># PaddleOCR의 .pdparams 로드
</span>    <span class="n">state_dict</span> <span class="o">=</span> <span class="n">paddle</span><span class="p">.</span><span class="n">load</span><span class="p">(</span><span class="n">in_path</span><span class="p">)</span>

    <span class="c1"># head.* 키 제거
</span>    <span class="n">kept</span> <span class="o">=</span> <span class="p">{</span><span class="n">k</span><span class="p">:</span> <span class="n">v</span> <span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">state_dict</span><span class="p">.</span><span class="n">items</span><span class="p">()</span>
            <span class="k">if</span> <span class="ow">not</span> <span class="nb">any</span><span class="p">(</span><span class="n">k</span><span class="p">.</span><span class="n">startswith</span><span class="p">(</span><span class="n">p</span><span class="p">)</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">drop_prefixes</span><span class="p">)}</span>

    <span class="c1"># 저장
</span>    <span class="n">paddle</span><span class="p">.</span><span class="n">save</span><span class="p">(</span><span class="n">kept</span><span class="p">,</span> <span class="n">out_path</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"[filter] saved -&gt; </span><span class="si">{</span><span class="n">out_path</span><span class="si">}</span><span class="s"> "</span>
          <span class="sa">f</span><span class="s">"(kept=</span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">kept</span><span class="p">)</span><span class="si">}</span><span class="s">, dropped=</span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">state_dict</span><span class="p">)</span><span class="o">-</span><span class="nb">len</span><span class="p">(</span><span class="n">kept</span><span class="p">)</span><span class="si">}</span><span class="s">)"</span><span class="p">)</span>

<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">"__main__"</span><span class="p">:</span>
    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">3</span><span class="p">:</span>
        <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Usage: python </span><span class="si">{</span><span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s"> &lt;input.pdparams&gt; &lt;output.pdparams&gt;"</span><span class="p">)</span>
        <span class="n">sys</span><span class="p">.</span><span class="nb">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>

    <span class="n">src</span><span class="p">,</span> <span class="n">dst</span> <span class="o">=</span> <span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span> <span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span>
    <span class="n">filter_head</span><span class="p">(</span><span class="n">src</span><span class="p">,</span> <span class="n">dst</span><span class="p">)</span>
</code></pre></div></div>

<p>실행 예시:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python filter_ckpt.py <span class="se">\</span>
  /workspace/idxkim/pretrained/best_accuracy.pdparams <span class="se">\</span>
  /workspace/idxkim/pretrained/backbone_neck_only.pdparams
</code></pre></div></div>

<p>YAML 설정:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">Global</span><span class="pi">:</span>
  <span class="na">pretrained_model</span><span class="pi">:</span> <span class="s">/workspace/idxkim/pretrained/backbone_neck_only.pdparams</span>
</code></pre></div></div>

<p>Head 제거 확인:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">paddle</span>
<span class="n">st</span> <span class="o">=</span> <span class="n">paddle</span><span class="p">.</span><span class="n">load</span><span class="p">(</span><span class="s">"/workspace/idxkim/pretrained/backbone_neck_only.pdparams"</span><span class="p">)</span>
<span class="k">assert</span> <span class="nb">all</span><span class="p">(</span><span class="ow">not</span> <span class="n">k</span><span class="p">.</span><span class="n">startswith</span><span class="p">(</span><span class="s">"head."</span><span class="p">)</span> <span class="k">for</span> <span class="n">k</span> <span class="ow">in</span> <span class="n">st</span><span class="p">.</span><span class="n">keys</span><span class="p">())</span>
<span class="k">print</span><span class="p">(</span><span class="s">"✅ head.* 없음"</span><span class="p">)</span>
</code></pre></div></div>

<hr />

<h2 id="마무리">마무리</h2>

<p>이번 프로젝트에서 PaddleOCR 기반 fine-tuning을 통해 도메인 특화 OCR 성능을 개선했습니다.
데이터셋 설계와 checkpoint 관리가 학습 안정성과 성능에 미치는 영향을 검증했습니다.</p>

<p>실험 결과, 다음과 같은 접근이 효과적이었습니다.</p>

<ul>
  <li>환경 설정 및 종속성 설치를 표준화하여 재현성 확보</li>
  <li>Head를 분리하고 Backbone/Neck 전용 checkpoint를 구성해 안정적으로 전이학습 진행</li>
  <li>Recognition → Detection → End-to-End 순으로 단계적 fine-tuning 적용</li>
  <li>실험 로그, 하이퍼파라미터, 환경 버전을 체계적으로 기록해 재실행 가능성 보장</li>
</ul>

<p>위 절차를 통해 복잡한 OCR 프레임워크에서도 안정적인 학습 파이프라인을 구성할 수 있었습니다. 
또한, 데이터셋 품질과 checkpoint 전략이 OCR 성능 향상에 직결됨을 확인했습니다.</p>]]></content><author><name>indexkim</name></author><category term="vision-ai" /><category term="model" /><category term="data" /><summary type="html"><![CDATA[OCR(Optical Character Recognition, 광학 문자 인식)은 이미지나 스캔 문서 속의 문자를 식별해 디지털 텍스트로 변환하는 기술입니다. 단순히 글자를 읽는 기능처럼 보이지만, 실제 구현 과정에서는 다양한 글자 모양·배경·촬영 환경에 대응해야 하므로 Detection(문자 영역 탐지)과 Recognition(문자열 인식)이라는 두 단계로 나누어 처리합니다.]]></summary></entry><entry><title type="html">PoC, 데모, 2025 AI EXPO 후기 - 기술 설계와 전시의 경계</title><link href="https://indexkim.github.io//proof-of-concepts-demo-expo-review/" rel="alternate" type="text/html" title="PoC, 데모, 2025 AI EXPO 후기 - 기술 설계와 전시의 경계" /><published>2025-07-25T00:00:00+09:00</published><updated>2025-07-25T00:00:00+09:00</updated><id>https://indexkim.github.io//proof-of-concepts-demo-expo-review</id><content type="html" xml:base="https://indexkim.github.io//proof-of-concepts-demo-expo-review/"><![CDATA[<p>PoC(Proof of Concept)는 AI 프로젝트에서 모델의 실현 가능성과 적용 방향을 검토하는 출발점입니다.
핵심은 실제 환경에서 어떻게 쓰일 수 있고, 얼마나 유의미한 성능을 낼 수 있는지를 입증하는 것입니다.</p>

<p>그런 점에서 PoC, 기술 데모, 그리고 전시용 홍보 영상은 목적은 다르지만 중요한 공통점을 공유합니다.
세 가지 모두, 완성된 제품이 아닌 “기술이 작동한다”는 메시지를 전하는 작업이며,
제약된 시간과 리소스 안에서 납득 가능한 형태로 결과를 구성하고 전달해야 합니다.</p>

<p>결국 핵심은 기능 구현 자체가 아니라, 그 기능을 어떻게 보여주고 해석되게 하느냐입니다.
실제 현장에서는 모델 정확도보다 신뢰성과 시각적 완성도가 더 중요하게 평가되는 경우도 많습니다.</p>

<p>이 글에서는 2025년 기준으로 진행된 몇 가지 실제 PoC와 전시용 기술 데모 사례를 바탕으로,
각 과제에서 어떤 구조를 설계하고, 어떤 방식으로 대응했는지를 정리해 보려 합니다.</p>

<p>먼저 PoC 검토 시 기본적으로 점검하는 항목들을 살펴본 뒤,
각 프로젝트별 흐름과 기술적 선택 과정을 차례로 다뤄보겠습니다.</p>

<hr />

<h2 id="기본-검토-순서">기본 검토 순서</h2>

<p>PoC 진행 전에는 다음 항목을 기준으로 기술적 사전 검토를 수행합니다:</p>

<ol>
  <li>모델이 있는가? (기존 모델 또는 베이스라인 존재 여부)</li>
  <li>데이터셋이 있는가? (고객사 제공 여부 포함)</li>
  <li>라벨링이 되어 있는가? (라벨 품질, 포맷 일치 여부)</li>
  <li>테스트 영상이 있는가? (실제 운영 환경 기반의 입력)</li>
</ol>

<hr />

<h2 id="poc-진행-현황-2025년-기준">PoC 진행 현황 (2025년 기준)</h2>

<table>
  <thead>
    <tr>
      <th>상태</th>
      <th>고객사</th>
      <th>메모</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>PoC 완료 → 계약 <del>예정</del> 완료</td>
      <td>A사</td>
      <td>성능 기준 충족, 고객사 피드백 반영 중</td>
    </tr>
    <tr>
      <td>PoC 진행 중 → MOU 예정</td>
      <td>B사</td>
      <td>기능 요구사항 재정의 중, 추가 테스트 예정</td>
    </tr>
    <tr>
      <td>전시용 데모 및 홍보 영상</td>
      <td>AWS, EXPO</td>
      <td>기능 홍보 중심, 커스텀 적용 아님</td>
    </tr>
  </tbody>
</table>

<p>이제 각 사례를 중심으로, 실제 어떤 기술적 과정을 수행했는지 정리해보겠습니다.</p>

<blockquote>
  <p><em>본 포스팅에 포함된 일부 시각 자료는 실 서비스 관련 내용이 포함되어 있어, 내부 정책 또는 보안상의 이유로 사전 고지 없이 삭제될 수 있습니다.</em></p>
</blockquote>

<hr />

<h2 id="1-poc-완료-사례">1. PoC 완료 사례</h2>

<ul>
  <li>고객사: A사</li>
  <li>상태: <del>PoC 완료, 금주 중 본 계약 조건 협의 예정</del> 계약 완료</li>
  <li>
    <p>비고: 해당 제품의 주 담당은 아니었지만, 일부 기능 PoC를 수행했습니다.</p>
  </li>
  <li>주요 업무:
    <ul>
      <li>고객 요청에 따른 Object Detection, Super Resolution 기능 PoC 수행</li>
      <li>요구 클래스에 대한 데이터셋 확보 및 전처리</li>
      <li>모델 구성 및 파이프라인 설계</li>
      <li>시각화 영상 및 결과물 제작</li>
      <li>리소스 사용량 테스트 및 GPU 견적 대응</li>
      <li>기술 미팅용 보고자료 구성</li>
    </ul>
  </li>
</ul>

<hr />

<h3 id="1-1-object-detection">1-1. Object Detection</h3>

<ul>
  <li>사용 모델:
    <ul>
      <li>Ultralytics YOLOv11</li>
      <li>실험 가능성과 신뢰성을 모두 고려했고, 비교적 빠르게 튜닝 가능한 구조라는 점에서 초기 테스트에 적합하다고 판단했습니다.</li>
    </ul>
  </li>
  <li>
    <p>인바운드 요청: “클래스 B 탐지 가능한가?”<br />
→ 단 1문장 요청이었고, 응답 기한은 3일, 내부 정리는 2.5일 안에 마쳐야 했습니다.</p>
  </li>
  <li>데이터셋 이슈:
    <ul>
      <li>COCO에는 없었고, Object365에는 있었지만 데이터셋 규모가 커서 다운로드 및 전처리에 시간이 많이 소요될 것으로 판단했습니다.</li>
      <li>클래스 B는 일상적인 대상이었으나, 공개된 pretrained 모델 중 해당 클래스를 포함한 모델은 확보되지 않았습니다.</li>
      <li>최종적으로 Roboflow에서 유사 클래스가 포함된 여러 개의 소형 데이터셋을 수집하여 병합 구성했습니다.</li>
      <li>클래스 간 중복 및 누락 여부를 수동으로 점검했고, 라벨 일관성 확보를 위해 일부 샘플은 직접 수정했습니다.</li>
    </ul>
  </li>
  <li>테스트 영상 확보 및 편집:
    <ul>
      <li>실제로 가장 어려운 단계는 적절한 테스트 영상을 확보하는 것이었습니다.</li>
      <li>유튜브 등에서 클래스 B가 명확히 등장하는 장면을 선별하고,<br />
고객 요구조건, 탐지 난이도, 실적용 가능성을 모두 고려해 편집했습니다.</li>
      <li>영상은 탐지가 잘 되도록 조명, 프레임 구성, 해상도 등을 다각도로 조정했습니다.</li>
    </ul>
  </li>
  <li>반복 작업:
    <ul>
      <li>동일 테스트 영상에 대해 다양한 모델 파라미터를 변경하며 반복 실험을 수행했습니다.</li>
      <li>그 과정에서 영상 재편집 → 데이터셋 보완 → 모델 재학습 사이클을 4~5회 반복하며,<br />
탐지 정확도와 시각적 신뢰도를 모두 만족시키도록 성능을 안정화했습니다.</li>
    </ul>
  </li>
  <li>산출물 구성:
    <ul>
      <li>최종 결과물은 다음 세 가지로 구성했습니다.
        <ol>
          <li>“됩니다.”라는 결론 문장</li>
          <li>“됩니다.”의 근거가 되는 시각화 영상</li>
          <li>테스트 환경에서의 리소스 사용 정보 요약본</li>
        </ol>
      </li>
      <li>시각화 영상은 테스트 영상 위에 탐지 결과를 오버레이해 생성했고,<br />
Object Detection 기능이 실환경에서도 충분히 유효하다는 점을 직관적으로 전달할 수 있도록 구성했습니다.</li>
    </ul>
  </li>
</ul>

<hr />

<h3 id="1-2-super-resolution-화질-개선">1-2. Super Resolution (화질 개선)</h3>

<ul>
  <li>사용 모델 및 구성:
    <ul>
      <li>Real-ESRGAN (Super Resolution)</li>
      <li>GFPGAN (Face Enhancement)<br />
→ 기능적으로 구분되며 리소스 특성이 다릅니다.</li>
    </ul>
  </li>
  <li>배경 및 모델 서치:
    <ul>
      <li>해당 주제는 대학원 시절 직접 다룬 경험이 있어,<br />
PoC 시점에서 모델 서치 및 구조 이해에 소요되는 시간을 절약할 수 있었습니다.</li>
      <li>검토 초기부터 Real-ESRGAN + GFPGAN 조합으로 방향을 정하고 테스트에 집중했습니다.</li>
    </ul>
  </li>
  <li>작업 흐름:
    <ul>
      <li>화질 개선 효과를 직관적으로 비교하기 위해 원본 해상도를 인위적으로 낮춘 뒤<br />
전후 영상을 구성하고, 출력 퀄리티와 인식 적합성을 함께 평가했습니다.</li>
      <li>upscale 비율에 따른 성능 차이, 영상 길이에 따른 처리 시간,<br />
단독 사용 대비 병합 사용 시의 리소스 소모를 비교했습니다.</li>
      <li>또한, 고객사 측에서 GPU 견적 문의가 있었기 때문에<br />
Object Detection과 달리 리소스 사용량에 대한 정량 테스트가 특히 중요했습니다.</li>
    </ul>
  </li>
  <li>특이사항:
    <ul>
      <li>일반적인 단일 이미지 추론에서는 비교적 가볍지만,<br />
고해상도 영상의 프레임 전체를 연속적으로 생성하거나,<br />
두 모델을 동시에 사용할 경우 GPU 메모리 사용량과 latency가 급격히 증가합니다.</li>
      <li>특히 GAN 계열 모델은 inference 상황에서도 frame 단위 병렬 처리가 제대로 되지 않으면<br />
OOM 이슈가 발생할 수 있으므로, 분석엔진 인터페이스 설계 시<br />
다른 모델들과의 자원 분배 및 스케줄링 조정이 필수적입니다.</li>
    </ul>
  </li>
  <li>산출물 구성:
    <ul>
      <li>최종 결과물은 다음 세 가지로 구성했습니다.
        <ol>
          <li>“됩니다.”라는 결론 문장</li>
          <li>“됩니다.”의 근거가 되는 전후 비교 영상</li>
          <li>영상 해상도, 길이, 적용 모델별 리소스 사용 요약 리포트</li>
        </ol>
      </li>
      <li>시각화 영상은 화질 저하 전/후를 프레임 단위로 비교할 수 있도록 구성했고,<br />
실제 사용 환경에서의 개선 효과를 직관적으로 보여주는 데 초점을 맞췄습니다.</li>
    </ul>
  </li>
</ul>

<hr />

<h3 id="시스템-구성">시스템 구성</h3>

<blockquote>
  <p>계약 체결이 완료됨에 따라,<br />
협약 기관의 보안 정책에 의거하여 
기존에 공개되었던 일부 내용을 비공개 처리하였습니다.</p>
</blockquote>

<hr />

<h3 id="a사-poc-과제별-기본-검토-항목">A사 PoC 과제별 기본 검토 항목</h3>

<table>
  <thead>
    <tr>
      <th>항목 번호</th>
      <th>검토 항목</th>
      <th>Object Detection</th>
      <th>Super Resolution</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>모델이 있는가?</td>
      <td>X <em>(직접 학습)</em></td>
      <td>O <em>(pretrained)</em></td>
    </tr>
    <tr>
      <td>2</td>
      <td>데이터셋이 있는가?</td>
      <td>X <em>(직접 구축)</em></td>
      <td>X <em>(불필요)</em></td>
    </tr>
    <tr>
      <td>3</td>
      <td>라벨링이 되어 있는가?</td>
      <td>O <em>(Roboflow 다운로드)</em></td>
      <td>X <em>(불필요)</em></td>
    </tr>
    <tr>
      <td>4</td>
      <td>테스트 영상이 있는가?</td>
      <td>X <em>(직접 생성)</em></td>
      <td>X <em>(직접 생성)</em></td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="poc-구성-비교">PoC 구성 비교</h3>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>Object Detection</th>
      <th>Super Resolution</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>테스트 환경 구성 난이도</td>
      <td>영상 수집 및 조건 맞춤 편집 중심</td>
      <td>해상도/길이 조절 중심</td>
    </tr>
    <tr>
      <td>리소스 측정 중요도</td>
      <td>중간</td>
      <td>매우 높음 (GPU 견적과 연동됨)</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="정리">정리</h3>

<p>A사의 Object Detection과 Super Resolution PoC는 모두 사전 모델과 완성된 데이터 없이 시작된 과제로,<br />
Applied AI Engineer가 데이터 확보부터 테스트셋 구성까지 전 과정을 직접 수행한 실무 사례입니다.<br />
<em>(단, 이 과정은 시스템 관점의 End-to-End가 아닌, 모델 파이프라인에 한정된 End-to-End 구조였습니다.)</em></p>

<ul>
  <li>
    <p>Object Detection은 분석엔진 인터페이스가 이미 개발되어 있던 상황에서,<br />
모델 성능 검증과 데이터 파이프라인 설계에 집중한 모델 관점의 End-to-End형 PoC였습니다.<br />
<em>(외부 공개 데이터를 재가공해 학습하고, 테스트셋 설계 및 시각화까지 직접 수행)</em></p>
  </li>
  <li>
    <p>Super Resolution은 학습 없이 pretrained 모델을 활용했지만,<br />
영상 테스트셋은 직접 생성 및 편집하여 화질 향상 효과와 리소스 사용량을 실증적으로 검증했습니다.</p>
  </li>
</ul>

<p>→ 두 과제 모두 기술 검증은 물론, 리소스 요구 수준과 운영 가능성까지 평가한 실무 기반의 PoC로 구성되었습니다.</p>

<p>개인적으로 AI Engineer라면 최소한 모델 단위 End-to-End 구성 역량은 필수라고 생각합니다.<br />
데이터 수집, 정제, 라벨링을 포함한 전처리 단계에 대한 이해와 경험이 부족하면,
PoC 이후 실제 제품화 단계에서 리더나 고객사의 피드백에 적절히 대응하기 어려워질 수 있습니다.
특히 문제의 원인이 데이터에 있을 때, 모델이나 시스템만 조정해서는 해결되지 않는 경우가 많기 때문입니다.</p>

<blockquote>
  <p>PoC 단계에서 데이터 흐름 전반을 직접 점검하고 설계해보는 경험은<br />
AI Engineer의 실무 역량을 근본적으로 끌어올리는 기반이 됩니다.</p>
</blockquote>

<hr />

<h2 id="2-poc-진행-중--mou-예정">2. PoC 진행 중 → MOU 예정</h2>

<ul>
  <li>고객사: B사</li>
  <li>상태: PoC 진행 중, MOU 체결 예정</li>
  <li>적용 방식: 영상 업로드 후 자동 분석</li>
  <li>비고: 최초 해외 레퍼런스로, 실시간 영상 스트리밍 기반이 아닌 동영상 업로드 후 분석하는 후처리 구조</li>
  <li>주요 담당 업무:
    <ul>
      <li>Head Detection 모델 검증 및 탐지 한계 분석</li>
      <li>카운팅 로직 설계 및 구조 전환 (기준선 → Polygon + Hysteresis 기반)</li>
      <li>카운팅 후처리 로직 구현 및 안정성 검토</li>
      <li>분석엔진 내부 인터페이스 및 시각화 연동 구현</li>
      <li>시연용 결과물 및 데모 영상 제작</li>
      <li>기준선/Zone 정의 가이드 제공 및 카메라 설치 위치 조율</li>
      <li>고객사 PoC 현장에 적용된 On-Premise 분석엔진 운영 및 서버 이슈 대응</li>
    </ul>
  </li>
</ul>

<hr />

<h3 id="2-1-object-counting-logic-설계-및-구조-전환">2-1. Object Counting Logic 설계 및 구조 전환</h3>

<h4 id="기반-구조-detection--tracking-결합">기반 구조: Detection + Tracking 결합</h4>

<ul>
  <li>
    <p>본 PoC는 Object Detection과 Object Tracking을 결합한 후처리 구조로,
매 프레임 객체를 감지한 뒤, track_id 기반 이동 경로를 활용하여
객체의 진입/이탈 상태 변화를 판단하고 중복 없이 카운팅합니다.</p>
  </li>
  <li>
    <p>트래킹 알고리즘은 ByteTrack, OC-SORT, BoT-SORT를 검토하였으며,
속도, ID 일관성, 실환경 적합성 기준으로 최종적으로 ByteTrack을 적용하였습니다.</p>
  </li>
</ul>

<hr />

<h4 id="초기-구조-기준선-기반-cross-product-방식">초기 구조: 기준선 기반 Cross Product 방식</h4>

<ul>
  <li>
    <p>PoC 초기 환경은 지하철 개찰구와 유사한 수평 통행 구조로,
객체가 정해진 방향으로만 이동하는 조건이었습니다.
이 구조에서는 기준선을 수평으로 설정하고, 객체 중심 좌표가 선을 어느 방향으로 교차하는지를 cross product로 계산하여 방향을 판별했습니다.
<img src="/assets/posts/6/counting_line.blur.jpg" alt="counting_line_prototype" /></p>
  </li>
  <li>
    <p>동일 객체가 중복 카운트되지 않도록 <code class="language-plaintext highlighter-rouge">track_id</code> 기반으로 기록하여 관리했습니다.</p>
  </li>
</ul>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">cross_product</span> <span class="o">=</span> <span class="n">movement_vec</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">*</span> <span class="n">line_vec</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">-</span> <span class="n">movement_vec</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">*</span> <span class="n">line_vec</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>

<span class="k">if</span> <span class="n">cross_product</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">:</span>
    <span class="n">direction</span> <span class="o">=</span> <span class="s">"IN"</span>
<span class="k">elif</span> <span class="n">cross_product</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">:</span>
    <span class="n">direction</span> <span class="o">=</span> <span class="s">"OUT"</span>
</code></pre></div></div>

<hr />

<h4 id="기준선-기반-구조의-전제-조건">기준선 기반 구조의 전제 조건</h4>

<ul>
  <li>기준선은 수평 또는 수직에 가까운 직선일 것</li>
  <li>객체의 Bbox 크기는 프레임 간에 큰 변동이 없어야 할 것
→ 위 조건이 충족되지 않으면 방향 오판단, 중복 또는 누락 카운팅이 발생할 수 있습니다.</li>
</ul>

<hr />

<h3 id="2-2-구조-전환-hysteresis-기반-polygon-zone-방식">2-2. 구조 전환: Hysteresis 기반 Polygon Zone 방식</h3>

<h4 id="구조-전환-배경">구조 전환 배경</h4>

<ul>
  <li>
    <p>실제 분석 대상은 지하철 계단 환경이었으며, 다음과 같은 문제점이 있었습니다:</p>

    <ul>
      <li>객체가 계단을 오르내리며 기준선을 반복적으로 교차</li>
      <li>Head detection만으로는 기준선 교차 시점을 정확히 포착하기 어려움</li>
      <li>GT 라벨 기준은 Zone 진입 여부였지만, 기준선 기반 로직과 일치하지 않음</li>
    </ul>
  </li>
</ul>

<hr />

<h4 id="hysteresis란">Hysteresis란?</h4>

<p>Hysteresis(이력현상)는 객체가 경계 근처에 있다고 해서 바로 상태를 전환하지 않고,
일정 조건이 충족될 때에만 상태 전이를 허용하는 방식입니다.
이를 통해 짧은 시간 내 진입/이탈 반복이나 오탐을 방지할 수 있습니다.</p>

<hr />

<h4 id="zone-기반-로직-설계">Zone 기반 로직 설계</h4>

<ul>
  <li>아래는 계단에서 Polygon 기반 영역(Hysteresis 적용 포함)을 시각화한 이미지입니다.</li>
</ul>

<p><img src="/assets/posts/6/counting_zone.blur.jpg" alt="counting_zone_prototype" /></p>

<blockquote>
  <p>현재는 모자나 대머리와 같이 특징이 뚜렷하지 않은 객체에 대한 탐지가 어려운 상황이며,<br />
고객사와 MOU 체결 이후, Head Detection 모델 파인튜닝을 통해 개선할 예정입니다.</p>
</blockquote>

<ul>
  <li>계단 상단을 Polygon 영역으로 정의하고,<br />
내부(inner), 외부(outer), 경계 buffer 구간을 설정해 객체의 위치 상태를 판단합니다.</li>
  <li>또한, 이전 상태와의 비교 및 최소 프레임 간격 조건을 함께 고려해 카운팅을 수행합니다.</li>
</ul>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">inner_zone</span><span class="p">.</span><span class="n">contains</span><span class="p">(</span><span class="n">object_point</span><span class="p">):</span>
    <span class="n">new_state</span> <span class="o">=</span> <span class="bp">True</span>
<span class="k">elif</span> <span class="ow">not</span> <span class="n">outer_zone</span><span class="p">.</span><span class="n">contains</span><span class="p">(</span><span class="n">object_point</span><span class="p">):</span>
    <span class="n">new_state</span> <span class="o">=</span> <span class="bp">False</span>
<span class="k">else</span><span class="p">:</span>
    <span class="n">new_state</span> <span class="o">=</span> <span class="n">track_data</span><span class="p">[</span><span class="s">"inside"</span><span class="p">]</span> <span class="k">if</span> <span class="n">track_data</span><span class="p">[</span><span class="s">"inside"</span><span class="p">]</span> <span class="ow">is</span> <span class="ow">not</span> <span class="bp">None</span> <span class="k">else</span> <span class="n">counting_zone</span><span class="p">.</span><span class="n">contains</span><span class="p">(</span><span class="n">object_point</span><span class="p">)</span>
</code></pre></div></div>
<ul>
  <li>프레임 간격 조건:</li>
</ul>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">new_state</span> <span class="o">!=</span> <span class="n">track_data</span><span class="p">[</span><span class="s">"inside"</span><span class="p">]</span> <span class="ow">and</span> <span class="p">(</span><span class="n">frame_index</span> <span class="o">-</span> <span class="n">track_data</span><span class="p">[</span><span class="s">"last_counted_frame"</span><span class="p">]</span> <span class="o">&gt;=</span> <span class="n">frame_threshold</span><span class="p">):</span>
    <span class="p">...</span>
</code></pre></div></div>

<hr />

<h4 id="방향-보정-필요-시">방향 보정 (필요 시)</h4>

<ul>
  <li>Hysteresis 기반 구조는 객체의 존재 여부 판단에는 강점이 있으나,
이동 방향 자체는 제공하지 않기 때문에 필요 시 y축 좌표 변화량을 기준으로 방향을 보정합니다:</li>
</ul>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">y_drift</span> <span class="o">=</span> <span class="n">track_data</span><span class="p">[</span><span class="s">"points"</span><span class="p">][</span><span class="o">-</span><span class="mi">1</span><span class="p">][</span><span class="mi">1</span><span class="p">]</span> <span class="o">-</span> <span class="n">track_data</span><span class="p">[</span><span class="s">"points"</span><span class="p">][</span><span class="o">-</span><span class="mi">5</span><span class="p">][</span><span class="mi">1</span><span class="p">]</span>
<span class="n">direction</span> <span class="o">=</span> <span class="s">"DOWN"</span> <span class="k">if</span> <span class="n">y_drift</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="k">else</span> <span class="s">"UP"</span>
</code></pre></div></div>

<p>이 방식은 수직 이동이 명확한 계단 환경에서 특히 유효하며,
“내려온 사람만 카운트”와 같은 조건 필터링에도 적합합니다.</p>

<hr />

<h3 id="2-3-시각화-및-결과물-구성">2-3. 시각화 및 결과물 구성</h3>

<ul>
  <li>Bounding Box 및 Track ID 시각화</li>
  <li>in1, out2 등 실시간 라벨 표시</li>
  <li>IN/OUT 오버레이 (<code class="language-plaintext highlighter-rouge">IN: 7 OUT: 5</code>)</li>
  <li>최종 카운팅 결과 요약본 (<code class="language-plaintext highlighter-rouge">.json</code>)</li>
</ul>

<hr />

<h3 id="b사-poc-과제별-기본-검토-항목-object-detection-기준">B사 PoC 과제별 기본 검토 항목 (Object Detection 기준)</h3>

<table>
  <thead>
    <tr>
      <th>항목 번호</th>
      <th>검토 항목</th>
      <th>MOU 전 상태</th>
      <th>MOU 후 예상 상태</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>모델이 있는가?</td>
      <td>△ <em>(연구소에서 TensorRT 엔진 전달받음)</em></td>
      <td>O <em>(모자, 대머리 등 파인튜닝 예정)</em></td>
    </tr>
    <tr>
      <td>2</td>
      <td>데이터셋이 있는가?</td>
      <td>△ <em>(일부 확보됨)</em></td>
      <td>△ <em>(전달 가능성 있음 또는 별도 구축 필요)</em></td>
    </tr>
    <tr>
      <td>3</td>
      <td>라벨링이 되어 있는가?</td>
      <td>△ <em>(일부 확보됨)</em></td>
      <td>△ <em>(라벨 정합성 검토 및 보완 필요)</em></td>
    </tr>
    <tr>
      <td>4</td>
      <td>테스트 영상이 있는가?</td>
      <td>△ <em>(일부 확보됨)</em></td>
      <td>O <em>(대량 테스트 영상 전달 예정)</em></td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="정리-1">정리</h3>

<p>본 과제는 지하철 유동인구 분석을 위한 영상 기반 카운팅 PoC로,
시간대에 따라 프레임 내 객체(Bbox) 수가 수백 개 이상 발생하는 환경입니다.
이에 따라 트래킹 및 후처리 병목이 발생할 수 있으며,
성능 최적화 및 리소스 개선은 MOU 체결 이후 단계에서 추진될 예정입니다.</p>

<ul>
  <li>
    <p>초기에 기준선 기반 cross product 로직을 적용했으나,
계단 환경에서는 객체의 반복 교차, ID 변경, 기준 불일치 등의 문제가 발생했습니다.</p>
  </li>
  <li>
    <p>이를 보완하기 위해 Polygon Zone + Hysteresis 기반 구조로 전환하여
상태 유지, 프레임 간 조건, 중복 방지 조건 등을 통합한 더 안정적인 카운팅 구조를 구성했습니다.</p>
  </li>
  <li>
    <p>트래킹 알고리즘은 ByteTrack, OC-SORT, BoT-SORT를 비교 실험하였으며,
ByteTrack이 속도와 ID 일관성 면에서 가장 적합하여 최종적으로 적용되었습니다.</p>
  </li>
</ul>

<p>객체를 탐지한 뒤, 어떻게 “추적하고 판단할 것인지”에 따라
전체 시스템의 신뢰도와 결과 정확도가 크게 달라진다는 점을 보여준 프로젝트였습니다.</p>

<hr />

<h2 id="3-전시용-데모-및-홍보-영상">3. 전시용 데모 및 홍보 영상</h2>

<ul>
  <li>대상: 클라우드 행사(AWS), 산업 전시회(EXPO) 등</li>
  <li>목적: 기술력, 브랜드 이미지 강조</li>
</ul>

<hr />

<h3 id="주요-업무">주요 업무</h3>
<ul>
  <li>시연용 영상에 최적화된 모델 결과 구성</li>
  <li>박스, 색상, 텍스트 등 시각 요소 강조</li>
  <li>프레임 수, 타이밍 등 영상 편집용 포맷 대응</li>
  <li>마케팅/기획팀과 협업하며 연출 반복</li>
</ul>

<hr />

<h3 id="특징">특징</h3>
<ul>
  <li>실제 성능보다 “보여지는 느낌”이 더 중요합니다.</li>
  <li>기술적 난이도는 낮지만 손이 가장 많이 가는 작업입니다.</li>
  <li>모델 설계부터 시각화까지 흐름을 스스로 구성한<br />
Full-cycle Model-level End-to-End 사례입니다.<br />
<em>(이 또한 시스템 단위의 End-to-End는 아닙니다.)</em></li>
</ul>

<hr />

<h3 id="3-1-aws">3-1. AWS</h3>

<ul>
  <li>주제 조건
    <ol>
      <li>제조업, 스마트팩토리 관련</li>
      <li>카운팅 기능 포함</li>
    </ol>
  </li>
  <li>
    <p>ShutterStock에서 후보 영상을 탐색하고, 상사의 컨펌을 받아 영상 구매 후 연구소에 전달하였습니다.<br />
연구소는 라벨링 후 Detection 모델 학습을, 저는 시각화만 담당하기로 되어 있었습니다.</p>
  </li>
  <li>
    <p>그러나 의장님의 피드백:</p>

    <blockquote>
      <p>“bbox 너무 식상해, 색깔도 너무 안 예쁘고.”</p>
    </blockquote>

    <p>→ 해당 피드백으로 기존 박스 기반 구조는 폐기되었고, Segmentation 방식으로 전환되었습니다.<br />
이후 라벨링부터 모델 구조 변경, 시각화까지 모두 단독으로 진행하게 되었습니다.</p>
  </li>
  <li>
    <p>이처럼 PoC 단계에서는 클래스 정의와 구조 자체가 유동적이기 때문에,<br />
라벨링을 외주화하기보다는 AI Engineer가 직접 손을 대는 경우가 많습니다.</p>

    <p>→ 이 사례 역시, 간단한 요청에서 출발했지만 전체 라벨 구조와 모델 파이프라인이 바뀐 대표적인 경우입니다.</p>
  </li>
  <li>
    <p>마케팅팀 요구사항을 반영하며 시각화 수정이 반복되었고,<br />
해당 영상은 행사 발표 자료 및 현장 반복 재생용으로 사용되었습니다.</p>
  </li>
  <li>추가 이슈
    <ul>
      <li>기본 설계는 On-Premise 환경을 기준으로 되어 있었으나,<br />
행사 목적에 맞춰 클라우드 기반 제품으로의 확장 가능성을 보여주기 위해<br />
분석엔진과 웹 인터페이스를 AWS 환경에 임시 배포해 시연을 구성하였습니다.</li>
      <li>추론 속도를 높이고 GPU 사용률을 낮추기 위해 TensorRT 변환을 적용하여,<br />
클라우드 환경에서의 리소스 비용 부담도 일부 완화할 수 있었습니다.</li>
      <li>웹 시연을 대비해 사내 CCTV를 활용하여 보호구 착용 후 데모용 시연 영상을<br />
직접 촬영 및 편집하였습니다.</li>
      <li>AI Backend 담당자가 예비군 훈련으로 부재하여<br />
(지금 생각해보면 고의성이 의심됩니다.)<br />
분석엔진 백업 운영을 본사에서 직접 대응하게 되었습니다.</li>
    </ul>
  </li>
</ul>

<hr />

<h3 id="3-2-ai-expo-전시용-영상">3-2. AI EXPO 전시용 영상</h3>

<ul>
  <li>주제 조건:
    <ol>
      <li>제조업, 스마트팩토리 관련</li>
      <li>카운팅 기능 포함</li>
      <li>가능하다면 로보틱스, 자율주행 관련 포함</li>
    </ol>
  </li>
  <li>전체 구성: 총 3개 영상, 약 1개월간 준비</li>
  <li>작업 흐름:<br />
ShutterStock 영상 후보 탐색 → 상사 및 팀원과 검토 후 구매 → 전처리<br />
→ Segmentation 라벨링 (1000장 이상, 폴리곤 기준)<br />
→ 오버피팅 학습 → 시각화 결과 제작 → 무한 피드백 반영 → 재학습 반복</li>
  <li>작업 특이사항:
    <ul>
      <li>ShutterStock 영상 후보를 고르는 과정부터 많은 시간이 소요되었으며,<br />
영상의 구도, 대상 객체, 연출 가능성 등을 고려해 상사와 팀원까지 모두 참여한 의사결정이 필요했습니다.</li>
      <li>Segmentation 라벨링은 Roboflow에서 제공하는 세미 오토 라벨링을 도입해봤으나,<br />
완전 자동은 불가능했고 후처리와 수작업 보정이 많아 실제 피로도는 수동과 큰 차이가 없었습니다.
<img src="/assets/posts/6/roboflow_1.jpg" alt="Roboflow Segmentation_1" /></li>
      <li>영상별 요구사항이 달라 3개의 모델을 별도 구성해야 했으며,
오버피팅 기반 재학습만 각기 3~4회 이상 반복하였습니다.
이 과정에서 클래스 정의가 바뀌거나, 데이터셋이 수시로 추가됩니다.
<img src="/assets/posts/6/roboflow_2.jpg" alt="Roboflow Segmentation_2" /></li>
      <li>
        <p>그럼에도 오탐이 발생해 시각화 코드에서 특정 ROI 구간을 지정하고,<br />
해당 영역에는 결과를 표시하지 않도록 처리해 영상 완성도를 맞추기도 했습니다.</p>

        <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Define restricted zones (x1, y1, x2, y2)
</span><span class="n">restricted_zones</span> <span class="o">=</span> <span class="p">[</span>
    <span class="p">(</span><span class="mi">678</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">896</span><span class="p">,</span> <span class="mi">81</span><span class="p">),</span>
    <span class="p">(</span><span class="mi">1044</span><span class="p">,</span> <span class="mi">199</span><span class="p">,</span> <span class="mi">1191</span><span class="p">,</span> <span class="mi">273</span><span class="p">),</span>
    <span class="p">(</span><span class="mi">1466</span><span class="p">,</span> <span class="mi">873</span><span class="p">,</span> <span class="mi">1583</span><span class="p">,</span> <span class="mi">983</span><span class="p">)</span>
<span class="p">]</span>

<span class="k">def</span> <span class="nf">is_inside_zone</span><span class="p">(</span><span class="n">box</span><span class="p">,</span> <span class="n">zones</span><span class="p">):</span>
    <span class="n">bx1</span><span class="p">,</span> <span class="n">by1</span><span class="p">,</span> <span class="n">bx2</span><span class="p">,</span> <span class="n">by2</span> <span class="o">=</span> <span class="n">box</span>
    <span class="k">for</span> <span class="n">zone</span> <span class="ow">in</span> <span class="n">zones</span><span class="p">:</span>
        <span class="n">zx1</span><span class="p">,</span> <span class="n">zy1</span><span class="p">,</span> <span class="n">zx2</span><span class="p">,</span> <span class="n">zy2</span> <span class="o">=</span> <span class="n">zone</span>
        <span class="c1"># Check if the box is completely inside the zone
</span>        <span class="k">if</span> <span class="n">zx1</span> <span class="o">&lt;=</span> <span class="n">bx1</span> <span class="ow">and</span> <span class="n">zy1</span> <span class="o">&lt;=</span> <span class="n">by1</span> <span class="ow">and</span> <span class="n">zx2</span> <span class="o">&gt;=</span> <span class="n">bx2</span> <span class="ow">and</span> <span class="n">zy2</span> <span class="o">&gt;=</span> <span class="n">by2</span><span class="p">:</span>
            <span class="k">return</span> <span class="bp">True</span>
    <span class="k">return</span> <span class="bp">False</span>

<span class="c1"># For segmentation results
</span><span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">mask</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">masks</span><span class="p">):</span>
    <span class="c1"># Get mask bounding box
</span>    <span class="n">rows</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nb">any</span><span class="p">(</span><span class="n">mask</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
    <span class="n">cols</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nb">any</span><span class="p">(</span><span class="n">mask</span><span class="p">,</span> <span class="n">axis</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">np</span><span class="p">.</span><span class="nb">any</span><span class="p">(</span><span class="n">rows</span><span class="p">)</span> <span class="ow">and</span> <span class="n">np</span><span class="p">.</span><span class="nb">any</span><span class="p">(</span><span class="n">cols</span><span class="p">):</span>
        <span class="n">y1</span><span class="p">,</span> <span class="n">y2</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">where</span><span class="p">(</span><span class="n">rows</span><span class="p">)[</span><span class="mi">0</span><span class="p">][[</span><span class="mi">0</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">]]</span>
        <span class="n">x1</span><span class="p">,</span> <span class="n">x2</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">where</span><span class="p">(</span><span class="n">cols</span><span class="p">)[</span><span class="mi">0</span><span class="p">][[</span><span class="mi">0</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">]]</span>
            
        <span class="c1"># Skip drawing mask if inside restricted zone
</span>        <span class="k">if</span> <span class="n">is_inside_zone</span><span class="p">((</span><span class="n">x1</span><span class="p">,</span> <span class="n">y1</span><span class="p">,</span> <span class="n">x2</span><span class="p">,</span> <span class="n">y2</span><span class="p">),</span> <span class="n">restricted_zones</span><span class="p">):</span>
            <span class="k">continue</span>
        
    <span class="c1"># Mask drawing code...
</span></code></pre></div>        </div>
      </li>
      <li>협업 없이 전체 과정을 단독 수행하였으며,<br />
특히 1번 영상은 내부 구성원들로부터 완성도에 대한 긍정적인 평가를 받았고,
최종적으로 행사 주요 시연 영상으로 지정되었습니다.</li>
    </ul>
  </li>
</ul>

<hr />

<h3 id="전시용-데모-및-홍보-영상-검토-항목">전시용 데모 및 홍보 영상 검토 항목</h3>

<table>
  <thead>
    <tr>
      <th>항목 번호</th>
      <th>검토 항목</th>
      <th>AWS 데모 (웹 시연용)</th>
      <th>AWS 홍보영상</th>
      <th>AI EXPO 홍보영상</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>모델이 있는가?</td>
      <td>O <em>(모델 보유, TRT 변환 필요)</em></td>
      <td>X <em>(직접 학습)</em></td>
      <td>X <em>(직접 학습)</em></td>
    </tr>
    <tr>
      <td>2</td>
      <td>데이터셋이 있는가?</td>
      <td>O <em>(보호구 클래스별 기존 데이터)</em></td>
      <td>X <em>(주제 기반 수집 및 구성)</em></td>
      <td>X <em>(주제 기반 수집 및 구성)</em></td>
    </tr>
    <tr>
      <td>3</td>
      <td>라벨링이 되어 있는가?</td>
      <td>O <em>(BBox 기반, 일부 백업 참여)</em></td>
      <td>X <em>(Segmentation 직접 수행)</em></td>
      <td>X <em>(Segmentation 직접 수행)</em></td>
    </tr>
    <tr>
      <td>4</td>
      <td>테스트 영상이 있는가?</td>
      <td>X <em>(직접 보호구 착용 후 촬영)</em></td>
      <td>O <em>(학습 영상 = 테스트 영상)</em></td>
      <td>O <em>(학습 영상 = 테스트 영상)</em></td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="세부-구성-비교-요약표">세부 구성 비교 요약표</h3>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>AWS 데모 (웹 시연용)</th>
      <th>AWS 홍보영상</th>
      <th>AI EXPO 홍보영상</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>목적</td>
      <td>분석엔진 시연 (웹/클라우드 기반)</td>
      <td>브랜드 이미지 강조용 시각화 영상 제작</td>
      <td>산업 전시회 전용 시각화 영상 제작 (총 3편)</td>
    </tr>
    <tr>
      <td>모델</td>
      <td>보유 (YOLO 기반, TRT 변환 필요)</td>
      <td>미보유 → Segmentation 직접 학습</td>
      <td>미보유 → Segmentation 직접 학습</td>
    </tr>
    <tr>
      <td>데이터셋</td>
      <td>기존 보호구 클래스 중심</td>
      <td>주제 기반으로 직접 수집 및 구성</td>
      <td>주제 기반으로 직접 수집 및 구성</td>
    </tr>
    <tr>
      <td>라벨링 방식</td>
      <td>기존 BBox (일부 백업 참여)</td>
      <td>Bbox 일부, Segmentation 라벨링 직접 수행</td>
      <td>Segmentation 라벨링 직접 수행 (1000장 이상)</td>
    </tr>
    <tr>
      <td>테스트 영상 구성</td>
      <td>보호구 착용 후 직접 촬영</td>
      <td>학습 영상 = 테스트 영상 (오버피팅 기반)</td>
      <td>학습 영상 = 테스트 영상 (오버피팅 기반)</td>
    </tr>
    <tr>
      <td>시각화 반복</td>
      <td>낮음</td>
      <td>높음 (내부 피드백 반복 적용)</td>
      <td>매우 높음 (3편 각각 반복 피드백, ROI 예외 처리 포함)</td>
    </tr>
    <tr>
      <td>협업 여부</td>
      <td>분석엔진 구축 포함 (AI Backend 협업)</td>
      <td>Detection 연구소 협업, Segmentation 단독 전환</td>
      <td>단독 수행</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="홍보-영상-왜-가장-고생스러울까">홍보 영상, 왜 가장 고생스러울까?</h2>

<h3 id="기능보다-시각-연출이-우선됨">기능보다 시각 연출이 우선됨</h3>
<ul>
  <li>기능 구현 여부보다 시각적 인상과 연출이 우선 기준이 됩니다.</li>
  <li>수 초 단위 영상에서도 컬러, 타이밍, 박스 위치 등 세부 요소에 대한 반복 요청이 발생합니다.</li>
  <li>일부 피드백은 전체 라벨 구조 변경이나 시각화 방식 변경까지 유도합니다.</li>
  <li>제작 중 추가 요청이 들어오는 경우도 잦으며, 일정 변경 없이 요구사항이 누적되는 경향이 있습니다.</li>
</ul>

<hr />

<h3 id="비전문가-중심의-피드백-루프">비전문가 중심의 피드백 루프</h3>
<ul>
  <li>기술적 구현 난이도와 무관하게 결과에 대한 주관적 평가가 이루어집니다.</li>
  <li>예시 피드백:
    <ul>
      <li>“더 극적으로 표현할 수 없나요?”</li>
      <li>“왜 이 장면에서는 객체를 인식하지 못하나요?”</li>
    </ul>
  </li>
  <li>감성적 피드백에 따라 반복적인 수정이 발생하며, 기준이 명확하지 않은 경우가 많습니다.</li>
</ul>

<hr />

<h3 id="반복되는-후속-요청과-품질-기준의-유동성">반복되는 후속 요청과 품질 기준의 유동성</h3>
<ul>
  <li>영상 압축, 배속 조절, 자막 위치, 색상 조정 등 세부 항목에 대한 수정이 반복됩니다.</li>
  <li>실제 모델 성능보다는 시각적 일관성과 만족도를 기준으로 품질이 판단되는 구조입니다.</li>
</ul>

<p>주요 피드백 사례:</p>
<ul>
  <li>“이 부분은 투명하게 해달라” → “더 투명하게 해달라” → “이건 너무 투명하다”</li>
  <li>“이 클래스는 이 색상이 더 나아 보인다”</li>
  <li>“글자가 너무 크다 / 글씨체가 어울리지 않는다”</li>
  <li>“카운팅 숫자가 가려진다 / 위치를 바꿔달라”</li>
  <li>“배속 / 슬로우 효과 추가”, “좌우에 영상 클립 병합”</li>
  <li>“고화질로 변환 가능 여부”, “TV 재생을 고려해 영상 해상도 조정 필요”</li>
</ul>

<blockquote>
  <p>저는 꾸미기에 소질이 없기 때문에, 초반에 시각화 요청을 구체적으로 말해주지 않으면<br />
무한 피드백 지옥에 빠지게 됩니다.</p>
</blockquote>

<hr />

<h2 id="4-2025-ai-expo-후기">4. 2025 AI EXPO 후기</h2>

<p>2025년 AI EXPO에 자사 제품의 기술 담당자로 참가했습니다.
총 3일간 진행된 행사 중 첫날 부스 운영과 현장 대응을 맡았고,
제가 제작한 3종 홍보 영상과 AWS 데모용 웹 시연 영상(실제 보호구 착용 장면 포함)이
전시 부스 내에서 반복 재생되는 형태로 운영되었습니다.</p>

<p>첫날에 배정된 덕분에, 참관객들의 질의응답을 가장 많이 대응할 수 있었고
현장에서 다양한 피드백을 직접 들을 수 있었습니다.
유튜브 라이브 카메라를 피해 다니려 노력했지만 몇 차례 노출되었고,
보도자료 사진에도 제 모습이 실리게 되었습니다.</p>

<p>다른 부스들도 둘러보고 싶었지만, 여유 시간이 모자라
두 군데 정도만 짧게 방문할 수 있었습니다.
이런 전시 행사는 바로 성과로 이어지기보다는,
수개월~1년 뒤에 실질적인 문의로 연결되는 경우가 많습니다.
실제로 이번 EXPO 이후, 벌써 2건의 기업 문의가 유입되기도 했습니다.</p>

<p>저는 공학 석사이자 경영학 학사 출신으로,
기술과 제품 사이의 균형을 항상 고민합니다.<br />
“어떻게 만들어야 잘 팔릴까?”는 제가 AI Engineer로서 꾸준히 갖고 있는 질문이며,<br />
결국 기술은 제품화되고, 선택되어야 살아남는다는 점을 늘 염두에 두고 있습니다.</p>

<p>이번 EXPO 현장에서도 모델 성능보다는 실사용 환경에서의 경험을 중심으로 설명했습니다.
영상 분석 모델은 이미 일정 수준의 정확도에 도달해 있기 때문에,
현장에서는 알람 피로도, 사용자 맞춤 필터링, 도입 이후의 관리 용이성 등이
더 중요한 평가 요소로 작용합니다.</p>

<p>실제로 저는 다음과 같은 점을 강조했습니다:</p>

<ul>
  <li>UI상에서 채널별 / 관리자별 / 모델별 / 클래스별로 세분화된 필터링 가능</li>
  <li>알람 빈도나 대상 조건을 현장 환경에 맞춰 유연하게 조정 가능</li>
  <li>기존 고객사 사례에서 보호구 탐지 → 물류 영역으로 확장 적용이 논의되고 있음</li>
</ul>

<p>기능 설명을 넘어서, 기술의 유연성과 확장 가능성까지 함께 전달하는 데 중점을 두었습니다.
이는 곧 제품 설계 측면에서 실질적인 경쟁력이 무엇인지, 현장의 언어로 풀어내는 작업이기도 했습니다</p>

<p>이번 EXPO는 제품의 정식 런칭 이후 첫 외부 공개 무대였습니다.
내부적으로는 이전부터 납품 경험이 있었지만,
공식적인 전시는 이번이 처음이었기에
제게도 기술적 설명 이상의 의미가 있었습니다.</p>

<p>외국인 방문객의 대응도 중요한 경험이었습니다.
준비되지 않은 상태에서의 즉흥적인 영어 설명은 다소 어려웠고,
다음 행사부터는 기술 포인트를 영어로도 매끄럽게 전달할 수 있도록
사전 준비가 필요하겠다는 점을 느꼈습니다.</p>

<p>돌이켜보면, 첫날에 배정된 것도 어쩌면 의도된 선택이었는지 모릅니다.<br />
팀 내에서 입사 순서상 막내였기 때문이죠.
(물론 나이로는 막내가 아닙니다.)</p>

<p>당시에는 몰랐지만, 시간이 지나고 나니 알게 되는 것들이 있습니다.<br />
그래서 기록이 중요하다는 걸 다시 한번 느꼈고, 지금이라도 남길 수 있어 다행입니다.</p>

<hr />

<h2 id="마무리">마무리</h2>

<p>PoC는 늘 시간과 리소스가 빠듯한 상태에서 시작됩니다.
정해진 모델도, 준비된 데이터셋도 없이, 어떻게든 가능성을 증명해야 하는 과제가 남겨집니다.
그래서 때로는 모델보다 데이터에 더 많은 시간을 쓰게 되고, 설계보다 시각화에 더 많은 공을 들이게 됩니다. 
그 과정은 외롭고 반복적이며, 방향이 맞는지 확신할 수 없는 순간도 많습니다.</p>

<p>그래도 끝까지 손을 놓지 않으면 “여기까지는 도달할 수 있다”는 기준선은 남길 수 있습니다.<br />
누군가에게 보여주기 위해서든, 스스로 확인하기 위해서든 말입니다.
기술은 결국 보여지는 방식으로 이해되고, 설득은 수치보다 맥락과 타이밍에 좌우되는 경우가 많습니다.</p>

<p>전시 영상은 더 명확한 목적과 방향성을 갖고 제작됩니다.
기능 구현보다는 메시지 전달과 연출, 보여주는 방식의 완성도가 핵심입니다.
또한 예측 가능성이 높은 영역이기도 합니다.
결과를 어느 정도 예상하며 만들 수 있고, 요구사항에 맞춰 시각적으로 조정해 나가는 과정에 집중할 수 있습니다.</p>

<p>이번 전시도 그런 흐름으로 마무리되었습니다. 
제가 제작한 영상이 부스에서 재생됐고, 현장에서 여러 방문자들 앞에서 제품 설명을 이어갔습니다.
피곤했지만, 좋은 질문도 몇 가지 오갔습니다.</p>

<p>그리고 다시 PoC 자리로 돌아왔습니다.
이 모든 과정을 마쳤어도 정식 계약으로 이어질지는 미지수입니다.
결과가 확정되지 않으면 남는 건 쓰임을 다한 산출물뿐이며, 그 앞에서 한동안 생각이 머뭅니다.
특히 처음부터 끝까지 혼자 감당한 프로젝트일수록 더 깊게 남습니다.</p>

<p>어떤 프로젝트는 결과로 이어지고, 어떤 프로젝트는 마음에 남은 채 조용히 사라질지도 모릅니다.
그 둘을 지금은 구분할 수 없지만, 제가 할 수 있는 일은 모든 과정을 끝까지 겪어내는 것입니다.</p>

<p>그리고 언젠가 그 경험이 다음 선택의 기준이 될 수 있기를 바랍니다.</p>

<hr />]]></content><author><name>indexkim</name></author><category term="vision-ai" /><category term="model" /><category term="review" /><summary type="html"><![CDATA[PoC(Proof of Concept)는 AI 프로젝트에서 모델의 실현 가능성과 적용 방향을 검토하는 출발점입니다. 핵심은 실제 환경에서 어떻게 쓰일 수 있고, 얼마나 유의미한 성능을 낼 수 있는지를 입증하는 것입니다.]]></summary></entry><entry><title type="html">Vision AI 시스템을 위한 CCTV 설치 및 구성 실전 가이드</title><link href="https://indexkim.github.io//cctv-for-vision-ai/" rel="alternate" type="text/html" title="Vision AI 시스템을 위한 CCTV 설치 및 구성 실전 가이드" /><published>2025-07-18T00:00:00+09:00</published><updated>2025-07-18T00:00:00+09:00</updated><id>https://indexkim.github.io//cctv-for-vision-ai</id><content type="html" xml:base="https://indexkim.github.io//cctv-for-vision-ai/"><![CDATA[<p>Vision AI의 시작점은 모델이 아니라 CCTV입니다.<br />
입력이 불완전하면, 어떤 모델도 기대한 성능을 내기 어렵습니다.</p>

<p>카메라를 설치하는 것 자체보다 중요한 것은,<br />
카메라의 종류, 설치 위치, 화각, 해상도, 전송 방식 등<br />
여러 물리적·기술적 요소를 분석 목적에 맞게 정밀하게 설계하는 것입니다.</p>

<p>적절한 사전 설계 없이 영상만 확보한다고 해서<br />
AI가 객체를 정확히 인식하거나, 원하는 결과를 안정적으로 제공하는 것은 어렵습니다.</p>

<p>이 포스팅은 실제 Vision AI 프로젝트의 설계 및 운영 경험을 바탕으로,<br />
카메라 선택부터 설치 위치, 영상 연결 구조, 분석 설정 구성까지<br />
실무 관점에서 반드시 고려해야 할 내용을 정리해 보았습니다.</p>

<hr />

<h2 id="1-cctv-카메라의-주요-종류">1. CCTV 카메라의 주요 종류</h2>

<p>Vision AI 시스템에 활용되는 CCTV는 설치 환경과 목적에 따라 카메라 형태가 달라집니다.</p>

<p>예를 들어 실내용은 Dome형, 외부 장거리 감시는 Bullet형이 주로 사용되며,<br />
넓은 공간을 커버해야 하는 경우에는 PTZ나 360도 카메라가 적합합니다.</p>

<p>카메라의 형태는 분석 성능뿐 아니라 설치 난이도, 유지 관리에도 영향을 미치므로<br />
환경에 맞는 타입 선택이 필요합니다.</p>

<table>
  <thead>
    <tr>
      <th>종류</th>
      <th>설명</th>
      <th>특징</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Dome형</td>
      <td>반구형 실내용</td>
      <td>은폐에 유리, 깔끔한 외형</td>
    </tr>
    <tr>
      <td>Bullet형</td>
      <td>원통형 외부형</td>
      <td>장거리 감시, 경고 효과</td>
    </tr>
    <tr>
      <td>PTZ형</td>
      <td>회전/줌 가능</td>
      <td>넓은 공간 감시, 제어 필요</td>
    </tr>
    <tr>
      <td>Fisheye형</td>
      <td>단일 어안렌즈 탑재</td>
      <td>초광각, 왜곡 발생</td>
    </tr>
    <tr>
      <td>360도형</td>
      <td>회전형/다중 렌즈</td>
      <td>공간 전체 시야 확보</td>
    </tr>
    <tr>
      <td>Box형</td>
      <td>산업용 고정형</td>
      <td>고해상도, 대형 렌즈</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="2-카메라-스펙이-분석에-미치는-영향">2. 카메라 스펙이 분석에 미치는 영향</h2>

<p>고해상도 카메라는 객체 디테일 확보에 유리하며,<br />
센서 크기가 클수록 야간이나 저조도 환경에서 노이즈를 줄이고 색 재현력을 향상시킬 수 있습니다.</p>

<p>일반적으로 프레임레이트가 지나치게 낮을 경우,<br />
순간적인 이벤트나 객체 추적 성능 저하가 발생할 수 있습니다.<br />
예를 들어, 5fps 이하에서는 빠른 움직임을 놓치기 쉽고,<br />
15fps 이상이면 안정적인 실시간 분석이 가능합니다.</p>

<p>또한 압축방식에 따라 스트림 품질 및 분석 지연시간이 영향을 받습니다.<br />
특히 AI 분석에서는 Keyframe 간격, 압축 artifact 발생 여부 등도 중요한 고려 요소입니다.</p>

<blockquote>
  <p>실무에서는 화질, 성능, 시스템 부하 간의 균형이 중요합니다.</p>
</blockquote>

<p>현재 담당 중인 제품은 FHD 해상도와 10~15fps 환경에 맞춰 설계되어 있으며,<br />
처리 효율성과 분석 안정성을 확보하는 데 적합한 접근으로 평가받고 있습니다.</p>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>설명</th>
      <th>분석 영향</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>해상도</td>
      <td>HD, FHD, 4K 등</td>
      <td>객체 디테일 확보</td>
    </tr>
    <tr>
      <td>센서 크기</td>
      <td>1/3”, 1/2.7”, 1” 등</td>
      <td>저조도 성능, 노이즈 억제, 색 재현력 향상</td>
    </tr>
    <tr>
      <td>프레임레이트</td>
      <td>10 ~ 30fps</td>
      <td>이벤트 감지, 추적 정확도에 영향</td>
    </tr>
    <tr>
      <td>압축방식</td>
      <td>H.264, H.265 등</td>
      <td>스트림 품질, 지연, 압축 artifacts에 영향</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="3-rgbir-촬영-특성과-vision-ai">3. RGB/IR 촬영 특성과 Vision AI</h2>

<p>영상의 색상 정보는 탐지 정확도에 직접적인 영향을 미칩니다.</p>

<p>일반적으로 RGB 카메라가 기본으로 사용되며,<br />
실내외 대부분의 환경에서 안정적인 성능을 보입니다.</p>

<p>RGB 영상은 색상 정보를 그대로 보존하므로,<br />
안전모 색상이나 작업복 구분 등 시각적 세부 요소 분석에 적합합니다.</p>

<p>반면 IR(Infrared) 카메라는 야간 감시나 조도 부족 환경에서<br />
객체의 윤곽이나 움직임을 감지하는 데 강점을 가지며,<br />
보조 수단으로 활용되는 경우가 많습니다.</p>

<p>분석 목적이 색상 인식이라면 RGB가 기본이며,<br />
어두운 환경에서는 IR 카메라를 병행해 사용하는 방식이 효과적입니다.</p>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>RGB 카메라</th>
      <th>IR 카메라</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>색상 정보</td>
      <td>색상 보존</td>
      <td>없음</td>
    </tr>
    <tr>
      <td>야간 성능</td>
      <td>약함(조명 필요)</td>
      <td>강함</td>
    </tr>
    <tr>
      <td>분석 적합도</td>
      <td>색상 분류, 세부 인식</td>
      <td>윤곽 탐지, 야간 감시</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="4-렌즈--화각-유형별-비교">4. 렌즈 &amp; 화각 유형별 비교</h2>

<p>렌즈의 초점 거리는 카메라가 확보할 수 있는<br />
시야의 폭(FOV, Field of View)을 결정하는 핵심 요소입니다.</p>

<p>예를 들어 2.8mm 광각 렌즈는 넓은 영역을 한 번에 촬영할 수 있지만,<br />
객체는 작게 보이고 왜곡이 발생할 수 있습니다.</p>

<p>반면 16mm 이상의 망원 렌즈는 멀리 있는 객체를 크게 포착할 수 있지만,<br />
시야가 좁아 다른 객체를 놓칠 수 있습니다.</p>

<p>설치 환경과 분석 목적에 따라 광각, 표준, 망원 선택이 달라지며,<br />
초점 거리를 수동 조절할 수 있는 Varifocal 렌즈나<br />
원격 제어가 가능한 Motorized 렌즈도 실무에서 자주 활용됩니다.</p>

<table>
  <thead>
    <tr>
      <th>렌즈 유형</th>
      <th>초점 거리 (예시)</th>
      <th>시야 특성</th>
      <th>조절 방식</th>
      <th>시야각 (FOV)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>광각 (Wide)</td>
      <td>≤ 2.8mm</td>
      <td>넓은 시야, 왜곡 가능성 있음</td>
      <td>고정형 (Fixed)</td>
      <td>약 120~160°</td>
    </tr>
    <tr>
      <td>표준 (Normal)</td>
      <td>3.6~4mm</td>
      <td>사람 시야와 유사, 균형 잡힌 화각</td>
      <td>고정형 (Fixed)</td>
      <td>약 90~100°</td>
    </tr>
    <tr>
      <td>망원 (Tele)</td>
      <td>≥ 12~16mm</td>
      <td>멀리 있는 객체 확대, 좁은 시야</td>
      <td>고정형 (Fixed)</td>
      <td>약 30~60°</td>
    </tr>
    <tr>
      <td>Varifocal</td>
      <td>2.8~12mm 등</td>
      <td>설치 후 수동 초점 조절 가능</td>
      <td>가변형 (Manual)</td>
      <td>약 30~130° (가변)</td>
    </tr>
    <tr>
      <td>Motorized</td>
      <td>2.8~12mm 등</td>
      <td>원격 줌 및 초점 조절 가능</td>
      <td>전동형 (Remote)</td>
      <td>약 30~130° (가변)</td>
    </tr>
  </tbody>
</table>

<ul>
  <li>3.6mm 렌즈는 일반적으로 사람의 시야각과 가장 유사한 화각을 제공합니다.</li>
  <li>실내 고정 감시는 표준형 또는 Varifocal, 외부 침입 감지는 광각 또는 망원이 자주 쓰입니다.</li>
  <li>Motorized 렌즈는 설치 이후 원격 환경에서 시야를 조정해야 할 때 유리합니다.</li>
</ul>

<hr />

<h2 id="5-cctv-화각-비교-및-왜곡-분석--실사례-기반-4종-비교">5. CCTV 화각 비교 및 왜곡 분석 – 실사례 기반 4종 비교</h2>

<p>화각은 한 대의 카메라가 커버할 수 있는 시야의 각도입니다.<br />
객체가 화면에서 얼마나 크게 보이는지에 영향을 줍니다.</p>

<table>
  <thead>
    <tr>
      <th>화각</th>
      <th>시야 설명</th>
      <th>사용 예</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>60~90°</td>
      <td>일반</td>
      <td>얼굴, 번호판 인식</td>
    </tr>
    <tr>
      <td>120~150°</td>
      <td>광각</td>
      <td>실내 공간 전체</td>
    </tr>
    <tr>
      <td>≥180°</td>
      <td>어안</td>
      <td>회전문, 로비 등</td>
    </tr>
  </tbody>
</table>

<p>Vision AI 시스템에서는 카메라의 화각(FOV, Field of View)과<br />
왜곡 특성이 모델의 분석 성능에 직접적인 영향을 미칩니다.</p>

<p>현장에서는 일반적으로 광각 렌즈(약 100~130°) 가 많이 활용됩니다. 이는 적은 수의 카메라로 넓은 공간을 커버할 수 있어 설치 수량과 비용을 줄이는 데 유리하기 때문입니다.</p>

<p>또한, 실제 환경에서는 카메라를 자유롭게 설치하기 어려운 경우가 많아, 넓은 화각으로 시야 확보를 우선 고려해야 하는 경우가 많습니다.</p>

<p>반면, 분석 정확도나 왜곡 최소화가 중요한 환경에서는 일반각(85~90°)이 더 적합할 수 있습니다. 일반각은 왜곡이 적고, 객체의 형태와 위치를 보다 정밀하게 분석할 수 있기 때문입니다.</p>

<h3 id="정리">정리</h3>

<ul>
  <li>화각이 넓을수록 왜곡이 발생하기 쉬우며, 이는 분석 정확도 하락으로 이어질 수 있습니다.</li>
  <li>특히 객체가 중앙에 위치해 있더라도,<br />
광각 렌즈 자체의 원근 과장 효과로 인해 왜곡 현상이 발생할 수 있습니다.</li>
  <li>따라서 분석 목적에 따라 적절한 화각을 선택하고,<br />
렌즈 보정 특성을 함께 고려해야만 최적의 모델 성능을 확보할 수 있습니다.</li>
</ul>

<hr />

<h2 id="6-cctv-설치가-vision-ai-시스템에-끼치는-영향">6. CCTV 설치가 Vision AI 시스템에 끼치는 영향</h2>

<p>CCTV는 영상 확보 장비를 넘어, Vision AI 시스템과 연결될 경우<br />
설치 조건 자체가 분석 성능에 직접적인 영향을 줍니다.</p>

<p>따라서 “잘 보이는 곳”이 아닌,<br />
분석 목적에 맞는 위치와 시야 조건을 고려한 설치가 중요합니다.</p>

<h2 id="설치-위치는-초기에-정확히-정하는-것이-가장-효율적">설치 위치는 초기에 정확히 정하는 것이 가장 효율적</h2>

<p>설치 후에도 각도나 초점 조정은 가능하지만, 실제 현장에서는 다음과 같은 제약이 많습니다:</p>

<ul>
  <li>위치를 바꾸려면 방향 조정만으로는 부족하고, 배선 및 브래킷 재설치가 필요합니다.</li>
  <li>대부분의 프로젝트에서 CCTV가 AI 시스템보다 먼저 설치되므로,<br />
 화각이 맞지 않으면 분석이 지연되거나 반복 수정이 발생합니다.</li>
  <li>특히 산업 및 야외 환경에서는, 설치 후 위치 변경이 사실상 불가능한 경우도 많습니다.</li>
</ul>

<hr />

<h2 id="분석보다-더-큰-리스크는-설치-지연">분석보다 더 큰 리스크는 “설치 지연”</h2>

<p>설치 이후에도 조정은 가능하지만,<br />
초기 설치 자체가 늦어지면 프로젝트 전체 일정에 영향을 줄 수 있습니다.</p>

<ul>
  <li>카메라가 없으면 화각 검토나 분석 테스트 자체가 불가능합니다.</li>
  <li>위치가 확정되지 않으면 분석 조건도 설정할 수 없습니다.</li>
  <li>실제로 일정이 지연된 프로젝트 상당수는 설치 시점을 확정하지 못했던 경우였습니다.</li>
</ul>

<p>Vision AI 분석이 병행되는 프로젝트일수록,<br />
초기에 설치를 완료하고, 운영 중 조정하는 방식이 더 현실적입니다.</p>

<p>“완벽한 설치 타이밍”을 기다리기보다는,<br />
일단 시작하고 보정하는 접근이 더 성공적입니다.</p>

<blockquote>
  <p>단 한 번의 설치가 모든 걸 해결해주지 않습니다.<br />
하지만 단 한 번도 설치하지 않으면, 아무것도 시작되지 않습니다.</p>
</blockquote>

<hr />

<h2 id="실제-사례로-본-설치-리스크">실제 사례로 본 설치 리스크</h2>

<ul>
  <li>각도나 초점은 조정할 수 있지만,<br />
설치 위치는 일단 정해지면 변경이 쉽지 않습니다.</li>
  <li>여러 프로젝트에서 화각 조정은 자주 있었지만,<br />
위치 변경은 일정 지연·장비 재설치로 이어졌습니다.</li>
  <li>현재도 진행 중인 실제 사례가 있습니다.<br />
PoC 완료, 모델 구축, 화각 시뮬레이션까지 마친 상태지만,<br />
CCTV 설치가 지연되면서 사업 마무리가 기약 없이 멈춰 있는 상황입니다.</li>
  <li>Vision AI 프로젝트일수록,<br />
설치 전 시야 조건을 검토하고 빠르게 설치에 착수할 수 있는 결정 구조가 필요합니다.</li>
  <li>설치는 분석의 시작점입니다. <br />
미루기보다 시작하고 조정하는 것이 훨씬 실용적입니다.</li>
</ul>

<hr />

<h2 id="7-영상-내-객체-크기-추정">7. 영상 내 객체 크기 추정</h2>

<p>Vision AI 분석 성능은 알고리즘의 성능뿐만 아니라,<br />
CCTV 설치 위치, 해상도, 렌즈 화각(FOV), 프레임 속도(FPS) 같은 물리적 요소가<br />
객체 탐지의 성공 여부에 큰 영향을 미칩니다.</p>

<hr />

<h3 id="탐지-성능에-영향을-주는-요소들">탐지 성능에 영향을 주는 요소들</h3>

<ul>
  <li>해상도: 높을수록 객체가 화면에서 더 크게 보임 (권장: 1080p 이상)</li>
  <li>렌즈 화각(FOV): 좁을수록 멀리 있는 객체도 더 크게 보임 (권장: 60도 이하)</li>
  <li>카메라 위치: 설치 높이와 거리 조정으로 탐지 가능 범위 최적화</li>
  <li>프레임 속도(FPS): 움직임 기반 분석은 최소 10fps 이상 권장</li>
  <li>사전 검증: 실제 촬영 또는 시뮬레이션 기반 검증이 필수</li>
</ul>

<p>이러한 조건이 적절히 설계되지 않으면,<br />
객체가 화면에 너무 작게 찍혀 AI가 탐지하지 못하거나, 정확도가 떨어질 수 있습니다.</p>

<hr />

<h3 id="객체-크기와-탐지-성능의-관계">객체 크기와 탐지 성능의 관계</h3>

<p>객체가 영상 내에서 얼마나 큰 크기로 보이느냐는,<br />
AI 모델이 해당 객체를 정확히 검출하고 탐지할 수 있는지를<br />
결정짓는 핵심 요소입니다.</p>

<p>일반적으로 화면 세로 해상도의 약 6~8% 이상에 해당하는 크기로 객체가 나타날 경우,<br />
대부분의 모델에서 안정적인 탐지가 가능합니다.</p>
<ul>
  <li>720p(1280×720) 기준: 약 40~50픽셀 이상</li>
  <li>1080p(1920×1080) 기준: 약 60~80픽셀 이상</li>
  <li>※ 단, 모델 구조 및 학습 데이터셋에 따라 달라질 수 있습니다.</li>
</ul>

<hr />

<h3 id="정리-1">정리</h3>

<ul>
  <li>객체가 너무 작으면 AI가 탐지하지 못하거나 탐지 정확도가 크게 떨어질 수 있습니다.</li>
  <li>설치 전, 목표 객체의 거리/크기에 기반해 최소 탐지 픽셀 크기 기준을 확보해야 합니다.</li>
  <li>설치 조건과 카메라 성능은 AI 분석 효과를 좌우하는 가장 기본적인 설계 변수입니다.</li>
</ul>

<hr />

<h2 id="8-영상-연결-방식과-스트리밍-프로토콜">8. 영상 연결 방식과 스트리밍 프로토콜</h2>

<p>영상 스트리밍은 데이터를 수신하는 과정을 넘어서,<br />
실시간성, 지연 시간, 해상도 보존, 호환성 등 시스템 전반의 성능에 영향을 주는 핵심 요소입니다.</p>

<p>RTSP는 Vision AI 분석 시스템과 연동하기에 가장 일반적인 방식이지만,<br />
브라우저에서는 직접 사용할 수 없기 때문에 미디어 서버를 통한 중계가 필요합니다.</p>

<p>RTMP는 방송 송출용으로 널리 사용되며,<br />
상대적으로 낮은 지연을 제공하지만 WebRTC보다는 지연이 큰 편입니다.</p>

<p>WebRTC는 초저지연 특성을 지녀<br />
브라우저 기반의 실시간 분석 결과 확인에 적합합니다.</p>

<p>분석 지연 시간(Latency), 사용자 대시보드 제공 여부, 다중 스트림 분배 등의 조건에 따라<br />
여러 스트리밍 프로토콜을 혼합 설계하는 것이 일반적입니다.</p>

<table>
  <thead>
    <tr>
      <th>프로토콜</th>
      <th>용도</th>
      <th>특징</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>RTSP</td>
      <td>실시간 CCTV 분석용</td>
      <td>저지연, OpenCV 연동에 유리</td>
    </tr>
    <tr>
      <td>RTMP</td>
      <td>방송/중계</td>
      <td>YouTube 등으로 송출, 상대적 저지연</td>
    </tr>
    <tr>
      <td>WebRTC</td>
      <td>초저지연 브라우저 기반</td>
      <td>사용자 실시간 모니터링에 적합</td>
    </tr>
    <tr>
      <td>HLS / DASH</td>
      <td>HTTP 기반 스트리밍</td>
      <td>수 초 이상 지연, 브라우저 호환성 우수</td>
    </tr>
    <tr>
      <td>ONVIF</td>
      <td>장비 제어, PTZ(Pan-Tilt-Zoom)</td>
      <td>자동 장치 검색 및 제어 기능 포함</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="9-미디어-서버">9. 미디어 서버</h2>

<p>미디어 서버는 영상 스트림을 분배, 변환, 보정, 동기화하는 핵심 인프라입니다.</p>

<p>예를 들어, RTSP 기반 CCTV 영상이 입력되면<br />
이를 분석 시스템에 전달하는 동시에 WebRTC로 변환하여 대시보드에 실시간 전송할 수 있습니다.</p>

<p>또한 분석 결과(Bounding Box, Segmentation Mask, Pose Keypoints 등)를<br />
영상에 Overlay하여 사용자에게 실시간으로 송출할 수 있습니다.</p>

<p>다수의 CCTV 스트림을 하나의 분석 파이프라인에서 처리하기 위해서는<br />
프레임 동기화, 일시적 끊김에 대한 복원 처리, 네트워크 재접속 관리 등을<br />
수행하는 미디어 서버가 필수적입니다.</p>

<p>이는 시스템의 실시간성, 안정성, 유연성을 확보하는 데 있어 핵심 구성 요소입니다.</p>

<table>
  <thead>
    <tr>
      <th>기능</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>스트림 변환</td>
      <td>RTSP → WebRTC 등 다양한 포맷 변환</td>
    </tr>
    <tr>
      <td>멀티 송출</td>
      <td>하나의 CCTV → 여러 시스템으로 동시에 분배</td>
    </tr>
    <tr>
      <td>Overlay 적용</td>
      <td>분석 결과를 영상에 실시간 삽입</td>
    </tr>
    <tr>
      <td>보안 제어</td>
      <td>접근 권한, IP 필터링 등 제어 기능</td>
    </tr>
    <tr>
      <td>예시 시스템</td>
      <td>Ant Media Server, Wowza, Janus, GStreamer</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="10-실전-대응-체크리스트-예시">10. 실전 대응 체크리스트 예시</h2>

<p>아래는 실무에서 인바운드 문의를 받을 때 활용하는 예시 양식입니다.<br />
설치 환경, 카메라 조건, 분석 대상, 성능 기대치 등을 사전에 파악할 수 있도록 구성되어 있습니다.</p>

<hr />

<p>[신규 Inbound 양식]<br />
모든 항목은 복수 선택 / 공란 허용됩니다.<br />
모르거나 확인이 어려운 항목은 아시는 대로만 작성해 주셔도 됩니다.</p>

<hr />

<h3 id="1-고객-정보">1. 고객 정보</h3>
<ul>
  <li>End-user:</li>
  <li>발주처:</li>
  <li>사업명 (있을 경우):</li>
</ul>

<hr />

<h3 id="2-고객이-요구하는-기능-또는-상황-복수-선택-가능">2. 고객이 요구하는 기능 또는 상황 (복수 선택 가능)</h3>
<ul>
  <li>☐ 특정 구역에 출입/잔류 인원 수 파악</li>
  <li>☐ 야생동물, 사람 등의 침입 시 알림</li>
  <li>☐ 사람이 쓰러진 상황 자동 감지</li>
  <li>☐ 화재/연기 발생 시 빠른 탐지</li>
  <li>☐ 보호구 착용 여부 탐지</li>
  <li>☐ 사람 또는 차량의 이동 동선 파악</li>
  <li>☐ 문자 또는 번호판 인식</li>
  <li>☐ 얼굴 식별 또는 출입 통제</li>
  <li>☐ 기타: <strong>__</strong><strong>__</strong><strong>__</strong><strong>__</strong><em>__</em></li>
</ul>

<hr />

<h3 id="3-카메라--설치-환경">3. 카메라 / 설치 환경</h3>

<h4 id="31-설치-여부-및-기본-정보">3.1 설치 여부 및 기본 정보</h4>
<ul>
  <li>설치 여부: [☐ 설치 완료 / ☐ 설치 예정 / ☐ 미정]</li>
  <li>CCTV 제조사: [☐ 한화비전 / ☐ 기타: <strong>__</strong>_____]</li>
  <li>채널 수 (카메라 대수): [   ] 대</li>
  <li>카메라 타입: [☐ 고정형 / ☐ PTZ / ☐ 360도 / ☐ 어안형(Fisheye) / ☐ 기타: <strong>__</strong>_____]</li>
</ul>

<h4 id="32-해상도-및-조명-조건">3.2 해상도 및 조명 조건</h4>
<ul>
  <li>화소/해상도: [☐ 1920x1080 / ☐ 4K / ☐ 기타: <strong>__</strong>_____]</li>
  <li>RGB/IR 여부: [☐ RGB(주간 전용) / ☐ IR(야간 전용) / ☐ 불확실]</li>
  <li>야간 촬영 포함 여부: [☐ 야간 포함 / ☐ 주간만 / ☐ 불확실]</li>
  <li>조명 유무: [☐ 있음 / ☐ 없음 / ☐ 불확실]</li>
</ul>

<h4 id="33-화각-렌즈-시야-조건">3.3 화각, 렌즈, 시야 조건</h4>
<ul>
  <li>화각(FOV): [☐ 일반각(≤90°) / ☐ 광각(&gt;120°) / ☐ 어안형(&gt;180°)]</li>
  <li>렌즈 초점 방식: [☐ 고정 초점 / ☐ 수동 가변 초점 / ☐ 원격 조절(모터 구동)]</li>
  <li>촬영 각도: [☐ 정면 / ☐ 측면 / ☐ 사선 / ☐ 불확실]</li>
  <li>시야 중첩 여부: [☐ 있음 / ☐ 없음 / ☐ 불확실]</li>
</ul>

<h4 id="34-설치-위치-및-주요-거리">3.4 설치 위치 및 주요 거리</h4>
<ul>
  <li>설치 위치 및 높이: (예: 노지 외부, 2.5m)</li>
  <li>주요 촬영 거리: (예: 객체까지 약 6m)</li>
</ul>

<h4 id="35-탐지-대상-관련">3.5 탐지 대상 관련</h4>
<ul>
  <li>카메라 시야 캡쳐 이미지 여부: [☐ 있음 / ☐ 없음]<br />
(※ 설치된 CCTV 영상의 대표 화면 1~2장 첨부 가능 시 체크)</li>
  <li>영상 내 객체 크기 추정 (영상 없을 경우 작성):<br />
(예: 화면에 사람 1~2명이 꽉 차게 보일 것으로 예상 / 5~6명까지 화면에 보일 수 있음 등)</li>
</ul>

<hr />

<h3 id="4-ai-분석-조건">4. AI 분석 조건</h3>
<ul>
  <li>클래스 수 및 종류: (예: 사람, 멧돼지 = 2종)</li>
  <li>샘플 영상 제공 여부: [☐ 있음 / ☐ 없음 / ☐ 확보 예정]</li>
  <li>데이터 수집 방식: [☐ 고객 제공 / ☐ 당사 수집 / ☐ 미정]</li>
  <li>성능 기대 수준: (예: 정확도 90% 이상, 오탐 최소화 등)</li>
  <li>성능 기준 중 우선순위: [☐ 정확도 / ☐ 오탐 최소화 / ☐ 속도(Latency) / ☐ 누락 최소화 / ☐ 기타: <strong>__</strong><strong>__</strong>_]</li>
  <li>결과 출력 방식:  [☐ 실시간 알림 / ☐ 대시보드 / ☐ 리포트 / ☐ 불확실]</li>
</ul>

<hr />

<h3 id="5-요청-사항">5. 요청 사항</h3>
<ul>
  <li>요청 유형: [ ☐ 실제 도입 문의 / ☐ PoC 요청 / ☐ 기술 검토 목적 / ☐ 기타: <strong>__</strong><strong>__</strong>_ ]</li>
  <li>개발 MM 요청 여부: [☐ 예 / ☐ 아니오]</li>
  <li>하드웨어 관련 현황:<br />
[☐ 견적 필요 (스펙 제안 요청)<br />
 ☐ 사내 GPU/서버 보유 중 (모델명: <strong>__</strong><strong>__</strong>_)<br />
 ☐ 아직 파악되지 않음 (확인 필요)]</li>
  <li>사업 기간:</li>
  <li>회신 희망일시: (예: 7월 23일 14시까지)</li>
  <li>예상 예산 규모 (선택):</li>
  <li>추가 요청 사항/메모:</li>
</ul>

<hr />

<h2 id="마무리-분석-설정은-누구를-위한-것인가">마무리: 분석 설정은 누구를 위한 것인가?</h2>

<p>객체지향 프로그래밍(OOP)의 설계 원칙 중 하나인<br />
SRP(Single Responsibility Principle, 단일 책임 원칙)는<br />
“하나의 책임(Responsibility)은 하나의 변화 주체(Actor)에 귀속돼야 한다”고 말합니다.</p>

<p>이 원칙은 단순히 코드 구조뿐 아니라,<br />
Vision AI 시스템의 분석 설정 방식에도 통찰을 제공할 수 있습니다.</p>

<p>Vision AI 시스템은 다양한 CCTV 채널에서 입력된 영상을 분석하지만,<br />
채널마다 설치 위치, 화각, 촬영 거리, 왜곡 정도가 다르기 때문에<br />
동일한 Vision 모델을 적용하더라도 검출 결과의 품질이나 형태는 달라질 수 있습니다.</p>

<p>기술적인 변수 외에도 중요한 점은,<br />
그 결과를 해석하고 활용하는 주체(Actor)에 따라<br />
분석 결과가 갖는 의미 자체가 달라진다는 사실입니다.</p>

<ul>
  <li>보안 담당자는 “누가 들어왔는가”를 주로 보고,</li>
  <li>안전 관리자는 “보호구 착용 여부”를 확인하며,</li>
  <li>시설 관리자는 “쓰러짐 여부나 체류 시간”을 주목합니다.</li>
</ul>

<p>즉, 같은 객체를 대상으로 하더라도<br />
누가 그 결과를 보고 어떻게 해석하느냐에 따라<br />
필요한 탐지 기준, 임계값, 알림 조건은 전혀 달라져야 합니다.</p>

<p>그럼에도 불구하고 많은 시스템은 여전히 객체 중심으로 구성되어 있어,<br />
하나의 분석 설정(탐지 클래스, 임계값, 알림 조건 등)을<br />
모든 채널에 일괄 적용하는 방식을 사용하고 있습니다.</p>

<p>그러나 분석 설정이 객체(Object)를 기준으로 일괄 고정되어서는,<br />
실제 업무 목적에 부합하는 유연한 대응이 어려워집니다.</p>

<p>따라서 분석 설정은 객체가 아니라 Actor(사용자)의 역할과 목적에 따라 구성되어야 합니다.<br />
이러한 관점은 “Single Actor Principle(SAP)”로도 알려져 있으며,<br />
저 또한 이 원칙에 공감하고 실무 전반에 적용하고 있습니다.</p>

<p>각 CCTV 채널은 특정 Actor의 목적을 위해 설치되며,</p>
<ul>
  <li>어떤 객체를 탐지할지,</li>
  <li>어떤 조건에서 경고할지,</li>
  <li>어떤 방식으로 알림을 전달할지는<br />
그 결과를 해석하고 책임지는 사람의 역할에 따라 달라져야 합니다.</li>
</ul>

<blockquote>
  <p>객체 탐지 알고리즘은 “사실”을 판단하지만,<br />
사람은 “맥락”을 해석합니다.<br />
위에서 내려다본 시선, 정면에서 마주보는 시선, 아래에서 올려다본 시선은<br />
동일한 객체라도 전혀 다른 의미를 만들어냅니다.</p>
</blockquote>

<p>현재 담당 중인 Vision AI 제품은 이러한 SAP 관점에 따라,<br />
CCTV 채널별뿐 아니라 사용자별로도 독립된 분석 설정을 구성하고 있으며,<br />
모델, 클래스, 탐지 주기, 알림 주기, 임계값까지 채널 단위로 세분화해 운영되고 있습니다.</p>

<p>이는 단순한 기술적 구조가 아닌,<br />
각 사용자(Actor)의 역할과 목적에 따라 책임을 분리하는 전략적 설계 방식이며,<br />
현장에서 의미 있는 결과와 해석 가능성을 높이기 위한 구조적 기반입니다.</p>

<p>결국 Vision AI 시스템이 현장에서 유의미하게 작동하려면,<br />
Object 중심이 아니라 Actor 중심의 분석 설정,<br />
즉 SAP(Single Actor Principle)에 기반한 설계가 필요합니다.</p>

<hr />]]></content><author><name>indexkim</name></author><category term="vision-ai" /><category term="product" /><summary type="html"><![CDATA[Vision AI의 시작점은 모델이 아니라 CCTV입니다. 입력이 불완전하면, 어떤 모델도 기대한 성능을 내기 어렵습니다.]]></summary></entry><entry><title type="html">AI 직무 구분과 R&amp;amp;R - 실무 관점에서 본 역할 정의</title><link href="https://indexkim.github.io//ai-roles-and-responsibilities/" rel="alternate" type="text/html" title="AI 직무 구분과 R&amp;amp;R - 실무 관점에서 본 역할 정의" /><published>2025-07-11T00:00:00+09:00</published><updated>2025-07-11T00:00:00+09:00</updated><id>https://indexkim.github.io//ai-roles-and-responsibilities</id><content type="html" xml:base="https://indexkim.github.io//ai-roles-and-responsibilities/"><![CDATA[<h2 id="ai-engineer-정말-다-같은-엔지니어일까">AI Engineer, 정말 다 같은 엔지니어일까?</h2>

<p>“AI Engineer로 지원했는데 실제로는 데이터 정제만 하고 있어요.”<br />
“AI Research Engineer와 AI Engineer의 차이가 뭔가요?”<br />
“End-to-End AI Engineer라는 직함을 처음 봤는데, 정확히 뭘 하는 건가요?”</p>

<p>AI 업계에 종사하면서 자주 받는 질문들입니다. 
실제로 AI 관련 직무는 회사마다, 팀마다 다르게 정의되어 있어서 
지원자도, 채용하는 쪽도, 심지어 같은 팀 내에서도 혼란스러워하는 경우가 많습니다.</p>

<p>저도 처음 AI 커리어를 시작할 때 이런 혼란을 겪었고, 
지금까지 다양한 포지션을 거치면서 각 직무의 실제 모습을 경험했습니다.
이 글에서는 제가 직접 겪은 AI 직무들의 실제 역할과 책임, 
그리고 개인적인 커리어 변화 과정을 공유해보려고 합니다.</p>

<hr />

<h3 id="1-ai-researcher">1. AI Researcher</h3>

<p>AI Researcher는 새로운 알고리즘이나 모델 구조를 제안하고, 
기존 기술의 한계를 극복하기 위한 연구를 수행하는 역할입니다.
산출물은 주로 논문이며, CVPR, NeurIPS, ICML, ICCV 등 국제 학회에 제출해 기술력을 증명합니다.
제품을 만드는 목적보다는, 이론적 기여나 SOTA 갱신, 모델의 성능 향상에 초점을 둡니다.
대부분 대학이나 대기업 산하 연구소에 소속되어 있으며, 
현실의 데이터보다 논문용 정제 데이터셋과 벤치마크를 많이 다룹니다.
구현보다는 아이디어 중심의 작업을 많이 하며, 
코드는 실험을 위한 도구일 뿐 핵심은 기획과 설계에 있습니다.</p>

<hr />

<h3 id="2-ai-research-engineer">2. AI Research Engineer</h3>

<p>AI Research Engineer는 AI Researcher가 제안한 논문 기반 기술을 실제로 구현하고, 
실험을 통해 성능을 재현하거나 개선하는 역할입니다.
논문을 코드로 옮기고 실험 파이프라인을 구성하며, 
벤치마크 데이터셋을 기반으로 다양한 실험을 반복합니다.
연구의 논리적 타당성과 구현상의 현실성 사이의 다리를 놓는 엔지니어로, 
코딩 스킬과 수학적 이해력을 모두 요구받습니다.</p>

<hr />

<h3 id="3-ai-engineer">3. AI Engineer</h3>

<p>AI Engineer는 실제 제품이나 서비스에 적용할 AI 모델을 학습하고, 
데이터셋을 구성하거나 모델의 성능을 개선하는 역할을 합니다.
주어진 목적에 맞는 모델을 선정하고, 필요한 경우 커스터마이징하거나 파인튜닝합니다.
학습된 모델을 기반으로 추론 결과를 만들어내고, 
그 결과를 평가하여 다음 학습에 반영하기도 합니다.
비즈니스 요구사항과 기술 사이에서 균형을 맞추는 실무형 엔지니어입니다.</p>

<hr />

<h3 id="4-applied-ai-engineer">4. Applied AI Engineer</h3>
<p>Applied AI Engineer는 고객이나 사용자의 니즈에 맞춰 AI 기술을 실질적인 문제 해결에 적용하는 엔지니어입니다.
자체적으로 Task를 정의하거나, 주어진 과제를 해결하기 위해 적절한 데이터셋을 구성하고, 
모델을 실용적인 형태로 만들고 설명하는 데 집중합니다.
“이건 가능한가요?”라는 질문에 기술적으로나 현실적으로 “가능합니다” 혹은 “불가능합니다”를 판단하고 설계합니다.
실제 서비스와 맞닿은 AI 개발자의 입장에 가장 가까우며, 
설계적 사고와 유연한 문제해결력이 중요한 포지션입니다.</p>

<hr />

<h3 id="5-ai-backend-engineer">5. AI Backend Engineer</h3>
<p>AI Backend Engineer는 학습된 AI 모델을 실제 서비스에 연동하기 위한 시스템을 구성하고, 
모델의 추론 결과를 API나 소켓을 통해 전달하는 역할을 합니다.
Docker 환경 구성, Python-C++ 바인딩, Redis 및 캐시 브로커 구성 등을 통해 
분석엔진과 실서비스 간의 연결을 책임집니다.
모델 학습에는 직접 관여하지 않으며, 학습된 모델을 “서빙 가능한 형태”로 가공하고 
실행 환경을 안정화하는 데 초점을 둡니다.
서비스 운영 단계에서 가장 가까운 위치에 있는 AI 시스템 엔지니어입니다.</p>

<hr />

<h3 id="6-end-to-end-ai-engineer">6. End-to-End AI Engineer</h3>
<p>일반적으로 End-to-End AI Engineer는 데이터 수집부터 모델 학습, 분석엔진 구현, API 연동, 
결과 시각화, 운영 배포까지 전 단계를 책임집니다.
다양한 직무 간 경계를 넘나들며 기술적 소통을 주도하고, 
시스템 전체의 흐름을 이해하며 실무를 진행합니다.
종종 PM 없는 PO, AI 기술 총괄 역할을 수행하게 되며, 
기술 인바운드 대응이나 고객 피드백 수용까지 맡는 경우도 많습니다.
과업의 깊이보다는 넓이와 연결성을 중시하며, 
혼자서 프로토타입을 구성하거나 제품화까지 이끄는 유연성이 필요한 포지션입니다.</p>

<hr />

<h3 id="7-full-stack-ai-engineer">7. Full Stack AI Engineer</h3>
<p>Full Stack AI Engineer는 AI 모델 개발뿐 아니라 프론트엔드와 백엔드까지 아우르며, 
실제 사용자에게 도달하는 시스템 전체를 구축할 수 있는 엔지니어입니다.
React, FastAPI, AWS 등 다양한 기술 스택을 조합해 웹 인터페이스와 모델을 통합 구현합니다.
POC, 데모, 전시용 AI 시스템을 짧은 시간 내에 만드는 데 강점을 가지며, 
디자인 감각이나 사용성 고려도 병행하는 경우가 많습니다.
AI와 Web 사이의 단절 없이 하나의 서비스로 완성해내는 융합형 개발자입니다.</p>

<hr />

<h3 id="8-그-외-실무형-역할들-직무로-인정받지-않지만-중요함">8. 그 외 실무형 역할들 (직무로 인정받지 않지만 중요함)</h3>

<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>QA/검수</td>
      <td>라벨 정확성 검토, 클래스 분류 체계 정비</td>
    </tr>
    <tr>
      <td>보조 명칭</td>
      <td>Data Manager, Annotation QA, Data Ops 등</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>실무에서 가장 시간이 오래 걸리지만, 공식 직무로 분류되지 않거나<br />
외주/비정규직/도급 형태로 분리되는 경우가 많습니다.</p>
</blockquote>

<hr />

<h3 id="ai-개발-프로세스-단계별-직무-기여도">AI 개발 프로세스 단계별 직무 기여도</h3>

<table>
  <thead>
    <tr>
      <th>단계</th>
      <th>AI Researcher</th>
      <th>AI Research Engineer</th>
      <th>AI Engineer</th>
      <th>Applied AI Engineer</th>
      <th>AI Backend Engineer</th>
      <th>End-to-End AI Engineer</th>
      <th>Full Stack AI Engineer</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>기획</td>
      <td>✕</td>
      <td>✕</td>
      <td>△</td>
      <td>△</td>
      <td>△</td>
      <td>○</td>
      <td>○</td>
    </tr>
    <tr>
      <td>Task 정의</td>
      <td>✕</td>
      <td>△</td>
      <td>△</td>
      <td>○</td>
      <td>△</td>
      <td>○</td>
      <td>△</td>
    </tr>
    <tr>
      <td>데이터 수집</td>
      <td>✕</td>
      <td>△</td>
      <td>○</td>
      <td>△</td>
      <td>△</td>
      <td>○</td>
      <td>△</td>
    </tr>
    <tr>
      <td>데이터 정제</td>
      <td>✕</td>
      <td>△</td>
      <td>○</td>
      <td>△</td>
      <td>△</td>
      <td>○</td>
      <td>△</td>
    </tr>
    <tr>
      <td>데이터 가공</td>
      <td>△</td>
      <td>○</td>
      <td>○</td>
      <td>△</td>
      <td>△</td>
      <td>○</td>
      <td>△</td>
    </tr>
    <tr>
      <td>학습 데이터셋 구축</td>
      <td>△</td>
      <td>○</td>
      <td>○</td>
      <td>△</td>
      <td>✕</td>
      <td>○</td>
      <td>△</td>
    </tr>
    <tr>
      <td>모델 학습</td>
      <td>○</td>
      <td>○</td>
      <td>○</td>
      <td>△</td>
      <td>✕</td>
      <td>○</td>
      <td>△</td>
    </tr>
    <tr>
      <td>모델 검증</td>
      <td>○</td>
      <td>○</td>
      <td>○</td>
      <td>○</td>
      <td>✕</td>
      <td>○</td>
      <td>△</td>
    </tr>
    <tr>
      <td>모델 최적화/경량화</td>
      <td>△</td>
      <td>○</td>
      <td>○</td>
      <td>○</td>
      <td>△</td>
      <td>○</td>
      <td>△</td>
    </tr>
    <tr>
      <td>분석엔진 개발</td>
      <td>✕</td>
      <td>△</td>
      <td>○</td>
      <td>○</td>
      <td>○</td>
      <td>○</td>
      <td>△</td>
    </tr>
    <tr>
      <td>시스템 배포/운영</td>
      <td>✕</td>
      <td>✕</td>
      <td>△</td>
      <td>△</td>
      <td>○</td>
      <td>○</td>
      <td>○</td>
    </tr>
    <tr>
      <td>웹(프론트/UI)</td>
      <td>✕</td>
      <td>✕</td>
      <td>✕</td>
      <td>△</td>
      <td>△</td>
      <td>△</td>
      <td>○</td>
    </tr>
  </tbody>
</table>

<p>기호 설명:</p>
<ul>
  <li>○ = 주도</li>
  <li>△ = 일부/협업</li>
  <li>✕ = 관여하지 않음
    <blockquote>
      <p>이 표는 각 직무가 관여하는 AI 개발 단계만을 나타냅니다.
투입 시간, 책임 강도, 병행 과업 수는 포함되지 않습니다.</p>
    </blockquote>
  </li>
</ul>

<hr />

<h2 id="현-조직의-ai-직무-구성">현 조직의 AI 직무 구성</h2>

<table>
  <thead>
    <tr>
      <th>역할</th>
      <th>간단 설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Team Leader</td>
      <td>팀 총괄 및 일정, 외부 협의 전반을 담당</td>
    </tr>
    <tr>
      <td>(Almost) Full Stack AI Engineer</td>
      <td>모델 및 시스템 전반 구현, 웹 기반 데모 제작 및 검색엔진 연동 등</td>
    </tr>
    <tr>
      <td>다양한 컴포넌트 개발</td>
      <td> </td>
    </tr>
    <tr>
      <td>End-to-End AI Engineer</td>
      <td>모델부터 시스템까지 전방위 대응</td>
    </tr>
    <tr>
      <td>AI Backend Engineer</td>
      <td>분석엔진을 실서비스에 연동, 시스템 인터페이스 담당</td>
    </tr>
    <tr>
      <td>AI Research Engineer</td>
      <td>모델 학습, 알고리즘 연구, 성능 개선, 대회/논문/기술검증 등 수행</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="team-leader">Team Leader</h3>
<p>팀 전체 일정과 외부 대응을 총괄합니다.
고객사, 영업, 대표단과의 기술 협의에 참여하며, 
내부 리소스 배분과 역할 조율을 주도합니다.
직접적인 모델 학습이나 분석엔진 개발에는 참여하지 않지만, 
전반적인 방향성과 일정을 리드합니다.</p>

<hr />

<h3 id="almost-full-stack-ai-engineer">(Almost) Full Stack AI Engineer</h3>
<p>모델 결과를 기반으로 한 웹 기반 데모 제작, 검색엔진 연동, UI/UX 구성 등 
시스템 전반을 폭넓게 다룹니다.
Flask 기반 웹서버 구성, ChromaDB, Elasticsearch 연동 등 
프론트·백엔드 경계를 넘나드는 구현이 가능하며,
분석엔진이 아닌 데모 중심의 시스템을 주로 설계하고, 
필요 시 모델 파인튜닝도 수행합니다.
제품화에 가까운 프로토타입을 구성하지만, 
실제 제품화 시에는 프론트엔드·백엔드 개발자가 별도로 투입됩니다.
조직 내에서는 비주력 제품을 중심으로 활동하며, 
보조 기술 지원 역할도 함께 수행합니다.</p>

<hr />

<h3 id="end-to-end-ai-engineer">End-to-End AI Engineer</h3>
<p>AI 모델 개발부터 시스템 연동, 실서비스 대응까지 전체 흐름을 책임지며, 
특히 외부 대응, 출장, 사업관리 협업이 모두 집중되는 핵심 포지션입니다.
모델을 학습하는 것뿐 아니라, 분석엔진에 탑재하고 실시간 환경에서 안정적으로 동작하도록 
설계·조율합니다.
배포 이후 발생하는 이슈에 직접 대응하며, 추론 파이프라인, 테스트 구성, config 조정 등 
운영 관점까지 폭넓게 다룹니다.
외부 회의 참석, 실증 환경 데이터 수집, 성능 검증, 제품화 대응까지 포함되어 있어, 
실질적으로 AI 기반 시스템 전체를 관리합니다.</p>

<hr />

<h3 id="ai-backend-engineer">AI Backend Engineer</h3>
<p>분석엔진의 추론 결과를 실서비스와 연결하는 인터페이스 개발을 핵심으로 담당합니다.
Python 기반 분석엔진과 외부 시스템 사이의 REST API, WebSocket, Python-C++ 바인딩 모듈을 구현하며,
Docker 환경 구성, 디코더/인코더 설정, 메모리 관리, 속도 튜닝 등도 병행합니다.
모델 학습에는 관여하지 않으며, 
“분석 결과가 실시간으로 서비스에 전달되도록 가공하는 역할”에 집중합니다.
또한, 시스템 내에서 예외 처리, 중단/재시작 로직, timeout 설정 등 
운영 안정성을 확보하는 역할도 수행합니다.</p>

<hr />

<h3 id="ai-research-engineer">AI Research Engineer</h3>
<p>모델 학습을 중심으로 한 연구 개발을 전담합니다.
딥러닝 기반 구조 설계, 실험 설계, 성능 튜닝 등 학습 관련 주요 업무를 수행하며,
외부 경진대회 참가, 기술 검증, 논문 작성 등 학술적 성과에도 적극적으로 참여합니다.
모델의 구조적 완성도와 성능 향상이 주요 과업이며, 
분석엔진 연동이나 실서비스 적용에는 직접 관여하지 않습니다.
기술의 원천성과 확장 가능성을 높이는 데 집중하며, 
연구소 내 가장 학문 중심적인 포지션입니다.</p>

<hr />

<h2 id="career-path">Career Path</h2>

<h3 id="1-data-analyst">1. Data Analyst</h3>
<p>저는 전형적인 CS 출신 개발자가 아닌, 상경계열 출신의 Data Analyst로서 AI 커리어를 시작했습니다.
사업관리팀 소속이었지만, 당시에는 개발자가 전혀 없는 조직에서 
Python 기반 자동화 도구를 혼자 개발하며 실무를 주도했습니다.
엑셀보다는 Pandas, CLI보다는 Jupyter에 익숙했고, 
Slack 알림 봇, 리사이징/이름변경/비식별화 도구, 시각화 스크립트 등 
다양한 툴을 만들어 데이터 흐름을 자동화했습니다.</p>

<p>수집부터 정제, 가공, 라벨링, 학습 테스트, 검수에 이르기까지 모든 단계를 
하나의 FTP 서버 안에서 처리하도록 구성한 초기형 데이터 파이프라인을 직접 설계하고 운영했습니다.</p>

<p>또한, 데이터 상태를 기반으로 공정별 잔여 물량을 분석하고, 일일 리포트를 자동 생성하며, 
KPI 달성을 위한 추가 수집/보완 계획을 수립하는 등 정량적 관리와 품질 통제까지 총괄했습니다.</p>

<p>모델 학습 자체는 공식 R&amp;R이 아니었지만, 가공단의 책임을 입증하고 
“라벨링 문제로 성능이 안 나온다”는 학습단의 주장을 선제적으로 차단하기 위해
직접 라벨링한 데이터를 학습해보는 방식으로 사내 품질 검증 체계까지 구축했습니다.</p>

<p>정식 직무명은 Data Analyst였지만, 실제 역할은 거의 Entry-level AI Engineer에 가까웠습니다.
다만 당시에는 모델 개발보다 “데이터 상태를 분석하고 일정과 품질을 맞추는 것”이 핵심 목적이었기 때문에 
경력증명서 상에는 Analyst로 기재되었으며,
이 경험은 훗날 AI Engineer로 전향하는 데 강력한 기반이 되었습니다.</p>

<hr />

<h3 id="2-ai-engineer">2. AI Engineer</h3>
<p>Data Analyst 시절 경험을 기반으로, 본격적으로 AI 모델 학습 중심의 업무에 전념하기 시작했습니다.
특수대학원에서 AI 전공을 병행하며, 실무와 학문을 함께 다져나간 시기이기도 했습니다.
조직 내에서는 연구소 소속으로, 주로 Vision AI 기반 Detection, Segmentation, Tracking 모델을 직접 학습하며 다양한 실험을 반복했습니다.</p>

<p>가장 큰 특징은 단일 모델 구조만을 고집하지 않고, 다양한 아키텍처를 바꿔가며 학습 실험을 주도했다는 점입니다.
오픈소스 기반 모델뿐 아니라, 프로젝트 목적에 맞춰 구조를 튜닝하거나 config를 조정하며 
다량의 학습을 수행하는 것이 핵심 역할이었습니다.</p>

<p>실험 목적에 따라 필요한 경우에는 직접 데이터 수집, 정제, 라벨링까지 선행적으로 수행하기도 했습니다.
Data Analyst 시절부터 데이터 흐름 전반을 이해하고 체화해온 덕분에, 
학습을 위한 데이터 확보와 준비 과정도 능동적으로 처리할 수 있었습니다.</p>

<p>당시에는 최적화, 경량화, 서빙과 같은 후처리 단계에는 직접 관여하지 않았으며, 
학습이 끝난 후 성능 지표를 기록하고,
필요시 시각화 기반 정성적 검토를 통해 모델을 개선하는 데 집중했습니다.</p>

<p>정식 직무는 AI Engineer였으며, 핵심 업무는 다양한 모델을 “직접 학습해보는 것” 자체에 있었습니다.
후속 배포보다는 실험적 모델링과 학습 반복 중심의 역할이었고,
학습 중심의 수행 경험이 Applied AI Engineer로 이어지는 기반이 되었습니다.</p>

<hr />

<h3 id="3-applied-ai-engineer">3. Applied AI Engineer</h3>
<p>AI Engineer 시절에는 학습 자체에 집중했다면,
Applied AI Engineer로 넘어오면서부터는 학습 결과를 실환경에 적용 가능하게 만드는 일에 무게가 실리기 시작했습니다.</p>

<p>실제 업무는 학습부터 검증, 최적화, 분석엔진 탑재 전까지의 전 과정을 직접 수행하거나 조율했습니다.
당시 제품화 중심의 조직에서, 팀의 필요와 방향성에 따라 Applied AI Engineer라는 직함이 새롭게 도입되었고 
그 첫 역할로 합류하게 되었습니다.</p>

<p>연구소가 만든 모델을 검증하고 문제를 파악해 수정하거나, 요청한 모델이 일정 등의 사유로 전달되지 않을 경우 직접 학습부터 경량화까지 전 과정을 대신 수행하기도 했습니다.</p>

<p>정식 모델 개발이 아닌 PoC, 기능 시연용, 전시용 영상 제작 등을 위해
수집부터 정제, 가공, 학습, 검증, 분석엔진 적용 전 시각화까지의 전체 흐름을 
혼자서 처리한 경험도 많습니다.</p>

<p>다양한 프로젝트에서 검증 대상이 된 모델들의 정량·정성 평가,
실제 시스템 탑재 전 리소스 체크(fps, 메모리, 실행시간 등),
성능을 보완하기 위한 추가 실험 및 보조 학습,
그리고 테스트셋 구성과 결과 분석까지 포함된 역할이었습니다.</p>

<p>당시에는 모델 개발과 검증의 R&amp;R이 불명확했기 때문에,
연구소와 제품화 팀 사이의 공백을 메우는 실무 조율자 역할도 수행했습니다.</p>

<p>Applied AI Engineer는 말 그대로,
현실적인 제약을 반영하여 모델을 실사용 가능한 형태로 “적용”시키는 역할이었습니다.
실험보다 현장 적용, 구조보다 리소스 조건, 알고리즘보다 ROI(Return on Investment)가 중요한 상황에서
가능 여부를 판단하고, 안 되는 것을 되게 만드는 것이 Applied의 핵심이었습니다.
극단적으로는 “돼요? 안돼요?”가 실질적인 산출물이 되는 경우도 많았습니다.</p>

<hr />

<h3 id="4-end-to-end-ai-engineer">4. End-to-End AI Engineer</h3>
<p>조직 개편과 AI Backend Engineer의 퇴사로 인해,
Applied AI Engineer에서 분석엔진과 시스템 연동까지 책임지는 포지션으로 확장되었습니다.
그 결과, 현재는 모델 개발뿐 아니라 모델이 탑재된 AI 시스템 전체를 책임지는 
End-to-End AI Engineer 역할을 수행하고 있습니다.</p>

<p>기획 의도 파악, 사전 검증, 모델 설계 및 학습, 분석엔진 최적화, 시스템 반영, 성능 대응, 운영 안정화까지
AI 모델이 현장에서 쓰일 수 있도록 하기 위한 전 과정을 조율하고 실행합니다.</p>

<p>예를 들어, 아래와 같은 역할을 실제로 수행하고 있습니다.</p>

<ul>
  <li>고객 요구 사항 기반 Task 정의 및 PoC용 데이터 수집</li>
  <li>라벨링 가이드 작성 및 라벨 검수/피드백</li>
  <li>모델 설계 및 반복 학습</li>
  <li>시각화 기반 정성 평가</li>
  <li>모델 최적화 및 경량화</li>
  <li>분석엔진 성능 저하 이슈 대응 (실시간성, 리소스 등)</li>
  <li>외부 회의 참석 및 실서비스 이슈 실시간 해결</li>
  <li>실제 웹 인터페이스에서 결과 확인 및 파라미터 조정</li>
  <li>분석엔진 추론 로그 기반 이상 탐지 및 개선점 도출</li>
</ul>

<p>기술 스택 면에서는 Python 기반 모델 프레임워크와 함께
GPU 점유율, 추론 속도, 시스템 동작 상태를 체크하며 전체 AI 파이프라인을 다룹니다.
Streamlit 기반 간단한 데모도 직접 구성하지만,
복잡한 API 연동이나 시스템 설계는 AI Backend Engineer 또는 Full Stack Engineer와 협업합니다.</p>

<p>모델을 넘어서 시스템 전체를 보는 것이 End-to-End의 핵심입니다.
다만, 시스템 아키텍처 자체를 처음부터 설계하거나 구조적으로 리팩터링하는 CS 중심의 역량은 포함되지 않습니다.
대신 현장에서 바로 돌아가는 실체를 다룬다는 점에서,
실용적 End-to-End에 가깝다고 할 수 있습니다.</p>

<hr />

<h3 id="정리">정리</h3>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>AI Engineer</th>
      <th>Applied AI Engineer</th>
      <th>End-to-End AI Engineer</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>목표</td>
      <td>모델을 잘 만든다</td>
      <td>기술로 문제를 푼다</td>
      <td>제품을 완성시킨다</td>
    </tr>
    <tr>
      <td>중점</td>
      <td>모델 구현·학습·서빙</td>
      <td>문제 정의 + 도메인 적용</td>
      <td>전체 AI 파이프라인 구성</td>
    </tr>
    <tr>
      <td>책임 범위</td>
      <td>모델 단위 (학습~서빙)</td>
      <td>비즈니스 문제에 맞는 솔루션 제공</td>
      <td>데이터 → 모델 → 추론 → 시스템 연동 전 과정</td>
    </tr>
    <tr>
      <td>시스템 기여도</td>
      <td>분석엔진 일부 또는 모델 컴포넌트</td>
      <td>분석엔진 사용·조정 중심</td>
      <td>분석엔진 + 추론 파이프라인 + API 구조까지 설계</td>
    </tr>
    <tr>
      <td>고객/도메인 이해</td>
      <td>낮음</td>
      <td>매우 높음</td>
      <td>중간~높음</td>
    </tr>
    <tr>
      <td>산출물</td>
      <td>모델, 서빙 코드, 실험 리포트</td>
      <td>PoC 데모, 기능 검증 시각화 영상, 커스터마이징 모델</td>
      <td>AI 시스템 전체 또는 제품 초기버전</td>
    </tr>
    <tr>
      <td>협업 대상</td>
      <td>백엔드, MLOps</td>
      <td>백엔드, PM, 고객사, 도메인 전문가</td>
      <td>백엔드, 프론트, 인프라, PM, 고객 등 전방위</td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="applied-vs-e2e-2개월차-회고">Applied vs E2E, 2개월차 회고</h2>

<h3 id="1-end-to-end-진짜-좋을까">1. End-to-End, 진짜 좋을까?</h3>

<p>최근 신입 AI Backend Engineer가 합류했지만, 저는 여전히 E2E 역할에서 벗어나지 못하고 있습니다.
외부 회의와 출장 일정이 잦아지면서, 인수인계도 충분히 하지 못한 채 하루하루를 넘기고 있습니다.
일주일에 개발을 이틀도 못 할 정도로 일정이 쏠려 있고, 본래 집중해야 할 개발 업무는 점점 밀려만 갑니다.</p>

<p>전임자가 퇴사한 뒤 AI Backend 역할까지 자연스레 떠맡게 되면서 체력적, 정신적으로도 부담이 큽니다.
막상 일을 나누려 해도 애매한 부분이 많고, 실력 있는 분이긴 하지만 시스템 구조 전체를 파악하기엔 아직 시간이 더 필요합니다.
결국 모델 서치 같은 업무까지도 백엔드에게 부탁하게 되는 현실이 가끔은 미안하게 느껴지기도 합니다.
애초에 그쪽 업무는 그분의 역할이 아닌데 말이죠.</p>

<p>Applied에선 경험으로 커버할 수 있었던 일도 많았지만, E2E는 그렇지 않습니다.
CS 기반이 부족한 저에겐 네트워크나 시스템 구조, 인터페이스 설계가 버겁게 다가오고,
이해하지 못한 채 넘기지 않으려다 보니 오히려 더 많은 리소스를 쓰게 됩니다.</p>

<hr />

<h3 id="2-applied의-명료한-구조가-그리운-이유">2. Applied의 명료한 구조가 그리운 이유</h3>

<p>그래서인지 Applied AI Engineer 시절이 자주 떠오릅니다.
요청 기반 업무라 실적을 수치로 드러내긴 어렵지만, 책임 범위와 산출물이 명확했고, 제 성향과도 잘 맞았습니다.
지금처럼 흐릿한 책임선 사이에서 모델, 시스템, 회의, 고객 대응까지 모두 챙겨야 하는 구조보다는,
그 시절엔 단 하나의 질문만 있으면 됐습니다.</p>

<blockquote>
  <p><strong>“돼요?” / “안 돼요?”</strong></p>
</blockquote>

<p>그 한마디로 갈리는 명료한 구조가 저에겐 큰 안정감을 줬습니다.
기술이 기준을 만족하느냐 마느냐, 그 단순한 결론이 오히려 제게는 더 잘 맞았던 것 같습니다.
무엇보다 처음엔 “안 돼요”였던 걸 “되게” 만들어가는 과정이 참 매력적이었습니다.
조금씩 성능을 끌어올려 결국 OK를 받아내던 그 순간의 성취감은 지금도 생생히 기억납니다.</p>

<p>가끔은 퇴사한 전임자가 돌아오고, 제가 다시 Applied로 복귀할 수 있다면 좋겠다는 생각도 듭니다.
하지만 이제는 연차도 쌓였고, 더 많은 책임을 받아들여야 할 시기라는 것도 알고 있습니다.</p>

<hr />

<h3 id="3-무게는-늘었지만-권한은-줄었다">3. 무게는 늘었지만 권한은 줄었다</h3>

<p>사실 저에게 인수인계를 하고 나간 사람이 벌써 네 명입니다.
그 중에는 리더급 1명도 포함되어 있었고,
그 자리를 명확히 메우기도 전에 자연스레 제게로 업무가 몰려들었습니다.
공식적인 리더는 아니지만, 실질적인 리더 역할까지 떠맡게 된 셈입니다.</p>

<p>리더가 아니면 권한은 없고, 책임만 생깁니다.
회의와 구조 파악, 고객 대응까지도 제 몫이 되었고,
정작 본업인 개발은 손에 꼽을 만큼밖에 못 하고 있습니다.
VSCode도, Cursor도 켜는 날보다 닫는 날이 많아졌습니다.
이제는 “내가 왜?”, “왜 나만?” 같은 생각조차 줄어들었습니다.
에너지를 아끼기 위해 그런 물음에 더는 힘을 쓰지 않기로 했습니다.</p>

<hr />

<h3 id="4-실력은-남는다-결국은">4. 실력은 남는다, 결국은</h3>

<p>그래도 한 가지는 분명합니다.
실력은 늡니다.
Data Analyst 시절에도 처음엔 구조도 모른 채 일을 떠맡았지만,
버티고 부딪히며 익힌 것들이 결국 지금의 밑바탕이 되었습니다.
그때처럼 지금도 매일 조금씩 영역이 확장되고 있다는 걸 체감합니다.</p>

<p>이직한 지 얼마 안 된 경우에는 인수인계 문서보다 먼저 조직의 구조와 사람들을 살펴봐야 합니다.
어디가 비어 있는지, 그 빈틈이 무너지면 어떤 연쇄 반응이 나에게까지 번질지 생각해야 합니다.
지금의 저는 그 연쇄 반응의 끝에 서 있는 사람입니다.</p>

<p>다음에 이직을 하게 된다면, E2E보다는 Applied AI Engineer 포지션에 더 집중해보고 싶습니다.
현 회사도 제안을 받아 입사했듯이, 언젠가 Applied로 다시 함께하자는 제안이 오길 조용히 기다리고 있습니다.
그 때는 서로의 기대가 일치하는 자리에서, 더 단단하게 함께 일해보고 싶습니다.</p>

<p>그 전까지는 지금의 자리에서 제 역할에 최선을 다할 생각입니다.<br />
그래도 야근은 하지 않습니다. 무슨 일이 있어도 칼퇴근합니다.<br />
최소한의 리소스는 남겨둬야 다음 날도 일할 수 있기 때문입니다.<br />
출장은 여전히 가기 싫지만요.</p>

<hr />]]></content><author><name>indexkim</name></author><category term="career" /><category term="vision-ai" /><summary type="html"><![CDATA[AI Engineer, 정말 다 같은 엔지니어일까?]]></summary></entry><entry><title type="html">Vision AI 제품 개발 과정 - (3) 모델 최적화 및 경량화, 분석엔진 개발, 시스템 배포 및 운영</title><link href="https://indexkim.github.io//vision-ai-product-dev-3/" rel="alternate" type="text/html" title="Vision AI 제품 개발 과정 - (3) 모델 최적화 및 경량화, 분석엔진 개발, 시스템 배포 및 운영" /><published>2025-06-26T00:00:00+09:00</published><updated>2025-06-26T00:00:00+09:00</updated><id>https://indexkim.github.io//vision-ai-product-dev-3</id><content type="html" xml:base="https://indexkim.github.io//vision-ai-product-dev-3/"><![CDATA[<p>이 글은 <em>Vision AI 제품 개발 과정</em>의 세 번째 편으로,<br />
전체 내용은 다음과 같은 흐름으로 구성되어 있습니다:</p>

<ul>
  <li>(1) Task 정의, 데이터 수집, 데이터 정제</li>
  <li>(2) 데이터 가공, 학습데이터셋 구축, 모델 학습 및 검증</li>
  <li><strong>(3) 모델 최적화 및 경량화, 분석엔진 개발, 시스템 배포 및 운영</strong></li>
</ul>

<hr />

<h2 id="8-모델-최적화-및-경량화">8. 모델 최적화 및 경량화</h2>

<p>모델 학습과 검증이 완료되면,<br />
실제 시스템 환경에 맞춰 모델을 최적화하고 경량화하는 단계가 필요합니다.<br />
이 과정에서는 추론 속도, 메모리 사용량, 배치 처리 구조, 플랫폼 이식성 등<br />
운영 환경 전반을 고려해 실무에 적합한 모델 구조를 완성합니다.</p>

<hr />

<h3 id="왜-필요한가">왜 필요한가?</h3>

<p>실제 제품 환경에서는 다음과 같은 요구사항들이 모델 최적화를 필요로 합니다:</p>

<table>
  <thead>
    <tr>
      <th>요구사항</th>
      <th>세부 내용</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>실시간성</td>
      <td>실시간 분석을 위한 FPS 확보</td>
    </tr>
    <tr>
      <td>자원 효율성</td>
      <td>GPU 메모리와 연산 자원 효율 활용</td>
    </tr>
    <tr>
      <td>시스템 연동</td>
      <td>분석엔진이나 웹 연동 시 부하 최소화</td>
    </tr>
    <tr>
      <td>환경 제약</td>
      <td>폐쇄망 등 자원 제약 환경 대응</td>
    </tr>
    <tr>
      <td>확장성</td>
      <td>모델 복수 탑재 및 멀티 채널 입력 대응</td>
    </tr>
  </tbody>
</table>

<p>이러한 요구사항들을 충족하기 위해서는 모델 단위의 성능 개선을 넘어서,<br />
시스템 전체 관점에서의 최적화가 필요합니다.</p>

<hr />

<h3 id="최적화optimization">최적화(Optimization)</h3>

<h4 id="onnx-open-neural-network-exchange">ONNX (Open Neural Network Exchange)</h4>

<ul>
  <li>PyTorch 모델을 범용 포맷으로 변환</li>
  <li>다양한 프레임워크 및 엔진과의 호환성을 높일 수 있음</li>
  <li>플랫폼 간 이식성을 확보하는 데 유리</li>
  <li>다양한 하드웨어(CPU, GPU, NPU 등) 환경에 대응이 가능</li>
  <li>단, ONNX 자체는 실행 엔진이 아니므로<br />
실제 성능은 런타임에 따라 달라짐</li>
</ul>

<h4 id="tensorrt-trt">TensorRT (TRT)</h4>

<ul>
  <li>NVIDIA 기반 고속 추론 최적화 엔진</li>
  <li>Layer fusion, kernel auto-tune, quantization 내장</li>
  <li>YOLO 계열과의 궁합이 뛰어남</li>
  <li>엔진 생성 시점의 GPU 아키텍처에 종속되는 구조</li>
</ul>

<h5 id="하드웨어-종속성과-아키텍처-호환성">하드웨어 종속성과 아키텍처 호환성</h5>

<p>TensorRT 엔진은 하드웨어에 종속되는 형식이지만,<br />
동일한 GPU 아키텍처 간에는 호환됩니다.</p>

<p>예를 들어, A10과 A40은 둘 다 Ampere 아키텍처 기반이기 때문에,<br />
동일한 조건에서 생성한 <code class="language-plaintext highlighter-rouge">.trt</code> 파일을 서로 호환하여 사용할 수 있습니다.<br />
실무에서는 A40에서 추출한 엔진 파일을 A10 시스템에 그대로 이식해<br />
테스트/운영을 빠르게 진행하기도 합니다.</p>

<table>
  <thead>
    <tr>
      <th>아키텍처</th>
      <th>출시</th>
      <th>GPU</th>
      <th>특징</th>
      <th>TRT</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Volta</td>
      <td>2017</td>
      <td>V100</td>
      <td>1세대 Tensor Core</td>
      <td>≤8.6</td>
    </tr>
    <tr>
      <td>Ampere</td>
      <td>2020</td>
      <td>A10,A40,A100</td>
      <td>범용 학습·추론</td>
      <td>≤8.6</td>
    </tr>
    <tr>
      <td>Ada Lovelace</td>
      <td>2022</td>
      <td>RTX 4080,4090</td>
      <td>고해상도 비전</td>
      <td>≤8.6</td>
    </tr>
    <tr>
      <td>Hopper</td>
      <td>2022~</td>
      <td>H100,GH200</td>
      <td>LLM 학습 최적</td>
      <td>9.x~</td>
    </tr>
    <tr>
      <td>Blackwell</td>
      <td>2024~</td>
      <td>B100,B200,5090</td>
      <td>초거대 추론 특화</td>
      <td>≥10.8</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>아키텍처별 CUDA와 TensorRT 버전 호환성을 반드시 확인해야 함</p>
  <ul>
    <li>아키텍처가 변경될 경우(예: Ampere → Blackwell) <code class="language-plaintext highlighter-rouge">.trt</code> 엔진 재생성 필요</li>
    <li>TensorRT 8.6.1은 Ampere 및 Ada Lovelace까지만 사용 가능</li>
    <li>Blackwell은 반드시 TensorRT 10.8 이상 + CUDA 12.8 이상 환경에서 생성해야 함</li>
  </ul>
</blockquote>

<hr />

<h3 id="경량화quantization">경량화(Quantization)</h3>

<table>
  <thead>
    <tr>
      <th>Precision</th>
      <th>설명</th>
      <th>특징</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>FP32</td>
      <td>32bit 부동소수점</td>
      <td>가장 정확하지만 무겁고 느림</td>
    </tr>
    <tr>
      <td>FP16</td>
      <td>16bit 부동소수점</td>
      <td>대부분의 실무에서 가장 적절</td>
    </tr>
    <tr>
      <td>INT8</td>
      <td>8bit 정수형</td>
      <td>가장 빠르지만 정확도 저하 우려 있음</td>
    </tr>
  </tbody>
</table>

<p>일반적으로 FP16 + batch=8이 가장 안정적인 설정으로 많이 사용됩니다.<br />
INT8은 별도의 calibration dataset이 필요하며,<br />
작은 객체 탐지에서는 성능 저하 가능성이 있으므로 주의가 필요합니다.</p>

<hr />

<h3 id="학습-배치-vs-추론-배치">학습 배치 vs 추론 배치</h3>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>학습 배치 (train batch)</th>
      <th>추론 배치 (inference batch)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>목적</td>
      <td>모델 업데이트(loss 평균 계산)</td>
      <td>결과 생성 속도 최적화</td>
    </tr>
    <tr>
      <td>설정 기준</td>
      <td>GPU 메모리, 모델 크기 등</td>
      <td>실시간성(FPS), latency 등</td>
    </tr>
    <tr>
      <td>실무 적용 예</td>
      <td>batch=8~64: 학습 안정성 확보</td>
      <td>batch=8: 실시간/반실시간 기준 최적</td>
    </tr>
  </tbody>
</table>

<p>실시간성이 중요한 시스템에서는<br />
batch=8 정도로 latency와 throughput의 균형을 맞추는 경우가 많습니다.<br />
반면, 실시간 반응성이 매우 중요한 경우에는 batch=1 설정도 고려됩니다.</p>

<hr />

<h3 id="실행-최적화로서의-batch-size-조정">실행 최적화로서의 batch size 조정</h3>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>목표</td>
      <td>초당 처리 속도 증가 (throughput), GPU 자원 최대 활용</td>
    </tr>
    <tr>
      <td>영향 받는 요소</td>
      <td>GPU VRAM, 연산 성능, 메모리 대역폭</td>
    </tr>
    <tr>
      <td>예시</td>
      <td>batch=1: 지연(latency) 최소화, batch=8~32: 처리량 최대화</td>
    </tr>
    <tr>
      <td>사용 예</td>
      <td>TensorRT, ONNX Runtime, OpenVINO 등에서도 –batch 옵션 사용</td>
    </tr>
  </tbody>
</table>

<p>즉, batch=8로 설정하는 것은 모델 구조를 바꾸지 않고<br />
실행 환경을 조정하여 성능을 개선하는 실행 최적화 전략(<em>inference optimization</em>)입니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># TensorRT 엔진 변환 실제 사용 예시
</span><span class="n">profile</span><span class="p">.</span><span class="n">set_shape</span><span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">input_name</span><span class="p">,</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">1280</span><span class="p">,</span> <span class="mi">1280</span><span class="p">],</span>  <span class="c1"># 최소 입력 (1장)
</span>                                   <span class="p">[</span><span class="mi">8</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">1280</span><span class="p">,</span> <span class="mi">1280</span><span class="p">],</span>  <span class="c1"># 최적 입력 (8장)
</span>                                   <span class="p">[</span><span class="mi">8</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">1280</span><span class="p">,</span> <span class="mi">1280</span><span class="p">])</span>  <span class="c1"># 최대 입력 (8장)
# min=1, opt=8, max=8 설정 → 실무에서 자주 사용되는 안정적 구성
</span></code></pre></div></div>
<ul>
  <li>단, batch=8 설정 시 추론을 시작하려면 프레임 8장이 먼저 수집되어야 하므로,<br />
입력 프레임 도착 속도에 따라 추가 지연(latency) 이 발생할 수 있습니다.</li>
  <li>실시간 시스템에서는 이 대기 시간이 3~5초 수준의 latency로 나타날 수 있으며,<br />
Throughput을 높이기 위한 선택이 즉시 응답성과는 상충할 수 있다는 점을 고려해야 합니다.</li>
</ul>

<hr />

<h3 id="latency-vs-throughput">Latency vs Throughput</h3>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Latency</td>
      <td>하나의 입력(예: 1장 이미지)이 처리되는 데 걸리는 시간 (단위: ms)</td>
    </tr>
    <tr>
      <td>Throughput</td>
      <td>단위 시간당 처리 가능한 입력 수량 (ex. FPS, samples/sec)</td>
    </tr>
  </tbody>
</table>

<ul>
  <li>실시간성이 중요한 경우 Latency 최소화</li>
  <li>대량 데이터 분석이 중요한 경우 Throughput 최대화</li>
</ul>

<p>→ 이 둘은 Trade-Off 관계에 있으므로 목적에 따라 조정해야 합니다.</p>

<h4 id="vision-ai-기준에서의-latency-vs-throughput">Vision AI 기준에서의 Latency vs Throughput</h4>

<p>Vision AI에서는</p>
<ul>
  <li>Latency는 실시간 반응성을 결정하고,</li>
  <li>Throughput은 전체 시스템 처리 효율에 영향을 미칩니다.</li>
</ul>

<p>예시:</p>
<ul>
  <li>Latency: 3초 → 프레임 입력 후 3초 뒤 결과 도달</li>
  <li>Throughput: 10 FPS → 초당 10장의 프레임 처리 가능</li>
</ul>

<p>두 지표는 일반적으로 반비례 관계이며,<br />
모델 처리 속도, 프레임 수집 주기, 배치 크기, 스트림 버퍼링 등의 요소에 따라 달라집니다.</p>

<blockquote>
  <ul>
    <li>실시간 알림이 중요한 시스템 → 낮은 Latency</li>
    <li>병렬 분석이나 대량 처리 목적 → 높은 Throughput</li>
  </ul>
</blockquote>

<p>일반적으로 Throughput을 높이기 위해 batch size를 증가시키면 Latency가 길어지고,<br />
Latency를 줄이기 위해 batch size를 낮추면 Throughput은 감소합니다.</p>

<p>Vision AI 시스템에서는<br />
실제 사용 목적에 맞는 Latency/Throughput 밸런스를 설계하는 것이 핵심입니다.</p>

<hr />

<h3 id="실무-사례-실시간이지만-모든-프레임을-처리하지-않는-구조">실무 사례: 실시간이지만 모든 프레임을 처리하지 않는 구조</h3>

<p>현재 담당 중인 제품은 실시간 CCTV 기반이지만 모든 프레임을 추론하지는 않으며,<br />
업무 목적에 맞는 반응성 확보를 목표로 시스템이 설계되어 있습니다.</p>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>과거 설정</th>
      <th>현재 설정</th>
      <th>개선 효과</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>배치 크기</td>
      <td>batch=16</td>
      <td>batch=8</td>
      <td>메모리 사용량 감소, latency 완화</td>
    </tr>
    <tr>
      <td>Latency</td>
      <td>수 초 이상</td>
      <td>약 3~5초 수준</td>
      <td>업무 목적상 허용 가능한 지연</td>
    </tr>
    <tr>
      <td>처리 방식</td>
      <td>모든 프레임 처리</td>
      <td>프레임 샘플링 + 배치 추론</td>
      <td>안정성과 효율성 균형 확보</td>
    </tr>
  </tbody>
</table>

<hr />

<h4 id="처리-구조-설명">처리 구조 설명</h4>

<ol>
  <li>CCTV 스트림으로부터 초당 30프레임(FPS)의 영상이 시스템에 수신됨</li>
  <li>프레임 샘플링을 통해 초당 약 10장의 프레임만 분석 대상으로 선택</li>
  <li>선택된 프레임은 내부 큐에 저장되어 batch 단위로 묶임</li>
  <li>batch가 채워지면 TensorRT 엔진을 통해 추론 수행</li>
  <li>추론 결과는 후처리 과정을 거쳐 알림 시스템에 전달됨<br />
(이때 알림 시스템의 처리 속도에 따라 추가적인 지연이 발생할 수 있음)</li>
</ol>

<hr />

<h4 id="왜-모든-프레임을-처리하지-않는가">왜 모든 프레임을 처리하지 않는가?</h4>

<ul>
  <li>모든 프레임을 처리하면 GPU 점유율 급상승 및 시스템 불안정성 초래</li>
  <li>90% 이상의 GPU 점유율은 멀티 채널 확장 및 장시간 운영에 위험</li>
  <li>작업자가 빠르게 이동하거나 카메라가 흔들리는 경우 등에서<br />
프레임 일부를 선택적으로 처리해도 안전 이벤트 감지에는 문제가 없음</li>
  <li>업무 목적은 “즉시 탐지”가 아니라, “위험 상황 발생 시 경고”이기 때문에<br />
3~5초 지연은 업무상 허용 가능한 수준</li>
</ul>

<hr />

<h4 id="병목-현상">병목 현상</h4>

<p>현재 시스템은 전체적으로 안정적으로 동작하지만,<br />
간헐적인 처리 지연 또는 추론 큐 대기 시간 증가 현상이 관찰되고 있습니다.</p>

<ul>
  <li>영상 수신 지연, 큐 적체, 배치 대기, 후처리 전송 등<br />
다양한 병목 가능성이 존재하며</li>
  <li>현재는 정확한 원인 파악을 위한 프로파일링을 진행 중입니다.</li>
</ul>

<hr />

<h4 id="실시간성의-재정의">실시간성의 재정의</h4>

<table>
  <thead>
    <tr>
      <th>구분</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>잘못된 정의</td>
      <td>모든 프레임을 가능한 한 빠르게 처리하는 것</td>
    </tr>
    <tr>
      <td>올바른 정의</td>
      <td>업무 목적에 맞는 속도로 반응 가능한 시스템을 구성하는 것</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>실시간성은 “속도”가 아니라 “적시성”입니다.<br />
<strong>“언제까지 반응하면 되는가”</strong>를 기준으로 설계 방향을 잡아야 합니다.</p>
</blockquote>

<hr />

<h3 id="정리">정리</h3>

<p>최적화와 경량화는 AI 모델을 실제 제품 환경에 맞게 다듬는 핵심 과정입니다.</p>

<ul>
  <li>실시간성과 정확도의 균형점 찾기</li>
  <li>시스템 자원과 요구사항에 맞는 최적화 전략 수립</li>
  <li>플랫폼 및 환경에 맞는 이식성 확보</li>
  <li>실무 환경에 적합한 배치 처리 구조 설계</li>
</ul>

<p>추론 속도, 메모리 사용량, 배치 처리 구조, 플랫폼 이식성까지 고려해야 하며,<br />
그 모든 조합의 끝에서 “운영 가능한 AI 모델”이 완성됩니다.</p>

<hr />

<h2 id="9-분석엔진-개발">9. 분석엔진 개발</h2>

<p>모델 최적화 및 경량화가 완료되었더라도, 바로 제품에 탑재할 수는 없습니다.<br />
모델을 어떻게 호출하고, 결과를 어떤 방식으로 처리·활용할 것인지까지 모두 구조화해야 합니다.<br />
이 과정을 분석엔진 개발이라 합니다.</p>

<p>분석엔진은 단순히 모델을 실행하는 것을 넘어,<br />
여러 모델을 통합하고 외부 시스템과 연동하는 AI 제품의 핵심 미들웨어 역할을 수행합니다.</p>

<p>특히 실시간 CCTV 분석처럼 멀티 채널이 기본인 환경에서는<br />
분석엔진 구조가 시스템의 안정성과 확장성을 좌우합니다.</p>

<hr />

<h3 id="분석엔진-구조의-핵심-요소">분석엔진 구조의 핵심 요소</h3>

<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>
    <tr>
      <td>블록 기반 구조</td>
      <td>각 분석 기능을 Block 단위로 분리하여 재사용성과 유지보수 향상</td>
    </tr>
    <tr>
      <td>알람 로직 내장</td>
      <td>조건 기반 이벤트 감지 및 시스템 알림 연동 가능</td>
    </tr>
  </tbody>
</table>

<p>이러한 구조를 갖추면, 모델이 변경되더라도 외부 시스템과의 연동 방식은 동일하게 유지됩니다.<br />
또한, 결과값은 서비스단에서 후처리하거나<br />
웹과 연동하기에 적합한 형태로 출력되도록 구성되어 있습니다.</p>

<hr />

<h3 id="제품-적용-시-고려사항">제품 적용 시 고려사항</h3>

<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>FPS, 메모리 사용률, latency 등 시스템 자원과 성능 관리</td>
    </tr>
    <tr>
      <td>인터페이스 설계</td>
      <td>다양한 모델을 유연하게 통합하기 위한 공통 구조 필요</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="인터페이스-개발과-모델-통합">인터페이스 개발과 모델 통합</h3>

<p>Vision AI 시스템은 시간이 지남에 따라 지속적으로 새로운 모델이 추가됩니다.<br />
따라서 분석엔진은 유연한 인터페이스 구조를 가져야 하며,<br />
모델별 통합을 위한 개발이 중요합니다.</p>

<p>모델 인터페이스는 다음과 같은 함수 구조를 따릅니다:</p>

<table>
  <thead>
    <tr>
      <th>함수</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>load()</td>
      <td>학습된 모델을 메모리에 로드</td>
    </tr>
    <tr>
      <td>preprocess()</td>
      <td>입력 데이터를 모델이 처리할 수 있도록 전처리</td>
    </tr>
    <tr>
      <td>inference()</td>
      <td>모델을 실행하여 결과 도출</td>
    </tr>
    <tr>
      <td>postprocess()</td>
      <td>모델의 출력을 서비스에 맞게 가공</td>
    </tr>
  </tbody>
</table>

<p>이러한 구조는 엔진에서 호출되는 방식이 동일하게 유지되기 때문에,<br />
새로운 모델이 추가되더라도 인터페이스만 맞춰주면 통합이 가능합니다.</p>

<p>또한 모델별로 입력 해상도, 출력 형태, 후처리 방식이 다르므로<br />
config 파일을 함께 구성하여 데이터 흐름을 정의합니다.</p>

<hr />

<h3 id="정리-1">정리</h3>

<p>분석엔진은 AI 모델을 실제 서비스로 연결하는 핵심 인프라입니다.</p>

<ul>
  <li>모델 통합을 위한 공통 구조 설계</li>
  <li>안정적인 연동을 위한 표준화된 출력 처리</li>
  <li>실시간성과 유지보수까지 고려한 구조</li>
  <li>알람 로직을 통한 즉각 대응 체계 마련</li>
  <li>신규 모델도 인터페이스만 맞추면 손쉽게 통합 가능</li>
</ul>

<blockquote>
  <p>분석엔진의 구조가 곧 제품의 신뢰성과 품질을 결정합니다.<br />
기술을 서비스로 만드는 마지막 관문이기에<br />
구조화와 모듈화에 집중된 설계가 필요합니다.</p>
</blockquote>

<hr />

<h2 id="10-시스템-배포-및-운영">10. 시스템 배포 및 운영</h2>

<p>분석엔진을 실제 서비스에 적용하려면<br />
① 시스템 연동 → ② 현장 설치 → ③ 안정적인 운영의<br />
세 단계를 순차적으로 거쳐야 합니다.</p>

<hr />

<h3 id="연동-외부-시스템과-연결하기">연동: 외부 시스템과 연결하기</h3>

<p>분석 결과를 사용자에게 전달하고, 외부 시스템과 통합하는 단계입니다.<br />
분석 요청부터 결과 시각화, 스트리밍, 저장, 음성 경고 기능까지 연동하는 것이 주요 역할입니다.</p>

<table>
  <thead>
    <tr>
      <th>구성 요소</th>
      <th>역할</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Flask (API Agent)</td>
      <td>REST API 서버</td>
      <td>사용자 요청을 분석엔진에 전달하는 웹 인터페이스</td>
    </tr>
    <tr>
      <td>Redis</td>
      <td>임시 저장소</td>
      <td>분석 상태, 시나리오 정보 공유용 메모리 DB</td>
    </tr>
    <tr>
      <td>RTMP</td>
      <td>스트리밍 프로토콜</td>
      <td>실시간 영상 전송을 위한 표준 프로토콜</td>
    </tr>
    <tr>
      <td>Ant Media Server</td>
      <td>미디어 서버</td>
      <td>RTMP를 웹 브라우저용으로 변환하는 서버</td>
    </tr>
    <tr>
      <td>IP Speaker</td>
      <td>음성 경고</td>
      <td>TTS 음성 안내 송출 (예: “보호구를 착용해주세요”)</td>
    </tr>
  </tbody>
</table>

<p>이 구성 요소들은 분석엔진 바깥에서 동작하며,<br />
사용자 요청 → 분석 실행 → 결과 전송 → 현장 음성 경고 송출까지의 흐름을 연결합니다.</p>

<p>화학공장에서는 시각적 알림만으로는 즉각적인 대응이 어려운 상황을 고려해,<br />
IP Speaker를 활용한 실시간 음성 안내 시스템을 구축하였습니다.</p>

<hr />

<h4 id="연동-흐름-예시">연동 흐름 예시</h4>

<ol>
  <li>사용자가 웹에서 분석 요청</li>
  <li>Flask 서버가 요청을 분석엔진에 전달</li>
  <li>분석 결과 영상이 RTMP로 송출</li>
  <li>Ant Media Server를 통해 웹에 실시간 중계</li>
  <li>Redis 등을 통해 상태값 공유 및 알림 연동</li>
  <li>분석 결과에 따라 관제 시스템 알림 및 IP Speaker를 통한 음성 알림 송출</li>
</ol>

<h4 id="시스템-데이터-흐름-다이어그램">시스템 데이터 흐름 다이어그램</h4>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[웹 브라우저] ←→ [Flask API] ←→ [분석엔진]
                      ↓
                  [Redis DB]
                      ↓
[분석엔진] → [RTMP] → [Ant Media Server] → [웹 브라우저]
                      ↓
                  [IP Speaker]
</code></pre></div></div>

<p>데이터 흐름 설명:</p>
<ul>
  <li>요청 흐름: 웹 → Flask → 분석엔진 (분석 시작/중지)</li>
  <li>상태 관리: Flask ↔ Redis (분석 상태, 설정 정보 공유)</li>
  <li>영상 스트리밍: 분석엔진 → RTMP → Ant Media Server → 웹 (실시간 영상 중계)</li>
  <li>음성 알림: 분석 결과 → IP Speaker (현장 음성 경고)</li>
</ul>

<hr />

<h3 id="설치-실제-환경에-적용하기">설치: 실제 환경에 적용하기</h3>

<p>시스템 연동이 완료되면, 이를 실제 현장 환경에 설치하고 안정적으로 작동하도록 구성합니다.<br />
이 때 하드웨어 제약, 네트워크 환경, 보안 정책 등 실무 요소를 종합적으로 고려해야 합니다.</p>

<h4 id="배포-환경-고려사항">배포 환경 고려사항</h4>

<table>
  <thead>
    <tr>
      <th>고려 항목</th>
      <th>세부 내용</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>하드웨어 환경</td>
      <td>GPU 사양, 메모리, 저장 공간 등</td>
    </tr>
    <tr>
      <td>네트워크 구성</td>
      <td>폐쇄망, 인터넷 연결, 포트 및 대역폭 설정</td>
    </tr>
    <tr>
      <td>보안 정책</td>
      <td>방화벽, 접근 권한, 데이터 암호화 등</td>
    </tr>
    <tr>
      <td>운영 환경</td>
      <td>Linux, Windows, Docker 등 컨테이너 구성 포함</td>
    </tr>
    <tr>
      <td>모니터링 체계</td>
      <td>로그 수집, 성능 지표, 이상 감지 등</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="운영-안정적인-사용을-위한-관리-체계">운영: 안정적인 사용을 위한 관리 체계</h3>

<p>운영 단계에서는 시스템을 안정적·지속적으로 유지할 모니터링과 대응 체계가 필요합니다.
예기치 못한 성능 저하나 사용자 피드백에 유연하게 대응할 수 있어야 합니다.</p>

<p>또한, 시운전 및 실제 사용 과정에서 수집된 데이터를 바탕으로<br />
모델 재학습과 성능 개선 작업을 지속적으로 수행합니다.<br />
이러한 재학습 활동은 AI 시스템의 안정적 운영을 위한 핵심 유지보수 과정의 하나입니다.</p>

<h4 id="운영-모니터링-항목">운영 모니터링 항목</h4>

<table>
  <thead>
    <tr>
      <th>항목</th>
      <th>설명</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>시스템 성능</td>
      <td>CPU, GPU, 메모리, 네트워크 사용률 등</td>
    </tr>
    <tr>
      <td>모델 성능</td>
      <td>추론 속도, 정확도, 오탐률 등</td>
    </tr>
    <tr>
      <td>로그 분석</td>
      <td>에러 발생 시점, 원인 추적, 경고 로그 확인</td>
    </tr>
    <tr>
      <td>사용자 피드백</td>
      <td>기능 개선 요청, 사용 편의성 검토 등</td>
    </tr>
  </tbody>
</table>

<hr />

<h3 id="시운전-및-모델-개선">시운전 및 모델 개선</h3>

<p>시운전 단계에서는 실시간 분석 품질과 오탐/미탐 여부를 집중적으로 점검하였습니다.<br />
현장 적용 초기에 수집된 결과를 기반으로,<br />
다음과 같은 절차로 모델 재학습 및 버전 업데이트가 이루어졌습니다.</p>

<h4 id="화학공장에서의-모델-유지보수-및-재학습-사례">화학공장에서의 모델 유지보수 및 재학습 사례</h4>

<ol>
  <li>초기 시운전
    <ul>
      <li>YOLOv11L 모델을 기반으로 시운전 시작</li>
      <li>실시간 분석 품질, 오탐/미탐 여부 집중 점검</li>
    </ul>
  </li>
  <li>오탐 수집 및 재학습
    <ul>
      <li>시운전 중 수집된 오탐 데이터를 기반으로</li>
      <li>AI Researcher가 YOLOv12L 모델로 업데이트 및 재학습 수행</li>
    </ul>
  </li>
  <li>성능 문제 발생
    <ul>
      <li>YOLOv12L 적용 시 현장 시스템에서 OOM(Out-Of-Memory) 문제 발생</li>
      <li>메모리 부족으로 안정적인 운용 어려움</li>
    </ul>
  </li>
  <li>모델 개선 및 최적화
    <ul>
      <li>AI Engineer가 모델 크기와 추론 효율을 고려</li>
      <li>YOLOv12M으로 모델 변경 후, 재학습 및 최적화</li>
    </ul>
  </li>
  <li>운영 모델 구축 및 탑재
    <ul>
      <li>경량화된 YOLOv12M 모델을 분석엔진에 탑재</li>
      <li>실시간 운영에 적합한 모델로 최종 구축</li>
    </ul>
  </li>
</ol>

<blockquote>
  <p>실무에서는 더 큰 모델이 항상 더 좋은 성능을 보장하지 않으며,<br />
운영 환경에 맞는 모델 크기 조정과 메모리 최적화가 중요합니다.</p>
</blockquote>

<hr />

<h3 id="정리-2">정리</h3>

<p>시스템 배포 및 운영은<br />
AI 모델이 실제 환경에서 안정적으로 작동할 수 있도록<br />
구성 요소를 연동하고, 운영 환경에 맞춰 설치 및 유지관리하는 단계입니다.</p>

<ul>
  <li>시스템 연동: 분석 요청부터 음성 알림까지 외부 시스템과 연결</li>
  <li>현장 설치: 하드웨어, 네트워크, 보안 환경에 맞춘 구성</li>
  <li>운영 관리: 성능 모니터링 및 사용자 피드백 반영</li>
  <li>모델 유지보수: 시운전 → 오탐 수집 → 경량화 재학습 순환 구조</li>
</ul>

<blockquote>
  <p>시스템 배포와 운영은 기술을 제품으로 완성하는 마지막 실행 단계입니다.<br />
현장의 제약 속에서도 안정성과 확장성을 유지할 수 있도록<br />
설계와 설치, 유지보수까지 하나의 흐름으로 이어져야 합니다.</p>
</blockquote>

<hr />

<h2 id="vision-ai-제품-개발에서-ai-engineer의-역할">Vision AI 제품 개발에서 AI Engineer의 역할</h2>

<p>앞서 살펴본 10단계는<br />
AI 분석 시스템을 기획 의도에 맞춰 구현하고, 현장에서 작동할 수 있도록 준비하는<br />
End-to-End AI Engineer의 책임 범위입니다.</p>

<p>이후에는 분석 결과를 사용자에게 제공하고,<br />
웹을 통해 제어 가능한 형태로 연동하는 작업이 이어집니다.</p>

<p>해당 영역은 프론트엔드 및 백엔드 개발을 포함한 웹 서비스 구축의 영역에 해당합니다.</p>

<hr />

<h2 id="마무리-vision-ai-제품-개발-그-전-과정을-돌아보며">마무리: Vision AI 제품 개발, 그 전 과정을 돌아보며</h2>

<p>AI 모델을 만드는 일도 결코 간단하지 않지만,<br />
그 모델을 실제 현장에 적용해 안정적으로 작동하도록 만드는 일은<br />
또 다른 차원의 과제입니다.</p>

<p>Vision AI 제품 개발은 알고리즘 구현에 국한되지 않으며,<br />
현실의 문제를 해결하는 실용적 시스템을 완성하는 과정에 가깝습니다.</p>

<p>Task 정의부터 데이터 수집, 모델 학습, 분석엔진 개발, 시스템 배포와 운영에 이르기까지<br />
모든 단계는 서로 유기적으로 연결되어 있습니다.</p>

<p>각 과정은 기술과 실무, 이론과 현실의 경계를 넘나들며 완성에 이릅니다.</p>

<p>이 글을 통해 단지 “모델을 잘 만드는 방법”이 아니라,<br />
AI를 제품으로, 그리고 가치로 전환하는 과정의 본질에 조금 더 가까워졌기를 바랍니다.</p>

<p>결국 중요한 것은 얼마나 정교한 모델을 만들었느냐가 아니라,<br />
얼마나 현실에서 유용하고 지속 가능한 방식으로 구현했느냐일 것입니다.</p>

<p>Vision AI 제품화는 기술의 완성 이전에, 사람과 현실을 이해하는 데서 시작됩니다.</p>

<hr />]]></content><author><name>indexkim</name></author><category term="vision-ai" /><category term="product" /><category term="model" /><category term="engine" /><summary type="html"><![CDATA[이 글은 Vision AI 제품 개발 과정의 세 번째 편으로, 전체 내용은 다음과 같은 흐름으로 구성되어 있습니다:]]></summary></entry></feed>