<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>jh_dev.log</title>
        <link>https://velog.io/</link>
        <description>개발자로 성장하기</description>
        <lastBuildDate>Sat, 17 Jan 2026 08:19:03 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>jh_dev.log</title>
            <url>https://velog.velcdn.com/images/jh_devlog/profile/4ccbfee0-dff4-4666-8d36-e41a31f0d1a8/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. jh_dev.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/jh_devlog" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Java] 코딩 테스트 개념 정리: 스택/큐]]></title>
            <link>https://velog.io/@jh_devlog/Java-%EC%BD%94%EB%94%A9-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC-%EC%8A%A4%ED%83%9D%ED%81%90</link>
            <guid>https://velog.io/@jh_devlog/Java-%EC%BD%94%EB%94%A9-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC-%EC%8A%A4%ED%83%9D%ED%81%90</guid>
            <pubDate>Sat, 17 Jan 2026 08:19:03 GMT</pubDate>
            <description><![CDATA[<h2 id="1-스택큐-개념">1. 스택/큐 개념?</h2>
<h3 id="1-1-스택">1-1. 스택</h3>
<ul>
<li><strong>LIFO (Last-In, First-Out)</strong>: 나중에 들어온 데이터가 먼저 나가는 “후입선출” 구조</li>
<li><strong>비유</strong>: 접시 더미에서 가장 위 접시부터 꺼내는 것</li>
<li><strong>활용</strong>: 뒤로 가기(Undo), 괄호 짝 맞추기, 재귀 함수 호출 관리, DFS(깊이 우선 탐색) 등</li>
</ul>
<h3 id="1-2-큐">1-2. 큐</h3>
<ul>
<li><strong>FIFO (First-In, First-Out)</strong>: 먼저 들어온 데이터가 먼저 나가는 “선입선출” 구조</li>
<li><strong>비유</strong>: 은행 창구 줄서기</li>
<li><strong>활용</strong>: 프린터 출력 대기열, 은행 창구 업무, 작업/프로세스 스케줄링, BFS(너비 우선 탐색) 등</li>
</ul>
<blockquote>
<p><strong>원형 큐(Circular Queue)</strong>란 ?
일반적인 선형 큐(배열 기반)는 dequeue 할 때 앞부분이 비게 됩니다. 이 빈 공간을 재사용하려면 데이터를 앞으로 당기는 작업(shift, O(n))이 필요할 수 있는데, 이를 해결하기 위해 배열의 끝과 시작이 연결된 것처럼 인덱스를 순환시키는 구조가 원형 큐입니다.</p>
</blockquote>
<ul>
<li>참고: Java의 <code>ArrayDeque</code>는 내부적으로 <strong>원형(서큘러 버퍼) + 자동 리사이즈</strong> 방식이라, 코딩 테스트에서는 별도로 원형 큐를 구현할 일이 거의 없습니다.</li>
</ul>
<h3 id="1-3-시간복잡도">1-3. 시간복잡도</h3>
<p>스택과 큐는 데이터의 입구/출구가 정해져 있어 성능이 매우 뛰어납니다.</p>
<table>
<thead>
<tr>
<th>연산</th>
<th align="right">시간 복잡도</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>삽입 (Push/Offer)</td>
<td align="right">O(1)</td>
<td>끝(또는 위)에 추가 <em>(평균/분할 상환)</em></td>
</tr>
<tr>
<td>삭제 (Pop/Poll)</td>
<td align="right">O(1)</td>
<td>정해진 위치에서 제거</td>
</tr>
<tr>
<td>조회 (Peek)</td>
<td align="right">O(1)</td>
<td>제거 없이 가장 앞/위 확인</td>
</tr>
<tr>
<td>탐색 (Search)</td>
<td align="right">O(n)</td>
<td>특정 값 찾으려면 전체 순회 필요</td>
</tr>
</tbody></table>
<hr>
<h2 id="2-java에서-스택큐-다루기">2. Java에서 스택/큐 다루기</h2>
<h3 id="2-1-생성-및-초기화">2-1. 생성 및 초기화</h3>
<pre><code class="language-java">import java.util.*;

// 스택(LIFO)
Stack&lt;Integer&gt; stack1 = new Stack&lt;&gt;();
Deque&lt;Integer&gt; stack2 = new ArrayDeque&lt;&gt;();

// 큐(FIFO)
Queue&lt;Integer&gt; queue = new ArrayDeque&lt;&gt;();</code></pre>
<ul>
<li>스택처럼 쓸 때: <code>push / pop / peek</code></li>
<li>큐처럼 쓸 때: <code>offer / poll / peek</code> </li>
<li>Deque를 큐로 더 명시적으로 쓰고 싶으면: <code>addLast / pollFirst / peekFirst</code></li>
</ul>
<blockquote>
<p>Java의 <code>Stack</code> 클래스는 내부적으로 <code>Vector</code>를 상속받아 만들어졌고, 많은 메서드가 <code>synchronized</code> 기반이라 코딩 테스트처럼 단일 스레드 환경에서는 불필요한 오버헤드가 생길 수 있습니다. 그래서 보통 <code>ArrayDeque(Deque)</code> 를 스택/큐로 활용하는 것을 추천합니다.</p>
</blockquote>
<h3 id="2-2-자주-쓰는-유틸">2-2. 자주 쓰는 유틸</h3>
<ul>
<li><code>size()</code>: 현재 데이터 개수 확인</li>
<li><code>isEmpty()</code>: 비어있는지 확인 (pop/poll 전 필수)</li>
<li><code>clear()</code>: 전체 초기화</li>
</ul>
<hr>
<h2 id="3-기본-메서드">3. 기본 메서드</h2>
<h3 id="3-1-스택-메서드">3-1. 스택 메서드</h3>
<table>
<thead>
<tr>
<th>기능</th>
<th>메서드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>삽입</td>
<td><code>push(e)</code></td>
<td>맨 위에 요소 추가</td>
</tr>
<tr>
<td>삭제</td>
<td><code>pop()</code></td>
<td>맨 위 요소 제거 후 반환 <strong>(비어있으면 예외 발생)</strong></td>
</tr>
<tr>
<td>확인</td>
<td><code>peek()</code></td>
<td>제거 없이 맨 위 요소 확인</td>
</tr>
</tbody></table>
<h3 id="3-2-큐-메서드">3-2. 큐 메서드</h3>
<table>
<thead>
<tr>
<th>기능</th>
<th>메서드</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>삽입</td>
<td><code>offer(e)</code></td>
<td>큐 뒤에 추가 <strong>(실패 시 false)</strong></td>
</tr>
<tr>
<td>삭제</td>
<td><code>poll()</code></td>
<td>큐 앞 요소 제거 후 반환 <strong>(비어있으면 null)</strong></td>
</tr>
<tr>
<td>확인</td>
<td><code>peek()</code></td>
<td>제거 없이 맨 앞 요소 확인</td>
</tr>
</tbody></table>
<h3 id="3-3-비었을-때-동작">3-3. 비었을 때 동작</h3>
<ul>
<li><strong>예외 발생 계열</strong>: <code>pop</code>, <code>remove</code>, <code>element</code> → 비어있으면 예외</li>
<li><strong>null 반환 계열(Queue/Deque)</strong>: <code>poll</code>, <code>peek</code> → 비어있으면 <code>null</code></li>
<li>(참고) <code>Stack</code> 클래스의 <code>pop()/peek()</code> 는 비어있으면 <code>EmptyStackException</code></li>
</ul>
<hr>
<h2 id="4-대표-패턴-및-실전-예제">4. 대표 패턴 및 실전 예제</h2>
<p>코딩 테스트에서 스택과 큐가 어떻게 쓰이는지, 가장 자주 나오는 패턴의 템플릿입니다.</p>
<h3 id="4-1-괄호-검사-stack">4-1. 괄호 검사 (Stack)</h3>
<p>문자열을 순회하며 짝이 맞는지 확인하는 전형적인 스택 문제입니다.</p>
<pre><code class="language-java">public boolean isValid(String s) {
    Deque&lt;Character&gt; stack = new ArrayDeque&lt;&gt;();
    for (char c : s.toCharArray()) {
        if (c == &#39;(&#39;) stack.push(&#39;)&#39;);
        else if (c == &#39;{&#39;) stack.push(&#39;}&#39;);
        else if (c == &#39;[&#39;) stack.push(&#39;]&#39;);
        else if (stack.isEmpty() || stack.pop() != c) return false;
    }
    return stack.isEmpty();
}</code></pre>
<h3 id="4-2-bfs-탐색-queue">4-2. BFS 탐색 (Queue)</h3>
<p>최단 거리나 인접 노드 탐색 시 사용되는 기본 구조입니다.</p>
<pre><code class="language-java">public void bfs(int startNode) {
    Queue&lt;Integer&gt; queue = new ArrayDeque&lt;&gt;();
    queue.offer(startNode);
    visited[startNode] = true;

    while (!queue.isEmpty()) {
        int curr = queue.poll();
        for (int next : adj[curr]) {
            if (!visited[next]) {
                visited[next] = true;
                queue.offer(next);
            }
        }
    }
}</code></pre>
<h3 id="4-3-우선순위-큐-priorityqueue">4-3. 우선순위 큐 (PriorityQueue)</h3>
<p>들어온 순서가 아니라 <strong>우선순위(값의 크기 등)</strong>에 따라 데이터가 나가는 자료구조입니다.
“가장 작은 값/큰 값”을 계속 뽑아야 할 때 유용합니다. (최소힙/최대힙)</p>
<pre><code class="language-java">// 최소 힙 (오름차순)
PriorityQueue&lt;Integer&gt; minHeap = new PriorityQueue&lt;&gt;();

// 최대 힙 (내림차순)
PriorityQueue&lt;Integer&gt; maxHeap = new PriorityQueue&lt;&gt;(Collections.reverseOrder());</code></pre>
<h3 id="4-4-요세푸스-문제-queuedeque">4-4. 요세푸스 문제 (Queue/Deque)</h3>
<p>큐의 <strong>회전(Rotate)</strong> 성질을 이용한 대표적인 문제입니다. 
$N$명의 사람이 원형으로 앉아 있을 때, $K$번째 사람을 계속해서 제거하는 로직입니다.</p>
<pre><code class="language-java">public List&lt;Integer&gt; josephus(int n, int k) {
    Deque&lt;Integer&gt; queue = new ArrayDeque&lt;&gt;();
    List&lt;Integer&gt; result = new ArrayList&lt;&gt;();

    // 1. 큐 초기화 (1번부터 N번까지)
    for (int i = 1; i &lt;= n; i++) queue.addLast(i);

    // 2. 큐가 빌 때까지 반복
    while (!queue.isEmpty()) {
        // K-1번 동안 맨 앞의 요소를 뒤로 보냄
        for (int i = 0; i &lt; k - 1; i++) {
            queue.addLast(queue.pollFirst());
        }
        // K번째 요소를 제거하고 결과 리스트에 추가
        result.add(queue.pollFirst());
    }
    return result;
}</code></pre>
<hr>
<h2 id="5-주의사항">5. 주의사항</h2>
<ol>
<li><p><strong>Empty Check</strong></p>
<ul>
<li><code>poll()/peek()</code>는 비어있을 때 <code>null</code>을 반환</li>
<li><code>pop()/remove()</code>는 비어있을 때 예외 발생
 → 코딩 테스트에서는 안전하게 <code>isEmpty()</code>로 먼저 확인하거나, <code>poll/peek</code> 계열을 선호하는 편이 좋습니다.</li>
</ul>
</li>
<li><p><strong>null 처리</strong></p>
<ul>
<li><code>poll()</code>이나 <code>peek()</code> 반환값은 비어있으면 <code>null</code></li>
<li>기본 타입 <code>int</code>에는 <code>null</code>을 담을 수 없으니 <code>Integer</code>로 받거나, <code>isEmpty()</code>로 먼저 검사하세요.</li>
</ul>
</li>
<li><p><strong>ArrayDeque vs LinkedList</strong></p>
<ul>
<li>단순 삽입/삭제만 반복하는 큐/스택 용도라면 보통 <code>ArrayDeque</code>가 더 빠르고 메모리 효율적입니다.</li>
<li><code>LinkedList</code>는 노드 객체가 많아져 오버헤드가 생길 수 있습니다.</li>
<li><code>ArrayDeque</code>는 null 요소를 허용하지 않습니다. (<code>offer(null)</code> 같은 것 금지)</li>
</ul>
</li>
</ol>
<hr>
<h2 id="마무리">마무리</h2>
<p>오늘은 알고리즘의 기초인 스택과 큐에 대하여 정리해 보았습니다.
공부하다 보면 구현 그 자체보다 <strong>&#39;이 문제에 왜 이 자료구조를 써야 하지?&#39;</strong>를 고민하는 게 가장 어려운 것 같습니다.
아직 어렵지만 문제를 많이 풀다보면, 적절한 자료구조를 찾는 것도 익숙해지지 않을까 싶습니다.☺️
다음 포스팅은 해시(Hash) 관련 내용을 정리해보겠습니다.🔥</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Java] 코딩 테스트 개념 정리: 배열(Array)]]></title>
            <link>https://velog.io/@jh_devlog/Java-%EC%BD%94%EB%94%A9-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC-%EB%B0%B0%EC%97%B4Array</link>
            <guid>https://velog.io/@jh_devlog/Java-%EC%BD%94%EB%94%A9-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EA%B0%9C%EB%85%90-%EC%A0%95%EB%A6%AC-%EB%B0%B0%EC%97%B4Array</guid>
            <pubDate>Thu, 08 Jan 2026 06:38:53 GMT</pubDate>
            <description><![CDATA[<h2 id="1-배열이-뭔데">1. 배열이 뭔데?</h2>
<h3 id="1-1-배열의-핵심-특징">1-1. 배열의 핵심 특징</h3>
<p><img src="https://velog.velcdn.com/images/jh_devlog/post/5a0f31f8-3c35-4ca1-9c23-41cf529cc854/image.png" alt=""></p>
<p>배열은 <strong>연속된 메모리 공간</strong>에 동일한 타입의 데이터를 순차적으로 저장하는 자료구조입니다.</p>
<ul>
<li><strong>고정 크기</strong>: 생성 시 크기를 지정하며, 한 번 정해진 크기는 변경할 수 없습니다.</li>
<li><strong>인덱스 접근</strong>: 0부터 시작하는 인덱스를 통해 데이터에 직접 접근하므로 속도가 매우 빠릅니다.</li>
<li><strong>논리적 순서 = 물리적 순서</strong>: 메모리 상에 데이터가 붙어 있어 CPU 캐시 효율이 좋습니다.</li>
</ul>
<h3 id="1-2-시간-복잡도">1-2. 시간 복잡도</h3>
<table>
<thead>
<tr>
<th>연산</th>
<th align="right">시간 복잡도</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td>인덱스로 접근/수정 (<code>arr[i]</code>)</td>
<td align="right"><strong>O(1)</strong></td>
<td>주소 계산으로 바로 접근</td>
</tr>
<tr>
<td>전체 순회 (<code>for</code>)</td>
<td align="right"><strong>O(n)</strong></td>
<td>모든 원소를 한 번씩 확인</td>
</tr>
<tr>
<td>값 탐색(선형 탐색)</td>
<td align="right"><strong>O(n)</strong></td>
<td>최악의 경우 끝까지 확인 필요</td>
</tr>
<tr>
<td>정렬 (<code>Arrays.sort</code>)</td>
<td align="right">평균 <strong>O(n log n)</strong></td>
<td>비교 기반 정렬</td>
</tr>
<tr>
<td>중간 삽입/삭제(shift 발생)</td>
<td align="right"><strong>O(n)</strong></td>
<td>요소들을 한 칸씩 밀거나 당김</td>
</tr>
</tbody></table>
<blockquote>
<p>코테에서 “삽입/삭제가 자주 발생”한다면 배열 말고 다른 구조(<code>ArrayList</code>, <code>Deque</code>, <code>LinkedList</code> 등)도 같이 고려하는 게 좋습니다.</p>
</blockquote>
<hr>
<h2 id="2-java에서-배열-다루기">2. Java에서 배열 다루기</h2>
<h3 id="2-1-생성-및-초기화">2-1. 생성 및 초기화</h3>
<pre><code class="language-java">import java.util.Arrays;

int[] arr1 = new int[5];          // 0으로 초기화
int[] arr2 = {1, 2, 3, 4, 5};     // 선언과 동시에 초기화

Arrays.fill(arr1, -1);            // 특정 값으로 채우기
</code></pre>
<h3 id="2-2-자주-쓰는-유틸-javautilarrays">2-2. 자주 쓰는 유틸 (java.util.Arrays)</h3>
<ul>
<li>정렬: <code>Arrays.sort(arr);</code> </li>
<li>복사: <code>Arrays.copyOf(arr, newLength);</code> (새로운 배열 객체 생성)</li>
<li>출력: <code>Arrays.toString(arr);</code></li>
<li>리스트 변환: <code>Arrays.asList(arr);</code> (<strong>객체 배열</strong>일 때만 기대한 대로 동작)</li>
</ul>
<h3 id="2-3-다차원-배열-2차원">2-3. 다차원 배열 (2차원)</h3>
<p><img src="https://velog.velcdn.com/images/jh_devlog/post/2f0b70d5-b23b-49d9-9af4-0e6e128f7504/image.png" alt=""></p>
<p>2차원 배열에서는 <code>arr[row][col]</code> 순서(행 → 열)를 헷갈리지 않도록 주의해야 합니다. </p>
<pre><code class="language-java">int[][] matrix = new int[3][3]; // 3x3 격자

int rows = matrix.length;       // 행의 개수
int cols = matrix[0].length;    // 열의 개수</code></pre>
<hr>
<h2 id="3-arraylist-정리">3. ArrayList 정리</h2>
<p>크기가 동적으로 변경되는 배열이 필요할 때 <code>ArrayList</code>를 활용합니다.</p>
<h3 id="3-1-arraylist란">3-1. ArrayList란?</h3>
<p><code>ArrayList</code>는 Java의 대표적인 리스트 구현체로, 내부적으로 배열을 사용해 데이터를 저장합니다.
원소가 늘어나면 더 큰 배열을 만들어 복사(리사이징)하며 크기를 자동으로 확장합니다.</p>
<h4 id="리사이징resizing-동작-원리">리사이징(Resizing) 동작 원리</h4>
<ul>
<li>초기 용량(capacity)은 기본 10개입니다.</li>
<li>원소가 늘어나 용량이 가득 차면, 내부 배열을 약 1.5배 크기로 새로 만들고 기존 데이터를 복사합니다.</li>
<li>이 때문에 개별 추가는 평균 O(1)이지만, 리사이징 시점에는 O(n)이 걸립니다. (Amortized O(1))</li>
<li>원소 개수를 미리 알면 <code>new ArrayList&lt;&gt;(expectedSize)</code>로 초기화하여 리사이징을 줄일 수 있습니다.</li>
</ul>
<h3 id="3-2-배열-vs-arraylist-비교">3-2. 배열 vs ArrayList 비교</h3>
<table>
<thead>
<tr>
<th>항목</th>
<th>배열(Array)</th>
<th>ArrayList</th>
</tr>
</thead>
<tbody><tr>
<td>크기 변경</td>
<td>불가능</td>
<td>가능(자동 리사이징)</td>
</tr>
<tr>
<td>접근/수정</td>
<td>O(1)</td>
<td>O(1)</td>
</tr>
<tr>
<td>맨 뒤 추가</td>
<td>직접 구현 시 O(n) 복사 필요</td>
<td>평균 O(1), 리사이징 시 O(n)</td>
</tr>
<tr>
<td>중간 삽입/삭제</td>
<td>O(n)</td>
<td>O(n)</td>
</tr>
<tr>
<td>타입</td>
<td>primitive 가능(<code>int[]</code>)</td>
<td>객체만 가능(<code>Integer</code>)</td>
</tr>
<tr>
<td>메모리</td>
<td>상대적으로 효율적</td>
<td>오토박싱/객체 오버헤드 가능</td>
</tr>
</tbody></table>
<blockquote>
<p>고정 길이/성능 중심이면 배열, 가변 길이/구현 편의성이 필요하면 <code>ArrayList</code>를 선택합니다.</p>
</blockquote>
<h3 id="3-3-기본-패턴">3-3. 기본 패턴</h3>
<pre><code class="language-java">import java.util.*;

List&lt;Integer&gt; list = new ArrayList&lt;&gt;();

// 추가
list.add(10);           // 맨 뒤에 추가
list.add(0, 5);         // 인덱스 0에 삽입

// 조회
int v = list.get(0);

// 수정
list.set(0, 99);

// 삭제
list.remove(1);         // 인덱스 1 삭제
list.remove(Integer.valueOf(10)); // 값 10 삭제

// 크기
int size = list.size();

// 비었는지 여부 확인
boolean empty = list.isEmpty();

// 포함 여부 확인
boolean contains = list.contains(10);

// 정렬
Collections.sort(list);                    // 오름차순
list.sort(Comparator.reverseOrder());      // 내림차순
</code></pre>
<h3 id="3-4-주의사항">3-4. 주의사항</h3>
<h4 id="1-remove-오버로드">1) remove() 오버로드</h4>
<p><code>ArrayList&lt;Integer&gt;</code>에서 <code>remove(1)</code>은 “값 1 제거”가 아니라 <strong>인덱스 1 제거</strong>로 동작합니다.
값으로 제거하려면 <code>Integer.valueOf()</code>를 사용해야 합니다.</p>
<pre><code class="language-java">List&lt;Integer&gt; list = new ArrayList&lt;&gt;(List.of(1, 2, 3));

list.remove(1);                    // 인덱스 1 삭제 → 값 2가 삭제됨
list.remove(Integer.valueOf(1));   // 값 1 삭제
</code></pre>
<h4 id="2-primitive-배열-↔-arraylist-변환">2) primitive 배열 ↔ ArrayList 변환</h4>
<pre><code class="language-java">// int[] -&gt; List&lt;Integer&gt;
int[] nums1 = {1, 2, 3};
List&lt;Integer&gt; list1 = Arrays.stream(nums1)
                            .boxed()
                            .collect(Collectors.toList());

// List&lt;Integer&gt; -&gt; int[]
List&lt;Integer&gt; list2 = List.of(1, 2, 3);
int[] nums2 = list2.stream()
                   .mapToInt(Integer::intValue)
                   .toArray();
</code></pre>
<h4 id="3-arraysaslist는-객체-배열에서만-기대한-대로-동작">3) Arrays.asList()는 객체 배열에서만 기대한 대로 동작</h4>
<pre><code class="language-java">// 정상 동작
Integer[] objArr = {1, 2, 3};
List&lt;Integer&gt; ok = Arrays.asList(objArr);

// 주의: 원소 1개짜리 리스트처럼 동작
int[] primArr = {1, 2, 3};
List&lt;int[]&gt; weird = Arrays.asList(primArr);</code></pre>
<hr>
<h2 id="4-스트림stream-활용">4. 스트림(Stream) 활용</h2>
<p>Java 8부터 도입된 스트림은 배열과 컬렉션을 다룰 때 가독성을 높여줍니다. 특히 필터링이나 변환 작업에서 유용합니다.</p>
<pre><code class="language-java">int[] nums = {1, 2, 3, 4, 5};

// 1. 필터링 및 변환
int[] evenSquared = Arrays.stream(nums)
                          .filter(n -&gt; n % 2 == 0)
                          .map(n -&gt; n * n)
                          .toArray();

// 2. 집계 (max, sum 등)
int max = Arrays.stream(nums).max().orElse(0);

// 3. 인덱스 기반 스트림 (IntStream)
int weightedSum = IntStream.range(0, nums.length)
                           .map(i -&gt; nums[i] * i)
                           .sum();</code></pre>
<hr>
<h2 id="5-대표-패턴-및-실전-예제">5. 대표 패턴 및 실전 예제</h2>
<h3 id="5-1-빈도-세기-counting">5-1. 빈도 세기 (Counting)</h3>
<p>값의 범위가 작을 때 배열 인덱스를 키(Key)로 활용합니다. ($O(n)$)</p>
<pre><code class="language-java">int[] count = new int[26];
for (char c : &quot;hello&quot;.toCharArray()) {
    count[c - &#39;a&#39;]++; // 알파벳별 개수 저장
}</code></pre>
<h3 id="5-2-투-포인터-two-pointers">5-2. 투 포인터 (Two Pointers)</h3>
<p>정렬된 배열에서 양끝 포인터를 좁혀가며 조건을 찾습니다. ($O(n)$)</p>
<pre><code class="language-java">Arrays.sort(nums);
int left = 0;
int right = nums.length - 1;

while (left &lt; right) {
    int sum = nums[left] + nums[right];
    if (sum == target) return true;
    if (sum &lt; target) left++;
    else right--;
}
return false;
</code></pre>
<h3 id="5-3-누적합-prefix-sum">5-3. 누적합 (Prefix Sum)</h3>
<p>반복적인 구간 합 쿼리를 $O(1)$에 해결합니다.</p>
<pre><code class="language-java">int[] nums = {2, 1, 5, 3, 4};
int n = nums.length;

// 전처리: prefix[i] = 0부터 i-1까지의 합
int[] prefix = new int[n + 1];
for (int i = 0; i &lt; n; i++) prefix[i + 1] = prefix[i] + nums[i];

// 구간 [l, r] 합
int l = 1, r = 3; // 1 + 5 + 3 = 9
int sum = prefix[r + 1] - prefix[l];

</code></pre>
<h3 id="5-4-슬라이딩-윈도우-sliding-window">5-4. 슬라이딩 윈도우 (Sliding Window)</h3>
<p>고정된 크기 또는 가변 크기의 윈도우를 배열 위에서 이동시키며 조건을 만족하는 구간을 찾습니다. ($O(n)$)</p>
<pre><code class="language-java">public int minSubArrayLen(int target, int[] nums) {
    int left = 0, sum = 0;
    int minLen = Integer.MAX_VALUE;

    for (int right = 0; right &lt; nums.length; right++) {
        sum += nums[right];

        while (sum &gt;= target) {
            minLen = Math.min(minLen, right - left + 1);
            sum -= nums[left++];
        }
    }

    return minLen == Integer.MAX_VALUE ? 0 : minLen;
}</code></pre>
<h3 id="5-5-카데인-알고리즘-kadanes-algorithm">5-5. 카데인 알고리즘 (Kadane&#39;s Algorithm)</h3>
<p><strong>연속된 부분 배열의 최대 합</strong>을 구하는 DP의 기초 알고리즘입니다. ($O(n)$)</p>
<pre><code class="language-java">public int maxSubArray(int[] nums) {
    int maxSum = nums[0];
    int currentSum = nums[0];

    for (int i = 1; i &lt; nums.length; i++) {
        // [DP 로직] 현재 원소부터 새로 시작할지, 기존 합에 더해서 이어갈지 결정
        currentSum = Math.max(nums[i], currentSum + nums[i]);

        // 전체 구간 중 가장 컸던 합을 갱신
        maxSum = Math.max(maxSum, currentSum);
    }

    return maxSum;
}</code></pre>
<h3 id="5-6-그래프-인접-리스트-arraylist-활용">5-6. 그래프 인접 리스트 (ArrayList 활용)</h3>
<p>정점별 연결 정보를 저장해야 하는 그래프 문제에 활용합니다.($O(E)$)</p>
<pre><code class="language-java">import java.util.*;

public List&lt;List&lt;Integer&gt;&gt; buildGraph(int n, int[][] edges) {
    List&lt;List&lt;Integer&gt;&gt; graph = new ArrayList&lt;&gt;();
    for (int i = 0; i &lt; n; i++) graph.add(new ArrayList&lt;&gt;());

    for (int[] e : edges) {
        int u = e[0], v = e[1];
        graph.get(u).add(v);
        graph.get(v).add(u); // 무방향
    }
    return graph;
}</code></pre>
<h3 id="5-7-2차원-배열-상하좌우-탐색">5-7. 2차원 배열 상하좌우 탐색</h3>
<p>BFS/DFS로 격자에서 연결 요소 탐색, 최단거리, 영역 개수 세기, flood fill 등에 사용합니다. ($O(R*C)$)</p>
<pre><code class="language-java">import java.util.*;

public class GridBfsCount {
    static final int[] dr = {-1, 1, 0, 0};
    static final int[] dc = {0, 0, -1, 1};

    public static int countIslands(int[][] grid) {
        int R = grid.length, C = grid[0].length;
        boolean[][] visited = new boolean[R][C];

        int count = 0;
        for (int r = 0; r &lt; R; r++) {
            for (int c = 0; c &lt; C; c++) {
                if (grid[r][c] == 1 &amp;&amp; !visited[r][c]) {
                    bfs(grid, visited, r, c);
                    count++;
                }
            }
        }
        return count;
    }

    private static void bfs(int[][] grid, boolean[][] visited, int sr, int sc) {
        int R = grid.length, C = grid[0].length;

        Queue&lt;int[]&gt; q = new ArrayDeque&lt;&gt;();
        q.add(new int[]{sr, sc});
        visited[sr][sc] = true;

        while (!q.isEmpty()) {
            int[] cur = q.poll();
            int r = cur[0], c = cur[1];

            for (int d = 0; d &lt; 4; d++) {
                int nr = r + dr[d], nc = c + dc[d];
                if (nr &lt; 0 || nr &gt;= R || nc &lt; 0 || nc &gt;= C) continue;

                if (grid[nr][nc] == 1 &amp;&amp; !visited[nr][nc]) {
                    visited[nr][nc] = true;
                    q.add(new int[]{nr, nc});
                }
            }
        }
    }
}

</code></pre>
<hr>
<h2 id="6-자주-하는-실수">6. 자주 하는 실수</h2>
<ol>
<li>배열은 <code>arr.length</code> / <code>리스트는 list.size()</code></li>
<li><strong>2차원 배열 복사</strong>: <code>matrix.clone()</code>은 얕은 복사 -&gt; 행마다 <code>clone()</code> 필요</li>
<li><code>ArrayList.remove(int index)</code>: 값이 아니라 인덱스 삭제임</li>
<li><strong>빈 배열 예외</strong>: <code>max()</code>, <code>min()</code> 같은 연산은 빈 배열 처리를 하지 않으면 에러가 날 수 있음 (<code>orElse</code>, 조건문)</li>
</ol>
<hr>
<h2 id="마무리">마무리</h2>
<p>배열은 코딩 테스트에서 가장 자주 등장하는 기본 자료구조이기 때문에 꼭 알아둘 필요가 있습니다.
이번에 내용을 정리하면서 다시 공부해보았는데, 특히 자주 쓰는 패턴들을 정리하는 과정이 큰 도움이 되었습니다.</p>
<p>다음 포스팅에서는 <strong>Stack/Queue</strong>를 정리해보겠습니다.🙌</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[첫 면접, 도망가고 싶었지만 그래도 보길 잘했다]]></title>
            <link>https://velog.io/@jh_devlog/%EC%B2%AB-%EB%A9%B4%EC%A0%91-%EB%8F%84%EB%A7%9D%EA%B0%80%EA%B3%A0-%EC%8B%B6%EC%97%88%EC%A7%80%EB%A7%8C-%EA%B7%B8%EB%9E%98%EB%8F%84-%EB%B3%B4%EA%B8%B8-%EC%9E%98%ED%96%88%EB%8B%A4</link>
            <guid>https://velog.io/@jh_devlog/%EC%B2%AB-%EB%A9%B4%EC%A0%91-%EB%8F%84%EB%A7%9D%EA%B0%80%EA%B3%A0-%EC%8B%B6%EC%97%88%EC%A7%80%EB%A7%8C-%EA%B7%B8%EB%9E%98%EB%8F%84-%EB%B3%B4%EA%B8%B8-%EC%9E%98%ED%96%88%EB%8B%A4</guid>
            <pubDate>Tue, 06 Jan 2026 16:06:35 GMT</pubDate>
            <description><![CDATA[<p>오늘 첫 번째 면접을 봤고, 기억이 생생할 때 후기를 남겨보려고 한다.
(사실 완전 첫 면접은 아니지만, 백엔드 개발자로서는 처음이었다.)</p>
<p>면접 전에는 솔직히 준비가 많이 부족하다는 걸 스스로 느껴 도망가고 싶은 마음이 컸다.
그럼에도 &quot;실전 경험에서만 얻을 수 있는 게 분명히 있다&quot;고 생각했고,
면접이 끝난 지금은 역시 <strong>보길 잘했다</strong>는 결론이다.</p>
<hr>
<h3 id="어떤-회사였나">어떤 회사였나?</h3>
<p>이번에 면접 본 회사는 <strong>온라인 플랫폼/서비스를 운영하는 B2B 스타트업</strong>이었다.
설립된 지 오래되지는 않았지만, 서비스 방향과 성장 흐름을 보았을 때 성장 가능성이 커 보이는 팀이라는 인상을 받았다.</p>
<hr>
<h3 id="면접-방식">면접 방식</h3>
<p>면접은 크게 아래 3가지로 구성되어 있었다.</p>
<ul>
<li>간단한 실무 테스트</li>
<li>기술 면접(이력서 기반 포함)</li>
<li>기타 질문(컬쳐핏/커뮤니케이션/성향 질문)</li>
</ul>
<p>실무 테스트는 처음이라 정말 무서웠다.
어떻게 대비해야 하는지 감도 잘 안 잡혔고, 그래서 테스트 대비보다는 <strong>기술 질문 / 이력서 기반 질문 / 컬쳐핏 질문</strong> 위주로 준비했다.</p>
<hr>
<h3 id="실무-테스트｜20분-그리고-ai-자유-사용">실무 테스트｜20분, 그리고 AI 자유 사용</h3>
<p>실무 테스트는 메일로 전달받은 링크로 접속해 진행하는 방식이었다.
문제 상황이 주어지고, 질문별로 파트가 나뉘어 있었으며 각 파트마다 <strong>요구사항(무엇을 작성해야 하는지)</strong>도 정리되어 있었다.</p>
<p>가장 특이했던 점은 <strong>AI 사용이 완전히 허용</strong>되었다는 것.
면접관이 “AI를 어떻게 활용하는지도 함께 본다”고 말해주셔서, 요즘은 진짜로 <strong>AI 활용 역량</strong> 자체가 평가 요소가 될 수 있겠다는 걸 체감했다.</p>
<p>(화면 공유 상태로 진행되었고 손이 떨려 계속 오타가 났다. 😨)</p>
<h3 id="결국-주어진-시간에-문제를-다-풀지-못했다⏰">결국 주어진 시간에 문제를 다 풀지 못했다..⏰</h3>
<p>주어진 시간은 20분, 풀어야 하는 문제 상황은 2개였다.</p>
<p>그런데 첫 번째 문제부터 문제 파악 자체가 어려웠었다.</p>
<p>AI를 마음대로 써도 된다고는 했지만, “활용 방식도 본다”는 말이 신경 쓰여서 다음 방식으로 접근했다.</p>
<ul>
<li>먼저 <strong>내가 문제를 최대한 이해</strong>한다</li>
<li>그 다음 <strong>AI에게 상황 분석을 요청</strong>한다</li>
<li>결과 중 <strong>필요한 부분만 선택해서 정리</strong>한다</li>
</ul>
<p>다만 이 방식은 생각보다 시간이 많이 걸렸다.
결국 시간 배분에 실패했고, 두 번째 문제는 아예 손도 못 대고 끝나버렸다… 🥲</p>
<p>다른 지원자 분은 첫 문제를 푸는 동시에 두 번째 문제도 미리 AI에 붙여넣어 두 문제를 동시에 분석해두고 진행했다고 한다.
돌이켜보면 그 방향이 더 좋은 전략이었던 것 같다.
(특히 스타트업 환경에서는 “완벽한 이해”보다 <strong>제한된 시간 안에 요구사항을 빠르게 충족하는 능력</strong>을 더 중요하게 볼 것 같았다.)</p>
<p>면접 중에도 AI 활용 방식에 대한 질문이 있었고, 전반적으로 이 회사가 <strong>AI 도구 활용 역량</strong>을 꽤 중요하게 본다는 느낌을 강하게 받았다.
요즘은 기술 공부뿐 아니라, AI를 ‘어떻게 잘 쓰는지’도 꾸준히 연습해야겠다는 생각이 들었다.</p>
<hr>
<h3 id="기술-면접｜22-그리고-멘붕">기술 면접｜2:2, 그리고 멘붕</h3>
<p>면접은 <strong>2:2</strong>로 진행됐다.</p>
<p>이력서 기반 질문이 시작되자, 함께 면접 본 다른 지원자 분이 말을 너무 잘하셔서 나도 모르게 자신감이 급격히 떨어졌다.
게다가 준비하지 못한 질문도 나와 답변이 깔끔하게 나오지 않았고, 특히 <strong>기술 선택/의사결정 이유</strong>를 묻는 질문에서 준비 부족이 그대로 드러났다.</p>
<p>주로 내가 했던 선택의 이유를 묻는 질문이 많았다.</p>
<ul>
<li>특정 기능 고도화를 위해 기술을 도입했다가 다시 단순화했는데, 그때 <strong>복잡도와 가치의 균형</strong>을 어떤 기준으로 판단했는지</li>
<li>멀티 인스턴스 환경에서 채팅 메시지 동기화에 Redis를 선택한 이유는 무엇인지</li>
</ul>
<p>결국은 내가 내린 판단의 기준과 근거를 설명하는 질문들이었는데, 이 부분을 더 구조적으로 준비해야겠다고 느꼈다.</p>
<blockquote>
<p>“기술을 왜 선택했는지”는
단순히 경험을 나열하는 게 아니라,
<strong>기준과 근거를 구조적으로 설명할 수 있어야 한다.</strong></p>
</blockquote>
<hr>
<h3 id="컬쳐핏-질문｜오히려-더-편하게-답했다">컬쳐핏 질문｜오히려 더 편하게 답했다</h3>
<p>기술 면접 이후에는 기술 외 질문을 하는 시간도 있었다.</p>
<ul>
<li>개발을 시작한 계기</li>
<li>스트레스 해소 방식</li>
<li>갈등 해결 방식 등</li>
</ul>
<p>이런 질문들은 비교적 편하게 답할 수 있었다.
기술 질문보다 오히려 이쪽이 내 경험을 자연스럽게 꺼내기 쉬웠던 것 같다.</p>
<hr>
<h3 id="전체적인-평가">전체적인 평가</h3>
<p>이번 면접은 솔직히 잘 봤다고 말하기는 어렵다.
특히 실무 테스트에서는 시간 배분 실패로 아쉬움이 컸고, 기술 질문 중 일부는 준비가 부족해서 답변이 흔들렸다.</p>
<p>그럼에도 불구하고 이번 면접을 통해 얻은 건 많은 것 같다.</p>
<ul>
<li>실무 테스트는 ‘문제 파악 + 시간 관리’가 핵심</li>
<li>AI 활용 방식 자체가 평가 포인트가 될 수 있음</li>
<li>기술 선택 질문은 기준/근거/대안까지 준비해야 함</li>
<li>“면접 경험”은 준비 부족을 정확히 보여주는 최고의 피드백</li>
</ul>
<p>아쉬움이 남았지만, 다음 면접을 더 잘 보기 위한 확실한 피드백을 얻은 하루였다.</p>
<hr>
<h3 id="추가로">추가로...</h3>
<p>면접을 끝내고, 그냥 집에 가기엔 헛헛해서 교보문고에 들렀다.
장바구니에 담아두었던 책들도 사고, 잠깐 리프레시도 했다.</p>
<p><img src="https://velog.velcdn.com/images/jh_devlog/post/7ae6ca22-bf4f-497a-ab83-9f6765d8954e/image.jpeg" alt="">
(우울해서 책 샀슨....🥺)</p>
<p>다음 면접 전까지는 책도 조금씩 읽으면서 CS 공부를 매일 루틴으로 다시 잡아봐야겠다.
그래도 내내 걱정하던 면접이 끝났다는 사실만으로도 한결 후련하다. 🙂👍</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2025년 회고]]></title>
            <link>https://velog.io/@jh_devlog/2025%EB%85%84%EC%9D%84-%EB%8F%8C%EC%95%84%EB%B3%B4%EB%A9%B0</link>
            <guid>https://velog.io/@jh_devlog/2025%EB%85%84%EC%9D%84-%EB%8F%8C%EC%95%84%EB%B3%B4%EB%A9%B0</guid>
            <pubDate>Wed, 31 Dec 2025 04:02:45 GMT</pubDate>
            <description><![CDATA[<h2 id="2025년을-돌아보며">2025년을 돌아보며</h2>
<p>2025년은 <strong>내가 가야 할 방향을 정하고, 끝까지 해내는 해</strong>였다.
불안이 있어도 결정을 미루지 않았고, 그 선택의 결과를 스스로 감당하는 법을 배웠다.
올해의 키워드를 따라가며 2025년을 차근차근 정리해보려 한다.</p>
<hr>
<h2 id="올해의-키워드">올해의 키워드</h2>
<p>올해를 대표하는 키워드는 다음 다섯 가지다.</p>
<p><strong>1. 퇴사
2. 코드잇 스프린트
3. 렌즈삽입술
4. 발리 여행
5. 정보처리기사 실기 &amp; 운전면허</strong></p>
<hr>
<h2 id="1-퇴사">1. 퇴사</h2>
<p><img src="https://velog.velcdn.com/images/jh_devlog/post/8927f7fa-efc0-438d-b102-2dae5b7bc9cb/image.png" alt=""></p>
<p>올해 가장 큰 사건 중 하나는 2년 동안 다니던 회사 MWW를 퇴사한 일이다.
퇴사를 결심한 이유는 여러 가지가 있었지만, 회사 상황에 대한 불안과 내 커리어 방향에 대한 고민이 가장 컸던 것 같다.</p>
<p>돌이켜보면 MWW는 &#39;커리어를 설계해서 들어간 회사&#39;라기보다, 당장의 취업이라는 목표에 더 가까운 선택이었다.
대학 졸업 후 학교 연계로 회사를 알게 되었고, 당시에는 “내가 앞으로 어떤 개발자가 되고 싶은지”보다 “일단 사회에 나가 일해보자”가 우선이었다. 
(대학 프로젝트가 챗봇과 관련이 있었고 회사도 챗봇 회사라서 더 쉽게 결정했던 것 같다.)</p>
<p>그렇게 인턴을 거쳐 정규직으로 전환되면서 첫 회사 생활을 시작했다.
지금와서 이때를 돌아보니, 나는 취업 준비의 어려움을 겪어보지 못하고 빠르게 사회인이 되었구나하는 생각이 든다.</p>
<p>첫 회사로 MWW를 다닌 건 행운이었다.객관적으로 봐도 복지가 좋았고, 좋은 사람들이 많았다. 
(특히 4.5일제는 앞으로 어떤 회사를 가더라도 종종 떠올리게 될 것 같다. 삶의 질 향상 Goat...✨)</p>
<p>무엇보다 이 회사에서 얻은 가장 큰 건 사람이었다.
7명의 동기들과 함께하며 많은 의지가 되었고, 업무 외에도 개인적으로 자주 놀러다니며 가까워졌다.
물론 프로젝트를 하며 힘든 순간도 있었지만, 그 시간을 포함해서 MWW는 내게 좋은 추억으로 남을 것 같다.</p>
<hr>
<h2 id="2-코드잇-스프린트">2. 코드잇 스프린트</h2>
<p><img src="https://velog.velcdn.com/images/jh_devlog/post/756a49c3-e4af-461d-836b-4471387f06ed/image.jpeg" alt="">
<img src="https://velog.velcdn.com/images/jh_devlog/post/81b10aab-4bd8-49e3-8010-2f1d74d90ba7/image.png" alt=""></p>
<p>퇴사를 결심한 뒤, 내가 앞으로 가고 싶은 방향이 <strong>‘백엔드 개발자’</strong>라는 건 분명했다.
다만 그 방향으로 가기엔 내가 아직 많이 부족하다는 것을 알고 있었고, 본격적으로 부트캠프를 알아보기 시작했다. 그중 코드잇 스프린트 과정이 가장 눈에 들어왔다.</p>
<p>지원 과정에서 코딩테스트와 면접을 통해 인원을 선발한다는 점이 인상적이었고, 커리큘럼도 내가 원하던 학습 방식과 잘 맞았다. 
무엇보다 수료 이후에도 커리어 프로그램을 통해 취업을 지원한다는 점이 좋았다.</p>
<p>그렇게 스프린트에 지원했고, Spring Backend 3기로 참여하게 되었다.</p>
<p>처음에는 비대면 수업이 아쉽게 느껴졌다. 그런데 시간이 지날수록 오히려 이 방식에 감사해졌다. 집중해서 수업을 듣고, 과제를 하고, 프로젝트까지 따라가려면 체력과 시간이 정말 많이 필요했기 때문이다.
학교 다니던 시절을 떠올리며 설레는 마음으로 시작했지만, 9시부터 19시까지 이어지는 풀타임 일정은 예상보다 훨씬 힘들었다.🥹 </p>
<p>그렇지만 그만큼 얻은 것도 컸다. 단순히 새로운 지식을 배운 것 이상으로, 내가 지금 어느 수준에 있는지를 정확히 알게 됐다.
(이전의 나는 정말 개발자라 하기엔 민망한 수준이었음을...)
스프린트 과정을 거치며 “이걸 안 들었으면 큰일 날 뻔했다”는 생각을 여러 번 했던 것 같다. </p>
<p>과정을 수행하면서 과제와 프로젝트를 병행하느라 시간이 정말 빠르게 지나갔다.
그 과정 속에서 작은 성취들도 쌓였고, 사진처럼 엉덩이 1톤상도 받았다. 스스로 생각해도 성실하게 참여했던 것 같아서 뿌듯하다. 😎✌️</p>
<p>수료 이후에는 조금 느슨해지긴 했는데, 이제부터는 내가 스스로 루틴을 만들고 유지하는 단계라고 생각한다. 정신차리고 열심히 하자...💪</p>
<hr>
<h2 id="3-렌즈삽입술">3. 렌즈삽입술</h2>
<p><img src="https://velog.velcdn.com/images/jh_devlog/post/46758635-ea31-45ef-8815-cd0fecb93c87/image.png" alt=""></p>
<p>올해의 또 다른 큰 이벤트는 렌즈삽입술(ICL) 수술이었다. 수술은 추석 연휴에 진행했다.
나는 평생 고도근시 시력으로 살며 학창 시절엔 3번 압축한 두께로 안경을 맞췄고🤓, 성인이 된 후로는 렌즈 없이는 밖에 나가기가 어려울 정도였다.
(진짜 심각한 고도근시였다. 왼쪽 -9.5 오른쪽 -10.0😱)</p>
<p>그래서 시력 교정 수술은 오래전부터 하고 싶었으나, 돈과 시간이 동시에 맞아떨어지는 시기가 없었다.
이번에 퇴사를 하며 퇴직금이 생겼고, 마침 긴 추석 연휴까지 겹치면서 “지금이 아니면 또 미루겠다”는 생각에 수술을 결심했다.</p>
<p>렌즈삽입술은 다른 수술들보다 회복이 빠르고, 각막을 직접 깎지 않는 방식이라 그나마 안전하다고 생각해 이를 선택했다.
후기를 찾아보면 “크게 아프지 않고 금방 끝난다”는 말이 많아서 큰 걱정 없이 병원에 갔는데.. 그래도 수술은 수술이었다.</p>
<p>내 예상보다 훨씬 아팠고, 수술 다음 날 까지는 하루 종일 선글라스를 끼고 눈물이 계속 났었다.
그렇지만 회복은 정말 빠르게 됐고, 지금은 수술을 후회하진 않는다.</p>
<p>아침에 일어나서 안경을 찾지 않아도 되고, 외출 전에 렌즈부터 챙기지 않아도 된다는 점이 정말 좋다. 삶의 질이 확실히 올라갔다.🙌</p>
<hr>
<h2 id="4-발리-여행">4. 발리 여행</h2>
<p><img src="https://velog.velcdn.com/images/jh_devlog/post/aed97702-3173-4c11-b305-e7ee359f93ac/image.png" alt=""></p>
<p>코드잇 스프린트 과정을 수료한 뒤, 친구들과 발리로 여행을 다녀왔다.
올해 초부터 계획했던 일정이었는데, 마침 스프린트를 끝낸 직후라 마음 편히 쉬고 올 수 있었던 여행이었다.</p>
<p>발리는 확실히 휴양지로 좋았지만, 벌레가 꽤 많아서 예상보다 쉽지 않기도 했다. 😅
그래도 지프 투어, 스노클링 같은 액티비티를 즐기고, 사진도 많이 찍으면서 좋은 추억을 많이 남겼다.</p>
<p>한국으로 돌아온 뒤에도 한동안 발리 생각을 자주 하며 마음이 붕 떠 있기도 했다.
그래도 덕분에 에너지를 충전했고, 다시 일상으로 돌아갈 힘도 얻은 것 같다.</p>
<p>내년에는 혼자 떠나는 여행도 한 번 도전해보고 싶다.</p>
<hr>
<h2 id="5-정보처리기사-실기--운전면허">5. 정보처리기사 실기 &amp; 운전면허</h2>
<p><img src="https://velog.velcdn.com/images/jh_devlog/post/1840521f-7095-4436-ae49-07e6a331c75f/image.png" alt=""></p>
<p>정보처리기사 실기와 운전면허 등 자격 취득에도 도전했다.
둘 다 “언젠가 해야지” 하고 미뤄두던 목표였는데, 막상 마음먹고 시작하니 생각보다 에너지와 시간이 많이 필요했다.</p>
<p>둘 다 한 번씩은 실패를 경험했지만, 그때 포기하지 않고 다시 준비해서 재도전했다.
그리고 결국 두 개 모두 합격했다. 완벽하게 한 번에 끝냈다기보다, 한 번 실패해도 다시 잡고 끝까지 해낸 결과라서 더 의미가 있는 것 같다.</p>
<p>내년에도 새로운 자격증 도전을 꾸준히 이어가고, 운전은 꾸준히 연습해서 실제 생활에서 자연스럽게 할 수 있는 수준까지 만들어보고 싶다.</p>
<hr>
<h2 id="마무리">마무리</h2>
<p>올해의 나는 <strong>결단, 성장, 회복</strong>으로 표현할 수 있을 것 같다.</p>
<p>무엇보다 “끝까지 해낸다”는 감각을 몸으로 얻은 게 올해의 가장 큰 수확이다.</p>
<p>이렇게 정리해보니 2025년을 나름 알차게 보낸 것 같아 만족스럽다.
내년의 나도 화이팅 🙌
일단 취업부터 하자!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[StaleObjectStateException 동시성 이슈 해결기]]></title>
            <link>https://velog.io/@jh_devlog/StaleObjectStateException-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%EA%B8%B0</link>
            <guid>https://velog.io/@jh_devlog/StaleObjectStateException-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88-%ED%95%B4%EA%B2%B0%EA%B8%B0</guid>
            <pubDate>Fri, 19 Dec 2025 07:50:42 GMT</pubDate>
            <description><![CDATA[<p>팀 프로젝트에서 로컬 개발 환경을 세팅하던 중 <strong>동시성 이슈(<code>StaleObjectStateException</code>)로 애플리케이션 기동이 실패</strong>했습니다.</p>
<p>처음엔 단순 트러블슈팅처럼 보였지만, 해결 과정에서 자연스럽게 “실제 서비스라면 어떻게 설계해야 할까?”까지 고민이 확장되어 정리해보았습니다.</p>
<hr>
<h3 id="🚨-사건의-발단--애플리케이션-기동-실패">🚨 사건의 발단 : 애플리케이션 기동 실패</h3>
<p>로컬에서 애플리케이션을 실행하자마자 아래 오류가 발생하며 기동이 실패했습니다.</p>
<blockquote>
<p><strong>StaleObjectStateException</strong>: Row was updated or deleted by another transaction</p>
</blockquote>
<p>원인을 추적해보니 <strong>초기 데이터 자동 입력을 담당하는 데이터 시더(Seeder)</strong>가 문제였습니다.</p>
<h3 id="무엇이-문제였나">무엇이 문제였나?</h3>
<p><code>Comment(댓글)</code> 데이터를 넣는 과정에서, 부모 엔티티인 <code>Article(기사)</code>의 <code>commentCount</code>를 업데이트하는 로직이 함께 실행되고 있었는데요.
이 과정에서 <strong>같은 <code>Article</code> 레코드를 여러 트랜잭션이 동시에 수정</strong>하려고 하면서 충돌이 발생했습니다.</p>
<p>즉, “로컬 실행 → 시딩 → 동일 Row 업데이트 경합 → 예외 → 애플리케이션 종료” 흐름으로 개발 환경 세팅 자체가 막혀버렸습니다.</p>
<blockquote>
<p>JPA에서는 동일 엔티티를 여러 트랜잭션이 동시에 갱신할 경우, 먼저 커밋된 변경으로 인해 이후 트랜잭션의 update가 실패하면서 <code>StaleObjectStateException</code>이 발생할 수 있습니다.</p>
</blockquote>
<hr>
<h3 id="💡-해결책--실행-순서를-명확히-하자">💡 해결책 : “실행 순서”를 명확히 하자</h3>
<p>핵심 원인은 <strong>시더 실행 순서가 보장되지 않는 상태에서</strong>, 동일 레코드를 건드리는 작업이 겹쳤다는 점이었습니다.</p>
<p>그래서 모든 시더를 한 곳에서 통제하는 <code>AllDataSeederRunner</code>를 도입해 <strong>순차 실행을 강제</strong>했습니다.</p>
<pre><code class="language-java">@Profile(&quot;dev&quot;)
@Component
@RequiredArgsConstructor
public class AllDataSeederRunner {
    private final List&lt;DataSeeder&gt; seeders;

    @PostConstruct
    public void runAllSeeders() {
        seeders.stream()
            .sorted(Comparator.comparingInt(this::getOrder))
            .forEach(DataSeeder::seed);
    }

    private int getOrder(DataSeeder seeder) {
        if (seeder instanceof UserDataSeeder) return 1;
        if (seeder instanceof InterestDataSeeder) return 2;
        if (seeder instanceof ArticleDataSeeder) return 3;
        if (seeder instanceof CommentDataSeeder) return 4;
        if (seeder instanceof CommentLikeDataSeeder) return 5;
        return 99;
    }
}</code></pre>
<p>이렇게 실행 순서를 명확히 하니 충돌 없이 데이터가 정상 입력되었고, 애플리케이션도 정상 기동되었습니다. 😎</p>
<hr>
<h3 id="✅-결과-일관된-개발-환경-보장">✅ 결과: 일관된 개발 환경 보장</h3>
<p>순차 실행을 보장함으로써 <strong>동일 레코드에 대한 경합을 원천 차단</strong>할 수 있었습니다.
덕분에 팀원 모두가 로컬에서 매번 동일하고 안정적인 초기 데이터를 기준으로 개발을 진행할 수 있게 되었습니다.</p>
<hr>
<h3 id="🤔-만약-실제-서비스에서-동시성-이슈가-발생한다면">🤔 만약 &quot;실제 서비스&quot;에서 동시성 이슈가 발생한다면?</h3>
<p>시더 문제는 순차 실행으로 해결했지만, &quot;수만 명의 유저가 사용하는 실제 서비스라면?&quot; 이야기가 완전히 달라질 수 있습니다.</p>
<p>예를 들어 회원가입/결제처럼 중복 요청이 들어오면 치명적인 도메인에서는, 단순한 <code>if (exists)</code> 체크만으로 동시성을 막을 수 없습니다.</p>
<h3 id="💭-예시-회원가입-요청이-중복으로-들어온다면">💭 예시: 회원가입 요청이 중복으로 들어온다면?</h3>
<p>유저가 가입 버튼을 여러 번 누르거나 네트워크 문제로 요청이 중복 전송되는 상황을 상상해보겠습니다.</p>
<ul>
<li>요청 A: “홍길동 아이디 있나요?” → 서버: “없네요, 가입 진행!” (처리 중)</li>
<li>요청 B: (A 완료 직전) “홍길동 있나요?” → 서버: “아직 없네요, 가입 진행!”</li>
</ul>
<p>-&gt; 결과: <strong>동일 아이디 유저가 중복 생성될 수 있음</strong> 😱</p>
<hr>
<h3 id="🛠️-동시성-대응-어떤-선택지가-있을까">🛠️ 동시성 대응: 어떤 선택지가 있을까?</h3>
<p>동시성 이슈를 막는 방법은 상황에 따라 달라지지만, 크게 아래 3가지 축으로 정리할 수 있습니다.</p>
<h3 id="1-db-제약-조건unique-index-활용">1) DB 제약 조건(Unique Index) 활용</h3>
<ul>
<li>DB 레벨에서 UNIQUE를 걸어두면, 마지막 커밋 순간에 DB가 중복을 막아줍니다.</li>
<li>애플리케이션은 중복 키 예외를 잡아서 <strong>적절한 에러 응답/재시도 정책</strong>을 설계하면 됩니다.</li>
<li>실무에서는 보통 가장 기본이자 반드시 필요한 안전장치입니다.</li>
</ul>
<h3 id="2-애플리케이션-레벨-락lock">2) 애플리케이션 레벨 락(Lock)</h3>
<ul>
<li><p><strong>낙관적 락(Optimistic Lock)</strong> 
  <code>@Version</code>으로 버전을 비교해, 누군가 먼저 수정했다면 “충돌 → 재시도/실패 처리”로 대응합니다. (충돌이 드물고 재시도 가능한 경우 유리)</p>
</li>
<li><p><strong>비관적 락(Pessimistic Lock)</strong>
  <code>SELECT ... FOR UPDATE</code>처럼 조회 시점부터 락을 걸어 다른 트랜잭션 접근을 막습니다.
(정합성이 극도로 중요한 구간에서 사용하지만, 성능/대기시간 비용이 큼)</p>
</li>
</ul>
<h3 id="3-분산-락-distributed-lock">3) 분산 락 (Distributed Lock)</h3>
<ul>
<li>서버가 여러 대인 분산 환경에서는 DB 락만으로 부족할 수 있어 Redis 같은 외부 저장소로 락을 관리합니다.</li>
<li>“특정 키(예: 회원가입 아이디, 주문번호)에 대해 한 번에 한 요청만 처리” 같은 제어가 가능합니다.</li>
</ul>
<hr>
<h3 id="✨-마치며-결국-핵심은-멱등성">✨ 마치며: 결국 핵심은 “멱등성”</h3>
<p>이번 경험을 통해 가장 크게 느낀 건,</p>
<blockquote>
<p><strong>여러 번 호출돼도 결과는 같아야 한다(멱등성)</strong>
라는 원칙이 시스템 안정성의 핵심이라는 점이었습니다.</p>
</blockquote>
<p>예를 들어 결제 시스템이라면,</p>
<ul>
<li>주문 번호를 유니크하게 관리하고</li>
<li>요청마다 Request-ID 같은 <strong>중복 방지 키</strong>를 두며</li>
<li>중복 요청이 와도 “한 번 처리된 요청이면 같은 결과를 반환”하도록 설계해야 합니다.</li>
</ul>
<p>단순한 시더 오류 해결에서 시작된 고민이었지만, 동시성은 결국 <strong>시스템의 신뢰성을 결정짓는 방어선</strong>이라는 점을 깊이 체감했습니다.
앞으로도 “기능이 돌아가는 코드”를 넘어, 유저의 예측 불가능한 행동과 네트워크 불확실성까지 고려하는 방어적 설계를 습관화해야 할 것 같습니다.🙌</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SB 3기] 코드잇 고급 프로젝트 회고 : 옷장을 부탁해]]></title>
            <link>https://velog.io/@jh_devlog/SB-3%EA%B8%B0-%EC%BD%94%EB%93%9C%EC%9E%87-%EA%B3%A0%EA%B8%89-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0-%EC%98%B7%EC%9E%A5%EC%9D%84-%EB%B6%80%ED%83%81%ED%95%B4</link>
            <guid>https://velog.io/@jh_devlog/SB-3%EA%B8%B0-%EC%BD%94%EB%93%9C%EC%9E%87-%EA%B3%A0%EA%B8%89-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0-%EC%98%B7%EC%9E%A5%EC%9D%84-%EB%B6%80%ED%83%81%ED%95%B4</guid>
            <pubDate>Thu, 20 Nov 2025 07:20:41 GMT</pubDate>
            <description><![CDATA[<h2 id="코드잇-고급-프로젝트-회고--옷장을-부탁해">코드잇 고급 프로젝트 회고 : 옷장을 부탁해</h2>
<p>고급 프로젝트 종료 후 약 한 달이 지났다. (시간이 너무 빨라요⏰)
이제 더는 미룰 수 없다는 생각으로 회고를 작성해보려 한다..!
기간이 제일 길었던 만큼 새로운 기술 적용도 많이 하고 열정적으로 임했던 프로젝트였다.</p>
<hr>
<h2 id="🗂️-프로젝트-개요">🗂️ 프로젝트 개요</h2>
<blockquote>
<p><strong>옷장을 부탁해(Otboo)</strong>
실시간 날씨와 사용자의 옷장을 기반으로 오늘 입을 옷을 추천해주는 개인화 코디 추천 서비스입니다.
사용자가 보유한 의상을 체계적으로 관리하고, OOTD 피드·팔로우·DM 등 소셜 기능을 통해 코디를 공유할 수 있습니다.</p>
</blockquote>
<h3 id="✅-주요-기능">✅ 주요 기능</h3>
<ul>
<li><p><strong>사용자 · 인증 시스템</strong></p>
<ul>
<li>회원가입/로그인, OAuth2 기반 소셜 로그인(Google, Kakao)</li>
<li>프로필 정보 관리</li>
</ul>
</li>
<li><p><strong>날씨 · 위치 연동</strong></p>
<ul>
<li>기상청 API를 활용한 실시간/예보 날씨 데이터 수집 및 저장</li>
<li>Kakao API 기반 위치 검색</li>
</ul>
</li>
<li><p><strong>옷장 관리 시스템</strong></p>
<ul>
<li>의상 등록/수정/삭제, 속성 관리</li>
<li>외부 쇼핑몰 <strong>구매 링크 연동</strong> 기능</li>
</ul>
</li>
<li><p><strong>의상 추천 시스템</strong></p>
<ul>
<li>날씨, 사용자 프로필, 보유 의상을 조합한 코디 추천 로직 설계</li>
<li>LLM 기반 추천 이유 생성</li>
<li>추천 점수 기반 필터링 + 랜덤 폴백 전략으로 <strong>최소 추천 세트 보장</strong></li>
<li>Redis를 활용해 최근 추천 아이템을 TTL 동안 제외하여 <strong>중복 추천 방지</strong></li>
</ul>
</li>
<li><p><strong>피드 시스템</strong></p>
<ul>
<li>날씨·착장 정보를 포함한 OOTD 피드 등록/수정/삭제</li>
<li>댓글 등록/목록 조회, 좋아요/취소</li>
<li>피드 검색 및 정렬</li>
</ul>
</li>
<li><p><strong>실시간 기능</strong></p>
<ul>
<li>WebSocket 기반 실시간 DM 기능</li>
<li>팔로우/댓글/좋아요 등 이벤트 발생 시 <strong>실시간 알림 발송 및 목록 조회</strong></li>
</ul>
</li>
<li><p><strong>파일 업로드 · 인프라</strong></p>
<ul>
<li>AWS S3를 통한 피드/프로필 이미지 업로드 및 스토리지 관리</li>
<li>Docker 기반 로컬 개발 환경 구성, AWS ECS를 통한 컨테이너 배포</li>
</ul>
</li>
<li><p><strong>팔로우 시스템</strong></p>
<ul>
<li>사용자 간 팔로우/언팔로우 및 팔로잉 피드 조회</li>
</ul>
</li>
</ul>
<h3 id="📌-기술-요구-사항">📌 기술 요구 사항</h3>
<ul>
<li>유효성 검사</li>
<li>커스텀 예외 처리</li>
<li>로그 관리</li>
<li>테스트 주도 개발 (TDD)</li>
<li>테스트 커버리지 80% 이상</li>
<li>CI/CD 파이프라인 구축 (GitHub Actions)</li>
<li>Spring Batch를 통한 배치 작업 관리</li>
<li>분산 환경 구성</li>
<li>Elasticsearch를 활용하여 피드 검색 기능 개선</li>
</ul>
<hr>
<h3 id="👩💻-나의-역할">👩‍💻 나의 역할</h3>
<h4 id="1️⃣-기능-개발">1️⃣ 기능 개발</h4>
<ul>
<li><p><strong>OOTD 피드 도메인</strong></p>
<ul>
<li>피드 등록, 수정, 논리 삭제 기능 구현</li>
<li>정렬 및 커서 기반 페이지네이션 피드 목록 조회 기능 구현 (RDB, Elasticsearch)</li>
<li>댓글 등록, 목록 조회 기능 구현</li>
<li>피드 좋아요/취소 기능 구현</li>
</ul>
</li>
<li><p><strong>의상 추천</strong></p>
<ul>
<li>날씨 데이터, 사용자 보유 의상, 프로필 정보를 활용한 자체 추천 알고리즘 개발</li>
<li><strong>LLM 기반 후처리 레이어</strong> 설계/적용(프롬프트 설계 포함)로 추천 품질 고도화</li>
<li>추천 <strong>임계값 미달 시 랜덤 폴백</strong> 로직 추가로 무추천 케이스 최소화</li>
</ul>
</li>
<li><p><strong>OAuth2 소셜 계정 연동</strong></p>
<ul>
<li>Google 계정 연동 및 인증 구현</li>
<li>Kakao 계정 연동 및 인증 구현</li>
</ul>
</li>
<li><p>DM 대화방 목록 조회 API 설계 구현</p>
</li>
</ul>
<h4 id="2️⃣-개발-외-역할">2️⃣ 개발 외 역할</h4>
<ul>
<li><strong>프로젝트 UI 개선</strong><ul>
<li>로그인 화면 일러스트/레이아웃 수정</li>
<li>로고 리디자인</li>
<li>전반적인 색상 통일</li>
<li>DM 탭 분리 및 DM 대화방 목록 화면 추가</li>
</ul>
</li>
<li><strong>PM 역할</strong><ul>
<li>일정 관리 및 작업 재분배</li>
</ul>
</li>
</ul>
<h3 id="🔗-깃허브-링크">🔗 깃허브 링크</h3>
<ul>
<li><a href="https://github.com/33otot/sb03-otboo-team03">Otboo GitHub Repository</a></li>
</ul>
<hr>
<h2 id="📚-개발-환경-및-사용-스택">📚 개발 환경 및 사용 스택</h2>
<pre><code>⚙️ Backend Stack
📦 Framework
├── Spring Boot 3.5.5        # 메인 애플리케이션 프레임워크
├── Spring Data JPA          # ORM 및 데이터 접근
├── QueryDSL                   # 동적 쿼리 작성
├── Spring Batch             # 대용량 배치 처리 및 스케줄링
├── Spring Security          # 인증·인가 및 보안 설정
├── OAuth2 Client            # 소셜 로그인(OAuth2) 연동
├── Spring WebSocket         # 실시간 양방향 통신
├── Spring WebFlux           # 비동기 HTTP 클라이언트
├── Spring Mail              # 이메일 발송
└── Gradle                   # 빌드 및 의존성 관리 도구

🗄️ Database &amp; Cache
├── PostgreSQL              # 운영 환경 RDBMS
├── H2 Database             # 개발/테스트용 In-memory DB
└── Redis                   # 캐싱 관리

🔎 Search &amp; AI
├── Elasticsearch             # 검색 엔진
└── Spring AI 1.0.3           # OpenAI 연동

📧 Messaging
├── Apache Kafka            # 도메인 이벤트 스트리밍
└── Spring Kafka            # Kafka 퍼블리셔·컨슈머 구현

💿 Storage &amp; External APIs
├── AWS S3 2.31.7           # 파일 스토리지
└── Jsoup 1.18.1            # HTML 파싱

📚 Documentation
├── Swagger/OpenAPI 3.0     # API 문서 자동화
└── Notion                  # 프로젝트 문서 및 협업 기록
└── Github Projects         # 일정 및 이슈 관리

🔧 Development Tools
├── IntelliJ IDEA           # 통합 개발 환경(IDE)
├── Git &amp; GitHub            # 버전 관리 및 협업
├── Discord                 # 팀 커뮤니케이션
└── Postman                 # API 테스트 도구

🚀 Infrastructure &amp; Deployment
├── AWS                     # 클라우드 인프라
└── Docker                  # 컨테이너 기반 실행·배포</code></pre><hr>
<h2 id="🛠️-트러블슈팅-사례">🛠️ 트러블슈팅 사례</h2>
<h3 id="1-피드날씨-연관관계-설계-오류">1) 피드–날씨 연관관계 설계 오류</h3>
<ul>
<li><p><strong>현상</strong>  </p>
<ul>
<li>피드 목록 조회 시 <code>weather_id</code>가 <code>null</code>인 레코드가 들어오면서, 프론트에서 렌더링이 실패했다.</li>
<li>weatherId가 null로 설정될 경우 서비스단에서 날씨 정보를 찾을 수 없다는 404에러가 발생했다.</li>
</ul>
</li>
<li><p><strong>원인</strong> </p>
<ul>
<li>초기 스키마에서 <code>weather_id</code>에 NOT NULL 제약을 걸지 않았다.</li>
<li>외래키 제약을 <code>ON DELETE SET NULL</code>로 설정해, 날씨 데이터가 삭제되면 피드의 <code>weather_id</code>가 <code>null</code>로 바뀌도록 설계했다.</li>
<li>서비스 단에서는 항상 유효한 날씨 정보가 있다고 가정하고 조회했다.</li>
</ul>
</li>
<li><p><strong>해결</strong>  </p>
<ul>
<li><code>weather_id</code> 컬럼에 <strong>NOT NULL 제약</strong>을 추가했다.</li>
<li>외래키 제약 조건을 <strong><code>ON DELETE RESTRICT</code></strong>로 변경해, 참조 중인 날씨 데이터는 삭제되지 않도록 보호했다.</li>
</ul>
</li>
<li><p><strong>배운 점</strong><br>조회에 <strong>필수적인 연관관계</strong>는 애플리케이션이 아니라 <strong>DB 스키마에서 강하게 보장</strong>하는 것이 안전하다.<br>특히 <code>ON DELETE SET NULL</code>은 편리해 보이지만, 실제 비즈니스 요구사항과 맞지 않으면 오히려 장애의 원인이 될 수 있다.</p>
</li>
</ul>
<h3 id="2-피드-목록-조회---카운트-갱신-시-updatedat-미갱신">2) 피드 목록 조회 - <strong>카운트 갱신 시 <code>updatedAt</code> 미갱신</strong></h3>
<ul>
<li><p><strong>현상</strong>  </p>
<ul>
<li>좋아요/댓글 카운트만 증가시키는 업데이트 후에도 <code>updated_at</code>이 갱신되지 않았다.</li>
</ul>
</li>
<li><p><strong>원인</strong>  </p>
<ul>
<li>카운트 갱신에 <strong>JPQL 벌크 UPDATE</strong>를 사용했다.</li>
<li>벌크 UPDATE는 영속성 컨텍스트를 거치지 않기 때문에, JPA Auditing(@LastModifiedDate)이 적용되지 않는다.</li>
</ul>
</li>
<li><p><strong>해결</strong>  </p>
<ul>
<li>쿼리를 <strong>네이티브 UPDATE</strong>로 전환했다.</li>
<li><code>updated_at = CURRENT_TIMESTAMP</code>를 쿼리에서 명시적으로 업데이트하도록 변경했다.</li>
</ul>
</li>
<li><p><strong>배운 점</strong><br>대량 업데이트가 필요해 벌크 UPDATE를 사용할 때는,<br><strong>Auditing/엔티티 리스너가 동작하지 않는다는 점을 항상 염두</strong>에 두고 쿼리에서 필드를 갱신해야 한다.</p>
</li>
</ul>
<h3 id="3-commentrepositorytest-간헐적-실패---시간-의존-테스트">3) CommentRepositoryTest 간헐적 실패 - 시간 의존 테스트</h3>
<ul>
<li><p><strong>현상</strong> </p>
<ul>
<li>댓글 정렬/조회 관련 테스트가 로컬에서는 통과하지만, CI 환경이나 특정 시점에 간헐적으로 실패했다.</li>
</ul>
</li>
<li><p><strong>원인</strong>  </p>
<ul>
<li>테스트 데이터의 시간 값을 <code>Instant.now()</code>로 생성했다.</li>
<li>실행 타이밍에 따라 경계 조건(예: 동일 시각, 정렬 순서)이 달라지는 <strong>비결정적 테스트</strong>가 되었다.</li>
</ul>
</li>
<li><p><strong>해결</strong>  </p>
<ul>
<li>테스트에서 사용하는 시간을 <code>Instant.parse(&quot;2025-01-01T00:00:00Z&quot;)</code>처럼 <strong>고정 값</strong>으로 치환하여 <strong>결정적 테스트</strong>를 보장했다.</li>
</ul>
</li>
<li><p><strong>배운 점</strong><br>테스트는 항상 <strong>결정적(deterministic)</strong>이어야 한다.<br>현재 시간, 랜덤 값에 직접 의존하는 코드는 설계 단계에서부터 테스트 가능성을 함께 고려해야 한다.</p>
</li>
</ul>
<h3 id="4-배포-환경에서-피드-조회-실패--es-localdatetime-컨버전-문제">4) 배포 환경에서 피드 조회 실패 – ES LocalDateTime 컨버전 문제</h3>
<ul>
<li><p><strong>현상</strong>  </p>
<ul>
<li><p>운영 환경에서만 피드 조회 API가 다음 에러와 함께 500을 반환했다.</p>
<pre><code class="language-yaml">ConversionException: Unable to convert value &#39;1760968800000&#39; to java.time.LocalDateTime for property &#39;forecastedAt&#39;</code></pre>
</li>
</ul>
</li>
<li><p><strong>원인</strong></p>
<ul>
<li>Elasticsearch 문서의 <code>forecastedAt</code> 필드 타입은 <code>epoch_millis(Long)</code>이었다.</li>
<li>Spring Data Elasticsearch가 이를 도메인 객체의 LocalDateTime로 역직렬화할 때, 숫자 → LocalDateTime 변환 컨버터가 등록되어 있지 않았다.</li>
<li>DTO에 @JsonFormat으로 문자열 포맷을 강제하면서, 타입 변환 경로가 더 복잡해졌다.</li>
</ul>
</li>
<li><p><strong>해결</strong></p>
<ul>
<li>DTO에서 <code>@JsonFormat</code>을 제거해 문자열 포맷 강제를 해제했다.</li>
<li>Long ↔ LocalDateTime 변환용 Converter Bean을 정의하고,
특정 프로파일에만 적용되던 <code>@Profile</code>을 제거해 모든 프로파일에서 공통으로 등록되도록 했다.</li>
</ul>
</li>
<li><p><strong>배운 점</strong><br>검색 인프라(ES)의 타입과 애플리케이션 도메인 타입(LocalDateTime 등) 사이의 매핑은 <strong>명시적으로 설계</strong>해야 한다.<br>특히 테스트/운영 프로파일에 따라 Bean 구성이 달라지지 않도록 관리하는 것이 중요하다.</p>
</li>
</ul>
<h3 id="5-피드-삭제-후에도-검색-결과에-노출되던-문제">5) 피드 삭제 후에도 검색 결과에 노출되던 문제</h3>
<ul>
<li><p><strong>현상</strong>  </p>
<ul>
<li>피드를 삭제(soft delete)했는데도, ES 기반 피드 검색 결과에 계속 노출되었다.</li>
</ul>
</li>
<li><p><strong>원인</strong>  </p>
<ul>
<li>색인 시 ES 문서 <code>_id</code>를 DB <code>feed_id</code>가 아닌 자동 생성 ID로 사용했다.
→ <code>delete(id)</code> 호출 시 다른 문서를 바라보게 됨.</li>
<li>soft delete를 도입했지만 ES 검색 쿼리에 <code>is_deleted = false</code> 필터를 까먹고 넣지 않았다.</li>
</ul>
</li>
<li><p><strong>해결</strong>  </p>
<ul>
<li>색인 시 ES 문서의 <code>_id</code>를 DB <code>feed_id</code>와 동일한 값으로 고정했다.</li>
<li>soft delete 시 DB뿐 아니라 ES 문서에도 <code>is_deleted = true</code>로 반영했다.</li>
<li>ES 검색 쿼리의 기본 조건에 <code>is_deleted = false</code> 필터를 강제했다.</li>
</ul>
</li>
<li><p><strong>배운 점</strong><br>RDB와 검색 인덱스를 함께 사용할 때는 ID 전략(PK와 _id)과 삭제 전략(soft/hard delete, 조회 필터)을 처음부터 일관성 있게 설계해야 한다.</p>
</li>
</ul>
<h3 id="6-nori-플러그인부트스트랩-타이밍-문제">6) Nori 플러그인/부트스트랩 타이밍 문제</h3>
<ul>
<li><p><strong>현상</strong>  </p>
<ul>
<li>초기 기동 시 한글 분석기 부재로 인덱스 생성/매핑이 실패하면서, 최초 기동 시 에러가 발생했다.</li>
</ul>
</li>
<li><p><strong>원인</strong>  </p>
<ul>
<li>Elasticsearch 컨테이너 기동 이후에 <code>analysis-nori</code> 플러그인을 설치하려 했다.</li>
<li>플러그인 설치 실패 시에도 컨테이너가 계속 떠 있어 문제를 조기에 인지하지 못했다.</li>
</ul>
</li>
<li><p><strong>해결</strong>  </p>
<ul>
<li>컨테이너 빌드 단계에서 Nori 플러그인을 선 설치하도록 Dockerfile/스크립트를 수정했다.</li>
<li>플러그인 설치가 실패하면 빌드 자체를 실패시키도록 해, 문제를 빠르게 감지할 수 있게 했다.</li>
</ul>
</li>
<li><p><strong>배운 점</strong><br>인프라 의존성이 있는 플러그인은 빌드 타임에 강제하는 것이 전체 시스템 안정성에 유리하다.</p>
</li>
</ul>
<h3 id="7-의상-추천-엔진-품질-개선">7) 의상 추천 엔진 품질 개선</h3>
<h3 id="7-1-softmax-샘플링-품질-이슈">7-1) Softmax 샘플링 품질 이슈</h3>
<ul>
<li><p><strong>현상</strong>  </p>
<ul>
<li>점수가 비슷한 후보들이 많거나, 특정 후보만 극단적으로 높을 때 확률이 한 후보로 지나치게 쏠리거나 다양성이 저하되었다.</li>
</ul>
</li>
<li><p><strong>해결</strong>  </p>
<ul>
<li>동점 구간은 균등 샘플링으로 처리해 불필요한 연산을 줄이고 공정성을 확보했다.</li>
<li>극단 분포가 감지될 때 로그를 남기도록 해, 품질 이슈를 조기에 파악할 수 있게 했다.</li>
</ul>
</li>
</ul>
<h3 id="7-2-최근-추천-아이템-반복-노출">7-2) 최근 추천 아이템 반복 노출</h3>
<ul>
<li><p><strong>현상</strong>  </p>
<ul>
<li>같은 사용자가 짧은 시간 안에 추천을 다시 받을 경우, 동일한 조합이 반복 노출됐다.</li>
</ul>
</li>
<li><p><strong>해결</strong>  </p>
<ul>
<li>Redis에 카테고리별 <strong>최근 추천 아이템 목록을 TTL(30분)</strong>로 저장했다.</li>
<li>추천 후보군 생성 시, 해당 목록에 포함된 아이템을 우선 제외해 중복 노출을 줄였다.</li>
</ul>
</li>
</ul>
<h3 id="7-3-추천-임계값-미달-시-빈-리스트-반환">7-3) 추천 임계값 미달 시 빈 리스트 반환</h3>
<ul>
<li><p><strong>현상</strong>  </p>
<ul>
<li>사용자의 옷은 존재하지만, 점수 임계값을 넘는 후보가 하나도 없으면 <strong>빈 리스트를 반환</strong>했다.
→ 사용자 입장에서는 “추천이 없는 서비스”처럼 보이는 UX 문제.</li>
</ul>
</li>
<li><p><strong>해결</strong>  </p>
<ul>
<li>점수 기반 추천 후보가 없을 경우, <strong>랜덤 추천 fallback</strong>을 추가했다.</li>
<li>각 부위별로 한 벌씩을 무작위로 선택해, 최소한의 추천 조합은 보장되도록 했다.</li>
</ul>
</li>
</ul>
<h3 id="7-4-사용자-의상이-등록되어-있지-않은-경우-404-반환">7-4) 사용자 의상이 등록되어 있지 않은 경우 404 반환</h3>
<ul>
<li><p><strong>현상</strong>  </p>
<ul>
<li>사용자가 옷을 한 벌도 등록하지 않은 상태에서 추천 API를 호출하면 404를 반환했다.</li>
</ul>
</li>
<li><p><strong>해결</strong></p>
<ul>
<li>HTTP 200 + 빈 리스트를 반환하도록 변경해, 클라이언트에서 더 자연스럽게 처리할 수 있도록 했다.</li>
</ul>
</li>
</ul>
<h3 id="8-oauth2--google-로그인-클라이언트-유형-설정-오류">8) OAuth2 – Google 로그인 클라이언트 유형 설정 오류</h3>
<ul>
<li><p><strong>현상</strong>  </p>
<ul>
<li>Google OAuth2 로그인 시도 시 인증 실패가 발생했다.</li>
</ul>
</li>
<li><p><strong>원인</strong>  </p>
<ul>
<li>Google Cloud Console에서 OAuth 클라이언트를 <strong>Desktop App 유형</strong>으로 생성했다.</li>
<li>Desktop App은 redirect URI를 등록할 수 없어, Spring Security OAuth2 클라이언트 설정과 맞지 않았다.</li>
</ul>
</li>
<li><p><strong>해결</strong>  </p>
<ul>
<li>클라이언트 유형을 <strong>Web application</strong>으로 새로 생성했다.</li>
<li>로컬/운영 환경별로 승인된 redirect URI를 등록해, 환경에 따라 올바른 콜백 URL로 인증이 이뤄지도록 했다.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="📌-개선-계획">📌 개선 계획</h2>
<ul>
<li>로컬 및 운영 환경에서 분산 환경 적용</li>
<li>전체적인 UI 개선</li>
<li>추가 API 개발<ul>
<li>회원 탈퇴</li>
<li>인기 피드 랭킹</li>
<li>어드민 전체 공지</li>
</ul>
</li>
<li>꼭 필요한 배포환경만 남기고 비용 최소화</li>
<li>피드 목록 조회: <strong>Elasticsearch vs RDB</strong> 의사결정</li>
</ul>
<p>👉 프로젝트 종료 후 피드 목록 조회 RDB 기반 변경, 피드 물리 삭제, 논리 삭제 복구 기능, DM 대화방 생성 등의 리팩토링을 완료했다!🤩</p>
<hr>
<h2 id="💡-프로젝트를-통해-배운-점">💡 프로젝트를 통해 배운 점</h2>
<h3 id="✅-기술적-성과">✅ 기술적 성과</h3>
<ul>
<li><p><strong>검색/탐색 품질 고도화</strong></p>
<ul>
<li>Elasticsearch를 활용하여 피드 검색 기능을 개선했다.</li>
</ul>
</li>
<li><p><strong>추천 품질 향상</strong></p>
<ul>
<li>의상 추천 결과에 대해 LLM 기반 <strong>추천 이유 생성</strong>을 도입하고, 프롬프트를 표준화했다.</li>
<li>사용자가 “왜 이 조합을 추천받았는지” 이해할 수 있는 설명을 제공하면서, <strong>자연스러운 표현과 일관된 톤</strong>을 유지했다.</li>
</ul>
</li>
<li><p><strong>인증 신뢰성 확보 (OAuth2)</strong></p>
<ul>
<li>OAuth2 클라이언트 타입과 리디렉션 URI를 환경별로 정합성 있게 맞추어 설정했다.</li>
<li>잘못된 설정으로 인한 <strong>로그인 실패 케이스를 제거</strong>하고 인증 플로우를 안정화했다.</li>
</ul>
</li>
<li><p><strong>UI 일관성</strong></p>
<ul>
<li>색상·일러스트 재정비로 화면 간 <strong>시각적 일관성</strong>을 강화했다.</li>
</ul>
</li>
<li><p><strong>테스트 품질 향상</strong></p>
<ul>
<li>단위/통합 테스트를 꾸준히 추가해 <strong>테스트 커버리지 80% 이상</strong>을 달성했다.</li>
<li>리팩토링이나 신규 기능 추가 시에도 테스트를 기반으로 <strong>안정적으로 코드를 수정</strong>할 수 있었다.</li>
</ul>
</li>
<li><p><strong>프론트엔드까지 포함한 문제 해결 경험</strong></p>
<ul>
<li>백엔드 API 수정에 그치지 않고, 알림 목록 무한스크롤, DM 탭분리 등 프론트엔드 코드도 함께 수정했다.</li>
<li>화면/UI 문제를 “프론트 영역”으로만 넘기지 않고, <strong>엔드투엔드 관점에서 이슈를 추적하고 해결하는 풀스택 시야</strong>를 갖추게 되었다.</li>
</ul>
</li>
</ul>
<h3 id="🤝-비기술적-성과">🤝 비기술적 성과</h3>
<ul>
<li><p><strong>일정 가시화 &amp; 리스크 관리</strong></p>
<ul>
<li>스프린트 보드와 캘린더로 작업 현황을 가시화하고, 매일 팀 내에서 진행 상황을 공유했다.</li>
<li>팀원의 일정 변경이나 지연 가능성이 보이면 즉시 공유하고, <strong>데드라인 재협의·작업 재배정</strong>으로 리스크를 초기에 줄였다.</li>
</ul>
</li>
<li><p><strong>커뮤니케이션 &amp; 협업 방식</strong></p>
<ul>
<li>요구사항 변경, 버그, 설계 고민 등을 혼자 오래 붙잡지 않고, <strong>초기에 팀원과 공유해 함께 논의</strong>하는 습관을 들였다.</li>
<li>코드 리뷰를 통해 서로의 코드 스타일과 설계 의도를 맞춰 가며, <strong>피드백을 주고받는 문화를 유지</strong>했다.</li>
</ul>
</li>
</ul>
<h3 id="📌-교훈">📌 교훈</h3>
<ul>
<li><p><strong>새로운 기술은 필요할 때 도입하자</strong>
피드 검색 기능에 Elasticsearch를 도입해 보면서, 현재 사용량 기준에서는 과설계에 가까울 수 있다는 점을 체감했다.<br>새로운 기술을 도입할 때는 “왜 필요한지”, “우리 규모에 맞는지”를 먼저 검토하고 도입 범위를 정해야 한다는 교훈을 얻었다.</p>
</li>
<li><p><strong>LLM은 만능이 아니라, 적절하게 써야 한다</strong> 
추천 로직 전체를 LLM으로 처리했을 때는 평균 응답 시간이 4초 이상으로 느려져 실서비스에는 적합하지 않았다.<br>결국 <strong>추천 산출은 자체 알고리즘</strong>, LLM은 <strong>추천 이유 생성</strong>으로 역할을 분리해 성능과 사용자 경험을 모두 만족시킬 수 있었다.<br>이 경험을 통해, 무작정 LLM을 도입하기보다 <strong>성능·비용·사용 시점</strong>을 함께 고려해 역할을 설계하는 것이 중요하다는 것을 배웠다.</p>
</li>
<li><p><strong>일정은 “가시화 + 공유”로 관리해야 한다</strong><br>스프린트 보드와 캘린더로 일정을 가시화하고, 팀 내에서 진척 상황을 꾸준히 공유하자 지연이 발생했을 때도 작업 재분배를 훨씬 빠르고 효율적으로 할 수 있었다.<br>팀원의 일정 변경이나 지연 가능성이 보이면 즉시 공유하고, <strong>데드라인을 재협의하거나 작업을 재배정</strong>하는 방식이 프로젝트 리스크를 줄이는 데 큰 도움이 된다는 점을 배웠다.</p>
</li>
<li><p><strong>백엔드에만 머무르지 않고 프론트까지 챙길 때 완성도가 올라간다</strong><br>백엔드 기능 개발뿐만 아니라, 프론트엔드(UI·화면 흐름)까지 함께 다루면서 버그 대응 속도와 전체 서비스 완성도가 확실히 올라간다는 걸 느꼈다.<br>한쪽 영역에만 머무르기보다, 필요할 때는 프론트도 직접 보고 고칠 수 있는 역량이 협업과 운영에 큰 힘이 된다는 점을 깨달았다.</p>
</li>
<li><p><strong>“내 도메인”을 넘어서 전체 코드베이스를 이해하려는 태도가 필요하다</strong><br>다른 팀원의 도메인과 코드를 평소에도 함께 파악해 두어야, 작업 재배정이 필요할 때 맥락을 빠르게 이해하고 투입될 수 있음을 느꼈다.
협업 환경에서는 개인 담당 영역만 파는 것에서 끝나는 것이 아니라, <strong>서비스 전체 구조와 주요 흐름을 함께 이해하려는 자세</strong>가 중요하다는 걸 느꼈다.</p>
</li>
</ul>
<hr>
<h2 id="✍️-마무리">✍️ 마무리</h2>
<p><img src="https://velog.velcdn.com/images/jh_devlog/post/8d25d974-2d97-43d1-b52a-11ed49ddd6ca/image.jpeg" alt=""></p>
<p>이번 프로젝트에서는 Spring Security, Redis, Kafka, OAuth2, WebSocket, Elasticsearch 등 새로운 기술을 많이 도입해 볼 수 있었다.</p>
<p>무엇보다도 이번에도 너무 좋은 팀원들을 만나 재밌게 프로젝트를 진행할 수 있었다.🙌
각자 맡은 영역에서 열심히 하는 모습을 보며 나도 많이 자극을 받았고,<br>의사소통 방식이나 협업 태도에서도 배울 점이 정말 많았다.</p>
<p>이번 프로젝트를 시작하면서 “팀원들의 코드를 꼼꼼히 리뷰해 보자”는 나만의 목표를 세웠는데,<br>꽤 성실하게 지켜낸 것 같아 개인적으로는 만족스럽다.<br>(적어도 내 역량 내에서는 최선을 다했다고 자부한다..! 👀)</p>
<p>물론 아쉬운 부분들도 꽤나 있었다.
우선 PM 역할을 처음 맡아보면서 일정을 더 적극적으로 관리하지 못한 점이 아쉽다.<br>초반부터 팀원들의 일정까지 함께 고려해 전체 스케줄을 설계했어야 하는데,<br>“내 일만 제때 끝나면 되겠지” 하는 안일한 마음으로 시작했던 것 같다.😵‍💫</p>
<p>일정이 조금씩 밀리기 시작했을 때도,<br>“1~2일 정도 밀리는 건 괜찮지 않을까?”라는 생각에 일정 조정을 대부분 팀원들에게 맡겨 두었다. 
그때 강사님께서 <strong>데드라인을 팀과 함께 분명히 정하고, 지키기 어려운 경우에는 작업을 재분배하는 방식</strong>을 추천해주셨고, 이를 반영하면서 일정을 조금 더 타이트하게 관리할 수 있었다.<br>결과적으로는 작업을 재분배한 덕분에 일정 안에 마무리할 수 있었고, PM의 중요성을 다시 한 번 느끼게 되었다.<br>앞으로는 의견을 피력하거나 일정 관련해서 조율·재촉해야 할 때, 좀 더 분명하게 어필하고 적극적인 자세로 임해야겠다고 느꼈다.</p>
<p>의사소통 측면에서도 아쉬움이 남는다.<br>회의 시간에 수동적으로 따르는 태도를 보였던 순간들이 많았는데,<br>다음에는 회의 전에 미리 생각을 정리해 두고, 내 의견을 명확히 정리해 조금 더 주도적으로 참여해 보고 싶다.</p>
<p>개인적으로 이번 프로젝트에서 가장 좋았던 점은,
“프로젝트 끝났으니까 여기까지!”가 아니라 <strong>이후 리팩토링까지 함께 이어갔다는 것</strong>이다.<br>기간이 끝난 뒤에도 팀원들끼리 코어 타임을 정해서 리팩토링과 기능 보완을 이어갔고,
실제로 서비스를 계속 다듬어 가는 경험을 할 수 있었다.</p>
<p>이번 프로젝트는 나에게 꽤 의미 있는 프로젝트로 남을 것 같다.👍</p>
<p><del>실은 정말 회고 쓰기 귀찮았는데</del> 막상 쓰고 나니 정리도 되고, 추억도 새록새록 떠올라서 뿌듯하다. 이제 진짜 취준하러 가야지...💨</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SB 3기] 코드잇 중급 프로젝트 회고 : Monew]]></title>
            <link>https://velog.io/@jh_devlog/SB-3%EA%B8%B0-%EC%BD%94%EB%93%9C%EC%9E%87-%EC%A4%91%EA%B8%89-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0-Monew</link>
            <guid>https://velog.io/@jh_devlog/SB-3%EA%B8%B0-%EC%BD%94%EB%93%9C%EC%9E%87-%EC%A4%91%EA%B8%89-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0-Monew</guid>
            <pubDate>Tue, 09 Sep 2025 03:42:49 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/jh_devlog/post/96417467-fd46-4311-8948-e06d3d0d0c44/image.png" alt=""></p>
<h2 id="코드잇-중급-프로젝트-회고--monew">코드잇 중급 프로젝트 회고 : Monew</h2>
<p>고급 프로젝트을 앞두고, 그 전 진행했던 코드잇 중급 프로젝트를 정리해보려 한다..! 👀
초급 프로젝트에서는 기본기를 다지고 협업을 경험해보았다면, 이번에는 새로운 기술들을 도입하고 조금 더 깊은 개발 경험을 쌓을 수 있었다. 
배우는 게 정말 많았고, 재밌게 몰입할 수 있었던 프로젝트였다!👍</p>
<h2 id="🗂️-프로젝트-개요">🗂️ 프로젝트 개요</h2>
<blockquote>
<p><strong>모뉴(MoNew)</strong></p>
<p>모뉴는 다양한 뉴스 출처를 통합하여 관심사 기반으로 뉴스를 저장하는 뉴스 통합 관리 플랫폼입니다.
관심 있는 주제의 기사가 등록되면 실시간 알림을 받고, 댓글과 좋아요를 통해 다른 사용자와 의견을 나눌 수 있는 소셜 기능도 함께 제공됩니다.</p>
</blockquote>
<h3 id="✅-주요-기능">✅ 주요 기능</h3>
<ul>
<li>사용자 관리 (등록/조회/수정/삭제/로그인)</li>
<li>관심사 키워드 관리 (등록/조회/수정/삭제/목록/구독)</li>
<li>뉴스 기사 관리 (수집/조회/삭제/목록/백업 및 복구)</li>
<li>댓글 관리 (등록/조회/수정/삭제/목록/좋아요)</li>
<li>활동 내역 관리 (조회)</li>
<li>알림 관리 (등록/조회/수정/삭제/목록)</li>
</ul>
<h3 id="📌-기술-요구-사항">📌 기술 요구 사항</h3>
<ul>
<li>유효성 검사</li>
<li>커스텀 예외 처리</li>
<li>로그 관리</li>
<li>테스트 주도 개발 (TDD)</li>
<li>CI/CD 파이프라인 구축</li>
</ul>
<hr>
<h3 id="👩💻-나의-역할">👩‍💻 나의 역할</h3>
<h4 id="1️⃣-기능-개발">1️⃣ 기능 개발</h4>
<ul>
<li><strong>댓글 도메인</strong><ul>
<li>댓글 등록, 수정, 논리 삭제, 물리 삭제 기능</li>
<li>정렬 및 커서 기반 페이지네이션 목록 조회 기능</li>
<li>좋아요/좋아요 취소 기능</li>
</ul>
</li>
<li><strong>알림 도메인</strong><ul>
<li>좋아요 알림 등록 기능</li>
<li>전체 알림 확인/개별 알림 확인 기능</li>
</ul>
</li>
</ul>
<h4 id="2️⃣-개발-외-역할">2️⃣ 개발 외 역할</h4>
<ul>
<li><strong>Jira ↔ Git 연동</strong><ul>
<li>Jira → Git : Jira 백로그 이슈를 GitHub Issue로 자동 등록 (수동 트리거버튼 활용)</li>
<li>Git → Jira : GitHub 이슈 close 시 Jira 이슈 자동 완료 처리</li>
</ul>
</li>
<li><strong>CI 파이프라인 구축 (GitHub Actions)</strong><ul>
<li>PR 생성 시 자동 테스트 및 커버리지 리포트 실행</li>
<li>PR 댓글에 테스트 커버리지 요약 자동 추가</li>
</ul>
</li>
<li><strong>CD 파이프라인 구축 (GitHub Actions)</strong><ul>
<li>Docker 이미지 ECR 빌드 -&gt; AWS ECR 푸시</li>
<li>ECS 서비스 자동 배포 구현</li>
</ul>
</li>
</ul>
<h3 id="🔗-깃허브-링크">🔗 깃허브 링크</h3>
<ul>
<li><a href="https://github.com/sb3-monew-team1/sb03-monew-team1">Monew GitHub Repository</a></li>
</ul>
<hr>
<h2 id="📚-개발-환경-및-사용-스택">📚 개발 환경 및 사용 스택</h2>
<pre><code>⚙️ Backend Stack
📦 Framework
├── Spring Boot 3.x          # 메인 애플리케이션 프레임워크
├── Spring Data JPA          # ORM 및 데이터 접근
├── Spring Batch             # 대용량 배치 처리
└── Gradle                   # 빌드 및 의존성 관리 도구


🗄️ Database
├── H2 Database             # 개발/테스트용 In-memory DB
├── PostgreSQL              # 운영 환경 RDBMS
└── MongoDB                 # 문서 지향 NoSQL DB

📚 Documentation
├── Swagger/OpenAPI 3.0     # API 문서 자동화
└── Notion                  # 프로젝트 문서 및 협업 기록
└── Jira                    # 일정 및 이슈 관리

🔧 Development Tools
├── IntelliJ IDEA           # 통합 개발 환경(IDE)
├── Git &amp; GitHub            # 버전 관리 및 협업
├── Discord                 # 팀 커뮤니케이션
└── Postman                 # API 테스트 도구

🚀 Deployment &amp; Monitoring
├── AWS                     # 클라우드 인프라
├── Docker                  # 컨테이너 기반 배포
├── Grafana                 # 모니터링 시각화
└── Prometheus              # 메트릭 수집 및 모니터링</code></pre><hr>
<h2 id="🛠️-트러블슈팅-사례">🛠️ 트러블슈팅 사례</h2>
<h3 id="1-댓글-목록-조회---n1-쿼리-문제">1) 댓글 목록 조회 - <strong>N+1 쿼리 문제</strong></h3>
<ul>
<li><strong>문제</strong>: 댓글 조회 시 각 댓글마다 좋아요 여부를 확인하면서 N+1 문제가 발생</li>
<li><strong>해결</strong>: 댓글 ID를 한 번에 수집 → 좋아요 여부를 한 번의 쿼리로 조회 → 매핑 처리</li>
<li><strong>결과</strong>: 쿼리 수 <code>N+1</code> → <code>2</code>로 최적화</li>
</ul>
<h3 id="2-댓글-논리-삭제---where-필터링의-한계">2) 댓글 논리 삭제 - <code>@Where</code> 필터링의 한계</h3>
<ul>
<li><strong>문제</strong>: 엔티티 단에서 <code>@Where</code>로 <code>is_deleted=false</code>를 필터링 → 모든 조회에 일괄 적용되어 유연성 부족</li>
<li><strong>해결</strong>: <code>@Where</code> 제거 후 QueryDSL, JPA 메서드 기반으로 조건을 명시적으로 관리</li>
<li><strong>결과</strong>: 조회 조건을 상황에 맞게 제어 가능, Hibernate 의존도 감소</li>
</ul>
<h3 id="3-데이터-시더-실행---낙관적-락-충돌">3) 데이터 시더 실행 - 낙관적 락 충돌</h3>
<ul>
<li><strong>문제</strong>: 동일한 엔티티(<code>Article</code>)를 반복 수정하면서 <code>StaleObjectStateException</code> 발생</li>
<li><strong>해결</strong>: Seeder 클래스 간 실행 순서를 명시적으로 보장하여 의존 관계에 맞게 순차 실행</li>
<li><strong>결과</strong>: 충돌 제거 및 안정적인 초기 데이터 세팅 확보</li>
</ul>
<h3 id="4-cd-파이프라인---task-definitionjson-인식-불가">4) CD 파이프라인 - <code>task-definition.json</code> 인식 불가</h3>
<ul>
<li><strong>문제</strong>: GitHub Actions의 Job 간 독립 실행 환경 때문에 <code>deploy</code> Job에서 파일을 찾지 못함</li>
<li><strong>해결</strong>: <code>deploy</code> Job에도 <code>checkout</code> 단계 추가</li>
<li><strong>결과</strong>: CD 파이프라인 정상 동작 및 자동 배포 성공</li>
</ul>
<h3 id="5-jpql---current_timestamp-타입-불일치">5) JPQL - <code>CURRENT_TIMESTAMP</code> 타입 불일치</h3>
<ul>
<li><strong>문제</strong>: <code>Instant</code> 필드에 <code>CURRENT_TIMESTAMP</code>를 매핑하면서 H2에서 타입 오류 발생</li>
<li><strong>해결</strong>: Native Query로 수정하여 DB 환경에 관계없이 일관 처리</li>
<li><strong>결과</strong>: 테스트/운영 환경 모두에서 정상 동작</li>
</ul>
<hr>
<h2 id="💡-프로젝트를-통해-배운-점">💡 프로젝트를 통해 배운 점</h2>
<h3 id="✅-기술적-성과">✅ 기술적 성과</h3>
<ul>
<li><code>Spring Boot</code>, <code>JPA</code>, <code>QueryDSL</code>,<code>Spring Batch</code>, <code>PostgreSQL</code>, <code>Docker</code>, <code>AWS</code> 등 다양한 기술 스택 활용</li>
<li>QueryDSL 기반 커서 페이지네이션 도입</li>
<li>GitHub Actions 기반 CI/CD 구축 -&gt; 빌드/배포 과정을 자동화하여 배포 시간 단축 및 안정적 배포 환경 확보</li>
<li>로드밸런서(ALB) + 롤링 업데이트 기반 <strong>무중단 배포</strong> 경험</li>
<li>댓글 서비스 테스트 커버리지 달성<ul>
<li>서비스 계층: 라인 98% / 브랜치 90%</li>
<li>레포지토리 계층: 라인 100% / 브랜치 92%</li>
</ul>
</li>
<li>이벤트 리스너 기반 알림 처리 구현</li>
</ul>
<h3 id="🤝-비기술적-성과">🤝 비기술적 성과</h3>
<ul>
<li>Jira를 통한 스프린트/이슈 관리 → 팀 내 작업 가시성 확보</li>
<li>Git ↔ Jira 연동으로 워크플로우 자동화</li>
<li>체크리스트 기반 PR 템플릿 + 피드백 반영 → 리뷰 효율성과 코드 품질 향상</li>
</ul>
<hr>
<h2 id="📌-개선-계획">📌 개선 계획</h2>
<ul>
<li>댓글 물리 삭제 시 관리자 권한 검증 추가 (Spring Security)</li>
<li>기사별 댓글 정렬 옵션 추가 (최신순 / 좋아요순) -&gt; UX 개선</li>
</ul>
<hr>
<h2 id="✍️-마무리">✍️ 마무리</h2>
<p>이번 프로젝트는 여러모로 새롭게 경험해보는 게 많아서 재미있었다.😊</p>
<p>특히 테스트 주도 개발(TDD), CI/CD 자동화, 무중단 배포 같은 실무 기술들을 직접 경험해볼 수 있었는데, 이론으로만 접했을 때는 잘 와닿지 않던 개념들이 실제로 구현해보니 훨씬 이해가 잘됐다.💡 (TDD는 정말 쉽지 않은 녀석이었다...🥹)</p>
<p>새로운 툴도 다양하게 활용해봤다. 멘토님 추천으로 처음 도입한 <strong>Jira는</strong> 작업 흐름을 한눈에 파악할 수 있어서 협업에 유용했고, AI 코드 리뷰 툴인 <strong>CodeRabbit은</strong> PR 요약, 수정 제안, 다이어그램 생성까지 자동으로 해줘서 코드 리뷰가 한결 편해졌다. 덕분에 코드 품질도 더 좋아졌다. (코드래빗 is God🧎‍♀️)</p>
<p>무엇보다도 기억에 남는 건 <strong>팀원들과의 협업</strong>이었다.🤝 
이슈가 생겼을 때 다같이 고민하고 해결해 나가는 과정이 인상 깊었고, 새벽이나 주말에도 적극적으로 참여하는 팀원들을 보며 자극도 많이 받았다.🔥 
서로 새로운 기술이나 지식을 적극적으로 공유하는 분위기 속에서 배우는 것도 많았고, 팀 분위기가 좋아서 프로젝트 기간 내내 즐겁게 개발할 수 있었다. 🙌 (1조 할머니 보쌈팀 최고...👍)</p>
<p>아쉬운 점이 있다면, 팀원들의 모든 코드 리뷰를 꼼꼼히 해보며 코드들을 전부 파악하고 싶었는데 그러지는 못한 것 같아 조금 아쉽다.</p>
<p>이제 고급 프로젝트를 앞두고 있는데, 중급 프로젝트의 경험을 바탕으로 더욱 수월하게 진행할 수 있을 것 같아 기대가 된다. 정말 많이 배우고 성장할 수 있었던 프로젝트였다!☺️</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SB 3기] 코드잇 스프린트 위클리페이퍼 17주차]]></title>
            <link>https://velog.io/@jh_devlog/SB-3%EA%B8%B0-%EC%BD%94%EB%93%9C%EC%9E%87-%EC%8A%A4%ED%94%84%EB%A6%B0%ED%8A%B8-%EC%9C%84%ED%81%B4%EB%A6%AC%ED%8E%98%EC%9D%B4%ED%8D%BC-17%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@jh_devlog/SB-3%EA%B8%B0-%EC%BD%94%EB%93%9C%EC%9E%87-%EC%8A%A4%ED%94%84%EB%A6%B0%ED%8A%B8-%EC%9C%84%ED%81%B4%EB%A6%AC%ED%8E%98%EC%9D%B4%ED%8D%BC-17%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Sun, 31 Aug 2025 13:11:09 GMT</pubDate>
            <description><![CDATA[<h3 id="q1-tcpip-4계층-모델과-osi-7계층-모델에-대해-각각-설명하고-두-모델을-비교해보세요">Q1. TCP/IP 4계층 모델과 OSI 7계층 모델에 대해 각각 설명하고, 두 모델을 비교해보세요.</h3>
<p><img src="https://velog.velcdn.com/images/jh_devlog/post/ff8f0fba-acee-4c0b-8957-27056dc989be/image.png" alt="">
<sub>이미지 출처 : cheapsslsecurity</sub></p>
<hr>
<h3 id="✅-tcpip-4계층-모델">✅ TCP/IP 4계층 모델</h3>
<ul>
<li>인터넷 표준인 <strong>TCP/IP 프로토콜</strong>을 계층화한 실용적인 네트워크 모델</li>
<li>현재 실제 인터넷 통신에서 사용되는 구조</li>
</ul>
<h4 id="1-네트워크-연결-계층-network-access-layer">1. 네트워크 연결 계층 (Network Access Layer)</h4>
<ul>
<li>물리적인 데이터 전송 담당 (LAN, WAN, Ethernet, Wi-Fi 등)</li>
<li><strong>프레임 단위</strong> 통신</li>
<li>예: Ethernet, Wi-Fi, ARP</li>
</ul>
<h4 id="2-인터넷-계층-internet-layer">2. 인터넷 계층 (Internet Layer)</h4>
<ul>
<li>출발지 -&gt; 목적지까지 I<strong>P 주소 기반 패킷 전달(라우팅)</strong></li>
<li><strong>패킷 단위</strong> 통신</li>
<li>예: IP, ICMP, ARP, RARP</li>
</ul>
<h4 id="3-전송-계층-transport-layer">3. 전송 계층 (Transport Layer)</h4>
<ul>
<li>송수신 프로세스 간 데이터 전송 제어</li>
<li><strong>TCP(연결 지향, 신뢰성 보장) / UDP(비연결, 빠른 전송)</strong></li>
<li><strong>세그먼트 단위</strong> 통신</li>
<li>예: TCP, UDP</li>
</ul>
<h4 id="4-애플리케이션-계층-application-layer">4. 애플리케이션 계층 (Application Layer)</h4>
<ul>
<li>사용자가 직접 접하는 서비스 제공</li>
<li>예: HTTP, FTP, SMTP, DNS</li>
</ul>
<hr>
<h3 id="✅-osi-7계층-모델">✅ OSI 7계층 모델</h3>
<ul>
<li>국제 표준화 기구(ISO)에서 정의한 네트워크 표준 참조 모델</li>
<li><strong>네트워크 통신 과정을 7단계로 구분</strong></li>
</ul>
<h4 id="1-물리-계층-physical-layer">1. 물리 계층 (Physical Layer)</h4>
<ul>
<li>역할 : 네트워크의 가장 하위 계층으로, 데이터를 전기 신호(0과 1) 형태로 주고받음</li>
<li>목적 : 전송 매체를 통해 신호를 <strong>그대로 잘 전달하는</strong> 것</li>
<li>단위: 비트(Bit)</li>
<li>장비 예시 : 허브, 리피터, 케이블, 커넥터</li>
</ul>
<h4 id="2-데이터-링크-계층-data-link-layer">2. 데이터 링크 계층 (Data Link Layer)</h4>
<ul>
<li>역할 : 물리 계층에서 받은 신호를 <strong>프레임(Frame)</strong> 단위로 묶어 처리</li>
<li>목적 : 오류 검출 및 수정, 출발지/목적지 <strong>MAC 주소 확인</strong></li>
<li>특징 : 스위치는 MAC 주소 기반으로 올바른 포트를 선택해 전달</li>
<li>단위 : 프레임(Frame)</li>
<li>장비 예시 : 스위치, 네트워크 인터페이스 카드(NIC)</li>
</ul>
<h4 id="3-네트워크-계층-network-layer">3. 네트워크 계층 (Network Layer)</h4>
<ul>
<li>역할 : 목적지까지의 경로를 설정하고 데이터 전달</li>
<li>특징 : IP 주소(논리 주소)를 사용하여 라우팅 수행</li>
<li>단위 : 패킷(Packet)</li>
<li>장비 예시 : 라우터</li>
</ul>
<h4 id="4-전송-계층-transport-layer">4. 전송 계층 (Transport Layer)</h4>
<ul>
<li>역할 : 애플리케이션 간 데이터가 <strong>정상적으로 전달되도록 보장</strong></li>
<li>기능 : 데이터 분할, 순서 제어, 오류 복구<ul>
<li>TCP : 신뢰성 있는 연결 (세그먼트 단위) </li>
<li>UDP : 빠른 전송, 비연결성 (데이터그램 단위)</li>
</ul>
</li>
<li>단위 : 세그먼트(TCP), 데이터그램(UDP)</li>
<li>장비 예시 : 방화벽, 로드 밸런서</li>
</ul>
<h4 id="5-세션-계층-session-layer">5. 세션 계층 (Session Layer)</h4>
<ul>
<li>역할 : 두 애플리케이션 간의 <strong>연결(세션) 생성, 유지, 종료 관리</strong></li>
<li>기능 : 통신 중단 시 에러 복구 및 재전송</li>
</ul>
<h4 id="6-표현-계층-presentation-layer">6. 표현 계층 (Presentation Layer)</h4>
<ul>
<li>역할 : 데이터의 표현 형식 통일</li>
<li>기능 : 암호화, 압축, 인코딩/디코딩</li>
<li>예시: JPEG, GIF, MIME 인코딩</li>
</ul>
<h4 id="7-응용-계층-application-layer">7. 응용 계층 (Application Layer)</h4>
<ul>
<li>역할 : 사용자가 직접 접근하는 네트워크 서비스 제공</li>
<li>특징 : UI와 가장 밀접한 계층, 다양한 애플리케이션 프로토콜 포함</li>
<li>대표 프로토콜 : TTP, FTP, SMTP, TELNET</li>
</ul>
<h4 id="📌-계층-구분">📌 계층 구분</h4>
<ul>
<li>데이터 플로 계층 (1~4, 하위 계층) : 데이터 전송에 초점</li>
<li>애플리케이션 계층 (5~7, 상위 계층) : 데이터 해석과 표현에 초점</li>
</ul>
<h3 id="🆚-tcpip-4계층과-osi-7계층-비교">🆚 TCP/IP 4계층과 OSI 7계층 비교</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>TCP/IP 4계층</th>
<th>OSI 7계층</th>
</tr>
</thead>
<tbody><tr>
<td><strong>계층 수</strong></td>
<td>4계층</td>
<td>7계층</td>
</tr>
<tr>
<td><strong>기준</strong></td>
<td>실제 인터넷 프로토콜 설계 기반</td>
<td>이론적 표준 모델</td>
</tr>
<tr>
<td><strong>구조</strong></td>
<td>단순하고 실용적</td>
<td>세분화·개념적</td>
</tr>
<tr>
<td><strong>매핑 관계</strong></td>
<td>애플리케이션 계층 ↔ 응용·표현·세션 계층<br>전송 계층 ↔ 전송 계층<br>인터넷 계층 ↔ 네트워크 계층<br>네트워크 연결 계층 ↔ 데이터링크·물리 계층</td>
<td>계층별로 독립적으로 구분</td>
</tr>
<tr>
<td><strong>활용</strong></td>
<td>실제 인터넷 통신에 사용</td>
<td>네트워크 개념 학습·분석용</td>
</tr>
</tbody></table>
<hr>
<h3 id="q2-전송-계층에서-tcp와-udp의-차이점은-무엇이며-각각-어떤-상황에서-사용하는-것이-적절한가요">Q2. 전송 계층에서 TCP와 UDP의 차이점은 무엇이며, 각각 어떤 상황에서 사용하는 것이 적절한가요?</h3>
<p><img src="https://velog.velcdn.com/images/jh_devlog/post/7e86fd48-9d4c-4a09-a21a-2a40b7b5db06/image.png" alt="">
<sub>이미지 출처 : cheapsslsecurity</sub></p>
<hr>
<h3 id="✅-tcp와-udp란">✅ TCP와 UDP란?</h3>
<h4 id="tcp-transmission-control-protocol">TCP (Transmission Control Protocol)</h4>
<ul>
<li><strong>연결 지향형 프로토콜</strong> -&gt; 송·수신 측이 먼저 연결(3-way handshake)을 맺은 후 데이터 전송</li>
<li>데이터가 순서대로 정확하게 도착하도록 <strong>흐름 제어, 오류 제어, 재전송</strong> 제공</li>
<li>신뢰성이 높지만 속도가 상대적으로 느리고, 헤더 크기가 크다.</li>
<li>주로 <strong>유니캐스트(Unicast, 1:1 통신)</strong> 기반으로 동작하며, <strong>전이중(Full Duplex)</strong> 통신 지원</li>
</ul>
<h4 id="udp-user-datagram-protocol">UDP (User Datagram Protocol)</h4>
<ul>
<li><strong>비연결형 프로토콜</strong> -&gt; 별도의 연결 과정 없이 빠르게 데이터 전송</li>
<li><strong>데이터그램 단위로 처리</strong>, 순서 보장·오류 복구 없음 </li>
<li>오버헤드가 적어 속도는 빠르지만, 신뢰성은 낮다.</li>
<li><strong>유니캐스트(1:1), 멀티캐스트(1:N), 브로드캐스트(1:모두)</strong> 모두 지원</li>
<li>기본적으로 단방향이지만, 응용에 따라 양방향(반이중/전이중)도 가능  </li>
</ul>
<hr>
<h3 id="🆚-차이점">🆚 차이점</h3>
<table>
<thead>
<tr>
<th>구분</th>
<th>TCP (Transmission Control Protocol)</th>
<th>UDP (User Datagram Protocol)</th>
</tr>
</thead>
<tbody><tr>
<td><strong>연결 방식</strong></td>
<td>연결 지향형 (3-way handshake)</td>
<td>비연결형</td>
</tr>
<tr>
<td><strong>신뢰성</strong></td>
<td>전송 보장 (순서·오류 제어)</td>
<td>보장하지 않음 (손실 가능)</td>
</tr>
<tr>
<td><strong>속도</strong></td>
<td>상대적으로 느림</td>
<td>빠름</td>
</tr>
<tr>
<td><strong>단위</strong></td>
<td>세그먼트 (Segment)</td>
<td>데이터그램 (Datagram)</td>
</tr>
<tr>
<td><strong>오버헤드</strong></td>
<td>큼 (헤더 20바이트 이상)</td>
<td>작음 (헤더 8바이트)</td>
</tr>
<tr>
<td><strong>통신 형태</strong></td>
<td>유니캐스트</td>
<td>유니캐스트, 멀티캐스트, 브로드캐스트</td>
</tr>
<tr>
<td><strong>전송 모드</strong></td>
<td>전이중(양방향 동시 통신)</td>
<td>단방향 위주 (응용에 따라 양방향도 가능)</td>
</tr>
</tbody></table>
<hr>
<h3 id="👍-적절한-사용-시점">👍 적절한 사용 시점</h3>
<ul>
<li><p>TCP: 신뢰성과 정확성이 중요한 경우</p>
<ul>
<li>웹 브라우징(HTTP/HTTPS)</li>
<li>이메일(SMTP/IMAP)</li>
<li>파일 전송(FTP)</li>
</ul>
</li>
<li><p>UDP: 지연이 적고 빠른 전송이 중요한 경우</p>
<ul>
<li>실시간 화상 통화 </li>
<li>온라인 게임 </li>
<li>실시간 스트리밍 서비스</li>
<li>DNS 요청</li>
</ul>
</li>
</ul>
<hr>
<h3 id="📄-참고-문서">📄 참고 문서</h3>
<ul>
<li><a href="https://westahn.com/osi-4-%EA%B3%84%EC%B8%B5%EC%9D%B4%EB%9E%80/">OSI 4 계층이란?</a></li>
<li><a href="https://cheapsslsecurity.com/blog/what-is-the-tcp-model-an-exploration-of-tcp-ip-layers/?utm_source=chatgpt.com">What Is the TCP Model? An Exploration of TCP/IP Layers</a></li>
<li><a href="https://www.wallarm.com/what/transmission-control-protocol-tcp">Transmission control protocol - TCP</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SB 3기] 코드잇 스프린트 위클리페이퍼 16주차]]></title>
            <link>https://velog.io/@jh_devlog/SB-3%EA%B8%B0-%EC%BD%94%EB%93%9C%EC%9E%87-%EC%8A%A4%ED%94%84%EB%A6%B0%ED%8A%B8-%EC%9C%84%ED%81%B4%EB%A6%AC%ED%8E%98%EC%9D%B4%ED%8D%BC-16%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@jh_devlog/SB-3%EA%B8%B0-%EC%BD%94%EB%93%9C%EC%9E%87-%EC%8A%A4%ED%94%84%EB%A6%B0%ED%8A%B8-%EC%9C%84%ED%81%B4%EB%A6%AC%ED%8E%98%EC%9D%B4%ED%8D%BC-16%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Sun, 24 Aug 2025 14:07:53 GMT</pubDate>
            <description><![CDATA[<h3 id="q1-spring-cache에서-cacheable-cacheput-cacheevict의-차이점과-각각을-어떤-상황에서-사용하는-것이-적절한지-설명해주세요">Q1. Spring Cache에서 @Cacheable, @CachePut, @CacheEvict의 차이점과 각각을 어떤 상황에서 사용하는 것이 적절한지 설명해주세요.</h3>
<hr>
<h4 id="✅-spring-cache란">✅ Spring Cache란?</h4>
<ul>
<li>Spring Cache는 반복적인 DB 조회나 계산 비용이 큰 메서드 실행 결과를 캐시에 저장하고 재활용할 수 있도록 도와주는 <strong>캐시 추상화 레이어</strong>이다.</li>
<li>어노테이션을 통해 <strong>선언적으로 캐시를 관리</strong>할 수 있다.</li>
<li>사용 전 <code>@EnableCaching</code> 설정으로 <strong>캐시 기능을 활성화</strong>해야 한다.</li>
<li>주요 어노테이션으로는 <code>@Cacheable</code>, <code>@CachePut</code>, <code>@CacheEvict</code>가 있다.</li>
</ul>
<blockquote>
<p>💡 Tip</p>
</blockquote>
<ul>
<li>Spring Cache는 <strong>AOP 프록시 기반</strong>이라 같은 클래스 내에서 자기 자신 메서드를 호출할 땐 적용되지 않는다. → 자기 자신을 주입받아 호출하거나, 캐시 적용 메서드를 별도 Bean으로 분리하여 해결</li>
<li>Spring Cache는 TTL, 만료 정책, 최대 크기 설정을 직접 제공하지 않는다. → 구현체(Caffeine/Redis 등)에서 설정해야 함</li>
</ul>
<hr>
<h4 id="✅-cacheable">✅ @Cacheable</h4>
<ul>
<li>캐시 조회 후 히트되는게 없으면 메서드를 실행한다. -&gt; 메서드 실행 결과를 <strong>캐시에 저장</strong>한다.</li>
<li>캐시가 히트되면 <strong>메서드를 실행하지 않고 캐시 값을 반환</strong>한다.</li>
<li>특징<ul>
<li>조건부 캐싱 지원 (<code>condition</code>, <code>unless</code>)</li>
<li>캐시 키 커스터마이징 가능 (<code>key</code> 속성에 SpEL 활용)</li>
<li>null 결과는 캐싱하지 않도록 설정 가능 (<code>unless = &quot;#result == null&quot;</code>)</li>
</ul>
</li>
</ul>
<pre><code class="language-java">@Service
public class MenuService {

    @Cacheable(value = &quot;menus&quot;, key = &quot;#id&quot;, unless = &quot;#result == null&quot;)
    public Menu getMenu(Long id) {
        return menuRepository.findById(id)
            .orElseThrow(() -&gt; new MenuNotFoundException(id));
    }
}</code></pre>
<hr>
<h4 id="✅-cacheput">✅ @CachePut</h4>
<ul>
<li><strong>항상 메서드를 실행</strong>하고 결과를 <strong>캐시에 갱신(덮어쓰기)</strong>한다.</li>
<li>조회시 사용한 <code>@Cacheable</code>과 키를 동일하게 지정해야 한다. (캐시 일관성 유지)</li>
<li>기본적으로 트랜잭션이 커밋 후 반영된다.</li>
</ul>
<pre><code class="language-java">@Service
public class MenuService {

    @CachePut(value = &quot;menus&quot;, key = &quot;#menu.id&quot;)
    public Menu updateMenu(Menu menu) {
        Menu updatedMenu = menuRepository.save(menu);
        return updatedMenu;
    }
}</code></pre>
<hr>
<h4 id="✅-cacheevict">✅ @CacheEvict</h4>
<ul>
<li><strong>특정 캐시 항목을 제거</strong>하거나 <strong>전체 캐시 무효화</strong> 역할을 한다. </li>
<li>기본적으로 트랜잭션이 커밋 후 반영된다.</li>
<li><strong>주요 속성</strong><ul>
<li><code>allEntries = true</code>: 캐시 전체 삭제</li>
<li><code>beforeInvocation</code>: 메서드 실행 전 캐시 제거 여부로, true로 설정하면 <strong>예외 발생 시에도 캐시가 제거됨</strong> (기본값 : <code>false</code>) </li>
</ul>
</li>
</ul>
<pre><code class="language-java">// 특정 캐시 항목 제거
@CacheEvict(value = &quot;menus&quot;, key = &quot;#id&quot;)
@Transactional
public void deleteMenu(Long id) {
    menuRepository.deleteById(id);
}

// 모든 캐시 항목 제거
@CacheEvict(value = &quot;menus&quot;, allEntries = true)
@Transactional
public void clearAllMenuCache() {
    menuRepository.bulkUpdateStatus();
}

// 예외가 발생해도 캐시 무효화
@CacheEvict(value = &quot;menus&quot;, allEntries = true, beforeInvocation = true)
public void clearAllMenuCacheForce() {
    ...
}
</code></pre>
<hr>
<h4 id="🆚-차이점">🆚 차이점</h4>
<table>
<thead>
<tr>
<th>어노테이션</th>
<th>메서드 실행</th>
<th>캐시 동작</th>
</tr>
</thead>
<tbody><tr>
<td><code>@Cacheable</code></td>
<td>캐시 히트 시 생략</td>
<td>캐시 미스 시 저장</td>
</tr>
<tr>
<td><code>@CachePut</code></td>
<td>항상 실행</td>
<td>항상 캐시를 갱신(덮어쓰기)</td>
</tr>
<tr>
<td><code>@CacheEvict</code></td>
<td>실행 여부와 무관</td>
<td>특정 키 또는 전체 캐시 제거</td>
</tr>
</tbody></table>
<hr>
<h4 id="📌-활용-예시">📌 활용 예시</h4>
<ul>
<li><code>@Cacheable</code> : 읽기 비중이 크고 자주 변경되지 않는 데이터  <ul>
<li>ex. 상품 상세 보기, 설정 값 조회</li>
</ul>
</li>
<li><code>@CachePut</code> : 최신 상태를 즉시 캐시에 반영해야 하는 수정/생성 작업<ul>
<li>ex. 상품 수정, 신규 등록 시 캐시 갱신</li>
</ul>
</li>
<li><code>@CacheEvict</code> : 데이터 삭제/벌크 변경 후 캐시 무효화<ul>
<li>ex. 상품 삭제, 배치로 대량 상태 변경 후 캐시 초기화</li>
</ul>
</li>
</ul>
<blockquote>
<p>💡 <code>@Caching</code>  : 한 메서드에서 여러 캐시 동작을 조합하여 사용할 수 있다.</p>
</blockquote>
<pre><code class="language-java">@Caching(
    put = { @CachePut(value = &quot;menus&quot;, key = &quot;#menu.id&quot;) },
    evict = { @CacheEvict(value = &quot;menuList&quot;, allEntries = true) }
)
public Menu updateAndInvalidateList(Menu menu) {
    return menuRepository.save(menu);
}</code></pre>
<hr>
<h3 id="q2-로컬-캐시와-분산-캐시의-개념-차이와-각각의-장단점-그리고-실무에서-어떤-기준으로-선택해야-하는지-설명해주세요">Q2. 로컬 캐시와 분산 캐시의 개념 차이와 각각의 장단점, 그리고 실무에서 어떤 기준으로 선택해야 하는지 설명해주세요.</h3>
<hr>
<h4 id="✅-캐시cache란">✅ 캐시(Cache)란?</h4>
<ul>
<li>자주 사용하는 데이터를 빠르게 가져오기 위해 임시 저장소에 저장하는 기술이다.</li>
<li><strong>핵심 원리</strong><ul>
<li>시간 지역성(Temporal Locality) : 최근 접근한 데이터는 다시 접근될 가능성이 높다.</li>
<li>공간 지역성(Spatial Locality) : 접근한 데이터 근처의 데이터도 접근될 가능성이 높다.</li>
</ul>
</li>
</ul>
<hr>
<h4 id="✅-로컬-캐시-local-cache">✅ 로컬 캐시 (Local Cache)</h4>
<ul>
<li>애플리케이션과 같은 <strong>JVM 메모리 공간</strong>에서 동작하는 캐시이다.</li>
<li>대표적으로 Caffeine, Guava 등이 있다.</li>
<li>특징<ul>
<li><strong>네트워크를 거치지 않고</strong> 메모리에 직접 접근 -&gt; 가장 빠름</li>
<li>네트워크 장애와 무관하게 동작</li>
<li>JVM 힙 메모리에 의존 -&gt; <strong>메모리 크기 제약</strong></li>
<li>다중 서버 환경에서 <strong>서버 간 캐시 불일치</strong> 발생</li>
<li>애플리케이션 재시작 시 캐시 데이터 손실</li>
</ul>
</li>
</ul>
<hr>
<h4 id="✅-분산-캐시-distributed-cache">✅ 분산 캐시 (Distributed Cache)</h4>
<ul>
<li><strong>별도의 캐시 서버</strong>에 데이터를 저장하고 여러 서버 인스턴스가 공유한다.</li>
<li>대표적으로 Redis, Memcached 등이 있다.<ul>
<li>Redis는 영속화 옵션 제공 -&gt; 보조 데이터 저장소로 활용 가능</li>
</ul>
</li>
<li>특징<ul>
<li><strong>네트워크를 통해 접근</strong> -&gt; 로컬 캐시보다 상대적으로 느림</li>
<li>여러 서버가 <strong>동일한 데이터 공유</strong> -&gt; 다중 서버 환경에 적합</li>
<li>캐시 서버는 독립적으로 <strong>확장/클러스터링</strong> 가능</li>
<li><strong>네트워크 지연</strong> 및 <strong>추가 인프라 비용</strong> 발생</li>
</ul>
</li>
</ul>
<blockquote>
<p>💡 클러스터링(Clustering) : 여러 캐시 서버를 하나의 클러스터(집합)로 묶어 부하 분산과 고가용성을 제공하는 기술</p>
</blockquote>
<hr>
<h4 id="🆚-장단점">🆚 장단점</h4>
<table>
<thead>
<tr>
<th>구분</th>
<th>로컬 캐시(Local Cache)</th>
<th>분산 캐시(Distributed Cache)</th>
</tr>
</thead>
<tbody><tr>
<td><strong>속도</strong></td>
<td>매우 빠름</td>
<td>상대적으로 느림</td>
</tr>
<tr>
<td><strong>구현 난이도</strong></td>
<td>단순 (라이브러리 추가)</td>
<td>별도 캐시 서버 구축 및 운영 필요</td>
</tr>
<tr>
<td><strong>일관성</strong></td>
<td>서버별 불일치 발생 가능</td>
<td>여러 서버가 데이터 공유 -&gt; 일관성 ↑</td>
</tr>
<tr>
<td><strong>확장성</strong></td>
<td>서버 인스턴스마다 캐시 중복</td>
<td>캐시 서버 확장/클러스터링으로 처리량 증가</td>
</tr>
<tr>
<td><strong>장애 대응</strong></td>
<td>앱 재시작 시 데이터 손실</td>
<td>복제/클러스터링으로 장애 대응 가능</td>
</tr>
<tr>
<td><strong>비용</strong></td>
<td>별도 인프라 불필요</td>
<td>추가 인프라 운영 비용 발생</td>
</tr>
</tbody></table>
<hr>
<h4 id="💡-실무-선택-기준">💡 실무 선택 기준</h4>
<ul>
<li><p><strong>로컬 캐시가 적합한 경우</strong></p>
<ul>
<li>단일 서버 / 소규모 애플리케이션</li>
<li>읽기 중심, 데이터 변경이 드문 경우</li>
<li>TTL 짧은 데이터 (ex. 1~2초 주기)</li>
<li>네트워크 비용/복잡도를 줄이고 싶은 경우</li>
</ul>
</li>
<li><p><strong>분산 캐시가 적합한 경우</strong></p>
<ul>
<li>다중 서버 / 대규모 트래픽 환경</li>
<li>쓰기/갱신 작업이 빈번하고, 데이터 일관성이 중요한 경우</li>
<li>고가용성, 확장성이 요구되는 클라우드 환경</li>
</ul>
</li>
</ul>
<blockquote>
<p>💡 <strong>혼합 전략 (다중 레벨 캐시, L1 + L2)</strong></p>
</blockquote>
<ul>
<li>L1(로컬) : 애플리케이션 내부 초고속 응답</li>
<li>L2(분산) : 서버 간 데이터 일관성 확보</li>
<li>ex. Spring Boot : Caffeine(로컬, L1) + Redis(분산, L2) </li>
<li>실무에서는 혼합전략을 가장 많이 활용함</li>
</ul>
<hr>
<h3 id="📄-참고-문서">📄 참고 문서</h3>
<ul>
<li>코드잇 강의 교안</li>
<li><a href="https://docs.spring.io/spring-framework/reference/integration/cache/annotations.html">Spring Framework 공식 문서 - 선언적 주석 기반 캐싱</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SB 3기] 코드잇 스프린트 위클리페이퍼 15주차]]></title>
            <link>https://velog.io/@jh_devlog/SB-3%EA%B8%B0-%EC%BD%94%EB%93%9C%EC%9E%87-%EC%8A%A4%ED%94%84%EB%A6%B0%ED%8A%B8-%EC%9C%84%ED%81%B4%EB%A6%AC%ED%8E%98%EC%9D%B4%ED%8D%BC-15%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@jh_devlog/SB-3%EA%B8%B0-%EC%BD%94%EB%93%9C%EC%9E%87-%EC%8A%A4%ED%94%84%EB%A6%B0%ED%8A%B8-%EC%9C%84%ED%81%B4%EB%A6%AC%ED%8E%98%EC%9D%B4%ED%8D%BC-15%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Sun, 17 Aug 2025 08:34:16 GMT</pubDate>
            <description><![CDATA[<h3 id="q1-멀티스레드-환경에서-발생하는-대표적인-문제-중-하나인-경쟁-상태race-condition에-대해-설명하고-이를-해결하기-위한-다양한-전략을-설명해보세요">Q1. 멀티스레드 환경에서 발생하는 대표적인 문제 중 하나인 경쟁 상태(Race Condition)에 대해 설명하고, 이를 해결하기 위한 다양한 전략을 설명해보세요.</h3>
<hr>
<h4 id="✅-경쟁-상태race-condition란">✅ 경쟁 상태(Race Condition)란?</h4>
<ul>
<li>여러 스레드나 프로세스가 <strong>동시에 공유 자원(변수, 메모리, 파일 등)</strong>에 접근할 때, 실행 순서에 따라 결과가 달라질 수 있는 상태를 말한다.</li>
</ul>
<blockquote>
<p>👉 예시 :</p>
</blockquote>
<ul>
<li>A의 계좌에 15만원이 있을 때, 두 명이 동시에 1만원씩 입금하면 정상 결과는 17만원이어야 한다.</li>
<li>하지만 두 스레드가 동시에 15만원을 읽어 각각 1만원을 더해 쓰면 최종 결과가 16만원으로 잘못 저장될 수 있다.</li>
</ul>
<hr>
<h4 id="✅-임계-구역critical-section이란">✅ 임계 구역(Critical Section)이란?</h4>
<ul>
<li>공유 자원에 접근하는 코드 블록을 말한다.</li>
<li>경쟁 상태가 발생할 수 있으므로 공유 자원의 독점을 보장해줘야 하는 영역이다.</li>
</ul>
<hr>
<h4 id="📌--경쟁-상태-해결-조건-3가지">📌  경쟁 상태 해결 조건 (3가지)</h4>
<p><strong>1. 상호 배제(Mutual Exclusion)</strong> : 한 스레드가 임계 구역을 실행 중이면 다른 스레드는 진입할 수 없어야 한다.</p>
<p><strong>2. 진행(Progress)</strong> : 임계 구역에 실행 중인 스레드가 없을 때는, 대기 중인 스레드 중 하나가 반드시 진입할 수 있어야 한다.</p>
<p><strong>3. 한정 대기(Bounded Waiting)</strong> : 특정 스레드가 무한정 대기하지 않도록 보장해야 한다. (기아 상태 방지)</p>
<hr>
<h4 id="🛠️-경쟁-상태-해결-전략">🛠️ 경쟁 상태 해결 전략</h4>
<ul>
<li>해결 방법은 크게 <strong>1) 락 기반 동기화, 2) 원자적 연산 활용, 3) Thread-safe 자료구조 사용</strong>으로 나눌 수 있다.</li>
</ul>
<p><strong>⭐️ 1. 동기화 전략 (Lock 기반)</strong></p>
<ul>
<li><p>락을 걸어 순서를 보장하는 전통적인 방식</p>
</li>
<li><p>스레드가 임계 구역에 <strong>순차적으로 진입</strong>하도록 제어한다.</p>
</li>
<li><p>** 대표적인 해결 방법:**</p>
<ul>
<li><strong>뮤텍스(Mutex)</strong> <ul>
<li>한 번에 하나의 스레드만 공유 자원에 접근 가능</li>
<li>다른 스레드는 해당 자원이 해제될 때까지 대기해야 함</li>
<li>단순하고 안정적이지만, 동시에 많은 스레드가 몰리면 성능 저하 발생</li>
</ul>
</li>
<li><strong>세마포어(Semaphore)</strong> <ul>
<li>동시에 접근할 수 있는 스레드의 최대 개수를 제한</li>
<li>ex. 최대 3개 스레드까지만 DB 접근 허용</li>
<li>뮤텍스는 세마포어의 개수가 1인 특수한 경우라고 볼 수 있음</li>
</ul>
</li>
<li><strong>모니터(Monitor)</strong><ul>
<li>공유 자원과 접근 메서드를 객체 단위로 캡슐화하여 관리</li>
<li>자바의 <code>synchronized</code> 블록이나 메서드가 대표적인 구현체</li>
</ul>
</li>
</ul>
</li>
</ul>
<p><strong>2. 원자적 연산 사용 (Lock-free)</strong></p>
<ul>
<li>락 대신 CPU 명령어로 원자성을 보장하는 방식</li>
<li>CAS(Compare-And-Swap) 기반으로, 공유 자원 수정이 쪼갤 수 없는 단일 연산처럼 처리된다.</li>
<li>다른 스레드가 끼어들 수 없다.</li>
<li>ex. <code>AtomicInteger.incrementAndGet()</code></li>
</ul>
<p><strong>3. Thread-Safe 자료 구조 사용</strong> </p>
<ul>
<li>동기화가 내장된 자료구조 활용하는 방식</li>
<li>멀티스레드 환경에서도 안전하게 데이터에 접근 가능하다.</li>
<li>ex. <code>ConcurrentHashMap</code>, <code>CopyOnWriteArrayList</code>, <code>ConcurrentLinkedQueue</code></li>
</ul>
<blockquote>
<p>💡 상황에 따라 성능과 안정성을 고려해 적절한 방법을 선택해야 한다. </p>
</blockquote>
<ul>
<li>락 기반 방식은 직관적이지만 성능 저하가 있을 수 있다.</li>
<li>원자적 연산은 빠르지만 복잡한 로직에는 한계가 있다. </li>
<li>복잡한 데이터 구조는 Thread-safe 자료구조를 사용하는 것이 더 안전하다.</li>
</ul>
<hr>
<h3 id="q2-비동기-환경에서-mdclogback-mapped-diagnostic-context나-securitycontext-같은-컨텍스트-정보를-스레드-간에-전달해야-할-경우-처리하는-방법에-대해-설명하세요">Q2. 비동기 환경에서 MDC(Logback Mapped Diagnostic Context)나 SecurityContext 같은 컨텍스트 정보를 스레드 간에 전달해야 할 경우, 처리하는 방법에 대해 설명하세요.</h3>
<hr>
<h4 id="✅-비동기async-환경이란">✅ 비동기(Async) 환경이란?</h4>
<ul>
<li>여러 작업들이 서로 영향을 주지 않고 독립적으로 실행될 수 있는 환경이다.</li>
<li>요청 처리 스레드와 별도 스레드풀에서 작업을 실행함</li>
<li>이는 응답 시간을 줄여 시간이 오래 걸리는 작업을 처리할 때 효율적이다.</li>
</ul>
<hr>
<h4 id="⚠️-발생할-수-있는-문제">⚠️ 발생할 수 있는 문제</h4>
<ul>
<li><code>ThreadLocal</code> 기반 컨텍스트(MDC, <code>SecurityContextHolder</code>)는 스레드가 바뀌면 자동 전파되지 않는다. (사라짐)</li>
<li>ex. 로그(traceId)가 유실될 수 있음</li>
<li>ex. 인증/인가 정보가 사라져 권한체크가 실패할 수 있음</li>
</ul>
<blockquote>
<p><code>ThreadLocal</code>은 스레드의 로컬 컨텍스트 변수로 스레드가 살아있는 동안 계속해서 유지되는 변수를 말한다. (스레드의 글로벌 변수)</p>
</blockquote>
<hr>
<h4 id="🛠️-문제-해결-방법--taskdecorator-활용">🛠️ 문제 해결 방법 : TaskDecorator 활용</h4>
<ul>
<li><code>TaskDecorator</code>는 스레드풀에서 실행되는 Runnable을 감싸 전·후 처리를 할 수 있게 해주는 인터페이스이다.</li>
<li>이를 이용하면 비동기 실행 시에도 MDC와 SecurityContext같은 <code>ThreadLocal</code> 기반 컨텍스트를 안전하게 전파할 수 있다.</li>
<li>원리 : 컨텍스트를 <strong>캡처</strong> → 비동기 태스크에 <strong>래핑</strong> → 실행 스레드에서 <strong>복원</strong> → 실행 후 <strong>정리</strong> </li>
</ul>
<p><strong>1. <code>TaskDecorator</code> 구현</strong></p>
<pre><code class="language-java">public class ContextCopyingTaskDecorator implements TaskDecorator {
    @Override
    public Runnable decorate(Runnable task) {
        // 1) Capture
        Map&lt;String, String&gt; callerMdc = MDC.getCopyOfContextMap();
        SecurityContext callerSec = SecurityContextHolder.getContext();

        return () -&gt; {
            try {
                // 2) Restore
                if (callerMdc != null) MDC.setContextMap(callerMdc);
                SecurityContextHolder.setContext(callerSec);

                // 3) Run
                task.run();
            } finally {
                // 4) Clear
                MDC.clear();
                SecurityContextHolder.clearContext();
            }
        };
    }
}
</code></pre>
<p><strong>2. 스레드풀에 등록 + <code>@Async</code> 메서드에서 활용</strong></p>
<pre><code class="language-java">@EnableAsync
@Configuration
public class AsyncConfig {

    @Bean(name = &quot;appExecutor&quot;)
    public ThreadPoolTaskExecutor appExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setTaskDecorator(new ContextCopyingTaskDecorator());
        executor.initialize();
        return executor;
    }
}</code></pre>
<pre><code class="language-java">@Service
public class MyService {
    @Async(&quot;appExecutor&quot;)
    public void doAsyncWork() {
        // 여기서 MDC와 SecurityContext 정상 동작
    }
}
</code></pre>
<blockquote>
<h4 id="🔁-스레드-간-컨텍스트-전달-원칙">🔁 스레드 간 컨텍스트 전달 원칙</h4>
</blockquote>
<ul>
<li><strong>캡처(Capture)</strong>: 현재 스레드의 컨텍스트 저장</li>
<li><strong>래핑(Wrap)</strong>: 비동기 태스크를 컨텍스트 포함 형태로 감싸기</li>
<li><strong>복원(Restore)</strong>: 실행 스레드에서 컨텍스트 복원</li>
<li><strong>정리(Clear)</strong>: 실행 후 반드시 초기화 (스레드풀 재사용 시 누수 방지)</li>
</ul>
<hr>
<h3 id="📄-참고-문서">📄 참고 문서</h3>
<ul>
<li><a href="https://charles098.tistory.com/88">[운영체제] 경쟁상태(Race Condition)와 동기화(Synchronization)의 필요성, 임계 구역(Critical Section)</a></li>
<li><a href="https://zangzangs.tistory.com/115">[OS] Race Condition 경쟁상태란?</a></li>
<li><a href="https://turtledev.tistory.com/47">[CS]경쟁 상태(Race Condition)과 교착 상태(Dead Lock)
</a></li>
<li><a href="https://moonong.tistory.com/129">[Java/Spring] 비동기 처리 시 ThreadContext 를 관리하는 방법</a></li>
<li><a href="https://blog.gangnamunni.com/post/mdc-context-task-decorator/">Spring 의 동기, 비동기, 배치 처리시 항상 context 를 유지하고 로깅하기</a></li>
<li><a href="https://yeonyeon.tistory.com/318">@Async와 함께 사라지다 (feat. TaskDecorator)</a></li>
<li><a href="https://velog.io/@kk95610/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-Async%EC%8B%A4%ED%96%89-%EC%8B%9C-TaskDecorator-%EC%84%A4%EC%A0%95">[트러블슈팅] @Async실행 시 TaskDecorator 설정</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SB 3기] 코드잇 스프린트 위클리페이퍼 14주차]]></title>
            <link>https://velog.io/@jh_devlog/SB-3%EA%B8%B0-%EC%BD%94%EB%93%9C%EC%9E%87-%EC%8A%A4%ED%94%84%EB%A6%B0%ED%8A%B8-%EC%9C%84%ED%81%B4%EB%A6%AC%ED%8E%98%EC%9D%B4%ED%8D%BC-14%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@jh_devlog/SB-3%EA%B8%B0-%EC%BD%94%EB%93%9C%EC%9E%87-%EC%8A%A4%ED%94%84%EB%A6%B0%ED%8A%B8-%EC%9C%84%ED%81%B4%EB%A6%AC%ED%8E%98%EC%9D%B4%ED%8D%BC-14%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Sun, 10 Aug 2025 16:19:57 GMT</pubDate>
            <description><![CDATA[<h3 id="q-spring-기반-웹-애플리케이션에서-발생할-수-있는-4가지-주요-보안-공격-csrf-xss-세션-고정-jwt-탈취에-대해-설명하고-각각에-대한-spring-security-또는-일반적인-대응-전략을-설명하세요">Q. Spring 기반 웹 애플리케이션에서 발생할 수 있는 4가지 주요 보안 공격 (CSRF, XSS, 세션 고정, JWT 탈취)에 대해 설명하고, 각각에 대한 Spring Security 또는 일반적인 대응 전략을 설명하세요.</h3>
<hr>
<h4 id="👾-1-csrf-cross-site-request-forgery">👾 1. CSRF (Cross-Site Request Forgery)</h4>
<ul>
<li>사용자의 인증 상태(세션/쿠키)를 악용해 사용자가 의도하지 않은 요청을 전송하도록 만드는 공격</li>
</ul>
<h4 id="공격-원리">공격 원리:</h4>
<ol>
<li>사용자가 서비스에 로그인(쿠키 발급) </li>
<li>공격 페이지 유도</li>
<li>사용자 브라우저가 쿠키를 자동 포함해 요청 전송</li>
<li>서버는 사용자의 정상 요청으로 오인</li>
<li>의도치 않은 동작 수행</li>
</ol>
<h4 id="대응-전략">대응 전략:</h4>
<ul>
<li><strong>CSRF 토큰 사용</strong>: 서버가 토큰 발급 -&gt; 클라이언트가 요청 헤더로 회송</li>
<li><strong>보호 범위</strong>: 상태 변경 메서드(POST/PUT/PATCH/DELETE)에서 토큰 강제</li>
<li><strong>쿠키 옵션</strong>: <code>SameSite=Lax|Strict</code>, <code>Secure</code>, <code>HttpOnly</code>
➡️ ⭐️ 서버가 준 CSRF 토큰을 요청에 실어 보내고, 쿠키는 SameSite로 잠그기</li>
</ul>
<h4 id="spring-security-설정-예시">Spring Security 설정 예시:</h4>
<pre><code class="language-java">http
  .csrf(csrf -&gt; csrf
    .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
  );</code></pre>
<blockquote>
<p>⚠️ 기본 헤더명은 &quot;X-XSRF-TOKEN&quot;이므로, &quot;X-CSRF-TOKEN&quot;을 사용하려면<code>CookieCsrfTokenRepository</code>에서 직접 지정해주어야 한다. (<code>repo.setHeaderName(&quot;X-CSRF-TOKEN&quot;);</code>)</p>
</blockquote>
<hr>
<h4 id="💉-2-xss-cross-site-scripting">💉 2. XSS (Cross-Site Scripting)</h4>
<ul>
<li>입력값에 악성 스크립트를 삽입하여 사용자의 브라우저에서 실행시키는 공격</li>
<li>종류 : Stored, Reflected, DOM</li>
</ul>
<h4 id="공격-원리-1">공격 원리:</h4>
<ol>
<li>공격자가 스크립트가 포함된 데이터를 서버/클라이언트 경로로 주입 </li>
<li>브라우저가 이를 코드로 해석하고 실행</li>
</ol>
<h4 id="stored--reflected--dom-xss">Stored / Reflected / DOM XSS</h4>
<table>
<thead>
<tr>
<th>종류</th>
<th>공격 방식</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Stored XSS</strong> (저장형)</td>
<td>악성 스크립트를 <strong>서버에 저장</strong> 후, 사용자가 해당 데이터를 불러올 때 실행</td>
</tr>
<tr>
<td><strong>Reflected XSS</strong> (반사형)</td>
<td>악성 스크립트를 <strong>요청 파라미터나 URL</strong>에 담아 서버 응답에 즉시 반영</td>
</tr>
<tr>
<td><strong>DOM XSS</strong></td>
<td>서버 응답을 거치지 않고, <strong>클라이언트 측(JavaScript)</strong>에서 DOM 조작 시 발생</td>
</tr>
</tbody></table>
<h4 id="대응-전략-1">대응 전략:</h4>
<ul>
<li><strong>출력 인코딩</strong>: 템플릿 엔진 기본 이스케이프(<code>th:text</code>) 사용</li>
<li><strong>Sanitizer 사용</strong>: 리치 텍스트는 화이트리스트 기반 HTML Sanitizer 적용</li>
<li><strong>DOM 조작 주의</strong>: <code>innerHTML</code> 등 신뢰되지 않은 HTML 직접 삽입 금지</li>
<li><strong>보안 헤더 설정</strong>: CSP(Content Security Policy), X-Content-Type-Options, X-Frame-Options 등 적용</li>
</ul>
<h4 id="spring-security-설정-예시-1">Spring Security 설정 예시:</h4>
<pre><code class="language-java">http.headers(h -&gt; h
  // CSP 설정
  .contentSecurityPolicy(csp -&gt; csp.policyDirectives(
    &quot;default-src &#39;self&#39;; &quot; +
    &quot;script-src &#39;self&#39;; &quot; +
    &quot;object-src &#39;none&#39;; &quot; +
    &quot;base-uri &#39;self&#39;; &quot; +
    &quot;frame-ancestors &#39;none&#39;&quot;))
  .xssProtection(x -&gt; x.disable())        // 구형 XSS 필터 비활성 → CSP로 대체
  .contentTypeOptions(withDefaults())    // MIME 타입 스니핑 방지
  .frameOptions(frame -&gt; frame.deny())    // 클릭재킹 방지
  // HTTPS 강제
  .httpStrictTransportSecurity(hsts -&gt; hsts     
    .maxAgeInSeconds(31536000) // 1년
    .includeSubdomains(true)
  )
);
</code></pre>
<hr>
<h4 id="🔓-3-세션-고정-공격-session-fixation">🔓 3. 세션 고정 공격 (Session Fixation)</h4>
<ul>
<li>공격자가 미리 정한 세션 ID를 사용자가 쓰도록 유도하고, 인증 후에도 같은 세션을 공유해 권한을 탈취하는 공격</li>
</ul>
<h4 id="공격-원리-2">공격 원리:</h4>
<ol>
<li>로그인 전 세션을 공격자가 고정</li>
<li>로그인 후에도 동일 세션 ID 유지 시 탈취</li>
</ol>
<h4 id="대응-전략-2">대응 전략:</h4>
<ul>
<li><strong>로그인 시 세션 재발급</strong>: <code>migrateSession</code>(기본값)</li>
<li><strong>로그아웃 시 세션 무효화</strong></li>
<li><strong>URL Rewriting 금지</strong>: <code>JSESSIONID</code>가 URL에 노출되지 않도록 설정</li>
<li><strong>쿠키 옵션</strong>: <code>HttpOnly</code>, <code>Secure</code>, <code>SameSite</code></li>
<li><strong>동시 세션 제한 설정</strong></li>
</ul>
<h4 id="spring-security-설정-예시-2">Spring Security 설정 예시:</h4>
<pre><code class="language-java">http.sessionManagement(sm -&gt; sm
  .sessionFixation(sf -&gt; sf.migrateSession())  // 로그인 시 세션 재발급
  .maximumSessions(1)                          // 동시 세션 제한
  .maxSessionsPreventsLogin(false)
);</code></pre>
<h4 id="applicationyml-예시">application.yml 예시:</h4>
<pre><code class="language-yaml">server:
  servlet:
    session:
      tracking-modes: cookie   # URL에 JSESSIONID 붙는 것 차단
      cookie:
        same-site: Lax
        secure: true</code></pre>
<hr>
<h4 id="🪪-4-jwt-탈취">🪪 4. JWT 탈취</h4>
<ul>
<li>Access/Refresh 토큰이 유출되면 만료 전까지 임의 호출 가능</li>
</ul>
<h4 id="공격-원리-3">공격 원리:</h4>
<ul>
<li>XSS, 피싱, 로컬스토리지 노출, 로그 유출, 네트워크 스니핑(비TLS) 등으로 토큰 획득</li>
</ul>
<h4 id="대응-전략-3">대응 전략:</h4>
<ul>
<li><strong>만료 전략</strong>: 짧은 Access Token + 긴 Refresh Token으로 설계</li>
<li><strong>토큰 회전(Token Rotation)</strong> + 재사용 감지(<code>jti</code> 기반)</li>
<li>*<em>저장 위치 전략 *</em><ul>
<li>쿠키: <code>HttpOnly</code> , <code>Secure</code>, <code>SameSite</code> + CSRF 방어</li>
<li>메모리/헤더(Bearer): CSRF엔 강하지만 XSS 방어 철저</li>
</ul>
</li>
<li><strong>서명 검증</strong>: <code>iss</code>, <code>aud</code>, <code>exp</code> 등 검증</li>
<li><strong>네트워크 보안</strong>: HTTPS 강제, CORS 최소 허용, 토큰 로깅 금지</li>
</ul>
<h4 id="spring-security-설정-예시-3">Spring Security 설정 예시:</h4>
<pre><code class="language-java">http
  .oauth2ResourceServer(o -&gt; o
    .jwt(jwt -&gt; jwt.jwtAuthenticationConverter(customConverter))
  )
  .authorizeHttpRequests(auth -&gt; auth
    .requestMatchers(&quot;/api/public/**&quot;).permitAll()
    .anyRequest().authenticated()
  );

// jti 기반 재사용 차단
boolean isRevoked(String jti){ return redis.hasKey(&quot;revoked:&quot;+jti); }
void revoke(String jti, Duration ttl){ redis.set(&quot;revoked:&quot;+jti, &quot;1&quot;, ttl); }
</code></pre>
<hr>
<h3 id="q-jwtjson-web-token의-구조와-각-구성-요소가-어떤-역할을-하는지-구체적으로-설명하세요">Q. JWT(JSON Web Token)의 구조와 각 구성 요소가 어떤 역할을 하는지 구체적으로 설명하세요.</h3>
<hr>
<h4 id="✅-jwt란">✅ JWT란?</h4>
<ul>
<li>JWT는 JSON 객체를 안전하게 전송하기 위한 토큰 표준(RFC 7519)이다.</li>
<li>토큰 자체가 필요한 인증/인가 정보를 모두 포함하고 있어, 서버는 별도의 세션 저장소 없이도 사용자 상태를 확인할 수 있다.</li>
<li>주로 API 인증·인가, SSO(Single Sign-On) 등에서 사용된다.</li>
</ul>
<blockquote>
<p>⚠️ JWT는 기본적으로 <strong>서명(Signature)</strong>으로 위·변조 여부만 검증하며, <strong>암호화는 하지 않는다</strong>.
누구나 Payload를 디코딩할 수 있으므로, <strong>민감 정보는 절대 포함하면 안된다</strong>.</p>
</blockquote>
<hr>
<h4 id="🔍-jwt의-구조">🔍 JWT의 구조</h4>
<ul>
<li>JWT는 <code>.</code>(점)으로 구분된 세 부분(<code>Header</code>,<code>Payload</code>,<code>Signature</code>)으로 구성된다.</li>
<li>각 부분은 <strong>Base64URL 인코딩</strong>되어 있으며, 디코딩하면 원본 JSON을 확인할 수 있다.<pre><code>xxxxx.yyyyy.zzzzz
 ↑      ↑     ↑
Header Payload Signature</code></pre></li>
</ul>
<h4 id="1-header">1. Header</h4>
<ul>
<li><p>토큰의 타입(<code>typ</code>)과 서명 알고리즘(<code>alg</code>), 필요 시 키 식별자(<code>kid</code>)를 포함한다.</p>
</li>
<li><p><strong>예시</strong></p>
<pre><code class="language-java">{
&quot;alg&quot;: &quot;RS256&quot;,
&quot;typ&quot;: &quot;JWT&quot;,
&quot;kid&quot;: &quot;key-2025-08&quot;
}
</code></pre>
</li>
</ul>
<pre><code>- `alg`: 서명 알고리즘 (예: `HS256`, `RS256`, `ES256`) -&gt; `none` **사용 금지** ❌
- `typ`: 토큰 타입 (일반적으로 &quot;JWT&quot;)
- `kid`: Key ID — 키 로테이션 시 어떤 키를 사용할지 식별


#### 2. Payload
- 토큰에 담길 **클레임(Claim)** 정보로 인증/인가에 필요한 식별자, 권한, 메타데이터 등을 포함한다.

- **예시**
```java
{
  &quot;sub&quot;: &quot;user-123&quot;,
  &quot;name&quot;: &quot;Gil Dong&quot;,
  &quot;exp&quot;: 1754803200,
  &quot;iat&quot;: 1754796000,
  &quot;scope&quot;: &quot;read write&quot;,
  &quot;roles&quot;: [&quot;USER&quot;, &quot;ADMIN&quot;],
  &quot;email&quot;: &quot;user@example.com&quot;
}</code></pre><h4 id="클레임-종류">클레임 종류</h4>
<p><strong>1) 표준 클레임 (Registered Claims)</strong> : 사전에 정의된 예약 키</p>
<ul>
<li><code>iss</code> (issuer): 토큰 발급자</li>
<li><code>sub</code> (subject): 토큰 주체 (사용자 ID)</li>
<li><code>aud</code> (audience): 토큰 대상</li>
<li><code>exp</code> (expiration): 만료 시각</li>
<li><code>iat</code> (issued at): 발급 시각</li>
<li><code>nbf</code> (not before): 토큰 활성화 시작 시각</li>
<li><code>jti</code> (jwt id) : 토큰 고유 식별자 (재사용 감지에 활용)</li>
</ul>
<p><strong>2) 공개 클레임 (Public Claims)</strong> : 표준 외의 일반 키, 충돌 방지 위해 네임스페이스(주로 URL 형태) 권장</p>
<p><strong>3) 비공개 클레임 (Private Claims)</strong> : 발급자와 소비자 간 합의된 키 (ex. roles, scope 등)</p>
<h4 id="3-signature">3. Signature</h4>
<ul>
<li><p>Header와 Payload가 위/변조되지 않았는지를 검증한다.</p>
</li>
<li><p><strong>서명 방식</strong></p>
<ul>
<li><strong>대칭키(HS256)</strong>: 하나의 비밀키로 서명/검증 -&gt; 단일 서버에 적합</li>
<li><strong>비대칭키(RS256/ES256)</strong>: 발급자는 개인키로 서명, 검증은 공개키로 수행 -&gt; 마이크로서비스/리소스 서버에 적합</li>
</ul>
</li>
<li><p><strong>예시</strong></p>
<pre><code class="language-java">HMACSHA256(
base64UrlEncode(header) + &quot;.&quot; + base64UrlEncode(payload),
secret
)</code></pre>
</li>
</ul>
<hr>
<h3 id="📄-참고문서">📄 참고문서</h3>
<ul>
<li>코드잇 강의 교안</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Security 공식 문서 정리 - 아키텍처]]></title>
            <link>https://velog.io/@jh_devlog/Spring-Security-%EA%B3%B5%EC%8B%9D-%EB%AC%B8%EC%84%9C-%EC%A0%95%EB%A6%AC-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98</link>
            <guid>https://velog.io/@jh_devlog/Spring-Security-%EA%B3%B5%EC%8B%9D-%EB%AC%B8%EC%84%9C-%EC%A0%95%EB%A6%AC-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98</guid>
            <pubDate>Thu, 07 Aug 2025 06:50:30 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>📌 본 글은 <a href="https://docs.spring.io/spring-security/reference/servlet/architecture.html">Spring Security 공식 문서</a> 를 기반으로 정리한 글입니다.
Spring Security의 필터 체인 구조, DelegatingFilterProxy의 동작 방식, 요청 저장 및 예외 처리 흐름 등을 예시 코드와 함께 설명합니다.
단, 공식 문서 전체를 번역하거나 완전히 포함한 글은 아니며, 일부 내용을 선별하여 정리하였습니다.</p>
</blockquote>
<h1 id="spring-security-architecture">Spring Security Architecture</h1>
<h2 id="📚-목차">📚 목차</h2>
<ol>
<li>A Review of Filters</li>
<li>DelegatingFilterProxy</li>
<li>FilterChainProxy</li>
<li>SecurityFilterChain</li>
<li>Security Filters</li>
<li>Printing the Security Filters</li>
<li>Adding Filters to the Filter Chain</li>
<li>Handling Security Exceptions</li>
<li>Saving Requests Between Authentication</li>
<li>Logging</li>
</ol>
<hr>
<h3 id="1-a-review-of-filters">1. A Review of Filters</h3>
<p><img src="https://velog.velcdn.com/images/jh_devlog/post/eb7d768d-75bc-42be-9e83-0ecb7874e13f/image.png" alt="FilterChain 개요"></p>
<ul>
<li>Spring Security의 보안 기능들은 <strong>Servlet Filter를 기반으로 동작</strong>한다. </li>
<li>Filter는 요청/응답을 가로채거나 수정할 수 있기 때문에, <strong>FilterChain의 구성과 각 필터의 실행 순서가 매우 중요하다.</strong></li>
</ul>
<hr>
<h3 id="2-delegatingfilterproxy">2. DelegatingFilterProxy</h3>
<p><img src="https://velog.velcdn.com/images/jh_devlog/post/4e13f7b0-8a7d-46eb-97da-b4a98e187026/image.png" alt=""></p>
<h4 id="📌-문제-상황-필터-등록-시점-충돌">📌 문제 상황: 필터 등록 시점 충돌</h4>
<ol>
<li>서블릿 컨테이너(Tomcat 등)는 애플리케이션 시작 시, 모든 <code>Filter</code>들을 <strong>미리 등록</strong>해야 함</li>
<li>하지만 우리가 쓰는 <strong>Spring Security Filter</strong>는 일반 Filter가 아닌 <strong>Spring Bean</strong></li>
<li><strong>Spring Bean</strong>은 <strong>Spring Context가 초기화된 이후</strong>에야 생성됨 (ContextLoaderListener 이후)</li>
<li>따라서 컨테이너가 필터를 등록하려는 시점엔 <strong>아직 Spring Bean이 준비되지 않은 상태</strong></li>
</ol>
<p>-&gt; 즉, <strong>필터가 필요한 시점</strong>과 <strong>생성 시점의 타이밍이 맞지 않는 문제</strong> 발생</p>
<h4 id="✅-해결-방법">✅ 해결 방법:</h4>
<p><code>DelegatingFilterProxy</code>를 통해 이 시점 차이를 해결할 수 있다.    </p>
<ul>
<li>겉보기엔 일반 Filter처럼 등록됨 (서블릿 컨테이너가 인식할 수 있도록)</li>
<li>실제 요청이 들어오면 -&gt; 그때 Spring Context에서 해당 이름의 Filter Bean을 찾아서 실행함(<strong>지연 초기화 방식</strong>)</li>
</ul>
<blockquote>
<p>⭐️ <strong>요약</strong></p>
</blockquote>
<ul>
<li>DelegatingFilterProxy는 서블릿 컨테이너와 Spring Context의 초기화 시점 차이를 안전하게 연결해주는 브릿지 역할을 한다.</li>
</ul>
<hr>
<h3 id="3-filterchainproxy">3. FilterChainProxy</h3>
<ul>
<li>요청마다 어떤 SecurityFilterChain을 적용할지 결정하고 실행하는 중간 관리자 역할을 한다.
<img src="https://velog.velcdn.com/images/jh_devlog/post/cdd7668d-024f-48d1-93f5-720f58dcf49d/image.png" alt=""></li>
</ul>
<h4 id="🧠-filterchainproxy를-쓰는-이유">🧠 FilterChainProxy를 쓰는 이유</h4>
<ol>
<li>Spring Security의 모든 필터 처리는 <strong>FilterChainProxy 하나로 시작됨</strong><ul>
<li>문제 생기면 <code>FilterChainProxy</code>에 <strong>디버깅 포인트</strong>를 찍으면 모든 필터 동작을 추적할 수 있음.</li>
</ul>
</li>
<li>Spring Security 전용 기능을 다루기 위해 꼭 필요함 <ul>
<li>예시:<ul>
<li>요청 처리 끝나고 <strong>SecurityContext(인증 정보)를 반드시 초기화</strong>해서 메모리 누수 방지</li>
<li><strong>HttpFirewall</strong>을 통해 이상한 요청 차단 (ex. <code>/..%2f..%2fadmin</code>)</li>
</ul>
</li>
<li><blockquote>
<p>이런 것들은 일반 Filter에서는 처리 불가능.  Spring Security의 철저한 보안 처리를 위해 꼭 필요함.</p>
</blockquote>
</li>
</ul>
</li>
<li>URL 외의 조건으로도 필터 체인을 다르게 설정할 수 있음<ul>
<li>일반 Filter는 단순히 URL 패턴으로만 동작하지만 <code>FilterChainProxy</code>는 Spring의 <code>RequestMatcher</code>를 통해 더 복잡한 조건도 가능하다.</li>
<li>예시:<pre><code class="language-java">  RequestMatcher matcher = new AndRequestMatcher(
      new AntPathRequestMatcher(&quot;/admin/**&quot;),
      request -&gt; request.getHeader(&quot;X-Secret-Key&quot;) != null
  );</code></pre>
</li>
</ul>
</li>
</ol>
<blockquote>
<p>⭐️ <strong>요약</strong></p>
</blockquote>
<ul>
<li>FilterChainProxy는 Sprint Security의 핵심 필터 매니저 역할을 하며, 단순히 서블릿 필터를 위임하는 <code>DelegatingFilterProxy</code>보다 더 많은 역할을 수행한다.</li>
<li>단순한 URL 패턴 매칭을 넘어, <strong>유연한 요청 조건 처리</strong>와 <strong>보안 기능 적용</strong>을 위한 핵심 컴포넌트이다.</li>
</ul>
<hr>
<h3 id="4-securityfilterchain">4. SecurityFilterChain</h3>
<ul>
<li>FilterChainProxy에 의해 현재 요청에 대하여 어떤 Spring Security Filter 인스턴스를 호출해야 하는지 결정한다.</li>
<li>하나의 URL 조건에 대해 적용할 필터 목록을 정의함</li>
</ul>
<p>📌 <strong>실무에서의 사용</strong></p>
<ul>
<li>보통 SecurityFilterChain을 URL별로 나누는 방식으로 사용된다.<ul>
<li>요청 유형마다 요구하는 보안 정책이 다르기 때문<ul>
<li>체인을 분리하면 각 체인은 한 가지 역할만 담당하므로 깔끔하고 명확해짐</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>❓ <strong>빈 SecurityFilterChain 설정</strong></p>
<ul>
<li>어떤 <code>SecurityFilterChain</code>은 <strong>보안 필터를 하나도 포함하지 않을 수 있다.</strong>  <ul>
<li>이는 <strong>특정 요청을 Spring Security가 무시하도록</strong> 설정하고 싶은 경우에 사용된다.</li>
</ul>
</li>
</ul>
<h4 id="🆚-websecuritycustomizer-vs-빈-securityfilterchain">🆚 WebSecurityCustomizer vs 빈 SecurityFilterChain</h4>
<table>
<thead>
<tr>
<th>구분</th>
<th><code>WebSecurityCustomizer</code> (<code>web.ignoring()</code>)</th>
<th>빈 <code>SecurityFilterChain</code> 등록</th>
</tr>
</thead>
<tbody><tr>
<td>목적</td>
<td><strong>Spring Security 자체를 완전히 우회</strong></td>
<td><strong>Spring Security를 거치되, 아무 필터도 적용하지 않음</strong></td>
</tr>
<tr>
<td>적용 시점</td>
<td><strong>FilterChainProxy 진입 전</strong></td>
<td><strong>FilterChainProxy 진입 후</strong>, 필터가 없어서 바로 통과</td>
</tr>
<tr>
<td>보안 필터 동작</td>
<td>아예 동작 안 함 (서블릿 필터 체인 자체를 안 탐)</td>
<td>FilterChainProxy는 실행됨. 단, 필터가 없음</td>
</tr>
<tr>
<td>성능</td>
<td>가장 빠름 (Spring Security 필터 아예 안 거침)</td>
<td>약간 느림 (Security 진입은 하니까)</td>
</tr>
<tr>
<td>추천 사용처</td>
<td>정적 리소스, 에러 페이지 등 <strong>전혀 보안이 필요 없는 요청</strong></td>
<td>요청은 보안 대상이지만, <strong>명시적으로 허용해야 할 요청</strong> (예: <code>/public/**</code>, 공개 API)</td>
</tr>
<tr>
<td>예시 요청</td>
<td><code>/favicon.ico</code>, <code>/css/style.css</code></td>
<td><code>/public/info</code>, <code>/docs/open-access</code></td>
</tr>
</tbody></table>
<hr>
<h3 id="5-security-filters">5. Security Filters</h3>
<ul>
<li>Spring Security는 여러 개의 보안 필터들을 <strong>정해진 순서대로 FilterChainProxy에 등록하여 실행</strong>한다.</li>
</ul>
<h4 id="🔢-필터-순서의-중요성">🔢 필터 순서의 중요성</h4>
<ul>
<li>필터는 올바른 순서대로 실행되어야 보안이 제대로 작동한다.</li>
<li>예시:<ul>
<li>인증 필터가 먼저 실행되어야 인가 필터가 그 인증 결과를 기반으로 접근 권한을 판단한다.</li>
</ul>
</li>
</ul>
<h4 id="🔍-필터-순서확인-방법">🔍 필터 순서확인 방법</h4>
<ul>
<li>Spring Security의 내부 클래스인 <code>FilterOrderRegistration</code>를 통해 각 필터의 등록 순서를 확인할 수 있다.</li>
</ul>
<h4 id="⚠️-커스텀-필터-추가시-주의사항">⚠️ 커스텀 필터 추가시 주의사항</h4>
<ul>
<li>필터를 추가할 때는 반드시 <strong>어느 필터 앞/뒤에 위치해야 하는지</strong> 명확히 알아야 한다.</li>
<li>잘못된 순서에 배치하면 <strong>필터가 제대로 동작하지 않거나, 예상치 못한 예외</strong>가 발생할 수 있다.</li>
</ul>
<h4 id="📌-필터-선언-방식">📌 필터 선언 방식</h4>
<ul>
<li>대부분의 보안 필터는 <code>HttpSecurity</code> DSL을 통해 선언된다.</li>
</ul>
<pre><code class="language-java">@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(Customizer.withDefaults())
            .httpBasic(Customizer.withDefaults())
            .formLogin(Customizer.withDefaults())
            .authorizeHttpRequests(authorize -&gt; authorize
                .anyRequest().authenticated()
            );

        return http.build();
    }

}</code></pre>
<hr>
<h3 id="6-printing-the-security-filters">6. Printing the Security Filters</h3>
<h4 id="🔨-필터-디버깅-방법">🔨 필터 디버깅 방법</h4>
<ol>
<li>애플리케이션 시작 시, 보안 필터 목록 확인하기<ul>
<li>Spring Security는 애플리케이션이 실행될 때, 각 요청 경로(URL 패턴)마다 <strong>적용되는 보안 필터 목록을 DEBUG 로그 수준으로 출력</strong>함</li>
<li>이 로그를 통해 내가 추가한 필터가 정상적으로 필터 체인에 포함되었는지 확인 가능</li>
</ul>
</li>
<li>요청마다 어떤 필터가 실제로 실행되는지 확인하기<ul>
<li>단순히 필터가 등록되었는지뿐 아니라, <strong>특정 요청을 처리할 때 실제 어떤 필터들이 실행되었는지</strong>도 확인 가능</li>
</ul>
</li>
</ol>
<hr>
<h3 id="7-adding-filters-to-the-filter-chain">7. Adding Filters to the Filter Chain</h3>
<h4 id="➕-httpsecurity에서-필터를-추가하는-3가지-방법">➕ HttpSecurity에서 필터를 추가하는 3가지 방법</h4>
<ol>
<li><code>#addFilterBefore(Filter, Class&lt;?&gt;)</code> : 다른 필터 전에 필터를 추가함</li>
<li><code>#addFilterAfter(Filter, Class&lt;?&gt;)</code> : 다른 필터 후에 필터를 추가함</li>
<li><code>#addFilterAt(Filter, Class&lt;?&gt;)</code> : 다른 필터를 필터로 교체함</li>
</ol>
<ul>
<li>커스텀 필터를 생성하는 경우, 필터 체인의 위치를 결정해야 한다.</li>
<li><code>Filter</code> 인터페이스를 직접 구현하는 대신,  요청마다 <strong>딱 한 번만 실행되는 필터의 기본 클래스</strong>인 <code>OncePerRequestFilter</code>를 상속할 수 있다.  (<code>doFilterInternal</code> 메서드 제공)</li>
</ul>
<h4 id="❌-filter-중복-실행-방지">❌ Filter 중복 실행 방지</h4>
<ul>
<li>필터를 Spring Bean으로 등록하면, Spring Boot는 <strong>자동으로 해당 필터를 서블릿 컨테이너에도 등록</strong>한다.</li>
<li>그런데 Spring Security는 <code>FilterChainProxy</code>를 통해 <strong>이미 그 필터를 실행하고 있을 수 있기 때문에</strong>,
  → <strong>필터가 한 요청에서 두 번 실행되는 문제가 발생할 수 있다.</strong></li>
<li>따라서 필터를 꼭 Spring Bean으로 등록해야 할 경우,  <code>FilterRegistrationBean</code>을 사용해 <code>enabled = false</code>로 설정하여 서블릿 컨테이너에 중복 등록되지 않도록 해야 한다.<pre><code class="language-java">@Bean
public FilterRegistrationBean&lt;MyCustomFilter&gt; myFilterRegistration(MyCustomFilter filter) {
  FilterRegistrationBean&lt;MyCustomFilter&gt; registration = new FilterRegistrationBean&lt;&gt;(filter);
  registration.setEnabled(false); // ✅ 컨테이너에 등록되지 않도록 설정
  return registration;
}</code></pre>
<h4 id="🛠️-customizing-a-spring-security-filter">🛠️ Customizing a Spring Security Filter</h4>
</li>
<li>필터의 DSL 메서드를 사용하여 스프링 보안 필터를 구성할 수 있다.</li>
<li>Spring Security는 <code>HttpSecurity</code> DSL을 통해 보안 필터를 자동으로 추가함.</li>
<li>같은 필터를 <code>addFilterAt()</code>으로 직접 추가하려 하면 중복 등록으로 예외 발생함.</li>
<li>이때는 기존 DSL 설정을 <strong>제거하거나 <code>disable()</code>로 비활성화</strong>해야 함.</li>
</ul>
<pre><code class="language-java">.httpBasic((basic) -&gt; basic.disable())</code></pre>
<hr>
<h3 id="8-handling-security-exceptions">8. Handling Security Exceptions</h3>
<p><img src="https://velog.velcdn.com/images/jh_devlog/post/0c9e7933-dbef-48df-a23d-cd1f1e65d125/image.png" alt=""></p>
<h4 id="📌-exceptiontranslationfilter">📌 ExceptionTranslationFilter</h4>
<ul>
<li>Spring Security에서 인증 및 인가 과정에서 예외가 발생했을 때, 그 예외를 처리해주는 필터이다.</li>
<li><code>AccessDeniedException</code> 및 <code>AuthenticationException</code>를 HTTP 응답으로 변환함<h4 id="🔄-exceptiontranslationfilter의-동작-흐름">🔄 ExceptionTranslationFilter의 동작 흐름</h4>
</li>
</ul>
<pre><code class="language-java">try {
    filterChain.doFilter(request, response);
} catch (AccessDeniedException | AuthenticationException ex) {
    if (!authenticated || ex instanceof AuthenticationException) {
        startAuthentication();
    } else {
        accessDenied();
    }
}</code></pre>
<ol>
<li><p>요청을 다음 필터로 넘김</p>
<ul>
<li><code>ExceptionTranslationFilter</code>는 요청을 <strong>그대로 다음 필터 체인에 전달</strong>함  </li>
<li>예외가 발생하는 경우에만 동작함</li>
</ul>
</li>
<li><p>예외가 발생한 경우</p>
<ul>
<li>2-1. <strong>인증 예외</strong> (<code>AuthenticationException</code>)<ul>
<li>사용자가 로그인하지 않았거나, 로그인에 실패한 경우</li>
<li>처리 방식:<ul>
<li><code>SecurityContextHolder</code>를 <strong>비워서 이전 인증 정보 제거</strong></li>
<li>현재 요청(HttpServletRequest)을 <code>RequestCache</code>에 저장</li>
</ul>
</li>
<li><code>AuthenticationEntryPoint</code>를 호출 -&gt; 로그인 페이지로 리다이렉트 또는 <code>401 Unauthorized</code> 응답</li>
</ul>
</li>
<li>2-2. <strong>인가 예외</strong> (<code>AccessDeniedException</code>)<ul>
<li>로그인은 했지만, <strong>권한이 없는 리소스에 접근한 경우</strong></li>
<li>처리 방식:<ul>
<li><code>AccessDeniedHandler</code> 호출 -&gt; <code>403 Forbidden</code> 응답 반환</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><p>예외가 없을 경우</p>
<ul>
<li><strong>아무 작업 없이</strong> 요청을 그대로 통과시킴</li>
</ul>
</li>
</ol>
<hr>
<h3 id="9-saving-requests-between-authentication">9. Saving Requests Between Authentication</h3>
<ul>
<li>인증되지 않은 사용자가 <strong>인증이 필요한 자원</strong>에 접근하면,  Spring Security는 해당 요청을 <strong><code>RequestCache</code>에 저장</strong>해 둔다.</li>
<li>이후 사용자가 <strong>로그인에 성공하면</strong>, 저장된 요청을 꺼내어 <strong>자동으로 다시 실행</strong>한다.</li>
<li>이 덕분에 사용자는 로그인 후에도 <strong>원래 가려던 페이지로 자연스럽게 이동</strong>할 수 있다.</li>
</ul>
<blockquote>
<p>RequestCache: 원래 요청을 저장해두는 Spring Security의 인터페이스
HttpServletRequest: 사용자가 보낸 원래 요청 객체 (URL, 파라미터 포함)
리플레이(re-request): 로그인 성공 후, 저장된 요청을 다시 보내는 것</p>
</blockquote>
<h4 id="📦-requestcache">📦 RequestCache</h4>
<ul>
<li><code>ExceptionTranslationFilter</code><ul>
<li>인증 예외를 감지하면, 로그인 페이지로 리다이렉트하기 전에 <strong>요청을 저장</strong>함</li>
</ul>
</li>
<li><code>RequestCacheAwareFilter</code> <ul>
<li>로그인에 성공한 뒤, <code>RequestCache</code>에 저장된 요청이 있는지 확인해서 <strong>다시 실행함</strong></li>
</ul>
</li>
<li>기본 구현체는 <code>HttpSessionRequestCache</code><ul>
<li>저장된 요청은 <strong>HTTP 세션(HttpSession)</strong> 에 보관되며, 로그인 이후에 해당 세션에서 요청 정보를 꺼내어 리다이렉트함.</li>
</ul>
</li>
<li>커스터마이징 가능<ul>
<li>예를 들어, 특정 파라미터가 있는 요청만 다시 실행하고 싶을 때 <ul>
<li><code>continue</code>라는 파라미터가 있을 때만 <code>RequestCache</code>를 확인하도록 구현 가능</li>
</ul>
</li>
</ul>
</li>
</ul>
<pre><code class="language-java">@Bean
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
    HttpSessionRequestCache requestCache = new HttpSessionRequestCache();
    requestCache.setMatchingRequestParameterName(&quot;continue&quot;);
    http
        // ...
        .requestCache((cache) -&gt; cache
            .requestCache(requestCache)
        );
    return http.build();
}</code></pre>
<h4 id="🚫-prevent-the-request-from-being-saved-요청-저장-비활성화">🚫 Prevent the Request From Being Saved (요청 저장 비활성화)</h4>
<ul>
<li>Spring Security의 기본 동작은 인증되지 않은 사용자의 요청을 세션에 저장하는 것이지만,  필요에 따라 이 기능을 비활성화할 수 있다.</li>
</ul>
<p>📌 <strong>요청을 저장하지 않으려는 이유</strong></p>
<ul>
<li>세션을 사용하지 않으려는 경우 <ul>
<li>요청 정보를 세션 대신 브라우저(쿠키 등)나 DB에 저장하고 싶을 때</li>
</ul>
</li>
<li>보안 또는 서버 부담을 줄이기 위해 <ul>
<li>민감한 요청을 세션에 저장하지 않게 하고 싶을 때</li>
</ul>
</li>
<li>항상 고정된 페이지로 리다이렉트하고 싶은 경우<ul>
<li>로그인 후 원래 요청으로 돌아갈 필요 없이 홈(/) 등으로 이동시키고 싶을 때</li>
</ul>
</li>
</ul>
<p>✅ <strong>해결 방법:</strong> <code>NullRequestCache</code> </p>
<ul>
<li>Spring Security가 제공하는 <code>NullRequestCache</code>는 이름 그대로, <strong>요청을 전혀 저장하지 않는 구현체</strong>이다.</li>
<li>이 구현체를 사용하면 로그인 후에도 <strong>이전 요청 정보가 없기 때문에</strong>, 항상 <strong>지정한 기본 페이지로 리다이렉트</strong>된다.</li>
</ul>
<pre><code class="language-java">@Bean
SecurityFilterChain springSecurity(HttpSecurity http) throws Exception {
    RequestCache nullRequestCache = new NullRequestCache();
    http
        // ...
        .requestCache((cache) -&gt; cache
            .requestCache(nullRequestCache)
        );
    return http.build();
}</code></pre>
<hr>
<h3 id="10-logging">10. Logging</h3>
<ul>
<li>Spring Security는 보안상 요청 거부 사유를 응답에 담지 않기 때문에,  디버깅을 위해 TRACE 수준 로그를 활성화하면 문제 원인을 로그에서 확인할 수 있다.<pre><code class="language-java">logging:
level:
  org.springframework.security: TRACE</code></pre>
</li>
</ul>
<hr>
<h3 id="📝-마무리">📝 마무리</h3>
<p>Spring Security 공식 문서를 읽기 전에는 구조가 너무 복잡하고 어렵게 느껴졌으나 하나씩 따라가며 정리해보니 잘 몰랐던 동작 원리들을 조금씩 이해할 수 있었다. (물론 ChatGPT의 도움을 많이 받았다.😉)</p>
<p>이번 글이 Spring Security의 구조를 이해하는 데 조금이나마 도움이 되었길 바라며, 앞으로도 Spring Security에 대한 내용을 차근차근 더 정리해볼 예정이다. 🙇‍</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SB 3기] 코드잇 스프린트 위클리페이퍼 13주차]]></title>
            <link>https://velog.io/@jh_devlog/SB-3%EA%B8%B0-%EC%BD%94%EB%93%9C%EC%9E%87-%EC%8A%A4%ED%94%84%EB%A6%B0%ED%8A%B8-%EC%9C%84%ED%81%B4%EB%A6%AC%ED%8E%98%EC%9D%B4%ED%8D%BC-13%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@jh_devlog/SB-3%EA%B8%B0-%EC%BD%94%EB%93%9C%EC%9E%87-%EC%8A%A4%ED%94%84%EB%A6%B0%ED%8A%B8-%EC%9C%84%ED%81%B4%EB%A6%AC%ED%8E%98%EC%9D%B4%ED%8D%BC-13%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Sun, 03 Aug 2025 15:20:14 GMT</pubDate>
            <description><![CDATA[<h3 id="q-세션-기반-인증과-토큰-기반-인증의-차이점과-각각의-보안-고려사항에-대해-설명하세요">Q. 세션 기반 인증과 토큰 기반 인증의 차이점과 각각의 보안 고려사항에 대해 설명하세요.</h3>
<h4 id="✅-인증이란">✅ 인증이란?</h4>
<p><strong>인증(Authentication)</strong>이란 사용자의 신원을 확인하는 과정이다.
웹 서비스에서는 주로 로그인 기능을 통해 인증을 수행하며, 인증된 사용자는 서비스에 접근할 수 있다.</p>
<blockquote>
<p>💡 물론, 인증만으로 서비스의 모든 리소스에 접근이 가능한 것은 아니다. 특정 리소스에 대한 접근은 인가(Authorization) 과정을 통해 부여된다.</p>
</blockquote>
<h4 id="✅-세션-기반-인증">✅ 세션 기반 인증</h4>
<ul>
<li>서버가 발급한 <strong>세션 ID</strong>를 통해 인증을 진행하는 방식이다.</li>
<li>세션 ID는 <strong>고유성, 예측 불가능, 임시성</strong>을 갖추어야 하며 서버 메모리나 데이터베이스에 저장된다.</li>
<li>클라이언트는 발급받은 세션 ID를 <strong>쿠키(Cookie)</strong>에 담아 서버에 전달하고, 서버는 해당 세션을 확인하여 인증을 유지한다.</li>
</ul>
<blockquote>
<p>🍪 쿠키(Cookie)란?
클라이언트와 서버 간의 상태를 유지하기 위해 사용하는 작은 데이터 조각을 뜻한다.
웹 서버가 생성하여 브라우저에 전송하고, 브라우저는 이후 요청시 해당 쿠키를 서버에 함께 전송하여 세션 정보를 식별할 수 있도록 한다.</p>
</blockquote>
<h4 id="🔓-세션-기반-인증의-보안-고려사항">🔓 세션 기반 인증의 보안 고려사항</h4>
<ul>
<li>세션 ID<ul>
<li>충분히 길고 예측하기 어려운 값을 사용해야 한다.</li>
<li>로그인 시마다 새션 ID를 재발급해 <strong>세션 고정(Session Fixation)</strong> 공격을 방지한다.</li>
</ul>
</li>
<li>쿠키 설정<ul>
<li><code>Secure</code> : HTTPS 연결에서만 쿠키를 전송되도록 제한한다.</li>
<li><code>HttpOnly</code> : JavaScript를 통한 쿠키 접근을 차단해 XSS 공격을 방어한다.</li>
<li><code>SameSite</code> : 크로스 사이트 요청 시 쿠키 전송을 제어하여 CSRF 공격을 방어한다.</li>
</ul>
</li>
<li>세션 만료 정책<ul>
<li>일정 시간 사용하지 않으면 세션을 만료시켜 세션 탈취 후 장기 사용을 방지한다.</li>
</ul>
</li>
</ul>
<h4 id="✅-토큰-기반-인증">✅ 토큰 기반 인증</h4>
<ul>
<li>서버가 발급하는 <strong>토큰(Token)</strong>을 사용해 인증을 진행하는 방식이다.</li>
<li>토큰은 사용자의 인증 정보와 권한을 포함한 디지털 자격 증명으로, 서버가 상태를 저장하지 않는 <strong>Stateless</strong> 구조를 가진다.</li>
</ul>
<blockquote>
<p>📌 토큰의 특징</p>
</blockquote>
<ul>
<li>자체 포함성 (Self-contained) : 토큰 자체에 인증 및 권한 정보 포함</li>
<li>무상태성 (Stateless) : 서버에서 세션 상태를 관리하지 않음</li>
<li>이식성 (Portability) : 여러 서비스와 도메인에서 공통 사용 가능</li>
</ul>
<h4 id="🔓-토큰-기반-인증의-보안-고려사항">🔓 토큰 기반 인증의 보안 고려사항</h4>
<ul>
<li><strong>Refresh Token 보호</strong><ul>
<li><code>HttpOnly</code>, <code>Secure</code>, <code>SameSite</code> 설정으로 XSS·CSRF 방어</li>
</ul>
</li>
<li><strong>토큰 순환 (Token Rotation)</strong><ul>
<li>Refresh Token 사용 시 새 토큰 발급, 기존 토큰은 즉시 무효화</li>
<li>재사용 감지 시 전체 토큰 폐기 및 강제 재로그인</li>
</ul>
</li>
<li><strong>만료 시간 설정</strong> <ul>
<li>토큰 탈취 피해를 최소화하기 위해 적절한 만료 시간 설정</li>
<li>⭐️ Access Token은 짧은 만료 시간으로 설정</li>
</ul>
</li>
<li><strong>서명 검증</strong>!<ul>
<li>토큰의 서명(Signature)을 검증해 변조 여부 및 무결성 확인    </li>
</ul>
</li>
</ul>
<blockquote>
<p>⚠️ Refresh Token은 일반적으로 긴 만료 시간을 갖기 때문에 탈취 시 장기간 악용될 수 있다. 따라서 매우 민감하게 관리해야 한다.</p>
</blockquote>
<h4 id="🆚-세션-기반-인증과-토큰-기반-인증의-차이점">🆚 세션 기반 인증과 토큰 기반 인증의 차이점</h4>
<table>
<thead>
<tr>
<th>구분</th>
<th>세션 기반 인증</th>
<th>토큰 기반 인증</th>
</tr>
</thead>
<tbody><tr>
<td><strong>상태 관리</strong></td>
<td>서버가 세션 상태를 저장 (<strong>Stateful</strong>)</td>
<td>서버는 상태를 저장하지 않음 (<strong>Stateless</strong>)</td>
</tr>
<tr>
<td><strong>확장성</strong></td>
<td>서버 간 세션 공유 필요 → 확장성 낮음</td>
<td>상태가 없으므로 서버 확장 용이</td>
</tr>
<tr>
<td><strong>저장소</strong></td>
<td>서버 메모리/DB</td>
<td>클라이언트 측에서 토큰 관리</td>
</tr>
<tr>
<td><strong>인증 속성</strong></td>
<td>세션 ID 기반</td>
<td>토큰 자체에 인증 및 권한 정보 포함</td>
</tr>
<tr>
<td><strong>보안 포인트</strong></td>
<td>세션 ID 탈취 방지, 쿠키 설정, 세션 만료 정책 설정</td>
<td>토큰 탈취 방지, 서명 검증, 저장 위치 및 만료 관리</td>
</tr>
</tbody></table>
<hr>
<h3 id="q-oauth-20의-주요-컴포넌트와-authorization-code-grant-흐름을-설명하세요">Q. OAuth 2.0의 주요 컴포넌트와 Authorization Code Grant 흐름을 설명하세요.</h3>
<h4 id="✅-oauth란">✅ OAuth란?</h4>
<ul>
<li><strong>OAuth(Open Authoriazation)</strong>란 사용자가 비밀번호를 직접 제공하지 않고도 다른 서비스(구글, 네이버 등)에 저장된 개인 정보에 대하여 제한된 접근 권한을 제3자 애플리케이션에 위임할 수 있도록 하는 표준 프로토콜이다.</li>
</ul>
<blockquote>
<p>예시 : Velog에서 GitHub, Google, Facebook 계정으로 로그인할 수 있는 소셜 로그인 기능
<img src="https://velog.velcdn.com/images/jh_devlog/post/82219720-ccd2-45f2-a5fd-d1e494286d60/image.png" alt="OAuth 예시 이미지 - velog"></p>
</blockquote>
<p><strong>📌 특징</strong></p>
<ul>
<li>토큰 기반 인증/인가 프로토콜이다.</li>
<li>비밀번호 노출 없이 권한을 안전하게 위임할 수 있다.</li>
<li><strong>Scope(권한 범위)</strong>를 통해 애플리케이션이 접근할 수 있는 리소스를 제한할 수 있다.</li>
</ul>
<h4 id="✅-oauth-20">✅ OAuth 2.0</h4>
<ul>
<li>현재 가장 널리 사용되는 표준으로, 이전 버전보다 단순한 구조와 다양한 인증 방식을 지원한다. </li>
<li>Access Token과 Refresh Token 개념을 도입해 더욱 안전하고 유연한 인증/인가를 제공한다.</li>
<li>모바일 앱, 웹 애플리케이션, 서버 간 통신 등 다양한 환경에서 활용된다.</li>
</ul>
<h4 id="📌-oauth-20의-주요-컴포넌트">📌 OAuth 2.0의 주요 컴포넌트</h4>
<ul>
<li>OAuth 2.0은 네 가지 핵심 주체로 이루어져있다.</li>
</ul>
<table>
<thead>
<tr>
<th>구분</th>
<th>역할</th>
<th>설명</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Resource Owner (리소스 소유자)</strong></td>
<td>리소스 소유자</td>
<td>자신의 리소스에 대한 접근을 허용할지 결정</td>
<td>내 구글 계정 정보의 소유자인 &quot;나&quot;</td>
</tr>
<tr>
<td><strong>Client (클라이언트)</strong></td>
<td>제3자 앱</td>
<td>사용자를 대신해 리소스에 접근</td>
<td>Velog, Notion 등</td>
</tr>
<tr>
<td><strong>Authorization Server (인가 서버)</strong></td>
<td>인증/토큰 발급</td>
<td>사용자 인증 후 Access Token 발급</td>
<td>Google Authorization Server</td>
</tr>
<tr>
<td><strong>Resource Server (리소스 서버)</strong></td>
<td>리소스 제공</td>
<td>데이터(API)를 제공하고 Access Token 검증</td>
<td>Google API Server</td>
</tr>
</tbody></table>
<h4 id="🌊-authorization-code-grant-흐름">🌊 Authorization Code Grant 흐름</h4>
<ul>
<li><strong>Authorization Code Grant</strong>은 권한 부여 승인 코드 방식을 뜻하며, OAuth 2.0에서 가장 널리 사용되는 승인 방식이다.</li>
<li>인가 서버에서 발급한 Authorization Code(승인 코드)를 통해 Access Token을 발급받는 방식이다.
<img src="https://velog.velcdn.com/images/jh_devlog/post/81181db0-8231-4f88-99bf-8ef0b7f07b3b/image.svg" alt=""></li>
</ul>
<p><strong>1️⃣ 권한 부여 요청</strong></p>
<ul>
<li>클라이언트는 사용자의 브라우저를 인가 서버로 리다이렉션하여 <code>Client ID</code>, <code>Redirect URI</code> 등을 함께 전달한다.</li>
<li>사용자의 브라우저는 인가 서버에 로그인 페이지를 요청하고, 인가 서버는 로그인 화면을 제공한다.</li>
<li>사용자는 로그인 후 애플리케이션이 요청하는 권한(Scope)에 동의한다.</li>
</ul>
<p><strong>2️⃣ Authorization Code 발급</strong></p>
<ul>
<li>인가 서버는 사용자의 브라우저를 통해 클라이언트의 Redirect URI로 <strong>Authorization Code(승인 코드)</strong>를 전달한다.</li>
</ul>
<p><strong>3️⃣ Access Token 요청</strong></p>
<ul>
<li>클라이언트는 받은 Authorization Code와 함께 자신의 <strong>Client ID/Secret</strong>을 인가 서버로 전달하여 Access Token을 요청한다.</li>
</ul>
<p><strong>4️⃣ Access Token 발급</strong></p>
<ul>
<li>인가 서버는 클라이언트의 정보를 검증한 후 Access Token을 발급한다. (+필요 시 Refresh Token도 함께 발급)</li>
</ul>
<p><strong>5️⃣ 리소스 요청</strong></p>
<ul>
<li>클라이언트는 Access Token을 이용해 리소스 서버에 보호된 리소스를 요청한다.</li>
</ul>
<p><strong>6️⃣ 리소스 제공</strong></p>
<ul>
<li>리소스 서버는 Access Token의 유효성을 검증한 후 클라이언트에게 요청된 리소스를 제공한다.</li>
</ul>
<blockquote>
<p>이 외에도 Implicit Grant, Resource Owner Password Credentials Grant, Client Credentials Grant 등 다양한 승인 방식이 있다.</p>
</blockquote>
<hr>
<h3 id="📄-참고문서">📄 참고문서</h3>
<ul>
<li>코드잇 강의 자료</li>
<li><a href="https://inpa.tistory.com/entry/WEB-%F0%9F%93%9A-OAuth-20-%EA%B0%9C%EB%85%90-%F0%9F%92%AF-%EC%A0%95%EB%A6%AC">OAuth 2.0 개념 - 그림으로 이해하기 쉽게 설명</a></li>
<li><a href="https://charming-kyu.tistory.com/36">OAuth 2.0 개념 총 정리</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SB 3기] 코드잇 초급 프로젝트 회고 : HR Bank]]></title>
            <link>https://velog.io/@jh_devlog/SB-3%EA%B8%B0-%EC%BD%94%EB%93%9C%EC%9E%87-%EC%B4%88%EA%B8%89-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0-HR-Bank</link>
            <guid>https://velog.io/@jh_devlog/SB-3%EA%B8%B0-%EC%BD%94%EB%93%9C%EC%9E%87-%EC%B4%88%EA%B8%89-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%9A%8C%EA%B3%A0-HR-Bank</guid>
            <pubDate>Mon, 07 Jul 2025 10:31:57 GMT</pubDate>
            <description><![CDATA[<h2 id="코드잇-초급-프로젝트-회고--hr-bank">코드잇 초급 프로젝트 회고 : HR Bank</h2>
<p>중급 프로젝트을 앞두고, 코드잇 초급 프로젝트에 대한 회고를 뒤늦게나마 정리해보려 한다..
짧은 기간이었지만, 다양한 기술과 협업을 경험할 수 있었고 새로운 도전으로 가득했던 의미 있는 프로젝트였다. 👍</p>
<h2 id="🗂️-프로젝트-개요">🗂️ 프로젝트 개요</h2>
<ul>
<li>프로젝트명: HR Bank - 기업 인적 자원 관리 서비스</li>
<li>프로젝트 기간: 2025/06/03 - 2025/06/13</li>
<li>목표: 기업의 인사관리 업무를 효율적으로 관리하는 디지털 인사관리 시스템 개발<h3 id="✅-주요-기능">✅ 주요 기능</h3>
</li>
<li>직원 정보 관리 (등록 / 조회 / 수정 / 삭제)</li>
<li>부서 관리 (등록 / 조회 / 수정 / 삭제)</li>
<li>직원 정보 수정 이력 관리</li>
<li>직원 수 기반 통계 대시보드</li>
<li>정기 데이터 백업 및 백업 이력 관리</li>
<li>파일 저장 및 메타데이터 관리</li>
</ul>
<h3 id="👩💻-나의-역할">👩‍💻 나의 역할</h3>
<ul>
<li>기능 개발 : 데이터 백업 관리, 파일 관리</li>
<li>개발 외 역할 : Git 형상관리, 환경 설정 파일 관리 </li>
</ul>
<h3 id="🔗-깃허브-링크">🔗 깃허브 링크</h3>
<ul>
<li><a href="https://github.com/sb3-HRBANK-team4/sb3-hrbank-team4">HRBANK GitHub Repository</a></li>
</ul>
<hr>
<h2 id="📚-개발-환경-및-사용-스택">📚 개발 환경 및 사용 스택</h2>
<pre><code>⚙️ Backend Stack
📦 Framework
├── Spring Boot 3.x          # 메인 프레임워크
├── Spring Data JPA          # ORM 및 데이터 접근
└── Gradle                   # 빌드 도구

🗄️ Database
├── H2 Database             # 개발 환경용 (In-memory)
└── PostgreSQL              # 운영 환경용 (RDBMS)

📚 Documentation
├── Swagger/OpenAPI 3.0 # API 문서 자동화
└── Notion                   # 프로젝트 문서 및 협업 기록

🔧 Development Tools
├── IntelliJ IDEA           # IDE
├── Git &amp; GitHub            # 버전 관리
├── Discord                 # 팀 커뮤니케이션
└── Postman                 # API 테스트**</code></pre><hr>
<h2 id="🛠️-트러블슈팅">🛠️ 트러블슈팅</h2>
<h3 id="1-배치-스케줄러-중복-실행">1) 배치 스케줄러 중복 실행</h3>
<p>*<em>문제 *</em>
배치 데이터 백업 기능이 의도치 않게 짧은 시간 간격으로 중복 실행되었다.</p>
<p><img src="https://lh7-rt.googleusercontent.com/slidesz/AGV_vUdd8ea6EDdxguW-8meNbXVQQeBNS_87Mxw1na5uWXF97Si9-7719L_CR0c77i7dvMG6dMpaUdyiUJZI601apUmZc5Y3hsPQwA0fpXKlb8OmrbkeCZD--JUIpvjm_lrGZ1KUkQMXLXfdMpa9jVwXow=s2048?key=JJ6VqC9kJmfSdk7SCCFLsA" alt="" title="스크린샷 2025-06-12 오후 7.54.48.png"></p>
<p><strong><img src="https://lh7-rt.googleusercontent.com/slidesz/AGV_vUe187ElodgUp-a-NBa1kp9ha3DHdV2zqvESPpuPqm0Bek9cTPSZTiH8ib_kHcR7vrVYOnQsov1EOl5pujpD7zFdDQP86nsQ2uK2IUbDVOyJuKq_X3zsMBeGukjXG4_5lZwY-6GFkSyLXRbGoGv4LA=s2048?key=JJ6VqC9kJmfSdk7SCCFLsA" alt=""></strong><img src="https://lh7-rt.googleusercontent.com/slidesz/AGV_vUda_CspOhzJrHRrDjvyh7TVfMZ2Rvd7EQjvan2ANGbeU2Dai1wrWPDFqLfv-frY0655povdpC3pcM8Zi8Qt0EdJshXkZxsszizmzRu92CfGKCmcnvcobBG1eKVPOuO2yjjdSoLRc0tBX_hIf0usXpE=s2048?key=JJ6VqC9kJmfSdk7SCCFLsA" alt=""></p>
<p><strong>원인</strong>
<code>@Scheduled</code> 메서드가 반복 호출되며, 해당 Bean이 중복 등록되는 것을 확인하였다.
이는 <strong>Spring Boot DevTools의 자동 재시작 기능</strong>이 활성화되어 있어 <code>ApplicationContext</code>가 반복 생성되면서 생긴 문제였다.</p>
<p>*<em>해결 *</em>
DevTools의 자동 재시작 기능을 비활성화 하였다.</p>
<pre><code class="language-yaml">spring:
  devtools:
    restart:
      enabled: false</code></pre>
<blockquote>
<p>당연히 내 코드나 환경 설정에 문제가 있을 것이라 생각했었는데, 프레임워크의 기능으로 인한 문제였다니.. 프레임워크에 대한 이해도 중요하다는 것을 깨달았다.</p>
</blockquote>
<h3 id="2-aop-적용-시-어노테이션-인식-실패">2) AOP 적용 시 어노테이션 인식 실패</h3>
<p><strong>문제</strong>
커스텀 어노테이션 기반으로 AOP를 적용하려 했으나, <code>@target</code>을 사용했을 때 어노테이션 인식에 실패하는 문제가 발생하였다.</p>
<p><strong>원인</strong>
<code>@target</code>은 런타임 객체에 선언된 어노테이션을 기준으로 필터링 하는데, 
Spring AOP는 프록시 기반으로 동작하기 때문에 클래스 레벨의 어노테이션을 제대로 인식하지 못하는 경우가 있다.</p>
<blockquote>
<p>즉, 프록시 객체는 실제 클래스의 어노테이션 정보를 직접 가지지 않기 때문에 <code>@target</code>이 제대로 인식하지 못한다. 
반면, <code>@within</code>은 타입(클래스)에 선언된 어노테이션을 기준으로 하기 때문에, 프록시 환경에서도 안정적으로 동작한다.</p>
</blockquote>
<p>*<em>해결 *</em>
포인트컷 표현식을 <code>@target</code> -&gt; <code>@within</code> 으로 변경하여 클래스에 선언된 어노테이션 기준으로 필터링되도록 하였다.</p>
<pre><code class="language-java">@Around(&quot;@within(com.fource.hrbank.annotation.Logging)&quot;)
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        ...
    }</code></pre>
<h3 id="3-postgresql---jpql-조건절-오류">3) PostgreSQL - JPQL 조건절 오류</h3>
<p>*<em>문제 *</em>
아래와 같은 JPQL 조건절을 사용하였더니 PostgreSQL에서 <code>ould not determine data type</code> 오류가 발생하였다.</p>
<pre><code class="language-sql">SELECT COUNT(e)
FROM Employee e
WHERE (:from IS NULL OR e.hireDate &gt;= :from)</code></pre>
<p>*<em>원인 *</em>
PostgreSQL은 파라미터에 명확한 타입 정보가 없으면 실행이 불가하다. (ex. ? 또는 :param)
그래서 <code>:from IS NULL</code> 구문에서 타입 추론이 불가해 오류가 발생하였던 것이다.</p>
<p>*<em>해결 *</em>
QueryDSL을 도입하여 조건을 Java 코드로 조합하였다.</p>
<pre><code class="language-java">// where 조건 생성을 위한 빌더
BooleanBuilder where = new BooleanBuilder();

// 검색 조건 : 작업자_부분일치, 시작시간_범위조건, 상태_완전일치
if (worker != null) {
    where.and(qBackupLog.worker.contains(worker));
}
if (startedAtFrom != null &amp;&amp; startedAtTo != null) {
    where.and(qBackupLog.startedAt.between(startedAtFrom, startedAtTo));

}
if (status != null) {
    where.and(qBackupLog.status.eq(status));
} </code></pre>
<blockquote>
<p>DBMS마다 SQL 동작 방식에 차이가 있으므로, 조건 분기는 가급적 Java에서 처리하는 것이 더 안정적이다.</p>
</blockquote>
<hr>
<h2 id="💡-프로젝트를-통해-배운-점">💡 프로젝트를 통해 배운 점</h2>
<h3 id="✅-기술적-성과">✅ 기술적 성과</h3>
<ul>
<li>Git을 활용한 형상관리</li>
<li>Spring Scheduler 기반 배치 처리 구현</li>
<li>커서 페이지네이션 로직 이해</li>
<li>커스텀 예외처리 활용</li>
<li>QueryDSL을 활용한 동적 쿼리 작성</li>
<li>JUnit 기반 단위 테스트 작성</li>
</ul>
<h3 id="🤝-비기술-역량">🤝 비기술 역량</h3>
<ul>
<li>팀 Ground Rule 및 컨벤션 규칙 수립</li>
<li>PR 기반 협업 경험</li>
<li>역할 분담 및 일정 관리</li>
</ul>
<hr>
<h2 id="📌-개선-계획">📌 개선 계획</h2>
<ul>
<li>테스트 코드 리팩토링</li>
<li>배치 간격 cron 표현식 사용하여 표현</li>
<li>백업 데이터를 CSV 외에 JSON 또는 Excel(.xlsx) 등 다양한 포맷으로 지원하는 기능 추가</li>
</ul>
<hr>
<h2 id="✍️-마무리">✍️ 마무리</h2>
<p>이번 프로젝트는 첫 팀 프로젝트였기에 더욱 의미가 컸던 것 같다.
단순한 기능 구현뿐만 아니라 설계부터 협업, 형상관리 등 전체 개발 프로세스를 경험해볼 수 있었고, 팀원들과 협업하며 문제를 해결해가는 경험이 힘들기보단 재밌었다. 😁 </p>
<p>아무래도 개발 기간이 짧았다 보니 코드나 테스트 작성의 완성도 측면에서 아쉬움도 있었지만, 이는 이후 리팩토링을 통해 더 나은 결과물로 발전시켜볼 계획이다..!💪</p>
<p>앞으로 진행할 많은 프로젝트에서도 이번 경험을 바탕으로, 한층 더 발전된 모습으로 진행할 수 있기를! 👍</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SB 3기] 코드잇 스프린트 위클리페이퍼 12주차]]></title>
            <link>https://velog.io/@jh_devlog/SB-3%EA%B8%B0-%EC%BD%94%EB%93%9C%EC%9E%87-%EC%8A%A4%ED%94%84%EB%A6%B0%ED%8A%B8-%EC%9C%84%ED%81%B4%EB%A6%AC%ED%8E%98%EC%9D%B4%ED%8D%BC-12%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@jh_devlog/SB-3%EA%B8%B0-%EC%BD%94%EB%93%9C%EC%9E%87-%EC%8A%A4%ED%94%84%EB%A6%B0%ED%8A%B8-%EC%9C%84%ED%81%B4%EB%A6%AC%ED%8E%98%EC%9D%B4%ED%8D%BC-12%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Sun, 06 Jul 2025 13:56:19 GMT</pubDate>
            <description><![CDATA[<h3 id="q-aws-rds를-활용하는-주요-이점과-ec2에-직접-데이터베이스를-설치하여-운영하는-것과-비교했을-때의-차별점에-대해-설명해주세요-그리고-rds를-사용하는-것이-적합하지-않을-수-있는-상황도-함께-언급해주세요">Q. AWS RDS를 활용하는 주요 이점과 EC2에 직접 데이터베이스를 설치하여 운영하는 것과 비교했을 때의 차별점에 대해 설명해주세요. 그리고 RDS를 사용하는 것이 적합하지 않을 수 있는 상황도 함께 언급해주세요.</h3>
<h4 id="✅-aws-rds란">✅ AWS RDS란?</h4>
<ul>
<li>AWS에서 제공하는 관계형 데이터베이스를 손쉽게 관리할 수 있도록 지원하는 서비스이다.</li>
<li>MySQL, PostgreSQL, Oracle, SQL Server 등 다양한 데이터베이스 엔진을 지원하며, 설정, 운영, 확장 등 복잡한 작업을 자동화하여 사용자 편의성을 높여준다.</li>
</ul>
<h4 id="👍-rds-활용시의-이점">👍 RDS 활용시의 이점</h4>
<ol>
<li><strong>운영 부담 감소</strong> : 
서버 설치, 패치, 정기 백업 등 복잡한 관리 작업을 AWS가 자동으로 처리한다.</li>
<li><strong>간편한 백업과 복구</strong> : 
몇 번의 클릭만으로 자동 백업을 설정할 수 있으며, 특정 시점으로 복구도 가능하다.</li>
<li><strong>뛰어난 고가용성 및 안정</strong>성 : 
Multi-AZ(다중 가용 영역) 구성을 통해 장애 발생 시 예비 인스턴스로 자동 전환되어 서비스 중단을 최소화 할 수 있다.</li>
<li><strong>유연한 성능 확장</strong> : 
트래픽 증가나 데이터량 증가에 따라 스토리지 용량을 늘리거나, 읽기 전용 복제본을 추가하는 등 손쉽게 확장할 수 있다.</li>
<li><strong>강화된 보안 설정</strong> : 
VPC(Virtual Private Cloud) 내에 DB를 배치하거나, IAM(Identity and Access Management)을 활용해 접근 권한을 제어하는 등 보안 설정을 쉽게 강화할 수 있다.</li>
</ol>
<h4 id="🆚-ec2에-직접-db를-설치하는-경우과의-차이점">🆚 EC2에 직접 DB를 설치하는 경우과의 차이점</h4>
<table>
<thead>
<tr>
<th>항목</th>
<th>RDS 사용</th>
<th>EC2에 DB 직접 설치</th>
</tr>
</thead>
<tbody><tr>
<td>설치 및 설정</td>
<td>AWS에서 자동으로 처리</td>
<td>사용자가 직접 설치 및 설정 필요</td>
</tr>
<tr>
<td>유지보수</td>
<td>자동 백업, 복구, 패치 지원</td>
<td>모두 수동으로 처리</td>
</tr>
<tr>
<td>장애 대응</td>
<td>자동 페일오버 지원 (Multi-AZ)</td>
<td>수동 복구 또는 별도 이중화 구성 필요</td>
</tr>
<tr>
<td>보안 설정</td>
<td>IAM, VPC등으로 손쉽게 설정</td>
<td>직접 구성해야 함 (복잡한 설정)</td>
</tr>
<tr>
<td>시스템 제어 범위</td>
<td>제한적 (커널 파라미터 등은 변경 불가)</td>
<td>전체 시스템 제어 가능</td>
</tr>
<tr>
<td>비용</td>
<td>라이센스  및 관리 비용 포함</td>
<td>사용한 EC2, 스토리지 자원에 대한 비용만 발생</td>
</tr>
</tbody></table>
<h4 id="⚠️-rds가-적합하지-않은-상황">⚠️ RDS가 적합하지 않은 상황</h4>
<ul>
<li><p><strong>세밀한 시스템 설정이 필요한 경우</strong> :
RDS는 일부 시스템 설정(ex. 커널 파라미터, OS 세팅 등)이 제한되므로, 설정을 최적화해야 하는 특수한 경우에는 적합하지 않을 수 있다.</p>
</li>
<li><p><strong>소규모 프로젝트나 간단한 테스트 환경</strong> : 
RDS는 최소 스펙이 정해져 있어, 소규모 프로젝트나 테스트 환경에는 과한 선택일 수 있다. 이럴 때는 EC2에 직접 설치하거나 로컬 환경을 활용하는 것이 더 효율적이다.</p>
</li>
</ul>
<hr>
<h3 id="q-github-actions-워크플로우에서-사용할-수-있는-다양한-트리거trigger-유형을-설명하고-각-트리거-유형이-적합한-cicd-시나리오에-대해-설명하세요">Q. GitHub Actions 워크플로우에서 사용할 수 있는 다양한 트리거(Trigger) 유형을 설명하고, 각 트리거 유형이 적합한 CI/CD 시나리오에 대해 설명하세요.</h3>
<h4 id="✅-github-actions란">✅ GitHub Actions란?</h4>
<ul>
<li>GitHub 저장소 내에서 코드 빌드, 테스트, 배포 등의 작업을 자동화 할 수 있는 CI/CD 플랫폼이다. </li>
<li>각 작업은 <code>workflow</code>라는 단위로 정의하며, 이는 특정 이벤트(Trigger)가 발생했을 때 실행된다.</li>
</ul>
<h4 id="📌-다양한-trigger-유형과-활용-시나리오">📌 다양한 Trigger 유형과 활용 시나리오</h4>
<ol>
<li><code>push</code> : 특정 브랜치에 새로운 커밋이 푸시될 때 실행</li>
</ol>
<ul>
<li>활용 시나리오 : <code>main</code> 브랜치에 코드가 푸시될 때마다 자동으로 단위 테스트 실행<pre><code class="language-yaml">on:
push:
  branches: [ main ]</code></pre>
</li>
</ul>
<ol start="2">
<li><code>pull_request</code> : Pull Request(PR)가 생성되거나 커밋으로 업데이트 될 때 실행</li>
</ol>
<ul>
<li>활용 시나리오 : PR 병합 전에 통합 테스트나 코드 스타일 검사를 자동으로 수행하여 코드 안정성을 검증<pre><code class="language-yaml">on:
pull_request:
 branches: [ main ]</code></pre>
</li>
</ul>
<ol start="3">
<li><code>schedule</code> : 정해진 시간마다 주기적으로 실행 (cron 표현식 사용)</li>
</ol>
<ul>
<li>활용 시나리오 : 매일 자정에 데이터를 백업하거나, 로그를 정리<pre><code class="language-yaml">on:
schedule:
  - cron: &#39;0 0 * * *&#39;</code></pre>
</li>
</ul>
<ol start="4">
<li><code>workflow_dispatch</code> : GitHub 웹 UI에서 사용자가 수동으로 실행</li>
</ol>
<ul>
<li>활용 시나리오 : 운영자가 직접 배포 버튼을 눌러 실행할 경우<pre><code class="language-yaml">on:
workflow_dispatch:</code></pre>
</li>
</ul>
<ol start="5">
<li><code>release</code> : GitHub Release가 생성, 수정, 삭제될 때 실행</li>
</ol>
<ul>
<li>활용 시나리오 : 릴리스 생성 시 배포 파이프라인 자동 실행 또는 릴리스 노트 생성<pre><code class="language-yaml">on:
release:
  types: [created]
</code></pre>
</li>
</ul>
<p>```</p>
<hr>
<h3 id="📄-참고-문서">📄 참고 문서</h3>
<ul>
<li><a href="https://docs.aws.amazon.com/ko_kr/AmazonRDS/latest/UserGuide/Welcome.html">Amazon Relational Database Service(Amazon RDS)란 무엇입니까?</a></li>
<li>코드잇 강의 교안</li>
<li><a href="https://docs.github.com/ko/actions/using-workflows/triggering-a-workflow">GitHub Actions : 워크플로 트리거</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SB 3기] 코드잇 스프린트 위클리페이퍼 11주차]]></title>
            <link>https://velog.io/@jh_devlog/SB-3%EA%B8%B0-%EC%BD%94%EB%93%9C%EC%9E%87-%EC%8A%A4%ED%94%84%EB%A6%B0%ED%8A%B8-%EC%9C%84%ED%81%B4%EB%A6%AC%ED%8E%98%EC%9D%B4%ED%8D%BC-11%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@jh_devlog/SB-3%EA%B8%B0-%EC%BD%94%EB%93%9C%EC%9E%87-%EC%8A%A4%ED%94%84%EB%A6%B0%ED%8A%B8-%EC%9C%84%ED%81%B4%EB%A6%AC%ED%8E%98%EC%9D%B4%ED%8D%BC-11%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Sun, 29 Jun 2025 17:58:35 GMT</pubDate>
            <description><![CDATA[<h3 id="q-컨테이너-기술과-docker를-명확히-구분하여-설명하세요-컨테이너-기술이-docker-이전에도-존재했던-개념임을-언급하고-docker가-컨테이너-기술을-구현한-하나의-도구라는-관점에서-설명해주세요-또한-docker-외에-컨테이너-기술을-구현한-다른-도구의-예시를-들어보세요">Q. 컨테이너 기술과 Docker를 명확히 구분하여 설명하세요. 컨테이너 기술이 Docker 이전에도 존재했던 개념임을 언급하고, Docker가 컨테이너 기술을 구현한 하나의 도구라는 관점에서 설명해주세요. 또한, Docker 외에 컨테이너 기술을 구현한 다른 도구의 예시를 들어보세요.</h3>
<h4 id="✅-컨테이너-기술이란">✅ 컨테이너 기술이란?</h4>
<p><strong>컨테이너(Container)</strong>는 애플리케이션과 그 실행 환경(라이브러리, 설정 파일 등)을 함께 패키징하여, 격리된 공간에서 실행할 수 있도록 해주는 가상화 기술이다.
이는 호스트 운영체제의 커널을 공유하며, 독립된 프로세스로 실행된다. </p>
<blockquote>
<p>이 기술은 Docker가 등장하기 이전부터 존재했으며, 핵심 기반 기술로는 Linux의 <code>chroot</code>, <code>cgroups</code>, <code>namespaces</code> 등이 있다. </p>
</blockquote>
<h4 id="👍-컨테이너-기술의-주요-장점">👍 컨테이너 기술의 주요 장점</h4>
<ul>
<li><strong>가볍고 빠른 속도</strong> : 가상머신과 달리 별도의 OS를 포함하지 않아 리소스 사용량이 적고, 실행 속도가 빠르다.</li>
<li><strong>환경 일관성</strong> : 어떤 환경에서도 동일하게 실행된다.</li>
<li><strong>배포 자동화</strong> : CI/CD 파이프라인과의 연동을 통해 컨테이너 단위로 손쉽게 배포 및 운영 자동화가 가능하다.</li>
<li><strong>효율적인 자원 활용</strong> : 여러 컨테이너가 하나의 OS 커널을 공유하므로 서버 자원을 보다 효율적으로 활용할 수 있다.</li>
</ul>
<h4 id="✅-docker란">✅ Docker란?</h4>
<p>컨테이너 기술을 쉽고 효율적으로 사용할 수 있도록 만든 오픈소스 플랫폼이다.</p>
<p><strong>📌 Docker의 특징</strong></p>
<ul>
<li><strong>이미지 기반 배포 방식</strong> : <code>Dockerfile</code>을 통해 컨테이너 이미지를 정의하고 재사용할 수 있다.</li>
<li><strong>중앙 저장소 제공</strong> : <code>Docker Hub</code>를 통해 이미지를 공유하고 관리할 수 있다.</li>
<li><strong>간편한 CLI 명령어</strong> : <code>docker run</code>, <code>docker build</code> 등의 직관적인 명령어로 컨테이너 조작이 가능하다.</li>
</ul>
<p>👉 Docker는 컨테이너 기술 그 자체가 아니라, 해당 기술을 쉽게 사용할 수 있게 만든 구현 도구 중 하나이다.</p>
<h4 id="✅-docker-외에-컨테이너-기술을-구현한-도구-예시">✅ Docker 외에 컨테이너 기술을 구현한 도구 예시</h4>
<ul>
<li>Podman : 데몬 없이 컨테이너를 실행할 수 있는 Docker 호환 CLI 도구</li>
<li>LXC (Linux Containers) : Docker 이전에 사용된 리눅스 기반의 전통적인 컨테이너 기술</li>
<li>rkt (Rocket) : CoreOS에서 개발한 컨테이너 런타임으로 보안성과 유연성을 강화했으나 현재는 개발이 중단된 상태</li>
<li>containerd : CNCF가 관리하는 범용 컨테이너 런타임으로, Docker와 Kubernetes 모두에서 사용 가능  </li>
<li>CRI-O : Kubernetes 전용 컨테이너 런타임으로, OCI(Open Container Initiative) 표준을 따름</li>
</ul>
<hr>
<h3 id="q-컨테이너-오케스트레이션의-개념과-필요성을-설명하고-docker-단독-사용-환경과-비교하여-컨테이너-오케스트레이션이-해결하는-주요-문제점-3가지자동-확장-자가-복구-선언적-인프라를-설명하세요">Q. 컨테이너 오케스트레이션의 개념과 필요성을 설명하고, Docker 단독 사용 환경과 비교하여 컨테이너 오케스트레이션이 해결하는 주요 문제점 3가지(자동 확장, 자가 복구, 선언적 인프라)를 설명하세요.</h3>
<h4 id="✅-컨테이너-오케스트레이션이란">✅ 컨테이너 오케스트레이션이란?</h4>
<p>여러 컨테이너의 배포, 관리, 확장, 네트워킹, 로드 밸런싱 등을 자동화하는 기술이다.
이는 단일 서버에서 여러 개의 컨테이너를 수동으로 관리해야 하는 Docker의 한계를 극복하고, 대규모 컨테이너 기반 인프라를 안정적이고 효율적으로 운영하기 위해 필요하다.</p>
<h4 id="🧐-컨테이너-오케스트레이션이-해결하는-주요-문제점-3가지-🆚-docker">🧐 컨테이너 오케스트레이션이 해결하는 주요 문제점 3가지 (🆚 Docker)</h4>
<p><strong>1. 자동 확장</strong>
Docker 환경에서는 트래픽 증가 시 컨테이너 수를 수동으로 조절해야 하지만, 오케스트레이션 도구는 <strong>CPU, 메모리 등의 리소스 사용률</strong>을 기준으로 컨테이너를 <strong>자동으로 확장하거나 축소</strong>할 수 있어 서비스 탄력성과 안정성을 확보할 수 있다.</p>
<p><strong>2. 자가 복구</strong>
Docker는 컨테이너가 비정상적으로 종료되면 자동으로 복구하지 않는다.
반면, 오케스트레이션 도구는 <strong>장애가 발생한 컨테이너를 자동으로 재시작하거나 다른 노드에 재배포</strong>하여 서비스의 안정성과 가용성을 유지할 수 있다.</p>
<p><strong>3. 선언적 인프라</strong>
Docker는 실행 명령어나 Docker Compose 파일을 통해 현재 상태를 <strong>직접 관리</strong>해야 한다.
반면, 오케스트레이션 도구는 <strong>선언적 설정 파일(YAML 등)</strong>을 통해 원하는 상태를 정의하면, 시스템이 자동으로 그 상태를 유지하도록 관리한다.
이를 통해 일관된 인프라 구성 및 운영 자동화가 가능하다.</p>
<blockquote>
<p>📌 대표적인 컨테이너 오케스트레이션 도구로는 Kubernetes, Docker Swarm, Amazon ECS 등이 있으며, 이 중 Kubernetes가 가장 널리 사용된다.</p>
</blockquote>
<hr>
<h3 id="📄-참고-문서">📄 참고 문서</h3>
<ul>
<li>코드잇 강의 교안</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SB 3기] 코드잇 스프린트 위클리페이퍼 10주차]]></title>
            <link>https://velog.io/@jh_devlog/SB-3%EA%B8%B0-%EC%BD%94%EB%93%9C%EC%9E%87-%EC%8A%A4%ED%94%84%EB%A6%B0%ED%8A%B8-%EC%9C%84%ED%81%B4%EB%A6%AC%ED%8E%98%EC%9D%B4%ED%8D%BC-10%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@jh_devlog/SB-3%EA%B8%B0-%EC%BD%94%EB%93%9C%EC%9E%87-%EC%8A%A4%ED%94%84%EB%A6%B0%ED%8A%B8-%EC%9C%84%ED%81%B4%EB%A6%AC%ED%8E%98%EC%9D%B4%ED%8D%BC-10%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Sun, 22 Jun 2025 13:50:20 GMT</pubDate>
            <description><![CDATA[<h3 id="q-애플리케이션의-각-계층에서-수행되는-입력값-검증의-범위와-책임을-어떻게-나눌-것인지에-대해-설명해주세요-특히-중복-검증을-피하면서도-안정성을-확보하는-방안과-이와-관련된-트레이드오프에-대해-설명해주세요">Q. 애플리케이션의 각 계층에서 수행되는 입력값 검증의 범위와 책임을 어떻게 나눌 것인지에 대해 설명해주세요. 특히 중복 검증을 피하면서도 안정성을 확보하는 방안과, 이와 관련된 트레이드오프에 대해 설명해주세요.</h3>
<h4 id="✅--각-계층의-입력값-검증-범위와-책임">✅  각 계층의 입력값 검증 범위와 책임</h4>
<h4 id="1-presentation-layer-controller">1. Presentation Layer (Controller)</h4>
<ul>
<li><p>사용자의 입력값이 <strong>기본적인 형식 요건</strong>을 충족하는지 검증하는 <strong>1차 방어선</strong>이다.</p>
</li>
<li><p>ex. 문자열 길이 제한, 이메일 형식, null 허용 여부 등</p>
</li>
<li><p>일반적으로 <code>@Valid</code>, <code>@NotNull</code>, <code>@Size</code>, <code>@Email</code> 등 <strong>Bean Validation</strong> 을 사용하여 DTO 필드의 형식을 검증한다.</p>
</li>
</ul>
<pre><code class="language-java">    @PatchMapping(path = &quot;/{messageId}&quot;)
    @Override
    public ResponseEntity&lt;MessageDto&gt; update(
            @PathVariable UUID messageId,
            @Valid @RequestBody MessageUpdateRequest messageUpdateRequest
    ) {
        ...
    }</code></pre>
<blockquote>
<p>💡 <strong>Bean Validation</strong>이란?
어노테이션을 통해 객체의 필드에 제약 조건을 <strong>선언적으로 정의</strong>할 수 있는 검증 API이다.
이를 사용하면 검증 로직이 명확해지고, 코드의 가독성과 유지보수성을 높일 수 있다.
또한, <strong>표준화된 방식</strong>으로 검증을 수행하여 일관된 검증을 구현할 수 있다.</p>
</blockquote>
<h4 id="2-business-layer-service">2. Business Layer (Service)</h4>
<ul>
<li><p>비즈니스 규칙에 따른 유효성을 검증한다.</p>
</li>
<li><p>ex. 이메일 중복 여부, 사용자 권한 검증, 상태값 검증 등</p>
</li>
<li><p>검증 실패 시 적절한 예외를 발생시켜 도메인 계층에 잘못된 요청이 전달되지 않도록 한다.</p>
</li>
</ul>
<pre><code class="language-java">if (userRepository.existsByEmail(email)) {
    log.warn(&quot;유저 생성 실패: 이미 존재하는 이메일&quot;);
    throw UserAlreadyExistsException.byEmail(email);
}</code></pre>
<h4 id="3-domain-layer-entity">3. Domain Layer (Entity)</h4>
<ul>
<li><p>객체 자체가 지켜야 할 <strong>불변 조건</strong>을 검증한다.</p>
</li>
<li><p>잘못된 상태의 객체 생성을 방지하기 위해, 주로 생성자나 정적 팩토리 메서드 내부에서 검증을 수행한다.</p>
</li>
<li><p>ex. 이메일은 null일 수 없음, 상태는 enum 범위 내여야 함 등</p>
</li>
</ul>
<pre><code class="language-java">public User(String email, String name) {
    if (email == null || email.isBlank()) {
        throw new IllegalArgumentException(&quot;이메일은 필수입니다.&quot;);
    }
    this.email = email;
    this.name = name;
}</code></pre>
<blockquote>
<p>📌 <strong>왜 도메인(Entity)에서도 유효성 검증이 필요한가?</strong>
<code>@Valid</code>를 활용한 DTO 검증과 도메인 내부의 검증이 유사하게 느껴져 &#39;중복 검사 아닌가?&#39; 하는 의문이 들 수 있다.
하지만 이는 <strong>컨트롤러를 거치지 않고 객체가 직접 생성되거나 조작되는 경우</strong>에도 유효성을 보장하기 위한 안전 장치다. 즉, 생성 시점의 불변 조건 검증은 도메인의 책임으로, 이는 중복이 아닌 <strong>계층적 책임 분리</strong>로 이해해야 한다.</p>
</blockquote>
<h4 id="4-persistence-layer-repository-db">4. Persistence Layer (Repository, DB)</h4>
<ul>
<li><p>데이터베이스 제약 조건을 통해 <strong>데이터 무결성</strong>을 보장하는 <strong>최종 방어선</strong> 역할을 한다.</p>
</li>
<li><p>앞 계층에서 모든 유효성 검증을 수행했더라도, <strong>동시성 이슈나 예외 상황</strong> 등으로 인해 잘못된 데이터가 저장될 수 있다.</p>
</li>
<li><p>애플리케이션 코드 외부에서 발생하는 <strong>비정상 입력</strong>도 막을 수 있어야 한다.</p>
</li>
<li><p>주요 제약 조건:</p>
<ul>
<li><code>UNIQUE</code>: 중복 방지</li>
<li><code>FOREIGN KEY</code>: 참조 무결성 보장</li>
<li><code>NOT NULL</code>: 필수 컬럼 누락 방지</li>
<li><code>CHECK</code>: 값의 범위나 조건 제약</li>
</ul>
</li>
</ul>
<h4 id="📌-계층별-검증-역할-요약">📌 계층별 검증 역할 요약</h4>
<table>
<thead>
<tr>
<th>계층</th>
<th>검증 목적</th>
<th>예시 및 기술 방식</th>
</tr>
</thead>
<tbody><tr>
<td>Presentation (Controller, DTO)</td>
<td>사용자 입력 형식 검증</td>
<td><code>@Valid</code>, <code>@NotBlank</code>, <code>@Email</code> 등</td>
</tr>
<tr>
<td>Business (Service)</td>
<td>비지니스 규칙 검증 (중복, 상태 등)</td>
<td>이메일 중복, 권한 체크</td>
</tr>
<tr>
<td>Domain (Entity)</td>
<td>객체의 불변 조건 보장</td>
<td>생성자/팩토리 메서드에서 null, enum 등 검증</td>
</tr>
<tr>
<td>Persistence (DB)</td>
<td>데이터 무결성 보장</td>
<td><code>UNIQUE</code>, <code>NOT NULL</code>, <code>FOREIGN KEY</code>, <code>CHECK</code> 등</td>
</tr>
</tbody></table>
<hr>
<h4 id="✅-중복-검증이란">✅ 중복 검증이란?</h4>
<ul>
<li><p>동일한 검증 로직(ex. 이메일 형식 체크, 중복 여부 등)이 여러 계층에서 반복되는 현상이다.</p>
</li>
<li><p>이는 코드 중복, 유지보수 비용 증가, 버그 유발 가능성으로 인해 지양해야 한다.</p>
</li>
</ul>
<h4 id="💡-중복-검증을-피하면서-안정성-확보하는-방안">💡 중복 검증을 피하면서 안정성 확보하는 방안</h4>
<ul>
<li><p><strong>각 계층의 책임</strong>에 따라 검증 로직을 명확히 분리한다. </p>
</li>
<li><p>단, <strong>예외 상황이나 동시성 이슈</strong>에 대비해 일부 검증은 <strong>의도적인 중첩 방어 전략</strong>으로 허용되어야 한다. </p>
</li>
</ul>
<blockquote>
<p>💡 <strong>의도적인 중첩 방어</strong>가 필요한 경우
ex. 이메일 중복 
-&gt; Service 단에서 중복 체크 + DB에서 <code>UNIQUE</code> 제약으로 최종 무결성 보장
-&gt; 이는 단순한 중복이 아니라, <strong>시스템 신뢰성과 무결성을 확보하기 위한 방어적 설계</strong>로 볼 수 있다.</p>
</blockquote>
<h4 id="🔄-트레이드오프">🔄 트레이드오프</h4>
<p>👍 <strong>장점</strong></p>
<ul>
<li><p><strong>계층 간 책임 분리</strong>로 인해 유지보수가 쉬워지고 테스트가 용이하다.</p>
</li>
<li><p>검증 로직의 <strong>일관성, 재사용성, 가독성</strong>이 높아진다.</p>
</li>
<li><p>중복을 줄여 코드 품질이 향상된다.</p>
</li>
</ul>
<p>👎 <strong>단점</strong></p>
<ul>
<li><p>계층간 책임이 명확하지 않으면 <strong>책임 누락/중복 가능성</strong>이 생길 수 있다.</p>
</li>
<li><p>예외 상황에 대한 방어력이 낮아질 수 있다. (ex. 동시성, 외부 API 등)</p>
</li>
</ul>
<p>➡️ 중복 검증이 불가피한 경우, <strong>&quot;의도적인 중첩 방어&quot;</strong>임을 명시하고, 주석이나 문서를 통해 그 목적을 기록해두어야 한다.</p>
<blockquote>
<p>📌 <strong>트레이드 오프</strong>란?
어떤 것을 얻기 위해 다른 것을 포기해야 하는 상황을 말하며, 현실적인 선택을 위해서는 선택 간의 상충 관계를 명확히 파악하고 우선순위를 고려해야 한다.</p>
</blockquote>
<hr>
<h3 id="q-테스트에서-사용되는-mockito의-mock-stub-spy-개념을-각각-설명하고-어떤-상황에서-어떤-방식을-선택해야-하는지-구체적인-예시와-함께-설명하세요">Q. 테스트에서 사용되는 Mockito의 Mock, Stub, Spy 개념을 각각 설명하고, 어떤 상황에서 어떤 방식을 선택해야 하는지 구체적인 예시와 함께 설명하세요.</h3>
<h4 id="✅-mockito란">✅ Mockito란?</h4>
<ul>
<li><strong>의존 객체를 가짜(Mock)로 대체</strong>하여 <strong>격리된 환경에서 테스트</strong>할 수 있도록 지원하는 Java 기반 테스트 프레임워크이다.</li>
</ul>
<hr>
<h4 id="1-mock">1. Mock</h4>
<ul>
<li>실제 객체를 <strong>완전히 대체하는 모의 객체</strong>로, 원하는 행위를 사전에 설정할 수 있다.</li>
<li>내부 로직은 실행되지 않으며 메서드 호출 여부, 횟수 등의 행위 자체를 검증하는 데 중점을 둔다.</li>
</ul>
<pre><code class="language-java">@Test
void 파일업로드_호출확인() {
    // given (Mock 객체 생성)
    FileUploader mockUploader = Mockito.mock(FileUploader.class);

    // when
    mockUploader.upload(&quot;simple.txt&quot;);

    // then
    verify(mockUploader).upload(&quot;simple.txt&quot;); // 호출 검증
}</code></pre>
<h4 id="💡-언제-사용하면-좋을까">💡 언제 사용하면 좋을까?</h4>
<ul>
<li>내부 로직 대신 <strong>행위 기반 테스트</strong>가 필요한 경우</li>
<li>외부 시스템 호출 여부 확인 (ex.이메일 전송)</li>
</ul>
<hr>
<h4 id="2-stub">2. Stub</h4>
<ul>
<li>특정 메서드 호출에 대하여 예측 가능한 결과값을 미리 설정하는 것이다.</li>
<li>주로 <code>Mock + when().thenReturn()</code> 형태로 사용된다.</li>
</ul>
<pre><code class="language-java">
@Test
void 파일업로드_성공() {
    // given (Mock 객체의 동작 정의)
    FileUploader stubUploader = Mockito.mock(FileUploader.class);
    when(stubUploader.upload(anyString())).thenReturn(true);

    // when
    boolean result = stubUploader.upload(&quot;simple.txt&quot;);

    // then
    assertTrue(result);
}</code></pre>
<h4 id="💡-언제-사용하면-좋을까-1">💡 언제 사용하면 좋을까?</h4>
<ul>
<li>테스트 대상 로직 외의 의존 객체 동작을 단순화 하고 싶을 때 (ex. 외부 API)</li>
<li>테스트 결과 제어가 필요할 때 (예외 상황 유도 등)</li>
</ul>
<hr>
<h4 id="3-spy">3. Spy</h4>
<ul>
<li>실제 객체를 감싸서 사용하는 <strong>부분 모의 객체</strong>다.</li>
<li>대부분의 메서드는 실제 동작을 유지하고, 일부 메서드만 내가 원하는 대로 조작할 수 있다.</li>
<li><strong>상태 기반 테스트</strong>와 <strong>행위 검증</strong>을 동시에 할 수 있다.</li>
</ul>
<pre><code class="language-java">
@Test
void 파일업로드_일부상황만_조작() {
    // given (실제/Spy 객체 생성)
    FileUploader realUploader = new FileUploader();
    FileUploader spyUploader = Mockito.spy(realUploader);

    // 메서드 동작 정의 (특정 상황만 업로드 실패)
    doReturn(false).when(spyUploader).upload(&quot;bad/file.txt&quot;);

    // when
    boolean normal = spyUploader.upload(&quot;good/file.txt&quot;); // 실제 동작
    boolean blocked = spyUploader.upload(&quot;bad/file.txt&quot;); // 가짜 동작

    // then
    assertTrue(normal);   // 실제 업로드
    assertFalse(blocked); // 강제로 실패 처리
    verify(spyUploader).upload(&quot;bad/file.txt&quot;);
}</code></pre>
<h4 id="💡-언제-사용하면-좋을까-2">💡 언제 사용하면 좋을까?</h4>
<ul>
<li>실제 객체 기반 테스트를 하되, 특정 상황만 제어하고 싶은 경우</li>
<li>상태 검증과 행위 검증을 동시에 하고 싶을 때</li>
</ul>
<blockquote>
<p>⚠️ 단, Spy는 실제 객체의 상태에 의존하므로, 복잡한 로직 테스트에서는 주의가 필요하다.</p>
</blockquote>
<hr>
<h4 id="📌-mock-stub-spy-차이점">📌 Mock, Stub, Spy 차이점</h4>
<ul>
<li><p>Mock : 호출 유무와 횟수 등 <strong>행위 자체를 검증</strong></p>
<ul>
<li>ex. <code>upload()</code> 메서드를 누가 호출했는지 알고 싶어</li>
</ul>
</li>
<li><p>Stub : 특정 입력에 대해 <strong>고정된 응답만 반환</strong></p>
<ul>
<li>ex. <code>upload()</code> 메서드를 호출하면 항상 &quot;true&quot;를 반환해줘</li>
</ul>
</li>
<li><p>Spy : 실제 객체처럼 동작하지만, <strong>일부 동작만 조작 가능</strong></p>
<ul>
<li>ex. <code>bad.txt</code> 파일을 업로드 할때만 &quot;false&quot;를 반환해줘</li>
</ul>
</li>
</ul>
<table>
<thead>
<tr>
<th>구분</th>
<th>객체 생성 방식</th>
<th>실제 동작 여부</th>
<th>주로 사용하는 목적</th>
</tr>
</thead>
<tbody><tr>
<td>Mock</td>
<td><code>mock()</code></td>
<td>❌ 실행 안 됨</td>
<td>호출 여부 검증, 외부 API 대체</td>
</tr>
<tr>
<td>Stub</td>
<td><code>mock()</code> + <code>when().thenReturn()</code></td>
<td>❌ 실행 안 됨</td>
<td>특정 입력 → 고정된 반환값 지정</td>
</tr>
<tr>
<td>Spy</td>
<td><code>spy()</code></td>
<td>⭕ 실제 동작 (일부 ❌)</td>
<td>실제 동작 유지 + 일부 동작만 조작 가능</td>
</tr>
</tbody></table>
<hr>
<h3 id="📄-참고-문서">📄 참고 문서</h3>
<ul>
<li>코드잇 강의 교안</li>
<li><a href="https://velog.io/@jihwankim128/%EA%B2%80%EC%A6%9D%EC%9D%80-%EC%96%B4%EB%94%94%EC%84%9C-DTO-vs-Service-vs-Domain">계층 별 검증은 어떻게 하는가?</a></li>
<li><a href="https://adjh54.tistory.com/346">[Java] Spring Boot Mockito 이해하기 : 테스트 흐름 및 사용예시</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Boot 모니터링 : Grafana와 Prometheus를 이용한 시각화 ]]></title>
            <link>https://velog.io/@jh_devlog/Spring-Boot-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-Grafana%EC%99%80-Prometheus%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%8B%9C%EA%B0%81%ED%99%94</link>
            <guid>https://velog.io/@jh_devlog/Spring-Boot-%EB%AA%A8%EB%8B%88%ED%84%B0%EB%A7%81-Grafana%EC%99%80-Prometheus%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%8B%9C%EA%B0%81%ED%99%94</guid>
            <pubDate>Tue, 17 Jun 2025 11:45:00 GMT</pubDate>
            <description><![CDATA[<h3 id="💡-개념-정리">💡 개념 정리</h3>
<h4 id="✅-prometheus란">✅ Prometheus란?</h4>
<ul>
<li>대상 시스템에서 다양한 <strong>모니터링 지표(metrics)</strong>를 수집하고 저장하며, 쿼리를 통해 검색하고 시각화 할 수 있는 오픈소스 모니터링 시스템이다.</li>
</ul>
<h4 id="✅-grafana란">✅ Grafana란?</h4>
<ul>
<li>Prometheus와 같은 모니터링 데이터 소스를 시각화해주는 대시보드 기반의 시각화 도구로 지표를 그래프, 수치 등 다양한 방식으로 표현할 수 있다.</li>
</ul>
<hr>
<h3 id="👀-시각화-흐름도">👀 시각화 흐름도</h3>
<p><img src="https://velog.velcdn.com/images/jh_devlog/post/bd8b5082-91bd-4432-80e3-d3b696fab318/image.png" alt=""></p>
<ul>
<li>사용자는 Grafana를 통해 대시보드를 구성하고 조회한다.</li>
<li>Grafana는 설정된 데이터 소스인 Prometheus에 쿼리를 전송하여 필요한 메트릭 데이터를 요청한다.</li>
<li>Prometheus는 Spring Boot 애플리케이션으로부터 메트릭을 수집하여 저장하고, Grafana의 요청에 따라 응답한다.</li>
</ul>
<hr>
<h3 id="⚒️-prometheus-및-grafana-설치">⚒️ Prometheus 및 Grafana 설치</h3>
<h4 id="prometheus-설치">Prometheus 설치</h4>
<ul>
<li>Mac (Homebrew)<pre><code>brew install prometheus
brew services start prometheus
</code></pre></li>
</ul>
<p>brew services list    // 현재 실행 상태 확인(started)</p>
<pre><code>
- Windows
[Prometheus 다운로드 페이지] (https://prometheus.io/download/)에서 다운로드한다.

---

#### Grafana 설치
- Mac (Homebrew)</code></pre><p>brew install grafana
brew services start grafana</p>
<pre><code>
- Windows
[Grafana 다운로드 페이지](https://grafana.com/grafana/download?pg=get&amp;plcmt=selfmanaged-box1-cta1&amp;platform=windows)에서 다운로드한다.

---
### 📊 시각화 구성 단계

#### 1. Spring Boot 의존성 추가
- `build.gradle`에 다음 의존성을 추가한다.
```java
// actuator
implementation &#39;org.springframework.boot:spring-boot-starter-actuator&#39;
// prometheus
implementation &#39;io.micrometer:micrometer-registry-prometheus&#39;</code></pre><h4 id="2-actuator-엔드포인트-설정">2. Actuator 엔드포인트 설정</h4>
<ul>
<li><code>application.yaml</code>에 Prometheus 엔드포인트를 노출하도록 설정을 추가한다.</li>
</ul>
<pre><code class="language-yaml">management:
  endpoints:
    web:
      exposure:
        include: prometheus  # prometheus 엔드포인트 노출
      base-path: /actuator    # 기본 경로 (default: /actuator)</code></pre>
<blockquote>
<p>설정 후, <code>http://localhost:8080/actuator/prometheus</code>로 접속
<img src="https://velog.velcdn.com/images/jh_devlog/post/df7bce1e-6ed7-42f4-a07d-ccc39cfc6329/image.png" alt=""></p>
</blockquote>
<h4 id="3-prometheus-설정-파일-구성">3. Prometheus 설정 파일 구성</h4>
<ul>
<li><code>prometheus.yml</code> 파일을 수정 후 저장한다. (Mac 기준)<pre><code>open /opt/homebrew/etc/prometheus.yml    // 파일 열기</code></pre></li>
<li><code>prometheus.yml</code><pre><code class="language-yaml">scrape_configs:
- job_name: &#39;spring-actuator&#39;
  metrics_path: &#39;/actuator/prometheus&#39;
  static_configs:
    - targets: [&#39;localhost:8080&#39;]  # Spring Boot 앱 포트
</code></pre>
</li>
</ul>
<pre><code>- 설정 후 재시작하여 적용한다.</code></pre><p>brew services restart prometheus</p>
<pre><code>

#### 확인
- `http://localhost:9090`으로 접속 후 **Status &gt; Targets** 메뉴에서 Spring Boot 타겟이 UP 상태인지 확인한다.
![](https://velog.velcdn.com/images/jh_devlog/post/65f84810-db61-4902-968f-ca630628ff59/image.png)

#### 4. Grafana 접속
1. `http://localhost:3000/login` 으로 접속
2. 기본 로그인 정보  (id: admin / pw: admin)
![](https://velog.velcdn.com/images/jh_devlog/post/08d22683-07ea-4035-999b-96df458130f2/image.png)

#### 5. Prometheus 데이터 소스 연결
- Add data source &gt;`Prometheus`를 선택한다.
![](https://velog.velcdn.com/images/jh_devlog/post/1dc06448-1234-4127-8839-0d8709084b96/image.png)![](https://velog.velcdn.com/images/jh_devlog/post/8814aa8b-20b2-4fe7-a0ec-d1be1f3f1a22/image.png)
- URL : `http://localhost:9090`를 입력 후 저장한다.
![](https://velog.velcdn.com/images/jh_devlog/post/3f091103-4b21-4a9b-832e-3ae68bb4a1a7/image.png)

#### 6. 대시보드 및 패널 생성
- 대시보드를 생성한다. (**Dashboard &gt; New Dashboard**)
- 조회를 원하는 `Metrics` 필드를 선택 후 `Run queries` 버튼을 클릭한다.
- 시각화 타입(Time series, Stat, Gauge)을 선택한다.
![](https://velog.velcdn.com/images/jh_devlog/post/2777f8ec-edbc-4790-adf8-62b113037589/image.png)


#### ✅ 최종 결과
![](https://velog.velcdn.com/images/jh_devlog/post/c0bf8590-6baf-4512-89ee-8852d15ceecf/image.png)


</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[SB 3기] 코드잇 스프린트 위클리페이퍼 9주차]]></title>
            <link>https://velog.io/@jh_devlog/SB-3%EA%B8%B0-%EC%BD%94%EB%93%9C%EC%9E%87-%EC%8A%A4%ED%94%84%EB%A6%B0%ED%8A%B8-%EC%9C%84%ED%81%B4%EB%A6%AC%ED%8E%98%EC%9D%B4%ED%8D%BC-9%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@jh_devlog/SB-3%EA%B8%B0-%EC%BD%94%EB%93%9C%EC%9E%87-%EC%8A%A4%ED%94%84%EB%A6%B0%ED%8A%B8-%EC%9C%84%ED%81%B4%EB%A6%AC%ED%8E%98%EC%9D%B4%ED%8D%BC-9%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Mon, 16 Jun 2025 00:02:18 GMT</pubDate>
            <description><![CDATA[<h3 id="q-jpa에서-발생하는-n1-문제의-발생-원인과-해결-방안에-대해-설명하세요">Q. JPA에서 발생하는 N+1 문제의 발생 원인과 해결 방안에 대해 설명하세요.</h3>
<h4 id="✅-n1-문제란">✅ N+1 문제란?</h4>
<ul>
<li>JPA 또는 ORM 환경에서 특정 엔티티를 조회할 때, 연관 관계를 가진 엔티티 또한 조회하게 되면서 예상치 못한 N개의 추가 쿼리가 발생하여 성능 저하를 유발하는 문제이다.<pre><code class="language-java">List&lt;Member&gt; members = memberRepository.findAll(); // 1개의 쿼리
for (Member member : members) {
  System.out.println(member.getTeam().getName()); // N개의 추가 쿼리
} </code></pre>
</li>
<li>회원 100명을 조회하고, 각 회원의 소속 부서를 출력하는 경우 :
<code>1(회원조회) + 100(부서조회) -&gt; 총 101개의 쿼리가 실행</code></li>
</ul>
<blockquote>
<p>⚠️ 데이터가 많아질수록 쿼리 개수가 선형적으로 증가하기 때문에, 대규모 데이터 처리 시 심각한 성능 저하를 초래할 수 있다.</p>
</blockquote>
<h4 id="🔍-문제-발생-원인">🔍 문제 발생 원인</h4>
<ul>
<li>연관된 엔티티를 지연 로딩(Lazy Loading)으로 설정할 경우, 엔티티를 조회 시에는 실제 쿼리가 실행되지 않다가, 해당 연관 엔티티에 접근하는 시점(Runtime)에 추가 쿼리가 발생한다.</li>
<li>또한, 즉시 로딩(Eager Loading)을 사용하더라도 다수의 엔티티를 컬렉션이나 연관 객체를 통해 반복적으로 접근하면, N+1 문제가 발생할 수 있다.</li>
</ul>
<h4 id="💡-해결-방안">💡 해결 방안</h4>
<h4 id="1-fetch-join-사용">1. Fetch Join 사용</h4>
<ul>
<li>JPQL에서 <code>join fetch</code>를 활용하여 연관된 엔티티를 하나의 쿼리로 함께 조회할 수 있다.</li>
<li>연관된 엔티티도 함께 영속성 컨텍스트에 등록되므로, 추가 쿼리 없이 접근 가능하다.<pre><code class="language-java">@Query(&quot;SELECT m FROM Member m JOIN FETCH m.team&quot;)
List&lt;Member&gt; findAllWithTeam();</code></pre>
👍 <strong>장점</strong></li>
<li>가장 직관적인 방식</li>
<li>복잡한 쿼리에도 활용 가능</li>
</ul>
<p>👎 <strong>단점</strong></p>
<ul>
<li>페이징 처리 불가 (메모리에서 페이징 처리가 이루어져 성능 저하)</li>
<li>2개 이상의 컬렉션에 대한 Fetch Join은 불가능</li>
<li>1:N 관계에서 중복된 데이터가 발생할 수 있음 -&gt; <code>DISTINCT</code> 필요</li>
</ul>
<h4 id="2-entitygraph-사용">2. EntityGraph 사용</h4>
<ul>
<li>JPA의 <code>@EntityGraph</code> 기능을 활용하면, JPQL 없이도 연관 엔티티를 함께 조회할 수 있다.</li>
<li>선언적인 방식으로 간결하게 Fetch Join 효과를 낼 수 있다.<pre><code class="language-java">@EntityGraph(attributePaths = {&quot;team&quot;})
@Override
List&lt;Member&gt; findAll();</code></pre>
👍 <strong>장점</strong></li>
<li>간결한 코드 작성 가능</li>
<li>페이징 처리와 함께 사용 가능</li>
</ul>
<p>👎 <strong>단점</strong> </p>
<ul>
<li>복잡한 조인 조건이나 필터링이 필요한 쿼리에는 적합하지 않음</li>
</ul>
<hr>
<h3 id="q-트랜잭션의-acid-속성-중-격리성isolation이-보장되지-않을-때-발생할-수-있는-문제점들을-설명하고-이를-해결하기-위한-트랜잭션-격리-수준들을-설명하세요">Q. 트랜잭션의 ACID 속성 중 격리성(Isolation)이 보장되지 않을 때 발생할 수 있는 문제점들을 설명하고, 이를 해결하기 위한 트랜잭션 격리 수준들을 설명하세요.</h3>
<h4 id="✅-acid-속성이란">✅ ACID 속성이란?</h4>
<p>데이터베이스에서 트랜잭션의 신뢰성과 일관성을 보장하기 위한 4가지 속성이다.</p>
<ul>
<li>A (Atomicity, 원자성): 
트랜잭션 내의 모든 작업은 전부 수행되거나 전부 실패해야 한다.</li>
<li>C (Consistency, 일관성): 
트랜잭션 수행 전후로 데이터의 일관성이 유지되어야 한다.</li>
<li>I (Isolation, 격리성): 
여러 트랜잭션이 동시에 수행되더라도 서로 영향을 미치지 않아야 한다.</li>
<li>D (Durability, 지속성): 
커밋된 데이터는 영구적으로 저장되어야 한다.</li>
</ul>
<h4 id="🔍-격리성이-보장되지-않을-때-발생할-수-있는-문제점">🔍 격리성이 보장되지 않을 때 발생할 수 있는 문제점</h4>
<p><strong>1. Dirty Read</strong> </p>
<ul>
<li>다른 트랜잭션이 아직 커밋하지 않은 데이터를 읽는 현상</li>
<li><blockquote>
<p>해당 트랜잭션이 롤백될 경우 잘못된 데이터를 읽게 된다.</p>
</blockquote>
</li>
</ul>
<p><strong>2. Non-repeatable Read</strong></p>
<ul>
<li>동일한 쿼리를 두 번 실행했을 때 결과가 달라지는 현상</li>
<li><blockquote>
<p>다른 트랜잭션이 중간에 값을 변경했기 때문에 발생</p>
</blockquote>
</li>
</ul>
<p>*<em>3. Phantom Read *</em></p>
<ul>
<li>같은 조건으로 쿼리를 반복 실행했을 때, 처음에는 없던 행이 나중에는 조회되는 현상</li>
<li><blockquote>
<p>다른 트랜잭션이 중간에 데이터를 삽입하여 발생</p>
</blockquote>
</li>
</ul>
<blockquote>
<p>예를 들어, 쇼핑몰에서 같은 상품의 재고가 1개일 때, 두 사용자가 동시에 주문할 경우, 격리성이 보장되지 않으면 재고 초과 판매 문제가 발생할 수 있다.</p>
</blockquote>
<h4 id="💡-트랜잭션-격리-수준">💡 트랜잭션 격리 수준</h4>
<p>트랜잭션 격리 수준은 ANSI SQL 표준으로 4단계로 정의되며, 격리 수준이 높을수록 데이터 정합성은 보장되지만 성능은 저하될 수 있다.</p>
<table>
<thead>
<tr>
<th>격리 수준</th>
<th>설명</th>
<th>방지되는 현상</th>
</tr>
</thead>
<tbody><tr>
<td><strong>READ UNCOMMITTED</strong></td>
<td>커밋되지 않은 데이터도 읽을 수 있음</td>
<td>❌ Dirty Read 허용</td>
</tr>
<tr>
<td><strong>READ COMMITTED</strong></td>
<td>커밋된 데이터만 읽을 수 있음 (대부분의 DB 기본값)</td>
<td>✅ Dirty Read 방지</td>
</tr>
<tr>
<td><strong>REPEATABLE READ</strong></td>
<td>동일 쿼리 결과가 항상 동일함 (MySQL InnoDB 기본값)</td>
<td>✅ Non-Repeatable Read 방지<br> ❌ Phantom Read는 허용</td>
</tr>
<tr>
<td><strong>SERIALIZABLE</strong></td>
<td>가장 높은 격리 수준, 트랜잭션 간 완전한 순차 실행 보장 (A와 B가 동시에 작업 불가) <br>성능 저하가 크게 발생</td>
<td>✅ Phantom Read까지 방지 (모든 문제 방지)</td>
</tr>
</tbody></table>
<hr>
<h3 id="📄-참고-문서">📄 참고 문서</h3>
<ul>
<li>코드잇 강의 교안</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SB 3기] 코드잇 스프린트 미션 5 회고]]></title>
            <link>https://velog.io/@jh_devlog/SB-3%EA%B8%B0-%EC%BD%94%EB%93%9C%EC%9E%87-%EC%8A%A4%ED%94%84%EB%A6%B0%ED%8A%B8-%EB%AF%B8%EC%85%98-5-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@jh_devlog/SB-3%EA%B8%B0-%EC%BD%94%EB%93%9C%EC%9E%87-%EC%8A%A4%ED%94%84%EB%A6%B0%ED%8A%B8-%EB%AF%B8%EC%85%98-5-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Mon, 02 Jun 2025 07:29:32 GMT</pubDate>
            <description><![CDATA[<h2 id="📖-미션-내용">📖 미션 내용</h2>
<h4 id="🚀-스프린트-미션-5">🚀 스프린트 미션 5</h4>
<ul>
<li>RESTful API로 재설계 및 리팩토링</li>
<li>Swagger를 활용한 API 문서 자동화</li>
<li>프론트엔드 연동</li>
<li>PaaS를 활용한 배포<ul>
<li>Railway.app 활용</li>
</ul>
</li>
</ul>
<hr>
<h2 id="💡-코드-리뷰-사항-및-개선-포인트">💡 코드 리뷰 사항 및 개선 포인트</h2>
<h3 id="📌-스프린트-미션-5">📌 스프린트 미션 5</h3>
<h4 id="1-aop-적용-시-패키지-기준이-아닌-커스텀-어노테이션-방식-활용">1. AOP 적용 시, 패키지 기준이 아닌 커스텀 어노테이션 방식 활용</h4>
<p>AOP를 특정 패키지 기준으로 적용할 경우, 해당 패키지가 변경되면 코드도 함께 수정해야 하는 문제가 발생할 수 있다.</p>
<pre><code class="language-java">// AS IS
@Around(&quot;execution(* com.sprint.mission.discodeit.service..*(..))&quot;)

// TO BE 
@Retention(value = RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Logging {
    String value() default &quot;&quot;;
}

@Around(&quot;@annotation(com.sprint.mission.discodeit.annotation.Logging)&quot;)</code></pre>
<p>위와 같이 커스텀 어노테이션을 정의해 설정하면, 코드 변경에도 유연하게 대응할 수 있다.</p>
<h4 id="2-반복되는-로직은-static-메서드로-분리하여-재사용">2. 반복되는 로직은 static 메서드로 분리하여 재사용</h4>
<p><code>resolve ~</code> 등의 유사한 처리를 하는 메서드가 반복되는 경우, 이를 별도의 유틸리티 메서드로 추출해 재사용하는 것이 효율적이다.</p>
<h4 id="3-매직-넘버는-상수-처리">3. 매직 넘버는 상수 처리</h4>
<p>아래의 코드에서 숫자 5는 상수로 선언하여 의미를 명확히 표현하는 것이 좋다.</p>
<pre><code class="language-java">// AS IS
Instant instantFiveMinuteAgo = Instant.now().minus(Duration.ofMinutes(5));

// TO BE
private static final int TIMEOUT_MINUTES = 5;
Instant instantFiveMinuteAgo = Instant.now().minus(Duration.ofMinutes(TIMEOUT_MINUTES));</code></pre>
<h2 id="회고">회고</h2>
<h3 id="👍-좋았던-점">👍 좋았던 점</h3>
<ul>
<li>RESTful한 API로 리팩토링 하는 과정을 통해 REST의 개념을 더 이해할 수 있었고, 어떻게 해야 더 RESTful하게 설계할 수 있을지 고민해볼 수 있었던 점이 좋았다.</li>
</ul>
<h3 id="😅-아쉬운-점">😅 아쉬운 점</h3>
<ul>
<li>❌ (이번 미션에서는 아쉬운 점이 딱히 없었다.)</li>
</ul>
<h3 id="🧠-배운-점">🧠 배운 점</h3>
<ul>
<li>RESTful한 API 설계 원칙에 대해 더 명확히 이해할 수 있었다.</li>
<li>커스텀 어노테이션을 활용하는 방법을 새롭게 배웠다.</li>
<li>AOP 적용 시 커스텀 어노테이션을 활용하는 방법을 구현해보면서, 생각지도 못한 부분에서 유지보수성과 유연성을 높일 수 있다는 점을 알게 되었고 이러한 설계를 중요하게 고민해봐야 한다는 것을 알게 되었다.</li>
<li>반복되는 유틸성 로직을 static 메서드로 추출하여 재사용 하는 것이 유지보수에 효과적임을 다시 깨달았다.(알고 있는 개념이라도 실제 코드에 꾸준히 적용하는 습관이 중요하다..⭐️)</li>
<li>매직 넘버는 의미 있는 상수로 치환해 코드의 가독성과 명확성을 높이는것이 중요하다.</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>