<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>아침에 깨달으면 저녁에 죽어도 좋다</title>
        <link>https://velog.io/</link>
        <description>알고리즘, 자료구조 블로그: https://gyun97.github.io/</description>
        <lastBuildDate>Wed, 12 Nov 2025 13:37:57 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. 아침에 깨달으면 저녁에 죽어도 좋다. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/kim_dg" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Redis와 캐시(Cache)]]></title>
            <link>https://velog.io/@kim_dg/Redis%EC%99%80-%EC%BA%90%EC%8B%9CCache</link>
            <guid>https://velog.io/@kim_dg/Redis%EC%99%80-%EC%BA%90%EC%8B%9CCache</guid>
            <pubDate>Wed, 12 Nov 2025 13:37:57 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kim_dg/post/31230fda-1e06-4b67-9717-3b5bef11137a/image.png" alt=""></p>
<br>

<h1 id="redis와-캐시cache란-무엇인가">Redis와 캐시(Cache)란 무엇인가</h1>
<blockquote>
<p><strong><span style = "color:red"><code>Redis</code></span>: Redis(Remote Dictionary Server)란 인메모리 데이터 구조의 저장소이다. 쉽게 말해서 데이터를 메모리(RAM)에 저장하고 관리하여 읽고 쓰는 속도가 매우 빠른 저장소이다.</strong></p>
</blockquote>
<p>보통 MySQL, Oracle 같은 RDBMS는 데이터를 디스크(하드)에 저장하기 때문에 읽고 쓸 때마다 파일을 매번 뒤져야 하니까 속도가 좀 느리다. 하지만 Redis는 모든 데이터를 메모리(RAM) 에 넣고 다루기 때문에 응답 일반적인 RDB에 비해 속도가 수십 배에서 수천 배 빠르고 이러한 이유 때문에 Redis가 자주 쓰인다. Redis는 여러 기능을 제공하지만 특히 캐싱(Caching) 기능을 구현하는 데에 자주 사용된다.</p>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/c65ae31a-d0f0-4852-b723-f6eed6f7d900/image.png" alt=""></p>
<blockquote>
<p><strong><span style = "color:red"><code>캐싱(Caching)</code></span>:  자주 사용되는 데이터를 임시 저장소(캐시)에 복사 또는 저장해두고, 원본 저장소보다 훨씬 빠르게 데이터에 접근하는 캐시 방식을 이용한 성능을 향상시키는 기술이다.</strong></p>
</blockquote>
<br>

<p><img src="https://velog.velcdn.com/images/kim_dg/post/838e99fe-92a2-4c8b-96c5-280bf7fc4866/image.png" alt=""></p>
<p>일반적으로 서비스는 클라이언트가 요청을 서버에 보내면 서버에서 DB의 데이터를 사용하는 <code>3 layer</code> 구조를 갖는다. </p>
<br>

<p><img src="https://velog.velcdn.com/images/kim_dg/post/299a3fc1-b883-47e6-8910-1b45a9b16344/image.png" alt=""></p>
<p>이용자의 수가 적은 소규모 서비스라면 크게 문제없지만 클라이언트의 수가 늘어나고 서버에 보내는 요청의 수가 늘어나면 자연스럽게 DB에 대한 부하도 크게 늘어나 서비스의 성능에 문제가 생긴다. </p>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/64f55f5b-3764-4c99-b6d3-892864341614/image.png" alt=""></p>
<p>DB에 대한 부하를 줄이기 위해 캐시를 도입한다면 DB에 대한 부하도 줄이고 데이터를 이용하는 속도도 크게 높일 수 있어 서비스의 성능을 개선할 수 있다.</p>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/6c2a1d0f-4046-42c3-8f4a-1c95959ffbb0/image.png" alt=""></p>
<p>Redis는 이러한 캐시를 사용하는 데에 매우 적합한 솔루션이다. </p>
<p>우선 데이터를 단순한 key-value 형태로 저장하고 다양한 형식과 자료구조로 데이터를 저장할 수 있어 어떠한 데이터라도 쉽게 저장할 수 있다. 또한 모든 데이터를 RAM에 저장하는 In-memory 데이터 저장소이기 때문에 평균 작업 속도가 1ms 이하라 초당 수백만 건의 작업이 가능하다. 이렇게 빠른 처리 속도를 가지고 있기 때문에 Redis와 같은 캐시를 적절하게 사용하면 지연 시간(latency)도 감소하고 처리량도 획기적으로 늘릴 수 있어 서비스의 성능을 극대화시킬 수 있다. </p>
<br>
<br>

<h1 id="캐싱-전략caching-stratgies">캐싱 전략(Caching Stratgies)</h1>
<br>

<p>캐시를 사용하면 일반적으로 <strong><code>Cache Hit</code></strong>과 <strong><code>Cache Miss</code></strong>라는 두 가지 상황 중 하나의 상황이 발생한다.</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/8fa9c57b-de9f-4002-a583-8b0ccab640ea/image.png" alt=""></p>
<p>애플리케이션은 보통 데이터를 찾을 때 캐시 저장소를 먼저 확인한다. 만약 캐시에 찾으려고 하는 데이터가 존재한다면 캐시에서 데이터를 가져오는 것을 반복한다. </p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/b5991a5c-c800-4710-a26d-7dc6ea9131d9/image.png" alt=""></p>
<p>이렇게 <strong>캐시에 찾으려고 하는 데이터가 존재하는 상황을 <code>Cache Hit</code></strong>이라고 한다. </p>
<br>

<p><img src="https://velog.velcdn.com/images/kim_dg/post/300d1711-87f7-41ff-b775-d56b22a88424/image.png" alt=""></p>
<p>하지만 애플리케이션이 캐시를 조회했는데 찾고자 하는 데이터가 해당 캐시에 존재하지 않아 DB를 직접 조회해야 하는 상황도 존재할 것이다. </p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/5a6d6411-2fe3-48be-ae05-98a07eff634b/image.png" alt=""></p>
<p>이렇게 <strong>캐시에 찾고자 하는 데이터가 없다면 DB를 확인하여 직접 데이터를 갖고 온 후 캐시에 데이터를 저장해야 하는데 이렇게 캐시에 찾고자 하는 데이터가 존재하지 않는 상황을 <code>Cache Miss</code></strong>라고 한다.</p>
<br>
<br>

<p>Redis를 이용하여 캐싱을 구현할 때에 어떠한 전략을 사용하는지가 서비스의 성능에 큰 영향을 끼치기도 한다. 이때 데이터의 유형과 해당 데이터에 접근하는 엑세스 패턴을 잘 고려하여 선택해야 한다.</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/ee3acb4c-fdbb-4115-8e1a-fdedf48b4300/image.png" alt=""></p>
<p>캐싱 전략에는 여러 전략이 존재하는데 크게 &#39;읽기 전략&#39;과 &#39;쓰기 전략&#39;으로 나뉜다.</p>
<br>

<h2 id="읽기-전략">읽기 전략</h2>
<p>읽기 전략은 애플리케이션에서 데이터를 읽는 상황이 많을 때 사용하는 전략이다.</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/ad736b72-8c7b-429a-b05b-d597d3c797ea/image.png" alt=""></p>
<p>읽기 전략에는 <strong><code>Look Aside(Lazy Loading)</code></strong> 전략과 <strong><code>Read Through</code></strong> 전략이 존재한다.</p>
<br>

<h3 id="읽기-전략1--look-asidelazy-loading">읽기 전략1 : Look-Aside(Lazy Loading)</h3>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/0c0484d6-173b-4212-96bc-5cab17ff967d/image.png" alt=""></p>
<p>이 전략은 Redis를 캐시로 쓸 때 가장 일반적으로 많이 사용하는 전략이다. </p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/2310f832-dd31-4aa1-8b60-ed772e5cc289/image.png" alt=""></p>
<p>클라이언트가 애플리케이션에 요청을 보내면 애플리케이션이 캐시 스토어를 조회해서 Cache Hit이면 해당 데이터를 바로 가져온다.</p>
<br>

<p><img src="https://velog.velcdn.com/images/kim_dg/post/a151a24e-7bb1-40ea-8ad4-d4e1caff0bd2/image.png" alt=""></p>
<p>하지만 만약 캐시에 데이터가 없어 Cache Miss가 발생했다면 DB(Aside:옆)에서 데이터를 찾고 해당 데이터를 Cache에 저장하고 애플리케이션에 반환한다. </p>
<p>이렇게 캐시에 찾는 데이터가 없을 때에만 입력되기 때문에 Look-Aside 전략을 다른 말로 <code>Lazy Loading</code>이라고도 부른다.</p>
<br>


<p>이 구조는 Redis와 같은 캐시가 다운되더라도 바로 시스템의 장애로 이뤄지지 않고 DB에서 데이터를 갖고 올 수 있다는 장점이 존재한다. 하지만 캐시에 붙어있던 커넥션이 많다면 캐시에 붙어있던 커넥션들이 전부 DB에 붙기 때문에 DB에 갑작스러운 과부하가 발생할 수 있다는 단점도 존재한다.</p>
<p>또한 캐시를 새로 투입하거나 DB에만 새로운 데이터를 저장했다면 초기에 수많은 <code>Cache Miss</code>가 발생해서 서비스의 성능에 저하가 발생할 수 있다.</p>
<br>

<p><img src="https://velog.velcdn.com/images/kim_dg/post/2b48080b-4bcf-4656-8392-1bbf8abcd8c4/image.png" alt=""></p>
<p>그래서 이럴 때를 대비해서 미리 DB에서 캐시로 데이터를 밀어넣어 주는 작업을 할 수 있는데 이러한 작업을 <code>Cache Warming</code>이라고 한다.</p>
<br>

<p>이외에도 Look Aside 전략은 캐시와 DB간의 별다른 커넥션이 존재하지 않아 동기화가 제대로 이루어지지 않아 DB와 캐시간의 데이터가 서로 일치하지 않는 <strong>데이터 정합성 문제</strong>가 발생하기 쉽다.</p>
<br>
<br>


<h3 id="읽기-전략2--read-through">읽기 전략2 : Read Through</h3>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/715e57c3-e2c4-4b4e-944d-d926e06676d8/image.png" alt=""></p>
<p>Read Through 전략은 항상 캐시를 통해서만 데이터를 읽어오는 전략이다. </p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/49c6f271-39e1-416e-b7eb-505afb89051d/image.png" alt=""></p>
<p>Cache Hit이라면 그대로 캐시에서 데이터를 가져온다.</p>
<br>    

<p><img src="https://velog.velcdn.com/images/kim_dg/post/b4911dd1-9519-4501-839d-f43ace3bcaac/image.png" alt=""></p>
<p>하지만 Cache Miss라면 애플리케이션이 아니라 캐시 스토어가 DB에서 직접 데이터를 가져와 애플리케이션에 반환하는 구조이다. </p>
<br>    


<p><img src="https://velog.velcdn.com/images/kim_dg/post/733601e4-e03a-4ef2-ad40-bbe15a96396b/image.png" alt=""></p>
<p>이 전략의 장점은 캐시와 DB간에 커넥션이 존재하기 때문에 동기화가 이루어져 데이터의 정합성이 보장된다는 것이다. 하지만 단점으로는 만약 캐시가 다운된다면 이는 서비스 전체의 장애로 이루어질 수 있다.</p>
<br>
<br>

<h2 id="쓰기-전략">쓰기 전략</h2>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/2e8dd0ad-836d-481e-be1f-6b75cca195f6/image.png" alt=""></p>
<p>쓰기 전략에는 <code>Write Around</code>, <code>Write Back</code>, <code>Write Throug</code>라는 3가지 전략이 존재한다.</p>
<br>
<br>


<h3 id="쓰기-전략1--wrtie-around">쓰기 전략1 : Wrtie Around</h3>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/4a56e072-8aba-484b-99d7-cdfae758b5c5/image.png" alt=""></p>
<p>Write Around 전략은 캐시를 우회해서 직접 쓰는 전략이다. </p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/62968568-ac4b-459d-867f-545a2799ddfc/image.png" alt=""></p>
<p>이 전략은 일반적인 애플리케이션 구조와 같이 Write할 때에는 데이터를 DB에 직접 주입한다. </p>
<br>

<p><img src="https://velog.velcdn.com/images/kim_dg/post/e035e56c-e224-469a-a805-093da6db2693/image.png" alt=""></p>
<p>하지만 데이터를 읽어올 때 캐시를 조회하고 만약 Cache Miss가 발생한다면 DB에서 데이터를 조회한 후 해당 데이터를 반환함과 동시에 캐시에 해당 데이터를 저장한다.</p>
<br>

<p>Write Around 전략은 DB에 직접 쓰기 때문에 성능이 좋고 불필요한 데이터는 캐시에 저장하지 않기 때문에 리소스를 절약할 수 있다는 장점이 존재한다. </p>
<p>하지만 캐시 스토어와 DB에 별다른 커넥션이 존재하지 않기 때문에 양측의 데이터가 서로 다를 수 있어 데이터 정합성 유지가 까다롭다는 단점도 존재한다.</p>
<br>
<br>
<br>

<h3 id="쓰기-전략2--write-back">쓰기 전략2 : Write Back</h3>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/d01b2038-4b4a-48e2-9ec3-470d5c1d04da/image.png" alt=""></p>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/f7cb29ad-dab1-4954-a765-ee33ead204a0/image.png" alt=""></p>
<p>이 전략은 DB가 아니라 캐시 스토어에 데이터들을 먼저 저장(write)한 후 <code>scheduing</code>을 통해 주기적으로, 그리고 일괄적으로 캐시에 쌓아둔 새로운 데이터들을 DB에 저장하는 방식이다.</p>
<br>

<p>이 전략을 통해 한꺼번에 많은 양의 데이터들을 한 번의 쓰기 요청으로 DB에 저장할 수 있다. 데이터가 저장될 때마다 일일이 insert문을 날려야 하는 기존의 방식과는 한 번의 insert문으로 많은 양의 데이터를 저장할 수 있어 쓰기 비용 횟수를 획기적으로 줄일 수 있다는 성능상의 장점이 존재한다. </p>
<p>하지만 DB로 데이터를 옮기기 전에 캐시 스토어나 캐시 스토어에 저장된 데이터에 문제가 생기거나 하면 데이터가 유실될 수 있다는 단점도 존재한다. </p>
<br>
<br>
<br>


<h3 id="쓰기-전략3--write-through">쓰기 전략3 : Write Through</h3>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/d93704e3-12b6-422e-a132-d6e66bcce778/image.png" alt=""></p>
<p>해당 전략은 항상 캐시를 통해서만 쓰기를 진행하는 방식이다.</p>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/72a1bbf8-328c-4115-8646-ad5801ee34f3/image.png" alt=""></p>
<p>이 전략은 캐시에 데이터를 저장한 후 캐시에서 DB에 데이터를 바로 저장한다. </p>
<br>


<p>해당 방식은 항상 캐시를 거친 후에 DB에 도착하기 때문에 데이터의 정합성이 보장된다는 장점이 존재한다. </p>
<p>하지만 매번 데이터를 write할 때마다 무조건 두 번의 쓰기가 발생하기 때문에 성능적으로 떨어진다는 단점이 존재한다. 또한 재사용되지 않는 데이터들도 전부 캐시에 저장하기 때문에 불필요하게 낭비되는 리소스가 발생하기 쉽다.</p>
<br>
<br>
<br>


<h1 id="redis의-데이터-타입-종류">Redis의 데이터 타입 종류</h1>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/16983bed-8c67-4617-9ba3-d9c17fdabf54/image.png" alt=""></p>
<p>Redis는 <code>key-value</code> 형태로 데이터들을 저장하는데 실질적인 데이터가 저장되는 <code>value</code>에 다양한 데이터 타입을 제공하여 다양한 데이터 타입으로 데이터를 저장할 수 있다. </p>
<br>



<p><img src="https://velog.velcdn.com/images/kim_dg/post/6e4b0711-5aa0-41be-a2fd-7d7f8a89f3a0/image.png" alt=""></p>
<p>우선 <code>String</code>은 Redis에서 가장 기본적인 데이터 저장 타입이다. <code>set</code> 커맨드를 통해 저장되는 데이터들은 모두 String 형태로 저장된다.</p>
<br>

<p><img src="https://velog.velcdn.com/images/kim_dg/post/2102292a-7b09-4ed4-abcc-d8065ad38a63/image.png" alt=""></p>
<p><code>Bitmap</code>은 String의 변형 타입이고 비트 단위의 연산이 가능하다.</p>
<br>

<p><img src="https://velog.velcdn.com/images/kim_dg/post/d15fa2d7-ff15-4970-82fb-393599ad16c3/image.png" alt=""></p>
<p>데이터를 순서대로 저장하는 <code>List</code>는 <code>Queue</code>로 사용하는 데에 적합한 데이터 타입이다.</p>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/6b564a2d-1ecf-4491-9dce-faf7847dd3f9/image.png" alt=""></p>
<p><code>Hash</code>는 하나의 key 안에 또 여러 개의 key-value가 존재하는 형태이다. </p>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/31616ff0-8815-456e-8ffa-d0c98e783c4a/image.png" alt=""></p>
<p><code>Set</code>은 중복되지 않는 문자열들의 집합이다. </p>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/01bf50ac-67af-41af-b3e2-6ce1d1b49807/image.png" alt=""></p>
<p><code>Sorted Set</code>은 Set처럼 중복되지 않는 값을 저장하지만 모든 값을 <code>score</code>라는 특정 필드를 지정해 해당 값의 순서대로 정렬(기본은 오름차순, score 동일하면 사전순)하는 방식이다. 랭킹 같은 서비스를 구현하는 데에 자주 사용된다.</p>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/950574ba-dc49-4a9f-90b7-cf0e8fc49c64/image.png" alt=""></p>
<p><code>HyperLogLog</code>는 굉장히 많은 데이터를 다룰 때 사용되면 중복되지 않는 값의 개수를 count할 때 자주 사용된다. </p>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/8b68c82c-a7ba-49b2-aac7-67a2132441c7/image.png" alt=""></p>
<p><code>Stream</code>은 로그를 저장하기 가장 좋은 자료구조이다. </p>
<br>
<br>


<p>그렇다면 해당 데이터 타입들을 어떠한 방식으로 사용할 수 있을까??</p>
<p>몇 가지 사용 예시를 살펴보자.</p>
<br>
<br>
<br>


<h1 id="redis의-데이터-타입-사용-예시">Redis의 데이터 타입 사용 예시</h1>
<h2 id="1-counting-예시">1. Counting 예시</h2>
<p>우선 데이터 <code>Counting</code>이 필요한 상황을 생각해보자.</p>
<p>Redis에서 카운팅하기에 가장 쉬운 방법은 <code>key</code>를 만들고 카운팅이 필요한 상황마다 수치를 1씩 증가시키는 방법이다. 이건 <code>String</code>의 <code>INCR</code>(Increment) 함수를 사용하면 간단하게 구현이 가능하다. </p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/93b27f39-3f43-4d34-b778-73100ae9619c/image.png" alt=""></p>
<p>예제를 통해 살펴보자.</p>
<p>우선 <code>score:a</code>라는 키를 생성하고 해당 키에 10을 저장한 후에 <code>INCR</code> 함수를 사용하면 1이 증가하여 11이 된다. </p>
<p>여기서 <code>INCRBY</code> 함수를 사용하여 값을 4로 지정하면 4가 증가하여 15가 되는 것을 확인할 수 있다.</p>
<br>
<br>

<p>두 번째 <code>Counting</code> 방식은 Bit 연산을 사용하는 방식이다. </p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/a65d8bd9-e32a-4f8b-b49b-2707e989469f/image.png" alt=""></p>
<p>해당 방식을 이용하면 저장 공간을 많이 절약할 수 있다. 예를 들어 서비스에 접속한 유저 수를 카운팅하고 싶다면 날짜 키를 생성하고 유저 ID에 해당하는 비트를 1로 올려준다. 1개의 비트가 1명을 의미하므로 천 만 명 개의 비트는 천 만 명의 접속자를 표현 가능하지만 1.2MB의 매우 작은 저장 공간만 차지한다. </p>
<p>하지만 해당 방식을 사용하려면 모든 데이터를 정수로 표현가능해야 된다는 전제 조건이 존재한다. 유저 ID가 0 이상의 정수 값이라면 해당 방식을 사용할 수 있을 것이다. </p>
<br>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/253aced9-17b7-4e26-82f7-90297d5634fe/image.png" alt=""></p>
<p>마지막 카운팅 방식은 <code>HyperLogLog</code>를 사용하는 방식이다. HyperLogLog는 모든 String 데이터 값을 유니크하게 구별할 수 있다. 이는 Set과 비슷하지만 대량의 데이터를 카운팅할 때에 훨씬 더 적절하다. 그 이유는 HyperLogLog는 저장되는 데이터의 개수와 상관없이 모든 값이 12KB로 고정되어 저장되기 때문이다. </p>
<p>대신에 한 번 저장된 값은 다시 불러올 수는 없는데 경우에 따라 데이터를 보호하기 위한 목적으로 사용할 수 있다. 예를 들어 웹사이트에 방문한 IP가 몇 개나 되는지, 크롤링한 URL의 개수가 몇 개인지, 검색 엔진에서 검색된 단어가 몇 개인지 등의 엄청 크고 유니크한 값을 카운팅할 때 매우 적절하다.</p>
<p><code>PFADD</code>    커맨드로 데이터를 저장하고 <code>PFCOUNT</code>로 저장된 데이터를 조회할 수 있다. </p>
<p>만약 일별로 데이터를 저장했는데 일주일치를 취합해서 보고 싶다면 <code>PFMERGE</code>로 키들을 merge할 수 있다. </p>
<br>
<br>
<br>

<h2 id="2-messaging-예시">2. Messaging 예시</h2>
<p>Redis의 <code>List</code>는 <code>Message(Event) Queue</code>로 사용하기 적절하다. </p>
<p>특히 자체적으로 <code>blocking</code> (어떤 작업을 실행할 때, 해당 작업의 완료를 기다리느라 다른 작업들이 중단되고 대기하는 상태) 기능을 제공하기 때문에 이를 적절히 사용하면 불필요한 <code>polling</code> (하나의 장치가 다른 장치의 상태를 주기적으로 확인하여, 특정 조건이 만족되었을 때 데이터를 처리하는 방식)을 막을 수 있다.</p>
<br>

<p>아래 예제를 통해 살펴보자.</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/e231349f-2f1c-4c14-a549-f2984067316f/image.png" alt=""></p>
<p>클라이언트 A가 <code>BRPOP</code> 커맨드를 통해 myqueue라는 리스트에서 데이터를 꺼내오려고 하지만 현재 리스트 안에 데이터가 없어 대기를 하고 있는 상황이라고 가정해보자. </p>
<br>



<p><img src="https://velog.velcdn.com/images/kim_dg/post/d27dbb71-10d1-4a01-8417-41fefafe1d1e/image.png" alt=""></p>
<p>이 때 클라이언트 B가 &quot;hi&quot;라는 값을 넣어주면 클라이언트 A에서 해당 값을 바로 확인할 수 있다. </p>
<br>
<br>



<p><code>LPUSHX</code>나 <code>RPUSHX</code> 같은 커맨드를 사용하면 키가 있을 때에만 리스트에 데이터를 추가하는데 이를 잘 사용하면 굉장히 유용하다. </p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/91c3f6a5-e8f8-4e39-a2ad-598eafd0a814/image.png" alt=""></p>
<p>키가 이미 있다는 것은 사용된 적이 있는 큐라는 의미이고 사용했던 큐에만 메시지를 넣어줄 수 있기 때문에 비효율적인 데이터의 이동을 방지할 수 있다. </p>
<p>예시로 인스타그램, 페이스북, 트위터 같은 SNS에는 각 유저별로 타임라인이 존재하고 타임라인에 팔로워들의 데이터가 나오는데 트위터에서는 각 유저의 타임라인에 보일 트윗을 캐싱하기 위해 레디스의 리스트를 사용하는데 이 때 <code>RPUSHX</code> 커맨드를 사용한다. 이를 이용하여 트위터를 자주 사용하던 유저의 타임라인에만 새로운 데이터를 미리 캐시해놓을 수 있으며, 자주 사용하지 않는 유저는 캐싱 키가 존재하지 않기 때문에 트위터를 자주 사용하지 않는 유저들의 데이터를 미리 쌓아놓는 비효율적인 리소스 낭비를 방지하고 있다.  </p>
<br>
<br>





<p>Redis에서 메시징하는 마지막 방법은 <code>Stream</code>이다. </p>
<p>Stream은 로그를 저장하기 가장 적절한 자료구조이다. 실제 서버에 로그가 쌓이는 것처럼 모든 데이터는 <code>append-only</code> (모든 쓰기(write) 연산을 파일에 순차적으로 추가하여 저장) 방식으로 저장되며 중간에 데이터가 바뀌지 않는다. </p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/c07f9fed-4cfb-4385-97e8-3ab4db3d1dd0/image.png" alt=""></p>
<p>예제에서는 <code>XADD</code>라는 커맨드를 이용하여 mystreams라는 키에 데이터를 저장하고 있다. 키 이름 옆의 *은 ID 값을 의미하며 별표를 사용하면 레디스가 알아서 저장하고 ID 값을 반환해준다. 
반환된 값은 데이터가 저장된 시간을 의미한다. 이 값 뒤로는 해시처럼 key-value 형태로 데이터가 저장되는데 예제에서는 sensor ID 값에 1234를, 온도에는 19.8를 저장하였다. </p>
<p>Steram의 데이터를 읽는 방법은 다양한데 ID 값을 이용하여 시간 대역대로 저장된 값을 검색할 수 있고 실제 서버에서 로그를 읽는 것처럼 <code>ail-f</code> (리눅스에서 파일의 마지막 내용을 실시간으로 모니터링하는 명령어)를 사용하는 것처럼 새로 들어오는 데이터만 리스닝할 수 있다. </p>
<p>또한 카프카처럼 <code>소비자 그룹(Consumer Group)</code>이라는 개념이 존재하기 때문에 원하는 소비자만 특정 데이터를 읽게 할 수도 있다. 
실제로 Stream은 카프카의 개념을 많이 차용했는데 레디스 공식 문서에서 Stream은 메시지 브로커가 필요할 때 카프카를 대체해서 간단하게 사용할 수 있는 자료구조라고 소개하고 있다.</p>
<br>
<br>
<br>


<h1 id="redis-데이터-영구-저장rdb-vs-aof">Redis 데이터 영구 저장(RDB vs AOF)</h1>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/0fe7e4db-2d4c-4f32-b181-63a470146280/image.png" alt=""></p>
<p>Redis는 In-memory 데이터 스토어이기 때문에 모든 데이터가 메모리에 저장되어 서버나 레디스 인스턴스가 재시작되면 모든 데이터가 유실된다. 복제 구조를 사용하고 있더라도 코드상의 버그나 사람의 실수가 발생하여 데이터 유실이 발생하면 복제본에도 똑같이 적용되기 때문에 데이터 유실에 완전히 안전하다고는 할 수 없다. </p>
<p>따라서 Redis를 단순 캐시 이외의 용도로 사용하려면 적절한 데이터의 백업은 필수적이다. </p>
<br>
<br>


<p>Redis에서는 데이터를 영구 저장하는 데에 2가지 방법을 제공하고 있다.</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/70caed33-98b9-4631-ae58-3c3727c2d7d0/image.png" alt=""></p>
<p>그 중 하나인 <code>AOF(Append Only File)</code>은 데이터를 변경하는 커맨드가 들어오면 커맨드를 그대로 모두 저장한다. </p>
<p>다른 하나인 <code>RDB</code>는 스냅샷 방식으로 작동하기 때문에 저장 당시의 메모리에 있는 데이터를 그대로 사진 찍듯이 찍어서 파일로 저장한다.</p>
<br>


<p>AOF에는 key1에 a가 저장되었다가 apple로 변경되고 key2가 들어왔다가 삭제된 기록이 모두 남아있다. </p>
<p>하지만 RDB에는 key1이 apple인 값만 남아있다. </p>
<p>즉, AOF는 Append Only하게 동작하기 때문에 데이터가 추가되기만 하여 대부분의 경우 RDB 파일보다 용량이 크다. 따라서 AOF 파일은 주기적으로 압축하여 재작성되는 과정을 거쳐야 한다. </p>
<p>(참고로 위의 예제 그림은 단순히 설명을 돕기 위한 예시고 AOF 파일은 레디스 프로토콜 형태로 저장되고, RDB 파일은 바이너리 파일 형태로 저장되기 때문에 직접 읽을 수는 없다.)</p>
<br>

<p>이제 이 두 가지 파일은 생성하는 방법을 알아보자.</p>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/3bdf5a72-c52a-4dc9-b871-36e321477780/image.png" alt=""></p>
<p>AOF 파일과 RDB 파일 모두 커맨드를 이용하여 파일을 직접 생성할 수 있으며 원하는 시점에 파일이 자동 생성되도록 설정할 수도 있다. </p>
<p>RDB의 경우 시간 단위로 파일을 저장할 수 있고, AOF의 경우 파일의 크기를 기준으로 해서 압축되는 시점을 설정할 수 있다.</p>
<br>

<p>그렇다면 2가지 방법 중에 어떠한 방법을 선택해야 할까???(단순히 Redis를 캐시용으로만 사용한다면 영구 저장할 필요는 없다.)</p>
<br>


<br>




<p><img src="https://velog.velcdn.com/images/kim_dg/post/279212cb-d781-44c0-b9ab-dfab669d6545/image.png" alt=""></p>
<p>만약 데이터 백업이 필요한데 어느 정도의 데이터 손실은 감수할 수 있다면 RDB만 사용해도 된다. 단, redis.conf 파일에 &#39;SAVE&#39; 옵션을 적절히 사용해야 한다. </p>
<p>위의 예시의 &#39;SAVE 900 1&#39;이라는 커맨드를 통해 900초 동안 1개 이상의 키가 변경되었을 때 RDB 파일을 재작성하라는 의미이다. </p>
<br>


<p>하지만 만약 장애 상황 직전까지의 모든 데이터가 보장되어야 하는 경우라면 AOF를 사용해야 한다. 이 때 <code>APPENDFSYNC</code> 옵션이 기본값인 every second의 경우라면 최대 1초 사이의 데이터는 유실될 가능성이 존재한다. </p>
<br>

<p>마지막으로 가장 강력한 내구성이 필요하다면 RDB와 AOF를 동시에 사용하라고 Redis 공식 문서에 기재되어 있다. </p>
<br>
<br>
<br>


<h1 id="redis-아키텍처repulication-vs-sentinel-vs-cluster">Redis 아키텍처(Repulication vs Sentinel vs Cluster)</h1>
<br>

<p>Redis는 간단히 3개의 아키텍처로 나눌 수 있다. </p>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/3e5c9966-6e19-44a6-8733-175d4e343247/image.png" alt=""></p>
<p><strong><code>리플리케이션</code></strong> 구성, 즉 복제 구성은 <code>마스터 노드</code>와 마스터를 복제하는 <code>리플리카 노드</code>만 존재하는 가장 간단한 구조이다.  </p>
<p><strong><code>센티널 구성</code></strong>은 마스터와 리플리카 노드 외에 추가로 각 노드들이 제대로 작동하고 있는지 계속 모니터링하는 <code>센티널 노드</code>들이 필요하다. </p>
<p><strong><code>클러스터 구성에</code></strong>서는 최소 3대의 마스터가 필요하고 <code>샤딩</code> 기능을 제공한다.</p>
<br>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/ff1dc49f-a850-414f-9a9d-d8c62014908e/image.png" alt=""></p>
<p>리플리케이션 구성은 단순히 복제만 구현된 구조이다. 모든 Redis의 구조에서는 복제는 비동기식으로 동작하여 마스터에서 복제본에 데이터가 잘 전달되었는지 매번 확인하고 기다리지 않는다. </p>
<p>이 구조는 <code>HA 기능</code>(고가용성(High Availability),  하드웨어나 소프트웨어 장애 발생 시에도 서비스가 중단되지 않도록 시스템 이중화를 통해 안정성을 확보하는 기술)이 없기 때문에 마스터에 장애가 발생하면 수동으로 여러 작업들을 변경해줘야 한다. </p>
<p>우선 리플리카 노드에 직접 접속하여 복제를 끊어야 하고, 애플리케이션에서도 연결 설정을 변경하여 배포하는 작업이 필요하다. </p>
<br>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/9b895712-3f1a-422d-a8b4-b4372b577602/image.png" alt=""></p>
<p>센티널 노드는 일반적으로 다른 노드들을 계속 모니터링하는 역할을 담당하는데 그러다 마스터 노드가 죽으면 자동으로 <code>fail over</code> (시스템, 서버, 네트워크 등에서 장애가 발생했을 때 예비 시스템이 자동으로 주 시스템을 대신하여 서비스를 이어받는 기능)을 발생시켜 기존의 리플리카 노드 중 하나가 마스터 노드로 승격된다. </p>
<p>이 때 애플리케이션에서는 연결 정보를 변경할 필요 없이 센티널 노드만 알고 있으면 되고 센티널 측에서 변경된 마스터 노드로 자동으로 설정을 변경해준다. </p>
<p>센티널 구조를 사용하기 위해서는 센티널 프로세스를 추가로 띄워야 하는데 이때 마스터 노드가 fail over되였는지 여부를 센티널들의 과반수 투표를 통해 결정하기 때문에 센티널은 항상 3대 이상의 홀수여야 한다.</p>
<br>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/47197430-c4c0-4520-a98f-cae5ad5c1e5c/image.png" alt=""></p>
<p>클러스터 구성에서는 데이터가 여러 마스터 노드에 분할되어 저장되는 샤딩 기능을 제공한다. 이 구성에서는 모든 노드가 서로를 감시하고 있다가 마스터가 비정상 상태라고 판단되면 자동으로 fail over를 진행한다. </p>
<p>이를 위해서는 최소 3대 이상의 마스터 노드가 필요하며 각 마스터 노드에 리플리카 노드를 하나씩 추가하는 게 일반적인 구조이다. </p>
<br>
<br>

<p>그렇다면 우리 서비스에는 어떠한 아키텍처를 선택해야 할까??? 간단하게 정리해보자.</p>
<br>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/2c48cabf-91c6-470a-9698-8110f286f220/image.png" alt=""></p>
<p>먼저 복제 기능이 필요한지, 자동 HA 기능(fail over) 필요한지의 여부를 판단하고 필요하다면 샤딩 기능도 필요한지 판단해보자. </p>
<p>서비스의 scale out이 필요하여 샤딩이 필요하다면 클러스터 구조를 사용하고, 그게 아니라 HA 기능만 필요하다면 센티널 구조를, HA도 필요없고 복제만 필요하다면 리플리카 구조를, 전부 필요없다면 마스터 노드 1개만 사용하는 스탠드 얼론 구조를 선택하자.</p>
<br>
<br>
<br>
<br>






<p>자료 출처:
<a href="https://www.youtube.com/watch?v=tVZ15cCRAyE">https://www.youtube.com/watch?v=tVZ15cCRAyE</a>
<a href="https://www.youtube.com/watch?v=92NizoBL4uA">https://www.youtube.com/watch?v=92NizoBL4uA</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spotless: 코드 포맷팅 툴]]></title>
            <link>https://velog.io/@kim_dg/Spotless-%EC%BD%94%EB%93%9C-%ED%8F%AC%EB%A7%B7%ED%8C%85-%ED%88%B4</link>
            <guid>https://velog.io/@kim_dg/Spotless-%EC%BD%94%EB%93%9C-%ED%8F%AC%EB%A7%B7%ED%8C%85-%ED%88%B4</guid>
            <pubDate>Sat, 26 Apr 2025 10:58:36 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kim_dg/post/9d80b172-68c4-46ff-87f5-2bf0c1d35ea6/image.png" alt=""></p>
<p><br><br><br></p>
<h1 id="spotless란">Spotless란?</h1>
<blockquote>
<p><strong><span style = "color:red"><code>Spotless</code></span>: 코드 포맷팅 툴로, 코드 스타일 규칙을 자동으로 적용하여 코드베이스의 일관성을 유지하는 데 도움을 준다.</strong></p>
</blockquote>
<p><br><br><br></p>
<h1 id="spotless-적용">Spotless 적용</h1>
<br>

<h2 id="0-buildgradle-설정">0. build.gradle 설정</h2>
<p><code>build.gralde</code>의 <code>plugins</code>에 다음 spotelss 설정들을 추가한다.</p>
<pre><code class="language-groovy">plugins {
    id &#39;com.diffplug.spotless&#39; version &#39;6.25.0&#39;
}</code></pre>
<br>

<pre><code class="language-groovy">// spotless 설정
spotless {
    java {
        target(&quot;**/*.java&quot;) // 포매팅할 파일의 대상 지정(프로젝트 하위 모든 디렉토리의 .java 파일)
        googleJavaFormat().aosp() // Google Java Format을 사용해서 포매팅(AOSP 스타일은 기본 Google 스타일보다 줄 바꿈을 좀 더 많이 하고, 줄 길이에 더 민감하게 반응함)
        importOrder() // import 문 정리 순서 지정(디폴트 설정(알파벳 순, static import 아래)이 적용)
        removeUnusedImports() // 사용하지 않는 import 구문 제거.
        trimTrailingWhitespace() // 각 라인 끝에 있는 불필요한 공백 제거.
        endWithNewline() // 파일 끝에 빈 줄 한 줄 추가.
    }
}</code></pre>
<p><br><br></p>
<h2 id="1-커밋-시-자동-포맷팅-적용">1. 커밋 시 자동 포맷팅 적용</h2>
<p>이제 <code>git commit</code> 명령어로 커밋시 자동으로 spotless가 설정한 포맷팅들을 적용하게 설정해보자.</p>
<h3 id="githookspre-commit-파일-생성">.git/hooks/pre-commit 파일 생성</h3>
<p>커밋 시에 포매팅이 적용되게 할려면 .git파일의 hooks에서 spotless가 자동 실행되게 설정하면 된다. hooks 중 커밋 이전의 단걔인 pre-commit 이라는 단계를 활용하기 위해서 아래 디렉토리에 접근하여 파일을 만든다.</p>
<br>

<p><code>git init</code> 커맨드나 프로젝트를 생성할 때 레포지토리 생성 옵션을 체크해서 <code>.git</code> 파일이 있다는 가정하에 해당 커맨드들을 입력해주자.</p>
<br>

<pre><code class="language-bash">cd .git/hooks/  # .git의 hooks 디렉토리로 이동
touch pre-commit  # pre-commit 파일 생성
open pre-commit  # pre-commit 파일 열기</code></pre>
<br>

<p>pre-commit 파일에 아래 내용을 붙여넣고 저장해주자.</p>
<pre><code class="language-bash">#!/bin/bash

./gradlew spotlessJavaApply

if [ $? -ne 0 ]; then
  echo &quot;spotlessJavaApply 태스크 실행에 실패&quot;
  exit 1
fi

echo &quot;코드 정리 완료&quot;
exit 0
</code></pre>
<br>



<p>이후 hook 실행 권한을 부여해주자.</p>
<pre><code class="language-bash">chmod ug+x .git/hooks/*</code></pre>
<br>

<p>이제 <code>git commit</code>시 코드 포맷팅이 실행되는 것을 확인할 수 있을 것이다. 만약 <code>spotless</code>에 의해 코드 수정이 이루어졌다면 다시 <code>add-commit</code>을 반복한 후 깃허브에 올리면 된다.</p>
<p><br><br><br></p>
<p>참고 사이트: <a href="https://whitepro.tistory.com/952">https://whitepro.tistory.com/952</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Flyway:  DB 형상 관리 시스템]]></title>
            <link>https://velog.io/@kim_dg/Flyway-DB-%ED%98%95%EC%83%81-%EA%B4%80%EB%A6%AC-%EC%8B%9C%EC%8A%A4%ED%85%9C</link>
            <guid>https://velog.io/@kim_dg/Flyway-DB-%ED%98%95%EC%83%81-%EA%B4%80%EB%A6%AC-%EC%8B%9C%EC%8A%A4%ED%85%9C</guid>
            <pubDate>Wed, 16 Apr 2025 19:00:01 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kim_dg/post/967a4f61-2ec8-4768-8a7f-1711e8e1fa47/image.png" alt=""></p>
<p>Flyway 공식 Github: <a href="https://github.com/flyway/flyway">https://github.com/flyway/flyway</a></p>
<p><br><br></p>
<h1 id="flyway란">Flyway란?</h1>
<blockquote>
<p><strong><span style = "color:red"><code>Flyway</code></span>:  Flyway는 <code>데이터베이스 마이그레이션 도구(Database Migration Tool)</code>이다. 쉽게 말해서, DB 스키마 버전을 코드로 관리하게 해주는 형상 관리 도구(Version Control System)이라고 생각하면 된다.</strong><br>
<strong>※ Database Migration: 특정 데이터베이스에서 다른 데이터베이스로 이동하는 것이다. Flyway에서는 데이터베이스에서 일어나는 모든 변경을 의미한다.</strong></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/a565a07d-cdd7-4dd3-91a4-e8738a0d92c8/image.png" alt=""></p>
<p>소스 코드의 변경 감지와 형상 관리는 깃허브에서 진행되는 것과 같이 DB의 변경 감지와 형상 관리는 Flyway에서 일어난다.</p>
<br>

<p><strong>※ 형상 관리: 소프트웨어의 변경 사항을 체계적으로 추적하고 통제하는 것</strong></p>
<p><br><br></p>
<h1 id="flyway를-굳이-쓰지-않는다면">Flyway를 굳이 쓰지 않는다면?</h1>
<p>개발 도중에 DB의 스키마 변경이 일어나는 경우는 종종 생겨난다. 예를 들어 특정 테이블에 새로운 컬럼을 추가한다고 가정해보자. 이런 경우에는 Flyway 적용 전에는 보통 다음과 같은 두 가지 방식을 많이 사용하였을 것이다.</p>
<br>

<p><strong>1. jpa의 <code>ddl-auto</code> 사용</strong></p>
<pre><code class="language-yml">spring:
  jpa:
    hibernate:
      ddl-auto: create or update</code></pre>
<br>

<p>보통 가장 흔히 사용한 방법은 <code>ddl-auto</code>로 JPA에서 자동으로 변경된 테이블을 새로 생성하거나 수정하는 방식이었을 것이다. 하지만 <code>ddl-auto</code> 자체가 실무에서는 많이 지양되는 기술이고 <code>create</code> 옵션을 사용하면 기존 테이블에 있던 이전 데이터까지 전부 초기화해버리기 때문에 배포 환경에서 절대 사용불가능하다.</p>
<br>

<p>또한 <code>update</code> 옵션 역시 위험하기는 마찬가지이다. 새로운 엔티티나 테이블을 추가하는 것까지는 큰 문제가 발생하지 않는다.</p>
<p>하지만 예를 들어 <code>username → name</code>으로 변경하면 Hibernate는 <code>username 컬럼이 사라졌다고 판단</code> → <code>DROP COLUMN</code> 후 <code>ADD COLUMN name</code>을 시도하여 해당 컬럼의 기존 데이터가 전부 날아갈 수 있고 컬럼의 데이터 타입을 <code>String → int</code>으로 바꾸어도 타입이 바뀐 것을 확실히 인식 못하거나 DB마다 다르게 동작할 가능성이 존재한다. 또한 <code>Hibernate</code>는 제약 조건 변경에 있어 매우 조심스럽고 보수적으로 판단하기 때문에 <code>nullable=true → nullable=false</code> 바꿔도 <code>ALTER TABLE</code>을 안 하는 것처럼 반영하지 않거나 애매하게 처리할 수 있다.</p>
<br>

<p><img src="https://velog.velcdn.com/images/kim_dg/post/4ea3ba3b-ae3c-4c8a-85b6-b0b9a7e4f62c/image.png" alt=""></p>
<p><br><br></p>
<p><strong>2. 각 배포 환경(<code>local</code>, <code>dev</code>, <code>prod</code> 등)을 일일이 돌아다니며 직접 schema 수정</strong></p>
<p>이 방법은 굳이 필자가 말 안 해도 매번 사용하기에는 매우 비효율적이고 위험한 방법이다...</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/9eabc298-a609-4e3b-8754-bd6de4dffd64/image.png" alt=""></p>
<p>local에서 h2를 사용하여 개발하고 개발이 완료되면 MySQL을 사용하는 dev로 merge하고 1주일에 한 번씩 MySQL을 사용하는 deploy로 merge한다고 가정해보자. </p>
<br>

<p><img src="https://velog.velcdn.com/images/kim_dg/post/b39264ad-ba51-4a22-8c23-702e7e363d9b/image.png" alt=""></p>
<p>Crew 테이블에 주소 저장 로직을 추가해달라는 요청을 받아</p>
<br>

<p><img src="https://velog.velcdn.com/images/kim_dg/post/5ea87438-921a-410f-b813-6d2c971ffc94/image.png" alt=""></p>
<p><code>address</code> 컬럼을 추가하고 주소 저장 로직까지 작성하여 </p>
<br>

<p><img src="https://velog.velcdn.com/images/kim_dg/post/3dc2477f-38af-4d14-846f-e7ff5d66241e/image.png" alt=""></p>
<p>테스트까지 성공하고 dev 브랜치에 merge하고 deploy DB에 컬럼을 추가하려고 했지만 </p>
<br>

<p><img src="https://velog.velcdn.com/images/kim_dg/post/f4ee49dd-d9ba-43ac-95b4-9b403e0fd556/image.png" alt=""></p>
<p>회의로 인해 deploy DB에 컬럼을 추가하는 것을 까먹었다고 가정해보자.</p>
<br>

<p><img src="https://velog.velcdn.com/images/kim_dg/post/cdaa9873-5d98-4f92-8105-aea3c28c195e/image.png" alt=""></p>
<p>이로 인하여 배포 과정에서 주소 저장 로직에 실패하여 오류가 발생하였다.</p>
<p><br><br></p>
<p>하지만 ddl-auto와 직접 수작업으로 컬럼 추가 대신에 만약 Flyway를 사용하였다면 어떻게 되었을까??</p>
<p><br><br></p>
<h1 id="flyway를-사용해야-하는-이유">Flyway를 사용해야 하는 이유</h1>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/67fdf33d-68ea-45e9-b02e-94dec7b18c90/image.png" alt=""></p>
<p>Flyway를 사용하였다면 엔티티에 <code>address</code> 컬럼을 추가하고 비즈니스 로직에 <code>주소 저장 로직 코드</code>를 추가한 후에 직접 deploy DB에 컬럼을 추가하는 것이 아니라 <strong><code>V2__add_address.sql</code> 파일을 만들고 해당 파일에 address 컬럼을 추가하는 쿼리를 작성해놓으면 <span style = "color:red">배포가 될 때 자동으로 Flyway가 DB 변경 사항을 적용시켜줌으로써 장애가 발생하지 않는다.</span></strong></p>
<br>

<p><img src="https://velog.velcdn.com/images/kim_dg/post/e6c1212f-a6f5-44d2-aaa9-e7d2523e80d2/image.png" alt=""></p>
<br>

<p><img src="https://velog.velcdn.com/images/kim_dg/post/79d3bc24-32a3-4d83-8243-e41ff4b77200/image.png" alt=""></p>
<br>

<p><strong><span style = "color:red">※ 이처럼 개발 과정 중에 빈번이 일어나는 DB의 변경을 효율적이고 안전하고 자동으로 처리하고 여러 편의성으로 인하여 <code>Flyway</code>를 사용한다.</span></strong></p>
<p><br><br></p>
<h1 id="flyway의-특징">Flyway의 특징</h1>
<ul>
<li><strong><span style = "color:red"><code>Migrate</code></span></strong>: 데이터베이스의 변경</li>
</ul>
<ul>
<li><p><strong><span style = "color:red"><code>Baseline</code></span></strong>: 비어있지 않은 기존 데이터베이스에서 Flyway 적용</p>
</li>
<li><p><strong><span style = "color:red"><code>Info</code></span></strong>: 데이터베이스의 변경 이력 저장</p>
</li>
<li><p><code>Repair</code>: 실패한 마이그레이션 파일을 복구</p>
</li>
<li><p><code>Validate</code>: Flyway를 복구할 때 데이터베이스 적용 가능 여부 확인</p>
</li>
<li><p><code>Undo</code>: 최근에 적용되었던 마이그레이션 파일 적용 취소</p>
</li>
<li><p><code>Clean</code>: 데이터베이스 초기화</p>
</li>
</ul>
<br>

<p>이 중 비중이 높은 <code>Migrate</code>, <code>Baseline</code>, <code>Info</code> 3가지 특성에 대해 자세히 살펴보자.</p>
<p><br><br></p>
<h2 id="migration-데이터베이스의-변경">Migration: 데이터베이스의 변경</h2>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/09eba074-98ce-4348-9119-efb898492a52/image.png" alt=""></p>
<p>비어있는 DB에서 Flyway를 사용하여 Crew라는 테이블을 만들기 위해</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/4998bd47-43d1-4c56-99cc-ac4ae8167154/image.png" alt=""></p>
<p><code>V1__init.sql</code> 파일을 만들고 파일 안에 <code>create</code> 쿼리를 작성하고 Flyway가 적용된 스프링 부트 애플리케이션을 실행시키면 </p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/2702cc41-24eb-4b30-aa8c-010855c15938/image.png" alt=""></p>
<p>자동으로 Flyway가 <code>Crew</code> 테이블이 만들어지고 Flyway가 처음 적용되었기 때문에 자동적으로 <strong>데이터베이스 변경 이력이 저장되는</strong> <code>flyway_schma_history</code>라는 테이블이 만들어진다.</p>
<br>

<h3 id="migration-파일-이름-규칙">Migration 파일 이름 규칙</h3>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/6263933e-976d-4c1a-958c-78ca340bbad3/image.png" alt=""></p>
<br>

<p><img src="https://velog.velcdn.com/images/kim_dg/post/7b5dc132-ed9e-4f88-ac53-a48533f562ba/image.png" alt=""></p>
<br>

<p>[파일 이름 예시]</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/64eda715-be0c-4ed9-a0d9-818f148d27a8/image.png" alt=""></p>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/62085591-4eaf-4ad4-9dc8-7d87226d650a/image.png" alt=""></p>
<p>이 때 파일의 버전은 새로 적용하려는 파일은 기존에 적용된 파일의 버전보다 무조건 높아야 한다.</p>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/d143b7ec-dab5-4799-a5a0-daf757632349/image.png" alt=""></p>
<p>예를 들어 이미 <code>V4</code>까지 파일이 적용된 상태에서 <code>V1</code>과 <code>V2</code> 사이에 새로운 작업을 추가하고 싶다고 <code>V1.2</code>라는 새로운 마이그레이션 파일을 정의한다고 해도 무시되어 적용되지 않는다.</p>
<br>

<p><img src="https://velog.velcdn.com/images/kim_dg/post/1c4cfa8f-1fee-4135-bb98-0b4089ecabd6/image.png" alt=""></p>
<p><br><br></p>
<h3 id="baseline-비어있지-않은-기존-데이터베이스에서-flyway-적용">Baseline: 비어있지 않은 기존 데이터베이스에서 Flyway 적용</h3>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/0fedf75d-51d0-4b59-bbcb-27389d0f6a7c/image.png" alt=""></p>
<p>이미 <code>Crew</code>라는 테이블이 존재하는 DB에서 Flyway를 사용하여 <code>Address</code>라는 테이블을 생성하기 위해서는 어떻게 해야 할까??</p>
<br>

<p><img src="https://velog.velcdn.com/images/kim_dg/post/97c87e1d-8278-41ae-a7ed-e0ab7755f91a/image.png" alt=""></p>
<p>우선 아무것도 입력하지 않은 <code>V1__baseline.sql</code> 파일을 만든 다음 </p>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/d4aada6b-0a14-4ef0-8ead-4199ec8bb8cd/image.png" alt=""></p>
<p>그 다음 <code>create address</code> 쿼리 문이 적힌 <code>V2__address.sql</code> 파일을 작성하고 Flyway가 적용된 스프링 부트 애플리케이션을 실행시키면 </p>
<br>

<p><img src="https://velog.velcdn.com/images/kim_dg/post/17722653-a26d-4dc2-884c-fc5b816bddda/image.png" alt=""></p>
<p>Flyway가 자동적으로 <code>Address</code> 테이블을 만들고 처음 Flyway가 적용되었기 때문에 자동적으로 <code>history</code> 테이블도 생성된다.</p>
<p><br><br></p>
<h3 id="info-데이터베이스의-변경-이력-저장">Info: 데이터베이스의 변경 이력 저장</h3>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/8fadd068-36d9-42e7-97df-1e962ad455cc/image.png" alt=""></p>
<p>Info는 Flyway가 처음으로 적용되면 자동으로 생성되는 <code>flyway_schema_history</code> 테이블에 저장되는데 이 중 중요한 것은 마이그레이션 파일의 버전이 기록되는 <code>version</code> 컬럼과 마이그레이션 파일의 해시값을 나타내는 <code>checksum</code> 컬럼이다.</p>
<p><code>version</code>이 높아지면 파일이 적용되고 만약 기존에 적용된 파일에서 sql 쿼리가 변경된다면 Flyway는 파일이 오염되었다고 판단해 <code>checksum</code> 값을 바꿔버리고 이 변경된 checksum 값으로 인해 오류가 발생한다. </p>
<p><strong><span style = "color:red">따라서 데이터베이스의 추가나 삭제같은 변경 작업을 진행하려면 항상 새로운 파일을 추가해서 진행해야 한다.</span></strong></p>
<p><br><br><br></p>
<h1 id="flyway-작동-원리">Flyway 작동 원리</h1>
<ol>
<li><p>Spring Boot가 실행됨</p>
</li>
<li><p>flyway.enabled=true 설정으로 Flyway 활성화됨</p>
</li>
<li><p>classpath:db/migration 폴더에서 V1__, V2__ 식으로 된 SQL 파일을 순서대로 찾음</p>
</li>
<li><p>이미 실행된 마이그레이션은 flyway_schema_history 테이블에 기록되어 있어, 다시 실행하지 않음</p>
</li>
<li><p>새로 추가된 SQL 파일만 실행함</p>
</li>
</ol>
<p><br><br><br></p>
<h1 id="flyway-설정법">Flyway 설정법</h1>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/bcc3eb20-9856-46ae-9b07-1016ed065c19/image.png" alt=""></p>
<p>MySQL 8.x 버전 이상부터는 <code>build.gradle</code>의 <code>dependencies</code>에</p>
<pre><code class="language-gradle">implementation &#39;org.flywaydb:flyway-mysql&#39; // mysql 8.x 이상부터는 추가</code></pre>
<p>까지 추가해줘야 한다.</p>
<br>


<pre><code class="language-yml">spring:
  flyway:
    enabled: true  # flway 실행(기본값 true)
    url: jdbc:mysql://localhost:3306/${DB_NAME}?serverTimezone=Asia/Seoul  # DB 주소
    username: ${DB_USERNAME}  # DB user 아이디
    password: ${DB_PASSWORD}  # DB user 비밀번
    baseline-on-migrate: true  # Baseline 적용(변경이력 테이블이 자동생성되지 않는 경우도 방지)
    locations: classpath:db/migration  #마이그레이션 파일 위치 지정(기본 위치: db/migration )
</code></pre>
<p><code>applicaion.yml</code>에 해당 flyway 설정도 추가해줘야 한다.</p>
<br>

<p>[Flyway 기본 디렉토리 구조]</p>
<pre><code class="language-pqsql">src
└── main
    └── resources
        └── db
            └── migration
                ├── V1__create_users_table.sql
                ├── V2__create_questions_table.sql
                └── V3__insert_sample_data.sql
</code></pre>
<p>디렉토리 구조는 자유롭게 커스텀이 가능하다.</p>
<p><br><br><br><br></p>
<p>[참고 문헌]</p>
<ul>
<li><p>[10분 테코톡] 에단의 Flyway: <a href="https://www.youtube.com/watch?v=_fgOxPRo8tU">https://www.youtube.com/watch?v=_fgOxPRo8tU</a></p>
</li>
<li><p>[10분 테코톡] 🐶 코기의 Flyway: <a href="https://www.youtube.com/watch?v=pxDlj5jA9z4&amp;t=242s">https://www.youtube.com/watch?v=pxDlj5jA9z4&amp;t=242s</a></p>
</li>
<li><p>우리 팀에서 Flyway를 사용하는 이유: <a href="https://ecsimsw.tistory.com/m/entry/Flyway%EB%A1%9C-DB-Migration">https://ecsimsw.tistory.com/m/entry/Flyway%EB%A1%9C-DB-Migration</a></p>
</li>
<li><p>[Flyway] DB에 Flyway 적용하기: <a href="https://dbjh.tistory.com/90">https://dbjh.tistory.com/90</a></p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[데이터교환형식 JSON: 직렬화와 역직렬화]]></title>
            <link>https://velog.io/@kim_dg/%EB%8D%B0%EC%9D%B4%ED%84%B0%EA%B5%90%ED%99%98%ED%98%95%EC%8B%9D-JSON%EC%9D%98-%EC%A7%81%EB%A0%AC%ED%99%94%EC%99%80-%EC%97%AD%EC%A7%81%EB%A0%AC%ED%99%94</link>
            <guid>https://velog.io/@kim_dg/%EB%8D%B0%EC%9D%B4%ED%84%B0%EA%B5%90%ED%99%98%ED%98%95%EC%8B%9D-JSON%EC%9D%98-%EC%A7%81%EB%A0%AC%ED%99%94%EC%99%80-%EC%97%AD%EC%A7%81%EB%A0%AC%ED%99%94</guid>
            <pubDate>Sun, 30 Mar 2025 15:13:40 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kim_dg/post/13f332b1-04cd-49fc-b9fe-c5c2f87080c6/image.png" alt=""></p>
<br>






<h1 id="json이란">JSON이란???</h1>
<blockquote>
</blockquote>
<p><span style = "color:red"><strong>JSON</span>: JSON은 JavaScript Object Notation의 줄임말로 <span style = "color:red"><code>Javascript 객체 문법 으로 구조화된 데이터교환형식</code></span>이다. python, javascript, java 등 대부분의 언어에서 데이터 교환형식으로 쓰이며 객체문법 이외에도 단순 배열, 문자열도 표현 가능하다.</strong><br></p>
<blockquote>
</blockquote>
<br>

<p><span style = "color:red"><strong>※ JSON은 REST API에서 서버와 클라이언트 간의 데이터 교환 형식으로 많이 사용된다.</span></strong></p>
<p>예제: JSON 형태의 HTTP 응답</p>
<pre><code class="language-json">
{
  &quot;status&quot;: &quot;success&quot;,
  &quot;data&quot;: {
    &quot;userId&quot;: 42,
    &quot;username&quot;: &quot;springNcode&quot;,
    &quot;email&quot;: &quot;user@example.com&quot;
  }
}</code></pre>
<p>※ JSON 데이터는 <code>.json</code> 파일 확장자로 저장된다. -&gt; <code>ex) data.json</code></p>
<p>▶︎ <strong>JSON은 (여러 언어, 플랫폼 등에 대해서) 종속적이지 않고 독립적이다.</strong> 특정 언어나 플랫폼이 업데이트되거나 변화한다고 해도 데이터 형식인 JSON에는 영향이 가지 않는다.</p>
<br>

<h1 id="json의-기본-구조">JSON의 기본 구조</h1>
<p><strong>JSON 데이터는 <code>키(key)</code>와 <code>값(value)</code>으로 이루어진 속성(property) 쌍의 집합이다.
JSON 객체는 <code>중괄호 {}</code>로 감싸며, 배열(array) 은 <code>대괄호 []</code> 를 사용한다.</strong></p>
<pre><code class="language-json">{
  &quot;name&quot;: &quot;Alice&quot;,
  &quot;age&quot;: 25,
  &quot;isStudent&quot;: false,
  &quot;skills&quot;: [&quot;Java&quot;, &quot;Python&quot;, &quot;JavaScript&quot;],
  &quot;address&quot;: {
    &quot;city&quot;: &quot;Seoul&quot;,
    &quot;zipCode&quot;: &quot;12345&quot;
  }
}</code></pre>
<br>


<h2 id="json-데이터-유형">JSON 데이터 유형</h2>
<br>

<p>JSON에서는 6가지 데이터 유형을 지원한다.</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/1d3ddf38-4cf3-46b7-99a9-67c3195767b9/image.png" alt=""></p>
<p><br><br></p>
<h1 id="json-사용">JSON 사용</h1>
<p>이 JSON 파일을 언어에서 바로 쓸 수는 없다. 이 JSON을 언어에서 사용하기 위해서는 객체, 해시테이블, 딕셔너리 등으로 변환하는 과정이 필요하고 데이터로써 전송하기 위해서는 객체를 JSON으로 변환하는 별도의 과정들이 필요한데 이 때 사용되는 방법이 <strong><code>직렬화</code></strong>와 <strong><code>역직렬화</code></strong>이다.</p>
<ul>
<li>Javascript, Java : JSON을 객체로 변환하여 사용</li>
<li>Python : JSON을 딕셔너리로 변환하여 사용</li>
</ul>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/fa34029b-8834-4d5f-a98b-5a2d0ae551bf/image.png" alt=""></p>
<br>

<h2 id="직렬화serialization">직렬화(Serialization)</h2>
<blockquote>
<p><strong><span style = "color:red"><code>직렬화</code></span>: 외부의 시스템에서도 사용할 수 있도록 바이트(이진 데이터) 형태로 데이터를 변환하는 기술이다.<br>
<span style = "color:red">객체 → 바이트(파일, JSON, XML, 문자열 등) 형태로 변환</span></strong></p>
</blockquote>
<br>

<p><strong>&lt;Java에서 직렬화 예제 (Object → JSON)&gt;</strong></p>
<pre><code class="language-java">import com.fasterxml.jackson.databind.ObjectMapper; // JSON 직렬화를 위한 Jackson 라이브러리 import
import java.io.*; // 파일 및 객체 입출력을 위한 Java 기본 패키지 import

// User 클래스 정의, 직렬화를 위해 Serializable 인터페이스 구현
class User implements Serializable {
    private static final long serialVersionUID = 1L; // 직렬화된 객체의 버전 관리를 위한 UID
    public String name; // 사용자 이름 필드
    public int age; // 사용자 나이 필드

    // 생성자 정의
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

public class SerializationExample {
    public static void main(String[] args) throws Exception {
        // User 객체 생성
        User user = new User(&quot;Alice&quot;, 25);

        // JSON 직렬화 (객체 → JSON 문자열)
        ObjectMapper objectMapper = new ObjectMapper(); // Jackson의 ObjectMapper 인스턴스 생성
        String jsonString = objectMapper.writeValueAsString(user); // User 객체를 JSON 문자열로 변환
        System.out.println(&quot;직렬화된 JSON: &quot; + jsonString); // JSON 문자열 출력

        // 바이너리 직렬화 (객체 → 파일)
        FileOutputStream fileOut = new FileOutputStream(&quot;user.ser&quot;); // user.ser 파일 생성
        ObjectOutputStream out = new ObjectOutputStream(fileOut); // 객체를 파일에 저장할 스트림 생성
        out.writeObject(user); // User 객체를 직렬화하여 파일에 저장
        out.close(); // 스트림 닫기
        fileOut.close(); // 파일 스트림 닫기
        System.out.println(&quot;객체가 user.ser 파일에 저장됨&quot;); // 저장 완료 메시지 출력
    }
}
</code></pre>
<br>

<p>[출력 예시]</p>
<pre><code class="language-pqsql">직렬화된 JSON: {&quot;name&quot;:&quot;Alice&quot;,&quot;age&quot;:25}
객체가 user.ser 파일에 저장됨
</code></pre>
<p>객체가 JSON 또는 파일(user.ser)로 저장된다.</p>
<p><br><br></p>
<h2 id="역직렬화deserialization">역직렬화(Deserialization)</h2>
<blockquote>
<p><strong><span style = "color:red"><code>직렬화</code></span>: 해당 시스템에서 사용할 수 있도록 바이트(이진 데이터) 데이터를 알맞은 데이터(객체, 해시테이블, 문자열 등)로 변환하는 기술이다.<br>
<span style = "color:red">바이트 데이터(JSON, XML 등) → 객체로 변환하는 과정</span></strong></p>
</blockquote>
<br>

<p><strong>&lt;Java에서 역직렬화 예제 (JSON → Object)&gt;</strong></p>
<pre><code class="language-java">public class DeserializationExample {
    public static void main(String[] args) throws Exception {
        // JSON 역직렬화 (JSON 문자열 → 객체)
        ObjectMapper objectMapper = new ObjectMapper(); // Jackson 라이브러리의 ObjectMapper 객체 생성

        String jsonString = &quot;{\&quot;name\&quot;:\&quot;Alice\&quot;,\&quot;age\&quot;:25}&quot;; // JSON 문자열 예시

        // ObjectMapper를 사용하여 JSON 문자열을 User 객체로 역직렬화
        User user = objectMapper.readValue(jsonString, User.class);

        // 역직렬화된 객체의 값 출력
        System.out.println(&quot;역직렬화된 객체: &quot; + user.name + &quot;, &quot; + user.age);

        // 바이너리 역직렬화 (파일 → 객체)
        // &quot;user.ser&quot; 파일을 읽기 위한 입력 스트림 생성
        FileInputStream fileIn = new FileInputStream(&quot;user.ser&quot;); 

        // 파일 입력 스트림을 사용하여 객체를 읽을 수 있는 ObjectInputStream 생성
        ObjectInputStream in = new ObjectInputStream(fileIn); 

        // ObjectInputStream을 사용하여 파일에서 User 객체를 역직렬화
        User deserializedUser = (User) in.readObject();

        // 스트림을 닫기 전에 자원 해제
        in.close();
        fileIn.close();

        // 파일에서 로드된 객체의 값 출력
        System.out.println(&quot;파일에서 로드된 객체: &quot; + deserializedUser.name + &quot;, &quot; + deserializedUser.age);
    }
}

</code></pre>
<br>

<p>[출력 예시]</p>
<pre><code class="language-pqsql">역직렬화된 객체: Alice, 25
파일에서 로드된 객체: Alice, 25</code></pre>
<p>JSON 문자열과 파일에서 객체로 변환되었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준 11049] 피보나치 함수(Java, Python)]]></title>
            <link>https://velog.io/@kim_dg/%EB%B0%B1%EC%A4%80-11049-%ED%94%BC%EB%B3%B4%EB%82%98%EC%B9%98-%ED%95%A8%EC%88%98Java-Python</link>
            <guid>https://velog.io/@kim_dg/%EB%B0%B1%EC%A4%80-11049-%ED%94%BC%EB%B3%B4%EB%82%98%EC%B9%98-%ED%95%A8%EC%88%98Java-Python</guid>
            <pubDate>Sun, 30 Mar 2025 13:32:27 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kim_dg/post/0d4d7fd6-aa9d-4b8d-ad16-15392ebb042e/image.png" alt=""></p>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/81819a9a-2fcd-4b4b-9690-808bcc154729/image.png" alt="">
<img src="https://velog.velcdn.com/images/kim_dg/post/ab735482-64ab-4ddc-a1f2-82c96e622282/image.png" alt=""></p>
<p><br><br></p>
<h1 id="문제-해설">문제 해설</h1>
<pre><code class="language-java">import java.io.*;

public class Main {

    static int countZero, countOne;

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

        while (T-- &gt; 0) {
            int N = Integer.parseInt(br.readLine());
            countZero = 0;
            countOne = 0;
            fibonacci(N);
            System.out.println(countZero + &quot; &quot; + countOne);

        }
    }

    private static int fibonacci(int n) {
        if (n == 0) {
            countZero++; // 0 출력 횟수
            return 0;
        } else if (n == 1) {
            countOne++; // 1 출력 횟수
            return 1;
        } else {
            return fibonacci(n - 1) + fibonacci(n - 2);
        }
    }
}

</code></pre>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/cf1bb66c-ff4b-4a31-9bab-c4d73f97a411/image.png" alt=""></p>
<p>이런 식으로 기존의 피보나치 코드에다가 출력 횟수만 추가하면 중복된 계산량이 많아 <code>시간 초과</code>가 발생한다. 계산이 중복되는 것을 방지하기 위해 <strong><code>DP</code></strong>를 사용하여 이미 한 번 연산했던 값은 재사용해야하는 문제이다.</p>
<p><br><br></p>
<h1 id="문제-해설-1">문제 해설</h1>
<p>이번 문제는 피보나치 수에서 0과 1이 호출되는 횟수를 구하는 것이다. 처음에 N이 입력되면 N - 1, N - 2, ... 0까지 N 이하의 모든 수들이 결국 0과 1을 찾기 위하여 자신들보다 작은 수를 재귀호출할 것이다.
때문에 N에 관한 정답이 구해진다면 이후에 N 이하의 수가 입력된다면 다시 계산할 것 없이 이미 한 번 연산되어 <code>DP 테이블</code>에 저장되어 있는 <code>dp[N]</code> 값을 바로 꺼내와 반환하면 되므로 계산 과정을 획기적으로 단축시킬 수 있다.</p>
<p><br><br></p>
<h1 id="java-정답-코드">Java 정답 코드</h1>
<pre><code class="language-java">import java.io.*;

public class Main {

    static Integer[][] dp = new Integer[41][2]; // 인덱스 n : [0 출력 횟수, 1 출력 횟수]

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

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

        dp[0][0] = 1; // N = 0일 때, 0 호출 횟수
        dp[0][1] = 0; // N = 0일 때, 1 호출 횟수
        dp[1][0] = 0; // N = 1일 때, 0 호출 횟수
        dp[1][1] = 1; // N = 1일 때, 1 호출 횟수

        while (T-- &gt; 0) {
            int N = Integer.parseInt(br.readLine());
            fibonacci(N);
            bw.write(dp[N][0] + &quot; &quot; + dp[N][1] + &quot;\n&quot;);
        }

        bw.flush();
        bw.close();
    }

   private static Integer[] fibonacci(int N) {
      // 만약 N에 대한 0과 1의 호출 횟수가 아직 계산되지 않았다면 계산 수행
      if (dp[N][0] == null || dp[N][1] == null) {
          // N번째 피보나치 수에서 0이 출력되는 횟수는
          // (N-1번째 피보나치 수에서 0이 출력되는 횟수) + (N-2번째 피보나치 수에서 0이 출력되는 횟수)
          dp[N][0] = fibonacci(N - 1)[0] + fibonacci(N - 2)[0]; 

          // N번째 피보나치 수에서 1이 출력되는 횟수는
          // (N-1번째 피보나치 수에서 1이 출력되는 횟수) + (N-2번째 피보나치 수에서 1이 출력되는 횟수)
          dp[N][1] = fibonacci(N - 1)[1] + fibonacci(N - 2)[1];
      }

      // N번째 피보나치 수에서 0과 1이 출력된 횟수를 담은 배열 반환
      return dp[N];
  }
</code></pre>
<p><br><br></p>
<h1 id="python-정답-코드">Python 정답 코드</h1>
<pre><code class="language-python">import sys

sys.setrecursionlimit(10**6)
input = sys.stdin.readline

T = int(input())

dp = [[None, None] for _ in range(41)]

dp[0][0] = 1
dp[0][1] = 0
dp[1][0] = 0
dp[1][1] = 1


def fibonacci(N):
    if dp[N][0] is None or dp[N][1] is None:
        dp[N][0] = fibonacci(N - 1)[0] + fibonacci(N - 2)[0]
        dp[N][1] = fibonacci(N - 1)[1] + fibonacci(N - 2)[1]

    return dp[N]


for _ in range(T):
    N = int(input())
    print(fibonacci(N)[0], fibonacci(N)[1])




</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준 1002] 터렛(Java)]]></title>
            <link>https://velog.io/@kim_dg/%EB%B0%B1%EC%A4%80-1002-%ED%84%B0%EB%A0%9BJava</link>
            <guid>https://velog.io/@kim_dg/%EB%B0%B1%EC%A4%80-1002-%ED%84%B0%EB%A0%9BJava</guid>
            <pubDate>Sun, 30 Mar 2025 13:14:58 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kim_dg/post/bdd2de1b-3adc-4d8e-83be-ce712f024548/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/7c3eb394-3246-438c-872e-815d09793870/image.png" alt="">
<img src="https://velog.velcdn.com/images/kim_dg/post/1c298413-bcf9-4ef7-98f9-f5a1c04d04a4/image.png" alt=""></p>
<p><br><br></p>
<h1 id="문제-링크">문제 링크</h1>
<p><a href="https://www.acmicpc.net/problem/1002">https://www.acmicpc.net/problem/1002</a></p>
<p><br><br></p>
<h1 id="문제-해설">문제 해설</h1>
<p>이 문제는 결국 <strong>두 원의 교점 개수를 구하는 문제</strong>이다.</p>
<p>문제에서는 조규현과 백승환의 좌표 <code>(x₁, y₁)</code>와 <code>(x₂, y₂)</code>, 그리고 각각 적(류재명)까지의 거리 <code>r₁</code>과 <code>r₂</code>가 주어진다.
이를 수식으로 나타내면 다음과 같다.</p>
<p>류재명의 좌표를 <code>(x, y)</code>라고 하면, 류재명은 조규현을 중심으로 한 <code>반지름 r₁의 원 위</code>에 있어야 하며 동시에 류재명은 백승환을 중심으로 한 반지름 <code>r₂의 원 위</code>에도 있어야 한다.</p>
<p>즉, 두 개의 원의 교점 개수를 구하는 문제가 된다.
<br></p>
<p>두 원의 교점 개수를 구하기 위해서는 <strong>두 원의 중심점 사이의 거리를 유클리드 거리 공식을 이용하여 두 점 사이의 거리를 구하고 이 거리를 이용하여 두 원의 위치 관계를 조건에 따라 분류해야 한다.</strong></p>
<blockquote>
<p>※ <span style = "color:red"><strong><code>유클리드 거리 공식</code></span>: 두 점 사이의 거리를 구하는 공식</strong></p>
</blockquote>
<ul>
<li>*<em><code>( (𝑥₂ - 𝑥₁)² + (𝑦₂ - 𝑦₁)² )½  &lt;  ∣𝑟₂ - 𝑟₁∣</code> *</em><blockquote>
</blockquote>
</li>
</ul>
<br>

<h2 id="1-두-원의-접점의-개수가-무한대일-때">1. 두 원의 접점의 개수가 무한대일 때</h2>
<p>두 원의 접점의 개수가 무한대라는 것은 두 원이 완전히 정확하게 겹친 상태라는 뜻이다. 두 원이 완전하게 겹치기 위해서는 당연히 중심점의 좌표가 서로 같고 반지름의 길이도 서로 같아야 한다.</p>
<p><code>𝑥₁ = 𝑥₂, 𝑦₁ = 𝑦₂, 𝑟₂ = 𝑟₁</code></p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/7b694e36-d444-4791-8635-ddf7f668a8e3/image.png" alt=""></p>
<p><br><br></p>
<h2 id="2-두-원에-접점이-존재하지-않을-때접점-개수가-0">2. 두 원에 접점이 존재하지 않을 때(접점 개수가 0)</h2>
<p>두 원의 접점이 존재하지 않다는 것은 두 원이 서로 만나지 않는다는, 서로 겹치지 않는다는 것이다. 여기에는 두 가지 경우가 존재한다.</p>
<br>

<h3 id="2---1-두-원이-서로-외부에-있는-경우">2 - 1. 두 원이 서로 외부에 있는 경우</h3>
<p>두 원의 중점 사이의 거리가 두 원의 반지름을 합친 것보다 더 크다.</p>
<p><code>( (𝑥₂ - 𝑥₁)² + (𝑦₂ - 𝑦₁)² )½  &gt; 𝑟₁ + 𝑟₂</code></p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/c6fc0725-5d42-4bb7-a1c4-ec7eba2c1e58/image.png" alt=""></p>
<br>


<h3 id="2---1-한-원이-다른-원에-겹치지-않게-있는-경우">2 - 1. 한 원이 다른 원에 겹치지 않게 있는 경우</h3>
<p>이 경우 접점을 갖지 않으려면 반지름이 같지 않으면서 반지름의 차가 두 원간의 중점 거리보다 크면 된다.</p>
<p><code>( (𝑥₂ - 𝑥₁)² + (𝑦₂ - 𝑦₁)² )½  &lt;  ∣𝑟₂ - 𝑟₁∣</code></p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/2bd5e6dc-0bbb-45b5-8d6f-e9efab5c43e2/image.png" alt=""></p>
<p><br><br></p>
<h2 id="3-접점이-한-개일-때">3. 접점이 한 개일 때</h2>
<br>

<h3 id="3---1-내접하는-경우">3 - 1. 내접하는 경우</h3>
<p>이 경우는 중심점 사이의 거리가 반지름의 차와 같으면 된다.</p>
<p><code>( (𝑥₂ - 𝑥₁)² + (𝑦₂ - 𝑦₁)² )½  =  ∣𝑟₂ - 𝑟₁∣</code></p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/bee97a4a-04f8-4844-9b0d-aeb8a7557cdb/image.png" alt=""></p>
<br>





<h3 id="3---1-외접하는-경우">3 - 1. 외접하는 경우</h3>
<p>이 경우는 중심점 사이의 거리가 반지름의 합과 같으면 된다.</p>
<p><code>( (𝑥₂ - 𝑥₁)² + (𝑦₂ - 𝑦₁)² )½  =  ∣𝑟₂ + 𝑟₁∣</code></p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/a8bc71a3-cc07-4570-996e-62c401c299d3/image.png" alt=""></p>
<h2 id="접점이-두-개인-경우">접점이 두 개인 경우</h2>
<p>접점이 두 개인 경우는 다음과 같이 오직 1가지이다. </p>
<p><code>( (𝑥₂ - 𝑥₁)² + (𝑦₂ - 𝑦₁)² )½ &lt; ∣𝑟₂ + 𝑟₁∣ and ( (𝑥₂ - 𝑥₁)² + (𝑦₂ - 𝑦₁)² )½ &gt; ∣𝑟₂ - 𝑟₁∣</code></p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/2058f999-7ac2-41e1-ab8e-83e6b20d3259/image.png" alt=""></p>
<br>



<h1 id="두-원의-중점-사이의-거리-구하기">두 원의 중점 사이의 거리 구하기</h1>
<p>두 원의 중점인 (x1, y1)와 (x2, y2) 사이의 거리는 <code>( (𝑥₂ - 𝑥₁)² + (𝑦₂ - 𝑦₁)² )½</code>이다.
이는 코드로 다음과 같이 나타낼 수 있다.</p>
<pre><code class="language-java">double dis = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y2, 2));</code></pre>
<br>

<p>하지만 double, float형은 부소수점 타입이어서 근사치로 나오기 때문에 오차 때문에 == 비교 힘들다.
따라서 Math.sprt()로 루트 형태로 쓰지 말고 제곱되어 있는 형태인 <code>(𝑥₂ - 𝑥₁)² + (𝑦₂ - 𝑦₁)²</code>을 사용하고 동시에 비교 대상인 반지름 <code>r</code>에 제곱한 값인 <code>r²</code>과 비교하자. 이를 코드로 나타내면 다음과 같이 나타낼 수 있다.</p>
<pre><code class="language-java">int dis = (int) (Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));</code></pre>
<br>


<h1 id="정답-코드java">정답 코드(Java)</h1>
<pre><code class="language-java">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Main {

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

        int T = Integer.parseInt(br.readLine());
        while (T-- &gt; 0) {
            StringTokenizer st = new StringTokenizer(br.readLine(), &quot; &quot;);
            int x1 = Integer.parseInt(st.nextToken());
            int y1 = Integer.parseInt(st.nextToken());
            int r1 = Integer.parseInt(st.nextToken());
            int x2 = Integer.parseInt(st.nextToken());
            int y2 = Integer.parseInt(st.nextToken());
            int r2 = Integer.parseInt(st.nextToken());

            sb.append(tangentPoint(x1, y1, r1, x2, y2, r2)).append(&quot;\n&quot;);
        }
        System.out.println(sb);


    }

    private static int tangentPoint(int x1, int y1, int r1, int x2, int y2, int r2) {

        // 조규현과 백승환의 사이의 거리(두 점 사이의 거리)
        int dis = (int) (Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));

        /* 1. 두 원의 접점의 개수가 무한대일 때 =&gt; 두 원이 완전히 겹침*/
        if (x1 == x2 &amp;&amp; y2 == y1 &amp;&amp; r1 == r2) return -1;


        /* 2. 두 원의 접점의 개수가 0일 때 =&gt; 두 원이 만나지 X */

        // 2 - 1. 두 원이 서로 완전히 떨어져 있다 -&gt; 두 점 사이의 거리가 각 원의 반지름의 합보다 클 때 =&gt; ( (𝑥₂ - 𝑥₁)² + (𝑦₂ - 𝑦₁)² )½  &gt; 𝑟₁ + 𝑟₂ =&gt; (𝑥₂ - 𝑥₁)² + (𝑦₂ - 𝑦₁)²  &gt; (𝑟₁ + 𝑟₂)²
        else if (dis &gt; Math.pow(r1 + r2, 2)) return 0;

        // 2- 2. 한 원 안에 겹치지 않는 다른 원이 있다  -&gt; 반지름의 차가 두 원간의 중점 거리보다 크다 =&gt; ( (𝑥₂ - 𝑥₁)² + (𝑦₂ - 𝑦₁)² )½  &lt;  ∣𝑟₂ - 𝑟₁∣ =&gt; (𝑥₂ - 𝑥₁)² + (𝑦₂ - 𝑦₁)²  &lt;  (𝑟₂ - 𝑟₁)²
        else if (dis &lt; Math.pow(r1 - r2, 2)) return 0;


        /* 3. 두 원의 접점의 개수가 1일 때 =&gt; 두 원이 한 점에서 만난다*/

        // 3 - 1. 두 원이 외접한다 -&gt; 반지름의 합이 두 원간의 중점 거리와 같다 =&gt; ( (𝑥₂ - 𝑥₁)² + (𝑦₂ - 𝑦₁)² )½  =  𝑟₂ + 𝑟₁ =&gt;  (𝑥₂ - 𝑥₁)² + (𝑦₂ - 𝑦₁)²  =  (𝑟₂ + 𝑟₁)²
        else if (dis == Math.pow(r2 + r1, 2)) return 1;

        // 3- - 2. 두 원이 내접한다 -&gt; 두 반지름의 차가 두 좌표간의 차랑 같 =&gt; ( (𝑥₂ - 𝑥₁)² + (𝑦₂ - 𝑦₁)² )½  =  ∣𝑟₂ - 𝑟₁∣ =&gt; (𝑥₂ - 𝑥₁)² + (𝑦₂ - 𝑦₁)²  =  (𝑟₂ - 𝑟₁)²
        else if (dis == Math.pow(r2 - r1, 2)) return 1;

        /* 4. 두 원의 접점의 개수가 2일 때 =&gt; 두 원이 두 점에서 만난다.*/
        else return 2;
    }


}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준 11049] 행렬 곱셈 순서(Java, Python)]]></title>
            <link>https://velog.io/@kim_dg/%EB%B0%B1%EC%A4%80-11049-%ED%96%89%EB%A0%AC-%EA%B3%B1%EC%85%88-%EC%88%9C%EC%84%9CJava-Python</link>
            <guid>https://velog.io/@kim_dg/%EB%B0%B1%EC%A4%80-11049-%ED%96%89%EB%A0%AC-%EA%B3%B1%EC%85%88-%EC%88%9C%EC%84%9CJava-Python</guid>
            <pubDate>Wed, 26 Feb 2025 09:52:41 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kim_dg/post/c6f48d18-0e8b-4545-99b4-f28141fd01f6/image.png" alt=""></p>
<p><br><br></p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/39e12f3f-db21-4bff-b858-329a9a384414/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/a1b4c24c-6600-406e-84b6-58cb90afaebe/image.png" alt=""></p>
<br>
<br>


<h1 id="문제-해설">문제 해설</h1>
<p><strong>이 문제는 <span style = "color:red"><code>동적 프로그래밍(DP)</code></span>을 이용하여 <code>행렬 곱셈 연산의 최소 비용</code>을 구하는 문제이다.</strong></p>
<p>N개의 행렬이 주어졌을 때, 곱셈의 연산 순서를 최적화하여 최소 연산 횟수를 구하는 문제로 <strong>행렬 곱셈은 결합법칙(Associativity) 을 따르므로 괄호를 어디에 치느냐에 따라 연산량이 달라진다.</strong></p>
<p>예를 들어, 3개의 행렬이 있을 때:
(A × B) × C 와 A × (B × C) 의 연산량이 다를 수 있기 때문에 가장 적은 연산량을 가지는 최적의 행렬 곱셈 순서를 찾는 것이 목표다.</p>
<br>
<br>

<h1 id="정답-코드java-및-설명">정답 코드(Java) 및 설명</h1>
<br>


<pre><code class="language-java">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Main {

    public static void main(String[] args) throws IOException {
        // 입력을 빠르게 받기 위해 BufferedReader 사용
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        // 결과를 한 번에 출력하기 위해 StringBuilder 사용
        StringBuilder sb = new StringBuilder();

        // 행렬의 개수 입력 받기
        int N = Integer.parseInt(br.readLine());

        // 행렬의 크기를 저장할 배열 (N개의 행렬이므로, 크기를 N+1로 설정)
        int[][] matrix = new int[N + 1][2];

        // 최소 곱셈 연산 횟수를 저장할 DP 테이블 (N+1 x N+1 크기)
        int[][] dp = new int[N + 1][N + 1];

        // 행렬의 크기 입력 받기
        for (int i = 1; i &lt;= N; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            matrix[i][0] = Integer.parseInt(st.nextToken()); // 행렬의 행(row) 크기
            matrix[i][1] = Integer.parseInt(st.nextToken()); // 행렬의 열(column) 크기
        }

        // 행렬 체인 곱셈의 최소 연산 횟수를 구하는 DP 수행
        // len: 현재 고려하는 행렬 곱셈의 길이(몇 개의 행렬을 곱할 것인지) (2개부터 N개까지)
        for (int len = 2; len &lt;= N; len++) {
            // i: 시작 행렬의 인덱스
            for (int i = 1; i &lt;= N - len + 1; i++) {
                int j = i + len - 1; // 끝 행렬의 인덱스
                dp[i][j] = Integer.MAX_VALUE; // 최소값을 찾기 위해 초기값을 최댓값으로 설정

                // k: 행렬을 나누는 위치 (i &lt;= k &lt; j)
                for (int k = i; k &lt; j; k++) {
                    // 현재 분할 위치(k)에서 곱셈 연산 비용 계산
                    int cost = dp[i][k] + dp[k + 1][j] + (matrix[i][0] * matrix[k][1] * matrix[j][1]);
                    // 최소 연산 횟수 업데이트
                    dp[i][j] = Math.min(dp[i][j], cost);
                }
            }
        }

        // 최소 연산 횟수를 StringBuilder에 저장 후 출력
        sb.append(dp[1][N]);
        System.out.println(sb);
    }
}
</code></pre>
<br>


<pre><code class="language-java">int cost = dp[i][k] + dp[k + 1][j] + (matrix[i][0] * matrix[k][1] * matrix[j][1]);
</code></pre>
<ul>
<li><p>dp[i][k]: 행렬 A_i ~ 행렬 A_k 곱하는 최소 연산 횟수</p>
</li>
<li><p>dp[k+1][j]: 행렬 A_(k+1) ~ 행렬 A_j 곱하는 최소 연산 횟수</p>
</li>
<li><p>(matrix[i][0] <strong>X</strong>  matrix[k][1] <strong>X</strong> matrix[j][1]):</p>
</li>
<li><ul>
<li>matrix[i][0]: 시작 행렬의 행 크기</li>
</ul>
</li>
<li><ul>
<li>matrix[k][1]: k번째 행렬의 열 크기 → 곱셈의 중간 연결점</li>
</ul>
</li>
<li><ul>
<li>matrix[j][1]: j번째 행렬의 열 크기</li>
</ul>
</li>
<li><ul>
<li>이 값이 실제 곱셈 연산 횟수를 결정</li>
</ul>
</li>
</ul>
<br>
<br>

<h1 id="python-정답-코드">Python 정답 코드</h1>
<p>(※ 백준에서 채점할 때 Python3로는 시간 초과가 나기 때문에 PyPy3를 사용하여 채점하여야 한다.)</p>
<pre><code class="language-python">
import sys
input = sys.stdin.readline  # 입력 속도를 빠르게 하기 위해 sys.stdin.readline 사용
inf = float(&#39;inf&#39;)  # 무한대 값 설정 (최소값을 갱신하기 위해 사용)

N = int(input())  # 행렬의 개수 입력 받기

# 행렬의 크기를 저장할 배열 (N개의 행렬이므로 크기를 N+1로 설정)
matrix = [[0, 0] for _ in range(N + 1)]

# 최소 곱셈 연산 횟수를 저장할 DP 테이블 (N+1 x N+1 크기)
dp = [[0] * (N + 1) for _ in range(N + 1)]

# 행렬의 크기 입력 받기
for i in range(1, N + 1):
    a, b = map(int, input().split())  # 행렬의 행(a)과 열(b) 크기 입력 받기
    matrix[i][0] = a  # i번째 행렬의 행 크기 저장
    matrix[i][1] = b  # i번째 행렬의 열 크기 저장

# 행렬 체인 곱셈의 최소 연산 횟수를 구하는 DP 수행
# len: 현재 고려하는 행렬 곱셈의 길이 (2개부터 N개까지)
for len in range(2, N + 1):
    # i: 시작 행렬의 인덱스
    for i in range(1, N - len + 2):
        j = i + len - 1  # 끝 행렬의 인덱스 설정
        dp[i][j] = inf  # 최소값을 찾기 위해 초기값을 무한대로 설정

        # k: 행렬을 나누는 위치 (i &lt;= k &lt; j)
        for k in range(i, j):
            # 현재 분할 위치(k)에서 곱셈 연산 비용 계산
            cost = dp[i][k] + dp[k + 1][j] + (matrix[i][0] * matrix[k][1] * matrix[j][1])

            # 최소 연산 횟수 업데이트
            dp[i][j] = min(dp[i][j], cost)

# 최소 연산 횟수를 출력 (1번 행렬부터 N번 행렬까지 곱할 때의 최소 곱셈 연산 횟수)
print(dp[1][N])
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준 1987] 알파벳(Java, Python)]]></title>
            <link>https://velog.io/@kim_dg/%EB%B0%B1%EC%A4%80-1987-%EC%95%8C%ED%8C%8C%EB%B2%B3Java-Python</link>
            <guid>https://velog.io/@kim_dg/%EB%B0%B1%EC%A4%80-1987-%EC%95%8C%ED%8C%8C%EB%B2%B3Java-Python</guid>
            <pubDate>Thu, 30 Jan 2025 16:44:08 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kim_dg/post/a3e69d7c-39c4-4b80-bf39-5cbbcc0dd54b/image.png" alt=""></p>
<br>
<br>

<h1 id="문제">문제</h1>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/93dda217-5a61-41c1-ab8e-a773bb25d505/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/e9167771-c0f7-401d-a400-797e9552eaa5/image.png" alt=""></p>
<br>
<br>

<h1 id="해설">해설</h1>
<p><strong>이 문제는 그래프에서 여러 루트를 탐색하면서 최적의 루트를 찾아야 하는 문제이기 때문에 전형적인 <span style = "color:red"><code>DFS - 백트래킹</code></span> 문제이다.</strong></p>
<p>좌측 상단의 1행 1열에서 시작하여 DFS를 이용하여 보드를 탐색해나가는데 방문한 알파벳을 기록한다.
방문 알파벳을 기록하는 방식은 알파벳의 개수인 26만큼의 인덱스 개수를 갖고 있는 boolean 배열인 <code>visited</code>를 만들고 해당 위치의 보드에 있는 알파벳에서 <code>&#39;A&#39;</code>를 빼서 <code>visited</code> 배열의 0번 인덱스인 <code>A</code>를 기준으로 몇 번째 알파벳인지를 추출하여 그 숫자의 <code>visited</code> 배열의 인덱스를 방문한 알파벳이라는 표시를 위해 <code>True</code>로 전환하였다.</p>
<p>또한 탐색을 진행하면서 해당 위치를 몇 번째만에 갈 수 있는지를 나타내는 수치인 <code>depth</code>를 <code>dfs()</code>의 파라미터로 함께 넘겨주고 만약 조건(보드의 범위 안인 동시에 아직 방문하지 않은 알파벳)을 만족하면 기존 <code>depth</code>에 +1을 하여 재귀 호출하였다.
만약 파라미터로 넘어온 <code>depth</code>의 수치가 <code>정답(최대한 지날 수 있는 칸 수)</code>을 나타내는 기존 <code>maxDepth(최대 깊이)</code>보다 더 크다면 갱신해줘야 한다.</p>
<br>
이 때 가장 중요한 게 해당 칸에서 더 이상 조건을 만족하는 루트가 없어 각 재귀 함수가 종료되면서 

<pre><code class="language-java">visited[board[x][y] - &#39;A&#39;] = true;</code></pre>
<p>백트래킹 방식을 통해 해당 알파벳의 방문 기록을 해제해줘야 한다. </p>
<p>왜냐하면 방문 기록을 해제하지 않으면 더 이상 조건을 만족하는 루트가 없어 함수가 종료되어 이전 칸으로 거슬러 올라가서(더 최적의 루트를 찾기 위해 이전 루트는 없었던 셈 치고) 이전 칸에서 다른 루트를 탐색해보려고 시도할 때에 한 번 방문한 알파벳을 다시 탐색할 수 없기 때문에 특정 경로를 탐색한 후 다른 경로를 시도할 수 없으므로 maxDepth 값이 제대로 갱신되지 않아 최적의 해를 찾기 못하는 문제점이 발생한다.</p>
<br>

<blockquote>
<p><strong><span style = "color:red"><code>백트래킹(Backtracking)</code></span>은 문제 해결을 위한 알고리즘 설계 기법 중 하나로, 재귀적 탐색을 통해 문제의 해를 찾는 방식이다.</strong><br> 
<strong>주어진 문제에서 가능한 모든 해를 탐색하면서 조건을 만족하지 않는 경우에는 더 이상 진행하지 않고 이전 단계로 돌아가서 다른 가능한 경로를 탐색하는 방식이다. 이러한 방식을 &quot;되돌리기&quot;(backtracking)라고도 한다.</strong></p>
</blockquote>
<br>

<h2 id="1-백트래킹의-핵심-아이디어">1. 백트래킹의 핵심 아이디어</h2>
<p>탐색 트리를 구성하여 가능한 해를 모두 탐색한다.
각 단계에서 유효하지 않은 선택을 발견하면, 해당 경로를 더 이상 진행하지 않고 되돌아가(backtrack) 다른 경로를 시도한다.
(백트래킹은 <code>깊이 우선 탐색(DFS)</code>의 변형으로 볼 수 있다.)</p>
<br>

<h2 id="2-백트래킹의-일반적인-과정">2. 백트래킹의 일반적인 과정</h2>
<ol>
<li><p><strong>현재 상태에서 선택</strong>: 가능한 선택 중 하나를 선택하고, 그 선택을 다음 단계로 이어서 진행한다.</p>
</li>
<li><p><strong>유효성 검사</strong>: 현재 선택이 조건을 만족하는지 확인합니다. 조건을 만족하면 계속 진행하고, 만족하지 않으면 되돌아가서 다른 선택을 한다.</p>
</li>
<li><p><strong>되돌리기(backtrack)</strong>: 선택이 잘못된 경로였다고 판단되면, 이전 상태로 돌아가서 다른 선택을 시도한다.</p>
</li>
<li><p><strong>해를 찾으면 종료</strong>: 해가 발견되면 결과를 반환하고 종료한다.</p>
</li>
</ol>
<br>
<br>


<h1 id="정답-코드">정답 코드</h1>
<h2 id="java">Java</h2>
<pre><code class="language-java">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

public class Main {
    static int R, C, maxDepth;
    static char[][] board;
    static boolean[] visited = new boolean[26]; // A-Z 방문 여부 체크
    static int[] dx = {1, -1, 0, 0};
    static int[] dy = {0, 0, 1, -1};

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

        R = Integer.parseInt(st.nextToken());
        C = Integer.parseInt(st.nextToken());

        board = new char[R][C];

        for (int i = 0; i &lt; R; i++) {
            board[i] = br.readLine().toCharArray();
        }

        maxDepth = 0;
        dfs(0, 0, 1);
        sb.append(maxDepth);
        System.out.println(sb);
    }

    private static void dfs(int x, int y, int depth) {
        maxDepth = Math.max(maxDepth, depth); // 현재까지의 최대 깊이 갱신
        visited[board[x][y] - &#39;A&#39;] = true; // 현재 위치의 알파벳 방문 체크

        for (int i = 0; i &lt; 4; i++) {
            int nx = x + dx[i];
            int ny = y + dy[i];

            if (nx &gt;= 0 &amp;&amp; nx &lt; R &amp;&amp; ny &gt;= 0 &amp;&amp; ny &lt; C) {
                if (!visited[board[nx][ny] - &#39;A&#39;]) { // 새로운 알파벳이라면
                    dfs(nx, ny, depth + 1); // 다음 칸은 기존 깊이보다 + 1
                }
            }
        }

        visited[board[x][y] - &#39;A&#39;] = false; // 백트래킹 (방문 해제)
    }
}

</code></pre>
<br>


<h2 id="python">Python</h2>
<pre><code class="language-python">
import sys
input = sys.stdin.readline

R, C = map(int, input().split())

board = [list(sys.stdin.readline().strip()) for _ in range(R)]
visited = [False] * 26
max_depth = 0
dx = [1, -1, 0, 0]
dy = [0, 0, 1, -1]


def dfs(x, y, depth):
    global max_depth
    max_depth = max(max_depth, depth)
    visited[ord(board[x][y]) - ord(&#39;A&#39;)] = True

    for i in range(4):
        nx, ny = x + dx[i], y + dy[i]
        if 0 &lt;= nx &lt; R and 0 &lt;= ny &lt; C:
            if not visited[ord(board[nx][ny]) - ord(&#39;A&#39;)]:
                dfs(nx, ny, depth + 1)

    visited[ord(board[x][y]) - ord(&#39;A&#39;)] = False


dfs(0, 0, 1)
print(max_depth)



</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준 2606] 바이러스(Java, Python)]]></title>
            <link>https://velog.io/@kim_dg/%EB%B0%B1%EC%A4%80-2606-%EB%B0%94%EC%9D%B4%EB%9F%AC%EC%8A%A4Java-Python</link>
            <guid>https://velog.io/@kim_dg/%EB%B0%B1%EC%A4%80-2606-%EB%B0%94%EC%9D%B4%EB%9F%AC%EC%8A%A4Java-Python</guid>
            <pubDate>Tue, 28 Jan 2025 18:22:39 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kim_dg/post/9089fdb8-0135-4851-966a-f27ce14f8c78/image.png" alt=""></p>
<p>문제 링크: <a href="https://www.acmicpc.net/problem/2606">https://www.acmicpc.net/problem/2606</a></p>
<br>
<br>

<h1 id="문제">문제</h1>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/e38b68d0-d682-4fe8-9044-1dad779a8db4/image.png" alt=""></p>
<br>
<br>

<h1 id="해설">해설</h1>
<p>매우 전형적이고 기본적인 BFS, DFS를 통한 그래프 탐색 문제이다. BFS, DFS 이론을 차근차근 적용해나가면 누구나 풀 수 있는 쉬운 문제이다. BFS와 DFS에 대한 개념이 잘 설명되어 있는 포스팅은 워낙 많다보니 해당 포스팅에서는 BFS와 DFS의 개념은 따로 설명하지 않겠다.</p>
<p>BFS, DFS 두 가지 모든 방식으로 풀어보겠다.</p>
<br>
<br>


<h1 id="bfs">BFS</h1>
<h2 id="java">Java</h2>
<pre><code class="language-java">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;

public class Main {

    static int N, M;
    static List&lt;Integer&gt;[] graph;
    static boolean[] visited;
    static int count;

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

        N = Integer.parseInt(br.readLine()); // 컴퓨터(노드)의 수
        M = Integer.parseInt(br.readLine()); // 간선의 수

        // 그래프 초기화
        graph = new ArrayList[N + 1];
        for (int i = 1; i &lt;= N; i++) {
            graph[i] = new ArrayList&lt;&gt;(); // 1 ~ N번 인덱스에 각각 빈 리스트 할당
        }

        while (M-- &gt; 0) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            int a = Integer.parseInt(st.nextToken());
            int b = Integer.parseInt(st.nextToken());
            // 양방향(무방향) 그래프이므로 양쪽 모두 연결
            graph[a].add(b);
            graph[b].add(a);
        }

        // 방문 체크 배열 초기화
        visited = new boolean[N + 1];

        // 1번 컴퓨터가 감염시킨 컴퓨터 수(1번 노드와 연결된 노드 수) 초기화
        count = 0;

        // bfs 실행(bfs로 그래프 탐색해서 1번 노드와 몇 개의 노드가 연결되어 있는지 확인)
        sb.append(bfs(1));
        System.out.println(sb);

    }

    private static int bfs(int x) {
        Queue&lt;Integer&gt; queue = new ArrayDeque&lt;&gt;();
        queue.offer(x); // 큐에 1번 컴퓨터(노드) 추가
        visited[x] = true; // 1번 노드 방문 처리
        while (!queue.isEmpty()) {
            int cur = queue.poll();
            for (int next : graph[cur]) { // 현재 노드와 인접한 노드들 순차적으로 탐색
                if (!visited[next]) { // 해당 노드를 아직 방문 전이라면 로직 처리
                    count++;
                    visited[next] = true;
                    queue.offer(next);
                }
            }
        }
        return count;

    }
}
</code></pre>
<br>
<br>

<h2 id="python">Python</h2>
<pre><code class="language-python">import sys
from collections import deque
input = sys.stdin.readline


N = int(input())  # 노드 수
M = int(input())  # 간선 수

# 노드 연결 정보 담은 그래프 초기화
graph = [[] for _ in range(N + 1)]
for _ in range(M):
    a, b = map(int, input().split())
    graph[a].append(b)
    graph[b].append(a)

visited = [False] * (N + 1)  # 노드 방문 여부 확인 리스트
count = 0  # 1번 컴퓨터가 감염시키는 연결된 컴퓨터의 수


def bfs(x):
    &quot;&quot;&quot;bfs로 그래프 탐색해서 1번 노드와 몇 개의 노드가 연결되어 있는지 확인&quot;&quot;&quot;
    global count
    queue = deque([x])
    visited[x] = True
    while queue:
        cur = queue.popleft()
        for next in graph[cur]:
            if not visited[next]:
                count += 1
                visited[next] = True
                queue.append(next)

    return count


print(bfs(1))  # 1번 노드에서 시작해 bfs 실행
</code></pre>
<br>
<br>


<h1 id="dfs">DFS</h1>
<h2 id="java-1">Java</h2>
<pre><code class="language-java">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;

public class Main {

    static int N, M;
    static List&lt;Integer&gt;[] graph;
    static boolean[] visited;
    static int count;

    public static void main(String[] args) throws IOException {

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

        N = Integer.parseInt(br.readLine()); // 컴퓨터(노드)의 수
        M = Integer.parseInt(br.readLine()); // 간선의 수

        // 그래프 초기화
        graph = new ArrayList[N + 1];
        for (int i = 1; i &lt;= N; i++) {
            graph[i] = new ArrayList&lt;&gt;();
        }

        while (M-- &gt; 0) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            int a = Integer.parseInt(st.nextToken());
            int b = Integer.parseInt(st.nextToken());
            graph[a].add(b);
            graph[b].add(a);
        }

        // 방문 체크 배열 초기화
        visited = new boolean[N + 1];

        // 1번 컴퓨터가 감염시킨 컴퓨터 수 초기화
        count = 0;

        // dfs 실행(dfs로 그래프 탐색해서 1번 노드와 몇 개의 노드가 연결되어 있는지 확인)
        sb.append(dfs(1));
        System.out.println(sb);

    }

    private static int dfs(int x) {
        visited[x] = true; // 중복 안 되게 이미 카운트한 노드(컴퓨터) 방문 처리
        for (int next : graph[x]) { // 현재 노드와 인접 노드들 탐색
            if (!visited[next]) { // 아직 방문 X 노드라면
                count++; // 카운트
                dfs(next); // 재귀 호출
            }

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


<h2 id="python-1">Python</h2>
<pre><code class="language-python">import sys
from collections import deque
input = sys.stdin.readline

N = int(input())  # 노드 수
M = int(input())  # 간선 수

# 노드 연결 정보 담은 그래프 초기화
graph = [[] for _ in range(N + 1)]
for _ in range(M):
    a, b = map(int, input().split())
    graph[a].append(b)
    graph[b].append(a)

visited = [False] * (N + 1)  # 노드 방문 여부 확인 리스트
count = 0  # 1번 컴퓨터가 감염시키는 연결된 컴퓨터의 수


def dfs(x):
    &quot;&quot;&quot;dfs로 그래프 탐색해서 1번 노드와 몇 개의 노드가 연결되어 있는지 확인&quot;&quot;&quot;
    global count  # global로 count 전역 변수로 선언해 함수 내에서도 수정 가능
    visited[x] = True  # base case(해당 노드의 visited가 True일 시 재귀 탈출)
    for next in graph[x]:
        if not visited[next]:
            count += 1
            dfs(next)

    return count


print(dfs(1))


</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[백준 1389] 케빈 베이컨의 6단계 법칙(Java, Python)]]></title>
            <link>https://velog.io/@kim_dg/%EB%B0%B1%EC%A4%80-1389-%EC%BC%80%EB%B9%88-%EB%B2%A0%EC%9D%B4%EC%BB%A8%EC%9D%98-6%EB%8B%A8%EA%B3%84-%EB%B2%95%EC%B9%99Java-Python</link>
            <guid>https://velog.io/@kim_dg/%EB%B0%B1%EC%A4%80-1389-%EC%BC%80%EB%B9%88-%EB%B2%A0%EC%9D%B4%EC%BB%A8%EC%9D%98-6%EB%8B%A8%EA%B3%84-%EB%B2%95%EC%B9%99Java-Python</guid>
            <pubDate>Tue, 28 Jan 2025 15:52:52 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kim_dg/post/2e9b77c5-d074-4a72-8311-b0a7f5953718/image.png" alt=""></p>
<p>문제 링크: <a href="https://www.acmicpc.net/problem/1389">https://www.acmicpc.net/problem/1389</a></p>
<br>
<br>

<h1 id="문제">문제</h1>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/272509ff-603a-4c75-9ca9-76c3c18fd483/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/af4ae6fc-ddfa-4c3c-bd97-f834f33cdc2a/image.png" alt=""></p>
<br>
<br>


<h1 id="해설">해설</h1>
<p>시작점이 존재하지 않고, 모든 노드에서 다른 노드까지의 최단 거리를 구해야 하기 때문에 전형적인 플로이드-워셜 알고리즘이다. 또한 문제 특성상 시작 노드에서 현재 노드까지의 깊이(거리)를 기록 및 갱신하면서 그래프를 전체 탐색하여도 문제가 풀리기 때문에 BFS를 사용하여 문제를 풀어도 무방하다.</p>
<p>BFS, 플로이드-워셜 두 가지 방법으로 문제를 풀어보겠다.</p>
<br>
<br>


<h1 id="bfs-풀이">BFS 풀이</h1>
<h2 id="java">Java</h2>
<pre><code class="language-java">
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;

public class Main {
    static int N, M;
    static List&lt;Integer&gt;[] graph;

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

        N = Integer.parseInt(st.nextToken()); // 유저(노드)의 수
        M = Integer.parseInt(st.nextToken()); // 인맥(간선)의 수

        graph = new ArrayList[N + 1]; // 유저(노드)간의 관계가 저장된 인맥(그래프) - 인접 배열
        for (int i = 1; i &lt;= N; i++) {
            graph[i] = new ArrayList&lt;&gt;();
        }

        while (M-- &gt; 0) {
            st = new StringTokenizer(br.readLine());
            int a = Integer.parseInt(st.nextToken());
            int b = Integer.parseInt(st.nextToken());
            /*무방향 그래프이기 때문에 양쪽에 모두 저장*/
            graph[a].add(b);
            graph[b].add(a);
        }

        int minCount = Integer.MAX_VALUE; // 현재까지의 최소 케빈 베이컨의 수
        int minIdx = 0; // 현재까지의 최소 케빈 베이컨의 수를 가진 사람

        for (int i = 1; i &lt;= N; i++) {
            // bfs: 해당 유저(노드) i의 케빈 베이컨의 수를 구하기 위한 bfs
            int count = bfs(i);
            if (minCount &gt; count) { // 현재 유저 i의 케빈 베이컨이 현재의 최소 케빈 베이컨보다 더 작다면
                minCount = count; // 유저 i의 케빈 베이컨 수로 최소 베이컨 갱신
                minIdx = i; // 최소 유저를 i로 갱신
            }
        }
        sb.append(minIdx);
        System.out.println(sb);

    }

    private static int bfs(int user) {
        int count = 0; // 해당 유저(노드)의 현재까지의 케빈 베이컨 수 카운트
        int[] dist = new int[N + 1]; // 해당 유저에서 각 다른 유저들과의 단계(거리) 수를 저장하는 배열
        Arrays.fill(dist, -1); // 다른 모든 유저들간의 거리 -1로 초기화(-1이면 해당 유저과의 거리는 아직 계산하지 않은 거고 0이면 유저 본인)
        Queue&lt;Integer&gt; queue = new ArrayDeque&lt;&gt;(); // bfs 수행을 위한 큐

        queue.offer(user);
        dist[user] = 0; // 유저 자기 자신(현재 시작 노드)과의 거리는 0

        while (!queue.isEmpty()) {
            int cur = queue.poll();
            for (int next : graph[cur]) {
                if (dist[next] == -1) { // 아직 거리를 계산하지 않은(거리가 -1인) 인접 유저라면
                    dist[next] = dist[cur] + 1; // cur의 인접 노드인 next 노드는 cur 노드보다 시작 노드에서 +1 더 떨어져 있다 
                    count += dist[cur]; // 케빈 베이컨 수: 구하고자 하는 기준 노드에서 다른 모든 노드와의 거리의 총합이기 때문에 현재 살펴보는 노드와의 거리 누적합하기
                    queue.offer(next);
                }
            }
        }

        return count; // 파라미터 user 노드의 케빈 베이컨 수 최종 반환
    }


}
</code></pre>
<br>

<h2 id="python">Python</h2>
<pre><code class="language-python">import sys
from collections import deque

input = sys.stdin.readline

N, M = map(int, input().split())  # N: 유저(노드)의 수, M : 인맥(엣지)의 수

graph = [[] for _ in range(N + 1)]  # 노드간의 관계를 나타내는 인접 리스트
for _ in range(M):
    a, b = map(int, input().split())
    graph[a].append(b)
    graph[b].append(a)


def bfs(x):
    dist = [-1] * (N + 1)  # 현재 노드 x와 다른 노드들간의 거리를 나타내는 리스트(-1은 거리 미갱신, 0은 자기 자신)
    count = 0 # 현재 노드 x의 케빈 베이컨의 수
    queue = deque([x])
    dist[x] = 0  # 시작 노드 x의 거리 0으로 갱신
    while queue:
        cur = queue.popleft()  # x와의 거리를 구할 현재 노드
        for next in graph[cur]:  # cur 노드와의 인접 노드들
            if dist[next] == -1:  # 아직 해당 인접 노드와 거리 갱신이 되지 않았다면
                dist[next] = dist[cur] + 1  # 인접 노드는 cur 노드보다 시작 노드와 +1 더 멀다
                count += dist[cur]  # 케빈 베이컨 수를 구하기 위해 시작 노드와 cur 노드와의 거리 누적합
                queue.append(next)

    return count


min_count = sys.maxsize  # 현재의 최소 케빈 베이컨 수
min_idx = 0  # 현재의 최소 케빈 베이컨 수를 가진 유저

for i in range(1, N + 1):
    count = bfs(i)  # i 노드의 케빈 베이컨 수(다른 노드들과의 거리의 총합)을 구하기 위해 bfs 수행
    if min_count &gt; count:  # 만약 최소 케빈 베이컨 수가 나온다면 최소 케빈 베이컨 수, 최소 유저 갱신
        min_count = count
        min_idx = i

print(min_idx)
</code></pre>
<br>
<br>
<br>

<h1 id="플로이드-와샬">플로이드-와샬</h1>
<blockquote>
</blockquote>
<p><strong><span style = "color:red"><code>플로이드-워셜 알고리즘</code></span>은 그래프에서 모든 정점 간의 최단 경로를 구하는 알고리즘으로 다익스트라 알고리즘처럼 단일 출발점에서 다른 정점들까지의 최단 경로를 구하는 것이 아니라, 모든 쌍의 정점 간 최단 경로를 한 번의 실행으로 계산한다.</strong></p>
<p>플로이드-워셜은 동적 프로그래밍을 기반으로 동작한다.</p>
<p><strong>정 두 정점 u,v 사이의 최단 경로는, 다른 정점 k를 거쳐가는 경우와 거치지 않는 경우 중 더 짧은 경로로 갱신되는데 이를 수식으로 나타내면 다음과 같다:</strong></p>
<blockquote>
<p><strong>d[i][j]=min(d[i][j], d[i][k]+d[k][j])</strong></p>
</blockquote>
<br>

<h2 id="java-1">Java</h2>
<pre><code class="language-java">import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;

public class Main {
    static int N, M;
    static long[][] graph;
    static final int INF = Integer.MAX_VALUE; // 무한대(아직 최단거리 갱신하지 않은 노드들간의 초기값)

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

        N = Integer.parseInt(st.nextToken());
        M = Integer.parseInt(st.nextToken());

        graph = new long[N + 1][N + 1]; // 노드 연결 정보를 나타내는 2차원 인접 행렬
        for (int i = 1; i &lt;= N; i++) {
            for (int j = 1; j &lt;= N; j++) {
                if (i == j) graph[i][j] = 0;  // 자기 자신과의 거리는 0
                else graph[i][j] = INF; // 다른 노드과의 거리는 무한대로 초기화
            }
        }

        while (M-- &gt; 0) {
            st = new StringTokenizer(br.readLine());
            int a = Integer.parseInt(st.nextToken());
            int b = Integer.parseInt(st.nextToken());
            graph[a][b] = graph[b][a] = 1; // 연결되어 있는 인접 노드들끼리는 인접 행렬에 0으로 표시(무방향 그래프이기 때문에 양방향 표시)

        }

        long minCount = INF; // 현재의 최소 케빈 베이컨 수
        int minIdx = 0; // 현재의 최소 유저

        floydWarshall(); // 플로이드 워셜 실행해서 인접행렬 그래프 형태로 노드들간의 최단 거리 전부 갱신

        for (int i = 1; i &lt;= N; i++) {
            long count = Arrays.stream(graph[i]).sum(); // i 노드와 다른 모든 노드들간의 거리 총합으로 구한 케빈 베이컨 수(i행의 데이터 총합)
            if (minCount &gt; count) { // 현재의 최소 케빈 베이컨 수보다 i 노드의 케빈 베이컨이 더 작다면 갱신
                minCount = count;
                minIdx = i;
            }
        }

        sb.append(minIdx);
        System.out.println(sb);

    }

    private static void floydWarshall() {
        for (int k = 1; k &lt;= N; k++) { // k : 출발 지점과 도착 지점 사이의 경유지
            for (int i = 1; i &lt;= N; i++) { // i : 출발 지점
                for (int j = 1; j &lt;= N; j++) { // j : 도착 지점
//                    if (graph[i][j] &gt; graph[i][k] + graph[k][j]) graph[i][j] = graph[i][k] + graph[k][j];
                    graph[i][j] = Math.min(graph[i][j], graph[i][k] + graph[k][j]); // 기존 i와 j간의 최단 경로와 새로운 경유지인 k를 거친 i와 j간의 거리를 비교하여 최단 거리 갱신
                }
            }
        }

    }
}</code></pre>
<br>

<h2 id="python-1">Python</h2>
<pre><code class="language-python">import sys
input = sys.stdin.readline
inf = float(&#39;inf&#39;)

N, M = map(int, input().split())
graph = [[0 for _ in range(N + 1)] for _ in range(N + 1)]

for i in range(1, N + 1):
    for j in range(1, N + 1):
        if (i != j): graph[i][j] = inf

for _ in range(M):
    a, b = map(int, input().split())
    graph[a][b] = graph[b][a] = 1


def floyd_warshall():
    for k in range(1, N + 1):
        for i in range(1, N + 1):
            for j in range(1, N + 1):
                graph[i][j] = min(graph[i][j], graph[i][k] + graph[k][j])


floyd_warshall()

min_count = inf
min_idx = 0

for i in range(1, N + 1):
    count = sum(graph[i])
    if min_count &gt; count:
        min_count = count
        min_idx = i

print(min_idx)
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[동시성 제어(Java, 트랜잭션 격리 레벨, Locking(낙관 락, 비관 락, 분산 락))]]></title>
            <link>https://velog.io/@kim_dg/%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4Java-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EA%B2%A9%EB%A6%AC-%EB%A0%88%EB%B2%A8-Locking%EB%82%99%EA%B4%80-%EB%9D%BD-%EB%B9%84%EA%B4%80-%EB%9D%BD-%EB%B6%84%EC%82%B0-%EB%9D%BD</link>
            <guid>https://velog.io/@kim_dg/%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%A0%9C%EC%96%B4Java-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EA%B2%A9%EB%A6%AC-%EB%A0%88%EB%B2%A8-Locking%EB%82%99%EA%B4%80-%EB%9D%BD-%EB%B9%84%EA%B4%80-%EB%9D%BD-%EB%B6%84%EC%82%B0-%EB%9D%BD</guid>
            <pubDate>Wed, 11 Dec 2024 18:06:22 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kim_dg/post/b7a09206-9415-43c3-8815-950d0f6a768f/image.png" alt=""></p>
<p>트랜잭션 격리 수준에 대한 이해가 부족하다면 이전 포스팅인 <a href="https://velog.io/@kim_dg/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EA%B2%A9%EB%A6%AC-%EC%88%98%EC%A4%80%EA%B3%BC-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88">트랜잭션 격리 수준(Isolation)</a>을 먼저 읽고 오는 것을 권장합니다.</p>
<h1 id="동시성-제어concurrency-control란">동시성 제어(Concurrency Control)란</h1>
<blockquote>
<p><strong><span style = "color:red">동시성 제어(Concurrency Control)</span>: 동시성 제어는 데이터베이스와 같은 공유 자원에서 여러 사용자(프로세스와 쓰레드)가 동시에 작업을 수행할 때 데이터의 무결성을 유지하고 충돌을 방지하기 위한 메커니즘이다. 이를 통해 다수의 트랜잭션이 동시에 실행되더라도 데이터의 일관성과 정확성이 보장된다.</strong></p>
</blockquote>
<p>동시성 제어가 제대로 이루어지지 않으면 어떠한 문제점들이 발생할 수 있을까???</p>
<p>예를 들어 쇼핑몰에 주문이 들어와 결제가 이루어지고 해당 상품의 재고 수가 1 감소해야 하는 상황이라고 가정해보자. </p>
<p>하지만 만약 똑같은 상품에 5개의 주문들이 거의 동시에 들어오고 동시성 제어가 제대로 되어 있지 않다면 재고가 5가 감소해야 되는데 그보다 적은 3, 4개만 감소하는 상황이 발생하여 데이터의 정확성과 무결성이 무너지는 문제점이 발생할 수도 있다.</p>
<br>

<p><img src="https://velog.velcdn.com/images/kim_dg/post/0e212b6d-d0b7-4572-8f68-c2b5185b6acd/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/c850735c-cfb0-4f72-a5ba-c6035e10ec03/image.png" alt=""></p>
<p>(※ 프로세스는 하나의 프로그램, 쓰레드는 하나의 프로세스 안에서의 각 작업의 단위이다. 스프링에서는 각 요청에 하나의 쓰레드가 할당된다.)</p>
<br>

<p>쓰레드 1과 쓰레드 2와 쓰레드 3이 동시에 하나의 데이터를 수정하려고 접근한다고 생각해보자. 과연 어떤 쓰레드의 요청이 최종적으로 반영되게 될까?</p>
<p>정답은 <code>그때 그때 달라서 아무도 모른다</code>이다.</p>
<p>이러한 다수의 동시성이 일어나는 상황에서 생겨나는 현상이 바로 <code>Race Condition</code>이다.</p>
<br>

<blockquote>
<p><strong><span style = "color:red"><code>Race Condition(경쟁 상태)</code></span>: Race Condition은 두 개 이상의 프로세스나 스레드가 동시에 공유 자원에 접근하거나 작업을 수행할 때, 실행 순서와 타이밍에 따라 결과가 달라질 수 있는 상황을 말한다. 이는 병렬 처리나 멀티스레드 환경에서 흔히 발생하며, 예측하지 못한 결과를 초래할 수 있다.</strong></p>
</blockquote>
<ul>
<li><code>공유 자원 접근</code>: 여러 스레드나 프로세스가 하나의 자원을 동시에 읽거나 쓰려고 함.</li>
<li><code>실행 순서에 의존</code>: 어떤 스레드가 먼저 실행되느냐에 따라 결과가 달라짐.</li>
<li><code>예측 불가능한 결과</code>: 올바른 데이터 처리나 로직이 보장되지 않음.</li>
</ul>
<br>

<p>예시를 통해 살펴보자.</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/235cedd2-26ce-4115-8004-c0a0506260e0/image.png" alt="">
이미지를 보면서 <code>Race Condition</code>에 대해 이해해보자. User 1이 <code>counter</code> = 42라는 데이터를 먼저 읽어오고 아직 수정 및 커밋하지 않은 상태에서 User 2가 또 <code>counter</code> = 42를 읽어온 후 User 1이 <code>counter</code>를 1 증가시켜 43으로 만들고 커밋했지만 직후 User 2가 <code>conter</code>를 1 증가시키고 커밋하면 42라는 수치를 총 2번 증가시켰기 때문에 <code>counter</code>는 44가 되어야 정상이지만 유저 두 명이 각각 42를 1씩 증가시켰기 때문에 <code>counter</code>는 43으로 반영되는 문제가 발생한다.</p>
<br>

<p>이러한 문제를 방지하는 것이 바로 <strong><code>동시성 제어</code></strong>이다.</p>
<br>
<br>

<h1 id="race-condition의-구체적-유형">Race Condition의 구체적 유형</h1>
<p>동시성이 발생하는 상황에서는 <code>Race condition</code>으로 인하여 여러 문제가 발생할 수 있다.</p>
<blockquote>
<p><strong>Dirty Read (더티 리드)
한 트랜잭션이 커밋되지 않은 다른 트랜잭션의 데이터를 읽는 문제.</strong><br>
EX) 트랜잭션 A가 데이터를 변경했으나 아직 커밋하지 않았을 때, 트랜잭션 B가 그 데이터를 읽어 잘못된 값을 사용할 가능성.</p>
</blockquote>
<blockquote>
<p><strong>Non-Repeatable Read (비반복 읽기)
같은 트랜잭션 내에서 동일한 쿼리를 두 번 실행했을 때, 다른 트랜잭션이 데이터를 수정하거나 삭제하여 결과가 달라지는 문제.</strong><br>
EX) 트랜잭션 A가 데이터를 읽고 나중에 다시 읽을 때 값이 변경됨.</p>
</blockquote>
<blockquote>
<p><strong>Phantom Read (팬텀 리드)
같은 트랜잭션 내에서 동일한 쿼리를 실행했을 때, 다른 트랜잭션이 새로운 데이터를 삽입해 결과가 달라지는 문제.</strong><br>
EX) 트랜잭션 A가 조건에 맞는 데이터를 읽은 후 다시 쿼리했을 때 새로운 행이 추가됨.</p>
</blockquote>
<blockquote>
<p><strong>Lost Update (갱신 손실)
두 트랜잭션이 동시에 데이터를 수정하면 마지막에 실행된 트랜잭션의 결과로 이전 업데이트가 덮어씌워지는 문제.</strong><br>
EX) 사용자 A와 B가 동시에 잔액을 수정했을 때, 한쪽의 수정 사항이 유실.</p>
</blockquote>
<br>

<p>해당 문제들의 자세한 내용은 <a href="https://velog.io/@kim_dg/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EA%B2%A9%EB%A6%AC-%EC%88%98%EC%A4%80%EA%B3%BC-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88">트랜잭션 격리 수준(Isolation)</a>포스팅에 정리되어 있습니다.</p>
<br>
<br>

<h1 id="동시성-제어-방법">동시성 제어 방법</h1>
<br>

<h2 id="v1-java-언어-기능-활용">V1. Java 언어 기능 활용</h2>
<p>Java에는 동시성을 제어할 수 있는 <code>synchronized</code>, <code>ReentrantLock</code> 등의 여러 기능들이 존재한다.</p>
<p>그 중에서도 대표적으로 <code>synchronized</code>는 메서드나 블록에 적용하여 해당 코드가 동시에 여러 스레드에 의해 실행되지 않도록 보장하여 멀티스레드 환경에서 동기화된 코드를 만들기 위해 사용 방법이다.</p>
<p>실제 예시 코드를 통해 살펴보자.</p>
<pre><code class="language-java">public class SynchronizedExample {
    private int counter = 0;

    // 동기화된 메서드
    public synchronized void increment() {
        counter++;
    }

    public int getCounter() {
        return counter;
    }

    public static void main(String[] args) {
        SynchronizedExample example = new SynchronizedExample();

        // 두 개의 스레드 생성
        Thread thread1 = new Thread(() -&gt; {
            for (int i = 0; i &lt; 1000; i++) {
                example.increment();
            }
        });

        Thread thread2 = new Thread(() -&gt; {
            for (int i = 0; i &lt; 1000; i++) {
                example.increment();
            }
        });

        // 스레드 시작
        thread1.start();
        thread2.start();

        // 스레드가 종료될 때까지 대기
        try {
        // join() : 호출한 스레드(현재 실행 중인 스레드)가 특정 다른 스레드가 종료될 때까지 실행을 멈추고(차단하고) 기다리게 하는 동기화 기능
            thread1.join(); 
            thread2.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        // 최종 결과 출력
        System.out.println(&quot;Final counter value: &quot; + example.getCounter());
    }
}
</code></pre>
<p>increment() 메서드에 <code>synchronized</code> 키워드를 사용하여 한 번에 하나의 스레드만 이 메서드를 실행할 수 있도록 하였다. 이를 통해 다중 스레드가 동시에 counter 값을 변경하는 상황에서 데이터의 일관성을 보장한다.
한 번에 하나의 스레드만 메서드를 실행하였기 때문에 thread1과 thread2는 각각 1000번씩 increment() 메서드를 호출했다. 동기화 덕분에 경쟁 상태가 발생하지 않고, counter의 값은 제대로 2000이 출력된다.</p>
<p>하지만 <code>synchronized</code>는 많은 단점들이 존재한다. 동기화 방식이기 때문에 성능적으로 부족하고 교착 상태(Deadlock) 문제가 발생할 수 있으며, 이를 예방하기 위한 설계가 필요하다. </p>
<p>자바의 <code>synchronized</code>는 간단한 동기화 문제를 해결하는 데 유용하지만, 고성능 멀티스레드 프로그램을 위해서는 <code>java.util.concurrent</code> 패키지의 도구를 활용하는 것이 일반적이다.</p>
<br>

<p><strong>하지만 이러한 자바 기능을 이용한 동시성 제어는 그다지 권장되지 않는다.</strong> </p>
<p>synchronized는 공유 자원의 동시 접근을 제어하는 데 사용된다.
공유 자원이 서버에 존재하면 서버가 상태(Stateful)를 유지하게 되는데, 이는 Stateless 서버의 설계 원칙에 위배된다.</p>
<p>서버는 상태를 가지지 않고 최대한 <code>Statless</code>해야 한다. <code>Scale out</code> 등으로 인하여 서버가 여러 대 존재한다고 가정해보자. 이러면 여러 개의 프로세스가 생겨나는데 해당 방식으로는 스레드간에 동시성 처리는 가능하지만 프로세스간의 동시성 처리는 적용되지 않는다.</p>
<p>결론적으로 자바 기능만을 이용하여 동시성 처리를 하면 한 서버의 상태가 다른 서버와 동기화될 수 없으므로, 결과적으로 Lock이 동작하지 않게 된다. <strong>기본적으로 서버는 항상 확장 가능하게, stateless하게 설계해야 한다.</strong></p>
<blockquote>
<p><strong><span style = "color:red"><code>서버 statless</code></span>: 서버가 상태를 가지지 않는다(statless)는 것은 서버가 각 요청과 관련된 정보를 메모리 등 서버에는 저장하지 않고, 요청마다 독립적으로 처리한다는 의미이다.</strong><br>
stateless한 서버는 상태 관리가 필요한 경우 <code>DB</code>, <code>쿠키</code>, <code>HTTP 헤더</code>, <code>JWT</code>, <code>캐시 서버(Redis, Memcached 등)</code> 서버 외부의 시스템에 맡기고 결과만을 반환해야 하는데 <code>synchronized</code> 같은 자바 기능을 통해 동시성을 제어하면 서버의 메모리 변수에 데이터를 저장하고 여러 요청에서 이를 읽고 써야 하기 때문에 서버가 <code>stateful</code>하게 된다.</p>
</blockquote>
<br>
<br>

<h2 id="v2-트랜잭션-격리-레벨-활용transaction-isolation-level">V2. 트랜잭션 격리 레벨 활용(Transaction Isolation Level)</h2>
<p><strong>해당 포스팅에서는 트랜잭션 격리 수준에 대한 별도의 설명은 제외되었으니 트랜잭션 격리 수준에 대한 이해가 부족하다면 이전 포스팅인 <a href="https://velog.io/@kim_dg/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EA%B2%A9%EB%A6%AC-%EC%88%98%EC%A4%80%EA%B3%BC-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88"><code>트랜잭션 격리 수준(Isolation)</code></a>을 먼저 읽고 오는 것을 권장합니다.</strong></p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/2b44492c-c42e-46e9-b721-6a71fe0fe44e/image.png" alt=""></p>
<p>서버의 stateless를 유지하고 단순하고 쉽게 동시성 제어를 하고 싶다면 전체 데이터베이스를 대상으로 트랜잭션의 전역(global) 격리 수준을 <code>Serializable</code>로 설정하면 된다.</p>
<br>

<p>다만 이 방식에도 치명적인 단점들이 존재한다.</p>
<p>우선 트랜잭션을 직렬적으로 하나 하나 처리하면서 데이터 무결성과 안정성을 보장하는 방식이기 때문에 동시성으로 인한 문제는 일어나지 않지만 성능이 매우 나쁘기 때문에 프로그램의 전반적인 성능이 매우 열악할 것이다.
<br></p>
<p>그렇다면 전역적으로가 아니라 동시성 문제가 일어나는 트랜잭션에만 <code>@Transactional(isolation = Isolation.SERIALIZABLE)</code>을 걸면 되지 않을까???
<br></p>
<p>하지만 이 부분적으로 <code>Serializable</code>을 거는 방식에는 문제가 있다. 
왜냐하면, <code>Serializable</code>은 <code>WRITE</code>는 막지만, <code>READ</code>는 막지 못하기 때문이다. 단순 <code>SELECT</code>문으로 테이블을 읽어버리기 때문에 다른 트랜잭션이 여전히 오래된 데이터를 읽고 사용할 가능성이 있기 때문에 마찬가지로 동시성 제어가 되지 못한다.</p>
<br>

<p>또한 <code>Serializable</code>은 데이터 읽기 시 <code>공유 잠금(Shared Lock)</code>, 쓰기 시 <code>배타적 잠금(Exclusive Lock)</code>을 사용하는데 서로 다른 트랜잭션이 동일한 자원에 대해 상충하는 잠금을 요청하면 <code>Dead Lock(데드락)</code>이 발생할 가능성이 높아지고 <code>Serializable</code>에서는 데이터를 완벽히 일관되게 처리하기 위해 잠금을 많이 걸기 때문에 <code>Dead Lock</code>은 거의 필연적이다.</p>
<br>

<p><strong>이러한 여러 단점들 때문에 <code>Serializable</code>을 통한 동시성 제어는 별로 권장하지 않는 바이다.</strong></p>
<br>
<br>

<h2 id="v3-db-레벨에서의-lock-제어locking">V3. DB 레벨에서의 Lock 제어(Locking)</h2>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/be609cd8-c190-4a8a-8039-2f769ba385a8/image.png" alt=""></p>
<p>스프링에서는 <code>낙관적 락(Optimistic Lock)</code>, <code>비관적 락(Pessimistic Lock)</code> 두 가지 방식의 락(Lock)을 제공한다.</p>
<br>

<h3 id="1-낙관적-락optimistic-lock">1. 낙관적 락(Optimistic Lock)</h3>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/f5f102e5-a9d7-4f64-a44e-1b7c50a80a05/image.png" alt=""></p>
<blockquote>
<p><strong><span style = "color:red"><code>낙관적 락(Optimistic Lock)</code></span>: 낙관적 락은 데이터베이스나 애플리케이션에서 데이터의 충돌 가능성이 낮다고 가정하고, 락을 걸지 않고 동시성을 제어하는 방법이다. 데이터의 수정이 끝날 때 <code>version</code> 필드나 <code>타임 스탬프</code> 등을 통하여 데이터가 변경되었는지 확인하는 방식으로 충돌 여부를 검사하여 문제를 해결하는 방식이다.</strong></p>
</blockquote>
<p>장점으로는 데이터를 읽는 시점에는 아무 대응하지 않고 데이터를 쓰는 시점에 대응하기 때문에 다른 트랜잭션에 영향을 끼치지 않는다.</p>
<p><strong>즉, 낙관적 락은 데이터를 읽을 때는 락을 걸지 않고, 데이터를 업데이트할 때만 이전 데이터와 현재 데이터를 비교하여 충돌 여부를 판단한다. 이 방식은 데이터베이스의 성능 저하를 최소화하고, 동시성을 높이는 데 유리하다.</strong></p>
<p>*<em>하지만 충돌이 일어날 때마다 재시도를 해야 한다는 단점이 존재하기 때문에 충돌이 자주 발생하는 상황이면 반복되는 재시도들로 인하여 성능이 저하된다. *</em></p>
<br>


<p>이미지를 통하여 낙관적 락의 원리를 살펴보자.</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/c53ee674-c611-487f-bcbe-ade688c075e1/image.png" alt=""></p>
<p>트랜잭션 A에서<code>Course</code>의 <code>react</code>라는 데이터를 조회하고 있는데 비관적 락과 다르게 아무 락도 걸지 않았기 때문에 트랜잭션 B에서도 같은 데이터를 조회할 수 있다. 낙관적 락에서는 트랜잭션이 데이터를 조회할 때 해당 데이터의 version 값을 함께 읽어온다.</p>
<p>근데 트랜잭션 A보다 트랜잭션 B가 먼저 데이터를 <code>Java</code>로 수정해서 커밋을 시도하면 DB는 UPDATE 시점에 현재 DB에 저장된 version 값과 트랜잭션이 처음 조회했을 때의 version 값을 비교한다. 만약 두 값이 동일하면 다른 트랜잭션의 변경이 없었다고 판단해 업데이트를 수행하고 version을 증가시키며, 값이 다르면 이미 다른 트랜잭션에 의해 수정된 데이터로 판단하여 업데이트를 실패시킨다.</p>
<p>(※ version: 각 트랜잭션에 의해 데이터가 몇 번째로 수정되었나 확인하는 수치 정도로 생각하면 될 듯하다.)</p>
<p>이후 트랜잭션 A에서 데이터 수정 후 커밋하는 과정에서 DB에 있는 데이터의 현재 <strong><code>version</code></strong> 과 해당 트랜잭션이 들고 있는 수정한 데이터의 <strong><code>verson</code></strong> 을 비교해보니까 DB의 <code>version</code>은 트랜잭션 B에 의해 이미 수정되어 값이 2인데 반해 트랜잭션 A는 수정되기 이전의 데이터를 사용왔기 때문에 <code>version</code>이 1이어서 <code>version</code>의 값이 동일하지 않아 커밋에 실패하고 롤백되고 다시 <code>조회 - 수정 시도</code>하는 과정을 시도한다.</p>
<p>만약 충돌이 계속 발생한다면 (retry count를 따로 설정해주지 않는 이상은) 이러한 조회 - 수정 시도 과정이 반복이 될 것이고 애플리케이션의 성능 저하가 심화될 것이다.</p>
<br>

<p><strong>JPA에서 <code>낙관적 락</code>을 적용하고 싶다면 락을 걸고 싶은 대상의 엔티티에 <code>version</code>이라는 필드를 만들고 <code>@Version</code> 애노테이션을 붙여 활성화해줘야 한다.</strong></p>
<p><strong><code>@Version</code>을 사용하면 데이터를 조회하면 낙관적 락이 적용되어 <code>쓰기 작업</code>을 수행하면 내부적으로 <code>version</code>의 수치가 1씩 증가하도록 되어 있기 때문에 따로 증가 로직을 구현하면 안 된다.</strong></p>
<br>

<p><strong>[엔티티에 version 추가]</strong></p>
<pre><code class="language-java">import jakarta.persistence.*;

@Entity
public class Example {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private int quantity;

    @Version // 버전 필드로 낙관적 락을 사용
    private Long version;
}
</code></pre>
<br>

<p>이후 <code>데이터 충돌을 감지</code>하는 로직과 <code>충돌 시 재시도</code>를 처리하는 서비스 로직을 추가해야 한다. 
낙관적 락은 충돌이 발생할 경우 <code>OptimisticLockException</code>을 던지는데 서비스 로직에서 이를 감지하고 처리해줘야 한다.</p>
<p>상품의 재고를 감소시키는 예시 코드로 낙관적 락의 서비스 로직 처리를 살펴보자.</p>
<pre><code class="language-java">
// 충돌 감지 및 예외 처리 로직
@Transactional
public void updateStock(Long productId, int quantity) {
    try {
        Product product = productRepository.findById(productId)
                                           .orElseThrow(() -&gt; new IllegalArgumentException(&quot;Product not found&quot;));
        product.setStock(product.getStock() - quantity);

    } catch (OptimisticLockException e) {
        // 충돌 시 재시도 또는 사용자에게 알림
        throw new RuntimeException(&quot;Data conflict occurred. Please try again.&quot;);
    }
}

// 충돌 발생시 재시도 로직
public void updateWithRetry(Long productId, int quantity, int maxRetries) {
    int attempt = 0;

    while (attempt &lt; maxRetries) {
        try {
            updateStock(productId, quantity);
            break; // 성공 시 루프 종료
        } catch (OptimisticLockException e) {
            attempt++;
            if (attempt &gt;= maxRetries) {
                throw new RuntimeException(&quot;Failed to update after &quot; + maxRetries + &quot; attempts&quot;);
            }
        }
    }
}</code></pre>
<br>
<br>


<h3 id="2-비관적-락pessimistic-lock">2. 비관적 락(Pessimistic Lock)</h3>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/933a8343-8abe-4052-abd5-2d2057d904c5/image.png" alt=""></p>
<blockquote>
<p><strong><span style = "color:red">비관적 락(Pessimistic Lock)</span>: 비관적 락은 <code>&quot;충돌이 자주 발생하여 다른 트랜잭션이 내 데이터를 변경할 가능성이 높다&quot;</code>고 가정하고, 데이터 사용을 <span style = "color:red">시작</span>할 때 미리 잠금(Lock)을 걸어 충돌을 방지하는 방식이다.</strong><br>
<strong>데이터를 읽거나 쓰기 전에 잠금을 걸어 다른 트랜잭션이 접근하지 못하게 하고 트랜잭션이 끝난 후 잠금을 해제한다.</strong></p>
</blockquote>
<p>장점으로는 아예 시작 때 락을 걸어버리기 때문에 다른 트랜잭션들은 대기 중이어서 만약 충돌이 발생하더라도 추가적인 재시도 로직이 필요 없고 일정 시간 기다리거나 쉽게 에러 처리가 가능하여 충돌 비용이 낮고, 확실하게 직접적으로 락을 걸기 때문에 데이터 안정성이 높다는 점이 있다.</p>
<p>하지만 락으로 인하여 특정 자원에 동시에 트랜잭션이 접근하지 못하고 처리 중인 트랜잭션을 제외하고 나머지 트랜잭션들은 대기 상태에 있기 때문에 성능 저하가 발생한다.</p>
<br>

<p>이미지를 통해 비관적 락의 원리에 대해 천천히 살펴보자.</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/a49d7626-0f09-48c3-99e9-905a60ac00e0/image.png" alt=""></p>
<p><code>Course</code>라는 테이블의 <code>react</code>라는 같은 데이터에 접근하려는 트랜잭션 A, B가 있다고 가정해보자. 
트랜잭션 A에서 먼저 아무 트랜잭션도 사용하고 있지 않은 해당 데이터에 접근하다. 이 때 MySQL에서는 내부적으로 <strong><code>SELECT ... FOR UPDATE</code></strong> 쿼리를 발생시켜 <strong><code>배타 락(Exclusive Lock)</code></strong>을 걸고 특정 행을 다른 트랜잭션이 접근, 수정하거나 삭제하지 못하도록 잠금을 설정한다.</p>
<p>(상황에 따라서는 비관적 락에서 배타 락이 아니라 <strong><code>공유 락(Shared Lock)</code></strong>을 걸도록 설정할 수도 있다. 공유 락을 걸면 MySQL에서 내부적으로 <strong><code>SELECT FOR SHARE</code></strong> 쿼리가 발생한다.)</p>
<blockquote>
<p><strong>※ <span style = "color:red"><code>배타 락(Exclusvie Lock)</code>(<code>쓰기 락(Write Lock))</code></span>: 배타 락(Exclusive Lock)은 (쓰기 락(Write Lock)이라고도 한다) 데이터에 대한 <code>읽기와 쓰기 모두</code>를 차단하는 락의 한 종류이다.<br> 
배타 락이 걸린 데이터는 해당 트랜잭션이 끝나기 전까지 다른 트랜잭션이 읽거나 수정할 수 없다.</strong></p>
</blockquote>
<p><strong>[배타 락 쿼리 예시]</strong></p>
<pre><code class="language-sql">START TRANSACTION;

SELECT * FROM course WHERE course_id = 1 FOR UPDATE;

COMMIT;</code></pre>
<br>

<blockquote>
<p><strong><span style = "color:red"><code>공유 락(Shared Lock)(읽기 락(Read Lock))</code></span>: 공유 락은 다른 트랙재션도 데이터를 읽을 수는 있지만, 수정은 제한하는 락의 한 종류이다. 공유 락은 읽기 작업(Read) 간의 동시성을 허용하면서도 데이터의 일관성을 보장하는 데 사용된다.</strong><br>
<strong>즉, 공유 락이 걸린 데이터는 여러 트랜잭션이 동시에 읽을 수 있지만 데이터를 수정하려는 트랜잭션은 해당 공유 락이 해제될 때까지 대기해야 한다.</strong></p>
</blockquote>
<p><strong>[공유 락 쿼리 예시]</strong></p>
<pre><code class="language-sql">START TRANSACTION;

-- 공유 락을 걸고 데이터 읽기
SELECT * FROM course WHERE course_id = 1 LOCK IN SHARE MODE;

-- 다른 트랜잭션에서 동일 데이터를 읽는 것은 가능
-- 하지만 데이터 수정은 불가능

COMMIT;</code></pre>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/a49d7626-0f09-48c3-99e9-905a60ac00e0/image.png" alt=""></p>
<p>트랜잭션 A가 락을 걸고 작업 도중인데 트랜잭션 B가 해당 데이터에 접근하면 락이 걸려있기 때문에 트랜잭션 B는 트랜잭션 A가 작업을 끝내고 락을 해제할 때까지 대기 상태에 빠진다. 이후 락이 풀리면 트랜잭션 B도 <code>SELECT ... FOR UPDATE</code> 쿼리로 락을 걸고 데이터를 조회하고 로직을 수행한다.</p>
<br>

<p><code>JPA 비관적 락</code>을 사용하고 싶으면 트랜잭션을 필수로 사용하고 <strong><code>@Lock(LockModeType.PESSIMISTIC_WRITE)</code>/<code>@Lock(LockModeType.PESSIMISTIC_READ)</code></strong> 애노테이션을 명시해주거나 <code>EntityManager</code>의 옵션으로 <strong><code>LockModeType.PESSIMISTIC_WRITE</code>/<code>LockModeType.PESSIMISTIC_READ</code></strong>을 명시해주면 된다.</p>
<br>

<p><strong>[예시 1: <code>@Lock</code> 사용]</strong></p>
<pre><code class="language-java">public interface AccountRepository extends JpaRepository&lt;Account, Long&gt; {

    // @Lock(LockModeType.PESSIMISTIC_READ) // 공유 락 
    @Lock(LockModeType.PESSIMISTIC_WRITE) // 배타 락
    @Query(&quot;SELECT a FROM Account a WHERE a.id = :id&quot;)
    Optional&lt;Account&gt; findByIdWithLock(@Param(&quot;id&quot;) Long id);
}
</code></pre>
<br>

<p><strong>[예시 2: <code>EntityManager</code> 사용]</strong></p>
<pre><code class="language-java">@Service
public class AccountService {

    @PersistenceContext
    private EntityManager entityManager;

    @Transactional
    public Account findAccountWithLock(Long id) {
        // return entityManager.find(Account.class, id, LockModeType.PESSIMISTIC_WRITE); // 공유 락
        return entityManager.find(Account.class, id, LockModeType.PESSIMISTIC_WRITE); // 배타 락
    }
}
</code></pre>
<br>
<br>

<p>비관적 락을 사용하면 데이터 일관성을 확실하게 유지할 수 있고 충돌을 미리 사전에 방지하기 때문에 트랜잭션 실패가 현저히 줄어들어 트랜잭션 충돌로 인한 회복 비용을 아낄 수 있다는 장점이 존재한다.</p>
<p>하지만 데드 락이 발생할 위험성이 존재하며 이를 방지하거나 해결하기 위한 설계가 필요하고 성능이 저하된다는 단점이 존재한다.</p>
<blockquote>
<p><strong><span style = "color:red"><code>데드 락(Dead Lock)</code></span>:  데드 락은 컴퓨터 시스템에서 두 개 이상의 작업(프로세스, 스레드, 트랜잭션 등)이 서로 상대방이 점유한 자원을 무한정 기다리며 멈춰 서서 아무것도 완료되지 못하는 교착 상태이다. 이는 동시성 제어 환경에서 주로 발생하며, 각 프로세스나 트랜잭션이 필요한 자원을 얻지 못한 채 대기하면서 작업이 진행되지 않는 상태를 의미한다.</strong></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/75ea9639-c6d3-4655-8930-a2268ac076b0/image.png" alt=""></p>
<br>
<br>


<h3 id="낙관적-락-vs-비관적-락">낙관적 락 vs 비관적 락</h3>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/e1c939ab-8e9d-484e-8d1a-5db3a361758d/image.png" alt=""></p>
<br>
<br>



<h3 id="3-분산-락-by-redisson">3. 분산 락 (By Redisson)</h3>
<blockquote>
<p><strong><span style = "color:red"><code>분산 락</code></span>: 분산 락(Distributed Lock)은 여러 대의 서버나 프로세스가 동시에 하나의 공유 자원(데이터, 파일 등)에 접근하려 할 때, 한 번에 단 하나의 요청만 자원을 사용하도록 제어하여 데이터 무결성과 동시성 문제를 해결하는 기술이다. 단일 시스템에서는 단일 메모리 공간이나 OS 수준의 락으로 동시성 문제를 해결할 수 있지만, 분산 환경에서는 여러 노드에서 동시에 동일한 리소스를 사용할 수 있기 때문에 별도의 분산 락 메커니즘이 필요하다.</strong><br>
<strong>분산 환경에서 동일한 자원에 대한 경쟁 조건을 방지하고 데이터의 무결성을 유지하기 위해 사용된다. 일반적으로 Redis, ZooKeeper, MySQL 등의 외부 공용 저장소를 이용하여 구현한다.</strong></p>
</blockquote>
<p>분산락에 일반적으로 사용되는 <code>Redis</code>에는 Jedis, Lettuce, Redisson 등의 다양한 선택지들이 존재하지만 이번 포스팅에서는 현재 가장 각광받고 있는 <code>Redisson</code>를 통한 분산 락을 살펴보자.</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/7b3a121f-5c5c-4dc0-874f-9aea61c47493/image.png" alt=""></p>
<p><strong>한 서버가 특정 자원을 점유하여 사용하고 싶으면 락 관리자인 Redisson에게 해당 자원에 대한 락을 요청하는데, 만약 아무 서버도 점유하지 않은 상태라면 해당 자원에 대한 락을 획득하고 이미 다른 서버가 점유 중이라면 서버의 요청은 대기하거나 거절된다. 이후 자원을 점유하고 있던 서버의 작업이 끝나면 락은 다시 락 관리작인 Redisson에게 반납되고 Redisson은 대기하고 있던 다른 서버에 락을 부여한다.</strong></p>
<p><strong><code>Redisson</code>은 Redis의 **<code>Pub/sub</code></strong> 기능을 사용해서 Lock 획득을 재시도한다.**
*<em>만약 동시성 문제로 Lock 획득에 실패하면, Redisson은 특정 채널을 구독하고, Lock이 다시 획득할 수 있는 이벤트가 재발행되어 신호를 받았을 때, 다시 Lock 점유 권한을 부여하는 것이다. *</em></p>
<p>이는 Lock이 획득될 때까지 계속 setnx, setex과 같은 명령어를 이용해 지속적으로 Redis에게 락이 해제되었는지 요청을 보내는 방식인 <code>스핀락</code>을 사용하는 <code>Lettuce</code>보다 효율적이고, Redis 서버에도 부하를 덜 간다는 장점이 존재한다.</p>
<p>또한 <code>Lettuce</code>로 분산락을 사용하기 위해서는 <code>setnx</code>, <code>setex</code> 등을 이용해 분산락을 직접 구현해야 돼서 개발자가 직접 retry, timeout과 같은 기능을 구현해 주어야 한다는 번거로움이 있지만,
이에 비해 <code>Redisson</code>은 별도의 <code>Lock interface</code>를 지원한다. 락에 대해 타임아웃과 같은 설정을 지원하기에 락을 보다 안전하게 사용할 수 있다.</p>
<br>
<br>

<h4 id="redison-환경-설정">Redison 환경 설정</h4>
<br>

<p><strong><code>Redisson을 통한 분산 락</code></strong>을 사용하기 위해서는 우선 <code>build.gradle</code>에 <code>Redis</code>와 <code>Redisson</code> 의존성 주입이 필요하다.</p>
<p><strong>[build.gradle]</strong></p>
<pre><code class="language-java">// 필요한 의존성
implementation &#39;org.springframework.boot:spring-boot-starter-data-redis&#39; // 레디스 주입
implementation &#39;org.redisson:redisson-spring-boot-starter:3.37.0&#39; // 레디슨 주입</code></pre>
<blockquote>
<p>※ <strong><span style = "color:red"><code>Redisson</code></span>: Redisson은 Java 기반의 Redis 클라이언트 라이브러리로, Redis를 더 쉽게 활용할 수 있게 도와주는 고수준 API를 제공한다. Redis의 다양한 기능을 Java 애플리케이션에서 손쉽게 사용할 수 있도록 하며, 특히 분산 락, 분산 데이터 구조, 분산 캐시 등 고급 기능을 지원한다.</strong></p>
</blockquote>
<br>

<p><strong>[RedissonConfig]</strong></p>
<pre><code class="language-java">import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedissonConfig {


    @Value(&quot;${spring.redis.host}&quot;)
    private String redisHost;

    @Value(&quot;${spring.redis.port}&quot;)
    private int redisPort;    

    private static final String REDISSON_HOST_PREFIX = &quot;redis://&quot;;

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        // 단일 노드(기본 레디스 or 레디스 센티널) 사용시
          config.useSingleServer().setAddress(REDISSON_HOST_PREFIX + &quot;localhost:6379&quot;);

        // 레디스 클러스터 사용시 
       //  config.useClusterServers().addNodeAddress(&quot;redis://127.0.0.1:7000&quot;, &quot;redis://127.0.0.1:7001&quot;);

        return Redisson.create(config);
    }
}
</code></pre>
<p>이후 Redis가 기본 포트인 <code>localhost:6379</code>에 떠 있다는 가정 하에서 <code>RedissonClient</code>을 사용하기 위해 <code>Redisson</code> 설정 클래스를 만들고 Bean으로 등록해주자.</p>
<br>

<h4 id="분산-락에-aop-적용">분산 락에 AOP 적용</h4>
<p>분산락을 구현하는 데에는 다양한 방법이 존재한다. 그 중에서도 애노테이션을 이용하여 AOP를 적용하면 여러 이점을 챙길 수 있다.</p>
<ul>
<li>분산락 처리 로직을 비즈니스 로직이 오염되지 않게 분리해서 사용할 수 있다.</li>
<li>waitTime, leaseTime을 커스텀하게 지정이 가능하다.</li>
<li>락의 name에 대해 사용자로부터 손쉽게 커스텀하게 받아 처리할 수 있다.</li>
<li>추가 요구사항에 대해서 공통으로 관리할 수 있다.</li>
</ul>
<br>

<p><strong>[DistributedLock.java]</strong></p>
<pre><code class="language-java">@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {

    /**
     * 락의 이름
     */
    String key();

    /**
     * 락의 시간 단위
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;

    /**
     * 락을 기다리는 시간 (default - 5s)
     * 락 획득을 위해 waitTime 만큼 대기한다
     */
    long waitTime() default 5L;

    /**
     * 락 임대 시간 (default - 3s)
     * 락을 획득한 이후 leaseTime 이 지나면 락을 해제한다
     */
    long leaseTime() default 3L;
}</code></pre>
<p><code>DistributedLock</code> 애노테이션의 파라미터 중에서 key는 필수, 나머지 값들은 커스텀 하게 설정할 수 있다.</p>
<br>


<p><strong>[DistributedLockAop]</strong></p>
<pre><code class="language-java">/**
 * @DistributedLock 선언 시 수행되는 Aop class
 */
@Aspect //  특정 시점(메서드 호출 전후 등)에 실행될 로직을 정의
@Component
@RequiredArgsConstructor
@Sl4j
public class DistributedLockAop {

    // 모든 락 키에 &quot;LOCK:&quot;를 접두사로 추가하여 이름 충돌을 방지
    private static final String REDISSON_LOCK_PREFIX = &quot;LOCK:&quot;;

    // Redisson의 클라이언트 객체로, Redis와의 통신 및 락 관리를 수행
    private final RedissonClient redissonClient;

    // 트랜잭션 처리를 지원하는 커스텀 클래스. AOP로 트랜잭션 경계를 설정하는 역할
    private final AopForTransaction aopForTransaction;

    @Around(&quot;@annotation(com.kurly.rms.aop.DistributedLock)&quot;)
    public Object lock(final ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature(); // 호출된 메서드의 메타정보를 가져오기
        Method method = signature.getMethod(); // 메서드 정보를 추출
        DistributedLock distributedLock = method.getAnnotation(DistributedLock.class); // 해당 메서드에 선언된 @DistributedLock 어노테이션 정보를 가져오기. 이 어노테이션을 통해 락 키, 대기 시간, 임대 시간 등을 설정

        String key = REDISSON_LOCK_PREFIX + CustomSpringELParser.getDynamicValue(signature.getParameterNames(), joinPoint.getArgs(), distributedLock.key()); // 메서드의 매개변수 이름과 값, 그리고 @DistributedLock의 key() 속성을 기반으로 동적인 락 키를 생성

        RLock rLock = redissonClient.getLock(key);  // (1) 주어진 키에 대한 Redis 기반 락 객체(RLock)를 가져오기. 이 객체를 사용하여 락을 획득하거나 해제
        try {
            // available: 락 획득 여부
            boolean available = rLock.tryLock(distributedLock.waitTime(), distributedLock.leaseTime(), distributedLock.timeUnit());  // (2) 지정된 대기 시간(waitTime) 동안 락 시도. 락을 획득하면 임대 시간(leaseTime) 동안 유지
            if (!available) {
                return false; // 락을 획득하지 못한 경우 false를 반환
            }

            return aopForTransaction.proceed(joinPoint);  // (3) AOP를 사용해 원래 호출된 비즈니스 메서드를 실행
        } catch (InterruptedException e) {
            throw new InterruptedException();
        } finally {
            try {
                rLock.unlock();   // (4) 락을 해제
            } catch (IllegalMonitorStateException e) {
                   // 예외 발생 시 락이 이미 해제되었음을 로그로 기록
                log.info(&quot;Redisson Lock Already UnLock {} {}&quot;,
                        kv(&quot;serviceName&quot;, method.getName()),
                        kv(&quot;key&quot;, key)
                );
            }
        }
    }
}
</code></pre>
<p><code>DistributedLockAop</code>은 @DistributedLock 어노테이션 선언 시 수행되는 aop 클래스이다.
<code>@DistributedLock</code> 어노테이션의 파라미터 값을 가져와 분산락을 획득 시도하고 애노테이션이 선언된 메서드를 실행한다.</p>
<p>작동 방식과 순서는 다음과 같다.</p>
<ol>
<li>락의 이름으로 RLock 인스턴스를 가져온다.</li>
<li>정의된 waitTime까지 획득을 시도하고 락을 획득했으면 정의된 leaseTime이 지나면 잠금을 해제한다.</li>
<li>DistributedLock 어노테이션이 선언된 메서드를 별도의 트랜잭션으로 실행한다.</li>
<li>종료 시 무조건 락을 해제한다.</li>
</ol>
<p>해당 로직에서 신경써야 할 부분은 <code>CustomSpringELParser</code> 와 <code>AopForTransaction</code> 클래스이다.
이 두 클래스는 어떤 역할을 수행할까???</p>
<br>

<p><strong>[CustomSpringELParser]</strong></p>
<pre><code class="language-java">/**
 * Spring Expression Language Parser
 */
public class CustomSpringELParser {
    private CustomSpringELParser() {
    }

    public static Object getDynamicValue(String[] parameterNames, Object[] args, String key) {
        ExpressionParser parser = new SpelExpressionParser();
        StandardEvaluationContext context = new StandardEvaluationContext();

        for (int i = 0; i &lt; parameterNames.length; i++) {
            context.setVariable(parameterNames[i], args[i]);
        }

        return parser.parseExpression(key).getValue(context, Object.class);
    }
}</code></pre>
<p><code>CustomSpringELParser</code>는 전달받은 Lock의 이름을 <code>Spring Expression Language</code> 로 파싱하여 읽어온다.</p>
<blockquote>
<p>※ <strong><code>Spring Expression Language(SpEL)</code></strong>: SPEL(스프링 표현 언어)는 Spring Framework에서 제공하는 강력하고 유연한 표현 언어로, 런타임 시점에 객체 그래프의 속성, 메서드 호출, 배열/리스트/맵 조회, 조건문, 수학 연산 등을 처리할 수 있게 설계되었다.<br>
스프링의 빈 설정, AOP, 보안, 데이터 검증 등 다양한 영역에서 표현식을 동적으로 정의하거나 처리하는 데 사용된다.</p>
</blockquote>
<br>

<p><strong>[SpEL 사용 예시]</strong></p>
<pre><code class="language-java">
// (1)
@DistributedLock(key = &quot;#lockName&quot;)
public void shipment(String lockName) {
    ...
}

// (2)
@DistributedLock(key = &quot;#model.getName().concat(&#39;-&#39;).concat(#model.getShipmentOrderNumber())&quot;)
public void shipment(ShipmentModel model) {
    ...
}


ShipmentModel.java
public class ShipmentModel {
    private String name;
    private String shipmentNumber;

    public String getName() {
        return name;
    }

    public String getShipmentNumber() {
        return shipmentNumber;
    }

    ...
}</code></pre>
<p>Spring Expression Language를 사용하면 다음과 같이 Lock의 이름을 보다 자유롭게 전달할 수 있다.</p>
<br>


<p><strong>[AopForTransaction]</strong></p>
<pre><code class="language-java">/**
 * AOP에서 트랜잭션 분리를 위한 클래스
 */
@Component
public class AopForTransaction {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Object proceed(final ProceedingJoinPoint joinPoint) throws Throwable {
        return joinPoint.proceed();
    }
}
</code></pre>
<p><code>AopForTransactional</code> 클래스를 정의하여 락을 획득하고 비즈니스 로직이 정의된 메서드를 실행함으로써 @DistributedLock 이 선언된 메서드는 <code>Propagation.REQUIRES_NEW</code> 옵션을 지정해 부모 트랜잭션의 유무에 관계없이 별도의 트랜잭션으로 동작하게끔 설정 한 후에 반드시 트랜잭션 커밋 이후 락이 해제되게끔 처리하였다.</p>
<br>

<p>그렇다면 왜 트랜잭션 커밋 이후에 락이 해제되어야 하는 것일까???</p>
<br>

<p>간단하게 재고 감소 로직을 통한 예를 생각해보자.</p>
<p>만약 재고 감소 로직에서 커밋 이전에 락이 해제된다고 생각해보자. 재고가 10개인 상태에서 트랜잭션 A가 재고를 1개 감소시키고 커밋 이전에 락을 해제한다면 트랜잭션 B가 락 해제 신호를 받고 락을 획득하고 재고를 조회해보면 아직 커밋이 되지 않았기 때문에 재고가 10개로 나올 수 있다. 이 상태에서 트랜잭션 B에서도 재고를 1 감소시킨다면 개발자의 의도는 재고가 두 번 감소하였으니 재고가 8이 나와야 하지만 실제로는 트랜잭션 A, B가 각각 재고가 10인 상태에서 재고를 1 감소시켰으니 재고는 9가 되는 <code>race codntion</code> 현상이 발생한다.</p>
<br>

<p><strong>이렇듯 <span style = "color:red">트랜잭션을 커밋한 직후에 락을 해제</span>하는 이유는 포스팅 위쪽에서 자세히 설명한 동시성 환경에서의 <code>race condition</code>으로 인하여 <code>데이터 정합성</code>이 깨지는 것을 방지하여 <span style = "color:red"><code>데이터 정합성</code>을 보장하기 위해서이다</span>.</strong></p>
<blockquote>
<p>※ <span style = "color:red"><strong><code>데이터 정합성</code></span>: 데이터 정합성은 데이터가 일관성 있고 정확하게 유지되는 상태를 의미한다. 시스템 내의 데이터가 서로 모순되지 않고, 신뢰할 수 있는 상태를 보장하는 것이 목표이다.</strong></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[트랜잭션 격리 수준(Isolation)]]></title>
            <link>https://velog.io/@kim_dg/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EA%B2%A9%EB%A6%AC-%EC%88%98%EC%A4%80%EA%B3%BC-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88</link>
            <guid>https://velog.io/@kim_dg/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EA%B2%A9%EB%A6%AC-%EC%88%98%EC%A4%80%EA%B3%BC-%EB%8F%99%EC%8B%9C%EC%84%B1-%EC%9D%B4%EC%8A%88</guid>
            <pubDate>Sat, 30 Nov 2024 12:02:12 GMT</pubDate>
            <description><![CDATA[<h1 id="트랜잭션-격리-수준이란">트랜잭션 격리 수준이란</h1>
<blockquote>
</blockquote>
<p><strong><span style = "color:red">트랜잭션 격리 수준(Transaction Isolation Level)</span>: 트랜잭션 격리 수준은 데이터베이스에서 여러 트랜잭션이 동시에 실행될 때, 트랜잭션 간의 데이터 간섭을 방지하기 위한 동작 방식을 설정하는 것이다. 어떤 데이터에 접근을 허용하고 차단할지를 결정하여 데이터 무결성을 유지하려는 것에 목적이 있다.</strong></p>
<p><strong>트랜잭션 격리 수준은 낮은 수준일수록 성능은 좋아지지만 데이터 무결성 문제가 발생할 가능성이 높아지고, 높은 수준일수록 무결성은 잘 보장되지만 성능에 영향을 줄 수 있다.</strong></p>
<br>
<br>


<h1 id="트랜잭션-격리-수준-종류과-각-문제점들">트랜잭션 격리 수준 종류과 각 문제점들</h1>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/9727355f-f112-4c1e-958a-7300949e06a8/image.png" alt=""></p>
<p>개발을 하다 보면 상황에 따라, 비즈니스 요구 사항에 따라 트랜잭션에 어떤 격리 수준을 적용해야 하는지 판단해야 하는 상황이 발생한다.</p>
<p>예를 들어, 요금 결제같은 정말 중요한 작업에서는 가장 성능이 느리지만 가장 안전한 <code>SERIALIZABLE</code>을, 하지만 약간의 충돌이 발생해도 무방하지만 성능이 중요시되는 작업에서는 <code>READ_UNCOMMITED</code>을 고려해보는 등 어떤 격리 수준이 현재 상황에 적절하진 잘 판단할 수 있어야 한다.</p>
<p>이제 각각의 격리수준 종류에 대해 자세히 알아보자.</p>
<br>
<br>

<h2 id="1-read_uncommited--dirty-read">1. READ_UNCOMMITED &amp; Dirty Read</h2>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/e7829020-4763-4c48-a1b5-ec8eadf0189d/image.png" alt=""></p>
<blockquote>
<h3 id="span-style--colorredread-uncommitted-커밋되지-않은-읽기-허용spanbr"><span style = "color:red">Read Uncommitted (커밋되지 않은 읽기 허용)</span><br></h3>
</blockquote>
<ul>
<li><strong>특징: 가장 낮은 수준의 격리. 다른 트랜잭션이 아직 커밋하지 않은 데이터를 읽을 수 있음.</strong></li>
<li>문제 발생 가능: Dirty Read, Non-Repeatable Read, Phantom Read.</li>
<li>장점: 동시성이 가장 높아 성능이 좋음.</li>
<li>사용: 실시간 데이터 업데이트가 빈번하고 약간의 데이터 불일치가 허용되는 경우.</li>
</ul>
<p>실제 예시를 통해 알아보자.</p>
<br>

<p>&lt;왼쪽: 세션 1, 오른쪽: 세션 2&gt;
<img src="https://velog.velcdn.com/images/kim_dg/post/c6880c67-03e0-457f-a6ed-7e5185d30915/image.png" alt=""></p>
<p>MySQL에서 세션 1,2에서 두 개의 트랜잭션이 같은 DB에서 동시에 진행되는 상황을 가정해보자.</p>
<br>

<p>&lt;세션 1, 2&gt;</p>
<pre><code class="language-sql">SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;</code></pre>
<p>두 트랜잭션의 격리 수준을 모두 <code>READ UNCOMMITED</code>로 설정해주고</p>
<br>

<p>&lt;세션 1, 2&gt;</p>
<pre><code class="language-sql">SET autocommit = 0;</code></pre>
<p><code>AutoCommit</code>을 취소하여 두 개의 트랜잭션을 모두 수동관리 설정해주고</p>
<br>

<p>&lt;세션 1, 2&gt;</p>
<pre><code class="language-sql">START transaction;</code></pre>
<p>세션 1,2의 트랜잭션을 모두 시작해주자.
(트랜잭션 격리 수준을 변경하려면 트랜잭션을 명시적으로 시작해야 한다.</p>
<br>

<p>&lt;세션 1, 2&gt;</p>
<pre><code class="language-sql">SELECT * from course;</code></pre>
<p>DB의 수강 과목을 뜻하는 <code>course</code>라는 테이블을 조회해보면</p>
<br>

<p><img src="https://velog.velcdn.com/images/kim_dg/post/7e097937-2636-4892-8ef4-4fd8e9c97c7b/image.png" alt=""></p>
<p>현재 세션 1,2 모두 동일하게 <code>react</code>라는 과목이 존재함을 확인할 수 있다.</p>
<br>

<p>&lt;세션 1&gt;</p>
<pre><code class="language-sql">UPDATE course SET name=&quot;java&quot; WHERE course_id=1;</code></pre>
<p>이 때 세션 1에서 과목의 이름을 <code>java</code>로 바꾸고</p>
<br>


<p>&lt;세션 2&gt;</p>
<pre><code class="language-sql">SELECT * from course;</code></pre>
<p>세션 2에서 다시 테이블을 조회해보면</p>
<br>

<p><img src="https://velog.velcdn.com/images/kim_dg/post/e374ed24-c773-432f-84ad-8b67e4c0924e/image.png" alt=""></p>
<p><strong>세션 1에서 한 update 결과가 세션 2에서도 반영한 것을 확인할 수 있다.</strong></p>
<br>

<p><strong>즉, READ_UNCOMMITED을 사용하면 다른 트랜잭션에서 변경한 아직 커밋되지 않은 데이터도 실시간으로 읽을 수 있는 <code>Dirty Read(더티 리드)</code> 문제가 발생한다.</strong>
(가장 낮은 격리 수준이기 때문에 <code>Non-Repeatable Read</code>, <code>Phantom Read</code> 문제점도 발생하지만 해당 문제들은 이후의 다른 격리 수준에서 다루겠다.)</p>
<blockquote>
<p><strong><span style = "color:red">Dirty Read(더티 리드)</span>: Dirty Read는 데이터베이스 트랜잭션에서 발생할 수 있는 트랜잭션 격리 수준 관련 문제 중 하나로, 한 트랜잭션에서 아직 커밋되지 않은 데이터를 다른 트랜잭션이 실시간으로 읽는 현상을 말한다.</strong></p>
</blockquote>
<p>그럼 Dirty Read는 어떠한 문제를 일으킬 수 있을까???</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/70f840b0-1361-416e-873d-99b1e5473eb8/image.png" alt=""></p>
<p>&lt;세션 1&gt;</p>
<pre><code class="language-sql">rollback;</code></pre>
<p>만약 세션 1에서 롤백하여 다시 과목 이름을 <code>react</code>로 돌려버려도 세션 2에서는 다시 테이블을 재조회하지 않는 이상은 과목 이름이 <code>react</code>로 반영되어 있어 <strong><span style = "color:red">Dirty Read가 발생하면 데이터 무결성이 깨져 버리고 잘못된 데이터를 조작할 수 있다는 의도치 않은 문제점이 발생한다.</span></strong> </p>
<br>
<br>


<h2 id="2-read_commited--non-repeatable-read">2. READ_COMMITED &amp; Non-repeatable Read</h2>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/340c0323-619a-4ab3-9290-3158ebfebfef/image.png" alt=""></p>
<blockquote>
<h3 id="span-style--colorredread-committed-커밋된-읽기span"><strong><span style = "color:red">Read Committed (커밋된 읽기)</span></strong></h3>
<p><strong>특징: 다른 트랜잭션이 커밋한 데이터만 읽을 수 있음.</strong>
문제 발생 가능: Non-Repeatable Read, Phantom Read.
장점: Dirty Read 방지.
사용: 일반적인 트랜잭션에서 가장 널리 사용됨.</p>
</blockquote>
<p>※ <code>READ_COMMITED</code>는 대부분의 데이터베이스의 기본 격리 수준이지만 현재 포스팅에서 다루는 MySQL의 경우에는 기본 격리 수준이 <code>REPEATABLE_READ</code>이다.</p>
<p><strong><code>READ_COMMITED</code>는 <code>READ_UNCOMMITED</code>와는 달리 다른 트랜잭션에서 커밋하여 데이터베이스에 완전히 반영된 데이터만 읽을 수 있기 때문에 <code>Dirty Read</code>가 발생하지 않아 데이터 일관성 보장 수준이 더 높다.
다만 데이터 안정성이 더 높아졌기 때문에 <code>READ_UNCOMMITED</code>에 비해 성능과 동시성 처리가 더 뒤떨어진다는 단점도 존재한다.</strong></p>
<br>

<p>직접 실습을 통해 알아보자.</p>
<br>

<p>&lt;왼쪽: 세션 1, 오른쪽: 세션 2&gt;</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/c6880c67-03e0-457f-a6ed-7e5185d30915/image.png" alt=""></p>
<br>

<p>&lt;세션1, 2&gt; 격리수준(READ_COMMITTED) 설정</p>
<pre><code class="language-sql">SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;</code></pre>
<br>

<p>&lt;세션1, 2&gt; 트랜잭션 시작</p>
<pre><code class="language-sql">START transaction;</code></pre>
<br>

<p>&lt;세션1, 2&gt; course 데이터 조회</p>
<pre><code class="language-sql">SELECT * FROM course;</code></pre>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/2bfa5f37-95c9-40ce-8550-8a1911149362/image.png" alt=""></p>
<p>아직까지는 당연히 세션 1,2 모두 같은 데이터를 가지고 있다.</p>
<br>

<p>&lt;세션1&gt; course 데이터 변경</p>
<pre><code class="language-sql">UPDATE course SET name=&quot;java&quot; WHERE course_id=1;</code></pre>
<p>이제 세션 1에서 아까처럼 과목 이름을 <code>java</code>로 바꿔보자.</p>
<br>
<세션1, 2> course 데이터 조회 확인

<pre><code class="language-sql">SELECT * FROM course;</code></pre>
<p>변경 이후 세션 1,2에서 각각 <code>course</code> 테이블을 확인해보면</p>
<br>

<p>&lt;세션 1&gt;</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/993cf4fe-5adc-41e8-8f5e-c4bf55eafe4f/image.png" alt=""></p>
<p>데이터를 <code>update</code>로 직접 수정한 세션 1은 아직 커밋되지 않았더라도 당연히 데이터가 <code>java</code>로 변경된 것을 확인할 수 있지만</p>
<p>&lt;세션 2&gt;</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/4b7afc88-cd3d-444a-8e0d-eee3f71de19c/image.png" alt=""></p>
<p>데이터를 직접 변경하지 않은 세션 2에서는 데이터가 아직 커밋되었지 않기 때문에 변경되지 않은 기존의 데이터인 <code>react</code>가 조회되는 것을 확인할 수 있다.</p>
<br>

<p>&lt;세션1&gt; 커밋</p>
<pre><code class="language-sql">commit;</code></pre>
<p>여기서 데이터를 커밋하면</p>
<br>

<p>&lt;세션2&gt; course 데이터 조회</p>
<pre><code class="language-sql">SELECT * FROM course;</code></pre>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/bb06ce55-0c62-4bc5-bcbe-a88767d44f4e/image.png" alt=""></p>
<p>세션 2에서도 비로소 수정된 데이터를 조회할 수 있다.</p>
<br>

<p>하지만 세션 2에서 아직 트랜잭션이 끝나지도 않았는데 다른 트랜잭션들에 의해 데이터 값들이 변경되는 것이 정말 옳은 것일까???
<br></p>
<p><strong>read_committed 격리 수준에서는 한 트랜잭션이 데이터를 읽고 나서 같은 데이터를 다시 읽을 때, 다른 트랜잭션이 그 사이에 해당 데이터를 수정하거나 삭제할 수 있다. 그래서 두 번째 읽기 결과가 첫 번째 읽기 결과와 다를 수 있다.</strong></p>
<p>이로 인해  발생하는 문제가 바로 <code>Non-repeatable read</code> 문제이다.</p>
<blockquote>
<p><strong><span style = "color:red"><code>Non-repeatable read</code></span>: non-repeatable read 문제는 트랜잭션의 격리 수준에서 발생하는 문제 중 하나로, 트랜잭션이 동일한 데이터를 두 번 읽을 때 그 사이에 다른 트랜잭션에 의해 데이터가 변경될 수 있는 상황을 의미한다. 이로 인해 같은 쿼리를 두 번 실행할 때 서로 다른 결과가 나올 수 있다.</strong></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/70192efb-d3fe-466f-99c5-4efa34e9b8ea/image.png" alt=""></p>
<p>전처럼 트랜잭션 A 에서 롤백이 발생한 경우, 트랜잭션 B 가 영향을 받지는 않는다.</p>
<p>하지만 롤백이 아니라 트랜잭션 A 에서 데이터 변경이 성공적으로 커밋된다면, 트랜잭션 B는 이제는 새로이 변경된 데이터 값을 읽어 처리하기 때문에 예상치 못한 동작이 발생할 수 있다.</p>
<p><strong><span style = "color:red">같은 트랜잭션 내에서 동일한 데이터를 여러 번 읽을 때 그 값이 달라져 데이터 일관성을 보장할 수 없는 문제가 바로 <code>Non-repeatable Read</code> 문제다.</span></strong></p>
<br>
<br>

<h2 id="3-repeatable_read--phantom-read">3. REPEATABLE_READ &amp; Phantom Read</h2>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/57a380bd-5b51-4d5e-804d-9696eefb4ef1/image.png" alt=""></p>
<blockquote>
<ol start="3">
<li><h3 id="span-style--colorredrepeatable-readspan"><strong><span style = "color:red"><code>Repeatable Read</code></span></strong></h3>
</li>
</ol>
<p><strong>특징: 한 트랜잭션에서 같은 데이터를 여러 번 읽어도 결과가 동일하도록 보장.</strong>
문제 발생 가능: Phantom Read.
장점: Dirty Read, Non-Repeatable Read 방지.
사용: 높은 무결성이 요구되는 경우, 예를 들어 은행 계좌 잔액 계산.</p>
</blockquote>
<p><strong><code>Repeatable Read</code>는 말 그대로 특정 트랜잭션 내에서 트랜잭션이 끝날 때까지 데이터를 여러 번 읽어와도 처음에 읽어온 데이터를 보장시켜주는 격리 수준이다.</strong></p>
<br>

<p>직접 실습으로 해보자.</p>
<br>


<p>&lt;왼쪽: 세션 1, 오른쪽: 세션 2&gt;</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/c6880c67-03e0-457f-a6ed-7e5185d30915/image.png" alt=""></p>
<br>


<p>&lt;세션1, 2&gt; 격리수준(REPRETABLE READ) 설정</p>
<pre><code class="language-sql">SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;</code></pre>
<p>세션 1,2의 격리 수준을 모두 <code>REPEATABLE_READ</code>로 맞춰주고</p>
<br>

<p>&lt;세션1, 2&gt; 트랜잭션 시작</p>
<pre><code class="language-sql">START transaction;</code></pre>
<p>세션 1,2 각각 트랜잭션을 시작해보자.</p>
<br>

<p>&lt;세션1, 2&gt; 데이터조회</p>
<pre><code class="language-sql">SELECT * FROM course;</code></pre>
<p>트랜잭션을 시작하고 세션 1,2 에서 데이터를 조회하면</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/dde7370f-f879-40e8-8443-de9d2098fa07/image.png" alt=""></p>
<p>트랜잭션 시작 이후 별도의 데이터 수정이 없었기 때문에 세션 1,2 모두 초기값으로 course 테이블의 name 값이 방금 <code>READ_COMMITED</code>에서 바꾼대로 <code>java</code>인 것을 확인할 수 있을 것이다.</p>
<br>

<p>&lt;세션1&gt; 데이터 변경</p>
<pre><code class="language-sql">UPDATE course SET name=&quot;spring&quot; WHERE course_id=1;</code></pre>
<p>세션 1에서 course의 이름을 기존 <code>java</code>에서 <code>spring</code>으로 바꾼 후에</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/3e1eacdd-264b-451f-93a1-9cd69289f754/image.png" alt=""></p>
<br>


<p>&lt;세션2&gt; 데이터 조회</p>
<pre><code class="language-sql">SELECT * FROM course;</code></pre>
<p>세션 2의 트랜잭션에서 데이터를 다시 조회해보면 </p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/201e3a29-bd37-4772-b8dc-29863b21972b/image.png" alt=""></p>
<p>세션 1에서 커밋을 하지 않고 데이터를 수정한 상태와는 달리 세션 2에서는 데이터가 수정되지 않고 그대로 <code>java</code>인 것을 보아 <strong><code>Dirty Read</code> 문제가 발생하지 않은 것을 확인할 수 있다.</strong></p>
<br>

<p>&lt;세션 1&gt; 변경된 데이터 커밋</p>
<pre><code class="language-sql">commit;</code></pre>
<p>세션 1에서 커밋하여 바뀐 데이터를 DB에 영구반영시킨 뒤에</p>
<br>

<p>&lt;세션2&gt; 데이터 조회</p>
<pre><code class="language-sql">SELECT * FROM course;</code></pre>
<p>세션 2에서 다시 데이터를 조회해보면</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/201e3a29-bd37-4772-b8dc-29863b21972b/image.png" alt=""></p>
<p>최초 조회, 직전 조회와 마찬가지로 name이 <code>java</code> 그대로인 것을 확인할 수 있어 <strong><code>Non-repeatable read</code> 문제도 발생하지 않을 것을 확인할 수 있다.</strong></p>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/e0f29625-3220-4956-9e79-7d7a1b480d2f/image.png" alt=""></p>
<p><strong>마지막으로 요약하자면 <span style = "color:red"><code>REPEATABLE_READ</code>는 특정 트랜잭션에서 데이터들의 <code>값 수정</code>이 일어나고 그에 따른 커밋, 롤백이 일어난다 하더라도 변경 이전에 다른 트랜잭션이 <code>이미 조회한 데이터들의 값</code>에 한해서는 트랜잭션이 끝날 때까지 변하지 않고 보호되는 격리 수준이다.</span></strong></p>
<p><strong>하지만 <code>REPEATABLE_READ</code>에서는 <code>Phantom Read</code>라는 새로운 문제점이 발생한다.
※ 다만 <code>MySQL</code>, <code>MariaDB</code>에서는 <code>Gap Lock</code>이라는 자체 기술을 사용하여 문제점을 해결했기 때문에 Phantom Read가 발생하지 않는다.</strong></p>
<br>

<p>그렇다면 <strong><code>Phantom Read</code></strong>란 무엇일까???</p>
<br>

<p><img src="https://velog.velcdn.com/images/kim_dg/post/8bff277c-d490-4e03-a5cc-24564105886b/image.png" alt=""></p>
<blockquote>
<p><strong><span style = "color:red"><code>Phantom Read</code></span>: Phantom Read는 데이터베이스 트랜잭션에서 발생하는 일관성 문제 중 하나로, 트랜잭션이 동일한 조회 쿼리를 두 번 이상 실행할 때, 중간에 다른 트랜잭션이 데이터를 <code>삽입</code>하거나 <code>삭제</code>하여 첫 번째 조회에서는 없던 레코드(유령 데이터)가 두 번째 조회에서 나타나거나 사라지는 데이터 불일치 현상을 말한다.</strong></p>
</blockquote>
<p><strong>즉, <code>Phantom Read</code>는 특정 트랜잭션에서 데이터 반복 조회시 다른 트랜잭션에서의 <code>데이터 삽입, 삭제</code>로 인하여 <code>데이터 결과 집합</code>이 달라지는 문제이다.</strong></p>
<p><strong><code>Phantom Read</code>는 가장 엄격한 격리 수준인 <code>SERIALIZABLE</code>에서 해결이 가능하다.</strong></p>
<br>
<br>


<h2 id="4-serializable">4. SERIALIZABLE</h2>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/14f3dd87-d7ca-4be7-a477-a5df8354997c/image.png" alt=""></p>
<blockquote>
<h3 id="span-style--colorredserializable-span"><span style = "color:red">Serializable </span></h3>
<p><strong>특징: 가장 높은 수준의 격리. 트랜잭션이 순차적으로 실행되는 것처럼 동작.</strong>
문제 발생 가능: 없음 (Dirty Read, Non-Repeatable Read, Phantom Read 모두 방지).
단점: 동시성 처리 능력 저하, 성능 부담.
사용: 무결성이 최우선인 상황, 예: 금융 시스템, 주식 거래.</p>
</blockquote>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/302091a5-d66f-4344-9671-3cc79a0d5fb6/image.png" alt=""></p>
<p><strong><code>SERIALIZABLE</code>에서는 이전에 발생했던 모든 문제점들인 <code>Dirty Read</code>, <code>Non-Repeatable Read</code>, <code>Phantom Read</code>가 모두 방지된다.</strong></p>
<p>이미지처럼 <strong>모든 데이터베이스 작업이 직렬적으로 처리되어 모든 트랜잭션이 완전한 격리가 보장되어 다른 트랜잭션에 의한 간섭이 일어나지 않기 때문이다. 하지만 모든 작업이 직렬적으로 이루어지기 때문에 이로 인하여 동시성 처리 성능이 매우 떨어진다.</strong></p>
<br>

<p>실습을 통해 알아보자.</p>
<br>



<p>&lt;왼쪽: 세션 1, 오른쪽: 세션 2&gt;</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/c6880c67-03e0-457f-a6ed-7e5185d30915/image.png" alt=""></p>
<br>


<p>&lt;세션1, 2&gt; 격리수준(SERIALZABLE) 설정</p>
<pre><code class="language-sql">SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;</code></pre>
<p>세션 1,2 모두 격리수준을 <code>SERIALZABLE</code>로 설정하고</p>
<br>

<p>&lt;세션1, 2&gt; 트랜잭션 시작</p>
<pre><code class="language-sql">START transaction;</code></pre>
<p>각각 트랜잭션을 시작해보자.</p>
<br>


<p>&lt;세션1, 2&gt; 조회</p>
<pre><code class="language-sql">SELECT * FROM course;</code></pre>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/2cd497cd-30fc-4023-b607-54aa2681287c/image.png" alt=""></p>
<p>아직까지는 아무런 작업을  하지 않았기 때문에 모두 course의 name이 <code>spring</code>으로 조회된다.</p>
<br>

<p>&lt;세션1&gt; 데이터 수정</p>
<pre><code class="language-sql">UPDATE course SET name=&quot;java&quot; WHERE course_id=1;</code></pre>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/9adb88e8-34b5-4d4a-a0b3-a1046f0e5a50/image.png" alt=""></p>
<p>여기서 세션 1에서 이름을 <code>java</code>로 바꾸어보면 이전과 달리 <code>Query Ok</code> 문구가 뜨지 않고 데이터 쓰기 작업이 발생하는 즉시 멈춰버린다. 왜냐하면 다른 트랜잭션이 끝나기를 대기하고 있는 상황이기 때문이다. 
**<span style = "color:red"><code>Serializable</code> 격리 수준에서는 동일 데이터를 다루는 다른 트랜잭션이 완료되어야 특정 트랜잭션의 쓰기 작업이 완료된다.</span> **</p>
<br>

<p><img src="https://velog.velcdn.com/images/kim_dg/post/72b48117-5198-4b21-b59c-edf99a90400b/image.png" alt=""></p>
<pre><code class="language-sql">ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction</code></pre>
<p>일정 시간(기본 1분) 내에 다른 트랜잭션이 완료되지 않으면 위의 문구가 출력되고 다시 롤백되어버린다.</p>
<br>

<p>&lt;세션2&gt; 커밋</p>
<pre><code class="language-sql">commit;</code></pre>
<p>세션 2에서 커밋이나 롤백으로 트랜잭션을 끝내야 비로소</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/c9ef621b-d131-4392-a14e-a3e1b126c958/image.png" alt=""></p>
<p>세션 1에서 한 쓰기 작업이 반영되어 <code>Query Ok</code> 문구가 출력된다.</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/ccf3497e-4468-4f10-b1d0-fa2e37094aa5/image.png" alt=""></p>
<br>
<br>

<h1 id="스프링에서의-트랜잭션-격리-수준isolation-사용-방법">스프링에서의 트랜잭션 격리 수준(ISOLATION) 사용 방법</h1>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/dab8160f-816f-4b2e-bd1a-22d2d2e1353e/image.png" alt=""></p>
<p>스프링에서의 트랜잭션 <code>ISOLATION</code> 사용 방법은 매우 간단하다. 
어떤 격리 수준을 사용할 것인지 @Transactional 애노테이션 옆에 옵션을 넣어 <code>@Transactional(isolation = Isolation.READ_COMMITTED)</code> 설정해주면 된다.</p>
<br>

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





]]></description>
        </item>
        <item>
            <title><![CDATA[스프링에서의 트랜잭션(Transaction)과 Self-invocation 문제 해결 방법]]></title>
            <link>https://velog.io/@kim_dg/%EC%8A%A4%ED%94%84%EB%A7%81%EC%97%90%EC%84%9C%EC%9D%98-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98Transaction%EA%B3%BC-Self-invocation-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95</link>
            <guid>https://velog.io/@kim_dg/%EC%8A%A4%ED%94%84%EB%A7%81%EC%97%90%EC%84%9C%EC%9D%98-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98Transaction%EA%B3%BC-Self-invocation-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95</guid>
            <pubDate>Fri, 29 Nov 2024 08:50:56 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kim_dg/post/cb6fb11b-a78c-4708-9894-8eaa877d07c7/image.png" alt=""></p>
<h1 id="트랜잭션transaction이란">트랜잭션(Transaction)이란</h1>
<blockquote>
<p><strong><span style = "color:red">트랜잭션(Transaction)</span>: 트랜잭션은 데이터베이스에서 하나의 논리적인 작업 단위를 의미하며, 데이터의 일관성을 보장하기 위해 사용된다. 트랜잭션은 여러 작업을 하나로 묶어, 모두 성공하거나 모두 실패하도록 처리한다. 트랜잭션이 성공적으로 완료되면 커밋(Commit)하여 영구적으로 반영하고, 문제가 발생하면 롤백(Rollback) 하여 이전 상태로 되돌된다.</strong></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/4ae5ae7b-d84d-4ce8-af85-f8d9eaaca392/image.png" alt=""></p>
<p>트랜잭션의 일부인 작업 C가 실패하면 나머지 작업은 전부 성공했더라도 작업이 일어나기 전으로 돌아가는 롤백(Rollback)이 일어난다.</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/a8719d97-414f-4fce-b5e2-375361d566aa/image.png" alt="">
트랜잭션 안의 모든 작업이 성공해야지 트랜잭션이 성공하고 데이터베이스에 커밋되어서 반영된다.</p>
<br>
<br>



<h1 id="acid-원칙">ACID 원칙</h1>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/d0fe4fc9-1c39-4be4-9869-60e093a58bb0/image.png" alt=""></p>
<br>
<br>


<h1 id="스프링-트랜잭션-관리">스프링 트랜잭션 관리</h1>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/8ff1acd2-ba70-403b-9c3d-225c5f009579/image.png" alt=""></p>
<p>스프링에서는 룰백정책(rollbackFor), 전파(Propagation), 격리수준(Isolation)의 3가지 속성을 조작해서 트랜잭션의 범위와 세부적인 조작이 가능하다.</p>
<p>격리수준은 따로 차후 트랜잭션과 동시성 제어 포스팅에서 자세하게 다뤄 이번 포스팅에서는 제외할 것이다.</p>
<br>
<br>


<h2 id="트랜잭션-롤백-정책-설정">트랜잭션 롤백 정책 설정</h2>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/797ca238-7726-47dc-8a3d-0e282d34f715/image.png" alt=""></p>
<p><strong>스프링의 기본 롤백 정책은 <span style = "color:red"><code>RuntimeException(언체크 에외)</code></span>와 그 하위 예외가 발생했을 때에만 롤백이 진행되는 것이다.</strong>
<strong>즉, 이 말은 트랜잭션 내에서 <span style = "color:red"><code>Exception(체크 예외)</code></span>가 발생했더라도 해당 트랜잭션에서 롤백이 일어나지 않는다는 뜻이다.</strong></p>
<br>
하지만 체크 예외가 발생하더라도 롤백이 일어나야 하는 상황은 존재할 것이다.
이런 경우를 대비하여 스프링에서는 옵션 설정을 통해 유연하게 대처할 수 있게 하는 설정들이 존재한다.

<p><img src="https://velog.velcdn.com/images/kim_dg/post/4c82d12d-0f41-4f17-97b4-fd5c0305d0be/image.png" alt=""></p>
<p>이미지처럼 <strong>만약 특정 예외를 롤백 정책에 추가하고 싶다면 <span style = "color:red"><code>rollbackFor</code></span> 옵션을, 특정 예외를 롤백 정책에서 제거하고 싶다면 <span style = "color:red"><code>noRollbackFor</code></span> 옵션을 사용하면 된다.</strong></p>
<p>에시 코드를 통해 살펴보자.</p>
<pre><code class="language-jav">
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class RollbackService {

    private final CourseRepository courseRepository;

    /**
     * 기본 롤백 정책
     * 언체크예외(RuntimeException) 만 rollback 시킵니다.
     */
    @Transactional
    public void updateCourseWithDefaultRollback() throws Exception {
        // 1. 조회: init 수업 조회
        Course foundCourse = courseRepository.findById(1L)
                .orElseThrow(RuntimeException::new);

        // 2. 변경 후 저장: Java 수업 저장
        Course updateCourse = foundCourse.updateName(&quot;Java&quot;);
        Course savedCourse1 = courseRepository.save(updateCourse);


        // ---------언체크 예외 발생------------
        if (true) {
            throw new RuntimeException();
        }
        // --------------------------------

        // 3. 변경 후 저장: Mysql 수업 저장
        Course updateCourse2 = savedCourse1.updateName(&quot;MySQL&quot;);
        Course savedCourse2 = courseRepository.save(updateCourse2);
    }</code></pre>
<p>현재는 따로 롤백 관련해서 설정을 안 했기 때문에 RuntimeExcpetion시에만 롤백되는 기본 설정이다.
트랜잭션 안에서 Java로 수업 이름을 변경 후 RuntimeException이 일어났기 때문에 트랜잭션이 실패하여 다시 기존의 상태로 롤백되어 Course의 이름은 Java로 바뀌었던 것이 취소되고 그대로 &quot;init&quot;일 것이다.</p>
<br>

<p>그렇다면 RuntimeException이 아니라 Exception이 발생하면 어떻게 될까???</p>
<pre><code class="language-java">@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class RollbackService {

    private final CourseRepository courseRepository;

    /**
     * 기본 롤백 정책
     * 언체크예외(RuntimeException) 만 rollback 시킵니다.
     */
    @Transactional
    public void updateCourseWithDefaultRollback() throws Exception {
        // 1. 조회: init 수업 조회
        Course foundCourse = courseRepository.findById(1L)
                .orElseThrow(RuntimeException::new);

        // 2. 변경 후 저장: Java 수업 저장
        Course updateCourse = foundCourse.updateName(&quot;Java&quot;);
        Course savedCourse1 = courseRepository.save(updateCourse);

        // ------ 체크 예외 발생 ---------------
        if (true) {
            throw new Exception();
        }
        // ----------------------------------

        // 3. 변경 후 저장: Mysql 수업 저장
        Course updateCourse2 = savedCourse1.updateName(&quot;MySQL&quot;);
        Course savedCourse2 = courseRepository.save(updateCourse2);
    }
</code></pre>
<p>앞서 말했듯이 스프링의 기본 롤백 설정은 RuntimeExcpetion이 발생했을 때에만 롤백이 발생하기 때문에 Exception만 발생한 현재 코드에서는 롤백이 일어나지 않는다. 따라서 DB의 Course의 이름을 확인해보면 Exception이 발생하기 전인 &quot;Java&quot;로 바뀐 것을 확인할 수 있을 것이다.</p>
<br>

<p><img src="https://velog.velcdn.com/images/kim_dg/post/4106f61f-a174-48f6-8382-092fe09a1fe6/image.png" alt=""></p>
<p>이번에는 기본 롤백 정책을 바꿔서 <code>rollbackFor</code>를 통해 특정 예외를 롤백 정책 조건에 포함해보자.</p>
<br>

<pre><code class="language-java">    @Transactional(rollbackFor = 추가하고 싶은 예외.class)</code></pre>
<p><code>rollbackFor</code> 조건을 사용하기 위해서는 <code>@Transactional</code> 애노테이션에 <code>(rollbackFor = 추가하고 싶은 예외.class)</code>를 추가해주면 된다.</p>
<br>

<pre><code class="language-java"> /**
     * rollbackFor 활용
     * 기존 rollback 정책 조건에 특정 예외를 추가합니다.
     */
    @Transactional(rollbackFor = Exception.class)
    public void updateCourseWithRollbackFor() throws Exception {
        // 1. 조회: init 수업 조회
        Course foundCourse = courseRepository.findById(1L)
                .orElseThrow(RuntimeException::new);

        // 2. 변경 후 저장: Java 수업 저장
        Course updateCourse = foundCourse.updateName(&quot;Java&quot;);
        Course savedCourse1 = courseRepository.save(updateCourse);

        if (true) {
            throw new Exception();
        }

        // 3. 변경 후 저장: Mysql 수업 저장
        Course updateCourse2 = savedCourse1.updateName(&quot;MySQL&quot;);
        Course savedCourse2 = courseRepository.save(updateCourse2);
    }
</code></pre>
<p>기존에는 트랜잭션에서 롤백에 적용 안 되던 <code>Exception</code>을 추가한 후 Exception을 발생시키면 롤백이 발생하여 &quot;Java&quot;로 Course 이름이 바뀐 것이 취소되어 다시 Course 이름은 &quot;init&quot;으로 롤백될 것이다.</p>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/e0cf47b8-db0e-4f66-b5b8-6c9881498ab9/image.png" alt=""></p>
<p>이번에는 <code>noRollbackFor</code> 옵션을 사용하여 특정 예외를 롤백 정책에서 제외시켜 보자.</p>
<br>


<pre><code class="language-java">@Transactional(noRollbackFor = RuntimeException.class)</code></pre>
<p>이번에도 마찬가지로 @Transactional 애노테이션에 <code>(noRollbackFor = 제외하고 싶은 예외.class)</code> 조건을 추가해주면 적용된다.</p>
<br>

<pre><code class="language-java">  /**
     * noRollbackFor 활용
     */
    @Transactional(noRollbackFor = RuntimeException.class)
    public void updateCourseWithNoRollbackFor() throws Exception {
        // 1. 조회: init 수업 조회
        Course foundCourse = courseRepository.findById(1L)
                .orElseThrow(RuntimeException::new);

        // 2. 변경 후 저장: Java 수업 저장
        Course updateCourse = foundCourse.updateName(&quot;Java&quot;);
        Course savedCourse1 = courseRepository.save(updateCourse);

        if (true) {
            throw new RuntimeException();
        }

        // 3. 변경 후 저장: Mysql 수업 저장
        Course updateCourse2 = savedCourse1.updateName(&quot;MySQL&quot;);
        Course savedCourse2 = courseRepository.save(updateCourse2);
    }
</code></pre>
<p>기존에는 <code>RuntimeExcpetion</code>이 발생하면 롤백되었지만 현재는 RuntimeException을 롤백 정책에서 제외하였기 때문에 RuntimeException이 발생하였어도 롤백이 일어나지 않아 DB의 Course 이름을 &quot;Java&quot;로 변경하는 코드가 적용될 것이다.</p>
<br>
<br>
<br>


<h2 id="트랜잭션-전파---propagation">트랜잭션 전파 - Propagation</h2>
<blockquote>
<p><strong><span style = "color:red">트랜잭션 전파(Propagation)</span>: 트랜잭션 전파란 하나의 트랜잭션이 실행 중일 때, 그 내부에서 호출된 메서드가 어떻게 트랜잭션에 참여하거나 새로운 트랜잭션을 생성할지를 정의하는 동작 방식을 의미한다.</strong></p>
</blockquote>
<p>쉽게 말해 <strong>스프링에서 트랜잭션이 메서드간 겹칠 때 어떤 식으로 동작할지를 개발자가 결정해줄 수 있는 속성이다.</strong></p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/d8fa45f7-3706-4164-8c50-ccd332f83f50/image.png" alt="">
<strong>스프링의 트랜잭션 기본 전파 속성은 <span style = "color:red"><code>REQUIRED</code></span> 속성이다.<br>REQUIRED는 트랜잭션이 만약 존재한다면 기존 트랜잭션에 참여하고, 기존 트랜잭션이 없다면 트랜잭션을 새로 시작하는 가장 일반적으로 많이 사용되는 속성이다.</strong></p>
<p>예시를 통해 살펴보자.</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/5ea68f52-1225-4158-ab6a-6b9cfabc76d8/image.png" alt=""></p>
<p>&quot;processTransaction&quot;이라는 메서드에서 하나의 트랜잭션이 이미 진행 중인 상태에서 그 안에 또 별도로 트랜잭션을 가지고 있는 메서드인 &quot;methodA&quot;가 호출이 되는 상황이다.</p>
<p>둘 다 @Transactional을 통해 각각 트랜잭션이 진행되는 상황이기 때문에 트랜잭션이 겹쳤을 때 어떤 식으로 트랜잭션을 전파해야 하는지를 결정해야 한다.</p>
<p>현재는 별도의 설정을 하지 않은 <code>REQUIRED</code> 상태이기 때문에 부모 메서드와 자식 메서드의 두 트랜잭션이 하나의 트랜잭션으로 같이 묶여 사용될 것이다.</p>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/762a3166-db93-4291-bc7f-3cb0098040ad/image.png" alt=""></p>
<p>스프링에서는 트랜잭션의 전파 속성으로 모두 7가지의 속성을 제공한다. <strong>하지만 일반적으로 실무에서 가장 많이 사용되는 방식은 <span style = "color:red"><code>REQUIRED</code></span>, <span style = "color:red"><code>REQUIRES_NEW</code></span> 이 두 가지 방식이고 이 두가지 방식으로 거의 모든 케이스를 커버할 수 있기 때문에 이 두 가지 방식을 집중적으로 공부하고 나머지 속성들은 기본적인 특징 정도만 간단하게 살펴보고 넘어가는 것을 추천한다.</strong></p>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/60c96c11-9d56-4fac-9c8a-726777b69ea5/image.png" alt=""></p>
<p>모든 트랜잭션 전파의 유형을 간단하게 정리해보자면 다음과 같다.</p>
<blockquote>
<ol>
<li><strong><span style = "color:red"><code>REQUIRED(기본값)</code></span></strong>:</li>
</ol>
<p><strong>현재 트랜잭션이 있으면 이를 사용하고, 없으면 새로운 트랜잭션을 생성한다.
 가장 일반적으로 사용되는 방식으로, 부모 트랜잭션과 자식 트랜잭션이 같은 범위에서 관리된다.</strong><br>
 예시: 부모 메서드와 자식 메서드가 모두 REQUIRED라면 하나의 트랜잭션으로 묶인다.</p>
</blockquote>
<blockquote>
<ol start="2">
<li><strong><span style = "color:red"><code>REQUIRES_NEW</code></span></strong>: </li>
</ol>
<p><strong>항상 새로운 트랜잭션을 생성하며, 기존 트랜잭션은 일시 중단된다.
새로운 트랜잭션은 독립적으로 커밋되거나 롤백된다.</strong><br>
 예시: 기존 트랜잭션이 롤백되어도 새로운 트랜잭션은 영향을 받지 않는다.
 주로 독립적인 작업이 필요하거나, 부모 트랜잭션 실패 시에도 자식 트랜잭션이 영향을 받지 않도록 해야 할   때 사용한다.</p>
</blockquote>
<p> <img src="https://velog.velcdn.com/images/kim_dg/post/72865f1f-781e-4f7e-a15d-4b2b6dd26798/image.png" alt=""></p>
<p>하지만 <code>REQUIRES_NEW</code>를 사용할 때 주의해야할 점이 있다. 만약 <code>REQUIRES_NEW</code>가 다른 트랜잭션 메서드 안에서 호출되면 별도의 독립적인 트랜잭션을 생성하는 것은 맞지만 우리는 <strong>예외는 항상 상위 계층으로 전파된다</strong>는 사실을 망각해서는 안 된다. 
<strong>예외 전파 속성으로 인해 <code>REQUIRES_NEW</code>로 만들어진 자식 트랜잭션 안에서 예외가 발생되면 상위 계층인 부모 트랜잭션으로 예외가 전파되어 마찬가지로 예외가 발생하여 롤백되어서는 안되는 부모 트랜잭션도 자식 트랜잭션과 같이 롤백될 것이다</strong></p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/90b730e8-3be8-4c43-84ae-d1a6d370ebc5/image.png" alt=""></p>
<p><strong><span style = "color:red">이러한 이유로 <code>REQUIRES_NEW</code>를 사용할 때에는 부모 트랜잭션에서도 꼭 <code>REQUIRES_NEW</code>로 만들어진 자식 트랜잭션에서 발생할 수 있는 예외에 대한 예외 처리가 이루어져야 한다.</span></strong>
<br> </p>
<blockquote>
<ol start="3">
<li><strong><code>SUPPORTS</code></strong>: </li>
</ol>
<p><strong>현재 트랜잭션이 있으면 이를 사용하고, 없으면 트랜잭션 없이 실행된다. 주로 트랜잭션이 필수적이지 않은 경우에 사용한다.<br></strong>
 예시: 읽기 전용 작업에서 유용하다.</p>
</blockquote>
<blockquote>
<ol start="4">
<li><strong><code>NOT_SUPPORTED</code></strong>: </li>
</ol>
<p><strong>현재 트랜잭션이 있으면 이를 일시 중단하고 트랜잭션 없이 실행한다.</strong>
트랜잭션이 필요하지 않은 작업에서 주로 사용된다.<br>
  예시:로깅 또는 비트랜잭션 작업.</p>
</blockquote>
<blockquote>
<ol start="5">
<li><strong><code>MANDATORY</code></strong>:</li>
</ol>
<p>** 현재 트랜잭션이 반드시 존재해야 하며, 없으면 예외를 발생시킨다.**
 상위 트랜잭션 없이 동작할 수 없는 작업에 주로 사용된다.<br>
 예시: 항상 부모 트랜잭션에 의존해야 하는 경우.</p>
</blockquote>
<blockquote>
<ol start="6">
<li><strong><code>NEVER</code></strong>:</li>
</ol>
<p><strong>트랜잭션이 존재하면 예외를 발생시키고, 항상 트랜잭션 없이 실행한다.</strong><br>
 예시: 트랜잭션이 있으면 비효율적인 작업이 될 수 있는 경우.</p>
</blockquote>
<blockquote>
<ol start="7">
<li><strong><code>NESTED</code></strong>:</li>
</ol>
<p>** 현재 트랜잭션이 있으면 그 안에서 새로운 중첩 트랜잭션을 시작한다.
 중첩 트랜잭션은 부모 트랜잭션의 일부로 간주되며, 부모가 롤백되면 중첩 트랜잭션도 롤백된다.**
 독립적으로 커밋될 수 있지만, 부모 트랜잭션에 영향을 받을 수 있다.<br>
 예시: 동일 트랜잭션 안에서 세분화된 작업 단위가 필요한 경우.</p>
</blockquote>
<br>
<br>

<h1 id="트랜잭션-사용시-주의-사항">트랜잭션 사용시 주의 사항</h1>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/cc98b073-341b-4ef6-963c-df06324bc686/image.png" alt="">
수강신청을 하면 Course, Payment, PaymentLog가 생성되는 시나리오가 존재한다고 가정해보자. </p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/8b29fddf-d75d-4c8c-9d77-cc31ae850084/image.png" alt=""></p>
<pre><code class="language-java"> @Transactional
    public void processEnrollV1() {
        printActiveTransaction(&quot;::: EnrollmentService.processEnrollV1()&quot;);

        // 1. 수강 처리: Course 엔티티 생성
        processCourse();

        // 2. 결제 처리: Payment 엔티티 생성
        processPayment();

        // 3. 로그 처리: PaymentLog 로그 생성
        processLog();

    }

    public void processCourse() {
        printActiveTransaction(&quot;processCourse()&quot;);

        // 1. 저장 - Course 저장
        Course newCourse = Course.createNewCourse(&quot;newSpring&quot;);
        courseRepository.save(newCourse);
    }

    public void processPayment() {
        printActiveTransaction(&quot;processPayment()&quot;);

        // 1. 저장 - Payment 저장
        Payment newPayment = new Payment();
        paymentRepository.save(newPayment);
    }

    public void processLog() {
        printActiveTransaction(&quot;processLog()&quot;);

        // 1. 저장 - PaymentLog 저장
        PaymentLog newPaymentLog = new PaymentLog();
        logRepository.save(newPaymentLog);
    }
</code></pre>
<pre><code class="language-java">  /**
     * 트랜잭션 적용 여부를 확인합니다.
     */
    private void printActiveTransaction(String methodName) {
        boolean actualTransactionActive = TransactionSynchronizationManager.isActualTransactionActive();
        boolean isNewTx = TransactionAspectSupport.currentTransactionStatus().isNewTransaction();
        log.info(&quot;{} - isActive: {}, isNew: {}&quot;, methodName, actualTransactionActive, isNewTx);
    }
</code></pre>
<br>

<p><img src="https://velog.velcdn.com/images/kim_dg/post/8f3c6a80-84b2-45ca-b2d7-c35a946c92a0/image.png" alt=""></p>
<p>현재의 방식으로는 수강 신청 과정에서 Course 생성, Payment 생성되어서 수강 처리 후 결제가 완료되어도 PaymentLog 즉, 결제 기록 생성이 실패하면 수강 처리와 결제까지 전체 롤백되는 구조인데 이게 과연 옳은 것일까??</p>
<p>이 과정에서 중요한 것은 수강 처리와 결제 부분이지 결제 기록 생성 부분은 상대적으로 중요하지 않은 부분이기 때문에 로그 생성이 실패했다고 전체 롤백되는 것은 옳지 못할 것이다. 
따라서 PaymentLog 부분은 별도의 트랜잭션으로 따로 처리하는 과정이 필요하다.</p>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/e655e41f-5363-4e8b-9067-fc35e7289097/image.png" alt="">
Course 로직과 Payment 로직에는 <code>@Transactional(REQUIRED)</code>를 추가하여 기존의 트랜잭션과 합치고 따로 분리하고 싶은 Log 로직에는 <code>@Transactional(REQUIRES_NEW)</code>를 추가하여 별도의 트랜잭션으로 처리하면 이 문제를 해결할 수 있을까???</p>
<p>위의 과정을 코드로 나타내면 다음과 같다.</p>
<pre><code class="language-java">    @Transactional
    public void processEnrollV1() {
        printActiveTransaction(&quot;::: EnrollmentService.processEnrollV1()&quot;);

        // 1. 수강 처리: Course 엔티티 생성
        processCourse();

        // 2. 결제 처리: Payment 엔티티 생성
        processPayment();

        // 3. 로그 처리: PaymentLog 로그 생성
        try {
            processLog();
        } catch (Exception e) {
            log.info(&quot;로그 예외 발생&quot;);
        }

    }

    @Transactional
    public void processCourse() {
        printActiveTransaction(&quot;processCourse()&quot;);

        // 1. 저장 - Course 저장
        Course newCourse = Course.createNewCourse(&quot;newSpring&quot;);
        courseRepository.save(newCourse);
    }

    @Transactional
    public void processPayment() {
        printActiveTransaction(&quot;processPayment()&quot;);

        // 1. 저장 - Payment 저장
        Payment newPayment = new Payment();
        paymentRepository.save(newPayment);
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void processLog() {
        printActiveTransaction(&quot;processLog()&quot;);

        // 1. 저장 - PaymentLog 저장
        PaymentLog newPaymentLog = new PaymentLog();
        logRepository.save(newPaymentLog);
        throw new RuntimeException(&quot;로그 예외 발생&quot;);
    }
</code></pre>
<p>(트랜잭션 적용 여부 및 새 트랜잭션 여부)</p>
<pre><code class="language-java">EnrollmentService.processEnrollV1() - isActive: true, isNew: true
processCourse() - isActive: true, isNew: true
processPayment() - isActive: true, isNew: true
processLog() - isActive: true, isNew: true</code></pre>
<p>결과를 확인해보면 예상과 다르다. <code>processCourse()</code>와 <code>processPayment()</code>는 <code>REQUIRED</code>가 적용되어서 기존 부모 트랜잭션에 참여되었기 때문에 <code>isNew</code>가 false가 나와야 하는데 true가 나왔다.</p>
<p>또한 <code>processLog</code>는 <code>isNew</code>가 true로 나와 제대로 적용된 것처럼 보이지만 <code>RuntimeException</code>이 발생하였기 때문에 트랜잭션이 롤백되어 DB에 아무 데이터도 없어야 정상인데 </p>
<pre><code class="language-sql">select * from payment_log;</code></pre>
<p>로 테이블을 확인해보면 payment_log 데이터가 1개 생성된 것이 확인되기 때문에 <code>processLog()</code>의 트랜잭션에도 문제가 생겼음을 확인할 수 있다.</p>
<p>즉, 트랜잭션 걸어놓은 것들이 모두 정상적으로 작동을 하지 않아 Log도 꼬이고 데이터도 꼬인 상황이다...</p>
<br>

<p>왜 이러한 문제점이 발생했을까???</p>
<p><strong>그 이유는 바로 최상단 메서드인 processEnrollV1 에 달린 @Transactional 어노테이션만 적용이 되고 나머지 processEnrollV1에서 호출된 다른 메서드들의 @Transacional 어노테이션들은 적용되지 않았기 때문이다.</strong></p>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/238d34c9-3d17-4bb9-bd02-4ec9718d0265/image.png" alt="">
여기서 잠깐 AOP 방식인 <code>@Transactional</code>의 동작 원리를 살펴보자.
AOP를 적용하면 프록시 객체가 대상 객체를 감싸서 Bean으로 컨테이너에 등록되고 프록시 객체가 컨트롤러의 호출을 가로채기 때문에 AOP는 항상 프록시 객체에서 실행된다.</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/52d0a5a4-ed41-4cb6-86e7-6d01af7fa2ba/image.png" alt=""></p>
<p>AOP를 적용하게 되면 프록시 객체가 타겟 객체를 상속받고 프록시 객체 내부에서 이후 트랜잭션이 시작되고 타겟 객체의 비즈니스 로직이 수행되고 그 결과에 따라 트랜잭션 커밋/롤백이 이루어지는 것이 AOP의 내부 원리이다.</p>
<p>이제 이 AOP 동작 원리를 기억하고 다시 문제점을 살펴보자.</p>
<br>


<p><strong>메서드 안에서 호출된 메서드들에 적용된 @Transactional이 적용되지 않은 원인은 바로 <code>self-invocation</code> 문제가 발생했기 때문이다.</strong></p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/d5fa621b-c153-4bae-b8e8-ee12222a6266/image.png" alt=""></p>
<blockquote>
<p><strong><span style = "color:red">Self-Invocation 문제</span>: Self-Invocation 문제는 스프링의 AOP(Aspect-Oriented Programming) 기능을 사용할 때 발생하는 제약사항 중 하나이다. 이는 스프링의 프록시 기반 AOP 메커니즘에서 비롯되며, 클래스 내부의 메서드가 자신의 또 다른 메서드를 호출할 때, 호출된 메서드에 적용되어 있는 AOP가 작동하지 않는 문제를 의미한다.</strong></p>
</blockquote>
<p>현재 상황에서는 porcessEnrollV1()에 @Transactional이 붙어있기 때문에 컨트롤러에서 porcessEnrollV1()를 호출하면 porcessEnrollV1()의 프록시 객체가 컨트롤러의 호출을 가로채 호출되고 프록시 객체에서 트랜잭션 안에서 상속받은 실제의 porcessEnrollV1()를 호출한다.</p>
<p>여기까지는 문제가 없다. 하지만 실제 porcessEnrollV1()가 프록시 객체에 의해 호출되면 porcessEnrollV1() 내부에서 호출되는 processCourse(), processPayment(), processLog()에 붙어있는 @Transactional이 제대로 적용되려면 AOP 방식으로 각각의 프록시 객체에 의해 호출되어야 하는데 현재는 각 메서드들이 같은 클래스 내부에 있는 porcessEnrollV1()에 의해 직접, AOP로 감싸지지 않은 실제 메서드들이 호출되고 있기 때문에 세 메서드 모두 AOP가 아예 적용되지 않아 @Transactional이 적용되지 않는 self-invocation 문제가 발생했다.</p>
<br>

<p>그럼 어떻게 이러한 <code>self-invoction</code> 문제를 해결할 수 있을까???</p>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/98de7b89-2aa0-4c67-a842-605bc4379f34/image.png" alt=""></p>
<p><strong>해결 방법은 <span style = "color:red">AOP가 적용되어 있는(여기서는 @Transactional이 적용된) 모든 메서드들을 각각의 클래스로 분리하여 프록시 객체를 호출할 수 있는 구조로 만들어주면 된다.</span></strong></p>
<p>각각의 메서드들을 각각의 클래스에 분리하면 컨트롤러에 의해 processEnrollV1()의 프록시 객체가 실제 대상 객체인 EnrollmentService(processEnrollV1())을 호출하고 이제 그 안에서 별도의 클래스들로 분리된 processCourse(), processPayment(), processPaymentLog()를 호출하면 이전처럼 진짜 메서드들이 호출되는 것이 아니라 각 메서드들의 프록시 객체가 호출을 가로채 호출되고 AOP(@Transactional)이 적용된다.</p>
<br>
각 메서드들을 각 클래스들로 분리하면 코드는 다음과 같을 것이다.

<p><strong>[TxService1]</strong></p>
<pre><code class="language-java">@Slf4j
@Service
@RequiredArgsConstructor
public class TxService1 {

    private final CourseRepository courseRepository;

    @Transactional
    public void processCourse() {

        printActiveTransaction(&quot;processCourse()&quot;);
        // 1. 저장 - Course 저장
        Course newCourse = Course.createNewCourse(&quot;newSpring&quot;);
        courseRepository.save(newCourse);
    }

    private void printActiveTransaction(String methodName) {
        boolean actualTransactionActive = TransactionSynchronizationManager.isActualTransactionActive();
        boolean isNewTransaction = TransactionAspectSupport.currentTransactionStatus().isNewTransaction();
        log.info(&quot;-&gt; {}: isTxActive: {}, isNew: {}&quot;, methodName, actualTransactionActive, isNewTransaction);
    }
}</code></pre>
<br>

<p><strong>[TxService2]</strong></p>
<pre><code class="language-java">@Slf4j
@Service
@RequiredArgsConstructor
public class TxService2 {

    private final PaymentRepository paymentRepository;

    @Transactional
    public void processPayment() {
        printActiveTransaction(&quot;processPayment()&quot;);

        // 1. 저장 - Payment 저장
        Payment newPayment = new Payment();
        paymentRepository.save(newPayment);
    }

    private void printActiveTransaction(String methodName) {
        boolean actualTransactionActive = TransactionSynchronizationManager.isActualTransactionActive();
        boolean isNewTransaction = TransactionAspectSupport.currentTransactionStatus().isNewTransaction();
        log.info(&quot;-&gt; {}: isTxActive: {}, isNew: {}&quot;, methodName, actualTransactionActive, isNewTransaction);
    }
}</code></pre>
<br>

<p><strong>[TxService3]</strong></p>
<pre><code class="language-java">@Slf4j
@Service
@RequiredArgsConstructor
public class TxService3 {

    private final LogRepository paymentLogRepository;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void processLog() {
        printActiveTransaction(&quot;processLog()&quot;);

        // 1. 저장 - PaymentLog 저장
        PaymentLog newPaymentLog = new PaymentLog();
        paymentLogRepository.save(newPaymentLog);
        throw new RuntimeException();
    }

    private void printActiveTransaction(String methodName) {
        boolean actualTransactionActive = TransactionSynchronizationManager.isActualTransactionActive();
        boolean isNewTransaction = TransactionAspectSupport.currentTransactionStatus().isNewTransaction();
        log.info(&quot;-&gt; {}: isTxActive: {}, isNew: {}&quot;, methodName, actualTransactionActive, isNewTransaction);
    }
}</code></pre>
 <br>



<p><strong>[EnrollmentService]</strong></p>
<pre><code class="language-java">@Slf4j
@Service
@RequiredArgsConstructor
public class EnrollmentService {

    private final CourseRepository courseRepository;
    private final PaymentRepository paymentRepository;
    private final LogRepository logRepository;

    private final TxService1 txService1; // processCourse 클래스 분리
    private final TxService2 txService2; // processPayment 클래스 분리
    private final TxService3 txService3; // processPaymentLog 클래스 분리

    /**
     * 문제 해결
     */
    @Transactional
    public void processEnrollV2() {
        printActiveTransaction(&quot;::: processEnroll()&quot;);

        // 1. 수강 처리
        txService1.processCourse();

        // 2. 결제 처리
        txService2.processPayment();

        // 3. 로그처리
        try {
            txService3.processLog();
        } catch (Exception e) {
            log.info(&quot;::: 로그 처리 실패&quot;);
        }
    }
}</code></pre>
<br>


<p><strong>[트랜잭션 적용 여부 및 새 트랜잭션 여부]</strong></p>
<pre><code class="language-java">EnrollmentService.processEnrollV2() - isActive: true, isNew: true
processCourse() - isActive: true, isNew: false
processPayment() - isActive: true, isNew: false
processLog() - isActive: true, isNew: true</code></pre>
<p><code>procecessEnrollV2</code>는 새로운 트랜잭션을 시작하는 거니까 <code>true</code>가 맞고 <code>processCourse()</code>와 <code>processPayment</code>는 <code>REQUIRED</code>이기 때문에 기존의 트랜잭션에 합류하기 때문에 <code>isNew</code> 속성이 <code>false</code>가 맞다. 또한 <code>processLog</code>는 <code>REQUIRES_NEW</code>이기 때문에 새로운 트랜잭션을 발생시켜 <code>isNEw</code>가 <code>true</code>이다.</p>
<p>이를 통해 <code>self-invocation</code> 문제가 잘 해결되었음을 확인할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[매칭 서비스 고도화 트러블 슈팅]]></title>
            <link>https://velog.io/@kim_dg/%EB%A7%A4%EC%B9%AD-%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B3%A0%EB%8F%84%ED%99%94-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</link>
            <guid>https://velog.io/@kim_dg/%EB%A7%A4%EC%B9%AD-%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B3%A0%EB%8F%84%ED%99%94-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85</guid>
            <pubDate>Wed, 20 Nov 2024 02:23:41 GMT</pubDate>
            <description><![CDATA[<h1 id="매칭-서비스-개발-배경">매칭 서비스 개발 배경</h1>
<p>저희가 만든 서비스는 타인과 관심사나 취미를 함께 공유하고 싶은 사람들을 겨냥하여 개발되었기 때문에 최소한의 노력으로 타인과 어울리고 싶은 서비스 사용자들을 위해 지역, 관심사 등을 기준으로 매칭되는 1:1 유저 매칭 서비스를 기획하고 개발하게 되었습니다.</p>
<h1 id="problem-1--서버-과부하">Problem 1 : 서버 과부하</h1>
<p>단기간에 수많은 유저가 몰릴 수 있는 매칭 서비스의 특성상 초당 10000명의 유저가 매칭 서비스에 동시에 몰려도 충분히 버틸 수 있는 서비스를 만드는 것이 기존의 설계였지만 실제로 10초에 걸쳐 점진적으로 10000개의 매칭 요청을 전송한 후 서버의 상태를 프로메테우스와 그라파나를 연동하여 모니터링해봤더니</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/bb019057-9280-46aa-8ec3-19ca2f4c50e9/image.png" alt=""></p>
<p>이미 해당 매칭 요청들을 처리하는 데에만 서버의 CPU의 94.8%가 사용되어 서버 과부하가 매우 심하여 실제 서비스였다면 서버의 장애로 이어져 매칭 서비스를 포함한 모든 서비스가 확실하게 셧다운되었을 심각한 상황이기 때문에 시급한 대응이 필요한 상황이라고 판단했습니다.</p>
<br>

<h1 id="solution-1--서버-부하-분산">Solution 1 : 서버 부하 분산</h1>
<p>이러한 매칭 트래픽 폭증으로 인한 서버 과부하 문제를 해결하기 위해 저희는 현재와 같은 단일 서버구조만으로는 한계가 있다고 판단하고 오로지 매칭 비즈니스 로직만을 따로 처리하는 매칭 서버를 증설하여 서버의 온전한 리소스를 매칭에 사용함으로써 매칭 서비스에 대한 대량의 트래픽에 대비하고 다른 모든 기능들이 존재하는 메인 서버의 부하 매칭 서버로 분산시키기로 결정하였습니다.</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/626dec8c-7cae-4dee-835f-b95fc4b993dd/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/7818a2dd-504e-4ace-a00f-cf583fa4632a/image.png" alt=""></p>
<p>매칭 서비스를 별도의 서버로 분리한 뒤 똑같은 조건에서 이전과 같은 트래픽을 발생시켜 보니 메인 서버의 CPU 사용량이 기존 94.8%에서 50.4%까지 감소하여 여전히 안정적인 수치는 아니지만 기존 대비 46.83%의 CPU 사용량 감소율을 보여주어 서버의 분산 부하가 성공적으로 이루어졌다는 것을 확인할 수 있었습니다.</p>
<br>

<h1 id="problem-2--저조한-처리량">Problem 2 : 저조한 처리량</h1>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/b1c57ffc-82c5-45f8-be7a-5f1a77698380/image.png" alt=""></p>
<p>이후 처리량을 확인해보니 메인 서버의 초당 처리량이 54.7ops/s에 불과하여 성능적으로 매우 저조한 모습이 확인되어 성능 개선 또한 시급한 문제였습니다.</p>
<br>

<h1 id="solution-2--메세지-큐를-통한-비동기-처리">Solution 2 : 메세지 큐를 통한 비동기 처리</h1>
<p>왜 이렇게 처리 성능이 저조한 것인지 생각해보니 기존의 방식은 매칭 요청이 들어오면 메인 서버가 매칭 서버의 API를 호출한 뒤 매칭 처리가 끝난 후 매칭 결과를 응답값으로 받아야 다음 요청을 처리하는 동기 방식이기 때문에 이러한 동기 방식이 성능 저하의 원인이라고 생각하고 매칭 처리가 완전히 끝날 때까지 기다리지 않아도 되어 처리 속도의 향상을 기대할 수 있는 비동기 방식을 적용하기로 결정하였습니다.</p>
<p>그렇다면 이제 어떤 방식으로 비동기를 적용할지 고려해봐야 했습니다. 비동기 방식을 적용하는 데에는 수많은 방법이 존재하지만 저희는 메세지 큐를 통한 비동기 처리 방식을 선택하였습니다.</p>
<p>저희가 메세지 큐를 통한 비동기 방식을 선택한 이유는 우선 첫 번째, 메세지 큐를 사용하면 메세지 큐의 작동 방식 덕분에 서버 부하 개선 과 처리 성능 개선 이라는 두 마리 토끼를 동시에 잡을 수 있다고 생각했기 때문입니다. 메세지 큐를 사용하면 메세지를 큐에 일시적으로 저장하고 서버의 상태와 처리 능력에 따라 순차적으로 메세지를 처리할 수 있어 트래픽 폭증시 매칭 서버의 부하를 관리하는 데에 효과적일 것이라고 생각하였습니다. 또한 메세지 큐를 사용함으로써 메인 서버는 이전과는 달리 응답을 기다리지 않고 단순히 메세지 큐에 데이터를 전송하고 다음 요청을 비동기적으로 처리할 수 있어 매칭 요청 처리의 성능의 향상 또한 기대할 수 있다는 점을 고려하였습니다.</p>
<p>두 번째 이유는 메세지 큐를 사용하면 여타 다른 메인 서비스들과 메인 서비스간에 결합을 느슨하게 할 수 있어 그저 부가기능인 매칭 서비스에서 발생한 장애가 메인 서비스로 전파되는 것을 방지할 수 있다는 점 때문이었습니다.</p>
<p>저희가 고려한 메세지 및 이벤트 브로커 선택지는 Redis <code>Pub/Sub</code>, <code>RabbitMQ</code>, <code>Kafka</code> 3가지였습니다.</p>
<p>우선 Redis Pub/Sub의 경우 데이터 영속성이 아예 보장되지 않아 메세지 처리가 실패한 경우 해당 메세지가 소실된다는 점이 우려되어 선택지에서 제외하였고 Kafka는 실시간 처리량이 높고 대규모 데이터 전송에 유리하며 데이터 영속성이 보장된다는 강력한 장점이 존재하지만 RabbitMQ에 비해 초기 설정이 복잡하고 초기 학습 비용이 높아 막대한 대규모의 트래픽의 발생할 염려가 적고 매칭 시스템 특성상 매칭 데이터는 처리가 끝나면 영속성이 보장되어야할 만큼 중요한 데이터는 아니라고 판단하여 굳이 Kafka를 적용할 필요를 느끼지 못하여 <code>RabbitMQ</code>를 적용하였습니다.</p>
<p>메인 서버와 매칭 서버간에 RabbitMQ를 적용시켜 비동기 방식으로 매칭 처리가 이루어지게 설정한 후 같은 조건에서 모니터링해보니</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/517c7b8c-4234-44fb-b8ae-e25c5a453898/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/8d06003a-d28c-444c-bd3b-63a241ffbf86/image.png" alt=""></p>
<p>이전과 달리 서버간 API 호출을 통한 동기적 처리가 아니라 메세지 큐를 통해 비동기로 매칭이 처리되어 54.7ops/s에 불과했던 수치가 181ops/s까지 기존 처리량에 비해 처리량이 3.31배 증가했음을 확인할 수 있었습니다.</p>
<br>

<h1 id="problem-3--큐-병목-현상-발생">Problem 3 : 큐 병목 현상 발생</h1>
<p>성능을 더 개선하기 위해 두 서버 사이에서 데이터를 전달하는 RabbitMQ의 동작을 프로메테우스와 그라파나를 통해 모니터링해보았더니 메세지 큐 안에 적재되어 있는 데이터 양을 나타내는 “Message ready to be delivered to consumers” 항목에서</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/84ec8626-69b3-47af-941e-36a09cdc774d/image.png" alt="">
프로듀서의 데이터 전송량에 비해 컨슈머의 데이터 처리량이 훨씬 떨어져 10000건의 데이터를 전달하는 데에 최대 7000건의 데이터가 메세지 큐 안에 쌓여있는 병목 현상이 발생하여 매칭 요청 처리 성능이 저하되고 있다는 것을 확인할 수 있었습니다.</p>
<br>

<h1 id="solution-3--rabbitmq-클러스터-적용">Solution 3 : RabbitMQ 클러스터 적용</h1>
<p>이러한 병목 현상으로 인한 성능 저하를 해결하기 위해 기존 1개의 RabbitMQ 서버를 운영하던 것을 2개의 RabbitMQ를 더 추가하여 총 3개의 RabbitMQ를 생성한 후 RabbitMQ 클러스터를 생성하여 기존 1개에서 처리하던 전송-수신 작업을 3개의 노드에서 병렬적으로 작업하게 설정하였습니다.</p>
<pre><code class="language-#">
networks:
  rabbitmq-net:
    driver: bridge

services:
  rabbit1:
    image: rabbitmq:management
    hostname: rabbit1
    container_name: rabbit1

    ports:
      - &quot;15672:15672&quot; # RabbitMQ UI
      - &quot;5672:5672&quot;   # AMQP
      - &quot;15692:15692&quot; # RabbitMQ Prometheus
    networks:
      - rabbitmq-net
    environment:
      RABBITMQ_ERLANG_COOKIE: &#39;secret&#39;
      RABBITMQ_DEFAULT_USER: admin
      RABBITMQ_DEFAULT_PASS: 1234

  rabbit2:
    image: rabbitmq:management
    hostname: rabbit2
    container_name: rabbit2
    networks:
      - rabbitmq-net
    environment:
      RABBITMQ_ERLANG_COOKIE: &#39;secret&#39;

  rabbit3:
    image: rabbitmq:management
    hostname: rabbit3
    container_name: rabbit3
    networks:
      - rabbitmq-net
    environment:
      RABBITMQ_ERLANG_COOKIE: &#39;secret&#39;</code></pre>
<pre><code># rabbit2를 rabbit@rabbit1가 속한 클러스터에 합류
docker exec -it rabbit2 rabbitmqctl stop_app
docker exec -it rabbit2 rabbitmqctl join_cluster rabbit@rabbit1
docker exec -it rabbit2 rabbitmqctl start_app

# rabbit3를 rabbit@rabbit1가 속한 클러스터에 합류
docker exec -it rabbit3 rabbitmqctl stop_app
docker exec -it rabbit3 rabbitmqctl join_cluster rabbit@rabbit1
docker exec -it rabbit3 rabbitmqctl start_app</code></pre><p><img src="https://velog.velcdn.com/images/kim_dg/post/f113316b-e7c1-475d-a4c1-ad24db6cd6db/image.png" alt=""></p>
<p>3개의 노드를 클러스터로 적용한 후 동일한 조건에서 10초 동안 10000개의 매칭 요청을 발생시켜보았더니 기존 7000개까지 큐에 적재되던 데이터가 3000개까지 대폭 완화되어 데이터 병목 현상이 기존 대비 약 <code>57.14%</code> 감소된 것을 확인할 수 있었습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[메세지 브로커: 래빗엠큐(RabbiMQ)]]></title>
            <link>https://velog.io/@kim_dg/%EB%9E%98%EB%B9%97%EC%97%A0%ED%81%90RabbiMQ</link>
            <guid>https://velog.io/@kim_dg/%EB%9E%98%EB%B9%97%EC%97%A0%ED%81%90RabbiMQ</guid>
            <pubDate>Fri, 08 Nov 2024 02:54:34 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kim_dg/post/1ff150a1-ac2f-40d7-83c6-42a5d6a14e69/image.png" alt=""></p>
<br>
<br>

<h1 id="래빗엠큐rabbitmq란">래빗엠큐(RabbitMQ)란?</h1>
<blockquote>
<p><strong><span style = "color:red"><code>래빗엠큐(RabbitMq)</code></span>: 래빗엠큐는 고성능의 메시지 브로커로, 시스템 간의 비동기 통신을 가능하게 해 주는 메시지 큐 시스템아다. 오픈소스로 제공되며, 특히 AMQP(Advanced Message Queuing Protocol)를 기반으로 메시지를 처리한다. 다양한 프로토콜을 지원하고 확장성이 뛰어나며, 특히 신뢰성 있는 메시지 전달과 복잡한 메시지 라우팅에 적합하다는 특징을 가지고 있다.</strong></p>
</blockquote>
<br>
<br>


<h1 id="래빗엠큐-특징">래빗엠큐 특징</h1>
<ul>
<li><strong><code>메시지 큐 시스템</code></strong>
래빗엠큐는 메시지를 큐에 저장하고, 이를 수신할 준비가 된 컨슈머에게 전달한다. 프로듀서(발신자)가 보낸 메시지를 래빗엠큐가 관리하는 큐에 담아두었다가 컨슈머(수신자)에게 전달하며, 비동기적이기 때문에 프로듀서와 컨슈머가 동시에 작업할 필요 없이 독립적으로 메시지를 송수신할 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/b623446e-9ea5-49a9-b990-baf2a0fb32a1/image.png" alt=""></p>
<br>

<ul>
<li><strong><code>AMQP 지원</code></strong>
AMQP는 메시지 브로커와 클라이언트 간의 메시지 송수신 규칙을 정의한 프로토콜로, 신뢰성과 보안성을 강화한다. RabbitMQ는 이 프로토콜을 기본으로 지원하며, MQTT, STOMP, HTTP 등도 사용할 수 있어 다양한 시스템과 쉽게 연동할 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/957e45d9-4e95-4da7-b156-f2cb5aab76c5/image.png" alt=""></p>
<br>

<ul>
<li><p><strong><code>Exchange와 Queue</code></strong></p>
</li>
<li><ul>
<li><strong>Exchange</strong>: 프로듀서가 보낸 메시지를 큐에 정확하게 배분하는 역할을 한다. Exchange는 메시지를 큐에 라우팅하기 위한 규칙을 가진다.</li>
</ul>
</li>
<li><ul>
<li><strong>Queue</strong>: 실제 메시지가 저장되는 장소이다. Exchange가 메시지를 어떤 큐로 보낼지 결정하며, 이 큐에 쌓인 메시지를 컨슈머가 가져가 처리한다.</li>
</ul>
</li>
</ul>
<br>

<p>※ <strong><code>라우팅(Routing)</code></strong>은 메시지를 목적지 큐에 맞게 분배하거나 경로를 지정하는 과정이다. 메시지를 여러 서비스나 큐로 전송할 때, 메시지를 각 목적지에 맞게 필터링하거나 전송 규칙을 설정하여 효율적으로 분배하는 것이 라우팅이다.</p>
<p>※ <strong><code>라우팅 키(Routing Key)</code></strong> 메시지의 속성을 나타내는 문자열이다. Producer가 메시지를 전송할 때 Exchange에 전달하는 키로, 특정 큐에 메시지를 전달하거나 분배하는 데 사용된다.</p>
<p>라우팅 키는 단일 단어 또는 <strong>&quot;.&quot;(점)</strong>으로 구분된 단어 조합으로 구성될 수 있다.</p>
<ul>
<li>예: &quot;info&quot;, &quot;user.create&quot;, &quot;order.cancelled&quot;</li>
</ul>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/882cf16b-2dbf-4d20-8214-b30dbed5e250/image.png" alt=""></p>
<br>

<ul>
<li><p><strong><code>Exchange 유형</code></strong>
RabbitMQ의 Exchange는 몇 가지 유형이 있으며, 각 유형은 메시지의 라우팅 방식을 정의한다:</p>
</li>
<li><ul>
<li><strong><code>Direct</code></strong>: 특정 라우팅 키와 일치하는 메시지를 지정된 큐로 전달한다.</li>
</ul>
</li>
<li><ul>
<li><strong><code>Fanout</code></strong>: 라우팅 키와 상관없이 모든 연결된 큐에 메시지를 전송한다.</li>
</ul>
</li>
<li><ul>
<li><strong><code>Topic</code></strong>: 라우팅 키의 패턴과 일치하는 큐에 메시지를 전달한다. (예: &quot;user.signup&quot; 또는 &quot;order.payment&quot;)</li>
</ul>
</li>
<li><ul>
<li><strong><code>Headers</code></strong>: 라우팅 키 대신 헤더 값을 기반으로 큐에 전달한다.</li>
</ul>
</li>
</ul>
<ul>
<li><strong><code>Binding(바인딩)</code></strong>:
Binding은 Exchange와 Queue 사이의 연결을 나타내며, 특정 Exchange와 특정 큐 간의 바인딩을 설정함으로써 메시지의 라우팅 규칙을 결정한다.
Exchange와 Queue 간에 바인딩이 이루어지면, Exchange는 해당 바인딩에 따라 메시지를 큐로 전달한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/edf1af81-18ea-4e2e-bd0a-aa6cd3874625/image.png" alt=""></p>
<br>

<ul>
<li><p><strong><code>메시지 확인 및 안정성</code></strong>
RabbitMQ는 메시지 전달 과정에서 메시지 유실을 방지하기 위한 다양한 안전 장치를 제공한다.</p>
</li>
<li><ul>
<li><strong><code>Ack/Nack (Acknowledgment)</code></strong>: 컨슈머가 메시지를 수신하고 처리했음을 래빗엠큐에 확인해 줌으로써 메시지가 정확히 한 번 처리되었음을 보장한다. 만약 컨슈머가 메시지를 처리하지 못하면 Nack을 보내 재전송을 요청할 수 있다.</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/dc0d8d70-3dfd-47a7-8ccd-f1900ebd1444/image.png" alt=""></p>
<ul>
<li><ul>
<li><strong><code>Persistent 메시지</code></strong>: 메시지를 디스크에 저장하여 래빗엠큐 서버가 재시작되더라도 메시지를 유지한다.</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/17eebda3-6288-4842-b377-15d23dcbfa72/image.png" alt=""></p>
<br>

<ul>
<li><strong><code>확장성과 고가용성</code></strong>
RabbitMQ는 여러 노드로 구성된 클러스터를 지원하며, 이를 통해 확장성과 가용성을 높일 수 있다. 특히 고가용성 기능을 위해 미러링된 큐를 제공하여 큐의 복제본을 다른 노드에 생성하여 장애 발생 시 다른 노드에서 작업을 이어갈 수 있다.</li>
</ul>
<br>

<ul>
<li><strong><code>플러그인 및 관리 도구</code></strong>
RabbitMQ는 다양한 플러그인(예: HTTP API, MQTT 지원 등)을 제공하며, RabbitMQ Management Plugin을 통해 웹 기반 UI에서 큐의 상태를 모니터링하고, 메시지를 관리할 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/24a571f6-199c-48df-b3c4-7031a30a0607/image.png" alt=""></p>
<br>
<br>

<h1 id="래빗엠큐-장단점">래빗엠큐 장단점</h1>
<h2 id="장점">장점</h2>
<ul>
<li><p><code>풍부한 메시징 패턴 지원</code>
여러 유형의 Exchange (Direct, Topic, Fanout, Headers)를 지원하여 다양한 라우팅 규칙과 패턴을 사용할 수 있어 메시지를 특정 큐로 정확하게 전달하거나, 여러 큐에 동시에 메시지를 전송하는 등의 유연한 메시징이 가능하다.</p>
</li>
<li><p><code>높은 신뢰성과 내구성</code>
메시지의 영구 저장과 전송 실패 시 재전송을 지원해 신뢰할 수 있는 메시지 전송이 가능하다.
데이터 유실 방지를 위해 메시지를 디스크에 저장하거나, 클러스터링을 통한 고가용성 설정이 가능하다.</p>
</li>
<li><p><code>다양한 프로토콜 지원</code>
AMQP, MQTT, STOMP 등 여러 프로토콜을 지원해 다양한 시스템 및 환경에서 활용할 수 있다.</p>
</li>
<li><p><code>클러스터링과 고가용성</code>
여러 노드를 클러스터링하여 하나의 메시지 큐 시스템을 확장할 수 있어, 분산 환경에서도 안정적인 운영이 가능하여 장애 발생 시 복제 및 백업을 통해 메시지의 안전성을 보장한다.</p>
</li>
<li><p><code>손쉬운 관리</code>
RabbitMQ는 웹 기반의 관리 콘솔을 제공해 실시간으로 큐와 메시지 상태를 확인하고 관리할 수 있다.</p>
</li>
</ul>
<br>

<h2 id="단점">단점</h2>
<ul>
<li><p><code>확장성의 한계</code>
RabbitMQ는 복잡한 메시지 분배를 처리하는 데 최적화되어 있지만, 카프카와 같은 높은 스루풋을 요구하는 대용량 스트리밍 처리에는 적합하지 않다.</p>
</li>
<li><p><code>설정과 운영의 복잡성</code>
고가용성 및 클러스터링 설정 시 고려해야 할 사항이 많아 운영과 관리가 복잡할 수 있습니다.</p>
</li>
<li><p><code>메시지 처리량</code>
빠른 순차적 메시지 처리보다 안정성과 신뢰성에 초점이 맞춰져 있어, 고속 스트리밍보다는 트랜잭션 중심의 시스템에 적합합니다.</p>
</li>
</ul>
<br>
<br>

<h1 id="래빗엠큐-사용-예시">래빗엠큐 사용 예시</h1>
<ul>
<li><p>이메일 또는 알림 시스템: 사용자 가입 시 이메일 발송, 주문 발생 시 알림 전송 등 비동기로 처리해도 되는 작업을 메시지 큐를 통해 처리함으로써 메인 프로세스의 부담을 줄인다.</p>
</li>
<li><p>비동기 데이터 처리 필요시: 이미지 처리나 데이터 분석 등 시간이 오래 걸리는 작업을 메시지 큐에 전달하여, 작업이 완료되면 결과를 전달기 때문에 유저를 마냥 기다리게 할 필요 없어 유저의 사용 편의성이 증대된다.</p>
</li>
<li><p>마이크로서비스(MSA) 간 통신: 서로 독립적으로 동작하는 마이크로서비스들이 비동기로 데이터를 교환할 때 RabbitMQ를 사용하면 메시지 손실 없이 신뢰성 있는 통신을 보장할 수 있다.</p>
</li>
<li><p>주문 처리 시스템: 결제 완료 후 주문 처리 작업을 RabbitMQ 큐에 저장하고, 다양한 백엔드 시스템에서 이를 비동기로 받아 처리함으로써 실시간성을 유지할 수 있다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[WebSocket과 STOMP]]></title>
            <link>https://velog.io/@kim_dg/%EC%9B%B9%EC%86%8C%EC%BC%93WebSocket</link>
            <guid>https://velog.io/@kim_dg/%EC%9B%B9%EC%86%8C%EC%BC%93WebSocket</guid>
            <pubDate>Thu, 07 Nov 2024 15:43:35 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kim_dg/post/7c70c60f-8439-4d7b-ab0a-d3e63d575fae/image.png" alt=""></p>
<h2 id="웹소켓이란">웹소켓이란</h2>
<blockquote>
<p><strong><span style = "color:red"><code>웹소켓(WebSocket)</code></span>: 웹소켓은 클라이언트와 서버 간에 양방향 통신을 실시간으로 가능하게 하는 전송(transport) 계층의 TCP 기반의 프로토콜이다. HTTP와 달리 웹소켓은 연결이 지속적으로 유지되어, 클라이언트와 서버가 실시간으로 데이터를 주고받을 수 있다.</strong></p>
</blockquote>
<br>


<h2 id="웹소켓의-특징">웹소켓의 특징</h2>
<ul>
<li><p><strong><code>양방향 통신</code>: 웹소켓은 클라이언트와 서버 간의 양방향 통신을 지원한다. HTTP 요청과 응답의 일방적인 통신 방식과 달리, 웹소켓은 클라이언트와 서버가 상호 간에 데이터를 자유롭게 주고받을 수 있도록 합니다.</strong></p>
</li>
<li><p><strong><code>지속적인 연결</code>: 웹소켓은 연결 유지 방식을 사용한다. 한번 연결이 수립되면, 지속적인 연결을 유지하면서 서버는 클라이언트가 요청할 때마다 새로운 연결을 만들지 않고, 클라이언트와 서버 간에 단일 연결을 통해 데이터를 주고받는다.</strong></p>
</li>
<li><p><strong><code>저지연성</code>: 웹소켓은 지속적인 연결을 통해 데이터를 전송하기 때문에, 새로운 연결을 생성하는 오버헤드가 없고, 빠른 응답을 제공하여 저지연성을 유지한다. 이로 인해 실시간 애플리케이션에서 유리하다.</strong></p>
</li>
<li><p><strong><code>상태 유지</code>: HTTP는 상태 비저장(stateless) 프로토콜로 요청마다 새로운 연결을 만들고, 그 이후에 상태를 저장하지 않지만, 웹소켓은 연결이 유지되는 동안 연결 상태를 계속 유지하면서 클라이언트와 서버가 데이터를 주고받을 수 있다.</strong></p>
</li>
</ul>
<br>

<h2 id="웹소켓의-작동-방식">웹소켓의 작동 방식</h2>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/2de0af8f-53f7-40f6-b082-7deb971f77fd/image.png" alt=""></p>
<ul>
<li><p><strong><code>핸드쉐이크(Handshake)</code>:웹소켓은 HTTP 프로토콜을 사용하여 연결을 초기화한다. 클라이언트가 서버에 웹소켓 핸드쉐이크 요청을 보내면, 서버는 이를 승인하고 웹소켓 연결을 수립한다. 이 핸드쉐이크 과정이 끝나면, HTTP 프로토콜은 더 이상 사용되지 않고 웹소켓 프로토콜이 사용된다.</strong></p>
</li>
<li><p><strong><code>데이터 전송</code>: 연결이 수립된 후, 클라이언트와 서버는 실시간으로 양방향 데이터를 주고받을 수 있다. 서버는 클라이언트에게 데이터를 푸시할 수 있고, 클라이언트도 서버에 데이터를 보낼 수 있다.</strong></p>
</li>
<li><p>** <code>연결 종료</code>: 클라이언트 또는 서버 중 어느 한 쪽이 연결 종료 메시지를 보내고 다른 한 쪽이 이를 확인하면 연결이 종료된다.(어느 한 쪽이 close 프레임을 받지 못 하는 상황이면 비정상적인 종료를 감지하는 몇 가지 방법도 존재한다.)**</p>
</li>
</ul>
<br>

<h2 id="웹소켓과-http의-차이점">웹소켓과 HTTP의 차이점</h2>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/6877e92a-4b56-4444-ab10-2e84a283a2cc/image.png" alt=""></p>
<p>HTTP는 요청-응답 방식으로, 서버가 클라이언트의 요청에 대해 응답을 보내면 연결이 종료되는 방식이다. 
하지만 웹소켓은 지속적인 연결을 유지하고, 클라이언트만 서버에게 요청을 보낼 수 있는 단방향 통신인 HTTP와는 다르게 클라이언트와 서버 간에 양방향 통신이 가능하여 실시간으로 데이터를 주고받을 수 있다는 차이점이 있다.</p>
<p><strong>웹소켓은 무상태(stateless) 프로토콜인 HTTP와 다르게 상태를 유지(stateful)하는 연결 방식이다.</strong></p>
<br>

<table>
<thead>
<tr>
<th>HTTP</th>
<th>WebSocker</th>
</tr>
</thead>
<tbody><tr>
<td>비연결형(Connectionless)</td>
<td>연결 지향형(Connection-oriented)</td>
</tr>
<tr>
<td>단방향 통신 (Unidirectional Communication)</td>
<td>양방향 통신 (Bidirectional Communication)</td>
</tr>
</tbody></table>
<br>
<br>


<p>그렇다면 어떤 경우에 HTTP가 아니라 웹소켓을 사용하는 것이 좋을까?</p>
<p>예를 들어 HTTP 응답 요청 방식을 이용하여 채팅 서비스를 구현한다고 가정해보자. </p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/e52c4b7c-8ba1-4878-9705-259674e38dea/image.png" alt=""></p>
<p>클라이언트에서 서버에 HTTP 방식으로 &#39;안녕&#39;이라는 메시지를 전송한다.</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/b83bce4c-11f3-4501-b7cc-ae836905886e/image.png" alt=""></p>
<p>그럼 이에 대한 답장이 왔는지 확인하기 위해서는 클라이언트에서 일일이 주기적으로 서버에 새로운 메시지가 들어왔는지 요청을 날려야 한다.</p>
<p>당연히 이 방식은 답장 존재 여부에 상관없이 계속 주기적으로 요청을 보내야 하기 때문에 서비스가 커지면 커질수록, 사용자만 많으면 많아질수록 불필요한 요청과 커넥션 생성, 연결, 연결 해제 등의 비용이 크게 발생하는 매우 비효율적인 방식이다.</p>
<br>

<p>이러한 경우에 해당 문제를 해결하기 위해서는 웹소켓을 사용해야 한다. </p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/e5ae2205-13ae-4779-bac3-b57f90c7cee3/image.png" alt=""></p>
<p>클라이언트와 서버가 웹소켓을 통해 연결되면 서버와 클라이언트 클라이언트에서 주기적인 요청을 통해 답장 메시지를 확인할 필요 없이 양쪽에 메시지가 들어올 때마다 새로 커넥션을 맺지 않고도 실시간으로 서로에게 메시지를 전송할 수 있다. </p>
<br>
<br>


<h2 id="웹소켓의-장단점">웹소켓의 장단점</h2>
<h3 id="장점">장점</h3>
<ul>
<li><p><code>실시간 통신</code>: 웹소켓은 실시간으로 데이터를 주고받을 수 있어서 빠른 응답이 필요한 애플리케이션에 유용다.</p>
</li>
<li><p><code>낮은 대기 시간</code>: 연결이 지속되기 때문에 새로운 연결을 설정할 필요 없이 빠르게 데이터를 주고받을 수 있다.</p>
</li>
<li><p><code>효율성</code>: HTTP보다 오버헤드가 적고, 여러 요청을 하나의 연결로 처리할 수 있어 서버 자원을 효율적으로 사용할 수 있다.</p>
</li>
</ul>
<br>

<h3 id="단점">단점</h3>
<ul>
<li><p>연결 유지: 클라이언트와 서버 간에 연결이 지속적으로 유지되어야 하므로, 많은 클라이언트를 지원하려면 서버가 높은 부하를 처리해야 할 수 있다. 또한 오랫동안 데이터 전송이 없어도 연결을 유지해야 하기 때문에 필요 이상의 연결 유지 비용이 소모될 수도 있다.</p>
</li>
<li><p>보안: 웹소켓은 HTTP 기반으로 연결을 시작하므로, 보안에 대한 고려가 필요하다.</p>
</li>
</ul>
<br>

<h2 id="웹소켓-사용-예시">웹소켓 사용 예시</h2>
<ul>
<li><p>실시간 채팅 애플리케이션</p>
</li>
<li><p>온라인 게임의 실시간 데이터 교환</p>
</li>
<li><p>주식 가격 실시간 업데이트</p>
</li>
<li><p>알림 시스템</p>
</li>
</ul>
<br>

<p><strong>웹소켓은 주로 실시간 데이터 통신이 필요한 애플리케이션에서 많이 사용된다.</strong></p>
<br>
<br>

<h2 id="웹소켓-세션session">웹소켓 세션(Session)</h2>
<blockquote>
<p><strong><span style = "color:red"><code>웹소켓 세션(WebSocket Session)</code></span>: 웹소켓 세션이란 클라이언트와 서버 간에 유지되는 장기적이고 양방향 통신 채널을 의미한다. 웹소켓은 HTTP와 달리 한 번 연결이 수립되면 통신이 끊기지 않고 지속되며, 이 상태를 <code>&#39;세션&#39;</code>이라고 부른다. 웹소켓 세션을 통해 실시간 채팅, 게임, 증권 거래 정보 등 지연 없이 즉각적인 데이터 전송이 가능하다.</strong></p>
</blockquote>
<p>즉, 웹소켓 세션이란 웹소켓 연결이 연결된 상태이자 서버와 브라우저가 데이터를 주고받는 각각의 통로라고 생각하면 된다.</p>
<p>실제로 사용자가 웹 사이트에 들어와 웹소켓 연결을 맺을 때마다 서버는 해당 유저와 연결된 세션을 만들어 관리하고 서버는 특정 세션에만 메시지를 전달할 수도, 전체 세션에 동시에 데이터를 전달할 수도 있다.</p>
<p>일반적으로 사용자가 웹소켓에 접속하면 세션이 생겨나고, 사용자가 창을 닫거나 서버와의 연결이 끊어지면 세션이 없어진다.</p>
<br>
<br>
<br>

<h2 id="stompsimple-text-oriented-messaging-protocol란">STOMP(Simple Text Oriented Messaging Protocol)란?</h2>
<blockquote>
<p>**<span style = "color:red"><code>STOMP</code></span>: STOMP란 WebSocket 위에서 메시지를 구조적으로 주고받기 위한 프로토콜(규약)이다. STOMP는 복잡한 웹소켓 통신을 추상화하여, 개발자가 메시지 교환을 보다 쉽게 구현할 수 있도록 돕는다. **</p>
</blockquote>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/d9bd6de7-7857-42c3-84e6-e5c5a7a3b2ba/image.png" alt=""></p>
<p>앞서 설명했듯이 서버와 클라이언트가 웹소켓으로 연결되면 서로 메시지를 전달할 수 있게 되지만 웹소켓은 단지 바이너리와 텍스트 데이터만 전송이 가능할 뿐 메시지 형식은 제공되지 않는다는 문제점이 존재한다. </p>
<p>이 때문에 바이너리와 텍스트 데이터처럼 단순한 메시지가 아니라 다른 복잡한 형식의 메시지가 전송되면 클라이언트와 서버의 입장에서는 전달받은 메시지를 어떻게 해석해야 하는지, 어떻게 처리해야 되는지 알 수 없다. </p>
<p>따라서 이 문제를 해결하기 위해서는 <strong>클라이언트와 서버가 전달받은 메시지를 이해하고 처리할 수 있는 구조화된 형식이 필요하고 이 구조화된 메시지 형식이 <code>STOMP</code>이다.</strong></p>
<br>


<p>STOMP의 형식은 크게 3가지로 나눌 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/24e5e33d-4279-461d-ac1a-29622beba594/image.png" alt=""></p>
<p>우선 가장 상단에 있는 <strong><code>COMMAND</code></strong> 부분은 <strong>무엇을 할 것인지를 지시하는 명령어</strong>로 연결, 메시지 전송, 연결 해제 등의 명령어들이 존재한다.</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/85225022-3730-4499-a74a-dbf24042ca87/image.png" alt=""></p>
<p>그 다음 중간 부분에는 <strong><code>header</code></strong>가 존재하는데 <code>header</code>는 메시지 경로, 내용 형식 등의 <strong><code>COMMAND</code>에 대한 등의 추가 정보나 옵션</strong>을 나타낸다. </p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/94d0bd01-2616-4a62-9945-e4009b17c99d/image.png" alt=""></p>
<p>마지막으로 <strong><code>Body</code></strong> 부분에는 <strong>실제 전송할 메시지의 내용</strong>이 포함된다.</p>
<br>
<br>


<h3 id="stomp의-pubsub">STOMP의 Pub/Sub</h3>
<p>STOMP의 특징으로는 STOMP는 Pub/Sub 구조를 지원한다는 특징이 존재한다. Pub/Sub 구조를 이용하면 하나의 메시지를 다른 여러 사용자들에게 동시에 전달할 수 있다. </p>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/d09ede69-2a11-4777-941a-ca287ec4289f/image.png" alt=""></p>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/d1972973-8fd3-4367-87eb-2a7439025e5b/image.png" alt=""></p>
<p>메시지를 발행하는 주체인 <code>Publisher</code>가 메시지를 발행하면 메시지를 수신하는 주체인 <code>Subscriber</code>들이 메시지를 분류하는 경로인 특정 <code>topic</code>을 구독하여 메시지를 전달받는다.</p>
<br>

<p><img src="https://velog.velcdn.com/images/kim_dg/post/7b13e910-40f6-4e0e-8341-158080271917/image.png" alt=""></p>
<br>

<p>Spring에서는 STOMP가 이미 구현되어 있고 내부에서 어떻게 처리하는지 알아보자.</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/7696f414-3dd0-4c64-aaf5-eb70dc86b8eb/image.png" alt=""></p>
<p>특정 클라이언트나 서버가 웹소켓을 통해 텍스트 형태의 메시지를 전송하면 <code>inBoundChannel</code>로 메시지가 전송된다. 그럼 <code>inBoundChannel</code>에서 텍스트 형태로 받은 메시지를 STOMP 형식으로 파싱한다.</p>
<p>파싱된 메시지의 header에서 경로를 확인하고 해당 경로의 메시지를 처리할 수 있는 해당 경로의 컨트롤러인 <code>MessageHandler</code>로 전달한다. MessageHandler를 통해 작성한 비즈니스 로직이 실행되고 그 결과로 새로운 메시지를 반환하게 된다. </p>
<p>이렇게 반환된 메시지는<code>brokerChannel</code>로 이동하게 되는데 brokerChannel은 메시지를 수신하면 모든 채널들의 구독 경로를 확인하고 해당 메시지의 경로에 매칭되는 채널들에 해당 메시지를 전달한다. </p>
<p>그 중에서 클라이언트의 응답 전용 채널인 <code>outBoundChannel</code>에서 해당 메시지를 전달받아 구독된 클라이언트들에게 응답 메세지를 전달한다.</p>
<br>
<br>




<p><img src="blob:https://velog.io/bd34b9d6-27dd-40c1-804c-92d5437f8116" alt="업로드중.."></p>
<p>자료 출처: <a href="https://www.youtube.com/watch?v=ckxmMbwmn98&amp;t=310s">https://www.youtube.com/watch?v=ckxmMbwmn98&amp;t=310s</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[이벤트 브로커: 카프카(Kafka)]]></title>
            <link>https://velog.io/@kim_dg/%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%B8%8C%EB%A1%9C%EC%BB%A4-%EC%B9%B4%ED%94%84%EC%B9%B4Kafka</link>
            <guid>https://velog.io/@kim_dg/%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%B8%8C%EB%A1%9C%EC%BB%A4-%EC%B9%B4%ED%94%84%EC%B9%B4Kafka</guid>
            <pubDate>Thu, 07 Nov 2024 08:40:31 GMT</pubDate>
            <description><![CDATA[<p>메세지 큐와 브로커의 개념에 대해 잘 모르시는 분은 이전 포스팅인 <a href="https://velog.io/@kim_dg/MOM-%EB%A9%94%EC%84%B8%EC%A7%80-%ED%81%90Message-Queue%EC%99%80-%EB%B8%8C%EB%A1%9C%EC%BB%A4Broker">MOM: 메세지 큐(Message Queue)와 브로커(Broker)</a>을 정독하고 오시는 것을 권장합니다.</p>
<br>

<h1 id="카프카kafka란">카프카(Kafka)란?</h1>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/82bd1ab9-6a98-430b-8653-32e86269613a/image.png" alt=""></p>
<blockquote>
<p><strong><span style = "color:red">카프카(Apache Kafka)</span> : 카프카는 대규모 실시간 데이터 스트리밍 및 이벤트 처리 시스템을 위한 분산형 메시징 시스템으로 여러 애플리케이션간에 데이터를 공유할 수 있게 하는 메시지 브로커 역할을 한다.</strong></p>
</blockquote>
<br>

<h1 id="kafka의-특징">Kafka의 특징</h1>
<ul>
<li><p><strong>높은 처리량</strong>: kafka는 매우 높은 메시지 처리량을 자랑하기 때문에 대규모 데이터 스트리밍을 처리하는 데 적합하다.</p>
</li>
<li><p><strong>확장성</strong>: kafka는 수평적으로 확장이 가능하여 클러스터에 브로커를 추가하여 처리 성능을 높일 수 있다.</p>
</li>
<li><p><strong>데이터 영속성</strong>: kafka는 메시지를 디스크에 영속적으로 저장되며, 여러 복제본을 통해 내구성을 보장하기 때문에 데이터가 손실될 염려가 없다.</p>
</li>
<li><p><strong>실시간 처리</strong> : kafka는 실시간으로 데이터를 처리할 수 있어, 이벤트 기반 아키텍처에 적합하다.</p>
</li>
<li><p><strong>분산 시스템</strong>: kafka는 분산 아키텍처로 설계되어 장애에 강하고, 데이터 손실 없이 메시지를 전달할 수 있다.</p>
</li>
</ul>
<br>

<h1 id="kafka-주요-구성-요소">Kafka 주요 구성 요소</h1>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/7507e5f9-2854-4f32-ac5f-60067fbb259f/image.png" alt=""></p>
<h2 id="producer">Producer</h2>
<blockquote>
<p><strong><span style = "color:red"><code>Producer</code></span>: 데이터를 kafka로 보내는 주체로 Producer는 데이터를 특정 토픽(Topic)에 담아 전송한다.</strong></p>
</blockquote>
<ul>
<li>메시지 전송 시 Batch 처리가 가능하다.</li>
<li>key값을 지정하여 특정 파티션으로만 전송이 가능하다.</li>
<li>전송 acks값을 설정하여 효율성을 높일 수 있다.</li>
<li><ul>
<li>ACKS=0(확인 응답 기다리지 X) -&gt; 매우 빠르게 전송. 파티션 리더가 받았는 지 알 수 없다.
ACKS=1(리더 파티션의 응답만 필요) -&gt; 파티션 리더가 받았는지 확인. 기본값
ACKS=ALL(모든 복제 파티션의 응답 필요) -&gt; 파티션 리더 뿐만 아니라 팔로워까(복제본을 보관하는 다른 파티션)지 메시지를 받았는지 확인</li>
</ul>
</li>
</ul>
<p>※ 전송 acks: Kafka의 acks(acknowledgments) 설정은 프로듀서(데이터를 보내는 쪽)가 메시지를 전송할 때 브로커가 어느 정도 수준의 확인 응답을 해야 전송을 성공했다고 간주할지를 설정하는 옵션이다. 
acks 설정에 따라 데이터의 신뢰성과 지연 속도 간의 균형이 달라진다. 각 설정 값에 따라 메시지 전송 성공 여부가 결정되는 방식이 다르다.</p>
<br>


<h2 id="consumer">Consumer</h2>
<blockquote>
<p><strong><span style = "color:red"><code>Consumer</code></span>: Kafka에서 데이터를 소비하는 주체로 Consumer는 Kafka의 토픽에서 데이터를 읽어온다.</strong></p>
</blockquote>
<ul>
<li>메세지를 Batch 처리할 수 있다.</li>
<li>한 개의 컨슈머는 여러 개의 토픽을 처리할 수 있다.</li>
<li>메시지를 소비하여도 메시지를 삭제하지는 않는다. (Kafka delete policy에 의해 삭제)</li>
<li>한 번 저장된 메시지를 여러번 소비도 가능하다.</li>
<li>컨슈머는 컨슈머 그룹에 속한다.</li>
<li>각 파티션은 동일한 컨슈머그룹 안에서 여러 개의 컨슈머에서 연결할 수 없다.(오직 하나의 컨슈머에만 연결)</li>
</ul>
<br>


<h2 id="broker">Broker</h2>
<blockquote>
<p><strong><span style = "color:red"><code>Broker</code></span>: Kafka 클러스터 내에서 데이터를 저장하고, <code>Producer와 Consumer 간의 메시지를 중계하는 역할</code>을 하는 서버로 Kafka는 여러 개의 브로커로 구성된 클러스터로 동작할 수 있다.</strong></p>
</blockquote>
<ul>
<li>현재 실행된 각 카프카 서버를 의미한다.</li>
<li>프로듀서와 컨슈머는 별도의 애플리케이션으로 구성되는 반면, 브로커는 카프카 자체이다.</li>
<li>Broker(각 서버)는 Kafka Cluster 내부에 존재한다.</li>
<li>서버 내부에서 메시지를 저장하고 발송하고 관리하는 역할을 수행한다.</li>
</ul>
<br>


<h2 id="topic">Topic</h2>
<blockquote>
<p><strong><span style = "color:red"><code>Topic</code></span>: Kafka에서 데이터를 분류하는 논리적 단위이다. Producer는 메시지를 설정한 특정 토픽으로 전송하고, Broker는 토픽을 저장하며, Consumer가 해당 토픽을 구독하여 메시지를 가져온다.</strong></p>
</blockquote>
<ul>
<li>각각의 메시지를 목적에 맞게 구분할 때 사용된다.</li>
<li>메시지를 전송하거나 소비할 때 반드시 Topic을 입력해야 한다.</li>
<li>Consumer는 자신이 구독(담당)하는 Topic의 메시지를 처리한다.</li>
<li>한 개의 토픽은 한 개 이상의 파티션으로 구성되어야 한다.</li>
</ul>
<br>


<h2 id="partition">Partition</h2>
<blockquote>
<p><strong><span style = "color:red"><code>Partition</code></span>: Kafka의 토픽은 여러 개의 파티션으로 나뉘어져 데이터를 분산 처리한다. 파티션은 메시지의 순서를 보장하고, 데이터를 병렬로 처리할 수 있도록 해준다.</strong><br>
<strong>데이터 중복 처리를 방지하기 위해 파티션은 한 컨슈머 그룹 내에서는 무조건 하나의 컨슈머하고만 연결되어야만 한다.</strong></p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/e567d58b-910b-47d4-8777-dc65f5cf2a51/image.png" alt=""></p>
<br>

<ul>
<li>분산 처리를 위해 사용된다.</li>
<li>Topic 생성 시 partition 개수를 지정할 수 있다.(파티션 개수 추가만 가능, 삭제 불가능)</li>
<li>파티션이 1개라면 모든 메시지에 대해 순서가 보장된다.</li>
<li>파티션 내부에서 각 메시지는 인덱스 같은 offset(고유 번호)로 구분된다.</li>
<li>파티션이 여러개라면 Kafka 클러스터가 라운드 로빈 방식(순서대로 돌아가면서 작업을 처리)으로 분배해서 분산처리되기 때문에 메시지가 여러 파티션으로 분산되는 과정에서 순서가 보장되지 않는다.</li>
<li>파티션이 많을 수록 처리량이 좋지만 장애 복구 시간이 늘어난다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/ed6d7fa3-df30-4991-8cef-a714d0a22249/image.png" alt=""></p>
<ul>
<li>각 파티션은 하나의 리더와 여러 개의 팔로워를 가질 수 있는데 이때 리더는 주키퍼에 의해 선출되고 파티션에 대한 모든 읽기/쓰기 요청을 처리하고, 팔로워들은 리더의 데이터를 복제한다.</li>
</ul>
<br>

<h2 id="zookeeper">Zookeeper</h2>
<blockquote>
<p><strong><span style = "color:red"><code>Zookeeper</code></span>: Kafka는 Zookeeper를 사용하여 Kafka 클러스터의 메타데이터를 관리하고, 브로커의 상태를 모니터링하며, 파티션의 리더 선출을 처리한다.</strong></p>
</blockquote>
<ul>
<li>분산 애플리케이션 관리를 위해 리더 선출, 서버 간의 작업 분배, 임계 영역 관리 등을 실행하는 코디네이션 시스템이다.</li>
<li>분산 메시지큐의 메타 정보를 중앙에서 관리하는 역할이다.</li>
</ul>
<br>

<h2 id="consumer-group">Consumer Group</h2>
<blockquote>
<p><strong><span style = "color:red"><code>Consumer Group</code></span>: 하나 이상의 Consumer가 같은 Consumer Group으로 묶여 하나의 토픽을 병렬로 소비할 수 있도록 한다. 이 방식으로 메시지의 처리를 분산시키고, 각 메시지는 중복 없이 한 번만 소비되도록 할 수 있다.</strong></p>
</blockquote>
<br>


<h2 id="offset">Offset</h2>
<blockquote>
<p><strong><span style = "color:red"><code>Offset</code></span>: Offset는 카프카에서 <code>각 메시지가 파티션 내에서 저장되는 위치를 나타내는 고유한 번호이자 Consumer에서 메시지를 어디까지 읽었는지 저장하는데 활용되는 값</code>이다. 
카프카는 메시지를 순차적으로 기록하고, 각 메시지에 대해 파티션 내에서 고유한 offset을 할당한다. 이 offset을 통해 카프카 클러스터 내의 파티션에서 메시지를 구별하고, 각 컨슈머에서 특정 파티션의 데이터를 어디까지 읽어 갔는지 기록할 수 있어 메시지를 정확히 찾아서 읽을 수 있게 한다.</strong></p>
</blockquote>
<p>Consumer에서 데이터를 잘 가져갔으면 offset을 브로커에 전달하고 그럼 브로커에서는 <code>__consumer_offsets</code> 라는 저장 공간에 각 <code>컨슈머 그룹</code>에서 파티션 별로 어디까지 데이터를 가져갔는지 기록합니다. </p>
<p>offset을 통해서 컨슈머 그룹은 이전에 가져갔던 데이터를 다시 가져가는 중복 컨슘의 문제를 피할 수 있
다.</p>
<ul>
<li>컨슈머 그룹의 컨슈머들은 각각의 파티션에 자신이 가져간 메시지의 위치 정보(offset) 을 기록</li>
<li>컨슈머 장애 발생 후 다시 살아나도, 전에 마지막으로 읽었던 위치에서부터 다시 읽어들일 수 있다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/923751d0-a059-40cb-8498-bbc8961ba6af/image.png" alt=""></p>
<br>
<br>

<h2 id="kafka-브로커의-역할">Kafka 브로커의 역할</h2>
<ul>
<li><p><strong>메시지 저장</strong>: Kafka 브로커는 메시지를 디스크에 저장한다. 메시지는 특정 토픽의 특정 파티션에 저장되어, Consumer가 요청할 때 메세지를 읽을 수 있다.</p>
</li>
<li><p><strong>메시지 전달</strong>: Producer가 보낸 메시지를 저장하고, 이를 구독한 Consumer에게 전달하면 Consumer는 Kafka 브로커에서 데이터를 읽어온다.</p>
</li>
<li><p><strong>메시지 복제</strong>: Kafka는 데이터를 여러 브로커에 복제하여 내구성을 보장한다. 이로 인해 각 파티션에 대해 복제본이 여러 개 존재할 수 있다.</p>
</li>
</ul>
<br>
Kafka 클러스터는 여러 서버(브로커)로 구성되어 있으며, 그 각 브로커는 데이터를 저장하고 전달하는 "서버" 역할을 한다. Kafka 자체는 하나의 서버가 아니라, 여러 브로커와 Zookeeper를 포함한 분산 시스템이다.


<br>
<br>

<h1 id="partition-사용-이유">Partition 사용 이유</h1>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/86b27fb0-5ce2-4dbd-ab93-c95b1eb6fecf/image.png" alt=""></p>
<p>여러 파티션을 사용함으로써 얻을 수 있는 주요 이점은 다음과 같다.
<br></p>
<ol>
<li><p><strong><code>확장성(Scalability)</code></strong>:
하나의 파티션만 사용할 경우, 해당 파티션에 대한 읽기 및 쓰기 요청은 단일 서버(혹은 브로커)에 집중되기 때문에 만약 데이터 양이 많아지면, 하나의 서버로는 처리하기 어려운 상황이 발생할 수 있다.
하지만 여러 파티션을 사용하면 데이터를 여러 서버에 분산시킬 수 있어, 데이터 양이 증가하더라도 시스템의 수평 확장이 가능해져 각 파티션이 독립적으로 운영되므로, 파티션 수를 늘리면 시스템 용량을 효율적으로 늘릴 수 있다.</p>
</li>
<li><p><strong><code>병렬 처리 (Parallel Processing)</code></strong>:</p>
</li>
</ol>
<p><strong>각 파티션은 독립적인 단위로 메시지를 저장하고 처리하므로, 여러 컨슈머가 동시에 각 파티션에서 메시지를 읽어가서 동시에 작업 진행이 가능해진다. 즉, 파티션 수가 많을수록 더 많은 병렬 처리가 가능해져 전체 서비스의 작업 속도를 향상시킬 수 있다.</strong>
예를 들어, 컨슈머 그룹에서 각 컨슈머가 하나의 파티션을 담당하면서 메시지를 병렬로 처리할 수 있다. 이로 인해 처리 속도가 빨라지고, 시스템이 더 많은 데이터를 빠르게 처리할 수 있다.</p>
<ol start="3">
<li><p><strong><code>가용성 및 장애 대응 (Availability and Fault Tolerance)</code></strong>:
카프카는 파티션 복제를 통해 장애 복구와 고가용성을 보장한다. 각 파티션은 여러 개의 브로커에 복제본을 두고 있어서, 특정 브로커가 장애를 일으켜도 해당 파티션의 데이터를 다른 브로커에서 제공할 수 있다.
복제된 파티션이 많을수록 시스템의 장애 대응 능력이 향상된다. 각 파티션이 다른 서버에 분산되어 있기 때문에, 데이터 손실을 최소화하고, 시스템의 가용성을 높일 수 있다.</p>
</li>
<li><p><strong><code>부하 분산 (Load Balancing)</code></strong>:
파티션을 여러 개로 나누면, 읽기/쓰기에 대한 부하를 여러 서버에 고르게 분배할 수 있다. 
예를 들어, 쓰기 작업은 특정 파티션에만 집중되지만, 파티션 수가 많으면 각 서버로 분산되어 부하가 균등하게 나뉜다.
이로 인해, 서버 자원(CPU, 메모리 등)을 효율적으로 활용할 수 있다.</p>
</li>
<li><p><strong><code>유연한 파티션 재배치 (Flexible Partition Rebalancing)</code></strong>:
파티션 수를 늘리거나 줄일 수 있어 동적 확장에 유리하다. 시스템의 부하가 증가하면 새로운 파티션을 추가하여 처리량을 확장할 수 있고, 부하가 줄어들면 파티션을 조정하여 자원을 최적화할 수 있다.
카프카는 파티션 재배치(파티션의 리더와 복제본을 다른 브로커로 이동)를 통해 이러한 유연성을 제공한다.</p>
</li>
<li><p><strong><code>효율적인 데이터 배포 (Efficient Data Distribution)</code></strong>:
카프카는 메시지를 여러 파티션에 분배하여 효율적으로 데이터를 분산 저장하고, 각 파티션은 독립적인 로그로 저장된다. 이로 인해 대규모 데이터를 분산 처리하고 관리하는 데 유리하다.</p>
</li>
</ol>
<br>

<p>결국 하나의 토픽을 여러 개의 파티션으로 나누는 이유는 <strong><code>카프카의 성능</code>, <code>확장성</code>, <code>병렬 처리</code>, <code>가용성</code> 등을 최적화</strong>하기 위해서이다. 
여러 파티션을 활용하면 데이터 양이 많아지더라도 시스템을 수평적으로 확장할 수 있고, 병렬 처리와 부하 분산을 통해 더 효율적으로 데이터를 처리할 수 있다. 또한, 파티션의 복제를 통해 고가용성 및 장애 복구가 가능해진다.</p>
<br>
<br>

<h1 id="컨슈머-그룹-사용-이유">컨슈머 그룹 사용 이유</h1>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/cf327a21-7319-478c-802b-abb40f111b07/image.png" alt=""></p>
<p><strong>컨슈머 그룹(Consumer Group)</strong>은 카프카에서 매우 중요한 개념으로, 여러 개의 <strong>컨슈머(Consumer)</strong>가 하나의 그룹을 이루어 토픽에서 메시지를 병렬로 처리하도록 하는 방식입니다. 이를 사용하는 이유는 성능 향상, 부하 분산, 장애 복구 등 여러 가지 장점이 있기 때문입니다. 주요 이유는 다음과 같습니다:</p>
<ol>
<li><p><strong><code>부하 분산 (Load Balancing)</code></strong>:
하나의 토픽에 여러 개의 컨슈머 그룹이 있다면, 각 컨슈머는 독립적으로 메시지를 처리하게 됩니다. <strong>각 컨슈머는 그룹 내에서 하나의 파티션에만 메시지를 소비하게 되므로, 여러 컨슈머가 각각 다른 파티션을 처리하며 병렬로 작업을 분담합니다.</strong>
이로 인해, 하나의 컨슈머가 처리할 수 있는 데이터 양을 초과할 경우 여러 컨슈머가 각각 다른 파티션을 담당하여 부하를 분산시킬 수 있다.</p>
</li>
<li><p><strong><code>병렬 처리 (Parallel Processing)</code></strong>:
카프카에서 컨슈머 그룹을 사용하면, 여러 개의 컨슈머가 동일한 토픽을 처리하되, 각 컨슈머가 독립적인 파티션을 읽도록 함으로써 병렬 처리가 가능해진다.
예를 들어, 하나의 토픽에 4개의 파티션이 있고 4개의 컨슈머가 같은 컨슈머 그룹에 속하면, 각 컨슈머는 각 파티션에서 독립적으로 메시지를 읽어온다. 이를 통해 처리 속도를 대폭 향상시킬 수 있다.</p>
</li>
<li><p><strong><code>메시지 처리의 효율성 (Message Processing Efficiency)</code></strong>:
각 컨슈머가 하나의 파티션을 처리하기 때문에, 메시지의 순서 보장을 유지할 수 있다. 카프카는 파티션 단위로 메시지 순서를 보장하므로, 동일 파티션을 담당하는 컨슈머가 메시지를 처리하면 순서대로 메시지가 처리된다.
여러 컨슈머가 있을 경우 메시지 처리가 병렬로 이루어져 효율성이 극대화된다.</p>
</li>
<li><p><strong><code>확장성 (Scalability)</code></strong>:
카프카는 컨슈머 그룹을 통해 토픽에 대한 메시지 처리량을 수평적으로 확장할 수 있다. 예를 들어, 처음에는 2개의 컨슈머만 있을 때는 메시지가 두 개의 컨슈머에서만 소비되지만, 컨슈머 그룹에 새로운 컨슈머를 추가하면 파티션 수에 맞춰 자동으로 작업이 분배되어 성능이 향상된다.
파티션의 수에 맞춰 컨슈머 그룹에 컨슈머 수를 확장할 수 있어, 수요에 맞는 유연한 확장이 가능하다.</p>
</li>
<li><p><strong><code>장애 복구 및 내결함성 (Fault Tolerance &amp; Resilience)</code></strong>:
카프카에서 컨슈머 그룹을 사용하면, 컨슈머 장애 시 다른 컨슈머가 대체하여 메시지를 소비할 수 있다. 예를 들어, 하나의 컨슈머가 장애로 중단되면, 같은 컨슈머 그룹에 속한 다른 컨슈머가 해당 파티션을 재배치 받아 메시지를 계속 처리할 수 있다.
이렇게 함으로써, 시스템의 장애 복구가 용이하고, 전체 시스템의 가용성을 높일 수 있다.</p>
</li>
<li><p><strong><code>중복 메시지 처리 방지 (Message Deduplication)</code></strong>:
컨슈머 그룹을 사용하면 각 파티션에 대한 메시지를 하나의 컨슈머만 읽을 수 있기 때문에, 중복 처리를 방지할 수 있다. 각 컨슈머는 자신에게 할당된 파티션에서만 메시지를 읽기 때문에, 다른 컨슈머가 이미 처리한 메시지를 중복해서 처리하는 일이 없다. 
이를 통해 정확한 데이터 처리가 보장된다.</p>
</li>
<li><p><strong><code>메시지의 공평한 분배 (Fair Message Distribution)</code></strong>:
카프카는 파티션 수와 컨슈머 수에 맞게 메시지를 공평하게 분배한다. 각 파티션은 컨슈머 그룹 내에서 하나의 컨슈머에게만 할당되므로, 모든 컨슈머가 공평하게 메시지를 처리할 수 있다.
이로 인해, 하나의 컨슈머가 지나치게 많은 메시지를 처리하는 일이 없고, 전체 시스템의 부하를 고르게 분산시킬 수 있다.</p>
</li>
<li><p><strong><code>단일 파티션에 대한 메시지 순서 보장 (Message Ordering Within a Partition)</code></strong>:
카프카는 파티션 내에서 메시지 순서를 보장합니다. 컨슈머 그룹을 사용하면 각 컨슈머가 처리하는 파티션에서 메시지 순서를 보장하면서도, 여러 컨슈머가 동시에 다른 파티션을 처리하므로 순서 보장과 병렬 처리 두 가지를 동시에 만족할 수 있다.
예를 들어, 중요한 이벤트를 순차적으로 처리해야 하는 경우, 동일한 파티션에서 메시지를 처리하는 컨슈머를 통해 순서를 보장하면서도 병렬 처리의 이점을 취할 수 있다.</p>
</li>
<li><p><strong><code>스케일링과 고가용성 (Scaling and High Availability)</code></strong>:
컨슈머 그룹은 카프카의 고가용성과 수평 확장성을 지원한다. 컨슈머 그룹에 컨슈머를 추가하면, 더 많은 파티션을 처리할 수 있어 시스템의 성능과 처리 용량을 증가시킬 수 있다.
또한, 컨슈머 그룹은 장애가 발생했을 때 다른 컨슈머가 장애를 자동으로 대체할 수 있어 시스템의 고가용성을 유지할 수 있다.</p>
</li>
</ol>
<br>

<p><strong>결론적으로 카프카에서 컨슈머 그룹을 사용하는 이유는 <code>부하 분산</code>, <code>병렬 처리</code>, <code>확장성</code>, <code>장애 복구</code>, <code>메시지 중복 방지</code> 등을 통해 시스템의 성능과 안정성을 극대화하기 위해서이다.</strong>
특히, 대규모 분산 시스템에서 많은 데이터를 효율적으로 처리하고, 여러 개의 컨슈머가 협력하여 토픽에서 메시지를 처리하는 방식으로, 성능과 안정성을 크게 향상시킬 수 있다.</p>
<br>
<br>



<h1 id="특정-브로커-장애-발생-시">특정 브로커 장애 발생 시</h1>
<p>Kafka에서 특정 브로커가 장애를 일으키면 다음과 같은 단계로 복구 및 재분배가 이루어진다.</p>
<br>

<ol>
<li><p><strong>파티션 리더 이동</strong>:
장애가 발생한 브로커가 특정 파티션의 리더로 지정된 상태였다면, 주키퍼(ZooKeeper) 또는 카프카의 내부 컨트롤러가 자동으로 이 파티션의 리더 역할을 다른 팔로워 브로커로 이동시킨다. 팔로워 브로커 중 하나가 새 리더로 지정되어 클라이언트 요청을 계속 처리할 수 있게 한다.</p>
</li>
<li><p><strong>장애 복구와 데이터 일관성 유지</strong>:
장애가 난 브로커가 복구되어 다시 클러스터에 참여하면, 카프카는 복제 데이터를 동기화하여 손실된 데이터가 없도록 한다. 이 과정에서 복제본의 오프셋 차이를 비교하고, 필요한 데이터를 새로 복제하여 동기화한다.</p>
</li>
<li><p><strong>가용성 보장</strong>:
카프카는 기본적으로 <strong>복제본을 활용한 고가용성(HA)</strong>을 제공한다. 따라서, 장애가 발생해도 클러스터 내 다른 복제본들이 동일한 데이터를 유지하고 있기에 서비스가 중단되지 않도록 설계되어 있다.</p>
</li>
</ol>
<ol start="4">
<li><strong>복구 실패 시 알림</strong>:
만약 장애가 발생한 브로커가 오랜 시간 동안 복구되지 않는다면, 카프카는 관리 콘솔이나 설정된 알림을 통해 이를 관리자에게 통보한다. 이를 통해 관리자는 브로커를 수동으로 점검하고 조치를 취할 수 있다.</li>
</ol>
<br>

<p>Kafka는 이러한 방식으로 장애 상황에서도 데이터 일관성을 유지하고, 서비스의 지속성을 보장하여 안정적인 메시징 서비스를 제공한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MOM: 메세지 큐(Message Queue)와 브로커(Broker)]]></title>
            <link>https://velog.io/@kim_dg/MOM-%EB%A9%94%EC%84%B8%EC%A7%80-%ED%81%90Message-Queue%EC%99%80-%EB%B8%8C%EB%A1%9C%EC%BB%A4Broker</link>
            <guid>https://velog.io/@kim_dg/MOM-%EB%A9%94%EC%84%B8%EC%A7%80-%ED%81%90Message-Queue%EC%99%80-%EB%B8%8C%EB%A1%9C%EC%BB%A4Broker</guid>
            <pubDate>Thu, 07 Nov 2024 08:03:02 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kim_dg/post/ecab1562-a148-4344-acea-f79bde88c2b9/image.png" alt=""></p>
<p>이번 포스팅에서는 데이터 전송을 비동기적으로 처리하고, 서버 간 데이터를 전달할 때 효율성을 높일 수 있다는 장점 덕분에
분산 시스템이나 MSA(마이크로서비스 아키텍쳐)에서 서비스간의 통신을 원활히 처리하기 위해 사용되는 메시지 큐(Message Queue)와 브로커(Broker)에 대해 알아보자. </p>
<br>

<h1 id="mom메세지-지향-미들웨어-message-oriented-middleware">MOM(메세지 지향 미들웨어, Message-Oriented Middleware)</h1>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/40f63ed5-b6bc-4711-8c4b-aa580be35713/image.png" alt=""></p>
<blockquote>
<p><strong><span style = "color:red;"><code>MOM(Message-Oriented Middleware)</code></span>은 분산 시스템 내에서 서로 다른 애플리케이션 간에 메시지를 비동기적으로 주고받을 수 있게 하는 미들웨어이다. 애플리케이션 간의 데이터 전송을 돕고 통신을 중재하는 역할을 하며, <code>메시지 큐</code>, <code>메시지 브로커</code>, <code>이벤트 브로커</code>와 같은 다양한 형태로 제공된다. MOM은 특히 비동기적 처리와 비동기적 통신이 필요한 경우에 효과적이다.</strong></p>
</blockquote>
<p>※ <strong><code>비동기</code>는 어떤 작업이 완료될 때까지 기다리지 않고, 그 작업이 끝나면 나중에 결과를 받는 방식이다. 특정 작업을 실행하더라도 기다리지 않고 그동안 다른 일을 계속할 수 있도록 하는 것이 비동기의 핵심이다.</strong></p>
<br>

<h2 id="mom의-특징">MOM의 특징</h2>
<ul>
<li><strong>비동기 통신</strong>: 송신자(Producer)가 메시지를 전송해도 수신자(Consumer)는 즉시 메세지를 받아 처리한 결과를 응답하지 않아도 된다. 송신자는 자신의 작업을 계속 진행할 수 있으며, 수신자는 자신이 준비됐을 때 메시지를 처리할 수 있다.</li>
</ul>
<ul>
<li><strong>내결함성 및 신뢰성</strong>: MOM 시스템은 메시지를 안전하게 보관하고, 네트워크 장애 등으로 인해 메시지가 유실되지 않도록 한다. 메시지 브로커, 이벤트 브로커는 메시지 재전송, 메시지 보관, 중복 방지 등의 기능을 제공한다.</li>
</ul>
<ul>
<li><p><strong>확장성</strong>: 여러 서비스가 동시에 연결되어도 쉽게 확장할 수 있으며, 메시지 전달의 병렬 처리가 가능하여 이를 통해 대규모 트래픽을 효율적으로 처리할 수 있다.</p>
</li>
<li><p><strong>분산 시스템 통합</strong>: MOM은 서로 다른 기술 스택을 사용하거나 분산된 환경에 있는 시스템들을 연결하는 데 사용되어 서로 다른 언어나 플랫폼으로 작성된 애플리케이션들 간에도 메시지 전달을 가능하게 해 분산되어 있는 시스템 상호작용이 가능하게 한다.</p>
</li>
</ul>
<br>

<h2 id="mom의-주요-구성-요소">MOM의 주요 구성 요소</h2>
<ul>
<li><p><strong><code>메세지 큐</code></strong></p>
</li>
<li><p><strong><code>브로커</code></strong></p>
</li>
</ul>
<br>



<h1 id="메세지-큐message-queue란">메세지 큐(Message Queue)란???</h1>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/3527c355-f321-4f32-8089-99eb31c8fa39/image.png" alt=""></p>
<blockquote>
<p><strong><span style = "color:red;"><code>메세지 큐</code></span>: 메시지 큐는 데이터를 전송할 때 <code>Producer(송신자)</code>와 <code>Consumer(수신자)</code>가 직접적으로 연결되지 않고 Producer에서 전송된 데이터가 저장되는 <code>큐(Queue)</code> 형태의 Producer와 Consumer 사이의 메세지 전달 매개체이다. 
Producer가 메세지 큐에 데이터를 넣어두면 Consumer가 데이터 처리가 가능할 때 메세지 큐에서 데이터를 꺼내는 방식이기 때문에 송신자와 수신자가 서로 독립적으로 작업할 수 있게 해준다.</strong></p>
</blockquote>
<p>개인적으로 메세지 큐를 현실의 사례에 쉽게 비유하자면 &#39;우체통&#39;이라고 생각하면 될 것 같다. 고객이 보내고 싶은 우편물을 직접 상대방에게 보내는 것이 아니라 우체통에 넣어 놓으면 우체국 측에서 차후에 우체통에서 해당 우편물을 수거하여 우편물을 보내주는 역할을 하는 것처럼 데이터를 보내주는 <code>송신자(Producer)</code> 역할을 하는 서비스가 전달하고 싶은 데이터를 메세지 큐에 저장하면 데이터를 받아 사용해야 하는 <code>수신자(Consumer)</code> 역할의 서비스가 메세지 큐에서 데이터를 꺼내어 사용하는 구조이다.     </p>
<p>아주 간단하게 말하자면 메시지 큐는 단순히 메시지(데이터)를 줄 세워서 대기시키는 큐 형태의 자료구조라고 이해하면 된다.</p>
<br>

<h2 id="메세지-큐-주요-구성-요소">메세지 큐 주요 구성 요소</h2>
<ul>
<li><p><code>큐(Queue)</code>: 송신자가 보낸 데이터(메시지)가 순서대로 저장되는 데이터 구조로 큐에 저장된 메시지는 일반적으로 FIFO(First In, First Out) 방식으로 처리된다.</p>
</li>
<li><p><code>메시지(Message)</code>: 송신자가 보내는 단위 데이터이다. 메시지에는 수신자가 알아야 할 정보를 포함하며, 주로 JSON, XML 등의 형태로 저장된다.</p>
</li>
<li><p><code>송신자(Producer)</code>: 메시지를 생성하여 큐에 보내는 주체이다.</p>
</li>
<li><p><code>수신자(Consumer)</code>: 큐에서 메시지를 꺼내어 처리하는 주체이다.</p>
</li>
</ul>
<br>

<h2 id="메세지-큐의-특징">메세지 큐의 특징</h2>
<ul>
<li><p><strong>비동기 처리</strong>: 메시지 큐에 메시지를 저장한 뒤 송신자는 그 이후의 과정은 아예 신경 쓰지 않고 작업을 완료했다고 간주할 수 있으며, 수신자는 언제든지 처리가 가능할 때 큐에서 메시지를 꺼내 처리할 수 있어 비동기적 처리가 쉽다.</p>
</li>
<li><p><strong>부하 분산</strong>: 수신자가 여러 개일 때 메시지 큐는 부하를 분산하여, 각 수신자가 부담을 적게 받고 작업할 수 있다.</p>
</li>
<li><p><strong>서비스 간 결합도 낮추기</strong>: 송신자와 수신자가 직접 연결되지 않기 때문에 시스템 간 결합도와 종속성이 낮아지고, 수신자가 나중에 추가되거나 변경되더라도 송신자는 수정할 필요가 없다.</p>
</li>
<li><p><strong>데이터 유실 방지</strong>: 송신자가 데이터를 보내도 수신자가 처리할 준비가 안 되어 있을 때 메시지 큐에 저장해 두면 데이터 유실을 방지할 수 있다.</p>
</li>
</ul>
<br>

<h2 id="메세지-큐의-예시">메세지 큐의 예시</h2>
<ul>
<li><code>RabbitMQ의 Queue</code></li>
<li><code>Kafka의 topic</code></li>
<li><code>ActiveMQ의 Queue</code></li>
</ul>
<br>


<h3 id="메시지-큐의-활용-예시">메시지 큐의 활용 예시</h3>
<ul>
<li>주문 처리 시스템에서 주문 접수, 결제, 배송 등의 작업을 각각 별도의 서비스에서 처리할 때</li>
<li>이메일 발송 서비스에서 대량의 메일을 보낼 때 메일 발송 요청을 큐에 저장하여 비동기적으로 처리할 때</li>
</ul>
<br>
<br>

<h1 id="브로커broker">브로커(Broker)</h1>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/cac65ef0-d8ff-4235-b37f-9bff51778fec/image.png" alt=""></p>
<blockquote>
<p><strong><span style = "color:red;"><code>브로커(Broker)</code></span>: 메시지 브로커(Message Broker)는 메세지 큐와 같은 채널의 상위 개념이자 Producer와 Consumer 사이에서 <span style = "color:red;"><code>메시지 전송 및 관리를 담당하는 중개 시스템</code></span>으로, 브로커는 데이터 전송의 안정성과 효율성을 높이기 위해 여러 수신자에게 메시지를 전달하거나 특정 조건에 따라 메시지를 필터링 및 분배하는 기능을 제공한다.
메시지 브로커는 큐뿐만 아니라, 게시-구독(Pub/Sub) 같은 다양한 통신 패턴을 지원하여 메시지를 분배하거나 필요한 수신자에게만 전달하는 기능도 포함한다.</strong></p>
</blockquote>
<br>

<h2 id="브로커의-주요-역할">브로커의 주요 역할</h2>
<ul>
<li><p><strong>메시지 중계 및 전달</strong>: 브로커는 클라이언트나 서버 간의 메시지를 중계하고, 메시지가 올바른 수신자에게 전달되도록 한다.</p>
</li>
<li><p><strong>메시지 저장 및 관리</strong>: 메시지를 일시적으로 저장하고, 큐에서 메시지를 꺼내서 처리하거나 전달할 수 있도록 관리한다. 예를 들어, 메시지 큐 시스템에서는 메시지를 보관한 후 소비자가 이를 처리할 때까지 기다린다.</p>
</li>
<li><p><strong>메시지 라우팅</strong>: 메시지를 어떤 경로로 전달할지를 결정하는 라우팅 역할을 한다. 
예를 들어, RabbitMQ나 ActiveMQ는 각기 다른 큐로 메시지를 라우팅할 수 있다.</p>
</li>
</ul>
<br>

<h2 id="브로커의-종류">브로커의 종류</h2>
<ul>
<li><p><code>RabbitMQ</code>: 메시지 큐 시스템의 일종으로, 메시지를 큐에 저장하고 이를 소비자가 가져가도록 중계한다.</p>
</li>
<li><p><code>Apache Kafka</code>: 대규모 분산 시스템에서 사용되는 메시지 브로커로, 데이터를 일관성 있게 저장하고 여러 소비자가 읽을 수 있도록 메시지를 분배한다.</p>
</li>
<li><p><code>ActiveMQ</code>: Java 기반의 메시지 브로커로, 다양한 메시징 프로토콜을 지원하고 큐를 사용하여 메시지를 전달한다.</p>
</li>
<li><p><code>Amazon SQS (Simple Queue Service)</code>: AWS에서 제공하는 메시지 큐 서비스로, 메시지 브로커 역할을 하며 확장성 있는 메시징 시스템을 제공한다.</p>
</li>
</ul>
<br>

<h1 id="메세지-큐와-브로커를-사용하는-이유">메세지 큐와 브로커를 사용하는 이유</h1>
<p>하지만 여기서 한 가지 의문점이 생길 수 있다. 전통적인 방식대로 데이터를 생성한 서비스에서 각각 데이터를 활용하는 서비스들의 API를 호출해주면 되는 거 아닌가?</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/bbed5b74-27ac-4e70-a3d6-be477bd59527/image.png" alt=""></p>
<p>하지만 이러한 API 호출은 몇 가지 문제점을 가지고 있다. 
만약 고객이 주문을 하였는데 이 방식은 <code>동기</code>적으로 처리되기 때문에 속도도 느리고 만약 처리 중간에 서버 오류로 인하여 데이터가 유실되며 그 이후의 서비스들은 스스로 진행이 불가능하다.</p>
<p>현재 구조에서는 주문 서비스가 너무 많은 역할을 하고 있고 이로 인하여 주문 서비스에 모든 서비스가 의존적이라서 주문 서비스의 장애는 곧 모든 서비스의 장애로 이루어진다는 문제점이 존재한다.
<br></p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/93c3a49c-2662-4d1b-b029-e12e2172dd7f/image.png" alt=""></p>
<p>하지만 Kafka, RabbitMQ 등의 브로커를 도입한다면 주문 서비스에서 주문이 생성되어 이벤트가 발생하면 각 서비스에서 앞 순서의 서비스를 기다리지 않고, 다른 서비스의 장애에 영향을 받지도 않고 각각 데이터를 꺼내가 <code>비동기</code>적으로 각 서비스를 수행할 수 있다.</p>
<br>


<p><img src="https://velog.velcdn.com/images/kim_dg/post/6ab47359-d639-47c2-ad2b-990b6891e5b5/image.png" alt=""></p>
<p>이러한 방식에는 각 서비스의 책임은 각 서비스들 본인들만 진다는 특징이 존재한다. 
서비스 A가 자신의 서비스를 수행하고 카프카같은 브로커에게 데이터를 넘기기만 하면 데이터를 다른 서비스에 전달하는 일은 브로커가 알아서 해주기 때문에 그 이후의 일은 서비스 A는 알 필요도, 신경쓸 필요도 없다.</p>
<br>


<p>API 통신 대신 메시지 큐(MQ)나 메시지 브로커를 사용하는 이유를 정리하자면 다음과 같다.</p>
<ol>
<li><p><strong><code>비동기 처리 지원</code></strong>
API 통신은 동기 방식으로, 한 서비스가 요청을 보내면 상대방이 응답을 보낼 때까지 기다려야 한다.
반면, 메시지 큐나 메시지 브로커를 통해 메시지를 전달하면 비동기 방식으로 처리할 수 있다. 요청을 보내는 서비스는 메시지를 브로커에 넣고 바로 다른 작업을 할 수 있다. 상대방이 이 메시지를 나중에 꺼내 처리하더라도 원활히 작동한다.</p>
</li>
<li><p><strong><code>확장성과 부하 분산</code></strong>
메시지 큐는 여러 개의 소비자(컨슈머)를 통해 하나의 작업을 동시에 처리할 수 있는 구조를 갖추고 있어, 확장이 용이하다.
예를 들어, 이벤트 처리와 같은 고부하 작업이 발생할 때 컨슈머 수를 늘려 부하를 분산할 수 있다. API 통신 방식은 각 서비스가 동시에 처리할 수 있는 요청 수에 제약이 있을 수 있어 부하 분산이 어렵다.</p>
</li>
<li><p><strong><code>서비스 간 독립성 유지로 장애 전파 최소화</code></strong>
메시지 큐는 서비스 간 결합도를 낮춰 서로 독립적으로 동작할 수 있도록 한다.
만약 서비스 A가 서비스 B와 API 통신을 통해 상호작용할 경우, 서비스 B가 중단되면 A도 기능에 문제가 생길 수 있다. 하지만 메시지 브로커를 사용하면, 서비스 B가 일시적으로 중단되어도 A는 메시지를 브로커에 넣고 계속 운영할 수 있으며, B가 복구되면 이어서 메시지를 꺼내어 처리할 수 있다.</p>
</li>
<li><p><strong><code>다양한 수신자에게 동시에 전달 가능</code></strong>
메시지 브로커를 사용하면 하나의 메시지를 여러 수신자가 받아서 처리할 수 있다.
예를 들어, Kafka의 경우 특정 이벤트를 여러 서비스가 동시에 구독하여 처리할 수 있어, 로그 분석, 알림, 실시간 통계 등의 기능을 동시에 구현할 수 있다. 반면 API 통신 방식은 일대일 방식으로 주로 사용되어 여러 수신자에게 같은 메시지를 전송하려면 요청을 반복해야 한다.</p>
</li>
<li><p><strong><code>재처리 및 메시지 보관</code></strong>
Kafka와 같은 메시지 브로커는 메시지를 오래 보관하고, 특정 시점으로 돌아가 다시 소비할 수 있는 기능을 제공한다.
이를 통해 오류가 발생했을 때 메시지를 다시 가져와 재처리하거나, 특정 시점 이후의 메시지를 새로 추가된 서비스에 처리하게 할 수 있다. 반면 API 통신은 이런 기능을 기본적으로 지원하지 않기 때문에, 이와 같은 기능을 구현하려면 추가적인 개발이 필요하다.</p>
</li>
</ol>
<br>

<p><strong>이와 같이 브로커를 사용하면 API 통신의 동기적 제약과 결합성 문제를 해결하면서도 확장성과 장애 복구 능력이 뛰어난 시스템을 구축할 수 있다는 장점이 존재한다.</strong></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[내일배움캠프 최종 프로젝트 - 프로젝트 기획(1주차)]]></title>
            <link>https://velog.io/@kim_dg/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B8%B0%ED%9A%8D1%EC%A3%BC%EC%B0%A8</link>
            <guid>https://velog.io/@kim_dg/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-%EC%B5%9C%EC%A2%85-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B8%B0%ED%9A%8D1%EC%A3%BC%EC%B0%A8</guid>
            <pubDate>Sun, 27 Oct 2024 13:45:11 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kim_dg/post/05b9a8b7-d2db-4843-b97c-c95424dd90d3/image.png" alt=""></p>
<h1 id="최종-프로젝트-개막">최종 프로젝트 개막</h1>
<p>내일배움캠프 Spring 6기의 최종 프로젝트가 요번 주에 시작되었다.
우리 팀은 팀장인 나를 포함하여 다른 팀보다 1, 2명 많은 6명으로 구성되었다. 팀원 중 한 분이 요번 주 토요일에 결혼하시고 2주 동안 신혼여행을 가셨기 때문에 실질적으로 다른 팀과 인원 차이는 크게 다르지 않기는 하다.</p>
<br>
<br>

<h1 id="주제-선정">주제 선정</h1>
<blockquote>
</blockquote>
<ul>
<li><strong>프로젝트 주제: 우리 지금 만나(아 당장 만나)</strong><br><br></li>
<li><strong>프로젝트 요약: 프로젝트 한줄 요약 사용자가 쉽고 안전하게 소모임을 생성하고, 활발한 커뮤니티를 형성할 수 있도록 직관적인 사용자 경험과 신뢰 기반의 환경을 제공</strong><blockquote>
</blockquote>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/dd063331-440b-498e-ad0f-784c3b09e6b6/image.png" alt=""></p>
<p>같은 동네주민들끼리 특정 목적을 위해 소모임을 만들어 모이고 그 안에서 멤버들끼리 또 일정을 만들어 같이 만나 특정 활동을 공유할 수 있는 <code>당근마켓의 동네생활의 모임</code> 에서 아이디어를 많이 얻었다.</p>
<br>
<br>

<h1 id="프로젝트-기획">프로젝트 기획</h1>
<h2 id="개요">개요</h2>
<ul>
<li>개인주의가 많아진 현대사회에서, 각자의 취미생활을 공유할 수 있는 그룹을 찾기위한 소모임 개최 및 참여형 애플리케이션입니다.</li>
<li>사용자가 다양한 활동(운동, 공부, 식사, 취미 등)을 함께할 사람들과 소모임을 쉽게 구성하고 그 안에서 일정을 함께할 수 있도록 도와주는 웹 기반 플랫폼입니다.</li>
</ul>
<h2 id="배경">배경</h2>
<ul>
<li>현대 사회에서는 개인의 관심사나 취미를 공유할 수 있는 소모임을 찾는 것이 중요한 요소로 부상하고 있지만, 현실적으로 관심사나 위치가 비슷한 사람들과 함께 모일 수 있는 플랫폼은 제한적입니다.</li>
<li>이 서비스는 다양한 활동을 매개로 사람들을 연결하여, 이들이 손쉽게 모임을 만들고 참여할 수 있는 환경을 제공하는 데 목적을 둡니다.</li>
</ul>
<h2 id="필수-기능">필수 기능</h2>
<blockquote>
</blockquote>
<ul>
<li><strong>회원 가입 및 로그인<br><br></strong></li>
<li><strong>프로필 설정 : 사용자 관심사, 지역, MBTI, 이미지 등 프로필 정도 설정 및 수정<br><br></strong></li>
<li><strong>소모임 생성 : 사용자가 주제를 설정하고 소모임을 직접 생성할 수 있는 기능<br><br></strong></li>
<li><strong>소모임 참여 : 사용자가 소모임에 신청 및 참여할 수 있는 기능</strong></li>
<li><ul>
<li><strong>소모임 참여 신청 수락, 거절<br><br></strong></li>
</ul>
</li>
<li><strong>일정 생성</strong></li>
<li><ul>
<li><strong>일정 참여 신청 수락, 거절<br><br></strong></li>
</ul>
</li>
<li><strong>강제퇴장</strong></li>
</ul>
<h2 id="핵심-기능">핵심 기능</h2>
<blockquote>
</blockquote>
<ul>
<li>소모임 검색(인덱싱)<ul>
<li>인덱싱 : 검색 시 성능을 향상하기 위해 소모임의 주요 검색 필드(제목, 주제, 지역 등)에 대해 데이터베이스 인덱스를 적용.</li>
<li>동시성 제어 : 여러 사용자가 동시에 검색할 때, 캐시를 사용하여 데이터베이스 접근을 줄여 동시성 문제를 해결<br><br></li>
</ul>
</li>
<li>소모임 관리<ul>
<li>캐싱 : 자주 접근하는 소모임 정보(멤버 리스트, 게시글 목록 등)는 캐시에 저장하여 빠르게 조회 가능하도록 설정.<br><br></li>
</ul>
</li>
<li>소모임 매칭(메시지(이벤트) 브로커, 동시성 제어)<ul>
<li>캐싱 : 소모임 매칭 작업에서 추천 소모임 목록을 캐시에 저장하여 매칭 속도를 개선.</li>
<li>인덱싱 : 매칭 알고리즘에서 사용하는 주요 필드(사용자의 관심사, 지역 등)를 인덱싱하여 매칭 결과를 빠르게 제공.</li>
<li>매칭 요청이 다수 발생할 경우 큐 기반 처리를 통해 동시성을 관리하고, 여러 매칭 작업이 동시에 일어나더라도 서버 부하를 줄이며 안정적으로 처리할 수 있음.</li>
</ul>
</li>
</ul>
<h2 id="워크로드-분산-고려-사항">워크로드 분산 고려 사항</h2>
<blockquote>
</blockquote>
<ol>
<li>캐싱 시스템 도입<ol>
<li>자주 조회되거나 수정이 적은 데이터(소모임 리스트, 인기 소모임, 매칭 결과 등)를 캐시하여, 데이터베이스 부하를 줄이고 빠른 응답을 제공<br><br></li>
</ol>
</li>
<li>데이터베이스 인덱싱<ol>
<li>검색, 매칭, 관리 작업에서 성능을 개선하기 위해 소모임의 <code>title</code>, <code>location</code> 같은 필드에 인덱싱을 적용하여 데이터베이스 조회 성능을 최적화.<br><br></li>
</ol>
</li>
<li>동시성 제어 및 락킹 전략<ol>
<li>여러 사용자가 동시에 소모임 정보나 매칭 데이터를 수정할 경우 충돌을 방지하고, 중요한 수정 작업이 일어날 때는 데이터의 일관성을 보장할 수 있도록 락킹 메커니즘을 적용.<br><br></li>
</ol>
</li>
<li>메세지 큐<ol>
<li>RabbitMQ, Kafka 등의 메시지 큐 시스템</li>
<li>매칭 작업이나 대규모 데이터 처리가 필요한 경우 메시지 큐를 사용해 요청을 비동기적으로 처리하고 서버 부하를 분산하며 락킹 전략으로 동시성 문제를 해결</li>
<li>여러 사용자가 동시에 매칭 요청을 보낼 때 성능 저하를 방지할 수 있음.</li>
</ol>
</li>
</ol>
<h2 id="추가했으면-하는-기능">추가했으면 하는 기능</h2>
<blockquote>
</blockquote>
<ul>
<li>광고(수익 창출)</li>
<li>광고 제거(구독제: 수익 창출)</li>
<li>매칭 시스템(일대일 또는 다대다)(1순위)</li>
<li>날씨에 따라 추천되는 모임(노출도)</li>
<li>클래스(수업을 원하는 유저가 제안서를 업로드하면 튜터가 수업받고 싶은 유저를 선택해 매칭되는 방식)</li>
</ul>
<br>
<br>

<h1 id="와이어-프레임-구상">와이어 프레임 구상</h1>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/24b03182-c1e8-4a50-a42f-c1b9eb38eb9c/image.png" alt=""></p>
<br>
<br>


<h1 id="erd-구상">ERD 구상</h1>
<p>ERD는 현재 필수 기능과 관련된 테이블만 추가되어 있어서 차후 프로젝트를 개발해나가면서 더 추가될 예정이다.</p>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/c4c6bb97-f91b-4c77-8a23-6caf139df253/image.png" alt=""></p>
<p>(크기가 작아서 잘 안 보일시 마우스 왼쪽 클릭 후 &#39;새 탭에서 이미지 보기&#39;로 확대하여 보시기를 권장합니다.)</p>
<br>
<br>


<h1 id="api-명세서-일부-예시">API 명세서 일부 예시</h1>
<p><img src="https://velog.velcdn.com/images/kim_dg/post/393077e8-d5da-4d65-9f9a-88f541071c6b/image.png" alt=""></p>
<br>
<br>



]]></description>
        </item>
        <item>
            <title><![CDATA[내일배움캠프 Trello 프로젝트 - KPT 회고]]></title>
            <link>https://velog.io/@kim_dg/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-Trello-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-KPT-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@kim_dg/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-Trello-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-KPT-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Fri, 18 Oct 2024 12:13:30 GMT</pubDate>
            <description><![CDATA[<h1 id="keep---현재-만족하고-있는-부분">KEEP - 현재 만족하고 있는 부분</h1>
<ul>
<li><p>개발 속도가 꽤 빨랐던 게 만족스러운 부분이었던 것 같습니다.</p>
</li>
<li><p>각자에게 문제가 생겼을 시 문제 공유가 잘 이루어지고 화면 공유, 적극적인 의견 표현 등 트러블 슈팅이 신속하게 이루어졌더 부분이 좋았습니다.</p>
</li>
<li><p>각 부문의 기획 조건에 맞게 발생할 수 있는 다양한 예외 상황을 고려한 꼼꼼한 예외 처리가 이루어져 만족스러웠습니다.</p>
</li>
<li><p>Redis, Github Action, Docker Compose 등 새로운 기술 스택을 적용해볼 수 있는 기회가 되어 좋았습니다.</p>
</li>
<li><p>예외 처리, 알림 수신 등의 부분에 대해 적극적으로 AOP를 적용해볼 수 있었던 점이 의미 있었습니다.</p>
</li>
<li><p>GitHub 관련 규칙 등이 잘 이루어지고 커밋 메시지가 직관적이어서 팀원간의 협업이 수월하였습니다.</p>
</li>
<li><p>팀원 모두가 리팩토링을 통해 코드 컨벤션을 지키고 객체지향적이고 고도화된 코드를 짜기 위해 노력하는 모습이 보기 좋았습니다.</p>
</li>
</ul>
<br>
<br>
<br>

<h1 id="problem---현재-만족하고-있는-부분">Problem - 현재 만족하고 있는 부분</h1>
<ul>
<li><p>기초 설계가 다소 미흡했던 부분이 있어 개발 도중에 변경 상황이 여러 번 발생하여 아쉬웠습니다.</p>
</li>
<li><p>팀원간의 커뮤니케이션에 다소 부족했던 부분이 있는 것 같아서 아쉬웠습니다.</p>
</li>
<li><p>권한 관련 유효성 검증 리팩도링 도중에 실수가 발생했던 점이 불편하였습니다.</p>
</li>
<li><p>각 도메인이 복잡하게 얽혀 있다 보니까 전부 병합했을 때 완벽하게 유기적으로 작동하지는 않았다는 점이 아쉬웠습니다.</p>
</li>
<li><p>각박한 스케줄로 인하여 코드 병합시 팀원간의 코드 리뷰가 거의 부재했던 점이 문제였던 것 같습니다.</p>
</li>
<li><p>프로젝트기간 동안 TIL작성에 조금 소홀해졌습니다.</p>
</li>
</ul>
<br>
<br>
<br>


<h1 id="try---problem에-대한-해결책-당장-실행-가능한-것">Try - Problem에 대한 해결책, 당장 실행 가능한 것</h1>
<ul>
<li><p>전체적인 서비스 관점에서 기획 요구 조건들을 확실히 파악하고 탄탄한 기초 설계를 작성하고 시작해야만 개발 과정이 한층 수월해질 것 같습니다.</p>
</li>
<li><p>기획 단계에 시간을 더 투자해서라도  탄탄하고 면밀한 설계를 하는 것이 중요할 것 같습니다</p>
</li>
<li><p>서로간의 적극적인 코드 리뷰와 코드 공유를 통하여 더욱 품질 높은 코드를 작성하는 것이 좋은 방법이 될 수 있을 것 같습니다.</p>
</li>
<li><p>특정 기능을 구현할 때 요구 조건을 더욱 면밀히 파악하고 기능을 구현하여  불필요한 코드 수정을 최대한 방지하기 위해 노력해야 합니다.</p>
</li>
<li><p>개발 일정이 짧다면 설계 단계에서도 짧은 개발 기간을 고려하여 설계 단계에서 이를 염두하고 설계를 진행 해야할 것 같습니다.</p>
</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>