<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>leemin-jae.log</title>
        <link>https://velog.io/</link>
        <description>초보 개발자</description>
        <lastBuildDate>Mon, 27 Apr 2026 08:33:15 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>leemin-jae.log</title>
            <url>https://velog.velcdn.com/images/leemin-jae/profile/6c08f047-baeb-4fea-bf79-401ebcb2cd7d/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. leemin-jae.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/leemin-jae" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[linux 계정, 권한 정리]]></title>
            <link>https://velog.io/@leemin-jae/linux-%EA%B3%84%EC%A0%95-%EA%B6%8C%ED%95%9C-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@leemin-jae/linux-%EA%B3%84%EC%A0%95-%EA%B6%8C%ED%95%9C-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Mon, 27 Apr 2026 08:33:15 GMT</pubDate>
            <description><![CDATA[<h1 id="linux-계정-권한-정리">linux 계정, 권한 정리</h1>
<h2 id="1-ls--al-출력-구조">1. ls -al 출력 구조</h2>
<pre><code class="language-bash">-rwxr-x--- 1 myaccount mygroup 1234 Apr 27 test.txt</code></pre>
<p><strong>각 항목 의미</strong></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>-rwxr-x---</code></td>
<td>권한</td>
</tr>
<tr>
<td><code>myaccount</code></td>
<td>소유자 (user)</td>
</tr>
<tr>
<td><code>mygroup</code></td>
<td>그룹 (group)</td>
</tr>
<tr>
<td><code>test.txt</code></td>
<td>파일명</td>
</tr>
</tbody></table>
<hr>
<h2 id="2-권한-구조-핵심">2. 권한 구조 (핵심)</h2>
<p>권한은 항상 아래 순서로 나뉜다:</p>
<pre><code class="language-text">rwx | r-x | ---
 u    g     o</code></pre>
<table>
<thead>
<tr>
<th>구분</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>u</code> (user)</td>
<td>소유자</td>
</tr>
<tr>
<td><code>g</code> (group)</td>
<td>그룹</td>
</tr>
<tr>
<td><code>o</code> (others)</td>
<td>나머지</td>
</tr>
</tbody></table>
<hr>
<h2 id="3-권한-종류">3. 권한 종류</h2>
<table>
<thead>
<tr>
<th>권한</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>r</code></td>
<td>읽기</td>
</tr>
<tr>
<td><code>w</code></td>
<td>쓰기 (수정)</td>
</tr>
<tr>
<td><code>x</code></td>
<td>실행</td>
</tr>
</tbody></table>
<hr>
<h2 id="4-권한-적용-기준-매우-중요">4. 권한 적용 기준 (매우 중요)</h2>
<p><strong>권한은 단 하나만 적용된다</strong></p>
<p><strong>우선순위:</strong></p>
<ol>
<li>1순위: user (소유자)</li>
<li>2순위: group</li>
<li>3순위: others</li>
</ol>
<p>내가 어떤 위치에 속하느냐에 따라 권한이 결정됨</p>
<hr>
<h2 id="5-사용자-조건">5. 사용자 조건</h2>
<ul>
<li>내 계정: <code>myaccount</code></li>
<li>내 그룹: <code>mygroup</code></li>
<li>다른 계정: <code>anotheraccount</code></li>
<li>다른 그룹: <code>anothergroup</code></li>
</ul>
<h3 id="케이스별-권한-해석">케이스별 권한 해석</h3>
<p><strong>1. 내가 만든 파일</strong>
<code>-rwxr-x--- myaccount mygroup file.txt</code></p>
<ul>
<li><strong>나 (<code>myaccount</code>)</strong>: user 권한 적용 (<code>rwx</code>)<ul>
<li>읽기 (가능), 수정 (가능), 실행 (가능)</li>
</ul>
</li>
<li><strong>같은 그룹 (<code>mygroup</code>)</strong>: group 권한 적용 (<code>r-x</code>)<ul>
<li>읽기 (가능), 수정 (불가능), 실행 (가능)</li>
</ul>
</li>
<li><strong>다른 사용자</strong>: others (<code>---</code>)<ul>
<li>아무것도 못함 (불가능)</li>
</ul>
</li>
</ul>
<p><strong>2. 내가 만든 파일 (다른 그룹)</strong>
<code>-rwxr-x--- myaccount anothergroup file.txt</code></p>
<ul>
<li><strong>나</strong>: <code>rwx</code> (모든 권한)</li>
<li><strong>내 그룹 (<code>mygroup</code>)</strong>: group 아님 -&gt; others 적용 (<code>---</code>)<ul>
<li>접근 불가</li>
</ul>
</li>
</ul>
<p><strong>3. 다른 사람이 만든 파일 (같은 그룹)</strong>
<code>-rwxr-x--- anotheraccount mygroup file.txt</code></p>
<ul>
<li><strong>나</strong>: group 권한 (<code>r-x</code>)<ul>
<li>읽기 (가능), 수정 (불가능), 실행 (가능)</li>
</ul>
</li>
</ul>
<p><strong>4. 완전히 남의 파일</strong>
<code>-rwxr-x--- anotheraccount anothergroup file.txt</code></p>
<ul>
<li><strong>나</strong>: others (<code>---</code>)<ul>
<li>접근 불가</li>
</ul>
</li>
</ul>
<hr>
<h2 id="6-디렉토리-권한-중요">6. 디렉토리 권한 (중요)</h2>
<p><code>drwxr-x--- myaccount mygroup mydir/</code></p>
<p><strong>디렉토리 권한 의미</strong></p>
<table>
<thead>
<tr>
<th>권한</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td><code>r</code></td>
<td>파일 목록 조회 (<code>ls</code>)</td>
</tr>
<tr>
<td><code>w</code></td>
<td>파일 생성/삭제</td>
</tr>
<tr>
<td><code>x</code></td>
<td>디렉토리 진입 (<code>cd</code>)</td>
</tr>
</tbody></table>
<p><strong>해석</strong></p>
<ul>
<li><strong>user (<code>myaccount</code>)</strong>: <code>rwx</code><ul>
<li><code>ls</code> (가능), <code>cd</code> (가능), 파일 생성/삭제 (가능)</li>
</ul>
</li>
<li><strong>group (<code>mygroup</code>)</strong>: <code>r-x</code><ul>
<li><code>ls</code> (가능), <code>cd</code> (가능), 생성/삭제 (불가능)</li>
</ul>
</li>
<li><strong>others</strong>: <code>---</code><ul>
<li>접근 자체 불가</li>
</ul>
</li>
</ul>
<hr>
<h2 id="7-자주-사용하는-권한-숫자">7. 자주 사용하는 권한 숫자</h2>
<table>
<thead>
<tr>
<th>숫자</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td>755</td>
<td>소유자만 수정, 나머지는 읽기/실행</td>
</tr>
<tr>
<td>644</td>
<td>소유자만 수정, 나머지는 읽기</td>
</tr>
<tr>
<td>700</td>
<td>나만 접근</td>
</tr>
<tr>
<td>777</td>
<td>모두 가능 (매우 위험)</td>
</tr>
</tbody></table>
<hr>
<h2 id="8-권한과-소유자-변경-필수-명령어">8. 권한과 소유자 변경 (필수 명령어)</h2>
<p>권한이나 소유자를 변경할 때는 주로 두 가지 명령어를 사용합니다.</p>
<p><strong>1. 대상의 권한 변경 (<code>chmod</code>)</strong></p>
<ul>
<li><code>chmod 755 file.txt</code> (숫자 방식: 소유자 rwx, 나머지는 r-x)</li>
<li><code>chmod u+x file.txt</code> (기호 방식: 소유자(u)에게 실행(x) 권한 추가)</li>
</ul>
<p><strong>2. 대상의 소유자/그룹 변경 (<code>chown</code>)</strong></p>
<ul>
<li><code>chown myaccount file.txt</code> (소유자 변경)</li>
<li><code>chown myaccount:mygroup file.txt</code> (소유자와 그룹 동시 변경)</li>
<li>보통 소유권을 넘기는 작업은 보안상 슈퍼 유저 권한이 필요하여 보통 <code>sudo</code>와 함께 사용합니다.</li>
</ul>
<hr>
<h2 id="9-절대-권력자-root-계정과-sudo">9. 절대 권력자: root 계정과 sudo</h2>
<ul>
<li><strong>root 계정</strong>: 시스템의 모든 권한을 가진 최고 관리자입니다. 앞서 설명한 <code>rwx</code> 권한을 무시하고 어떤 파일이든 읽고 쓰고 실행할 수 있습니다.</li>
<li><strong>sudo 명령어</strong>: 일반 사용자가 일시적으로 <code>root</code>의 권한을 빌려 올 때 사용합니다. (예: <code>sudo chown ...</code>)</li>
</ul>
<hr>
<h2 id="10-실무-권한-에러-해결-troubleshooting">10. 실무 권한 에러 해결 (Troubleshooting)</h2>
<p><strong>1. &quot;Permission denied&quot; 에러가 날 때 확인법</strong></p>
<ul>
<li>내가 이 파일의 소유자(user)인가? 아니면 그룹(group)에 속해있는가?</li>
<li>디렉토리에 진입(<code>cd</code>)하거나 안의 파일을 읽으려 할 때, 해당 디렉토리에 <strong>실행(<code>x</code>) 권한</strong>이 있는지 확인해보세요. (디렉토리는 <code>x</code>가 있어야 접근 가능합니다)</li>
</ul>
<p><strong>2. 웹 서버(Nginx, Apache) 사용 시 403 Forbidden</strong></p>
<ul>
<li>사용자가 만든 파일을 웹 서버가 읽지 못해서 발생합니다. 해당 프로젝트 폴더의 소유자를 웹 서버 사용자인 <code>www-data</code>(또는 nginx 계정 권한)로 맞추거나, 다른 사용자(others)에게 읽기/실행 권한을 주어야 합니다.</li>
</ul>
<hr>
<h2 id="11-핵심-요약">11. 핵심 요약</h2>
<ul>
<li>권한은 하나만 적용된다.</li>
<li>내가 소유자면 무조건 user 권한이 적용된다.</li>
<li>그룹이면 group 권한이 적용된다.</li>
<li>둘 다 아니면 others가 적용된다.</li>
<li>결국 핵심은 이것 하나: <strong>&quot;나는 이 파일에서 user / group / others 중 어디에 속하는가?&quot;</strong></li>
</ul>
<blockquote>
<p><strong>한줄 정리</strong>
Linux 권한은 <strong>&quot;누가(user/group/others) 무엇을(r/w/x) 할 수 있는지&quot;</strong>를 정의한다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Ollama 정리 - 설치 및 사용]]></title>
            <link>https://velog.io/@leemin-jae/Ollama-%EC%A0%95%EB%A6%AC-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%82%AC%EC%9A%A9</link>
            <guid>https://velog.io/@leemin-jae/Ollama-%EC%A0%95%EB%A6%AC-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%82%AC%EC%9A%A9</guid>
            <pubDate>Mon, 20 Apr 2026 09:13:31 GMT</pubDate>
            <description><![CDATA[<h1 id="ollama-정리---설치-및-사용">Ollama 정리 - 설치 및 사용</h1>
<hr>
<h2 id="목차">목차</h2>
<ol>
<li><a href="#ollama%EB%9E%80">Ollama란?</a></li>
<li><a href="#windows-%EC%84%A4%EC%B9%98">Windows 설치</a></li>
<li><a href="#%EB%AA%A8%EB%8D%B8-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EC%8B%A4%ED%96%89">모델 설치 및 실행</a></li>
<li><a href="#cli-%EC%A3%BC%EC%9A%94-%EB%AA%85%EB%A0%B9%EC%96%B4">CLI 주요 명령어</a></li>
<li><a href="#rest-api-%EC%82%AC%EC%9A%A9%EB%B2%95">REST API 사용법</a></li>
<li><a href="#python-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC">Python 라이브러리</a></li>
<li><a href="#gui-%EC%95%B1-%EC%97%B0%EB%8F%99">GUI 앱 연동</a></li>
<li><a href="#%EC%8B%9C%EC%8A%A4%ED%85%9C-%EC%9A%94%EA%B5%AC%EC%82%AC%ED%95%AD-%EB%B0%8F-gpu-%EA%B0%80%EC%86%8D">시스템 요구사항 및 GPU 가속</a></li>
</ol>
<hr>
<h2 id="ollama란">Ollama란?</h2>
<p>Ollama는 <strong>로컬 환경에서 LLM(대형 언어 모델)을 손쉽게 실행</strong>할 수 있는 오픈소스 도구입니다.</p>
<ul>
<li>인터넷 연결 없이 AI 모델 실행 가능</li>
<li>ChatGPT와 유사한 채팅 인터페이스 제공</li>
<li>REST API로 앱 연동 가능</li>
<li>완전 무료, 데이터가 외부로 나가지 않음</li>
</ul>
<hr>
<h2 id="windows-설치">Windows 설치</h2>
<h3 id="1-설치-파일-다운로드">1. 설치 파일 다운로드</h3>
<p>공식 사이트에서 Windows 설치 파일을 받습니다.</p>
<pre><code>https://ollama.com/download/windows</code></pre><h3 id="2-설치-실행">2. 설치 실행</h3>
<p><code>OllamaSetup.exe</code> 실행 → <strong>Install</strong> 클릭</p>
<blockquote>
<p>💡 Windows Defender 경고가 뜨면 <strong>&quot;추가 정보&quot; → &quot;실행&quot;</strong> 클릭</p>
</blockquote>
<p>설치 완료 후 시스템 트레이(우측 하단)에 라마 아이콘이 생성됩니다.</p>
<h3 id="3-설치-확인">3. 설치 확인</h3>
<p>PowerShell 또는 명령 프롬프트(cmd)를 열고 확인합니다.</p>
<pre><code class="language-powershell">ollama --version</code></pre>
<hr>
<h2 id="모델-설치-및-실행">모델 설치 및 실행</h2>
<h3 id="추천-모델-목록-한국어-지원-포함">추천 모델 목록 (한국어 지원 포함)</h3>
<table>
<thead>
<tr>
<th>모델</th>
<th>크기</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td><code>gemma3:4b</code></td>
<td>3.3GB</td>
<td>입문 추천, 성능 균형</td>
</tr>
<tr>
<td><code>llama3.2:3b</code></td>
<td>2.0GB</td>
<td>경량, 빠른 응답</td>
</tr>
<tr>
<td><code>qwen2.5:7b</code></td>
<td>4.7GB</td>
<td>한국어 성능 강점</td>
</tr>
<tr>
<td><code>mistral:7b</code></td>
<td>4.1GB</td>
<td>영어 성능 강점</td>
</tr>
</tbody></table>
<h3 id="모델-실행-처음-실행-시-자동-다운로드">모델 실행 (처음 실행 시 자동 다운로드)</h3>
<pre><code class="language-powershell">ollama run gemma3:4b</code></pre>
<p>다운로드 완료 후 <code>&gt;&gt;&gt;</code> 프롬프트가 나타나면 바로 대화할 수 있습니다.</p>
<pre><code>&gt;&gt;&gt; 안녕하세요! 파이썬 리스트 컴프리헨션을 설명해줘</code></pre><p>종료하려면 <code>/bye</code> 또는 <code>Ctrl+D</code>를 입력합니다.</p>
<hr>
<h2 id="cli-주요-명령어">CLI 주요 명령어</h2>
<pre><code class="language-powershell"># 설치된 모델 목록 확인
ollama list

# 모델 다운로드만 (실행 없이)
ollama pull llama3.2:3b

# 모델 삭제
ollama rm gemma3:4b

# 현재 실행 중인 모델 확인
ollama ps

# 모델 상세 정보
ollama show gemma3:4b

# API 서버 수동 실행
ollama serve</code></pre>
<hr>
<h2 id="rest-api-사용법">REST API 사용법</h2>
<p>Ollama를 설치하면 <strong>백그라운드에서 자동으로 API 서버가 실행</strong>됩니다.</p>
<ul>
<li>기본 주소: <code>http://localhost:11434</code></li>
<li>별도 설정 없이 바로 호출 가능</li>
</ul>
<h3 id="텍스트-생성-generate">텍스트 생성 (generate)</h3>
<pre><code class="language-bash">curl http://localhost:11434/api/generate \
  -d &#39;{
    &quot;model&quot;: &quot;gemma3:4b&quot;,
    &quot;prompt&quot;: &quot;하늘은 왜 파란가요?&quot;,
    &quot;stream&quot;: false
  }&#39;</code></pre>
<h3 id="채팅-chat">채팅 (chat)</h3>
<pre><code class="language-bash">curl http://localhost:11434/api/chat \
  -d &#39;{
    &quot;model&quot;: &quot;gemma3:4b&quot;,
    &quot;messages&quot;: [
      {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;안녕하세요!&quot;}
    ],
    &quot;stream&quot;: false
  }&#39;</code></pre>
<h3 id="응답-구조">응답 구조</h3>
<pre><code class="language-json">{
  &quot;model&quot;: &quot;gemma3:4b&quot;,
  &quot;message&quot;: {
    &quot;role&quot;: &quot;assistant&quot;,
    &quot;content&quot;: &quot;안녕하세요! 무엇을 도와드릴까요?&quot;
  }
}</code></pre>
<hr>
<h2 id="python-라이브러리">Python 라이브러리</h2>
<h3 id="설치">설치</h3>
<pre><code class="language-bash">pip install ollama</code></pre>
<h3 id="기본-채팅">기본 채팅</h3>
<pre><code class="language-python">import ollama

response = ollama.chat(
    model=&quot;gemma3:4b&quot;,
    messages=[
        {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: &quot;당신은 친절한 AI 도우미입니다.&quot;},
        {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;파이썬 리스트 컴프리헨션을 설명해줘&quot;}
    ]
)

print(response[&quot;message&quot;][&quot;content&quot;])</code></pre>
<h3 id="멀티턴-대화">멀티턴 대화</h3>
<p>이전 메시지를 <code>messages</code> 리스트에 누적해서 넘기면 맥락이 유지됩니다.</p>
<pre><code class="language-python">import ollama

messages = []

def chat(user_input):
    messages.append({&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: user_input})
    response = ollama.chat(model=&quot;gemma3:4b&quot;, messages=messages)
    assistant_msg = response[&quot;message&quot;][&quot;content&quot;]
    messages.append({&quot;role&quot;: &quot;assistant&quot;, &quot;content&quot;: assistant_msg})
    return assistant_msg

print(chat(&quot;내 이름은 홍길동이야&quot;))
print(chat(&quot;내 이름이 뭐라고 했지?&quot;))  # 맥락 유지됨</code></pre>
<h3 id="스트리밍-출력">스트리밍 출력</h3>
<pre><code class="language-python">import ollama

stream = ollama.chat(
    model=&quot;gemma3:4b&quot;,
    messages=[{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;파이썬의 장점 5가지&quot;}],
    stream=True
)

for chunk in stream:
    print(chunk[&quot;message&quot;][&quot;content&quot;], end=&quot;&quot;, flush=True)
print()</code></pre>
<h3 id="단일-프롬프트-생성-generate">단일 프롬프트 생성 (generate)</h3>
<pre><code class="language-python">response = ollama.generate(
    model=&quot;gemma3:4b&quot;,
    prompt=&quot;하늘이 파란 이유를 한 문장으로 설명해줘&quot;,
    system=&quot;간결하게 답변하세요&quot;,
    options={&quot;temperature&quot;: 0.7}
)

print(response[&quot;response&quot;])</code></pre>
<h3 id="텍스트-임베딩-벡터-생성">텍스트 임베딩 (벡터 생성)</h3>
<p>RAG, 유사도 검색, 문서 분류 등에 활용합니다.</p>
<pre><code class="language-python"># 임베딩 전용 모델 먼저 설치 필요
# ollama pull nomic-embed-text

result = ollama.embeddings(
    model=&quot;nomic-embed-text&quot;,
    prompt=&quot;안녕하세요&quot;
)

vector = result[&quot;embedding&quot;]
print(f&quot;벡터 차원 수: {len(vector)}&quot;)</code></pre>
<h3 id="비동기-asyncclient">비동기 (AsyncClient)</h3>
<p>FastAPI, aiohttp 등 비동기 웹 프레임워크에서 사용합니다.</p>
<pre><code class="language-python">import asyncio
import ollama

async def main():
    client = ollama.AsyncClient()
    response = await client.chat(
        model=&quot;gemma3:4b&quot;,
        messages=[{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;안녕!&quot;}]
    )
    print(response[&quot;message&quot;][&quot;content&quot;])

asyncio.run(main())</code></pre>
<h3 id="모델-관리">모델 관리</h3>
<pre><code class="language-python">import ollama

# 설치된 모델 목록
models = ollama.list()
for m in models[&quot;models&quot;]:
    print(m[&quot;name&quot;], m[&quot;size&quot;])

# 모델 다운로드
ollama.pull(&quot;llama3.2:3b&quot;)

# 모델 정보 조회
info = ollama.show(&quot;gemma3:4b&quot;)
print(info[&quot;modelfile&quot;])

# 모델 삭제
ollama.delete(&quot;gemma3:4b&quot;)</code></pre>
<h3 id="원격-서버-연결">원격 서버 연결</h3>
<pre><code class="language-python">import ollama

# 기본값은 http://localhost:11434
client = ollama.Client(host=&quot;http://192.168.1.100:11434&quot;)

response = client.chat(
    model=&quot;gemma3:4b&quot;,
    messages=[{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;테스트&quot;}]
)
print(response[&quot;message&quot;][&quot;content&quot;])</code></pre>
<hr>
<h2 id="gui-앱-연동">GUI 앱 연동</h2>
<h3 id="open-webui-추천">Open WebUI (추천)</h3>
<p>ChatGPT와 유사한 웹 인터페이스를 제공합니다. Docker가 필요합니다.</p>
<pre><code class="language-powershell">docker run -d -p 3000:8080 `
  --add-host=host.docker.internal:host-gateway `
  -v open-webui:/app/backend/data `
  ghcr.io/open-webui/open-webui:main</code></pre>
<p>설치 후 브라우저에서 <code>http://localhost:3000</code> 접속</p>
<h3 id="vs-code-연동-continue-확장">VS Code 연동 (Continue 확장)</h3>
<ol>
<li>VS Code Marketplace에서 <strong>Continue</strong> 확장 설치</li>
<li><code>config.json</code>에서 Ollama 모델 지정</li>
<li>코드 자동완성, 채팅 등 GitHub Copilot처럼 사용 가능</li>
</ol>
<hr>
<h2 id="시스템-요구사항-및-gpu-가속">시스템 요구사항 및 GPU 가속</h2>
<h3 id="권장-사양">권장 사양</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>최소</th>
<th>권장</th>
</tr>
</thead>
<tbody><tr>
<td>RAM</td>
<td>8GB</td>
<td>16GB (7B 모델 기준)</td>
</tr>
<tr>
<td>저장소</td>
<td>모델당 2~8GB</td>
<td>SSD 권장</td>
</tr>
<tr>
<td>OS</td>
<td>Windows 10 64-bit</td>
<td>Windows 11</td>
</tr>
<tr>
<td>GPU</td>
<td>없어도 동작 (CPU)</td>
<td>NVIDIA GPU (CUDA)</td>
</tr>
</tbody></table>
<h3 id="gpu-가속">GPU 가속</h3>
<p>NVIDIA GPU가 있으면 설치 즉시 <strong>CUDA 가속이 자동 적용</strong>됩니다. 별도 설정이 필요 없습니다.</p>
<blockquote>
<p>💡 CPU 대비 <strong>5~20배</strong> 빠른 응답 속도를 경험할 수 있습니다.</p>
</blockquote>
<p>GPU 사용 여부를 확인하려면:</p>
<pre><code class="language-powershell">ollama ps</code></pre>
<p>실행 중인 모델 옆에 GPU 사용량이 표시됩니다.</p>
<hr>
<h2 id="마무리">마무리</h2>
<p>Ollama는 로컬 AI 환경을 구성하는 가장 빠른 방법입니다. Python 라이브러리와 REST API를 통해 다양한 애플리케이션에 LLM을 손쉽게 연동할 수 있고, Open WebUI를 통해 ChatGPT와 동일한 사용 경험도 얻을 수 있습니다.</p>
<hr>
<p><em>참고: <a href="https://ollama.com">Ollama 공식 문서</a> | <a href="https://github.com/ollama/ollama">Ollama GitHub</a></em></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Ollama 정리]]></title>
            <link>https://velog.io/@leemin-jae/Ollama-%EC%A0%95%EB%A6%AC-a9qq8oqy</link>
            <guid>https://velog.io/@leemin-jae/Ollama-%EC%A0%95%EB%A6%AC-a9qq8oqy</guid>
            <pubDate>Mon, 20 Apr 2026 08:41:51 GMT</pubDate>
            <description><![CDATA[<h1 id="ollama-정리">Ollama 정리</h1>
<blockquote>
<p>로컬 환경에서 LLM을 손쉽게 실행할 수 있는 Ollama의 모든 것을 정리했습니다.</p>
</blockquote>
<hr>
<h2 id="목차">목차</h2>
<ol>
<li><a href="#1-ollama%EB%9E%80">Ollama란?</a></li>
<li><a href="#2-%EC%A3%BC%EC%9A%94-%EA%B8%B0%EB%8A%A5-%EC%A0%95%EB%A6%AC">주요 기능 정리</a></li>
<li><a href="#3-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95-%EC%8B%9C-%EC%A3%BC%EC%9D%98%EC%A0%90">환경 구축 시 주의점</a></li>
<li><a href="#4-%ED%99%98%EA%B2%BD-%EB%B3%80%EC%88%98-%EC%A0%84%EC%B2%B4-%EC%A0%95%EB%A6%AC">환경 변수 전체 정리</a></li>
<li><a href="#5-ollama-vs-vllm-%EB%B9%84%EA%B5%90">Ollama vs vLLM 비교</a></li>
</ol>
<hr>
<h2 id="1-ollama란">1. Ollama란?</h2>
<p><strong>Ollama</strong>는 로컬 환경(내 컴퓨터)에서 대형 언어 모델(LLM)을 쉽게 실행할 수 있게 해주는 오픈소스 도구입니다.</p>
<p>클라우드 기반 AI(ChatGPT, Claude 등)는 인터넷을 통해 원격 서버에서 실행되지만, Ollama를 사용하면 <strong>인터넷 없이 내 PC나 서버에서 직접</strong> AI 모델을 돌릴 수 있습니다.</p>
<h3 id="주요-특징">주요 특징</h3>
<ul>
<li><strong>로컬 실행</strong> — 데이터가 외부로 나가지 않아 프라이버시 보호</li>
<li><strong>간편한 설치</strong> — 명령어 한 줄로 모델 다운로드 및 실행</li>
<li><strong>다양한 모델 지원</strong> — Llama 3, Mistral, Gemma, Qwen, Phi 등 수백 가지 모델</li>
<li><strong>OpenAI 호환 API</strong> — 기존 앱에 쉽게 통합 가능</li>
<li><strong>무료 &amp; 오픈소스</strong> — 사용 비용 없음</li>
<li><strong>macOS / Windows / Linux</strong> 모두 지원</li>
</ul>
<h3 id="기본-사용법">기본 사용법</h3>
<pre><code class="language-bash"># 모델 실행 (없으면 자동 다운로드)
ollama run llama3

# 모델 목록 확인
ollama list

# 백그라운드 서버 실행
ollama serve</code></pre>
<h3 id="한계">한계</h3>
<ul>
<li><strong>고성능 GPU/RAM 필요</strong> — 큰 모델일수록 더 많은 하드웨어 요구</li>
<li><strong>클라우드 최신 모델 수준은 아님</strong> — GPT-4, Claude Opus 같은 최상위 모델은 로컬 실행 불가</li>
<li><strong>초기 다운로드</strong> — 모델 파일이 수 GB에 달함</li>
</ul>
<hr>
<h2 id="2-주요-기능-정리">2. 주요 기능 정리</h2>
<h3 id="2-1-모델-관리">2-1. 모델 관리</h3>
<table>
<thead>
<tr>
<th>명령어</th>
<th>기능</th>
</tr>
</thead>
<tbody><tr>
<td><code>ollama pull &lt;모델명&gt;</code></td>
<td>모델 다운로드</td>
</tr>
<tr>
<td><code>ollama push &lt;모델명&gt;</code></td>
<td>모델 업로드 (레지스트리)</td>
</tr>
<tr>
<td><code>ollama list</code></td>
<td>설치된 모델 목록</td>
</tr>
<tr>
<td><code>ollama rm &lt;모델명&gt;</code></td>
<td>모델 삭제</td>
</tr>
<tr>
<td><code>ollama show &lt;모델명&gt;</code></td>
<td>모델 상세 정보 확인</td>
</tr>
<tr>
<td><code>ollama cp &lt;원본&gt; &lt;복사본&gt;</code></td>
<td>모델 복사</td>
</tr>
</tbody></table>
<h3 id="2-2-모델-실행">2-2. 모델 실행</h3>
<pre><code class="language-bash">ollama run llama3          # 대화형 실행
ollama run llama3 &quot;질문&quot;   # 단발성 질문
ollama run llama3:70b      # 특정 버전(태그) 지정</code></pre>
<ul>
<li><strong>대화형 모드</strong> — 터미널에서 채팅처럼 사용</li>
<li><strong>파이프 입력</strong> — <code>echo &quot;요약해줘&quot; | ollama run llama3</code></li>
<li><strong>멀티모달</strong> — 이미지 입력 지원 모델 사용 가능 (예: <code>llava</code>)</li>
</ul>
<h3 id="2-3-rest-api-서버">2-3. REST API 서버</h3>
<p><code>ollama serve</code> 실행 시 <strong>로컬 HTTP API</strong> 자동 제공 (기본 포트: <code>11434</code>)</p>
<pre><code class="language-bash"># 텍스트 생성
POST http://localhost:11434/api/generate

# 채팅
POST http://localhost:11434/api/chat

# 임베딩 생성
POST http://localhost:11434/api/embeddings

# 모델 목록
GET  http://localhost:11434/api/tags</code></pre>
<blockquote>
<p><strong>OpenAI 호환 엔드포인트</strong>도 지원 → 기존 OpenAI SDK 코드를 거의 그대로 사용 가능</p>
</blockquote>
<h3 id="2-4-modelfile-커스텀-모델-생성">2-4. Modelfile (커스텀 모델 생성)</h3>
<p>Docker의 <code>Dockerfile</code>처럼, 모델을 커스터마이징할 수 있는 설정 파일입니다.</p>
<pre><code class="language-dockerfile">FROM llama3

# 시스템 프롬프트 설정
SYSTEM &quot;당신은 한국어 전문 번역가입니다.&quot;

# 파라미터 조정
PARAMETER temperature 0.7
PARAMETER top_p 0.9
PARAMETER num_ctx 4096</code></pre>
<pre><code class="language-bash">ollama create my-model -f Modelfile</code></pre>
<h3 id="2-5-지원-모델-주요">2-5. 지원 모델 (주요)</h3>
<table>
<thead>
<tr>
<th>모델</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Llama 3.x</strong></td>
<td>Meta, 범용</td>
</tr>
<tr>
<td><strong>Mistral / Mixtral</strong></td>
<td>유럽산, 경량 고성능</td>
</tr>
<tr>
<td><strong>Gemma 3</strong></td>
<td>Google</td>
</tr>
<tr>
<td><strong>Qwen 2.5</strong></td>
<td>Alibaba, 한국어 우수</td>
</tr>
<tr>
<td><strong>Phi-4</strong></td>
<td>Microsoft, 소형 고성능</td>
</tr>
<tr>
<td><strong>DeepSeek</strong></td>
<td>추론 특화</td>
</tr>
<tr>
<td><strong>LLaVA</strong></td>
<td>이미지 이해 (멀티모달)</td>
</tr>
<tr>
<td><strong>nomic-embed</strong></td>
<td>텍스트 임베딩 전용</td>
</tr>
</tbody></table>
<h3 id="2-6-외부-도구-연동">2-6. 외부 도구 연동</h3>
<table>
<thead>
<tr>
<th>도구</th>
<th>연동 방식</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Open WebUI</strong></td>
<td>웹 기반 ChatGPT 스타일 UI</td>
</tr>
<tr>
<td><strong>Continue (VS Code)</strong></td>
<td>코드 자동완성</td>
</tr>
<tr>
<td><strong>LangChain / LlamaIndex</strong></td>
<td>AI 앱 개발 프레임워크</td>
</tr>
<tr>
<td><strong>Dify / Flowise</strong></td>
<td>노코드 AI 워크플로우</td>
</tr>
<tr>
<td><strong>AnythingLLM</strong></td>
<td>로컬 RAG 시스템</td>
</tr>
</tbody></table>
<h3 id="2-7-하드웨어-가속">2-7. 하드웨어 가속</h3>
<table>
<thead>
<tr>
<th>환경</th>
<th>지원</th>
</tr>
</thead>
<tbody><tr>
<td>NVIDIA GPU (CUDA)</td>
<td>✅</td>
</tr>
<tr>
<td>AMD GPU (ROCm)</td>
<td>✅</td>
</tr>
<tr>
<td>Apple Silicon (Metal)</td>
<td>✅</td>
</tr>
<tr>
<td>CPU 전용</td>
<td>✅ (느림)</td>
</tr>
</tbody></table>
<hr>
<h2 id="3-환경-구축-시-주의점">3. 환경 구축 시 주의점</h2>
<h3 id="3-1-하드웨어-요구사항">3-1. 하드웨어 요구사항</h3>
<h4 id="ram-가장-중요">RAM (가장 중요)</h4>
<table>
<thead>
<tr>
<th>모델 크기</th>
<th>최소 RAM</th>
<th>권장 RAM</th>
</tr>
</thead>
<tbody><tr>
<td>1B ~ 3B</td>
<td>4GB</td>
<td>8GB</td>
</tr>
<tr>
<td>7B ~ 8B</td>
<td>8GB</td>
<td>16GB</td>
</tr>
<tr>
<td>13B ~ 14B</td>
<td>16GB</td>
<td>32GB</td>
</tr>
<tr>
<td>30B ~ 34B</td>
<td>32GB</td>
<td>64GB</td>
</tr>
<tr>
<td>70B+</td>
<td>64GB</td>
<td>128GB+</td>
</tr>
</tbody></table>
<blockquote>
<p>⚠️ <strong>RAM 부족 시</strong> 모델이 디스크로 스왑 → 속도 극단적으로 느려짐</p>
</blockquote>
<h4 id="gpu-vram">GPU VRAM</h4>
<pre><code>VRAM ≥ 모델 크기(GB)  →  전체 GPU 실행 (빠름)
VRAM &lt; 모델 크기      →  일부 CPU 분산 실행 (느려짐)
GPU 없음              →  CPU 전용 실행 (매우 느림)</code></pre><h4 id="디스크-공간">디스크 공간</h4>
<table>
<thead>
<tr>
<th>모델</th>
<th>용량</th>
</tr>
</thead>
<tbody><tr>
<td>7B 모델 (Q4)</td>
<td>~4GB</td>
</tr>
<tr>
<td>13B 모델 (Q4)</td>
<td>~8GB</td>
</tr>
<tr>
<td>70B 모델 (Q4)</td>
<td>~40GB</td>
</tr>
</tbody></table>
<blockquote>
<p>⚠️ 여러 모델 사용 시 <strong>수백 GB</strong>까지 증가 가능 → SSD 권장</p>
</blockquote>
<h3 id="3-2-설치-환경별-주의점">3-2. 설치 환경별 주의점</h3>
<h4 id="windows">Windows</h4>
<pre><code>✅ Windows 10/11 (64bit) 지원
⚠️ NVIDIA GPU 사용 시 → CUDA 드라이버 최신 버전 필수
⚠️ AMD GPU → ROCm이 Windows에서 불안정할 수 있음
⚠️ WSL2 환경에서 실행 시 GPU 인식 문제 발생 가능</code></pre><h4 id="macos">macOS</h4>
<pre><code>✅ Apple Silicon (M1/M2/M3/M4) → Metal 가속, 성능 우수
⚠️ Intel Mac → CPU 실행만 가능, 속도 매우 느림
⚠️ macOS 11 Big Sur 이상 필요</code></pre><h4 id="linux">Linux</h4>
<pre><code>✅ 가장 안정적인 환경
⚠️ NVIDIA → nvidia-driver + CUDA toolkit 사전 설치 필요
⚠️ AMD → ROCm 버전과 GPU 모델 호환성 확인 필수
⚠️ 방화벽 설정 확인 (포트 11434)</code></pre><h3 id="3-3-네트워크--보안">3-3. 네트워크 &amp; 보안</h3>
<pre><code class="language-bash"># 기본값: 로컬에서만 접근 가능 (안전)
OLLAMA_HOST=127.0.0.1:11434

# 외부 허용 시 → 인증 없이 누구나 접근 가능!
OLLAMA_HOST=0.0.0.0:11434   # ⚠️ 위험</code></pre>
<p>외부 노출 시 필수 조치:</p>
<ul>
<li>✅ 방화벽으로 IP 화이트리스트 설정</li>
<li>✅ Nginx / Caddy로 리버스 프록시 + 인증 추가</li>
<li>✅ API Key 인증 레이어 별도 구성</li>
<li>✅ HTTPS 적용</li>
<li>❌ 포트를 그냥 공개하면 절대 안 됨</li>
</ul>
<h3 id="3-4-모델-양자화quantization-이해">3-4. 모델 양자화(Quantization) 이해</h3>
<pre><code>Q2  → 매우 작음, 품질 낮음
Q4  → 균형 (일반적으로 권장) ✅
Q5  → 품질 좋음, 용량 큼
Q8  → 원본에 가까움, 매우 큰 용량
F16 → 원본, 전문 용도</code></pre><pre><code class="language-bash">ollama run llama3          # 기본 (최신 태그)
ollama run llama3:8b       # 8B 파라미터
ollama run llama3:8b-q4_0  # 8B + Q4 양자화 명시</code></pre>
<blockquote>
<p>⚠️ 태그 미지정 시 자동으로 특정 버전이 선택됨 → 의도치 않은 대용량 다운로드 주의</p>
</blockquote>
<h3 id="3-5-자주-발생하는-문제">3-5. 자주 발생하는 문제</h3>
<table>
<thead>
<tr>
<th>증상</th>
<th>원인</th>
<th>해결</th>
</tr>
</thead>
<tbody><tr>
<td><code>error: model not found</code></td>
<td>모델명 오타</td>
<td><code>ollama list</code> 로 확인</td>
</tr>
<tr>
<td>응답이 극도로 느림</td>
<td>GPU 미인식, CPU 실행 중</td>
<td>드라이버 재설치, <code>ollama ps</code>로 확인</td>
</tr>
<tr>
<td><code>out of memory</code></td>
<td>RAM/VRAM 부족</td>
<td>더 작은 모델 또는 낮은 양자화 사용</td>
</tr>
<tr>
<td>포트 충돌</td>
<td>11434 이미 사용 중</td>
<td><code>OLLAMA_HOST</code> 포트 변경</td>
</tr>
<tr>
<td>모델 다운로드 실패</td>
<td>네트워크 / 디스크 부족</td>
<td>디스크 여유 확인, 재시도</td>
</tr>
</tbody></table>
<h3 id="3-6-구축-전-체크리스트">3-6. 구축 전 체크리스트</h3>
<pre><code>□ RAM 16GB 이상 확보 (7B 모델 기준)
□ 디스크 여유 공간 50GB 이상
□ GPU 드라이버 최신 버전 설치
□ 외부 접근 시 인증/방화벽 설정 계획
□ 모델 저장 경로 (용량 큰 드라이브) 지정
□ 운영 목적에 맞는 모델 &amp; 양자화 수준 결정</code></pre><hr>
<h2 id="4-환경-변수-전체-정리">4. 환경 변수 전체 정리</h2>
<h3 id="4-1-서버--네트워크">4-1. 서버 &amp; 네트워크</h3>
<table>
<thead>
<tr>
<th>환경 변수</th>
<th>기본값</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>OLLAMA_HOST</code></td>
<td><code>127.0.0.1:11434</code></td>
<td>바인딩 주소 및 포트</td>
</tr>
<tr>
<td><code>OLLAMA_ORIGINS</code></td>
<td><code>*</code></td>
<td>CORS 허용 출처</td>
</tr>
<tr>
<td><code>OLLAMA_PROXY</code></td>
<td>-</td>
<td>HTTP 프록시 서버 주소</td>
</tr>
</tbody></table>
<pre><code class="language-bash">OLLAMA_HOST=0.0.0.0:11434          # 외부 접근 허용
OLLAMA_HOST=192.168.1.100:11434    # 특정 IP 바인딩
OLLAMA_ORIGINS=https://myapp.com   # 특정 도메인만 허용
OLLAMA_PROXY=http://proxy:8080     # 프록시 경유</code></pre>
<h3 id="4-2-모델--파일-경로">4-2. 모델 &amp; 파일 경로</h3>
<table>
<thead>
<tr>
<th>환경 변수</th>
<th>기본값</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>OLLAMA_MODELS</code></td>
<td><code>~/.ollama/models</code></td>
<td>모델 저장 경로</td>
</tr>
<tr>
<td><code>OLLAMA_TMPDIR</code></td>
<td>시스템 임시 폴더</td>
<td>임시 파일 경로</td>
</tr>
</tbody></table>
<pre><code class="language-bash">OLLAMA_MODELS=/mnt/ssd/ollama-models   # 대용량 드라이브로 변경
OLLAMA_MODELS=D:\ollama\models         # Windows 예시
OLLAMA_TMPDIR=/tmp/ollama              # 임시 파일 위치 지정</code></pre>
<h3 id="4-3-성능--메모리">4-3. 성능 &amp; 메모리</h3>
<table>
<thead>
<tr>
<th>환경 변수</th>
<th>기본값</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>OLLAMA_NUM_PARALLEL</code></td>
<td><code>1</code></td>
<td>동시 처리 요청 수</td>
</tr>
<tr>
<td><code>OLLAMA_MAX_LOADED_MODELS</code></td>
<td><code>1</code></td>
<td>메모리에 올릴 최대 모델 수</td>
</tr>
<tr>
<td><code>OLLAMA_MAX_QUEUE</code></td>
<td><code>512</code></td>
<td>대기열 최대 요청 수</td>
</tr>
<tr>
<td><code>OLLAMA_KEEP_ALIVE</code></td>
<td><code>5m</code></td>
<td>모델 메모리 유지 시간</td>
</tr>
<tr>
<td><code>OLLAMA_MAX_VRAM</code></td>
<td>-</td>
<td>사용할 최대 VRAM (bytes)</td>
</tr>
</tbody></table>
<pre><code class="language-bash">OLLAMA_NUM_PARALLEL=4          # 동시 4개 요청 처리
OLLAMA_MAX_LOADED_MODELS=2     # 최대 2개 모델 상주
OLLAMA_KEEP_ALIVE=10m          # 10분간 메모리 유지
OLLAMA_KEEP_ALIVE=-1           # 영구 메모리 유지
OLLAMA_KEEP_ALIVE=0            # 요청 후 즉시 메모리 해제
OLLAMA_MAX_VRAM=4000000000     # VRAM 4GB 제한</code></pre>
<h3 id="4-4-gpu-설정">4-4. GPU 설정</h3>
<table>
<thead>
<tr>
<th>환경 변수</th>
<th>기본값</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>OLLAMA_GPU_OVERHEAD</code></td>
<td><code>0</code></td>
<td>GPU 메모리 예비 확보량 (bytes)</td>
</tr>
<tr>
<td><code>CUDA_VISIBLE_DEVICES</code></td>
<td>-</td>
<td>사용할 NVIDIA GPU 지정</td>
</tr>
<tr>
<td><code>ROCR_VISIBLE_DEVICES</code></td>
<td>-</td>
<td>사용할 AMD GPU 지정</td>
</tr>
<tr>
<td><code>OLLAMA_CPU_ONLY</code></td>
<td><code>false</code></td>
<td>GPU 무시, CPU만 사용</td>
</tr>
</tbody></table>
<pre><code class="language-bash">CUDA_VISIBLE_DEVICES=0             # 첫 번째 GPU만 사용
CUDA_VISIBLE_DEVICES=0,1           # 0번, 1번 GPU 사용
ROCR_VISIBLE_DEVICES=0             # AMD GPU 0번 지정
OLLAMA_GPU_OVERHEAD=512000000      # GPU 메모리 512MB 예비
OLLAMA_CPU_ONLY=true               # CPU 전용 강제</code></pre>
<h3 id="4-5-디버그--로깅">4-5. 디버그 &amp; 로깅</h3>
<table>
<thead>
<tr>
<th>환경 변수</th>
<th>기본값</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>OLLAMA_DEBUG</code></td>
<td><code>false</code></td>
<td>디버그 로그 활성화</td>
</tr>
<tr>
<td><code>OLLAMA_NOPRUNE</code></td>
<td><code>false</code></td>
<td>모델 레이어 캐시 유지</td>
</tr>
</tbody></table>
<pre><code class="language-bash">OLLAMA_DEBUG=1      # 상세 로그 출력
OLLAMA_NOPRUNE=1    # 캐시 삭제 방지</code></pre>
<h3 id="4-6-설정-방법">4-6. 설정 방법</h3>
<h4 id="linux--macos--systemd-영구-설정">Linux / macOS — systemd 영구 설정</h4>
<pre><code class="language-bash">sudo systemctl edit ollama.service</code></pre>
<pre><code class="language-ini">[Service]
Environment=&quot;OLLAMA_HOST=0.0.0.0:11434&quot;
Environment=&quot;OLLAMA_MODELS=/mnt/ssd/models&quot;
Environment=&quot;OLLAMA_NUM_PARALLEL=4&quot;
Environment=&quot;OLLAMA_KEEP_ALIVE=10m&quot;</code></pre>
<pre><code class="language-bash">sudo systemctl daemon-reload
sudo systemctl restart ollama</code></pre>
<h4 id="windows--powershell-영구-설정">Windows — PowerShell 영구 설정</h4>
<pre><code class="language-powershell">[System.Environment]::SetEnvironmentVariable(
  &quot;OLLAMA_HOST&quot;, &quot;0.0.0.0:11434&quot;, &quot;User&quot;
)
[System.Environment]::SetEnvironmentVariable(
  &quot;OLLAMA_MODELS&quot;, &quot;D:\ollama\models&quot;, &quot;User&quot;
)
# 설정 후 Ollama 재시작 필요</code></pre>
<h4 id="docker-compose">Docker Compose</h4>
<pre><code class="language-yaml">services:
  ollama:
    image: ollama/ollama
    environment:
      - OLLAMA_HOST=0.0.0.0:11434
      - OLLAMA_MODELS=/root/.ollama/models
      - OLLAMA_NUM_PARALLEL=4
      - OLLAMA_KEEP_ALIVE=10m
      - OLLAMA_MAX_LOADED_MODELS=2
    volumes:
      - ollama_data:/root/.ollama</code></pre>
<h3 id="4-7-상황별-추천-설정">4-7. 상황별 추천 설정</h3>
<h4 id="개인-pc-ram-16gb-gpu-8gb">개인 PC (RAM 16GB, GPU 8GB)</h4>
<pre><code class="language-bash">OLLAMA_NUM_PARALLEL=1
OLLAMA_MAX_LOADED_MODELS=1
OLLAMA_KEEP_ALIVE=5m</code></pre>
<h4 id="팀-서버-ram-64gb-gpu-24gb">팀 서버 (RAM 64GB, GPU 24GB)</h4>
<pre><code class="language-bash">OLLAMA_NUM_PARALLEL=4
OLLAMA_MAX_LOADED_MODELS=3
OLLAMA_KEEP_ALIVE=30m
OLLAMA_HOST=0.0.0.0:11434</code></pre>
<h4 id="메모리-절약-모드">메모리 절약 모드</h4>
<pre><code class="language-bash">OLLAMA_MAX_LOADED_MODELS=1
OLLAMA_KEEP_ALIVE=0       # 사용 후 즉시 메모리 해제
OLLAMA_NUM_PARALLEL=1</code></pre>
<h4 id="디버깅--문제-해결">디버깅 / 문제 해결</h4>
<pre><code class="language-bash">OLLAMA_DEBUG=1
OLLAMA_CPU_ONLY=true      # GPU 문제 우회</code></pre>
<hr>
<h2 id="5-ollama-vs-vllm-비교">5. Ollama vs vLLM 비교</h2>
<h3 id="5-1-한눈에-보기">5-1. 한눈에 보기</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>Ollama</th>
<th>vLLM</th>
</tr>
</thead>
<tbody><tr>
<td><strong>목적</strong></td>
<td>개인/소규모 로컬 실행</td>
<td>고성능 프로덕션 서빙</td>
</tr>
<tr>
<td><strong>대상 사용자</strong></td>
<td>개발자, 개인</td>
<td>ML 엔지니어, 기업</td>
</tr>
<tr>
<td><strong>설치 난이도</strong></td>
<td>⭐ 매우 쉬움</td>
<td>⭐⭐⭐ 복잡</td>
</tr>
<tr>
<td><strong>성능</strong></td>
<td>보통</td>
<td>매우 높음</td>
</tr>
<tr>
<td><strong>GPU 요구사항</strong></td>
<td>선택 (CPU도 가능)</td>
<td>사실상 필수</td>
</tr>
<tr>
<td><strong>Windows 지원</strong></td>
<td>✅</td>
<td>❌ (Linux 전용)</td>
</tr>
<tr>
<td><strong>라이선스</strong></td>
<td>MIT</td>
<td>Apache 2.0</td>
</tr>
</tbody></table>
<h3 id="5-2-아키텍처-차이">5-2. 아키텍처 차이</h3>
<h4 id="ollama">Ollama</h4>
<pre><code>사용자 → Ollama 서버 → llama.cpp 엔진 → 모델 실행
                          ↓
                    CPU / GPU 혼합 실행 가능
                    양자화(Q4 등) 기본 지원</code></pre><h4 id="vllm">vLLM</h4>
<pre><code>사용자 → vLLM 서버 → PagedAttention 엔진 → GPU 실행
                          ↓
                    GPU VRAM 최적화 특화
                    FP16 / BF16 정밀도 중심</code></pre><blockquote>
<p><strong>핵심 차이</strong>: Ollama는 <code>llama.cpp</code> 기반, vLLM은 자체 <strong>PagedAttention</strong> 기술 기반</p>
</blockquote>
<h3 id="5-3-주요-기능-비교">5-3. 주요 기능 비교</h3>
<table>
<thead>
<tr>
<th>기능</th>
<th>Ollama</th>
<th>vLLM</th>
</tr>
</thead>
<tbody><tr>
<td><strong>PagedAttention</strong></td>
<td>❌</td>
<td>✅ (핵심 기술)</td>
</tr>
<tr>
<td><strong>Continuous Batching</strong></td>
<td>제한적</td>
<td>✅</td>
</tr>
<tr>
<td><strong>양자화 (Q4/Q8)</strong></td>
<td>✅ 기본 지원</td>
<td>✅ (AWQ, GPTQ 등)</td>
</tr>
<tr>
<td><strong>멀티 GPU</strong></td>
<td>제한적</td>
<td>✅ 완전 지원</td>
</tr>
<tr>
<td><strong>Tensor Parallelism</strong></td>
<td>❌</td>
<td>✅</td>
</tr>
<tr>
<td><strong>모델 커스터마이징</strong></td>
<td>✅ Modelfile</td>
<td>❌</td>
</tr>
<tr>
<td><strong>OpenAI 호환 API</strong></td>
<td>✅</td>
<td>✅</td>
</tr>
<tr>
<td><strong>스트리밍</strong></td>
<td>✅</td>
<td>✅</td>
</tr>
<tr>
<td><strong>멀티모달</strong></td>
<td>✅</td>
<td>✅</td>
</tr>
<tr>
<td><strong>LoRA 어댑터</strong></td>
<td>❌</td>
<td>✅</td>
</tr>
<tr>
<td><strong>CPU 실행</strong></td>
<td>✅</td>
<td>❌</td>
</tr>
<tr>
<td><strong>Apple Silicon</strong></td>
<td>✅</td>
<td>❌</td>
</tr>
</tbody></table>
<h3 id="5-4-지원-모델-형식">5-4. 지원 모델 형식</h3>
<table>
<thead>
<tr>
<th>형식</th>
<th>Ollama</th>
<th>vLLM</th>
</tr>
</thead>
<tbody><tr>
<td><strong>GGUF</strong></td>
<td>✅ 기본</td>
<td>❌</td>
</tr>
<tr>
<td><strong>GPTQ</strong></td>
<td>❌</td>
<td>✅</td>
</tr>
<tr>
<td><strong>AWQ</strong></td>
<td>❌</td>
<td>✅</td>
</tr>
<tr>
<td><strong>HuggingFace</strong></td>
<td>제한적</td>
<td>✅ 완전 지원</td>
</tr>
<tr>
<td><strong>SafeTensors</strong></td>
<td>❌</td>
<td>✅</td>
</tr>
</tbody></table>
<h3 id="5-5-설치-및-실행-비교">5-5. 설치 및 실행 비교</h3>
<h4 id="ollama-1">Ollama</h4>
<pre><code class="language-bash"># 설치 (한 줄)
curl -fsSL https://ollama.com/install.sh | sh

# 실행
ollama run llama3</code></pre>
<h4 id="vllm-1">vLLM</h4>
<pre><code class="language-bash"># 설치
pip install vllm

# CUDA, Python 버전 호환성 확인 필요
# NVIDIA GPU 필수

# 실행
python -m vllm.entrypoints.openai.api_server \
  --model meta-llama/Llama-3-8B-Instruct \
  --tensor-parallel-size 2</code></pre>
<h3 id="5-6-하드웨어-요구사항">5-6. 하드웨어 요구사항</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>Ollama</th>
<th>vLLM</th>
</tr>
</thead>
<tbody><tr>
<td><strong>OS</strong></td>
<td>Win / Mac / Linux</td>
<td>Linux 전용</td>
</tr>
<tr>
<td><strong>GPU</strong></td>
<td>선택 사항</td>
<td>NVIDIA GPU 필수</td>
</tr>
<tr>
<td><strong>CUDA</strong></td>
<td>선택</td>
<td>필수 (11.8+)</td>
</tr>
<tr>
<td><strong>최소 VRAM</strong></td>
<td>제한 없음</td>
<td>8GB+ 권장</td>
</tr>
<tr>
<td><strong>Apple Silicon</strong></td>
<td>✅ 최적화</td>
<td>❌</td>
</tr>
</tbody></table>
<h3 id="5-7-언제-무엇을-쓸까">5-7. 언제 무엇을 쓸까?</h3>
<h4 id="✅-ollama를-선택할-때">✅ Ollama를 선택할 때</h4>
<ul>
<li>개인 PC / 노트북에서 실행</li>
<li>Windows 또는 Mac 환경</li>
<li>빠른 프로토타이핑, 간단한 테스트</li>
<li>CPU 또는 저사양 GPU 환경</li>
<li>초보자, 간단한 설치 원할 때</li>
<li>소규모 팀 내부 사용</li>
</ul>
<h4 id="✅-vllm을-선택할-때">✅ vLLM을 선택할 때</h4>
<ul>
<li>프로덕션 서비스 배포</li>
<li>고트래픽, 다수 동시 사용자</li>
<li>NVIDIA GPU 서버 보유</li>
<li>최대 성능 / 처리량이 필요할 때</li>
<li>LoRA, AWQ 등 고급 기능 필요</li>
<li>MLOps 파이프라인 구축</li>
</ul>
<h3 id="5-8-함께-사용하는-패턴">5-8. 함께 사용하는 패턴</h3>
<pre><code>개발 단계  →  Ollama (빠른 테스트)
           ↓
프로덕션   →  vLLM (고성능 서빙)

동일한 OpenAI 호환 API 구조이므로
코드 변경 최소화로 전환 가능 ✅</code></pre><hr>
<h2 id="마무리-요약">마무리 요약</h2>
<pre><code>Ollama  =  쉽고 빠르게, 어디서든, 누구나
vLLM    =  빠르고 무겁게, Linux GPU 서버에서, 프로덕션용</code></pre><table>
<thead>
<tr>
<th></th>
<th>Ollama</th>
<th>vLLM</th>
</tr>
</thead>
<tbody><tr>
<td><strong>한 줄 요약</strong></td>
<td>로컬에서 LLM을 쉽게</td>
<td>서버에서 LLM을 빠르게</td>
</tr>
<tr>
<td><strong>핵심 엔진</strong></td>
<td>llama.cpp</td>
<td>PagedAttention</td>
</tr>
<tr>
<td><strong>주요 사용자</strong></td>
<td>개인, 소규모 팀</td>
<td>기업, ML 엔지니어</td>
</tr>
<tr>
<td><strong>환경</strong></td>
<td>어디서든</td>
<td>Linux + NVIDIA GPU</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[zTree 정리 — jQuery 트리 플러그인]]></title>
            <link>https://velog.io/@leemin-jae/zTree-%EC%A0%95%EB%A6%AC-jQuery-%ED%8A%B8%EB%A6%AC-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-djgn2a9y</link>
            <guid>https://velog.io/@leemin-jae/zTree-%EC%A0%95%EB%A6%AC-jQuery-%ED%8A%B8%EB%A6%AC-%ED%94%8C%EB%9F%AC%EA%B7%B8%EC%9D%B8-djgn2a9y</guid>
            <pubDate>Thu, 09 Apr 2026 05:33:51 GMT</pubDate>
            <description><![CDATA[<h1 id="ztree-정리--jquery-트리-플러그인">zTree 정리 — jQuery 트리 플러그인</h1>
<blockquote>
<p>jQuery 기반의 강력한 트리 UI 플러그인 <strong>zTree</strong>의 설치부터 고급 활용까지 한 번에 정리합니다.</p>
</blockquote>
<hr>
<h2 id="목차">목차</h2>
<ol>
<li><a href="#1-ztree%EB%9E%80">zTree란?</a></li>
<li><a href="#2-%EC%84%A4%EC%B9%98-%EB%B0%8F-%EA%B8%B0%EB%B3%B8-%EC%84%B8%ED%8C%85">설치 및 기본 세팅</a></li>
<li><a href="#3-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EA%B5%AC%EC%A1%B0">데이터 구조</a></li>
<li><a href="#4-setting-%EC%98%B5%EC%85%98-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5">setting 옵션 완전 정복</a></li>
<li><a href="#5-%EC%BD%9C%EB%B0%B1callback-%EC%9D%B4%EB%B2%A4%ED%8A%B8">콜백(Callback) 이벤트</a></li>
<li><a href="#6-%EC%B2%B4%ED%81%AC%EB%B0%95%EC%8A%A4--%EB%9D%BC%EB%94%94%EC%98%A4-%EB%AA%A8%EB%93%9C">체크박스 / 라디오 모드</a></li>
<li><a href="#7-ajax-%EB%8F%99%EC%A0%81%EC%A7%80%EC%97%B0-%EB%A1%9C%EB%94%A9">Ajax 동적(지연) 로딩</a></li>
<li><a href="#8-%EB%85%B8%EB%93%9C-%ED%8E%B8%EC%A7%91-%EB%B0%8F-%EB%93%9C%EB%9E%98%EA%B7%B8-%EC%95%A4-%EB%93%9C%EB%A1%AD">노드 편집 및 드래그 앤 드롭</a></li>
<li><a href="#9-api-%EB%A9%94%EC%84%9C%EB%93%9C-%EC%B4%9D%EC%A0%95%EB%A6%AC">API 메서드 총정리</a></li>
<li><a href="#10-%EC%BB%A4%EC%8A%A4%ED%85%80-%EC%95%84%EC%9D%B4%EC%BD%98--%EC%8A%A4%ED%83%80%EC%9D%BC">커스텀 아이콘 &amp; 스타일</a></li>
<li><a href="#11-%EC%8B%A4%EC%A0%84-%EC%98%88%EC%A0%9C--%ED%8C%8C%EC%9D%BC-%ED%83%90%EC%83%89%EA%B8%B0-ui">실전 예제 — 파일 탐색기 UI</a></li>
<li><a href="#12-%EB%A7%88%EC%B9%98%EB%A9%B0--%EB%8C%80%EC%95%88-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC">마치며 — 대안 라이브러리</a></li>
</ol>
<hr>
<h2 id="1-ztree란">1. zTree란?</h2>
<p><strong>zTree</strong>는 jQuery 기반의 오픈소스 트리 UI 플러그인입니다. 2010년대 초반부터 국내 공공기관, 기업 인트라넷, 관리자 페이지에서 폭넓게 사용되어 왔습니다.</p>
<h3 id="주요-특징">주요 특징</h3>
<table>
<thead>
<tr>
<th>특징</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>경량</td>
<td>압축 기준 약 60KB 수준</td>
</tr>
<tr>
<td>체크박스/라디오</td>
<td>부모-자식 연동 자동 처리</td>
</tr>
<tr>
<td>Ajax 지연 로딩</td>
<td>클릭 시 자식 노드를 서버에서 동적으로 가져옴</td>
</tr>
<tr>
<td>드래그 앤 드롭</td>
<td>노드 이동, 순서 변경 지원</td>
</tr>
<tr>
<td>인라인 편집</td>
<td>노드 추가·수정·삭제를 UI 상에서 직접 처리</td>
</tr>
<tr>
<td>대용량 처리</td>
<td>수천 개 노드에서도 안정적인 성능</td>
</tr>
</tbody></table>
<h3 id="언제-사용하면-좋을까">언제 사용하면 좋을까?</h3>
<ul>
<li>레거시 jQuery 기반 프로젝트</li>
<li>공공기관/기업 관리자 페이지</li>
<li>파일 탐색기, 조직도, 메뉴 구조, 카테고리 관리</li>
</ul>
<hr>
<h2 id="2-설치-및-기본-세팅">2. 설치 및 기본 세팅</h2>
<h3 id="cdn-방식">CDN 방식</h3>
<pre><code class="language-html">&lt;!-- jQuery (필수 의존성) --&gt;
&lt;script src=&quot;https://code.jquery.com/jquery-3.6.0.min.js&quot;&gt;&lt;/script&gt;

&lt;!-- zTree CSS --&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdn.jsdelivr.net/npm/ztree@3.5.48/css/zTreeStyle/zTreeStyle.min.css&quot;&gt;

&lt;!-- zTree JS (all = core + excheck + exedit) --&gt;
&lt;script src=&quot;https://cdn.jsdelivr.net/npm/ztree@3.5.48/js/jquery.ztree.all.min.js&quot;&gt;&lt;/script&gt;</code></pre>
<blockquote>
<p>기능별로 분리 로딩도 가능합니다.</p>
<ul>
<li><code>jquery.ztree.core.min.js</code> — 기본 트리</li>
<li><code>jquery.ztree.excheck.min.js</code> — 체크박스/라디오 (core 필요)</li>
<li><code>jquery.ztree.exedit.min.js</code> — 편집/드래그 (core 필요)</li>
</ul>
</blockquote>
<h3 id="npm-방식">npm 방식</h3>
<pre><code class="language-bash">npm install ztree</code></pre>
<pre><code class="language-js">import &#39;ztree/css/zTreeStyle/zTreeStyle.css&#39;;
import &#39;ztree&#39;;</code></pre>
<h3 id="기본-초기화">기본 초기화</h3>
<pre><code class="language-html">&lt;!-- 트리 컨테이너: ul 태그에 ztree 클래스 필수 --&gt;
&lt;ul id=&quot;myTree&quot; class=&quot;ztree&quot;&gt;&lt;/ul&gt;

&lt;script&gt;
const setting = {};

const zNodes = [
  { name: &quot;루트&quot;, open: true, children: [
    { name: &quot;자식 1&quot; },
    { name: &quot;자식 2&quot; }
  ]}
];

// 초기화: $.fn.zTree.init(컨테이너, 설정, 데이터)
$.fn.zTree.init($(&quot;#myTree&quot;), setting, zNodes);
&lt;/script&gt;</code></pre>
<hr>
<h2 id="3-데이터-구조">3. 데이터 구조</h2>
<p>zTree는 두 가지 데이터 구조를 지원합니다.</p>
<h3 id="방식-a--중첩표준-데이터">방식 A — 중첩(표준) 데이터</h3>
<p>JSON 구조 그대로 <code>children</code> 배열로 계층을 표현합니다.</p>
<pre><code class="language-js">const zNodes = [
  {
    name: &quot;회사&quot;,
    open: true,
    children: [
      {
        name: &quot;개발팀&quot;,
        children: [
          { name: &quot;프론트엔드&quot; },
          { name: &quot;백엔드&quot; }
        ]
      },
      { name: &quot;디자인팀&quot; }
    ]
  }
];</code></pre>
<h3 id="방식-b--단순평면-데이터-⭐-실무-추천">방식 B — 단순(평면) 데이터 ⭐ 실무 추천</h3>
<p>DB에서 조회한 평면 배열을 그대로 사용할 수 있어 실무에서 더 많이 쓰입니다.</p>
<pre><code class="language-js">const setting = {
  data: {
    simpleData: {
      enable: true,      // 단순 데이터 모드 활성화
      idKey: &quot;id&quot;,       // 기본값 &quot;id&quot;
      pIdKey: &quot;pId&quot;,     // 부모 참조 키, 기본값 &quot;pId&quot;
      rootPId: 0         // 루트 노드의 pId 값, 기본값 null
    }
  }
};

const zNodes = [
  { id: 1, pId: 0, name: &quot;회사&quot;,      open: true },
  { id: 2, pId: 1, name: &quot;개발팀&quot;,    open: true },
  { id: 3, pId: 1, name: &quot;디자인팀&quot; },
  { id: 4, pId: 2, name: &quot;프론트엔드&quot; },
  { id: 5, pId: 2, name: &quot;백엔드&quot; }
];</code></pre>
<h3 id="노드-주요-속성">노드 주요 속성</h3>
<table>
<thead>
<tr>
<th>속성</th>
<th>타입</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>name</code></td>
<td>string</td>
<td>표시될 노드 이름</td>
</tr>
<tr>
<td><code>id</code></td>
<td>any</td>
<td>노드 고유 ID (단순 모드)</td>
</tr>
<tr>
<td><code>pId</code></td>
<td>any</td>
<td>부모 노드 ID (단순 모드)</td>
</tr>
<tr>
<td><code>open</code></td>
<td>boolean</td>
<td>펼침 여부 (기본 false)</td>
</tr>
<tr>
<td><code>isParent</code></td>
<td>boolean</td>
<td>자식 없어도 폴더처럼 표시</td>
</tr>
<tr>
<td><code>checked</code></td>
<td>boolean</td>
<td>체크 초기값</td>
</tr>
<tr>
<td><code>icon</code></td>
<td>string</td>
<td>커스텀 아이콘 경로</td>
</tr>
<tr>
<td><code>iconOpen</code></td>
<td>string</td>
<td>펼쳐진 상태 아이콘</td>
</tr>
<tr>
<td><code>iconClose</code></td>
<td>string</td>
<td>닫힌 상태 아이콘</td>
</tr>
<tr>
<td><code>url</code></td>
<td>string</td>
<td>클릭 시 이동할 URL</td>
</tr>
<tr>
<td><code>target</code></td>
<td>string</td>
<td>URL 열기 대상 (<code>_blank</code> 등)</td>
</tr>
<tr>
<td><code>nocheck</code></td>
<td>boolean</td>
<td>이 노드만 체크박스 숨김</td>
</tr>
<tr>
<td><code>chkDisabled</code></td>
<td>boolean</td>
<td>체크박스 비활성화</td>
</tr>
</tbody></table>
<hr>
<h2 id="4-setting-옵션-완전-정복">4. setting 옵션 완전 정복</h2>
<p>setting 객체는 <code>view</code>, <code>data</code>, <code>async</code>, <code>check</code>, <code>edit</code>, <code>callback</code> 6개 영역으로 나뉩니다.</p>
<h3 id="4-1-view--외관-설정">4-1. view — 외관 설정</h3>
<pre><code class="language-js">const setting = {
  view: {
    showIcon: true,           // 노드 아이콘 표시 여부 (기본 true)
    showLine: true,           // 연결선 표시 여부 (기본 true)
    showTitle: true,          // 마우스 오버 title 속성 표시
    selectedMulti: false,     // 다중 선택 허용 (기본 false)
    expandSpeed: &quot;fast&quot;,      // 펼침 애니메이션 속도 (&quot;&quot;, &quot;slow&quot;, &quot;normal&quot;, &quot;fast&quot;)
    dblClickExpand: true,     // 더블클릭으로 펼치기 (기본 true)
    nameIsHTML: false,        // name을 HTML로 렌더링할지 여부
    fontCss: function(treeId, node) {
      // 노드별 인라인 스타일 반환
      return node.level === 0
        ? { &quot;font-weight&quot;: &quot;bold&quot; }
        : {};
    },
    addDiyDom: function(treeId, node) {
      // 노드 DOM에 커스텀 요소 추가
      const id = node.tId + &quot;_span&quot;;
      if ($(&quot;#&quot; + id).children(&quot;span.badge&quot;).length === 0) {
        $(&quot;#&quot; + id).after(`&lt;span class=&quot;badge&quot;&gt;${node.count || 0}&lt;/span&gt;`);
      }
    }
  }
};</code></pre>
<h3 id="4-2-data--데이터-매핑">4-2. data — 데이터 매핑</h3>
<pre><code class="language-js">const setting = {
  data: {
    simpleData: {
      enable: true,
      idKey: &quot;id&quot;,
      pIdKey: &quot;pId&quot;,
      rootPId: 0
    },
    key: {
      name: &quot;label&quot;,       // name 키 이름을 &quot;label&quot;로 변경
      title: &quot;tip&quot;,        // title 속성 키 이름 변경
      children: &quot;nodes&quot;,   // children 키 이름을 &quot;nodes&quot;로 변경
      url: &quot;link&quot;          // url 키 이름 변경
    }
  }
};</code></pre>
<blockquote>
<p><code>data.key</code>는 서버 API 응답 필드명이 다를 때 유용합니다.</p>
</blockquote>
<hr>
<h2 id="5-콜백callback-이벤트">5. 콜백(Callback) 이벤트</h2>
<pre><code class="language-js">const setting = {
  callback: {
    // 노드 클릭
    onClick: function(event, treeId, treeNode) {
      console.log(&quot;클릭:&quot;, treeNode.name, treeNode.id);
    },

    // 노드 더블클릭
    onDblClick: function(event, treeId, treeNode) {
      console.log(&quot;더블클릭:&quot;, treeNode.name);
    },

    // 체크박스 상태 변경
    onCheck: function(event, treeId, treeNode) {
      console.log(&quot;체크:&quot;, treeNode.name, treeNode.checked);
    },

    // 노드 펼치기 전
    beforeExpand: function(treeId, treeNode) {
      return true; // false 반환 시 펼치기 중단
    },

    // 노드 펼친 후
    onExpand: function(event, treeId, treeNode) {
      console.log(&quot;펼침:&quot;, treeNode.name);
    },

    // 노드 접은 후
    onCollapse: function(event, treeId, treeNode) {
      console.log(&quot;접힘:&quot;, treeNode.name);
    },

    // Ajax 로딩 성공 후
    onAsyncSuccess: function(event, treeId, treeNode, msg) {
      console.log(&quot;로딩 완료:&quot;, treeNode.name);
    },

    // Ajax 로딩 실패
    onAsyncError: function(event, treeId, treeNode, XMLHttpRequest, textStatus, errorThrown) {
      console.error(&quot;로딩 실패:&quot;, textStatus);
    },

    // 노드 추가 후
    onNodeCreated: function(event, treeId, treeNode) {
      console.log(&quot;노드 생성:&quot;, treeNode.name);
    }
  }
};</code></pre>
<hr>
<h2 id="6-체크박스--라디오-모드">6. 체크박스 / 라디오 모드</h2>
<h3 id="체크박스-기본-설정">체크박스 기본 설정</h3>
<pre><code class="language-js">const setting = {
  check: {
    enable: true,                 // 체크박스 활성화
    chkStyle: &quot;checkbox&quot;,         // &quot;checkbox&quot; 또는 &quot;radio&quot;
    chkboxType: {
      &quot;Y&quot;: &quot;ps&quot;,   // 체크 시: p(부모 연동), s(자식 연동)
      &quot;N&quot;: &quot;ps&quot;    // 해제 시: p(부모 연동), s(자식 연동)
    }
    // 연동 옵션: &quot;p&quot; 부모만, &quot;s&quot; 자식만, &quot;ps&quot; 둘 다, &quot;&quot; 연동 없음
  }
};</code></pre>
<h3 id="체크박스-연동-옵션-정리">체크박스 연동 옵션 정리</h3>
<table>
<thead>
<tr>
<th>chkboxType 값</th>
<th>동작</th>
</tr>
</thead>
<tbody><tr>
<td><code>{ &quot;Y&quot;: &quot;ps&quot;, &quot;N&quot;: &quot;ps&quot; }</code></td>
<td>체크/해제 시 부모·자식 모두 연동 (기본)</td>
</tr>
<tr>
<td><code>{ &quot;Y&quot;: &quot;s&quot;, &quot;N&quot;: &quot;s&quot; }</code></td>
<td>체크 시 자식만 연동, 부모 영향 없음</td>
</tr>
<tr>
<td><code>{ &quot;Y&quot;: &quot;&quot;, &quot;N&quot;: &quot;&quot; }</code></td>
<td>완전 독립 (연동 없음)</td>
</tr>
<tr>
<td><code>{ &quot;Y&quot;: &quot;p&quot;, &quot;N&quot;: &quot;p&quot; }</code></td>
<td>체크 시 부모만 연동</td>
</tr>
</tbody></table>
<h3 id="체크된-노드-가져오기">체크된 노드 가져오기</h3>
<pre><code class="language-js">const treeObj = $.fn.zTree.getZTreeObj(&quot;myTree&quot;);

// 체크된 노드만
const checkedNodes = treeObj.getCheckedNodes(true);

// 체크 안 된 노드만
const uncheckedNodes = treeObj.getCheckedNodes(false);

// ID 배열로 변환
const checkedIds = checkedNodes.map(node =&gt; node.id);
console.log(checkedIds); // [1, 3, 5]</code></pre>
<h3 id="프로그래밍으로-체크-제어">프로그래밍으로 체크 제어</h3>
<pre><code class="language-js">const treeObj = $.fn.zTree.getZTreeObj(&quot;myTree&quot;);

// 특정 노드 체크
const node = treeObj.getNodeByParam(&quot;id&quot;, 3);
treeObj.checkNode(node, true, true); // (노드, 체크여부, 자식연동)

// 전체 체크 / 해제
treeObj.checkAllNodes(true);
treeObj.checkAllNodes(false);</code></pre>
<h3 id="라디오-모드">라디오 모드</h3>
<pre><code class="language-js">const setting = {
  check: {
    enable: true,
    chkStyle: &quot;radio&quot;,
    radioType: &quot;all&quot;  // &quot;level&quot;: 같은 레벨끼리만, &quot;all&quot;: 전체에서 하나만
  }
};</code></pre>
<hr>
<h2 id="7-ajax-동적지연-로딩">7. Ajax 동적(지연) 로딩</h2>
<p>노드 클릭 시 서버에서 자식 데이터를 받아오는 방식입니다. 초기 로드 시간을 줄이고 대용량 트리에 유리합니다.</p>
<pre><code class="language-js">const setting = {
  async: {
    enable: true,
    url: &quot;/api/tree/children&quot;,       // 서버 엔드포인트
    autoParam: [&quot;id&quot;, &quot;name=label&quot;], // 요청에 포함할 노드 속성
    // &quot;name=label&quot;은 name 값을 label 파라미터명으로 전송
    otherParam: {                    // 추가 고정 파라미터
      token: &quot;my-secret-token&quot;,
      type: &quot;menu&quot;
    },
    type: &quot;post&quot;,                    // HTTP 메서드 (기본 &quot;post&quot;)
    contentType: &quot;application/x-www-form-urlencoded&quot;,
    dataFilter: function(treeId, parentNode, responseData) {
      // 서버 응답을 zTree 형식으로 가공
      return responseData.result || [];
    }
  }
};

// isParent: true 인 노드는 자식 없이 폴더처럼 표시되다가
// 클릭 시 Ajax 요청 발생
const zNodes = [
  { id: 1, pId: 0, name: &quot;전체 메뉴&quot;, isParent: true, open: false }
];</code></pre>
<h3 id="서버-응답-예시-spring-boot">서버 응답 예시 (Spring Boot)</h3>
<pre><code class="language-java">@GetMapping(&quot;/api/tree/children&quot;)
public List&lt;TreeNode&gt; getChildren(@RequestParam Long id) {
    return treeService.findByParentId(id);
}</code></pre>
<pre><code class="language-json">[
  { &quot;id&quot;: 10, &quot;pId&quot;: 1, &quot;name&quot;: &quot;서브메뉴 1&quot;, &quot;isParent&quot;: false },
  { &quot;id&quot;: 11, &quot;pId&quot;: 1, &quot;name&quot;: &quot;서브메뉴 2&quot;, &quot;isParent&quot;: true }
]</code></pre>
<h3 id="특정-노드만-ajax-로딩-조건부">특정 노드만 Ajax 로딩 (조건부)</h3>
<pre><code class="language-js">const setting = {
  async: {
    enable: true,
    url: function(treeId, node) {
      // 노드 타입별로 다른 API 호출
      return node.type === &quot;folder&quot;
        ? &quot;/api/tree/folder&quot;
        : &quot;/api/tree/file&quot;;
    }
  }
};</code></pre>
<hr>
<h2 id="8-노드-편집-및-드래그-앤-드롭">8. 노드 편집 및 드래그 앤 드롭</h2>
<h3 id="편집-모드-활성화">편집 모드 활성화</h3>
<pre><code class="language-js">const setting = {
  edit: {
    enable: true,            // 편집 기능 전체 활성화
    showRemoveBtn: true,     // 삭제 버튼 표시
    showRenameBtn: true,     // 이름 변경 버튼 표시
    removeTitle: &quot;삭제&quot;,
    renameTitle: &quot;이름 변경&quot;,

    drag: {
      enable: true,          // 드래그 앤 드롭 활성화
      autoExpandTrigger: true, // 드래그 중 자동 펼침
      prev: true,            // 노드 앞에 드롭 허용
      next: true,            // 노드 뒤에 드롭 허용
      inner: true            // 노드 안으로 드롭 허용 (자식으로)
    }
  },

  callback: {
    // 삭제 전 확인
    beforeRemove: function(treeId, treeNode) {
      return confirm(`&quot;${treeNode.name}&quot;을 삭제하시겠습니까?`);
    },

    // 이름 변경 완료 후
    onRename: function(event, treeId, treeNode, isCancel) {
      if (!isCancel) {
        // 서버에 변경 사항 저장
        $.post(&quot;/api/tree/rename&quot;, { id: treeNode.id, name: treeNode.name });
      }
    },

    // 드롭 전 유효성 검사
    beforeDrop: function(treeId, treeNodes, targetNode, moveType) {
      // 루트 노드는 이동 불가
      if (targetNode &amp;&amp; targetNode.level === 0 &amp;&amp; moveType === &quot;inner&quot;) {
        return false;
      }
      return true;
    },

    // 드롭 완료 후
    onDrop: function(event, treeId, treeNodes, targetNode, moveType, isCopy) {
      console.log(&quot;이동된 노드:&quot;, treeNodes[0].name);
      console.log(&quot;이동 위치:&quot;, moveType); // &quot;prev&quot;, &quot;next&quot;, &quot;inner&quot;
    }
  }
};</code></pre>
<hr>
<h2 id="9-api-메서드-총정리">9. API 메서드 총정리</h2>
<pre><code class="language-js">// 트리 인스턴스 가져오기
const treeObj = $.fn.zTree.getZTreeObj(&quot;myTree&quot;);</code></pre>
<h3 id="노드-조회">노드 조회</h3>
<pre><code class="language-js">// 루트 노드 목록
const roots = treeObj.getNodes();

// 조건으로 노드 검색 (단일)
const node = treeObj.getNodeByParam(&quot;id&quot;, 5);

// 조건으로 노드 검색 (복수)
const nodes = treeObj.getNodesByParam(&quot;type&quot;, &quot;folder&quot;);

// 체크된 노드
const checked = treeObj.getCheckedNodes(true);

// 선택(하이라이트)된 노드
const selected = treeObj.getSelectedNodes();

// 부모 노드
const parent = treeObj.getNodeByTId(node.parentTId);</code></pre>
<h3 id="노드-추가">노드 추가</h3>
<pre><code class="language-js">// 특정 부모 아래에 추가
const parent = treeObj.getNodeByParam(&quot;id&quot;, 1);
treeObj.addNodes(parent, [
  { id: 100, name: &quot;새 노드 1&quot; },
  { id: 101, name: &quot;새 노드 2&quot; }
]);

// 루트에 추가 (parent = null)
treeObj.addNodes(null, [{ id: 200, name: &quot;루트 노드&quot; }]);

// 특정 위치에 삽입 (index)
treeObj.addNodes(parent, 0, [{ id: 102, name: &quot;맨 앞에 추가&quot; }]);</code></pre>
<h3 id="노드-수정-및-삭제">노드 수정 및 삭제</h3>
<pre><code class="language-js">// 이름 변경
const node = treeObj.getNodeByParam(&quot;id&quot;, 5);
node.name = &quot;변경된 이름&quot;;
treeObj.updateNode(node);

// 삭제
treeObj.removeNode(node);

// 모든 자식 삭제
treeObj.removeChildNodes(parent);</code></pre>
<h3 id="펼치기--접기">펼치기 / 접기</h3>
<pre><code class="language-js">// 전체 펼치기 / 접기
treeObj.expandAll(true);    // 전체 펼치기
treeObj.expandAll(false);   // 전체 접기

// 특정 노드
treeObj.expandNode(node, true, true, true);
// 인자: (노드, 펼침여부, 자식포함, 애니메이션)</code></pre>
<h3 id="선택-및-체크">선택 및 체크</h3>
<pre><code class="language-js">// 선택 처리
treeObj.selectNode(node);
treeObj.cancelSelectedNode(node);

// 체크
treeObj.checkNode(node, true, true);   // 체크
treeObj.checkNode(node, false, true);  // 해제
treeObj.checkAllNodes(true);           // 전체 체크</code></pre>
<h3 id="트리-전체-제어">트리 전체 제어</h3>
<pre><code class="language-js">// 데이터 새로고침
$.fn.zTree.init($(&quot;#myTree&quot;), setting, newData);

// 트리 인스턴스 제거
$.fn.zTree.destroy(&quot;myTree&quot;);</code></pre>
<hr>
<h2 id="10-커스텀-아이콘--스타일">10. 커스텀 아이콘 &amp; 스타일</h2>
<h3 id="이미지-아이콘-지정">이미지 아이콘 지정</h3>
<pre><code class="language-js">const zNodes = [
  {
    id: 1, pId: 0, name: &quot;서버&quot;,
    icon: &quot;/icons/server.png&quot;,        // 기본 아이콘
    iconOpen: &quot;/icons/server-on.png&quot;, // 펼쳐진 상태
    iconClose: &quot;/icons/server-off.png&quot; // 닫힌 상태
  }
];</code></pre>
<h3 id="css-클래스로-아이콘-변경">CSS 클래스로 아이콘 변경</h3>
<pre><code class="language-js">const setting = {
  view: {
    addDiyDom: function(treeId, treeNode) {
      const aObj = $(&quot;#&quot; + treeNode.tId + &quot;_a&quot;);
      // 기존 아이콘 숨기고 Font Awesome 등 적용
      aObj.find(&quot;.button&quot;).css(&quot;display&quot;, &quot;none&quot;);
      if (treeNode.isParent) {
        aObj.prepend(&#39;&lt;i class=&quot;fa fa-folder&quot; style=&quot;margin-right:4px&quot;&gt;&lt;/i&gt;&#39;);
      } else {
        aObj.prepend(&#39;&lt;i class=&quot;fa fa-file&quot; style=&quot;margin-right:4px&quot;&gt;&lt;/i&gt;&#39;);
      }
    }
  }
};</code></pre>
<h3 id="노드별-css-스타일-동적-적용">노드별 CSS 스타일 동적 적용</h3>
<pre><code class="language-js">const setting = {
  view: {
    fontCss: function(treeId, treeNode) {
      if (treeNode.disabled) {
        return { color: &quot;#aaa&quot;, &quot;text-decoration&quot;: &quot;line-through&quot; };
      }
      if (treeNode.level === 0) {
        return { &quot;font-weight&quot;: &quot;bold&quot;, color: &quot;#333&quot; };
      }
      return {};
    }
  }
};</code></pre>
<h3 id="css-커스터마이징-예시">CSS 커스터마이징 예시</h3>
<pre><code class="language-css">/* 선택된 노드 배경색 변경 */
.ztree li a.curSelectedNode {
  background-color: #e8f4ff;
  border: 1px solid #b8d9f8;
  color: #1a73e8;
}

/* 호버 스타일 */
.ztree li a:hover {
  background-color: #f5f5f5;
}

/* 연결선 색상 변경 */
.ztree li span.button.switch {
  background-color: transparent;
}</code></pre>
<hr>
<h2 id="11-실전-예제--파일-탐색기-ui">11. 실전 예제 — 파일 탐색기 UI</h2>
<p>체크박스 + Ajax 동적 로딩 + 노드 클릭 이벤트를 조합한 파일 탐색기 예제입니다.</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;ko&quot;&gt;
&lt;head&gt;
  &lt;meta charset=&quot;UTF-8&quot;&gt;
  &lt;title&gt;파일 탐색기&lt;/title&gt;
  &lt;link rel=&quot;stylesheet&quot; href=&quot;css/zTreeStyle/zTreeStyle.css&quot;&gt;
  &lt;style&gt;
    body { font-family: sans-serif; display: flex; height: 100vh; margin: 0; }
    #sidebar { width: 280px; border-right: 1px solid #ddd; padding: 16px; overflow-y: auto; }
    #content { flex: 1; padding: 16px; }
    #selectedPath { color: #555; font-size: 14px; margin-bottom: 12px; }
    .ztree li a.curSelectedNode { background: #e8f4ff; border-color: #b8d9f8; }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;div id=&quot;sidebar&quot;&gt;
  &lt;h3 style=&quot;margin-top:0&quot;&gt;파일 탐색기&lt;/h3&gt;
  &lt;ul id=&quot;fileTree&quot; class=&quot;ztree&quot;&gt;&lt;/ul&gt;
&lt;/div&gt;
&lt;div id=&quot;content&quot;&gt;
  &lt;div id=&quot;selectedPath&quot;&gt;← 파일을 선택하세요&lt;/div&gt;
  &lt;div id=&quot;fileInfo&quot;&gt;&lt;/div&gt;
&lt;/div&gt;

&lt;script src=&quot;js/jquery.min.js&quot;&gt;&lt;/script&gt;
&lt;script src=&quot;js/jquery.ztree.all.min.js&quot;&gt;&lt;/script&gt;
&lt;script&gt;
$(function () {
  const setting = {
    view: {
      showIcon: true,
      selectedMulti: false
    },
    data: {
      simpleData: { enable: true }
    },
    check: {
      enable: true,
      chkboxType: { &quot;Y&quot;: &quot;ps&quot;, &quot;N&quot;: &quot;ps&quot; }
    },
    async: {
      enable: true,
      url: &quot;/api/files/children&quot;,
      autoParam: [&quot;id&quot;],
      dataFilter: function (treeId, parentNode, res) {
        return res.data || [];
      }
    },
    callback: {
      onClick: function (event, treeId, node) {
        if (!node.isParent) {
          $(&quot;#selectedPath&quot;).text(&quot;선택: &quot; + getFullPath(node));
          loadFileInfo(node.id);
        }
      },
      onCheck: function (event, treeId, node) {
        const checked = $.fn.zTree.getZTreeObj(&quot;fileTree&quot;).getCheckedNodes(true);
        console.log(&quot;체크된 파일 수:&quot;, checked.length);
      }
    }
  };

  const initNodes = [
    { id: 1, pId: 0, name: &quot;루트&quot;,      isParent: true, open: true,
      icon: &quot;img/folder.png&quot; },
    { id: 2, pId: 1, name: &quot;문서&quot;,      isParent: true },
    { id: 3, pId: 1, name: &quot;사진&quot;,      isParent: true },
    { id: 4, pId: 2, name: &quot;보고서.docx&quot;, isParent: false }
  ];

  $.fn.zTree.init($(&quot;#fileTree&quot;), setting, initNodes);

  // 노드의 전체 경로 계산
  function getFullPath(node) {
    const parts = [node.name];
    let current = node;
    const treeObj = $.fn.zTree.getZTreeObj(&quot;fileTree&quot;);
    while (current.parentTId) {
      current = treeObj.getNodeByTId(current.parentTId);
      parts.unshift(current.name);
    }
    return parts.join(&quot; / &quot;);
  }

  // 파일 정보 로드
  function loadFileInfo(id) {
    $.get(&quot;/api/files/&quot; + id, function (data) {
      $(&quot;#fileInfo&quot;).html(`
        &lt;p&gt;&lt;b&gt;이름:&lt;/b&gt; ${data.name}&lt;/p&gt;
        &lt;p&gt;&lt;b&gt;크기:&lt;/b&gt; ${data.size} KB&lt;/p&gt;
        &lt;p&gt;&lt;b&gt;수정일:&lt;/b&gt; ${data.updatedAt}&lt;/p&gt;
      `);
    });
  }
});
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<hr>
<h2 id="12-마치며--대안-라이브러리">12. 마치며 — 대안 라이브러리</h2>
<p>zTree는 jQuery 생태계에서 매우 성숙한 라이브러리지만, 현대적인 프레임워크 환경에서는 아래 대안도 고려하세요.</p>
<table>
<thead>
<tr>
<th>상황</th>
<th>추천 라이브러리</th>
</tr>
</thead>
<tbody><tr>
<td>React</td>
<td><code>rc-tree</code> (Ant Design), <code>react-arborist</code>, <code>@mui/x-tree-view</code></td>
</tr>
<tr>
<td>Vue 3</td>
<td><code>el-tree</code> (Element Plus), <code>vue-treeselect</code></td>
</tr>
<tr>
<td>순수 JS</td>
<td><code>jsTree</code>, <code>Treant.js</code></td>
</tr>
<tr>
<td>대용량 + 가상화</td>
<td><code>react-arborist</code> (가상 스크롤 내장)</td>
</tr>
</tbody></table>
<h3 id="정리">정리</h3>
<ul>
<li><strong>레거시 jQuery 프로젝트</strong> → zTree가 여전히 최선</li>
<li><strong>신규 React/Vue 프로젝트</strong> → 각 프레임워크 생태계의 트리 컴포넌트 사용</li>
<li><strong>공공기관 SI</strong> → zTree 요구사항이 많으므로 숙지해두면 유리</li>
</ul>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[Python DB 연결]]></title>
            <link>https://velog.io/@leemin-jae/Python-DB-%EC%97%B0%EA%B2%B0</link>
            <guid>https://velog.io/@leemin-jae/Python-DB-%EC%97%B0%EA%B2%B0</guid>
            <pubDate>Mon, 06 Apr 2026 07:49:38 GMT</pubDate>
            <description><![CDATA[<h1 id="python-db-연결---conn과-cursor-정리">Python DB 연결 - conn과 cursor 정리</h1>
<blockquote>
<p>PostgreSQL 기준으로 작성되었습니다.</p>
</blockquote>
<hr>
<h2 id="conn이란">conn이란?</h2>
<p><code>conn</code>은 <strong>데이터베이스와의 연결 자체</strong>를 담당하는 객체입니다.</p>
<pre><code class="language-python">self.conn = connection.Connection().postgresql_connection()</code></pre>
<p>DB와의 <strong>통로</strong> 역할만 하며, SQL을 직접 실행하는 기능은 없습니다.</p>
<hr>
<h2 id="cursor란">cursor란?</h2>
<p><code>cursor</code>는 <code>conn</code>을 통해 <strong>실제 SQL을 실행하는 객체</strong>입니다.</p>
<pre><code>conn   = 은행 입장 (문)
cursor = 창구 직원 (실제 업무 처리)</code></pre><pre><code class="language-python">with self.conn.cursor() as cursor:
    cursor.execute(&quot;SELECT * FROM users&quot;)
    result = cursor.fetchall()</code></pre>
<h3 id="cursor-주요-메서드">cursor 주요 메서드</h3>
<table>
<thead>
<tr>
<th>메서드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>cursor.execute(sql)</code></td>
<td>SQL 1개 실행</td>
</tr>
<tr>
<td><code>cursor.executemany(sql, list)</code></td>
<td>SQL 여러 개 실행</td>
</tr>
<tr>
<td><code>cursor.fetchone()</code></td>
<td>결과 1행 반환</td>
</tr>
<tr>
<td><code>cursor.fetchmany(n)</code></td>
<td>결과 n행 반환</td>
</tr>
<tr>
<td><code>cursor.fetchall()</code></td>
<td>결과 전체 반환</td>
</tr>
<tr>
<td><code>cursor.close()</code></td>
<td>커서 닫기</td>
</tr>
</tbody></table>
<hr>
<h2 id="cursor를-사용하는-이유">cursor를 사용하는 이유</h2>
<h3 id="1-conn은-sql-직접-실행-불가">1. conn은 SQL 직접 실행 불가</h3>
<pre><code class="language-python">self.conn.execute(&quot;SELECT * FROM users&quot;)          # ❌ 불가능
self.conn.cursor().execute(&quot;SELECT * FROM users&quot;) # ✅ 가능</code></pre>
<h3 id="2-결과를-내부에-저장">2. 결과를 내부에 저장</h3>
<p>cursor는 SQL 실행 후 결과를 내부에 임시 저장합니다.</p>
<pre><code>DB 서버                cursor 내부
┌─────────┐           ┌──────────────┐
│ 100만 행 │  →SQL→   │ 결과 임시 저장 │ → fetchone()으로 하나씩
└─────────┘           └──────────────┘</code></pre><pre><code class="language-python">cursor.execute(&quot;SELECT * FROM users&quot;)
cursor.fetchone()     # 1행 반환
cursor.fetchmany(100) # 100행 반환
cursor.fetchall()     # 전체 반환</code></pre>
<h3 id="3-여러-작업을-독립적으로-처리">3. 여러 작업을 독립적으로 처리</h3>
<p>하나의 <code>conn</code>에서 <strong>여러 cursor를 동시에</strong> 운용할 수 있습니다.</p>
<pre><code class="language-python">cursor1 = self.conn.cursor()
cursor2 = self.conn.cursor()

cursor1.execute(&quot;SELECT * FROM users&quot;)   # 독립 실행
cursor2.execute(&quot;SELECT * FROM orders&quot;)  # 서로 영향 없음

result1 = cursor1.fetchall()
result2 = cursor2.fetchall()</code></pre>
<h3 id="4-트랜잭션-관리">4. 트랜잭션 관리</h3>
<p>cursor 단위로 작업하고, <code>conn</code>으로 commit / rollback 합니다.</p>
<pre><code class="language-python">with self.conn.cursor() as cursor:
    try:
        cursor.execute(&quot;INSERT INTO users VALUES (%s)&quot;, (&quot;Alice&quot;,))
        cursor.execute(&quot;INSERT INTO orders VALUES (%s)&quot;, (1,))
        self.conn.commit()    # 둘 다 성공 시 저장
    except:
        self.conn.rollback()  # 하나라도 실패 시 전부 취소</code></pre>
<h3 id="5-메모리-효율">5. 메모리 효율</h3>
<p>한 번에 모든 데이터를 가져오지 않고 필요한 만큼만 가져올 수 있습니다.</p>
<pre><code class="language-python">cursor.execute(&quot;SELECT * FROM users&quot;)  # 100만 행 조회

cursor.fetchone()      # ✅ 1행만 메모리에 올림
cursor.fetchmany(100)  # ✅ 100행만 메모리에 올림
cursor.fetchall()      # ⚠️ 100만 행 전부 메모리에 올림</code></pre>
<hr>
<h2 id="with문과-함께-사용하는-이유">with문과 함께 사용하는 이유</h2>
<p><code>with</code> 블록이 끝나면 자동으로 <code>cursor.close()</code>가 호출됩니다.</p>
<pre><code class="language-python"># with 없이
cursor = self.conn.cursor()
cursor.execute(&quot;SELECT * FROM users&quot;)
result = cursor.fetchall()
cursor.close()  # 직접 닫아야 함 ⚠️

# with 사용
with self.conn.cursor() as cursor:
    cursor.execute(&quot;SELECT * FROM users&quot;)
    result = cursor.fetchall()
# 자동으로 cursor.close() 호출 ✅</code></pre>
<hr>
<h2 id="⚠️-with-사용-시-주의사항">⚠️ with 사용 시 주의사항</h2>
<p><code>cursor</code>와 <code>conn</code>의 with 종료 시 동작이 다릅니다.</p>
<table>
<thead>
<tr>
<th>구문</th>
<th>종료 시</th>
</tr>
</thead>
<tbody><tr>
<td><code>with self.conn.cursor() as cursor</code></td>
<td><code>cursor.close()</code> 만 호출</td>
</tr>
<tr>
<td><code>with self.conn as conn</code></td>
<td><code>conn.close()</code> 호출 ⚠️</td>
</tr>
</tbody></table>
<pre><code class="language-python">self.conn = connection.Connection().postgresql_connection()

with self.conn.cursor() as cursor:
    cursor.execute(&quot;SELECT * FROM users&quot;)
# cursor만 닫힘, conn은 유지 ✅

print(self.conn.closed)  # 0 = 열림 ✅</code></pre>
<p><code>self.conn</code>을 <code>with</code>로 직접 감싸면 블록 종료 시 연결이 닫혀 <strong>재사용이 불가능</strong>합니다.</p>
<pre><code class="language-python">with self.conn as conn:  # ⚠️ 블록 끝나면 self.conn이 닫힘
    pass

# 이후 self.conn 재사용 불가! ❌</code></pre>
<hr>
<h2 id="권장-패턴">권장 패턴</h2>
<p><code>self.conn</code>을 여러 메서드에서 재사용하는 경우, <strong>cursor만 with로 관리</strong>하는 것이 좋습니다.</p>
<pre><code class="language-python">class UserService:
    def __init__(self):
        self.conn = connection.Connection().postgresql_connection()

    def get_users(self):
        with self.conn.cursor() as cursor:      # cursor만 with로 관리
            cursor.execute(&quot;SELECT * FROM users&quot;)
            return cursor.fetchall()
        # cursor 닫힘, conn 유지 → 재사용 가능 ✅

    def insert_user(self, name):
        with self.conn.cursor() as cursor:      # 같은 conn 재사용
            cursor.execute(&quot;INSERT INTO users VALUES (%s)&quot;, (name,))
            self.conn.commit()

    def close(self):
        self.conn.close()                       # 모든 작업 종료 후 명시적으로 닫기</code></pre>
<hr>
<h2 id="전체-흐름-요약">전체 흐름 요약</h2>
<pre><code>conn 생성
  │
  ├── cursor 생성 (with)
  │     ├── execute() : SQL 실행
  │     ├── fetch()   : 결과 가져오기
  │     └── close()   : 자동 호출 (with 종료 시)
  │
  ├── commit() / rollback()  : conn이 담당
  │
  └── conn.close()  : 모든 작업 종료 후 명시적 호출</code></pre><table>
<thead>
<tr>
<th></th>
<th>conn</th>
<th>cursor</th>
</tr>
</thead>
<tbody><tr>
<td>역할</td>
<td>DB 연결 유지</td>
<td>SQL 실행</td>
</tr>
<tr>
<td>생성</td>
<td>한 번만</td>
<td>작업마다</td>
</tr>
<tr>
<td>with 종료 시</td>
<td>연결 닫힘 ⚠️</td>
<td>cursor만 닫힘 ✅</td>
</tr>
<tr>
<td>SQL 실행</td>
<td>❌</td>
<td>✅</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[PostgreSQL 롤(Role)]]></title>
            <link>https://velog.io/@leemin-jae/PostgreSQL-%EB%A1%A4Role</link>
            <guid>https://velog.io/@leemin-jae/PostgreSQL-%EB%A1%A4Role</guid>
            <pubDate>Mon, 06 Apr 2026 01:37:55 GMT</pubDate>
            <description><![CDATA[<h1 id="postgresql-롤role-정리">PostgreSQL 롤(Role) 정리</h1>
<h2 id="롤role이란">롤(Role)이란?</h2>
<p>PostgreSQL에서 <strong>롤(Role)</strong> 은 데이터베이스 접근 권한을 관리하는 개체입니다.
과거에는 사용자(User)와 그룹(Group)을 별도로 관리했지만, PostgreSQL은 이 둘을 <strong>롤 하나로 통합</strong>했습니다.</p>
<blockquote>
<p>💡 <strong>핵심 개념</strong>: PostgreSQL에서는 모든 것이 롤입니다. 사용자도, 그룹도 모두 롤입니다.</p>
</blockquote>
<hr>
<h2 id="롤의-두-가지-종류">롤의 두 가지 종류</h2>
<p><code>LOGIN</code> 속성 유무에 따라 역할이 나뉩니다.</p>
<table>
<thead>
<tr>
<th>종류</th>
<th>LOGIN 속성</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>로그인 롤</strong></td>
<td>O</td>
<td>DB 접속 가능, 사용자처럼 동작</td>
</tr>
<tr>
<td><strong>그룹 롤</strong></td>
<td>X</td>
<td>권한 묶음 역할, 그룹처럼 동작</td>
</tr>
</tbody></table>
<pre><code class="language-sql">-- 그룹 롤 생성 (로그인 불가)
CREATE ROLE developer;

-- 로그인 롤 생성 (로그인 가능)
CREATE ROLE alice WITH LOGIN PASSWORD &#39;password123&#39;;

-- CREATE USER = CREATE ROLE + LOGIN 축약 문법
CREATE USER bob WITH PASSWORD &#39;password456&#39;;</code></pre>
<hr>
<h2 id="주요-속성attribute">주요 속성(Attribute)</h2>
<table>
<thead>
<tr>
<th>속성</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>LOGIN</code></td>
<td>DB 접속 허용</td>
</tr>
<tr>
<td><code>SUPERUSER</code></td>
<td>모든 권한 보유 (슈퍼유저)</td>
</tr>
<tr>
<td><code>CREATEDB</code></td>
<td>데이터베이스 생성 가능</td>
</tr>
<tr>
<td><code>CREATEROLE</code></td>
<td>다른 롤 생성 가능</td>
</tr>
<tr>
<td><code>REPLICATION</code></td>
<td>복제(Replication) 연결 허용</td>
</tr>
<tr>
<td><code>INHERIT</code></td>
<td>부여받은 롤의 권한 자동 상속 (기본값)</td>
</tr>
<tr>
<td><code>PASSWORD</code></td>
<td>로그인 비밀번호 설정</td>
</tr>
</tbody></table>
<pre><code class="language-sql">CREATE ROLE admin WITH LOGIN SUPERUSER CREATEDB CREATEROLE PASSWORD &#39;adminpass&#39;;</code></pre>
<hr>
<h2 id="롤-확인-방법">롤 확인 방법</h2>
<h3 id="psql-메타-명령">psql 메타 명령</h3>
<pre><code class="language-sql">\du</code></pre>
<pre><code>                                   List of roles
 Role name |                         Attributes
-----------+------------------------------------------------------------
 alice     | Superuser, Create DB
 bob       |
 developer | Cannot login        ← 그룹 롤
 admin     | Create role, Create DB</code></pre><p><code>Cannot login</code> 이 표시되면 그룹 롤, 없으면 로그인 롤입니다.</p>
<h3 id="pg_roles-뷰로-쿼리">pg_roles 뷰로 쿼리</h3>
<pre><code class="language-sql">SELECT
    rolname          AS 롤이름,
    rolcanlogin      AS 로그인가능,
    rolsuper         AS 슈퍼유저,
    rolcreatedb      AS DB생성,
    rolcreaterole    AS 롤생성,
    rolinherit       AS 권한상속
FROM pg_roles
WHERE rolname NOT LIKE &#39;pg_%&#39;
ORDER BY rolcanlogin DESC;</code></pre>
<ul>
<li><code>rolcanlogin = t</code> → 로그인 롤</li>
<li><code>rolcanlogin = f</code> → 그룹 롤</li>
</ul>
<h3 id="로그인-롤--그룹-롤-분리-조회">로그인 롤 / 그룹 롤 분리 조회</h3>
<pre><code class="language-sql">-- 로그인 롤만 조회
SELECT rolname AS 로그인롤
FROM pg_roles
WHERE rolcanlogin = true
  AND rolname NOT LIKE &#39;pg_%&#39;;

-- 그룹 롤만 조회
SELECT rolname AS 그룹롤
FROM pg_roles
WHERE rolcanlogin = false
  AND rolname NOT LIKE &#39;pg_%&#39;;</code></pre>
<hr>
<h2 id="그룹-롤을-로그인-롤에-부여하기">그룹 롤을 로그인 롤에 부여하기</h2>
<h3 id="기본-문법">기본 문법</h3>
<pre><code class="language-sql">GRANT 그룹롤 TO 로그인롤;</code></pre>
<h3 id="실습-예시">실습 예시</h3>
<pre><code class="language-sql">-- 1. 그룹 롤 생성 및 권한 부여
CREATE ROLE developer;
GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA public TO developer;

-- 2. 로그인 롤 생성
CREATE ROLE alice WITH LOGIN PASSWORD &#39;password123&#39;;
CREATE ROLE bob   WITH LOGIN PASSWORD &#39;password456&#39;;

-- 3. 그룹 롤을 로그인 롤에 부여
GRANT developer TO alice;
GRANT developer TO alice, bob;  -- 여러 명에게 한번에 부여</code></pre>
<h3 id="inherit-여부에-따른-동작-차이">INHERIT 여부에 따른 동작 차이</h3>
<pre><code class="language-sql">-- ✅ INHERIT (기본값): 권한 자동 상속
CREATE ROLE alice WITH LOGIN INHERIT;
GRANT developer TO alice;
-- alice로 로그인하면 developer 권한이 자동으로 활성화

-- ⚠️ NOINHERIT: 수동으로 롤 전환 필요
CREATE ROLE alice WITH LOGIN NOINHERIT;
GRANT developer TO alice;
SET ROLE developer;  -- 수동 전환
RESET ROLE;          -- 원래 롤로 복귀</code></pre>
<h3 id="멤버십-확인">멤버십 확인</h3>
<pre><code class="language-sql">SELECT
    r.rolname  AS 그룹롤,
    m.rolname  AS 멤버롤
FROM pg_roles r
JOIN pg_auth_members am ON am.roleid = r.oid
JOIN pg_roles m         ON m.oid = am.member;</code></pre>
<pre><code>  그룹롤   | 멤버롤
-----------+--------
 developer | alice
 developer | bob</code></pre><h3 id="롤-회수">롤 회수</h3>
<pre><code class="language-sql">REVOKE developer FROM alice;
REVOKE developer FROM alice, bob;  -- 여러 명에게서 한번에 회수</code></pre>
<hr>
<h2 id="그룹-롤을-부여할-수-있는-롤">그룹 롤을 부여할 수 있는 롤</h2>
<p>모든 로그인 롤이 <code>GRANT</code>를 실행할 수 있는 것은 아닙니다.
다음 세 가지 경우에만 그룹 롤을 다른 롤에게 부여할 수 있습니다.</p>
<h3 id="1-superuser">1. SUPERUSER</h3>
<pre><code class="language-sql">CREATE ROLE admin WITH LOGIN SUPERUSER PASSWORD &#39;adminpass&#39;;
-- 모든 롤을 제한 없이 부여 가능</code></pre>
<h3 id="2-createrole">2. CREATEROLE</h3>
<pre><code class="language-sql">CREATE ROLE manager WITH LOGIN CREATEROLE PASSWORD &#39;managerpass&#39;;
-- 그룹 롤을 다른 롤에게 부여 가능</code></pre>
<blockquote>
<p>⚠️ <strong>PostgreSQL 16부터 변경사항</strong></p>
<ul>
<li>16 이전: <code>CREATEROLE</code>이 있으면 <strong>모든 롤</strong> 부여 가능</li>
<li>16 이후: <code>CREATEROLE</code>이 있어도 <strong>본인이 속한 롤만</strong> 부여 가능 (보안 강화)</li>
</ul>
</blockquote>
<h3 id="3-with-admin-option">3. WITH ADMIN OPTION</h3>
<pre><code class="language-sql">-- alice에게 developer 롤을 부여할 수 있는 권한까지 위임
GRANT developer TO alice WITH ADMIN OPTION;

-- 이제 alice도 developer 롤을 다른 사람에게 부여 가능
GRANT developer TO bob;    -- ✅ 가능
GRANT developer TO carol;  -- ✅ 가능</code></pre>
<h4 id="with-admin-option의-핵심-해당-롤-하나에만-적용">WITH ADMIN OPTION의 핵심: &quot;해당 롤 하나에만&quot; 적용</h4>
<p><code>WITH ADMIN OPTION</code>은 부여받은 <strong>그 롤 하나에 대해서만</strong> 다른 사람에게 줄 수 있는 권한입니다.</p>
<pre><code class="language-sql">GRANT developer TO alice WITH ADMIN OPTION;
-- designer, devops는 ADMIN OPTION 없음

-- alice로 로그인 후...
GRANT developer TO bob;   -- ✅ 가능 (admin option 있음)
GRANT designer  TO bob;   -- ❌ 불가 (admin option 없음)
GRANT devops    TO bob;   -- ❌ 불가 (admin option 없음)</code></pre>
<p>여러 롤에 각각 부여하는 것도 가능합니다.</p>
<pre><code class="language-sql">GRANT developer TO alice WITH ADMIN OPTION;
GRANT designer  TO alice WITH ADMIN OPTION;

GRANT developer TO bob;   -- ✅ 가능
GRANT designer  TO bob;   -- ✅ 가능
GRANT devops    TO bob;   -- ❌ 불가</code></pre>
<h4 id="admin-option-확인">ADMIN OPTION 확인</h4>
<pre><code class="language-sql">SELECT
    r.rolname        AS 그룹롤,
    m.rolname        AS 멤버롤,
    am.admin_option  AS 어드민권한
FROM pg_auth_members am
JOIN pg_roles r ON r.oid = am.roleid
JOIN pg_roles m ON m.oid = am.member;</code></pre>
<pre><code>  그룹롤   | 멤버롤 | 어드민권한
-----------+--------+-----------
 developer | alice  | t    ← alice는 developer를 남에게 부여 가능
 developer | bob    | f    ← bob은 부여 불가</code></pre><h3 id="세-가지-비교">세 가지 비교</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>부여 범위</th>
<th>설정 방법</th>
</tr>
</thead>
<tbody><tr>
<td><code>SUPERUSER</code></td>
<td>모든 롤 제한 없음</td>
<td><code>CREATE ROLE ... SUPERUSER</code></td>
</tr>
<tr>
<td><code>CREATEROLE</code></td>
<td>본인이 속한 롤 (PG16+)</td>
<td><code>CREATE ROLE ... CREATEROLE</code></td>
</tr>
<tr>
<td><code>WITH ADMIN OPTION</code></td>
<td>해당 특정 그룹 롤만</td>
<td><code>GRANT 롤 TO 유저 WITH ADMIN OPTION</code></td>
</tr>
</tbody></table>
<blockquote>
<p>💡 <strong>최소 권한 원칙</strong>: 가능하면 <code>WITH ADMIN OPTION</code>을 사용하는 것이 가장 안전합니다.</p>
</blockquote>
<hr>
<h2 id="전체-구조-요약">전체 구조 요약</h2>
<pre><code>롤(Role)
├── 로그인 롤 (LOGIN O) : 실제 DB 접속 계정
└── 그룹 롤  (LOGIN X) : 권한 묶음

그룹 롤 부여 가능한 롤
├── SUPERUSER          → 제한 없이 모든 롤 부여 가능
├── CREATEROLE         → 본인이 속한 롤만 부여 가능 (PG16+)
└── WITH ADMIN OPTION  → 해당 특정 그룹 롤만 부여 가능</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[SOLID 원칙으로 리팩토링하기]]></title>
            <link>https://velog.io/@leemin-jae/SOLID-%EC%9B%90%EC%B9%99%EC%9C%BC%EB%A1%9C-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@leemin-jae/SOLID-%EC%9B%90%EC%B9%99%EC%9C%BC%EB%A1%9C-%EB%A6%AC%ED%8C%A9%ED%86%A0%EB%A7%81%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 05 Apr 2026 07:19:35 GMT</pubDate>
            <description><![CDATA[<h1 id="python-connectionpy를-solid-원칙으로-리팩토링하기">Python connection.py를 SOLID 원칙으로 리팩토링하기</h1>
<blockquote>
<p>실무에서 사용하던 DB/서비스 연결 모듈을 SOLID 원칙에 맞게 단계적으로 개선한 과정을 정리합니다.</p>
</blockquote>
<hr>
<h2 id="개요">개요</h2>
<p>Airflow 기반 데이터 파이프라인에서 사용하던 <code>connection.py</code>는 하나의 <code>Connection</code> 클래스가 Hive, PostgreSQL, Oracle, Redis, SFTP 등 모든 서비스 연결을 담당하고 있었습니다.
기능은 동작했지만, 확장하거나 유지보수할수록 코드가 점점 복잡해지는 문제가 있었습니다.</p>
<p>이를 <strong>SOLID 원칙</strong> 기준으로 5단계에 걸쳐 개선했습니다.</p>
<hr>
<h2 id="1-_resolve_config_path-분리-srp--단일-책임-원칙">1. <code>_resolve_config_path</code> 분리 (SRP — 단일 책임 원칙)</h2>
<h3 id="기존-코드">기존 코드</h3>
<pre><code class="language-python">def _load_config(self, filename):
    config = configparser.ConfigParser()
    config_path = os.path.join(CONF_DIR, filename)
    if not os.path.exists(config_path):
        local_path = os.path.join(
            os.path.dirname(os.path.dirname(os.path.abspath(__file__))), &#39;cfg&#39;, filename
        )
        if os.path.exists(local_path):
            config_path = local_path
        else:
            raise FileNotFoundError(f&quot;Configuration file not found: {config_path}&quot;)
    config.read(config_path)
    return config</code></pre>
<h3 id="개선된-코드">개선된 코드</h3>
<pre><code class="language-python">def _resolve_config_path(self, filename: str) -&gt; str:
    &quot;&quot;&quot;
    설정 파일의 실제 경로를 탐색하여 반환합니다.
    1순위: 운영 경로 (CONF_DIR)
    2순위: 로컬 경로 (테스트 환경 폴백)
    &quot;&quot;&quot;
    primary = os.path.join(CONF_DIR, filename)
    if os.path.exists(primary):
        return primary

    fallback = os.path.join(
        os.path.dirname(os.path.dirname(os.path.abspath(__file__))), &#39;cfg&#39;, filename
    )
    if os.path.exists(fallback):
        return fallback

    raise FileNotFoundError(f&quot;Configuration file not found: {primary}&quot;)

def _load_config(self, filename: str):
    &quot;&quot;&quot;지정된 파일명으로 설정 파일을 파싱하여 반환합니다.&quot;&quot;&quot;
    config = configparser.ConfigParser()
    config.read(self._resolve_config_path(filename))
    return config</code></pre>
<h3 id="왜-이렇게-개선해야-하는가">왜 이렇게 개선해야 하는가?</h3>
<p><code>_load_config</code>는 원래 하나의 메서드에서 <strong>&quot;경로 탐색&quot;</strong> 과 <strong>&quot;파일 파싱&quot;</strong> 두 가지를 동시에 처리하고 있었습니다.
SRP는 클래스/함수가 <strong>하나의 이유만으로 변경되어야 한다</strong>고 말합니다.</p>
<ul>
<li>운영 경로 전략이 바뀌면 → <code>_resolve_config_path</code>만 수정</li>
<li>파싱 방식이 바뀌면 → <code>_load_config</code>만 수정</li>
</ul>
<p>두 책임을 분리함으로써 각 메서드의 변경 이유가 명확해집니다.</p>
<hr>
<h2 id="2-configloaderget_config-통합-ocp--개방폐쇄-원칙">2. <code>ConfigLoader.get_config()</code> 통합 (OCP — 개방/폐쇄 원칙)</h2>
<h3 id="기존-코드-1">기존 코드</h3>
<pre><code class="language-python">def get_sftp_config(self):
    config = self._load_config(&#39;sftp.conf&#39;)
    return config[self.env]

def get_postgresql_config(self):
    config = self._load_config(&#39;postgresql.conf&#39;)
    return config[self.env]

def get_oracle_config(self):
    config = self._load_config(&#39;oracle.conf&#39;)
    return config[self.env]

def get_redis_cluster_config(self):
    config = self._load_config(&#39;redis_cluster.conf&#39;)
    return config[self.env]

def get_hive_config(self):
    config = self._load_config(&#39;hive.conf&#39;)
    return config[self.env]</code></pre>
<h3 id="개선된-코드-1">개선된 코드</h3>
<pre><code class="language-python">def get_config(self, service_name: str):
    &quot;&quot;&quot;
    서비스명에 해당하는 환경 설정 섹션을 반환합니다.
    새 서비스 추가 시 {service_name}.conf 파일만 추가하면 됩니다.
    &quot;&quot;&quot;
    return self._load_config(f&#39;{service_name}.conf&#39;)[self.env]</code></pre>
<p>사용 측 코드도 일관된 방식으로 통일됩니다.</p>
<pre><code class="language-python"># 변경 전
cfg = self.loader.get_sftp_config()
cfg = self.loader.get_postgresql_config()

# 변경 후
cfg = self.loader.get_config(&#39;sftp&#39;)
cfg = self.loader.get_config(&#39;postgresql&#39;)</code></pre>
<h3 id="왜-이렇게-개선해야-하는가-1">왜 이렇게 개선해야 하는가?</h3>
<p>OCP는 <strong>&quot;확장에는 열려 있고, 수정에는 닫혀 있어야 한다&quot;</strong> 는 원칙입니다.</p>
<p>기존 코드는 새 서비스(예: HDFS, ETT)를 추가할 때마다 <code>ConfigLoader</code>에 <code>get_xxx_config()</code> 메서드를 <strong>직접 추가(수정)</strong>해야 했습니다.
개선 후에는 <code>{service_name}.conf</code> 파일만 추가하면 <code>get_config(&#39;hdfs&#39;)</code>로 바로 사용할 수 있습니다. <code>ConfigLoader</code>를 수정할 필요가 없습니다.</p>
<hr>
<h2 id="3-baseconnection-추상-인터페이스-도입-lsp--리스코프-치환-원칙">3. <code>BaseConnection</code> 추상 인터페이스 도입 (LSP — 리스코프 치환 원칙)</h2>
<h3 id="기존-코드-2">기존 코드</h3>
<pre><code class="language-python">class Connection:
    def __init__(self):
        self.loader = ConfigLoader()  # 구현체 직접 생성

    def close(self, connections):
        for name in connections:       # 문자열로 attribute 이름 전달
            conn = getattr(self, name, None)
            try:
                if conn is not None:
                    conn.close()
            except Exception:
                pass</code></pre>
<h3 id="개선된-코드-2">개선된 코드</h3>
<pre><code class="language-python">from abc import ABC, abstractmethod

class BaseConnection(ABC):
    &quot;&quot;&quot;모든 연결 클래스의 추상 기반 클래스.&quot;&quot;&quot;

    @abstractmethod
    def connect(self):
        &quot;&quot;&quot;연결 객체를 생성하여 반환합니다.&quot;&quot;&quot;
        ...</code></pre>
<pre><code class="language-python"># 연결 해제는 모듈 수준 독립 함수로 분리
def close_connections(*conns) -&gt; None:
    &quot;&quot;&quot;서비스와 무관한 범용 연결 해제 함수.&quot;&quot;&quot;
    for conn in conns:
        if conn is None:
            continue
        try:
            conn.close()
        except Exception:
            pass</code></pre>
<p>사용 측 코드 변화:</p>
<pre><code class="language-python"># 변경 전 — 문자열로 attribute 이름 전달 (동작 보장 불가)
connection.Connection().close([&quot;conn_oracle&quot;, &quot;rc11&quot;])

# 변경 후 — 연결 객체를 직접 전달 (타입 안전)
connection.close_connections(self.conn_oracle, self.rc11)</code></pre>
<h3 id="왜-이렇게-개선해야-하는가-2">왜 이렇게 개선해야 하는가?</h3>
<p>LSP는 <strong>&quot;서브타입은 언제나 기반 타입으로 교체할 수 있어야 한다&quot;</strong> 는 원칙입니다.</p>
<p><code>BaseConnection</code>을 도입하면:</p>
<ul>
<li>모든 연결 클래스가 <code>connect()</code> 구현을 강제받아 <strong>인터페이스 일관성</strong>을 보장합니다.</li>
<li><code>OracleConnection</code>, <code>HiveConnection</code> 등이 <code>BaseConnection</code> 타입으로 다형적으로 사용될 수 있습니다.</li>
</ul>
<p>또한 기존 <code>close(connections)</code>는 문자열 attribute 이름 리스트를 받는 위험한 방식이었습니다.
<code>close_connections(*conns)</code>는 연결 객체를 직접 받으므로 <strong>런타임 오류 없이 안전하게 동작</strong>합니다.</p>
<hr>
<h2 id="4-constructor-injection-적용-dip--의존성-역전-원칙">4. Constructor Injection 적용 (DIP — 의존성 역전 원칙)</h2>
<h3 id="기존-코드-3">기존 코드</h3>
<pre><code class="language-python">class Connection:
    def __init__(self):
        self.loader = ConfigLoader()  # 구현체에 강하게 결합</code></pre>
<h3 id="개선된-코드-3">개선된 코드</h3>
<pre><code class="language-python">from typing import Optional

class _BaseServiceConnection(BaseConnection):
    def __init__(self, loader: Optional[ConfigLoader] = None):
        self.loader = loader or ConfigLoader()  # 외부 주입 or 기본값</code></pre>
<h3 id="왜-이렇게-개선해야-하는가-3">왜 이렇게 개선해야 하는가?</h3>
<p>DIP는 <strong>&quot;고수준 모듈이 저수준 모듈에 의존해서는 안 된다&quot;</strong> 는 원칙입니다.</p>
<p>기존 코드는 <code>Connection</code> 내부에서 <code>ConfigLoader()</code>를 직접 생성해 구현체에 강하게 결합되어 있었습니다.</p>
<p>Constructor Injection을 적용하면:</p>
<ul>
<li><strong>테스트 시</strong> Mock ConfigLoader를 주입해 실제 파일 시스템 없이 단위 테스트 가능</li>
<li><strong>확장 시</strong> 다른 설정 소스(환경변수, AWS Parameter Store 등)로 교체 가능</li>
</ul>
<pre><code class="language-python"># 테스트 코드 예시
mock_loader = MockConfigLoader()
conn = OracleConnection(loader=mock_loader)  # 실제 설정 파일 불필요</code></pre>
<hr>
<h2 id="5-서비스별-클래스-분리-srp--단일-책임-원칙">5. 서비스별 클래스 분리 (SRP — 단일 책임 원칙)</h2>
<h3 id="기존-코드-4">기존 코드</h3>
<pre><code class="language-python">class Connection:
    &quot;&quot;&quot;5가지 서비스 연결을 모두 담당하는 단일 클래스&quot;&quot;&quot;

    def sftp_connection(self): ...
    def redis_cluster_connection(self, cluster_section): ...
    def postgresql_connection(self): ...
    def oracle_connection(self): ...
    def hive_connection(self): ...</code></pre>
<h3 id="개선된-코드-4">개선된 코드</h3>
<pre><code class="language-python">class _BaseServiceConnection(BaseConnection):
    &quot;&quot;&quot;공통 기반: loader, _get_aes 공유&quot;&quot;&quot;
    ...

class SftpConnection(_BaseServiceConnection):
    def connect(self): ...   # SFTP만 책임

class RedisClusterConnection(_BaseServiceConnection):
    def connect(self, cluster_section: str = &#39;rc00&#39;): ...   # Redis만 책임

class PostgresqlConnection(_BaseServiceConnection):
    def connect(self): ...   # PostgreSQL만 책임

class OracleConnection(_BaseServiceConnection):
    def connect(self): ...   # Oracle만 책임

class HiveConnection(_BaseServiceConnection):
    def connect(self): ...   # Hive만 책임</code></pre>
<p>사용 측 코드:</p>
<pre><code class="language-python"># 직접 서비스 클래스를 사용
self.conn_oracle = connection.OracleConnection().connect()
self.rc11        = connection.RedisClusterConnection().connect(&#39;rc11&#39;)

# 해제
connection.close_connections(self.conn_oracle, self.rc11)</code></pre>
<h3 id="왜-이렇게-개선해야-하는가-4">왜 이렇게 개선해야 하는가?</h3>
<p>기존 <code>Connection</code> 클래스는 5가지 서비스의 연결 로직을 모두 보유하고 있었습니다.
Oracle 연결 방식이 바뀌면 <code>Connection</code> 전체를 수정해야 하고, 이는 SFTP나 Hive 연결에도 영향을 줄 위험이 있습니다.</p>
<p>분리 후에는:</p>
<ul>
<li><strong>변경 영향 범위 최소화</strong>: <code>OracleConnection</code>만 수정하면 됩니다.</li>
<li><strong>가독성 향상</strong>: 클래스 이름만 봐도 어떤 서비스인지 명확합니다.</li>
<li><strong>테스트 용이성</strong>: 서비스별로 독립적인 단위 테스트 작성 가능합니다.</li>
</ul>
<hr>
<h2 id="최종-클래스-구조">최종 클래스 구조</h2>
<pre><code>BaseConnection (ABC)                  ← 순수 인터페이스 계약
└── _BaseServiceConnection            ← 프로젝트 공통 기반 (loader, AES)
    ├── SftpConnection
    ├── RedisClusterConnection
    ├── PostgresqlConnection
    ├── OracleConnection
    └── HiveConnection

close_connections(*conns)             ← 모듈 수준 범용 연결 해제 함수
ConfigLoader                          ← 환경별 설정 파일 로드</code></pre><hr>
<h2 id="개선-전후-비교-요약">개선 전/후 비교 요약</h2>
<table>
<thead>
<tr>
<th>개선 항목</th>
<th>SOLID 원칙</th>
<th>핵심 효과</th>
</tr>
</thead>
<tbody><tr>
<td><code>_resolve_config_path</code> 분리</td>
<td>SRP</td>
<td>경로 탐색과 파일 파싱 책임 분리</td>
</tr>
<tr>
<td><code>get_config()</code> 메서드 통합</td>
<td>OCP</td>
<td>새 서비스 추가 시 코드 수정 불필요</td>
</tr>
<tr>
<td><code>BaseConnection</code> ABC 도입</td>
<td>LSP</td>
<td><code>connect()</code> 구현 강제, 다형성 보장</td>
</tr>
<tr>
<td>Constructor Injection</td>
<td>DIP</td>
<td>구현체 교체/테스트 가능 구조</td>
</tr>
<tr>
<td>서비스별 클래스 분리</td>
<td>SRP</td>
<td>변경 영향 범위 최소화</td>
</tr>
</tbody></table>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[좋은 개발자가 알아야 할 객체지향 설계 원칙 (OOP + SOLID)]]></title>
            <link>https://velog.io/@leemin-jae/%EC%A2%8B%EC%9D%80-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EC%95%8C%EC%95%84%EC%95%BC-%ED%95%A0-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-%EC%84%A4%EA%B3%84-%EC%9B%90%EC%B9%99-OOP-SOLID</link>
            <guid>https://velog.io/@leemin-jae/%EC%A2%8B%EC%9D%80-%EA%B0%9C%EB%B0%9C%EC%9E%90%EA%B0%80-%EC%95%8C%EC%95%84%EC%95%BC-%ED%95%A0-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-%EC%84%A4%EA%B3%84-%EC%9B%90%EC%B9%99-OOP-SOLID</guid>
            <pubDate>Sun, 05 Apr 2026 05:24:24 GMT</pubDate>
            <description><![CDATA[<h1 id="좋은-개발자가-알아야-할-객체지향-설계-원칙-oop--solid">좋은 개발자가 알아야 할 객체지향 설계 원칙 (OOP + SOLID)</h1>
<blockquote>
<p>코드를 짤 때 단순히 &quot;동작하는 코드&quot;를 넘어서, <strong>변경에 강하고 확장하기 쉬운 구조</strong>를 만드는 방법을 정리했습니다.</p>
</blockquote>
<hr>
<h2 id="📌-목차">📌 목차</h2>
<ol>
<li><a href="#1-%EC%84%A4%EA%B3%84--%EA%B5%AC%EC%A1%B0-%EA%B8%B0%EB%B3%B8-%EC%9B%90%EC%B9%99">설계 &amp; 구조 기본 원칙</a></li>
<li><a href="#2-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-4%EB%8C%80-%ED%95%B5%EC%8B%AC-%EA%B0%9C%EB%85%90">객체지향 4대 핵심 개념</a></li>
<li><a href="#3-solid-%EC%9B%90%EC%B9%99">SOLID 원칙</a></li>
<li><a href="#4-%EC%8B%A4%EC%A0%84-%EC%98%88%EC%A0%9C-ocp-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5">실전 예제: OCP 완전 정복</a></li>
</ol>
<hr>
<h2 id="1-설계--구조-기본-원칙">1. 설계 &amp; 구조 기본 원칙</h2>
<h3 id="srp--단일-책임-원칙">SRP — 단일 책임 원칙</h3>
<p>하나의 함수/클래스는 <strong>하나의 일</strong>만 한다.</p>
<pre><code class="language-python"># ❌ 나쁜 예시 — DB 저장 + 이메일 전송 + 로그 출력을 한 함수가 다 함
def handle_user(user):
    db.save(user)
    email.send(user.email, &quot;환영합니다!&quot;)
    print(f&quot;[LOG] 유저 생성: {user.name}&quot;)

# ✅ 좋은 예시 — 역할별로 분리
def save_user(user):
    db.save(user)

def send_welcome_email(user):
    email.send(user.email, &quot;환영합니다!&quot;)

def log_user_created(user):
    print(f&quot;[LOG] 유저 생성: {user.name}&quot;)</code></pre>
<blockquote>
<p>한 함수가 여러 역할을 하면, 하나를 고칠 때 다른 기능이 망가질 수 있다.</p>
</blockquote>
<hr>
<h3 id="dry--dont-repeat-yourself">DRY — Don&#39;t Repeat Yourself</h3>
<p>같은 로직이 두 곳 이상에 있으면, <strong>반드시 추상화</strong>하라.</p>
<pre><code class="language-javascript">// ❌ 할인가 계산이 두 곳에서 따로 구현됨
const priceA = originalA * 0.9;
const priceB = originalB * 0.9;

// ✅ 함수로 추출
const applyDiscount = (price, rate = 0.9) =&gt; price * rate;

const priceA = applyDiscount(originalA);
const priceB = applyDiscount(originalB);</code></pre>
<blockquote>
<p>나중에 할인율이 바뀌면? DRY를 지키면 한 곳만 수정하면 된다.</p>
</blockquote>
<hr>
<h3 id="kiss--keep-it-simple-stupid">KISS — Keep It Simple, Stupid</h3>
<p>동작하는 <strong>가장 단순한 방법</strong>을 선택하라.</p>
<pre><code class="language-python"># ❌ 불필요하게 복잡한 홀짝 판별
def is_even(n):
    return True if len([i for i in range(n) if i % 2 == 0]) == n // 2 + (1 if n % 2 == 0 else 0) else False

# ✅ 단순하게
def is_even(n):
    return n % 2 == 0</code></pre>
<blockquote>
<p>복잡한 코드 = 버그가 숨을 공간이 많아진다. 단순함이 곧 안정성이다.</p>
</blockquote>
<hr>
<h3 id="yagni--you-arent-gonna-need-it">YAGNI — You Aren&#39;t Gonna Need It</h3>
<p><strong>지금 당장 필요하지 않은 기능은 만들지 마라.</strong></p>
<pre><code class="language-python"># ❌ &quot;나중에 쓸 수도 있으니까...&quot; 하고 미리 만든 기능들
class UserService:
    def get_user(self): ...
    def export_to_csv(self): ...          # 아직 요구사항 없음
    def send_push_notification(self): ... # 아직 요구사항 없음
    def generate_report(self): ...        # 아직 요구사항 없음

# ✅ 지금 필요한 것만
class UserService:
    def get_user(self): ...</code></pre>
<blockquote>
<p>미리 만든 코드는 유지보수 비용만 늘린다. 요구사항은 항상 바뀐다.</p>
</blockquote>
<hr>
<h3 id="낮은-결합도--high-cohesion--low-coupling">낮은 결합도 — High Cohesion / Low Coupling</h3>
<p>모듈은 서로를 <strong>최대한 몰라야</strong> 한다.</p>
<pre><code class="language-javascript">// ❌ 강한 결합 — 결제 수단이 OrderService 안에 직접 박혀 있음
class OrderService {
  checkout(order) {
    const result = new KakaoPay().pay(order.amount);
    new NaverEmail().send(order.user, &quot;주문 완료&quot;);
  }
}

// ✅ 느슨한 결합 — 의존성 주입
class OrderService {
  constructor(paymentGateway, notifier) {
    this.paymentGateway = paymentGateway;
    this.notifier = notifier;
  }

  checkout(order) {
    this.paymentGateway.pay(order.amount);
    this.notifier.send(order.user, &quot;주문 완료&quot;);
  }
}

// 카카오페이 → 토스페이로 교체해도 OrderService 코드는 안 바뀜
const service = new OrderService(new TossPay(), new NaverEmail());</code></pre>
<blockquote>
<p>결합도가 높으면 A를 바꿀 때 B, C, D도 줄줄이 바꿔야 한다.</p>
</blockquote>
<hr>
<h2 id="2-객체지향-4대-핵심-개념">2. 객체지향 4대 핵심 개념</h2>
<h3 id="캡슐화-encapsulation">캡슐화 (Encapsulation)</h3>
<p>내부 구현은 숨기고, <strong>필요한 것만 외부에 공개</strong>한다.</p>
<pre><code class="language-python"># ❌ 누구나 잔액을 직접 조작 가능
class BankAccount:
    balance = 0

account = BankAccount()
account.balance = -99999  # 마음대로 조작 가능 💀

# ✅ 검증된 메서드로만 접근
class BankAccount:
    def __init__(self):
        self.__balance = 0  # private

    def deposit(self, amount):
        if amount &gt; 0:
            self.__balance += amount

    def withdraw(self, amount):
        if amount &gt; self.__balance:
            raise Exception(&quot;잔액 부족&quot;)
        self.__balance -= amount

    def get_balance(self):
        return self.__balance</code></pre>
<hr>
<h3 id="상속-inheritance">상속 (Inheritance)</h3>
<p>공통 기능을 부모에 두고, <strong>자식이 물려받아 확장</strong>한다.</p>
<pre><code class="language-python">class Animal:
    def __init__(self, name):
        self.name = name

    def breathe(self):
        print(f&quot;{self.name}이 숨을 쉰다&quot;)

class Dog(Animal):
    def bark(self):
        print(f&quot;{self.name}이 짖는다: 멍멍!&quot;)

class Cat(Animal):
    def meow(self):
        print(f&quot;{self.name}이 운다: 야옹!&quot;)

dog = Dog(&quot;바둑이&quot;)
dog.breathe()  # 부모 메서드 사용 가능
dog.bark()</code></pre>
<blockquote>
<p>⚠️ <strong>상속은 남용하면 독이다.</strong> &quot;is-a&quot; 관계일 때만 사용할 것.</p>
<ul>
<li>✅ Dog <strong>is a</strong> Animal → 상속 적절</li>
<li>❌ Car <strong>is a</strong> Engine → 상속 부적절 → <strong>조합(Composition)</strong> 을 사용</li>
</ul>
</blockquote>
<hr>
<h3 id="다형성-polymorphism">다형성 (Polymorphism)</h3>
<p>같은 인터페이스로 <strong>다른 동작</strong>을 한다.</p>
<pre><code class="language-python">class Shape:
    def area(self):
        raise NotImplementedError

class Circle(Shape):
    def __init__(self, r):
        self.r = r
    def area(self):
        return 3.14 * self.r ** 2

class Rectangle(Shape):
    def __init__(self, w, h):
        self.w, self.h = w, h
    def area(self):
        return self.w * self.h

# 어떤 도형이든 동일한 방식으로 처리 가능
shapes = [Circle(5), Rectangle(3, 4), Circle(2)]
for shape in shapes:
    print(shape.area())</code></pre>
<blockquote>
<p>if-else로 타입을 분기하는 코드가 보이면 → 다형성으로 바꿀 신호다.</p>
</blockquote>
<hr>
<h3 id="추상화-abstraction">추상화 (Abstraction)</h3>
<p>복잡한 내부는 감추고, <strong>단순한 인터페이스만 노출</strong>한다.</p>
<pre><code class="language-python">from abc import ABC, abstractmethod

class PaymentGateway(ABC):
    @abstractmethod
    def pay(self, amount: int) -&gt; bool:
        pass

class KakaoPay(PaymentGateway):
    def pay(self, amount):
        print(f&quot;카카오페이로 {amount}원 결제&quot;)
        return True

class TossPay(PaymentGateway):
    def pay(self, amount):
        print(f&quot;토스페이로 {amount}원 결제&quot;)
        return True

# 사용하는 쪽은 내부 구현을 몰라도 된다
def checkout(gateway: PaymentGateway, amount: int):
    gateway.pay(amount)</code></pre>
<hr>
<h2 id="3-solid-원칙">3. SOLID 원칙</h2>
<h3 id="s--단일-책임-원칙-srp">S — 단일 책임 원칙 (SRP)</h3>
<p>클래스도 하나의 책임만 가진다. <em>(위 내용 참고)</em></p>
<hr>
<h3 id="o--개방-폐쇄-원칙-ocp">O — 개방-폐쇄 원칙 (OCP)</h3>
<p><strong>확장에는 열려 있고, 수정에는 닫혀 있어야</strong> 한다.</p>
<pre><code class="language-python"># ❌ 새 결제수단 추가할 때마다 이 함수를 수정해야 함
def process_payment(type, amount):
    if type == &quot;kakao&quot;:
        ...
    elif type == &quot;toss&quot;:
        ...
    elif type == &quot;naver&quot;:  # 추가할 때마다 여기를 건드려야 함
        ...

# ✅ 새 결제수단은 클래스만 추가하면 됨 (기존 코드 수정 X)
class NaverPay(PaymentGateway):
    def pay(self, amount):
        print(f&quot;네이버페이로 {amount}원 결제&quot;)</code></pre>
<hr>
<h3 id="l--리스코프-치환-원칙-lsp">L — 리스코프 치환 원칙 (LSP)</h3>
<p><strong>자식 클래스는 부모 클래스를 완전히 대체</strong>할 수 있어야 한다.</p>
<pre><code class="language-python"># ❌ 위반 사례 — Rectangle을 상속했는데 동작이 깨짐
class Rectangle:
    def set_width(self, w): self.width = w
    def set_height(self, h): self.height = h
    def area(self): return self.width * self.height

class Square(Rectangle):
    def set_width(self, w):
        self.width = w
        self.height = w  # 정사각형이라 강제로 같게 만듦 💀

rect = Square()
rect.set_width(4)
rect.set_height(5)
print(rect.area())  # 25 출력 — Rectangle이라면 20이어야 함</code></pre>
<blockquote>
<p>부모 타입으로 써도 동일한 계약이 보장되어야 한다.</p>
</blockquote>
<hr>
<h3 id="i--인터페이스-분리-원칙-isp">I — 인터페이스 분리 원칙 (ISP)</h3>
<p><strong>사용하지 않는 메서드를 구현하도록 강요하지 마라.</strong></p>
<pre><code class="language-python"># ❌ 로봇은 밥을 안 먹는데 eat()을 구현해야 함
class Worker(ABC):
    @abstractmethod
    def work(self): pass
    @abstractmethod
    def eat(self): pass

class Robot(Worker):
    def work(self): print(&quot;일한다&quot;)
    def eat(self): pass  # 의미없는 구현 강요 💀

# ✅ 인터페이스를 분리
class Workable(ABC):
    @abstractmethod
    def work(self): pass

class Eatable(ABC):
    @abstractmethod
    def eat(self): pass

class Human(Workable, Eatable):
    def work(self): print(&quot;일한다&quot;)
    def eat(self): print(&quot;밥 먹는다&quot;)

class Robot(Workable):
    def work(self): print(&quot;일한다&quot;)</code></pre>
<hr>
<h3 id="d--의존성-역전-원칙-dip">D — 의존성 역전 원칙 (DIP)</h3>
<p><strong>구체가 아닌 추상에 의존</strong>하라.</p>
<pre><code class="language-python"># ❌ 고수준(OrderService)이 저수준(MySQL)에 직접 의존
class OrderService:
    def __init__(self):
        self.db = MySQLDatabase()  # 특정 DB에 묶여버림

# ✅ 추상화에 의존 → DB를 바꿔도 OrderService는 그대로
class Database(ABC):
    @abstractmethod
    def save(self, data): pass

class MySQLDatabase(Database):
    def save(self, data): ...

class MongoDB(Database):
    def save(self, data): ...

class OrderService:
    def __init__(self, db: Database):  # 추상화에만 의존
        self.db = db</code></pre>
<hr>
<h2 id="4-실전-예제-ocp-완전-정복">4. 실전 예제: OCP 완전 정복</h2>
<p>OCP를 처음 접하면 &quot;그럼 실제로 어떻게 호출해?&quot;라는 의문이 생긴다. 전체 흐름을 보자.</p>
<pre><code class="language-python">from abc import ABC, abstractmethod

# 1. 추상 인터페이스 정의
class PaymentGateway(ABC):
    @abstractmethod
    def pay(self, amount: int) -&gt; bool:
        pass

# 2. 각 결제수단 구현 (새 수단 추가 시 여기에만 클래스 추가)
class KakaoPay(PaymentGateway):
    def pay(self, amount):
        print(f&quot;카카오페이로 {amount}원 결제&quot;)

class TossPay(PaymentGateway):
    def pay(self, amount):
        print(f&quot;토스페이로 {amount}원 결제&quot;)

class NaverPay(PaymentGateway):
    def pay(self, amount):
        print(f&quot;네이버페이로 {amount}원 결제&quot;)

# 3. 사용하는 쪽 — 구체 클래스를 전혀 모름
class OrderService:
    def __init__(self, gateway: PaymentGateway):
        self.gateway = gateway

    def checkout(self, amount):
        self.gateway.pay(amount)

# 4. 실제 호출 — 사용자 선택에 따라 동적으로 결정
GATEWAYS = {
    &quot;kakao&quot;: KakaoPay,
    &quot;toss&quot;:  TossPay,
    &quot;naver&quot;: NaverPay,
}

def get_gateway(type: str) -&gt; PaymentGateway:
    gateway_class = GATEWAYS.get(type)
    if not gateway_class:
        raise ValueError(f&quot;지원하지 않는 결제수단: {type}&quot;)
    return gateway_class()

# 새 결제수단 추가 시 → GATEWAYS 딕셔너리에 한 줄만 추가하면 끝
user_choice = &quot;naver&quot;
service = OrderService(get_gateway(user_choice))
service.checkout(15000)  # 네이버페이로 15000원 결제</code></pre>
<h3 id="if-else-방식-vs-ocp-방식-비교">if-else 방식 vs OCP 방식 비교</h3>
<table>
<thead>
<tr>
<th>비교 항목</th>
<th>❌ if-else 방식</th>
<th>✅ OCP 방식</th>
</tr>
</thead>
<tbody><tr>
<td>결제수단 추가 시</td>
<td>함수 직접 수정</td>
<td>클래스 하나만 추가</td>
</tr>
<tr>
<td>기존 코드 영향</td>
<td>건드릴 때마다 버그 위험</td>
<td>기존 코드 무변경</td>
</tr>
<tr>
<td>테스트</td>
<td>전체 함수 다시 테스트</td>
<td>새 클래스만 테스트</td>
</tr>
</tbody></table>
<blockquote>
<p><code>OrderService</code>는 <code>NaverPay</code>가 뭔지 전혀 모른다. <code>pay()</code>라는 메서드가 있다는 것만 알 뿐이다. 이게 추상화의 힘이다.</p>
</blockquote>
<hr>
<h2 id="📐-전체-원칙-한눈에-보기">📐 전체 원칙 한눈에 보기</h2>
<pre><code>객체지향 설계
│
├── 기본 원칙
│   ├── SRP    → 하나의 함수/클래스는 하나의 일만
│   ├── DRY    → 중복 로직은 추상화로 제거
│   ├── KISS   → 항상 가장 단순한 방법을 선택
│   ├── YAGNI  → 지금 필요한 것만 만든다
│   └── 낮은 결합도 → 모듈은 서로를 최대한 모르게
│
├── 4대 개념
│   ├── 캡슐화  → 내부를 숨겨라
│   ├── 상속    → 공통을 물려받아라 (is-a 관계만)
│   ├── 다형성  → 같은 인터페이스, 다른 동작
│   └── 추상화  → 복잡함을 단순하게 노출
│
└── SOLID
    ├── S → 클래스는 하나의 책임만
    ├── O → 수정 말고 확장으로 해결
    ├── L → 자식은 부모를 완전히 대체 가능
    ├── I → 인터페이스는 작게 쪼개라
    └── D → 구체가 아닌 추상에 의존하라</code></pre><hr>
<h2 id="💡-마치며">💡 마치며</h2>
<p>OOP는 &quot;현실 세계를 코드로 모델링&quot;하는 것이 목적이 아니다.
<strong>변경에 강하고 확장하기 쉬운 구조</strong>를 만드는 것이 진짜 목적이다.</p>
<p>SOLID 원칙들은 서로 연결되어 있다.</p>
<ul>
<li>SRP를 지키면 → 자연스럽게 결합도가 낮아진다</li>
<li>OCP를 지키면 → DIP를 따르게 된다</li>
<li>DIP를 지키면 → 테스트하기 쉬운 코드가 된다</li>
</ul>
<p>원칙을 외우는 것보다, <strong>&quot;이 코드를 나중에 바꿔야 한다면 얼마나 힘들까?&quot;</strong> 를 항상 생각하는 습관이 더 중요하다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[IT 엔지니어를 위한 네트워크 입문] 5장 라우터/L3 스위치: 3계층 장비]]></title>
            <link>https://velog.io/@leemin-jae/IT-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A5%BC-%EC%9C%84%ED%95%9C-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EC%9E%85%EB%AC%B8-5%EC%9E%A5-%EB%9D%BC%EC%9A%B0%ED%84%B0L3-%EC%8A%A4%EC%9C%84%EC%B9%98-3%EA%B3%84%EC%B8%B5-%EC%9E%A5%EB%B9%84</link>
            <guid>https://velog.io/@leemin-jae/IT-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A5%BC-%EC%9C%84%ED%95%9C-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EC%9E%85%EB%AC%B8-5%EC%9E%A5-%EB%9D%BC%EC%9A%B0%ED%84%B0L3-%EC%8A%A4%EC%9C%84%EC%B9%98-3%EA%B3%84%EC%B8%B5-%EC%9E%A5%EB%B9%84</guid>
            <pubDate>Mon, 10 Feb 2025 18:51:25 GMT</pubDate>
            <description><![CDATA[<h1 id="it-엔지니어를-위한-네트워크-입문-5장-라우터l3-스위치-3계층-장비">[IT 엔지니어를 위한 네트워크 입문] 5장 라우터/L3 스위치: 3계층 장비</h1>
<h2 id="📌-라우터란">📌 라우터란?</h2>
<ul>
<li>네트워크에서 패킷의 경로를 지정해주는 장비</li>
<li>패킷의 목적지 IP 주소를 확인하고, 라우팅 테이블 기반 최적 경로로 포워딩</li>
<li>원격지 네트워크 연결 시 필수 장비</li>
</ul>
<hr>
<h2 id="🏗️-라우터의-동작-방식과-역할">🏗️ 라우터의 동작 방식과 역할</h2>
<ul>
<li>다양한 경로 정보 수집 → 최적 경로를 라우팅 테이블에 저장</li>
<li>패킷 도착 시 목적지 IP와 라우팅 테이블 비교 후 포워딩</li>
</ul>
<h3 id="🚀-라우터의-핵심-동작">🚀 라우터의 핵심 동작</h3>
<ol>
<li>목적지 주소가 라우팅 테이블에 없으면 패킷 버림</li>
<li>기존 2계층 헤더 제거 후 새로운 2계층 헤더 생성하여 전송</li>
</ol>
<hr>
<h2 id="1️⃣-경로-지정-routing">1️⃣ 경로 지정 (Routing)</h2>
<h3 id="✅-경로-정보-획득-방법">✅ 경로 정보 획득 방법</h3>
<ul>
<li><strong>다이렉트 커넥티드</strong>: IP 설정 시 인접 네트워크 정보 자동 획득</li>
<li><strong>스태틱 라우팅</strong>: 관리자가 직접 경로 입력</li>
<li><strong>다이나믹 라우팅</strong>: 라우터 간 경로 정보 자동 교환 (RIP, OSPF, BGP)</li>
</ul>
<h3 id="✅-패킷-포워딩-과정">✅ 패킷 포워딩 과정</h3>
<ol>
<li>목적지 IP와 라우팅 테이블 비교</li>
<li>최적 경로 선택 후 전송</li>
<li>경로 없을 시 패킷 드롭</li>
</ol>
<hr>
<h2 id="2️⃣-브로드캐스트-컨트롤">2️⃣ 브로드캐스트 컨트롤</h2>
<h3 id="🚀-주요-기능">🚀 주요 기능</h3>
<ul>
<li>브로드캐스트/멀티캐스트 트래픽 차단</li>
<li>네트워크 성능 저하 방지</li>
<li><strong>💡 스위치 vs 라우터</strong>: 스위치는 동일 네트워크 내 브로드캐스트 전파, 라우터는 브로드캐스트 도메인 분리</li>
</ul>
<hr>
<h2 id="3️⃣-프로토콜-변환">3️⃣ 프로토콜 변환</h2>
<h3 id="🔄-변환-과정">🔄 변환 과정</h3>
<ol>
<li>도착한 패킷의 2계층 헤더 제거</li>
<li>3계층 주소(IP) 확인 후 최적 경로 선택</li>
<li>새로운 2계층 헤더 생성 및 전송</li>
</ol>
<ul>
<li><strong>📢 서로 다른 네트워크 간 통신 가능!</strong></li>
</ul>
<hr>
<h2 id="🚀-라우팅-테이블-구성-요소">🚀 라우팅 테이블 구성 요소</h2>
<table>
<thead>
<tr>
<th>구성 요소</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>목적지 주소</td>
<td>패킷의 최종 목적지 네트워크</td>
</tr>
<tr>
<td>넥스트 홉 IP</td>
<td>다음 라우터의 IP 주소</td>
</tr>
<tr>
<td>나가는 인터페이스</td>
<td>패킷이 전송될 물리적 포트</td>
</tr>
</tbody></table>
<h3 id="🔼-특징">🔼 특징</h3>
<ul>
<li><strong>롱기스트 프리픽스 매치</strong>: 가장 긴 서브넷 마스크 일치 경로 우선 선택</li>
<li>출발지 주소는 고려하지 않음</li>
</ul>
<hr>
<h2 id="🔥-라우터의-경로-획득-방법">🔥 라우터의 경로 획득 방법</h2>
<table>
<thead>
<tr>
<th>방법</th>
<th>설명</th>
<th>우선순위</th>
</tr>
</thead>
<tbody><tr>
<td>다이렉트 커넥티드</td>
<td>IP 설정 시 자동 생성 (인터페이스 비활성화 시 삭제)</td>
<td>1</td>
</tr>
<tr>
<td>스태틱 라우팅</td>
<td>관리자 수동 입력 (소규모 네트워크 적합)</td>
<td>2</td>
</tr>
<tr>
<td>다이나믹 라우팅</td>
<td>라우터 간 자동 경로 교환 (대규모 네트워크/장애 우회 가능)</td>
<td>3</td>
</tr>
</tbody></table>
<hr>
<h2 id="⚡-라우팅-vs-스위칭">⚡ 라우팅 vs 스위칭</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>라우팅 (Routing)</th>
<th>스위칭 (Switching)</th>
</tr>
</thead>
<tbody><tr>
<td><strong>기능</strong></td>
<td>최적 경로 결정</td>
<td>패킷 전달</td>
</tr>
<tr>
<td><strong>기반 주소</strong></td>
<td>IP 주소</td>
<td>MAC/IP 주소</td>
</tr>
<tr>
<td><strong>동작 계층</strong></td>
<td>3계층</td>
<td>2계층 또는 3계층</td>
</tr>
</tbody></table>
<hr>
<h3 id=""></h3>
]]></description>
        </item>
        <item>
            <title><![CDATA[[IT 엔지니어를 위한 네트워크 입문] 4장 스위치 2계층 장비]]></title>
            <link>https://velog.io/@leemin-jae/IT-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A5%BC-%EC%9C%84%ED%95%9C-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EC%9E%85%EB%AC%B8-4%EC%9E%A5-%EC%8A%A4%EC%9C%84%EC%B9%98-2%EA%B3%84%EC%B8%B5-%EC%9E%A5%EB%B9%84</link>
            <guid>https://velog.io/@leemin-jae/IT-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A5%BC-%EC%9C%84%ED%95%9C-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EC%9E%85%EB%AC%B8-4%EC%9E%A5-%EC%8A%A4%EC%9C%84%EC%B9%98-2%EA%B3%84%EC%B8%B5-%EC%9E%A5%EB%B9%84</guid>
            <pubDate>Mon, 10 Feb 2025 16:56:50 GMT</pubDate>
            <description><![CDATA[<h1 id="it-엔지니어를-위한-네트워크-입문-4장-스위치---2계층-장비">[IT 엔지니어를 위한 네트워크 입문] 4장 스위치 - 2계층 장비</h1>
<h2 id="📌-스위치란">📌 스위치란?</h2>
<p>스위치는 <strong>2계층 주소인 MAC 주소를 기반으로 동작</strong>하며, 네트워크 중간에서 패킷을 받아 <strong>필요한 곳에만 전달</strong>하는 네트워크의 중재자 역할을 합니다.  </p>
<p>✅ <strong>기본 동작</strong>  </p>
<ul>
<li>별다른 설정 없이 네트워크에 연결해도 MAC 주소 기반 패킷 전달 가능  </li>
<li>네트워크를 논리적으로 분리하는 <strong>VLAN</strong> 기능  </li>
<li>네트워크 루프 방지 기능인 <strong>STP(Spanning Tree Protocol)</strong>  </li>
</ul>
<hr>
<h2 id="🏗️-스위치-동작-방식">🏗️ 스위치 동작 방식</h2>
<p>스위치는 <strong>MAC 주소 테이블</strong>을 사용하여 통신합니다.<br>MAC 주소 테이블에는 <strong>단말의 MAC 주소와 해당 단말이 연결된 포트 정보</strong>가 저장됩니다.  </p>
<h3 id="📋-mac-주소-테이블-예시">📋 MAC 주소 테이블 예시</h3>
<table>
<thead>
<tr>
<th>MAC 주소</th>
<th>포트</th>
</tr>
</thead>
<tbody><tr>
<td>1111:2222:3333</td>
<td>Eth1</td>
</tr>
</tbody></table>
<p>스위치는 <strong>패킷의 헤더에서 목적지 MAC 주소를 확인</strong>한 후, MAC 주소 테이블에서 해당 포트를 찾아 패킷을 전달합니다.  </p>
<hr>
<h2 id="🔄-스위치의-3가지-동작-방식">🔄 스위치의 3가지 동작 방식</h2>
<h3 id="1️⃣-플러딩-flooding">1️⃣ 플러딩 (Flooding)</h3>
<p>🚀 <strong>MAC 주소를 모를 때, 모든 포트로 패킷을 전송</strong>  </p>
<ul>
<li>스위치가 부팅되면 MAC 주소 테이블이 비어 있음  </li>
<li>패킷이 들어오면 <strong>목적지 MAC 주소를 확인</strong>하지만, 정보가 없으면 <strong>모든 포트에 같은 패킷을 전송</strong>  </li>
</ul>
<hr>
<h3 id="2️⃣-어드레스-러닝-address-learning">2️⃣ 어드레스 러닝 (Address Learning)</h3>
<p>📝 <strong>MAC 주소 테이블을 생성하고 유지하는 과정</strong>  </p>
<ul>
<li>패킷의 <strong>출발지 MAC 주소와 포트 정보를 기록</strong>  </li>
<li>이를 통해 같은 네트워크에서 반복되는 플러딩을 방지  </li>
</ul>
<p>✅ <strong>하지만!</strong>  </p>
<ul>
<li>브로드캐스트나 멀티캐스트 주소는 학습할 수 없음  </li>
</ul>
<hr>
<h3 id="3️⃣-포워딩--필터링-forwarding--filtering">3️⃣ 포워딩 / 필터링 (Forwarding / Filtering)</h3>
<p>🔍 <strong>MAC 주소 테이블을 활용한 효율적인 패킷 전달</strong>  </p>
<ul>
<li>스위치는 <strong>도착지 MAC 주소를 MAC 테이블과 비교</strong>하여 <strong>일치하는 포트로만 패킷을 포워딩</strong>  </li>
<li>동시에 다른 포트로는 <strong>전달하지 않음(필터링)</strong>  </li>
</ul>
<p>📢 <strong>즉!</strong>  </p>
<ul>
<li><strong>유니캐스트</strong> 트래픽은 <strong>포워딩/필터링</strong>  </li>
<li><strong>브로드캐스트(Broadcast) 및 멀티캐스트(Multicast)</strong> 트래픽은 <strong>플러딩</strong>  </li>
</ul>
<hr>
<h2 id="🌎-vlan-virtual-lan">🌎 VLAN (Virtual LAN)</h2>
<p>VLAN은 <strong>물리적 배치와 관계없이 네트워크를 논리적으로 분리</strong>하는 기술입니다.  </p>
<p>📌 <strong>VLAN을 사용하는 이유</strong>  </p>
<ul>
<li><strong>브로드캐스트 트래픽 감소</strong> → 성능 향상  </li>
<li><strong>보안 강화</strong> → 특정 네트워크만 접근 가능  </li>
<li><strong>관리 용이</strong> → 서비스별로 논리적 그룹화 가능  </li>
</ul>
<h3 id="🛠️-vlan-할당-방식">🛠️ VLAN 할당 방식</h3>
<table>
<thead>
<tr>
<th>방식</th>
<th>기준</th>
<th>특징</th>
</tr>
</thead>
<tbody><tr>
<td><strong>포트 기반 VLAN</strong></td>
<td>스위치의 포트</td>
<td>특정 포트에 연결된 단말기는 동일 VLAN에 속함</td>
</tr>
<tr>
<td><strong>MAC 기반 VLAN</strong></td>
<td>단말기의 MAC 주소</td>
<td>어느 포트에 접속해도 동일 VLAN에 속함</td>
</tr>
</tbody></table>
<p>📢 <strong>VLAN 간 통신을 하려면?</strong><br>→ 3계층 장비(라우터 또는 L3 스위치)가 필요!  </p>
<hr>
<h2 id="🔄-stp-spanning-tree-protocol">🔄 STP (Spanning Tree Protocol)</h2>
<p>STP는 <strong>네트워크 루프를 방지하는 프로토콜</strong>입니다.  </p>
<p>📌 <strong>네트워크 루프란?</strong><br>스위치가 MAC 주소 테이블을 갱신하는 과정에서 <strong>무한히 패킷이 전송되는 현상</strong><br>→ 네트워크 장애 발생 🚨  </p>
<p>📌 <strong>STP 동작 원리</strong><br>1️⃣ 스위치 간 정보를 교환하여 네트워크의 트리를 생성<br>2️⃣ 루프가 감지되면 특정 포트를 <strong>차단(blocking)</strong><br>3️⃣ 네트워크 변경이 감지되면 차단된 포트를 <strong>활성화</strong>  </p>
<p>📢 <strong>BPDU(Bridge Protocol Data Unit) 패킷을 통해 스위치 간 정보 교환</strong>  </p>
<p>✅ <strong>STP의 핵심 역할</strong>  </p>
<ul>
<li>루프 발생 시 <strong>자동으로 차단</strong>  </li>
<li>네트워크 변경 감지 시 <strong>자동으로 복구</strong>  </li>
</ul>
<hr>
<h2 id="🔥-정리">🔥 정리</h2>
<table>
<thead>
<tr>
<th>개념</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>MAC 주소 테이블</strong></td>
<td>MAC 주소와 포트를 매핑하여 패킷 전달</td>
</tr>
<tr>
<td><strong>플러딩</strong></td>
<td>MAC 주소를 모를 때 모든 포트로 패킷 전송</td>
</tr>
<tr>
<td><strong>어드레스 러닝</strong></td>
<td>MAC 주소 테이블을 생성하여 네트워크 효율화</td>
</tr>
<tr>
<td><strong>포워딩/필터링</strong></td>
<td>목적지 MAC을 확인 후 패킷을 전송(포워딩) 또는 차단(필터링)</td>
</tr>
<tr>
<td><strong>VLAN</strong></td>
<td>물리적 배치와 무관하게 논리적으로 네트워크 분리</td>
</tr>
<tr>
<td><strong>STP</strong></td>
<td>네트워크 루프를 방지하여 안정적인 네트워크 유지</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[[IT 엔지니어를 위한 네트워크 입문] 3장]]></title>
            <link>https://velog.io/@leemin-jae/IT-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A5%BC-%EC%9C%84%ED%95%9C-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EC%9E%85%EB%AC%B8-3%EC%9E%A5</link>
            <guid>https://velog.io/@leemin-jae/IT-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A5%BC-%EC%9C%84%ED%95%9C-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%EC%9E%85%EB%AC%B8-3%EC%9E%A5</guid>
            <pubDate>Sat, 01 Feb 2025 01:01:28 GMT</pubDate>
            <description><![CDATA[<h2 id="통신-방식">통신 방식</h2>
<h4 id="유니캐스트">유니캐스트</h4>
<ul>
<li>1대1 통신</li>
<li>출발지와 목적지가 1:1로 통신<h4 id="브로드-캐스트">브로드 캐스트</h4>
</li>
<li>1:모든 통신</li>
<li>동일 네트워크에 존재하는 모든 호스트가 목적<h4 id="멀티캐스트">멀티캐스트</h4>
</li>
<li>1:그룹 통신</li>
<li>하나의 출발지에서 다수의 특정 목적지로 데이터 ㅈ너송<h4 id="애니캐스트">애니캐스트</h4>
</li>
<li>1대1 통신(목적지 동일 그룹 내 1개 호스트)</li>
<li>다수의 동일 그룹 중 가장 가까운 호스트에서 응답</li>
</ul>
<table>
<thead>
<tr>
<th>타입</th>
<th>통신 대상</th>
<th>범위</th>
</tr>
</thead>
<tbody><tr>
<td>유니캐스트</td>
<td>1:1</td>
<td>전체 네트워크</td>
</tr>
<tr>
<td>브로드캐스트</td>
<td>1:모든</td>
<td>서브넷(로컬 네트워크)</td>
</tr>
<tr>
<td>멀티캐스트</td>
<td>1:그룹</td>
<td>정의된 구간</td>
</tr>
<tr>
<td>애니캐스트</td>
<td>1:1</td>
<td>전체 네트워크</td>
</tr>
</tbody></table>
<blockquote>
<p>구분은 출발지가 아니라 목적지 주소를 기준으로 구분한다.</p>
</blockquote>
<h4 id="bum-트래픽">BUM 트래픽</h4>
<ul>
<li>B(Broadcast), U(Unkonwn Unicast), M(Multicatst)</li>
</ul>
<p>언노운 유니캐스트는 목적지 주소는 명확히 명시되어있지만  네트워크 동작은 브로드캐스트와 같을때
스위치가 목적지에대한 주소를 학습 못한 상황에서 모든 포트로 전송함
BUM 트래픽이 많아지면 네트워크 성능이 저하됨</p>
<h3 id="macmedia-access-control-주소">MAC(Media access Control) 주소</h3>
<p>2계층에서 통신을 위해 네트워크 인터페이스에 할당된 고유 식별자</p>
<p>MAC 주소 : 48비트 16진수 12자리
00 | 00 | 00 | 00 | 00 | 00 |
앞의 24비트 = OUI -&gt; IEEE가 제조사에 할당하는 부분
뒤의 24비트 = UAA -&gt; 각 제조사에서 네트워크 구성 요소에 할당하는 부분</p>
<ul>
<li>MAC의 주소는 유일하지 않을 수 있음 동일 네트워크에서만 중복되지 않으면 됨</li>
</ul>
<h4 id="mac-주소-동작">MAC 주소 동작</h4>
<p>NIC는 자신의 주소를 가지고 있고 전기 신호가 들어오면 2계층에서 데이터 형태(패킷)으로 변환 내용을 확인하여 도착지 MAC 주소를 확인. 만약 도착지 MAC 주소가 자신이 갖고 있는 MAC 주소와 다르면 그 퍠킷 폐기. 처리해야 할 주소로 인지하면 패킷 정보를 상위 계층으로 넘겨준다.</p>
<ul>
<li><p>상위 계층으로 갈 경우 NIC 자체적으로 패킷을 처리하는 것이 아닌 OS나 애플리케이션에서 처리해 시스템 부하가 작용한다.</p>
</li>
<li><p>MAC 주소는 단말이 아닌 NIC에 종속된다. 단말은 NIC를 여러개 가질 수 있으므로 MAC 주소도 여러개 가질 수 잇음</p>
</li>
</ul>
<h3 id="ip-주소">IP 주소</h3>
<p>2계층 물리 주소인 MAC 사용
3계층 논리 주소인 IP 주소를 사용</p>
<ul>
<li>3계층 주소 특징<ol>
<li>사용자가 변경 가능한 논리 주소</li>
<li>주소에 레벨이 있고. 그룹을 의미하는 네트워크 주소와 호스트 주소로 나뉨</li>
</ol>
</li>
</ul>
<h4 id="ip-주소-체계">IP 주소 체계</h4>
<p>IPv4 = 32비트 IPv6 = 128비트</p>
<ul>
<li><p>네트워크 주소
호스트들을 모은 네트워크를 지칭하는 주소.</p>
</li>
<li><p>호스트 주소 
하나의 네트워크 내에 존재하는 호스트를 구분하기 위한 주소01</p>
</li>
</ul>
<p>구분자 위치에 따라 A,B,C,D,E 클래스로 구분된다.
클래스를 도입하므로 고정 네트워크에 비해 주소를 절약할 수 잇다는 장점이 있음</p>
<h4 id="클래스풀과-클래스리스">클래스풀과 클래스리스</h4>
<p>클래스 기반의 IP 주소 체계 = 클래스풀 -&gt; 서브넷 마스트 필요없음</p>
<p>클래스 풀 기반으로는 IP 주소 부족과 낭비 문제가 발생하여 이것을 해결하기 위해 3가지 보존, 전환전략을 만들어냄</p>
<ol>
<li>클래스리스, CIDR(Classless Inter-Domain Routing) 기반 주소 체계 -&gt; 단기 </li>
<li>NAT와 사설 IP 주소 -&gt; 중기</li>
<li>차세대 IP IPv6 -&gt; 장기 대책</li>
</ol>
<h3 id="tcp-udp">TCP UDP</h3>
<p>계층 간 데이터 전송 중 가장 중요한 2가지</p>
<ol>
<li>각 계층에서 정의하는 정보 -&gt; 4계층 ACK번호</li>
<li>상위 프로토콜 지시자 정보 -&gt; 2계층 이더 타입, 3계층 프로토콜 번호, 4계층 포트 번호&#39;</li>
</ol>
<p>4계층의 목적 - 애플리케이션에서 사용하는 프로세스를 정확히 찾아가고 데이터를 분할한 패킷을 잘 쪼개고 잘 조립하기</p>
<h4 id="tcp">TCP</h4>
<p>TCP 프로토콜은 신회할 수 없는 공용망에서도 정보 유실 없는 통신을 보장하기 위해 세션을 안전하게 연결하고 데이터가 잘 전송되었는지 확인하는 기능이 있음.
패킷에 번호(Sequence Number)를 부여하고 잘 전송되었는지 응답(Acknowledge Number)하고 전송 크기(Window size)를 고려해 통신함</p>
<h4 id="udp">UDP</h4>
<p>데이터 전송을 보장하지 않는 프로토콜, 단방향으로 다수의 단말과 통신해 응답을 받기 어려운 경우에 사용된다.</p>
<h3 id="arpaddress-resolution-protocol">ARP(Address Resolution Protocol)</h3>
<p>상대방의 MAC주소를 알아내기 위해 사용되는 프로토콜</p>
<h4 id="apr이란">APR이란</h4>
<p>데이터 통신은 2계층 물리적 주소인 MAC과 3계층 논리적 IP 주소 두 개가 사용됨
두 주소는 전혀 연관성이 없으므로 두개의 주소를 연계시켜 주기 위한 메커니즘이 APR</p>
<ol>
<li><p>의 MAC 주소를 알아내려면 ARP 브로드캐스트를 이용 네트워크 전체에 상대방의 MAC 주소를 질의한다.</p>
</li>
<li><p>로드캐스트를 받은 목적지는 ARP 프로토콜을 이용해 자신의 MAC 주소를 응답.</p>
</li>
<li><p>작업이 완료 후 출발지, 목적지 둘다 상대의 MAC 주소를 학습하고 패킷이 정상적으로 인캡슐레이션 되어 상대방에게 전달됨</p>
</li>
</ol>
<ul>
<li>NAT,NIC</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Optional 주의해서 사용하기]]></title>
            <link>https://velog.io/@leemin-jae/Optional-%EC%A3%BC%EC%9D%98%ED%95%B4%EC%84%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@leemin-jae/Optional-%EC%A3%BC%EC%9D%98%ED%95%B4%EC%84%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sun, 10 Mar 2024 15:33:45 GMT</pubDate>
            <description><![CDATA[<h3 id="서론">서론</h3>
<p>이번에 Optional을 정리하면서 여러 자료를 찾아보던 중 Optional과 관련된 포스팅 하나가 정말 정리가 잘되었다고 생각하게 되었다.
<a href="https://dzone.com/articles/using-optional-correctly-is-not-optional">https://dzone.com/articles/using-optional-correctly-is-not-optional</a>
<a href="https://www.latera.kr/blog/2019-07-02-effective-optional/">https://www.latera.kr/blog/2019-07-02-effective-optional/</a>
<a href="https://dev-coco.tistory.com/178">[Java] Optional 올바르게 사용하기</a>
블로그에 정리...를 빙자한 배껴쓰기를 하면서 내 머리에도 정리가 될 수 있도록 꼼꼼히 읽으면서 정리하려고 한다.</p>
<hr>
<h3 id="optional-올바르게-사용하기">Optional 올바르게 사용하기</h3>
<ol>
<li>Optional에 절대 null을 할당하면 안된다.
나쁜 예 :<pre><code>Optional&lt;Member&gt; findById(Long id) {
// find Member from db    
 if (result == 0) {
 return null;        
 }
}</code></pre>좋은 예 :<pre><code>Optional&lt;Member&gt; findById(Long id) { 
 // find Member from db    
   if (result == 0) {
       return Optional.empty();    
   }
}</code></pre></li>
</ol>
<p>당연히 Optional 대신 null 을 반환하는 것은 Optional 의 도입 의도와 맞지 않는다.
Optional 은 내부 값을 null 로 초기화한 싱글톤 객체를 <strong><code>Optional.empty()</code></strong> 메소드를 통해 제공하고 있다.
위에서 말한 &quot;결과 없음&quot;을 표현해야 하는 경우라면 <strong>null 대신</strong> <strong>Optional.empty()</strong>  를 반환하자.</p>
<ol start="2">
<li><p><code>Optional.get()</code> 호출 전에 Optional 객체가 값을 가지고 있음을 확실히 할 것
<code>Optional.get()</code> 메소드를 통해 해당 값을 접근 할 수 있다.
만약 빈 Optional 객체에 get() 메소드를 호출한 경우 NoSuchElementException 이 발생하기 때문에 값을 가져오기 전에 반드시 값이 있는지 확인해야 한다.</p>
</li>
<li><p>값이 없는 경우, Optional.orElse() , Optional.orElseGet()을 통해 값을 제공한다.
이전 글에서 정리했던 것 처럼 orElse는 Optional에 값이 있던 없던 무조건 실행된다.
만약 값이 있다면 인자로 실행된 값은 무시되고 버려진다.</p>
<ul>
<li>orElse는 이미 생성된었거나 계산된 값일 때만 사용한다.</li>
<li>그렇지 않을 경우는 orElseGet()을 통해 불필요한 연산을 하지 않게 한다.</li>
</ul>
</li>
<li><p>값이 없는 경우, <code>Optional.orElseThrow()</code> 를 통해 명시적으로 예외를 던질 것</p>
<p>예시</p>
<pre><code class="language-java">Member member = findById(1).orElseThrow(() -&gt; new NoSuchElementException(&quot;Member Not Found&quot;));</code></pre>
</li>
<li><p>isPresent() - get() 은 orElse() 나 orElseXXX 등으로 대체</p>
<pre><code class="language-java">Optional&lt;Member&gt; optionalMember = findById(1);
if(optionalMember.isPresent()) {
  System.out.println(&quot;member : &quot; +optionalMember.get());
} else {
  throw new MemberNotFoundException(&quot;Member Not Found id : &quot; + 1);
}</code></pre>
<p>좋은 예:</p>
<pre><code class="language-java">Member member = findById(1)        
          .orElseThrow(() -&gt; new MemberNotFoundException(&quot;Member not found id : &quot; + 1));
System.out.println(&quot;member : &quot; + member.get());</code></pre>
</li>
<li><p>Optional 을 생성자나 메소드 인자로 사용하지 말 것</p>
<p>Optional 을 생성자나 메소드 인자로 사용하면, 호출할 때마다 Optional 을 생성해서 인자로 전달해줘야 한다.굳이 비싼 Optional 을 인자로 사용하지 말고 호출되는 쪽에 null 체크 책임을 남겨두는 것이 좋다.</p>
<pre><code class="language-java">     void increaseSalary (Member member,int salary){
         if (member != null) {
             member.increaseSalary(salary);
         }
     }</code></pre>
</li>
<li><p>단지 값을 얻을 목적이라면 Optional 대신 null 비교</p>
<p>Optional 은 비싸기 때문에 과도하게 사용하지 말아야 한다.단순히 값 또는 null 을 얻을 목적이라면 Optional 대신 null 비교를 사용하자</p>
<pre><code class="language-java">return Optional.ofNullable(member).orElse(UNKNOWN);
//변경
return member != null ? member : UNKNOWN;</code></pre>
<ol start="8">
<li>Optional 을 빈 컬렉션이나 배열을 반환하는 데 사용하지 말 것</li>
</ol>
<p>컬렉션이나 배열로 복수의 결과를 반환하는 메소드가 &quot;결과 없음&quot;을 가장 명확하게 나타내는 방법은 대부분의 경우 빈(empty) 컬렉션 또는 배열을 반환하는 방법이다.</p>
<pre><code class="language-java">List&lt;Member&gt; members = team.getMember();
return Optional.ofNullable(members);

//변경

List&lt;Member&gt; members = team.getMembers();
return members != null ? members : Collections.emptyList();</code></pre>
<p>JPA에서도 컬렉션을 Optional로 감싸는 것은 좋지 못하다.
컬렉션을 반환하는 Spring Data JPA Repository 메소드는 null 을 반환하지 않고 비어있는 컬렉션을 반환해주므로 Optional 로 감싸서 반환 할 필요가 없다.</p>
<pre><code class="language-java">public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
   Optional&lt;List&lt;Member&gt;&gt; findAllByNameContaining(String keyword);
}

//변경

public interface MemberRepository extends JpaRepository&lt;Member, Long&gt; {
   List&lt;Member&gt; findAllByNameContaining(String keyword);
}</code></pre>
</li>
</ol>
<ol start="9">
<li><p><code>Optional.of()</code> 와 <code>Optional.ofNullable()</code> 을 혼동하지 말 것</p>
<p>of(X)  는 X 가 null 이 아님이 확실할 때만 사용해야 하며, X 가 null 이면 NullPointerException이 발생 한다.
ofNullable(X)  은 X가 null 일 가능성이 있을 때 사용해야 하며, X 가 null 이 아님이 확실하면 of(X) 를 사용해야 한다.</p>
<pre><code class="language-java">//나쁜 예 :
return Optional.of(member.getName()); // member의 name이 null 이면 NPE 발생 
return Optional.ofNullable(MEMBER_STATUS);
//좋은 예 :
return Optional.ofNullable(member.getName());
return Optional.of(MEMBER_STATUS);</code></pre>
</li>
<li><p>원시 타입의 Optional 에는 <code>OptionalInt</code> , <code>OptionalLong</code> , <code>OptionalDouble</code> 사용을 고려할 것</p>
<p>원시 타입(primitive type)을 Optional 로 사용하면 Boxing 과 UnBoxing 을 거치면서 오버헤드가 생기게 된다.
반드시 Optional 의 제네릭 타입에 맞춰야 하는 경우가 아니라면 int , long , double 타입에는 OptionalXXX 타입 사용을 고려하는 것이 좋다.
이들은 내부 값을 래퍼 클래스가 아닌 원시 타입으로 갖고, 값의 존재 여부를 나타내는 isPresent 필드를 함께 갖는 구현체들이다.</p>
<pre><code class="language-java">//나쁜 예 :
Optional&lt;Integer&gt; cnt = Optional.of(10); // boxing 발생
for(int i = 0; i &lt; cnt.get(); i++) { ... } // unboxing 발생
//좋은 예 :
OptionalInt cnt = OptionalInt.of(10); // boxing 발생 안 함
for(int i = 0; i &lt; cnt.getAsInt(); i++) { ... } // unboxing 발생 안 함
</code></pre>
<ol start="15">
<li>내부 값 비교에는 <code>Optional.equals</code> 사용을 고려할 것</li>
</ol>
</li>
</ol>
<p>  기본적인 참조 확인과 타입 확인 이후에 두 Optional 의 동치성은 내부 값의 equals 구현이 결정한다.
  즉, Optional 객체 maybeA 와 maybeB 의 두 내부 객체 a 와 b 에 대해 a.equals(b)  가 true 이면maybeA.equals(maybeB)  도 true  이며 그 역도 성립한다.
  굳이 내부 값의 비교만을 위해 값을 꺼낼 필요는 없다는 의미이다.</p>
<pre><code class="language-java">  //나쁜 예 :
boolean compareMemberById(long id1, long id2) {
      Optional&lt;Member&gt; maybeMemberA = findById(id1);
      Optional&lt;Member&gt; maybeMemberB = findById(id2);
      if(!maybeMemberA.isPresent() &amp;&amp; !maybeMemberB.isPresent()) {return false; }    
      if (maybeMemberA.isPresent() &amp;&amp; maybeMemberB.isPresent()) {
          return maybeMemberA.get().equals(maybeMemberB.get());    
      }    
      return false;
}
//좋은 예 :
boolean compareMemberById(long id1, long id2) {
      Optional&lt;Member&gt; maybeMemberA = findById(id1);
      Optional&lt;Member&gt; maybeMemberB = findById(id2);
      if(!maybeMemberA.isPresent() &amp;&amp; !maybeMemberB.isPresent()) { return false; }
      return findById(id1).equals(findById(id2));
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JPA] 자바 ORM 표준 JPA 프로그래밍 - 영속성 관리]]></title>
            <link>https://velog.io/@leemin-jae/JPA-%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%EC%98%81%EC%86%8D%EC%84%B1-%EA%B4%80%EB%A6%AC</link>
            <guid>https://velog.io/@leemin-jae/JPA-%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%EC%98%81%EC%86%8D%EC%84%B1-%EA%B4%80%EB%A6%AC</guid>
            <pubDate>Thu, 07 Mar 2024 15:04:14 GMT</pubDate>
            <description><![CDATA[<ul>
<li><p>영속성 컨텍스트(PersistenceContext) : 엔티티를 영구 저장하는 환경</p>
<ul>
<li>EntityManager.persist(entity) : 사실 DB가 아니라 영속성 컨텍스트에 저장한다는 뜻</li>
<li>엔티티 매니저를 통해서 영속성 컨텍스트에 접근</li>
</ul>
</li>
<li><p>엔티티 생명주기</p>
<ul>
<li>비영속 : 컨텍스트와 전혀 <code>관계없는 새로운</code> 상태</li>
<li>영속 : <code>컨텍스트에 관리</code>되는 상태</li>
<li>준영속 : 저장되었다 <code>분리</code>된 상태</li>
<li>삭제 : <code>삭제</code>된 상태</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/leemin-jae/post/2a809a74-0a7a-42b2-874b-8b0be545f1ff/image.png" alt=""></p>
<pre><code class="language-java">//객체를 생성한 상태(비영속)
Member member = new Member();
member.setId(&quot;member1&quot;);
////

EntityManager em = emf.createEntityManager();
em.getTransation().begin();

//객체를 저장한 상태(영속)   -&gt;  영속성 컨텍스트에 저장되었을 뿐 DB에 저장된게 아님
em.persist(member);
//

//컨텍스트에서 분리
em.detach(member);

//객체를 삭제
em.remove(member);</code></pre>
<h3 id="영속성-컨택스트의-이점">영속성 컨택스트의 이점</h3>
<ul>
<li><p>1차 캐시</p>
</li>
<li><p>동일성 보장</p>
</li>
<li><p>트랜잭션을 지원하는 쓰기 지연</p>
<ul>
<li>배치 기능이 가능</li>
</ul>
</li>
<li><p>변경 감지(더티 체킹)</p>
<ul>
<li><p>set함수로 값 변경 시 데이터 Update 가능</p>
</li>
<li><p>JPA는 commit 시점에 flush가 발생하면서 엔티티와 스냅샷(가져온 시점의 값)을 비교 후 update 쿼리를 쓰기 지연 sql 저장소에 보냄</p>
</li>
</ul>
</li>
</ul>
<ul>
<li>지연 로딩</li>
</ul>
<h3 id="플러시">플러시</h3>
<ul>
<li><p>영속성 컨텍스트의 변경 내용을 데이터베이스에 반영</p>
</li>
<li><p>기능</p>
<ul>
<li><p>변경 감지</p>
</li>
<li><p>수정된 엔티티 쓰기 지연 SQL 저장소 등록</p>
</li>
<li><p>쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송 (등록, 수정, 삭제)</p>
</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JPA] 자바 ORM 표준 JPA 프로그래밍 - 기본편 - JPA 소개]]></title>
            <link>https://velog.io/@leemin-jae/JPA-%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%EA%B8%B0%EB%B3%B8%ED%8E%B8-JPA-%EC%86%8C%EA%B0%9C</link>
            <guid>https://velog.io/@leemin-jae/JPA-%EC%9E%90%EB%B0%94-ORM-%ED%91%9C%EC%A4%80-JPA-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%EA%B8%B0%EB%B3%B8%ED%8E%B8-JPA-%EC%86%8C%EA%B0%9C</guid>
            <pubDate>Tue, 05 Mar 2024 15:04:29 GMT</pubDate>
            <description><![CDATA[<h1 id="섹션-1-jpa-소개">섹션 1. JPA 소개</h1>
<h2 id="jpa">JPA</h2>
<ul>
<li>Java Persistence API</li>
<li>자바 진영의 ORM 기술 표준</li>
</ul>
<h3 id="orm">ORM?</h3>
<ul>
<li>Object-relational mapping(객체 관계 매핑)</li>
<li>객체는 객체 대로 db는 db 대로 설계</li>
<li>orm 프레임워크가 중간에서 매핑</li>
</ul>
<p><img src="https://velog.velcdn.com/images/leemin-jae/post/8b04b501-8988-48b1-9753-b696376ec30c/image.PNG" alt=""></p>
<blockquote>
<p>JPA는 애플리케이션과 JDBC 사이에서 동작</p>
</blockquote>
<ul>
<li><strong>JPA는 인터페이스의 모음</strong></li>
</ul>
<h3 id="jpa를-사용해야-하는-이유">JPA를 사용해야 하는 이유</h3>
<ul>
<li>SQL 중심 개발에서 객체 중심 개발</li>
<li>생산성</li>
<li>유지 보수</li>
<li>패러다임의 불일치 해결</li>
<li>성능</li>
<li>데이터 접근 추상화와 벤더 독립성</li>
<li>표준</li>
</ul>
<h4 id="생산성---crud">생산성 - CRUD</h4>
<ul>
<li>저장 : jpa.persist(member)</li>
<li>조회 : Member member = jpa.find(memberId);</li>
<li>수정 : member.setName(&quot;변경할 이름&quot;)</li>
<li>삭제 : jpa.remove(member)</li>
</ul>
<h4 id="유지보수">유지보수</h4>
<ul>
<li>기존 : 필드 변경 시 모든 sql 수정 -&gt; JPA : 필드 변경 시 나머지 sql은 JPA가 처리해준다.</li>
</ul>
<h4 id="상속-가능--연관관계-객체-그래프-탐색---신뢰-할-수-있는-엔티티-계층">상속 가능 , 연관관계, 객체 그래프 탐색 -&gt; 신뢰 할 수 있는 엔티티, 계층</h4>
<h4 id="성능-최적화-기능">성능 최적화 기능</h4>
<ul>
<li>1차 캐시와 동일성(identity) 보장<ul>
<li>JPA는 같은 트랜잭션 안에서는 같은 엔티티를 반환 - 약간의 조희 성능 향상</li>
</ul>
</li>
<li>트랜잭션을 지원하는 쓰기 지연<ul>
<li>트랜잭션을 커밋 할 때까지 INSERT SQL을 모음<ul>
<li>JDBC BATCH SQL 기능을 사용해서 한번에 SQL 전송</li>
</ul>
</li>
<li>UPDATE, DELETE로 인한 로우(ROW)락 시간 최소화<ul>
<li>트랜잭션 커밋 시 UPDATE, DELETE SQL 실행하고, 바로 커밋</li>
</ul>
</li>
</ul>
</li>
<li>지연 로딩<ul>
<li>지연 로딩 : 객체가 실제 사용될 때 로딩</li>
<li>즉시 로딩 : JOIN SQL로 한번에 연관된 객체까지 미리 조회</li>
</ul>
</li>
</ul>
<h4 id="주의사항">주의사항</h4>
<ul>
<li><p>JPA는 항상 트랜잭션 안에서 실행되어야 한다.</p>
</li>
<li><p>엔티티 매니저 팩토리는 하나만 생성해서 애플리케이션 전체에서 공유</p>
</li>
<li><p>엔티티 매니저는 쓰레드 간 공유X(사용하고 버려야 한다.)</p>
</li>
<li><p>JPA의 모든 데이터 변경은 트랜잭션 안에서 실행</p>
</li>
</ul>
<h4 id="jpql">JPQL</h4>
<ul>
<li><p>JPA를 사용하면 엔티티 객체를 중심으로 개발</p>
</li>
<li><p>문제는 검색 쿼리</p>
</li>
<li><p>검색을 할 때도 테이블X 엔티티 객체를 대상으로 검색</p>
</li>
<li><p>모든 db 데이터를 객체로 변환해서 검색은 불가능</p>
</li>
<li><p>애플리케이션이 필요한 데이터만 db에서 불러오려면 결국 검색 조건이 포함된 sql이 필요</p>
<blockquote>
<p>JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어 제공</p>
<blockquote>
<p><strong>JPQL</strong> - 엔티티 객체를 대상으로 쿼리</p>
<pre><code> **SQL** - 데이터베이스 테이블을 대상으로 쿼리.</code></pre></blockquote>
</blockquote>
</li>
</ul>
<blockquote>
<p>ORM은 <strong>객체</strong>와 <strong>RDB</strong> 두 기둥 위에 있는 기술</p>
</blockquote>
<hr>
<h3 id="마치며">마치며</h3>
<p>이미 전 부터 JPA를 사용해서 몇 번 프로젝트를 개발해 왔지만 항상 사용하면서 이렇게 사용하는게 맞을까?? 라는 고민을 많이 했었다. 새로 프로젝트를 시작하면서 모르는 부분을 채우고 알던 확실하게 다지는 기회가 되도록 유명한 김영한 선생님의 인강을 정리하면서 공부하기로 결정했고 배운 부분을 바로바로 프로젝트에 적용할 수 있도록 하겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JPA]Jpa Auditing 기능 사용하기]]></title>
            <link>https://velog.io/@leemin-jae/JPAJpa-Auditing-%EA%B8%B0%EB%8A%A5-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@leemin-jae/JPAJpa-Auditing-%EA%B8%B0%EB%8A%A5-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0</guid>
            <pubDate>Tue, 05 Mar 2024 12:44:45 GMT</pubDate>
            <description><![CDATA[<p>이전에 프로젝트를 하면서 JPA 데이터베이스 테이블에 도메인을 매핑 할 때 생성일자나 수정일 등 중복되는 필드,컬럼들이 많았었다.</p>
<p>그 때는 모든 도메인에 해당 컬럼을 만들면서 진행하였다. 그러다보니 같은 코드를 계속 작성하며 시간을 낭비하고 다른 도메인을 담당한 팀원들과 컬럼명이 다르게 적고 나중에 깨닫는 사고도 경험했었다.</p>
<p>그러다 중복을 처리해주는 JPA Auditiong이라는 것을 알게 되었고 새 프로젝트에서는 이 기능을 사용하기 위해 정리하려고 한다.</p>
<h3 id="jpa-auditing">JPA Auditing</h3>
<p>JPA에서는 Audit이라는 기능을 제공한다 Audit은 Spring Data JPA에서 시간에 대해서 자동으로 값을 넣어주는 기능이다. 
도메인을 영속성 컨텍스트에 저장하거나 조회를 수행한 후에 update를 하는 경우 매번 시간 데이터를 입력하여 주어야 하는데, Audit을 이용하면 자동으로 시간을 매핑하여 데이터베이스의 테이블에 넣어주게 된다. 
JPA Audit은 데이터의 변경 기록을 추적하고 데이터 변화에 대한 이력을 기록하는 것을 목적으로 한다.</p>
<hr>
<h3 id="auditing-활성화">Auditing 활성화</h3>
<pre><code class="language-java">@SpringBootApplication
@EnableJpaAuditing
public class CosmosApplication {

    public static void main(String[] args) {
        SpringApplication.run(CosmosApplication.class, args);
    }

}</code></pre>
<p>SpringBootApplication에 <code>@EnableJpaAuditing</code> 어노테이션을 추가</p>
<h3 id="baseentity-생성하기">BaseEntity 생성하기</h3>
<pre><code class="language-java">@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime modifiedDate;
}</code></pre>
<p><strong>@MappedSuperclass</strong></p>
<ul>
<li>Entity에서 Table에 대한 공통 매핑 정보가 필요할 때 부모 클래스에 정의하고 상속받아 해당 필드를 사용하여 중복을 제거</li>
</ul>
<p><strong>@EntityListeners</strong></p>
<ul>
<li>Entity를 DB에 적용하기 이전, 이후에 커스텀 콜백을 요청할 수 있는 어노테이션</li>
</ul>
<p><strong>Class AuditingEntityListener (org.springframework.data.jpa)</strong></p>
<ul>
<li>Entity 영속성 및 업데이트에 대한 Auditing 정보를 캡처하는 JPA Entity Listener</li>
</ul>
<p><strong>@CreatedDate (org.springframework.data)</strong></p>
<ul>
<li>데이터 생성 날짜 자동 저장 어노테이션</li>
</ul>
<p><strong>@LastModifiedDate (org.springframework.data)</strong></p>
<ul>
<li>데이터 수정 날짜 자동 저장 어노테이션</li>
</ul>
<p><strong>@CreatedBy (org.springframework.data)</strong></p>
<ul>
<li>데이터 생성자 자동 저장 어노테이션</li>
</ul>
<p><strong>@LastModifiedBy (org.springframework.data)</strong></p>
<ul>
<li>데이터 수정자 자동 저장 어노테이션</li>
</ul>
<hr>
<h3 id="entity-적용">Entity 적용</h3>
<pre><code class="language-java">@Getter
@Entity
@NoArgsConstructor(access = PROTECTED)
public class Board extends BaseEntity {

    @Id
    private Long id;

}</code></pre>
<p><img src="https://velog.velcdn.com/images/leemin-jae/post/91c03ea8-f826-4511-a800-4028b9a3b6ff/image.png" alt=""></p>
<p>선언한 변수는 id뿐이지만 2개의 컬럼이 추가된 것을 확인 할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Boot 3에 Swagger 적용하기(springdoc-openapi)]]></title>
            <link>https://velog.io/@leemin-jae/Spring-Boot-3%EC%97%90-Swagger-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0springdoc-openapi</link>
            <guid>https://velog.io/@leemin-jae/Spring-Boot-3%EC%97%90-Swagger-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0springdoc-openapi</guid>
            <pubDate>Tue, 05 Mar 2024 05:52:17 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-java">implementation &#39;io.springfox:springfox-swagger2:3.0.0&#39;</code></pre>
<p>이전에 Swagger를 적용하여 api를 문서화를 진행한 적이 있어서 이번에도 똑같이 Swagger를 통한 문서화를 진행하려고 했다.</p>
<p>spring boot 3.2.3을 사용하고 springfox3.0.0을 추가했다. 하지만 오류가 발생하여 SpringFox를 사용할 수 없었다.</p>
<p>spring boot 3버전에서는 SpringFox 가 적용되지 않는다. Java EE가 Spring boot 3에서는 JakartaEE로 변경되었지만 현재 JakartaEE를 지원하는 SpringFox는 없기 때문이다.</p>
<p>spring boot의 버전을 낮추거나,** springdoc-openapi**를 이용해야 한다.</p>
<blockquote>
<p>[Spring Fox]
  2020년 7월 3.0.0 버전을 마지막으로 업데이트 되고 있지 않음</p>
<p>  [Spring Doc]
  2023년 4월 1.7.0 버전 업데이트</p>
</blockquote>
<h3 id="dependency-설정">dependency 설정</h3>
<pre><code class="language-java">// https://mvnrepository.com/artifact/org.springdoc/springdoc-openapi-ui
//    implementation &#39;org.springdoc:springdoc-openapi-ui:1.7.0&#39;  해당 의존성은 오류 발생
    implementation &#39;org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2&#39;</code></pre>
<h3 id="openapi-bean-등록">OpenAPI Bean 등록</h3>
<pre><code class="language-java">@Configuration
public class SwaggerConfig {
    @Bean
    public OpenAPI openAPI() {
        return new OpenAPI()
                .components(new Components())
                .info(apiInfo());
    }

    private Info apiInfo() {
        return new Info()
                .title(&quot;Springdoc 테스트&quot;)
                .description(&quot;Springdoc을 사용한 Swagger UI 테스트&quot;)
                .version(&quot;1.0.0&quot;);
    }
}</code></pre>
<h3 id="applicationyml-설정">application.yml 설정</h3>
<p>springfox에서는 bean으로 설정을 등록했었지만 springdoc는 apllication에서 모든 설정을 등록 할 수 있다. </p>
<pre><code class="language-yml">springdoc:
  packagesToScan: com.nklcb.cosmos
  default-consumes-media-type: application/json;charset=UTF-8
  default-produces-media-type: application/json;charset=UTF-8
  swagger-ui:
    path: /swagger.html
    disable-swagger-default-url: true
    display-request-duration: true
    operations-sorter: alpha</code></pre>
<p>모든 설정이 완료 된 후 path에 설정해둔 경로로 이동하면 정상적으로 화면이 보인다.</p>
<p><img src="https://velog.velcdn.com/images/leemin-jae/post/4581e9e7-4e63-4072-aed6-c6c687b08de2/image.png" alt=""></p>
<h3 id="spring-security-설정">spring security 설정</h3>
<p>향후 추가</p>
<h3 id="api-스펙-작성">API 스펙 작성</h3>
<p><img src="https://velog.velcdn.com/images/leemin-jae/post/10fa5be2-03c6-45af-b0d1-62b2b28cb1e2/image.png" alt=""></p>
<pre><code class="language-java">@RestController
@RequiredArgsConstructor
@RequestMapping(&quot;/api/members&quot;)
@Validated
@Tag(name = &quot;Member&quot;, description = &quot;Member API&quot;)
public class MemberController {

    @GetMapping(&quot;/{memberId}&quot;)
    @Operation(summary = &quot;Get member profile&quot;, description = &quot;특정 멤버의 상세 정보를 조회한다.&quot;)
    @ApiResponses(value = {
        @ApiResponse(responseCode = &quot;200&quot;, description = &quot;성공&quot;,
        content = {@Content(schema = @Schema(implementation = MemberProfileRes.class))}),
        @ApiResponse(responseCode = &quot;404&quot;, description = &quot;해당 ID의 유저가 존재하지 않습니다.&quot;),
        })
    public MemberProfileRes getMemberProfile(
        @PathVariable
        @Positive(message = &quot;유저 ID는 양수입니다.&quot;)
        @Schema(description = &quot;Member ID&quot;, example = &quot;1&quot;)
        Long memberId,

        // TODO: Replace with member ID from JWT or that from any other authentication method
        @Parameter(name = &quot;loginId&quot;, description = &quot;로그인 유저 ID 값&quot;, example = &quot;3&quot;, required = true)
        @Positive(message = &quot;유저 ID는 양수입니다.&quot;) @RequestParam final Long loginId,

        @RequestBody @Valid MemberProfileUpdateReq request
    ) {
      return memberMapper.toResponse(
          memberService.findProfileByMemberId(memberId, loginId)
      );
      }

}</code></pre>
<blockquote>
<pre><code class="language-java">@Tag(name = &quot;Member&quot;, description = &quot;Member API&quot;)</code></pre>
</blockquote>
<pre><code>
**`@Tag`**를 통해 controller 단위에 tag를 지정해 준다.

Swaaget UI에서는 tag단위로 그룹핑 된다.


&gt; ```java
@Operation(summary = &quot;Get member profile&quot;, description = &quot;특정 멤버의 상세 정보를 조회한다.&quot;)</code></pre><p><strong><code>@Operation</code></strong>을 이용해 해당 API가 어떤 리소스를 나타내는지 간략한 설명을 추가할 수 있다.</p>
<blockquote>
<pre><code class="language-java">@ApiResponses(value = {
        @ApiResponse(responseCode = &quot;200&quot;, description = &quot;성공&quot;,
        content = {@Content(schema = @Schema(implementation = MemberProfileRes.class))}),
        @ApiResponse(responseCode = &quot;404&quot;, description = &quot;해당 ID의 유저가 존재하지 않습니다.&quot;),
        })</code></pre>
</blockquote>
<pre><code>
**`@ApiResponses`**를 이용해 해당 API의 Response 정보들을 나타낼 수 있다.

**`@ApiResponse`**는 단일 Response에 대한 정보를 나타낸다.

**responseCode** : HTTP status code
**description** : 이 response의 의미
**content** : response 데이터 포맷. void이거나 response field에 대한 설명을 추가하지 않을거라면 생략하면 된다.

&gt; ```java
@ApiResponses(value = {
    @ApiResponse(responseCode = &quot;200&quot;, description = &quot;성공&quot;,
        content = {
            @Content(mediaType = &quot;application/json&quot;, array = @ArraySchema(schema = @Schema(implementation = MemberRes.class)))
        })
})</code></pre><p>응답이 list 일 경우** arry **옵션 사용</p>
<hr>
<h3 id="dto-api-스펙-작성">DTO API 스펙 작성</h3>
<pre><code class="language-java">@Getter
@AllArgsConstructor
@Schema(description = &quot;Member profile update request&quot;)
public class MemberProfileUpdateReq {

  @NotBlank(message = &quot;사용자 이름을 입력해주세요.&quot;)
  @Length(max = 20, message = &quot;사용자 이름은 20글자 이하로 입력해야 합니다.&quot;)
  @Schema(description = &quot;member name&quot;, example = &quot;John Doe&quot;)
  private String name;

  @NotBlank(message = &quot;사용자 닉네임을 입력해주세요.&quot;)
  @Length(max = 20, message = &quot;사용자 닉네임은 20글자 이하로 입력해야 합니다.&quot;)
  @Schema(description = &quot;member nickname&quot;, example = &quot;johndoe&quot;)
  private String nickname;

  @NotBlank
  @Schema(description = &quot;member profile emoji&quot;, example = &quot;👨🏻‍💻&quot;)
  private String profileEmoji;
}</code></pre>
<p>@Schema를 사용해서 클래스 설명 및 변수 설명을 설정할수 있다.</p>
<p>변수가 특정 객체를 타입으로 가진다면 implementation 옵션을 사용한다.</p>
<p>@NotBlank, @Length 등 Spring validation은 자동으로 인식할 수 있다.</p>
<hr>
<p>이전 프로젝트에서 swagger를 적용할 때 api를 개발하면서 swagger 사용법을 검색하고 적용하다 보니 필요한 내용이 누락되었던 경우가 조금씩 발생했었다. 덕분에 다른 팀원들이 api관련해서 swagger가 있어도 설명해주라는 경우가 있었는데 이번에는 본격적인 프로젝트 시작 전에 사용법을 정리하고 이런 경우가 없도록 깔끔하게 정리하고 싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TDD, 테스트 주도 개발 정리]]></title>
            <link>https://velog.io/@leemin-jae/TDD-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%A3%BC%EB%8F%84-%EA%B0%9C%EB%B0%9C-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@leemin-jae/TDD-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%A3%BC%EB%8F%84-%EA%B0%9C%EB%B0%9C-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Thu, 29 Feb 2024 12:40:38 GMT</pubDate>
            <description><![CDATA[<h3 id="tdd란">TDD란?</h3>
<p><code>TDD</code>란 <code>Test Driven Development</code>의 약자로 &#39;테스트 주도 개발&#39;이라고 한다. </p>
<p>반복 테스트를 이용한 소프트웨어 방법론으로, 작은 단위의 테스트 케이스를 작성하고 이를 통과하는 코드를 추가하는 단계를 반복하여 구현한다.</p>
<p><img src="https://velog.velcdn.com/images/leemin-jae/post/9569c2ea-dbbd-472b-9bb4-cc911979a0ad/image.webp" alt=""></p>
<p><strong>RED</strong> : 실패하는 테스트 코드(Failing Test)를 작성한다.
<strong>GREEN</strong> : 테스트 코드를 성공시키기 위한 코드(Test Pass)를 작성한다.
<strong>BLUE</strong> : 중복 코드 제거, 일반화 등의 리팩토링(Refactor)을 수행한다.</p>
<p>중요한 것은 실패하는 테스트 코드를 작성할 때까지 실제 코드를 작성하지 않는 것과, 실패하는 테스트를 통과할 정도의 최소 실제 코드를 작성해야하는 것이다. 
이를 통해 실제 코드에 대해 기대되는 바를 보다 명확하게 정의 함으로써 불필요한 설계를 피할 수 있고, 정확한 요구 사항에 집중할 수 있다.</p>
<hr>
<p><img src="https://velog.velcdn.com/images/leemin-jae/post/f64f73ad-8c07-4ba7-a232-4878258aed51/image.webp" alt="">
보통 개발 방식은 ‘요구사항 분석 → 설계 → 개발 → 테스트 → 배포’의 형태의 개발 주기를 갖는다.</p>
<p>이러한 방식은 소프트웨어 개발을 느리게 하는 잠재적 위험이 존재한다.</p>
<p>그 이유는 아래와 같다.</p>
<blockquote>
</blockquote>
<ul>
<li>소비자의 요구사항이 처음부터 명확하지 않을 수 있다.</li>
<li>따라서 처음부터 완벽한 설계는 어렵다.<ul>
<li>자체 버그 검출 능력 저하 또는 소스코드의 품질이 저하될 수 있다.</li>
<li>자체 테스트 비용이 증가할 수 있다.</li>
</ul>
</li>
</ul>
<p>이러한 문제점이 발생되는 이유는 어느 프로젝트든 초기 설계가 완벽하다고 말할 수 없기 때문이다.</p>
<p>고객의 요구사항 또는 디자인의 오류 등 많은 외부 또는 내부 조건에 의해 재설계하여 점진적으로 완벽한 설계로 나아간다.</p>
<p>재설계로 인해 개발자는 코드를 삽입, 수정, 삭제하는 과정에서 <strong>불필요한 코드가 남거나 중복처리</strong> 될 가능성이 크다.</p>
<blockquote>
<p>결론적으로 이러한 코드들은 재사용이 어렵고 관리가 어려워서 유지보수를 어렵게 만든다.</p>
</blockquote>
<p>작은 부분의 기능 수정에도 모든 부분을 테스트해야 하므로 전체적인 버그를 검출하기 어려워진다. 따라서 자체 버그 검출 능력이 저하된다. 그 결과 어디서 버그가 발생할지 모르기 때문에 잘못된 코드도 고치지 않으려 하는 현상이 나타나게 된다.</p>
<hr>
<p><img src="https://velog.velcdn.com/images/leemin-jae/post/84c7dc9b-af0b-43e2-8162-c4563be5f19e/image.webp" alt=""></p>
<p>TDD 개발 방식은 테스트 코드를 작성한 뒤에 실제 코드를 작성한다는 것이다.</p>
<p>디자인(설계) 단계에서 프로그래밍 목적을 반드시 미리 정의해야만 하고, 무엇보다 테스트해야 할지 미리 정의(테스트 케이스 작성)해야만 한다.</p>
<p>테스트 코드를 작성하는 도중 발생하는 예외 사항(버그 및 수정사항)은 테스트 케이스에 추가하고 설계를 개선한다.</p>
<p>이후 테스트가 통과된 코드만을 코드 개발 단계에서 실제 코드로 작성한다.</p>
<hr>
<h3 id="개발-방식-비교">개발 방식 비교</h3>
<p>차이점은 테스트 코드를 작성한 뒤에 코드개발을 진행한다는 것이다.</p>
<p>디자인(설계) 단계에서 프로그래밍 목적을 반드시 미리 정의하고 테스트해야할 지 미리 정의를 해야한다.
이후 테스트 코드 작성 단계에서는 위와 같은 3단계 개발 주기를 통해 테스트 코드 작성과 리팩토링 단계를 거친다.</p>
<p>이러한 단계가 반복적으로 진행되면서 자연스럽게 코드의 버그는 줄어들고 소스의 코드는 간결해진다. 또한 테스트 케이스 작성으로 인해 자연스럽게 설계가 개선됨으로 재설계 시간이 절감될 수 있다.</p>
<hr>
<h3 id="tdd의-장점">TDD의 장점</h3>
<p><strong>1. 디버깅 시간을 단축 할 수 있다.</strong>
이는 유닛 테스팅을 하는 이점이기도 하다. 
예를 들면 사용자의 데이터가 잘못 나온다면 DB의 문제인지, 비즈니스 레이어의 문제인지 UI의 문제인지 실제 모든 레이러들을 전부 디버깅 해야하지만, TDD의 경우 자동화 된 유닛테스팅을 전재하므로 특정 버그를 손 쉽게 찾아낼 수 있다. </p>
<p><strong>2. 코드가 내 손을 벗어나기 전에 가장 빠르게 피드백 받을 수 있다.</strong>
개발 프로세스에서는 보통 ‘인수 테스트’를 한다. 이미 배치된 시스템을 대상으로 클라이언트가 의뢰한 소프트웨어가 사용자 관점에서 사용할 수 있는 수준인지 체크하는 과정이다.
이미 90% 이상 완성된 코드를 가지고 테스트하기 때문에, 문제를 발견해도, 정확하게 원인이 무엇인지 진단하기는 힘들다.
하지만 TDD를 사용하면 기능 단위로 테스트를 진행하기 때문에 코드가 모두 완성되어 프로그래머의 손을 떠나기 전에 피드백을 받는 것이 가능하다.
 
<strong>3. 작성한 코드가 가지는 불안정성을 개선하여 생산성을 높일 수 있다.</strong>
켄트 백은 TDD는 불안함을 지루함으로 바꾸는 마법의 돌이라고 말했다.
앞서 말한 것처럼 TDD를 사용하면, 코드가 내 손을 떠나 사용자에게 도달하기 전에 문제가 없는지 먼저 진단 받을 수 있다. 그러므로 코드가 지닌 불안정성과 불확실성을 지속적으로 해소해준다.
 
<strong>4. 재설계 시간을 단축 할 수 있다.</strong>
테스트 코드를 먼저 작성하기 때문에 개발자가 지금 무엇을 해야하는지 분명히 정의하고 개발을 시작하게 된다. 
또한 테스트 시나리오를 작성하면서 다양한 예외사항에 대해 생각해볼 수 있다. 
이는 개발 진행 중 소프트웨어의 전반적인 설계가 변경되는 일을 방지할 수 있다.
 
<strong>5. 추가 구현이 용이하다.</strong>
개발이 완료된 소프트웨어에 어떤 기능을 추가할 때 가장 우려되는 점은 해당 기능이 기존 코드에 어떤 영향을 미칠지 알지 못한다는 것이다. 
하지만 TDD의 경우 자동화된 유닛 테스팅을 전제하므로 테스트 기간을 획기적으로 단축시킬 수 있다.</p>
<h3 id="tdd의-단점">TDD의 단점</h3>
<p><strong>1. 가장 큰 단점은 바로 생산성의 저하이다. **
개발 속도가 느려진다고 생각하는 사람이 많기 때문에 TDD에 대해 반신반의 한다.왜냐하면 처음부터 2개의 코드를 짜야하고, 중간중간 테스트를 하면서 고쳐나가야 하기 때문이다.TDD 방식의 개발 시간은 일반적인 개발 방식에 비해 대략 10~30% 정도로 늘어난다. SI 프로젝트에서는 소프트웨어의 품질보다 납기일 준수가 훨씬 중요하기 때문에 TDD 방식을 잘 사용하지 않는다.
 
*<em>2. 이제까지 자신이 개발하던 방식을 많이 바꿔야 한다. *</em>
몸에 체득한 것이 많을 수록 바꾸기가 어렵다. 오히려 개발을 별로 해보지 않은 사람들에겐 적용하기가 쉽다.
 
**3. 구조에 얽매힌다.</strong>
TDD로 프로젝트를 진행하면서 어려운 예외가 생길 수 있는데 그것 때문에 고민하는 순간이 찾아오게 된다. 
원칙을 깰 수는 없고 꼼수가 있기는 한데 그 꼼수를 위해서 구조를 바꾸자니 이건 아무래도 아닌 것 같고, 테스트는 말 그대로 테스트일 뿐 실제 코드가 더 중요한 상황인데도 불구하고 테스트 원칙 때문에 쉽게 넘어가지 못하는 그런 경우다.</p>
<hr>
<p>이번에 회사에서 테스트를 진행하면서 1400개나 되는 실제 케이스를 전부 돌려서 안되는 케이스를 찾는 노가다 업무를 맡으면서 테스트에 대해 많은 생각을 하게 되면서 TDD를 공부하고 다음 개발에 적극적으로 적용해보자는 생각으로 정리하게 되었습니다.. </p>
<p>개념 정리 뿐만 아니라 실제 TDD를 적용하는 방법도 이 정리를 하고 앞으로 테스트를 하기 위해 노가다를 뛰는 경험이 없었으면 하는 작은 바램으로 공부하겠습니다.</p>
<hr>
<h4 id="참고자료">참고자료</h4>
<p><a href="https://hanamon.kr/tdd%EB%9E%80-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%A3%BC%EB%8F%84-%EA%B0%9C%EB%B0%9C/">TDD란? 테스트 주도 개발</a>
<a href="https://velog.io/@wngud4950/TDD%EB%9E%80">TDD, 테스트 주도 개발이란?</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준] 이모티콘 / JAVA]]></title>
            <link>https://velog.io/@leemin-jae/%EB%B0%B1%EC%A4%80-%EC%9D%B4%EB%AA%A8%ED%8B%B0%EC%BD%98-JAVA</link>
            <guid>https://velog.io/@leemin-jae/%EB%B0%B1%EC%A4%80-%EC%9D%B4%EB%AA%A8%ED%8B%B0%EC%BD%98-JAVA</guid>
            <pubDate>Tue, 27 Feb 2024 07:42:53 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/leemin-jae/post/26fa2104-d764-41fd-8a8f-76e3dcdad154/image.JPG" alt=""></p>
<h3 id="풀이">풀이</h3>
<p>bfs탐색을 하면서 1,2,3번 조건을 추가하며 최종 S에 도달할 때 까지 진행한다.
check[][]를 통해 해당 값의 clip에 복사되었던 적이 있었는지 체크하면서 중복을 제거한다.</p>
<p>풀고 나니 간단한 bfs였지만 문제를 푸는 동안에 방문처리를 어떻게 해야되는지 생각이 안나서 3번 이모티콘 갯수를 복사한 값의 절반까지 진행하는 등 이상한 조건을 쓰는 등 헛수고를 오래 하다가 풀게 되었다.</p>
<h3 id="코드">코드</h3>
<pre><code class="language-java">
public class BOJ_14226_이모티콘 {
    static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    static StringTokenizer st;
    static class Node{
        int cnt;
        int num;
        int clip;
        Node(int cnt, int num , int clip){
            this.cnt = cnt;
            this.num = num;
            this.clip = clip;
        }
    }
    public static void main(String[] args) throws IOException {

        int result = Integer.parseInt(br.readLine());
        Queue&lt;Node&gt; q = new LinkedList&lt;&gt;();
        q.add(new Node(0,1,0));
        boolean checked[][] =new boolean[10001][10001];

        while (!q.isEmpty()){
            Node now = q.poll();
            if(checked[now.num][now.clip]){
                continue;
            }
            checked[now.num][now.clip] = true;

            if(now.num == result){
                System.out.println(now.cnt);
                return;
            }

            if(now.clip != now.num) {
                q.add(new Node(now.cnt+1, now.num, now.num ));
            }
            if(now.clip != 0) {
                q.add(new Node(now.cnt+1, now.num + now.clip, now.clip));
            }
            if(now.clip/2 &lt; now.num) {
                q.add(new Node(now.cnt+1, now.num -1, now.clip));
            }

        }



    }

}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[git]git pull 시 로컬 파일을 덮어쓰는 방법]]></title>
            <link>https://velog.io/@leemin-jae/gitgit-pull-%EC%8B%9C-%EB%A1%9C%EC%BB%AC-%ED%8C%8C%EC%9D%BC%EC%9D%84-%EB%8D%AE%EC%96%B4%EC%93%B0%EB%8A%94-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@leemin-jae/gitgit-pull-%EC%8B%9C-%EB%A1%9C%EC%BB%AC-%ED%8C%8C%EC%9D%BC%EC%9D%84-%EB%8D%AE%EC%96%B4%EC%93%B0%EB%8A%94-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Tue, 27 Feb 2024 07:23:50 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/leemin-jae/post/014bdd08-aea3-4b45-a186-bff6705e7e3c/image.jpg" alt=""></p>
<p>가끔 로컬과 저장소의 파일이 다를 때 동기화를 위해서 <code>git pull</code> 명령어를 사용해도 
<code>git pull Already up to date.</code> 라는 메세지만 뜬 후 코드가 가져 올 수 없을 때가 있다.</p>
<hr>
<ul>
<li>해결 방법</li>
</ul>
<pre><code class="language-bash">$git fetch --all

$git reset --hard origin/master

$git pull</code></pre>
<p>위 명령어를 통해 이전 코드와 상관 없이 최신 커밋을 덮어 씌울 수 있습니다.</p>
<hr>
<h3 id="git-fetch">git fetch</h3>
<p><strong>원격 저장소(github)에 있는 정보들을 가져오는 명령어</strong></p>
<p>어떤 변화가 있는지 확인할 수 있습니다. 정보를 저장하는 것은 아니고, 정보를 확인하고 필요하면 병합(merge)할 수 있습니다. fetch를 사용해서 최신 커밋 정보를 가져오면  FETCH_HEAD라는 브랜치로 가져옵니다. 따라서, 커밋 정보를 보기 위해서는 해당 브랜치로 이동(checkout)해야합니다.</p>
<p>이렇게 최신 커밋 정보를 가져와서 정보를 저장하려면(합치려면) $git pull을 사용해도 되고, $git merge를 사용해도 됩니다. </p>
<h3 id="git-reset">git reset</h3>
<p><strong>HEAD의 포인터를 특정 위치로 옮기는 명령어</strong></p>
<p><code>--hard</code> 옵션을 추가해서 이전 커밋으로 돌아가면, 그 커밋 이후에 내용들은 삭제됩니다.</p>
<p><code>--mixed</code> 옵션을 추가해서 커밋을 이동하면 변경 이력이 모두 삭제되지만 스테이지에 코드가 남아있습니다. 이 코드를 add 후 커밋하면 됩니다.</p>
<p><code>--soft</code> 옵션을 추가하면 mixed 옵션과 같지만 이미 스테이징 되어있습니다. 이 말은, add 없이 바로 커밋하면 된다는 뜻입니다.</p>
<p>이 명령어는 commit history를 덮어 씌우는 commit history로 바꾸기 때문에 기록이 없어집니다. 이 경우 다른 사람과 협업하는 상황에서 문제를 일으킬 수 있는 여지가 있습니다. 지금 제 상황과 같이 코드에 대한 확신이 있는 경우나 혼자 사용하는 브랜치에만 사용하는 것이 좋습니다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[프로그래머스] 외벽 점검 / Java]]></title>
            <link>https://velog.io/@leemin-jae/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%99%B8%EB%B2%BD-%EC%A0%90%EA%B2%80-Java</link>
            <guid>https://velog.io/@leemin-jae/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%99%B8%EB%B2%BD-%EC%A0%90%EA%B2%80-Java</guid>
            <pubDate>Fri, 16 Feb 2024 06:09:20 GMT</pubDate>
            <description><![CDATA[<h3 id="문제-설명">문제 설명</h3>
<p>레스토랑을 운영하고 있는 &quot;스카피&quot;는 레스토랑 내부가 너무 낡아 친구들과 함께 직접 리모델링 하기로 했습니다. 레스토랑이 있는 곳은 스노우타운으로 매우 추운 지역이어서 내부 공사를 하는 도중에 주기적으로 외벽의 상태를 점검해야 할 필요가 있습니다.</p>
<p>레스토랑의 구조는 완전히 동그란 모양이고 외벽의 총 둘레는 n미터이며, 외벽의 몇몇 지점은 추위가 심할 경우 손상될 수도 있는 취약한 지점들이 있습니다. 따라서 내부 공사 도중에도 외벽의 취약 지점들이 손상되지 않았는 지, 주기적으로 친구들을 보내서 점검을 하기로 했습니다. 다만, 빠른 공사 진행을 위해 점검 시간을 1시간으로 제한했습니다. 친구들이 1시간 동안 이동할 수 있는 거리는 제각각이기 때문에, 최소한의 친구들을 투입해 취약 지점을 점검하고 나머지 친구들은 내부 공사를 돕도록 하려고 합니다. 편의 상 레스토랑의 정북 방향 지점을 0으로 나타내며, 취약 지점의 위치는 정북 방향 지점으로부터 시계 방향으로 떨어진 거리로 나타냅니다. 또, 친구들은 출발 지점부터 시계, 혹은 반시계 방향으로 외벽을 따라서만 이동합니다.</p>
<p>외벽의 길이 n, 취약 지점의 위치가 담긴 배열 weak, 각 친구가 1시간 동안 이동할 수 있는 거리가 담긴 배열 dist가 매개변수로 주어질 때, 취약 지점을 점검하기 위해 보내야 하는 친구 수의 최소값을 return 하도록 solution 함수를 완성해주세요.</p>
<h3 id="제한사항">제한사항</h3>
<ul>
<li>n은 1 이상 200 이하인 자연수입니다.    <ul>
<li>weak의 길이는 1 이상 15 이하입니다.</li>
<li>서로 다른 두 취약점의 위치가 같은 경우는 주어지지 않습니다.</li>
<li>취약 지점의 위치는 오름차순으로 정렬되어 주어집니다.</li>
<li>weak의 원소는 0 이상 n - 1 이하인 정수입니다.</li>
<li>dist의 길이는 1 이상 8 이하입니다.</li>
<li>dist의 원소는 1 이상 100 이하인 자연수입니다.</li>
<li>친구들을 모두 투입해도 취약 지점을 전부 점검할 수 없는 경우에는 -1을 return 해주세요.</li>
</ul>
</li>
</ul>
<h3 id="풀이">풀이</h3>
<p> 이 문제의 핵심 키워드는 완전 탐색과 순열이다. </p>
<p> 완전탐색은, n, weak, dist 모두 값의 갯수가 적기 때문에 완전 탐색을 통해 정답을 얻게 되기 때문이다.</p>
<p>순열은, weak과 dist 모두 모든 경우를 만들어줘야 하기 때문에 순열로 모든 케이스를 따져 주어야한다.</p>
<p>문제 풀이 방식은 다음과 같다.</p>
<ol>
<li><p>모든 weak 케이스 만들기: </p>
<p>예를 들어 [1,5,6,10]의 weak이 존재한다고 할 때, weak에서 나올 수 있는 모든 케이스는 [1,5,6,10], [5,6,10,13], [6,10,13,17], [10,13,17,18] 이 된다. 원형 큐의 방식처럼 만들어주면 된다.</p>
<p>애초에 weak의 길이가 최대 15이므로, 이런식으로 모든 테스트 케이스를 만들어줘도 최대 15개까지밖에 되지않는다. 이런 식으로 모든 테스트 케이스를 만들어주면, 한방향으로만 탐색을 계속 진행해도 모든 경우를 다 탐색할 수 있게 된다.</p>
</li>
</ol>
<ol start="2">
<li><p>모든 dist 케이스 만들기:</p>
<p>완전 탐색이므로, 순열을 이용하여 모든 dist 케이스를 만들어준다. 예를 들어 [3,5,7]의 dist가 존재한다고 할 때, [3,5,7], [3,7,5], [5,3,7], [5,7,3], [7,5,3], [7,3,5] 의 dist 케이스가 나타난다.</p>
</li>
<li><p>모든 weak 케이스에 대해 모든 dist 케이스 검사:</p>
<p>정답을 얻어내기 위해, 모든 weak 케이스에 대해 모든 dist 케이스를 검사한다. 반복문을 통해 weak 케이스의 시작 지점부터 시작해서 dist 케이스의 값을 하나씩 가져오면서 검사가 되는 weak 지점을 모두 패스한다. </p>
<p>예를 들어 [1,3,4,9,10]의 weak과 [3,5,7]의 dist를 이용하여 탐색한다고 가정하면, 1번 지점에서 3짜리 dist가 사용된다. 1+3 = 4이므로, 3,4번 지점은 한번에 체크가 된다. 그럼 다음 출발지는 9번 지점이 되고, 여기서 5짜리 dist가 사용된다. 9+5 = 14이므로, 10번 지점까지 한번에 체크가 된다. 이렇게 되면 2개의 dist값만 사용하고 모두 탐색이 진행된 것이다.</p>
</li>
</ol>
<h3 id="코드">코드</h3>
<pre><code class="language-java">class Solution {
     int [] weak , dist;
    int[][] weak_cases;
    int n, answer;

    public int solution(int n, int[] weak, int[] dist) {
        weak_cases = new int[weak.length][weak.length];
         this.answer = dist.length+1;
        this.weak = weak;
        this.dist = dist;
        this.n = n;

        makeWeak();
        makeDist(new boolean[dist.length], new int[dist.length], 0);
        if(answer == dist.length+1)
            return -1;
        else
            return answer;

    }
    public void makeWeak(){
        int [] weak_case = this.weak.clone();
        weak_cases[0] = weak_case.clone();

        for (int i = 1; i &lt; weak.length; i++) {
            int temp = weak_case[0];
            for (int j = 1; j &lt; weak.length; j++) {
                weak_case[j-1] = weak_case[j];
            }
            weak_case[weak.length -1] = temp+n;
            weak_cases[i] = weak_case.clone();
        }
    }
    public void makeDist(boolean[] dist_visit, int[] dist_case, int idx){
        if(idx == dist.length){
            for(int[] weak_case: weak_cases)
                check(dist_case, weak_case);
        }
        for(int i = 0; i &lt; dist.length; i++){
            if(!dist_visit[i]){
                dist_visit[i] = true;
                dist_case[idx] = dist[i];
                makeDist(dist_visit, dist_case, idx+1);
                dist_case[idx] = 0;
                dist_visit[i] = false;
            }
        }
    }

    private void check(int[] dist_case, int[] weak_case) {
        int cur = 0 , next;
        int dist_idx = 0;
        while (cur &lt; weak_case.length &amp;&amp; dist_idx &lt; dist_case.length){
            next = cur+1;
            while (next &lt; weak_case.length &amp;&amp; weak_case[cur] + dist_case[dist_idx] &gt;= weak_case[next]){
                next++;
            }
            cur = next;
            dist_idx++;
        }

        if(cur == weak_case.length &amp;&amp; dist_idx &lt; answer){
            answer = dist_idx;
        }

    }
}</code></pre>
]]></description>
        </item>
    </channel>
</rss>