<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">

 <title>SON Kiseok Technical Blog</title>
 <link href="/atom.xml" rel="self"/>
 <link href="https://son1004007.github.io/"/>
 <updated>2026-09-08T03:21:17+00:00</updated>
 <id>https://son1004007.github.io</id>
 <author>
   <name>SON Kiseok</name>
   <email></email>
 </author>

 
 <entry>
   <title>채용공고 데이터로 내 커리어 방향을 분석해 본 과정: 검색어 표본에서 JobKorea Native Filter까지</title>
   <link href="https://son1004007.github.io/career-data-analysis/2026/09/08/career-job-posting-data-analysis/"/>
   <updated>2026-09-08T00:00:00+00:00</updated>
   <id>https://son1004007.github.io/career-data-analysis/2026/09/08/career-job-posting-data-analysis</id>
   <content type="html">&lt;h2 id=&quot;왜-커리어를-데이터로-분석했나&quot;&gt;왜 커리어를 데이터로 분석했나&lt;/h2&gt;

&lt;p&gt;커리어 방향을 정할 때 흔히 하는 방법은 다음과 같다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;지금까지 해온 기술을 보고 잘 맞아 보이는 직무를 추천한다.&lt;/li&gt;
  &lt;li&gt;앞으로 유망할 것 같은 직무를 고른다.&lt;/li&gt;
  &lt;li&gt;자격증이나 학위를 추가하면 갈 수 있을 것 같은 직무를 찾는다.&lt;/li&gt;
  &lt;li&gt;몇 개의 채용공고를 보고 시장 전체가 그런 것처럼 판단한다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;문제는 이런 방식이 대부분 &lt;strong&gt;추론과 인상에 의존한다는 점&lt;/strong&gt;이다.&lt;/p&gt;

&lt;p&gt;나는 약 11년의 IT 경력 중 네트워크·시스템·보안과 애플리케이션 개발을 모두 경험했고, 최근에는 Java/Spring, Python, 데이터 처리, LLM/RAG/Agent 기반 업무 시스템을 개발하고 있다.&lt;/p&gt;

&lt;p&gt;이런 경력은 Backend, AI Application, Security, IAM, AppSec, IT Audit, IT Risk, GRC 등 여러 방향으로 설명할 수 있다. 그래서 AI에게 단순히 “내게 어떤 직업이 좋은가”라고 묻는 대신 다음 질문으로 바꾸었다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;실제 채용시장에서 내 경력을 가장 많이 인정하면서, 보상은 높이고 구조적인 운영·온콜·야간·주말 책임은 줄일 수 있는 직무는 무엇인가?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;그리고 이 질문은 가능한 한 실제 채용공고 데이터로 확인하기로 했다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;첫-번째-실패-검색어로-직군별-40개를-맞춰-수집했다&quot;&gt;첫 번째 실패: 검색어로 직군별 40개를 맞춰 수집했다&lt;/h1&gt;

&lt;p&gt;처음에는 다음과 같은 분석용 직군을 임의로 만들었다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Backend / AI Backend
IAM / Auth
Product / AppSec
Cloud Security / Security Platform
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;그리고 JobKorea에서 여러 검색어를 사용한 뒤 각 직군마다 40건씩 채워 160건의 표본을 만들었다.&lt;/p&gt;

&lt;p&gt;예를 들면 다음과 같은 검색어였다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Backend
- 백엔드 개발자
- Java Spring 백엔드
- AI 백엔드

IAM
- IAM
- SSO
- 통합인증
- 인증 인가

AppSec
- AppSec
- Application Security
- 개발보안
- 시큐어코딩
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;겉보기에는 균형 잡힌 데이터처럼 보였지만 분석 방법 자체에 문제가 있었다.&lt;/p&gt;

&lt;h2 id=&quot;문제가-된-이유&quot;&gt;문제가 된 이유&lt;/h2&gt;

&lt;p&gt;첫째, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Backend_AI&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IAM_Auth&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Product_AppSec&lt;/code&gt;은 &lt;strong&gt;JobKorea가 제공하는 직군이 아니라 분석을 위해 임의로 만든 분류&lt;/strong&gt;였다.&lt;/p&gt;

&lt;p&gt;둘째, 검색 결과에는 잡음이 많이 섞였다. 실제로 Backend 표본 안에 다음과 같은 공고도 들어왔다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;K8S Cloud 운영 개발자&lt;/li&gt;
  &lt;li&gt;자동매매 프로그램 개발&lt;/li&gt;
  &lt;li&gt;CCTV/IP Camera 펌웨어 개발&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;셋째, 각 분류를 강제로 40개씩 채웠기 때문에 실제 시장 규모를 알 수 없었다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Backend 시장이 1,000건이고 IAM이 30건이어도
수집 결과는 각각 40건이 된다.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 데이터로는 다음 질문에 답할 수 없다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;어느 직무의 채용시장이 더 큰가?&lt;/li&gt;
  &lt;li&gt;실제로 지원 가능한 공고가 얼마나 되는가?&lt;/li&gt;
  &lt;li&gt;IAM이나 AppSec이 Backend보다 더 좋은 시장인가?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;따라서 &lt;strong&gt;검색어 기반 160건 표본은 커리어 의사결정용 데이터에서 제외&lt;/strong&gt;했다.&lt;/p&gt;

&lt;p&gt;이 작업에서 가장 중요했던 교훈은 다음이다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;분류 결과가 데이터 수집 여부를 결정하면 안 된다. 먼저 모집단을 수집하고, 직무 분류는 그 다음에 해야 한다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;두-번째-방법-jobkorea가-실제-제공하는-필터를-모집단으로-사용&quot;&gt;두 번째 방법: JobKorea가 실제 제공하는 필터를 모집단으로 사용&lt;/h1&gt;

&lt;p&gt;다음 분석부터는 검색어를 사용하지 않았다.&lt;/p&gt;

&lt;p&gt;브라우저 자동화로 JobKorea 검색 화면의 실제 필터 구조와 내부 값을 확인했다.&lt;/p&gt;

&lt;p&gt;JobKorea에는 다음과 같은 상위 필터가 존재했다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;직무
지역
경력
기업형태
학력
고용형태
조건추가
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AI·개발·데이터&lt;/code&gt; 아래에는 다음과 같은 JobKorea 자체 세부 직무가 있었다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;백엔드개발자
프론트엔드개발자
웹개발자
앱개발자
시스템엔지니어
네트워크엔지니어
DBA
데이터엔지니어
데이터사이언티스트
보안엔지니어
소프트웨어개발자
AI/ML엔지니어
블록체인개발자
클라우드엔지니어
IT컨설팅
QA
AI/ML연구원
데이터분석가
프롬프트엔지니어
AI보안전문가
MLOps엔지니어
AI서비스개발자
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;여기서 현재 경력 및 장기 커리어와 연결되는 JobKorea native 직무만 선택했다.&lt;/p&gt;

&lt;h2 id=&quot;primary-pool-조건&quot;&gt;Primary Pool 조건&lt;/h2&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;지역
- 서울 전지역

경력
- 경력

고용형태
- 정규직

기업형태
- 대기업
- 30대그룹사
- 매출1000대기업
- 중견기업
- 외국계기업
- 코스피
- 코스닥
- 해외상장

직무
- 백엔드개발자
- 시스템엔지니어
- 데이터엔지니어
- 보안엔지니어
- 소프트웨어개발자
- AI/ML엔지니어
- 클라우드엔지니어
- AI보안전문가
- MLOps엔지니어
- AI서비스개발자
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;연봉 5,000만원 이상 같은 조건은 사용하지 않았다.&lt;/p&gt;

&lt;p&gt;이유는 높은 연봉을 지급하는 회사도 공고에 연봉을 공개하지 않는 경우가 많아, 연봉 공개 여부로 모집단을 자르면 오히려 좋은 회사를 제거할 수 있기 때문이다.&lt;/p&gt;

&lt;p&gt;이 native filter를 실제 브라우저에서 적용한 결과 &lt;strong&gt;182개의 공고&lt;/strong&gt;가 남았다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;182건의-상세-jd를-전부-확보&quot;&gt;182건의 상세 JD를 전부 확보&lt;/h1&gt;

&lt;p&gt;목록만 분석하면 기술 요구사항이나 실제 업무를 알 수 없기 때문에 182건의 상세페이지를 모두 수집했다.&lt;/p&gt;

&lt;p&gt;초기에는 JobPosting JSON-LD가 있는 공고만 읽었는데 결과가 다음처럼 나왔다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;전체 공고        182
JSON-LD 확보      36
JSON-LD 없음     146
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;146건을 버리면 데이터가 크게 왜곡되므로 Chromium으로 실제 페이지를 렌더링하고 다음 순서로 fallback을 구성했다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;JSON-LD
   ↓ 없거나 description이 지나치게 짧음
렌더링된 DOM
   ↓
공고 iframe
   ↓
모집요강/지원자격 구간 추출
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;최종적으로 다음 상태까지 확보했다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;공고            182 / 182
상세 레코드      182 / 182
본문 200자 이상  182 / 182
누락                0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;또-하나의-오류-페이지-전체-텍스트를-jd로-사용하면-안-된다&quot;&gt;또 하나의 오류: 페이지 전체 텍스트를 JD로 사용하면 안 된다&lt;/h1&gt;

&lt;p&gt;상세본문을 확보한 뒤 처음 분류했을 때 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;K8S Cloud 운영 개발자&lt;/code&gt;가 IAM/Auth로 분류되는 이상한 결과가 나왔다.&lt;/p&gt;

&lt;p&gt;원인을 확인해보니 공고 본문뿐 아니라 페이지 주변의 추천공고, 회사정보, 관련 콘텐츠 텍스트까지 함께 들어가 있었다.&lt;/p&gt;

&lt;p&gt;따라서 다시 다음 규칙으로 정제했다.&lt;/p&gt;

&lt;h2 id=&quot;직무-분류-규칙&quot;&gt;직무 분류 규칙&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;공고 제목을 최우선으로 사용한다.&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;제목이 일반적인 경우에만 상세 JD를 보조적으로 사용한다.&lt;/li&gt;
  &lt;li&gt;JD는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;담당업무&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;지원자격&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;자격요건&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;우대사항&lt;/code&gt; 등의 모집 구간만 사용한다.&lt;/li&gt;
  &lt;li&gt;추천공고·사이드바·회사소개 등의 주변 텍스트는 제외한다.&lt;/li&gt;
  &lt;li&gt;분류가 애매하면 강제로 특정 직무에 넣지 않는다.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;이 수정 후 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;K8S Cloud 운영 개발자&lt;/code&gt;는 정상적으로 Cloud/Platform 계열로 분류되었다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;임의의-10점-점수를-만들지-않았다&quot;&gt;임의의 10점 점수를 만들지 않았다&lt;/h1&gt;

&lt;p&gt;이번 분석에서는 다음과 같은 방식의 점수를 만들지 않았다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Backend 8.5점
IAM 9점
AppSec 8점
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;가중치를 어떻게 정하느냐에 따라 결과가 마음대로 바뀌기 때문이다.&lt;/p&gt;

&lt;p&gt;대신 공고마다 관찰 가능한 변수를 분리했다.&lt;/p&gt;

&lt;h2 id=&quot;경력-적합성&quot;&gt;경력 적합성&lt;/h2&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;요구경력
현재 직접경력으로 충족 가능한가
인접경력으로만 설명 가능한가
직접 도메인 경력이 별도로 필요한가
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;기술-증거&quot;&gt;기술 증거&lt;/h2&gt;

&lt;p&gt;현재 실제 업무에서 설명 가능한 기술이 JD에 몇 개 등장하는지를 기록했다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Java
Spring
Python
SQL / RDBMS
Linux
Docker
CI/CD
SSO / 인증·인가
LLM
RAG
AI Agent
CISSP 등
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 숫자는 “합격확률”이 아니다.&lt;/p&gt;

&lt;p&gt;단지 &lt;strong&gt;현재 경력기술서에 근거를 제시할 수 있는 요구사항 수&lt;/strong&gt;다.&lt;/p&gt;

&lt;h2 id=&quot;업무구조-위험&quot;&gt;업무구조 위험&lt;/h2&gt;

&lt;p&gt;다음 요소도 별도로 분류했다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;LOW
- 제품/서비스 개발 중심
- 구조적인 운영책임 신호 없음

MEDIUM
- 운영
- 유지보수
- 시스템 관리
- SI 구축
- 고객 대응 등의 요소 존재

HIGH
- 고객사 상주
- 파견
- 24x7
- 온콜
- 교대근무
- 구조적인 야간/주말 대응
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;정제한-182건에서-나온-직무-분포&quot;&gt;정제한 182건에서 나온 직무 분포&lt;/h1&gt;

&lt;p&gt;이 숫자는 &lt;strong&gt;한국 전체 IT 시장의 비율이 아니다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;서울 + 경력 + 정규직 + 우량 기업형태 + 선택한 JobKorea native IT 직무&lt;/code&gt;라는 모집단 안에서의 분포다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;분석 후 직무군&lt;/th&gt;
      &lt;th style=&quot;text-align: right&quot;&gt;공고 수&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Security&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;30&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Cloud / Platform&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;29&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;AI Application&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;24&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;기타 Software&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;20&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;IT Consulting / PM&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;18&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Support / Operations&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;15&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;분류 불충분·혼합&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;13&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;AppSec&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;12&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Backend&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;11&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;IAM / Auth&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;4&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Data Engineering&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;4&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;AI Security&lt;/td&gt;
      &lt;td style=&quot;text-align: right&quot;&gt;2&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;가장 먼저 눈에 띈 것은 IAM과 AI Security 공고가 생각보다 매우 적었다는 점이다.&lt;/p&gt;

&lt;p&gt;이것만으로 시장이 작다고 단정할 수는 없지만, 적어도 “IAM/Product Security가 압도적으로 좋은 다음 커리어”라는 가설을 데이터가 강하게 지지하지는 않았다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;더-중요한-결과는-공고-수가-아니라-업무구조였다&quot;&gt;더 중요한 결과는 공고 수가 아니라 업무구조였다&lt;/h1&gt;

&lt;p&gt;182건의 업무구조 위험을 분류하면 다음과 같았다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;LOW       71 / 182 = 39.0%
MEDIUM   105 / 182 = 57.7%
HIGH       6 / 182 =  3.3%
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;즉 대기업·중견·상장·외국계 등으로 회사 필터를 먼저 걸었는데도 절반 이상에서 다음과 같은 요소가 발견됐다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;운영&lt;/li&gt;
  &lt;li&gt;유지보수&lt;/li&gt;
  &lt;li&gt;구축&lt;/li&gt;
  &lt;li&gt;SI delivery&lt;/li&gt;
  &lt;li&gt;시스템 관리&lt;/li&gt;
  &lt;li&gt;고객 지원&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;따라서 좋은 커리어를 찾기 위해서는 &lt;strong&gt;회사 규모만 필터링해서도 안 된다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;내가 원하는 것은 많은 회사에 지원하는 것이 아니라 다음 조건을 동시에 만족하는 회사다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;높은 보상
+ 기존 경력 인정
+ 낮은 구조적 운영책임
+ 낮은 온콜/야간/주말 위험
+ 40~50대까지 이어질 경력자산
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;backend를-버려야-한다는-근거는-없었다&quot;&gt;Backend를 버려야 한다는 근거는 없었다&lt;/h1&gt;

&lt;p&gt;AI가 코딩을 빠르게 대체하고 있기 때문에 처음에는 Backend 자체에서 빨리 벗어나야 하는지 고민했다.&lt;/p&gt;

&lt;p&gt;하지만 실제 공고를 보면 AI Application 역할에서도 기존 개발 역량이 계속 사용됐다.&lt;/p&gt;

&lt;p&gt;반복적으로 등장한 것은 다음 조합이다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Java / Spring
Python
SQL / RDBMS
Linux
Docker / CI-CD
LLM / RAG / AI Agent
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;즉 AI 시대의 애플리케이션 개발은 Backend를 없애기보다 &lt;strong&gt;Backend 위에 AI 기능을 추가하는 형태&lt;/strong&gt;가 많이 보였다.&lt;/p&gt;

&lt;p&gt;따라서 현재까지의 데이터에서는 다음 가설이 더 적절했다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Backend 경력은 버릴 자산이 아니라, AI·Security·Governance 중 어떤 도메인으로 이동하더라도 가격을 높여주는 기반 자산이다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;product-security--appsec도-생각보다-쉬운-전환은-아니다&quot;&gt;Product Security / AppSec도 생각보다 쉬운 전환은 아니다&lt;/h1&gt;

&lt;p&gt;Security를 개발자와 잘 맞는 다음 경력이라고 생각하기 쉽다.&lt;/p&gt;

&lt;p&gt;하지만 실제 AppSec 공고는 다음과 같은 &lt;strong&gt;직접 경험&lt;/strong&gt;을 별도로 요구하는 경우가 많았다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Secure SDLC&lt;/li&gt;
  &lt;li&gt;SAST / DAST / SCA&lt;/li&gt;
  &lt;li&gt;소스코드 보안검토&lt;/li&gt;
  &lt;li&gt;취약점 분석&lt;/li&gt;
  &lt;li&gt;Threat Modeling&lt;/li&gt;
  &lt;li&gt;보안 테스트&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;즉&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Backend 5년 + 과거 보안경력
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;을 기업이 자동으로&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;AppSec 5년
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;으로 인정한다고 볼 수 없다.&lt;/p&gt;

&lt;p&gt;그래서 AppSec은 여전히 좋은 전문화 후보이지만 &lt;strong&gt;직접 Secure SDLC/AppSec 증거를 추가한 뒤 시장에서 검증해야 하는 방향&lt;/strong&gt;으로 정리했다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;가장-예상-밖의-발견-기술경력을-인정하는-it-audit-시장&quot;&gt;가장 예상 밖의 발견: 기술경력을 인정하는 IT Audit 시장&lt;/h1&gt;

&lt;p&gt;엔지니어링 182건과 별개로 IT Audit / IT Risk / GRC 공고도 교차검증했다.&lt;/p&gt;

&lt;p&gt;처음에는 IT감사로 이동하려면 직접 ITGC·감사 경력이 반드시 필요할 것으로 예상했다.&lt;/p&gt;

&lt;p&gt;실제로 그런 공고가 존재한다.&lt;/p&gt;

&lt;p&gt;하지만 다른 형태의 공고도 확인됐다.&lt;/p&gt;

&lt;h2 id=&quot;기술경력을-feeder로-인정하는-it-audit&quot;&gt;기술경력을 feeder로 인정하는 IT Audit&lt;/h2&gt;

&lt;p&gt;일부 증권사 IT Audit 공고는 다음과 같은 경력을 지원자격으로 인정했다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;개발
운영
인프라
정보보안
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;그리고 다음 기술통제 이해를 요구했다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;시스템 아키텍처
네트워크
서버
DB
정보보안
로그 분석
형상관리
접근통제
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;CISA와 CISSP는 우대조건으로 연결됐다.&lt;/p&gt;

&lt;p&gt;이런 JD는 “감사 신입으로 리셋”하는 직무와 다르다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;11년 IT 기술경력
      ↓
Technology Audit / IT Risk
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;로 기존 기술경력을 변환하는 &lt;strong&gt;Bridge Role&lt;/strong&gt;에 가깝다.&lt;/p&gt;

&lt;p&gt;또 다른 금융사 IT감사 공고에서도 다음 중 하나를 인정하는 사례를 확인했다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;IT감사 직접경력&lt;/li&gt;
  &lt;li&gt;CISA/CIA/ISMS-P 등 자격&lt;/li&gt;
  &lt;li&gt;IT 또는 정보보호 장기 경력&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;따라서 “기술경력에서 Audit/Risk로 직접 이동하는 시장이 존재하는가?”라는 질문에는 이제 &lt;strong&gt;존재한다&lt;/strong&gt;고 답할 수 있다.&lt;/p&gt;

&lt;p&gt;다만 이런 공고가 시장에 얼마나 반복적으로 등장하는지는 더 추적해야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;grc를-하나의-직무로-보면-안-된다&quot;&gt;GRC를 하나의 직무로 보면 안 된다&lt;/h1&gt;

&lt;p&gt;실제 공고를 비교하면서 GRC 내부에서도 진입장벽이 크게 다르다는 것을 확인했다.&lt;/p&gt;

&lt;p&gt;대략 다음처럼 구분해야 했다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;유형&lt;/th&gt;
      &lt;th&gt;관찰된 진입 특성&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Technical IT Audit&lt;/td&gt;
      &lt;td&gt;일반 IT 기술경력을 인정하는 공고가 존재&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;IT Planning + Internal Control&lt;/td&gt;
      &lt;td&gt;개발/IT 경력을 활용하는 Bridge가 존재&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Security Audit&lt;/td&gt;
      &lt;td&gt;보안 경력 + 점검/감사 경험을 요구하는 경우가 많음&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Security Policy&lt;/td&gt;
      &lt;td&gt;내부점검/인증대응 직접경력을 요구하는 경우가 있음&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Privacy Governance&lt;/td&gt;
      &lt;td&gt;개인정보보호 직접 장기경력을 요구하는 상위 포지션이 많음&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;따라서&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Audit = GRC = Privacy = Security Policy
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;처럼 하나로 묶어 비교하면 잘못된 결론이 나온다.&lt;/p&gt;

&lt;p&gt;특히 현재 경력에서는 Privacy Governance보다 &lt;strong&gt;기술형 IT Audit / Technology Risk가 더 자연스러운 Bridge가 될 가능성&lt;/strong&gt;이 확인됐다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;cisa-pmp-aws-자격증은-역할이-다르다&quot;&gt;CISA, PMP, AWS 자격증은 역할이 다르다&lt;/h1&gt;

&lt;p&gt;공고를 보면서 자격증도 이름 자체와 실무 요구를 분리해서 봤다.&lt;/p&gt;

&lt;p&gt;엔지니어링 공고에서는 AWS 자격증보다 &lt;strong&gt;AWS 실제 경험&lt;/strong&gt;이 더 중요한 경우가 많았다.&lt;/p&gt;

&lt;p&gt;따라서 AWS 자격증을 취득하더라도 목적은 자격증 개수를 늘리는 것이 아니라 실제 AWS 설계·구축 경험을 만들기 위한 보조수단이어야 한다.&lt;/p&gt;

&lt;p&gt;반면 CISA는 IT Audit, IT Risk, Security Audit, Security Governance 쪽에서 직접 우대조건으로 반복 등장했다.&lt;/p&gt;

&lt;p&gt;따라서 현재 해석은 다음과 같다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;CISA
→ Audit / IT Risk / Security Governance 시장과 직접 연결

PMP
→ IT기획 / PM / Governance 선택지 확장

AWS Certification
→ Cloud 실무 증거를 보조

CKA / CCIE
→ 현재 목표시장에서는 우선순위가 낮음
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;자격증이 경력을 대신해주지는 않는다.&lt;/p&gt;

&lt;p&gt;중요한 것은 &lt;strong&gt;그 자격증이 실제 목표 JD에서 어떤 경력과 함께 요구되는가&lt;/strong&gt;다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;회사-평균연봉도-그대로-사용하지-않았다&quot;&gt;회사 평균연봉도 그대로 사용하지 않았다&lt;/h1&gt;

&lt;p&gt;커리어 분석에서 연봉 데이터는 매우 중요하지만 공개 데이터의 함정도 크다.&lt;/p&gt;

&lt;p&gt;회사 평균연봉은 다음과 같지 않다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;회사 평균연봉 = 내가 받을 연봉
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;직군, 직급, 성과급, 계약연봉 구조가 모두 다르기 때문이다.&lt;/p&gt;

&lt;p&gt;그래서 회사 평균연봉은 최종 연봉예측에 사용하지 않고 &lt;strong&gt;회사 보상수준의 proxy&lt;/strong&gt;로만 사용하기로 했다.&lt;/p&gt;

&lt;p&gt;실제 최종 지원 후보에서는 다음 순서로 확인해야 한다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;공고에 공개된 position salary band&lt;/li&gt;
  &lt;li&gt;같은 회사·유사 직급의 시장 데이터&lt;/li&gt;
  &lt;li&gt;회사 전체 평균연봉&lt;/li&gt;
  &lt;li&gt;실제 면접/오퍼 데이터&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;결국 가장 정확한 개인 데이터는 &lt;strong&gt;실제 지원 결과와 실제 오퍼&lt;/strong&gt;다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;지금까지의-커리어-가설&quot;&gt;지금까지의 커리어 가설&lt;/h1&gt;

&lt;p&gt;현재까지 확보한 데이터만 놓고 보면 과거의 다음 가설은 약해졌다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Backend를 빨리 버린다.
Product Security 또는 IAM을 다음 직무로 확정한다.
Cloud 자격증을 차례대로 취득한다.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;대신 다음 가설이 더 강해졌다.&lt;/p&gt;

&lt;h2 id=&quot;1-technical-it-audit--it-risk&quot;&gt;1. Technical IT Audit / IT Risk&lt;/h2&gt;

&lt;p&gt;단, 반드시 다음 조건을 만족하는 공고만 본다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;개발/인프라/보안 등 일반 IT 경력을 인정
+ 감사 신입 리셋 없음
+ 구조적 온콜/야간 책임 낮음
+ 보상 상승 가능
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;2-it-planning--internal-control--audit-bridge&quot;&gt;2. IT Planning + Internal Control / Audit Bridge&lt;/h2&gt;

&lt;p&gt;기존 IT 구현 경험을 유지하면서 통제·감사 직접경력 한 줄을 만드는 방향이다.&lt;/p&gt;

&lt;h2 id=&quot;3-security-audit--technical-security-governance&quot;&gt;3. Security Audit / Technical Security Governance&lt;/h2&gt;

&lt;p&gt;CISSP 및 과거 보안경력이 강점이지만 직접 점검·감사·인증경력은 별도로 확인해야 한다.&lt;/p&gt;

&lt;h2 id=&quot;4-좋은-backend--ai-application&quot;&gt;4. 좋은 Backend / AI Application&lt;/h2&gt;

&lt;p&gt;좋은 제품회사에서 보상과 WLB가 충분하다면 계속 경쟁시킨다.&lt;/p&gt;

&lt;p&gt;즉 커리어 전환 자체가 목적이 아니다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;같은 시간에 더 높은 보상을 받고, 지금까지 만든 경력자산을 더 오래 활용할 수 있는가가 목적이다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;분석하면서-폐기한-데이터도-기록해야-한다&quot;&gt;분석하면서 폐기한 데이터도 기록해야 한다&lt;/h1&gt;

&lt;p&gt;데이터 분석에서는 성공한 결과만 남기면 안 된다.&lt;/p&gt;

&lt;p&gt;이번 작업에서는 다음 데이터를 의사결정 근거에서 제외했다.&lt;/p&gt;

&lt;h2 id=&quot;검색어-기반-160건-표본&quot;&gt;검색어 기반 160건 표본&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;임의 직군을 먼저 정의함&lt;/li&gt;
  &lt;li&gt;검색 결과가 모집단이 됨&lt;/li&gt;
  &lt;li&gt;각 분류를 40건으로 강제함&lt;/li&gt;
  &lt;li&gt;시장규모를 왜곡함&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;따라서 폐기했다.&lt;/p&gt;

&lt;h2 id=&quot;governanceaudit-68건-1차-stratified-sample&quot;&gt;Governance/Audit 68건 1차 stratified sample&lt;/h2&gt;

&lt;p&gt;IT Risk/Audit, Security GRC, Privacy, Security Review를 별도 검색해 68건을 수집했지만 품질검사에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IT_RISK_AUDIT&lt;/code&gt; 표본 대부분에 Privacy 신호가 나타나는 비정상 패턴을 확인했다.&lt;/p&gt;

&lt;p&gt;상세페이지의 본문 구간이 충분히 정제되지 않아 페이지 주변 텍스트가 섞인 것으로 판단했다.&lt;/p&gt;

&lt;p&gt;따라서 이 68건의 &lt;strong&gt;집계 수치는 현재 커리어 결론에 사용하지 않는다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;다만 이 실패 덕분에 Governance 분석도 엔지니어링 표본과 동일하게 다음 품질 규칙이 필요하다는 것을 확인했다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;제목 우선 직무 판정
모집요강 구간만 추출
주변 콘텐츠 제거
실제 IT 통제/감사 문맥 확인
공고별 수동 spot check
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;아직-분석이-완전히-끝난-것은-아니다&quot;&gt;아직 분석이 완전히 끝난 것은 아니다&lt;/h1&gt;

&lt;p&gt;현재 끝난 것은 다음 범위다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[완료]
JobKorea native filter 구조 확인
좋은 회사 후보군 필터 정의
182개 공고 모집단 확보
182개 상세 JD 100% 확보
본문 오염 교정
직무군 재분류
경력/기술 Match 분석
운영·상주·온콜 위험 분석
실제 IT Audit/GRC 공고 교차검증
잘못된 표본 폐기 및 QA
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;반면 다음은 아직 추가 데이터가 필요하다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[진행 필요]
Governance/Audit native/정제 표본 재구축
회사별 실제 재무안정성
position-level 연봉 데이터
실제 WLB/온콜 구조 검증
지원 → 서류 → 면접 → 오퍼 결과 축적
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;따라서 이번 결과는 &lt;strong&gt;커리어 방향을 확정하는 최종 모델이 아니라, 잘못된 가설을 제거하고 2027년 지원전략을 설계하기 위한 1차 실증 분석&lt;/strong&gt;이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;다음에는-실제-지원-결과를-데이터로-만들-계획이다&quot;&gt;다음에는 실제 지원 결과를 데이터로 만들 계획이다&lt;/h1&gt;

&lt;p&gt;공고 데이터만으로는 기업이 내 경력을 실제로 어떻게 평가하는지 알 수 없다.&lt;/p&gt;

&lt;p&gt;앞으로는 지원할 때마다 다음을 기록할 계획이다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;회사
직무
지원일
직무 특성
필수경력 충족 여부
직접경력 gap
사용한 이력서 버전
서류 합격/탈락
면접
오퍼
제시 연봉
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;20~30건만 쌓여도 단순한 AI 추천보다 훨씬 가치 있는 개인 데이터가 된다.&lt;/p&gt;

&lt;p&gt;그때부터는 다음과 같은 질문에 답할 수 있다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Backend보다 IT Audit에서 실제 서류 합격률이 높은가?&lt;/li&gt;
  &lt;li&gt;CISA 취득 전후로 Audit/Risk 반응이 달라지는가?&lt;/li&gt;
  &lt;li&gt;어떤 회사군이 11년 전체 IT 경력을 가장 잘 인정하는가?&lt;/li&gt;
  &lt;li&gt;높은 연봉을 주는 회사에서 어떤 경력 표현이 통하는가?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;결국 커리어 분석에서 가장 중요한 데이터는 시장 평균보다 &lt;strong&gt;내가 시장에 실제로 던졌을 때 받은 응답&lt;/strong&gt;일 가능성이 높다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;정리&quot;&gt;정리&lt;/h1&gt;

&lt;p&gt;이번 분석에서 가장 크게 배운 것은 특정 직무가 유망하다는 사실이 아니었다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;커리어 분석도 일반 데이터 분석과 똑같이 sampling frame과 data quality가 먼저&lt;/strong&gt;라는 점이었다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;검색어로 원하는 결과를 모은다
→ 가설을 증명하기 쉽지만 편향된다.

실제 서비스의 native filter로 모집단을 만든다
→ 데이터를 먼저 확보하고 나중에 분류할 수 있다.

상세본문 전체를 사용한다
→ 주변 페이지 텍스트 때문에 오분류된다.

JD 구간만 정제한다
→ 직무와 요구조건을 더 안정적으로 비교할 수 있다.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;그리고 현재 시점의 커리어 가설은 다음 한 문장으로 정리할 수 있다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;기존 Backend·보안·인프라 경력을 버리지 않고, 그 전체 기술경력을 높은 보상과 낮은 운영책임으로 변환해주는 Technology Audit / IT Risk / IT Planning-Control 직무와 좋은 Backend·AI 제품직을 실제 시장에서 병렬 검증한다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;이 결론도 고정하지 않을 생각이다.&lt;/p&gt;

&lt;p&gt;앞으로 실제 채용공고와 실제 지원 결과가 쌓이면 다시 데이터로 수정할 것이다.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>커리어 채용공고 분석 V2: 연봉 추정을 버리고 기업의 경력 인정 수준을 본다</title>
   <link href="https://son1004007.github.io/career-data-analysis/2026/09/08/career-analysis-v2-employer-recognition/"/>
   <updated>2026-09-08T00:00:00+00:00</updated>
   <id>https://son1004007.github.io/career-data-analysis/2026/09/08/career-analysis-v2-employer-recognition</id>
   <content type="html">&lt;p&gt;앞선 채용공고 데이터 분석에서는 JobKorea의 native filter를 이용해 서울·경력·정규직·우량 기업형태의 관련 IT 공고 182건을 수집하고, 182건 전체의 상세 JD를 확보했다.&lt;/p&gt;

&lt;p&gt;그 분석에서 한 가지 기준을 다시 수정했다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;한국 경력직 채용에서는 지원 전에 실제 제시 연봉을 알기 어렵기 때문에, 회사 평균연봉이나 인터넷 연봉 추정치를 후보 순위의 핵심 근거로 사용하면 안 된다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;회사 평균연봉은 해당 회사의 전체 구성원 평균일 뿐이고, 특정 직무·직급에서 내가 받을 오퍼와 동일하지 않다. 실제 처우는 최종합격 이후 처우협의에서 확인되는 경우가 많다.&lt;/p&gt;

&lt;p&gt;그래서 V2에서는 연봉 proxy를 순위와 탈락 조건에서 제거했다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;v2에서-무엇을-보나&quot;&gt;V2에서 무엇을 보나&lt;/h1&gt;

&lt;p&gt;지원 전에는 다음 순서로 본다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;회사 실명 확인
→ 자체 제품/핵심사업 여부
→ 재무·사업 안정성
→ 역할의 업무구조
→ 현재 직접경력 인정 가능성
→ 직함/레벨 reset 위험
→ 실제 채용 과정의 경력 인정 신호
→ 최종 오퍼 후 실제 보상 비교
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;지원 전의 compensation은 의도적으로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UNKNOWN&lt;/code&gt;으로 둔다.&lt;/p&gt;

&lt;p&gt;실제 오퍼나 회사가 공식적으로 제시한 range가 나오기 전에는 예상 연봉 숫자를 만들지 않는다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;더-중요한-데이터-기업이-내-경력을-어떤-레벨로-보는가&quot;&gt;더 중요한 데이터: 기업이 내 경력을 어떤 레벨로 보는가&lt;/h1&gt;

&lt;p&gt;연봉을 모르더라도 면접 과정에서는 꽤 중요한 정보를 얻을 수 있다.&lt;/p&gt;

&lt;p&gt;예를 들어 기업이 나를:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;현재보다 낮은 레벨로 보는지&lt;/li&gt;
  &lt;li&gt;일부 경력만 인정하는지&lt;/li&gt;
  &lt;li&gt;현재 경력을 온전히 인정하는지&lt;/li&gt;
  &lt;li&gt;Senior/Manager급 자산으로 인정하는지&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;를 볼 수 있다.&lt;/p&gt;

&lt;p&gt;앞으로 실제 지원에서는 다음 값을 기록한다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;recognition_level_signal
years_credited_if_explicit
title_or_level_signal
scope_signal
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;예를 들어 두 직무에 모두 합격했다고 해보자.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Backend
→ Senior Engineer scope

IT Audit
→ Junior / entry audit scope
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 경우 연봉을 아직 몰라도 Backend 시장이 내 현재 경력을 더 높은 수준으로 인정한다고 볼 수 있다.&lt;/p&gt;

&lt;p&gt;반대로:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Backend
→ Mid-level 5년차

Technical Audit
→ Manager-level technical auditor
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;라면 전체 IT 경력이 Audit 시장에서 더 높은 seniority로 환산될 가능성이 있다.&lt;/p&gt;

&lt;p&gt;즉 커리어 선택에서 중요한 질문이 바뀐다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;어디가 평균연봉이 높은가?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;보다&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;어느 시장이 지금까지 쌓은 경력을 더 높은 레벨로 인정하는가?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;를 먼저 확인한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;연봉-proxy를-제거한-뒤에도-1순위는-바뀌지-않았다&quot;&gt;연봉 proxy를 제거한 뒤에도 1순위는 바뀌지 않았다&lt;/h1&gt;

&lt;p&gt;V2에서도 현재 가장 강한 시장 테스트는 &lt;strong&gt;좋은 Product Backend / AI / Identity Backend&lt;/strong&gt;다.&lt;/p&gt;

&lt;p&gt;이유는 연봉 추정치가 아니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;현재 Backend/응용SW 직접경력을 가장 적게 리셋한다.&lt;/li&gt;
  &lt;li&gt;Java/Spring, Python, SQL/RDBMS, Linux/Docker, LLM/RAG/Agent 경험을 직접 활용한다.&lt;/li&gt;
  &lt;li&gt;자체 제품을 만드는 우량회사에서 실제 공고가 존재한다.&lt;/li&gt;
  &lt;li&gt;새로운 도메인의 직접경력 수년을 먼저 요구하지 않는 공고가 있다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;대표적인 사례는 다음과 같다.&lt;/p&gt;

&lt;h2 id=&quot;당근-backend---identity-service&quot;&gt;당근 Backend - Identity Service&lt;/h2&gt;

&lt;p&gt;이 역할은 Backend 3년 이상을 기본 feeder로 보고, OAuth 2.0/OIDC 경험은 우대한다.&lt;/p&gt;

&lt;p&gt;따라서 Identity를 별도의 IAM 커리어로 완전히 갈아타는 역할이라기보다:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Backend
+ 인증/인가
+ Identity domain
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;으로 전문화하는 경로에 가깝다.&lt;/p&gt;

&lt;p&gt;현재 SSO/RBAC/Backend/보안 경험을 모두 사용할 수 있으면서도 direct IAM 경력 N년을 먼저 요구하지 않는다는 점이 중요하다.&lt;/p&gt;

&lt;h2 id=&quot;무신사-core-ai-backend&quot;&gt;무신사 Core AI Backend&lt;/h2&gt;

&lt;p&gt;개발 5년 이상 또는 그에 준하는 역량을 요구하고 Java/Spring, Python, PostgreSQL, AI Agent 경험이 현재 업무와 직접 겹친다.&lt;/p&gt;

&lt;p&gt;이 역할의 시장 테스트에서 확인할 것은 예상연봉이 아니라:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;현재 개발경력을 실제로 5년급 Product AI Backend로 인정하는가?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;이다.&lt;/p&gt;

&lt;h2 id=&quot;오늘의집--당근페이--토스-계열-product-backend&quot;&gt;오늘의집 / 당근페이 / 토스 계열 Product Backend&lt;/h2&gt;

&lt;p&gt;이 역할들도 현재 Backend 경력을 유지하면서 제품회사로 이동할 수 있는 시장 테스트 대상이다.&lt;/p&gt;

&lt;p&gt;다만 금융·결제·대규모 서비스는 장애대응, 고가용성, on-call responsibility를 실제 면접에서 확인해야 한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;technical-it-audit은-2순위-선택적-테스트&quot;&gt;Technical IT Audit은 2순위 선택적 테스트&lt;/h1&gt;

&lt;p&gt;Technical IT Audit도 완전히 제외하지 않는다.&lt;/p&gt;

&lt;p&gt;일부 공고는 개발·운영·인프라·보안을 포함한 일반 IT 경력을 Audit feeder로 인정한다.&lt;/p&gt;

&lt;p&gt;하지만 V2에서는 평균연봉 기대치를 제거했기 때문에 훨씬 엄격하게 본다.&lt;/p&gt;

&lt;p&gt;다음 조건이 필요하다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;기존 IT경력을 feeder로 명시
+ direct audit N년이 절대 필수가 아님
+ 정규직
+ 회사 안정성 양호
+ 직급/레벨 reset이 과도하지 않음
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Technical Audit에서 확인할 핵심 데이터는:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;11년 전체 IT경력을 회사가 어떤 Audit level/title/scope로 인정하는가?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;이다.&lt;/p&gt;

&lt;p&gt;단순히 CISA가 있거나 Audit이라는 직함이 좋아 보여서 이동하지 않는다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;ai-product-security는-작은-실험으로-유지&quot;&gt;AI Product Security는 작은 실험으로 유지&lt;/h1&gt;

&lt;p&gt;일반 AppSec 공고는 Secure SDLC, SAST/DAST/SCA, Threat Modeling, 취약점 분석 같은 직접경력을 요구하는 경우가 많다.&lt;/p&gt;

&lt;p&gt;그래서 Product Security 전체를 1순위로 올리지 않는다.&lt;/p&gt;

&lt;p&gt;대신 당근 AI Security처럼 LLM/RAG/Agent 구현경험과 개인 연구·CTF·블로그 같은 실제 증거를 인정하는 역할은 작은 비용으로 테스트할 수 있다.&lt;/p&gt;

&lt;p&gt;기존 Agent/RAG 서비스에 대해:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Prompt Injection&lt;/li&gt;
  &lt;li&gt;Tool Abuse&lt;/li&gt;
  &lt;li&gt;RAG Poisoning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;같은 공격 시나리오를 재현한 짧은 보고서를 만든 뒤 실제 시장 반응을 확인하는 방식이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;최종-v2-순서&quot;&gt;최종 V2 순서&lt;/h1&gt;

&lt;h2 id=&quot;1순위&quot;&gt;1순위&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Good Product Backend / AI / Identity Backend&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;현재 직접경력을 가장 적게 리셋하면서 자체 제품 도메인으로 이동할 수 있다.&lt;/p&gt;

&lt;h2 id=&quot;2순위&quot;&gt;2순위&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Broad-feeder Technical IT Audit / Assurance&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;기존 IT경력을 seniority에 실제 반영하는 회사만 선택적으로 테스트한다.&lt;/p&gt;

&lt;h2 id=&quot;3순위&quot;&gt;3순위&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;AI Product Security / development-friendly AppSec&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;직접 AppSec 경력 N년보다 개발·AI·보안 실전 evidence를 인정하는 역할만 본다.&lt;/p&gt;

&lt;h2 id=&quot;후순위&quot;&gt;후순위&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;direct Cloud Security&lt;/li&gt;
  &lt;li&gt;IT SOX&lt;/li&gt;
  &lt;li&gt;Privacy Governance&lt;/li&gt;
  &lt;li&gt;순수 Security Policy&lt;/li&gt;
  &lt;li&gt;고객사 상주 SI&lt;/li&gt;
  &lt;li&gt;Forward Deployed / Support-heavy&lt;/li&gt;
  &lt;li&gt;구조적 24x7/on-call 역할&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;실제-지원-데이터가-다음-분석의-핵심이다&quot;&gt;실제 지원 데이터가 다음 분석의 핵심이다&lt;/h1&gt;

&lt;p&gt;앞으로는 지원 결과마다 다음을 기록한다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;회사
직무
지원일
서류 결과
면접 결과
기업이 인정한 경력 level
명시적으로 인정한 경력연수
직함/레벨
업무 scope
실제 on-call/WLB 정보
최종 오퍼 여부
실제 제시 보상
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;보상은 마지막에 기록한다.&lt;/p&gt;

&lt;p&gt;그 전에는 알 수 없는 숫자를 예측하지 않는다.&lt;/p&gt;

&lt;p&gt;이번 V2의 핵심 결론은 다음과 같다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;좋은 회사에서 얼마를 받을지 미리 맞히는 것보다, 지금까지 쌓은 경력을 어느 시장이 더 높은 레벨로 인정하는지 먼저 검증한다. Product Backend/AI/Identity를 1차로 테스트하고, Technical Audit과 AI Product Security는 비교군으로 병렬 테스트한다. 실제 보상은 오퍼가 나온 뒤 최종 선택에 사용한다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content>
 </entry>
 
 <entry>
   <title>Synology NAS의 로컬 웹서비스를 Docker cloudflared Quick Tunnel로 외부에 공유하기</title>
   <link href="https://son1004007.github.io/infrastructure/2026/09/03/synology-nas-cloudflare-quick-tunnel/"/>
   <updated>2026-09-03T00:00:00+00:00</updated>
   <id>https://son1004007.github.io/infrastructure/2026/09/03/synology-nas-cloudflare-quick-tunnel</id>
   <content type="html">&lt;p&gt;Synology NAS에서 개발 중인 웹 화면을 휴대폰으로 확인해야 했습니다. 처음에는 NAS의 포트를 인터넷에 직접 열거나 공유기 포트포워딩을 추가하는 방법을 생각할 수 있지만, 단기 퍼블리싱 검토에는 그보다 &lt;strong&gt;로컬 웹서버는 NAS 내부에만 열고, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cloudflared&lt;/code&gt;가 Cloudflare로 outbound 터널을 만드는 구조&lt;/strong&gt;가 더 단순했습니다.&lt;/p&gt;

&lt;p&gt;여기서 한 가지를 정확히 구분해야 합니다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cloudflared&lt;/code&gt;가 웹서버 역할까지 하는 것은 아닙니다.&lt;/strong&gt;&lt;br /&gt;
실제 HTML/CSS/JS를 제공하는 로컬 웹서버가 별도로 실행되고, Docker 컨테이너의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cloudflared&lt;/code&gt;는 그 웹서버를 외부 URL에 연결하는 터널 역할만 합니다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;실제로 사용한 구조는 다음과 같습니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;GitHub repository
      |
      | 최신 정적 파일 배포
      v
Synology NAS
      |
      | 127.0.0.1:18088
      v
Local static web server
(Python http.server)
      ^
      |
      | Cloudflare Tunnel
      |
Docker: cloudflared
      |
      | outbound connection
      v
Cloudflare Network
      |
      v
https://&amp;lt;random&amp;gt;.trycloudflare.com
      |
      v
휴대폰 / 외부 PC 브라우저
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 방식으로 NAS 관리 포트나 애플리케이션 포트를 인터넷에 직접 노출하지 않고도 외부 브라우저에서 퍼블리싱 결과를 확인할 수 있었습니다.&lt;/p&gt;

&lt;h2 id=&quot;문제점&quot;&gt;문제점&lt;/h2&gt;

&lt;p&gt;정적 HTML 퍼블리싱 결과를 NAS에 올린 뒤 외부에서 확인하려면 다음 중 하나가 필요합니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;공유기에서 포트포워딩을 추가한다.&lt;/li&gt;
  &lt;li&gt;NAS의 Reverse Proxy와 DDNS를 구성한다.&lt;/li&gt;
  &lt;li&gt;VPN/Tailscale에 모든 테스트 단말을 연결한다.&lt;/li&gt;
  &lt;li&gt;외부의 별도 정적 호스팅 서비스를 사용한다.&lt;/li&gt;
  &lt;li&gt;NAS에서 외부로 터널을 만든다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;장기 서비스라면 고정 도메인과 인증 정책까지 설계하는 것이 맞지만, 화면을 빠르게 검토하는 단계에서는 공유기와 DSM 설정을 반복해서 바꾸고 싶지 않았습니다.&lt;/p&gt;

&lt;p&gt;또한 정적 퍼블리싱 화면 때문에 NAS의 관리 서비스나 다른 컨테이너까지 외부에 노출하는 것도 피하고 싶었습니다.&lt;/p&gt;

&lt;h2 id=&quot;선택한-구조&quot;&gt;선택한 구조&lt;/h2&gt;

&lt;p&gt;구조를 두 단계로 나눴습니다.&lt;/p&gt;

&lt;h3 id=&quot;1-웹서버는-nas의-loopback에만-바인딩&quot;&gt;1. 웹서버는 NAS의 loopback에만 바인딩&lt;/h3&gt;

&lt;p&gt;배포된 정적 파일을 NAS의 전용 디렉터리에 두고 다음처럼 웹서버를 실행합니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;python3 &lt;span class=&quot;nt&quot;&gt;-m&lt;/span&gt; http.server 18088 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--bind&lt;/span&gt; 127.0.0.1 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--directory&lt;/span&gt; /path/to/static-site
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;핵심은 다음 부분입니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;--bind 127.0.0.1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;따라서 NAS의 LAN IP나 공인 IP에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;18088&lt;/code&gt; 포트로 직접 접근할 수 있게 여는 것이 아니라, &lt;strong&gt;NAS 내부 프로세스만 접근할 수 있는 웹서비스&lt;/strong&gt;로 둡니다.&lt;/p&gt;

&lt;p&gt;먼저 NAS 내부에서 정상인지 확인합니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;curl &lt;span class=&quot;nt&quot;&gt;-fsS&lt;/span&gt; http://127.0.0.1:18088/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 단계가 실패하면 Cloudflare Tunnel을 확인할 필요가 없습니다. 먼저 로컬 웹서버부터 해결해야 합니다.&lt;/p&gt;

&lt;h2 id=&quot;docker로-cloudflared-quick-tunnel-실행&quot;&gt;Docker로 cloudflared Quick Tunnel 실행&lt;/h2&gt;

&lt;p&gt;Cloudflare의 Quick Tunnel은 로컬 웹서비스를 임시 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;*.trycloudflare.com&lt;/code&gt; 주소로 연결할 수 있습니다.&lt;/p&gt;

&lt;p&gt;가장 단순한 형태는 다음과 같습니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;cloudflared tunnel &lt;span class=&quot;nt&quot;&gt;--url&lt;/span&gt; http://localhost:18088
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;NAS에서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cloudflared&lt;/code&gt;를 별도 설치하기보다 Docker 컨테이너로 격리했습니다.&lt;/p&gt;

&lt;p&gt;예시는 다음과 같습니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker run &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--name&lt;/span&gt; static-preview-tunnel &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--network&lt;/span&gt; host &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--read-only&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--cap-drop&lt;/span&gt; ALL &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--security-opt&lt;/span&gt; no-new-privileges:true &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--tmpfs&lt;/span&gt; /tmp:rw,nosuid,nodev,size&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;16m &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  cloudflare/cloudflared:latest &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  tunnel &lt;span class=&quot;nt&quot;&gt;--no-autoupdate&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--url&lt;/span&gt; http://127.0.0.1:18088
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 구성에서 각 역할은 다음과 같습니다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;구성&lt;/th&gt;
      &lt;th&gt;역할&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;python3 -m http.server&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;실제 HTML/CSS/JS 응답&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;127.0.0.1:18088&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;NAS 내부에서만 접근 가능한 origin&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Docker&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cloudflared&lt;/code&gt; 프로세스 격리&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cloudflared&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Cloudflare로 outbound 터널 생성&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;trycloudflare.com&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;임시 외부 접근 URL&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--network host&lt;/code&gt;를 사용한 이유는 컨테이너의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cloudflared&lt;/code&gt;가 NAS host의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;127.0.0.1:18088&lt;/code&gt; origin에 접근하도록 하기 위해서입니다.&lt;/p&gt;

&lt;p&gt;컨테이너에는 웹사이트 파일이나 NAS 관리 권한이 필요하지 않으므로 가능한 권한을 줄였습니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;read-only filesystem
capabilities drop
no-new-privileges
temporary /tmp only
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;외부-url-확인&quot;&gt;외부 URL 확인&lt;/h2&gt;

&lt;p&gt;Quick Tunnel을 시작하면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cloudflared&lt;/code&gt; 로그에 임시 URL이 출력됩니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker logs static-preview-tunnel
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;예를 들면 다음 형태입니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;https://random-words.trycloudflare.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;그 다음 두 구간을 각각 확인합니다.&lt;/p&gt;

&lt;h3 id=&quot;nas-내부-origin-검증&quot;&gt;NAS 내부 origin 검증&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;curl &lt;span class=&quot;nt&quot;&gt;-fsS&lt;/span&gt; http://127.0.0.1:18088/ &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;/dev/null
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;cloudflare-경유-외부-검증&quot;&gt;Cloudflare 경유 외부 검증&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;curl &lt;span class=&quot;nt&quot;&gt;-fsS&lt;/span&gt; https://random-words.trycloudflare.com/ &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;/dev/null
&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;둘 다 성공해야 합니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Local origin PASS
     +
Public tunnel PASS
     =
외부 브라우저 검토 가능
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Cloudflare URL만 확인하면 origin 문제와 tunnel 문제를 분리하기 어렵기 때문에 이 두 단계 검증을 분리하는 편이 좋습니다.&lt;/p&gt;

&lt;h2 id=&quot;github에서-nas까지-자동-배포&quot;&gt;GitHub에서 NAS까지 자동 배포&lt;/h2&gt;

&lt;p&gt;퍼블리싱 화면을 수정할 때마다 NAS에 파일을 직접 복사하는 대신 다음 흐름으로 정리했습니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;GitHub main
   |
   | 검증
   v
정적 HTML/CSS/JS
   |
   | NAS deploy
   v
NAS 전용 site directory
   |
   v
127.0.0.1 local web server
   |
   v
cloudflared Quick Tunnel
   |
   v
외부 휴대폰 검토
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;배포 시에는 다음을 확인합니다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;GitHub의 최신 commit을 가져옵니다.&lt;/li&gt;
  &lt;li&gt;지정된 정적 파일만 site 디렉터리에 복사합니다.&lt;/li&gt;
  &lt;li&gt;기존 local HTTP server를 종료하고 새 파일 기준으로 다시 시작합니다.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;127.0.0.1&lt;/code&gt;에서 HTTP 응답을 확인합니다.&lt;/li&gt;
  &lt;li&gt;기존 Quick Tunnel 컨테이너를 교체합니다.&lt;/li&gt;
  &lt;li&gt;새 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;trycloudflare.com&lt;/code&gt; URL을 얻습니다.&lt;/li&gt;
  &lt;li&gt;외부 URL의 HTTP 응답까지 확인합니다.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;화면 개발 과정에서는 JavaScript 문법과 필수 파일 존재 여부도 CI에서 먼저 검사하도록 했습니다.&lt;/p&gt;

&lt;h2 id=&quot;이-방식의-장점&quot;&gt;이 방식의 장점&lt;/h2&gt;

&lt;h3 id=&quot;공유기-포트포워딩이-필요하지-않음&quot;&gt;공유기 포트포워딩이 필요하지 않음&lt;/h3&gt;

&lt;p&gt;Cloudflare Tunnel은 NAS에서 Cloudflare 쪽으로 연결을 시작합니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;기존 포트포워딩
Internet -&amp;gt; Router -&amp;gt; NAS

Cloudflare Tunnel
NAS -&amp;gt; Cloudflare
          ^
          |
      Internet user
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;따라서 preview를 만들기 위해 공유기의 inbound port를 하나씩 추가할 필요가 없습니다.&lt;/p&gt;

&lt;h3 id=&quot;nas-origin-포트를-외부에-직접-열-필요가-없음&quot;&gt;NAS origin 포트를 외부에 직접 열 필요가 없음&lt;/h3&gt;

&lt;p&gt;웹서버는 계속 다음 주소에만 존재합니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;127.0.0.1:18088
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;외부 사용자는 NAS의 이 포트로 직접 접근하는 것이 아니라 Cloudflare URL을 통해 접근합니다.&lt;/p&gt;

&lt;h3 id=&quot;임시-화면-공유에-빠름&quot;&gt;임시 화면 공유에 빠름&lt;/h3&gt;

&lt;p&gt;도메인을 별도로 준비하지 않아도 URL이 바로 발급되므로 다음 용도에는 편리합니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;모바일 화면 검토&lt;/li&gt;
  &lt;li&gt;퍼블리싱 결과 공유&lt;/li&gt;
  &lt;li&gt;개발 중인 정적 UI 확인&lt;/li&gt;
  &lt;li&gt;브라우저 호환성 확인&lt;/li&gt;
  &lt;li&gt;짧게 유지하는 demo/preview&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;주의점-quick-tunnel은-운영-서비스가-아님&quot;&gt;주의점: Quick Tunnel은 운영 서비스가 아님&lt;/h2&gt;

&lt;p&gt;Cloudflare 공식 문서에서도 Quick Tunnel은 &lt;strong&gt;testing/development 용도&lt;/strong&gt;로 안내합니다.&lt;/p&gt;

&lt;p&gt;특히 다음 특성이 있습니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;새 tunnel process를 시작하면 랜덤 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;trycloudflare.com&lt;/code&gt; 주소가 바뀔 수 있습니다.&lt;/li&gt;
  &lt;li&gt;SLA가 보장되는 production endpoint가 아닙니다.&lt;/li&gt;
  &lt;li&gt;동시 in-flight request 제한이 있습니다.&lt;/li&gt;
  &lt;li&gt;Server-Sent Events(SSE)는 지원하지 않습니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;따라서 다음 단계로 넘어가면 Quick Tunnel을 그대로 운영 서비스로 사용하지 않는 것이 좋습니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Quick Tunnel
  -&amp;gt; UI 검토 / 개발 / 임시 공유

Named Cloudflare Tunnel
  -&amp;gt; 고정 hostname
  -&amp;gt; 정식 접근정책
  -&amp;gt; 필요 시 Cloudflare Access
  -&amp;gt; 지속 서비스
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;공식 문서:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/trycloudflare/&quot;&gt;Cloudflare Quick Tunnels&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://developers.cloudflare.com/tunnel/setup/&quot;&gt;Cloudflare Tunnel Setup&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;보안상-특히-주의할-점&quot;&gt;보안상 특히 주의할 점&lt;/h2&gt;

&lt;p&gt;Tunnel을 쓴다고 해서 &lt;strong&gt;아무 내부 서비스를 공개해도 안전해지는 것은 아닙니다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;이번 방식은 공개해도 되는 synthetic 정적 UI를 확인하는 용도로 사용했습니다.&lt;/p&gt;

&lt;p&gt;다음 대상은 인증 없이 Quick Tunnel로 바로 공개하지 않는 편이 좋습니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;DSM 관리자 화면&lt;/li&gt;
  &lt;li&gt;NAS 관리 API&lt;/li&gt;
  &lt;li&gt;데이터베이스&lt;/li&gt;
  &lt;li&gt;SSH&lt;/li&gt;
  &lt;li&gt;개인정보가 있는 애플리케이션&lt;/li&gt;
  &lt;li&gt;사용자별 진행 상태를 저장하는 내부 서비스&lt;/li&gt;
  &lt;li&gt;회사/고객 데이터가 보이는 시스템&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;preview 용도라면 다음 기준을 적용할 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;공개 가능한 정적 데이터만 사용
origin은 loopback에만 bind
외부 API 호출 최소화
secret을 HTML/JS에 넣지 않음
관리 서비스와 별도 runtime 사용
필요 없으면 tunnel 즉시 종료
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;검색 노출 자체가 필요 없는 검토 페이지라면 HTML에 다음을 추가할 수도 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;meta&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;robots&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;content=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;noindex,nofollow,noarchive&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;다만 이것은 접근통제가 아닙니다. 민감한 서비스라면 Named Tunnel과 실제 인증/접근정책을 사용해야 합니다.&lt;/p&gt;

&lt;h2 id=&quot;장애를-나눠서-확인하는-방법&quot;&gt;장애를 나눠서 확인하는 방법&lt;/h2&gt;

&lt;h3 id=&quot;페이지-자체가-열리지-않는-경우&quot;&gt;페이지 자체가 열리지 않는 경우&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;curl http://127.0.0.1:18088/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;부터 확인합니다.&lt;/p&gt;

&lt;p&gt;실패한다면 정적 웹서버 문제입니다.&lt;/p&gt;

&lt;h3 id=&quot;nas에서는-열리지만-외부-url이-안-되는-경우&quot;&gt;NAS에서는 열리지만 외부 URL이 안 되는 경우&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker ps
docker logs static-preview-tunnel
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;으로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cloudflared&lt;/code&gt; 상태와 발급 URL을 확인합니다.&lt;/p&gt;

&lt;h3 id=&quot;재배포-후-url이-달라진-경우&quot;&gt;재배포 후 URL이 달라진 경우&lt;/h3&gt;

&lt;p&gt;Quick Tunnel의 정상적인 특성입니다. 새로운 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cloudflared&lt;/code&gt; 프로세스가 시작되면 새 랜덤 URL이 생성될 수 있습니다.&lt;/p&gt;

&lt;p&gt;고정 URL이 필요하면 Quick Tunnel이 아니라 Named Tunnel로 전환합니다.&lt;/p&gt;

&lt;h2 id=&quot;정리&quot;&gt;정리&lt;/h2&gt;

&lt;p&gt;이번에 사용한 구성은 다음 한 줄로 요약할 수 있습니다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;NAS에는 localhost 웹서버만 열고, Docker로 실행한 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cloudflared&lt;/code&gt;가 outbound Quick Tunnel을 만들어 임시 외부 URL로 연결한다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;이 방식은 NAS에 정적 퍼블리싱 결과를 올려 실제 휴대폰 화면을 검토할 때 상당히 편리했습니다.&lt;/p&gt;

&lt;p&gt;다만 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;trycloudflare.com&lt;/code&gt; Quick Tunnel은 어디까지나 개발/검토용입니다. 화면과 기능이 안정된 뒤에는 고정 hostname, 인증, 접근정책을 갖춘 Named Tunnel 또는 다른 정식 배포 구조로 전환하는 것이 적절합니다.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>SSH -L 로컬 포트 포워딩: 네트워크 구조와 실무 예제</title>
   <link href="https://son1004007.github.io/infrastructure/2026/09/03/ssh-local-port-forwarding/"/>
   <updated>2026-09-03T00:00:00+00:00</updated>
   <id>https://son1004007.github.io/infrastructure/2026/09/03/ssh-local-port-forwarding</id>
   <content type="html">&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ssh -L&lt;/code&gt;은 먼저 &lt;strong&gt;어느 노드가 어디로 접속하는지&lt;/strong&gt;를 이해하면 쉽습니다.&lt;/p&gt;

&lt;p&gt;가장 중요한 구조는 다음과 같습니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[Node A: ssh -L 명령을 실행한 노드]
Application
  |
  | 127.0.0.1:LOCAL_PORT
  v
SSH Client
  ||
  || SSH encrypted channel
  \/
[Node B: SSH_SERVER]
  |
  | TCP connection
  v
[TARGET_IP:TARGET_PORT]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;즉 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ssh -L&lt;/code&gt; 명령을 실행한 &lt;strong&gt;Node A에 LOCAL_PORT가 열리고&lt;/strong&gt;, 그 포트로 들어온 연결은 &lt;strong&gt;Node B인 SSH 서버를 경유한 뒤&lt;/strong&gt;, SSH 서버가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TARGET_IP:TARGET_PORT&lt;/code&gt;로 실제 TCP 연결을 만듭니다.&lt;/p&gt;

&lt;p&gt;따라서 가장 중요한 기준은 다음 한 문장입니다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TARGET_IP&lt;/code&gt; 또는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;destination_host&lt;/code&gt;는 내 PC 기준 목적지가 아니라, &lt;strong&gt;SSH_SERVER가 접속할 수 있는 네트워크 기준의 목적지&lt;/strong&gt;입니다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;예를 들어 다음 명령을 실행한다고 가정합니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ssh &lt;span class=&quot;nt&quot;&gt;-N&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-L&lt;/span&gt; 127.0.0.1:18080:10.0.20.15:8080 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  user@bastion.example.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 경우 연결 흐름은 다음과 같습니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;내 PC의 127.0.0.1:18080
  -&amp;gt; bastion.example.com SSH 연결
  -&amp;gt; Bastion이 10.0.20.15:8080으로 접속
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;내 PC가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;10.0.20.15&lt;/code&gt;에 직접 접근할 수 없어도 됩니다. &lt;strong&gt;Bastion이 해당 IP와 포트에 접근할 수 있으면&lt;/strong&gt;, 내 PC에서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;127.0.0.1:18080&lt;/code&gt;으로 접속해 그 서비스를 사용할 수 있습니다.&lt;/p&gt;

&lt;p&gt;이 구조가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ssh -L&lt;/code&gt;의 핵심입니다.&lt;/p&gt;

&lt;h2 id=&quot;문제점&quot;&gt;문제점&lt;/h2&gt;

&lt;p&gt;실무에서는 다음과 같은 상황을 자주 만납니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;SSH 서버가 접근할 수 있는 내부망 서비스에 내 PC에서는 직접 접근할 수 없습니다.&lt;/li&gt;
  &lt;li&gt;SSH 서버의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;127.0.0.1:8080&lt;/code&gt;에서만 실행되는 웹 애플리케이션을 내 PC 브라우저에서 확인해야 합니다.&lt;/li&gt;
  &lt;li&gt;외부에서 직접 접근할 수 없는 PostgreSQL, MySQL 같은 내부 DB에 접속해야 합니다.&lt;/li&gt;
  &lt;li&gt;원격 서버의 Jupyter, 개발 서버, 관리 UI를 인터넷에 공개하고 싶지 않습니다.&lt;/li&gt;
  &lt;li&gt;방화벽을 열거나 서비스의 listen 주소를 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.0.0.0&lt;/code&gt;으로 변경하지 않고 임시로 접근해야 합니다.&lt;/li&gt;
  &lt;li&gt;Bastion 또는 Jump Host를 통해서만 내부망 서비스에 접근할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이런 경우 서비스 포트를 외부에 직접 공개하는 대신, 이미 인증과 암호화가 적용된 SSH 연결을 통로로 사용할 수 있습니다.&lt;/p&gt;

&lt;h2 id=&quot;핵심-개념과-문법&quot;&gt;핵심 개념과 문법&lt;/h2&gt;

&lt;p&gt;OpenSSH의 기본 문법은 다음과 같습니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ssh &lt;span class=&quot;nt&quot;&gt;-L&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;bind_address:]local_port:destination_host:destination_port &lt;span class=&quot;o&quot;&gt;[&lt;/span&gt;user@]ssh_server
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;실무에서는 로컬 접근만 허용하도록 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bind_address&lt;/code&gt;를 명시하는 편이 안전합니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ssh &lt;span class=&quot;nt&quot;&gt;-L&lt;/span&gt; 127.0.0.1:local_port:destination_host:destination_port user@ssh_server
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;각 값의 의미는 다음과 같습니다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;항목&lt;/th&gt;
      &lt;th&gt;어느 노드 기준인가&lt;/th&gt;
      &lt;th&gt;의미&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;127.0.0.1&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ssh -L&lt;/code&gt; 명령을 실행한 로컬 노드&lt;/td&gt;
      &lt;td&gt;포트를 열 주소&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;local_port&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ssh -L&lt;/code&gt; 명령을 실행한 로컬 노드&lt;/td&gt;
      &lt;td&gt;애플리케이션이 접속할 포트&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;destination_host&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;SSH 서버 기준&lt;/td&gt;
      &lt;td&gt;SSH 서버가 접속할 대상 호스트/IP&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;destination_port&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;SSH 서버 기준&lt;/td&gt;
      &lt;td&gt;대상 서비스 포트&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ssh_server&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;로컬 노드가 접속하는 서버&lt;/td&gt;
      &lt;td&gt;터널을 중계할 SSH 서버&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;특히 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;destination_host&lt;/code&gt;를 어느 노드 기준으로 해석하는지가 중요합니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ssh -L 127.0.0.1:18080:127.0.0.1:8080 user@server.example.com
                         ^^^^^^^^^
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;여기서 두 번째 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;127.0.0.1&lt;/code&gt;은 &lt;strong&gt;내 PC가 아니라 SSH 서버 자신의 loopback 주소&lt;/strong&gt;입니다.&lt;/p&gt;

&lt;p&gt;반대로 다음처럼 내부 IP를 지정했다면,&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ssh &lt;span class=&quot;nt&quot;&gt;-L&lt;/span&gt; 127.0.0.1:18080:10.0.20.15:8080 user@bastion.example.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;10.0.20.15:8080&lt;/code&gt;에 접속하는 주체는 내 PC가 아니라 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bastion.example.com&lt;/code&gt;입니다.&lt;/p&gt;

&lt;p&gt;OpenSSH는 내 PC의 로컬 포트에서 연결을 받은 뒤 SSH 암호화 채널을 통해 SSH 서버로 전달하고, SSH 서버가 다시 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;destination_host:destination_port&lt;/code&gt;로 TCP 연결을 만듭니다.&lt;/p&gt;

&lt;p&gt;즉 구조를 한 줄로 표현하면 다음과 같습니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;LOCAL_NODE:LOCAL_PORT
  -&amp;gt; SSH_SERVER
  -&amp;gt; SSH_SERVER가 접근 가능한 DESTINATION_HOST:DESTINATION_PORT
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;cli만-가능한-linux-서버에서-웹-인증이-필요한-경우&quot;&gt;CLI만 가능한 Linux 서버에서 웹 인증이 필요한 경우&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ssh -L&lt;/code&gt;은 DB나 웹서비스 접근뿐 아니라 &lt;strong&gt;GUI가 없는 Linux 서버에서 웹 로그인 화면이 필요한 자동화 작업&lt;/strong&gt;에도 유용합니다.&lt;/p&gt;

&lt;p&gt;예를 들어 다음과 같은 경우입니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;CLI 전용 서버에서 Tistory/Kakao 로그인을 최초 한 번 수행해야 합니다.&lt;/li&gt;
  &lt;li&gt;OAuth 로그인 중 브라우저 승인이 필요합니다.&lt;/li&gt;
  &lt;li&gt;2차 인증, OTP 승인, CAPTCHA 등 때문에 완전한 headless 로그인 자동화가 적절하지 않습니다.&lt;/li&gt;
  &lt;li&gt;로그인 이후에는 Playwright 같은 브라우저 자동화가 저장된 세션을 재사용해야 합니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;가능하면 서비스가 공식적으로 device-code나 CLI 인증 방식을 제공할 때는 그 방식을 먼저 사용하는 것이 단순합니다. 하지만 &lt;strong&gt;실제 브라우저 화면에서 로그인을 완료해야 하는 서비스라면&lt;/strong&gt;, 서버에 임시 브라우저를 띄우고 그 화면만 로컬 PC로 전달할 수 있습니다.&lt;/p&gt;

&lt;h3 id=&quot;구성&quot;&gt;구성&lt;/h3&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[내 PC]
Browser
  |
  | http://127.0.0.1:16080
  v
127.0.0.1:16080
  |
  | ssh -L
  v
[CLI Linux Server]
127.0.0.1:6080
[noVNC]
  |
  v
VNC / Virtual Display
  |
  v
Chromium / Playwright Browser
  |
  v
Tistory / Kakao / OAuth Provider
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;중요한 점은 &lt;strong&gt;브라우저 프로세스와 로그인 세션은 Linux 서버에 존재하고, 화면만 SSH 터널을 통해 내 PC 브라우저에서 보는 것&lt;/strong&gt;입니다.&lt;/p&gt;

&lt;p&gt;따라서 인증이 끝난 뒤 서버에 남은 브라우저 세션을 이후 자동화에서 그대로 사용할 수 있습니다.&lt;/p&gt;

&lt;h3 id=&quot;1-linux-서버에-임시-gui-브라우저-준비&quot;&gt;1. Linux 서버에 임시 GUI 브라우저 준비&lt;/h3&gt;

&lt;p&gt;CLI 서버에서는 일반적으로 다음 구성 중 하나를 사용합니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Xvfb 또는 가상 디스플레이
  + Chromium/Chrome
  + VNC server
  + noVNC
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;컨테이너로 이 구성을 격리해도 됩니다. 인증용 브라우저는 장시간 외부에 공개해 두기보다 &lt;strong&gt;필요할 때만 임시로 실행하고 인증이 끝나면 종료&lt;/strong&gt;하는 편이 안전합니다.&lt;/p&gt;

&lt;h3 id=&quot;2-novnc를-서버의-localhost에만-열기&quot;&gt;2. noVNC를 서버의 localhost에만 열기&lt;/h3&gt;

&lt;p&gt;VNC 서버가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;127.0.0.1:5901&lt;/code&gt;에서 실행 중이라고 가정하면 noVNC의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;novnc_proxy&lt;/code&gt;는 다음과 같이 localhost에만 바인딩할 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;./utils/novnc_proxy &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--vnc&lt;/span&gt; localhost:5901 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--listen&lt;/span&gt; localhost:6080
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이렇게 하면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;6080&lt;/code&gt; 포트는 인터넷에 직접 노출되지 않습니다.&lt;/p&gt;

&lt;h3 id=&quot;3-내-pc에서-ssh-로컬-포워딩&quot;&gt;3. 내 PC에서 SSH 로컬 포워딩&lt;/h3&gt;

&lt;p&gt;내 PC에서 다음 명령을 실행합니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ssh &lt;span class=&quot;nt&quot;&gt;-NT&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;ExitOnForwardFailure&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;yes&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-L&lt;/span&gt; 127.0.0.1:16080:127.0.0.1:6080 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  user@cli-server.example.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이번 명령도 앞에서 설명한 구조와 동일합니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;내 PC 127.0.0.1:16080
  -&amp;gt; SSH
  -&amp;gt; cli-server.example.com
  -&amp;gt; 서버 자신의 127.0.0.1:6080
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;내 PC 브라우저에서는 다음 주소를 엽니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;http://127.0.0.1:16080/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;그러면 실제로는 원격 Linux 서버에서 실행 중인 브라우저 화면을 보게 됩니다.&lt;/p&gt;

&lt;h3 id=&quot;4-tistorykakao-같은-웹-로그인을-직접-완료&quot;&gt;4. Tistory/Kakao 같은 웹 로그인을 직접 완료&lt;/h3&gt;

&lt;p&gt;이제 원격 브라우저 화면에서 정상적인 로그인 절차를 진행합니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;아이디/비밀번호 입력
  -&amp;gt; 필요한 경우 2차 인증
  -&amp;gt; CAPTCHA가 나오면 사용자가 직접 처리
  -&amp;gt; Tistory 관리 화면 등 인증 완료 상태 확인
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;OTP나 CAPTCHA를 자동 우회하는 것이 아니라 &lt;strong&gt;사람이 필요한 인증 단계만 원격 브라우저에서 정상적으로 수행&lt;/strong&gt;하는 구조입니다.&lt;/p&gt;

&lt;h3 id=&quot;5-인증-세션을-서버에-저장&quot;&gt;5. 인증 세션을 서버에 저장&lt;/h3&gt;

&lt;p&gt;Playwright는 로그인된 Browser Context의 cookies, local storage 등 인증 상태를 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;storageState&lt;/code&gt; 파일로 저장하고 이후 새 Context에서 다시 사용할 수 있습니다.&lt;/p&gt;

&lt;p&gt;예를 들면 다음과 같습니다.&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;storageState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;/var/lib/browser-auth/storage-state.json&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이후 자동화에서는 저장한 상태를 다시 로드할 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;context&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;browser&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;newContext&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;({&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;storageState&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;/var/lib/browser-auth/storage-state.json&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&apos;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;그러면 정상적인 세션 유효기간 동안에는 매번 사람이 로그인하지 않고 headless 브라우저가 인증 상태를 재사용할 수 있습니다.&lt;/p&gt;

&lt;h3 id=&quot;6-세션-파일은-인증정보로-취급&quot;&gt;6. 세션 파일은 인증정보로 취급&lt;/h3&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;storageState&lt;/code&gt; 같은 파일에는 세션 쿠키 등 계정 접근에 사용할 수 있는 정보가 포함될 수 있습니다.&lt;/p&gt;

&lt;p&gt;따라서 다음 원칙을 지키는 것이 중요합니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Git repository에 commit하지 않기
로그나 CI artifact에 출력하지 않기
서버의 전용 디렉터리에만 저장
파일 권한을 최소화하기 (예: 0600)
자동화 컨테이너에도 필요한 파일만 mount
세션이 만료되면 기존 파일을 폐기하고 다시 인증
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Playwright 공식 문서도 저장된 인증 상태 파일에 민감한 쿠키와 헤더가 포함될 수 있으므로 repository에 넣지 말 것을 권고합니다.&lt;/p&gt;

&lt;h3 id=&quot;7-운영-시에는-인증과-게시를-분리&quot;&gt;7. 운영 시에는 인증과 게시를 분리&lt;/h3&gt;

&lt;p&gt;Tistory 게시 자동화를 예로 들면 다음 구조가 실용적입니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;최초 또는 세션 만료 시
사용자
  -&amp;gt; ssh -L
  -&amp;gt; 임시 noVNC
  -&amp;gt; 원격 Chromium에서 로그인
  -&amp;gt; storageState 저장

평상시
Scheduler
  -&amp;gt; Headless Playwright
  -&amp;gt; storageState 로드
  -&amp;gt; Tistory 관리 화면 접근
  -&amp;gt; 글 작성/수정
  -&amp;gt; 결과 확인
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;자동 게시 중 로그인 페이지로 리다이렉트되거나 세션이 만료된 것이 확인되면, 비밀번호 재시도나 CAPTCHA 우회를 반복하는 대신 &lt;strong&gt;게시 작업을 중단하고 재인증이 필요하다고 처리&lt;/strong&gt;하는 것이 안전합니다.&lt;/p&gt;

&lt;h3 id=&quot;ssh-포워딩-자체가-서버-정책으로-막혀-있다면&quot;&gt;SSH 포워딩 자체가 서버 정책으로 막혀 있다면&lt;/h3&gt;

&lt;p&gt;서버에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AllowTcpForwarding&lt;/code&gt; 등이 정책상 비활성화되어 있으면 위 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ssh -L&lt;/code&gt; 방식은 사용할 수 없습니다.&lt;/p&gt;

&lt;p&gt;이 경우 포워딩 제한을 무조건 해제하기보다 다음 순서로 판단하는 편이 좋습니다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;관리자가 허용한다면 특정 인증용 목적지만 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PermitOpen&lt;/code&gt; 등으로 제한적으로 허용합니다.&lt;/li&gt;
  &lt;li&gt;신뢰할 수 있는 사설 LAN 안의 서버라면 noVNC를 &lt;strong&gt;LAN 주소에 임시로만 바인딩&lt;/strong&gt;하고 방화벽으로 접근 출발지를 제한하는 방식을 검토합니다.&lt;/li&gt;
  &lt;li&gt;인증이 완료되면 임시 브라우저와 VNC/noVNC 서비스를 즉시 종료합니다.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;즉 &lt;strong&gt;웹 인증을 위해 CLI 서버 자체에 데스크톱 환경을 상시 공개할 필요는 없습니다.&lt;/strong&gt; 필요한 시점에만 임시 브라우저를 띄우고, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ssh -L&lt;/code&gt;로 화면 접근 경로를 제한한 뒤, 인증 상태만 안전하게 보존하는 방식으로 구성할 수 있습니다.&lt;/p&gt;

&lt;h2 id=&quot;예제-1-ssh-서버의-로컬-웹서비스-접속&quot;&gt;예제 1. SSH 서버의 로컬 웹서비스 접속&lt;/h2&gt;

&lt;p&gt;원격 서버에서 다음 서비스가 실행 중이라고 가정합니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;SSH Server: server.example.com:22
Web App:    127.0.0.1:8080
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;외부에서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;8080&lt;/code&gt;에 직접 접근할 수 없고 SSH만 허용되어 있습니다.&lt;/p&gt;

&lt;h3 id=&quot;네트워크-구성&quot;&gt;네트워크 구성&lt;/h3&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[내 PC]
Browser
  |
  | http://127.0.0.1:18080
  v
127.0.0.1:18080
  |
  | SSH encrypted channel
  v
[server.example.com:22]
sshd
  |
  | TCP connection from SSH server
  v
127.0.0.1:8080
[Web Application]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;명령은 다음과 같습니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ssh &lt;span class=&quot;nt&quot;&gt;-N&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-L&lt;/span&gt; 127.0.0.1:18080:127.0.0.1:8080 user@server.example.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이후 내 PC 브라우저에서 접속합니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;http://127.0.0.1:18080
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;실제 흐름은 다음과 같습니다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;브라우저가 내 PC의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;127.0.0.1:18080&lt;/code&gt;에 접속합니다.&lt;/li&gt;
  &lt;li&gt;SSH 클라이언트가 해당 연결을 받습니다.&lt;/li&gt;
  &lt;li&gt;데이터가 SSH 암호화 채널을 통해 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;server.example.com&lt;/code&gt;으로 전달됩니다.&lt;/li&gt;
  &lt;li&gt;SSH 서버가 자신의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;127.0.0.1:8080&lt;/code&gt;으로 연결합니다.&lt;/li&gt;
  &lt;li&gt;응답이 같은 경로를 반대로 돌아옵니다.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-N&lt;/code&gt;은 원격 쉘 명령을 실행하지 않고 포트 포워딩만 사용하겠다는 의미입니다.&lt;/p&gt;

&lt;h2 id=&quot;예제-2-bastion을-경유해-내부-postgresql-접속&quot;&gt;예제 2. Bastion을 경유해 내부 PostgreSQL 접속&lt;/h2&gt;

&lt;p&gt;이번에는 DB가 SSH 서버와 다른 내부 호스트에 있다고 가정합니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Internet
   |
   v
[Bastion]
bastion.example.com:22
   |
   | Internal Network
   v
[PostgreSQL]
db.internal.example:5432
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;내 PC에서 로컬 포트 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;15432&lt;/code&gt;를 사용합니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ssh &lt;span class=&quot;nt&quot;&gt;-N&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-L&lt;/span&gt; 127.0.0.1:15432:db.internal.example:5432 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  user@bastion.example.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;네트워크-흐름&quot;&gt;네트워크 흐름&lt;/h3&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[내 PC]
psql / DBeaver
  |
  | 127.0.0.1:15432
  v
SSH Client
  ||
  || encrypted SSH connection
  \/
[Bastion]
  |
  | db.internal.example:5432
  v
[PostgreSQL]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;DB 클라이언트에서는 실제 DB 주소 대신 로컬 주소를 사용합니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;psql &lt;span class=&quot;nt&quot;&gt;-h&lt;/span&gt; 127.0.0.1 &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; 15432 &lt;span class=&quot;nt&quot;&gt;-U&lt;/span&gt; appuser appdb
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;DBeaver 같은 GUI 도구에서도 다음처럼 입력하면 됩니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Host: 127.0.0.1
Port: 15432
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;여기서 중요한 조건은 &lt;strong&gt;Bastion 서버가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;db.internal.example:5432&lt;/code&gt;에 실제로 접근할 수 있어야 한다는 것&lt;/strong&gt;입니다.&lt;/p&gt;

&lt;p&gt;내 PC가 내부 DB를 직접 볼 수 있을 필요는 없습니다.&lt;/p&gt;

&lt;h2 id=&quot;예제-3-ssh-포트가-22가-아닐-때-jupyter-접속&quot;&gt;예제 3. SSH 포트가 22가 아닐 때 Jupyter 접속&lt;/h2&gt;

&lt;p&gt;SSH 서버가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2222&lt;/code&gt; 포트를 사용하고 Jupyter가 원격 서버의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;127.0.0.1:8888&lt;/code&gt;에서 실행 중이라고 가정합니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ssh &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; 2222 &lt;span class=&quot;nt&quot;&gt;-N&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-L&lt;/span&gt; 127.0.0.1:18888:127.0.0.1:8888 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  user@server.example.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;브라우저에서는 다음 주소로 접속합니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;http://127.0.0.1:18888
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;각 포트의 의미는 서로 다릅니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;2222  = SSH 서버 접속 포트
18888 = 내 PC에서 열리는 로컬 포트
8888  = 원격 서버의 Jupyter 포트
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-p&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-L&lt;/code&gt;의 포트를 혼동하지 않는 것이 중요합니다.&lt;/p&gt;

&lt;h2 id=&quot;예제-4-하나의-ssh-연결로-여러-서비스-포워딩&quot;&gt;예제 4. 하나의 SSH 연결로 여러 서비스 포워딩&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-L&lt;/code&gt;은 여러 번 사용할 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ssh &lt;span class=&quot;nt&quot;&gt;-N&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-L&lt;/span&gt; 127.0.0.1:15432:db.internal.example:5432 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-L&lt;/span&gt; 127.0.0.1:18080:app.internal.example:8080 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-L&lt;/span&gt; 127.0.0.1:19090:monitor.internal.example:9090 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  user@bastion.example.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 경우 내 PC에서는 다음처럼 접근합니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;127.0.0.1:15432 -&amp;gt; PostgreSQL
127.0.0.1:18080 -&amp;gt; Internal Web App
127.0.0.1:19090 -&amp;gt; Monitoring Service
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;여러 내부 서비스를 잠깐 확인할 때 SSH 연결을 각각 만들 필요가 없습니다.&lt;/p&gt;

&lt;h2 id=&quot;자주-사용하는-옵션&quot;&gt;자주 사용하는 옵션&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;옵션&lt;/th&gt;
      &lt;th&gt;의미&lt;/th&gt;
      &lt;th&gt;실무 사용 기준&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-L&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Local port forwarding&lt;/td&gt;
      &lt;td&gt;핵심 옵션&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-N&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;원격 명령을 실행하지 않음&lt;/td&gt;
      &lt;td&gt;터널 전용 연결에 권장&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-T&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;pseudo-terminal 할당 안 함&lt;/td&gt;
      &lt;td&gt;비대화형 터널에서 사용 가능&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-p&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;SSH 서버 포트 지정&lt;/td&gt;
      &lt;td&gt;SSH가 22가 아닐 때&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-i&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;SSH private key 지정&lt;/td&gt;
      &lt;td&gt;특정 키를 사용할 때&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-v&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-vv&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-vvv&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;디버그 로그 증가&lt;/td&gt;
      &lt;td&gt;연결 실패 분석&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-f&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;인증 후 background 전환&lt;/td&gt;
      &lt;td&gt;Unix 계열에서 터널을 백그라운드로 둘 때&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-g&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;다른 호스트가 로컬 포워딩 포트에 접속하도록 허용&lt;/td&gt;
      &lt;td&gt;보안상 특별한 이유가 없으면 사용하지 않음&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;터널 전용으로는 다음처럼 사용할 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ssh &lt;span class=&quot;nt&quot;&gt;-NT&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;ExitOnForwardFailure&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;yes&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-L&lt;/span&gt; 127.0.0.1:15432:db.internal.example:5432 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  user@bastion.example.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ExitOnForwardFailure=yes&lt;/code&gt;는 요청한 포워딩 리스너를 만들지 못했을 때 SSH 연결을 종료하도록 합니다.&lt;/p&gt;

&lt;p&gt;다만 이것이 최종 대상 서비스의 지속적인 정상 접속까지 보장하는 것은 아닙니다. 예를 들어 로컬 리스너는 만들어졌지만 이후 DB가 중지되면 실제 DB 연결은 실패할 수 있습니다.&lt;/p&gt;

&lt;h2 id=&quot;sshconfig로-명령어-줄이기&quot;&gt;~/.ssh/config로 명령어 줄이기&lt;/h2&gt;

&lt;p&gt;반복해서 사용하는 터널은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~/.ssh/config&lt;/code&gt;에 저장하면 편합니다.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-sshconfig&quot;&gt;Host work-db-tunnel
    HostName bastion.example.com
    User user
    IdentityFile ~/.ssh/id_ed25519
    LocalForward 127.0.0.1:15432 db.internal.example:5432
    ExitOnForwardFailure yes
    ServerAliveInterval 30
    ServerAliveCountMax 3
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;이후에는 다음만 실행하면 됩니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ssh &lt;span class=&quot;nt&quot;&gt;-N&lt;/span&gt; work-db-tunnel
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;여러 포워딩도 추가할 수 있습니다.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-sshconfig&quot;&gt;Host work-services
    HostName bastion.example.com
    User user
    LocalForward 127.0.0.1:15432 db.internal.example:5432
    LocalForward 127.0.0.1:18080 app.internal.example:8080
    LocalForward 127.0.0.1:19090 monitor.internal.example:9090
    ExitOnForwardFailure yes
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&quot;검증-방법&quot;&gt;검증 방법&lt;/h2&gt;

&lt;p&gt;터널이 안 될 때는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SSH 접속&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;로컬 포트&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SSH 서버에서 대상 서비스 접근&lt;/code&gt;을 분리해서 확인해야 합니다.&lt;/p&gt;

&lt;h3 id=&quot;1-ssh-자체가-되는지-확인&quot;&gt;1. SSH 자체가 되는지 확인&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ssh &lt;span class=&quot;nt&quot;&gt;-v&lt;/span&gt; user@bastion.example.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;SSH 연결 자체가 실패한다면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-L&lt;/code&gt;보다 먼저 인증, 방화벽, SSH 포트를 확인해야 합니다.&lt;/p&gt;

&lt;h3 id=&quot;2-내-pc에서-포트가-열렸는지-확인&quot;&gt;2. 내 PC에서 포트가 열렸는지 확인&lt;/h3&gt;

&lt;p&gt;Linux에서는 다음처럼 확인할 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ss &lt;span class=&quot;nt&quot;&gt;-lntp&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;grep &lt;/span&gt;15432
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;예상 형태는 다음과 같습니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;127.0.0.1:15432 LISTEN
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Windows PowerShell에서는 다음처럼 확인할 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-powershell highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;netstat&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;-ano&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;findstr&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;15432&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;3-로컬-포트로-실제-요청-확인&quot;&gt;3. 로컬 포트로 실제 요청 확인&lt;/h3&gt;

&lt;p&gt;웹서비스라면 다음처럼 확인합니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;curl &lt;span class=&quot;nt&quot;&gt;-v&lt;/span&gt; http://127.0.0.1:18080/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;PostgreSQL이라면 다음처럼 접속합니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;psql &lt;span class=&quot;nt&quot;&gt;-h&lt;/span&gt; 127.0.0.1 &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; 15432 &lt;span class=&quot;nt&quot;&gt;-U&lt;/span&gt; appuser appdb
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;4-ssh-서버에서-최종-목적지-접근-확인&quot;&gt;4. SSH 서버에서 최종 목적지 접근 확인&lt;/h3&gt;

&lt;p&gt;Bastion에 로그인한 뒤 대상 서비스까지 연결 가능한지 확인합니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;nc &lt;span class=&quot;nt&quot;&gt;-vz&lt;/span&gt; db.internal.example 5432
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;또는 웹서비스라면 다음처럼 확인할 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;curl &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; http://app.internal.example:8080/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;SSH 터널이 정상이어도 Bastion에서 대상 서버로 갈 수 없다면 연결은 실패합니다.&lt;/p&gt;

&lt;h2 id=&quot;자주-발생하는-오류&quot;&gt;자주 발생하는 오류&lt;/h2&gt;

&lt;h3 id=&quot;bind-address-already-in-use&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bind: Address already in use&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;내 PC의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;local_port&lt;/code&gt;를 다른 프로세스가 이미 사용 중인 경우입니다.&lt;/p&gt;

&lt;p&gt;해결 방법은 사용 중인 프로세스를 확인하거나 다른 로컬 포트를 선택하는 것입니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ssh &lt;span class=&quot;nt&quot;&gt;-N&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-L&lt;/span&gt; 127.0.0.1:25432:db.internal.example:5432 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  user@bastion.example.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;목적지 포트가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;5432&lt;/code&gt;라고 해서 로컬 포트도 반드시 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;5432&lt;/code&gt;일 필요는 없습니다.&lt;/p&gt;

&lt;h3 id=&quot;channel--open-failed-administratively-prohibited&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;channel ... open failed: administratively prohibited&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;SSH 서버 정책에서 TCP forwarding이 제한된 경우에 발생할 수 있습니다.&lt;/p&gt;

&lt;p&gt;서버 관리자는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sshd_config&lt;/code&gt;의 다음 항목을 확인해야 합니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;AllowTcpForwarding
PermitOpen
DisableForwarding
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;또한 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;authorized_keys&lt;/code&gt;의 키별 옵션으로 포워딩 목적지가 제한되어 있을 수도 있습니다.&lt;/p&gt;

&lt;p&gt;정책상 금지된 환경에서는 설정을 우회하지 말고 서버 접근 정책에 맞게 허용 범위를 조정해야 합니다.&lt;/p&gt;

&lt;h3 id=&quot;터널은-열렸는데-connection-refused&quot;&gt;터널은 열렸는데 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Connection refused&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;이 경우 SSH 자체보다는 최종 목적지 서비스를 확인해야 합니다.&lt;/p&gt;

&lt;p&gt;예를 들어 다음을 점검합니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;1. 대상 프로세스가 실행 중인가?
2. destination_port가 맞는가?
3. 대상 서비스가 어떤 주소에 listen 중인가?
4. Bastion에서 destination_host:destination_port에 접근 가능한가?
5. 내부 방화벽 또는 보안 그룹이 차단하고 있지 않은가?
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;연결이-자주-끊기는-경우&quot;&gt;연결이 자주 끊기는 경우&lt;/h3&gt;

&lt;p&gt;필요하면 SSH keepalive를 설정할 수 있습니다.&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-sshconfig&quot;&gt;ServerAliveInterval 30
ServerAliveCountMax 3
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;다만 네트워크 장애 자체를 해결하는 옵션은 아니므로, 반복적으로 끊긴다면 VPN, NAT timeout, 방화벽, SSH 서버 로그를 함께 확인해야 합니다.&lt;/p&gt;

&lt;h2 id=&quot;보안상-주의점&quot;&gt;보안상 주의점&lt;/h2&gt;

&lt;h3 id=&quot;1-가능하면-127001에만-바인딩&quot;&gt;1. 가능하면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;127.0.0.1&lt;/code&gt;에만 바인딩&lt;/h3&gt;

&lt;p&gt;다음처럼 명시하는 것을 권장합니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;-L&lt;/span&gt; 127.0.0.1:15432:db.internal.example:5432
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;반대로 다음처럼 모든 인터페이스에 포트를 열면 다른 장비에서 내 PC의 포워딩 포트에 접근할 수 있는 상태가 될 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;-L&lt;/span&gt; 0.0.0.0:15432:db.internal.example:5432
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-g&lt;/code&gt;도 로컬 포워딩 포트를 다른 호스트에 개방하는 방향의 옵션이므로 필요성을 명확히 확인하고 사용해야 합니다.&lt;/p&gt;

&lt;h3 id=&quot;2-ssh-암호화-구간을-정확히-이해&quot;&gt;2. SSH 암호화 구간을 정확히 이해&lt;/h3&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ssh -L&lt;/code&gt;이 암호화하는 핵심 구간은 다음입니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;내 PC
  == SSH encrypted ==&amp;gt;
SSH Server
  -- destination protocol --&amp;gt;
Destination Service
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;즉 SSH 서버 이후의 두 번째 구간까지 SSH가 자동으로 암호화해 주는 것은 아닙니다.&lt;/p&gt;

&lt;p&gt;예를 들어 Bastion에서 PostgreSQL 서버로 가는 구간의 암호화 여부는 PostgreSQL의 TLS 설정 등 최종 애플리케이션 프로토콜에 따라 결정됩니다.&lt;/p&gt;

&lt;h3 id=&quot;3-내부망-접근-권한을-우회-수단으로-사용하지-않기&quot;&gt;3. 내부망 접근 권한을 우회 수단으로 사용하지 않기&lt;/h3&gt;

&lt;p&gt;SSH 포트 포워딩은 Bastion이 가진 네트워크 접근성을 내 PC에서 사용할 수 있게 합니다.&lt;/p&gt;

&lt;p&gt;따라서 편리하지만 잘못 사용하면 네트워크 분리 정책을 사실상 우회하는 통로가 될 수 있습니다. 회사나 고객사 환경에서는 승인된 대상과 포트에만 사용해야 합니다.&lt;/p&gt;

&lt;p&gt;서버 측에서는 필요하면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PermitOpen&lt;/code&gt;이나 키별 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;permitopen&lt;/code&gt; 제한을 사용해 허용 목적지를 좁힐 수 있습니다.&lt;/p&gt;

&lt;h2 id=&quot;-l--r--d-차이&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-L&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-R&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-D&lt;/code&gt; 차이&lt;/h2&gt;

&lt;p&gt;세 옵션은 방향이 다릅니다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;옵션&lt;/th&gt;
      &lt;th&gt;목적&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-L&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;내 PC에서 원격/내부 서비스에 접속&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-R&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;원격 서버에서 내 PC 또는 내 PC가 접근 가능한 서비스에 접속&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-D&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;내 PC에 SOCKS 프록시를 열어 동적으로 목적지를 선택&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;일반적으로 &lt;strong&gt;내 PC에서 내부 DB나 원격 웹서비스를 열고 싶다면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-L&lt;/code&gt;&lt;/strong&gt;부터 생각하면 됩니다.&lt;/p&gt;

&lt;h2 id=&quot;실무용-기본-명령&quot;&gt;실무용 기본 명령&lt;/h2&gt;

&lt;p&gt;특별한 이유가 없다면 다음 형태를 기본값으로 사용하면 이해하기 쉽습니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ssh &lt;span class=&quot;nt&quot;&gt;-NT&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;ExitOnForwardFailure&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;yes&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-L&lt;/span&gt; 127.0.0.1:LOCAL_PORT:DESTINATION_HOST:DESTINATION_PORT &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  user@SSH_SERVER
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;예를 들어 내부 PostgreSQL이라면 다음과 같습니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ssh &lt;span class=&quot;nt&quot;&gt;-NT&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;ExitOnForwardFailure&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;yes&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-L&lt;/span&gt; 127.0.0.1:15432:db.internal.example:5432 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  user@bastion.example.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;기억해야 할 핵심은 한 줄입니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;내 PC의 LOCAL_PORT에 접속하면
SSH_SERVER를 경유해서
SSH_SERVER가 DESTINATION_HOST:DESTINATION_PORT로 연결한다.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 구조만 정확히 이해하면 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ssh -L&lt;/code&gt;의 대부분의 실무 상황을 해석할 수 있습니다.&lt;/p&gt;

&lt;h2 id=&quot;공식-문서&quot;&gt;공식 문서&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;OpenSSH &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ssh(1)&lt;/code&gt;: &lt;a href=&quot;https://man.openbsd.org/ssh&quot;&gt;https://man.openbsd.org/ssh&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;OpenSSH &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ssh_config(5)&lt;/code&gt;: &lt;a href=&quot;https://man.openbsd.org/ssh_config&quot;&gt;https://man.openbsd.org/ssh_config&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;OpenSSH &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sshd_config(5)&lt;/code&gt;: &lt;a href=&quot;https://man.openbsd.org/sshd_config&quot;&gt;https://man.openbsd.org/sshd_config&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Playwright Authentication: &lt;a href=&quot;https://playwright.dev/docs/auth&quot;&gt;https://playwright.dev/docs/auth&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;noVNC: &lt;a href=&quot;https://github.com/novnc/noVNC&quot;&gt;https://github.com/novnc/noVNC&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
 </entry>
 
 <entry>
   <title>2030년까지 서울 내 집 마련을 어떻게 시도할 것인가</title>
   <link href="https://son1004007.github.io/career/2026/08/30/seoul-home-ownership-strategy-through-2030/"/>
   <updated>2026-08-30T00:00:00+00:00</updated>
   <id>https://son1004007.github.io/career/2026/08/30/seoul-home-ownership-strategy-through-2030</id>
   <content type="html">&lt;h2 id=&quot;배경&quot;&gt;배경&lt;/h2&gt;

&lt;p&gt;서울에서 가족이 장기간 거주할 집을 마련하고 싶지만, 선호하는 생활권의 기존 아파트 가격은 현재 감당 가능한 수준을 크게 넘어선다.&lt;/p&gt;

&lt;p&gt;특히 석촌호수 도보권의 대단지 아파트를 바로 매수하는 방식은 현실적인 선택지가 아니다. 그렇다고 주택 소유 자체를 포기하기보다, &lt;strong&gt;공공분양을 먼저 충분히 시도하고 그 이후에는 생활권과 주택 유형을 단계적으로 조정하는 방식&lt;/strong&gt;으로 계획을 단순화하기로 했다.&lt;/p&gt;

&lt;p&gt;이 글의 목적은 매번 부동산 가격과 청약 일정을 다시 검색하며 불안해하지 않도록, 현재 판단과 향후 확인 시점을 하나의 장기 의사결정 기록으로 남기는 것이다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;이 글에 적힌 공급 일정과 제도는 2026년 8월 기준이다. 실제 신청 여부는 각 사업의 최신 공식 모집공고를 기준으로 다시 판단한다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;원하는-주거-조건&quot;&gt;원하는 주거 조건&lt;/h2&gt;

&lt;p&gt;우선순위는 다음과 같다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;서울 내 장기 실거주&lt;/li&gt;
  &lt;li&gt;전용 59~84㎡ 중심&lt;/li&gt;
  &lt;li&gt;지하철 접근성&lt;/li&gt;
  &lt;li&gt;공원과 초등학교를 걸어서 이용할 수 있는 환경&lt;/li&gt;
  &lt;li&gt;아이들이 성장했을 때 학원 접근성이 지나치게 나쁘지 않을 것&lt;/li&gt;
  &lt;li&gt;무리한 대출로 생활비와 현금흐름을 훼손하지 않을 것&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;가장 선호하는 생활상은 석촌호수 도보권이지만, 가격이 맞지 않으면 &lt;strong&gt;올림픽공원 도보 생활권의 방이·오금&lt;/strong&gt;, 그 다음으로 &lt;strong&gt;둔촌 서쪽 생활권&lt;/strong&gt;까지 확장한다. 위례는 우선 검토 대상에서 제외한다.&lt;/p&gt;

&lt;h2 id=&quot;전략-1-2030년까지-공공분양-기회를-먼저-사용한다&quot;&gt;전략 1. 2030년까지 공공분양 기회를 먼저 사용한다&lt;/h2&gt;

&lt;p&gt;현재 공공분양 가이드에서는 신혼부부를 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;혼인기간 7년 이내이거나 6세 이하 자녀를 둔 사람&lt;/code&gt;으로 규정하고 있다. 현재 가족 구성과 현행 기준이 유지된다는 전제에서는 2030년까지 신혼부부 특별공급을 검토할 여지가 있다.&lt;/p&gt;

&lt;p&gt;따라서 &lt;strong&gt;2030년을 공공분양 전략의 종료 시점&lt;/strong&gt;으로 둔다. 다만 제도는 바뀔 수 있으므로 자격을 미리 확정하지 않고 각 모집공고에서 다시 확인한다.&lt;/p&gt;

&lt;p&gt;공식 참고: &lt;a href=&quot;https://apply.lh.or.kr/lhapply/cm/cntnts/cntntsView.do?cntntsId=1201476&amp;amp;mi=1202028&quot;&gt;LH 청약플러스 분양가이드 - 신혼부부 특별공급&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;우선-확인할-공공분양&quot;&gt;우선 확인할 공공분양&lt;/h2&gt;

&lt;h3 id=&quot;1-마곡-16단지&quot;&gt;1. 마곡 16단지&lt;/h3&gt;

&lt;p&gt;가장 먼저 도전할 대상이다.&lt;/p&gt;

&lt;p&gt;2026년 3월 SH 업무보고 기준 마곡16단지는 총 608세대이며, 분양 304세대와 임대 304세대로 구성되어 있다. 서울시는 2026년 2월 설명자료에서 신규 본청약 물량 31호를 2027년 2월 예정으로 안내했다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;총 608세대&lt;/li&gt;
  &lt;li&gt;분양 304세대 / 임대 304세대&lt;/li&gt;
  &lt;li&gt;신규 본청약 계획 물량 31호&lt;/li&gt;
  &lt;li&gt;당시 본청약 예정: 2027년 2월&lt;/li&gt;
  &lt;li&gt;현재 공사 준공 계획: 2027년 11월&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;사전예약자 포기·부적격 물량이 추가될 수 있지만, 이를 당연한 추가 물량으로 계산하지 않는다.&lt;/p&gt;

&lt;p&gt;공식 참고:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://mediahub.seoul.go.kr/archives/2017153&quot;&gt;서울시 설명자료 - 마곡16단지 본청약 계획&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://mas.smc.seoul.kr/attach/record/SEOUL/appendix/a11/A0071006.pdf?time=20260621214133&quot;&gt;SH 2026년 3월 주요 업무보고&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;2-송파-창의혁신-공공주택&quot;&gt;2. 송파 창의혁신 공공주택&lt;/h3&gt;

&lt;p&gt;마곡16 이후 가장 중요하게 볼 사업이다.&lt;/p&gt;

&lt;p&gt;SH의 2026년 3월 업무보고에서는 옛 성동구치소 부지인 송파구 가락동 162번지에 총 1,240세대를 건설하고, &lt;strong&gt;620세대를 분양&lt;/strong&gt;하는 것으로 제시되어 있다. 준공계획은 2029년 5월이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;위치: 송파구 가락동 162번지&lt;/li&gt;
  &lt;li&gt;총 1,240세대&lt;/li&gt;
  &lt;li&gt;분양 620세대 / 임대 620세대&lt;/li&gt;
  &lt;li&gt;준공계획: 2029년 5월&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;마곡16의 신규 물량보다 분양 규모가 훨씬 크기 때문에 장기적으로 가장 중요한 후속 후보로 본다. 다만 실제 공공분양 주택형, 특별공급 배정, 분양가와 청약 일정은 모집공고 전까지 확정된 것으로 간주하지 않는다.&lt;/p&gt;

&lt;p&gt;공식 참고:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://mas.smc.seoul.kr/attach/record/SEOUL/appendix/a11/A0071006.pdf?time=20260621214133&quot;&gt;SH 2026년 3월 주요 업무보고&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.seoul.go.kr/news/news_report.do?nttNo=424175&quot;&gt;서울시 송파 창의혁신 공공주택 설계적격심의&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;3-세곡6단지&quot;&gt;3. 세곡6단지&lt;/h3&gt;

&lt;p&gt;강남 세곡지구의 마지막 공공주택용지다.&lt;/p&gt;

&lt;p&gt;서울시는 총 206호 중 &lt;strong&gt;공공분양 107호&lt;/strong&gt;, 미리내집 99호를 공급할 계획이며 전용 47·51·84㎡로 구성한다고 밝혔다. 2028년 준공을 목표로 추진 중이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;총 206호&lt;/li&gt;
  &lt;li&gt;공공분양 107호&lt;/li&gt;
  &lt;li&gt;전용 47·51·84㎡ 계획&lt;/li&gt;
  &lt;li&gt;2028년 준공 목표&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;입지가 최우선 선호지역은 아니지만 84㎡ 공공분양 가능성이 있으므로 청약 조건과 가격이 맞으면 검토한다.&lt;/p&gt;

&lt;p&gt;공식 참고: &lt;a href=&quot;https://www.seoul.go.kr/news/news_report.do?nttNo=449414&quot;&gt;서울시 - 세곡6단지 공공주택 공급계획&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;4-성뒤마을-a1&quot;&gt;4. 성뒤마을 A1&lt;/h3&gt;

&lt;p&gt;서초 성뒤마을 A1은 SH 업무보고 기준 총 900세대 중 &lt;strong&gt;공공분양 292세대&lt;/strong&gt;, 임대 608세대로 계획되어 있다. 2026년 9월 착공, 2029년 9월 준공 계획이 제시되어 있다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;총 900세대&lt;/li&gt;
  &lt;li&gt;공공분양 292세대&lt;/li&gt;
  &lt;li&gt;임대 608세대&lt;/li&gt;
  &lt;li&gt;준공계획: 2029년 9월&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;주거환경과 분양가격, 실제 면적 구성을 확인한 뒤 판단한다.&lt;/p&gt;

&lt;p&gt;공식 참고: &lt;a href=&quot;https://mas.smc.seoul.kr/attach/record/SEOUL/appendix/a11/A0071006.pdf?time=20260621214133&quot;&gt;SH 2026년 3월 주요 업무보고&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;5-서리풀1-공공주택지구&quot;&gt;5. 서리풀1 공공주택지구&lt;/h3&gt;

&lt;p&gt;국토교통부는 서리풀1 공공주택지구에 약 1만8천호를 공급하고 &lt;strong&gt;2029년 첫 분양&lt;/strong&gt;을 목표로 추진한다고 발표했다.&lt;/p&gt;

&lt;p&gt;규모가 크기 때문에 2030년 이전 새로운 공공분양 기회가 될 수 있다. 그러나 현재 단계에서는 세부 블록별 공급유형과 분양가가 확정되지 않았으므로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;확정 후보&lt;/code&gt;가 아니라 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;대규모 관찰 대상&lt;/code&gt;으로 둔다.&lt;/p&gt;

&lt;p&gt;공식 참고: &lt;a href=&quot;https://www.molit.go.kr/USR/NEWS/m_71/dtl.jsp?id=95091654&quot;&gt;국토교통부 - 서리풀1 공공주택지구 지정&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;마곡17단지는-어떻게-볼-것인가&quot;&gt;마곡17단지는 어떻게 볼 것인가&lt;/h2&gt;

&lt;p&gt;마곡17단지는 본청약이 끝났고 예비입주자까지 선정되어 있다. 따라서 기존 낙첨자가 현재 물량을 바로 승계할 가능성을 기대하지 않는다.&lt;/p&gt;

&lt;p&gt;다만 기존 예비자까지 소진된 뒤 SH가 &lt;strong&gt;잔여세대 또는 추가모집을 별도 공고&lt;/strong&gt;하는 경우에는 다시 신청할 수 있으므로, 이를 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;보너스 기회&lt;/code&gt;로만 확인한다.&lt;/p&gt;

&lt;p&gt;즉 마곡17 잔여물량을 기다리느라 다른 계획을 미루지 않는다.&lt;/p&gt;

&lt;h2 id=&quot;전략-2-공공분양-실패-후-바로-빌라로-가지-않는다&quot;&gt;전략 2. 공공분양 실패 후 바로 빌라로 가지 않는다&lt;/h2&gt;

&lt;p&gt;2030년까지 공공분양에서 결과가 없으면 기존 계획처럼 곧바로 구축 빌라를 사는 것이 아니라, 다음 순서로 매입 대상을 찾는다.&lt;/p&gt;

&lt;h3 id=&quot;1순위-석촌호수-생활권의-소규모-구축-아파트&quot;&gt;1순위: 석촌호수 생활권의 소규모 구축 아파트&lt;/h3&gt;

&lt;p&gt;잠실 대단지는 가격이 너무 높으므로 제외한다. 대신 석촌동·송파동의 나홀로 또는 소규모 구축 아파트 중 장기 실거주에 문제가 없는 물건을 먼저 찾는다.&lt;/p&gt;

&lt;p&gt;목적은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;잠실 대단지 소유&lt;/code&gt;가 아니라 &lt;strong&gt;석촌호수를 걸어서 이용할 수 있는 생활권에 집을 소유하는 것&lt;/strong&gt;이다.&lt;/p&gt;

&lt;h3 id=&quot;2순위-올림픽공원-도보-생활권&quot;&gt;2순위: 올림픽공원 도보 생활권&lt;/h3&gt;

&lt;p&gt;석촌호수 생활권의 가격도 맞지 않을 경우 다음 범위로 이동한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;방이동 남동부&lt;/li&gt;
  &lt;li&gt;오금동&lt;/li&gt;
  &lt;li&gt;필요하면 둔촌동 서쪽&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;공원 접근성과 아이들의 학교·학원 이동을 함께 본다. 대형 학원가가 집 앞에 있어야 하는 것은 아니지만 방이·잠실 학원권으로 이동하기 어려운 위치는 피한다.&lt;/p&gt;

&lt;h3 id=&quot;3순위-구축-빌라&quot;&gt;3순위: 구축 빌라&lt;/h3&gt;

&lt;p&gt;아파트가 예산에 맞지 않을 때만 송파권 구축 빌라를 검토한다.&lt;/p&gt;

&lt;p&gt;빌라는 재개발 가능성을 전제로 사지 않는다. 재개발이 없어도 계속 거주할 수 있어야 한다.&lt;/p&gt;

&lt;p&gt;반드시 확인할 항목은 다음과 같다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;정상적인 대지권&lt;/li&gt;
  &lt;li&gt;위반건축물 여부&lt;/li&gt;
  &lt;li&gt;침수·누수 이력&lt;/li&gt;
  &lt;li&gt;공용배관 상태&lt;/li&gt;
  &lt;li&gt;주차&lt;/li&gt;
  &lt;li&gt;채광과 환기&lt;/li&gt;
  &lt;li&gt;도로 접근성&lt;/li&gt;
  &lt;li&gt;방 3개 등 가족 실거주 가능한 구조&lt;/li&gt;
  &lt;li&gt;매입비 + 취득비 + 수리비 + 예비비를 포함한 총비용&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;하지-않을-것&quot;&gt;하지 않을 것&lt;/h2&gt;

&lt;p&gt;장기 계획을 유지하려면 무엇을 하지 않을지도 정해야 한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;집값이 올랐다는 이유만으로 무리하게 추격 매수하지 않는다.&lt;/li&gt;
  &lt;li&gt;청약 경쟁률만 보고 미리 포기하지 않는다.&lt;/li&gt;
  &lt;li&gt;반대로 낮은 분양가만 보고 입지와 실제 거주성을 무시하지 않는다.&lt;/li&gt;
  &lt;li&gt;잔여세대가 나올 것이라는 기대를 기본계획에 넣지 않는다.&lt;/li&gt;
  &lt;li&gt;매주 공급계획을 검색하며 계속 고민하지 않는다.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;확인-주기&quot;&gt;확인 주기&lt;/h2&gt;

&lt;p&gt;주택 문제는 &lt;strong&gt;반기에 한 번만 정기적으로 확인&lt;/strong&gt;한다.&lt;/p&gt;

&lt;p&gt;확인할 내용은 다음과 같다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;SH·LH·서울시·국토교통부의 공식 공급계획 변경&lt;/li&gt;
  &lt;li&gt;마곡16, 송파 창의혁신, 세곡6, 성뒤마을, 서리풀1의 모집 또는 본청약 일정&lt;/li&gt;
  &lt;li&gt;59·84㎡ 공급 여부&lt;/li&gt;
  &lt;li&gt;신혼부부 특별공급 자격과 소득·자산 기준 변경&lt;/li&gt;
  &lt;li&gt;분양가와 실제 자금조달 가능성&lt;/li&gt;
  &lt;li&gt;마곡17 잔여세대 신규공고 여부&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;구체적인 새 정보가 없으면 다음 점검일까지 다시 고민하지 않는다.&lt;/p&gt;

&lt;h2 id=&quot;최종-의사결정-시점&quot;&gt;최종 의사결정 시점&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;2030년 12월&lt;/strong&gt;을 1차 최종 판단 시점으로 둔다.&lt;/p&gt;

&lt;p&gt;그때까지 공공분양의 실제 기회가 남아 있으면 자격·분양가·입주 시점을 다시 계산한다. 현실적인 기회가 없다면 다음 순서로 자가 매입을 추진한다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;석촌호수 도보권 구축 아파트
→ 올림픽공원 도보권(방이·오금) 구축 아파트
→ 둔촌 서쪽까지 범위 확대
→ 송파권 구축 빌라
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;정리&quot;&gt;정리&lt;/h2&gt;

&lt;p&gt;마곡16에 실패한다고 주택 소유의 기회가 끝나는 것은 아니다.&lt;/p&gt;

&lt;p&gt;현재 확인할 수 있는 공공분양만 보더라도 송파 창의혁신, 세곡6, 성뒤마을, 서리풀1과 같은 후속 후보가 있다. 따라서 2030년까지는 공공분양을 차분히 확인하고, 그 이후에도 석촌호수 또는 올림픽공원 생활권의 구축 아파트라는 두 번째 경로가 남아 있다.&lt;/p&gt;

&lt;p&gt;핵심은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;반드시 특정 아파트를 가져야 한다&lt;/code&gt;가 아니라 다음 조건을 지키면서 실제 소유 가능한 집을 찾는 것이다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;가족이 오래 살 수 있는 위치와 면적을 확보하되, 주택 때문에 현재의 생활 자체를 무너뜨리지 않는다.&lt;/p&gt;
&lt;/blockquote&gt;
</content>
 </entry>
 
 <entry>
   <title>Galaxy S25에서 ChatGPT 잠금 해제 후 이전 대화가 열리지 않는 문제 해결</title>
   <link href="https://son1004007.github.io/career/2026/08/29/galaxy-s25-chatgpt-chat-state-retention/"/>
   <updated>2026-08-29T00:00:00+00:00</updated>
   <id>https://son1004007.github.io/career/2026/08/29/galaxy-s25-chatgpt-chat-state-retention</id>
   <content type="html">&lt;h2 id=&quot;문제점&quot;&gt;문제점&lt;/h2&gt;

&lt;p&gt;Galaxy S25에서 ChatGPT Android 앱을 사용하다 화면을 잠근 뒤 다시 들어오면, 잠금 전에 보고 있던 대화가 아니라 새 대화 화면 또는 처음 진입한 화면이 표시되는 현상이 있었다.&lt;/p&gt;

&lt;p&gt;대화 내용 자체가 삭제되는 것은 아니었지만, 긴 대화를 계속 사용하고 있는 경우 매번 채팅 목록을 열어 기존 대화를 다시 찾아야 했다.&lt;/p&gt;

&lt;p&gt;특히 모바일에서 ChatGPT를 업무 메모나 장시간 이어지는 작업에 사용하는 경우 흐름이 자주 끊기는 문제가 있었다.&lt;/p&gt;

&lt;p&gt;정리하면 증상은 다음과 같았다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Galaxy S25에서 ChatGPT Android 앱 사용&lt;/li&gt;
  &lt;li&gt;특정 대화를 연 상태에서 화면 잠금&lt;/li&gt;
  &lt;li&gt;잠금 해제 후 ChatGPT로 복귀&lt;/li&gt;
  &lt;li&gt;직전에 사용하던 대화가 아닌 다른 초기 화면이 표시됨&lt;/li&gt;
  &lt;li&gt;기존 대화는 채팅 기록에는 정상적으로 남아 있음&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;원인&quot;&gt;원인&lt;/h2&gt;

&lt;p&gt;처음에는 ChatGPT에서 특정 채팅을 고정하면 해결될 것으로 생각할 수 있다.&lt;/p&gt;

&lt;p&gt;하지만 ChatGPT의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Pin chat&lt;/code&gt; 기능은 중요한 대화를 &lt;strong&gt;채팅 목록 상단에 고정하는 기능&lt;/strong&gt;이다. OpenAI 공식 릴리스 노트에서도 모바일에서는 채팅을 길게 눌러 고정할 수 있다고 설명하고 있으며, 앱 재진입 시 해당 채팅을 항상 자동으로 다시 여는 기능으로 설명되지는 않는다.&lt;/p&gt;

&lt;p&gt;따라서 다음 두 문제를 구분할 필요가 있다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;채팅 고정
= 원하는 대화를 채팅 목록에서 쉽게 찾기 위한 기능

앱 상태 유지
= 화면 잠금 또는 앱 전환 이후에도 기존 Activity/프로세스 상태를 최대한 유지하는 문제
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Galaxy는 배터리 절약을 위해 사용 패턴에 따라 앱의 백그라운드 실행을 제한할 수 있다. Samsung 공식 문서에서도 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Background usage limits&lt;/code&gt;에서 앱을 Sleeping/Deep sleeping 상태로 관리하며, 필요한 앱은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Never sleeping apps&lt;/code&gt;에 추가할 수 있다고 설명한다.&lt;/p&gt;

&lt;p&gt;이번 현상의 내부 원인을 직접 추적한 것은 아니므로 ChatGPT 프로세스가 실제로 종료되었다고 단정할 수는 없다. 다만 다음 사실을 근거로 Galaxy의 백그라운드 앱 관리와 화면 상태 복원이 주요 원인 후보라고 판단했다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;대화 데이터 자체는 정상적으로 존재했다.&lt;/li&gt;
  &lt;li&gt;문제가 화면 잠금 후 앱 복귀 과정에서 발생했다.&lt;/li&gt;
  &lt;li&gt;ChatGPT를 백그라운드 종료/절전 대상에서 제외한 뒤 현상이 사라졌다.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;즉 데이터 손실 문제가 아니라 &lt;strong&gt;모바일 앱의 실행 상태 유지 문제&lt;/strong&gt;에 가까웠다.&lt;/p&gt;

&lt;h2 id=&quot;해결&quot;&gt;해결&lt;/h2&gt;

&lt;p&gt;해결에는 Galaxy의 두 설정을 함께 적용했다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;최근 앱 화면에서 ChatGPT를 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;열어두기(Keep open)&lt;/code&gt; 상태로 설정&lt;/li&gt;
  &lt;li&gt;배터리 설정에서 ChatGPT를 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;절전하지 않는 앱(Never sleeping apps)&lt;/code&gt;에 추가&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;ChatGPT의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Pin chat&lt;/code&gt;도 함께 사용할 수 있지만, 이것은 문제 자체를 해결하기보다 원하는 대화를 다시 찾기 쉽게 만드는 보조 설정이다.&lt;/p&gt;

&lt;p&gt;최종적으로 사용한 구성은 다음과 같다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ChatGPT 채팅 고정        : 선택 사항
Galaxy 최근 앱 열어두기 : 적용
Galaxy 절전 예외         : 적용
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;실행-방법&quot;&gt;실행 방법&lt;/h2&gt;

&lt;h3 id=&quot;1-chatgpt를-최근-앱에서-열어두기&quot;&gt;1. ChatGPT를 최근 앱에서 열어두기&lt;/h3&gt;

&lt;p&gt;ChatGPT를 실행한 상태에서 Galaxy의 최근 앱 화면으로 이동한다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ChatGPT 실행
-&amp;gt; 최근 앱 화면 열기
-&amp;gt; ChatGPT 미리보기 상단의 앱 아이콘 선택
-&amp;gt; &quot;열어두기&quot; 또는 &quot;Keep open&quot; 선택
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;One UI 버전에 따라 메뉴의 한글 표기는 조금 다를 수 있다.&lt;/p&gt;

&lt;p&gt;이 설정은 최근 앱 정리 과정에서 ChatGPT가 쉽게 종료되지 않도록 하는 데 도움이 된다.&lt;/p&gt;

&lt;h3 id=&quot;2-chatgpt를-절전-예외-앱으로-추가&quot;&gt;2. ChatGPT를 절전 예외 앱으로 추가&lt;/h3&gt;

&lt;p&gt;Galaxy 설정에서 다음 경로로 이동한다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;설정
-&amp;gt; 배터리
-&amp;gt; 백그라운드 사용 제한
-&amp;gt; 절전하지 않는 앱 / Never sleeping apps
-&amp;gt; +
-&amp;gt; ChatGPT 추가
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Samsung은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Never sleeping apps&lt;/code&gt;에 추가된 앱은 자동으로 Sleeping 또는 Deep sleeping 상태로 전환되지 않는다고 안내한다.&lt;/p&gt;

&lt;p&gt;다만 백그라운드 실행을 더 오래 허용하기 때문에 배터리 사용량은 다소 증가할 수 있다.&lt;/p&gt;

&lt;h3 id=&quot;3-필요하면-chatgpt-채팅도-고정&quot;&gt;3. 필요하면 ChatGPT 채팅도 고정&lt;/h3&gt;

&lt;p&gt;자주 사용하는 대화라면 ChatGPT 내부에서도 채팅을 고정한다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ChatGPT 메뉴
-&amp;gt; 원하는 채팅 길게 누르기
-&amp;gt; Pin chat / 채팅 고정
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 설정은 앱이 다른 화면으로 열렸을 때 원하는 대화를 빠르게 찾는 용도로 사용한다.&lt;/p&gt;

&lt;h2 id=&quot;검증-방법&quot;&gt;검증 방법&lt;/h2&gt;

&lt;p&gt;설정 후 실제 Galaxy S25에서 다음 순서로 확인했다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;1. ChatGPT에서 기존 대화 열기
2. 화면 잠금
3. 잠금 해제
4. ChatGPT 다시 진입
5. 잠금 전에 사용하던 대화가 그대로 표시되는지 확인
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;열어두기&lt;/code&gt;와 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Never sleeping apps&lt;/code&gt;를 적용한 이후 기존에 반복되던 현상이 더 이상 발생하지 않았고, 잠금 전에 사용하던 대화 화면으로 정상 복귀했다.&lt;/p&gt;

&lt;p&gt;따라서 이 환경에서는 두 설정의 조합이 유효한 대응 방법이었다.&lt;/p&gt;

&lt;h2 id=&quot;재발-방지--개선-방향&quot;&gt;재발 방지 / 개선 방향&lt;/h2&gt;

&lt;p&gt;같은 증상이 다시 발생하면 다음 순서로 확인하는 것이 효율적이다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;ChatGPT가 Sleeping 또는 Deep sleeping apps에 들어가 있지 않은지 확인한다.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Never sleeping apps&lt;/code&gt;에 ChatGPT가 유지되어 있는지 확인한다.&lt;/li&gt;
  &lt;li&gt;최근 앱의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Keep open&lt;/code&gt; 설정이 유지되고 있는지 확인한다.&lt;/li&gt;
  &lt;li&gt;ChatGPT 앱 업데이트 후에만 문제가 재발하는지 확인한다.&lt;/li&gt;
  &lt;li&gt;Galaxy One UI 업데이트 후 배터리 관련 설정이 초기화되지 않았는지 확인한다.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;중요한 점은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Pin chat&lt;/code&gt;과 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;앱 실행 상태 유지&lt;/code&gt;를 같은 기능으로 생각하지 않는 것이다.&lt;/p&gt;

&lt;p&gt;특정 대화를 목록 위에 고정하는 것과 Android가 ChatGPT 앱의 실행 상태를 계속 유지하도록 하는 것은 서로 다른 계층의 문제다.&lt;/p&gt;

&lt;h2 id=&quot;정리&quot;&gt;정리&lt;/h2&gt;

&lt;p&gt;Galaxy S25에서 화면 잠금 이후 ChatGPT가 직전 대화로 복귀하지 않는 문제는 다음 설정으로 해결했다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;최근 앱 -&amp;gt; ChatGPT -&amp;gt; 열어두기
설정 -&amp;gt; 배터리 -&amp;gt; 백그라운드 사용 제한
     -&amp;gt; Never sleeping apps -&amp;gt; ChatGPT 추가
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;필요하면 ChatGPT 내부의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Pin chat&lt;/code&gt;을 추가해 자주 사용하는 대화를 목록 상단에 고정한다.&lt;/p&gt;

&lt;p&gt;이번 사례에서 핵심은 앱 자체의 대화 데이터 문제와 Android의 앱 실행 상태 관리 문제를 분리해서 접근한 것이다. 모바일 앱에서 재현이 불규칙한 문제도 OS의 프로세스 관리, 배터리 최적화, 앱 내부 기능을 각각 분리해서 확인하면 원인을 더 빠르게 좁힐 수 있다.&lt;/p&gt;

&lt;h2 id=&quot;참고-자료&quot;&gt;참고 자료&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.samsung.com/us/support/answer/ANS10002534/&quot;&gt;Samsung - Get the most out of your Galaxy phone or tablet battery&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.samsung.com/us/support/galaxy-battery/optimization/&quot;&gt;Samsung - Galaxy Battery Optimization&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://help.openai.com/ko-kr/articles/6825453-chatgpt-%EB%A6%B4%EB%A6%AC%EC%8A%A4-%EB%85%B8%ED%8A%B8&quot;&gt;OpenAI - ChatGPT Release Notes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
 </entry>
 
 <entry>
   <title>ChatGPT Custom Instructions로 GitHub 작업 기준을 자동으로 불러오게 만드는 방법</title>
   <link href="https://son1004007.github.io/infrastructure/2026/08/24/chatgpt-custom-instructions-github-control-bootstrap/"/>
   <updated>2026-08-24T00:00:00+00:00</updated>
   <id>https://son1004007.github.io/infrastructure/2026/08/24/chatgpt-custom-instructions-github-control-bootstrap</id>
   <content type="html">&lt;p&gt;여러 ChatGPT 채팅에서 같은 GitHub 프로젝트를 반복해서 다루다 보면 매번 같은 설명을 다시 해야 하는 문제가 생긴다.&lt;/p&gt;

&lt;p&gt;예를 들어 다음 내용을 새 채팅마다 다시 전달하게 된다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;어떤 저장소가 전체 작업 기준인지&lt;/li&gt;
  &lt;li&gt;어떤 프로젝트 저장소를 실제 Source of Truth로 봐야 하는지&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AI_CONTEXT.md&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CURRENT_STATE.md&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;WORKS.md&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TASKS.md&lt;/code&gt; 중 무엇을 먼저 읽어야 하는지&lt;/li&gt;
  &lt;li&gt;원격 서버 작업은 어떤 제어 저장소와 안전 규칙을 따라야 하는지&lt;/li&gt;
  &lt;li&gt;모르는 기술 동작은 공식 문서를 먼저 확인해야 한다는 원칙&lt;/li&gt;
  &lt;li&gt;테스트나 실행 증거 없이 완료라고 판단하면 안 된다는 규칙&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이 문제를 해결하기 위해 GitHub 쪽에는 중앙 Control 저장소와 프로젝트별 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt;를 두고, 서버의 Codex에는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$CODEX_HOME/AGENTS.md&lt;/code&gt;를 두었다. 하지만 일반 ChatGPT의 새 채팅은 Codex처럼 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$CODEX_HOME/AGENTS.md&lt;/code&gt;를 자동 탐색하지 않는다.&lt;/p&gt;

&lt;p&gt;그래서 ChatGPT에는 &lt;strong&gt;Custom Instructions를 아주 얇은 bootstrap layer로 사용&lt;/strong&gt;했다.&lt;/p&gt;

&lt;p&gt;이 글은 &lt;a href=&quot;/infrastructure/2026/08/23/global-codex-agents-control-plane/&quot;&gt;여러 프로젝트와 서버에서 Codex 운영 기준을 자동으로 이어받게 만든 방법&lt;/a&gt;에서 구성한 GitHub/서버 Control 구조를 일반 ChatGPT 새 채팅까지 확장한 기록이다.&lt;/p&gt;

&lt;h2 id=&quot;문제점&quot;&gt;문제점&lt;/h2&gt;

&lt;p&gt;GitHub에 중앙 정책 저장소가 있어도 일반 ChatGPT 새 채팅은 그 저장소의 존재를 자동으로 알 수 없다.&lt;/p&gt;

&lt;p&gt;따라서 이런 요청을 했을 때:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;포트폴리오 계속 진행해줘.
CISA 서비스 상태 확인해줘.
Office에서 테스트해줘.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;ChatGPT가 이전 대화를 기준으로 추측하거나, 사용자에게 이미 GitHub에 기록된 상태를 다시 설명해 달라고 요구할 수 있다.&lt;/p&gt;

&lt;p&gt;중앙 Control의 목적은 오히려 반대다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;사용자가 과거 상태를 다시 설명
        X

ChatGPT가 GitHub에서 현재 상태 복원
        O
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;즉 ChatGPT가 &lt;strong&gt;어디서 시작해야 하는지 한 번만 알려주는 계정 수준 bootstrap&lt;/strong&gt;이 필요했다.&lt;/p&gt;

&lt;h2 id=&quot;원인&quot;&gt;원인&lt;/h2&gt;

&lt;p&gt;Codex와 일반 ChatGPT는 지침을 발견하는 위치가 다르다.&lt;/p&gt;

&lt;p&gt;Codex CLI는 사용자 환경의 global &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt;와 프로젝트의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt;를 이용할 수 있다. 반면 일반 ChatGPT 새 채팅은 내 GitHub의 특정 private repository를 임의로 찾아 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CONTROL.md&lt;/code&gt;부터 읽는다는 보장이 없다.&lt;/p&gt;

&lt;p&gt;그래서 역할을 다음처럼 분리했다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ChatGPT Custom Instructions
  -&amp;gt; 중앙 Control의 위치만 알려줌

GitHub 중앙 Control
  -&amp;gt; 어떤 저장소를 어떤 순서로 읽을지 결정

각 프로젝트 repository
  -&amp;gt; 코드와 실제 현재 상태의 Source of Truth

Office / NAS Codex
  -&amp;gt; $CODEX_HOME/AGENTS.md로 동일한 중앙 Control 발견
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;핵심은 &lt;strong&gt;Custom Instructions에 프로젝트 상태를 복사하지 않는 것&lt;/strong&gt;이다.&lt;/p&gt;

&lt;p&gt;프로젝트 상태까지 Custom Instructions에 넣으면 시간이 지나면서 GitHub와 내용이 달라지고 다시 관리 포인트가 생긴다.&lt;/p&gt;

&lt;h2 id=&quot;github-공식-문서에서도-확인되는-유사한-구조&quot;&gt;GitHub 공식 문서에서도 확인되는 유사한 구조&lt;/h2&gt;

&lt;p&gt;이 구조를 만든 뒤 GitHub 공식 문서를 확인해 보니, GitHub Copilot도 instruction을 한 파일에 모두 넣기보다 &lt;strong&gt;사용자 수준, repository 수준, path 수준, agent 수준으로 나누는 구조&lt;/strong&gt;를 공식 지원하고 있었다.&lt;/p&gt;

&lt;p&gt;GitHub 공식 문서:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/copilot/how-tos/copilot-on-github/customize-copilot/add-custom-instructions/add-repository-instructions&quot;&gt;Adding repository custom instructions for GitHub Copilot&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/copilot/how-tos/configure-custom-instructions-in-your-ide/add-repository-instructions-in-your-ide&quot;&gt;Adding repository custom instructions for GitHub Copilot in your IDE&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-custom-instructions&quot;&gt;Adding custom instructions for GitHub Copilot CLI&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/copilot/reference/custom-instructions-support&quot;&gt;Support for different types of custom instructions&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://docs.github.com/en/copilot/how-tos/copilot-on-github/customize-copilot/customize-copilot-overview&quot;&gt;Customize Copilot for your project&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;repository-wide-instructions&quot;&gt;Repository-wide instructions&lt;/h3&gt;

&lt;p&gt;GitHub은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.github/copilot-instructions.md&lt;/code&gt;를 repository 전체에 적용되는 지속적인 지침으로 정의한다.&lt;/p&gt;

&lt;p&gt;이 파일에는 프로젝트 구조, 코딩 규칙, build/test 방법처럼 해당 repository를 이해하고 수정할 때 반복적으로 필요한 정보를 둘 수 있다.&lt;/p&gt;

&lt;p&gt;개념적으로 내가 사용하는 root &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt;와 비슷한 역할이다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;repository
  -&amp;gt; .github/copilot-instructions.md
  -&amp;gt; AGENTS.md
  -&amp;gt; code / tests / local state docs
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;다만 둘은 같은 파일 형식이 아니다. 사용하는 AI와 실행 환경이 지원하는 instruction 종류를 확인해야 한다.&lt;/p&gt;

&lt;h3 id=&quot;path-specific-instructions&quot;&gt;Path-specific instructions&lt;/h3&gt;

&lt;p&gt;GitHub은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.github/instructions/**/*.instructions.md&lt;/code&gt; 파일과 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;applyTo&lt;/code&gt; glob을 이용해 특정 경로에만 지침을 적용할 수 있도록 한다.&lt;/p&gt;

&lt;p&gt;예를 들어 Java와 frontend 코드의 규칙이 다르다면 전역 문서를 거대하게 만드는 대신 다음처럼 범위를 분리할 수 있다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;.github/instructions/
  backend.instructions.md
  frontend.instructions.md
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이는 중앙 Control에 모든 프로젝트 세부 규칙을 넣지 않고 &lt;strong&gt;가장 가까운 실제 작업 범위에 세부 정책을 둔다&lt;/strong&gt;는 현재 설계와 같은 방향이다.&lt;/p&gt;

&lt;h3 id=&quot;agent-instructions와-agentsmd&quot;&gt;Agent instructions와 AGENTS.md&lt;/h3&gt;

&lt;p&gt;GitHub 공식 문서는 Copilot agent가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt;를 agent instruction으로 사용할 수 있다고 설명한다.&lt;/p&gt;

&lt;p&gt;하위 디렉터리에 여러 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt;가 있다면 작업 위치와 가까운 파일이 더 구체적인 지침 역할을 한다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;root AGENTS.md
  -&amp;gt; 전체 repository 규칙

backend/AGENTS.md
  -&amp;gt; backend에 더 구체적인 규칙

backend/payment/AGENTS.md
  -&amp;gt; payment 작업에 가장 구체적인 규칙
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 점은 현재 사용 중인 &lt;strong&gt;전역 Control은 공통 원칙만 소유하고, 실제 프로젝트 repository의 local &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt;가 더 구체적인 제한을 소유하는 방식&lt;/strong&gt;과 잘 맞는다.&lt;/p&gt;

&lt;h3 id=&quot;copilot-cli에는-사용자-수준-전역-instructions도-있다&quot;&gt;Copilot CLI에는 사용자 수준 전역 instructions도 있다&lt;/h3&gt;

&lt;p&gt;GitHub Copilot CLI 공식 문서는 다음 위치를 사용자 수준 instruction으로 정의한다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$HOME/.copilot/copilot-instructions.md
$HOME/.copilot/instructions/**/*.instructions.md
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 지침은 여러 repository에 걸쳐 적용된다.&lt;/p&gt;

&lt;p&gt;역할만 비교하면 현재 구조의 다음 두 요소와 유사하다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ChatGPT
 -&amp;gt; Custom Instructions

Codex
 -&amp;gt; $CODEX_HOME/AGENTS.md
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;즉 &lt;strong&gt;사용자 수준에는 얇은 공통 bootstrap을 두고, repository 수준에는 구체적인 프로젝트 지침을 두는 패턴&lt;/strong&gt; 자체가 GitHub Copilot의 공식 customization 모델에서도 확인된다.&lt;/p&gt;

&lt;h3 id=&quot;다른-파일을-참조하는-기능도-공식-지원한다&quot;&gt;다른 파일을 참조하는 기능도 공식 지원한다&lt;/h3&gt;

&lt;p&gt;Copilot CLI 문서에서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.github/copilot-instructions.md&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CLAUDE.md&lt;/code&gt; 등에서 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@&lt;/code&gt;와 상대 경로를 사용해 다른 파일을 포함할 수 있다고 설명한다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;@docs/build-and-test.md
@docs/security-policy.md
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 기능은 한 문서에 세부 내용을 복사하지 않고 원본 문서를 참조하는 데 유용하다.&lt;/p&gt;

&lt;p&gt;내 중앙 Control 설계도 같은 원칙을 사용하지만 구현 방식은 다르다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Custom Instructions
 -&amp;gt; CONTROL.md를 확인하라고 지시
 -&amp;gt; registry를 읽음
 -&amp;gt; 실제 Source of Truth로 이동
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;일반 ChatGPT에서는 GitHub Copilot CLI의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@relative/path&lt;/code&gt; 포함 동작을 그대로 가정하지 않는다. &lt;strong&gt;GitHub 접근이 가능한 ChatGPT에게 자연어 instruction으로 해당 파일을 실제 확인하도록 요구하는 bootstrap&lt;/strong&gt;이다.&lt;/p&gt;

&lt;h3 id=&quot;모든-제품-표면이-같은-instruction을-지원하는-것은-아니다&quot;&gt;모든 제품 표면이 같은 instruction을 지원하는 것은 아니다&lt;/h3&gt;

&lt;p&gt;GitHub은 별도 support matrix를 제공한다. GitHub.com Copilot Chat, Copilot cloud agent, code review, VS Code, JetBrains, Copilot CLI 등에서 지원하는 instruction 종류가 서로 다르다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;AGENTS.md가 GitHub의 한 기능에서 지원됨
 !=
모든 AI/모든 실행환경에서 자동 적용됨
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;따라서 현재 구조에서는 특정 제품의 자동 탐색 기능 하나에만 의존하지 않고 다음을 함께 사용한다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ChatGPT Custom Instructions
 + repository-local AGENTS.md
 + central CONTROL.md
 + Codex runtime global AGENTS.md
 + local security fallback
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 방식은 중복처럼 보일 수 있지만 실제로는 &lt;strong&gt;서로 다른 AI 실행 표면 사이의 bootstrap gap을 메우는 최소 포인터&lt;/strong&gt;다.&lt;/p&gt;

&lt;h2 id=&quot;해결&quot;&gt;해결&lt;/h2&gt;

&lt;p&gt;ChatGPT의 Custom Instructions에는 중앙 GitHub Control을 찾기 위한 bootstrap만 넣었다.&lt;/p&gt;

&lt;p&gt;현재 OpenAI 도움말 기준으로 Custom Instructions는 Web, Desktop, iOS, Android에서 사용할 수 있고, 활성화하면 채팅 전반에 적용된다.&lt;/p&gt;

&lt;p&gt;OpenAI 공식 문서:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://help.openai.com/en/articles/8096356-chatgpt-custom-instructions&quot;&gt;ChatGPT Custom Instructions&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://help.openai.com/en/articles/10169521-projects-in-chatgpt&quot;&gt;Projects in ChatGPT&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Android에서는 현재 다음 순서로 설정할 수 있다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ChatGPT Settings
 -&amp;gt; Customize ChatGPT
 -&amp;gt; Enable customization ON
 -&amp;gt; Custom Instructions
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Web/Desktop에서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Settings -&amp;gt; Personalization&lt;/code&gt;에서 Custom Instructions를 설정한다.&lt;/p&gt;

&lt;h2 id=&quot;실행-방법&quot;&gt;실행 방법&lt;/h2&gt;

&lt;p&gt;Custom Instructions에는 아래와 같은 bootstrap을 넣는다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[GitHub AI Work Bootstrap]

내 GitHub 저장소, 개발, 서버, 배포, Agent 작업 또는 이전 작업의 연속성을 다루는 요청에서는 GitHub 접근이 가능하면 먼저 다음 전역 Control을 확인한다.

son1004007/ai-agent-workflow-playbook/CONTROL.md

그 다음 CONTROL.md의 repository registry를 따라 실제 대상 저장소를 찾고, 해당 저장소의 AGENTS.md와 AI_CONTEXT.md, CURRENT_STATE.md, WORKS.md, TASKS.md 등 존재하는 최신 상태 문서를 확인한다.

프로젝트 코드와 현재 상태는 각 대상 repository를 Source of Truth로 사용한다. GitHub commit, PR, Issue, Actions 또는 실제 runtime evidence로 확인 가능한 내용은 사용자에게 다시 설명해 달라고 요구하지 말고 직접 확인한다.

원격 실행이 필요하면 CONTROL.md의 mapping을 따라 device-control을 확인하고 기존 보안, sandbox, 배포 정책을 유지한다.

기술 동작이 불확실하거나 버전, 인증, 보안, sandbox, OS/runtime에 의존하면 추측하지 말고 Official-Source-First를 적용한다.

테스트나 실제 실행 증거 없이 완료를 주장하지 않는다. 전역 Control에 접근할 수 없으면 local AGENTS.md와 안전 규칙을 사용하며, 이를 이유로 보안 제한을 완화하지 않는다.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;여기에는 서버 IP, SSH 계정, 토큰, 프로젝트의 현재 진행률 같은 mutable/private 정보를 넣지 않는다.&lt;/p&gt;

&lt;p&gt;그 정보는 각각의 Source of Truth에서 읽어야 한다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Custom Instructions
        |
        v
ai-agent-workflow-playbook/CONTROL.md
        |
        +-&amp;gt; config/repositories.yml
        |
        +-&amp;gt; engineering-career-portfolio
        |      -&amp;gt; AGENTS.md
        |      -&amp;gt; AI_CONTEXT / WORKS / TASKS
        |
        +-&amp;gt; cisa-playbook
        |      -&amp;gt; AGENTS.md
        |      -&amp;gt; service/runbook docs
        |
        +-&amp;gt; device-control
               -&amp;gt; remote runtime policy/evidence
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;project-instructions와의-차이&quot;&gt;Project Instructions와의 차이&lt;/h2&gt;

&lt;p&gt;ChatGPT Project를 사용하는 경우 프로젝트별 지침도 따로 설정할 수 있다.&lt;/p&gt;

&lt;p&gt;OpenAI 공식 도움말 기준으로 Project Instructions는 &lt;strong&gt;그 프로젝트 안에서만 적용되고 전역 Custom Instructions보다 우선한다.&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;일반 채팅
 -&amp;gt; global Custom Instructions

특정 ChatGPT Project 안의 채팅
 -&amp;gt; Project Instructions
 -&amp;gt; 필요 시 global bootstrap과 같은 중앙 Control 사용
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;프로젝트별로 완전히 다른 규칙이 필요하지 않다면 Project Instructions에도 세부 프로젝트 상태를 복사하지 않고 다음 정도의 포인터만 두는 편이 관리하기 쉽다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;GitHub 개발/운영 작업에서는
son1004007/ai-agent-workflow-playbook/CONTROL.md를 먼저 확인하고,
그 registry와 대상 repo의 local AGENTS.md를 따른다.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;검증-방법&quot;&gt;검증 방법&lt;/h2&gt;

&lt;p&gt;설정을 넣은 뒤에는 새 ChatGPT 채팅에서 실제로 복원 흐름이 동작하는지 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;내 포트폴리오 현재 진행 상태 확인해줘.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;기대한 동작은 다음과 같다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Custom Instructions
 -&amp;gt; CONTROL.md
 -&amp;gt; repositories.yml
 -&amp;gt; engineering-career-portfolio
 -&amp;gt; AGENTS.md
 -&amp;gt; AI_CONTEXT / WORKS / TASKS
 -&amp;gt; latest commit / PR / Actions
 -&amp;gt; 필요한 경우 device-control / Office runtime
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;다음과 같은 응답 패턴이면 bootstrap이 제대로 활용되지 않았을 가능성이 있다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;이전에 어디까지 했는지 다시 설명해주세요.
서버가 어디인지 알려주세요.
프로젝트 repo 이름을 다시 알려주세요.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;물론 GitHub 접근 권한이 없는 ChatGPT 환경에서는 private Control repository를 실제로 읽을 수 없다. 이 경우에는 접근 불가 사실을 표시하고 사용자가 제공한 현재 자료나 공개 repository 범위에서만 판단해야 한다.&lt;/p&gt;

&lt;h2 id=&quot;재발-방지--개선-방향&quot;&gt;재발 방지 / 개선 방향&lt;/h2&gt;

&lt;p&gt;이 구조에서 관리해야 할 원본은 최대한 줄인다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;1. 전역 작업 정책
   -&amp;gt; ai-agent-workflow-playbook/CONTROL.md

2. repository 관계
   -&amp;gt; config/repositories.yml

3. 프로젝트 현재 상태
   -&amp;gt; 각 프로젝트 repository

4. Codex 실행환경 bootstrap
   -&amp;gt; $CODEX_HOME/AGENTS.md managed block

5. 일반 ChatGPT bootstrap
   -&amp;gt; Custom Instructions
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Custom Instructions에는 &lt;strong&gt;중앙 Control을 찾는 규칙만 유지&lt;/strong&gt;한다.&lt;/p&gt;

&lt;p&gt;새 repository를 만들거나 기존 repository를 다시 활성화할 때는 중앙 registry와 해당 repository의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt;를 갱신한다. Custom Instructions는 보통 다시 수정할 필요가 없다.&lt;/p&gt;

&lt;p&gt;GitHub Copilot까지 같은 운영 철학을 적용한다면 공식 지원 위치를 그대로 활용할 수 있다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Copilot user-level
 -&amp;gt; $HOME/.copilot/copilot-instructions.md

Repository-wide
 -&amp;gt; .github/copilot-instructions.md

Path-specific
 -&amp;gt; .github/instructions/**/*.instructions.md

Agent-specific
 -&amp;gt; AGENTS.md
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;다만 모든 파일에 동일한 내용을 복사하는 것은 피하고, 각 계층에는 자기 범위에 필요한 최소 지침과 원본 문서 포인터만 두는 편이 낫다.&lt;/p&gt;

&lt;p&gt;기술 동작을 잘 모를 때의 공통 판단 방식은 &lt;a href=&quot;/infrastructure/2026/08/23/official-source-first-ai-troubleshooting/&quot;&gt;Official-Source-First 원칙&lt;/a&gt;을 그대로 사용한다.&lt;/p&gt;

&lt;h2 id=&quot;주의점&quot;&gt;주의점&lt;/h2&gt;

&lt;p&gt;Custom Instructions에 다음 정보를 넣지 않는 편이 좋다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;비밀번호, API key, SSH private key&lt;/li&gt;
  &lt;li&gt;서버 IP와 외부 공개가 불필요한 접속 정보&lt;/li&gt;
  &lt;li&gt;고객사명과 내부 시스템 식별자&lt;/li&gt;
  &lt;li&gt;현재 연봉, 개인정보 등 개발 workflow와 무관한 민감정보&lt;/li&gt;
  &lt;li&gt;프로젝트의 수시로 변하는 현재 commit/status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Custom Instructions는 계정 전반의 많은 대화에 영향을 줄 수 있는 위치이므로 &lt;strong&gt;세부 상태 저장소가 아니라 bootstrap contract&lt;/strong&gt;로 취급하는 것이 적절하다.&lt;/p&gt;

&lt;p&gt;또한 Custom Instructions가 있다고 해서 GitHub 접근 권한 자체가 생기는 것은 아니다. private repository를 확인해야 하는 작업에서는 해당 ChatGPT 환경이 GitHub에 접근할 수 있어야 한다.&lt;/p&gt;

&lt;p&gt;GitHub Copilot의 공식 instruction 파일이 존재한다는 사실도 일반 ChatGPT가 그 파일을 자동으로 읽는다는 의미는 아니다. 제품과 실행 표면별 공식 지원 범위를 분리해서 판단해야 한다.&lt;/p&gt;

&lt;h2 id=&quot;포트폴리오-관점의-의미&quot;&gt;포트폴리오 관점의 의미&lt;/h2&gt;

&lt;p&gt;이 작업은 단순한 ChatGPT 설정 편의 기능이 아니다.&lt;/p&gt;

&lt;p&gt;여러 AI와 여러 실행 환경이 있는 개발 workflow에서 중요한 것은 프롬프트를 길게 작성하는 것이 아니라 다음을 명확히 만드는 것이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;정책의 단일 Source of Truth&lt;/li&gt;
  &lt;li&gt;프로젝트별 상태의 소유권&lt;/li&gt;
  &lt;li&gt;Agent가 작업 시작 시 context를 복원하는 bootstrap 절차&lt;/li&gt;
  &lt;li&gt;실행 환경별 안전 경계&lt;/li&gt;
  &lt;li&gt;실제 test/runtime evidence에 기반한 완료 판정&lt;/li&gt;
  &lt;li&gt;사람이 반복해서 설명해야 하는 관리 포인트의 축소&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;최종 구조는 다음과 같다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ChatGPT Custom Instructions
          |
          v
GitHub Global Control
          |
          +-------&amp;gt; Project AGENTS.md
          |              |
          |              v
          |        Project Source of Truth
          |
          +-------&amp;gt; device-control
                         |
             +-----------+-----------+
             |                       |
             v                       v
      Ubuntu/Codex              NAS/Codex
      global AGENTS             global AGENTS
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;GitHub Copilot을 추가한다면 공식적으로 지원하는 user/repository/path/agent instruction 계층을 같은 Source-of-Truth 원칙에 맞춰 연결할 수 있다.&lt;/p&gt;

&lt;p&gt;결과적으로 일반 ChatGPT 새 채팅, GitHub repository에서 시작한 Agent, Ubuntu 개발 서버의 Codex, NAS에서 격리 실행되는 Codex가 모두 &lt;strong&gt;같은 중앙 운영 기준을 발견하되 실제 프로젝트 상태는 각자의 Source of Truth에서 읽는 구조&lt;/strong&gt;가 된다.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>AI와 운영 문제를 해결할 때 공식 문서를 기본값으로 사용하는 방법</title>
   <link href="https://son1004007.github.io/infrastructure/2026/08/23/official-source-first-ai-troubleshooting/"/>
   <updated>2026-08-23T00:00:00+00:00</updated>
   <id>https://son1004007.github.io/infrastructure/2026/08/23/official-source-first-ai-troubleshooting</id>
   <content type="html">&lt;p&gt;AI에게 익숙하지 않은 도구의 설치나 운영 문제를 맡기면 가장 위험한 순간은 “대충 아는 내용”을 확정적인 사실처럼 사용하는 때다. 특히 CLI 옵션, 보안 샌드박스, 인증, 컨테이너 런타임처럼 버전과 OS 정책에 따라 동작이 달라지는 영역은 기억이나 검색 결과만으로 변경하면 불필요한 우회 설정을 만들기 쉽다.&lt;/p&gt;

&lt;p&gt;최근 Linux 환경의 AI CLI 샌드박스 문제를 다루면서 이 원칙을 다시 확인했다. 처음에는 컨테이너 런타임을 별도로 도입하는 방향도 검토했지만, 설치된 버전과 Ubuntu의 AppArmor 정책, Bubblewrap 동작을 공식 자료와 실제 런타임으로 다시 확인하자 더 단순한 해결 경로가 있었다.&lt;/p&gt;

&lt;p&gt;이 경험을 계기로 AI-assisted development/operations의 기본 규칙을 다음처럼 정리했다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;모르는 부분, 기억이 불확실한 부분, 버전에 따라 달라질 가능성이 있는 부분은 추측하지 않고 공식 문서를 먼저 확인한다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;문제점&quot;&gt;문제점&lt;/h2&gt;

&lt;p&gt;AI는 일반적인 기술 패턴을 빠르게 제안할 수 있지만 다음 상황에서는 오래된 지식이나 다른 버전의 동작을 섞을 가능성이 있다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;CLI 문법이나 옵션이 버전별로 바뀐 경우&lt;/li&gt;
  &lt;li&gt;Ubuntu, RHEL, Synology DSM처럼 OS 보안 정책이 다른 경우&lt;/li&gt;
  &lt;li&gt;Docker, Bubblewrap, AppArmor, seccomp처럼 여러 보안 계층이 겹치는 경우&lt;/li&gt;
  &lt;li&gt;OAuth, API Key, Device Login 등 인증 흐름이 바뀐 경우&lt;/li&gt;
  &lt;li&gt;공식 지원 방식과 커뮤니티 우회 방법이 함께 검색되는 경우&lt;/li&gt;
  &lt;li&gt;에러 메시지만 보고 원인을 단정하기 어려운 경우&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이때 바로 설정을 변경하면 원래 필요하지 않았던 Docker 설치, 전역 보안 정책 완화, 위험한 bypass 옵션 같은 부작용이 생길 수 있다.&lt;/p&gt;

&lt;h2 id=&quot;원인&quot;&gt;원인&lt;/h2&gt;

&lt;p&gt;가장 큰 원인은 “지식”과 “현재 환경의 사실”을 구분하지 않는 것이다.&lt;/p&gt;

&lt;p&gt;예를 들어 어떤 도구가 과거에는 다음과 같은 명령을 사용했다고 하더라도:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;some-cli sandbox linux /bin/true
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;현재 설치된 버전에서는 다음처럼 문법이 바뀌었을 수 있다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;some-cli sandbox &lt;span class=&quot;nt&quot;&gt;--&lt;/span&gt; /bin/true
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 차이는 기억으로 해결할 문제가 아니다. 설치된 바이너리의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--version&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--help&lt;/code&gt;와 현재 공식 문서를 확인해야 한다.&lt;/p&gt;

&lt;p&gt;운영 환경에서는 세 종류의 근거를 분리해야 한다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;현재 환경의 사실&lt;/strong&gt;: 실제 서버의 버전, 설정, 에러, exit code&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;제품의 지원 방식&lt;/strong&gt;: 공식 문서, 공식 release note, 공식 source repository&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;해결 가능성&lt;/strong&gt;: 최소 변경의 read-only probe 또는 재현 테스트&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;해결&quot;&gt;해결&lt;/h2&gt;

&lt;p&gt;나는 이 절차를 &lt;strong&gt;Official-Source-First&lt;/strong&gt; 규칙으로 사용한다.&lt;/p&gt;

&lt;h3 id=&quot;1-먼저-현재-환경을-관측한다&quot;&gt;1. 먼저 현재 환경을 관측한다&lt;/h3&gt;

&lt;p&gt;변경 전에 최소한 다음을 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;tool &lt;span class=&quot;nt&quot;&gt;--version&lt;/span&gt;
tool &lt;span class=&quot;nt&quot;&gt;--help&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;uname&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;필요하면 OS/package 상태도 읽기 전용으로 확인한다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;dpkg-query &lt;span class=&quot;nt&quot;&gt;-W&lt;/span&gt; &amp;lt;package&amp;gt;
sysctl &amp;lt;relevant-key&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;중요한 점은 아직 설정을 변경하지 않는 것이다.&lt;/p&gt;

&lt;h3 id=&quot;2-불확실하면-공식-문서를-기본-검색-대상으로-한다&quot;&gt;2. 불확실하면 공식 문서를 기본 검색 대상으로 한다&lt;/h3&gt;

&lt;p&gt;검색 우선순위는 다음과 같이 둔다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;1. 현재 설치된 바이너리의 --version / --help
2. 제품의 최신 공식 문서
3. 공식 release note / changelog
4. 공식 source repository의 문서와 issue
5. 재현 가능한 read-only runtime probe
6. Stack Overflow, Reddit, 블로그 등 커뮤니티 자료
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;커뮤니티 자료는 문제 발견에는 유용하지만 최종 운영 설정의 근거로 바로 사용하지 않는다.&lt;/p&gt;

&lt;h3 id=&quot;3-버전이-맞는지-확인한다&quot;&gt;3. 버전이 맞는지 확인한다&lt;/h3&gt;

&lt;p&gt;공식 문서가 최신 버전을 설명하고 서버에는 이전 버전이 설치되어 있을 수 있다.&lt;/p&gt;

&lt;p&gt;따라서 다음 질문을 항상 확인한다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;이 문서는 현재 설치 버전에 적용되는가?&lt;/li&gt;
  &lt;li&gt;옵션이 현재 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--help&lt;/code&gt;에 실제로 존재하는가?&lt;/li&gt;
  &lt;li&gt;release note에서 변경된 동작은 없는가?&lt;/li&gt;
  &lt;li&gt;OS 버전에 따라 추가 정책이 필요한가?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;공식 문서와 설치된 바이너리가 충돌하면 &lt;strong&gt;현재 설치된 바이너리의 실제 동작을 우선 관측 사실로 기록하고&lt;/strong&gt;, 왜 차이가 생겼는지 release note나 source를 추가 확인한다.&lt;/p&gt;

&lt;h3 id=&quot;4-가장-작은-변경으로-검증한다&quot;&gt;4. 가장 작은 변경으로 검증한다&lt;/h3&gt;

&lt;p&gt;바로 운영 구성을 크게 바꾸지 않는다.&lt;/p&gt;

&lt;p&gt;예를 들어 sandbox 문제가 발생했다면:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;나쁜 순서
에러 발생
 -&amp;gt; 전역 보안 설정 비활성화
 -&amp;gt; Docker 설치
 -&amp;gt; 전체 구조 변경

좋은 순서
에러 발생
 -&amp;gt; 버전/도움말 확인
 -&amp;gt; 공식 문서 확인
 -&amp;gt; 필요한 정책 한 개만 변경
 -&amp;gt; 최소 명령으로 exit code 확인
 -&amp;gt; 실제 read-only E2E 확인
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;5-성공-조건을-수치-또는-상태로-남긴다&quot;&gt;5. 성공 조건을 수치 또는 상태로 남긴다&lt;/h3&gt;

&lt;p&gt;“되는 것 같다”가 아니라 다음처럼 남긴다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;probe exit code = 0
read-only violation = no
workspace cleanup = yes
CI = success
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 기록이 있어야 이후 AI가 같은 문제를 다시 추측하지 않는다.&lt;/p&gt;

&lt;h2 id=&quot;실행-방법&quot;&gt;실행 방법&lt;/h2&gt;

&lt;p&gt;AI에게 기술 작업을 시킬 때 프롬프트나 저장소의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt;에 다음 규칙을 넣어 두면 효과적이다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;When a behavior is unfamiliar, version-sensitive, security-sensitive,
or not directly verified:

1. Do not guess.
2. Inspect the installed version/help/runtime state first.
3. Consult current official vendor documentation.
4. Check official release notes/source issues when docs and runtime differ.
5. Prefer the smallest reversible/read-only probe before configuration changes.
6. Do not weaken global security settings merely to make a tool work.
7. Record the exact evidence used for the decision.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;한국어로는 다음 정도면 충분하다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;잘 모르는 동작, 버전 의존 동작, 보안/인증/런타임 설정은 추측하지 않는다.
현재 버전과 실제 상태를 먼저 확인하고 공식 문서를 조회한다.
공식 문서와 실제 바이너리가 다르면 release note/source를 추가 확인한다.
가능하면 read-only 최소 재현 후에만 설정을 변경한다.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;검증-방법&quot;&gt;검증 방법&lt;/h2&gt;

&lt;p&gt;이 규칙이 실제로 동작하려면 문서만 작성해서는 부족하다.&lt;/p&gt;

&lt;p&gt;저장소 수준에서는 다음을 같이 적용하는 것이 좋다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt;에 Official-Source-First 규칙 추가&lt;/li&gt;
  &lt;li&gt;중요한 런타임 결정은 별도 문서에 근거 URL과 검증 결과 기록&lt;/li&gt;
  &lt;li&gt;CI에서 제거된 과거 정책 문구가 다시 들어오는지 검사&lt;/li&gt;
  &lt;li&gt;실제 서버 작업은 read-only/status operation부터 수행&lt;/li&gt;
  &lt;li&gt;변경 후 E2E 증적을 Issue 또는 Actions log에 남김&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;즉 정책, 코드, 런타임 증거가 서로 일치해야 한다.&lt;/p&gt;

&lt;h2 id=&quot;재발-방지--개선-방향&quot;&gt;재발 방지 / 개선 방향&lt;/h2&gt;

&lt;p&gt;AI 작업에서 특히 다음 단어가 등장하면 공식 문서 확인을 자동 트리거로 보는 편이 안전하다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;모르겠다
아마
예전에는
버전에 따라
permission denied
operation not permitted
unsupported
unknown option
authentication
sandbox
AppArmor
seccomp
Docker
OAuth
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;또한 운영 서버에서 다음 변경은 공식 근거 없이 바로 진행하지 않는다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;kernel/sysctl 보안 정책 완화&lt;/li&gt;
  &lt;li&gt;AppArmor/SELinux 전체 비활성화&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--dangerously-*&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--privileged&lt;/code&gt; 같은 bypass&lt;/li&gt;
  &lt;li&gt;Docker socket 노출&lt;/li&gt;
  &lt;li&gt;root 권한 확대&lt;/li&gt;
  &lt;li&gt;인증 파일 전체 공유&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;가능하면 항상 &lt;strong&gt;국소적인 허용 &amp;gt; 전역 보안 완화&lt;/strong&gt; 순서로 판단한다.&lt;/p&gt;

&lt;h2 id=&quot;포트폴리오-관점의-의미&quot;&gt;포트폴리오 관점의 의미&lt;/h2&gt;

&lt;p&gt;이 원칙은 단순히 “검색을 잘한다”는 의미가 아니다.&lt;/p&gt;

&lt;p&gt;운영과 개발에서 중요한 것은 다음 능력이다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;현재 런타임과 문서의 차이를 구분하는 능력&lt;/li&gt;
  &lt;li&gt;보안 설정을 불필요하게 약화하지 않는 판단&lt;/li&gt;
  &lt;li&gt;최소 변경으로 원인을 검증하는 troubleshooting 방식&lt;/li&gt;
  &lt;li&gt;AI의 제안을 그대로 실행하지 않고 근거를 확인하는 검증 습관&lt;/li&gt;
  &lt;li&gt;변경 후 재현 가능한 증적을 남기는 운영 방식&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI 도구를 많이 사용하는 환경일수록 중요한 것은 더 많은 명령을 빠르게 실행하는 것이 아니라, &lt;strong&gt;불확실한 순간에 추측을 멈추고 가장 신뢰할 수 있는 근거로 전환하는 것&lt;/strong&gt;이다.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>여러 프로젝트와 서버에서 Codex 운영 기준을 자동으로 이어받게 만든 방법</title>
   <link href="https://son1004007.github.io/infrastructure/2026/08/23/global-codex-agents-control-plane/"/>
   <updated>2026-08-23T00:00:00+00:00</updated>
   <id>https://son1004007.github.io/infrastructure/2026/08/23/global-codex-agents-control-plane</id>
   <content type="html">&lt;p&gt;AI를 여러 프로젝트와 여러 실행 환경에서 사용하다 보면 코드 작성보다 더 귀찮은 문제가 생긴다.&lt;/p&gt;

&lt;p&gt;새 채팅이나 새 Codex 세션을 시작할 때마다 다음 내용을 다시 설명해야 하는 문제다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;어떤 저장소가 기준 저장소인지&lt;/li&gt;
  &lt;li&gt;무엇을 먼저 읽어야 하는지&lt;/li&gt;
  &lt;li&gt;모르는 기술 동작은 공식 문서부터 확인해야 한다는 규칙&lt;/li&gt;
  &lt;li&gt;서버 작업은 어디를 통해 실행해야 하는지&lt;/li&gt;
  &lt;li&gt;테스트와 실행 증거 없이 완료라고 하면 안 된다는 규칙&lt;/li&gt;
  &lt;li&gt;프로젝트 상태는 중앙 문서가 아니라 각 프로젝트 저장소에서 확인해야 한다는 원칙&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;프로젝트가 몇 개 없을 때는 반복 설명으로 버틸 수 있지만 저장소와 실행 환경이 늘어나면 같은 설명을 여러 곳에 복사하게 된다. 그러면 결국 문서끼리 상태가 달라진다.&lt;/p&gt;

&lt;p&gt;이번에는 이 문제를 &lt;strong&gt;중앙 Control + 프로젝트별 AGENTS.md + 실행 환경의 전역 AGENTS.md&lt;/strong&gt; 구조로 정리했다.&lt;/p&gt;

&lt;h2 id=&quot;목표&quot;&gt;목표&lt;/h2&gt;

&lt;p&gt;사용자가 매번 긴 운영 프롬프트를 쓰지 않아도 다음 정도의 요청으로 작업을 이어갈 수 있게 하는 것이 목표였다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;이 프로젝트 계속 진행해줘.
기존 기준으로 진행해줘.
서버에서 검증까지 해줘.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Agent는 이전 대화를 요구하기 전에 GitHub 저장소와 실행 증거를 이용해 현재 상태를 복원해야 한다.&lt;/p&gt;

&lt;h2 id=&quot;중앙-저장소에는-상태가-아니라-찾는-방법만-둔다&quot;&gt;중앙 저장소에는 상태가 아니라 ‘찾는 방법’만 둔다&lt;/h2&gt;

&lt;p&gt;중앙 Control 저장소에 모든 프로젝트의 현재 상태를 복사하면 관리 포인트만 늘어난다.&lt;/p&gt;

&lt;p&gt;그래서 중앙에는 다음만 둔다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;CONTROL.md
  - 공통 작업 시작 절차
  - Source of Truth 규칙
  - Official-Source-First
  - 검증/완료 판정 원칙

repository registry
  - 저장소 역할
  - 먼저 읽을 파일
  - 필요한 경우 실행 환경 매핑
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;실제 프로젝트 상태는 계속 각 저장소가 소유한다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;중앙 Control
  -&amp;gt; 어떤 저장소와 문서를 읽을지 결정

각 프로젝트 저장소
  -&amp;gt; 코드
  -&amp;gt; AGENTS.md
  -&amp;gt; AI_CONTEXT / CURRENT_STATE / WORKS / TASKS
  -&amp;gt; 테스트
  -&amp;gt; 배포/복구 절차
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이렇게 하면 중앙 저장소가 stale 상태를 복제하지 않는다.&lt;/p&gt;

&lt;h2 id=&quot;프로젝트에서-중앙-control을-다시-발견하게-한다&quot;&gt;프로젝트에서 중앙 Control을 다시 발견하게 한다&lt;/h2&gt;

&lt;p&gt;중앙 저장소만 잘 만들어도 다른 프로젝트에서 직접 작업을 시작하면 중앙 규칙을 모를 수 있다.&lt;/p&gt;

&lt;p&gt;그래서 반복적으로 사용하는 프로젝트의 root &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt;에는 짧은 전역 포인터를 넣었다.&lt;/p&gt;

&lt;p&gt;개념적으로는 다음과 같다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Global control repository의 CONTROL.md를 먼저 확인한다.
그 후 현재 repository의 AGENTS.md와 현재 상태 문서로 돌아온다.
현재 repository가 자신의 코드와 상태에 대한 Source of Truth다.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;새 프로젝트에 사용하는 bootstrap template에도 같은 포인터를 넣었다.&lt;/p&gt;

&lt;p&gt;따라서 앞으로 새로 만든 프로젝트는 처음부터 같은 운영 계약을 갖는다.&lt;/p&gt;

&lt;p&gt;오래된 모든 저장소를 한 번에 수정하지는 않았다. 오래된 프로젝트를 다시 활성화할 때 전역 포인터와 registry를 추가하는 &lt;strong&gt;on-first-touch&lt;/strong&gt; 방식으로 관리한다.&lt;/p&gt;

&lt;h2 id=&quot;codex-실행-환경에도-전역-agentsmd를-둔다&quot;&gt;Codex 실행 환경에도 전역 AGENTS.md를 둔다&lt;/h2&gt;

&lt;p&gt;Repository-level &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt;만으로도 대부분의 작업은 해결되지만 서버에서 어떤 repository를 열더라도 동일한 기본 규칙을 먼저 읽게 하고 싶었다.&lt;/p&gt;

&lt;p&gt;OpenAI가 설명하는 Codex instruction 구성에서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$CODEX_HOME&lt;/code&gt;의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt; 또는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.override.md&lt;/code&gt;를 사용자 지침으로 읽고, 이어서 Git/project root에서 현재 작업 디렉터리까지의 프로젝트 지침을 추가한다.&lt;/p&gt;

&lt;p&gt;관련 공식 설명:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://openai.com/index/unrolling-the-codex-agent-loop/&quot;&gt;Unrolling the Codex agent loop&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://openai.com/index/introducing-codex/&quot;&gt;Introducing Codex&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;그래서 Linux 개발 서버와 Synology NAS의 Codex 환경 모두에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$CODEX_HOME/AGENTS.md&lt;/code&gt; managed block을 설치했다.&lt;/p&gt;

&lt;p&gt;전역 파일은 프로젝트 상태를 포함하지 않는다. 다음 원칙만 갖는다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;1. GitHub 접근이 가능하면 중앙 CONTROL.md를 먼저 확인
2. 다시 현재 repository의 local AGENTS.md로 돌아오기
3. 프로젝트 상태와 코드는 현재 repository를 Source of Truth로 사용
4. 불확실한 기술 동작은 Official-Source-First 적용
5. repository와 runtime evidence에서 복원 가능한 내용을 사용자에게 반복 질문하지 않기
6. sandbox/auth/security 정책을 문제 해결 목적으로 함부로 완화하지 않기
7. 중앙 Control에 접근할 수 없으면 local 규칙으로 fail-safe하게 동작
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;기존 사용자가 만들어 둔 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$CODEX_HOME/AGENTS.md&lt;/code&gt;가 있을 수도 있기 때문에 파일 전체를 덮어쓰지 않았다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;기존 사용자 지침

&amp;lt;!-- managed block start --&amp;gt;
Global control bootstrap
&amp;lt;!-- managed block end --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;처럼 관리 구간만 추가하거나 교체하도록 구현했다.&lt;/p&gt;

&lt;h2 id=&quot;두-실행-환경의-격리-방식은-그대로-유지했다&quot;&gt;두 실행 환경의 격리 방식은 그대로 유지했다&lt;/h2&gt;

&lt;p&gt;전역 지침을 추가했다고 해서 실행 격리까지 하나로 통일하지 않았다.&lt;/p&gt;

&lt;p&gt;Ubuntu 개발 환경은 이미 검증된 구조를 유지한다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;clean canonical checkout
 -&amp;gt; detached disposable git worktree
 -&amp;gt; Codex native Bubblewrap read-only sandbox
 -&amp;gt; 변경 여부 검사
 -&amp;gt; worktree remove/prune
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;NAS에서는 OS 특성상 Codex native Linux sandbox를 억지로 맞추지 않고 기존 외부 컨테이너 경계를 유지한다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;persistent application runtime
 -&amp;gt; disposable snapshot
 -&amp;gt; read-only Docker/Container Manager boundary
 -&amp;gt; temporary Codex HOME
 -&amp;gt; Codex
 -&amp;gt; snapshot 변경 여부 검사
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;NAS runner는 호스트의 Codex 인증/사용자 디렉터리를 임시 컨테이너 HOME에 복사하므로, 호스트 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$CODEX_HOME/AGENTS.md&lt;/code&gt;의 managed global instruction도 컨테이너 실행에 전달된다.&lt;/p&gt;

&lt;p&gt;핵심은 &lt;strong&gt;공통 정책은 공유하되 각 환경에서 검증된 격리 방법은 유지하는 것&lt;/strong&gt;이다.&lt;/p&gt;

&lt;h2 id=&quot;파일이-존재하는지만-확인하지-않았다&quot;&gt;파일이 존재하는지만 확인하지 않았다&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt; 파일을 생성했다고 해서 Codex가 실제로 읽었다고 단정하면 안 된다.&lt;/p&gt;

&lt;p&gt;그래서 managed block에 검증용 marker를 하나 넣었다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;DEVICE_GLOBAL_CONTROL_MARKER=global-control-v1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;그 다음 실제 read-only Codex task에는 다음 조건을 줬다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;AGENTS.md와 CODEX_HOME 파일을 직접 열거나 검색하지 않는다.
이미 초기 context에 로드된 instruction만 이용한다.
DEVICE_GLOBAL_CONTROL_MARKER 값을 보고한다.
파일은 수정하지 않는다.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;결과적으로 두 환경 모두 Codex가 다음 값을 반환했다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;DEVICE_GLOBAL_CONTROL_MARKER=global-control-v1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Ubuntu 개발 환경에서는 추가로 다음 조건도 확인됐다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;read_only_violation=no
worktree_cleanup=yes
agent_exit_code=0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;NAS 외부 컨테이너 환경에서도 다음 조건을 확인했다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;read_only_violation=no
agent_exit_code=0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;즉 &lt;strong&gt;파일 배치 확인이 아니라 실제 Codex context ingestion까지 E2E로 검증&lt;/strong&gt;했다.&lt;/p&gt;

&lt;h2 id=&quot;검증-중-stale-checkout도-발견했다&quot;&gt;검증 중 stale checkout도 발견했다&lt;/h2&gt;

&lt;p&gt;첫 Ubuntu E2E는 Codex까지 도달하지 못했다.&lt;/p&gt;

&lt;p&gt;원인은 sandbox가 아니라 canonical checkout이 GitHub main보다 한 commit 뒤에 있었기 때문이다.&lt;/p&gt;

&lt;p&gt;preflight는 이 상태에서 작업을 계속하지 않고 실패시켰다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;local commit != GitHub main commit
 -&amp;gt; agent 실행 차단
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;canonical checkout을 fast-forward로 동기화하고 clean 상태를 다시 확인한 뒤 E2E를 재실행했고 통과했다.&lt;/p&gt;

&lt;p&gt;이 과정은 오히려 현재 구조가 의도대로 동작한다는 증거였다. 전역 정책을 읽게 만드는 것보다 중요한 것은 &lt;strong&gt;잘못된 source 상태에서 Agent가 실행되지 않는 것&lt;/strong&gt;이기 때문이다.&lt;/p&gt;

&lt;h2 id=&quot;최종-구조&quot;&gt;최종 구조&lt;/h2&gt;

&lt;p&gt;현재 구조를 단순화하면 다음과 같다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Central AI workflow/control
        |
        +-&amp;gt; repository registry
        +-&amp;gt; Official-Source-First
        +-&amp;gt; bootstrap template
        |
        v
Project root AGENTS.md
        |
        +-&amp;gt; project state / code / tests
        |
        v
Runtime $CODEX_HOME/AGENTS.md
        |
        v
Codex
        |
        +-&amp;gt; Ubuntu: disposable worktree + bwrap
        |
        +-&amp;gt; NAS: disposable snapshot + external container isolation
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;실제 적용 시 Codex는 전역 지침과 더 구체적인 repository-local 지침을 함께 받는다.&lt;/p&gt;

&lt;p&gt;중앙 Control은 프로젝트 상태를 대신하지 않고 &lt;strong&gt;어디서 현재 사실을 찾아야 하는지를 알려주는 index&lt;/strong&gt;로만 유지한다.&lt;/p&gt;

&lt;h2 id=&quot;운영-원칙&quot;&gt;운영 원칙&lt;/h2&gt;

&lt;p&gt;이 구조를 유지하기 위한 규칙은 몇 가지로 제한했다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;중앙 Control에 프로젝트의 mutable 상태를 복사하지 않는다.&lt;/li&gt;
  &lt;li&gt;프로젝트별 코드는 해당 repository가 Source of Truth다.&lt;/li&gt;
  &lt;li&gt;새 프로젝트 template에는 global control pointer를 자동 포함한다.&lt;/li&gt;
  &lt;li&gt;기존 프로젝트는 다시 작업할 때 on-first-touch 방식으로 연결한다.&lt;/li&gt;
  &lt;li&gt;실행 환경의 global AGENTS는 managed block으로 갱신한다.&lt;/li&gt;
  &lt;li&gt;전역 지침 적용 여부도 실제 Agent E2E로 검증한다.&lt;/li&gt;
  &lt;li&gt;Global Control 접근 실패가 보안 정책 완화의 이유가 되어서는 안 된다.&lt;/li&gt;
  &lt;li&gt;모르는 기술 동작은 &lt;a href=&quot;/infrastructure/2026/08/23/official-source-first-ai-troubleshooting/&quot;&gt;Official-Source-First&lt;/a&gt; 원칙을 따른다.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;결과&quot;&gt;결과&lt;/h2&gt;

&lt;p&gt;이전에는 새 채팅이나 다른 Agent에서 프로젝트를 이어가려면 운영 규칙을 다시 설명해야 했다.&lt;/p&gt;

&lt;p&gt;지금은 다음 흐름을 목표로 한다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;현재 repository 확인
 -&amp;gt; local AGENTS.md에서 global control 발견
 -&amp;gt; 중앙 Control에서 읽을 위치 결정
 -&amp;gt; 현재 repository의 최신 상태 복원
 -&amp;gt; 필요할 때만 remote runtime 확인
 -&amp;gt; 구현
 -&amp;gt; 검증
 -&amp;gt; 상태 문서 갱신
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;완전히 모든 AI 환경에서 자동 상속되는 단일 GitHub 기능이 있는 것은 아니다. 따라서 repository pointer와 runtime global instruction을 함께 사용했다.&lt;/p&gt;

&lt;p&gt;결과적으로 &lt;strong&gt;GitHub에서 시작해도, Ubuntu 개발 서버의 Codex에서 시작해도, NAS의 격리된 Codex에서 시작해도 동일한 기본 운영 철학을 발견할 수 있는 구조&lt;/strong&gt;가 됐다.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>GitHub를 Control Plane으로 사용해 ChatGPT·Codex 개발과 배포를 반자동화하는 방법</title>
   <link href="https://son1004007.github.io/infrastructure/2026/08/20/github-control-plane-ai-semi-automation/"/>
   <updated>2026-08-20T00:00:00+00:00</updated>
   <id>https://son1004007.github.io/infrastructure/2026/08/20/github-control-plane-ai-semi-automation</id>
   <content type="html">&lt;h2 id=&quot;배경&quot;&gt;배경&lt;/h2&gt;

&lt;p&gt;ChatGPT나 Codex를 이용해 실제 프로그램을 수정하다 보면 대화 안에서 코드를 만드는 것보다 더 어려운 문제가 생긴다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;AI가 바뀌면 이전 작업 방법을 다시 설명해야 한다.&lt;/li&gt;
  &lt;li&gt;모바일에서 ChatGPT를 사용하고 실제 실행 환경은 사내 Linux 서버에 있을 수 있다.&lt;/li&gt;
  &lt;li&gt;AI가 서버에 직접 접속할 수 없는 경우가 있다.&lt;/li&gt;
  &lt;li&gt;코드는 수정됐지만 실제 배포가 되었는지 확인하기 어렵다.&lt;/li&gt;
  &lt;li&gt;AI가 “정상 동작한다”고 말해도 실환경에서 확인되지 않았을 수 있다.&lt;/li&gt;
  &lt;li&gt;고객사가 바뀔 때마다 SSH, CI/CD, 테스트 방법을 다시 설계하게 된다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이 문제를 해결하기 위해 AI를 실행 플랫폼으로 사용하지 않고 &lt;strong&gt;GitHub를 공통 Control Plane으로 사용하는 구조&lt;/strong&gt;를 사용한다.&lt;/p&gt;

&lt;p&gt;이 글의 목표는 특정 AI 제품에 종속된 자동화가 아니다. 이 글 자체를 ChatGPT, Codex, Claude, Gemini 등 repository를 다룰 수 있는 AI에게 전달했을 때 비슷한 프로젝트 운영 구조를 다시 만들 수 있도록 하는 것이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;한-장으로-보는-전체-구조&quot;&gt;한 장으로 보는 전체 구조&lt;/h1&gt;

&lt;p&gt;처음 보면 GitHub, AI, Runner, 서버의 역할이 섞여 보일 수 있다. 가장 먼저 아래 구조만 이해하면 된다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;┌──────────────────────────────────────────────────────────────┐
│                           사용자                             │
│        목표 전달 · 우선순위 결정 · 운영 반영 승인           │
└──────────────────────────────┬───────────────────────────────┘
                               │
                               ▼
┌──────────────────────────────────────────────────────────────┐
│                    ChatGPT / Codex / Other AI                │
│                                                              │
│  요구사항 정리 → 코드 조사 → 수정 → GitHub 반영             │
│  실패 로그 분석 → 수정 → 다시 GitHub 반영                   │
└──────────────────────────────┬───────────────────────────────┘
                               │
                               ▼
┌──────────────────────────────────────────────────────────────┐
│                    GitHub = Control Plane                    │
│                                                              │
│  AGENTS.md        AI가 따라야 할 규칙                        │
│  agent-ops.yaml   배포·검증 계약                             │
│  Issue            작업 요청 / 현재 상태 포인터              │
│  Branch / PR      변경 격리 / 검토                           │
│  Actions          실제 작업 실행 Trigger                     │
│  Secrets          인증정보 보관                              │
└──────────────────────────────┬───────────────────────────────┘
                               │ workflow 실행
                               ▼
┌──────────────────────────────────────────────────────────────┐
│                GitHub Actions / Jenkins / Runner             │
│                                                              │
│  Build → Test → Docker Build → Deploy → Runtime Verify       │
└──────────────────────────────┬───────────────────────────────┘
                               │
                               ▼
┌──────────────────────────────────────────────────────────────┐
│                         실행 환경                            │
│                                                              │
│      Preview Server / Linux / NAS / Cloud / Customer VM      │
│                       Application + DB                       │
└──────────────────────────────┬───────────────────────────────┘
                               │
                               │ health / API / DB / log
                               ▼
┌──────────────────────────────────────────────────────────────┐
│                     검증 결과를 GitHub로                     │
│                                                              │
│ BUILD=PASS · TEST=PASS · DEPLOY=PASS · HEALTH=PASS           │
│ commit SHA · workflow run ID · runtime state                 │
└──────────────────────────────┬───────────────────────────────┘
                               │
                               └──────────────► AI가 다시 읽음
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;핵심은 다음과 같다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;AI가 서버에 직접 명령을 내리는 것이 아니라, AI가 GitHub의 상태를 변경하고 GitHub의 Runner가 실제 명령을 실행한 뒤 그 결과를 다시 GitHub에 남긴다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;실제로-한-번의-요청이-처리되는-순서&quot;&gt;실제로 한 번의 요청이 처리되는 순서&lt;/h1&gt;

&lt;p&gt;사용자가 모바일 ChatGPT에서 “이 기능 수정해서 테스트 서버에 반영해줘”라고 요청했다고 가정한다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[1] 사용자
    &quot;기능 A를 수정하고 Preview에서 확인해줘&quot;
             │
             ▼
[2] ChatGPT / Codex
    repository 조사
    AGENTS.md 확인
    agent-ops.yaml 확인
             │
             ▼
[3] AI 작업 Branch
    feature/function-a
    코드 + 테스트 수정
             │
             ▼
[4] GitHub Push
    commit SHA 생성
             │
             ▼
[5] GitHub Actions
    Build
      ↓
    Unit Test
      ↓
    Integration Test
      ↓
    Container Build
             │
             ▼
[6] Preview 배포
    운영과 분리된 Container / Port / DB
             │
             ▼
[7] 실환경 검증
    /health
    API smoke test
    DB row count
    container status
             │
             ▼
[8] GitHub에 결과 기록
    commit=abcdef1
    run=123456
    TEST=PASS
    DEPLOY_PREVIEW=PASS
    HEALTH=PASS
             │
             ▼
[9] AI가 결과 재확인
       ┌───────────────┐
       │   성공했나?   │
       └───────┬───────┘
           YES │ NO
               │  └────► 로그 분석 → 코드 수정 → 다시 [4]
               ▼
[10] 사용자 승인
     Production 반영 여부 결정
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;사용자가 직접 해야 하는 것은 점점 다음 세 가지에 가까워진다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;무엇을 만들 것인가
무엇이 완료인가
운영에 반영해도 되는가
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;실패했을-때-어떻게-자동으로-다시-수정하는가&quot;&gt;실패했을 때 어떻게 자동으로 다시 수정하는가&lt;/h1&gt;

&lt;p&gt;반자동화의 핵심은 첫 배포 성공이 아니라 &lt;strong&gt;실패 결과를 AI에게 다시 전달할 수 있다는 점&lt;/strong&gt;이다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;             ┌──────────────┐
             │ AI 코드 수정 │◄───────────────────────┐
             └──────┬───────┘                        │
                    │ push                            │
                    ▼                                 │
             ┌──────────────┐                        │
             │ GitHub Action│                        │
             └──────┬───────┘                        │
                    │                                 │
                    ▼                                 │
             ┌──────────────┐                        │
             │ Test / Deploy│                        │
             └──────┬───────┘                        │
                    │                                 │
             ┌──────▼───────┐                        │
             │ PASS / FAIL? │                        │
             └───┬──────┬───┘                        │
              PASS      FAIL                         │
               │          │                           │
               │          ▼                           │
               │   ┌──────────────┐                  │
               │   │ GitHub Log   │                  │
               │   │ 원인 / stack │                  │
               │   │ failed step  │                  │
               │   └──────┬───────┘                  │
               │          │                           │
               │          ▼                           │
               │   ┌──────────────┐                  │
               │   │ AI가 로그 읽음│──────────────────┘
               │   └──────────────┘       원인 반영 후 코드 수정
               │
               ▼
        ┌──────────────┐
        │ Preview 완료 │
        └──────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;즉 FAIL 경로는 다음처럼 반복된다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;FAIL
-&amp;gt; GitHub Log
-&amp;gt; AI가 로그 읽음
-&amp;gt; AI 코드 수정
-&amp;gt; push
-&amp;gt; GitHub Actions 재실행
-&amp;gt; PASS가 될 때까지 반복
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;따라서 AI에게 서버 Shell을 직접 제공하지 않아도 된다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;AI
-&amp;gt; GitHub를 수정
-&amp;gt; GitHub Runner가 실제 환경에서 실행
-&amp;gt; 실행 결과를 GitHub가 보관
-&amp;gt; AI가 GitHub 결과를 읽음
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 피드백 루프가 만들어지면 사용자가 매번 SSH 접속해서 명령을 복사하고 결과를 AI에게 다시 붙여넣는 작업을 크게 줄일 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;preview와-production을-왜-분리하는가&quot;&gt;Preview와 Production을 왜 분리하는가&lt;/h1&gt;

&lt;p&gt;AI가 수정한 코드를 바로 운영에 배포하면 반자동화가 아니라 위험한 자동화가 된다.&lt;/p&gt;

&lt;p&gt;권장 구조는 다음과 같다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;                        GitHub
                          │
               ┌──────────┴──────────┐
               │                     │
               ▼                     ▼
       feature/* branch           main branch
               │                     │
               ▼                     ▼
        Preview Workflow       Production Workflow
               │                     │
               ▼                     ▼
┌─────────────────────────┐   ┌─────────────────────────┐
│       PREVIEW           │   │       PRODUCTION        │
│                         │   │                         │
│ preview container       │   │ production container    │
│ preview port            │   │ production port         │
│ preview DB/schema       │   │ production DB           │
│ preview volume          │   │ production volume       │
└────────────┬────────────┘   └────────────┬────────────┘
             │                              ▲
             │ 자동 검증                    │
             ▼                              │ 사람 승인
      Build/Test/Health                     │
             │                              │
             └──────── PASS ────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;즉 기본 정책을 다음처럼 둔다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Preview
= AI가 반복적으로 수정·배포·검증 가능

Production
= 사람이 최종 승인
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이를 자동화 Level로 표현하면 일반적으로 다음 조합이 적절하다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Preview    = L3
Production = L4
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;github가-하는-일과-하지-않는-일&quot;&gt;GitHub가 하는 일과 하지 않는 일&lt;/h1&gt;

&lt;p&gt;GitHub를 Control Plane이라고 하면 GitHub가 모든 작업을 직접 처리한다고 오해할 수 있다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;GitHub가 하는 일
────────────────────────────────
작업 상태 저장
코드 변경 이력 저장
Workflow Trigger
Secret 전달
Runner 실행 요청
실행 Log 보관
검증 결과 보관

GitHub가 직접 하지 않는 일
────────────────────────────────
고객 업무 요구사항 판단
AI 추론
서버 애플리케이션 실행 자체
DB를 항상 직접 운영
운영 승인 판단
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;실제 명령을 실행하는 것은 Runner다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;GitHub
  │
  ├─ GitHub-hosted Runner
  ├─ Self-hosted Runner
  └─ Jenkins
          │
          ▼
       Target Server
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;github가-아니어도-가능한가-gitlab과-gitea&quot;&gt;GitHub가 아니어도 가능한가: GitLab과 Gitea&lt;/h1&gt;

&lt;p&gt;이 구조의 본질은 GitHub라는 제품 자체가 아니다. 필요한 것은 다음 기능을 제공하는 &lt;strong&gt;Git 기반 Control Plane&lt;/strong&gt;이다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Repository
+ Issue 또는 Work Item
+ Branch / PR 또는 MR
+ CI/CD Pipeline
+ Runner
+ Secret 관리
+ 실행 Log / Evidence
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;따라서 GitLab과 Gitea도 같은 구조를 만들 수 있다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;개념&lt;/th&gt;
      &lt;th&gt;GitHub&lt;/th&gt;
      &lt;th&gt;GitLab&lt;/th&gt;
      &lt;th&gt;Gitea&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;코드 저장소&lt;/td&gt;
      &lt;td&gt;Repository&lt;/td&gt;
      &lt;td&gt;Project/Repository&lt;/td&gt;
      &lt;td&gt;Repository&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;작업 단위&lt;/td&gt;
      &lt;td&gt;Issue&lt;/td&gt;
      &lt;td&gt;Issue / Work Item&lt;/td&gt;
      &lt;td&gt;Issue&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;변경 검토&lt;/td&gt;
      &lt;td&gt;Pull Request&lt;/td&gt;
      &lt;td&gt;Merge Request&lt;/td&gt;
      &lt;td&gt;Pull Request&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;CI/CD&lt;/td&gt;
      &lt;td&gt;GitHub Actions&lt;/td&gt;
      &lt;td&gt;GitLab CI/CD&lt;/td&gt;
      &lt;td&gt;Gitea Actions&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;실행기&lt;/td&gt;
      &lt;td&gt;GitHub Runner&lt;/td&gt;
      &lt;td&gt;GitLab Runner&lt;/td&gt;
      &lt;td&gt;Gitea Runner / act_runner&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;CI 설정&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.github/workflows/*.yml&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.gitlab-ci.yml&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.gitea/workflows/*.yml&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;상태 증거&lt;/td&gt;
      &lt;td&gt;Actions log / Issue&lt;/td&gt;
      &lt;td&gt;Pipeline/Job log / Issue&lt;/td&gt;
      &lt;td&gt;Actions log / Issue&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;추상화하면 다음처럼 볼 수 있다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;                 AI
                  │
                  ▼
        ┌─────────────────────┐
        │ Git Control Plane   │
        │                     │
        │ GitHub / GitLab /   │
        │ Gitea               │
        └──────────┬──────────┘
                   │
                   ▼
             CI/CD Runner
                   │
                   ▼
            Preview / Target
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;다만 &lt;strong&gt;AI가 Control Plane을 직접 읽고 수정하는 Adapter 수준은 제품마다 다르다.&lt;/strong&gt;&lt;/p&gt;

&lt;h3 id=&quot;github&quot;&gt;GitHub&lt;/h3&gt;

&lt;p&gt;ChatGPT에서 repository를 직접 연결해 코드와 문서를 읽을 수 있는 공식 통합이 있고, API/Connector를 사용할 수 있는 환경에서는 Issue, branch, 파일, workflow 결과를 같은 대화에서 다루기 쉽다.&lt;/p&gt;

&lt;p&gt;따라서 다음 형태의 &lt;strong&gt;ChatGPT 중심 모바일 반자동화&lt;/strong&gt;에는 현재 가장 단순하다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;모바일 ChatGPT
-&amp;gt; GitHub 수정
-&amp;gt; GitHub Actions
-&amp;gt; Preview
-&amp;gt; Actions Log
-&amp;gt; ChatGPT가 다시 확인
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;gitlab&quot;&gt;GitLab&lt;/h3&gt;

&lt;p&gt;GitLab 자체의 Control Plane 기능은 충분하다. GitLab CI/CD는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.gitlab-ci.yml&lt;/code&gt;의 job을 GitLab Runner가 실행하고, build/test/deploy 결과를 다시 GitLab에 기록할 수 있다. Merge Request와 Issue도 동일한 역할을 수행할 수 있다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;AI / Codex
-&amp;gt; GitLab branch
-&amp;gt; Merge Request
-&amp;gt; GitLab CI/CD
-&amp;gt; GitLab Runner
-&amp;gt; Preview
-&amp;gt; Pipeline / Job Log
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;특히 고객사가 이미 GitLab Self-Managed를 사용하거나 소스와 Runner를 고객망 내부에 유지해야 한다면 &lt;strong&gt;GitHub보다 GitLab이 더 적절할 수도 있다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;다만 ChatGPT 관점에서는 현재 GitHub repository 통합과 같은 수준으로 GitLab 코드·CI 전체를 직접 다루는 경로를 기본 전제로 두지 않는 편이 안전하다. ChatGPT에는 GitLab Issues 동기화 기능이 존재하지만, 이것만으로 repository 코드 수정과 pipeline 제어까지 동일하게 된다고 가정하지 않는다.&lt;/p&gt;

&lt;p&gt;따라서 ChatGPT가 orchestration을 담당해야 한다면 다음 중 하나가 추가로 필요할 수 있다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;GitLab API
glab CLI
MCP / Custom Plugin
별도 Agent Runner
Codex가 실행되는 고객사 내부 서버
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;gitea&quot;&gt;Gitea&lt;/h3&gt;

&lt;p&gt;Gitea도 Gitea Actions와 별도 Runner를 제공하므로 같은 구조를 만들 수 있다. Gitea Actions는 GitHub Actions와 유사하고 상당 부분 호환되므로 소규모 사내 구축이나 비용을 최소화한 self-hosted 환경에 유리하다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;AI / Codex
-&amp;gt; Gitea
-&amp;gt; .gitea/workflows
-&amp;gt; Gitea Runner
-&amp;gt; Preview
-&amp;gt; Actions Log
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;하지만 GitHub Actions와 완전히 동일하다고 가정하지 않는다. Event, context, 일부 Action 호환성에는 차이가 있을 수 있으므로 실제 workflow는 Gitea에서 검증해야 한다.&lt;/p&gt;

&lt;p&gt;또한 ChatGPT에서 Gitea를 직접 제어하는 공식 연결을 전제로 하지 않고 API/MCP/CLI 또는 별도 Agent Adapter가 필요하다고 보는 편이 안전하다.&lt;/p&gt;

&lt;h3 id=&quot;어떤-것을-선택할-것인가&quot;&gt;어떤 것을 선택할 것인가&lt;/h3&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ChatGPT에서 모바일로 직접 관리하는 편의성이 최우선
-&amp;gt; GitHub 우선

고객사가 이미 GitLab을 표준으로 사용
또는 소스/Runner를 고객망 내부에 유지해야 함
-&amp;gt; GitLab 우선

작은 사내망 / 개인 서버 / 완전 self-hosted / 비용 최소화
그리고 Adapter를 직접 구성할 수 있음
-&amp;gt; Gitea도 가능
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;즉 &lt;strong&gt;Repository Contract는 공통으로 유지하고 플랫폼별 Adapter만 바꾼다.&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;공통
AGENTS.md
agent-ops.yaml
Preview / Production 정책
검증 기준
승인 기준

플랫폼별
GitHub Actions  &amp;lt;-&amp;gt; GitLab CI/CD &amp;lt;-&amp;gt; Gitea Actions
GitHub API      &amp;lt;-&amp;gt; GitLab API   &amp;lt;-&amp;gt; Gitea API
PR              &amp;lt;-&amp;gt; MR           &amp;lt;-&amp;gt; PR
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이렇게 하면 향후 고객사에 따라 Git 플랫폼이 바뀌어도 자동화 원칙 자체를 다시 설계할 필요가 없다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;왜-github가-control-plane인가&quot;&gt;왜 GitHub가 Control Plane인가&lt;/h1&gt;

&lt;p&gt;이 글에서는 실제 적용 편의 때문에 GitHub를 기본 예제로 사용한다.&lt;/p&gt;

&lt;p&gt;AI 대화는 작업 상태를 보존하는 데 적합하지 않다. 새로운 대화를 시작하거나 다른 AI를 사용하면 앞선 판단과 실행 결과를 다시 설명해야 한다.&lt;/p&gt;

&lt;p&gt;반면 GitHub에는 다음이 남는다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;정보&lt;/th&gt;
      &lt;th&gt;GitHub에서의 위치&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;프로젝트 규칙&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;실행/배포 계약&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ops/agent-ops.yaml&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;요구사항&lt;/td&gt;
      &lt;td&gt;docs / Issue&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;변경 내용&lt;/td&gt;
      &lt;td&gt;Commit / Branch / PR&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;테스트&lt;/td&gt;
      &lt;td&gt;GitHub Actions&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;배포 결과&lt;/td&gt;
      &lt;td&gt;workflow log / deployment&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;현재 상태&lt;/td&gt;
      &lt;td&gt;Issue 또는 status document&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;새로운 AI가 들어오면 다음 순서로 상태를 복구한다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;새로운 AI
     │
     ▼
Repository
     │
     ├─ AGENTS.md
     ├─ agent-ops.yaml
     ├─ docs
     ├─ Current Issue / PR
     └─ Latest Workflow
     │
     ▼
현재 상태 복구
     │
     ▼
기존 방식으로 작업 계속
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;최소-repository-구조&quot;&gt;최소 Repository 구조&lt;/h1&gt;

&lt;p&gt;새 프로젝트에 아래 구조를 권장한다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;project/
  AGENTS.md

  ops/
    agent-ops.yaml

  docs/
    00-project-context.md
    01-requirements.md
    02-architecture.md
    03-test-plan.md
    04-operation-and-deployment.md
    06-ai-operations.md

  .github/
    workflows/
      ai-control-plane-verify.yml
      preview-deploy.yml
      production-deploy.yml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;프로젝트가 작다면 문서 수는 줄일 수 있다. 그러나 다음 세 개는 유지하는 편이 좋다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;AGENTS.md
ops/agent-ops.yaml
docs/04-operation-and-deployment.md
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;1-agentsmd-ai가-따라야-할-규칙&quot;&gt;1. AGENTS.md: AI가 따라야 할 규칙&lt;/h1&gt;

&lt;p&gt;AI마다 프롬프트를 다시 작성하지 않고 repository 자체에 규칙을 둔다.&lt;/p&gt;

&lt;p&gt;최소한 다음 내용을 포함한다.&lt;/p&gt;

&lt;div class=&quot;language-markdown highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;gh&quot;&gt;# AGENTS.md&lt;/span&gt;

&lt;span class=&quot;gu&quot;&gt;## Required reading&lt;/span&gt;

작업 전에 다음 순서로 확인한다.
&lt;span class=&quot;p&quot;&gt;
1.&lt;/span&gt; AGENTS.md
&lt;span class=&quot;p&quot;&gt;2.&lt;/span&gt; ops/agent-ops.yaml
&lt;span class=&quot;p&quot;&gt;3.&lt;/span&gt; docs/00-project-context.md
&lt;span class=&quot;p&quot;&gt;4.&lt;/span&gt; docs/04-operation-and-deployment.md
&lt;span class=&quot;p&quot;&gt;5.&lt;/span&gt; docs/06-ai-operations.md
&lt;span class=&quot;p&quot;&gt;6.&lt;/span&gt; 현재 Issue/PR
&lt;span class=&quot;p&quot;&gt;7.&lt;/span&gt; 관련 workflow와 실제 코드/테스트

&lt;span class=&quot;gu&quot;&gt;## Rules&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;
-&lt;/span&gt; 확인하지 않은 host, 계정, credential, 배포 경로를 추측하지 않는다.
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; main에 바로 개발하지 않는다.
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; 변경은 별도 branch에서 수행한다.
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; 테스트하지 않은 결과를 PASS라고 보고하지 않는다.
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; secret을 코드, 문서, workflow log에 출력하지 않는다.
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; Preview에서 먼저 검증한다.
&lt;span class=&quot;p&quot;&gt;-&lt;/span&gt; Production 배포와 운영 DB 변경은 사람 승인 후 실행한다.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Codex는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AGENTS.md&lt;/code&gt; 기반 작업 규칙과 잘 맞는다. 다른 AI도 첫 요청에서 이 파일을 읽도록 지정하면 같은 기준을 적용할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;2-agent-opsyaml-ai가-읽을-수-있는-운영-계약&quot;&gt;2. agent-ops.yaml: AI가 읽을 수 있는 운영 계약&lt;/h1&gt;

&lt;p&gt;자연어 문서만 두면 AI가 매번 운영 방식을 다시 해석해야 한다. 따라서 machine-readable 파일을 하나 둔다.&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;project&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;example-service&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;default_branch&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;main&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;control_plane&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;provider&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;github&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;work_item&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;issue&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;change_method&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;branch-pr&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;evidence_source&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;github-actions&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;branches&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;production&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;main&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;preview_patterns&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;feature/*&quot;&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;agent/*&quot;&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;workflow&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;verify&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;.github/workflows/ai-control-plane-verify.yml&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;preview_deploy&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;.github/workflows/preview-deploy.yml&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;production_deploy&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;.github/workflows/production-deploy.yml&quot;&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;runtime&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;preview&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;target_ref&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;customer-preview&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;isolation&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;separate_port&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;separate_container_or_namespace&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;separate_database_or_schema&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;separate_persistent_storage_when_needed&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;production&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;target_ref&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;customer-production&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;verification&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;required&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;health_endpoint&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;/health&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;success_markers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;BUILD=PASS&quot;&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;TEST=PASS&quot;&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;DEPLOY_PREVIEW=PASS&quot;&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;HEALTH=PASS&quot;&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;method&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;github-issue&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;issue_number&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;null&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;secrets&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;repository&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;github-actions-secrets&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;runtime&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;target-host-or-secret-manager&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;commit_values_to_repository&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;false&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;network&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;runner_to_target&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;null&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;safety&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;production_deploy&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;human-approval&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;production_data_write&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;human-approval&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;destructive_operation&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;human-approval&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;secret_output&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;forbidden&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;target_ref&lt;/code&gt;는 실제 IP가 아니다. 실제 서버 주소와 credential은 GitHub Secrets, Environment, 사내 secret manager 등에 보관한다.&lt;/p&gt;

&lt;p&gt;GitLab이나 Gitea에 적용하는 경우 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;control_plane.provider&lt;/code&gt;, workflow 경로, CI 명칭을 해당 플랫폼에 맞게 변경한다. 운영 원칙과 승인 기준은 유지한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;3-push를-테스트-trigger로-사용한다&quot;&gt;3. Push를 테스트 Trigger로 사용한다&lt;/h1&gt;

&lt;p&gt;AI가 branch에 push하면 CI가 자동으로 검증한다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;AI가 code 수정
    │
    ▼
git push
    │
    ▼
GitHub Actions
    │
    ├─ Build
    ├─ Unit Test
    ├─ Integration Test
    └─ Security / Static Check
    │
    ▼
PASS이면 Preview Deploy
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Workflow 예시는 다음과 같다.&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Preview Verify&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;push&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;branches&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;feature/**&quot;&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;agent/**&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;workflow_dispatch&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;jobs&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;runs-on&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;steps&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;uses&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Build&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;./project-build-command&lt;/span&gt;

      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Test&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;run&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;./project-test-command&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위 명령은 예시다. AI에게 &lt;strong&gt;현재 repository를 조사해 실제 build/test 명령을 확인한 후 작성하도록 해야 한다.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;모르는 프로젝트에 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mvn test&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;npm test&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pytest&lt;/code&gt; 같은 명령을 임의로 넣지 않는다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;4-preview-배포도-github-actions가-실행한다&quot;&gt;4. Preview 배포도 GitHub Actions가 실행한다&lt;/h1&gt;

&lt;p&gt;고객사 정책에 따라 연결 방법을 선택한다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;                       GitHub Actions
                              │
          ┌───────────────────┼────────────────────┐
          │                   │                    │
          ▼                   ▼                    ▼
    Public SSH/HTTPS     VPN Overlay          Self-hosted
                        Tailscale             Runner
                        WireGuard                 │
                        Customer VPN              │
          │                   │                    │
          └───────────────────┴────────────────────┘
                              │
                              ▼
                        Target Server
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;방법-a-github-hosted-runner에서-직접-접근&quot;&gt;방법 A: GitHub-hosted Runner에서 직접 접근&lt;/h3&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;GitHub Actions
-&amp;gt; HTTPS / SSH
-&amp;gt; target
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;공개 endpoint가 허용되는 경우에 사용할 수 있다.&lt;/p&gt;

&lt;h3 id=&quot;방법-b-vpn-overlay&quot;&gt;방법 B: VPN Overlay&lt;/h3&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;GitHub Actions
-&amp;gt; Tailscale / WireGuard / Customer VPN
-&amp;gt; Private Server
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;서버 SSH를 인터넷에 공개하지 않아도 된다.&lt;/p&gt;

&lt;h3 id=&quot;방법-c-self-hosted-runner&quot;&gt;방법 C: Self-hosted Runner&lt;/h3&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;GitHub
-&amp;gt; 내부 self-hosted runner
-&amp;gt; 내부 서버
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;외부 SaaS runner에서 고객망으로 접근할 수 없는 경우 적합하다.&lt;/p&gt;

&lt;h3 id=&quot;방법-d-jenkins를-executor로-사용&quot;&gt;방법 D: Jenkins를 Executor로 사용&lt;/h3&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;GitHub Issue / webhook
-&amp;gt; Jenkins
-&amp;gt; Codex 또는 deploy script
-&amp;gt; target
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이미 Jenkins를 운영 중인 고객사라면 기존 CI/CD 체계를 유지하면서 GitHub를 작업 상태 관리 계층으로 사용할 수 있다.&lt;/p&gt;

&lt;p&gt;GitLab 또는 Gitea를 사용하는 경우에도 이 계층은 각각 GitLab Runner, Gitea Runner 또는 기존 Jenkins로 치환할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;5-테스트-결과가-아니라-실환경-결과까지-다시-확인한다&quot;&gt;5. 테스트 결과가 아니라 실환경 결과까지 다시 확인한다&lt;/h1&gt;

&lt;p&gt;CI가 통과했다고 실제 배포가 성공했다고 볼 수 없다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Source
  │
  ▼
Syntax / Static
  │
  ▼
Unit Test
  │
  ▼
Integration Test
  │
  ▼
Container Build
  │
  ▼
Preview Deploy
  │
  ▼
Health Check
  │
  ▼
API Smoke Test
  │
  ▼
DB / Runtime State
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Workflow 마지막에는 AI가 읽기 쉬운 marker를 남길 수 있다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;BUILD=PASS
TEST=PASS
DEPLOY_PREVIEW=PASS
HEALTH=PASS
RUNTIME_STATE=PASS
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;AI는 workflow의 초록색 체크만 보는 것이 아니라 필요하면 job log를 확인해 어떤 검증이 실제 수행됐는지 확인한다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;6-github-issue를-상태-포인터로-사용한다&quot;&gt;6. GitHub Issue를 상태 포인터로 사용한다&lt;/h1&gt;

&lt;p&gt;여러 대화와 여러 AI가 같은 프로젝트를 다룬다면 “마지막 배포 상태”를 GitHub Issue에 기록해둘 수 있다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;                 ┌──────────────────────────────┐
                 │ GitHub Issue                 │
                 │ Latest Preview Deployment    │
                 │                              │
                 │ branch: feature/example      │
                 │ commit: abcdef1              │
                 │ run: 123456                  │
                 │ status: PASS                 │
                 └──────────────┬───────────────┘
                                │
                 ┌──────────────▼───────────────┐
                 │ 다음 ChatGPT / Codex가 읽음 │
                 └──────────────┬───────────────┘
                                │
                                ▼
                   Latest commit/run과 비교
                                │
                    ┌───────────┴───────────┐
                    │                       │
                  동일                    다름
                    │                       │
                    ▼                       ▼
               작업 계속            실제 workflow 우선
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;상태 Issue 예시는 다음과 같다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[Runtime] Latest Preview Deployment

branch: feature/example
commit: abcdef1
workflow_run: 123456
status: PASS
build: PASS
test: PASS
deploy: PASS
health: PASS
runtime_state: PASS
verified_at: 2026-08-20T20:00:00+09:00
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Issue는 &lt;strong&gt;증거 자체가 아니라 포인터&lt;/strong&gt;다. Issue가 오래된 경우 실제 최신 workflow를 우선한다.&lt;/p&gt;

&lt;p&gt;GitLab에서는 Issue/Work Item과 Pipeline/Job, Gitea에서는 Issue와 Actions Run을 같은 역할로 사용할 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;7-자동화-수준을-단계적으로-높인다&quot;&gt;7. 자동화 수준을 단계적으로 높인다&lt;/h1&gt;

&lt;p&gt;처음부터 완전자동화를 만들지 않는다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Level&lt;/th&gt;
      &lt;th&gt;동작&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;L0&lt;/td&gt;
      &lt;td&gt;AI가 작업 방법만 제안&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;L1&lt;/td&gt;
      &lt;td&gt;AI가 repository code/docs 수정&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;L2&lt;/td&gt;
      &lt;td&gt;Push 후 CI 자동 실행&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;L3&lt;/td&gt;
      &lt;td&gt;Preview 배포와 runtime 검증까지 자동 실행&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;L4&lt;/td&gt;
      &lt;td&gt;사람 승인 후 Production 배포&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;L5&lt;/td&gt;
      &lt;td&gt;사전에 승인된 저위험 작업을 Production까지 자동 처리&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;일반 프로젝트에서는 다음 수준을 권장한다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;                 자동화 수준

L0   방법 제안
 │
L1   코드 수정
 │
L2   CI 자동 실행
 │
L3   Preview 배포 + 실환경 검증    ◄── AI 기본 자동화 범위
 │
L4   Production 배포               ◄── 사람 승인
 │
L5   제한적 완전자동화
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;권장 기본값:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Preview = L3
Production = L4
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;8-secret을-github-repository에-저장하지-않는다&quot;&gt;8. Secret을 GitHub repository에 저장하지 않는다&lt;/h1&gt;

&lt;p&gt;저장소에는 secret의 &lt;strong&gt;이름과 위치만&lt;/strong&gt; 기록한다.&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;network&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;ssh_key_secret&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;CUSTOMER_PREVIEW_SSH_KEY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;실제 값은 commit하지 않는다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;password
API token
private SSH key
실제 고객사 credential
private certificate key
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;가능한 저장 위치:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;GitHub Actions Secrets / Environment Secrets&lt;/li&gt;
  &lt;li&gt;GitLab CI/CD Variables 또는 외부 Secret Manager&lt;/li&gt;
  &lt;li&gt;Gitea Actions Secrets&lt;/li&gt;
  &lt;li&gt;고객사 Secret Manager&lt;/li&gt;
  &lt;li&gt;target host의 권한 제한 runtime env&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Workflow에서도 secret 값을 그대로 출력하지 않는다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;9-다른-고객사에-적용할-때-먼저-확인할-것&quot;&gt;9. 다른 고객사에 적용할 때 먼저 확인할 것&lt;/h1&gt;

&lt;p&gt;기술적으로 가능한 것과 고객 정책상 가능한 것은 다르다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;고객사 Repository
       │
       ▼
┌────────────────────────────┐
│ 어떤 Git 플랫폼인가?      │
│ GitHub / GitLab / Gitea    │
└──────────┬─────────────────┘
           │
           ▼
┌─────────────────────────────┐
│ SaaS Runner 사용 가능한가? │
└──────────┬──────────────────┘
           │
           ▼
┌──────────────────────────────┐
│ 서버까지 어떤 경로가 있는가?│
│ Public / VPN / Internal      │
└──────────┬───────────────────┘
           │
           ▼
┌────────────────────────────┐
│ Preview를 분리할 수 있는가?│
└──────────┬─────────────────┘
           │
           ▼
┌────────────────────────────┐
│ DB / Secret / Rollback 확인│
└──────────┬─────────────────┘
           │
           ▼
      자동화 범위 결정
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;실제 확인 항목은 다음과 같다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;1. Git 플랫폼과 고객사 표준
2. Hosted runner 허용 여부
3. 소스 외부 저장 제한 여부
4. 고객망 접근 방식
5. self-hosted runner 또는 Jenkins 사용 가능 여부
6. Preview 환경을 분리할 수 있는지
7. 운영 DB와 테스트 DB를 분리할 수 있는지
8. Secret 저장 위치
9. CI log에 기록하면 안 되는 정보
10. Production 배포 승인자와 rollback 방법
11. ChatGPT/Codex가 해당 Git 플랫폼에 접근하는 방법
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;핵심 정보가 확인되지 않았다면 AI는 배포 자동화를 추측해서 완성하지 않고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UNKNOWN&lt;/code&gt; 또는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BLOCKED&lt;/code&gt;로 남긴다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;10-ai가-달라져도-같은-구조를-사용한다&quot;&gt;10. AI가 달라져도 같은 구조를 사용한다&lt;/h1&gt;

&lt;p&gt;AI 제품별 차이는 Git 플랫폼에 접근하는 방법에만 두고, 프로젝트 계약은 공통으로 유지한다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;                  Repository Contract
               AGENTS.md + agent-ops.yaml
                          │
             ┌────────────┼────────────┐
             │            │            │
             ▼            ▼            ▼
         ChatGPT        Codex      Claude/Gemini
             │            │            │
       Connector/API    git/CLI     API/MCP/CLI
             │            │            │
             └────────────┼────────────┘
                          │
                          ▼
                 GitHub / GitLab / Gitea
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;chatgpt&quot;&gt;ChatGPT&lt;/h3&gt;

&lt;p&gt;GitHub connector/API를 사용할 수 있다면 대화 중 repository 수정, Issue 확인, Actions 결과 조회를 수행하기 쉽다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;사용자 요청
-&amp;gt; ChatGPT repository 조사
-&amp;gt; branch/code 수정
-&amp;gt; GitHub push
-&amp;gt; Actions 실행
-&amp;gt; ChatGPT가 workflow 결과 확인
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;GitLab이나 Gitea에서는 사용 가능한 App/Plugin/API/MCP 범위에 따라 동일한 작업을 직접 수행할 수 있는지 먼저 확인한다.&lt;/p&gt;

&lt;h3 id=&quot;codex&quot;&gt;Codex&lt;/h3&gt;

&lt;p&gt;repository에 직접 접근할 수 있으므로 Git provider에 대한 제약이 상대적으로 작다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;repository clone
-&amp;gt; AGENTS.md
-&amp;gt; agent-ops.yaml
-&amp;gt; implementation
-&amp;gt; local test
-&amp;gt; branch/push
-&amp;gt; CI/Preview result 확인
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;GitHub에서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gh&lt;/code&gt;, GitLab에서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;glab&lt;/code&gt;, Gitea에서는 git과 REST API 또는 적절한 CLI를 사용할 수 있다.&lt;/p&gt;

&lt;h3 id=&quot;claude--gemini--기타-ai&quot;&gt;Claude / Gemini / 기타 AI&lt;/h3&gt;

&lt;p&gt;Git API, CLI, MCP, IDE integration 등 사용 가능한 연결 방식이 다를 뿐 동일한 계약을 적용한다.&lt;/p&gt;

&lt;p&gt;중요한 것은 &lt;strong&gt;AI별 프롬프트나 Git 제품을 표준으로 삼지 않고 repository contract를 표준으로 삼는 것&lt;/strong&gt;이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;11-ai에게-이-글을-전달할-때-사용할-bootstrap-prompt&quot;&gt;11. AI에게 이 글을 전달할 때 사용할 Bootstrap Prompt&lt;/h1&gt;

&lt;p&gt;다른 고객사 또는 새로운 프로젝트에서 &lt;strong&gt;이 글의 URL 또는 본문을 AI에게 전달한 뒤&lt;/strong&gt; 다음 요청을 사용한다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;이 글의 &quot;Git 기반 Control Plane AI 반자동화&quot; 구조를 현재 repository에 적용해줘.

GitHub를 기본 예제로 사용하지만 고객사가 GitLab 또는 Gitea를 사용한다면 같은 역할을 해당 플랫폼의 Issue/MR/PR/CI/CD/Runner/API로 매핑해줘.

목표:
- ChatGPT, Codex, 다른 AI가 같은 repository contract를 사용해야 한다.
- Git 플랫폼을 작업 상태와 실행 증거의 Control Plane으로 사용한다.
- AI는 별도 branch에서 코드를 수정하고 push한다.
- GitHub Actions, GitLab CI/CD, Gitea Actions 또는 기존 CI가 build/test를 실행한다.
- 가능한 경우 별도 Preview 환경에 배포하고 runtime까지 검증한다.
- Production 배포 및 운영 데이터 변경은 사람 승인을 유지한다.

먼저 구현하지 말고 현재 repository와 실행 환경을 조사해 다음을 CONFIRMED / UNKNOWN으로 구분해줘.

1. Git provider와 repository 접근 방식
2. default/production branch
3. 실제 build command
4. 실제 test command
5. 현재 CI/CD
6. 배포 target
7. runner에서 target까지의 네트워크 경로
8. Preview 환경 분리 가능 여부
9. DB/schema/volume 분리 방법
10. health/smoke 검증 방법
11. secret 보관 위치
12. rollback 방법
13. 운영 승인 필요 항목
14. 현재 AI가 repository/issue/CI log를 읽고 쓸 수 있는 Adapter

조사 후 다음 파일을 현재 프로젝트에 맞게 작성하거나 기존 파일과 병합해줘.

- AGENTS.md
- ops/agent-ops.yaml
- docs/00-project-context.md
- docs/04-operation-and-deployment.md
- docs/06-ai-operations.md
- 현재 Git 플랫폼에 맞는 verify workflow/pipeline
- 프로젝트에 필요한 preview deploy workflow/pipeline

규칙:
- 기존 AGENTS.md 또는 CI/CD를 무작정 덮어쓰지 말고 diff/병합한다.
- 확인하지 않은 host, 계정, credential, port, path를 추측하지 않는다.
- secret 값은 repository에 commit하지 않는다.
- main/production에서 직접 개발하지 않는다.
- Preview가 필요한 경우 운영과 port/container/network/DB/schema/volume 중 필요한 경계를 분리한다.
- 테스트하지 않은 결과는 PASS라고 쓰지 않는다.
- 완료 여부는 commit SHA, workflow/pipeline run, test output, runtime response 같은 증거로 보고한다.
- 자동화 목표는 Preview L3, Production L4를 기본값으로 한다.
- 현재 AI가 Git provider에 직접 접근할 수 없다면 그것을 숨기지 말고 필요한 API/MCP/CLI/Runner Adapter를 제안한다.

마지막에 다음을 보고해줘.

- 추가/수정 파일
- 현재 자동화 Level
- 자동으로 수행 가능한 범위
- 사람이 해야 하는 설정
- Secret store에 추가할 secret 이름(값은 제외)
- Preview 검증 방법
- Production 승인/rollback 방법
- Git provider와 AI 사이의 Adapter 방식
- 아직 UNKNOWN/BLOCKED인 항목
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 프롬프트의 목적은 AI가 바로 YAML을 만들어내게 하는 것이 아니라 &lt;strong&gt;현재 프로젝트의 실제 상태를 먼저 조사한 뒤 같은 운영 구조를 프로젝트에 맞게 구성하게 하는 것&lt;/strong&gt;이다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;12-새로운-ai에게-이어서-작업시키는-prompt&quot;&gt;12. 새로운 AI에게 이어서 작업시키는 Prompt&lt;/h1&gt;

&lt;p&gt;이미 이 구조가 적용된 repository라면 더 짧게 요청할 수 있다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;이 repository의 AI 작업 규칙을 먼저 읽고 현재 상태부터 복구해줘.

읽기 순서:
1. AGENTS.md
2. ops/agent-ops.yaml
3. docs/00-project-context.md
4. docs/04-operation-and-deployment.md
5. docs/06-ai-operations.md
6. 현재 Issue/PR/MR
7. 최신 workflow/pipeline run

문서의 상태와 실제 commit/workflow/pipeline이 다르면 실제 실행 증거를 우선하고 차이를 보고해줘.
그 후 내가 요청한 작업을 기존 branch/Preview/검증/승인 정책에 맞게 수행해줘.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 정도만 전달하면 AI 제품이나 Git provider가 바뀌더라도 프로젝트 운영 방식을 처음부터 다시 설명하는 비용을 줄일 수 있다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;실제-적용에서-중요했던-점&quot;&gt;실제 적용에서 중요했던 점&lt;/h1&gt;

&lt;p&gt;개인 서비스와 원격 서버에 이 방식을 적용하면서 특히 효과가 있었던 부분은 다음과 같았다.&lt;/p&gt;

&lt;p&gt;첫째, 운영 서비스를 사용하는 동안 별도 branch와 Preview 포트, 별도 DB를 만들어 AI가 계속 수정하고 배포해도 운영 사용을 중단하지 않을 수 있었다.&lt;/p&gt;

&lt;p&gt;둘째, AI가 서버에 직접 접근하지 못해도 CI/CD Runner를 다시 실행해 실제 DB row count, container 상태, health endpoint 등을 간접 확인할 수 있었다.&lt;/p&gt;

&lt;p&gt;셋째, 첫 workflow가 실패했을 때 AI가 job log를 읽고 수정한 뒤 다시 push하는 반복 작업이 가능했다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;구현
-&amp;gt; push
-&amp;gt; FAIL
-&amp;gt; log 분석
-&amp;gt; 코드 수정
-&amp;gt; push
-&amp;gt; PASS
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 구조가 반복되면 사용자는 모든 명령을 직접 실행하는 작업자보다 &lt;strong&gt;요구사항과 승인 지점을 관리하는 역할&lt;/strong&gt;에 가까워진다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;재발-방지와-운영-원칙&quot;&gt;재발 방지와 운영 원칙&lt;/h1&gt;

&lt;p&gt;반자동화를 사용하면서 발견한 문제를 어디에 기록할지도 중요하다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;한 번만 필요한 사실
-&amp;gt; Issue / project context

프로젝트에서 항상 지켜야 하는 규칙
-&amp;gt; AGENTS.md

배포나 실행에 필요한 구조화된 값
-&amp;gt; ops/agent-ops.yaml

반복되는 절차
-&amp;gt; script / Skill

자동으로 판정할 수 있는 규칙
-&amp;gt; test / CI pipeline

현재 실제 상태
-&amp;gt; workflow/pipeline evidence / status Issue
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;문서를 계속 늘리는 것이 목적이 아니다. 같은 실수가 발생하지 않도록 가장 적절한 위치에 규칙을 옮기는 것이 중요하다.&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;최종-요약-도식&quot;&gt;최종 요약 도식&lt;/h1&gt;

&lt;p&gt;마지막으로 전체 흐름을 한 줄로 정리하면 다음과 같다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;┌────────┐    ┌─────────┐    ┌─────────────────┐    ┌────────┐    ┌─────────┐
│ 사용자 │ -&amp;gt; │   AI    │ -&amp;gt; │ Git Control Plane│ -&amp;gt; │ Runner │ -&amp;gt; │ Preview │
└────────┘    └─────────┘    │ GitHub/GitLab/  │    └────────┘    └────┬────┘
                              │ Gitea            │                      │
                              └───────┬──────────┘                      │
                                      ▲                                 │
                                      │       Test/Health/DB 결과       │
                                      └─────────────────────────────────┘
                                             │
                                             ▼
                                       AI가 재판단
                                             │
                                   ┌─────────┴─────────┐
                                   │                   │
                                 FAIL                PASS
                                   │                   │
                             AI 코드 수정          사람 승인
                                   │                   │
                                   └── push 재실행     ▼
                                                 Production
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 구조의 핵심은 다음 한 문장으로 정리할 수 있다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;AI가 서버를 직접 제어하도록 만드는 것이 아니라, Git 기반 Control Plane에 의도를 기록하고 승인된 Runner가 실행하며 그 증거를 다시 Control Plane으로 돌려보내 AI가 다음 판단을 하게 만든다.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;고객사가 달라져도 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Git provider + repository contract + runner + evidence&lt;/code&gt;라는 중심 구조는 유지하고, GitHub/GitLab/Gitea와 네트워크 연결, 배포 target만 고객 환경에 맞게 교체하면 된다.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Java/Spring 업무 코드에 &apos;무엇&apos;보다 &apos;왜&apos;를 남기는 주석 기준</title>
   <link href="https://son1004007.github.io/backend/2026/08/04/java-spring-business-code-commenting-standard/"/>
   <updated>2026-08-04T00:00:00+00:00</updated>
   <id>https://son1004007.github.io/backend/2026/08/04/java-spring-business-code-commenting-standard</id>
   <content type="html">&lt;h2 id=&quot;코드가-말하지-못하는-것&quot;&gt;코드가 말하지 못하는 것&lt;/h2&gt;

&lt;p&gt;Java/Spring 업무 코드를 인수인계하거나 오랜만에 다시 보면 문법보다 판단 근거를 이해하는 데 시간이 더 걸립니다.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@Transactional&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;@PreAuthorize&lt;/code&gt;, Repository 호출과 예외 처리를 읽으면 코드가 무엇을 하는지는 알 수 있습니다. 하지만 다음 질문은 코드만으로 답하기 어려울 수 있습니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;왜 이 상태에서만 변경을 허용하는가?&lt;/li&gt;
  &lt;li&gt;같은 요청이 두 번 들어오면 어느 계층이 중복을 막는가?&lt;/li&gt;
  &lt;li&gt;선조회 이후에도 DB 유니크 제약이 필요한 이유는 무엇인가?&lt;/li&gt;
  &lt;li&gt;외부 모델이나 API가 잘못된 값을 반환하면 어디서 중단하는가?&lt;/li&gt;
  &lt;li&gt;권한이 없는 사용자에게 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;403&lt;/code&gt;과 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;404&lt;/code&gt; 중 어떤 응답을 주는가?&lt;/li&gt;
  &lt;li&gt;이 순서를 바꾸면 어떤 테스트와 문서를 함께 수정해야 하는가?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;반대로 모든 줄에 설명을 붙이면 코드보다 주석이 더 많아집니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// 요청을 저장한다.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;repository&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;save&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 상태를 반환한다.&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;getStatus&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이런 주석은 코드가 이미 말하는 내용을 반복합니다. 변경할 때 같이 고치지 않으면 오히려 잘못된 정보를 남깁니다.&lt;/p&gt;

&lt;p&gt;코딩 도구가 자동으로 설명을 생성하는 환경에서는 이 문제가 더 커질 수 있습니다. 생성 속도보다 어떤 설명을 남기고 무엇을 생략할지에 대한 기준이 먼저 필요합니다.&lt;/p&gt;

&lt;h2 id=&quot;코드와-설명의-수명이-다르다&quot;&gt;코드와 설명의 수명이 다르다&lt;/h2&gt;

&lt;p&gt;주석의 역할을 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;코드를 한글로 번역하는 것&lt;/code&gt;으로 잡으면 설명이 많아도 유지보수에 도움이 되지 않습니다.&lt;/p&gt;

&lt;p&gt;코드와 설명이 담당해야 할 정보의 수명이 다르기 때문입니다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;구분&lt;/th&gt;
      &lt;th&gt;우선 표현할 내용&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;코드&lt;/td&gt;
      &lt;td&gt;타입, 실행 순서, 조건, 계산과 호출 관계&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Javadoc&lt;/td&gt;
      &lt;td&gt;타입과 메서드가 지켜야 하는 지속적인 계약&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;인라인 주석&lt;/td&gt;
      &lt;td&gt;해당 코드 가까이에서만 이해할 수 있는 이유와 제약&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;테스트&lt;/td&gt;
      &lt;td&gt;설명한 계약이 실제로 지켜지는지 확인하는 동작 증거&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;설계 문서&lt;/td&gt;
      &lt;td&gt;여러 컴포넌트에 걸친 구조와 대안 선택&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;업무 규칙을 코드 밖 문서에만 두면 구현과 멀어집니다. 반대로 모든 설계를 소스 주석에 넣으면 변경 범위와 책임이 불분명해집니다. 설명 수준을 나누는 것이 필요합니다.&lt;/p&gt;

&lt;h2 id=&quot;기준-무엇은-코드에-왜는-설명에&quot;&gt;기준: 무엇은 코드에, 왜는 설명에&lt;/h2&gt;

&lt;p&gt;기준은 간단합니다.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;코드는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;무엇&lt;/code&gt;을 보여주고, 설명은 코드만으로 알기 어려운 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;왜&lt;/code&gt;, 제약, 실패 방식과 변경 영향을 알려 줍니다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;설명은 세 단계로 나눕니다.&lt;/p&gt;

&lt;h3 id=&quot;1-타입-수준-javadoc&quot;&gt;1. 타입 수준 Javadoc&lt;/h3&gt;

&lt;p&gt;업무 책임이 있는 클래스와 인터페이스에는 다음 내용을 필요한 범위에서 작성합니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;목적과 책임&lt;/li&gt;
  &lt;li&gt;주요 입력과 출력 또는 협력 객체&lt;/li&gt;
  &lt;li&gt;핵심 업무와 보안 기준&lt;/li&gt;
  &lt;li&gt;실패 시 동작과 부작용&lt;/li&gt;
  &lt;li&gt;수정할 때 함께 확인할 테스트와 문서&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;2-메서드-수준-javadoc&quot;&gt;2. 메서드 수준 Javadoc&lt;/h3&gt;

&lt;p&gt;다음 메서드는 이름만으로 계약을 파악하기 어렵기 때문에 설명할 가치가 큽니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Controller와 애플리케이션 서비스의 공개 진입점&lt;/li&gt;
  &lt;li&gt;상태를 변경하는 도메인 메서드&lt;/li&gt;
  &lt;li&gt;트랜잭션, 멱등성, 잠금과 동시성 규칙이 있는 메서드&lt;/li&gt;
  &lt;li&gt;권한과 객체 접근 범위를 결정하는 메서드&lt;/li&gt;
  &lt;li&gt;DB, 파일, 메시지 브로커, LLM과 외부 API를 사용하는 메서드&lt;/li&gt;
  &lt;li&gt;단위, 시간대, 식별자와 외부 응답을 변환하는 메서드&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;3-인라인-주석&quot;&gt;3. 인라인 주석&lt;/h3&gt;

&lt;p&gt;인라인 주석은 다음처럼 코드 가까이에 있어야 의미가 있는 이유만 설명합니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;특정 저장 순서를 지키는 이유&lt;/li&gt;
  &lt;li&gt;경합 상황의 최종 판정 기준&lt;/li&gt;
  &lt;li&gt;실패 시 일부 데이터만 남지 않게 하는 방법&lt;/li&gt;
  &lt;li&gt;외부 시스템 제약 때문에 필요한 우회&lt;/li&gt;
  &lt;li&gt;변경할 때 함께 바뀌어야 하는 계약&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;단순 getter, 필드 대입만 하는 생성자, record 접근자와 이름이 충분히 명확한 한 줄 위임에는 설명을 붙이지 않습니다.&lt;/p&gt;

&lt;h2 id=&quot;javaspring에-적용하기&quot;&gt;Java/Spring에 적용하기&lt;/h2&gt;

&lt;p&gt;합성 구매 승인 서비스를 예로 들면, 모델이 구매 요청 초안을 만들더라도 승인과 저장 가능 여부는 Spring 애플리케이션이 결정해야 합니다. 이 경계를 타입 설명에 남길 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cm&quot;&gt;/**
 * 신뢰할 수 없는 자연어 요청을 검증된 구매 요청 초안으로 변환하는 서비스.
 *
 * &amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;입력/출력:&amp;lt;/strong&amp;gt; 사용자 요청과 서버가 조회한 정책 근거를 받아
 * 아직 승인되지 않은 초안을 반환한다.
 *
 * &amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;핵심 기준:&amp;lt;/strong&amp;gt; 모델은 초안만 제안하며 승인, 상태 전이와
 * 저장 가능 여부는 애플리케이션과 도메인 규칙이 최종 결정한다.
 *
 * &amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;수정 시 주의:&amp;lt;/strong&amp;gt; 모델 출력 계약을 바꾸면 구조 검증,
 * 실패 시나리오 테스트와 아키텍처 문서를 함께 확인한다.
 */&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PurchaseDraftService&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;멱등성을 처리하는 메서드는 반환값뿐 아니라 같은 키에 다른 입력이 들어왔을 때의 정책을 설명해야 합니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cm&quot;&gt;/**
 * 같은 요청자의 재시도를 하나의 구매 초안으로 수렴시킨다.
 *
 * &amp;lt;p&amp;gt;같은 멱등성 키와 같은 입력이면 기존 결과를 반환하고, 같은 키에 다른 입력이
 * 들어오면 기존 결과를 덮어쓰지 않고 충돌로 중단한다.
 *
 * @param idempotencyKey 재시도 사이에 유지되는 요청 식별키
 * @param requestText 신뢰할 수 없는 자연어 구매 요청
 * @return 새로 생성했거나 동일 입력으로 확인한 구매 요청
 */&lt;/span&gt;
&lt;span class=&quot;kd&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;PurchaseRequest&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;createDraft&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;idempotencyKey&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;String&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;requestText&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;// 구현 생략&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;선조회와 저장 사이의 경합을 처리한다면 다음처럼 이유를 인라인으로 남깁니다.&lt;/p&gt;

&lt;div class=&quot;language-java highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nc&quot;&gt;PurchaseRequest&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;existing&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;repository&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;findByActorAndKey&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;actor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;orElse&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;existing&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;requireSameInput&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;existing&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fingerprint&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// 애플리케이션 선조회만으로는 최초 요청끼리의 경합을 제거할 수 없다.&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// DB 고유 제약을 사용하는 INSERT ... ON CONFLICT DO NOTHING이 승자를 정한다.&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;repository&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;insertIfAbsent&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;newDraft&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;nc&quot;&gt;PurchaseRequest&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;winner&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;repository&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;findByActorAndKey&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;actor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;na&quot;&gt;orElseThrow&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;IllegalStateException:&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;new&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;requireSameInput&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;winner&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fingerprint&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;위 코드는 트랜잭션 경계를 생략한 구조 예시입니다. PostgreSQL의 충돌 무시 구문이나 별도 저장 트랜잭션처럼, 제약 위반 뒤 현재 JPA 트랜잭션이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rollback-only&lt;/code&gt;가 되지 않는 방식을 선택해야 합니다. 단순 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;save()&lt;/code&gt;는 flush 시점이 늦어 예외를 같은 블록에서 잡지 못할 수 있으므로 실제 구현에서는 사용 중인 DB와 트랜잭션 방식에 맞는 테스트가 필요합니다.&lt;/p&gt;

&lt;p&gt;주석을 추가하기 전에 메서드 이름과 구조도 함께 봐야 합니다. 설명이 여러 문단 필요할 정도로 한 메서드에 책임이 몰려 있다면 먼저 검증, 저장과 외부 호출을 분리하는 편이 낫습니다.&lt;/p&gt;

&lt;p&gt;계층별로는 다음 관점을 우선합니다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;계층&lt;/th&gt;
      &lt;th&gt;설명할 내용&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Controller/API&lt;/td&gt;
      &lt;td&gt;신뢰하지 않는 입력과 응답의 정보 노출 경계&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Application Service&lt;/td&gt;
      &lt;td&gt;업무 흐름, 트랜잭션, 멱등성과 경합 처리&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Domain&lt;/td&gt;
      &lt;td&gt;허용 상태 전이와 불변조건&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Persistence&lt;/td&gt;
      &lt;td&gt;유니크 제약, 잠금과 조회 결과의 업무 의미&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;External Adapter&lt;/td&gt;
      &lt;td&gt;timeout, 출력 검증, fallback과 재시도 정책&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Security&lt;/td&gt;
      &lt;td&gt;역할 권한과 객체 권한의 분리, 존재 여부 은닉 기준&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Test&lt;/td&gt;
      &lt;td&gt;경합, 롤백과 실패 주입 장치의 의도&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&quot;설명도-검증-대상이다&quot;&gt;설명도 검증 대상이다&lt;/h2&gt;

&lt;p&gt;주석은 많다고 좋은 것이 아니므로 존재 여부와 내용의 정확성을 나누어 검증합니다.&lt;/p&gt;

&lt;h3 id=&quot;자동-검증&quot;&gt;자동 검증&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;Checkstyle 규칙이나 커스텀 검사를 구성한 프로젝트라면 지정한 타입과 메서드의 Javadoc 누락을 검사합니다.&lt;/li&gt;
  &lt;li&gt;Javadoc/doclint 생성 단계에서 태그, 링크와 문법 오류를 확인합니다.&lt;/li&gt;
  &lt;li&gt;컴파일과 단위·통합 테스트로 설명한 계약의 실제 동작을 확인합니다.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git diff --check&lt;/code&gt;로 불필요한 공백과 형식 오류를 확인합니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;일반적인 Maven 프로젝트에서는 다음 검증을 사용할 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;./mvnw verify
./mvnw javadoc:javadoc
git diff &lt;span class=&quot;nt&quot;&gt;--check&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;프로젝트에 Checkstyle이나 Javadoc 검증이 구성되지 않았다면 명령을 완료 기준으로 기록하기 전에 먼저 빌드 구성을 확인해야 합니다.&lt;/p&gt;

&lt;h3 id=&quot;코드-리뷰&quot;&gt;코드 리뷰&lt;/h3&gt;

&lt;p&gt;자동 검사는 설명의 존재와 형식은 확인할 수 있지만 사실 여부까지 판단하지는 못합니다. 리뷰에서는 다음을 확인합니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;설명이 실제 코드와 테스트에 일치하는가?&lt;/li&gt;
  &lt;li&gt;코드가 이미 말하는 내용을 반복하지 않는가?&lt;/li&gt;
  &lt;li&gt;업무 규칙, 신뢰 경계, 실패 방식과 변경 영향이 드러나는가?&lt;/li&gt;
  &lt;li&gt;오래된 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TODO&lt;/code&gt;와 주석 처리한 코드가 남아 있지 않은가?&lt;/li&gt;
  &lt;li&gt;고객 식별정보, 내부 경로, 인증정보와 운영 데이터가 없는가?&lt;/li&gt;
  &lt;li&gt;검증하지 않은 성능이나 보안을 보장한다고 표현하지 않는가?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;팀-작업-기준으로-유지하기&quot;&gt;팀 작업 기준으로 유지하기&lt;/h2&gt;

&lt;p&gt;주석 품질을 개인 습관에만 맡기지 않으려면 저장소의 작업 기준과 연결해야 합니다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;재사용 가능한 코드 설명 표준을 저장소에 둡니다.&lt;/li&gt;
  &lt;li&gt;Agent 작업 지침과 리뷰 체크리스트에서 표준을 참조합니다.&lt;/li&gt;
  &lt;li&gt;보안, 상태 변경, 외부 I/O, 트랜잭션과 동시성 코드를 우선 검토합니다.&lt;/li&gt;
  &lt;li&gt;관련 동작을 바꾸는 커밋에서 Javadoc과 주석도 함께 검토합니다.&lt;/li&gt;
  &lt;li&gt;단순 getter까지 강제하지 않도록 자동 검사 예외 범위를 명확히 합니다.&lt;/li&gt;
  &lt;li&gt;설명과 구현이 충돌하면 테스트와 설계 문서를 함께 확인해 의도한 계약을 다시 결정합니다.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;이 글에서 사용한 상세 기준과 체크리스트는 &lt;a href=&quot;https://github.com/son1004007/engineering-career-portfolio/blob/main/03_portfolio/code-explanation-standard.md&quot;&gt;Java/Spring 코드 설명과 주석 표준&lt;/a&gt;에 별도로 정리했습니다. 다른 저장소에서는 공개 범위, 언어와 검증 도구를 조정해 기본 표준으로 재사용할 수 있습니다.&lt;/p&gt;

&lt;h2 id=&quot;마무리&quot;&gt;마무리&lt;/h2&gt;

&lt;p&gt;좋은 주석은 코드 양을 늘리는 작업이 아니라 업무 규칙을 유지 가능한 계약으로 바꾸는 작업입니다.
트랜잭션, 멱등성, 권한과 외부 시스템 경계처럼 변경 위험이 큰 곳부터 이유를 남기고 테스트와 함께 검토하면 됩니다. 결국 중요한 것은 주석의 개수가 아니라, 다음 개발자가 안전하게 변경할 만큼 판단 근거가 남아 있는지입니다.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Rocky Linux 디스크 마운트 실패 대응 절차</title>
   <link href="https://son1004007.github.io/infrastructure/2026/06/22/rocky-linux-disk-mount-failure/"/>
   <updated>2026-06-22T00:00:00+00:00</updated>
   <id>https://son1004007.github.io/infrastructure/2026/06/22/rocky-linux-disk-mount-failure</id>
   <content type="html">&lt;h2 id=&quot;문제점&quot;&gt;문제점&lt;/h2&gt;

&lt;p&gt;Linux 서버를 재부팅한 뒤 기존에 자동으로 마운트되던 디스크가 정상적으로 마운트되지 않는 상황이 발생할 수 있습니다.&lt;/p&gt;

&lt;p&gt;대표적인 증상은 다음과 같습니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;df -h&lt;/code&gt;에서 기존 마운트 경로가 보이지 않습니다.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lsblk&lt;/code&gt;에서는 디스크나 파티션이 보이지만 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MOUNTPOINT&lt;/code&gt;가 비어 있습니다.&lt;/li&gt;
  &lt;li&gt;서비스가 특정 데이터 경로를 찾지 못합니다.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/fstab&lt;/code&gt;에 등록된 항목 때문에 부팅이 지연되거나 emergency mode로 진입할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이 문제는 단순히 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mount&lt;/code&gt; 명령만 다시 실행해서 끝낼 일이 아닙니다. 디스크 인식, 파일시스템 상태, UUID 변경 여부, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/fstab&lt;/code&gt; 설정 오류를 순서대로 확인해야 합니다.&lt;/p&gt;

&lt;h2 id=&quot;원인&quot;&gt;원인&lt;/h2&gt;

&lt;p&gt;디스크 마운트 실패의 원인은 보통 다음 범위에 있습니다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;디스크 또는 파티션이 OS에서 인식되지 않음&lt;/li&gt;
  &lt;li&gt;파일시스템 손상&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/fstab&lt;/code&gt;에 잘못된 장치명 또는 UUID가 등록됨&lt;/li&gt;
  &lt;li&gt;기존 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/dev/sdb2&lt;/code&gt; 같은 장치명이 재부팅 후 바뀜&lt;/li&gt;
  &lt;li&gt;마운트 대상 디렉터리가 없거나 권한이 맞지 않음&lt;/li&gt;
  &lt;li&gt;이전에 수동 마운트만 했고 자동 마운트 설정이 없었음&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;특히 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/dev/sdb2&lt;/code&gt; 같은 장치명은 고정값으로 보기 어렵습니다. 재부팅 후 디스크 탐지 순서가 바뀌면 다른 이름으로 잡힐 수 있습니다. 운영 서버에서는 장치명보다 UUID 기준으로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/fstab&lt;/code&gt;을 작성하는 편이 안전합니다.&lt;/p&gt;

&lt;h2 id=&quot;해결&quot;&gt;해결&lt;/h2&gt;

&lt;p&gt;대응 원칙은 다음입니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;디스크 인식 확인
-&amp;gt; 파일시스템 확인
-&amp;gt; UUID 확인
-&amp;gt; 수동 마운트 테스트
-&amp;gt; /etc/fstab 수정
-&amp;gt; 재부팅 검증
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;장애 상황에서는 바로 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/fstab&lt;/code&gt;을 수정하기보다 먼저 수동 마운트로 정상 접근 가능한지 확인해야 합니다.&lt;/p&gt;

&lt;h2 id=&quot;실행-방법&quot;&gt;실행 방법&lt;/h2&gt;

&lt;h3 id=&quot;1-디스크와-파티션-확인&quot;&gt;1. 디스크와 파티션 확인&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;lsblk &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt;
blkid
&lt;span class=&quot;nb&quot;&gt;df&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-h&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;확인할 항목은 다음입니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;디스크가 보이는지&lt;/li&gt;
  &lt;li&gt;파티션이 보이는지&lt;/li&gt;
  &lt;li&gt;파일시스템 타입이 보이는지&lt;/li&gt;
  &lt;li&gt;UUID가 있는지&lt;/li&gt;
  &lt;li&gt;기존 마운트 경로가 비어 있는지&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;예시:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;NAME   FSTYPE UUID                                 MOUNTPOINT
sdb
└─sdb2 xfs    11111111-2222-3333-4444-555555555555
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;2-기존-fstab-확인&quot;&gt;2. 기존 fstab 확인&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;cat&lt;/span&gt; /etc/fstab
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/dev/sdb2&lt;/code&gt;처럼 장치명으로 등록되어 있다면 UUID 방식으로 바꾸는 것이 좋습니다.&lt;/p&gt;

&lt;p&gt;나쁜 예:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-fstab&quot;&gt;/dev/sdb2 /data xfs defaults 0 0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;좋은 예:&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-fstab&quot;&gt;UUID=11111111-2222-3333-4444-555555555555 /data xfs defaults,nofail 0 0
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nofail&lt;/code&gt; 옵션을 넣으면 해당 디스크 마운트에 실패해도 부팅 전체가 중단되는 위험을 줄일 수 있습니다. 단, 서비스가 해당 경로에 의존한다면 별도 점검 로직은 필요합니다.&lt;/p&gt;

&lt;h3 id=&quot;3-마운트-대상-디렉터리-확인&quot;&gt;3. 마운트 대상 디렉터리 확인&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;ls&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-ld&lt;/span&gt; /data
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;없으면 생성합니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; /data
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;4-수동-마운트-테스트&quot;&gt;4. 수동 마운트 테스트&lt;/h3&gt;

&lt;p&gt;파일시스템 타입이 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;xfs&lt;/code&gt;인 경우:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;mount &lt;span class=&quot;nt&quot;&gt;-t&lt;/span&gt; xfs &lt;span class=&quot;nv&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;11111111-2222-3333-4444-555555555555 /data
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ext4&lt;/code&gt;인 경우:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;mount &lt;span class=&quot;nt&quot;&gt;-t&lt;/span&gt; ext4 &lt;span class=&quot;nv&quot;&gt;UUID&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;11111111-2222-3333-4444-555555555555 /data
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;마운트 후 확인합니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;df&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-h&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;ls&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-la&lt;/span&gt; /data
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;5-fstab-적용-테스트&quot;&gt;5. fstab 적용 테스트&lt;/h3&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/fstab&lt;/code&gt; 수정 후 바로 재부팅하지 말고 다음 명령으로 검증합니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;mount &lt;span class=&quot;nt&quot;&gt;-a&lt;/span&gt;
systemctl daemon-reload
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;오류가 있으면 즉시 수정합니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;findmnt &lt;span class=&quot;nt&quot;&gt;--verify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;6-재부팅-검증&quot;&gt;6. 재부팅 검증&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;reboot
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;재부팅 후 확인합니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;lsblk &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;df&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-h&lt;/span&gt;
findmnt /data
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;검증-방법&quot;&gt;검증 방법&lt;/h2&gt;

&lt;p&gt;정상 상태는 다음 조건을 만족해야 합니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lsblk -f&lt;/code&gt;에서 대상 파티션의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MOUNTPOINT&lt;/code&gt;가 표시됩니다.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;df -h&lt;/code&gt;에서 마운트 경로가 표시됩니다.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;findmnt /data&lt;/code&gt;가 정상 결과를 반환합니다.&lt;/li&gt;
  &lt;li&gt;재부팅 후에도 동일하게 마운트됩니다.&lt;/li&gt;
  &lt;li&gt;해당 경로를 사용하는 서비스가 정상 기동됩니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;재발-방지--개선-방향&quot;&gt;재발 방지 / 개선 방향&lt;/h2&gt;

&lt;p&gt;운영 서버에서는 다음 기준을 적용하는 것이 좋습니다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/fstab&lt;/code&gt;에는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/dev/sdb2&lt;/code&gt; 대신 UUID를 사용합니다.&lt;/li&gt;
  &lt;li&gt;데이터 디스크에는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nofail&lt;/code&gt; 옵션 적용을 검토합니다.&lt;/li&gt;
  &lt;li&gt;마운트 경로를 사용하는 서비스는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RequiresMountsFor=&lt;/code&gt; 같은 systemd 의존성 설정을 검토합니다.&lt;/li&gt;
  &lt;li&gt;디스크 증설, 교체, 파티션 변경 후에는 재부팅 검증까지 수행합니다.&lt;/li&gt;
  &lt;li&gt;장애 대응 기록에는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lsblk -f&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;blkid&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/fstab&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;df -h&lt;/code&gt; 결과를 함께 남깁니다.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;포트폴리오-관점의-의미&quot;&gt;포트폴리오 관점의 의미&lt;/h2&gt;

&lt;p&gt;이 사례는 단순 Linux 명령어 사용 경험이 아니라 운영 서버에서 장애 원인을 단계적으로 좁히는 역량을 보여줍니다.&lt;/p&gt;

&lt;p&gt;특히 다음 역량과 연결됩니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Linux 파일시스템과 마운트 구조 이해&lt;/li&gt;
  &lt;li&gt;재부팅 후 자동 복구 가능한 운영 설정&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/fstab&lt;/code&gt; 변경 시 부팅 장애 리스크 인지&lt;/li&gt;
  &lt;li&gt;수동 조치와 영구 설정을 분리하는 운영 습관&lt;/li&gt;
  &lt;li&gt;장애 대응 결과를 재사용 가능한 체크리스트로 정리하는 능력&lt;/li&gt;
&lt;/ul&gt;
</content>
 </entry>
 
 <entry>
   <title>Nginx + Tomcat + Let’s Encrypt 기반 웹 서비스 운영 구조</title>
   <link href="https://son1004007.github.io/infrastructure/2026/06/22/nginx-tomcat-letsencrypt-service-architecture/"/>
   <updated>2026-06-22T00:00:00+00:00</updated>
   <id>https://son1004007.github.io/infrastructure/2026/06/22/nginx-tomcat-letsencrypt-service-architecture</id>
   <content type="html">&lt;h2 id=&quot;문제점&quot;&gt;문제점&lt;/h2&gt;

&lt;p&gt;웹 서비스를 운영하다 보면 Apache, Nginx, Tomcat, Java 애플리케이션, SSL 인증서 설정이 섞이면서 구조가 복잡해지는 경우가 많습니다.&lt;/p&gt;

&lt;p&gt;특히 기존 서버에 여러 설정이 누적되어 있으면 다음 문제가 발생합니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;어떤 프로세스가 80/443 포트를 점유하는지 불명확합니다.&lt;/li&gt;
  &lt;li&gt;정적 파일 제공과 WAS 프록시 역할이 섞입니다.&lt;/li&gt;
  &lt;li&gt;Apache 설정과 Nginx 설정이 동시에 남아 있습니다.&lt;/li&gt;
  &lt;li&gt;Tomcat은 정상인데 외부 도메인에서는 404, 502, 504가 발생합니다.&lt;/li&gt;
  &lt;li&gt;SSL 인증서 갱신 경로와 웹서버 reload 방식이 정리되어 있지 않습니다.&lt;/li&gt;
  &lt;li&gt;장애 발생 시 어느 계층부터 확인해야 하는지 모호합니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이런 상태에서는 설정을 조금씩 수정할수록 장애 원인이 더 불분명해집니다. 따라서 운영 구조를 단순화하고 역할을 명확히 나누는 것이 우선입니다.&lt;/p&gt;

&lt;h2 id=&quot;원인&quot;&gt;원인&lt;/h2&gt;

&lt;p&gt;구조가 복잡해지는 원인은 대개 다음과 같습니다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;초기에는 단순 정적 웹으로 시작했지만 이후 Java WAS가 추가됩니다.&lt;/li&gt;
  &lt;li&gt;Apache와 Nginx를 번갈아 사용하면서 설정 파일이 누적됩니다.&lt;/li&gt;
  &lt;li&gt;SSL 적용 시점에 임시 설정이 운영 설정으로 남습니다.&lt;/li&gt;
  &lt;li&gt;80, 443, 8080, 8009 등 포트 역할이 문서화되지 않습니다.&lt;/li&gt;
  &lt;li&gt;웹서버와 WAS의 책임 경계가 정리되지 않습니다.&lt;/li&gt;
  &lt;li&gt;인증서 갱신 자동화와 서비스 reload 절차가 빠집니다.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;문제의 핵심은 특정 도구가 아니라 &lt;strong&gt;역할 분리 부재&lt;/strong&gt;입니다.&lt;/p&gt;

&lt;h2 id=&quot;해결&quot;&gt;해결&lt;/h2&gt;

&lt;p&gt;권장 구조는 단순합니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Client
  -&amp;gt; HTTPS 443
  -&amp;gt; Nginx
  -&amp;gt; Tomcat 8080
  -&amp;gt; Java Web Application
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;각 구성요소의 역할은 다음처럼 분리합니다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;구성요소&lt;/th&gt;
      &lt;th&gt;역할&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Nginx&lt;/td&gt;
      &lt;td&gt;80/443 수신, SSL 종료, 정적 파일 제공, Reverse Proxy&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Let’s Encrypt&lt;/td&gt;
      &lt;td&gt;TLS 인증서 발급 및 갱신&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Tomcat&lt;/td&gt;
      &lt;td&gt;Java 애플리케이션 실행&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Spring Boot / WAR&lt;/td&gt;
      &lt;td&gt;비즈니스 로직과 화면/API 처리&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;systemd&lt;/td&gt;
      &lt;td&gt;Nginx, Tomcat 프로세스 관리&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Apache를 꼭 써야 할 이유가 없다면, Nginx를 외부 진입점으로 고정하고 Apache 설정은 제거하거나 비활성화하는 것이 좋습니다.&lt;/p&gt;

&lt;h2 id=&quot;실행-방법&quot;&gt;실행 방법&lt;/h2&gt;

&lt;h3 id=&quot;1-포트-점유-상태-확인&quot;&gt;1. 포트 점유 상태 확인&lt;/h3&gt;

&lt;p&gt;먼저 어떤 프로세스가 외부 포트를 사용 중인지 확인합니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;ss &lt;span class=&quot;nt&quot;&gt;-lntp&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-E&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;:80|:443|:8080|:8009&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;확인 기준은 다음입니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;80   -&amp;gt; Nginx
443  -&amp;gt; Nginx
8080 -&amp;gt; Tomcat
8009 -&amp;gt; 필요하지 않으면 비활성화 검토
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Nginx와 Apache가 동시에 80/443을 사용하려고 하면 구조가 불안정해집니다. 하나만 외부 진입점으로 선택해야 합니다.&lt;/p&gt;

&lt;h3 id=&quot;2-서비스-상태-확인&quot;&gt;2. 서비스 상태 확인&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;systemctl status nginx
systemctl status tomcat
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;배포 방식에 따라 Tomcat 서비스 이름은 다를 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;systemctl list-units &lt;span class=&quot;nt&quot;&gt;--type&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;service | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-Ei&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;tomcat|java|was&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;3-tomcat-직접-응답-확인&quot;&gt;3. Tomcat 직접 응답 확인&lt;/h3&gt;

&lt;p&gt;Nginx 설정 전에 Tomcat이 로컬에서 정상 응답하는지 확인합니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;curl &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; http://127.0.0.1:8080/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;애플리케이션 context path가 있다면 해당 경로로 확인합니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;curl &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; http://127.0.0.1:8080/app/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Tomcat이 직접 응답하지 않으면 Nginx 설정을 고쳐도 외부 서비스는 정상화되지 않습니다.&lt;/p&gt;

&lt;h3 id=&quot;4-nginx-reverse-proxy-설정&quot;&gt;4. Nginx Reverse Proxy 설정&lt;/h3&gt;

&lt;p&gt;기본 구조는 다음과 같습니다.&lt;/p&gt;

&lt;div class=&quot;language-nginx highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;server&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kn&quot;&gt;listen&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;80&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;kn&quot;&gt;server_name&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;example.com&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;www.example.com&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;kn&quot;&gt;location&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;/.well-known/acme-challenge/&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kn&quot;&gt;root&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;/var/www/letsencrypt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;kn&quot;&gt;location&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kn&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;301&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;https://&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$host$request_uri&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;server&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kn&quot;&gt;listen&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;443&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ssl&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;http2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;kn&quot;&gt;server_name&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;example.com&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;www.example.com&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;kn&quot;&gt;ssl_certificate&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;/etc/letsencrypt/live/example.com/fullchain.pem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;kn&quot;&gt;ssl_certificate_key&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;/etc/letsencrypt/live/example.com/privkey.pem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;kn&quot;&gt;location&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kn&quot;&gt;proxy_pass&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;http://127.0.0.1:8080&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;kn&quot;&gt;proxy_set_header&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Host&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$host&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;kn&quot;&gt;proxy_set_header&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;X-Real-IP&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$remote_addr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;kn&quot;&gt;proxy_set_header&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;X-Forwarded-For&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;kn&quot;&gt;proxy_set_header&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;$scheme&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;운영 환경에서는 도메인, context path, 정적 파일 경로, 업로드 용량 제한 등을 서비스에 맞게 조정해야 합니다.&lt;/p&gt;

&lt;h3 id=&quot;5-nginx-설정-검증&quot;&gt;5. Nginx 설정 검증&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;nginx &lt;span class=&quot;nt&quot;&gt;-t&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;systemctl reload nginx
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;설정 변경 후 바로 재시작하기보다 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nginx -t&lt;/code&gt;로 문법을 먼저 검증합니다.&lt;/p&gt;

&lt;h3 id=&quot;6-lets-encrypt-인증서-발급&quot;&gt;6. Let’s Encrypt 인증서 발급&lt;/h3&gt;

&lt;p&gt;Certbot을 사용하는 경우 일반적인 흐름은 다음과 같습니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;certbot certonly &lt;span class=&quot;nt&quot;&gt;--webroot&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-w&lt;/span&gt; /var/www/letsencrypt &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; example.com &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; www.example.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Nginx 플러그인을 사용할 수도 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;certbot &lt;span class=&quot;nt&quot;&gt;--nginx&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; example.com &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; www.example.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;다만 운영 서버에서는 자동으로 Nginx 설정을 수정하는 방식보다, 직접 관리하는 설정 파일을 기준으로 인증서만 발급받는 방식이 더 예측 가능할 때가 많습니다.&lt;/p&gt;

&lt;h3 id=&quot;7-인증서-자동-갱신-확인&quot;&gt;7. 인증서 자동 갱신 확인&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;systemctl list-timers | &lt;span class=&quot;nb&quot;&gt;grep &lt;/span&gt;certbot
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;certbot renew &lt;span class=&quot;nt&quot;&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;갱신 후 Nginx reload가 필요한지 확인합니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;systemctl reload nginx
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;8-외부-접속-확인&quot;&gt;8. 외부 접속 확인&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;curl &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; http://example.com
curl &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; https://example.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;기대 결과는 다음입니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;http  -&amp;gt; 301 또는 308 redirect to https
https -&amp;gt; 200, 302, 또는 애플리케이션이 의도한 응답
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;검증-방법&quot;&gt;검증 방법&lt;/h2&gt;

&lt;p&gt;운영 구조가 정상인지 확인하려면 계층별로 점검합니다.&lt;/p&gt;

&lt;h3 id=&quot;프로세스&quot;&gt;프로세스&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;systemctl is-active nginx
systemctl is-active tomcat
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;포트&quot;&gt;포트&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;ss &lt;span class=&quot;nt&quot;&gt;-lntp&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-E&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;:80|:443|:8080&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;로컬-was&quot;&gt;로컬 WAS&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;curl &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; http://127.0.0.1:8080/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;외부-httphttps&quot;&gt;외부 HTTP/HTTPS&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;curl &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; http://example.com
curl &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; https://example.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;인증서&quot;&gt;인증서&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;certbot certificates
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;로그&quot;&gt;로그&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo tail&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; /var/log/nginx/access.log
&lt;span class=&quot;nb&quot;&gt;sudo tail&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; /var/log/nginx/error.log
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;journalctl &lt;span class=&quot;nt&quot;&gt;-u&lt;/span&gt; tomcat &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;장애가 발생하면 외부 URL부터 보는 것이 아니라, 포트 → Nginx → Tomcat → 애플리케이션 순서로 좁히는 것이 좋습니다.&lt;/p&gt;

&lt;h2 id=&quot;재발-방지--개선-방향&quot;&gt;재발 방지 / 개선 방향&lt;/h2&gt;

&lt;p&gt;운영 구조를 안정화하려면 다음 기준을 적용합니다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;외부 진입점은 Nginx 하나로 고정합니다.&lt;/li&gt;
  &lt;li&gt;Apache를 사용하지 않으면 stop/disable 처리하고 설정 파일도 별도로 백업합니다.&lt;/li&gt;
  &lt;li&gt;80/443/8080 포트 역할을 문서화합니다.&lt;/li&gt;
  &lt;li&gt;Nginx 설정 파일은 도메인 단위로 분리합니다.&lt;/li&gt;
  &lt;li&gt;SSL 인증서 발급/갱신/검증 절차를 문서화합니다.&lt;/li&gt;
  &lt;li&gt;Tomcat 직접 응답 확인 절차를 장애 대응 문서에 포함합니다.&lt;/li&gt;
  &lt;li&gt;배포 후에는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nginx -t&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;curl&lt;/code&gt;, 로그 확인을 표준 검증 절차로 둡니다.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;예시 운영 체크리스트는 다음입니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[ ] nginx -t 성공
[ ] nginx active
[ ] tomcat active
[ ] 80 -&amp;gt; nginx
[ ] 443 -&amp;gt; nginx
[ ] 8080 -&amp;gt; tomcat
[ ] http -&amp;gt; https redirect
[ ] https 정상 응답
[ ] certbot renew --dry-run 성공
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;포트폴리오-관점의-의미&quot;&gt;포트폴리오 관점의 의미&lt;/h2&gt;

&lt;p&gt;이 글은 단순히 Nginx 설정 예시를 정리한 것이 아니라, 운영 서버에서 웹서버, WAS, SSL 인증서의 책임을 분리하고 장애 대응 가능한 구조로 정리하는 역량을 보여줍니다.&lt;/p&gt;

&lt;p&gt;특히 다음 역량과 연결됩니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Nginx Reverse Proxy 운영 경험&lt;/li&gt;
  &lt;li&gt;Tomcat 기반 Java 웹 서비스 운영 이해&lt;/li&gt;
  &lt;li&gt;Let’s Encrypt 인증서 적용 및 갱신 구조 이해&lt;/li&gt;
  &lt;li&gt;80/443/8080 포트 역할 분리&lt;/li&gt;
  &lt;li&gt;Apache/Nginx/Tomcat이 섞인 환경을 단순화하는 판단력&lt;/li&gt;
  &lt;li&gt;장애 발생 시 계층별로 원인을 좁히는 운영 역량&lt;/li&gt;
&lt;/ul&gt;
</content>
 </entry>
 
 <entry>
   <title>분석 결과를 웹 서비스로 시스템화할 때 고려할 점</title>
   <link href="https://son1004007.github.io/data-systemization/2026/06/22/data-analysis-systemization/"/>
   <updated>2026-06-22T00:00:00+00:00</updated>
   <id>https://son1004007.github.io/data-systemization/2026/06/22/data-analysis-systemization</id>
   <content type="html">&lt;h2 id=&quot;문제점&quot;&gt;문제점&lt;/h2&gt;

&lt;p&gt;데이터 분석 프로젝트에서는 분석 결과를 보고서나 노트북 형태로 전달하는 것에서 끝나지 않고, 웹 화면이나 운영 시스템에 반영해야 하는 경우가 많습니다.&lt;/p&gt;

&lt;p&gt;하지만 분석 결과를 곧바로 서비스로 만드는 과정에서는 다음 문제가 자주 발생합니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;분석 코드는 일회성 실행을 전제로 작성되어 있습니다.&lt;/li&gt;
  &lt;li&gt;입력 데이터 경로와 컬럼 구조가 고정되어 있지 않습니다.&lt;/li&gt;
  &lt;li&gt;분석 결과의 검증 기준이 명확하지 않습니다.&lt;/li&gt;
  &lt;li&gt;웹 화면에서 필요한 조회 조건과 분석 결과 구조가 맞지 않습니다.&lt;/li&gt;
  &lt;li&gt;운영 DB에 저장할 테이블 설계가 늦게 결정됩니다.&lt;/li&gt;
  &lt;li&gt;분석가와 개발자가 같은 용어를 다르게 사용합니다.&lt;/li&gt;
  &lt;li&gt;현업은 분석 결과뿐 아니라 수정, 이력관리, 엑셀 업로드 같은 업무 기능까지 요구할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이 경우 단순히 분석 코드를 서버에 올리는 것만으로는 시스템화가 어렵습니다.&lt;/p&gt;

&lt;h2 id=&quot;원인&quot;&gt;원인&lt;/h2&gt;

&lt;p&gt;분석과 시스템화의 목적이 다르기 때문입니다.&lt;/p&gt;

&lt;p&gt;분석의 목적은 주로 인사이트 도출입니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;데이터 이해 -&amp;gt; 전처리 -&amp;gt; 모델링/집계 -&amp;gt; 결과 해석 -&amp;gt; 보고
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;반면 시스템화의 목적은 반복 가능한 운영입니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;입력 표준화 -&amp;gt; 처리 자동화 -&amp;gt; 결과 저장 -&amp;gt; 화면 조회 -&amp;gt; 권한/로그/이력 관리 -&amp;gt; 장애 대응
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;분석 결과가 의미 있어도 운영 시스템에 넣으려면 데이터 구조, 실행 주기, 예외 처리, 저장 방식, 사용자 화면, 권한 기준이 추가로 필요합니다.&lt;/p&gt;

&lt;h2 id=&quot;해결&quot;&gt;해결&lt;/h2&gt;

&lt;p&gt;분석 결과를 웹 서비스로 만들 때는 먼저 산출물을 세 가지로 나눠야 합니다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;분석 산출물&lt;/li&gt;
  &lt;li&gt;데이터 산출물&lt;/li&gt;
  &lt;li&gt;서비스 산출물&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;각 산출물은 다음처럼 구분합니다.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;구분&lt;/th&gt;
      &lt;th&gt;내용&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;분석 산출물&lt;/td&gt;
      &lt;td&gt;분석 목적, 방법론, 모델, 해석, 지표&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;데이터 산출물&lt;/td&gt;
      &lt;td&gt;입력 데이터, 중간 데이터, 결과 테이블, 컬럼 정의&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;서비스 산출물&lt;/td&gt;
      &lt;td&gt;화면, API, 권한, 배치, 로그, 이력관리&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;이 구분이 없으면 분석 과제인지 개발 과제인지 범위가 흐려집니다.&lt;/p&gt;

&lt;h2 id=&quot;실행-방법&quot;&gt;실행 방법&lt;/h2&gt;

&lt;h3 id=&quot;1-분석-결과의-사용-방식을-먼저-확인&quot;&gt;1. 분석 결과의 사용 방식을 먼저 확인&lt;/h3&gt;

&lt;p&gt;아래 질문에 답해야 합니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;분석 결과를 누가 보는가?
얼마나 자주 보는가?
조회만 하는가, 수정도 하는가?
결과를 저장해야 하는가?
이전 결과와 비교해야 하는가?
엑셀 다운로드가 필요한가?
업로드 기능이 필요한가?
운영 시스템에 다시 반영해야 하는가?
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;조회만 필요한 경우와 수정/이력관리가 필요한 경우는 시스템 범위가 완전히 달라집니다.&lt;/p&gt;

&lt;h3 id=&quot;2-입력-데이터-기준-정의&quot;&gt;2. 입력 데이터 기준 정의&lt;/h3&gt;

&lt;p&gt;분석 코드를 운영하려면 입력 데이터 기준이 필요합니다.&lt;/p&gt;

&lt;p&gt;확인 항목은 다음입니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;데이터 출처&lt;/li&gt;
  &lt;li&gt;적재 주기&lt;/li&gt;
  &lt;li&gt;기준일자&lt;/li&gt;
  &lt;li&gt;필수 컬럼&lt;/li&gt;
  &lt;li&gt;결측치 처리 기준&lt;/li&gt;
  &lt;li&gt;코드값 정의&lt;/li&gt;
  &lt;li&gt;데이터 변경 가능성&lt;/li&gt;
  &lt;li&gt;수동 파일 업로드 여부&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;입력 데이터가 안정적이지 않으면 웹 화면을 먼저 만들어도 운영이 어렵습니다.&lt;/p&gt;

&lt;h3 id=&quot;3-결과-테이블-설계&quot;&gt;3. 결과 테이블 설계&lt;/h3&gt;

&lt;p&gt;웹 서비스에서 분석 결과를 조회하려면 결과를 DB 테이블로 저장하는 것이 일반적입니다.&lt;/p&gt;

&lt;p&gt;예시 구조:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;analysis_result
- result_id
- base_date
- target_type
- target_id
- metric_name
- metric_value
- rank_value
- created_at
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;하지만 모든 분석 결과를 key-value 형태로 저장하면 화면 개발과 검증이 어려워질 수 있습니다. 지표가 명확하고 화면 구조가 정해져 있다면 전용 결과 테이블을 설계하는 편이 낫습니다.&lt;/p&gt;

&lt;h3 id=&quot;4-분석-코드와-서비스-코드를-분리&quot;&gt;4. 분석 코드와 서비스 코드를 분리&lt;/h3&gt;

&lt;p&gt;분석 코드는 보통 Python, R, Notebook 형태로 작성됩니다. 웹 서비스는 Java/Spring Boot, FastAPI, Node.js 등으로 구현될 수 있습니다.&lt;/p&gt;

&lt;p&gt;권장 구조는 다음입니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;분석 코드
-&amp;gt; 결과 CSV 또는 DB 저장
-&amp;gt; 백엔드 API
-&amp;gt; 웹 화면 조회
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;분석 코드를 웹 요청마다 직접 실행하는 구조는 피하는 것이 좋습니다. 실행 시간이 길고, 장애 원인 분리가 어렵고, 동일 요청에 대해 결과가 달라질 수 있기 때문입니다.&lt;/p&gt;

&lt;h3 id=&quot;5-배치와-api-역할-분리&quot;&gt;5. 배치와 API 역할 분리&lt;/h3&gt;

&lt;p&gt;반복 분석이 필요하면 배치 작업으로 결과를 생성하고, 웹 API는 이미 생성된 결과를 조회하는 구조가 안정적입니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Batch: 데이터 수집, 전처리, 분석, 결과 저장
API: 결과 조회, 필터링, 다운로드
UI: 검색 조건, 차트, 테이블 표시
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 구조는 장애 대응에도 유리합니다. 분석 실패와 화면 장애를 분리해서 볼 수 있습니다.&lt;/p&gt;

&lt;h3 id=&quot;6-현업-요구사항-범위-분리&quot;&gt;6. 현업 요구사항 범위 분리&lt;/h3&gt;

&lt;p&gt;현업이 요청하는 내용은 분석과 업무 기능이 섞일 수 있습니다.&lt;/p&gt;

&lt;p&gt;예시:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;분석 범위:
- 고객 유형별 입금 패턴 분석
- 예측 결과 산출
- 지표별 순위 제공

업무 기능 범위:
- 엑셀 업로드
- 데이터 수정
- 이력관리
- 승인 프로세스
- ERP 반영
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 둘은 일정, 기술, 책임 범위가 다릅니다. 초기에 구분하지 않으면 분석 과제가 개발 프로젝트로 확대될 수 있습니다.&lt;/p&gt;

&lt;h2 id=&quot;검증-방법&quot;&gt;검증 방법&lt;/h2&gt;

&lt;p&gt;시스템화 가능 여부는 다음 기준으로 확인합니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;같은 입력 데이터로 같은 결과가 재현됩니다.&lt;/li&gt;
  &lt;li&gt;결과 테이블 컬럼 정의가 문서화되어 있습니다.&lt;/li&gt;
  &lt;li&gt;화면에서 필요한 조회 조건이 정의되어 있습니다.&lt;/li&gt;
  &lt;li&gt;분석 실패 시 재실행 기준이 있습니다.&lt;/li&gt;
  &lt;li&gt;결과 생성 시점과 화면 조회 시점이 구분됩니다.&lt;/li&gt;
  &lt;li&gt;현업이 결과를 어떻게 사용할지 설명할 수 있습니다.&lt;/li&gt;
  &lt;li&gt;운영 중 데이터 수정이 필요한지 여부가 확인되어 있습니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;재발-방지--개선-방향&quot;&gt;재발 방지 / 개선 방향&lt;/h2&gt;

&lt;p&gt;분석 결과 시스템화 프로젝트에서는 다음 문서를 먼저 만드는 것이 좋습니다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;분석 시나리오 정의서&lt;/li&gt;
  &lt;li&gt;입력 데이터 정의서&lt;/li&gt;
  &lt;li&gt;결과 데이터 정의서&lt;/li&gt;
  &lt;li&gt;화면 조회 조건 정의서&lt;/li&gt;
  &lt;li&gt;배치 실행 기준&lt;/li&gt;
  &lt;li&gt;오류 및 재처리 기준&lt;/li&gt;
  &lt;li&gt;분석 범위와 업무 기능 범위 구분표&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;이 문서가 있으면 분석가, 개발자, 현업, PM 간의 인식 차이를 줄일 수 있습니다.&lt;/p&gt;

&lt;h2 id=&quot;포트폴리오-관점의-의미&quot;&gt;포트폴리오 관점의 의미&lt;/h2&gt;

&lt;p&gt;이 주제는 단순 백엔드 개발이나 단순 분석이 아니라, 분석 결과를 운영 가능한 서비스로 연결하는 역량을 보여줍니다.&lt;/p&gt;

&lt;p&gt;특히 다음 역량과 연결됩니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;분석가와 개발자 사이의 요구사항 번역 능력&lt;/li&gt;
  &lt;li&gt;데이터 결과물을 API와 화면으로 연결하는 설계 능력&lt;/li&gt;
  &lt;li&gt;분석 범위와 개발 범위 구분 능력&lt;/li&gt;
  &lt;li&gt;배치, DB, API, UI 간 역할 분리 이해&lt;/li&gt;
  &lt;li&gt;공공/기업 데이터 분석 프로젝트의 시스템화 경험&lt;/li&gt;
&lt;/ul&gt;
</content>
 </entry>
 
 <entry>
   <title>Podman 기반 CloudBeaver 운영 검토</title>
   <link href="https://son1004007.github.io/infrastructure/2026/06/22/cloudbeaver-with-podman/"/>
   <updated>2026-06-22T00:00:00+00:00</updated>
   <id>https://son1004007.github.io/infrastructure/2026/06/22/cloudbeaver-with-podman</id>
   <content type="html">&lt;h2 id=&quot;문제점&quot;&gt;문제점&lt;/h2&gt;

&lt;p&gt;분석가나 개발자가 운영 DB 또는 분석 DB에 접근해야 할 때, 로컬 PC에 DB 클라이언트를 각각 설치하고 네트워크를 개별로 허용하는 방식은 관리 비용이 큽니다.&lt;/p&gt;

&lt;p&gt;특히 다음 상황에서는 웹 기반 DB 클라이언트가 필요해질 수 있습니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;사용자 PC에서 DB 포트 접근이 제한됩니다.&lt;/li&gt;
  &lt;li&gt;분석 서버에서는 DB 접근이 가능하지만 개인 PC에서는 접근이 어렵습니다.&lt;/li&gt;
  &lt;li&gt;여러 사용자가 같은 분석 서버를 통해 DB를 조회해야 합니다.&lt;/li&gt;
  &lt;li&gt;DBeaver 같은 클라이언트를 각자 설치하고 설정하는 방식이 번거롭습니다.&lt;/li&gt;
  &lt;li&gt;SSH는 가능하지만 GUI 기반 DB 조회 환경이 필요합니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이때 CloudBeaver Community Edition을 분석 서버에 올리고, 사용자는 웹 브라우저로 접속하게 하는 구성을 검토할 수 있습니다.&lt;/p&gt;

&lt;h2 id=&quot;원인&quot;&gt;원인&lt;/h2&gt;

&lt;p&gt;DB 접근 환경이 복잡해지는 원인은 대개 다음과 같습니다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;DB 접근 허용 네트워크가 제한되어 있습니다.&lt;/li&gt;
  &lt;li&gt;사용자 PC마다 보안 정책과 설치 환경이 다릅니다.&lt;/li&gt;
  &lt;li&gt;분석 서버는 DB 접근이 가능하지만 사용자별 편의 도구가 부족합니다.&lt;/li&gt;
  &lt;li&gt;Docker가 없고 Podman만 사용 가능한 서버 환경이 있습니다.&lt;/li&gt;
  &lt;li&gt;root 권한 없이 일반 계정으로 서비스를 운영해야 합니다.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;이 경우 Docker Compose 기반 예제를 그대로 적용하기 어렵고, rootless Podman 기준으로 운영 방식을 다시 잡아야 합니다.&lt;/p&gt;

&lt;h2 id=&quot;해결&quot;&gt;해결&lt;/h2&gt;

&lt;p&gt;기본 방향은 다음입니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;분석 서버에 CloudBeaver 컨테이너 실행
-&amp;gt; 웹 포트만 사용자에게 제공
-&amp;gt; DB 접속 정보는 CloudBeaver 내부에서 관리
-&amp;gt; 데이터 조회/분석 작업은 브라우저 기반으로 수행
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;다만 운영 기준을 명확히 해야 합니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;누구에게 웹 접근을 허용할 것인가&lt;/li&gt;
  &lt;li&gt;CloudBeaver 계정 관리를 어떻게 할 것인가&lt;/li&gt;
  &lt;li&gt;DB 계정 권한은 읽기 전용으로 제한할 것인가&lt;/li&gt;
  &lt;li&gt;컨테이너 데이터 디렉터리는 어디에 둘 것인가&lt;/li&gt;
  &lt;li&gt;서버 재부팅 후 자동 기동이 필요한가&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;실행-방법&quot;&gt;실행 방법&lt;/h2&gt;

&lt;h3 id=&quot;1-작업-디렉터리-생성&quot;&gt;1. 작업 디렉터리 생성&lt;/h3&gt;

&lt;p&gt;root 권한 없이 일반 계정으로 운영한다면 홈 디렉터리 하위에 두는 것이 단순합니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; ~/services/cloudbeaver/workspace
&lt;span class=&quot;nb&quot;&gt;cd&lt;/span&gt; ~/services/cloudbeaver
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;2-podman-이미지-실행&quot;&gt;2. Podman 이미지 실행&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;podman run &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--name&lt;/span&gt; cloudbeaver &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; 8978:8978 &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;-v&lt;/span&gt; ~/services/cloudbeaver/workspace:/opt/cloudbeaver/workspace &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  dbeaver/cloudbeaver:latest
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;실행 상태를 확인합니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;podman ps
podman logs &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; cloudbeaver
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;3-방화벽-또는-리버스-프록시-검토&quot;&gt;3. 방화벽 또는 리버스 프록시 검토&lt;/h3&gt;

&lt;p&gt;내부 사용자에게만 제공한다면 서버의 8978 포트를 제한된 네트워크에만 허용합니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;http://server-address:8978
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;외부 노출이 필요하다면 Nginx 또는 Apache 리버스 프록시를 통해 HTTPS를 적용하는 것이 좋습니다.&lt;/p&gt;

&lt;p&gt;예시 구조:&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;사용자 브라우저 -&amp;gt; HTTPS 443 -&amp;gt; Nginx/Apache -&amp;gt; CloudBeaver 8978
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;4-db-권한-제한&quot;&gt;4. DB 권한 제한&lt;/h3&gt;

&lt;p&gt;CloudBeaver가 편하다고 해서 강한 권한의 DB 계정을 그대로 등록하면 위험합니다.&lt;/p&gt;

&lt;p&gt;권장 기준은 다음입니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;조회 목적: read-only 계정 사용&lt;/li&gt;
  &lt;li&gt;분석 목적: 별도 schema 또는 sandbox DB 사용&lt;/li&gt;
  &lt;li&gt;운영 DB 직접 수정 권한 금지&lt;/li&gt;
  &lt;li&gt;사용자별 계정 또는 최소 권한 계정 사용&lt;/li&gt;
  &lt;li&gt;접속 정보 공유 금지&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;5-데이터-디렉터리-백업&quot;&gt;5. 데이터 디렉터리 백업&lt;/h3&gt;

&lt;p&gt;CloudBeaver 설정은 workspace에 저장됩니다. 따라서 컨테이너만 삭제하면 설정이 사라질 수 있습니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;ls&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-la&lt;/span&gt; ~/services/cloudbeaver/workspace
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;백업 대상은 다음입니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;~/services/cloudbeaver/workspace
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;검증-방법&quot;&gt;검증 방법&lt;/h2&gt;

&lt;p&gt;정상 운영 여부는 다음 기준으로 확인합니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;podman ps&lt;/code&gt;에서 CloudBeaver 컨테이너가 실행 중입니다.&lt;/li&gt;
  &lt;li&gt;브라우저에서 CloudBeaver 로그인 화면이 열립니다.&lt;/li&gt;
  &lt;li&gt;DB 연결 테스트가 성공합니다.&lt;/li&gt;
  &lt;li&gt;등록된 DB 계정이 필요한 권한만 가지고 있습니다.&lt;/li&gt;
  &lt;li&gt;서버 재부팅 후 수동 또는 자동으로 재기동할 수 있습니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;재발-방지--개선-방향&quot;&gt;재발 방지 / 개선 방향&lt;/h2&gt;

&lt;p&gt;운영 목적으로 사용하려면 다음 개선이 필요합니다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;CloudBeaver 전용 서비스 디렉터리를 표준화합니다.&lt;/li&gt;
  &lt;li&gt;DB 연결 계정은 최소 권한 원칙을 적용합니다.&lt;/li&gt;
  &lt;li&gt;가능하면 HTTPS 리버스 프록시를 적용합니다.&lt;/li&gt;
  &lt;li&gt;접근 가능한 사용자 네트워크를 제한합니다.&lt;/li&gt;
  &lt;li&gt;workspace 디렉터리 백업 기준을 정합니다.&lt;/li&gt;
  &lt;li&gt;컨테이너 재기동 절차를 문서화합니다.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;systemd 사용자 서비스로 등록하면 재부팅 후 자동 기동도 가능합니다. 다만 초기 검토 단계에서는 수동 기동으로 충분한지 먼저 판단하는 것이 좋습니다.&lt;/p&gt;

&lt;h2 id=&quot;포트폴리오-관점의-의미&quot;&gt;포트폴리오 관점의 의미&lt;/h2&gt;

&lt;p&gt;이 사례는 단순히 DB 툴을 설치하는 문제가 아니라, 분석가와 개발자가 사용할 수 있는 공용 데이터 접근 환경을 어떻게 설계할지에 대한 판단입니다.&lt;/p&gt;

&lt;p&gt;특히 다음 역량과 연결됩니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;rootless Podman 운영 이해&lt;/li&gt;
  &lt;li&gt;웹 기반 DB 클라이언트 도입 검토&lt;/li&gt;
  &lt;li&gt;DB 접근 권한과 편의성의 균형 판단&lt;/li&gt;
  &lt;li&gt;분석 서버 기반 협업 환경 구성&lt;/li&gt;
  &lt;li&gt;운영 환경에서 보안과 생산성을 함께 고려하는 능력&lt;/li&gt;
&lt;/ul&gt;
</content>
 </entry>
 
 <entry>
   <title>Apache VirtualHost 404 장애 원인 분석</title>
   <link href="https://son1004007.github.io/infrastructure/2026/06/22/apache-virtualhost-404-troubleshooting/"/>
   <updated>2026-06-22T00:00:00+00:00</updated>
   <id>https://son1004007.github.io/infrastructure/2026/06/22/apache-virtualhost-404-troubleshooting</id>
   <content type="html">&lt;h2 id=&quot;문제점&quot;&gt;문제점&lt;/h2&gt;

&lt;p&gt;Apache HTTP Server를 사용해 웹 서비스를 운영할 때, 설정 파일은 정상으로 보이지만 도메인 접속 시 계속 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;404 Not Found&lt;/code&gt;가 발생하는 상황이 있습니다.&lt;/p&gt;

&lt;p&gt;대표적인 증상은 다음과 같습니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;systemctl status httpd&lt;/code&gt;는 정상입니다.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;apachectl configtest&lt;/code&gt; 또는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;httpd -t&lt;/code&gt; 결과가 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Syntax OK&lt;/code&gt;입니다.&lt;/li&gt;
  &lt;li&gt;80 포트는 정상적으로 listen 중입니다.&lt;/li&gt;
  &lt;li&gt;하지만 브라우저 또는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;curl&lt;/code&gt; 요청은 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;404 Not Found&lt;/code&gt;를 반환합니다.&lt;/li&gt;
  &lt;li&gt;에러 로그에는 명확한 치명 오류가 없고 access log에 404만 남습니다.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이 경우 단순히 Apache가 죽은 문제가 아니라, 요청이 의도한 VirtualHost 또는 DocumentRoot, ProxyPass로 연결되지 않는 문제일 가능성이 높습니다.&lt;/p&gt;

&lt;h2 id=&quot;원인&quot;&gt;원인&lt;/h2&gt;

&lt;p&gt;Apache 404 장애는 보통 다음 중 하나로 발생합니다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;요청 Host 헤더가 VirtualHost의 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ServerName&lt;/code&gt; 또는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ServerAlias&lt;/code&gt;와 맞지 않음&lt;/li&gt;
  &lt;li&gt;기본 VirtualHost가 요청을 받아 다른 DocumentRoot로 처리함&lt;/li&gt;
  &lt;li&gt;DocumentRoot 경로에 실제 파일이 없음&lt;/li&gt;
  &lt;li&gt;Directory 권한 또는 SELinux 컨텍스트 문제&lt;/li&gt;
  &lt;li&gt;ProxyPass 경로와 실제 백엔드 경로가 맞지 않음&lt;/li&gt;
  &lt;li&gt;80/443 포트 처리 주체가 Apache가 아닌 다른 서비스임&lt;/li&gt;
  &lt;li&gt;HTTPS 요청인데 443 VirtualHost가 없거나 인증서 설정이 빠짐&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;특히 여러 웹서버를 교체하면서 운영한 환경에서는 Nginx, Apache, Tomcat, Java 애플리케이션의 역할이 뒤섞여 원인 파악이 어려워질 수 있습니다.&lt;/p&gt;

&lt;h2 id=&quot;해결&quot;&gt;해결&lt;/h2&gt;

&lt;p&gt;대응 순서는 다음처럼 가져가는 것이 좋습니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;포트 확인
-&amp;gt; Host 헤더 기준 VirtualHost 매칭 확인
-&amp;gt; DocumentRoot 확인
-&amp;gt; ProxyPass 확인
-&amp;gt; 로그 확인
-&amp;gt; 80/443 분리 확인
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;가장 중요한 점은 브라우저로만 확인하지 않고 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;curl -H &apos;Host: ...&apos;&lt;/code&gt; 방식으로 VirtualHost 매칭을 직접 검증하는 것입니다.&lt;/p&gt;

&lt;h2 id=&quot;실행-방법&quot;&gt;실행 방법&lt;/h2&gt;

&lt;h3 id=&quot;1-apache-설정-문법-확인&quot;&gt;1. Apache 설정 문법 확인&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apachectl configtest
&lt;span class=&quot;c&quot;&gt;# 또는&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;httpd &lt;span class=&quot;nt&quot;&gt;-t&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;정상 예시는 다음입니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Syntax OK
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;문법이 정상이어도 VirtualHost 매칭 오류는 발생할 수 있습니다.&lt;/p&gt;

&lt;h3 id=&quot;2-포트-listen-확인&quot;&gt;2. 포트 listen 확인&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;ss &lt;span class=&quot;nt&quot;&gt;-lntp&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-E&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;:80|:443&apos;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;확인할 항목은 다음입니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;80 포트를 Apache가 listen 중인지&lt;/li&gt;
  &lt;li&gt;443 포트를 Apache가 listen 중인지&lt;/li&gt;
  &lt;li&gt;Nginx 등 다른 웹서버가 같은 포트를 사용 중인지&lt;/li&gt;
  &lt;li&gt;백엔드 애플리케이션 포트가 정상인지&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;3-virtualhost-목록-확인&quot;&gt;3. VirtualHost 목록 확인&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;apachectl &lt;span class=&quot;nt&quot;&gt;-S&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 명령은 어떤 VirtualHost가 어떤 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ServerName&lt;/code&gt;으로 등록되어 있는지 보여줍니다.&lt;/p&gt;

&lt;p&gt;확인할 항목은 다음입니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;요청 도메인과 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ServerName&lt;/code&gt;이 일치하는지&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ServerAlias&lt;/code&gt;에 필요한 도메인이 포함되어 있는지&lt;/li&gt;
  &lt;li&gt;기본 VirtualHost가 의도한 설정인지&lt;/li&gt;
  &lt;li&gt;80과 443 각각에 VirtualHost가 있는지&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;4-host-헤더로-직접-요청-테스트&quot;&gt;4. Host 헤더로 직접 요청 테스트&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;curl &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-H&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Host: example.com&apos;&lt;/span&gt; http://127.0.0.1/
curl &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-H&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Host: www.example.com&apos;&lt;/span&gt; http://127.0.0.1/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;외부 도메인 DNS나 방화벽 문제와 Apache 설정 문제를 분리하기 위해 로컬 요청부터 확인합니다.&lt;/p&gt;

&lt;h3 id=&quot;5-documentroot-확인&quot;&gt;5. DocumentRoot 확인&lt;/h3&gt;

&lt;p&gt;설정 파일에서 DocumentRoot를 확인합니다.&lt;/p&gt;

&lt;div class=&quot;language-apache highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;VirtualHost&lt;/span&gt;&lt;span class=&quot;sr&quot;&gt; *:80&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;
&lt;/span&gt;    &lt;span class=&quot;nc&quot;&gt;ServerName&lt;/span&gt; example.com
    &lt;span class=&quot;nc&quot;&gt;ServerAlias&lt;/span&gt; www.example.com
    &lt;span class=&quot;nc&quot;&gt;DocumentRoot&lt;/span&gt; /var/www/example
&lt;span class=&quot;p&quot;&gt;&amp;lt;/&lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;VirtualHost&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;실제 경로가 있는지 확인합니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;ls&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-la&lt;/span&gt; /var/www/example
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;정적 파일을 제공하는 구조라면 최소한 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;index.html&lt;/code&gt; 또는 라우팅을 처리할 파일이 있어야 합니다.&lt;/p&gt;

&lt;h3 id=&quot;6-directory-권한-확인&quot;&gt;6. Directory 권한 확인&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;namei &lt;span class=&quot;nt&quot;&gt;-l&lt;/span&gt; /var/www/example
&lt;span class=&quot;nb&quot;&gt;ls&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-ld&lt;/span&gt; /var/www/example
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Apache 실행 계정이 경로를 읽을 수 있어야 합니다.&lt;/p&gt;

&lt;p&gt;SELinux가 활성화되어 있다면 컨텍스트도 확인합니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;getenforce
&lt;span class=&quot;nb&quot;&gt;ls&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-Z&lt;/span&gt; /var/www/example
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;필요 시 웹 루트 컨텍스트를 적용합니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;restorecon &lt;span class=&quot;nt&quot;&gt;-Rv&lt;/span&gt; /var/www/example
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;7-proxypass-확인&quot;&gt;7. ProxyPass 확인&lt;/h3&gt;

&lt;p&gt;백엔드 애플리케이션으로 넘기는 구조라면 ProxyPass 경로를 확인합니다.&lt;/p&gt;

&lt;div class=&quot;language-apache highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nc&quot;&gt;ProxyPass&lt;/span&gt; /app http://127.0.0.1:8080/app
&lt;span class=&quot;nc&quot;&gt;ProxyPassReverse&lt;/span&gt; /app http://127.0.0.1:8080/app
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;백엔드가 실제로 응답하는지 확인합니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;curl &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; http://127.0.0.1:8080/app
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Apache 404인지, 백엔드 애플리케이션의 404인지도 구분해야 합니다.&lt;/p&gt;

&lt;h3 id=&quot;8-로그-확인&quot;&gt;8. 로그 확인&lt;/h3&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;sudo tail&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; /var/log/httpd/access_log
&lt;span class=&quot;nb&quot;&gt;sudo tail&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; /var/log/httpd/error_log
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;VirtualHost별 로그 파일을 따로 지정했다면 해당 로그를 확인합니다.&lt;/p&gt;

&lt;div class=&quot;language-apache highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nc&quot;&gt;ErrorLog&lt;/span&gt; logs/example-error.log
&lt;span class=&quot;nc&quot;&gt;CustomLog&lt;/span&gt; logs/example-access.log combined
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;검증-방법&quot;&gt;검증 방법&lt;/h2&gt;

&lt;p&gt;정상화 여부는 다음 기준으로 확인합니다.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;curl &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-H&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Host: example.com&apos;&lt;/span&gt; http://127.0.0.1/
curl &lt;span class=&quot;nt&quot;&gt;-I&lt;/span&gt; http://example.com/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;확인할 항목은 다음입니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;HTTP status가 200, 301, 302 등 의도한 값인지&lt;/li&gt;
  &lt;li&gt;access log에 올바른 VirtualHost 로그가 남는지&lt;/li&gt;
  &lt;li&gt;error log에 권한 또는 proxy 오류가 없는지&lt;/li&gt;
  &lt;li&gt;80/443 모두 의도한 서버가 응답하는지&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;재발-방지--개선-방향&quot;&gt;재발 방지 / 개선 방향&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;VirtualHost는 도메인별로 파일을 분리합니다.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;apachectl -S&lt;/code&gt; 결과를 운영 문서에 남깁니다.&lt;/li&gt;
  &lt;li&gt;80과 443 설정을 같이 관리합니다.&lt;/li&gt;
  &lt;li&gt;Nginx와 Apache를 동시에 운영할 경우 포트 소유자를 명확히 합니다.&lt;/li&gt;
  &lt;li&gt;정적 파일 제공과 백엔드 프록시 역할을 구분합니다.&lt;/li&gt;
  &lt;li&gt;장애 대응 시 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;curl -H Host&lt;/code&gt; 테스트를 표준 절차에 포함합니다.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;포트폴리오-관점의-의미&quot;&gt;포트폴리오 관점의 의미&lt;/h2&gt;

&lt;p&gt;이 사례는 웹서버 장애를 단순 재시작으로 처리하지 않고, 네트워크 포트, VirtualHost 매칭, DocumentRoot, ProxyPass, 백엔드 응답을 분리해서 점검하는 역량을 보여줍니다.&lt;/p&gt;

&lt;p&gt;특히 다음 역량과 연결됩니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Apache HTTPD 운영 경험&lt;/li&gt;
  &lt;li&gt;웹서버와 WAS 역할 분리 이해&lt;/li&gt;
  &lt;li&gt;404 오류의 발생 계층 구분&lt;/li&gt;
  &lt;li&gt;운영 장애 상황에서의 점검 순서 수립&lt;/li&gt;
  &lt;li&gt;Nginx/Apache/Tomcat 기반 서비스 운영 역량&lt;/li&gt;
&lt;/ul&gt;
</content>
 </entry>
 
 <entry>
   <title>ChatGPT를 활용해 GitHub Pages 기술 블로그를 시작합니다</title>
   <link href="https://son1004007.github.io/career/2026/06/21/start-github-pages-blog-with-chatgpt/"/>
   <updated>2026-06-21T00:00:00+00:00</updated>
   <id>https://son1004007.github.io/career/2026/06/21/start-github-pages-blog-with-chatgpt</id>
   <content type="html">&lt;h2 id=&quot;배경&quot;&gt;배경&lt;/h2&gt;

&lt;p&gt;이 블로그는 단순한 개발 기록장이 아니라, 실무에서 겪은 문제 해결 과정과 기술적 판단을 정리하기 위한 공간입니다.&lt;/p&gt;

&lt;p&gt;백엔드/풀스택 개발자로 일하면서 Java, Spring Boot, 데이터베이스, Linux 서버 운영, 웹 서비스 배포, 데이터 분석 시스템화와 관련된 업무를 수행하고 있습니다. 업무 중에는 장애 대응, 서버 설정, 데이터베이스 이슈, 배포 환경 정리, 분석가와 개발자 사이의 요구사항 조율처럼 문서화하지 않으면 사라지는 경험이 많습니다.&lt;/p&gt;

&lt;p&gt;그동안 이런 내용은 개인 메모, ChatGPT 대화, 업무 보고, GitHub 저장소 등에 흩어져 있었습니다. 이제는 이 경험들을 정리해서 재사용 가능한 기술 자산으로 만들고자 합니다.&lt;/p&gt;

&lt;h2 id=&quot;왜-github-pages인가&quot;&gt;왜 GitHub Pages인가&lt;/h2&gt;

&lt;p&gt;기술 블로그를 만들 수 있는 방법은 많습니다. Tistory, Velog, Medium, Notion 같은 서비스형 플랫폼은 빠르고 간편하게 글을 공개하기 좋고, Obsidian 기반 정적 사이트도 Git과 조합해 운영할 수 있습니다. 저는 그중 Markdown 글과 이미지, 정적 사이트 설정, 변경 이력을 GitHub 저장소에서 관리하고 GitHub 프로필·Actions·Pages까지 하나의 개발 작업 흐름으로 연결할 수 있다는 점을 우선했습니다.&lt;/p&gt;

&lt;p&gt;첫째, 개발자의 작업 흐름과 잘 맞습니다. GitHub Pages 게시 설정과 배포 워크플로를 구성해 두면 Markdown으로 글을 작성하고 Git으로 변경 이력을 관리한 뒤, GitHub에 push한 변경을 자동으로 빌드하고 사이트에 배포할 수 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/blog/github-pages-deployment-success.webp&quot; alt=&quot;GitHub Actions에서 GitHub Pages 빌드와 배포가 성공한 요약 화면&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;이 저장소의 GitHub Pages 워크플로에서 build, report-build-status, deploy 작업이 성공한 화면입니다. 실제 공개 페이지의 글과 이미지가 정상 표시되는지는 배포 후 별도로 확인합니다.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;이 저장소에서는 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_posts&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;assets&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docs&lt;/code&gt;, Jekyll(정적 사이트 생성기) 설정 파일을 일반 코드 저장소처럼 함께 관리합니다.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/images/blog/github-blog-repository-structure.webp&quot; alt=&quot;GitHub 저장소의 블로그 파일과 Markdown 글 관리 구조&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Markdown 글, 이미지, 문서와 Jekyll 설정을 하나의 Git 저장소에서 함께 관리하는 구조입니다.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;둘째, GitHub 프로필과 포트폴리오를 연결하기 좋습니다. 코드 저장소와 공개 기술 기록을 같은 GitHub 생태계에서 관리할 수 있어, 이력서에서 짧게 설명한 경험을 더 구체적인 문제 상황과 해결 과정으로 연결하기 쉽습니다.&lt;/p&gt;

&lt;p&gt;셋째, 코드와 문서를 함께 관리할 수 있습니다. 단순한 회고 글이 아니라 설정 파일, 명령어, 체크리스트, 장애 대응 절차를 함께 정리할 수 있습니다.&lt;/p&gt;

&lt;h2 id=&quot;왜-chatgpt를-활용하는가&quot;&gt;왜 ChatGPT를 활용하는가&lt;/h2&gt;

&lt;p&gt;ChatGPT는 단순히 질문에 답을 받거나 글을 대신 써 주는 도구보다, 실무 경험을 정리하고 검증 가능한 기록으로 옮기는 작업 흐름의 보조 도구로 사용합니다.&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;대화
-&amp;gt; 문제와 판단 근거 정리
-&amp;gt; 공개 가능 여부 확인
-&amp;gt; Markdown 작성
-&amp;gt; GitHub 변경
-&amp;gt; Pages 배포
-&amp;gt; 실제 결과 검증
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;이 흐름에서 검증은 AI가 정리한 기술 내용과 게시 결과를 나눠 수행합니다. 기술 내용은 코드·테스트·로그로 확인하고, 게시 결과는 공개 URL에서 글·이미지·링크가 의도대로 표시되는지 확인합니다.&lt;/p&gt;

&lt;p&gt;업무 중 겪은 문제를 설명하면 ChatGPT를 통해 다음 형태로 정리할 수 있습니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;문제점&lt;/li&gt;
  &lt;li&gt;원인&lt;/li&gt;
  &lt;li&gt;해결 방법&lt;/li&gt;
  &lt;li&gt;실행 명령어&lt;/li&gt;
  &lt;li&gt;재발 방지 방안&lt;/li&gt;
  &lt;li&gt;공개 가능한 포트폴리오용 글&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;실무에서는 문제를 해결하는 데 집중하다 보면 과정을 남기지 못하는 경우가 많습니다. ChatGPT를 활용하면 대화 내용을 바탕으로 기술 문서, 회고, 체크리스트, 블로그 글 초안으로 빠르게 변환할 수 있습니다. 다만 AI의 답변이나 초안을 그대로 게시하지 않고 검증 가능한 부분을 실제 증거로 확인하며, Git 이력으로 변경 과정을 남깁니다. 업무 내용을 AI에 입력할 때도 고객사명, 내부 주소, 계정·인증정보처럼 공개하면 안 되는 정보는 먼저 제거하거나 일반화합니다.&lt;/p&gt;

&lt;h2 id=&quot;이-블로그에-정리할-내용&quot;&gt;이 블로그에 정리할 내용&lt;/h2&gt;

&lt;p&gt;앞으로 이 블로그에는 다음 내용을 중심으로 정리할 예정입니다.&lt;/p&gt;

&lt;h3 id=&quot;backend&quot;&gt;Backend&lt;/h3&gt;

&lt;p&gt;Java, Spring Boot, JSP, MyBatis, REST API, 인증/권한, 배포 구조와 관련된 내용을 정리합니다.&lt;/p&gt;

&lt;h3 id=&quot;database&quot;&gt;Database&lt;/h3&gt;

&lt;p&gt;PostgreSQL, Oracle, Tibero, SQL 튜닝, 데이터 모델링, 분석 결과와 서비스 테이블을 연결하는 방법을 정리합니다.&lt;/p&gt;

&lt;h3 id=&quot;infrastructure&quot;&gt;Infrastructure&lt;/h3&gt;

&lt;p&gt;Linux, Rocky Linux, Ubuntu, Nginx, Apache, Tomcat, SSL, Podman, Docker, 서버 운영 중 발생한 문제와 해결 과정을 정리합니다.&lt;/p&gt;

&lt;h3 id=&quot;data-systemization&quot;&gt;Data Systemization&lt;/h3&gt;

&lt;p&gt;데이터 분석 결과를 DB, API, 화면, 웹 서비스와 운영 시스템으로 연결하는 과정에서 필요한 개발, 배포, 운영 기준을 정리합니다.&lt;/p&gt;

&lt;h3 id=&quot;project-management&quot;&gt;Project Management&lt;/h3&gt;

&lt;p&gt;PMP 학습, 업무일지, 일일보고, 요구사항 정리, 고객 커뮤니케이션, 분석/개발 범위 조율 경험을 정리합니다.&lt;/p&gt;

&lt;h3 id=&quot;security--audit&quot;&gt;Security &amp;amp; Audit&lt;/h3&gt;

&lt;p&gt;정보보안, CISA, ISMS-P, 내부통제, 보안 점검, 접근통제, 로그 관리와 관련된 학습 및 실무 내용을 정리합니다.&lt;/p&gt;

&lt;h2 id=&quot;작성-원칙&quot;&gt;작성 원칙&lt;/h2&gt;

&lt;p&gt;이 블로그의 글은 다음 기준으로 작성합니다.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;실무 문제를 중심으로 작성합니다.&lt;/li&gt;
  &lt;li&gt;단순 개념 설명보다 실제 문제 해결 과정을 우선합니다.&lt;/li&gt;
  &lt;li&gt;문제점, 원인, 해결, 실행 방법 구조를 기본으로 사용합니다.&lt;/li&gt;
  &lt;li&gt;고객사명, 내부 IP, 계정, 비공개 경로, 민감한 업무 내용은 공개하지 않습니다.&lt;/li&gt;
  &lt;li&gt;특정 프로젝트 경험은 일반화하여 재사용 가능한 지식으로 정리합니다.&lt;/li&gt;
  &lt;li&gt;이력서와 포트폴리오에서 참고할 수 있는 수준의 글을 목표로 합니다.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;목표&quot;&gt;목표&lt;/h2&gt;

&lt;p&gt;이 블로그의 최종 목표는 글을 많이 쓰는 것이 아닙니다.&lt;/p&gt;

&lt;p&gt;실무에서 겪은 문제를 다시 사용할 수 있는 지식으로 만들고, 어떤 방식으로 문제를 분석하고 해결하는 개발자인지 보여주는 것입니다.&lt;/p&gt;

&lt;p&gt;이력서에는 기술 스택과 프로젝트명을 적을 수 있습니다. 하지만 실제로 어떤 문제를 어떻게 해결했는지는 짧은 이력서만으로 설명하기 어렵습니다.&lt;/p&gt;

&lt;p&gt;이 블로그는 그 부족한 부분을 보완하는 공간이 될 것입니다.&lt;/p&gt;

&lt;h2 id=&quot;앞으로의-계획&quot;&gt;앞으로의 계획&lt;/h2&gt;

&lt;p&gt;우선 다음 주제부터 정리할 예정입니다.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Rocky Linux 디스크 마운트 실패 대응&lt;/li&gt;
  &lt;li&gt;Apache VirtualHost 404 장애 분석&lt;/li&gt;
  &lt;li&gt;Nginx와 Tomcat 기반 웹 서비스 운영 구조&lt;/li&gt;
  &lt;li&gt;Podman 기반 CloudBeaver 운영 검토&lt;/li&gt;
  &lt;li&gt;Spring Boot 운영 환경에서 자주 발생하는 문제&lt;/li&gt;
  &lt;li&gt;분석 결과를 웹 서비스로 시스템화할 때 고려할 점&lt;/li&gt;
  &lt;li&gt;PMP와 CISA 학습 내용을 실무 관점으로 정리하는 방법&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;이 블로그는 완성된 지식만 올리는 공간이 아니라, 실무 경험을 정제하고 축적하는 공간으로 운영할 계획입니다.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>git</title>
   <link href="https://son1004007.github.io/study/2022/02/01/git/"/>
   <updated>2022-02-01T00:00:00+00:00</updated>
   <id>https://son1004007.github.io/study/2022/02/01/git</id>
   <content type="html">&lt;h2 id=&quot;내용1-작업-내용&quot;&gt;내용1. 작업 내용&lt;/h2&gt;
&lt;h3 id=&quot;1-최신화현행화-작업&quot;&gt;1. 최신화(현행화) 작업&lt;/h3&gt;
&lt;ol&gt;
  &lt;li&gt;branch checkout&lt;/li&gt;
  &lt;li&gt;reference를 develop branch(기준이 되는 branch)로 선택&lt;/li&gt;
  &lt;li&gt;pull&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;2-cherry-pick-작업&quot;&gt;2. cherry-pick 작업&lt;/h3&gt;
&lt;p&gt;하나의 branch에 여러 사람이 push 하는 환경에서 local에 commit 하고나서 최신화(현행화) 하지 않은 것을 깨달았을때&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;기준이 되는 branch가 변경되고 local에 commit 상태에서 push도 pull도 안돼는 상황&lt;/li&gt;
  &lt;li&gt;commit id를 확인한다. : git log –online 하여 commit id(=7777777)를 확인&lt;/li&gt;
  &lt;li&gt;이전 commit으로 돌아간다(reset) : git reset hard HEAD^&lt;/li&gt;
  &lt;li&gt;최신화(pull)한다. : git pull&lt;/li&gt;
  &lt;li&gt;commit 내용을 복구(cherry-pick)하고 : git cherry-pick 7777777&lt;/li&gt;
  &lt;li&gt;push 한다. : git push 브랜치이름&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;내용2-git-개념--그림-그려서-정리하기&quot;&gt;내용2. git 개념 : 그림 그려서 정리하기&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./git_term.png&quot; alt=&quot;placeholder&quot; title=&quot;git term&quot; /&gt;
&lt;img src=&quot;https://github.com/son1004007/son1004007.github.io/blob/3b2dfecc7a2e07bc07f99fd90d7eb679190ea169/_posts/git_term.png&quot; alt=&quot;placeholder&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;git remote repository : 원격에 있는 깃 저장소&lt;/li&gt;
  &lt;li&gt;git local repository : 내 컴퓨터의 깃 저장소. 코드를 save한 곳과는 별도의 저장소임&lt;/li&gt;
  &lt;li&gt;branch : 1개의 저장소안에서 여러개의 기준점으로 저장소를 나누는 방식&lt;/li&gt;
  &lt;li&gt;master branch : 오픈한 서비스의 코드 저장소&lt;/li&gt;
  &lt;li&gt;develop branch : 오픈전/후 서비스의 코드 저장소&lt;/li&gt;
  &lt;li&gt;create : repository 또는 branch 생성&lt;/li&gt;
  &lt;li&gt;commit : 변경한 코드를 local repository에 저장하기&lt;/li&gt;
  &lt;li&gt;pull : remote repository branch에서 변경된 코드를 저장하기&lt;/li&gt;
  &lt;li&gt;clone : 원격에 생성된 repository 또는 branch를 로컬에 저장&lt;/li&gt;
  &lt;li&gt;push : 변경한 코드 remote repository에 저장하기&lt;/li&gt;
  &lt;li&gt;pull request : 변경한 코드를 다른 branch에 Merge하기 위해 pull 해달라고 요청하는 것&lt;/li&gt;
  &lt;li&gt;merge : 변경한 코드를 합치는 것&lt;/li&gt;
  &lt;li&gt;conflict : 변경한 코드가 같은 부분일때 어떤 것인지 선택해야 하는 것&lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;checkout : 선택한 branch로 이동하는 것&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;rebase : commit id 기준을 다시 잡아 기준 이후의 커밋을 버리고 합치는것&lt;/li&gt;
  &lt;li&gt;fetch : remote repository branch에서 변경된 코드를 가져오기만 하는 것(merge 하지 않음)&lt;/li&gt;
  &lt;li&gt;revert : push한 상태의 commit한 내용을 제거 할때&lt;/li&gt;
  &lt;li&gt;reset : commit head를 변경하는 것&lt;/li&gt;
  &lt;li&gt;amend : commit message 의 내용을 변경하는 것&lt;/li&gt;
  &lt;li&gt;cherry-pick : commit한 내용을 변경할 때&lt;/li&gt;
  &lt;li&gt;head revision : commit id로 이동&lt;/li&gt;
  &lt;li&gt;squash : 여러개의 commit을 1개의 commit으로 묶는 것.&lt;/li&gt;
  &lt;li&gt;tag : commit 한 사람 이름 넣는 정도로 써봄&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;내용3-초급이-사용하는-작업과-형상관리-전문이-하는-작업&quot;&gt;내용3. 초급이 사용하는 작업과 형상관리 전문이 하는 작업&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;초급 : 내용1 + create, commit, pull, clone, push, pull request, merge, conflict, checkout&lt;/li&gt;
  &lt;li&gt;전문 : 모든것&lt;/li&gt;
&lt;/ul&gt;
</content>
 </entry>
 

</feed>
