<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Junha's Note</title>
        <link>https://velog.io/</link>
        <description>A Sound Code in a Sound Body💪</description>
        <lastBuildDate>Fri, 23 May 2025 10:52:53 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>Junha's Note</title>
            <url>https://velog.velcdn.com/images/jun-ha/profile/7fe6dbac-e81e-4acf-9171-30fc5d1fc50b/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. Junha's Note. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/jun-ha" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[대규모시스템설계(1)- MySQL Query Plan, 쿼리 튜닝]]></title>
            <link>https://velog.io/@jun-ha/MySQL-QueryPlan-%EB%B0%8F-%EC%BF%BC%EB%A6%AC-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0</link>
            <guid>https://velog.io/@jun-ha/MySQL-QueryPlan-%EB%B0%8F-%EC%BF%BC%EB%A6%AC-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0</guid>
            <pubDate>Fri, 23 May 2025 10:52:53 GMT</pubDate>
            <description><![CDATA[<h3 id="article-테이블">Article 테이블</h3>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/7f627c51-fb6f-4378-bcd7-482d84a9a0fa/image.png" alt=""></p>
<p>게시글 정보를 담는 article 테이블에 약 1200만건의 데이터를 삽입하였다.
분산 데이터베이스 환경(샤딩)을 고려하여 FK는 설정하지 않았다.(board_id, writer_id)</p>
<p>QueryPlan을 확인하고, 인덱스를 설정하는 등 테스트를 진행하며 성능을 개선해보자.</p>
<hr>
<h4 id="페이징-구현">페이징 구현</h4>
<p>특정 게시판의 N번 페이지에서 M개의 게시글을 불러오는 쿼리는 아래와 같다. (최신순 정렬)</p>
<pre><code class="language-sql">select * from article 
    where board_id = {board_id}
    order by created_at desc
    limit M offset (N-1) * M</code></pre>
<p>위 쿼리 그대로 4번 페이지에서 30개의 게시글을 조회했다.</p>
<p><strong>페이지 조회 결과</strong></p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/c260cf0e-df27-42af-bcb6-68b6cefee35b/image.png" alt=""></p>
<p>고작 1200만건의 데이터에서 30개의 게시글을 조회하는데 실행에 5.48초가 걸렸다.
정상적인 서비스가 어려운 수준이다.</p>
<hr>
<h4 id="실행계획query-plan">실행계획(Query Plan)</h4>
<p><code>explain</code>키워드를 통해 실행계획을 확인해보자.
<img src="https://velog.velcdn.com/images/jun-ha/post/ee462977-5c65-4fd4-a57b-e27037735774/image.png" alt=""></p>
<p>*<em>type = ALL *</em></p>
<ul>
<li>테이블 전체 순회 - Full Table Scan</li>
</ul>
<p><strong>Extra = Using where; Using filesort</strong></p>
<ul>
<li>where 절을 사용하며, 데이터가 많아 메모리에서 정렬할 수 없어서, 파일(디스크)에서 데이터를 정렬한다.</li>
</ul>
<p>전체 데이터에 대해 필터링 및 정렬을 수행하므로 아주 큰 비용이 든 것을 확인할 수 있다.</p>
<blockquote>
<p><strong>Using filesort</strong>
MySQL InnoDB 스토리지 엔진은 <code>Order by</code> 사용시 기본적으로 메모리의 정렬 버퍼(sort_buffer)를 사용한다. <br>
하지만 정렬할 레코드 양이 너무 많아 정렬 버퍼의 크기를 초과할 경우, 
디스크의 임시공간을 만들어 그곳에서 정렬을 한다. <br>
: Disk I/O가 다수 발생 -&gt; 속도 저하</p>
</blockquote>
<hr>
<h4 id="인덱스">인덱스</h4>
<p>이를 개선하기 위해 인덱스를 사용할 수 있다.</p>
<pre><code class="language-sql">create index idx_board_id_article_id 
on article(board_id asc, article_id desc);</code></pre>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/d68daefa-9e63-40aa-a236-d9bb40937438/image.png" alt=""></p>
<p>인덱스를 통해 
board_id에 대해 오름차순 정렬,
board_id가 같다면 article_id에 대해 내림차순 정렬된 B+ tree가 생성된다.</p>
<p>article_id는 <code>Snowflake</code> 알고리즘을 사용하기 때문에 
article_id에 대한 정렬이 곧 시간순 정렬이다. </p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/f16fc7ee-714a-4ae4-b5f7-7181f998b316/image.png" alt=""></p>
<p>인덱스를 걸고 동일한 쿼리를 실행한 결과 아주 빠르게 조회되는 것을 확인할 수 있다.
<strong>하지만 이것으로 모든 문제가 해결되었을까?</strong></p>
<p>동일한 쿼리로 50000번째 페이지를 조회해보자.
<code>offset = (50000 - 1) * 30</code></p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/66b2e2bd-dfbe-4a5e-98ea-7ef008a6c21f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/5df8e9c8-e33c-44d9-82c5-d60402822e20/image.png" alt=""></p>
<p>실행 시간은 1.83초로 매우 느려졌다.
Query Plan을 살펴봐도 인덱스가 사용되는 것을 확인할 수 있다.</p>
<p>인덱스가 제대로 적용되었다면, offset이 늘어났다고해서 실행 시간에 유의미한 변화가 없을 것으로 기대했다. </p>
<p><strong>왜 이런 문제가 발생할까?</strong></p>
<hr>
<h4 id="clustered-index--secondary-index">Clustered Index / Secondary Index</h4>
<p>MySQL InnoDB 스토리지 엔진에는 두가지 주요한 인덱스가 존재한다.</p>
<table>
<thead>
<tr>
<th></th>
<th>Clustered Index</th>
<th>Secondary Index</th>
</tr>
</thead>
<tbody><tr>
<td>생성</td>
<td>테이블의 PK로 자동생성</td>
<td>컬럼으로 직접 생성</td>
</tr>
<tr>
<td>포함 데이터</td>
<td>실제 행 데이터(row data)</td>
<td>포인터 + 인덱스 값</td>
</tr>
<tr>
<td>개수</td>
<td>테이블 당 1개</td>
<td>테이블 당 N개</td>
</tr>
</tbody></table>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/524cf46c-48e7-4ddf-adcf-854fc2223394/image.png" alt=""></p>
<p>현재 우리의 Article 테이블에서는 
PK인 <code>{article_id}</code>로 Clustered Index가 생성되어있고,
<code>{board_id, article_id}</code> 로 Secondary Index를 생성한 상태이다.</p>
<p><strong>실행 쿼리</strong></p>
<pre><code class="language-sql">select * from article 
    where board_id = 1
    order by article_id desc
    limit 30 offset 1499970</code></pre>
<p><strong>예상 동작</strong></p>
<p>1) 세컨더리 인덱스의 리프 노드 시작점에서 1499970의 offset이 될 때까지 곧장 skip한다.</p>
<p>2) 30개의 포인터가 참조하는 클러스터드 인덱스의 데이터를 조회한다.</p>
<p><strong>실제 동작</strong></p>
<p>1) 세컨더리 인덱스의 리프 노드 시작점에서 클러스터드 인덱스의 데이터를 
<strong>전부 조회하면서</strong> 1499970의 offset이 될 때까지 skip한다.</p>
<p>2) 30개의 포인터가 참조하는 클러스터드 인덱스의 데이터를 조회한다.</p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/3ac30c35-bc89-44c2-8146-a4348a8e2823/image.png" alt=""></p>
<p>offset이 될 때까지 전부 조회하면서, offset이 클 경우 성능 문제가 발생하는 것이다.</p>
<blockquote>
<p><strong>예상대로 동작하지 않는 이유? (추정)</strong><br>
현재 실행하는 쿼리는 <code>select *</code>로 최종적으로 <strong>세컨더리 인덱스만으로는 확인할 수 없는 값을 조회한다.</strong>
InnoDB 스토리지 엔진은 이런 경우 <strong>where 조건에 부합하는지 여부를 확인하기 위해</strong> 실제 데이터를 전부 조회하는 과정을 거친다. <br>
물론 현재 쿼리는 세컨더리 인덱스에서 확인할 수 있는 컬럼인 <code>board_id</code>만으로 where 조건문이 구성되어있지만, 이는 특수한 경우이다.
쿼리 옵티마이저가 특수한 경우를 모두 캐치해서 최적화하지는 않는 것으로 추정된다.</p>
</blockquote>
<hr>
<h3 id="쿼리-튜닝">쿼리 튜닝</h3>
<h4 id="covering-index">Covering Index</h4>
<p>세컨더리 인덱스만으로 필요한 데이터 컬럼을 모두 가져올 수 있으면,
이를 커버링 인덱스라고 부르고 성능이 매우 우수해진다.</p>
<pre><code class="language-sql">select * from (
    select article_id from article //세컨더리 인덱스만으로 확인 가능
    where board_id = 1
    order by article_id desc
    limit 30 offset 144970
) t left join article on t.article_id = article.article_id;</code></pre>
<p>기존 인덱스 사용이 필요한 부분을 내부쿼리로 변경하고, 
커버링 인덱스가 되도록 article_id만 조회하였다.</p>
<p>이후 기존 테이블과 left join을 한다.</p>
<br>

<p><strong>튜닝 결과</strong>
<img src="https://velog.velcdn.com/images/jun-ha/post/12104434-ad59-4ddf-b5ab-7ce9441251ad/image.png" alt=""></p>
<p>같은 결과를 출력하는데 1.83 -&gt; 0.04초로 속도가 상당히 개선되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[0-1 Knapsack 변형]]></title>
            <link>https://velog.io/@jun-ha/0-1-Knapsack-%EB%B3%80%ED%98%95</link>
            <guid>https://velog.io/@jun-ha/0-1-Knapsack-%EB%B3%80%ED%98%95</guid>
            <pubDate>Wed, 07 May 2025 06:02:38 GMT</pubDate>
            <description><![CDATA[<p>DP의 대표격인 <code>0-1 Knapsack</code> 문제를 정리해보자.</p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/3f13c5e4-0404-46c5-ad16-23b063f31861/image.png" alt=""></p>
<p>조건</p>
<ul>
<li>아이템을 선택하거나 선택하지 않거나 둘 중 하나만 가능. (0-1)</li>
<li>각 아이템은 <strong>무게(Weight)</strong>와 <strong>가치(Value)</strong>를 지닌다.</li>
</ul>
<hr>
<h3 id="기본-knapsack">기본 Knapsack</h3>
<blockquote>
<p>최대 W의 무게에서, 최대 가치의 조합을 찾아라.</p>
</blockquote>
<p>기본 Knapsack 문제는 일차원 배열 혹은 이차원 배열로 해결할 수 있다.</p>
<h4 id="일차원-dp">일차원 DP</h4>
<ul>
<li><strong>dp[w] = 무게 w일 때의 최대 가치</strong> </li>
</ul>
<h4 id="이차원-dp">이차원 DP</h4>
<ul>
<li><strong>dp[i][w] = i번째 아이템까지 고려했을 때, 총 무게가 w일 때의 최대 가치</strong></li>
</ul>
<p>두 해결 방법 모두 시간복잡도는 <code>O(NW)</code>로 동일하지만, 
공간복잡도는 <code>O(W)</code>, <code>O(NW)</code>로 다르다.</p>
<p>따라서 아이템의 개수(N)가 많고, <strong>선택한 아이템을 역추적할 필요가 없으면</strong>,
일차원 DP로 해결하는 것이 적절하다.</p>
<h4 id="일차원-dp-알고리즘">일차원 DP 알고리즘</h4>
<pre><code class="language-java">int[] dp = new int[maxWeight];

//모든 아이템에 대해
for(int i = 0; i &lt; N; i++) {
    int tmpWeight = //현재 아이템 무게
    int tmpValue = //현재 아이템 가치

    // *중요 뒤에서부터 순회 (중복 선택 방지)
    for(int w = maxWeight; w &gt;= tmpWeight; w--) {
        dp[w] = Math.max(dp[w], dp[w - tmpWeight] + tmpValue);
    }
}
</code></pre>
<p><strong>점화식</strong></p>
<ul>
<li><code>dp[w] = Math.max(dp[w], dp[w - tmpWeight] + tmpValue)</code></li>
</ul>
<p>현재 무게(w) 에서의 최대 가치를, 
현재 아이템을 넣는 것을 고려했을 때의 가치와 비교하여 갱신한다.</p>
<p>앞에서부터 순회할 경우 현재 아이템을 중복으로 삽입하는 것이므로 주의.</p>
<h4 id="이차원-dp-알고리즘">이차원 DP 알고리즘</h4>
<pre><code class="language-java">int[] dp = new int[N][maxWeight]

//모든 아이템에 대해
for(int i = 0; i &lt; N; i++) {
    int tmpWeight = //현재 아이템 무게
    int tmpValue = //현재 아이템 가치

    for(int w = 0; w &lt; maxWeight; w++) {
        if(w &lt; tmpWeight) {
            dp[i][w] = dp[i-1][w]; //현재 아이템을 못넣음
        } else {
            dp[i][w] = Math.max(
                dp[i-1][w], //아이템을 안넣음
                dp[i-1][w - tmpWeight] + tmpValue //아이템을 넣음
            )
        }
    }
}
</code></pre>
<p><strong>점화식</strong></p>
<p>현재 무게(w) 보다 아이템의 무게가 큰 경우 (아이템을 넣지 못할때)</p>
<ul>
<li><code>dp[i][w] = dp[i-1][w]</code></li>
</ul>
<p>현재 무게(w) 가 아이템의 무게보다 크거나 같은 경우 (아이템을 넣을 수 있을 때)</p>
<ul>
<li><code>dp[i][w] = Math.max(dp[i-1][w], dp[i-1][w-tmpWeight] + tmpValue</code></li>
</ul>
<p>일차원 DP와 마찬가지로 현재 무게에서의 최대 가치를,
현재 아이템을 넣는 것을 고려했을 때의 가치와 비교하여 갱신하면 된다.</p>
<p><strong>선택 역추적</strong>
이차원 배열을 갱신할 때, 
아이템을 선택한 경우 <code>dp[i][w] = dp[i-1][w-tmpWeight] + tmpValue</code>로 갱신된다.</p>
<p>즉, <code>dp[i][w] == dp[i-1][w]</code> 인 경우 i 번째 아이템은 선택하지 않은 것이다.</p>
<p><code>dp[N][W]</code> 에서 시작하여, </p>
<ul>
<li><code>if(dp[i][w] == dp[i-1][w])</code> -&gt; i 번째 아이템은 선택 X, <code>i--</code></li>
<li><code>else</code> -&gt; i 번째 아이템 선택 O, <code>i--</code>, <code>w = w - weight[i]</code></li>
</ul>
<hr>
<h3 id="변형-knapsack">변형 Knapsack</h3>
<blockquote>
<p>V의 가치 이상을 만족하는 조합 중, 최소 무게를 찾아라.</p>
</blockquote>
<p>변형이라고는 하지만 위의 두 방식으로도 해결이 가능하다.</p>
<ul>
<li>1차원 배열을 처음부터 순회하며, 값이 V 이상인 최소 무게를 찾는다.</li>
<li>2차원 배열의 마지막 row를 순회하며 값이 V 이상인 최소 무게를 찾는다.</li>
</ul>
<p>하지만, 기본 Knapsack은 가방의 크기(최대 무게)를 문제에서 제약조건으로 제시하므로, 크기가 합리적인 수준인 경우가 대부분이다.</p>
<p>변형 Knapsack 같은 경우 각 아이템의 크기의 합이 엄청나게 커질 수 있으므로, 최대 무게(모든 아이템 무게의 합)에 맞춰 배열을 만들수 없는 경우가 발생한다.</p>
<p>그럴 때는 가치(V) 기준으로 배열을 만들어서 해결할 수 있다.</p>
<h4 id="일차원-dp-1">일차원 DP</h4>
<ul>
<li>dp[v] = 가치 V일 때의 최소 무게</li>
</ul>
<pre><code class="language-java">//maxValue = sum of all item value
int[] dp = new int[maxValue + 1]

Arrays.fill(dp, INF);
dp[0] = 0;

//모든 아이템에 대해
for(int i = 0; i &lt; N; i++) {
    int tmpWeight = //현재 아이템 무게
    int tmpValue = //현재 아이템 가치

    // *중요 뒤에서부터 순회 (중복 선택 방지)
    for(int v = maxValue; v &gt;= tmpValue; v--) {
        dp[v] = Math.min(dp[v], dp[v - tmpValue] + tmpWeight);
    }
}

int answer = 0;
for(int v = targetValue; v &lt;= maxValue; v++) {
    answer = Math.min(answer, dp[v]);
}</code></pre>
<p>시간복잡도는 <code>O(NW)</code>로 동일하지만, 공간복잡도가 <code>O(W)</code>가 아닌, <code>O(V)</code>로 변경되었다.</p>
<p>주어진 문제에서 무게에 비해 가치의 합이 크지 않을 경우 위 방법을 사용하면 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Java의 Volatile은 Read Committed와 유사하다.]]></title>
            <link>https://velog.io/@jun-ha/Java%EC%9D%98-Volatile%EC%9D%80-Read-Committed%EC%99%80-%EC%9C%A0%EC%82%AC%ED%95%98%EB%8B%A4</link>
            <guid>https://velog.io/@jun-ha/Java%EC%9D%98-Volatile%EC%9D%80-Read-Committed%EC%99%80-%EC%9C%A0%EC%82%AC%ED%95%98%EB%8B%A4</guid>
            <pubDate>Sun, 04 May 2025 06:34:27 GMT</pubDate>
            <description><![CDATA[<p>Java에서 동시성(concurrency)을 다루는 방법은 크게 세가지가 존재한다.</p>
<ol>
<li><code>synchronized</code></li>
<li><code>volatile</code></li>
<li><code>Atomic</code></li>
</ol>
<h3 id="volatile">Volatile</h3>
<p>이 중 <code>volatile</code>은 다른 스레드의 변경 사항에 대한 <strong>가시성(Visibility)</strong>을 확보시켜 준다. 
하지만 원자성을 보장하지 않기 때문에, 동시성 문제를 완전히 해결할 수 없다.</p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/9b299023-96b9-472c-9590-64955c035c72/image.png" alt=""></p>
<p>메모리 계층 구조에서 CPU 코어와 메인메모리 사이에는 캐시가 존재한다.</p>
<p>프로세서의 발전속도를 메인메모리가 따라가지 못하면서, 
<strong>CPU와 메인메모리 사이의 속도 간극을 줄여주는 완충제 역할</strong>을 위해 캐시를 도입한 것이다.</p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/af60fce9-324b-4036-992f-bad20f4414a6/image.png" alt=""></p>
<p>멀티코어 환경에서는 <strong>각 CPU 코어 별로 캐시메모리가 존재</strong>한다.</p>
<p><strong>즉, 멀티스레딩 시 서로 다른 스레드가 서로 다른 코어에서 실행되면, 
서로 다른 캐시를 사용하게 될 수 있다.</strong></p>
<p>이로인해 <strong>메모리 가시성 문제</strong>(다른 스레드의 변경 사항이 보이지 않음)가 발생하며, 이를 해결하기 위해 <code>volatile</code>을 사용할 수 있다.</p>
<p><code>volatile</code>은 JVM이 해당 변수의 읽기/쓰기를 <strong>항상 메인메모리 기준으로 강제한다.</strong>
즉, 어떤 스레드든 캐시된 값이 아닌, 최신 값을 공유하도록 보장한다.</p>
<hr>
<h3 id="read-committed-와의-유사성">Read Committed 와의 유사성</h3>
<p>트랜잭션 격리 수준 중 <code>Read Committed</code>은 커밋된 다른 트랜잭션의 변경사항을 볼 수 있게 해준다.</p>
<p>하지만, 그것만으로 쓰기 작업에 대한 동시성 문제를 해결할 수 없다.</p>
<blockquote>
<p>읽기 -&gt; (다른 트랜잭션의 쓰기 커밋) -&gt; 읽은 값에 기반한 쓰기</p>
</blockquote>
<p>위와 같은 시나리오에서 동시성 문제가 발생하고, <code>Read Committed</code> 만으로는 해결이 불가능하며, 별도의 락킹 메커니즘이 필요하다.</p>
<p>마찬가지로, <code>volatile</code>은 다른 스레드의 변경 사항에 대한 가시성은 확보할 수 있으나, 
복합 연산을 <strong>불가분한 연산</strong>으로 만들지 못하므로, 동시성 문제를 완벽히 해결할 수 없다.</p>
<hr>
<h3 id="volatile만으로-동시성-문제를-해결하려면">Volatile만으로 동시성 문제를 해결하려면?</h3>
<p>Read Only, Write Only 스레드를 나누어야 한다.</p>
<ul>
<li>Read 작업은 여러 스레드에서 동시에 가능</li>
<li>Write 작업은 오직 하나의 스레드에서만!</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[RestTemplate 타임아웃 적용]]></title>
            <link>https://velog.io/@jun-ha/RestTemplate-%ED%83%80%EC%9E%84%EC%95%84%EC%9B%83-%EC%A0%81%EC%9A%A9</link>
            <guid>https://velog.io/@jun-ha/RestTemplate-%ED%83%80%EC%9E%84%EC%95%84%EC%9B%83-%EC%A0%81%EC%9A%A9</guid>
            <pubDate>Mon, 03 Feb 2025 16:18:32 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/jun-ha/post/eff6284e-0d24-4a3e-80c1-4b722c35ed90/image.png" alt=""></p>
<p>여러 풋살 매칭 플랫폼에 요청을 보내 데이터를 가공하여 내보내는 &#39;풋살파인더&#39; 프로젝트가 있다.</p>
<p><strong>만약 요청을 보낸 여러 플랫폼 중 특정 플랫폼이 응답하지 않는다면?</strong>
응답하지 않는 플랫폼은 제외하고, 나머지 데이터만 가공해서 내려주어야 한다.</p>
<h4 id="기존-코드">기존 코드</h4>
<pre><code class="language-java">//플랫폼 별 요청코드
ResponseEntity&lt;String&gt; response = new RestTemplate().exchange(
    requestUrl,
    HttpMethod.GET,
    new HttpEntity&lt;&gt;(new HttpHeaders()),
    String.class
);</code></pre>
<p>기존코드는 <code>new RestTemplate()</code>으로 매 요청마다 새롭게 RestTemplate 객체를 생성하여 각 플랫폼에 요청을 보낸다.</p>
<p>이 방식은 타임아웃을 설정하지 않아, 특정 플랫폼이 응답하지 않는다면 상당히 오랜시간 대기하여 전체 응답속도가 매우 느려지는 문제가 발생하였다. </p>
<p>또한 매번 RestTemplate() 객체를 생성하는 것도 메모리 낭비가 발생한다.</p>
<h4 id="개선">개선</h4>
<pre><code class="language-java">@Configuration
public class RestTemplateConfig {

    @Bean
    public RestTemplate restTemplate() {
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectionRequestTimeout(Timeout.ofSeconds(3)) // 연결 타임아웃
                .setResponseTimeout(Timeout.ofSeconds(3))  // 읽기 타임아웃
                .build();

        CloseableHttpClient httpClient = HttpClients.custom()
                .setDefaultRequestConfig(requestConfig)
                .build();

        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);

        return new RestTemplate(factory);
    }

}</code></pre>
<p><code>RestTemplateConfig</code>를 통해 연결 타임아웃, 응답 타임아웃을 설정하고 
<code>RestTemplate</code>을 싱글톤으로 관리한다.</p>
<p>이후 RestTemplate를 주입받아서 사용하는 방식으로 변경하였다.</p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/76a00d9b-1fad-4b3d-b124-ad819cbbd4d5/image.png" alt=""></p>
<p>특정 헤더 값을 제외하고 요청을 보내보니, 설정한 타임아웃 만큼 시간이 지나자 예외가 발생하였고, 로그가 남은 것을 확인할 수 있다.</p>
<p>풋살파인더도 무한정 대기하지 않고, 응답이 오지않은 플랫폼을 제외하고 데이터를 응답하였다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터베이스 워밍업, 간단한 테스트]]></title>
            <link>https://velog.io/@jun-ha/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%9B%8C%EB%B0%8D%EC%97%85-%EA%B0%84%EB%8B%A8%ED%95%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8</link>
            <guid>https://velog.io/@jun-ha/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%9B%8C%EB%B0%8D%EC%97%85-%EA%B0%84%EB%8B%A8%ED%95%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8</guid>
            <pubDate>Fri, 24 Jan 2025 09:57:36 GMT</pubDate>
            <description><![CDATA[<p>MySQL <strong>InnoDB 버퍼 풀</strong>은 쿼리의 성능과 밀접하게 연결돼 있다.
버퍼 풀은 메모리 상에 적재되고, 애플리케이션의 DML을 통해 조작된다.</p>
<ul>
<li><p>조작하려는 데이터가 이미 버퍼풀에 존재한다면, <strong>메모리 I/O</strong> 만 일어나므로 애플리케이션은 빠른 응답을 받을 수 있다.</p>
</li>
<li><p>하지만 조작하려는 데이터가 버퍼풀에 존재하지 않는다면, 디스크로부터 데이터 페이지를 버퍼풀에 로드하는 과정이 선행되고, 이 과정은 <strong>디스크 I/O</strong>가 발생하므로 상대적으로 느리다.</p>
</li>
</ul>
<h3 id="속도-측정-테스트">속도 측정 테스트</h3>
<pre><code class="language-java">@RestController
@RequiredArgsConstructor
public class ExampleController {

    private final ExampleEntityRepository repository;

    @GetMapping(&quot;/test-buffer-hit&quot;)
    public String testBufferHit() {
        // 첫 번째 요청 (버퍼 풀에 없는 경우)
        long start1 = System.currentTimeMillis();
        Optional&lt;ExampleEntity&gt; entity1 = repository.findById(1L);
        long end1 = System.currentTimeMillis();

        // 두 번째 요청 (버퍼 풀에 있는 경우)
        long start2 = System.currentTimeMillis();
        Optional&lt;ExampleEntity&gt; entity2 = repository.findById(1L);
        long end2 = System.currentTimeMillis();

        return String.format(
                &quot;First Query Time: %d ms, Second Query Time: %d ms&quot;,
                (end1 - start1), (end2 - start2)
        );
    }
}</code></pre>
<p>간단히 속도 측정 테스트를 해보았다. 과정은 다음과 같다.</p>
<ol>
<li>MySQL 서버를 띄우고 데이터를 삽입한다.</li>
<li>MySQL 서버를 Shutdown 후, 다시 띄운다.</li>
<li>위 컨트롤러로 요청을 한다. </li>
</ol>
<p>2에서 서버를 Shutdown 하는 이유는 데이터 삽입 시 버퍼풀에 페이지가 존재하기 때문이다.</p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/e564c742-d017-42ba-977f-d5993c4e5396/image.png" alt=""></p>
<p>테스트 결과</p>
<ul>
<li>첫 번째 요청 : 71ms</li>
<li>두 번째 요청 : 6ms</li>
</ul>
<p>첫 번째 요청(버퍼풀에 데이터 X)은 <code>디스크 -&gt; 버퍼풀</code>로 데이터를 읽어오는 과정이 포함되어,
두 번째 요청(버퍼풀에 데이터 O)에 비해 상당히 느리게 처리된 것을 확인할 수 있다.</p>
<h3 id="워밍업warming-up">워밍업(Warming Up)</h3>
<p>워밍업은 쿼리에 사용되는 데이터가 버퍼 풀에 적재돼 있는 상태를 의미한다.</p>
<p>MySQL 5.5 버전 이하에서는 점검을 위해 MySQL 서버를 재시작해야하는 경우, 서비스를 오픈하기 전에 <strong>강제 워밍업을 위해 주요 테이블과 인덱스에 대해 풀 스캔을 한 번씩 실행하고 서비스를 오픈했다고 한다.</strong></p>
<p>하지만 MySQL 5.6 이후 버전에서는 버퍼 풀 덤프 및 적재 기능이 도입됐다.
서버 재시작시, 버퍼 풀의 백업과 복구를 할 수 있는 기능으로 
<code>innodb_buffer_pool_dump_at_shutdown</code> 과 <code>innodb_buffer_pool_load_at_startup</code> 설정을 MySQL 설정 파일에 넣어두면 된다고 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터베이스 - 이상현상, 함수적 종속성, 정규화]]></title>
            <link>https://velog.io/@jun-ha/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%9D%B4%EC%83%81%ED%98%84%EC%83%81-%ED%95%A8%EC%88%98%EC%A0%81-%EC%A2%85%EC%86%8D%EC%84%B1-%EC%A0%95%EA%B7%9C%ED%99%94</link>
            <guid>https://velog.io/@jun-ha/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%9D%B4%EC%83%81%ED%98%84%EC%83%81-%ED%95%A8%EC%88%98%EC%A0%81-%EC%A2%85%EC%86%8D%EC%84%B1-%EC%A0%95%EA%B7%9C%ED%99%94</guid>
            <pubDate>Thu, 23 Jan 2025 12:47:48 GMT</pubDate>
            <description><![CDATA[<h3 id="데이터베이스-이상현상anomalies">데이터베이스 이상현상(anomalies)</h3>
<p><strong>데이터베이스 이상현상</strong>이란 데이터베이스 설계가 잘못되었거나, 비정규화된 상태에서 발생하는 비효율적이고 부정확한 데이터 처리 문제를 의미한다.</p>
<p>아래 테이블로 <strong>삽입이상</strong>, <strong>갱신이상</strong>, <strong>삭제이상</strong>의 예시를 들어보자. </p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/41c0635f-13e0-4488-b6e7-dea9d4e3c625/image.png" alt=""></p>
<p>사진출처 - <a href="https://dev-coco.tistory.com/63">https://dev-coco.tistory.com/63</a></p>
<h4 id="삽입이상">삽입이상</h4>
<ul>
<li><p>데이터 삽입 시 불필요한 데이터도 함께 삽입해야하는 문제</p>
</li>
<li><p>&#39;컴퓨터 네트워크&#39; 라는 새로운 강의가 추가되었을 때, 아직 수강하는 학생이 없을 수 있다. 그럼에도 데이터를 추가하기 위해서는 불필요한 학생 정보를 함께 삽입해야 한다.</p>
</li>
</ul>
<h4 id="갱신이상">갱신이상</h4>
<ul>
<li><p>데이터 중복으로 인해 일부만 수정하면 불일치가 발생하는 문제</p>
</li>
<li><p>&#39;김현수&#39; 학생의 전화번호를 수정했을 때, 중복된 모든 열을 찾아서 수정하지 않으면 불일치가 발생한다. </p>
</li>
</ul>
<h4 id="삭제이상">삭제이상</h4>
<ul>
<li><p>특정 데이터 삭제 시, 다른 연관된 데이터도 삭제되는 문제</p>
</li>
<li><p>&#39;이병철&#39; 학생의 정보를 삭제했더니, &#39;알고리즘&#39; 강의의 정보도 함께 삭제된다.</p>
</li>
</ul>
<hr>
<h3 id="함수적-종속성functional-dependency">함수적 종속성(Functional Dependency)</h3>
<p>함수적 종속성이란 특정 속성 집합(attribute)이 다른 속성 집합을 유일하게 결정 짓는 관계를 의미한다.</p>
<p>예를 들어, Primary Key는 테이블의 모든 속성을 유일하게 결정 지으므로 
{PK} -&gt; {All Attributes} 의 FD가 성립한다.</p>
<p>함수적 종속성은 개념적인 Schema로 판명해야한다. 특정 순간의 Instance로 판단하면 안된다. </p>
<p>즉, 함수적 종속성은 <strong>데이터 간의 본질적인 관계</strong>를 나타내며, 특정 순간의 데이터 상태가 아니라 데이터 스키마 설계를 기준으로 평가해야 한다. </p>
<h4 id="1-완전-함수적-종속성full-functional-dependency">1) 완전 함수적 종속성(Full Functional Dependency)</h4>
<p>완전 함수적 종속성이란 어떤 속성들이 <strong>PK 전체에 의존하는 것을 의미한다.</strong>
<strong>PK의 일부만으로 결정되지 않고, 반드시 전체에 의존해야한다.</strong></p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/41c0635f-13e0-4488-b6e7-dea9d4e3c625/image.png" alt=""></p>
<p><code>{학번, 강의코드}</code>는 테이블에서 각 행을 고유하게 식별할 수 있는 최소 속성 집합이다.</p>
<ul>
<li><code>{학번, 강의코드}</code> -&gt; <code>{이름}</code></li>
<li><code>{학번, 강의코드}</code> -&gt; <code>{나이}</code></li>
<li><code>{학번, 강의코드}</code> -&gt; <code>{성별}</code></li>
<li><code>{학번, 강의코드}</code> -&gt; <code>{강의명}</code></li>
<li><code>{학번, 강의코드}</code> -&gt; <code>{전화번호}</code></li>
</ul>
<p>위와 같이 테이블의 모든 속성에 대해 함수적 종속성이 존재한다.
하지만, 이는 <strong>완전 함수적 종속성을 만족하지 않는다.</strong></p>
<ul>
<li><code>{학번}</code> -&gt; <code>{이름, 나이, 성별, 전화번호}</code></li>
<li><code>{강의코드}</code> -&gt; <code>{강의명}</code></li>
</ul>
<p>위와 같이 PK의 일부 속성 만으로 특정 속성을 결정 지을 수 있기 때문이다.</p>
<p>만약 테이블이 <code>{학번, 이름, 나이, 성별, 전화번호}</code> 만으로 구성되어 있고, PK가 <code>{학번}</code> 이라면, 완전 함수적 종속성을 만족할 것이다.</p>
<h4 id="2-부분-함수적-종속성partial-functional-dependency">2) 부분 함수적 종속성(Partial Functional Dependency)</h4>
<p>부분 함수적 종속이란, <strong>기본키의 일부 속성만으로 특정 속성을 결정할 수 있을 때 발생한다.</strong> 
위의 예시처럼 PK는 <code>{학번, 강의코드}</code> 인데, <code>{학번}</code> 만으로 결정지을 수 있는 속성들이 존재하는 상황이다.</p>
<h4 id="3-이행적-함수적-종속성transitive-functional-dependency">3) 이행적 함수적 종속성(Transitive Functional Dependency)</h4>
<p>이행적 함수적 종속성이란, </p>
<ul>
<li><code>{A}</code> -&gt; <code>{B}</code></li>
<li><code>{B}</code> -&gt; <code>{C}</code></li>
</ul>
<p>의 FD가 존재할 때,</p>
<ul>
<li><code>{A}</code> -&gt; <code>{C}</code> 의 함수적 종속성이 발생하는 상황을 의미한다.</li>
</ul>
<p><strong>이러한 현상은 주로 PK에 포함되지 않은 일반 속성이 또 다른 일반 속성을 결정 지을 때 발생한다.</strong> (<code>PK</code> -&gt; <code>{A}</code> -&gt; <code>{B}</code>)</p>
<p>위의 예시를 다시 살펴보면, <code>{전화번호}</code> -&gt; <code>{이름}</code> 으로의 FD가 존재한다. </p>
<ul>
<li><code>{학번, 강의코드}</code> -&gt; <code>{전화번호}</code> -&gt; <code>{이름}</code> 의 이행적 함수 종속이 존재하는 것이다.</li>
</ul>
<hr>
<h3 id="데이터베이스-정규화normalization">데이터베이스 정규화(Normalization)</h3>
<p>데이터베이스 정규화란 Good Form(정규형)을 정하고, 모든 테이블이 Good Form이 될 때까지 Lossless Decomposition(무손실 분해)을 반복적으로 수행하는 것을 의미한다.</p>
<p><strong>Lossless Decomposition</strong>(무손실 분해)란, 분해된 테이블을 다시 조인했을 때, 데이터 손실이 없는 것을 의미한다.</p>
<blockquote>
<p><strong>무손실 분해의 조건</strong>
Relation R을 R1과 R2로 분해할 때, 아래 둘 중 하나의 조건을 만족해야한다.</p>
</blockquote>
<ul>
<li>{R1 ∩ R2} -&gt; R1 </li>
<li>{R1 ∩ R2} -&gt; R2 </li>
</ul>
<p>이를 통해 불필요한 데이터의 중복을 줄이며, 이상현상을 방지할 수 있다.</p>
<h4 id="제-1-정규형">제 1 정규형</h4>
<ul>
<li>컬럼이 원자값(Atomic Value, 하나의 값)을 갖도록 테이블을 분해하는 것이다.</li>
</ul>
<h4 id="제-2-정규형">제 2 정규형</h4>
<ul>
<li>제 1 정규형을 만족한 테이블에 대해 <strong>완전 함수 종속</strong>을 만족시킨다. 즉, <strong>부분적 함수 종속</strong>을 제거한다.</li>
</ul>
<h4 id="제-3-정규형">제 3 정규형</h4>
<ul>
<li>제 2 정규형을 만족한 테이블에 대해 <strong>이행적 함수 종속</strong>을 제거한다.
즉, 기본키를 제외한 속성들 간의 함수적 종속성이 존재하면 안된다.</li>
</ul>
<h4 id="bcnf-정규형">BCNF 정규형</h4>
<ul>
<li>제 3 정규형을 만족한 테이블에 대해 모든 결정자가 후보키 집합에 속해야 한다.</li>
<li>즉, 후보키 집합에 속하지 않은 일반 속성이 결정자가 되면 안된다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[CI/CD 구축 - no basic auth credentials 에러 해결]]></title>
            <link>https://velog.io/@jun-ha/CICD-%EA%B5%AC%EC%B6%95-no-basic-auth-credentials-%EC%97%90%EB%9F%AC</link>
            <guid>https://velog.io/@jun-ha/CICD-%EA%B5%AC%EC%B6%95-no-basic-auth-credentials-%EC%97%90%EB%9F%AC</guid>
            <pubDate>Tue, 21 Jan 2025 09:59:53 GMT</pubDate>
            <description><![CDATA[<p>도커 컨테이너 기반의 프로젝트에서 CI/CD 적용하면서 겪은 과정을 정리하려고 한다.</p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/050049df-ab32-43e9-841a-b30398064a4f/image.png" alt=""></p>
<hr>
<h3 id="cicd-과정">CI/CD 과정</h3>
<h4 id="githubworkflowsdeployyml">.github/workflows/deploy.yml</h4>
<pre><code class="language-yml">name: Deploy To EC2

on:
  push:
    branches:
      - main
      - dev

jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      APP_NAME: liveblog-server
    steps:
      - name: Github Repository 파일 불러오기
        uses: actions/checkout@v4

      - name: JDK 17버전 설치
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 17

      - name: application.yml 파일 만들기
        run: echo &quot;${{ secrets.APPLICATION_PROPERTIES }}&quot; &gt; ./src/main/resources/application.yml

      - name: 테스트 및 빌드하기
        run: ./gradlew clean build

      - name: AWS Resource에 접근할 수 있게 AWS credentials 설정
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-region: ap-northeast-2
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

      - name: ECR에 로그인하기
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Docker 이미지 생성
        run: docker build -t $APP_NAME .

      - name: Docker 이미지에 Tag 붙이기
        run: docker tag $APP_NAME ${{ steps.login-ecr.outputs.registry }}/$APP_NAME:latest

      - name: ECR에 Docker 이미지 Push하기
        run: docker push ${{ steps.login-ecr.outputs.registry }}/$APP_NAME:latest

      - name: SSH로 EC2에 접속하기
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_PRIVATE_KEY }}
          script_stop: true
          script: |
            APP_NAME=${{ env.APP_NAME }}
            docker stop $APP_NAME || true
            docker rm $APP_NAME || true
            docker pull ${{ steps.login-ecr.outputs.registry }}/$APP_NAME:latest
            docker run -d --name $APP_NAME -p 80:8080 ${{ steps.login-ecr.outputs.registry }}/$APP_NAME:latest</code></pre>
<p>대략적인 Flow는 다음과 같다.</p>
<ol>
<li><p>Git Push</p>
</li>
<li><p>application.yml 파일을 GitHub Secrets에서 복사한다.</p>
</li>
<li><p>Docker 이미지를 생성한다. (docker build -t .)</p>
<ul>
<li>프로젝트 디렉토리 하위에 Dockerfile을 정의.</li>
</ul>
</li>
<li><p>ECR에 Docker 이미지를 push</p>
</li>
<li><p>SSH로 EC2 접속, ECR에서 도커 이미지를 다운 후 동작</p>
</li>
</ol>
<blockquote>
<p>빌드 과정에서 application.yml을 GitHub Secrets에 저장한 후 복사해 사용하는 방식은 민감한 정보를 코드베이스에 노출하지 않아 보안을 강화할 수 있다.</p>
</blockquote>
<hr>
<h3 id="에러-발생">에러 발생</h3>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/55f114cd-3302-4b84-bea1-bb3d442a7e3f/image.png" alt=""></p>
<p><strong>SSH로 EC2 접속하기</strong> 부분에서<br><code>no basic auth credentials</code> 에러가 지속적으로 발생하면서 실패하였다.</p>
<p>결과적으로 아래 3번 권한 설정이 누락되어 발생한 문제임을 깨닫고 해결하였다.</p>
<hr>
<h3 id="권한-설정">권한 설정</h3>
<p>위 flow가 성공하려면 세가지 권한 설정이 필요하다.</p>
<ol>
<li><p><strong>Github Actions</strong>가 <strong>AWS ECR</strong>에 접근 가능해야한다. (이미지 푸시)</p>
</li>
<li><p><strong>Github Actions</strong>가 <strong>EC2 인스턴스</strong>에 접근 가능해야한다. (SSH 접속)</p>
</li>
<li><p><strong>EC2 인스턴스</strong>가 <strong>AWS ECR</strong>에 접근 가능해야한다. (이미지 다운)</p>
</li>
</ol>
<p>3의 권한 설정이 필요한 이유는 ECR의 <strong>Private Repository</strong>에 이미지를 저장하는 방식을 선택했기 때문이다.</p>
<h4 id="1-github-actions---aws-ecr">1. Github Actions -&gt; AWS ECR</h4>
<p>Github Actions가 내 AWS Resource에 접근하려면 
<strong>적절하게 권한이 설정된 IAM 유저의 <code>Access Key</code>와 <code>Secret Access Key</code>가 필요하다.</strong></p>
<p><strong><code>Access Key</code></strong>는 유저를 식별하는 ID, <strong><code>Secret Access Key</code></strong>는 Password 같은 개념이다.</p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/b8f95431-7907-4157-81fe-791941a19d5f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/19efe6d3-9fef-4da5-88f3-77d5ba694a54/image.png" alt=""></p>
<p>이를 위해 CI-CD 라는 이름의 User Group을 만들고, IAM User 하나를 생성해 연결하였다.</p>
<p>CI-CD 유저 그룹에는 <strong><code>AmazonEC2ContainerRegistryFullAccess</code></strong>(ECR) 정책을 설정한다.</p>
<pre><code class="language-yml">name: AWS Resource에 접근할 수 있게 AWS credentials 설정
uses: aws-actions/configure-aws-credentials@v4
with:
    aws-region: ap-northeast-2
    aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
    aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

name: ECR에 로그인하기
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2</code></pre>
<p>이렇게 생성한 유저의 Access Key, Secret Access Key를 통해 Github Actions에서 ECR에 로그인한다.</p>
<blockquote>
<p>AWS는 <strong>관리 효율성</strong>, <strong>일관성 보장</strong>, <strong>보안 강화</strong> 측면에서 유저 그룹에 정책을 할당하고, 유저를 해당 그룹에 추가하는 방식을 사용하는 것을 강력히 권장한다. </p>
</blockquote>
<br>

<h4 id="2-github-actions---ec2-인스턴스">2. Github Actions -&gt; EC2 인스턴스</h4>
<p>EC2 인스턴스는 <code>.pem</code> 키를 통한 SSH 방식으로 접속하기 때문에 따로 권한 설정이 필요하지 않다. 
따라서 CI-CD 유저 그룹에도 EC2 인스턴스 관련 정책은 포함되지 않는다.</p>
<br>

<h4 id="3-ec2-인스턴스---aws-ecr">3. EC2 인스턴스 -&gt; AWS ECR</h4>
<p>EC2 인스턴스가 ECR의 <strong>Private Repository</strong>에 접근하려면 적절한 정책이 설정된 <strong>Role</strong> 을 부여해야한다.</p>
<p>EC2 인스턴스는 유저가 아니기 때문에 Role을 사용해야한다.</p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/4e3cb954-b499-4caa-b8a9-60f01fc4e4d3/image.png" alt=""></p>
<p>유저 그룹과 비슷하게 EC2-ECR 라는 Role을 만들고 <strong><code>AmazonEC2ContainerRegistryFullAccess</code></strong> 정책을 설정한다.</p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/039fd325-0434-461e-a4ba-efc7f5b407d0/image.png" alt=""></p>
<p>EC2 인스턴스에 생성한 Role을 부여한다.</p>
<p>또한 EC2 내부에서 <code>amazon-ecr-credential-helper</code> 설정을 추가적으로 해주어야한다. </p>
<p><em>참조</em> <a href="https://github.com/awslabs/amazon-ecr-credential-helper?tab=readme-ov-file">https://github.com/awslabs/amazon-ecr-credential-helper?tab=readme-ov-file</a></p>
<p>이제 EC2 에서 ECR의 Private Repository 로 접근이 가능하다.</p>
<hr>
<h3 id="문제-해결">문제 해결</h3>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/6953bd06-57bf-484d-9490-2e005123e8cb/image.png" alt=""></p>
<p>앞서 언급한 <code>no basic auth credentials</code> 에러는 (3)의 권한을 제대로 부여하지 않아 생긴 문제였다.</p>
<p>EC2에 Role을 부여하니 해결되었다.</p>
<hr>
<h3 id="ecr---life-cycle-policy">ECR - Life Cycle Policy</h3>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/fd73e2f3-7a72-4cd1-bc3b-21a56c895a32/image.png" alt=""></p>
<p>ECR에 Docker 이미지를 업로드하면, latest가 아닌 이전 버전의 이미지들이 지속적으로 남아있게된다.</p>
<p>ECR은 <strong>사용하는 용량만큼 요금이 청구되기 때문에 이를 주기적으로 삭제해야 한다.</strong></p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/97ac6813-1491-4ef1-bbbe-3578a5ac8845/image.png" alt=""></p>
<p>Life Cycle Policy를 통해 자동으로 삭제가 가능하며, </p>
<p>untagged 이미지는 삭제하는 정책을 할당하였다.</p>
<p>정책을 할당해도 곧바로 삭제되지는 않았는데, 이는 AWS에서 주기적으로 처리하기 때문에 시간이 걸린다고 한다.</p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/75ab7c1e-5ec1-4e84-91d6-9a45a2fcc0d1/image.png" alt=""></p>
<p>시간이 지나자 이전 버전의 이미지들이 자동으로 삭제되는 것을 확인할 수 있다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[캐시 설계 전략]]></title>
            <link>https://velog.io/@jun-ha/%EC%BA%90%EC%8B%B1-%EC%A0%84%EB%9E%B5</link>
            <guid>https://velog.io/@jun-ha/%EC%BA%90%EC%8B%B1-%EC%A0%84%EB%9E%B5</guid>
            <pubDate>Fri, 17 Jan 2025 19:09:20 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/jun-ha/post/92889d3a-cc3a-4765-8b89-e59f880806e2/image.png" alt=""></p>
<p>게시글 참고: <a href="https://inpa.tistory.com/entry/REDIS-%F0%9F%93%9A-%EC%BA%90%EC%8B%9CCache-%EC%84%A4%EA%B3%84-%EC%A0%84%EB%9E%B5-%EC%A7%80%EC%B9%A8-%EC%B4%9D%EC%A0%95%EB%A6%AC?pidx=7">inpa.tistory.com - REDIS-📚-캐시Cache-설계-전략-지침-총정리</a></p>
<p>캐시는 일반적으로 메모리를 사용하기 때문에 데이터베이스 보다 훨씬 빠르게 응답이 가능하다. </p>
<p>하지만 캐시는 데이터 정합성 문제를 야기할 가능성이 있으며, 메모리가 상대적으로 비용이 높기 때문에 효율적인 캐시 설계 전략이 중요하다.</p>
<h3 id="캐시-읽기-전략read-cache-strategy">캐시 읽기 전략(Read Cache Strategy)</h3>
<h4 id="look-aside-패턴">Look Aside 패턴</h4>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/c8e65e96-d6f3-411f-b275-3a8302ed2e45/image.png" alt=""></p>
<ul>
<li><p>데이터를 찾을 때, <strong>우선적으로 캐시를 확인하는 전략.</strong>
만일 캐시에 데이터가 없을 경우 DB에서 조회.</p>
</li>
<li><p>반복적인 읽기가 많은 호출에 적합하며, 원하는 데이터만 별도로 구성하여 캐시에 저장한다.</p>
</li>
</ul>
<p>Look Aside 방식은 캐시에 문제가 발생하더라도, DB에 요청을 전달함으로써 서비스 문제는 대비할 수 있다. 하지만 캐시와 DB 간 정합성 문제가 발생할 수 있으며 초기 조회 시 무조건 DB를 호출해야 하므로 단건 호출 빈도가 높은 서비스에 적합하지 않다. 대신 <strong>반복적으로 동일 쿼리를 수행하는 서비스</strong>에 적합한 아키텍처이다. </p>
<h4 id="read-through-패턴">Read Through 패턴</h4>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/cd41d07a-fe39-41a5-87c6-ab59130ed488/image.png" alt=""></p>
<ul>
<li><strong>캐시에서만 데이터를 읽어오는 전략</strong></li>
<li>데이터 동기화를 캐시에 위임하여 조회 속도가 느릴 수 있다.</li>
<li><strong>캐시와 DB간 항상 데이터 동기화가 유지된다.</strong></li>
</ul>
<p>Look Aside 패턴과 유사하지만, <strong>항상 캐시를 통해 데이터를 읽는 패턴</strong>이다.
캐시와 DB 간 데이터가 동기화 되지만, 캐시에 문제가 발생할 경우 서비스 전체에 장애가 발생할 수 있다. 
그렇기 때문에 Replication 등을 활용해 고가용성을 구축하는 것이 중요하다.</p>
<hr>
<h3 id="캐시-쓰기-전략write-cache-strategy">캐시 쓰기 전략(Write Cache Strategy)</h3>
<h4 id="write-back-패턴">Write Back 패턴</h4>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/fd9837a2-14ac-4ebc-9a2e-b9b4071decc3/image.png" alt=""></p>
<ul>
<li><strong>데이터를 저장할때, DB가 아닌 캐시에 저장</strong></li>
<li>캐시에서는 일정 주기 배치 작업을 통해 DB에 반영.</li>
<li>캐시에서 오류가 발생하면 데이터가 소실될 수 있음.</li>
</ul>
<p>Write Back 방식은 데이터를 저장할때 DB가 아닌 먼저 캐시에 저장하여 모아놓았다가 특정 시점마다 DB로 쓰는 방식으로 캐시가 일종의 Queue 역할을 겸하게 된다.</p>
<p>캐시 읽기 전략인 Read-Through와 결합하면 가장 최근에 업데이트된 데이터를 항상 캐시에서 사용할 수 있는 혼합 워크로드에 적합하다.</p>
<h4 id="write-through-패턴">Write Through 패턴</h4>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/118d422d-c7ca-47f6-88ae-d06a86ced3ef/image.png" alt=""></p>
<ul>
<li><strong>데이터베이스와 Cache에 동시에 데이터를 저장하는 전략</strong></li>
<li>캐시와 DB의 데이터가 동기화 됨</li>
<li>매번 두 단계의 쓰기 과정을 거치기 때문에 상대적으로 느림.</li>
</ul>
<blockquote>
<p>write throuth 패턴과 write back 패턴 둘 다 모두 자주 사용되지 않는 데이터가 저장되어 리소스 낭비가 발생되는 문제점을 안고 있기 때문에, 이를 해결하기 위해 TTL을 꼭 사용하여 사용되지 않는 데이터를 반드시 삭제해야 한다. (expire 명령어)</p>
</blockquote>
<h4 id="write-around-패턴">Write Around 패턴</h4>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/66ef983b-a6c7-4ef3-bd43-f36793c2cc63/image.png" alt=""></p>
<ul>
<li><strong>모든 데이터를 DB에 저장</strong></li>
<li>캐시를 통한 쓰기 방식보다 훨씬 빠름</li>
<li>캐시와 DB의 데이터가 동기화 되지 않을 수 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터베이스 인덱스]]></title>
            <link>https://velog.io/@jun-ha/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%9D%B8%EB%8D%B1%EC%8A%A4</link>
            <guid>https://velog.io/@jun-ha/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-%EC%9D%B8%EB%8D%B1%EC%8A%A4</guid>
            <pubDate>Thu, 16 Jan 2025 13:38:39 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[채팅 시스템 디자인]]></title>
            <link>https://velog.io/@jun-ha/%EC%B1%84%ED%8C%85-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EB%94%94%EC%9E%90%EC%9D%B8</link>
            <guid>https://velog.io/@jun-ha/%EC%B1%84%ED%8C%85-%EC%8B%9C%EC%8A%A4%ED%85%9C-%EB%94%94%EC%9E%90%EC%9D%B8</guid>
            <pubDate>Thu, 16 Jan 2025 13:31:40 GMT</pubDate>
            <description><![CDATA[<h3 id="1-요구사항">1. 요구사항</h3>
<p>1000명이 동시에 채팅할 수 있는 Group-Chat 시스템을 설계해보자.</p>
<p>TPS = 1000 이라고 가정한다.</p>
<h3 id="2-설계">2. 설계</h3>
<h4 id="2-1-웹소켓-연결">2-1 웹소켓 연결</h4>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/51f91311-f3e3-42c1-a4b8-12a5f3262e52/image.png" alt=""></p>
<p>1000명의 유저가 단 하나의 서버에 웹소켓을 연결하고 메시지를 주고받는다면, 
서버 과부하가 발생할 수 있어 안정적인 서비스를 지원할 수 없다.</p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/670e1947-8305-4931-8cc2-14cd1be9d584/image.png" alt=""></p>
<p>이를 해결하기 위해 유저와 웹소켓을 연결하는 Chat Server를 수평적으로 확장하고, 각 서버의 과부하를 모니터링하는 서버를 앞단에 두는 방식을 생각해보았다.</p>
<p>Flow는 다음과 같다.</p>
<ol>
<li><p>유저는 모니터링 서버에 웹소켓 연결이 가능한 서버 IP를 요청한다.</p>
</li>
<li><p>모니터링 서버는 과부하가 적은 Chat Server의 IP를 반환한다.</p>
</li>
<li><p>반환받은 IP로 웹소켓을 연결한다.</p>
</li>
<li><p>웹소켓 연결 후 서버는 Key-Value 저장소에 연결정보를 저장한다.</p>
</li>
</ol>
<p>로드밸런서를 통한 Proxy 방식은, 로드밸런서의 과부하가 발생할 수 있다고 판단하여 초기 연결에만 관여하는 모니터링 서버를 도입하는 방식을 생각해보았다.</p>
<p>또한 추후에 메시지를 보낼 때, 각 유저가 어떤 서버와 연결되어있는지 알아야 메시지를 보낼 수 있으므로 Key-Value 저장소에 연결정보를 저장한다.</p>
<p>메시지를 빠르게 보내려면 Redis와 같은 저장소를 쓰는 것이 좋을 것 같다.</p>
<h4 id="2-2-메시지-발행">2-2 메시지 발행</h4>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/80cea8ca-f2e8-4cde-bd90-f8dbbdbf8216/image.png" alt=""></p>
<p>메시지 발행의 Flow는 다음과 같다.</p>
<ol>
<li>유저가 웹소켓으로 연결된 서버로 메시지를 보낸다.</li>
<li>서버는 데이터베이스에 특정 채팅방에 속한 유저 아이디를 요청한다.</li>
<li>응답받은 유저 아이디를 기반으로 Key-Value 저장소에 각 유저가 연결된 서버 IP를 요청한다.</li>
<li>응답받은 서버 IP에 Relay 요청을 보낸다.</li>
<li>각 서버는 연결된 유저에 메시지를 보낸다.</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[InnoDB 스토리지 엔진]]></title>
            <link>https://velog.io/@jun-ha/InnoDB-%EC%8A%A4%ED%86%A0%EB%A6%AC%EC%A7%80-%EC%97%94%EC%A7%84</link>
            <guid>https://velog.io/@jun-ha/InnoDB-%EC%8A%A4%ED%86%A0%EB%A6%AC%EC%A7%80-%EC%97%94%EC%A7%84</guid>
            <pubDate>Thu, 09 Jan 2025 14:06:03 GMT</pubDate>
            <description><![CDATA[<p>MySQL의 스토리지 엔진 중 기본적으로 가장 많이 사용되는 엔진은 <strong>InnoDB</strong> 이다. </p>
<p>InnoDB는 MySQL의 스토리지 엔진 중 거의 유일하게 <strong>레코드 기반의 잠금</strong>을 제공하며, 그 때문에 <strong>높은 동시성 처리가 가능하고 안정적이며 성능이 뛰어나다.</strong></p>
<h2 id="innodb-아키텍처-및-특징">InnoDB 아키텍처 및 특징</h2>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/9a5a6185-14b8-4fdc-9909-407509d7cccf/image.png" alt=""></p>
<h3 id="프라이머리-키에-의한-클러스터링">프라이머리 키에 의한 클러스터링</h3>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/ddd995e9-9f7b-49c5-aae7-b071b410779e/image.png" alt=""></p>
<p><strong>클러스터링</strong>이란 여러 개를 하나로 묶는다는 의미이다.</p>
<p><strong>클러스터링 인덱스(키)</strong>는 <strong>인덱스(키)</strong> 값이 비슷한 레코드끼리 묶어서 저장하는 형태를 의미한다. <strong>(공간적 지역성)</strong></p>
<p>InnoDB의 모든 테이블은 PK를 기준으로 클러스터링 되어 저장된다.
<strong>즉, PK 값에 의해 실제 레코드의 물리적 저장 위치가 결정된다.</strong></p>
<p>반면, 세컨더리 키는 실제 레코드의 주소가 아닌 PK를 참조한다.</p>
<h4 id="클러스터링-인덱스의-장단점">클러스터링 인덱스의 장단점</h4>
<p><strong>장점</strong></p>
<ul>
<li><p><strong>PK(클러스터링 키)로 검색할 때 처리 성능이 매우 빠름(특히, PK에 의한 범위 검색)</strong></p>
</li>
<li><p>모든 세컨더리 인덱스가 PK를 가지고 있기 때문에, 인덱스 만으로 처리될 수 있는 경우가 많음. <strong>(커버링 인덱스)</strong></p>
</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li><p>PK 값이 클 경우, 모든 세컨더리 인덱스 저장 공간이 커짐</p>
</li>
<li><p>세컨더리 인덱스를 통해 검색할 때, PK 를 통해 다시 한 번 검색해야함</p>
</li>
<li><p>INSERT 할 때, PK 값에 의해 레코드 저장 위치가 결정되므로 처리 성능이 느림</p>
</li>
<li><p>PK 변경 시(잘 일어나진 않지만) 레코드를 DELETE 하고 INSERT 해야하므로 처리 성능이 느림</p>
</li>
</ul>
<blockquote>
<p><strong>커버링 인덱스</strong>란, 인덱스만으로 필요한 데이터를 가져올 수 있는 인덱스를 의미한다. 만약 age에 인덱스가 걸려있고, age &gt; 30 이상인 모든 유저의 PK를 알고 싶다면, InnoDB는 age 인덱스가 PK를 참조하므로 실제 데이터가 있는 테이블을 조회하지 않아도 된다.</p>
</blockquote>
<p>클러스터링 인덱스의 장점은 <strong>빠른 읽기(SELECT)</strong>이며, 단점은 <strong>느린 쓰기(INSERT, UPDATE, DELETE)</strong>라는 것을 알 수 있다.</p>
<p>일반적인 웹 서비스는 읽기와 쓰기의 비율이 <code>8:2</code> 혹은 <code>9:1</code> 정도이기 때문에 *<em>조금 느린 쓰기를 감수하고 읽기를 빠르게 유지하는 것이 좋다고 한다. *</em></p>
<p><strong>InnoDB</strong>와 달리 <strong>MyISAM</strong> 엔진은 클러스터링 키를 지원하지 않으며, 모든 인덱스는 물리적인 레코드의 주소 값(ROWID)를 가진다.</p>
<hr>
<h3 id="외래-키fk-지원">외래 키(FK) 지원</h3>
<p><strong>MyISAM</strong>, <strong>MEMORY</strong> 스토리지 엔진과 달리 <strong>InnoDB</strong>는 외래 키를 지원한다.</p>
<p>외래 키는 부모와 자식 테이블 모두 <strong>해당 칼럼에 인덱스 생성이 필요</strong>하고, 변경 시 <strong>잠금이 여러 테이블로 전파되므로 데드락 발생을 유념해야 한다.</strong></p>
<hr>
<h3 id="mvccmulti-version-concurrency-control">MVCC(Multi Version Concurrency Control)</h3>
<p>MVCC는 <strong>레코드 레벨의 트랜잭션</strong>을 지원하는 DBMS가 제공하는 기능이며, <strong>가장 큰 목적은 잠금을 사용하지 않는 일관된 읽기를 제공하는 것</strong>이다.</p>
<p>하나의 레코드에 대해 여러 버전이 동시에 관리되며,
InnoDB는 <strong>InnoDB 버퍼풀</strong>과 <strong>Undo Log</strong>를 사용하여 이를 구현한다.</p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/08184346-4009-4c21-9dde-36c973e97547/image.png" alt=""></p>
<p>위와 같이 INSERT 문이 실행된 이후의 상황에서, 버퍼 풀에 있는 특정 레코드를 업데이트 한다면 어떻게 될까? (m_area 서울 -&gt; 경기)</p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/11c07931-bcd9-48d4-b095-e170151ce263/image.png" alt=""></p>
<p>UPDATE가 실행되면, 커밋 여부와 관계없이 <strong>버퍼 풀의 데이터를 즉시 수정하고, 언두 로그에 변경 이전 값을 복사한다.</strong></p>
<p>만약 커밋이 일어나기 전에 다른 트랜잭션에서 레코드를 조회하면,
<code>READ_UNCOMMITTED</code> 격리 수준에서는 <strong>버퍼 풀</strong>에 있는 값을,</p>
<p><code>READ_COMMITTED</code>, <code>REPEATABLE_READ</code>, <code>SERIALIZABLE</code> 에서는 아직 커밋되지 않았기 때문에, 변경 이전인 <strong>언두 로그</strong>에서 값을 읽어서 반환한다.</p>
<h4 id="잠금-없는-일관된-읽기">잠금 없는 일관된 읽기</h4>
<p>트랜잭션 격리수준을 보장하기 위해 락을 활용하는 기법들이 있지만, 동시성 처리 성능이 떨어진다. 
<strong>InnoDB</strong>에서 <code>READ_COMMITTED</code>, <code>REPEATABLE_READ</code> 수준의 읽기 작업은 잠금을 대기하지 않고 곧바로 실행되기 때문에 동시 처리 성능이 뛰어나다.</p>
<hr>
<h3 id="innodb-버퍼-풀">InnoDB 버퍼 풀</h3>
<h4 id="이점">이점</h4>
<p>InnoDB 버퍼 풀은 디스크의 데이터 파일이나 인덱스 정보를 메모리에 캐시해 두는 공간이다. <strong>쓰기 작업을 지연</strong>시켜 일괄 작업으로 처리할 수 있게 해주는 버퍼 역할도 같이 한다. </p>
<p><strong>쓰기 작업의 지연은 어떤 이점을 가져올까?</strong></p>
<h4 id="data-access-patterns">Data Access Patterns</h4>
<p>디스크에 저장된 데이터에 접근하는 패턴은 <code>Random</code>과 <code>Sequential</code>이 있다.</p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/267b7781-3120-4f79-a63b-f1b8110246d6/image.png" alt=""></p>
<p>HDD와 같은 저장장치에서 Random Access는 데이터가 존재하는 위치로 헤더가 물리적으로 이동하는데 시간이 소모된다.</p>
<p>반면 Sequential Access는 마지막으로 헤더가 움직인 위치에 지속적으로 데이터를 쓰기 때문에, 헤더가 움직이는 물리적 시간이 소모되지 않는다.</p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/04e2b79a-ab57-4f08-8806-4f8a1bae7874/image.png" alt=""></p>
<p><a href="https://www.youtube.com/watch?v=UNUz1-msbOM">System Design: Why is Kafka fast?</a> 에 따르면 두 패턴은 성능에 엄청난 차이를 보인다.</p>
<p>일반적인 애플리케이션에서는 INSERT, UPDATE, DELETE 처럼 데이터를 변경하는 쿼리는 데이터 파일의 이곳저곳에 위치한 레코드를 변경하기 때문에 <strong>Random Access 작업을 발생시킨다.</strong></p>
<p>InnoDB 버퍼 풀과 쓰기 지연을 통해 이러한 Random Access를 모아서 처리하면 그 횟수를 줄여 성능이 개선된다.</p>
<hr>
<h4 id="버퍼-풀과-리두-로그">버퍼 풀과 리두 로그</h4>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/c0b3bbd0-e064-4b0b-ab82-217431baf4f9/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DBMS -  Relational Model & Algebra]]></title>
            <link>https://velog.io/@jun-ha/DBMS-Relational-Model-Algebra</link>
            <guid>https://velog.io/@jun-ha/DBMS-Relational-Model-Algebra</guid>
            <pubDate>Sat, 04 Jan 2025 02:56:21 GMT</pubDate>
            <description><![CDATA[<p><em>CMU Andy Pavlo 교수님의 강의를 정리한 내용입니다.</em>
<a href="https://www.youtube.com/watch?v=APqWIjtzNGE&amp;list=PLSE8ODhjZXjYDBpQnSymaectKjxCy6BYq&amp;t=892s">#01 - Relational Model &amp; Algebra (CMU Intro to Database Systems) - Andy Pavlo</a></p>
<h3 id="파일-저장-방식의-문제점">파일 저장 방식의 문제점</h3>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/9bfa7b85-3761-4699-b023-4400f10df9a4/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/8334a82f-d2e7-4a25-a57d-cb0d9da2b014/image.png" alt=""></p>
<p>데이터를 컴마로 구분된, 파일에 저장하면 어떤 문제점이 발생하는가?
그리고 이를 프로그래밍 언어를 통해 직접 파싱해서 다뤄야 한다면?</p>
<h4 id="data-integrity">Data Integrity</h4>
<ul>
<li><p>만약 누군가가 year를 저장해야하는 곳에 Invalid String을 삽입하면? </p>
</li>
<li><blockquote>
<p>도메인 무결성 X</p>
</blockquote>
</li>
<li><p>하나의 앨범에 여러 아티스트가 존재하면? </p>
</li>
<li><blockquote>
<p>파일의 구조를 변경해야하고, 작성한 코드는 더 이상 사용할 수 없다.</p>
</blockquote>
</li>
<li><p>아티스트를 삭제한다면? </p>
</li>
<li><blockquote>
<p>앨범 파일에서의 삭제 처리를 보장할 수 있는가? </p>
</blockquote>
</li>
<li><p>동일한 아티스트가 여러 앨범을 만들었을 경우, 같은 아티스트임을 어떻게 보장할 수 있는가? </p>
</li>
<li><blockquote>
<p>참조 무결성 X</p>
</blockquote>
</li>
</ul>
<h4 id="implementation">Implementation</h4>
<ul>
<li><p>특정 레코드를 어떻게 찾을 것인가?</p>
</li>
<li><blockquote>
<p>파일의 내용을 full-scan 해야만 한다.</p>
</blockquote>
</li>
<li><p>같은 데이터베이스를 사용하는 애플리케이션을 하나 더 만든다면? 그리고 그것이 서로 다른 Machine 에서 작동한다면?</p>
</li>
<li><blockquote>
<p>전체를 Copy, Paste?</p>
</blockquote>
</li>
<li><p>서로 다른 스레드에서 동시에 파일에 접근한다면?</p>
</li>
</ul>
<h4 id="durability">Durability</h4>
<ul>
<li><p>레코드를 업데이트하는 도중 프로그램이 멈추면?</p>
</li>
<li><blockquote>
<p>트랜잭션 지원 X</p>
</blockquote>
</li>
<li><p>Replication 등을 활용해 고가용성을 지원할 수 있는가?</p>
</li>
</ul>
<p><strong>DBMS는 위와 같은 문제들을 해결하는 소프트웨어이다.</strong></p>
<hr>
<h3 id="data-models">Data Models</h3>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/0cf6cfcc-e276-41f0-a631-d33031e5b03e/image.png" alt=""></p>
<p><strong>Data Model</strong>은 데이터베이스에 데이터가 어떤 형식으로 저장되는지 묘사한 것.</p>
<p><strong>Schema</strong>는 특정 Data Model을 기반으로 데이터의 형식을 표현한 것.</p>
<p>대부분의 DBMS는 관계형 모델이다.</p>
<h3 id="data-independence-데이터-독립성">Data Independence (데이터 독립성)</h3>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/5238a94c-0fe7-4a9a-a71d-afa19c433fbc/image.png" alt=""></p>
<h4 id="3단계-데이터-구조">3단계 데이터 구조</h4>
<ul>
<li><p><strong>Physical Schema</strong>
: 데이터의 물리적 저장구조를 정의한다</p>
</li>
<li><p><strong>Logical Schema</strong>
: 데이터의 논리적 구조를 정의한다.</p>
</li>
<li><p><strong>External Schema</strong>
: 뷰등 사용자 관점에서 데이터베이스를 바라보는 관점을 정의한다.</p>
</li>
</ul>
<p>관계형 모델의 구조적 핵심은 <strong>데이터 독립성</strong>이다.
<strong>데이터베이스의 테이블과 내용을, 물리적인 저장 방식과 독립적으로 다루는 것이다.</strong></p>
<p>DBMS를 사용하는 애플리케이션 개발자는 데이터가 물리적으로 어떻게 저장되는지 알 필요가 없다. 개발자는 오직 high-level의 애플리케이션 로직에만 집중하면 된다.  (추상화) </p>
<p>그에 따라 <strong>물리적인 저장장치가 변경되어도, 애플리케이션의 코드가 변경될 필요가 없다.</strong></p>
<ul>
<li><p><strong>Physical Data Independence(물리적 데이터 독립성)</strong>
: <strong>Physical Schema</strong>(데이터의 물리적 저장방식)가 변경되어도 <strong>Logical Schema</strong>에 영향을 주지않는다. 데이터베이스의 논리적 구조를 변경하지 않고도 저장 장치를 변경할 수 있다.</p>
</li>
<li><p><strong>Logical Data Independence(논리적 데이터 독립성)</strong>
: <strong>Logical Schema</strong>(논리적 구조)가 변경되어도, <strong>External Schema</strong>(애플리케이션에서의 사용)에 영향을 주지 않는다. 애플리케이션의 코드를 변경하지 않고도 데이터베이스의 논리 구조를 변경할 수 있다.</p>
</li>
</ul>
<hr>
<h3 id="document-data-model">Document Data Model</h3>
<p>Document Data Model은 Object와 Relation의 불일치를 피하기 위해 등장했다.
<strong>객체와 데이터베이스를 강하게 결합하는 방식이다.</strong></p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/70e32428-192d-4951-a206-95aa8b090d46/image.png" alt=""></p>
<p>Relational 모델은 위와 같이 Artist와 Album 간의 관계를 맺고, ArtistAlbum 테이블을 생성하는 방식으로 데이터를 저장한다. 하지만 이러한 방식은 객체지향의 세계와 일치하지 않는다. </p>
<p><strong>객체지향에서는 참조자를 통해 다른 객체를 포함하지만, 관계형 모델은 외래키를 통해 테이블을 조인해야만 한다.</strong>  </p>
<p><strong>조인에는 많은 비용이 소모되고, Document Model을 고안한 사람들은 이러한 조인 비용이 좋지않다고 생각했다(교수님 왈)</strong></p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/b2fa30b8-791d-4113-9147-90e5c3b34305/image.png" alt=""></p>
<p><strong>Document 모델은 객체지향에서 한 객체가 다른 객체를 포함하듯이, 데이터를 저장한다.</strong></p>
<p>마치 Artist 객체가 Album 객체의 리스트를 포함하듯, Artist 내부에 Json Document 형식으로 데이터를 저장한다.</p>
<p>조인 비용이 소모되지 않으므로, 빠른 데이터 접근이 가능하다.</p>
<h4 id="document-모델의-문제점">Document 모델의 문제점</h4>
<p>하지만 이러한 방식은 어떤 문제점이 있을까?</p>
<ul>
<li><p><strong>데이터 중복</strong>
: 하나의 앨범이 여러 아티스트가 작업했다면? 각 아티스트마다 동일한 앨범정보가 포함될 것이다. </p>
</li>
<li><p><strong>추상화 X</strong>
: 내 애플리케이션이 데이터를 다루기 위해 데이터 구조를 잘 알아야한다. 즉, 추상화가 되지 않는다. 데이터 구조가 변경되면 애플리케이션 코드도 변경되어야 한다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[MySQL 엔진 아키텍처]]></title>
            <link>https://velog.io/@jun-ha/MySQL-%EC%97%94%EC%A7%84-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98</link>
            <guid>https://velog.io/@jun-ha/MySQL-%EC%97%94%EC%A7%84-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98</guid>
            <pubDate>Thu, 02 Jan 2025 20:18:45 GMT</pubDate>
            <description><![CDATA[<h2 id="아키텍처">아키텍처</h2>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/7459d3da-3361-4a16-8442-c2c5a2ce400e/image.png" alt=""></p>
<p>MySQL 서버는 크게 <strong>MySQL 엔진</strong>과 <strong>스토리지 엔진</strong>으로 구분할 수 있다.</p>
<p>MySQL 엔진은 사람의 머리와 같은 역할로, <strong>쿼리를 최적화 하고 실행계획을 수립한다.</strong></p>
<p>스토리지 엔진은 사람의 손발과 같은 역할로, <strong>실제 데이터를 디스크 스토리지에 읽고 쓰는 역할</strong>을 맡는다.</p>
<p>스토리지 엔진은 <strong>핸들러 API</strong>를 만족하면 누구든지 구현해서 MySQL 서버에 추가해서 사용이 가능하다. 또한 <strong>여러 스토리지 엔진을 동시에 사용할 수 있으며, 테이블마다 다른 스토리지 엔진을 사용할 수 있다.</strong></p>
<h3 id="mysql-엔진">MySQL 엔진</h3>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/3841ea26-73bb-444e-93fe-10bd4a1e55cc/image.png" alt=""></p>
<p>MySQL 엔진은 클라이언트의 접속 및 쿼리 요청을 처리하는 <strong>커넥션 핸들러</strong>와 <strong>SQL 파서</strong> 및 <strong>전처리기</strong>, 최적화된 실행을 위한 <strong>옵티마이저</strong>가 중심을 이룬다.</p>
<ul>
<li><p><strong>SQL 파서</strong>
: 들어온 <strong>SQL을 토큰으로 분리하여 트리 형태의 구조로 만들어낸다.</strong> 이 과정에서 기본 문법 오류들이 발견되고, 사용자에게 오류 메시지를 전달한다.</p>
</li>
<li><p><strong>전처리기</strong>
: <strong>파서 트리를 기반으로 구조적인 문제를 파악</strong>한다. 파싱된 각 토큰을 <strong>실제 객체와 매핑</strong>하고, 존재 여부와 접근 권한 등을 확인한다. 존재하지 않거나, 접근 권한이 없는 경우 이 단계에서 걸러진다.</p>
</li>
<li><p><strong>옵티마이저</strong>
: DBMS의 두뇌라고 할 수 있는 옵티마이저는 사용자의 쿼리를 <strong>가장 저렴한 비용으로 가장 빠르게 처리할 수 있도록 변환하고, 실행 계획을 수립한다.</strong></p>
</li>
<li><p><strong>쿼리 실행기(실행 엔진)</strong>
: 옵티마이저에 의해 만들어진 각 계획대로 스토리지 엔진(핸들러)에 직접 수행하는 역할을 한다. <strong>실행 계획의 각 단계에서 만들어진 결과를 다음 단계의 입력으로 연결하는 역할을 수행한다.</strong> </p>
</li>
</ul>
<br>

<h3 id="스토리지-엔진핸들러">스토리지 엔진(핸들러)</h3>
<p>스토리지 엔진은 <strong>핸들러 API를 구현</strong>하며, 디스크에 읽고 쓰는 역할을 담당한다.</p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/ed177791-b796-4d0d-91de-000cd79839b7/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/85240178-f12e-4e71-9b7e-3a11efec6c04/image.png" alt=""></p>
<p>핸들러 API는 <code>SHOW GLOBAL STATUS LIKE &#39;Handler%;&#39;</code>명령어로 확인이 가능하며, 얼마나 많은 데이터 작업이 있었는지 확인할 수 있다.</p>
<p>스토리지 엔진은 <strong>InnoDB</strong>, <strong>MyISAM</strong>, <strong>MEMORY</strong> 등이 있으며, 그 중 가장 많이 사용되는 엔진은 <strong>InnoDB 스토리지 엔진</strong>이다.</p>
<p>각 스토리지 엔진은 성능 향상을 위해 <strong>키 캐시(MyISAM)</strong>나 <strong>버퍼 풀(InnoDB)</strong>과 같은 기능을 내장하고 있다.</p>
<hr>
<h2 id="mysql-스레딩--구조">MySQL 스레딩  구조</h2>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/cb2589aa-e614-4ef2-aa5c-40d8cd9f811a/image.png" alt=""></p>
<p>MySQL 서버는 프로세스 기반이 아닌 스레드 기반으로 동작하며, 크게 <strong>포그라운드(Foreground) 스레드</strong>, <strong>백그라운드(Background) 스레드</strong>로 구분할 수 있다.</p>
<p>MySQL 서버에서 실행 중인 스레드 목록은 performance_schema 데이터베이스의 threads 테이블을 통해 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/5d75ff51-299a-4c88-b23e-2c73340fb22d/image.png" alt=""></p>
<h3 id="foreground-스레드">Foreground 스레드</h3>
<p>포그라운드 스레드는 최소한 서버에 접속된 클라이언트의 수만큼 존재하며, 주로 각 클라이언트 <strong>사용자가 요청하는 쿼리 문장을 처리한다.</strong></p>
<p>클라이언트가 MySQL에 접속하면 <strong>서버는 클라이언트의 요청을 처리해 줄 스레드를 생성해 그 클라이언트에게 할당한다.</strong> </p>
<p>포그라운드 스레드는 데이터를 MySQL의 데이터 버퍼나 캐시로부터 가져오며, 버퍼나 캐시에 없는 경우 직접 디스크의 데이터나 인덱스 파일로부터 데이터를 읽어와서 작업을 처리한다.</p>
<p><strong>MyISAM</strong> 스토리지 엔진은 <strong>디스크 쓰기 작업까지 포그라운드 스레드가 처리</strong>하지만, 
<strong>InnoDB</strong> 스토리지 엔진은 <strong>데이터 버퍼나 캐시까지만 포그라운드 스레드가 처리</strong>하고, 나머지 <strong>버퍼로부터 디스크까지의 쓰기 작업은 백그라운드 스레드가 처리</strong>한다.</p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/58a23911-bebe-4178-85b2-8cbdc5650519/image.png" alt=""></p>
<p>Spring-boot 웹 애플리케이션을 하나 띄웠을 때, 위와 같이 포그라운드 스레드가 10개 추가로 생성된 것을 확인할 수 있다.</p>
<p>그런데 왜 포그라운드 스레드가 10개나 생성된 것일까? 
그 이유는 <strong>데이터베이스 커넥션 풀(Connection pool)</strong> 설정 때문이다.</p>
<p><strong>HikariCP</strong>(Spring boot 기본 데이터베이스 커넥션 풀링 라이브러리)는 최대 성능을 보장하기 위해 <strong>초기화 시 미리 커넥션을 생성하고 유지</strong>하는데, 이 동작이 MySQL 서버에 연결된 <strong>활성 스레드 수를 증가시킨다.</strong></p>
<p>애플리케이션 시작 시, 기본적으로 10개의 커넥션을 생성하여 풀에 보관하고, MySQL 서버는 이 10개의 커넥션을 유지하기 위해 10 개의 포그라운드 스레드를 생성하는 것이다. </p>
<p><strong>즉, MySQL 서버는 기본적으로 1:1로 커넥션과 스레드를 매핑한다.</strong></p>
<p>Spring boot의 데이터베이스 커넥션 풀 설정</p>
<pre><code class="language-yml">spring:
  datasource:
    hikari:
      maximum-pool-size: 10 //최대 커넥션 수
      minimum-idle: 5 //최소 유휴 커넥션 수</code></pre>
<p>위와 같이 커넥션 풀 설정이 가능하며, 이에 따라 MySQL 서버의 포그라운드 스레드 수도 달라진다.</p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/0562820f-8a4a-4f0c-9268-970d687d8c17/image.png" alt=""></p>
<p>애플리케이션 시작 시, <code>minumum-idle</code> 만큼 5개의 포그라운드 스레드만 생성된 것을 확인할 수 있다.</p>
<p>만약 애플리케이션이 동시에 5개의 커넥션을 모두 사용하면서 새로운 커넥션을 요청한다면, 최대 10개까지 커넥션을 새롭게 생성할 것이며, 그에따라 <strong>MySQL의 포그라운드 스레드 수도 늘어날 것이다.</strong></p>
<h3 id="백그라운드-스레드">백그라운드 스레드</h3>
<p>InnoDB는 다음과 같이 여러 작업이 백그라운드로 처리된다.</p>
<ul>
<li>Insert Buffer를 병합하는 스레드</li>
<li>로그를 디스크로 기록하는 스레드</li>
<li>InnoDB 버퍼 풀의 데이터를 디스크로 기록하는 스레드</li>
<li>데이터를 버퍼로 읽어 오는 스레드</li>
<li>잠금이나 데드락을 모니터링하는 스레드</li>
</ul>
<p>InnoDB를 포함한 일반적인 상용 DBMS에는 대부분 <strong>쓰기 작업을 버퍼링해서 일괄 처리하는 기능</strong>이 탑재되어있다. </p>
<p>이러한 이유로 InnoDB에서는 INSERT, UPDATE, DELETE 쿼리로 데이터가 변경되는 경우 <strong>데이터가 디스크의 데이터 파일로 완전히 저장될 때까지 기다리지 않아도 된다.</strong> 
(이는 백그라운드 쓰기 스레드가 처리한다)</p>
<hr>
<h2 id="메모리-할당-및-사용-구조">메모리 할당 및 사용 구조</h2>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/d7d2bc9c-52c7-4961-b3da-10757a0f14e6/image.png" alt=""></p>
<p>MySQL에서 사용되는 메모리 공간은 크게 <strong>글로벌 메모리 영역</strong>과 <strong>로컬 메모리 영역</strong>으로 나뉜다.</p>
<p><strong>글로벌 메모리 영역</strong>은 클라이언트 스레드의 수와 무관하게 하나의 메모리 공간만 할당되고, 모든 스레드에 의해 공유된다. </p>
<p><strong>로컬(세션) 메모리 영역</strong>은 클라이언트 스레드가 쿼리를 처리하는 데 사용되는 영역이다. </p>
<hr>
<h2 id="플러그인-스토리지-엔진-모델">플러그인 스토리지 엔진 모델</h2>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/39a2889f-227f-4893-ba0c-c631893201b4/image.png" alt=""></p>
<h4 id="플러그인-모델">플러그인 모델</h4>
<p>MySQL의 독특한 구조 중 대표적인 것은 <strong>플러그인 모델</strong>이다.</p>
<p>각 스토리지 엔진을 플러그인의 형태로 사용할 수 있으며 뿐만 아니라 검색 엔진을 위한 검색어 파서, 인증 기능도 모두 플러그인으로 구현되어 제공된다. </p>
<p>사용자가 직접 스토리지 엔진 플러그인을 개발하더라도, <strong>사람의 머리 역할을 하는 MySQL 엔진은 그대로 동작한다.</strong>
따라서 이는 DBMS 전체 기능이 아닌 일부분의 기능만 수행하는 엔진을 작성하는 것이다.</p>
<h4 id="컴포넌트">컴포넌트</h4>
<p>MySQL 8.0 부터는 기존의 플러그인 아키텍처를 대체하기 위해 <strong>컴포넌트 아키텍처</strong>가 지원된다.</p>
<p>플러그인의 단점은 다음과 같다.</p>
<ul>
<li>플러그인은 오직 MySQL 서버와 인터페이스할 수 있고, <strong>플러그인끼리는 통신 불가</strong></li>
<li>플러그인은 <strong>MySQL의 서버 변수나 함수를 직접 호출</strong>하기 때문에 안전하지 않음(캡슐화 안됨)</li>
<li>플러그인은 <strong>상호 의존관계를 설정할 수 없어서 초기화 어려움</strong></li>
</ul>
<p>컴포넌트는 이러한 단점들을 보완해서 구현되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[@Transactional은 동시성 문제를 해결해주지 않는다.]]></title>
            <link>https://velog.io/@jun-ha/Transactional%EC%9D%80-%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C%EB%A5%BC-%ED%95%B4%EA%B2%B0%ED%95%B4%EC%A3%BC%EC%A7%80-%EC%95%8A%EB%8A%94%EB%8B%A4</link>
            <guid>https://velog.io/@jun-ha/Transactional%EC%9D%80-%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C%EB%A5%BC-%ED%95%B4%EA%B2%B0%ED%95%B4%EC%A3%BC%EC%A7%80-%EC%95%8A%EB%8A%94%EB%8B%A4</guid>
            <pubDate>Thu, 26 Dec 2024 09:12:32 GMT</pubDate>
            <description><![CDATA[<h3 id="회원가입-로직">회원가입 로직</h3>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class RegisterUserService implements RegisterUserUseCase {

    private final UserRepository userRepository;

    @Override
    @Transactional
    public void register(RegisterCommand command) {
        if(userRepository.existsByUsername(command.getUsername())) {
            throw new DuplicateUsernameException(ErrorCode.DUPLICATE_USERNAME, &quot;이미 존재하는 유저 이름입니다.&quot;);
        } //이름이 같은 유저가 존재할 경우, 예외를 던진다.

        User user = new User(command.getUsername(), command.getPassword());

        userRepository.save(user);
    }
}</code></pre>
<p>회원가입을 처리하는 <code>RegisterUserService</code> 구현체이다. </p>
<p>회원가입 로직은,
<strong>중복된 유저 이름이 발생하지 않도록 하기 위해</strong> </p>
<ol>
<li>데이터베이스에 같은 이름이 존재하는지 확인한다.</li>
<li>존재한다면, 예외를 발생시킨다. 이 예외는 @ControllerAdvice에서 캐치되어 클라이언트에게 에러 응답을 내려준다.</li>
<li>존재하지 않는다면, 데이터베이스에 저장한다.</li>
</ol>
<p>간단해보이는 이 코드는 의도대로 <strong>중복된 이름의 회원가입을 방지</strong>하며 제대로 동작할까?</p>
<p>JMeter를 통해 10개의 스레드로 동시요청을 보내보았다.</p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/1b5734ce-a79d-4c4f-8172-cb0a3b6795e4/image.png" alt=""></p>
<p>의도대로라면, 10개의 동일한 요청 중 <strong>첫번째 요청만 성공하고 나머지는 실패</strong>해야하지만,</p>
<p>위와 같이 10개 중 <strong>3개의 요청이 성공</strong>하는 것을 확인할 수 있다.</p>
<hr>
<h3 id="원인">원인</h3>
<pre><code class="language-java">@Override
@Transactional
public void register(RegisterCommand command) {
    if(userRepository.existsByUsername(command.getUsername())) {
        throw new DuplicateUsernameException(ErrorCode.DUPLICATE_USERNAME, &quot;이미 존재하는 유저 이름입니다.&quot;);
    }
    /**
     이 지점에 여러 스레드가 동시에 진입할 수 있다.
    */
    User user = new User(command.getUsername(), command.getPassword());

    userRepository.save(user);
}</code></pre>
<p>문제의 원인은 첫번째 <code>userRepository.save(user)</code> 가 호출되기 이전에, 
동시에 여러 스레드에서 <code>userRepository.existsByUsername()</code> 조건문을 통과할 가능성이 있다는 점이다.</p>
<p>부끄럽지만 지금까지 @Transactional 어노테이션이 동시성 문제를 해결해준다고 생각했다. 
(ACID 중 Isolation 특성이, 한 트랜잭션에서 메서드를 호출하면 다른 트랜잭션이 호출할 수 없게 &#39;고립&#39; 시킨다고 생각했다라나 뭐라나..)</p>
<hr>
<h3 id="해결방안">해결방안</h3>
<h4 id="엔티티의-unique-제약조건"><strong>엔티티의 unique 제약조건</strong></h4>
<pre><code class="language-java">@Entity
public class User {
    //...

    @Column(nullable = false, unique = true)
    private String username;
}
</code></pre>
<p>위와 같이 Entity의 username 컬럼에 unique constraint를 걸어준다. 그러면 여러 스레드가 
동시에 조건문을 통과하더라도, 데이터베이스에 <strong>저장하는 시점에 제약조건에 의해 단 하나의 스레드만 성공하게 된다.</strong></p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/3583377f-73f8-487a-a7a2-0da21cec0649/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/9987d160-ab47-419f-9355-432b88191727/image.png" alt=""></p>
<p>테스트 결과, 단 하나의 요청만 OK 응답을 받았다.</p>
<p>하지만 위와 같이 <strong>실패 응답의 status code가 서로 다른 문제가 발생한다.</strong></p>
<p>이는 여전히 일부 스레드는 조건문을 통과하여 핸들링하지 않은 예외(유니크 제약조건을 위반할 시)를 발생시키기 때문이다.</p>
<p>사실 unique 제약 조건을 건 순간, 앞단의 조건문은 의미가 없다. 따라서 코드를 다음과 같이 개선하였다.</p>
<pre><code class="language-java">@Service
@RequiredArgsConstructor
public class RegisterUserService implements RegisterUserUseCase {

    private final UserRepository userRepository;

    @Override
    @Transactional
    public void register(RegisterCommand command) {
        try {
            userRepository.save(
                    new User(command.getUsername(), command.getPassword())
            );
        } catch (DataIntegrityViolationException e) {
            throw new DuplicateUsernameException(ErrorCode.DUPLICATE_USERNAME, &quot;이미 존재하는 유저 이름입니다.&quot;);
        }
    }
}</code></pre>
<p>곧바로 데이터베이스에 저장 쿼리를 날리고, <strong>유니크 제약조건 위반 예외가 발생하면, 애플리케이션의 예외로 변환하는 방식</strong>이다.</p>
<p>그렇게 다시 한 번 테스트를 시도했는데..</p>
<h4 id="또-다른-문제">또 다른 문제</h4>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/b8787f03-f706-40e8-b163-1ffcffdcb528/image.png" alt=""></p>
<p>엥? 어째서인지 예외가 캐치되지 않고, 콘솔에 그대로 스택 트레이스가 출력되었다.</p>
<p><code>DataIntegrityViolationException</code> 이 아닌가? 싶어서 
<code>Exception</code> 으로 바꿔서 <strong>모든 예외를 캐치하도록 했음에도, 여전히 콘솔에 스택 트레이스가 출력되었다.</strong></p>
<h4 id="원인-1">원인</h4>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/100855ce-82a9-4338-9310-d9f574fc4db4/image.png" alt=""></p>
<p>ChatGPT에게 이유를 물어보았고 여러 답변이 나왔다. 그 중 위 답변을 보며 <strong>JPA의 영속성 컨텍스트(Persistence Context)</strong>를 떠올리게 되었다.</p>
<p>@Transactional 어노테이션을 통해 <strong>영속성 컨텍스트(캐시)</strong> 내에서 작업이 이루어지게 되고, <strong>실제 DB에 쿼리가 날아가는 flush()가 호출되는 시점은 트랜잭션이 종료되는 시점이다.</strong> </p>
<pre><code class="language-java">@Override
@Transactional
public void register(RegisterCommand command) {
    try {
        userRepository.save(
                new User(command.getUsername(), command.getPassword())
        ); //여기서 실제 DB에 쿼리를 날리지 않는다. 
    } catch (DataIntegrityViolationException e) {
        throw new DuplicateUsernameException(ErrorCode.DUPLICATE_USERNAME, &quot;이미 존재하는 유저 이름입니다.&quot;);
    }

    //여기서 DB에 쿼리가 날아간다. 따라서 예외가 캐치되지 않는다.
}</code></pre>
<p><code>userRepository.save()</code> 를 하는 것은 엔티티를 <strong>persist</strong> 상태로 만드는 행위이며 이는, 엔티티 매니저에의해 관리되는 영속 상태를 만드는 것을 의미한다.</p>
<p><strong>따라서 이 시점에는 DB에 쿼리를 날리지 않아, 유니크 제약조건 위반이 확인되지 않는다.</strong></p>
<h4 id="개선">개선</h4>
<pre><code class="language-java">@Override
@Transactional
public void register(RegisterCommand command) {
    try {
        userRepository.save(
                new User(command.getUsername(), command.getPassword())
        );
        userRepository.flush(); //try-catch 문 내부에서 flush()를 진행
    } catch (DataIntegrityViolationException e) {
        throw new DuplicateUsernameException(ErrorCode.DUPLICATE_USERNAME, &quot;이미 존재하는 유저 이름입니다.&quot;);
    }
}</code></pre>
<p>위와 같이 <code>try-catch</code>문 내부에서 곧바로 flush()를 호출하는 방식으로 수정하였다.</p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/5babba20-1d2d-4aaf-94c3-bf566ff61b80/image.png" alt=""></p>
<p>콘솔에 스택 트레이스가 출력되지 않고, 유니크 제약조건 위반 예외가 애플리케이션의 예외로 변환되어 제대로 핸들링 된 것을 확인할 수 있다!</p>
<hr>
<h3 id="역할과-책임">역할과 책임</h3>
<h4 id="의문점">의문점</h4>
<p>해결은 했으나 아직 여러 의문이 들었다.</p>
<p>이렇게 엔티티의 unique constraint를 통해 해결하는 것이 과연 적절할까?</p>
<p><strong>&#39;중복된 username은 허용하지 않는다&#39;</strong> 는 것은 <strong>비즈니스의 규칙</strong>이다.
따라서 <strong>도메인 레이어(서비스)</strong>에서 이를 다룰 <strong>책임</strong>이 있다고 생각한다.</p>
<pre><code class="language-java">@Override
@Transactional
public void register(RegisterCommand command) {
    try {
        userRepository.save(
                new User(command.getUsername(), command.getPassword())
        );
        userRepository.flush();
    } catch (DataIntegrityViolationException e) {
        throw new DuplicateUsernameException(ErrorCode.DUPLICATE_USERNAME, &quot;이미 존재하는 유저 이름입니다.&quot;);
    }
}</code></pre>
<p>위 코드는 &#39;중복된 username은 허용하지 않는다&#39;는 <strong>비즈니스 규칙이 잘 드러나는가?</strong> (애플리케이션 예외로 변환하면서 잘 드러나는 것 같기도 하고)</p>
<p>unique 제약조건을 통해 해결하는 것은, <strong>도메인 레이어의 책임을 영속성 레이어로 전가하는 것은 아닌가?</strong></p>
<hr>
<h4 id="트레이드-오프">트레이드 오프</h4>
<p>다시 ChatGPT에게 물었다.
일리가 있는 주장이나, <strong>트레이드 오프</strong>이므로 장단점을 잘 파악해야한다는 답변을 주었다.</p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/bbc5f57f-0884-458d-983d-da97b42dd874/image.png" alt=""></p>
<p>문제가 있던 기존 코드는 확실히 비즈니스 규칙이 잘 드러난다. 
-&gt; 하지만 <strong>동시성 문제가 발생</strong>한다.</p>
<p>유니크 제약 조건을 사용하여 <strong>경쟁상태를 원천적으로 차단</strong>하였다. 이를 통해 중복 이메일 확인 조건도 삭제할 수 있었다.
-&gt; 코드 중복이 줄고, 중복 방지 로직을 쉽게 구현할 수 있었다.
-&gt; 하지만 비즈니스 로직과 데이터베이스 설계가 결합되었다.
-&gt; <strong>비즈니스 규칙의 변경이 필요할 경우, 데이터베이스 설계를 수정해야한다.</strong></p>
<p>정답은 없는 것 같다. 어떤 선택에든 트레이드 오프가 따르기 마련이다.</p>
<p>비즈니스 규칙이 변경될 가능성이 극히 적은데도, 변경에 열려있는 설계를 하기 위해 너무 많이 고민하는 것은 오버 엔지니어링이 아닐까 싶다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Monad란 무엇인가]]></title>
            <link>https://velog.io/@jun-ha/Monad%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80</link>
            <guid>https://velog.io/@jun-ha/Monad%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80</guid>
            <pubDate>Wed, 18 Dec 2024 11:31:30 GMT</pubDate>
            <description><![CDATA[<p>본 게시글은 <a href="https://dabletech.oopy.io/cd271ce2-3583-4fbc-ac48-f5f3a2514705">모나드와 함수형 아키텍처 - 김성철</a> 님의 게시글을 참고하여 정리하였습니다.</p>
<h3 id="타입과-함수">타입과 함수</h3>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/c7c05111-2cc9-45c8-951f-ec09c74eeebe/image.png" alt=""></p>
<h4 id="타입">타입</h4>
<p>타입은 <strong>집합</strong>이다. boolean, int 같은 원시타입(Primitive Type) 외에도 구조체, 클래스, Enum 등도 모두 타입에 해당한다.
ex)
<code>Boolean = {False, True}</code></p>
<p><code>Integer = {... -1, 0, 1, ...}</code></p>
<p><code>Double = {... 0.9, 0.99, 1.0 ...}</code></p>
<h4 id="함수">함수</h4>
<p><code>f : X → Y</code></p>
<p>함수는 <strong>두 집합을 연결하여 관계를 만들어주는 연산</strong>이다.</p>
<p>수학에서의 함수는 <strong>순수함수</strong>이다.</p>
<p>순수함수의 특징은 다음과 같다.</p>
<blockquote>
<ol>
<li>동일한 인자가 주어졌을 때, 항상 동일한 결과를 반환한다.</li>
<li>순수함수 <code>f : X → Y</code> 는 <strong>집합 X의 원소를 함수 f에 대입하면 집합 Y의 원소가 나오는 성질이 항상 유지된다.</strong></li>
</ol>
</blockquote>
<p><strong>객체지향 프로그래밍의 메서드는 일반적으로 순수함수일까?</strong> </p>
<p>객체지향 세계에서 각 객체들은 <strong>상태(state)</strong>와 <strong>행위(behavior)</strong>를 가진다.
그리고 <strong>객체의 행위는 상태에 영향을 받는다.</strong></p>
<p>아래 예제를 보자.</p>
<pre><code class="language-java">class MyClass {
    int factor = 1;

    public int calc(int val) {
        return val + this.factor;
    }
}</code></pre>
<p>calc() 메서드의 반환값은 <strong>외부요소인 factor에 의존적</strong>이다. 만약 factor의 값이 변하면, 동일한 입력에 대해 다른 출력값을 반환한다.
따라서 이는 순수함수가 아니다.</p>
<pre><code class="language-java">public int div(int a, int b) {
    return a / b;
}</code></pre>
<p>그렇다면 위와 같은 div() 메서드는 어떨까?
의존하는 외부요소가 없으므로 순수함수라 생각할 수 있다.</p>
<p>하지만 만약 b가 0이라면, 
<code>java.lang.ArithmeticException</code> 이 발생한다.</p>
<p><code>int 집합 → int 집합</code> 의 관계가 항상 보장되지 않으므로 순수함수가 아닌 것이다. </p>
<blockquote>
<p>함수 외부 요소에 의존하지 않더라도, <strong>결과 집합 이외의 집합값을 발생시키면 순수함수가 아니다.</strong></p>
</blockquote>
<hr>
<h3 id="함수의-합성과-사이드이펙트">함수의 합성과 사이드이펙트</h3>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/8a2c04ea-9244-43ed-9587-51adb791ca9d/image.png" alt=""></p>
<p>함수 
<code>f : X → Y</code>, <code>g : Y → Z</code> 일 때,</p>
<p>두 함수의 합성은
<code>g ∘ f : X → Z</code> 이다.</p>
<p>프로그래밍 세계에서도 합성이 존재한다.
예를 들어 인터넷에서 사이트를 옮겨 다니게 하는 <code>링크</code>는 함수이다.
링크를 계속 클릭하여 <code>웹</code>을 탐방하는 것은 함수의 합성으로 생각할 수 있다.</p>
<p><code>link : site → site</code>
<code>web : link ∘ link ∘ link ...∘ link</code> </p>
<p>함수의 합성 덕분에 우리는 <strong>커다란 문제를 작은 문제들로 쪼개어 풀 수 있다.</strong></p>
<p>하지만 그것이 항상 성공하지는 않는다. 왜냐하면, <strong>사이드이펙트(Side Effect)</strong> 때문이다.</p>
<p><strong>사이드이펙트</strong>는 어떤 함수가 존재할 때, 이 함수가 <strong>순수함수가 될 수 없게 만드는 모든 것</strong>을 의미한다.</p>
<p><strong>사이드이펙트가 존재하여 순수함수성이 깨지면, 함수의 합성은 더 이상 진행될 수 없다.</strong></p>
<blockquote>
<ul>
<li>우리가 작성한 프로그램은 커다란 문제를 작은 문제들로 쪼개어 해결하는 함수의 합성이다.</li>
</ul>
</blockquote>
<ul>
<li>사이드 이펙트가 존재하면 함수의 합성이 실패할 수 있다. </li>
</ul>
<hr>
<h3 id="monad">Monad</h3>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/5af4d4d4-5454-497c-9532-27b8e0083862/image.png" alt=""></p>
<p>Monad는 일종의 디자인 패턴으로써, <strong>결과값 집합과 오류값 집합을 하나의 집합으로 만든 것이다.</strong></p>
<p><strong>함수의 실행 결과를 모나드로 반환한다면, 순수함수의 성질을 잃지 않을 수 있다.</strong>
순수함수의 성질을 잃지 않기 때문에, 함수의 합성을 지속적으로 이어나갈 수 있다.</p>
<p>간단한 모나드의 예제를 보자.</p>
<pre><code class="language-java">public abstract class Result&lt;T&gt; {
    public static class Success&lt;T&gt; extends Result&lt;T&gt; {
        T value;

        public Success(T value) {
            this.value = value;
        }
    }
    public static class Fail&lt;T&gt; extends Result&lt;T&gt; {

    }
}</code></pre>
<p><code>Result&lt;T&gt;</code> 모나드를 도입하고 {Success, Fail} 집합을 포함한다. 
함수 내부 연산에서 사이드 이펙트가 발생하면, Fail을 반환한다.</p>
<pre><code class="language-java">public int div(int a, int b) {
    return a / b;
} // b가 0일 때 사이드 이펙트가 발생한다.</code></pre>
<pre><code class="language-java">public Result&lt;Integer&gt; div(int a, int b) {
    try {
        return new Result.Success&lt;&gt;(a / b);
    } catch (Throwable e) {
        return new Result.Fail&lt;&gt;();
    }
}</code></pre>
<p>div()의 반환값을 모나드로 변경한다. </p>
<pre><code class="language-java">var a = div(10, 0);

if(a instanceof Result.Success&lt;Integer&gt;) {
    //...
} else if(a instanceof Result.Fail&lt;Integer&gt;) {
    //...
}</code></pre>
<p>위와 같이 사용한다면, div의 두 번째 매개변수로 0이 입력되더라도, 에러가 발생하지 않는다.</p>
<hr>
<h4 id="map과-flatmap">Map과 FlatMap</h4>
<p>모나드에는 Map과 FlatMap 기능이 존재한다.</p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/b1ba6194-33f3-4d5a-a4ef-b2151ef8c26b/image.png" alt=""></p>
<p>Map은 <code>구체타입 -&gt; 구체타입</code>으로 변환하는 함수를 입력받고, 모나드를 반환한다.</p>
<p>FlatMap은 <code>구체타입 -&gt; 모나드</code>로 변환하는 함수를 입력받고, 모나드를 반환한다.</p>
<pre><code class="language-java">Optional&lt;Integer&gt; len1 = Optional.of(&quot;Hello&quot;)
                .map(s -&gt; s.length());


Optional&lt;Integer&gt; len2 = Optional.of(&quot;Hello&quot;)
                .flatMap(s -&gt; Optional.of(s.length()));</code></pre>
<p>위와 같이 map() 에는 구체타입을 반환하는 람다식이, 
flatMap()에는 Optional&lt;&gt;을 반환하는 람다식이 들어가는 것을 알 수 있다.</p>
<p>두 함수의 반환값은 모두 Optional&lt;&gt;로 모나드를 반환한다.</p>
<p>즉 map, flatMap을 사용해서 <strong>메서드 체이닝</strong>이 가능하며, 모나드 패턴을 적용하여 <strong>체이닝의 중간 단계에서 실패하지 않는다.</strong></p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/bbf3bcc7-9d30-4dca-bef6-bbf8d896a9d1/image.png" alt=""></p>
<p>Optional.map()의 구현 코드를 보면 값이 존재하지 않을 때, mapper 함수를 적용하지 않고, Optional.empty()를 반환하는 것을 볼 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Java - String.matches(REGEX)]]></title>
            <link>https://velog.io/@jun-ha/Java-String.matchesREGEX</link>
            <guid>https://velog.io/@jun-ha/Java-String.matchesREGEX</guid>
            <pubDate>Sat, 14 Dec 2024 16:57:57 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/jun-ha/post/5b2fa5cc-ccb3-4cc9-be98-246eae40bd79/image.png" alt=""></p>
<p><a href="https://school.programmers.co.kr/learn/courses/30/lessons/64064">2019 카카오 개발자 겨울 인턴십 - 불량 사용자</a></p>
<h4 id="내-정답-코드">내 정답 코드</h4>
<pre><code class="language-java">import java.util.*;

class Solution {
    boolean[] visited;
    List&lt;Integer&gt;[] cases;
    Set&lt;String&gt; answerSet = new HashSet&lt;&gt;();

    void dfs(int idx) {
        if(idx == cases.length) {
            StringBuilder sb = new StringBuilder();
            for(int i = 0; i &lt; visited.length; i++) {
                if(visited[i]) sb.append(i);
            }
            answerSet.add(sb.toString());
            return;
        }

        List&lt;Integer&gt; possibles = cases[idx];

        for(int p : possibles) {
            if(visited[p]) continue;

            visited[p] = true;
            dfs(idx + 1);
            visited[p] = false;
        }
    }

    boolean isMatched(String a, String b) {
        if(a.length() != b.length()) return false;
        for(int i = 0; i &lt; a.length(); i++) {
            if(a.charAt(i) == &#39;*&#39; || b.charAt(i) == &#39;*&#39;) continue;
            if(a.charAt(i) != b.charAt(i)) return false;
        }
        return true;
    }

    public int solution(String[] user_id, String[] banned_id) {
        visited = new boolean[user_id.length];
        cases = new List[banned_id.length];

        for(int i = 0; i &lt; banned_id.length; i++) {
            cases[i] = new ArrayList&lt;&gt;();
            for(int j = 0; j &lt; user_id.length; j++) {
                if(isMatched(banned_id[i], user_id[j])) {
                    cases[i].add(j);
                }
            }
        }

        dfs(0);

        return answerSet.size();
    }
}</code></pre>
<hr>
<h4 id="matchesstring-regex">matches(String regex)</h4>
<p>문제에서는 <strong>user_id</strong> 와 문자열의 일부가 &#39;*&#39;로 이루어진 <strong>banned_id</strong>가 매칭되는지를 파악해야한다.</p>
<pre><code class="language-java">boolean isMatched(String a, String b) {
        if(a.length() != b.length()) return false;
        for(int i = 0; i &lt; a.length(); i++) {
            if(a.charAt(i) == &#39;*&#39; || b.charAt(i) == &#39;*&#39;) continue;
            if(a.charAt(i) != b.charAt(i)) return false;
        }
        return true;
    }</code></pre>
<p>나는 위와 같이 일일이 파악하였으나(ㅎㅎ;)
풀고나서 다른 사람의 풀이를 보다가 String 클래스에서 제공하는 matches 메서드를 알게되었다. </p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/578be462-0362-43e6-bdec-4d8842ccd322/image.png" alt=""></p>
<p>matches()는 <strong>문자열을 정규표현식과 매칭되는지 파악</strong>해주는 유용한 함수이다</p>
<pre><code class="language-java">String reg = banned_id.replace(&quot;*&quot;, &quot;[\\w]&quot;) 
if(user_id.matches(reg)) {
    ///매칭되는경우
}</code></pre>
<p>banned_id에서 <code>*</code>를 전부 <code>[\\w]</code> 로 변경한 Regular Expression에 대해 matches() 메서드를 호출하면 쉽게 파악할 수 있다!</p>
<blockquote>
<p>\w: 알파벳(a-z, A-Z), 숫자(0-9), 밑줄(_)을 포함합니다.
\d: 숫자(0-9)를 포함합니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring에서 동시에 요청을 처리하는 방법과 Thread Pool]]></title>
            <link>https://velog.io/@jun-ha/Spring%EC%97%90%EC%84%9C-%EB%8F%99%EC%8B%9C%EC%97%90-%EC%9A%94%EC%B2%AD%EC%9D%84-%EC%B2%98%EB%A6%AC%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95%EA%B3%BC-Thread-Pool</link>
            <guid>https://velog.io/@jun-ha/Spring%EC%97%90%EC%84%9C-%EB%8F%99%EC%8B%9C%EC%97%90-%EC%9A%94%EC%B2%AD%EC%9D%84-%EC%B2%98%EB%A6%AC%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95%EA%B3%BC-Thread-Pool</guid>
            <pubDate>Thu, 12 Dec 2024 08:16:20 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/jun-ha/post/387bcf6f-89af-406e-a7ab-d37d184b9b84/image.png" alt=""></p>
<p>스레드는 <strong>Unit of Execution</strong>으로 불리며, CPU 코어의 실행단위이다.
즉, 하나의 프로세스에서 두 개 이상의 스레드를 사용함으로써 두 가지 이상의 작업을 동시에 실행할 수 있다. </p>
<p>하지만 단순히 Thread만 사용해서 동시에 여러 작업을 처리하는 프로그램을 만든다면 문제가 발생한다.</p>
<p>만약 <strong>작업 요청이 들어올때마다 스레드를 생성하여 처리</strong>한다면 어떤 문제가 발생할까?</p>
<h3 id="스레드-생성비용-문제">스레드 생성비용 문제</h3>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/d98e6f62-c8d3-4216-9645-3c0daf01751c/image.png" alt=""></p>
<p>Java의 경우 One-To-One Threading 모델로 스레드를 생성한다.</p>
<p>즉, User Thread 생성 시, OS Thread와 연결해야하며,
이는 새로운 스레드를 생성할 때마다 오버헤드가 크게 발생함을 의미한다. </p>
<p>작업 요청에 대해 매번 새롭게 스레드를 생성하여 처리한다면, 결과적으로 <strong>최종적인 요청 처리시간이 증가</strong>하는 문제가 발생한다.</p>
<h3 id="과도한-스레드-생성-문제">과도한 스레드 생성 문제</h3>
<p>만약 프로세스의 요청 처리 속도보다 더 빠른 속도로 요청이 들어온다면 어떻게 될까?</p>
<p>새로운 스레드가 무제한적으로 계속 생성되며, 스레드가 많아질 수록 메모리를 차지하고, Context-Switching이 더 자주 발생한다. </p>
<p>또한 CPU 자원을 경합하는 경우가 발생할 수 있으며,
이는 하나 이상의 스레드가 데이터를 기록하려고 할 때 다른 스레드가 해당 데이터를 읽으려고 하는 경우이다.</p>
<p>이에 따라 메모리 문제가 발생할 수 있고, CPU 오버헤드가 증가한다.</p>
<hr>
<h3 id="thread-pool">Thread Pool</h3>
<p>이러한 문제를 해결하기 위해 Thread Pool(스레드풀)을 사용한다.
스레드풀은 스레드를 허용된 수 만큼만 사용하도록 제한하는 시스템이다.
<img src="https://velog.velcdn.com/images/jun-ha/post/2275c951-0551-4675-a9fa-81f5f56c36c5/image.png" alt=""></p>
<p>스레드풀의 기본 플로우는 다음과 같다.</p>
<ol>
<li><p>처음에는 core size만큼의 스레드를 생성한다.</p>
</li>
<li><p>유저 요청(Connection)이 들어올때마다 작업 큐에 담는다.</p>
</li>
<li><p>유휴상태(idle)인 스레드가 있다면 작업 큐에서 작업을 꺼내 스레드에 작업을 할당하여 처리한다.</p>
</li>
<li><p>1 만약 유휴상태인 스레드가 없다면 작업은 작업 큐에서 대기한다.</p>
</li>
<li><p>2 작업 큐가 가득 차면 스레드를 생성한다.</p>
</li>
<li><p>3 max size 만큼의 스레드가 존재하고, 작업 큐도 가득차면 connection-refused 오류를 반환한다.</p>
</li>
<li><p>작업이 완료되면 스레드는 <strong>다시 유휴상태로 돌아간다.</strong></p>
</li>
</ol>
<p>위와 같은 방식으로 생성될 수 있는 스레드의 개수를 제한하고, 한 번 생성된 스레드를 없애지 않고 재사용함으로써, 스레드 생성에 따른 오버헤드를 없앨 수 있다.</p>
<p><strong>즉, 여러 개의 작업을 동시에 처리하면서도 안정적으로 처리하고 싶을 때 Thread Pool은 효과적이다.</strong></p>
<hr>
<h3 id="web-server">Web Server</h3>
<p>웹서버의 특성 상 동시에 여러 요청을 처리해야하며, 앞서 설명한 Thread Pool을 사용하기 매우 적합하다.</p>
<h4 id="tomcat">Tomcat</h4>
<p>Tomcat은 Spring Boot의 내장 Servlet Container 중 하나이며, Java 기반의 WAS이며,
Java의 Thread Pool과 매우 유사한 자체 스레드풀 구현체를 가지고 있다.</p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/40d7de33-be49-49b5-bf07-9c1b8dd7f6cb/image.png" alt=""></p>
<p>톰캣의 스레드풀에서는 두 가지 추가적인 요소가 존재한다.</p>
<ol>
<li><p><strong>Max-Connections</strong>
: 톰캣이 최대로 동시에 처리할 수 있는 Connection의 개수를 의미한다. Web 요청이 들어오면 톰캣의 Connector가 Connection을 생성하면서 요청된 작업을 ThreadPool의 Thread와 연결한다.</p>
</li>
<li><p><strong>Accept-Count</strong>
: Max-Connections 이상의 요청이 들어왔을때 사용하는 대기열 Queue 사이즈이다. 이 대기열이 꽉 찼을때 들어오는 요청은 거절될 수 있다.</p>
</li>
</ol>
<p>톰캣의 스레드풀 관련 설정은 <code>application.yml</code> 혹은 <code>application.properties</code> 같은 설정파일을 통해 가능하다.</p>
<pre><code>server:
  tomcat:
    threads:
      max: 200
      min-spare: 10
    max-connections: 8192
    accept-count: 100 # Task queue size
    connection-timeout: 20000</code></pre><p>위 설정을 하나하나 살펴보자면, </p>
<ul>
<li><p>max
: Thread Pool에 생성될 수 있는 스레드의 최대 개수를 의미한다. 기본값은 200이다.</p>
</li>
<li><p>min-spare
: 최소한으로 유지할 스레드의 수를 의미한다.</p>
</li>
<li><p>max-connections
: 한 번에 처리할 수 있는 최대 연결 수이며, 이는 Keep-Alive 상태도 포함한다. 기본값은 8192이다. <strong>사실 상 서버의 실질적인 동시요청 처리 개수라고도 생각할 수 있다.</strong></p>
</li>
<li><p>accept-count
: max-connections를 초과하는 요청이 들어올때 대기할 수 있는 최대 수. 기본 값은 100이다. 너무 작게 설정한다면, 요청이 몰렸을 때 들어오는 요청들을 모두 거절할 수도 있다.</p>
</li>
</ul>
<p>Tomcat 8 버전 이후부터는 <strong>Non-Blocking I/O</strong> 방식을 채택하여, 기존의 1 connection - 1 thread 방식에서 벗어나 <strong>N connection - 1 thread 방식으로 전환되었다.</strong> 
이를 통해 하나의 스레드가 여러 연결의 작업을 관리할 수 있어, 더 적은 스레드로도 높은 동시성을 처리할 수 있다. </p>
<p>따라서 Non-Blocking I/O 방식의 최신 버전 톰캣을 사용한다면, 최대 스레드의 개수보다 적거나 같은 수의 max-connections를 설정하는 것은 비효율적이라고 한다.</p>
<p>Thread Pool의 설정들은 TPS와 요청에 대한 응답시간을 결정하는 하나의 요소이며, 이러한 설정들이 적절하지 않다면, 병목현상 및 CPU 오버헤드와 메모리 문제를 유발할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프로세스, 스레드, 웹브라우저 아키텍처]]></title>
            <link>https://velog.io/@jun-ha/%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4-%EC%8A%A4%EB%A0%88%EB%93%9C-%EC%9B%B9%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98</link>
            <guid>https://velog.io/@jun-ha/%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4-%EC%8A%A4%EB%A0%88%EB%93%9C-%EC%9B%B9%EB%B8%8C%EB%9D%BC%EC%9A%B0%EC%A0%80-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98</guid>
            <pubDate>Tue, 10 Dec 2024 08:24:58 GMT</pubDate>
            <description><![CDATA[<h2 id="프로세스와-스레드">프로세스와 스레드</h2>
<h3 id="프로세스">프로세스</h3>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/7b4a2df6-b131-46ad-af97-05bc53aebc54/image.png" alt=""></p>
<p><strong>프로세스</strong>는 실행중인 프로그램이며, 운영체제로부터 독립된 메모리 공간을 할당받는다. 메모리 공간은 <strong>Code/Data/Heap/Stack</strong> 영역으로 이루어져있다.</p>
<ul>
<li><strong>Code</strong> 영역은 read-only로, 실행할 프로그램의 기계어 코드가 저장된다.</li>
<li><strong>Data</strong> 영역은 전역변수, 정적변수 등이 저장된다.</li>
<li><strong>Heap</strong> 영역은 동적으로 할당된 구역으로 객체, 배열 등이 저장된다. Java의 <code>new</code>, C의 <code>malloc</code> 키워드 등이 해당된다. </li>
<li><strong>Stack</strong> 영역은 각각의 함수 호출의 지역변수, 매개변수, 리턴주소 값 등이 저장된다. </li>
</ul>
<p>프로세스는 운영체제의 CPU 스케줄링의 대상이며, <strong>컨텍스트 스위칭(Context Switching)</strong>을 통해 여러 프로세스가 CPU를 번갈아 사용한다.</p>
<p>컨텍스트 스위칭은 CPU가 하나의 프로세스에서 다른 프로세스로 전환될 때, 이전 프로세스의 레지스터 값, 프로그램 카운터(PC), 스택 포인터 등의 상태를 저장하고, 새로운 프로세스의 상태를 복원하는 작업을 말한다. 컨텍스트 스위칭은 오버헤드가 발생하므로, 적절한 스케줄링 알고리즘이 필요하다.</p>
<p>또한 프로세스는 각각 독립된 메모리 공간을 가지기 때문에, 데이터를 주고 받기 위해 <strong>IPC(Inter-Process-Communication)</strong> 기법을 사용해야한다.</p>
<p>IPC에는 다음과 같은 방식들이 존재한다.</p>
<ul>
<li><p><strong>Message Queue</strong>
: 운영체제가 관리하는 큐를 통해 프로세스 간 데이터를 교환하는 방식.
프로세스는 큐에 메시지를 쓰거나 읽어서 데이터를 주고받는다.</p>
</li>
<li><p><strong>Shared Memory</strong>
: 두 프로세스가 동일한 메모리 영역을 공유하여 데이터를 주고받는 방식</p>
</li>
<li><p><strong>Socket</strong>
: 네트워크 통신에서 주로 사용되는 소켓은, 같은 호스트의 프로세스 간 통신에도 사용할 수 있다.</p>
</li>
</ul>
<br>

<hr>
<h3 id="스레드">스레드</h3>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/d77aedac-b72c-4a3a-9e61-fef7289d3a8c/image.png" alt=""></p>
<p><strong>스레드</strong>는 하나의 프로세스 내에서 동시에 진행되는 실행 흐름의 단위이다.
각각의 스레드는 독립된 Stack 영역과 레지스터 값을 보유하며, Code, Data, Heap 영역을 공유한다.</p>
<p>스레드간의 메모리를 공유하기 때문에, 프로세스간 통신(IPC)보다 오버헤드가 적다.</p>
<p>다만, <strong>공유 자원 문제</strong>로 인해 경쟁 상태, 데이터 불일치 등의 문제가 발생할 수 있다. </p>
<p>공유 자원 문제란, 여러 스레드가 동시에 하나의 자원에 접근하거나 수정할 때 발생하는 문제로, 예상치 못한 동작이나 데이터 손상을 초래할 수 있는 상황을 말한다.</p>
<p>이러한 문제를 방지하기 위해 적절한 동기화 메커니즘을 사용하는 것이 중요하다.</p>
<hr>
<h3 id="크롬-브라우저">크롬 브라우저</h3>
<p>크롬 브라우저에서는 각각의 페이지마다 탭이 존재하여 손쉽게 전환이 가능하다.
웹페이지 화면을 렌더링하는 각각의 탭을 직접 구현한다면, 프로세스와 스레드를 어떻게 활용해야할까?</p>
<p><strong>하나의 프로세스에서 여러 스레드를 생성하여 모든 탭을 관리하는 구조</strong>, 혹은 <strong>각 탭을 독립된 프로세스로 생성하는 구조</strong>로 설계할 수 있을 것이다.</p>
<p>각각의 장단점을 비교해보자.</p>
<h4 id="하나의-프로세스--여러-스레드탭-구조">하나의 프로세스 + 여러 스레드(탭) 구조</h4>
<p>장점</p>
<ul>
<li><p>메모리 효율성
: 모든 스레드가 Code, Data, Heap 영역을 공유하므로 각 탭마다 프로세스 전체 메모리 공간을 할당하는 방식보다 사용량이 적다.</p>
</li>
<li><p>빠른 통신
: 공유메모리를 통한 스레드 간의 통신은 IPC에 비해 훨씬 빠르다.</p>
</li>
</ul>
<p>단점</p>
<ul>
<li><p>안정성 저하
: <strong>만약 하나의 스레드에 문제가 발생하면, 다른 스레드도 제대로 동작하지 않을 수 있다.</strong></p>
</li>
<li><p>병목현상
: 공유 자원에 접근할 때 동기화 문제가 발생할 수 있으며, 이를 해결하기 위해 락을 많이 사용하면 성능이 저하될 수 있다.</p>
</li>
</ul>
<h4 id="각-탭별-독립된-프로세스-구조">각 탭별 독립된 프로세스 구조</h4>
<p>장점 </p>
<ul>
<li>안정성
: 각 탭이 독립된 프로세스에서 실행되므로, 한 탭에서 문제가 발생해도 다른 탭이나 브라우저 전체에 영향을 미치지 않습니다.</li>
</ul>
<p>단점</p>
<ul>
<li><p>높은 메모리 사용량
: 각 탭이 독립된 메모리 공간을 가지기 때문에 메모리 사용량이 증가합니다.</p>
</li>
<li><p>복잡한 통신
: 프로세스 간 통신은 IPC를 사용해야 하므로, 스레드 간 통신에 비해 구현이 복잡하고, 성능이 낮을 수 있다.</p>
<br>

</li>
</ul>
<p>하나의 스레드에 문제가 발생하면, 다른 스레드가 제대로 동작하지 않는 이유는 무엇일까? <strong>하나의 스레드가 다른 스레드에 어떤 문제들을 발생 시킬 수 있는 것인가?</strong></p>
<p>앞서 보앗듯, 스레드끼리는 Code, Data, Heap 영역을 공유하는데, Heap 영역은 프로세스 실행 도중 동적으로 크기가 변하는 영역이다. 만약 하나의 스레드가 이를 과도하게 할당하여 사용한다면? 
-&gt; 메모리 부족 에러가 발생하여 다른 스레드가 더 이상 메모리를 사용할 수 없게 될 것이다.</p>
<p>또 만약 하나의 스레드가 특정 공유자원에 대한 락을 점유한 상태로 무한루프에 빠지거나 과도한 연산을 요청하여 응답하지 않는다면? 
-&gt; 이를 획득해야하는 다른 스레드는 <strong>데드락</strong> 상태에 빠지고 무한 대기 상태에 빠질 수 있다.</p>
<p>이와 같은 이유로 하나의 프로세스 + 여러 스레드 구조에서는 하나의 스레드(탭)가 트롤짓을 한다면 전체 스레드(전체 탭)에 문제가 생길 가능성이 있다. </p>
<hr>
<p>그렇다면 하나의 탭이 문제를 일으킬 가능성을 따져봐야한다.</p>
<p>웹서버가 제공한 HTML, CSS, JavaScript 코드는 브라우저 내에서 실행되며, 이 코드는 웹 개발자가 작성한 로직에 따라 동작하므로 브라우저 입장에서는 실행 결과를 예측할 수 없다.</p>
<p>특히, JavaScript는 클라이언트 측에서 실행되며, 무한 루프, 과도한 메모리 사용, 대규모 이벤트 처리 등 의도치 않은 동작(트롤짓)을 유발할 가능성이 있다.</p>
<p><strong>특정 웹페이지의 동작 방식을 사전에 예측할 수 없기 때문에, 하나의 프로세스가 여러 탭을 관리하는 구조는 안정성 측면에서 매우 취약하다.</strong></p>
<p>따라서 각각의 탭을 렌더링하는 기능은 다중 프로세서로 설계하는 것이 적합하다고 생각한다.</p>
<hr>
<h3 id="크롬-브라우저-아키텍처">크롬 브라우저 아키텍처</h3>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/709fb675-72bc-450c-8f19-3dcc3c689965/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/5dd5581f-e9e6-4477-98ce-510f99053ea2/image.png" alt=""></p>
<p>실제로 크롬 브라우저 아키텍처는 위와 같은 구조로 되어있다고 한다.</p>
<p>각각의 탭을 렌더링하는 &#39;렌더러 프로세스&#39; 외에도 &#39;브라우저 프로세스&#39;, &#39;플러그인 프로세스&#39;, &#39;GPU 프로세서&#39; 등이 함께 동작한다.</p>
<p>다만 사진에서 볼 수 있듯, 렌더러 프로세스는 탭별로 프로세스를 할당하는 구조로 되어있다.</p>
<blockquote>
<p>한 탭이 응답하지 않더라도 다른 탭은 사용 가능하다는 점은 각 탭마다 독립적인 렌더러 프로세스를 유지했을 때의 이점이다. 
웹 페이지에서 처리할 작업이 많아 응답하지 못하는 경우나 웹 페이지를 담당하던 렌더러 프로세스의 실행이 중단된 경우 등에 이런 이점을 확인할 수 있다.</p>
</blockquote>
<p>안정성 확보 측면 외에도 <strong>보안과 격리(sandbox)</strong>의 이점도 있다고한다.
운영체제를 통해 각각의 프로세스의 권한을 제한한다면, 특정 프로세스가 특정 기능을 사용할 수 없게 제한할 수 있다.</p>
<p>예를 들어, 웹서버의 코드가 실행되는 <strong>렌더러 프로세스</strong>와, 쿠키를 저장하고 관리하는 <strong>스토리지 프로세스(Storage Process)</strong>가 분리되어 있음으로써 보안과 안정성을 강화할 수 있다.</p>
<p>이러한 구조에서는 렌더러 프로세스에서 실행되는 예측 불가능한 코드가 쿠키와 같은 민감한 데이터에 직접 접근하지 못하게 된다.</p>
<p>또한, 스토리지 프로세스는 렌더러 프로세스의 충돌이나 비정상적인 동작으로부터 격리되어, 데이터를 안전하게 보호하고 브라우저의 안정성을 유지할 수 있다.</p>
<p><a href="https://developer.chrome.com/blog/inside-browser-part1">사진출처 : Chrome for Developers - Inside look at modern web browser (part 1) </a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Git 지속적인 remote: Invalid username or password 에러 해결]]></title>
            <link>https://velog.io/@jun-ha/Git-%EC%A7%80%EC%86%8D%EC%A0%81%EC%9D%B8-remote-Invalid-username-or-password-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@jun-ha/Git-%EC%A7%80%EC%86%8D%EC%A0%81%EC%9D%B8-remote-Invalid-username-or-password-%EC%97%90%EB%9F%AC-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Fri, 15 Nov 2024 19:44:56 GMT</pubDate>
            <description><![CDATA[<p>내가 속한 Organization의 레포에 오랜만에 push를 했는데,
Personal Access Token이 만료되어서 재로그인을 해야했다.</p>
<p>그런데 깃허브 username과 새로 발급한 토큰을 정확하게 입력해도 지속적으로 
<code>remote: Invalid username or password.</code>
에러가 발생하였다.</p>
<p>토큰을 재발급해보고, Scope도 전부 선택해보고, 
origin을 <code>https://github.com/[path]</code> 에서
<code>https://[username]:[token]@github.com/[path]</code> 형태로 바꾸기도 해봤지만 해결이 되지 않았다.</p>
<hr>
<p>문제는 ~/.gitconfig 파일의 기본 설정이었다.</p>
<pre><code>[http &quot;https://github.com/&quot;] 
    extraheader = AUTHORIZATION: basic &lt;Base64 인코딩된 인증 정보&gt;</code></pre><p>위 설정을 통해 Git이 Github와 통신할 때, HTTP 요청에 Authorization 헤더가 자동으로 추가된다.
위 코드에 만료된 토큰이 들어가 있어서 지속적으로 문제가 발생,
삭제하고 다시 시도하니 해결되었다.</p>
<blockquote>
<p>GitHub는 기본 인증 방식(Basic Authentication)을 더 이상 권장하지 않으며, HTTPS를 사용할 경우 Personal Access Token을 별도로 입력하는 방식을 더 추천한다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[BOJ 17472 - 다리 만들기 2] - 최소스패닝트리, 프림알고리즘]]></title>
            <link>https://velog.io/@jun-ha/BOJ-17472-%EB%8B%A4%EB%A6%AC-%EB%A7%8C%EB%93%A4%EA%B8%B0-2-%EC%B5%9C%EC%86%8C%EC%8A%A4%ED%8C%A8%EB%8B%9D%ED%8A%B8%EB%A6%AC-%ED%94%84%EB%A6%BC%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</link>
            <guid>https://velog.io/@jun-ha/BOJ-17472-%EB%8B%A4%EB%A6%AC-%EB%A7%8C%EB%93%A4%EA%B8%B0-2-%EC%B5%9C%EC%86%8C%EC%8A%A4%ED%8C%A8%EB%8B%9D%ED%8A%B8%EB%A6%AC-%ED%94%84%EB%A6%BC%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98</guid>
            <pubDate>Fri, 01 Nov 2024 13:02:49 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.acmicpc.net/problem/17472">BOJ 17472 - 다리만들기 2</a></p>
<p><img src="https://velog.velcdn.com/images/jun-ha/post/451d741b-ae84-4593-ac82-6b0bed1b676f/image.png" alt=""></p>
<p>내일 현대오토에버 코테를 앞두고 빡센 구현 문제 하나 풀어보았다. </p>
<p>주어진 이차원 평면에서 조건에 맞게 다리를 건설하고, 모든 경로를 잇는 다리 길이의 총합의 최소를 구하는 문제이다.</p>
<h3 id="접근법">접근법</h3>
<p>각각의 섬을 그래프의 노드로, 다리를 간선으로 생각할 수 있다.</p>
<ol>
<li>먼저 각각의 섬을 구분짓는다.</li>
<li>조건에 맞는 가능한 모든 다리를 건설한다. 동일한 섬을 연결한다면 최소 길이의 다리만 남긴다.</li>
<li>주어진 정보로 그래프를 구성한다.</li>
<li>최소스패닝트리(MST)를 구한다.</li>
</ol>
<hr>
<h3 id="최소-스패닝-트리mst">최소 스패닝 트리(MST)</h3>
<blockquote>
<p>최소 스패닝 트리(Minimum Spanning Tree, MST)는 연결된 무향 그래프에서 <strong>모든 정점을 포함하는 부분 그래프 중 간선 가중치의 합이 최소인 트리</strong>를 말합니다. 여기서 &quot;스패닝 트리&quot;란, 그래프의 모든 정점을 포함하되 <strong>사이클이 없는</strong> 연결 그래프입니다. 즉, MST는 그래프 내의 모든 정점을 연결하면서 간선 비용을 최소화하는 구조입니다.</p>
</blockquote>
<p>최소 스패닝 트리의 특징은</p>
<ul>
<li>트리 구조이며 따라서 사이클이 없다. 모든 정점이 연결되어있다.</li>
<li>n개의 노드에 대해 간선의 개수는 n-1이다. </li>
<li>간선 가중치의 합이 최소이다. </li>
</ul>
<hr>
<h3 id="프림-알고리즘-prims-algorithm">프림 알고리즘 (Prim’s Algorithm)</h3>
<p>프림 알고리즘은 최소신장트리를 구하는 알고리즘이며 직관적이어서 이해가 쉽다.
<strong>현재까지 구성한 MST에 인접한 가장 짧은 간선을 지속적으로 선택해 나아가면 된다.</strong></p>
<p>우선순위큐를 통해 쉽게 구현이 가능하며,
시간복잡도는 모든 간선만큼 반복하며 우선순위큐에 삽입, 삭제를 하므로  <strong>O(E logV)</strong>이다. </p>
<blockquote>
<p>프림 알고리즘은 하나의 정점에서 시작하여 인접한 간선 중 가장 낮은 가중치를 가진 간선을 선택해 MST에 추가한다. 이후 추가된 정점의 인접 간선 중 최소 가중치 간선을 선택하는 과정을 반복하여 모든 정점이 포함될 때까지 진행한다.</p>
</blockquote>
<hr>
<h3 id="전체-코드">전체 코드</h3>
<p>구현문제를 풀때는 최대한 간단하게 작은 단위의 함수들로 쪼개어 푸는 편이다.</p>
<pre><code class="language-java">import java.util.*;
import java.io.*;

public class Main {
    static int[][] map;
    static int row;
    static int col;

    static int[] dRow = {-1, 1, 0, 0};
    static int[] dCol = {0, 0, -1, 1};

    static int[][] adjs;

    static class Bridge {
        int to;
        int length = -1;
    }

    static boolean isValidRange(int r, int c) {
        return 0 &lt;= r &amp;&amp; r &lt; row &amp;&amp; 0 &lt;= c &amp;&amp; c &lt; col;
    }

    static void bfsToNumbering(int r, int c, int islandNum, boolean[][] visited) {
        Queue&lt;int[]&gt; q = new ArrayDeque&lt;&gt;();
        q.add(new int[]{r, c});

        while(!q.isEmpty()) {
            int[] tmp = q.poll();
            int tmpR = tmp[0];
            int tmpC = tmp[1];

            if(visited[tmpR][tmpC]) continue;
            visited[tmpR][tmpC] = true;
            map[tmpR][tmpC] = islandNum;

            for(int i = 0; i &lt; 4; i++) {
                int nextR = tmpR + dRow[i];
                int nextC = tmpC + dCol[i];
                if(islandNum == 2 &amp;&amp; i == 1) {
                    int here = 1;
                }
                if(isValidRange(nextR, nextC) &amp;&amp; map[nextR][nextC] != 0) {
                    q.add(new int[]{nextR, nextC});
                }
            }
        }
    }

    static void numberingIsland() {
        boolean[][] visited = new boolean[row][col];

        int islandNum = 0;
        for(int i = 0; i &lt; row; i++) {
            for(int j = 0; j &lt; col; j++) {
                if(map[i][j] == 1 &amp;&amp; !visited[i][j]) {
                    bfsToNumbering(i, j, ++islandNum, visited);
                }
            }
        }

        adjs = new int[islandNum + 1][islandNum + 1];
        for(int i = 0; i &lt; islandNum + 1; i++) {
            Arrays.fill(adjs[i], Integer.MAX_VALUE);
            adjs[i][i] = 0;
        }
    }

    static boolean isNearWater(int r, int c) {
        for(int i = 0; i &lt; 4; i++) {
            int nextR = r + dRow[i];
            int nextC = c + dCol[i];
            if(isValidRange(nextR, nextC) &amp;&amp; map[nextR][nextC] == 0) return true;
        }
        return false;
    }

    static Bridge makeBridge(int r, int c, int dRow, int dCol, int startIslandNum) {
        Bridge b = new Bridge();
        int len = 0;
        while(isValidRange(r, c) &amp;&amp; map[r][c] == 0) {
            r += dRow;
            c += dCol;
            len++;
        }

        if(isValidRange(r, c) &amp;&amp; map[r][c] != startIslandNum) {
            b.length = len;
            b.to = map[r][c];
        }

        return b;
    }

    static void setAdj(int r, int c) {
        int tmpIslandNum = map[r][c];

        for(int i = 0; i &lt; 4; i++) {
            int nextR = r + dRow[i];
            int nextC = c + dCol[i];
            if(isValidRange(nextR, nextC) &amp;&amp; map[nextR][nextC] == 0) {
                Bridge b = makeBridge(nextR, nextC, dRow[i], dCol[i], tmpIslandNum);
                if(b.length &gt;= 2) {
                    adjs[tmpIslandNum][b.to] = Math.min(adjs[tmpIslandNum][b.to], b.length);
                    adjs[b.to][tmpIslandNum] = Math.min(adjs[b.to][tmpIslandNum], b.length);
                }
            }
        }
    }

    static void setAdjs() {
        for(int i = 0; i &lt; row; i++) {
            for(int j = 0; j &lt; col; j++) {
                if(map[i][j] != 0 &amp;&amp; isNearWater(i, j)) {
                    setAdj(i, j);
                }
            }
        }
    }

    public static int getMinBridgeSum() {
        boolean[] visited = new boolean[adjs.length];

        int count = 0;
        int sumOfChosenBridge = 0;

        //len, isLandNum
        PriorityQueue&lt;int[]&gt; pq = new PriorityQueue&lt;&gt;(Comparator.comparingInt(a -&gt; a[0]));
        pq.add(new int[]{0, 1});

        while (!pq.isEmpty()) {
            int[] tmp = pq.poll();
            int len = tmp[0];
            int tmpNode = tmp[1];

            if(visited[tmpNode]) continue;
            visited[tmpNode] = true;
            count++;
            sumOfChosenBridge += len;

            if(count == adjs.length - 1) break;

            for(int i = 1; i &lt; adjs.length; i++) {
                if(adjs[tmpNode][i] != Integer.MAX_VALUE &amp;&amp; !visited[i]) {
                    pq.add(new int[]{adjs[tmpNode][i], i});
                }
            }
        }

        if(count != adjs.length - 1) return -1;

        return sumOfChosenBridge;
    }

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

        row = Integer.parseInt(st.nextToken());
        col = Integer.parseInt(st.nextToken());

        map = new int[row][col];

        for(int i = 0; i &lt; row; i++) {
            st = new StringTokenizer(br.readLine());
            for(int j = 0; j &lt; col; j++) {
                map[i][j] = Integer.parseInt(st.nextToken());
            }
        }

        numberingIsland(); //각각의 섬을 구분짓는다.
        setAdjs(); //그래프를 구성한다

        System.out.println(getMinBridgeSum()); //최소신장트리의 간선의 합을 출력한다.
    }
}</code></pre>
]]></description>
        </item>
    </channel>
</rss>