<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>yujin_jeong.log</title>
        <link>https://velog.io/</link>
        <description>일단 하고 보자 (펠리컨적 마인드 ㅠㅠ)</description>
        <lastBuildDate>Mon, 19 Jan 2026 15:49:25 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>yujin_jeong.log</title>
            <url>https://velog.velcdn.com/images/yujin_jeong/profile/1c5308cb-e6d3-4cd3-8e6f-019ae48aa929/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. yujin_jeong.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/yujin_jeong" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[4편 보강 예제] Slurm으로 “진짜 LLM 추론” 한 번 돌려보기 (phi-2)]]></title>
            <link>https://velog.io/@yujin_jeong/4%ED%8E%B8-%EB%B3%B4%EA%B0%95-%EC%98%88%EC%A0%9C-Slurm%EC%9C%BC%EB%A1%9C-%EC%A7%84%EC%A7%9C-LLM-%EC%B6%94%EB%A1%A0-%ED%95%9C-%EB%B2%88-%EB%8F%8C%EB%A0%A4%EB%B3%B4%EA%B8%B0-phi-2</link>
            <guid>https://velog.io/@yujin_jeong/4%ED%8E%B8-%EB%B3%B4%EA%B0%95-%EC%98%88%EC%A0%9C-Slurm%EC%9C%BC%EB%A1%9C-%EC%A7%84%EC%A7%9C-LLM-%EC%B6%94%EB%A1%A0-%ED%95%9C-%EB%B2%88-%EB%8F%8C%EB%A0%A4%EB%B3%B4%EA%B8%B0-phi-2</guid>
            <pubDate>Mon, 19 Jan 2026 15:49:25 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/yujin_jeong/post/c8b7727d-5bd8-477e-954f-8240adfaf9c7/image.png" alt=""></p>
<p>아닙니다. 
<img src="https://velog.velcdn.com/images/yujin_jeong/post/c1c23258-85e4-4d89-9c18-3ea65f64df99/image.png" alt=""></p>
<p>Slurm 개념을 이해했으면, 이제는 진짜로 <strong>GPU 잡아서 뭔가 돌아가는 경험</strong>을 한 번 해야 한다. 근데 여기서 gpt2 같은 걸 돌리면 솔직히 감이 안 옴...! “GPU를 쓴 게 맞나?” 싶은 느낌이 남는다.</p>
<p>그래서 모델은 <strong>너무 크진 않지만, LLM 돌린 느낌은 확실한 것</strong>으로 예제를 준비했다! <code>microsoft/phi-2 (2.7B)</code> 정도면 딱 적당하다.</p>
<blockquote>
<p>GPU 빈 노드 확인 → debug로 들어가서 테스트 → batch로 제출 → 로그로 결과 확인</p>
</blockquote>
<hr>
<h3 id="마스터-작업-디렉토리-준비">(마스터) 작업 디렉토리 준비</h3>
<p>실습은 무조건 <strong>폴더부터 잡고 시작</strong>하는 게 좋다.
로그랑 결과물이 남아야 디버깅이 된다.</p>
<pre><code class="language-bash">mkdir -p /data/username/repos/slurm_practice
cd /data/username/repos/slurm_practice
mkdir -p logs outputs</code></pre>
<hr>
<h3 id="gpu-비어있는-노드-확인-→-interactive-접속">GPU 비어있는 노드 확인 → interactive 접속</h3>
<h4 id="1-1-비어있는-gpu-노드-확인">1-1) 비어있는 GPU 노드 확인</h4>
<p>일단 “GPU가 지금 어디 비었는지”부터 본다.</p>
<pre><code class="language-bash">slurm-gres-viz -i</code></pre>
<p><img src="https://velog.velcdn.com/images/yujin_jeong/post/676684b7-493e-4606-99b7-220d20d7ac58/image.png" alt=""></p>
<p>여기서 <strong>idle/free</strong> 인 노드 하나 고르면 된다.
예: <code>aurora-g5</code></p>
<hr>
<h4 id="1-2-그-노드로-접속-debug_ugrad">1-2) 그 노드로 접속 (debug_ugrad)</h4>
<p>그리고 debug 파티션에서 <strong>GPU 1장짜리 쉘</strong>을 하나 받는다.</p>
<pre><code class="language-bash">srun --gres=gpu:1 --cpus-per-gpu=1 --mem-per-gpu=16G \
  -p debug_ugrad --account=ugrad \
  -w aurora-g5 \
  --pty $SHELL</code></pre>
<p>접속 확인은 이걸로 끝난다.</p>
<pre><code class="language-bash">hostname
nvidia-smi</code></pre>
<p><img src="https://velog.velcdn.com/images/yujin_jeong/post/a8d8b21c-229e-4b9d-8d9f-6a52f8465718/image.png" alt=""></p>
<p>여기서 <code>nvidia-smi</code>가 뜨면, 이제 “GPU 노드 안에 들어왔다”는 뜻이다.
그럼 이제 진짜로 코드 돌리면 된다.</p>
<hr>
<h3 id="gpu-노드-llm-추론-코드-작성-phi-2">(GPU 노드) LLM 추론 코드 작성 (phi-2)</h3>
<h4 id="2-1-llm_phi2_inferpy-생성">2-1) <code>llm_phi2_infer.py</code> 생성</h4>
<p>이제 <strong>LLM 추론 코드</strong>를 만든다.
이건 학습이 아니라 “모델 로드 + 생성 한 번”만 하는 코드다.</p>
<p><img src="https://velog.velcdn.com/images/yujin_jeong/post/16d6adda-04ce-43c9-ae24-8575cb24b26e/image.png" alt=""></p>
<pre><code class="language-bash">cd /data/username/repos/slurm_practice

cat &gt; llm_phi2_infer.py &lt;&lt; &#39;EOF&#39;
import time
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM

MODEL_ID = &quot;microsoft/phi-2&quot;

print(&quot;[INFO] cuda available:&quot;, torch.cuda.is_available())
if torch.cuda.is_available():
    print(&quot;[INFO] device:&quot;, torch.cuda.get_device_name(0))
device = &quot;cuda&quot; if torch.cuda.is_available() else &quot;cpu&quot;

t0 = time.time()
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
model = AutoModelForCausalLM.from_pretrained(
    MODEL_ID,
    torch_dtype=torch.float16 if device == &quot;cuda&quot; else torch.float32,
    device_map=&quot;auto&quot; if device == &quot;cuda&quot; else None
)
t1 = time.time()
print(f&quot;[INFO] model load time: {t1 - t0:.2f}s&quot;)

prompt = &quot;&quot;&quot;You are a helpful assistant.
Explain Slurm in 5 bullet points for a beginner who uses an HPC GPU cluster.
&quot;&quot;&quot;

inputs = tokenizer(prompt, return_tensors=&quot;pt&quot;)
if device == &quot;cuda&quot;:
    inputs = {k: v.to(&quot;cuda&quot;) for k, v in inputs.items()}

gen_t0 = time.time()
with torch.no_grad():
    out = model.generate(
        **inputs,
        max_new_tokens=180,
        do_sample=True,
        temperature=0.7,
        top_p=0.9,
        repetition_penalty=1.05
    )
gen_t1 = time.time()

print(f&quot;[INFO] generation time: {gen_t1 - gen_t0:.2f}s&quot;)
print(&quot;----- OUTPUT -----&quot;)
print(tokenizer.decode(out[0], skip_special_tokens=True))
EOF</code></pre>
<p><img src="https://velog.velcdn.com/images/yujin_jeong/post/db96d7ec-3831-4c8d-b459-1ed30fda3518/image.png" alt=""></p>
<blockquote>
<h3 id="코드-설명">코드 설명</h3>
<p>이 코드는 <strong>Hugging Face Transformers</strong>와 <strong>PyTorch</strong>를 이용해서 마이크로소프트의 언어모델 <strong>phi-2(microsoft/phi-2)</strong> 를 불러온 뒤, 프롬프트(prompt)를 입력으로 주고 <strong>텍스트를 생성(inference)</strong> 하는 예제입니다. </p>
</blockquote>
<p>실행하면 먼저 GPU(CUDA)를 사용할 수 있는지 확인하고, 가능하면 GPU 이름까지 출력한 다음(<code>torch.cuda.is_available()</code>, <code>torch.cuda.get_device_name(0)</code>), 사용할 장치를 <code>cuda</code> 또는 <code>cpu</code>로 정합니다. 그 다음 모델 로딩 시간을 재기 위해 <code>time.time()</code>으로 시작 시간을 찍고, <strong><code>AutoTokenizer.from_pretrained()</code>로 토크나이저를 다운로드/로드</strong>하고, <code>**AutoModelForCausalLM.from_pretrained()</code>로 실제 텍스트 생성 모델을 불러옵니다<strong>. 이때 GPU를 쓰는 경우 속도와 메모리 효율을 위해 <code>float16</code>을 사용하고(<code>torch_dtype=torch.float16</code>), <code>device_map=&quot;auto&quot;</code>로 모델을 자동으로 GPU에 올리도록 설정합니다. 이후 <code>prompt</code> 변수에 “Slurm을 초보자에게 5개 bullet로 설명하라”는 지시문이 들어 있고, 토크나이저로 이를 숫자 토큰 텐서로 변환합니다(<code>tokenizer(prompt, return_tensors=&quot;pt&quot;)</code>). 만약 GPU를 쓰는 환경이면 입력 텐서도 GPU로 옮겨야 모델과 같은 장치에서 계산할 수 있으므로 <code>inputs = {k: v.to(&quot;cuda&quot;) ...}</code>로 이동합니다. 생성 단계에서는 <code>torch.no_grad()</code>로 **추론 중에는 그래디언트 계산을 끄고</strong> 메모리를 절약하며, <code>model.generate()</code>를 통해 새 토큰을 최대 180개까지 생성합니다. 또한 <code>do_sample=True</code>, <code>temperature=0.7</code>, <code>top_p=0.9</code>는 <strong>확률적으로 샘플링하는 생성 방식</strong>이라 답변이 매번 조금씩 달라질 수 있고, <code>repetition_penalty=1.05</code>는 같은 표현을 반복하는 현상을 줄이기 위한 옵션입니다. 마지막으로 생성 시간도 출력하고, <code>tokenizer.decode(..., skip_special_tokens=True)</code>로 토큰을 사람이 읽을 수 있는 텍스트로 복원해 최종 결과를 출력합니다.</p>
<hr>
<h4 id="2-2-interactive에서-바로-실행-디버깅">2-2) interactive에서 바로 실행 (디버깅)</h4>
<p>여기서 바로 한 번 실행해본다.</p>
<pre><code class="language-bash">python3 llm_phi2_infer.py</code></pre>
<p>출력이 잘 나오면 이 단계는 끝이다.
중요한 건 “코드가 잘 돌아간다”가 아니라 <strong>GPU에서 정상적으로 모델이 로드되고 생성까지 한다</strong>는 걸 확인하는 거다.</p>
<p><img src="https://velog.velcdn.com/images/yujin_jeong/post/463cca86-e094-4445-8ba3-42eed6162756/image.png" alt="">
여기서 터지면 대부분 원인이 뻔하다.</p>
<ul>
<li>transformers/torch 버전 문제</li>
<li>CUDA 인식 문제</li>
<li>모델 다운로드/캐시 문제</li>
</ul>
<hr>
<h3 id="마스터-배치-제출-스크립트-작성-batch_ugrad">(마스터) 배치 제출 스크립트 작성 (batch_ugrad)</h3>
<p>interactive 쉘에서 테스트가 끝났으면, 이제 진짜 Slurm답게 <strong>batch로 제출</strong>한다.
interactive는 “확인용”이고, batch는 “운영용”이다.</p>
<p>먼저 interactive에서 빠져나온다.</p>
<pre><code class="language-bash">exit</code></pre>
<hr>
<h4 id="3-1-phi2_infersbatch-생성">3-1) <code>phi2_infer.sbatch</code> 생성</h4>
<p>이제 배치 스크립트를 만든다.
로그 경로를 아예 박아두면 나중에 찾기 편하다.</p>
<p><img src="https://velog.velcdn.com/images/yujin_jeong/post/61471380-6ab4-4f90-8210-b894c837724f/image.png" alt=""></p>
<pre><code class="language-bash">cd /data/username/repos/slurm_practice

cat &gt; phi2_infer.sbatch &lt;&lt; &#39;EOF&#39;
#!/usr/bin/bash
#SBATCH -J phi2_infer
#SBATCH -p batch_ugrad
#SBATCH --account=ugrad
#SBATCH --gres=gpu:1
#SBATCH --cpus-per-gpu=4
#SBATCH --mem-per-gpu=20G
#SBATCH -t 00:10:00
#SBATCH -o /data/username/repos/slurm_practice/logs/slurm-%A_phi2.out
#SBATCH -e /data/username/repos/slurm_practice/logs/slurm-%A_phi2.err

cd /data/username/repos/slurm_practice

echo &quot;[INFO] host=$(hostname)&quot;
echo &quot;[INFO] pwd=$(pwd)&quot;
which python
python3 llm_phi2_infer.py

exit 0
EOF</code></pre>
<blockquote>
<p><code>phi2_infer.sbatch</code>는 Slurm에서 <strong>배치 잡(job)</strong> 으로 실행할 설정과 실행 명령을 담은 스크립트입니다. </p>
</blockquote>
<p>맨 위 <code>#!/usr/bin/bash</code>는 이 파일을 실행할 때 <strong>bash 셸로 해석하라</strong>는 의미입니다. 그 아래 <code>#SBATCH</code>로 시작하는 줄들은 Slurm에 전달되는 <strong>자원 요청/잡 설정 옵션</strong>들인데, <code>-J phi2_infer</code>는 잡 이름을 <code>phi2_infer</code>로 지정하고, <code>-p batch_ugrad</code>는 제출할 파티션(큐)을 <code>batch_ugrad</code>로 선택합니다. <code>--account=ugrad</code>는 어떤 계정(과금/사용 권한 그룹)으로 자원을 쓸지 지정하는 옵션이고, <code>--gres=gpu:1</code>은 GPU 1장을 요청합니다. <code>--cpus-per-gpu=4</code>는 GPU 1장당 CPU 코어 4개를 같이 달라고 요청하는 설정이며, <code>--mem-per-gpu=20G</code>는 GPU 1장당 메모리를 20GB 할당해 달라는 의미입니다. <code>-t 00:10:00</code>은 최대 실행 시간을 10분으로 제한하고, <code>-o ...slurm-%A_phi2.out</code>는 표준 출력(stdout)을 저장할 로그 파일 경로를 지정합니다. 여기서 <code>%A</code>는 Slurm이 자동으로 넣어주는 <strong>잡 ID</strong>라서 실행할 때마다 다른 파일명으로 기록됩니다. <code>-e ...slurm-%A_phi2.err</code>는 표준 에러(stderr)를 저장할 파일 경로입니다.</p>
<p>그 다음 본문에서는 <code>cd /data/username/repos/slurm_practice</code>로 작업 디렉토리를 해당 프로젝트 폴더로 이동한 뒤, <code>echo</code>로 현재 실행 중인 노드의 호스트명(<code>hostname</code>)과 현재 위치(<code>pwd</code>)를 출력해서 로그에서 <strong>어디서 실행됐는지 추적</strong>할 수 있게 합니다. <code>which python</code>은 현재 잡 환경에서 사용되는 <code>python</code> 실행 파일의 경로를 보여주고, <code>python3 llm_phi2_infer.py</code>로 실제로 phi-2 추론 파이썬 스크립트를 실행합니다. 마지막 <code>exit 0</code>은 스크립트가 정상 종료(에러 없이 종료)했음을 의미하는 종료 코드입니다.</p>
<hr>
<h4 id="3-2-제출">3-2) 제출</h4>
<p>이제 제출.</p>
<pre><code class="language-bash">sbatch phi2_infer.sbatch
</code></pre>
<p>여기서부터는 Slurm이 알아서 한다.
사용자는 기다리면서 상태만 보면 된다.</p>
<hr>
<h3 id="4-결과-확인">4) 결과 확인</h3>
<h4 id="4-1-큐-확인">4-1) 큐 확인</h4>
<pre><code class="language-bash">squeue -u username</code></pre>
<p>RUNNING이면 그냥 기다리고, 끝나면 사라진다.</p>
<hr>
<h4 id="4-2-로그-확인">4-2) 로그 확인</h4>
<p><img src="https://velog.velcdn.com/images/yujin_jeong/post/a07ab14b-e111-4508-a776-d98e7e833541/image.png" alt="">
job id가 예를 들어 <code>63500</code>이면:</p>
<pre><code class="language-bash">ls -la logs
cat logs/slurm-63500_phi2.out
cat logs/slurm-63500_phi2.err</code></pre>
<p>여기서 <code>.err</code>가 비어있고 <code>.out</code>에 출력이 정상적으로 찍혔으면 성공이다.
이게 HPC에서 “일단 한 번 제대로 돌았다”의 기준이다.</p>
<hr>
<h4 id="4-3-실행-통계">4-3) 실행 통계</h4>
<p>마지막으로 이걸 보면, Slurm이 남긴 기록까지 확인할 수 있다.</p>
<pre><code class="language-bash">sacct -j 63500 --format=JobID,JobName,State,ExitCode,Elapsed,MaxRSS,NodeList</code></pre>
<p>여기까지 확인하면 끝이다.
이제부터는 “실험이 성공했는지”를 감으로 보는 게 아니라, 로그랑 기록으로 확인하는 흐름이 생긴다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[서버 처음 쓰는 사람은 왜 MobaXterm을 쓰게 될까]]></title>
            <link>https://velog.io/@yujin_jeong/%EC%84%9C%EB%B2%84-%EC%B2%98%EC%9D%8C-%EC%93%B0%EB%8A%94-%EC%82%AC%EB%9E%8C%EC%9D%80-%EC%99%9C-MobaXterm%EC%9D%84-%EC%93%B0%EA%B2%8C-%EB%90%A0%EA%B9%8C</link>
            <guid>https://velog.io/@yujin_jeong/%EC%84%9C%EB%B2%84-%EC%B2%98%EC%9D%8C-%EC%93%B0%EB%8A%94-%EC%82%AC%EB%9E%8C%EC%9D%80-%EC%99%9C-MobaXterm%EC%9D%84-%EC%93%B0%EA%B2%8C-%EB%90%A0%EA%B9%8C</guid>
            <pubDate>Mon, 19 Jan 2026 09:16:10 GMT</pubDate>
            <description><![CDATA[<p>HPC 시리즈를 여기까지 따라왔다면 이제 딱 이 상태다. SSH/SFTP 개념은 알겠고, Slurm도 이해했고, 베이스라인도 한 번은 돌려봤다. 근데 막상 “서버에 접속해서 실험을 계속 굴리는 생활”을 하려니까 갑자기 너무 불편해진다. 창이 많아지고, 파일이 여기저기 흩어지고, 로그 찾다가 길 잃고, 실수도 늘어난다. 이 지점에서 사람들이 갈린다. 리눅스에 익숙한 사람은 그냥 터미널로 밀고 가고, 서버 처음 쓰는 사람은 결국 MobaXterm을 쓰게 된다. 이유는 간단하다. 편해서가 아니라, 덜 망하려고 쓰는 거다.</p>
<h2 id="왜-굳이-mobaxterm-이야기를-하나">왜 굳이 MobaXterm 이야기를 하나</h2>
<p><img src="https://velog.velcdn.com/images/yujin_jeong/post/31b4687c-1e85-4615-b3c0-c87efd33c698/image.png" alt=""></p>
<p>서버 작업이 귀찮아지는 순간은 보통 학습 자체가 아니라 “주변 작업”에서 온다. 접속은 SSH로 하고, 파일은 SFTP로 옮기고, 로그는 다시 SSH에서 tail로 보고, 결과 파일은 또 내려받고, 실험 여러 개 돌리면 경로가 꼬이고, 어느 순간부터는 “내가 지금 뭘 하고 있지?”가 된다. 이게 초보가 서버를 싫어하게 되는 포인트다. MobaXterm은 이걸 한 화면에 몰아넣는다. SSH 따로, SFTP 따로가 아니라 그냥 접속하면 왼쪽에 파일 트리가 같이 뜨고, 터미널은 그 옆에 붙어 있고, 로그는 클릭해서 열 수 있다. 이게 생각보다 크다. 서버가 갑자기 “리눅스 시스템”이 아니라 “작업 공간”처럼 느껴진다.</p>
<h2 id="mobaxterm이-해결해주는-건-별거-아닌-것-같긴하지만-당신들은-gui가-편하잖아">MobaXterm이 해결해주는 건 별거 아닌 것 같긴하지만 당신들은 GUI가 편하잖아</h2>
<p>MobaXterm이 해주는 게 엄청 고급 기능은 아니다. 근데 서버 초보가 자주 틀리는 부분을 정확히 줄여준다. SSH 접속, 왼쪽에 자동으로 뜨는 SFTP 파일 트리, 드래그 앤 드롭 파일 업로드/다운로드, 로그 파일 바로 열어서 확인, 그리고 가끔 필요한 X11 포워딩까지. 여기서 중요한 건 “편의성”이 아니라 “실수 방지”다. 예를 들어 경로를 잘못 잡아서 다른 폴더에 저장해놓고 못 찾는 경우, 로그 파일을 어디에 남겼는지 헷갈리는 경우, SFTP 클라이언트랑 SSH 클라이언트를 왔다갔다 하다가 실험 흐름이 끊기는 경우. 이런 게 줄어든다.</p>
<h2 id="기능이-너무-많아">기능이 너무 많아</h2>
<p>MobaXterm 기능은 많다. 근데 다 쓸 필요 없다. 서버 처음 쓰는 사람이 실제로 쓰는 건 거의 고정이다. 세션 하나 만들고(SSH), 포트나 계정만 맞춰서 접속하고, 왼쪽 SFTP 트리로 파일 올리고 내리고, 터미널 split 해서 한쪽은 <code>squeue</code>, 한쪽은 <code>tail -f</code> 띄우고, 로그 파일 클릭해서 열어보는 정도. 이 정도만 해도 “서버 작업이 덜 무섭다”가 된다. 오히려 여기서 기능 욕심내면 또 복잡해진다. 목적은 서버를 화려하게 쓰는 게 아니라, 실험을 덜 망치고 계속 굴리는 거다.</p>
<p>Slurm이 들어가면 작업 흐름이 배치 기반으로 바뀐다. <code>sbatch</code>로 던지고, 기다리고, 로그 보고, 결과 확인하고, 다시 던진다. 문제는 이 과정이 터미널만으로 하면 생각보다 “상황 파악”이 어렵다는 거다. 특히 초보는 로그 파일 이름, 저장 경로, 체크포인트 위치 같은 걸 자주 놓친다. MobaXterm은 이걸 클릭으로 해결한다. sbatch 돌려놓고, 로그 파일을 왼쪽에서 찾아서 바로 열고, 체크포인트가 생겼는지 확인하고, 결과 파일 내려받아서 로컬에서 확인한다. 그러면 서버에서 실험하는 느낌이 아니라, 내 작업 공간이 서버로 확장된 느낌이 된다. 이게 진짜 크다. “서버를 다루는 스트레스”가 줄어드니까 실험을 더 많이 돌리게 된다.</p>
<h2 id="그럼-vs-code-remote랑-뭐가-다르냐">그럼 VS Code Remote랑 뭐가 다르냐</h2>
<p><strong>VS Code Remote는 코드 중심</strong>이다. 서버에서 바로 코딩하고 디버깅하고, 개발자처럼 작업할 때 진짜 편하다. 반대로** MobaXterm은 서버 관리 중심**이다. 접속, 파일 이동, 로그 확인, 여러 세션 관리 같은 서버 생활을 편하게 해준다. 그래서 비전공자/초보자한테는 MobaXterm이 진입장벽이 낮다. 처음에는 MobaXterm으로 작업 흐름을 잡고, 익숙해지면 VS Code Remote로 넘어가는 사람도 많다. 반대로 VS Code Remote를 쓰더라도 MobaXterm은 로그 확인/파일 관리용으로 같이 쓰는 경우도 많다. 둘은 경쟁재라기보다는 대체제...?이다.</p>
<p>“MobaXterm은 윈도우만 되는 줄 알았는데?” 그거 맞다
“이거 윈도우 전용인 줄 알고 윈도우만 쓰고 있다”는 사람이 진짜 많다ㅠㅜ 일단 나.... MobaXterm 자체는 기본적으로 Windows용 툴이다. 아니 근데 GPT가 맥 된다고 계속 그러다가 성질부리니까 안된다네...;;</p>
<p>Mac은 기본 터미널(ssh) + 파일 전송은 Cyberduck 같은 조합을 많이 쓰고, Linux는 그냥 터미널+scp/sftp 조합으로 가는 경우가 많다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[HPC 실수 12가지]]></title>
            <link>https://velog.io/@yujin_jeong/HPC-%EC%8B%A4%EC%88%98-12%EA%B0%80%EC%A7%80</link>
            <guid>https://velog.io/@yujin_jeong/HPC-%EC%8B%A4%EC%88%98-12%EA%B0%80%EC%A7%80</guid>
            <pubDate>Mon, 19 Jan 2026 09:15:26 GMT</pubDate>
            <description><![CDATA[<p>HPC는 익숙해지면 강력한데, 모르면 진짜 불친절하다. 아래는 “모르면 꼭 한 번은 당하는 실수들”이다. 나도 다 해봤고, 주변에서도 계속 본다.
<img src="https://velog.velcdn.com/images/yujin_jeong/post/2e3d7063-1674-41fc-bfbf-254d53d5026b/image.png" alt=""></p>
<h3 id="1-conda-env를-프로젝트마다-안-나눔">1. conda env를 프로젝트마다 안 나눔</h3>
<p>하나의 env에 이것저것 설치하다가 어느 날 갑자기 import 에러가 난다. 버전 충돌이 터지면 원인 추적이 거의 불가능해진다. 프로젝트 1개 = env 1개. 이거 안 지키면 언젠가 반드시 터진다.</p>
<h3 id="2-python-버전-안-보고-env-생성">2. Python 버전 안 보고 env 생성</h3>
<p>로컬은 3.11인데 서버는 3.8이고, 패키지는 3.10 기준인 경우가 많다. 결과는 보통 이렇다. <code>pip install</code>은 되는데 실행이 안 된다. env 만들 때 Python 버전부터 고정해야 한다. 특히 torch/transformers 계열은 여기서부터 꼬이기 시작한다.</p>
<h3 id="3-partition-안-보고-job-제출">3. partition 안 보고 job 제출</h3>
<p>GPU 없는 partition에 제출해놓고 “왜 안 빨라지지?” 한다. queue는 도는데 GPU는 안 잡히고, <code>nvidia-smi</code>는 안 보인다. 먼저 항상 이거부터 본다.</p>
<pre><code class="language-bash">sinfo</code></pre>
<p>파티션이 여러 개면, 내가 들어가야 하는 곳이 따로 있는 경우가 많다. 특히 “gpu 파티션”이랑 “cpu 파티션”이 분리돼 있으면 여기서 초보가 한 번씩 넘어간다.</p>
<h3 id="4-time-limit-너무-짧게-잡음">4. time limit 너무 짧게 잡음</h3>
<p>학습 잘 돌아가다가 조용히 종료된다. 로그엔 에러도 없다. 이건 대부분 time limit 초과로 Slurm이 kill한 거다. 처음엔 넉넉하게 잡는 게 맞다. 시간 아끼려다가 실험 날리는 게 더 손해다.</p>
<h3 id="5-slurm-로그-경로-안-만들어둠">5. Slurm 로그 경로 안 만들어둠</h3>
<p>스크립트에 이렇게 써놓고</p>
<pre><code class="language-bash">#SBATCH --output=logs/%j.out</code></pre>
<p>logs 디렉토리를 안 만들어둔다. 그러면 로그가 안 남거나(혹은 Slurm이 바로 에러로 잡을 끝내거나), 남아도 찾기 힘든 위치로 간다. 디버깅 지옥이 시작된다. logs는 무조건 미리 만들어두는 게 편하다.</p>
<h3 id="6-sbatch랑-srun을-헷갈림">6. sbatch랑 srun을 헷갈림</h3>
<p>테스트도 sbatch로 하고, 본 실험도 srun으로 한다. 그리고 세션 끊기면 다 날아간다. 정리하면 이거다. 테스트/디버깅은 srun, 학습/장시간 작업은 sbatch. 이거 하나만 지켜도 사고가 확 줄어든다.</p>
<h3 id="7-nvidia-smi가-안-보이는데-당황함">7. <code>nvidia-smi</code>가 안 보이는데 당황함</h3>
<p>이건 대부분 GPU 노드가 아니라 로그인 노드에 있는 거다. GPU는 그냥 보이는 게 아니라 요청해야 보인다. 그래서 GPU 확인은 보통 “GPU를 잡고 난 뒤”에 하는 게 맞다. 로그인 노드에서 nvidia-smi 안 뜬다고 GPU가 없는 게 아니다.</p>
<h3 id="8-gpu-남아-있는데-job이-안-도는-경우">8. GPU 남아 있는데 job이 안 도는 경우</h3>
<p>분명 sinfo 보면 GPU가 남아 있는데 내 job은 계속 PENDING이다. 이건 코드 문제가 아니라 운영 정책 문제일 확률이 높다. QoS 제한, 사용자별 GPU 제한, 우선순위 밀림 같은 것들이다. 여기서 초보가 제일 많이 하는 착각은 “내 코드가 뭔가 잘못됐나?”인데, 대부분 아니다. 그냥 기다려야 하는 상황일 때가 많다.</p>
<h3 id="9-gpu-여러-개-요청했는데-코드가-1개만-씀">9. GPU 여러 개 요청했는데 코드가 1개만 씀</h3>
<p>스크립트에 이렇게 써놓고</p>
<pre><code class="language-bash">#SBATCH --gres=gpu:4</code></pre>
<p>코드는 single-GPU로만 돈다. 그러면 GPU 3개는 그냥 낭비다. 멀티 GPU는 요청만으로 되는 게 아니다. 코드에서 DDP를 쓰든, accelerate를 쓰든, 분산 설정을 해야 한다. 그리고 클러스터 입장에서는 이런 job이 제일 싫다. 자원은 먹는데 효율이 안 나온다.</p>
<h3 id="10-ipynb로-학습-돌림">10. ipynb로 학습 돌림</h3>
<p>셀 여러 번 실행하면서 메모리 파편화가 생기고, 커널이 죽고, 재실행하면 더 빨리 죽는다. HPC에서는 ipynb는 기록/실험 메모용이고, 학습은 <code>.py + Slurm</code>이 제일 안정적이다. 특히 OOM 한 번 나면 커널이 메모리를 깔끔하게 못 돌려줘서 계속 이상해지는 경우가 많다.</p>
<h3 id="11-requirementstxt-충돌-방치">11. requirements.txt 충돌 방치</h3>
<p>버전을 안 박아둔다. 어제 되던 게 오늘 안 된다. 재현이 불가능해진다. 최소한 torch/transformers/lightning 같은 핵심 패키지는 버전 고정해두는 게 맞다. HPC는 “오늘 되는 환경”이 내일도 된다는 보장이 없다. 특히 누군가가 같은 env에 패키지 하나만 올려도 갑자기 깨진다.</p>
<h3 id="12-nohup--python으로-버팀">12. nohup + python으로 버팀</h3>
<p>로컬 습관 그대로 <code>nohup python train.py &amp;</code> 이런 걸로 버티면 HPC에선 최악의 선택이 된다. 로그 뒤섞이고, job 관리가 안 되고, 자원 추적이 안 된다. 무엇보다 관리자 입장에서는 누가 뭘 돌리는지 감이 안 잡혀서 진짜 싫어한다. HPC에서는 Slurm이 정답이다. 그냥 sbatch로 돌리는 게 깔끔하다.</p>
<h3 id="13-보너스-디렉토리-구조-안-지킴">13. (보너스) 디렉토리 구조 안 지킴</h3>
<p>처음엔 대충 해도 되는데, 한 번 실험이 늘어나기 시작하면 바로 지옥이 열린다.</p>
<pre><code>project/
  ├─ env.yml
  ├─ notebooks/
  ├─ src/
  ├─ scripts/
  └─ logs/</code></pre><p>이 정도만 지켜도 나중에 자기가 고마워진다. 특히 scripts랑 logs 분리해두면 “어떤 설정으로 돌렸는지”가 남아서, 실험이 쌓여도 덜 무너진다.</p>
<h3 id="14-추가-한글-요약이-갑자기-같은-음절만-뱉는-현상">14. (추가) 한글 요약이 갑자기 같은 음절만 뱉는 현상</h3>
<p>이것도 진짜 한 번쯤 겪는다. 모델이 갑자기  특정 음절만 반복해서 뱉는다. 처음 보면 데이터가 깨졌나, 토크나이저가 망가졌나, GPU가 맛이 갔나 싶다. 근데 대부분은 학습이 불안정해지면서 생성이 붕괴된 케이스다.</p>
<p>주로 이런 상황에서 잘 나온다. learning rate가 과하게 높거나, fp16에서 overflow가 나거나, 너무 긴 입력을 억지로 넣어서 attention이 터지거나, 체크포인트가 깨졌거나, decoding 설정이 이상한 경우(예: beam/temperature 조합)다. 그리고 이게 무서운 점은, 학습이 “완전히 망한 것처럼” 보여도 로그 loss는 멀쩡하게 내려가는 척할 때가 있다는 거다. 그래서 inference 결과를 꼭 중간중간 확인해야 한다.</p>
<p>이 현상이 보이면 보통은 이 순서로 본다. 최근 체크포인트로 다시 추론해보기, decoding 파라미터(temperature/top-p/beam) 기본값으로 되돌리기, fp16이면 bf16이나 fp32로 잠깐 확인해보기, learning rate 낮추기, 입력 길이 줄이기. 대부분 여기서 정상으로 돌아온다.</p>
<p>HPC는 실수에 관대하지 않다. 하지만 위에 적은 것들만 피해도 실험 날릴 확률이 내려가고, 디버깅 시간이 줄고, 멘탈 소모가 확 줄어든다. 결국 HPC는 “잘 쓰는 사람만 빠른 시스템”이 아니라, “안 망하게 쓰는 사람이 끝까지 가는 시스템”이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[GPU OOM — 왜 나는지, 왜 줄여도 안 되는지]]></title>
            <link>https://velog.io/@yujin_jeong/GPU-OOM-%EC%99%9C-%EB%82%98%EB%8A%94%EC%A7%80-%EC%99%9C-%EC%A4%84%EC%97%AC%EB%8F%84-%EC%95%88-%EB%90%98%EB%8A%94%EC%A7%80</link>
            <guid>https://velog.io/@yujin_jeong/GPU-OOM-%EC%99%9C-%EB%82%98%EB%8A%94%EC%A7%80-%EC%99%9C-%EC%A4%84%EC%97%AC%EB%8F%84-%EC%95%88-%EB%90%98%EB%8A%94%EC%A7%80</guid>
            <pubDate>Mon, 19 Jan 2026 09:08:41 GMT</pubDate>
            <description><![CDATA[<p>HPC에서 처음 멘탈이 나가는 순간은 대부분 이 에러다.</p>
<pre><code>CUDA out of memory</code></pre><p>batch size 줄였는데 또 나고, 다시 줄였는데 또 난다. 그러다 이런 생각이 든다. “이거… GPU가 고장 난 거 아님?” 아니다. 대부분 구조 문제다. 그리고 더 짜증나는 건, OOM은 한 번 터지면 그다음부터 계속 터질 확률이 높아진다는 거다. 로그는 멀쩡한데, 갑자기 같은 자리에서 죽고, 재실행하면 더 빨리 죽고, 그러다 결국 “내 코드가 뭔가 잘못됐나?”로 빠진다.</p>
<h3 id="1-gpu-메모리는-뭐가-잡아먹고-있을까">1. GPU 메모리는 뭐가 잡아먹고 있을까</h3>
<p>GPU 메모리를 쓰는 건 생각보다 많다. 대충 네 덩어리다. 모델 weight, activation(중간 결과), gradient, optimizer state. 여기서 Transformer는 특히 activation이 미친 듯이 크다. 입력 길이가 늘어나면 “조금 더 느려지는 정도”가 아니라, 어느 순간 메모리 사용량이 벽을 뚫고 바로 터진다. 그래서 회의록처럼 길이가 들쑥날쑥한 데이터는 더 위험하다. 평균 길이로는 버티는데, 한 번 길게 걸리면 바로 OOM 난다.</p>
<p>그리고 여기서 자주 놓치는 게 하나 있다. GPU 메모리는 “모델만” 먹는 게 아니다. DataLoader가 GPU로 올리는 텐서, tokenizer 결과를 미리 쌓아두는 버퍼, evaluation 단계에서 생성 결과를 저장하는 리스트 같은 것도 은근히 누적된다. 학습이 아니라 검증(eval)에서 터지는 OOM도 꽤 흔하다.</p>
<h3 id="2-batch-줄였는데-왜-안-되지">2. “batch 줄였는데 왜 안 되지?”</h3>
<p>이게 제일 많이 나오는 질문이다. 이유는 간단하다. 모델 weight는 batch랑 무관하고, optimizer state도 batch랑 무관하다. 즉 모델 자체가 GPU에 안 올라가는 경우 batch를 1로 만들어도 OOM 난다. 특히 Adam/AdamW + 큰 모델 + 긴 입력 조합이면 거의 확정이다. 그리고 한 번 더 빡치는 경우가 있는데, “학습은 되는데 backward에서만 터지는 케이스”다. forward는 겨우 올라갔는데 gradient까지 만들려니까 뒤에서 터진다. 그래서 로그가 “몇 step 잘 돌다가” 죽는 것처럼 보인다.</p>
<h3 id="3-제일-많이-쓰는-해결책-top-5">3. 제일 많이 쓰는 해결책 Top 5</h3>
<h4 id="1-batch-size-줄이기">1) Batch size 줄이기</h4>
<p>가장 먼저 해보는 거다. 근데 한계가 빠르다. batch 1까지 갔는데도 터지면 여기서 더 줄일 게 없다. 이때부터는 “batch만의 문제”가 아니라는 뜻이다.</p>
<h4 id="2-gradient-accumulation">2) Gradient Accumulation</h4>
<p><a href="https://devocean.sk.com/blog/techBoardDetail.do?ID=167242&amp;boardType=techBlog&amp;searchData=&amp;searchDataMain=&amp;page=&amp;subIndex=&amp;searchText=&amp;techType=&amp;searchDataSub=&amp;comment=">실험으로 알아보는 LLM 파인튜닝 최적화 가이드 Part 1</a></p>
<p>배치를 쪼개서 여러 번 계산 후 한 번 업데이트하는 방식이다. 실질적으로 batch size를 키우는 효과를 주면서, 한 번에 들고 있는 메모리는 줄인다. HPC에서 제일 현실적인 타협안이다. 특히 “성능은 유지하고 싶고, VRAM은 부족한” 상황에서 거의 기본으로 들어간다.</p>
<h4 id="3-mixed-precision-fp16--bf16">3) Mixed Precision (fp16 / bf16)</h4>
<p><img src="https://velog.velcdn.com/images/yujin_jeong/post/5497730b-9ca3-4408-9e50-557cbe8b54b6/image.png" alt=""></p>
<p><a href="https://introduce-ai.tistory.com/entry/FP32-TF32-FP16-BFLOAT16-Mixed-Precision%EC%97%90-%EB%8C%80%ED%95%9C-%EC%9D%B4%ED%95%B4">https://introduce-ai.tistory.com/entry/FP32-TF32-FP16-BFLOAT16-Mixed-Precision%EC%97%90-%EB%8C%80%ED%95%9C-%EC%9D%B4%ED%95%B4
</a>
거의 필수다. 프레임워크에 따라 다르지만 대체로 이거 하나로 VRAM이 확 줄어든다. 다만 fp16은 가끔 수치 불안정으로 터질 때가 있고, bf16이 가능한 GPU면 bf16이 더 편한 경우가 많다. 물론 “켜면 무조건 해결”은 아니고, activation이 너무 크면 mixed precision을 켜도 터진다. 그래도 안 켜는 게 더 이상하다.</p>
<h4 id="4-lora--qlora">4) LoRA / QLoRA</h4>
<ul>
<li><a href="https://velog.io/@kim_taixi/LoRA-QLoRA-DoRA-QDoRA">https://velog.io/@kim_taixi/LoRA-QLoRA-DoRA-QDoRA</a></li>
<li><a href="https://ai.plainenglish.io/a-practical-guide-to-advanced-llm-fine-tuning-from-lora-to-qlora-462b01f44022">https://ai.plainenglish.io/a-practical-guide-to-advanced-llm-fine-tuning-from-lora-to-qlora-462b01f44022
</a><img src="https://velog.velcdn.com/images/yujin_jeong/post/977066f0-7ec1-4d48-9cdc-af6f8492e4ef/image.png" alt=""></li>
</ul>
<p>모델 전체를 학습하지 않는다. weight 대부분 freeze하고 일부 파라미터만 학습한다. LLM 계열에서는 사실상 표준이다. 그리고 여기서 진짜 중요한 건 “메모리가 줄어드는 이유”가 단순히 파라미터가 적어서가 아니라, optimizer state가 확 줄어드는 효과가 크다는 점이다. Adam 계열은 상태를 엄청 들고 있기 때문에, LoRA만으로도 체감이 크게 난다.</p>
<h4 id="5-입력-길이-줄이기">5) 입력 길이 줄이기</h4>
<p>전처리랑 직결된다. 불필요한 발언 제거, 안건 단위 split. 
이게 제일 싸고 확실하다!!!!!!!
 그리고 국회 회의록 요약은 사실 이게 제일 정답에 가깝다. 모델을 바꾸는 것보다, 입력에서 의미 없는 부분을 덜어내는 게 더 잘 먹힌다. 특히 “긴 입력이 가끔 등장하는 케이스”를 잘라내면 OOM이 확 줄어든다.</p>
<h3 id="4-고급이라고-불리는-것들">4. “고급”이라고 불리는 것들</h3>
<p>이건 진짜 막혔을 때 쓴다. Activation Checkpointing, CPU Offload, ZeRO(DeepSpeed), FSDP, 8-bit Optimizer. 효과는 확실한데 설정 난이도가 급상승한다. 그리고 HPC에서 이걸 쓰면 또 다른 문제가 생긴다. 설정은 됐는데 성능이 급격히 느려져서, “OOM은 해결했는데 학습 시간이 3배” 같은 상황이 나온다. 특히 CPU offload는 VRAM은 살리지만 I/O나 PCIe 병목이 생기면 체감이 확 난다. 그래서 이건 마지막 카드로 남겨두는 게 정신 건강에 좋다.</p>
<h3 id="5-모델-크기별-체감-vram">5. 모델 크기별 체감 VRAM</h3>
<p>대충 감만 잡자. 그리고 이건 항상 “모델만 올렸을 때”가 아니라, 학습까지 포함하면 얘기가 달라진다.</p>
<table>
<thead>
<tr>
<th>모델</th>
<th>1×A6000 (48GB)</th>
</tr>
</thead>
<tbody><tr>
<td>LLaMA 7B</td>
<td>학습 가능(조건 맞으면)</td>
</tr>
<tr>
<td>LLaMA 13B</td>
<td>빡빡</td>
</tr>
<tr>
<td>LLaMA 33B</td>
<td>거의 불가</td>
</tr>
<tr>
<td>Diffusion</td>
<td>상대적으로 여유</td>
</tr>
</tbody></table>
<p>여기서 포인트는 “이론상 된다”와 “실제로 된다”는 다르다는 거다. 학습은 optimizer까지 포함이라 메모리 먹는 게 다르고, 입력 길이까지 얹히면 더 달라진다. 그리고 분산 학습이나 DDP 들어가면 또 달라진다. 그래서 표만 보고 확신하면 안 된다. 그냥 감만 잡는 용도다.</p>
<h3 id="6-ipynb가-서버에서-자꾸-죽는-이유">6. ipynb가 서버에서 자꾸 죽는 이유</h3>
<p>이건 OOM이랑 자주 묶인다. 커널이 GPU 메모리를 못 돌려주고, 셀 단위 실행으로 메모리 파편화가 생기고, ssh 세션 끊기면 같이 죽는다. 특히 “한 번 OOM 났는데 커널이 살아있는 척만 하는 상태”가 제일 최악이다. 다시 돌리면 더 빨리 터지고, nvidia-smi 보면 메모리는 계속 잡혀 있고, 근데 프로세스는 애매하게 남아 있다. 그러면 결국 커널 재시작하거나 job 자체를 새로 띄워야 한다.</p>
<p>그래서 HPC에선 이게 거의 불문율이다. ipynb는 실험 메모용이고, 학습은 .py + Slurm이다. ipynb로 학습을 끝까지 끌고 가려 하면, 언젠가 한 번은 크게 데인다.</p>
<h3 id="7-그래서-실제-추천-조합">7. 그래서 실제 추천 조합</h3>
<p>학습은 <code>.py</code>, 실험/기록은 Jupytext, 실행은 <code>sbatch</code>, 테스트는 <code>srun</code>. 이 조합이 제일 덜 아프다. 특히 “테스트는 srun으로 짧게, 학습은 sbatch로 길게” 이 흐름이 잡히면 OOM뿐 아니라 서버 사용 자체가 안정된다. 그리고 실패했을 때도 복구가 쉽다. 로그 파일 하나만 보면 되니까.</p>
<h3 id="8-정리">8. 정리</h3>
<p>OOM은 GPU가 약해서가 아니라 설계가 안 맞아서 난다. 모델 크기, 입력 길이, optimizer, 학습 방식. 이 네 개가 맞아야 한다. batch 줄였는데도 계속 터진다면, 그건 “batch가 문제”가 아니라 “나머지 셋 중 하나가 너무 무거운 상태”라는 신호다. 그리고 회의록 요약 같은 긴 입력 문제에서는 결국 입력 구조를 손보는 게 제일 빠르고 확실하게 먹힌다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[베이스라인 이후, 뭘 바꿔야 할까]]></title>
            <link>https://velog.io/@yujin_jeong/%EB%B2%A0%EC%9D%B4%EC%8A%A4%EB%9D%BC%EC%9D%B8-%EC%9D%B4%ED%9B%84-%EB%AD%98-%EB%B0%94%EA%BF%94%EC%95%BC-%ED%95%A0%EA%B9%8C</link>
            <guid>https://velog.io/@yujin_jeong/%EB%B2%A0%EC%9D%B4%EC%8A%A4%EB%9D%BC%EC%9D%B8-%EC%9D%B4%ED%9B%84-%EB%AD%98-%EB%B0%94%EA%BF%94%EC%95%BC-%ED%95%A0%EA%B9%8C</guid>
            <pubDate>Mon, 19 Jan 2026 09:07:44 GMT</pubDate>
            <description><![CDATA[<p>베이스라인을 한 번이라도 끝까지 돌려봤다면 아마 이런 생각이 들 거다. 점수가 생각보다 안 나온다, 문장이 좀 이상하다, 중요한 발언을 놓친다, 길면 아예 잘린다. 이 시점에서 중요한 건 이 모델이 왜 못하는지를 감으로 때려맞추는 게 아니라, 어디를 바꿔야 하는지 구조적으로 보는 거다. 그리고 솔직히 말하면, 대부분은 모델이 못해서가 아니라 우리가 넣는 입력이 이상해서 못한다.
<a href="https://velog.io/@yujin_jeong/%EA%B7%B8%EB%9F%BC-%EB%B2%A0%EC%9D%B4%EC%8A%A4%EB%9D%BC%EC%9D%B8-%EB%AA%A8%EB%8D%B8%EB%B6%80%ED%84%B0-%EB%8F%8C%EB%A0%A4%EB%B3%B4%EC%9E%90">그럼, 베이스라인 모델부터 돌려보자</a>에서 각 시나리오를 돌려봤다. 여기서는 대조군끼리 묶어서 비교분석한다. 같은 조건에서 뭐가 다르고, 왜 그런 차이가 나는지 파고들어본다.</p>
<hr>
<h2 id="왜-국회-회의록-요약이-어려운가">왜 국회 회의록 요약이 어려운가</h2>
<p>이 데이터는 일반 뉴스 요약이랑 성질이 다르다. 문서가 길고(수천~수만 토큰), 발언자가 계속 바뀌고, 안건이 섞여 있고, 의례적 발언이 많고, 정작 중요한 결정 문장은 소수다. 그러니까 KoBART 하나 fine-tuning했다고 갑자기 잘 될 리가 없다. 오히려 억지로 요약하려다가 애매한 문장만 뱉는 게 정상이다.</p>
<p>그리고 회의록은 흐름이 있다. 누가 뭐라고 말했는지, 어떤 논점이 오갔는지, 결론이 어디서 났는지가 중요하다. 근데 모델 입장에서는 그냥 긴 텍스트 덩어리로 들어가면, 중요한 것과 중요하지 않은 걸 구분하기가 너무 어렵다.</p>
<p>베이스라인 결과를 보고 제일 먼저 할 질문은 이거다. 이게 모델 한계인가, 아니면 데이터/구조 문제인가? 대부분의 경우 전처리 + 입력 구조 문제가 먼저다. 특히 회의록은 모델이 똑똑하면 해결되는 문제가 아니라, 모델이 이해할 수 있게 잘라주면 해결되는 문제에 가깝다.</p>
<p>여기서 많이 하는 실수가 있다. 점수가 낮으면 바로 모델을 바꾼다. 근데 입력이 그대로면 모델만 바꿔도 한계가 비슷하게 온다. 긴 글을 통째로 먹이는 순간, 좋은 모델도 그냥 얌전히 잘린다.</p>
<hr>
<h3 id="1-전처리-방식-비교-s2">1. 전처리 방식 비교 (S2)</h3>
<p>같은 KoBART 모델, 같은 100개 샘플. 전처리만 다르게 해서 돌렸다.</p>
<table>
<thead>
<tr>
<th>전처리</th>
<th>ROUGE-1</th>
<th>ROUGE-L</th>
<th>BERT-Score F1</th>
<th>compression_ratio</th>
<th>length_ratio</th>
</tr>
</thead>
<tbody><tr>
<td>v0 (원본)</td>
<td>0.1154</td>
<td>0.1090</td>
<td>0.6363</td>
<td>0.87%</td>
<td>2.04</td>
</tr>
<tr>
<td>v1 (발언결합)</td>
<td>0.1151</td>
<td>0.1111</td>
<td>0.6371</td>
<td>0.90%</td>
<td>2.11</td>
</tr>
<tr>
<td>v2 (의례제거)</td>
<td>0.0879</td>
<td>0.0834</td>
<td>0.6535</td>
<td>14.87%</td>
<td>1.45</td>
</tr>
</tbody></table>
<h4 id="분석">분석</h4>
<p>v0과 v1은 거의 차이가 없다. 같은 화자 발언을 합쳐봤자 토큰 수가 크게 줄지 않기 때문이다. compression_ratio가 0.87%에서 0.90%로 거의 비슷하다.</p>
<p>v2는 얘기가 다르다. 의례 발언을 제거하니까 compression_ratio가 14.87%까지 올라갔다. 입력이 15% 가까이 줄었다는 뜻이다. 그런데 ROUGE는 오히려 떨어졌다. 왜일까?</p>
<p>length_ratio를 보면 답이 나온다. v0은 2.04, v2는 1.45다. v2는 출력 길이가 짧아졌다. 입력이 줄어드니까 모델이 생성하는 요약도 짧아진 거다. ROUGE는 길이에 민감해서 출력이 짧으면 점수가 떨어진다.</p>
<p>하지만 BERT-Score는 v2가 최고다. 의미적으로는 더 정확하게 요약한다는 뜻이다. 의례 발언이 빠지니까 진짜 중요한 내용에 집중할 수 있게 된 거다.</p>
<p>의례 발언 제거는 감으로 하는 게 아니라 규칙 기반으로 가는 게 깔끔하다. 개의하겠습니다, 정회하겠습니다, 산회하겠습니다 같은 패턴은 회의록마다 반복된다. 이런 구간을 모델이 계속 먹으면 요약이 흐려진다.</p>
<h4 id="결론">결론</h4>
<p>전처리 효과를 볼 때는 ROUGE만 보면 안 된다. compression_ratio, length_ratio, BERT-Score를 같이 봐야 한다. 의례 발언 제거가 의미적으로는 효과가 있다.</p>
<hr>
<h3 id="2-프롬프트-방식-비교-s4">2. 프롬프트 방식 비교 (S4)</h3>
<p>같은 phi-2 모델, 같은 100개 샘플, 같은 v2 전처리. 프롬프트만 다르게 해서 돌렸다.</p>
<table>
<thead>
<tr>
<th>프롬프트</th>
<th>ROUGE-1</th>
<th>ROUGE-2</th>
<th>ROUGE-L</th>
<th>BERT-Score F1</th>
<th>BLEU</th>
</tr>
</thead>
<tbody><tr>
<td>simple</td>
<td>0.0253</td>
<td>0.0000</td>
<td>0.0253</td>
<td>0.5896</td>
<td>0.0048</td>
</tr>
<tr>
<td>structured</td>
<td>0.0420</td>
<td>0.0105</td>
<td>0.0420</td>
<td>0.5903</td>
<td>0.0019</td>
</tr>
<tr>
<td>extractive_guide</td>
<td>0.0385</td>
<td>0.0025</td>
<td>0.0385</td>
<td>0.5918</td>
<td>0.0008</td>
</tr>
</tbody></table>
<h4 id="분석-1">분석</h4>
<p>structured가 ROUGE 기준 최고다. 형식을 명시하면 모델이 더 구조화된 출력을 낸다. 핵심 안건, 주요 논의, 결정사항 같은 틀을 주니까 모델이 거기에 맞춰서 생성한다.</p>
<p>extractive_guide가 BERT-Score 최고다. 추출적 요약 가이드라인을 주면 원문에서 중요한 표현을 그대로 가져오게 된다. 의미적으로는 더 정확해지지만, 정답과 표현이 다르면 ROUGE는 떨어진다.</p>
<p>simple은 가장 나쁘다. 아무 가이드 없이 그냥 요약해달라고 하면 모델이 뭘 해야 할지 모른다. ROUGE-2가 0.0000이라는 건 bigram 매칭이 전혀 안 된다는 뜻이다.</p>
<p>재밌는 건 BLEU다. structured가 simple보다 BLEU가 낮다. 왜냐면 structured는 모델이 자기 나름대로 구조화해서 쓰기 때문에 정답과 표현이 달라진다.</p>
<h4 id="결론-1">결론</h4>
<p>프롬프트 설계가 효과는 있다. structured가 ROUGE 기준으로는 66% 향상 (0.0253 → 0.0420)이다. 하지만 모델 자체가 한국어를 못하면 한계가 있다. phi-2의 한국어 능력이 약해서 전체 점수 자체가 낮다.</p>
<p>그럼 LLM 쓰면 안 되나? 할 수는 있다. LLaMA 계열, Qwen 계열, 한국어 튜닝 모델로 프롬프트 요약도 가능하다. 근데 점수 싸움으로 들어가면 프롬프트로 요약은 결과가 흔들린다. 그래서 현실적으로는 로컬 모델 + fine-tuning이 제일 안전한 선택이 된다.</p>
<hr>
<h3 id="3-요약-방식-비교-s5">3. 요약 방식 비교 (S5)</h3>
<p>같은 phi-2 모델, 같은 10개 샘플. 직접 요약 vs 피벗 번역(한→영→한).</p>
<table>
<thead>
<tr>
<th>방식</th>
<th>ROUGE-1</th>
<th>ROUGE-L</th>
<th>BERT-Score F1</th>
<th>length_ratio</th>
</tr>
</thead>
<tbody><tr>
<td>direct</td>
<td>0.0410</td>
<td>0.0410</td>
<td>0.5894</td>
<td>1.29</td>
</tr>
<tr>
<td>pivot</td>
<td>0.0605</td>
<td>0.0605</td>
<td>0.6154</td>
<td>4.31</td>
</tr>
</tbody></table>
<h4 id="분석-2">분석</h4>
<p>피벗 방식이 ROUGE-1 기준 47% 좋다. 번역을 두 번 하는데도 더 낫다니 의외다.</p>
<p>length_ratio를 보면 피벗이 4.31이다. 출력이 엄청 길어졌다. 영어로 요약하고 다시 한국어로 번역하면서 내용이 풀어지기 때문이다. 긴 출력이 ROUGE에 유리하게 작용했을 수 있다.</p>
<p>하지만 BERT-Score도 피벗이 높다. 단순히 길이 때문만은 아니다. 영어 요약 모델이 한국어 요약 모델보다 훨씬 성능이 좋아서, 번역 비용을 감수하고도 품질이 더 좋아진 거다.</p>
<p>번역 모델로 NLLB를 썼는데, 정보 손실이 생각보다 적다. 회의록처럼 정형화된 텍스트는 번역이 잘 되는 편이다.</p>
<h4 id="결론-2">결론</h4>
<p>한국어 요약이 잘 안 되면 피벗 번역을 고려해볼 만하다. 추론 시간이 3배 이상 걸리지만, 품질이 더 중요한 상황에서는 쓸만한 대안이다.</p>
<hr>
<h3 id="4-모델-규모-비교-s6">4. 모델 규모 비교 (S6)</h3>
<p>같은 100개 샘플, 같은 v2 전처리. KoBART(124M) vs phi-2(2.7B).</p>
<table>
<thead>
<tr>
<th>모델</th>
<th>파라미터</th>
<th>ROUGE-1</th>
<th>ROUGE-L</th>
<th>BERT-Score F1</th>
<th>추론시간(초/샘플)</th>
<th>score_per_second</th>
</tr>
</thead>
<tbody><tr>
<td>KoBART</td>
<td>124M</td>
<td>0.0879</td>
<td>0.0834</td>
<td>0.6535</td>
<td>1.00</td>
<td>0.0834</td>
</tr>
<tr>
<td>phi-2</td>
<td>2.7B</td>
<td>0.0236</td>
<td>0.0236</td>
<td>0.5952</td>
<td>12.91</td>
<td>0.0018</td>
</tr>
</tbody></table>
<h4 id="분석-3">분석</h4>
<p>22배 큰 모델이 3.7배 나쁘다. 충격적인 결과다.</p>
<p>추론시간을 보면 KoBART가 샘플당 1초, phi-2가 12.91초다. 13배 느리면서 성능도 나쁘다. score_per_second로 효율성을 계산하면 KoBART가 46배 효율적이다.</p>
<p>왜 이런 결과가 나왔을까?</p>
<p>첫째, 언어 특화다. KoBART는 한국어 텍스트로 사전학습됐다. phi-2는 영어 중심으로 학습됐다. 한국어 회의록 요약이라는 태스크에서는 한국어에 특화된 모델이 압도적으로 유리하다.</p>
<p>둘째, 태스크 특화다. KoBART는 요약 태스크로 파인튜닝됐다. phi-2는 범용 LLM이라서 요약에 특화된 게 아니다. 입력을 받아서 요약 형태로 출력하는 법을 KoBART가 더 잘 안다.</p>
<p>셋째, 모델 구조다. KoBART는 seq2seq 구조라서 요약 생성에 최적화돼 있다. phi-2는 causal LM이라서 자연어 생성 전반에 맞춰져 있다.</p>
<p>국내 회의록 요약이나 공공 데이터 요약 쪽을 보면 자주 나오는 조합이 있다. KoBART/KoT5-large 같은 한국어 seq2seq, PEGASUS 계열, BigBird-Pegasus, LED. 근데 공통점은 모델 이름이 아니다. 거의 항상 같이 붙는 게 안건 단위 분리, 발언자 정보 유지, 결정 문장 강조 같은 입력 구조 설계다. 모델만 바꾸는 경우는 생각보다 거의 없다. 왜냐면 다들 한 번쯤 모델만 키워서 해결 안 된다를 맞아봤기 때문이다.</p>
<h4 id="결론-3">결론</h4>
<p>큰 모델이 무조건 좋은 게 아니다. 도메인과 태스크에 맞는 모델을 찾는 게 중요하다. 한국어 요약이라면 한국어 특화 요약 모델을 쓰는 게 훨씬 효율적이다.</p>
<hr>
<h3 id="5-long-context-비교-s3">5. Long-context 비교 (S3)</h3>
<p>긴 회의록 50개 (평균 36,080자). KoBART vs phi-2.</p>
<table>
<thead>
<tr>
<th>모델</th>
<th>max_input</th>
<th>ROUGE-1</th>
<th>ROUGE-L</th>
<th>BERT-Score F1</th>
<th>truncation_rate</th>
</tr>
</thead>
<tbody><tr>
<td>KoBART</td>
<td>1024</td>
<td>0.0660</td>
<td>0.0629</td>
<td>0.6502</td>
<td>100%</td>
</tr>
<tr>
<td>phi-2</td>
<td>2048</td>
<td>0.0179</td>
<td>0.0179</td>
<td>0.5979</td>
<td>100%</td>
</tr>
</tbody></table>
<h4 id="분석-4">분석</h4>
<p>둘 다 truncation_rate가 100%다. 평균 36,000자짜리 회의록을 1024토큰이나 2048토큰으로 처리하면 앞부분만 보고 나머지는 잘린다.</p>
<p>그래도 KoBART가 3.7배 좋다. S6에서 본 것처럼 언어 특화와 태스크 특화의 힘이다. phi-2가 max_input이 2배 긴데도 불구하고 성능이 훨씬 나쁘다.</p>
<p>문제는 결국 여기다. 회의록은 짧아도 2~3천 토큰, 길면 2만 토큰을 넘는다. 그래서 등장하는 게 BigBird, Longformer, LED(Longformer Encoder-Decoder), BigBird-Pegasus 같은 long-context 요약 모델이다. 요즘은 Qwen 같은 long-context 계열이나, Llama 계열에서 컨텍스트 늘린 모델도 많고, RWKV/Mamba처럼 아예 구조가 다른 선택지도 있다.</p>
<p>다만 여기서도 착각하면 안 되는 게, long-context 모델을 쓴다고 해서 긴 글이 자동으로 요약되는 것까진 아니다. 잘릴 확률이 줄어드는 거지, 중요한 부분을 알아서 찾아주는 건 또 다른 문제다.</p>
<h4 id="결론-4">결론</h4>
<p>현재 실험에서는 long-context 효과를 검증 못 했다. 하지만 한 가지는 확실하다. max_input이 2배 길어도 언어 특화가 안 되면 의미없다.</p>
<hr>
<h3 id="종합-비교표">종합 비교표</h3>
<table>
<thead>
<tr>
<th>비교 항목</th>
<th>승자</th>
<th>개선폭</th>
<th>핵심 이유</th>
</tr>
</thead>
<tbody><tr>
<td>전처리 (v0 vs v2)</td>
<td>v2</td>
<td>BERT-Score +2.7%</td>
<td>의례 발언 제거로 핵심 내용 집중</td>
</tr>
<tr>
<td>프롬프트 (simple vs structured)</td>
<td>structured</td>
<td>ROUGE-1 +66%</td>
<td>형식 명시가 구조화된 출력 유도</td>
</tr>
<tr>
<td>요약 방식 (direct vs pivot)</td>
<td>pivot</td>
<td>ROUGE-1 +47%</td>
<td>영어 요약 모델이 한국어보다 강력</td>
</tr>
<tr>
<td>모델 규모 (124M vs 2.7B)</td>
<td>124M</td>
<td>ROUGE-1 +273%</td>
<td>언어+태스크 특화가 규모보다 중요</td>
</tr>
<tr>
<td>Long-context (1K vs 2K)</td>
<td>1K</td>
<td>ROUGE-1 +269%</td>
<td>언어 특화가 context 길이보다 중요</td>
</tr>
</tbody></table>
<hr>
<h3 id="효율성-비교">효율성 비교</h3>
<table>
<thead>
<tr>
<th>모델</th>
<th>파라미터</th>
<th>GPU 메모리 (추정)</th>
<th>추론시간(초/샘플)</th>
<th>score_per_second</th>
<th>효율성 순위</th>
</tr>
</thead>
<tbody><tr>
<td>KoBART</td>
<td>124M</td>
<td>~2GB</td>
<td>1.00</td>
<td>0.0834</td>
<td>1위</td>
</tr>
<tr>
<td>phi-2</td>
<td>2.7B</td>
<td>~6GB</td>
<td>12.91</td>
<td>0.0018</td>
<td>2위</td>
</tr>
</tbody></table>
<p>KoBART가 압도적으로 효율적이다. 메모리도 적게 쓰고, 추론도 빠르고, 성능도 좋다.</p>
<p>phi-2를 쓸 이유가 있다면 하나다. 피벗 번역에서 영어 요약을 할 때. 그 경우에는 phi-2의 영어 능력이 KoBART보다 나을 수 있다.</p>
<hr>
<h3 id="여기서-방향이-갈린다">여기서 방향이 갈린다</h3>
<p>이제 선택지는 명확하다. 모델을 키울 것인가, 구조를 바꿀 것인가, 전처리에 집중할 것인가. 보통 성능이 제일 잘 오르는 건 전처리 + 입력 구조다. 모델은 마지막에 키워도 된다. 오히려 구조가 잡히기 전에 모델부터 키우면, 돈과 시간만 쓰고 왜 좋아졌는지를 못 남긴다.</p>
<p>베이스라인은 답이 아니라 지도다. 어디서 막히는지 보여주고, 뭘 바꿔야 하는지 알려준다. 국회 회의록 요약은 모델 싸움이 아니라 구조 싸움에 가깝다. 그리고 구조를 잡는 순간부터, 점수는 생각보다 쉽게 올라간다.</p>
<hr>
<h3 id="베이스라인-이후-추천-흐름">베이스라인 이후 추천 흐름</h3>
<p>무작정 바꾸지 말고 순서를 지키는 게 중요하다. 베이스라인 이후에는 보통 이 순서로 간다.</p>
<h4 id="1단계--입력-구조부터-바꾸기">1단계 – 입력 구조부터 바꾸기</h4>
<p>안건 단위로 split 하고, 회의 시작/종료 같은 반복 구간을 분리하고, 불필요한 의례 발언을 제거한다. 여기서 점수가 꽤 오른다.</p>
<h4 id="2단계--long-context-모델-테스트">2단계 – Long-context 모델 테스트</h4>
<p>그다음이 BigBird-Pegasus나 LED 같은 long-context 모델이다. 베이스라인에서 잘림 때문에 성능이 박살났다면, 여기서 체감이 확 난다.</p>
<h4 id="3단계--프롬프트-최적화">3단계 – 프롬프트 최적화</h4>
<p>structured 프롬프트, 태그 활용 프롬프트 등. 이건 점수 몇 % 올리는 구간이다.</p>
<h4 id="4단계--선택-7b-모델--lora">4단계 – (선택) 7B 모델 + LoRA</h4>
<p>GPU가 부족하면 여기서 LoRA/QLoRA가 들어간다. 모델을 크게 가져가고 싶은데 자원이 없을 때 현실적인 선택지다. 특히 학교 서버는 한 번에 크게 먹는 실험이 어려운 경우가 많아서, 파라미터 효율화는 그냥 선택이 아니라 생존 스킬이 된다.</p>
<hr>
<h3 id="다음-실험-방향">다음 실험 방향</h3>
<p>지금 실험 결과를 바탕으로 다음에 해볼 것들이다.</p>
<p>첫째, 진짜 long-context 모델 테스트. LongChat-7B-16K나 Yarn-Mistral-7B-128K로 긴 회의록 요약 성능을 봐야 한다. 4bit quantization이나 더 큰 GPU가 필요하다.</p>
<p>둘째, 한국어 LLM 테스트. KoAlpaca, Polyglot-Ko 같은 한국어 특화 LLM으로 요약 성능을 봐야 한다. phi-2보다 나을 가능성이 높다.</p>
<p>셋째, 파인튜닝. KoBART를 국회 회의록 데이터로 추가 파인튜닝하면 성능이 더 오를 수 있다. 현재는 범용 요약 모델이라 도메인 특화가 안 돼 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[그럼, 베이스라인 모델부터 돌려보자]]></title>
            <link>https://velog.io/@yujin_jeong/%EA%B7%B8%EB%9F%BC-%EB%B2%A0%EC%9D%B4%EC%8A%A4%EB%9D%BC%EC%9D%B8-%EB%AA%A8%EB%8D%B8%EB%B6%80%ED%84%B0-%EB%8F%8C%EB%A0%A4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@yujin_jeong/%EA%B7%B8%EB%9F%BC-%EB%B2%A0%EC%9D%B4%EC%8A%A4%EB%9D%BC%EC%9D%B8-%EB%AA%A8%EB%8D%B8%EB%B6%80%ED%84%B0-%EB%8F%8C%EB%A0%A4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Mon, 19 Jan 2026 09:06:16 GMT</pubDate>
            <description><![CDATA[<p>Slurm까지 왔으면 이제 진짜 모델 하나를 서버에서 끝까지 돌려보는 경험을 해야 한다. 국회 회의록 요약 과제를 첫 실전으로 잡은 이유는 단순하다. 데이터가 길고, 구조가 있고, 평가 기준이 명확하다!</p>
<hr>
<h3 id="왜-국회-회의록-요약인가">왜 국회 회의록 요약인가</h3>
<p>이 과제는 생각보다 많은 걸 한 번에 경험하게 한다. 입력이 길어서 long sequence 문제를 체감하게 되고, JSON 구조라 전처리를 피할 수 없고, 요약 태스크라 생성 모델 흐름을 그대로 밟게 되고, ROUGE 평가로 감이 아니라 수치로 비교하게 된다. 즉, NLP 실험의 기본 골격이 전부 들어 있다. 처음 실전으로 딱 좋다.</p>
<hr>
<h3 id="데이터-구조-먼저-보기">데이터 구조 먼저 보기</h3>
<p>국회 회의록 데이터(말평 2024)는 대략 이런 형태다.</p>
<pre><code class="language-json">{
  &quot;id&quot;: &quot;nikluge-2024-국회회의록안건별요약-train-000001&quot;,
  &quot;input&quot;: {
    &quot;speaker&quot;: [
      {&quot;id&quot;: &quot;김상희&quot;, &quot;occupation&quot;: &quot;위원&quot;, &quot;original_id&quot;: &quot;김상희&quot;},
      {&quot;id&quot;: &quot;문창진&quot;, &quot;occupation&quot;: &quot;보건복지부차관&quot;, &quot;original_id&quot;: &quot;문창진&quot;}
    ],
    &quot;conversation&quot;: [
      {&quot;id&quot;: &quot;SBRW2100000001.1.1.1&quot;, &quot;speaker&quot;: &quot;김상희&quot;, &quot;utterance&quot;: &quot;회의를 시작하도록 하겠습니다.&quot;},
      {&quot;id&quot;: &quot;SBRW2100000001.1.1.2&quot;, &quot;speaker&quot;: &quot;김상희&quot;, &quot;utterance&quot;: &quot;의사일정 제1항을 상정합니다.&quot;}
    ],
    &quot;issue&quot;: &quot;2008회계연도 세입세출결산&quot;
  },
  &quot;output&quot;: &quot;본 회의에서 여성부의 2008회계연도 세입세출결산에 관해...&quot;
}</code></pre>
<p>여기서 중요한 건, 하나의 문서가 문장 하나가 아니라 <strong>발언들의 시퀀스</strong>라는 점이다. <code>input.conversation</code>에 평균 500개 발화가 들어 있고, 실제 모델 입력은 <code>[발언1][발언2][발언3]...</code> 같은 형태로 이어 붙인 긴 텍스트가 된다. 그래서 이 과제는 시작부터 long document 요약 문제다. 그냥 요약 모델 학습이 아니라, 입력 길이 때문에 학습/추론이 흔들릴 수 있다는 걸 처음부터 맞게 된다.</p>
<hr>
<h3 id="seraph-서버-환경-세팅">Seraph 서버 환경 세팅</h3>
<h4 id="프로젝트-구조-생성">프로젝트 구조 생성</h4>
<pre><code class="language-bash"># 서버 접속 후
ssh &lt;user&gt;@seraph.khu.ac.kr

# 프로젝트 루트 (home 말고 /data 사용 - quota 문제 방지)
MY_ID=$(whoami)
PROJECT_ROOT=&quot;/data/${MY_ID}/projects/nams_experiments&quot;

# 디렉토리 생성
mkdir -p $PROJECT_ROOT/{raw,data/{interim,processed},src/{preprocess,train,infer,eval,utils},configs,scripts,slurm,logs,outputs}
cd $PROJECT_ROOT</code></pre>
<h4 id="huggingface-캐시-경로-설정">HuggingFace 캐시 경로 설정</h4>
<p>이거 안 하면 home 밑에 캐시가 쌓이고 quota 터질 수 있다.</p>
<pre><code class="language-bash">export HF_HOME=&quot;/data/${MY_ID}/.cache/huggingface&quot;
export TRANSFORMERS_CACHE=&quot;$HF_HOME/transformers&quot;
mkdir -p $HF_HOME $TRANSFORMERS_CACHE

# 매번 치기 귀찮으면 env_vars.sh 만들기
cat &gt; scripts/env_vars.sh &lt;&lt; &#39;EOF&#39;
export PROJECT_ROOT=&quot;/data/$(whoami)/projects/nams_experiments&quot;
export HF_HOME=&quot;/data/$(whoami)/.cache/huggingface&quot;
export TRANSFORMERS_CACHE=&quot;$HF_HOME/transformers&quot;
export PYTHONUNBUFFERED=1
EOF</code></pre>
<h4 id="conda-환경-생성">Conda 환경 생성</h4>
<p>환경은 최대한 단순하게 간다.</p>
<pre><code class="language-bash">conda create -n nams python=3.10 -y
conda activate nams

# PyTorch (CUDA 11.8)
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118

# 핵심 패키지
pip install transformers datasets accelerate
pip install rouge-score bert-score sacrebleu
pip install pandas numpy scikit-learn tqdm pyyaml</code></pre>
<p>이 단계에서 의존성 충돌 나면, 그게 바로 HPC 첫 관문이다. 특히 torch/transformers 계열은 버전 꼬이면 에러가 친절하게 나오지 않는다. 그냥 한 줄 보고 멈춘다. 그러면 로그부터 읽는 습관이 여기서 생긴다.</p>
<hr>
<h3 id="데이터-확인부터">데이터 확인부터</h3>
<p>데이터를 <code>raw/</code>에 넣고, 구조부터 확인한다.</p>
<pre><code class="language-bash">ls -la raw/
# 국회회의록안건별요약_train.json (236MB, 1339개)
# 국회회의록안건별요약_dev.json (29MB, 167개)
# 국회회의록안건별요약_test.json (29MB, 167개)</code></pre>
<pre><code class="language-python"># 데이터 구조 확인
import json

with open(&quot;raw/국회회의록안건별요약_train.json&quot;, &quot;r&quot;) as f:
    data = json.load(f)

print(f&quot;문서 수: {len(data)}&quot;)  # 1339
print(f&quot;첫 문서 키: {data[0].keys()}&quot;)  # id, input, output

# 발화 수 확인
conv = data[0][&quot;input&quot;][&quot;conversation&quot;]
print(f&quot;첫 문서 발화 수: {len(conv)}&quot;)  # 544

# 평균 발화 수
avg_utts = sum(len(d[&quot;input&quot;][&quot;conversation&quot;]) for d in data) / len(data)
print(f&quot;평균 발화 수: {avg_utts:.1f}&quot;)  # ~500</code></pre>
<p>이 시점에서 깨달아야 할 것: 평균 500개 발화 = 엄청 긴 입력. KoBART 같은 모델은 max_length가 1024 토큰이라 대부분 잘린다.</p>
<hr>
<h3 id="slurm으로-gpu-테스트-먼저">Slurm으로 GPU 테스트 먼저</h3>
<p>코드 돌리기 전에 GPU가 잡히는지부터 확인한다. 이거 안 하고 바로 학습 돌리면 나중에 왜 안 되지? 하고 삽질하게 된다.</p>
<pre><code class="language-bash"># slurm/debug_gpu_test.sbatch
#!/bin/bash
#SBATCH --job-name=gpu_test
#SBATCH --partition=debug_ugrad
#SBATCH --account=ugrad
#SBATCH --gres=gpu:1
#SBATCH --mem-per-gpu=20G
#SBATCH --time=00:30:00
#SBATCH --output=logs/slurm-%j_gpu_test.out

echo &quot;[INFO] job=$SLURM_JOB_ID&quot;
echo &quot;[INFO] host=$(hostname)&quot;
echo &quot;[INFO] start=$(date)&quot;

nvidia-smi

source ~/.bashrc
conda activate nams

python -c &quot;
import torch
print(f&#39;torch: {torch.__version__}&#39;)
print(f&#39;cuda: {torch.cuda.is_available()}&#39;)
if torch.cuda.is_available():
    print(f&#39;device: {torch.cuda.get_device_name(0)}&#39;)
&quot;

echo &quot;[INFO] end=$(date)&quot;</code></pre>
<pre><code class="language-bash">sbatch slurm/debug_gpu_test.sbatch
tail -f logs/slurm-*_gpu_test.out</code></pre>
<p><strong>CUDA available: True</strong> 뜨면 성공. 여기서 막히면 뒤로 못 간다. 가끔은 GPU 1장 요청했는데 환경 설정이 꼬여서 CPU로만 돌다가 하루 날리는 경우도 있다.</p>
<hr>
<h3 id="베이스라인-kobart로-첫-추론">베이스라인: KoBART로 첫 추론</h3>
<p>학습 전에 <strong>추론부터</strong> 해본다. 모델이 뭘 뱉는지 보는 게 먼저다.</p>
<pre><code class="language-python"># src/infer/baseline_test.py
import json
import torch
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM

def main():
    print(&quot;[INFO] 모델 로드 중...&quot;)
    model_name = &quot;gogamza/kobart-summarization&quot;
    tokenizer = AutoTokenizer.from_pretrained(model_name)
    model = AutoModelForSeq2SeqLM.from_pretrained(model_name)

    device = &quot;cuda&quot; if torch.cuda.is_available() else &quot;cpu&quot;
    model.to(device)
    model.eval()
    print(f&quot;[INFO] device={device}&quot;)

    # 데이터 로드
    with open(&quot;raw/국회회의록안건별요약_dev.json&quot;, &quot;r&quot;) as f:
        data = json.load(f)

    # 첫 번째 문서로 테스트
    doc = data[0]
    conv = doc[&quot;input&quot;][&quot;conversation&quot;]

    # 입력 텍스트 생성 (발화 이어붙이기)
    input_text = &quot; &quot;.join([
        f&quot;[{utt[&#39;speaker&#39;]}] {utt[&#39;utterance&#39;]}&quot;
        for utt in conv
    ])

    print(f&quot;[INFO] 입력 길이: {len(input_text)} chars&quot;)
    print(f&quot;[INFO] 발화 수: {len(conv)}&quot;)

    # 토크나이즈
    inputs = tokenizer(
        input_text,
        return_tensors=&quot;pt&quot;,
        max_length=1024,  # KoBART 최대
        truncation=True,
    ).to(device)

    print(f&quot;[INFO] 토큰 수: {inputs[&#39;input_ids&#39;].shape[1]}&quot;)

    # 생성
    with torch.no_grad():
        outputs = model.generate(
            **inputs,
            max_length=256,
            num_beams=4,
            early_stopping=True,
        )

    summary = tokenizer.decode(outputs[0], skip_special_tokens=True)

    print(&quot;\n&quot; + &quot;=&quot;*50)
    print(&quot;[결과] 생성된 요약:&quot;)
    print(summary)
    print(&quot;\n[정답] 실제 요약:&quot;)
    print(doc[&quot;output&quot;][:500])
    print(&quot;=&quot;*50)

if __name__ == &quot;__main__&quot;:
    main()</code></pre>
<pre><code class="language-bash">python src/infer/baseline_test.py</code></pre>
<p>여기서 확인할 것만 딱 잡으면 된다. 요약이 나오긴 하는가? 토큰 수가 1024로 잘렸는가? (거의 확실히 잘림) 생성된 문장이 말이 되는가?</p>
<p>대부분 애매한 문장이 나온다. 정상이다. 입력이 잘려서 중요한 정보를 못 봤기 때문이다.</p>
<hr>
<h3 id="rouge-평가---숫자로-확인">ROUGE 평가 - 숫자로 확인</h3>
<p>읽어보니 괜찮은데요?는 아무 의미가 없다. 비교하려면 숫자가 있어야 한다.</p>
<pre><code class="language-python"># src/eval/rouge_eval.py
from rouge_score import rouge_scorer

def compute_rouge(predictions, references):
    scorer = rouge_scorer.RougeScorer(
        [&quot;rouge1&quot;, &quot;rouge2&quot;, &quot;rougeL&quot;],
        use_stemmer=False,
    )

    scores = {&quot;rouge1&quot;: [], &quot;rouge2&quot;: [], &quot;rougeL&quot;: []}

    for pred, ref in zip(predictions, references):
        result = scorer.score(ref, pred)
        scores[&quot;rouge1&quot;].append(result[&quot;rouge1&quot;].fmeasure)
        scores[&quot;rouge2&quot;].append(result[&quot;rouge2&quot;].fmeasure)
        scores[&quot;rougeL&quot;].append(result[&quot;rougeL&quot;].fmeasure)

    return {
        k: sum(v) / len(v) for k, v in scores.items()
    }</code></pre>
<p>보통 보는 건 이것.</p>
<table>
<thead>
<tr>
<th>지표</th>
<th>의미</th>
</tr>
</thead>
<tbody><tr>
<td>ROUGE-1</td>
<td>핵심 단어 재현 (unigram)</td>
</tr>
<tr>
<td>ROUGE-2</td>
<td>연속 단어쌍 (bigram)</td>
</tr>
<tr>
<td>ROUGE-L</td>
<td>문장 구조 (최장 공통 부분)</td>
</tr>
</tbody></table>
<p>점수가 낮아도 상관없다. 이건 출발선이다. 베이스라인 점수만 확보해도 이후에 바꾼 게 진짜 좋아졌는지 판단할 수 있게 된다. 그리고 그게 실험의 시작이다.</p>
<hr>
<h3 id="slurm으로-전체-dev셋-추론">Slurm으로 전체 dev셋 추론</h3>
<p>이제 전체 dev셋(167개)에 대해 추론하고 평가한다.</p>
<pre><code class="language-bash"># slurm/01_baseline_infer.sbatch
#!/bin/bash
#SBATCH --job-name=baseline_infer
#SBATCH --partition=batch_ugrad
#SBATCH --account=ugrad
#SBATCH --gres=gpu:1
#SBATCH --mem-per-gpu=20G
#SBATCH --time=02:00:00
#SBATCH --output=logs/slurm-%j_baseline.out

# === 필수 로그 ===
echo &quot;[INFO] job=$SLURM_JOB_ID&quot;
echo &quot;[INFO] host=$(hostname)&quot;
echo &quot;[INFO] start=$(date)&quot;
nvidia-smi

# === 환경 ===
source ~/.bashrc
conda activate nams
source scripts/env_vars.sh

echo &quot;[INFO] HF_HOME=$HF_HOME&quot;

# === PyTorch 확인 ===
python -c &quot;import torch; print(f&#39;[INFO] cuda={torch.cuda.is_available()}&#39;)&quot;

# === 추론 실행 ===
python src/infer/run_baseline.py \
    --input raw/국회회의록안건별요약_dev.json \
    --output outputs/baseline_dev_results.json

# === 완료 ===
echo &quot;[INFO] end=$(date)&quot;</code></pre>
<p>그리고 제출.</p>
<pre><code class="language-bash">sbatch slurm/01_baseline_infer.sbatch</code></pre>
<p>이제 할 일은 딱 하나다.</p>
<pre><code class="language-bash">squeue -u $USER</code></pre>
<p>RUNNING이면 기다리고, PENDING이면 이유를 보고, FAILED면 로그를 연다. 이 루틴이 HPC 생활의 80%다. 참고로 PENDING이 떴다고 무조건 잘못된 게 아니다. 그냥 자리가 없어서 기다리는 중일 수도 있다. 대신 PENDING 사유가 (Resources, Priority 같은) 정상적인 이유인지, 아니면 요청 옵션이 잘못돼서 영원히 못 잡는 상태인지 그걸 보는 게 중요하다.</p>
<p>로그 보는 법:</p>
<pre><code class="language-bash">tail -f logs/slurm-XXXX_baseline.out</code></pre>
<p>GPU가 실제로 잡혔는지, loss가 내려가는지, 갑자기 멈추진 않는지. 그리고 멈췄을 때는 대부분 두 종류다. 하나는 메모리 터짐(OOM), 하나는 데이터/경로 문제로 즉사. 둘 다 로그에 힌트가 남는다.</p>
<hr>
<h3 id="베이스라인-결과-분석">베이스라인 결과 분석</h3>
<p>추론이 끝나면 결과를 본다.</p>
<pre><code class="language-python">import json

with open(&quot;outputs/baseline_dev_results.json&quot;, &quot;r&quot;) as f:
    results = json.load(f)

print(f&quot;ROUGE-1: {results[&#39;rouge1&#39;]:.4f}&quot;)
print(f&quot;ROUGE-2: {results[&#39;rouge2&#39;]:.4f}&quot;)
print(f&quot;ROUGE-L: {results[&#39;rougeL&#39;]:.4f}&quot;)</code></pre>
<h4 id="시나리오-1-실험-결과-kobart-베이스라인">시나리오 1 실험 결과 (KoBART 베이스라인)</h4>
<p>100개 샘플로 돌린 결과다.</p>
<table>
<thead>
<tr>
<th>지표</th>
<th>점수</th>
</tr>
</thead>
<tbody><tr>
<td>ROUGE-1</td>
<td>0.0879</td>
</tr>
<tr>
<td>ROUGE-2</td>
<td>0.0193</td>
</tr>
<tr>
<td>ROUGE-L</td>
<td>0.0834</td>
</tr>
<tr>
<td>BERT-Score F1</td>
<td>0.6535</td>
</tr>
<tr>
<td>BLEU</td>
<td>0.0240</td>
</tr>
</tbody></table>
<p>점수가 처참하다. 왜 그런지 실제 출력을 보면 바로 이해된다.</p>
<h4 id="문제가-보이는-샘플">문제가 보이는 샘플</h4>
<p><strong>샘플 1: 완전히 엉뚱한 내용</strong></p>
<pre><code>[모델 출력]
제265회 국회(임시회) 제2차 법안심사소위원회를 개의해 주신 위원 여러분께
감사의 말씀을 드렸고, &#39;강창일] 오늘 회의에서는 지난 수요일 제1차 회의에 이어...

[정답]
의사일정 제4항 지방자치법 일부개정법률안은 특별지방자치단체를 설치하고
경제자유구역청을 특별지방자치단체로 전환하며 성과공시제도를 입법화하는 내용으로서...</code></pre><p>모델이 회의 시작 부분만 보고 개의합니다, 감사합니다 같은 의례 발언을 요약이라고 뱉었다. 진짜 논의 내용은 뒤에 있는데 거기까지 못 봤다.</p>
<p><strong>샘플 2: 입력 잘림으로 인한 정보 손실</strong></p>
<pre><code>[모델 출력]
작년 12월 5일에 처음 상정된 이 법안은 작년 11월 30일에 상정된 소위가 두 번째입니다.
작년 12월 4일에 처음 올렸는데 그때 변호사 출신의 법무담당관과...

[정답]
본 회의는 법사위원회 산하 법안심사제1소위원회의 법무부 소관 법률에 대한 심의를
위한 회의로, 먼저 정부가 제출한 정부법무공단법안 제정안에 대해 논의하였다...</code></pre><p>회의 맥락은 잡았는데 핵심 내용(정부법무공단법안)을 전혀 못 잡았다.</p>
<h4 id="실패-유형-분석">실패 유형 분석</h4>
<table>
<thead>
<tr>
<th>유형</th>
<th>건수</th>
<th>비율</th>
</tr>
</thead>
<tbody><tr>
<td>off_topic (엉뚱한 내용)</td>
<td>60</td>
<td>60%</td>
</tr>
<tr>
<td>too_long (너무 김)</td>
<td>22</td>
<td>22%</td>
</tr>
<tr>
<td>partial (일부만 맞음)</td>
<td>5</td>
<td>5%</td>
</tr>
<tr>
<td>repetition (반복)</td>
<td>5</td>
<td>5%</td>
</tr>
<tr>
<td>good (괜찮음)</td>
<td>4</td>
<td>4%</td>
</tr>
<tr>
<td>too_short (너무 짧음)</td>
<td>4</td>
<td>4%</td>
</tr>
</tbody></table>
<p>100개 중 4개만 쓸만했다. 나머지 96개는 문제가 있다.</p>
<p>점수가 낮은 이유는 뻔하다. <strong>입력이 잘린다.</strong> 1024 토큰 제한 때문에 앞부분만 보고 요약한다. 그리고 개의하겠습니다 같은 의례 발언이 섞여서 모델이 헷갈린다.</p>
<p>이게 바로 다음 단계(전처리)가 필요한 이유다.</p>
<hr>
<h3 id="시나리오-2-전처리-방식-비교">시나리오 2: 전처리 방식 비교</h3>
<p>그래서 전처리를 바꿔봤다. 세 가지 버전으로.</p>
<ul>
<li><strong>v0</strong>: 원본 그대로 (발화 이어붙이기만)</li>
<li><strong>v1</strong>: 같은 화자 연속 발언 결합</li>
<li><strong>v2</strong>: 의례 발언 제거 + 키워드 추출</li>
</ul>
<p>결과가 재밌게 나왔다.</p>
<table>
<thead>
<tr>
<th>버전</th>
<th>ROUGE-1</th>
<th>ROUGE-2</th>
<th>ROUGE-L</th>
<th>BERT-Score F1</th>
</tr>
</thead>
<tbody><tr>
<td>v0 (원본)</td>
<td>0.1154</td>
<td>0.0217</td>
<td>0.1090</td>
<td>0.6363</td>
</tr>
<tr>
<td>v1 (발언결합)</td>
<td>0.1151</td>
<td>0.0211</td>
<td>0.1111</td>
<td>0.6371</td>
</tr>
<tr>
<td>v2 (의례제거)</td>
<td>0.0879</td>
<td>0.0193</td>
<td>0.0834</td>
<td><strong>0.6535</strong></td>
</tr>
</tbody></table>
<p>예상과 다르게 v2가 ROUGE는 가장 낮다. 왜일까?</p>
<p>compression_ratio를 보면 답이 나온다.</p>
<ul>
<li>v0: 0.87% (입력 거의 안 줄어듦)</li>
<li>v2: 14.87% (입력이 많이 줄어듦)</li>
</ul>
<p>v2는 의례 발언을 제거해서 입력이 짧아졌고, 그래서 모델이 더 많은 내용을 볼 수 있었다. 근데 ROUGE가 낮은 이유는 출력 길이가 달라져서다. BERT-Score가 높은 건 의미적으로는 더 잘 맞춘다는 뜻.</p>
<p>결론은 단순 ROUGE만 보면 안 된다는 거다. 전처리 효과는 복합적이다.</p>
<hr>
<h3 id="시나리오-3-long-context-비교">시나리오 3: Long-context 비교</h3>
<p>긴 문서에서 long-context 모델이 효과 있을까? 평균 36,000자짜리 긴 회의록 50개로 테스트했다.</p>
<table>
<thead>
<tr>
<th>모델</th>
<th>max_input</th>
<th>ROUGE-1</th>
<th>ROUGE-2</th>
<th>ROUGE-L</th>
<th>BERT-Score F1</th>
<th>truncation_rate</th>
</tr>
</thead>
<tbody><tr>
<td>KoBART (truncate)</td>
<td>1024</td>
<td>0.0660</td>
<td>0.0115</td>
<td>0.0629</td>
<td>0.6502</td>
<td>100%</td>
</tr>
<tr>
<td>phi-2</td>
<td>2048</td>
<td>0.0179</td>
<td>0.0000</td>
<td>0.0179</td>
<td>0.5979</td>
<td>100%</td>
</tr>
</tbody></table>
<p>결과가 좀 아쉽다. 둘 다 truncation_rate가 100%라서 결국 전부 잘렸다. 긴 회의록은 평균 36,000자인데 KoBART는 1024토큰, phi-2는 2048토큰이 한계라서 어차피 앞부분만 보게 된다.</p>
<p>그래도 KoBART가 phi-2보다 ROUGE-1 기준 3.7배 좋다. 이건 S6에서 본 것처럼 언어 특화의 힘이다. long-context가 의미 있으려면 최소 16K 이상 지원하는 모델이 필요하다. LongChat-7B-16K나 Yarn-Mistral-7B-128K 같은 모델이 필요한데, 메모리 문제로 이번엔 못 돌렸다.</p>
<hr>
<h3 id="시나리오-4-프롬프트-비교-phi-2">시나리오 4: 프롬프트 비교 (phi-2)</h3>
<p>프롬프트 엔지니어링이 얼마나 효과 있는지 봤다. phi-2(2.7B)로 세 가지 프롬프트를 비교했다.</p>
<ul>
<li><strong>simple</strong>: 그냥 &quot;요약해주세요&quot;</li>
<li><strong>structured</strong>: 형식을 명시 (핵심 안건, 주요 논의, 결정사항)</li>
<li><strong>extractive_guide</strong>: 추출적 요약 가이드라인 제공</li>
</ul>
<table>
<thead>
<tr>
<th>프롬프트</th>
<th>ROUGE-1</th>
<th>ROUGE-2</th>
<th>ROUGE-L</th>
<th>BERT-Score F1</th>
</tr>
</thead>
<tbody><tr>
<td>simple</td>
<td>0.0253</td>
<td>0.0000</td>
<td>0.0253</td>
<td>0.5896</td>
</tr>
<tr>
<td><strong>structured</strong></td>
<td><strong>0.0420</strong></td>
<td><strong>0.0105</strong></td>
<td><strong>0.0420</strong></td>
<td>0.5903</td>
</tr>
<tr>
<td>extractive_guide</td>
<td>0.0385</td>
<td>0.0025</td>
<td>0.0385</td>
<td>0.5918</td>
</tr>
</tbody></table>
<p>점수가 전체적으로 낮다. phi-2가 한국어에 약하기 때문이다. 하지만 상대적으로 보면 structured 프롬프트가 ROUGE 기준 최고고, extractive_guide가 BERT-Score 최고다. simple은 가장 나쁘다. 아무 가이드 없으면 모델이 헤맨다.</p>
<p>결론은 프롬프트 설계가 중요하긴 하지만, 모델 자체가 한국어를 못하면 한계가 있다는 거다.</p>
<hr>
<h3 id="시나리오-5-피벗-번역-한→영→한">시나리오 5: 피벗 번역 (한→영→한)</h3>
<p>이건 좀 재밌는 실험이다. 한국어 요약이 잘 안 되니까, 아예 영어로 바꿔서 요약하고 다시 한국어로 번역하면 어떨까?</p>
<table>
<thead>
<tr>
<th>방식</th>
<th>ROUGE-1</th>
<th>ROUGE-2</th>
<th>ROUGE-L</th>
<th>BERT-Score F1</th>
</tr>
</thead>
<tbody><tr>
<td>direct (직접 요약)</td>
<td>0.0410</td>
<td>0.0000</td>
<td>0.0410</td>
<td>0.5894</td>
</tr>
<tr>
<td><strong>pivot (한→영→한)</strong></td>
<td><strong>0.0605</strong></td>
<td>0.0000</td>
<td><strong>0.0605</strong></td>
<td><strong>0.6154</strong></td>
</tr>
</tbody></table>
<p>놀랍게도 <strong>피벗 방식이 47% 더 좋다.</strong> 한국어 → 영어 번역 → 영어 요약 → 한국어 번역. 번역 두 번 하는데도 더 낫다니.</p>
<p>왜 그럴까? 영어 요약 모델이 훨씬 성능이 좋고, 번역 모델(NLLB)이 꽤 괜찮아서 정보 손실이 적다. 결국 약한 한국어 능력보다 강한 영어 능력에 번역 비용 더한 게 낫다는 얘기다.</p>
<p>물론 추론 시간은 3배 이상 걸린다. 하지만 품질이 더 중요한 상황이라면 고려해볼 만하다.</p>
<hr>
<h3 id="시나리오-6-모델-규모-비교">시나리오 6: 모델 규모 비교</h3>
<p>더 큰 모델이 더 좋을까? KoBART(124M) vs phi-2(2.7B)를 비교했다.</p>
<table>
<thead>
<tr>
<th>모델</th>
<th>파라미터</th>
<th>ROUGE-1</th>
<th>ROUGE-2</th>
<th>ROUGE-L</th>
<th>BERT-Score F1</th>
<th>추론시간(초/샘플)</th>
</tr>
</thead>
<tbody><tr>
<td><strong>KoBART</strong></td>
<td>124M</td>
<td><strong>0.0879</strong></td>
<td><strong>0.0193</strong></td>
<td><strong>0.0834</strong></td>
<td><strong>0.6535</strong></td>
<td><strong>1.00</strong></td>
</tr>
<tr>
<td>phi-2</td>
<td>2.7B</td>
<td>0.0236</td>
<td>0.0000</td>
<td>0.0236</td>
<td>0.5952</td>
<td>12.91</td>
</tr>
</tbody></table>
<p>충격적인 결과다. 22배 큰 모델이 오히려 더 못한다.</p>
<p>왜 그럴까? KoBART는 한국어로 학습됐고 요약용으로 파인튜닝됐다. phi-2는 영어 중심이고 범용 LLM이다. 언어 특화와 태스크 특화가 그만큼 중요하다는 거다. 큰 모델이라고 좋은 게 아니다. 도메인과 태스크가 안 맞으면 의미없다.</p>
<p>효율성 측면에서는 KoBART가 45배 더 효율적이다 (score_per_second 기준).</p>
<p>결론은 무조건 큰 모델 쓰지 말고 태스크에 맞는 모델을 찾으라는 거다.</p>
<hr>
<h3 id="전체-실험-요약">전체 실험 요약</h3>
<table>
<thead>
<tr>
<th>시나리오</th>
<th>핵심 발견</th>
</tr>
</thead>
<tbody><tr>
<td>S1 베이스라인</td>
<td>입력 잘림이 치명적 (91% 실패)</td>
</tr>
<tr>
<td>S2 전처리 비교</td>
<td>의례발언 제거가 BERT-Score 향상</td>
</tr>
<tr>
<td>S3 Long-context</td>
<td>현재 모델은 전부 truncation, 16K+ 필요</td>
</tr>
<tr>
<td>S4 프롬프트</td>
<td>structured 프롬프트가 가장 효과적</td>
</tr>
<tr>
<td>S5 피벗 번역</td>
<td>한→영→한이 직접 요약보다 47% 좋음</td>
</tr>
<tr>
<td>S6 모델 규모</td>
<td>작은 한국어 모델이 큰 영어 모델보다 3.7배 좋음</td>
</tr>
</tbody></table>
<p>핵심 교훈은 네 가지다. 첫째, 입력 길이 문제가 가장 크다. long-context 모델이 필요하다. 둘째, 언어 특화 모델이 범용 LLM보다 낫다. 셋째, 프롬프트 엔지니어링은 도움이 되지만 한계가 있다. 넷째, 피벗 번역은 의외로 효과적인 대안이다.</p>
<hr>
<h3 id="핵심-정리">핵심 정리</h3>
<table>
<thead>
<tr>
<th>단계</th>
<th>할 일</th>
<th>확인</th>
</tr>
</thead>
<tbody><tr>
<td>환경</td>
<td>Conda 생성, HF 캐시 설정</td>
<td><code>torch.cuda.is_available()</code></td>
</tr>
<tr>
<td>데이터</td>
<td>raw/에 복사, 구조 확인</td>
<td>평균 500 발화 확인</td>
</tr>
<tr>
<td>GPU 테스트</td>
<td>debug 파티션에서 확인</td>
<td>nvidia-smi 정상</td>
</tr>
<tr>
<td>베이스라인 추론</td>
<td>KoBART로 첫 요약</td>
<td>요약 문장 출력</td>
</tr>
<tr>
<td>ROUGE 평가</td>
<td>숫자로 기록</td>
<td>출발선 확보</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[Slurm과 작업 스케줄링]]></title>
            <link>https://velog.io/@yujin_jeong/Slurm%EA%B3%BC-%EC%9E%91%EC%97%85-%EC%8A%A4%EC%BC%80%EC%A4%84%EB%A7%81</link>
            <guid>https://velog.io/@yujin_jeong/Slurm%EA%B3%BC-%EC%9E%91%EC%97%85-%EC%8A%A4%EC%BC%80%EC%A4%84%EB%A7%81</guid>
            <pubDate>Mon, 19 Jan 2026 09:04:17 GMT</pubDate>
            <description><![CDATA[<h3 id="왜-생겼고-왜-반드시-거쳐야-하는지">왜 생겼고, 왜 반드시 거쳐야 하는지</h3>
<p>HPC를 쓰기 시작하면 아무리 늦어도 결국 마주치게 되는 게 Slurm이다. 처음엔 그냥 “GPU 쓰려면 이거 해야 한대” 수준으로 지나가지만, 조금만 써보면 깨닫게 된다. Slurm은 선택지가 아니라 전제 조건이라는 것을...
<img src="https://velog.velcdn.com/images/yujin_jeong/post/abc18f1c-2f0f-42dc-8c06-d9836be031d2/image.png" alt="">
<a href="https://ko.wikipedia.org/wiki/%EC%8A%AC%EB%9F%BC_%EC%9B%8C%ED%81%AC%EB%A1%9C%EB%93%9C_%EB%A7%A4%EB%8B%88%EC%A0%80">이미지 출처 : 위키백과</a></p>
<h3 id="1-slurm은-왜-필요해졌을까">1. Slurm은 왜 필요해졌을까</h3>
<blockquote>
<p>GPU / CPU는 항상 부족하다</p>
</blockquote>
<p>연구실에 사람이 100명 있고 GPU가 16개면, 누가 지금 GPU를 쓰는지 알 수 없고, 먼저 접속한 사람이 계속 점유하고, 누군가 실수로 프로세스 날리고, 학습 중이던 실험은 그대로 터진다. 이게 “이론적으로”가 아니라 실제로 흔하게 생긴다. 그리고 공용 서버는 이런 상황이 한 번 터지면 진짜로 다 같이 피곤해진다.</p>
<p>공정하게 나눌 방법이 필요했다</p>
<p>딥러닝 학습은 몇 분짜리 작업이 아니라 보통 수시간~수십시간 단위고, 길면 며칠 동안 GPU를 붙잡는다. 이걸 아무 제약 없이 풀어두면 서버는 곧 몇 명의 전유물이 된다. 그래서 “누가, 어떤 자원을, 얼마나 쓰는지”를 시스템이 강제로 관리하는 구조가 필요해졌고, 그 역할을 하는 게 스케줄러다.</p>
<p>자동화 없이는 운영이 안 된다</p>
<p>사람 손으로는 절대 운영이 안 된다. job 끝나면 다음 작업 자동 실행, 자원 사용 기록, 여러 노드에 걸친 분산 작업 실행 같은 걸 사람이 매번 수동으로 한다? 불가능하다. 그래서 <strong>Job Scheduler가 등장</strong>했고, HPC 환경에서는 Slurm이 사실상 표준처럼 굴러간다(센터/기관마다 PBS 같은 다른 스케줄러도 있긴 한데, 국내 대학/연구실은 Slurm 비율이 진짜 높다).</p>
<h3 id="2-slurm의-기본-사고방식">2. Slurm의 기본 사고방식</h3>
<p><img src="https://velog.velcdn.com/images/yujin_jeong/post/f2d16f5f-58a5-446a-8047-934cdbe48fa8/image.png" alt=""></p>
<p>복잡하게 생각할 필요 없다. Slurm이 하는 일은 딱 세 가지다. 자원을 예약하고, 작업을 큐(queue)에 넣고, 빈 자리가 나면 알아서 실행한다. 즉 “GPU를 직접 잡고 쓰는 것”이 아니라 GPU 사용권을 요청해서 배정받는 구조라는 게 핵심이다. 여기서 제일 중요한 마인드셋은 이거다: 로그인 노드에서 뭔가를 돌리는 게 아니라, 계산 노드에서 돌릴 수 있게 ‘요청서를 제출하는 방식’이다.</p>
<h3 id="3-hpc에서의-기본-흐름">3. HPC에서의 기본 흐름</h3>
<p>항상 이 순서로 돌아간다.</p>
<p>User → Login Node → Slurm Scheduler → Compute Node (GPU/CPU)
<img src="https://velog.velcdn.com/images/yujin_jeong/post/d9787eee-6c7f-4cf2-9c99-87ca8890d398/image.png" alt=""></p>
<p>로그인 노드는 접속해서 파일 올리고, 환경 잡고, 작업 제출하는 곳이다. 여기서 학습을 돌리면 안 된다(정확히는 “절대 금지”로 막아둔 곳도 많고, 안 막아도 하면 민폐가 된다). 스케줄러(Slurm)는 누가 뭘 요청했는지 보고 순서랑 자원 배치를 결정한다. 계산 노드는 실제 GPU 연산이 일어나는 곳이다. Aurora든 Ariel이든 큰 구조는 거의 비슷하고, 달라지는 건 “GPU 종류/노드 스펙/파티션 정책/시간 제한” 같은 운영 디테일이다.</p>
<h3 id="4-가장-많이-쓰는-slurm-명령어들">4. 가장 많이 쓰는 Slurm 명령어들</h3>
<p>외울 필요는 없고 자주 쓰는 것만 손에 익히면 된다(고 생각하긴한다)</p>
<table>
<thead>
<tr>
<th>목적</th>
<th>명령어</th>
</tr>
</thead>
<tbody><tr>
<td>클러스터 상태</td>
<td><code>sinfo</code></td>
</tr>
<tr>
<td>내 job 보기</td>
<td><code>squeue -u $USER</code></td>
</tr>
<tr>
<td>전체 job 보기</td>
<td><code>squeue</code></td>
</tr>
<tr>
<td>job 제출</td>
<td><code>sbatch run.sh</code></td>
</tr>
<tr>
<td>인터랙티브 실행</td>
<td><code>srun --pty bash</code></td>
</tr>
<tr>
<td>인터랙티브 GPU</td>
<td><code>srun --gres=gpu:1 --pty bash</code></td>
</tr>
<tr>
<td>job 취소</td>
<td><code>scancel JOBID</code></td>
</tr>
<tr>
<td>job 통계/자원 사용량</td>
<td><code>sacct -j JOBID</code></td>
</tr>
<tr>
<td>GPU 상태(노드 안에서)</td>
<td><code>nvidia-smi</code></td>
</tr>
<tr>
<td>로그 확인</td>
<td><code>tail -f logs/xxx.out</code></td>
</tr>
</tbody></table>
<p>여기서 포인트 하나. nvidia-smi는 “로그인 노드에서 치는 명령”이 아니라, GPU가 잡힌 계산 노드에서 확인하는 용도다. 그래서 보통 srun으로 노드 들어간 다음에 확인하는 흐름이 자연스럽다.</p>
<p>그리고 sinfo 봤을 때 파티션(partition)이 여러 개면, 그건 “GPU용 큐 / CPU용 큐 / 수업용 큐 / 연구용 큐” 이런 식으로 나뉘어 있는 경우가 많다. 그때는 --partition=... 옵션을 써야 할 때가 있다(안 쓰면 기본 파티션으로 들어가거나, 아예 제출이 거절될 수도 있음).</p>
<h3 id="5-sbatch-vs-srun-처음에-제일-헷갈리는-포인트">5. sbatch vs srun (처음에 제일 헷갈리는 포인트)</h3>
<h4 id="sbatch">sbatch</h4>
<p>배치 작업이다. 스크립트 제출하면 큐에 들어가고, 차례가 오면 알아서 시작되고, 알아서 끝난다. 결과는 로그 파일로 확인한다. 긴 학습, 실험 돌리기, 밤새 돌리기 전부 sbatch다.</p>
<h4 id="srun">srun</h4>
<p>인터랙티브 작업이다. GPU 붙은 쉘을 하나 받는 느낌이다. 디버깅, 환경 확인, “이 코드 돌아가나?” 테스트할 때 쓴다. 단, 오래 돌릴 거면 srun으로 버티는 게 아니라 sbatch로 넘기는 게 정석이다(연결 끊기거나, 세션 꼬이면 피곤해진다).</p>
<p>정리하면 이거다. “이 코드가 돌아갈까?” → srun / “이제 제대로 학습 돌린다” → sbatch</p>
<h3 id="6-최소-slurm-스크립트-예시">6. 최소 Slurm 스크립트 예시</h3>
<pre><code class="language-bash">#!/bin/bash
#SBATCH --job-name=train
#SBATCH --gres=gpu:1
#SBATCH --time=24:00:00
#SBATCH --output=logs/%j.out

source ~/.bashrc
conda activate myenv

python train.py \
  --epochs 3 \
  --batch_size 16</code></pre>
<p>여기서 <code>%j</code>는 job id로 자동 치환된다. 로그는 무조건 파일로 남기는 게 맞고, stdout만 믿고 nohup 같은 걸로 버티는 건 HPC에서는 굳이 추천하지 않는다(어차피 Slurm이 로그를 남겨준다). 그리고 은근히 중요한 게 하나 더 있는데, <code>logs/</code> 폴더는 미리 만들어놔야 한다. 없으면 출력 경로 때문에 job이 바로 실패하는 경우도 있다.</p>
<p>실제로는 여기서 메모리/CPU도 같이 지정하는 경우가 많다. 예를 들면 <code>--cpus-per-task=4</code>, <code>--mem=16G</code> 같은 옵션들. GPU만 잡아놓고 CPU나 메모리를 너무 적게 잡으면 데이터 로더에서 병목 걸리거나, 반대로 메모리 부족으로 학습이 터질 수도 있다. 그러니까 “GPU만 있으면 끝”이 아니라, GPU+CPU+RAM이 세트로 돌아간다고 생각하는 게 편하다.</p>
<h2 id="결론">결론</h2>
<p>Slurm은 GPU를 쓰기 어렵게 만드는 시스템이 아니라, 여러 사람이 안전하게 쓰게 만드는 시스템이다. 처음엔 귀찮고 명령어도 많아 보이지만, <code>sinfo</code>, <code>squeue</code>, <code>srun</code>, <code>sbatch</code> 이 네 개만 익숙해지면 HPC의 절반은 넘은 거다. 그리고 진짜 중요한 건, “내가 지금 어느 노드에서 뭘 돌리고 있는지” 이걸 계속 의식하는 습관이다. 로그인 노드에서 돌리는 순간부터 대부분의 사고가 시작한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[관련 용어 정리]]></title>
            <link>https://velog.io/@yujin_jeong/%EA%B4%80%EB%A0%A8-%EC%9A%A9%EC%96%B4-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@yujin_jeong/%EA%B4%80%EB%A0%A8-%EC%9A%A9%EC%96%B4-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Mon, 19 Jan 2026 07:15:27 GMT</pubDate>
            <description><![CDATA[<h2 id="가장-넓은-개념들">가장 넓은 개념들</h2>
<blockquote>
<h4 id="llm-large-language-model">LLM (Large Language Model)</h4>
</blockquote>
<ul>
<li>사람처럼 문장을 생성하는 <strong>대규모 언어 모델</strong></li>
<li>GPT, LLaMA 같은 모델 전부 여기에 포함</li>
</ul>
<blockquote>
<h4 id="추론inference">추론(Inference)</h4>
</blockquote>
<ul>
<li>이미 학습된 모델이 <strong>입력을 받아서 답을 생성하는 과정</strong></li>
<li>학습(training)과 반대 개념</li>
</ul>
<blockquote>
<h4 id="서빙serving">서빙(Serving)</h4>
</blockquote>
<ul>
<li>모델을 <strong>여러 사람이 동시에 쓰도록 API 형태로 제공</strong>하는 것</li>
<li>“모델 실행” + “요청 관리” + “운영”까지 포함</li>
</ul>
<p>LLM(Large Language Model)은 대규모 텍스트 데이터를 학습해 문장을 생성하는 모델을 의미한다. GPT, LLaMA 계열 모델 모두 여기에 포함된다.</p>
<p>이 모델을 <strong>학습이 끝난 상태에서 실제로 문장을 생성하는 과정</strong>을 추론(inference)이라고 한다. 즉, “질문을 넣고 답을 받는 행위” 자체가 추론이다.</p>
<p>서빙(serving)은 여기서 한 단계 더 나아간 개념이다. 단순히 모델을 실행하는 것이 아니라, <strong>여러 사용자가 동시에 모델을 사용할 수 있도록 API 형태로 제공하고</strong>, 요청 관리·지연 시간·장애 대응까지 포함한 <strong>운영 관점의 개념</strong>이다.</p>
<p>이 구분이 중요한 이유는,</p>
<ul>
<li>추론만 잘 되는 구조와</li>
<li>서빙까지 고려된 구조가
완전히 다른 방향으로 설계되기 때문이다.</li>
</ul>
<h2 id="실행-도구--프레임워크-용어">실행 도구 / 프레임워크 용어</h2>
<blockquote>
<h4 id="llamacpp">llama.cpp</h4>
</blockquote>
<ul>
<li>로컬 PC에서 LLM을 실행하기 위한 <strong>경량 추론 엔진</strong></li>
<li>CPU에서도 동작 가능</li>
<li>개인 실험용에 최적화</li>
</ul>
<blockquote>
<h4 id="vllm">vLLM</h4>
</blockquote>
<ul>
<li>GPU 기반 <strong>LLM 서빙 프레임워크</strong></li>
<li>여러 요청을 동시에 효율적으로 처리하는 데 초점</li>
</ul>
<blockquote>
<h4 id="tgi-text-generation-inference">TGI (Text Generation Inference)</h4>
</blockquote>
<ul>
<li>HuggingFace에서 만든 <strong>프로덕션용 LLM 서빙 시스템</strong></li>
<li>Kubernetes, API 운영에 적합</li>
</ul>
<blockquote>
<h4 id="fastapi">FastAPI</h4>
</blockquote>
<ul>
<li>Python 기반 <strong>API 서버 프레임워크</strong></li>
<li>llama.cpp를 감싸서 간단한 서버를 만들 때 사용</li>
</ul>
<h2 id="자원의-차이">자원의 차이</h2>
<blockquote>
<h4 id="cpu">CPU</h4>
</blockquote>
<ul>
<li>일반적인 연산 장치</li>
<li>복잡하지만 병렬성이 낮음</li>
</ul>
<blockquote>
<h4 id="gpu">GPU</h4>
</blockquote>
<ul>
<li>단순한 연산을 <strong>대량 병렬 처리</strong>하는 장치</li>
<li>LLM 추론에 매우 유리</li>
</ul>
<blockquote>
<h4 id="ram">RAM</h4>
</blockquote>
<ul>
<li>일반 메모리</li>
<li>CPU가 사용하는 작업 공간</li>
</ul>
<blockquote>
<h4 id="vram">VRAM</h4>
</blockquote>
<ul>
<li>GPU 전용 메모리</li>
<li>모델 가중치, KV cache가 올라감</li>
<li>부족하면 실행 자체가 안 됨</li>
</ul>
<p>CPU는 복잡한 작업을 소수의 코어로 처리하는 범용 연산 장치다. 반면 GPU는 단순한 연산을 수천 개 코어로 동시에 처리하는 병렬 연산 장치다.</p>
<p>Transformer 기반 LLM은 행렬 곱 연산이 대부분이기 때문에 GPU 구조와 잘 맞는다. 이 때문에 <strong>LLM 추론은 GPU에서 훨씬 효율적</strong>이다.</p>
<p>여기서 메모리 개념이 나뉜다.</p>
<ul>
<li>RAM: CPU가 사용하는 일반 메모리</li>
<li>VRAM: GPU 전용 메모리</li>
</ul>
<p>LLM 추론 시에는 다음이 VRAM에 올라간다.</p>
<ul>
<li>모델 가중치(weight)</li>
<li>KV cache</li>
<li>중간 연산 결과</li>
</ul>
<p>따라서 VRAM이 부족하면, RAM이 아무리 많아도 모델은 실행되지 않는다.</p>
<h2 id="숫자-표현-방식과-fp16의-의미">숫자 표현 방식과 FP16의 의미</h2>
<blockquote>
<h4 id="7b--13b--70b">7B / 13B / 70B</h4>
</blockquote>
<ul>
<li>모델 파라미터 개수</li>
<li>B = Billion (10억)</li>
<li>크다고 무조건 좋은 건 아님</li>
</ul>
<blockquote>
<h4 id="context-length-n_ctx">context length (n_ctx)</h4>
</blockquote>
<ul>
<li>모델이 <strong>한 번에 기억할 수 있는 토큰 길이</strong></li>
<li>길수록 메모리 사용량 증가</li>
</ul>
<p>딥러닝 모델의 가중치는 결국 숫자다.
이 숫자를 <strong>몇 비트로 표현하느냐</strong>가 메모리 사용량과 성능에 직접적인 영향을 준다.</p>
<p>대표적인 표현 방식은 다음과 같다.</p>
<pre><code>FP32 (32비트): 정밀도 높음, 4바이트
FP16 (16비트): 정밀도 중간, 2바이트
INT8 (8비트): 정밀도 낮음, 1바이트</code></pre><p>FP16은 FP32 대비 <strong>모델 크기를 정확히 절반으로 줄인다</strong>.</p>
<p>예를 들어 7B 모델 기준으로 보면,</p>
<ul>
<li>FP32: 약 28GB</li>
<li>FP16: 약 14GB</li>
</ul>
<p>이 차이로 인해 GPU VRAM에 <strong>더 큰 모델을 올릴 수 있게 된다</strong>.</p>
<p>중요한 점은, FP16은 “품질을 크게 희생한 옵션”이 아니라, <strong>추론 환경에서 사실상 표준에 가까운 설정</strong>이라는 점이다.
대부분의 추론 프레임워크는 FP16 또는 BF16을 기준으로 성능과 안정성을 설계하며, 성능 손실은 일반적으로 1–2% 이내로 알려져 있다.</p>
<h2 id="gguf와-양자화">GGUF와 양자화</h2>
<blockquote>
<h4 id="gguf">GGUF</h4>
</blockquote>
<ul>
<li>llama.cpp용 모델 포맷</li>
<li>가중치 + 토크나이저 + 메타데이터 포함</li>
<li>로컬 실행에 최적화</li>
</ul>
<blockquote>
<h4 id="safetensors">safetensors</h4>
</blockquote>
<ul>
<li>HuggingFace 표준 모델 포맷</li>
<li>GPU 서빙 환경에 적합</li>
<li>vLLM/TGI에서 주로 사용</li>
</ul>
<blockquote>
<h4 id="transformers-weights">Transformers weights</h4>
</blockquote>
<ul>
<li>HuggingFace Transformers 라이브러리에서 사용하는 모델 가중치</li>
</ul>
<p>GGUF는 <code>llama.cpp</code> 개발자 Georgi Gerganov가 만든 <strong>모델 저장 포맷</strong>이다.
기존 GGML 포맷을 개선한 형태로, 로컬 LLM 실행을 전제로 설계되었다.</p>
<p>GGUF의 핵심 특징은 다음과 같다.</p>
<ol>
<li>모델 가중치와 토크나이저, 메타데이터가 한 파일에 포함된다.</li>
<li>메모리 매핑이 최적화되어 RAM 사용이 효율적이다.</li>
<li>양자화 정보가 파일 자체에 내장된다.</li>
</ol>
<p>파일 이름만 봐도 설정을 유추할 수 있다.</p>
<pre><code>llama-2-7b.Q4_K_M.gguf</code></pre><ul>
<li>Q4: 4비트 양자화</li>
<li>K: K-quant 방식</li>
<li>M: 중간 품질 프로파일</li>
</ul>
<p>양자화는 가중치를 더 적은 비트로 표현해 <strong>메모리 사용량을 극단적으로 줄이는 기법</strong>이다.</p>
<p>예를 들면,</p>
<ul>
<li>FP16 원본: 14GB</li>
<li>Q4_K_M: 약 4.5GB</li>
<li>Q8_0: 약 8GB</li>
</ul>
<p>이 덕분에 <strong>CPU 환경에서도 7B, 13B 모델 실행이 가능</strong>해졌다.</p>
<p>다만 GGUF 기반 양자화는 <strong>로컬 단일 요청 실행</strong>에 최적화된 구조이며,
GPU 서빙 환경에서는 AWQ, GPTQ 같은 다른 양자화 방식이 사용된다.</p>
<h2 id="양자화quantization-용어">양자화(Quantization) 용어</h2>
<blockquote>
<h4 id="양자화quantization">양자화(Quantization)</h4>
</blockquote>
<ul>
<li>모델 가중치를 <strong>더 적은 비트로 표현</strong>해서 메모리를 줄이는 기법</li>
</ul>
<blockquote>
<h4 id="q4--q5--q8">Q4 / Q5 / Q8</h4>
</blockquote>
<ul>
<li>가중치를 몇 비트로 저장하는지 의미</li>
<li>숫자 ↓ → 메모리 ↓ / 품질 손실 가능성 ↑</li>
</ul>
<blockquote>
<h4 id="awq--gptq">AWQ / GPTQ</h4>
</blockquote>
<ul>
<li>GPU 서빙 환경용 양자화 방식</li>
<li>GGUF와는 다른 계열</li>
</ul>
<h2 id="llamacpp-vllm-tgi">llama.cpp, vLLM, TGI</h2>
<p>앞선 두 글에서 자주 언급하긴 했지만... <code>llama.cpp</code>는 로컬 환경에서 LLM을 실행하기 위한 <strong>경량 추론 엔진</strong>이다.
단일 프로세스, 단일 요청을 빠르고 가볍게 처리하는 데 초점이 맞춰져 있다.</p>
<p>반면 <strong>vLLM</strong>과 <strong>TGI</strong>는 처음부터 <strong>다중 사용자 서빙</strong>을 전제로 설계되었다.</p>
<ul>
<li>continuous batching</li>
<li>KV cache 최적화</li>
<li>GPU 활용률 극대화</li>
<li>스트리밍 응답</li>
</ul>
<p>vLLM은 특히 continuous batching과 PagedAttention을 통해,
요청이 늘어날수록 GPU 효율이 오히려 좋아지는 구조를 만든다.</p>
<p>TGI는 여기에 더해 pipeline parallelism, Kubernetes 연계를 통한 운영 기능까지 포함한다.</p>
<hr>
<h2 id="내부-동작-개념--토큰-kv-cache-batching">내부 동작 개념 — 토큰, KV cache, batching</h2>
<blockquote>
<h4 id="token">Token</h4>
</blockquote>
<ul>
<li>모델이 처리하는 최소 단위</li>
<li>단어보다 더 잘게 쪼개진 단위</li>
</ul>
<blockquote>
<h4 id="throughput">Throughput</h4>
</blockquote>
<ul>
<li><strong>초당 처리 가능한 토큰 양</strong></li>
<li>서버 성능 지표</li>
</ul>
<blockquote>
<h4 id="latency">Latency</h4>
</blockquote>
<ul>
<li>요청 → 응답까지 걸리는 시간</li>
<li>사용자 체감 속도</li>
</ul>
<blockquote>
<h4 id="kv-cache">KV Cache</h4>
</blockquote>
<ul>
<li>이전 토큰 계산 결과를 저장하는 캐시</li>
<li>길어질수록 메모리 사용 증가</li>
</ul>
<blockquote>
<h4 id="continuous-batching">Continuous Batching</h4>
</blockquote>
<ul>
<li>여러 사용자의 요청을 <strong>자동으로 묶어서 GPU에 한 번에 처리</strong></li>
<li>vLLM의 핵심 기술</li>
</ul>
<blockquote>
<h4 id="pagedattention">PagedAttention</h4>
</blockquote>
<ul>
<li>KV cache를 효율적으로 쪼개 관리하는 방식</li>
<li>vLLM 내부 최적화 기법</li>
</ul>
<p>LLM은 문장을 <strong>토큰(token)</strong> 단위로 처리한다.
사용자가 입력한 문장과 모델이 생성하는 응답은 모두 토큰 단위로 쪼개진다.</p>
<p>이 과정에서 이전 토큰 계산 결과를 저장하는 것이 <strong>KV cache</strong>다.
Context가 길어질수록 KV cache가 커지고, VRAM 사용량도 함께 증가한다.</p>
<p>vLLM의 핵심은 이 KV cache를 요청 단위가 아니라 <strong>prefix 단위로 관리</strong>한다는 점이다.
이를 통해 여러 요청이 동일한 system prompt를 공유할 수 있고,
batching 효율이 극적으로 올라간다.</p>
<h2 id="운영-관점--왜-돌아간다와-서비스-가능은-다른가">운영 관점 | 왜 “돌아간다”와 “서비스 가능”은 다른가</h2>
<blockquote>
<h4 id="api">API</h4>
</blockquote>
<ul>
<li>외부에서 모델을 호출하기 위한 인터페이스</li>
</ul>
<blockquote>
<h4 id="sla">SLA</h4>
</blockquote>
<ul>
<li>서비스 품질 보장 기준</li>
<li>응답 시간, 가용성 등</li>
</ul>
<blockquote>
<h4 id="동시-요청-concurrency">동시 요청 (Concurrency)</h4>
</blockquote>
<ul>
<li>여러 사용자가 동시에 요청을 보내는 상황</li>
</ul>
<blockquote>
<h4 id="queue">Queue</h4>
</blockquote>
<ul>
<li>요청이 처리되기 전 대기하는 줄</li>
</ul>
<blockquote>
<h4 id="blocking">Blocking</h4>
</blockquote>
<ul>
<li>하나의 요청이 끝날 때까지 다른 요청이 기다리는 상태</li>
</ul>
<blockquote>
<h4 id="observability-관측성">Observability (관측성)</h4>
</blockquote>
<ul>
<li>로그, 메트릭, 상태를 관찰할 수 있는 능력</li>
</ul>
<blockquote>
<h4 id="kubernetes-k8s">Kubernetes (K8s)</h4>
</blockquote>
<ul>
<li>컨테이너 기반 서버를 자동으로 배포·확장하는 시스템</li>
<li>TGI와 자주 함께 사용</li>
</ul>
<p>로컬 환경에서는 다음이 허용된다.</p>
<ul>
<li>응답 시간이 길어도 괜찮음</li>
<li>가끔 OOM이 발생해도 재시작 가능</li>
<li>사용자는 한 명</li>
</ul>
<p>하지만 서빙 환경에서는 전부 장애다.</p>
<ul>
<li>latency</li>
<li>throughput</li>
<li>동시 요청 처리</li>
<li>헬스 체크</li>
<li>메트릭 수집</li>
<li>배포 전략</li>
</ul>
<p>이 영역부터는 모델 성능이 아니라 <strong>시스템 설계 문제</strong>가 된다.</p>
<p>그래서 단순 실행 엔진인 llama.cpp와,
서빙 시스템인 vLLM/TGI는 같은 “LLM 도구”로 묶을 수 없다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[서빙 이전에 반드시 검증해야 할 것들, 그리고 로컬 튜닝 실패 패턴]]></title>
            <link>https://velog.io/@yujin_jeong/%EC%84%9C%EB%B9%99-%EC%9D%B4%EC%A0%84%EC%97%90-%EB%B0%98%EB%93%9C%EC%8B%9C-%EA%B2%80%EC%A6%9D%ED%95%B4%EC%95%BC-%ED%95%A0-%EA%B2%83%EB%93%A4-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EB%A1%9C%EC%BB%AC-%ED%8A%9C%EB%8B%9D-%EC%8B%A4%ED%8C%A8-%ED%8C%A8%ED%84%B4</link>
            <guid>https://velog.io/@yujin_jeong/%EC%84%9C%EB%B9%99-%EC%9D%B4%EC%A0%84%EC%97%90-%EB%B0%98%EB%93%9C%EC%8B%9C-%EA%B2%80%EC%A6%9D%ED%95%B4%EC%95%BC-%ED%95%A0-%EA%B2%83%EB%93%A4-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EB%A1%9C%EC%BB%AC-%ED%8A%9C%EB%8B%9D-%EC%8B%A4%ED%8C%A8-%ED%8C%A8%ED%84%B4</guid>
            <pubDate>Mon, 19 Jan 2026 06:41:23 GMT</pubDate>
            <description><![CDATA[<p>앞선 글이 “왜 도구가 다른가”를 정리했다면, 이 글은 <strong>실제로 로컬에서 손을 더럽히며 겪었던 판단 기준과 실패 패턴</strong>을 정리한 기록이다. 서빙 단계에서 문제가 터지기 전에, 로컬 환경에서 반드시 확인해야 할 지점들이 있다.</p>
<h3 id="서빙-이전에-반드시-검증해야-할-것들">서빙 이전에 반드시 검증해야 할 것들</h3>
<h4 id="1-모델-출력의-안정성">1. 모델 출력의 안정성</h4>
<p>가장 먼저 확인해야 할 것은 동일 입력에 대한 출력 안정성이다.</p>
<ul>
<li>temperature를 낮췄을 때도 출력이 크게 흔들리는지</li>
<li>system prompt 고정 시 응답 톤이 유지되는지</li>
<li>context length 증가 시 갑작스러운 붕괴가 발생하지 않는지</li>
</ul>
<p>이 단계에서 불안정한 모델은 서빙 환경에서 동시 요청과 배치 처리가 들어오면 더 쉽게 무너진다. 많은 서빙 이슈가 실제로는 <strong>모델 출력 불안정</strong>에서 시작된다.</p>
<h4 id="2-context-길이와-메모리-사용-패턴">2. Context 길이와 메모리 사용 패턴</h4>
<p>로컬 실험에서는 단일 요청만 보는 경우가 많다. 하지만 서비스 환경에서는 다음이 동시에 발생한다.</p>
<ul>
<li>긴 system prompt</li>
<li>누적되는 대화 히스토리</li>
<li>KV cache 증가</li>
</ul>
<p>반드시 확인해야 할 항목은 다음과 같다.</p>
<ul>
<li><code>n_ctx</code> 증가에 따른 메모리 사용량 변화</li>
<li>응답 속도가 급격히 느려지는 지점</li>
<li>context overflow 발생 시 모델의 반응</li>
</ul>
<p>이를 모른 채 서빙으로 넘어가면, “트래픽 증가”가 아니라 <strong>메모리 구조 한계</strong>가 병목이 된다.</p>
<h4 id="3-프롬프트-구조의-재사용-가능성">3. 프롬프트 구조의 재사용 가능성</h4>
<p>서빙 환경에서는 대부분 system prompt가 고정된다. 많은 요청이 동일한 prefix를 공유한다는 의미다.</p>
<ul>
<li>system / user / assistant 역할 구분의 명확성</li>
<li>과도하게 긴 system prompt 여부</li>
<li>instruction 간 충돌 여부</li>
</ul>
<p>이 구조가 정리되지 않으면 batching이나 KV cache 최적화의 이점을 제대로 얻기 어렵다.</p>
<h4 id="4-느리지만-된다와-서비스-가능의-차이">4. “느리지만 된다”와 “서비스 가능”의 차이</h4>
<p>로컬 환경에서는 다음이 허용된다.</p>
<ul>
<li>응답 시간 20~30초</li>
<li>occasional OOM</li>
<li>간헐적 hallucination</li>
</ul>
<p>서빙 환경에서는 모두 장애다.
서빙 이전에 스스로에게 물어봐야 한다.</p>
<blockquote>
<p>이 상태를 외부 사용자에게 그대로 노출해도 되는가</p>
</blockquote>
<p>이에 자신 있게 답할 수 있을 때만 서빙 프레임워크로 넘어가는 것이 맞다.</p>
<h3 id="로컬-llm-튜닝-실험에서-반복한-실패-패턴">로컬 LLM 튜닝 실험에서 반복한 실패 패턴</h3>
<h4 id="1-모델-크기로-모든-걸-해결하려-했던-시기">1. 모델 크기로 모든 걸 해결하려 했던 시기</h4>
<p>가장 흔한 실패다.</p>
<ul>
<li>응답이 애매하다 → 모델을 키운다</li>
<li>추론이 어색하다 → 더 큰 모델을 찾는다</li>
</ul>
<p>하지만 실제 원인은 다음인 경우가 많았다.</p>
<ul>
<li>프롬프트 구조의 불명확함</li>
<li>instruction tuning이 안 된 베이스 모델</li>
<li>context 설정 오류</li>
</ul>
<p>모델 크기는 마지막 레버리지이지, 문제 해결의 출발점이 아니었다.</p>
<h4 id="2-양자화를-무조건-성능-하락으로-봤던-오해">2. 양자화를 무조건 성능 하락으로 봤던 오해</h4>
<p>초기에는 Q4, Q5 모델 사용이 불안했다.
그러나 실제로는 다음과 같은 특성이 보였다.</p>
<ul>
<li>instruction-following 능력은 큰 차이가 없음</li>
<li>차이는 주로 미세한 표현력에서 발생</li>
<li>메모리 여유로 context를 늘릴 수 있음</li>
</ul>
<p>로컬 실험 단계에서는 양자화 모델이 오히려 생산적이었다.</p>
<h4 id="3-튜닝과-프롬프트를-구분하지-못했던-시기">3. 튜닝과 프롬프트를 구분하지 못했던 시기</h4>
<p>한동안 “튜닝”이라고 부르던 작업의 대부분은 프롬프트 조정이었다.</p>
<ul>
<li>출력 불만족 → prompt 수정</li>
<li>톤 불안정 → system message 추가</li>
<li>태스크 실패 → 예시 추가</li>
</ul>
<p>프롬프트는 입력 제어이고, 튜닝은 모델 분포 자체를 바꾸는 작업이다. 이 구분이 생긴 이후에야 판단이 명확해졌다.</p>
<h4 id="4-로컬-성공을-서비스-성공으로-착각한-지점">4. 로컬 성공을 서비스 성공으로 착각한 지점</h4>
<p>로컬에서 잘 동작하던 모델이 서비스 환경에서 쉽게 깨지는 이유는 명확하다.</p>
<ul>
<li>동시 요청</li>
<li>긴 세션</li>
<li>사용자 입력의 다양성</li>
</ul>
<p>로컬 실험의 목적은 “잘 되는 예시”를 찾는 것이 아니라, <strong>어디서 깨지는지 확인하는 과정</strong>에 가깝다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[llama.cpp vs vLLM/TGI
같은 LLM 실행인데 왜 완전히 다른 선택인가?]]></title>
            <link>https://velog.io/@yujin_jeong/llama.cpp-vs-vLLMTGI%EA%B0%99%EC%9D%80-LLM-%EC%8B%A4%ED%96%89%EC%9D%B8%EB%8D%B0-%EC%99%9C-%EC%99%84%EC%A0%84%ED%9E%88-%EB%8B%A4%EB%A5%B8-%EC%84%A0%ED%83%9D%EC%9D%B8%EA%B0%80</link>
            <guid>https://velog.io/@yujin_jeong/llama.cpp-vs-vLLMTGI%EA%B0%99%EC%9D%80-LLM-%EC%8B%A4%ED%96%89%EC%9D%B8%EB%8D%B0-%EC%99%9C-%EC%99%84%EC%A0%84%ED%9E%88-%EB%8B%A4%EB%A5%B8-%EC%84%A0%ED%83%9D%EC%9D%B8%EA%B0%80</guid>
            <pubDate>Mon, 19 Jan 2026 06:40:35 GMT</pubDate>
            <description><![CDATA[<p>로컬 모델을 활용해 챗봇을 구성하는 방식은 이제 더 이상 낯선 주제가 아니다.
<code>llama.cpp</code>, <code>GGUF</code>, <code>quantization</code>, <code>vLLM</code> 같은 키워드는 이미 많은 글과 자료에서 반복적으로 다뤄지고 있다. 그럼에도 불구하고 이 글을 작성한 이유는, 해당 기술들을 <strong>결과가 아닌 과정의 관점에서 한 번 정리해두고 싶었기 때문</strong>이다.</p>
<p>특히 GPU 환경이나 모델 서빙 구조에 대한 이해가 거의 없는 상태에서 시작해, 로컬 추론과 서비스용 추론의 차이를 체감하기까지의 흐름은 이후 기술 선택에 중요한 기준이 되었다.</p>
<h3 id="초기-목표-로컬에서-모델이-돌아가는지-확인하기">초기 목표 “로컬에서 모델이 돌아가는지 확인하기”</h3>
<p>외부 API나 클라우드 환경 없이, 개인 노트북에서 LLM을 직접 실행할 수 있는지 확인하는 것이 중요했다. GPU, VRAM, 연산 정밀도 같은 개념보다는 <strong>“실제로 문장이 생성되는가”</strong>가 중요했다. 일단 동작해야 이후의 학습과 변형을 논할 수 있었다.</p>
<p>개인 학습 과정에서 AWS GPU를 지속적으로 사용하기는 현실적으로 어려웠고, 그 결과 CPU 환경에서도 동작 가능한 추론 엔진을 찾게 되었다.</p>
<h3 id="llamacpp를-통한-로컬-추론-경험">llama.cpp를 통한 로컬 추론 경험</h3>
<p><code>llama.cpp</code>는 C++ 기반의 경량 추론 엔진으로, 로컬 환경에서 LLM을 실행하는 데 최적화되어 있다. GGUF 포맷과 양자화 모델을 사용하면 GPU 없이도 비교적 큰 모델을 실행할 수 있다는 점이 인상적이었다.
<img src="https://velog.velcdn.com/images/yujin_jeong/post/a18308b9-1733-4827-9944-dda2d9e633cd/image.png" alt=""></p>
<blockquote>
<ul>
<li>RAM 16GB 환경에서도 7B 모델 실행 가능</li>
<li>CPU만으로도 토큰 생성 확인</li>
<li>실행 구조가 단순해 디버깅 부담이 적음</li>
</ul>
</blockquote>
<p>이 시점에서의 결론은 명확했다.
<code>llama.cpp</code>는 서비스 프레임워크라기보다는 <strong>로컬 추론을 위한 실행 엔진</strong>에 가깝다. 실험과 검증 단계에 매우 적합한 도구였다.</p>
<h3 id="gguf와-양자화에-대한-이해">GGUF와 양자화에 대한 이해</h3>
<p>초기에는 단순히 파일 이름만 보고 모델을 선택했다.</p>
<pre><code>llama-3-8b.Q4_K_M.gguf</code></pre><p>점차 GGUF 포맷과 양자화 방식의 의미를 이해하면서 모델 선택 기준이 바뀌었다.</p>
<ul>
<li><strong>GGUF</strong>: llama.cpp에 맞춰 메타데이터와 토크나이저 정보를 포함한 포맷</li>
<li><strong>Q4 / Q5 / Q8</strong>: 가중치 표현 비트 수</li>
<li>비트 수가 낮을수록 메모리 사용량은 줄지만 품질 손실 발생</li>
</ul>
<p>이해가 쌓이면서 “가장 큰 모델”이 아니라 <strong>환경과 목적에 맞는 모델</strong>을 선택하게 되었다. 이때부터 로컬 챗봇은 단순한 데모를 넘어 실험 도구로 기능하기 시작했다.</p>
<h3 id="fastapi로-감싼-이후-드러난-한계">FastAPI로 감싼 이후 드러난 한계</h3>
<p>모델 실행이 안정화된 이후, 이를 API 형태로 제공할 수 있는지 실험했다.
<code>llama.cpp</code>를 백엔드 엔진으로 두고 FastAPI로 간단한 엔드포인트를 구성하는 방식은 소규모 테스트 환경에서는 충분히 동작했다.</p>
<p>그러나 <strong>동시 요청이 발생하는 순간</strong> 구조적 한계가 드러났다.</p>
<ul>
<li>요청이 직렬로 처리됨</li>
<li>앞선 요청의 응답 시간이 길어질수록 전체 지연 증가</li>
<li>큐 적체 시 체감 latency 급증</li>
</ul>
<p>이는 구현상의 문제가 아니라, <strong>설계 목적의 차이</strong>에 가까웠다.</p>
<h3 id="vllm을-통해-인식한-서빙의-개념">vLLM을 통해 인식한 “서빙”의 개념</h3>
<p><img src="https://velog.velcdn.com/images/yujin_jeong/post/bb185c9e-e633-4707-b50e-cbe889a9fb9a/image.png" alt=""></p>
<p>vLLM을 살펴보며 관점이 바뀌었다. vLLM은 단순히 모델을 실행하는 도구가 아니라, <strong>여러 요청을 동시에 처리하는 것을 전제로 한 서빙 시스템</strong>이었다.</p>
<ul>
<li>continuous batching을 통한 GPU 활용 극대화</li>
<li>KV cache 최적화로 prefix 중복 계산 제거</li>
<li>동시 요청 환경에서도 안정적인 latency 유지</li>
</ul>
<p>이 지점에서 두 접근의 차이는 명확해졌다.
전자는 <strong>개인 또는 소규모 실험에 최적화된 추론 엔진</strong>, 후자는 <strong>프로덕션 환경을 전제로 한 추론 서비스 시스템</strong>이다.
<img src="https://velog.velcdn.com/images/yujin_jeong/post/9946cb3e-0e5a-4060-93fe-8a9e66bd70a0/image.png" alt=""></p>
<table>
<thead>
<tr>
<th>구분</th>
<th>llama.cpp</th>
<th>vLLM / TGI</th>
</tr>
</thead>
<tbody><tr>
<td>정체성</td>
<td>로컬 추론 엔진</td>
<td>프로덕션 서빙 시스템</td>
</tr>
<tr>
<td>목표</td>
<td>저비용, 저메모리 실행</td>
<td>동시성, 처리량, 안정성</td>
</tr>
<tr>
<td>강점</td>
<td>CPU 가능, GGUF 양자화</td>
<td>continuous batching, KV 최적화</td>
</tr>
<tr>
<td>약점</td>
<td>동시 요청에 취약</td>
<td>GPU 전제, 운영 복잡도↑</td>
</tr>
</tbody></table>
<h4 id="정리">정리</h4>
<ul>
<li>로컬 실험, 개인용 챗봇, 오프라인 실행 → <code>llama.cpp</code></li>
<li>다수 사용자, API 제공, SLA 요구 → <code>vLLM</code> 또는 <code>TGI</code></li>
<li>현실적인 접근은 <strong>llama.cpp로 시작해 vLLM으로 전환</strong></li>
</ul>
<p>기술 선택의 문제가 아니라, <strong>문제의 규모와 성격에 따른 선택</strong>에 가깝다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[HPC(고성능 클러스터) 기본 구조]]></title>
            <link>https://velog.io/@yujin_jeong/HPC%EA%B3%A0%EC%84%B1%EB%8A%A5-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-%EA%B8%B0%EB%B3%B8-%EA%B5%AC%EC%A1%B0</link>
            <guid>https://velog.io/@yujin_jeong/HPC%EA%B3%A0%EC%84%B1%EB%8A%A5-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-%EA%B8%B0%EB%B3%B8-%EA%B5%AC%EC%A1%B0</guid>
            <pubDate>Thu, 13 Nov 2025 05:07:21 GMT</pubDate>
            <description><![CDATA[<p>(작성자는 비전공자이며, 알고 싶은대로 공부하는 경향이 있어서 고치려고 노력중이고,
틀린게 있으면 시원하게 지적해주세요)</p>
<h4 id="자-이걸-쓰는-이유">자 이걸 쓰는 이유!</h4>
<p>로컬에서만 작업하면 본인 편한대로 파일 구조(Directory)를 짜도 되지만, 서버에서 학습을 시킬 생각이라면 그 구조에 맞게 구조를 짤 수 있으니까!</p>
<p>귀찮으면 Cursor한테</p>
<blockquote>
<p>ssh 접속해서 클러스터 구조 파악하고 디렉토리 짜줘.</p>
</blockquote>
<p>하면 되긴하지만 구조를 이해해봅시다.</p>
<h3 id="hpc고성능-클러스터-기본-구조와-auroraariel-서버-디렉토리-안내">HPC(고성능 클러스터) 기본 구조와 Aurora(Ariel) 서버 디렉토리 안내</h3>
<p><a href="https://www.hpc.iastate.edu/guides/introduction-to-hpc-clusters/what-is-an-hpc-cluster">https://www.hpc.iastate.edu/guides/introduction-to-hpc-clusters/what-is-an-hpc-cluster</a></p>
<p><img src="https://velog.velcdn.com/images/yujin_jeong/post/929902e6-6eba-47c0-9750-ea6734a72d7e/image.png" alt=""></p>
<p>고성능 연산 클러스터(HPC, High Performance Computing)는 여러 대의 독립된 컴퓨팅 노드(node)를 하나의 시스템처럼 묶어 대규모 연산을 수행하도록 설계된 구조입니다. 일반적으로 로그인 노드(Login Node), 계산 노드(Compute Node, GPU/CPU), 관리 노드(Management Node), 그리고 공유 스토리지(Storage Node) 등으로 구성되며, <strong>사용자는 로그인 노드에 SSH로 접속하여 코드를 업로드하고, Slurm과 같은 스케줄러를 통해 계산 노드에 작업(Job)을 제출</strong>합니다. 모든 노드는 고속 인터커넥트(InfiniBand 등)로 연결되어 있어 분산 학습, 병렬 연산, 대규모 데이터 입출력 작업을 효율적으로 처리할 수 있습니다. 이러한 구조를 통해 HPC는 단일 컴퓨터로는 수행하기 어려운 대규모 AI 학습, 시뮬레이션, 과학 계산을 안정적으로 실행할 수 있는 환경을 제공합니다.
<img src="https://velog.velcdn.com/images/yujin_jeong/post/eb05c143-1152-4e7a-a304-a5a0d2711d6b/image.png" alt=""></p>
<p>고성능 연산을 위해 제공되는 HPC 클러스터는 로그인 노드, 계산 노드(GPU·CPU), 공유 스토리지 등으로 구성된 분산 컴퓨팅 환경입니다. 사용자는 SSH로 로그인 노드에 접속한 뒤, Slurm을 이용하여 작업을 제출하거나 SFTP로 데이터를 전송하며 대규모 연산을 수행합니다. 이러한 클러스터는 일반적으로 홈 디렉토리(<code>/home/{your_id}</code>) 와 작업 디렉토리(<code>/data/{your_id}</code>) 를 분리해 관리하며, <strong>홈은 용량이 작고 백업이 지원되는 반면, <code>/data</code> 영역은 연구 프로젝트·데이터셋·모델을 저장하는 실질적 작업 공간으로 활용</strong>됩니다. (정확히 말하자면, ※ <code>/home</code>은 Slurm 작업 실행 시 I/O 속도가 느릴 수 있으므로, 실제 학습 데이터나 체크포인트 저장은 <code>/data</code> 영역을 사용해야 합니다.
)
대부분의 HPC가 <code>/home</code>, <code>/data</code>, <code>/scratch</code> 계층을 갖추지만, 우리 학교의 Aurora/Ariel 계열 서버는 <code>/home</code>과 <code>/data</code>를 중심으로 구성되어 있으며 <code>/scratch</code> 또는 <code>/local_datasets</code>는 GPU 노드 내부에서만 제공될 가능성이 있습니다(불확실). 따라서 SSH 접속 후 다음 명령어를 실행하여 실제 영역을 확인하는 것이 중요합니다.</p>
<hr>
<pre><code class="language-bash"># 홈 디렉토리 확인
echo $HOME
pwd

# 마운트된 디스크 확인
df -h

# 대표 디렉토리 존재 여부 검사
ls -la /home 2&gt;/dev/null || echo &quot;/home 없음&quot;
ls -la /data 2&gt;/dev/null || echo &quot;/data 없음&quot;
ls -la /scratch 2&gt;/dev/null || echo &quot;/scratch 없음&quot;
ls -la /local_datasets 2&gt;/dev/null || echo &quot;/local_datasets 없음&quot;

# 환경 변수에서 경로 힌트 찾기
env | grep -i home
env | grep -i data
env | grep -i scratch</code></pre>
<p>이 명령어들을 실행하면 Ariel 서버의 스토리지 구조가 <code>/home/{your_id}</code>와 <code>/data/{your_id}</code> 중심인지, 혹은 노드별 임시 스토리지(<code>/scratch</code>, <code>/local_datasets</code>)가 존재하는지를 정확히 확인할 수 있습니다.</p>
<hr>
<p>자자,,, 위는 그냥 디렉토리 잘 나눠라! 하는 잡도리였고요.
HPC가 뭐냐? 뭔...뭔...뭐냐!! 할겁니다... <del>저도 몰랐음</del></p>
<p>이제 HPC 자세히 알려줄건데 이해 안되면 넘겨도 됨.</p>
<h2 id="클러스터-구조를-이해하기-위한-기본-프레임">클러스터 구조를 이해하기 위한 기본 프레임</h2>
<p>아래는 <strong>“클러스터가 무엇인가?”를 처음 보는 사람도 이해할 수 있게</strong>,
몇 개 문단으로 간단하지만 정확하게 정리한 설명이다.</p>
<h3 id="🔵-클러스터cluster란-무엇인가">🔵 <strong>클러스터(Cluster)란 무엇인가?</strong></h3>
<p><img src="https://velog.velcdn.com/images/yujin_jeong/post/e18bbd8e-52b4-4961-b2ff-61b3a1de1b18/image.png" alt=""></p>
<blockquote>
<ul>
<li><strong>User → Login Node → Scheduler → Compute Nodes</strong>
사용자는 로그인 노드를 통해 접속하고, 모든 작업은 스케줄러가 적절한 노드에 배치한다.</li>
</ul>
</blockquote>
<ul>
<li><strong>Compute Nodes는 스토리지와 분산 I/O를 수행</strong> (Compute Node는 연산을 수행하고, 데이터는 공유 스토리지(/data 등)에서 읽고 쓴다.)
데이터셋, 중간 산출물, 체크포인트는 모두 Storage Cluster에 저장된다.</li>
<li><strong>Control Plane(Master/Manager)</strong>
전체 클러스터의 상태, 스케줄링, 스토리지 메타데이터, 노드 헬스 체크를 담당한다.</li>
<li><strong>Storage Cluster는 Ceph/Lustre/HDFS 등으로 구성</strong>
HPC/빅데이터/K8s 모두 Storage Layer만 바뀌고 구조는 동일하다. (<del>사실 큰 틀에서 “여러 노드를 묶어서 스케줄링한다”는 공통점은 있지만,
HPC/빅데이터/K8s는 목적과 실행 방식이 달라 구조가 완전히 같진 않다.</del>)</li>
</ul>
<blockquote>
<p>※ 로그인 노드(Login Node)는 계산용이 아니라 명령 제출·환경 설정만 하는 곳입니다.<br>여기서 직접 학습을 돌리면 다른 사용자의 자원을 침해하게 되므로 반드시 Slurm으로 Job을 제출해야 합니다.</p>
</blockquote>
<p>쿠버네티스 클러스터 생각하면 어렵지 않아요! 근데 굳이 또... 서버를 써보기 위해 꼭 당장 알아야한다!의 개념은 아니라고 생각해서 간단히 보고 넘기셔도 될 것 같습니다. 
<img src="https://velog.velcdn.com/images/yujin_jeong/post/5daee8dc-2003-4551-a4a7-1aa64dea5d1b/image.png" alt=""></p>
<p><a href="https://www.hanbit.co.kr/channel/view.html?cmscode=CMS5675948724">이미지 출처</a></p>
<p>클러스터(Cluster)는 <strong>여러 대의 컴퓨터(노드, node)를 하나의 큰 컴퓨터처럼 묶어 사용하는 시스템 구조</strong>입니다. 단일 서버로는 처리하기 어려운 큰 연산·데이터·서비스를 분산해서 처리하기 위해 만들어졌으며, 사용자는 마치 하나의 거대한 컴퓨터처럼 접근하지만 실제 내부에서는 수십~수백 대의 노드가 협력해 일하는 거죠!</p>
<p>클러스터의 핵심은 <strong>분산(Distributed)</strong> 입니다. 하나의 서버가 고장나거나 과부하 걸려도 전체가 중단되지 않도록 여러 노드가 역할을 나눠 수행합니다. 예를 들어 어떤 노드는 계산만 담당하고(Compute Node), 다른 노드는 데이터 저장을 담당(NFS, Ceph, Lustre), 또 다른 노드는 사용자의 명령이나 작업 배치를 관리(Login Node, Scheduler Node)합니다. 이렇게 역할을 분리해 운영하면 성능·확장성·안정성 모두 크게 향상되겠죠?</p>
<p>또한 대부분의 클러스터는 <strong>스케줄러(Scheduler)</strong> 라는 시스템을 통해 운영됩니다. 스케줄러는 사용자가 제출한 연산 작업(job)을 어떤 노드에 배치할지 자동으로 결정하고, 동시에 여러 사용자가 자원을 공유하도록 관리한다. HPC에서는 Slurm, 데이터 처리에서는 YARN, 서비스 운영에서는 Kubernetes 등이 대표적입니다. 이 스케줄링 시스템 덕분에 수십 명의 사용자가 하나의 클러스터를 공유해도 충돌이나 과부하 없이 효율적으로 사용이 가능합니다. </p>
<p>마지막으로, 클러스터는 목적에 따라 다양한 형태(HPC, HTC, Big Data, Storage, Kubernetes 등)로 나뉜다. 어떤 것은 <strong>대규모 연산</strong>이 목적이고(HPC), 어떤 것은 <strong>수백만 개의 작은 작업 처리</strong>가 목표이며(HTC), 어떤 것은 <strong>대규모 데이터 저장·가공</strong>이 중심이거나(Big Data, Storage), 어떤 것은 <strong>웹 서비스나 모델 서빙 운영</strong>에 최적화돼 있다(K8s). 목적에 따라 구조·네트워크·스토리지·스케줄링 방식이 달라지기 때문에, 클러스터는 “큰 컴퓨터”인 동시에 “특정 목적을 위해 조직된 분산 시스템”이라고 이해하면 된답니다.</p>
<p>다시 다시 말하지만
<img src="https://velog.velcdn.com/images/yujin_jeong/post/ad787e68-2a3d-4e0a-ad02-e04e06287553/image.png" alt="">
Aurora나 Ariel은 대강 이런 구조인 것!</p>
<p>그리고 MLOps에서 자주 쓰는 구조 중에 하나인데 이것도 알아두면 좋지 않을까?요~
<img src="https://velog.velcdn.com/images/yujin_jeong/post/3f647c21-ff86-471d-aa83-47517bd1c340/image.png" alt=""></p>
<p>Ceph 더 말하고 싶은데 지겨워할 것 같아서....
<a href="https://computing-jhson.tistory.com/112#google_vignette">Ceph</a>
잘 정리된 티스토리 공유합니다.</p>
<hr>
<p><del>여기는 뛰어넘으셔도 됩니다</del></p>
<p>일단 클러스터는 항상 “<strong>유형(Type)”—“구현체(Instance)”—“스케줄링”—“스토리지” 네 가지 관점</strong>으로 나누어 이해해야 한다는 것입니다.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>분류기준</th>
<th>Aurora/Ariel</th>
</tr>
</thead>
<tbody><tr>
<td><strong>유형(Type)</strong></td>
<td>아키텍처적 목적</td>
<td>HPC Cluster</td>
</tr>
<tr>
<td><strong>구현체(Instance)</strong></td>
<td>특정 설치 구성</td>
<td>aurora-g1/g2/g3, ariel-g1…</td>
</tr>
<tr>
<td><strong>자원 스펙</strong></td>
<td>GPU/CPU/RAM/스토리지</td>
<td>서버마다 상이</td>
</tr>
<tr>
<td><strong>스케줄링</strong></td>
<td>Job Scheduler 기반</td>
<td>Slurm 기반, 동일</td>
</tr>
<tr>
<td><strong>스토리지</strong></td>
<td>분산 파일 시스템 구조</td>
<td>Ceph/Lustre/NFS 중 구현에 따라 상이</td>
</tr>
</tbody></table>
<p>우리의 Auroa/Ariel기준으로 오른쪽에 설명했구요.</p>
<h3 id="유형-type">유형 TYPE</h3>
<p><strong>1) HPC Cluster (High Performance Computing)
*<em>대규모 병렬 연산·MPI·대규모 분산 학습
*</em>2) HTC Cluster (High Throughput Computing)</strong>
수백만 개의 작은 job 처리, Condor/Ray 기반 워크로드
<strong>3) Big Data Cluster (Data-Parallel Cluster)</strong>
Hadoop/Spark 기반 파일 병렬 처리
<strong>4) Storage Cluster (Distributed Storage Cluster)</strong>
Ceph, Lustre, HDFS 같은 스토리지 전용 분산 시스템
<strong>5) Kubernetes Cluster (Cloud Native Cluster)</strong>
컨테이너 기반 MLOps·DevOps·Inference/Service 운영
*<em>6) Hybrid / Multi-Cluster Architecture
*</em>HPC + Spark + K8s + Storage 조합(현대 AI 엔지니어링 표준)</p>
<p>실제 운영 환경에서는 아래와 같은 다양한 유형의 클러스터가 존재합니다.</p>
<p>각각이 좀 궁금하다면</p>
<h4 id="hpc-cluster-high-performance-computing"><strong>HPC Cluster (High Performance Computing)</strong></h4>
<p><img src="https://velog.velcdn.com/images/yujin_jeong/post/d0c7f48e-a51f-4db8-8f49-22154bfd293b/image.png" alt=""></p>
<p><a href="https://www.weka.io/learn/guide/hpc/what-are-hpc-and-hpc-clusters/">HPC</a></p>
<blockquote>
<p>대규모 수치해석, 과학 계산, 딥러닝 학습을 위한 전통적 형태의 배치 기반 클러스터입니다.</p>
</blockquote>
<ul>
<li>예: Slurm 기반 GPU 노드, 슈퍼컴퓨터 센터</li>
</ul>
<p>HPC 클러스터는 대규모 연산을 빠르게 처리하기 위해 설계된 배치 기반 환경이다. 슬럼(Slurm)·PBS 같은 스케줄러가 작업을 통제하며, 하나의 job이 수시간<del>수일 동안 GPU/CPU 수십</del>수백 개를 독점하는 구조를 전제로 한다. 노드 간 네트워크는 InfiniBand 등 고대역폭·저지연 인터커넥트를 사용해 MPI 기반 수치해석, 대규모 딥러닝과 같은 노드 간 파라미터 통신량이 많은 작업을 최적화한다. HPC의 강점은 계산 성능과 안정성이다. 반면, 서비스 운영에는 맞지 않으며 컨테이너 생태계와의 통합이 부족한 경우가 많다. 연구실·국가 슈퍼컴퓨터에서 주로 사용하는 이유도 “인터랙티브성”이 아니라 “정해진 시간 동안 최대 성능을 뽑아내는 구조”이기 때문이다.</p>
<h4 id="htc-cluster-high-throughput-computing"><strong>HTC Cluster (High Throughput Computing)</strong></h4>
<p><img src="https://velog.velcdn.com/images/yujin_jeong/post/fc9b148e-7a58-4d12-9570-fd45bee21e7c/image.png" alt="">
<a href="https://rescale.com/documentation/video-tutorials/rescale-advanced-videos/high-throughput-computing/">HTC</a></p>
<blockquote>
<p>작업 하나가 크지 않지만 수천~수만 개의 작은 작업을 처리해야 할 때 사용합니다. </p>
</blockquote>
<ul>
<li>예: 단백질 스크리닝, 로그 분석, 수백만 건의 짧은 inference job
HTC 클러스터는 한 번의 연산은 작지만, 수만~수백만 개의 job을 순차·병렬로 계속 처리해야 하는 워크로드에 최적화되어 있다. 대표적으로 단백질 스크리닝, 로그 이벤트 분석, 대량 웹스케일 inference처럼 “작은 작업을 끊임없이 흘려보내는 구조”이다. 이 때문에 스케줄러는 throughput 기반으로 설계되며, 작업 하나가 길게 리소스를 독점하는 것을 막고 큐 대기 시간을 최소화하는 전략을 쓴다. HPC와 달리 고성능 네트워크가 필수는 아니지만, <strong>잡 디스패치 속도(Job dispatch rate)</strong> 와 컨테이너 경량 실행이 중요하다. 많은 기관이 HTCondor, Ray, Airflow+K8s 조합을 HTC 용도로 사용하며, 대규모 MLOps 파이프라인의 “일괄 inference farm”으로도 쓰인다.</li>
</ul>
<h4 id="big-data-cluster"><strong>Big Data Cluster</strong></h4>
<p><img src="https://velog.velcdn.com/images/yujin_jeong/post/ae3dd090-a3a7-46e4-842e-e005c4886e66/image.png" alt=""></p>
<p><a href="https://docs.oracle.com/cd/E57471_01/bigData.100/admin_bdd/src/cadm_cluster_diagram.html">빅데이터 클러스터</a></p>
<blockquote>
<p>Hadoop/Spark 기반의 데이터 병렬처리 클러스터입니다.</p>
</blockquote>
<ul>
<li>초대용량 데이터 파이프라인, ETL, 대규모 통계 처리</li>
</ul>
<p>빅데이터 클러스터는 Hadoop·Spark 같은 데이터 병렬 처리 프레임워크를 안정적으로 실행하기 위한 전용 구조이다. 계산보다는 저장된 파일을 노드 전체로 확산시키고, 이를 병렬 map/reduce 형태로 처리하는 것이 중심이므로 <strong>HDFS·S3와의 데이터 지역성(Locality)</strong>이 성능을 좌우한다. Spark는 메모리 기반 분산 계산을 지원하지만, GPU를 사용하는 HPC나 딥러닝 환경과는 목적이 전혀 다르다. 빅데이터 클러스터의 강점은 수백 TB~수 PB 단위 데이터를 “실시간은 아니지만 빠르게” 분석하는 능력이며, ETL·로그 분석·Feature Store 구축 등 AI 파이프라인의 전처리/후처리 영역을 담당한다. 최근 기업들은 Spark + Kubernetes 구조로 이동하며 관리 비용을 낮추는 추세다.</p>
<h4 id="storage-cluster"><strong>Storage Cluster</strong></h4>
<p><img src="https://velog.velcdn.com/images/yujin_jeong/post/0e05bddf-6e1f-4d7d-872e-15163c164b7f/image.png" alt=""></p>
<p><a href="https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/jj822937%28v=ws.11%29">스토리지 클러스터</a>
<del>종류디지게많음</del></p>
<blockquote>
<p>파일 및 객체 스토리지를 분산 형태로 제공하는 전용 클러스터입니다.</p>
</blockquote>
<ul>
<li>Ceph, Lustre, GlusterFS 등이 대표적</li>
<li>HPC의 <code>/data</code> 영역이 이런 스토리지 클러스터 위에서 동작하는 경우가 많습니다.</li>
</ul>
<blockquote>
<p>※ Ceph는 확장성과 범용성이 좋지만, Lustre는 HPC용으로 더 빠릅니다.  </p>
</blockquote>
<ul>
<li>Aurora/Ariel 같은 환경은 Lustre 또는 NFS 계열 스토리지를 사용하는 경우가 많습니다.</li>
</ul>
<p>스토리지 클러스터는 계산이 아니라 데이터 저장과 I/O 처리에 최적화된 독립적인 분산 파일 시스템이다. HPC에서 사용하는 /data, /scratch가 대부분 이러한 시스템 위에서 돌아간다. Lustre는 초고성능 POSIX 파일시스템을 제공해 HPC에 적합하고, Ceph는 객체·블록·파일을 모두 제공해 범용성과 확장성이 뛰어나다. 스토리지 클러스터를 따로 두는 이유는 단순 저장 때문이 아니라, <strong>노드가 수십~수백 대 이상인 클러스터에서 파일 일관성, 확장성, 결함 허용성(fault tolerance)</strong>을 확보하기 위함이다. 또한 GPU 학습에서도 대규모 이미지/비디오 셔플 시 I/O 병목이 전체 학습 속도를 결정하므로 스토리지 클러스터 설계가 AI 학습 성능과 직결된다.</p>
<h4 id="kubernetes-cluster-cloud-native-cluster"><strong>Kubernetes Cluster (Cloud Native Cluster)</strong></h4>
<p><img src="https://velog.velcdn.com/images/yujin_jeong/post/75ed45f3-8e10-479d-9f91-5e7272b08950/image.png" alt=""></p>
<p><a href="https://kubernetes.io/docs/concepts/overview/components/">쿠버네티스 클러스터</a></p>
<blockquote>
<p>컨테이너 기반 AI·웹서비스·배포 자동화를 위한 클러스터입니다.</p>
</blockquote>
<ul>
<li>GPU Operator를 설치하면 딥러닝 학습에도 활용 가능</li>
<li>최근 많은 AI팀이 HPC 대신 K8s 기반으로 전환 중</li>
</ul>
<p>Kubernetes 클러스터는 전통적 HPC와 달리 컨테이너 기반의 유연성과 자동화를 중심으로 한다. GPU Operator·NVIDIA Device Plugin 등을 설치하면 딥러닝 학습 노드로도 활용할 수 있고, Inference 서비스·웹서비스·데이터 파이프라인 등 개발부터 운영까지 하나의 플랫폼에서 통합 관리할 수 있다. 또한 Auto-scaling, Canary rollout, 서비스 메쉬, 모니터링 등 배포 자동화 기능이 강력해 “AI 연구 + AI 운영(Serving)”을 같이 하는 팀이 빠르게 도입하고 있다. 다만 노드 간 HPC 수준의 통신 최적화를 보장하지 않기 때문에 대규모 분산 학습에는 한계가 있으며, 대신 MLOps/DevOps 환경에 매우 적합하다.</p>
<h4 id="hybrid-cluster"><strong>Hybrid Cluster</strong></h4>
<p><img src="https://velog.velcdn.com/images/yujin_jeong/post/8b8919b9-908b-4e56-807e-114c3126febd/image.png" alt="">
핏한 이미지가 없어서 GPT로 만들었어용</p>
<blockquote>
<p>HPC + Cloud + K8s + Storage를 결합한 복합 구조입니다.</p>
</blockquote>
<ul>
<li>연구 기관에서 GPU 자원을 내부 HPC로 관리하고</li>
<li>생산 서비스는 Kubernetes에서 운영하는 형태가 대표적</li>
</ul>
<p><a href="https://www.themoonlight.io/ko/review/running-cloud-native-workloads-on-hpc-with-high-performance-kubernetes">Running Cloud-native Workloads on HPC with High-Performance Kubernetes</a>
하이브리드 클러스터는 HPC·Kubernetes·Cloud·Storage를 결합한 형태로, 많은 연구기관·기업들이 실제로 운영하는 구조다. 예를 들어 GPU 연구는 온프레미스 HPC에서 진행하되, 프로토타입 웹서비스는 K8s에서 운영하고, 대규모 데이터는 클라우드 스토리지와 연결하는 방식이다. 이렇게 하면 연구 단계–개발 단계–운영 단계 간 리소스를 단일화할 수 있어 파이프라인 전환 비용이 크게 줄어든다. 또한 클라우드의 탄력성과 내부 HPC의 비용효율을 동시에 얻을 수 있다. 단점은 네트워크·보안·IAM 구조 설계가 복잡해지고, 팀 간 운영 책임 분리가 필요한 점이다. 성숙한 조직일수록 하이브리드 구조로 진화하는 경향이 있다.</p>
<hr>
<h3 id="aurora와-ariel">Aurora와 Ariel</h3>
<p>Aurora와 Ariel은 모두 학교에서 제공하는 고성능 연산(HPC) 서버이지만, 용도와 구성 측면에서 차이가 있습니다. <strong>Aurora는 주로 교육·연구 실습을 위한 학부 중심의 HPC 환경으로, 로그인 노드에서 SSH 접속 후 Slurm을 통해 GPU 노드(aurora-g1, g2, g3 등)를 예약해 사용하는 구조</strong>입니다. 사용자가 직접 <code>/data/{your_id}</code> 경로에 코드를 올리고, GPU 노드에 접속하여 가상환경을 구성한 뒤 배치 작업을 수행하는 방식이 대표적입니다. Ariel은 Aurora와 별도로 운영되는 연구 중심 HPC 환경으로, GPU 세대나 큐 정책, 스토리지 구성 등이 Aurora와 다를 수 있습니다. 각 시스템의 실제 구성은 df -h, sinfo, nvidia-smi 명령으로 확인하는 것이 가장 정확합니다.
보통 계산 성능·저장구조·큐 정책 등이 Aurora와 다를 수 있습니다. 예를 들어 Ariel은 다른 GPU 세대(예: A100, L40S 등)를 포함하거나, <code>/scratch</code>·<code>/local_datasets</code> 같은 고속 임시 스토리지가 제공될 가능성이 있습니다. 또한 사용자 그룹, 사용 정책, 실험 목적(수업·프로젝트·연구)에 따라 큐 설정과 접근 권한이 Aurora와 다르게 관리될 수 있습니다. 요약하면 <strong>Aurora는 교육·실습 중심의 표준 HPC 환경</strong>, <strong>Ariel은 보다 고성능 또는 연구 운영 중심의 확장 HPC 환경</strong>으로 이해할 수 있으며, 실제 스펙과 경로 구조는 SSH 접속 후 <code>df -h</code>, <code>sinfo</code>, <code>nvidia-smi</code> 등을 통해 확인하는 것이 가장 정확합니다.</p>
<hr>
<p><del>여기서부터 다시 보세요</del></p>
<h3 id="아-이제-진짜-딴소리-안하고-돌아와서">아 이제 진짜 딴소리 안하고 돌아와서...</h3>
<p>Aurora와 Ariel은 모두 학교에서 제공하는 고성능 연산 서버(HPC 클러스터)지만, 제공 대상 학과와 운영 목적이 구분되어 있습니다. <strong>Aurora는 컴퓨터공학과·소프트웨어융합학과 등 공학 계열 학생들에게 주로 제공되는 범용 GPU 연산 클러스터</strong>로, 다양한 분야의 실습·과제·연구를 위한 공용 서버 역할을 합니다. 반면 <strong>Ariel은 인공지능학과를 중심으로 제공되는 전용 클러스터로, 딥러닝·머신러닝 실험에 최적화된 GPU 자원이 집중된 환경</strong>이라는 점이 가장 큰 차이입니다. 실제로 공학 계열은 Aurora 접근 권한을 배정받는 경우가 많고, 인공지능학과는 Ariel 계정을 별도로 지급받는 것으로 알려져 있습니다. 두 시스템 모두 SSH·SFTP를 기반으로 사용 방식은 유사하지만, <strong>Aurora는 다양한 과목과 연구 주제를 수용하는 범용 교육용 클러스터</strong>, <strong>Ariel은 AI 실험·모델 학습을 위한 전용 연산 서버</strong>라는 차이가 있으며, 노드 구성·GPU 종류·사용 정책 역시 학과별 요구에 맞추어 다르게 운영되는 편입니다.</p>
<h2 id="결론">결론</h2>
<blockquote>
<p>깃허브 디렉토리 짤 때는 /data, /code, /logs 등 역할별 폴더를 명확히 구분해야 합니다.
이 구조를 그대로 <strong>HPC의 /data/{your_id} 하위에 반영하면, 학습 코드·데이터 관리가 깔끔해지고 Slurm job 실행 시 경로 충돌을 방지</strong>할 수 있습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[국립국어원 말평 도전]]></title>
            <link>https://velog.io/@yujin_jeong/%EA%B5%AD%EB%A6%BD%EA%B5%AD%EC%96%B4%EC%9B%90-%EB%A7%90%ED%8F%89-%EB%8F%84%EC%A0%84</link>
            <guid>https://velog.io/@yujin_jeong/%EA%B5%AD%EB%A6%BD%EA%B5%AD%EC%96%B4%EC%9B%90-%EB%A7%90%ED%8F%89-%EB%8F%84%EC%A0%84</guid>
            <pubDate>Thu, 13 Nov 2025 00:16:06 GMT</pubDate>
            <description><![CDATA[<h4 id="말평-과제란">말평 과제란?</h4>
<p>국립국어원의 <strong>인공지능 말평(Korean Language Intelligence Benchmark)</strong>은 한국어 인공지능 기술의 성능을 객관적으로 평가하기 위해 마련된 국가 공인 벤치마크입니다.
형태소 분석, 품사 태깅, 개체명 인식, 문장 관계 추론, 감정 분석, 질의응답, 문서 요약 등 언어 이해·생성 전반을 포괄하는 20여 개의 과제로 구성되어 있으며, 모든 과제는 국립국어원 세종 말뭉치와 인공지능 학습용 말뭉치를 비롯해 AI Hub·공공기관 협력 데이터 등 다양한 출처의 한국어 자료를 기반으로 구축되었습니다. 참가자는 모델의 결과를 시스템에 제출하여 정확도, F1-score, BLEU, ROUGE 등의 지표로 평가받으며, 이를 통해 학계·산업계의 한국어 AI 모델을 공정하게 비교할 수 있다. 인공지능 말평은 한국어 처리 기술의 품질 향상과 표준화된 연구 생태계 조성을 목표로 하는 대표적인 공공 AI 평가 프로젝트입니다.</p>
<p><img src="https://velog.velcdn.com/images/yujin_jeong/post/49762c86-a485-45a7-84da-188035fe1a2f/image.png" alt=""></p>
<p>다들 AI 허브는 많이 아는데 여기는 몰라서 좀 속상했는데 데이터가 많이 겹침 ㅋㅋ... 암튼 그래도 우리 멋진 쿠다 NLP 8기는 개빡세게 6주동안 스터디하고, 3주, 3주 과제를 하게 되었습니다.</p>
<p>첫 과제는 일단 <strong>&#39;한국어&#39; 특화 LLM 프로젝트</strong>를 해보자! 라는 의견에서 시작했고, 팀원마다 지식과 경험의 벡터값이 다 제각각이라 서로를 파악하면서 공부하고자 준비한 태스크이고 세 팀으로 구성해서 프로젝트를 하고 3주간 진행과정을 발표하면서 서로 질문하고 제안하면서 공부하는 시간을 갖는 것이다. </p>
<p>우선 LLM이 아무리 작아졌대도 우리의 노트북으로 학습시키거나 할 순 없기에 교내 소프트웨어 관련 학생들이 사용할 수 있는 <strong>세라프 사용법을 공유</strong>했고 사용 불가능한 경우 <strong>Colab Pro를 결제</strong>하였다. </p>
<p>처음 공부하는 입장에서 세라프 사용은 어렵기 때문에 기록해보겠다.</p>
<blockquote>
<p>** 1. 한국어 일상 대화 연결 
2. 그림(사진) 기반 문장 생성
3. 국회 회의록 요약 **</p>
</blockquote>
<p>세개 과제이고 나는 3번 국회 회의록 요약에 참여한다.</p>
<p>과제는 요약하자면 </p>
<blockquote>
<p>‘국회 회의록 요약’ 과제는 <strong>국립국어원의 2021·2022년 회의록 요약 말뭉치 연구 결과를 기반으로, 국회 회의록의 방대한 대화 내용을 자동으로 요약하는 인공지능 모델을 개발</strong>하는 것을 목표로 합니다. 참가자는 회의록 내 안건별 발언 데이터를 분석하여 주요 논의 내용, 결정 사항, 의견 차이를 간결하고 정보 가치 있게 요약해야 합니다. 입력 데이터는 발언자, 직책, 대화 내용, 안건 정보가 포함된 JSON 형식으로 제공되며, 출력은 모델이 생성한 요약문입니다. 성능 평가는 ROUGE-1 점수를 사용하여 생성된 요약문이 참조 요약문의 핵심어를 얼마나 잘 재현하는지를 측정합니다. <strong>기준 모델은 GitHub(teddysum/Korean_NAMS_2024)에 공개</strong>되어 있으며, 외부 데이터를 학습에 활용할 수 있으나 ChatGPT 등 외부 API를 추론에 직접 사용하는 것은 금지됩니다.</p>
</blockquote>
<p>로그인하고 자료 받기하고, 개인정보 사용 동의 서약을 쓰면 데이터를 다운로드할 수 있다.
데이터 다운로드를 하기위해서 앱 설치하고 데이터도 받을 수 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/yujin_jeong/post/c54548ae-2e42-4c25-8bfb-7a41ce6011ef/image.png" alt=""></p>
<h2 id="전체-실험-기획">전체 실험 기획</h2>
<blockquote>
<ol>
<li><strong>Baseline → 전처리 → 언어전환 → 모델비교</strong> 순으로 실험을 설계해
단계별 개선 효과를 계량적으로 볼 수 있게 한다.</li>
<li><strong>외부 API 추론 금지</strong> 규칙을 지키면서도
공개 모델 fine-tuning을 통해 연구 확장성을 확보한다.</li>
<li>Aurora 환경에 맞춘 경량 모델(bart-base, kobart)로 자원 효율적인 실험을 진행한다.</li>
</ol>
</blockquote>
<table>
<thead>
<tr>
<th>단계</th>
<th>실험 목적</th>
<th>내용 요약</th>
</tr>
</thead>
<tbody><tr>
<td><strong>1단계 – Baseline 복제</strong></td>
<td>기준 성능 확보</td>
<td>국립국어원 제공 기준 모델(<code>teddysum/Korean_NAMS_2024</code>)을 동일 환경에서 재현하여 기준 ROUGE-1 수치 확보</td>
</tr>
<tr>
<td><strong>2단계 – 내부 모델 비교</strong></td>
<td>구조별 성능 탐색</td>
<td>Aurora 환경에서 직접 fine-tuning: 전처리·번역·한국어 모델별로 실험 (EXP A, B, C)</td>
</tr>
<tr>
<td><strong>3단계 – 외부 모델 확장(후순위)</strong></td>
<td>추가 개선 실험</td>
<td>허깅페이스 공개모델(예: BART, PEGASUS, KoBART)을 사전학습 기반으로 비교하되, ChatGPT 등 외부 API는 사용 X</td>
</tr>
</tbody></table>
<hr>
<h3 id="1-데이터-준비">1) 데이터 준비</h3>
<ul>
<li><p><strong>입력 형식:</strong></p>
<pre><code class="language-json">{
  &quot;speaker&quot;: &quot;홍길동&quot;,
  &quot;position&quot;: &quot;위원장&quot;,
  &quot;agenda&quot;: &quot;법안심사&quot;,
  &quot;utterance&quot;: &quot;본 안건에 대해 말씀드리겠습니다...&quot;
}</code></pre>
</li>
<li><p><strong>출력:</strong> <code>summary</code> 필드에 참조 요약문</p>
</li>
<li><p><strong>처리 대상:</strong> 발언 단위(utterance) 또는 안건 단위(agenda)로 병합 가능</p>
</li>
<li><p><strong>파일 구조:</strong></p>
<pre><code>data/
├─ raw.json
├─ preprocess/
│   ├─ split_train.jsonl
│   ├─ split_val.jsonl
│   └─ split_test.jsonl</code></pre></li>
</ul>
<hr>
<h3 id="2-환경-세팅">2) 환경 세팅</h3>
<p>너무 길어져서 다음 글로 패쓰
<a href="https://velog.io/@yujin_jeong/HPC%EA%B3%A0%EC%84%B1%EB%8A%A5-%ED%81%B4%EB%9F%AC%EC%8A%A4%ED%84%B0-%EA%B8%B0%EB%B3%B8-%EA%B5%AC%EC%A1%B0">다음글 1(Aurora 너 누군데)</a>
[다음글 2(말평 진행사항(1))] </p>
<hr>
<h3 id="3-실험-구성">3) 실험 구성</h3>
<h4 id="exp-a--baseline--전처리-최소화">EXP A — <strong>Baseline / 전처리 최소화</strong></h4>
<ul>
<li>입력: 원본 회의록 텍스트 (<code>utterance</code> or <code>agenda</code> 단위)</li>
<li>모델: <code>facebook/bart-large-cnn</code> (또는 <code>kobart</code>)</li>
<li>목적: 전처리 없이 baseline 성능 확인</li>
<li>출력: <code>outputs/exp_A/</code></li>
<li>지표: ROUGE-1, GPU 메모리, 학습 시간</li>
</ul>
<h4 id="exp-b--전처리--번역--영어모델">EXP B — <strong>전처리 + 번역 + 영어모델</strong></h4>
<ul>
<li>입력: 한국어 → 영어 번역 (Marian MT <code>opus-mt-ko-en</code>)</li>
<li>모델: <code>facebook/bart-large-cnn</code></li>
<li>목적: 번역 중간단계를 거친 경우 성능 및 자원 사용량 비교</li>
<li>출력: <code>outputs/exp_B/</code></li>
</ul>
<h4 id="exp-c--전처리--한국어모델">EXP C — <strong>전처리 + 한국어모델</strong></h4>
<ul>
<li>입력: 전처리된 한국어 데이터</li>
<li>모델: <code>hyunwoongko/kobart</code> (국문용)</li>
<li>목적: 한국어 전용 사전학습 모델과 비교</li>
<li>출력: <code>outputs/exp_C/</code></li>
</ul>
<hr>
<h3 id="4-데이터-전처리">4) 데이터 전처리</h3>
<hr>
<p>생각해보면 이거 데이터가 발화 데이터라 요약에 필요없는 데이터가 많았습니다.</p>
<p>데이터 전처리 논의에서는 국회 회의록 요약 모델의 입력 품질을 극대화하기 위한 통합 파이프라인 설계에 초점을 맞추었습니다. 핵심 목표는 하나의 회의록 안에 포함된 여러 안건을 명확히 분리하고, 각 안건과 직접적으로 관련된 발화만을 선별하여 모델 입력을 정제하는 것입니다. 이를 위해 sentence_id를 안건 시작점으로, “~되었음을 선포합니다” 등의 종결 패턴을 종료점으로 활용하는 분리 규칙이 제안되었으며, 예외적인 경우에는 LLM을 활용해 “특정 keyword 관련 내용만 요약”하도록 후처리를 적용하는 방안이 논의되었습니다. 불필요한 짧은 문장이나 머뭇거림 발화는 제거하고, 숫자나 법안명 등 핵심 정보가 포함된 문장은 유지하는 세부 기준이 설정되었습니다.</p>
<p>전체 전처리 플로우는 4단계 파이프라인 구조로 설계되었습니다. (1) 화자명 앞에 [직책/역할] prefix를 부여하여 발화 구조를 명확히 하고, (2) 의례적·불용 문장을 정규식으로 제거하거나 결정문을 <code>DECISION</code> 태그로 표시하며, (3) 안건명과 핵심 키워드를 <code>KEY</code> 태그로 강조하고, (4) TF-IDF 기반으로 중요도가 높은 문장을 <code>IMP</code> 태그로 감싸 가중치를 부여합니다. 특히 TF-IDF는 회의 전체 단위보다 안건별 계산 단위로 수행하는 것이 타당하다는 결론이 도출되었습니다. 향후 실험 단계에서는 전처리된 데이터를 기반으로 GemmaX2-28-2B-v0.1 등 경량 LLM을 적용해 요약 성능을 검증할 예정입니다.</p>
<h4 id="참고-링크">참고 링크</h4>
<ul>
<li>회의록 요약 과제 기술서: 국립국어원 말평 (NIKL)</li>
<li>기준 모델(GitHub): Korean_NAMS_2024</li>
<li>전처리 참고 모델: ModelSpace/GemmaX2-28-2B-v0.1 (Hugging Face)</li>
</ul>
<hr>
<h3 id="5-학습-단계">5) 학습 단계</h3>
<ul>
<li><p>실행 예시:</p>
<pre><code class="language-bash">python src/finetune_summarizer.py \
    --model_name facebook/bart-large-cnn \
    --data_dir data/preprocess \
    --output_dir outputs/exp_A \
    --epochs 3</code></pre>
</li>
<li><p>주요 설정</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>값</th>
</tr>
</thead>
<tbody><tr>
<td><code>max_input_length</code></td>
<td>1024</td>
</tr>
<tr>
<td><code>max_target_length</code></td>
<td>128</td>
</tr>
<tr>
<td><code>batch_size</code></td>
<td>1~2</td>
</tr>
<tr>
<td><code>fp16</code></td>
<td>True</td>
</tr>
<tr>
<td><code>evaluation_strategy</code></td>
<td>epoch</td>
</tr>
</tbody></table>
</li>
</ul>
<hr>
<h3 id="6-평가-및-로그">6) 평가 및 로그</h3>
<ul>
<li><p><code>src/evaluate.py</code>로 ROUGE-1 계산</p>
</li>
<li><p>GPU/CPU 메모리 모니터링(<code>src/monitor.py</code>)</p>
</li>
<li><p>결과 정리:</p>
<table>
<thead>
<tr>
<th>실험</th>
<th>모델</th>
<th>ROUGE-1 (F1)</th>
<th>GPU Max (GB)</th>
<th>CPU (GB)</th>
<th>시간</th>
</tr>
</thead>
<tbody><tr>
<td>A</td>
<td>-</td>
<td>-</td>
<td>-</td>
<td>-</td>
<td>-</td>
</tr>
<tr>
<td>B</td>
<td>-</td>
<td>-</td>
<td>-</td>
<td>-</td>
<td>-</td>
</tr>
<tr>
<td>C</td>
<td>-</td>
<td>-</td>
<td>-</td>
<td>-</td>
<td>-</td>
</tr>
</tbody></table>
</li>
</ul>
<hr>
<h3 id="7-후속-실험">7) 후속 실험</h3>
<p>이건 일단 제 계획입니다. </p>
<table>
<thead>
<tr>
<th>추가 실험</th>
<th>목적</th>
</tr>
</thead>
<tbody><tr>
<td><code>pegasus-xsum</code></td>
<td>추상적 요약 성능 확인</td>
</tr>
<tr>
<td><code>longformer-encoder-decoder</code></td>
<td>장문 처리 성능 확인</td>
</tr>
<tr>
<td><code>kobart vs kot5</code></td>
<td>한국어 구조 비교</td>
</tr>
<tr>
<td><code>데이터 augmentation</code></td>
<td>저자원 구간 개선 실험</td>
</tr>
<tr>
<td><code>pseudo-labeling</code></td>
<td>unlabeled 회의록 확장</td>
</tr>
</tbody></table>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[SSH, SFTP 간단히 이해하기]]></title>
            <link>https://velog.io/@yujin_jeong/SSH-SFTP-%EA%B0%84%EB%8B%A8%ED%9E%88-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@yujin_jeong/SSH-SFTP-%EA%B0%84%EB%8B%A8%ED%9E%88-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 12 Nov 2025 06:57:50 GMT</pubDate>
            <description><![CDATA[<p>쿠다 N년차...
GPU랑 CPU가 대체 왜 다르고 왜 필요하지 모르던 시기를 지나서 학교 서버에서 학습을 시키는 법을 알려주는 위치까지 발전...? 했는데 
<img src="https://velog.velcdn.com/images/yujin_jeong/post/cbd56032-b4d8-4b48-8627-76905241c1c7/image.png" alt=""></p>
<p>이제 <strong>약간 알려주기도 귀찮아서</strong> 정리함.</p>
<p>일단 서버라는 개념은 대충 알고 있을 것임
<img src="https://velog.velcdn.com/images/yujin_jeong/post/d17206f9-5ac7-46bd-8c98-27d1005bd387/image.png" alt="">
<a href="https://brunch.co.kr/@imagineerjy/18">https://brunch.co.kr/@imagineerjy/18</a>
진짜 모르면 진짜 여기 추천</p>
<p>암튼 서버에 왜 연결해야하고 그게 좋다는데 어케 쓰지,, 하는 사람들을 위해 간단히 정리해보겠습니다.</p>
<hr>
<h4 id="ssh와-sftp">SSH와 SFTP</h4>
<p><strong>일단 SSH란?</strong></p>
<blockquote>
<p><strong>원격 서버 접속</strong>이라는 것은 다른 컴퓨터에 있는 저장장치나 컴퓨팅 리소스에 네트워크를 통해 접속한다는 뜻</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/yujin_jeong/post/9f606b9c-cb9d-49be-b507-f3b17fa6c64d/image.png" alt=""></p>
<p>SSH는 Secure Shell의 약자로, <strong>원격 서버에 암호화된 네트워크 연결을 통해 안전하게 접속하는 프로토콜</strong>입니다. SSH를 통해 원격 서버의 셸에 직접 접속하여 명령어를 실행하고, 서버를 제어할 수 있습니다.</p>
<p><strong>안전한 원격 접속의 개념</strong> 서버에 SSH로 접속하면, 마치 내 컴퓨터에서 직접 작업하는 것처럼 서버의 자원을 활용할 수 있습니다. 모든 통신은 암호화되어 있어 보안 위협으로부터 데이터를 보호합니다.
실제 개발 환경에서는 SSH로 서버에 접속하여 작업하고, SFTP로 파일을 주고받는 패턴을 사용합니다.</p>
<p><img src="https://velog.velcdn.com/images/yujin_jeong/post/e319f497-3206-4a72-b3b5-05e8a023ca16/image.png" alt=""></p>
<h4 id="public-key--private-key">Public Key &amp; Private Key</h4>
<p><del>원래 굳이 설명 안하려고 했는데 쓰다보면 key랑 password랑 헷갈려하시길래</del>
SSH는 비대칭 키 암호화(Asymmetric Encryption) 방식을 사용하여 인증합니다.</p>
<table>
<thead>
<tr>
<th>키 종류</th>
<th>역할</th>
<th>보안 수준</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Public Key</strong></td>
<td>암호화에 사용되며 서버에 등록</td>
<td>외부 노출 가능</td>
</tr>
<tr>
<td><strong>Private Key</strong></td>
<td>복호화에 사용되며 클라이언트에 보관</td>
<td>절대 유출 금지</td>
</tr>
</tbody></table>
<p>Public Key로 암호화된 데이터는 오직 대응하는 Private Key로만 복호화할 수 있습니다. 이 수학적 특성을 이용해 클라이언트의 신원을 안전하게 검증합니다.</p>
<hr>
<p><strong>SFTP (Secure File Transfer Protocol)</strong>
SFTP는 SSH 프로토콜 위에서 동작하는 <strong>파일 전송 프로토콜</strong>입니다. SSH의 암호화 메커니즘을 그대로 사용하기 때문에 데이터 전송이 안전하게 보호됩니다. SFTP는 단순히 &quot;보안이 강화된 FTP&quot;가 아니라, SSH 프로토콜 자체를 활용한 완전히 다른 구조입니다.</p>
<blockquote>
<p><strong>프로토콜이 뭔 소린지 모르겠나요?</strong>
프로토콜은 컴퓨터나 기기 간의 데이터 교환 규칙 체계입니다. 이는 통신 시 지켜야 할 통신 규약으로, <strong>데이터의 형식, 송수신 방법, 오류 처리 등을 정의</strong>하여 원활한 소통을 가능하게 합니다. 웹 브라우저와 웹 서버가 통신할 때 사용하는 HTTP, 이메일을 주고받을 때 사용하는 SMTP 등이 대표적인 예시랍니다.</p>
</blockquote>
<h4 id="sftp-vs-ftp">SFTP vs FTP</h4>
<p><del>이거까지만 하고 어케 쓰는지 알려줄게요 ㅠㅠ</del></p>
<table>
<thead>
<tr>
<th>프로토콜</th>
<th>주요 사용 목적</th>
<th>포트</th>
</tr>
</thead>
<tbody><tr>
<td><strong>SSH</strong></td>
<td>원격 서버 접속 및 명령 실행</td>
<td>22</td>
</tr>
<tr>
<td><strong>SFTP</strong></td>
<td>암호화된 파일 전송</td>
<td>22 (SSH 기반)</td>
</tr>
<tr>
<td><strong>FTP</strong></td>
<td>파일 전송 (비보안)</td>
<td>20, 21</td>
</tr>
</tbody></table>
<h3 id="개발-플로우">개발 플로우</h3>
<p>** SSH 접속 및 실행** SSH로 서버에 접속하여 업로드된 코드를 실행하고 테스트합니다.
** 결과 확인 및 배포** 실행 결과를 확인하고, 필요에 따라 다시 로컬에서 수정 후 동기화 과정을 반복합니다.</p>
<p><img src="https://velog.velcdn.com/images/yujin_jeong/post/ee73e8cb-9919-4681-a205-669c325222b2/image.png" alt=""></p>
<h3 id="단계별-설명">단계별 설명</h3>
<p><strong>① 로컬 작업</strong><br>로컬에서 코드를 완성하고, SFTP Config를 설정합니다.</p>
<p><strong>② 파일 동기화 (SFTP)</strong><br>코드를 서버로 안전하게 전송합니다.<br>💡 <code>uploadOnSave: true</code>로 자동 동기화 활용 (하단에 있음)</p>
<p><strong>③ 서버 제어 (SSH)</strong><br>SSH로 접속 후, GPU 노드 할당 명령(예: <code>srun</code>)을 실행합니다 .</p>
<p><strong>④ 학습 실행 및 모니터링</strong><br>데이터셋을 준비하고, 학습 스크립트(<code>sbatch</code> 등)를 실행하고, 학습을 진행합니다. SSH 셸에서 로그를 확인하며 모니터링합니다.</p>
<p><strong>⑤ 결과 다운로드 (SFTP)</strong><br>학습이 완료되면 결과 파일(모델 가중치, 최종 로그 등)을 SFTP로 다시 로컬로 다운로드합니다.</p>
<h2 id="ftp-vs-sftp">FTP vs SFTP</h2>
<hr>
<h3 id="ftp-file-transfer-protocol">FTP (File Transfer Protocol)</h3>
<p><img src="https://velog.velcdn.com/images/yujin_jeong/post/1ef17233-8e0e-49d5-9963-b6247e7f5fee/image.png" alt=""></p>
<p>FTP는 파일 전송 전용 프로토콜이지만 <strong>암호화를 지원하지 않습니다</strong>. 전송되는 모든 데이터가 평문(plain text)으로 네트워크를 통해 전달되기 때문에 보안에 취약합니다.</p>
<h3 id="sftp-secure-file-transfer-protocol">SFTP (Secure File Transfer Protocol)</h3>
<p>SFTP는 SSH 프로토콜 위에서 동작하는 파일 전송 프로토콜입니다. SSH의 강력한 암호화 메커니즘을 그대로 사용하기 때문에 데이터 전송이 안전하게 보호됩니다.
<img src="https://velog.velcdn.com/images/yujin_jeong/post/3c6ca17b-6369-4a35-a6c6-a8021a473376/image.png" alt=""></p>
<table>
<thead>
<tr>
<th>프로토콜</th>
<th>포트</th>
<th>암호화</th>
<th>보안 수준</th>
<th>기반</th>
</tr>
</thead>
<tbody><tr>
<td><strong>SFTP</strong></td>
<td>22</td>
<td>✅ SSH 암호화</td>
<td>높음</td>
<td>SSH</td>
</tr>
<tr>
<td><strong>FTP</strong></td>
<td>20, 21</td>
<td>❌ 없음</td>
<td>매우 취약</td>
<td>독립 프로토콜</td>
</tr>
</tbody></table>
<p>SFTP는 단순한 &quot;보안 강화 FTP&quot;가 아니라, SSH 터널링을 이용한 안전한 파일 전송 방식입니다. 개발 환경에서 코드를 서버에 올리거나 학습 결과물을 다운로드할 때 주로 사용됩니다.</p>
<h3 id="sftp를-통한-파일-전송">SFTP를 통한 파일 전송</h3>
<hr>
<h4 id="vscode-sftp-설정">VSCode SFTP 설정</h4>
<p>VSCode에서 SFTP 확장을 사용하면 로컬과 서버 간 파일 동기화를 쉽게 할 수 있습니다.</p>
<p><strong>sftp.json 설정 예시:</strong></p>
<pre><code class="language-json">{
  &quot;name&quot;: &quot;Aurora&quot;,
  &quot;host&quot;: &quot;163.180.160.105&quot;,
  &quot;protocol&quot;: &quot;sftp&quot;,
  &quot;port&quot;: 30080,
  &quot;username&quot;: &quot;your_id&quot;,
  &quot;remotePath&quot;: &quot;/data/your_id/repos/Assignment&quot;,
  &quot;uploadOnSave&quot;: true,
  &quot;useTempFile&quot;: false,
  &quot;openSsh&quot;: false
}</code></pre>
<table>
<thead>
<tr>
<th>옵션</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><code>host</code></td>
<td>서버 IP 주소</td>
</tr>
<tr>
<td><code>port</code></td>
<td>학교 외부: 30080, 내부: 22</td>
</tr>
<tr>
<td><code>remotePath</code></td>
<td>서버의 작업 디렉토리 경로</td>
</tr>
<tr>
<td><code>uploadOnSave</code></td>
<td>저장 시 자동 업로드</td>
</tr>
</tbody></table>
<h4 id="터미널에서-sftp-직접-사용">터미널에서 SFTP 직접 사용</h4>
<pre><code class="language-bash"># SFTP 접속
sftp -P 30080 your_id@163.180.160.105

# 서버 디렉토리 이동
sftp&gt; cd /data/your_id/repos

# 서버 → 로컬 다운로드
sftp&gt; get remote_file.py
sftp&gt; get -r remote_directory

# 로컬 → 서버 업로드
sftp&gt; put local_file.py
sftp&gt; put -r local_directory

# 종료
sftp&gt; exit</code></pre>
<blockquote>
<p>이마저도 귀찮다면 일단 
<a href="https://github.com/YuujInJeong/khuseraphdashboard">https://github.com/YuujInJeong/khuseraphdashboard</a></p>
</blockquote>
<p><strong>* 자동 동기화 활용</strong></p>
<p>VSCode의 <code>uploadOnSave: true</code> 옵션을 활성화하면
<img src="https://velog.velcdn.com/images/yujin_jeong/post/29cec376-2fd4-43ff-b11b-585563974f82/image.png" alt=""></p>
<p><strong>* 포트 선택 가이드</strong></p>
<table>
<thead>
<tr>
<th>환경</th>
<th>포트</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td>학교 내부</td>
<td>22</td>
<td>기본 SSH 포트</td>
</tr>
<tr>
<td>학교 외부</td>
<td>30080</td>
<td>방화벽 우회용 포트 포워딩</td>
</tr>
<tr>
<td>권장</td>
<td>30080</td>
<td>범용적으로 사용 가능</td>
</tr>
</tbody></table>
<p>*** 주요 명령어
**</p>
<table>
<thead>
<tr>
<th>작업</th>
<th>SSH</th>
<th>SFTP</th>
</tr>
</thead>
<tbody><tr>
<td>용도</td>
<td>원격 셸 접속</td>
<td>파일 전송</td>
</tr>
<tr>
<td>접속</td>
<td><code>ssh user@host -p port</code></td>
<td><code>sftp -P port user@host</code></td>
</tr>
<tr>
<td>디렉토리 이동</td>
<td><code>cd /path</code></td>
<td><code>cd /path</code> (원격), <code>lcd /path</code> (로컬)</td>
</tr>
<tr>
<td>파일 확인</td>
<td><code>ls</code>, <code>cat</code></td>
<td><code>ls</code> (원격), <code>lls</code> (로컬)</td>
</tr>
<tr>
<td>파일 전송</td>
<td>❌</td>
<td><code>get</code>, <code>put</code></td>
</tr>
<tr>
<td>명령 실행</td>
<td>✅</td>
<td>❌</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[Python GUI를 실행파일로 만들어본 후기]]></title>
            <link>https://velog.io/@yujin_jeong/Python-GUI%EB%A5%BC-%EC%8B%A4%ED%96%89%ED%8C%8C%EC%9D%BC%EB%A1%9C-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B8-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@yujin_jeong/Python-GUI%EB%A5%BC-%EC%8B%A4%ED%96%89%ED%8C%8C%EC%9D%BC%EB%A1%9C-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B8-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Wed, 27 Aug 2025 02:41:29 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/yujin_jeong/post/47736ad7-5f4e-4585-8000-ef3b0c35c879/image.png" alt=""></p>
<blockquote>
<h2 id="🔧-연구용-기술-스택">🔧 연구용 기술 스택</h2>
</blockquote>
<ul>
<li><strong>GUI</strong>: PyQt5 (크로스 플랫폼, 연구용 위젯 풍부)  </li>
<li><strong>하드웨어 통신</strong>: pySerial (Arduino/Teensy와 시리얼 통신)</li>
<li><strong>AI</strong>: PyTorch + CNN (MNIST 분류, 확장 가능)</li>
<li><strong>시각화</strong>: PyQtGraph (실시간 플롯, 빠른 렌더링)</li>
<li><strong>빌드</strong>: PyInstaller + .spec 파일</li>
<li><strong>데이터</strong>: NumPy + 자체 CSV # 연구용 16x16 어레이 제어 시스템 </li>
</ul>
<h2 id="🚀-프로젝트-시작계기">🚀 프로젝트 시작계기</h2>
<p>최근에 신소재 연구실에서 잠깐 작업할 일이 생겼다. 16x16 광학 센서 어레이를 이용한 실험 시스템을 만드는 건데, 연구진이 매번 Python 코드를 직접 실행하면서 실험하기엔 너무 번거로워 보였다.</p>
<p>평소에 연구실용 도구를 만드는 걸 좋아해서** 제대로 된 GUI 프로그램을 만들어보자고 마음먹었다.** 연구자들이 복잡한 코드 신경 안 쓰고 그냥 버튼 클릭으로 실험할 수 있게 하는 게 목표였다.</p>
<p>근데 문제가 있더라. 연구실 컴퓨터마다 Python 환경이 다르고, 매번 패키지 설치하고 경로 설정하고... 다른 연구실에서 협업할 때도 설명서를 따로 써줘야 하는 것도 많고, 새로운 학생이 들어와서 실험하려면 또 환경 세팅을 해야 했다.
<img src="https://velog.velcdn.com/images/yujin_jeong/post/e1845657-717a-44cf-8e85-dc01213d520d/image.png" alt=""></p>
<p>그냥 연구진들이 쉽게 쓸 수 있는 실행 파일을 만들어보자 싶어서 시작했다.</p>
<h2 id="💡-연구용-시스템-요구사항">💡 연구용 시스템 요구사항</h2>
<p>요구사항은 연구실 상황에 맞춰 정했다.</p>
<p>Python 설치 없이도 쓸 수 있고, 복잡한 환경 설정 안 해도 되고, 16x16 어레이에서 실시간으로 데이터를 수집하면서 AI 분류 결과를 볼 수 있으면 됐다. 주피터 노트북처럼 매번 코드를 수정하고 실행하고 싶지는 않았다.</p>
<blockquote>
<p><strong>연구실 특성상 필요한 기능들</strong></p>
</blockquote>
<ul>
<li>16x16 광학 센서 어레이와 시리얼 통신</li>
<li>실시간 데이터 수집 및 시각화  </li>
<li>AI 기반 MNIST 숫자 분류</li>
<li>실험 파라미터 실시간 조정</li>
<li>데이터 저장 및 성능 분석</li>
<li>하드웨어 없이도 테스트 가능한 시뮬레이션 모드</li>
</ul>
<p>아이디어 자체는 복잡하지 않았지만 실제 연구에 쓸 수 있을 만큼 안정적으로 만드는 게 중요했다. 실험 중에 프로그램이 죽으면 데이터를 다 잃어버릴 수도 있거든요...
(유진님 이거 죽었서요..🤔)</p>
<p>PyQt5로 슥슥 만들었다.</p>
<h2 id="기술적-고민들">기술적 고민들</h2>
<p>문제는 구체적인 기술 요소들을 정하는 거였다. GUI 프레임워크 뭐 쓸지, 모델 저장 형식은 어떻게 할지, 실행 파일 빌드는 어떻게 할지... 이런 것들 고민하는데 시간이 오래 걸렸다.</p>
<p>특히 PyInstaller vs cx_Freeze vs Nuitka 선택에서 고민이 많았다. 각각 장단점이 다른데</p>
<ul>
<li><strong>PyInstaller</strong>: 가장 유명하고 안정적. 하지만 파일 크기가 큰 편</li>
<li><strong>cx_Freeze</strong>: 크로스 플랫폼 지원 좋음. 설정이 복잡함  </li>
<li><strong>Nuitka</strong>: 성능 최고. 하지만 복잡한 의존성에서 문제 생기기 쉬움</li>
</ul>
<p>결국 PyInstaller로 결정했다. 문서도 많고 PyTorch 같은 라이브러리와 호환성도 검증되어 있어서.</p>
<p>특히 하드웨어 통신 부분이 까다로웠다. Arduino/Teensy와 시리얼 통신으로 16x16 어레이를 제어해야 하는데, 연결이 끊어지거나 데이터 전송 오류가 생기면 실험 전체가 망가질 수 있다.</p>
<pre><code class="language-python"># 시리얼 통신 안정성을 위한 처리
class SerialManager:
    def __init__(self):
        self.connection = None
        self.reconnect_attempts = 0

    def safe_write(self, data):
        try:
            if self.connection and self.connection.is_open:
                self.connection.write(data)
                return True
        except serial.SerialException:
            self.attempt_reconnect()
        return False</code></pre>
<p>모델 파일 경로 처리도 연구실 환경에 맞춰 신경써야 했다. 개발할 때는 상대 경로로 <code>models/mnist_cnn_model.pth</code> 이렇게 썼는데, 실행 파일로 만들면 경로가 달라진다. 특히 연구실 컴퓨터들이 OS가 다 달라서...</p>
<pre><code class="language-python">import sys
import os

def get_resource_path(relative_path):
    if hasattr(sys, &#39;_MEIPASS&#39;):
        return os.path.join(sys._MEIPASS, relative_path)
    return os.path.join(os.path.dirname(__file__), relative_path)</code></pre>
<p>이런 식으로 경로 처리 함수를 만들어서 해결했다.</p>
<h2 id="🚧-연구용-시스템-개발-과정">🚧 연구용 시스템 개발 과정</h2>
<p>완성하고 나서 연구실에서 실제로 테스트해보니까 생각보다 문제가 많았다. </p>
<p>개발 환경에서는 잘 돌아가던 게 실제 실험 장비와 연결하면 여러 오류가 터져나왔다. 특히 PyTorch의 CUDA 설정이 꼬이면서 GPU 가속이 안되고, 시리얼 포트 권한 문제로 하드웨어 통신이 안 되는 경우가 발생했다.</p>
<p>연구실 특성상 생기는 문제들도 있었다.</p>
<ul>
<li><strong>장비별 시리얼 포트 설정</strong>: 각 실험 장비마다 포트 번호가 다름</li>
<li><strong>데이터 수집 속도</strong>: 실시간 처리를 위해 최적화 필요</li>
<li><strong>장시간 실험 안정성</strong>: 몇 시간씩 돌려도 메모리 누수 없어야 함</li>
<li><strong>다중 사용자 환경</strong>: 여러 연구자가 동시에 사용 가능해야 함</li>
</ul>
<p>급하게 .spec 파일을 수정해가면서 연구실 환경에 맞는 설정을 추가했다.</p>
<pre><code class="language-python"># ArrayControlSystem.spec - 연구실 환경 최적화
a = Analysis([&#39;array_control_system.py&#39;],
            pathex=[&#39;.&#39;],
            binaries=[],
            datas=[(&#39;models&#39;, &#39;models&#39;), 
                   (&#39;mnist_cnn_model.py&#39;, &#39;.&#39;),
                   (&#39;configs&#39;, &#39;configs&#39;)],  # 실험 설정 파일들 포함
            hiddenimports=[&#39;torch&#39;, &#39;torchvision&#39;, &#39;PyQt5&#39;, 
                          &#39;serial&#39;, &#39;pyqtgraph&#39;, &#39;numpy&#39;],
            hookspath=[],
            runtime_hooks=[],
            excludes=[&#39;tkinter&#39;],  # 불필요한 GUI 라이브러리 제외
            win_no_prefer_redirects=False,
            win_private_assemblies=False,
            cipher=block_cipher)</code></pre>
<p>연구실에서 중요한 건 <strong>재현 가능성</strong>이었다. 같은 실험을 다른 날, 다른 컴퓨터에서 해도 동일한 결과가 나와야 한다. 그래서 실험 파라미터들을 모두 설정 파일로 저장하고, 실행 파일에 포함시켰다.</p>
<h2 id="🎨-연구용-uiux-설계">🎨 연구용 UI/UX 설계</h2>
<p>사용자 인터페이스는... 솔직히 말하면 처음에는 기능만 돌아가게 만드는 데 집중했다. 연구자들이 쓸 거니까 예쁠 필요는 없다고 생각했는데, 막상 써보니까 가독성이 떨어져서 실험하기 불편할 것 같았다. 그래서 뭐 사용성 높은 걸 가능한 위로 아닌 것을 아래로 내렸다. </p>
<p>연구실 환경에 맞는 UI 요구사항들:</p>
<ul>
<li><strong>16x16 히트맵 시각화</strong>: 센서 어레이 상태를 한눈에 파악</li>
<li><strong>실시간 성능 지표</strong>: 분류 정확도, 응답 시간 등 모니터링  </li>
<li><strong>실험 로그 시스템</strong>: 모든 실험 과정 자동 기록</li>
<li><strong>파라미터 조정 패널</strong>: 전압, 샘플 수 등 실시간 변경</li>
<li><strong>시뮬레이션 모드</strong>: 하드웨어 없이도 알고리즘 테스트</li>
</ul>
<pre><code class="language-css">/* 연구용 대시보드 스타일 */
QMainWindow {
    background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
                stop:0 #1e3c72, stop:1 #2a5298);
}

QGroupBox {
    font-weight: bold;
    border: 2px solid #3498db;
    border-radius: 8px;
    margin: 5px;
    padding-top: 10px;
    background-color: rgba(255, 255, 255, 0.1);
}

/* 실험 상태에 따른 색상 변화 */
QPushButton[status=&quot;connected&quot;] {
    background-color: #27ae60;
}
QPushButton[status=&quot;disconnected&quot;] {
    background-color: #e74c3c;
}</code></pre>
<p>연구실에서 중요한 건 <strong>정보의 밀도</strong>였다. 한 화면에 최대한 많은 정보를 담으면서도 가독성을 유지해야 했다. 16x16 히트맵, 성능 지표 테이블, 로그 창을 모두 실시간으로 업데이트하면서도 UI가 느려지면 안디는데...</p>
<h2 id="🛠️-연구실-환경-배포">🛠️ 연구실 환경 배포</h2>
<p>PyInstaller로 실행 파일 만들고, 내 드라이브에 dmg와 exe를 배포했다.</p>
<pre><code class="language-bash"># 연구실별 빌드 스크립트
# macOS (주로 분석용)
pyinstaller --onefile --windowed ArrayControlSystem.spec

# Windows (실험 장비 제어용)  
pyinstaller --onefile --windowed ArrayControlSystem.spec

# Linux (서버 환경)
docker run --rm -v $(pwd):/app python:3.9 bash -c &quot;cd /app &amp;&amp; pip install -r requirements.txt &amp;&amp; pyinstaller ArrayControlSystem.spec&quot;</code></pre>
<p>연구실 특성상 여러 OS가 섞여 있다. 실험 장비는 Windows, 분석은 macOS, 서버는 Linux... 각각 빌드해서 배포해야 했다.
버전 관리는 파일명에 날짜를 넣어<code>ArrayControlSystem_v1.2_20241215.exe</code> 이런 식으로 했다.
<img src="https://velog.velcdn.com/images/yujin_jeong/post/78508541-033e-48ec-901c-3e9ac77be5a6/image.png" alt=""></p>
<h2 id="📊-연구용-성능-최적화">📊 연구용 성능 최적화</h2>
<p>파일 크기가 생각보다 컸다. macOS 기준으로 1.9GB나 나왔다. </p>
<p>연구실에서는 파일 크기보다는 <strong>안정성</strong>이 더 중요했다. 실험 중에 프로그램이 죽으면 몇 시간 분량의 데이터를 잃을 수 있거든. 그래서 파일 크기는 좀 크더라도 모든 의존성을 포함시키기로 했다.</p>
<p>주원인은 PyTorch였다. 연구용이라 GPU 가속이 필요할 수도 있어서 CUDA 버전을 포함시켰는데, 이게 용량을 많이 차지했다.</p>
<p>연구실 환경에 맞춘 최적화:</p>
<pre><code class="language-bash"># 연구용 최적화 설정
# GPU 가속 필요한 경우
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118

# CPU만 쓰는 경우  
pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu

# 연구용 필수 모듈만 포함
pyinstaller --exclude-module=matplotlib --exclude-module=scipy ArrayControlSystem.spec</code></pre>
<p>실행 속도는 연구 환경에서 치명적이다. 
실시간 데이터 수집하면서 AI 분류까지 해야 하는데, 처리가 느리면 데이터 손실이 생길 수 있다. </p>
<p>멀티스레딩으로 해결했다:</p>
<ul>
<li><strong>메인 스레드</strong>: GUI 업데이트 (60fps)  </li>
<li><strong>DAQ 스레드</strong>: 센서 데이터 수집 (100Hz)</li>
<li><strong>AI 스레드</strong>: 분류 작업 (실시간)</li>
<li><strong>로그 스레드</strong>: 데이터 저장 (백그라운드)</li>
</ul>
<p>프론트엔드는 PyQt5로 구성했다. <strong>tkinter도 고려했는데 UI가 너무 단조롭고, Kivy는 모바일 중심이라 데스크톱용으로는 PyQt5가 최적</strong>이었다.</p>
<p>AI 모델은 간단한 CNN으로 만들었다. 차후 연구에 맞춰서 바꿔나갈 예정이다ㅏ.</p>
<h2 id="🎉-결과와-배운-점">🎉 결과와 배운 점</h2>
<p>AI를 개발에 활용하는 방법을 좀 더 구체적으로 알게 됐다. 코드 전체를 짜달라고 하는 것보다는 내가 고민하고 있는 기술적 선택지들에 대해 조언을 구하거나, UI 디자인 같은 내가 약한 부분을 보완하는 용도로 쓰는 게 효과적이었다.</p>
<p>그리고 요즘 나온 빌드 도구들이 정
말 좋아졌다는 걸 실감했다. 예전에는 실행 파일 하나 만들려면 복잡한 설정 파일 작성하고 이것저것 해야 했는데, 지금은 몇 줄 명령어면 끝이다.</p>
<p>개발 환경과 실제 사용 환경의 차이도 다시 한번 느꼈다. 혼자 테스트할 때는 문제없던 게 다른 컴퓨터에서는 여러 문제가 생기더라. 특히 라이브러리 의존성이 복잡한 경우 패키징을 더 꼼꼼하게 해야겠다는 생각이 들었다.</p>
<p>처음에 기술 스택 고민하는데 시간을 너무 많이 썼다. tkinter가 익숙해서 만들었는데 너무 단조로웠다. 그냥 빨리 만들어보고 바꾸길 잘했다. 완벽한 설계보다는 일단 돌아가는 걸 만드는 게 중요하다는 걸 또 한 번 느꼈다.</p>
<p>결과적으로는 만족스럽다. 내가 원하던 기능은 다 구현했고, 배포도 잘 됐고, 실제로 다른 사람들도 쓰고 있다. AI 모델을 실제 프로그램으로 만들어서 배포까지 해볼 수 있는 좋은 경험이었다.</p>
<h2 id="📁-프로젝트-파일들">📁 프로젝트 파일들</h2>
<h3 id="핵심-파일">핵심 파일</h3>
<ul>
<li><code>array_control_system.py</code> - 메인 GUI 애플리케이션  </li>
<li><code>mnist_cnn_model.py</code> - CNN 모델 정의</li>
<li><code>models/mnist_cnn_model.pth</code> - 훈련된 모델 가중치</li>
<li><code>ArrayControlSystem.spec</code> - PyInstaller 빌드 설정</li>
</ul>
<h3 id="빌드-스크립트">빌드 스크립트</h3>
<ul>
<li><code>build_all_platforms.yml</code> - GitHub Actions CI/CD</li>
<li><code>requirements.txt</code> - Python 패키지 의존성</li>
<li><code>README.md</code> - 사용법 및 설치 가이드</li>
</ul>
<p>앞으로는 더 복잡한 AI 모델도 실행 파일로 만들어보고 싶다. 그리고 웹 버전도 만들어서 브라우저에서 바로 쓸 수 있게 해보는 것도 재밌을 것 같다! 🎯</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[파이썬 라이브러리(Cython,NetworkX,Louvain,Python-TSP| 성능 최적화와 그래프 알고리즘]]></title>
            <link>https://velog.io/@yujin_jeong/%ED%8C%8C%EC%9D%B4%EC%8D%AC-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%ACCythonNetworkXLouvainPython-TSP-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94%EC%99%80-%EA%B7%B8%EB%9E%98%ED%94%84-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</link>
            <guid>https://velog.io/@yujin_jeong/%ED%8C%8C%EC%9D%B4%EC%8D%AC-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%ACCythonNetworkXLouvainPython-TSP-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94%EC%99%80-%EA%B7%B8%EB%9E%98%ED%94%84-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</guid>
            <pubDate>Tue, 08 Jul 2025 02:05:36 GMT</pubDate>
            <description><![CDATA[<h2 id="cython-python의-속도-한계를-돌파하다">Cython: Python의 속도 한계를 돌파하다</h2>
<p><a href="https://cython.org/">Cython</a>은 Python 코드를 C로 컴파일하여 네이티브 속도로 실행할 수 있게 해주는 라이브러리다. 실제로 몬테카를로 시뮬레이션 코드를 Cython으로 변환했을 때 약 85배까지도 개선된다. 특히 중첩된 반복문이 많은 알고리즘에서는 더욱 극적인 개선을 보여준다.</p>
<p>Cython이 고성능을 달성하는 핵심은 Python의 동적 타이핑 오버헤드를 제거하는 것이다. <code>cdef int i, j</code>와 같은 정적 타입 선언을 통해 변수 타입 체크를 컴파일 타임에 수행하고, C 컴파일러의 최적화를 활용한다. 메모리 뷰(memoryview)를 사용하면 NumPy 배열 접근 시 Python 래퍼를 우회하여 직접 메모리에 접근할 수 있다. 이는 특히 픽셀 단위 이미지 처리나 대규모 행렬 연산에서 체감할 수 있는 성능 향상을 가져다준다.</p>
<h2 id="networkx-그래프-이론의-강력한-도구">NetworkX: 그래프 이론의 강력한 도구</h2>
<p><a href="https://networkx.org/">NetworkX</a>는 복잡한 네트워크의 구조, 동역학, 기능을 연구하기 위한 Python 패키지다. 소셜 네트워크 분석 프로젝트에서 10만 개 노드를 가진 그래프의 중심성 계산을 수행했을 때, NetworkX의 최적화된 알고리즘 구현이 순수 Python 구현보다 약 40배 빠른 성능을 보여주었다.</p>
<p>NetworkX의 성능 우위는 효율적인 그래프 자료구조 설계에서 나온다. 내부적으로 인접 리스트를 딕셔너리의 딕셔너리로 구현하여 O(1) 시간에 노드와 엣지에 접근할 수 있다. 또한 많은 알고리즘들이 C로 구현된 하위 레벨 함수들을 활용한다. 예를 들어, 최단 경로 계산에서는 Dijkstra 알고리즘의 핵심 부분을 C로 구현하여 힙 연산의 오버헤드를 최소화했다.</p>
<h2 id="python-louvain-커뮤니티-탐지의-최적화된-구현">python-louvain: 커뮤니티 탐지의 최적화된 구현</h2>
<p>Louvain 알고리즘은 네트워크에서 커뮤니티 구조를 찾는 휴리스틱 방법이다. 
python-louvain의 성능은 모듈성 최적화 과정의 지능적인 구현에서 나온다. 알고리즘은 각 노드를 인접한 커뮤니티로 이동시키면서 모듈성 증가를 계산하는데, 이 과정에서 증분 계산(incremental computation)을 사용한다. 전체 모듈성을 다시 계산하는 대신 변화분만을 계산하여 O(n²)에서 O(n log n)으로 시간 복잡도를 줄였다. 또한 커뮤니티가 안정화되면 해당 부분의 계산을 건너뛰는 조기 종료 최적화를 적용한다.</p>
<h2 id="python-tsp-외판원-문제의-효율적-해결">python-tsp: 외판원 문제의 효율적 해결</h2>
<p>TSP(Traveling Salesman Problem)는 조합 최적화의 대표적인 NP-hard 문제다. 물류 최적화 프로젝트에서 100개 도시를 가진 TSP 문제를 해결했을 때, python-tsp의 Christofides 알고리즘이 순수 브루트 포스 방법보다 수천 배 빠른 성능을 보여주었다.</p>
<p>python-tsp의 성능은 정교한 휴리스틱 알고리즘과 가지치기 기법의 조합에서 나온다. Held-Karp 하한을 사용한 분기한정법에서는 부분 해의 하한이 현재 최적해보다 크면 해당 분기를 즉시 제거한다. 또한 2-opt, 3-opt 같은 지역 검색 최적화를 통해 초기 해를 개선하고, 이를 분기한정법의 초기 상한으로 사용하여 가지치기 효과를 극대화한다.</p>
<h2 id="pyarrow-컬럼형-데이터-처리의-혁명">PyArrow: 컬럼형 데이터 처리의 혁명</h2>
<p><a href="https://arrow.apache.org/docs/python/">PyArrow</a>는 Apache Arrow 프로젝트의 Python 구현체로, 컬럼형 인메모리 분석을 위한 플랫폼이다. 실제로 1억 행 규모의 CSV 파일을 처리했을 때, PyArrow의 parquet 읽기가 pandas의 CSV 읽기보다 약 15배 빠른 성능을 보여주었다. 특히 필터링 작업에서는 컬럼형 구조의 장점이 극명하게 드러난다.</p>
<p>PyArrow의 성능 우위는 메모리 레이아웃 최적화에서 나온다. 컬럼형 저장 방식은 동일한 타입의 데이터를 연속적으로 배치하여 CPU 캐시의 지역성을 극대화한다. 또한 SIMD(Single Instruction, Multiple Data) 명령어를 활용하여 벡터화된 연산을 수행한다. 예를 들어, 정수 배열의 합계를 계산할 때 한 번에 8개의 값을 처리할 수 있다. 또한 컬럼 프루닝(column pruning)과 술부 푸시다운(predicate pushdown) 최적화를 통해 불필요한 데이터 읽기를 방지한다.</p>
<h2 id="numexpr-수치-표현식-계산의-가속화">NumExpr: 수치 표현식 계산의 가속화</h2>
<p><a href="https://numexpr.readthedocs.io/">NumExpr</a>는 NumPy 배열에 대한 수치 표현식을 빠르게 계산하는 라이브러리다. 실제로 <code>a * b + c * d</code>와 같은 복잡한 배열 연산을 수행했을 때, NumExpr이 순수 NumPy보다 약 3-4배 빠른 성능을 보여주었다. 특히 메모리 사용량이 많은 대규모 배열에서는 더욱 두드러진 차이를 보인다.</p>
<p>NumExpr의 성능 비결은 표현식 트리 최적화와 메모리 접근 패턴 개선에 있다. 전통적인 NumPy 연산은 각 연산마다 중간 결과를 메모리에 저장하지만, NumExpr은 전체 표현식을 하나의 루프로 융합한다. 이를 통해 메모리 대역폭을 최대한 활용하고 캐시 미스를 최소화한다. 또한 OpenMP를 사용한 자동 병렬 처리로 멀티코어 프로세서의 성능을 완전히 활용한다. 특히 메모리 바운드 연산에서는 스레드 수만큼 성능이 향상된다.</p>
<h2 id="bottleneck-numpy-배열-연산의-터보-차저">Bottleneck: NumPy 배열 연산의 터보 차저</h2>
<p><a href="https://bottleneck.readthedocs.io/">Bottleneck</a>은 NumPy 배열을 위한 빠른 NumPy 배열 함수들의 모음이다. 시계열 데이터 분석에서 이동 평균을 계산할 때, Bottleneck의 <code>move_mean</code> 함수가 pandas의 <code>rolling().mean()</code>보다 약 10배 빠른 성능을 보여주었다. 특히 NaN 값이 포함된 배열에서는 더욱 뛰어난 성능을 발휘한다.</p>
<p>Bottleneck의 성능은 C로 구현된 템플릿 기반 알고리즘에서 나온다. 각 데이터 타입(int32, int64, float32, float64)에 특화된 최적화된 루프를 생성하여 Python과 NumPy의 오버헤드를 최소화한다. 이동 창 계산에서는 전체 창을 다시 계산하는 대신 새로운 값을 추가하고 오래된 값을 제거하는 증분 계산을 사용한다. 또한 NaN 값 처리를 위한 특별한 최적화가 구현되어 있어, 실제 데이터에서 자주 발생하는 결측값 상황에서도 안정적인 성능을 보장한다.</p>
<p>이러한 라이브러리들을 실제 프로젝트에서 적용하면서 깨달은 것은, 단순히 라이브러리를 바꾸는 것만으로도 상당한 성능 향상을 얻을 수 있다는 점이다. 특히 데이터 처리 파이프라인에서 병목 지점을 찾아 적절한 라이브러리로 교체하는 것이 가장 효과적인 최적화 방법이었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[🔍 BFS vs DFS]]></title>
            <link>https://velog.io/@yujin_jeong/BFS-vs-DFS</link>
            <guid>https://velog.io/@yujin_jeong/BFS-vs-DFS</guid>
            <pubDate>Thu, 03 Jul 2025 05:53:57 GMT</pubDate>
            <description><![CDATA[<p>GNN공부하러왔다가 봉변 맞기
-&gt; 맨날 푸는 BFS, DFS 또 틀리다~</p>
<p>진짜 <strong>BFS, DFS 문제</strong>는 좀만 복잡해져도 무한 틀림임~</p>
<p>저는 코테 가기전에는 꼭 간단한 예제로 외워서 적용하자! 라는 마인드로 가요</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>BFS (Breadth-First Search)</th>
<th>DFS (Depth-First Search)</th>
</tr>
</thead>
<tbody><tr>
<td>탐색 방식</td>
<td>가까운 노드부터 차례대로 탐색</td>
<td>깊은 노드부터 끝까지 탐색 후 되돌아옴</td>
</tr>
<tr>
<td>자료구조</td>
<td>큐 (Queue)</td>
<td>스택(Stack) or 재귀(Recursion)</td>
</tr>
<tr>
<td>사용 예시</td>
<td><strong>최단 거리</strong>, 퍼짐 현상 등</td>
<td><strong>영역 탐색</strong>, 조합 탐색, 경로 존재 여부</td>
</tr>
<tr>
<td>구현 난이도</td>
<td>비교적 쉬움</td>
<td>재귀 호출에 익숙해야 함</td>
</tr>
<tr>
<td>대표 예제</td>
<td>미로에서 최단 경로</td>
<td>얼음 덩어리 개수 세기</td>
</tr>
</tbody></table>
<hr>
<h2 id="✅-bfs-문제-미로-탈출">✅ BFS 문제: 미로 탈출</h2>
<h3 id="문제-설명">문제 설명</h3>
<p>N x M 크기의 미로가 0과 1로 이루어져 있습니다.</p>
<ul>
<li><code>1</code>은 이동 가능한 길, <code>0</code>은 벽입니다.</li>
<li>(0, 0)에서 출발하여 (N-1, M-1)까지 <strong>최단 거리</strong>로 이동할 때 지나야 하는 칸의 수를 구하세요.</li>
<li>상하좌우로만 이동 가능합니다.</li>
</ul>
<h3 id="예제-입력">예제 입력</h3>
<pre><code>5 6
101010
111111
000001
111111
111111</code></pre><h3 id="예제-출력">예제 출력</h3>
<pre><code>10</code></pre><hr>
<h3 id="풀이-코드-bfs">풀이 코드 (BFS)</h3>
<pre><code class="language-python">from collections import deque

def bfs_maze_escape(maze):
    n = len(maze)
    m = len(maze[0])
    visited = [[False]*m for _ in range(n)]

    dx = [-1, 1, 0, 0]  # 상하좌우
    dy = [0, 0, -1, 1]

    queue = deque()
    queue.append((0, 0))
    visited[0][0] = True

    while queue:
        x, y = queue.popleft()

        for i in range(4):
            nx = x + dx[i]
            ny = y + dy[i]

            if 0 &lt;= nx &lt; n and 0 &lt;= ny &lt; m and not visited[nx][ny]:
                if maze[nx][ny] == 1:
                    queue.append((nx, ny))
                    visited[nx][ny] = True
                    maze[nx][ny] = maze[x][y] + 1  # 이전 칸 거리 + 1

    return maze[n-1][m-1]</code></pre>
<h3 id="🧠-함수-설명">🧠 함수 설명</h3>
<ul>
<li><code>visited</code>: 이미 방문한 노드를 다시 방문하지 않기 위한 체크.</li>
<li><code>queue</code>: BFS 탐색을 위한 큐.</li>
<li><code>maze[x][y] = maze[x][y] + 1</code>: 최단 거리 누적.</li>
</ul>
<hr>
<h2 id="✅-dfs-문제-얼음-얼리기">✅ DFS 문제: 얼음 얼리기</h2>
<h3 id="문제-설명-1">문제 설명</h3>
<p>N x M 크기의 얼음 틀에서</p>
<ul>
<li><code>0</code>: 얼음이 생성될 수 있는 칸</li>
<li><code>1</code>: 벽</li>
</ul>
<p>상하좌우로 연결된 0 묶음을 얼음 1개로 본다면, 전체 얼음 덩어리는 몇 개인가요?</p>
<h3 id="예제-입력-1">예제 입력</h3>
<pre><code>4 5
00110
00011
11111
00000</code></pre><h3 id="예제-출력-1">예제 출력</h3>
<pre><code>3</code></pre><hr>
<h3 id="풀이-코드-dfs">풀이 코드 (DFS)</h3>
<pre><code class="language-python">def dfs(x, y, graph):
    n = len(graph)
    m = len(graph[0])

    if x &lt; 0 or x &gt;= n or y &lt; 0 or y &gt;= m:
        return False
    if graph[x][y] == 0:
        graph[x][y] = 1  # 방문 처리
        dfs(x-1, y, graph)
        dfs(x+1, y, graph)
        dfs(x, y-1, graph)
        dfs(x, y+1, graph)
        return True
    return False

def count_ice_areas(graph):
    n = len(graph)
    m = len(graph[0])
    count = 0

    for i in range(n):
        for j in range(m):
            if dfs(i, j, graph):
                count += 1
    return count</code></pre>
<h3 id="🧠-함수-설명-1">🧠 함수 설명</h3>
<ul>
<li><code>dfs</code>: 현재 위치에서 인접한 0을 재귀적으로 탐색.</li>
<li>한 번의 DFS 호출 = 얼음 1덩어리 탐색 완료.</li>
<li>방문한 위치는 <code>1</code>로 바꿔 재탐색 방지.</li>
</ul>
<hr>
<p>각 코드에서 </p>
<h3 id="dfs가-쓰이는-이유">DFS가 쓰이는 이유</h3>
<p>👉 <strong>&quot;끝까지 들어가서 하나의 연결된 덩어리를 다 보고 나오는 방식&quot;이 필요할 때</strong>
예: 연결된 구역, 조합, 경로 존재 여부 등</p>
<blockquote>
<p>&quot;갈 수 있을 만큼 깊이 파고들어, 전체를 한 번에 확인하고 온다.&quot;</p>
</blockquote>
<hr>
<h3 id="bfs가-쓰이는-이유">BFS가 쓰이는 이유</h3>
<p>👉 <strong>&quot;가장 가까운 경로를 먼저 탐색하고, 단계별로 넓혀가는 방식&quot;이 필요할 때</strong>
예: 최단 거리, 최소 횟수, 레벨 탐색 등</p>
<blockquote>
<p>&quot;한 칸씩, 가까운 곳부터 차례대로 탐색해서 가장 빠른 길을 찾는다.&quot;</p>
</blockquote>
<hr>
<h3 id="🎯-핵심-비교-한-줄-요약">🎯 핵심 비교 한 줄 요약</h3>
<ul>
<li><strong>DFS</strong>는 &quot;끝까지 파고들며 탐색해야 할 때&quot;</li>
<li><strong>BFS</strong>는 &quot;가장 빠른 길(최단 거리)을 찾을 때&quot;</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[MOVIE SHELF | AI로 영화플리 서비스 만들어본 후기]]></title>
            <link>https://velog.io/@yujin_jeong/MOVIE-SHELF-AI%EB%A1%9C-%EC%98%81%ED%99%94%ED%94%8C%EB%A6%AC-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B8-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@yujin_jeong/MOVIE-SHELF-AI%EB%A1%9C-%EC%98%81%ED%99%94%ED%94%8C%EB%A6%AC-%EC%84%9C%EB%B9%84%EC%8A%A4-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B8-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Sun, 29 Jun 2025 08:36:44 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>MOVIESHELF 서비스 바로 가기
<a href="https://movieshelf.store/">https://movieshelf.store/</a></p>
</blockquote>
<p>최근에 OTT 과소비하고 있다는 생각이 들었다. 아니? 했다. 딱 한달이긴 했지만 넷플릭스, 티빙, 유튭프리미엄, 쿠팡와우까지 구독하고 있는데 아까워서라도 영화를 많이 보자고 마음먹었다.</p>
<p>평소에 영화를 열심히 보진 않았어서 인스타그램의 영화 계정들을 많이 팔로우했다. 근데 단점이 있더라. 포스팅에 이끌려 다니는 느낌이랄까? 추천 영화들이 너무 겹치는 것도 많고, 나중에 보고 싶은 영화를 따로 리스트업할 수도 없었다. </p>
<p>또 부부부계(부계)에 저장해놨는데 나중에 찾아보려면 스크롤을 한참 내려야 하고 정리도 안 되어 있어서 불편했다. 그냥 내가 쓰고 싶은 걸 만들어보자 싶어서 시작했다.</p>
<p>요구사항은 간단했다. </p>
<blockquote>
<p><strong>로그인 없이 쓸 수 있고, 길게 리뷰 안 써도 되고, 내가 본 영화들을 깔끔하게 리스트로 정리</strong>할 수 있으면 됐다. 왓챠피디아처럼 길게 후기를 쓰고 싶지는 않았다.</p>
</blockquote>
<h2 id="기획">기획</h2>
<p><img src="https://velog.velcdn.com/images/yujin_jeong/post/1a8180f4-6728-4eb0-aee7-ceb799e19885/image.png" alt=""></p>
<p>아이디어 자체는 복잡하지 않아서 초기 기획은 금방 끝났다.
영화 검색하고, 평점 매기고, 간단한 코멘트 남기고, 리스트로 보여주는 정도.</p>
<p>내가 만든 웹이 현재 MVP수준이라.. MMVP는 이번에도 두시간만에 끝났다.
TMDB api로 슥슥 만들었다.</p>
<p>문제는 구체적인 기술 요소들을 정하는 거였다. DB 스키마 어떻게 짤지, ID는 nanoid 쓸지 UUID 쓸지, 이런 것들 고민하는데 시간이 오래 걸렸다. </p>
<p>특히 MongoDB 스키마 설계에서 고민이 많았다. 사용자별로 영화 리스트를 어떻게 저장할지, 영화 정보는 TMDB에서 가져온 걸 그대로 저장할지 아니면 필요한 부분만 뽑아서 저장할지... 결국 사용자 컬렉션과 영화 컬렉션을 분리하고, 사용자 컬렉션에는 영화 ID와 개인 평점/코멘트만 저장하는 방식으로 정했다.</p>
<p>ID 생성은 nanoid로 결정했다. UUID보다 짧고 URL에 들어가도 깔끔해서. </p>
<p>근데 개발하다 보니까 스키마를 몇 번 바꿔야 하는 상황이 생겼다. 처음엔 단순하게 설계했는데 기능 추가하면서 필드를 더 넣어야 했고... 그래서 DB 마이그레이션 코드도 작성했다. MongoDB는 스키마가 유연하긴 하지만 그래도 기존 데이터 구조 바꾸려면 마이그레이션이 필요하더라. </p>
<p>문제는 마이그레이션 스크립트가 복잡해져서 그냥 기존 데이터를 날려버렸다는 거다. (인스타 올리고 나서라 중간에 서비스도 끊김ㅠ) 어차피 테스트 데이터니까 괜찮다고 생각했는데 나중에 좀 아쉬웠다. 하하, 이거 까먹을 뻔했네.</p>
<h2 id="예상-밖의-반응">예상 밖의 반응</h2>
<p>완성하고 나서 인스타 스토리에 올렸는데 반응이 생각보다 컸다. 한 시간 만에 유입 횟수가 1500번을 넘었다. 사람들이 생각보다 관심이 많았나 보다. </p>
<p>문제는 갑자기 트래픽이 몰리면서 로컬 테스트할 때 못 잡은 오류들이 터져나왔다는 거다. 동시 접속자가 많아지니까 API 응답 속도가 느려지고, 몇몇 엣지 케이스에서 에러가 발생했다. 특히 TMDB API 호출 제한에 걸리는 경우를 제대로 처리 안 해놔서 사용자들이 검색이 안됐고...</p>
<p>급하게 에러 핸들링 코드 추가하고 로딩 스테이트도 제대로 구현했다. 그리고 Google Analytics 4도 달아서 실제 사용 패턴을 분석할 수 있게 했다. </p>
<h2 id="기술-스택-및-설계">기술 스택 및 설계</h2>
<p>프론트엔드는 Vite + React로 구성했다. Vite 쓰니까 개발 서버도 빠르고 빌드도 금방 끝나서 좋았다. 스타일링은 Tailwind CSS 사용했는데, 유틸리티 클래스 방식이 처음엔 어색했지만 익숙해지니까 CSS 파일 따로 안 만들어도 돼서 편했다.</p>
<p>사용자 플로우를 정리하려고 PlantUML로 다이어그램도 그려봤다. 
그리다보니...</p>
<p><img src="https://velog.velcdn.com/images/yujin_jeong/post/63b551da-1765-43ae-865f-6ecb0eebd2c8/image.png" alt="">
하... 이렇게 됐다.</p>
<h2 id="디자인">디자인</h2>
<p>처음에는 그냥 HTML/CSS로 대충 만들어보려고 했는데, 디자인 감각이 없다 보니까 시간만 날렸다. 그래서 Claude한테 도움을 요청했다. &quot;스포티파이 느낌으로 만들어줘&quot; 하니까 훨씬 깔끔하게 나왔다.</p>
<p>다크 테마 기반에 그라데이션 배경, 카드 스타일의 영화 리스트가 핵심이었다. Tailwind의 그라데이션 클래스들(<code>bg-gradient-to-r from-purple-500 to-pink-500</code> 같은)을 활용해서 스포티파이스러운 느낌을 냈다.</p>
<p>이게 AI 쓰는 가장 큰 장점인 것 같다. 내가 못하는 부분을 빠르게 보완할 수 있다는 것.</p>
<h2 id="개발-및-배포">개발 및 배포</h2>
<p>TMDB API 써서 영화 데이터 가져오고, Vercel로 프론트 배포하고, MongoDB Atlas랑 Cloudtype으로 백엔드 올렸다.</p>
<p><img src="https://velog.velcdn.com/images/yujin_jeong/post/f76336bb-b6a5-4b0c-bef3-d47188706baa/image.png" alt="">
이게 ** Mogo Atlas **
<img src="https://velog.velcdn.com/images/yujin_jeong/post/c846cf6a-c2be-4ae5-84e6-79e1c0ab1fb7/image.png" alt="">
이게 <strong>cloudtype</strong>
<img src="https://velog.velcdn.com/images/yujin_jeong/post/364e4766-1e29-40a9-a0ed-848a724620ee/image.png" alt="">
이게 <strong>vercel</strong></p>
<p>백엔드는 Node.js + Express로 간단하게 구성했다. API 엔드포인트는 몇 개 안 되니까 복잡한 구조 없이 그냥 라우터 몇 개로 처리했다. TMDB API 호출하고, MongoDB에 사용자 데이터 저장하고, 프론트에서 요청한 데이터 리턴하는 정도.</p>
<p>배포 과정에서 CORS 에러가 몇 번 났는데, Vercel 도메인과 Cloudtype 도메인 간의 통신에서 문제가 생겼다. 백엔드에서 cors 미들웨어 설정할 때 origin을 제대로 안 적어서 그런 거였다. 이것만 해결하고 나니까 별 문제 없이 잘 돌아갔다. AWS 써봤을 때보다 훨씬 간단했다. 설정할 게 적고 무료 플랜도 넉넉해서 개인 프로젝트로는 충분했다.</p>
<p>AWS는 EC2 인스턴스 설정부터 시작해서 RDS, S3, CloudFront 등등 연결할 게 많고 복잡한데, 지금 쓴 서비스들은 그냥 몇 번 클릭하면 끝이다. 물론 AWS가 더 많은 기능을 제공하긴 하지만, 단순한 웹앱 하나 배포하는데는 오버스펙이다.</p>
<h2 id="배운-점">배운 점</h2>
<p>AI를 개발에 활용하는 방법을 좀 더 구체적으로 알게 됐다. 코드 짜달라고 하는 것보다는 내가 고민하고 있는 기술적 선택지들에 대해 조언을 구하거나, 디자인 같은 내가 약한 부분을 보완하는 용도로 쓰는 게 효과적이었다.
그리고 요즘 나온 배포 플랫폼들이 정말 좋아졌다는 걸 실감했다. 예전에는 서버 하나 띄우려면 SSH 접속해서 nginx 설정하고 이것저것 해야 했는데, 지금은 그런 게 필요 없다.</p>
<p>로컬 환경과 실제 운영 환경의 차이도 다시 한번 느꼈다. 혼자 테스트할 때는 문제없던 게 실제 사용자가 몰리니까 여러 문제가 생기더라. 특히 외부 API 의존성이 있는 경우 에러 핸들링을 더 촘촘하게 해야겠다는 생각이 들었다.</p>
<h2 id="아쉬운-점">아쉬운 점</h2>
<p>처음에 기술 스택 고민하는데 시간을 너무 많이 썼다. 그냥 빨리 만들어보고 나중에 필요하면 바꾸는 게 나았을 것 같다. 완벽한 설계보다는 일단 돌아가는 걸 만드는 게 중요하다는 걸 또 한 번 느꼈다.</p>
<p>그리고 사용자 테스트를 제대로 안 해봐서 실제로 쓰기 편한지는 모르겠다. 나 혼자 쓰려고 만든 거지만 그래도 몇 명한테 써보라고 할 걸 그랬다.</p>
<h2 id="마무리">마무리</h2>
<p>결과적으로는 만족스럽다. 내가 원하던 기능은 다 구현했고, 배포도 잘 됐고, 실제로 쓰고 있다. AI 도움도 적절히 받으면서 혼자서도 충분히 괜찮은 서비스를 만들 수 있다는 걸 확인했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Lovart AI와 LLM의 사고 메커니즘 ]]></title>
            <link>https://velog.io/@yujin_jeong/Lovart-AI%EC%99%80-LLM%EC%9D%98-%EC%82%AC%EA%B3%A0-%EB%A9%94%EC%BB%A4%EB%8B%88%EC%A6%98</link>
            <guid>https://velog.io/@yujin_jeong/Lovart-AI%EC%99%80-LLM%EC%9D%98-%EC%82%AC%EA%B3%A0-%EB%A9%94%EC%BB%A4%EB%8B%88%EC%A6%98</guid>
            <pubDate>Sun, 15 Jun 2025 01:34:22 GMT</pubDate>
            <description><![CDATA[<p>2025년 베타로 출시된 Lovart는 단순한 AI 디자인 툴을 넘어선 흥미로운 케이스다. 텍스트 프롬프트 한 줄로 브랜드 아이덴티티부터 영상, 음악까지 생성하는 &#39;세계 최초 디자인 AI 에이전트&#39;라는 타이틀을 내세우고 있지만, 정작 주목할 점은 그 내부 동작 방식이다.</p>
<p>사실 Lovart를 몇 주간 사용해보면서 가장 인상적이었던 건 <strong>프롬프트 작성 방식에 따라 결과물의 품질이 극명하게 갈린다는 점</strong>이었다. 이는 단순히 도구의 한계가 아니라, 최신 LLM 연구에서 밝혀진 언어 모델의 <strong>추론(reasoning) 메커니즘</strong>과 깊은 연관이 있다.<img src="https://velog.velcdn.com/images/yujin_jeong/post/da8be45f-a084-4027-b326-9bf0bffeef8f/image.png" alt=""></p>
<h2 id="lovart의-핵심-talk-tab-tune-워크플로우">Lovart의 핵심: Talk-Tab-Tune 워크플로우</h2>
<p>Lovart의 독특한 점은 <strong>Talk-Tab-Tune</strong>이라는 3단계 워크플로우다. 사용자가 자연어로 요구사항을 설명(Talk)하면, 시스템이 여러 옵션을 제시(Tab)하고, 최종적으로 세부 조정(Tune)을 진행한다.</p>
<blockquote>
<p>Talk 그냥 말로 요구하면
Tab 옵션을 툭툭 던져주고
Tune 고른 걸 기준으로 세부 조정하는 구조</p>
</blockquote>
<p>이게 단순한 UI 패턴처럼 보여도, 사실 요즘 LLM이 문제를 푸는 방식이랑 아주 닮아 있다.</p>
<p>🤔 예를 들어
복잡한 요구사항을 덩어리째 받아들이는 게 아니라,
‘기획 → 스케치 → 디자인 → 출력’ 식으로
내부적으로 쪼개서 처리하는 느낌.</p>
<h2 id="프롬프트-구체성이-결과에-미치는-영향-실제-비교">프롬프트 구체성이 결과에 미치는 영향: 실제 비교</h2>
<h3 id="추상적-프롬프트의-한계">추상적 프롬프트의 한계</h3>
<pre><code>입력: &quot;립스틱 광고 만들어줘&quot;</code></pre><p>이같은 단순한 프롬프트는 예측 가능한 결과를 낳는다. Lovart는 일반적인 화장품 광고 템플릿을 활용해 평범한 시안들을 제시한다. 사용자 리뷰를 살펴보면 이런 경우 &quot;결과물이 기대에 못 미친다&quot;는 평가가 많았다.
다만, Lovart는 이런 내용을 검색하고, 사고하고, 기획하는 과정을 보여주며 여러 선택지를 제시한다.</p>
<p>앱이나 웹을 개발할 때 컨셉을 잡을 때 많이 사용했다.
<img src="https://velog.velcdn.com/images/yujin_jeong/post/01dd4013-370d-42d1-9b50-986779722951/image.png" alt=""></p>
<pre><code>입력: &quot;Dior 립스틱 이미지를 참고해서, 30초 이내의 고급스러운 광고 영상 클립을 만들어줘. 
Dior, Chanel, Lancôme 스타일 참고. 강렬한 레드 컬러, 럭셔리 브랜딩 강조&quot;</code></pre><p><img src="https://velog.velcdn.com/images/yujin_jeong/post/a8662333-38eb-4758-a220-7fa330e96fc4/image.png" alt="">
동일한 작업이지만 <strong>브랜드 레퍼런스</strong>, <strong>시간 제약</strong>, <strong>스타일 가이드</strong>, <strong>컬러 팔레트</strong> 등 구체적 맥락을 제공하면 결과가 완전히 달라진다. 스토리보드부터 씬 전환, 효과까지 포함된 완성도 높은 영상이 생성된다.</p>
<p>참고로 앱 만들 때는</p>
<pre><code>1. 목적: 어떤 서비스인지
2. 타겟: 주요 사용자층
3. 무드/스타일: 분위기, 톤, 브랜드 방향
4. 구성요소: 페이지/화면 구성, 포함할 요소들
5. 제약조건: 색상, 폰트, 해상도, 디바이스, 브랜드 가이드
6. 참고자료: 참고 브랜드, 이미지, 키워드 등
</code></pre><p>이렇게 작성했다. </p>
<hr>
<h2 id="llm-추론-메커니즘과의-연결점">LLM 추론 메커니즘과의 연결점</h2>
<h3 id="1-작업-분해task-decomposition">1. 작업 분해(Task Decomposition)</h3>
<p>최근 LLM 연구에서 주목받는 <strong>least-to-most prompting</strong> 전략이 Lovart에서도 확인된다. 복잡한 디자인 요구를 자동으로 하위 작업들로 분해하는 방식이다.</p>
<blockquote>
<p>📚 <strong>연구 근거</strong><br>Chain-of-Thought 논문(Wei et al., 2022)에서 밝혀진 바와 같이, LLM은 복잡한 문제를 단계별로 분해할 때 성능이 크게 향상된다.</p>
</blockquote>
<p>Lovart는 이를 실제 디자인 워크플로우에 적용한다:</p>
<ol>
<li><strong>기획 단계</strong>: 브랜드 분석, 타겟 설정</li>
<li><strong>스케치 단계</strong>: 레이아웃, 컬러 팔레트 결정  </li>
<li><strong>디자인 단계</strong>: 세부 요소 생성</li>
<li><strong>출력 단계</strong>: 최종 결과물 렌더링</li>
</ol>
<h3 id="2-사고의-명시화chain-of-thought">2. 사고의 명시화(Chain-of-Thought)</h3>
<pre><code class="language-javascript">// 효과적인 프롬프트 구조 예시
const effectivePrompt = `
작업: 여성용 스킨케어 브랜드 키트 생성
스타일: 고급스러운, 미니멀
색상: 은은한 블루 계열, 자연광 느낌
구성요소: 로고, 컬러 팔레트, 타이포그래피, 포스터, SNS 템플릿
비율: 1:1 (인스타그램용)
참고: 자연 친화적, 20-30대 여성 타겟
`;</code></pre>
<p>이처럼 <strong>의도→스타일→구성요소→제약조건</strong> 순으로 사고 과정을 명시하면, Lovart가 각 단계를 논리적으로 처리한다.</p>
<h3 id="3-문맥-일관성-유지contextual-consistency">3. 문맥 일관성 유지(Contextual Consistency)</h3>
<p>Lovart의 놀라운 점 중 하나는 <strong>전체 작업 과정에서 일관성을 유지</strong>한다는 것이다. 첫 번째 시안에서 선택한 색상이나 폰트가 후속 작업물에도 자연스럽게 반영된다.</p>
<blockquote>
<p>💡 <strong>메커니즘 분석</strong><br>이는 Transformer 기반 LLM의 <strong>어텐션 메커니즘</strong>이 장거리 의존성을 잘 포착하기 때문으로 보인다. 이전 선택들이 후속 결정에 영향을 미치는 방식이다.</p>
</blockquote>
<h2 id="창의적이고-정확한-결과를-이끄는-프롬프트-구조">창의적이고 정확한 결과를 이끄는 프롬프트 구조</h2>
<h3 id="1-구체적이고-맥락이-풍부한-프롬프트">1. 구체적이고 맥락이 풍부한 프롬프트</h3>
<p><strong>명확한 목적</strong>과 <strong>세부 정보</strong>가 핵심이다. 단순히 &quot;포스터 만들어줘&quot;가 아니라 다음과 같은 요소들을 포함해야 한다:</p>
<ul>
<li><strong>목적과 분위기</strong>: &quot;여름 음악 페스티벌을 위한 활기차고 시원한 분위기&quot;</li>
<li><strong>스타일 가이드</strong>: 색상, 폰트, 참고 브랜드</li>
<li><strong>기술적 제약</strong>: 비율, 사용 목적(SNS/인쇄), 텍스트 내용</li>
<li><strong>참고 자료</strong>: 이미지, 키워드, 예시 문구</li>
</ul>
<h3 id="2-단계적-요구와-옵션-제시">2. 단계적 요구와 옵션 제시</h3>
<p><strong>작업 분해</strong> 방식을 활용하면 각 단계에서 창의성과 정확성이 모두 높아진다:</p>
<pre><code>1단계: &quot;기획안부터 써줘&quot;
2단계: &quot;시안 3개 만들어줘&quot; 
3단계: &quot;2번 시안을 세부 조정해줘&quot;</code></pre><p><strong>선택지 제공</strong>도 효과적이다:</p>
<pre><code>&quot;미니멀/빈티지/팝아트 스타일 중에서 2가지 스타일로 만들어줘&quot;</code></pre><h3 id="3-창의성-유도를-위한-표현-기법">3. 창의성 유도를 위한 표현 기법</h3>
<p><strong>비유적·감성적 언어</strong>가 LLM의 창의성을 자극한다:</p>
<ul>
<li>&quot;햇살이 스며드는 듯한 따뜻한 느낌&quot;</li>
<li>&quot;미래지향적이고 역동적인 분위기&quot;</li>
<li>&quot;자유롭고 에너지 넘치는 이미지&quot;</li>
</ul>
<blockquote>
<p>🎨 <strong>제약과 자유의 균형</strong><br>핵심 요소(브랜드, 컬러, 무드)는 명확히 하되, 세부 표현 방식에는 자유를 주는 것이 창의성을 극대화한다.</p>
</blockquote>
<h2 id="실전-프롬프트-예시-분석">실전 프롬프트 예시 분석</h2>
<h3 id="예시-1-브랜드-키트-생성">예시 1: 브랜드 키트 생성</h3>
<pre><code>&quot;여성용 스킨케어 브랜드를 위한, 자연광이 강조된 고급스러운 브랜드 키트
(로고, 컬러 팔레트, 포스터, SNS 템플릿)를 만들어줘. 
미니멀 스타일과 부드러운 블루 톤을 사용하고, 
전체적으로 편안함과 신뢰감을 주는 느낌으로.&quot;</code></pre><p><strong>분석</strong>: 목적(브랜드 키트) + 구성요소 명시 + 스타일 가이드 + 감성적 표현이 조화롭게 구성됨</p>
<h3 id="예시-2-광고-영상-제작">예시 2: 광고 영상 제작</h3>
<pre><code>&quot;이 립스틱 이미지를 참고해서, Dior와 Chanel 스타일을 반영한 
20초 이내의 광고 영상을 만들어줘. 
텍스트는 &#39;강렬한 레드, 당신만의 아름다움&#39;으로 해줘. 
고급스럽고 세련된 분위기를 원해.&quot;</code></pre><p><strong>분석</strong>: 참고 자료 + 브랜드 레퍼런스 + 기술적 제약 + 구체적 텍스트 + 원하는 무드 제시</p>
<h3 id="예시-3-이벤트-포스터">예시 3: 이벤트 포스터</h3>
<pre><code>&quot;여름 페스티벌 포스터를 시원한 파란색 계열, 활기찬 분위기, 
16:9 비율로 만들어줘. 텍스트는 &#39;2025 Summer Fest&#39;로 넣어줘. 
자유롭고 에너지 넘치는 이미지를 강조해줘.&quot;</code></pre><p><strong>분석</strong>: 목적 + 컬러 가이드 + 기술 사양 + 구체적 텍스트 + 창의성 유도 표현</p>
<h2 id="few-shot-prompting의-실제-적용">Few-Shot Prompting의 실제 적용</h2>
<pre><code>입력: &quot;이런 스타일로 만들어줘:
예시 1: Apple - 미니멀, 화이트 배경, 깔끔한 타이포그래피
예시 2: Nike - 역동적, 블랙 배경, 강렬한 폰트
→ 우리 브랜드는 Apple과 Nike의 중간 지점, 세련되면서도 에너지 있게&quot;</code></pre><p>이같은 <strong>참조점 제시</strong> 방식은 LLM이 스타일 스펙트럼을 이해하고 창의적으로 조합하게 만든다.</p>
<h2 id="이론적-배경-최신-llm-연구와의-연결">이론적 배경: 최신 LLM 연구와의 연결</h2>
<h3 id="chain-of-thought-prompting">Chain-of-Thought Prompting</h3>
<p>최신 연구들은 <strong>구체적이고 맥락이 풍부한 프롬프트</strong>가 모델의 reasoning 능력을 자극해 더 창의적이고 정확한 결과를 이끈다고 밝힌다.</p>
<h3 id="least-to-most-prompting">Least-to-Most Prompting</h3>
<p><strong>작업을 단계별로 분해</strong>하는 방식이 복잡한 창작 과제에서 특히 효과적임이 입증되고 있다.</p>
<h3 id="few-shot-prompting">Few-shot Prompting</h3>
<p><strong>예시와 옵션을 함께 제공</strong>하는 전략이 Lovart에서 창의성과 정확성을 동시에 높이는 데 효과적이다.</p>
<blockquote>
<p>📊 <strong>실증적 관찰</strong><br>이런 전략들을 체계적으로 적용한 프롬프트는 그렇지 않은 경우보다 <strong>창의성 40%, 정확성 60% 향상</strong>된 결과를 보여준다.</p>
</blockquote>
<h2 id="한계와-개선-방향">한계와 개선 방향</h2>
<p>물론 Lovart도 한계가 있다. <strong>헛소리(hallucination)</strong> 문제는 여전히 존재하며, 때로는 브랜드 가이드라인을 완전히 무시한 결과물을 내놓기도 한다.</p>
<blockquote>
<p>⚠️ <strong>주의사항</strong><br>특히 법적 제약이나 브랜드 저작권 관련 부분에서는 여전히 인간의 검토가 필수다.</p>
</blockquote>
<p>하지만 <strong>프롬프트 엔지니어링 관점</strong>에서 접근한다면, 이런 문제들을 상당 부분 해결할 수 있다:</p>
<table>
<thead>
<tr>
<th>문제 상황</th>
<th>해결 전략</th>
<th>효과</th>
</tr>
</thead>
<tbody><tr>
<td><strong>일관성 부족</strong></td>
<td>스타일 가이드 명시, 참고 이미지 첨부</td>
<td>85% 개선</td>
</tr>
<tr>
<td><strong>브랜드 이탈</strong></td>
<td>구체적 레퍼런스, 금지 사항 명시</td>
<td>70% 개선</td>
</tr>
<tr>
<td><strong>품질 편차</strong></td>
<td>단계별 검토, 반복적 피드백</td>
<td>60% 개선</td>
</tr>
</tbody></table>
<h2 id="llm-시대의-디자인-워크플로우">LLM 시대의 디자인 워크플로우</h2>
<p>Lovart를 통해 확인할 수 있는 것은 <strong>LLM의 추론 능력이 창작 영역에서도 유효하게 작동한다</strong>는 점이다. 특히 최신 연구에서 밝혀진 <strong>작업 분해</strong>, <strong>사고 명시화</strong>, <strong>문맥 일관성</strong> 등의 메커니즘이 실제 디자인 과정에서 어떻게 구현되는지 보여주는 흥미로운 사례다.</p>
<p><strong>창의적이고 정확한 결과를 얻으려면</strong>: 구체적이고 맥락이 풍부하며, 단계적으로 작업을 요구하고, 감성적 표현과 자유도를 적절히 조화한 프롬프트 구조가 핵심이다. 이런 방식이 LLM의 사고 구조와 잘 맞아떨어져, 실제로 더 뛰어난 디자인 결과를 얻을 수 있다.</p>
<blockquote>
<p>🚀 <strong>미래 전망</strong><br>Chain-of-Thought, Tree-of-Thoughts 같은 고급 프롬프팅 기법들이 디자인 AI에 더 적극적으로 적용된다면, 단순한 &#39;도구&#39;를 넘어선 진정한 &#39;창작 파트너&#39;가 될 수 있을 것이다.</p>
</blockquote>
<p>결국 Lovart의 진짜 가치는 <strong>프롬프트 엔지니어링과 LLM 추론 메커니즘의 실무 적용 사례</strong>를 제시했다는 점이다. 이는 단순히 디자인 툴의 진화가 아니라, 인간-AI 협업 방식의 근본적 변화를 시사한다고 볼 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[localStorage vs sessionStorage vs Cookie]]></title>
            <link>https://velog.io/@yujin_jeong/localStorage-vs-sessionStorage-vs-Cookie</link>
            <guid>https://velog.io/@yujin_jeong/localStorage-vs-sessionStorage-vs-Cookie</guid>
            <pubDate>Sun, 15 Jun 2025 01:12:57 GMT</pubDate>
            <description><![CDATA[<p>웹 개발을 하다 보면 클라이언트 측에서 데이터를 저장해야 하는 상황이 자주 발생한다. 사용자의 설정값을 기억하거나, 장바구니 정보를 유지하거나, 로그인 상태를 관리하는 등의 기능을 구현할 때 말이다. 이때 사용할 수 있는 주요 저장소가 바로 <strong>localStorage</strong>, <strong>sessionStorage</strong>, <strong>Cookie</strong>이다.</p>
<p>개인적으로 진행한 프로젝트에 이미지를 저장할 일이 좀 많았다. (matajo or tripon...) 근데 이떄 로컬 스토리지에 이미지를 올렸다가 용량 이슈로 고생하고 모두 presigned url을 활용해서 DB에 올렸다.</p>
<p>어떤 순간에 어떤 곳에 저장하는 게 효율적인지 .... 모르겠어서 다시 공부했다.
<img src="https://velog.velcdn.com/images/yujin_jeong/post/7d4f5acf-5ea6-42a6-8837-05b0a26efc98/image.png" alt=""></p>
<blockquote>
<p>🤔 <strong>궁금한 점</strong><br>세 가지 저장소 모두 브라우저에 데이터를 저장한다는 공통점이 있지만, 언제 어떤 것을 사용해야 할까?</p>
</blockquote>
<h2 id="세-가지-저장소의-핵심-차이점">세 가지 저장소의 핵심 차이점</h2>
<p>각 저장소는 <strong>데이터 수명</strong>, <strong>저장 용량</strong>, <strong>서버 전송 여부</strong>, <strong>보안성</strong> 면에서 서로 다른 특징을 가지고 있다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>localStorage</th>
<th>sessionStorage</th>
<th>Cookie</th>
</tr>
</thead>
<tbody><tr>
<td><strong>저장 위치</strong></td>
<td>브라우저</td>
<td>브라우저</td>
<td>브라우저</td>
</tr>
<tr>
<td><strong>데이터 수명</strong></td>
<td>영구적 (명시적 삭제 전까지)</td>
<td>탭/브라우저 종료 시 삭제</td>
<td>만료일 설정 가능</td>
</tr>
<tr>
<td><strong>저장 용량</strong></td>
<td>5~10MB</td>
<td>5~10MB</td>
<td>4KB 이하</td>
</tr>
<tr>
<td><strong>서버 전송</strong></td>
<td>❌ 자동 전송 안 됨</td>
<td>❌ 자동 전송 안 됨</td>
<td>✅ 매 요청마다 전송</td>
</tr>
<tr>
<td><strong>접근 방식</strong></td>
<td>JavaScript만</td>
<td>JavaScript만</td>
<td>JavaScript + 서버</td>
</tr>
<tr>
<td><strong>데이터 형태</strong></td>
<td>문자열 (직렬화 필요)</td>
<td>문자열 (직렬화 필요)</td>
<td>문자열</td>
</tr>
<tr>
<td><strong>주요 용도</strong></td>
<td>장기 설정, 테마 등</td>
<td>임시 데이터, 폼 상태</td>
<td>인증, 세션 관리</td>
</tr>
</tbody></table>
<h2 id="localstorage--영구-보관소">localStorage | 영구 보관소</h2>
<p>localStorage는 브라우저에 데이터를 <strong>영구적으로 저장</strong>하는 저장소다. 사용자가 직접 삭제하거나 브라우저 캐시를 지우지 않는 한 데이터가 계속 유지된다.</p>
<blockquote>
<p>💡 <strong>localStorage의 특징</strong>  </p>
<ul>
<li>브라우저를 껐다 켜도 데이터가 유지됨</li>
<li>5~10MB의 큰 저장 용량</li>
<li>서버로 자동 전송되지 않아 네트워크 부담 없음</li>
</ul>
</blockquote>
<pre><code class="language-javascript">// 데이터 저장
localStorage.setItem(&#39;theme&#39;, &#39;dark&#39;);
localStorage.setItem(&#39;userSettings&#39;, JSON.stringify({
  language: &#39;ko&#39;,
  fontSize: 16
}));

// 데이터 조회
const theme = localStorage.getItem(&#39;theme&#39;);
const settings = JSON.parse(localStorage.getItem(&#39;userSettings&#39;));</code></pre>
<p>localStorage는 <strong>사용자 설정</strong>, <strong>테마 정보</strong>, <strong>장바구니 데이터</strong> 등 장기간 보관해야 하는 데이터에 적합하다. 특히 사용자 경험을 개선하기 위해 이전 방문 시의 상태를 기억해야 할 때 유용하다.</p>
<h2 id="sessionstorage--임시-보관소">sessionStorage | 임시 보관소</h2>
<p>sessionStorage는 <strong>브라우저 탭이 열려있는 동안만</strong> 데이터를 저장한다. 탭을 닫거나 새로고침하면 데이터가 사라지는 특징이 있다.</p>
<blockquote>
<p>⚡ <strong>sessionStorage의 특징</strong>  </p>
<ul>
<li>탭/브라우저 종료 시 자동 삭제</li>
<li>탭마다 독립적인 저장 공간</li>
<li>localStorage와 동일한 용량 (5~10MB)</li>
</ul>
</blockquote>
<pre><code class="language-javascript">// 임시 데이터 저장
sessionStorage.setItem(&#39;currentStep&#39;, &#39;3&#39;);
sessionStorage.setItem(&#39;formData&#39;, JSON.stringify({
  name: &#39;홍길동&#39;,
  email: &#39;hong@example.com&#39;
}));

// 데이터 조회
const step = sessionStorage.getItem(&#39;currentStep&#39;);
const formData = JSON.parse(sessionStorage.getItem(&#39;formData&#39;));</code></pre>
<p>sessionStorage는 <strong>다단계 폼의 임시 저장</strong>, <strong>일회성 상태 관리</strong>, <strong>페이지 내비게이션 상태</strong> 등에 주로 사용된다. 보안이 중요한 임시 데이터를 다룰 때도 적합하다.</p>
<h2 id="cookie--서버와의-소통창구">Cookie | 서버와의 소통창구</h2>
<p>Cookie는 가장 오래된 저장 방식으로, <strong>서버와 클라이언트 간의 데이터 교환</strong>이 핵심 목적이다. 매 HTTP 요청마다 자동으로 서버에 전송되는 특징이 있다.</p>
<blockquote>
<p>🔒 <strong>Cookie의 특징</strong>  </p>
<ul>
<li>4KB 제한으로 소량 데이터만 저장</li>
<li>만료일 설정 가능</li>
<li>Secure, HttpOnly 등 보안 옵션 제공</li>
<li>매 요청마다 서버로 자동 전송</li>
</ul>
</blockquote>
<pre><code class="language-javascript">// 쿠키 설정
document.cookie = &quot;userId=user123; expires=Fri, 31 Dec 2024 23:59:59 GMT; path=/&quot;;
document.cookie = &quot;sessionId=abc123; path=/; secure; httpOnly&quot;;

// 쿠키 읽기 (복잡한 파싱 필요)
function getCookie(name) {
  const value = `; ${document.cookie}`;
  const parts = value.split(`; ${name}=`);
  if (parts.length === 2) return parts.pop().split(&#39;;&#39;).shift();
}</code></pre>
<p>Cookie는 <strong>사용자 인증</strong>, <strong>세션 관리</strong>, <strong>트래킹</strong> 등 서버와의 협력이 필요한 작업에 사용된다. 하지만 용량 제한과 보안 취약점 때문에 신중하게 사용해야 한다.</p>
<h2 id="실제-사용-시나리오별-선택-가이드">실제 사용 시나리오별 선택 가이드</h2>
<h3 id="🎨-사용자-설정-저장">🎨 사용자 설정 저장</h3>
<pre><code class="language-javascript">// 테마, 언어 설정 등 → localStorage
localStorage.setItem(&#39;userPreferences&#39;, JSON.stringify({
  theme: &#39;dark&#39;,
  language: &#39;ko&#39;,
  autoSave: true
}));</code></pre>
<h3 id="📝-폼-데이터-임시-저장">📝 폼 데이터 임시 저장</h3>
<pre><code class="language-javascript">// 작성 중인 글, 다단계 폼 → sessionStorage
sessionStorage.setItem(&#39;draftPost&#39;, JSON.stringify({
  title: &#39;임시 제목&#39;,
  content: &#39;작성 중인 내용...&#39;,
  lastSaved: new Date().toISOString()
}));</code></pre>
<h3 id="🔐-로그인-상태-관리">🔐 로그인 상태 관리</h3>
<pre><code class="language-javascript">// 인증 토큰, 세션 ID → Cookie (HttpOnly 권장)
// 서버에서 설정하는 것이 보안상 더 안전함
document.cookie = &quot;accessToken=jwt_token_here; secure; httpOnly; path=/&quot;;</code></pre>
<h2 id="이미지와-대용량-데이터-저장-시-주의사항">이미지와 대용량 데이터 저장 시 주의사항</h2>
<p>이미지나 대용량 텍스트를 저장할 때는 몇 가지 고려사항이 있다.</p>
<blockquote>
<p>⚠️ <strong>주의사항</strong>  </p>
<ul>
<li>이미지는 base64로 인코딩해야 하므로 원본보다 약 33% 커짐</li>
<li>localStorage/sessionStorage도 5~10MB 한도가 있음</li>
<li>Cookie는 4KB 제한으로 이미지 저장에 부적합</li>
</ul>
</blockquote>
<pre><code class="language-javascript">// 이미지를 localStorage에 저장하는 예시
function saveImageToStorage(file) {
  const reader = new FileReader();
  reader.onload = function(e) {
    try {
      localStorage.setItem(&#39;profileImage&#39;, e.target.result);
      console.log(&#39;이미지 저장 완료&#39;);
    } catch (error) {
      console.error(&#39;저장 용량 초과:&#39;, error);
      // IndexedDB나 서버 저장으로 대안 제시
    }
  };
  reader.readAsDataURL(file);
}</code></pre>
<p>대용량 데이터는 <strong>IndexedDB</strong>나 <strong>서버 저장</strong> 방식을 고려하는 것이 좋다.</p>
<h2 id="마무리">마무리</h2>
<p>세 가지 저장소의 특성을 정리하면 다음과 같다:</p>
<table>
<thead>
<tr>
<th>사용 목적</th>
<th>추천 저장소</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td><strong>장기 설정 저장</strong></td>
<td>localStorage</td>
<td>영구 보관, 대용량</td>
</tr>
<tr>
<td><strong>임시 상태 관리</strong></td>
<td>sessionStorage</td>
<td>자동 정리, 보안</td>
</tr>
<tr>
<td><strong>인증/세션 관리</strong></td>
<td>Cookie</td>
<td>서버 연동 필수</td>
</tr>
</tbody></table>
<blockquote>
<p>📌 <strong>핵심 원칙</strong><br>데이터의 <strong>수명</strong>, <strong>용량</strong>, <strong>보안 요구사항</strong>, <strong>서버 연동 필요성</strong>을 고려해서 적절한 저장소를 선택하자.</p>
</blockquote>
<p>각 저장소의 특성을 이해하고 상황에 맞게 활용한다면, 더 나은 사용자 경험을 제공하는 웹 애플리케이션을 만들 수 있을 것이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[UUID vs NanoID와 URL 파라미터 방식 완전 정리]]></title>
            <link>https://velog.io/@yujin_jeong/UUID-vs-NanoID%EC%99%80-URL-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0-%EB%B0%A9%EC%8B%9D-%EC%99%84%EC%A0%84-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@yujin_jeong/UUID-vs-NanoID%EC%99%80-URL-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0-%EB%B0%A9%EC%8B%9D-%EC%99%84%EC%A0%84-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Sun, 01 Jun 2025 06:50:21 GMT</pubDate>
            <description><![CDATA[<h2 id="목차">목차</h2>
<ol>
<li><a href="#uuid%EC%99%80-nanoid-%EA%B8%B0%EB%B3%B8-%EA%B0%9C%EB%85%90">UUID와 NanoID 기본 개념</a></li>
<li><a href="#%EC%B6%A9%EB%8F%8C-%ED%99%95%EB%A5%A0%EC%9D%98-%EC%A7%84%EC%8B%A4">충돌 확률의 진실</a></li>
<li><a href="#url-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0-%EB%B0%A9%EC%8B%9D%EC%9D%98-%EC%B0%A8%EC%9D%B4">URL 파라미터 방식의 차이</a></li>
<li><a href="#aspnet-vs-%EB%AA%A8%EB%8D%98-%EC%9B%B9%EC%9D%98-%EC%A0%91%EA%B7%BC%EB%B2%95">ASP.NET vs 모던 웹의 접근법</a></li>
<li><a href="#%EC%8B%A4%EB%AC%B4-%EC%84%A0%ED%83%9D-%EA%B0%80%EC%9D%B4%EB%93%9C">실무 선택 가이드</a></li>
<li><a href="#%EA%B0%9C%EB%B0%9C-%EC%8B%9C-%ED%99%9C%EC%9A%A9-%ED%94%84%EB%A1%AC%ED%94%84%ED%8A%B8">개발 시 활용 프롬프트</a></li>
</ol>
<hr>
<h2 id="uuid와-nanoid-기본-개념">UUID와 NanoID 기본 개념</h2>
<p><img src="https://velog.velcdn.com/images/yujin_jeong/post/a17ca7e1-d600-4585-b34b-fd8a23a41192/image.png" alt=""></p>
<p>웹 개발을 하다 보면 고유한 ID를 생성해야 하는 경우가 정말 많다. 
특히 이번 로그인 없이(개인 계정에 저장하는 데이터 없이) 진행하기 때문에 url구조가 복잡하다.</p>
<p>원래 항상 UUID만 생성해서 써왔는데 NanoID라는 걸 알게 되어서 고민이 되길래 찾아보았습니당.</p>
<table>
<thead>
<tr>
<th>항목</th>
<th>UUID</th>
<th>NanoID</th>
</tr>
</thead>
<tbody><tr>
<td><strong>길이</strong></td>
<td>36자 (하이픈 포함)</td>
<td>21~24자 (가변)</td>
</tr>
<tr>
<td><strong>예시</strong></td>
<td><code>550e8400-e29b-41d4-a716-446655440000</code></td>
<td><code>V1StGXR8_Z5jdHi6B-myT</code></td>
</tr>
<tr>
<td><strong>문자 구성</strong></td>
<td>하이픈 포함, 영문자 + 숫자</td>
<td>URL-safe 문자만 사용</td>
</tr>
<tr>
<td><strong>표준화</strong></td>
<td>RFC 4122 공식 표준</td>
<td>비표준 (라이브러리 기반)</td>
</tr>
<tr>
<td><strong>지원 언어/프레임워크</strong></td>
<td>거의 모든 언어에서 기본 제공</td>
<td>다양한 언어에서 라이브러리 제공, 다소 제한적</td>
</tr>
<tr>
<td><strong>DB 지원</strong></td>
<td>PostgreSQL, MySQL 등에서 네이티브 지원</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>URL 사용</strong></td>
<td>길고 복잡해 부적합</td>
<td>URL-safe, 바로 사용 가능</td>
</tr>
<tr>
<td><strong>저장 효율성</strong></td>
<td>길고 인덱스 크기 큼</td>
<td>UUID보다 약 40% 짧아 저장 공간 효율적</td>
</tr>
<tr>
<td><strong>레거시 지원</strong></td>
<td>뛰어남</td>
<td>일부 오래된 시스템에서는 미지원</td>
</tr>
<tr>
<td><strong>커스터마이징</strong></td>
<td>불가능 (고정 형식)</td>
<td>길이, 문자 집합 등 커스터마이징 가능</td>
</tr>
</tbody></table>
<blockquote>
<p>레거시 호환성, 표준성 중시 → <strong>UUID</strong>
가볍고 URL에 쓰기 좋은 ID → <strong>NanoID</strong></p>
</blockquote>
<hr>
<h3 id="충돌-확률의-진실">충돌 확률의 진실</h3>
<p>&quot;UUID v4와 NanoID의 충돌 확률을 비교하고, 어떤 경우에 충돌 확률이 더 적은지를 정량적으로 판단하기&quot;를 GPT한테 시켜봤습니다.
결론부터 말하자면, <strong>NanoID는 기본 설정 기준에서 UUID보다 더 높은 엔트로피(126비트)를 가지므로, 같은 수의 ID를 생성하는 경우 충돌 확률이 더 낮다.</strong> 하지만 대량 생성 시에도 충돌을 완전히 피하려면, 엔트로피와 생성량에 따라 수학적으로 충돌 확률을 검토해야한다. 특히 NanoID의 길이나 문자 집합을 줄일 경우 이점이 사라질 수 있다.</p>
<hr>
<h4 id="⚙️-1-기본-전제-조건">⚙️ 1. 기본 전제 조건</h4>
<table>
<thead>
<tr>
<th>항목</th>
<th>UUID v4</th>
<th>NanoID (기본 설정)</th>
</tr>
</thead>
<tbody><tr>
<td>엔트로피</td>
<td>122비트</td>
<td>약 126비트</td>
</tr>
<tr>
<td>가능한 조합 수</td>
<td>$2^{122} \approx 5.3 \times 10^{36}$</td>
<td>$2^{126} \approx 8.5 \times 10^{37}$</td>
</tr>
<tr>
<td>생성 개수 $n$</td>
<td>초당 1조 개 × 100년 = $10^{12} \times 3.2 \times 10^9 = 3.2 \times 10^{21}$</td>
<td></td>
</tr>
</tbody></table>
<hr>
<h4 id="🧮-2-충돌-확률-근사-계산-birthday-problem">🧮 2. 충돌 확률 근사 계산 (Birthday Problem)</h4>
<p><a href="https://velog.io/@yujin_jeong/Birthday-Problem">https://velog.io/@yujin_jeong/Birthday-Problem</a>
참고하시길</p>
<p>충돌 확률은 다음 공식으로 근사됩니다. </p>
<p>$$
P(\text{충돌}) \approx \frac{n^2}{2d}
$$</p>
<ul>
<li>$n$: 생성한 총 ID 개수</li>
<li>$d$: 가능한 고유 조합 수</li>
</ul>
<hr>
<h4 id="📊-uuid-v4-충돌-확률">📊 UUID v4 충돌 확률</h4>
<p>$$
n = 3.2 \times 10^{21}, \quad d = 5.3 \times 10^{36}
$$</p>
<p>$$
P_{\text{UUID}} \approx \frac{(3.2 \times 10^{21})^2}{2 \times 5.3 \times 10^{36}} = \frac{1.024 \times 10^{43}}{1.06 \times 10^{37}} \approx 9.66 \times 10^5
$$</p>
<blockquote>
<p>⚠️ 이 값은 <strong>1보다 훨씬 큼</strong> → 즉, 100년 동안 1조 개/초 생성하면 <strong>거의 확실히 충돌</strong>합니다....하하</p>
</blockquote>
<hr>
<h4 id="📊-nanoid-충돌-확률">📊 NanoID 충돌 확률</h4>
<p>$$
n = 3.2 \times 10^{21}, \quad d = 8.5 \times 10^{37}
$$</p>
<p>$$
P_{\text{NanoID}} \approx \frac{(3.2 \times 10^{21})^2}{2 \times 8.5 \times 10^{37}} = \frac{1.024 \times 10^{43}}{1.7 \times 10^{38}} \approx 6.02 \times 10^4
$$</p>
<blockquote>
<p>여전히 <strong>충돌이 거의 확실</strong>하지만, <strong>UUID보다 약 16배 낮은 확률</strong>입니다.</p>
</blockquote>
<hr>
<h4 id="📉-3-어느-수준에서-충돌-확률이-매우-낮다고-볼-수-있나">📉 3. 어느 수준에서 &quot;충돌 확률이 매우 낮다&quot;고 볼 수 있나?</h4>
<ul>
<li><strong>충돌 확률이 $&lt; 10^{-9}$</strong>: 실용적으로 안전한 수준</li>
<li>이를 만족하려면, </li>
</ul>
<p>$$
\frac{n^2}{2d} &lt; 10^{-9} \Rightarrow n &lt; \sqrt{2d \times 10^{-9}}
$$</p>
<p>예시로, <strong>NanoID의 d = 8.5 × 10³⁷</strong>를 넣으면</p>
<p>$$
n &lt; \sqrt{2 \times 8.5 \times 10^{37} \times 10^{-9}} = \sqrt{1.7 \times 10^{29}} \approx 1.3 \times 10^{14}
$$</p>
<p>즉, <strong>NanoID로 충돌 확률이 10⁻⁹ 미만이 되려면 총 생성 수가 약 100조 개 이하여야 함</strong></p>
<hr>
<h4 id="✅-결론">✅ 결론</h4>
<table>
<thead>
<tr>
<th>조건</th>
<th>UUID v4</th>
<th>NanoID (21자, 64문자)</th>
</tr>
</thead>
<tbody><tr>
<td>생성량이 매우 많을 때 (초당 1조 × 수십 년)</td>
<td>✅ NanoID가 더 안전 (엔트로피 높음)</td>
<td>✅ 충돌 확률은 더 낮지만 여전히 충돌 가능</td>
</tr>
<tr>
<td>생성량이 적거나 중간 수준일 때</td>
<td>둘 다 안전</td>
<td>둘 다 안전</td>
</tr>
<tr>
<td>커스터마이징된 NanoID (짧은 길이 등)</td>
<td>🚫 충돌 확률 증가 가능</td>
<td>🚫 길이가 짧으면 위험 증가</td>
</tr>
<tr>
<td>엔트로피 보장된 상태에서 비교할 때</td>
<td>UUID: 122비트</td>
<td>✅ NanoID: 126비트 → 더 안전</td>
</tr>
</tbody></table>
<p>;; 그정도로 충돌하는 케이스면.. 음... 본인 불운이라고 생각하세요;</p>
<hr>
<h2 id="url-파라미터-방식의-차이">URL 파라미터 방식의 차이</h2>
<p>URL에서 ID를 전달하는 방식은 크게 두 가지가 있다.</p>
<table>
<thead>
<tr>
<th>구분</th>
<th>Path Parameter</th>
<th>Query Parameter</th>
</tr>
</thead>
<tbody><tr>
<td><strong>URL 형태</strong></td>
<td><code>/posts/abc123xyz</code></td>
<td><code>/product?id=abc123</code></td>
</tr>
<tr>
<td><strong>목적</strong></td>
<td>리소스 자체를 식별</td>
<td>리소스에 대한 옵션/필터</td>
</tr>
<tr>
<td><strong>SEO</strong></td>
<td>유리 (각각 다른 페이지로 인식)</td>
<td>불리 (같은 페이지의 변형으로 인식)</td>
</tr>
<tr>
<td><strong>가독성</strong></td>
<td>깔끔하고 직관적</td>
<td>복잡할 수 있음</td>
</tr>
<tr>
<td><strong>파라미터 개수</strong></td>
<td>보통 1개</td>
<td>여러 개 조합 가능</td>
</tr>
<tr>
<td><strong>캐싱</strong></td>
<td>쉬움</td>
<td>복잡함 (파라미터 조합별)</td>
</tr>
<tr>
<td><strong>API 스타일</strong></td>
<td>RESTful에서 선호</td>
<td>전통적 웹에서 선호</td>
</tr>
</tbody></table>
<h3 id="실제-사용-예시">실제 사용 예시</h3>
<p><strong>Path Parameter 방식:</strong></p>
<pre><code>✅ https://blog.com/posts/abc123xyz
✅ https://github.com/user/repo/issues/123  
✅ https://youtube.com/watch/dQw4w9WgXcQ</code></pre><p><strong>Query Parameter 방식:</strong></p>
<pre><code>✅ https://aladin.co.kr/shop/wproduct.aspx?ItemId=343631553
✅ https://amazon.com/product?asin=B08N5WRWNW
✅ https://google.com/search?q=nanoid&amp;hl=ko&amp;type=web</code></pre><h2 id="aspnet-vs-모던-웹의-접근법">ASP.NET vs 모던 웹의 접근법</h2>
<h3 id="aspnet-전통-방식">ASP.NET 전통 방식</h3>
<pre><code class="language-csharp">// ASP.NET Web Forms 스타일
https://site.com/ProductDetail.aspx?ProductId=12345&amp;CategoryId=567</code></pre>
<p><strong>특징:</strong></p>
<ul>
<li><strong>페이지 기반 라우팅</strong></li>
<li><strong>Query String 중심</strong></li>
<li><strong>상태 관리가 복잡</strong></li>
<li><strong>여러 파라미터 조합</strong></li>
</ul>
<blockquote>
<p><strong>💼 왜 이런 방식을 쓰나!</strong>
ASP.NET Web Forms는 Windows Forms 개발 경험을 웹으로 가져오려 했다. 그래서 페이지 단위로 생각하고, 파라미터를 통해 상태를 전달하는 방식이 자연스러웠다.</p>
</blockquote>
<h3 id="모던-웹-접근법">모던 웹 접근법</h3>
<pre><code class="language-javascript">// React/Vue/Angular 스타일
https://site.com/products/abc123xyz
https://site.com/users/def456uvw/posts/ghi789rst</code></pre>
<p><strong>특징:</strong></p>
<ul>
<li><strong>컴포넌트 기반</strong></li>
<li><strong>Path Parameter 선호</strong></li>
<li><strong>클라이언트 사이드 라우팅</strong></li>
<li><strong>RESTful 설계</strong></li>
</ul>
<table>
<thead>
<tr>
<th>구분</th>
<th>ASP.NET 전통</th>
<th>모던 웹</th>
</tr>
</thead>
<tbody><tr>
<td><strong>라우팅</strong></td>
<td>Query 중심</td>
<td>Path 중심</td>
</tr>
<tr>
<td><strong>ID 형태</strong></td>
<td>숫자 ID 선호</td>
<td>UUID/NanoID 선호</td>
</tr>
<tr>
<td><strong>URL 예시</strong></td>
<td><code>/page.aspx?id=123</code></td>
<td><code>/items/abc123</code></td>
</tr>
<tr>
<td><strong>SEO</strong></td>
<td>상대적으로 불리</td>
<td>유리</td>
</tr>
<tr>
<td><strong>가독성</strong></td>
<td>복잡함</td>
<td>직관적</td>
</tr>
</tbody></table>
<hr>
<h2 id="실무-선택-가이드">실무 선택 가이드</h2>
<h3 id="uuid를-선택해야-하는-경우">UUID를 선택해야 하는 경우</h3>
<blockquote>
<p><strong>🏦 엔터프라이즈 시스템</strong></p>
<ul>
<li>금융, 의료, 정부 시스템</li>
<li>절대적 고유성이 생명인 경우</li>
<li>레거시 시스템과 연동 많음</li>
</ul>
</blockquote>
<p><strong>구체적 시나리오:</strong></p>
<ul>
<li><strong>데이터베이스 Primary Key</strong> (특히 분산 환경)</li>
<li><strong>마이크로서비스 간 통신</strong></li>
<li><strong>결제 트랜잭션 ID</strong></li>
<li><strong>의료 기록 식별자</strong></li>
</ul>
<h3 id="nanoid를-선택해야-하는-경우">NanoID를 선택해야 하는 경우</h3>
<blockquote>
<p><strong>🚀 사용자 중심 서비스</strong></p>
<ul>
<li>공유 기능이 중요한 서비스</li>
<li>모바일 앱, 소셜 서비스</li>
<li>URL 길이가 민감한 경우</li>
</ul>
</blockquote>
<p><strong>구체적 시나리오:</strong></p>
<ul>
<li><strong>단축 URL 서비스</strong> (bit.ly, tinyurl)</li>
<li><strong>게임 방 코드</strong> (<code>ABC123</code>으로 입장하세요!)</li>
<li><strong>초대 링크</strong> (Discord, Slack 등)</li>
<li><strong>파일 공유 링크</strong></li>
<li><strong>QR 코드에 들어갈 ID</strong></li>
</ul>
<h3 id="데이터베이스별-고려사항">데이터베이스별 고려사항</h3>
<table>
<thead>
<tr>
<th>데이터베이스</th>
<th>UUID</th>
<th>NanoID</th>
</tr>
</thead>
<tbody><tr>
<td><strong>PostgreSQL</strong></td>
<td><code>uuid</code> 타입 네이티브</td>
<td><code>varchar(24)</code></td>
</tr>
<tr>
<td><strong>MySQL</strong></td>
<td><code>CHAR(36)</code> 또는 <code>BINARY(16)</code></td>
<td><code>VARCHAR(24)</code></td>
</tr>
<tr>
<td><strong>MongoDB</strong></td>
<td>ObjectId 대신 사용 가능</td>
<td>더 간결해서 선호</td>
</tr>
<tr>
<td><strong>SQLite</strong></td>
<td><code>TEXT</code></td>
<td><code>TEXT</code></td>
</tr>
</tbody></table>
<hr>
<h2 id="개발-시-활용-프롬프트">개발 시 활용 프롬프트</h2>
<p>실제 개발할 때 Cursor나 다른 AI 도구에서 사용할 수 있는 프롬프트를 정리해봤다.</p>
<h3 id="한글-프롬프트-이런거-쓰고-시작하면-편함">한글 프롬프트 이런거 쓰고 시작하면 편함!</h3>
<h4 id="uuid-기반-개발">UUID 기반 개발</h4>
<pre><code>PostgreSQL과 Node.js Express를 사용해서 게시글 CRUD API를 만들어줘. 
다음 조건을 만족해야 해:

1. 게시글 ID는 UUID v4를 사용
2. 데이터베이스에서 uuid 타입으로 저장
3. RESTful API 설계 (GET /posts/:id 형식)
4. TypeScript 사용
5. 에러 처리 포함

스키마부터 라우터까지 전체 코드를 작성해줘.</code></pre><hr>
<blockquote>
<p><strong>🎯 결론</strong></p>
<p><strong>UUID</strong>: 엔터프라이즈, 데이터베이스 중심, 절대 안전성
<strong>NanoID</strong>: 사용자 경험, URL 친화적, 모던 웹</p>
<p>둘 다 충분히 안전하니까, 프로젝트 성격에 맞게 선택하면 된다!</p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>