<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>YUZE.log</title>
        <link>https://velog.io/</link>
        <description>안녕하세요</description>
        <lastBuildDate>Mon, 22 Dec 2025 02:15:02 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>YUZE.log</title>
            <url>https://velog.velcdn.com/images/yuze_dbwm/profile/94020628-b705-42c3-a392-5f6cbed4dbc3/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. YUZE.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/yuze_dbwm" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[BOJ 16928 뱀과 사다리 게임]]></title>
            <link>https://velog.io/@yuze_dbwm/BOJ-16928-%EB%B1%80%EA%B3%BC-%EC%82%AC%EB%8B%A4%EB%A6%AC-%EA%B2%8C%EC%9E%84</link>
            <guid>https://velog.io/@yuze_dbwm/BOJ-16928-%EB%B1%80%EA%B3%BC-%EC%82%AC%EB%8B%A4%EB%A6%AC-%EA%B2%8C%EC%9E%84</guid>
            <pubDate>Mon, 22 Dec 2025 02:15:02 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-java">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;

public class Main {

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        /*
         * 주사위를 조작해 내가 원하는 수가 나오게 만들 수 있다면...
         * 최소 몇 번 만에 도착점에 도착?
         *
         * 1. 1~6 주사위
         * 2. 10x10 주사위 100 칸
         * 3. 1에서 100까지 수가 하나씩 순서대로 적혀져있다.
         *
         * - 주사위 굴린 결과가 100번 칸을 넘어간다면 이동 x
         * - 도착한 칸이 사다리면 사다리를 타고 위로 이동
         * - 뱀이 있는 칸데 도착하면, 뱀을 따라서 내려가야 됨
         *
         * 1. 1~100까지 간다
         * 2. 100번 칸에 도착하기 위한 최솟값은?
         *
         *  n : 사다리 수 m : 뱀의 수
         * n 개의 줄에는 사다리 x &gt; y이동
         * m 까지는 뱀 x &gt; y
         * */

        // dp나 다익스트라 같음
        // 기본으로 가다가...적은 cost로 다음칸에서 더 멀리 갈 수 있는 애 보기
        // x쪽에 있는 거 밟으면 확인해야함

        Deque&lt;Integer&gt; deque = new ArrayDeque&lt;&gt;();
        StringTokenizer st = new StringTokenizer(br.readLine());

        int n = Integer.parseInt(st.nextToken());
        int m = Integer.parseInt(st.nextToken());

        Map&lt;Integer, Integer&gt; dist = new HashMap&lt;&gt;();
        Map&lt;Integer, Integer&gt; mapCase = new HashMap&lt;&gt;();

        for (int i = 0; i &lt; m + n; i++) {
            st = new StringTokenizer(br.readLine());
            mapCase.put(Integer.parseInt(st.nextToken()), Integer.parseInt(st.nextToken()));
        }

        int[] nx = {1, 2, 3, 4, 5, 6};

        deque.add(1);
        dist.put(1, 0);

        while (!deque.isEmpty()) {
            int now = deque.remove();

            if (now == 100) {
                break;
            }

            for (int i = 0; i &lt; nx.length; i++) {
                int next = now + nx[i];
                if (next &gt; 100) {
                    continue;
                }
                // 특수성이 존재하니?
                if (mapCase.containsKey(next)) {
                    next = mapCase.get(next);
                }
                if (dist.containsKey(next)) {
                    // 이미 탐색함
                    continue;
                }
                // 최대거리 구하기
                dist.put(next, dist.get(now) + 1);
                deque.add(next);
            }
        }
        System.out.println(dist.get(100));
    }
}</code></pre>
<p>이 문제는 힌트를 봤따... 사실 dp와 다익스트라 중에서 고민했는데(지름길 문제랑 비슷하다고 느껴서)
그렇게 생각하니까 구현이 좀 막막하게 느껴지고 묘하게 불편하게 느껴졌다...어떻게 구현해야되지? 그냥 그런생각이 들었는데
힌트를 보니 bfs 문제였다.</p>
<br>

<p><strong>이 문제가 너비 탐색인 이유는</strong></p>
<ol>
<li>주사위 비용은 모두 1로 고정이 되어있음 -&gt; 다익스트라 x</li>
<li>100으로 가는 최단 거리 -&gt; bfs 너비 탐색</li>
<li>뱀과 사다리로인해, 사이클(그래프)과 같은 형상이 될 수 있기 때문에 dp로 풀기 어려울 것 -&gt; dp x</li>
<li>시간복잡도 충분 -&gt; dp x</li>
</ol>
<p>그래서 결론적으로 너비 우선 탐색 그래프 탐색 문제이다.</p>
<br>
<br>



<p>결론적으로 이 문제의 핵심은 bfs를 떠올릴 것.
그리고 내가 실수한 부분은 또 이부분이다.</p>
<pre><code class="language-java"> // 특수성이 존재하니?
                if (mapCase.containsKey(next)) {
                    next = mapCase.get(next);
                }
                if (dist.containsKey(next)) {
                    // 이미 탐색함
                    continue;
                }</code></pre>
<p>방문 체크를 하기 전에, 뱀 또는 사다리가 있는 부분인지 확인했어야됐는데 나는 저 if문 두개의 순서를 반대로 했었다.</p>
<p>이 부분을 유의해야지 논리적인 오류가 나지 않는다.</p>
<p>그리고 이 문제는 주사위 dx = {1, 2, 3, 4, 5, 6}
이런식으로 만들어서 그래프 탐색을 해줘야 하는게 포인트다</p>
<p>끝.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준 16637] IF문 좀 대신 써줘 (feat. 이분탐색 정리)]]></title>
            <link>https://velog.io/@yuze_dbwm/%EB%B0%B1%EC%A4%80-16637-IF%EB%AC%B8-%EC%A2%80-%EB%8C%80%EC%8B%A0-%EC%8D%A8%EC%A4%98-feat.-%EC%9D%B4%EB%B6%84%ED%83%90%EC%83%89-%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@yuze_dbwm/%EB%B0%B1%EC%A4%80-16637-IF%EB%AC%B8-%EC%A2%80-%EB%8C%80%EC%8B%A0-%EC%8D%A8%EC%A4%98-feat.-%EC%9D%B4%EB%B6%84%ED%83%90%EC%83%89-%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 16 Dec 2025 10:15:32 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/4b895889-34e8-4160-8d4c-a0b7513bc771/image.png" alt=""></p>
<p>터무니 없는 n의 길이를 보고... 이분탐색인가 했다.
요즘 이분탐색 문제를 자주 풀고 있는데... 뭔가 유형별로 정리가 필요할 거 같아서 정리해본다.</p>
<ol start="0">
<li>기본 이분탐색 (값이 존재하는가)<pre><code class="language-java">int l = 0, r = n - 1;
</code></pre>
</li>
</ol>
<p>while (l &lt;= r) {
    int mid = (l + r) / 2;
    if (arr[mid] == target) {
        // 찾음
        break;
    } else if (arr[mid] &lt; target) {
        l = mid + 1;
    } else {
        r = mid - 1;
    }
}</p>
<pre><code>
## 인덱스  유형
### Lower Bound
target 이상이 처음 나오는 위치

```java
int l = 0, r = n;

while (l &lt; r) {
    int mid = (l + r) / 2;
    if (arr[mid] &gt;= target) {
        r = mid;
    } else {
        l = mid + 1;
    }
}
// l이 답
</code></pre><h3 id="upper-bound">Upper Bound</h3>
<p>target 초과가 처음 나오는 위치</p>
<pre><code>int l = 0, r = n;

while (l &lt; r) {
    int mid = (l + r) / 2;
    if (arr[mid] &gt; target) {
        r = mid;
    } else {
        l = mid + 1;
    }
}
// l이 답
</code></pre><h2 id="값의-범위-유형">값의 범위 유형</h2>
<h3 id="first-true최소-만족-값">First True(최소 만족 값)</h3>
<pre><code class="language-java">int l = 0, r = n;

while (l &lt; r) {
    int mid = (l + r) / 2;
    if (arr[mid] &gt; target) {
        r = mid;
    } else {
        l = mid + 1;
    }
}
// l이 답
</code></pre>
<h3 id="last-true최대-만족-값">Last True(최대 만족 값)</h3>
<pre><code class="language-java">int l = left, r = right;
int ans = -1;

while (l &lt;= r) {
    int mid = (l + r) / 2;
    if (check(mid)) {
        ans = mid;
        l = mid + 1;
    } else {
        r = mid - 1;
    }
}
System.out.println(ans);
</code></pre>
<table>
<thead>
<tr>
<th>구분</th>
<th>Upper Bound</th>
<th>First True</th>
</tr>
</thead>
<tbody><tr>
<td>대상</td>
<td><strong>배열 인덱스</strong></td>
<td><strong>값의 범위</strong></td>
</tr>
<tr>
<td>전제</td>
<td>정렬된 배열</td>
<td>단조 조건</td>
</tr>
<tr>
<td>조건</td>
<td><code>arr[mid] &gt; target</code></td>
<td><code>check(mid)</code></td>
</tr>
<tr>
<td>목적</td>
<td>위치</td>
<td>값</td>
</tr>
</tbody></table>
<table>
<thead>
<tr>
<th>질문</th>
<th>답</th>
</tr>
</thead>
<tbody><tr>
<td>최소 ○○</td>
<td>First True</td>
</tr>
<tr>
<td>최대 ○○</td>
<td>Last True</td>
</tr>
<tr>
<td>개수</td>
<td>Upper - Lower</td>
</tr>
<tr>
<td>처음 ≥</td>
<td>Lower Bound</td>
</tr>
<tr>
<td>처음 &gt;</td>
<td>Upper Bound</td>
</tr>
</tbody></table>
<p>그렇다면 이 문제는...</p>
<p>9999
10000 100000 100000
9999이상인 값이 제일 처음으로 나오는 위치가 답이다. 따라서 Lower Bound 유형</p>
<pre><code class="language-java">import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.*;

public class Main {
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());

        int count = Integer.parseInt(st.nextToken());
        int cCount = Integer.parseInt(st.nextToken());

        ArrayList&lt;Title&gt; titles = new ArrayList&lt;&gt;();

        for (int i = 0; i &lt; count; i++) {
            st = new StringTokenizer(br.readLine());
            titles.add(new Title(st.nextToken(), Long.valueOf(st.nextToken())));
        }
        titles.sort(Comparator.comparing(title -&gt; title.limit));
        StringBuilder sb = new StringBuilder();

        for (int i = 0; i &lt; cCount; i++) {
            Long power = Long.valueOf(br.readLine());

            int left = 0;
            int right = titles.size() - 1;

            while (left &lt; right) {
                int mid = (right + left) / 2;
                Title now = titles.get(mid);

                if (now.limit &gt;= power) {
                    right = mid;
                    continue;
                }
                left = mid + 1;
            }
            sb.append(titles.get(left).name);
            sb.append(&quot;\n&quot;);
        }
        System.out.println(sb);
    }

    public static class Title {
        String name;
        Long limit;

        public Title(String name, Long limit) {
            this.name = name;
            this.limit = limit;
        }
    }
}


</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[1515 수 이어 쓰기]]></title>
            <link>https://velog.io/@yuze_dbwm/1515-%EC%88%98-%EC%9D%B4%EC%96%B4-%EC%93%B0%EA%B8%B0</link>
            <guid>https://velog.io/@yuze_dbwm/1515-%EC%88%98-%EC%9D%B4%EC%96%B4-%EC%93%B0%EA%B8%B0</guid>
            <pubDate>Wed, 10 Dec 2025 06:51:15 GMT</pubDate>
            <description><![CDATA[<ul>
<li>처음 틀렸던 코드</li>
</ul>
<p>생각했던 로직</p>
<ol>
<li>밖에서 number를 생성하고</li>
<li>해당 number에 해당하는 것이 있는지 sb에 contains로 확인</li>
</ol>
<p>but &gt;&gt; 부분 수열을 가정하고 만들었기 때문에 틀렸음.
1001 이렇게 있어도 11 이렇게만 남을 수 있기 떄문에 1001을 for문으로 붙잡고 포함되었는지를 확인 했어야됐다. </p>
<pre><code class="language-java">import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        String number = br.readLine();
        char[] arr = number.toCharArray();
        Deque&lt;Character&gt; deque = new ArrayDeque&lt;&gt;();

        int k = 1;

        for (char a : arr) {
            deque.add(a);
        }

        while (!deque.isEmpty()) {
            check(k, deque);
            k++;
        }

        System.out.println(k - 1);
    }

    public static void check(int number, Deque&lt;Character&gt; deque) {
        String targetNumber = Integer.toString(number);
        StringBuilder sb = new StringBuilder();

        while (!deque.isEmpty()) {
            Character now = deque.removeFirst();
            sb.append(now);

            if (targetNumber.length() == sb.length()) {
                if (targetNumber.contains(sb)) {
                    return;
                }
                deque.addFirst(now);
                return;
            }
            if (!targetNumber.contains(sb)) {
                deque.addFirst(now);
                return;
            }
        }

        return;
    }
}</code></pre>
<br>

<ul>
<li>다시풀이</li>
</ul>
<pre><code class="language-java">
import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        String number = br.readLine();

        int idx = 0;

        //1234
        for (int i = 1; ; i++) {
            String num = Integer.toString(i);

            for (int j = 0; j &lt; num.length(); j++) {
                if (idx &lt; number.length() &amp;&amp; num.charAt(j) == number.charAt(idx)) {
                    idx++;
                }
            }
            if (idx &gt;= number.length()) {
                System.out.println(i);
                return;
            }
        }
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[2512 예산]]></title>
            <link>https://velog.io/@yuze_dbwm/2512-%EC%98%88%EC%82%B0</link>
            <guid>https://velog.io/@yuze_dbwm/2512-%EC%98%88%EC%82%B0</guid>
            <pubDate>Wed, 10 Dec 2025 04:35:25 GMT</pubDate>
            <description><![CDATA[<p>처음에는 limit의 평균을 구해서, 넘는 값에 대해 적정 선을 맞추는 방식으로 했는데 틀렸음</p>
<p>이 문제는 &#39;이분탐색&#39;으로 풀어야되는 문제임</p>
<p>일부 테스트케이스는 맞았지만, 실제로는 틀렸음</p>
<pre><code class="language-java">import java.io.*;
import java.util.*;
import javax.swing.Icon;

public class Main {
    public static void main(String[] args) throws IOException {
        /*
         * 총액 이하에서 가능한 한 최대의 총 에싼을 배정
         *
         * 1. 모든 요청이 배정될 수 있는 경우에는 요청한 금액을 그대로 배정
         * 2. 모든 요청이 배정될 수 없는 경우에는 특정한 정수 상한액을 계산하여, 그 이상인 예상 요청에는 모두 상한액을 배정
         * 상한액 이하의 예산요청에 대해서는 요청한 금액을 그대로 배정
         *
         * o(n)으로 풀어야함
         *
         * 520 - 485 = 35 오바
         * 485 -&gt;
         * 121
         * -1, -11, +19, + 29
         * - 12 / 남은 개수
         * 평균보다 6씩 더주기
         * +48 인 상황인데
         * */

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        int n = Integer.parseInt(br.readLine());

        StringTokenizer st = new StringTokenizer(br.readLine());
        PriorityQueue&lt;Integer&gt; numbers = new PriorityQueue&lt;&gt;(Comparator.reverseOrder());

        int total = 0;
        for (int i = 0; i &lt; n; i++) {
            int number = Integer.parseInt(st.nextToken());
            numbers.add(number);
            total += number;
        }

        int limit = Integer.parseInt(br.readLine());

        if (limit &gt;= total) {
            System.out.println(numbers.remove());
            return;
        }

        int limitAvg = limit / n;
        int overCount = 0;
        int underScore = 0;

        int under = 0;
        while (!numbers.isEmpty()) {
            int number = numbers.remove();

            if (number &gt; limitAvg) {
                overCount++;
            } else {
                int temp = limitAvg - number;
                underScore += temp;
                under += number;
            }
        }
        int result = limitAvg + (underScore / overCount);
        if (overCount == n) {
            System.out.println(limitAvg);
            return;
        }
        System.out.println(result);
    }
}
</code></pre>
<p>계속해서 최적의 해를 찾아가는 것이기 떄문에 적절함</p>
<ul>
<li>초안
최적화 하지 않았음</li>
</ul>
<pre><code class="language-java">import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = Integer.parseInt(br.readLine());
        StringTokenizer st = new StringTokenizer(br.readLine());
        int[] requests = new int[n];

        for (int i = 0; i &lt; n; i++) {
            requests[i] = Integer.parseInt(st.nextToken());
        }

        int limit = Integer.parseInt(br.readLine());

        Arrays.sort(requests);

        int answer = 0;
        int left = 0;
        int right = requests[requests.length - 1];

        while (left &lt;= right) {
            int mid = (left + right) / 2;
            int sum = 0;

            for (int request : requests) {
                sum += Math.min(request, mid);
            }

            if (sum &lt;= limit) {
                left++;
                answer = Math.max(answer, mid);
            } else {
                right--;
            }
        }
        System.out.println(answer);
    }
}</code></pre>
<ul>
<li>최적화 ver
mid보다 큰 애들은 상한값 처리 하고, 나머지는 누적합처리해서 계속해서 계산해야되는 부분을 최적화했음</li>
</ul>
<pre><code class="language-java">import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        int n = Integer.parseInt(br.readLine());
        StringTokenizer st = new StringTokenizer(br.readLine());

        int[] requests = new int[n];
        int max = 0;
        int total = 0;

        for (int i = 0; i &lt; n; i++) {
            int number = Integer.parseInt(st.nextToken());
            requests[i] = number;
            max = Math.max(max, number);
            total += number;
        }

        int limit = Integer.parseInt(br.readLine());

        if (limit &gt;= total) {
            System.out.println(max);
            return;
        }

        Arrays.sort(requests);

        int left = 0;
        int right = requests[requests.length - 1];
        int answer = 0;
        int[] sumArr = new int[n];

        for (int i = 0; i &lt; n - 1; i++) {
            sumArr[i + 1] = sumArr[i] + requests[i];
        }

        while (left &lt;= right) {
            int mid = (left + right) / 2;

            int idx = underScore(requests, mid);
            int sum = sumArr[idx] + (mid * (n - idx));

            if (sum &lt;= limit) {
                answer = mid;
                left = left + 1;
            }
            else {
                right = right - 1;
            }
        }
        System.out.println(answer);
    }

    public static int underScore(int[] requests, int target) {
        int left = 0;
        int right = requests.length - 1;

        while (left &lt; right) {
            int mid = (left + right) / 2;

            if (requests[mid] &lt;= target) {
                left++;
            } else {
                right = mid;
            }
        }
        return left;
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[20006 랭킹전 대기열]]></title>
            <link>https://velog.io/@yuze_dbwm/20006-%EB%9E%AD%ED%82%B9%EC%A0%84-%EB%8C%80%EA%B8%B0%EC%97%B4</link>
            <guid>https://velog.io/@yuze_dbwm/20006-%EB%9E%AD%ED%82%B9%EC%A0%84-%EB%8C%80%EA%B8%B0%EC%97%B4</guid>
            <pubDate>Tue, 09 Dec 2025 03:00:58 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-java">import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int count = Integer.parseInt(st.nextToken());
        int limitCount = Integer.parseInt(st.nextToken());
        List&lt;Room&gt; rooms = new ArrayList&lt;&gt;();

        for (int i = 0; i &lt; count; i++) {
            st = new StringTokenizer(br.readLine());

            int level = Integer.parseInt(st.nextToken());
            String nickname = st.nextToken();
            Person person = new Person(nickname, level);

            if (rooms.isEmpty()) {
                Room room = new Room(person.level, limitCount);
                room.people.add(person);
                rooms.add(room);
                continue;
            }

            boolean flag = false;
            for (Room room : rooms) {
                if (level &gt; room.level + 10 || level &lt; room.level - 10) {
                    continue;
                }
                if (room.people.size() == room.limit) {
                    continue;
                }
                room.people.add(person);
                flag = true;
                break;
            }

            if (!flag) {
                Room room = new Room(person.level, limitCount);
                room.people.add(person);
                rooms.add(room);
            }
        }

        StringBuilder sb = new StringBuilder();

        for (Room r : rooms) {
            if (r.people.size() == r.limit) {
                sb.append(&quot;Started!&quot;);
                sb.append(&quot;\n&quot;);
            }
            else {
                sb.append(&quot;Waiting!&quot;);
                sb.append(&quot;\n&quot;);
            }

            r.people.sort(Comparator.comparing((p) -&gt; p.name));
            for (Person p : r.people) {
                sb.append(p.level);
                sb.append(&quot; &quot;);
                sb.append(p.name);
                sb.append(&quot;\n&quot;);
            }
        }
        System.out.println(sb);
    }

    public static class Room {
        Integer level;
        Integer limit;
        ArrayList&lt;Person&gt; people = new ArrayList&lt;&gt;();

        public Room(Integer level, Integer limit) {
            this.level = level;
            this.limit = limit;
        }
    }

    public static class Person {
        String name;
        Integer level;

        public Person(String name, Integer level) {
            this.name = name;
            this.level = level;
        }
    }
}
</code></pre>
<p>완전탐색 + 구현 문제</p>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/59f8817c-7159-4f5a-9da1-76e8ab587eb3/image.png" alt=""></p>
<p>시간 복잡도를 고려해야 한다는 조건이 나오지 않았기 때문에 위와 같이 코드를 작성했다.</p>
<br>

<p><strong>간단한 로직 설명</strong> </p>
<ol>
<li>list는 생성 된 room들을 담는 것. 무조건 생성 순서로 쌓이는 것이 보장됨</li>
<li>방 생성<ul>
<li>list가 비어있을 경우 -&gt; 생성 되어 있는 room이 없는 경우</li>
<li>user에 맞게 매치되는 방이 없는 경우, room을 생성</li>
</ul>
</li>
<li>방 매칭<ul>
<li>만원 방 빼고 방 매칭</li>
<li>매칭 안 될 경우 2 다시 수행</li>
</ul>
</li>
</ol>
<p><a href="https://www.acmicpc.net/problem/20006">20006 랭킹전 대기열</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 1406 에디터]]></title>
            <link>https://velog.io/@yuze_dbwm/%EB%B0%B1%EC%A4%80-1406-%EC%97%90%EB%94%94%ED%84%B0</link>
            <guid>https://velog.io/@yuze_dbwm/%EB%B0%B1%EC%A4%80-1406-%EC%97%90%EB%94%94%ED%84%B0</guid>
            <pubDate>Tue, 09 Dec 2025 01:40:34 GMT</pubDate>
            <description><![CDATA[<ul>
<li>틀렸던 코드<pre><code class="language-java">import java.io.*;
import java.util.*;
</code></pre>
</li>
</ul>
<p>public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));</p>
<pre><code>    // 최대 60만
    /*
     * 모든 명령어를 수행하고 편집기에 입력되어있는 문자열은?
     * O(n)
     * */
    LinkedList&lt;String&gt; list = new LinkedList&lt;&gt;();
    String data = br.readLine();
    String[] dataArr = data.split(&quot;&quot;);

    for (int i = 0; i &lt; dataArr.length; i++) {
        list.add(i, dataArr[i]);
    }

    int n = Integer.parseInt(br.readLine());

    int size = list.size();
    int cursor = size;

    for (int i = 0; i &lt; n; i++) {
        StringTokenizer st = new StringTokenizer(br.readLine());
        String operator = st.nextToken();

        if (operator.equals(&quot;P&quot;)) {
            String word = st.nextToken();

            list.add(cursor, word);
            size++;
            cursor++;
            continue;
        }
        if (operator.equals(&quot;L&quot;)) {
            if (cursor == 0) {
                continue;
            }
            cursor--;
            continue;
        }
        if (operator.equals(&quot;D&quot;)) {
            if (cursor == size) {
                continue;
            }
            cursor++;
            continue;
        }
        if (operator.equals(&quot;B&quot;)) {
            if (cursor == 0) {
                continue;
            }
            size--;
            list.remove(cursor - 1);
            cursor--;
        }
    }

    StringBuilder sb = new StringBuilder();
    for (String s : list) {
        sb.append(s);
    }

    System.out.print(sb);
}</code></pre><p>}</p>
<pre><code>
처음에는. 중간에서 삽입 삭제를 하기 편한 linkedlist를 사용했다.
그러나 

| 동작                               | ArrayList      | LinkedList                    |
| -------------------------------- | -------------- | ----------------------------- |
| 임의 인덱스 접근 (`get(i)`)             | O(1)        | O(n)(i번째까지 이동 필요)      |
| 중간 삽입/삭제 (`add(i)`, `remove(i)`) | O(n) (뒤 요소 밀기) | O(n) (i까지 이동 후 포인터 연결) |
| 앞/뒤 삽입 (`addFirst`, `addLast`)   | O(1) | O(1)                          |
| Iterator 삽입/삭제                   | O(1)           | O(1)                          |


자료구조 시간 복잡도를 모르고 있었다. 중간 삽입 삭제가 O(n)이 걸린다. 그래서 이 문제는 O(n) 수준으로 풀어야되는데, O(n^2) 이상으로 풀고 있었던 것이다.

&lt;br&gt;
&lt;br&gt;
&lt;br&gt;

### 해결 방법(1)

1. listIterator 사용

| 항목       | LinkedList                      | ListIterator                   |
| -------- | ------------------------------- | ------------------------------ |
| 내부 구조    | 이중 연결 리스트 (prev / value / next) | 연결 리스트의 **노드를 직접 참조하는 커서**     |
| 위치 접근 방식 | 인덱스로 접근 → 노드 탐색 필요              | 현재 커서 노드를 기억하므로 탐색 없음          |
| 주요 목적    | 자료 저장                           | 리스트 순회 + 편집                    |
| 양방향 이동   | 가능하지만 `get(i)`는 O(N)            | `next()`, `previous()` 모두 O(1) |

&lt;br&gt;


| 연산            | LinkedList (인덱스 기반)    | LinkedList + ListIterator |
| ------------- | ---------------------- | ------------------------- |
| get(i)        | O(N)                 | O(1) (커서가 이미 위치를 알고 있음) |
| add(index, x) | O(N) (index까지 탐색 필요) | O(1)                    |
| remove(index) | O(N)                 | O(1)                    |
| 이전 요소 이동      | O(N) → get(i-1)      | previous() → O(1)       |
| 다음 요소 이동      | O(N) → get(i+1)      | next() → O(1)           |

``` java
import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        LinkedList&lt;Character&gt; list = new LinkedList&lt;&gt;();
        String data = br.readLine();

        for (char d : data.toCharArray()) {
            list.add(d);
        }

        int n = Integer.parseInt(br.readLine());
        ListIterator&lt;Character&gt; li = list.listIterator(list.size());

        for (int i = 0; i &lt; n; i++) {
            String line = br.readLine();
            char operator = line.charAt(0);

            if (operator == &#39;P&#39;) {
                char x = line.charAt(2);
                li.add(x);
                continue;
            }
            if (operator == &#39;L&#39;) {
                if (!li.hasPrevious()) {
                    continue;
                }
                li.previous();
                continue;
            }
            if (operator == &#39;D&#39;) {
                if (!li.hasNext()) {
                    continue;
                }
                li.next();
                continue;
            }
            if (operator == &#39;B&#39;) {
                if (!li.hasPrevious()) {
                    continue;
                }
                li.previous();
                li.remove();
            }
        }

        StringBuilder sb = new StringBuilder();
        for (char c : list) {
            sb.append(c);
        }
        System.out.print(sb.toString());
    }
}</code></pre><br>
<br>
<br>


<h3 id="해결-방법2">해결 방법(2)</h3>
<p>deque 2개를 이용해서 left의 마지막은 cursor 위치로 고정 -&gt; O(n) 수준으로 풀이 가능</p>
<pre><code class="language-java">import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        Deque&lt;Character&gt; left = new ArrayDeque&lt;&gt;();
        Deque&lt;Character&gt; right = new ArrayDeque&lt;&gt;();

        String data = br.readLine();

        for (char d : data.toCharArray()) {
            left.add(d);
        }

        int n = Integer.parseInt(br.readLine());

        for (int i = 0; i &lt; n; i++) {
            String line = br.readLine();
            char operator = line.charAt(0);

            if (operator == &#39;P&#39;) {
                char x = line.charAt(2);
                left.add(x);
                continue;
            }
            if (operator == &#39;L&#39;) {
                if (left.isEmpty()) {
                    continue;
                }
                right.addFirst(left.removeLast());
                continue;
            }
            if (operator == &#39;D&#39;) {
                if (right.isEmpty()) {
                    continue;
                }
                left.addLast(right.removeFirst());
                continue;
            }
            if (operator == &#39;B&#39;) {
                if (left.isEmpty()) {
                    continue;
                }
                left.removeLast();
            }
        }

        StringBuilder sb = new StringBuilder();

        while (!left.isEmpty()) {
            sb.append(left.remove());
        }
        while (!right.isEmpty()) {
            sb.append(right.remove());
        }
        System.out.print(sb);
    }
}
</code></pre>
<br>
<br>
<br>



<p><a href="https://www.acmicpc.net/problem/1406">1406 에디터</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[백준 11501번 주식]]></title>
            <link>https://velog.io/@yuze_dbwm/%EB%B0%B1%EC%A4%80-11501%EB%B2%88-%EC%A3%BC%EC%8B%9D</link>
            <guid>https://velog.io/@yuze_dbwm/%EB%B0%B1%EC%A4%80-11501%EB%B2%88-%EC%A3%BC%EC%8B%9D</guid>
            <pubDate>Sun, 07 Dec 2025 12:21:19 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-java">
public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringBuilder sb = new StringBuilder();

        int k = Integer.parseInt(br.readLine());

        for (int i = 0; i &lt; k; i++) {
            ArrayDeque&lt;Integer&gt; stocks = new ArrayDeque&lt;&gt;();
            int n = Integer.parseInt(br.readLine());
            StringTokenizer st = new StringTokenizer(br.readLine());

            for (int j = 0; j &lt; n; j++) {
                Integer stock = Integer.parseInt(st.nextToken());
                stocks.add(stock);
            }

            int maxNum = Integer.MIN_VALUE;
            long total = 0;

            while(!stocks.isEmpty()) {
                int now = stocks.removeLast();

                if (maxNum &lt; now) {
                    maxNum = now;
                    continue;
                }
                total += maxNum - now;
            }
            sb.append(total);
            sb.append(&quot;\n&quot;);
        }
        System.out.print(sb);
    }
}
</code></pre>
<p>주목할 점</p>
<ol>
<li>주식을 하나밖에 못 산다.</li>
<li>원하는 만큼 가지고 있는 주식을 판다.</li>
<li>아무것도 안한다.</li>
</ol>
<p>하루에 할 수 있는 action은 한 개 이고, 주식은 무조건 하루에 하나 밖에 사지 못한다.
우리는 미래의 주식 가격을 아니까, 미래부터 과거로 순회하면서 O(n)으로 풀이를 진행할 수 있다.</p>
<br>

<p>하루에 주식을 하나밖에 못사니까, 미래의 최대 가격보다 낮으면 사서 차곡 차곡 수익을 만드는 것이 이득이다.</p>
<p>우리가 해야될 것은,</p>
<ol>
<li>미래의 최대가격을 갱신</li>
<li>미래 가격보다 저렴하다면 수익실현</li>
</ol>
<p>이 두가지만 코드로 구현해내면 된다.</p>
<ul>
<li>못풀었던 이유
아이디어 부족.. 최대값을 도출</li>
</ul>
<ul>
<li><p>출처
<a href="https://www.acmicpc.net/problem/11501">백준 11501</a></p>
</li>
<li><p>비슷한 문제
<a href="https://school.programmers.co.kr/learn/courses/30/lessons/42584">프로그래머스 주식가격</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[신투 프디아] MSA 설계]]></title>
            <link>https://velog.io/@yuze_dbwm/%EC%8B%A0%ED%88%AC-%ED%94%84%EB%94%94%EC%95%84-MSA-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@yuze_dbwm/%EC%8B%A0%ED%88%AC-%ED%94%84%EB%94%94%EC%95%84-MSA-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Sat, 11 Oct 2025 11:44:01 GMT</pubDate>
            <description><![CDATA[<p>저희 사륜구동 팀의 주제는, <strong>&#39;블록 기반 사용자 전략 자동 매매 서비스&#39;</strong>입니다.</p>
<p>사용자가 보조지표와 지표 블록(EMA, RSI, MACD, 거래량 등)을 조합해 자신만의 매매 전략을 구성하고 이를 실시간으로 실행할 수 있도록 설계했습니다.</p>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/8470f161-b910-4bff-a71a-fa3cd5289d81/image.png" alt=""></p>
<p>저희 시스템 아키텍처를 보면 위와 같이 구성되어있습니다.</p>
<h3 id="기술-선정">기술 선정</h3>
<hr>
<table>
<thead>
<tr>
<th>기술</th>
<th>이유</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Spring Boot</strong></td>
<td>유지보수 용이</td>
</tr>
<tr>
<td><strong>MongoDB</strong></td>
<td>주문에 관련된 반정형 JSON 전략 구조에 적합</td>
</tr>
<tr>
<td><strong>MariaDB</strong></td>
<td>사용자/주문 데이터의 정형화된 관리</td>
</tr>
<tr>
<td><strong>Kafka</strong></td>
<td>실시간 스트리밍</td>
</tr>
<tr>
<td><strong>ArgoCD + K8s</strong></td>
<td>전략별 pod 배포 자동화</td>
</tr>
<tr>
<td><strong>WebClient</strong></td>
<td>논블로킹 방식의 OpenAPI 호출</td>
</tr>
<tr>
<td><strong>Redis</strong></td>
<td>중앙에서 토큰 관리</td>
</tr>
<tr>
<td><strong>Flask</strong></td>
<td>전략 실행, 가벼운 파이썬 서버</td>
</tr>
</tbody></table>
<br>
<br>
<br>


<h3 id="주요-포인트">주요 포인트</h3>
<hr>
<h4 id="1-1전략-1파드">(1) 1전략 1파드</h4>
<p>저희는 사용자 전략마다 pod를 1개씩 띄웁니다. 그렇게 생각한 이유는, kafka에서 지표, 보조지표, 시세데이터들이 뿌려질 때마다 사용자들의 전략이 실행되어야합니다.</p>
<p>그리고, 그 전략에 알맞는 상황일 경우 매매를 수행하고 전략을 꺼야되는데, 실시간 성을 살리고 전략끼리의 독립성을 유지하기 위해서 pod를 여러개를 띄우는 구조로 정했습니다.</p>
<br>
<br>

<h4 id="2-kafka-사용">(2) kafka 사용</h4>
<p>market module에서 시세데이터를 받아서 모든 보조지표와 지표를 계산해서 pod들에게 뿌려줘야합니다. 따라서, 여러 pod들에게 빠르게 실시간으로 전달해야 됐기 떄문에 kafka를 사용했습니다.</p>
<p>저희는 토픽을 설계할 때, 
[주식 ticker].[봉단위]로 이름 지어서 토픽을 발행 하고 있습니다.
따라서, pod들은 자신에 맞는 ticker topic만 consume하는 구조로 설계되어있습니다</p>
<br>
<br>

<h4 id="3-전략-json-저장에-mogodb-사용">(3) 전략 json 저장에, mogodb 사용</h4>
<pre><code>{
    &quot;strategy_name&quot;: &quot;전략 이름&quot;,
    &quot;version&quot;: 1,
    &quot;owner_id&quot;: &quot;user_123&quot;,
    &quot;meta&quot;: {
        &quot;universe&quot;: [&quot;005930&quot;], // 종목정보
        &quot;enabled&quot;: true // 실행 여부
    },
    &quot;buy&quot;: { // 매수 블록
        &quot;node&quot;: {
            &quot;type&quot;: &quot;GROUP&quot;,
            &quot;logic&quot;: &quot;ALL&quot;,
            &quot;label&quot;: &quot;BUY ROOT: 볼린저 밴드 상단 돌파&quot;,
            &quot;children&quot;: [
                {
                    &quot;type&quot;: &quot;CROSS&quot;,
                    &quot;label&quot;: &quot;A1: close(15m) CROSS UP BB.upper(20, 2, 15m)&quot;,
                    &quot;direction&quot;: &quot;UP&quot;,
                    &quot;left&quot;: {
                        &quot;kind&quot;: &quot;PRICE&quot;,
                        &quot;field&quot;: &quot;close&quot;,
                        &quot;timeframe&quot;: &quot;15m&quot;
                    },
                    &quot;right&quot;: {
                        &quot;kind&quot;: &quot;INDICATOR&quot;,
                        &quot;name&quot;: &quot;BOLLINGER_BANDS&quot;,
                        &quot;args&quot;: { &quot;period&quot;: 20, &quot;stddev&quot;: 2 },
                        &quot;subfield&quot;: &quot;upper&quot;,
                        &quot;timeframe&quot;: &quot;15m&quot;
                    }
                }
            ]
        }
    }
}

</code></pre><p>저희의 전략 json은 다양한 조합으로 구성됩니다. 사용자의 요청에 따라 다양한 형태의 json으로 오기 때문에, rdb에 이것을 저장하기에는, key마다 쪼개야 하는 것도 많고 복잡하기 때문에, 연산량도 너무 많을 것이라고 생각했습니다.</p>
<p>따라서, mongodb에 이것을 저장해서 관리하게 된다면, 유저가 다양한 조합을 주더라도 그대로 저장할 수 있을 것이라고 생각해서 mongodb를 사용해서 저장하기로 결정했습니다.</p>
<br>
<br>

<h4 id="4-redis-사용">(4) Redis 사용</h4>
<p>저희 서비스에서는, Redis를 통해 중앙에서 토큰을 관리합니다. 이를 통해, 여러 서비스가 동일한 토큰을 공유하고 만료 시점을 일괄적으로 갱신할 수 있습니다.</p>
<p>특히 백엔드에서 redis를 사용할 때, read와 write를 나눠서, 모듈마다 쓰기 제한을 하도록 구성했습니다.</p>
<ul>
<li>공통 모듈에서는 <strong>read</strong>만
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/dd39090c-daf1-4a4c-9691-fa2058628efa/image.png" alt="">
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/c2444a4d-3bb1-4a64-a6a8-8249239d0abd/image.png" alt=""></li>
</ul>
<br>
<br>

<ul>
<li>auth 모듈에서는 <strong>write, read</strong> 둘 다
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/c8c9af7e-c505-4a29-b5d5-dc1bf553e928/image.png" alt="">
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/d6a41e19-a803-4303-ae4a-743fbeb341e5/image.png" alt=""></li>
</ul>
<p>그리고 auth 서버에서만 redis에 write 가능하도록 구성해서 책임을 분리했습니다.</p>
<h4 id="5-주식-주문--event-driven-구조">(5) 주식 주문 — Event Driven 구조</h4>
<ol>
<li>주문 생성 후 DB 저장 및 이벤트 발행
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/e8371008-40a4-4e40-af32-2b59e95241ef/image.png" alt=""></li>
</ol>
<ol start="2">
<li>이벤트가 발행 될 경우, 한국투자증권 API 주문 전송 (매수/매도 처리)
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/384eac61-f17c-4065-ba78-068e5776c008/image.png" alt=""></li>
</ol>
<h4 id="왜-이렇게헀나요">왜 이렇게...헀나요..?</h4>
<ul>
<li>주문 생성과 KIS 주문 로직 분리로 결합도 낮춤</li>
<li>비동기 처리로 사용자 응답 속도 향상</li>
<li>주문 생성과 KIS 주문을 트랜잭션 분리 → 데이터 일관성 보장</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[신투 프디아] 너가 내려라 시세데이터...(feat Kafka)]]></title>
            <link>https://velog.io/@yuze_dbwm/%EC%8B%A0%ED%88%AC-%ED%94%84%EB%94%94%EC%95%84-%EB%84%88%EA%B0%80-%EB%82%B4%EB%A0%A4%EB%9D%BC-%EC%8B%9C%EC%84%B8%EB%8D%B0%EC%9D%B4%ED%84%B0...%E3%84%B9</link>
            <guid>https://velog.io/@yuze_dbwm/%EC%8B%A0%ED%88%AC-%ED%94%84%EB%94%94%EC%95%84-%EB%84%88%EA%B0%80-%EB%82%B4%EB%A0%A4%EB%9D%BC-%EC%8B%9C%EC%84%B8%EB%8D%B0%EC%9D%B4%ED%84%B0...%E3%84%B9</guid>
            <pubDate>Thu, 02 Oct 2025 04:32:54 GMT</pubDate>
            <description><![CDATA[<p>안녕하세요...
오늘은 실시간 체결가로 시세를 된 사륜구동 CTO YUZE입니다.</p>
<BR>
이 과정에서 많은 시행착오가 있었고, 그 과정에 대해서 공유해 드리려고 글로 남깁니다.

<br>
<br>


<p>저희 서비스의 시세데이터 아키텍처는 다음과 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/af96394e-cf9a-47ca-8d4d-4c0a04a6feae/image.png" alt=""></p>
<p>저희 서비스는 유저가 전략을 생성하고, 전략마다 pod 1개가 할당되는 구조입니다.</p>
<p>따라서, pod가 스스로 전략에 대한 조건을 체크하기 위해서 중앙에서 시세를 내려줘야 합니다.</p>
<p>프로젝트 모듈 중, 시세 데이터를 수집해 pod에게 뿌려주는 모듈을 market-service로 정의했고</p>
<p>한국투자증권의 체결 데이터를 구독해서, 실시간 체결가를 1분봉으로 계산하는 구조로 아키텍처를 구성했습니다.</p>
<BR>
<BR>
<BR>


<h3 id="구현-방법">구현 방법</h3>
<hr>
<ul>
<li>중앙 시세 서버에 체결가 시세가 내려옴</li>
<li>중앙 시세 서버에서 1분봉으로 만든다</li>
<li>보조지표를 계산한 다음에 전략 pod들한테 뿌린다</li>
</ul>
<BR>
<BR>
<BR>




<h3 id="실시간으로-체결가-가져오기">실시간으로 체결가 가져오기</h3>
<hr>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/8970ad72-1a38-46c7-9f06-c039f264356e/image.png" alt=""></p>
<p>  <a href="https://apiportal.koreainvestment.com/apiservice-apiservice?/tryitout/H0STCNT0">한국투자증권 실시간 체결가</a></p>
<p>위 api를 통해서 한국투자증권 체결가를 구독하면, 체결가가 실시간으로 내려옵니다.
  <br></p>
<h3 id="한투-api-틱단위로-제공해주는-것">한투 api 틱단위로 제공해주는 것</h3>
<hr>
<p>주요 필드 매핑</p>
<pre><code class="language-java">  class ResponseBody:
    MKSC_SHRN_ISCD: str    #유가증권 단축 종목코드
    STCK_CNTG_HOUR: str    #주식 체결 시간
    STCK_PRPR: float    #주식 현재가
    PRDY_VRSS_SIGN: str    #전일 대비 부호
    PRDY_VRSS: float    #전일 대비
    PRDY_CTRT: float    #전일 대비율
    WGHN_AVRG_STCK_PRC: float    #가중 평균 주식 가격 (VWAP)
    STCK_OPRC: float    #주식 시가
    STCK_HGPR: float    #주식 최고가
    STCK_LWPR: float    #주식 최저가
    ASKP1: float    #매도호가1
    BIDP1: float    #매수호가1
    CNTG_VOL: float    #체결 거래량
    ACML_VOL: float    #누적 거래량
    ACML_TR_PBMN: float    #누적 거래 대금
    SELN_CNTG_CSNU: float    #매도 체결 건수
    SHNU_CNTG_CSNU: float    #매수 체결 건수
    NTBY_CNTG_CSNU: float    #순매수 체결 건수
    CTTR: float    #체결강도
    SELN_CNTG_SMTN: float    #총 매도 수량
    SHNU_CNTG_SMTN: float    #총 매수 수량
    CCLD_DVSN: str    #체결구분
    SHNU_RATE: float    #매수비율
    PRDY_VOL_VRSS_ACML_VOL_RATE: float    #전일 거래량 대비 등락율
    OPRC_HOUR: str    #시가 시간
    OPRC_VRSS_PRPR_SIGN: str    #시가대비구분
    OPRC_VRSS_PRPR: float    #시가대비
    HGPR_HOUR: str    #최고가 시간
    HGPR_VRSS_PRPR_SIGN: str    #고가대비구분
    HGPR_VRSS_PRPR: float    #고가대비
    LWPR_HOUR: str    #최저가 시간
    LWPR_VRSS_PRPR_SIGN: str    #저가대비구분
    LWPR_VRSS_PRPR: float    #저가대비
    BSOP_DATE: str    #영업 일자
    NEW_MKOP_CLS_CODE: str    #신 장운영 구분 코드
    TRHT_YN: str    #거래정지 여부
    ASKP_RSQN1: float    #매도호가 잔량1
    BIDP_RSQN1: float    #매수호가 잔량1
    TOTAL_ASKP_RSQN: float    #총 매도호가 잔량
    TOTAL_BIDP_RSQN: float    #총 매수호가 잔량
    VOL_TNRT: float    #거래량 회전율
    PRDY_SMNS_HOUR_ACML_VOL: float    #전일 동시간 누적 거래량
    PRDY_SMNS_HOUR_ACML_VOL_RATE: float    #전일 동시간 누적 거래량 비율
    HOUR_CLS_CODE: str    #시간 구분 코드
    MRKT_TRTM_CLS_CODE: str    #임의종료구분코드
    VI_STND_PRC: float    #정적VI발동기준가</code></pre>
<p> 시세데이터가 위와같이 틱단위로 내려오게 되면, 이것을 체결가를 <code>STCK_CNTG_HOUR(체결시간)</code>을 쌓았다가 1분 단위로 분봉 데이터를 만들어, 보죄표를 계산합니다.</p>
<p>저희가 계산해야되는 보조지표는, </p>
<h3 id="틱-단위-체결-데이터로-1분봉-만들기">틱 단위 체결 데이터로 1분봉 만들기</h3>
<hr>
<p>[Step  1] 시세 데이터를 받아온 다음에 1분봉 만드는 로직 부르기
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/b8cb0388-1656-4814-bffa-5b6af1356d9e/image.png" alt=""></p>
<br>
<br>



<p>[Step 2] 1분봉을 만든다
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/8add17af-3c8e-4754-ab1b-a55aa1a85c46/image.png" alt=""></p>
<h4 id="update_candle-동작-방식">update_candle() 동작 방식</h4>
<p>(1) 같은 분일 때</p>
<ul>
<li>high: 현재 고가와 비교해서 더 높은 값으로 갱신</li>
<li>low: 현재 저가와 비교해서 더 낮은 값으로 갱신</li>
<li>close: 최신 체결가로 갱신</li>
<li>volume: 누적 합</li>
<li>vwap_num, vwap_den: 거래량 가중 평균가 계산을 위한 값 누적</li>
</ul>
<br>
<br>

<p>(2) 분이 바뀌었을 때</p>
<ul>
<li>직전 current_candle을 closed로 확정</li>
<li>candles_1m.append(closed) 로 1분봉 리스트에 저장</li>
<li>새로운 current_candle을 초기화 (open, high, low, close = 현재 체결가)</li>
</ul>
<p>[step 3]  closed_candle이 만들어졌다면(1분봉이 만들어졌다면) 보조 지표계산 <code>on_candle</code></p>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/d633dffb-53e2-4a13-ab73-e65c0733b262/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/656e1601-ec22-4744-a9c3-f196ea5e06c5/image.png" alt="">
분봉이 보조지표가 원하는 만큼 모였을 때, 지표를 계산해서 카프카 Producer로 발행</p>
<p>[step 4] 카프카 프로듀서 발행</p>
<ul>
<li>카프카 도커 컴포즈
경량 카프카를 사용해서, 간단하게 로컬에서 카프카 테스트를 수행
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/40a36f30-0af8-497f-8e38-243d38b4fcf5/image.png" alt=""></li>
</ul>
<ul>
<li>카프카 토픽의 메시지로 발행하는 코드 
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/49ee6519-44a8-4f1f-bf4d-9af4eab6040f/image.png" alt=""></li>
</ul>
<ul>
<li>테스트를 위한 카프카 컨슈머 코드
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/0f182e0b-e29e-4365-87dd-c9c5f289f958/image.png" alt=""></li>
</ul>
<ul>
<li>실행 결과
분봉 단위로 보조지표가 잘 계산돼서 컨슈머들이 받고 있다
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/eb67873b-137e-4f20-8b55-8857608e5c66/image.png" alt=""></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[신투 프디아] 일어난 자만 발언권을 얻는 회의... 이제 디자인, 와이어프레임 회의를 곁들인]]></title>
            <link>https://velog.io/@yuze_dbwm/%EC%8B%A0%ED%88%AC-%ED%94%84%EB%94%94%EC%95%84-TEAM-%EC%82%AC%EB%A5%9C%EA%B5%AC%EB%8F%99-%EC%9D%BC%EC%96%B4%EB%82%9C-%EC%9E%90%EB%A7%8C-%EB%B0%9C%EC%96%B8%EA%B6%8C%EC%9D%84-%EC%96%BB%EB%8A%94-%ED%9A%8C%EC%9D%98...-%EC%9D%B4%EC%A0%9C-%EB%94%94%EC%9E%90%EC%9D%B8-%EC%99%80%EC%9D%B4%EC%96%B4%ED%94%84%EB%A0%88%EC%9E%84-%ED%9A%8C%EC%9D%98%EB%A5%BC-%EA%B3%81%EB%93%A4%EC%9D%B8</link>
            <guid>https://velog.io/@yuze_dbwm/%EC%8B%A0%ED%88%AC-%ED%94%84%EB%94%94%EC%95%84-TEAM-%EC%82%AC%EB%A5%9C%EA%B5%AC%EB%8F%99-%EC%9D%BC%EC%96%B4%EB%82%9C-%EC%9E%90%EB%A7%8C-%EB%B0%9C%EC%96%B8%EA%B6%8C%EC%9D%84-%EC%96%BB%EB%8A%94-%ED%9A%8C%EC%9D%98...-%EC%9D%B4%EC%A0%9C-%EB%94%94%EC%9E%90%EC%9D%B8-%EC%99%80%EC%9D%B4%EC%96%B4%ED%94%84%EB%A0%88%EC%9E%84-%ED%9A%8C%EC%9D%98%EB%A5%BC-%EA%B3%81%EB%93%A4%EC%9D%B8</guid>
            <pubDate>Wed, 17 Sep 2025 13:32:44 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>&quot;이 글은 알파코에서 진행되는 [신한투자증권] 프로디지털아카데미 과정 중, 김송아 강사님과 함께하는 &#39;파이널 프로젝트&#39;를 기반으로 작성되었습니다&quot;</p>
</blockquote>
<br>
<br>


<p>안녕하세요, 팀 사륜구동의 CTO겸 막스 베르스타펜을 맡고 있는 블로그 주인장 YUZE입니다
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/36c3a030-5730-49fb-a431-578a0fee485f/image.png" alt="">
(저희 팀에는 르끌레르, 러셀, 피아스트리가 있습니다.)</p>
<br>

<p>프로디지털 아카데미 &quot;파이널 프로젝트&quot;가 본격적으로... 아주 본격적으로 시작 되었습니다.</p>
<p>저희 팀이 진짜 몰입을 하고 있는데요...  저희 팀 좀 자랑해보겠습니다.</p>
<br>
<br>
<br>

<h3 id="2025-09-15--2025-09-173일"><strong>2025-09-15 ~ 2025-09-17(3일)</strong></h3>
<hr>
<p>스프린트 기간 동안 한 것</p>
<ul>
<li>기능 명세</li>
<li>와이어프레임</li>
<li>디자인 회의</li>
</ul>
<BR>


<p>저희 팀은 프론트엔드가 한 명(aka 조지 러셀)이기 때문에, 디자인을 잘 해가는 것이 중요합니다.</p>
<p>사실 제가 이 팀에서 <strong>디자이너</strong>(오랜 추구미)를 맡고있다는 사실!</p>
<p>제가 디자인을 해 가면,
저희 PM(르끌레르)님, 프론트 리더(러셀), 백엔드 리더(피아스트리) 님들 께서 디자인에 대해, 어떤 것이 더 예쁘고 사용자 친화적일지 회의를 하는 구조로 진행했습니다.</p>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/ba71940f-b2ba-4c3f-84e7-e5e7798306e7/image.png" alt=""></p>
<ul>
<li>발언권을 얻기 위해 일어난 <strong>백엔드 3인방...</strong></li>
</ul>
<p>백엔드 3인방은... 어떤 디자인이 사용자 친화적인지 프로듀스 101처럼 <strong>프론트 리더 조지 러셀</strong>에게.... 컨펌을 받고 있습니다.</p>
<br>
<br>


<h3 id="1-유저가-편하다고-느낄만한-uiux란"><strong>(1) 유저가 편하다고 느낄만한 UI/UX</strong>란?</h3>
<hr>
<p>유저의 입장에서 고민하면서, 아래와 같은 생각을 기반으로 토론했습니다.</p>
<ul>
<li>유저가 이 화면에서 <strong>기대</strong>하는 것은 무엇일까</li>
<li><strong>Depth가 너무 깊으면 불편하다</strong></li>
<li><strong>왼쪽 대각선 위에서부터 시선이 떨어지니까</strong> 시선에 맡게 화면 구성을 해야된다</li>
</ul>
<br>
<br>


<h3 id="2-고민-지점--전략-생성-유저-플로우와-디자인-관련-고민">(2) 고민 지점 : 전략 생성 유저 플로우와 디자인 관련 고민</h3>
<hr>
<h4 id="1-전략-생성-페이지의-팔레트는-왼쪽-오른쪽-어디에-놓을까">1. 전략 생성 페이지의 <strong>팔레트</strong>는 왼쪽, 오른쪽 어디에 놓을까?</h4>
<p>  <img src="https://velog.velcdn.com/images/yuze_dbwm/post/d1cb7ded-a40f-4848-9a88-3c5a13084150/image.png" alt=""></p>
<ul>
<li>전략생성 페이지는 위와 같이 <strong>캔버스</strong>컨셉</li>
<li>팔레트에서 블록과 종목을 선택 → 캔버스에 반영되는 구조
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/ff45754b-db2b-4099-835a-f4a0c7b60213/image.png" alt=""></li>
<li><strong>but!</strong> 왼쪽에는 이미 <strong>네비게이션을 위한 사이드바</strong>가 있어서,  팔레트를 왼쪽에 두는 게 네비게이션바가 두개인 거 같아서 실제로 적용해보니 어색했다.</li>
</ul>
<BR>

<blockquote>
</blockquote>
<p>나 : 그렇다면 팔레트를 오른쪽에 두자
팀원1 : 사람은 왼쪽에서 오른쪽으로 시선이 가기 때문에, 오른쪽에서 나오는 게 어색하다
팀원2 : 그럼 왼쪽에 두자
팀원3 : 둘이 같이 나와서 네비게이션바가 너무 어색함
(무한루프)</p>
<BR>
<BR>


<h4 id="2-사용자의-모든-전략을-대시보드에서-다-보여주는것은-이상하다">2. 사용자의 모든 전략을 대시보드에서 다 보여주는것은 이상하다.</h4>
<p>저희 서비스에서는, 전략을 생성하는 것과, 실행하는 것 두 가지 액션이 있습니다.
<br>
따라서, 실행중이지 않은 전략도 있는데, 이 모든 것을 대시보드에서 다 보여주는 것은 너무 정보가 많다는 문제가 있습니다</p>
<p>제가 만약 유저라면, 대시보드에서는 <strong>전략에 대한 전체적인 요약</strong>을 보고싶을 거 같다고 생각했습니다.</p>
<blockquote>
<p>라우팅 네비게이션 바에는 <strong>대시보드</strong>랑 <strong>전략 생성</strong> 두 가지만 존재하는데... 어떻게 묶어야지 더 자연스러운 흐름일까?</p>
</blockquote>
<BR>
<BR>


<h3 id="3-팀원들과-오랜-회의-끝에-나온-화면-재구성">(3) 팀원들과 오랜 회의 끝에 나온 화면 재구성</h3>
<hr>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/3772ff18-9345-4285-92f4-c214f984db56/image.png" alt="와이어프레임"></p>
<p>토론 끝에 나온, 피아스트리가 그려준 와이어프레임입니다.</p>
<h4 id="1-팔레트-위치-문제">1. 팔레트 위치 문제</h4>
<p>사용자의 시선은 왼쪽에서 시작하는데, 팔레트 때문에 네비게이션 바가 두 개 겹쳐 보여서 어떻게 구성해야될지 고민
      #### [해결방안]</p>
<ul>
<li><p>팔레트는 전략 생성 시에만 필요한 것이기 때문에, 팔레트를 좌측 플로팅으로 띄워 네비게이션 중복 문제와 UI 깊이 문제를 해소</p>
<BR>


</li>
</ul>
<h4 id="2-대시보드-정보-과잉">2. 대시보드 정보 과잉</h4>
<p>   대시보드에 사용자의 모든 전략을 다 노출하면 정보량이 과도해져 가독성이 떨어짐</p>
<h4 id="해결방안">[해결방안]</h4>
<ul>
<li>별도의 전략 탭을 생성</li>
<li>전략 탭에서는 사용자의 모든 전략을 한눈에 확인하도록 구성</li>
<li>전략 탭에서 <strong>전략 생성</strong> 버튼 → 캔버스 이동
 2번 클릭 이내로 주요 액션 가능, depth 과도하게 깊어지지 않음</li>
</ul>
<BR>
<BR>



<h3 id="결론-전략-페이지-디자인-초안">[결론] 전략 페이지 디자인 초안</h3>
<hr>
<h4 id="변경-전---1안">변경 전 - (1안)</h4>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/a573f7d5-a155-477b-9480-4f2d48561fa2/image.png" alt=""></p>
<h4 id="변경-전---2안">변경 전 - (2안)</h4>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/ff45754b-db2b-4099-835a-f4a0c7b60213/image.png" alt=""></p>
<br>
<br>


<h4 id="변경-후">변경 후</h4>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/280d972f-a944-49e0-bf9c-34a301e312d1/image.png" alt=""></p>
<br>
<br>
<br>

<br>
<br>
<br>

<hr>
<h3 id="ps-칭찬감옥">ps. 칭찬감옥</h3>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/90e6698e-a4ce-4810-b631-e45482c44ad3/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[신투 프디아] 기획 고도화]]></title>
            <link>https://velog.io/@yuze_dbwm/%EC%8B%A0%ED%88%AC-%ED%94%84%EB%94%94%EC%95%84-%EA%B8%B0%ED%9A%8D-%EA%B3%A0%EB%8F%84%ED%99%94</link>
            <guid>https://velog.io/@yuze_dbwm/%EC%8B%A0%ED%88%AC-%ED%94%84%EB%94%94%EC%95%84-%EA%B8%B0%ED%9A%8D-%EA%B3%A0%EB%8F%84%ED%99%94</guid>
            <pubDate>Sun, 14 Sep 2025 13:30:07 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>&quot;이 글은 알파코에서 진행되는 [신한투자증권] 프로디지털아카데미 과정 중, 김송아 강사님과 함께하는 &#39;파이널 프로젝트&#39;를 기반으로 작성되었습니다&quot;</p>
</blockquote>
<br>
<br>


<p>안녕하세요, 팀 사륜구동의 CTO겸 막스 베르스타펜을 맡고 있는 블로그 주인장 YUZE입니다.</p>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/36c3a030-5730-49fb-a431-578a0fee485f/image.png" alt="">
(저희 팀에는 <strong>PM</strong> 르끌레르, <strong>프론트 리더</strong> 조지 러셀, <strong>백엔드 리더</strong> 피아스트리가 있습니다.)</p>
<p>프로디지털 아카데미 &quot;파이널 프로젝트&quot;에 맞춰서, 1주차 기획에 대한 고도화 내용을 한 번 작성해보려고 합니다.</p>
<br>
<br>
<br>
<br>
<br>
<br>


<h3 id="0-어떤-방향성을-가져갈까">(0) 어떤 방향성을 가져갈까?</h3>
<hr>
<p>저희 팀은 프로젝트 방향성을 잡기 위해 여러 증권사의 MTS를 분석했습니다.</p>
<p>그 과정에서 공통적으로 제공되는 기능인 <strong>감시 주문</strong>을 확인할 수 있었지만, 제공되는 <strong>조건이 제한적</strong>이라는 점을 발견했습니다.</p>
<p>저희는 해당 조건 이외의 지표들을 더해서, 투자자들이 더 <strong>개인화 된 자동매매</strong>를 제공하면 좋겠다고 생각을 하면서 방향을 잡아갔습니다.</p>
<br>
<br>
<br>




<h3 id="1-우리의-주제는">(1) 우리의 주제는?</h3>
<hr>
<ol>
<li><p>현재의 트레이딩 툴에서는 다양한 지표로 매매를 자동화
할 수 없습니다.</p>
</li>
<li><p>더 다양한 지표로 트레이딩을 하기 위해서는, 증권사 open api를 연결해서 자동 매매를 해야되는데 전문 지식 + 개발 역량 + 인프라 환경이 필수라, 초보 투자자가 쉽게 접근하기 어려운 분야입니다.</p>
</li>
</ol>
<p>따라서 저희팀은, &#39;노코드 + 자동 매매 실행&#39;이라는 결합을 통해, <strong>기존 MTS, HTS, WTS 사용자들에게 더 다양한 지표로 자동매매를 수행할 수 있도록 도움을 주고자 프로젝트를 기획했습니다</strong>.</p>
<br>
<br>
<br>




<h3 id="2-어떤-지표가-알고리즘-트레이딩에-많이-사용될까">(2) 어떤 지표가 알고리즘 트레이딩에 많이 사용될까?</h3>
<hr>
<h4 id="1-이동평균선">1. 이동평균선</h4>
<ul>
<li>가장 기본적이고 가장 많이 쓰이는 지표.</li>
<li><strong>추세 기반 전략</strong>의 뼈대.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/4976a369-8515-430e-ace7-08074febc3db/image.png" alt="이동평균선"></p>
<p>🔸 주황색(골든 크로스) : 단기 이동평균선이 장기 이동평균선을 위로 돌파할 때 발생
해석: 상승 추세로 전환할 가능성이 크다고 봐서 매수 신호로 많이 씀.</p>
<p>🔸 노란색(데드 크로스) : 단기 이동평균선이 장기 이동평균선을 아래로 돌파할 때 발생
해석: 하락 추세 전환 신호로 보고 매도 신호로 많이 씀.</p>
<h3 id="q-골든-크로스-데드-크로스는-어떤-봉-단위로-보는-게-좋을까---우리-서비스에서-어떻게-살릴까">Q. 골든 크로스, 데드 크로스는 어떤 봉 단위로 보는 게 좋을까? -&gt; 우리 서비스에서 어떻게 살릴까?</h3>
<h4 id="1-일봉-기준">(1) 일봉 기준</h4>
<p>장점: 신뢰성 높음, 노이즈 적음
단점: 신호 늦음, 이미 많이 오른 뒤 매수 신호 발생할 수 있음
적합: 스윙·중장기 투자자</p>
<h4 id="2-분봉-기준">(2) 분봉 기준</h4>
<p>장점: 빠른 진입/청산 가능, 단타/데이 트레이딩 적합
단점: 잦은 오신호, 손절 필수, 보조지표 필수
적합: 단기·데이 트레이더</p>
<blockquote>
<h3>우리 서비스에서는 ?</h3> 우리는 사용자들에게, 분봉과 일봉 둘 다 제공 할 예정이다. 조건 블록에서 일봉과 분봉 기준으로 선택을 할 수 있게 해서, 보다 개인화 된 조건을 제공하기로 결정했다.
</blockquote>
<br>
<br>


<h4 id="2-rsi-relative-strength-index">2. RSI (Relative Strength Index)</h4>
<ul>
<li><strong>모멘텀</strong> 지표 중 사용 빈도가 가장 높음.</li>
<li>과매수(&gt;70), 과매도(&lt;30) 구간을 조건으로 매매.</li>
<li>단독으로도 쓰이지만, 이동평균과 조합해 “필터”로 자주 활용됨</li>
</ul>
<h4 id="3-macd-moving-average-convergence-divergence">3. MACD (Moving Average Convergence Divergence)</h4>
<ul>
<li>이동평균을 응용한 추세 + 모멘텀 복합 지표.</li>
<li>MACD 라인이 시그널 라인을 돌파할 때 진입/청산 신호.</li>
</ul>
<h4 id="4-볼린저-밴드-bollinger-bands">4. 볼린저 밴드 (Bollinger Bands)</h4>
<ul>
<li><strong>변동성</strong> 기반 지표 중 가장 대표적.</li>
<li>가격이 상단 밴드를 돌파하면 과매수/돌파 신호, 하단 밴드 터치 시 과매도/반등 신호로 활용.</li>
<li>변동성 돌파 전략, 리스크 관리 필터로도 많이 쓰임.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/4f09819f-b9a7-4133-82e3-4937c02075c9/image.png" alt="볼린저 밴드"></p>
<p>빨간 원: 하단 밴드 돌파 → 과매도 구간 → 기술적 반등 가능성.
파란 원: 중심선 돌파 + 상단 밴드 근접 → 단기 과열 가능성 → 차익 실현/매도 신호로 해석 가능.</p>
<h3 id="q-볼린저밴드-봉은-뭘-써야될까---우리-서비스에서-어떻게-살릴까">Q. 볼린저밴드 봉은 뭘 써야될까? -&gt; 우리 서비스에서 어떻게 살릴까?</h3>
<p>** 1. 일봉 (Daily) : 가장 일반적이고 많이 쓰임**</p>
<ul>
<li>장기 투자자·스윙 트레이더들이 추세 전환이나 과매수/과매도 구간을 확인할 때 사용</li>
<li>장점: 노이즈 적고 신뢰도 높음.</li>
<li>단점: 신호가 늦음, 이미 가격이 많이 움직인 뒤 알 수 있음.</li>
</ul>
<p>** 2. 주봉/월봉 (Weekly/Monthly)** </p>
<ul>
<li>중장기 추세 확인용.</li>
<li>기관·포트폴리오 매니저들이 큰 그림 확인할 때 씀.</li>
<li>신호 빈도는 적지만, 의미는 큼.</li>
</ul>
<p><strong>3. 분봉</strong></p>
<ul>
<li>단타·데이 트레이딩에 자주 활용.</li>
<li>밴드 돌파(상단/하단) → 진입/청산 시그널로 쓰임.</li>
<li>장점: 빠른 신호로 단타 대응 가능.</li>
<li>단점: 가짜 신호 많음 → RSI, 거래량 같은 보조지표 꼭 병행해야 함.</li>
</ul>
<blockquote>
 <h3>우리 서비스에서는 ?</h3>
분봉과 일봉만 제공할 예정입니다.
<br>
월봉과 주봉은 장기적인 시그널에 해당합니다. 장기 시그널을 포함할 경우, 유저 플로우가 불필요하게 복잡해질 수 있습니다.
<br>
특히 프로젝트의 기본 플로우는 '매일매일 감시'이기 떄문에, 사용 맥락에 맞추는 것이 자연스럽다고 판단했습니다.
</blockquote>
<br>
<br>


<h4 id="5-adx-average-directional-index">5. <strong>ADX (Average Directional Index)</strong></h4>
<ul>
<li>추세의 “강도”를 수치화한 지표.</li>
<li>ADX &gt; 25 → 추세 강함, 추세추종 전략 유효.</li>
<li>ADX &lt; 20 → 횡보장, 오실레이터 계열 전략 유효.</li>
</ul>
<br>
<br>
<br>
<br>
<br>


<h3 id="3-기획-멘토링-후기">(3) 기획 멘토링 후기</h3>
<hr>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/97ff530c-b099-48a2-af2f-7ae12d52baa1/image.png" alt=""></p>
<p>프디아 시작하면서 생긴 목표가 &#39;말 잘하기&#39;였는데, 마침 좋은 기회가 생겨서 기획 PT를 제가 하게 됐습니다.</p>
<p>현업에 계신 분들께 브리핑을 해보는 것은 처음이라, (매우) 긴장 됐지만.... 열심히 연습한대로 잘 전달드리려고 해보았습니다.</p>
<br>

<p>저희 팀이 받은 피드백은, <strong>조건을 조합</strong>하는 과정을 정말 <strong>사용자 친화적인 UI/UX</strong>로 풀어내는 게 이번의 가장 큰 TASK 입니다.</p>
<br>
<br>



<br>
<br>
<br>
<br>
<br>


<hr>
<p>사륜구동 팀 화이팅! (러셀, 르끌레르, 피아스트리 보고있나!)</p>
<h3 id="ps-앞으로-방향성">ps. 앞으로 방향성...</h3>
<blockquote>
<p>항상 프로젝트를 할 때마다, 기록을 체계적으로 남기지 않아서 결과물이 남지 않아 아쉬움이 많았습니다.
따라서, 이번 파이널 프로젝트에서는 많은 기술적 도전과 고민이 있을텐데 그 내용에 대해서 잘 풀어 보고싶습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[페어프로그래밍 후기]]></title>
            <link>https://velog.io/@yuze_dbwm/%ED%8E%98%EC%96%B4%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%ED%9B%84%EA%B8%B0</link>
            <guid>https://velog.io/@yuze_dbwm/%ED%8E%98%EC%96%B4%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%ED%9B%84%EA%B8%B0</guid>
            <pubDate>Wed, 06 Aug 2025 04:55:25 GMT</pubDate>
            <description><![CDATA[<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/42576?language=java">완주하지 못한 선수</a></p>
<p>페어프로그래밍 후기를 남기고자 한다.</p>
<p>먼저 &#39;완주하지 못한 선수&#39; 라는 코딩테스트 문제를 풀었고, 10분의 시간 동안 팀끼리 문제를 분석하며 어떻게 풀이를 할 지 협의하는 과정을 거쳤다.</p>
<p>실제로, 나는 해당 문제를 &#39;정렬&#39;을 통해 푼 경험이 있는데, O(n)을 지켜서 문제를 풀어야 하지만, 나는 기존에 O(nlogn)의 시간 복잡도로 풀었고, 성공했다.</p>
<p>따라서, 이번에는 팀원들과 협의해서 &#39;해시&#39;를 통해 참가자와 완주자가 길이가 1 차이 나는 것을 이용해서 풀기로 결정했다.</p>
<p>서로 드라이버와 네비게이터가 되어서</p>
<p>드라이버를 5분씩 맡으며, 3명이서 돌아가며 코딩테스트 문제를 풀었다.</p>
<p>사공이 많으면, 배가 산으로 간다는 말이 있는데, 실제로 서로 사공들끼리 처음에 변수명 부터 시작해서, 어떻게 map을 초기화 할 지 부터 난관이었다.</p>
<p>그렇지만, 점점 적응되고 역할에 적응해가다 보니, 서로의 의견을 수용하면서 하다보니 수월하게 문제 풀이를 완료할 수 있었다.</p>
<p>페어프로그래밍은 너무 재밌다고 생각했다. 성취감이 컸다고 느꼈습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[INSERT 했는데 안 보인다고? - 멀티 스레드 환경의 트랜잭션 격리 수준 이슈 분석 (2)]]></title>
            <link>https://velog.io/@yuze_dbwm/INSERT-%ED%96%88%EB%8A%94%EB%8D%B0-%EC%95%88-%EB%B3%B4%EC%9D%B8%EB%8B%A4%EA%B3%A0-%EB%A9%80%ED%8B%B0-%EC%8A%A4%EB%A0%88%EB%93%9C-%ED%99%98%EA%B2%BD%EC%9D%98-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EA%B2%A9%EB%A6%AC-%EC%88%98%EC%A4%80-%EC%9D%B4%EC%8A%88-%EB%B6%84%EC%84%9D-2</link>
            <guid>https://velog.io/@yuze_dbwm/INSERT-%ED%96%88%EB%8A%94%EB%8D%B0-%EC%95%88-%EB%B3%B4%EC%9D%B8%EB%8B%A4%EA%B3%A0-%EB%A9%80%ED%8B%B0-%EC%8A%A4%EB%A0%88%EB%93%9C-%ED%99%98%EA%B2%BD%EC%9D%98-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EA%B2%A9%EB%A6%AC-%EC%88%98%EC%A4%80-%EC%9D%B4%EC%8A%88-%EB%B6%84%EC%84%9D-2</guid>
            <pubDate>Fri, 01 Aug 2025 06:50:51 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>🔷 <strong>이번 글에서 다루는 것</strong><br> </p>
</blockquote>
<ol>
<li>Spring Boot의 트랜잭션 처리 구조</li>
<li>MVCC</li>
<li>트랜잭션 격리 수준<blockquote>
</blockquote>
</li>
</ol>
<br>
<br>
<br>
<br>


<p>본격적으로 들어가기에 앞서, 트랜잭션 격리수준과, 격리 수준에 따른 이상현상을 간단히 표로 정리한다.</p>
<h3 id="트랜잭션-격리-수준-종류">트랜잭션 격리 수준 종류</h3>
<hr>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/a36704ae-cfe9-4fd8-a358-960dae400f7e/image.png" alt=""></p>
<br>
<br>


<h3 id="isolation-이상-현상">Isolation 이상 현상</h3>
<hr>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/3e0237ed-66e5-4dda-8872-53502ce83497/image.png" alt=""></p>
<br>
<br>
<br>
<br>



<p>지난 포스팅에서, 메인스레드에서 조회가 됐지만, 새로 생긴 서브 스레드에서 조회가 안 되는 현상이 있었다.</p>
<p>그 이유에 대해 나름대로 분석했는데, 다음과 같은 이상 포인트들이 있었다.</p>
<br>


<h3 id="이상-현상과-의심할-포인트">이상 현상과, 의심할 포인트</h3>
<hr>
<ol>
<li><code>@Transactional</code> 이 감싸는 범위가 너무 큼</li>
<li>기존 스레드에 <code>＠Transactional</code>이 붙어있더라도, 그 안에서 스레드를 새로 생성된다면 기존 스레드와 새로운 스레드는 같은 트랜잭션을 공유하지 않음 </li>
</ol>
<br>
<br>
<br>


<blockquote>
<p>🔷 <strong>용어 정리</strong>
<br>다음 설명에서, 서블릿 컨테이너의 워커 스레드를 그냥 <code>스레드</code>라고 부르면서 설명하도록 하겠음</p>
</blockquote>
<h3 id="springboot가-db에-접근하고-받아오는-방법">SpringBoot가 DB에 접근하고 받아오는 방법</h3>
<hr>
<p><strong>[step 1] Spring 스레드가 로직 처리 중 DB 접근 필요</strong></p>
<ul>
<li>웹 요청이 들어오면 Spring의 스레드 해당 요청을 처리하다가 DB에 접근할 필요가 생김</li>
</ul>
<p><strong>[step 2] Connection Pool(HikariCP 등)에서 Idle 상태 커넥션 하나 빌림</strong></p>
<ul>
<li>Spring Boot는 기본적으로 HikariCP와 같은 커넥션 풀을 사용</li>
<li>커넥션 풀에서 유휴 상태의 커넥션을 스레드에 빌려줌</li>
<li>같은 트랜잭션 내에서는 동일한 커넥션이 유지되어, 여러 DB 작업이 하나의 커넥션으로 처리</li>
</ul>
<p><strong>[step 3] 빌려온 커넥션으로 DB에 접근</strong></p>
<ul>
<li>커넥션 풀에서 빌린 커넥션은 이미 MySQL 서버의 포그라운드 스레드와 TCP 소켓 연결이 되어 있는 상태</li>
</ul>
<p><strong>[step 4] 쿼리 실행</strong></p>
<ul>
<li>빌린 커넥션을 통해 SQL 쿼리를 실행하면, MySQL 서버의 포그라운드 스레드가 해당 쿼리를 처리</li>
<li>결과를 Spring 애플리케이션으로 반환</li>
</ul>
<p><strong>[step 5] 쿼리 결과를 받아옴</strong></p>
<ul>
<li>쿼리 결과를 받아서 애플리케이션이 처리</li>
</ul>
<p><strong>[step 6] 커넥션을 Connection Pool로 반환</strong></p>
<ul>
<li>트랜잭션이 끝나거나, 커넥션 사용이 끝나면 커넥션을 다시 커넥션 풀에 반환</li>
<li>반환된 커넥션은 다음 요청에서 재사용</li>
</ul>
<blockquote>
<p>🔷 요약
Spring Boot에서 <code>@Transactional</code>이 걸리면. . .<br></p>
</blockquote>
<ol>
<li>트랜잭션 단위로 Connection Pool에서 커넥션 1개 빌림</li>
<li>트랜잭션 동안 실행되는 모든 DB 작업은 그 커넥션 하나로 처리</li>
<li>트랜잭션이 끝나면 커넥션은 다시 Connection Pool로 반납<blockquote>
</blockquote>
</li>
</ol>
<br>
<br>
<br>



<h3 id="mvccmulti-version-concurrency-control">MVCC(Multi Version Concurrency Control)</h3>
<hr>
<p>MySQL의 기본 트랜잭션 격리 수준은 <code>REPEATABLE READ</code>다. 이는 트랜잭션이 시작될 때의 스냅샷을 기준으로 조회하기 때문에, 다른 트랜잭션에서 커밋되지 않은 데이터는 조회할 수 없다.</p>
<p>MVCC는 InnoDB가 사용하는 방식으로, <code>언두 로그</code>를 통해 일관된 읽기를 제공한다.</p>
<br>
<br>
<br>


<h3 id="문제-발생-코드-분석">문제 발생 코드 분석</h3>
<hr>
<ul>
<li>문제가 발생한 테스트 코드 전문</li>
</ul>
<pre><code class="language-java">    @Test
    @Transactional
    void 참가자_5명이_동시에_접근할_경우_동시성_이슈가_발생한다() throws InterruptedException {
        // given
        Member member1 = createMember(&quot;user1@example.com&quot;, &quot;UserOne&quot;, &quot;2021001&quot;, &quot;010-1234-5678&quot;, Gender.WOMAN);
        Member member2 = createMember(&quot;user2@example.com&quot;, &quot;UserTwo&quot;, &quot;2021002&quot;, &quot;010-9876-5432&quot;, Gender.WOMAN);
        Member member3 = createMember(&quot;user3@example.com&quot;, &quot;UserThree&quot;, &quot;2021003&quot;, &quot;010-5555-5555&quot;, Gender.MAN);
        Member member4 = createMember(&quot;user4@example.com&quot;, &quot;UserFour&quot;, &quot;1231312&quot;, &quot;010-1232-5555&quot;, Gender.MAN);
        Member member5 = createMember(&quot;user5@example.com&quot;, &quot;UserFive&quot;, &quot;13213123&quot;, &quot;010-5555-2342&quot;, Gender.MAN);
        Member member6 = createMember(&quot;user6@example.com&quot;, &quot;UserSix&quot;, &quot;1121212&quot;, &quot;010-5555-2333&quot;, Gender.MAN);

        MeetingPost meetingPost = createMeetingPost();
        System.out.println(&quot;메인스레드 : &quot; + meetingPostService.findById(meetingPost.getId()).getTitle());

        for (Member member : Arrays.asList(member2, member3, member4, member5, member6)) {
            executorService.submit(() -&gt; {
                try {
                    System.out.println(&quot;새로운 스레드 : &quot; + meetingPostService.findById(meetingPost.getId()).getTitle());
                    participantService.participate(meetingPost.getId(), member.getId());
                }
                catch (Exception e) {
                    System.out.println(e.getMessage());
                }
                finally {
                    latch.countDown();
                }
            });
        }

        latch.await();

        assertThat(participantRepository.findByMeetingPost(meetingPost).size()).isEqualTo(3); // 참가 제한은 3명이어야 함
    }</code></pre>
<br>
<br>
<br>


<ul>
<li>코드를 시각화 하면 다음과 같다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/f7971c4f-45ee-4ab9-974f-d77f6bd5896d/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/f59ae13d-b93e-4ac9-bef9-49c55bbbbffb/image.png" alt=""></p>
<br>
<br>
<br>


<table>
<thead>
<tr>
<th>시간</th>
<th>스레드</th>
<th>작업 내용</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>t1</td>
<td>메인</td>
<td>트랜잭션 시작 + INSERT</td>
<td>아직 커밋 안 됨</td>
</tr>
<tr>
<td>t2</td>
<td>서브</td>
<td>트랜잭션 시작 + SELECT</td>
<td><code>REPEATABLE READ</code>이므로 메인 스레드에서 INSERT 된 쿼리를 볼 수 없음</td>
</tr>
<tr>
<td>t3</td>
<td>메인</td>
<td>SELECT</td>
<td>자신의 트랜잭션이므로 정상 조회</td>
</tr>
</tbody></table>
<p>메인 스레드는 자신의 트랜잭션 안에 있는 데이터를 INSERT하고 조회하기 때문에, 문제 없이 조회가 가능하다.</p>
<p>테스트 코드 스레드(메인 스레드)가 커밋되기 전에, 새롭게 생긴 스레드들이 조회를 하려고 시도한다.</p>
<p>그러나, 새로운 스레드들은 트랜잭션 격리 수준에 따라 다르지만, MYSQL의 default 격리 수준은  <strong><code>REPEATABLE_READ</code></strong> 이기 때문에 commit이 되지 않는 이상 볼 수 없었던 이슈가 생겼었다</p>
<br>
<br>
<br>

<h3 id="해결-방법-트랜잭션-범위-조절">해결 방법: 트랜잭션 범위 조절</h3>
<p>메인 스레드에서 실행되는 createMember, createMeetingPost 같은 메서드에 각각 <code>@Transactional</code>이 붙어 있다면, 각각 독립된 트랜잭션으로 커밋된다.</p>
<p>즉, 테스트 메서드에 <code>@Transactional</code>을 붙이는 대신, 내부 create~ 메서드에만 붙이면 트랜잭션을 더 작게 분할할 수 있다.</p>
<p>이렇게 하면, 테스트 실행 시 각 INSERT가 메서드 종료 시점에 커밋되고, 이후 서브 스레드들이 이 데이터를 조회 가능하게 된다.</p>
<br>
<br>
<br>


<h3 id="결론">결론</h3>
<ul>
<li>스레드는 트랜잭션 컨텍스트를 공유하지 않는다</li>
<li>MySQL은 REPEATABLE READ를 기본으로 하며, MVCC로 인해 커밋 전 데이터는 타 트랜잭션에서 조회되지 않는다</li>
<li>트랜잭션의 범위를 줄이면 커밋 타이밍이 앞당겨져 다른 스레드에서도 조회 가능해진다</li>
</ul>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/87eb573f-98a6-4125-9c7e-de0c252e37fb/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/6475e2fe-a27f-479a-a08f-609018805f8d/image.png" alt=""></p>
<ul>
<li><code>@Transactional</code>을 제거하면, 트랜잭션의 범위를 줄여 작은 단위로 커밋할 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/98d6ce5c-d461-41e1-8685-6fe7f0fb2894/image.png" alt=""></p>
<ul>
<li>각 메서드에 <code>@Transactional</code>이 붙어 있다<ul>
<li><code>createMember(...)</code>, <code>createMeetingPost(...)</code> 등 각 <code>create</code> 함수는 별도의 트랜잭션으로 동작한다.</li>
<li>정상적으로 메서드가 종료되면 해당 트랜잭션은 안전하게 commit된다.</li>
</ul>
</li>
</ul>
<p>따라서 이후 동시성 테스트 로직에서는 이 데이터들이 이미 DB에 반영된 상태로 존재하게 된다.</p>
<ul>
<li>메서드마다 <code>@Transactional</code> 이 붙어있기 때문에, 트랜잭션 메서드가 끝나면, 안전하게 트랜잭션이 commit 된다.</li>
</ul>
<br>
<br>
<br>


<blockquote>
<p>｢도움이 된 자료｣
<a href="https://mariadb.com/docs/server/server-usage/storage-engines/innodb/innodb-undo-log">https://mariadb.com/docs/server/server-usage/storage-engines/innodb/innodb-undo-log</a>
<a href="https://dev.mysql.com/doc/refman/9.0/en/innodb-multi-versioning.html">https://dev.mysql.com/doc/refman/9.0/en/innodb-multi-versioning.html</a>
📘 참고 서적
Real MySQL 8.0 백은빈, 이성욱 지음</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[INSERT 했는데 안 보인다고? - 멀티 스레드 환경의 트랜잭션 격리 수준 이슈 분석 (1)]]></title>
            <link>https://velog.io/@yuze_dbwm/INSERT-%ED%96%88%EB%8A%94%EB%8D%B0-%EC%95%88-%EB%B3%B4%EC%9D%B8%EB%8B%A4%EA%B3%A0-%EB%A9%80%ED%8B%B0-%EC%8A%A4%EB%A0%88%EB%93%9C-%ED%99%98%EA%B2%BD%EC%9D%98-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EA%B2%A9%EB%A6%AC-%EC%88%98%EC%A4%80-%EC%9D%B4%EC%8A%88-%EB%B6%84%EC%84%9D-1</link>
            <guid>https://velog.io/@yuze_dbwm/INSERT-%ED%96%88%EB%8A%94%EB%8D%B0-%EC%95%88-%EB%B3%B4%EC%9D%B8%EB%8B%A4%EA%B3%A0-%EB%A9%80%ED%8B%B0-%EC%8A%A4%EB%A0%88%EB%93%9C-%ED%99%98%EA%B2%BD%EC%9D%98-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EA%B2%A9%EB%A6%AC-%EC%88%98%EC%A4%80-%EC%9D%B4%EC%8A%88-%EB%B6%84%EC%84%9D-1</guid>
            <pubDate>Tue, 17 Jun 2025 04:39:31 GMT</pubDate>
            <description><![CDATA[<br>
<br>

<blockquote>
<p> <strong><h3>🔷 이번 글에서 다루는 것</h3></strong>
이 문제는 동시성 이슈가 발생하는 메서드를 테스트하기 위해 작성한 <strong>테스트 코드</strong>에서 발생한 문제이다.
<br></p>
</blockquote>
<ol>
<li>테스트 환경 및 코드에 대한 개요 설명</li>
<li>테스트 실행 결과 분석</li>
<li>테스트 코드에서 INSERT한 결과가 발생 하지 않는 문제 발생</li>
</ol>
<br>
<br>
<br>

<h2 id="1-테스트-시나리오-및-코드-구성">1. 테스트 시나리오 및 코드 구성</h2>
<h3 id="1-1-테스트-목적">1-1. 테스트 목적</h3>
<hr>
<ul>
<li>5명의 유저가 참여 경합 상황을 가정 → 5개의 스레드로 하나의 동시성 이슈가 메서드와 컬럼에 경합을 유도</li>
<li>제한 인원보다 많은 인원의 유저가 들어오는 문제를 확인</li>
<li>lock을 걸어서 제한 인원 만큼의 인원만 들어올 수 있도록 동시성을 제어하는 것을 목표로 함</li>
</ul>
<br>
<br>
<br>



<h3 id="1-2-테스트-환경">1-2. 테스트 환경</h3>
<hr>
<ul>
<li>DB : MYSQL 8.0 (InnoDB)</li>
<li>Thread : 테스트 메서드에 경합하는 Thread 5개 ⇒ <code>유저 5명</code> 가정</li>
<li>참여 제한 : 최대 3명 (작성자 포함)</li>
</ul>
<br>
<br>
<br>


<h3 id="1-3-기대-결과">1-3. 기대 결과</h3>
<hr>
<ul>
<li>최종 참여 인원 : 작성자 1명 + 참여자 2명</li>
<li>참여 인원 5명이 동시에 접근하더라도 참여자는 2명까지만 허용되어야 함</li>
</ul>
<br>
<br>



<p>기대 결과와 테스트 조건은 위와 같다. 동시성 제어 문제가 잘 해결되는 지 확인하기 위해, 아래와 같이 테스트 코드를 작성했다.</p>
<br>
<br>
<br>


<h3 id="1-4-테스트-코드-구성">1-4. 테스트 코드 구성</h3>
<hr>
<p><strong>🔷 메인 테스트 메서드</strong></p>
<pre><code class="language-java">    @Test
    @Transactional
    void 참가자_5명이_동시에_접근할_경우_동시성_이슈가_발생한다() throws InterruptedException {
        // given
        Member member1 = createMember(&quot;user1@example.com&quot;, &quot;UserOne&quot;, &quot;2021001&quot;, &quot;010-1234-5678&quot;, Gender.WOMAN);
        Member member2 = createMember(&quot;user2@example.com&quot;, &quot;UserTwo&quot;, &quot;2021002&quot;, &quot;010-9876-5432&quot;, Gender.WOMAN);
        Member member3 = createMember(&quot;user3@example.com&quot;, &quot;UserThree&quot;, &quot;2021003&quot;, &quot;010-5555-5555&quot;, Gender.MAN);
        Member member4 = createMember(&quot;user4@example.com&quot;, &quot;UserFour&quot;, &quot;1231312&quot;, &quot;010-1232-5555&quot;, Gender.MAN);
        Member member5 = createMember(&quot;user5@example.com&quot;, &quot;UserFive&quot;, &quot;13213123&quot;, &quot;010-5555-2342&quot;, Gender.MAN);
        Member member6 = createMember(&quot;user6@example.com&quot;, &quot;UserSix&quot;, &quot;1121212&quot;, &quot;010-5555-2333&quot;, Gender.MAN);

        MeetingPost meetingPost = createMeetingPost();

        for (Member member : Arrays.asList(member2, member3, member4, member5, member6)) {
            executorService.submit(() -&gt; {
                try {
                    participantService.participate(meetingPost.getId(), member.getId());
                }
                catch (Exception e) {
                    System.out.println(e.getMessage());
                }
                finally {
                    latch.countDown();
                }
            });
        }

        latch.await();

        assertThat(participantRepository.findByMeetingPost(meetingPost).size()).isEqualTo(3); // 참가 제한은 3명이어야 함
    }</code></pre>
<br>
<br>


<p><strong>🔷 테스트 메서드 - createPost</strong></p>
<pre><code class="language-java"> public MeetingPost createMeetingPost(Member member) {
        return meetingPostService.writePost(
                member.getId(),
                new MeetingPostContent(
                        &quot;스터디 모집합니다!&quot;,
                        &quot;자바 스터디 함께 하실 분 모집합니다. 초보자 환영!&quot;,
                        &quot;NONE&quot;,
                        &quot;3&quot;,
                        &quot;스터디모집&quot;,
                        &quot;2026-02-20T18:00&quot;,
                        &quot;2026-02-25T15:00&quot;,
                        &quot;서울시 강남구 테헤란로 123&quot;,
                        &quot;37.498095&quot;,
                        &quot;127.027610&quot;
                ), new MeetingPostImagesUris(
                        List.of(&quot;uri&quot;)
                ));
    }</code></pre>
<br>
<br>

<p><strong>🔷 테스트 메서드 - createMember</strong></p>
<pre><code class="language-java">  public Member createMember(String email, String name, String studentId, String phone, Gender gender) {
        Member member = Member.create(email, name, studentId, phone, gender, Membership.PRO,
                Department.ART_AND_PHYSICS);
        memberService.createMember(member);
        return member;
    }</code></pre>
<br>
<br>
<br>

<h2 id="2-테스트-실행-결과-및-예상-외의-현상">2. 테스트 실행 결과 및 예상 외의 현상</h2>
<p>동시성 이슈를 제어하지 않은 코드이기 때문에, 참여 가능 인원보다 참여인원이 많기를 기대하면서 테스트를 해보았다. </p>
<p>그러나, 아래와 같이 기대와 다른 결과가 발생했다.</p>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/846e2965-6ff6-4cb5-90aa-fe8f380d95f7/image.png" alt=""></p>
<h3 id="2-1-테스트-결과-요약">2-1 테스트 결과 요약</h3>
<hr>
<ul>
<li><strong>expect</strong> : 참여자가 참여 가능자보다 많아야 한다</li>
<li><strong>actual</strong> : 참여자는 작성자 1명 뿐, 나머지 5명은 모두 실패</li>
</ul>
<p>참여 가능 인원보다 참여한 사람이 많으면 많아야 하지, 왜 아무도 참여를 못했을까 의문이 들어서 error로그를 눈으로 확인해보았다.</p>
<br>
<br>
<br>


<h2 id="3-원인-분석-및-문제-정의">3. 원인 분석 및 문제 정의</h2>
<hr>
<blockquote>
<h3> 🔷 테스트 코드는 다음과 같은 절차를 거친다 </h3>
<br>
</blockquote>
<ol>
<li>member 생성<br></li>
<li>meeting post INSERT + wirter member와 함께 insert<br></li>
<li>마감일에 맞춰서 게시글의 상태를 바꿔주는 이벤트를 예약<br></li>
<li>thread 5개가 participate 동시 호출<br>


</li>
</ol>
<br>

<h3 id="3-1-insert-후-조회-실패-로그-분석">3-1. INSERT 후 조회 실패 로그 분석</h3>
<hr>
<ul>
<li>로그를 확인해보니 DB에 INSERT한 모임 글이 조회가 안 되는 현상</li>
</ul>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/10585834-16e5-4ee5-9319-e7694fb64436/image.png" alt=""></p>
<br>



<ul>
<li>분명 테스트를 위한 데이터를 넣어 놓은 상황인데 위와 같은 예외가 발생</li>
</ul>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/fa894162-2c1d-40a5-b7ce-ebd0e23d65e4/image.png" alt=""></p>
<br>



<ul>
<li>INSERT 쿼리도 나가는 것을 확인할 수 있음</li>
</ul>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/545fd2b0-9c2f-43e4-9eae-f08c8eb56160/image.png" alt=""></p>
<br>

<br>




<p>모임 글이 DB에 INSERT되었음에도, 조회되지 않는 문제가 발생했다.
모임 글을 찾는 메서드를 호출하는 곳은 participate 메서드 안에 있다. 따라서,  스레드별로 모임글을 확인해 보는 작업을 해보도록 하겠다.</p>
<p><br><br><br></p>
<h3 id="3-2-스레드-별-데이터-조회-여부-검증">3-2 스레드 별 데이터 조회 여부 검증</h3>
<hr>
<ul>
<li>테스트 코드가 돌아가는 메인 스레드 (1개)</li>
<li>테스트 코드에서 새로 만드는 스레드 (5개)</li>
</ul>
<p>위와 같은 구조로 되어있기 때문에 아래와 같이 출력 문을 배치하였다.</p>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/9b6a1585-0106-4523-b1b0-1585508e7379/image.png" alt=""></p>
<p><br><br><br></p>
<h3 id="실행-결과">실행 결과</h3>
<p>🔹 메인 스레드에서 조회 성공</p>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/90ea26bf-8062-457d-8fe6-abb650f20a8b/image.png" alt=""></p>
<br>


<p>🔹 새 스레드에서 조회 실패</p>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/0f267613-c5b7-4b27-a4a5-c582285bfb58/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/2ce9e5bc-92d5-43c2-a110-8f582d144a60/image.png" alt=""></p>
<p><br><br></p>
<p>이를 바탕으로 문제를 아래와 같이 정의하고 파고들기로 했다.</p>
<blockquote>
<h3>🔷 문제 정의</h3>
테스트 코드 내에서 INSERT된 데이터가, 새로 생성된 스레드에서 조회되지 않는 현상 발생
<br>
</blockquote>
<br>
<br>
<br>




<hr>
<br>
<br>
<br>


<blockquote>
<h3>🔷 다음 글에서 다룰 것</h3>
</blockquote>
<ol>
<li>MVCC와 트랜잭션 격리 수준에 대한 이해<br></li>
<li>컬럼이 조회 안 되는 이유 분석</li>
</ol>
<p><br><br></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[백엔드 리소스를 Private Subnet에 위치 시켜보자 (2) RDS Private Subnet에 배치]]></title>
            <link>https://velog.io/@yuze_dbwm/%EB%B0%B1%EC%97%94%EB%93%9C-%EB%A6%AC%EC%86%8C%EC%8A%A4%EB%A5%BC-Private-Subnet%EC%97%90-%EC%9C%84%EC%B9%98-%EC%8B%9C%EC%BC%9C%EB%B3%B4%EC%9E%90-2-RDS-Private-Subnet%EC%97%90-%EB%B0%B0%EC%B9%98</link>
            <guid>https://velog.io/@yuze_dbwm/%EB%B0%B1%EC%97%94%EB%93%9C-%EB%A6%AC%EC%86%8C%EC%8A%A4%EB%A5%BC-Private-Subnet%EC%97%90-%EC%9C%84%EC%B9%98-%EC%8B%9C%EC%BC%9C%EB%B3%B4%EC%9E%90-2-RDS-Private-Subnet%EC%97%90-%EB%B0%B0%EC%B9%98</guid>
            <pubDate>Fri, 23 May 2025 06:01:10 GMT</pubDate>
            <description><![CDATA[<h2 id="vpc-생성">VPC 생성</h2>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/87181301-88df-4523-8b41-2d3118a730c7/image.png" alt=""></p>
<br>
<br>


<hr>
<br>
<br>

<h2 id="subnet-생성">Subnet 생성</h2>
<p><strong>Subnet을 VPC에 연결</strong>
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/64a00f1a-17de-4a4a-be25-44b8c7b264c8/image.png" alt=""></p>
<br>
<br>


<p><strong>private subnet-1</strong>
10.0.2.0/24
가용영역 ap-norheast-2a
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/45190556-b8c1-460c-9ef8-3cdb07863a16/image.png" alt=""></p>
<br>
<br>

<p><strong>private subnet-2</strong>
10.0.3.0/24
가용영역 ap-norheast-2c
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/4686b026-0945-4b6b-8631-54941ac70d1c/image.png" alt=""></p>
<br>
<br>


<p><strong>public subnet</strong>
10.0.1.0/24
가용영역 ap-norheast-2c</p>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/e207a1eb-e953-4ecf-a30a-8ff6ec92323f/image.png" alt=""></p>
<br>
<br>



<hr>
<br>
<br>

<h2 id="라우팅-테이블-생성">라우팅 테이블 생성</h2>
<p><strong>private 라우팅 테이블 생성</strong> 
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/5ae1ea2d-71d9-4186-a56d-2793fd0784f4/image.png" alt="">
private subnet을 연결할 route를 생성해서, igw와 연결 된 public subnet과 privates subnet을 나눈다.</p>
<br>
<br>




<p><strong>라우팅 테이블에 private 서브넷 연결</strong> 
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/28b08375-191d-4064-8b4d-d6b4a73b2983/image.png" alt="">
private subnet은 VPC 내부 IP 대역(10.0.0.0/16)에 대해서만 통신이 가능하고, 외부 인터넷으로의 트래픽은 차단되어 있음</p>
<ul>
<li>라우팅 테이블을 통해, private subnet은 VPC 내부의 IP 범위의 대해서만 통신이 가능하게 한다</li>
<li>외부로 나가는 경로는 설정되어 있지 않도록 한다</li>
</ul>
<br>
<br>


<p><strong>public route도 동일한 방법으로 생성</strong></p>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/79ea9ddc-2061-4d8b-a894-3bc22a08db68/image.png" alt="">
생성된 public route가 인터넷과 연결되도록 만들어야 한다. 따라서, igw(인터넷게이트웨이)를 생성해서 public route에 연결해주는 것 까지 해보겠다. </p>
<br>
<br>

<hr>
<br>
<br>


<h2 id="igw-생성">IGW 생성</h2>
<p><strong>IGW를 생성</strong>
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/8c46a456-77e3-4559-95d8-3ec1059813ee/image.png" alt=""></p>
<br>
<br>



<p><strong>IGW를 VPC에 연결</strong>
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/45ce62e9-514a-4228-bc10-cf3388990aec/image.png" alt=""></p>
<br>
<br>

<p><strong>퍼블릭 라우트 테이블에 igw를 통해 외부 네트워크와 연결될 수 있도록 옵션 추가</strong>
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/1c03307f-81e0-4096-910c-9e95b7dadb15/image.png" alt=""></p>
<br>
<br>


<br>
<br>
<br>

<p>따라서, 전체 VPC 전체 구성은 다음과 같이 구성되었다. </p>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/2bde7102-7761-467f-b19e-de6af5a1a98f/image.png" alt=""></p>
<p>여기서 <code>rtb-058d44a00cbf8c4c3</code> 는, VPC를 생성해서 자동으로 생긴 기본 라우팅 테이블이다.</p>
<p>특별하게 라우팅을 만들지 않는 경우, 해당 라우팅 테이블에 연결되고, 이 라우팅 테이블을 수정해서 사용하면 된다.</p>
<p>⇒ 이름을 좀 더 직관적으로 보고자 하여, 실습에서는 기본 라우팅 테이블을 사용하지 않았다.
⇒ subnet에 연결되는 라우팅 테이블 중 하나는, 기본 라우팅 테이블을 사용해도 무방하다. </p>
<br>
<br>

<hr>
<br>
<br>



<h1 id="rds-생성">RDS 생성</h1>
<h3 id="rds-서브넷-생성">RDS 서브넷 생성</h3>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/5ec69045-28cd-40ad-9ffb-490c3279aff6/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/bd326989-b543-4217-aac2-13ac863c036e/image.png" alt=""></p>
<blockquote>
<p>Subnet Group에 두 개 이상의 서브넷을 지정한 이유 → RDS는 기본적인 컨셉이 고가용성과 장애 복구를 지원</p>
</blockquote>
<p>단일 장애 지점(<strong>SPOF</strong>)을 방지, 가용영역(AZ) 장애 시에도 데이터베이스를 자동으로 다른 가용영역에서 띄울 수 있도록 설계된 구조</p>
<blockquote>
<p>프리티어 수준에서는 Single AZ 배포만 지원하지만, .RDS 구조상 2개 이상의 서로 다른 가용영역의 서브넷을 포함하는 Subnet Group을 지정해야 함</p>
</blockquote>
<p>⇒ 장애 복구 및 향후 Multi-AZ 전환을 위한 최소 요건</p>
<ul>
<li>RDS는 인스턴스 장애, 가용영역 장애 등 다양한 상황에 대비해 관리형 서비스를 제공</li>
<li>Single-AZ 배포라도, AWS 내부적으로 장애 복구나 유지보수 작업 시 다른 가용영역으로 인스턴스를 재배치할 수 있도록 설계 됨</li>
</ul>
<p>⇒ <strong>RDS DB Subnet Group</strong>에는 <strong>최소 2개 이상의 서로 다른 AZ에 속한 서브넷</strong>을 반드시 지정해야만 RDS 생성이 가능</p>
<br>
<br>
<br>
<br>




<h3 id="rds에서-사용할-보안그룹-생성">RDS에서 사용할 보안그룹 생성</h3>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/64d79e90-eac2-4f95-b4e8-b2711cfb1c8a/image.png" alt="">
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/61b52442-aabc-489b-834e-1412ef7fd5b0/image.png" alt=""></p>
<br>
<br>
<br>
<br>


<h3 id="rds-생성-1">RDS 생성</h3>
<p>템플릿 : 프리티어
배포 옵션</p>
<ul>
<li>단일 AZ DB 인스터스 배포</li>
<li>VPC연결</li>
<li>DB 서브넷 연결</li>
<li>퍼블릭 IP 생성 X
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/2e93449d-e652-4153-9efc-8626f30a8fc9/image.png" alt="">
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/608b9ece-5c8b-4fc7-b012-14dcc531ec69/image.png" alt=""></li>
</ul>
<br>
<br>
]]></description>
        </item>
        <item>
            <title><![CDATA[백엔드 리소스들을 Private Subnet에 위치 시켜보자 (1) 기본 개념]]></title>
            <link>https://velog.io/@yuze_dbwm/%EB%B0%B1%EC%97%94%EB%93%9C-%EB%A6%AC%EC%86%8C%EC%8A%A4%EB%93%A4%EC%9D%84-Private-Subnet%EC%97%90-%EC%9C%84%EC%B9%98-%EC%8B%9C%EC%BC%9C%EB%B3%B4%EC%9E%901</link>
            <guid>https://velog.io/@yuze_dbwm/%EB%B0%B1%EC%97%94%EB%93%9C-%EB%A6%AC%EC%86%8C%EC%8A%A4%EB%93%A4%EC%9D%84-Private-Subnet%EC%97%90-%EC%9C%84%EC%B9%98-%EC%8B%9C%EC%BC%9C%EB%B3%B4%EC%9E%901</guid>
            <pubDate>Tue, 20 May 2025 07:56:05 GMT</pubDate>
            <description><![CDATA[<p>개발 역사 중 흑역사라고 한다면, RDS의 public IP를 허용하고, ID와 비밀번호를 쉽게 설정하여, 해킹 당하여 AWS 요금이 많이 나왔던 것이다.</p>
<br>

<p>이런 일이 없게, 백엔드 서버 또는 RDS를 외부와 완벽히 격리 시키고 싶다면, 어떻게 하는 게 좋을까?
⇒ 결론적으로 VPC와 Private Subnet을 이용하여 네트워크를 구성하면 된다. </p>
<br>


<p>AWS의 가장 핵심적인 기능이라고 할 수 있는 VPC에 대해서 알아보도록 하자.
<br>
<br>
<br></p>
<h2 id="vpc란">VPC란?</h2>
<hr>
<p>AWS와 같은, 거대한 퍼블릭 클라우드에서 VPC(Virtual Private Cloud)를 사용하여 <strong>논리적으로 독립된 나만의 가상 네트워크 공간</strong>을 만드는 것</p>
<p>왜 VPC가 필요한데? ⇒ 내가 원하는 대로 네트워크를 세밀하게 설계하고 제어할 수 있다.</p>
<ul>
<li>사설 IP 주소 범위(CIDR 블록) 지정</li>
<li>서브넷(Subnet) 생성 및 분리</li>
<li>라우팅 테이블 설정</li>
<li>인터넷 게이트웨이, NAT 게이트웨이 등 네트워크 게이트웨이 연결</li>
<li>네트워크 ACL, 보안 그룹 등 방화벽 정책 적용</li>
</ul>
<p>즉, VPC는 AWS의 물리적 인프라 위에 논리적으로 완전히 분리된 나만의 네트워크 공간을 만드는 기능이다.</p>
<blockquote>
<p>AWS에서 기본으로 제공하는 VPC는 빠르게 리소스를 사용하기 위해, 만들어 놓은 VPC이다. 웬만하면 새롭게 만들어서 사용하도록 하자</p>
</blockquote>
<br>
<br>



<h2 id="subnet">Subnet</h2>
<hr>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/9dead727-8a01-48af-9ca9-137409cbfd52/image.png" alt=""></p>
<p>VPC 안에 존재하는 더 작은 네트워크 구역</p>
<p>VPC의 IP 주소 범위를 더 작게 나눈 논리적 네트워크 영역</p>
<ul>
<li>VPC라는 큰 네트워크 공간을, 여러 개의 작은 네트워크 영역으로 쪼갠 것</li>
<li>서브넷마다 리소스를 배치하고, 네트워크 정책을 세밀하게 적용할 수 있음</li>
<li>하나의 가용영역에만 속함</li>
<li>독립적인 라우팅 테이블, 보안 그룹, 네트워크 ACL 설정 가능</li>
</ul>
<table>
<thead>
<tr>
<th>구분</th>
<th>Public Subnet (퍼블릭 서브넷)</th>
<th>Private Subnet (프라이빗 서브넷)</th>
</tr>
</thead>
<tbody><tr>
<td>인터넷 접근성</td>
<td>외부 인터넷과 직접 통신 가능</td>
<td>외부에서 직접 접근 불가</td>
</tr>
<tr>
<td>라우팅 테이블</td>
<td>인터넷 게이트웨이(IGW)로 향하는 경로(0.0.0.0/0) 등록</td>
<td>IGW 경로 없음, 필요시 NAT 게이트웨이로 아웃바운드 트래픽만 허용</td>
</tr>
<tr>
<td>사용 용도</td>
<td>웹 서버, Bastion Host 등 외부에서 접근이 필요한 리소스</td>
<td>DB, 내부 서비스 등 외부 노출이 필요 없는 리소스</td>
</tr>
<tr>
<td>공인 IP 할당</td>
<td>가능 (퍼블릭 IP 할당 시 인터넷에서 접근 가능)</td>
<td>불가 (공인 IP 할당해도 IGW 경로 없어 외부 접근 불가)</td>
</tr>
<tr>
<td>아웃바운드 트래픽</td>
<td>인터넷으로 직접 나갈 수 있음</td>
<td>NAT 게이트웨이 통해서만 인터넷으로 나갈 수 있음</td>
</tr>
<tr>
<td>인바운드 트래픽</td>
<td>외부에서 직접 접속 가능</td>
<td>외부에서 직접 접속 불가</td>
</tr>
</tbody></table>
<br>
<br>
<br>


<h2 id="public-subnet">Public Subnet</h2>
<hr>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/12c25ab3-a5d1-4eaa-98a3-5bbd9aa1ca34/image.png" alt=""></p>
<p>인터넷 게이트웨이에 연결되었다면 그것은 Public Subnet이라고 한다.</p>
<br>
<br>
<br>

<h2 id="private-subnet">Private Subnet</h2>
<hr>
<ul>
<li>인터넷 게이트웨이와 연결되지 않은, 고립된 Subnet이다.</li>
<li>Private subnet에 위치한 리소스는 외부 인터넷에서 직접 접근할 수 없음</li>
<li>주로 데이터베이스, 내부 서버 등 외부 노출이 필요 없는 리소스를 배치</li>
</ul>
<br>
<br>
<br>


<h2 id="nat-gateway--아웃바운드-전용-인터넷-허용">NAT gateway : 아웃바운드 전용 인터넷 허용</h2>
<hr>
<ul>
<li>Private Subnet에 있는 인스턴스도 운영체제(OS) 업데이트 등 외부 인터넷 접속이 필요한 경우가 있음</li>
<li>NAT Gateway를 Public Subnet에 배치하여, Private Subnet의 인스턴스가 NAT Gateway를 통해 아웃바운드 인터넷 트래픽을 사용할 수 있도록 함</li>
<li>NAT Gateway는 아웃바운드 트래픽만 허용 → Private Subnet의 인스턴스는 외부로 나가는 트래픽과 그에 대한 응답만 받을 수 있고, 인바운드는 불가능</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[UNIVEUS] Main Page 조회 성능 개선 (1) OFFSET]]></title>
            <link>https://velog.io/@yuze_dbwm/UNIVEUS-Main-Page-%EC%A1%B0%ED%9A%8C-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0-1-OFFSET</link>
            <guid>https://velog.io/@yuze_dbwm/UNIVEUS-Main-Page-%EC%A1%B0%ED%9A%8C-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0-1-OFFSET</guid>
            <pubDate>Mon, 24 Feb 2025 04:37:03 GMT</pubDate>
            <description><![CDATA[<p>먼저 UNIVEUS 프로젝트의 메인페이지는, <code>offset</code> 기반으로 페이지네이션으로 구현되어있다. </p>
<p>데이터가 많지 않아서 랜더링이 늦지 않았고, JPA Pageable을 이용해 손쉽게 구현할 수 있기 때문에 offset을 택했다. </p>
<p><strong>그러나, 서비스가 정말 잘 돼서 대용량의 데이터를 다루어야 한다면, offset 기반 페이지네이션은 어떤 퍼포먼스를 보일지 의문이 들었다.</strong></p>
<br>
<br>


<hr>
<br>
<br>

<h2 id="0-실험-환경-구축---약-300만-개의-데이터">0. 실험 환경 구축 - 약 300만 개의 데이터</h2>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/4c6cbf68-780f-45d7-a2c1-1db9da700963/image.png" alt=""></p>
<p>카테고리는 총 20개이고, 전체 게시물 개수는 300만개이다.</p>
<p>그 중 <code>STUDY</code> 카테고리의 비율이 압도적으로 많도록 구성했다. 그 이유는, 실제 웹 사이트를 운영하다보면, 유독 인기가 많은 카테고리가 존재한다. </p>
<p>그 사실을 반영해, 하나의 카테고리가 30% 이상의 점유율을 차지하도록 했다.</p>
<br>
<br>
<br>



<h2 id="1-offset-기반-페이지네이션의-한계">1. Offset 기반 페이지네이션의 한계</h2>
<p>가장 큰 점유율의 카테고리인 <code>STUDY</code>를 기준으로 쿼리를 실행해보았다.</p>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/789a1c70-9e2c-4ea2-be50-c48d5ebde0d1/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/1c0a7a3f-ed9b-4971-9dc4-c4628dda62fc/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/5ebbe6fc-6c1d-4c64-b8d2-f26527a699b3/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/26bbce22-4cdc-4888-adca-54eec37d8037/image.png" alt=""></p>
<p>offset이 증가할수록 실행 시간이 늘어나는 양상을 볼 수 있다. </p>
<p>이는, offset 기반 페이지네이션에서 흔히 발생하는 이슈이다.</p>
<br>
<br>


<div style="text-align: center; width: 100%; display: block;">
  <img src="https://velog.velcdn.com/images/yuze_dbwm/post/4e742886-d2a8-4f75-9010-53aa53309c19/image.png" alt="인덱스 적용 전" style="margin: 0; border: none;">
  <sub><span style="color: gray; font-size: 12px;">위 쿼리의 동작 방식을 간략하게 시각화 (offset 기반)</span></sub>
</div>



<br>
<br>


<p>offset 기반의 페이지네이션의 성능 이슈는, offset 만큼의 행은 무조건 건너뛰어야하는데에서 나온다. </p>
<p>위 그림을 참고해서 설명해보자면, PK 1, 2만 읽고 싶다고 해도, 23번부터 차례로 읽어서 offset 만큼의 행의 개수를 계산을 해야 된다. </p>
<p>정리하자면, offset 크기가 <strong>N이라면 최소 N개 데이터를 무조건 스캔해야 한다.</strong></p>
<p><code>LIMIT 10 OFFSET 1000000</code> 이라는 쿼리를 실행하면, MySQL은 최소 <strong>100만 개의 행을 읽고 나서</strong> 그다음 10개만 반환한다는 뜻 이다.</p>
<p>즉, <strong>필요 없는 데이터도 읽기 때문에,</strong> 데이터가 많아지고 뒤 페이지를 조회해야 할수록 속도가 느려지는 문제가 발생한다.</p>
<br>
<br>
<br>



<h2 id="2-쿼리에-인덱스-적용">2. 쿼리에 인덱스 적용</h2>
<pre><code class="language-sql">SELECT *
FROM meeting_post p
WHERE meeting_category = :category
ORDER BY p.id DESC
LIMIT :pageSize OFFSET :offset;</code></pre>
<pre><code class="language-sql">CREATE INDEX idx_category_id ON meeting_post (meeting_category, id DESC);</code></pre>
<br>
<br>
<br>


<h3 id="인덱스를-위와-같이-구성한-이유">인덱스를 위와 같이 구성한 이유</h3>
<hr>
<ul>
<li>Cateogry와 ID를 <code>복합 인덱스</code>로 지정한다</li>
<li>WHERE 절에서 Category 사용하도록 한다</li>
<li>Order By 절에서 내림차순 정렬을 고려하여, 인덱스의 두번째 컬럼을 id를 DESC로 지정한다<ul>
<li>offset이 커질수록 리프노드에서 대량의 데이터 레코드를 읽어야한다.</li>
<li>많은 데이터를 읽어야 하는 상황에서는, 인덱스 정순 스캔이 인덱스 역순 스캔보다 속도가 빠르다. 따라서 내림차순 정렬을 고려해서 ID를 정순 스캔하도록 만들었다</li>
</ul>
</li>
</ul>
<br>
<br>
<br>


<h3 id="인덱스-적용-결과">인덱스 적용 결과</h3>
<hr>
<div style="text-align: center; width: 100%; display: block;">
  <img src="https://velog.velcdn.com/images/yuze_dbwm/post/925c49b8-10b8-4bf1-a7a9-c013c822a0dc/image.png" alt="인덱스 적용 전" style="margin: 0; border: none;">
  <sub><span style="color: gray; font-size: 12px;">인덱스 적용 전</span></sub>
</div>


<br>
<br>


<div style="text-align: center; width: 100%; display: block;">
  <img src="https://velog.velcdn.com/images/yuze_dbwm/post/d201c28b-ed97-43b0-a22d-3d3a21abce8d/image.png" alt="인덱스 적용 후" style="margin: 0; border: none;">
  <sub><span style="color: gray; font-size: 12px;">인덱스 적용 후</span></sub>
</div>



<br>
<br>


<div style="text-align: center; width: 100%; display: block;">
  <img src="https://velog.velcdn.com/images/yuze_dbwm/post/15548cf6-405d-42a0-b71d-5108a8fef9a7/image.png" alt="인덱스 적용 후" style="margin: 0; border: none;">
  <sub><span style="color: gray; font-size: 12px;">인덱스 적용 전의 실행 계획</span></sub>
</div>


<br>
<br>


<div style="text-align: center; width: 100%; display: block;">
  <img src="https://velog.velcdn.com/images/yuze_dbwm/post/ce40233d-b4fa-4d4b-844c-00bf774512f9/image.png" alt="인덱스 적용 후" style="margin: 0; border: none;">
  <sub><span style="color: gray; font-size: 12px;">인덱스 적용 후의 실행 계획</span></sub>
</div>

<br>
<br>





<p>실행 계획을 살펴보면, 인덱스가 사용되고 있다는 것을 볼 수 있다.</p>
<p>그러나 인덱스 적용 후가 전 보다 약 1초 느린 것을 볼 수 있다.  </p>
<p>그렇다면, 우리가 기대한 만큼 성능을 못 보이는 이유는 무엇일까?</p>
<br>
<br>
<br>


<h2 id="3-성능-저하-이유와-인덱스-손익-분기점">3. 성능 저하 이유와 인덱스 손익 분기점</h2>
<hr>
<div style="text-align: center; width: 100%; display: block;">
  <img src="https://velog.velcdn.com/images/yuze_dbwm/post/604b5d2f-4213-4eda-8a1a-dd2b4891283e/image.png" alt="인덱스 적용 전의 실행 계획" alt="인덱스 적용 후 동작을 간략하게 시각화" style="margin: 0; border: none;">
  <sub><span style="color: gray; font-size: 12px;">인덱스 적용 후 동작을 간략하게 시각화</span></sub>
</div>


<br>
<br>


<ul>
<li><p><code>STUDY</code> 카테고리는 데이터 분포의 약 <code>36.67%</code>를 차지하고 있다</p>
</li>
<li><p>인덱스로 <code>STUDY</code> 카테고리를 빠르게 필터링 하고 역순으로 조회한다고 해도, 대부분의 데이터셋을 차지하고 있기 때문에, 상위 N건을 읽을 때 발생하는 비용이 여전히 크다</p>
</li>
<li><p>또한, 위 그림을 예시로 들면, offset으로 데이터 행을 건너뛰는 과정에서, select 절에 존재하지 않는 컬럼으로 인해 인덱스에서 건너 뛰는 것이 아닌, 데이터 레코드를 접근하고 이 과정에서 다량의 랜덤 I/O가 발생한다</p>
</li>
<li><p>또한, 데이터를 읽어야 하는 숫자가 <code>20~25%</code>를 넘어가는 경우, 인덱스를 통해 데이터를 읽는 것 보다 데이터 레코드를 순차 I/O로 읽는 것이 더 빠른데, offset이 커질수록 랜덤 I/O로 읽어야하는 데이터 수는 증가한다</p>
</li>
</ul>
<br>
<br>
<br>




<blockquote>
<h3>🗨️ 인덱스의 손익 분기점</h3> 
</blockquote>
<p> 일반적으로 DBMS의 옵티마이저에서는 인덱스를 통해 레코드 1건을 읽는 것이, 테이블에서 직접 레코드 1건을 읽는 것보다 `4~5`배 정도 비용이 더 많이 드는 작업인 것으로 예측한다. </p>
그 이유는, 인덱스를 통해 데이터를 읽으면 레코드 한 건 단위로 `랜덤 I/O`가 발생하기 때문이다. 
<p> 따라서, 읽어야 하는 레코드 수가 전체 테이블 레코드의 `20~25%`를 넘기게 된다면, 인덱스를 이용하지 않고 테이블을 모두 직접 읽어서 필요한 레코드만 가려내는 방식(필터링 방식)이 더 효율적이다.</p> 
>


<br>
<br>




<h3 id="그렇다면-작은-데이터-분포를-가진-카테고리에서의-실행시간은-어떨까">그렇다면, 작은 데이터 분포를 가진 카테고리에서의 실행시간은 어떨까?</h3>
<hr>
<p>실제로 아래와 같이, 가장 작은 분포인 (약 2% 차지) <code>DESIGN</code> 카테고리의 인덱싱 전 후를 비교하게 된다면, 인덱스 적용 전에 비해 후가 약 <code>869ms</code> 단축 된 거를 볼 수 있다.</p>
<div style="text-align: center; width: 100%; display: block;">
  <img src="https://velog.velcdn.com/images/yuze_dbwm/post/eecb1d99-b60b-40ae-87b4-92c51a26072a/image.png" alt="인덱스 적용 전의 실행 계획" alt="인덱스 적용 전" style="margin: 0; border: none;">
  <sub><span style="color: gray; font-size: 12px;">인덱스 적용 전</span></sub>
</div>


<br>
<br>


<div style="text-align: center; width: 100%; display: block;">
  <img src="https://velog.velcdn.com/images/yuze_dbwm/post/bf5e1d01-0e29-4792-ac23-72568e405064/image.png" alt="인덱스 적용 전의 실행 계획" alt="인덱스 적용 후" style="margin: 0; border: none;">
  <sub><span style="color: gray; font-size: 12px;">인덱스 적용 후</span></sub>
</div>


<br>
<br>
<br>



<h2 id="4-커버링-인덱스를-활용한-쿼리-최적화">4. 커버링 인덱스를 활용한 쿼리 최적화</h2>
<hr>
<h4 id="인덱스를-지우는-것은">인덱스를 지우는 것은?</h4>
<p>⇒ 작은 데이터 분포를 가지고 있는 데이터에 대해서, 좋은 퍼포먼스를 보이기 어려울 것이다. </p>
<h4 id="인덱스를-그냥-적용하는-것은">인덱스를 그냥 적용하는 것은?</h4>
<p>⇒ 손익 분기점을 넘어가는 데이터 분포 환경에서 사용하기에는, 성능적 이슈가 여전히 존재한다.</p>
<h4 id="인덱스를-더-효과적으로-사용할-수-있는-방법은-없을까">인덱스를 더 효과적으로 사용할 수 있는 방법은 없을까?</h4>
<p>⇒ <code>커버링 인덱스</code> 를 이용해서 조회할 ID만 추출해서 PRIMARY INDEX로 바로 접근 하는 건 어떨까?</p>
<br>
<br>
<br>



<h3 id="최종-쿼리">최종 쿼리</h3>
<hr>
<pre><code class="language-sql">SELECT p.*
FROM meeting_post p
JOIN (
    SELECT id
    FROM meeting_post
    WHERE meeting_category = :category
    ORDER BY id DESC
    LIMIT :pageSize OFFSET :offset
) sub ON p.id = sub.id;</code></pre>
<ul>
<li>서브쿼리에서는 ID만 가져오도록 한다 <code>커버링 인덱스</code></li>
<li>메인 쿼리에서는, 가져온 20개의 ID에 대해서만 데이터 파일에 접근한다</li>
</ul>
<br>
<br>
<br>


<p><strong>[STEP 1] 커버링 인덱스로 데이터 파일에 접근하지 않고, ID 만 SELECT한다</strong>
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/d79c43c9-1041-459e-b917-23a0acf1882b/image.png" alt="">
<br></p>
<p><strong>[STEP 2] SELECT한 ID(클러스터링 인덱스)를 이용해서 데이터 파일에서 레코드를 읽어온다</strong>
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/9cb5d5cc-dea0-4392-ad28-7db6da016c52/image.png" alt=""></p>
<br>
<br>


<h3 id="위-쿼리의-장점">위 쿼리의 장점</h3>
<hr>
<ul>
<li><p>SELECT절에 ID만 있기 때문에, <code>커버링 인덱스</code>를 통해 offset으로 건너뛰는 데이터들에 대한 테이블 접근하는 것을 회피한다</p>
<p>  ⇒ 페이지네이션 자체를 인덱스 레벨에서 해결하여, LIMIT 20에 대한 랜덤 I/O만 발생한다.</p>
</li>
<li><p>서브쿼리는 인덱스 탐색만 수행하여 최소한의 데이터만 가져오고, 메인 쿼리에서는 클러스터링 인덱스인 id를 이용해서 빠르게 데이터에 접근한다. <code>O(1) 수준</code></p>
</li>
</ul>
<br>
<br>
<br>
<br>





<h3 id="쿼리-튜닝-전-후-테스트">쿼리 튜닝 전 후 테스트</h3>
<hr>
<div style="text-align: center; width: 100%; display: block;">
  <img src="https://velog.velcdn.com/images/yuze_dbwm/post/d201c28b-ed97-43b0-a22d-3d3a21abce8d/image.png" alt="쿼리 튜닝 전" alt="인덱스 적용 후" style="margin: 0; border: none;">
  <sub><span style="color: gray; font-size: 12px;">쿼리 튜닝 전</span></sub>
</div>


<br>
<br>



<div style="text-align: center; width: 100%; display: block;">
  <img src="https://velog.velcdn.com/images/yuze_dbwm/post/c5efabbb-3278-4d26-b36f-fb1d149ba4ca/image.png" alt="쿼리 튜닝 전" alt="쿼리 튜닝 후" style="margin: 0; border: none;">
  <sub><span style="color: gray; font-size: 12px;">쿼리 튜닝 후</span></sub>
</div>



<br>


<blockquote>
</blockquote>
<p>✅ 3.188s → 146ms (약 3.04s 감소, 약 95.4% 향상)
✅ 기존 3.188s에서 146ms로 최적화됨 (약 21.8배 개선)</p>
<blockquote>
</blockquote>
<br>
<br>
<br>



<h3 id="튜닝-쿼리-인덱싱-전-후-비교">튜닝 쿼리 인덱싱 전 후 비교</h3>
<hr>
<div style="text-align: center; width: 100%; display: block;">
  <img src="https://velog.velcdn.com/images/yuze_dbwm/post/ee413ee5-5692-4085-8cbb-628bc293db0d/image.png" alt="쿼리 튜닝 전" alt="인덱스 적용 전" style="margin: 0; border: none;">
  <sub><span style="color: gray; font-size: 12px;">인덱스 적용 전</span></sub>
</div>


<br>
<br>

<div style="text-align: center; width: 100%; display: block;">
  <img src="https://velog.velcdn.com/images/yuze_dbwm/post/03c16e52-cbf7-42bf-99b2-8806c103f902/image.png" alt="쿼리 튜닝 전" alt="인덱스 적용 후" style="margin: 0; border: none;">
  <sub><span style="color: gray; font-size: 12px;">인덱스 적용 후</span></sub>
</div>


<br>



<blockquote>
</blockquote>
<p>✅ 1.575s → 146ms (약 1.43s 감소, 약 90.7% 향상)
✅ 기존 1.575s에서 146ms로 최적화됨 (약 10.9배 개선)</p>
<blockquote>
</blockquote>
<br>
<br>
<br>


<h2 id="5-결론---더-고민해야-할-부분">5. 결론 - 더 고민해야 할 부분</h2>
<hr>
<p>커버링 인덱스로 데이터 접근을 최소화 하여, 실행 시간이 많이 줄었다. 
그렇지만, 근본적인 원인인 <code>offset 만큼의 불필요한 인덱스를 읽는 것</code> 은 해결하지 못했다.</p>
<p>offset이 커질수록 어떤 장애가 또 생길지 모른다는 것이다.</p>
<p>그렇다면, 이를 해결할 수 있는 페이지네이션 기법은 없을까?</p>
<p>⇒ 다음 포스팅에서는, NO OFFSET 기반 페이지 네이션을 통해 성능 개선을 완료하도록 하겠다.</p>
<p>=&gt; 파티셔닝 &amp; 샤딩에 대해 공부도 추가적으로 하도록 하겠다.</p>
<br>
<br>



<blockquote>
<p>📘 참고 서적
Real MySQL 8.0 백은빈, 이성욱 지음
📗 참고 블로그
｢기억보단 기록을｣ <a href="https://jojoldu.tistory.com/529">https://jojoldu.tistory.com/529</a>
📂 도움이 되었던 영상
｢우아한 테크코스｣ <a href="https://youtu.be/edpYzFgHbqs?feature=shared">https://youtu.be/edpYzFgHbqs?feature=shared</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[권한 체크에 Spring AOP를 적용해보자!]]></title>
            <link>https://velog.io/@yuze_dbwm/%EA%B6%8C%ED%95%9C-%EC%B2%B4%ED%81%AC%EC%97%90-Spring-AOP%EB%A5%BC-%EC%A0%81%EC%9A%A9%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@yuze_dbwm/%EA%B6%8C%ED%95%9C-%EC%B2%B4%ED%81%AC%EC%97%90-Spring-AOP%EB%A5%BC-%EC%A0%81%EC%9A%A9%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Thu, 05 Dec 2024 15:53:40 GMT</pubDate>
            <description><![CDATA[<h2 id="무엇이-문제인가">무엇이 문제인가?</h2>
<hr>
<pre><code class="language-java">public enum Role {
    USER,
    ADMIN,
    GUEST
}</code></pre>
<p>옥션 프로젝트에서 접근하는 유저의 권한은 <strong>Guest, User, Admin</strong>으로 나뉜다. </p>
<p>따라서, 엔드포인트 마다 접근한 유저에 대해 권한 체크를 하고, 올바르지 못한 접근에 대해 예외를 던져야 한다.</p>
<p>그러나, 토큰을 사용하는 모든 엔드포인트에서 중복되는 권한 체크 코드가 생길텐데. . . 같이 개발하는 팀원들이 해당 코드를 작성하지 않게 할 수 있는 방법이 있을까?</p>
<p>그렇게 해서 Spring AOP에 대해 앞서서 공부하였다.</p>
<blockquote>
</blockquote>
<p>🗨️ <a href="https://velog.io/@yuze_dbwm/Spring-Aop-1-%EA%B8%B0%EB%B3%B8-%EA%B0%9C%EB%85%90">Spring AOP 개념 관련 포스팅 (1)</a>
🗨️ <a href="https://velog.io/@yuze_dbwm/Spring-AOP-2-%EA%B5%AC%ED%98%84">Spring AOP 개념 관련 포스팅 (2)</a>
🗨️ <a href="https://velog.io/@yuze_dbwm/Spring-AOP-%EC%96%B4%EB%93%9C%EB%B0%94%EC%9D%B4%EC%8A%A4-%EC%A2%85%EB%A5%98">Spring AOP 개념 관련 포스팅 (3)</a></p>
<br>

<h2 id="프로젝트에-어떻게-aop를-적용하였나">프로젝트에 어떻게 AOP를 적용하였나?</h2>
<hr>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/95c2934d-3ddd-4216-85b4-e866f033678e/image.png" alt=""></p>
<p>Auction 프로젝트에서의 user 요청 후 프로세스를 간략화 하면 다음과 같다.</p>
<ul>
<li>user가 요청</li>
<li>user의 토큰을 검증 후, 권한에 따라서 ADMIN, USER, GUEST 역할을 부여</li>
<li>엔드포인트에서 접근 가능한 역할인지 확인</li>
</ul>
<br>

<p>위 프로세스를 거친다.</p>
<p>엔드포인트에서 접근 가능한 역할인지 확인하는 과정은 토큰을 사용하는 모든 컨트롤러 메서드에서 반복 해야한다.</p>
<p>따라서, 해당 부분을 컨트롤러 메서드에 직접 로직을 작성하는 것이 아니라, <code>부가 기능</code>으로 판단하여 <code>핵심 로직</code>으로부터 분리하기로 결정하였다.</p>
<br>

<h3 id="프로젝트-적용-코드">프로젝트 적용 코드</h3>
<h4 id="커스텀-어노테이션-생성---포인트-컷-생성을-위한-작업">커스텀 어노테이션 생성 - 포인트 컷 생성을 위한 작업</h4>
<pre><code class="language-java">@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserOnly {
}</code></pre>
<p>먼저, 커스텀 어노테이션을 만들었다. 
AOP에서 포인트 컷을 지정하는 방법으로, 메서드에 커스텀 메서드를 붙이는 방법을 택했다.
<code>@UserOnly</code>를 컨트롤러 메서드에 붙여주면, <code>@UserOnly</code>가 붙은 메서드가 실행되는 시점에 AOP가 실행된다.</p>
<br>

<p>이제 AOP 실행 되는 메서드를 특정하는 것을 정의했으니, 부가 기능 그 자체를 구현하는 것에 대해 알아보도록 하겠다.</p>
<h4 id="aop-구현---부가-기능-작성">AOP 구현 - 부가 기능 작성</h4>
<pre><code class="language-java">// AspectJ 어노테이션 이용해서 Aop 구현하겠다 지정한다
@Aspect
// 빈으로 등록해서 spring aop로 사용가능하게한다
@Component
public class UserCheck {
    // Advice(&quot;point cut&quot;)를 부여하고자 하는 기능(메서드)에 붙여준다 -&gt; 기능 실행 시점과 특정 위치를 지정한다
    @Before(&quot;@annotation(UserOnly)&quot;)
    public void userCheck(JoinPoint joinPoint) {
    // joinPoint.getArgs()를 통해 가져오는 것은 타겟 메서드의 매개변수(Arguments)
    // 즉, AOP가 적용된 메서드가 호출될 때 전달된 실제 매개변수 값들을 배열 형태로 반환
        Object[] args = joinPoint.getArgs();

        for (Object arg : args) {
            if (arg instanceof Accessor accessor) {
                if (!accessor.isUser()) {
                    throw new IllegalArgumentException(&quot;안됨&quot;);
                }
            }
        }
    }
}</code></pre>
<p>위 코드는, 메서드에 접근을 시도하는 사람이 <code>USER</code> 역할을 가진 사람인지 확인하는 메서드이다.(AOP)</p>
<p><code>@Before</code> 어노테이션에 의해, <code>Point Cut</code>으로 특정한 메서드가 실행 되기 직전에 해당 AOP가 실행 되고, 예외를 던지지 않을 경우, 메서드 실행 후 <code>target</code>이 실행된다.</p>
<blockquote>
<p>만약 @Before에 대한 자세한 내용이 궁금하다면 해당 포스팅을 참고하면 된다.
<a href="https://velog.io/@yuze_dbwm/Spring-AOP-%EC%96%B4%EB%93%9C%EB%B0%94%EC%9D%B4%EC%8A%A4-%EC%A2%85%EB%A5%98">Advice 어노테이션에 대한 포스팅</a></p>
</blockquote>
<p>만약, 유저의 접근 권한이 올바르지 않을 경우 예외를 던지고 <code>target</code>이 실행되지 않는다.</p>
<p>따라서 AOP를 통해, <code>UserOnly</code> 어노테이션이 메서드에 붙어있는 경우, <code>USER</code> 역할을 부여받은 접근자만 접근할 수 있게 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring AOP (3) : 어드바이스 종류]]></title>
            <link>https://velog.io/@yuze_dbwm/Spring-AOP-%EC%96%B4%EB%93%9C%EB%B0%94%EC%9D%B4%EC%8A%A4-%EC%A2%85%EB%A5%98</link>
            <guid>https://velog.io/@yuze_dbwm/Spring-AOP-%EC%96%B4%EB%93%9C%EB%B0%94%EC%9D%B4%EC%8A%A4-%EC%A2%85%EB%A5%98</guid>
            <pubDate>Fri, 29 Nov 2024 11:58:51 GMT</pubDate>
            <description><![CDATA[<p>어드바이스는 <span style="color: #2E64FE;"><strong>어느 시점</strong></span>에 <span style="color: #2E64FE;"><strong>어떤 기능</strong></span>을 의미한다. 
따라서, 이번 포스팅에서는 어드바이스의 <span style="color: #2E64FE;"><strong>시점 지정</strong></span>에 대해 알아볼 것이다.
어드 바이스는 다양한 어노테이션을 AOP 실행 시점을 지정할 수 있다.</p>
<br>


<h2 id="adivce-어노테이션의-종류">Adivce 어노테이션의 종류</h2>
<hr>
<table>
<thead>
<tr>
<th>어노테이션</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td><strong>@Around</strong></td>
<td>- 메서드 호출 전후에 수행</td>
</tr>
<tr>
<td></td>
<td>- 가장 강력한 어드바이스</td>
</tr>
<tr>
<td></td>
<td>- 조인 포인트 실행 여부 선택</td>
</tr>
<tr>
<td></td>
<td>- 반환 값 변환</td>
</tr>
<tr>
<td></td>
<td>- 예외 변환 등이 가능</td>
</tr>
<tr>
<td><strong>@Before</strong></td>
<td>- 조인 포인트 실행 이전에 실행</td>
</tr>
<tr>
<td><strong>@After</strong></td>
<td>- 조인 포인트가 정상 또는 예외에 관계없이 실행 (finally와 유사)</td>
</tr>
<tr>
<td><strong>@AfterReturning</strong></td>
<td>- 조인 포인트가 정상 완료 후 실행</td>
</tr>
<tr>
<td><strong>@AfterThrowing</strong></td>
<td>- 메서드가 예외를 던지는 경우 실행</td>
</tr>
</tbody></table>
<br>
<br>



<h3 id="around">@Around</h3>
<pre><code class="language-java"> @Aspect
    @Order(2)
    public static class TxAspect {
        @Around(&quot;isyoudwn.aop.order.aop.PointCuts.orderAndService()&quot;)
        public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
            try {
                 // @Before
                log.info(&quot;[트랜잭션 시작] {}&quot;, joinPoint.getSignature());

                // 조인 포인트 실행
                Object result = joinPoint.proceed();

                // @AfterReturning
                log.info(&quot;[트랜잭션 커밋] {}&quot;, joinPoint.getSignature());
                return result;
            } catch (Exception e) {
                // @AfterThrowing
                log.info(&quot;[트랜잭션 롤백] {}&quot;, joinPoint.getSignature());
                throw e;
            } finally {
                 // @After
                log.info(&quot;[리소스 릴리스] {}&quot;, joinPoint.getSignature());
            }
        }
    }</code></pre>
<ul>
<li>메서드 실행 전 후에 작업을 수행한다<ul>
<li>조인 포인트의 실행 여부를 선택할 수 있다</li>
<li>전달값(파라미터)을 변환할 수 있다</li>
<li>메서드의 return이 아닌 다른 것을 반환할 수 있다</li>
<li>예외를 변환해서 throw 할 수 있다</li>
<li>try-catch-finally 모두 들어가는 구문 처리 가능하다</li>
</ul>
</li>
<li>ProceedingJoinPoint를 사용해야한다(규칙)<ul>
<li>proceed()를 가지고있다</li>
<li>JoinPoint의 하위 타입이다</li>
</ul>
</li>
<li>조인포인트를 직접 실행해서 타겟을 실행해야한다<ul>
<li>proceed()를 통해 대상을 실행한다</li>
<li>proceed()를 여러번 실행할 수 있다(재시도 기능 가능)</li>
</ul>
</li>
</ul>
<blockquote>
<p>💡 Around 하나만 있어도, AOP의 모든 시점을 구현할 수 있다. 
그러나, <strong>Target</strong>을 <strong>직접 실행</strong>해야 되는 단점이 있고, Advice의 내부 로직을 이해해야지만, 그 Advice가 어떤 시점에 실행되는지 알 수 있다. 
따라서, 아래와 같이 시점을 특정하는 어노테이션이 존재한다.</p>
</blockquote>
<br>
<br>


<h3 id="before">@Before</h3>
<p>조인 포인트 실행 전 Advice 실행</p>
<pre><code class="language-java">    // before은 로직 수행 후 joinpoint를 실행한다
    @Before(&quot;isyoudwn.aop.order.aop.PointCuts.orderAndService()&quot;)
    public void doBefore(JoinPoint joinPoint) {
        log.info(&quot;[before] {}&quot;, joinPoint.getSignature());
    }</code></pre>
<ul>
<li>메서드 종료시 자동으로 다음 타켓이 호출된다</li>
<li>예외가 발생하면 다음 코드가 호출되지 않는다
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/746f87fb-b73d-4f7f-8e4a-469555fccc88/image.png" alt=""></li>
</ul>
<br>
<br>


<h3 id="afterreturning">@AfterReturning</h3>
<p>메서드 실행 후 정상적으로 Return될 때 Advice 실행</p>
<pre><code class="language-java"> @AfterReturning(value = &quot;isyoudwn.aop.order.aop.PointCuts.orderAndService()&quot;, returning = &quot;result&quot;)
    public void doReturn(JoinPoint joinPoint, Object result) {
        log.info(&quot;[return] {} return = {}&quot;, joinPoint.getSignature(), result);
    }</code></pre>
<ul>
<li>value에 포인트 컷을 넣는다</li>
<li><code>returning</code>속성에 사용된 이름과 파라미터를 동일한 이름으로 지정하면, 매칭되어서 파라미터에 return 값이 들어온다. <span style = "color: #2E64FE;"><strong>이때, 파라미터 타입은 메서드의 반환 타입과 동일해야 된다(부모 타입을 지정하면 모든 자식 타입은 인정된다)</strong></span></li>
<li>return값이 매칭이 안되면 어드바이스 자체가 실행이 안된다</li>
<li>메서드 자체에 반환하는 return 을 조작할 수 없다</li>
</ul>
<br>
<br>


<h3 id="afterthrowing">@AfterThrowing</h3>
<p>메서드 실행이 예외를 던져서 종료될 때 실행될 때 Advice 실행</p>
<pre><code class="language-java">    @AfterThrowing(value = &quot;isyoudwn.aop.order.aop.PointCuts.orderAndService()&quot;, throwing = &quot;ex&quot;)
    public void doReturn(JoinPoint joinPoint, Exception ex) {
        log.info(&quot;[ex] message = {}&quot;, ex.getMessage());
    }</code></pre>
<ul>
<li><code>throwing</code> 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야한다</li>
<li><code>throwing</code> 파라미터에 지정된 타입과 맞는 예외를 대상으로 실행한다(부모 타입을 지정하면, 모든 자식 타입은 인정된다)</li>
</ul>
<br>
<br>


<h3 id="after">@After</h3>
<p>메서드 실행이 종료되면 Advice가 실행된다. (finally를 생각하면 된다)</p>
<pre><code class="language-java">  @After(value = &quot;isyoudwn.aop.order.aop.PointCuts.orderAndService()&quot;)
    public void doAfter(JoinPoint joinPoint) {
        log.info(&quot;[after] {}&quot;, joinPoint.getSignature());
    }</code></pre>
<ul>
<li>정상 및 예외 반환 조건을 모두 처리한다</li>
<li>리소스를 해제 하는데 일반적으로 많이 사용한다</li>
</ul>
<br>
<br>

<h3 id="전체-코드">전체 코드</h3>
<pre><code class="language-java">@Slf4j
@Aspect
public class AspectV6Advice {

    // before은 로직 수행 후 joinpoint를 실행한다
    @Before(&quot;isyoudwn.aop.order.aop.PointCuts.orderAndService()&quot;)
    public void doBefore(JoinPoint joinPoint) {
        log.info(&quot;[before] {}&quot;, joinPoint.getSignature());
    }

    @AfterReturning(value = &quot;isyoudwn.aop.order.aop.PointCuts.orderAndService()&quot;, returning = &quot;result&quot;)
    public void doReturn(JoinPoint joinPoint, Object result) {
        log.info(&quot;[return] {} return = {}&quot;, joinPoint.getSignature(), result);
    }

    @AfterThrowing(value = &quot;isyoudwn.aop.order.aop.PointCuts.orderAndService()&quot;, throwing = &quot;ex&quot;)
    public void doReturn(JoinPoint joinPoint, Exception ex) {
        log.info(&quot;[ex] message = {}&quot;, ex.getMessage());
    }

    @After(value = &quot;isyoudwn.aop.order.aop.PointCuts.orderAndService()&quot;)
    public void doAfter(JoinPoint joinPoint) {
        log.info(&quot;[after] {}&quot;, joinPoint.getSignature());
    }
}</code></pre>
<br>
<br>



<h3 id="결과">결과</h3>
<p>Around를 사용하지 않고, 시점을 특정하는 어노테이션을 총동원해서 작성한 Advice 들의 실행 결과는 아래와 같다.</p>
<ul>
<li>*<em>success *</em>
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/d34dfe28-bacb-4969-8a24-1f0657450df0/image.png" alt=""></li>
</ul>
<ul>
<li><strong>exception</strong>
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/09b3f6ff-9cb5-46ff-832f-134fcefd9468/image.png" alt=""></li>
</ul>
<br>
<br>


<h2 id="동일한-조인-포인트에서-어드바이스-실행-순서">동일한 조인 포인트에서 어드바이스 실행 순서</h2>
<hr>
<p><img src="https://velog.velcdn.com/images/yuze_dbwm/post/69eec9cd-6f3d-416c-bf77-74a37bd9461d/image.png" alt=""></p>
<p>Aspect 안에 동일한 종류의 어드바이스가 2개이면 순서가 보장되지 않기 때문에, 이 경우에는 @Order을 사용해서 순서를 지정해주어야 한다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring AOP (2) : 구현]]></title>
            <link>https://velog.io/@yuze_dbwm/Spring-AOP-2-%EA%B5%AC%ED%98%84</link>
            <guid>https://velog.io/@yuze_dbwm/Spring-AOP-2-%EA%B5%AC%ED%98%84</guid>
            <pubDate>Fri, 29 Nov 2024 10:50:07 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>이 포스팅은 간단한 예제를 이용해서, Spring AOP를 구현하며 이해하는 것이 목적이다</p>
</blockquote>
<br>

<h2 id="aop를-사용하기-위한-설정">AOP를 사용하기 위한 설정</h2>
<hr>
<p>JPA 또는 Spring AOP를 이용해 구현한 것을 프로젝트의 dependency에 이미 가지고 있는 경우, 이미 AOP의존성을 사용하고 있기 때문에, dependency를 명시적으로 넣어주지 않아도 된다.</p>
<p>그렇지 않을 경우, build.gradle에 아래 내용을 추가하여 AOP를 사용하겠다고 명시 해야 한다.</p>
<pre><code class="language-java">    implementation &#39;org.springframework.boot:spring-boot-starter-aop&#39;</code></pre>
<p><code>@Aspect</code> 를 사용하기 위해서는, main함수 위에 아래 내용을 추가해야 한다. 그러나, 스프링 부트를 사용할 경우 자동으로 추가되어 있으니 따로 명시해주지 않아도 된다.</p>
<pre><code class="language-java"> @EnableAspectJAutoProxy</code></pre>
<br>

<br>


<h2 id="aop-구현">AOP 구현</h2>
<hr>
<h3 id="target--aop-적용-대상">Target : AOP 적용 대상</h3>
<h4 id="orderservice">OrderService</h4>
<p>orderItem이 실행되면 log가 찍히고, repository에 save한다.</p>
<pre><code class="language-java">@RequiredArgsConstructor
@Service
@Slf4j
public class OrderService {
    private final OrderRepository orderRepository;

    public void orderItem(String itemId) {
        log.info(&quot;[orderService] 실행&quot;);
        orderRepository.save(itemId);
    }
}</code></pre>
<h4 id="orderrepository">OrderRepository</h4>
<p>save가 실행되면 Log가 찍히고, 올바를 경우 &quot;ok&quot; 옳지 않을 경우 예외를 반환한다.</p>
<pre><code class="language-java">@Slf4j
@Repository
public class OrderRepository {
    public String save(String itemId) {
        log.info(&quot;[orderRepository] 실행&quot;);

        if (itemId.equals(&quot;ex&quot;)) {
            throw new IllegalStateException(&quot;예외 발생&quot;);
        }
        return &quot;ok&quot;;
    }
}
</code></pre>
<br>

<h3 id="aspect--여러개의-adviser을-가지고-있는-클래스모듈">Aspect : 여러개의 Adviser을 가지고 있는 클래스(모듈)</h3>
<pre><code class="language-java">@Slf4j
@Aspect
public class AspectV1 {
    @Around(&quot;execution(* isyoudwn.aop.order..*(..))&quot;)
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info(&quot;[log] {}&quot;, joinPoint.getSignature()); // join point 시그니처
        return joinPoint.proceed(); // 타깃 메서드 호출
    }
}</code></pre>
<br>

<h3 id="advice-개발"><strong>Advice 개발</strong></h3>
<pre><code class="language-java">    @Around(&quot;execution(* isyoudwn.aop.order..*(..))&quot;)
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info(&quot;[log] {}&quot;, joinPoint.getSignature()); // join point 시그니처
        return joinPoint.proceed(); // 타깃 호출
    }</code></pre>
<p><code>@Around 애너테이션의 doLog()</code> </p>
<ul>
<li>어느 시점에? 어떤 기능을</li>
<li>어드바이스(Advice)</li>
</ul>
<p><code>(&quot;execution(* isyoudwn.aop.order..*(..))&quot;)</code> </p>
<ul>
<li>포인트 컷</li>
<li>isyoudwn.aop.order 패키지와 그 하위 패키지(. .)에 존재하는 모든 메서드들이 실행될 때 실행할 것임
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/55af1a0a-ca44-40c9-8cb8-a3bba417823c/image.png" alt=""></li>
</ul>
<br>


<h3 id="스프링-aop를-사용하기-위해서는-bean으로-등록해-주어야-한다">스프링 Aop를 사용하기 위해서는, Bean으로 등록해 주어야 한다</h3>
<p><strong>1. @Component
2. @Bean 
3. @Import(@Configuration)</strong></p>
<p>위 세가지 중, test 코드에 간단하게 import해서 확인할 것이기 때문에 Import를 사용하였지만, Component Scan을 자동으로 하는 Spring Boot의 특성을 살려, Component를 이용하는 것도 좋은 방법이다.</p>
<br>


<h3 id="aop-적용-확인을-할-테스트-코드-작성">AOP 적용 확인을 할 테스트 코드 작성</h3>
<pre><code class="language-java">@Slf4j
@SpringBootTest
@Import({AspectV6Advice.class})
public class AopTest {

    @Autowired
    OrderService orderService;

    @Autowired
    OrderRepository orderRepository;

    @Test
    void aopInfo() {
        log.info(&quot;isAopProxy, orderService={}&quot;, AopUtils.isAopProxy(orderService));
        log.info(&quot;isAopProxy, orderRepository={}&quot;, AopUtils.isAopProxy(orderRepository));
    }

    @Test
    void success() {
        orderService.orderItem(&quot;itemA&quot;);
    }

    @Test
    void exception() {
        Assertions.assertThatThrownBy(()
                        -&gt; orderService.orderItem(&quot;ex&quot;))
                .isInstanceOf(IllegalStateException.class);
    }
}
</code></pre>
<br>

<h3 id="실행-결과">실행 결과</h3>
<ul>
<li>전체 실행 결과
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/6940b7f1-5ddc-447b-8e93-4beddf89721f/image.png" alt=""></li>
</ul>
<ul>
<li>orderService와 orderRepository에 프록시가 적용 됐는지 확인한다. 원래는 false 였던 값이 true로 변경되었다
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/4898834e-7281-44cf-b461-992a5e54fe4c/image.png" alt=""></li>
</ul>
<ul>
<li>orderService와 orderRepository가 실행되기 전 AOP가 불리는 모습이다
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/542fda2d-26cc-461a-bc64-fb89256bc866/image.png" alt=""></li>
</ul>
<br>
<br>


<h2 id="pointcut-분리">PointCut 분리</h2>
<hr>
<p>지금까지는 Advcie와 PointCut이 결합된 상태였는데, PointCut은 따로 분리할 수 있다.</p>
<pre><code class="language-java">@Aspect
@Slf4j
public class AspectV2 {

    @Pointcut(&quot;execution(* isyoudwn.aop.order..*(..))&quot;)
    private void allOrder() {}; // pointcut signature

    @Around(&quot;allOrder()&quot;)
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info(&quot;[log] {}&quot;, joinPoint.getSignature()); // join point 시그니처
        return joinPoint.proceed(); // 타깃 호출
    }
}
</code></pre>
<ul>
<li>포인트 컷을 분리해서 사용한다면, 의미를 부여할 수 있는 장점이 있음</li>
<li>포인트 컷들만 모아둔다면 프로그램을 모듈화 할 수 있다는 장점이 있음</li>
</ul>
<br>
<br>

<h2 id="aspect안에-advice를-추가할-수-있다">Aspect안에 Advice를 추가할 수 있다</h2>
<hr>
<pre><code class="language-java">@Aspect
@Slf4j
public class AspectV3 {

    @Pointcut(&quot;execution(* isyoudwn.aop.order..*(..))&quot;)
    private void allOrder() {};

    @Pointcut(&quot;execution(* *..*Service.*(..))&quot;)
    private void allService() {};

    @Around(&quot;allOrder()&quot;)
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info(&quot;[log] {}&quot;, joinPoint.getSignature()); // join point 시그니처
        return joinPoint.proceed(); // 타깃 호출
    }

    //hello.aop.order 패키지와 하위 패키지 이면서 클래스 이름 패턴이 *Service 인 것
    @Around(&quot;allOrder() &amp;&amp; allService()&quot;)
    public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            log.info(&quot;[트랜잭션 시작] {}&quot;, joinPoint.getSignature());
            Object result = joinPoint.proceed();
            log.info(&quot;[트랜잭션 커밋] {}&quot;, joinPoint.getSignature());
            return result;
        } catch (Exception e) {
            log.info(&quot;[트랜잭션 롤백] {}&quot;, joinPoint.getSignature());
            throw e;
        } finally {
            log.info(&quot;[리소스 릴리스] {}&quot;, joinPoint.getSignature());
        }
    }
}
</code></pre>
<ul>
<li>OrderService에는 doLog와 doTransaction 두 어드바이스가 모두 적용된다</li>
<li>OrderRepository에는 doLog 어드파이스만 적용된다</li>
</ul>
<br>

<ul>
<li>트랜잭션 커밋
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/393d1bf9-b30e-42e9-9243-4a638eeba34f/image.png" alt=""></li>
</ul>
<ul>
<li>트랜잭션 롤백
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/674df50c-b323-4efc-85fe-e5c429d297d9/image.png" alt=""></li>
</ul>
<blockquote>
<p>AOP 실행 순서는, 직접 지정해주지 않는 이상 순서가 보장되지 않는다. 더 자세한 내용은 &#39;Advice 순서 지정&#39;에서 설명하도록 하겠다.</p>
</blockquote>
<br>
<br>


<h2 id="pointcut을-모듈화-해서-관리">PointCut을 모듈화 해서 관리</h2>
<hr>
<pre><code class="language-java">public class PointCuts {

    @Pointcut(&quot;execution(* isyoudwn.aop.order..*(..))&quot;)
    public void allOrder() {};

    // 클래스 이름 패턴이 *Service 인 것
    @Pointcut(&quot;execution(* *..*Service.*(..))&quot;)
    public void allService() {};

    @Pointcut(&quot;allOrder() &amp;&amp; allService()&quot;)
    public void orderAndService() {};
}
</code></pre>
<p>Advice에서는 포인트 컷을 참조를 통해 부르면 된다</p>
<pre><code class="language-java">@Slf4j
@Aspect
public class AspectV4 {
    @Around(&quot;isyoudwn.aop.order.aop.PointCuts.allOrder()&quot;)
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info(&quot;[log] {}&quot;, joinPoint.getSignature()); // join point 시그니처
        return joinPoint.proceed(); // 타깃 호출
    }

    //hello.aop.order 패키지와 하위 패키지 이면서 클래스 이름 패턴이 *Service 인 것
    @Around(&quot;isyoudwn.aop.order.aop.PointCuts.orderAndService()&quot;)
    public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            log.info(&quot;[트랜잭션 시작] {}&quot;, joinPoint.getSignature());
            Object result = joinPoint.proceed();
            log.info(&quot;[트랜잭션 커밋] {}&quot;, joinPoint.getSignature());
            return result;
        } catch (Exception e) {
            log.info(&quot;[트랜잭션 롤백] {}&quot;, joinPoint.getSignature());
            throw e;
        } finally {
            log.info(&quot;[리소스 릴리스] {}&quot;, joinPoint.getSignature());
        }
    }
}
</code></pre>
<br>
<br>


<h2 id="advice-순서-지정">Advice 순서 지정</h2>
<hr>
<p>Advice가 실행되는 순서가 보장되어 있지 않다(즉, 한 메서드에 여러 AOP를 지정하면, 어떤 것이 먼저 실행될지는 모른다는 것이다)</p>
<p>따라서 Order 어노테이션을 이용해서 순서를 직접 지정해 주어야 한다. </p>
<p>그러나 Order 순서는 <strong>클래스 단위</strong>로 보장된다 → 즉, Aspect 단위로 보장된다.</p>
<p>아래와 같이, 하나의  Aspect 안에 여러 Advice가 존재할 경우 메서드에 Order을 작성해도 순서가 보장되지 않는다는 것이다.</p>
<pre><code class="language-java">
@Slf4j
@Aspect
public class AspectV4
    @Around(&quot;isyoudwn.aop.order.aop.PointCuts.allOrder()&quot;)
    @Order(2)
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info(&quot;[log] {}&quot;, joinPoint.getSignature()); // join point 시그니처
        return joinPoint.proceed(); // 타깃 호출
    }

    //hello.aop.order 패키지와 하위 패키지 이면서 클래스 이름 패턴이 *Service 인 것
    @Around(&quot;isyoudwn.aop.order.aop.PointCuts.orderAndService()&quot;)
    @Order(1)
    public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            log.info(&quot;[트랜잭션 시작] {}&quot;, joinPoint.getSignature());
            Object result = joinPoint.proceed();
            log.info(&quot;[트랜잭션 커밋] {}&quot;, joinPoint.getSignature());
            return result;
        } catch (Exception e) {
            log.info(&quot;[트랜잭션 롤백] {}&quot;, joinPoint.getSignature());
            throw e;
        } finally {
            log.info(&quot;[리소스 릴리스] {}&quot;, joinPoint.getSignature());
        }
    }
}
</code></pre>
<br>

<p>아래와 같이 작성하면 순서를 보장 받을 수 있다.</p>
<pre><code class="language-java">@Slf4j
public class AspectV5 {

    @Aspect
    @Order(2)
    public static class LogAspect {
        @Around(&quot;isyoudwn.aop.order.aop.PointCuts.allOrder()&quot;)
        public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info(&quot;[log] {}&quot;, joinPoint.getSignature()); // join point 시그니처
            return joinPoint.proceed(); // 타깃 호출
        }
    }

    @Aspect
    @Order(1)
    public static class TxAspect {
        @Around(&quot;isyoudwn.aop.order.aop.PointCuts.orderAndService()&quot;)
        public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
            try {
                log.info(&quot;[트랜잭션 시작] {}&quot;, joinPoint.getSignature());
                Object result = joinPoint.proceed();
                log.info(&quot;[트랜잭션 커밋] {}&quot;, joinPoint.getSignature());
                return result;
            } catch (Exception e) {
                log.info(&quot;[트랜잭션 롤백] {}&quot;, joinPoint.getSignature());
                throw e;
            } finally {
                log.info(&quot;[리소스 릴리스] {}&quot;, joinPoint.getSignature());
            }
        }
    }
}
</code></pre>
<br>

<ul>
<li>트랜잭션의 순서가 1, Log AOP의 순서가 2일 때 결과
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/9e4ae5e3-4e2e-4bb9-9f58-6e14a39ee77f/image.png" alt=""></li>
</ul>
<ul>
<li>Log AOP의 순서가 1, 트랜잭션 AOP의 순서가 2일 때
<img src="https://velog.velcdn.com/images/yuze_dbwm/post/5f0b972a-473f-4f31-80d2-ac7b224dcc90/image.png" alt=""></li>
</ul>
]]></description>
        </item>
    </channel>
</rss>